diff --git a/example/lib/src/storybook/stories/accordion.dart b/example/lib/src/storybook/stories/accordion.dart new file mode 100644 index 00000000..949476dc --- /dev/null +++ b/example/lib/src/storybook/stories/accordion.dart @@ -0,0 +1,92 @@ +import 'package:example/src/storybook/common/options.dart'; +import 'package:flutter/material.dart'; +import 'package:moon_design/moon_design.dart'; +import 'package:storybook_flutter/storybook_flutter.dart'; + +class AccordionStory extends Story { + AccordionStory() + : super( + name: "Accordion", + builder: (context) { + final accordionSizesKnob = context.knobs.options( + label: "MoonAccordionSize", + description: "Accordion size variants.", + initial: MoonAccordionSize.md, + options: const [ + Option(label: "sm", value: MoonAccordionSize.sm), + Option(label: "md", value: MoonAccordionSize.md), + Option(label: "lg", value: MoonAccordionSize.lg), + Option(label: "xl", value: MoonAccordionSize.xl) + ], + ); + + final backgroundColorsKnob = context.knobs.options( + label: "backgroundColor", + description: "MoonColors variants for Accordion background.", + initial: 4, // gohan + options: colorOptions, + ); + + final backgroundColor = colorTable(context)[backgroundColorsKnob]; + + final expandedBackgroundColorsKnob = context.knobs.options( + label: "expandedBackgroundColor", + description: "MoonColors variants for expanded Accordion background.", + initial: 4, // gohan + options: colorOptions, + ); + + final expandedBackgroundColor = colorTable(context)[expandedBackgroundColorsKnob]; + + final showDividerKnob = context.knobs.boolean( + label: "showDivider", + description: "Show divider in the Accordion.", + initial: true, + ); + + final showBorderKnob = context.knobs.boolean( + label: "showBorder", + description: "Show border around the Accordion.", + ); + + final showShadowKnob = context.knobs.boolean( + label: "Show shadows", + description: "Show shadows under the Accordion.", + initial: true, + ); + + final setRtlModeKnob = context.knobs.boolean( + label: "RTL mode", + description: "Switch between LTR and RTL modes.", + ); + + return Directionality( + textDirection: setRtlModeKnob ? TextDirection.rtl : TextDirection.ltr, + child: Center( + child: SizedBox( + height: 245, + child: Column( + children: [ + MoonAccordion( + accordionSize: accordionSizesKnob, + backgroundColor: backgroundColor, + expandedBackgroundColor: expandedBackgroundColor, + showBorder: showBorderKnob, + showDivider: showDividerKnob, + shadows: showShadowKnob == true ? null : [], + childrenPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + title: const Text("Accordion title"), + children: const [ + Text( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); +} diff --git a/example/lib/src/storybook/stories/tooltip.dart b/example/lib/src/storybook/stories/tooltip.dart index 9b7d500d..a26b210d 100644 --- a/example/lib/src/storybook/stories/tooltip.dart +++ b/example/lib/src/storybook/stories/tooltip.dart @@ -78,7 +78,7 @@ class TooltipStory extends Story { ); final showShadowKnob = context.knobs.boolean( - label: "Show shadow", + label: "Show shadows", description: "Show shadows under the Tooltip.", initial: true, ); diff --git a/example/lib/src/storybook/storybook.dart b/example/lib/src/storybook/storybook.dart index 507cfc9a..9d73e534 100644 --- a/example/lib/src/storybook/storybook.dart +++ b/example/lib/src/storybook/storybook.dart @@ -1,4 +1,5 @@ import 'package:example/src/storybook/common/widgets/version.dart'; +import 'package:example/src/storybook/stories/accordion.dart'; import 'package:example/src/storybook/stories/avatar.dart'; import 'package:example/src/storybook/stories/button.dart'; import 'package:example/src/storybook/stories/checkbox.dart'; @@ -34,7 +35,7 @@ class StorybookPage extends StatelessWidget { return Stack( children: [ Storybook( - initialStory: "Avatar", + initialStory: "Accordion", plugins: _plugins, wrapperBuilder: (context, child) => MaterialApp( title: "Moon Design for Flutter", @@ -66,6 +67,7 @@ class StorybookPage extends StatelessWidget { ), ), stories: [ + AccordionStory(), AvatarStory(), ButtonStory(), CheckboxStory(), diff --git a/lib/moon_design.dart b/lib/moon_design.dart index 33d998c9..9ef92c53 100644 --- a/lib/moon_design.dart +++ b/lib/moon_design.dart @@ -1,5 +1,6 @@ library moon_design; +export 'package:moon_design/src/theme/accordion/accordion_theme.dart'; export 'package:moon_design/src/theme/avatar/avatar_theme.dart'; export 'package:moon_design/src/theme/borders.dart'; export 'package:moon_design/src/theme/button/button_theme.dart'; @@ -26,6 +27,7 @@ export 'package:moon_design/src/utils/extensions.dart'; export 'package:moon_design/src/utils/measure_size.dart'; export 'package:moon_design/src/utils/widget_surveyor.dart'; +export 'package:moon_design/src/widgets/accordion/accordion.dart'; export 'package:moon_design/src/widgets/avatar/avatar.dart'; export 'package:moon_design/src/widgets/buttons/button.dart'; export 'package:moon_design/src/widgets/buttons/ghost_button.dart'; diff --git a/lib/src/theme/accordion/accordion_colors.dart b/lib/src/theme/accordion/accordion_colors.dart new file mode 100644 index 00000000..07adfd81 --- /dev/null +++ b/lib/src/theme/accordion/accordion_colors.dart @@ -0,0 +1,98 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/colors.dart'; + +@immutable +class MoonAccordionColors extends ThemeExtension with DiagnosticableTreeMixin { + static final light = MoonAccordionColors( + backgroundColor: MoonColors.light.gohan, + expandedBackgroundColor: MoonColors.light.gohan, + borderColor: MoonColors.light.beerus, + dividerColor: MoonColors.light.beerus, + expandedIconColor: MoonColors.light.bulma, + iconColor: MoonColors.light.trunks, + ); + + static final dark = MoonAccordionColors( + backgroundColor: MoonColors.dark.gohan, + expandedBackgroundColor: MoonColors.dark.gohan, + borderColor: MoonColors.dark.beerus, + dividerColor: MoonColors.dark.beerus, + iconColor: MoonColors.dark.trunks, + expandedIconColor: MoonColors.dark.bulma, + ); + + /// Accordion background color. + final Color backgroundColor; + + /// Expanded accordion background color. + final Color expandedBackgroundColor; + + /// Accordion border color. + final Color borderColor; + + /// Accordion divider color. + final Color dividerColor; + + /// Accordion icon color. + final Color iconColor; + + /// Expanded accordion icon color. + final Color expandedIconColor; + + const MoonAccordionColors({ + required this.backgroundColor, + required this.expandedBackgroundColor, + required this.borderColor, + required this.dividerColor, + required this.expandedIconColor, + required this.iconColor, + }); + + @override + MoonAccordionColors copyWith({ + Color? backgroundColor, + Color? expandedBackgroundColor, + Color? borderColor, + Color? dividerColor, + Color? expandedIconColor, + Color? iconColor, + }) { + return MoonAccordionColors( + backgroundColor: backgroundColor ?? this.backgroundColor, + expandedBackgroundColor: expandedBackgroundColor ?? this.expandedBackgroundColor, + borderColor: borderColor ?? this.borderColor, + dividerColor: dividerColor ?? this.dividerColor, + expandedIconColor: expandedIconColor ?? this.expandedIconColor, + iconColor: iconColor ?? this.iconColor, + ); + } + + @override + MoonAccordionColors lerp(ThemeExtension? other, double t) { + if (other is! MoonAccordionColors) return this; + + return MoonAccordionColors( + backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t)!, + expandedBackgroundColor: Color.lerp(expandedBackgroundColor, other.expandedBackgroundColor, t)!, + borderColor: Color.lerp(borderColor, other.borderColor, t)!, + dividerColor: Color.lerp(dividerColor, other.dividerColor, t)!, + expandedIconColor: Color.lerp(expandedIconColor, other.expandedIconColor, t)!, + iconColor: Color.lerp(iconColor, other.iconColor, t)!, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonAccordionColors")) + ..add(ColorProperty("backgroundColor", backgroundColor)) + ..add(ColorProperty("expandedBackgroundColor", expandedBackgroundColor)) + ..add(ColorProperty("borderColor", borderColor)) + ..add(ColorProperty("dividerColor", dividerColor)) + ..add(ColorProperty("expandedIconColor", expandedIconColor)) + ..add(ColorProperty("iconColor", iconColor)); + } +} diff --git a/lib/src/theme/accordion/accordion_properties.dart b/lib/src/theme/accordion/accordion_properties.dart new file mode 100644 index 00000000..2437c3cd --- /dev/null +++ b/lib/src/theme/accordion/accordion_properties.dart @@ -0,0 +1,68 @@ +import 'package:figma_squircle/figma_squircle.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/borders.dart'; + +@immutable +class MoonAccordionProperties extends ThemeExtension with DiagnosticableTreeMixin { + static final properties = MoonAccordionProperties( + transitionDuration: const Duration(milliseconds: 200), + transitionCurve: Curves.easeInOutCubic, + borderRadius: SmoothBorderRadius.all( + SmoothRadius( + cornerRadius: MoonBorders.borders.interactiveSm.topLeft.x, + cornerSmoothing: 1, + ), + ), + ); + + /// Accordion transition duration. + final Duration transitionDuration; + + /// Accordion transition curve. + final Curve transitionCurve; + + /// Accordion border radius. + final SmoothBorderRadius borderRadius; + + const MoonAccordionProperties({ + required this.borderRadius, + required this.transitionDuration, + required this.transitionCurve, + }); + + @override + MoonAccordionProperties copyWith({ + Duration? transitionDuration, + Curve? transitionCurve, + SmoothBorderRadius? borderRadius, + }) { + return MoonAccordionProperties( + transitionDuration: transitionDuration ?? this.transitionDuration, + transitionCurve: transitionCurve ?? this.transitionCurve, + borderRadius: borderRadius ?? this.borderRadius, + ); + } + + @override + MoonAccordionProperties lerp(ThemeExtension? other, double t) { + if (other is! MoonAccordionProperties) return this; + + return MoonAccordionProperties( + transitionDuration: lerpDuration(transitionDuration, other.transitionDuration, t), + transitionCurve: other.transitionCurve, + borderRadius: SmoothBorderRadius.lerp(borderRadius, other.borderRadius, t)!, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonAccordionProperties")) + ..add(DiagnosticsProperty("transitionDuration", transitionDuration)) + ..add(DiagnosticsProperty("transitionCurve", transitionCurve)) + ..add(DiagnosticsProperty("borderRadius", borderRadius)); + } +} diff --git a/lib/src/theme/accordion/accordion_shadows.dart b/lib/src/theme/accordion/accordion_shadows.dart new file mode 100644 index 00000000..0462b93e --- /dev/null +++ b/lib/src/theme/accordion/accordion_shadows.dart @@ -0,0 +1,46 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/shadows.dart'; + +@immutable +class MoonAccordionShadows extends ThemeExtension with DiagnosticableTreeMixin { + static final light = MoonAccordionShadows( + accordionShadows: MoonShadows.light.sm, + ); + + static final dark = MoonAccordionShadows( + accordionShadows: MoonShadows.dark.sm, + ); + + /// Accordion shadows. + final List accordionShadows; + + const MoonAccordionShadows({ + required this.accordionShadows, + }); + + @override + MoonAccordionShadows copyWith({List? accordionShadows}) { + return MoonAccordionShadows( + accordionShadows: accordionShadows ?? this.accordionShadows, + ); + } + + @override + MoonAccordionShadows lerp(ThemeExtension? other, double t) { + if (other is! MoonAccordionShadows) return this; + + return MoonAccordionShadows( + accordionShadows: BoxShadow.lerpList(accordionShadows, other.accordionShadows, t)!, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonAccordionShadows")) + ..add(DiagnosticsProperty>("shadows", accordionShadows)); + } +} diff --git a/lib/src/theme/accordion/accordion_size_properties.dart b/lib/src/theme/accordion/accordion_size_properties.dart new file mode 100644 index 00000000..528d6be4 --- /dev/null +++ b/lib/src/theme/accordion/accordion_size_properties.dart @@ -0,0 +1,95 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/sizes.dart'; +import 'package:moon_design/src/theme/typography/text_styles.dart'; + +@immutable +class MoonAccordionSizeProperties extends ThemeExtension with DiagnosticableTreeMixin { + static final sm = MoonAccordionSizeProperties( + headerHeight: MoonSizes.sizes.sm, + iconSizeValue: MoonSizes.sizes.x2s, + headerPadding: EdgeInsets.symmetric(horizontal: MoonSizes.sizes.x4s), + textStyle: MoonTextStyles.heading.text12, + ); + + static final md = MoonAccordionSizeProperties( + headerHeight: MoonSizes.sizes.md, + iconSizeValue: MoonSizes.sizes.xs, + headerPadding: EdgeInsets.symmetric(horizontal: MoonSizes.sizes.x3s), + textStyle: MoonTextStyles.heading.text14, + ); + + static final lg = MoonAccordionSizeProperties( + headerHeight: MoonSizes.sizes.lg, + iconSizeValue: MoonSizes.sizes.xs, + headerPadding: EdgeInsets.symmetric(horizontal: MoonSizes.sizes.x3s), + textStyle: MoonTextStyles.heading.text14, + ); + + static final xl = MoonAccordionSizeProperties( + headerHeight: MoonSizes.sizes.xl, + iconSizeValue: MoonSizes.sizes.xs, + headerPadding: EdgeInsets.symmetric(horizontal: MoonSizes.sizes.x2s), + textStyle: MoonTextStyles.heading.text16, + ); + + /// Accordion header height. + final double headerHeight; + + /// Accordion icon size value. + final double iconSizeValue; + + /// Padding around accordion title and icon. + final EdgeInsets headerPadding; + + /// Accordion text style. + final TextStyle textStyle; + + const MoonAccordionSizeProperties({ + required this.headerHeight, + required this.iconSizeValue, + required this.headerPadding, + required this.textStyle, + }); + + @override + MoonAccordionSizeProperties copyWith({ + double? headerHeight, + double? iconSizeValue, + EdgeInsets? headerPadding, + TextStyle? textStyle, + }) { + return MoonAccordionSizeProperties( + headerHeight: headerHeight ?? this.headerHeight, + iconSizeValue: iconSizeValue ?? this.iconSizeValue, + headerPadding: headerPadding ?? this.headerPadding, + textStyle: textStyle ?? this.textStyle, + ); + } + + @override + MoonAccordionSizeProperties lerp(ThemeExtension? other, double t) { + if (other is! MoonAccordionSizeProperties) return this; + + return MoonAccordionSizeProperties( + headerHeight: lerpDouble(headerHeight, other.headerHeight, t)!, + iconSizeValue: lerpDouble(iconSizeValue, other.iconSizeValue, t)!, + headerPadding: EdgeInsets.lerp(headerPadding, other.headerPadding, t)!, + textStyle: TextStyle.lerp(textStyle, other.textStyle, t)!, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonAccordionSizeProperties")) + ..add(DoubleProperty("headerHeight", headerHeight)) + ..add(DoubleProperty("iconSizeValue", iconSizeValue)) + ..add(DiagnosticsProperty("headerPadding", headerPadding)) + ..add(DiagnosticsProperty("textStyle", textStyle)); + } +} diff --git a/lib/src/theme/accordion/accordion_sizes.dart b/lib/src/theme/accordion/accordion_sizes.dart new file mode 100644 index 00000000..33d72ca8 --- /dev/null +++ b/lib/src/theme/accordion/accordion_sizes.dart @@ -0,0 +1,71 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/accordion/accordion_size_properties.dart'; + +@immutable +class MoonAccordionSizes extends ThemeExtension with DiagnosticableTreeMixin { + static final sizes = MoonAccordionSizes( + sm: MoonAccordionSizeProperties.sm, + md: MoonAccordionSizeProperties.md, + lg: MoonAccordionSizeProperties.lg, + xl: MoonAccordionSizeProperties.xl, + ); + + /// Small accordion properties. + final MoonAccordionSizeProperties sm; + + /// Medium accordion properties. + final MoonAccordionSizeProperties md; + + /// Large accordion properties. + final MoonAccordionSizeProperties lg; + + /// Extra large accordion properties. + final MoonAccordionSizeProperties xl; + + const MoonAccordionSizes({ + required this.sm, + required this.md, + required this.lg, + required this.xl, + }); + + @override + MoonAccordionSizes copyWith({ + MoonAccordionSizeProperties? sm, + MoonAccordionSizeProperties? md, + MoonAccordionSizeProperties? lg, + MoonAccordionSizeProperties? xl, + }) { + return MoonAccordionSizes( + sm: sm ?? this.sm, + md: md ?? this.md, + lg: lg ?? this.lg, + xl: xl ?? this.xl, + ); + } + + @override + MoonAccordionSizes lerp(ThemeExtension? other, double t) { + if (other is! MoonAccordionSizes) return this; + + return MoonAccordionSizes( + sm: sm.lerp(other.sm, t), + md: md.lerp(other.md, t), + lg: lg.lerp(other.lg, t), + xl: xl.lerp(other.xl, t), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonAccordionSizes")) + ..add(DiagnosticsProperty("sm", sm)) + ..add(DiagnosticsProperty("md", md)) + ..add(DiagnosticsProperty("lg", lg)) + ..add(DiagnosticsProperty("xl", xl)); + } +} diff --git a/lib/src/theme/accordion/accordion_theme.dart b/lib/src/theme/accordion/accordion_theme.dart new file mode 100644 index 00000000..f4f1ecd2 --- /dev/null +++ b/lib/src/theme/accordion/accordion_theme.dart @@ -0,0 +1,81 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/accordion/accordion_colors.dart'; +import 'package:moon_design/src/theme/accordion/accordion_properties.dart'; +import 'package:moon_design/src/theme/accordion/accordion_shadows.dart'; +import 'package:moon_design/src/theme/accordion/accordion_sizes.dart'; + +@immutable +class MoonAccordionTheme extends ThemeExtension with DiagnosticableTreeMixin { + static final light = MoonAccordionTheme( + colors: MoonAccordionColors.light, + properties: MoonAccordionProperties.properties, + sizes: MoonAccordionSizes.sizes, + shadows: MoonAccordionShadows.light, + ); + + static final dark = MoonAccordionTheme( + colors: MoonAccordionColors.dark, + properties: MoonAccordionProperties.properties, + sizes: MoonAccordionSizes.sizes, + shadows: MoonAccordionShadows.dark, + ); + + /// Accordion colors. + final MoonAccordionColors colors; + + /// Accordion properties. + final MoonAccordionProperties properties; + + /// Accordion sizes. + final MoonAccordionSizes sizes; + + /// Accordion shadows. + final MoonAccordionShadows shadows; + + const MoonAccordionTheme({ + required this.colors, + required this.properties, + required this.sizes, + required this.shadows, + }); + + @override + MoonAccordionTheme copyWith({ + MoonAccordionColors? colors, + MoonAccordionProperties? properties, + MoonAccordionSizes? sizes, + MoonAccordionShadows? shadows, + }) { + return MoonAccordionTheme( + colors: colors ?? this.colors, + properties: properties ?? this.properties, + sizes: sizes ?? this.sizes, + shadows: shadows ?? this.shadows, + ); + } + + @override + MoonAccordionTheme lerp(ThemeExtension? other, double t) { + if (other is! MoonAccordionTheme) return this; + + return MoonAccordionTheme( + colors: colors.lerp(other.colors, t), + properties: properties.lerp(other.properties, t), + sizes: sizes.lerp(other.sizes, t), + shadows: shadows.lerp(other.shadows, t), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder diagnosticProperties) { + super.debugFillProperties(diagnosticProperties); + diagnosticProperties + ..add(DiagnosticsProperty("type", "MoonAccordionTheme")) + ..add(DiagnosticsProperty("colors", colors)) + ..add(DiagnosticsProperty("properties", properties)) + ..add(DiagnosticsProperty("sizes", sizes)) + ..add(DiagnosticsProperty("shadows", shadows)); + } +} diff --git a/lib/src/theme/theme.dart b/lib/src/theme/theme.dart index 6cdafb67..fecbf2c0 100644 --- a/lib/src/theme/theme.dart +++ b/lib/src/theme/theme.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:moon_design/src/theme/accordion/accordion_theme.dart'; import 'package:moon_design/src/theme/avatar/avatar_theme.dart'; import 'package:moon_design/src/theme/borders.dart'; @@ -24,6 +25,7 @@ import 'package:moon_design/src/theme/typography/typography.dart'; @immutable class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { static final light = MoonTheme( + accordionTheme: MoonAccordionTheme.light, avatarTheme: MoonAvatarTheme.light, borders: MoonBorders.borders, buttonTheme: MoonButtonTheme.light, @@ -46,6 +48,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { ); static final dark = MoonTheme( + accordionTheme: MoonAccordionTheme.dark, avatarTheme: MoonAvatarTheme.dark, borders: MoonBorders.borders, buttonTheme: MoonButtonTheme.dark, @@ -67,6 +70,9 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { typography: MoonTypography.dark, ); + /// Moon Design System MoonAccordion widget theming. + final MoonAccordionTheme accordionTheme; + /// Moon Design System MoonAvatar widget theming. final MoonAvatarTheme avatarTheme; @@ -125,6 +131,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { final MoonTypography typography; const MoonTheme({ + required this.accordionTheme, required this.avatarTheme, required this.borders, required this.buttonTheme, @@ -148,6 +155,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { @override MoonTheme copyWith({ + MoonAccordionTheme? accordionTheme, MoonAvatarTheme? avatarTheme, MoonBorders? borders, MoonButtonTheme? buttonTheme, @@ -169,6 +177,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { MoonTypography? typography, }) { return MoonTheme( + accordionTheme: accordionTheme ?? this.accordionTheme, avatarTheme: avatarTheme ?? this.avatarTheme, borders: borders ?? this.borders, buttonTheme: buttonTheme ?? this.buttonTheme, @@ -196,6 +205,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { if (other is! MoonTheme) return this; return MoonTheme( + accordionTheme: accordionTheme.lerp(other.accordionTheme, t), avatarTheme: avatarTheme.lerp(other.avatarTheme, t), borders: borders.lerp(other.borders, t), buttonTheme: buttonTheme.lerp(other.buttonTheme, t), @@ -223,6 +233,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty("type", "MoonTheme")) + ..add(DiagnosticsProperty("MoonAccordionTheme", accordionTheme)) ..add(DiagnosticsProperty("MoonAvatarTheme", avatarTheme)) ..add(DiagnosticsProperty("MoonBorders", borders)) ..add(DiagnosticsProperty("MoonButtonTheme", buttonTheme)) @@ -247,6 +258,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { extension MoonThemeX on BuildContext { MoonTheme? get moonTheme => Theme.of(this).extension(); + MoonAccordionTheme? get moonAccordionTheme => moonTheme?.accordionTheme; MoonBorders? get moonBorders => moonTheme?.borders; MoonColors? get moonColors => moonTheme?.colors; MoonEffects? get moonEffects => moonTheme?.effects; diff --git a/lib/src/widgets/accordion/accordion.dart b/lib/src/widgets/accordion/accordion.dart new file mode 100644 index 00000000..4dab79be --- /dev/null +++ b/lib/src/widgets/accordion/accordion.dart @@ -0,0 +1,504 @@ +import 'package:figma_squircle/figma_squircle.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/accordion/accordion_size_properties.dart'; +import 'package:moon_design/src/theme/borders.dart'; +import 'package:moon_design/src/theme/colors.dart'; +import 'package:moon_design/src/theme/effects/focus_effects.dart'; +import 'package:moon_design/src/theme/effects/hover_effects.dart'; +import 'package:moon_design/src/theme/shadows.dart'; +import 'package:moon_design/src/theme/theme.dart'; +import 'package:moon_design/src/widgets/common/effects/focus_effect.dart'; +import 'package:moon_design/src/widgets/common/icons/icons.dart'; + +enum MoonAccordionSize { + sm, + md, + lg, + xl, +} + +class MoonAccordion extends StatefulWidget { + /// Called when the accordion expands or collapses. + /// + /// When the accordion starts expanding, this function is called with the value + /// true. When the accordion starts collapsing, this function is called with + /// the value false. + final ValueChanged? onExpansionChanged; + + /// Specifies if the accordion is initially expanded (true) or collapsed (false, the default). + final bool initiallyExpanded; + + /// The size of the accordion. + final MoonAccordionSize? accordionSize; + + /// The background color of the accordion when expanded. + final Color? backgroundColor; + + /// The background color of the accordion when collapsed. + final Color? expandedBackgroundColor; + + /// The color of the border of the accordion. + final Color? borderColor; + + /// The color of the divider between the header and the body. + final Color? dividerColor; + + /// The icon color of accordion's expansion arrow icon when the accordion is expanded. + final Color? iconColor; + + /// The icon color of accordion's expansion arrow icon when the accordion is collapsed. + final Color? expandedIconColor; + + /// The color of the accordion's titles when the accordion is expanded. + final Color? textColor; + + /// Whether to show a border around the accordion. + final bool showBorder; + + /// Whether to show a divider between the header and the body. + final bool showDivider; + + /// Specifies whether the state of the children is maintained when the accordion expands and collapses. + /// + /// When true, the children are kept in the tree while the accordion is collapsed. + /// When false (default), the children are removed from the tree when the accordion is + /// collapsed and recreated upon expansion. + final bool maintainState; + + /// The height of the accordion header. + final double? headerHeight; + + /// Specifies padding for the accordion header. + final EdgeInsets? headerPadding; + + /// Specifies padding for [children]. + final EdgeInsetsGeometry? childrenPadding; + + /// The accordion's border radius. + final SmoothBorderRadius? borderRadius; + + /// Specifies the alignment of [children], which are arranged in a column when + /// the accordion is expanded. + /// The internals of the expanded accordion make use of a [Column] widget for + /// [children], and [Align] widget to align the column. The [expandedAlignment] + /// parameter is passed directly into the [Align]. + /// + /// Modifying this property controls the alignment of the column within the + /// expanded accordion, not the alignment of [children] widgets within the column. + /// To align each child within [children], see [expandedCrossAxisAlignment]. + /// + /// The width of the column is the width of the widest child widget in [children]. + final Alignment? expandedAlignment; + + /// Specifies the alignment of each child within [children] when the accordion is expanded. + /// + /// The internals of the expanded accordion make use of a [Column] widget for + /// [children], and the `crossAxisAlignment` parameter is passed directly into the [Column]. + /// + /// Modifying this property controls the cross axis alignment of each child + /// within its [Column]. Note that the width of the [Column] that houses + /// [children] will be the same as the widest child widget in [children]. It is + /// not necessarily the width of [Column] is equal to the width of expanded accordion. + /// + /// To align the [Column] along the expanded accordion, use the [expandedAlignment] property + /// instead. + /// + /// When the value is null, the value of [expandedCrossAxisAlignment] is [CrossAxisAlignment.center]. + final CrossAxisAlignment? expandedCrossAxisAlignment; + + /// {@macro flutter.material.Material.clipBehavior} + final Clip? clipBehavior; + + /// Accordion shadows. + final List? shadows; + + /// Accordion transition duration (expand or collapse animation). + final Duration? transitionDuration; + + /// Accordion transition curve (expand or collapse animation). + final Curve? transitionCurve; + + /// {@macro flutter.widgets.Focus.autofocus} + final bool autofocus; + + /// {@macro flutter.widgets.Focus.focusNode}. + final FocusNode? focusNode; + + /// A widget to display before the title. + /// + /// Typically a [CircleAvatar] widget. + /// + /// Note that depending on the value of [controlAffinity], the [leading] widget + /// may replace the rotating expansion arrow icon. + final Widget? leading; + + /// The primary content of the accordion header. + /// + /// Typically a [Text] widget. + final Widget title; + + /// A widget to display after the title. + /// + /// Note that depending on the value of [controlAffinity], the [trailing] widget + /// may replace the rotating expansion arrow icon. + final Widget? trailing; + + /// The widgets that are displayed when the accordion expands. + final List children; + + /// MDS accordion widget. + const MoonAccordion({ + super.key, + this.onExpansionChanged, + this.initiallyExpanded = false, + this.accordionSize, + this.borderColor, + this.backgroundColor, + this.expandedBackgroundColor, + this.dividerColor, + this.iconColor, + this.expandedIconColor, + this.textColor, + this.showBorder = false, + this.showDivider = true, + this.maintainState = false, + this.headerHeight, + this.headerPadding, + this.childrenPadding, + this.borderRadius, + this.expandedAlignment, + this.expandedCrossAxisAlignment, + this.clipBehavior, + this.shadows, + this.transitionDuration, + this.transitionCurve, + this.autofocus = false, + this.focusNode, + this.leading, + required this.title, + this.trailing, + this.children = const [], + }) : assert( + expandedCrossAxisAlignment != CrossAxisAlignment.baseline, + 'CrossAxisAlignment.baseline is not supported since the expanded children ' + 'are aligned in a column, not a row. Try to use another constant.', + ); + + @override + State createState() => _MoonAccordionState(); +} + +class _MoonAccordionState extends State with SingleTickerProviderStateMixin { + static final Animatable _halfTween = Tween(begin: 0.0, end: 0.5); + + late final Map> _actions = { + ActivateIntent: CallbackAction(onInvoke: (_) => _handleTap()) + }; + + AnimationController? _animationController; + CurvedAnimation? _curvedAnimation; + + Animation? _iconColorAnimation; + Animation? _backgroundColorAnimation; + + bool _isExpanded = false; + bool _isFocused = false; + bool _isHovered = false; + + FocusNode? _focusNode; + FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); + + void _handleHover(bool hover) { + if (hover != _isHovered && mounted) { + setState(() => _isHovered = hover); + } + } + + void _handleFocus(bool focus) { + if (focus != _isFocused && mounted) { + setState(() => _isFocused = focus); + } + } + + void _handleFocusChange(bool hasFocus) { + setState(() { + _isFocused = hasFocus; + }); + } + + void _handleTap() { + setState(() { + _isExpanded = !_isExpanded; + if (_isExpanded) { + _animationController!.forward(); + } else { + _animationController!.reverse().then((void value) { + if (!mounted) { + return; + } + setState(() { + // Rebuild without widget.children. + }); + }); + } + PageStorage.maybeOf(context)?.writeState(context, _isExpanded); + }); + widget.onExpansionChanged?.call(_isExpanded); + } + + MoonAccordionSizeProperties _getMoonAccordionSize(BuildContext context, MoonAccordionSize? moonAccordionSize) { + switch (moonAccordionSize) { + case MoonAccordionSize.sm: + return context.moonTheme?.accordionTheme.sizes.sm ?? MoonAccordionSizeProperties.sm; + case MoonAccordionSize.md: + return context.moonTheme?.accordionTheme.sizes.md ?? MoonAccordionSizeProperties.md; + case MoonAccordionSize.lg: + return context.moonTheme?.accordionTheme.sizes.lg ?? MoonAccordionSizeProperties.lg; + case MoonAccordionSize.xl: + return context.moonTheme?.accordionTheme.sizes.xl ?? MoonAccordionSizeProperties.xl; + default: + return context.moonTheme?.accordionTheme.sizes.md ?? MoonAccordionSizeProperties.md; + } + } + + Color _getTextColor(BuildContext context, {required Color effectiveBackgroundColor}) { + if (widget.backgroundColor == null && context.moonTypography != null) { + return context.moonTypography!.colors.bodyPrimary; + } + + final backgroundLuminance = effectiveBackgroundColor.computeLuminance(); + if (backgroundLuminance > 0.5) { + return MoonColors.light.bulma; + } else { + return MoonColors.dark.bulma; + } + } + + @override + void initState() { + super.initState(); + + _isExpanded = PageStorage.maybeOf(context)?.readState(context) as bool? ?? widget.initiallyExpanded; + + if (_isExpanded) { + _animationController!.value = 1.0; + } + } + + @override + void dispose() { + _animationController!.dispose(); + super.dispose(); + } + + Widget? _buildIcon(BuildContext context) { + final double iconSize = _getMoonAccordionSize(context, widget.accordionSize).iconSizeValue; + + final Color effectiveBackgroundColor = + widget.backgroundColor ?? context.moonTheme?.accordionTheme.colors.backgroundColor ?? MoonColors.light.gohan; + + final Color effectiveIconColor = widget.iconColor ?? + context.moonTheme?.accordionTheme.colors.iconColor ?? + _getTextColor(context, effectiveBackgroundColor: effectiveBackgroundColor); + + final Color effectiveExpandedIconColor = + widget.expandedIconColor ?? context.moonTheme?.accordionTheme.colors.expandedIconColor ?? effectiveIconColor; + + _iconColorAnimation = + ColorTween(begin: effectiveIconColor, end: effectiveExpandedIconColor).animate(_curvedAnimation!); + + return IconTheme( + data: IconThemeData(color: _iconColorAnimation?.value), + child: RotationTransition( + turns: _halfTween.animate(_curvedAnimation!), + child: Icon(MoonIconsControls.chevron_down24, size: iconSize), + ), + ); + } + + Widget _buildChildren(BuildContext context, Widget? child) { + final Color effectiveBackgroundColor = + widget.backgroundColor ?? context.moonTheme?.accordionTheme.colors.backgroundColor ?? MoonColors.light.gohan; + + final Color effectiveExpandedBackgroundColor = widget.expandedBackgroundColor ?? + context.moonTheme?.accordionTheme.colors.expandedBackgroundColor ?? + MoonColors.light.gohan; + + final Color effectiveTextColor = + _getTextColor(context, effectiveBackgroundColor: _backgroundColorAnimation?.value ?? Colors.transparent); + + final Color effectiveBorderColor = + widget.borderColor ?? context.moonTheme?.accordionTheme.colors.borderColor ?? MoonColors.light.beerus; + + final MoonAccordionSizeProperties effectiveMoonAccordionSize = _getMoonAccordionSize(context, widget.accordionSize); + + final double effectiveHeaderHeight = widget.headerHeight ?? effectiveMoonAccordionSize.headerHeight; + final EdgeInsets effectiveHeaderPadding = widget.headerPadding ?? effectiveMoonAccordionSize.headerPadding; + + final List effectiveShadows = + widget.shadows ?? context.moonTheme?.accordionTheme.shadows.accordionShadows ?? MoonShadows.light.sm; + + final Duration effectiveTransitionDuration = widget.transitionDuration ?? + context.moonTheme?.accordionTheme.properties.transitionDuration ?? + const Duration(milliseconds: 200); + + final Curve effectiveTransitionCurve = + widget.transitionCurve ?? context.moonTheme?.accordionTheme.properties.transitionCurve ?? Curves.easeInOutCubic; + + final double effectiveFocusEffectExtent = + context.moonEffects?.controlFocusEffect.effectExtent ?? MoonFocusEffects.lightFocusEffect.effectExtent; + + final Color effectiveFocusEffectColor = + context.moonEffects?.controlFocusEffect.effectColor ?? MoonFocusEffects.lightFocusEffect.effectColor; + + final Curve effectiveFocusEffectCurve = + context.moonEffects?.controlFocusEffect.effectCurve ?? MoonFocusEffects.lightFocusEffect.effectCurve; + + final Duration effectiveFocusEffectDuration = + context.moonEffects?.controlFocusEffect.effectDuration ?? MoonFocusEffects.lightFocusEffect.effectDuration; + + final Color effectiveHoverEffectColor = context.moonEffects?.controlHoverEffect.primaryHoverColor ?? + MoonHoverEffects.lightHoverEffect.primaryHoverColor; + + final Curve effectiveHoverEffectCurve = + context.moonEffects?.controlHoverEffect.hoverCurve ?? MoonHoverEffects.lightHoverEffect.hoverCurve; + + final Duration effectiveHoverEffectDuration = + context.moonEffects?.controlHoverEffect.hoverDuration ?? MoonHoverEffects.lightHoverEffect.hoverDuration; + + final SmoothBorderRadius effectiveBorderRadius = widget.borderRadius ?? + context.moonTheme?.accordionTheme.properties.borderRadius ?? + SmoothBorderRadius.all( + SmoothRadius( + cornerRadius: MoonBorders.borders.interactiveSm.topLeft.x, + cornerSmoothing: 1, + ), + ); + + _animationController ??= AnimationController(duration: effectiveTransitionDuration, vsync: this); + _curvedAnimation ??= CurvedAnimation(parent: _animationController!, curve: effectiveTransitionCurve); + + _backgroundColorAnimation = + ColorTween(begin: effectiveBackgroundColor, end: effectiveExpandedBackgroundColor).animate(_curvedAnimation!); + + final Color? resolvedBackgroundColor = _isHovered || _isFocused + ? Color.alphaBlend(effectiveHoverEffectColor, _backgroundColorAnimation!.value!) + : _backgroundColorAnimation!.value; + + return FocusableActionDetector( + actions: _actions, + focusNode: _effectiveFocusNode, + autofocus: widget.autofocus, + onFocusChange: _handleFocusChange, + onShowFocusHighlight: _handleFocus, + onShowHoverHighlight: _handleHover, + child: Semantics( + enabled: _isExpanded, + focused: _isFocused, + child: RepaintBoundary( + child: MoonFocusEffect( + show: _isFocused, + effectExtent: effectiveFocusEffectExtent, + effectColor: effectiveFocusEffectColor, + effectDuration: effectiveFocusEffectDuration, + effectCurve: effectiveFocusEffectCurve, + childBorderRadius: effectiveBorderRadius, + child: AnimatedContainer( + duration: effectiveHoverEffectDuration, + curve: effectiveHoverEffectCurve, + clipBehavior: widget.clipBehavior ?? Clip.none, + decoration: ShapeDecoration( + color: resolvedBackgroundColor, + shadows: effectiveShadows, + shape: SmoothRectangleBorder( + side: widget.showBorder ? BorderSide(color: effectiveBorderColor) : BorderSide.none, + borderRadius: effectiveBorderRadius, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _handleTap, + child: Container( + height: effectiveHeaderHeight, + padding: effectiveHeaderPadding, + child: Row( + children: [ + if (widget.leading != null) widget.leading!, + AnimatedDefaultTextStyle( + style: effectiveMoonAccordionSize.textStyle.copyWith(color: effectiveTextColor), + duration: effectiveTransitionDuration, + curve: effectiveTransitionCurve, + child: Expanded(child: widget.title), + ), + widget.trailing ?? _buildIcon(context)!, + ], + ), + ), + ), + ), + ClipRect( + child: Column( + children: [ + Align( + alignment: widget.expandedAlignment ?? Alignment.topCenter, + heightFactor: _curvedAnimation!.value, + child: child, + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final Duration effectiveTransitionDuration = widget.transitionDuration ?? + context.moonTheme?.accordionTheme.properties.transitionDuration ?? + const Duration(milliseconds: 200); + + _animationController ??= AnimationController(duration: effectiveTransitionDuration, vsync: this); + + final bool closed = !_isExpanded && _animationController!.isDismissed; + final bool shouldRemoveChildren = closed && !widget.maintainState; + + final Color effectiveDividerColor = + widget.dividerColor ?? context.moonTheme?.accordionTheme.colors.dividerColor ?? MoonColors.light.beerus; + + final Widget result = Offstage( + offstage: closed, + child: TickerMode( + enabled: !closed, + child: Column( + children: [ + if (widget.showDivider) Container(height: 1, color: effectiveDividerColor), + Padding( + padding: widget.childrenPadding ?? EdgeInsets.zero, + child: Column( + crossAxisAlignment: widget.expandedCrossAxisAlignment ?? CrossAxisAlignment.center, + children: widget.children, + ), + ), + ], + ), + ), + ); + + return AnimatedBuilder( + animation: _animationController!.view, + builder: _buildChildren, + child: shouldRemoveChildren ? null : result, + ); + } +}