@oscarpalmer/oui 0.28.0 → 0.29.1
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/js/dialog.d.mts +1 -1
- package/dist/js/dialog.mjs +19 -20
- 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 +2 -2
- package/dist/js/index.mjs +1 -0
- package/dist/js/internal/attributable.mjs +3 -4
- package/dist/js/internal/draggable.d.mts +1 -1
- package/dist/js/internal/draggable.mjs +11 -8
- 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 +28 -13
- package/dist/js/resizable/resizable.embedded.mjs +16 -11
- 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 +29 -25
- 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 +7 -3
- package/src/js/dialog.ts +22 -26
- 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 +1 -0
- package/src/js/internal/attributable.ts +3 -5
- package/src/js/internal/draggable.ts +12 -9
- 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 +42 -16
- package/src/js/resizable/resizable.embedded.ts +17 -10
- package/src/js/sortable/sortable.embedded.ts +17 -22
- package/src/js/splitter.ts +40 -35
- package/src/js/toolbar.ts +27 -178
- package/src/js/tooltip.ts +18 -30
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import {clamp} from '@oscarpalmer/atoms/number';
|
|
2
|
+
import {getAria, setRole} from '@oscarpalmer/toretto/aria';
|
|
3
|
+
import {on} from '@oscarpalmer/toretto/event';
|
|
4
|
+
import {findAncestor} from '@oscarpalmer/toretto/find';
|
|
5
|
+
import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
|
|
6
|
+
import type {AriaRole, RemovableEventListener} from '@oscarpalmer/toretto/models';
|
|
7
|
+
|
|
8
|
+
// #region Types
|
|
9
|
+
|
|
10
|
+
export class OuiNavigable {
|
|
11
|
+
active: OuiNavigableActive = {
|
|
12
|
+
index: 0,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
element: HTMLElement;
|
|
16
|
+
|
|
17
|
+
listeners: RemovableEventListener[];
|
|
18
|
+
|
|
19
|
+
options: OuiNavigableOptions;
|
|
20
|
+
|
|
21
|
+
orientation: OuiNavigableOrientation;
|
|
22
|
+
|
|
23
|
+
constructor(element: HTMLElement, options: OuiNavigableOptions) {
|
|
24
|
+
this.element = element;
|
|
25
|
+
this.options = options;
|
|
26
|
+
|
|
27
|
+
this.listeners = [
|
|
28
|
+
on(element, 'focusin', event => {
|
|
29
|
+
onFocusin(this, event);
|
|
30
|
+
}),
|
|
31
|
+
on(element, 'keydown', event => {
|
|
32
|
+
onKeydown(this, event);
|
|
33
|
+
}),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
this.orientation = getOrientation(element, options);
|
|
37
|
+
|
|
38
|
+
if (options.role != null) {
|
|
39
|
+
setRole(element, options.role);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
destroy(): void {
|
|
44
|
+
const listeners = this.listeners.splice(0);
|
|
45
|
+
|
|
46
|
+
for (const listener of listeners) {
|
|
47
|
+
listener();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.active.element = undefined;
|
|
51
|
+
this.element = undefined as never;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type OuiNavigableActive = {
|
|
56
|
+
element?: HTMLElement;
|
|
57
|
+
index: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type OuiNavigableOptions = {
|
|
61
|
+
role?: AriaRole;
|
|
62
|
+
selector: string;
|
|
63
|
+
vertical: boolean;
|
|
64
|
+
onDeactivate?: (event: KeyboardEvent, navigable: OuiNavigable) => void;
|
|
65
|
+
onPreventNavigation?: (event: KeyboardEvent, navigable: OuiNavigable) => boolean;
|
|
66
|
+
onShouldActivate?: (event: KeyboardEvent, navigable: OuiNavigable) => boolean;
|
|
67
|
+
onShouldDeactivate?: (event: KeyboardEvent, navigable: OuiNavigable) => boolean | undefined;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type OuiNavigableOrientation = 'horizontal' | 'vertical';
|
|
71
|
+
|
|
72
|
+
// #endregion
|
|
73
|
+
|
|
74
|
+
// #region Functions
|
|
75
|
+
|
|
76
|
+
function getItems(container: HTMLElement, selector: string): HTMLElement[] {
|
|
77
|
+
return [...container.querySelectorAll(selector)].filter(isValidItem);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getNavigable(element: HTMLElement, options: OuiNavigableOptions): OuiNavigable {
|
|
81
|
+
return new OuiNavigable(element, options);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getOrientation(
|
|
85
|
+
element: HTMLElement,
|
|
86
|
+
options: OuiNavigableOptions,
|
|
87
|
+
): OuiNavigableOrientation {
|
|
88
|
+
const attribute = getAria(element, 'orientation');
|
|
89
|
+
|
|
90
|
+
return ORIENTATIONS.has(attribute as OuiNavigableOrientation)
|
|
91
|
+
? (attribute as OuiNavigableOrientation)
|
|
92
|
+
: options.vertical
|
|
93
|
+
? ORIENTATION_VERTICAL
|
|
94
|
+
: ORIENTATION_HORIZONTAL;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isValidItem(element: Element): element is HTMLElement {
|
|
98
|
+
return 'disabled' in element ? element.disabled !== true : true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function onFocusin(navigable: OuiNavigable, event: FocusEvent): void {
|
|
102
|
+
const item = findAncestor(event, navigable.options.selector);
|
|
103
|
+
|
|
104
|
+
if (!isHTMLOrSVGElement(item) || !isValidItem(item)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const items = getItems(navigable.element, navigable.options.selector);
|
|
109
|
+
const index = items.indexOf(item);
|
|
110
|
+
|
|
111
|
+
if (index === -1) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
navigable.active.element = item;
|
|
116
|
+
navigable.active.index = index;
|
|
117
|
+
|
|
118
|
+
updateNavigableFocus(navigable, false, items);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function onKeydown(navigable: OuiNavigable, event: KeyboardEvent): void {
|
|
122
|
+
const escaped = event.key === 'Escape';
|
|
123
|
+
const stopPropagation = navigable.options.onShouldDeactivate?.(event, navigable);
|
|
124
|
+
|
|
125
|
+
if (escaped || stopPropagation != null) {
|
|
126
|
+
if (escaped || stopPropagation) {
|
|
127
|
+
event.stopPropagation();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
navigable.options.onDeactivate?.(event, navigable);
|
|
131
|
+
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!KEYS_ALL.has(event.key)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const mustActivate = KEYS_ACTIVATE.has(event.key);
|
|
140
|
+
const shouldActivate = navigable.options.onShouldActivate?.(event, navigable) === true;
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
mustActivate ||
|
|
144
|
+
shouldActivate ||
|
|
145
|
+
navigable.options.onPreventNavigation?.(event, navigable) === true
|
|
146
|
+
) {
|
|
147
|
+
event.stopPropagation();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (mustActivate || shouldActivate) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const items = getItems(navigable.element, navigable.options.selector);
|
|
155
|
+
const {length} = items;
|
|
156
|
+
|
|
157
|
+
if (length === 0) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let index: number | undefined;
|
|
162
|
+
|
|
163
|
+
if (KEYS_ABSOLUTE.has(event.key)) {
|
|
164
|
+
index = event.key === 'Home' ? 0 : items.length - 1;
|
|
165
|
+
} else {
|
|
166
|
+
const direction = NAVIGABLE_KEYS_HORIZONTAL.has(event.key)
|
|
167
|
+
? ORIENTATION_HORIZONTAL
|
|
168
|
+
: ORIENTATION_VERTICAL;
|
|
169
|
+
|
|
170
|
+
if (navigable.orientation === direction) {
|
|
171
|
+
if (navigable.active.element instanceof HTMLSelectElement) {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
index = clamp(
|
|
176
|
+
navigable.active.index + (KEYS_NEXT.has(event.key) ? 1 : -1),
|
|
177
|
+
0,
|
|
178
|
+
length - 1,
|
|
179
|
+
true,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (index == null || index === navigable.active.index) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
navigable.active.element = items[index];
|
|
189
|
+
navigable.active.index = index;
|
|
190
|
+
|
|
191
|
+
updateNavigableFocus(navigable, true, items);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function updateNavigableFocus(
|
|
195
|
+
navigable: OuiNavigable,
|
|
196
|
+
focus: boolean,
|
|
197
|
+
items?: HTMLElement[],
|
|
198
|
+
): void {
|
|
199
|
+
const updated = items ?? getItems(navigable.element, navigable.options.selector);
|
|
200
|
+
const {length} = updated;
|
|
201
|
+
|
|
202
|
+
if (length === 0) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (let index = 0; index < length; index += 1) {
|
|
207
|
+
updated[index].tabIndex = index === navigable.active.index ? 0 : -1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
navigable.active.element ??= updated[navigable.active.index];
|
|
211
|
+
|
|
212
|
+
if (focus) {
|
|
213
|
+
navigable.active.element?.focus();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// #endregion
|
|
218
|
+
|
|
219
|
+
// #region Variables
|
|
220
|
+
|
|
221
|
+
const KEYS_ABSOLUTE = new Set(['End', 'Home']);
|
|
222
|
+
|
|
223
|
+
const KEYS_ACTIVATE = new Set(['Enter', ' ']);
|
|
224
|
+
|
|
225
|
+
export const NAVIGABLE_KEYS_HORIZONTAL = new Set(['ArrowRight', 'ArrowLeft']);
|
|
226
|
+
|
|
227
|
+
export const NAVIGABLE_KEYS_VERTICAL = new Set(['ArrowDown', 'ArrowUp']);
|
|
228
|
+
|
|
229
|
+
const KEYS_ALL = new Set([
|
|
230
|
+
...KEYS_ABSOLUTE,
|
|
231
|
+
...KEYS_ACTIVATE,
|
|
232
|
+
...NAVIGABLE_KEYS_HORIZONTAL,
|
|
233
|
+
...NAVIGABLE_KEYS_VERTICAL,
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
const KEYS_NEXT = new Set(['ArrowDown', 'ArrowRight']);
|
|
237
|
+
|
|
238
|
+
const ORIENTATION_HORIZONTAL: OuiNavigableOrientation = 'horizontal';
|
|
239
|
+
|
|
240
|
+
const ORIENTATION_VERTICAL: OuiNavigableOrientation = 'vertical';
|
|
241
|
+
|
|
242
|
+
const ORIENTATIONS = new Set<OuiNavigableOrientation>([
|
|
243
|
+
ORIENTATION_HORIZONTAL,
|
|
244
|
+
ORIENTATION_VERTICAL,
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
// #endregion
|
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 {
|