@rescui/select 0.17.2 → 0.18.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.
@@ -0,0 +1,572 @@
1
+ # Select
2
+
3
+ Select is a control that allows users to select one option from a longer list.
4
+
5
+ ## Usage
6
+
7
+ ### Features showcase
8
+
9
+ ### Usage
10
+
11
+ Countries:
12
+
13
+ ```tsx
14
+ import { useState } from 'react';
15
+ import { Select } from '@rescui/select';
16
+ import COUNTRIES from '@/parts/select/countries';
17
+
18
+ const options = COUNTRIES.map(country => ({
19
+ label: country,
20
+ value: country
21
+ }));
22
+
23
+ const MySelect = () => {
24
+ const [selected, setSelected] = useState(options[42]);
25
+
26
+ return (
27
+ <div style={{ maxWidth: '380px' }}>
28
+ <Select
29
+ options={options}
30
+ value={selected}
31
+ onChange={value => setSelected(value)}
32
+ size="s"
33
+ isSearchable
34
+ label="Select country"
35
+ />
36
+ </div>
37
+ );
38
+ };
39
+
40
+ render(<MySelect />);
41
+ ```
42
+
43
+ Default value:
44
+
45
+ ```tsx
46
+ import { useState } from 'react';
47
+ import { Select } from '@rescui/select';
48
+
49
+ const options = [
50
+ { label: 'Light', value: 'light' },
51
+ { label: 'Regular', value: 'regular' },
52
+ { label: 'Medium', value: 'medium' },
53
+ { label: 'Bold', value: 'bold' }
54
+ ];
55
+ const MySelect = () => {
56
+ const [selected, setSelected] = useState({
57
+ label: 'Medium',
58
+ value: 'medium'
59
+ });
60
+
61
+ return (
62
+ <div style={{ maxWidth: '380px' }}>
63
+ <Select
64
+ options={options}
65
+ value={selected}
66
+ onChange={value => setSelected(value)}
67
+ size="m"
68
+ placeholder="Select font weight..."
69
+ aria-label="Select font weight"
70
+ />
71
+ </div>
72
+ );
73
+ };
74
+
75
+ render(<MySelect />);
76
+ ```
77
+
78
+ #### With fallback to NativeSelect on iOS and Android devices
79
+
80
+ ```tsx
81
+ import { useEffect, useState } from 'react';
82
+ import Sniffr from 'sniffr';
83
+ import { Select, NativeSelect } from '@rescui/select';
84
+ import COUNTRIES from '@/parts/select/countries';
85
+
86
+ const options = COUNTRIES.map(country => ({ label: country, value: country }));
87
+
88
+ const FallbackSelect = props => {
89
+ const [os, setOs] = useState(null);
90
+
91
+ useEffect(() => {
92
+ const sniffer = new Sniffr();
93
+ const os = sniffer.sniff(navigator.userAgent).os.name;
94
+ setOs(os);
95
+ }, []);
96
+
97
+ return os === 'ios' || os === 'android' ? (
98
+ <NativeSelect {...props} />
99
+ ) : (
100
+ <Select {...props} />
101
+ );
102
+ };
103
+
104
+ const MySelect = () => {
105
+ const [selected, setSelected] = useState(null);
106
+
107
+ return (
108
+ <div style={{ maxWidth: '380px' }}>
109
+ <FallbackSelect
110
+ options={options}
111
+ value={selected}
112
+ onChange={setSelected}
113
+ size="s"
114
+ placeholder="Select country..."
115
+ aria-label="Select country"
116
+ />
117
+ </div>
118
+ );
119
+ };
120
+
121
+ render(<MySelect />);
122
+ ```
123
+
124
+ #### GroupOptions example with headers
125
+
126
+ ```tsx
127
+ import { useEffect, useState } from 'react';
128
+ import Sniffr from 'sniffr';
129
+ import { Select, NativeSelect } from '@rescui/select';
130
+ import COUNTRIES from '@/parts/select/countries';
131
+
132
+ const COUNTRIES_WITH_OFFICES = [
133
+ 'Czech Republic',
134
+ 'Germany',
135
+ 'Netherlands',
136
+ 'United States'
137
+ ];
138
+
139
+ const popularCountries = COUNTRIES_WITH_OFFICES.map(country => ({
140
+ label: country,
141
+ value: country
142
+ }));
143
+
144
+ const allCountries = COUNTRIES.map(country => ({
145
+ label: country,
146
+ value: country
147
+ }));
148
+
149
+ const options = [
150
+ { label: 'Popular countries', options: popularCountries },
151
+ { label: 'All countries', options: allCountries }
152
+ ];
153
+
154
+ const FallbackSelect = props => {
155
+ const [os, setOs] = useState(null);
156
+
157
+ useEffect(() => {
158
+ const sniffer = new Sniffr();
159
+ const os = sniffer.sniff(navigator.userAgent).os.name;
160
+ setOs(os);
161
+ }, []);
162
+
163
+ return os === 'ios' || os === 'android' ? (
164
+ <NativeSelect {...props} />
165
+ ) : (
166
+ <Select {...props} />
167
+ );
168
+ };
169
+
170
+ const MySelect = () => {
171
+ const [selected, setSelected] = useState(null);
172
+
173
+ return (
174
+ <div style={{ maxWidth: '380px' }}>
175
+ <FallbackSelect
176
+ options={options}
177
+ value={selected}
178
+ onChange={value => setSelected(value)}
179
+ size="s"
180
+ placeholder="Select country..."
181
+ aria-label="Select country"
182
+ />
183
+ </div>
184
+ );
185
+ };
186
+
187
+ render(<MySelect />);
188
+ ```
189
+
190
+ #### Why pass `SelectOption` as value instead of `option.value`?
191
+
192
+ There are two main options of passing values:
193
+
194
+ 1. Passing only `value={option.value}`
195
+ 2. Passing the entire option as `value={option}`
196
+
197
+ We've adopted a second option. Here are the main reasons:
198
+
199
+ - _Async Options Loading:_ The first method is not suitable when options are loaded asynchronously and a default option is already known.
200
+
201
+ - _Easy Wrapper Implementation:_ It’s easier to create a wrapper that turns the second option into the first option, than the other way around.
202
+
203
+ ##### Example Wrapper:
204
+
205
+ Here's a concise example of a wrapper that modifies the API to a key-only approach, offering a cleaner interface.
206
+
207
+ ```tsx
208
+ import { useState } from 'react';
209
+ import { Select as RSelect } from '@rescui/select';
210
+
211
+ const Select = ({ value, options, onChange, ...restProps }) => {
212
+ const selectedOption = options.find(o => o.value === value);
213
+
214
+ const handleChange = option => onChange(option.value);
215
+
216
+ return (
217
+ <RSelect
218
+ {...restProps}
219
+ value={selectedOption}
220
+ onChange={handleChange}
221
+ options={options}
222
+ />
223
+ );
224
+ };
225
+
226
+ // -------- End of Wrapper --------
227
+
228
+ // -------- Code for Demo --------
229
+
230
+ const options = [
231
+ { label: 'Munich', value: 'MUC' },
232
+ { label: 'Amsterdam', value: 'AMS' },
233
+ { label: 'Berlin', value: 'BER' },
234
+ { label: 'Paphos', value: 'PFO' },
235
+ { label: 'Belgrade', value: 'BEG' },
236
+ { label: 'Yerevan', value: 'EVN' }
237
+ ];
238
+
239
+ const MySelect = () => {
240
+ const [selected, setSelected] = useState('AMS');
241
+
242
+ return (
243
+ <div style={{ maxWidth: '380px' }}>
244
+ <Select
245
+ options={options}
246
+ value={selected}
247
+ onChange={value => setSelected(value)}
248
+ size="s"
249
+ placeholder="Select city..."
250
+ aria-label="Select city"
251
+ />
252
+ </div>
253
+ );
254
+ };
255
+
256
+ render(<MySelect />);
257
+ ```
258
+
259
+ #### Contrast primary color
260
+
261
+ If your primary color background requires dark text color, use `--rs-select-contrast-primary` CSS variable.
262
+
263
+ ```tsx
264
+ import { useState } from 'react';
265
+ import { Select } from '@rescui/select';
266
+
267
+ const options = [
268
+ { label: 'Light', value: 'light' },
269
+ { label: 'Regular', value: 'regular' },
270
+ { label: 'Bold', value: 'bold' }
271
+ ];
272
+ const MySelect = () => {
273
+ const [selected, setSelected] = useState(options[1]);
274
+
275
+ return (
276
+ <div style={{ maxWidth: '380px' }} className="light-green-primary">
277
+ <Select
278
+ options={options}
279
+ value={selected}
280
+ onChange={value => setSelected(value)}
281
+ className="select-with-contrast-primary"
282
+ size="m"
283
+ placeholder="Select font weight..."
284
+ aria-label="Select font weight"
285
+ />
286
+ </div>
287
+ );
288
+ };
289
+
290
+ render(<MySelect />);
291
+ ```
292
+
293
+ ```postcss attached
294
+ .select-with-contrast-primary {
295
+ --rs-select-contrast-primary: 1;
296
+ }
297
+
298
+ .light-green-primary {
299
+ --rs-color-primary-light-theme: rgb(71, 224, 84);
300
+ --rs-color-primary-t-dim-light-theme: rgba(71, 224, 84, 0.8);
301
+ --rs-color-primary-t-fog-light-theme: rgba(71, 224, 84, 0.2);
302
+
303
+ --rs-color-primary-dark-theme: rgb(71, 224, 84);
304
+ --rs-color-primary-t-dim-dark-theme: rgba(71, 224, 84, 0.8);
305
+ --rs-color-primary-t-fog-dark-theme: rgba(71, 224, 84, 0.2);
306
+ }
307
+ ```
308
+
309
+ #### In the native dialog element
310
+
311
+ Because the [popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API/Using) places the dialog element on the Top Layer, any value of `z-index` for the options list will be ignored.
312
+
313
+ To work around this, we can use the `renderInTrigger` prop to render the popover in the dialog element instead of the bottom of the `body`.
314
+
315
+ Also use `disableFlip` prop and `overflow: visible` css on the dialog if options will overflow it.
316
+
317
+ ```tsx
318
+ import { useState } from 'react';
319
+ import { Select } from '@rescui/select';
320
+
321
+ const options = [
322
+ { label: 'Light', value: 'light' },
323
+ { label: 'Regular', value: 'regular' },
324
+ { label: 'Medium', value: 'medium' },
325
+ { label: 'Bold', value: 'bold' }
326
+ ];
327
+
328
+ const MySelect = () => {
329
+ const [selected, setSelected] = useState(options[2]);
330
+
331
+ return (
332
+ <Select
333
+ options={options}
334
+ value={selected}
335
+ onChange={value => setSelected(value)}
336
+ size="m"
337
+ placeholder="Select font weight..."
338
+ aria-label="Select font weight"
339
+ placement="bottom"
340
+ renderInTrigger
341
+ disableFlip
342
+ />
343
+ );
344
+ };
345
+
346
+ render(
347
+ <>
348
+ <button command="show-modal" commandfor="my-dialog">
349
+ Open dialog
350
+ </button>
351
+
352
+ <dialog id="my-dialog" style={{ width: 500, overflow: 'visible' }}>
353
+ <div>
354
+ <button commandfor="my-dialog" command="close">
355
+ Close
356
+ </button>
357
+ </div>
358
+ <div style={{ marginTop: 12 }}>
359
+ <MySelect />
360
+ </div>
361
+ </dialog>
362
+ </>
363
+ );
364
+ ```
365
+
366
+ ## Native
367
+
368
+ ### Features showcase
369
+
370
+ #### Example:
371
+
372
+ ```tsx
373
+ import { useState } from 'react';
374
+ import { NativeSelect } from '@rescui/select';
375
+
376
+ const options = [
377
+ { label: 'Light', value: 'light' },
378
+ { label: 'Regular', value: 'regular' },
379
+ { label: 'Bold', value: 'bold' }
380
+ ];
381
+
382
+ const MySelect = () => {
383
+ const [selected, setSelected] = useState(options[1]);
384
+
385
+ return (
386
+ <div style={{ maxWidth: '380px' }}>
387
+ <NativeSelect
388
+ options={options}
389
+ value={selected}
390
+ onChange={value => setSelected(value)}
391
+ size="m"
392
+ placeholder="Select font weight..."
393
+ aria-label="Select font weight"
394
+ />
395
+ </div>
396
+ );
397
+ };
398
+
399
+ render(<MySelect />);
400
+ ```
401
+
402
+ #### Custom trigger for NativeSelect
403
+
404
+ ```tsx
405
+ import { useState } from 'react';
406
+ import { NativeSelect } from '@rescui/select';
407
+
408
+ const options = [
409
+ { label: 'Light', value: '300' },
410
+ { label: 'Regular', value: '400' },
411
+ { label: 'Bold', value: '600' }
412
+ ];
413
+ const MySelect = () => {
414
+ const [selected, setSelected] = useState(options[0]);
415
+
416
+ return (
417
+ <div style={{ maxWidth: '380px' }}>
418
+ <NativeSelect
419
+ options={options}
420
+ value={selected}
421
+ onChange={value => setSelected(value)}
422
+ placeholder="Select font weight..."
423
+ aria-label="Select font weight"
424
+ trigger={({ selectedLabel, selectedValue, hovered, focused }) => {
425
+ return (
426
+ <div
427
+ style={{
428
+ fontWeight: selectedValue,
429
+ borderRadius: '30px',
430
+ padding: '15px',
431
+ border: `1px solid ${focused || hovered ? '#aaa' : '#ddd'}`
432
+ }}
433
+ >
434
+ Hey, I am {selectedLabel}
435
+ </div>
436
+ );
437
+ }}
438
+ />
439
+ </div>
440
+ );
441
+ };
442
+
443
+ render(<MySelect />);
444
+ ```
445
+
446
+ ## CSS Mixins API
447
+
448
+ #### `Select` mixins
449
+
450
+ | Mixin name | Details |
451
+ | --- | --- |
452
+ | `rs-select-size-s` | prop: `size`<br><br>value: `"s"` |
453
+ | `rs-select-size-m` | prop: `size`<br><br>value: `"m"` |
454
+ | `rs-select-size-l` | prop: `size`<br><br>value: `"l"` |
455
+
456
+ ##### Demo
457
+
458
+ Use the controls to see the effect at different viewport widths.
459
+
460
+ ```tsx
461
+ import { useState } from 'react';
462
+ import { Select } from '@rescui/select';
463
+
464
+ const options = [
465
+ { label: 'St. Petersburg', value: 'spb' },
466
+ { label: 'Moscow', value: 'msk' },
467
+ { label: 'Novosibirsk', value: 'nsk' },
468
+ { label: 'Prague', value: 'prg' },
469
+ { label: 'Boston', value: 'bos' },
470
+ { label: 'Munich', value: 'muc' }
471
+ ];
472
+
473
+ const MySelect = ({ className }) => {
474
+ const [selected, setSelected] = useState(options[0]);
475
+
476
+ return (
477
+ <div style={{ maxWidth: 380, height: 380 }}>
478
+ <Select
479
+ options={options}
480
+ value={selected}
481
+ onChange={value => setSelected(value)}
482
+ size="m"
483
+ placeholder="Сhoose your favorite city..."
484
+ aria-label="Choose your favorite city"
485
+ className={className}
486
+ />
487
+ </div>
488
+ );
489
+ };
490
+
491
+ render(<MySelect className="my-select" />);
492
+ ```
493
+
494
+ ```postcss attached
495
+ @import '@rescui/select/lib/public-api.p.css';
496
+
497
+ .my-select {
498
+ @media (max-width: 640px) {
499
+ @mixin rs-select-size-l;
500
+ }
501
+ @media (max-width: 500px) {
502
+ @mixin rs-select-size-s;
503
+ }
504
+ }
505
+ ```
506
+
507
+ #### `Select` CSS Vars
508
+
509
+ | Variable | Syntax | Description |
510
+ | --- | --- | --- |
511
+ | `--rs-select-contrast-primary` | `0 \| 1` | The flag whenever the current primary is contrast enough and highlighted text color should be inverted |
512
+
513
+ Check out [this example](/components/select/#contrast-primary-color) for contrast primary.
514
+
515
+ #### `NativeSelect` mixins
516
+
517
+ | Mixin name | Details |
518
+ | --- | --- |
519
+ | `rs-native-select-size-s` | prop: `size`<br><br>value: `"s"` |
520
+ | `rs-native-select-size-m` | prop: `size`<br><br>value: `"m"` |
521
+ | `rs-native-select-size-l` | prop: `size`<br><br>value: `"l"` |
522
+
523
+ ##### Demo
524
+
525
+ Use the controls to see the effect at different viewport widths.
526
+
527
+ ```tsx
528
+ import { useState } from 'react';
529
+ import { NativeSelect } from '@rescui/select';
530
+
531
+ const options = [
532
+ { label: 'St. Petersburg', value: 'spb' },
533
+ { label: 'Moscow', value: 'msk' },
534
+ { label: 'Novosibirsk', value: 'nsk' },
535
+ { label: 'Prague', value: 'prg' },
536
+ { label: 'Boston', value: 'bos' },
537
+ { label: 'Munich', value: 'muc' }
538
+ ];
539
+
540
+ const MyNativeSelect = ({ className }) => {
541
+ const [selected, setSelected] = useState(options[0]);
542
+
543
+ return (
544
+ <div style={{ maxWidth: 380, height: 380 }}>
545
+ <NativeSelect
546
+ options={options}
547
+ value={selected}
548
+ onChange={value => setSelected(value)}
549
+ size="m"
550
+ placeholder="Сhoose your favorite city..."
551
+ aria-label="Choose your favorite city"
552
+ className={className}
553
+ />
554
+ </div>
555
+ );
556
+ };
557
+
558
+ render(<MyNativeSelect className="my-native-select" />);
559
+ ```
560
+
561
+ ```postcss
562
+ @import '@rescui/select/lib/public-api.p.css';
563
+
564
+ .my-native-select {
565
+ @media (max-width: 640px) {
566
+ @mixin rs-native-select-size-l;
567
+ }
568
+ @media (max-width: 500px) {
569
+ @mixin rs-native-select-size-s;
570
+ }
571
+ }
572
+ ```
package/llm/index.md ADDED
@@ -0,0 +1,3 @@
1
+ - Components and Hooks
2
+ - [Multiselect](./components/multiselect.md)
3
+ - [Select](./components/select.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rescui/select",
3
- "version": "0.17.2",
3
+ "version": "0.18.0",
4
4
  "description": "",
5
5
  "license": "Apache-2.0",
6
6
  "author": "JetBrains",
@@ -9,6 +9,7 @@
9
9
  "src:main": "src/index.ts",
10
10
  "files": [
11
11
  "lib",
12
+ "llm",
12
13
  "LICENSE.txt"
13
14
  ],
14
15
  "publishConfig": {
@@ -16,10 +17,10 @@
16
17
  },
17
18
  "dependencies": {
18
19
  "@babel/runtime-corejs3": "^7.26.0",
19
- "@rescui/dropdown": "^0.10.2",
20
- "@rescui/form-field": "^0.1.4",
21
- "@rescui/tag": "^0.13.10",
22
- "@rescui/use-resize-observer": "^0.3.1",
20
+ "@rescui/dropdown": "^0.11.0",
21
+ "@rescui/form-field": "^0.1.5",
22
+ "@rescui/tag": "^0.13.11",
23
+ "@rescui/use-resize-observer": "^0.3.3",
23
24
  "classnames": "^2.2.6",
24
25
  "downshift": "^9.3.2",
25
26
  "scroll-into-view-if-needed": "3.1.0"
@@ -30,9 +31,9 @@
30
31
  "react": ">=16.8.0 <20"
31
32
  },
32
33
  "devDependencies": {
33
- "@rescui/colors": "^0.3.1",
34
+ "@rescui/colors": "^0.3.2",
34
35
  "@rescui/scripts": "^0.5.2",
35
- "@rescui/typography": "^0.27.1",
36
+ "@rescui/typography": "^0.27.2",
36
37
  "@rescui/visual-regression": "^0.1.4",
37
38
  "sniffr": "^1.2.0"
38
39
  },
@@ -41,5 +42,5 @@
41
42
  "build-pcss-api": "rescui-scripts build-pcss-api --file public-api.p.css"
42
43
  },
43
44
  "nx": {},
44
- "gitHead": "a04f6790abce8e72834eedce3512ee83513c5e78"
45
+ "gitHead": "7aade8fced69c4a1ac61078a1daf2c1cd4f5a1c9"
45
46
  }