@viax.io/uxm 4.36.0 → 4.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +74 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js.map +1 -1
- package/dist/previews/index.cjs +3 -0
- package/dist/previews/index.cjs.map +1 -1
- package/dist/previews/index.d.ts +3 -2
- package/dist/previews/index.d.ts.map +1 -1
- package/dist/previews/index.js +2 -0
- package/dist/previews/index.js.map +1 -1
- package/dist/studio/index.cjs.map +1 -1
- package/dist/studio/index.d.ts +2 -2
- package/dist/studio/index.js.map +1 -1
- package/dist/studio/lib/registry/app.cjs +52 -0
- package/dist/studio/lib/registry/app.cjs.map +1 -1
- package/dist/studio/lib/registry/app.d.ts.map +1 -1
- package/dist/studio/lib/registry/app.js +52 -0
- package/dist/studio/lib/registry/app.js.map +1 -1
- package/dist/studio/lib/registry/buttons.cjs +2 -2
- package/dist/studio/lib/registry/buttons.cjs.map +1 -1
- package/dist/studio/lib/registry/buttons.js +2 -2
- package/dist/studio/lib/registry/buttons.js.map +1 -1
- package/dist/studio/lib/registry/composite.cjs +1 -1
- package/dist/studio/lib/registry/composite.cjs.map +1 -1
- package/dist/studio/lib/registry/composite.js +1 -1
- package/dist/studio/lib/registry/composite.js.map +1 -1
- package/dist/studio/lib/registry/diagram.cjs +1 -1
- package/dist/studio/lib/registry/diagram.cjs.map +1 -1
- package/dist/studio/lib/registry/diagram.js +1 -1
- package/dist/studio/lib/registry/diagram.js.map +1 -1
- package/dist/studio/lib/registry/display.cjs +2 -2
- package/dist/studio/lib/registry/display.cjs.map +1 -1
- package/dist/studio/lib/registry/display.js +2 -2
- package/dist/studio/lib/registry/display.js.map +1 -1
- package/dist/studio/lib/registry/feedback.cjs +3 -3
- package/dist/studio/lib/registry/feedback.cjs.map +1 -1
- package/dist/studio/lib/registry/feedback.js +3 -3
- package/dist/studio/lib/registry/feedback.js.map +1 -1
- package/dist/studio/lib/registry/inputs.cjs +12 -12
- package/dist/studio/lib/registry/inputs.cjs.map +1 -1
- package/dist/studio/lib/registry/inputs.js +12 -12
- package/dist/studio/lib/registry/inputs.js.map +1 -1
- package/dist/studio/lib/types.cjs.map +1 -1
- package/dist/studio/persistence/generate-css.cjs +15 -0
- package/dist/studio/persistence/generate-css.cjs.map +1 -1
- package/dist/studio/persistence/generate-css.d.ts.map +1 -1
- package/dist/studio/persistence/generate-css.js +15 -0
- package/dist/studio/persistence/generate-css.js.map +1 -1
- package/dist/studio/shell/canvas.cjs +1 -0
- package/dist/studio/shell/canvas.cjs.map +1 -1
- package/dist/studio/shell/canvas.d.ts.map +1 -1
- package/dist/studio/shell/canvas.js +2 -0
- package/dist/studio/shell/canvas.js.map +1 -1
- package/dist/ui/calendar/calendar.cjs +3 -1
- package/dist/ui/calendar/calendar.cjs.map +1 -1
- package/dist/ui/calendar/calendar.d.ts +6 -2
- package/dist/ui/calendar/calendar.d.ts.map +1 -1
- package/dist/ui/calendar/calendar.js +3 -1
- package/dist/ui/calendar/calendar.js.map +1 -1
- package/dist/ui/currency-input/currency-input.cjs +3 -1
- package/dist/ui/currency-input/currency-input.cjs.map +1 -1
- package/dist/ui/currency-input/currency-input.d.ts +4 -2
- package/dist/ui/currency-input/currency-input.d.ts.map +1 -1
- package/dist/ui/currency-input/currency-input.js +3 -1
- package/dist/ui/currency-input/currency-input.js.map +1 -1
- package/dist/ui/date-input/date-input.cjs +2 -1
- package/dist/ui/date-input/date-input.cjs.map +1 -1
- package/dist/ui/date-input/date-input.d.ts +11 -1
- package/dist/ui/date-input/date-input.d.ts.map +1 -1
- package/dist/ui/date-input/date-input.js +2 -1
- package/dist/ui/date-input/date-input.js.map +1 -1
- package/dist/ui/editable-cell/editable-cell.cjs +20 -6
- package/dist/ui/editable-cell/editable-cell.cjs.map +1 -1
- package/dist/ui/editable-cell/editable-cell.d.ts +24 -1
- package/dist/ui/editable-cell/editable-cell.d.ts.map +1 -1
- package/dist/ui/editable-cell/editable-cell.js +20 -6
- package/dist/ui/editable-cell/editable-cell.js.map +1 -1
- package/dist/ui/file-upload/file-upload.cjs +36 -9
- package/dist/ui/file-upload/file-upload.cjs.map +1 -1
- package/dist/ui/file-upload/file-upload.d.ts +46 -4
- package/dist/ui/file-upload/file-upload.d.ts.map +1 -1
- package/dist/ui/file-upload/file-upload.js +36 -9
- package/dist/ui/file-upload/file-upload.js.map +1 -1
- package/dist/ui/index.cjs +12 -2
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.ts +5 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +7 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/language-switcher/index.cjs +29 -0
- package/dist/ui/language-switcher/index.cjs.map +1 -0
- package/dist/ui/language-switcher/index.d.ts +3 -0
- package/dist/ui/language-switcher/index.d.ts.map +1 -0
- package/dist/ui/language-switcher/index.js +5 -0
- package/dist/ui/language-switcher/index.js.map +1 -0
- package/dist/ui/language-switcher/language-switcher-preview.cjs +137 -0
- package/dist/ui/language-switcher/language-switcher-preview.cjs.map +1 -0
- package/dist/ui/language-switcher/language-switcher-preview.d.ts +3 -0
- package/dist/ui/language-switcher/language-switcher-preview.d.ts.map +1 -0
- package/dist/ui/language-switcher/language-switcher-preview.js +113 -0
- package/dist/ui/language-switcher/language-switcher-preview.js.map +1 -0
- package/dist/ui/language-switcher/language-switcher.cjs +127 -0
- package/dist/ui/language-switcher/language-switcher.cjs.map +1 -0
- package/dist/ui/language-switcher/language-switcher.css +95 -0
- package/dist/ui/language-switcher/language-switcher.d.ts +73 -0
- package/dist/ui/language-switcher/language-switcher.d.ts.map +1 -0
- package/dist/ui/language-switcher/language-switcher.js +103 -0
- package/dist/ui/language-switcher/language-switcher.js.map +1 -0
- package/dist/ui/locale/index.cjs +33 -0
- package/dist/ui/locale/index.cjs.map +1 -0
- package/dist/ui/locale/index.d.ts +3 -0
- package/dist/ui/locale/index.d.ts.map +1 -0
- package/dist/ui/locale/index.js +7 -0
- package/dist/ui/locale/index.js.map +1 -0
- package/dist/ui/locale/locale.cjs +43 -0
- package/dist/ui/locale/locale.cjs.map +1 -0
- package/dist/ui/locale/locale.d.ts +49 -0
- package/dist/ui/locale/locale.d.ts.map +1 -0
- package/dist/ui/locale/locale.js +17 -0
- package/dist/ui/locale/locale.js.map +1 -0
- package/dist/ui/page-shell/page-shell.cjs.map +1 -1
- package/dist/ui/page-shell/page-shell.d.ts +1 -1
- package/dist/ui/page-shell/page-shell.d.ts.map +1 -1
- package/dist/ui/page-shell/page-shell.js.map +1 -1
- package/dist/ui/range-slider/range-slider.cjs +7 -3
- package/dist/ui/range-slider/range-slider.cjs.map +1 -1
- package/dist/ui/range-slider/range-slider.d.ts +17 -1
- package/dist/ui/range-slider/range-slider.d.ts.map +1 -1
- package/dist/ui/range-slider/range-slider.js +7 -3
- package/dist/ui/range-slider/range-slider.js.map +1 -1
- package/dist/ui/styles.css +2 -1
- package/package.json +4 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/ui/currency-input/currency-input.tsx"],"sourcesContent":["import { useCallback, useRef, useState } from 'react';\n\nimport { cn } from '@/helpers';\n\nimport {\n CURATED_CURRENCIES,\n findCurrency,\n type Currency,\n} from '../../lib/currencies';\nimport { FieldError } from '../field-error';\nimport { Icon } from '../icon';\nimport { IconButton } from '../icon-button';\nimport { Listbox } from '../listbox';\n\nimport type { ChangeEvent, FocusEvent, InputHTMLAttributes } from 'react';\n\n/** CurrencyInput value: split into ISO 4217 code and the raw amount string. */\nexport interface CurrencyValue {\n /** ISO 4217 currency code (e.g. \"USD\"). */\n currency: string;\n /** Amount as a raw digit string with optional `.` decimal (e.g. \"1234.56\"). */\n amount: string;\n}\n\nexport interface CurrencyInputProps\n extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n 'onChange' | 'value' | 'defaultValue' | 'type' | 'min' | 'max' | 'style'\n > {\n /** Controlled value. Pair with `onChange`. */\n value?: CurrencyValue;\n /** Initial value for uncontrolled usage. */\n defaultValue?: CurrencyValue;\n /** Fires on every keystroke or currency pick with the next value. */\n onChange?: (next: CurrencyValue) => void;\n /**\n * Currency list shown in the picker. Defaults to ~20 curated entries\n * covering the regions most B2B / fintech apps transact in. Pass a\n * custom list to extend or restrict (e.g. pass a single-entry array\n * to effectively lock currency selection at design time).\n */\n currencies?: Currency[];\n /**\n * BCP-47 locale tag (e.g. `\"en-US\"`, `\"de-DE\"`). Drives thousands\n * separator style on the blur-display format. Defaults to `\"en-US\"`.\n * Does NOT drive symbol position — the symbol always sits in the\n * leading picker slot, mirroring PhoneInput's country-code shape.\n */\n locale?: string;\n /** Clamp the committed amount down to this minimum on blur. */\n min?: number;\n /** Clamp the committed amount up to this maximum on blur. */\n max?: number;\n /** Allow a leading `-` sign for refunds / credits. Defaults to `false`. */\n allowNegative?: boolean;\n /**\n * Show a clear (✕) button at the trailing edge when the amount has a\n * value. On by default (opt out with `clearable={false}`). The component\n * owns the reset — it wipes its own uncontrolled state and fires `onChange`\n * with an empty amount (keeping the selected currency), so no separate\n * `onClear` is needed (mirrors NumberInput / PhoneInput).\n */\n clearable?: boolean;\n /** Accessible name for the clear button. Default `\"Clear\"`. */\n clearLabel?: string;\n /** Accessible name for the currency picker trigger. Default `\"Choose currency\"`. */\n chooseCurrencyLabel?: string;\n /**\n * Inline style applied to the WRAPPER (not the inner <input>). CSS\n * custom properties set here cascade to the inner field AND the\n * popover so theming knobs reach all parts.\n */\n style?: React.CSSProperties;\n /**\n * When set to a non-empty string, the field renders in its error state:\n * red border (`.uxm-currency-input--error`), `aria-invalid` on the\n * amount input, and the message rendered below the field. Omit (or pass\n * an empty string) for the normal state.\n */\n error?: string;\n}\n\n/** Strip non-digit-non-dot and cap decimals to the active currency's precision. */\nfunction maskAmount(raw: string, allowNegative: boolean, decimals: number): string {\n if (raw === '') return '';\n let negative = false;\n let body = raw;\n if (allowNegative && body.startsWith('-')) {\n negative = true;\n body = body.slice(1);\n }\n body = body.replace(/[^\\d.]/g, '');\n if (decimals === 0) {\n body = body.replace(/\\./g, '');\n } else {\n const firstDot = body.indexOf('.');\n if (firstDot !== -1) {\n body = body.slice(0, firstDot + 1) + body.slice(firstDot + 1).replace(/\\./g, '');\n const [intPart, decPart = ''] = body.split('.');\n body = intPart + '.' + decPart.slice(0, decimals);\n }\n }\n return negative ? '-' + body : body;\n}\n\n/** `1234.5` → `1,234.50` (locale-aware). Empty / partial inputs pass through. */\nfunction formatForDisplay(raw: string, locale: string, decimals: number): string {\n if (raw === '' || raw === '-' || raw === '.') return raw;\n const n = Number(raw);\n if (!Number.isFinite(n)) return raw;\n return new Intl.NumberFormat(locale, {\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n }).format(n);\n}\n\n/** Parse a locale-formatted display string back into the raw digit form. */\nfunction parseFromDisplay(formatted: string, allowNegative: boolean, decimals: number): string {\n let body = formatted;\n // Normalize a European `,` decimal (`1.234,56`) into our internal `.`\n // form so the same mask works regardless of locale on input.\n if (!body.includes('.') && body.includes(',')) {\n const lastComma = body.lastIndexOf(',');\n body = body.slice(0, lastComma) + '.' + body.slice(lastComma + 1);\n }\n return maskAmount(body, allowNegative, decimals);\n}\n\nfunction clampNumeric(value: string, min?: number, max?: number): string {\n if (value === '' || value === '-' || value === '.') return value;\n const n = Number(value);\n if (!Number.isFinite(n)) return value;\n let clamped = n;\n if (min !== undefined && clamped < min) clamped = min;\n if (max !== undefined && clamped > max) clamped = max;\n if (clamped === n) return value;\n return String(clamped);\n}\n\n/** Case-insensitive currency filter — matches name, ISO code, or symbol. */\nfunction filterCurrencies(items: Currency[], query: string): Currency[] {\n const q = query.trim().toLowerCase();\n if (!q) return items;\n return items.filter(\n (c) =>\n c.name.toLowerCase().includes(q) ||\n c.code.toLowerCase().includes(q) ||\n c.symbol.toLowerCase().includes(q),\n );\n}\n\n/**\n * A monetary input with a leading interactive currency picker. Mirrors\n * PhoneInput's architecture: the leading slot is a clickable button\n * that opens a searchable popover; the inner field handles the value.\n * Value is an object `{ currency, amount }` — `currency` is the ISO\n * 4217 code, `amount` is the raw digit string (no thousands\n * separators or symbol).\n *\n * Display behavior is the standard finance UX pattern:\n * - On focus → show raw digits (easy to edit)\n * - On blur → show locale-formatted (`1,234.56`, easy to read)\n *\n * Pass a single-entry `currencies` array to effectively lock the\n * currency at design time (the picker still renders but only offers\n * one option). For non-monetary numeric input use `NumberInput`; for\n * integer values with step buttons use `NumberStepper`.\n */\nexport function CurrencyInput({\n value,\n defaultValue,\n onChange,\n currencies = CURATED_CURRENCIES,\n locale = 'en-US',\n min,\n max,\n allowNegative = false,\n clearable = true,\n clearLabel = 'Clear',\n chooseCurrencyLabel = 'Choose currency',\n className,\n style,\n disabled,\n onFocus,\n onBlur,\n placeholder,\n error,\n ...rest\n}: CurrencyInputProps) {\n // Uncontrolled default currency must match what's actually displayed —\n // `currencies[0]` when the consumer restricts the list, `'USD'` only when\n // no list is given at all. Hardcoding `'USD'` here would desync every\n // `onChange` from the displayed currency whenever `currencies` excludes it.\n const [internal, setInternal] = useState<CurrencyValue>(\n () => defaultValue ?? { currency: currencies[0]?.code ?? 'USD', amount: '' },\n );\n const isControlled = value !== undefined;\n const current = isControlled ? value : internal;\n\n const [focused, setFocused] = useState(false);\n // The field wrapper is the picker's popover anchor: the clickable trigger is\n // the small currency button, but the panel spans the full field width.\n const containerRef = useRef<HTMLDivElement>(null);\n\n const currency = findCurrency(current.currency, currencies) ?? currencies[0];\n const decimals = currency.decimals;\n\n const commit = useCallback(\n (next: CurrencyValue) => {\n if (!isControlled) setInternal(next);\n onChange?.(next);\n },\n [isControlled, onChange],\n );\n\n const handleAmountChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n // Focused → input shows raw digits, mask treats input as raw.\n // Blurred → input shows formatted text; parse-from-display\n // strips separators first (rare path — users typically focus\n // before typing).\n const next = focused\n ? maskAmount(e.target.value, allowNegative, decimals)\n : parseFromDisplay(e.target.value, allowNegative, decimals);\n commit({ currency: current.currency, amount: next });\n },\n [focused, allowNegative, decimals, current.currency, commit],\n );\n\n const handleCurrencyPick = useCallback(\n (code: string) => {\n // Switching currency may invalidate the current decimal count\n // (USD/2 → JPY/0). Re-mask the amount under the new precision\n // so the displayed value matches what the new currency allows.\n const nextCurrency = findCurrency(code, currencies);\n const nextDecimals = nextCurrency?.decimals ?? 2;\n const reMasked = maskAmount(current.amount, allowNegative, nextDecimals);\n commit({ currency: code, amount: reMasked });\n },\n [currencies, current.amount, allowNegative, commit],\n );\n\n const handleFocus = useCallback(\n (e: FocusEvent<HTMLInputElement>) => {\n setFocused(true);\n onFocus?.(e);\n },\n [onFocus],\n );\n\n const handleBlur = useCallback(\n (e: FocusEvent<HTMLInputElement>) => {\n setFocused(false);\n const clamped = clampNumeric(current.amount, min, max);\n if (clamped !== current.amount) {\n commit({ currency: current.currency, amount: clamped });\n }\n onBlur?.(e);\n },\n [current.amount, current.currency, min, max, commit, onBlur],\n );\n\n // Clear owns its own reset: wipe the amount but KEEP the selected currency\n // (the picker is a separate affordance — clearing the value shouldn't reset\n // it). Mirrors NumberInput / PhoneInput which hold their own value state,\n // so no `onClear` prop is needed.\n const handleClear = useCallback(() => {\n commit({ currency: current.currency, amount: '' });\n }, [commit, current.currency]);\n const showClear = clearable && current.amount !== '' && !disabled;\n\n // Focus toggle drives whether the input shows raw digits or the\n // locale-formatted display value. Same pattern as standard finance\n // inputs (Stripe, Wise, etc.).\n const displayValue = focused\n ? current.amount\n : formatForDisplay(current.amount, locale, decimals);\n\n return (\n <>\n <div\n className={cn(\n 'uxm-currency-input',\n disabled && 'uxm-currency-input--disabled',\n error && 'uxm-currency-input--error',\n className,\n )}\n style={style}\n ref={containerRef}\n >\n {/* Currency picker — the shared Listbox owns the popover (positioning,\n portal, dismiss, keyboard nav, search, ARIA). `anchorRef` points at\n the field wrapper so the panel spans the full width; `showCheckmark`\n is off because each row already carries its ISO code on the right. */}\n <Listbox<Currency>\n items={currencies}\n getKey={(c) => c.code}\n getLabel={(c) => c.name}\n filterItems={filterCurrencies}\n value={currency}\n onChange={(next) => next && handleCurrencyPick(next.code)}\n disabled={disabled}\n anchorRef={containerRef}\n placement=\"bottom-start\"\n searchPlaceholder=\"Search currencies\"\n searchAriaLabel=\"Search currencies\"\n showCheckmark={false}\n panelClassName=\"uxm-currency-input__panel\"\n panelStyle={style}\n className=\"uxm-currency-input__picker-wrap\"\n aria-label={chooseCurrencyLabel}\n renderTrigger={({ open, triggerProps }) => (\n <button\n {...triggerProps}\n type=\"button\"\n className=\"uxm-currency-input__picker\"\n aria-label={`Currency: ${currency.name}`}\n >\n <span className=\"uxm-currency-input__symbol\" aria-hidden=\"true\">{currency.symbol}</span>\n <span className=\"uxm-currency-input__code\">{currency.code}</span>\n <span className=\"uxm-currency-input__caret\" aria-hidden=\"true\">\n <Icon\n glyph=\"chevron-down\"\n size={12}\n strokeWidth={2.2}\n style={{ transform: open ? 'rotate(180deg)' : 'none' }}\n />\n </span>\n </button>\n )}\n renderItem={(c) => (\n <>\n <span className=\"uxm-currency-input__row-symbol\" aria-hidden=\"true\">{c.symbol}</span>\n <span className=\"uxm-currency-input__row-name\">{c.name}</span>\n <span className=\"uxm-currency-input__row-code\">{c.code}</span>\n </>\n )}\n />\n <input\n // Same `type=\"text\"` + `inputMode=\"decimal\"` rationale as\n // PhoneInput / NumberInput: native `type=\"number\"` ships\n // browser spinners + autofill heuristics that fight our mask.\n type=\"text\"\n inputMode={decimals > 0 ? 'decimal' : 'numeric'}\n autoComplete=\"off\"\n className={cn(\n 'uxm-currency-input__input',\n clearable && 'uxm-currency-input__input--clearable',\n )}\n value={displayValue}\n onChange={handleAmountChange}\n onFocus={handleFocus}\n onBlur={handleBlur}\n disabled={disabled}\n placeholder={placeholder ?? (decimals > 0 ? '0.00' : '0')}\n aria-invalid={error ? true : undefined}\n {...rest}\n />\n {showClear && (\n <IconButton\n className=\"uxm-field-clear uxm-currency-input__clear\"\n aria-label={clearLabel}\n // Prevent the button from stealing focus away from whatever's\n // currently focused — a blur here would clamp the about-to-be-wiped\n // value first (same guard as NumberInput).\n onMouseDown={(e) => e.preventDefault()}\n onClick={handleClear}\n >\n <Icon glyph=\"close\" />\n </IconButton>\n )}\n </div>\n {error && <FieldError className=\"uxm-currency-input__error-message\">{error}</FieldError>}\n </>\n );\n}\n// Static marker so FormField only forwards its `error` prop into children\n// that accept one (avoids React unknown-prop warnings on non-input children).\nCurrencyInput.hasError = true;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/currency-input/currency-input.tsx"],"sourcesContent":["import { useCallback, useRef, useState } from 'react';\n\nimport { cn } from '@/helpers';\n\nimport {\n CURATED_CURRENCIES,\n findCurrency,\n type Currency,\n} from '../../lib/currencies';\nimport { FieldError } from '../field-error';\nimport { Icon } from '../icon';\nimport { IconButton } from '../icon-button';\nimport { Listbox } from '../listbox';\nimport { useUxmLocale } from '../locale';\n\nimport type { ChangeEvent, FocusEvent, InputHTMLAttributes } from 'react';\n\n/** CurrencyInput value: split into ISO 4217 code and the raw amount string. */\nexport interface CurrencyValue {\n /** ISO 4217 currency code (e.g. \"USD\"). */\n currency: string;\n /** Amount as a raw digit string with optional `.` decimal (e.g. \"1234.56\"). */\n amount: string;\n}\n\nexport interface CurrencyInputProps\n extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n 'onChange' | 'value' | 'defaultValue' | 'type' | 'min' | 'max' | 'style'\n > {\n /** Controlled value. Pair with `onChange`. */\n value?: CurrencyValue;\n /** Initial value for uncontrolled usage. */\n defaultValue?: CurrencyValue;\n /** Fires on every keystroke or currency pick with the next value. */\n onChange?: (next: CurrencyValue) => void;\n /**\n * Currency list shown in the picker. Defaults to ~20 curated entries\n * covering the regions most B2B / fintech apps transact in. Pass a\n * custom list to extend or restrict (e.g. pass a single-entry array\n * to effectively lock currency selection at design time).\n */\n currencies?: Currency[];\n /**\n * BCP-47 locale tag (e.g. `\"en-US\"`, `\"de-DE\"`). Drives thousands\n * separator style on the blur-display format. Defaults to the nearest\n * `UxmLocaleProvider`, then to `\"en-US\"` — set it here only to pin one\n * amount to a locale that differs from the rest of the app.\n * Does NOT drive symbol position — the symbol always sits in the\n * leading picker slot, mirroring PhoneInput's country-code shape.\n */\n locale?: string;\n /** Clamp the committed amount down to this minimum on blur. */\n min?: number;\n /** Clamp the committed amount up to this maximum on blur. */\n max?: number;\n /** Allow a leading `-` sign for refunds / credits. Defaults to `false`. */\n allowNegative?: boolean;\n /**\n * Show a clear (✕) button at the trailing edge when the amount has a\n * value. On by default (opt out with `clearable={false}`). The component\n * owns the reset — it wipes its own uncontrolled state and fires `onChange`\n * with an empty amount (keeping the selected currency), so no separate\n * `onClear` is needed (mirrors NumberInput / PhoneInput).\n */\n clearable?: boolean;\n /** Accessible name for the clear button. Default `\"Clear\"`. */\n clearLabel?: string;\n /** Accessible name for the currency picker trigger. Default `\"Choose currency\"`. */\n chooseCurrencyLabel?: string;\n /**\n * Inline style applied to the WRAPPER (not the inner <input>). CSS\n * custom properties set here cascade to the inner field AND the\n * popover so theming knobs reach all parts.\n */\n style?: React.CSSProperties;\n /**\n * When set to a non-empty string, the field renders in its error state:\n * red border (`.uxm-currency-input--error`), `aria-invalid` on the\n * amount input, and the message rendered below the field. Omit (or pass\n * an empty string) for the normal state.\n */\n error?: string;\n}\n\n/** Strip non-digit-non-dot and cap decimals to the active currency's precision. */\nfunction maskAmount(raw: string, allowNegative: boolean, decimals: number): string {\n if (raw === '') return '';\n let negative = false;\n let body = raw;\n if (allowNegative && body.startsWith('-')) {\n negative = true;\n body = body.slice(1);\n }\n body = body.replace(/[^\\d.]/g, '');\n if (decimals === 0) {\n body = body.replace(/\\./g, '');\n } else {\n const firstDot = body.indexOf('.');\n if (firstDot !== -1) {\n body = body.slice(0, firstDot + 1) + body.slice(firstDot + 1).replace(/\\./g, '');\n const [intPart, decPart = ''] = body.split('.');\n body = intPart + '.' + decPart.slice(0, decimals);\n }\n }\n return negative ? '-' + body : body;\n}\n\n/** `1234.5` → `1,234.50` (locale-aware). Empty / partial inputs pass through. */\nfunction formatForDisplay(raw: string, locale: string, decimals: number): string {\n if (raw === '' || raw === '-' || raw === '.') return raw;\n const n = Number(raw);\n if (!Number.isFinite(n)) return raw;\n return new Intl.NumberFormat(locale, {\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n }).format(n);\n}\n\n/** Parse a locale-formatted display string back into the raw digit form. */\nfunction parseFromDisplay(formatted: string, allowNegative: boolean, decimals: number): string {\n let body = formatted;\n // Normalize a European `,` decimal (`1.234,56`) into our internal `.`\n // form so the same mask works regardless of locale on input.\n if (!body.includes('.') && body.includes(',')) {\n const lastComma = body.lastIndexOf(',');\n body = body.slice(0, lastComma) + '.' + body.slice(lastComma + 1);\n }\n return maskAmount(body, allowNegative, decimals);\n}\n\nfunction clampNumeric(value: string, min?: number, max?: number): string {\n if (value === '' || value === '-' || value === '.') return value;\n const n = Number(value);\n if (!Number.isFinite(n)) return value;\n let clamped = n;\n if (min !== undefined && clamped < min) clamped = min;\n if (max !== undefined && clamped > max) clamped = max;\n if (clamped === n) return value;\n return String(clamped);\n}\n\n/** Case-insensitive currency filter — matches name, ISO code, or symbol. */\nfunction filterCurrencies(items: Currency[], query: string): Currency[] {\n const q = query.trim().toLowerCase();\n if (!q) return items;\n return items.filter(\n (c) =>\n c.name.toLowerCase().includes(q) ||\n c.code.toLowerCase().includes(q) ||\n c.symbol.toLowerCase().includes(q),\n );\n}\n\n/**\n * A monetary input with a leading interactive currency picker. Mirrors\n * PhoneInput's architecture: the leading slot is a clickable button\n * that opens a searchable popover; the inner field handles the value.\n * Value is an object `{ currency, amount }` — `currency` is the ISO\n * 4217 code, `amount` is the raw digit string (no thousands\n * separators or symbol).\n *\n * Display behavior is the standard finance UX pattern:\n * - On focus → show raw digits (easy to edit)\n * - On blur → show locale-formatted (`1,234.56`, easy to read)\n *\n * Pass a single-entry `currencies` array to effectively lock the\n * currency at design time (the picker still renders but only offers\n * one option). For non-monetary numeric input use `NumberInput`; for\n * integer values with step buttons use `NumberStepper`.\n */\nexport function CurrencyInput({\n value,\n defaultValue,\n onChange,\n currencies = CURATED_CURRENCIES,\n locale: localeProp,\n min,\n max,\n allowNegative = false,\n clearable = true,\n clearLabel = 'Clear',\n chooseCurrencyLabel = 'Choose currency',\n className,\n style,\n disabled,\n onFocus,\n onBlur,\n placeholder,\n error,\n ...rest\n}: CurrencyInputProps) {\n const locale = useUxmLocale(localeProp);\n // Uncontrolled default currency must match what's actually displayed —\n // `currencies[0]` when the consumer restricts the list, `'USD'` only when\n // no list is given at all. Hardcoding `'USD'` here would desync every\n // `onChange` from the displayed currency whenever `currencies` excludes it.\n const [internal, setInternal] = useState<CurrencyValue>(\n () => defaultValue ?? { currency: currencies[0]?.code ?? 'USD', amount: '' },\n );\n const isControlled = value !== undefined;\n const current = isControlled ? value : internal;\n\n const [focused, setFocused] = useState(false);\n // The field wrapper is the picker's popover anchor: the clickable trigger is\n // the small currency button, but the panel spans the full field width.\n const containerRef = useRef<HTMLDivElement>(null);\n\n const currency = findCurrency(current.currency, currencies) ?? currencies[0];\n const decimals = currency.decimals;\n\n const commit = useCallback(\n (next: CurrencyValue) => {\n if (!isControlled) setInternal(next);\n onChange?.(next);\n },\n [isControlled, onChange],\n );\n\n const handleAmountChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n // Focused → input shows raw digits, mask treats input as raw.\n // Blurred → input shows formatted text; parse-from-display\n // strips separators first (rare path — users typically focus\n // before typing).\n const next = focused\n ? maskAmount(e.target.value, allowNegative, decimals)\n : parseFromDisplay(e.target.value, allowNegative, decimals);\n commit({ currency: current.currency, amount: next });\n },\n [focused, allowNegative, decimals, current.currency, commit],\n );\n\n const handleCurrencyPick = useCallback(\n (code: string) => {\n // Switching currency may invalidate the current decimal count\n // (USD/2 → JPY/0). Re-mask the amount under the new precision\n // so the displayed value matches what the new currency allows.\n const nextCurrency = findCurrency(code, currencies);\n const nextDecimals = nextCurrency?.decimals ?? 2;\n const reMasked = maskAmount(current.amount, allowNegative, nextDecimals);\n commit({ currency: code, amount: reMasked });\n },\n [currencies, current.amount, allowNegative, commit],\n );\n\n const handleFocus = useCallback(\n (e: FocusEvent<HTMLInputElement>) => {\n setFocused(true);\n onFocus?.(e);\n },\n [onFocus],\n );\n\n const handleBlur = useCallback(\n (e: FocusEvent<HTMLInputElement>) => {\n setFocused(false);\n const clamped = clampNumeric(current.amount, min, max);\n if (clamped !== current.amount) {\n commit({ currency: current.currency, amount: clamped });\n }\n onBlur?.(e);\n },\n [current.amount, current.currency, min, max, commit, onBlur],\n );\n\n // Clear owns its own reset: wipe the amount but KEEP the selected currency\n // (the picker is a separate affordance — clearing the value shouldn't reset\n // it). Mirrors NumberInput / PhoneInput which hold their own value state,\n // so no `onClear` prop is needed.\n const handleClear = useCallback(() => {\n commit({ currency: current.currency, amount: '' });\n }, [commit, current.currency]);\n const showClear = clearable && current.amount !== '' && !disabled;\n\n // Focus toggle drives whether the input shows raw digits or the\n // locale-formatted display value. Same pattern as standard finance\n // inputs (Stripe, Wise, etc.).\n const displayValue = focused\n ? current.amount\n : formatForDisplay(current.amount, locale, decimals);\n\n return (\n <>\n <div\n className={cn(\n 'uxm-currency-input',\n disabled && 'uxm-currency-input--disabled',\n error && 'uxm-currency-input--error',\n className,\n )}\n style={style}\n ref={containerRef}\n >\n {/* Currency picker — the shared Listbox owns the popover (positioning,\n portal, dismiss, keyboard nav, search, ARIA). `anchorRef` points at\n the field wrapper so the panel spans the full width; `showCheckmark`\n is off because each row already carries its ISO code on the right. */}\n <Listbox<Currency>\n items={currencies}\n getKey={(c) => c.code}\n getLabel={(c) => c.name}\n filterItems={filterCurrencies}\n value={currency}\n onChange={(next) => next && handleCurrencyPick(next.code)}\n disabled={disabled}\n anchorRef={containerRef}\n placement=\"bottom-start\"\n searchPlaceholder=\"Search currencies\"\n searchAriaLabel=\"Search currencies\"\n showCheckmark={false}\n panelClassName=\"uxm-currency-input__panel\"\n panelStyle={style}\n className=\"uxm-currency-input__picker-wrap\"\n aria-label={chooseCurrencyLabel}\n renderTrigger={({ open, triggerProps }) => (\n <button\n {...triggerProps}\n type=\"button\"\n className=\"uxm-currency-input__picker\"\n aria-label={`Currency: ${currency.name}`}\n >\n <span className=\"uxm-currency-input__symbol\" aria-hidden=\"true\">{currency.symbol}</span>\n <span className=\"uxm-currency-input__code\">{currency.code}</span>\n <span className=\"uxm-currency-input__caret\" aria-hidden=\"true\">\n <Icon\n glyph=\"chevron-down\"\n size={12}\n strokeWidth={2.2}\n style={{ transform: open ? 'rotate(180deg)' : 'none' }}\n />\n </span>\n </button>\n )}\n renderItem={(c) => (\n <>\n <span className=\"uxm-currency-input__row-symbol\" aria-hidden=\"true\">{c.symbol}</span>\n <span className=\"uxm-currency-input__row-name\">{c.name}</span>\n <span className=\"uxm-currency-input__row-code\">{c.code}</span>\n </>\n )}\n />\n <input\n // Same `type=\"text\"` + `inputMode=\"decimal\"` rationale as\n // PhoneInput / NumberInput: native `type=\"number\"` ships\n // browser spinners + autofill heuristics that fight our mask.\n type=\"text\"\n inputMode={decimals > 0 ? 'decimal' : 'numeric'}\n autoComplete=\"off\"\n className={cn(\n 'uxm-currency-input__input',\n clearable && 'uxm-currency-input__input--clearable',\n )}\n value={displayValue}\n onChange={handleAmountChange}\n onFocus={handleFocus}\n onBlur={handleBlur}\n disabled={disabled}\n placeholder={placeholder ?? (decimals > 0 ? '0.00' : '0')}\n aria-invalid={error ? true : undefined}\n {...rest}\n />\n {showClear && (\n <IconButton\n className=\"uxm-field-clear uxm-currency-input__clear\"\n aria-label={clearLabel}\n // Prevent the button from stealing focus away from whatever's\n // currently focused — a blur here would clamp the about-to-be-wiped\n // value first (same guard as NumberInput).\n onMouseDown={(e) => e.preventDefault()}\n onClick={handleClear}\n >\n <Icon glyph=\"close\" />\n </IconButton>\n )}\n </div>\n {error && <FieldError className=\"uxm-currency-input__error-message\">{error}</FieldError>}\n </>\n );\n}\n// Static marker so FormField only forwards its `error` prop into children\n// that accept one (avoids React unknown-prop warnings on non-input children).\nCurrencyInput.hasError = true;\n"],"mappings":"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","names":[]}
|
|
@@ -23,7 +23,9 @@ export interface CurrencyInputProps extends Omit<InputHTMLAttributes<HTMLInputEl
|
|
|
23
23
|
currencies?: Currency[];
|
|
24
24
|
/**
|
|
25
25
|
* BCP-47 locale tag (e.g. `"en-US"`, `"de-DE"`). Drives thousands
|
|
26
|
-
* separator style on the blur-display format. Defaults to
|
|
26
|
+
* separator style on the blur-display format. Defaults to the nearest
|
|
27
|
+
* `UxmLocaleProvider`, then to `"en-US"` — set it here only to pin one
|
|
28
|
+
* amount to a locale that differs from the rest of the app.
|
|
27
29
|
* Does NOT drive symbol position — the symbol always sits in the
|
|
28
30
|
* leading picker slot, mirroring PhoneInput's country-code shape.
|
|
29
31
|
*/
|
|
@@ -77,7 +79,7 @@ export interface CurrencyInputProps extends Omit<InputHTMLAttributes<HTMLInputEl
|
|
|
77
79
|
* one option). For non-monetary numeric input use `NumberInput`; for
|
|
78
80
|
* integer values with step buttons use `NumberStepper`.
|
|
79
81
|
*/
|
|
80
|
-
export declare function CurrencyInput({ value, defaultValue, onChange, currencies, locale, min, max, allowNegative, clearable, clearLabel, chooseCurrencyLabel, className, style, disabled, onFocus, onBlur, placeholder, error, ...rest }: CurrencyInputProps): import("react/jsx-runtime").JSX.Element;
|
|
82
|
+
export declare function CurrencyInput({ value, defaultValue, onChange, currencies, locale: localeProp, min, max, allowNegative, clearable, clearLabel, chooseCurrencyLabel, className, style, disabled, onFocus, onBlur, placeholder, error, ...rest }: CurrencyInputProps): import("react/jsx-runtime").JSX.Element;
|
|
81
83
|
export declare namespace CurrencyInput {
|
|
82
84
|
var hasError: boolean;
|
|
83
85
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"currency-input.d.ts","sourceRoot":"","sources":["../../../src/ui/currency-input/currency-input.tsx"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,QAAQ,EACd,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"currency-input.d.ts","sourceRoot":"","sources":["../../../src/ui/currency-input/currency-input.tsx"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,QAAQ,EACd,MAAM,sBAAsB,CAAC;AAO9B,OAAO,KAAK,EAA2B,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAE1E,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBACf,SAAQ,IAAI,CACV,mBAAmB,CAAC,gBAAgB,CAAC,EACrC,UAAU,GAAG,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,OAAO,CACzE;IACD,8CAA8C;IAC9C,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;IACzC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,QAAQ,EAAE,CAAC;IACxB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oFAAoF;IACpF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,UAA+B,EAC/B,MAAM,EAAE,UAAU,EAClB,GAAG,EACH,GAAG,EACH,aAAqB,EACrB,SAAgB,EAChB,UAAoB,EACpB,mBAAuC,EACvC,SAAS,EACT,KAAK,EACL,QAAQ,EACR,OAAO,EACP,MAAM,EACN,WAAW,EACX,KAAK,EACL,GAAG,IAAI,EACR,EAAE,kBAAkB,2CA4LpB;yBAhNe,aAAa"}
|
|
@@ -9,6 +9,7 @@ import { FieldError } from "../field-error/index.js";
|
|
|
9
9
|
import { Icon } from "../icon/index.js";
|
|
10
10
|
import { IconButton } from "../icon-button/index.js";
|
|
11
11
|
import { Listbox } from "../listbox/index.js";
|
|
12
|
+
import { useUxmLocale } from "../locale/index.js";
|
|
12
13
|
function maskAmount(raw, allowNegative, decimals) {
|
|
13
14
|
if (raw === "") return "";
|
|
14
15
|
let negative = false;
|
|
@@ -69,7 +70,7 @@ function CurrencyInput({
|
|
|
69
70
|
defaultValue,
|
|
70
71
|
onChange,
|
|
71
72
|
currencies = CURATED_CURRENCIES,
|
|
72
|
-
locale
|
|
73
|
+
locale: localeProp,
|
|
73
74
|
min,
|
|
74
75
|
max,
|
|
75
76
|
allowNegative = false,
|
|
@@ -85,6 +86,7 @@ function CurrencyInput({
|
|
|
85
86
|
error,
|
|
86
87
|
...rest
|
|
87
88
|
}) {
|
|
89
|
+
const locale = useUxmLocale(localeProp);
|
|
88
90
|
const [internal, setInternal] = useState(
|
|
89
91
|
() => defaultValue ?? { currency: currencies[0]?.code ?? "USD", amount: "" }
|
|
90
92
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/ui/currency-input/currency-input.tsx"],"sourcesContent":["import { useCallback, useRef, useState } from 'react';\n\nimport { cn } from '@/helpers';\n\nimport {\n CURATED_CURRENCIES,\n findCurrency,\n type Currency,\n} from '../../lib/currencies';\nimport { FieldError } from '../field-error';\nimport { Icon } from '../icon';\nimport { IconButton } from '../icon-button';\nimport { Listbox } from '../listbox';\n\nimport type { ChangeEvent, FocusEvent, InputHTMLAttributes } from 'react';\n\n/** CurrencyInput value: split into ISO 4217 code and the raw amount string. */\nexport interface CurrencyValue {\n /** ISO 4217 currency code (e.g. \"USD\"). */\n currency: string;\n /** Amount as a raw digit string with optional `.` decimal (e.g. \"1234.56\"). */\n amount: string;\n}\n\nexport interface CurrencyInputProps\n extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n 'onChange' | 'value' | 'defaultValue' | 'type' | 'min' | 'max' | 'style'\n > {\n /** Controlled value. Pair with `onChange`. */\n value?: CurrencyValue;\n /** Initial value for uncontrolled usage. */\n defaultValue?: CurrencyValue;\n /** Fires on every keystroke or currency pick with the next value. */\n onChange?: (next: CurrencyValue) => void;\n /**\n * Currency list shown in the picker. Defaults to ~20 curated entries\n * covering the regions most B2B / fintech apps transact in. Pass a\n * custom list to extend or restrict (e.g. pass a single-entry array\n * to effectively lock currency selection at design time).\n */\n currencies?: Currency[];\n /**\n * BCP-47 locale tag (e.g. `\"en-US\"`, `\"de-DE\"`). Drives thousands\n * separator style on the blur-display format. Defaults to `\"en-US\"`.\n * Does NOT drive symbol position — the symbol always sits in the\n * leading picker slot, mirroring PhoneInput's country-code shape.\n */\n locale?: string;\n /** Clamp the committed amount down to this minimum on blur. */\n min?: number;\n /** Clamp the committed amount up to this maximum on blur. */\n max?: number;\n /** Allow a leading `-` sign for refunds / credits. Defaults to `false`. */\n allowNegative?: boolean;\n /**\n * Show a clear (✕) button at the trailing edge when the amount has a\n * value. On by default (opt out with `clearable={false}`). The component\n * owns the reset — it wipes its own uncontrolled state and fires `onChange`\n * with an empty amount (keeping the selected currency), so no separate\n * `onClear` is needed (mirrors NumberInput / PhoneInput).\n */\n clearable?: boolean;\n /** Accessible name for the clear button. Default `\"Clear\"`. */\n clearLabel?: string;\n /** Accessible name for the currency picker trigger. Default `\"Choose currency\"`. */\n chooseCurrencyLabel?: string;\n /**\n * Inline style applied to the WRAPPER (not the inner <input>). CSS\n * custom properties set here cascade to the inner field AND the\n * popover so theming knobs reach all parts.\n */\n style?: React.CSSProperties;\n /**\n * When set to a non-empty string, the field renders in its error state:\n * red border (`.uxm-currency-input--error`), `aria-invalid` on the\n * amount input, and the message rendered below the field. Omit (or pass\n * an empty string) for the normal state.\n */\n error?: string;\n}\n\n/** Strip non-digit-non-dot and cap decimals to the active currency's precision. */\nfunction maskAmount(raw: string, allowNegative: boolean, decimals: number): string {\n if (raw === '') return '';\n let negative = false;\n let body = raw;\n if (allowNegative && body.startsWith('-')) {\n negative = true;\n body = body.slice(1);\n }\n body = body.replace(/[^\\d.]/g, '');\n if (decimals === 0) {\n body = body.replace(/\\./g, '');\n } else {\n const firstDot = body.indexOf('.');\n if (firstDot !== -1) {\n body = body.slice(0, firstDot + 1) + body.slice(firstDot + 1).replace(/\\./g, '');\n const [intPart, decPart = ''] = body.split('.');\n body = intPart + '.' + decPart.slice(0, decimals);\n }\n }\n return negative ? '-' + body : body;\n}\n\n/** `1234.5` → `1,234.50` (locale-aware). Empty / partial inputs pass through. */\nfunction formatForDisplay(raw: string, locale: string, decimals: number): string {\n if (raw === '' || raw === '-' || raw === '.') return raw;\n const n = Number(raw);\n if (!Number.isFinite(n)) return raw;\n return new Intl.NumberFormat(locale, {\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n }).format(n);\n}\n\n/** Parse a locale-formatted display string back into the raw digit form. */\nfunction parseFromDisplay(formatted: string, allowNegative: boolean, decimals: number): string {\n let body = formatted;\n // Normalize a European `,` decimal (`1.234,56`) into our internal `.`\n // form so the same mask works regardless of locale on input.\n if (!body.includes('.') && body.includes(',')) {\n const lastComma = body.lastIndexOf(',');\n body = body.slice(0, lastComma) + '.' + body.slice(lastComma + 1);\n }\n return maskAmount(body, allowNegative, decimals);\n}\n\nfunction clampNumeric(value: string, min?: number, max?: number): string {\n if (value === '' || value === '-' || value === '.') return value;\n const n = Number(value);\n if (!Number.isFinite(n)) return value;\n let clamped = n;\n if (min !== undefined && clamped < min) clamped = min;\n if (max !== undefined && clamped > max) clamped = max;\n if (clamped === n) return value;\n return String(clamped);\n}\n\n/** Case-insensitive currency filter — matches name, ISO code, or symbol. */\nfunction filterCurrencies(items: Currency[], query: string): Currency[] {\n const q = query.trim().toLowerCase();\n if (!q) return items;\n return items.filter(\n (c) =>\n c.name.toLowerCase().includes(q) ||\n c.code.toLowerCase().includes(q) ||\n c.symbol.toLowerCase().includes(q),\n );\n}\n\n/**\n * A monetary input with a leading interactive currency picker. Mirrors\n * PhoneInput's architecture: the leading slot is a clickable button\n * that opens a searchable popover; the inner field handles the value.\n * Value is an object `{ currency, amount }` — `currency` is the ISO\n * 4217 code, `amount` is the raw digit string (no thousands\n * separators or symbol).\n *\n * Display behavior is the standard finance UX pattern:\n * - On focus → show raw digits (easy to edit)\n * - On blur → show locale-formatted (`1,234.56`, easy to read)\n *\n * Pass a single-entry `currencies` array to effectively lock the\n * currency at design time (the picker still renders but only offers\n * one option). For non-monetary numeric input use `NumberInput`; for\n * integer values with step buttons use `NumberStepper`.\n */\nexport function CurrencyInput({\n value,\n defaultValue,\n onChange,\n currencies = CURATED_CURRENCIES,\n locale = 'en-US',\n min,\n max,\n allowNegative = false,\n clearable = true,\n clearLabel = 'Clear',\n chooseCurrencyLabel = 'Choose currency',\n className,\n style,\n disabled,\n onFocus,\n onBlur,\n placeholder,\n error,\n ...rest\n}: CurrencyInputProps) {\n // Uncontrolled default currency must match what's actually displayed —\n // `currencies[0]` when the consumer restricts the list, `'USD'` only when\n // no list is given at all. Hardcoding `'USD'` here would desync every\n // `onChange` from the displayed currency whenever `currencies` excludes it.\n const [internal, setInternal] = useState<CurrencyValue>(\n () => defaultValue ?? { currency: currencies[0]?.code ?? 'USD', amount: '' },\n );\n const isControlled = value !== undefined;\n const current = isControlled ? value : internal;\n\n const [focused, setFocused] = useState(false);\n // The field wrapper is the picker's popover anchor: the clickable trigger is\n // the small currency button, but the panel spans the full field width.\n const containerRef = useRef<HTMLDivElement>(null);\n\n const currency = findCurrency(current.currency, currencies) ?? currencies[0];\n const decimals = currency.decimals;\n\n const commit = useCallback(\n (next: CurrencyValue) => {\n if (!isControlled) setInternal(next);\n onChange?.(next);\n },\n [isControlled, onChange],\n );\n\n const handleAmountChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n // Focused → input shows raw digits, mask treats input as raw.\n // Blurred → input shows formatted text; parse-from-display\n // strips separators first (rare path — users typically focus\n // before typing).\n const next = focused\n ? maskAmount(e.target.value, allowNegative, decimals)\n : parseFromDisplay(e.target.value, allowNegative, decimals);\n commit({ currency: current.currency, amount: next });\n },\n [focused, allowNegative, decimals, current.currency, commit],\n );\n\n const handleCurrencyPick = useCallback(\n (code: string) => {\n // Switching currency may invalidate the current decimal count\n // (USD/2 → JPY/0). Re-mask the amount under the new precision\n // so the displayed value matches what the new currency allows.\n const nextCurrency = findCurrency(code, currencies);\n const nextDecimals = nextCurrency?.decimals ?? 2;\n const reMasked = maskAmount(current.amount, allowNegative, nextDecimals);\n commit({ currency: code, amount: reMasked });\n },\n [currencies, current.amount, allowNegative, commit],\n );\n\n const handleFocus = useCallback(\n (e: FocusEvent<HTMLInputElement>) => {\n setFocused(true);\n onFocus?.(e);\n },\n [onFocus],\n );\n\n const handleBlur = useCallback(\n (e: FocusEvent<HTMLInputElement>) => {\n setFocused(false);\n const clamped = clampNumeric(current.amount, min, max);\n if (clamped !== current.amount) {\n commit({ currency: current.currency, amount: clamped });\n }\n onBlur?.(e);\n },\n [current.amount, current.currency, min, max, commit, onBlur],\n );\n\n // Clear owns its own reset: wipe the amount but KEEP the selected currency\n // (the picker is a separate affordance — clearing the value shouldn't reset\n // it). Mirrors NumberInput / PhoneInput which hold their own value state,\n // so no `onClear` prop is needed.\n const handleClear = useCallback(() => {\n commit({ currency: current.currency, amount: '' });\n }, [commit, current.currency]);\n const showClear = clearable && current.amount !== '' && !disabled;\n\n // Focus toggle drives whether the input shows raw digits or the\n // locale-formatted display value. Same pattern as standard finance\n // inputs (Stripe, Wise, etc.).\n const displayValue = focused\n ? current.amount\n : formatForDisplay(current.amount, locale, decimals);\n\n return (\n <>\n <div\n className={cn(\n 'uxm-currency-input',\n disabled && 'uxm-currency-input--disabled',\n error && 'uxm-currency-input--error',\n className,\n )}\n style={style}\n ref={containerRef}\n >\n {/* Currency picker — the shared Listbox owns the popover (positioning,\n portal, dismiss, keyboard nav, search, ARIA). `anchorRef` points at\n the field wrapper so the panel spans the full width; `showCheckmark`\n is off because each row already carries its ISO code on the right. */}\n <Listbox<Currency>\n items={currencies}\n getKey={(c) => c.code}\n getLabel={(c) => c.name}\n filterItems={filterCurrencies}\n value={currency}\n onChange={(next) => next && handleCurrencyPick(next.code)}\n disabled={disabled}\n anchorRef={containerRef}\n placement=\"bottom-start\"\n searchPlaceholder=\"Search currencies\"\n searchAriaLabel=\"Search currencies\"\n showCheckmark={false}\n panelClassName=\"uxm-currency-input__panel\"\n panelStyle={style}\n className=\"uxm-currency-input__picker-wrap\"\n aria-label={chooseCurrencyLabel}\n renderTrigger={({ open, triggerProps }) => (\n <button\n {...triggerProps}\n type=\"button\"\n className=\"uxm-currency-input__picker\"\n aria-label={`Currency: ${currency.name}`}\n >\n <span className=\"uxm-currency-input__symbol\" aria-hidden=\"true\">{currency.symbol}</span>\n <span className=\"uxm-currency-input__code\">{currency.code}</span>\n <span className=\"uxm-currency-input__caret\" aria-hidden=\"true\">\n <Icon\n glyph=\"chevron-down\"\n size={12}\n strokeWidth={2.2}\n style={{ transform: open ? 'rotate(180deg)' : 'none' }}\n />\n </span>\n </button>\n )}\n renderItem={(c) => (\n <>\n <span className=\"uxm-currency-input__row-symbol\" aria-hidden=\"true\">{c.symbol}</span>\n <span className=\"uxm-currency-input__row-name\">{c.name}</span>\n <span className=\"uxm-currency-input__row-code\">{c.code}</span>\n </>\n )}\n />\n <input\n // Same `type=\"text\"` + `inputMode=\"decimal\"` rationale as\n // PhoneInput / NumberInput: native `type=\"number\"` ships\n // browser spinners + autofill heuristics that fight our mask.\n type=\"text\"\n inputMode={decimals > 0 ? 'decimal' : 'numeric'}\n autoComplete=\"off\"\n className={cn(\n 'uxm-currency-input__input',\n clearable && 'uxm-currency-input__input--clearable',\n )}\n value={displayValue}\n onChange={handleAmountChange}\n onFocus={handleFocus}\n onBlur={handleBlur}\n disabled={disabled}\n placeholder={placeholder ?? (decimals > 0 ? '0.00' : '0')}\n aria-invalid={error ? true : undefined}\n {...rest}\n />\n {showClear && (\n <IconButton\n className=\"uxm-field-clear uxm-currency-input__clear\"\n aria-label={clearLabel}\n // Prevent the button from stealing focus away from whatever's\n // currently focused — a blur here would clamp the about-to-be-wiped\n // value first (same guard as NumberInput).\n onMouseDown={(e) => e.preventDefault()}\n onClick={handleClear}\n >\n <Icon glyph=\"close\" />\n </IconButton>\n )}\n </div>\n {error && <FieldError className=\"uxm-currency-input__error-message\">{error}</FieldError>}\n </>\n );\n}\n// Static marker so FormField only forwards its `error` prop into children\n// that accept one (avoids React unknown-prop warnings on non-input children).\nCurrencyInput.hasError = true;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/currency-input/currency-input.tsx"],"sourcesContent":["import { useCallback, useRef, useState } from 'react';\n\nimport { cn } from '@/helpers';\n\nimport {\n CURATED_CURRENCIES,\n findCurrency,\n type Currency,\n} from '../../lib/currencies';\nimport { FieldError } from '../field-error';\nimport { Icon } from '../icon';\nimport { IconButton } from '../icon-button';\nimport { Listbox } from '../listbox';\nimport { useUxmLocale } from '../locale';\n\nimport type { ChangeEvent, FocusEvent, InputHTMLAttributes } from 'react';\n\n/** CurrencyInput value: split into ISO 4217 code and the raw amount string. */\nexport interface CurrencyValue {\n /** ISO 4217 currency code (e.g. \"USD\"). */\n currency: string;\n /** Amount as a raw digit string with optional `.` decimal (e.g. \"1234.56\"). */\n amount: string;\n}\n\nexport interface CurrencyInputProps\n extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n 'onChange' | 'value' | 'defaultValue' | 'type' | 'min' | 'max' | 'style'\n > {\n /** Controlled value. Pair with `onChange`. */\n value?: CurrencyValue;\n /** Initial value for uncontrolled usage. */\n defaultValue?: CurrencyValue;\n /** Fires on every keystroke or currency pick with the next value. */\n onChange?: (next: CurrencyValue) => void;\n /**\n * Currency list shown in the picker. Defaults to ~20 curated entries\n * covering the regions most B2B / fintech apps transact in. Pass a\n * custom list to extend or restrict (e.g. pass a single-entry array\n * to effectively lock currency selection at design time).\n */\n currencies?: Currency[];\n /**\n * BCP-47 locale tag (e.g. `\"en-US\"`, `\"de-DE\"`). Drives thousands\n * separator style on the blur-display format. Defaults to the nearest\n * `UxmLocaleProvider`, then to `\"en-US\"` — set it here only to pin one\n * amount to a locale that differs from the rest of the app.\n * Does NOT drive symbol position — the symbol always sits in the\n * leading picker slot, mirroring PhoneInput's country-code shape.\n */\n locale?: string;\n /** Clamp the committed amount down to this minimum on blur. */\n min?: number;\n /** Clamp the committed amount up to this maximum on blur. */\n max?: number;\n /** Allow a leading `-` sign for refunds / credits. Defaults to `false`. */\n allowNegative?: boolean;\n /**\n * Show a clear (✕) button at the trailing edge when the amount has a\n * value. On by default (opt out with `clearable={false}`). The component\n * owns the reset — it wipes its own uncontrolled state and fires `onChange`\n * with an empty amount (keeping the selected currency), so no separate\n * `onClear` is needed (mirrors NumberInput / PhoneInput).\n */\n clearable?: boolean;\n /** Accessible name for the clear button. Default `\"Clear\"`. */\n clearLabel?: string;\n /** Accessible name for the currency picker trigger. Default `\"Choose currency\"`. */\n chooseCurrencyLabel?: string;\n /**\n * Inline style applied to the WRAPPER (not the inner <input>). CSS\n * custom properties set here cascade to the inner field AND the\n * popover so theming knobs reach all parts.\n */\n style?: React.CSSProperties;\n /**\n * When set to a non-empty string, the field renders in its error state:\n * red border (`.uxm-currency-input--error`), `aria-invalid` on the\n * amount input, and the message rendered below the field. Omit (or pass\n * an empty string) for the normal state.\n */\n error?: string;\n}\n\n/** Strip non-digit-non-dot and cap decimals to the active currency's precision. */\nfunction maskAmount(raw: string, allowNegative: boolean, decimals: number): string {\n if (raw === '') return '';\n let negative = false;\n let body = raw;\n if (allowNegative && body.startsWith('-')) {\n negative = true;\n body = body.slice(1);\n }\n body = body.replace(/[^\\d.]/g, '');\n if (decimals === 0) {\n body = body.replace(/\\./g, '');\n } else {\n const firstDot = body.indexOf('.');\n if (firstDot !== -1) {\n body = body.slice(0, firstDot + 1) + body.slice(firstDot + 1).replace(/\\./g, '');\n const [intPart, decPart = ''] = body.split('.');\n body = intPart + '.' + decPart.slice(0, decimals);\n }\n }\n return negative ? '-' + body : body;\n}\n\n/** `1234.5` → `1,234.50` (locale-aware). Empty / partial inputs pass through. */\nfunction formatForDisplay(raw: string, locale: string, decimals: number): string {\n if (raw === '' || raw === '-' || raw === '.') return raw;\n const n = Number(raw);\n if (!Number.isFinite(n)) return raw;\n return new Intl.NumberFormat(locale, {\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n }).format(n);\n}\n\n/** Parse a locale-formatted display string back into the raw digit form. */\nfunction parseFromDisplay(formatted: string, allowNegative: boolean, decimals: number): string {\n let body = formatted;\n // Normalize a European `,` decimal (`1.234,56`) into our internal `.`\n // form so the same mask works regardless of locale on input.\n if (!body.includes('.') && body.includes(',')) {\n const lastComma = body.lastIndexOf(',');\n body = body.slice(0, lastComma) + '.' + body.slice(lastComma + 1);\n }\n return maskAmount(body, allowNegative, decimals);\n}\n\nfunction clampNumeric(value: string, min?: number, max?: number): string {\n if (value === '' || value === '-' || value === '.') return value;\n const n = Number(value);\n if (!Number.isFinite(n)) return value;\n let clamped = n;\n if (min !== undefined && clamped < min) clamped = min;\n if (max !== undefined && clamped > max) clamped = max;\n if (clamped === n) return value;\n return String(clamped);\n}\n\n/** Case-insensitive currency filter — matches name, ISO code, or symbol. */\nfunction filterCurrencies(items: Currency[], query: string): Currency[] {\n const q = query.trim().toLowerCase();\n if (!q) return items;\n return items.filter(\n (c) =>\n c.name.toLowerCase().includes(q) ||\n c.code.toLowerCase().includes(q) ||\n c.symbol.toLowerCase().includes(q),\n );\n}\n\n/**\n * A monetary input with a leading interactive currency picker. Mirrors\n * PhoneInput's architecture: the leading slot is a clickable button\n * that opens a searchable popover; the inner field handles the value.\n * Value is an object `{ currency, amount }` — `currency` is the ISO\n * 4217 code, `amount` is the raw digit string (no thousands\n * separators or symbol).\n *\n * Display behavior is the standard finance UX pattern:\n * - On focus → show raw digits (easy to edit)\n * - On blur → show locale-formatted (`1,234.56`, easy to read)\n *\n * Pass a single-entry `currencies` array to effectively lock the\n * currency at design time (the picker still renders but only offers\n * one option). For non-monetary numeric input use `NumberInput`; for\n * integer values with step buttons use `NumberStepper`.\n */\nexport function CurrencyInput({\n value,\n defaultValue,\n onChange,\n currencies = CURATED_CURRENCIES,\n locale: localeProp,\n min,\n max,\n allowNegative = false,\n clearable = true,\n clearLabel = 'Clear',\n chooseCurrencyLabel = 'Choose currency',\n className,\n style,\n disabled,\n onFocus,\n onBlur,\n placeholder,\n error,\n ...rest\n}: CurrencyInputProps) {\n const locale = useUxmLocale(localeProp);\n // Uncontrolled default currency must match what's actually displayed —\n // `currencies[0]` when the consumer restricts the list, `'USD'` only when\n // no list is given at all. Hardcoding `'USD'` here would desync every\n // `onChange` from the displayed currency whenever `currencies` excludes it.\n const [internal, setInternal] = useState<CurrencyValue>(\n () => defaultValue ?? { currency: currencies[0]?.code ?? 'USD', amount: '' },\n );\n const isControlled = value !== undefined;\n const current = isControlled ? value : internal;\n\n const [focused, setFocused] = useState(false);\n // The field wrapper is the picker's popover anchor: the clickable trigger is\n // the small currency button, but the panel spans the full field width.\n const containerRef = useRef<HTMLDivElement>(null);\n\n const currency = findCurrency(current.currency, currencies) ?? currencies[0];\n const decimals = currency.decimals;\n\n const commit = useCallback(\n (next: CurrencyValue) => {\n if (!isControlled) setInternal(next);\n onChange?.(next);\n },\n [isControlled, onChange],\n );\n\n const handleAmountChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n // Focused → input shows raw digits, mask treats input as raw.\n // Blurred → input shows formatted text; parse-from-display\n // strips separators first (rare path — users typically focus\n // before typing).\n const next = focused\n ? maskAmount(e.target.value, allowNegative, decimals)\n : parseFromDisplay(e.target.value, allowNegative, decimals);\n commit({ currency: current.currency, amount: next });\n },\n [focused, allowNegative, decimals, current.currency, commit],\n );\n\n const handleCurrencyPick = useCallback(\n (code: string) => {\n // Switching currency may invalidate the current decimal count\n // (USD/2 → JPY/0). Re-mask the amount under the new precision\n // so the displayed value matches what the new currency allows.\n const nextCurrency = findCurrency(code, currencies);\n const nextDecimals = nextCurrency?.decimals ?? 2;\n const reMasked = maskAmount(current.amount, allowNegative, nextDecimals);\n commit({ currency: code, amount: reMasked });\n },\n [currencies, current.amount, allowNegative, commit],\n );\n\n const handleFocus = useCallback(\n (e: FocusEvent<HTMLInputElement>) => {\n setFocused(true);\n onFocus?.(e);\n },\n [onFocus],\n );\n\n const handleBlur = useCallback(\n (e: FocusEvent<HTMLInputElement>) => {\n setFocused(false);\n const clamped = clampNumeric(current.amount, min, max);\n if (clamped !== current.amount) {\n commit({ currency: current.currency, amount: clamped });\n }\n onBlur?.(e);\n },\n [current.amount, current.currency, min, max, commit, onBlur],\n );\n\n // Clear owns its own reset: wipe the amount but KEEP the selected currency\n // (the picker is a separate affordance — clearing the value shouldn't reset\n // it). Mirrors NumberInput / PhoneInput which hold their own value state,\n // so no `onClear` prop is needed.\n const handleClear = useCallback(() => {\n commit({ currency: current.currency, amount: '' });\n }, [commit, current.currency]);\n const showClear = clearable && current.amount !== '' && !disabled;\n\n // Focus toggle drives whether the input shows raw digits or the\n // locale-formatted display value. Same pattern as standard finance\n // inputs (Stripe, Wise, etc.).\n const displayValue = focused\n ? current.amount\n : formatForDisplay(current.amount, locale, decimals);\n\n return (\n <>\n <div\n className={cn(\n 'uxm-currency-input',\n disabled && 'uxm-currency-input--disabled',\n error && 'uxm-currency-input--error',\n className,\n )}\n style={style}\n ref={containerRef}\n >\n {/* Currency picker — the shared Listbox owns the popover (positioning,\n portal, dismiss, keyboard nav, search, ARIA). `anchorRef` points at\n the field wrapper so the panel spans the full width; `showCheckmark`\n is off because each row already carries its ISO code on the right. */}\n <Listbox<Currency>\n items={currencies}\n getKey={(c) => c.code}\n getLabel={(c) => c.name}\n filterItems={filterCurrencies}\n value={currency}\n onChange={(next) => next && handleCurrencyPick(next.code)}\n disabled={disabled}\n anchorRef={containerRef}\n placement=\"bottom-start\"\n searchPlaceholder=\"Search currencies\"\n searchAriaLabel=\"Search currencies\"\n showCheckmark={false}\n panelClassName=\"uxm-currency-input__panel\"\n panelStyle={style}\n className=\"uxm-currency-input__picker-wrap\"\n aria-label={chooseCurrencyLabel}\n renderTrigger={({ open, triggerProps }) => (\n <button\n {...triggerProps}\n type=\"button\"\n className=\"uxm-currency-input__picker\"\n aria-label={`Currency: ${currency.name}`}\n >\n <span className=\"uxm-currency-input__symbol\" aria-hidden=\"true\">{currency.symbol}</span>\n <span className=\"uxm-currency-input__code\">{currency.code}</span>\n <span className=\"uxm-currency-input__caret\" aria-hidden=\"true\">\n <Icon\n glyph=\"chevron-down\"\n size={12}\n strokeWidth={2.2}\n style={{ transform: open ? 'rotate(180deg)' : 'none' }}\n />\n </span>\n </button>\n )}\n renderItem={(c) => (\n <>\n <span className=\"uxm-currency-input__row-symbol\" aria-hidden=\"true\">{c.symbol}</span>\n <span className=\"uxm-currency-input__row-name\">{c.name}</span>\n <span className=\"uxm-currency-input__row-code\">{c.code}</span>\n </>\n )}\n />\n <input\n // Same `type=\"text\"` + `inputMode=\"decimal\"` rationale as\n // PhoneInput / NumberInput: native `type=\"number\"` ships\n // browser spinners + autofill heuristics that fight our mask.\n type=\"text\"\n inputMode={decimals > 0 ? 'decimal' : 'numeric'}\n autoComplete=\"off\"\n className={cn(\n 'uxm-currency-input__input',\n clearable && 'uxm-currency-input__input--clearable',\n )}\n value={displayValue}\n onChange={handleAmountChange}\n onFocus={handleFocus}\n onBlur={handleBlur}\n disabled={disabled}\n placeholder={placeholder ?? (decimals > 0 ? '0.00' : '0')}\n aria-invalid={error ? true : undefined}\n {...rest}\n />\n {showClear && (\n <IconButton\n className=\"uxm-field-clear uxm-currency-input__clear\"\n aria-label={clearLabel}\n // Prevent the button from stealing focus away from whatever's\n // currently focused — a blur here would clamp the about-to-be-wiped\n // value first (same guard as NumberInput).\n onMouseDown={(e) => e.preventDefault()}\n onClick={handleClear}\n >\n <Icon glyph=\"close\" />\n </IconButton>\n )}\n </div>\n {error && <FieldError className=\"uxm-currency-input__error-message\">{error}</FieldError>}\n </>\n );\n}\n// Static marker so FormField only forwards its `error` prop into children\n// that accept one (avoids React unknown-prop warnings on non-input children).\nCurrencyInput.hasError = true;\n"],"mappings":"AA4TU,SAmBA,UAbE,KANF;AA5TV,SAAS,aAAa,QAAQ,gBAAgB;AAE9C,SAAS,UAAU;AAEnB;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AACrB,SAAS,kBAAkB;AAC3B,SAAS,eAAe;AACxB,SAAS,oBAAoB;AAyE7B,SAAS,WAAW,KAAa,eAAwB,UAA0B;AACjF,MAAI,QAAQ,GAAI,QAAO;AACvB,MAAI,WAAW;AACf,MAAI,OAAO;AACX,MAAI,iBAAiB,KAAK,WAAW,GAAG,GAAG;AACzC,eAAW;AACX,WAAO,KAAK,MAAM,CAAC;AAAA,EACrB;AACA,SAAO,KAAK,QAAQ,WAAW,EAAE;AACjC,MAAI,aAAa,GAAG;AAClB,WAAO,KAAK,QAAQ,OAAO,EAAE;AAAA,EAC/B,OAAO;AACL,UAAM,WAAW,KAAK,QAAQ,GAAG;AACjC,QAAI,aAAa,IAAI;AACnB,aAAO,KAAK,MAAM,GAAG,WAAW,CAAC,IAAI,KAAK,MAAM,WAAW,CAAC,EAAE,QAAQ,OAAO,EAAE;AAC/E,YAAM,CAAC,SAAS,UAAU,EAAE,IAAI,KAAK,MAAM,GAAG;AAC9C,aAAO,UAAU,MAAM,QAAQ,MAAM,GAAG,QAAQ;AAAA,IAClD;AAAA,EACF;AACA,SAAO,WAAW,MAAM,OAAO;AACjC;AAGA,SAAS,iBAAiB,KAAa,QAAgB,UAA0B;AAC/E,MAAI,QAAQ,MAAM,QAAQ,OAAO,QAAQ,IAAK,QAAO;AACrD,QAAM,IAAI,OAAO,GAAG;AACpB,MAAI,CAAC,OAAO,SAAS,CAAC,EAAG,QAAO;AAChC,SAAO,IAAI,KAAK,aAAa,QAAQ;AAAA,IACnC,uBAAuB;AAAA,IACvB,uBAAuB;AAAA,EACzB,CAAC,EAAE,OAAO,CAAC;AACb;AAGA,SAAS,iBAAiB,WAAmB,eAAwB,UAA0B;AAC7F,MAAI,OAAO;AAGX,MAAI,CAAC,KAAK,SAAS,GAAG,KAAK,KAAK,SAAS,GAAG,GAAG;AAC7C,UAAM,YAAY,KAAK,YAAY,GAAG;AACtC,WAAO,KAAK,MAAM,GAAG,SAAS,IAAI,MAAM,KAAK,MAAM,YAAY,CAAC;AAAA,EAClE;AACA,SAAO,WAAW,MAAM,eAAe,QAAQ;AACjD;AAEA,SAAS,aAAa,OAAe,KAAc,KAAsB;AACvE,MAAI,UAAU,MAAM,UAAU,OAAO,UAAU,IAAK,QAAO;AAC3D,QAAM,IAAI,OAAO,KAAK;AACtB,MAAI,CAAC,OAAO,SAAS,CAAC,EAAG,QAAO;AAChC,MAAI,UAAU;AACd,MAAI,QAAQ,UAAa,UAAU,IAAK,WAAU;AAClD,MAAI,QAAQ,UAAa,UAAU,IAAK,WAAU;AAClD,MAAI,YAAY,EAAG,QAAO;AAC1B,SAAO,OAAO,OAAO;AACvB;AAGA,SAAS,iBAAiB,OAAmB,OAA2B;AACtE,QAAM,IAAI,MAAM,KAAK,EAAE,YAAY;AACnC,MAAI,CAAC,EAAG,QAAO;AACf,SAAO,MAAM;AAAA,IACX,CAAC,MACC,EAAE,KAAK,YAAY,EAAE,SAAS,CAAC,KAC/B,EAAE,KAAK,YAAY,EAAE,SAAS,CAAC,KAC/B,EAAE,OAAO,YAAY,EAAE,SAAS,CAAC;AAAA,EACrC;AACF;AAmBO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,sBAAsB;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAuB;AACrB,QAAM,SAAS,aAAa,UAAU;AAKtC,QAAM,CAAC,UAAU,WAAW,IAAI;AAAA,IAC9B,MAAM,gBAAgB,EAAE,UAAU,WAAW,CAAC,GAAG,QAAQ,OAAO,QAAQ,GAAG;AAAA,EAC7E;AACA,QAAM,eAAe,UAAU;AAC/B,QAAM,UAAU,eAAe,QAAQ;AAEvC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAG5C,QAAM,eAAe,OAAuB,IAAI;AAEhD,QAAM,WAAW,aAAa,QAAQ,UAAU,UAAU,KAAK,WAAW,CAAC;AAC3E,QAAM,WAAW,SAAS;AAE1B,QAAM,SAAS;AAAA,IACb,CAAC,SAAwB;AACvB,UAAI,CAAC,aAAc,aAAY,IAAI;AACnC,iBAAW,IAAI;AAAA,IACjB;AAAA,IACA,CAAC,cAAc,QAAQ;AAAA,EACzB;AAEA,QAAM,qBAAqB;AAAA,IACzB,CAAC,MAAqC;AAKpC,YAAM,OAAO,UACT,WAAW,EAAE,OAAO,OAAO,eAAe,QAAQ,IAClD,iBAAiB,EAAE,OAAO,OAAO,eAAe,QAAQ;AAC5D,aAAO,EAAE,UAAU,QAAQ,UAAU,QAAQ,KAAK,CAAC;AAAA,IACrD;AAAA,IACA,CAAC,SAAS,eAAe,UAAU,QAAQ,UAAU,MAAM;AAAA,EAC7D;AAEA,QAAM,qBAAqB;AAAA,IACzB,CAAC,SAAiB;AAIhB,YAAM,eAAe,aAAa,MAAM,UAAU;AAClD,YAAM,eAAe,cAAc,YAAY;AAC/C,YAAM,WAAW,WAAW,QAAQ,QAAQ,eAAe,YAAY;AACvE,aAAO,EAAE,UAAU,MAAM,QAAQ,SAAS,CAAC;AAAA,IAC7C;AAAA,IACA,CAAC,YAAY,QAAQ,QAAQ,eAAe,MAAM;AAAA,EACpD;AAEA,QAAM,cAAc;AAAA,IAClB,CAAC,MAAoC;AACnC,iBAAW,IAAI;AACf,gBAAU,CAAC;AAAA,IACb;AAAA,IACA,CAAC,OAAO;AAAA,EACV;AAEA,QAAM,aAAa;AAAA,IACjB,CAAC,MAAoC;AACnC,iBAAW,KAAK;AAChB,YAAM,UAAU,aAAa,QAAQ,QAAQ,KAAK,GAAG;AACrD,UAAI,YAAY,QAAQ,QAAQ;AAC9B,eAAO,EAAE,UAAU,QAAQ,UAAU,QAAQ,QAAQ,CAAC;AAAA,MACxD;AACA,eAAS,CAAC;AAAA,IACZ;AAAA,IACA,CAAC,QAAQ,QAAQ,QAAQ,UAAU,KAAK,KAAK,QAAQ,MAAM;AAAA,EAC7D;AAMA,QAAM,cAAc,YAAY,MAAM;AACpC,WAAO,EAAE,UAAU,QAAQ,UAAU,QAAQ,GAAG,CAAC;AAAA,EACnD,GAAG,CAAC,QAAQ,QAAQ,QAAQ,CAAC;AAC7B,QAAM,YAAY,aAAa,QAAQ,WAAW,MAAM,CAAC;AAKzD,QAAM,eAAe,UACjB,QAAQ,SACR,iBAAiB,QAAQ,QAAQ,QAAQ,QAAQ;AAErD,SACE,iCACA;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,YAAY;AAAA,UACZ,SAAS;AAAA,UACT;AAAA,QACF;AAAA,QACA;AAAA,QACA,KAAK;AAAA,QAML;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,QAAQ,CAAC,MAAM,EAAE;AAAA,cACjB,UAAU,CAAC,MAAM,EAAE;AAAA,cACnB,aAAa;AAAA,cACb,OAAO;AAAA,cACP,UAAU,CAAC,SAAS,QAAQ,mBAAmB,KAAK,IAAI;AAAA,cACxD;AAAA,cACA,WAAW;AAAA,cACX,WAAU;AAAA,cACV,mBAAkB;AAAA,cAClB,iBAAgB;AAAA,cAChB,eAAe;AAAA,cACf,gBAAe;AAAA,cACf,YAAY;AAAA,cACZ,WAAU;AAAA,cACV,cAAY;AAAA,cACZ,eAAe,CAAC,EAAE,MAAM,aAAa,MACnC;AAAA,gBAAC;AAAA;AAAA,kBACE,GAAG;AAAA,kBACJ,MAAK;AAAA,kBACL,WAAU;AAAA,kBACV,cAAY,aAAa,SAAS,IAAI;AAAA,kBAEtC;AAAA,wCAAC,UAAK,WAAU,8BAA6B,eAAY,QAAQ,mBAAS,QAAO;AAAA,oBACjF,oBAAC,UAAK,WAAU,4BAA4B,mBAAS,MAAK;AAAA,oBAC1D,oBAAC,UAAK,WAAU,6BAA4B,eAAY,QACtD;AAAA,sBAAC;AAAA;AAAA,wBACC,OAAM;AAAA,wBACN,MAAM;AAAA,wBACN,aAAa;AAAA,wBACb,OAAO,EAAE,WAAW,OAAO,mBAAmB,OAAO;AAAA;AAAA,oBACvD,GACF;AAAA;AAAA;AAAA,cACF;AAAA,cAEF,YAAY,CAAC,MACX,iCACE;AAAA,oCAAC,UAAK,WAAU,kCAAiC,eAAY,QAAQ,YAAE,QAAO;AAAA,gBAC9E,oBAAC,UAAK,WAAU,gCAAgC,YAAE,MAAK;AAAA,gBACvD,oBAAC,UAAK,WAAU,gCAAgC,YAAE,MAAK;AAAA,iBACzD;AAAA;AAAA,UAEJ;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cAIC,MAAK;AAAA,cACL,WAAW,WAAW,IAAI,YAAY;AAAA,cACtC,cAAa;AAAA,cACb,WAAW;AAAA,gBACT;AAAA,gBACA,aAAa;AAAA,cACf;AAAA,cACA,OAAO;AAAA,cACP,UAAU;AAAA,cACV,SAAS;AAAA,cACT,QAAQ;AAAA,cACR;AAAA,cACA,aAAa,gBAAgB,WAAW,IAAI,SAAS;AAAA,cACrD,gBAAc,QAAQ,OAAO;AAAA,cAC5B,GAAG;AAAA;AAAA,UACN;AAAA,UACC,aACC;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,cAAY;AAAA,cAIZ,aAAa,CAAC,MAAM,EAAE,eAAe;AAAA,cACrC,SAAS;AAAA,cAET,8BAAC,QAAK,OAAM,SAAQ;AAAA;AAAA,UACtB;AAAA;AAAA;AAAA,IAEJ;AAAA,IACC,SAAS,oBAAC,cAAW,WAAU,qCAAqC,iBAAM;AAAA,KAC3E;AAEJ;AAGA,cAAc,WAAW;","names":[]}
|
|
@@ -125,6 +125,7 @@ function DateInput({
|
|
|
125
125
|
clearLabel = "Clear",
|
|
126
126
|
openCalendarLabel = "Open calendar",
|
|
127
127
|
calendarDialogLabel = "Choose date",
|
|
128
|
+
invalidMessage,
|
|
128
129
|
style,
|
|
129
130
|
onFocus,
|
|
130
131
|
onBlur,
|
|
@@ -219,7 +220,7 @@ function DateInput({
|
|
|
219
220
|
const date = parseDate(current ?? "", format);
|
|
220
221
|
return { start: date, end: date };
|
|
221
222
|
})();
|
|
222
|
-
const shownError = error || (invalid ? invalidDateMessage(format) : void 0);
|
|
223
|
+
const shownError = error || (invalid ? invalidMessage ?? invalidDateMessage(format) : void 0);
|
|
223
224
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
224
225
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
225
226
|
"div",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/ui/date-input/date-input.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useRef, useState } from 'react';\n\nimport { cn } from '@/helpers';\n\nimport { useFocusOnMount } from '../../hooks/use-focus-on-mount';\nimport { Calendar, type CalendarValue } from '../calendar';\nimport { FieldError } from '../field-error';\nimport { Icon } from '../icon';\nimport { IconButton } from '../icon-button';\n\nimport type { ChangeEvent, FocusEvent, InputHTMLAttributes } from 'react';\n\nexport type DateInputFormat = 'mdy' | 'dmy' | 'ymd';\nexport type DateInputMode = 'single' | 'range';\n\nconst RANGE_SEPARATOR = ' – ';\n\nexport interface DateInputProps\n extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'defaultValue' | 'type' | 'style'> {\n /** Format mask. Drives placeholder, the digit-segment layout, and Calendar ↔ input parsing. */\n format?: DateInputFormat;\n /**\n * Selection mode. `single` picks one date and closes after one click; `range`\n * keeps the popover open after the first click and shows both dates joined by\n * `\" – \"` once the second click commits the end. Range mode makes the input\n * read-only — the `\"MM/DD/YYYY – MM/DD/YYYY\"` shape isn't practical to type.\n */\n mode?: DateInputMode;\n /** Formatted string value (controlled). In range mode use `\"start – end\"`. */\n value?: string;\n /** Initial value for uncontrolled usage. */\n defaultValue?: string;\n /** Called with the masked, formatted value after each keystroke or calendar pick. */\n onChange?: (formatted: string) => void;\n /**\n * Render the calendar icon button and enable the click-to-open popover.\n * Defaults to true. Pass false for a typing-only date field (no icon, no popover).\n */\n calendar?: boolean;\n /**\n * Show a clear (✕) button when the field has a value. On by default (opt out\n * with `clearable={false}`). The ✕ sits just left of the calendar icon (or\n * at the trailing edge when `calendar={false}`). The component owns the reset\n * — it wipes its own uncontrolled state and fires `onChange(\"\")` — so no\n * separate `onClear` is needed. Works in both single and range mode.\n */\n clearable?: boolean;\n /** Accessible name for the clear button. Default `\"Clear\"`. */\n clearLabel?: string;\n /** Accessible name for the calendar trigger. Default `\"Open calendar\"`. */\n openCalendarLabel?: string;\n /** Accessible name for the calendar popover dialog. Default `\"Choose date\"`. */\n calendarDialogLabel?: string;\n /**\n * Inline style applied to the WRAPPER (not the inner <input>). CSS custom\n * properties set here cascade to every descendant — the input AND the\n * popover (a sibling of the input) — so theming knobs reach all parts.\n */\n style?: React.CSSProperties;\n /**\n * When set to a non-empty string, the field renders in its error state:\n * red border (`.uxm-date-input--error`), `aria-invalid` on the input,\n * and the message rendered below the field. Omit (or pass an empty\n * string) for the normal state.\n */\n error?: string;\n}\n\nexport const FORMAT_SPEC: Record<DateInputFormat, { segments: number[]; sep: string; placeholder: string }> = {\n mdy: { segments: [2, 2, 4], sep: '/', placeholder: 'MM/DD/YYYY' },\n dmy: { segments: [2, 2, 4], sep: '/', placeholder: 'DD/MM/YYYY' },\n ymd: { segments: [4, 2, 2], sep: '-', placeholder: 'YYYY-MM-DD' },\n};\n\n/** Auto-format raw input per the chosen format. Strips non-digits, caps at the format's total digit count, inserts separators. */\nexport function maskDate(raw: string, format: DateInputFormat): string {\n const spec = FORMAT_SPEC[format];\n const maxDigits = spec.segments.reduce((sum, n) => sum + n, 0);\n const digits = raw.replace(/\\D/g, '').slice(0, maxDigits);\n let out = '';\n let consumed = 0;\n for (let i = 0; i < spec.segments.length; i++) {\n const segDigits = digits.slice(consumed, consumed + spec.segments[i]);\n if (!segDigits) break;\n if (i > 0) out += spec.sep;\n out += segDigits;\n consumed += spec.segments[i];\n }\n return out;\n}\n\n/**\n * Parse a formatted date string back to a Date so the Calendar can highlight\n * it. Returns null until enough digits are typed for a complete date AND the\n * date is real — a round-trip check rejects impossible dates like Feb 30 or\n * Apr 31, which JS `Date` would otherwise silently roll into the next month.\n * A non-null result is therefore always the exact date the digits spell.\n */\nexport function parseDate(formatted: string, format: DateInputFormat): Date | null {\n const digits = formatted.replace(/\\D/g, '');\n if (digits.length !== 8) return null;\n let y: number, m: number, d: number;\n if (format === 'ymd') {\n y = Number(digits.slice(0, 4));\n m = Number(digits.slice(4, 6));\n d = Number(digits.slice(6, 8));\n } else if (format === 'mdy') {\n m = Number(digits.slice(0, 2));\n d = Number(digits.slice(2, 4));\n y = Number(digits.slice(4, 8));\n } else {\n d = Number(digits.slice(0, 2));\n m = Number(digits.slice(2, 4));\n y = Number(digits.slice(4, 8));\n }\n if (!m || !d || !y || m < 1 || m > 12 || d < 1 || d > 31) return null;\n const date = new Date(y, m - 1, d);\n // Round-trip guard: `new Date(2024, 1, 30)` silently rolls Feb 30 over to\n // Mar 1 rather than failing. Require the constructed date to spell back the\n // exact digits so callers can trust a non-null result is a real date.\n if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) {\n return null;\n }\n return date;\n}\n\n/** Format a Date back into the chosen mask. Inverse of `parseDate`. */\nexport function formatDate(date: Date, format: DateInputFormat): string {\n const m = String(date.getMonth() + 1).padStart(2, '0');\n const d = String(date.getDate()).padStart(2, '0');\n const y = String(date.getFullYear()).padStart(4, '0');\n switch (format) {\n case 'mdy': return `${m}/${d}/${y}`;\n case 'dmy': return `${d}/${m}/${y}`;\n case 'ymd': return `${y}-${m}-${d}`;\n }\n}\n\n/** Range-aware value formatter: `\"<start>\"` or `\"<start> – <end>\"`. */\nfunction formatRangeValue(value: CalendarValue, format: DateInputFormat): string {\n if (!value.start) return '';\n const startStr = formatDate(value.start, format);\n if (!value.end) return startStr;\n return `${startStr}${RANGE_SEPARATOR}${formatDate(value.end, format)}`;\n}\n\n/** Range-aware value parser. Splits on ` – ` and parses each half independently. */\nfunction parseRangeValue(formatted: string, format: DateInputFormat): CalendarValue {\n const parts = formatted.split(RANGE_SEPARATOR);\n return {\n start: parts[0] ? parseDate(parts[0], format) : null,\n end: parts[1] ? parseDate(parts[1], format) : null,\n };\n}\n\n/** Build a placeholder that doubles up the mask for range mode. */\nfunction placeholderFor(format: DateInputFormat, mode: DateInputMode): string {\n const single = FORMAT_SPEC[format].placeholder;\n return mode === 'range' ? `${single}${RANGE_SEPARATOR}${single}` : single;\n}\n\n/**\n * Inline-validation message for an unparseable date, naming the expected mask.\n * Shared with EditableCell's date field so both surface the identical wording.\n */\nexport function invalidDateMessage(format: DateInputFormat): string {\n return `Enter a valid date (${FORMAT_SPEC[format].placeholder})`;\n}\n\nexport function DateInput({\n format = 'mdy',\n mode = 'single',\n className,\n placeholder,\n value,\n defaultValue,\n onChange,\n calendar = true,\n clearable = true,\n clearLabel = 'Clear',\n openCalendarLabel = 'Open calendar',\n calendarDialogLabel = 'Choose date',\n style,\n onFocus,\n onBlur,\n disabled,\n error,\n 'aria-describedby': ariaDescribedBy,\n ...rest\n}: DateInputProps) {\n // Initial state — same masker for single, plain string for range (typing\n // disabled in range mode so no need to mask).\n const [internal, setInternal] = useState<string>(() =>\n mode === 'single' ? maskDate(defaultValue ?? '', format) : defaultValue ?? '',\n );\n const isControlled = value !== undefined;\n const current = isControlled ? value : internal;\n\n const [isOpen, setIsOpen] = useState(false);\n // Inline validity: flipped true on blur when the typed value is non-empty\n // but doesn't resolve to a real date (out-of-range like 06/36, or impossible\n // like Feb 30). Cleared the moment the user edits again and re-checked on the\n // next blur — mirrors EditableCell's date field so the two behave the same.\n const [invalid, setInvalid] = useState(false);\n const errorId = useId();\n const containerRef = useRef<HTMLDivElement>(null);\n\n // Mode OR format switch resets the internal value — a range string is\n // invalid input in single mode (and vice versa), and an existing digit\n // string would otherwise be mis-parsed under a new format's segment\n // boundaries (e.g. \"mdy\" digits re-sliced as \"ymd\"). Also close the\n // popover so the user starts a fresh pick under the new mode/format.\n // Only touches uncontrolled state; if the consumer is driving `value`\n // they own the reset themselves.\n //\n // The ref guard skips the FIRST run of this effect so the `useState`\n // initializer (which masks `defaultValue`) doesn't get clobbered on mount.\n // Without the guard, `<DateInput defaultValue=\"01/01/2020\" />` renders\n // empty because the effect fires once at mount and overwrites the\n // masked initial value with \"\".\n const isFirstModeEffect = useRef(true);\n useEffect(() => {\n if (isFirstModeEffect.current) {\n isFirstModeEffect.current = false;\n return;\n }\n setInternal('');\n setIsOpen(false);\n setInvalid(false);\n }, [mode, format]);\n\n // Close on click outside or Escape. Mounting only when `isOpen` is true\n // keeps the global listeners off when the popover is closed.\n useEffect(() => {\n if (!isOpen) return;\n const handleClick = (e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setIsOpen(false);\n }\n };\n const handleKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') setIsOpen(false);\n };\n document.addEventListener('mousedown', handleClick);\n document.addEventListener('keydown', handleKey);\n return () => {\n document.removeEventListener('mousedown', handleClick);\n document.removeEventListener('keydown', handleKey);\n };\n }, [isOpen]);\n\n // No focus restoration on close: the field opens the popover on input\n // `onFocus`, so restoring focus to the input after an outside-click close\n // would immediately re-fire `onFocus` and reopen the popover — the same\n // reopen loop TimeInput opts out of. Dismissal paths (outside click on\n // another surface, Escape while the input keeps focus) don't strand focus\n // on the removed panel because the panel itself never takes focus.\n useFocusOnMount({ active: isOpen, returnFocus: false });\n\n const handleInputChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n // Range mode is read-only; this guard prevents any sneaky programmatic edits.\n if (mode === 'range') return;\n const formatted = maskDate(e.target.value, format);\n if (!isControlled) setInternal(formatted);\n // Stop showing the error as soon as the user starts fixing the value;\n // it's re-evaluated on the next blur. (No-op re-render when already false.)\n setInvalid(false);\n onChange?.(formatted);\n },\n [mode, isControlled, onChange, format],\n );\n\n const handleCalendarChange = useCallback(\n (calValue: CalendarValue) => {\n if (!calValue.start) return;\n // A calendar pick is always a real date — clear any lingering typed-error.\n setInvalid(false);\n if (mode === 'range') {\n // Calendar fires after every click: end=null after first, end set after second.\n // Update the input either way (so the user sees the in-flight start) but only\n // close when the range is fully committed.\n const formatted = formatRangeValue(calValue, format);\n if (!isControlled) setInternal(formatted);\n onChange?.(formatted);\n if (calValue.end) setIsOpen(false);\n } else {\n // Single mode — first click commits and closes.\n const formatted = formatDate(calValue.start, format);\n if (!isControlled) setInternal(formatted);\n onChange?.(formatted);\n setIsOpen(false);\n }\n },\n [mode, isControlled, onChange, format],\n );\n\n // Clear owns its own reset: wipe the value in both single and range mode\n // and notify via onChange(\"\"). Mirrors NumberInput / CurrencyInput — the\n // component holds its own state, so no onClear prop is needed.\n const handleClear = useCallback(() => {\n if (!isControlled) setInternal('');\n setInvalid(false);\n onChange?.('');\n }, [isControlled, onChange]);\n const showClear = clearable && (current ?? '').length > 0 && !disabled;\n\n // Validate when focus leaves the field. Empty is always fine; a non-empty\n // value that doesn't parse to a real date flips into the error state. Range\n // mode is read-only (filled only via the calendar, which never yields an\n // impossible date), so it never validates here. Chains the consumer's own\n // `onBlur` afterward so wiring it up stays possible.\n const handleBlur = useCallback(\n (e: FocusEvent<HTMLInputElement>) => {\n if (mode !== 'range') {\n const v = current ?? '';\n setInvalid(v !== '' && parseDate(v, format) === null);\n }\n onBlur?.(e);\n },\n [mode, current, format, onBlur],\n );\n\n // Mirror the input's current value into the Calendar so it highlights what\n // the user has selected. Range mode parses two halves; single mode parses one.\n // In single mode we set `end = start` (same-day \"range\") so Calendar's click\n // logic sees a *committed* selection, not an in-flight first-click. Otherwise\n // the next click would be treated as the second click of a range — exactly\n // the bug where switching back to single still drew range styling.\n const calendarValue: CalendarValue = (() => {\n if (mode === 'range') return parseRangeValue(current ?? '', format);\n const date = parseDate(current ?? '', format);\n return { start: date, end: date };\n })();\n\n // A consumer-supplied `error` wins; otherwise surface the internal\n // invalid-date message. Both drive the same visual: red border,\n // `aria-invalid`, and the message below the field.\n const shownError = error || (invalid ? invalidDateMessage(format) : undefined);\n\n return (\n <>\n <div\n className={cn(\n 'uxm-date-input',\n calendar && 'uxm-date-input--has-calendar',\n clearable && 'uxm-date-input--clearable',\n shownError && 'uxm-date-input--error',\n className,\n )}\n ref={containerRef}\n style={style}\n >\n <input\n // `{...rest}` is spread FIRST so the managed props below always win —\n // notably `aria-describedby`, which must stay linked to our error\n // message and not be clobbered by a caller-supplied value.\n {...rest}\n type=\"text\"\n inputMode=\"numeric\"\n readOnly={mode === 'range'}\n className=\"uxm-date-input__input\"\n placeholder={placeholder ?? placeholderFor(format, mode)}\n value={current}\n onChange={handleInputChange}\n onFocus={(e) => {\n if (calendar) setIsOpen(true);\n onFocus?.(e);\n }}\n onBlur={handleBlur}\n aria-invalid={shownError ? true : undefined}\n // Point at our error message while one shows AND keep any\n // caller-supplied `aria-describedby` (e.g. unrelated help text) —\n // both ids are announced, ours never clobbers theirs.\n aria-describedby={[shownError && errorId, ariaDescribedBy].filter(Boolean).join(' ') || undefined}\n disabled={disabled}\n />\n {showClear && (\n <IconButton\n className=\"uxm-field-clear uxm-date-input__clear\"\n aria-label={clearLabel}\n // Prevent the button from stealing focus away from whatever's\n // currently focused — clearing must not re-open the calendar\n // popover via the input's onFocus handler.\n onMouseDown={(e) => e.preventDefault()}\n onClick={handleClear}\n >\n <Icon glyph=\"close\" />\n </IconButton>\n )}\n {calendar && (\n <button\n type=\"button\"\n className=\"uxm-date-input__icon\"\n onClick={() => setIsOpen((o) => !o)}\n aria-label={openCalendarLabel}\n aria-expanded={isOpen}\n disabled={disabled}\n >\n <Icon glyph=\"calendar\" />\n </button>\n )}\n {calendar && isOpen && (\n <div className=\"uxm-date-input__popover\" role=\"dialog\" aria-label={calendarDialogLabel}>\n {/* Swallow mousedown so a calendar click never blurs the input: blur\n now runs validation (handleBlur), and a partially-typed value\n would flash the error state for a frame before the pick lands.\n Focus-retention only — the interactive controls are the\n Calendar's own buttons. */}\n {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- mousedown swallow for focus retention only; nothing interactive lives on this div */}\n <div onMouseDown={(e) => e.preventDefault()}>\n <Calendar value={calendarValue} onChange={handleCalendarChange} />\n </div>\n </div>\n )}\n </div>\n {shownError && (\n <FieldError id={errorId} className=\"uxm-date-input__error-message\">{shownError}</FieldError>\n )}\n </>\n );\n}\n// Static marker so FormField only forwards its `error` prop into children\n// that accept one (avoids React unknown-prop warnings on non-input children).\nDateInput.hasError = true;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/date-input/date-input.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useRef, useState } from 'react';\n\nimport { cn } from '@/helpers';\n\nimport { useFocusOnMount } from '../../hooks/use-focus-on-mount';\nimport { Calendar, type CalendarValue } from '../calendar';\nimport { FieldError } from '../field-error';\nimport { Icon } from '../icon';\nimport { IconButton } from '../icon-button';\n\nimport type { ChangeEvent, FocusEvent, InputHTMLAttributes } from 'react';\n\nexport type DateInputFormat = 'mdy' | 'dmy' | 'ymd';\nexport type DateInputMode = 'single' | 'range';\n\nconst RANGE_SEPARATOR = ' – ';\n\nexport interface DateInputProps\n extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'defaultValue' | 'type' | 'style'> {\n /** Format mask. Drives placeholder, the digit-segment layout, and Calendar ↔ input parsing. */\n format?: DateInputFormat;\n /**\n * Selection mode. `single` picks one date and closes after one click; `range`\n * keeps the popover open after the first click and shows both dates joined by\n * `\" – \"` once the second click commits the end. Range mode makes the input\n * read-only — the `\"MM/DD/YYYY – MM/DD/YYYY\"` shape isn't practical to type.\n */\n mode?: DateInputMode;\n /** Formatted string value (controlled). In range mode use `\"start – end\"`. */\n value?: string;\n /** Initial value for uncontrolled usage. */\n defaultValue?: string;\n /** Called with the masked, formatted value after each keystroke or calendar pick. */\n onChange?: (formatted: string) => void;\n /**\n * Render the calendar icon button and enable the click-to-open popover.\n * Defaults to true. Pass false for a typing-only date field (no icon, no popover).\n */\n calendar?: boolean;\n /**\n * Show a clear (✕) button when the field has a value. On by default (opt out\n * with `clearable={false}`). The ✕ sits just left of the calendar icon (or\n * at the trailing edge when `calendar={false}`). The component owns the reset\n * — it wipes its own uncontrolled state and fires `onChange(\"\")` — so no\n * separate `onClear` is needed. Works in both single and range mode.\n */\n clearable?: boolean;\n /** Accessible name for the clear button. Default `\"Clear\"`. */\n clearLabel?: string;\n /** Accessible name for the calendar trigger. Default `\"Open calendar\"`. */\n openCalendarLabel?: string;\n /** Accessible name for the calendar popover dialog. Default `\"Choose date\"`. */\n calendarDialogLabel?: string;\n /**\n * Replaces the built-in \"unparseable date\" message raised on blur. Defaults\n * to `invalidDateMessage(format)` — `` `Enter a valid date (MM/DD/YYYY)` ``.\n * Unrelated to `error`, which is the consumer's own message and always wins.\n */\n invalidMessage?: string;\n /**\n * Inline style applied to the WRAPPER (not the inner <input>). CSS custom\n * properties set here cascade to every descendant — the input AND the\n * popover (a sibling of the input) — so theming knobs reach all parts.\n */\n style?: React.CSSProperties;\n /**\n * When set to a non-empty string, the field renders in its error state:\n * red border (`.uxm-date-input--error`), `aria-invalid` on the input,\n * and the message rendered below the field. Omit (or pass an empty\n * string) for the normal state.\n */\n error?: string;\n}\n\nexport const FORMAT_SPEC: Record<DateInputFormat, { segments: number[]; sep: string; placeholder: string }> = {\n mdy: { segments: [2, 2, 4], sep: '/', placeholder: 'MM/DD/YYYY' },\n dmy: { segments: [2, 2, 4], sep: '/', placeholder: 'DD/MM/YYYY' },\n ymd: { segments: [4, 2, 2], sep: '-', placeholder: 'YYYY-MM-DD' },\n};\n\n/** Auto-format raw input per the chosen format. Strips non-digits, caps at the format's total digit count, inserts separators. */\nexport function maskDate(raw: string, format: DateInputFormat): string {\n const spec = FORMAT_SPEC[format];\n const maxDigits = spec.segments.reduce((sum, n) => sum + n, 0);\n const digits = raw.replace(/\\D/g, '').slice(0, maxDigits);\n let out = '';\n let consumed = 0;\n for (let i = 0; i < spec.segments.length; i++) {\n const segDigits = digits.slice(consumed, consumed + spec.segments[i]);\n if (!segDigits) break;\n if (i > 0) out += spec.sep;\n out += segDigits;\n consumed += spec.segments[i];\n }\n return out;\n}\n\n/**\n * Parse a formatted date string back to a Date so the Calendar can highlight\n * it. Returns null until enough digits are typed for a complete date AND the\n * date is real — a round-trip check rejects impossible dates like Feb 30 or\n * Apr 31, which JS `Date` would otherwise silently roll into the next month.\n * A non-null result is therefore always the exact date the digits spell.\n */\nexport function parseDate(formatted: string, format: DateInputFormat): Date | null {\n const digits = formatted.replace(/\\D/g, '');\n if (digits.length !== 8) return null;\n let y: number, m: number, d: number;\n if (format === 'ymd') {\n y = Number(digits.slice(0, 4));\n m = Number(digits.slice(4, 6));\n d = Number(digits.slice(6, 8));\n } else if (format === 'mdy') {\n m = Number(digits.slice(0, 2));\n d = Number(digits.slice(2, 4));\n y = Number(digits.slice(4, 8));\n } else {\n d = Number(digits.slice(0, 2));\n m = Number(digits.slice(2, 4));\n y = Number(digits.slice(4, 8));\n }\n if (!m || !d || !y || m < 1 || m > 12 || d < 1 || d > 31) return null;\n const date = new Date(y, m - 1, d);\n // Round-trip guard: `new Date(2024, 1, 30)` silently rolls Feb 30 over to\n // Mar 1 rather than failing. Require the constructed date to spell back the\n // exact digits so callers can trust a non-null result is a real date.\n if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) {\n return null;\n }\n return date;\n}\n\n/** Format a Date back into the chosen mask. Inverse of `parseDate`. */\nexport function formatDate(date: Date, format: DateInputFormat): string {\n const m = String(date.getMonth() + 1).padStart(2, '0');\n const d = String(date.getDate()).padStart(2, '0');\n const y = String(date.getFullYear()).padStart(4, '0');\n switch (format) {\n case 'mdy': return `${m}/${d}/${y}`;\n case 'dmy': return `${d}/${m}/${y}`;\n case 'ymd': return `${y}-${m}-${d}`;\n }\n}\n\n/** Range-aware value formatter: `\"<start>\"` or `\"<start> – <end>\"`. */\nfunction formatRangeValue(value: CalendarValue, format: DateInputFormat): string {\n if (!value.start) return '';\n const startStr = formatDate(value.start, format);\n if (!value.end) return startStr;\n return `${startStr}${RANGE_SEPARATOR}${formatDate(value.end, format)}`;\n}\n\n/** Range-aware value parser. Splits on ` – ` and parses each half independently. */\nfunction parseRangeValue(formatted: string, format: DateInputFormat): CalendarValue {\n const parts = formatted.split(RANGE_SEPARATOR);\n return {\n start: parts[0] ? parseDate(parts[0], format) : null,\n end: parts[1] ? parseDate(parts[1], format) : null,\n };\n}\n\n/** Build a placeholder that doubles up the mask for range mode. */\nfunction placeholderFor(format: DateInputFormat, mode: DateInputMode): string {\n const single = FORMAT_SPEC[format].placeholder;\n return mode === 'range' ? `${single}${RANGE_SEPARATOR}${single}` : single;\n}\n\n/**\n * Inline-validation message for an unparseable date, naming the expected mask.\n * Shared with EditableCell's date field so both surface the identical wording.\n *\n * English-only by design: it is the *default*, not the contract. Localise via\n * `DateInput`'s `invalidMessage` prop (or `EditableCell`'s `invalidDateMessage`),\n * both of which take precedence over this.\n */\nexport function invalidDateMessage(format: DateInputFormat): string {\n return `Enter a valid date (${FORMAT_SPEC[format].placeholder})`;\n}\n\nexport function DateInput({\n format = 'mdy',\n mode = 'single',\n className,\n placeholder,\n value,\n defaultValue,\n onChange,\n calendar = true,\n clearable = true,\n clearLabel = 'Clear',\n openCalendarLabel = 'Open calendar',\n calendarDialogLabel = 'Choose date',\n invalidMessage,\n style,\n onFocus,\n onBlur,\n disabled,\n error,\n 'aria-describedby': ariaDescribedBy,\n ...rest\n}: DateInputProps) {\n // Initial state — same masker for single, plain string for range (typing\n // disabled in range mode so no need to mask).\n const [internal, setInternal] = useState<string>(() =>\n mode === 'single' ? maskDate(defaultValue ?? '', format) : defaultValue ?? '',\n );\n const isControlled = value !== undefined;\n const current = isControlled ? value : internal;\n\n const [isOpen, setIsOpen] = useState(false);\n // Inline validity: flipped true on blur when the typed value is non-empty\n // but doesn't resolve to a real date (out-of-range like 06/36, or impossible\n // like Feb 30). Cleared the moment the user edits again and re-checked on the\n // next blur — mirrors EditableCell's date field so the two behave the same.\n const [invalid, setInvalid] = useState(false);\n const errorId = useId();\n const containerRef = useRef<HTMLDivElement>(null);\n\n // Mode OR format switch resets the internal value — a range string is\n // invalid input in single mode (and vice versa), and an existing digit\n // string would otherwise be mis-parsed under a new format's segment\n // boundaries (e.g. \"mdy\" digits re-sliced as \"ymd\"). Also close the\n // popover so the user starts a fresh pick under the new mode/format.\n // Only touches uncontrolled state; if the consumer is driving `value`\n // they own the reset themselves.\n //\n // The ref guard skips the FIRST run of this effect so the `useState`\n // initializer (which masks `defaultValue`) doesn't get clobbered on mount.\n // Without the guard, `<DateInput defaultValue=\"01/01/2020\" />` renders\n // empty because the effect fires once at mount and overwrites the\n // masked initial value with \"\".\n const isFirstModeEffect = useRef(true);\n useEffect(() => {\n if (isFirstModeEffect.current) {\n isFirstModeEffect.current = false;\n return;\n }\n setInternal('');\n setIsOpen(false);\n setInvalid(false);\n }, [mode, format]);\n\n // Close on click outside or Escape. Mounting only when `isOpen` is true\n // keeps the global listeners off when the popover is closed.\n useEffect(() => {\n if (!isOpen) return;\n const handleClick = (e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setIsOpen(false);\n }\n };\n const handleKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') setIsOpen(false);\n };\n document.addEventListener('mousedown', handleClick);\n document.addEventListener('keydown', handleKey);\n return () => {\n document.removeEventListener('mousedown', handleClick);\n document.removeEventListener('keydown', handleKey);\n };\n }, [isOpen]);\n\n // No focus restoration on close: the field opens the popover on input\n // `onFocus`, so restoring focus to the input after an outside-click close\n // would immediately re-fire `onFocus` and reopen the popover — the same\n // reopen loop TimeInput opts out of. Dismissal paths (outside click on\n // another surface, Escape while the input keeps focus) don't strand focus\n // on the removed panel because the panel itself never takes focus.\n useFocusOnMount({ active: isOpen, returnFocus: false });\n\n const handleInputChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n // Range mode is read-only; this guard prevents any sneaky programmatic edits.\n if (mode === 'range') return;\n const formatted = maskDate(e.target.value, format);\n if (!isControlled) setInternal(formatted);\n // Stop showing the error as soon as the user starts fixing the value;\n // it's re-evaluated on the next blur. (No-op re-render when already false.)\n setInvalid(false);\n onChange?.(formatted);\n },\n [mode, isControlled, onChange, format],\n );\n\n const handleCalendarChange = useCallback(\n (calValue: CalendarValue) => {\n if (!calValue.start) return;\n // A calendar pick is always a real date — clear any lingering typed-error.\n setInvalid(false);\n if (mode === 'range') {\n // Calendar fires after every click: end=null after first, end set after second.\n // Update the input either way (so the user sees the in-flight start) but only\n // close when the range is fully committed.\n const formatted = formatRangeValue(calValue, format);\n if (!isControlled) setInternal(formatted);\n onChange?.(formatted);\n if (calValue.end) setIsOpen(false);\n } else {\n // Single mode — first click commits and closes.\n const formatted = formatDate(calValue.start, format);\n if (!isControlled) setInternal(formatted);\n onChange?.(formatted);\n setIsOpen(false);\n }\n },\n [mode, isControlled, onChange, format],\n );\n\n // Clear owns its own reset: wipe the value in both single and range mode\n // and notify via onChange(\"\"). Mirrors NumberInput / CurrencyInput — the\n // component holds its own state, so no onClear prop is needed.\n const handleClear = useCallback(() => {\n if (!isControlled) setInternal('');\n setInvalid(false);\n onChange?.('');\n }, [isControlled, onChange]);\n const showClear = clearable && (current ?? '').length > 0 && !disabled;\n\n // Validate when focus leaves the field. Empty is always fine; a non-empty\n // value that doesn't parse to a real date flips into the error state. Range\n // mode is read-only (filled only via the calendar, which never yields an\n // impossible date), so it never validates here. Chains the consumer's own\n // `onBlur` afterward so wiring it up stays possible.\n const handleBlur = useCallback(\n (e: FocusEvent<HTMLInputElement>) => {\n if (mode !== 'range') {\n const v = current ?? '';\n setInvalid(v !== '' && parseDate(v, format) === null);\n }\n onBlur?.(e);\n },\n [mode, current, format, onBlur],\n );\n\n // Mirror the input's current value into the Calendar so it highlights what\n // the user has selected. Range mode parses two halves; single mode parses one.\n // In single mode we set `end = start` (same-day \"range\") so Calendar's click\n // logic sees a *committed* selection, not an in-flight first-click. Otherwise\n // the next click would be treated as the second click of a range — exactly\n // the bug where switching back to single still drew range styling.\n const calendarValue: CalendarValue = (() => {\n if (mode === 'range') return parseRangeValue(current ?? '', format);\n const date = parseDate(current ?? '', format);\n return { start: date, end: date };\n })();\n\n // A consumer-supplied `error` wins; otherwise surface the internal\n // invalid-date message. Both drive the same visual: red border,\n // `aria-invalid`, and the message below the field.\n const shownError =\n error || (invalid ? invalidMessage ?? invalidDateMessage(format) : undefined);\n\n return (\n <>\n <div\n className={cn(\n 'uxm-date-input',\n calendar && 'uxm-date-input--has-calendar',\n clearable && 'uxm-date-input--clearable',\n shownError && 'uxm-date-input--error',\n className,\n )}\n ref={containerRef}\n style={style}\n >\n <input\n // `{...rest}` is spread FIRST so the managed props below always win —\n // notably `aria-describedby`, which must stay linked to our error\n // message and not be clobbered by a caller-supplied value.\n {...rest}\n type=\"text\"\n inputMode=\"numeric\"\n readOnly={mode === 'range'}\n className=\"uxm-date-input__input\"\n placeholder={placeholder ?? placeholderFor(format, mode)}\n value={current}\n onChange={handleInputChange}\n onFocus={(e) => {\n if (calendar) setIsOpen(true);\n onFocus?.(e);\n }}\n onBlur={handleBlur}\n aria-invalid={shownError ? true : undefined}\n // Point at our error message while one shows AND keep any\n // caller-supplied `aria-describedby` (e.g. unrelated help text) —\n // both ids are announced, ours never clobbers theirs.\n aria-describedby={[shownError && errorId, ariaDescribedBy].filter(Boolean).join(' ') || undefined}\n disabled={disabled}\n />\n {showClear && (\n <IconButton\n className=\"uxm-field-clear uxm-date-input__clear\"\n aria-label={clearLabel}\n // Prevent the button from stealing focus away from whatever's\n // currently focused — clearing must not re-open the calendar\n // popover via the input's onFocus handler.\n onMouseDown={(e) => e.preventDefault()}\n onClick={handleClear}\n >\n <Icon glyph=\"close\" />\n </IconButton>\n )}\n {calendar && (\n <button\n type=\"button\"\n className=\"uxm-date-input__icon\"\n onClick={() => setIsOpen((o) => !o)}\n aria-label={openCalendarLabel}\n aria-expanded={isOpen}\n disabled={disabled}\n >\n <Icon glyph=\"calendar\" />\n </button>\n )}\n {calendar && isOpen && (\n <div className=\"uxm-date-input__popover\" role=\"dialog\" aria-label={calendarDialogLabel}>\n {/* Swallow mousedown so a calendar click never blurs the input: blur\n now runs validation (handleBlur), and a partially-typed value\n would flash the error state for a frame before the pick lands.\n Focus-retention only — the interactive controls are the\n Calendar's own buttons. */}\n {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- mousedown swallow for focus retention only; nothing interactive lives on this div */}\n <div onMouseDown={(e) => e.preventDefault()}>\n <Calendar value={calendarValue} onChange={handleCalendarChange} />\n </div>\n </div>\n )}\n </div>\n {shownError && (\n <FieldError id={errorId} className=\"uxm-date-input__error-message\">{shownError}</FieldError>\n )}\n </>\n );\n}\n// Static marker so FormField only forwards its `error` prop into children\n// that accept one (avoids React unknown-prop warnings on non-input children).\nDateInput.hasError = true;\n"],"mappings":"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","names":[]}
|
|
@@ -36,6 +36,12 @@ export interface DateInputProps extends Omit<InputHTMLAttributes<HTMLInputElemen
|
|
|
36
36
|
openCalendarLabel?: string;
|
|
37
37
|
/** Accessible name for the calendar popover dialog. Default `"Choose date"`. */
|
|
38
38
|
calendarDialogLabel?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Replaces the built-in "unparseable date" message raised on blur. Defaults
|
|
41
|
+
* to `invalidDateMessage(format)` — `` `Enter a valid date (MM/DD/YYYY)` ``.
|
|
42
|
+
* Unrelated to `error`, which is the consumer's own message and always wins.
|
|
43
|
+
*/
|
|
44
|
+
invalidMessage?: string;
|
|
39
45
|
/**
|
|
40
46
|
* Inline style applied to the WRAPPER (not the inner <input>). CSS custom
|
|
41
47
|
* properties set here cascade to every descendant — the input AND the
|
|
@@ -70,9 +76,13 @@ export declare function formatDate(date: Date, format: DateInputFormat): string;
|
|
|
70
76
|
/**
|
|
71
77
|
* Inline-validation message for an unparseable date, naming the expected mask.
|
|
72
78
|
* Shared with EditableCell's date field so both surface the identical wording.
|
|
79
|
+
*
|
|
80
|
+
* English-only by design: it is the *default*, not the contract. Localise via
|
|
81
|
+
* `DateInput`'s `invalidMessage` prop (or `EditableCell`'s `invalidDateMessage`),
|
|
82
|
+
* both of which take precedence over this.
|
|
73
83
|
*/
|
|
74
84
|
export declare function invalidDateMessage(format: DateInputFormat): string;
|
|
75
|
-
export declare function DateInput({ format, mode, className, placeholder, value, defaultValue, onChange, calendar, clearable, clearLabel, openCalendarLabel, calendarDialogLabel, style, onFocus, onBlur, disabled, error, 'aria-describedby': ariaDescribedBy, ...rest }: DateInputProps): import("react/jsx-runtime").JSX.Element;
|
|
85
|
+
export declare function DateInput({ format, mode, className, placeholder, value, defaultValue, onChange, calendar, clearable, clearLabel, openCalendarLabel, calendarDialogLabel, invalidMessage, style, onFocus, onBlur, disabled, error, 'aria-describedby': ariaDescribedBy, ...rest }: DateInputProps): import("react/jsx-runtime").JSX.Element;
|
|
76
86
|
export declare namespace DateInput {
|
|
77
87
|
var hasError: boolean;
|
|
78
88
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"date-input.d.ts","sourceRoot":"","sources":["../../../src/ui/date-input/date-input.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAA2B,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAE1E,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AACpD,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC;AAI/C,MAAM,WAAW,cACf,SAAQ,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,UAAU,GAAG,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;IAC7G,+FAA+F;IAC/F,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gFAAgF;IAChF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,eAAe,EAAE;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAIzG,CAAC;AAEF,kIAAkI;AAClI,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,MAAM,CAcrE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,GAAG,IAAI,CA0BjF;AAED,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,GAAG,MAAM,CAStE;AAyBD
|
|
1
|
+
{"version":3,"file":"date-input.d.ts","sourceRoot":"","sources":["../../../src/ui/date-input/date-input.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAA2B,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAE1E,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AACpD,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC;AAI/C,MAAM,WAAW,cACf,SAAQ,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,UAAU,GAAG,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;IAC7G,+FAA+F;IAC/F,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gFAAgF;IAChF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,eAAe,EAAE;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAIzG,CAAC;AAEF,kIAAkI;AAClI,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,MAAM,CAcrE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,GAAG,IAAI,CA0BjF;AAED,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,GAAG,MAAM,CAStE;AAyBD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAElE;AAED,wBAAgB,SAAS,CAAC,EACxB,MAAc,EACd,IAAe,EACf,SAAS,EACT,WAAW,EACX,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,QAAe,EACf,SAAgB,EAChB,UAAoB,EACpB,iBAAmC,EACnC,mBAAmC,EACnC,cAAc,EACd,KAAK,EACL,OAAO,EACP,MAAM,EACN,QAAQ,EACR,KAAK,EACL,kBAAkB,EAAE,eAAe,EACnC,GAAG,IAAI,EACR,EAAE,cAAc,2CAyOhB;yBA9Pe,SAAS"}
|
|
@@ -97,6 +97,7 @@ function DateInput({
|
|
|
97
97
|
clearLabel = "Clear",
|
|
98
98
|
openCalendarLabel = "Open calendar",
|
|
99
99
|
calendarDialogLabel = "Choose date",
|
|
100
|
+
invalidMessage,
|
|
100
101
|
style,
|
|
101
102
|
onFocus,
|
|
102
103
|
onBlur,
|
|
@@ -191,7 +192,7 @@ function DateInput({
|
|
|
191
192
|
const date = parseDate(current ?? "", format);
|
|
192
193
|
return { start: date, end: date };
|
|
193
194
|
})();
|
|
194
|
-
const shownError = error || (invalid ? invalidDateMessage(format) : void 0);
|
|
195
|
+
const shownError = error || (invalid ? invalidMessage ?? invalidDateMessage(format) : void 0);
|
|
195
196
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
196
197
|
/* @__PURE__ */ jsxs(
|
|
197
198
|
"div",
|