Skip to content

Commit

Permalink
feat: [MDS-990] Combobox component (#359)
Browse files Browse the repository at this point in the history
Co-authored-by: Birgitt Majas <[email protected]>
  • Loading branch information
GittHub-d and Birgitt Majas authored Feb 23, 2024
1 parent 334176b commit ee2f3b6
Show file tree
Hide file tree
Showing 40 changed files with 891 additions and 166 deletions.
292 changes: 292 additions & 0 deletions example/assets/code_snippets/combobox.md
Original file line number Diff line number Diff line change
@@ -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<ComboboxStory> createState() => _ComboboxStoryState();
}

class _ComboboxStoryState extends State<ComboboxStory> {
final Map<Options, bool> _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<Options, bool> _multiSelectedItems = {};

Map<Options, bool> _singleSelectFilteredData = {};
Map<Options, bool> _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<Options> 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<Options> 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),
),
),
),
),
],
);
}
}
37 changes: 17 additions & 20 deletions example/assets/code_snippets/dropdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};
}
}

Expand Down Expand Up @@ -69,26 +67,25 @@ class _DropdownState extends State<Dropdown> {
],
),
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,
),
),
)
Expand Down
2 changes: 1 addition & 1 deletion example/lib/src/storybook/common/widgets/text_divider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class TextDivider extends StatelessWidget {
const TextDivider({
super.key,
required this.text,
this.paddingTop = 40,
this.paddingTop = 48,
this.paddingBottom = 32,
});

Expand Down
Loading

0 comments on commit ee2f3b6

Please sign in to comment.