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
This commit is contained in:
Richard Shiue 2025-04-11 11:10:17 +08:00
parent f6e3290aa4
commit d8401e09c9
9 changed files with 113 additions and 37 deletions

View File

@ -7,6 +7,13 @@ typedef AFBaseButtonColorBuilder = Color Function(
bool disabled, bool disabled,
); );
typedef AFBaseButtonBorderColorBuilder = Color Function(
BuildContext context,
bool isHovering,
bool disabled,
bool isFocused,
);
class AFBaseButton extends StatefulWidget { class AFBaseButton extends StatefulWidget {
const AFBaseButton({ const AFBaseButton({
super.key, super.key,
@ -16,40 +23,83 @@ class AFBaseButton extends StatefulWidget {
required this.borderRadius, required this.borderRadius,
this.borderColor, this.borderColor,
this.backgroundColor, this.backgroundColor,
this.ringColor,
this.disabled = false, this.disabled = false,
}); });
final VoidCallback? onTap; final VoidCallback? onTap;
final AFBaseButtonColorBuilder? borderColor; final AFBaseButtonBorderColorBuilder? borderColor;
final AFBaseButtonBorderColorBuilder? ringColor;
final AFBaseButtonColorBuilder? backgroundColor; final AFBaseButtonColorBuilder? backgroundColor;
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
final double borderRadius; final double borderRadius;
final bool disabled; final bool disabled;
final Widget Function(BuildContext context, bool isHovering, bool disabled) final Widget Function(
builder; BuildContext context,
bool isHovering,
bool disabled,
) builder;
@override @override
State<AFBaseButton> createState() => _AFBaseButtonState(); State<AFBaseButton> createState() => _AFBaseButtonState();
} }
class _AFBaseButtonState extends State<AFBaseButton> { class _AFBaseButtonState extends State<AFBaseButton> {
final FocusNode focusNode = FocusNode();
bool isHovering = false; bool isHovering = false;
bool isFocused = false;
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color borderColor = _buildBorderColor(context); final Color borderColor = _buildBorderColor(context);
final Color backgroundColor = _buildBackgroundColor(context); final Color backgroundColor = _buildBackgroundColor(context);
final Color ringColor = _buildRingColor(context);
return MouseRegion( return Actions(
cursor: actions: {
widget.disabled ? SystemMouseCursors.basic : SystemMouseCursors.click, ActivateIntent: CallbackAction<ActivateIntent>(
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), onEnter: (_) => setState(() => isHovering = true),
onExit: (_) => setState(() => isHovering = false), onExit: (_) => setState(() => isHovering = false),
child: GestureDetector( child: GestureDetector(
onTap: widget.disabled ? null : widget.onTap, 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( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: backgroundColor, color: backgroundColor,
@ -58,7 +108,14 @@ class _AFBaseButtonState extends State<AFBaseButton> {
), ),
child: Padding( child: Padding(
padding: widget.padding, padding: widget.padding,
child: widget.builder(context, isHovering, widget.disabled), child: widget.builder(
context,
isHovering,
widget.disabled,
),
),
),
),
), ),
), ),
), ),
@ -67,7 +124,8 @@ class _AFBaseButtonState extends State<AFBaseButton> {
Color _buildBorderColor(BuildContext context) { Color _buildBorderColor(BuildContext context) {
final theme = AppFlowyTheme.of(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; theme.borderColorScheme.greyTertiary;
} }
@ -76,4 +134,22 @@ class _AFBaseButtonState extends State<AFBaseButton> {
return widget.backgroundColor?.call(context, isHovering, widget.disabled) ?? return widget.backgroundColor?.call(context, isHovering, widget.disabled) ??
theme.fillColorScheme.transparent; 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;
}
} }

View File

@ -115,7 +115,7 @@ class AFFilledButton extends StatelessWidget {
return AFBaseButton( return AFBaseButton(
disabled: disabled, disabled: disabled,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
borderColor: (_, __, ___) => Colors.transparent, borderColor: (_, __, ___, ____) => Colors.transparent,
padding: padding ?? size.buildPadding(context), padding: padding ?? size.buildPadding(context),
borderRadius: borderRadius ?? size.buildBorderRadius(context), borderRadius: borderRadius ?? size.buildBorderRadius(context),
onTap: onTap, onTap: onTap,

View File

@ -121,7 +121,7 @@ class AFFilledTextButton extends AFBaseTextButton {
return AFBaseButton( return AFBaseButton(
disabled: disabled, disabled: disabled,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
borderColor: (_, __, ___) => Colors.transparent, borderColor: (_, __, ___, ____) => Colors.transparent,
padding: padding ?? size.buildPadding(context), padding: padding ?? size.buildPadding(context),
borderRadius: borderRadius ?? size.buildBorderRadius(context), borderRadius: borderRadius ?? size.buildBorderRadius(context),
onTap: onTap, onTap: onTap,

View File

@ -86,7 +86,7 @@ class AFGhostButton extends StatelessWidget {
return AFBaseButton( return AFBaseButton(
disabled: disabled, disabled: disabled,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
borderColor: (_, __, ___) => Colors.transparent, borderColor: (_, __, ___, ____) => Colors.transparent,
padding: padding ?? size.buildPadding(context), padding: padding ?? size.buildPadding(context),
borderRadius: borderRadius ?? size.buildBorderRadius(context), borderRadius: borderRadius ?? size.buildBorderRadius(context),
onTap: onTap, onTap: onTap,

View File

@ -109,7 +109,7 @@ class AFGhostIconTextButton extends StatelessWidget {
return AFBaseButton( return AFBaseButton(
disabled: disabled, disabled: disabled,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
borderColor: (context, isHovering, disabled) { borderColor: (context, isHovering, disabled, isFocused) {
return Colors.transparent; return Colors.transparent;
}, },
padding: padding ?? size.buildPadding(context), padding: padding ?? size.buildPadding(context),

View File

@ -88,7 +88,7 @@ class AFGhostTextButton extends AFBaseTextButton {
return AFBaseButton( return AFBaseButton(
disabled: disabled, disabled: disabled,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
borderColor: (_, __, ___) => Colors.transparent, borderColor: (_, __, ___, ____) => Colors.transparent,
padding: padding ?? size.buildPadding(context), padding: padding ?? size.buildPadding(context),
borderRadius: borderRadius ?? size.buildBorderRadius(context), borderRadius: borderRadius ?? size.buildBorderRadius(context),
onTap: onTap, onTap: onTap,

View File

@ -38,7 +38,7 @@ class AFOutlinedButton extends StatelessWidget {
padding: padding, padding: padding,
borderRadius: borderRadius, borderRadius: borderRadius,
disabled: disabled, disabled: disabled,
borderColor: (context, isHovering, disabled) { borderColor: (context, isHovering, disabled, isFocused) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
if (disabled) { if (disabled) {
return theme.borderColorScheme.greyTertiary; return theme.borderColorScheme.greyTertiary;
@ -79,7 +79,7 @@ class AFOutlinedButton extends StatelessWidget {
padding: padding, padding: padding,
borderRadius: borderRadius, borderRadius: borderRadius,
disabled: disabled, disabled: disabled,
borderColor: (context, isHovering, disabled) { borderColor: (context, isHovering, disabled, isFocused) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
if (disabled) { if (disabled) {
return theme.fillColorScheme.errorThick; return theme.fillColorScheme.errorThick;
@ -118,7 +118,7 @@ class AFOutlinedButton extends StatelessWidget {
padding: padding, padding: padding,
borderRadius: borderRadius, borderRadius: borderRadius,
disabled: true, disabled: true,
borderColor: (context, isHovering, disabled) { borderColor: (context, isHovering, disabled, isFocused) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
if (disabled) { if (disabled) {
return theme.borderColorScheme.greyTertiary; return theme.borderColorScheme.greyTertiary;
@ -148,7 +148,7 @@ class AFOutlinedButton extends StatelessWidget {
final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? padding;
final double? borderRadius; final double? borderRadius;
final AFBaseButtonColorBuilder? borderColor; final AFBaseButtonBorderColorBuilder? borderColor;
final AFBaseButtonColorBuilder? backgroundColor; final AFBaseButtonColorBuilder? backgroundColor;
final AFOutlinedButtonWidgetBuilder builder; final AFOutlinedButtonWidgetBuilder builder;

View File

@ -46,7 +46,7 @@ class AFOutlinedIconTextButton extends StatelessWidget {
borderRadius: borderRadius, borderRadius: borderRadius,
disabled: disabled, disabled: disabled,
alignment: alignment, alignment: alignment,
borderColor: (context, isHovering, disabled) { borderColor: (context, isHovering, disabled, isFocused) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
if (disabled) { if (disabled) {
return theme.borderColorScheme.greyTertiary; return theme.borderColorScheme.greyTertiary;
@ -101,7 +101,7 @@ class AFOutlinedIconTextButton extends StatelessWidget {
borderRadius: borderRadius, borderRadius: borderRadius,
disabled: disabled, disabled: disabled,
alignment: alignment, alignment: alignment,
borderColor: (context, isHovering, disabled) { borderColor: (context, isHovering, disabled, isFocused) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
if (disabled) { if (disabled) {
return theme.fillColorScheme.errorThick; return theme.fillColorScheme.errorThick;
@ -156,7 +156,7 @@ class AFOutlinedIconTextButton extends StatelessWidget {
? theme.textColorScheme.tertiary ? theme.textColorScheme.tertiary
: theme.textColorScheme.primary; : theme.textColorScheme.primary;
}, },
borderColor: (context, isHovering, disabled) { borderColor: (context, isHovering, disabled, isFocused) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
if (disabled) { if (disabled) {
return theme.borderColorScheme.greyTertiary; return theme.borderColorScheme.greyTertiary;
@ -190,7 +190,7 @@ class AFOutlinedIconTextButton extends StatelessWidget {
final AFOutlinedIconBuilder iconBuilder; final AFOutlinedIconBuilder iconBuilder;
final AFBaseButtonColorBuilder? textColor; final AFBaseButtonColorBuilder? textColor;
final AFBaseButtonColorBuilder? borderColor; final AFBaseButtonBorderColorBuilder? borderColor;
final AFBaseButtonColorBuilder? backgroundColor; final AFBaseButtonColorBuilder? backgroundColor;
@override @override

View File

@ -40,7 +40,7 @@ class AFOutlinedTextButton extends AFBaseTextButton {
disabled: disabled, disabled: disabled,
alignment: alignment, alignment: alignment,
textStyle: textStyle, textStyle: textStyle,
borderColor: (context, isHovering, disabled) { borderColor: (context, isHovering, disabled, isFocused) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
if (disabled) { if (disabled) {
return theme.borderColorScheme.greyTertiary; return theme.borderColorScheme.greyTertiary;
@ -95,7 +95,7 @@ class AFOutlinedTextButton extends AFBaseTextButton {
disabled: disabled, disabled: disabled,
alignment: alignment, alignment: alignment,
textStyle: textStyle, textStyle: textStyle,
borderColor: (context, isHovering, disabled) { borderColor: (context, isHovering, disabled, isFocused) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
if (disabled) { if (disabled) {
return theme.fillColorScheme.errorThick; return theme.fillColorScheme.errorThick;
@ -150,7 +150,7 @@ class AFOutlinedTextButton extends AFBaseTextButton {
? theme.textColorScheme.tertiary ? theme.textColorScheme.tertiary
: theme.textColorScheme.primary; : theme.textColorScheme.primary;
}, },
borderColor: (context, isHovering, disabled) { borderColor: (context, isHovering, disabled, isFocused) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
if (disabled) { if (disabled) {
return theme.borderColorScheme.greyTertiary; return theme.borderColorScheme.greyTertiary;
@ -173,7 +173,7 @@ class AFOutlinedTextButton extends AFBaseTextButton {
); );
} }
final AFBaseButtonColorBuilder? borderColor; final AFBaseButtonBorderColorBuilder? borderColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {