feat: Rac step list - #10601
feat: Rac step list#10601snowystinger wants to merge 11 commits into
Conversation
# Conflicts: # packages/react-aria/src/link/useLink.ts
|
Build successful! 🎉 |
|
Build successful! 🎉 |
| className={props.className ?? 'react-aria-StepList'}> | ||
| <StepListStateContext.Provider value={state}> | ||
| <StepListContext.Provider value={props}> | ||
| <CollectionRoot collection={collection} /> |
There was a problem hiding this comment.
We should persist the current and focused key. I suppose Tabs will have to consume via context, similar to the KeyboardDelegate TODO?
There was a problem hiding this comment.
sorry I'm not following, what's the concern here? virtualizer support?
There was a problem hiding this comment.
yes. I just wanted to highlight that we will encounter difficulty getting the current key persisted with the Tabs wrapper, which is a similar problem to the KeyboardDelegate.
| <div style={{display: 'flex', flexDirection: 'column', gap: '8px'}}> | ||
| <StepList {...args} onSelectionChange={handleSelectionChange} selectedKey={selectedKey} /> | ||
| <div> | ||
| <button onClick={() => setStepNumber(Math.max(stepNumber - 1, 1))}>Prev</button> | ||
| <button onClick={() => setStepNumber(Math.min(stepNumber + 1, keys.length))}>Next</button> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
I'm not sure whether we create an accessibility issue by allowing these to be rendered independently. Control elements should generally precede content in the tab sequence. Have you guys done research on this previously?
There was a problem hiding this comment.
Yeah, I was debating putting in a next/prev button context. It might be weird if someone's next is a form submission though and needs to do some other things.
So then I was instead thinking I might just expose it via state on the context/render props. But for the list one, there's not really a good place to get them.
I'm also revisiting our API and considering if we want to make this whole thing more like Nav Tree where it must always be controlled, in which case it's a little less weird.
In answer to your top level question:
Reviewing is welcome, just keep in mind I'm focused on the structure and accessibility right now and trying to decide between a list of links vs tabs vs something i haven't thought of yet. I'm not really looking at the state machine logic or styles or any of that unless it has a direct influence on the first goal (colours excepted).
There was a problem hiding this comment.
Reviewing is welcome, just keep in mind I'm focused on the structure and accessibility right now and trying to decide between a list of links vs tabs vs something i haven't thought of yet.
Yeah, I got that. Its tough. With Tabs we also get the problem of having to render a TabPanel. Tabs is also due for a rework if you followed our conversation in the Carousel RFC, so I'm not sure how much should be built on top before addressing some of that work.
I'm also revisiting our API and considering if we want to make this whole thing more like Nav Tree where it must always be controlled, in which case it's a little less weird.
I see the appeal, though I'm not sure it's that nice to leave control element placement up to user land. NavTree doesn't really see control elements associating with itself, but rather with the URL, so I don't think it was facing the same problem.
I will try to put some more thought into it as well. Perhaps its calling for two components, similar to Carousel, i.e. StepIndicator (ol & li, display only) & StepList (Tabs + control buttons)
|
Build successful! 🎉 |
## API Changes
react-aria-components/react-aria-components:Tab Tab {
+ aria-current?: AriaAttributes['aria-current']
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
className?: ClassNameOrFunction<TabRenderProps> = 'react-aria-Tab'
download?: boolean | string
href?: Href
hrefLang?: string
id?: Key
isDisabled?: boolean
onBlur?: (FocusEvent<Target>) => void
onClick?: (MouseEvent<FocusableElement>) => void
onFocus?: (FocusEvent<Target>) => void
onFocusChange?: (boolean) => void
onHoverChange?: (boolean) => void
onHoverEnd?: (HoverEvent) => void
onHoverStart?: (HoverEvent) => void
onPress?: (PressEvent) => void
onPressChange?: (boolean) => void
onPressEnd?: (PressEvent) => void
onPressStart?: (PressEvent) => void
onPressUp?: (PressEvent) => void
ping?: string
referrerPolicy?: HTMLAttributeReferrerPolicy
rel?: string
render?: (DetailedHTMLProps<LinkWithRequiredHref, HTMLAnchorElement> | React.JSX.IntrinsicElements[keyof React.JSX.IntrinsicElements], TabRenderProps) => ReactElement
routerOptions?: RouterOptions
style?: StyleOrFunction<T>
target?: HTMLAttributeAnchorTarget
}/react-aria-components:TabProps TabProps {
+ aria-current?: AriaAttributes['aria-current']
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
className?: ClassNameOrFunction<TabRenderProps> = 'react-aria-Tab'
download?: boolean | string
href?: Href
hrefLang?: string
id?: Key
isDisabled?: boolean
onBlur?: (FocusEvent<Target>) => void
onClick?: (MouseEvent<FocusableElement>) => void
onFocus?: (FocusEvent<Target>) => void
onFocusChange?: (boolean) => void
onHoverChange?: (boolean) => void
onHoverEnd?: (HoverEvent) => void
onHoverStart?: (HoverEvent) => void
onPress?: (PressEvent) => void
onPressChange?: (boolean) => void
onPressEnd?: (PressEvent) => void
onPressStart?: (PressEvent) => void
onPressUp?: (PressEvent) => void
ping?: string
referrerPolicy?: HTMLAttributeReferrerPolicy
rel?: string
render?: (DetailedHTMLProps<LinkWithRequiredHref, HTMLAnchorElement> | React.JSX.IntrinsicElements[keyof React.JSX.IntrinsicElements], TabRenderProps) => ReactElement
routerOptions?: RouterOptions
style?: StyleOrFunction<T>
target?: HTMLAttributeAnchorTarget
}/react-aria-components:StepList+StepList <T extends {}> {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ children?: ReactNode | (T) => ReactNode
+ className?: string = 'react-aria-StepList'
+ collection?: Collection<Node<T>>
+ defaultLastCompletedStep?: Key
+ defaultSelectedKey?: Key | null
+ dependencies?: ReadonlyArray<any>
+ disabledKeys?: Iterable<Key>
+ disallowEmptySelection?: boolean
+ id?: string
+ isDisabled?: boolean
+ isReadOnly?: boolean
+ items?: Iterable<T>
+ lastCompletedStep?: Key
+ onLastCompletedStepChange?: (Key | null) => void
+ onSelectionChange?: (Key) => void
+ render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
+ selectedKey?: Key | null
+ slot?: string | null
+ style?: CSSProperties
+}/react-aria-components:StepListContext+StepListContext {
+ UNTYPED
+}/react-aria-components:StepListStateContext+StepListStateContext {
+ UNTYPED
+}/react-aria-components:StepListItem+StepListItem {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ children?: ChildrenOrFunction<StepListItemRenderProps>
+ className?: ClassNameOrFunction<StepListItemRenderProps> = 'react-aria-StepListItem'
+ href?: string
+ id?: Key
+ render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, StepListItemRenderProps>
+ style?: StyleOrFunction<StepListItemRenderProps>
+}/react-aria-components:StepListProps+StepListProps <T> {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ children?: ReactNode | (T) => ReactNode
+ className?: string = 'react-aria-StepList'
+ collection?: Collection<Node<T>>
+ defaultLastCompletedStep?: Key
+ defaultSelectedKey?: Key | null
+ dependencies?: ReadonlyArray<any>
+ disabledKeys?: Iterable<Key>
+ disallowEmptySelection?: boolean
+ id?: string
+ isDisabled?: boolean
+ isReadOnly?: boolean
+ items?: Iterable<T>
+ lastCompletedStep?: Key
+ onLastCompletedStepChange?: (Key | null) => void
+ onSelectionChange?: (Key) => void
+ render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
+ selectedKey?: Key | null
+ slot?: string | null
+ style?: CSSProperties
+}/react-aria-components:StepListItemProps+StepListItemProps {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ children?: ChildrenOrFunction<StepListItemRenderProps>
+ className?: ClassNameOrFunction<StepListItemRenderProps> = 'react-aria-StepListItem'
+ href?: string
+ id?: Key
+ render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, StepListItemRenderProps>
+ style?: StyleOrFunction<StepListItemRenderProps>
+}/react-aria-components:StepListItemRenderProps+StepListItemRenderProps {
+ isCompleted: boolean
+ isCurrent: boolean
+ isDisabled: boolean
+ isFocusVisible: boolean
+ isFocused: boolean
+ isHovered: boolean
+ isPressed: boolean
+}@react-aria/steplist/@react-aria/steplist:AriaStepListProps AriaStepListProps <T> {
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
- children: CollectionChildren<T>
+ collection?: Collection<Node<T>>
defaultLastCompletedStep?: Key
defaultSelectedKey?: Key | null
disabledKeys?: Iterable<Key>
disallowEmptySelection?: boolean
isDisabled?: boolean
isReadOnly?: boolean
items?: Iterable<T>
lastCompletedStep?: Key
onLastCompletedStepChange?: (Key | null) => void
onSelectionChange?: (Key) => void
selectedKey?: Key | null
}/@react-aria/steplist:StepListItemAria StepListItemAria {
+ allowsSelection: boolean
+ hasAction: boolean
+ isDisabled: boolean
+ isFocused: boolean
+ isPressed: boolean
+ isSelected: boolean
+ linkProps: AriaLinkOptions
stepProps: HTMLAttributes<HTMLElement>
stepStateProps?: HTMLAttributes<HTMLElement>
stepStateText?: String
}@react-spectrum/s2/@react-spectrum/s2:Tab Tab {
UNSAFE_className?: UnsafeClassName
UNSAFE_style?: CSSProperties
+ aria-current?: AriaAttributes['aria-current']
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
download?: boolean | string
href?: Href
hrefLang?: string
id?: Key
isDisabled?: boolean
onBlur?: (FocusEvent<Target>) => void
onFocus?: (FocusEvent<Target>) => void
onFocusChange?: (boolean) => void
onHoverChange?: (boolean) => void
onHoverEnd?: (HoverEvent) => void
onHoverStart?: (HoverEvent) => void
onPress?: (PressEvent) => void
onPressChange?: (boolean) => void
onPressEnd?: (PressEvent) => void
onPressStart?: (PressEvent) => void
onPressUp?: (PressEvent) => void
ping?: string
referrerPolicy?: HTMLAttributeReferrerPolicy
rel?: string
routerOptions?: RouterOptions
styles?: StylesProp
target?: HTMLAttributeAnchorTarget
}/@react-spectrum/s2:TabProps TabProps {
UNSAFE_className?: UnsafeClassName
UNSAFE_style?: CSSProperties
+ aria-current?: AriaAttributes['aria-current']
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
download?: boolean | string
href?: Href
hrefLang?: string
id?: Key
isDisabled?: boolean
onBlur?: (FocusEvent<Target>) => void
onFocus?: (FocusEvent<Target>) => void
onFocusChange?: (boolean) => void
onHoverChange?: (boolean) => void
onHoverEnd?: (HoverEvent) => void
onHoverStart?: (HoverEvent) => void
onPress?: (PressEvent) => void
onPressChange?: (boolean) => void
onPressEnd?: (PressEvent) => void
onPressStart?: (PressEvent) => void
onPressUp?: (PressEvent) => void
ping?: string
referrerPolicy?: HTMLAttributeReferrerPolicy
rel?: string
routerOptions?: RouterOptions
styles?: StylesProp
target?: HTMLAttributeAnchorTarget
}@react-spectrum/steplist/@react-spectrum/steplist:StepList StepList <T extends {}> {
UNSAFE_className?: string
UNSAFE_style?: CSSProperties
alignSelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'center' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'stretch'>
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
bottom?: Responsive<DimensionValue>
- children: CollectionChildren<{}>
+ collection?: Collection<Node<{}>>
defaultLastCompletedStep?: Key
defaultSelectedKey?: Key | null
disabledKeys?: Iterable<Key>
disallowEmptySelection?: boolean
flex?: Responsive<string | number | boolean>
flexBasis?: Responsive<number | string>
flexGrow?: Responsive<number>
flexShrink?: Responsive<number>
gridArea?: Responsive<string>
gridColumn?: Responsive<string>
gridColumnEnd?: Responsive<string>
gridColumnStart?: Responsive<string>
gridRow?: Responsive<string>
gridRowEnd?: Responsive<string>
gridRowStart?: Responsive<string>
height?: Responsive<DimensionValue>
id?: string
isDisabled?: boolean
isEmphasized?: boolean = false
isHidden?: Responsive<boolean>
isReadOnly?: boolean
items?: Iterable<{}>
justifySelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'center' | 'left' | 'right' | 'stretch'>
lastCompletedStep?: Key
left?: Responsive<DimensionValue>
margin?: Responsive<DimensionValue>
marginBottom?: Responsive<DimensionValue>
marginEnd?: Responsive<DimensionValue>
marginStart?: Responsive<DimensionValue>
marginTop?: Responsive<DimensionValue>
marginX?: Responsive<DimensionValue>
marginY?: Responsive<DimensionValue>
maxHeight?: Responsive<DimensionValue>
maxWidth?: Responsive<DimensionValue>
minHeight?: Responsive<DimensionValue>
minWidth?: Responsive<DimensionValue>
onLastCompletedStepChange?: (Key | null) => void
onSelectionChange?: (Key) => void
order?: Responsive<number>
orientation?: Orientation = 'horizontal'
position?: Responsive<'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'>
right?: Responsive<DimensionValue>
selectedKey?: Key | null
size?: 'S' | 'M' | 'L' | 'XL' = 'M'
start?: Responsive<DimensionValue>
top?: Responsive<DimensionValue>
width?: Responsive<DimensionValue>
zIndex?: Responsive<number>
}/@react-spectrum/steplist:SpectrumStepListProps SpectrumStepListProps <T> {
UNSAFE_className?: string
UNSAFE_style?: CSSProperties
alignSelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'center' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'stretch'>
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
bottom?: Responsive<DimensionValue>
- children: CollectionChildren<T>
+ collection?: Collection<Node<T>>
defaultLastCompletedStep?: Key
defaultSelectedKey?: Key | null
disabledKeys?: Iterable<Key>
disallowEmptySelection?: boolean
flex?: Responsive<string | number | boolean>
flexBasis?: Responsive<number | string>
flexGrow?: Responsive<number>
flexShrink?: Responsive<number>
gridArea?: Responsive<string>
gridColumn?: Responsive<string>
gridColumnEnd?: Responsive<string>
gridColumnStart?: Responsive<string>
gridRow?: Responsive<string>
gridRowEnd?: Responsive<string>
gridRowStart?: Responsive<string>
height?: Responsive<DimensionValue>
id?: string
isDisabled?: boolean
isEmphasized?: boolean = false
isHidden?: Responsive<boolean>
isReadOnly?: boolean
items?: Iterable<T>
justifySelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'center' | 'left' | 'right' | 'stretch'>
lastCompletedStep?: Key
left?: Responsive<DimensionValue>
margin?: Responsive<DimensionValue>
marginBottom?: Responsive<DimensionValue>
marginEnd?: Responsive<DimensionValue>
marginStart?: Responsive<DimensionValue>
marginTop?: Responsive<DimensionValue>
marginX?: Responsive<DimensionValue>
marginY?: Responsive<DimensionValue>
maxHeight?: Responsive<DimensionValue>
maxWidth?: Responsive<DimensionValue>
minHeight?: Responsive<DimensionValue>
minWidth?: Responsive<DimensionValue>
onLastCompletedStepChange?: (Key | null) => void
onSelectionChange?: (Key) => void
order?: Responsive<number>
orientation?: Orientation = 'horizontal'
position?: Responsive<'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'>
right?: Responsive<DimensionValue>
selectedKey?: Key | null
size?: 'S' | 'M' | 'L' | 'XL' = 'M'
start?: Responsive<DimensionValue>
top?: Responsive<DimensionValue>
width?: Responsive<DimensionValue>
zIndex?: Responsive<number>
}@react-stately/steplist/@react-stately/steplist:useStepListState useStepListState <T extends {}> {
- props: StepListProps<T>
+ props: StepListStateOptions<T>
returnVal: undefined
}/@react-stately/steplist:StepListStateOptions+StepListStateOptions <T> {
+ collection?: Collection<Node<T>>
+ defaultLastCompletedStep?: Key
+ defaultSelectedKey?: Key | null
+ disabledKeys?: Iterable<Key>
+ disallowEmptySelection?: boolean
+ isDisabled?: boolean
+ isReadOnly?: boolean
+ items?: Iterable<T>
+ lastCompletedStep?: Key
+ onLastCompletedStepChange?: (Key | null) => void
+ onSelectionChange?: (Key) => void
+ selectedKey?: Key | null
+} |
Closes RSP Component Milestones (view),
Picking up this previously spectrum alpha component. Revisiting the accessibility first, I've create two methods of implementing a steplist/wizard/stepper.
The first one is based on WAI tutorials and the work we did on SideNav/NavigationTree.
The other is based on Tabs, which is a natural component to use here because the steps usually control a panel of some variety, one at a time. The APIs and structure for Tabs already handle most of the use case, so it's less overall code.
We need to test on various devices and screen reader technologies to see which one behaves better or what we want to modify. We'll also run this past the a11y team.
RSP Component Milestones (view)
RSP Component Milestones (view)
✅ Pull Request Checklist:
📝 Test Instructions:
🧢 Your Project: