@asafarim/react-dropdowns 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +359 -232
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,101 +1,136 @@
|
|
|
1
1
|
# @asafarim/react-dropdowns
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Production-ready dropdown components for React** with full TypeScript support, accessibility, and mobile optimization. Built on ASafariM design tokens.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[Live Demo](https://alisafari-it.github.io/react-dropdowns/) • [GitHub](https://github.com/AliSafari-IT/react-dropdowns) • [npm](https://www.npmjs.com/package/@asafarim/react-dropdowns)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- ♿ **Accessible**: Full keyboard navigation and screen reader support
|
|
9
|
-
- 📱 **Mobile-First**: Optimized for touch devices with responsive design
|
|
10
|
-
- 🎨 **Themeable**: Uses ASafariM design tokens with dark theme support
|
|
11
|
-
- 🔧 **TypeScript**: Full type safety and IntelliSense support
|
|
12
|
-
- ⚡ **Performant**: Lightweight with minimal dependencies
|
|
13
|
-
- 🎪 **Flexible**: Multiple placement options and customization
|
|
7
|
+
---
|
|
14
8
|
|
|
15
|
-
##
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- **🎯 Comprehensive** — Multiple components for different use cases (simple dropdowns, custom triggers, advanced menus)
|
|
12
|
+
- **♿ Fully Accessible** — WCAG 2.1 compliant with keyboard navigation, screen reader support, and ARIA attributes
|
|
13
|
+
- **📱 Mobile-First** — Touch-friendly, responsive design with automatic viewport adjustment
|
|
14
|
+
- **🎨 Design Token Integration** — Seamless integration with ASafariM design tokens and dark mode support
|
|
15
|
+
- **🔧 TypeScript** — Full type safety with IntelliSense and zero runtime overhead
|
|
16
|
+
- **⚡ Performant** — Lightweight (~5KB gzipped) with minimal dependencies
|
|
17
|
+
- **🎪 Flexible** — 12 placement options, 3 sizes, multiple button variants, and extensive customization
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 📦 Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm add @asafarim/react-dropdowns
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or with your preferred package manager:
|
|
16
28
|
|
|
17
29
|
```bash
|
|
18
30
|
npm install @asafarim/react-dropdowns
|
|
19
|
-
|
|
31
|
+
or
|
|
20
32
|
yarn add @asafarim/react-dropdowns
|
|
21
|
-
# or
|
|
22
|
-
pnpm add @asafarim/react-dropdowns
|
|
23
33
|
```
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
Then import the styles in your app (in index.tsx or main.tsx):
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import '@asafarim/react-dropdowns/dist/dropdown.css';
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🚀 Quick Start
|
|
44
|
+
|
|
45
|
+
The simplest way to get started with a basic dropdown menu:
|
|
26
46
|
|
|
27
47
|
```tsx
|
|
28
48
|
import { Dropdown } from '@asafarim/react-dropdowns';
|
|
29
49
|
import '@asafarim/react-dropdowns/dist/dropdown.css';
|
|
30
50
|
|
|
31
|
-
function App() {
|
|
51
|
+
export function App() {
|
|
32
52
|
return (
|
|
33
53
|
<Dropdown
|
|
34
54
|
items={[
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
label: 'Edit',
|
|
38
|
-
onClick: () => console.log('Edit clicked')
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
id: 'delete',
|
|
42
|
-
label: 'Delete',
|
|
43
|
-
danger: true,
|
|
44
|
-
onClick: () => console.log('Delete clicked')
|
|
45
|
-
}
|
|
55
|
+
{ id: 'edit', label: 'Edit', onClick: () => console.log('Edit') },
|
|
56
|
+
{ id: 'delete', label: 'Delete', danger: true, onClick: () => console.log('Delete') }
|
|
46
57
|
]}
|
|
47
58
|
placement="bottom-start"
|
|
48
59
|
>
|
|
49
|
-
|
|
60
|
+
Actions
|
|
50
61
|
</Dropdown>
|
|
51
62
|
);
|
|
52
63
|
}
|
|
53
64
|
```
|
|
54
65
|
|
|
55
|
-
|
|
66
|
+
That's it! The dropdown handles state, positioning, keyboard navigation, and accessibility automatically.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 📚 Components
|
|
71
|
+
|
|
72
|
+
### Dropdown (Recommended)
|
|
56
73
|
|
|
57
|
-
|
|
74
|
+
The main component that combines trigger and menu functionality. Use this for most cases.
|
|
58
75
|
|
|
59
|
-
|
|
76
|
+
**Features:**
|
|
77
|
+
|
|
78
|
+
- Automatic state management
|
|
79
|
+
- Built-in click-outside detection
|
|
80
|
+
- Keyboard navigation (arrow keys, Enter, Escape)
|
|
81
|
+
- Automatic menu positioning
|
|
82
|
+
- Optional controlled state
|
|
83
|
+
|
|
84
|
+
**Basic Usage:**
|
|
60
85
|
|
|
61
86
|
```tsx
|
|
62
87
|
<Dropdown
|
|
63
88
|
items={[
|
|
64
89
|
{
|
|
65
|
-
id: '
|
|
66
|
-
label: '
|
|
67
|
-
icon: <
|
|
68
|
-
onClick: () =>
|
|
69
|
-
disabled: false,
|
|
70
|
-
danger: false
|
|
90
|
+
id: 'edit',
|
|
91
|
+
label: 'Edit',
|
|
92
|
+
icon: <Edit size={16} />,
|
|
93
|
+
onClick: () => handleEdit()
|
|
71
94
|
},
|
|
72
|
-
{ divider: true }, // Separator
|
|
73
95
|
{
|
|
74
|
-
id: '
|
|
75
|
-
label: '
|
|
76
|
-
|
|
96
|
+
id: 'delete',
|
|
97
|
+
label: 'Delete',
|
|
98
|
+
icon: <Trash2 size={16} />,
|
|
99
|
+
danger: true,
|
|
100
|
+
onClick: () => handleDelete()
|
|
77
101
|
}
|
|
78
102
|
]}
|
|
103
|
+
placement="bottom-start"
|
|
104
|
+
size="md"
|
|
105
|
+
>
|
|
106
|
+
Actions
|
|
107
|
+
</Dropdown>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**With Controlled State:**
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
114
|
+
|
|
115
|
+
<Dropdown
|
|
116
|
+
items={items}
|
|
79
117
|
isOpen={isOpen}
|
|
80
118
|
onToggle={setIsOpen}
|
|
81
119
|
placement="bottom-start"
|
|
82
|
-
size="md"
|
|
83
|
-
disabled={false}
|
|
84
|
-
closeOnSelect={true}
|
|
85
120
|
>
|
|
86
|
-
|
|
121
|
+
Menu
|
|
87
122
|
</Dropdown>
|
|
88
123
|
```
|
|
89
124
|
|
|
90
125
|
### DropdownItem
|
|
91
126
|
|
|
92
|
-
Individual menu item component.
|
|
127
|
+
Individual menu item component. Used inside `Dropdown` or `DropdownMenu`.
|
|
93
128
|
|
|
94
129
|
```tsx
|
|
95
130
|
<DropdownItem
|
|
96
|
-
label="Edit
|
|
97
|
-
icon={<
|
|
98
|
-
onClick={() =>
|
|
131
|
+
label="Edit"
|
|
132
|
+
icon={<Edit size={16} />}
|
|
133
|
+
onClick={() => handleEdit()}
|
|
99
134
|
disabled={false}
|
|
100
135
|
danger={false}
|
|
101
136
|
/>
|
|
@@ -103,82 +138,182 @@ Individual menu item component.
|
|
|
103
138
|
|
|
104
139
|
### DropdownMenu
|
|
105
140
|
|
|
106
|
-
|
|
141
|
+
Low-level menu component for advanced custom implementations. Use with `useDropdown` hook for full control.
|
|
142
|
+
|
|
143
|
+
**When to use:**
|
|
144
|
+
|
|
145
|
+
- Custom trigger designs (cards, images, etc.)
|
|
146
|
+
- Complex menu layouts
|
|
147
|
+
- Integration with other positioning libraries
|
|
148
|
+
|
|
149
|
+
**Example:**
|
|
107
150
|
|
|
108
151
|
```tsx
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
152
|
+
import { createPortal } from 'react-dom';
|
|
153
|
+
import { DropdownMenu, DropdownItem, useDropdown, useClickOutside } from '@asafarim/react-dropdowns';
|
|
154
|
+
|
|
155
|
+
function CustomDropdown() {
|
|
156
|
+
const { isOpen, position, toggle, triggerRef, menuRef, close } = useDropdown();
|
|
157
|
+
const containerRef = useRef(null);
|
|
158
|
+
|
|
159
|
+
useClickOutside({
|
|
160
|
+
ref: containerRef,
|
|
161
|
+
handler: close,
|
|
162
|
+
enabled: isOpen,
|
|
163
|
+
excludeRefs: [menuRef]
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div ref={containerRef}>
|
|
168
|
+
<div ref={triggerRef} onClick={toggle} style={{ cursor: 'pointer' }}>
|
|
169
|
+
Click me
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{isOpen && createPortal(
|
|
173
|
+
<DropdownMenu ref={menuRef} isOpen={isOpen} position={position}>
|
|
174
|
+
<DropdownItem label="Option 1" onClick={() => {}} />
|
|
175
|
+
<DropdownItem label="Option 2" onClick={() => {}} />
|
|
176
|
+
</DropdownMenu>,
|
|
177
|
+
document.body
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
117
182
|
```
|
|
118
183
|
|
|
119
|
-
|
|
184
|
+
---
|
|
120
185
|
|
|
121
|
-
|
|
186
|
+
## 🎛️ Props Reference
|
|
187
|
+
|
|
188
|
+
### Dropdown Props
|
|
122
189
|
|
|
123
190
|
| Prop | Type | Default | Description |
|
|
124
191
|
|------|------|---------|-------------|
|
|
125
|
-
| `children` | `ReactNode` |
|
|
126
|
-
| `items` | `DropdownItemData[]` | `[]` | Menu items |
|
|
127
|
-
| `isOpen` | `boolean` |
|
|
128
|
-
| `onToggle` | `(isOpen: boolean) => void` |
|
|
129
|
-
| `placement` | `DropdownPlacement` | `'bottom-start'` | Menu position |
|
|
130
|
-
| `size` | `
|
|
192
|
+
| `children` | `ReactNode` | — | Trigger element content |
|
|
193
|
+
| `items` | `DropdownItemData[]` | `[]` | Menu items to display |
|
|
194
|
+
| `isOpen` | `boolean` | — | (Optional) Controlled open state |
|
|
195
|
+
| `onToggle` | `(isOpen: boolean) => void` | — | (Optional) State change callback |
|
|
196
|
+
| `placement` | `DropdownPlacement` | `'bottom-start'` | Menu position relative to trigger |
|
|
197
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Menu size |
|
|
198
|
+
| `variant` | `ButtonVariant` | `'primary'` | Trigger button style |
|
|
131
199
|
| `disabled` | `boolean` | `false` | Disable the dropdown |
|
|
132
|
-
| `closeOnSelect` | `boolean` | `true` |
|
|
200
|
+
| `closeOnSelect` | `boolean` | `true` | Auto-close menu on item click |
|
|
201
|
+
| `showChevron` | `boolean` | `true` | Show chevron icon on trigger |
|
|
202
|
+
| `className` | `string` | — | Custom CSS class for wrapper |
|
|
203
|
+
| `data-testid` | `string` | — | Test ID for testing |
|
|
133
204
|
|
|
134
|
-
### DropdownItemData
|
|
205
|
+
### DropdownItemData Props
|
|
135
206
|
|
|
136
207
|
| Prop | Type | Default | Description |
|
|
137
208
|
|------|------|---------|-------------|
|
|
138
|
-
| `id` | `string` |
|
|
139
|
-
| `label` | `string` |
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
209
|
+
| `id` | `string` | — | Unique identifier |
|
|
210
|
+
| `label` | `string` | — | Item display text |
|
|
211
|
+
| `icon` | `ReactNode` | — | Icon to display before label |
|
|
212
|
+
| `onClick` | `(event: MouseEvent) => void` | — | Click handler |
|
|
142
213
|
| `disabled` | `boolean` | `false` | Disable the item |
|
|
143
|
-
| `danger` | `boolean` | `false` |
|
|
144
|
-
| `divider` | `boolean` | `false` | Render as
|
|
145
|
-
| `
|
|
214
|
+
| `danger` | `boolean` | `false` | Red danger styling |
|
|
215
|
+
| `divider` | `boolean` | `false` | Render as visual separator |
|
|
216
|
+
| `value` | `string` | — | Optional data value |
|
|
146
217
|
|
|
147
|
-
|
|
218
|
+
### DropdownMenu Props
|
|
148
219
|
|
|
149
|
-
|
|
220
|
+
| Prop | Type | Default | Description |
|
|
221
|
+
|------|------|---------|-------------|
|
|
222
|
+
| `children` | `ReactNode` | — | Menu content |
|
|
223
|
+
| `isOpen` | `boolean` | — | Show/hide menu |
|
|
224
|
+
| `position` | `DropdownPosition` | — | Absolute position (from `useDropdown`) |
|
|
225
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Menu size |
|
|
226
|
+
| `className` | `string` | — | Custom CSS class |
|
|
227
|
+
| `ref` | `RefObject<HTMLDivElement>` | — | Menu element reference |
|
|
150
228
|
|
|
151
|
-
|
|
152
|
-
- `bottom`, `bottom-start`, `bottom-end`
|
|
153
|
-
- `left`, `left-start`, `left-end`
|
|
154
|
-
- `right`, `right-start`, `right-end`
|
|
229
|
+
---
|
|
155
230
|
|
|
156
|
-
##
|
|
231
|
+
## 🎨 Customization
|
|
157
232
|
|
|
158
|
-
|
|
233
|
+
### Placement Options
|
|
159
234
|
|
|
160
|
-
|
|
161
|
-
- `md` - Default size for most use cases
|
|
162
|
-
- `lg` - Large size for better touch targets
|
|
235
|
+
Position the menu relative to the trigger:
|
|
163
236
|
|
|
164
|
-
|
|
237
|
+
```
|
|
238
|
+
Top: top | top-start | top-end
|
|
239
|
+
Bottom: bottom | bottom-start | bottom-end
|
|
240
|
+
Left: left | left-start | left-end
|
|
241
|
+
Right: right | right-start | right-end
|
|
242
|
+
```
|
|
165
243
|
|
|
166
|
-
|
|
244
|
+
```tsx
|
|
245
|
+
<Dropdown items={items} placement="top-end">
|
|
246
|
+
Menu
|
|
247
|
+
</Dropdown>
|
|
248
|
+
```
|
|
167
249
|
|
|
168
|
-
|
|
250
|
+
### Size Options
|
|
169
251
|
|
|
170
252
|
```tsx
|
|
171
|
-
|
|
253
|
+
<Dropdown items={items} size="sm">Compact</Dropdown>
|
|
254
|
+
<Dropdown items={items} size="md">Default</Dropdown>
|
|
255
|
+
<Dropdown items={items} size="lg">Large</Dropdown>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Button Variants
|
|
259
|
+
|
|
260
|
+
Style the trigger button:
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
<Dropdown items={items} variant="primary">Primary</Dropdown>
|
|
264
|
+
<Dropdown items={items} variant="secondary">Secondary</Dropdown>
|
|
265
|
+
<Dropdown items={items} variant="ghost">Ghost</Dropdown>
|
|
266
|
+
<Dropdown items={items} variant="outline">Outline</Dropdown>
|
|
267
|
+
<Dropdown items={items} variant="danger">Danger</Dropdown>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Custom Styling
|
|
271
|
+
|
|
272
|
+
Override default styles using CSS classes:
|
|
273
|
+
|
|
274
|
+
```css
|
|
275
|
+
/* Menu container */
|
|
276
|
+
.asm-dropdown-menu {
|
|
277
|
+
background: var(--asm-color-surface);
|
|
278
|
+
border: 1px solid var(--asm-color-border);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* Menu item */
|
|
282
|
+
.asm-dropdown-item {
|
|
283
|
+
padding: var(--asm-space-3);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* Danger item */
|
|
287
|
+
.asm-dropdown-item--danger {
|
|
288
|
+
color: var(--asm-color-danger);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* Disabled item */
|
|
292
|
+
.asm-dropdown-item:disabled {
|
|
293
|
+
opacity: 0.5;
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 🪝 Hooks
|
|
300
|
+
|
|
301
|
+
### useDropdown
|
|
302
|
+
|
|
303
|
+
Build custom dropdowns with full control over positioning and state.
|
|
304
|
+
|
|
305
|
+
**Returns:**
|
|
172
306
|
|
|
307
|
+
```tsx
|
|
173
308
|
const {
|
|
174
|
-
isOpen,
|
|
175
|
-
position,
|
|
176
|
-
triggerRef,
|
|
177
|
-
menuRef,
|
|
178
|
-
toggle,
|
|
179
|
-
open,
|
|
180
|
-
close,
|
|
181
|
-
handleItemClick
|
|
309
|
+
isOpen, // boolean - Menu visibility state
|
|
310
|
+
position, // DropdownPosition - Calculated position
|
|
311
|
+
triggerRef, // RefObject - Attach to trigger element
|
|
312
|
+
menuRef, // RefObject - Attach to menu element
|
|
313
|
+
toggle, // () => void - Toggle open/closed
|
|
314
|
+
open, // () => void - Open menu
|
|
315
|
+
close, // () => void - Close menu
|
|
316
|
+
handleItemClick // () => void - Handle item selection
|
|
182
317
|
} = useDropdown({
|
|
183
318
|
placement: 'bottom-start',
|
|
184
319
|
offset: 8,
|
|
@@ -186,133 +321,119 @@ const {
|
|
|
186
321
|
});
|
|
187
322
|
```
|
|
188
323
|
|
|
324
|
+
**Example:**
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
function CustomDropdown() {
|
|
328
|
+
const { isOpen, position, toggle, triggerRef, menuRef } = useDropdown();
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<>
|
|
332
|
+
<button ref={triggerRef} onClick={toggle}>
|
|
333
|
+
Open Menu
|
|
334
|
+
</button>
|
|
335
|
+
{isOpen && (
|
|
336
|
+
<DropdownMenu ref={menuRef} isOpen={isOpen} position={position}>
|
|
337
|
+
{/* Menu items */}
|
|
338
|
+
</DropdownMenu>
|
|
339
|
+
)}
|
|
340
|
+
</>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
189
345
|
### useClickOutside
|
|
190
346
|
|
|
191
|
-
|
|
347
|
+
Detect clicks outside an element to close menus.
|
|
192
348
|
|
|
193
349
|
```tsx
|
|
194
|
-
import { useClickOutside } from '@asafarim/react-dropdowns';
|
|
195
|
-
|
|
196
350
|
useClickOutside({
|
|
197
|
-
ref:
|
|
198
|
-
handler: () => setIsOpen(false),
|
|
199
|
-
enabled: isOpen
|
|
351
|
+
ref: containerRef, // Element to monitor
|
|
352
|
+
handler: () => setIsOpen(false), // Callback on outside click
|
|
353
|
+
enabled: isOpen, // Enable/disable detection
|
|
354
|
+
excludeRefs: [menuRef] // Refs to exclude from detection
|
|
200
355
|
});
|
|
201
356
|
```
|
|
202
357
|
|
|
203
358
|
### useKeyboardNavigation
|
|
204
359
|
|
|
205
|
-
|
|
360
|
+
Add keyboard navigation to custom dropdowns.
|
|
206
361
|
|
|
207
362
|
```tsx
|
|
208
|
-
import { useKeyboardNavigation } from '@asafarim/react-dropdowns';
|
|
209
|
-
|
|
210
363
|
useKeyboardNavigation({
|
|
211
|
-
isOpen,
|
|
212
|
-
menuRef,
|
|
364
|
+
isOpen, // boolean
|
|
365
|
+
menuRef, // RefObject to menu
|
|
213
366
|
onClose: () => setIsOpen(false),
|
|
214
367
|
onSelect: (index) => selectItem(index)
|
|
215
368
|
});
|
|
216
369
|
```
|
|
217
370
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
The components use CSS custom properties (CSS variables) from the ASafariM design token system. Import the CSS file:
|
|
221
|
-
|
|
222
|
-
```tsx
|
|
223
|
-
import '@asafarim/react-dropdowns/dist/dropdown.css';
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
### Custom Styling
|
|
227
|
-
|
|
228
|
-
You can override the default styles by targeting the CSS classes:
|
|
371
|
+
---
|
|
229
372
|
|
|
230
|
-
|
|
231
|
-
.asm-dropdown-menu {
|
|
232
|
-
/* Custom menu styles */
|
|
233
|
-
}
|
|
373
|
+
## ♿ Accessibility
|
|
234
374
|
|
|
235
|
-
.
|
|
236
|
-
/* Custom item styles */
|
|
237
|
-
}
|
|
375
|
+
Built with WCAG 2.1 AA compliance in mind:
|
|
238
376
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
## Accessibility
|
|
245
|
-
|
|
246
|
-
The dropdown components are built with accessibility in mind:
|
|
247
|
-
|
|
248
|
-
- **Keyboard Navigation**: Arrow keys, Enter, Escape, Home, End
|
|
249
|
-
- **Screen Reader Support**: Proper ARIA attributes and roles
|
|
250
|
-
- **Focus Management**: Automatic focus handling and restoration
|
|
251
|
-
- **High Contrast**: Support for high contrast mode
|
|
252
|
-
- **Reduced Motion**: Respects user's motion preferences
|
|
377
|
+
- **Keyboard Navigation** — Full support for arrow keys, Enter, Escape, Home, End
|
|
378
|
+
- **Screen Readers** — Proper ARIA roles, labels, and live regions
|
|
379
|
+
- **Focus Management** — Automatic focus handling and restoration
|
|
380
|
+
- **High Contrast** — Works with high contrast mode
|
|
381
|
+
- **Reduced Motion** — Respects `prefers-reduced-motion` setting
|
|
253
382
|
|
|
254
383
|
### Keyboard Shortcuts
|
|
255
384
|
|
|
256
385
|
| Key | Action |
|
|
257
386
|
|-----|--------|
|
|
258
|
-
| `Space` / `Enter` |
|
|
259
|
-
| `Arrow Down` |
|
|
260
|
-
| `Arrow Up` |
|
|
261
|
-
| `Home` |
|
|
262
|
-
| `End` |
|
|
263
|
-
| `Escape` | Close
|
|
264
|
-
| `Tab` | Close
|
|
387
|
+
| `Space` / `Enter` | Toggle menu or select item |
|
|
388
|
+
| `Arrow Down` | Next item / Open menu |
|
|
389
|
+
| `Arrow Up` | Previous item |
|
|
390
|
+
| `Home` | First item |
|
|
391
|
+
| `End` | Last item |
|
|
392
|
+
| `Escape` | Close menu |
|
|
393
|
+
| `Tab` | Close menu and move focus |
|
|
265
394
|
|
|
266
|
-
|
|
395
|
+
---
|
|
267
396
|
|
|
268
|
-
|
|
397
|
+
## 💡 Real-World Examples
|
|
398
|
+
|
|
399
|
+
### File Menu
|
|
269
400
|
|
|
270
401
|
```tsx
|
|
271
402
|
<Dropdown
|
|
272
403
|
items={[
|
|
273
|
-
{ id: 'new', label: 'New',
|
|
274
|
-
{ id: '
|
|
404
|
+
{ id: 'new', label: 'New', icon: <FileText size={16} /> },
|
|
405
|
+
{ id: 'open', label: 'Open', icon: <FolderOpen size={16} /> },
|
|
406
|
+
{ divider: true },
|
|
407
|
+
{ id: 'save', label: 'Save', icon: <Save size={16} /> },
|
|
408
|
+
{ id: 'export', label: 'Export', icon: <Download size={16} /> },
|
|
275
409
|
{ divider: true },
|
|
276
|
-
{ id: '
|
|
410
|
+
{ id: 'exit', label: 'Exit', danger: true, icon: <X size={16} /> }
|
|
277
411
|
]}
|
|
412
|
+
placement="bottom-start"
|
|
278
413
|
>
|
|
279
|
-
|
|
414
|
+
File
|
|
280
415
|
</Dropdown>
|
|
281
416
|
```
|
|
282
417
|
|
|
283
|
-
### User Menu
|
|
418
|
+
### User Account Menu
|
|
284
419
|
|
|
285
420
|
```tsx
|
|
421
|
+
const [user, setUser] = useState({ name: 'John Doe', avatar: '...' });
|
|
422
|
+
|
|
286
423
|
<Dropdown
|
|
287
424
|
items={[
|
|
288
|
-
{
|
|
289
|
-
|
|
290
|
-
label: 'Profile',
|
|
291
|
-
icon: <UserIcon />,
|
|
292
|
-
onClick: () => navigate('/profile')
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
id: 'settings',
|
|
296
|
-
label: 'Settings',
|
|
297
|
-
icon: <SettingsIcon />,
|
|
298
|
-
onClick: () => navigate('/settings')
|
|
299
|
-
},
|
|
425
|
+
{ id: 'profile', label: 'Profile', icon: <User size={16} /> },
|
|
426
|
+
{ id: 'settings', label: 'Settings', icon: <Settings size={16} /> },
|
|
300
427
|
{ divider: true },
|
|
301
|
-
{
|
|
302
|
-
id: 'logout',
|
|
303
|
-
label: 'Logout',
|
|
304
|
-
icon: <LogoutIcon />,
|
|
305
|
-
danger: true,
|
|
306
|
-
onClick: handleLogout
|
|
307
|
-
}
|
|
428
|
+
{ id: 'logout', label: 'Logout', danger: true, icon: <LogOut size={16} /> }
|
|
308
429
|
]}
|
|
309
430
|
placement="bottom-end"
|
|
310
431
|
>
|
|
311
|
-
<img src={user.avatar} alt={user.name} />
|
|
432
|
+
<img src={user.avatar} alt={user.name} style={{ width: 32, height: 32, borderRadius: '50%' }} />
|
|
312
433
|
</Dropdown>
|
|
313
434
|
```
|
|
314
435
|
|
|
315
|
-
### Filter
|
|
436
|
+
### Filter Selector
|
|
316
437
|
|
|
317
438
|
```tsx
|
|
318
439
|
const [filter, setFilter] = useState('all');
|
|
@@ -322,100 +443,105 @@ const [filter, setFilter] = useState('all');
|
|
|
322
443
|
{
|
|
323
444
|
id: 'all',
|
|
324
445
|
label: 'All Items',
|
|
325
|
-
icon: filter === 'all' ? <
|
|
446
|
+
icon: filter === 'all' ? <Check size={16} /> : undefined,
|
|
326
447
|
onClick: () => setFilter('all')
|
|
327
448
|
},
|
|
328
449
|
{
|
|
329
450
|
id: 'active',
|
|
330
451
|
label: 'Active Only',
|
|
331
|
-
icon: filter === 'active' ? <
|
|
452
|
+
icon: filter === 'active' ? <Check size={16} /> : undefined,
|
|
332
453
|
onClick: () => setFilter('active')
|
|
333
454
|
},
|
|
334
455
|
{
|
|
335
456
|
id: 'archived',
|
|
336
457
|
label: 'Archived',
|
|
337
|
-
icon: filter === 'archived' ? <
|
|
458
|
+
icon: filter === 'archived' ? <Check size={16} /> : undefined,
|
|
338
459
|
onClick: () => setFilter('archived')
|
|
339
460
|
}
|
|
340
461
|
]}
|
|
462
|
+
placement="bottom-start"
|
|
341
463
|
>
|
|
342
|
-
<
|
|
343
|
-
|
|
344
|
-
Filter: {filter}
|
|
345
|
-
<ChevronDownIcon />
|
|
346
|
-
</button>
|
|
464
|
+
<Filter size={16} />
|
|
465
|
+
{filter}
|
|
347
466
|
</Dropdown>
|
|
348
467
|
```
|
|
349
468
|
|
|
350
|
-
|
|
469
|
+
### Context Menu (Advanced)
|
|
351
470
|
|
|
352
|
-
|
|
471
|
+
See the demo app for a complete example using `useDropdown` with custom card trigger styling.
|
|
353
472
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
## 🧪 Testing
|
|
476
|
+
|
|
477
|
+
All components are fully testable with standard React testing libraries:
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
import { render, screen } from '@testing-library/react';
|
|
481
|
+
import userEvent from '@testing-library/user-event';
|
|
482
|
+
|
|
483
|
+
test('opens dropdown on click', async () => {
|
|
484
|
+
render(
|
|
485
|
+
<Dropdown items={[{ id: 'test', label: 'Test', onClick: jest.fn() }]}>
|
|
486
|
+
Trigger
|
|
487
|
+
</Dropdown>
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const trigger = screen.getByText('Trigger');
|
|
491
|
+
await userEvent.click(trigger);
|
|
492
|
+
|
|
493
|
+
expect(screen.getByText('Test')).toBeInTheDocument();
|
|
494
|
+
});
|
|
358
495
|
```
|
|
359
496
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
- Interactive examples (filters, user selectors)
|
|
364
|
-
- Different sizes and states
|
|
365
|
-
- Mobile optimizations
|
|
366
|
-
- Dark theme support
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## 🌐 Browser Support
|
|
367
500
|
|
|
368
|
-
|
|
501
|
+
| Browser | Version |
|
|
502
|
+
|---------|---------|
|
|
503
|
+
| Chrome | 88+ |
|
|
504
|
+
| Firefox | 78+ |
|
|
505
|
+
| Safari | 14+ |
|
|
506
|
+
| Edge | 88+ |
|
|
369
507
|
|
|
370
|
-
|
|
371
|
-
- Firefox 78+
|
|
372
|
-
- Safari 14+
|
|
373
|
-
- Edge 88+
|
|
508
|
+
---
|
|
374
509
|
|
|
375
|
-
## Contributing
|
|
510
|
+
## 🤝 Contributing
|
|
511
|
+
|
|
512
|
+
Contributions are welcome! Please follow these steps:
|
|
376
513
|
|
|
377
514
|
1. Fork the repository
|
|
378
|
-
2. Create
|
|
379
|
-
3. Commit
|
|
380
|
-
4. Push to
|
|
515
|
+
2. Create a feature branch: `git checkout -b feature/your-feature`
|
|
516
|
+
3. Commit changes: `git commit -m 'Add your feature'`
|
|
517
|
+
4. Push to branch: `git push origin feature/your-feature`
|
|
381
518
|
5. Open a Pull Request
|
|
382
519
|
|
|
383
|
-
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## 📄 License
|
|
384
523
|
|
|
385
524
|
MIT © ASafariM
|
|
386
525
|
|
|
387
|
-
|
|
526
|
+
---
|
|
388
527
|
|
|
389
|
-
|
|
528
|
+
## 🔗 Resources
|
|
390
529
|
|
|
391
|
-
-
|
|
392
|
-
-
|
|
393
|
-
-
|
|
394
|
-
-
|
|
395
|
-
- `danger` - Red destructive button
|
|
396
|
-
- `info` - Cyan info button
|
|
397
|
-
- `ghost` - Transparent ghost button
|
|
398
|
-
- `outline` - Outlined button
|
|
399
|
-
- `link` - Text link style
|
|
400
|
-
- `brand` - Brand-specific color
|
|
530
|
+
- [Live Demo](https://alisafari-it.github.io/react-dropdowns/)
|
|
531
|
+
- [GitHub Repository](https://github.com/AliSafari-IT/react-dropdowns)
|
|
532
|
+
- [npm Package](https://www.npmjs.com/package/@asafarim/react-dropdowns)
|
|
533
|
+
- [ASafariM Design Tokens](https://github.com/AliSafari-IT/design-tokens)
|
|
401
534
|
|
|
402
|
-
|
|
403
|
-
<Dropdown variant="secondary" items={items}>
|
|
404
|
-
<button>Secondary Dropdown</button>
|
|
405
|
-
</Dropdown>
|
|
406
|
-
```
|
|
535
|
+
---
|
|
407
536
|
|
|
408
|
-
##
|
|
537
|
+
## 📋 Changelog
|
|
409
538
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
```tsx
|
|
413
|
-
<Dropdown showChevron={false} items={items}>
|
|
414
|
-
<button>No Chevron</button>
|
|
415
|
-
</Dropdown>
|
|
416
|
-
```
|
|
539
|
+
### 1.8.0
|
|
417
540
|
|
|
418
|
-
|
|
541
|
+
- Added an advanced `useDropdown` demo section with custom trigger, portal rendering, and click-outside handling
|
|
542
|
+
- Documented low-level hook usage with full examples and testing guidance
|
|
543
|
+
- Rewrote README for clearer onboarding (installation, components, customization)
|
|
544
|
+
- Improved demo styles and behavior (auto-close on outside click, refined trigger states)
|
|
419
545
|
|
|
420
546
|
### 1.1.1
|
|
421
547
|
|
|
@@ -424,6 +550,7 @@ The dropdown automatically adds a chevron icon to the trigger button. You can di
|
|
|
424
550
|
- Fixed Vite base path configuration for GitHub Pages deployment
|
|
425
551
|
- Improved demo app layout with grid-based examples
|
|
426
552
|
- Added support for multiple button variants in trigger
|
|
553
|
+
- Added advanced custom dropdown example with `useDropdown` hook
|
|
427
554
|
|
|
428
555
|
### 1.1.0
|
|
429
556
|
|
package/package.json
CHANGED