@dloizides/ui-forms 1.15.0 → 1.17.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
@@ -970,4 +970,60 @@ declare function suggestOptions(options: readonly ControlOption[], query: string
970
970
  /** True when `text` is non-empty yet matches no option's value or label exactly (case-insensitive). */
971
971
  declare function isUnmatched(options: readonly ControlOption[], text: string): boolean;
972
972
 
973
- export { AnchoredMenu, type AnchoredMenuColors, 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, 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 };
973
+ /**
974
+ * Group selection — the pure tri-state logic behind a "select all" / "select this category"
975
+ * control, kept out of any component so it is testable as LOGIC rather than through the DOM.
976
+ *
977
+ * ## Why it lives here and not in @dloizides/ui-tables
978
+ *
979
+ * `ui-tables` already had this arithmetic inside `DataTable/selection.ts`, but deliberately
980
+ * unexported: its tri-state is a `const enum`, which in a published `.d.ts` is an ambient const
981
+ * enum that every `isolatedModules` consumer rejects. It is also page-scoped and wired to the
982
+ * table's select-all banner. So the second consumer (a 12-tile console picker with no table in
983
+ * sight, and no reason to pull a table package into its bundle budget) could not reach it.
984
+ *
985
+ * This module is the extraction: domain-neutral (keys in, keys out — nothing here knows what a
986
+ * key *is*), component-free, and crossing the package boundary as a plain interface rather than
987
+ * an enum. `ui-tables` can adopt it and delete its private copy.
988
+ */
989
+ /**
990
+ * A group's tri-state, as two booleans rather than an enum.
991
+ *
992
+ * `all` and `some` are mutually exclusive: a fully selected group is `{ all: true, some: false }`,
993
+ * so a caller can branch on `all` then `some` without re-deriving "partial" itself. Both false
994
+ * means nothing in the group is selected — which is also what an EMPTY group reports, because an
995
+ * empty group is neither fully nor partially selected.
996
+ */
997
+ interface GroupSelection {
998
+ /** Every key in the group is selected (never true for an empty group). */
999
+ readonly all: boolean;
1000
+ /** At least one, but not every, key in the group is selected — the mixed/indeterminate state. */
1001
+ readonly some: boolean;
1002
+ }
1003
+ /**
1004
+ * Resolve a group's tri-state from the selection ∩ the group.
1005
+ *
1006
+ * Scoped to the GROUP, not to the whole selection: selected keys OUTSIDE the group must never
1007
+ * make this group look partially selected.
1008
+ */
1009
+ declare function resolveGroupSelection<T>(selected: readonly T[], groupKeys: readonly T[]): GroupSelection;
1010
+ /**
1011
+ * The value an `aria-checked` / `accessibilityState` consumer needs for a tri-state control.
1012
+ *
1013
+ * A partially selected group is `'mixed'`, which is what makes a screen reader announce the
1014
+ * control honestly instead of claiming the whole category is on.
1015
+ */
1016
+ declare function groupAriaChecked(state: GroupSelection): boolean | 'mixed';
1017
+ /**
1018
+ * Toggle a whole group, returning the NEXT selection array. Pure — never mutates the input.
1019
+ *
1020
+ * All-on → off. Partial or off → on: the conventional behaviour, and the one a mixed checkbox
1021
+ * looks like it will do. Keys OUTSIDE the group always survive untouched, so a category control
1022
+ * can never silently clear a selection it does not own.
1023
+ *
1024
+ * `order`, when given, is the canonical key order the result is sorted into (catalogue order, say)
1025
+ * so the selection does not carry click order around. Without it, newly added keys are appended.
1026
+ */
1027
+ declare function toggleGroupSelection<T>(selected: readonly T[], groupKeys: readonly T[], order?: readonly T[]): T[];
1028
+
1029
+ export { AnchoredMenu, type AnchoredMenuColors, 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, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormSwitch, type FormSwitchProps, type GroupSelection, type RecentValues, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, groupAriaChecked, isUnmatched, resolveGroupSelection, suggestOptions, toggleGroupSelection, useRecentValues, useThemedInput };
package/dist/index.d.ts CHANGED
@@ -970,4 +970,60 @@ declare function suggestOptions(options: readonly ControlOption[], query: string
970
970
  /** True when `text` is non-empty yet matches no option's value or label exactly (case-insensitive). */
971
971
  declare function isUnmatched(options: readonly ControlOption[], text: string): boolean;
972
972
 
973
- export { AnchoredMenu, type AnchoredMenuColors, 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, 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 };
973
+ /**
974
+ * Group selection — the pure tri-state logic behind a "select all" / "select this category"
975
+ * control, kept out of any component so it is testable as LOGIC rather than through the DOM.
976
+ *
977
+ * ## Why it lives here and not in @dloizides/ui-tables
978
+ *
979
+ * `ui-tables` already had this arithmetic inside `DataTable/selection.ts`, but deliberately
980
+ * unexported: its tri-state is a `const enum`, which in a published `.d.ts` is an ambient const
981
+ * enum that every `isolatedModules` consumer rejects. It is also page-scoped and wired to the
982
+ * table's select-all banner. So the second consumer (a 12-tile console picker with no table in
983
+ * sight, and no reason to pull a table package into its bundle budget) could not reach it.
984
+ *
985
+ * This module is the extraction: domain-neutral (keys in, keys out — nothing here knows what a
986
+ * key *is*), component-free, and crossing the package boundary as a plain interface rather than
987
+ * an enum. `ui-tables` can adopt it and delete its private copy.
988
+ */
989
+ /**
990
+ * A group's tri-state, as two booleans rather than an enum.
991
+ *
992
+ * `all` and `some` are mutually exclusive: a fully selected group is `{ all: true, some: false }`,
993
+ * so a caller can branch on `all` then `some` without re-deriving "partial" itself. Both false
994
+ * means nothing in the group is selected — which is also what an EMPTY group reports, because an
995
+ * empty group is neither fully nor partially selected.
996
+ */
997
+ interface GroupSelection {
998
+ /** Every key in the group is selected (never true for an empty group). */
999
+ readonly all: boolean;
1000
+ /** At least one, but not every, key in the group is selected — the mixed/indeterminate state. */
1001
+ readonly some: boolean;
1002
+ }
1003
+ /**
1004
+ * Resolve a group's tri-state from the selection ∩ the group.
1005
+ *
1006
+ * Scoped to the GROUP, not to the whole selection: selected keys OUTSIDE the group must never
1007
+ * make this group look partially selected.
1008
+ */
1009
+ declare function resolveGroupSelection<T>(selected: readonly T[], groupKeys: readonly T[]): GroupSelection;
1010
+ /**
1011
+ * The value an `aria-checked` / `accessibilityState` consumer needs for a tri-state control.
1012
+ *
1013
+ * A partially selected group is `'mixed'`, which is what makes a screen reader announce the
1014
+ * control honestly instead of claiming the whole category is on.
1015
+ */
1016
+ declare function groupAriaChecked(state: GroupSelection): boolean | 'mixed';
1017
+ /**
1018
+ * Toggle a whole group, returning the NEXT selection array. Pure — never mutates the input.
1019
+ *
1020
+ * All-on → off. Partial or off → on: the conventional behaviour, and the one a mixed checkbox
1021
+ * looks like it will do. Keys OUTSIDE the group always survive untouched, so a category control
1022
+ * can never silently clear a selection it does not own.
1023
+ *
1024
+ * `order`, when given, is the canonical key order the result is sorted into (catalogue order, say)
1025
+ * so the selection does not carry click order around. Without it, newly added keys are appended.
1026
+ */
1027
+ declare function toggleGroupSelection<T>(selected: readonly T[], groupKeys: readonly T[], order?: readonly T[]): T[];
1028
+
1029
+ export { AnchoredMenu, type AnchoredMenuColors, 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, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormSwitch, type FormSwitchProps, type GroupSelection, type RecentValues, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, groupAriaChecked, isUnmatched, resolveGroupSelection, suggestOptions, toggleGroupSelection, useRecentValues, useThemedInput };
package/dist/index.js CHANGED
@@ -964,10 +964,13 @@ var OUTLINE_PAD_H = 12;
964
964
  var OUTLINE_PAD_V = 5;
965
965
  var OUTLINE_FONT_SIZE = 13;
966
966
  var CHIP_ICON_GAP = 6;
967
+ var MIN_TARGET_PX = 44;
967
968
  var chipStyles = reactNative.StyleSheet.create({
968
969
  chip: {
969
970
  paddingHorizontal: 12,
970
971
  paddingVertical: 6,
972
+ minHeight: MIN_TARGET_PX,
973
+ justifyContent: "center",
971
974
  borderRadius: SOLID_RADIUS,
972
975
  backgroundColor: TRANSPARENT_COLOR,
973
976
  borderWidth: 1
@@ -975,6 +978,8 @@ var chipStyles = reactNative.StyleSheet.create({
975
978
  chipOutline: {
976
979
  paddingHorizontal: OUTLINE_PAD_H,
977
980
  paddingVertical: OUTLINE_PAD_V,
981
+ minHeight: MIN_TARGET_PX,
982
+ justifyContent: "center",
978
983
  borderRadius: OUTLINE_RADIUS,
979
984
  borderWidth: 1
980
985
  },
@@ -1679,6 +1684,30 @@ function DateRangeControl({
1679
1684
  ] });
1680
1685
  }
1681
1686
 
1687
+ // src/groupSelection/groupSelection.ts
1688
+ var NONE = 0;
1689
+ function countSelected(selected, groupKeys) {
1690
+ return groupKeys.filter((key) => selected.includes(key)).length;
1691
+ }
1692
+ function resolveGroupSelection(selected, groupKeys) {
1693
+ const count = countSelected(selected, groupKeys);
1694
+ if (count === NONE) return { all: false, some: false };
1695
+ return count === groupKeys.length ? { all: true, some: false } : { all: false, some: true };
1696
+ }
1697
+ function groupAriaChecked(state) {
1698
+ if (state.all) return true;
1699
+ return state.some ? "mixed" : false;
1700
+ }
1701
+ function toggleGroupSelection(selected, groupKeys, order) {
1702
+ const { all } = resolveGroupSelection(selected, groupKeys);
1703
+ if (all) return selected.filter((key) => !groupKeys.includes(key));
1704
+ const merged = [...selected];
1705
+ groupKeys.forEach((key) => {
1706
+ if (!merged.includes(key)) merged.push(key);
1707
+ });
1708
+ return order === void 0 ? merged : order.filter((key) => merged.includes(key));
1709
+ }
1710
+
1682
1711
  exports.AnchoredMenu = AnchoredMenu;
1683
1712
  exports.ChipSelector = ChipSelector;
1684
1713
  exports.DEFAULT_MAX_RECENTS = DEFAULT_MAX_RECENTS;
@@ -1698,8 +1727,11 @@ exports.SelectControl = SelectControl;
1698
1727
  exports.ThemedTextInput = ThemedTextInput;
1699
1728
  exports.TypeaheadControl = TypeaheadControl;
1700
1729
  exports.controlStyles = controlStyles;
1730
+ exports.groupAriaChecked = groupAriaChecked;
1701
1731
  exports.isUnmatched = isUnmatched;
1732
+ exports.resolveGroupSelection = resolveGroupSelection;
1702
1733
  exports.suggestOptions = suggestOptions;
1734
+ exports.toggleGroupSelection = toggleGroupSelection;
1703
1735
  exports.useRecentValues = useRecentValues;
1704
1736
  exports.useThemedInput = useThemedInput;
1705
1737
  //# sourceMappingURL=index.js.map