@dloizides/ui-forms 1.9.0 → 1.11.1

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 CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.11.0
4
+
5
+ **`ThemedTextInput` shipped no metrics, so every direct caller got a raw 19px input.**
6
+
7
+ It documented itself as "the ONE source of truth for the input look" while shipping
8
+ only **colours** — no border, radius, padding or height. `FormField` quietly passed its
9
+ own private duplicate of those metrics, so anything reached *through* `FormField` looked
10
+ correct and the gap was invisible. Every **direct** caller — the documented, blessed
11
+ usage — got a raw browser input.
12
+
13
+ Measured live in the Digital Kin CMS before: **10/10 inputs below the 48px floor**, text
14
+ inputs 19px, textareas 38px. After: **0/10**, at 48/96px.
15
+
16
+ **Label association was absent entirely** (0 `label[for]`, 0 ids). Two defects only a
17
+ real browser could find, both fixed here:
18
+
19
+ - **`htmlFor` is silently dropped by react-native-web** — it is not on the prop
20
+ allowlist, the same family as the dropped `accessibilityHint`. A prop-level assertion
21
+ passes against the broken build; only a DOM-level test catches it, so that is what the
22
+ new tests assert.
23
+ - **Dangling `for`.** `Field` assumed render-function children place the id they are
24
+ handed, but `SelectControl` uses `aria-labelledby` instead — so the difficulty label
25
+ shipped `for="field-control-3"` pointing at **nothing**, passing every structural check
26
+ while focusing no field. `useHtmlFor` now sets `for` only when the target is genuinely
27
+ in the document.
28
+
29
+ > ⚠️ **Cross-fleet visual change.** `controlStyles.input` gaining `minHeight: 48` lifts
30
+ > dense filter inputs and select triggers in `aml-v2`/`agora-web` from ~38-41px to 48px.
31
+ > This cannot be avoided: the input floor is what fixes the 19px defect, and nothing
32
+ > fixes that without also lifting inputs that already had padding. Owner-approved.
33
+
34
+
3
35
  ## 1.9.0
4
36
 
5
37
  **Campaign F3 — `Field`'s spacing model, and the layout primitives.** `Field` hard-coded a 16px
package/dist/index.d.mts CHANGED
@@ -94,6 +94,21 @@ interface FieldChildContext {
94
94
  */
95
95
  describedById?: string;
96
96
  hasError: boolean;
97
+ /**
98
+ * `id` this field's `<label for>` points at. Assign it to the control (RN `id` → DOM `id`) and
99
+ * clicking the label text focuses the control — a free doubling of the hit area that every
100
+ * field was throwing away.
101
+ *
102
+ * A plain-node child gets this injected automatically (see below); a render-function child has
103
+ * to place it, because only the caller knows which element inside is the actual control.
104
+ */
105
+ controlId: string;
106
+ /**
107
+ * `id` OF the label element. For a control that `<label for>` cannot address — anything that is
108
+ * not a native form element, e.g. the `<div role="button">` a `SelectControl` trigger renders —
109
+ * point `aria-labelledby` here instead.
110
+ */
111
+ labelId: string;
97
112
  }
98
113
  type FieldChildren = React.ReactNode | ((context: FieldChildContext) => React.ReactNode);
99
114
  interface FieldProps {
@@ -356,6 +371,13 @@ declare const FormActions: ({ onSave, onCancel, saveLabel, cancelLabel, saveHint
356
371
  * soft ring) for free, driven by the active theme.
357
372
  */
358
373
 
374
+ /**
375
+ * The private `styles.input` that used to live here — padding 10/12, radius 8, border 1 — was a
376
+ * verbatim duplicate of `controlStyles.input`, and `ThemedTextInput` now applies that itself. The
377
+ * duplication is what hid the real defect: because `FormField` supplied metrics, inputs reached
378
+ * through it looked correct, while every DIRECT `ThemedTextInput` caller got an unstyled 19px box
379
+ * and nobody could see the difference from in here.
380
+ */
359
381
  interface FormFieldProps extends Omit<TextInputProps, 'style'> {
360
382
  label: string;
361
383
  required?: boolean;
@@ -373,6 +395,20 @@ declare const FormField: ({ label, required, error, containerStyle, ...textInput
373
395
  * It is the ONE source of truth for the input look: `FormField` renders it, and raw filter inputs
374
396
  * (search / country / year / date fallbacks) use it directly instead of forking the styles.
375
397
  *
398
+ * ## The box metrics are applied HERE, and that is new
399
+ *
400
+ * This component claimed to be the source of truth for the input look while shipping only
401
+ * COLOURS — no border, no radius, no padding, no height. `FormField` happened to pass its own
402
+ * private copy of those metrics, so every input reached through `FormField` looked right and the
403
+ * gap stayed invisible. Any caller that used `ThemedTextInput` directly — the documented, blessed
404
+ * way to use it — got a raw browser input: **19px tall**, measured in production on an admin CMS
405
+ * where all 10 inputs failed the hit-target floor.
406
+ *
407
+ * So `controlStyles.input` is now the component's own base, ahead of the caller's `style` in the
408
+ * cascade. Callers that already passed the same metrics (`FormField`) are unaffected — the values
409
+ * are identical, which is precisely why the duplication was invisible — and callers that pass
410
+ * their own overrides still win, because their `style` merges after the base.
411
+ *
376
412
  * Every existing `TextInputProps` (value, placeholder, onChangeText, testID, accessibility…) is
377
413
  * forwarded untouched; `onFocus`/`onBlur` you pass are composed with the internal focus state.
378
414
  */
@@ -387,6 +423,16 @@ interface ThemedTextInputProps extends TextInputProps {
387
423
  describedById?: string;
388
424
  /** Marks the field required for assistive tech (`aria-required` on web). No-op on native. */
389
425
  requiredField?: boolean;
426
+ /**
427
+ * Opt OUT of the shared box metrics (border, radius, padding, the kit's min-height floor) and
428
+ * render colours only — the pre-1.11 behaviour.
429
+ *
430
+ * For the rare control that supplies its own complete box, e.g. an input embedded inside an
431
+ * already-bordered composite where a second border would double up. It is a prop rather than
432
+ * the default because the default has to be the accessible one: the previous default shipped
433
+ * 19px inputs to production precisely because "no metrics" was the silent fallback.
434
+ */
435
+ unstyledBox?: boolean;
390
436
  }
391
437
  declare const ThemedTextInput: React.ForwardRefExoticComponent<ThemedTextInputProps & React.RefAttributes<TextInput>>;
392
438
 
@@ -576,6 +622,17 @@ interface SelectControlProps {
576
622
  */
577
623
  accessibilityLabel: string;
578
624
  accessibilityHint?: string;
625
+ /**
626
+ * `id` of the `Field` label naming this control — take it from the render-child context's
627
+ * `labelId`. The trigger is a `<div role="button">`, which `<label for>` cannot address (that
628
+ * only works for native form elements), so the association runs the other way: the trigger
629
+ * points AT the label.
630
+ *
631
+ * `accessibilityLabel` still wins for the accessible NAME, deliberately — it is the only one of
632
+ * the two that carries the current selection ("Status: Active"). This adds the structural link
633
+ * without changing a single announcement.
634
+ */
635
+ labelledById?: string;
579
636
  /** PRE-LOCALIZED accessibility hint applied to every option in the menu. */
580
637
  optionHint?: string;
581
638
  /** Merged LAST onto the anchor wrapper — the consumer always wins over the shared style. */
@@ -583,7 +640,7 @@ interface SelectControlProps {
583
640
  /** Trigger is `${testID}-trigger`, menu `${testID}-menu`, options `${testID}-option-${value}`. */
584
641
  testID: string;
585
642
  }
586
- declare function SelectControl({ options, value, onChange, placeholder, accessibilityLabel, accessibilityHint, optionHint, style, testID, }: SelectControlProps): React.ReactElement;
643
+ declare function SelectControl({ options, value, onChange, placeholder, accessibilityLabel, accessibilityHint, labelledById, optionHint, style, testID, }: SelectControlProps): React.ReactElement;
587
644
 
588
645
  /**
589
646
  * `TypeaheadControl` — a search-as-you-type combobox (aml-v2's CountryPicker, generalized). A
@@ -702,16 +759,24 @@ interface AnchoredMenuProps {
702
759
  declare function AnchoredMenu({ options, selectedValue, onSelect, onDismiss, colors, optionHint, testID, anchorRef, }: AnchoredMenuProps): React.ReactElement;
703
760
 
704
761
  declare const controlStyles: {
705
- /** The shared bordered box every dense text control renders in. */
762
+ /**
763
+ * The shared bordered box every dense text control renders in.
764
+ *
765
+ * `minHeight` is the kit floor: padding alone produced a ~38px box, and a bare input with no
766
+ * box style at all measured **19px** in production. Height is a hit-target guarantee, so it
767
+ * is stated rather than left to emerge from font size + padding.
768
+ */
706
769
  input: {
707
770
  borderWidth: number;
708
771
  borderRadius: number;
709
772
  paddingHorizontal: number;
710
773
  paddingVertical: number;
711
774
  fontSize: number;
775
+ minHeight: number;
712
776
  };
713
777
  /** The select trigger: a bordered field box matching the text inputs + a chevron. */
714
778
  selectTrigger: {
779
+ minHeight: number;
715
780
  flexDirection: "row";
716
781
  alignItems: "center";
717
782
  justifyContent: "space-between";
package/dist/index.d.ts CHANGED
@@ -94,6 +94,21 @@ interface FieldChildContext {
94
94
  */
95
95
  describedById?: string;
96
96
  hasError: boolean;
97
+ /**
98
+ * `id` this field's `<label for>` points at. Assign it to the control (RN `id` → DOM `id`) and
99
+ * clicking the label text focuses the control — a free doubling of the hit area that every
100
+ * field was throwing away.
101
+ *
102
+ * A plain-node child gets this injected automatically (see below); a render-function child has
103
+ * to place it, because only the caller knows which element inside is the actual control.
104
+ */
105
+ controlId: string;
106
+ /**
107
+ * `id` OF the label element. For a control that `<label for>` cannot address — anything that is
108
+ * not a native form element, e.g. the `<div role="button">` a `SelectControl` trigger renders —
109
+ * point `aria-labelledby` here instead.
110
+ */
111
+ labelId: string;
97
112
  }
98
113
  type FieldChildren = React.ReactNode | ((context: FieldChildContext) => React.ReactNode);
99
114
  interface FieldProps {
@@ -356,6 +371,13 @@ declare const FormActions: ({ onSave, onCancel, saveLabel, cancelLabel, saveHint
356
371
  * soft ring) for free, driven by the active theme.
357
372
  */
358
373
 
374
+ /**
375
+ * The private `styles.input` that used to live here — padding 10/12, radius 8, border 1 — was a
376
+ * verbatim duplicate of `controlStyles.input`, and `ThemedTextInput` now applies that itself. The
377
+ * duplication is what hid the real defect: because `FormField` supplied metrics, inputs reached
378
+ * through it looked correct, while every DIRECT `ThemedTextInput` caller got an unstyled 19px box
379
+ * and nobody could see the difference from in here.
380
+ */
359
381
  interface FormFieldProps extends Omit<TextInputProps, 'style'> {
360
382
  label: string;
361
383
  required?: boolean;
@@ -373,6 +395,20 @@ declare const FormField: ({ label, required, error, containerStyle, ...textInput
373
395
  * It is the ONE source of truth for the input look: `FormField` renders it, and raw filter inputs
374
396
  * (search / country / year / date fallbacks) use it directly instead of forking the styles.
375
397
  *
398
+ * ## The box metrics are applied HERE, and that is new
399
+ *
400
+ * This component claimed to be the source of truth for the input look while shipping only
401
+ * COLOURS — no border, no radius, no padding, no height. `FormField` happened to pass its own
402
+ * private copy of those metrics, so every input reached through `FormField` looked right and the
403
+ * gap stayed invisible. Any caller that used `ThemedTextInput` directly — the documented, blessed
404
+ * way to use it — got a raw browser input: **19px tall**, measured in production on an admin CMS
405
+ * where all 10 inputs failed the hit-target floor.
406
+ *
407
+ * So `controlStyles.input` is now the component's own base, ahead of the caller's `style` in the
408
+ * cascade. Callers that already passed the same metrics (`FormField`) are unaffected — the values
409
+ * are identical, which is precisely why the duplication was invisible — and callers that pass
410
+ * their own overrides still win, because their `style` merges after the base.
411
+ *
376
412
  * Every existing `TextInputProps` (value, placeholder, onChangeText, testID, accessibility…) is
377
413
  * forwarded untouched; `onFocus`/`onBlur` you pass are composed with the internal focus state.
378
414
  */
@@ -387,6 +423,16 @@ interface ThemedTextInputProps extends TextInputProps {
387
423
  describedById?: string;
388
424
  /** Marks the field required for assistive tech (`aria-required` on web). No-op on native. */
389
425
  requiredField?: boolean;
426
+ /**
427
+ * Opt OUT of the shared box metrics (border, radius, padding, the kit's min-height floor) and
428
+ * render colours only — the pre-1.11 behaviour.
429
+ *
430
+ * For the rare control that supplies its own complete box, e.g. an input embedded inside an
431
+ * already-bordered composite where a second border would double up. It is a prop rather than
432
+ * the default because the default has to be the accessible one: the previous default shipped
433
+ * 19px inputs to production precisely because "no metrics" was the silent fallback.
434
+ */
435
+ unstyledBox?: boolean;
390
436
  }
391
437
  declare const ThemedTextInput: React.ForwardRefExoticComponent<ThemedTextInputProps & React.RefAttributes<TextInput>>;
392
438
 
@@ -576,6 +622,17 @@ interface SelectControlProps {
576
622
  */
577
623
  accessibilityLabel: string;
578
624
  accessibilityHint?: string;
625
+ /**
626
+ * `id` of the `Field` label naming this control — take it from the render-child context's
627
+ * `labelId`. The trigger is a `<div role="button">`, which `<label for>` cannot address (that
628
+ * only works for native form elements), so the association runs the other way: the trigger
629
+ * points AT the label.
630
+ *
631
+ * `accessibilityLabel` still wins for the accessible NAME, deliberately — it is the only one of
632
+ * the two that carries the current selection ("Status: Active"). This adds the structural link
633
+ * without changing a single announcement.
634
+ */
635
+ labelledById?: string;
579
636
  /** PRE-LOCALIZED accessibility hint applied to every option in the menu. */
580
637
  optionHint?: string;
581
638
  /** Merged LAST onto the anchor wrapper — the consumer always wins over the shared style. */
@@ -583,7 +640,7 @@ interface SelectControlProps {
583
640
  /** Trigger is `${testID}-trigger`, menu `${testID}-menu`, options `${testID}-option-${value}`. */
584
641
  testID: string;
585
642
  }
586
- declare function SelectControl({ options, value, onChange, placeholder, accessibilityLabel, accessibilityHint, optionHint, style, testID, }: SelectControlProps): React.ReactElement;
643
+ declare function SelectControl({ options, value, onChange, placeholder, accessibilityLabel, accessibilityHint, labelledById, optionHint, style, testID, }: SelectControlProps): React.ReactElement;
587
644
 
588
645
  /**
589
646
  * `TypeaheadControl` — a search-as-you-type combobox (aml-v2's CountryPicker, generalized). A
@@ -702,16 +759,24 @@ interface AnchoredMenuProps {
702
759
  declare function AnchoredMenu({ options, selectedValue, onSelect, onDismiss, colors, optionHint, testID, anchorRef, }: AnchoredMenuProps): React.ReactElement;
703
760
 
704
761
  declare const controlStyles: {
705
- /** The shared bordered box every dense text control renders in. */
762
+ /**
763
+ * The shared bordered box every dense text control renders in.
764
+ *
765
+ * `minHeight` is the kit floor: padding alone produced a ~38px box, and a bare input with no
766
+ * box style at all measured **19px** in production. Height is a hit-target guarantee, so it
767
+ * is stated rather than left to emerge from font size + padding.
768
+ */
706
769
  input: {
707
770
  borderWidth: number;
708
771
  borderRadius: number;
709
772
  paddingHorizontal: number;
710
773
  paddingVertical: number;
711
774
  fontSize: number;
775
+ minHeight: number;
712
776
  };
713
777
  /** The select trigger: a bordered field box matching the text inputs + a chevron. */
714
778
  selectTrigger: {
779
+ minHeight: number;
715
780
  flexDirection: "row";
716
781
  alignItems: "center";
717
782
  justifyContent: "space-between";