@spear-ai/spectral 1.14.2 → 1.15.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 (40) hide show
  1. package/dist/Button.d.ts +0 -6
  2. package/dist/Button.d.ts.map +1 -1
  3. package/dist/Button.js +36 -51
  4. package/dist/Button.js.map +1 -1
  5. package/dist/Checkbox.js +3 -3
  6. package/dist/Checkbox.js.map +1 -1
  7. package/dist/Combobox.js +3 -3
  8. package/dist/Combobox.js.map +1 -1
  9. package/dist/ControlGroup/ControlGroupSelect.d.ts +1 -1
  10. package/dist/ControlGroup/ControlGroupSelect.js +1 -1
  11. package/dist/ControlGroup/ControlGroupSelect.js.map +1 -1
  12. package/dist/ControlGroup.d.ts.map +1 -1
  13. package/dist/ControlGroup.js +3 -3
  14. package/dist/ControlGroup.js.map +1 -1
  15. package/dist/DateTimePicker.js +1 -1
  16. package/dist/DateTimePicker.js.map +1 -1
  17. package/dist/FormFieldMessage.js +1 -1
  18. package/dist/FormFieldMessage.js.map +1 -1
  19. package/dist/Input.js +3 -3
  20. package/dist/Input.js.map +1 -1
  21. package/dist/InputNumeric.d.ts +4 -2
  22. package/dist/InputNumeric.d.ts.map +1 -1
  23. package/dist/InputNumeric.js +3 -1
  24. package/dist/InputNumeric.js.map +1 -1
  25. package/dist/InputOTP.js +1 -1
  26. package/dist/InputOTP.js.map +1 -1
  27. package/dist/MultiSelect/MultiSelectBase.js +3 -3
  28. package/dist/MultiSelect/MultiSelectBase.js.map +1 -1
  29. package/dist/RadioGroup.js +3 -3
  30. package/dist/RadioGroup.js.map +1 -1
  31. package/dist/Select.js +3 -3
  32. package/dist/Select.js.map +1 -1
  33. package/dist/Switch.js +3 -3
  34. package/dist/Switch.js.map +1 -1
  35. package/dist/Textarea.d.ts +1 -1
  36. package/dist/Textarea.js +3 -3
  37. package/dist/Textarea.js.map +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/styles/spectral.css +1 -1
  40. package/package.json +1 -1
@@ -20,7 +20,7 @@ const renderFieldMessageContent = (message) => {
20
20
  }
21
21
  return String(message);
22
22
  };
23
- const FormFieldMessage = ({ ariaLive, className, containerClassName, dataTestId, id, message, messageReserveLines = 1, messageReserveSpace = true, role, tone }) => {
23
+ const FormFieldMessage = ({ ariaLive, className, containerClassName, dataTestId, id, message, messageReserveLines = 1, messageReserveSpace = false, role, tone }) => {
24
24
  const content = renderFieldMessageContent(message);
25
25
  const isVisible = Boolean(content);
26
26
  const reservedHeight = messageReserveSpace && messageReserveLines > 0 ? `${Math.max(messageReserveLines, 1) * 1.25}rem` : void 0;
@@ -1 +1 @@
1
- {"version":3,"file":"FormFieldMessage.js","names":[],"sources":["../src/components/FormFieldMessage/FormFieldMessage.tsx"],"sourcesContent":["import { cn } from '@utils/twUtils'\nimport { type ReactNode } from 'react'\n\nexport type FormFieldMessageValue = string | string[] | Record<string, unknown> | null | undefined\n\ninterface BaseFieldMessageProps {\n className?: string\n containerClassName?: string\n dataTestId?: string\n id: string\n message: FormFieldMessageValue\n messageReserveLines?: number\n messageReserveSpace?: boolean\n}\n\nconst renderFieldMessageContent = (message: FormFieldMessageValue): ReactNode => {\n if (!message) return null\n\n if (typeof message === 'string') {\n return message\n }\n\n if (Array.isArray(message)) {\n return message.join(', ')\n }\n\n if (typeof message === 'object') {\n if ('message' in message && typeof message.message === 'string') return message.message\n if ('error' in message && typeof message.error === 'string') return message.error\n if ('details' in message && typeof message.details === 'string') return message.details\n try {\n return JSON.stringify(message)\n } catch {\n return '[Circular]'\n }\n }\n\n return String(message)\n}\n\nconst FormFieldMessage = ({\n ariaLive,\n className,\n containerClassName,\n dataTestId,\n id,\n message,\n messageReserveLines = 1,\n messageReserveSpace = true,\n role,\n tone,\n}: BaseFieldMessageProps & {\n ariaLive: 'assertive' | 'polite'\n role: 'alert' | 'status'\n tone: 'danger' | 'warning'\n}) => {\n const content = renderFieldMessageContent(message)\n const isVisible = Boolean(content)\n const reservedHeight = messageReserveSpace && messageReserveLines > 0 ? `${Math.max(messageReserveLines, 1) * 1.25}rem` : undefined\n const toneClasses = tone === 'danger' ? 'text-danger-400!' : 'text-warning-400!'\n\n return (\n <div\n aria-hidden={!isVisible}\n className={cn(\n 'transition-[opacity,transform] duration-150 ease-out motion-reduce:transition-none',\n messageReserveSpace ? 'pt-1' : 'pt-0',\n isVisible ? 'translate-y-0 opacity-100' : '-translate-y-0.5 opacity-0',\n !messageReserveSpace && !isVisible && 'hidden',\n containerClassName,\n )}\n style={reservedHeight ? { minHeight: reservedHeight } : undefined}\n >\n <p aria-atomic={isVisible ? 'true' : undefined} aria-live={isVisible ? ariaLive : undefined} className={cn('m-0! text-sm leading-5 overflow-hidden', toneClasses, className)} data-testid={isVisible ? dataTestId : undefined} id={id} role={isVisible ? role : undefined}>\n {content}\n </p>\n </div>\n )\n}\n\nexport const ErrorMessage = ({ className, containerClassName, dataTestId = 'spectral-form-field-error-message', id, message, messageReserveLines, messageReserveSpace }: BaseFieldMessageProps) => {\n return <FormFieldMessage ariaLive='polite' className={className} containerClassName={containerClassName} dataTestId={dataTestId} id={id} message={message} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace} role='status' tone='danger' />\n}\n\nexport const WarningMessage = ({ className, containerClassName, dataTestId = 'spectral-form-field-warning-message', id, message, messageReserveLines, messageReserveSpace }: BaseFieldMessageProps) => {\n return <FormFieldMessage ariaLive='polite' className={className} containerClassName={containerClassName} dataTestId={dataTestId} id={id} message={message} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace} role='status' tone='warning' />\n}\n"],"mappings":";;;;;;AAeA,MAAM,6BAA6B,YAA8C;AAC/E,KAAI,CAAC,QAAS,QAAO;AAErB,KAAI,OAAO,YAAY,SACrB,QAAO;AAGT,KAAI,MAAM,QAAQ,QAAQ,CACxB,QAAO,QAAQ,KAAK,KAAK;AAG3B,KAAI,OAAO,YAAY,UAAU;AAC/B,MAAI,aAAa,WAAW,OAAO,QAAQ,YAAY,SAAU,QAAO,QAAQ;AAChF,MAAI,WAAW,WAAW,OAAO,QAAQ,UAAU,SAAU,QAAO,QAAQ;AAC5E,MAAI,aAAa,WAAW,OAAO,QAAQ,YAAY,SAAU,QAAO,QAAQ;AAChF,MAAI;AACF,UAAO,KAAK,UAAU,QAAQ;UACxB;AACN,UAAO;;;AAIX,QAAO,OAAO,QAAQ;;AAGxB,MAAM,oBAAoB,EACxB,UACA,WACA,oBACA,YACA,IACA,SACA,sBAAsB,GACtB,sBAAsB,MACtB,MACA,WAKI;CACJ,MAAM,UAAU,0BAA0B,QAAQ;CAClD,MAAM,YAAY,QAAQ,QAAQ;CAClC,MAAM,iBAAiB,uBAAuB,sBAAsB,IAAI,GAAG,KAAK,IAAI,qBAAqB,EAAE,GAAG,KAAK,OAAO;CAC1H,MAAM,cAAc,SAAS,WAAW,qBAAqB;AAE7D,QACE,oBAAC,OAAD;EACE,eAAa,CAAC;EACd,WAAW,GACT,sFACA,sBAAsB,SAAS,QAC/B,YAAY,8BAA8B,8BAC1C,CAAC,uBAAuB,CAAC,aAAa,UACtC,mBACD;EACD,OAAO,iBAAiB,EAAE,WAAW,gBAAgB,GAAG;YAExD,oBAAC,KAAD;GAAG,eAAa,YAAY,SAAS;GAAW,aAAW,YAAY,WAAW;GAAW,WAAW,GAAG,0CAA0C,aAAa,UAAU;GAAE,eAAa,YAAY,aAAa;GAAe;GAAI,MAAM,YAAY,OAAO;aAC7P;GACC;EACA;;AAIV,MAAa,gBAAgB,EAAE,WAAW,oBAAoB,aAAa,qCAAqC,IAAI,SAAS,qBAAqB,0BAAiD;AACjM,QAAO,oBAAC,kBAAD;EAAkB,UAAS;EAAoB;EAA+B;EAAgC;EAAgB;EAAa;EAA8B;EAA0C;EAAqB,MAAK;EAAS,MAAK;EAAW;;AAG/Q,MAAa,kBAAkB,EAAE,WAAW,oBAAoB,aAAa,uCAAuC,IAAI,SAAS,qBAAqB,0BAAiD;AACrM,QAAO,oBAAC,kBAAD;EAAkB,UAAS;EAAoB;EAA+B;EAAgC;EAAgB;EAAa;EAA8B;EAA0C;EAAqB,MAAK;EAAS,MAAK;EAAY"}
1
+ {"version":3,"file":"FormFieldMessage.js","names":[],"sources":["../src/components/FormFieldMessage/FormFieldMessage.tsx"],"sourcesContent":["import { cn } from '@utils/twUtils'\nimport { type ReactNode } from 'react'\n\nexport type FormFieldMessageValue = string | string[] | Record<string, unknown> | null | undefined\n\ninterface BaseFieldMessageProps {\n className?: string\n containerClassName?: string\n dataTestId?: string\n id: string\n message: FormFieldMessageValue\n messageReserveLines?: number\n messageReserveSpace?: boolean\n}\n\nconst renderFieldMessageContent = (message: FormFieldMessageValue): ReactNode => {\n if (!message) return null\n\n if (typeof message === 'string') {\n return message\n }\n\n if (Array.isArray(message)) {\n return message.join(', ')\n }\n\n if (typeof message === 'object') {\n if ('message' in message && typeof message.message === 'string') return message.message\n if ('error' in message && typeof message.error === 'string') return message.error\n if ('details' in message && typeof message.details === 'string') return message.details\n try {\n return JSON.stringify(message)\n } catch {\n return '[Circular]'\n }\n }\n\n return String(message)\n}\n\nconst FormFieldMessage = ({\n ariaLive,\n className,\n containerClassName,\n dataTestId,\n id,\n message,\n messageReserveLines = 1,\n messageReserveSpace = false,\n role,\n tone,\n}: BaseFieldMessageProps & {\n ariaLive: 'assertive' | 'polite'\n role: 'alert' | 'status'\n tone: 'danger' | 'warning'\n}) => {\n const content = renderFieldMessageContent(message)\n const isVisible = Boolean(content)\n const reservedHeight = messageReserveSpace && messageReserveLines > 0 ? `${Math.max(messageReserveLines, 1) * 1.25}rem` : undefined\n const toneClasses = tone === 'danger' ? 'text-danger-400!' : 'text-warning-400!'\n\n return (\n <div\n aria-hidden={!isVisible}\n className={cn(\n 'transition-[opacity,transform] duration-150 ease-out motion-reduce:transition-none',\n messageReserveSpace ? 'pt-1' : 'pt-0',\n isVisible ? 'translate-y-0 opacity-100' : '-translate-y-0.5 opacity-0',\n !messageReserveSpace && !isVisible && 'hidden',\n containerClassName,\n )}\n style={reservedHeight ? { minHeight: reservedHeight } : undefined}\n >\n <p aria-atomic={isVisible ? 'true' : undefined} aria-live={isVisible ? ariaLive : undefined} className={cn('m-0! text-sm leading-5 overflow-hidden', toneClasses, className)} data-testid={isVisible ? dataTestId : undefined} id={id} role={isVisible ? role : undefined}>\n {content}\n </p>\n </div>\n )\n}\n\nexport const ErrorMessage = ({ className, containerClassName, dataTestId = 'spectral-form-field-error-message', id, message, messageReserveLines, messageReserveSpace }: BaseFieldMessageProps) => {\n return <FormFieldMessage ariaLive='polite' className={className} containerClassName={containerClassName} dataTestId={dataTestId} id={id} message={message} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace} role='status' tone='danger' />\n}\n\nexport const WarningMessage = ({ className, containerClassName, dataTestId = 'spectral-form-field-warning-message', id, message, messageReserveLines, messageReserveSpace }: BaseFieldMessageProps) => {\n return <FormFieldMessage ariaLive='polite' className={className} containerClassName={containerClassName} dataTestId={dataTestId} id={id} message={message} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace} role='status' tone='warning' />\n}\n"],"mappings":";;;;;;AAeA,MAAM,6BAA6B,YAA8C;AAC/E,KAAI,CAAC,QAAS,QAAO;AAErB,KAAI,OAAO,YAAY,SACrB,QAAO;AAGT,KAAI,MAAM,QAAQ,QAAQ,CACxB,QAAO,QAAQ,KAAK,KAAK;AAG3B,KAAI,OAAO,YAAY,UAAU;AAC/B,MAAI,aAAa,WAAW,OAAO,QAAQ,YAAY,SAAU,QAAO,QAAQ;AAChF,MAAI,WAAW,WAAW,OAAO,QAAQ,UAAU,SAAU,QAAO,QAAQ;AAC5E,MAAI,aAAa,WAAW,OAAO,QAAQ,YAAY,SAAU,QAAO,QAAQ;AAChF,MAAI;AACF,UAAO,KAAK,UAAU,QAAQ;UACxB;AACN,UAAO;;;AAIX,QAAO,OAAO,QAAQ;;AAGxB,MAAM,oBAAoB,EACxB,UACA,WACA,oBACA,YACA,IACA,SACA,sBAAsB,GACtB,sBAAsB,OACtB,MACA,WAKI;CACJ,MAAM,UAAU,0BAA0B,QAAQ;CAClD,MAAM,YAAY,QAAQ,QAAQ;CAClC,MAAM,iBAAiB,uBAAuB,sBAAsB,IAAI,GAAG,KAAK,IAAI,qBAAqB,EAAE,GAAG,KAAK,OAAO;CAC1H,MAAM,cAAc,SAAS,WAAW,qBAAqB;AAE7D,QACE,oBAAC,OAAD;EACE,eAAa,CAAC;EACd,WAAW,GACT,sFACA,sBAAsB,SAAS,QAC/B,YAAY,8BAA8B,8BAC1C,CAAC,uBAAuB,CAAC,aAAa,UACtC,mBACD;EACD,OAAO,iBAAiB,EAAE,WAAW,gBAAgB,GAAG;YAExD,oBAAC,KAAD;GAAG,eAAa,YAAY,SAAS;GAAW,aAAW,YAAY,WAAW;GAAW,WAAW,GAAG,0CAA0C,aAAa,UAAU;GAAE,eAAa,YAAY,aAAa;GAAe;GAAI,MAAM,YAAY,OAAO;aAC7P;GACC;EACA;;AAIV,MAAa,gBAAgB,EAAE,WAAW,oBAAoB,aAAa,qCAAqC,IAAI,SAAS,qBAAqB,0BAAiD;AACjM,QAAO,oBAAC,kBAAD;EAAkB,UAAS;EAAoB;EAA+B;EAAgC;EAAgB;EAAa;EAA8B;EAA0C;EAAqB,MAAK;EAAS,MAAK;EAAW;;AAG/Q,MAAa,kBAAkB,EAAE,WAAW,oBAAoB,aAAa,uCAAuC,IAAI,SAAS,qBAAqB,0BAAiD;AACrM,QAAO,oBAAC,kBAAD;EAAkB,UAAS;EAAoB;EAA+B;EAAgC;EAAgB;EAAa;EAA8B;EAA0C;EAAqB,MAAK;EAAS,MAAK;EAAY"}
package/dist/Input.js CHANGED
@@ -38,7 +38,7 @@ const getAutoCompleteValue = (type) => {
38
38
  }[type] || "off";
39
39
  };
40
40
  const Input = (allProps) => {
41
- const { className, clearOnFocus = false, defaultValue, disabled, endIcon, errorMessage, id, label, labelClassName, messageReserveLines = 1, messageReserveSpace = true, name, onBlur, onChange, onFocus, placeholder, prefix, ref, required, showClearButton = false, showStateIcon = true, startIcon, state = "default", suppressHydrationWarning = true, type = "text", value: valueProp, warningMessage, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...props } = allProps;
41
+ const { className, clearOnFocus = false, defaultValue, disabled, endIcon, errorMessage, id, label, labelClassName, messageReserveLines = 1, messageReserveSpace = false, name, onBlur, onChange, onFocus, placeholder, prefix, ref, required, showClearButton = false, showStateIcon = true, startIcon, state = "default", suppressHydrationWarning = true, type = "text", value: valueProp, warningMessage, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...props } = allProps;
42
42
  const inputId = useFormFieldId(id, name);
43
43
  const errorMessageId = `${inputId}-error`;
44
44
  const warningMessageId = `${inputId}-warning`;
@@ -185,14 +185,14 @@ const Input = (allProps) => {
185
185
  id: errorMessageId,
186
186
  message: isInvalid ? errorMessage ?? null : null,
187
187
  messageReserveLines,
188
- messageReserveSpace
188
+ messageReserveSpace: messageReserveSpace && state === "error"
189
189
  }),
190
190
  /* @__PURE__ */ jsx(WarningMessage, {
191
191
  dataTestId: "spectral-input-warning-message",
192
192
  id: warningMessageId,
193
193
  message: state === "warning" ? warningMessage ?? null : null,
194
194
  messageReserveLines,
195
- messageReserveSpace
195
+ messageReserveSpace: messageReserveSpace && state === "warning"
196
196
  })
197
197
  ]
198
198
  })]
package/dist/Input.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"Input.js","names":[],"sources":["../src/components/Input/Input.tsx"],"sourcesContent":["import { CheckCircleIcon, CloseCircleIcon, ErrorIcon, EyeClosedIcon, EyeOpenIcon, LoaderIcon, WarningIcon } from '@components/Icons'\nimport { Label } from '@components/Label/Label'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { ErrorMessage, WarningMessage, getAriaProps, getFormFieldCSSProperties, getInputClasses, useFormFieldId, useFormFieldState, type BaseFormFieldProps } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useRef, type ChangeEvent, type CSSProperties, type FocusEvent, type InputHTMLAttributes, type ReactElement, type Ref } from 'react'\nimport { useClearOnFocus, usePasswordVisibility, usePrefixWidth } from './InputUtils'\n\nexport type InputType = 'text' | 'email' | 'url' | 'tel' | 'password' | 'number' | 'date' | 'datetime-local'\n\nexport type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'id' | 'onChange'> &\n BaseFormFieldProps & {\n className?: string\n clearOnFocus?: boolean\n endIcon?: ReactElement\n labelClassName?: string\n onBlur?: (e: FocusEvent<HTMLInputElement>) => void\n onChange?: (value: string) => void\n onFocus?: (e: FocusEvent<HTMLInputElement>) => void\n placeholder?: string\n prefix?: string\n showClearButton?: boolean\n showStateIcon?: boolean\n startIcon?: ReactElement\n suppressHydrationWarning?: boolean\n type?: InputType\n value?: string\n warningMessage?: BaseFormFieldProps['errorMessage']\n }\n\nconst mergeRefs = <T,>(...refs: (Ref<T> | undefined)[]): Ref<T> => {\n return (value: T | null) => {\n refs.forEach((ref) => {\n if (!ref) return\n if (typeof ref === 'function') {\n ref(value)\n } else {\n ;(ref as { current: T | null }).current = value\n }\n })\n }\n}\n\nconst getAutoCompleteValue = (type: InputType): string => {\n const autoCompleteMap: Record<InputType, string> = {\n date: 'off',\n email: 'email',\n number: 'off',\n password: 'current-password',\n tel: 'tel',\n text: 'off',\n url: 'url',\n 'datetime-local': 'off',\n }\n return autoCompleteMap[type] || 'off'\n}\n\nexport const Input = (\n allProps: InputProps & {\n ref?: Ref<HTMLInputElement>\n },\n): ReactElement => {\n const {\n className,\n clearOnFocus = false,\n defaultValue,\n disabled,\n endIcon,\n errorMessage,\n id,\n label,\n labelClassName,\n messageReserveLines = 1,\n messageReserveSpace = true,\n name,\n onBlur,\n onChange,\n onFocus,\n placeholder,\n prefix,\n ref,\n required,\n showClearButton = false,\n showStateIcon = true,\n startIcon,\n state = 'default',\n suppressHydrationWarning = true,\n type = 'text',\n value: valueProp,\n warningMessage,\n 'aria-label': ariaLabel,\n 'aria-describedby': ariaDescribedBy,\n ...props\n } = allProps\n const inputId = useFormFieldId(id, name)\n const errorMessageId = `${inputId}-error`\n const warningMessageId = `${inputId}-warning`\n const { isDisabled, isLoading, isInvalid } = useFormFieldState(disabled, state)\n const messageId = state === 'error' ? errorMessageId : state === 'warning' && warningMessage ? warningMessageId : undefined\n const ariaProps = getAriaProps(state, ariaDescribedBy, required, messageId)\n const normalizedDefaultValue = typeof defaultValue === 'string' ? defaultValue : defaultValue !== undefined && defaultValue !== null ? String(defaultValue) : ''\n const [value, setValue] = useUncontrolledState<string>({\n value: valueProp,\n defaultValue: normalizedDefaultValue,\n onChange,\n })\n\n const internalRef = useRef<HTMLInputElement>(null)\n const inputRef = mergeRefs(ref, internalRef)\n\n const { isVisible, toggleVisibility, inputType } = usePasswordVisibility()\n const { prefixWidth, prefixRef } = usePrefixWidth(prefix)\n const { handleFocus: clearOnFocusHandler } = useClearOnFocus(clearOnFocus, (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value))\n\n const handleBlur = useCallback(\n (e: FocusEvent<HTMLInputElement>): void => {\n onBlur?.(e)\n },\n [onBlur],\n )\n\n const handleFocus = useCallback(\n (e: FocusEvent<HTMLInputElement>): void => {\n clearOnFocusHandler(e, onFocus)\n },\n [clearOnFocusHandler, onFocus],\n )\n\n const handleChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>): void => {\n const newValue = e.target.value\n setValue(newValue)\n },\n [setValue],\n )\n\n const handleClear = useCallback((): void => {\n const element = internalRef.current\n if (element) {\n setValue('')\n element.focus()\n }\n }, [setValue])\n\n const showClearButtonNow = showClearButton && value.length > 0\n\n const getEndIcon = (): ReactElement | null => {\n const iconClasses = 'absolute right-4 top-1/2 -translate-y-1/2 text-input-icon hover:text-input-icon--hover focus:outline-none cursor-pointer'\n\n const iconComponents = {\n password: () => (\n <button aria-controls={inputId} aria-label={isVisible ? `Hide ${label ?? 'password'}` : `Show ${label ?? 'password'}`} aria-pressed={isVisible} className={iconClasses} data-testid='spectral-input-password-toggle' onClick={toggleVisibility} type='button'>\n {isVisible ? <EyeClosedIcon size={22} /> : <EyeOpenIcon size={22} />}\n </button>\n ),\n clear: () => (\n <button aria-label={String(`Clear ${label ?? 'input'}`)} className={iconClasses} data-testid='spectral-input-clear-button' onClick={handleClear} type='button'>\n <CloseCircleIcon size={24} />\n </button>\n ),\n loading: () => (\n <div className='right-4 text-input-icon absolute top-1/2 -translate-y-1/2' data-testid='spectral-input-loading-icon'>\n <LoaderIcon size={24} />\n </div>\n ),\n error: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-danger-400' data-testid='spectral-input-error-icon'>\n <ErrorIcon size={24} />\n </div>\n ),\n success: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-success-400' data-testid='spectral-input-success-icon'>\n <CheckCircleIcon size={24} />\n </div>\n ),\n warning: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-warning-400' data-testid='spectral-input-warning-icon'>\n <WarningIcon size={24} />\n </div>\n ),\n }\n\n if (endIcon) return <div className='right-4 text-input-icon absolute top-1/2 -translate-y-1/2'>{endIcon}</div>\n if (type === 'password') return iconComponents.password()\n if (showClearButtonNow) return iconComponents.clear()\n if (isLoading) return iconComponents.loading()\n if (!showStateIcon && (state === 'success' || state === 'warning' || state === 'error')) return null\n if (state === 'success') return iconComponents.success()\n if (state === 'warning') return iconComponents.warning()\n if (state === 'error') return iconComponents.error()\n\n return null\n }\n\n const getStartIcon = (): ReactElement | null => {\n if (startIcon) return startIcon\n return null\n }\n\n const usesTabularNumbers = type === 'number' || type === 'date' || type === 'datetime-local'\n const inputClasses = cn(getInputClasses(state, className), '[text-indent:var(--prefix-width)]', showClearButtonNow && 'pr-10', usesTabularNumbers && 'tabular-nums')\n\n const prefixClasses = cn('inset-y-0 left-4 text-base pointer-events-none absolute flex items-center text-input-text-prefix opacity-100 peer-disabled:opacity-50')\n\n return (\n <div className='space-y-1.5 w-full' data-testid='spectral-input-container'>\n {label && (\n <Label className={cn('mb-2 block', labelClassName, isDisabled && 'cursor-not-allowed text-input-text--disabled placeholder:text-input-text-placeholder')} data-testid='spectral-input-label' htmlFor={inputId}>\n {label}\n </Label>\n )}\n <div className='relative' data-testid='spectral-input-wrapper'>\n <div className='relative'>\n {getStartIcon()}\n {prefix && (\n <span ref={prefixRef} className={prefixClasses}>\n {prefix}\n </span>\n )}\n <input\n aria-label={ariaLabel ?? label}\n autoComplete={getAutoCompleteValue(type)}\n className={inputClasses}\n data-state={state}\n data-testid='spectral-input'\n disabled={isDisabled}\n id={inputId}\n name={name}\n onBlur={handleBlur}\n onChange={handleChange}\n onFocus={handleFocus}\n placeholder={placeholder ?? label}\n ref={inputRef}\n required={required}\n style={\n getFormFieldCSSProperties({\n '--prefix-width': prefix ? `${prefixWidth}px` : '0',\n }) as CSSProperties\n }\n suppressHydrationWarning={suppressHydrationWarning}\n type={type === 'password' ? inputType : type}\n value={value}\n {...ariaProps}\n {...props}\n />\n {getEndIcon()}\n </div>\n\n <ErrorMessage dataTestId='spectral-input-error-message' id={errorMessageId} message={isInvalid ? (errorMessage ?? null) : null} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace} />\n <WarningMessage dataTestId='spectral-input-warning-message' id={warningMessageId} message={state === 'warning' ? (warningMessage ?? null) : null} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace} />\n </div>\n </div>\n )\n}\nInput.displayName = 'Input'\n"],"mappings":"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"}
1
+ {"version":3,"file":"Input.js","names":[],"sources":["../src/components/Input/Input.tsx"],"sourcesContent":["import { CheckCircleIcon, CloseCircleIcon, ErrorIcon, EyeClosedIcon, EyeOpenIcon, LoaderIcon, WarningIcon } from '@components/Icons'\nimport { Label } from '@components/Label/Label'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { ErrorMessage, WarningMessage, getAriaProps, getFormFieldCSSProperties, getInputClasses, useFormFieldId, useFormFieldState, type BaseFormFieldProps } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useRef, type ChangeEvent, type CSSProperties, type FocusEvent, type InputHTMLAttributes, type ReactElement, type Ref } from 'react'\nimport { useClearOnFocus, usePasswordVisibility, usePrefixWidth } from './InputUtils'\n\nexport type InputType = 'text' | 'email' | 'url' | 'tel' | 'password' | 'number' | 'date' | 'datetime-local'\n\nexport type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'id' | 'onChange'> &\n BaseFormFieldProps & {\n className?: string\n clearOnFocus?: boolean\n endIcon?: ReactElement\n labelClassName?: string\n onBlur?: (e: FocusEvent<HTMLInputElement>) => void\n onChange?: (value: string) => void\n onFocus?: (e: FocusEvent<HTMLInputElement>) => void\n placeholder?: string\n prefix?: string\n showClearButton?: boolean\n showStateIcon?: boolean\n startIcon?: ReactElement\n suppressHydrationWarning?: boolean\n type?: InputType\n value?: string\n warningMessage?: BaseFormFieldProps['errorMessage']\n }\n\nconst mergeRefs = <T,>(...refs: (Ref<T> | undefined)[]): Ref<T> => {\n return (value: T | null) => {\n refs.forEach((ref) => {\n if (!ref) return\n if (typeof ref === 'function') {\n ref(value)\n } else {\n ;(ref as { current: T | null }).current = value\n }\n })\n }\n}\n\nconst getAutoCompleteValue = (type: InputType): string => {\n const autoCompleteMap: Record<InputType, string> = {\n date: 'off',\n email: 'email',\n number: 'off',\n password: 'current-password',\n tel: 'tel',\n text: 'off',\n url: 'url',\n 'datetime-local': 'off',\n }\n return autoCompleteMap[type] || 'off'\n}\n\nexport const Input = (\n allProps: InputProps & {\n ref?: Ref<HTMLInputElement>\n },\n): ReactElement => {\n const {\n className,\n clearOnFocus = false,\n defaultValue,\n disabled,\n endIcon,\n errorMessage,\n id,\n label,\n labelClassName,\n messageReserveLines = 1,\n messageReserveSpace = false,\n name,\n onBlur,\n onChange,\n onFocus,\n placeholder,\n prefix,\n ref,\n required,\n showClearButton = false,\n showStateIcon = true,\n startIcon,\n state = 'default',\n suppressHydrationWarning = true,\n type = 'text',\n value: valueProp,\n warningMessage,\n 'aria-label': ariaLabel,\n 'aria-describedby': ariaDescribedBy,\n ...props\n } = allProps\n const inputId = useFormFieldId(id, name)\n const errorMessageId = `${inputId}-error`\n const warningMessageId = `${inputId}-warning`\n const { isDisabled, isLoading, isInvalid } = useFormFieldState(disabled, state)\n const messageId = state === 'error' ? errorMessageId : state === 'warning' && warningMessage ? warningMessageId : undefined\n const ariaProps = getAriaProps(state, ariaDescribedBy, required, messageId)\n const normalizedDefaultValue = typeof defaultValue === 'string' ? defaultValue : defaultValue !== undefined && defaultValue !== null ? String(defaultValue) : ''\n const [value, setValue] = useUncontrolledState<string>({\n value: valueProp,\n defaultValue: normalizedDefaultValue,\n onChange,\n })\n\n const internalRef = useRef<HTMLInputElement>(null)\n const inputRef = mergeRefs(ref, internalRef)\n\n const { isVisible, toggleVisibility, inputType } = usePasswordVisibility()\n const { prefixWidth, prefixRef } = usePrefixWidth(prefix)\n const { handleFocus: clearOnFocusHandler } = useClearOnFocus(clearOnFocus, (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value))\n\n const handleBlur = useCallback(\n (e: FocusEvent<HTMLInputElement>): void => {\n onBlur?.(e)\n },\n [onBlur],\n )\n\n const handleFocus = useCallback(\n (e: FocusEvent<HTMLInputElement>): void => {\n clearOnFocusHandler(e, onFocus)\n },\n [clearOnFocusHandler, onFocus],\n )\n\n const handleChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>): void => {\n const newValue = e.target.value\n setValue(newValue)\n },\n [setValue],\n )\n\n const handleClear = useCallback((): void => {\n const element = internalRef.current\n if (element) {\n setValue('')\n element.focus()\n }\n }, [setValue])\n\n const showClearButtonNow = showClearButton && value.length > 0\n\n const getEndIcon = (): ReactElement | null => {\n const iconClasses = 'absolute right-4 top-1/2 -translate-y-1/2 text-input-icon hover:text-input-icon--hover focus:outline-none cursor-pointer'\n\n const iconComponents = {\n password: () => (\n <button aria-controls={inputId} aria-label={isVisible ? `Hide ${label ?? 'password'}` : `Show ${label ?? 'password'}`} aria-pressed={isVisible} className={iconClasses} data-testid='spectral-input-password-toggle' onClick={toggleVisibility} type='button'>\n {isVisible ? <EyeClosedIcon size={22} /> : <EyeOpenIcon size={22} />}\n </button>\n ),\n clear: () => (\n <button aria-label={String(`Clear ${label ?? 'input'}`)} className={iconClasses} data-testid='spectral-input-clear-button' onClick={handleClear} type='button'>\n <CloseCircleIcon size={24} />\n </button>\n ),\n loading: () => (\n <div className='right-4 text-input-icon absolute top-1/2 -translate-y-1/2' data-testid='spectral-input-loading-icon'>\n <LoaderIcon size={24} />\n </div>\n ),\n error: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-danger-400' data-testid='spectral-input-error-icon'>\n <ErrorIcon size={24} />\n </div>\n ),\n success: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-success-400' data-testid='spectral-input-success-icon'>\n <CheckCircleIcon size={24} />\n </div>\n ),\n warning: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-warning-400' data-testid='spectral-input-warning-icon'>\n <WarningIcon size={24} />\n </div>\n ),\n }\n\n if (endIcon) return <div className='right-4 text-input-icon absolute top-1/2 -translate-y-1/2'>{endIcon}</div>\n if (type === 'password') return iconComponents.password()\n if (showClearButtonNow) return iconComponents.clear()\n if (isLoading) return iconComponents.loading()\n if (!showStateIcon && (state === 'success' || state === 'warning' || state === 'error')) return null\n if (state === 'success') return iconComponents.success()\n if (state === 'warning') return iconComponents.warning()\n if (state === 'error') return iconComponents.error()\n\n return null\n }\n\n const getStartIcon = (): ReactElement | null => {\n if (startIcon) return startIcon\n return null\n }\n\n const usesTabularNumbers = type === 'number' || type === 'date' || type === 'datetime-local'\n const inputClasses = cn(getInputClasses(state, className), '[text-indent:var(--prefix-width)]', showClearButtonNow && 'pr-10', usesTabularNumbers && 'tabular-nums')\n\n const prefixClasses = cn('inset-y-0 left-4 text-base pointer-events-none absolute flex items-center text-input-text-prefix opacity-100 peer-disabled:opacity-50')\n\n return (\n <div className='space-y-1.5 w-full' data-testid='spectral-input-container'>\n {label && (\n <Label className={cn('mb-2 block', labelClassName, isDisabled && 'cursor-not-allowed text-input-text--disabled placeholder:text-input-text-placeholder')} data-testid='spectral-input-label' htmlFor={inputId}>\n {label}\n </Label>\n )}\n <div className='relative' data-testid='spectral-input-wrapper'>\n <div className='relative'>\n {getStartIcon()}\n {prefix && (\n <span ref={prefixRef} className={prefixClasses}>\n {prefix}\n </span>\n )}\n <input\n aria-label={ariaLabel ?? label}\n autoComplete={getAutoCompleteValue(type)}\n className={inputClasses}\n data-state={state}\n data-testid='spectral-input'\n disabled={isDisabled}\n id={inputId}\n name={name}\n onBlur={handleBlur}\n onChange={handleChange}\n onFocus={handleFocus}\n placeholder={placeholder ?? label}\n ref={inputRef}\n required={required}\n style={\n getFormFieldCSSProperties({\n '--prefix-width': prefix ? `${prefixWidth}px` : '0',\n }) as CSSProperties\n }\n suppressHydrationWarning={suppressHydrationWarning}\n type={type === 'password' ? inputType : type}\n value={value}\n {...ariaProps}\n {...props}\n />\n {getEndIcon()}\n </div>\n\n <ErrorMessage dataTestId='spectral-input-error-message' id={errorMessageId} message={isInvalid ? (errorMessage ?? null) : null} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace && state === 'error'} />\n <WarningMessage dataTestId='spectral-input-warning-message' id={warningMessageId} message={state === 'warning' ? (warningMessage ?? null) : null} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace && state === 'warning'} />\n </div>\n </div>\n )\n}\nInput.displayName = 'Input'\n"],"mappings":"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"}
@@ -6,8 +6,8 @@ import { ReactElement } from "react";
6
6
  type InputNumericProps = Omit<InputProps, 'inputMode' | 'onChange' | 'pattern' | 'type'> & {
7
7
  allowDecimal?: boolean;
8
8
  allowNegative?: boolean;
9
- locale?: string; /** Number of message lines to reserve (default: 1 via Input). */
10
- messageReserveLines?: number; /** Whether to keep message space reserved when hidden (default: true via Input). */
9
+ locale?: string; /** Number of message lines to reserve (default: 1). */
10
+ messageReserveLines?: number; /** Whether to keep message space reserved when hidden (default: false). */
11
11
  messageReserveSpace?: boolean;
12
12
  onChange?: (value: string) => void;
13
13
  value?: string;
@@ -19,6 +19,8 @@ declare function InputNumeric({
19
19
  defaultValue,
20
20
  locale,
21
21
  max,
22
+ messageReserveLines,
23
+ messageReserveSpace,
22
24
  min,
23
25
  onChange,
24
26
  onKeyDown,
@@ -1 +1 @@
1
- {"version":3,"file":"InputNumeric.d.ts","names":[],"sources":["../src/components/InputNumeric/InputNumeric.tsx"],"mappings":";;;;;KA2FY,iBAAA,GAAoB,IAAA,CAAK,UAAA;EACnC,YAAA;EACA,aAAA;EACA,MAAA;EAEA,mBAAA,WAL8B;EAO9B,mBAAA;EACA,QAAA,IAAY,KAAA;EACZ,KAAA;AAAA;AAAA;EAG6B,YAAA;EAAqB,aAAA;EAAuB,SAAA;EAAW,YAAA;EAAmB,MAAA;EAAQ,GAAA;EAAK,GAAA;EAAK,QAAA;EAAU,SAAA;EAAW,OAAA;EAAS,IAAA;EAAM,KAAA,EAAO,SAAA;EAAA,GAAc;AAAA,GAAS,iBAAA,GAAoB,YAAA;AAAA"}
1
+ {"version":3,"file":"InputNumeric.d.ts","names":[],"sources":["../src/components/InputNumeric/InputNumeric.tsx"],"mappings":";;;;;KA2FY,iBAAA,GAAoB,IAAA,CAAK,UAAA;EACnC,YAAA;EACA,aAAA;EACA,MAAA;EAEA,mBAAA,WAL8B;EAO9B,mBAAA;EACA,QAAA,IAAY,KAAA;EACZ,KAAA;AAAA;AAAA;EAG6B,YAAA;EAAqB,aAAA;EAAuB,SAAA;EAAW,YAAA;EAAmB,MAAA;EAAQ,GAAA;EAAK,mBAAA;EAAyB,mBAAA;EAA6B,GAAA;EAAK,QAAA;EAAU,SAAA;EAAW,OAAA;EAAS,IAAA;EAAM,KAAA,EAAO,SAAA;EAAA,GAAc;AAAA,GAAS,iBAAA,GAAoB,YAAA;AAAA"}
@@ -77,7 +77,7 @@ const roundToPrecision = (value, precision) => {
77
77
  const factor = 10 ** precision;
78
78
  return Math.round(value * factor) / factor;
79
79
  };
80
- const InputNumeric = ({ allowDecimal = true, allowNegative = false, className, defaultValue = "", locale, max, min, onChange, onKeyDown, onPaste, step, value: valueProp, ...props }) => {
80
+ const InputNumeric = ({ allowDecimal = true, allowNegative = false, className, defaultValue = "", locale, max, messageReserveLines = 1, messageReserveSpace = false, min, onChange, onKeyDown, onPaste, step, value: valueProp, ...props }) => {
81
81
  const [value, setValue] = useUncontrolledState({
82
82
  value: valueProp,
83
83
  defaultValue: typeof defaultValue === "string" ? defaultValue : defaultValue !== void 0 && defaultValue !== null ? String(defaultValue) : "",
@@ -157,6 +157,8 @@ const InputNumeric = ({ allowDecimal = true, allowNegative = false, className, d
157
157
  className: cn("tabular-nums", className),
158
158
  inputMode,
159
159
  max,
160
+ messageReserveLines,
161
+ messageReserveSpace,
160
162
  min,
161
163
  onChange: handleChange,
162
164
  onKeyDown: handleKeyDown,
@@ -1 +1 @@
1
- {"version":3,"file":"InputNumeric.js","names":[],"sources":["../src/components/InputNumeric/InputNumeric.tsx"],"sourcesContent":["import { Input, type InputProps } from '@components/Input/Input'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, type ClipboardEvent, type KeyboardEvent, type ReactElement } from 'react'\n\ntype NumericKeyDownEvent = KeyboardEvent<HTMLInputElement>\ntype NumericPasteEvent = ClipboardEvent<HTMLInputElement>\n\nconst DIGIT_REGEX = /^\\d$/\nconst DISALLOWED_SPECIAL_KEYS = new Set(['e', 'E', '+'])\nconst ALLOWED_CONTROL_KEYS = new Set(['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'Tab', 'Enter', 'Escape'])\n\nconst getDecimalSeparator = (locale?: string): string => {\n try {\n const formattedNumber = new Intl.NumberFormat(locale).formatToParts(1.1)\n return formattedNumber.find((part) => part.type === 'decimal')?.value ?? '.'\n } catch {\n return '.'\n }\n}\n\nconst normalizeDecimalSeparator = (value: string, decimalSeparator: string): string => {\n if (decimalSeparator === '.') return value\n return value.replaceAll(decimalSeparator, '.')\n}\n\nconst sanitizeNumericValue = (value: string, allowDecimal: boolean, allowNegative: boolean, decimalSeparator: string): string => {\n if (!value) return ''\n\n let sanitized = normalizeDecimalSeparator(value, decimalSeparator).replace(/[^\\d.-]/g, '')\n\n if (!allowNegative) {\n sanitized = sanitized.replace(/-/g, '')\n } else {\n sanitized = sanitized.replace(/(?!^)-/g, '')\n if (sanitized.startsWith('--')) {\n sanitized = `-${sanitized.replace(/-/g, '')}`\n }\n }\n\n if (!allowDecimal) {\n sanitized = sanitized.replace(/\\./g, '')\n } else {\n const firstDecimalIndex = sanitized.indexOf('.')\n if (firstDecimalIndex >= 0) {\n const integerPart = sanitized.slice(0, firstDecimalIndex + 1)\n const fractionalPart = sanitized.slice(firstDecimalIndex + 1).replace(/\\./g, '')\n sanitized = `${integerPart}${fractionalPart}`\n }\n }\n\n return sanitized\n}\n\nconst shouldAllowDecimalInput = (target: HTMLInputElement): boolean => {\n if (target.selectionStart === null || target.selectionEnd === null) {\n return !target.value.includes('.')\n }\n\n const selectedText = target.value.slice(target.selectionStart, target.selectionEnd)\n return !target.value.includes('.') || selectedText.includes('.')\n}\n\nconst shouldAllowNegativeInput = (target: HTMLInputElement): boolean => {\n if (target.selectionStart === null || target.selectionEnd === null) {\n return target.value.length === 0\n }\n\n const isAtStart = target.selectionStart === 0\n const selectedText = target.value.slice(target.selectionStart, target.selectionEnd)\n return isAtStart && (!target.value.includes('-') || selectedText.includes('-'))\n}\n\nconst parseNumericProp = (value: number | string | undefined): number | undefined => {\n if (value === undefined || value === null || value === '') return undefined\n const parsed = typeof value === 'number' ? value : Number.parseFloat(value)\n return Number.isFinite(parsed) ? parsed : undefined\n}\n\nconst getDecimalPlaces = (value: number): number => {\n const valueString = value.toString()\n if (!valueString.includes('.')) return 0\n return valueString.split('.')[1]?.length ?? 0\n}\n\nconst roundToPrecision = (value: number, precision: number): number => {\n if (precision <= 0) return value\n const factor = 10 ** precision\n return Math.round(value * factor) / factor\n}\n\nexport type InputNumericProps = Omit<InputProps, 'inputMode' | 'onChange' | 'pattern' | 'type'> & {\n allowDecimal?: boolean\n allowNegative?: boolean\n locale?: string\n /** Number of message lines to reserve (default: 1 via Input). */\n messageReserveLines?: number\n /** Whether to keep message space reserved when hidden (default: true via Input). */\n messageReserveSpace?: boolean\n onChange?: (value: string) => void\n value?: string\n}\n\nexport const InputNumeric = ({ allowDecimal = true, allowNegative = false, className, defaultValue = '', locale, max, min, onChange, onKeyDown, onPaste, step, value: valueProp, ...props }: InputNumericProps): ReactElement => {\n const normalizedDefaultValue = typeof defaultValue === 'string' ? defaultValue : defaultValue !== undefined && defaultValue !== null ? String(defaultValue) : ''\n const [value, setValue] = useUncontrolledState<string>({\n value: valueProp,\n defaultValue: normalizedDefaultValue,\n onChange,\n })\n const decimalSeparator = getDecimalSeparator(locale)\n const parsedMin = parseNumericProp(min)\n const parsedMax = parseNumericProp(max)\n const parsedStep = parseNumericProp(step)\n const stepValue = parsedStep && parsedStep > 0 ? parsedStep : 1\n const effectiveMin = parsedMin ?? (!allowNegative ? 0 : undefined)\n\n const handleKeyDown = useCallback(\n (event: NumericKeyDownEvent): void => {\n onKeyDown?.(event)\n if (event.defaultPrevented || event.nativeEvent.isComposing) return\n\n if (event.metaKey || event.ctrlKey || event.altKey) return\n if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {\n event.preventDefault()\n\n const direction = event.key === 'ArrowUp' ? 1 : -1\n const normalizedValue = normalizeDecimalSeparator(value, decimalSeparator)\n const currentValue = Number.parseFloat(normalizedValue)\n const hasCurrentValue = Number.isFinite(currentValue)\n const baseValue = hasCurrentValue ? currentValue : (effectiveMin ?? 0)\n\n let nextValue = baseValue + direction * stepValue\n if (effectiveMin !== undefined) {\n nextValue = Math.max(effectiveMin, nextValue)\n }\n if (parsedMax !== undefined) {\n nextValue = Math.min(parsedMax, nextValue)\n }\n\n if (!allowDecimal) {\n nextValue = Math.trunc(nextValue)\n }\n\n const precision = allowDecimal ? getDecimalPlaces(stepValue) : 0\n const roundedValue = roundToPrecision(nextValue, precision)\n const safeValue = Object.is(roundedValue, -0) ? 0 : roundedValue\n const nextString = precision > 0 ? safeValue.toFixed(precision).replace(/\\.?0+$/, '') : safeValue.toString()\n setValue(sanitizeNumericValue(nextString, allowDecimal, allowNegative, decimalSeparator))\n return\n }\n\n if (ALLOWED_CONTROL_KEYS.has(event.key)) return\n if (DIGIT_REGEX.test(event.key)) return\n\n if (DISALLOWED_SPECIAL_KEYS.has(event.key)) {\n event.preventDefault()\n return\n }\n\n const isDecimalKey = event.key === '.' || event.key === decimalSeparator\n\n if (isDecimalKey) {\n if (!allowDecimal || !shouldAllowDecimalInput(event.currentTarget)) {\n event.preventDefault()\n }\n return\n }\n\n if (event.key === '-') {\n if (!allowNegative || !shouldAllowNegativeInput(event.currentTarget)) {\n event.preventDefault()\n }\n return\n }\n\n event.preventDefault()\n },\n [allowDecimal, allowNegative, decimalSeparator, effectiveMin, onKeyDown, parsedMax, setValue, stepValue, value],\n )\n\n const handlePaste = useCallback(\n (event: NumericPasteEvent): void => {\n onPaste?.(event)\n },\n [onPaste],\n )\n\n const handleChange = useCallback(\n (newValue: string): void => {\n setValue(sanitizeNumericValue(newValue, allowDecimal, allowNegative, decimalSeparator))\n },\n [allowDecimal, allowNegative, decimalSeparator, setValue],\n )\n\n const inputMode = allowDecimal ? 'decimal' : 'numeric'\n const pattern = allowDecimal ? '[0-9]*[.]?[0-9]*' : '[0-9]*'\n const normalizedValue = normalizeDecimalSeparator(value, decimalSeparator)\n const ariaValueNow = normalizedValue === '' || normalizedValue === '-' || normalizedValue === '.' ? undefined : Number.parseFloat(normalizedValue)\n\n return (\n <Input\n {...props}\n aria-valuemax={parsedMax}\n aria-valuemin={effectiveMin}\n aria-valuenow={Number.isFinite(ariaValueNow) ? ariaValueNow : undefined}\n className={cn('tabular-nums', className)}\n inputMode={inputMode}\n max={max}\n min={min}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n onPaste={handlePaste}\n pattern={pattern}\n role='spinbutton'\n step={step}\n type='text'\n value={value}\n />\n )\n}\n\nInputNumeric.displayName = 'InputNumeric'\n"],"mappings":"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"}
1
+ {"version":3,"file":"InputNumeric.js","names":[],"sources":["../src/components/InputNumeric/InputNumeric.tsx"],"sourcesContent":["import { Input, type InputProps } from '@components/Input/Input'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, type ClipboardEvent, type KeyboardEvent, type ReactElement } from 'react'\n\ntype NumericKeyDownEvent = KeyboardEvent<HTMLInputElement>\ntype NumericPasteEvent = ClipboardEvent<HTMLInputElement>\n\nconst DIGIT_REGEX = /^\\d$/\nconst DISALLOWED_SPECIAL_KEYS = new Set(['e', 'E', '+'])\nconst ALLOWED_CONTROL_KEYS = new Set(['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'Tab', 'Enter', 'Escape'])\n\nconst getDecimalSeparator = (locale?: string): string => {\n try {\n const formattedNumber = new Intl.NumberFormat(locale).formatToParts(1.1)\n return formattedNumber.find((part) => part.type === 'decimal')?.value ?? '.'\n } catch {\n return '.'\n }\n}\n\nconst normalizeDecimalSeparator = (value: string, decimalSeparator: string): string => {\n if (decimalSeparator === '.') return value\n return value.replaceAll(decimalSeparator, '.')\n}\n\nconst sanitizeNumericValue = (value: string, allowDecimal: boolean, allowNegative: boolean, decimalSeparator: string): string => {\n if (!value) return ''\n\n let sanitized = normalizeDecimalSeparator(value, decimalSeparator).replace(/[^\\d.-]/g, '')\n\n if (!allowNegative) {\n sanitized = sanitized.replace(/-/g, '')\n } else {\n sanitized = sanitized.replace(/(?!^)-/g, '')\n if (sanitized.startsWith('--')) {\n sanitized = `-${sanitized.replace(/-/g, '')}`\n }\n }\n\n if (!allowDecimal) {\n sanitized = sanitized.replace(/\\./g, '')\n } else {\n const firstDecimalIndex = sanitized.indexOf('.')\n if (firstDecimalIndex >= 0) {\n const integerPart = sanitized.slice(0, firstDecimalIndex + 1)\n const fractionalPart = sanitized.slice(firstDecimalIndex + 1).replace(/\\./g, '')\n sanitized = `${integerPart}${fractionalPart}`\n }\n }\n\n return sanitized\n}\n\nconst shouldAllowDecimalInput = (target: HTMLInputElement): boolean => {\n if (target.selectionStart === null || target.selectionEnd === null) {\n return !target.value.includes('.')\n }\n\n const selectedText = target.value.slice(target.selectionStart, target.selectionEnd)\n return !target.value.includes('.') || selectedText.includes('.')\n}\n\nconst shouldAllowNegativeInput = (target: HTMLInputElement): boolean => {\n if (target.selectionStart === null || target.selectionEnd === null) {\n return target.value.length === 0\n }\n\n const isAtStart = target.selectionStart === 0\n const selectedText = target.value.slice(target.selectionStart, target.selectionEnd)\n return isAtStart && (!target.value.includes('-') || selectedText.includes('-'))\n}\n\nconst parseNumericProp = (value: number | string | undefined): number | undefined => {\n if (value === undefined || value === null || value === '') return undefined\n const parsed = typeof value === 'number' ? value : Number.parseFloat(value)\n return Number.isFinite(parsed) ? parsed : undefined\n}\n\nconst getDecimalPlaces = (value: number): number => {\n const valueString = value.toString()\n if (!valueString.includes('.')) return 0\n return valueString.split('.')[1]?.length ?? 0\n}\n\nconst roundToPrecision = (value: number, precision: number): number => {\n if (precision <= 0) return value\n const factor = 10 ** precision\n return Math.round(value * factor) / factor\n}\n\nexport type InputNumericProps = Omit<InputProps, 'inputMode' | 'onChange' | 'pattern' | 'type'> & {\n allowDecimal?: boolean\n allowNegative?: boolean\n locale?: string\n /** Number of message lines to reserve (default: 1). */\n messageReserveLines?: number\n /** Whether to keep message space reserved when hidden (default: false). */\n messageReserveSpace?: boolean\n onChange?: (value: string) => void\n value?: string\n}\n\nexport const InputNumeric = ({ allowDecimal = true, allowNegative = false, className, defaultValue = '', locale, max, messageReserveLines = 1, messageReserveSpace = false, min, onChange, onKeyDown, onPaste, step, value: valueProp, ...props }: InputNumericProps): ReactElement => {\n const normalizedDefaultValue = typeof defaultValue === 'string' ? defaultValue : defaultValue !== undefined && defaultValue !== null ? String(defaultValue) : ''\n const [value, setValue] = useUncontrolledState<string>({\n value: valueProp,\n defaultValue: normalizedDefaultValue,\n onChange,\n })\n const decimalSeparator = getDecimalSeparator(locale)\n const parsedMin = parseNumericProp(min)\n const parsedMax = parseNumericProp(max)\n const parsedStep = parseNumericProp(step)\n const stepValue = parsedStep && parsedStep > 0 ? parsedStep : 1\n const effectiveMin = parsedMin ?? (!allowNegative ? 0 : undefined)\n\n const handleKeyDown = useCallback(\n (event: NumericKeyDownEvent): void => {\n onKeyDown?.(event)\n if (event.defaultPrevented || event.nativeEvent.isComposing) return\n\n if (event.metaKey || event.ctrlKey || event.altKey) return\n if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {\n event.preventDefault()\n\n const direction = event.key === 'ArrowUp' ? 1 : -1\n const normalizedValue = normalizeDecimalSeparator(value, decimalSeparator)\n const currentValue = Number.parseFloat(normalizedValue)\n const hasCurrentValue = Number.isFinite(currentValue)\n const baseValue = hasCurrentValue ? currentValue : (effectiveMin ?? 0)\n\n let nextValue = baseValue + direction * stepValue\n if (effectiveMin !== undefined) {\n nextValue = Math.max(effectiveMin, nextValue)\n }\n if (parsedMax !== undefined) {\n nextValue = Math.min(parsedMax, nextValue)\n }\n\n if (!allowDecimal) {\n nextValue = Math.trunc(nextValue)\n }\n\n const precision = allowDecimal ? getDecimalPlaces(stepValue) : 0\n const roundedValue = roundToPrecision(nextValue, precision)\n const safeValue = Object.is(roundedValue, -0) ? 0 : roundedValue\n const nextString = precision > 0 ? safeValue.toFixed(precision).replace(/\\.?0+$/, '') : safeValue.toString()\n setValue(sanitizeNumericValue(nextString, allowDecimal, allowNegative, decimalSeparator))\n return\n }\n\n if (ALLOWED_CONTROL_KEYS.has(event.key)) return\n if (DIGIT_REGEX.test(event.key)) return\n\n if (DISALLOWED_SPECIAL_KEYS.has(event.key)) {\n event.preventDefault()\n return\n }\n\n const isDecimalKey = event.key === '.' || event.key === decimalSeparator\n\n if (isDecimalKey) {\n if (!allowDecimal || !shouldAllowDecimalInput(event.currentTarget)) {\n event.preventDefault()\n }\n return\n }\n\n if (event.key === '-') {\n if (!allowNegative || !shouldAllowNegativeInput(event.currentTarget)) {\n event.preventDefault()\n }\n return\n }\n\n event.preventDefault()\n },\n [allowDecimal, allowNegative, decimalSeparator, effectiveMin, onKeyDown, parsedMax, setValue, stepValue, value],\n )\n\n const handlePaste = useCallback(\n (event: NumericPasteEvent): void => {\n onPaste?.(event)\n },\n [onPaste],\n )\n\n const handleChange = useCallback(\n (newValue: string): void => {\n setValue(sanitizeNumericValue(newValue, allowDecimal, allowNegative, decimalSeparator))\n },\n [allowDecimal, allowNegative, decimalSeparator, setValue],\n )\n\n const inputMode = allowDecimal ? 'decimal' : 'numeric'\n const pattern = allowDecimal ? '[0-9]*[.]?[0-9]*' : '[0-9]*'\n const normalizedValue = normalizeDecimalSeparator(value, decimalSeparator)\n const ariaValueNow = normalizedValue === '' || normalizedValue === '-' || normalizedValue === '.' ? undefined : Number.parseFloat(normalizedValue)\n\n return (\n <Input\n {...props}\n aria-valuemax={parsedMax}\n aria-valuemin={effectiveMin}\n aria-valuenow={Number.isFinite(ariaValueNow) ? ariaValueNow : undefined}\n className={cn('tabular-nums', className)}\n inputMode={inputMode}\n max={max}\n messageReserveLines={messageReserveLines}\n messageReserveSpace={messageReserveSpace}\n min={min}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n onPaste={handlePaste}\n pattern={pattern}\n role='spinbutton'\n step={step}\n type='text'\n value={value}\n />\n )\n}\n\nInputNumeric.displayName = 'InputNumeric'\n"],"mappings":";;;;;;;;AAQA,MAAM,cAAc;AACpB,MAAM,0BAA0B,IAAI,IAAI;CAAC;CAAK;CAAK;CAAI,CAAC;AACxD,MAAM,uBAAuB,IAAI,IAAI;CAAC;CAAa;CAAU;CAAa;CAAc;CAAW;CAAa;CAAQ;CAAO;CAAO;CAAS;CAAS,CAAC;AAEzJ,MAAM,uBAAuB,WAA4B;AACvD,KAAI;AAEF,SADwB,IAAI,KAAK,aAAa,OAAO,CAAC,cAAc,IAC9C,CAAC,MAAM,SAAS,KAAK,SAAS,UAAU,EAAE,SAAS;SACnE;AACN,SAAO;;;AAIX,MAAM,6BAA6B,OAAe,qBAAqC;AACrF,KAAI,qBAAqB,IAAK,QAAO;AACrC,QAAO,MAAM,WAAW,kBAAkB,IAAI;;AAGhD,MAAM,wBAAwB,OAAe,cAAuB,eAAwB,qBAAqC;AAC/H,KAAI,CAAC,MAAO,QAAO;CAEnB,IAAI,YAAY,0BAA0B,OAAO,iBAAiB,CAAC,QAAQ,YAAY,GAAG;AAE1F,KAAI,CAAC,cACH,aAAY,UAAU,QAAQ,MAAM,GAAG;MAClC;AACL,cAAY,UAAU,QAAQ,WAAW,GAAG;AAC5C,MAAI,UAAU,WAAW,KAAK,CAC5B,aAAY,IAAI,UAAU,QAAQ,MAAM,GAAG;;AAI/C,KAAI,CAAC,aACH,aAAY,UAAU,QAAQ,OAAO,GAAG;MACnC;EACL,MAAM,oBAAoB,UAAU,QAAQ,IAAI;AAChD,MAAI,qBAAqB,EAGvB,aAAY,GAFQ,UAAU,MAAM,GAAG,oBAAoB,EAEjC,GADH,UAAU,MAAM,oBAAoB,EAAE,CAAC,QAAQ,OAAO,GAClC;;AAI/C,QAAO;;AAGT,MAAM,2BAA2B,WAAsC;AACrE,KAAI,OAAO,mBAAmB,QAAQ,OAAO,iBAAiB,KAC5D,QAAO,CAAC,OAAO,MAAM,SAAS,IAAI;CAGpC,MAAM,eAAe,OAAO,MAAM,MAAM,OAAO,gBAAgB,OAAO,aAAa;AACnF,QAAO,CAAC,OAAO,MAAM,SAAS,IAAI,IAAI,aAAa,SAAS,IAAI;;AAGlE,MAAM,4BAA4B,WAAsC;AACtE,KAAI,OAAO,mBAAmB,QAAQ,OAAO,iBAAiB,KAC5D,QAAO,OAAO,MAAM,WAAW;CAGjC,MAAM,YAAY,OAAO,mBAAmB;CAC5C,MAAM,eAAe,OAAO,MAAM,MAAM,OAAO,gBAAgB,OAAO,aAAa;AACnF,QAAO,cAAc,CAAC,OAAO,MAAM,SAAS,IAAI,IAAI,aAAa,SAAS,IAAI;;AAGhF,MAAM,oBAAoB,UAA2D;AACnF,KAAI,UAAU,UAAa,UAAU,QAAQ,UAAU,GAAI,QAAO;CAClE,MAAM,SAAS,OAAO,UAAU,WAAW,QAAQ,OAAO,WAAW,MAAM;AAC3E,QAAO,OAAO,SAAS,OAAO,GAAG,SAAS;;AAG5C,MAAM,oBAAoB,UAA0B;CAClD,MAAM,cAAc,MAAM,UAAU;AACpC,KAAI,CAAC,YAAY,SAAS,IAAI,CAAE,QAAO;AACvC,QAAO,YAAY,MAAM,IAAI,CAAC,IAAI,UAAU;;AAG9C,MAAM,oBAAoB,OAAe,cAA8B;AACrE,KAAI,aAAa,EAAG,QAAO;CAC3B,MAAM,SAAS,MAAM;AACrB,QAAO,KAAK,MAAM,QAAQ,OAAO,GAAG;;AAetC,MAAa,gBAAgB,EAAE,eAAe,MAAM,gBAAgB,OAAO,WAAW,eAAe,IAAI,QAAQ,KAAK,sBAAsB,GAAG,sBAAsB,OAAO,KAAK,UAAU,WAAW,SAAS,MAAM,OAAO,WAAW,GAAG,YAA6C;CAErR,MAAM,CAAC,OAAO,YAAY,qBAA6B;EACrD,OAAO;EACP,cAH6B,OAAO,iBAAiB,WAAW,eAAe,iBAAiB,UAAa,iBAAiB,OAAO,OAAO,aAAa,GAAG;EAI5J;EACD,CAAC;CACF,MAAM,mBAAmB,oBAAoB,OAAO;CACpD,MAAM,YAAY,iBAAiB,IAAI;CACvC,MAAM,YAAY,iBAAiB,IAAI;CACvC,MAAM,aAAa,iBAAiB,KAAK;CACzC,MAAM,YAAY,cAAc,aAAa,IAAI,aAAa;CAC9D,MAAM,eAAe,cAAc,CAAC,gBAAgB,IAAI;CAExD,MAAM,gBAAgB,aACnB,UAAqC;AACpC,cAAY,MAAM;AAClB,MAAI,MAAM,oBAAoB,MAAM,YAAY,YAAa;AAE7D,MAAI,MAAM,WAAW,MAAM,WAAW,MAAM,OAAQ;AACpD,MAAI,MAAM,QAAQ,aAAa,MAAM,QAAQ,aAAa;AACxD,SAAM,gBAAgB;GAEtB,MAAM,YAAY,MAAM,QAAQ,YAAY,IAAI;GAChD,MAAM,kBAAkB,0BAA0B,OAAO,iBAAiB;GAC1E,MAAM,eAAe,OAAO,WAAW,gBAAgB;GAIvD,IAAI,aAHoB,OAAO,SAAS,aACP,GAAG,eAAgB,gBAAgB,KAExC,YAAY;AACxC,OAAI,iBAAiB,OACnB,aAAY,KAAK,IAAI,cAAc,UAAU;AAE/C,OAAI,cAAc,OAChB,aAAY,KAAK,IAAI,WAAW,UAAU;AAG5C,OAAI,CAAC,aACH,aAAY,KAAK,MAAM,UAAU;GAGnC,MAAM,YAAY,eAAe,iBAAiB,UAAU,GAAG;GAC/D,MAAM,eAAe,iBAAiB,WAAW,UAAU;GAC3D,MAAM,YAAY,OAAO,GAAG,cAAc,GAAG,GAAG,IAAI;AAEpD,YAAS,qBADU,YAAY,IAAI,UAAU,QAAQ,UAAU,CAAC,QAAQ,UAAU,GAAG,GAAG,UAAU,UAAU,EAClE,cAAc,eAAe,iBAAiB,CAAC;AACzF;;AAGF,MAAI,qBAAqB,IAAI,MAAM,IAAI,CAAE;AACzC,MAAI,YAAY,KAAK,MAAM,IAAI,CAAE;AAEjC,MAAI,wBAAwB,IAAI,MAAM,IAAI,EAAE;AAC1C,SAAM,gBAAgB;AACtB;;AAKF,MAFqB,MAAM,QAAQ,OAAO,MAAM,QAAQ,kBAEtC;AAChB,OAAI,CAAC,gBAAgB,CAAC,wBAAwB,MAAM,cAAc,CAChE,OAAM,gBAAgB;AAExB;;AAGF,MAAI,MAAM,QAAQ,KAAK;AACrB,OAAI,CAAC,iBAAiB,CAAC,yBAAyB,MAAM,cAAc,CAClE,OAAM,gBAAgB;AAExB;;AAGF,QAAM,gBAAgB;IAExB;EAAC;EAAc;EAAe;EAAkB;EAAc;EAAW;EAAW;EAAU;EAAW;EAAM,CAChH;CAED,MAAM,cAAc,aACjB,UAAmC;AAClC,YAAU,MAAM;IAElB,CAAC,QAAQ,CACV;CAED,MAAM,eAAe,aAClB,aAA2B;AAC1B,WAAS,qBAAqB,UAAU,cAAc,eAAe,iBAAiB,CAAC;IAEzF;EAAC;EAAc;EAAe;EAAkB;EAAS,CAC1D;CAED,MAAM,YAAY,eAAe,YAAY;CAC7C,MAAM,UAAU,eAAe,qBAAqB;CACpD,MAAM,kBAAkB,0BAA0B,OAAO,iBAAiB;CAC1E,MAAM,eAAe,oBAAoB,MAAM,oBAAoB,OAAO,oBAAoB,MAAM,SAAY,OAAO,WAAW,gBAAgB;AAElJ,QACE,oBAAC,OAAD;EACE,GAAI;EACJ,iBAAe;EACf,iBAAe;EACf,iBAAe,OAAO,SAAS,aAAa,GAAG,eAAe;EAC9D,WAAW,GAAG,gBAAgB,UAAU;EAC7B;EACN;EACgB;EACA;EAChB;EACL,UAAU;EACV,WAAW;EACX,SAAS;EACA;EACT,MAAK;EACC;EACN,MAAK;EACE;EACP;;AAIN,aAAa,cAAc"}
package/dist/InputOTP.js CHANGED
@@ -15,7 +15,7 @@ const useRoot = () => {
15
15
  if (!context) throw new Error("useRoot must be used within an InputOTP");
16
16
  return context;
17
17
  };
18
- const Root = ({ autoFocus = false, children, className, errorMessage, id, inputMode = "numeric", messageReserveLines = 1, messageReserveSpace = true, maxLength, name, onChange, onComplete, pattern, ref, state = "default", value, variant = "outlined", ...props }) => {
18
+ const Root = ({ autoFocus = false, children, className, errorMessage, id, inputMode = "numeric", messageReserveLines = 1, messageReserveSpace = false, maxLength, name, onChange, onComplete, pattern, ref, state = "default", value, variant = "outlined", ...props }) => {
19
19
  const inputId = useFormFieldId(id, name);
20
20
  const errorMessageId = getErrorMessageId(inputId);
21
21
  const { isInvalid } = useFormFieldState(false, state);
@@ -1 +1 @@
1
- {"version":3,"file":"InputOTP.js","names":[],"sources":["../src/components/InputOTP/InputOTP.tsx"],"sourcesContent":["import { MinusIcon } from '@components/Icons'\nimport { ErrorMessage, getErrorMessageId, useFormFieldId, useFormFieldState, type FormFieldState } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { OTPInput, REGEXP_ONLY_DIGITS, type OTPInputProps } from 'input-otp'\nimport { createContext, useContext, type ClipboardEvent, type ComponentPropsWithoutRef, type ComponentRef, type Ref } from 'react'\n\nexport interface InputOTPBaseProps extends Omit<OTPInputProps, 'textAlign' | 'pushPasswordManagerStrategy' | 'pasteTransformer' | 'noScriptCSSFallback' | 'placeholder' | 'containerClassName' | 'render' | 'pattern'> {\n onComplete?: (...args: unknown[]) => void\n className?: string\n errorMessage?: string | undefined\n inputMode?: 'numeric' | 'text' | 'decimal' | 'tel' | 'search' | 'email' | 'url'\n messageReserveLines?: number\n messageReserveSpace?: boolean\n /**\n * Regex pattern string to restrict allowed characters.\n * When `inputMode=\"numeric\"`, defaults to digits-only pattern.\n * Set to `undefined` to allow any characters.\n */\n pattern?: string | undefined\n separator?: boolean\n state?: FormFieldState\n variant?: 'outlined' | 'filled'\n}\n\nexport type InputOTPProps = InputOTPBaseProps & ({ value: number | string; onChange: (newValue: number | string) => void } | { value?: never; onChange?: never })\n\nconst InputOTPStateContext = createContext<{ isInvalid?: boolean }>({})\n\nconst OTPInputContext = createContext<{\n maxLength?: number\n slots?: { char: string | null; hasFakeCaret: boolean; isActive: boolean }[]\n variant?: 'outlined' | 'filled'\n} | null>(null)\n\nconst useRoot = () => {\n const context = useContext(OTPInputContext)\n if (!context) {\n throw new Error('useRoot must be used within an InputOTP')\n }\n return context\n}\n\nconst Root = ({\n autoFocus = false,\n children,\n className,\n errorMessage,\n id,\n inputMode = 'numeric',\n messageReserveLines = 1,\n messageReserveSpace = true,\n maxLength,\n name,\n onChange,\n onComplete,\n pattern,\n ref,\n state = 'default',\n value,\n variant = 'outlined',\n ...props\n}: InputOTPProps & {\n ref?: Ref<ComponentRef<typeof OTPInput>>\n}) => {\n const inputId = useFormFieldId(id, name)\n const errorMessageId = getErrorMessageId(inputId)\n const { isInvalid } = useFormFieldState(false, state)\n\n // Apply digits-only pattern when inputMode is numeric (unless explicitly overridden)\n const effectivePattern = pattern ?? (inputMode === 'numeric' ? REGEXP_ONLY_DIGITS : undefined)\n\n const handlePaste = (e: ClipboardEvent<HTMLDivElement>): void => {\n let pasteData = e.clipboardData.getData('text/plain').trim().replaceAll('-', '')\n\n // Filter to digits only when in numeric mode\n if (inputMode === 'numeric') {\n pasteData = pasteData.replace(/\\D/g, '')\n }\n\n if (pasteData.length === maxLength && typeof onChange === 'function') {\n onChange(pasteData)\n }\n }\n\n return (\n <InputOTPStateContext.Provider value={{ isInvalid }}>\n <div className='gap-y-1 flex w-max flex-col'>\n <OTPInput\n /* eslint-disable-next-line jsx-a11y/no-autofocus -- intentional: consumers can opt in for OTP-first flows; defaults to false */\n autoFocus={autoFocus}\n containerClassName={cn('gap-2 flex items-center disabled:cursor-not-allowed has-[disabled]:opacity-50', className)}\n data-1p-ignore='true'\n data-dashlane-disabled-on-field='true'\n data-lpignore='true'\n data-protonpass-ignore='true'\n data-testid='spectral-input-otp'\n id={inputId}\n inputMode={inputMode}\n maxLength={maxLength}\n onChange={onChange}\n onComplete={onComplete}\n onPaste={handlePaste}\n pasteTransformer={(pasted) => pasted.replaceAll('-', '')}\n pattern={effectivePattern}\n pushPasswordManagerStrategy='none'\n ref={ref}\n aria-describedby={isInvalid && errorMessage ? errorMessageId : undefined}\n aria-invalid={isInvalid}\n role='textbox'\n textAlign='center'\n value={value}\n {...props}\n render={({ slots }) => (\n <OTPInputContext.Provider value={{ slots, variant, maxLength }}>\n {children ?? (\n <Group>\n <Slots />\n </Group>\n )}\n </OTPInputContext.Provider>\n )}\n />\n <ErrorMessage dataTestId='spectral-input-otp-error-message' id={errorMessageId} message={isInvalid ? errorMessage : null} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace} />\n </div>\n </InputOTPStateContext.Provider>\n )\n}\nRoot.displayName = 'InputOTP'\n\nconst Group = ({\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n ref?: Ref<ComponentRef<'div'>>\n}) => <div className='gap-x-2 flex items-center justify-center' data-testid='spectral-input-otp-group' ref={ref} {...props} />\nGroup.displayName = 'InputOTP.Group'\n\nconst Slot = ({\n className,\n index,\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n index: number\n ref?: Ref<ComponentRef<'div'>>\n}) => {\n const { variant = 'outlined', slots = [] } = useRoot()\n const { isInvalid } = useContext(InputOTPStateContext)\n const slot = slots[index] || { char: '', hasFakeCaret: true, isActive: false }\n\n return (\n <div\n className={cn(\n 'h-12 w-10 relative z-10 flex items-center justify-center rounded-[8px] border tabular-nums transition duration-200 focus:outline-none',\n variant === 'filled' ? 'border-level-one bg-level-one' : 'border-input-otp-border bg-transparent',\n !isInvalid && 'border',\n isInvalid && 'border-2 border-danger-400',\n slot.isActive && !isInvalid && 'z-10 border-input-otp-border--focus',\n slot.isActive && isInvalid && 'z-10 border-danger-400 focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-danger-400',\n className,\n )}\n data-index={index}\n data-testid='spectral-input-otp-slot'\n data-variant={variant}\n ref={ref}\n {...props}\n >\n {slot.char}\n {slot.hasFakeCaret && (\n <div className='inset-0 pointer-events-none absolute flex items-center justify-center motion-safe:animate-caret-blink'>\n <div className='h-8 w-px bg-input-otp-caret' />\n </div>\n )}\n </div>\n )\n}\nSlot.displayName = 'InputOTP.Slot'\n\ninterface SlotsProps {\n className?: string\n count?: number\n start?: number\n}\n\n/**\n * Helper component that automatically renders multiple InputOTP.Slot components.\n * Uses the maxLength from the parent InputOTP to determine how many slots to render.\n *\n * @example\n * // Render all 6 slots\n * <InputOTP maxLength={6}>\n * <InputOTP.Group>\n * <InputOTP.Slots />\n * </InputOTP.Group>\n * </InputOTP>\n *\n * @example\n * // Render slots in groups with a separator (3-3 split)\n * <InputOTP maxLength={6}>\n * <InputOTP.Group>\n * <InputOTP.Slots count={3} />\n * </InputOTP.Group>\n * <InputOTP.Separator />\n * <InputOTP.Group>\n * <InputOTP.Slots start={3} />\n * </InputOTP.Group>\n * </InputOTP>\n */\nconst Slots = ({ start = 0, count, className }: SlotsProps) => {\n const { maxLength = 0 } = useRoot()\n const end = count !== undefined ? start + count : maxLength\n const indices = Array.from({ length: end - start }, (_, i) => start + i)\n\n return (\n <>\n {indices.map((index) => (\n <Slot key={index} index={index} className={className} />\n ))}\n </>\n )\n}\nSlots.displayName = 'InputOTP.Slots'\n\nconst Separator = ({\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n ref?: Ref<ComponentRef<'div'>>\n}) => {\n const { variant = 'outlined' } = useRoot()\n\n return (\n <div ref={ref} role='separator' {...props} data-testid='spectral-input-otp-separator' data-variant={variant}>\n <MinusIcon size={24} color={variant === 'filled' ? 'var(--color-input-otp-filled-separator)' : 'var(--color-input-otp-border)'} />\n </div>\n )\n}\nSeparator.displayName = 'InputOTP.Separator'\n\nexport const InputOTP = Object.assign(Root, {\n Group,\n Slot,\n Slots,\n Separator,\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"InputOTP.js","names":[],"sources":["../src/components/InputOTP/InputOTP.tsx"],"sourcesContent":["import { MinusIcon } from '@components/Icons'\nimport { ErrorMessage, getErrorMessageId, useFormFieldId, useFormFieldState, type FormFieldState } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { OTPInput, REGEXP_ONLY_DIGITS, type OTPInputProps } from 'input-otp'\nimport { createContext, useContext, type ClipboardEvent, type ComponentPropsWithoutRef, type ComponentRef, type Ref } from 'react'\n\nexport interface InputOTPBaseProps extends Omit<OTPInputProps, 'textAlign' | 'pushPasswordManagerStrategy' | 'pasteTransformer' | 'noScriptCSSFallback' | 'placeholder' | 'containerClassName' | 'render' | 'pattern'> {\n onComplete?: (...args: unknown[]) => void\n className?: string\n errorMessage?: string | undefined\n inputMode?: 'numeric' | 'text' | 'decimal' | 'tel' | 'search' | 'email' | 'url'\n messageReserveLines?: number\n messageReserveSpace?: boolean\n /**\n * Regex pattern string to restrict allowed characters.\n * When `inputMode=\"numeric\"`, defaults to digits-only pattern.\n * Set to `undefined` to allow any characters.\n */\n pattern?: string | undefined\n separator?: boolean\n state?: FormFieldState\n variant?: 'outlined' | 'filled'\n}\n\nexport type InputOTPProps = InputOTPBaseProps & ({ value: number | string; onChange: (newValue: number | string) => void } | { value?: never; onChange?: never })\n\nconst InputOTPStateContext = createContext<{ isInvalid?: boolean }>({})\n\nconst OTPInputContext = createContext<{\n maxLength?: number\n slots?: { char: string | null; hasFakeCaret: boolean; isActive: boolean }[]\n variant?: 'outlined' | 'filled'\n} | null>(null)\n\nconst useRoot = () => {\n const context = useContext(OTPInputContext)\n if (!context) {\n throw new Error('useRoot must be used within an InputOTP')\n }\n return context\n}\n\nconst Root = ({\n autoFocus = false,\n children,\n className,\n errorMessage,\n id,\n inputMode = 'numeric',\n messageReserveLines = 1,\n messageReserveSpace = false,\n maxLength,\n name,\n onChange,\n onComplete,\n pattern,\n ref,\n state = 'default',\n value,\n variant = 'outlined',\n ...props\n}: InputOTPProps & {\n ref?: Ref<ComponentRef<typeof OTPInput>>\n}) => {\n const inputId = useFormFieldId(id, name)\n const errorMessageId = getErrorMessageId(inputId)\n const { isInvalid } = useFormFieldState(false, state)\n\n // Apply digits-only pattern when inputMode is numeric (unless explicitly overridden)\n const effectivePattern = pattern ?? (inputMode === 'numeric' ? REGEXP_ONLY_DIGITS : undefined)\n\n const handlePaste = (e: ClipboardEvent<HTMLDivElement>): void => {\n let pasteData = e.clipboardData.getData('text/plain').trim().replaceAll('-', '')\n\n // Filter to digits only when in numeric mode\n if (inputMode === 'numeric') {\n pasteData = pasteData.replace(/\\D/g, '')\n }\n\n if (pasteData.length === maxLength && typeof onChange === 'function') {\n onChange(pasteData)\n }\n }\n\n return (\n <InputOTPStateContext.Provider value={{ isInvalid }}>\n <div className='gap-y-1 flex w-max flex-col'>\n <OTPInput\n /* eslint-disable-next-line jsx-a11y/no-autofocus -- intentional: consumers can opt in for OTP-first flows; defaults to false */\n autoFocus={autoFocus}\n containerClassName={cn('gap-2 flex items-center disabled:cursor-not-allowed has-[disabled]:opacity-50', className)}\n data-1p-ignore='true'\n data-dashlane-disabled-on-field='true'\n data-lpignore='true'\n data-protonpass-ignore='true'\n data-testid='spectral-input-otp'\n id={inputId}\n inputMode={inputMode}\n maxLength={maxLength}\n onChange={onChange}\n onComplete={onComplete}\n onPaste={handlePaste}\n pasteTransformer={(pasted) => pasted.replaceAll('-', '')}\n pattern={effectivePattern}\n pushPasswordManagerStrategy='none'\n ref={ref}\n aria-describedby={isInvalid && errorMessage ? errorMessageId : undefined}\n aria-invalid={isInvalid}\n role='textbox'\n textAlign='center'\n value={value}\n {...props}\n render={({ slots }) => (\n <OTPInputContext.Provider value={{ slots, variant, maxLength }}>\n {children ?? (\n <Group>\n <Slots />\n </Group>\n )}\n </OTPInputContext.Provider>\n )}\n />\n <ErrorMessage dataTestId='spectral-input-otp-error-message' id={errorMessageId} message={isInvalid ? errorMessage : null} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace} />\n </div>\n </InputOTPStateContext.Provider>\n )\n}\nRoot.displayName = 'InputOTP'\n\nconst Group = ({\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n ref?: Ref<ComponentRef<'div'>>\n}) => <div className='gap-x-2 flex items-center justify-center' data-testid='spectral-input-otp-group' ref={ref} {...props} />\nGroup.displayName = 'InputOTP.Group'\n\nconst Slot = ({\n className,\n index,\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n index: number\n ref?: Ref<ComponentRef<'div'>>\n}) => {\n const { variant = 'outlined', slots = [] } = useRoot()\n const { isInvalid } = useContext(InputOTPStateContext)\n const slot = slots[index] || { char: '', hasFakeCaret: true, isActive: false }\n\n return (\n <div\n className={cn(\n 'h-12 w-10 relative z-10 flex items-center justify-center rounded-[8px] border tabular-nums transition duration-200 focus:outline-none',\n variant === 'filled' ? 'border-level-one bg-level-one' : 'border-input-otp-border bg-transparent',\n !isInvalid && 'border',\n isInvalid && 'border-2 border-danger-400',\n slot.isActive && !isInvalid && 'z-10 border-input-otp-border--focus',\n slot.isActive && isInvalid && 'z-10 border-danger-400 focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-danger-400',\n className,\n )}\n data-index={index}\n data-testid='spectral-input-otp-slot'\n data-variant={variant}\n ref={ref}\n {...props}\n >\n {slot.char}\n {slot.hasFakeCaret && (\n <div className='inset-0 pointer-events-none absolute flex items-center justify-center motion-safe:animate-caret-blink'>\n <div className='h-8 w-px bg-input-otp-caret' />\n </div>\n )}\n </div>\n )\n}\nSlot.displayName = 'InputOTP.Slot'\n\ninterface SlotsProps {\n className?: string\n count?: number\n start?: number\n}\n\n/**\n * Helper component that automatically renders multiple InputOTP.Slot components.\n * Uses the maxLength from the parent InputOTP to determine how many slots to render.\n *\n * @example\n * // Render all 6 slots\n * <InputOTP maxLength={6}>\n * <InputOTP.Group>\n * <InputOTP.Slots />\n * </InputOTP.Group>\n * </InputOTP>\n *\n * @example\n * // Render slots in groups with a separator (3-3 split)\n * <InputOTP maxLength={6}>\n * <InputOTP.Group>\n * <InputOTP.Slots count={3} />\n * </InputOTP.Group>\n * <InputOTP.Separator />\n * <InputOTP.Group>\n * <InputOTP.Slots start={3} />\n * </InputOTP.Group>\n * </InputOTP>\n */\nconst Slots = ({ start = 0, count, className }: SlotsProps) => {\n const { maxLength = 0 } = useRoot()\n const end = count !== undefined ? start + count : maxLength\n const indices = Array.from({ length: end - start }, (_, i) => start + i)\n\n return (\n <>\n {indices.map((index) => (\n <Slot key={index} index={index} className={className} />\n ))}\n </>\n )\n}\nSlots.displayName = 'InputOTP.Slots'\n\nconst Separator = ({\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n ref?: Ref<ComponentRef<'div'>>\n}) => {\n const { variant = 'outlined' } = useRoot()\n\n return (\n <div ref={ref} role='separator' {...props} data-testid='spectral-input-otp-separator' data-variant={variant}>\n <MinusIcon size={24} color={variant === 'filled' ? 'var(--color-input-otp-filled-separator)' : 'var(--color-input-otp-border)'} />\n </div>\n )\n}\nSeparator.displayName = 'InputOTP.Separator'\n\nexport const InputOTP = Object.assign(Root, {\n Group,\n Slot,\n Slots,\n Separator,\n})\n"],"mappings":"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"}
@@ -136,7 +136,7 @@ const useKeyboardNavigation = (options, onClearAll, onClose, onSelect, onSelectA
136
136
  }, [focusedIndex, focusableItems])
137
137
  };
138
138
  };
139
- const MultiSelectBase = ({ className, clearAllLabel = "Clear all", closeOnSelect = false, dropdownWidth = "trigger", emptyMessage = "No options found", errorMessage, defaultValue = [], disabled, id, label, loadingMessage = "Loading options…", messageReserveLines = 1, messageReserveSpace = true, maxCount = 3, name, onChange, options = [], placeholder = "Select options", ref, searchPlaceholder = "Search options…", selectAllLabel = "Select all", showClearAll = true, showSearch = true, showSelectAll = true, sortAlphabetically = false, state = "default", value: valueProp, warningMessage, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...props }) => {
139
+ const MultiSelectBase = ({ className, clearAllLabel = "Clear all", closeOnSelect = false, dropdownWidth = "trigger", emptyMessage = "No options found", errorMessage, defaultValue = [], disabled, id, label, loadingMessage = "Loading options…", messageReserveLines = 1, messageReserveSpace = false, maxCount = 3, name, onChange, options = [], placeholder = "Select options", ref, searchPlaceholder = "Search options…", selectAllLabel = "Select all", showClearAll = true, showSearch = true, showSelectAll = true, sortAlphabetically = false, state = "default", value: valueProp, warningMessage, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...props }) => {
140
140
  const generatedId = useId();
141
141
  const multiSelectId = useFormFieldId(id, name ?? `multiselect-${generatedId}`);
142
142
  const listboxId = `${multiSelectId}-listbox`;
@@ -432,14 +432,14 @@ const MultiSelectBase = ({ className, clearAllLabel = "Clear all", closeOnSelect
432
432
  id: errorMessageId,
433
433
  message: state === "error" ? errorMessage : null,
434
434
  messageReserveLines,
435
- messageReserveSpace
435
+ messageReserveSpace: messageReserveSpace && state === "error"
436
436
  }),
437
437
  /* @__PURE__ */ jsx(WarningMessage, {
438
438
  dataTestId: "spectral-multiselect-warning-message",
439
439
  id: warningMessageId,
440
440
  message: state === "warning" ? warningMessage : null,
441
441
  messageReserveLines,
442
- messageReserveSpace
442
+ messageReserveSpace: messageReserveSpace && state === "warning"
443
443
  })
444
444
  ]
445
445
  });
@@ -1 +1 @@
1
- {"version":3,"file":"MultiSelectBase.js","names":[],"sources":["../../src/components/MultiSelect/MultiSelectBase.tsx"],"sourcesContent":["import { CheckmarkIcon, ChevronDownIcon, CloseIcon, SearchIcon } from '@components/Icons'\nimport { Label } from '@components/Label/Label'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport * as Popover from '@radix-ui/react-popover'\nimport { useAutoDropdownHorizontalShift } from '@utils/dropdownPositioning'\nimport { EmptyState, ErrorMessage, getAriaProps, getDropdownSurfaceClasses, getDropdownWidthStyles, getErrorMessageId, getTriggerClasses, LoadingState, WarningMessage, useFormFieldId, type BaseFormFieldProps, type DropdownWidth, type FormFieldState } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useEffect, useId, useMemo, useRef, useState, type ButtonHTMLAttributes, type ChangeEvent, type CSSProperties, type KeyboardEvent, type Ref } from 'react'\n\nexport type MultiSelectState = Exclude<FormFieldState, 'disabled'>\n\nexport interface MultiSelectOption {\n disabled?: boolean\n group?: string\n label: string\n value: string\n}\n\nexport interface MultiSelectBaseProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'value' | 'onChange'> {\n clearAllLabel?: string\n closeOnSelect?: boolean\n dropdownWidth?: DropdownWidth\n emptyMessage?: string\n errorMessage?: BaseFormFieldProps['errorMessage']\n id?: string\n label?: string\n loadingMessage?: string\n maxCount?: number\n messageReserveLines?: number\n messageReserveSpace?: boolean\n name?: string\n defaultValue?: string[]\n onChange?: (value: string[]) => void\n options: MultiSelectOption[]\n placeholder?: string\n required?: boolean\n searchPlaceholder?: string\n showClearAll?: boolean\n showSearch?: boolean\n showSelectAll?: boolean\n selectAllLabel?: string\n sortAlphabetically?: boolean\n state?: MultiSelectState\n value?: string[]\n warningMessage?: BaseFormFieldProps['errorMessage']\n 'aria-label'?: string\n 'aria-describedby'?: string\n}\n\nconst ICON_SIZE = 'h-4 w-4'\n\nconst getDropdownClasses = (): string => {\n return cn(\n 'max-h-80 z-50 overflow-hidden',\n getDropdownSurfaceClasses(),\n 'motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:animate-in',\n 'motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:fade-in-0',\n 'motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:zoom-in-95',\n 'motion-safe:data-[side=bottom]:slide-in-from-top-2',\n 'motion-safe:data-[side=top]:slide-in-from-bottom-2',\n 'origin-(--radix-popover-content-transform-origin)',\n )\n}\n\ntype FocusableItem = { type: 'search' } | { type: 'select-all' } | { type: 'option'; index: number; value: string } | { type: 'clear-all' }\n\nconst useKeyboardNavigation = (\n options: MultiSelectOption[],\n onClearAll: () => void,\n onClose: () => void,\n onSelect: (value: string) => void,\n onSelectAll: () => void,\n searchInputRef: React.RefObject<HTMLInputElement | null>,\n showSearch: boolean,\n showSelectAll: boolean,\n showClearAll: boolean,\n) => {\n const [focusedIndex, setFocusedIndex] = useState(-1)\n\n // Build a flat list of all focusable items\n const focusableItems = useMemo((): FocusableItem[] => {\n const items: FocusableItem[] = []\n\n if (showSearch) {\n items.push({ type: 'search' })\n }\n\n if (showSelectAll) {\n items.push({ type: 'select-all' })\n }\n\n options.forEach((option, index) => {\n if (!option.disabled) {\n items.push({ type: 'option', index, value: option.value })\n }\n })\n\n if (showClearAll) {\n items.push({ type: 'clear-all' })\n }\n\n return items\n }, [options, showSearch, showSelectAll, showClearAll])\n\n // Focus the appropriate element when focusedIndex changes\n const focusCurrentItem = useCallback(\n (index: number) => {\n if (index < 0 || index >= focusableItems.length) return\n const item = focusableItems[index]\n if (item.type === 'search') {\n searchInputRef.current?.focus()\n }\n },\n [focusableItems, searchInputRef],\n )\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>) => {\n const currentItem = focusedIndex >= 0 && focusedIndex < focusableItems.length ? focusableItems[focusedIndex] : null\n\n // Don't prevent default for space in search input (allow typing spaces)\n if (event.key === ' ' && currentItem?.type === 'search') {\n return\n }\n\n // Don't prevent default for Enter in search input (allow form submission behavior)\n if (event.key === 'Enter' && currentItem?.type === 'search') {\n return\n }\n\n const keyHandlers: Record<string, () => void> = {\n ArrowDown: () => {\n event.preventDefault()\n const newIndex = Math.min(focusedIndex + 1, focusableItems.length - 1)\n setFocusedIndex(newIndex)\n focusCurrentItem(newIndex)\n },\n ArrowUp: () => {\n event.preventDefault()\n const newIndex = Math.max(focusedIndex - 1, 0)\n setFocusedIndex(newIndex)\n focusCurrentItem(newIndex)\n },\n Tab: () => {\n // Allow Tab to cycle through focusable items\n if (event.shiftKey) {\n if (focusedIndex <= 0) {\n // At start, close dropdown and return to trigger\n onClose()\n } else {\n event.preventDefault()\n const newIndex = focusedIndex - 1\n setFocusedIndex(newIndex)\n focusCurrentItem(newIndex)\n }\n } else {\n if (focusedIndex >= focusableItems.length - 1) {\n // At end, close dropdown and move to next element\n onClose()\n } else {\n event.preventDefault()\n const newIndex = focusedIndex + 1\n setFocusedIndex(newIndex)\n focusCurrentItem(newIndex)\n }\n }\n },\n Enter: () => {\n event.preventDefault()\n if (focusedIndex >= 0 && focusedIndex < focusableItems.length) {\n const item = focusableItems[focusedIndex]\n if (item.type === 'select-all') {\n onSelectAll()\n } else if (item.type === 'clear-all') {\n onClearAll()\n } else if (item.type === 'option') {\n onSelect(item.value)\n }\n }\n },\n ' ': () => {\n event.preventDefault()\n if (focusedIndex >= 0 && focusedIndex < focusableItems.length) {\n const item = focusableItems[focusedIndex]\n if (item.type === 'select-all') {\n onSelectAll()\n } else if (item.type === 'clear-all') {\n onClearAll()\n } else if (item.type === 'option') {\n onSelect(item.value)\n }\n }\n },\n Escape: () => {\n event.preventDefault()\n onClose()\n },\n }\n\n const handler = keyHandlers[event.key]\n if (handler) {\n handler()\n }\n },\n [focusableItems, focusedIndex, onSelect, onSelectAll, onClearAll, onClose, focusCurrentItem],\n )\n\n // Get the option index for visual focus styling (accounting for select-all offset)\n const getOptionFocusIndex = useCallback(\n (optionIndex: number): boolean => {\n if (focusedIndex < 0 || focusedIndex >= focusableItems.length) return false\n const item = focusableItems[focusedIndex]\n return item.type === 'option' && item.index === optionIndex\n },\n [focusedIndex, focusableItems],\n )\n\n const isSearchFocused = useMemo(() => {\n if (focusedIndex < 0 || focusedIndex >= focusableItems.length) return false\n return focusableItems[focusedIndex].type === 'search'\n }, [focusedIndex, focusableItems])\n\n const isSelectAllFocused = useMemo(() => {\n if (focusedIndex < 0 || focusedIndex >= focusableItems.length) return false\n return focusableItems[focusedIndex].type === 'select-all'\n }, [focusedIndex, focusableItems])\n\n const isClearAllFocused = useMemo(() => {\n if (focusedIndex < 0 || focusedIndex >= focusableItems.length) return false\n return focusableItems[focusedIndex].type === 'clear-all'\n }, [focusedIndex, focusableItems])\n\n const focusedOptionValue = useMemo(() => {\n if (focusedIndex < 0 || focusedIndex >= focusableItems.length) return null\n const item = focusableItems[focusedIndex]\n return item.type === 'option' ? item.value : null\n }, [focusedIndex, focusableItems])\n\n return {\n focusedIndex,\n setFocusedIndex,\n handleKeyDown,\n getOptionFocusIndex,\n isSearchFocused,\n isSelectAllFocused,\n isClearAllFocused,\n focusedOptionValue,\n }\n}\n\nexport const MultiSelectBase = ({\n className,\n clearAllLabel = 'Clear all',\n closeOnSelect = false,\n dropdownWidth = 'trigger',\n emptyMessage = 'No options found',\n errorMessage,\n defaultValue = [],\n disabled,\n id,\n label,\n loadingMessage = 'Loading options…',\n messageReserveLines = 1,\n messageReserveSpace = true,\n maxCount = 3,\n name,\n onChange,\n options = [],\n placeholder = 'Select options',\n ref,\n searchPlaceholder = 'Search options…',\n selectAllLabel = 'Select all',\n showClearAll = true,\n showSearch = true,\n showSelectAll = true,\n sortAlphabetically = false,\n state = 'default',\n value: valueProp,\n warningMessage,\n 'aria-label': ariaLabel,\n 'aria-describedby': ariaDescribedBy,\n ...props\n}: MultiSelectBaseProps & {\n ref?: Ref<HTMLButtonElement>\n}) => {\n const generatedId = useId()\n const fallbackName = name ?? `multiselect-${generatedId}`\n const multiSelectId = useFormFieldId(id, fallbackName)\n const listboxId = `${multiSelectId}-listbox`\n const errorMessageId = getErrorMessageId(multiSelectId)\n const warningMessageId = `${multiSelectId}-warning`\n const messageId = state === 'error' ? errorMessageId : state === 'warning' && warningMessage ? warningMessageId : undefined\n\n const [isOpen, setIsOpen] = useState(false)\n const { dropdownShiftStyle, setDropdownElement } = useAutoDropdownHorizontalShift(isOpen)\n const [searchValue, setSearchValue] = useState('')\n const [value, setValue] = useUncontrolledState<string[]>({\n value: valueProp,\n defaultValue,\n onChange,\n })\n\n const searchInputRef = useRef<HTMLInputElement>(null)\n\n const isDisabled = Boolean(disabled)\n const isLoading = state === 'loading'\n const ariaProps = getAriaProps(state, ariaDescribedBy, props.required, messageId)\n const { dropdownOverflowStyle, dropdownWidthMode, resolvedDropdownWidth } = getDropdownWidthStyles({\n dropdownWidth,\n triggerWidth: 'var(--radix-popover-trigger-width)',\n })\n\n const filteredOptions = useMemo(() => {\n let filtered = options.filter((option) => option.label.toLowerCase().includes(searchValue.toLowerCase()))\n\n if (sortAlphabetically) {\n filtered = [...filtered].sort((a, b) => a.label.localeCompare(b.label))\n }\n\n return filtered\n }, [options, searchValue, sortAlphabetically])\n\n const groupedOptions = useMemo(() => {\n const groups: Record<string, MultiSelectOption[]> = {}\n const ungrouped: MultiSelectOption[] = []\n\n filteredOptions.forEach((option) => {\n if (option.group) {\n if (!groups[option.group]) {\n groups[option.group] = []\n }\n groups[option.group].push(option)\n } else {\n ungrouped.push(option)\n }\n })\n\n return { groups, ungrouped, hasGroups: Object.keys(groups).length > 0 }\n }, [filteredOptions])\n\n const toggleOption = useCallback(\n (optionValue: string) => {\n const option = options.find((o) => o.value === optionValue)\n if (option?.disabled) return\n\n const newValue = value.includes(optionValue) ? value.filter((v) => v !== optionValue) : [...value, optionValue]\n\n setValue(newValue)\n\n if (closeOnSelect) {\n setIsOpen(false)\n }\n },\n [closeOnSelect, options, setValue, value],\n )\n\n const handleSelectAll = useCallback(() => {\n const allValues = options.filter((o) => !o.disabled).map((o) => o.value)\n const isAllSelected = allValues.every((v) => value.includes(v))\n\n if (isAllSelected) {\n setValue([])\n } else {\n setValue(allValues)\n }\n }, [options, setValue, value])\n\n const handleClearAll = useCallback(() => {\n setValue([])\n }, [setValue])\n\n // Check if all non-disabled options are selected\n const allSelectableValues = useMemo(() => options.filter((o) => !o.disabled).map((o) => o.value), [options])\n const isAllSelected = allSelectableValues.length > 0 && allSelectableValues.every((v) => value.includes(v))\n\n const { focusedOptionValue, getOptionFocusIndex, handleKeyDown, isSelectAllFocused, setFocusedIndex } = useKeyboardNavigation(\n filteredOptions,\n handleClearAll,\n () => setIsOpen(false),\n toggleOption,\n handleSelectAll,\n searchInputRef,\n showSearch,\n showSelectAll,\n false, // No separate clear-all button in dropdown\n )\n\n // Set initial focus index when dropdown opens/closes\n useEffect(() => {\n if (isOpen) {\n setFocusedIndex(0)\n } else {\n setFocusedIndex(-1)\n }\n }, [isOpen, setFocusedIndex])\n\n const handleSearchChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {\n setSearchValue(e.target.value)\n }, [])\n\n const renderSelectedItems = () => {\n if (value.length === 0) {\n return <span className='min-h-8 flex items-center text-input-text-placeholder'>{placeholder}</span>\n }\n\n const displayedValues = value.slice(0, maxCount)\n const remainingCount = value.length - maxCount\n\n return (\n <div className='gap-1 flex flex-wrap items-center overflow-hidden'>\n {displayedValues.map((val) => {\n const option = options.find((o) => o.value === val)\n if (!option) return null\n\n return (\n <span className='gap-1 px-2 py-1 rounded-md text-xs max-w-48 inline-flex items-center bg-input-bg--selected text-input-text' key={val}>\n <span className='truncate'>{option.label}</span>\n <span\n aria-hidden='true'\n className='hover:text-danger rounded-sm cursor-pointer'\n data-testid='spectral-multiselect-remove-item-button'\n onClick={(e) => {\n e.preventDefault()\n e.stopPropagation()\n toggleOption(val)\n }}\n onPointerDown={(e) => {\n e.stopPropagation()\n }}\n >\n <CloseIcon size={12} />\n </span>\n </span>\n )\n })}\n {remainingCount > 0 && <span className='text-input-text-secondary text-xs py-1 flex items-center tabular-nums'>+{remainingCount} more</span>}\n </div>\n )\n }\n\n const renderOption = (option: MultiSelectOption, index: number) => {\n const isSelected = value.includes(option.value)\n const isFocused = getOptionFocusIndex(index)\n const optionId = `${listboxId}-option-${option.value}`\n\n return (\n <button\n aria-selected={isSelected}\n className={cn(\n 'my-0.5 first:mt-0 last:mb-0 gap-3 rounded-sm px-3 py-2 text-sm flex w-full items-center text-left hover:bg-input-bg--hover focus-visible:bg-input-bg--hover focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',\n isFocused && 'bg-input-bg--hover',\n isSelected && 'font-medium text-input-text',\n )}\n disabled={option.disabled}\n id={optionId}\n key={option.value}\n onClick={() => toggleOption(option.value)}\n role='option'\n type='button'\n >\n <div data-testid='spectral-multiselect-selected-indicator' className={cn('w-4 h-4 rounded flex items-center justify-center border border-input-border', isSelected && 'bg-primary border-primary')}>\n {isSelected && <CheckmarkIcon size={12} />}\n </div>\n <span>{option.label}</span>\n </button>\n )\n }\n\n const getCSSCustomProperties = () => ({\n '--multiselect-border-radius': '0.5rem',\n '--multiselect-trigger-height': '3rem',\n '--multiselect-dropdown-max-height': '20rem',\n })\n\n return (\n <div className='w-full' data-testid='spectral-multiselect-root'>\n {label && (\n <Label className={cn('mb-2 block text-text-primary', isDisabled && 'text-text-secondary')} data-testid='spectral-multiselect-label' htmlFor={multiSelectId}>\n {label}\n </Label>\n )}\n <Popover.Root open={isOpen} onOpenChange={setIsOpen}>\n <div className='relative' data-testid='spectral-multiselect-wrapper' onKeyDown={isOpen ? handleKeyDown : undefined} role='none'>\n <Popover.Trigger asChild>\n <button\n aria-activedescendant={isOpen && focusedOptionValue ? `${listboxId}-option-${focusedOptionValue}` : undefined}\n aria-controls={isOpen ? listboxId : undefined}\n aria-expanded={isOpen}\n aria-label={ariaLabel ?? label}\n className={cn(getTriggerClasses(isOpen, state, className), 'max-h-22 py-2 text-sm')}\n data-state={state}\n data-testid='spectral-multiselect-trigger'\n disabled={isDisabled}\n id={multiSelectId}\n name={name}\n ref={ref}\n role='combobox'\n style={getCSSCustomProperties() as CSSProperties}\n type='button'\n {...ariaProps}\n {...props}\n >\n <div className='min-w-0 flex-1 overflow-hidden' data-testid='spectral-multiselect-selected-items'>\n {renderSelectedItems()}\n </div>\n <div className='gap-2 ml-2 flex shrink-0 items-center'>\n <ChevronDownIcon className={cn('text-input-icon transition-transform duration-200', isOpen && 'rotate-180')} size={20} />\n </div>\n </button>\n </Popover.Trigger>\n {showClearAll && value.length > 0 && (\n <button\n aria-label='Clear all selections'\n className='right-10 text-input-icon hover:text-input-icon--hover rounded-sm absolute top-1/2 z-10 -translate-y-1/2 cursor-pointer focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50'\n data-testid='spectral-multiselect-clear-all-button'\n disabled={isDisabled}\n onClick={(e) => {\n e.stopPropagation()\n handleClearAll()\n document.getElementById(multiSelectId)?.focus()\n }}\n type='button'\n >\n <CloseIcon size={12} />\n </button>\n )}\n\n <Popover.Portal>\n <Popover.Content\n align='start'\n avoidCollisions\n className={getDropdownClasses()}\n collisionPadding={10}\n data-dropdown-width-mode={dropdownWidthMode}\n data-dropdown-width-value={dropdownWidthMode === 'custom' ? dropdownWidth : undefined}\n data-testid='spectral-multiselect-dropdown'\n onOpenAutoFocus={(e) => {\n e.preventDefault()\n if (showSearch) {\n searchInputRef.current?.focus()\n }\n }}\n side='bottom'\n sideOffset={4}\n ref={setDropdownElement}\n style={{\n width: resolvedDropdownWidth,\n ...(dropdownWidth === 'trigger' ? {} : dropdownOverflowStyle),\n ...dropdownShiftStyle,\n }}\n >\n <div className='p-1'>\n {showSearch && (\n <div className='mb-2 relative'>\n <SearchIcon className={cn(ICON_SIZE, 'left-3 text-input-icon absolute top-1/2 -translate-y-1/2')} />\n <input\n aria-label='Search options'\n className='pl-9 pr-3 py-2 text-sm rounded-md focus-visible:ring-black w-full border border-input-border bg-input-bg focus-visible:border-input-border--focus focus-visible:ring-1 focus-visible:outline-none'\n data-testid='spectral-multiselect-search-input'\n onChange={handleSearchChange}\n placeholder={searchPlaceholder}\n ref={searchInputRef}\n type='text'\n value={searchValue}\n />\n </div>\n )}\n\n <div className='max-h-64 overflow-y-auto' id={listboxId} role='listbox' aria-multiselectable='true'>\n {isLoading ? (\n <LoadingState className='text-sm' message={loadingMessage} data-testid='spectral-multiselect-loading' />\n ) : filteredOptions.length === 0 ? (\n <EmptyState className='text-sm' data-testid='spectral-multiselect-empty-message' message={emptyMessage} />\n ) : (\n <>\n {showSelectAll && (\n <div className='mb-1'>\n <button\n className={cn(\n 'my-0.5 first:mt-0 last:mb-0 gap-3 rounded-sm px-3 py-2 text-sm font-medium text-input-text-secondary flex w-full items-center hover:bg-input-bg--hover focus-visible:bg-input-bg--hover focus-visible:outline-none',\n isSelectAllFocused && 'bg-input-bg--hover',\n )}\n data-testid='spectral-multiselect-select-all-button'\n onClick={handleSelectAll}\n type='button'\n >\n {isAllSelected ? clearAllLabel : selectAllLabel}\n </button>\n <div className='mx-3 my-1 h-px bg-input-border' />\n </div>\n )}\n\n {groupedOptions.ungrouped.length > 0 && <div className='mb-1'>{groupedOptions.ungrouped.map((option, index) => renderOption(option, index))}</div>}\n\n {Object.entries(groupedOptions.groups).map(([groupName, groupOptions]) => (\n <div key={groupName} className='mb-1' data-testid='spectral-multiselect-group'>\n {(groupedOptions.ungrouped.length > 0 || Object.keys(groupedOptions.groups).indexOf(groupName) > 0) && <div className='mx-3 my-1 h-px bg-input-border' />}\n <div data-testid='spectral-multiselect-group-name' className='px-3 py-1 text-xs font-semibold text-input-text-secondary tracking-wide uppercase'>\n {groupName}\n </div>\n {groupOptions.map((option, _index) => renderOption(option, filteredOptions.indexOf(option)))}\n </div>\n ))}\n </>\n )}\n </div>\n </div>\n </Popover.Content>\n </Popover.Portal>\n </div>\n </Popover.Root>\n\n <ErrorMessage dataTestId='spectral-multiselect-error-message' id={errorMessageId} message={state === 'error' ? errorMessage : null} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace} />\n <WarningMessage dataTestId='spectral-multiselect-warning-message' id={warningMessageId} message={state === 'warning' ? warningMessage : null} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace} />\n </div>\n )\n}\nMultiSelectBase.displayName = 'MultiSelectBase'\n"],"mappings":"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"}
1
+ {"version":3,"file":"MultiSelectBase.js","names":[],"sources":["../../src/components/MultiSelect/MultiSelectBase.tsx"],"sourcesContent":["import { CheckmarkIcon, ChevronDownIcon, CloseIcon, SearchIcon } from '@components/Icons'\nimport { Label } from '@components/Label/Label'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport * as Popover from '@radix-ui/react-popover'\nimport { useAutoDropdownHorizontalShift } from '@utils/dropdownPositioning'\nimport { EmptyState, ErrorMessage, getAriaProps, getDropdownSurfaceClasses, getDropdownWidthStyles, getErrorMessageId, getTriggerClasses, LoadingState, WarningMessage, useFormFieldId, type BaseFormFieldProps, type DropdownWidth, type FormFieldState } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useEffect, useId, useMemo, useRef, useState, type ButtonHTMLAttributes, type ChangeEvent, type CSSProperties, type KeyboardEvent, type Ref } from 'react'\n\nexport type MultiSelectState = Exclude<FormFieldState, 'disabled'>\n\nexport interface MultiSelectOption {\n disabled?: boolean\n group?: string\n label: string\n value: string\n}\n\nexport interface MultiSelectBaseProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'value' | 'onChange'> {\n clearAllLabel?: string\n closeOnSelect?: boolean\n dropdownWidth?: DropdownWidth\n emptyMessage?: string\n errorMessage?: BaseFormFieldProps['errorMessage']\n id?: string\n label?: string\n loadingMessage?: string\n maxCount?: number\n messageReserveLines?: number\n messageReserveSpace?: boolean\n name?: string\n defaultValue?: string[]\n onChange?: (value: string[]) => void\n options: MultiSelectOption[]\n placeholder?: string\n required?: boolean\n searchPlaceholder?: string\n showClearAll?: boolean\n showSearch?: boolean\n showSelectAll?: boolean\n selectAllLabel?: string\n sortAlphabetically?: boolean\n state?: MultiSelectState\n value?: string[]\n warningMessage?: BaseFormFieldProps['errorMessage']\n 'aria-label'?: string\n 'aria-describedby'?: string\n}\n\nconst ICON_SIZE = 'h-4 w-4'\n\nconst getDropdownClasses = (): string => {\n return cn(\n 'max-h-80 z-50 overflow-hidden',\n getDropdownSurfaceClasses(),\n 'motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:animate-in',\n 'motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:fade-in-0',\n 'motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:zoom-in-95',\n 'motion-safe:data-[side=bottom]:slide-in-from-top-2',\n 'motion-safe:data-[side=top]:slide-in-from-bottom-2',\n 'origin-(--radix-popover-content-transform-origin)',\n )\n}\n\ntype FocusableItem = { type: 'search' } | { type: 'select-all' } | { type: 'option'; index: number; value: string } | { type: 'clear-all' }\n\nconst useKeyboardNavigation = (\n options: MultiSelectOption[],\n onClearAll: () => void,\n onClose: () => void,\n onSelect: (value: string) => void,\n onSelectAll: () => void,\n searchInputRef: React.RefObject<HTMLInputElement | null>,\n showSearch: boolean,\n showSelectAll: boolean,\n showClearAll: boolean,\n) => {\n const [focusedIndex, setFocusedIndex] = useState(-1)\n\n // Build a flat list of all focusable items\n const focusableItems = useMemo((): FocusableItem[] => {\n const items: FocusableItem[] = []\n\n if (showSearch) {\n items.push({ type: 'search' })\n }\n\n if (showSelectAll) {\n items.push({ type: 'select-all' })\n }\n\n options.forEach((option, index) => {\n if (!option.disabled) {\n items.push({ type: 'option', index, value: option.value })\n }\n })\n\n if (showClearAll) {\n items.push({ type: 'clear-all' })\n }\n\n return items\n }, [options, showSearch, showSelectAll, showClearAll])\n\n // Focus the appropriate element when focusedIndex changes\n const focusCurrentItem = useCallback(\n (index: number) => {\n if (index < 0 || index >= focusableItems.length) return\n const item = focusableItems[index]\n if (item.type === 'search') {\n searchInputRef.current?.focus()\n }\n },\n [focusableItems, searchInputRef],\n )\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>) => {\n const currentItem = focusedIndex >= 0 && focusedIndex < focusableItems.length ? focusableItems[focusedIndex] : null\n\n // Don't prevent default for space in search input (allow typing spaces)\n if (event.key === ' ' && currentItem?.type === 'search') {\n return\n }\n\n // Don't prevent default for Enter in search input (allow form submission behavior)\n if (event.key === 'Enter' && currentItem?.type === 'search') {\n return\n }\n\n const keyHandlers: Record<string, () => void> = {\n ArrowDown: () => {\n event.preventDefault()\n const newIndex = Math.min(focusedIndex + 1, focusableItems.length - 1)\n setFocusedIndex(newIndex)\n focusCurrentItem(newIndex)\n },\n ArrowUp: () => {\n event.preventDefault()\n const newIndex = Math.max(focusedIndex - 1, 0)\n setFocusedIndex(newIndex)\n focusCurrentItem(newIndex)\n },\n Tab: () => {\n // Allow Tab to cycle through focusable items\n if (event.shiftKey) {\n if (focusedIndex <= 0) {\n // At start, close dropdown and return to trigger\n onClose()\n } else {\n event.preventDefault()\n const newIndex = focusedIndex - 1\n setFocusedIndex(newIndex)\n focusCurrentItem(newIndex)\n }\n } else {\n if (focusedIndex >= focusableItems.length - 1) {\n // At end, close dropdown and move to next element\n onClose()\n } else {\n event.preventDefault()\n const newIndex = focusedIndex + 1\n setFocusedIndex(newIndex)\n focusCurrentItem(newIndex)\n }\n }\n },\n Enter: () => {\n event.preventDefault()\n if (focusedIndex >= 0 && focusedIndex < focusableItems.length) {\n const item = focusableItems[focusedIndex]\n if (item.type === 'select-all') {\n onSelectAll()\n } else if (item.type === 'clear-all') {\n onClearAll()\n } else if (item.type === 'option') {\n onSelect(item.value)\n }\n }\n },\n ' ': () => {\n event.preventDefault()\n if (focusedIndex >= 0 && focusedIndex < focusableItems.length) {\n const item = focusableItems[focusedIndex]\n if (item.type === 'select-all') {\n onSelectAll()\n } else if (item.type === 'clear-all') {\n onClearAll()\n } else if (item.type === 'option') {\n onSelect(item.value)\n }\n }\n },\n Escape: () => {\n event.preventDefault()\n onClose()\n },\n }\n\n const handler = keyHandlers[event.key]\n if (handler) {\n handler()\n }\n },\n [focusableItems, focusedIndex, onSelect, onSelectAll, onClearAll, onClose, focusCurrentItem],\n )\n\n // Get the option index for visual focus styling (accounting for select-all offset)\n const getOptionFocusIndex = useCallback(\n (optionIndex: number): boolean => {\n if (focusedIndex < 0 || focusedIndex >= focusableItems.length) return false\n const item = focusableItems[focusedIndex]\n return item.type === 'option' && item.index === optionIndex\n },\n [focusedIndex, focusableItems],\n )\n\n const isSearchFocused = useMemo(() => {\n if (focusedIndex < 0 || focusedIndex >= focusableItems.length) return false\n return focusableItems[focusedIndex].type === 'search'\n }, [focusedIndex, focusableItems])\n\n const isSelectAllFocused = useMemo(() => {\n if (focusedIndex < 0 || focusedIndex >= focusableItems.length) return false\n return focusableItems[focusedIndex].type === 'select-all'\n }, [focusedIndex, focusableItems])\n\n const isClearAllFocused = useMemo(() => {\n if (focusedIndex < 0 || focusedIndex >= focusableItems.length) return false\n return focusableItems[focusedIndex].type === 'clear-all'\n }, [focusedIndex, focusableItems])\n\n const focusedOptionValue = useMemo(() => {\n if (focusedIndex < 0 || focusedIndex >= focusableItems.length) return null\n const item = focusableItems[focusedIndex]\n return item.type === 'option' ? item.value : null\n }, [focusedIndex, focusableItems])\n\n return {\n focusedIndex,\n setFocusedIndex,\n handleKeyDown,\n getOptionFocusIndex,\n isSearchFocused,\n isSelectAllFocused,\n isClearAllFocused,\n focusedOptionValue,\n }\n}\n\nexport const MultiSelectBase = ({\n className,\n clearAllLabel = 'Clear all',\n closeOnSelect = false,\n dropdownWidth = 'trigger',\n emptyMessage = 'No options found',\n errorMessage,\n defaultValue = [],\n disabled,\n id,\n label,\n loadingMessage = 'Loading options…',\n messageReserveLines = 1,\n messageReserveSpace = false,\n maxCount = 3,\n name,\n onChange,\n options = [],\n placeholder = 'Select options',\n ref,\n searchPlaceholder = 'Search options…',\n selectAllLabel = 'Select all',\n showClearAll = true,\n showSearch = true,\n showSelectAll = true,\n sortAlphabetically = false,\n state = 'default',\n value: valueProp,\n warningMessage,\n 'aria-label': ariaLabel,\n 'aria-describedby': ariaDescribedBy,\n ...props\n}: MultiSelectBaseProps & {\n ref?: Ref<HTMLButtonElement>\n}) => {\n const generatedId = useId()\n const fallbackName = name ?? `multiselect-${generatedId}`\n const multiSelectId = useFormFieldId(id, fallbackName)\n const listboxId = `${multiSelectId}-listbox`\n const errorMessageId = getErrorMessageId(multiSelectId)\n const warningMessageId = `${multiSelectId}-warning`\n const messageId = state === 'error' ? errorMessageId : state === 'warning' && warningMessage ? warningMessageId : undefined\n\n const [isOpen, setIsOpen] = useState(false)\n const { dropdownShiftStyle, setDropdownElement } = useAutoDropdownHorizontalShift(isOpen)\n const [searchValue, setSearchValue] = useState('')\n const [value, setValue] = useUncontrolledState<string[]>({\n value: valueProp,\n defaultValue,\n onChange,\n })\n\n const searchInputRef = useRef<HTMLInputElement>(null)\n\n const isDisabled = Boolean(disabled)\n const isLoading = state === 'loading'\n const ariaProps = getAriaProps(state, ariaDescribedBy, props.required, messageId)\n const { dropdownOverflowStyle, dropdownWidthMode, resolvedDropdownWidth } = getDropdownWidthStyles({\n dropdownWidth,\n triggerWidth: 'var(--radix-popover-trigger-width)',\n })\n\n const filteredOptions = useMemo(() => {\n let filtered = options.filter((option) => option.label.toLowerCase().includes(searchValue.toLowerCase()))\n\n if (sortAlphabetically) {\n filtered = [...filtered].sort((a, b) => a.label.localeCompare(b.label))\n }\n\n return filtered\n }, [options, searchValue, sortAlphabetically])\n\n const groupedOptions = useMemo(() => {\n const groups: Record<string, MultiSelectOption[]> = {}\n const ungrouped: MultiSelectOption[] = []\n\n filteredOptions.forEach((option) => {\n if (option.group) {\n if (!groups[option.group]) {\n groups[option.group] = []\n }\n groups[option.group].push(option)\n } else {\n ungrouped.push(option)\n }\n })\n\n return { groups, ungrouped, hasGroups: Object.keys(groups).length > 0 }\n }, [filteredOptions])\n\n const toggleOption = useCallback(\n (optionValue: string) => {\n const option = options.find((o) => o.value === optionValue)\n if (option?.disabled) return\n\n const newValue = value.includes(optionValue) ? value.filter((v) => v !== optionValue) : [...value, optionValue]\n\n setValue(newValue)\n\n if (closeOnSelect) {\n setIsOpen(false)\n }\n },\n [closeOnSelect, options, setValue, value],\n )\n\n const handleSelectAll = useCallback(() => {\n const allValues = options.filter((o) => !o.disabled).map((o) => o.value)\n const isAllSelected = allValues.every((v) => value.includes(v))\n\n if (isAllSelected) {\n setValue([])\n } else {\n setValue(allValues)\n }\n }, [options, setValue, value])\n\n const handleClearAll = useCallback(() => {\n setValue([])\n }, [setValue])\n\n // Check if all non-disabled options are selected\n const allSelectableValues = useMemo(() => options.filter((o) => !o.disabled).map((o) => o.value), [options])\n const isAllSelected = allSelectableValues.length > 0 && allSelectableValues.every((v) => value.includes(v))\n\n const { focusedOptionValue, getOptionFocusIndex, handleKeyDown, isSelectAllFocused, setFocusedIndex } = useKeyboardNavigation(\n filteredOptions,\n handleClearAll,\n () => setIsOpen(false),\n toggleOption,\n handleSelectAll,\n searchInputRef,\n showSearch,\n showSelectAll,\n false, // No separate clear-all button in dropdown\n )\n\n // Set initial focus index when dropdown opens/closes\n useEffect(() => {\n if (isOpen) {\n setFocusedIndex(0)\n } else {\n setFocusedIndex(-1)\n }\n }, [isOpen, setFocusedIndex])\n\n const handleSearchChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {\n setSearchValue(e.target.value)\n }, [])\n\n const renderSelectedItems = () => {\n if (value.length === 0) {\n return <span className='min-h-8 flex items-center text-input-text-placeholder'>{placeholder}</span>\n }\n\n const displayedValues = value.slice(0, maxCount)\n const remainingCount = value.length - maxCount\n\n return (\n <div className='gap-1 flex flex-wrap items-center overflow-hidden'>\n {displayedValues.map((val) => {\n const option = options.find((o) => o.value === val)\n if (!option) return null\n\n return (\n <span className='gap-1 px-2 py-1 rounded-md text-xs max-w-48 inline-flex items-center bg-input-bg--selected text-input-text' key={val}>\n <span className='truncate'>{option.label}</span>\n <span\n aria-hidden='true'\n className='hover:text-danger rounded-sm cursor-pointer'\n data-testid='spectral-multiselect-remove-item-button'\n onClick={(e) => {\n e.preventDefault()\n e.stopPropagation()\n toggleOption(val)\n }}\n onPointerDown={(e) => {\n e.stopPropagation()\n }}\n >\n <CloseIcon size={12} />\n </span>\n </span>\n )\n })}\n {remainingCount > 0 && <span className='text-input-text-secondary text-xs py-1 flex items-center tabular-nums'>+{remainingCount} more</span>}\n </div>\n )\n }\n\n const renderOption = (option: MultiSelectOption, index: number) => {\n const isSelected = value.includes(option.value)\n const isFocused = getOptionFocusIndex(index)\n const optionId = `${listboxId}-option-${option.value}`\n\n return (\n <button\n aria-selected={isSelected}\n className={cn(\n 'my-0.5 first:mt-0 last:mb-0 gap-3 rounded-sm px-3 py-2 text-sm flex w-full items-center text-left hover:bg-input-bg--hover focus-visible:bg-input-bg--hover focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',\n isFocused && 'bg-input-bg--hover',\n isSelected && 'font-medium text-input-text',\n )}\n disabled={option.disabled}\n id={optionId}\n key={option.value}\n onClick={() => toggleOption(option.value)}\n role='option'\n type='button'\n >\n <div data-testid='spectral-multiselect-selected-indicator' className={cn('w-4 h-4 rounded flex items-center justify-center border border-input-border', isSelected && 'bg-primary border-primary')}>\n {isSelected && <CheckmarkIcon size={12} />}\n </div>\n <span>{option.label}</span>\n </button>\n )\n }\n\n const getCSSCustomProperties = () => ({\n '--multiselect-border-radius': '0.5rem',\n '--multiselect-trigger-height': '3rem',\n '--multiselect-dropdown-max-height': '20rem',\n })\n\n return (\n <div className='w-full' data-testid='spectral-multiselect-root'>\n {label && (\n <Label className={cn('mb-2 block text-text-primary', isDisabled && 'text-text-secondary')} data-testid='spectral-multiselect-label' htmlFor={multiSelectId}>\n {label}\n </Label>\n )}\n <Popover.Root open={isOpen} onOpenChange={setIsOpen}>\n <div className='relative' data-testid='spectral-multiselect-wrapper' onKeyDown={isOpen ? handleKeyDown : undefined} role='none'>\n <Popover.Trigger asChild>\n <button\n aria-activedescendant={isOpen && focusedOptionValue ? `${listboxId}-option-${focusedOptionValue}` : undefined}\n aria-controls={isOpen ? listboxId : undefined}\n aria-expanded={isOpen}\n aria-label={ariaLabel ?? label}\n className={cn(getTriggerClasses(isOpen, state, className), 'max-h-22 py-2 text-sm')}\n data-state={state}\n data-testid='spectral-multiselect-trigger'\n disabled={isDisabled}\n id={multiSelectId}\n name={name}\n ref={ref}\n role='combobox'\n style={getCSSCustomProperties() as CSSProperties}\n type='button'\n {...ariaProps}\n {...props}\n >\n <div className='min-w-0 flex-1 overflow-hidden' data-testid='spectral-multiselect-selected-items'>\n {renderSelectedItems()}\n </div>\n <div className='gap-2 ml-2 flex shrink-0 items-center'>\n <ChevronDownIcon className={cn('text-input-icon transition-transform duration-200', isOpen && 'rotate-180')} size={20} />\n </div>\n </button>\n </Popover.Trigger>\n {showClearAll && value.length > 0 && (\n <button\n aria-label='Clear all selections'\n className='right-10 text-input-icon hover:text-input-icon--hover rounded-sm absolute top-1/2 z-10 -translate-y-1/2 cursor-pointer focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50'\n data-testid='spectral-multiselect-clear-all-button'\n disabled={isDisabled}\n onClick={(e) => {\n e.stopPropagation()\n handleClearAll()\n document.getElementById(multiSelectId)?.focus()\n }}\n type='button'\n >\n <CloseIcon size={12} />\n </button>\n )}\n\n <Popover.Portal>\n <Popover.Content\n align='start'\n avoidCollisions\n className={getDropdownClasses()}\n collisionPadding={10}\n data-dropdown-width-mode={dropdownWidthMode}\n data-dropdown-width-value={dropdownWidthMode === 'custom' ? dropdownWidth : undefined}\n data-testid='spectral-multiselect-dropdown'\n onOpenAutoFocus={(e) => {\n e.preventDefault()\n if (showSearch) {\n searchInputRef.current?.focus()\n }\n }}\n side='bottom'\n sideOffset={4}\n ref={setDropdownElement}\n style={{\n width: resolvedDropdownWidth,\n ...(dropdownWidth === 'trigger' ? {} : dropdownOverflowStyle),\n ...dropdownShiftStyle,\n }}\n >\n <div className='p-1'>\n {showSearch && (\n <div className='mb-2 relative'>\n <SearchIcon className={cn(ICON_SIZE, 'left-3 text-input-icon absolute top-1/2 -translate-y-1/2')} />\n <input\n aria-label='Search options'\n className='pl-9 pr-3 py-2 text-sm rounded-md focus-visible:ring-black w-full border border-input-border bg-input-bg focus-visible:border-input-border--focus focus-visible:ring-1 focus-visible:outline-none'\n data-testid='spectral-multiselect-search-input'\n onChange={handleSearchChange}\n placeholder={searchPlaceholder}\n ref={searchInputRef}\n type='text'\n value={searchValue}\n />\n </div>\n )}\n\n <div className='max-h-64 overflow-y-auto' id={listboxId} role='listbox' aria-multiselectable='true'>\n {isLoading ? (\n <LoadingState className='text-sm' message={loadingMessage} data-testid='spectral-multiselect-loading' />\n ) : filteredOptions.length === 0 ? (\n <EmptyState className='text-sm' data-testid='spectral-multiselect-empty-message' message={emptyMessage} />\n ) : (\n <>\n {showSelectAll && (\n <div className='mb-1'>\n <button\n className={cn(\n 'my-0.5 first:mt-0 last:mb-0 gap-3 rounded-sm px-3 py-2 text-sm font-medium text-input-text-secondary flex w-full items-center hover:bg-input-bg--hover focus-visible:bg-input-bg--hover focus-visible:outline-none',\n isSelectAllFocused && 'bg-input-bg--hover',\n )}\n data-testid='spectral-multiselect-select-all-button'\n onClick={handleSelectAll}\n type='button'\n >\n {isAllSelected ? clearAllLabel : selectAllLabel}\n </button>\n <div className='mx-3 my-1 h-px bg-input-border' />\n </div>\n )}\n\n {groupedOptions.ungrouped.length > 0 && <div className='mb-1'>{groupedOptions.ungrouped.map((option, index) => renderOption(option, index))}</div>}\n\n {Object.entries(groupedOptions.groups).map(([groupName, groupOptions]) => (\n <div key={groupName} className='mb-1' data-testid='spectral-multiselect-group'>\n {(groupedOptions.ungrouped.length > 0 || Object.keys(groupedOptions.groups).indexOf(groupName) > 0) && <div className='mx-3 my-1 h-px bg-input-border' />}\n <div data-testid='spectral-multiselect-group-name' className='px-3 py-1 text-xs font-semibold text-input-text-secondary tracking-wide uppercase'>\n {groupName}\n </div>\n {groupOptions.map((option, _index) => renderOption(option, filteredOptions.indexOf(option)))}\n </div>\n ))}\n </>\n )}\n </div>\n </div>\n </Popover.Content>\n </Popover.Portal>\n </div>\n </Popover.Root>\n\n <ErrorMessage dataTestId='spectral-multiselect-error-message' id={errorMessageId} message={state === 'error' ? errorMessage : null} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace && state === 'error'} />\n <WarningMessage dataTestId='spectral-multiselect-warning-message' id={warningMessageId} message={state === 'warning' ? warningMessage : null} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace && state === 'warning'} />\n </div>\n )\n}\nMultiSelectBase.displayName = 'MultiSelectBase'\n"],"mappings":"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"}
@@ -18,7 +18,7 @@ const RadioGroupContext = createContext({
18
18
  const DISABLED_STYLES = "pointer-events-none opacity-60";
19
19
  const RadioGroup = (allProps) => {
20
20
  const isControlled = "selected" in allProps;
21
- const { className, disabled, errorMessage, itemClassName, messageReserveLines = 1, messageReserveSpace = true, name, onChange, onValueChange, orientation = "vertical", ref, selected: selectedProp, state = "default", variant = "default", warningMessage, "aria-describedby": ariaDescribedBy, ...props } = allProps;
21
+ const { className, disabled, errorMessage, itemClassName, messageReserveLines = 1, messageReserveSpace = false, name, onChange, onValueChange, orientation = "vertical", ref, selected: selectedProp, state = "default", variant = "default", warningMessage, "aria-describedby": ariaDescribedBy, ...props } = allProps;
22
22
  const selected = isControlled ? selectedProp ?? "" : selectedProp;
23
23
  const groupId = useFormFieldId(props.id, name);
24
24
  const errorMessageId = `${groupId}-error`;
@@ -67,14 +67,14 @@ const RadioGroup = (allProps) => {
67
67
  id: errorMessageId,
68
68
  message: state === "error" ? errorMessage : null,
69
69
  messageReserveLines,
70
- messageReserveSpace
70
+ messageReserveSpace: messageReserveSpace && state === "error"
71
71
  }),
72
72
  /* @__PURE__ */ jsx(WarningMessage, {
73
73
  dataTestId: "spectral-radio-group-warning-message",
74
74
  id: warningMessageId,
75
75
  message: state === "warning" ? warningMessage : null,
76
76
  messageReserveLines,
77
- messageReserveSpace
77
+ messageReserveSpace: messageReserveSpace && state === "warning"
78
78
  })
79
79
  ]
80
80
  })