From d8401e09c9c48468cb9c61b0c77809dc74c19a0c Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:10:17 +0800 Subject: [PATCH] feat: implement keyboard triggering on buttons and add focus state (#7724) * feat: implement keyboard triggering on buttons and add focus state * chore: pass isFocused to builders --- .../button/base_button/base_button.dart | 116 +++++++++++++++--- .../button/filled_button/filled_button.dart | 2 +- .../filled_button/filled_text_button.dart | 2 +- .../button/ghost_button/ghost_button.dart | 2 +- .../ghost_button/ghost_icon_text_button.dart | 2 +- .../ghost_button/ghost_text_button.dart | 2 +- .../outlined_button/outlined_button.dart | 8 +- .../outlined_icon_text_button.dart | 8 +- .../outlined_button/outlined_text_button.dart | 8 +- 9 files changed, 113 insertions(+), 37 deletions(-) diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart index 22c5325681..b62f2cce9b 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart @@ -7,6 +7,13 @@ typedef AFBaseButtonColorBuilder = Color Function( bool disabled, ); +typedef AFBaseButtonBorderColorBuilder = Color Function( + BuildContext context, + bool isHovering, + bool disabled, + bool isFocused, +); + class AFBaseButton extends StatefulWidget { const AFBaseButton({ super.key, @@ -16,49 +23,99 @@ class AFBaseButton extends StatefulWidget { required this.borderRadius, this.borderColor, this.backgroundColor, + this.ringColor, this.disabled = false, }); final VoidCallback? onTap; - final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? ringColor; final AFBaseButtonColorBuilder? backgroundColor; final EdgeInsetsGeometry padding; final double borderRadius; final bool disabled; - final Widget Function(BuildContext context, bool isHovering, bool disabled) - builder; + final Widget Function( + BuildContext context, + bool isHovering, + bool disabled, + ) builder; @override State createState() => _AFBaseButtonState(); } class _AFBaseButtonState extends State { + final FocusNode focusNode = FocusNode(); + bool isHovering = false; + bool isFocused = false; + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { final Color borderColor = _buildBorderColor(context); final Color backgroundColor = _buildBackgroundColor(context); + final Color ringColor = _buildRingColor(context); - return MouseRegion( - cursor: - widget.disabled ? SystemMouseCursors.basic : SystemMouseCursors.click, - onEnter: (_) => setState(() => isHovering = true), - onExit: (_) => setState(() => isHovering = false), - child: GestureDetector( - onTap: widget.disabled ? null : widget.onTap, - child: DecoratedBox( - decoration: BoxDecoration( - color: backgroundColor, - border: Border.all(color: borderColor), - borderRadius: BorderRadius.circular(widget.borderRadius), - ), - child: Padding( - padding: widget.padding, - child: widget.builder(context, isHovering, widget.disabled), + return Actions( + actions: { + ActivateIntent: CallbackAction( + onInvoke: (_) { + if (!widget.disabled) { + widget.onTap?.call(); + } + return; + }, + ), + }, + child: Focus( + focusNode: focusNode, + onFocusChange: (isFocused) { + setState(() => this.isFocused = isFocused); + }, + child: MouseRegion( + cursor: widget.disabled + ? SystemMouseCursors.basic + : SystemMouseCursors.click, + onEnter: (_) => setState(() => isHovering = true), + onExit: (_) => setState(() => isHovering = false), + child: GestureDetector( + onTap: widget.disabled ? null : widget.onTap, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(widget.borderRadius), + border: isFocused + ? Border.all( + color: ringColor, + width: 2, + strokeAlign: BorderSide.strokeAlignOutside, + ) + : null, + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: backgroundColor, + border: Border.all(color: borderColor), + borderRadius: BorderRadius.circular(widget.borderRadius), + ), + child: Padding( + padding: widget.padding, + child: widget.builder( + context, + isHovering, + widget.disabled, + ), + ), + ), + ), ), ), ), @@ -67,7 +124,8 @@ class _AFBaseButtonState extends State { Color _buildBorderColor(BuildContext context) { final theme = AppFlowyTheme.of(context); - return widget.borderColor?.call(context, isHovering, widget.disabled) ?? + return widget.borderColor + ?.call(context, isHovering, widget.disabled, isFocused) ?? theme.borderColorScheme.greyTertiary; } @@ -76,4 +134,22 @@ class _AFBaseButtonState extends State { return widget.backgroundColor?.call(context, isHovering, widget.disabled) ?? theme.fillColorScheme.transparent; } + + Color _buildRingColor(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + if (widget.ringColor != null) { + return widget.ringColor! + .call(context, isHovering, widget.disabled, isFocused); + } + + if (isFocused) { + return AppFlowyTheme.of(context) + .borderColorScheme + .themeThick + .withAlpha(128); + } + + return theme.borderColorScheme.transparent; + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart index 68fb341827..e871626b59 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart @@ -115,7 +115,7 @@ class AFFilledButton extends StatelessWidget { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (_, __, ___) => Colors.transparent, + borderColor: (_, __, ___, ____) => Colors.transparent, padding: padding ?? size.buildPadding(context), borderRadius: borderRadius ?? size.buildBorderRadius(context), onTap: onTap, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart index 889ad1e429..d1b1d868d0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart @@ -121,7 +121,7 @@ class AFFilledTextButton extends AFBaseTextButton { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (_, __, ___) => Colors.transparent, + borderColor: (_, __, ___, ____) => Colors.transparent, padding: padding ?? size.buildPadding(context), borderRadius: borderRadius ?? size.buildBorderRadius(context), onTap: onTap, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart index 47ff96e878..6300c6f5a8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart @@ -86,7 +86,7 @@ class AFGhostButton extends StatelessWidget { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (_, __, ___) => Colors.transparent, + borderColor: (_, __, ___, ____) => Colors.transparent, padding: padding ?? size.buildPadding(context), borderRadius: borderRadius ?? size.buildBorderRadius(context), onTap: onTap, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart index e65eb2dd7e..af65599ea3 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart @@ -109,7 +109,7 @@ class AFGhostIconTextButton extends StatelessWidget { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { return Colors.transparent; }, padding: padding ?? size.buildPadding(context), diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart index 441b544f8a..d154d67dbd 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart @@ -88,7 +88,7 @@ class AFGhostTextButton extends AFBaseTextButton { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (_, __, ___) => Colors.transparent, + borderColor: (_, __, ___, ____) => Colors.transparent, padding: padding ?? size.buildPadding(context), borderRadius: borderRadius ?? size.buildBorderRadius(context), onTap: onTap, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart index 3b0ea7a06d..205d9931d6 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart @@ -38,7 +38,7 @@ class AFOutlinedButton extends StatelessWidget { padding: padding, borderRadius: borderRadius, disabled: disabled, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -79,7 +79,7 @@ class AFOutlinedButton extends StatelessWidget { padding: padding, borderRadius: borderRadius, disabled: disabled, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.fillColorScheme.errorThick; @@ -118,7 +118,7 @@ class AFOutlinedButton extends StatelessWidget { padding: padding, borderRadius: borderRadius, disabled: true, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -148,7 +148,7 @@ class AFOutlinedButton extends StatelessWidget { final EdgeInsetsGeometry? padding; final double? borderRadius; - final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? borderColor; final AFBaseButtonColorBuilder? backgroundColor; final AFOutlinedButtonWidgetBuilder builder; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart index 710a4ccca5..350594cd46 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart @@ -46,7 +46,7 @@ class AFOutlinedIconTextButton extends StatelessWidget { borderRadius: borderRadius, disabled: disabled, alignment: alignment, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -101,7 +101,7 @@ class AFOutlinedIconTextButton extends StatelessWidget { borderRadius: borderRadius, disabled: disabled, alignment: alignment, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.fillColorScheme.errorThick; @@ -156,7 +156,7 @@ class AFOutlinedIconTextButton extends StatelessWidget { ? theme.textColorScheme.tertiary : theme.textColorScheme.primary; }, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -190,7 +190,7 @@ class AFOutlinedIconTextButton extends StatelessWidget { final AFOutlinedIconBuilder iconBuilder; final AFBaseButtonColorBuilder? textColor; - final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? borderColor; final AFBaseButtonColorBuilder? backgroundColor; @override diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart index d5ae580583..d809d981b0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart @@ -40,7 +40,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { disabled: disabled, alignment: alignment, textStyle: textStyle, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -95,7 +95,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { disabled: disabled, alignment: alignment, textStyle: textStyle, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.fillColorScheme.errorThick; @@ -150,7 +150,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { ? theme.textColorScheme.tertiary : theme.textColorScheme.primary; }, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -173,7 +173,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { ); } - final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? borderColor; @override Widget build(BuildContext context) {