diff --git a/packages/remix/demo/lib/components/select.dart b/packages/remix/demo/lib/components/select.dart index bf9a6ae51..7b22e886e 100644 --- a/packages/remix/demo/lib/components/select.dart +++ b/packages/remix/demo/lib/components/select.dart @@ -41,15 +41,17 @@ class _SelectDemoState extends State { SizedBox( width: 200, child: Select( - disabled: - context.knobs.boolean(label: 'disabled', initialValue: false), - variants: [context.knobs.variant(FortalezaSelectStyle.variants)], value: selectedValue, onChanged: (value) => setState(() => selectedValue = value), - button: (spec) => spec( + trigger: (spec) => spec( text: selectedValue, trailingIcon: m.Icons.keyboard_arrow_down_rounded, ), + disabled: context.knobs.boolean( + label: 'disabled', + initialValue: false, + ), + variants: [context.knobs.variant(FortalezaSelectStyle.variants)], items: List.generate( items.length, (index) => SelectMenuItem( diff --git a/packages/remix/demo/macos/Flutter/ephemeral/FlutterInputs.xcfilelist b/packages/remix/demo/macos/Flutter/ephemeral/FlutterInputs.xcfilelist index 8a493cbf6..19306756c 100644 --- a/packages/remix/demo/macos/Flutter/ephemeral/FlutterInputs.xcfilelist +++ b/packages/remix/demo/macos/Flutter/ephemeral/FlutterInputs.xcfilelist @@ -1036,6 +1036,7 @@ /Users/lucasoliveira/Developer/Mix/PoCs/remix_toast/packages/mix/packages/remix/lib/src/helpers/color_utils.dart /Users/lucasoliveira/Developer/Mix/PoCs/remix_toast/packages/mix/packages/remix/lib/src/helpers/component_builder.dart /Users/lucasoliveira/Developer/Mix/PoCs/remix_toast/packages/mix/packages/remix/lib/src/helpers/context_ext.dart +/Users/lucasoliveira/Developer/Mix/PoCs/remix_toast/packages/mix/packages/remix/lib/src/helpers/object_ext.dart /Users/lucasoliveira/Developer/Mix/PoCs/remix_toast/packages/mix/packages/remix/lib/src/helpers/overlay.dart /Users/lucasoliveira/Developer/Mix/PoCs/remix_toast/packages/mix/packages/remix/lib/src/helpers/spec/composited_transform_follower_spec.dart /Users/lucasoliveira/Developer/Mix/PoCs/remix_toast/packages/mix/packages/remix/lib/src/helpers/spec/composited_transform_follower_spec.g.dart diff --git a/packages/remix/lib/src/components/dropdown_menu/dropdown_menu_widget.dart b/packages/remix/lib/src/components/dropdown_menu/dropdown_menu_widget.dart index 17214dd8b..8436a333c 100644 --- a/packages/remix/lib/src/components/dropdown_menu/dropdown_menu_widget.dart +++ b/packages/remix/lib/src/components/dropdown_menu/dropdown_menu_widget.dart @@ -116,6 +116,7 @@ class DropdownMenuState extends State { onTapOutside: widget.onPressOutside, showOverlay: widget.open, animationDuration: animatedStyle?.animated.duration ?? Duration.zero, + link: _link, ); } } diff --git a/packages/remix/lib/src/components/select/button/select_button_widget.dart b/packages/remix/lib/src/components/select/button/select_button_widget.dart index 4e617a4fc..83840b564 100644 --- a/packages/remix/lib/src/components/select/button/select_button_widget.dart +++ b/packages/remix/lib/src/components/select/button/select_button_widget.dart @@ -14,20 +14,36 @@ class SelectButtonSpecWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return SpecBuilder( - inherit: true, - builder: (context) { - final button = SelectSpec.of(context).button; - - final container = button.container; - final label = button.label; - final icon = button.icon; - - return container( - direction: Axis.horizontal, - children: [label(text), icon(trailingIcon)], - ); + final select = context.findAncestorStateOfType(); + + if (select == null) { + throw Exception('SelectButton must be a child of a Select'); + } + + final style = select.widget.style ?? context.remix.components.select; + final configuration = SpecConfiguration(context, SelectSpecUtility.self); + final appliedStyle = + style.makeStyle(configuration).applyVariants(select.widget.variants); + + return Pressable( + onPress: () { + select.openMenu(); }, + child: SpecBuilder( + style: appliedStyle, + builder: (context) { + final button = SelectSpec.of(context).button; + + final container = button.container; + final label = button.label; + final icon = button.icon; + + return container( + direction: Axis.horizontal, + children: [label(text), icon(trailingIcon)], + ); + }, + ), ); } } diff --git a/packages/remix/lib/src/components/select/item/select_menu_widget.dart b/packages/remix/lib/src/components/select/item/select_menu_widget.dart index c839a3387..6f6990913 100644 --- a/packages/remix/lib/src/components/select/item/select_menu_widget.dart +++ b/packages/remix/lib/src/components/select/item/select_menu_widget.dart @@ -36,20 +36,15 @@ class SelectMenuItemWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return SpecBuilder( - inherit: true, - builder: (context) { - final item = SelectSpec.of(context).item; + final item = SelectSpec.of(context).item; - final container = item.container; - final icon = item.icon; - final text = item.text; + final container = item.container; + final icon = item.icon; + final text = item.text; - return container( - direction: Axis.horizontal, - children: [if (iconData != null) icon(iconData), text(this.text)], - ); - }, + return container( + direction: Axis.horizontal, + children: [if (iconData != null) icon(iconData), text(this.text)], ); } } diff --git a/packages/remix/lib/src/components/select/select.dart b/packages/remix/lib/src/components/select/select.dart index 8265fe99a..b90d3adab 100644 --- a/packages/remix/lib/src/components/select/select.dart +++ b/packages/remix/lib/src/components/select/select.dart @@ -5,6 +5,8 @@ import 'package:mix_annotations/mix_annotations.dart'; import '../../core/theme/remix_theme.dart'; import '../../helpers/component_builder.dart'; +import '../../helpers/object_ext.dart'; +import '../../helpers/overlay.dart'; import '../../helpers/spec/composited_transform_follower_spec.dart'; part 'button/select_button.dart'; diff --git a/packages/remix/lib/src/components/select/select_style.dart b/packages/remix/lib/src/components/select/select_style.dart index 0a196358c..75ead1bf8 100644 --- a/packages/remix/lib/src/components/select/select_style.dart +++ b/packages/remix/lib/src/components/select/select_style.dart @@ -74,7 +74,10 @@ class SelectStyle extends SpecStyle { ...buttonStyle, ...menuStyle, ...positionStyle, - ]); + ]).animate( + duration: const Duration(milliseconds: 1500), + curve: Curves.easeInOut, + ); } } diff --git a/packages/remix/lib/src/components/select/select_widget.dart b/packages/remix/lib/src/components/select/select_widget.dart index 119f57b8f..37a72e576 100644 --- a/packages/remix/lib/src/components/select/select_widget.dart +++ b/packages/remix/lib/src/components/select/select_widget.dart @@ -12,7 +12,7 @@ class Select extends StatefulWidget { super.key, required this.value, required this.onChanged, - required this.button, + required this.trigger, required this.items, this.variants = const [], this.style, @@ -34,7 +34,7 @@ class Select extends StatefulWidget { /// Builder function that creates the button portion of the select component. /// When tapped, this button will display the dropdown menu. /// This allows customizing how the button is displayed. - final WidgetSpecBuilder button; + final WidgetSpecBuilder trigger; /// {@macro remix.component.disabled} final bool disabled; @@ -49,15 +49,9 @@ class Select extends StatefulWidget { class SelectState extends State> with SingleTickerProviderStateMixin { - final OverlayPortalController _tooltipController = OverlayPortalController(); late final MixWidgetStateController _menuStateController; late final MixWidgetStateController _buttonStateController; - final _baseAnimation = ( - duration: const Duration(milliseconds: 100), - curve: Curves.decelerate, - ); - final _link = LayerLink(); @override @@ -76,6 +70,18 @@ class SelectState extends State> _buttonStateController.disabled = widget.disabled; } + void openMenu() { + setState(() { + _menuStateController.selected = true; + }); + } + + void closeMenu() { + setState(() { + _menuStateController.selected = false; + }); + } + @override void dispose() { _buttonStateController.dispose(); @@ -90,108 +96,63 @@ class SelectState extends State> final appliedStyle = style.makeStyle(configuration).applyVariants(widget.variants); - return SpecBuilder( - controller: _buttonStateController, - style: appliedStyle, - builder: (context) { - final button = SelectSpec.of(context).button; - final position = SelectSpec.of(context).position; - - return CompositedTransformTarget( - link: _link, - child: OverlayPortal( - controller: _tooltipController, - overlayChildBuilder: (BuildContext context) { - return Stack(children: [ - GestureDetector( - onTap: () => hide(), - child: Container(color: Colors.transparent), - ), - CompositedTransformFollower( - link: _link, - targetAnchor: - position.targetAnchor.resolve(TextDirection.ltr), - followerAnchor: - position.followerAnchor.resolve(TextDirection.ltr), - child: SpecBuilder( - controller: _menuStateController, - style: appliedStyle.animate( - duration: _baseAnimation.duration, - curve: _baseAnimation.curve, - onEnd: () { - if (_menuStateController.selected == false) { - _tooltipController.hide(); - } + final animatedStyle = appliedStyle.cast(); + + return OverlayWrapper( + target: RepaintBoundary( + child: SpecBuilder( + controller: _buttonStateController, + style: appliedStyle, + builder: (context) { + final buttonSpec = SelectSpec.of(context).button; + + return widget.trigger(buttonSpec); + }, + ), + ), + overlayChild: SpecBuilder( + controller: _menuStateController, + style: appliedStyle, + builder: (context) { + final select = SelectSpec.of(context); + final menu = select.menu; + + final FlexContainer = menu.container.copyWith( + box: menu.container.box.copyWith( + width: menu.autoWidth ? _link.leaderSize!.width : null, + ), + ); + + return AnimatedBoxSpecWidget( + spec: FlexContainer.box, + duration: animatedStyle?.animated.duration ?? Duration.zero, + curve: animatedStyle?.animated.curve ?? Curves.easeInOut, + child: FlexContainer.flex( + direction: Axis.vertical, + children: widget.items + .map( + (item) => Pressable( + onPress: () { + widget.onChanged(item.value); + closeMenu(); }, + child: SpecBuilder( + style: appliedStyle, + builder: (context) { + return item.child; + }, + ), ), - builder: (context) { - final select = SelectSpec.of(context); - final menu = select.menu; - - final Container = menu.container.copyWith( - box: menu.container.box.copyWith( - width: - menu.autoWidth ? _link.leaderSize!.width : null, - ), - ); - - return Container( - direction: Axis.vertical, - children: widget.items.map((item) { - return Pressable( - onPress: () { - widget.onChanged(item.value); - hide(); - }, - child: SpecBuilder( - style: appliedStyle.animate( - duration: _baseAnimation.duration, - curve: _baseAnimation.curve, - ), - builder: (context) { - return item.child; - }, - ), - ); - }).toList(), - ); - }, - ), - ), - ]); - }, - child: RepaintBoundary( - child: Pressable( - enabled: !widget.disabled, - onPress: onTap, - child: widget.button(button), - ), + ) + .toList(), ), - ), - ); - }, + ); + }, + ), + onTapOutside: closeMenu, + showOverlay: _menuStateController.selected, + animationDuration: animatedStyle?.animated.duration ?? Duration.zero, + link: _link, ); } - - void show() { - if (!_tooltipController.isShowing) { - _tooltipController.show(); - } - - WidgetsBinding.instance.addPostFrameCallback((_) { - _menuStateController.selected = true; - }); - } - - void hide() { - _menuStateController.selected = false; - } - - void onTap() { - if (!_tooltipController.isShowing) { - show(); - } else { - hide(); - } - } } diff --git a/packages/remix/lib/src/helpers/overlay.dart b/packages/remix/lib/src/helpers/overlay.dart index 3fb32a405..271dd93ee 100644 --- a/packages/remix/lib/src/helpers/overlay.dart +++ b/packages/remix/lib/src/helpers/overlay.dart @@ -14,6 +14,7 @@ class OverlayWrapper extends StatefulWidget { this.onTapOutside, this.showOverlay = true, this.animationDuration = const Duration(), + required this.link, }); /// The trigger widget that opens the dropdown menu. @@ -42,12 +43,13 @@ class OverlayWrapper extends StatefulWidget { final Duration animationDuration; + final LayerLink link; + @override State createState() => OverlayWrapperState(); } class OverlayWrapperState extends State { - final _link = LayerLink(); OverlayPortalController? _controller; Timer? _timer; @@ -91,7 +93,7 @@ class OverlayWrapperState extends State { @override Widget build(BuildContext context) { return CompositedTransformTarget( - link: _link, + link: widget.link, child: OverlayPortal( controller: _controller!, overlayChildBuilder: (BuildContext context) { @@ -102,7 +104,7 @@ class OverlayWrapperState extends State { child: Container(color: Colors.transparent), ), CompositedTransformFollower( - link: _link, + link: widget.link, offset: widget.offset, targetAnchor: widget.targetAnchor, followerAnchor: widget.followerAnchor,