-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tabs: improve focus behavior #55287
Tabs: improve focus behavior #55287
Changes from all commits
f47b595
11d34e2
90617a3
0fc9c68
02dda6e
e779ff9
fcd555d
096f66a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,9 @@ type Tab = { | |
icon?: IconType; | ||
disabled?: boolean; | ||
}; | ||
tabpanel?: { | ||
focusable?: boolean; | ||
}; | ||
}; | ||
|
||
const TABS: Tab[] = [ | ||
|
@@ -83,7 +86,11 @@ const UncontrolledTabs = ( { | |
) ) } | ||
</Tabs.TabList> | ||
{ tabs.map( ( tabObj ) => ( | ||
<Tabs.TabPanel key={ tabObj.id } id={ tabObj.id }> | ||
<Tabs.TabPanel | ||
key={ tabObj.id } | ||
id={ tabObj.id } | ||
focusable={ tabObj.tabpanel?.focusable } | ||
> | ||
{ tabObj.content } | ||
</Tabs.TabPanel> | ||
) ) } | ||
|
@@ -184,6 +191,63 @@ describe( 'Tabs', () => { | |
); | ||
} ); | ||
} ); | ||
describe( 'Focus Behavior', () => { | ||
it( 'should focus on the related TabPanel when pressing the Tab key', async () => { | ||
const user = userEvent.setup(); | ||
|
||
render( <UncontrolledTabs tabs={ TABS } /> ); | ||
|
||
const selectedTabPanel = await screen.findByRole( 'tabpanel' ); | ||
|
||
// Tab should initially focus the first tab in the tablist, which | ||
// is Alpha. | ||
await user.keyboard( '[Tab]' ); | ||
expect( | ||
await screen.findByRole( 'tab', { name: 'Alpha' } ) | ||
).toHaveFocus(); | ||
|
||
// By default the tabpanel should receive focus | ||
await user.keyboard( '[Tab]' ); | ||
expect( selectedTabPanel ).toHaveFocus(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In your testing instructions, you have the following step:
Since you mentioned it in your testing instructions, maybe it would be worth including in the test? Something like:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for clarifying! 👍 |
||
} ); | ||
it( 'should not focus on the related TabPanel when pressing the Tab key if `focusable: false` is set', async () => { | ||
const user = userEvent.setup(); | ||
|
||
const TABS_WITH_ALPHA_FOCUSABLE_FALSE = TABS.map( ( tabObj ) => | ||
tabObj.id === 'alpha' | ||
? { | ||
...tabObj, | ||
content: ( | ||
<> | ||
Selected Tab: Alpha | ||
<button>Alpha Button</button> | ||
</> | ||
), | ||
tabpanel: { focusable: false }, | ||
} | ||
: tabObj | ||
); | ||
|
||
render( | ||
<UncontrolledTabs tabs={ TABS_WITH_ALPHA_FOCUSABLE_FALSE } /> | ||
); | ||
|
||
const alphaButton = await screen.findByRole( 'button', { | ||
name: /alpha button/i, | ||
} ); | ||
|
||
// Tab should initially focus the first tab in the tablist, which | ||
// is Alpha. | ||
await user.keyboard( '[Tab]' ); | ||
expect( | ||
await screen.findByRole( 'tab', { name: 'Alpha' } ) | ||
).toHaveFocus(); | ||
// Because the alpha tabpanel is set to `focusable: false`, pressing | ||
// the Tab key should focus the button, not the tabpanel | ||
await user.keyboard( '[Tab]' ); | ||
expect( alphaButton ).toHaveFocus(); | ||
} ); | ||
} ); | ||
|
||
describe( 'Tab Attributes', () => { | ||
it( "should apply the tab's `className` to the tab button", async () => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -128,7 +128,7 @@ export type TabPanelProps = { | |
*/ | ||
children?: React.ReactNode; | ||
/** | ||
* A unique identifier for the TabPanel, which is used to generate a unique `id` for the underlying element. | ||
* A unique identifier for the tabpanel, which is used to generate a unique `id` for the underlying element. | ||
*/ | ||
id: string; | ||
/** | ||
|
@@ -139,4 +139,12 @@ export type TabPanelProps = { | |
* Custom CSS styles for the rendered `TabPanel` component. | ||
*/ | ||
style?: React.CSSProperties; | ||
/** | ||
* Determines whether or not the tabpanel element should be focusable. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. I made one small change, but I didn't update the |
||
* If `false`, pressing the tab key will skip over the tabpanel, and instead | ||
* focus on the first focusable element in the panel (if there is one). | ||
* | ||
* @default true | ||
*/ | ||
focusable?: boolean; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you run into any flaky test behavior? When watching the tests, sometimes I'll get a random test failing. For an example:
It'll pass the next 4-5 times then fail again. I'm wondering if it's something related to past flaky test issues we've enountered in the ariakit migrations: #52133 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. If I run them a bunch in a row, I do see that. 🤔 Took me probably close to 20 runs to trigger it, but there it is. I can actually see two tests doing it:
Tabs › Uncontrolled mode › Disabled tab › should disable the tab when
disabledis
true`Tabs › Tab Activation › should not focus the next tab when the Tab key is pressed
Maybe it's related to that other issue you mentioned, but if it is, the same fix isn't helping us here... one of those failures is actually already using
findByRole
.I'll note this to investigate in a followup, that way if we deploy something to fix it, it isn't wrapped up in this change!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For tooltip, we added a helper function to ensure everything was resolved before moving to the next test. So I wonder if it would help to add another
user.tab()
at the end of each test (or to specific tests).Good plan!