paris 0.10.1 → 0.11.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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# paris
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2e075cc: Combobox: Improved custom option props, allowing comboboxes to act more like auto-complete inputs
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 2e075cc: Combobox: add Field overrides
|
|
12
|
+
|
|
13
|
+
## 0.10.2
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 014a642: Button: hide enhancers in loading state
|
|
18
|
+
|
|
3
19
|
## 0.10.1
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "paris",
|
|
3
3
|
"author": "Sanil Chawla <sanil@slingshot.fm> (https://sanil.co)",
|
|
4
4
|
"description": "Paris is Slingshot's React design system. It's a collection of reusable components, design tokens, and guidelines that help us build consistent, accessible, and performant user interfaces.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.11.0",
|
|
6
6
|
"homepage": "https://paris.slingshot.fm",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
@@ -14,8 +14,7 @@ import { Text } from '../text';
|
|
|
14
14
|
import type { Enhancer } from '../../types/Enhancer';
|
|
15
15
|
import { MemoizedEnhancer } from '../../helpers/renderEnhancer';
|
|
16
16
|
import { pvar } from '../theme';
|
|
17
|
-
import { Spinner } from '../icon';
|
|
18
|
-
import { NotificationDot } from '../icon/NotificationDot';
|
|
17
|
+
import { Spinner, NotificationDot } from '../icon';
|
|
19
18
|
|
|
20
19
|
const EnhancerSizes = {
|
|
21
20
|
large: 13,
|
|
@@ -200,7 +199,7 @@ export const Button: FC<ButtonProps> = ({
|
|
|
200
199
|
),
|
|
201
200
|
} : {}}
|
|
202
201
|
>
|
|
203
|
-
{!!startEnhancer && (
|
|
202
|
+
{!!(startEnhancer && !loading) && (
|
|
204
203
|
<MemoizedEnhancer
|
|
205
204
|
enhancer={startEnhancer}
|
|
206
205
|
size={EnhancerSizes[size]}
|
|
@@ -215,7 +214,7 @@ export const Button: FC<ButtonProps> = ({
|
|
|
215
214
|
)}
|
|
216
215
|
</Text>
|
|
217
216
|
)}
|
|
218
|
-
{!!endEnhancer && (
|
|
217
|
+
{!!(endEnhancer && !loading) && (
|
|
219
218
|
<MemoizedEnhancer
|
|
220
219
|
enhancer={endEnhancer}
|
|
221
220
|
size={EnhancerSizes[size]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable react-hooks/rules-of-hooks,react/no-children-prop */
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
3
|
import { createElement, useState } from 'react';
|
|
4
|
-
import type { Option } from './Combobox';
|
|
4
|
+
import type { ComboboxProps, Option } from './Combobox';
|
|
5
5
|
import { Combobox } from './Combobox';
|
|
6
6
|
import { Text } from '../text';
|
|
7
7
|
|
|
@@ -12,56 +12,78 @@ const meta: Meta<typeof Combobox> = {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export default meta;
|
|
15
|
-
type Story = StoryObj<typeof Combobox
|
|
15
|
+
type Story = StoryObj<typeof Combobox<{ name: string }>>;
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
name: 'Mia Dolan',
|
|
31
|
-
},
|
|
17
|
+
const ComboboxArgs: ComboboxProps<{ name: string }> = {
|
|
18
|
+
label: 'Share',
|
|
19
|
+
description: 'Search for a friend to share this document with.',
|
|
20
|
+
placeholder: 'Search...',
|
|
21
|
+
options: [
|
|
22
|
+
{
|
|
23
|
+
id: '1',
|
|
24
|
+
node: createElement(Text, {
|
|
25
|
+
kind: 'paragraphSmall',
|
|
26
|
+
children: 'Mia Dolan',
|
|
27
|
+
}),
|
|
28
|
+
metadata: {
|
|
29
|
+
name: 'Mia Dolan',
|
|
32
30
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
metadata: {
|
|
40
|
-
name: 'Sebastian Wilder',
|
|
41
|
-
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: '2',
|
|
34
|
+
node: 'SEB',
|
|
35
|
+
metadata: {
|
|
36
|
+
name: 'Sebastian Wilder',
|
|
42
37
|
},
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: '3',
|
|
41
|
+
node: createElement(Text, {
|
|
42
|
+
kind: 'paragraphSmall',
|
|
43
|
+
children: 'Amy Brandt',
|
|
44
|
+
}),
|
|
45
|
+
metadata: {
|
|
46
|
+
name: 'Amy Brandt',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: '4',
|
|
51
|
+
node: createElement(Text, {
|
|
52
|
+
kind: 'paragraphSmall',
|
|
53
|
+
children: 'Laura Wilder',
|
|
54
|
+
}),
|
|
55
|
+
metadata: {
|
|
56
|
+
name: 'Laura Wilder',
|
|
52
57
|
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const Default: Story = {
|
|
63
|
+
args: ComboboxArgs,
|
|
64
|
+
render: (args) => {
|
|
65
|
+
const [selected, setSelected] = useState<Option<{ name: string }> | null>(null);
|
|
66
|
+
const [inputValue, setInputValue] = useState<string>('');
|
|
67
|
+
return createElement('div', {
|
|
68
|
+
style: { minHeight: '200px' },
|
|
69
|
+
}, createElement(Combobox<{ name: string }>, {
|
|
70
|
+
...args,
|
|
71
|
+
value: (selected?.id === null) ? {
|
|
72
|
+
id: null,
|
|
73
|
+
node: inputValue,
|
|
59
74
|
metadata: {
|
|
60
|
-
name:
|
|
75
|
+
name: inputValue,
|
|
61
76
|
},
|
|
62
|
-
},
|
|
63
|
-
|
|
77
|
+
} : selected as Option<{ name: string }> | null,
|
|
78
|
+
options: (args.options as Option<{ name: string }>[]).filter((o) => (o.metadata?.name as string || '').toLowerCase().includes(inputValue.toLowerCase())),
|
|
79
|
+
onChange: (e) => setSelected(e),
|
|
80
|
+
onInputChange: (e) => setInputValue(e),
|
|
81
|
+
}));
|
|
64
82
|
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const AllowCustomValue: Story = {
|
|
86
|
+
args: { ...ComboboxArgs, allowCustomValue: true, customValueString: 'Add "%v"' },
|
|
65
87
|
render: (args) => {
|
|
66
88
|
const [selected, setSelected] = useState<Option | null>(null);
|
|
67
89
|
const [inputValue, setInputValue] = useState<string>('');
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useMemo, useId, useState,
|
|
6
|
+
} from 'react';
|
|
5
7
|
import { Combobox as HCombobox, Transition } from '@headlessui/react';
|
|
6
8
|
import clsx from 'clsx';
|
|
7
9
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
@@ -14,6 +16,7 @@ import { Text } from '../text';
|
|
|
14
16
|
import type { InputProps } from '../input';
|
|
15
17
|
import { MemoizedEnhancer } from '../../helpers/renderEnhancer';
|
|
16
18
|
import { pget, theme } from '../theme';
|
|
19
|
+
import type { FieldProps } from '../field';
|
|
17
20
|
import { Field } from '../field';
|
|
18
21
|
import { Button } from '../button';
|
|
19
22
|
|
|
@@ -60,6 +63,11 @@ export type ComboboxProps<T extends Record<string, any>> = {
|
|
|
60
63
|
* @default false
|
|
61
64
|
*/
|
|
62
65
|
allowCustomValue?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Whether to show the custom value option in the dropdown. This is irrelevant if `allowCustomValue` is `false`.
|
|
68
|
+
* @default true
|
|
69
|
+
*/
|
|
70
|
+
showCustomValueOption?: boolean;
|
|
63
71
|
/**
|
|
64
72
|
* The text to use for the custom creation option. This should include a `%v` placeholder, which will be replaced with the user's input.
|
|
65
73
|
*
|
|
@@ -67,6 +75,15 @@ export type ComboboxProps<T extends Record<string, any>> = {
|
|
|
67
75
|
* @default Create "%v"...
|
|
68
76
|
*/
|
|
69
77
|
customValueString?: string;
|
|
78
|
+
/**
|
|
79
|
+
* A function that will be called to create an {@link Option} based on the user's custom typed query value. This is useful for adding custom styling by allowing you to pass a custom `Option.node` based on the value. This overrides the `customValueString` prop.
|
|
80
|
+
* @param value
|
|
81
|
+
*/
|
|
82
|
+
customValueToOption?: (value: string) => Option<T>;
|
|
83
|
+
/**
|
|
84
|
+
* Whether to hide the clear button when a value is selected. This will never be hidden if the selected option's node is not a strong, because there is no other way to clear the value as of now.
|
|
85
|
+
*/
|
|
86
|
+
hideClearButton?: boolean;
|
|
70
87
|
/**
|
|
71
88
|
* The size of the options dropdown, in pixels.
|
|
72
89
|
*/
|
|
@@ -80,6 +97,7 @@ export type ComboboxProps<T extends Record<string, any>> = {
|
|
|
80
97
|
* Prop overrides for other rendered elements. Overrides for the input itself should be passed directly to the component.
|
|
81
98
|
*/
|
|
82
99
|
overrides?: {
|
|
100
|
+
field?: FieldProps;
|
|
83
101
|
container?: ComponentPropsWithoutRef<'div'>;
|
|
84
102
|
input?: ComponentPropsWithoutRef<'input'>;
|
|
85
103
|
optionsContainer?: ComponentPropsWithoutRef<'div'>;
|
|
@@ -94,6 +112,10 @@ export type ComboboxProps<T extends Record<string, any>> = {
|
|
|
94
112
|
/**
|
|
95
113
|
* A Combobox component is used to render a searchable select.
|
|
96
114
|
*
|
|
115
|
+
* When the selected option node is a strings, the combobox will act like an input even when an option is selected, allowing users to edit the selected option directly in order to pick a new one. To circumvent this and make selected options non-editable, pass nodes that are `Text` components instead.
|
|
116
|
+
*
|
|
117
|
+
* When `allowCustomValue` is `true`, a custom value option will be added to the dropdown. This option's text can be customized by passing a value for `customValueString`, where `%v` within the string is the user's input. You can provide an entirely custom node through `renderCustomValueOption`. By default, `onChange` will be called for every input change when custom values are allowed.
|
|
118
|
+
*
|
|
97
119
|
* <hr />
|
|
98
120
|
*
|
|
99
121
|
* To use this component, import it as follows:
|
|
@@ -118,7 +140,10 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
|
|
|
118
140
|
disabled,
|
|
119
141
|
onInputChange,
|
|
120
142
|
allowCustomValue,
|
|
143
|
+
showCustomValueOption = true,
|
|
121
144
|
customValueString = 'Create "%v"',
|
|
145
|
+
customValueToOption,
|
|
146
|
+
hideClearButton = false,
|
|
122
147
|
maxHeight = 320,
|
|
123
148
|
hasOptionBorder = false,
|
|
124
149
|
overrides,
|
|
@@ -127,6 +152,13 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
|
|
|
127
152
|
const [selectedID, setSelectedID] = useState<string | null>(value?.id || null);
|
|
128
153
|
const [query, setQuery] = useState('');
|
|
129
154
|
|
|
155
|
+
const optionsWithCustomValue = useMemo(() => ([
|
|
156
|
+
...((allowCustomValue && customValueToOption) ? [
|
|
157
|
+
customValueToOption(query),
|
|
158
|
+
] : []),
|
|
159
|
+
...options,
|
|
160
|
+
]), [allowCustomValue, customValueToOption, options, query]);
|
|
161
|
+
|
|
130
162
|
return (
|
|
131
163
|
<Field
|
|
132
164
|
htmlFor={inputID}
|
|
@@ -140,13 +172,14 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
|
|
|
140
172
|
label: overrides?.label,
|
|
141
173
|
description: overrides?.description,
|
|
142
174
|
}}
|
|
175
|
+
{...(overrides?.field ?? {})}
|
|
143
176
|
>
|
|
144
177
|
<HCombobox
|
|
145
178
|
as="div"
|
|
146
179
|
value={selectedID}
|
|
147
180
|
onChange={(id) => {
|
|
148
181
|
if (onChange) {
|
|
149
|
-
const sel =
|
|
182
|
+
const sel = optionsWithCustomValue.find((o) => o.id === id);
|
|
150
183
|
if (sel) {
|
|
151
184
|
onChange(sel);
|
|
152
185
|
setSelectedID(sel.id);
|
|
@@ -178,16 +211,23 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
|
|
|
178
211
|
</div>
|
|
179
212
|
)}
|
|
180
213
|
<div className={styles.content}>
|
|
181
|
-
{value ? value.node : (
|
|
214
|
+
{(value?.node && typeof value.node !== 'string') ? value.node : (
|
|
182
215
|
<HCombobox.Input
|
|
183
216
|
id={inputID}
|
|
184
217
|
{...overrides?.input}
|
|
185
218
|
placeholder={placeholder}
|
|
186
219
|
// value={query}
|
|
220
|
+
displayValue={(allowCustomValue && typeof value?.node === 'string') ? () => value.node as string : undefined}
|
|
187
221
|
onChange={(e) => {
|
|
188
222
|
setQuery(e.target.value);
|
|
189
223
|
if (onInputChange) onInputChange(e.target.value);
|
|
190
224
|
if (overrides?.input?.onChange) overrides.input.onChange(e);
|
|
225
|
+
if (allowCustomValue && e.target.value) {
|
|
226
|
+
onChange?.(customValueToOption?.(e.target.value) || {
|
|
227
|
+
id: null,
|
|
228
|
+
node: e.target.value,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
191
231
|
}}
|
|
192
232
|
aria-disabled={disabled}
|
|
193
233
|
data-status={disabled ? 'disabled' : (status || 'default')}
|
|
@@ -199,7 +239,8 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
|
|
|
199
239
|
/>
|
|
200
240
|
)}
|
|
201
241
|
</div>
|
|
202
|
-
|
|
242
|
+
|
|
243
|
+
{(!!value && (!hideClearButton || typeof value.node !== 'string')) && (
|
|
203
244
|
<Button
|
|
204
245
|
size="xs"
|
|
205
246
|
shape="circle"
|
|
@@ -246,7 +287,7 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
|
|
|
246
287
|
'--options-maxHeight': `${maxHeight}px`,
|
|
247
288
|
} as CSSProperties}
|
|
248
289
|
>
|
|
249
|
-
{(allowCustomValue && query.length > 0) && (
|
|
290
|
+
{(allowCustomValue && showCustomValueOption && !customValueToOption && query.length > 0) && (
|
|
250
291
|
<HCombobox.Option
|
|
251
292
|
value={query}
|
|
252
293
|
data-selected={false}
|
|
@@ -260,24 +301,29 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
|
|
|
260
301
|
</Text>
|
|
261
302
|
</HCombobox.Option>
|
|
262
303
|
)}
|
|
263
|
-
{
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
304
|
+
{
|
|
305
|
+
(
|
|
306
|
+
optionsWithCustomValue || []
|
|
307
|
+
)
|
|
308
|
+
.map((option) => (
|
|
309
|
+
<HCombobox.Option
|
|
310
|
+
key={option.id}
|
|
311
|
+
value={option.id}
|
|
312
|
+
data-selected={option.id === value}
|
|
313
|
+
className={clsx(
|
|
314
|
+
overrides?.option,
|
|
315
|
+
styles.option,
|
|
316
|
+
hasOptionBorder && styles.optionBorder,
|
|
317
|
+
)}
|
|
318
|
+
>
|
|
319
|
+
{typeof option.node === 'string' ? (
|
|
320
|
+
<Text as="span" kind="paragraphSmall">
|
|
321
|
+
{option.node}
|
|
322
|
+
</Text>
|
|
323
|
+
) : option.node}
|
|
324
|
+
</HCombobox.Option>
|
|
325
|
+
))
|
|
326
|
+
}
|
|
281
327
|
</HCombobox.Options>
|
|
282
328
|
</Transition>
|
|
283
329
|
</HCombobox>
|