Skip to content

Commit 491cdbe

Browse files
committed
fix: onClose callback triggered too often [ES-2025]
onClose callback now will be triggered only if outside click triggers closing of the dropdown add useDropdown & SfDropdown examples with proper a11y
1 parent 023abe7 commit 491cdbe

8 files changed

Lines changed: 113 additions & 59 deletions

File tree

.changeset/ten-buckets-help.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@storefront-ui/vue': major
3+
---
4+
5+
- \*\*[FIXED][BREAKING] From now on, `onClose` callback in `useDropdown` will be triggered only if outside click triggers closing of the dropdown.

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ By default, the floating content of `SfDropdown` will be placed below your trigg
4040

4141
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.
4242

43+
::tip
44+
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.
45+
::
46+
4347
## Playground
4448

4549
<Generate style="height: 450px" />

apps/docs/components/content/_hooks/useDropdown.md

Lines changed: 11 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,40 +8,13 @@
88

99
## Usage
1010

11+
::react-only
1112
For a minimal example, we can implement a floating element using two properties returned by the `useDropdown` hook.
1213

1314
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.
1415
2. `style` - An object containing the position styles for your floating element.
1516

1617
By binding these properties to the appropriate elements, we can create a dropdown menu that opens when a button is clicked.
17-
18-
::react-only
19-
```tsx
20-
import * as React from 'react';
21-
import { useDropdown, SfButton } from '@storefront-ui/react';
22-
23-
function Dropdown() {
24-
const [isOpen, setOpen] = React.useState(false);
25-
26-
const close = () => setOpen(false);
27-
const toggle = () => setOpen((isOpen) => !isOpen);
28-
29-
const { refs, style } = useDropdown({ isOpen, onClose: close });
30-
31-
return (
32-
<div ref={refs.setReference} className="w-max">
33-
<SfButton onClick={toggle}>Toggle</SfButton>
34-
{isOpen && (
35-
<ul ref={refs.setFloating} style={style.floating} className="absolute p-2 w-max rounded-sm bg-gray-100">
36-
<li>More</li>
37-
<li>About</li>
38-
<li>Settings</li>
39-
</ul>
40-
)}
41-
</div>
42-
);
43-
}
44-
```
4518
::
4619

4720
::vue-only
@@ -52,30 +25,19 @@ For a minimal example, we can implement a floating element using three propertie
5225
3. `style` - An object containing the position styles for your floating element.
5326

5427
By binding these properties to the appropriate elements, we can create a dropdown menu that opens when a button is clicked.
28+
::
29+
30+
<Showcase showcase-name="useDropdown/UseDropdown">
5531

56-
```vue
57-
<script lang="ts" setup>
58-
import { ref } from 'vue';
59-
import { useDropdown, SfButton } from '@storefront-ui/vue';
60-
61-
const isOpen = ref(false);
62-
63-
const { referenceRef, floatingRef, style } = useDropdown({ isOpen, onClose: () => isOpen.value = false });
64-
</script>
65-
66-
<template>
67-
<div ref="referenceRef" class="w-max">
68-
<SfButton @click="isOpen = !isOpen">Toggle</SfButton>
69-
<ul v-if="isOpen" ref="floatingRef" :style="style" class="absolute p-2 w-max rounded-sm bg-gray-100">
70-
<li>More</li>
71-
<li>About</li>
72-
<li>Settings</li>
73-
</ul>
74-
</div>
75-
</template>
76-
```
32+
::react-only
33+
<<<../../../../preview/next/pages/showcases/useDropdown/UseDropdown.tsx#source
34+
::
35+
::vue-only
36+
<<<../../../../preview/nuxt/pages/showcases/useDropdown/UseDropdown.vue
7737
::
7838

39+
</Showcase>
40+
7941
::tip There are more options!
8042
For a full list of the possible parameters and return values, see the API section.
8143
::

apps/preview/next/pages/showcases/Dropdown/BasicDropdown.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
import { ShowcasePageLayout } from '../../showcases';
22

33
// #region source
4+
import { useRef } from 'react';
45
import { SfButton, SfDropdown, useDisclosure } from '@storefront-ui/react';
56

67
export default function BasicDropdown() {
78
const { isOpen, toggle, close } = useDisclosure();
9+
const triggerRef = useRef<HTMLButtonElement>(null);
10+
11+
const handleClose = () => {
12+
close();
13+
triggerRef.current?.focus();
14+
};
815
return (
9-
<SfDropdown trigger={<SfButton onClick={toggle}>Toggle</SfButton>} open={isOpen} onClose={close}>
16+
<SfDropdown
17+
trigger={<SfButton ref={triggerRef}
18+
onClick={toggle}>Toggle</SfButton>}
19+
open={isOpen} onClose={handleClose}
20+
>
1021
<ul className="p-2 rounded-sm bg-gray-100">
11-
<li>More</li>
12-
<li>About</li>
13-
<li>Settings</li>
22+
<li tabIndex={0}>More</li>
23+
<li tabIndex={0}>About</li>
24+
<li tabIndex={0}>Settings</li>
1425
</ul>
1526
</SfDropdown>
1627
);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/* eslint-disable jsx-a11y/label-has-associated-control */
2+
import { ShowcasePageLayout } from '../../showcases';
3+
// #region source
4+
import * as React from 'react';
5+
import { useDropdown, SfButton } from '@storefront-ui/react';
6+
7+
export default function Dropdown() {
8+
const [isOpen, setOpen] = React.useState(false);
9+
const triggerRef = React.useRef<HTMLButtonElement>(null);
10+
11+
const close = () => {
12+
setOpen(false);
13+
triggerRef.current?.focus();
14+
};
15+
const toggle = () => setOpen((isOpen) => !isOpen);
16+
17+
const { refs, style } = useDropdown({ isOpen, onClose: close });
18+
19+
return (
20+
<div ref={refs.setReference} className="w-max">
21+
<SfButton ref={triggerRef} onClick={toggle}>Toggle</SfButton>
22+
{isOpen && (
23+
<ul ref={refs.setFloating} style={style} className="absolute p-2 w-max rounded-sm bg-gray-100">
24+
<li tabIndex={0}>More</li>
25+
<li tabIndex={0}>About</li>
26+
<li tabIndex={0}>Settings</li>
27+
</ul>
28+
)}
29+
</div>
30+
);
31+
}
32+
33+
// #endregion source
34+
Dropdown.getLayout = ShowcasePageLayout;
Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
<template>
2-
<SfDropdown v-model="isOpen">
2+
<SfDropdown v-model="isOpen" @update:modelValue="onToggle">
33
<template #trigger>
4-
<SfButton @click="toggle()">Toggle</SfButton>
4+
<SfButton ref="triggerRef" @click="toggle()">Toggle</SfButton>
55
</template>
66
<ul class="p-2 rounded-sm bg-gray-100">
7-
<li>More</li>
8-
<li>About</li>
9-
<li>Settings</li>
7+
<li tabIndex="0">More</li>
8+
<li tabIndex="0">About</li>
9+
<li tabIndex="0">Settings</li>
1010
</ul>
1111
</SfDropdown>
1212
</template>
1313

1414
<script lang="ts" setup>
1515
import { SfDropdown, useDisclosure, SfButton } from '@storefront-ui/vue';
16+
import { templateRef, unrefElement } from '@vueuse/core';
1617
1718
const { isOpen, toggle } = useDisclosure();
19+
const triggerRef = templateRef('triggerRef');
20+
21+
const onToggle = (isOpen: boolean) => {
22+
if (!isOpen) {
23+
unrefElement(triggerRef.value)?.focus();
24+
}
25+
};
1826
</script>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<template>
2+
<div ref="referenceRef" class="w-max">
3+
<SfButton ref="triggerRef" @click="isOpen = !isOpen">Toggle</SfButton>
4+
<ul v-if="isOpen" ref="floatingRef" :style="style" class="absolute p-2 w-max rounded-sm bg-gray-100">
5+
<li tabIndex="0">More</li>
6+
<li tabIndex="0">About</li>
7+
<li tabIndex="0">Settings</li>
8+
</ul>
9+
</div>
10+
</template>
11+
12+
<script lang="ts" setup>
13+
import { ref } from 'vue';
14+
import { useDropdown, SfButton } from '@storefront-ui/vue';
15+
import { templateRef, unrefElement } from '@vueuse/core';
16+
17+
const isOpen = ref(false);
18+
const triggerRef = templateRef('triggerRef');
19+
20+
const close = () => {
21+
isOpen.value = false;
22+
unrefElement(triggerRef.value)?.focus();
23+
};
24+
25+
const { referenceRef, floatingRef, style } = useDropdown({ isOpen, onClose: close });
26+
</script>

packages/sfui/frameworks/vue/composables/useDropdown/useDropdown.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ export function useDropdown(options: UseDropdownOptions) {
1313
...popoverOptions,
1414
});
1515

16-
onClickOutside(referenceRef as MaybeElementRef, onClose);
16+
onClickOutside(referenceRef as MaybeElementRef, () => {
17+
if (toValue(isOpen)) {
18+
onClose?.();
19+
}
20+
});
1721
onKeyStroke('Escape', onClose, { target: referenceRef as MaybeRefOrGetter<EventTarget | null | undefined> });
1822

1923
return { floatingRef, referenceRef, style };

0 commit comments

Comments
 (0)