@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.
Files changed (2) hide show
  1. package/README.md +359 -232
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,101 +1,136 @@
1
1
  # @asafarim/react-dropdowns
2
2
 
3
- Comprehensive, accessible, and mobile-first dropdown components for React with TypeScript support.
3
+ **Production-ready dropdown components for React** with full TypeScript support, accessibility, and mobile optimization. Built on ASafariM design tokens.
4
4
 
5
- ## Features
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
- - 🎯 **Comprehensive**: Multiple components for different use cases
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
- ## Installation
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
- # or
31
+ or
20
32
  yarn add @asafarim/react-dropdowns
21
- # or
22
- pnpm add @asafarim/react-dropdowns
23
33
  ```
24
34
 
25
- ## Quick Start
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
- id: 'edit',
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
- <button>Actions</button>
60
+ Actions
50
61
  </Dropdown>
51
62
  );
52
63
  }
53
64
  ```
54
65
 
55
- ## Components
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
- ### Dropdown
74
+ The main component that combines trigger and menu functionality. Use this for most cases.
58
75
 
59
- The main dropdown component that combines trigger and menu functionality.
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: 'option1',
66
- label: 'Option 1',
67
- icon: <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: 'option2',
75
- label: 'Option 2',
76
- onClick: () => {}
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
- <button>Trigger</button>
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 Item"
97
- icon={<EditIcon />}
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
- Standalone menu component for custom implementations.
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
- <DropdownMenu
110
- isOpen={isOpen}
111
- position={{ top: 100, left: 50 }}
112
- size="md"
113
- >
114
- <DropdownItem label="Option 1" onClick={() => {}} />
115
- <DropdownItem label="Option 2" onClick={() => {}} />
116
- </DropdownMenu>
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
- ## Props
184
+ ---
120
185
 
121
- ### DropdownProps
186
+ ## 🎛️ Props Reference
187
+
188
+ ### Dropdown Props
122
189
 
123
190
  | Prop | Type | Default | Description |
124
191
  |------|------|---------|-------------|
125
- | `children` | `ReactNode` | - | Trigger element |
126
- | `items` | `DropdownItemData[]` | `[]` | Menu items |
127
- | `isOpen` | `boolean` | - | Controlled open state |
128
- | `onToggle` | `(isOpen: boolean) => void` | - | Open state change handler |
129
- | `placement` | `DropdownPlacement` | `'bottom-start'` | Menu position |
130
- | `size` | `DropdownSize` | `'md'` | Menu 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` | Close menu on item select |
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` | - | Unique identifier |
139
- | `label` | `string` | - | Item text |
140
- | `value` | `string` | - | Item value |
141
- | `icon` | `ReactNode` | - | Item icon |
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` | Danger styling |
144
- | `divider` | `boolean` | `false` | Render as divider |
145
- | `onClick` | `(event: MouseEvent) => void` | - | Click handler |
214
+ | `danger` | `boolean` | `false` | Red danger styling |
215
+ | `divider` | `boolean` | `false` | Render as visual separator |
216
+ | `value` | `string` | | Optional data value |
146
217
 
147
- ## Placement Options
218
+ ### DropdownMenu Props
148
219
 
149
- The dropdown supports 12 different placement options:
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
- - `top`, `top-start`, `top-end`
152
- - `bottom`, `bottom-start`, `bottom-end`
153
- - `left`, `left-start`, `left-end`
154
- - `right`, `right-start`, `right-end`
229
+ ---
155
230
 
156
- ## Size Options
231
+ ## 🎨 Customization
157
232
 
158
- Three size variants are available:
233
+ ### Placement Options
159
234
 
160
- - `sm` - Compact size for tight spaces
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
- ## Hooks
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
- ### useDropdown
244
+ ```tsx
245
+ <Dropdown items={items} placement="top-end">
246
+ Menu
247
+ </Dropdown>
248
+ ```
167
249
 
168
- Custom hook for building dropdown functionality:
250
+ ### Size Options
169
251
 
170
252
  ```tsx
171
- import { useDropdown } from '@asafarim/react-dropdowns';
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
- Hook for detecting clicks outside an element:
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: elementRef,
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
- Hook for keyboard navigation support:
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
- ## Styling
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
- ```css
231
- .asm-dropdown-menu {
232
- /* Custom menu styles */
233
- }
373
+ ## ♿ Accessibility
234
374
 
235
- .asm-dropdown-item {
236
- /* Custom item styles */
237
- }
375
+ Built with WCAG 2.1 AA compliance in mind:
238
376
 
239
- .asm-dropdown-item--danger {
240
- /* Custom danger item styles */
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` | Open/close dropdown or select item |
259
- | `Arrow Down` | Navigate to next item or open dropdown |
260
- | `Arrow Up` | Navigate to previous item |
261
- | `Home` | Navigate to first item |
262
- | `End` | Navigate to last item |
263
- | `Escape` | Close dropdown |
264
- | `Tab` | Close dropdown and move to next element |
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
- ## Examples
395
+ ---
267
396
 
268
- ### Basic Menu
397
+ ## 💡 Real-World Examples
398
+
399
+ ### File Menu
269
400
 
270
401
  ```tsx
271
402
  <Dropdown
272
403
  items={[
273
- { id: 'new', label: 'New', onClick: () => {} },
274
- { id: 'edit', label: 'Edit', onClick: () => {} },
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: 'delete', label: 'Delete', danger: true, onClick: () => {} }
410
+ { id: 'exit', label: 'Exit', danger: true, icon: <X size={16} /> }
277
411
  ]}
412
+ placement="bottom-start"
278
413
  >
279
- <button>File</button>
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
- id: 'profile',
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 Dropdown
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' ? <CheckIcon /> : undefined,
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' ? <CheckIcon /> : undefined,
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' ? <CheckIcon /> : undefined,
458
+ icon: filter === 'archived' ? <Check size={16} /> : undefined,
338
459
  onClick: () => setFilter('archived')
339
460
  }
340
461
  ]}
462
+ placement="bottom-start"
341
463
  >
342
- <button>
343
- <FilterIcon />
344
- Filter: {filter}
345
- <ChevronDownIcon />
346
- </button>
464
+ <Filter size={16} />
465
+ {filter}
347
466
  </Dropdown>
348
467
  ```
349
468
 
350
- ## Demo
469
+ ### Context Menu (Advanced)
351
470
 
352
- Run the demo application to see all features in action:
471
+ See the demo app for a complete example using `useDropdown` with custom card trigger styling.
353
472
 
354
- ```bash
355
- cd packages/react-dropdowns/demo
356
- npm install
357
- npm start
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
- The demo showcases:
361
- - Basic usage examples
362
- - All placement options
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
- ## Browser Support
501
+ | Browser | Version |
502
+ |---------|---------|
503
+ | Chrome | 88+ |
504
+ | Firefox | 78+ |
505
+ | Safari | 14+ |
506
+ | Edge | 88+ |
369
507
 
370
- - Chrome 88+
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 your feature branch (`git checkout -b feature/amazing-feature`)
379
- 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
380
- 4. Push to the branch (`git push origin feature/amazing-feature`)
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
- ## License
520
+ ---
521
+
522
+ ## 📄 License
384
523
 
385
524
  MIT © ASafariM
386
525
 
387
- ## Button Variants
526
+ ---
388
527
 
389
- The dropdown trigger supports multiple button style variants:
528
+ ## 🔗 Resources
390
529
 
391
- - `primary` - Default primary button style
392
- - `secondary` - Secondary button with border
393
- - `success` - Green success button
394
- - `warning` - Orange warning button
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
- ```tsx
403
- <Dropdown variant="secondary" items={items}>
404
- <button>Secondary Dropdown</button>
405
- </Dropdown>
406
- ```
535
+ ---
407
536
 
408
- ## Chevron Icon
537
+ ## 📋 Changelog
409
538
 
410
- The dropdown automatically adds a chevron icon to the trigger button. You can disable it with the `showChevron` prop:
411
-
412
- ```tsx
413
- <Dropdown showChevron={false} items={items}>
414
- <button>No Chevron</button>
415
- </Dropdown>
416
- ```
539
+ ### 1.8.0
417
540
 
418
- ## Changelog
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asafarim/react-dropdowns",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "description": "Comprehensive reusable dropdown components for React with TypeScript and mobile-first design",
6
6
  "main": "dist/index.js",