-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
chore: Refactor S2 tabs to fix accessibility issues #7600
base: main
Are you sure you want to change the base?
Conversation
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.
Is this going to be confusing to users, swapping between two completely different components?
Especially if focus is lost, it may be non-obvious that you're back on the tab group because when it collapses, it no longer announces as a tab group. Maybe we can improve it by adding that to the Select's label.
I'm fine with the approach, I assume it aligns with the collapse work you were doing before across all components? you'd said you were able to get rid of the custom renderer there as well.
} | ||
prevFocused.current = state?.selectionManager.isFocused; | ||
}, [state?.selectionManager.isFocused, state?.selectionManager.focusedKey, showItems]); | ||
// let prevFocused = useRef<boolean | undefined>(false); |
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.
Noticed that both in the previous implementation and this one, swapping between the components will lose focus. I swear I had that addressed at one point, but we should probably include something so that doesn't occur. I suspect it was linked with this section of code originally
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.
It was definitely this code, however, it didn't restore back when the picker expanded back to the tabs, so I've omitted it for now to simplify getting the PR in
We can add that behavior later
That's pretty much what v3 does right? When it's collapsed the tablist is removed from the a11y tree. And it seemed kinda weird to have a |
Ok, yep, dang my recollection of v3 was bad on this one. Silly vacation-addled brain. |
# Conflicts: # packages/@react-spectrum/s2/src/Tabs.tsx
}})({density})}> | ||
<Picker | ||
id={id} | ||
aria-label={props['aria-label'] ?? stringFormatter.format('tabs.selectorLabel')} |
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.
Re-added a default here since aria-label isn't required for Tabs, but it is required for a picker that has no visible label.
const tabPanel = style({ | ||
display: 'flex', | ||
marginTop: 4, | ||
marginX: -4, |
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.
i adjusted some styles in here to make it easier to create a panel that scrolls for overflow and compensates for our focus halos automatically
it also automatically lays out in such a way that users can place a child which will stretch to the fill the space because it's now default display: flex
let me know if this is too controversial
another option was to use allowedOverrides for styles and add display/padding to it
## API Changes
@react-spectrum/s2/@react-spectrum/s2:Tabs Tabs {
UNSAFE_className?: string
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
children?: ReactNode
defaultSelectedKey?: Key
density?: 'compact' | 'regular' = 'regular'
disabledKeys?: Iterable<Key>
id?: string
isDisabled?: boolean
keyboardActivation?: 'automatic' | 'manual' = 'automatic'
+ labelBehavior?: 'show' | 'hide' = 'show'
onSelectionChange?: (Key) => void
orientation?: Orientation = 'horizontal'
selectedKey?: Key | null
slot?: string | null
} /@react-spectrum/s2:TabList TabList <T extends {}> {
UNSAFE_className?: string
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
- children?: ReactNode
+ children?: ReactNode | (T) => ReactNode
dependencies?: Array<any>
items?: Iterable<T>
styles?: StylesProp
} /@react-spectrum/s2:Tab Tab {
UNSAFE_className?: string
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
- children?: ReactNode
+ children: ReactNode
download?: boolean | string
href?: Href
hrefLang?: string
id?: Key
onHoverChange?: (boolean) => void
onHoverEnd?: (HoverEvent) => void
onHoverStart?: (HoverEvent) => void
ping?: string
referrerPolicy?: HTMLAttributeReferrerPolicy
rel?: string
routerOptions?: RouterOptions
styles?: StylesProp
target?: HTMLAttributeAnchorTarget
} /@react-spectrum/s2:TabsProps TabsProps {
UNSAFE_className?: string
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
children?: ReactNode
defaultSelectedKey?: Key
density?: 'compact' | 'regular' = 'regular'
disabledKeys?: Iterable<Key>
id?: string
isDisabled?: boolean
keyboardActivation?: 'automatic' | 'manual' = 'automatic'
+ labelBehavior?: 'show' | 'hide' = 'show'
onSelectionChange?: (Key) => void
orientation?: Orientation = 'horizontal'
selectedKey?: Key | null
slot?: string | null
} /@react-spectrum/s2:TabProps TabProps {
UNSAFE_className?: string
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
- children?: ReactNode
+ children: ReactNode
download?: boolean | string
href?: Href
hrefLang?: string
id?: Key
onHoverChange?: (boolean) => void
onHoverEnd?: (HoverEvent) => void
onHoverStart?: (HoverEvent) => void
ping?: string
referrerPolicy?: HTMLAttributeReferrerPolicy
rel?: string
routerOptions?: RouterOptions
styles?: StylesProp
target?: HTMLAttributeAnchorTarget
} /@react-spectrum/s2:TabListProps TabListProps <T> {
UNSAFE_className?: string
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
- children?: ReactNode
+ children?: ReactNode | (T) => ReactNode
dependencies?: Array<any>
items?: Iterable<T>
styles?: StylesProp
} |
Collapsed S2 tabs currently has a Picker inside a TabList, which is invalid aria according to axe. This refactors it to swap between RAC tabs and a simple picker + Group, which is similar to what we had in v3.
Probably some stuff still broken here, just a first pass.