Skip to content

Commit

Permalink
Merge branch 'master' into DX-2141
Browse files Browse the repository at this point in the history
  • Loading branch information
witwash authored Sep 6, 2024
2 parents 6e8d5ad + fe6261d commit 015c6bb
Show file tree
Hide file tree
Showing 14 changed files with 181 additions and 227 deletions.
2 changes: 1 addition & 1 deletion .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ runs:
- name: Set up Flutter
uses: subosito/flutter-action@ea686d7c56499339ad176e9f19c516ff6cf05a31
with:
flutter-version: 3.22.2
flutter-version: 3.24.2
cache: true

- name: Set up environment paths
Expand Down
4 changes: 4 additions & 0 deletions optimus/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.37.0+1

- Update a dependency to the latest release.

## 0.37.0

> Note: This release has breaking changes.
Expand Down
4 changes: 2 additions & 2 deletions optimus/lib/src/dropdown/dropdown_select.dart
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ class _DropdownSelectState<T> extends State<DropdownSelect<T>> {
});
}

void _handleOnBackPressed(bool didPop) {
void _handleOnBackPressed(bool didPop, _) {
if (didPop) return;
if (_effectiveFocusNode.hasFocus) {
_effectiveFocusNode.unfocus();
Expand Down Expand Up @@ -307,7 +307,7 @@ class _DropdownSelectState<T> extends State<DropdownSelect<T>> {

return PopScope(
canPop: _canPop,
onPopInvoked: _handleOnBackPressed,
onPopInvokedWithResult: _handleOnBackPressed,
child: widget.allowMultipleSelection && _hasValues
? MultiSelectInputField(
values: _values ?? [],
Expand Down
313 changes: 121 additions & 192 deletions optimus/lib/src/lists/nav_list_tile.dart
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,229 +16,157 @@ 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,
};
/// Whether to use horizontal padding.
final bool useHorizontalPadding;

@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: <Widget>[
if (leadingAvatar != null) _Avatar(avatar: leadingAvatar),
if (leadingAvatar == null && leadingIcon != null)
_LeadingIcon(icon: leadingIcon),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_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),
],
),
),
);
}
}
/// Whether the toggle is visible.
final bool isToggleVisible;

class _Description extends StatelessWidget {
const _Description({
required this.description,
required this.fontVariant,
});

final Widget description;
final FontVariant fontVariant;
/// Whether the chevron is visible.
final bool isChevronVisible;

@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.spacing100),
child: OptimusTypography(
resolveStyle: (_) => tokens.bodyMediumStrong,
color: fontVariant.secondaryColor,
child: DefaultTextStyle.merge(
maxLines: 2,
overflow: TextOverflow.ellipsis,
child: description,
),
),
);
}
}
/// Whether the toggle is toggled.
final bool isToggled;

class _LeadingIcon extends StatelessWidget {
const _LeadingIcon({required this.icon});
/// The callback that is called when the toggle is pressed.
final ValueChanged<bool>? onTogglePressed;

final Widget icon;
/// 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: IconTheme.merge(
data: IconThemeData(size: tokens.sizing300),
child: icon,
),
);
}
State<OptimusNavListTile> createState() => _OptimusNavListTileState();
}

class _Avatar extends StatelessWidget {
const _Avatar({required this.avatar});

final Widget avatar;
class _OptimusNavListTileState extends State<OptimusNavListTile>
with ThemeGetter {
final WidgetStatesController _controller = WidgetStatesController();

@override
Widget build(BuildContext context) {
final tokens = context.tokens;

return Padding(
padding: EdgeInsets.only(right: tokens.spacing200),
child: SizedBox(width: tokens.sizing500, child: avatar),
);
void dispose() {
_controller.dispose();
super.dispose();
}
}

class _Headline extends StatelessWidget {
const _Headline({required this.fontVariant, required this.headline});

final FontVariant fontVariant;
final Widget headline;

@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 didUpdateWidget(OptimusNavListTile oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isEnabled != oldWidget.isEnabled) {
_controller.update(WidgetState.disabled, !widget.isEnabled);
}
}
}

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 }
Loading

0 comments on commit 015c6bb

Please sign in to comment.