@jobber/components 6.103.3 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components",
3
- "version": "6.103.3",
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": "06b9bc94eb94f53486ddc9c41a978731163a85b5"
541
+ "gitHead": "d4ef4259debb09593eabc29a42b18faa208329b8"
542
542
  }