From ee2f3b6baf2939e96f00ff7e02e4e8a810fe309a Mon Sep 17 00:00:00 2001 From: Hub <79840500+GittHub-d@users.noreply.github.com> Date: Fri, 23 Feb 2024 15:29:36 +0200 Subject: [PATCH] feat: [MDS-990] Combobox component (#359) Co-authored-by: Birgitt Majas --- example/assets/code_snippets/combobox.md | 292 ++++++++++++ example/assets/code_snippets/dropdown.md | 37 +- .../common/widgets/text_divider.dart | 2 +- .../lib/src/storybook/routing/app_router.dart | 64 ++- .../routing/route_aware_stories.dart | 73 +-- .../lib/src/storybook/stories/accordion.dart | 2 +- example/lib/src/storybook/stories/alert.dart | 2 +- .../lib/src/storybook/stories/auth_code.dart | 2 +- example/lib/src/storybook/stories/avatar.dart | 4 +- .../src/storybook/stories/bottom_sheet.dart | 2 +- .../lib/src/storybook/stories/breadcrumb.dart | 4 +- example/lib/src/storybook/stories/button.dart | 4 +- .../lib/src/storybook/stories/carousel.dart | 4 +- .../lib/src/storybook/stories/checkbox.dart | 2 +- example/lib/src/storybook/stories/chip.dart | 4 +- .../storybook/stories/circular_loader.dart | 3 +- .../storybook/stories/circular_progress.dart | 3 +- .../lib/src/storybook/stories/combobox.dart | 418 ++++++++++++++++++ .../src/storybook/stories/dot_indicator.dart | 2 +- example/lib/src/storybook/stories/drawer.dart | 2 +- .../lib/src/storybook/stories/dropdown.dart | 77 ++-- example/lib/src/storybook/stories/icons.dart | 2 +- .../src/storybook/stories/linear_loader.dart | 2 +- .../storybook/stories/linear_progress.dart | 2 +- .../lib/src/storybook/stories/menu_item.dart | 2 +- example/lib/src/storybook/stories/modal.dart | 2 +- .../lib/src/storybook/stories/popover.dart | 2 +- example/lib/src/storybook/stories/radio.dart | 2 +- .../storybook/stories/segmented_control.dart | 2 +- example/lib/src/storybook/stories/switch.dart | 4 +- .../lib/src/storybook/stories/tab_bar.dart | 4 +- example/lib/src/storybook/stories/table.dart | 2 +- example/lib/src/storybook/stories/tag.dart | 2 +- .../lib/src/storybook/stories/text_area.dart | 3 +- .../lib/src/storybook/stories/text_input.dart | 9 +- .../storybook/stories/text_input_group.dart | 4 +- example/lib/src/storybook/stories/toast.dart | 2 +- .../lib/src/storybook/stories/tooltip.dart | 2 +- example/lib/src/storybook/storybook.dart | 4 +- .../bottom_sheet/modal_bottom_sheet.dart | 2 +- 40 files changed, 891 insertions(+), 166 deletions(-) create mode 100644 example/assets/code_snippets/combobox.md create mode 100644 example/lib/src/storybook/stories/combobox.dart diff --git a/example/assets/code_snippets/combobox.md b/example/assets/code_snippets/combobox.md new file mode 100644 index 00000000..79fdfc0d --- /dev/null +++ b/example/assets/code_snippets/combobox.md @@ -0,0 +1,292 @@ +import 'package:flutter/material.dart'; +import 'package:moon_design/moon_design.dart'; + +import 'package:example/src/storybook/common/color_options.dart'; +import 'package:example/src/storybook/common/widgets/text_divider.dart'; +import 'package:flutter/material.dart'; +import 'package:moon_design/moon_design.dart'; +import 'package:storybook_flutter/storybook_flutter.dart'; + +enum Options { + accordion, + alert, + bottomSheet, + button, + carousel, + chip, + dropdown, + menuItem, + tag, + tooltip, +} + +extension OptionsX on Options { + String get name { + return switch (this) { + Options.accordion => "Accordion", + Options.alert => "Alert", + Options.bottomSheet => "BottomSheet", + Options.button => "Button", + Options.carousel => "Carousel", + Options.chip => "Chip", + Options.dropdown => "Dropdown", + Options.menuItem => "MenuItem", + Options.tag => "Tag", + Options.tooltip => "Tooltip" + }; + } +} + +class ComboboxStory extends StatefulWidget { + const ComboboxStory({super.key}); + + @override + State createState() => _ComboboxStoryState(); +} + +class _ComboboxStoryState extends State { + final Map _optionsList = { + Options.accordion: false, + Options.alert: false, + Options.bottomSheet: false, + Options.button: false, + Options.carousel: false, + Options.chip: false, + Options.dropdown: false, + Options.menuItem: false, + Options.tag: false, + Options.tooltip: false, + }; + + final FocusNode _singleSelectFocusNode = FocusNode(); + final FocusNode _multiSelectFocusNode = FocusNode(); + + final TextEditingController _singleSelectSearchController = TextEditingController(); + final TextEditingController _multiSelectSearchController = TextEditingController(); + + final Map _multiSelectedItems = {}; + + Map _singleSelectFilteredData = {}; + Map _multiSelectFilteredData = {}; + + bool _showSingleSelectDropdown = false; + bool _showMultiSelectDropdown = false; + + String get _singleSelectInputValue => _singleSelectSearchController.text.trim().toLowerCase(); + + String get _multiSelectInputValue => _multiSelectSearchController.text.trim().toLowerCase(); + + // Single select. + void _performSingleSelectSearch() { + setState(() { + _singleSelectFilteredData = Map.fromEntries( + _optionsList.entries.where((entry) => entry.key.name.toLowerCase().contains(_singleSelectInputValue)).toList(), + ); + _showSingleSelectDropdown = true; + }); + } + + void _handleSingleSelect(Options item) { + setState(() { + _showSingleSelectDropdown = false; + _singleSelectSearchController.text = item.name; + _singleSelectFocusNode.unfocus(); + }); + } + + void _showSingleSelectAllOptionsList() { + setState(() { + _singleSelectFilteredData = _optionsList; + _showSingleSelectDropdown = !_showSingleSelectDropdown; + }); + } + + void _handleSingleSelectDropdownTapOutside() { + final bool isOptionSelected = + _optionsList.entries.any((item) => item.key.name.toLowerCase() == _singleSelectInputValue); + + setState(() { + _showSingleSelectDropdown = false; + if (!isOptionSelected) _singleSelectSearchController.clear(); + _singleSelectFocusNode.unfocus(); + }); + } + + void _handleSingleSelectInputTapOutside() { + if (_singleSelectFocusNode.hasFocus && !_showSingleSelectDropdown) { + _singleSelectSearchController.clear(); + _singleSelectFocusNode.unfocus(); + } + } + + // Multi select. + void _performMultiSelectSearch() { + setState(() { + _multiSelectFilteredData = Map.fromEntries( + _optionsList.entries.where((entry) => entry.key.name.toLowerCase().contains(_multiSelectInputValue)).toList(), + ); + _showMultiSelectDropdown = true; + }); + } + + void _handleMultiSelect(Options item, bool isSelected) { + setState(() => isSelected ? _multiSelectedItems[item] = true : _multiSelectedItems.remove(item)); + } + + void _showMultiSelectAllOptionsList() { + setState(() { + _multiSelectFilteredData = _optionsList; + _showMultiSelectDropdown = !_showMultiSelectDropdown; + }); + } + + void _handleMultiSelectDropdownTapOutside() { + setState(() { + _showMultiSelectDropdown = false; + _multiSelectSearchController.clear(); + _multiSelectFocusNode.unfocus(); + }); + } + + void _handleMultiSelectInputTapOutside() { + if (_multiSelectFocusNode.hasFocus && !_showMultiSelectDropdown) { + _multiSelectSearchController.clear(); + _multiSelectFocusNode.unfocus(); + } + } + + @override + void dispose() { + _singleSelectSearchController.dispose(); + _multiSelectSearchController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Single select combobox. + MoonDropdown( + show: _showSingleSelectDropdown, + constrainWidthToChild: true, + onTapOutside: () => _handleSingleSelectDropdownTapOutside(), + content: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200), + child: _singleSelectFilteredData.isEmpty + ? const MoonMenuItem( + label: Text('Nothing found.'), + ) + : ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: _singleSelectFilteredData.length, + itemBuilder: (BuildContext _, int index) { + final List keys = _singleSelectFilteredData.keys.toList(); + if (index >= keys.length) return const SizedBox.shrink(); + final Options item = keys[index]; + + return MoonMenuItem( + onTap: () => _handleSingleSelect(item), + label: Text(item.name), + ); + }, + ), + ), + child: MoonTextInput( + focusNode: _singleSelectFocusNode, + hintText: "Select single component", + controller: _singleSelectSearchController, + // The onTap() and onChanged() properties are used instead of a listener to initiate search on every input tap. + // Listener only triggers on input change. + onTap: () => _performSingleSelectSearch(), + onTapOutside: (PointerDownEvent _) => _handleSingleSelectInputTapOutside(), + onChanged: (String _) => _performSingleSelectSearch(), + trailing: MoonButton.icon( + hoverEffectColor: Colors.transparent, + onTap: () => _showSingleSelectAllOptionsList(), + icon: AnimatedRotation( + duration: const Duration(milliseconds: 200), + turns: _showSingleSelectDropdown ? -0.5 : 0, + child: const Icon(MoonIcons.controls_chevron_down_small_24_light), + ), + ), + ), + ), + + // Multi select combobox. + MoonDropdown( + show: _showMultiSelectDropdown, + constrainWidthToChild: true, + onTapOutside: () => _handleMultiSelectDropdownTapOutside(), + content: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200), + child: _multiSelectFilteredData.isEmpty + ? const MoonMenuItem( + label: Text('Nothing found.'), + ) + : ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: _multiSelectFilteredData.length, + itemBuilder: (BuildContext _, int index) { + final List keys = _multiSelectFilteredData.keys.toList(); + if (index >= keys.length) return const SizedBox.shrink(); + final bool isSelected = _multiSelectedItems.containsKey(keys[index]); + + return MoonMenuItem( + absorbGestures: true, + onTap: () => _handleMultiSelect(keys[index], !isSelected), + label: Text(keys[index].name), + trailing: MoonCheckbox( + value: isSelected, + tapAreaSizeValue: 0, + onChanged: (bool? _) {}, + ), + ); + }, + ), + ), + child: MoonTextInput( + focusNode: _multiSelectFocusNode, + hintText: "Select multiple components", + controller: _multiSelectSearchController, + // The onTap() and onChanged() properties are used instead of a listener to initiate search on every input tap. + // Listener only triggers on input change. onTap: () => _performMultiSelectSearch(), + onTapOutside: (PointerDownEvent _) => _handleMultiSelectInputTapOutside(), + onChanged: (String _) => _performMultiSelectSearch(), + leading: _multiSelectedItems.isNotEmpty + ? Center( + child: GestureDetector( + onTap: () => setState(() => _multiSelectedItems.clear()), + child: MoonTag( + tagSize: MoonTagSize.xs, + backgroundColor: context.moonColors!.bulma, + label: Text( + "${_multiSelectedItems.keys.length}", + style: TextStyle(color: context.moonColors!.gohan), + ), + trailing: Icon( + MoonIcons.controls_close_small_16_light, + color: context.moonColors!.gohan, + ), + ), + ), + ) + : null, + trailing: MoonButton.icon( + hoverEffectColor: Colors.transparent, + onTap: () => _showMultiSelectAllOptionsList(), + icon: AnimatedRotation( + duration: const Duration(milliseconds: 200), + turns: _showMultiSelectDropdown ? -0.5 : 0, + child: const Icon(MoonIcons.controls_chevron_down_small_24_light), + ), + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/example/assets/code_snippets/dropdown.md b/example/assets/code_snippets/dropdown.md index 1d42f223..679e15f3 100644 --- a/example/assets/code_snippets/dropdown.md +++ b/example/assets/code_snippets/dropdown.md @@ -7,12 +7,10 @@ enum Choices { first, second } extension ChoicesX on Choices { String get name { - switch (this) { - case Choices.first: - return "Choice #1"; - case Choices.second: - return "Choice #2"; - } + return switch (this) { + Choices.first => "Choice #1", + Choices.second => "Choice #2", + }; } } @@ -69,26 +67,25 @@ class _DropdownState extends State { ], ), child: MoonTextInput( - mouseCursor: MouseCursor.defer, - readOnly: true, width: 250, + readOnly: true, + canRequestFocus: false, + mouseCursor: MouseCursor.defer, hintText: "Choose an option", onTap: () => setState(() => _showChoices = !_showChoices), leading: _availableChoices.values.any((element) => element == true) ? Center( - child: GestureDetector( + child: MoonTag( + tagSize: MoonTagSize.xs, + backgroundColor: context.moonColors!.bulma, onTap: () => setState(() => _availableChoices.updateAll((key, value) => false)), - child: MoonTag( - tagSize: MoonTagSize.xs, - backgroundColor: context.moonColors!.bulma, - label: Text( - "${_availableChoices.values.where((element) => element == true).length}", - style: TextStyle(color: context.moonColors!.gohan), - ), - trailing: Icon( - MoonIcons.controls_close_small_16_light, - color: context.moonColors!.gohan, - ), + label: Text( + "${_availableChoices.values.where((element) => element == true).length}", + style: TextStyle(color: context.moonColors!.gohan), + ), + trailing: Icon( + MoonIcons.controls_close_small_16_light, + color: context.moonColors!.gohan, ), ), ) diff --git a/example/lib/src/storybook/common/widgets/text_divider.dart b/example/lib/src/storybook/common/widgets/text_divider.dart index 3f34e1ae..661ea75b 100644 --- a/example/lib/src/storybook/common/widgets/text_divider.dart +++ b/example/lib/src/storybook/common/widgets/text_divider.dart @@ -10,7 +10,7 @@ class TextDivider extends StatelessWidget { const TextDivider({ super.key, required this.text, - this.paddingTop = 40, + this.paddingTop = 48, this.paddingBottom = 32, }); diff --git a/example/lib/src/storybook/routing/app_router.dart b/example/lib/src/storybook/routing/app_router.dart index 59c8321b..052027f8 100644 --- a/example/lib/src/storybook/routing/app_router.dart +++ b/example/lib/src/storybook/routing/app_router.dart @@ -11,6 +11,7 @@ import 'package:example/src/storybook/stories/checkbox.dart'; import 'package:example/src/storybook/stories/chip.dart'; import 'package:example/src/storybook/stories/circular_loader.dart'; import 'package:example/src/storybook/stories/circular_progress.dart'; +import 'package:example/src/storybook/stories/combobox.dart'; import 'package:example/src/storybook/stories/dot_indicator.dart'; import 'package:example/src/storybook/stories/drawer.dart'; import 'package:example/src/storybook/stories/dropdown.dart'; @@ -35,10 +36,31 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; +const String primitivesDirectory = '/primitives'; +const String compositesDirectory = '/composites'; + GoRouter router = GoRouter( debugLogDiagnostics: true, initialLocation: AccordionStory.path, errorBuilder: (context, state) => const RoutingErrorWidget(), + redirect: (context, state) { + switch (state.uri.path) { + case primitivesDirectory: + return AccordionStory.path; + case compositesDirectory: + return ComboboxStory.path; + case CircularLoaderStory.subdirectory: + return CircularLoaderStory.path; + case CircularProgressStory.subdirectory: + return CircularProgressStory.path; + case '$primitivesDirectory${CircularLoaderStory.subdirectory}': + return CircularLoaderStory.path; + case '$primitivesDirectory${CircularProgressStory.subdirectory}': + return CircularProgressStory.path; + default: + return null; + } + }, routes: [ GoRoute( path: '/', @@ -47,30 +69,6 @@ GoRouter router = GoRouter( return AccordionStory.path; }, ), - GoRoute( - path: CircularLoaderStory.subdirectory, - redirect: (BuildContext context, GoRouterState state) => CircularLoaderStory.path, - routes: [ - GoRoute( - path: CircularLoaderStory.segment, - pageBuilder: (BuildContext context, GoRouterState state) => const NoTransitionPage( - child: CircularLoaderStory(), - ), - ), - ], - ), - GoRoute( - path: CircularProgressStory.subdirectory, - redirect: (BuildContext context, GoRouterState state) => CircularProgressStory.path, - routes: [ - GoRoute( - path: CircularProgressStory.segment, - pageBuilder: (BuildContext context, GoRouterState state) => const NoTransitionPage( - child: CircularProgressStory(), - ), - ), - ], - ), GoRoute( path: AccordionStory.path, pageBuilder: (BuildContext context, GoRouterState state) => const NoTransitionPage( @@ -131,6 +129,24 @@ GoRouter router = GoRouter( child: ChipStory(), ), ), + GoRoute( + path: CircularLoaderStory.path, + pageBuilder: (BuildContext context, GoRouterState state) => const NoTransitionPage( + child: CircularLoaderStory(), + ), + ), + GoRoute( + path: CircularProgressStory.path, + pageBuilder: (BuildContext context, GoRouterState state) => const NoTransitionPage( + child: CircularProgressStory(), + ), + ), + GoRoute( + path: ComboboxStory.path, + pageBuilder: (BuildContext context, GoRouterState state) => const NoTransitionPage( + child: ComboboxStory(), + ), + ), GoRoute( path: DotIndicatorStory.path, pageBuilder: (BuildContext context, GoRouterState state) => const NoTransitionPage( diff --git a/example/lib/src/storybook/routing/route_aware_stories.dart b/example/lib/src/storybook/routing/route_aware_stories.dart index 0903c017..95ab7ba9 100644 --- a/example/lib/src/storybook/routing/route_aware_stories.dart +++ b/example/lib/src/storybook/routing/route_aware_stories.dart @@ -11,6 +11,7 @@ import 'package:example/src/storybook/stories/checkbox.dart'; import 'package:example/src/storybook/stories/chip.dart'; import 'package:example/src/storybook/stories/circular_loader.dart'; import 'package:example/src/storybook/stories/circular_progress.dart'; +import 'package:example/src/storybook/stories/combobox.dart'; import 'package:example/src/storybook/stories/dot_indicator.dart'; import 'package:example/src/storybook/stories/drawer.dart'; import 'package:example/src/storybook/stories/dropdown.dart'; @@ -37,194 +38,202 @@ import 'package:storybook_flutter/storybook_flutter.dart'; const String directory = 'assets/code_snippets/'; final List routeAwareStories = [ + // Composite stories. Story.asRoute( - name: 'Accordion', + name: 'Composites/Combobox', + routePath: ComboboxStory.path, + router: router, + codeString: fetchAsset('combobox.md'), + ), + // Primitive stories. + Story.asRoute( + name: 'Primitives/Accordion', routePath: AccordionStory.path, router: router, codeString: fetchAsset('accordion.md'), ), Story.asRoute( - name: 'Alert', + name: 'Primitives/Alert', routePath: AlertStory.path, router: router, codeString: fetchAsset('alert.md'), ), Story.asRoute( - name: 'AuthCode', + name: 'Primitives/AuthCode', routePath: AuthCodeStory.path, router: router, codeString: fetchAsset('auth_code.md'), ), Story.asRoute( - name: 'Avatar', + name: 'Primitives/Avatar', routePath: AvatarStory.path, router: router, codeString: fetchAsset('avatar.md'), ), Story.asRoute( - name: 'BottomSheet', + name: 'Primitives/BottomSheet', routePath: BottomSheetStory.path, router: router, codeString: fetchAsset('bottom_sheet.md'), ), Story.asRoute( - name: 'Breadcrumb', + name: 'Primitives/Breadcrumb', routePath: BreadcrumbStory.path, router: router, codeString: fetchAsset('breadcrumb.md'), ), Story.asRoute( - name: 'Button', + name: 'Primitives/Button', routePath: ButtonStory.path, router: router, codeString: fetchAsset('button.md'), ), Story.asRoute( - name: 'Carousel', + name: 'Primitives/Carousel', routePath: CarouselStory.path, router: router, codeString: fetchAsset('carousel.md'), ), Story.asRoute( - name: 'Checkbox', + name: 'Primitives/Checkbox', routePath: CheckboxStory.path, router: router, codeString: fetchAsset('checkbox.md'), ), Story.asRoute( - name: 'Chip', + name: 'Primitives/Chip', routePath: ChipStory.path, router: router, codeString: fetchAsset('chip.md'), ), Story.asRoute( - name: 'Loader/CircularLoader', + name: 'Primitives/Loader/CircularLoader', routePath: CircularLoaderStory.path, router: router, codeString: fetchAsset('circular_loader.md'), ), Story.asRoute( - name: 'Progress/CircularProgress', + name: 'Primitives/Progress/CircularProgress', routePath: CircularProgressStory.path, router: router, codeString: fetchAsset('circular_progress.md'), ), Story.asRoute( - name: 'DotIndicator', + name: 'Primitives/DotIndicator', routePath: DotIndicatorStory.path, router: router, codeString: fetchAsset('dot_indicator.md'), ), Story.asRoute( - name: 'Drawer', + name: 'Primitives/Drawer', routePath: DrawerStory.path, router: router, codeString: fetchAsset('drawer.md'), ), Story.asRoute( - name: 'Dropdown', + name: 'Primitives/Dropdown', routePath: DropdownStory.path, router: router, codeString: fetchAsset('dropdown.md'), ), Story.asRoute( - name: 'Icons', + name: 'Primitives/Icons', routePath: IconsStory.path, router: router, codeString: fetchAsset('icons.md'), ), Story.asRoute( - name: 'Loader/LinearLoader', + name: 'Primitives/Loader/LinearLoader', routePath: LinearLoaderStory.path, router: router, codeString: fetchAsset('linear_loader.md'), ), Story.asRoute( - name: 'Progress/LinearProgress', + name: 'Primitives/Progress/LinearProgress', routePath: LinearProgressStory.path, router: router, codeString: fetchAsset('linear_progress.md'), ), Story.asRoute( - name: 'MenuItem', + name: 'Primitives/MenuItem', routePath: MenuItemStory.path, router: router, codeString: fetchAsset('menu_item.md'), ), Story.asRoute( - name: 'Modal', + name: 'Primitives/Modal', routePath: ModalStory.path, router: router, codeString: fetchAsset('modal.md'), ), Story.asRoute( - name: 'Popover', + name: 'Primitives/Popover', routePath: PopoverStory.path, router: router, codeString: fetchAsset('popover.md'), ), Story.asRoute( - name: 'Radio', + name: 'Primitives/Radio', routePath: RadioStory.path, router: router, codeString: fetchAsset('radio.md'), ), Story.asRoute( - name: 'SegmentedControl', + name: 'Primitives/SegmentedControl', routePath: SegmentedControlStory.path, router: router, codeString: fetchAsset('segmented_control.md'), ), Story.asRoute( - name: 'Switch', + name: 'Primitives/Switch', routePath: SwitchStory.path, router: router, codeString: fetchAsset('switch.md'), ), Story.asRoute( - name: 'TabBar', + name: 'Primitives/TabBar', routePath: TabBarStory.path, router: router, codeString: fetchAsset('tab_bar.md'), ), Story.asRoute( - name: 'Table', + name: 'Primitives/Table', routePath: TableStory.path, router: router, codeString: fetchAsset('table.md'), ), Story.asRoute( - name: 'Tag', + name: 'Primitives/Tag', routePath: TagStory.path, router: router, codeString: fetchAsset('tag.md'), ), Story.asRoute( - name: 'TextArea', + name: 'Primitives/TextArea', routePath: TextAreaStory.path, router: router, codeString: fetchAsset('text_area.md'), ), Story.asRoute( - name: 'TextInput', + name: 'Primitives/TextInput', routePath: TextInputStory.path, router: router, codeString: fetchAsset('text_input.md'), ), Story.asRoute( - name: 'TextInputGroup', + name: 'Primitives/TextInputGroup', routePath: TextInputGroupStory.path, router: router, codeString: fetchAsset('text_input_group.md'), ), Story.asRoute( - name: 'Toast', + name: 'Primitives/Toast', routePath: ToastStory.path, router: router, codeString: fetchAsset('toast.md'), ), Story.asRoute( - name: 'Tooltip', + name: 'Primitives/Tooltip', routePath: TooltipStory.path, router: router, codeString: fetchAsset('tooltip.md'), diff --git a/example/lib/src/storybook/stories/accordion.dart b/example/lib/src/storybook/stories/accordion.dart index 74e0a952..a1d88001 100644 --- a/example/lib/src/storybook/stories/accordion.dart +++ b/example/lib/src/storybook/stories/accordion.dart @@ -7,7 +7,7 @@ import 'package:storybook_flutter/storybook_flutter.dart'; enum AccordionItems { first, second } class AccordionStory extends StatefulWidget { - static const path = '/accordion'; + static const path = '/primitives/accordion'; const AccordionStory({super.key}); diff --git a/example/lib/src/storybook/stories/alert.dart b/example/lib/src/storybook/stories/alert.dart index 22b38e01..054e360f 100644 --- a/example/lib/src/storybook/stories/alert.dart +++ b/example/lib/src/storybook/stories/alert.dart @@ -5,7 +5,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class AlertStory extends StatefulWidget { - static const path = '/alert'; + static const path = '/primitives/alert'; const AlertStory({super.key}); diff --git a/example/lib/src/storybook/stories/auth_code.dart b/example/lib/src/storybook/stories/auth_code.dart index 5c2a91f8..abc2072d 100644 --- a/example/lib/src/storybook/stories/auth_code.dart +++ b/example/lib/src/storybook/stories/auth_code.dart @@ -5,7 +5,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class AuthCodeStory extends StatelessWidget { - static const path = '/auth_code'; + static const path = '/primitives/auth_code'; const AuthCodeStory({super.key}); diff --git a/example/lib/src/storybook/stories/avatar.dart b/example/lib/src/storybook/stories/avatar.dart index a8f6ad53..7706e773 100644 --- a/example/lib/src/storybook/stories/avatar.dart +++ b/example/lib/src/storybook/stories/avatar.dart @@ -5,7 +5,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class AvatarStory extends StatelessWidget { - static const path = '/avatar'; + static const path = '/primitives/avatar'; const AvatarStory({super.key}); @@ -133,7 +133,7 @@ class AvatarStory extends StatelessWidget { ), ), ), - const TextDivider(text: "Customized MoonAvatar with image background"), + const TextDivider(text: "Custom MoonAvatar with image background"), MoonAvatar( avatarSize: avatarSizeKnob, badgeSize: badgeSizeKnob?.toDouble(), diff --git a/example/lib/src/storybook/stories/bottom_sheet.dart b/example/lib/src/storybook/stories/bottom_sheet.dart index e98c0039..0d7e23f1 100644 --- a/example/lib/src/storybook/stories/bottom_sheet.dart +++ b/example/lib/src/storybook/stories/bottom_sheet.dart @@ -4,7 +4,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class BottomSheetStory extends StatelessWidget { - static const path = '/bottom_sheet'; + static const path = '/primitives/bottom_sheet'; const BottomSheetStory({super.key}); diff --git a/example/lib/src/storybook/stories/breadcrumb.dart b/example/lib/src/storybook/stories/breadcrumb.dart index b22e15fe..a99858ef 100644 --- a/example/lib/src/storybook/stories/breadcrumb.dart +++ b/example/lib/src/storybook/stories/breadcrumb.dart @@ -6,7 +6,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class BreadcrumbStory extends StatefulWidget { - static const path = '/breadcrumb'; + static const path = '/primitives/breadcrumb'; const BreadcrumbStory({super.key}); @@ -146,7 +146,7 @@ class _BreadcrumbStoryState extends State { ), ], ), - const TextDivider(text: "Customized MoonBreadcrumb with MoonDropdown"), + const TextDivider(text: "Custom MoonBreadcrumb with MoonDropdown"), StatefulBuilder( builder: (context, setState) { return MoonBreadcrumb( diff --git a/example/lib/src/storybook/stories/button.dart b/example/lib/src/storybook/stories/button.dart index cb256580..6d43df89 100644 --- a/example/lib/src/storybook/stories/button.dart +++ b/example/lib/src/storybook/stories/button.dart @@ -5,7 +5,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class ButtonStory extends StatelessWidget { - static const path = '/button'; + static const path = '/primitives/button'; const ButtonStory({super.key}); @@ -204,7 +204,7 @@ class ButtonStory extends StatelessWidget { label: showLabelKnob ? const Text("MoonTextButton") : null, trailing: showTrailingKnob ? Icon(resolvedIconVariant) : null, ), - const TextDivider(text: "Customized MoonButtons with non-standard children"), + const TextDivider(text: "Custom MoonButtons with non-standard children"), MoonButton( onTap: isDisabledKnob ? null : () {}, height: 40, diff --git a/example/lib/src/storybook/stories/carousel.dart b/example/lib/src/storybook/stories/carousel.dart index 8014aec8..8d31bab8 100644 --- a/example/lib/src/storybook/stories/carousel.dart +++ b/example/lib/src/storybook/stories/carousel.dart @@ -4,7 +4,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class CarouselStory extends StatefulWidget { - static const path = '/carousel'; + static const path = '/primitives/carousel'; const CarouselStory({super.key}); @@ -108,7 +108,7 @@ class _CarouselStoryState extends State { ), ), ), - const TextDivider(text: "Customized MoonCarousel with extras"), + const TextDivider(text: "Custom MoonCarousel with extras"), Column( children: [ SizedBox( diff --git a/example/lib/src/storybook/stories/checkbox.dart b/example/lib/src/storybook/stories/checkbox.dart index e439974a..4515ddfa 100644 --- a/example/lib/src/storybook/stories/checkbox.dart +++ b/example/lib/src/storybook/stories/checkbox.dart @@ -6,7 +6,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class CheckboxStory extends StatefulWidget { - static const path = '/checkbox'; + static const path = '/primitives/checkbox'; const CheckboxStory({super.key}); diff --git a/example/lib/src/storybook/stories/chip.dart b/example/lib/src/storybook/stories/chip.dart index dac270b9..02444252 100644 --- a/example/lib/src/storybook/stories/chip.dart +++ b/example/lib/src/storybook/stories/chip.dart @@ -5,7 +5,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class ChipStory extends StatelessWidget { - static const path = '/chip'; + static const path = '/primitives/chip'; const ChipStory({super.key}); @@ -156,7 +156,7 @@ class ChipStory extends StatelessWidget { label: showLabelKnob ? Text(customLabelTextKnob) : null, trailing: showTrailingKnob ? const Icon(MoonIcons.other_frame_24_light) : null, ), - const TextDivider(text: "Customized MoonChip"), + const TextDivider(text: "Custom MoonChip"), MoonChip( isActive: isActiveKnob, activeColor: context.moonColors!.dodoria, diff --git a/example/lib/src/storybook/stories/circular_loader.dart b/example/lib/src/storybook/stories/circular_loader.dart index 1c4287f4..f95fc6a9 100644 --- a/example/lib/src/storybook/stories/circular_loader.dart +++ b/example/lib/src/storybook/stories/circular_loader.dart @@ -5,8 +5,7 @@ import 'package:storybook_flutter/storybook_flutter.dart'; class CircularLoaderStory extends StatelessWidget { static const subdirectory = '/loader'; - static const path = '/loader/circular_loader'; - static const segment = 'circular_loader'; + static const path = '/primitives/loader/circular_loader'; const CircularLoaderStory({super.key}); diff --git a/example/lib/src/storybook/stories/circular_progress.dart b/example/lib/src/storybook/stories/circular_progress.dart index 084c9a11..251bedb4 100644 --- a/example/lib/src/storybook/stories/circular_progress.dart +++ b/example/lib/src/storybook/stories/circular_progress.dart @@ -5,8 +5,7 @@ import 'package:storybook_flutter/storybook_flutter.dart'; class CircularProgressStory extends StatelessWidget { static const subdirectory = '/progress'; - static const path = '/progress/circular_progress'; - static const segment = 'circular_progress'; + static const path = '/primitives/progress/circular_progress'; const CircularProgressStory({super.key}); diff --git a/example/lib/src/storybook/stories/combobox.dart b/example/lib/src/storybook/stories/combobox.dart new file mode 100644 index 00000000..3d2cbd41 --- /dev/null +++ b/example/lib/src/storybook/stories/combobox.dart @@ -0,0 +1,418 @@ +import 'package:example/src/storybook/common/color_options.dart'; +import 'package:example/src/storybook/common/widgets/text_divider.dart'; +import 'package:flutter/material.dart'; +import 'package:moon_design/moon_design.dart'; +import 'package:storybook_flutter/storybook_flutter.dart'; + +enum Options { + accordion, + alert, + bottomSheet, + button, + carousel, + chip, + dropdown, + menuItem, + tag, + tooltip, +} + +extension OptionsX on Options { + String get name { + return switch (this) { + Options.accordion => "Accordion", + Options.alert => "Alert", + Options.bottomSheet => "BottomSheet", + Options.button => "Button", + Options.carousel => "Carousel", + Options.chip => "Chip", + Options.dropdown => "Dropdown", + Options.menuItem => "MenuItem", + Options.tag => "Tag", + Options.tooltip => "Tooltip" + }; + } +} + +class ComboboxStory extends StatefulWidget { + static const path = '/composites/combobox'; + + const ComboboxStory({super.key}); + + @override + State createState() => _ComboboxStoryState(); +} + +class _ComboboxStoryState extends State { + final Map _optionsList = { + Options.accordion: false, + Options.alert: false, + Options.bottomSheet: false, + Options.button: false, + Options.carousel: false, + Options.chip: false, + Options.dropdown: false, + Options.menuItem: false, + Options.tag: false, + Options.tooltip: false, + }; + + final FocusNode _singleSelectFocusNode = FocusNode(); + final FocusNode _multiSelectFocusNode = FocusNode(); + + final TextEditingController _singleSelectSearchController = TextEditingController(); + final TextEditingController _multiSelectSearchController = TextEditingController(); + + final Map _multiSelectedItems = {}; + + Map _singleSelectFilteredData = {}; + Map _multiSelectFilteredData = {}; + + bool _showSingleSelectDropdown = false; + bool _showMultiSelectDropdown = false; + + String get _singleSelectInputValue => _singleSelectSearchController.text.trim().toLowerCase(); + + String get _multiSelectInputValue => _multiSelectSearchController.text.trim().toLowerCase(); + + // Single select. + void _performSingleSelectSearch() { + setState(() { + _singleSelectFilteredData = Map.fromEntries( + _optionsList.entries.where((entry) => entry.key.name.toLowerCase().contains(_singleSelectInputValue)).toList(), + ); + _showSingleSelectDropdown = true; + }); + } + + void _handleSingleSelect(Options item) { + setState(() { + _showSingleSelectDropdown = false; + _singleSelectSearchController.text = item.name; + _singleSelectFocusNode.unfocus(); + }); + } + + void _showSingleSelectAllOptionsList() { + setState(() { + _singleSelectFilteredData = _optionsList; + _showSingleSelectDropdown = !_showSingleSelectDropdown; + }); + } + + void _handleSingleSelectDropdownTapOutside() { + final bool isOptionSelected = + _optionsList.entries.any((item) => item.key.name.toLowerCase() == _singleSelectInputValue); + + setState(() { + _showSingleSelectDropdown = false; + if (isOptionSelected) _singleSelectSearchController.clear(); + _singleSelectFocusNode.unfocus(); + }); + } + + void _handleSingleSelectInputTapOutside() { + if (_singleSelectFocusNode.hasFocus && !_showSingleSelectDropdown) { + _singleSelectSearchController.clear(); + _singleSelectFocusNode.unfocus(); + } + } + + // Multi select. + void _performMultiSelectSearch() { + setState(() { + _multiSelectFilteredData = Map.fromEntries( + _optionsList.entries.where((entry) => entry.key.name.toLowerCase().contains(_multiSelectInputValue)).toList(), + ); + _showMultiSelectDropdown = true; + }); + } + + void _handleMultiSelect(Options item, bool isSelected) { + setState(() => isSelected ? _multiSelectedItems[item] = true : _multiSelectedItems.remove(item)); + } + + void _showMultiSelectAllOptionsList() { + setState(() { + _multiSelectFilteredData = _optionsList; + _showMultiSelectDropdown = !_showMultiSelectDropdown; + }); + } + + void _handleMultiSelectDropdownTapOutside() { + setState(() { + _showMultiSelectDropdown = false; + _multiSelectSearchController.clear(); + _multiSelectFocusNode.unfocus(); + }); + } + + void _handleMultiSelectInputTapOutside() { + if (_multiSelectFocusNode.hasFocus && !_showMultiSelectDropdown) { + _multiSelectSearchController.clear(); + _multiSelectFocusNode.unfocus(); + } + } + + @override + void dispose() { + _singleSelectSearchController.dispose(); + _multiSelectSearchController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final comboboxInputSizeKnob = context.knobs.nullable.options( + label: "textInputSize", + description: "Size variants for MoonTextInput.", + enabled: false, + initial: MoonTextInputSize.md, + options: const [ + Option(label: "sm", value: MoonTextInputSize.sm), + Option(label: "md", value: MoonTextInputSize.md), + Option(label: "lg", value: MoonTextInputSize.lg), + Option(label: "xl", value: MoonTextInputSize.xl), + ], + ); + + final activeBorderColorKnob = context.knobs.nullable.options( + label: "activeBorderColor", + description: "MoonColors variants for MoonTextInput active border.", + enabled: false, + initial: 0, + // piccolo + options: colorOptions, + ); + + final activeBorderColor = colorTable(context)[activeBorderColorKnob ?? 40]; + + final inactiveBorderColorKnob = context.knobs.nullable.options( + label: "inactiveBorderColor", + description: "MoonColors variants for MoonTextInput inactive border.", + enabled: false, + initial: 0, + // piccolo + options: colorOptions, + ); + + final inactiveBorderColor = colorTable(context)[inactiveBorderColorKnob ?? 40]; + + final hoverBorderColorKnob = context.knobs.nullable.options( + label: "hoverBorderColor", + description: "MoonColors variants for MoonTextInput border on hover.", + enabled: false, + initial: 0, + // piccolo + options: colorOptions, + ); + + final hoverBorderColor = colorTable(context)[hoverBorderColorKnob ?? 40]; + + final backgroundColorKnob = context.knobs.nullable.options( + label: "backgroundColor", + description: "MoonColors variants for MoonTextInput and MoonDropdown background.", + enabled: false, + initial: 0, + // piccolo + options: colorOptions, + ); + + final backgroundColor = colorTable(context)[backgroundColorKnob ?? 40]; + + final borderRadiusKnob = context.knobs.nullable.sliderInt( + label: "borderRadius", + description: "Border radius for MoonTextInput and MoonDropdown", + enabled: false, + initial: 8, + max: 32, + ); + + final distanceToTargetKnob = context.knobs.nullable.slider( + label: "distanceToTarget", + description: "Distance to target child widget.", + enabled: false, + initial: 8, + max: 100, + ); + + final showShadowKnob = context.knobs.boolean( + label: "Show shadow", + description: "Show shadows for MoonDropdown.", + initial: true, + ); + + final enabledKnob = context.knobs.boolean( + label: "enabled", + description: "Switch between MoonTextInput enabled and disabled states.", + initial: true, + ); + + final hasFloatingLabelKnob = context.knobs.boolean( + label: "hasFloatingLabel", + description: "Whether MoonTextInput has floating label.", + ); + + return Scaffold( + body: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 64.0, horizontal: 16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const TextDivider( + text: "Single select combobox", + paddingTop: 0, + ), + MoonDropdown( + show: _showSingleSelectDropdown && enabledKnob, + constrainWidthToChild: true, + backgroundColor: backgroundColor, + borderRadius: borderRadiusKnob != null ? BorderRadius.circular(borderRadiusKnob.toDouble()) : null, + distanceToTarget: distanceToTargetKnob, + dropdownShadows: showShadowKnob == true ? null : [], + onTapOutside: () => _handleSingleSelectDropdownTapOutside(), + content: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200), + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(scrollbars: false), + child: _singleSelectFilteredData.isEmpty + ? const MoonMenuItem( + label: Text('Nothing found.'), + ) + : ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: _singleSelectFilteredData.length, + itemBuilder: (BuildContext _, int index) { + final List keys = _singleSelectFilteredData.keys.toList(); + if (index >= keys.length) return const SizedBox.shrink(); + final Options item = keys[index]; + + return MoonMenuItem( + onTap: () => _handleSingleSelect(item), + label: Text(item.name), + ); + }, + ), + ), + ), + child: MoonTextInput( + enabled: enabledKnob, + hasFloatingLabel: hasFloatingLabelKnob, + width: 270, + focusNode: _singleSelectFocusNode, + activeBorderColor: activeBorderColor, + inactiveBorderColor: inactiveBorderColor, + backgroundColor: backgroundColor, + hoverBorderColor: hoverBorderColor, + borderRadius: borderRadiusKnob != null ? BorderRadius.circular(borderRadiusKnob.toDouble()) : null, + textInputSize: comboboxInputSizeKnob, + hintText: "Select single component", + controller: _singleSelectSearchController, + onTap: () => _performSingleSelectSearch(), + onTapOutside: (PointerDownEvent _) => _handleSingleSelectInputTapOutside(), + onChanged: (String _) => _performSingleSelectSearch(), + trailing: MoonButton.icon( + hoverEffectColor: Colors.transparent, + onTap: () => _showSingleSelectAllOptionsList(), + icon: AnimatedRotation( + duration: const Duration(milliseconds: 200), + turns: _showSingleSelectDropdown ? -0.5 : 0, + child: const Icon(MoonIcons.controls_chevron_down_small_24_light), + ), + ), + ), + ), + const TextDivider(text: "Multi select combobox"), + MoonDropdown( + show: _showMultiSelectDropdown && enabledKnob, + constrainWidthToChild: true, + backgroundColor: backgroundColor, + borderRadius: borderRadiusKnob != null ? BorderRadius.circular(borderRadiusKnob.toDouble()) : null, + distanceToTarget: distanceToTargetKnob, + dropdownShadows: showShadowKnob == true ? null : [], + onTapOutside: () => _handleMultiSelectDropdownTapOutside(), + content: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200), + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(scrollbars: false), + child: _multiSelectFilteredData.isEmpty + ? const MoonMenuItem( + label: Text('Nothing found.'), + ) + : ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: _multiSelectFilteredData.length, + itemBuilder: (BuildContext _, int index) { + final List keys = _multiSelectFilteredData.keys.toList(); + if (index >= keys.length) return const SizedBox.shrink(); + final bool isSelected = _multiSelectedItems.containsKey(keys[index]); + + return MoonMenuItem( + absorbGestures: true, + onTap: () => _handleMultiSelect(keys[index], !isSelected), + label: Text(keys[index].name), + trailing: MoonCheckbox( + value: isSelected, + tapAreaSizeValue: 0, + onChanged: (_) {}, + ), + ); + }, + ), + ), + ), + child: MoonTextInput( + enabled: enabledKnob, + hasFloatingLabel: hasFloatingLabelKnob, + width: 270, + focusNode: _multiSelectFocusNode, + textInputSize: comboboxInputSizeKnob, + activeBorderColor: activeBorderColor, + inactiveBorderColor: inactiveBorderColor, + backgroundColor: backgroundColor, + hoverBorderColor: hoverBorderColor, + borderRadius: borderRadiusKnob != null ? BorderRadius.circular(borderRadiusKnob.toDouble()) : null, + hintText: "Select multiple components", + controller: _multiSelectSearchController, + onTap: () => _performMultiSelectSearch(), + onTapOutside: (PointerDownEvent _) => _handleMultiSelectInputTapOutside(), + onChanged: (String _) => _performMultiSelectSearch(), + leading: _multiSelectedItems.isNotEmpty + ? Center( + child: MoonTag( + tagSize: MoonTagSize.xs, + backgroundColor: context.moonColors!.bulma, + onTap: () => setState(() => _multiSelectedItems.clear()), + label: Text( + "${_multiSelectedItems.keys.length}", + style: TextStyle(color: context.moonColors!.gohan), + ), + trailing: Icon( + MoonIcons.controls_close_small_16_light, + color: context.moonColors!.gohan, + ), + ), + ) + : null, + trailing: MoonButton.icon( + hoverEffectColor: Colors.transparent, + onTap: () => _showMultiSelectAllOptionsList(), + icon: AnimatedRotation( + duration: const Duration(milliseconds: 200), + turns: _showMultiSelectDropdown ? -0.5 : 0, + child: const Icon(MoonIcons.controls_chevron_down_small_24_light), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/src/storybook/stories/dot_indicator.dart b/example/lib/src/storybook/stories/dot_indicator.dart index 20b059eb..c6ba9ede 100644 --- a/example/lib/src/storybook/stories/dot_indicator.dart +++ b/example/lib/src/storybook/stories/dot_indicator.dart @@ -4,7 +4,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class DotIndicatorStory extends StatefulWidget { - static const path = '/dot_indicator'; + static const path = '/primitives/dot_indicator'; const DotIndicatorStory({super.key}); diff --git a/example/lib/src/storybook/stories/drawer.dart b/example/lib/src/storybook/stories/drawer.dart index 41fbf63d..d2c7b7aa 100644 --- a/example/lib/src/storybook/stories/drawer.dart +++ b/example/lib/src/storybook/stories/drawer.dart @@ -4,7 +4,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class DrawerStory extends StatelessWidget { - static const path = '/drawer'; + static const path = '/primitives/drawer'; const DrawerStory({super.key}); diff --git a/example/lib/src/storybook/stories/dropdown.dart b/example/lib/src/storybook/stories/dropdown.dart index 937389fc..5006c182 100644 --- a/example/lib/src/storybook/stories/dropdown.dart +++ b/example/lib/src/storybook/stories/dropdown.dart @@ -10,19 +10,16 @@ enum Choices { first, second, third } extension ChoicesX on Choices { String get name { - switch (this) { - case Choices.first: - return "Choice #1"; - case Choices.second: - return "Choice #2"; - case Choices.third: - return "Choice #3"; - } + return switch (this) { + Choices.first => "Choice #1", + Choices.second => "Choice #2", + Choices.third => "Choice #3", + }; } } class DropdownStory extends StatefulWidget { - static const path = '/dropdown'; + static const path = '/primitives/dropdown'; const DropdownStory({super.key}); @@ -125,9 +122,12 @@ class _DropdownStoryState extends State { padding: const EdgeInsets.symmetric(vertical: 64.0, horizontal: 16.0), child: Center( child: Column( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ - const TextDivider(text: "MoonDropdown with multiple choices"), + const TextDivider( + text: "MoonDropdown with multiple choices", + paddingTop: 0, + ), MoonDropdown( show: _showChoices, minWidth: 250, @@ -151,7 +151,6 @@ class _DropdownStoryState extends State { onChanged: (_) {}, ), ), - const SizedBox(height: 4), MoonMenuItem( absorbGestures: true, onTap: () => @@ -163,7 +162,6 @@ class _DropdownStoryState extends State { onChanged: (_) {}, ), ), - const SizedBox(height: 4), MoonMenuItem( absorbGestures: true, onTap: () => setState(() => _availableChoices[Choices.third] = !_availableChoices[Choices.third]!), @@ -177,43 +175,42 @@ class _DropdownStoryState extends State { ], ), child: MoonTextInput( - mouseCursor: MouseCursor.defer, + width: 270, readOnly: true, - width: 250, + canRequestFocus: false, + mouseCursor: MouseCursor.defer, hintText: "Choose an option", onTap: () => setState(() => _showChoices = !_showChoices), + onTapOutside: (PointerDownEvent _) => FocusManager.instance.primaryFocus?.unfocus(), leading: _availableChoices.values.any((element) => element == true) ? Center( - child: GestureDetector( + child: MoonTag( + tagSize: MoonTagSize.xs, + backgroundColor: context.moonColors!.bulma, onTap: () => setState(() => _availableChoices.updateAll((key, value) => false)), - child: MoonTag( - tagSize: MoonTagSize.xs, - backgroundColor: context.moonColors!.bulma, - label: Text( - "${_availableChoices.values.where((element) => element == true).length}", - style: TextStyle(color: context.moonColors!.gohan), - ), - trailing: Icon( - MoonIcons.controls_close_small_16_light, - color: context.moonColors!.gohan, - ), + label: Text( + "${_availableChoices.values.where((element) => element == true).length}", + style: TextStyle(color: context.moonColors!.gohan), + ), + trailing: Icon( + MoonIcons.controls_close_small_16_light, + color: context.moonColors!.gohan, ), ), ) : null, - trailing: Center( - child: AnimatedRotation( - duration: const Duration(milliseconds: 200), - turns: _showChoices ? -0.5 : 0, - child: const Icon( - MoonIcons.controls_chevron_down_small_16_light, - size: 16, + trailing: MouseRegion( + cursor: SystemMouseCursors.click, + child: Center( + child: AnimatedRotation( + duration: const Duration(milliseconds: 200), + turns: _showChoices ? -0.5 : 0, + child: const Icon(MoonIcons.controls_chevron_down_small_24_light), ), ), ), ), ), - const SizedBox(height: 32), const TextDivider(text: "MoonDropdown as a menu"), MoonDropdown( show: _showMenu, @@ -233,7 +230,6 @@ class _DropdownStoryState extends State { content: Column( children: [ MoonMenuItem( - borderRadius: const MoonSquircleBorderRadius.all(MoonSquircleRadius(cornerRadius: 12)), onTap: () => setState(() { _showMenu = false; _buttonName = "Piccolo"; @@ -243,7 +239,6 @@ class _DropdownStoryState extends State { ), const SizedBox(height: 4), MoonMenuItem( - borderRadius: const MoonSquircleBorderRadius.all(MoonSquircleRadius(cornerRadius: 12)), onTap: () => setState(() { _showMenu = false; _buttonName = "Krillin"; @@ -265,7 +260,6 @@ class _DropdownStoryState extends State { content: Column( children: [ MoonMenuItem( - borderRadius: const MoonSquircleBorderRadius.all(MoonSquircleRadius(cornerRadius: 12)), onTap: () => setState(() { _showMenu = false; _showMenuInner = false; @@ -276,7 +270,6 @@ class _DropdownStoryState extends State { ), const SizedBox(height: 4), MoonMenuItem( - borderRadius: const MoonSquircleBorderRadius.all(MoonSquircleRadius(cornerRadius: 12)), onTap: () => setState(() { _showMenu = false; _showMenuInner = false; @@ -287,7 +280,6 @@ class _DropdownStoryState extends State { ), const SizedBox(height: 4), MoonMenuItem( - borderRadius: const MoonSquircleBorderRadius.all(MoonSquircleRadius(cornerRadius: 12)), onTap: () => setState(() { _showMenu = false; _showMenuInner = false; @@ -300,10 +292,7 @@ class _DropdownStoryState extends State { ), child: MoonMenuItem( backgroundColor: _showMenuInner ? context.moonColors!.heles : null, - borderRadius: const MoonSquircleBorderRadius.all(MoonSquircleRadius(cornerRadius: 12)), - onTap: () => setState(() { - _showMenuInner = !_showMenuInner; - }), + onTap: () => setState(() => _showMenuInner = !_showMenuInner), label: const Text("Roshi"), trailing: const Icon( MoonIcons.controls_chevron_right_16_light, diff --git a/example/lib/src/storybook/stories/icons.dart b/example/lib/src/storybook/stories/icons.dart index 3b73cd02..f3fa2e8b 100644 --- a/example/lib/src/storybook/stories/icons.dart +++ b/example/lib/src/storybook/stories/icons.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:moon_design/moon_design.dart'; class IconsStory extends StatefulWidget { - static const path = '/icons'; + static const path = '/primitives/icons'; const IconsStory({super.key}); diff --git a/example/lib/src/storybook/stories/linear_loader.dart b/example/lib/src/storybook/stories/linear_loader.dart index 66c0a373..ec3f86b5 100644 --- a/example/lib/src/storybook/stories/linear_loader.dart +++ b/example/lib/src/storybook/stories/linear_loader.dart @@ -4,7 +4,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class LinearLoaderStory extends StatelessWidget { - static const path = '/loader/linear_loader'; + static const path = '/primitives/loader/linear_loader'; const LinearLoaderStory({super.key}); diff --git a/example/lib/src/storybook/stories/linear_progress.dart b/example/lib/src/storybook/stories/linear_progress.dart index 6e74752b..115e17be 100644 --- a/example/lib/src/storybook/stories/linear_progress.dart +++ b/example/lib/src/storybook/stories/linear_progress.dart @@ -4,7 +4,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class LinearProgressStory extends StatelessWidget { - static const path = '/progress/linear_progress'; + static const path = '/primitives/progress/linear_progress'; const LinearProgressStory({super.key}); diff --git a/example/lib/src/storybook/stories/menu_item.dart b/example/lib/src/storybook/stories/menu_item.dart index abed28a6..ad24be24 100644 --- a/example/lib/src/storybook/stories/menu_item.dart +++ b/example/lib/src/storybook/stories/menu_item.dart @@ -7,7 +7,7 @@ import 'package:storybook_flutter/storybook_flutter.dart'; enum MenuItem { first, second } class MenuItemStory extends StatefulWidget { - static const path = '/menu_item'; + static const path = '/primitives/menu_item'; const MenuItemStory({super.key}); diff --git a/example/lib/src/storybook/stories/modal.dart b/example/lib/src/storybook/stories/modal.dart index 1b674344..61ee5ba8 100644 --- a/example/lib/src/storybook/stories/modal.dart +++ b/example/lib/src/storybook/stories/modal.dart @@ -4,7 +4,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class ModalStory extends StatelessWidget { - static const path = '/modal'; + static const path = '/primitives/modal'; const ModalStory({super.key}); diff --git a/example/lib/src/storybook/stories/popover.dart b/example/lib/src/storybook/stories/popover.dart index a3520911..2791cd89 100644 --- a/example/lib/src/storybook/stories/popover.dart +++ b/example/lib/src/storybook/stories/popover.dart @@ -4,7 +4,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class PopoverStory extends StatefulWidget { - static const path = '/popover'; + static const path = '/primitives/popover'; const PopoverStory({super.key}); diff --git a/example/lib/src/storybook/stories/radio.dart b/example/lib/src/storybook/stories/radio.dart index 823f99c2..e6127765 100644 --- a/example/lib/src/storybook/stories/radio.dart +++ b/example/lib/src/storybook/stories/radio.dart @@ -10,7 +10,7 @@ enum ChoiceCustom { first, second } enum ChoiceLabel { first, second } class RadioStory extends StatefulWidget { - static const path = '/radio'; + static const path = '/primitives/radio'; const RadioStory({super.key}); diff --git a/example/lib/src/storybook/stories/segmented_control.dart b/example/lib/src/storybook/stories/segmented_control.dart index b8a2711b..933f0946 100644 --- a/example/lib/src/storybook/stories/segmented_control.dart +++ b/example/lib/src/storybook/stories/segmented_control.dart @@ -5,7 +5,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class SegmentedControlStory extends StatelessWidget { - static const path = '/segmented_control'; + static const path = '/primitives/segmented_control'; const SegmentedControlStory({super.key}); diff --git a/example/lib/src/storybook/stories/switch.dart b/example/lib/src/storybook/stories/switch.dart index ef707671..c0a5b01e 100644 --- a/example/lib/src/storybook/stories/switch.dart +++ b/example/lib/src/storybook/stories/switch.dart @@ -6,7 +6,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class SwitchStory extends StatefulWidget { - static const path = '/switch'; + static const path = '/primitives/switch'; const SwitchStory({super.key}); @@ -88,7 +88,7 @@ class _SwitchStoryState extends State { value: switchValue, onChanged: isDisabled ? null : (bool newValue) => setState(() => switchValue = newValue), ), - const TextDivider(text: "Customized MoonSwitch"), + const TextDivider(text: "Custom MoonSwitch"), MoonSwitch( switchSize: switchSizeKnob, activeThumbWidget: const Icon(MoonIcons.generic_check_alternative_16_light), diff --git a/example/lib/src/storybook/stories/tab_bar.dart b/example/lib/src/storybook/stories/tab_bar.dart index 4f64f72a..9f4b48fa 100644 --- a/example/lib/src/storybook/stories/tab_bar.dart +++ b/example/lib/src/storybook/stories/tab_bar.dart @@ -5,7 +5,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class TabBarStory extends StatefulWidget { - static const path = '/tab_bar'; + static const path = '/primitives/tab_bar'; const TabBarStory({super.key}); @@ -226,7 +226,7 @@ class _TabBarStoryState extends State with SingleTickerProviderStat ), ], ), - const TextDivider(text: "Customized MoonTabBar with TabBarView"), + const TextDivider(text: "Custom MoonTabBar with TabBarView"), MoonTabBar( isExpanded: true, tabController: tabController, diff --git a/example/lib/src/storybook/stories/table.dart b/example/lib/src/storybook/stories/table.dart index 5d4e5eee..386dfc35 100644 --- a/example/lib/src/storybook/stories/table.dart +++ b/example/lib/src/storybook/stories/table.dart @@ -8,7 +8,7 @@ import 'package:storybook_flutter/storybook_flutter.dart'; const int _rowsPerPage = 25; class TableStory extends StatefulWidget { - static const path = '/table'; + static const path = '/primitives/table'; const TableStory({super.key}); diff --git a/example/lib/src/storybook/stories/tag.dart b/example/lib/src/storybook/stories/tag.dart index a9a0d4eb..7375ec2d 100644 --- a/example/lib/src/storybook/stories/tag.dart +++ b/example/lib/src/storybook/stories/tag.dart @@ -4,7 +4,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class TagStory extends StatelessWidget { - static const path = '/tag'; + static const path = '/primitives/tag'; const TagStory({super.key}); diff --git a/example/lib/src/storybook/stories/text_area.dart b/example/lib/src/storybook/stories/text_area.dart index 2ecfd888..6d9b00d2 100644 --- a/example/lib/src/storybook/stories/text_area.dart +++ b/example/lib/src/storybook/stories/text_area.dart @@ -4,7 +4,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class TextAreaStory extends StatelessWidget { - static const path = '/text_area'; + static const path = '/primitives/text_area'; const TextAreaStory({super.key}); @@ -124,6 +124,7 @@ class TextAreaStory extends StatelessWidget { validator: (String? value) => value?.length != null && value!.length < 10 ? "The text should be longer than 10 characters." : null, + onTapOutside: (PointerDownEvent _) => FocusManager.instance.primaryFocus?.unfocus(), helper: showHelperKnob ? const Text("Supporting text") : null, ), const SizedBox(height: 32), diff --git a/example/lib/src/storybook/stories/text_input.dart b/example/lib/src/storybook/stories/text_input.dart index 373a6730..12daa4d7 100644 --- a/example/lib/src/storybook/stories/text_input.dart +++ b/example/lib/src/storybook/stories/text_input.dart @@ -4,7 +4,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class TextInputStory extends StatefulWidget { - static const path = '/text_input'; + static const path = '/primitives/text_input'; const TextInputStory({super.key}); @@ -159,7 +159,6 @@ class _TextInputStoryState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ MoonFormTextInput( - hoverBorderColor: hoverBorderColor, controller: _textController, enabled: enabledKnob, textInputSize: textInputSizeKnob, @@ -169,11 +168,13 @@ class _TextInputStoryState extends State { backgroundColor: backgroundColor, activeBorderColor: activeBorderColor, inactiveBorderColor: inactiveBorderColor, + hoverBorderColor: hoverBorderColor, errorColor: errorColor, borderRadius: borderRadius, hintText: "Enter text (over 10 characters)", validator: (String? value) => value != null && value.length < 10 ? "The text should be longer than 10 characters." : null, + onTapOutside: (PointerDownEvent _) => FocusManager.instance.primaryFocus?.unfocus(), leading: showLeadingKnob ? const Icon( MoonIcons.generic_search_24_light, @@ -209,10 +210,12 @@ class _TextInputStoryState extends State { backgroundColor: backgroundColor, activeBorderColor: activeBorderColor, inactiveBorderColor: inactiveBorderColor, + hoverBorderColor: hoverBorderColor, errorColor: errorColor, borderRadius: borderRadius, hintText: "Enter password (123abc)", validator: (String? value) => value != "123abc" ? "Wrong password." : null, + onTapOutside: (PointerDownEvent _) => FocusManager.instance.primaryFocus?.unfocus(), leading: showLeadingKnob ? const Icon( MoonIcons.security_password_24_light, @@ -245,7 +248,6 @@ class _TextInputStoryState extends State { const SizedBox(height: 16), MoonFormTextInput( readOnly: true, - hoverBorderColor: hoverBorderColor, controller: _dateController, enabled: enabledKnob, textInputSize: textInputSizeKnob, @@ -254,6 +256,7 @@ class _TextInputStoryState extends State { backgroundColor: backgroundColor, activeBorderColor: activeBorderColor, inactiveBorderColor: inactiveBorderColor, + hoverBorderColor: hoverBorderColor, errorColor: errorColor, borderRadius: borderRadius, hintText: "Pick a date", diff --git a/example/lib/src/storybook/stories/text_input_group.dart b/example/lib/src/storybook/stories/text_input_group.dart index aa6c401b..e384e580 100644 --- a/example/lib/src/storybook/stories/text_input_group.dart +++ b/example/lib/src/storybook/stories/text_input_group.dart @@ -4,7 +4,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class TextInputGroupStory extends StatefulWidget { - static const path = '/text_input_group'; + static const path = '/primitives/text_input_group'; const TextInputGroupStory({super.key}); @@ -169,6 +169,7 @@ class _TextInputGroupStoryState extends State { validator: (String? value) => value?.length != null && value!.length < 10 ? "The text should be longer than 10 characters." : null, + onTapOutside: (PointerDownEvent _) => FocusManager.instance.primaryFocus?.unfocus(), leading: const Icon( MoonIcons.generic_search_24_light, size: 24, @@ -198,6 +199,7 @@ class _TextInputGroupStoryState extends State { borderRadius: borderRadius, hintText: "Enter password (123abc)", validator: (String? value) => value != "123abc" ? "Wrong password." : null, + onTapOutside: (PointerDownEvent _) => FocusManager.instance.primaryFocus?.unfocus(), leading: const Icon( MoonIcons.security_password_24_light, size: 24, diff --git a/example/lib/src/storybook/stories/toast.dart b/example/lib/src/storybook/stories/toast.dart index 58dc3443..c48dab6e 100644 --- a/example/lib/src/storybook/stories/toast.dart +++ b/example/lib/src/storybook/stories/toast.dart @@ -4,7 +4,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class ToastStory extends StatelessWidget { - static const path = '/toast'; + static const path = '/primitives/toast'; const ToastStory({super.key}); diff --git a/example/lib/src/storybook/stories/tooltip.dart b/example/lib/src/storybook/stories/tooltip.dart index 85f257bb..5750948d 100644 --- a/example/lib/src/storybook/stories/tooltip.dart +++ b/example/lib/src/storybook/stories/tooltip.dart @@ -5,7 +5,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; class TooltipStory extends StatefulWidget { - static const path = '/tooltip'; + static const path = '/primitives/tooltip'; const TooltipStory({super.key}); diff --git a/example/lib/src/storybook/storybook.dart b/example/lib/src/storybook/storybook.dart index 34cb0c38..0b0a6e18 100644 --- a/example/lib/src/storybook/storybook.dart +++ b/example/lib/src/storybook/storybook.dart @@ -22,14 +22,14 @@ class StorybookPage extends StatelessWidget { @override Widget build(BuildContext context) { return Storybook( - initialStory: "Accordion", + initialStory: "Primitives/Accordion", plugins: _plugins, enableLayout: false, autoLayoutThreshold: autoLayoutThreshold, routeWrapperBuilder: RouteWrapperBuilder( title: "Moon Design for Flutter", theme: ThemeData.light().copyWith( - scaffoldBackgroundColor: const Color(0xfffafafa), // Use old Material2 scaffold background color + scaffoldBackgroundColor: const Color(0xfffafafa), // Use old Material2 scaffold background color. extensions: >[ MoonTheme( tokens: MoonTokens.light.copyWith( diff --git a/lib/src/widgets/bottom_sheet/modal_bottom_sheet.dart b/lib/src/widgets/bottom_sheet/modal_bottom_sheet.dart index 9bc6baa7..dfb3e157 100644 --- a/lib/src/widgets/bottom_sheet/modal_bottom_sheet.dart +++ b/lib/src/widgets/bottom_sheet/modal_bottom_sheet.dart @@ -116,7 +116,7 @@ class MoonModalBottomSheetRoute extends PageRoute { AnimationController? _animationController; - // RoutePopDisposition.pop breaks the bottomsheet drag to close functionality and eventually causes a crash. + // RoutePopDisposition.pop breaks the bottom sheet drag to close functionality and eventually causes a crash. bool get _hasScopedWillPopCallback => popDisposition == RoutePopDisposition.bubble; @override