"use client" import * as React from "react" import { cn } from "../../lib/utils" import { Input } from "./input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./select" /** * `UnitControl` is the replacement for `@wordpress/components`' `__experimentalUnitControl` - * **3 imports across 3 files**. * * AN ADAPTER, NOT A RENAME, AND THE REASON IS THE `onChange` PAYLOAD. WordPress does not hand the * handler the number the user typed. It hands back the quantity and the unit joined into one string: * * onChange={(value) => field.onChange(Number(value.replace(currentUnit, '')) || 0)} * * That is what all three call sites do. A control emitting a plain number would leave every one of * them parsing `"5"` against a unit that is not there - and for `type="quantity"` in the questionnaire * preview, storing a bare number where the form expects a quantity string. Nothing in this repo * renders those forms in a test. * * Clearing the field emits `''`, not `undefined`, which is WP's behaviour and what * `if (!value)` at the call sites checks for. * * CHANGING THE UNIT FIRES BOTH CALLBACKS, in WP's order: `onChange` first with the recombined value, * then `onUnitChange` with the new unit. `FhirDuration` writes the value from inside `onUnitChange`, * so the order is load-bearing. * * `__next40pxDefaultSize` is deliberately NOT accepted - meaningless outside WordPress, exactly as * `ui/input-control` and `ui/toggle-control` ruled. **Call sites drop it.** * * `onValidate` IS accepted, and the line between the two is worth stating: it is real API rather than * a WordPress opt-in marker, so it is kept and it is CALLED - on blur, which is when WP commits. What * is not reproduced is WP reverting the field when the validator throws. All three sites pass * `() => {}`, so there is nothing to revert; if one ever validates for real, that is the piece to add. */ export type WPUnitControlUnit = { /** The value stored and appended to the quantity, e.g. `px` or `years`. */ value: string /** What the picker shows. */ label: string /** Quantity to snap to when this unit is chosen. */ default?: number /** Step for the number field while this unit is active. */ step?: number } export interface UnitControlProps extends Omit, "onChange" | "value" | "type"> { label?: React.ReactNode /** The combined quantity and unit, e.g. `"5years"`. */ value?: string | number /** Forces the active unit. When absent it is read off `value`. */ unit?: string units?: WPUnitControlUnit[] /** WP's contract: the COMBINED string, never a number and never an event. */ onChange?: (value: string | undefined) => void onUnitChange?: (unit: string | undefined) => void /** Runs on blur with the combined value, which is when WordPress commits. */ onValidate?: (value: string | undefined) => void } // A leading number, then whatever is left is the unit. Matches WP's parser closely enough for every // shape the call sites produce: "5years", "12.5", "d", "". const QUANTITY_AND_UNIT = /^\s*(-?\d*\.?\d*)\s*(.*?)\s*$/ function parseValue( value: string | number | undefined, unitProp: string | undefined, units: WPUnitControlUnit[] ) { const match = QUANTITY_AND_UNIT.exec(String(value ?? "")) const quantity = match?.[1] ?? "" const parsedUnit = match?.[2] ?? "" // A single available unit wins outright - WP does the same, because there is nothing to choose. const unit = (units.length === 1 ? units[0].value : undefined) ?? unitProp ?? (units.some((option) => option.value === parsedUnit) ? parsedUnit : undefined) ?? units[0]?.value ?? parsedUnit return { quantity, unit } } function UnitControl({ label, value, unit: unitProp, units = [], className, disabled, id, onChange, onUnitChange, onValidate, onBlur, ...props }: UnitControlProps) { // A generated id is what lets the label be a real `