diff --git a/example/lib/home.dart b/example/lib/home.dart index 02538b11..85fb9327 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -10,6 +10,7 @@ import 'package:zeta_example/pages/components/button_example.dart'; import 'package:zeta_example/pages/components/checkbox_example.dart'; import 'package:zeta_example/pages/components/chip_example.dart'; import 'package:zeta_example/pages/components/dialpad_example.dart'; +import 'package:zeta_example/pages/components/list_item_example.dart'; import 'package:zeta_example/pages/components/navigation_bar_example.dart'; import 'package:zeta_example/pages/theme/color_example.dart'; import 'package:zeta_example/pages/components/password_input_example.dart'; @@ -38,6 +39,7 @@ final List components = [ Component(ButtonExample.name, (context) => const ButtonExample()), Component(CheckBoxExample.name, (context) => const CheckBoxExample()), Component(ChipExample.name, (context) => const ChipExample()), + Component(ListItemExample.name, (context) => const ListItemExample()), Component(NavigationBarExample.name, (context) => const NavigationBarExample()), Component(PasswordInputExample.name, (context) => const PasswordInputExample()), Component(ProgressExample.name, (context) => const ProgressExample()), diff --git a/example/lib/pages/components/list_item_example.dart b/example/lib/pages/components/list_item_example.dart new file mode 100644 index 00000000..9cc60696 --- /dev/null +++ b/example/lib/pages/components/list_item_example.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class ListItemExample extends StatefulWidget { + static const String name = 'ListItem'; + + const ListItemExample({super.key}); + + @override + State createState() => _ListItemExampleState(); +} + +class _ListItemExampleState extends State { + bool _isCheckBoxEnabled = false; + bool _isSelected = true; + + _onDefaultListItemTap() { + setState(() => _isCheckBoxEnabled = !_isCheckBoxEnabled); + } + + @override + Widget build(BuildContext context) { + final zetaColors = Zeta.of(context).colors; + + return ExampleScaffold( + name: ListItemExample.name, + child: Container( + color: zetaColors.surfaceSecondary, + child: SingleChildScrollView( + child: Column( + children: [ + // List Item with descriptor + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaListItem( + dense: true, + leading: Container( + width: 48, + height: 48, + decoration: BoxDecoration(borderRadius: ZetaRadius.rounded), + child: Placeholder(), + ), + subtitle: Text("Descriptor"), + title: Text("List Item"), + trailing: ZetaCheckbox( + value: _isCheckBoxEnabled, + onChanged: (_) => _onDefaultListItemTap(), + ), + onTap: _onDefaultListItemTap, + ), + ), + + // Enabled + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.l), + child: Text( + "Enabled", + style: ZetaTextStyles.titleLarge, + ), + ), + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.m), + child: ZetaListItem(title: Text("List Item")), + ), + + // Selected + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.l), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "Selected", + style: ZetaTextStyles.titleLarge, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.m), + child: ZetaListItem( + title: Text("List Item"), + selected: _isSelected, + trailing: _isSelected + ? Icon( + ZetaIcons.check_mark_sharp, + color: zetaColors.primary, + ) + : null, + onTap: () => setState(() => _isSelected = !_isSelected), + ), + ), + + // Disabled + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.l), + child: Text( + "Disabled", + style: ZetaTextStyles.titleLarge, + ), + ), + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.m), + child: ZetaListItem( + title: Text("List Item"), + enabled: false, + onTap: () {}, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/widgetbook/main.dart b/example/widgetbook/main.dart index c6d2620d..eb6dcac9 100644 --- a/example/widgetbook/main.dart +++ b/example/widgetbook/main.dart @@ -14,6 +14,7 @@ import 'pages/components/checkbox_widgetbook.dart'; import 'pages/components/chip_widgetbook.dart'; import 'pages/components/dial_pad_widgetbook.dart'; import 'pages/components/in_page_banner_widgetbook.dart'; +import 'pages/components/list_item_widgetbook.dart'; import 'pages/components/navigation_bar_widgetbook.dart'; import 'pages/components/password_input_widgetbook.dart'; import 'pages/components/progress_widgetbook.dart'; @@ -75,7 +76,12 @@ class HotReload extends StatelessWidget { ), WidgetbookUseCase(name: 'Password Input', builder: (context) => passwordInputUseCase(context)), WidgetbookUseCase(name: 'Content', builder: (context) => bottomSheetContentUseCase(context)), - WidgetbookUseCase(name: 'Dial Pad', builder: (context) => dialPadUseCase(context)), + WidgetbookUseCase( + name: 'Dial Pad', + builder: (context) => dialPadUseCase(context)), + WidgetbookUseCase( + name: 'List Item', + builder: (context) => listItemUseCase(context)), WidgetbookUseCase(name: 'Navigation Bar', builder: (context) => navigationBarUseCase(context)), WidgetbookComponent( name: 'Progress', diff --git a/example/widgetbook/pages/components/list_item_widgetbook.dart b/example/widgetbook/pages/components/list_item_widgetbook.dart new file mode 100644 index 00000000..40c403a1 --- /dev/null +++ b/example/widgetbook/pages/components/list_item_widgetbook.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +Widget listItemUseCase(BuildContext context) { + return WidgetbookTestWidget( + widget: StatefulBuilder( + builder: (context, setState) { + final subtitle = + context.knobs.stringOrNull(label: 'Descriptor', initialValue: null); + + final trailing = + context.knobs.boolean(label: 'Trailing', initialValue: false) + ? Checkbox(value: false, onChanged: (_) {}) + : null; + + final leading = + context.knobs.boolean(label: 'Leading', initialValue: false) + ? Container( + width: 48, + height: 48, + decoration: BoxDecoration(borderRadius: ZetaRadius.rounded), + child: Placeholder(), + ) + : null; + + return ZetaListItem( + dense: context.knobs.boolean(label: 'Dense', initialValue: false), + enabled: context.knobs.boolean(label: 'Enabled', initialValue: true), + enabledDivider: context.knobs.boolean( + label: 'Enabled Divider', + initialValue: true, + ), + selected: + context.knobs.boolean(label: 'Selected', initialValue: true), + leading: leading, + title: Text( + context.knobs.string(label: 'Title', initialValue: 'List Item'), + ), + subtitle: subtitle != null ? Text(subtitle) : null, + trailing: trailing, + onTap: () {}, + ); + }, + ), + ); +} diff --git a/lib/src/components/list_item/list_item.dart b/lib/src/components/list_item/list_item.dart new file mode 100644 index 00000000..e7d143ec --- /dev/null +++ b/lib/src/components/list_item/list_item.dart @@ -0,0 +1,213 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zeta_flutter.dart'; + +/// A single row that typically contains some text as well as a leading or trailing widgets. +class ZetaListItem extends StatelessWidget { + /// Creates a [ZetaListItem]. + const ZetaListItem({ + required this.title, + this.dense = false, + this.enabled = true, + this.enabledDivider = true, + this.leading, + this.onTap, + this.selected = false, + this.subtitle, + this.trailing, + super.key, + }); + + /// Dense list items have less space between widgets and use smaller [TextStyle] + final bool dense; + + /// Whether this [ZetaListItem] is interactive. + /// If false the [onTap] callback is inoperative. + final bool enabled; + + /// Whether to apply divider. Normally at the bottom of the [ZetaListItem]. + final bool enabledDivider; + + /// A Widget to display before the title; + final Widget? leading; + + /// Called when user taps on the [ZetaListItem]. + final VoidCallback? onTap; + + /// Applies selected styles. If selected is true trailing mu + final bool selected; + + /// Additional content displayed over the title. + /// Typically a [Text] widget. + final Widget? subtitle; + + /// The primary content of the [ZetaListItem]. + final Widget title; + + /// A widget to display after the title. + final Widget? trailing; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('dense', dense)) + ..add(DiagnosticsProperty('enabled', enabled)) + ..add(DiagnosticsProperty('enabledDivider', enabledDivider)) + ..add(DiagnosticsProperty('leading', leading)) + ..add(ObjectFlagProperty.has('onTap', onTap)) + ..add(DiagnosticsProperty('selected', selected)) + ..add(DiagnosticsProperty('subtitle', subtitle)) + ..add(DiagnosticsProperty('title', title)) + ..add(DiagnosticsProperty('trailing', trailing)); + } + + TextStyle get _titleTextStyle => + dense ? ZetaTextStyles.titleSmall : ZetaTextStyles.titleMedium; + + @override + Widget build(BuildContext context) { + final zetaColors = Zeta.of(context).colors; + + return _ListItemContainer( + enabled: enabled, + selected: selected, + onTap: onTap, + dense: dense, + enabledDivider: enabledDivider, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + children: [ + if (leading != null) + Padding( + padding: EdgeInsets.only( + right: dense ? ZetaSpacing.x2 : ZetaSpacing.x4, + ), + child: leading, + ), + Flexible( + child: Padding( + padding: EdgeInsets.symmetric( + vertical: dense ? 0.0 : ZetaSpacing.x4, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (subtitle != null) + DefaultTextStyle( + style: ZetaTextStyles.titleSmall.copyWith( + color: zetaColors.textSubtle, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: subtitle ?? const SizedBox(), + ), + DefaultTextStyle( + style: _titleTextStyle.copyWith( + color: enabled + ? zetaColors.textDefault + : zetaColors.textSubtle, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: title, + ), + ], + ), + ), + ), + ], + ), + ), + if (trailing != null) + Padding( + padding: EdgeInsets.only( + left: dense ? ZetaSpacing.x2 : ZetaSpacing.x4, + ), + child: trailing, + ), + if (trailing == null && selected && enabled) + Padding( + padding: EdgeInsets.only( + left: dense ? ZetaSpacing.x2 : ZetaSpacing.x4, + ), + child: Icon( + ZetaIcons.check_mark_round, + color: zetaColors.blue.shade60, + ), + ), + ], + ), + ); + } +} + +class _ListItemContainer extends StatelessWidget { + const _ListItemContainer({ + required this.child, + required this.dense, + required this.enabled, + required this.enabledDivider, + required this.onTap, + required this.selected, + }); + + final Widget child; + final bool dense; + final bool enabled; + final bool enabledDivider; + final VoidCallback? onTap; + final bool selected; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('child', child)) + ..add(DiagnosticsProperty('enabled', enabled)) + ..add(DiagnosticsProperty('selected', selected)) + ..add(ObjectFlagProperty.has('onTap', onTap)) + ..add(DiagnosticsProperty('dense', dense)) + ..add(DiagnosticsProperty('enabledDivider', enabledDivider)); + } + + @override + Widget build(BuildContext context) { + final zetaColors = Zeta.of(context).colors; + + return AbsorbPointer( + absorbing: !enabled, + child: Material( + color: enabled + ? selected + ? zetaColors.blue.shade10 + : zetaColors.white + : zetaColors.surfaceDisabled, + child: InkWell( + onTap: enabled ? onTap : null, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: dense ? ZetaSpacing.x4 : ZetaSpacing.x8, + vertical: dense ? ZetaSpacing.x2 : ZetaSpacing.x4, + ), + decoration: BoxDecoration( + border: enabled && enabledDivider + ? Border( + bottom: BorderSide( + color: selected + ? zetaColors.blue.shade40 + : zetaColors.borderDefault, + ), + ) + : null, + ), + child: child, + ), + ), + ), + ); + } +} diff --git a/lib/zeta_flutter.dart b/lib/zeta_flutter.dart index 75d941d8..964e8d14 100644 --- a/lib/zeta_flutter.dart +++ b/lib/zeta_flutter.dart @@ -23,6 +23,7 @@ export 'src/components/buttons/icon_button.dart'; export 'src/components/checkbox/checkbox.dart'; export 'src/components/chips/chip.dart'; export 'src/components/dial_pad/dial_pad.dart'; +export 'src/components/list_item/list_item.dart'; export 'src/components/navigation bar/navigation_bar.dart'; export 'src/components/password/password_input.dart'; export 'src/components/progress/progress_bar.dart';