From 0918445b9af55f0531ca1a8655f1c47e8d5e1e4f Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Mon, 19 Aug 2024 14:28:46 +0200 Subject: [PATCH 1/3] feat!: [DX-2054] Update NavListTile design --- optimus/lib/src/lists/nav_list_tile.dart | 311 +++++++----------- storybook/lib/stories/list/nav_list_tile.dart | 59 ++-- 2 files changed, 154 insertions(+), 216 deletions(-) diff --git a/optimus/lib/src/lists/nav_list_tile.dart b/optimus/lib/src/lists/nav_list_tile.dart index f8c4147f..36c3e1c2 100644 --- a/optimus/lib/src/lists/nav_list_tile.dart +++ b/optimus/lib/src/lists/nav_list_tile.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:optimus/optimus.dart'; +import 'package:optimus/src/common/gesture_wrapper.dart'; +import 'package:optimus/src/common/state_property.dart'; import 'package:optimus/src/lists/base_list_tile.dart'; -import 'package:optimus/src/typography/typography.dart'; /// Lists are vertically organized groups of data. Optimized for reading /// comprehension, a list consists of a single continuous column of rows, with @@ -15,229 +16,149 @@ import 'package:optimus/src/typography/typography.dart'; /// (such as supporting visuals and headlines) are placed in consistent /// locations across list items. It's not recommended to mix tiles with icon, /// avatar or without any leading widget in the same list. -class OptimusNavListTile extends StatelessWidget { +class OptimusNavListTile extends StatefulWidget { const OptimusNavListTile({ super.key, - required this.headline, - this.description, - this.leadingIcon, - this.leadingAvatar, - this.trailingIcon, - this.metadata, + required this.label, + this.leading, + this.rightDetail, + this.isToggleVisible = false, + this.isToggled = false, + this.isChevronVisible = false, + this.useHorizontalPadding = false, this.onTap, - this.fontVariant = FontVariant.normal, - this.tileSize = TileSize.normal, + this.isEnabled = true, + this.onTogglePressed, }); - /// Communicates the subject of the list item. - /// The primary content of the list item. - /// - /// Typically a [Text] widget. - final Widget headline; + /// The label of the list tile. + final Widget label; - /// Additional content displayed below the [headline]. - /// Can provide extra information needed for the user to make a choice. - /// - /// Typically a [Text] widget. - final Widget? description; + /// The leading widget of the list tile. + final Widget? leading; - /// Icons can help with scanning and speed up the user's decision. Remember - /// to use icons that can be easily recognized by the users. If - /// [leadingAvatar] is provided, the [leadingIcon] will be hidden. - final Widget? leadingIcon; - - /// An image that would be displayed on the leading position. Used for better - /// recognition. Will replace [leadingIcon] if provided. - final Widget? leadingAvatar; - - /// Additional cue to indicate the interactive character of the list item. - final Widget? trailingIcon; - - /// Can be used in addition to Additional Description, to communicate - /// meta-information about the list item, such as price, content count, or - /// other details. - final Widget? metadata; - - /// Action to be called on the tap gesture. + /// The callback that is called when the list tile is tapped. final VoidCallback? onTap; - /// Font variant, which will determine the text style. See [FontVariant] for - /// more details. - final FontVariant fontVariant; - - /// Depending on the screen size and list context you might need to use small - /// variant. Will be set to [TileSize.normal], if not provided. - /// - [TileSize.normal] - This variant should be used always when there is no - /// space constraint - /// - [TileSize.small] - Uses smaller font sizes and has less padding on top - /// and bottom than the default variant. This variant should only be used - /// when vertical space is scarce and showing more items on the list without - /// the need to scroll is important for the user's task completion. - final TileSize tileSize; - - double _getContentSpacing(OptimusTokens tokens) => switch (tileSize) { - TileSize.normal => tokens.spacing200, - TileSize.small => tokens.spacing100, - }; - - @override - Widget build(BuildContext context) { - final tokens = context.tokens; - final leadingIcon = this.leadingIcon; - final leadingAvatar = this.leadingAvatar; - - return BaseListTile( - onTap: onTap, - content: Padding( - padding: EdgeInsets.symmetric( - vertical: _getContentSpacing(tokens), - horizontal: tokens.spacing200, - ), - child: Row( - children: [ - if (leadingAvatar != null) _Avatar(avatar: leadingAvatar), - if (leadingAvatar == null && leadingIcon != null) - _LeadingIcon(icon: leadingIcon), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _Headline(fontVariant: fontVariant, headline: headline), - if (description case final description?) - _Description( - description: description, - fontVariant: fontVariant, - ), - ], - ), - ), - if (metadata case final metadata?) _Metadata(metadata: metadata), - if (trailingIcon case final trailingIcon?) - _TrailingIcon(icon: trailingIcon), - ], - ), - ), - ); - } -} - -class _Description extends StatelessWidget { - const _Description({ - required this.description, - required this.fontVariant, - }); + /// Whether to use horizontal padding. + final bool useHorizontalPadding; - final Widget description; - final FontVariant fontVariant; + /// Whether the toggle is visible. + final bool isToggleVisible; - @override - Widget build(BuildContext context) { - final tokens = context.tokens; + /// Whether the chevron is visible. + final bool isChevronVisible; - return Padding( - padding: EdgeInsets.only(right: tokens.spacing100), - child: OptimusTypography( - resolveStyle: (_) => tokens.bodyMediumStrong, - color: fontVariant.secondaryColor, - child: DefaultTextStyle.merge( - maxLines: 2, - overflow: TextOverflow.ellipsis, - child: description, - ), - ), - ); - } -} - -class _LeadingIcon extends StatelessWidget { - const _LeadingIcon({required this.icon}); - - final Widget icon; - - @override - Widget build(BuildContext context) { - final tokens = context.tokens; + /// The right detail widget of the list tile. + final Widget? rightDetail; - return Padding( - padding: EdgeInsets.only(right: tokens.spacing200), - child: IconTheme.merge( - data: IconThemeData(size: tokens.sizing300), - child: icon, - ), - ); - } -} + /// Whether the toggle is toggled. + final bool isToggled; -class _Avatar extends StatelessWidget { - const _Avatar({required this.avatar}); + /// The callback that is called when the toggle is pressed. + final ValueChanged? onTogglePressed; - final Widget avatar; + /// Whether the tile is enabled. + final bool isEnabled; @override - Widget build(BuildContext context) { - final tokens = context.tokens; - - return Padding( - padding: EdgeInsets.only(right: tokens.spacing200), - child: SizedBox(width: tokens.sizing500, child: avatar), - ); - } + State createState() => _OptimusNavListTileState(); } -class _Headline extends StatelessWidget { - const _Headline({required this.fontVariant, required this.headline}); - - final FontVariant fontVariant; - final Widget headline; +class _OptimusNavListTileState extends State + with ThemeGetter { + final WidgetStatesController _controller = WidgetStatesController(); @override - Widget build(BuildContext context) { - final tokens = context.tokens; - - return Padding( - padding: EdgeInsets.only(right: tokens.spacing100), - child: OptimusTypography( - resolveStyle: (_) => fontVariant.getPrimaryStyle(tokens), - child: DefaultTextStyle.merge( - maxLines: 1, - overflow: TextOverflow.ellipsis, - child: headline, - ), - ), - ); + void dispose() { + _controller.dispose(); + super.dispose(); } -} -class _Metadata extends StatelessWidget { - const _Metadata({required this.metadata}); - - final Widget metadata; - - @override - Widget build(BuildContext context) => OptimusTypography( - resolveStyle: (_) => context.tokens.bodySmallStrong, - color: OptimusTypographyColor.secondary, - child: metadata, + InteractiveStateColor get _backgroundColor => InteractiveStateColor( + defaultColor: tokens.backgroundInteractiveNeutralSubtleDefault, + disabled: Colors.transparent, + pressed: tokens.backgroundInteractiveNeutralSubtleActive, + hovered: tokens.backgroundInteractiveNeutralSubtleHover, ); -} - -class _TrailingIcon extends StatelessWidget { - const _TrailingIcon({required this.icon}); - - final Widget icon; @override Widget build(BuildContext context) { final tokens = context.tokens; - - return Padding( - padding: EdgeInsets.only(left: tokens.spacing200), - child: IconTheme.merge( - data: IconThemeData(size: tokens.sizing300), - child: icon, + final foregroundColor = + widget.isEnabled ? tokens.textStaticPrimary : tokens.textDisabled; + final iconTheme = IconThemeData(color: foregroundColor); + final contentPadding = EdgeInsets.only(right: tokens.spacing200); + + return IgnorePointer( + ignoring: !widget.isEnabled, + child: GestureWrapper( + onHoverChanged: (isHovered) => + setState(() => _controller.update(WidgetState.hovered, isHovered)), + onPressedChanged: (isPressed) => + setState(() => _controller.update(WidgetState.pressed, isPressed)), + child: DecoratedBox( + decoration: + BoxDecoration(color: _backgroundColor.resolve(_controller.value)), + child: BaseListTile( + onTap: widget.onTap, + content: Padding( + padding: EdgeInsets.symmetric( + horizontal: widget.useHorizontalPadding + ? tokens.spacing200 + : tokens.spacing0, + ), + child: Row( + children: [ + //leading + if (widget.leading case final leading?) + Padding( + padding: contentPadding, + child: DefaultTextStyle.merge( + style: TextStyle(color: foregroundColor), + child: IconTheme.merge(data: iconTheme, child: leading), + ), + ), + Expanded( + child: Padding( + padding: contentPadding, + child: DefaultTextStyle.merge( + child: widget.label, + style: tokens.bodyLarge.copyWith( + color: widget.isEnabled + ? tokens.textStaticPrimary + : tokens.textDisabled, + ), + ), + ), + ), + if (widget.rightDetail case final rightDetail?) + Padding( + padding: contentPadding, + child: + IconTheme.merge(data: iconTheme, child: rightDetail), + ), + if (widget.isToggleVisible) + Padding( + padding: contentPadding, + child: OptimusToggle( + onChanged: + widget.isEnabled ? widget.onTogglePressed : null, + isChecked: widget.isToggled, + ), + ), + if (widget.isChevronVisible) + Icon( + OptimusIcons.chevron_right, + color: foregroundColor, + size: tokens.sizing300, + ) + ], + ), + ), + ), + ), ), ); } } - -enum TileSize { normal, small } diff --git a/storybook/lib/stories/list/nav_list_tile.dart b/storybook/lib/stories/list/nav_list_tile.dart index ea0fbf9e..be403731 100644 --- a/storybook/lib/stories/list/nav_list_tile.dart +++ b/storybook/lib/stories/list/nav_list_tile.dart @@ -6,25 +6,37 @@ import 'package:storybook_flutter/storybook_flutter.dart'; final Story navListTileStory = Story( name: 'Data Display/List/Navigation List Tile', - builder: (context) { + builder: (context) => const _NavListExample(), +); + +class _NavListExample extends StatefulWidget { + const _NavListExample(); + + @override + State<_NavListExample> createState() => _NavListExampleState(); +} + +class _NavListExampleState extends State<_NavListExample> { + bool _isToggled = false; + + @override + Widget build(BuildContext context) { final k = context.knobs; - final headline = k.text(label: 'Headline', initial: 'Headline'); - final description = k.text(label: 'Description', initial: 'Description'); + final label = k.text(label: 'Label', initial: 'Label'); final leading = k.options( - label: 'Leading Icon', - initial: null, - options: exampleIcons, - ); - final trailing = k.options( - label: 'Trailing Icon', - initial: null, + label: 'Leading', + initial: OptimusIcons.magic, options: exampleIcons, ); - final fontVariant = k.options( - label: 'Font variant', - initial: FontVariant.normal, - options: FontVariant.values.toOptions(), + final rightDetail = + k.options(label: 'Right Detail', initial: null, options: exampleIcons); + final isToggleVisible = k.boolean( + label: 'Toggle', + initial: false, ); + final isChevronVisible = k.boolean(label: 'Chevron', initial: false); + final isEnabled = k.boolean(label: 'Enabled', initial: true); + final useHorizontalPadding = k.boolean(label: 'Use Padding', initial: true); return SingleChildScrollView( child: Center( @@ -32,11 +44,16 @@ final Story navListTileStory = Story( children: Iterable.generate(10) .map( (i) => OptimusNavListTile( - headline: Text(headline), - description: Text(description), - fontVariant: fontVariant, - leadingIcon: leading != null ? Icon(leading) : null, - trailingIcon: trailing != null ? Icon(trailing) : null, + label: Text(label), + rightDetail: rightDetail != null ? Icon(rightDetail) : null, + isChevronVisible: isChevronVisible, + isToggleVisible: isToggleVisible, + onTogglePressed: (isToggled) => + setState(() => _isToggled = isToggled), + isToggled: _isToggled, + isEnabled: isEnabled, + leading: leading != null ? Icon(leading) : null, + useHorizontalPadding: useHorizontalPadding, onTap: () {}, ), ) @@ -44,5 +61,5 @@ final Story navListTileStory = Story( ), ), ); - }, -); + } +} From 55045a1cd716791f14816cca4fe5a0e9f771e56f Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Tue, 27 Aug 2024 15:09:38 +0200 Subject: [PATCH 2/3] upd --- optimus/lib/src/lists/nav_list_tile.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/optimus/lib/src/lists/nav_list_tile.dart b/optimus/lib/src/lists/nav_list_tile.dart index 36c3e1c2..8350e03e 100644 --- a/optimus/lib/src/lists/nav_list_tile.dart +++ b/optimus/lib/src/lists/nav_list_tile.dart @@ -75,6 +75,14 @@ class _OptimusNavListTileState extends State super.dispose(); } + @override + void didUpdateWidget(OptimusNavListTile oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isEnabled != oldWidget.isEnabled) { + _controller.update(WidgetState.disabled, !widget.isEnabled); + } + } + InteractiveStateColor get _backgroundColor => InteractiveStateColor( defaultColor: tokens.backgroundInteractiveNeutralSubtleDefault, disabled: Colors.transparent, From 93d4017d01120344615c56d21f5a62b64f568d77 Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Tue, 27 Aug 2024 15:14:13 +0200 Subject: [PATCH 3/3] upd --- optimus/lib/src/lists/nav_list_tile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optimus/lib/src/lists/nav_list_tile.dart b/optimus/lib/src/lists/nav_list_tile.dart index 8350e03e..4b6090ff 100644 --- a/optimus/lib/src/lists/nav_list_tile.dart +++ b/optimus/lib/src/lists/nav_list_tile.dart @@ -160,7 +160,7 @@ class _OptimusNavListTileState extends State OptimusIcons.chevron_right, color: foregroundColor, size: tokens.sizing300, - ) + ), ], ), ),