@dloizides/ui-forms 1.13.4 → 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
  *
@@ -770,6 +815,12 @@ interface AnchoredMenuProps {
770
815
  colors: AnchoredMenuColors;
771
816
  /** PRE-LOCALIZED accessibility hint applied to every option. */
772
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;
773
824
  /** Base testID: the menu is `${testID}-menu`, each option `${testID}-option-${value}`. */
774
825
  testID: string;
775
826
  /** Node whose clicks count as "inside" (the anchor) so its own press doesn't self-dismiss. */
@@ -788,7 +839,7 @@ interface AnchoredMenuProps {
788
839
  */
789
840
  usePortal?: boolean;
790
841
  }
791
- 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;
792
843
 
793
844
  declare const controlStyles: {
794
845
  /**
@@ -893,4 +944,4 @@ declare function suggestOptions(options: readonly ControlOption[], query: string
893
944
  /** True when `text` is non-empty yet matches no option's value or label exactly (case-insensitive). */
894
945
  declare function isUnmatched(options: readonly ControlOption[], text: string): boolean;
895
946
 
896
- 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
  *
@@ -770,6 +815,12 @@ interface AnchoredMenuProps {
770
815
  colors: AnchoredMenuColors;
771
816
  /** PRE-LOCALIZED accessibility hint applied to every option. */
772
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;
773
824
  /** Base testID: the menu is `${testID}-menu`, each option `${testID}-option-${value}`. */
774
825
  testID: string;
775
826
  /** Node whose clicks count as "inside" (the anchor) so its own press doesn't self-dismiss. */
@@ -788,7 +839,7 @@ interface AnchoredMenuProps {
788
839
  */
789
840
  usePortal?: boolean;
790
841
  }
791
- 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;
792
843
 
793
844
  declare const controlStyles: {
794
845
  /**
@@ -893,4 +944,4 @@ declare function suggestOptions(options: readonly ControlOption[], query: string
893
944
  /** True when `text` is non-empty yet matches no option's value or label exactly (case-insensitive). */
894
945
  declare function isUnmatched(options: readonly ControlOption[], text: string): boolean;
895
946
 
896
- 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 };