@dloizides/ui-forms 1.15.0 → 1.16.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
@@ -1679,6 +1679,30 @@ function DateRangeControl({
1679
1679
  ] });
1680
1680
  }
1681
1681
 
1682
+ // src/groupSelection/groupSelection.ts
1683
+ var NONE = 0;
1684
+ function countSelected(selected, groupKeys) {
1685
+ return groupKeys.filter((key) => selected.includes(key)).length;
1686
+ }
1687
+ function resolveGroupSelection(selected, groupKeys) {
1688
+ const count = countSelected(selected, groupKeys);
1689
+ if (count === NONE) return { all: false, some: false };
1690
+ return count === groupKeys.length ? { all: true, some: false } : { all: false, some: true };
1691
+ }
1692
+ function groupAriaChecked(state) {
1693
+ if (state.all) return true;
1694
+ return state.some ? "mixed" : false;
1695
+ }
1696
+ function toggleGroupSelection(selected, groupKeys, order) {
1697
+ const { all } = resolveGroupSelection(selected, groupKeys);
1698
+ if (all) return selected.filter((key) => !groupKeys.includes(key));
1699
+ const merged = [...selected];
1700
+ groupKeys.forEach((key) => {
1701
+ if (!merged.includes(key)) merged.push(key);
1702
+ });
1703
+ return order === void 0 ? merged : order.filter((key) => merged.includes(key));
1704
+ }
1705
+
1682
1706
  exports.AnchoredMenu = AnchoredMenu;
1683
1707
  exports.ChipSelector = ChipSelector;
1684
1708
  exports.DEFAULT_MAX_RECENTS = DEFAULT_MAX_RECENTS;
@@ -1698,8 +1722,11 @@ exports.SelectControl = SelectControl;
1698
1722
  exports.ThemedTextInput = ThemedTextInput;
1699
1723
  exports.TypeaheadControl = TypeaheadControl;
1700
1724
  exports.controlStyles = controlStyles;
1725
+ exports.groupAriaChecked = groupAriaChecked;
1701
1726
  exports.isUnmatched = isUnmatched;
1727
+ exports.resolveGroupSelection = resolveGroupSelection;
1702
1728
  exports.suggestOptions = suggestOptions;
1729
+ exports.toggleGroupSelection = toggleGroupSelection;
1703
1730
  exports.useRecentValues = useRecentValues;
1704
1731
  exports.useThemedInput = useThemedInput;
1705
1732
  //# sourceMappingURL=index.js.map