@oscarpalmer/oui 0.27.0 → 0.29.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/floatable.css +9 -0
- package/dist/js/dialog.d.mts +3 -1
- package/dist/js/dialog.mjs +35 -19
- package/dist/js/{draggable-zszaW1qS.d.mts → draggable-fIYXd8eO.d.mts} +3 -3
- package/dist/js/floatable/floatable.embedded.d.mts +2 -2
- package/dist/js/floatable/floatable.embedded.mjs +17 -16
- package/dist/js/floatable/floatable.standalone.d.mts +1 -1
- package/dist/js/{floatable.embedded-YxGwzgbE.d.mts → floatable.embedded-DUR8bQBK.d.mts} +1 -2
- package/dist/js/focus-trap/focus-trap.embedded.mjs +6 -5
- package/dist/js/index.d.mts +4 -3
- package/dist/js/index.mjs +6 -2
- package/dist/js/internal/attributable.mjs +3 -4
- package/dist/js/internal/draggable.d.mts +1 -1
- package/dist/js/internal/draggable.mjs +12 -9
- package/dist/js/internal/navigable.d.mts +32 -0
- package/dist/js/internal/navigable.mjs +107 -0
- package/dist/js/menu.d.mts +1 -0
- package/dist/js/menu.mjs +168 -0
- package/dist/js/movable/movable.embedded.d.mts +1 -1
- package/dist/js/movable/movable.embedded.mjs +8 -13
- package/dist/js/movable/movable.standalone.d.mts +1 -1
- package/dist/js/popover.d.mts +1 -1
- package/dist/js/popover.mjs +27 -12
- package/dist/js/resizable/resizable.embedded.d.mts +2 -0
- package/dist/js/resizable/resizable.embedded.mjs +191 -0
- package/dist/js/resizable/resizable.standalone.d.mts +2 -0
- package/dist/js/resizable/resizable.standalone.mjs +2 -0
- package/dist/js/resizable.embedded-CXrUVPzR.d.mts +35 -0
- package/dist/js/sortable/sortable.embedded.d.mts +1 -1
- package/dist/js/sortable/sortable.embedded.mjs +15 -13
- package/dist/js/sortable/sortable.standalone.d.mts +1 -1
- package/dist/js/splitter.mjs +30 -26
- package/dist/js/toolbar.mjs +12 -88
- package/dist/js/tooltip.d.mts +1 -1
- package/dist/js/tooltip.mjs +12 -17
- package/package.json +11 -3
- package/src/css/floatable.scss +9 -0
- package/src/css/form.scss +6 -6
- package/src/js/dialog.ts +50 -24
- package/src/js/floatable/floatable.embedded.ts +19 -18
- package/src/js/focus-trap/focus-trap.embedded.ts +6 -5
- package/src/js/index.ts +3 -1
- package/src/js/internal/attributable.ts +3 -5
- package/src/js/internal/draggable.ts +13 -10
- package/src/js/internal/navigable.ts +247 -0
- package/src/js/menu.ts +345 -0
- package/src/js/movable/movable.embedded.ts +10 -13
- package/src/js/popover.ts +41 -15
- package/src/js/resizable/resizable.embedded.ts +390 -0
- package/src/js/resizable/resizable.standalone.ts +5 -0
- package/src/js/sortable/sortable.embedded.ts +17 -22
- package/src/js/splitter.ts +41 -36
- package/src/js/toolbar.ts +27 -178
- package/src/js/tooltip.ts +18 -30
package/src/js/menu.ts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import {setAria, setRole} from '@oscarpalmer/toretto/aria';
|
|
2
|
+
import {setAttribute} from '@oscarpalmer/toretto/attribute';
|
|
3
|
+
import {on} from '@oscarpalmer/toretto/event';
|
|
4
|
+
import {findAncestor} from '@oscarpalmer/toretto/find';
|
|
5
|
+
import {createEmbeddedFloatable, getOnBeforeToggleListener} from './floatable/floatable.embedded';
|
|
6
|
+
import type {OuiFloatable} from './floatable/floatable.standalone';
|
|
7
|
+
import {attributable} from './internal/attributable';
|
|
8
|
+
import {
|
|
9
|
+
NAVIGABLE_KEYS_HORIZONTAL,
|
|
10
|
+
NAVIGABLE_KEYS_VERTICAL,
|
|
11
|
+
type OuiNavigable,
|
|
12
|
+
type OuiNavigableOptions,
|
|
13
|
+
getNavigable,
|
|
14
|
+
updateNavigableFocus,
|
|
15
|
+
} from './internal/navigable';
|
|
16
|
+
|
|
17
|
+
// #region Types
|
|
18
|
+
|
|
19
|
+
type OuiBasicMenu = {
|
|
20
|
+
destroy(): void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type OuiBasicMenuOptions = Pick<OuiNavigableOptions, 'role' | 'selector' | 'vertical'>;
|
|
24
|
+
|
|
25
|
+
class OuiMenu {
|
|
26
|
+
content: HTMLElement;
|
|
27
|
+
|
|
28
|
+
element: HTMLElement;
|
|
29
|
+
|
|
30
|
+
floatable: OuiFloatable;
|
|
31
|
+
|
|
32
|
+
navigable: OuiNavigable;
|
|
33
|
+
|
|
34
|
+
toggle: HTMLElement;
|
|
35
|
+
|
|
36
|
+
get open(): boolean {
|
|
37
|
+
return this.floatable.open;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
constructor(element: HTMLElement, navigable: OuiNavigable) {
|
|
41
|
+
this.element = element;
|
|
42
|
+
this.navigable = navigable;
|
|
43
|
+
|
|
44
|
+
this.content = navigable.element;
|
|
45
|
+
|
|
46
|
+
this.toggle = element.querySelector(SELECTOR_MENU_TOGGLE)!;
|
|
47
|
+
|
|
48
|
+
this.floatable = createEmbeddedFloatable(this.toggle, this.content, {
|
|
49
|
+
attribute: ATTRIBUTE_MENU_POSITION,
|
|
50
|
+
position:
|
|
51
|
+
findAncestor(element.parentElement!, SELECTOR_MENU_CONTENT) == null
|
|
52
|
+
? 'below-start'
|
|
53
|
+
: 'end-top',
|
|
54
|
+
reusable: false,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
this.navigable.listeners.push(
|
|
58
|
+
getOnBeforeToggleListener(this.content),
|
|
59
|
+
on(this.content, 'click', event => {
|
|
60
|
+
event.stopPropagation();
|
|
61
|
+
|
|
62
|
+
const toggle = findAncestor(event, SELECTOR_MENU_TOGGLE);
|
|
63
|
+
|
|
64
|
+
if (toggle != null) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const elements = document.querySelectorAll(SELECTOR_MENU_CONTENT);
|
|
69
|
+
|
|
70
|
+
for (const element of elements) {
|
|
71
|
+
menus.get(element as HTMLElement)?.floatable.hide();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const menu = menus.get(elements[0] as HTMLElement);
|
|
75
|
+
|
|
76
|
+
if (menu != null && event.pointerId === -1) {
|
|
77
|
+
updateNavigableFocus(menu.navigable, true);
|
|
78
|
+
}
|
|
79
|
+
}),
|
|
80
|
+
on(this.content, 'toggle', event => {
|
|
81
|
+
const expanded = event.newState === 'open';
|
|
82
|
+
|
|
83
|
+
if (expanded) {
|
|
84
|
+
this.floatable?.update();
|
|
85
|
+
|
|
86
|
+
updateNavigableFocus(this.navigable, true);
|
|
87
|
+
} else {
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setAria(this.toggle, 'expanded', expanded);
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
this.floatable.update();
|
|
95
|
+
|
|
96
|
+
initializeMenu(this);
|
|
97
|
+
|
|
98
|
+
updateNavigableFocus(this.navigable, false);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
destroy(): void {
|
|
102
|
+
this.floatable.destroy();
|
|
103
|
+
this.navigable.destroy();
|
|
104
|
+
|
|
105
|
+
this.floatable = undefined as never;
|
|
106
|
+
this.navigable = undefined as never;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class OuiMenubar {
|
|
111
|
+
element: HTMLElement;
|
|
112
|
+
|
|
113
|
+
navigable: OuiNavigable;
|
|
114
|
+
|
|
115
|
+
constructor(element: HTMLElement, navigable: OuiNavigable) {
|
|
116
|
+
this.element = element;
|
|
117
|
+
this.navigable = navigable;
|
|
118
|
+
|
|
119
|
+
updateNavigableFocus(this.navigable, false);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
destroy(): void {
|
|
123
|
+
this.navigable.destroy();
|
|
124
|
+
|
|
125
|
+
this.navigable = undefined as never;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// #endregion
|
|
130
|
+
|
|
131
|
+
// #region Functions
|
|
132
|
+
|
|
133
|
+
function addInstance<Instance>(
|
|
134
|
+
instances: WeakMap<HTMLElement, Instance>,
|
|
135
|
+
ctor: new (element: HTMLElement, navigable: OuiNavigable) => Instance,
|
|
136
|
+
element: HTMLElement,
|
|
137
|
+
content: HTMLElement,
|
|
138
|
+
options: OuiBasicMenuOptions,
|
|
139
|
+
): void {
|
|
140
|
+
if (!instances.has(content)) {
|
|
141
|
+
instances.set(
|
|
142
|
+
content,
|
|
143
|
+
new ctor(
|
|
144
|
+
element,
|
|
145
|
+
getNavigable(content, {
|
|
146
|
+
...options,
|
|
147
|
+
onDeactivate,
|
|
148
|
+
onPreventNavigation,
|
|
149
|
+
onShouldActivate,
|
|
150
|
+
onShouldDeactivate,
|
|
151
|
+
}),
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function addMenu(element: HTMLElement): void {
|
|
158
|
+
const content = element.querySelector(SELECTOR_MENU_CONTENT);
|
|
159
|
+
const toggle = element.querySelector(SELECTOR_MENU_TOGGLE);
|
|
160
|
+
|
|
161
|
+
if (content != null && toggle != null) {
|
|
162
|
+
addInstance(menus, OuiMenu, element, content as never, {
|
|
163
|
+
selector: SELECTOR_MENU_ITEM,
|
|
164
|
+
vertical: true,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function addMenubar(element: HTMLElement): void {
|
|
170
|
+
addInstance(menubars, OuiMenubar, element, element, {
|
|
171
|
+
role: 'menubar',
|
|
172
|
+
selector: SELECTOR_MENUBAR_ITEM,
|
|
173
|
+
vertical: false,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getId(element: HTMLElement, value?: string): string {
|
|
178
|
+
let {id} = element;
|
|
179
|
+
|
|
180
|
+
if (id == null || id.trim().length === 0) {
|
|
181
|
+
id = value == null ? `oui_menu_${++index}` : `${value}_toggle`;
|
|
182
|
+
|
|
183
|
+
element.id = id;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return id;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function initializeMenu(menu: OuiMenu): void {
|
|
190
|
+
const id = getId(menu.content);
|
|
191
|
+
|
|
192
|
+
setAttribute(menu.toggle, 'popovertarget', id);
|
|
193
|
+
|
|
194
|
+
setAria(menu.toggle, {
|
|
195
|
+
controls: id,
|
|
196
|
+
expanded: 'false',
|
|
197
|
+
haspopup: 'menu',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
setAttribute(menu.content, 'tabindex', '-1');
|
|
201
|
+
|
|
202
|
+
setRole(menu.content, 'menu');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function onDeactivate(_: KeyboardEvent, navigable: OuiNavigable): void {
|
|
206
|
+
const menu = menus.get(navigable.element);
|
|
207
|
+
|
|
208
|
+
if (menu == null) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
menu.floatable.hide();
|
|
213
|
+
|
|
214
|
+
const element = findAncestor(menu.element.parentElement!, SELECTORS) as HTMLElement | null;
|
|
215
|
+
|
|
216
|
+
if (element == null) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const instance = menubars.get(element) ?? menus.get(element);
|
|
221
|
+
|
|
222
|
+
if (instance != null) {
|
|
223
|
+
updateNavigableFocus(instance.navigable, true);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function onPreventNavigation(_: KeyboardEvent, navigable: OuiNavigable): boolean {
|
|
228
|
+
const instance = menubars.get(navigable.element) ?? menus.get(navigable.element);
|
|
229
|
+
|
|
230
|
+
return findAncestor(instance?.element.parentElement!, SELECTORS) != null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function onShouldActivate(event: KeyboardEvent, navigable: OuiNavigable): boolean {
|
|
234
|
+
let activate = false;
|
|
235
|
+
|
|
236
|
+
if (menubars.has(navigable.element)) {
|
|
237
|
+
activate =
|
|
238
|
+
navigable.orientation === 'horizontal'
|
|
239
|
+
? NAVIGABLE_KEYS_VERTICAL.has(event.key)
|
|
240
|
+
: NAVIGABLE_KEYS_HORIZONTAL.has(event.key);
|
|
241
|
+
} else if (menus.has(navigable.element)) {
|
|
242
|
+
activate =
|
|
243
|
+
navigable.orientation === 'horizontal'
|
|
244
|
+
? NAVIGABLE_KEYS_VERTICAL.has(event.key)
|
|
245
|
+
: event.key === 'ArrowRight';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (activate) {
|
|
249
|
+
const target =
|
|
250
|
+
event.target instanceof HTMLElement && event.target.matches(SELECTOR_MENU_TOGGLE)
|
|
251
|
+
? event.target
|
|
252
|
+
: undefined;
|
|
253
|
+
|
|
254
|
+
if (target == null) {
|
|
255
|
+
activate = false;
|
|
256
|
+
} else {
|
|
257
|
+
target.click();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return activate;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function onShouldDeactivate(event: KeyboardEvent, navigable: OuiNavigable): boolean | undefined {
|
|
265
|
+
if (menubars.has(navigable.element)) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const menu = menus.get(navigable.element);
|
|
270
|
+
|
|
271
|
+
if (menu == null) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const horizontal = navigable.orientation === 'horizontal';
|
|
276
|
+
|
|
277
|
+
const altKey = horizontal ? 'ArrowDown' : 'ArrowRight';
|
|
278
|
+
const baseKey = horizontal ? 'ArrowUp' : 'ArrowLeft';
|
|
279
|
+
|
|
280
|
+
if (event.key === baseKey) {
|
|
281
|
+
return findAncestor(menu.element.parentElement!, SELECTOR_MENU_CONTENT) != null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (
|
|
285
|
+
event.key === altKey &&
|
|
286
|
+
event.target instanceof HTMLElement &&
|
|
287
|
+
!event.target.hasAttribute('popovertarget')
|
|
288
|
+
) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function removeInstance<Instance extends OuiBasicMenu>(
|
|
294
|
+
instances: WeakMap<HTMLElement, Instance>,
|
|
295
|
+
element: HTMLElement,
|
|
296
|
+
): void {
|
|
297
|
+
instances.get(element)?.destroy();
|
|
298
|
+
|
|
299
|
+
instances.delete(element);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function removeMenu(element: HTMLElement): void {
|
|
303
|
+
removeInstance(menus, element);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function removeMenubar(element: HTMLElement): void {
|
|
307
|
+
removeInstance(menubars, element);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// #endregion
|
|
311
|
+
|
|
312
|
+
// #region Variables
|
|
313
|
+
|
|
314
|
+
const ATTRIBUTE_MENU = 'oui-menu';
|
|
315
|
+
|
|
316
|
+
const ATTRIBUTE_MENU_POSITION = 'oui-menu-position';
|
|
317
|
+
|
|
318
|
+
const ATTRIBUTE_MENUBAR = 'oui-menubar';
|
|
319
|
+
|
|
320
|
+
const SELECTOR_MENUBAR = `[${ATTRIBUTE_MENUBAR}]`;
|
|
321
|
+
|
|
322
|
+
const SELECTOR_MENUBAR_ITEM = `:scope > [${ATTRIBUTE_MENUBAR}-item], :scope > * > [${ATTRIBUTE_MENUBAR}-item]`;
|
|
323
|
+
|
|
324
|
+
const SELECTOR_MENU_CONTENT = `[${ATTRIBUTE_MENU}-content]`;
|
|
325
|
+
|
|
326
|
+
const SELECTOR_MENU_ITEM = `:scope > [${ATTRIBUTE_MENU}-item], :scope > * > [${ATTRIBUTE_MENU}-item]`;
|
|
327
|
+
|
|
328
|
+
const SELECTOR_MENU_TOGGLE = `[${ATTRIBUTE_MENU}-toggle]`;
|
|
329
|
+
|
|
330
|
+
const SELECTORS = `${SELECTOR_MENUBAR}, ${SELECTOR_MENU_CONTENT}`;
|
|
331
|
+
|
|
332
|
+
const menubars = new WeakMap<HTMLElement, OuiMenubar>();
|
|
333
|
+
|
|
334
|
+
const menus = new WeakMap<HTMLElement, OuiMenu>();
|
|
335
|
+
|
|
336
|
+
let index = 0;
|
|
337
|
+
|
|
338
|
+
// #endregion
|
|
339
|
+
|
|
340
|
+
// #region Initialization
|
|
341
|
+
|
|
342
|
+
attributable(ATTRIBUTE_MENU, addMenu, removeMenu);
|
|
343
|
+
attributable(ATTRIBUTE_MENUBAR, addMenubar, removeMenubar);
|
|
344
|
+
|
|
345
|
+
// #endregion
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type {EventPosition, PlainObject} from '@oscarpalmer/atoms/models';
|
|
2
|
+
import {setAttribute} from '@oscarpalmer/toretto/attribute';
|
|
2
3
|
import {getPosition, on} from '@oscarpalmer/toretto/event';
|
|
3
4
|
import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
|
|
5
|
+
import {getStyles, setStyle, setStyles} from '@oscarpalmer/toretto/style';
|
|
4
6
|
import {attributable} from '../internal/attributable';
|
|
5
7
|
import {
|
|
6
8
|
addDraggable,
|
|
@@ -23,10 +25,7 @@ export class OuiMovable extends OuiDraggable<OuiMovableState> {
|
|
|
23
25
|
#style: OuiMovableStyle;
|
|
24
26
|
|
|
25
27
|
constructor(element: HTMLElement, options?: CreateOuiDraggableOptions) {
|
|
26
|
-
const style =
|
|
27
|
-
inset: element.style.inset,
|
|
28
|
-
position: element.style.position,
|
|
29
|
-
};
|
|
28
|
+
const style = getStyles(element, ['inset', 'position']);
|
|
30
29
|
|
|
31
30
|
super(
|
|
32
31
|
'movable',
|
|
@@ -80,8 +79,7 @@ export class OuiMovable extends OuiDraggable<OuiMovableState> {
|
|
|
80
79
|
override destroy(): void {
|
|
81
80
|
MOVED.delete(this);
|
|
82
81
|
|
|
83
|
-
this.#element
|
|
84
|
-
this.#element.style.position = this.#style.position;
|
|
82
|
+
setStyles(this.#element, this.#style);
|
|
85
83
|
|
|
86
84
|
this.#element = undefined as never;
|
|
87
85
|
this.#style = undefined as never;
|
|
@@ -98,8 +96,8 @@ type OuiMovableState = {
|
|
|
98
96
|
};
|
|
99
97
|
|
|
100
98
|
type OuiMovableStyle = {
|
|
101
|
-
inset: string;
|
|
102
|
-
position: string;
|
|
99
|
+
inset: string | undefined;
|
|
100
|
+
position: string | undefined;
|
|
103
101
|
};
|
|
104
102
|
|
|
105
103
|
// #endregion
|
|
@@ -168,10 +166,9 @@ function onCancel(globals: OuiDraggableGlobals, item: OuiDraggableItem): void {
|
|
|
168
166
|
const {x, y} = globals.original ?? {x: 0, y: 0};
|
|
169
167
|
|
|
170
168
|
if (moved) {
|
|
171
|
-
element
|
|
169
|
+
setStyle(element, 'inset', `${y}px auto auto ${x}px`);
|
|
172
170
|
} else {
|
|
173
|
-
element
|
|
174
|
-
element.style.inset = style.inset;
|
|
171
|
+
setStyles(element, style);
|
|
175
172
|
}
|
|
176
173
|
|
|
177
174
|
element.removeAttribute(ATTRIBUTE_MOVING);
|
|
@@ -227,7 +224,7 @@ function onMove(
|
|
|
227
224
|
__: OuiDraggableItem,
|
|
228
225
|
position: OuiDragMovePosition,
|
|
229
226
|
): PlainObject | undefined {
|
|
230
|
-
globals.element?.node
|
|
227
|
+
setAttribute(globals.element?.node as never, ATTRIBUTE_MOVING, '');
|
|
231
228
|
|
|
232
229
|
return {
|
|
233
230
|
from: {...globals.original},
|
|
@@ -270,7 +267,7 @@ function reposition(movable: OuiMovable): void {
|
|
|
270
267
|
const x = container.left + (state.offset?.x ?? 0);
|
|
271
268
|
const y = container.top + (state.offset?.y ?? 0);
|
|
272
269
|
|
|
273
|
-
state.element
|
|
270
|
+
setStyle(state.element, 'inset', `${y}px auto auto ${x}px`);
|
|
274
271
|
}
|
|
275
272
|
|
|
276
273
|
function setOffset(state: OuiDraggableState & OuiMovableState, container: DOMRect): void {
|
package/src/js/popover.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {getAria, setAria} from '@oscarpalmer/toretto/aria';
|
|
2
|
+
import {setAttribute} from '@oscarpalmer/toretto/attribute';
|
|
1
3
|
import {on} from '@oscarpalmer/toretto/event';
|
|
2
4
|
import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
|
|
3
5
|
import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
|
|
@@ -34,6 +36,8 @@ class OuiPopover {
|
|
|
34
36
|
constructor(state: OuiPopoverState) {
|
|
35
37
|
this.#state = state;
|
|
36
38
|
|
|
39
|
+
initialize(state);
|
|
40
|
+
|
|
37
41
|
this.#state.floatable.update();
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -93,6 +97,18 @@ function destroyPopover(state: OuiPopoverState): void {
|
|
|
93
97
|
state.popover = undefined as never;
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
function getId(element: HTMLElement, value?: string): string {
|
|
101
|
+
let {id} = element;
|
|
102
|
+
|
|
103
|
+
if (id == null || id.trim().length === 0) {
|
|
104
|
+
id = value == null ? `oui_popover_${++index}` : `${value}_toggle`;
|
|
105
|
+
|
|
106
|
+
element.id = id;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return id;
|
|
110
|
+
}
|
|
111
|
+
|
|
96
112
|
/**
|
|
97
113
|
* Get the _OuiPopover_ for an element
|
|
98
114
|
*
|
|
@@ -109,29 +125,30 @@ function getState(anchor: HTMLElement, content: HTMLElement): OuiPopoverState {
|
|
|
109
125
|
content,
|
|
110
126
|
floatable: createEmbeddedFloatable(anchor, content, options),
|
|
111
127
|
focusTrap:
|
|
112
|
-
content
|
|
128
|
+
getAria(content, 'modal') === 'true'
|
|
113
129
|
? createFocusTrap(content, {
|
|
114
130
|
noescape: true,
|
|
115
131
|
})
|
|
116
132
|
: undefined,
|
|
117
133
|
listeners: [
|
|
118
134
|
getOnBeforeToggleListener(content),
|
|
119
|
-
on(content,
|
|
120
|
-
|
|
135
|
+
on(content, 'toggle', event => {
|
|
136
|
+
const expanded = event.newState === 'open';
|
|
137
|
+
|
|
138
|
+
if (expanded) {
|
|
121
139
|
ignoreFocus = false;
|
|
122
140
|
|
|
123
141
|
state.floatable?.update();
|
|
124
142
|
|
|
125
|
-
content.setAttribute(ATTRIBUTE_OPEN, '');
|
|
126
|
-
|
|
127
143
|
content.focus();
|
|
128
144
|
} else {
|
|
129
|
-
content.removeAttribute(ATTRIBUTE_OPEN);
|
|
130
|
-
|
|
131
145
|
if (!ignoreFocus) {
|
|
132
146
|
anchor?.focus();
|
|
133
147
|
}
|
|
134
148
|
}
|
|
149
|
+
|
|
150
|
+
setAria(anchor, 'expanded', expanded);
|
|
151
|
+
setAttribute(content, ATTRIBUTE_OPEN, expanded ? '' : undefined);
|
|
135
152
|
}),
|
|
136
153
|
],
|
|
137
154
|
popover: undefined as never,
|
|
@@ -142,6 +159,21 @@ function getState(anchor: HTMLElement, content: HTMLElement): OuiPopoverState {
|
|
|
142
159
|
return state;
|
|
143
160
|
}
|
|
144
161
|
|
|
162
|
+
function initialize(state: OuiPopoverState): void {
|
|
163
|
+
const id = getId(state.content);
|
|
164
|
+
|
|
165
|
+
setAria(state.anchor, {
|
|
166
|
+
controls: id,
|
|
167
|
+
expanded: 'false',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const label = getAria(state.content, 'label') ?? getAria(state.content, 'labelledby');
|
|
171
|
+
|
|
172
|
+
if (label == null) {
|
|
173
|
+
setAria(state.content, 'labelledby', getId(state.anchor, id));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
145
177
|
function onPointerdown(): void {
|
|
146
178
|
ignoreFocus = true;
|
|
147
179
|
}
|
|
@@ -164,16 +196,8 @@ function removePopover(element: HTMLElement): void {
|
|
|
164
196
|
|
|
165
197
|
const ATTRIBUTE = 'oui-popover';
|
|
166
198
|
|
|
167
|
-
const ARIA_MODAL = 'aria-modal';
|
|
168
|
-
|
|
169
199
|
const ATTRIBUTE_OPEN = `${ATTRIBUTE}-open`;
|
|
170
200
|
|
|
171
|
-
const EVENT_TOGGLE = 'toggle';
|
|
172
|
-
|
|
173
|
-
const STATE_OPEN = 'open';
|
|
174
|
-
|
|
175
|
-
const TRUE = 'true';
|
|
176
|
-
|
|
177
201
|
const options: OuiFloatableOptions = {
|
|
178
202
|
attribute: `${ATTRIBUTE}position`,
|
|
179
203
|
position: 'below-start',
|
|
@@ -184,6 +208,8 @@ const states = new WeakMap<HTMLElement, OuiPopoverState>();
|
|
|
184
208
|
|
|
185
209
|
let ignoreFocus = false;
|
|
186
210
|
|
|
211
|
+
let index = 0;
|
|
212
|
+
|
|
187
213
|
// #endregion
|
|
188
214
|
|
|
189
215
|
// #region Initialization
|