Skip to content

Commit a708eaf

Browse files
authored
feat(tooltip): improve tooltip accessibility [ES-2025] (#3334)
support for passing id and data-testid attributes add possibility to open tooltip programatically close tooltip on Escape keypress update docs & showcases
1 parent 55fa6a2 commit a708eaf

16 files changed

Lines changed: 206 additions & 27 deletions

File tree

.changeset/blue-hats-care.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@storefront-ui/vue': minor
3+
---
4+
5+
- **[ADDED]** Support for setting `id` attribute on the content in `SfTooltip` component.
6+
- **[ADDED]** Possibility to open tooltip programatically via `modelValue` prop.
7+
- **[ADDED]** `useTooltip` now closes tooltip on `Escape` keypress.

.changeset/tricky-rats-buy.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@storefront-ui/react': minor
3+
---
4+
5+
- **[ADDED]** Support for passing `id` and `data-testid` attributes to `SfTooltip` component.
6+
- **[ADDED]** Possibility to open tooltip programatically via `open` prop.
7+
- **[ADDED]** `useTooltip` now closes tooltip on `Escape` keypress.

apps/docs/components/content/_components/tooltip.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Learn more about `useTooltip` composable in the [Composables > useTooltip docs](
2020

2121
### Basic Usage
2222

23+
The tooltip appears on hover and is useful for displaying extra information to desktop users. For accessibility, always set the `id` prop on `SfTooltip` and use the same value for the `aria-describedby` attribute on the child element.
24+
2325
<Showcase showcase-name="Tooltip/BasicTooltip">
2426

2527
::vue-only
@@ -31,6 +33,21 @@ Learn more about `useTooltip` composable in the [Composables > useTooltip docs](
3133

3234
</Showcase>
3335

36+
### Focusable Tooltip content
37+
38+
For improved accessibility and better support for mobile users, ensure the tooltip’s trigger element is focusable. You can do this by applying a `tabindex` attribute or by using a natively focusable element such as a `button` or `input`. Also, handle the `focus` and `blur` events on the trigger to control the tooltip’s visibility. See the showcase below for a implementation example.
39+
40+
<Showcase showcase-name="Tooltip/FocusableTooltip">
41+
42+
::vue-only
43+
<<<../../../../preview/nuxt/pages/showcases/Tooltip/FocusableTooltip.vue
44+
::
45+
::react-only
46+
<<<../../../../preview/next/pages/showcases/Tooltip/FocusableTooltip.tsx
47+
::
48+
49+
</Showcase>
50+
3451
## Accessibility notes
3552

3653
By default, this component sets `role="tooltip"`.
@@ -48,6 +65,7 @@ By default, this component sets `role="tooltip"`.
4865
| Prop name | Type | Default value | Possible values |
4966
| --------- | -------------------------------------------------------- | ------------- | --------------- |
5067
| `label`\* | `string` | | |
68+
| `modelValue` | `boolean` | `false` | |
5169
| `showArrow` | `boolean` | `false` | |
5270
| `placement` | `SfPopoverPlacement` | | |
5371
| `arrowSize` | `${number}px` &#124; `${number}em` &#124; `${number}rem` | | |
@@ -56,6 +74,7 @@ By default, this component sets `role="tooltip"`.
5674
| Prop name | Type | Default value | Possible values |
5775
| --------- | -------------------------------------------------------- | ------------- | --------------- |
5876
| `label`\* | `string` | | |
77+
| `open` | `boolean` | `false` | |
5978
| `showArrow` | `boolean` | `false` | |
6079
| `placement` | `SfPopoverPlacement` | | |
6180
| `arrowSize` | `${number}px` &#124; `${number}em` &#124; `${number}rem` | | |

apps/preview/next/pages/showcases/Tooltip/BasicTooltip.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { ShowcasePageLayout } from '../../showcases';
22
// #region source
3+
import { useId } from 'react';
34
import { SfTooltip } from '@storefront-ui/react';
45

56
export default function BasicTooltip() {
7+
const id = useId();
8+
const tooltipId = `${id}-tooltip`;
9+
610
return (
7-
<SfTooltip label="This is a tooltip!">
8-
<span>Hover me!</span>
11+
<SfTooltip label="This is a tooltip!" id={tooltipId}>
12+
<span aria-describedby={tooltipId}>Hover me!</span>
913
</SfTooltip>
1014
);
1115
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ShowcasePageLayout } from '../../showcases';
2+
// #region source
3+
import { useId } from 'react';
4+
import { SfTooltip, useDisclosure } from '@storefront-ui/react';
5+
6+
export default function BasicTooltip() {
7+
const id = useId();
8+
const tooltipId = `${id}-tooltip`;
9+
const { isOpen, open, close } = useDisclosure();
10+
11+
return (
12+
<SfTooltip label="This is a tooltip!" id={tooltipId} open={isOpen} showArrow placement="right">
13+
<span aria-describedby={tooltipId} onFocus={open} onBlur={close} tabIndex={0}>
14+
Hover or focus me!
15+
</span>
16+
</SfTooltip>
17+
);
18+
}
19+
20+
// #endregion source
21+
BasicTooltip.getLayout = ShowcasePageLayout;
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
<template>
22
<div class="mt-12">
3-
<SfTooltip label="This is a tooltip!"> Hover me! </SfTooltip>
3+
<SfTooltip label="This is a tooltip!" :id="tooltipId">
4+
<span :aria-describedby="tooltipId"> Hover me! </span>
5+
</SfTooltip>
46
</div>
57
</template>
68

79
<script setup lang="ts">
10+
import { useId } from 'vue';
811
import { SfTooltip } from '@storefront-ui/vue';
12+
13+
const id = useId();
14+
const tooltipId = `${id}-tooltip`;
915
</script>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<template>
2+
<SfTooltip label="This is a tooltip!" :id="tooltipId" v-model="isOpen" show-arrow placement="right">
3+
<span :aria-describedby="tooltipId" @focus="open" @blur="close" tabindex="0"> Hover or focus me! </span>
4+
</SfTooltip>
5+
</template>
6+
7+
<script setup lang="ts">
8+
import { useId } from 'vue';
9+
import { SfTooltip, useDisclosure } from '@storefront-ui/vue';
10+
11+
const id = useId();
12+
const tooltipId = `${id}-tooltip`;
13+
const { isOpen, open, close } = useDisclosure();
14+
</script>

packages/sfui/frameworks/react/components/SfBadge/SfBadge.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,16 @@ export default function SfBadge({
1919
return (
2020
<span
2121
className={twMerge(
22-
twMerge(
23-
'block absolute py-0.5 px-1 bg-negative-700 font-medium text-white text-[8px] leading-[8px] rounded-xl',
24-
{
25-
'min-w-[12px] min-h-[12px]': !isDot,
26-
'w-[10px] h-[10px]': isDot,
27-
'top-0 right-0 -translate-x-0.5 translate-y-0.5': placement === 'top-right',
28-
'top-0 left-0 translate-x-0.5 translate-y-0.5': placement === 'top-left',
29-
'bottom-0 right-0 -translate-x-0.5 -translate-y-0.5': placement === 'bottom-right',
30-
'bottom-0 left-0 translate-x-0.5 -translate-y-0.5': placement === 'bottom-left',
31-
},
32-
className,
33-
),
22+
'block absolute py-0.5 px-1 bg-negative-700 font-medium text-white text-[8px] leading-[8px] rounded-xl',
23+
{
24+
'min-w-[12px] min-h-[12px]': !isDot,
25+
'w-[10px] h-[10px]': isDot,
26+
'top-0 right-0 -translate-x-0.5 translate-y-0.5': placement === 'top-right',
27+
'top-0 left-0 translate-x-0.5 translate-y-0.5': placement === 'top-left',
28+
'bottom-0 right-0 -translate-x-0.5 -translate-y-0.5': placement === 'bottom-right',
29+
'bottom-0 left-0 translate-x-0.5 -translate-y-0.5': placement === 'bottom-left',
30+
},
31+
className,
3432
)}
3533
data-testid="badge"
3634
{...attributes}

packages/sfui/frameworks/react/components/SfTooltip/SfTooltip.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
11
'use client';
22
import { useTooltip } from '@storefront-ui/react';
33
import type { SfTooltipProps } from '@storefront-ui/react';
4+
import { useEffect } from 'react';
45

56
export default function SfTooltip(props: SfTooltipProps) {
6-
const { children, label, className, style, showArrow, ...tooltipOptions } = props;
7-
const { isOpen, getTriggerProps, getTooltipProps, getArrowProps } = useTooltip(tooltipOptions);
7+
const {
8+
children,
9+
label,
10+
className,
11+
style,
12+
open: openProp,
13+
showArrow,
14+
id,
15+
'data-testid': dataTestid,
16+
...tooltipOptions
17+
} = props;
18+
const { isOpen, open, close, getTriggerProps, getTooltipProps, getArrowProps } = useTooltip(tooltipOptions);
19+
20+
useEffect(() => {
21+
if (openProp) open();
22+
else close();
23+
}, [openProp, open, close]);
824

925
return (
10-
<span {...getTriggerProps({ className, style })} data-testid="tooltip">
26+
<span data-testid={dataTestid ?? 'tooltip'} {...getTriggerProps({ className, style })}>
1127
{children}
1228
{label && isOpen && (
1329
<div
1430
{...getTooltipProps({
1531
role: 'tooltip',
32+
id,
1633
className: 'bg-black px-2 py-1.5 rounded-md text-white text-xs w-max max-w-[360px] drop-shadow-sm',
1734
})}
1835
>

packages/sfui/frameworks/react/components/SfTooltip/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import type { PropsWithChildren } from 'react';
22
import type { UseTooltipOptions, PropsWithStyle } from '@storefront-ui/react';
33

44
export interface SfTooltipProps extends UseTooltipOptions, PropsWithChildren, PropsWithStyle {
5+
id?: string;
6+
'data-testid'?: string;
57
label: string;
68
showArrow?: boolean;
9+
open?: boolean;
710
}

0 commit comments

Comments
 (0)