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

feat(Chip): accessibility improvements #1471

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
80 changes: 80 additions & 0 deletions components/chip/src/chip-group/chip-group.js
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 }
1 change: 1 addition & 0 deletions components/chip/src/chip-group/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ChipGroup } from './chip-group.js'
149 changes: 0 additions & 149 deletions components/chip/src/chip.js

This file was deleted.

166 changes: 166 additions & 0 deletions components/chip/src/chip/chip.js
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') {
Copy link
Collaborator

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

onRemove({}, event)
}
}

return (
<button
Copy link
Member

Choose a reason for hiding this comment

The 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 onClick handler. It's not valid HTML to wrap a button in a link, so this component would have to support it.

It might be worth supporting a component-prop, and href as props. See how material UI does it: https://mui.com/material-ui/react-chip/#clickable-link . A lot of times you would want to use a Chip with eg. react-routers NavLink.

@kabaros @ismay

Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The 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 .

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a breaking change for sure.
Two things:
Links should not have the role = option.
The accessibility features require that each chip be a direct child of the chipGroup.

I can change the button back to span and handle the enter key event for now or add a component, to & href props which in this case will pass the role=option to links.

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 }
File renamed without changes.
File renamed without changes.
Loading
Loading