Skip to content
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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

devongovett
Copy link
Member

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.

@rspbot
Copy link

rspbot commented Jan 11, 2025

@devongovett devongovett mentioned this pull request Jan 12, 2025
5 tasks
Copy link
Member

@snowystinger snowystinger left a 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.

packages/@react-spectrum/s2/src/Tabs.tsx Outdated Show resolved Hide resolved
packages/@react-spectrum/s2/src/Tabs.tsx Show resolved Hide resolved
packages/@react-spectrum/s2/src/Tabs.tsx Outdated Show resolved Hide resolved
}
prevFocused.current = state?.selectionManager.isFocused;
}, [state?.selectionManager.isFocused, state?.selectionManager.focusedKey, showItems]);
// let prevFocused = useRef<boolean | undefined>(false);
Copy link
Member

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

Copy link
Member

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

@devongovett
Copy link
Member Author

Is this going to be confusing to users, swapping between two completely different components?

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 tabpanel without a tablist so group seems better?

@snowystinger
Copy link
Member

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 tabpanel without a tablist so group seems better?

Ok, yep, dang my recollection of v3 was bad on this one. Silly vacation-addled brain.
I think we can improve it with the addition of a label to the Picker saying "Tabs" somewhere in it, but I'm fine for that to come later.

# Conflicts:
#	packages/@react-spectrum/s2/src/Tabs.tsx
@rspbot
Copy link

rspbot commented Jan 14, 2025

}})({density})}>
<Picker
id={id}
aria-label={props['aria-label'] ?? stringFormatter.format('tabs.selectorLabel')}
Copy link
Member

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,
Copy link
Member

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

@snowystinger snowystinger marked this pull request as ready for review January 14, 2025 04:08
@snowystinger snowystinger changed the title wip: Refactor S2 tabs to fix accessibility issues chore: Refactor S2 tabs to fix accessibility issues Jan 14, 2025
@rspbot
Copy link

rspbot commented Jan 14, 2025

@rspbot
Copy link

rspbot commented Jan 14, 2025

@rspbot
Copy link

rspbot commented Jan 14, 2025

@rspbot
Copy link

rspbot commented Jan 14, 2025

@rspbot
Copy link

rspbot commented Jan 14, 2025

@rspbot
Copy link

rspbot commented Jan 14, 2025

## 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
 }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants