diff --git a/.changeset/ten-buckets-help.md b/.changeset/ten-buckets-help.md
new file mode 100644
index 0000000000..2b54d26987
--- /dev/null
+++ b/.changeset/ten-buckets-help.md
@@ -0,0 +1,5 @@
+---
+'@storefront-ui/vue': major
+---
+
+- **[FIXED][BREAKING]** From now on, `onClose` callback in `useDropdown` will be triggered only if outside click triggers closing of the dropdown. Previously onClose was also triggered when the dropdown was already closed.
diff --git a/apps/docs/components/content/_components/dropdown.md b/apps/docs/components/content/_components/dropdown.md
index a0b08bb4e2..871bbe6f73 100644
--- a/apps/docs/components/content/_components/dropdown.md
+++ b/apps/docs/components/content/_components/dropdown.md
@@ -40,6 +40,10 @@ By default, the floating content of `SfDropdown` will be placed below your trigg
The floating content area has an `aria-hidden` attribute that reflects the visibility of the dropdown (`modelValue`). When the dropdown is not open (`modelValue` is `false`), the `aria-hidden` attribute is set to `true`, ensuring that the content is hidden from assistive technologies.
+::tip
+This component lets you create many types of custom dropdowns, such as navigation menus, comboboxes, or select lists. However, accessibility depends on how you implement it in your use case. Always ensure users can move through items in the dropdown using arrow keys and/or the Tab key. Additionally, after the dropdown is closed, make sure focus returns to the trigger element.
+::
+
## Playground
diff --git a/apps/docs/components/content/_hooks/useDropdown.md b/apps/docs/components/content/_hooks/useDropdown.md
index e0295924ac..cf69d00392 100644
--- a/apps/docs/components/content/_hooks/useDropdown.md
+++ b/apps/docs/components/content/_hooks/useDropdown.md
@@ -8,40 +8,13 @@
## Usage
+::react-only
For a minimal example, we can implement a floating element using two properties returned by the `useDropdown` hook.
1. `refs` - An object that contains a `setReference` and `setFloating` function. These functions should be bound to the element that the floating element will be positioned relative to and the floating element itself, respectively.
2. `style` - An object containing the position styles for your floating element.
By binding these properties to the appropriate elements, we can create a dropdown menu that opens when a button is clicked.
-
-::react-only
-```tsx
-import * as React from 'react';
-import { useDropdown, SfButton } from '@storefront-ui/react';
-
-function Dropdown() {
- const [isOpen, setOpen] = React.useState(false);
-
- const close = () => setOpen(false);
- const toggle = () => setOpen((isOpen) => !isOpen);
-
- const { refs, style } = useDropdown({ isOpen, onClose: close });
-
- return (
-
-
Toggle
- {isOpen && (
-
- - More
- - About
- - Settings
-
- )}
-
- );
-}
-```
::
::vue-only
@@ -52,30 +25,19 @@ For a minimal example, we can implement a floating element using three propertie
3. `style` - An object containing the position styles for your floating element.
By binding these properties to the appropriate elements, we can create a dropdown menu that opens when a button is clicked.
+::
+
+
-```vue
-
-
-
-
-
Toggle
-
- - More
- - About
- - Settings
-
-
-
-```
+::react-only
+<<<../../../../preview/next/pages/showcases/useDropdown/UseDropdown.tsx#source
+::
+::vue-only
+<<<../../../../preview/nuxt/pages/showcases/useDropdown/UseDropdown.vue
::
+
+
::tip There are more options!
For a full list of the possible parameters and return values, see the API section.
::
diff --git a/apps/preview/next/pages/showcases/Dropdown/BasicDropdown.tsx b/apps/preview/next/pages/showcases/Dropdown/BasicDropdown.tsx
index 75a7bea872..7d89422987 100644
--- a/apps/preview/next/pages/showcases/Dropdown/BasicDropdown.tsx
+++ b/apps/preview/next/pages/showcases/Dropdown/BasicDropdown.tsx
@@ -1,16 +1,31 @@
import { ShowcasePageLayout } from '../../showcases';
// #region source
+import { useRef } from 'react';
import { SfButton, SfDropdown, useDisclosure } from '@storefront-ui/react';
export default function BasicDropdown() {
const { isOpen, toggle, close } = useDisclosure();
+ const triggerRef = useRef(null);
+
+ const handleClose = () => {
+ close();
+ triggerRef.current?.focus();
+ };
return (
- Toggle} open={isOpen} onClose={close}>
+
+ Toggle
+
+ }
+ open={isOpen}
+ onClose={handleClose}
+ >
- - More
- - About
- - Settings
+ - More
+ - About
+ - Settings
);
diff --git a/apps/preview/next/pages/showcases/MegaMenu/MegaMenuNavigation.tsx b/apps/preview/next/pages/showcases/MegaMenu/MegaMenuNavigation.tsx
index 2e43bd578d..85e109c3c9 100644
--- a/apps/preview/next/pages/showcases/MegaMenu/MegaMenuNavigation.tsx
+++ b/apps/preview/next/pages/showcases/MegaMenu/MegaMenuNavigation.tsx
@@ -402,8 +402,8 @@ export default function MegaMenuNavigation() {
const { close, open, isOpen } = useDisclosure();
const { refs, style } = useDropdown({
isOpen,
- onClose: (event: KeyboardEvent) => {
- if (event.key === 'Escape') {
+ onClose: (event) => {
+ if ('key' in event && event.key === 'Escape') {
refsByKey[activeNode[0]]?.current?.focus();
}
close();
diff --git a/apps/preview/next/pages/showcases/useDropdown/UseDropdown.tsx b/apps/preview/next/pages/showcases/useDropdown/UseDropdown.tsx
new file mode 100644
index 0000000000..944c87f814
--- /dev/null
+++ b/apps/preview/next/pages/showcases/useDropdown/UseDropdown.tsx
@@ -0,0 +1,36 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+import { ShowcasePageLayout } from '../../showcases';
+// #region source
+import * as React from 'react';
+import { useDropdown, SfButton } from '@storefront-ui/react';
+
+export default function Dropdown() {
+ const [isOpen, setOpen] = React.useState(false);
+ const triggerRef = React.useRef(null);
+
+ const close = () => {
+ setOpen(false);
+ triggerRef.current?.focus();
+ };
+ const toggle = () => setOpen((isOpen) => !isOpen);
+
+ const { refs, style } = useDropdown({ isOpen, onClose: close });
+
+ return (
+
+
+ Toggle
+
+ {isOpen && (
+
+ - More
+ - About
+ - Settings
+
+ )}
+
+ );
+}
+
+// #endregion source
+Dropdown.getLayout = ShowcasePageLayout;
diff --git a/apps/preview/nuxt/pages/showcases/Dropdown/BasicDropdown.vue b/apps/preview/nuxt/pages/showcases/Dropdown/BasicDropdown.vue
index 2ac2619066..295361286e 100644
--- a/apps/preview/nuxt/pages/showcases/Dropdown/BasicDropdown.vue
+++ b/apps/preview/nuxt/pages/showcases/Dropdown/BasicDropdown.vue
@@ -1,18 +1,26 @@
-
+
- Toggle
+ Toggle
- - More
- - About
- - Settings
+ - More
+ - About
+ - Settings
diff --git a/apps/preview/nuxt/pages/showcases/useDropdown/UseDropdown.vue b/apps/preview/nuxt/pages/showcases/useDropdown/UseDropdown.vue
new file mode 100644
index 0000000000..cf1a90f2d0
--- /dev/null
+++ b/apps/preview/nuxt/pages/showcases/useDropdown/UseDropdown.vue
@@ -0,0 +1,26 @@
+
+
+
Toggle
+
+ - More
+ - About
+ - Settings
+
+
+
+
+
diff --git a/packages/sfui/frameworks/react/hooks/useDropdown/types.ts b/packages/sfui/frameworks/react/hooks/useDropdown/types.ts
index 8f6b174b9b..f9df1fe353 100644
--- a/packages/sfui/frameworks/react/hooks/useDropdown/types.ts
+++ b/packages/sfui/frameworks/react/hooks/useDropdown/types.ts
@@ -3,7 +3,7 @@ import type { UsePopoverOptions } from '../usePopover';
export type UseDropdownOptions = Prettify<
UsePopoverOptions & {
- onClose: (event: KeyboardEvent) => void;
+ onClose: (event: KeyboardEvent | PointerEvent | MouseEvent | TouchEvent) => void;
onCloseDeps?: unknown[];
}
>;
diff --git a/packages/sfui/frameworks/react/hooks/useDropdown/useDropdown.ts b/packages/sfui/frameworks/react/hooks/useDropdown/useDropdown.ts
index 90b140b7f3..8faaa4b96a 100644
--- a/packages/sfui/frameworks/react/hooks/useDropdown/useDropdown.ts
+++ b/packages/sfui/frameworks/react/hooks/useDropdown/useDropdown.ts
@@ -9,12 +9,17 @@ export function useDropdown(options: UseDropdownOptions) {
onCloseDeps,
placement = 'bottom',
middleware = [offset(8), shift(), flip()],
+ isOpen,
...popoverOptions
} = options;
- const { refs, style } = usePopover({ placement, middleware, ...popoverOptions });
+ const { refs, style } = usePopover({ placement, middleware, isOpen, ...popoverOptions });
- useClickAway(refs.reference, onClose);
+ useClickAway(refs.reference, (e) => {
+ if (isOpen) {
+ onClose?.(e);
+ }
+ });
useKey('Escape', onClose, { target: refs.reference.current }, onCloseDeps);
return { refs, style };
diff --git a/packages/sfui/frameworks/vue/composables/useDropdown/useDropdown.ts b/packages/sfui/frameworks/vue/composables/useDropdown/useDropdown.ts
index 218e81eca3..6893d7b93b 100644
--- a/packages/sfui/frameworks/vue/composables/useDropdown/useDropdown.ts
+++ b/packages/sfui/frameworks/vue/composables/useDropdown/useDropdown.ts
@@ -13,7 +13,11 @@ export function useDropdown(options: UseDropdownOptions) {
...popoverOptions,
});
- onClickOutside(referenceRef as MaybeElementRef, onClose);
+ onClickOutside(referenceRef as MaybeElementRef, () => {
+ if (toValue(isOpen)) {
+ onClose?.();
+ }
+ });
onKeyStroke('Escape', onClose, { target: referenceRef as MaybeRefOrGetter });
return { floatingRef, referenceRef, style };
diff --git a/packages/tests/components/SfDropdown/SfDropdown.cy.tsx b/packages/tests/components/SfDropdown/SfDropdown.cy.tsx
index 1df822adc2..075594988a 100644
--- a/packages/tests/components/SfDropdown/SfDropdown.cy.tsx
+++ b/packages/tests/components/SfDropdown/SfDropdown.cy.tsx
@@ -91,7 +91,7 @@ describe('SfDropdown', () => {
describe('When click away', () => {
it('Should call onClose', () => {
- const props = { onClose: cy.spy() };
+ const props = { onClose: cy.spy(), modelValue: ref(true) };
initializeComponent(props);
page().clickAway(props.onClose);