Skip to content

Commit 415dd5a

Browse files
authored
[WB-2152] ActionMenu with custom opener not triggering focused prop change (#2864)
## Summary: - Converted `DropdownOpener` in a function component. - Fixed `DropdownOpener` to propagate the onBlur event correctly. This change allows to update the focused styles when the opener is blurred. Issue: https://khanacademy.atlassian.net/browse/WB-2152 ## Test plan: 1. Navigate to the ActionMenu with custom opener story: `/?path=/story/packages-dropdown-actionmenu--custom-opener` 2. Press `tab` until the focus is set on the opener. 3. Press `Enter` or `Space` to open the menu. 4. Press `Escape` to close it. 5. Verify that the focus returns to the opener. 6. Press `tab` again and ensure that the focus styles are removed from the opener. BONUS: Verify that the `SingleSelect` and `MultiSelect` with custom opener stories continue working as expected. - `/?path=/story/packages-dropdown-singleselect--custom-opener` - `/?path=/story/packages-dropdown-multiselect--custom-opener` Author: jandrade Reviewers: jandrade, marcysutton, nedredmond Required Reviewers: Approved By: marcysutton Checks: ✅ 12 checks were successful, ⏭️ 3 checks have been skipped Pull Request URL: #2864
1 parent d0f325d commit 415dd5a

File tree

2 files changed

+57
-51
lines changed

2 files changed

+57
-51
lines changed

.changeset/heavy-roses-try.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/wonder-blocks-dropdown": patch
3+
---
4+
5+
Fixes DropdownOpener to propagate onBlur event correctly. This change allows to update the focused styles when the opener is blurred.

packages/wonder-blocks-dropdown/src/components/dropdown-opener.tsx

Lines changed: 52 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type Props = Partial<Omit<AriaProps, "aria-disabled">> & {
2222
/**
2323
* Whether the opener is disabled. If disabled, disallows interaction.
2424
*/
25-
disabled: boolean;
25+
disabled?: boolean;
2626
/**
2727
* Callback for when the opener is pressed.
2828
*/
@@ -57,52 +57,46 @@ type Props = Partial<Omit<AriaProps, "aria-disabled">> & {
5757
role: "combobox" | "button";
5858
};
5959

60-
type DefaultProps = {
61-
disabled: Props["disabled"];
62-
};
63-
64-
class DropdownOpener extends React.Component<Props> {
65-
static defaultProps: DefaultProps = {
66-
disabled: false,
67-
};
68-
69-
getTestIdFromProps: (childrenProps?: any) => string = (childrenProps) => {
70-
return childrenProps.testId || childrenProps["data-testid"];
71-
};
60+
const DropdownOpener = React.forwardRef<HTMLElement, Props>((props, ref) => {
61+
const {
62+
disabled = false,
63+
testId,
64+
text,
65+
opened,
66+
"aria-controls": ariaControls,
67+
"aria-haspopup": ariaHasPopUp,
68+
"aria-required": ariaRequired,
69+
"aria-label": ariaLabel,
70+
id,
71+
role,
72+
onBlur,
73+
onClick,
74+
children,
75+
error,
76+
} = props;
7277

73-
renderAnchorChildren(
78+
const renderAnchorChildren = (
7479
eventState: ClickableState,
7580
clickableChildrenProps: ChildrenProps,
76-
): React.ReactElement {
77-
const {
78-
disabled,
79-
testId,
80-
text,
81-
opened,
82-
"aria-controls": ariaControls,
83-
"aria-haspopup": ariaHasPopUp,
84-
"aria-required": ariaRequired,
85-
id,
86-
role,
87-
onBlur,
88-
} = this.props;
89-
const renderedChildren = this.props.children({
81+
): React.ReactElement => {
82+
const renderedChildren = children({
9083
...eventState,
9184
text,
9285
opened,
9386
});
9487
const childrenProps = renderedChildren.props;
95-
const childrenTestId = this.getTestIdFromProps(childrenProps);
88+
const childrenTestId =
89+
childrenProps?.testId || childrenProps?.["data-testid"];
9690

9791
// If custom opener has `aria-label`, prioritize that.
9892
// If parent component has `aria-label`, fall back to that next.
99-
const renderedAriaLabel =
100-
childrenProps["aria-label"] ?? this.props["aria-label"];
93+
const renderedAriaLabel = childrenProps["aria-label"] ?? ariaLabel;
10194

10295
return React.cloneElement(renderedChildren, {
10396
...clickableChildrenProps,
97+
ref,
10498
"aria-label": renderedAriaLabel ?? undefined,
105-
"aria-invalid": this.props.error,
99+
"aria-invalid": error,
106100
disabled,
107101
"aria-controls": ariaControls,
108102
role,
@@ -122,26 +116,33 @@ class DropdownOpener extends React.Component<Props> {
122116
// try to get the testId from the child element
123117
// If it's not set, try to fallback to the parent's testId
124118
"data-testid": childrenTestId || testId,
125-
onBlur,
119+
onBlur: onBlur
120+
? (e: React.FocusEvent) => {
121+
// This is done to avoid overriding a custom onBlur
122+
// handler inside the children node
123+
onBlur(e);
124+
clickableChildrenProps.onBlur(e);
125+
}
126+
: clickableChildrenProps.onBlur,
126127
});
127-
}
128+
};
129+
130+
return (
131+
<ClickableBehavior
132+
onClick={onClick}
133+
disabled={disabled}
134+
// Allows the opener to be focused with the keyboard, which ends
135+
// up triggering onFocus/onBlur events needed to re-render the
136+
// dropdown opener.
137+
tabIndex={0}
138+
>
139+
{(eventState, handlers) =>
140+
renderAnchorChildren(eventState, handlers)
141+
}
142+
</ClickableBehavior>
143+
);
144+
});
128145

129-
render(): React.ReactNode {
130-
return (
131-
<ClickableBehavior
132-
onClick={this.props.onClick}
133-
disabled={this.props.disabled}
134-
// Allows the opener to be focused with the keyboard, which ends
135-
// up triggering onFocus/onBlur events needed to re-render the
136-
// dropdown opener.
137-
tabIndex={0}
138-
>
139-
{(eventState, handlers) =>
140-
this.renderAnchorChildren(eventState, handlers)
141-
}
142-
</ClickableBehavior>
143-
);
144-
}
145-
}
146+
DropdownOpener.displayName = "DropdownOpener";
146147

147148
export default DropdownOpener;

0 commit comments

Comments
 (0)