import { DismissibleAlert } from '../ui/dismissible-alert'; import { ComboboxControl } from "../ui/combobox-control"; import { CustomSelectControl } from "../ui/custom-select-control"; import { FormTokenField } from "../ui/form-token-field"; import { ProgressBar } from "../ui/progress-bar"; import type { CustomSelectOption } from "../ui/custom-select-control"; import type { TokenItem } from "../ui/form-token-field"; import type { ComboboxControlOption } from "../ui/combobox-control"; import { Controller, useFormContext } from 'react-hook-form'; import { IValueSet } from '../../services'; import { getNestedError } from '../../utils/formUtils.ts'; /** * / B50 - the key of the "Select..." row. * * A SENTINEL, NEVER A VALUE. WP's `CustomSelectControl` needs a real option object to display a * placeholder, so one exists in the list - but `onChange` maps it back to `undefined`, so it can never be * submitted and can never satisfy a `required` rule. That is the whole difference between this and the * `{ key: null, name: '' }` row it replaces, which WAS selectable and did satisfy the control while * failing the form. */ const PLACEHOLDER_KEY = '__placeholder__'; interface FhirCodeProps { label: string; context: string; options: IValueSet[]; multiple?: boolean; combobox?: boolean; required?: boolean; disabled?: boolean; loading?: boolean; /** * - MAY THE USER CHOOSE THE PLACEHOLDER TO CLEAR THIS FIELD? Defaults to NO, which is the * behaviour deliberately built and which every one of the other call sites keeps. * * OPT-IN, AND THAT IS THE WHOLE POINT. `PLACEHOLDER_KEY` is a sentinel that `onChange` maps back to * `undefined` precisely so it *"can never be submitted"* - correct for a REQUIRED field, where a * selectable "unset" is a way to satisfy the control and fail the form. But on an OPTIONAL field it * makes the blank option a **no-op**: a location's timezone, once set, could not be cleared. * * WHY THIS IS A NEW PROP RATHER THAN A FIX TO THE EXISTING LINE. Measured 2026-08-23: there are * **502 `` usages** and **164 places** in services/utils that test a value against `undefined` * explicitly. Changing what a cleared field emits, for everyone, is a large change wearing a one-line * disguise. Additive, so nothing that does not ask for it can change. */ clearable?: boolean; } const FhirCode = ({ label, context, options, multiple = false, combobox = false, required = false, disabled = false, loading = false, clearable = false }: FhirCodeProps) => { const { control, formState: { errors } } = useFormContext(); return ( <> { const selectedOption = options.find(option => option.code === field.value); const displayLabel = label.replace(/\s*\*\s*$/, ''); const showRequired = required || /\*\s*$/.test(label); let comboBoxValue: string | undefined; const transformedValue = selectedOption ? { key: selectedOption.code, name: selectedOption.display } : undefined; if (combobox) { const selectedOption = options.find(option => option.code === field.value); comboBoxValue = selectedOption ? selectedOption.code : undefined; } const tokenValues: (string | TokenItem)[] = multiple && Array.isArray(field.value) ? field.value.map((item: any) => { if (typeof item === 'string') { const option = options.find(o => o.code === item); return option ? option.display : item; } return item.display || ''; }) : []; if (multiple) { return (
{/* Named by aria-label rather than label: this control has no hideLabelFromVision, and the visible label above it is not associated with the input. */} { const codes = tokens.map(token => { const valueSet: IValueSet | undefined = options.find(option => option.display === token); return valueSet?.code; }); field.onChange(codes); }} suggestions={options.map((option: IValueSet) => option.display)}/> {loading && } {getNestedError(errors, context) && ( {displayLabel} is required )}
); } if (combobox) { return (
{/* The visible label above is not associated with the input, so the control needs its own to have an accessible name. */} ({ value: option.code, label: option.display })) ] as ComboboxControlOption[]} value={comboBoxValue} disabled={disabled} onChange={(value) => { const selectedOption = options.find(option => option.code === value); field.onChange(selectedOption?.code || null); }}/> {loading && } {getNestedError(errors, context) && ( {displayLabel} is required )}
) } return (
{/* Card 241 / findings B50 + B51 — this control rendered COMPLETELY BLANK when nothing was selected, on REQUIRED fields. Its only accessible text was "Currently selected:" followed by nothing, and the first Save then failed with "Type is required" for a block the user never chose to add. Two separate causes, both here: • `value={transformedValue}` is `undefined` until a choice is made, and WP's control renders nothing for that — no placeholder, no default. A field that cannot say "choose something" cannot be required. • a literal `{ key: null, name: '' }` was prepended, so even the OPEN list led with an empty row you could select (B51). ⛔ The blank option is REMOVED rather than relabelled: it was a selectable value meaning "unset" on a required field, which is a way to satisfy the control and fail the form. 📌 `PLACEHOLDER_KEY` is a sentinel, not a value — `onChange` maps it back to `undefined` below so it can never be submitted. Converted off @wordpress/components 2026-08-29. It was left on WP deliberately once, on the grounds that FhirCode has 152 consumers and swapping the control mid-design-pass was too wide. The whole package is gone now, so the reason expired with it. The control is ui/custom-select-control, which keeps WP's prop contract. */} {/* Same reason as the combobox branch: the visible label above is not associated with this control. */} ({ key: option.code, name: option.display })) ] as CustomSelectOption[]} value={transformedValue ?? { key: PLACEHOLDER_KEY, name: 'Select…' }} disabled={disabled} onChange={(newValue: { selectedItem: CustomSelectOption }) => { const key = newValue.selectedItem?.key; if (key !== PLACEHOLDER_KEY) { field.onChange(key); return; } // - an EMPTY STRING, not `undefined`, and the // difference is the whole fix. react-hook-form treats // `undefined` as "no value supplied" and leaves the previous // one in place, so the control kept displaying the old code // and the save never saw a change. `''` is a real value it // stores - and the transforms already read it correctly: // `withTimezoneExtension` trims and REMOVES the extension for // an empty value, which is what keeps "not set" // distinguishable from "set to nothing". // Only when the field asked to be clearable; every other // call site keeps sentinel exactly as it was. field.onChange(clearable ? '' : undefined); }}/> {loading && } {getNestedError(errors, context) && ( {displayLabel} is required )}
); }}/> ); }; export default FhirCode;