diff --git a/.changeset/blue-hats-care.md b/.changeset/blue-hats-care.md new file mode 100644 index 0000000000..94596140fd --- /dev/null +++ b/.changeset/blue-hats-care.md @@ -0,0 +1,7 @@ +--- +'@storefront-ui/vue': minor +--- + +- **[ADDED]** Support for setting `id` attribute on the content in `SfTooltip` component. +- **[ADDED]** Possibility to open tooltip programatically via `modelValue` prop. +- **[ADDED]** `useTooltip` now closes tooltip on `Escape` keypress. diff --git a/.changeset/great-geckos-dance.md b/.changeset/great-geckos-dance.md new file mode 100644 index 0000000000..d4cbf55f1a --- /dev/null +++ b/.changeset/great-geckos-dance.md @@ -0,0 +1,11 @@ +--- +'@storefront-ui/vue': major +--- + +- **[CHANGED][BREAKING]** Use `useId` method coming from `vue` package instead of custom implementation. To migrate: +1. Update your `vue` dependency version to at least 3.5.0. +2. Update every `useId` usage as follows: +```diff +-import { useId } from '@storefront-ui/vue'; ++import { useId } from 'vue'; +``` diff --git a/.changeset/tricky-rats-buy.md b/.changeset/tricky-rats-buy.md new file mode 100644 index 0000000000..7528c2dc2f --- /dev/null +++ b/.changeset/tricky-rats-buy.md @@ -0,0 +1,7 @@ +--- +'@storefront-ui/react': minor +--- + +- **[ADDED]** Support for passing `id` and `data-testid` attributes to `SfTooltip` component. +- **[ADDED]** Possibility to open tooltip programatically via `open` prop. +- **[ADDED]** `useTooltip` now closes tooltip on `Escape` keypress. diff --git a/apps/docs/components/content/_components/tooltip.md b/apps/docs/components/content/_components/tooltip.md index 9bdcbf4f25..2f375649fe 100644 --- a/apps/docs/components/content/_components/tooltip.md +++ b/apps/docs/components/content/_components/tooltip.md @@ -20,6 +20,8 @@ Learn more about `useTooltip` composable in the [Composables > useTooltip docs]( ### Basic Usage +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. + ::vue-only @@ -31,6 +33,21 @@ Learn more about `useTooltip` composable in the [Composables > useTooltip docs]( +### Focusable Tooltip content + +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. + + + +::vue-only +<<<../../../../preview/nuxt/pages/showcases/Tooltip/FocusableTooltip.vue +:: +::react-only +<<<../../../../preview/next/pages/showcases/Tooltip/FocusableTooltip.tsx +:: + + + ## Accessibility notes By default, this component sets `role="tooltip"`. @@ -48,6 +65,7 @@ By default, this component sets `role="tooltip"`. | Prop name | Type | Default value | Possible values | | --------- | -------------------------------------------------------- | ------------- | --------------- | | `label`\* | `string` | | | +| `modelValue` | `boolean` | `false` | | | `showArrow` | `boolean` | `false` | | | `placement` | `SfPopoverPlacement` | | | | `arrowSize` | `${number}px` | `${number}em` | `${number}rem` | | | @@ -56,6 +74,7 @@ By default, this component sets `role="tooltip"`. | Prop name | Type | Default value | Possible values | | --------- | -------------------------------------------------------- | ------------- | --------------- | | `label`\* | `string` | | | +| `open` | `boolean` | `false` | | | `showArrow` | `boolean` | `false` | | | `placement` | `SfPopoverPlacement` | | | | `arrowSize` | `${number}px` | `${number}em` | `${number}rem` | | | diff --git a/apps/preview/next/pages/showcases/Tooltip/BasicTooltip.tsx b/apps/preview/next/pages/showcases/Tooltip/BasicTooltip.tsx index f739408ed4..c8c12c2815 100644 --- a/apps/preview/next/pages/showcases/Tooltip/BasicTooltip.tsx +++ b/apps/preview/next/pages/showcases/Tooltip/BasicTooltip.tsx @@ -1,11 +1,15 @@ import { ShowcasePageLayout } from '../../showcases'; // #region source +import { useId } from 'react'; import { SfTooltip } from '@storefront-ui/react'; export default function BasicTooltip() { + const id = useId(); + const tooltipId = `${id}-tooltip`; + return ( - - Hover me! + + Hover me! ); } diff --git a/apps/preview/next/pages/showcases/Tooltip/FocusableTooltip.tsx b/apps/preview/next/pages/showcases/Tooltip/FocusableTooltip.tsx new file mode 100644 index 0000000000..d595718425 --- /dev/null +++ b/apps/preview/next/pages/showcases/Tooltip/FocusableTooltip.tsx @@ -0,0 +1,21 @@ +import { ShowcasePageLayout } from '../../showcases'; +// #region source +import { useId } from 'react'; +import { SfTooltip, useDisclosure } from '@storefront-ui/react'; + +export default function BasicTooltip() { + const id = useId(); + const tooltipId = `${id}-tooltip`; + const { isOpen, open, close } = useDisclosure(); + + return ( + + + Hover or focus me! + + + ); +} + +// #endregion source +BasicTooltip.getLayout = ShowcasePageLayout; diff --git a/apps/preview/nuxt/pages/showcases/Combobox/ComboboxBasic.vue b/apps/preview/nuxt/pages/showcases/Combobox/ComboboxBasic.vue index 752a522dad..9fab0823bf 100644 --- a/apps/preview/nuxt/pages/showcases/Combobox/ComboboxBasic.vue +++ b/apps/preview/nuxt/pages/showcases/Combobox/ComboboxBasic.vue @@ -107,7 +107,7 @@ diff --git a/apps/preview/nuxt/pages/showcases/RatingButton/CustomIcon.vue b/apps/preview/nuxt/pages/showcases/RatingButton/CustomIcon.vue index 6ceb429725..4c593a91b1 100644 --- a/apps/preview/nuxt/pages/showcases/RatingButton/CustomIcon.vue +++ b/apps/preview/nuxt/pages/showcases/RatingButton/CustomIcon.vue @@ -21,8 +21,8 @@ diff --git a/apps/preview/nuxt/pages/showcases/RatingButton/MaxNumber.vue b/apps/preview/nuxt/pages/showcases/RatingButton/MaxNumber.vue index 37cd11264c..9d2917b4ef 100644 --- a/apps/preview/nuxt/pages/showcases/RatingButton/MaxNumber.vue +++ b/apps/preview/nuxt/pages/showcases/RatingButton/MaxNumber.vue @@ -6,8 +6,8 @@ diff --git a/apps/preview/nuxt/pages/showcases/RatingForms/ProductRating.vue b/apps/preview/nuxt/pages/showcases/RatingForms/ProductRating.vue index 20c63e752a..9553046cb9 100644 --- a/apps/preview/nuxt/pages/showcases/RatingForms/ProductRating.vue +++ b/apps/preview/nuxt/pages/showcases/RatingForms/ProductRating.vue @@ -39,8 +39,8 @@ diff --git a/apps/preview/nuxt/pages/showcases/Tooltip/FocusableTooltip.vue b/apps/preview/nuxt/pages/showcases/Tooltip/FocusableTooltip.vue new file mode 100644 index 0000000000..ca0189eb62 --- /dev/null +++ b/apps/preview/nuxt/pages/showcases/Tooltip/FocusableTooltip.vue @@ -0,0 +1,14 @@ + + + diff --git a/packages/sfui/frameworks/nuxt/src/module.ts b/packages/sfui/frameworks/nuxt/src/module.ts index a4afe41b54..5cc25c992a 100644 --- a/packages/sfui/frameworks/nuxt/src/module.ts +++ b/packages/sfui/frameworks/nuxt/src/module.ts @@ -56,8 +56,7 @@ export default defineNuxtModule({ if (key.startsWith('Sf') && (storefrontUi[key].__name || storefrontUi[key].name)) { components.push(key); } else if (key.startsWith('use')) { - // `useId` is already available in nuxtjs, we omit `useId` because of duplication warning - if (key !== 'useId') composables.push(key); + composables.push(key); } }); diff --git a/packages/sfui/frameworks/react/components/SfBadge/SfBadge.tsx b/packages/sfui/frameworks/react/components/SfBadge/SfBadge.tsx index 6fb27bf2b9..98b18ed90a 100644 --- a/packages/sfui/frameworks/react/components/SfBadge/SfBadge.tsx +++ b/packages/sfui/frameworks/react/components/SfBadge/SfBadge.tsx @@ -19,18 +19,16 @@ export default function SfBadge({ return ( { + if (openProp) open(); + else close(); + }, [openProp, open, close]); return ( - + {children} {label && isOpen && (
diff --git a/packages/sfui/frameworks/react/components/SfTooltip/types.ts b/packages/sfui/frameworks/react/components/SfTooltip/types.ts index 5d811e5c65..83a2eb7a8f 100644 --- a/packages/sfui/frameworks/react/components/SfTooltip/types.ts +++ b/packages/sfui/frameworks/react/components/SfTooltip/types.ts @@ -2,6 +2,9 @@ import type { PropsWithChildren } from 'react'; import type { UseTooltipOptions, PropsWithStyle } from '@storefront-ui/react'; export interface SfTooltipProps extends UseTooltipOptions, PropsWithChildren, PropsWithStyle { + id?: string; + 'data-testid'?: string; label: string; showArrow?: boolean; + open?: boolean; } diff --git a/packages/sfui/frameworks/react/hooks/useTooltip/useTooltip.ts b/packages/sfui/frameworks/react/hooks/useTooltip/useTooltip.ts index 888b9a729e..538fe81a0d 100644 --- a/packages/sfui/frameworks/react/hooks/useTooltip/useTooltip.ts +++ b/packages/sfui/frameworks/react/hooks/useTooltip/useTooltip.ts @@ -9,6 +9,7 @@ import { useDisclosure, } from '@storefront-ui/react'; import type { UseTooltipOptions } from '@storefront-ui/react'; +import { useKey } from 'react-use'; export function useTooltip(options?: UseTooltipOptions) { const { @@ -67,6 +68,8 @@ export function useTooltip(options?: UseTooltipOptions) { style: { ...userProps.style, ...arrowStyle() }, })); + useKey('Escape', close, { target: refs.reference.current }, [close, refs.reference]); + return { refs: { ...refs, diff --git a/packages/sfui/frameworks/vue/components/SfChip/SfChip.vue b/packages/sfui/frameworks/vue/components/SfChip/SfChip.vue index 19e1911484..b0811f8854 100644 --- a/packages/sfui/frameworks/vue/components/SfChip/SfChip.vue +++ b/packages/sfui/frameworks/vue/components/SfChip/SfChip.vue @@ -5,9 +5,8 @@ const sizeClasses = { };