@dloizides/ui-forms 1.23.0 → 1.24.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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.24.0
4
+
5
+ BEAD-3 "Report form: searchable pattern field": review fix.
6
+
7
+ - New `useKeepInView(trigger)`, moved from Jinx: scrolls the returned view into sight each time `trigger` takes a new value, as far as needed and no further. Web only; a no-op on native.
8
+
3
9
  ## 1.23.0
4
10
 
5
11
  BEAD-3 "Report form: searchable pattern field and shared form feedback".
package/dist/index.d.mts CHANGED
@@ -1146,6 +1146,9 @@ type FocusableRefs<Field extends string> = Readonly<Record<Field, RefObject<Focu
1146
1146
  /** Returns a stable function that focuses the first listed field that is on screen. */
1147
1147
  declare function useFocusFirstInvalid<Field extends string>(refs: FocusableRefs<Field>): (invalid: readonly Field[]) => void;
1148
1148
 
1149
+ /** Scrolls the returned view fully into sight each time `trigger` takes a new value. */
1150
+ declare function useKeepInView(trigger: unknown): RefObject<View | null>;
1151
+
1149
1152
  /** Every string the field shows; all pre-localized. */
1150
1153
  interface UploadWithLinkLabels {
1151
1154
  title: string;
@@ -1170,4 +1173,4 @@ interface UploadWithLinkFieldProps {
1170
1173
  /** An image field: preview, an upload control, and a link input revealed on request. */
1171
1174
  declare const UploadWithLinkField: ({ value, onChange, labels, uploadControl, testID, disabled, previewSize, }: UploadWithLinkFieldProps) => React.ReactElement;
1172
1175
 
1173
- export { AlignToControl, type AlignToControlProps, AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuListbox, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipSingleSelectRole, 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, type Focusable, type FocusableRefs, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormStatusLine, type FormStatusLineProps, FormSwitch, type FormSwitchProps, type GroupSelection, INLINE_FIELD_ROW_MIN_WIDTH, InlineFieldRow, type InlineFieldRowLayout, type InlineFieldRowProps, type RecentValues, SelectControl, type SelectControlProps, type StepNeighbours, StepperNav, type StepperNavLabels, type StepperNavProps, type StepperStep, StepperTabs, type StepperTabsProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type TypeaheadSearch, type TypeaheadSearchProps, UploadWithLinkField, type UploadWithLinkFieldProps, type UploadWithLinkLabels, type UseThemedInputOptions, controlStyles, groupAriaChecked, inlineFieldRowLayout, isUnmatched, resolveGroupSelection, resolveStepNeighbours, suggestOptions, toggleGroupSelection, useFocusFirstInvalid, useRecentValues, useThemedInput };
1176
+ export { AlignToControl, type AlignToControlProps, AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuListbox, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipSingleSelectRole, 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, type Focusable, type FocusableRefs, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormStatusLine, type FormStatusLineProps, FormSwitch, type FormSwitchProps, type GroupSelection, INLINE_FIELD_ROW_MIN_WIDTH, InlineFieldRow, type InlineFieldRowLayout, type InlineFieldRowProps, type RecentValues, SelectControl, type SelectControlProps, type StepNeighbours, StepperNav, type StepperNavLabels, type StepperNavProps, type StepperStep, StepperTabs, type StepperTabsProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type TypeaheadSearch, type TypeaheadSearchProps, UploadWithLinkField, type UploadWithLinkFieldProps, type UploadWithLinkLabels, type UseThemedInputOptions, controlStyles, groupAriaChecked, inlineFieldRowLayout, isUnmatched, resolveGroupSelection, resolveStepNeighbours, suggestOptions, toggleGroupSelection, useFocusFirstInvalid, useKeepInView, useRecentValues, useThemedInput };
package/dist/index.d.ts CHANGED
@@ -1146,6 +1146,9 @@ type FocusableRefs<Field extends string> = Readonly<Record<Field, RefObject<Focu
1146
1146
  /** Returns a stable function that focuses the first listed field that is on screen. */
1147
1147
  declare function useFocusFirstInvalid<Field extends string>(refs: FocusableRefs<Field>): (invalid: readonly Field[]) => void;
1148
1148
 
1149
+ /** Scrolls the returned view fully into sight each time `trigger` takes a new value. */
1150
+ declare function useKeepInView(trigger: unknown): RefObject<View | null>;
1151
+
1149
1152
  /** Every string the field shows; all pre-localized. */
1150
1153
  interface UploadWithLinkLabels {
1151
1154
  title: string;
@@ -1170,4 +1173,4 @@ interface UploadWithLinkFieldProps {
1170
1173
  /** An image field: preview, an upload control, and a link input revealed on request. */
1171
1174
  declare const UploadWithLinkField: ({ value, onChange, labels, uploadControl, testID, disabled, previewSize, }: UploadWithLinkFieldProps) => React.ReactElement;
1172
1175
 
1173
- export { AlignToControl, type AlignToControlProps, AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuListbox, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipSingleSelectRole, 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, type Focusable, type FocusableRefs, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormStatusLine, type FormStatusLineProps, FormSwitch, type FormSwitchProps, type GroupSelection, INLINE_FIELD_ROW_MIN_WIDTH, InlineFieldRow, type InlineFieldRowLayout, type InlineFieldRowProps, type RecentValues, SelectControl, type SelectControlProps, type StepNeighbours, StepperNav, type StepperNavLabels, type StepperNavProps, type StepperStep, StepperTabs, type StepperTabsProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type TypeaheadSearch, type TypeaheadSearchProps, UploadWithLinkField, type UploadWithLinkFieldProps, type UploadWithLinkLabels, type UseThemedInputOptions, controlStyles, groupAriaChecked, inlineFieldRowLayout, isUnmatched, resolveGroupSelection, resolveStepNeighbours, suggestOptions, toggleGroupSelection, useFocusFirstInvalid, useRecentValues, useThemedInput };
1176
+ export { AlignToControl, type AlignToControlProps, AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuListbox, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipSingleSelectRole, 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, type Focusable, type FocusableRefs, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormStatusLine, type FormStatusLineProps, FormSwitch, type FormSwitchProps, type GroupSelection, INLINE_FIELD_ROW_MIN_WIDTH, InlineFieldRow, type InlineFieldRowLayout, type InlineFieldRowProps, type RecentValues, SelectControl, type SelectControlProps, type StepNeighbours, StepperNav, type StepperNavLabels, type StepperNavProps, type StepperStep, StepperTabs, type StepperTabsProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type TypeaheadSearch, type TypeaheadSearchProps, UploadWithLinkField, type UploadWithLinkFieldProps, type UploadWithLinkLabels, type UseThemedInputOptions, controlStyles, groupAriaChecked, inlineFieldRowLayout, isUnmatched, resolveGroupSelection, resolveStepNeighbours, suggestOptions, toggleGroupSelection, useFocusFirstInvalid, useKeepInView, useRecentValues, useThemedInput };
package/dist/index.js CHANGED
@@ -571,12 +571,16 @@ function listboxDomId(testID) {
571
571
  function isDetailBesideTitle(menuWidth) {
572
572
  return menuWidth >= DETAIL_BESIDE_MIN_MENU_WIDTH;
573
573
  }
574
+ function rowInk(colors, isActive, isHighlighted) {
575
+ if (isHighlighted) return { title: colors.surface, detail: colors.surface };
576
+ return { title: isActive ? colors.brand : colors.text, detail: colors.textSecondary ?? colors.text };
577
+ }
574
578
  function OptionRow({ option, isActive, isHighlighted, isListboxRow, menuWidth, hint, colors, testID, onSelect }) {
575
579
  const hasDetail = option.detail !== void 0 && option.detail !== "";
576
580
  const label = hasDetail ? `${option.label}, ${option.detail}` : option.label;
577
581
  const roleProps = isListboxRow ? { role: "option", tabIndex: -1, id: optionDomId(testID, option.value), "aria-selected": isHighlighted } : { accessibilityRole: "menuitem", "aria-selected": isActive };
578
582
  const isBold = isActive || isHighlighted;
579
- const titleColor = isActive ? colors.brand : colors.text;
583
+ const ink = rowInk(colors, isActive, isHighlighted);
580
584
  return /* @__PURE__ */ jsxRuntime.jsx(
581
585
  reactNative.Pressable,
582
586
  {
@@ -585,11 +589,11 @@ function OptionRow({ option, isActive, isHighlighted, isListboxRow, menuWidth, h
585
589
  accessibilityHint: hint,
586
590
  accessibilityState: { selected: isActive },
587
591
  onPress: () => onSelect(option.value),
588
- style: [controlStyles.option, isHighlighted ? { backgroundColor: colors.border } : null],
592
+ style: [controlStyles.option, isHighlighted ? { backgroundColor: colors.text } : null],
589
593
  testID: `${testID}-option-${option.value}`,
590
594
  children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: isDetailBesideTitle(menuWidth) ? styles7.beside : styles7.stacked, children: [
591
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [controlStyles.optionText, styles7.shrink, { color: titleColor, fontWeight: isBold ? ACTIVE_WEIGHT : IDLE_WEIGHT }], children: option.label }),
592
- hasDetail ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.detail, { color: colors.textSecondary ?? colors.text }], children: option.detail }) : null
595
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [controlStyles.optionText, styles7.shrink, { color: ink.title, fontWeight: isBold ? ACTIVE_WEIGHT : IDLE_WEIGHT }], children: option.label }),
596
+ hasDetail ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.detail, { color: ink.detail }], children: option.detail }) : null
593
597
  ] })
594
598
  }
595
599
  );
@@ -1761,6 +1765,24 @@ function SelectControl({
1761
1765
  ) : null
1762
1766
  ] });
1763
1767
  }
1768
+ var CLOSE_DELAY_MS2 = 140;
1769
+ function useDelayedBlurClose(open, close) {
1770
+ const timerRef = React2.useRef(null);
1771
+ const cancelClose = React2.useCallback(() => {
1772
+ if (timerRef.current !== null) clearTimeout(timerRef.current);
1773
+ timerRef.current = null;
1774
+ }, []);
1775
+ React2.useEffect(() => cancelClose, [cancelClose]);
1776
+ const handleFocus = React2.useCallback(() => {
1777
+ cancelClose();
1778
+ open();
1779
+ }, [cancelClose, open]);
1780
+ const handleBlur = React2.useCallback(() => {
1781
+ cancelClose();
1782
+ timerRef.current = setTimeout(close, CLOSE_DELAY_MS2);
1783
+ }, [cancelClose, close]);
1784
+ return { handleFocus, handleBlur };
1785
+ }
1764
1786
 
1765
1787
  // src/controls/suggestOptions.ts
1766
1788
  var RANK_VALUE_EXACT = 0;
@@ -1926,7 +1948,6 @@ function useTypeaheadKeys(input) {
1926
1948
  [rowCount, isOpen, highlighted, setIsOpen, setHighlightIndex, close, pick]
1927
1949
  );
1928
1950
  }
1929
- var CLOSE_DELAY_MS2 = 140;
1930
1951
  var DEFAULT_TYPEAHEAD_MIN_CHARS = 1;
1931
1952
  var DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS = 8;
1932
1953
  function comboboxAria(testID, isExpanded, highlighted) {
@@ -1948,6 +1969,7 @@ function TypeaheadControlImpl(props, ref) {
1948
1969
  const { colors, palette } = theme;
1949
1970
  const anchorRef = React2.useRef(null);
1950
1971
  const state = useTypeaheadState({ options, search, value, minChars, maxSuggestions, onChange, onSubmit });
1972
+ const { handleFocus, handleBlur } = useDelayedBlurClose(state.open, state.close);
1951
1973
  const showMenu = state.isOpen && (state.rows.length > 0 || state.statusText !== void 0);
1952
1974
  const isExpanded = state.isOpen && state.rows.length > 0;
1953
1975
  const hasError = error !== void 0 && error !== "";
@@ -1971,8 +1993,8 @@ function TypeaheadControlImpl(props, ref) {
1971
1993
  spellCheck: false,
1972
1994
  value,
1973
1995
  onChangeText: state.handleChange,
1974
- onFocus: state.open,
1975
- onBlur: () => setTimeout(state.close, CLOSE_DELAY_MS2),
1996
+ onFocus: handleFocus,
1997
+ onBlur: handleBlur,
1976
1998
  onKeyPress: state.handleKeyPress,
1977
1999
  onSubmitEditing: state.handleSubmit,
1978
2000
  style: [controlStyles.input, { borderColor: hasError ? fieldErrorInk(theme) : colors.border, backgroundColor: colors.surface, color: colors.text }],
@@ -2234,6 +2256,18 @@ function useFocusFirstInvalid(refs) {
2234
2256
  first?.focus();
2235
2257
  }, []);
2236
2258
  }
2259
+ function isScrollable(node) {
2260
+ return "scrollIntoView" in Object(node);
2261
+ }
2262
+ function useKeepInView(trigger) {
2263
+ const ref = React2.useRef(null);
2264
+ React2.useEffect(() => {
2265
+ const node = ref.current;
2266
+ const hasTrigger = trigger !== void 0 && trigger !== null;
2267
+ if (hasTrigger && isScrollable(node)) node.scrollIntoView({ block: "nearest" });
2268
+ }, [trigger]);
2269
+ return ref;
2270
+ }
2237
2271
  var DEFAULT_PREVIEW_PX = 48;
2238
2272
  var PREVIEW_RADIUS = 8;
2239
2273
  var GAP = 8;
@@ -2333,6 +2367,7 @@ exports.resolveStepNeighbours = resolveStepNeighbours;
2333
2367
  exports.suggestOptions = suggestOptions;
2334
2368
  exports.toggleGroupSelection = toggleGroupSelection;
2335
2369
  exports.useFocusFirstInvalid = useFocusFirstInvalid;
2370
+ exports.useKeepInView = useKeepInView;
2336
2371
  exports.useRecentValues = useRecentValues;
2337
2372
  exports.useThemedInput = useThemedInput;
2338
2373
  //# sourceMappingURL=index.js.map