-
Notifications
You must be signed in to change notification settings - Fork 1.4k
TagGroup #1922
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
Conversation
|
Looking for initial review on implementation. Things I'm not quite about:
Need to:
|
|
Build successful! 🎉 |
|
Latest TODO list:
|
|
[API] cycleMode prop (as mentioned in earlier comment) @snowystinger would you happen to know what these 2 points need to address? It's a bit on the vague side |
|
Build successful! 🎉 |
|
Build successful! 🎉 |
|
Build successful! 🎉 |
|
Build successful! 🎉 |
|
Build successful! 🎉 |
LFDanLu
left a 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.
Some addition things I noted but I'm fine with handling them in a followup PR
| // Ignore disabled tags | ||
| if (this.disabledKeys.has(newKey)) { | ||
| return this.getKeyBelow(newKey); | ||
| } |
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.
Open question: should we be skipping the disabled tags?
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.
ButtonGroup, ActionGroup, etc. do so I think so.
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'm not sure, since tags are both data as well as interactive elements. Can any tag be disabled? or only the ones with some sort of interactive capability such as close?
Can probably sort out in a later PR, but I don't know if the answer is as clearcut as I would like
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.
Originally I thought disabled tags should get skipped bc ActionGroup items do too, but if we should be able to delete a disabled tag then they need to be focusable. However it becomes an interactive element which then needs 4.5:1 color contrast which it won't get. Also agree it doesn't need to be a blocker for this PR
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.
Will address/continue conversation in followup PR
| let {rowProps} = useGridRow({ | ||
| node: item | ||
| }, state, tagRowRef); | ||
| // Don't want the row to be focusable or accessible via keyboard | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| let {tabIndex, ...otherRowProps} = rowProps; | ||
|
|
||
| let {gridCellProps} = useGridCell({ | ||
| node: [...item.childNodes][0], | ||
| focusMode: 'cell' | ||
| }, state, tagRef); |
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.
Followup question for next PR: should we be focusing the row or the cell in the TagGroup? Will have to experiment because I had run into issues making focus stay on the row in CardView myself. v2 focused the rows rather that the cells
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.
Will address/continue conversation in followup PR
| onFocus() { | ||
| state.selectionManager.setFocusedKey(item.childNodes[0].key); | ||
| }, |
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.
curious, what prompted the addition of this? Was it to override the onFocus from useGridCell? If so I don't think you need to since the raf won't trigger here since we have focusMode: cell.
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.
Will address in followup PR
| {...styleProps} | ||
| {...mergeProps(tagProps, hoverProps)} | ||
| {...mergeProps(tagProps, hoverProps, focusProps)} | ||
| role="gridcell" |
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.
can get rid of this role since it comes from tagProps
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.
Will address in followup PR
| // add column of clear buttons if removable | ||
| if (isRemovable) { | ||
| childNodes.push({ | ||
| key: `remove-${item.key}`, | ||
| type: 'cell', | ||
| index: 1, | ||
| value: null, | ||
| level: 0, | ||
| rendered: null, | ||
| textValue: item.textValue, // TODO localize? | ||
| hasChildNodes: false, | ||
| childNodes: [] | ||
| }); | ||
| } |
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.
Think we can remove this since we aren't supporting keyboard focusing the clear button separately? We can keep if there is future plans to reimplement that behavior. Same with the columnCount above
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.
Will address in followup PR
|
|
||
| // Don't want the grid to be focusable or accessible via keyboard | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| let {tabIndex, ...otherGridProps} = gridProps; |
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.
Do we need this? Feel like we should keep the tabIndex on the TagGroup and just add css to prevent the outline flicker from happening
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 was running into focus issues with keyboard nav when the group had a tabIndex. for the most part i was trying emulate ActionGroup's tabIndex-ing
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.
Will look into it more in followup PR
| */ | ||
|
|
||
| /// <reference types="css-module-types" /> | ||
|
|
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.
Should probably export Item as well like how ActionGroup and other components that use Item do
export {Item} from '@react-stately/collections';
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.
Will address in followup PR
| export interface SpectrumTagProps<T> extends TagProps<T> { | ||
| state: GridState<any, 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.
Don't think we expose Tag right? Can probably move this to TagProps or even send it through a context provider instead of through props
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.
Will address in followup PR
| const formatMessage = useMessageFormatter(intlMessages); | ||
| const removeString = formatMessage('remove'); | ||
| const tagId = useId(); | ||
| const labelId = useId(); |
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.
labelId is only returned in the aria-labelledby for clearButtonProps and is not assigned as an id for the Tag's label.
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.
Will address in followup PR
| clearButtonProps: mergeProps(pressProps, { | ||
| 'aria-label': removeString, | ||
| 'aria-labelledby': `${buttonId} ${tagId}`, | ||
| 'aria-labelledby': `${buttonId} ${labelId}`, |
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.
labelId is an invalid id reference because it is not applied as an id for the Tab's label element.
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.
Will address in followup PR
|
Build successful! 🎉 |
| icon: {UNSAFE_className: classNames(styles, 'spectrum-Tag-icon'), size: 'XS'}, | ||
| text: {UNSAFE_className: classNames(styles, 'spectrum-Tag-content', {'tags-removable': isRemovable})} | ||
| }}> | ||
| {typeof children === 'string' ? <div ref={labelRef} {...labelProps}><Text>{children}</Text></div> : children} |
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.
isn't this fundamentally different from what a user would pass in? I think it should be
| {typeof children === 'string' ? <div ref={labelRef} {...labelProps}><Text>{children}</Text></div> : children} | |
| {typeof children === 'string' ? <Text>{children}</Text> : children} |
where labelRef and labelProps are passed on the text slot? though what is the labelRef actually used for? i don't see it passed anywhere, looks like it's created and assigned, nothing else. So we might be able to just delete that.
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.
The labelProps has the labelId which in this case is used for voiceover in the remove scenario. I don't mind removing labelRef and any other unused refs.
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 think we need to remove that extra div as well though, it's not something that the user would provide, which means that providing a string vs providing React element children would be different right?
<Tag>apples</Tag>
would be labelled differently than
<Tag><Text>apples</Text></Tag>
Or am I reading into this the wrong way?
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.
you're right. i didn't consider that when adding labelProps. i might have to restyle the children scenario
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.
Will address in followup PR
ktabors
left a 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.
Generally looks fine. I think a number fo Daniel's concerns need to be addressed.
| @@ -0,0 +1,91 @@ | |||
| /* | |||
| * Copyright 2020 Adobe. All rights reserved. | |||
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.
Shouldn't hold this up, but the copyright should be 2022.
| id: buttonId, | ||
| title: removeString, | ||
| isDisabled, | ||
| role |
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 why when I turn on OSX voiceover and ctl-opt-arrow to the ClearButton it only says "dimmed" and not "dimmed and disabled button"?
| @@ -1,50 +0,0 @@ | |||
| /* | |||
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 see @snowystinger's suggestion to remove this, but his comment made it seem like a useTagGroup.test.js would be useful. This could be followup.
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.
integration meaning only test at the component level, I don't think useTagGroup would be a super useful test either. Testing hooks can be a bit contrived, better to use a component to put them through real world examples I think
| collection: gridCollection, | ||
| focusMode: 'cell' | ||
| }); | ||
| let keyboardDelegate = new TagKeyboardDelegate({ |
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.
When testing I noticed that home, end, page up, and page down didn't work. Should they?
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.
Will address/continue conversation in followup PR
| Name | Component | TagComponent | props | ||
| ${'TagGroup'} | ${TagGroup} | ${Tag} | ${{isReadOnly: true, isRemovable: true, onRemove: onRemoveSpy}} | ||
| `('$Name can be read only', ({Component, TagComponent, props}) => { | ||
| ${'TagGroup'} | ${TagGroup} | ${Item} | ${{isDisabled: true, isRemovable: true, onRemove: onRemoveSpy}} |
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.
Why is this an it.each story if there is only one condition? The same for the rest of them.
A lot of these have the same component definition, should these exist in a beforeEach()?
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.
we have a lot of tests like this, it's probably just copied, not worried about it
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 copied the structure from other tests (because they looked cool haha). i can change them to single tests
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.
Will address/continue conversation in followup PR
| expect(tagDisabled).toHaveAttribute('tabIndex', '-1'); | ||
| let tagRowNotDisabled = tagGroup.children[0]; | ||
| let tagNotDisabled = tagRowNotDisabled.children[0]; | ||
| act(() => {tagNotDisabled.focus();}); |
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.
Should we confirm it is focused by checking the document.activeElement?
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 it better practice to use document.activeElement? i came across .focus() and thought it seemed fine
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.
Will address/continue conversation in followup PR
| ${'(left/right arrows, ltr + horizontal) TagGroup'} | ${{locale: 'de-DE'}} | ${[{action: () => {userEvent.tab();}, index: 0}, {action: pressArrowRight, index: 1}, {action: pressArrowLeft, index: 0}, {action: pressArrowLeft, index: 0}]} | ||
| ${'(left/right arrows, rtl + horizontal) TagGroup'} | ${{locale: 'ar-AE'}} | ${[{action: () => {userEvent.tab();}, index: 0}, {action: pressArrowRight, index: 0}, {action: pressArrowLeft, index: 1}, {action: pressArrowLeft, index: 2}]} | ||
| ${'(up/down arrows, ltr + horizontal) TagGroup'} | ${{locale: 'de-DE'}} | ${[{action: () => {userEvent.tab();}, index: 0}, {action: pressArrowDown, index: 1}, {action: pressArrowUp, index: 0}, {action: pressArrowUp, index: 0}]} | ||
| ${'(up/down arrows, rtl + horizontal) TagGroup'} | ${{locale: 'ar-AE'}} | ${[{action: () => {userEvent.tab();}, index: 0}, {action: pressArrowDown, index: 1}, {action: pressArrowUp, index: 0}, {action: pressArrowUp, index: 0}]} |
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 last test does two steps at position 0, maybe start at position 2 and move to position 0? This would make it more similar to the second test.
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.
Will address in followup PR
| expect(tagGroup).toHaveAttribute('aria-labelledby', 'tag group'); | ||
| }); | ||
|
|
||
| it('TagGroup allows aria-label on Item', function () { |
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.
API question, does this mean we support a complex Item that is just an Icon and an aria-label? I confirmed that I was able to do this in storybook.
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 tried to emulate actiongroup (since they have similar structures). one of the action stories has aria-label on the Item so i added it as a test for taggroup. although i don't have any stories with aria-label in Item...so i am happy to remove this if it's not suppose to be supported
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.
Will address/continue conversation in followup PR
|
|
||
| let buttonBefore = getByLabelText('ButtonBefore'); | ||
| let buttonAfter = getByLabelText('ButtonAfter'); | ||
| let inputs = getAllByRole('gridcell'); |
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.
Shouldn't this variable be called tags? I'm looking at the last test where it is and the textfields are called inputs. This was confusing because the meaning of inputs changed between tests.
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.
good point. will 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.
Will address in followup PR
majornista
left a 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.
- Is there a plan to manage focus when a Tag is removed from the TagGroup?
- Do we need stories where
onRemovedoes more than just trigger an event? - We need to account for
aria-disabledstate on the row or gridcell for disabled keys. - We need to account for an empty TagGroup, where the TagGroup element should either not render, or its should have
role="group"instead ofrole="grid".
| } | ||
| const pressProps = { | ||
| onPress: e => onRemove(children, e) | ||
| onPress: e => onRemove?.(children, e) |
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.
If we use item.key, I think we should then be able to use the keyboard delegate to shift focus the next or previous Tag on remove when one exists.
| tagProps: mergeProps(domProps, gridCellProps, { | ||
| 'aria-errormessage': props['aria-errormessage'], | ||
| 'aria-label': props['aria-label'], | ||
| onKeyDown: !isDisabled && isRemovable ? onKeyDown : null, | ||
| tabIndex: (isFocused || state.selectionManager.focusedKey == null) && !isDisabled ? 0 : -1, | ||
| onFocus() { | ||
| state.selectionManager.setFocusedKey(item.childNodes[0].key); | ||
| }, |
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.
isDisabled does not add aria-disabled="true" to the Tag element with role="gridcell", which causes aXe accessibility automation tool to throw the following error regarding the color contrast for the Tag text content in Storybook example: "#react-aria1908471221-9 > .tags_spectrum-Tag-content_f7ae1 Element has insufficient color contrast of 2.08 (foreground color: #5c5c5c, background color: #2c2c2c, font size: 9.0pt (12px), font weight: normal). Expected contrast ratio of 4.5:1." To reproduce, open https://reactspectrum.blob.core.windows.net/reactspectrum/b7ddd9f7dec2dbf18633a71801a4ca50506e4762/storybook-16/index.html?path=/story/taggroup--disabledkeys, and in the Storybook Accessibility addOn, press the Tests Completed button to rerun the tests.
Note that this error does not seem to happen when the entire TagGroup is disabled, because aria-disabled="true" gets added to the TagGroup element with role="grid".
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.
will add aria-disabled
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.
Will address in followup PR
|
|
||
| // Don't want the grid to be focusable or accessible via keyboard | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| let {tabIndex, ...otherGridProps} = gridProps; |
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.
When there are no Tags within the TagGroup, the container with role="grid" will render with no descendants. This will cause aXe automated accessibility testing tool to throw an error, because elements with role="grid" are expected to have rows, or a rowgroup containing rows, as children.
In the case of an empty TagGroup, the TagGroup could either not render at all or use role="group", which is allowed to be empty, instead of role="grid".
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.
Will address in followup PR
|
Build successful! 🎉 |
|
Build successful! 🎉 |
|
Followup ticket with list of items to address #2837 |
dannify
left a 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.
I think this is good for an alpha. I agree that the role of grid might be overkill for a TagGroup. We don't have selection in this component so we might be able to get away with a role of "list" instead since the tags are not interactive and there is nothing very special about it?
|
Build successful! 🎉 |
Closes #1921 1921
✅ Pull Request Checklist:
📝 Test Instructions:
🧢 Your Project:
Adobe/Quarry