@planningcenter/tapestry 3.0.1 → 3.0.2-qa-693.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.
Files changed (37) hide show
  1. package/dist/components/select/Select.d.ts +66 -0
  2. package/dist/components/select/Select.d.ts.map +1 -0
  3. package/dist/components/select/Select.js +58 -0
  4. package/dist/components/select/Select.js.map +1 -0
  5. package/dist/components/select/SelectNative.d.ts +3 -1
  6. package/dist/components/select/SelectNative.d.ts.map +1 -1
  7. package/dist/components/select/SelectNative.js +34 -0
  8. package/dist/components/select/SelectNative.js.map +1 -0
  9. package/dist/components/select/SelectOptions.d.ts +1 -1
  10. package/dist/components/select/SelectOptions.d.ts.map +1 -1
  11. package/dist/components/select/SelectOptions.js +35 -0
  12. package/dist/components/select/SelectOptions.js.map +1 -0
  13. package/dist/components/select/SelectPopover.d.ts +11 -55
  14. package/dist/components/select/SelectPopover.d.ts.map +1 -1
  15. package/dist/components/select/SelectPopover.js +237 -0
  16. package/dist/components/select/SelectPopover.js.map +1 -0
  17. package/dist/components/select/index.d.ts +2 -1
  18. package/dist/components/select/index.d.ts.map +1 -1
  19. package/dist/index.css +3 -2
  20. package/dist/index.css.map +1 -1
  21. package/dist/reactRender.css +1318 -808
  22. package/dist/reactRender.css.map +1 -1
  23. package/dist/reactRenderLegacy.css +1318 -808
  24. package/dist/reactRenderLegacy.css.map +1 -1
  25. package/dist/unstable.css +512 -2
  26. package/dist/unstable.css.map +1 -1
  27. package/dist/unstable.js +1 -0
  28. package/dist/unstable.js.map +1 -1
  29. package/dist/utilities/keyboardUtils.d.ts +27 -0
  30. package/dist/utilities/keyboardUtils.d.ts.map +1 -0
  31. package/dist/utilities/keyboardUtils.js +101 -0
  32. package/dist/utilities/keyboardUtils.js.map +1 -0
  33. package/dist/utilities/selectUtils.d.ts +3 -1
  34. package/dist/utilities/selectUtils.d.ts.map +1 -1
  35. package/dist/utilities/selectUtils.js +103 -0
  36. package/dist/utilities/selectUtils.js.map +1 -0
  37. package/package.json +5 -5
@@ -0,0 +1,101 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Combobox keyboard utilities
3
+ // ---------------------------------------------------------------------------
4
+ // Reusable keyboard navigation for combobox, listbox, and menu patterns.
5
+ // Based on the W3C APG Select-Only Combobox pattern:
6
+ // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/
7
+ // ---------------------------------------------------------------------------
8
+ const ComboboxAction = {
9
+ ClosePopup: "ClosePopup",
10
+ CommitAndClose: "CommitAndClose",
11
+ MoveFirst: "MoveFirst",
12
+ MoveLast: "MoveLast",
13
+ MoveNext: "MoveNext",
14
+ MovePrevious: "MovePrevious",
15
+ OpenPopup: "OpenPopup",
16
+ TypeCharacter: "TypeCharacter",
17
+ };
18
+ const keyActionsWhenClosed = {
19
+ " ": ComboboxAction.OpenPopup,
20
+ ArrowDown: ComboboxAction.MoveFirst,
21
+ ArrowUp: ComboboxAction.MoveFirst,
22
+ End: ComboboxAction.MoveLast,
23
+ Enter: ComboboxAction.OpenPopup,
24
+ Home: ComboboxAction.MoveFirst,
25
+ };
26
+ const keyActionsWhenOpen = {
27
+ " ": ComboboxAction.CommitAndClose,
28
+ ArrowDown: ComboboxAction.MoveNext,
29
+ ArrowUp: ComboboxAction.MovePrevious,
30
+ End: ComboboxAction.MoveLast,
31
+ Enter: ComboboxAction.CommitAndClose,
32
+ Escape: ComboboxAction.ClosePopup,
33
+ Home: ComboboxAction.MoveFirst,
34
+ };
35
+ /**
36
+ * Returns the combobox action for a keyboard event, based on whether the
37
+ * popup is currently open or closed. Returns `undefined` when the key has
38
+ * no mapped behavior.
39
+ */
40
+ function getComboboxActionFromKey(event, popupIsOpen) {
41
+ const { altKey, ctrlKey, key, metaKey } = event;
42
+ if (key.length === 1 && key !== " " && !altKey && !ctrlKey && !metaKey) {
43
+ return ComboboxAction.TypeCharacter;
44
+ }
45
+ const actionMap = popupIsOpen ? keyActionsWhenOpen : keyActionsWhenClosed;
46
+ return actionMap[key];
47
+ }
48
+ function clamp(value, max) {
49
+ const min = 0;
50
+ if (value < min)
51
+ return min;
52
+ if (value > max)
53
+ return max;
54
+ return value;
55
+ }
56
+ /**
57
+ * Calculates the next active option index given the current index and the
58
+ * navigation action. Clamps the result within bounds.
59
+ * Skips disabled options by searching forward or backward from the target index.
60
+ * Returns -1 if all options are disabled.
61
+ */
62
+ function getUpdatedIndex(currentIndex, action, options) {
63
+ const maxIndex = options.length - 1;
64
+ // Calculate raw target index based on action
65
+ let targetIndex;
66
+ switch (action) {
67
+ case ComboboxAction.MoveFirst:
68
+ targetIndex = 0;
69
+ break;
70
+ case ComboboxAction.MoveLast:
71
+ targetIndex = maxIndex;
72
+ break;
73
+ case ComboboxAction.MoveNext:
74
+ targetIndex = clamp(currentIndex + 1, maxIndex);
75
+ break;
76
+ case ComboboxAction.MovePrevious:
77
+ targetIndex = clamp(currentIndex - 1, maxIndex);
78
+ break;
79
+ default:
80
+ return currentIndex;
81
+ }
82
+ // If target is enabled, return it
83
+ if (!options[targetIndex]?.disabled) {
84
+ return targetIndex;
85
+ }
86
+ // Determine search direction based on action
87
+ const searchForward = action === ComboboxAction.MoveNext || action === ComboboxAction.MoveFirst;
88
+ // Search for next enabled option
89
+ for (let i = 1; i < options.length; i++) {
90
+ const offset = searchForward ? i : -i;
91
+ const candidateIndex = (targetIndex + offset + options.length) % options.length;
92
+ if (!options[candidateIndex]?.disabled) {
93
+ return candidateIndex;
94
+ }
95
+ }
96
+ // All options are disabled
97
+ return -1;
98
+ }
99
+
100
+ export { ComboboxAction, getComboboxActionFromKey, getUpdatedIndex };
101
+ //# sourceMappingURL=keyboardUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboardUtils.js","sources":["../../src/utilities/keyboardUtils.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Combobox keyboard utilities\n// ---------------------------------------------------------------------------\n// Reusable keyboard navigation for combobox, listbox, and menu patterns.\n// Based on the W3C APG Select-Only Combobox pattern:\n// https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/\n// ---------------------------------------------------------------------------\n\nexport const ComboboxAction = {\n ClosePopup: \"ClosePopup\",\n CommitAndClose: \"CommitAndClose\",\n MoveFirst: \"MoveFirst\",\n MoveLast: \"MoveLast\",\n MoveNext: \"MoveNext\",\n MovePrevious: \"MovePrevious\",\n OpenPopup: \"OpenPopup\",\n TypeCharacter: \"TypeCharacter\",\n} as const\n\nexport type ComboboxAction =\n (typeof ComboboxAction)[keyof typeof ComboboxAction]\n\nconst keyActionsWhenClosed: Record<string, ComboboxAction> = {\n \" \": ComboboxAction.OpenPopup,\n ArrowDown: ComboboxAction.MoveFirst,\n ArrowUp: ComboboxAction.MoveFirst,\n End: ComboboxAction.MoveLast,\n Enter: ComboboxAction.OpenPopup,\n Home: ComboboxAction.MoveFirst,\n}\n\nconst keyActionsWhenOpen: Record<string, ComboboxAction> = {\n \" \": ComboboxAction.CommitAndClose,\n ArrowDown: ComboboxAction.MoveNext,\n ArrowUp: ComboboxAction.MovePrevious,\n End: ComboboxAction.MoveLast,\n Enter: ComboboxAction.CommitAndClose,\n Escape: ComboboxAction.ClosePopup,\n Home: ComboboxAction.MoveFirst,\n}\n\n/**\n * Returns the combobox action for a keyboard event, based on whether the\n * popup is currently open or closed. Returns `undefined` when the key has\n * no mapped behavior.\n */\nexport function getComboboxActionFromKey(\n event: Pick<KeyboardEvent, \"altKey\" | \"ctrlKey\" | \"key\" | \"metaKey\">,\n popupIsOpen: boolean\n): ComboboxAction | undefined {\n const { altKey, ctrlKey, key, metaKey } = event\n\n if (key.length === 1 && key !== \" \" && !altKey && !ctrlKey && !metaKey) {\n return ComboboxAction.TypeCharacter\n }\n\n const actionMap = popupIsOpen ? keyActionsWhenOpen : keyActionsWhenClosed\n return actionMap[key]\n}\n\nfunction clamp(value: number, max: number): number {\n const min = 0\n if (value < min) return min\n if (value > max) return max\n return value\n}\n\n/**\n * Calculates the next active option index given the current index and the\n * navigation action. Clamps the result within bounds.\n * Skips disabled options by searching forward or backward from the target index.\n * Returns -1 if all options are disabled.\n */\nexport function getUpdatedIndex(\n currentIndex: number,\n action: ComboboxAction,\n options: Array<{ disabled?: boolean }>\n): number {\n const maxIndex = options.length - 1\n\n // Calculate raw target index based on action\n let targetIndex: number\n switch (action) {\n case ComboboxAction.MoveFirst:\n targetIndex = 0\n break\n case ComboboxAction.MoveLast:\n targetIndex = maxIndex\n break\n case ComboboxAction.MoveNext:\n targetIndex = clamp(currentIndex + 1, maxIndex)\n break\n case ComboboxAction.MovePrevious:\n targetIndex = clamp(currentIndex - 1, maxIndex)\n break\n default:\n return currentIndex\n }\n\n // If target is enabled, return it\n if (!options[targetIndex]?.disabled) {\n return targetIndex\n }\n\n // Determine search direction based on action\n const searchForward =\n action === ComboboxAction.MoveNext || action === ComboboxAction.MoveFirst\n\n // Search for next enabled option\n for (let i = 1; i < options.length; i++) {\n const offset = searchForward ? i : -i\n const candidateIndex =\n (targetIndex + offset + options.length) % options.length\n\n if (!options[candidateIndex]?.disabled) {\n return candidateIndex\n }\n }\n\n // All options are disabled\n return -1\n}\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AAEO,MAAM,cAAc,GAAG;AAC5B,IAAA,UAAU,EAAE,YAAY;AACxB,IAAA,cAAc,EAAE,gBAAgB;AAChC,IAAA,SAAS,EAAE,WAAW;AACtB,IAAA,QAAQ,EAAE,UAAU;AACpB,IAAA,QAAQ,EAAE,UAAU;AACpB,IAAA,YAAY,EAAE,cAAc;AAC5B,IAAA,SAAS,EAAE,WAAW;AACtB,IAAA,aAAa,EAAE,eAAe;;AAMhC,MAAM,oBAAoB,GAAmC;IAC3D,GAAG,EAAE,cAAc,CAAC,SAAS;IAC7B,SAAS,EAAE,cAAc,CAAC,SAAS;IACnC,OAAO,EAAE,cAAc,CAAC,SAAS;IACjC,GAAG,EAAE,cAAc,CAAC,QAAQ;IAC5B,KAAK,EAAE,cAAc,CAAC,SAAS;IAC/B,IAAI,EAAE,cAAc,CAAC,SAAS;CAC/B;AAED,MAAM,kBAAkB,GAAmC;IACzD,GAAG,EAAE,cAAc,CAAC,cAAc;IAClC,SAAS,EAAE,cAAc,CAAC,QAAQ;IAClC,OAAO,EAAE,cAAc,CAAC,YAAY;IACpC,GAAG,EAAE,cAAc,CAAC,QAAQ;IAC5B,KAAK,EAAE,cAAc,CAAC,cAAc;IACpC,MAAM,EAAE,cAAc,CAAC,UAAU;IACjC,IAAI,EAAE,cAAc,CAAC,SAAS;CAC/B;AAED;;;;AAIG;AACG,SAAU,wBAAwB,CACtC,KAAoE,EACpE,WAAoB,EAAA;IAEpB,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,KAAK;AAE/C,IAAA,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,KAAK,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE;QACtE,OAAO,cAAc,CAAC,aAAa;IACrC;IAEA,MAAM,SAAS,GAAG,WAAW,GAAG,kBAAkB,GAAG,oBAAoB;AACzE,IAAA,OAAO,SAAS,CAAC,GAAG,CAAC;AACvB;AAEA,SAAS,KAAK,CAAC,KAAa,EAAE,GAAW,EAAA;IACvC,MAAM,GAAG,GAAG,CAAC;IACb,IAAI,KAAK,GAAG,GAAG;AAAE,QAAA,OAAO,GAAG;IAC3B,IAAI,KAAK,GAAG,GAAG;AAAE,QAAA,OAAO,GAAG;AAC3B,IAAA,OAAO,KAAK;AACd;AAEA;;;;;AAKG;SACa,eAAe,CAC7B,YAAoB,EACpB,MAAsB,EACtB,OAAsC,EAAA;AAEtC,IAAA,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC;;AAGnC,IAAA,IAAI,WAAmB;IACvB,QAAQ,MAAM;QACZ,KAAK,cAAc,CAAC,SAAS;YAC3B,WAAW,GAAG,CAAC;YACf;QACF,KAAK,cAAc,CAAC,QAAQ;YAC1B,WAAW,GAAG,QAAQ;YACtB;QACF,KAAK,cAAc,CAAC,QAAQ;YAC1B,WAAW,GAAG,KAAK,CAAC,YAAY,GAAG,CAAC,EAAE,QAAQ,CAAC;YAC/C;QACF,KAAK,cAAc,CAAC,YAAY;YAC9B,WAAW,GAAG,KAAK,CAAC,YAAY,GAAG,CAAC,EAAE,QAAQ,CAAC;YAC/C;AACF,QAAA;AACE,YAAA,OAAO,YAAY;;;IAIvB,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,QAAQ,EAAE;AACnC,QAAA,OAAO,WAAW;IACpB;;AAGA,IAAA,MAAM,aAAa,GACjB,MAAM,KAAK,cAAc,CAAC,QAAQ,IAAI,MAAM,KAAK,cAAc,CAAC,SAAS;;AAG3E,IAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AACvC,QAAA,MAAM,MAAM,GAAG,aAAa,GAAG,CAAC,GAAG,CAAC,CAAC;AACrC,QAAA,MAAM,cAAc,GAClB,CAAC,WAAW,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM;QAE1D,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,QAAQ,EAAE;AACtC,YAAA,OAAO,cAAc;QACvB;IACF;;IAGA,OAAO,EAAE;AACX;;;;"}
@@ -1,4 +1,4 @@
1
- import type { SelectItem, SelectOption, SelectOptionsGroup } from "../components/select/SelectPopover";
1
+ import type { SelectItem, SelectOption, SelectOptionsGroup, SelectOptionWithNodeLabel, SelectOptionWithTextLabel } from "../components/select/Select";
2
2
  export type OptionsSegment = {
3
3
  options: SelectOption[];
4
4
  type: "options";
@@ -12,4 +12,6 @@ export type GroupSegment = {
12
12
  export type Segment = GroupSegment | OptionsSegment;
13
13
  export declare function isGroup(item: SelectItem): item is SelectOptionsGroup;
14
14
  export declare function normalizeOptions(items: SelectItem[]): Segment[];
15
+ export declare function getSelectableOptionsFromSegments(segments: Segment[]): Array<SelectOptionWithNodeLabel | SelectOptionWithTextLabel>;
16
+ export declare function getIndexByLetter(options: SelectOption[], searchString: string, startIndex?: number): number;
15
17
  //# sourceMappingURL=selectUtils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"selectUtils.d.ts","sourceRoot":"","sources":["../../src/utilities/selectUtils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,kBAAkB,EACnB,MAAM,kCAAkC,CAAA;AAKzC,MAAM,MAAM,cAAc,GAAG;IAAE,OAAO,EAAE,YAAY,EAAE,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAA;AACzE,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,IAAI,EAAE,OAAO,CAAA;CACd,CAAA;AACD,MAAM,MAAM,OAAO,GAAG,YAAY,GAAG,cAAc,CAAA;AAKnD,wBAAgB,OAAO,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,IAAI,kBAAkB,CAEpE;AASD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,OAAO,EAAE,CAyB/D"}
1
+ {"version":3,"file":"selectUtils.d.ts","sourceRoot":"","sources":["../../src/utilities/selectUtils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,kBAAkB,EAClB,yBAAyB,EACzB,yBAAyB,EAC1B,MAAM,2BAA2B,CAAA;AAKlC,MAAM,MAAM,cAAc,GAAG;IAAE,OAAO,EAAE,YAAY,EAAE,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAA;AACzE,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,IAAI,EAAE,OAAO,CAAA;CACd,CAAA;AACD,MAAM,MAAM,OAAO,GAAG,YAAY,GAAG,cAAc,CAAA;AAKnD,wBAAgB,OAAO,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,IAAI,kBAAkB,CAEpE;AASD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,OAAO,EAAE,CAyB/D;AAED,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,OAAO,EAAE,GAClB,KAAK,CAAC,yBAAyB,GAAG,yBAAyB,CAAC,CAS9D;AAYD,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,YAAY,EAAE,EACvB,YAAY,EAAE,MAAM,EACpB,UAAU,GAAE,MAAU,GACrB,MAAM,CAiDR"}
@@ -0,0 +1,103 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Helpers
3
+ // ---------------------------------------------------------------------------
4
+ function isGroup(item) {
5
+ return "options" in item;
6
+ }
7
+ // ---------------------------------------------------------------------------
8
+ // normalizeOptions
9
+ // ---------------------------------------------------------------------------
10
+ // Transforms a mixed SelectItem[] into a flat list of segments.
11
+ // Consecutive ungrouped options are collected into a single "options" segment.
12
+ // Groups become their own segment with disabled inherited to child options.
13
+ // ---------------------------------------------------------------------------
14
+ function normalizeOptions(items) {
15
+ const segments = [];
16
+ for (const item of items) {
17
+ if (isGroup(item)) {
18
+ const options = item.options.map((opt) => item.disabled && !opt.divider ? { ...opt, disabled: true } : opt);
19
+ segments.push({
20
+ disabled: item.disabled,
21
+ label: item.label,
22
+ options,
23
+ type: "group",
24
+ });
25
+ }
26
+ else {
27
+ const lastSegment = segments[segments.length - 1];
28
+ if (lastSegment?.type === "options") {
29
+ lastSegment.options.push(item);
30
+ }
31
+ else {
32
+ segments.push({ options: [item], type: "options" });
33
+ }
34
+ }
35
+ }
36
+ return segments;
37
+ }
38
+ function getSelectableOptionsFromSegments(segments) {
39
+ return segments
40
+ .flatMap((segment) => segment.options)
41
+ .filter((option) => !option.divider);
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // getIndexByLetter
45
+ // ---------------------------------------------------------------------------
46
+ // Type-ahead search for combobox options. Finds the index of the first option
47
+ // whose text starts with the search string, starting from `startIndex` and
48
+ // wrapping around. If all characters in the search string are identical, cycles
49
+ // through options starting with that character. Skips disabled options.
50
+ //
51
+ // Returns `-1` if no match is found.
52
+ // ---------------------------------------------------------------------------
53
+ function getIndexByLetter(options, searchString, startIndex = 0) {
54
+ // Detect repeated same-character input, e.g. "aa" or "bbb".
55
+ // This happens when the user presses the same key multiple times quickly,
56
+ // meaning they want to cycle through options starting with that letter
57
+ // rather than search for a literal "aa" string.
58
+ const allSameChar = searchString.length > 1 &&
59
+ searchString.split("").every((ch) => ch === searchString[0]);
60
+ if (allSameChar) {
61
+ // Cycle through matches starting AFTER the current option so each
62
+ // repeated keypress advances to the next match instead of staying put.
63
+ //
64
+ // Example: Given options with labels Apple, Apricot, and Banana, startIndex = 0 (Apple)
65
+ // cycleStart = 1
66
+ // wrappedOptions contains Apricot, Banana, Apple (starting from index 1)
67
+ // Finds Apricot at wrappedOptions[0] → original index = (0 + 1) % 3 = 1
68
+ const cycleStart = (startIndex + 1) % options.length;
69
+ const wrappedOptions = [
70
+ ...options.slice(cycleStart),
71
+ ...options.slice(0, cycleStart),
72
+ ];
73
+ const lowerChar = searchString[0].toLowerCase();
74
+ const matchIndex = wrappedOptions.findIndex((option) => {
75
+ if (option.disabled)
76
+ return false;
77
+ const text = option.textValue ?? option.label;
78
+ return text.toLowerCase().startsWith(lowerChar);
79
+ });
80
+ // Convert the index in the rotated array back to the original array index.
81
+ //
82
+ // Example: matchIndex = 0, cycleStart = 1
83
+ // (0 + 1) % 3 = 1 → returns index 1 (Apricot)
84
+ return matchIndex >= 0 ? (matchIndex + cycleStart) % options.length : -1;
85
+ }
86
+ // For single characters or multi-character strings (e.g. "ap", "apr"),
87
+ // always search from the beginning of the list and return the first match.
88
+ //
89
+ // Example: Given options with labels Apple, Apricot, and Banana, searchString = "apr"
90
+ // "Apple".startsWith("apr") → false
91
+ // "Apricot".startsWith("apr") → true → returns index 1
92
+ const lowerSearch = searchString.toLowerCase();
93
+ const matchIndex = options.findIndex((option) => {
94
+ if (option.disabled)
95
+ return false;
96
+ const text = option.textValue ?? option.label;
97
+ return text.toLowerCase().startsWith(lowerSearch);
98
+ });
99
+ return matchIndex >= 0 ? matchIndex : -1;
100
+ }
101
+
102
+ export { getIndexByLetter, getSelectableOptionsFromSegments, isGroup, normalizeOptions };
103
+ //# sourceMappingURL=selectUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selectUtils.js","sources":["../../src/utilities/selectUtils.ts"],"sourcesContent":["import type {\n SelectItem,\n SelectOption,\n SelectOptionsGroup,\n SelectOptionWithNodeLabel,\n SelectOptionWithTextLabel,\n} from \"@components/select/Select\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\nexport type OptionsSegment = { options: SelectOption[]; type: \"options\" }\nexport type GroupSegment = {\n disabled?: boolean\n label: string\n options: SelectOption[]\n type: \"group\"\n}\nexport type Segment = GroupSegment | OptionsSegment\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\nexport function isGroup(item: SelectItem): item is SelectOptionsGroup {\n return \"options\" in item\n}\n\n// ---------------------------------------------------------------------------\n// normalizeOptions\n// ---------------------------------------------------------------------------\n// Transforms a mixed SelectItem[] into a flat list of segments.\n// Consecutive ungrouped options are collected into a single \"options\" segment.\n// Groups become their own segment with disabled inherited to child options.\n// ---------------------------------------------------------------------------\nexport function normalizeOptions(items: SelectItem[]): Segment[] {\n const segments: Segment[] = []\n\n for (const item of items) {\n if (isGroup(item)) {\n const options = item.options.map((opt) =>\n item.disabled && !opt.divider ? { ...opt, disabled: true } : opt\n )\n segments.push({\n disabled: item.disabled,\n label: item.label,\n options,\n type: \"group\",\n })\n } else {\n const lastSegment = segments[segments.length - 1]\n if (lastSegment?.type === \"options\") {\n lastSegment.options.push(item)\n } else {\n segments.push({ options: [item], type: \"options\" })\n }\n }\n }\n\n return segments\n}\n\nexport function getSelectableOptionsFromSegments(\n segments: Segment[]\n): Array<SelectOptionWithNodeLabel | SelectOptionWithTextLabel> {\n return segments\n .flatMap((segment) => segment.options)\n .filter(\n (\n option\n ): option is SelectOptionWithNodeLabel | SelectOptionWithTextLabel =>\n !option.divider\n )\n}\n\n// ---------------------------------------------------------------------------\n// getIndexByLetter\n// ---------------------------------------------------------------------------\n// Type-ahead search for combobox options. Finds the index of the first option\n// whose text starts with the search string, starting from `startIndex` and\n// wrapping around. If all characters in the search string are identical, cycles\n// through options starting with that character. Skips disabled options.\n//\n// Returns `-1` if no match is found.\n// ---------------------------------------------------------------------------\nexport function getIndexByLetter(\n options: SelectOption[],\n searchString: string,\n startIndex: number = 0\n): number {\n // Detect repeated same-character input, e.g. \"aa\" or \"bbb\".\n // This happens when the user presses the same key multiple times quickly,\n // meaning they want to cycle through options starting with that letter\n // rather than search for a literal \"aa\" string.\n const allSameChar =\n searchString.length > 1 &&\n searchString.split(\"\").every((ch) => ch === searchString[0])\n\n if (allSameChar) {\n // Cycle through matches starting AFTER the current option so each\n // repeated keypress advances to the next match instead of staying put.\n //\n // Example: Given options with labels Apple, Apricot, and Banana, startIndex = 0 (Apple)\n // cycleStart = 1\n // wrappedOptions contains Apricot, Banana, Apple (starting from index 1)\n // Finds Apricot at wrappedOptions[0] → original index = (0 + 1) % 3 = 1\n const cycleStart = (startIndex + 1) % options.length\n const wrappedOptions = [\n ...options.slice(cycleStart),\n ...options.slice(0, cycleStart),\n ]\n const lowerChar = searchString[0].toLowerCase()\n const matchIndex = wrappedOptions.findIndex((option) => {\n if (option.disabled) return false\n const text = option.textValue ?? (option.label as string)\n return text.toLowerCase().startsWith(lowerChar)\n })\n\n // Convert the index in the rotated array back to the original array index.\n //\n // Example: matchIndex = 0, cycleStart = 1\n // (0 + 1) % 3 = 1 → returns index 1 (Apricot)\n return matchIndex >= 0 ? (matchIndex + cycleStart) % options.length : -1\n }\n\n // For single characters or multi-character strings (e.g. \"ap\", \"apr\"),\n // always search from the beginning of the list and return the first match.\n //\n // Example: Given options with labels Apple, Apricot, and Banana, searchString = \"apr\"\n // \"Apple\".startsWith(\"apr\") → false\n // \"Apricot\".startsWith(\"apr\") → true → returns index 1\n const lowerSearch = searchString.toLowerCase()\n const matchIndex = options.findIndex((option) => {\n if (option.disabled) return false\n const text = option.textValue ?? (option.label as string)\n return text.toLowerCase().startsWith(lowerSearch)\n })\n return matchIndex >= 0 ? matchIndex : -1\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry",
3
- "version": "3.0.1",
3
+ "version": "3.0.2-qa-693.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "types": "./dist/index.d.ts",
53
53
  "devDependencies": {
54
- "@planningcenter/tapestry-render": "^3.0.1",
54
+ "@planningcenter/tapestry-render": "^3.0.2-qa-693.0",
55
55
  "@rollup/plugin-commonjs": "^26.0.1",
56
56
  "@rollup/plugin-node-resolve": "^16.0.3",
57
57
  "@rollup/plugin-typescript": "^12.3.0",
@@ -101,7 +101,7 @@
101
101
  "shadow-dom-testing-library": "^1.11.3",
102
102
  "storybook": "9.1.19",
103
103
  "style-dictionary": "^4.2.0",
104
- "tapestry-wc": "^3.0.1",
104
+ "tapestry-wc": "^3.0.2-qa-693.0",
105
105
  "tsc-alias": "^1.8.16",
106
106
  "typescript": "^5.5.3",
107
107
  "vite": "^6.4.1",
@@ -109,9 +109,9 @@
109
109
  },
110
110
  "dependencies": {
111
111
  "@planningcenter/icons": "^15.29.0",
112
- "@planningcenter/tapestry-tokens": "^3.0.1",
112
+ "@planningcenter/tapestry-tokens": "^3.0.2-qa-693.0",
113
113
  "classnames": "^2.5.1",
114
114
  "lodash": "^4.17.21"
115
115
  },
116
- "gitHead": "9334b80b11b57351d0794f9df7eaf7ed8cac53d1"
116
+ "gitHead": "e7140aa023a9958b895e9d5894bf2afb4def1e46"
117
117
  }