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

refactor(accessibility): Add accessibility features to Color Picker hue slider #5656

Merged
merged 7 commits into from
Aug 16, 2023
Merged
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"size-limit": [
{
"path": "all.js",
"limit": "141 kb",
"limit": "145 kb",
"ignore": [
"react",
"react-dom"
Expand Down
19 changes: 19 additions & 0 deletions packages/clay-color-picker/src/Custom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ type Props = {
*/
showPalette?: boolean;

/**
* Message for aria-label
*/
messages?: {
close: string;
customColor: string;
};

splotch?: number;

/**
Expand All @@ -56,11 +64,17 @@ type Props = {

const DEFAULT_SPLOTCH_COLOR = 'FFFFFF';

const defaultMessages = {
close: 'close',
customColor: 'custom color',
};

const ClayColorPickerCustom = ({
color,
colors,
editorActive,
label,
messages = defaultMessages,
onChange,
onColorsChange,
onEditorActiveChange,
Expand All @@ -79,6 +93,11 @@ const ClayColorPickerCustom = ({

{showPalette && (
<button
aria-label={
editorActive
? messages.close
: messages.customColor
}
className={`${
editorActive ? 'close' : ''
} component-action`}
Expand Down
23 changes: 17 additions & 6 deletions packages/clay-color-picker/src/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ function reducer(state: State, action: Partial<State>) {
};
}

export enum LimitValue {
maxRGB = 255,
maxHue = 360,
min = 0,
}

export function useEditor(
value: string,
color: Instance,
Expand Down Expand Up @@ -83,8 +89,8 @@ const RGBInput = ({name, onChange, value}: RGBInputProps) => {
<ClayInput
data-testid={`${name}Input`}
insetBefore
max="255"
min="0"
max={LimitValue.maxRGB}
min={LimitValue.min}
onChange={(event: any) => {
const value = event.target.value;

Expand All @@ -94,10 +100,10 @@ const RGBInput = ({name, onChange, value}: RGBInputProps) => {

let newVal = Number(value);

if (newVal < 0) {
newVal = 0;
} else if (newVal > 255) {
newVal = 255;
if (newVal < LimitValue.min) {
newVal = LimitValue.min;
} else if (newVal > LimitValue.maxRGB) {
newVal = LimitValue.maxRGB;
}

setInputValue(newVal);
Expand Down Expand Up @@ -188,6 +194,11 @@ export function Editor({

<Hue
onChange={(hue) => {
if (hue < LimitValue.min) {
hue = LimitValue.min;
} else if (hue > LimitValue.maxHue) {
hue = LimitValue.maxHue;
}
onHueChange(hue);
onColorChange(tinycolor({h: hue, s, v}));
}}
Expand Down
31 changes: 29 additions & 2 deletions packages/clay-color-picker/src/Hue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
* SPDX-License-Identifier: BSD-3-Clause
*/

import React from 'react';
import {Keys} from '@clayui/shared';
import React, {useCallback} from 'react';

import {LimitValue} from './Editor';
import {usePointerPosition} from './hooks';
import {hueToX, xToHue} from './util';

Expand Down Expand Up @@ -32,11 +34,26 @@ const ClayColorPickerHue = ({value = 0, onChange = () => {}}: Props) => {

const {onPointerMove, setXY, x, y} = usePointerPosition(containerRef);

const removeListeners = () => {
const removeListeners = useCallback(() => {
selectorActive.current = false;

window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', removeListeners);
}, []);

const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
event.preventDefault();

switch (event.key) {
case Keys.Left:
onChange(value - 1);
break;
case Keys.Right:
onChange(value + 1);
break;
default:
return;
}
};

useIsomorphicLayoutEffect(() => {
Expand All @@ -55,7 +72,12 @@ const ClayColorPickerHue = ({value = 0, onChange = () => {}}: Props) => {

return (
<div
aria-labelledby="color-range"
aria-valuemax={LimitValue.maxHue}
aria-valuemin={LimitValue.min}
aria-valuenow={value}
className="clay-color-range clay-color-range-hue"
onKeyDown={(event) => onKeyDown(event)}
onPointerDown={(event) => {
event.preventDefault();

Expand All @@ -70,9 +92,14 @@ const ClayColorPickerHue = ({value = 0, onChange = () => {}}: Props) => {
window.addEventListener('pointerup', removeListeners);
}}
ref={containerRef}
role="slider"
tabIndex={0}
>
<button
className="clay-color-pointer clay-color-range-pointer"
onBlur={() => {
selectorActive.current = false;
}}
style={{
background: `hsl(${value}, 100%, 50%)`,
left: x - 7,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ exports[`Interactions color editor interactions ability to change color of click
Custom Colors
</span>
<button
aria-label="close"
class="close component-action"
type="button"
>
Expand Down Expand Up @@ -305,7 +306,13 @@ exports[`Interactions color editor interactions ability to change color of click
</div>
</div>
<div
aria-labelledby="color-range"
aria-valuemax="360"
aria-valuemin="0"
aria-valuenow="0"
class="clay-color-range clay-color-range-hue"
role="slider"
tabindex="0"
>
<button
class="clay-color-pointer clay-color-range-pointer"
Expand Down
32 changes: 31 additions & 1 deletion packages/clay-color-picker/src/__tests__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
*/

import ClayColorPicker from '..';
import {cleanup, fireEvent, render} from '@testing-library/react';
import {cleanup, fireEvent, render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import getMouseEvent from '../../tests-util';
Expand Down Expand Up @@ -393,6 +394,35 @@ describe('Interactions', () => {

expect(document.body).toMatchSnapshot();
});

it('pressing right arrow key increase hue value', async () => {
const hueSlider = screen.getByRole('slider');

userEvent.click(hueSlider);
expect(handleColorsChange).toBeCalledTimes(1);
expect(handleValueChange).toBeCalledTimes(1);

userEvent.keyboard('[ArrowRight]');

expect(handleColorsChange).toBeCalledTimes(2);
expect(handleValueChange).toBeCalledTimes(2);
Copy link
Member Author

Choose a reason for hiding this comment

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

Hey @matuzalemsteles
The reason I had to add two checks on this test was because to set focus on the slider, I had to use a click and that triggers the onChange function and when i press the arrow key, it triggers again.


expect(hueSlider.getAttribute('aria-valuenow')).toBe('1');
});

it('pressing left arrow key descrease hue value', async () => {
const hueSlider = screen.getByRole('slider');

userEvent.click(hueSlider);
expect(handleColorsChange).toBeCalledTimes(1);
expect(handleValueChange).toBeCalledTimes(1);

userEvent.keyboard('[ArrowLeft]');
expect(handleColorsChange).toBeCalledTimes(2);
expect(handleValueChange).toBeCalledTimes(2);
Copy link
Member Author

Choose a reason for hiding this comment

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

Same thing here


expect(hueSlider.getAttribute('aria-valuenow')).toBe('0');
});
});

describe('color editor changing color value', () => {
Expand Down
53 changes: 32 additions & 21 deletions packages/clay-color-picker/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,45 @@
* SPDX-License-Identifier: BSD-3-Clause
*/

import React from 'react';
import React, {useCallback} from 'react';

/**
* Utility hook for calculating the mouse position
*/
export function usePointerPosition(containerRef: React.RefObject<any>) {
const [xy, setXY] = React.useState({x: 0, y: 0});

const onPointerMove = (event: React.PointerEvent | PointerEvent) => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const x = event.pageX;

let newLeft = x - (rect.left + window.pageXOffset);

newLeft =
newLeft < 0 ? 0 : newLeft > rect.width ? rect.width : newLeft;

const y = event.pageY;

let newTop = y - (rect.top + window.pageYOffset);

newTop =
newTop < 0 ? 0 : newTop > rect.height ? rect.height : newTop;

setXY({x: newLeft, y: newTop});
}
};
const onPointerMove = useCallback(
(event: React.PointerEvent | PointerEvent) => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const x = event.pageX;

let newLeft = x - (rect.left + window.pageXOffset);

newLeft =
newLeft < 0
? 0
: newLeft > rect.width
? rect.width
: newLeft;

const y = event.pageY;

let newTop = y - (rect.top + window.pageYOffset);

newTop =
newTop < 0
? 0
: newTop > rect.height
? rect.height
: newTop;

setXY({x: newLeft, y: newTop});
}
},
[]
);

return {...xy, onPointerMove, setXY};
}
13 changes: 12 additions & 1 deletion packages/clay-color-picker/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* SPDX-FileCopyrightText: © 2019 Liferay, Inc. <https://liferay.com>
* SPDX-FileCopyrightText: © 2023 Liferay, Inc. <https://liferay.com>
* SPDX-License-Identifier: BSD-3-Clause
*/

Expand Down Expand Up @@ -109,6 +109,14 @@ interface IProps
*/
label?: string;

/**
* Message for aria-label
*/
messages?: {
close: string;
customColor: string;
};

/**
* The input attribute for name
*/
Expand Down Expand Up @@ -191,6 +199,7 @@ const ClayColorPicker = ({
disabled,
dropDownContainerProps,
label,
messages,
name,
onActiveChange,
onChange,
Expand Down Expand Up @@ -343,6 +352,7 @@ const ClayColorPicker = ({
alignElementRef={triggerElementRef}
className="clay-color-dropdown-menu"
containerProps={dropDownContainerProps}
deps={[internalActive]}
onActiveChange={setInternalActive}
ref={dropdownContainerRef}
triggerRef={splotchRef}
Expand Down Expand Up @@ -374,6 +384,7 @@ const ClayColorPicker = ({
colors={customColors}
editorActive={customEditorActive}
label={label}
messages={messages}
onChange={(color, hex) => {
dispatch({
hex: color.toHex(),
Expand Down
8 changes: 7 additions & 1 deletion packages/clay-drop-down/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ export interface IProps extends React.HTMLAttributes<HTMLDivElement> {
*/
containerProps?: IPortalBaseProps;

/**
* @ignore
*/
deps?: Array<any>;

/**
* Flag to indicate if menu is displaying a clay-icon on the left.
*/
Expand Down Expand Up @@ -153,6 +158,7 @@ const Menu = React.forwardRef<HTMLDivElement, IProps>(
className,
closeOnClickOutside = true,
containerProps = {},
deps = [active, children],
hasLeftSymbols,
hasRightSymbols,
height,
Expand Down Expand Up @@ -189,7 +195,7 @@ const Menu = React.forwardRef<HTMLDivElement, IProps>(
ref: menuRef,
triggerRef: alignElementRef,
},
[active, children]
deps
);

return (
Expand Down
Loading