-
Notifications
You must be signed in to change notification settings - Fork 55
feat(Accessibility): Added focus handling for List component #256
Conversation
src/components/List/List.tsx
Outdated
@@ -90,7 +108,13 @@ class List extends UIComponent<Extendable<IListProps>, any> { | |||
const { items } = this.props | |||
const itemProps = _.pick(this.props, List.itemProps) | |||
|
|||
return _.map(items, item => ListItem.create(item, { defaultProps: itemProps })) | |||
return _.map(items, (item, idx) => { | |||
itemProps.focusableItemProps = this.focusContainer.assignAtomicItemsProps(idx, items.length) |
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.
probably, prepareItemProps
or createItemProps
(with Item
being a singular form) would be better, due to this method
- prepares props for single item element
- there is no assignment for the result props object made there
- probably, we don't need to repeat
Atomic
in method's names
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.
renamed
src/components/List/ListItem.tsx
Outdated
import { ComponentVariablesInput, ComponentPartStyle } from '../../../types/theme' | ||
import { Extendable } from '../../../types/utils' | ||
|
||
export interface IListItemProps { | ||
accessibility?: Accessibility | ||
as?: any | ||
focusableItemProps?: IFocusableItemProps |
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.
nit: for the sake of consistency, lets preserve alphabetical order
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.
done
src/components/List/ListItem.tsx
Outdated
@@ -35,6 +46,7 @@ class ListItem extends UIComponent<Extendable<IListItemProps>, any> { | |||
|
|||
static propTypes = { | |||
as: customPropTypes.as, | |||
focusableItemProps: PropTypes.object, |
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.
nit: maybe we should place it closer to accessibility
prop
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.
done
src/components/List/ListItem.tsx
Outdated
@@ -75,7 +87,17 @@ class ListItem extends UIComponent<Extendable<IListItemProps>, any> { | |||
accessibility: listItemBehavior as Accessibility, | |||
} | |||
|
|||
state: any = {} | |||
constructor(props: IListItemProps, context: any) { |
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.
context
should be omitted as well :) We are not supporting legacy React API. Also, in either case, there is no need to provide this arg here
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.
done
src/components/List/ListItem.tsx
Outdated
@@ -85,6 +107,10 @@ class ListItem extends UIComponent<Extendable<IListItemProps>, any> { | |||
this.setState({ isHovering: false }) | |||
} | |||
|
|||
componentDidUpdate() { | |||
this.focusableItem.tryFocus(ReactDOM.findDOMNode(this.itemRef.current!) as HTMLElement) |
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.
this is better to be rewritten the following way, as we will reduce amount of casts used by one:
ReactDOM.findDOMNode(this.itemRef.current) as HTMLElement
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.
done
@@ -11,6 +11,7 @@ const selectableListItemBehavior: Accessibility = (props: any) => ({ | |||
root: { | |||
role: 'option', | |||
'aria-selected': !!props['active'], | |||
tabIndex: props.focusableItemProps.isFocused ? '0' : '-1', |
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.
seems that we should at least specify type for props
here, so that it will be clear from which context focusableItemProps
come from. Also, we should think about following thing - what if this behavior will be applied to the component that doesn't have this prop support (for some reason, like some custom component that haven't introduced support for FocusableContainer
- to support some custom focus handling logic, for instance)? In that case we shouldn't make tabIndex
assignments here - otherwise behavior for selectableListItem
will be tightly coupled with the focus handling one.
May suggest to either
- consider to merge focus handling to be a thing that is defined by accessibility behavior (so that there won't be any code like
FocusContainer.create
in the component's code - all this will come as accessibility behavior concern). See this option is a preferable one - or decouple these two completely - so that focus handling logic will be entirely responsible for handling
tabIndex
attributes as well
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 the next round of refactoring
agreed with Sergey to address several follow-up points by means of PRs that will come next. For the sake of making the roadmap clear, will file an issue item where all these findings will be summarised - and after that merge the PR 👍 In general, lets scope the area to which this approach is applied to |
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.
Let me introduce focus handling for List component, which is based on the idea of and roving tabindex technique. Also, the idea is based on the current implementation of accessibility behaviors and key handlers. This implementation is based on the research and PoC located in the accessibility repository
Container/AtomicItem hierarchy
A lot of components can be presented as a hierarchical structure as container and item(s), each item may also contain a nested container with its own items. Having this in mind, we can encapsulate focus handling into two components and inject the corresponding functionality into the React component. When container created it can set the first (or any other) item to be focused. The item itself handles the pressed key and fires the corresponding event. Container receives this event and does what is requested as it knows about children, how to handle them and when they have to be updated and how.
Roving tabindex technique
Setting the
tabindex
of the focused element to "0" ensures that if the user tabs away from the widget and then return, the selected item within the group retains focus. Note that updating thetabindex
to "0" requires also updating the previously selected item totabindex="-1"
. This technique involves programmatically moving the focus in response to key events and updating thetabindex
to reflect the currently focused item.more details about roving tabindex