Skip to content

Commit 5b21adb

Browse files
authored
Add programmatic focus to Button (#2765)
## Summary: [WB-2038] Add programmatic focus to Button Issue: WB-2038 ## Test plan: 1. Review story for programmatic focus: /?path=/story/packages-button-button--receiving-focus-programmatically&globals=viewport:desktop; 2. Compare to ActivityButton: /?path=/story/packages-button-activitybutton--receiving-focus-programmatically&globals=viewport:desktop; [WB-2038]: https://khanacademy.atlassian.net/browse/WB-2038?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ Author: marcysutton Reviewers: beaesguerra Required Reviewers: Approved By: beaesguerra Checks: ✅ 13 checks were successful, ⏭️ 5 checks have been skipped, ⏹️ 6 checks were cancelled Pull Request URL: #2765
1 parent dcaee7c commit 5b21adb

File tree

4 files changed

+69
-2
lines changed

4 files changed

+69
-2
lines changed

.changeset/busy-seals-strive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/wonder-blocks-button": patch
3+
---
4+
5+
Add support for programmatic focus

__docs__/wonder-blocks-button/activity-button.stories.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,10 @@ export const ReceivingFocusProgrammatically: Story = {
179179
startIcon: magnifyingGlass,
180180
endIcon: caretRight,
181181
},
182+
parameters: {
183+
chromatic: {
184+
// Disable since it requires user interaction to see the focus ring.
185+
disableSnapshot: true,
186+
},
187+
},
182188
};

__docs__/wonder-blocks-button/button.stories.tsx

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from "react";
22
import {StyleSheet} from "aphrodite";
33
import type {Meta, StoryObj} from "@storybook/react";
4+
import {action} from "@storybook/addon-actions";
45

56
import {MemoryRouter} from "react-router-dom";
67
import {CompatRouter, Route, Routes} from "react-router-dom-v5-compat";
@@ -10,10 +11,12 @@ import type {StyleDeclaration} from "aphrodite";
1011
import pencilSimple from "@phosphor-icons/core/regular/pencil-simple.svg";
1112
import pencilSimpleBold from "@phosphor-icons/core/bold/pencil-simple-bold.svg";
1213
import plus from "@phosphor-icons/core/regular/plus.svg";
14+
import magnifyingGlass from "@phosphor-icons/core/regular/magnifying-glass.svg";
15+
import caretRight from "@phosphor-icons/core/regular/caret-right.svg";
1316

1417
import {View} from "@khanacademy/wonder-blocks-core";
1518
import {Strut} from "@khanacademy/wonder-blocks-layout";
16-
import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
19+
import {color, sizing, spacing} from "@khanacademy/wonder-blocks-tokens";
1720
import {LabelMedium, LabelLarge} from "@khanacademy/wonder-blocks-typography";
1821

1922
import Button from "@khanacademy/wonder-blocks-button";
@@ -657,3 +660,52 @@ WithRouter.parameters = {
657660
disableSnapshot: true,
658661
},
659662
};
663+
664+
/**
665+
* This button can receive focus programmatically. This is useful for cases where
666+
* you want to focus the button when the user interacts with another
667+
* component, such as a form field or another button.
668+
*
669+
* To do this, we use a `ref` to the button and call the `focus()` method
670+
* on it, so the `ActivityButton` receives focus.
671+
*/
672+
export const ReceivingFocusProgrammatically: StoryComponentType = {
673+
render: function Render(args) {
674+
// This story is used to test the focus ring when the button receives
675+
// focus programmatically. The button is focused when the story is
676+
// rendered.
677+
const buttonRef = React.useRef<HTMLButtonElement | null>(null);
678+
679+
return (
680+
<View style={{gap: sizing.size_160, flexDirection: "row"}}>
681+
<Button
682+
{...args}
683+
ref={buttonRef}
684+
onClick={(e) => action("clicked")(e)}
685+
/>
686+
<Button
687+
onClick={() => {
688+
// Focus the button when the button is clicked.
689+
if (buttonRef.current) {
690+
buttonRef.current.focus();
691+
}
692+
}}
693+
kind="secondary"
694+
>
695+
Focus on the Button (left)
696+
</Button>
697+
</View>
698+
);
699+
},
700+
args: {
701+
children: "Search",
702+
startIcon: magnifyingGlass,
703+
endIcon: caretRight,
704+
},
705+
parameters: {
706+
chromatic: {
707+
// Disable since it requires user interaction to see the focus ring.
708+
disableSnapshot: true,
709+
},
710+
},
711+
};

packages/wonder-blocks-button/src/components/button-core.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ const ButtonCore: React.ForwardRefExoticComponent<
6464
buttonStyles.default,
6565
disabled && buttonStyles.disabled,
6666
!disabled && pressed && buttonStyles.pressed,
67+
// Enables programmatic focus.
68+
!disabled && !pressed && focused && buttonStyles.focused,
6769
size === "small" && sharedStyles.small,
6870
size === "large" && sharedStyles.large,
6971
];
@@ -231,7 +233,7 @@ const sharedStyles = StyleSheet.create({
231233
},
232234
});
233235

234-
type ButtonStylesKey = "default" | "pressed" | "disabled";
236+
type ButtonStylesKey = "default" | "pressed" | "disabled" | "focused";
235237

236238
const styles: Record<string, Record<ButtonStylesKey, object>> = {};
237239

@@ -379,6 +381,8 @@ export const _generateStyles = (
379381
: {}),
380382
},
381383
pressed: pressStyles,
384+
// To receive programmatic focus.
385+
focused: focusStyles.focus[":focus-visible"],
382386
disabled: {
383387
cursor: "not-allowed",
384388
...disabledStatesStyles,

0 commit comments

Comments
 (0)