Skip to content

Commit 2cde896

Browse files
author
Kubit
committed
Implement useTooltipAsModalAriaLabel Hook for Enhanced Accessibility in Tooltip Component
This commit introduces the useTooltipAsModalAriaLabel hook to improve accessibility within the Tooltip component, specifically when tooltips are utilized as modals. The hook dynamically assigns the aria-label attribute by extracting the inner text of a referenced HTML element. This enhancement is demonstrated in the updated Tooltip story and is integrated across controlled and standalone tooltip variants. Additionally, the ITooltipStandAlone and ITooltipControlled interfaces are expanded to include the new tooltipAriaLabel property. Comprehensive tests for the new hook validate its functionality, ensuring the tooltip's accessibility compliance when acting as a modal.
1 parent 573e612 commit 2cde896

File tree

10 files changed

+102
-10
lines changed

10 files changed

+102
-10
lines changed

src/components/tooltip/__tests__/tooltip.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,4 +466,16 @@ describe('Tooltip', () => {
466466
expect(title).toBeVisible();
467467
expect(content).toBeVisible();
468468
});
469+
470+
it('Tooltip as modal - it should have the external aria-label when it set', () => {
471+
const externalArialLabel = 'external aria label';
472+
renderProvider(<Tooltip {...mockProps} tooltipAsModal tooltipAriaLabel={externalArialLabel} />);
473+
const label = screen.getByText(mockProps.children as string);
474+
475+
fireEvent.keyDown(label, ENTER);
476+
477+
const tooltip = screen.getByLabelText(externalArialLabel);
478+
479+
expect(tooltip).toBeInTheDocument();
480+
});
469481
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { renderHook } from '@testing-library/react-hooks';
2+
3+
import { useTooltipAsModalAriaLabel } from '../hooks/useTooltipAsModalAriaLabel';
4+
5+
describe('useTooltipAsModalAriaLabel', () => {
6+
it('should return the inner text of the ref', () => {
7+
const ref = {
8+
current: document.createElement('div'),
9+
};
10+
ref.current.innerText = 'Test text';
11+
12+
const { result } = renderHook(() => useTooltipAsModalAriaLabel(ref));
13+
expect(result.current).toEqual('Test text');
14+
});
15+
16+
it('should return undefined if the ref is null', () => {
17+
const ref = {
18+
current: null,
19+
};
20+
21+
const { result } = renderHook(() => useTooltipAsModalAriaLabel(ref));
22+
expect(result.current).toBeUndefined();
23+
});
24+
});

src/components/tooltip/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './useTooltip';
2+
export * from './useTooltipAsModalAriaLabel';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useEffect, useState } from 'react';
2+
3+
export const useTooltipAsModalAriaLabel = (
4+
ref: React.RefObject<HTMLDivElement>
5+
): string | undefined => {
6+
const [ariaLabel, setArialabel] = useState<string | undefined>(undefined);
7+
8+
useEffect(() => {
9+
if (!ref.current) {
10+
return;
11+
}
12+
setArialabel(ref.current.innerText);
13+
});
14+
15+
return ariaLabel;
16+
};

src/components/tooltip/stories/argtypes.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,17 @@ export const argtypes = (
103103
category: CATEGORY_CONTROL.MODIFIERS,
104104
},
105105
},
106+
tooltipAriaLabel: {
107+
description: 'Aria label for the tooltip when is modal',
108+
type: { name: 'string' },
109+
control: { type: 'text' },
110+
table: {
111+
type: {
112+
summary: 'string',
113+
},
114+
category: CATEGORY_CONTROL.ACCESIBILITY,
115+
},
116+
},
106117
popover: {
107118
description: 'Object with popover properties',
108119
type: { name: 'object' },

src/components/tooltip/stories/tooltip.stories.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Meta, StoryObj } from '@storybook/react';
2+
import React from 'react';
23

34
import { ICONS } from '@/assets';
45
import { STYLES_NAME } from '@/constants';
@@ -32,7 +33,14 @@ export const Tooltip: Story = {
3233
args: {
3334
variant: Object.values(variantsObject[themeSelected].TooltipVariantType || {})[0] as string,
3435
title: { content: 'Tootltip title' },
35-
content: { content: 'Tooltip content' },
36+
content: {
37+
content: (
38+
<div style={{ color: '#fff' }}>
39+
<div>content first line</div>
40+
<div>content second line</div>
41+
</div>
42+
),
43+
},
3644
children: 'Hover me',
3745
align: TooltipAlignType.TOP,
3846
closeIcon: { icon: ICONS.ICON_PLACEHOLDER, altText: 'Close icon' },

src/components/tooltip/tooltipControlled.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,38 @@ import { useStyles } from '@/hooks/useStyles/useStyles';
55
import { ErrorBoundary, FallbackComponent } from '@/provider/errorBoundary';
66

77
import { TOOLTIP_STYLES } from './constants';
8+
import { useTooltipAsModalAriaLabel } from './hooks/useTooltipAsModalAriaLabel';
89
import { TooltipStandAlone } from './tooltipStandAlone';
910
import { ITooltipControlled, TooltipVariantStylesProps } from './types';
1011
import { useTooltipAsModal } from './utils';
1112

1213
const TooltipControlledComponent = <V extends string | unknown>({
1314
tooltipAsModal,
1415
ctv,
16+
tooltipRef,
17+
tooltipAriaLabel,
1518
...props
1619
}: ITooltipControlled<V>): JSX.Element => {
1720
const styles = useStyles<TooltipVariantStylesProps, V>(TOOLTIP_STYLES, props.variant, ctv);
1821
const mediaDevice = useMediaDevice();
22+
const innerTooltipRef = React.useRef<HTMLDivElement>(null);
23+
const helpAriaLabel = useTooltipAsModalAriaLabel(innerTooltipRef);
24+
25+
React.useImperativeHandle(tooltipRef, () => {
26+
return innerTooltipRef.current as HTMLDivElement;
27+
}, []);
1928

2029
return (
2130
<TooltipStandAlone
2231
{...props}
2332
mediaDevice={mediaDevice}
2433
styles={styles}
34+
tooltipAriaLabel={tooltipAriaLabel ?? helpAriaLabel}
2535
tooltipAsModal={useTooltipAsModal({
2636
propTooltipAsModal: tooltipAsModal,
2737
styleTooltipAsModal: styles.tooltipAsModal,
2838
})}
39+
tooltipRef={innerTooltipRef}
2940
/>
3041
);
3142
};

src/components/tooltip/tooltipStandAlone.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const TooltipStandAlone = ({
5858
const Tooltip = (
5959
<TooltipExternalContainerStyled
6060
ref={props.tooltipRef}
61-
aria-labelledby={props.tooltipAsModal ? ariaDescriptorsBy : undefined}
61+
aria-label={props.tooltipAsModal ? props.tooltipAriaLabel : undefined}
6262
as={getHtmlTagForTooltip({
6363
mediaDevice: props.mediaDevice,
6464
tooltipAsModal: props.tooltipAsModal,

src/components/tooltip/tooltipUnControlled.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,22 @@ import { trapFocus } from '@/utils/focusHandlers/focusHandlers';
99

1010
import { TOOLTIP_STYLES } from './constants';
1111
import { useTooltip } from './hooks';
12+
import { useTooltipAsModalAriaLabel } from './hooks/useTooltipAsModalAriaLabel';
1213
import { TooltipStandAlone } from './tooltipStandAlone';
1314
import { ITooltipStandAlone, ITooltipUnControlled, TooltipVariantStylesProps } from './types';
1415
import { useTooltipAsModal } from './utils';
1516

1617
const TooltipUnControlledComponent = React.forwardRef(
1718
<V extends string | unknown>(
18-
{ ctv, tooltipAsModal, align, onOpenClose, variant, ...props }: ITooltipUnControlled<V>,
19+
{
20+
ctv,
21+
tooltipAsModal,
22+
align,
23+
onOpenClose,
24+
variant,
25+
tooltipAriaLabel,
26+
...props
27+
}: ITooltipUnControlled<V>,
1928
ref: React.ForwardedRef<HTMLDivElement> | undefined | null
2029
): JSX.Element => {
2130
const styles = useStyles<TooltipVariantStylesProps, V>(TOOLTIP_STYLES, variant, ctv);
@@ -24,18 +33,16 @@ const TooltipUnControlledComponent = React.forwardRef(
2433
const labelRef = React.useRef<HTMLDivElement>(null);
2534
const tooltipRef = React.useRef<HTMLDivElement>(null);
2635

36+
const helpAriaLabel = useTooltipAsModalAriaLabel(tooltipRef);
37+
2738
const tooltipAsModalValue = useTooltipAsModal({
2839
propTooltipAsModal: tooltipAsModal,
2940
styleTooltipAsModal: styles.tooltipAsModal,
3041
});
3142

32-
React.useImperativeHandle(
33-
ref,
34-
() => {
35-
return labelRef.current as HTMLDivElement;
36-
},
37-
[]
38-
);
43+
React.useImperativeHandle(ref, () => {
44+
return labelRef.current as HTMLDivElement;
45+
}, []);
3946

4047
const { showTooltip, hideTooltip, allowFocusOpenTooltip, open } = useTooltip<V>({
4148
labelRef,
@@ -144,6 +151,7 @@ const TooltipUnControlledComponent = React.forwardRef(
144151
mediaDevice={mediaDevice}
145152
popoverOpen={open}
146153
styles={styles}
154+
tooltipAriaLabel={tooltipAriaLabel ?? helpAriaLabel}
147155
tooltipAsModal={tooltipAsModalValue}
148156
tooltipRef={tooltipRef}
149157
onBlur={handleBlur}

src/components/tooltip/types/tooltip.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export interface ITooltipStandAlone {
5555
onTooltipKeyDown?: React.KeyboardEventHandler<HTMLElement>;
5656
popover?: TooltipPopoverType;
5757
dragIcon?: IElementOrIcon;
58+
tooltipAriaLabel?: string;
5859
}
5960

6061
/**

0 commit comments

Comments
 (0)