@oscarpalmer/oui 0.34.0 → 0.36.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 (93) hide show
  1. package/dist/css/combobox.css +1 -1
  2. package/dist/css/listbox.css +152 -0
  3. package/dist/js/combobox/constants.combobox.d.mts +2 -2
  4. package/dist/js/combobox/constants.combobox.mjs +40 -33
  5. package/dist/js/combobox/dom.combobox.d.mts +8 -91
  6. package/dist/js/combobox/dom.combobox.mjs +204 -2
  7. package/dist/js/combobox/event.combobox.d.mts +14 -8
  8. package/dist/js/combobox/event.combobox.mjs +2 -2
  9. package/dist/js/combobox/index.d.mts +1 -1
  10. package/dist/js/combobox/index.mjs +21 -18
  11. package/dist/js/combobox/misc.combobox.d.mts +2 -14
  12. package/dist/js/combobox/misc.combobox.mjs +2 -2
  13. package/dist/js/combobox/models.combobox.d.mts +44 -2
  14. package/dist/js/combobox/state.combobox.d.mts +9 -8
  15. package/dist/js/combobox/state.combobox.mjs +2 -2
  16. package/dist/js/constants.combobox-DmRAMC64.d.mts +26 -0
  17. package/dist/js/constants.listbox-DRsXUxd3.d.mts +14 -0
  18. package/dist/js/index-EfkUSals.d.mts +139 -0
  19. package/dist/js/index.d.mts +4 -2
  20. package/dist/js/index.mjs +4 -2
  21. package/dist/js/internal/attributable.mjs +4 -3
  22. package/dist/js/internal/listbox/listbox.constants.d.mts +11 -0
  23. package/dist/js/internal/listbox/listbox.constants.mjs +23 -0
  24. package/dist/js/internal/listbox/listbox.data.d.mts +20 -0
  25. package/dist/js/internal/listbox/listbox.data.mjs +2 -0
  26. package/dist/js/internal/listbox/listbox.dom.d.mts +66 -0
  27. package/dist/js/internal/listbox/listbox.dom.mjs +2 -0
  28. package/dist/js/internal/listbox/listbox.event.d.mts +5 -0
  29. package/dist/js/internal/listbox/listbox.event.mjs +63 -0
  30. package/dist/js/internal/listbox/listbox.form.d.mts +6 -0
  31. package/dist/js/internal/listbox/listbox.form.mjs +52 -0
  32. package/dist/js/internal/listbox/listbox.misc.d.mts +12 -0
  33. package/dist/js/internal/listbox/listbox.misc.mjs +2 -0
  34. package/dist/js/internal/listbox/listbox.model.d.mts +2 -0
  35. package/dist/js/internal/listbox/listbox.model.mjs +1 -0
  36. package/dist/js/internal/listbox/listbox.state.d.mts +7 -0
  37. package/dist/js/internal/listbox/listbox.state.mjs +40 -0
  38. package/dist/js/internal/misc.d.mts +6 -0
  39. package/dist/js/internal/misc.mjs +13 -0
  40. package/dist/js/l10n-Bn-7RcwR.d.mts +20 -0
  41. package/dist/js/l10n.d.mts +2 -0
  42. package/dist/js/l10n.mjs +80 -0
  43. package/dist/js/listbox/constants.listbox.d.mts +2 -0
  44. package/dist/js/listbox/constants.listbox.mjs +40 -0
  45. package/dist/js/listbox/event.listbox.d.mts +8 -0
  46. package/dist/js/listbox/event.listbox.mjs +2 -0
  47. package/dist/js/listbox/index.d.mts +116 -0
  48. package/dist/js/listbox/index.mjs +189 -0
  49. package/dist/js/listbox/misc.listbox.d.mts +2 -0
  50. package/dist/js/listbox/misc.listbox.mjs +2 -0
  51. package/dist/js/listbox/state.listbox.d.mts +11 -0
  52. package/dist/js/listbox/state.listbox.mjs +2 -0
  53. package/dist/js/listbox.data-BWd0PCy7.mjs +424 -0
  54. package/dist/js/listbox.model-CRReGOmu.d.mts +125 -0
  55. package/dist/js/misc.listbox-BI-XsTOf.mjs +197 -0
  56. package/dist/js/misc.listbox-gnoJhrTr.d.mts +20 -0
  57. package/dist/js/sortable/sortable.embedded.mjs +4 -2
  58. package/dist/js/state.combobox-4xq0FPCF.mjs +365 -0
  59. package/package.json +7 -3
  60. package/src/css/combobox.scss +1 -1
  61. package/src/css/listbox.scss +149 -0
  62. package/src/js/combobox/constants.combobox.ts +46 -25
  63. package/src/js/combobox/dom.combobox.ts +65 -452
  64. package/src/js/combobox/event.combobox.ts +129 -218
  65. package/src/js/combobox/index.ts +43 -30
  66. package/src/js/combobox/misc.combobox.ts +60 -94
  67. package/src/js/combobox/models.combobox.ts +7 -47
  68. package/src/js/combobox/state.combobox.ts +136 -125
  69. package/src/js/index.ts +1 -0
  70. package/src/js/internal/listbox/listbox.constants.ts +25 -0
  71. package/src/js/{combobox/data.combobox.ts → internal/listbox/listbox.data.ts} +94 -43
  72. package/src/js/internal/listbox/listbox.dom.ts +445 -0
  73. package/src/js/internal/listbox/listbox.event.ts +129 -0
  74. package/src/js/{combobox/form.combobox.ts → internal/listbox/listbox.form.ts} +33 -33
  75. package/src/js/internal/listbox/listbox.misc.ts +135 -0
  76. package/src/js/internal/listbox/listbox.model.ts +169 -0
  77. package/src/js/internal/listbox/listbox.state.ts +82 -0
  78. package/src/js/internal/misc.ts +27 -0
  79. package/src/js/l10n.ts +192 -0
  80. package/src/js/listbox/constants.listbox.ts +61 -0
  81. package/src/js/listbox/event.listbox.ts +106 -0
  82. package/src/js/listbox/index.ts +302 -0
  83. package/src/js/listbox/misc.listbox.ts +70 -0
  84. package/src/js/listbox/state.listbox.ts +196 -0
  85. package/dist/js/combobox/data.combobox.d.mts +0 -18
  86. package/dist/js/combobox/data.combobox.mjs +0 -2
  87. package/dist/js/combobox/form.combobox.d.mts +0 -6
  88. package/dist/js/combobox/form.combobox.mjs +0 -2
  89. package/dist/js/combobox/i18n.combobox.d.mts +0 -2
  90. package/dist/js/combobox/i18n.combobox.mjs +0 -2
  91. package/dist/js/constants.combobox-DqNUGBLS.d.mts +0 -243
  92. package/dist/js/data.combobox-8RwBv5ey.mjs +0 -1077
  93. package/src/js/combobox/i18n.combobox.ts +0 -179
@@ -1,62 +1,28 @@
1
- import {getAria} from '@oscarpalmer/toretto/aria';
2
- import {getAttribute, setAttribute} from '@oscarpalmer/toretto/attribute';
3
- import type {AnyAriaBooleanAttribute} from '@oscarpalmer/toretto/models';
4
- import type {OuiComboboxElement} from '.';
5
- import {COMBOBOX_AUTOCOMPLETES} from './constants.combobox';
6
- import {onComboboxFilter} from './event.combobox';
7
- import {updateComboboxValidity} from './form.combobox';
1
+ import {updateListboxMessage, updateListboxValueLabel} from '../internal/listbox/listbox.dom';
8
2
  import type {
9
- OuiComboboxAutoComplete,
10
- OuiComboboxAutoCompleteType,
11
- OuiComboboxState,
12
- } from './models.combobox';
3
+ OuiListboxAutoComplete,
4
+ OuiListboxAutoCompleteType,
5
+ OuiListboxLocalizations,
6
+ } from '../internal/listbox/listbox.model';
7
+ import {getAttributeValue} from '../internal/misc';
8
+ import {OuiLocalizations, updateLocalizations} from '../l10n';
9
+ import {COMBOBOX_AUTOCOMPLETES} from './constants.combobox';
10
+ import {updateComboboxSelectionItems} from './dom.combobox';
11
+ import type {OuiComboboxElement} from './index';
12
+ import {comboboxLocalizations, comboboxStates} from './state.combobox';
13
13
 
14
14
  // #region Functions
15
15
 
16
- export function generateComboboxIds(prefix: string, select: HTMLSelectElement): void {
17
- const elements = [...select.children].flatMap(child =>
18
- child instanceof HTMLOptGroupElement
19
- ? [
20
- child,
21
- ...[...child.children].filter(groupChild => groupChild instanceof HTMLOptionElement),
22
- ]
23
- : child instanceof HTMLOptionElement
24
- ? [child]
25
- : [],
26
- );
27
-
28
- const {length} = elements;
29
-
30
- for (let index = 0; index < length; index += 1) {
31
- const element = elements[index];
32
-
33
- if (element.id === '') {
34
- element.id = `${prefix}${element instanceof HTMLOptGroupElement ? '_group_' : '_option_'}${index}`;
35
- }
36
- }
37
- }
38
-
39
- export function getComboboxAttributeValue<Value>(
40
- element: HTMLElement,
41
- attribute: string,
42
- possible: Set<Value>,
43
- defaultValue: Value,
44
- ): Value {
45
- const value = getAttribute(element, attribute);
46
-
47
- return possible.has(value as Value) ? (value as Value) : defaultValue;
48
- }
49
-
50
16
  export function getComboboxAutoComplete(
51
17
  element: OuiComboboxElement,
52
18
  select: boolean,
53
- ): OuiComboboxAutoComplete {
54
- let value: OuiComboboxAutoCompleteType;
19
+ ): OuiListboxAutoComplete {
20
+ let value: OuiListboxAutoCompleteType;
55
21
 
56
22
  if (select) {
57
23
  value = 'list';
58
24
  } else {
59
- value = getComboboxAttributeValue(element, 'autocomplete', COMBOBOX_AUTOCOMPLETES, 'none');
25
+ value = getAttributeValue(element, 'autocomplete', COMBOBOX_AUTOCOMPLETES, 'none');
60
26
  }
61
27
 
62
28
  return {
@@ -68,65 +34,65 @@ export function getComboboxAutoComplete(
68
34
  };
69
35
  }
70
36
 
71
- export function getComboboxBooleanAria(
72
- element: HTMLElement,
73
- attribute: AnyAriaBooleanAttribute,
74
- ): boolean {
75
- const value = getAria(element, attribute);
76
-
77
- return value === true || value === 'true';
78
- }
79
-
80
37
  export function getComboboxId(): string {
81
38
  index += 1;
82
39
 
83
40
  return `oui_combobox_${index}`;
84
41
  }
85
42
 
86
- export function getComboboxVisibleOptions(state: OuiComboboxState): HTMLElement[] {
87
- return state.options.all.filter(option => !option.hidden);
88
- }
89
-
90
43
  export function hasTextSelection(element: HTMLElement): element is HTMLInputElement {
91
44
  return element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
92
45
  }
93
46
 
94
- export function setComboboxFilter(state: OuiComboboxState, value: unknown): void {
95
- if (typeof value !== 'string') {
96
- return;
97
- }
98
-
99
- const type: InputEvent['inputType'] =
100
- value.length < state.filter.element.value.length ? 'deleteContentBackward' : 'insertText';
101
-
102
- state.filter.element.value = value;
103
-
104
- onComboboxFilter(type, state);
47
+ /**
48
+ * Set combobox localizations for a specific language
49
+ *
50
+ * @param language Language code
51
+ * @param localizations Localizations for language
52
+ */
53
+ export function setComboboxLocalizations(
54
+ language: string,
55
+ localizations: Partial<OuiListboxLocalizations>,
56
+ ): void;
57
+
58
+ /**
59
+ * Set combobox localizations for one or more languages
60
+ *
61
+ * @param localizations Localizations for one or more languages
62
+ */
63
+ export function setComboboxLocalizations(
64
+ localizations: Record<string, Partial<OuiListboxLocalizations>>,
65
+ ): void;
66
+
67
+ export function setComboboxLocalizations(
68
+ first: string | Record<string, Partial<OuiListboxLocalizations>>,
69
+ second?: Partial<OuiListboxLocalizations>,
70
+ ): void {
71
+ updateComboboxLocalizations(comboboxLocalizations, comboboxLocalizations.values, first, second);
105
72
  }
106
73
 
107
- export function setComboboxLimit(
108
- type: 'max' | 'min',
109
- state: OuiComboboxState,
110
- value?: unknown,
74
+ export function updateComboboxLocalizations(
75
+ instance: OuiLocalizations<OuiListboxLocalizations>,
76
+ destination: Record<string, Partial<OuiListboxLocalizations>>,
77
+ first: string | Record<string, Partial<OuiListboxLocalizations>>,
78
+ second?: Partial<OuiListboxLocalizations>,
111
79
  ): void {
112
- let actual = value;
113
-
114
- if (typeof actual === 'string') {
115
- actual = Number.parseInt(actual, 10);
116
- }
117
-
118
- const updateLimit =
119
- typeof actual === 'number' && !Number.isNaN(actual) && actual >= 0
120
- ? actual !== state[type]
121
- : actual == null;
122
-
123
- if (updateLimit) {
124
- state[type] = actual as number | undefined;
125
-
126
- setAttribute(state.host, type, actual);
127
-
128
- updateComboboxValidity(state);
129
- }
80
+ updateLocalizations(
81
+ instance,
82
+ typeof first === 'string'
83
+ ? {
84
+ [first]: second!,
85
+ }
86
+ : first,
87
+ destination,
88
+ () => {
89
+ for (const state of comboboxStates.set) {
90
+ updateListboxMessage(state);
91
+ updateListboxValueLabel(state);
92
+ updateComboboxSelectionItems(state);
93
+ }
94
+ },
95
+ );
130
96
  }
131
97
 
132
98
  // #endregion
@@ -1,18 +1,13 @@
1
- import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
2
1
  import type {OuiFloatable} from '../floatable/floatable.embedded';
3
- import type {OuiComboboxElement} from './index';
2
+ import type {OuiListboxState, OuiListboxType} from '../internal/listbox/listbox.model';
3
+
4
+ // #region Types
4
5
 
5
6
  export type OuiComboboxAnchor = {
6
7
  container?: HTMLElement;
7
8
  element: HTMLElement;
8
9
  };
9
10
 
10
- export type OuiComboboxAutoComplete = Record<OuiComboboxAutoCompleteType, boolean> & {
11
- value: OuiComboboxAutoCompleteType;
12
- };
13
-
14
- export type OuiComboboxAutoCompleteType = 'both' | 'inline' | 'list' | 'none';
15
-
16
11
  export type OuiComboboxFilter = {
17
12
  element: HTMLInputElement;
18
13
  value: string;
@@ -29,24 +24,11 @@ export type OuiComboboxLabel = {
29
24
  id: string;
30
25
  };
31
26
 
32
- export type OuiComboboxLocalizations = {
33
- empty: string | (() => string);
34
- input: string | (() => string);
35
- invalid: string | (() => string);
36
- max: string | ((max: number) => string);
37
- min: string | ((min: number) => string);
38
- remove: string | ((label: string, value: string) => string);
39
- results: string | ((amount: number) => string);
40
- selected: string | ((amount: number) => string);
41
- };
42
-
43
27
  export type OuiComboboxOptions = {
44
28
  all: HTMLElement[];
45
29
  selected: HTMLElement[];
46
30
  };
47
31
 
48
- export type OuiComboboxPreview = 'none' | 'set' | 'typed';
49
-
50
32
  export type OuiComboboxSelection = {
51
33
  container: HTMLElement;
52
34
  items: OuiComboboxSelectionItem[];
@@ -58,37 +40,15 @@ export type OuiComboboxSelectionItem = {
58
40
  };
59
41
 
60
42
  export type OuiComboboxState = {
61
- active?: HTMLElement;
62
43
  anchor: OuiComboboxAnchor;
63
- autocomplete: OuiComboboxAutoComplete;
64
- content: HTMLElement;
65
- disabled: boolean;
66
- filter: OuiComboboxFilter;
67
44
  floatable: OuiFloatable;
68
- groups?: OuiComboboxGroup[];
69
- host: OuiComboboxElement;
70
- id: string;
71
- internals: ElementInternals;
72
- label: OuiComboboxLabel;
73
- listeners: RemovableEventListener[];
74
- localizations: Record<string, Partial<OuiComboboxLocalizations>>;
75
- max?: number;
76
- min?: number;
77
- message: HTMLElement;
78
- name: string;
79
- multiple: boolean;
80
- options: OuiComboboxOptions;
81
- readonly: boolean;
82
- required: boolean;
83
- select: HTMLSelectElement;
84
45
  selection?: OuiComboboxSelection;
85
- type: OuiComboboxType;
86
- values: OuiComboboxValues;
87
- };
88
-
89
- export type OuiComboboxType = 'autocomplete' | 'select';
46
+ type: OuiListboxType;
47
+ } & OuiListboxState;
90
48
 
91
49
  export type OuiComboboxValues = {
92
50
  current: string[];
93
51
  initial: string[];
94
52
  };
53
+
54
+ // #endregion
@@ -1,89 +1,41 @@
1
+ import {noop} from '@oscarpalmer/atoms/function';
1
2
  import {setAria} from '@oscarpalmer/toretto/aria';
2
- import {getAttribute, setAttribute} from '@oscarpalmer/toretto/attribute';
3
+ import {setAttributes} from '@oscarpalmer/toretto/attribute';
3
4
  import {getData} from '@oscarpalmer/toretto/data';
4
5
  import {on} from '@oscarpalmer/toretto/event';
5
6
  import {createEmbeddedFloatable} from '../floatable/floatable.embedded';
7
+ import {LISTBOX_TYPES} from '../internal/listbox/listbox.constants';
8
+ import {generateListboxIds} from '../internal/listbox/listbox.misc';
9
+ import type {
10
+ OuiListboxCallbacks,
11
+ OuiListboxLocalizations,
12
+ OuiListboxStates,
13
+ } from '../internal/listbox/listbox.model';
14
+ import {initializeListboxState} from '../internal/listbox/listbox.state';
15
+ import {getAttributeValue, getBooleanAriaValue} from '../internal/misc';
16
+ import {getLocalizations} from '../l10n';
17
+ import {COMBOBOX_ATTRIBUTES, COMBOBOX_MESSAGES} from './constants.combobox';
18
+ import {createComboboxAnchor, createComboboxContent} from './dom.combobox';
6
19
  import {
7
- COMBOBOX_MESSAGE_LABEL,
8
- COMBOBOX_MESSAGE_SELECT,
9
- COMBOBOX_TYPES,
10
- } from './constants.combobox';
11
- import {filterComboboxOptions} from './data.combobox';
12
- import {
13
- createComboboxAnchor,
14
- createComboboxContent,
15
- setComboboxDisabled,
16
- setComboboxReadonly,
17
- setComboboxRequired,
18
- updateComboboxMessage,
19
- updateComboboxPreview,
20
- updateComboboxSelectionItems,
21
- } from './dom.combobox';
22
- import {onComboboxClickContent, onComboboxFocusout} from './event.combobox';
23
- import {updateComboboxForm} from './form.combobox';
20
+ onComboboxActivation,
21
+ onComboboxApply,
22
+ onComboboxEnter,
23
+ onComboboxEscape,
24
+ onComboboxFilter,
25
+ onComboboxNavigation,
26
+ onComboboxPreview,
27
+ onComboboxSelection,
28
+ onComboboxTab,
29
+ onComboboxToggle,
30
+ preventComboboxActivation,
31
+ skipComboboxNavigation,
32
+ } from './event.combobox';
24
33
  import type {OuiComboboxElement} from './index';
25
- import {
26
- generateComboboxIds,
27
- getComboboxAttributeValue,
28
- getComboboxAutoComplete,
29
- getComboboxBooleanAria,
30
- getComboboxId,
31
- getComboboxVisibleOptions,
32
- setComboboxLimit,
33
- } from './misc.combobox';
34
+ import {getComboboxAutoComplete, getComboboxId} from './misc.combobox';
34
35
  import type {OuiComboboxState} from './models.combobox';
35
36
 
36
37
  // #region Functions
37
38
 
38
- export function attachCombobox(state: OuiComboboxState): void {
39
- const {anchor, content, filter, label, listeners, select} = state;
40
-
41
- label.element.htmlFor = filter.element.id;
42
- label.element.id = `${state.id}_label`;
43
-
44
- select.hidden = true;
45
-
46
- select.insertAdjacentElement('afterend', anchor.container ?? anchor.element);
47
- (anchor.container ?? anchor.element).insertAdjacentElement('afterend', content);
48
-
49
- select.remove();
50
-
51
- state.floatable = createEmbeddedFloatable(anchor.container ?? anchor.element, content, {
52
- position: 'below-start',
53
- reusable: false,
54
- });
55
-
56
- state.floatable.update();
57
-
58
- listeners.push(
59
- on(state.host, 'focusout', () => {
60
- setTimeout(() => {
61
- onComboboxFocusout(state);
62
- });
63
- }),
64
- on(content, 'click', event => {
65
- onComboboxClickContent(event, content);
66
- }),
67
- on(content, 'toggle', () => {
68
- setAria(anchor.element, 'expanded', state.floatable.open);
69
-
70
- setAria(
71
- filter.element,
72
- 'activedescendant',
73
- state.floatable.open ? state.active?.id : undefined,
74
- );
75
-
76
- if (state.floatable.open) {
77
- filter.element.focus();
78
- }
79
- }),
80
- );
81
-
82
- comboboxStates.map.set(state.host, state);
83
- comboboxStates.map.set(state.content, state);
84
- comboboxStates.set.add(state);
85
- }
86
-
87
39
  export function createComboboxState(
88
40
  element: OuiComboboxElement,
89
41
  internals: ElementInternals,
@@ -91,45 +43,58 @@ export function createComboboxState(
91
43
  const select = element.querySelector('select');
92
44
 
93
45
  if (select == null || select.id === '') {
94
- throw new Error(COMBOBOX_MESSAGE_SELECT);
46
+ throw new Error(COMBOBOX_MESSAGES.select);
95
47
  }
96
48
 
97
49
  const label = element.querySelector(`label[for="${select.id}"]`) as HTMLLabelElement;
98
50
 
99
51
  if (label == null) {
100
- throw new Error(COMBOBOX_MESSAGE_LABEL);
52
+ throw new Error(COMBOBOX_MESSAGES.label);
101
53
  }
102
54
 
103
55
  element.id = select.id;
104
56
 
105
- setAttribute(element, 'name', select.name === '' ? select.id : select.name);
106
-
107
- const disabled = getComboboxBooleanAria(element, 'disabled') || select.disabled;
57
+ const disabled = getBooleanAriaValue(element, 'disabled') || select.disabled;
108
58
  const multiple = element.multiple || select.multiple;
109
- const readonly = getComboboxBooleanAria(element, 'readonly');
110
- const required = getComboboxBooleanAria(element, 'required') || select.required;
59
+ const readonly = getBooleanAriaValue(element, 'readonly');
60
+ const required = getBooleanAriaValue(element, 'required') || select.required;
111
61
 
112
62
  const id = getComboboxId();
113
- const type = multiple
114
- ? 'select'
115
- : getComboboxAttributeValue(element, 'type', COMBOBOX_TYPES, 'select');
63
+ const type = multiple ? 'select' : getAttributeValue(element, 'type', LISTBOX_TYPES, 'select');
116
64
 
117
65
  const autocomplete = getComboboxAutoComplete(element, type === 'select');
118
66
 
67
+ setAttributes(element, {
68
+ autocomplete: autocomplete.value,
69
+ name: select.name === '' ? select.id : select.name,
70
+ });
71
+
119
72
  const labelId = label.id;
120
73
 
121
74
  label.htmlFor = id;
122
75
  label.id = `${id}_label`;
123
76
 
124
- generateComboboxIds(id, select);
77
+ generateListboxIds(id, select);
125
78
 
126
- const anchor = createComboboxAnchor({autocomplete, id, label, multiple, select, type});
127
- const content = createComboboxContent({autocomplete, id, label, multiple, select, type});
79
+ const parameters = {
80
+ autocomplete,
81
+ element,
82
+ id,
83
+ label,
84
+ multiple,
85
+ select,
86
+ type,
87
+ attributes: COMBOBOX_ATTRIBUTES,
88
+ listbox: false,
89
+ };
90
+
91
+ const anchor = createComboboxAnchor(parameters);
92
+ const content = createComboboxContent(parameters);
93
+
94
+ const {selected} = content.list.options;
128
95
 
129
96
  const values =
130
- content.list.selected.length === 0
131
- ? ['']
132
- : content.list.selected.map(option => getData(option, 'value', false) ?? '');
97
+ selected.length === 0 ? [''] : selected.map(option => getData(option, 'value', false) ?? '');
133
98
 
134
99
  const state: OuiComboboxState = {
135
100
  autocomplete,
@@ -141,12 +106,15 @@ export function createComboboxState(
141
106
  required,
142
107
  select,
143
108
  type,
109
+ active: selected[0],
144
110
  anchor: {
145
111
  container: anchor.container,
146
112
  element: anchor.element,
147
113
  },
148
- active: content.list.selected[0],
114
+ attributes: COMBOBOX_ATTRIBUTES,
115
+ callbacks: comboboxCallbacks as OuiComboboxState['callbacks'],
149
116
  content: content.element,
117
+ elements: [element, anchor.element, content.filter?.element],
150
118
  filter: {
151
119
  element: content.filter?.element ?? (anchor.element as HTMLInputElement),
152
120
  value: '',
@@ -158,51 +126,66 @@ export function createComboboxState(
158
126
  element: label,
159
127
  id: labelId,
160
128
  },
129
+ list: content.list.element,
130
+ listbox: false,
161
131
  listeners: [],
162
- localizations: {},
132
+ localizations: {
133
+ component: {},
134
+ global: comboboxLocalizations,
135
+ },
163
136
  message: content.message,
164
137
  name: element.name,
165
138
  options: {
166
- all: content.list.options,
167
- selected: content.list.selected,
139
+ selected,
140
+ all: content.list.options.all,
168
141
  },
169
142
  selection: anchor.selection,
170
143
  values: {
171
144
  current: values.slice(),
172
145
  initial: values.slice(),
146
+ label: content.value,
173
147
  },
174
148
  };
175
149
 
176
- attachCombobox(state);
150
+ return initializeListboxState(state, comboboxStates, onAttachCombobox);
151
+ }
177
152
 
178
- setComboboxDisabled(state, disabled);
179
- setComboboxLimit('max', state, getAttribute(element, 'max'));
180
- setComboboxLimit('min', state, getAttribute(element, 'min'));
181
- setComboboxReadonly(state, readonly);
182
- setComboboxRequired(state, required);
153
+ export function onAttachCombobox(state: OuiComboboxState): void {
154
+ const {anchor, content, filter, select} = state;
183
155
 
184
- filterComboboxOptions(state);
156
+ select.hidden = true;
185
157
 
186
- if (content.list.selected.length > 0) {
187
- updateComboboxPreview('set', state, content.list.selected[0]);
188
- }
158
+ select.insertAdjacentElement('afterend', anchor.container ?? anchor.element);
159
+ (anchor.container ?? anchor.element).insertAdjacentElement('afterend', content);
189
160
 
190
- updateComboboxForm(state, state.values.current, false);
191
- updateComboboxMessage(state, getComboboxVisibleOptions(state).length);
192
- updateComboboxSelectionItems(state);
161
+ select.remove();
193
162
 
194
- return state;
195
- }
163
+ state.floatable = createEmbeddedFloatable(anchor.container ?? anchor.element, content, {
164
+ position: 'below-start',
165
+ reusable: false,
166
+ });
196
167
 
197
- export function detachCombobox(state: OuiComboboxState): void {
198
- const {content, floatable, host, label, select} = state;
168
+ state.floatable.update();
199
169
 
200
- const listeners = state.listeners.splice(0);
201
- const {length} = listeners;
170
+ state.listeners.push(
171
+ on(content, 'toggle', () => {
172
+ setAria(anchor.element, 'expanded', state.floatable.open);
202
173
 
203
- for (let index = 0; index < length; index += 1) {
204
- listeners[index]();
205
- }
174
+ setAria(
175
+ filter.element,
176
+ 'activedescendant',
177
+ state.floatable.open ? state.active?.id : undefined,
178
+ );
179
+
180
+ if (state.floatable.open) {
181
+ filter.element.focus();
182
+ }
183
+ }),
184
+ );
185
+ }
186
+
187
+ export function onDetachCombobox(state: OuiComboboxState): void {
188
+ const {content, floatable, label, select} = state;
206
189
 
207
190
  floatable.destroy();
208
191
 
@@ -217,19 +200,47 @@ export function detachCombobox(state: OuiComboboxState): void {
217
200
  label.element.id = state.label.id;
218
201
 
219
202
  select.hidden = false;
220
-
221
- comboboxStates.map.delete(host);
222
- comboboxStates.map.delete(content);
223
- comboboxStates.set.delete(state);
224
203
  }
225
204
 
226
205
  // #endregion
227
206
 
228
207
  // #region Variables
229
208
 
230
- export const comboboxStates = {
231
- map: new WeakMap<HTMLElement, OuiComboboxState>(),
232
- set: new Set<OuiComboboxState>(),
209
+ const comboboxCallbacks: OuiListboxCallbacks<OuiComboboxState> = {
210
+ data: {
211
+ onApply: onComboboxApply,
212
+ onFilter: onComboboxFilter,
213
+ onForm: noop,
214
+ onPreview: onComboboxPreview,
215
+ onToggle: onComboboxToggle,
216
+ },
217
+ keydown: {
218
+ onActivation: onComboboxActivation,
219
+ onEnter: onComboboxEnter,
220
+ onEscape: onComboboxEscape,
221
+ onNavigation: onComboboxNavigation,
222
+ onSelection: onComboboxSelection,
223
+ onTab: onComboboxTab,
224
+ onToggle: onComboboxToggle,
225
+ preventActivation: preventComboboxActivation,
226
+ skipNavigation: skipComboboxNavigation,
227
+ },
228
+ };
229
+
230
+ export const comboboxLocalizations = getLocalizations<OuiListboxLocalizations>({
231
+ empty: 'No results found',
232
+ input: 'Please enter a value',
233
+ invalid: 'Please select an option from the list',
234
+ max: max => `Please select fewer options (maximum is ${max})`,
235
+ min: min => `Please select more options (minimum is ${min})`,
236
+ remove: label => `Remove ${label}`,
237
+ results: amount => `${amount} results available`,
238
+ selected: amount => `${amount} selected`,
239
+ });
240
+
241
+ export const comboboxStates: OuiListboxStates<OuiComboboxState> = {
242
+ map: new WeakMap(),
243
+ set: new Set(),
233
244
  };
234
245
 
235
246
  // #endregion
package/src/js/index.ts CHANGED
@@ -5,6 +5,7 @@ export * from './combobox/index';
5
5
  export * from './dialog';
6
6
  export * from './floatable/floatable.standalone';
7
7
  export * from './focus-trap/focus-trap.standalone';
8
+ export * from './listbox/index';
8
9
  export * from './movable/movable.standalone';
9
10
  export * from './popover';
10
11
  export * from './resizable/resizable.standalone';
@@ -0,0 +1,25 @@
1
+ import type {OuiListboxType} from './listbox.model';
2
+
3
+ // #region Variables
4
+
5
+ const keysActivation = new Set([' ', 'Backspace', 'Delete', 'Enter']);
6
+
7
+ const keysNavigation = new Set(['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp']);
8
+
9
+ const keysSelection = new Set(['ArrowRight', 'End']);
10
+
11
+ export const LISTBOX_KEYS = {
12
+ activation: keysActivation,
13
+ navigation: keysNavigation,
14
+ selection: keysSelection,
15
+ };
16
+
17
+ export const LISTBOX_TYPES = new Set<OuiListboxType>(['autocomplete', 'select']);
18
+
19
+ export const listboxScrollOptions: ScrollIntoViewOptions = {
20
+ behavior: 'auto',
21
+ block: 'nearest',
22
+ inline: 'nearest',
23
+ };
24
+
25
+ // #endregion