From 0240d50cc8f7a8fde9b8c5323f82b048c17d219f Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Fri, 25 Apr 2025 13:44:01 +0200 Subject: [PATCH 1/7] refactor(colorPicker): prevent duplicate screen reader output - moves the aria-live element up to the shared parent comonent --- .../eui/src/components/color_picker/color_picker.tsx | 11 +++++++++++ packages/eui/src/components/color_picker/hue.tsx | 3 --- .../eui/src/components/color_picker/saturation.tsx | 3 --- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/eui/src/components/color_picker/color_picker.tsx b/packages/eui/src/components/color_picker/color_picker.tsx index 4d82d640d28..2521673beb7 100644 --- a/packages/eui/src/components/color_picker/color_picker.tsx +++ b/packages/eui/src/components/color_picker/color_picker.tsx @@ -35,6 +35,7 @@ import { } from '../form'; import { useEuiI18n } from '../i18n'; import { EuiPopover } from '../popover'; +import { EuiScreenReaderOnly } from '../accessibility'; import { EuiColorPickerSwatch } from './color_picker_swatch'; import { EuiHue } from './hue'; @@ -212,6 +213,7 @@ export const EuiColorPicker: FunctionComponent = ({ const [ popoverLabel, colorLabel, + selectedColorLabel, colorErrorMessage, transparent, alphaLabel, @@ -221,6 +223,7 @@ export const EuiColorPicker: FunctionComponent = ({ [ 'euiColorPicker.popoverLabel', 'euiColorPicker.colorLabel', + 'euiColorPicker.selectedColorLabel', 'euiColorPicker.colorErrorMessage', 'euiColorPicker.transparent', 'euiColorPicker.alphaLabel', @@ -230,6 +233,7 @@ export const EuiColorPicker: FunctionComponent = ({ [ 'Color selection dialog', 'Color value', + 'Selected color', 'Invalid color value', 'Transparent', 'Alpha channel (opacity) value', @@ -514,6 +518,13 @@ export const EuiColorPicker: FunctionComponent = ({ onChange={handleHueSelection} onKeyDown={handleOnKeyDown} /> + + {/* Note: using EuiScreenReaderLive didn't work as expected for VoiceOver */} +

+ {/* use uppercase to ensure letters are spoken separately */} + {selectedColorLabel}: {chromaColor?.hex().toUpperCase()} +

+
)} {showSwatches && ( diff --git a/packages/eui/src/components/color_picker/hue.tsx b/packages/eui/src/components/color_picker/hue.tsx index fad046a5544..87e2947570c 100644 --- a/packages/eui/src/components/color_picker/hue.tsx +++ b/packages/eui/src/components/color_picker/hue.tsx @@ -57,9 +57,6 @@ export const EuiHue: FunctionComponent = ({ /> - -

{hex}

-
( aria-label={hex} aria-describedby={instructionsId} /> - From a2624ebe457b8524b328a8464bb3edd19583754c Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Fri, 25 Apr 2025 13:44:14 +0200 Subject: [PATCH 2/7] refactor(colorPicker): add tooltips and improve accessible labels --- .../src/components/color_picker/hue.styles.ts | 8 ++- .../src/components/color_picker/hue.test.tsx | 43 +++++++++++++++- .../eui/src/components/color_picker/hue.tsx | 49 ++++++++++++++----- .../color_picker/saturation.styles.ts | 8 ++- .../color_picker/saturation.test.tsx | 45 ++++++++++++++++- .../components/color_picker/saturation.tsx | 38 +++++++++----- 6 files changed, 161 insertions(+), 30 deletions(-) diff --git a/packages/eui/src/components/color_picker/hue.styles.ts b/packages/eui/src/components/color_picker/hue.styles.ts index 5cbf66c581e..662067f1ebd 100644 --- a/packages/eui/src/components/color_picker/hue.styles.ts +++ b/packages/eui/src/components/color_picker/hue.styles.ts @@ -59,7 +59,7 @@ export const euiHueStyles = (euiThemeContext: UseEuiTheme) => { })} `, - euiHue__range: css` + euiHue__tooltip: css` ${logicalCSS('height', thumbSize)} /* Allow for overlap */ ${logicalCSS('width', `calc(100% + 2px)`)} @@ -69,6 +69,12 @@ export const euiHueStyles = (euiThemeContext: UseEuiTheme) => { 'margin-top', mathWithUnits(height, (x) => x / -2) )} + `, + + euiHue__range: css` + ${logicalCSS('height', '100%')} + ${logicalCSS('width', '100%')} + /* Resets for the range */ appearance: none; diff --git a/packages/eui/src/components/color_picker/hue.test.tsx b/packages/eui/src/components/color_picker/hue.test.tsx index 4e191fd5764..bbd3d05213e 100644 --- a/packages/eui/src/components/color_picker/hue.test.tsx +++ b/packages/eui/src/components/color_picker/hue.test.tsx @@ -9,9 +9,14 @@ import React from 'react'; import { requiredProps } from '../../test/required_props'; import { shouldRenderCustomStyles } from '../../test/internal'; -import { render } from '../../test/rtl'; +import { + render, + waitForEuiToolTipHidden, + waitForEuiToolTipVisible, +} from '../../test/rtl'; import { EuiHue } from './hue'; +import { fireEvent } from '@testing-library/react'; const onChange = () => { /* empty */ @@ -50,4 +55,40 @@ describe('EuiHue', () => { expect(container).toMatchSnapshot(); }); + + test('it renders a color label tooltip on hover', async () => { + const { getByText } = render( + + ); + + const thumbElement = document.querySelector('.euiHue__range')!; + + fireEvent.mouseOver(thumbElement); + + await waitForEuiToolTipVisible(); + + expect(getByText('#00FFFF')).toBeInTheDocument(); + + fireEvent.mouseLeave(thumbElement); + + await waitForEuiToolTipHidden(); + }); + + test('it renders a color label tooltip on focus', async () => { + const { getByText } = render( + + ); + + const thumbElement = document.querySelector('.euiHue__range')!; + + fireEvent.focus(thumbElement); + + await waitForEuiToolTipVisible(); + + expect(getByText('#00FFFF')).toBeInTheDocument(); + + fireEvent.blur(thumbElement); + + await waitForEuiToolTipHidden(); + }); }); diff --git a/packages/eui/src/components/color_picker/hue.tsx b/packages/eui/src/components/color_picker/hue.tsx index 87e2947570c..0c111961e90 100644 --- a/packages/eui/src/components/color_picker/hue.tsx +++ b/packages/eui/src/components/color_picker/hue.tsx @@ -16,9 +16,10 @@ import classNames from 'classnames'; import { useEuiMemoizedStyles } from '../../services'; import { CommonProps } from '../common'; import { EuiScreenReaderOnly } from '../accessibility'; -import { EuiI18n } from '../i18n'; +import { EuiI18n, useEuiI18n } from '../i18n'; import { euiHueStyles } from './hue.styles'; +import { EuiToolTip } from '../tool_tip'; const HUE_RANGE = 359; @@ -43,10 +44,20 @@ export const EuiHue: FunctionComponent = ({ const classes = classNames('euiHue', className); const styles = useEuiMemoizedStyles(euiHueStyles); + const [ariaValueText, ariaRoleDescription] = useEuiI18n( + ['euiHue.ariaValueText', 'euiHue.ariaRoleDescription'], + ['Hue', 'Hue slider'] + ); + const handleChange = (e: ChangeEvent) => { onChange(Number(e.target.value)); }; + const hueValue = typeof hue === 'string' ? parseInt(hue) : hue; + // align the tooltip contextually closer to the thumb + const tooltipPosition = + hueValue < Math.floor(HUE_RANGE / 2) ? 'left' : 'right'; + return (
@@ -57,18 +68,30 @@ export const EuiHue: FunctionComponent = ({ /> - + {/* we can only wrap the entire input because the input slider thumb is not a standalone element */} + + +
); }; diff --git a/packages/eui/src/components/color_picker/saturation.styles.ts b/packages/eui/src/components/color_picker/saturation.styles.ts index 39d78f8165b..55d2d042dc6 100644 --- a/packages/eui/src/components/color_picker/saturation.styles.ts +++ b/packages/eui/src/components/color_picker/saturation.styles.ts @@ -91,12 +91,18 @@ export const euiSaturationStyles = (euiThemeContext: UseEuiTheme) => { background: linear-gradient(to top, rgba(0, 0, 0, 1), rgba(0, 0, 0, 0)); `, - euiSaturation__indicator: css` + euiSaturation__tooltip: css` z-index: 2; position: absolute; ${logicalSizeCSS(indicatorSize)} transform: translateX(-50%) translateY(-50%); border-radius: 100%; + `, + euiSaturation__indicator: css` + position: absolute; + inset: 0; + ${logicalSizeCSS(indicatorSize)} + border-radius: 100%; ${highContrastModeStyles(euiThemeContext, { none: ` diff --git a/packages/eui/src/components/color_picker/saturation.test.tsx b/packages/eui/src/components/color_picker/saturation.test.tsx index 4d97fa38398..94ee7c19f2a 100644 --- a/packages/eui/src/components/color_picker/saturation.test.tsx +++ b/packages/eui/src/components/color_picker/saturation.test.tsx @@ -9,15 +9,20 @@ import React from 'react'; import { requiredProps } from '../../test/required_props'; import { shouldRenderCustomStyles } from '../../test/internal'; -import { render } from '../../test/rtl'; +import { + render, + waitForEuiToolTipHidden, + waitForEuiToolTipVisible, +} from '../../test/rtl'; import { EuiSaturation } from './saturation'; +import { fireEvent } from '@testing-library/react'; const onChange = () => { /* empty */ }; -describe('EuiHue', () => { +describe('EuiSaturation', () => { shouldRenderCustomStyles(); test('is rendered', () => { @@ -39,4 +44,40 @@ describe('EuiHue', () => { expect(container.firstChild).toMatchSnapshot(); }); + + test('it renders a color label tooltip on hover', async () => { + const { getByText } = render( + + ); + + const thumbElement = document.querySelector('.euiSaturation__indicator')!; + + fireEvent.mouseOver(thumbElement); + + await waitForEuiToolTipVisible(); + + expect(getByText('#000000')).toBeInTheDocument(); + + fireEvent.mouseLeave(thumbElement); + + await waitForEuiToolTipHidden(); + }); + + test('it renders a color label tooltip on focus', async () => { + const { getByText } = render( + + ); + + const thumbElement = document.querySelector('.euiSaturation__indicator')!; + + fireEvent.focus(thumbElement); + + await waitForEuiToolTipVisible(); + + expect(getByText('#000000')).toBeInTheDocument(); + + fireEvent.blur(thumbElement); + + await waitForEuiToolTipHidden(); + }); }); diff --git a/packages/eui/src/components/color_picker/saturation.tsx b/packages/eui/src/components/color_picker/saturation.tsx index 71034884874..203c1ff4de4 100644 --- a/packages/eui/src/components/color_picker/saturation.tsx +++ b/packages/eui/src/components/color_picker/saturation.tsx @@ -28,6 +28,7 @@ import { isNil } from '../../services/predicate'; import { logicalStyles } from '../../global_styling'; import { CommonProps } from '../common'; import { useEuiI18n } from '../i18n'; +import { EuiToolTip } from '../tool_tip'; import { getEventPosition } from './utils'; import { euiSaturationStyles } from './saturation.styles'; @@ -74,10 +75,15 @@ export const EuiSaturation = forwardRef( const id = useGeneratedHtmlId({ conditionalId: _id }); const instructionsId = `${id}-instructions`; const indicatorId = `${id}-saturationIndicator`; - const [roleDescString, instructionsString] = useEuiI18n( - ['euiSaturation.ariaLabel', 'euiSaturation.screenReaderInstructions'], + const [ariaLabel, roleDescString, instructionsString] = useEuiI18n( [ - 'HSV color mode saturation and value 2-axis slider', + 'euiSaturation.ariaLabel', + 'euiSaturation.roleDescription', + 'euiSaturation.screenReaderInstructions', + ], + [ + 'Select a color', + 'HSV color mode saturation and value 2-axis slider.', "Arrow keys to navigate the square color gradient. Coordinates will be used to calculate HSV color mode 'saturation' and 'value' numbers, in the range of 0 to 1. Left and right to change the saturation. Up and down change the value.", ] ); @@ -206,15 +212,23 @@ export const EuiSaturation = forwardRef( className="euiSaturation__saturation" /> -