react-wheel-select 0.0.1
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/LICENSE +21 -0
- package/README.md +575 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +2 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +241 -0
- package/dist/index.d.ts +241 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +391 -0
- package/package.json +93 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vasil Rashkov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
# react-wheel-select
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
**A beautiful, accessible iOS-style wheel picker component for React**
|
|
11
|
+
|
|
12
|
+
[Live Demo](https://react-wheel-select.devapollo.com) ยท [Documentation](#-documentation) ยท [Examples](#-examples) ยท [Contributing](#-contributing)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## โจ Features
|
|
19
|
+
|
|
20
|
+
- ๐ก **Smooth Wheel Scrolling** โ CSS scroll-snap powered picker with momentum scrolling
|
|
21
|
+
- โฟ **Fully Accessible** โ ARIA compliant with full keyboard navigation
|
|
22
|
+
- ๐จ **Highly Customizable** โ 30+ CSS variables for complete visual control
|
|
23
|
+
- ๐ฑ **Touch Optimized** โ Works beautifully on mobile devices
|
|
24
|
+
- ๐ง **TypeScript First** โ Complete type definitions with generics support
|
|
25
|
+
- ๐ชถ **Lightweight** โ ~4KB minified + gzipped, zero dependencies
|
|
26
|
+
- ๐ **Theme Support** โ Built-in dark/light modes with auto-detection
|
|
27
|
+
- ๐ฏ **Render Props** โ Custom trigger and option rendering
|
|
28
|
+
- ๐ **Form Compatible** โ Hidden native select for form submission
|
|
29
|
+
- ๐ **Controlled & Uncontrolled** โ Works both ways with ref API
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## ๐ฆ Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# npm
|
|
37
|
+
npm install react-wheel-select
|
|
38
|
+
|
|
39
|
+
# yarn
|
|
40
|
+
yarn add react-wheel-select
|
|
41
|
+
|
|
42
|
+
# pnpm
|
|
43
|
+
pnpm add react-wheel-select
|
|
44
|
+
|
|
45
|
+
# bun
|
|
46
|
+
bun add react-wheel-select
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## ๐ Quick Start
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { useState } from 'react'
|
|
55
|
+
import { WheelSelect } from 'react-wheel-select'
|
|
56
|
+
import 'react-wheel-select/styles.css'
|
|
57
|
+
|
|
58
|
+
const fruits = [
|
|
59
|
+
{ value: 'apple', label: 'Apple' },
|
|
60
|
+
{ value: 'banana', label: 'Banana' },
|
|
61
|
+
{ value: 'cherry', label: 'Cherry' },
|
|
62
|
+
{ value: 'date', label: 'Date' },
|
|
63
|
+
{ value: 'elderberry', label: 'Elderberry' },
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
function App() {
|
|
67
|
+
const [fruit, setFruit] = useState('apple')
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<p>
|
|
71
|
+
I love to eat{' '}
|
|
72
|
+
<WheelSelect
|
|
73
|
+
options={fruits}
|
|
74
|
+
value={fruit}
|
|
75
|
+
onChange={setFruit}
|
|
76
|
+
/>
|
|
77
|
+
</p>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## ๐ Documentation
|
|
85
|
+
|
|
86
|
+
### Table of Contents
|
|
87
|
+
|
|
88
|
+
- [Props Reference](#props-reference)
|
|
89
|
+
- [Theme Configuration](#theme-configuration)
|
|
90
|
+
- [Sizing Configuration](#sizing-configuration)
|
|
91
|
+
- [Behavior Configuration](#behavior-configuration)
|
|
92
|
+
- [Custom Icons](#custom-icons)
|
|
93
|
+
- [Accessibility](#accessibility)
|
|
94
|
+
- [Event Callbacks](#event-callbacks)
|
|
95
|
+
- [Imperative API](#imperative-api)
|
|
96
|
+
- [CSS Customization](#css-customization)
|
|
97
|
+
- [TypeScript](#typescript)
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### Props Reference
|
|
102
|
+
|
|
103
|
+
| Prop | Type | Default | Description |
|
|
104
|
+
|------|------|---------|-------------|
|
|
105
|
+
| `options` | `WheelSelectOption[]` | **required** | Array of options to display |
|
|
106
|
+
| `value` | `string` | **required** | Currently selected value |
|
|
107
|
+
| `onChange` | `(value: string) => void` | **required** | Called when selection changes |
|
|
108
|
+
| `placeholder` | `string` | `'Select...'` | Placeholder when no value |
|
|
109
|
+
| `disabled` | `boolean` | `false` | Disable the component |
|
|
110
|
+
| `required` | `boolean` | `false` | Mark as required for forms |
|
|
111
|
+
| `name` | `string` | โ | Form field name |
|
|
112
|
+
| `id` | `string` | โ | Element ID |
|
|
113
|
+
| `className` | `string` | โ | Additional CSS class |
|
|
114
|
+
| `style` | `CSSProperties` | โ | Inline styles |
|
|
115
|
+
| `theme` | `WheelSelectTheme` | โ | Theme configuration |
|
|
116
|
+
| `sizing` | `WheelSelectSizing` | โ | Sizing configuration |
|
|
117
|
+
| `behavior` | `WheelSelectBehavior` | โ | Behavior configuration |
|
|
118
|
+
| `icons` | `WheelSelectIcons` | โ | Custom icons |
|
|
119
|
+
| `a11y` | `WheelSelectA11y` | โ | Accessibility options |
|
|
120
|
+
| `callbacks` | `WheelSelectCallbacks` | โ | Event callbacks |
|
|
121
|
+
| `renderTrigger` | `Function` | โ | Custom trigger renderer |
|
|
122
|
+
| `renderOption` | `Function` | โ | Custom option renderer |
|
|
123
|
+
| `zIndex` | `number` | `10001` | Overlay z-index |
|
|
124
|
+
|
|
125
|
+
#### Option Shape
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
interface WheelSelectOption<T extends string = string> {
|
|
129
|
+
value: T // Unique identifier
|
|
130
|
+
label: string // Display text
|
|
131
|
+
disabled?: boolean // Disable this option
|
|
132
|
+
data?: Record<string, unknown> // Custom data
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### Theme Configuration
|
|
139
|
+
|
|
140
|
+
Customize colors, typography, animations, and spacing:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
<WheelSelect
|
|
144
|
+
theme={{
|
|
145
|
+
// Color scheme
|
|
146
|
+
colorScheme: 'dark', // 'dark' | 'light' | 'auto'
|
|
147
|
+
|
|
148
|
+
// Custom colors
|
|
149
|
+
colors: {
|
|
150
|
+
text: '#ffffff',
|
|
151
|
+
textMuted: '#888888',
|
|
152
|
+
activeBg: 'rgba(255, 255, 255, 0.15)',
|
|
153
|
+
hoverBg: 'rgba(255, 255, 255, 0.1)',
|
|
154
|
+
backdropBg: 'rgba(0, 0, 0, 0.6)',
|
|
155
|
+
focusRing: 'rgba(59, 130, 246, 0.5)',
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// Border radius
|
|
159
|
+
borderRadius: 16,
|
|
160
|
+
|
|
161
|
+
// Typography
|
|
162
|
+
font: {
|
|
163
|
+
family: 'Inter, sans-serif',
|
|
164
|
+
size: 24,
|
|
165
|
+
weight: 600,
|
|
166
|
+
triggerSize: 18,
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// Animations
|
|
170
|
+
animation: {
|
|
171
|
+
duration: 250,
|
|
172
|
+
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
173
|
+
disabled: false, // Set true to disable all animations
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
// Spacing
|
|
177
|
+
spacing: {
|
|
178
|
+
triggerGap: 12,
|
|
179
|
+
triggerPadding: '10px 20px',
|
|
180
|
+
optionGap: 12,
|
|
181
|
+
optionPadding: '0 24px',
|
|
182
|
+
},
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### Sizing Configuration
|
|
190
|
+
|
|
191
|
+
Control dimensions of the wheel and options:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
<WheelSelect
|
|
195
|
+
sizing={{
|
|
196
|
+
wheelHeight: 400, // Height of the scroll container
|
|
197
|
+
wheelMinWidth: 280, // Minimum width
|
|
198
|
+
optionHeight: 64, // Height of each option
|
|
199
|
+
iconSize: 24, // Size of icons
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### Behavior Configuration
|
|
207
|
+
|
|
208
|
+
Fine-tune interaction behavior:
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
<WheelSelect
|
|
212
|
+
behavior={{
|
|
213
|
+
closeOnOutsideClick: true, // Close when clicking backdrop
|
|
214
|
+
closeOnEscape: true, // Close on Escape key
|
|
215
|
+
closeOnSelect: true, // Close after selection
|
|
216
|
+
scrollDebounceMs: 50, // Scroll detection delay
|
|
217
|
+
keyboardNavigation: true, // Enable keyboard nav
|
|
218
|
+
focusTriggerOnClose: true, // Return focus after close
|
|
219
|
+
portalTarget: document.body, // Portal mount point
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### Custom Icons
|
|
227
|
+
|
|
228
|
+
Replace default icons with your own:
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
import { ChevronDown, ArrowLeft } from 'lucide-react'
|
|
232
|
+
|
|
233
|
+
<WheelSelect
|
|
234
|
+
icons={{
|
|
235
|
+
chevron: <ChevronDown size={20} />,
|
|
236
|
+
arrow: <ArrowLeft size={20} />,
|
|
237
|
+
hideChevron: false, // Hide chevron icon
|
|
238
|
+
hideArrow: false, // Hide arrow on active item
|
|
239
|
+
}}
|
|
240
|
+
/>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### Accessibility
|
|
246
|
+
|
|
247
|
+
Full ARIA support with customizable labels:
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
<WheelSelect
|
|
251
|
+
a11y={{
|
|
252
|
+
triggerLabel: 'Select a fruit',
|
|
253
|
+
pickerLabel: 'Fruit options',
|
|
254
|
+
describedBy: 'fruit-helper-text',
|
|
255
|
+
}}
|
|
256
|
+
/>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Keyboard Support:**
|
|
260
|
+
|
|
261
|
+
| Key | Action |
|
|
262
|
+
|-----|--------|
|
|
263
|
+
| `Enter` / `Space` | Open picker / Select option |
|
|
264
|
+
| `Escape` | Close picker |
|
|
265
|
+
| `โ` / `โ` | Navigate options |
|
|
266
|
+
| `Home` | Jump to first option |
|
|
267
|
+
| `End` | Jump to last option |
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### Event Callbacks
|
|
272
|
+
|
|
273
|
+
Subscribe to component events:
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
<WheelSelect
|
|
277
|
+
callbacks={{
|
|
278
|
+
onOpen: () => console.log('Picker opened'),
|
|
279
|
+
onClose: () => console.log('Picker closed'),
|
|
280
|
+
onChange: (value, option) => {
|
|
281
|
+
console.log('Selected:', value, option)
|
|
282
|
+
},
|
|
283
|
+
onActiveChange: (index, option) => {
|
|
284
|
+
console.log('Highlighted:', index, option)
|
|
285
|
+
},
|
|
286
|
+
onKeyDown: (event) => {
|
|
287
|
+
console.log('Key pressed:', event.key)
|
|
288
|
+
},
|
|
289
|
+
}}
|
|
290
|
+
/>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
### Imperative API
|
|
296
|
+
|
|
297
|
+
Control the component programmatically using refs:
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
import { useRef } from 'react'
|
|
301
|
+
import { WheelSelect, WheelSelectRef } from 'react-wheel-select'
|
|
302
|
+
|
|
303
|
+
function App() {
|
|
304
|
+
const selectRef = useRef<WheelSelectRef>(null)
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<>
|
|
308
|
+
<WheelSelect ref={selectRef} {...props} />
|
|
309
|
+
|
|
310
|
+
<button onClick={() => selectRef.current?.open()}>
|
|
311
|
+
Open Picker
|
|
312
|
+
</button>
|
|
313
|
+
|
|
314
|
+
<button onClick={() => selectRef.current?.close()}>
|
|
315
|
+
Close Picker
|
|
316
|
+
</button>
|
|
317
|
+
|
|
318
|
+
<button onClick={() => selectRef.current?.scrollToIndex(5)}>
|
|
319
|
+
Scroll to Item 5
|
|
320
|
+
</button>
|
|
321
|
+
</>
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Ref Methods:**
|
|
327
|
+
|
|
328
|
+
| Method | Description |
|
|
329
|
+
|--------|-------------|
|
|
330
|
+
| `open()` | Open the picker |
|
|
331
|
+
| `close()` | Close the picker |
|
|
332
|
+
| `toggle()` | Toggle open state |
|
|
333
|
+
| `focus()` | Focus the trigger |
|
|
334
|
+
| `isOpen()` | Get current open state |
|
|
335
|
+
| `scrollToIndex(n)` | Scroll to specific index |
|
|
336
|
+
| `getNativeSelect()` | Get native select element |
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
### CSS Customization
|
|
341
|
+
|
|
342
|
+
#### Using CSS Variables
|
|
343
|
+
|
|
344
|
+
Override any variable at the root or component level:
|
|
345
|
+
|
|
346
|
+
```css
|
|
347
|
+
/* Global overrides */
|
|
348
|
+
:root {
|
|
349
|
+
--ws-color-active-bg: rgba(59, 130, 246, 0.2);
|
|
350
|
+
--ws-border-radius: 8px;
|
|
351
|
+
--ws-font-size: 20px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* Scoped overrides */
|
|
355
|
+
.my-custom-select {
|
|
356
|
+
--ws-color-text: #1a1a1a;
|
|
357
|
+
--ws-animation-duration: 300ms;
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
#### Available CSS Variables
|
|
362
|
+
|
|
363
|
+
```css
|
|
364
|
+
/* Colors */
|
|
365
|
+
--ws-color-text /* Text color */
|
|
366
|
+
--ws-color-text-muted /* Muted text color */
|
|
367
|
+
--ws-color-active-bg /* Active item background */
|
|
368
|
+
--ws-color-hover-bg /* Hover state background */
|
|
369
|
+
--ws-color-backdrop-bg /* Backdrop overlay color */
|
|
370
|
+
--ws-color-focus-ring /* Focus ring color */
|
|
371
|
+
|
|
372
|
+
/* Typography */
|
|
373
|
+
--ws-font-family /* Font family */
|
|
374
|
+
--ws-font-size /* Option font size */
|
|
375
|
+
--ws-font-weight /* Option font weight */
|
|
376
|
+
--ws-font-size-trigger /* Trigger font size */
|
|
377
|
+
|
|
378
|
+
/* Spacing */
|
|
379
|
+
--ws-border-radius /* Border radius */
|
|
380
|
+
--ws-trigger-gap /* Gap in trigger */
|
|
381
|
+
--ws-trigger-padding /* Trigger padding */
|
|
382
|
+
--ws-option-gap /* Gap in options */
|
|
383
|
+
--ws-option-padding /* Option padding */
|
|
384
|
+
|
|
385
|
+
/* Sizing */
|
|
386
|
+
--ws-wheel-height /* Wheel viewport height */
|
|
387
|
+
--ws-wheel-min-width /* Minimum wheel width */
|
|
388
|
+
--ws-option-height /* Option item height */
|
|
389
|
+
--ws-icon-size /* Icon dimensions */
|
|
390
|
+
--ws-spacer-height /* Top/bottom spacer */
|
|
391
|
+
|
|
392
|
+
/* Animation */
|
|
393
|
+
--ws-animation-duration /* Transition duration */
|
|
394
|
+
--ws-animation-easing /* Easing function */
|
|
395
|
+
|
|
396
|
+
/* Internal */
|
|
397
|
+
--ws-inactive-opacity /* Inactive items opacity */
|
|
398
|
+
--ws-hover-opacity /* Hover state opacity */
|
|
399
|
+
--ws-backdrop-blur /* Backdrop blur amount */
|
|
400
|
+
--ws-z-index /* Overlay z-index */
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
#### Custom Class Names
|
|
404
|
+
|
|
405
|
+
Target specific elements:
|
|
406
|
+
|
|
407
|
+
```css
|
|
408
|
+
.ws-root { } /* Root container */
|
|
409
|
+
.ws-trigger { } /* Trigger button */
|
|
410
|
+
.ws-trigger.ws-open { } /* Trigger when open */
|
|
411
|
+
.ws-trigger-text { } /* Trigger text */
|
|
412
|
+
.ws-chevron { } /* Chevron icon */
|
|
413
|
+
.ws-backdrop { } /* Fullscreen backdrop */
|
|
414
|
+
.ws-picker { } /* Picker container */
|
|
415
|
+
.ws-wheel { } /* Scrollable wheel */
|
|
416
|
+
.ws-spacer { } /* Top/bottom spacers */
|
|
417
|
+
.ws-option { } /* Option item */
|
|
418
|
+
.ws-option.ws-active { } /* Active/centered option */
|
|
419
|
+
.ws-option.ws-disabled { } /* Disabled option */
|
|
420
|
+
.ws-option-text { } /* Option label text */
|
|
421
|
+
.ws-arrow { } /* Active item arrow */
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
### TypeScript
|
|
427
|
+
|
|
428
|
+
Full generic support for type-safe values:
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
// Define your value type
|
|
432
|
+
type Fruit = 'apple' | 'banana' | 'cherry'
|
|
433
|
+
|
|
434
|
+
// Options with typed values
|
|
435
|
+
const options: WheelSelectOption<Fruit>[] = [
|
|
436
|
+
{ value: 'apple', label: 'Apple' },
|
|
437
|
+
{ value: 'banana', label: 'Banana' },
|
|
438
|
+
{ value: 'cherry', label: 'Cherry' },
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
// Component with type inference
|
|
442
|
+
function App() {
|
|
443
|
+
const [fruit, setFruit] = useState<Fruit>('apple')
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<WheelSelect<Fruit>
|
|
447
|
+
options={options}
|
|
448
|
+
value={fruit}
|
|
449
|
+
onChange={setFruit} // Type-safe!
|
|
450
|
+
/>
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## ๐จ Examples
|
|
458
|
+
|
|
459
|
+
### Inline Text Integration
|
|
460
|
+
|
|
461
|
+
```tsx
|
|
462
|
+
<p className="sentence">
|
|
463
|
+
I want to{' '}
|
|
464
|
+
<WheelSelect
|
|
465
|
+
options={actions}
|
|
466
|
+
value={action}
|
|
467
|
+
onChange={setAction}
|
|
468
|
+
theme={{ font: { triggerSize: 'inherit' } }}
|
|
469
|
+
/>
|
|
470
|
+
{' '}with my team.
|
|
471
|
+
</p>
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Custom Styled Trigger
|
|
475
|
+
|
|
476
|
+
```tsx
|
|
477
|
+
<WheelSelect
|
|
478
|
+
renderTrigger={({ label, isOpen, onClick }) => (
|
|
479
|
+
<button
|
|
480
|
+
onClick={onClick}
|
|
481
|
+
className={`custom-trigger ${isOpen ? 'active' : ''}`}
|
|
482
|
+
>
|
|
483
|
+
{label}
|
|
484
|
+
<ChevronIcon />
|
|
485
|
+
</button>
|
|
486
|
+
)}
|
|
487
|
+
/>
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Custom Option Rendering
|
|
491
|
+
|
|
492
|
+
```tsx
|
|
493
|
+
<WheelSelect
|
|
494
|
+
renderOption={({ option, isActive, isSelected }) => (
|
|
495
|
+
<div className="custom-option">
|
|
496
|
+
<img src={option.data?.icon} alt="" />
|
|
497
|
+
<span>{option.label}</span>
|
|
498
|
+
{isSelected && <CheckIcon />}
|
|
499
|
+
</div>
|
|
500
|
+
)}
|
|
501
|
+
/>
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### With Form Integration
|
|
505
|
+
|
|
506
|
+
```tsx
|
|
507
|
+
<form onSubmit={handleSubmit}>
|
|
508
|
+
<WheelSelect
|
|
509
|
+
name="country"
|
|
510
|
+
required
|
|
511
|
+
options={countries}
|
|
512
|
+
value={country}
|
|
513
|
+
onChange={setCountry}
|
|
514
|
+
/>
|
|
515
|
+
<button type="submit">Submit</button>
|
|
516
|
+
</form>
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Disabled Options
|
|
520
|
+
|
|
521
|
+
```tsx
|
|
522
|
+
const options = [
|
|
523
|
+
{ value: 'free', label: 'Free Plan' },
|
|
524
|
+
{ value: 'pro', label: 'Pro Plan' },
|
|
525
|
+
{ value: 'enterprise', label: 'Enterprise', disabled: true },
|
|
526
|
+
]
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
## ๐ Browser Support
|
|
532
|
+
|
|
533
|
+
- Chrome 88+
|
|
534
|
+
- Firefox 84+
|
|
535
|
+
- Safari 14+
|
|
536
|
+
- Edge 88+
|
|
537
|
+
|
|
538
|
+
Requires CSS `scroll-snap-type` and `backdrop-filter` support.
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
## ๐ License
|
|
543
|
+
|
|
544
|
+
MIT ยฉ [Vasil Rashkov](https://github.com/vasilrashkov)
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## ๐ค Contributing
|
|
549
|
+
|
|
550
|
+
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
551
|
+
|
|
552
|
+
1. Fork the repository
|
|
553
|
+
2. Create your feature branch (`git checkout -b feature/amazing`)
|
|
554
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
555
|
+
4. Push to the branch (`git push origin feature/amazing`)
|
|
556
|
+
5. Open a Pull Request
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## ๐ Support
|
|
561
|
+
|
|
562
|
+
If you find this project useful, please consider:
|
|
563
|
+
|
|
564
|
+
- โญ Starring the repository
|
|
565
|
+
- ๐ Reporting bugs
|
|
566
|
+
- ๐ก Suggesting features
|
|
567
|
+
- ๐ Improving documentation
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
<div align="center">
|
|
572
|
+
|
|
573
|
+
Made with โค๏ธ for the React community
|
|
574
|
+
|
|
575
|
+
</div>
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
'use strict';Object.defineProperty(exports,'__esModule',{value:true});var react=require('react'),reactDom=require('react-dom'),jsxRuntime=require('react/jsx-runtime');var Ee={colorScheme:"dark",colors:{text:"inherit",textMuted:"inherit",activeBg:"rgba(255, 255, 255, 0.12)",hoverBg:"rgba(255, 255, 255, 0.12)",backdropBg:"rgba(0, 0, 0, 0.5)",focusRing:"rgba(255, 255, 255, 0.5)"},borderRadius:12,font:{family:"inherit",size:28,weight:500,triggerSize:"inherit"},animation:{duration:200,easing:"ease",disabled:false},spacing:{triggerGap:16,triggerPadding:"8px 16px",optionGap:16,optionPadding:"0 20px"}},Oe={wheelHeight:320,wheelMinWidth:220,optionHeight:56,iconSize:20},We={closeOnOutsideClick:true,closeOnEscape:true,closeOnSelect:true,scrollDebounceMs:50,keyboardNavigation:true,portalTarget:null,focusTriggerOnClose:true},Pe=({size:i=20,className:a})=>jsxRuntime.jsxs("svg",{width:i,height:i,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:a,"aria-hidden":"true",children:[jsxRuntime.jsx("polyline",{points:"8 9 12 5 16 9"}),jsxRuntime.jsx("polyline",{points:"8 15 12 19 16 15"})]}),He=({size:i=20,className:a})=>jsxRuntime.jsxs("svg",{width:i,height:i,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:a,"aria-hidden":"true",children:[jsxRuntime.jsx("line",{x1:"19",y1:"12",x2:"5",y2:"12"}),jsxRuntime.jsx("polyline",{points:"12 19 5 12 12 5"})]}),D=(i,a="0")=>i===void 0?a:typeof i=="number"?`${i}px`:i,le=(i,a)=>{let r={...i};for(let c in a)a[c]!==void 0&&(typeof a[c]=="object"&&a[c]!==null&&!Array.isArray(a[c])?r[c]=le(i[c],a[c]):r[c]=a[c]);return r};function Me(i,a){let{options:r,value:c,onChange:q,placeholder:oe="Select...",disabled:V=false,required:R=false,name:te,id:v,className:A="",style:L,theme:j,sizing:H,behavior:M,icons:E,a11y:U,callbacks:k,renderTrigger:ie,renderOption:z,zIndex:re=10001}=i,s=react.useMemo(()=>le(Ee,j||{}),[j]),u=react.useMemo(()=>le(Oe,H||{}),[H]),g=react.useMemo(()=>le(We,M||{}),[M]),[b,x]=react.useState(false),[Y,O]=react.useState(()=>Math.max(0,r.findIndex(t=>t.value===c))),[W,ce]=react.useState(null),B=react.useRef(null),J=react.useRef(null),$=react.useRef(null),y=react.useRef([]),e=react.useRef(null),n=react.useRef(null),d=react.useRef(false),S=react.useMemo(()=>r.find(t=>t.value===c),[r,c]),T=S?.label??oe;react.useEffect(()=>{let t=r.findIndex(o=>o.value===c);t!==-1&&O(t);},[c,r]),react.useEffect(()=>()=>{n.current&&clearTimeout(n.current);},[]);let Q=react.useMemo(()=>{let t=typeof u.wheelHeight=="number"?u.wheelHeight:320,o=typeof u.optionHeight=="number"?u.optionHeight:56;return t/2-o/2},[u.wheelHeight,u.optionHeight]),P=react.useCallback(()=>{if(V)return;B.current&&ce(B.current.getBoundingClientRect());let t=r.findIndex(l=>l.value===c),o=t!==-1?t:0;e.current=o,O(o),x(true),k?.onOpen?.();},[V,r,c,k]),I=react.useCallback(()=>{x(false),g.focusTriggerOnClose&&B.current?.focus(),k?.onClose?.();},[g.focusTriggerOnClose,k]),X=react.useCallback(t=>{let o=r.find(l=>l.value===t);!o||o.disabled||(q(t),k?.onChange?.(t,o),J.current&&(J.current.value=t),g.closeOnSelect&&(x(false),g.focusTriggerOnClose&&B.current?.focus(),k?.onClose?.()));},[q,r,g.closeOnSelect,g.focusTriggerOnClose,k]);react.useEffect(()=>{if(b&&e.current!==null){let t=e.current;e.current=null,requestAnimationFrame(()=>{let o=$.current,l=y.current[t];if(o&&l){let f=o.clientHeight,h=l.clientHeight,Z=l.offsetTop-f/2+h/2;o.scrollTop=Z;}});}},[b]),react.useEffect(()=>{b&&$.current&&$.current.focus();},[b]);let ne=react.useCallback(()=>{let t=$.current;if(!t)return;let o=t.getBoundingClientRect(),l=o.top+o.height/2,f=0,h=1/0;y.current.forEach((Z,Te)=>{if(!Z)return;let be=Z.getBoundingClientRect(),Ce=be.top+be.height/2,fe=Math.abs(Ce-l);fe<h&&(h=fe,f=Te);}),O(f);let p=r[f];p&&k?.onActiveChange?.(f,p);},[r,k]),ve=react.useCallback(()=>{d.current=true,n.current&&clearTimeout(n.current),n.current=setTimeout(()=>{d.current=false,ne();},g.scrollDebounceMs);},[ne,g.scrollDebounceMs]),we=react.useCallback(t=>{if(g.keyboardNavigation)switch(k?.onKeyDown?.(t),t.key){case "Escape":g.closeOnEscape&&(t.preventDefault(),I());break;case "ArrowUp":t.preventDefault(),O(h=>{let p=h-1;for(;p>=0&&r[p]?.disabled;)p--;return p<0?h:(y.current[p]?.scrollIntoView({block:"center",behavior:"smooth"}),p)});break;case "ArrowDown":t.preventDefault(),O(h=>{let p=h+1;for(;p<r.length&&r[p]?.disabled;)p++;return p>=r.length?h:(y.current[p]?.scrollIntoView({block:"center",behavior:"smooth"}),p)});break;case "Enter":case " ":t.preventDefault();let o=r[Y];o&&!o.disabled&&X(o.value);break;case "Home":t.preventDefault();let l=r.findIndex(h=>!h.disabled);l!==-1&&(O(l),y.current[l]?.scrollIntoView({block:"center",behavior:"smooth"}));break;case "End":t.preventDefault();let f=r.findLastIndex(h=>!h.disabled);f!==-1&&(O(f),y.current[f]?.scrollIntoView({block:"center",behavior:"smooth"}));break}},[g.keyboardNavigation,g.closeOnEscape,r,Y,I,X,k]),ue=react.useCallback(t=>o=>{o.preventDefault(),o.stopPropagation(),n.current&&(clearTimeout(n.current),n.current=null);let l=r[t];l&&!l.disabled&&X(l.value);},[r,X]),ke=react.useCallback(t=>{g.closeOnOutsideClick&&t.target===t.currentTarget&&I();},[g.closeOnOutsideClick,I]),xe=react.useCallback(t=>{q(t.target.value);},[q]),ge=react.useCallback(t=>{y.current[t]?.scrollIntoView({block:"center",behavior:"smooth"});},[]);react.useImperativeHandle(a,()=>({open:P,close:I,toggle:()=>b?I():P(),focus:()=>B.current?.focus(),isOpen:()=>b,scrollToIndex:ge,getNativeSelect:()=>J.current}),[b,P,I,ge]);let he=react.useMemo(()=>({"--ws-color-text":s.colors.text,"--ws-color-text-muted":s.colors.textMuted,"--ws-color-active-bg":s.colors.activeBg,"--ws-color-hover-bg":s.colors.hoverBg,"--ws-color-backdrop-bg":s.colors.backdropBg,"--ws-color-focus-ring":s.colors.focusRing,"--ws-border-radius":D(s.borderRadius),"--ws-font-family":s.font.family,"--ws-font-size":D(s.font.size),"--ws-font-weight":String(s.font.weight),"--ws-font-size-trigger":D(s.font.triggerSize),"--ws-animation-duration":s.animation.disabled?"0ms":`${s.animation.duration}ms`,"--ws-animation-easing":s.animation.easing,"--ws-trigger-gap":D(s.spacing.triggerGap),"--ws-trigger-padding":s.spacing.triggerPadding,"--ws-option-gap":D(s.spacing.optionGap),"--ws-option-padding":s.spacing.optionPadding,"--ws-wheel-height":D(u.wheelHeight),"--ws-wheel-min-width":D(u.wheelMinWidth),"--ws-option-height":D(u.optionHeight),"--ws-icon-size":`${u.iconSize}px`,"--ws-spacer-height":`${Q}px`,"--ws-z-index":String(re)}),[s,u,Q,re]),ye=s.colorScheme==="auto"?"":s.colorScheme==="light"?"ws-light":"ws-dark",Se=()=>{if(!b||!W)return null;let t=g.portalTarget??document.body;return reactDom.createPortal(jsxRuntime.jsx("div",{className:"ws-backdrop",onClick:ke,role:"presentation",style:he,children:jsxRuntime.jsx("div",{className:"ws-picker",style:{left:W.left,top:W.top+W.height/2},role:"dialog","aria-modal":"true","aria-label":U?.pickerLabel??"Select an option",children:jsxRuntime.jsxs("div",{ref:$,className:"ws-wheel",role:"listbox",tabIndex:0,onKeyDown:we,onScroll:ve,"aria-activedescendant":`ws-option-${v??"default"}-${Y}`,"aria-describedby":U?.describedBy,children:[jsxRuntime.jsx("div",{className:"ws-spacer","aria-hidden":"true"}),r.map((o,l)=>{let f=l===Y,h=o.value===c;return z?jsxRuntime.jsx("div",{ref:p=>{y.current[l]=p;},id:`ws-option-${v??"default"}-${l}`,role:"option","aria-selected":h,"aria-disabled":o.disabled,className:`ws-option ${f?"ws-active":""} ${o.disabled?"ws-disabled":""}`,onClick:ue(l),children:z({option:o,index:l,isActive:f,isSelected:h})},o.value):jsxRuntime.jsxs("div",{ref:p=>{y.current[l]=p;},id:`ws-option-${v??"default"}-${l}`,role:"option","aria-selected":h,"aria-disabled":o.disabled,className:`ws-option ${f?"ws-active":""} ${o.disabled?"ws-disabled":""}`,onClick:ue(l),children:[jsxRuntime.jsx("span",{className:"ws-option-text",children:o.label}),f&&!E?.hideArrow&&(E?.arrow??jsxRuntime.jsx(He,{size:u.iconSize,className:"ws-arrow"}))]},o.value)}),jsxRuntime.jsx("div",{className:"ws-spacer","aria-hidden":"true"})]})})}),t)};return jsxRuntime.jsxs("span",{className:`ws-root ${ye} ${A}`,style:{...he,...L},children:[jsxRuntime.jsxs("select",{ref:J,name:te,id:v?`${v}-native`:void 0,value:c,onChange:xe,className:"ws-native-select",tabIndex:-1,"aria-hidden":"true",required:R,disabled:V,children:[!S&&jsxRuntime.jsx("option",{value:"",children:oe}),r.map(t=>jsxRuntime.jsx("option",{value:t.value,disabled:t.disabled,children:t.label},t.value))]}),ie?ie({value:c,label:T,isOpen:b,disabled:V,onClick:P}):jsxRuntime.jsxs("button",{ref:B,type:"button",id:v,className:`ws-trigger ${b?"ws-open":""}`,onClick:P,disabled:V,"aria-haspopup":"listbox","aria-expanded":b,"aria-label":U?.triggerLabel,"aria-describedby":U?.describedBy,style:{visibility:b?"hidden":"visible"},children:[jsxRuntime.jsx("span",{className:"ws-trigger-text",children:T}),!E?.hideChevron&&(E?.chevron??jsxRuntime.jsx(Pe,{size:u.iconSize,className:"ws-chevron"}))]}),Se()]})}var me=react.forwardRef(Me),Be=me;var Ae=()=>jsxRuntime.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:"base-select-chevron",children:[jsxRuntime.jsx("polyline",{points:"8 9 12 5 16 9"}),jsxRuntime.jsx("polyline",{points:"8 15 12 19 16 15"})]}),Le=()=>jsxRuntime.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:"base-select-arrow",children:[jsxRuntime.jsx("line",{x1:"19",y1:"12",x2:"5",y2:"12"}),jsxRuntime.jsx("polyline",{points:"12 19 5 12 12 5"})]});function ze({options:i,value:a,onChange:r,name:c,id:q,className:oe=""}){let[R,te]=react.useState(false),[v,A]=react.useState(()=>Math.max(0,i.findIndex(e=>e.value===a))),L=react.useRef(null),j=react.useRef(null),H=react.useRef(null),M=react.useRef([]),[E,U]=react.useState(null);react.useEffect(()=>{let e=i.findIndex(n=>n.value===a);e!==-1&&A(e);},[a,i]);let ie=i.find(e=>e.value===a)?.label??i[0]?.label??"",z=react.useRef(null),re=react.useCallback(()=>{L.current&&U(L.current.getBoundingClientRect());let e=i.findIndex(d=>d.value===a),n=e!==-1?e:0;z.current=n,A(n),te(true);},[i,a]),s=react.useCallback(()=>{te(false),L.current?.focus();},[]),u=react.useCallback(e=>{r(e),j.current&&(j.current.value=e),te(false),L.current?.focus();},[r]);react.useEffect(()=>{if(R&&z.current!==null){let e=z.current;z.current=null,requestAnimationFrame(()=>{let n=H.current,d=M.current[e];if(n&&d){let S=n.clientHeight,T=d.clientHeight,P=d.offsetTop-S/2+T/2;n.scrollTop=P;}});}},[R]);let g=react.useCallback(()=>{let e=H.current;if(!e)return;let n=e.getBoundingClientRect(),d=n.top+n.height/2,S=0,T=1/0;M.current.forEach((Q,P)=>{if(!Q)return;let I=Q.getBoundingClientRect(),X=I.top+I.height/2,ne=Math.abs(X-d);ne<T&&(T=ne,S=P);}),A(S);},[]),b=react.useRef(false),x=react.useRef(null),Y=react.useCallback(()=>{b.current=true,x.current&&clearTimeout(x.current),x.current=setTimeout(()=>{b.current=false,g();},50);},[g]),O=react.useCallback(e=>{switch(e.key){case "Escape":e.preventDefault(),s();break;case "ArrowUp":e.preventDefault(),A(n=>{let d=Math.max(0,n-1);return M.current[d]?.scrollIntoView({block:"center",behavior:"smooth"}),d});break;case "ArrowDown":e.preventDefault(),A(n=>{let d=Math.min(i.length-1,n+1);return M.current[d]?.scrollIntoView({block:"center",behavior:"smooth"}),d});break;case "Enter":e.preventDefault(),i[v]&&u(i[v].value);break}},[v,i,s,u]),W=react.useCallback(e=>{i[e]&&u(i[e].value);},[i,u]),ce=react.useCallback(e=>n=>{n.preventDefault(),n.stopPropagation(),x.current&&(clearTimeout(x.current),x.current=null),W(e);},[W]),B=react.useCallback(e=>{let d=e.target.closest(".base-select-option");if(d){let S=d.id,T=parseInt(S.replace("option-",""),10);!isNaN(T)&&i[T]&&W(T);}},[i,W]),J=react.useCallback(e=>{e.target===e.currentTarget&&s();},[s]);react.useEffect(()=>{R&&H.current&&H.current.focus();},[R]),react.useEffect(()=>()=>{x.current&&clearTimeout(x.current);},[]);let $=react.useCallback(e=>{r(e.target.value);},[r]),y=()=>!R||!E?null:reactDom.createPortal(jsxRuntime.jsx("div",{className:"base-select-backdrop",onClick:J,role:"presentation",children:jsxRuntime.jsx("div",{className:"base-select-picker",style:{left:E.left,top:E.top+E.height/2},role:"dialog","aria-modal":"true","aria-label":"Select an option",children:jsxRuntime.jsxs("div",{ref:H,className:"base-select-wheel",role:"listbox",tabIndex:0,onKeyDown:O,onScroll:Y,onClick:B,"aria-activedescendant":`option-${v}`,children:[jsxRuntime.jsx("div",{className:"base-select-spacer","aria-hidden":"true"}),i.map((e,n)=>jsxRuntime.jsxs("div",{ref:d=>{M.current[n]=d;},id:`option-${n}`,role:"option","aria-selected":n===v,className:`base-select-option ${n===v?"active":""}`,onClick:ce(n),children:[jsxRuntime.jsx("span",{className:"base-select-option-text",children:e.label}),n===v&&jsxRuntime.jsx(Le,{})]},e.value)),jsxRuntime.jsx("div",{className:"base-select-spacer","aria-hidden":"true"})]})})}),document.body);return jsxRuntime.jsxs("span",{className:`base-select-compat fallback ${oe}`,children:[jsxRuntime.jsx("select",{ref:j,name:c,id:q,value:a,onChange:$,className:"base-select-hidden",tabIndex:-1,"aria-hidden":"true",children:i.map(e=>jsxRuntime.jsx("option",{value:e.value,children:e.label},e.value))}),jsxRuntime.jsxs("button",{ref:L,type:"button",className:`base-select-trigger ${R?"open":""}`,onClick:re,"aria-haspopup":"listbox","aria-expanded":R,style:{visibility:R?"hidden":"visible"},children:[jsxRuntime.jsx("span",{className:"base-select-trigger-text",children:ie}),jsxRuntime.jsx(Ae,{})]}),y()]})}exports.BaseSelectCompat=ze;exports.WheelSelect=me;exports.default=Be;//# sourceMappingURL=index.cjs.map
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|