@dloizides/ui-forms 1.13.3 → 1.14.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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.14.0
4
+
5
+ **`FormField` gained an opt-in `recentKey` — remember the last N submitted values and offer them on focus.**
6
+
7
+ A search / filter / lookup box that the user retypes the same handful of values into now gets a
8
+ "recent values" dropdown for free. Set `recentKey="<namespace>"` on a `FormField` and the values it
9
+ submits are remembered (per key) and surfaced in a menu on focus; picking one fills the input. A
10
+ value is recorded on submit (Enter) **and** on blur-with-content, so both "hit enter" and "typed then
11
+ moved on" are captured. The dropdown reuses the kit's `AnchoredMenu` (portalled to `document.body`, so
12
+ it is never painted behind a table / card / the next field) with full a11y (menu name, per-row label +
13
+ hint + role).
14
+
15
+ Persistence is `localStorage` under `@dloizides/ui-forms:recent:<recentKey>`; the list de-dupes
16
+ case-insensitively, promotes a repeat to the front, and caps at `recentMax` (default 10). On native /
17
+ SSR / when storage is disabled it is a silent no-op (recents are always `[]`), and every storage
18
+ access is wrapped so a quota or privacy-mode failure can never throw into render.
19
+
20
+ **Additive and backward-compatible.** Leave `recentKey` unset — the default — and the field behaves
21
+ exactly as before: nothing rendered, no state/refs/effects, no storage touched. NEVER set it on a
22
+ password / secure field. The engine is also exported standalone as `useRecentValues(key, max?)` for
23
+ bespoke recents surfaces, and `AnchoredMenu` gained an optional `accessibilityLabel` for naming the menu.
24
+
3
25
  ## 1.13.3
4
26
 
5
27
  **Placeholder text was too dark — it read like an already-filled value.**
package/dist/index.d.mts CHANGED
@@ -394,8 +394,21 @@ interface FormFieldProps extends Omit<TextInputProps, 'style'> {
394
394
  required?: boolean;
395
395
  error?: string;
396
396
  containerStyle?: ViewStyle;
397
+ /**
398
+ * OPT-IN "recent values" behaviour. When set, the values submitted through this field are
399
+ * remembered (per this key) and offered in a dropdown on focus; picking one fills the input.
400
+ * Values are recorded on submit (Enter) and on blur-with-content. Persisted to `localStorage`
401
+ * under `@dloizides/ui-forms:recent:<recentKey>`; a silent no-op on native / when storage is
402
+ * unavailable.
403
+ *
404
+ * Leave it UNSET (the default) and the field behaves exactly as before — nothing rendered, no
405
+ * storage touched. NEVER set it on a password / secure field: recents must not persist secrets.
406
+ */
407
+ recentKey?: string;
408
+ /** Cap on remembered values when {@link recentKey} is set (most-recent-first). Default 10. */
409
+ recentMax?: number;
397
410
  }
398
- declare const FormField: ({ label, labelHidden, required, error, containerStyle, ...textInputProps }: FormFieldProps) => React.ReactElement;
411
+ declare const FormField: ({ label, labelHidden, required, error, containerStyle, recentKey, recentMax, ...textInputProps }: FormFieldProps) => React.ReactElement;
399
412
 
400
413
  /**
401
414
  * `ThemedTextInput` — the shared, readable + lively text-input primitive. A thin `<TextInput>`
@@ -479,6 +492,38 @@ interface ThemedInput {
479
492
  }
480
493
  declare function useThemedInput(options?: UseThemedInputOptions): ThemedInput;
481
494
 
495
+ interface RecentValues {
496
+ /** The remembered values, most-recent-first (never longer than `max`; `[]` when storage is unavailable). */
497
+ recents: string[];
498
+ /** Fold a value into the list (trim / de-dupe / promote / cap) and persist. Empty values are ignored. */
499
+ remember: (value: string) => void;
500
+ /** Forget every remembered value for this key and clear it from storage. */
501
+ clear: () => void;
502
+ }
503
+ /**
504
+ * @param key logical name for this input's history (namespaced under `@dloizides/ui-forms:recent:`).
505
+ * @param max most-recent-first cap on remembered values. Default {@link DEFAULT_MAX_RECENTS}.
506
+ */
507
+ declare function useRecentValues(key: string, max?: number): RecentValues;
508
+
509
+ /**
510
+ * `recentValuesCore` — the pure + storage-facing internals of {@link useRecentValues}, kept out of
511
+ * the hook file so the list algebra (trim / de-dupe / promote / cap) and the `localStorage` access
512
+ * are each unit-testable in isolation.
513
+ *
514
+ * ## RN-web storage reality
515
+ *
516
+ * `ui-forms` runs on react-native-web, so a component using this may mount on web (where
517
+ * `window.localStorage` exists), on native (where it does NOT), or under SSR / a hardened browser
518
+ * where touching `window.localStorage` THROWS (disabled cookies, privacy mode, quota). Every access
519
+ * therefore goes through {@link getStorage}, which returns `null` rather than throwing, and every
520
+ * read/write is wrapped so a storage failure degrades to an in-memory-only no-op instead of
521
+ * exploding inside render. On native / when storage is unavailable the feature is simply inert and
522
+ * the recents list is always `[]`.
523
+ */
524
+ /** Default cap on remembered values (most-recent-first). */
525
+ declare const DEFAULT_MAX_RECENTS = 10;
526
+
482
527
  /**
483
528
  * Chip/pill selector for selecting one or multiple options.
484
529
  *
@@ -737,12 +782,13 @@ declare function DateRangeControl({ value, onChange, onSubmit, fromLabel, toLabe
737
782
  /**
738
783
  * `AnchoredMenu` — the floating option list shared by `SelectControl` and `TypeaheadControl`.
739
784
  *
740
- * By default it renders an in-tree, absolutely-positioned popover under its anchor
741
- * (`position:absolute; top:100%; zIndex; elevation`). Set {@link AnchoredMenuProps.usePortal} to
742
- * render it — on web only — in a PORTAL to `document.body` with `position: fixed` at the trigger's
743
- * measured rect instead: that escapes react-native-web's trapped `z-index: 0` stacking contexts and
744
- * any ancestor `overflow: hidden`, so the menu is never painted under / clipped by later siblings
745
- * (the fix for the Country typeahead menu that hid behind the content below it). See {@link menuPortal}.
785
+ * By DEFAULT (web) it renders in a PORTAL to `document.body` with `position: fixed` at the trigger's
786
+ * measured rect: that escapes react-native-web's trapped `z-index: 0` stacking contexts and any
787
+ * ancestor `overflow: hidden`, so the menu is never painted under / clipped by later siblings (a
788
+ * table, a card, the next filter field). On native, and when {@link AnchoredMenuProps.usePortal} is
789
+ * explicitly `false`, it renders an in-tree, absolutely-positioned popover under its anchor
790
+ * (`position:absolute; top:100%; zIndex; elevation`) — which is exactly what left `SelectControl`'s
791
+ * menu hidden behind the attendee table until the default flipped. See {@link menuPortal}.
746
792
  * On web it dismisses on outside-click / Escape; on native the caller closes it on blur/select.
747
793
  * Purely presentational + theme-flat, so it stays reusable.
748
794
  *
@@ -769,6 +815,12 @@ interface AnchoredMenuProps {
769
815
  colors: AnchoredMenuColors;
770
816
  /** PRE-LOCALIZED accessibility hint applied to every option. */
771
817
  optionHint: string;
818
+ /**
819
+ * PRE-LOCALIZED accessible NAME for the menu container itself (e.g. "Recent values"). Optional and
820
+ * additive — omitted, the menu keeps only its `role="menu"` as before, so existing callers are
821
+ * unaffected. Supplied, a screen reader announces the list's purpose, not just "menu".
822
+ */
823
+ accessibilityLabel?: string;
772
824
  /** Base testID: the menu is `${testID}-menu`, each option `${testID}-option-${value}`. */
773
825
  testID: string;
774
826
  /** Node whose clicks count as "inside" (the anchor) so its own press doesn't self-dismiss. */
@@ -777,12 +829,17 @@ interface AnchoredMenuProps {
777
829
  * On web, render the popover in a portal to `document.body` with `position: fixed` at the anchor's
778
830
  * measured rect, so it escapes react-native-web's trapped stacking contexts and any ancestor
779
831
  * `overflow: hidden` (the fix for a menu that paints under / is clipped by the content below it).
780
- * No effect on native. Default `false` — the existing in-tree behaviour, so `SelectControl`
781
- * usages are unchanged; `TypeaheadControl` opts in.
832
+ * No effect on native (the `IS_WEB` guard keeps native in-tree regardless).
833
+ *
834
+ * Default `true` — portalling is the CORRECT default: an in-tree popover is always at risk of
835
+ * being painted under a later-painting sibling (a table, a card, the next filter field). The old
836
+ * default was `false`, which silently left `SelectControl`'s menu hidden behind the attendee table
837
+ * (a recurrence of the exact bug this mechanism fixed for `TypeaheadControl`). Pass `usePortal={false}`
838
+ * only for the rare case that genuinely wants the menu clipped to its container.
782
839
  */
783
840
  usePortal?: boolean;
784
841
  }
785
- declare function AnchoredMenu({ options, selectedValue, onSelect, onDismiss, colors, optionHint, testID, anchorRef, usePortal, }: AnchoredMenuProps): React.ReactElement;
842
+ declare function AnchoredMenu({ options, selectedValue, onSelect, onDismiss, colors, optionHint, accessibilityLabel, testID, anchorRef, usePortal, }: AnchoredMenuProps): React.ReactElement;
786
843
 
787
844
  declare const controlStyles: {
788
845
  /**
@@ -887,4 +944,4 @@ declare function suggestOptions(options: readonly ControlOption[], query: string
887
944
  /** True when `text` is non-empty yet matches no option's value or label exactly (case-insensitive). */
888
945
  declare function isUnmatched(options: readonly ControlOption[], text: string): boolean;
889
946
 
890
- export { AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, type ControlOption, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, type DateRangeControlProps, type DateRangeValue, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, type FieldSpacing, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormSwitch, type FormSwitchProps, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, isUnmatched, suggestOptions, useThemedInput };
947
+ export { AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, type ControlOption, DEFAULT_MAX_RECENTS, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, type DateRangeControlProps, type DateRangeValue, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, type FieldSpacing, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormSwitch, type FormSwitchProps, type RecentValues, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, isUnmatched, suggestOptions, useRecentValues, useThemedInput };
package/dist/index.d.ts CHANGED
@@ -394,8 +394,21 @@ interface FormFieldProps extends Omit<TextInputProps, 'style'> {
394
394
  required?: boolean;
395
395
  error?: string;
396
396
  containerStyle?: ViewStyle;
397
+ /**
398
+ * OPT-IN "recent values" behaviour. When set, the values submitted through this field are
399
+ * remembered (per this key) and offered in a dropdown on focus; picking one fills the input.
400
+ * Values are recorded on submit (Enter) and on blur-with-content. Persisted to `localStorage`
401
+ * under `@dloizides/ui-forms:recent:<recentKey>`; a silent no-op on native / when storage is
402
+ * unavailable.
403
+ *
404
+ * Leave it UNSET (the default) and the field behaves exactly as before — nothing rendered, no
405
+ * storage touched. NEVER set it on a password / secure field: recents must not persist secrets.
406
+ */
407
+ recentKey?: string;
408
+ /** Cap on remembered values when {@link recentKey} is set (most-recent-first). Default 10. */
409
+ recentMax?: number;
397
410
  }
398
- declare const FormField: ({ label, labelHidden, required, error, containerStyle, ...textInputProps }: FormFieldProps) => React.ReactElement;
411
+ declare const FormField: ({ label, labelHidden, required, error, containerStyle, recentKey, recentMax, ...textInputProps }: FormFieldProps) => React.ReactElement;
399
412
 
400
413
  /**
401
414
  * `ThemedTextInput` — the shared, readable + lively text-input primitive. A thin `<TextInput>`
@@ -479,6 +492,38 @@ interface ThemedInput {
479
492
  }
480
493
  declare function useThemedInput(options?: UseThemedInputOptions): ThemedInput;
481
494
 
495
+ interface RecentValues {
496
+ /** The remembered values, most-recent-first (never longer than `max`; `[]` when storage is unavailable). */
497
+ recents: string[];
498
+ /** Fold a value into the list (trim / de-dupe / promote / cap) and persist. Empty values are ignored. */
499
+ remember: (value: string) => void;
500
+ /** Forget every remembered value for this key and clear it from storage. */
501
+ clear: () => void;
502
+ }
503
+ /**
504
+ * @param key logical name for this input's history (namespaced under `@dloizides/ui-forms:recent:`).
505
+ * @param max most-recent-first cap on remembered values. Default {@link DEFAULT_MAX_RECENTS}.
506
+ */
507
+ declare function useRecentValues(key: string, max?: number): RecentValues;
508
+
509
+ /**
510
+ * `recentValuesCore` — the pure + storage-facing internals of {@link useRecentValues}, kept out of
511
+ * the hook file so the list algebra (trim / de-dupe / promote / cap) and the `localStorage` access
512
+ * are each unit-testable in isolation.
513
+ *
514
+ * ## RN-web storage reality
515
+ *
516
+ * `ui-forms` runs on react-native-web, so a component using this may mount on web (where
517
+ * `window.localStorage` exists), on native (where it does NOT), or under SSR / a hardened browser
518
+ * where touching `window.localStorage` THROWS (disabled cookies, privacy mode, quota). Every access
519
+ * therefore goes through {@link getStorage}, which returns `null` rather than throwing, and every
520
+ * read/write is wrapped so a storage failure degrades to an in-memory-only no-op instead of
521
+ * exploding inside render. On native / when storage is unavailable the feature is simply inert and
522
+ * the recents list is always `[]`.
523
+ */
524
+ /** Default cap on remembered values (most-recent-first). */
525
+ declare const DEFAULT_MAX_RECENTS = 10;
526
+
482
527
  /**
483
528
  * Chip/pill selector for selecting one or multiple options.
484
529
  *
@@ -737,12 +782,13 @@ declare function DateRangeControl({ value, onChange, onSubmit, fromLabel, toLabe
737
782
  /**
738
783
  * `AnchoredMenu` — the floating option list shared by `SelectControl` and `TypeaheadControl`.
739
784
  *
740
- * By default it renders an in-tree, absolutely-positioned popover under its anchor
741
- * (`position:absolute; top:100%; zIndex; elevation`). Set {@link AnchoredMenuProps.usePortal} to
742
- * render it — on web only — in a PORTAL to `document.body` with `position: fixed` at the trigger's
743
- * measured rect instead: that escapes react-native-web's trapped `z-index: 0` stacking contexts and
744
- * any ancestor `overflow: hidden`, so the menu is never painted under / clipped by later siblings
745
- * (the fix for the Country typeahead menu that hid behind the content below it). See {@link menuPortal}.
785
+ * By DEFAULT (web) it renders in a PORTAL to `document.body` with `position: fixed` at the trigger's
786
+ * measured rect: that escapes react-native-web's trapped `z-index: 0` stacking contexts and any
787
+ * ancestor `overflow: hidden`, so the menu is never painted under / clipped by later siblings (a
788
+ * table, a card, the next filter field). On native, and when {@link AnchoredMenuProps.usePortal} is
789
+ * explicitly `false`, it renders an in-tree, absolutely-positioned popover under its anchor
790
+ * (`position:absolute; top:100%; zIndex; elevation`) — which is exactly what left `SelectControl`'s
791
+ * menu hidden behind the attendee table until the default flipped. See {@link menuPortal}.
746
792
  * On web it dismisses on outside-click / Escape; on native the caller closes it on blur/select.
747
793
  * Purely presentational + theme-flat, so it stays reusable.
748
794
  *
@@ -769,6 +815,12 @@ interface AnchoredMenuProps {
769
815
  colors: AnchoredMenuColors;
770
816
  /** PRE-LOCALIZED accessibility hint applied to every option. */
771
817
  optionHint: string;
818
+ /**
819
+ * PRE-LOCALIZED accessible NAME for the menu container itself (e.g. "Recent values"). Optional and
820
+ * additive — omitted, the menu keeps only its `role="menu"` as before, so existing callers are
821
+ * unaffected. Supplied, a screen reader announces the list's purpose, not just "menu".
822
+ */
823
+ accessibilityLabel?: string;
772
824
  /** Base testID: the menu is `${testID}-menu`, each option `${testID}-option-${value}`. */
773
825
  testID: string;
774
826
  /** Node whose clicks count as "inside" (the anchor) so its own press doesn't self-dismiss. */
@@ -777,12 +829,17 @@ interface AnchoredMenuProps {
777
829
  * On web, render the popover in a portal to `document.body` with `position: fixed` at the anchor's
778
830
  * measured rect, so it escapes react-native-web's trapped stacking contexts and any ancestor
779
831
  * `overflow: hidden` (the fix for a menu that paints under / is clipped by the content below it).
780
- * No effect on native. Default `false` — the existing in-tree behaviour, so `SelectControl`
781
- * usages are unchanged; `TypeaheadControl` opts in.
832
+ * No effect on native (the `IS_WEB` guard keeps native in-tree regardless).
833
+ *
834
+ * Default `true` — portalling is the CORRECT default: an in-tree popover is always at risk of
835
+ * being painted under a later-painting sibling (a table, a card, the next filter field). The old
836
+ * default was `false`, which silently left `SelectControl`'s menu hidden behind the attendee table
837
+ * (a recurrence of the exact bug this mechanism fixed for `TypeaheadControl`). Pass `usePortal={false}`
838
+ * only for the rare case that genuinely wants the menu clipped to its container.
782
839
  */
783
840
  usePortal?: boolean;
784
841
  }
785
- declare function AnchoredMenu({ options, selectedValue, onSelect, onDismiss, colors, optionHint, testID, anchorRef, usePortal, }: AnchoredMenuProps): React.ReactElement;
842
+ declare function AnchoredMenu({ options, selectedValue, onSelect, onDismiss, colors, optionHint, accessibilityLabel, testID, anchorRef, usePortal, }: AnchoredMenuProps): React.ReactElement;
786
843
 
787
844
  declare const controlStyles: {
788
845
  /**
@@ -887,4 +944,4 @@ declare function suggestOptions(options: readonly ControlOption[], query: string
887
944
  /** True when `text` is non-empty yet matches no option's value or label exactly (case-insensitive). */
888
945
  declare function isUnmatched(options: readonly ControlOption[], text: string): boolean;
889
946
 
890
- export { AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, type ControlOption, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, type DateRangeControlProps, type DateRangeValue, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, type FieldSpacing, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormSwitch, type FormSwitchProps, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, isUnmatched, suggestOptions, useThemedInput };
947
+ export { AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, type ControlOption, DEFAULT_MAX_RECENTS, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, type DateRangeControlProps, type DateRangeValue, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, type FieldSpacing, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormSwitch, type FormSwitchProps, type RecentValues, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, isUnmatched, suggestOptions, useRecentValues, useThemedInput };