@jobber/components 6.103.2 → 6.103.4-uncontroll-d4ef425.5

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.
@@ -231,18 +231,26 @@ interface AutocompleteRebuiltBaseProps<Value extends OptionLike, Multiple extend
231
231
  */
232
232
  readonly multiple?: Multiple;
233
233
  /**
234
- * The currently selected value of the Autocomplete.
235
- * Single-select: undefined indicates no selection
234
+ * The currently selected value of the Autocomplete (controlled mode).
235
+ * Single-select: undefined indicates no selection.
236
+ * If omitted, the component manages its own value state (uncontrolled mode).
236
237
  */
237
- readonly value: AutocompleteValue<Value, Multiple>;
238
+ readonly value?: AutocompleteValue<Value, Multiple>;
238
239
  /**
239
- * The current input value of the Autocomplete.
240
+ * The initial value for uncontrolled mode.
241
+ * Only used when value is not provided.
240
242
  */
241
- readonly inputValue: string;
243
+ readonly defaultValue?: AutocompleteValue<Value, Multiple>;
244
+ /**
245
+ * The current input value of the Autocomplete (controlled mode).
246
+ * If omitted, the component manages its own input state based on the selected value.
247
+ */
248
+ readonly inputValue?: string;
242
249
  /**
243
250
  * Callback invoked when the input value changes.
251
+ * If omitted, the component manages its own input state based on the selected value.
244
252
  */
245
- readonly onInputChange: (value: string) => void;
253
+ readonly onInputChange?: (value: string) => void;
246
254
  /**
247
255
  * Callback invoked when the input is blurred.
248
256
  */
@@ -454,20 +462,17 @@ interface FreeFormOff<Value extends OptionLike, Multiple extends boolean> {
454
462
  readonly allowFreeForm?: false;
455
463
  /**
456
464
  * Callback invoked when the selection value changes.
465
+ * Optional when value is not provided (uncontrolled mode).
457
466
  */
458
- readonly onChange: (value: AutocompleteValue<Value, Multiple>) => void;
467
+ readonly onChange?: (value: AutocompleteValue<Value, Multiple>) => void;
459
468
  }
460
469
  interface FreeFormOn<Value extends OptionLike, Multiple extends boolean> {
461
470
  /**
462
- * Whether the autocomplete allows free-form input.
463
- * When true, the input value is not restricted to the options * in the menu. Input can be used to create a new value.
464
- * When false, the input value must match an option in the menu.
465
- * Input value will be cleared if no selection is made and
466
471
  * Whether the autocomplete allows free-form input.
467
472
  * When true, the input value is not restricted to the options in the menu. Input can be used to create a new value.
468
473
  * When false, the input value must match an option in the menu.
469
474
  * Input value will be cleared if no selection is made and focus is lost.
470
- * */
475
+ */
471
476
  readonly allowFreeForm: true;
472
477
  /**
473
478
  * Factory used to create a Value from free-form input when committing. Necessary with complex option values. The only value the input can produce is a string.
@@ -484,8 +489,9 @@ interface FreeFormOn<Value extends OptionLike, Multiple extends boolean> {
484
489
  * - The user selects an option with click or enter
485
490
  * - The user types a value that matches an option
486
491
  * - The user types a value that does not match an option and allowFreeForm is true
492
+ * Optional when value is not provided (uncontrolled mode).
487
493
  */
488
- readonly onChange: (value: AutocompleteValue<Value, Multiple>) => void;
494
+ readonly onChange?: (value: AutocompleteValue<Value, Multiple>) => void;
489
495
  }
490
496
  export type ActionOrigin = "menu" | "empty";
491
497
  export type AutocompleteRebuiltProps<Value extends OptionLike = OptionLike, Multiple extends boolean = false, SectionExtra extends object = ExtraProps, ActionExtra extends object = ExtraProps> = AutocompleteRebuiltBaseProps<Value, Multiple, SectionExtra, ActionExtra> & (FreeFormOn<Value, Multiple> | FreeFormOff<Value, Multiple>);
@@ -224,13 +224,47 @@ function useAutocompleteListNav({ navigableCount, shouldResetActiveIndexOnClose,
224
224
  // interactions and state transitions.
225
225
  // eslint-disable-next-line max-statements
226
226
  function useAutocomplete(props) {
227
- const { menu, emptyActions, getOptionLabel: getOptionLabelProp, isOptionEqualToValue, inputValue, onInputChange, value, onChange, multiple, openOnFocus = true, readOnly = false, debounce: debounceMs = 300, } = props;
227
+ const { menu, emptyActions, getOptionLabel: getOptionLabelProp, isOptionEqualToValue, inputValue: inputValueProp, onInputChange: onInputChangeProp, value: valueProp, defaultValue, onChange: onChangeProp, multiple, openOnFocus = true, readOnly = false, debounce: debounceMs = 300, } = props;
228
+ // Internal state for uncontrolled value
229
+ const [internalValue, setInternalValue] = React.useState(defaultValue);
230
+ // Use controlled value if provided, otherwise use internal state
231
+ const value = valueProp !== undefined ? valueProp : internalValue;
232
+ const onChange = onChangeProp !== null && onChangeProp !== void 0 ? onChangeProp : setInternalValue;
228
233
  // TODO: Clean up the types in these refs by enhancing the type system in useCallbackRef
229
234
  const getOptionLabelPropRef = jobberHooks.useCallbackRef((opt) => getOptionLabelProp === null || getOptionLabelProp === void 0 ? void 0 : getOptionLabelProp(opt));
230
235
  const getOptionLabel = React.useCallback((opt) => {
231
236
  const maybe = getOptionLabelPropRef(opt);
232
237
  return maybe !== null && maybe !== void 0 ? maybe : opt.label;
233
238
  }, [getOptionLabelPropRef]);
239
+ // Initialize internal input value from defaultValue
240
+ const [internalInputValue, setInternalInputValue] = React.useState(() => {
241
+ if (multiple)
242
+ return "";
243
+ const initialValue = (defaultValue !== null && defaultValue !== void 0 ? defaultValue : valueProp);
244
+ if (!initialValue)
245
+ return "";
246
+ // Call getOptionLabelProp directly if provided, otherwise use label
247
+ const customLabel = getOptionLabelProp === null || getOptionLabelProp === void 0 ? void 0 : getOptionLabelProp(initialValue);
248
+ return customLabel !== null && customLabel !== void 0 ? customLabel : initialValue.label;
249
+ });
250
+ // Track previous value to detect when it changes
251
+ const prevValueRef = React.useRef(value);
252
+ // Sync internal input value with selected value only when value changes (not on user input)
253
+ React.useEffect(() => {
254
+ const isInputControlled = inputValueProp !== undefined;
255
+ if (isInputControlled || multiple)
256
+ return;
257
+ // Only update if the value actually changed
258
+ if (prevValueRef.current !== value) {
259
+ prevValueRef.current = value;
260
+ const currentValue = value;
261
+ setInternalInputValue(currentValue ? getOptionLabel(currentValue) : "");
262
+ }
263
+ }, [value, inputValueProp, multiple, getOptionLabel]);
264
+ // Use controlled inputValue if provided, otherwise use internal state
265
+ const isInputControlled = inputValueProp !== undefined;
266
+ const inputValue = isInputControlled ? inputValueProp : internalInputValue;
267
+ const onInputChange = onInputChangeProp !== null && onInputChangeProp !== void 0 ? onInputChangeProp : setInternalInputValue;
234
268
  const isOptionEqualToValueRef = jobberHooks.useCallbackRef((a, b) => isOptionEqualToValue === null || isOptionEqualToValue === void 0 ? void 0 : isOptionEqualToValue(a, b));
235
269
  const equals = React.useCallback((a, b) => {
236
270
  const custom = isOptionEqualToValueRef(a, b);
@@ -486,7 +520,7 @@ function useAutocomplete(props) {
486
520
  const freeFormCreated = (_a = props.createFreeFormValue) === null || _a === void 0 ? void 0 : _a.call(props, inputText);
487
521
  if (!freeFormCreated)
488
522
  return false;
489
- props.onChange(freeFormCreated);
523
+ onChange(freeFormCreated);
490
524
  return true;
491
525
  }
492
526
  const tryRestoreInputToSelectedLabel = React.useCallback(() => {
@@ -666,6 +700,7 @@ function useAutocomplete(props) {
666
700
  activeIndex,
667
701
  setActiveIndex,
668
702
  listRef,
703
+ inputValue,
669
704
  // actions
670
705
  onSelection,
671
706
  onAction,
@@ -903,8 +938,8 @@ const AutocompleteRebuilt = React.forwardRef(AutocompleteRebuiltInternal);
903
938
  // eslint-disable-next-line max-statements
904
939
  function AutocompleteRebuiltInternal(props, forwardedRef) {
905
940
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
906
- const { inputValue, placeholder, disabled, error, invalid, description, size: sizeProp, loading = false, } = props;
907
- const { renderable, optionCount, persistentsHeaders, persistentsFooters, headerInteractiveCount, middleNavigableCount, getOptionLabel, isOptionSelected, refs, floatingStyles, context, getReferenceProps, getFloatingProps, getItemProps, activeIndex, open, listRef, onSelection, onAction, onInputChangeFromUser, onInputBlur, onInputFocus, onInputKeyDown, setReferenceElement, } = useAutocomplete(props);
941
+ const { placeholder, disabled, error, invalid, description, size: sizeProp, loading = false, } = props;
942
+ const { renderable, optionCount, persistentsHeaders, persistentsFooters, headerInteractiveCount, middleNavigableCount, getOptionLabel, isOptionSelected, refs, floatingStyles, context, getReferenceProps, getFloatingProps, getItemProps, activeIndex, open, listRef, inputValue, onSelection, onAction, onInputChangeFromUser, onInputBlur, onInputFocus, onInputKeyDown, setReferenceElement, } = useAutocomplete(props);
908
943
  const listboxId = React.useId();
909
944
  // Provides mount/unmount-aware transition styles for the floating element
910
945
  const { isMounted, styles: transitionStyles } = floatingUi_react.useTransitionStyles(context, {
@@ -920,7 +955,7 @@ function AutocompleteRebuiltInternal(props, forwardedRef) {
920
955
  onFocus: onInputFocus,
921
956
  onBlur: onInputBlur,
922
957
  });
923
- const inputProps = Object.assign(Object.assign(Object.assign(Object.assign({ version: 2, value: inputValue, onChange: props.readOnly ? undefined : onInputChangeFromUser }, (props.readOnly ? { onFocus: onInputFocus, onBlur: onInputBlur } : {})), { placeholder,
958
+ const inputProps = Object.assign(Object.assign(Object.assign(Object.assign({ version: 2, value: inputValue !== null && inputValue !== void 0 ? inputValue : "", onChange: props.readOnly ? undefined : onInputChangeFromUser }, (props.readOnly ? { onFocus: onInputFocus, onBlur: onInputBlur } : {})), { placeholder,
924
959
  disabled, readOnly: props.readOnly, error: error !== null && error !== void 0 ? error : undefined, name: props.name, invalid, autoComplete: "off", description, size: sizeProp ? sizeProp : undefined, prefix: props.prefix, suffix: props.suffix }), (props.readOnly ? {} : composedReferenceProps)), { role: "combobox", "aria-autocomplete": "list", "aria-expanded": open ? true : false, "aria-controls": listboxId, "aria-activedescendant": open && activeIndex != null
925
960
  ? `${listboxId}-item-${activeIndex}`
926
961
  : undefined });
@@ -222,13 +222,47 @@ function useAutocompleteListNav({ navigableCount, shouldResetActiveIndexOnClose,
222
222
  // interactions and state transitions.
223
223
  // eslint-disable-next-line max-statements
224
224
  function useAutocomplete(props) {
225
- const { menu, emptyActions, getOptionLabel: getOptionLabelProp, isOptionEqualToValue, inputValue, onInputChange, value, onChange, multiple, openOnFocus = true, readOnly = false, debounce: debounceMs = 300, } = props;
225
+ const { menu, emptyActions, getOptionLabel: getOptionLabelProp, isOptionEqualToValue, inputValue: inputValueProp, onInputChange: onInputChangeProp, value: valueProp, defaultValue, onChange: onChangeProp, multiple, openOnFocus = true, readOnly = false, debounce: debounceMs = 300, } = props;
226
+ // Internal state for uncontrolled value
227
+ const [internalValue, setInternalValue] = useState(defaultValue);
228
+ // Use controlled value if provided, otherwise use internal state
229
+ const value = valueProp !== undefined ? valueProp : internalValue;
230
+ const onChange = onChangeProp !== null && onChangeProp !== void 0 ? onChangeProp : setInternalValue;
226
231
  // TODO: Clean up the types in these refs by enhancing the type system in useCallbackRef
227
232
  const getOptionLabelPropRef = useCallbackRef((opt) => getOptionLabelProp === null || getOptionLabelProp === void 0 ? void 0 : getOptionLabelProp(opt));
228
233
  const getOptionLabel = useCallback((opt) => {
229
234
  const maybe = getOptionLabelPropRef(opt);
230
235
  return maybe !== null && maybe !== void 0 ? maybe : opt.label;
231
236
  }, [getOptionLabelPropRef]);
237
+ // Initialize internal input value from defaultValue
238
+ const [internalInputValue, setInternalInputValue] = useState(() => {
239
+ if (multiple)
240
+ return "";
241
+ const initialValue = (defaultValue !== null && defaultValue !== void 0 ? defaultValue : valueProp);
242
+ if (!initialValue)
243
+ return "";
244
+ // Call getOptionLabelProp directly if provided, otherwise use label
245
+ const customLabel = getOptionLabelProp === null || getOptionLabelProp === void 0 ? void 0 : getOptionLabelProp(initialValue);
246
+ return customLabel !== null && customLabel !== void 0 ? customLabel : initialValue.label;
247
+ });
248
+ // Track previous value to detect when it changes
249
+ const prevValueRef = useRef(value);
250
+ // Sync internal input value with selected value only when value changes (not on user input)
251
+ useEffect(() => {
252
+ const isInputControlled = inputValueProp !== undefined;
253
+ if (isInputControlled || multiple)
254
+ return;
255
+ // Only update if the value actually changed
256
+ if (prevValueRef.current !== value) {
257
+ prevValueRef.current = value;
258
+ const currentValue = value;
259
+ setInternalInputValue(currentValue ? getOptionLabel(currentValue) : "");
260
+ }
261
+ }, [value, inputValueProp, multiple, getOptionLabel]);
262
+ // Use controlled inputValue if provided, otherwise use internal state
263
+ const isInputControlled = inputValueProp !== undefined;
264
+ const inputValue = isInputControlled ? inputValueProp : internalInputValue;
265
+ const onInputChange = onInputChangeProp !== null && onInputChangeProp !== void 0 ? onInputChangeProp : setInternalInputValue;
232
266
  const isOptionEqualToValueRef = useCallbackRef((a, b) => isOptionEqualToValue === null || isOptionEqualToValue === void 0 ? void 0 : isOptionEqualToValue(a, b));
233
267
  const equals = useCallback((a, b) => {
234
268
  const custom = isOptionEqualToValueRef(a, b);
@@ -484,7 +518,7 @@ function useAutocomplete(props) {
484
518
  const freeFormCreated = (_a = props.createFreeFormValue) === null || _a === void 0 ? void 0 : _a.call(props, inputText);
485
519
  if (!freeFormCreated)
486
520
  return false;
487
- props.onChange(freeFormCreated);
521
+ onChange(freeFormCreated);
488
522
  return true;
489
523
  }
490
524
  const tryRestoreInputToSelectedLabel = useCallback(() => {
@@ -664,6 +698,7 @@ function useAutocomplete(props) {
664
698
  activeIndex,
665
699
  setActiveIndex,
666
700
  listRef,
701
+ inputValue,
667
702
  // actions
668
703
  onSelection,
669
704
  onAction,
@@ -901,8 +936,8 @@ const AutocompleteRebuilt = forwardRef(AutocompleteRebuiltInternal);
901
936
  // eslint-disable-next-line max-statements
902
937
  function AutocompleteRebuiltInternal(props, forwardedRef) {
903
938
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
904
- const { inputValue, placeholder, disabled, error, invalid, description, size: sizeProp, loading = false, } = props;
905
- const { renderable, optionCount, persistentsHeaders, persistentsFooters, headerInteractiveCount, middleNavigableCount, getOptionLabel, isOptionSelected, refs, floatingStyles, context, getReferenceProps, getFloatingProps, getItemProps, activeIndex, open, listRef, onSelection, onAction, onInputChangeFromUser, onInputBlur, onInputFocus, onInputKeyDown, setReferenceElement, } = useAutocomplete(props);
939
+ const { placeholder, disabled, error, invalid, description, size: sizeProp, loading = false, } = props;
940
+ const { renderable, optionCount, persistentsHeaders, persistentsFooters, headerInteractiveCount, middleNavigableCount, getOptionLabel, isOptionSelected, refs, floatingStyles, context, getReferenceProps, getFloatingProps, getItemProps, activeIndex, open, listRef, inputValue, onSelection, onAction, onInputChangeFromUser, onInputBlur, onInputFocus, onInputKeyDown, setReferenceElement, } = useAutocomplete(props);
906
941
  const listboxId = React__default.useId();
907
942
  // Provides mount/unmount-aware transition styles for the floating element
908
943
  const { isMounted, styles: transitionStyles } = useTransitionStyles(context, {
@@ -918,7 +953,7 @@ function AutocompleteRebuiltInternal(props, forwardedRef) {
918
953
  onFocus: onInputFocus,
919
954
  onBlur: onInputBlur,
920
955
  });
921
- const inputProps = Object.assign(Object.assign(Object.assign(Object.assign({ version: 2, value: inputValue, onChange: props.readOnly ? undefined : onInputChangeFromUser }, (props.readOnly ? { onFocus: onInputFocus, onBlur: onInputBlur } : {})), { placeholder,
956
+ const inputProps = Object.assign(Object.assign(Object.assign(Object.assign({ version: 2, value: inputValue !== null && inputValue !== void 0 ? inputValue : "", onChange: props.readOnly ? undefined : onInputChangeFromUser }, (props.readOnly ? { onFocus: onInputFocus, onBlur: onInputBlur } : {})), { placeholder,
922
957
  disabled, readOnly: props.readOnly, error: error !== null && error !== void 0 ? error : undefined, name: props.name, invalid, autoComplete: "off", description, size: sizeProp ? sizeProp : undefined, prefix: props.prefix, suffix: props.suffix }), (props.readOnly ? {} : composedReferenceProps)), { role: "combobox", "aria-autocomplete": "list", "aria-expanded": open ? true : false, "aria-controls": listboxId, "aria-activedescendant": open && activeIndex != null
923
958
  ? `${listboxId}-item-${activeIndex}`
924
959
  : undefined });
@@ -56,6 +56,7 @@ export declare function useAutocomplete<Value extends OptionLike, Multiple exten
56
56
  activeIndex: number | null;
57
57
  setActiveIndex: (index: number | null) => void;
58
58
  listRef: React.MutableRefObject<(HTMLElement | null)[]>;
59
+ inputValue: string;
59
60
  onSelection: (option: Value) => void;
60
61
  onAction: (action: ActionConfig) => void;
61
62
  onInputChangeFromUser: (val: string) => void;
package/dist/styles.css CHANGED
@@ -6401,10 +6401,9 @@ a._7BLGtYNuJOU-.zgRx3ehZ2z8-:hover {
6401
6401
  -webkit-transform: translateY(0) scale(1);
6402
6402
  transform: translateY(0) scale(1);
6403
6403
  }
6404
- .-hmXAsAfH9U- {
6405
6404
 
6406
6405
  /* options */
6407
- option {
6406
+ .-hmXAsAfH9U- option {
6408
6407
  padding: 4px 0;
6409
6408
  padding: var(--space-smaller) 0;
6410
6409
  color: hsl(198, 35%, 21%);
@@ -6414,16 +6413,16 @@ a._7BLGtYNuJOU-.zgRx3ehZ2z8-:hover {
6414
6413
  cursor: pointer;
6415
6414
  }
6416
6415
 
6417
- option:hover,
6418
- option:focus,
6419
- option:focus-visible,
6420
- option:checked {
6416
+ .-hmXAsAfH9U- option:hover,
6417
+ .-hmXAsAfH9U- option:focus,
6418
+ .-hmXAsAfH9U- option:focus-visible,
6419
+ .-hmXAsAfH9U- option:checked {
6421
6420
  background-color: hsl(53, 21%, 93%);
6422
6421
  background-color: var(--color-surface--hover);
6423
6422
  }
6424
6423
 
6425
6424
  /* optgroup headers */
6426
- optgroup {
6425
+ .-hmXAsAfH9U- optgroup {
6427
6426
  padding: 8px 0 0 0;
6428
6427
  padding: var(--space-small) 0 0 0;
6429
6428
  color: hsl(197, 90%, 12%);
@@ -6438,36 +6437,35 @@ a._7BLGtYNuJOU-.zgRx3ehZ2z8-:hover {
6438
6437
  }
6439
6438
 
6440
6439
  /* Divider directly under the group label */
6441
- optgroup option:first-child {
6440
+ .-hmXAsAfH9U- optgroup option:first-child {
6442
6441
  border-top: 1px solid hsl(200, 13%, 87%);
6443
6442
  border-top: var(--border-base) solid var(--color-border);
6444
6443
  }
6445
6444
 
6446
6445
  /* Disabled groups and their options */
6447
- optgroup[disabled] {
6446
+ .-hmXAsAfH9U- optgroup[disabled] {
6448
6447
  color: hsl(0, 0%, 58%);
6449
6448
  color: var(--color-disabled);
6450
6449
  }
6451
- optgroup[disabled] option,
6452
- option[disabled] {
6450
+ .-hmXAsAfH9U- optgroup[disabled] option,
6451
+ .-hmXAsAfH9U- option[disabled] {
6453
6452
  color: hsl(0, 0%, 58%);
6454
6453
  color: var(--color-disabled);
6455
6454
  cursor: default;
6456
6455
  }
6457
6456
 
6458
- optgroup[disabled] option:focus,
6459
- optgroup[disabled] option:focus-visible,
6460
- optgroup[disabled] option:checked,
6461
- optgroup[disabled] option:hover,
6462
- option[disabled]:focus,
6463
- option[disabled]:focus-visible,
6464
- option[disabled]:checked,
6465
- option[disabled]:hover {
6457
+ .-hmXAsAfH9U- optgroup[disabled] option:focus,
6458
+ .-hmXAsAfH9U- optgroup[disabled] option:focus-visible,
6459
+ .-hmXAsAfH9U- optgroup[disabled] option:checked,
6460
+ .-hmXAsAfH9U- optgroup[disabled] option:hover,
6461
+ .-hmXAsAfH9U- option[disabled]:focus,
6462
+ .-hmXAsAfH9U- option[disabled]:focus-visible,
6463
+ .-hmXAsAfH9U- option[disabled]:checked,
6464
+ .-hmXAsAfH9U- option[disabled]:hover {
6466
6465
  background-color: transparent;
6467
6466
  }
6468
6467
 
6469
6468
  /* Animate the chevron rotation for the provided postfix class */
6470
- }
6471
6469
  .-hmXAsAfH9U- + ._5ST4c1fXDYU- svg {
6472
6470
  transition: -webkit-transform 100ms;
6473
6471
  transition: -webkit-transform var(--timing-quick);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components",
3
- "version": "6.103.2",
3
+ "version": "6.103.4-uncontroll-d4ef425.5+d4ef4259d",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -538,5 +538,5 @@
538
538
  "> 1%",
539
539
  "IE 10"
540
540
  ],
541
- "gitHead": "901fec98e7fac64fac23c225d7ac6da63d0739f5"
541
+ "gitHead": "d4ef4259debb09593eabc29a42b18faa208329b8"
542
542
  }