@oscarpalmer/oui 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.
- package/dist/css/badge.css +18 -15
- package/dist/css/button.css +35 -31
- package/dist/css/color.css +20 -20
- package/dist/css/combobox.css +134 -35
- package/dist/css/content.css +29 -10
- package/dist/css/floatable.css +10 -10
- package/dist/css/form.css +393 -129
- package/dist/css/listbox.css +176 -0
- package/dist/css/message.css +13 -10
- package/dist/css/reset.css +29 -88
- package/dist/css/splitter.css +2 -2
- package/dist/css/tabs.css +103 -0
- package/dist/js/combobox/constants.combobox.d.mts +2 -2
- package/dist/js/combobox/constants.combobox.mjs +40 -33
- package/dist/js/combobox/dom.combobox.d.mts +8 -91
- package/dist/js/combobox/dom.combobox.mjs +204 -2
- package/dist/js/combobox/event.combobox.d.mts +14 -8
- package/dist/js/combobox/event.combobox.mjs +2 -2
- package/dist/js/combobox/index.d.mts +1 -1
- package/dist/js/combobox/index.mjs +20 -18
- package/dist/js/combobox/misc.combobox.d.mts +2 -2
- package/dist/js/combobox/misc.combobox.mjs +2 -2
- package/dist/js/combobox/models.combobox.d.mts +44 -2
- package/dist/js/combobox/state.combobox.d.mts +9 -10
- package/dist/js/combobox/state.combobox.mjs +2 -2
- package/dist/js/constants.combobox-DmRAMC64.d.mts +26 -0
- package/dist/js/constants.listbox-DRsXUxd3.d.mts +14 -0
- package/dist/js/focus-trap/focus-trap.embedded.mjs +6 -10
- package/dist/js/index-EfkUSals.d.mts +139 -0
- package/dist/js/index.d.mts +5 -2
- package/dist/js/index.mjs +5 -2
- package/dist/js/internal/attributable.mjs +4 -3
- package/dist/js/internal/draggable.mjs +16 -22
- package/dist/js/internal/listbox/listbox.constants.d.mts +11 -0
- package/dist/js/internal/listbox/listbox.constants.mjs +23 -0
- package/dist/js/internal/listbox/listbox.data.d.mts +20 -0
- package/dist/js/internal/listbox/listbox.data.mjs +2 -0
- package/dist/js/internal/listbox/listbox.dom.d.mts +66 -0
- package/dist/js/internal/listbox/listbox.dom.mjs +2 -0
- package/dist/js/internal/listbox/listbox.event.d.mts +5 -0
- package/dist/js/internal/listbox/listbox.event.mjs +63 -0
- package/dist/js/internal/listbox/listbox.form.d.mts +6 -0
- package/dist/js/internal/listbox/listbox.form.mjs +52 -0
- package/dist/js/internal/listbox/listbox.misc.d.mts +12 -0
- package/dist/js/internal/listbox/listbox.misc.mjs +2 -0
- package/dist/js/internal/listbox/listbox.model.d.mts +2 -0
- package/dist/js/internal/listbox/listbox.model.mjs +1 -0
- package/dist/js/internal/listbox/listbox.state.d.mts +7 -0
- package/dist/js/internal/listbox/listbox.state.mjs +40 -0
- package/dist/js/internal/misc.d.mts +6 -0
- package/dist/js/internal/misc.mjs +13 -0
- package/dist/js/internal/navigable.d.mts +11 -5
- package/dist/js/internal/navigable.mjs +44 -18
- package/dist/js/l10n-Bn-7RcwR.d.mts +20 -0
- package/dist/js/l10n.d.mts +2 -20
- package/dist/js/listbox/constants.listbox.d.mts +2 -0
- package/dist/js/listbox/constants.listbox.mjs +40 -0
- package/dist/js/listbox/event.listbox.d.mts +8 -0
- package/dist/js/listbox/event.listbox.mjs +2 -0
- package/dist/js/listbox/index.d.mts +116 -0
- package/dist/js/listbox/index.mjs +189 -0
- package/dist/js/listbox/misc.listbox.d.mts +2 -0
- package/dist/js/listbox/misc.listbox.mjs +2 -0
- package/dist/js/listbox/state.listbox.d.mts +11 -0
- package/dist/js/listbox/state.listbox.mjs +2 -0
- package/dist/js/listbox.data-BWd0PCy7.mjs +424 -0
- package/dist/js/listbox.model-CRReGOmu.d.mts +125 -0
- package/dist/js/misc.listbox-BI-XsTOf.mjs +197 -0
- package/dist/js/misc.listbox-gnoJhrTr.d.mts +20 -0
- package/dist/js/movable/movable.embedded.mjs +3 -6
- package/dist/js/resizable/resizable.embedded.mjs +16 -26
- package/dist/js/sortable/sortable.embedded.mjs +4 -2
- package/dist/js/splitter.mjs +15 -12
- package/dist/js/state.combobox-4xq0FPCF.mjs +365 -0
- package/dist/js/tabs.d.mts +25 -0
- package/dist/js/tabs.mjs +240 -0
- package/dist/js/tooltip.mjs +13 -12
- package/package.json +7 -3
- package/src/css/badge.scss +6 -18
- package/src/css/button.scss +6 -45
- package/src/css/color.scss +20 -20
- package/src/css/combobox.scss +21 -52
- package/src/css/content.scss +30 -3
- package/src/css/floatable.scss +17 -17
- package/src/css/form.scss +84 -72
- package/src/css/listbox.scss +120 -0
- package/src/css/message.scss +13 -28
- package/src/css/mixins.scss +253 -28
- package/src/css/reset.scss +37 -86
- package/src/css/splitter.scss +2 -2
- package/src/css/tabs.scss +85 -0
- package/src/js/combobox/constants.combobox.ts +46 -25
- package/src/js/combobox/dom.combobox.ts +36 -454
- package/src/js/combobox/event.combobox.ts +129 -218
- package/src/js/combobox/index.ts +37 -30
- package/src/js/combobox/misc.combobox.ts +30 -114
- package/src/js/combobox/models.combobox.ts +7 -47
- package/src/js/combobox/state.combobox.ts +126 -127
- package/src/js/focus-trap/focus-trap.embedded.ts +11 -19
- package/src/js/index.ts +2 -0
- package/src/js/internal/draggable.ts +29 -33
- package/src/js/internal/listbox/listbox.constants.ts +25 -0
- package/src/js/{combobox/data.combobox.ts → internal/listbox/listbox.data.ts} +94 -43
- package/src/js/internal/listbox/listbox.dom.ts +445 -0
- package/src/js/internal/listbox/listbox.event.ts +129 -0
- package/src/js/{combobox/form.combobox.ts → internal/listbox/listbox.form.ts} +13 -14
- package/src/js/internal/listbox/listbox.misc.ts +135 -0
- package/src/js/internal/listbox/listbox.model.ts +169 -0
- package/src/js/internal/listbox/listbox.state.ts +82 -0
- package/src/js/internal/misc.ts +27 -0
- package/src/js/internal/navigable.ts +80 -23
- package/src/js/l10n.ts +8 -8
- package/src/js/listbox/constants.listbox.ts +61 -0
- package/src/js/listbox/event.listbox.ts +106 -0
- package/src/js/listbox/index.ts +302 -0
- package/src/js/listbox/misc.listbox.ts +70 -0
- package/src/js/listbox/state.listbox.ts +196 -0
- package/src/js/movable/movable.embedded.ts +11 -13
- package/src/js/resizable/resizable.embedded.ts +25 -39
- package/src/js/splitter.ts +21 -18
- package/src/js/tabs.ts +447 -0
- package/src/js/tooltip.ts +20 -17
- package/dist/js/combobox/data.combobox.d.mts +0 -18
- package/dist/js/combobox/data.combobox.mjs +0 -2
- package/dist/js/combobox/form.combobox.d.mts +0 -6
- package/dist/js/combobox/form.combobox.mjs +0 -2
- package/dist/js/constants.combobox-h_kyDegw.d.mts +0 -248
- package/dist/js/data.combobox-B8HMkDOh.mjs +0 -1030
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {setAria} from '@oscarpalmer/toretto/aria';
|
|
2
|
+
import {setAttribute} from '@oscarpalmer/toretto/attribute';
|
|
3
|
+
import {filterListbox} from './listbox.data';
|
|
4
|
+
import {updateListboxValidity} from './listbox.form';
|
|
5
|
+
import type {OuiListboxState} from './listbox.model';
|
|
6
|
+
|
|
7
|
+
// #region Functions
|
|
8
|
+
|
|
9
|
+
export function generateListboxIds(prefix: string, select: HTMLSelectElement): void {
|
|
10
|
+
const elements = [...select.children].flatMap(child =>
|
|
11
|
+
child instanceof HTMLOptGroupElement
|
|
12
|
+
? [
|
|
13
|
+
child,
|
|
14
|
+
...[...child.children].filter(groupChild => groupChild instanceof HTMLOptionElement),
|
|
15
|
+
]
|
|
16
|
+
: child instanceof HTMLOptionElement
|
|
17
|
+
? [child]
|
|
18
|
+
: [],
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const {length} = elements;
|
|
22
|
+
|
|
23
|
+
for (let index = 0; index < length; index += 1) {
|
|
24
|
+
const element = elements[index];
|
|
25
|
+
|
|
26
|
+
if (element.id === '') {
|
|
27
|
+
element.id = `${prefix}${element instanceof HTMLOptGroupElement ? '_group_' : '_option_'}${index}`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getListboxVisibleOptions(state: OuiListboxState): HTMLElement[] {
|
|
33
|
+
return state.options.all.filter(option => !option.hidden);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function setListboxDisabled<State extends OuiListboxState>(
|
|
37
|
+
state: State,
|
|
38
|
+
value: boolean,
|
|
39
|
+
): void {
|
|
40
|
+
state.filter.element.readOnly = state.readonly || value;
|
|
41
|
+
|
|
42
|
+
setProperties('disabled', state, value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function setListboxFilter<State extends OuiListboxState>(
|
|
46
|
+
state: State,
|
|
47
|
+
value: unknown,
|
|
48
|
+
): void {
|
|
49
|
+
if (typeof value !== 'string') {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const type: InputEvent['inputType'] =
|
|
54
|
+
value.length < state.filter.element.value.length ? 'deleteContentBackward' : 'insertText';
|
|
55
|
+
|
|
56
|
+
state.filter.element.value = value;
|
|
57
|
+
|
|
58
|
+
filterListbox(type, state);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function setListboxLimit<State extends OuiListboxState>(
|
|
62
|
+
type: 'max' | 'min',
|
|
63
|
+
state: State,
|
|
64
|
+
value?: unknown,
|
|
65
|
+
): void {
|
|
66
|
+
let actual = value;
|
|
67
|
+
|
|
68
|
+
if (typeof actual === 'string') {
|
|
69
|
+
actual = Number.parseInt(actual, 10);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const updateLimit =
|
|
73
|
+
typeof actual === 'number' && !Number.isNaN(actual) && actual >= 0
|
|
74
|
+
? actual !== state[type]
|
|
75
|
+
: actual == null;
|
|
76
|
+
|
|
77
|
+
if (updateLimit) {
|
|
78
|
+
state[type] = actual as number | undefined;
|
|
79
|
+
|
|
80
|
+
setAttribute(state.host, type, actual);
|
|
81
|
+
|
|
82
|
+
updateListboxValidity(state);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function setListboxMultiple(element: HTMLElement, value: boolean): void {
|
|
87
|
+
if (value) {
|
|
88
|
+
element.setAttribute('multiple', '');
|
|
89
|
+
} else {
|
|
90
|
+
element.removeAttribute('multiple');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function setProperties<State extends OuiListboxState>(
|
|
95
|
+
key: 'disabled' | 'readonly' | 'required',
|
|
96
|
+
state: State,
|
|
97
|
+
value: boolean,
|
|
98
|
+
): void {
|
|
99
|
+
state[key] = value;
|
|
100
|
+
|
|
101
|
+
const {length} = state.elements;
|
|
102
|
+
|
|
103
|
+
for (let index = 0; index < length; index += 1) {
|
|
104
|
+
const element = state.elements[index];
|
|
105
|
+
|
|
106
|
+
if (element == null) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setAria(
|
|
111
|
+
element,
|
|
112
|
+
key,
|
|
113
|
+
key !== 'readonly' || !(element instanceof HTMLButtonElement) ? value : undefined,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function setListboxReadonly<State extends OuiListboxState>(
|
|
119
|
+
state: State,
|
|
120
|
+
value: boolean,
|
|
121
|
+
): void {
|
|
122
|
+
state.filter.element.readOnly = state.disabled || value;
|
|
123
|
+
|
|
124
|
+
setProperties('readonly', state, value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function setListboxRequired<State extends OuiListboxState>(
|
|
128
|
+
state: State,
|
|
129
|
+
value: boolean,
|
|
130
|
+
): void {
|
|
131
|
+
setProperties('required', state, value);
|
|
132
|
+
updateListboxValidity(state);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// #endregion
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
|
|
2
|
+
import type {OuiLocalizations, OuiLocalizationsValues} from '../../l10n';
|
|
3
|
+
|
|
4
|
+
// #region Types
|
|
5
|
+
|
|
6
|
+
export type OuiListboxAttributes = {
|
|
7
|
+
active: string;
|
|
8
|
+
content: string;
|
|
9
|
+
empty: string;
|
|
10
|
+
filter: string;
|
|
11
|
+
group: string;
|
|
12
|
+
header: string;
|
|
13
|
+
hidden: string;
|
|
14
|
+
input: string;
|
|
15
|
+
list: string;
|
|
16
|
+
message: string;
|
|
17
|
+
option: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type OuiListboxAutoComplete = Record<OuiListboxAutoCompleteType, boolean> & {
|
|
21
|
+
value?: OuiListboxAutoCompleteType;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type OuiListboxAutoCompleteType = 'both' | 'inline' | 'list' | 'none';
|
|
25
|
+
|
|
26
|
+
export type OuiListboxCallbacks<State extends OuiListboxState = OuiListboxState> = {
|
|
27
|
+
data: OuiListboxCallbacksData<State>;
|
|
28
|
+
keydown: OuiListboxCallbacksKeydown<State>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type OuiListboxCallbacksData<State extends OuiListboxState = OuiListboxState> = {
|
|
32
|
+
onApply: OuiListboxCallbacksDataOnApply<State>;
|
|
33
|
+
onFilter: OuiListboxCallbacksDataOnFilter<State>;
|
|
34
|
+
onForm: (state: State) => void;
|
|
35
|
+
onPreview: OuiListboxCallbacksDataOnPreview<State>;
|
|
36
|
+
onToggle: (state: State) => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type OuiListboxCallbacksDataOnApply<State extends OuiListboxState = OuiListboxState> = (
|
|
40
|
+
state: State,
|
|
41
|
+
option: HTMLElement | undefined,
|
|
42
|
+
values: string[],
|
|
43
|
+
added: boolean,
|
|
44
|
+
selected?: HTMLElement[],
|
|
45
|
+
emit?: boolean,
|
|
46
|
+
) => void;
|
|
47
|
+
|
|
48
|
+
type OuiListboxCallbacksDataOnFilter<State extends OuiListboxState = OuiListboxState> = (
|
|
49
|
+
preview: OuiListboxPreview,
|
|
50
|
+
state: State,
|
|
51
|
+
update: boolean,
|
|
52
|
+
active?: HTMLElement,
|
|
53
|
+
emit?: boolean,
|
|
54
|
+
) => void;
|
|
55
|
+
|
|
56
|
+
type OuiListboxCallbacksDataOnPreview<State extends OuiListboxState = OuiListboxState> = (
|
|
57
|
+
preview: OuiListboxPreview,
|
|
58
|
+
state: State,
|
|
59
|
+
active?: HTMLElement,
|
|
60
|
+
) => void;
|
|
61
|
+
|
|
62
|
+
type OuiListboxCallbacksKeydown<State extends OuiListboxState = OuiListboxState> = {
|
|
63
|
+
onActivation?: (state: State) => void;
|
|
64
|
+
onEnter?: (state: State) => void;
|
|
65
|
+
onEscape?: (state: State) => void;
|
|
66
|
+
onNavigation: OuiListboxCallbacksDataOnNavigation<State>;
|
|
67
|
+
onSelection?: (state: State) => void;
|
|
68
|
+
onTab?: (state: State) => void;
|
|
69
|
+
onToggle?: (state: State) => void;
|
|
70
|
+
preventActivation?: (state: State) => boolean;
|
|
71
|
+
skipNavigation?: (state: State) => boolean;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type OuiListboxCallbacksDataOnNavigation<State extends OuiListboxState = OuiListboxState> = (
|
|
75
|
+
preview: OuiListboxPreview,
|
|
76
|
+
state: State,
|
|
77
|
+
active?: HTMLElement,
|
|
78
|
+
) => void;
|
|
79
|
+
|
|
80
|
+
type OuiListboxFilter = {
|
|
81
|
+
element: HTMLInputElement;
|
|
82
|
+
value: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type OuiListboxFooter = {
|
|
86
|
+
container: HTMLElement;
|
|
87
|
+
message: HTMLElement;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type OuiListboxGroup = {
|
|
91
|
+
element: HTMLElement;
|
|
92
|
+
header: HTMLElement;
|
|
93
|
+
options: HTMLElement[];
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
type OuiListboxLabel = {
|
|
97
|
+
element: HTMLLabelElement;
|
|
98
|
+
id: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
type OuiListboxLocalization<Localizations extends OuiLocalizationsValues> = {
|
|
102
|
+
component: Record<string, Partial<Localizations>>;
|
|
103
|
+
global: OuiLocalizations<Localizations>;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type OuiListboxLocalizations = {
|
|
107
|
+
empty: string | (() => string);
|
|
108
|
+
input: string | (() => string);
|
|
109
|
+
invalid: string | (() => string);
|
|
110
|
+
max: string | ((max: number) => string);
|
|
111
|
+
min: string | ((min: number) => string);
|
|
112
|
+
remove: string | ((label: string, value: string) => string);
|
|
113
|
+
results: string | ((amount: number) => string);
|
|
114
|
+
selected: string | ((amount: number) => string);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type OuiListboxOptions = {
|
|
118
|
+
all: HTMLElement[];
|
|
119
|
+
selected: HTMLElement[];
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type OuiListboxPreview = 'none' | 'set' | 'typed';
|
|
123
|
+
|
|
124
|
+
export type OuiListboxState = {
|
|
125
|
+
active?: HTMLElement;
|
|
126
|
+
attributes: OuiListboxAttributes;
|
|
127
|
+
autocomplete: OuiListboxAutoComplete;
|
|
128
|
+
callbacks: OuiListboxCallbacks;
|
|
129
|
+
content: HTMLDivElement;
|
|
130
|
+
disabled: boolean;
|
|
131
|
+
elements: Array<HTMLElement | undefined>;
|
|
132
|
+
filter: OuiListboxFilter;
|
|
133
|
+
footer?: OuiListboxFooter;
|
|
134
|
+
groups?: OuiListboxGroup[];
|
|
135
|
+
host: HTMLElement;
|
|
136
|
+
id: string;
|
|
137
|
+
internals: ElementInternals;
|
|
138
|
+
label: OuiListboxLabel;
|
|
139
|
+
list: HTMLUListElement;
|
|
140
|
+
listbox: boolean;
|
|
141
|
+
listeners: RemovableEventListener[];
|
|
142
|
+
localizations: OuiListboxLocalization<OuiListboxLocalizations>;
|
|
143
|
+
max?: number;
|
|
144
|
+
min?: number;
|
|
145
|
+
message: HTMLElement;
|
|
146
|
+
multiple: boolean;
|
|
147
|
+
name: string;
|
|
148
|
+
options: OuiListboxOptions;
|
|
149
|
+
readonly: boolean;
|
|
150
|
+
required: boolean;
|
|
151
|
+
select: HTMLSelectElement;
|
|
152
|
+
type: OuiListboxType;
|
|
153
|
+
values: OuiListboxValues;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export type OuiListboxStates<State extends OuiListboxState = OuiListboxState> = {
|
|
157
|
+
map: WeakMap<HTMLElement, State>;
|
|
158
|
+
set: Set<State>;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export type OuiListboxType = 'autocomplete' | 'select';
|
|
162
|
+
|
|
163
|
+
type OuiListboxValues = {
|
|
164
|
+
current: string[];
|
|
165
|
+
initial: string[];
|
|
166
|
+
label: HTMLElement;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// #endregion
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {getAttribute} from '@oscarpalmer/toretto/attribute';
|
|
2
|
+
import {filterListboxOptions} from './listbox.data';
|
|
3
|
+
import {updateListboxMessage, updateListboxPreview, updateListboxValueLabel} from './listbox.dom';
|
|
4
|
+
import {updateListboxForm} from './listbox.form';
|
|
5
|
+
import {
|
|
6
|
+
setListboxDisabled,
|
|
7
|
+
setListboxLimit,
|
|
8
|
+
setListboxMultiple,
|
|
9
|
+
setListboxReadonly,
|
|
10
|
+
setListboxRequired,
|
|
11
|
+
} from './listbox.misc';
|
|
12
|
+
import type {OuiListboxState, OuiListboxStates} from './listbox.model';
|
|
13
|
+
|
|
14
|
+
// #region Functions
|
|
15
|
+
|
|
16
|
+
export function attachListbox<State extends OuiListboxState>(
|
|
17
|
+
state: State,
|
|
18
|
+
states: OuiListboxStates<State>,
|
|
19
|
+
onAttach: (state: State) => void,
|
|
20
|
+
): void {
|
|
21
|
+
const {filter, host, label} = state;
|
|
22
|
+
|
|
23
|
+
label.element.htmlFor = filter.element.id;
|
|
24
|
+
label.element.id = `${state.id}_label`;
|
|
25
|
+
|
|
26
|
+
onAttach(state);
|
|
27
|
+
|
|
28
|
+
states.map.set(host, state);
|
|
29
|
+
states.set.add(state);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function detachListbox<State extends OuiListboxState>(
|
|
33
|
+
state: State,
|
|
34
|
+
states: OuiListboxStates<State>,
|
|
35
|
+
onDetach: (state: State) => void,
|
|
36
|
+
): void {
|
|
37
|
+
const {host, label, select} = state;
|
|
38
|
+
|
|
39
|
+
const listeners = state.listeners.splice(0);
|
|
40
|
+
const {length} = listeners;
|
|
41
|
+
|
|
42
|
+
for (let index = 0; index < length; index += 1) {
|
|
43
|
+
listeners[index]();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
label.element.htmlFor = select.id;
|
|
47
|
+
label.element.id = label.id;
|
|
48
|
+
|
|
49
|
+
onDetach(state);
|
|
50
|
+
|
|
51
|
+
states.map.delete(host);
|
|
52
|
+
states.set.delete(state);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function initializeListboxState<State extends OuiListboxState>(
|
|
56
|
+
state: State,
|
|
57
|
+
states: OuiListboxStates<State>,
|
|
58
|
+
onAttach: (state: State) => void,
|
|
59
|
+
): State {
|
|
60
|
+
attachListbox(state, states, onAttach);
|
|
61
|
+
|
|
62
|
+
setListboxDisabled(state, state.disabled);
|
|
63
|
+
setListboxLimit('max', state, getAttribute(state.host, 'max'));
|
|
64
|
+
setListboxLimit('min', state, getAttribute(state.host, 'min'));
|
|
65
|
+
setListboxMultiple(state.host, state.multiple);
|
|
66
|
+
setListboxReadonly(state, state.readonly);
|
|
67
|
+
setListboxRequired(state, state.required);
|
|
68
|
+
|
|
69
|
+
filterListboxOptions(state);
|
|
70
|
+
|
|
71
|
+
updateListboxForm(state, state.values.current, false);
|
|
72
|
+
updateListboxMessage(state);
|
|
73
|
+
updateListboxValueLabel(state);
|
|
74
|
+
|
|
75
|
+
if (state.options.selected.length > 0) {
|
|
76
|
+
updateListboxPreview('none', state, state.options.selected[0], false);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return state;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// #endregion
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {getAria} from '@oscarpalmer/toretto/aria';
|
|
2
|
+
import {getAttribute} from '@oscarpalmer/toretto/attribute';
|
|
3
|
+
import type {AnyAriaBooleanAttribute} from '@oscarpalmer/toretto/models';
|
|
4
|
+
|
|
5
|
+
// #region Functions
|
|
6
|
+
|
|
7
|
+
export function getAttributeValue<Value>(
|
|
8
|
+
element: HTMLElement,
|
|
9
|
+
attribute: string,
|
|
10
|
+
possible: Set<Value>,
|
|
11
|
+
defaultValue: Value,
|
|
12
|
+
): Value {
|
|
13
|
+
const value = getAttribute(element, attribute);
|
|
14
|
+
|
|
15
|
+
return possible.has(value as Value) ? (value as Value) : defaultValue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getBooleanAriaValue(
|
|
19
|
+
element: HTMLElement,
|
|
20
|
+
attribute: AnyAriaBooleanAttribute,
|
|
21
|
+
): boolean {
|
|
22
|
+
const value = getAria(element, attribute);
|
|
23
|
+
|
|
24
|
+
return value === true || value === 'true';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// #endregion
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {clamp} from '@oscarpalmer/atoms/number';
|
|
2
|
-
import {getAria, setRole} from '@oscarpalmer/toretto/aria';
|
|
3
|
-
import {
|
|
2
|
+
import {getAria, setAria, setRole} from '@oscarpalmer/toretto/aria';
|
|
3
|
+
import {getAttribute} from '@oscarpalmer/toretto/attribute';
|
|
4
|
+
import {on} from '@oscarpalmer/toretto/event';
|
|
4
5
|
import {findAncestor} from '@oscarpalmer/toretto/find';
|
|
5
6
|
import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
|
|
6
7
|
import type {AriaRole, RemovableEventListener} from '@oscarpalmer/toretto/models';
|
|
@@ -25,6 +26,16 @@ export class OuiNavigable {
|
|
|
25
26
|
this.options = options;
|
|
26
27
|
|
|
27
28
|
this.listeners = [
|
|
29
|
+
on(
|
|
30
|
+
element,
|
|
31
|
+
'click',
|
|
32
|
+
event => {
|
|
33
|
+
onClick(this, event);
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
passive: false,
|
|
37
|
+
},
|
|
38
|
+
),
|
|
28
39
|
on(element, 'focusin', event => {
|
|
29
40
|
onFocusin(this, event);
|
|
30
41
|
}),
|
|
@@ -40,7 +51,9 @@ export class OuiNavigable {
|
|
|
40
51
|
),
|
|
41
52
|
];
|
|
42
53
|
|
|
43
|
-
this.orientation =
|
|
54
|
+
this.orientation = options?.orientation ?? getNavigableOrientation(element, options.vertical);
|
|
55
|
+
|
|
56
|
+
setAria(element, 'orientation', this.orientation);
|
|
44
57
|
|
|
45
58
|
if (options.role != null) {
|
|
46
59
|
setRole(element, options.role);
|
|
@@ -57,6 +70,14 @@ export class OuiNavigable {
|
|
|
57
70
|
this.active.element = undefined;
|
|
58
71
|
this.element = undefined as never;
|
|
59
72
|
}
|
|
73
|
+
|
|
74
|
+
navigate(index: number, focus?: boolean): void {
|
|
75
|
+
const items = getItems(this.element, this.options.selector);
|
|
76
|
+
|
|
77
|
+
if (index >= 0 && index < items.length) {
|
|
78
|
+
onNavigate(this, index, items, focus ?? false);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
type OuiNavigableActive = {
|
|
@@ -64,17 +85,25 @@ type OuiNavigableActive = {
|
|
|
64
85
|
index: number;
|
|
65
86
|
};
|
|
66
87
|
|
|
88
|
+
type OuiNavigableCallback<ReturnType> = (
|
|
89
|
+
event: KeyboardEvent,
|
|
90
|
+
navigable: OuiNavigable,
|
|
91
|
+
) => ReturnType;
|
|
92
|
+
|
|
67
93
|
export type OuiNavigableOptions = {
|
|
94
|
+
orientation?: OuiNavigableOrientation;
|
|
68
95
|
role?: AriaRole;
|
|
69
96
|
selector: string;
|
|
70
97
|
vertical: boolean;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
98
|
+
onActivate?: (navigable: OuiNavigable) => void;
|
|
99
|
+
onDeactivate?: OuiNavigableCallback<void>;
|
|
100
|
+
onNavigation?: (navigable: OuiNavigable) => void;
|
|
101
|
+
onPreventNavigation?: OuiNavigableCallback<boolean>;
|
|
102
|
+
onShouldActivate?: OuiNavigableCallback<boolean>;
|
|
103
|
+
onShouldDeactivate?: OuiNavigableCallback<boolean | undefined>;
|
|
75
104
|
};
|
|
76
105
|
|
|
77
|
-
type OuiNavigableOrientation = 'horizontal' | 'vertical';
|
|
106
|
+
export type OuiNavigableOrientation = 'horizontal' | 'vertical';
|
|
78
107
|
|
|
79
108
|
// #endregion
|
|
80
109
|
|
|
@@ -88,15 +117,17 @@ export function getNavigable(element: HTMLElement, options: OuiNavigableOptions)
|
|
|
88
117
|
return new OuiNavigable(element, options);
|
|
89
118
|
}
|
|
90
119
|
|
|
91
|
-
function
|
|
120
|
+
export function getNavigableOrientation(
|
|
92
121
|
element: HTMLElement,
|
|
93
|
-
|
|
122
|
+
vertical: boolean,
|
|
123
|
+
aria?: boolean,
|
|
94
124
|
): OuiNavigableOrientation {
|
|
95
|
-
const attribute =
|
|
125
|
+
const attribute =
|
|
126
|
+
(aria ?? true) ? getAria(element, 'orientation') : getAttribute(element, 'orientation');
|
|
96
127
|
|
|
97
128
|
return ORIENTATIONS.has(attribute as OuiNavigableOrientation)
|
|
98
129
|
? (attribute as OuiNavigableOrientation)
|
|
99
|
-
:
|
|
130
|
+
: vertical
|
|
100
131
|
? ORIENTATION_VERTICAL
|
|
101
132
|
: ORIENTATION_HORIZONTAL;
|
|
102
133
|
}
|
|
@@ -105,6 +136,39 @@ function isValidItem(element: Element): element is HTMLElement {
|
|
|
105
136
|
return 'disabled' in element ? element.disabled !== true : true;
|
|
106
137
|
}
|
|
107
138
|
|
|
139
|
+
function onClick(navigable: OuiNavigable, event: Event): void {
|
|
140
|
+
const item = findAncestor(event, navigable.options.selector) as HTMLElement;
|
|
141
|
+
const items = getItems(navigable.element, navigable.options.selector);
|
|
142
|
+
|
|
143
|
+
const index = items.indexOf(item);
|
|
144
|
+
|
|
145
|
+
if (index === -1) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (index !== navigable.active.index) {
|
|
150
|
+
onNavigate(navigable, index, items, false);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
navigable.options.onActivate?.(navigable);
|
|
154
|
+
|
|
155
|
+
item.focus();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function onNavigate(
|
|
159
|
+
navigable: OuiNavigable,
|
|
160
|
+
index: number,
|
|
161
|
+
items: HTMLElement[],
|
|
162
|
+
focus: boolean,
|
|
163
|
+
): void {
|
|
164
|
+
navigable.active.element = items[index];
|
|
165
|
+
navigable.active.index = index;
|
|
166
|
+
|
|
167
|
+
updateNavigableFocus(navigable, focus, items);
|
|
168
|
+
|
|
169
|
+
navigable.options.onNavigation?.(navigable);
|
|
170
|
+
}
|
|
171
|
+
|
|
108
172
|
function onFocusin(navigable: OuiNavigable, event: FocusEvent): void {
|
|
109
173
|
const item = findAncestor(event, navigable.options.selector);
|
|
110
174
|
|
|
@@ -155,6 +219,8 @@ function onKeydown(navigable: OuiNavigable, event: KeyboardEvent): void {
|
|
|
155
219
|
}
|
|
156
220
|
|
|
157
221
|
if (mustActivate || shouldActivate) {
|
|
222
|
+
navigable.options.onActivate?.(navigable);
|
|
223
|
+
|
|
158
224
|
return;
|
|
159
225
|
}
|
|
160
226
|
|
|
@@ -188,14 +254,9 @@ function onKeydown(navigable: OuiNavigable, event: KeyboardEvent): void {
|
|
|
188
254
|
}
|
|
189
255
|
}
|
|
190
256
|
|
|
191
|
-
if (index
|
|
192
|
-
|
|
257
|
+
if (index != null && index !== navigable.active.index) {
|
|
258
|
+
onNavigate(navigable, index, items, true);
|
|
193
259
|
}
|
|
194
|
-
|
|
195
|
-
navigable.active.element = items[index];
|
|
196
|
-
navigable.active.index = index;
|
|
197
|
-
|
|
198
|
-
updateNavigableFocus(navigable, true, items);
|
|
199
260
|
}
|
|
200
261
|
|
|
201
262
|
export function updateNavigableFocus(
|
|
@@ -219,10 +280,6 @@ export function updateNavigableFocus(
|
|
|
219
280
|
if (focus) {
|
|
220
281
|
navigable.active.element?.focus();
|
|
221
282
|
}
|
|
222
|
-
|
|
223
|
-
dispatch(navigable.active.element, 'navigable:change', {
|
|
224
|
-
detail: {...navigable.active},
|
|
225
|
-
});
|
|
226
283
|
}
|
|
227
284
|
|
|
228
285
|
// #endregion
|
package/src/js/l10n.ts
CHANGED
|
@@ -7,9 +7,7 @@ type LocalizationParameters<Localization> = Localization extends GenericCallback
|
|
|
7
7
|
? Parameters<Localization>
|
|
8
8
|
: never;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export class OuiLocalizations<Localizations extends OuiLocalizationValues> {
|
|
10
|
+
export class OuiLocalizations<Localizations extends OuiLocalizationsValues> {
|
|
13
11
|
readonly defaults: Localizations;
|
|
14
12
|
|
|
15
13
|
readonly keys: Set<keyof Localizations>;
|
|
@@ -49,6 +47,8 @@ export class OuiLocalizations<Localizations extends OuiLocalizationValues> {
|
|
|
49
47
|
}
|
|
50
48
|
}
|
|
51
49
|
|
|
50
|
+
export type OuiLocalizationsValues = Record<string, string | GenericCallback>;
|
|
51
|
+
|
|
52
52
|
// #endregion
|
|
53
53
|
|
|
54
54
|
// #region Functions
|
|
@@ -60,7 +60,7 @@ export function getLanguage(element: Element): string {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export function getLocalization<
|
|
63
|
-
Localizations extends
|
|
63
|
+
Localizations extends OuiLocalizationsValues,
|
|
64
64
|
Key extends keyof Localizations,
|
|
65
65
|
>(
|
|
66
66
|
localizations: OuiLocalizations<Localizations>,
|
|
@@ -101,14 +101,14 @@ export function getLocalization<
|
|
|
101
101
|
return localization ?? localizations.defaults[key];
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
export function getLocalizations<Localizations extends
|
|
104
|
+
export function getLocalizations<Localizations extends OuiLocalizationsValues>(
|
|
105
105
|
defaults: Localizations,
|
|
106
106
|
): OuiLocalizations<Localizations> {
|
|
107
107
|
return new OuiLocalizations(defaults);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
export function localize<
|
|
111
|
-
Localizations extends
|
|
111
|
+
Localizations extends OuiLocalizationsValues,
|
|
112
112
|
Key extends keyof Localizations,
|
|
113
113
|
Localization extends Localizations[Key],
|
|
114
114
|
>(
|
|
@@ -135,10 +135,10 @@ export function localize<
|
|
|
135
135
|
return localized;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
export function updateLocalizations<Localizations extends
|
|
138
|
+
export function updateLocalizations<Localizations extends OuiLocalizationsValues>(
|
|
139
139
|
instance: OuiLocalizations<Localizations>,
|
|
140
140
|
values: Record<string, Partial<Localizations>>,
|
|
141
|
-
destination
|
|
141
|
+
destination: Record<string, Partial<Localizations>>,
|
|
142
142
|
onAfter?: () => void,
|
|
143
143
|
): void {
|
|
144
144
|
if (typeof values !== 'object' || values == null) {
|