-
Notifications
You must be signed in to change notification settings - Fork 15
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
feat(Chip): accessibility improvements #1471
base: master
Are you sure you want to change the base?
Changes from 4 commits
fb5e383
a428d2b
8c0c379
51770e1
3b87bba
27b0671
35780fc
c058c28
bb6f402
e20053e
6a11cce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { theme } from '@dhis2/ui-constants' | ||
import PropTypes from 'prop-types' | ||
import React, { useRef } from 'react' | ||
|
||
const ChipGroup = ({ className, dataTest, children }) => { | ||
const chipContainer = useRef(null) | ||
|
||
const handleKeyDown = (event) => { | ||
const currentFocus = document.activeElement | ||
|
||
if (chipContainer.current && chipContainer.current === currentFocus) { | ||
const chips = Array.from( | ||
chipContainer.current.querySelectorAll('[role="option"]') | ||
) | ||
if (chips.length > 0) { | ||
chips[0].focus() | ||
} | ||
return | ||
} | ||
|
||
const role = currentFocus.getAttribute('role') | ||
|
||
if (role !== 'option') { | ||
return | ||
} | ||
|
||
const chips = Array.from( | ||
chipContainer.current.querySelectorAll('[role="option"]') | ||
) | ||
|
||
const currentIndex = chips.indexOf(currentFocus) | ||
|
||
if (event.key === 'ArrowRight') { | ||
event.preventDefault() | ||
const nextIndex = (currentIndex + 1) % chips.length | ||
chips[nextIndex].focus() | ||
} | ||
if (event.key === 'ArrowLeft') { | ||
event.preventDefault() | ||
const prevIndex = (currentIndex - 1 + chips.length) % chips.length | ||
chips[prevIndex].focus() | ||
} | ||
if (event.key === 'Backspace' || event.key === 'Delete') { | ||
const nextIndex = (currentIndex + 1) % chips.length | ||
chips[nextIndex].focus() | ||
} | ||
} | ||
return ( | ||
<div | ||
className={className} | ||
data-test={dataTest} | ||
onKeyDown={handleKeyDown} | ||
ref={chipContainer} | ||
tabIndex={0} | ||
> | ||
{children} | ||
<style jsx>{` | ||
div { | ||
display: flex; | ||
gap: 4px; | ||
} | ||
div:focus { | ||
outline: 1px solid ${theme.focus}; | ||
} | ||
`}</style> | ||
</div> | ||
) | ||
} | ||
|
||
ChipGroup.defaultProps = { | ||
dataTest: 'dhis2-uicore-chipgroup', | ||
} | ||
|
||
ChipGroup.propTypes = { | ||
children: PropTypes.node, | ||
className: PropTypes.string, | ||
dataTest: PropTypes.string, | ||
} | ||
|
||
export { ChipGroup } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { ChipGroup } from './chip-group.js' |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import { colors, theme } from '@dhis2/ui-constants' | ||
import cx from 'classnames' | ||
import PropTypes from 'prop-types' | ||
import React from 'react' | ||
import { Content } from './content.js' | ||
import { Icon } from './icon.js' | ||
import { Remove } from './remove.js' | ||
|
||
const DEFAULT_INLINE_MARGIN = '4' | ||
|
||
const Chip = ({ | ||
selected, | ||
dense, | ||
disabled, | ||
dragging, | ||
overflow, | ||
className, | ||
children, | ||
onRemove, | ||
onClick, | ||
icon, | ||
dataTest, | ||
marginBottom, | ||
marginLeft, | ||
marginRight, | ||
marginTop, | ||
marginInlineStart, | ||
marginInlineEnd, | ||
}) => { | ||
const handleKeyDown = (event) => { | ||
if (!onRemove) { | ||
return | ||
} | ||
if (event.key === 'Backspace' || event.key === 'Delete') { | ||
onRemove({}, event) | ||
} | ||
} | ||
|
||
return ( | ||
<button | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Chips are mainly used for links. Button is definitely better than the previous span, but I think we should look into how we would support links, instead of having to do the navigation imperatively in It might be worth supporting a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it sounds correct .. but not in the scope of this PR, right? we can add a separate ticket for it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right, but we may want to release that together. I brought it up because this PR changes the underlying element from span to a button. And wrapping a button in anchor tag is not valid, so it's potentially breaking . There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a breaking change for sure. I can change the button back to span and handle the if you have another way of doing it suggest it please. @Birkbjo @kabaros |
||
onClick={(e) => { | ||
if (!disabled && onClick) { | ||
onClick({}, e) | ||
} | ||
}} | ||
onKeyDown={handleKeyDown} | ||
className={cx(className, { | ||
selected, | ||
dense, | ||
disabled, | ||
dragging, | ||
})} | ||
data-test={dataTest} | ||
tabIndex={-1} | ||
role="option" | ||
aria-selected={selected ? 'true' : 'false'} | ||
aria-disabled={disabled ? 'true' : 'false'} | ||
> | ||
<Icon icon={icon} dataTest={`${dataTest}-icon`} /> | ||
<Content overflow={overflow}>{children}</Content> | ||
<Remove onRemove={onRemove} dataTest={`${dataTest}-remove`} /> | ||
|
||
<style jsx>{` | ||
button { | ||
display: inline-flex; | ||
align-items: center; | ||
height: 32px; | ||
border-radius: 16px; | ||
background-color: ${colors.grey200}; | ||
font-size: 14px; | ||
line-height: 16px; | ||
cursor: pointer; | ||
user-select: none; | ||
color: ${colors.grey900}; | ||
padding: 0; | ||
} | ||
|
||
.dense { | ||
height: 24px; | ||
font-size: 13px; | ||
line-height: 15px; | ||
} | ||
|
||
button:hover { | ||
background-color: ${colors.grey300}; | ||
} | ||
|
||
.selected { | ||
background-color: ${theme.secondary600}; | ||
font-weight: 500; | ||
} | ||
|
||
.selected:hover { | ||
background-color: #00695c; | ||
} | ||
|
||
.selected, | ||
.selected .icon, | ||
.selected .remove-icon { | ||
color: ${colors.white}; | ||
} | ||
|
||
.disabled { | ||
cursor: not-allowed; | ||
opacity: 0.3; | ||
} | ||
|
||
.disabled .remove-icon { | ||
pointer-events: none; | ||
} | ||
|
||
.dragging { | ||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), | ||
0 2px 2px 0 rgba(0, 0, 0, 0.14), | ||
0 1px 5px 0 rgba(0, 0, 0, 0.12); | ||
} | ||
`}</style> | ||
<style jsx>{` | ||
button { | ||
${marginBottom && `margin-bottom: ${marginBottom}px;`} | ||
margin-inline-start: ${marginInlineStart ?? | ||
marginLeft ?? | ||
DEFAULT_INLINE_MARGIN}px; | ||
margin-inline-end: ${marginInlineEnd ?? | ||
marginRight ?? | ||
DEFAULT_INLINE_MARGIN}px; | ||
${marginTop && `margin-top: ${marginTop}px`} | ||
} | ||
`}</style> | ||
</button> | ||
) | ||
} | ||
|
||
Chip.defaultProps = { | ||
dataTest: 'dhis2-uicore-chip', | ||
marginBottom: 4, | ||
marginTop: 4, | ||
} | ||
|
||
Chip.propTypes = { | ||
children: PropTypes.any, | ||
className: PropTypes.string, | ||
dataTest: PropTypes.string, | ||
dense: PropTypes.bool, | ||
disabled: PropTypes.bool, | ||
dragging: PropTypes.bool, | ||
icon: PropTypes.element, | ||
/** `margin-bottom` value, applied in `px` */ | ||
marginBottom: PropTypes.number, | ||
/** `margin-inline-end` value, applied in `px` */ | ||
marginInlineEnd: PropTypes.number, | ||
/** `margin-inline-start` value, applied in `px` */ | ||
marginInlineStart: PropTypes.number, | ||
/** `margin-inline-start` value, applied in `px` */ | ||
marginLeft: PropTypes.number, | ||
/** `margin-inline-end` value, applied in `px` */ | ||
marginRight: PropTypes.number, | ||
/** `margin-top` value, applied in `px` */ | ||
marginTop: PropTypes.number, | ||
overflow: PropTypes.bool, | ||
selected: PropTypes.bool, | ||
onClick: PropTypes.func, | ||
onRemove: PropTypes.func, | ||
} | ||
|
||
export { Chip } |
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'd say also
event.stopPropagation
here