@k2b/ui 0.35.0 → 0.37.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 (87) hide show
  1. package/dist/browser/actions/Dropdown.js +132 -116
  2. package/dist/browser/actions/Dropdown.js.map +1 -1
  3. package/dist/browser/actions/Tabs.js +18 -15
  4. package/dist/browser/actions/Tabs.js.map +1 -1
  5. package/dist/browser/chat/ChatComposer.js +58 -25
  6. package/dist/browser/chat/ChatComposer.js.map +1 -1
  7. package/dist/browser/content/MapView.js +128 -13
  8. package/dist/browser/content/MapView.js.map +1 -1
  9. package/dist/browser/content/MapViewFrame.js +1 -1
  10. package/dist/browser/content/MapViewFrame.js.map +1 -1
  11. package/dist/browser/content/MessageRow.js +164 -152
  12. package/dist/browser/content/MessageRow.js.map +1 -1
  13. package/dist/browser/content/index.js +1 -0
  14. package/dist/browser/content/index.js.map +1 -1
  15. package/dist/browser/content/map-view-glyphs.js +23 -0
  16. package/dist/browser/content/map-view-glyphs.js.map +1 -0
  17. package/dist/browser/content/map-view-live.js +45 -22
  18. package/dist/browser/content/map-view-live.js.map +1 -1
  19. package/dist/browser/content/map-view-snapshot.js +191 -0
  20. package/dist/browser/content/map-view-snapshot.js.map +1 -0
  21. package/dist/browser/content/map-view-types.js.map +1 -1
  22. package/dist/browser/feedback/prompts.js +23 -9
  23. package/dist/browser/feedback/prompts.js.map +1 -1
  24. package/dist/browser/inputs/Combobox.js +164 -82
  25. package/dist/browser/inputs/Combobox.js.map +1 -1
  26. package/dist/browser/inputs/RichSelect.js +86 -67
  27. package/dist/browser/inputs/RichSelect.js.map +1 -1
  28. package/dist/browser/inputs/TextInput.js +1 -1
  29. package/dist/browser/inputs/TextInput.js.map +1 -1
  30. package/dist/browser/internal/keyed-for.js +47 -0
  31. package/dist/browser/internal/keyed-for.js.map +1 -0
  32. package/dist/browser/layout/AppWorkspace.js +73 -70
  33. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  34. package/dist/browser/layout/ScrollArea.js +4 -1
  35. package/dist/browser/layout/ScrollArea.js.map +1 -1
  36. package/dist/browser/text-editor/engine.js +13 -10
  37. package/dist/browser/text-editor/engine.js.map +1 -1
  38. package/dist/ssr/actions/Dropdown.js +101 -85
  39. package/dist/ssr/actions/Dropdown.js.map +1 -1
  40. package/dist/ssr/actions/Tabs.js +12 -9
  41. package/dist/ssr/actions/Tabs.js.map +1 -1
  42. package/dist/ssr/chat/ChatComposer.js +41 -15
  43. package/dist/ssr/chat/ChatComposer.js.map +1 -1
  44. package/dist/ssr/content/MapView.js +128 -13
  45. package/dist/ssr/content/MapView.js.map +1 -1
  46. package/dist/ssr/content/MapViewFrame.js +1 -1
  47. package/dist/ssr/content/MapViewFrame.js.map +1 -1
  48. package/dist/ssr/content/MessageRow.js +9 -2
  49. package/dist/ssr/content/MessageRow.js.map +1 -1
  50. package/dist/ssr/content/index.js +1 -0
  51. package/dist/ssr/content/index.js.map +1 -1
  52. package/dist/ssr/content/map-view-glyphs.js +23 -0
  53. package/dist/ssr/content/map-view-glyphs.js.map +1 -0
  54. package/dist/ssr/content/map-view-snapshot.js +191 -0
  55. package/dist/ssr/content/map-view-snapshot.js.map +1 -0
  56. package/dist/ssr/content/map-view-types.js.map +1 -1
  57. package/dist/ssr/feedback/prompts.js +22 -8
  58. package/dist/ssr/feedback/prompts.js.map +1 -1
  59. package/dist/ssr/inputs/Combobox.js +79 -28
  60. package/dist/ssr/inputs/Combobox.js.map +1 -1
  61. package/dist/ssr/inputs/RichSelect.js +47 -28
  62. package/dist/ssr/inputs/RichSelect.js.map +1 -1
  63. package/dist/ssr/inputs/TextInput.js.map +1 -1
  64. package/dist/ssr/internal/keyed-for.js +47 -0
  65. package/dist/ssr/internal/keyed-for.js.map +1 -0
  66. package/dist/ssr/layout/AppWorkspace.js +1 -1
  67. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  68. package/dist/ssr/layout/ScrollArea.js +4 -1
  69. package/dist/ssr/layout/ScrollArea.js.map +1 -1
  70. package/dist/ssr/text-editor/engine.js +13 -10
  71. package/dist/ssr/text-editor/engine.js.map +1 -1
  72. package/dist/styles.css +1 -1
  73. package/dist/types/content/MapViewFrame.d.ts +1 -1
  74. package/dist/types/content/MessageRow.d.ts +1 -1
  75. package/dist/types/content/index.d.ts +2 -1
  76. package/dist/types/content/map-view-glyphs.d.ts +5 -0
  77. package/dist/types/content/map-view-live.d.ts +1 -1
  78. package/dist/types/content/map-view-snapshot.d.ts +13 -0
  79. package/dist/types/content/map-view-types.d.ts +15 -0
  80. package/dist/types/feedback/prompts.d.ts +5 -0
  81. package/dist/types/inputs/Combobox.d.ts +12 -0
  82. package/dist/types/inputs/TextInput.d.ts +2 -1
  83. package/dist/types/internal/keyed-for.d.ts +13 -0
  84. package/dist/types/layout/AppWorkspace.d.ts +4 -1
  85. package/dist/types/layout/ScrollArea.d.ts +2 -0
  86. package/dist/types/text-editor/engine.d.ts +1 -1
  87. package/package.json +4 -2
@@ -1,3 +1,4 @@
1
+ import { ssrStyleProperty as _$ssrStyleProperty } from "solid-js/web";
1
2
  import { memo as _$memo } from "solid-js/web";
2
3
  import { createComponent as _$createComponent } from "solid-js/web";
3
4
  import { ssr as _$ssr } from "solid-js/web";
@@ -6,14 +7,17 @@ import { mergeProps as _$mergeProps } from "solid-js/web";
6
7
  import { ssrAttribute as _$ssrAttribute } from "solid-js/web";
7
8
  import { escape as _$escape } from "solid-js/web";
8
9
  var _tmpl$ = "<i class=\"ti ti-loader-2 k2b-spin\" aria-hidden=\"true\"></i>",
9
- _tmpl$2 = ["<div class=\"k2b-combobox\"", "><div class=\"k2b-combobox__input\"><i class=\"ti ti-search\" aria-hidden=\"true\"></i>", "", "</div><div popover=\"manual\" class=\"k2b-choice-popover\"><div class=\"k2b-choice-options\" role=\"listbox\"", ">", "", "</div></div></div>"],
10
+ _tmpl$2 = ["<div class=\"k2b-combobox\"", "><div class=\"k2b-combobox__input\"><i class=\"ti ti-search\" aria-hidden=\"true\"></i>", "", "</div><div popover=\"manual\" class=\"k2b-choice-popover\"><div class=\"k2b-choice-options\" role=\"listbox\"", ">", "", "", "</div>", "</div></div>"],
10
11
  _tmpl$3 = "<i class=\"ti ti-chevron-down\" aria-hidden=\"true\"></i>",
11
- _tmpl$4 = ["<div class=\"k2b-choice-status\" data-tone=\"danger\"><span>", "</span><button type=\"button\">", "</button></div>"],
12
- _tmpl$5 = ["<div class=\"k2b-choice-status\">", "</div>"],
13
- _tmpl$6 = ["<button type=\"button\" id=\"", "\" class=\"k2b-choice-option\" role=\"option\"", ">", "<span><strong>", "</strong>", "</span></button>"],
14
- _tmpl$7 = ["<i", " aria-hidden=\"true\"></i>"],
15
- _tmpl$8 = ["<small>", "</small>"];
16
- import { createEffect, createSignal, For, Show } from "solid-js";
12
+ _tmpl$4 = ["<div class=\"k2b-choice-status\">", "</div>"],
13
+ _tmpl$5 = ["<div class=\"k2b-choice-status\" data-tone=\"danger\"><span>", "</span><button type=\"button\">", "</button></div>"],
14
+ _tmpl$6 = ["<div class=\"k2b-choice-loading\" role=\"status\"><span class=\"k2b-sr-only\">", "</span>", "</div>"],
15
+ _tmpl$7 = ["<div class=\"k2b-choice-loading__row\" aria-hidden=\"true\"><span class=\"k2b-skeleton\"></span><span class=\"k2b-skeleton\" style=\"", "\"></span></div>"],
16
+ _tmpl$8 = ["<button type=\"button\" id=\"", "\" class=\"k2b-choice-option\" role=\"option\"", ">", "<span><strong>", "</strong>", "</span></button>"],
17
+ _tmpl$9 = ["<i", " aria-hidden=\"true\"></i>"],
18
+ _tmpl$0 = ["<small>", "</small>"],
19
+ _tmpl$1 = ["<div class=\"k2b-choice-footer\">", "</div>"];
20
+ import { createEffect, createMemo, createSignal, For, on, Show } from "solid-js";
17
21
  import { createFieldMeta, Field, fieldControlAria } from "../internal/field";
18
22
  import { useUiMessages } from "../intl/messages";
19
23
  import { createChoiceLoader, createChoicePopover, nextEnabledChoiceIndex } from "./choice";
@@ -45,8 +49,11 @@ export function Combobox(props) {
45
49
  // 150ms keeps typing responsive without hammering the loader.
46
50
  () => props.debounceMs ?? 150);
47
51
  const popover = createChoicePopover(() => Boolean(props.disabled || props.readOnly));
48
- const options = loader.options;
52
+ // A host notice or an error replaces the results; hidden results are never selectable.
53
+ const options = () => props.status || loader.error() ? [] : loader.options();
54
+ const optionsById = createMemo(() => new Map(options().map(option => [option.value, option])));
49
55
  const focusedOption = () => options()[focusedIndex()];
56
+ const searchable = value => value.trim().length >= (props.minQueryLength ?? 0);
50
57
  const focus = index => {
51
58
  setFocusedIndex(index);
52
59
  optionRefs[index]?.scrollIntoView({
@@ -56,9 +63,9 @@ export function Combobox(props) {
56
63
  /** `eager` loads the currently controlled query on open; typing supplies its
57
64
  * own query, so opening from `onInput` skips the duplicate request. */
58
65
  const open = (eager = true) => {
59
- if (props.disabled || props.readOnly || popover.open()) return;
66
+ if (props.disabled || props.readOnly || popover.open() || !searchable(query())) return;
60
67
  popover.show();
61
- if (eager) loader.load(query(), true);
68
+ if (eager && !props.status) loader.load(query(), true);
62
69
  focus(-1);
63
70
  };
64
71
  const reset = () => {
@@ -68,8 +75,14 @@ export function Combobox(props) {
68
75
  };
69
76
  const close = () => {
70
77
  popover.hide();
71
- reset();
78
+ if (!props.freeText) return reset();
79
+ loader.cancel();
80
+ focus(-1);
72
81
  };
82
+ createEffect(on(options, list => {
83
+ const index = props.autoHighlight;
84
+ if (index !== undefined && focusedIndex() < 0 && popover.open() && list.length > 0) setFocusedIndex(Math.min(index, list.length - 1));
85
+ }));
73
86
  createEffect(() => {
74
87
  if (props.readOnly) {
75
88
  popover.hide();
@@ -86,7 +99,10 @@ export function Combobox(props) {
86
99
  description: option.description,
87
100
  icon: option.icon
88
101
  });
89
- reset();
102
+ if (props.freeText) {
103
+ loader.cancel();
104
+ focus(-1);
105
+ } else reset();
90
106
  inputRef?.focus();
91
107
  };
92
108
  const onKeyDown = event => {
@@ -98,7 +114,7 @@ export function Combobox(props) {
98
114
  // While the results are open, Enter belongs to them: it never submits an enclosing form, even with no option active.
99
115
  event.preventDefault();
100
116
  const option = options()[focusedIndex()];
101
- if (option) select(option);
117
+ if (option) select(option);else if (props.freeText) close();
102
118
  } else if ((event.key === "Escape" || event.key === "Tab") && popover.open()) {
103
119
  // Escape is only ours while the list is open; otherwise it has to reach an
104
120
  // enclosing dialog or drawer.
@@ -148,6 +164,7 @@ export function Combobox(props) {
148
164
  return props.readOnly ? "true" : undefined;
149
165
  },
150
166
  role: "combobox",
167
+ autocomplete: "off",
151
168
  "aria-autocomplete": "list",
152
169
  get ["aria-expanded"]() {
153
170
  return popover.open();
@@ -168,27 +185,61 @@ export function Combobox(props) {
168
185
  }
169
186
  })), _$ssrAttribute("id", _$escape(listboxId, true), false), _$escape(_$createComponent(Show, {
170
187
  get when() {
171
- return loader.error();
188
+ return props.status;
189
+ },
190
+ children: status => _$ssr(_tmpl$4, _$escape(status()))
191
+ })), _$escape(_$createComponent(Show, {
192
+ get when() {
193
+ return _$memo(() => !!!props.status)() && loader.error();
172
194
  },
173
- children: message => _$ssr(_tmpl$4, _$escape(message()), _$escape(messages().retry))
195
+ children: message => _$ssr(_tmpl$5, _$escape(message()), _$escape(messages().retry))
174
196
  })), _$escape(_$createComponent(For, {
175
197
  get each() {
176
- return _$memo(() => !!loader.error())() ? [] : options();
198
+ return options().map(option => option.value);
177
199
  },
178
200
  get fallback() {
179
- return _$ssr(_tmpl$5, query().length >= 2 ? _$escape(selectMessages().noResultsFound) : _$escape(selectMessages().typeToSearch));
201
+ return _$createComponent(Show, {
202
+ get when() {
203
+ return _$memo(() => !!!props.status)() && !loader.error();
204
+ },
205
+ get children() {
206
+ return _$createComponent(Show, {
207
+ get when() {
208
+ return loader.loading();
209
+ },
210
+ get fallback() {
211
+ return _$ssr(_tmpl$4, query().length >= 2 ? _$escape(props.empty ?? selectMessages().noResultsFound) : _$escape(selectMessages().typeToSearch));
212
+ },
213
+ get children() {
214
+ return _$ssr(_tmpl$6, _$escape(messages().loading), _$escape(_$createComponent(For, {
215
+ each: [62, 48, 55],
216
+ children: width => _$ssr(_tmpl$7, _$ssrStyleProperty("width:", `${_$escape(width, true)}%`))
217
+ })));
218
+ }
219
+ });
220
+ }
221
+ });
222
+ },
223
+ children: (id, index) => {
224
+ const first = optionsById().get(id);
225
+ const option = () => optionsById().get(id) ?? first;
226
+ return _$ssr(_tmpl$8, `${_$escape(listboxId, true)}-${_$escape(index(), true)}`, _$ssrAttribute("aria-selected", _$escape(index(), true) === _$escape(focusedIndex(), true), false) + _$ssrAttribute("data-focused", index() === focusedIndex() ? "true" : _$escape(undefined, true), false), _$escape(_$createComponent(Show, {
227
+ get when() {
228
+ return option().icon;
229
+ },
230
+ children: icon => _$ssr(_tmpl$9, _$ssrAttribute("class", _$escape(iconClass(icon()), true), false))
231
+ })), _$escape(option().label), _$escape(_$createComponent(Show, {
232
+ get when() {
233
+ return option().description;
234
+ },
235
+ children: description => _$ssr(_tmpl$0, _$escape(description()))
236
+ })));
237
+ }
238
+ })), _$escape(_$createComponent(Show, {
239
+ get when() {
240
+ return props.footer;
180
241
  },
181
- children: (option, index) => _$ssr(_tmpl$6, `${_$escape(listboxId, true)}-${_$escape(index(), true)}`, _$ssrAttribute("aria-selected", _$escape(index(), true) === _$escape(focusedIndex(), true), false) + _$ssrAttribute("data-focused", index() === focusedIndex() ? "true" : _$escape(undefined, true), false), _$escape(_$createComponent(Show, {
182
- get when() {
183
- return option.icon;
184
- },
185
- children: icon => _$ssr(_tmpl$7, _$ssrAttribute("class", _$escape(iconClass(icon()), true), false))
186
- })), _$escape(option.label), _$escape(_$createComponent(Show, {
187
- get when() {
188
- return option.description;
189
- },
190
- children: description => _$ssr(_tmpl$8, _$escape(description()))
191
- })))
242
+ children: footer => _$ssr(_tmpl$1, _$escape(footer()))
192
243
  })));
193
244
  }
194
245
  });
@@ -1 +1 @@
1
- {"version":3,"names":[],"sources":["inputs/Combobox.tsx"],"sourcesContent":["import { createEffect, createSignal, For, type JSX, Show } from \"solid-js\";\nimport { createFieldMeta, Field, fieldControlAria } from \"../internal/field\";\nimport { useUiMessages } from \"../intl/messages\";\nimport { type ChoiceOption, createChoiceLoader, createChoicePopover, nextEnabledChoiceIndex } from \"./choice\";\nimport type { FieldProps, MaybeAccessor } from \"./field-contract\";\nimport { resolveMaybeAccessor } from \"./field-contract\";\nimport { useSelectMessages } from \"./select-messages\";\n\nexport type ComboboxOption = { id: string; label: string; description?: string; icon?: string };\nexport type ComboboxProps = FieldProps & {\n readOnly?: boolean;\n query?: MaybeAccessor<string>;\n onQueryChange?: (query: string) => void;\n placeholder?: string;\n fetchData: (query: string, signal: AbortSignal) => Promise<ComboboxOption[]>;\n onSelect: (option: ComboboxOption) => void;\n debounceMs?: number;\n};\n\nconst iconClass = (icon: string) => (icon.split(/\\s+/).includes(\"ti\") ? icon : `ti ${icon}`);\n\nexport function Combobox(props: ComboboxProps): JSX.Element {\n const messages = useUiMessages();\n const selectMessages = useSelectMessages();\n const meta = createFieldMeta(props.id);\n const listboxId = `${meta.controlId}-listbox`;\n const [localQuery, setLocalQuery] = createSignal(\"\");\n const query = () => resolveMaybeAccessor(props.query) ?? localQuery();\n const setQuery = (value: string) => {\n setLocalQuery(value);\n props.onQueryChange?.(value);\n };\n const error = () => resolveMaybeAccessor(props.error);\n const [focusedIndex, setFocusedIndex] = createSignal(-1);\n let inputRef: HTMLInputElement | undefined;\n let optionRefs: HTMLButtonElement[] = [];\n const loader = createChoiceLoader(\n () => async (value, signal) => (await props.fetchData(value, signal)).map((option) => ({ ...option, value: option.id })),\n // Cloud debounces at 200ms and then pads the response with a 200ms minimum\n // load time to stop the spinner flickering. The spinner here sits in the\n // chevron slot and stale results stay visible, so there is nothing to pad —\n // 150ms keeps typing responsive without hammering the loader.\n () => props.debounceMs ?? 150,\n );\n const popover = createChoicePopover(() => Boolean(props.disabled || props.readOnly));\n const options = loader.options;\n const focusedOption = () => options()[focusedIndex()];\n const focus = (index: number) => {\n setFocusedIndex(index);\n optionRefs[index]?.scrollIntoView({ block: \"nearest\" });\n };\n /** `eager` loads the currently controlled query on open; typing supplies its\n * own query, so opening from `onInput` skips the duplicate request. */\n const open = (eager = true) => {\n if (props.disabled || props.readOnly || popover.open()) return;\n popover.show();\n if (eager) loader.load(query(), true);\n focus(-1);\n };\n const reset = () => {\n loader.cancel();\n setQuery(\"\");\n focus(-1);\n };\n const close = () => {\n popover.hide();\n reset();\n };\n createEffect(() => {\n if (props.readOnly) {\n popover.hide();\n loader.cancel();\n setFocusedIndex(-1);\n }\n });\n const select = (option: ChoiceOption<string>) => {\n if (props.disabled || props.readOnly) return;\n popover.hide();\n props.onSelect({ id: option.value, label: option.label, description: option.description, icon: option.icon });\n reset();\n inputRef?.focus();\n };\n const onKeyDown = (event: KeyboardEvent) => {\n if (props.disabled || props.readOnly) return;\n if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n event.preventDefault();\n if (!popover.open()) open();\n else focus(nextEnabledChoiceIndex(options(), focusedIndex(), event.key === \"ArrowDown\" ? 1 : -1));\n } else if (event.key === \"Enter\" && popover.open()) {\n // While the results are open, Enter belongs to them: it never submits an enclosing form, even with no option active.\n event.preventDefault();\n const option = options()[focusedIndex()];\n if (option) select(option);\n } else if ((event.key === \"Escape\" || event.key === \"Tab\") && popover.open()) {\n // Escape is only ours while the list is open; otherwise it has to reach an\n // enclosing dialog or drawer.\n if (event.key === \"Escape\") event.preventDefault();\n close();\n }\n };\n\n return (\n <Field\n class={props.class}\n label={props.label}\n description={props.description}\n error={error()}\n meta={meta}\n required={props.required}\n disabled={props.disabled}\n >\n <div class=\"k2b-combobox\" data-invalid={error() ? \"true\" : undefined}>\n <div ref={popover.setTrigger} class=\"k2b-combobox__input\">\n <i class=\"ti ti-search\" aria-hidden=\"true\" />\n {/* A click, typing, or an arrow key opens the list. Focus alone (Tab, a dialog's initial focus) does not, so Escape still closes the enclosing dialog. */}\n <input\n ref={(element) => {\n inputRef = element;\n }}\n id={meta.controlId}\n type=\"text\"\n value={query()}\n placeholder={props.placeholder ?? messages().search}\n disabled={props.disabled}\n readOnly={props.readOnly}\n aria-readonly={props.readOnly ? \"true\" : undefined}\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={popover.open()}\n aria-controls={listboxId}\n aria-activedescendant={focusedOption() ? `${listboxId}-${focusedIndex()}` : undefined}\n {...fieldControlAria(meta, props)}\n onClick={() => open()}\n onInput={(event) => {\n if (props.disabled || props.readOnly) {\n event.currentTarget.value = query();\n return;\n }\n setQuery(event.currentTarget.value);\n open(false);\n loader.load(event.currentTarget.value);\n focus(-1);\n }}\n onKeyDown={onKeyDown}\n />\n <Show when={loader.loading()} fallback={<i class=\"ti ti-chevron-down\" aria-hidden=\"true\" />}>\n <i class=\"ti ti-loader-2 k2b-spin\" aria-hidden=\"true\" />\n </Show>\n </div>\n <div ref={popover.setPopover} popover=\"manual\" class=\"k2b-choice-popover\">\n <div class=\"k2b-choice-options\" role=\"listbox\" id={listboxId}>\n <Show when={loader.error()}>\n {(message) => (\n <div class=\"k2b-choice-status\" data-tone=\"danger\">\n <span>{message()}</span>\n <button type=\"button\" onClick={loader.retry}>\n {messages().retry}\n </button>\n </div>\n )}\n </Show>\n <For\n each={loader.error() ? [] : options()}\n fallback={\n <div class=\"k2b-choice-status\">{query().length >= 2 ? selectMessages().noResultsFound : selectMessages().typeToSearch}</div>\n }\n >\n {(option, index) => (\n <button\n ref={(element) => (optionRefs[index()] = element)}\n type=\"button\"\n id={`${listboxId}-${index()}`}\n class=\"k2b-choice-option\"\n role=\"option\"\n aria-selected={index() === focusedIndex()}\n data-focused={index() === focusedIndex() ? \"true\" : undefined}\n onPointerMove={() => focus(index())}\n onPointerDown={(event) => event.preventDefault()}\n onClick={() => select(option)}\n >\n <Show when={option.icon}>{(icon) => <i class={iconClass(icon())} aria-hidden=\"true\" />}</Show>\n <span>\n <strong>{option.label}</strong>\n <Show when={option.description}>{(description) => <small>{description()}</small>}</Show>\n </span>\n </button>\n )}\n </For>\n </div>\n </div>\n </div>\n </Field>\n );\n}\n\nexport default Combobox;\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":[],"sources":["inputs/Combobox.tsx"],"sourcesContent":["import { createEffect, createMemo, createSignal, For, type JSX, on, Show } from \"solid-js\";\nimport { createFieldMeta, Field, fieldControlAria } from \"../internal/field\";\nimport { useUiMessages } from \"../intl/messages\";\nimport { type ChoiceOption, createChoiceLoader, createChoicePopover, nextEnabledChoiceIndex } from \"./choice\";\nimport type { FieldProps, MaybeAccessor } from \"./field-contract\";\nimport { resolveMaybeAccessor } from \"./field-contract\";\nimport { useSelectMessages } from \"./select-messages\";\n\nexport type ComboboxOption = { id: string; label: string; description?: string; icon?: string };\nexport type ComboboxProps = FieldProps & {\n readOnly?: boolean;\n query?: MaybeAccessor<string>;\n onQueryChange?: (query: string) => void;\n placeholder?: string;\n fetchData: (query: string, signal: AbortSignal) => Promise<ComboboxOption[]>;\n onSelect: (option: ComboboxOption) => void;\n debounceMs?: number;\n /** The query is the field's own value: picking or closing keeps the typed text, and Enter without an active option only closes the list. */\n freeText?: boolean;\n /** Below this many characters nothing loads and the list stays closed. */\n minQueryLength?: number;\n /** This result (by position) is active as soon as results arrive, so Enter takes it. */\n autoHighlight?: number;\n /** Replaces the results with a host-owned notice, such as an exhausted quota; nothing loads while it is set. */\n status?: JSX.Element;\n /** Shown instead of the default text when a search finds nothing. */\n empty?: JSX.Element;\n /** One line under the results, such as their source. */\n footer?: JSX.Element;\n};\n\nconst iconClass = (icon: string) => (icon.split(/\\s+/).includes(\"ti\") ? icon : `ti ${icon}`);\n\nexport function Combobox(props: ComboboxProps): JSX.Element {\n const messages = useUiMessages();\n const selectMessages = useSelectMessages();\n const meta = createFieldMeta(props.id);\n const listboxId = `${meta.controlId}-listbox`;\n const [localQuery, setLocalQuery] = createSignal(\"\");\n const query = () => resolveMaybeAccessor(props.query) ?? localQuery();\n const setQuery = (value: string) => {\n setLocalQuery(value);\n props.onQueryChange?.(value);\n };\n const error = () => resolveMaybeAccessor(props.error);\n const [focusedIndex, setFocusedIndex] = createSignal(-1);\n let inputRef: HTMLInputElement | undefined;\n let optionRefs: HTMLButtonElement[] = [];\n const loader = createChoiceLoader(\n () => async (value, signal) => (await props.fetchData(value, signal)).map((option) => ({ ...option, value: option.id })),\n // Cloud debounces at 200ms and then pads the response with a 200ms minimum\n // load time to stop the spinner flickering. The spinner here sits in the\n // chevron slot and stale results stay visible, so there is nothing to pad —\n // 150ms keeps typing responsive without hammering the loader.\n () => props.debounceMs ?? 150,\n );\n const popover = createChoicePopover(() => Boolean(props.disabled || props.readOnly));\n // A host notice or an error replaces the results; hidden results are never selectable.\n const options = () => (props.status || loader.error() ? [] : loader.options());\n const optionsById = createMemo(() => new Map(options().map((option) => [option.value, option])));\n const focusedOption = () => options()[focusedIndex()];\n const searchable = (value: string) => value.trim().length >= (props.minQueryLength ?? 0);\n const focus = (index: number) => {\n setFocusedIndex(index);\n optionRefs[index]?.scrollIntoView({ block: \"nearest\" });\n };\n /** `eager` loads the currently controlled query on open; typing supplies its\n * own query, so opening from `onInput` skips the duplicate request. */\n const open = (eager = true) => {\n if (props.disabled || props.readOnly || popover.open() || !searchable(query())) return;\n popover.show();\n if (eager && !props.status) loader.load(query(), true);\n focus(-1);\n };\n const reset = () => {\n loader.cancel();\n setQuery(\"\");\n focus(-1);\n };\n const close = () => {\n popover.hide();\n if (!props.freeText) return reset();\n loader.cancel();\n focus(-1);\n };\n createEffect(\n on(options, (list) => {\n const index = props.autoHighlight;\n if (index !== undefined && focusedIndex() < 0 && popover.open() && list.length > 0) setFocusedIndex(Math.min(index, list.length - 1));\n }),\n );\n createEffect(() => {\n if (props.readOnly) {\n popover.hide();\n loader.cancel();\n setFocusedIndex(-1);\n }\n });\n const select = (option: ChoiceOption<string>) => {\n if (props.disabled || props.readOnly) return;\n popover.hide();\n props.onSelect({ id: option.value, label: option.label, description: option.description, icon: option.icon });\n if (props.freeText) {\n loader.cancel();\n focus(-1);\n } else reset();\n inputRef?.focus();\n };\n const onKeyDown = (event: KeyboardEvent) => {\n if (props.disabled || props.readOnly) return;\n if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n event.preventDefault();\n if (!popover.open()) open();\n else focus(nextEnabledChoiceIndex(options(), focusedIndex(), event.key === \"ArrowDown\" ? 1 : -1));\n } else if (event.key === \"Enter\" && popover.open()) {\n // While the results are open, Enter belongs to them: it never submits an enclosing form, even with no option active.\n event.preventDefault();\n const option = options()[focusedIndex()];\n if (option) select(option);\n else if (props.freeText) close();\n } else if ((event.key === \"Escape\" || event.key === \"Tab\") && popover.open()) {\n // Escape is only ours while the list is open; otherwise it has to reach an\n // enclosing dialog or drawer.\n if (event.key === \"Escape\") event.preventDefault();\n close();\n }\n };\n\n return (\n <Field\n class={props.class}\n label={props.label}\n description={props.description}\n error={error()}\n meta={meta}\n required={props.required}\n disabled={props.disabled}\n >\n <div class=\"k2b-combobox\" data-invalid={error() ? \"true\" : undefined}>\n <div ref={popover.setTrigger} class=\"k2b-combobox__input\">\n <i class=\"ti ti-search\" aria-hidden=\"true\" />\n {/* A click, typing, or an arrow key opens the list. Focus alone (Tab, a dialog's initial focus) does not, so Escape still closes the enclosing dialog. */}\n <input\n ref={(element) => {\n inputRef = element;\n }}\n id={meta.controlId}\n type=\"text\"\n value={query()}\n placeholder={props.placeholder ?? messages().search}\n disabled={props.disabled}\n readOnly={props.readOnly}\n aria-readonly={props.readOnly ? \"true\" : undefined}\n role=\"combobox\"\n autocomplete=\"off\"\n aria-autocomplete=\"list\"\n aria-expanded={popover.open()}\n aria-controls={listboxId}\n aria-activedescendant={focusedOption() ? `${listboxId}-${focusedIndex()}` : undefined}\n {...fieldControlAria(meta, props)}\n onClick={() => open()}\n onInput={(event) => {\n if (props.disabled || props.readOnly) {\n event.currentTarget.value = query();\n return;\n }\n const value = event.currentTarget.value;\n setQuery(value);\n focus(-1);\n if (!searchable(value)) {\n popover.hide();\n loader.cancel();\n return;\n }\n open(false);\n if (!props.status) loader.load(value);\n }}\n onKeyDown={onKeyDown}\n />\n <Show when={loader.loading()} fallback={<i class=\"ti ti-chevron-down\" aria-hidden=\"true\" />}>\n <i class=\"ti ti-loader-2 k2b-spin\" aria-hidden=\"true\" />\n </Show>\n </div>\n <div ref={popover.setPopover} popover=\"manual\" class=\"k2b-choice-popover\">\n <div class=\"k2b-choice-options\" role=\"listbox\" id={listboxId}>\n <Show when={props.status}>{(status) => <div class=\"k2b-choice-status\">{status()}</div>}</Show>\n <Show when={!props.status && loader.error()}>\n {(message) => (\n <div class=\"k2b-choice-status\" data-tone=\"danger\">\n <span>{message()}</span>\n <button type=\"button\" onClick={loader.retry}>\n {messages().retry}\n </button>\n </div>\n )}\n </Show>\n {/* Keyed by option ID, not object: each fetch returns new objects, and a row replaced between press and release\n would swallow the click. */}\n <For\n each={options().map((option) => option.value)}\n fallback={\n <Show when={!props.status && !loader.error()}>\n <Show\n when={loader.loading()}\n fallback={\n <div class=\"k2b-choice-status\">\n {query().length >= 2 ? (props.empty ?? selectMessages().noResultsFound) : selectMessages().typeToSearch}\n </div>\n }\n >\n {/* The first search has nothing to keep visible yet: rows of the final size wait in its place. */}\n <div class=\"k2b-choice-loading\" role=\"status\">\n <span class=\"k2b-sr-only\">{messages().loading}</span>\n <For each={[62, 48, 55]}>\n {(width) => (\n <div class=\"k2b-choice-loading__row\" aria-hidden=\"true\">\n <span class=\"k2b-skeleton\" />\n <span class=\"k2b-skeleton\" style={{ width: `${width}%` }} />\n </div>\n )}\n </For>\n </div>\n </Show>\n </Show>\n }\n >\n {(id, index) => {\n const first = optionsById().get(id)!;\n const option = () => optionsById().get(id) ?? first;\n return (\n <button\n ref={(element) => (optionRefs[index()] = element)}\n type=\"button\"\n id={`${listboxId}-${index()}`}\n class=\"k2b-choice-option\"\n role=\"option\"\n aria-selected={index() === focusedIndex()}\n data-focused={index() === focusedIndex() ? \"true\" : undefined}\n onPointerMove={() => focus(index())}\n onPointerDown={(event) => event.preventDefault()}\n onClick={() => select(option())}\n >\n <Show when={option().icon}>{(icon) => <i class={iconClass(icon())} aria-hidden=\"true\" />}</Show>\n <span>\n <strong>{option().label}</strong>\n <Show when={option().description}>{(description) => <small>{description()}</small>}</Show>\n </span>\n </button>\n );\n }}\n </For>\n </div>\n <Show when={props.footer}>{(footer) => <div class=\"k2b-choice-footer\">{footer()}</div>}</Show>\n </div>\n </div>\n </Field>\n );\n}\n\nexport default Combobox;\n"],"mappings":"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","ignoreList":[]}
@@ -63,6 +63,7 @@ import { useRichSelectMessages } from "./rich-select-messages";
63
63
  export const RICH_SELECT_SEARCH_FROM = 8;
64
64
  const PHONE_QUERY = "(max-width: 47.999rem)";
65
65
  const PLACEHOLDER_ROWS = 4;
66
+ const rowKey = row => row.kind === "heading" ? `heading:${row.id}` : `item:${row.item.value}`;
66
67
  const isPhone = () => typeof matchMedia === "function" && matchMedia(PHONE_QUERY).matches;
67
68
  function RichSelectVisual(props) {
68
69
  return _$createComponent(Switch, {
@@ -254,6 +255,9 @@ export function RichSelect(props) {
254
255
  });
255
256
  return result;
256
257
  });
258
+ // Rows are keyed by heading and value, not by object: a host that rebuilds equal items must not replace the option
259
+ // under a press, which would swallow its click.
260
+ const rowsByKey = createMemo(() => new Map(rows().map(row => [rowKey(row), row])));
257
261
  const selected = createMemo(() => {
258
262
  const current = value();
259
263
  return current === null ? undefined : props.items.find(item => item.value === current);
@@ -584,44 +588,59 @@ export function RichSelect(props) {
584
588
  get children() {
585
589
  return _$createComponent(For, {
586
590
  get each() {
587
- return rows();
591
+ return rows().map(rowKey);
588
592
  },
589
593
  get fallback() {
590
594
  return _$ssr(_tmpl$25, _$escape(messages().noResults));
591
595
  },
592
- children: row => row.kind === "heading" ? _$ssr(_tmpl$26, _$escape(row.label)) : // biome-ignore lint/a11y/useKeyWithClickEvents: the trigger or search field handles the keys through aria-activedescendant.
593
- _$ssr(_tmpl$27, _$ssrAttribute("id", _$escape(optionId(row.index), true), false), _$ssrAttribute("aria-selected", _$escape(row.item.value, true) === _$escape(value(), true), false) + _$ssrAttribute("aria-disabled", row.item.disabledReason ? "true" : _$escape(undefined, true), false), `${_$escape(optionId(row.index), true)}-label`, _$ssrAttribute("aria-describedby", _$escape([row.item.disabledReason || row.item.description ? `${optionId(row.index)}-line` : "", withPreview() && row.index === active() ? previewId : ""].filter(Boolean).join(" ") || undefined, true), false) + _$ssrAttribute("data-active", row.index === active() ? "true" : _$escape(undefined, true), false), _$escape(_$createComponent(Show, {
594
- get when() {
595
- return anyVisual();
596
- },
597
- get children() {
598
- return _$createComponent(Show, {
596
+ children: key => {
597
+ const first = rowsByKey().get(key);
598
+ if (first.kind === "heading") {
599
+ const label = () => {
600
+ const current = rowsByKey().get(key);
601
+ return current?.kind === "heading" ? current.label : first.label;
602
+ };
603
+ return _$ssr(_tmpl$26, _$escape(label()));
604
+ }
605
+ const entry = () => {
606
+ const current = rowsByKey().get(key);
607
+ return current?.kind === "item" ? current : first;
608
+ };
609
+ return (// biome-ignore lint/a11y/useKeyWithClickEvents: the trigger or search field handles the keys through aria-activedescendant.
610
+ _$ssr(_tmpl$27, _$ssrAttribute("id", _$escape(optionId(entry().index), true), false), _$ssrAttribute("aria-selected", _$escape(entry().item.value, true) === _$escape(value(), true), false) + _$ssrAttribute("aria-disabled", entry().item.disabledReason ? "true" : _$escape(undefined, true), false), `${_$escape(optionId(entry().index), true)}-label`, _$ssrAttribute("aria-describedby", _$escape([entry().item.disabledReason || entry().item.description ? `${optionId(entry().index)}-line` : "", withPreview() && entry().index === active() ? previewId : ""].filter(Boolean).join(" ") || undefined, true), false) + _$ssrAttribute("data-active", entry().index === active() ? "true" : _$escape(undefined, true), false), _$escape(_$createComponent(Show, {
599
611
  get when() {
600
- return hasVisual(row.item);
601
- },
602
- get fallback() {
603
- return _$ssr(_tmpl$28);
612
+ return anyVisual();
604
613
  },
605
614
  get children() {
606
- return _$createComponent(RichSelectVisual, {
607
- get item() {
608
- return row.item;
615
+ return _$createComponent(Show, {
616
+ get when() {
617
+ return hasVisual(entry().item);
618
+ },
619
+ get fallback() {
620
+ return _$ssr(_tmpl$28);
621
+ },
622
+ get children() {
623
+ return _$createComponent(RichSelectVisual, {
624
+ get item() {
625
+ return entry().item;
626
+ }
627
+ });
609
628
  }
610
629
  });
611
630
  }
612
- });
613
- }
614
- })), _$escape(_$createComponent(RichSelectRowText, {
615
- get item() {
616
- return row.item;
617
- },
618
- get labelId() {
619
- return `${optionId(row.index)}-label`;
620
- },
621
- get lineId() {
622
- return `${optionId(row.index)}-line`;
623
- }
624
- })))
631
+ })), _$escape(_$createComponent(RichSelectRowText, {
632
+ get item() {
633
+ return entry().item;
634
+ },
635
+ get labelId() {
636
+ return `${optionId(entry().index)}-label`;
637
+ },
638
+ get lineId() {
639
+ return `${optionId(entry().index)}-line`;
640
+ }
641
+ })))
642
+ );
643
+ }
625
644
  });
626
645
  }
627
646
  })), _$escape(_$createComponent(Show, {