@oscarpalmer/oui 0.25.0 → 0.26.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/js/embedded-CHK82Uh5.d.mts +76 -0
- package/dist/js/{embedded-84gxpReS.d.mts → embedded-ai6cqURc.d.mts} +21 -17
- package/dist/js/floatable/embedded.d.mts +2 -2
- package/dist/js/floatable/embedded.mjs +27 -16
- package/dist/js/floatable/standalone.d.mts +2 -2
- package/dist/js/floatable/standalone.mjs +2 -2
- package/dist/js/focus-trap/embedded.d.mts +2 -2
- package/dist/js/focus-trap/embedded.mjs +157 -79
- package/dist/js/focus-trap/standalone.d.mts +2 -1
- package/dist/js/focus-trap/standalone.mjs +3 -3
- package/dist/js/index.d.mts +3 -2
- package/dist/js/index.mjs +4 -2
- package/dist/js/internal/draggable.d.mts +2 -2
- package/dist/js/internal/draggable.mjs +2 -2
- package/dist/js/{movable-BKFgoaOl.d.mts → movable-dNqBctx3.d.mts} +16 -16
- package/dist/js/movable.d.mts +2 -2
- package/dist/js/movable.mjs +4 -4
- package/dist/js/popover.d.mts +3 -3
- package/dist/js/popover.mjs +9 -9
- package/dist/js/sortable.d.mts +2 -2
- package/dist/js/sortable.mjs +4 -4
- package/dist/js/splitter.mjs +48 -47
- package/dist/js/toolbar.d.mts +1 -0
- package/dist/js/toolbar.mjs +107 -0
- package/dist/js/tooltip.d.mts +1 -1
- package/dist/js/tooltip.mjs +7 -7
- package/package.json +13 -1
- package/src/js/floatable/embedded.ts +65 -44
- package/src/js/floatable/standalone.ts +1 -1
- package/src/js/focus-trap/embedded.ts +219 -93
- package/src/js/focus-trap/standalone.ts +4 -2
- package/src/js/index.ts +1 -0
- package/src/js/internal/draggable.ts +20 -20
- package/src/js/movable.ts +17 -17
- package/src/js/popover.ts +18 -18
- package/src/js/sortable.ts +26 -26
- package/src/js/splitter.ts +98 -96
- package/src/js/toolbar.ts +214 -0
- package/src/js/tooltip.ts +14 -10
- package/dist/js/embedded-BpwzMSEF.d.mts +0 -26
|
@@ -5,18 +5,40 @@ import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
|
|
|
5
5
|
|
|
6
6
|
// #region Types
|
|
7
7
|
|
|
8
|
+
type CreateOuiFloatableOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* Open the _OuiFloatable_ when created? _(defaults to `true`)_
|
|
11
|
+
*/
|
|
12
|
+
open?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Position of the _OuiFloatable_ _(defaults to `below`)_
|
|
15
|
+
*/
|
|
16
|
+
position?: OuiFloatablePosition;
|
|
17
|
+
};
|
|
18
|
+
|
|
8
19
|
export class OuiFloatable {
|
|
9
20
|
#destroyed = false;
|
|
10
21
|
|
|
11
22
|
readonly #state: OuiFloatableState;
|
|
12
23
|
|
|
13
24
|
/**
|
|
14
|
-
* Is the
|
|
25
|
+
* Is the _OuiFloatable_ open?
|
|
15
26
|
*/
|
|
16
27
|
get open(): boolean {
|
|
17
28
|
return this.#state.content.checkVisibility();
|
|
18
29
|
}
|
|
19
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Open or close the _OuiFloatable_
|
|
33
|
+
*/
|
|
34
|
+
set open(value: boolean) {
|
|
35
|
+
if (value === true) {
|
|
36
|
+
this.show();
|
|
37
|
+
} else if (value === false) {
|
|
38
|
+
this.hide();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
20
42
|
constructor(state: OuiFloatableState, standalone: boolean) {
|
|
21
43
|
this.#state = state;
|
|
22
44
|
|
|
@@ -105,42 +127,10 @@ type OuiFloatableState = {
|
|
|
105
127
|
options: OuiFloatableOptions;
|
|
106
128
|
};
|
|
107
129
|
|
|
108
|
-
type GetOuiFloatableOptions = {
|
|
109
|
-
/**
|
|
110
|
-
* Open the _OuiFloatable_ when created? _(defaults to `true`)_
|
|
111
|
-
*/
|
|
112
|
-
open?: boolean;
|
|
113
|
-
/**
|
|
114
|
-
* Position of the _OuiFloatable_ _(defaults to `below`)_
|
|
115
|
-
*/
|
|
116
|
-
position?: OuiFloatablePosition;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
130
|
// #endregion
|
|
120
131
|
|
|
121
132
|
// #region Functions
|
|
122
133
|
|
|
123
|
-
export function createFloatable(
|
|
124
|
-
anchor: HTMLElement,
|
|
125
|
-
content: HTMLElement,
|
|
126
|
-
options: OuiFloatableOptions,
|
|
127
|
-
standalone: boolean,
|
|
128
|
-
): OuiFloatable {
|
|
129
|
-
if (!isHTMLOrSVGElement(anchor) || !isHTMLOrSVGElement(content)) {
|
|
130
|
-
throw new TypeError('Anchor and content must be an HTMLElement or SVGElement.');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let floatable = instances.get(content);
|
|
134
|
-
|
|
135
|
-
if (floatable == null) {
|
|
136
|
-
floatable = new OuiFloatable(getState(anchor, content, options), standalone);
|
|
137
|
-
|
|
138
|
-
instances.set(content, floatable);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return floatable;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
134
|
function getAnchorName(anchor: HTMLElement): string {
|
|
145
135
|
let {anchorName} = anchor.style;
|
|
146
136
|
|
|
@@ -153,24 +143,32 @@ function getAnchorName(anchor: HTMLElement): string {
|
|
|
153
143
|
return `--oui-floatable-anchor-${index}`;
|
|
154
144
|
}
|
|
155
145
|
|
|
146
|
+
export function createEmbeddedFloatable(
|
|
147
|
+
anchor: HTMLElement,
|
|
148
|
+
content: HTMLElement,
|
|
149
|
+
options: OuiFloatableOptions,
|
|
150
|
+
): OuiFloatable {
|
|
151
|
+
return createInstance(anchor, content, options, false);
|
|
152
|
+
}
|
|
153
|
+
|
|
156
154
|
/**
|
|
157
|
-
* Create a
|
|
155
|
+
* Create a _OuiFloatable_ for an anchor and content element
|
|
158
156
|
*
|
|
159
157
|
* @param anchor Anchor element
|
|
160
158
|
* @param content Content element
|
|
161
|
-
* @param options
|
|
162
|
-
* @returns
|
|
159
|
+
* @param options _OuiFloatable_ options
|
|
160
|
+
* @returns _OuiFloatable_ instance
|
|
163
161
|
*/
|
|
164
|
-
export function
|
|
165
|
-
anchor: HTMLElement
|
|
166
|
-
content: HTMLElement
|
|
167
|
-
options?:
|
|
162
|
+
export function createFloatable(
|
|
163
|
+
anchor: HTMLElement,
|
|
164
|
+
content: HTMLElement,
|
|
165
|
+
options?: CreateOuiFloatableOptions,
|
|
168
166
|
): OuiFloatable {
|
|
169
167
|
const {open, position} = getOptions(options);
|
|
170
168
|
|
|
171
|
-
const floatable =
|
|
172
|
-
anchor
|
|
173
|
-
content
|
|
169
|
+
const floatable = createInstance(
|
|
170
|
+
anchor,
|
|
171
|
+
content,
|
|
174
172
|
{
|
|
175
173
|
position,
|
|
176
174
|
},
|
|
@@ -186,6 +184,27 @@ export function getFloatable(
|
|
|
186
184
|
return floatable;
|
|
187
185
|
}
|
|
188
186
|
|
|
187
|
+
function createInstance(
|
|
188
|
+
anchor: HTMLElement,
|
|
189
|
+
content: HTMLElement,
|
|
190
|
+
options: OuiFloatableOptions,
|
|
191
|
+
standalone: boolean,
|
|
192
|
+
): OuiFloatable {
|
|
193
|
+
if (!isHTMLOrSVGElement(anchor) || !isHTMLOrSVGElement(content)) {
|
|
194
|
+
throw new TypeError(MESSAGE);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let floatable = instances.get(content);
|
|
198
|
+
|
|
199
|
+
if (floatable == null) {
|
|
200
|
+
floatable = new OuiFloatable(getState(anchor, content, options), standalone);
|
|
201
|
+
|
|
202
|
+
instances.set(content, floatable);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return floatable;
|
|
206
|
+
}
|
|
207
|
+
|
|
189
208
|
export function getOnBeforeToggleListener(element: HTMLElement): RemovableEventListener {
|
|
190
209
|
return on(
|
|
191
210
|
element,
|
|
@@ -203,7 +222,7 @@ export function getOnBeforeToggleListener(element: HTMLElement): RemovableEventL
|
|
|
203
222
|
);
|
|
204
223
|
}
|
|
205
224
|
|
|
206
|
-
function getOptions(input?:
|
|
225
|
+
function getOptions(input?: CreateOuiFloatableOptions): Required<CreateOuiFloatableOptions> {
|
|
207
226
|
const object = isPlainObject(input) ? input : {};
|
|
208
227
|
|
|
209
228
|
return {
|
|
@@ -287,6 +306,8 @@ const ATTRIBUTE_POSITION = 'oui-position';
|
|
|
287
306
|
|
|
288
307
|
const EVENT_BEFORE = 'beforetoggle';
|
|
289
308
|
|
|
309
|
+
const MESSAGE = 'Anchor and content must be an HTMLElement or SVGElement';
|
|
310
|
+
|
|
290
311
|
const POSITIONS = Object.fromEntries(
|
|
291
312
|
Object.entries(AREAS).map(([position, area]) => [area, position]),
|
|
292
313
|
) as Record<string, OuiFloatablePosition>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {createFloatable, type OuiFloatable, type OuiFloatablePosition} from './embedded';
|
|
@@ -1,118 +1,239 @@
|
|
|
1
|
+
import {isPlainObject} from '@oscarpalmer/atoms/is';
|
|
1
2
|
import {clamp} from '@oscarpalmer/atoms/number';
|
|
2
3
|
import {on} from '@oscarpalmer/toretto/event';
|
|
3
4
|
import {findAncestor} from '@oscarpalmer/toretto/find';
|
|
4
5
|
import {getFocusable} from '@oscarpalmer/toretto/focusable';
|
|
6
|
+
import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
disabled = false;
|
|
8
|
+
// #region Types
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
export class OuiFocusTrap {
|
|
11
|
+
#destroyed = false;
|
|
12
|
+
#tabIndex: number;
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
embedded: boolean;
|
|
14
|
+
readonly #state: OuiFocusTrapState;
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Is focused maintained within the _FocusTrap_ when clicking outside of it?
|
|
18
|
+
*/
|
|
15
19
|
get contain(): boolean {
|
|
16
|
-
return this.element.hasAttribute(
|
|
20
|
+
return this.#state.element.hasAttribute(ATTRIBUTE_FOCUSTRAP_CONTAIN);
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Should focus be maintained within the _FocusTrap_ when clicking outside of it?
|
|
25
|
+
*/
|
|
26
|
+
set contain(value: boolean) {
|
|
27
|
+
if (value === true) {
|
|
28
|
+
this.#state.element.setAttribute(ATTRIBUTE_FOCUSTRAP_CONTAIN, '');
|
|
29
|
+
} else if (value === false) {
|
|
30
|
+
this.#state.element.removeAttribute(ATTRIBUTE_FOCUSTRAP_CONTAIN);
|
|
31
|
+
}
|
|
21
32
|
}
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Is the _FocusTrap_ disabled?
|
|
36
|
+
*/
|
|
37
|
+
get disabled(): boolean {
|
|
38
|
+
return this.#state.disabled;
|
|
39
|
+
}
|
|
28
40
|
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Should the _FocusTrap_ be disabled?
|
|
43
|
+
*/
|
|
44
|
+
set disabled(value: boolean) {
|
|
45
|
+
if (value === true) {
|
|
46
|
+
this.disable();
|
|
47
|
+
} else if (value === false) {
|
|
48
|
+
this.enable();
|
|
31
49
|
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Is focus allowed to escape when pressing _Escape_ within the _FocusTrap_?
|
|
54
|
+
*/
|
|
55
|
+
get noescape(): boolean {
|
|
56
|
+
return this.#state.element.hasAttribute(ATTRIBUTE_FOCUSTRAP_NOESCAPE);
|
|
57
|
+
}
|
|
32
58
|
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Should the _FocusTrap_ allow focus to escape when pressing _Escape_?
|
|
61
|
+
*/
|
|
62
|
+
set noescape(value: boolean) {
|
|
63
|
+
if (value === true) {
|
|
64
|
+
this.#state.element.setAttribute(ATTRIBUTE_FOCUSTRAP_NOESCAPE, '');
|
|
65
|
+
} else if (value === false) {
|
|
66
|
+
this.#state.element.removeAttribute(ATTRIBUTE_FOCUSTRAP_NOESCAPE);
|
|
35
67
|
}
|
|
36
68
|
}
|
|
37
69
|
|
|
38
|
-
|
|
39
|
-
|
|
70
|
+
constructor(state: OuiFocusTrapState) {
|
|
71
|
+
this.#state = state;
|
|
72
|
+
this.#tabIndex = state.element.tabIndex;
|
|
73
|
+
|
|
74
|
+
state.element.tabIndex = -1;
|
|
75
|
+
|
|
76
|
+
state.element.setAttribute(ATTRIBUTE_FOCUSTRAP, '');
|
|
77
|
+
|
|
78
|
+
this.contain = state.options.contain;
|
|
79
|
+
this.noescape = state.options.noescape;
|
|
80
|
+
|
|
81
|
+
states.set(this, state);
|
|
40
82
|
}
|
|
41
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Destroys the _FocusTrap_
|
|
86
|
+
*/
|
|
42
87
|
destroy(): void {
|
|
43
|
-
this
|
|
88
|
+
if (this.#destroyed) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.#destroyed = true;
|
|
93
|
+
|
|
94
|
+
disabled.delete(this);
|
|
95
|
+
instances.delete(this.#state.element);
|
|
96
|
+
states.delete(this);
|
|
97
|
+
|
|
98
|
+
this.#state.element.tabIndex = this.#tabIndex;
|
|
99
|
+
|
|
100
|
+
this.#state.element.removeAttribute(ATTRIBUTE_FOCUSTRAP);
|
|
101
|
+
this.#state.element.removeAttribute(ATTRIBUTE_FOCUSTRAP_CONTAIN);
|
|
102
|
+
this.#state.element.removeAttribute(ATTRIBUTE_FOCUSTRAP_NOESCAPE);
|
|
103
|
+
|
|
104
|
+
this.#state.element = undefined as never;
|
|
44
105
|
}
|
|
45
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Disables the _FocusTrap_
|
|
109
|
+
*/
|
|
46
110
|
disable(): void {
|
|
47
|
-
if (!(this.disabled || this.noescape)) {
|
|
48
|
-
setDisabled(this, true);
|
|
111
|
+
if (!this.#destroyed && !(this.disabled || this.#state.options.noescape)) {
|
|
112
|
+
setDisabled(this, this.#state, true);
|
|
49
113
|
}
|
|
50
114
|
}
|
|
51
115
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Enables the _FocusTrap_
|
|
118
|
+
*/
|
|
56
119
|
enable(): void {
|
|
57
|
-
if (this.disabled) {
|
|
58
|
-
setDisabled(this, false);
|
|
120
|
+
if (!this.#destroyed && this.disabled) {
|
|
121
|
+
setDisabled(this, this.#state, false);
|
|
59
122
|
}
|
|
60
123
|
}
|
|
61
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Focuses the _FocusTrap_
|
|
127
|
+
*
|
|
128
|
+
* @param last When `true`, focuses the last focusable element within the _FocusTrap_ instead of the first
|
|
129
|
+
*/
|
|
62
130
|
focus(last: boolean): void {
|
|
63
|
-
|
|
131
|
+
if (!this.#destroyed && !this.disabled) {
|
|
132
|
+
tabToTrap(this, getFocusable(this.#state.element), this.#state.element, last ? -1 : 0);
|
|
133
|
+
}
|
|
64
134
|
}
|
|
65
135
|
}
|
|
66
136
|
|
|
67
|
-
type
|
|
68
|
-
|
|
137
|
+
type OuiFocusTrapOptions = {
|
|
138
|
+
/**
|
|
139
|
+
* Should focus be maintained within the _FocusTrap_ when clicking outside of it?
|
|
140
|
+
*/
|
|
141
|
+
contain?: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Should the _FocusTrap_ allow focus to escape when pressing _Escape_?
|
|
144
|
+
*/
|
|
145
|
+
noescape?: boolean;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
type OuiFocusTrapState = {
|
|
149
|
+
disabled: boolean;
|
|
150
|
+
element: HTMLElement;
|
|
151
|
+
options: Required<OuiFocusTrapOptions>;
|
|
69
152
|
};
|
|
70
153
|
|
|
71
|
-
//
|
|
154
|
+
// #endregion
|
|
155
|
+
|
|
156
|
+
// #region Functions
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Creates _(or retrieves)_ a _FocusTrap_ for an element
|
|
160
|
+
*
|
|
161
|
+
* @param element Element to trap focus within
|
|
162
|
+
* @param options _FocusTrap_ options
|
|
163
|
+
* @returns _FocusTrap_ instance
|
|
164
|
+
*/
|
|
165
|
+
export function createFocusTrap(element: HTMLElement, options?: OuiFocusTrapOptions): OuiFocusTrap {
|
|
166
|
+
if (!isHTMLOrSVGElement(element)) {
|
|
167
|
+
throw new TypeError(MESSAGE);
|
|
168
|
+
}
|
|
72
169
|
|
|
73
|
-
|
|
74
|
-
let focusTrap = FOCUSTRAPS_ALL.get(element);
|
|
170
|
+
let instance = instances.get(element);
|
|
75
171
|
|
|
76
|
-
if (
|
|
77
|
-
|
|
172
|
+
if (instance == null) {
|
|
173
|
+
instance = new OuiFocusTrap(getState(element, options));
|
|
78
174
|
|
|
79
|
-
|
|
175
|
+
instances.set(element, instance);
|
|
80
176
|
}
|
|
81
177
|
|
|
82
|
-
return
|
|
178
|
+
return instance;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getOptions(
|
|
182
|
+
element: HTMLElement,
|
|
183
|
+
input?: OuiFocusTrapOptions,
|
|
184
|
+
): Required<OuiFocusTrapOptions> {
|
|
185
|
+
const options = isPlainObject(input) ? input : {};
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
contain:
|
|
189
|
+
typeof options.contain === 'boolean'
|
|
190
|
+
? options.contain
|
|
191
|
+
: element.hasAttribute(ATTRIBUTE_FOCUSTRAP_CONTAIN),
|
|
192
|
+
noescape:
|
|
193
|
+
typeof options.noescape === 'boolean'
|
|
194
|
+
? options.noescape
|
|
195
|
+
: element.hasAttribute(ATTRIBUTE_FOCUSTRAP_NOESCAPE),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getState(element: HTMLElement, options?: OuiFocusTrapOptions): OuiFocusTrapState {
|
|
200
|
+
return {
|
|
201
|
+
element,
|
|
202
|
+
disabled: false,
|
|
203
|
+
options: getOptions(element, options),
|
|
204
|
+
};
|
|
83
205
|
}
|
|
84
206
|
|
|
85
207
|
function onEscape(event: KeyboardEvent): void {
|
|
86
|
-
let element = findAncestor(event,
|
|
208
|
+
let element = findAncestor(event, SELECTOR) as HTMLElement;
|
|
87
209
|
|
|
88
|
-
let
|
|
210
|
+
let instance = instances.get(element);
|
|
211
|
+
let state = states.get(instance!);
|
|
89
212
|
|
|
90
|
-
if (element == null ||
|
|
213
|
+
if (element == null || instance == null || state == null) {
|
|
91
214
|
return;
|
|
92
215
|
}
|
|
93
216
|
|
|
94
|
-
while (
|
|
95
|
-
element = findAncestor(
|
|
96
|
-
focusTrap.element.parentElement as HTMLElement,
|
|
97
|
-
ATTRIBUTE_SELECTOR,
|
|
98
|
-
) as HTMLElement;
|
|
217
|
+
while (state.disabled) {
|
|
218
|
+
element = findAncestor(state.element.parentElement as HTMLElement, SELECTOR) as HTMLElement;
|
|
99
219
|
|
|
100
|
-
|
|
220
|
+
instance = instances.get(element);
|
|
221
|
+
state = states.get(instance!);
|
|
101
222
|
|
|
102
|
-
if (element == null ||
|
|
223
|
+
if (element == null || instance == null || state == null) {
|
|
103
224
|
return;
|
|
104
225
|
}
|
|
105
226
|
|
|
106
|
-
if (!
|
|
227
|
+
if (!state.disabled) {
|
|
107
228
|
break;
|
|
108
229
|
}
|
|
109
230
|
}
|
|
110
231
|
|
|
111
|
-
|
|
232
|
+
instance?.disable();
|
|
112
233
|
}
|
|
113
234
|
|
|
114
235
|
function onFocusIn(event: Event): void {
|
|
115
|
-
const focusTrap =
|
|
236
|
+
const focusTrap = instances.get(findAncestor(event, SELECTOR) as HTMLElement);
|
|
116
237
|
|
|
117
238
|
lastTarget = focusTrap == null ? undefined : (event.target as HTMLElement);
|
|
118
239
|
}
|
|
@@ -133,38 +254,40 @@ function onKeydown(event: KeyboardEvent): void {
|
|
|
133
254
|
}
|
|
134
255
|
|
|
135
256
|
function onPointerdown(event: MouseEvent | TouchEvent): void {
|
|
136
|
-
const
|
|
137
|
-
const
|
|
257
|
+
const last = findAncestor(lastTarget as HTMLElement, SELECTOR) as HTMLElement;
|
|
258
|
+
const next = findAncestor(event, SELECTOR) as HTMLElement;
|
|
138
259
|
|
|
139
|
-
if (
|
|
140
|
-
if (
|
|
260
|
+
if (last != null && next !== last) {
|
|
261
|
+
if (states.get(instances.get(last)!)?.options.contain ?? false) {
|
|
141
262
|
event.preventDefault();
|
|
142
263
|
|
|
143
264
|
lastTarget?.focus();
|
|
144
265
|
}
|
|
145
266
|
}
|
|
146
267
|
|
|
147
|
-
if (
|
|
268
|
+
if (disabled.size === 0) {
|
|
148
269
|
return;
|
|
149
270
|
}
|
|
150
271
|
|
|
151
|
-
for (const focusTrap of
|
|
152
|
-
|
|
272
|
+
for (const focusTrap of disabled) {
|
|
273
|
+
const state = states.get(focusTrap);
|
|
274
|
+
|
|
275
|
+
if (state != null && state.element !== next) {
|
|
153
276
|
focusTrap.enable();
|
|
154
277
|
}
|
|
155
278
|
}
|
|
156
279
|
}
|
|
157
280
|
|
|
158
281
|
function onTab(event: KeyboardEvent): void {
|
|
159
|
-
const element = findAncestor(event,
|
|
282
|
+
const element = findAncestor(event, SELECTOR) as HTMLElement;
|
|
160
283
|
|
|
161
|
-
const
|
|
284
|
+
const instance = instances.get(element);
|
|
162
285
|
|
|
163
|
-
if (
|
|
286
|
+
if (instance == null) {
|
|
164
287
|
return;
|
|
165
288
|
}
|
|
166
289
|
|
|
167
|
-
if (!
|
|
290
|
+
if (!instance.disabled) {
|
|
168
291
|
event.preventDefault();
|
|
169
292
|
}
|
|
170
293
|
|
|
@@ -173,37 +296,30 @@ function onTab(event: KeyboardEvent): void {
|
|
|
173
296
|
const index = elements.indexOf(event.target as HTMLElement);
|
|
174
297
|
|
|
175
298
|
if (index === -1 || elements.length === 0) {
|
|
176
|
-
tabToTrap(
|
|
299
|
+
tabToTrap(instance, elements, element);
|
|
177
300
|
} else {
|
|
178
|
-
tabToElement(
|
|
301
|
+
tabToElement(instance, elements, index, event.shiftKey ? -1 : 1);
|
|
179
302
|
}
|
|
180
303
|
}
|
|
181
304
|
|
|
182
305
|
export function removeFocusTrap(element: HTMLElement): void {
|
|
183
|
-
|
|
184
|
-
FOCUSTRAPS_ALL.delete(element);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function setConnected(focusTrap: FocusTrap, value: boolean): void {
|
|
188
|
-
if (!focusTrap.embedded || focusTrap.connected === value) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
306
|
+
instances.get(element)?.destroy();
|
|
191
307
|
|
|
192
|
-
|
|
308
|
+
instances.delete(element);
|
|
193
309
|
}
|
|
194
310
|
|
|
195
|
-
function setDisabled(focusTrap:
|
|
196
|
-
|
|
311
|
+
function setDisabled(focusTrap: OuiFocusTrap, state: OuiFocusTrapState, value: boolean): void {
|
|
312
|
+
state.disabled = value;
|
|
197
313
|
|
|
198
314
|
if (value) {
|
|
199
|
-
|
|
315
|
+
disabled.add(focusTrap);
|
|
200
316
|
} else {
|
|
201
|
-
|
|
317
|
+
disabled.delete(focusTrap);
|
|
202
318
|
}
|
|
203
319
|
}
|
|
204
320
|
|
|
205
321
|
function tabToElement(
|
|
206
|
-
|
|
322
|
+
instance: OuiFocusTrap,
|
|
207
323
|
elements: Element[],
|
|
208
324
|
index: number,
|
|
209
325
|
modifier: number,
|
|
@@ -211,18 +327,18 @@ function tabToElement(
|
|
|
211
327
|
const {length} = elements;
|
|
212
328
|
const next = clamp(index + modifier, 0, length - 1, true);
|
|
213
329
|
|
|
214
|
-
if (
|
|
330
|
+
if (instance.disabled) {
|
|
215
331
|
if ((index === length - 1 && next === 0) || (index === 0 && next === length - 1)) {
|
|
216
332
|
lastTarget = undefined;
|
|
217
333
|
|
|
218
|
-
|
|
334
|
+
instance.enable();
|
|
219
335
|
}
|
|
220
336
|
|
|
221
337
|
return;
|
|
222
338
|
}
|
|
223
339
|
|
|
224
340
|
const element = elements[next] as HTMLElement;
|
|
225
|
-
const elementFocusTrap =
|
|
341
|
+
const elementFocusTrap = instances.get(element);
|
|
226
342
|
|
|
227
343
|
if (elementFocusTrap != null) {
|
|
228
344
|
elementFocusTrap.focus(modifier === -1);
|
|
@@ -234,15 +350,15 @@ function tabToElement(
|
|
|
234
350
|
}
|
|
235
351
|
|
|
236
352
|
function tabToTrap(
|
|
237
|
-
|
|
353
|
+
instance: OuiFocusTrap,
|
|
238
354
|
elements: Element[],
|
|
239
355
|
element: HTMLElement,
|
|
240
356
|
index?: number,
|
|
241
357
|
): void {
|
|
242
|
-
if (
|
|
358
|
+
if (instance.disabled) {
|
|
243
359
|
lastTarget = undefined;
|
|
244
360
|
|
|
245
|
-
|
|
361
|
+
instance.enable();
|
|
246
362
|
} else {
|
|
247
363
|
const next = elements.length === 0 ? element : elements.at(index ?? 0);
|
|
248
364
|
|
|
@@ -252,30 +368,40 @@ function tabToTrap(
|
|
|
252
368
|
}
|
|
253
369
|
}
|
|
254
370
|
|
|
255
|
-
//
|
|
371
|
+
// #endregion
|
|
256
372
|
|
|
257
|
-
|
|
373
|
+
// #region Variables
|
|
258
374
|
|
|
259
|
-
const
|
|
375
|
+
export const ATTRIBUTE_FOCUSTRAP = 'oui-focus-trap';
|
|
376
|
+
|
|
377
|
+
export const ATTRIBUTE_FOCUSTRAP_CONTAIN = `${ATTRIBUTE_FOCUSTRAP}-contain`;
|
|
378
|
+
|
|
379
|
+
export const ATTRIBUTE_FOCUSTRAP_NOESCAPE = `${ATTRIBUTE_FOCUSTRAP}-noescape`;
|
|
260
380
|
|
|
261
381
|
const EVENT_OPTIONS: AddEventListenerOptions = {
|
|
262
382
|
capture: true,
|
|
263
383
|
passive: false,
|
|
264
384
|
};
|
|
265
385
|
|
|
266
|
-
|
|
386
|
+
const MESSAGE = 'Element must be an HTMLElement or SVGElement';
|
|
387
|
+
|
|
388
|
+
const SELECTOR = `[${ATTRIBUTE_FOCUSTRAP}]`;
|
|
267
389
|
|
|
268
|
-
|
|
390
|
+
const disabled = new Set<OuiFocusTrap>();
|
|
269
391
|
|
|
270
|
-
const
|
|
392
|
+
const instances = new WeakMap<HTMLElement, OuiFocusTrap>();
|
|
271
393
|
|
|
272
|
-
const
|
|
394
|
+
const states = new WeakMap<OuiFocusTrap, OuiFocusTrapState>();
|
|
273
395
|
|
|
274
396
|
let lastTarget: HTMLElement | undefined;
|
|
275
397
|
|
|
276
|
-
//
|
|
398
|
+
// #endregion
|
|
399
|
+
|
|
400
|
+
// #region Initialization
|
|
277
401
|
|
|
278
402
|
on(document, 'focusin', onFocusIn, EVENT_OPTIONS);
|
|
279
403
|
on(document, 'keydown', onKeydown, EVENT_OPTIONS);
|
|
280
404
|
on(document, 'mousedown', onPointerdown, EVENT_OPTIONS);
|
|
281
405
|
on(document, 'touchstart', onPointerdown, EVENT_OPTIONS);
|
|
406
|
+
|
|
407
|
+
// #endregion
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {attributable} from '../internal/attributable';
|
|
2
|
-
import {createFocusTrap,
|
|
2
|
+
import {ATTRIBUTE_FOCUSTRAP, createFocusTrap, removeFocusTrap, type OuiFocusTrap} from './embedded';
|
|
3
3
|
|
|
4
|
-
attributable(
|
|
4
|
+
attributable(ATTRIBUTE_FOCUSTRAP, createFocusTrap, removeFocusTrap);
|
|
5
|
+
|
|
6
|
+
export {createFocusTrap, type OuiFocusTrap};
|