@dloizides/ui-forms 1.9.1 → 1.12.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/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,13 +371,31 @@ 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;
383
+ /**
384
+ * Hide the VISIBLE label row while keeping the accessible name.
385
+ *
386
+ * For a field whose placeholder already names it (a toolbar search box, an inline
387
+ * filter) the stacked visible label both wastes a row and misaligns the input
388
+ * against a bare button beside it. Setting this drops ONLY the visible `<label>`:
389
+ * the input still carries `accessibilityLabel={label}`, so a screen reader reads
390
+ * the same name and `label` stays REQUIRED — you never lose the accessible name by
391
+ * blanking it, which is the trap this option exists to remove. Default `false`.
392
+ */
393
+ labelHidden?: boolean;
361
394
  required?: boolean;
362
395
  error?: string;
363
396
  containerStyle?: ViewStyle;
364
397
  }
365
- declare const FormField: ({ label, required, error, containerStyle, ...textInputProps }: FormFieldProps) => React.ReactElement;
398
+ declare const FormField: ({ label, labelHidden, required, error, containerStyle, ...textInputProps }: FormFieldProps) => React.ReactElement;
366
399
 
367
400
  /**
368
401
  * `ThemedTextInput` — the shared, readable + lively text-input primitive. A thin `<TextInput>`
@@ -373,6 +406,20 @@ declare const FormField: ({ label, required, error, containerStyle, ...textInput
373
406
  * It is the ONE source of truth for the input look: `FormField` renders it, and raw filter inputs
374
407
  * (search / country / year / date fallbacks) use it directly instead of forking the styles.
375
408
  *
409
+ * ## The box metrics are applied HERE, and that is new
410
+ *
411
+ * This component claimed to be the source of truth for the input look while shipping only
412
+ * COLOURS — no border, no radius, no padding, no height. `FormField` happened to pass its own
413
+ * private copy of those metrics, so every input reached through `FormField` looked right and the
414
+ * gap stayed invisible. Any caller that used `ThemedTextInput` directly — the documented, blessed
415
+ * way to use it — got a raw browser input: **19px tall**, measured in production on an admin CMS
416
+ * where all 10 inputs failed the hit-target floor.
417
+ *
418
+ * So `controlStyles.input` is now the component's own base, ahead of the caller's `style` in the
419
+ * cascade. Callers that already passed the same metrics (`FormField`) are unaffected — the values
420
+ * are identical, which is precisely why the duplication was invisible — and callers that pass
421
+ * their own overrides still win, because their `style` merges after the base.
422
+ *
376
423
  * Every existing `TextInputProps` (value, placeholder, onChangeText, testID, accessibility…) is
377
424
  * forwarded untouched; `onFocus`/`onBlur` you pass are composed with the internal focus state.
378
425
  */
@@ -387,6 +434,16 @@ interface ThemedTextInputProps extends TextInputProps {
387
434
  describedById?: string;
388
435
  /** Marks the field required for assistive tech (`aria-required` on web). No-op on native. */
389
436
  requiredField?: boolean;
437
+ /**
438
+ * Opt OUT of the shared box metrics (border, radius, padding, the kit's min-height floor) and
439
+ * render colours only — the pre-1.11 behaviour.
440
+ *
441
+ * For the rare control that supplies its own complete box, e.g. an input embedded inside an
442
+ * already-bordered composite where a second border would double up. It is a prop rather than
443
+ * the default because the default has to be the accessible one: the previous default shipped
444
+ * 19px inputs to production precisely because "no metrics" was the silent fallback.
445
+ */
446
+ unstyledBox?: boolean;
390
447
  }
391
448
  declare const ThemedTextInput: React.ForwardRefExoticComponent<ThemedTextInputProps & React.RefAttributes<TextInput>>;
392
449
 
@@ -576,6 +633,17 @@ interface SelectControlProps {
576
633
  */
577
634
  accessibilityLabel: string;
578
635
  accessibilityHint?: string;
636
+ /**
637
+ * `id` of the `Field` label naming this control — take it from the render-child context's
638
+ * `labelId`. The trigger is a `<div role="button">`, which `<label for>` cannot address (that
639
+ * only works for native form elements), so the association runs the other way: the trigger
640
+ * points AT the label.
641
+ *
642
+ * `accessibilityLabel` still wins for the accessible NAME, deliberately — it is the only one of
643
+ * the two that carries the current selection ("Status: Active"). This adds the structural link
644
+ * without changing a single announcement.
645
+ */
646
+ labelledById?: string;
579
647
  /** PRE-LOCALIZED accessibility hint applied to every option in the menu. */
580
648
  optionHint?: string;
581
649
  /** Merged LAST onto the anchor wrapper — the consumer always wins over the shared style. */
@@ -583,7 +651,7 @@ interface SelectControlProps {
583
651
  /** Trigger is `${testID}-trigger`, menu `${testID}-menu`, options `${testID}-option-${value}`. */
584
652
  testID: string;
585
653
  }
586
- declare function SelectControl({ options, value, onChange, placeholder, accessibilityLabel, accessibilityHint, optionHint, style, testID, }: SelectControlProps): React.ReactElement;
654
+ declare function SelectControl({ options, value, onChange, placeholder, accessibilityLabel, accessibilityHint, labelledById, optionHint, style, testID, }: SelectControlProps): React.ReactElement;
587
655
 
588
656
  /**
589
657
  * `TypeaheadControl` — a search-as-you-type combobox (aml-v2's CountryPicker, generalized). A
@@ -702,16 +770,24 @@ interface AnchoredMenuProps {
702
770
  declare function AnchoredMenu({ options, selectedValue, onSelect, onDismiss, colors, optionHint, testID, anchorRef, }: AnchoredMenuProps): React.ReactElement;
703
771
 
704
772
  declare const controlStyles: {
705
- /** The shared bordered box every dense text control renders in. */
773
+ /**
774
+ * The shared bordered box every dense text control renders in.
775
+ *
776
+ * `minHeight` is the kit floor: padding alone produced a ~38px box, and a bare input with no
777
+ * box style at all measured **19px** in production. Height is a hit-target guarantee, so it
778
+ * is stated rather than left to emerge from font size + padding.
779
+ */
706
780
  input: {
707
781
  borderWidth: number;
708
782
  borderRadius: number;
709
783
  paddingHorizontal: number;
710
784
  paddingVertical: number;
711
785
  fontSize: number;
786
+ minHeight: number;
712
787
  };
713
788
  /** The select trigger: a bordered field box matching the text inputs + a chevron. */
714
789
  selectTrigger: {
790
+ minHeight: number;
715
791
  flexDirection: "row";
716
792
  alignItems: "center";
717
793
  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,13 +371,31 @@ 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;
383
+ /**
384
+ * Hide the VISIBLE label row while keeping the accessible name.
385
+ *
386
+ * For a field whose placeholder already names it (a toolbar search box, an inline
387
+ * filter) the stacked visible label both wastes a row and misaligns the input
388
+ * against a bare button beside it. Setting this drops ONLY the visible `<label>`:
389
+ * the input still carries `accessibilityLabel={label}`, so a screen reader reads
390
+ * the same name and `label` stays REQUIRED — you never lose the accessible name by
391
+ * blanking it, which is the trap this option exists to remove. Default `false`.
392
+ */
393
+ labelHidden?: boolean;
361
394
  required?: boolean;
362
395
  error?: string;
363
396
  containerStyle?: ViewStyle;
364
397
  }
365
- declare const FormField: ({ label, required, error, containerStyle, ...textInputProps }: FormFieldProps) => React.ReactElement;
398
+ declare const FormField: ({ label, labelHidden, required, error, containerStyle, ...textInputProps }: FormFieldProps) => React.ReactElement;
366
399
 
367
400
  /**
368
401
  * `ThemedTextInput` — the shared, readable + lively text-input primitive. A thin `<TextInput>`
@@ -373,6 +406,20 @@ declare const FormField: ({ label, required, error, containerStyle, ...textInput
373
406
  * It is the ONE source of truth for the input look: `FormField` renders it, and raw filter inputs
374
407
  * (search / country / year / date fallbacks) use it directly instead of forking the styles.
375
408
  *
409
+ * ## The box metrics are applied HERE, and that is new
410
+ *
411
+ * This component claimed to be the source of truth for the input look while shipping only
412
+ * COLOURS — no border, no radius, no padding, no height. `FormField` happened to pass its own
413
+ * private copy of those metrics, so every input reached through `FormField` looked right and the
414
+ * gap stayed invisible. Any caller that used `ThemedTextInput` directly — the documented, blessed
415
+ * way to use it — got a raw browser input: **19px tall**, measured in production on an admin CMS
416
+ * where all 10 inputs failed the hit-target floor.
417
+ *
418
+ * So `controlStyles.input` is now the component's own base, ahead of the caller's `style` in the
419
+ * cascade. Callers that already passed the same metrics (`FormField`) are unaffected — the values
420
+ * are identical, which is precisely why the duplication was invisible — and callers that pass
421
+ * their own overrides still win, because their `style` merges after the base.
422
+ *
376
423
  * Every existing `TextInputProps` (value, placeholder, onChangeText, testID, accessibility…) is
377
424
  * forwarded untouched; `onFocus`/`onBlur` you pass are composed with the internal focus state.
378
425
  */
@@ -387,6 +434,16 @@ interface ThemedTextInputProps extends TextInputProps {
387
434
  describedById?: string;
388
435
  /** Marks the field required for assistive tech (`aria-required` on web). No-op on native. */
389
436
  requiredField?: boolean;
437
+ /**
438
+ * Opt OUT of the shared box metrics (border, radius, padding, the kit's min-height floor) and
439
+ * render colours only — the pre-1.11 behaviour.
440
+ *
441
+ * For the rare control that supplies its own complete box, e.g. an input embedded inside an
442
+ * already-bordered composite where a second border would double up. It is a prop rather than
443
+ * the default because the default has to be the accessible one: the previous default shipped
444
+ * 19px inputs to production precisely because "no metrics" was the silent fallback.
445
+ */
446
+ unstyledBox?: boolean;
390
447
  }
391
448
  declare const ThemedTextInput: React.ForwardRefExoticComponent<ThemedTextInputProps & React.RefAttributes<TextInput>>;
392
449
 
@@ -576,6 +633,17 @@ interface SelectControlProps {
576
633
  */
577
634
  accessibilityLabel: string;
578
635
  accessibilityHint?: string;
636
+ /**
637
+ * `id` of the `Field` label naming this control — take it from the render-child context's
638
+ * `labelId`. The trigger is a `<div role="button">`, which `<label for>` cannot address (that
639
+ * only works for native form elements), so the association runs the other way: the trigger
640
+ * points AT the label.
641
+ *
642
+ * `accessibilityLabel` still wins for the accessible NAME, deliberately — it is the only one of
643
+ * the two that carries the current selection ("Status: Active"). This adds the structural link
644
+ * without changing a single announcement.
645
+ */
646
+ labelledById?: string;
579
647
  /** PRE-LOCALIZED accessibility hint applied to every option in the menu. */
580
648
  optionHint?: string;
581
649
  /** Merged LAST onto the anchor wrapper — the consumer always wins over the shared style. */
@@ -583,7 +651,7 @@ interface SelectControlProps {
583
651
  /** Trigger is `${testID}-trigger`, menu `${testID}-menu`, options `${testID}-option-${value}`. */
584
652
  testID: string;
585
653
  }
586
- declare function SelectControl({ options, value, onChange, placeholder, accessibilityLabel, accessibilityHint, optionHint, style, testID, }: SelectControlProps): React.ReactElement;
654
+ declare function SelectControl({ options, value, onChange, placeholder, accessibilityLabel, accessibilityHint, labelledById, optionHint, style, testID, }: SelectControlProps): React.ReactElement;
587
655
 
588
656
  /**
589
657
  * `TypeaheadControl` — a search-as-you-type combobox (aml-v2's CountryPicker, generalized). A
@@ -702,16 +770,24 @@ interface AnchoredMenuProps {
702
770
  declare function AnchoredMenu({ options, selectedValue, onSelect, onDismiss, colors, optionHint, testID, anchorRef, }: AnchoredMenuProps): React.ReactElement;
703
771
 
704
772
  declare const controlStyles: {
705
- /** The shared bordered box every dense text control renders in. */
773
+ /**
774
+ * The shared bordered box every dense text control renders in.
775
+ *
776
+ * `minHeight` is the kit floor: padding alone produced a ~38px box, and a bare input with no
777
+ * box style at all measured **19px** in production. Height is a hit-target guarantee, so it
778
+ * is stated rather than left to emerge from font size + padding.
779
+ */
706
780
  input: {
707
781
  borderWidth: number;
708
782
  borderRadius: number;
709
783
  paddingHorizontal: number;
710
784
  paddingVertical: number;
711
785
  fontSize: number;
786
+ minHeight: number;
712
787
  };
713
788
  /** The select trigger: a bordered field box matching the text inputs + a chevron. */
714
789
  selectTrigger: {
790
+ minHeight: number;
715
791
  flexDirection: "row";
716
792
  alignItems: "center";
717
793
  justifyContent: "space-between";