From de8da0f6d6d8732d71c4200c969fd0c22e294543 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Mon, 2 May 2022 09:22:18 -0400 Subject: [PATCH 1/3] fix(Tooltip): make ref variant announced by assistive tech --- .../src/components/Tabs/examples/Tabs.md | 4 +- .../src/components/Tooltip/Tooltip.tsx | 8 ++ .../components/Tooltip/examples/Tooltip.md | 117 ++++++++++++------ 3 files changed, 87 insertions(+), 42 deletions(-) diff --git a/packages/react-core/src/components/Tabs/examples/Tabs.md b/packages/react-core/src/components/Tabs/examples/Tabs.md index ac8d105e1fa..00dd90e41fc 100644 --- a/packages/react-core/src/components/Tabs/examples/Tabs.md +++ b/packages/react-core/src/components/Tabs/examples/Tabs.md @@ -99,7 +99,7 @@ class SimpleTabs extends React.Component { ### With tooltip react ref -When using a React ref to link a Tooltip to a Tab component, an `id` must be manually set on the Tooltip component, and the Tab component must have a matching `aria-describedby` attribute so that screen readers are able to announce the Tooltip contents. +When using a React ref to link a Tooltip to a Tab component via the `reference` prop, you should avoid manually passing in a value of off to the `aria-live` prop. Doing so may lead to the tooltip becoming less accessible to assistive technologies. ```js import React from 'react'; @@ -153,7 +153,6 @@ class SimpleTabs extends React.Component { title={ARIA Disabled (Tooltip)} isAriaDisabled ref={tooltipRef} - aria-describedby="tooltip-tab-5" > ARIA Disabled (Tooltip) @@ -161,7 +160,6 @@ class SimpleTabs extends React.Component {
, 'con * If you don't want that or prefer to add the aria attribute yourself on the trigger, set aria to 'none'. */ aria?: 'describedby' | 'labelledby' | 'none'; + /** + * Determines whether the tooltip is an aria-live region. If the reference prop is passed in the + * default behavior is 'polite' in order to ensure the tooltip contents is announced to + * assistive technologies. Otherwise the default behavior is 'off'. + */ + 'aria-live'?: 'off' | 'polite'; /** * The reference element to which the Tooltip is relatively placed to. * If you cannot wrap the reference with the Tooltip, you can use the reference prop instead. @@ -155,6 +161,7 @@ export const Tooltip: React.FunctionComponent = ({ children, animationDuration = 300, reference, + 'aria-live': ariaLive = reference ? 'polite' : 'off', boundary, isAppLauncher, tippyProps, @@ -251,6 +258,7 @@ export const Tooltip: React.FunctionComponent = ({ const hasCustomMaxWidth = maxWidth !== tooltipMaxWidth.value; const content = ( } > - I have a tooltip! + + I have a tooltip! + -
+; ``` ### Tooltip react ref + ```js import React from 'react'; import { Tooltip } from '@patternfly/react-core'; @@ -39,16 +43,19 @@ TooltipReactRef = () => { Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam id feugiat augue, nec fringilla turpis. +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam id feugiat augue, nec fringilla turpis. +
} reference={tooltipRef} /> ); -} +}; ``` ### Tooltip selector ref + ```js import React from 'react'; import { Tooltip } from '@patternfly/react-core'; @@ -61,10 +68,15 @@ import { Tooltip } from '@patternfly/react-core'; } reference={() => document.getElementById('tooltip-selector')} /> - +; ``` -### On icon +### On icon with dynamic content + +When the tooltip is used as a wrapper and its content will dynamically update, the `aria` prop should have a value of "off" passed in. This prevents assistive technologies from announcing the tooltip contents more than once. Additionally, the `aria-live` prop should have a value of "polite" passed in, in order for assistive technologies to announce when the tooltip contents gets updated. + +When using a React or selector ref with a tooltip that has dynamic content, the `aria` and `aria-live` props do not need to be manually passed in. + ```js import React from 'react'; import { Tooltip, Button } from '@patternfly/react-core'; @@ -77,8 +89,13 @@ IconExample = () => { const [content, setContent] = React.useState(copyText); return (
- - @@ -88,6 +105,7 @@ IconExample = () => { ``` ### Options + ```js import React from 'react'; import { Button, Tooltip, Checkbox, Select, SelectOption, TextInput } from '@patternfly/react-core'; @@ -105,26 +123,26 @@ OptionsTooltip = () => { const [exitDelayInput, setExitDelayInput] = React.useState(0); const [animationDuration, setAnimationDuration] = React.useState(300); const tipBoxRef = React.useRef(null); - + const scrollToRef = ref => { if (ref && ref.current) { ref.current.scrollTop = 400; ref.current.scrollLeft = 300; } - } - + }; + React.useEffect(() => { scrollToRef(tipBoxRef); }, []); - + return ( <>
-
+
{ + onChange={checked => { let updatedTrigger; checked && (updatedTrigger = trigger.concat('mouseenter')); !checked && (updatedTrigger = trigger.filter(t => t !== 'mouseenter')); @@ -137,7 +155,7 @@ OptionsTooltip = () => { { + onChange={checked => { let updatedTrigger; checked && (updatedTrigger = trigger.concat('focus')); !checked && (updatedTrigger = trigger.filter(t => t !== 'focus')); @@ -150,7 +168,7 @@ OptionsTooltip = () => { { + onChange={checked => { let updatedTrigger; checked && (updatedTrigger = trigger.concat('click')); !checked && (updatedTrigger = trigger.filter(t => t !== 'click')); @@ -163,36 +181,36 @@ OptionsTooltip = () => { { + onChange={checked => { let updatedTrigger; checked && (updatedTrigger = trigger.concat('manual')); !checked && (updatedTrigger = trigger.filter(t => t !== 'manual')); setIsVisible(false); setTrigger(updatedTrigger); }} - aria-label="trigger: manual" - id="trigger_manual" + aria-label="trigger: manual" + id="trigger_manual" />
-
+
setContentLeftAligned(checked)} + onChange={checked => setContentLeftAligned(checked)} aria-label="content left-aligned" - id="content_left_aligned" + id="content_left_aligned" />
-
+
setEnableFlip(checked)} - aria-label="enableFlip" - id="enable_flip" + onChange={checked => setEnableFlip(checked)} + aria-label="enableFlip" + id="enable_flip" />
-
+
position (will flip if enableFlip is true). The 'auto' position requires enableFlip to be set to true.
-
+
setIsVisible(checked)} - aria-label="isVisible" - id="is_visible" + onChange={checked => setIsVisible(checked)} + aria-label="isVisible" + id="is_visible" />
-
- Entry delay (ms) setEntryDelayInput(val)} aria-label="entry delay" /> - Exit delay (ms) setExitDelayInput(val)} aria-label="exit delay" /> - Animation duration (ms) setAnimationDuration(val)} aria-label="animation duration" /> +
+ Entry delay (ms){' '} + setEntryDelayInput(val)} + aria-label="entry delay" + /> + Exit delay (ms) setExitDelayInput(val)} + aria-label="exit delay" + /> + Animation duration (ms){' '} + setAnimationDuration(val)} + aria-label="animation duration" + />
-
- flip behavior examples (enableFlip has to be true). "flip" will try to flip the tooltip to the opposite of the starting position. The second option ensures that there are 3 escape positions for every possible starting position (default). This setting is ignored if position prop is set to 'auto'. +
+ flip behavior examples (enableFlip has to be true). "flip" will try to flip the tooltip to the opposite of the + starting position. The second option ensures that there are 3 escape positions for every possible starting + position (default). This setting is ignored if position prop is set to 'auto'.