paris 0.21.3 → 0.22.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 (31) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/package.json +1 -1
  3. package/src/helpers/OpenChangeEffect.tsx +21 -0
  4. package/src/helpers/useControllableState.test.ts +88 -0
  5. package/src/helpers/useControllableState.ts +59 -0
  6. package/src/stories/accordionselect/AccordionSelect.test.tsx +72 -0
  7. package/src/stories/accordionselect/AccordionSelect.tsx +22 -12
  8. package/src/stories/checkbox/Checkbox.test.tsx +53 -0
  9. package/src/stories/checkbox/Checkbox.tsx +21 -6
  10. package/src/stories/combobox/Combobox.test.tsx +111 -0
  11. package/src/stories/combobox/Combobox.tsx +165 -152
  12. package/src/stories/drawer/Drawer.module.scss +56 -15
  13. package/src/stories/drawer/Drawer.stories.tsx +287 -109
  14. package/src/stories/drawer/Drawer.test.tsx +486 -11
  15. package/src/stories/drawer/Drawer.tsx +366 -240
  16. package/src/stories/drawer/DrawerActions.tsx +28 -0
  17. package/src/stories/drawer/DrawerBottomPanel.tsx +55 -0
  18. package/src/stories/drawer/DrawerContext.tsx +31 -0
  19. package/src/stories/drawer/DrawerPage.tsx +37 -0
  20. package/src/stories/drawer/DrawerPageContext.tsx +35 -0
  21. package/src/stories/drawer/DrawerPaginationContext.tsx +22 -0
  22. package/src/stories/drawer/DrawerProgressBar.tsx +72 -0
  23. package/src/stories/drawer/DrawerSlotContext.tsx +172 -0
  24. package/src/stories/drawer/DrawerTitle.tsx +35 -0
  25. package/src/stories/drawer/index.ts +9 -0
  26. package/src/stories/menu/Menu.test.tsx +43 -0
  27. package/src/stories/menu/Menu.tsx +13 -2
  28. package/src/stories/popover/Popover.tsx +8 -5
  29. package/src/stories/select/Select.test.tsx +108 -0
  30. package/src/stories/select/Select.tsx +121 -92
  31. package/src/test/render.tsx +2 -2
@@ -12,7 +12,9 @@ import {
12
12
  import { clsx } from 'clsx';
13
13
  import type { ComponentPropsWithoutRef, CSSProperties, MouseEvent, ReactNode } from 'react';
14
14
  import { useCallback, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
15
+ import { OpenChangeEffect } from '../../helpers/OpenChangeEffect';
15
16
  import { MemoizedEnhancer } from '../../helpers/renderEnhancer';
17
+ import { useControllableState } from '../../helpers/useControllableState';
16
18
  import type { ButtonProps } from '../button';
17
19
  import { Button } from '../button';
18
20
  import type { FieldProps } from '../field';
@@ -52,6 +54,10 @@ export type ComboboxProps<T extends Record<string, any>> = {
52
54
  * If `null`, no option will be selected.
53
55
  */
54
56
  value?: Option<T> | null;
57
+ /**
58
+ * The initial value for uncontrolled mode. If `value` is provided, this is ignored.
59
+ */
60
+ defaultValue?: Option<T> | null;
55
61
  /**
56
62
  * The interaction handler for the Combobox. This will be called when the user selects an option from the dropdown.
57
63
  *
@@ -87,6 +93,10 @@ export type ComboboxProps<T extends Record<string, any>> = {
87
93
  * @param value
88
94
  */
89
95
  customValueToOption?: (value: string) => Option<T>;
96
+ /**
97
+ * Called when the combobox dropdown opens or closes.
98
+ */
99
+ onOpenChange?: (open: boolean) => void;
90
100
  /**
91
101
  * 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.
92
102
  */
@@ -143,6 +153,7 @@ export type ComboboxProps<T extends Record<string, any>> = {
143
153
  export function Combobox<T extends Record<string, any> = Record<string, any>>({
144
154
  options,
145
155
  value,
156
+ defaultValue,
146
157
  onChange,
147
158
  label,
148
159
  status,
@@ -159,6 +170,7 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
159
170
  showCustomValueOption = true,
160
171
  customValueString = 'Create "%v"',
161
172
  customValueToOption,
173
+ onOpenChange,
162
174
  hideClearButton = false,
163
175
  maxHeight = 320,
164
176
  hasOptionBorder = false,
@@ -166,7 +178,11 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
166
178
  overrides,
167
179
  }: ComboboxProps<T>) {
168
180
  const inputID = useId();
169
- const [selectedID, setSelectedID] = useState<string | null>(value?.id || null);
181
+ const [resolvedValue, setResolvedValue] = useControllableState<Option<T> | null>({
182
+ value,
183
+ defaultValue,
184
+ onChange,
185
+ });
170
186
  const [query, setQuery] = useState('');
171
187
  const containerElRef = useRef<HTMLElement | null>(null);
172
188
  const inputElRef = useRef<HTMLElement | null>(null);
@@ -186,7 +202,7 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
186
202
  const inputLeft = inputElRef.current.getBoundingClientRect().left;
187
203
  setAnchorOffset(containerLeft - inputLeft);
188
204
  }
189
- }, [startEnhancer, value]);
205
+ }, [startEnhancer, resolvedValue]);
190
206
 
191
207
  const optionsWithCustomValue = useMemo(
192
208
  () => [...(allowCustomValue && customValueToOption ? [customValueToOption(query)] : []), ...options],
@@ -212,171 +228,168 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
212
228
  <HCombobox
213
229
  as="div"
214
230
  immediate={!hideOptionsInitially}
215
- value={selectedID}
231
+ value={resolvedValue?.id ?? null}
216
232
  onChange={(id) => {
217
- if (onChange) {
218
- const sel = optionsWithCustomValue.find((o) => o.id === id);
219
- if (sel) {
220
- onChange(sel);
221
- setSelectedID(sel.id);
222
- } else if (id) {
223
- onChange({
224
- id: null,
225
- node: id,
226
- });
227
- }
233
+ const sel = optionsWithCustomValue.find((o) => o.id === id);
234
+ if (sel) {
235
+ setResolvedValue(sel);
236
+ } else if (id) {
237
+ setResolvedValue({ id: null, node: id } as Option<T>);
228
238
  }
229
239
  }}
230
240
  >
231
- <ComboboxButton
232
- as="div"
233
- ref={containerRef}
234
- tabIndex={-1}
235
- data-status={disabled ? 'disabled' : status || 'default'}
236
- {...overrides?.inputContainer}
237
- className={clsx(overrides?.inputContainer?.className, inputStyles.inputContainer)}
238
- >
239
- {!!startEnhancer && (
240
- <div
241
- {...overrides?.startEnhancerContainer}
242
- className={clsx(inputStyles.enhancer, overrides?.startEnhancerContainer?.className)}
241
+ {({ open }) => (
242
+ <>
243
+ <OpenChangeEffect open={open} onOpenChange={onOpenChange} />
244
+ <ComboboxButton
245
+ as="div"
246
+ ref={containerRef}
247
+ tabIndex={-1}
243
248
  data-status={disabled ? 'disabled' : status || 'default'}
249
+ {...overrides?.inputContainer}
250
+ className={clsx(overrides?.inputContainer?.className, inputStyles.inputContainer)}
244
251
  >
245
252
  {!!startEnhancer && (
246
- <MemoizedEnhancer
247
- enhancer={startEnhancer}
248
- size={parseInt(
249
- pget('typography.styles.paragraphSmall.fontSize') ||
250
- theme.typography.styles.paragraphSmall.fontSize,
251
- 10,
253
+ <div
254
+ {...overrides?.startEnhancerContainer}
255
+ className={clsx(inputStyles.enhancer, overrides?.startEnhancerContainer?.className)}
256
+ data-status={disabled ? 'disabled' : status || 'default'}
257
+ >
258
+ {!!startEnhancer && (
259
+ <MemoizedEnhancer
260
+ enhancer={startEnhancer}
261
+ size={parseInt(
262
+ pget('typography.styles.paragraphSmall.fontSize') ||
263
+ theme.typography.styles.paragraphSmall.fontSize,
264
+ 10,
265
+ )}
266
+ />
252
267
  )}
253
- />
268
+ </div>
254
269
  )}
255
- </div>
256
- )}
257
- <div className={styles.content}>
258
- {value?.node && typeof value.node !== 'string' ? (
259
- value.node
260
- ) : (
261
- <ComboboxInput
262
- ref={inputRef}
263
- id={inputID}
264
- {...overrides?.input}
265
- placeholder={placeholder}
266
- // value={query}
267
- displayValue={() => value?.node as string}
268
- onClick={(e: MouseEvent<HTMLInputElement>) => {
269
- e.stopPropagation();
270
- overrides?.input?.onClick?.(e);
271
- }}
272
- onKeyDown={(e) => {
273
- e.stopPropagation();
274
- overrides?.input?.onKeyDown?.(e);
275
- }}
276
- onChange={(e) => {
277
- setQuery(e.target.value);
278
- if (onInputChange) onInputChange(e.target.value);
279
- if (overrides?.input?.onChange) overrides.input.onChange(e);
280
- if (allowCustomValue && e.target.value) {
281
- onChange?.(
282
- customValueToOption?.(e.target.value) || {
283
- id: null,
284
- node: e.target.value,
285
- },
286
- );
287
- }
288
- }}
289
- aria-disabled={disabled}
290
- data-status={disabled ? 'disabled' : status || 'default'}
291
- className={clsx(overrides?.input?.className, inputStyles.input, styles.field)}
292
- />
293
- )}
294
- </div>
270
+ <div className={styles.content}>
271
+ {resolvedValue?.node && typeof resolvedValue.node !== 'string' ? (
272
+ resolvedValue.node
273
+ ) : (
274
+ <ComboboxInput
275
+ ref={inputRef}
276
+ id={inputID}
277
+ {...overrides?.input}
278
+ placeholder={placeholder}
279
+ // value={query}
280
+ displayValue={() => resolvedValue?.node as string}
281
+ onClick={(e: MouseEvent<HTMLInputElement>) => {
282
+ e.stopPropagation();
283
+ overrides?.input?.onClick?.(e);
284
+ }}
285
+ onKeyDown={(e) => {
286
+ e.stopPropagation();
287
+ overrides?.input?.onKeyDown?.(e);
288
+ }}
289
+ onChange={(e) => {
290
+ setQuery(e.target.value);
291
+ if (onInputChange) onInputChange(e.target.value);
292
+ if (overrides?.input?.onChange) overrides.input.onChange(e);
293
+ if (allowCustomValue && e.target.value) {
294
+ setResolvedValue(
295
+ customValueToOption?.(e.target.value) ||
296
+ ({
297
+ id: null,
298
+ node: e.target.value,
299
+ } as Option<T>),
300
+ );
301
+ }
302
+ }}
303
+ aria-disabled={disabled}
304
+ data-status={disabled ? 'disabled' : status || 'default'}
305
+ className={clsx(overrides?.input?.className, inputStyles.input, styles.field)}
306
+ />
307
+ )}
308
+ </div>
295
309
 
296
- {!!value && (!hideClearButton || typeof value.node !== 'string') && (
297
- <Button
298
- size="xs"
299
- shape="circle"
300
- startEnhancer={<FontAwesomeIcon icon={faClose} fontSize="10px" />}
301
- onClick={(e) => {
302
- e.stopPropagation();
303
- if (onChange) {
304
- onChange(null);
305
- }
306
- setSelectedID(null);
307
- }}
308
- {...overrides?.clearButton}
309
- >
310
- Clear
311
- </Button>
312
- )}
313
- {!!endEnhancer && (
314
- <div
315
- {...overrides?.endEnhancerContainer}
316
- className={clsx(inputStyles.enhancer, overrides?.endEnhancerContainer?.className)}
317
- data-status={disabled ? 'disabled' : status || 'default'}
318
- >
310
+ {!!resolvedValue && (!hideClearButton || typeof resolvedValue.node !== 'string') && (
311
+ <Button
312
+ size="xs"
313
+ shape="circle"
314
+ startEnhancer={<FontAwesomeIcon icon={faClose} fontSize="10px" />}
315
+ onClick={(e) => {
316
+ e.stopPropagation();
317
+ setResolvedValue(null);
318
+ }}
319
+ {...overrides?.clearButton}
320
+ >
321
+ Clear
322
+ </Button>
323
+ )}
319
324
  {!!endEnhancer && (
320
- <MemoizedEnhancer
321
- enhancer={endEnhancer}
322
- size={parseInt(
323
- pget('typography.styles.paragraphSmall.fontSize') ||
324
- theme.typography.styles.paragraphSmall.fontSize,
325
- 10,
325
+ <div
326
+ {...overrides?.endEnhancerContainer}
327
+ className={clsx(inputStyles.enhancer, overrides?.endEnhancerContainer?.className)}
328
+ data-status={disabled ? 'disabled' : status || 'default'}
329
+ >
330
+ {!!endEnhancer && (
331
+ <MemoizedEnhancer
332
+ enhancer={endEnhancer}
333
+ size={parseInt(
334
+ pget('typography.styles.paragraphSmall.fontSize') ||
335
+ theme.typography.styles.paragraphSmall.fontSize,
336
+ 10,
337
+ )}
338
+ />
326
339
  )}
327
- />
340
+ </div>
328
341
  )}
329
- </div>
330
- )}
331
- </ComboboxButton>
332
- <ComboboxOptions
333
- as="ul"
334
- anchor={{
335
- to: 'bottom start',
336
- gap: 9,
337
- offset: anchorOffset,
338
- }}
339
- transition
340
- {...overrides?.optionsContainer}
341
- className={clsx(overrides?.optionsContainer?.className, styles.options)}
342
- style={
343
- {
344
- '--options-maxHeight': `${maxHeight}px`,
345
- ...overrides?.optionsContainer?.style,
346
- } as CSSProperties
347
- }
348
- >
349
- {allowCustomValue && showCustomValueOption && !customValueToOption && query.length > 0 && (
350
- <ComboboxOption
351
- as="li"
352
- value={query}
353
- data-selected={false}
354
- className={clsx(overrides?.customValueOption?.className, styles.option)}
355
- {...overrides?.customValueOption}
342
+ </ComboboxButton>
343
+ <ComboboxOptions
344
+ as="ul"
345
+ anchor={{
346
+ to: 'bottom start',
347
+ gap: 9,
348
+ offset: anchorOffset,
349
+ }}
350
+ transition
351
+ {...overrides?.optionsContainer}
352
+ className={clsx(overrides?.optionsContainer?.className, styles.options)}
353
+ style={
354
+ {
355
+ '--options-maxHeight': `${maxHeight}px`,
356
+ ...overrides?.optionsContainer?.style,
357
+ } as CSSProperties
358
+ }
356
359
  >
357
- <Text as="span" kind="paragraphSmall">
358
- {customValueString.replace('%v', query)}
359
- </Text>
360
- </ComboboxOption>
361
- )}
362
- {(optionsWithCustomValue || []).map((option) => (
363
- <ComboboxOption
364
- as="li"
365
- key={option.id}
366
- value={option.id}
367
- {...overrides?.option}
368
- className={clsx(
369
- overrides?.option?.className,
370
- styles.option,
371
- hasOptionBorder && styles.optionBorder,
360
+ {allowCustomValue && showCustomValueOption && !customValueToOption && query.length > 0 && (
361
+ <ComboboxOption
362
+ as="li"
363
+ value={query}
364
+ data-selected={false}
365
+ className={clsx(overrides?.customValueOption?.className, styles.option)}
366
+ {...overrides?.customValueOption}
367
+ >
368
+ <Text as="span" kind="paragraphSmall">
369
+ {customValueString.replace('%v', query)}
370
+ </Text>
371
+ </ComboboxOption>
372
372
  )}
373
- >
374
- <TextWhenString as="span" kind="paragraphSmall">
375
- {option.node}
376
- </TextWhenString>
377
- </ComboboxOption>
378
- ))}
379
- </ComboboxOptions>
373
+ {(optionsWithCustomValue || []).map((option) => (
374
+ <ComboboxOption
375
+ as="li"
376
+ key={option.id}
377
+ value={option.id}
378
+ {...overrides?.option}
379
+ className={clsx(
380
+ overrides?.option?.className,
381
+ styles.option,
382
+ hasOptionBorder && styles.optionBorder,
383
+ )}
384
+ >
385
+ <TextWhenString as="span" kind="paragraphSmall">
386
+ {option.node}
387
+ </TextWhenString>
388
+ </ComboboxOption>
389
+ ))}
390
+ </ComboboxOptions>
391
+ </>
392
+ )}
380
393
  </HCombobox>
381
394
  </Field>
382
395
  );
@@ -11,8 +11,6 @@ $panelPaddingY: 20px;
11
11
  $duration: var(--pte-animations-duration-relaxed);
12
12
  $paginationDuration: var(--pte-animations-duration-normal);
13
13
  $panelAnimationDelay: var(--pte-animations-duration-fast);
14
- //$panelAnimationDelay: var(--pte-animations-duration-rapid);
15
- //$duration: var(--pte-animations-duration-normal);
16
14
 
17
15
  .root {
18
16
  position: fixed;
@@ -275,7 +273,6 @@ $panelAnimationDelay: var(--pte-animations-duration-fast);
275
273
  position: relative;
276
274
  height: 100%;
277
275
  padding: 0;
278
- //padding: $panelPaddingY $panelPaddingX;
279
276
  background-color: var(--pte-new-colors-surfacePrimary);
280
277
  overflow: auto;
281
278
 
@@ -283,7 +280,6 @@ $panelAnimationDelay: var(--pte-animations-duration-fast);
283
280
  flex-direction: column;
284
281
  justify-content: flex-start;
285
282
  align-items: stretch;
286
- //gap: $panelPaddingY;
287
283
 
288
284
  &.enterFrom {
289
285
  box-shadow: none;
@@ -339,14 +335,9 @@ $panelAnimationDelay: var(--pte-animations-duration-fast);
339
335
 
340
336
  .actionMenu {}
341
337
 
342
- .closeButton {
343
- //position: absolute;
344
- //right: $panelPaddingX;
345
- //top: $panelPaddingY - 5px;
346
- }
338
+ .closeButton {}
347
339
 
348
340
  .content {
349
- //padding: 20px;
350
341
  overflow-y: scroll;
351
342
  height: 100%;
352
343
  }
@@ -405,6 +396,42 @@ $panelAnimationDelay: var(--pte-animations-duration-fast);
405
396
  transition: $paginationDuration var(--pte-animations-timing-easeOutQuad);
406
397
  }
407
398
 
399
+ // Page transition containers
400
+ .pageStack {
401
+ display: grid;
402
+ grid-template-columns: 1fr;
403
+ min-height: 0;
404
+ }
405
+
406
+ .pageStackClip {
407
+ overflow: hidden;
408
+ }
409
+
410
+ .pageStackItem {
411
+ grid-area: 1 / 1;
412
+
413
+ &[data-active='false'] {
414
+ pointer-events: none;
415
+ }
416
+ }
417
+
418
+ // Progress bar
419
+ .progressBar {
420
+ position: relative;
421
+ width: 100%;
422
+ height: 2px;
423
+ background: var(--pte-new-colors-borderMedium);
424
+ }
425
+
426
+ .progressBarFill {
427
+ position: absolute;
428
+ top: 0;
429
+ left: 0;
430
+ height: 100%;
431
+ background: var(--pte-new-colors-contentPrimary);
432
+ transition: width $paginationDuration var(--pte-animations-timing-easeInQuad);
433
+ }
434
+
408
435
  .bottomPanel {
409
436
  border-top: 1px solid var(--pte-new-colors-borderStrong);
410
437
  position: absolute;
@@ -412,6 +439,21 @@ $panelAnimationDelay: var(--pte-animations-duration-fast);
412
439
  width: 100%;
413
440
  }
414
441
 
442
+ .bottomPanelSlots {
443
+ display: flex;
444
+ flex-direction: column;
445
+
446
+ // Auto-separator between portaled slot items
447
+ > * + * {
448
+ border-top: 1px solid var(--pte-new-colors-borderMedium);
449
+ }
450
+
451
+ // Separator between slots and base bottomPanel content that follows
452
+ &:not(:empty) ~ * {
453
+ border-top: 1px solid var(--pte-new-colors-borderMedium);
454
+ }
455
+ }
456
+
415
457
  .bottomPanelContent {
416
458
  position: relative;
417
459
  padding: 20px;
@@ -421,13 +463,12 @@ $panelAnimationDelay: var(--pte-animations-duration-fast);
421
463
  }
422
464
  }
423
465
 
424
- .bottomPanelSpacer {
466
+ .bottomPanelSlotItem {
467
+ position: relative;
425
468
  padding: 20px;
469
+ }
426
470
 
427
- &.noPadding {
428
- padding: 0;
429
- }
430
-
471
+ .bottomPanelSpacer {
431
472
  opacity: 0;
432
473
  pointer-events: none;
433
474
  }