@oscarpalmer/atoms 0.7.0 → 0.8.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/atoms.js +167 -22
- package/dist/js/touch.js +22 -0
- package/package.json +7 -2
- package/src/js/element/focusable.ts +275 -0
- package/src/js/{element.ts → element/index.ts} +31 -7
- package/src/js/event.ts +0 -33
- package/src/js/index.ts +1 -0
- package/src/js/touch.ts +34 -0
- package/types/element/focusable.d.ts +19 -0
- package/types/{element.d.ts → element/index.d.ts} +8 -2
- package/types/event.d.ts +0 -4
- package/types/index.d.ts +1 -0
- package/types/touch.d.ts +5 -0
package/dist/js/atoms.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/js/element.ts
|
|
1
|
+
// src/js/element/index.ts
|
|
2
2
|
function findParentElement(origin, selector) {
|
|
3
3
|
if (origin == null || selector == null) {
|
|
4
4
|
return;
|
|
@@ -18,13 +18,171 @@ function findParentElement(origin, selector) {
|
|
|
18
18
|
}
|
|
19
19
|
return parent ?? undefined;
|
|
20
20
|
}
|
|
21
|
-
function getElementUnderPointer(
|
|
21
|
+
function getElementUnderPointer(skipIgnore) {
|
|
22
22
|
const elements = Array.from(document.querySelectorAll(":hover")).filter((element) => {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
if (headPattern.test(element.tagName)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const style = getComputedStyle(element);
|
|
27
|
+
return typeof skipIgnore === "boolean" && skipIgnore || style.pointerEvents !== "none" && style.visibility !== "hidden";
|
|
25
28
|
});
|
|
26
29
|
return elements[elements.length - 1];
|
|
27
30
|
}
|
|
31
|
+
function getTextDirection(element) {
|
|
32
|
+
const attribute = element.getAttribute("dir");
|
|
33
|
+
if (attribute !== null && directionPattern.test(attribute)) {
|
|
34
|
+
return attribute.toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
return getComputedStyle?.(element)?.direction === "rtl" ? "rtl" : "ltr";
|
|
37
|
+
}
|
|
38
|
+
var directionPattern = /^(ltr|rtl)$/i;
|
|
39
|
+
var headPattern = /^head$/i;
|
|
40
|
+
// src/js/element/focusable.ts
|
|
41
|
+
var _getItem = function(type, element) {
|
|
42
|
+
return {
|
|
43
|
+
element,
|
|
44
|
+
tabIndex: type === "focusable" ? -1 : _getTabIndex(element)
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
var _getTabIndex = function(element) {
|
|
48
|
+
if (element.tabIndex < 0 && (audioDetailsVideoPattern.test(element.tagName) || _isEditable(element)) && !_hasTabIndex(element)) {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
return element.tabIndex;
|
|
52
|
+
};
|
|
53
|
+
var _getValidElements = function(type, parent, filters) {
|
|
54
|
+
const items = Array.from(parent.querySelectorAll(selector)).map((element) => _getItem(type, element)).filter((item) => !filters.some((filter) => filter(item)));
|
|
55
|
+
if (type === "focusable") {
|
|
56
|
+
return items.map((item) => item.element);
|
|
57
|
+
}
|
|
58
|
+
const indiced = [];
|
|
59
|
+
const zeroed = [];
|
|
60
|
+
const { length } = items;
|
|
61
|
+
let position = Number(length);
|
|
62
|
+
while (position--) {
|
|
63
|
+
const index = length - position - 1;
|
|
64
|
+
const item = items[index];
|
|
65
|
+
if (item.tabIndex === 0) {
|
|
66
|
+
zeroed.push(item.element);
|
|
67
|
+
} else {
|
|
68
|
+
indiced[item.tabIndex] = [
|
|
69
|
+
...indiced[item.tabIndex] ?? [],
|
|
70
|
+
item.element
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return [...indiced.flat(), ...zeroed];
|
|
75
|
+
};
|
|
76
|
+
var _hasTabIndex = function(element) {
|
|
77
|
+
return !Number.isNaN(Number.parseInt(element.getAttribute("tabindex"), 10));
|
|
78
|
+
};
|
|
79
|
+
var _isDisabled = function(item) {
|
|
80
|
+
if (inputPattern.test(item.element.tagName) && _isDisabledFromFieldset(item.element)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return (item.element.disabled ?? false) || item.element.getAttribute("aria-disabled") === "true";
|
|
84
|
+
};
|
|
85
|
+
var _isDisabledFromFieldset = function(element) {
|
|
86
|
+
let parent = element.parentElement;
|
|
87
|
+
while (parent !== null) {
|
|
88
|
+
if (parent instanceof HTMLFieldSetElement && parent.disabled) {
|
|
89
|
+
const children = Array.from(parent.children);
|
|
90
|
+
for (const child of children) {
|
|
91
|
+
if (child instanceof HTMLLegendElement) {
|
|
92
|
+
return parent.matches("fieldset[disabled] *") ? true : !child.contains(element);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
parent = parent.parentElement;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
};
|
|
101
|
+
var _isEditable = function(element) {
|
|
102
|
+
return booleanPattern.test(element.getAttribute("contenteditable"));
|
|
103
|
+
};
|
|
104
|
+
var _isHidden = function(item) {
|
|
105
|
+
if ((item.element.hidden ?? false) || item.element instanceof HTMLInputElement && item.element.type === "hidden") {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
const isDirectSummary = item.element.matches("details > summary:first-of-type");
|
|
109
|
+
const nodeUnderDetails = isDirectSummary ? item.element.parentElement : item.element;
|
|
110
|
+
if (nodeUnderDetails?.matches("details:not([open]) *") ?? false) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
const style = getComputedStyle(item.element);
|
|
114
|
+
if (style.display === "none" || style.visibility === "hidden") {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
const { height, width } = item.element.getBoundingClientRect();
|
|
118
|
+
return height === 0 && width === 0;
|
|
119
|
+
};
|
|
120
|
+
var _isInert = function(item) {
|
|
121
|
+
return (item.element.inert ?? false) || booleanPattern.test(item.element.getAttribute("inert")) || item.element.parentElement !== null && _isInert({
|
|
122
|
+
element: item.element.parentElement,
|
|
123
|
+
tabIndex: -1
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
var _isNotTabbable = function(item) {
|
|
127
|
+
return (item.tabIndex ?? -1) < 0;
|
|
128
|
+
};
|
|
129
|
+
var _isNotTabbableRadio = function(item) {
|
|
130
|
+
if (!(item.element instanceof HTMLInputElement) || item.element.type !== "radio" || !item.element.name || item.element.checked) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const parent = item.element.form ?? item.element.getRootNode?.() ?? item.element.ownerDocument;
|
|
134
|
+
const realName = CSS?.escape?.(item.element.name) ?? item.element.name;
|
|
135
|
+
const radios = Array.from(parent.querySelectorAll(`input[type="radio"][name="${realName}"]`));
|
|
136
|
+
const checked = radios.find((radio) => radio.checked);
|
|
137
|
+
return checked !== undefined && checked !== item.element;
|
|
138
|
+
};
|
|
139
|
+
var _isSummarised = function(item) {
|
|
140
|
+
return item.element instanceof HTMLDetailsElement && Array.from(item.element.children).some((child) => summaryPattern.test(child.tagName));
|
|
141
|
+
};
|
|
142
|
+
var _isValidElement = function(element, filters) {
|
|
143
|
+
const item = _getItem("focusable", element);
|
|
144
|
+
return !filters.some((filter) => filter(item));
|
|
145
|
+
};
|
|
146
|
+
function getFocusableElements(parent) {
|
|
147
|
+
return _getValidElements("focusable", parent, focusableFilters);
|
|
148
|
+
}
|
|
149
|
+
function getTabbableElements(parent) {
|
|
150
|
+
return _getValidElements("tabbable", parent, tabbableFilters);
|
|
151
|
+
}
|
|
152
|
+
function isFocusableElement(element) {
|
|
153
|
+
return _isValidElement(element, focusableFilters);
|
|
154
|
+
}
|
|
155
|
+
function isTabbableElement(element) {
|
|
156
|
+
return _isValidElement(element, tabbableFilters);
|
|
157
|
+
}
|
|
158
|
+
var audioDetailsVideoPattern = /^(audio|details|video)$/i;
|
|
159
|
+
var booleanPattern = /^(|true)$/i;
|
|
160
|
+
var focusableFilters = [
|
|
161
|
+
_isDisabled,
|
|
162
|
+
_isInert,
|
|
163
|
+
_isHidden,
|
|
164
|
+
_isSummarised
|
|
165
|
+
];
|
|
166
|
+
var inputPattern = /^(button|input|select|textarea)$/i;
|
|
167
|
+
var selector = [
|
|
168
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
169
|
+
"[tabindex]:not(slot)",
|
|
170
|
+
"a[href]",
|
|
171
|
+
"audio[controls]",
|
|
172
|
+
"button",
|
|
173
|
+
"details",
|
|
174
|
+
"details > summary:first-of-type",
|
|
175
|
+
"input",
|
|
176
|
+
"select",
|
|
177
|
+
"textarea",
|
|
178
|
+
"video[controls]"
|
|
179
|
+
].map((selector2) => `${selector2}:not([inert])`).join(",");
|
|
180
|
+
var summaryPattern = /^summary$/i;
|
|
181
|
+
var tabbableFilters = [
|
|
182
|
+
_isNotTabbable,
|
|
183
|
+
_isNotTabbableRadio,
|
|
184
|
+
...focusableFilters
|
|
185
|
+
];
|
|
28
186
|
// src/js/event.ts
|
|
29
187
|
function getPosition(event) {
|
|
30
188
|
let x;
|
|
@@ -38,23 +196,6 @@ function getPosition(event) {
|
|
|
38
196
|
}
|
|
39
197
|
return typeof x === "number" && typeof y === "number" ? { x, y } : undefined;
|
|
40
198
|
}
|
|
41
|
-
var supportsTouch = (() => {
|
|
42
|
-
let value = false;
|
|
43
|
-
try {
|
|
44
|
-
if ("matchMedia" in window) {
|
|
45
|
-
const media = matchMedia("(pointer: coarse)");
|
|
46
|
-
if (typeof media?.matches === "boolean") {
|
|
47
|
-
value = media.matches;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (!value) {
|
|
51
|
-
value = "ontouchstart" in window || navigator.maxTouchPoints > 0 || (navigator.msMaxTouchPoints ?? 0) > 0;
|
|
52
|
-
}
|
|
53
|
-
} catch {
|
|
54
|
-
value = false;
|
|
55
|
-
}
|
|
56
|
-
return value;
|
|
57
|
-
})();
|
|
58
199
|
// src/js/number.ts
|
|
59
200
|
function clampNumber(value, min, max) {
|
|
60
201
|
return Math.min(Math.max(getNumber(value), getNumber(min)), getNumber(max));
|
|
@@ -173,16 +314,20 @@ var objectConstructor = "Object";
|
|
|
173
314
|
var constructors = new Set(["Array", objectConstructor]);
|
|
174
315
|
var numberExpression = /^\d+$/;
|
|
175
316
|
export {
|
|
176
|
-
supportsTouch,
|
|
177
317
|
setValue,
|
|
318
|
+
isTabbableElement,
|
|
178
319
|
isObject,
|
|
179
320
|
isNullableOrWhitespace,
|
|
180
321
|
isNullable,
|
|
322
|
+
isFocusableElement,
|
|
181
323
|
isArrayOrObject,
|
|
182
324
|
getValue,
|
|
325
|
+
getTextDirection,
|
|
326
|
+
getTabbableElements,
|
|
183
327
|
getString,
|
|
184
328
|
getPosition,
|
|
185
329
|
getNumber,
|
|
330
|
+
getFocusableElements,
|
|
186
331
|
getElementUnderPointer,
|
|
187
332
|
findParentElement,
|
|
188
333
|
createUuid,
|
package/dist/js/touch.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/js/touch.ts
|
|
2
|
+
var supportsTouch = (() => {
|
|
3
|
+
let value = false;
|
|
4
|
+
try {
|
|
5
|
+
if ("matchMedia" in window) {
|
|
6
|
+
const media = matchMedia("(pointer: coarse)");
|
|
7
|
+
if (typeof media?.matches === "boolean") {
|
|
8
|
+
value = media.matches;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
if (!value) {
|
|
12
|
+
value = "ontouchstart" in window || navigator.maxTouchPoints > 0 || (navigator.msMaxTouchPoints ?? 0) > 0;
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
value = false;
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
})();
|
|
19
|
+
var touch_default = supportsTouch;
|
|
20
|
+
export {
|
|
21
|
+
touch_default as default
|
|
22
|
+
};
|
package/package.json
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"types": "./types/index.d.ts",
|
|
17
17
|
"import": "./dist/js/atoms.js"
|
|
18
18
|
},
|
|
19
|
+
"./supports-touch": {
|
|
20
|
+
"types": "./types/touch.d.ts",
|
|
21
|
+
"import": "./dist/js/touch.js"
|
|
22
|
+
},
|
|
19
23
|
"./package.json": "./package.json"
|
|
20
24
|
},
|
|
21
25
|
"files": ["dist", "src", "types"],
|
|
@@ -29,9 +33,10 @@
|
|
|
29
33
|
"url": "git+https://github.com/oscarpalmer/atoms.git"
|
|
30
34
|
},
|
|
31
35
|
"scripts": {
|
|
32
|
-
"build": "bun run build:css && bun run build:js && bun run types",
|
|
36
|
+
"build": "bun run build:css && bun run build:js && bun run build:js-touch && bun run types",
|
|
33
37
|
"build:css": "bunx sass ./src/css:./dist/css --no-source-map",
|
|
34
38
|
"build:js": "bunx bun build ./src/js/index.ts --outfile ./dist/js/atoms.js",
|
|
39
|
+
"build:js-touch": "bunx bun build ./src/js/touch.ts --outfile ./dist/js/touch.js",
|
|
35
40
|
"test": "bun test --preload ./test/_preload.ts --coverage",
|
|
36
41
|
"types": "bunx tsc -p ./tsconfig.json",
|
|
37
42
|
"watch:css": "bunx sass ./src/css:./dist/css --no-source-map --watch",
|
|
@@ -39,5 +44,5 @@
|
|
|
39
44
|
},
|
|
40
45
|
"type": "module",
|
|
41
46
|
"types": "./types/index.d.ts",
|
|
42
|
-
"version": "0.
|
|
47
|
+
"version": "0.8.0"
|
|
43
48
|
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// Based on https://github.com/focus-trap/tabbable :-)
|
|
2
|
+
|
|
3
|
+
type ElementWithTabIndex = {
|
|
4
|
+
element: FocusableElement;
|
|
5
|
+
tabIndex: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type Filter = (item: ElementWithTabIndex) => boolean;
|
|
9
|
+
type FocusableElement = HTMLElement | SVGElement;
|
|
10
|
+
type GetType = 'focusable' | 'tabbable';
|
|
11
|
+
type InertElement = FocusableElement & {inert: boolean};
|
|
12
|
+
type TabbableElement = FocusableElement;
|
|
13
|
+
|
|
14
|
+
const audioDetailsVideoPattern = /^(audio|details|video)$/i;
|
|
15
|
+
const booleanPattern = /^(|true)$/i;
|
|
16
|
+
|
|
17
|
+
const focusableFilters: Filter[] = [
|
|
18
|
+
_isDisabled,
|
|
19
|
+
_isInert,
|
|
20
|
+
_isHidden,
|
|
21
|
+
_isSummarised,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const inputPattern = /^(button|input|select|textarea)$/i;
|
|
25
|
+
|
|
26
|
+
const selector = [
|
|
27
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
28
|
+
'[tabindex]:not(slot)',
|
|
29
|
+
'a[href]',
|
|
30
|
+
'audio[controls]',
|
|
31
|
+
'button',
|
|
32
|
+
'details',
|
|
33
|
+
'details > summary:first-of-type',
|
|
34
|
+
'input',
|
|
35
|
+
'select',
|
|
36
|
+
'textarea',
|
|
37
|
+
'video[controls]',
|
|
38
|
+
]
|
|
39
|
+
.map(selector => `${selector}:not([inert])`)
|
|
40
|
+
.join(',');
|
|
41
|
+
|
|
42
|
+
const summaryPattern = /^summary$/i;
|
|
43
|
+
|
|
44
|
+
const tabbableFilters: Filter[] = [
|
|
45
|
+
_isNotTabbable,
|
|
46
|
+
_isNotTabbableRadio,
|
|
47
|
+
...focusableFilters,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function _getItem(
|
|
51
|
+
type: GetType,
|
|
52
|
+
element: FocusableElement,
|
|
53
|
+
): ElementWithTabIndex {
|
|
54
|
+
return {
|
|
55
|
+
element,
|
|
56
|
+
tabIndex: type === 'focusable' ? -1 : _getTabIndex(element),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function _getTabIndex(element: FocusableElement): number {
|
|
61
|
+
if (
|
|
62
|
+
element.tabIndex < 0 &&
|
|
63
|
+
(audioDetailsVideoPattern.test(element.tagName) || _isEditable(element)) &&
|
|
64
|
+
!_hasTabIndex(element)
|
|
65
|
+
) {
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return element.tabIndex;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _getValidElements(
|
|
73
|
+
type: GetType,
|
|
74
|
+
parent: Element,
|
|
75
|
+
filters: Filter[],
|
|
76
|
+
): Array<FocusableElement> {
|
|
77
|
+
const items: ElementWithTabIndex[] = Array.from(
|
|
78
|
+
parent.querySelectorAll(selector),
|
|
79
|
+
)
|
|
80
|
+
.map(element => _getItem(type, element as FocusableElement))
|
|
81
|
+
.filter(item => !filters.some(filter => filter(item)));
|
|
82
|
+
|
|
83
|
+
if (type === 'focusable') {
|
|
84
|
+
return items.map(item => item.element);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const indiced: Array<Array<FocusableElement>> = [];
|
|
88
|
+
const zeroed: Array<FocusableElement> = [];
|
|
89
|
+
|
|
90
|
+
const {length} = items;
|
|
91
|
+
|
|
92
|
+
let position = Number(length);
|
|
93
|
+
|
|
94
|
+
while (position--) {
|
|
95
|
+
const index = length - position - 1;
|
|
96
|
+
const item = items[index];
|
|
97
|
+
|
|
98
|
+
if (item.tabIndex === 0) {
|
|
99
|
+
zeroed.push(item.element);
|
|
100
|
+
} else {
|
|
101
|
+
indiced[item.tabIndex] = [
|
|
102
|
+
...(indiced[item.tabIndex] ?? []),
|
|
103
|
+
item.element,
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return [...indiced.flat(), ...zeroed];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function _hasTabIndex(element: FocusableElement): boolean {
|
|
112
|
+
return !Number.isNaN(
|
|
113
|
+
Number.parseInt(element.getAttribute('tabindex') as string, 10),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _isDisabled(item: ElementWithTabIndex): boolean {
|
|
118
|
+
if (
|
|
119
|
+
inputPattern.test(item.element.tagName) &&
|
|
120
|
+
_isDisabledFromFieldset(item.element)
|
|
121
|
+
) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
((item.element as HTMLInputElement).disabled ?? false) ||
|
|
127
|
+
item.element.getAttribute('aria-disabled') === 'true'
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _isDisabledFromFieldset(element: FocusableElement): boolean {
|
|
132
|
+
let parent = element.parentElement;
|
|
133
|
+
|
|
134
|
+
while (parent !== null) {
|
|
135
|
+
if (parent instanceof HTMLFieldSetElement && parent.disabled) {
|
|
136
|
+
const children = Array.from(parent.children);
|
|
137
|
+
|
|
138
|
+
for (const child of children) {
|
|
139
|
+
if (child instanceof HTMLLegendElement) {
|
|
140
|
+
return parent.matches('fieldset[disabled] *')
|
|
141
|
+
? true
|
|
142
|
+
: !child.contains(element);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
parent = parent.parentElement;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function _isEditable(element: Element): boolean {
|
|
156
|
+
return booleanPattern.test(element.getAttribute('contenteditable') as string);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function _isHidden(item: ElementWithTabIndex) {
|
|
160
|
+
if (
|
|
161
|
+
((item.element as HTMLElement).hidden ?? false) ||
|
|
162
|
+
(item.element instanceof HTMLInputElement && item.element.type === 'hidden')
|
|
163
|
+
) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const isDirectSummary = item.element.matches(
|
|
168
|
+
'details > summary:first-of-type',
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const nodeUnderDetails = isDirectSummary
|
|
172
|
+
? item.element.parentElement
|
|
173
|
+
: item.element;
|
|
174
|
+
|
|
175
|
+
if (nodeUnderDetails?.matches('details:not([open]) *') ?? false) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const style = getComputedStyle(item.element);
|
|
180
|
+
|
|
181
|
+
if (style.display === 'none' || style.visibility === 'hidden') {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const {height, width} = item.element.getBoundingClientRect();
|
|
186
|
+
|
|
187
|
+
return height === 0 && width === 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function _isInert(item: ElementWithTabIndex): boolean {
|
|
191
|
+
return (
|
|
192
|
+
((item.element as InertElement).inert ?? false) ||
|
|
193
|
+
booleanPattern.test(item.element.getAttribute('inert') as string) ||
|
|
194
|
+
(item.element.parentElement !== null &&
|
|
195
|
+
_isInert({
|
|
196
|
+
element: item.element.parentElement as FocusableElement,
|
|
197
|
+
tabIndex: -1,
|
|
198
|
+
}))
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function _isNotTabbable(item: ElementWithTabIndex) {
|
|
203
|
+
return (item.tabIndex ?? -1) < 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function _isNotTabbableRadio(item: ElementWithTabIndex): boolean {
|
|
207
|
+
if (
|
|
208
|
+
!(item.element instanceof HTMLInputElement) ||
|
|
209
|
+
item.element.type !== 'radio' ||
|
|
210
|
+
!item.element.name ||
|
|
211
|
+
item.element.checked
|
|
212
|
+
) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const parent =
|
|
217
|
+
item.element.form ??
|
|
218
|
+
item.element.getRootNode?.() ??
|
|
219
|
+
item.element.ownerDocument;
|
|
220
|
+
|
|
221
|
+
const realName = CSS?.escape?.(item.element.name) ?? item.element.name;
|
|
222
|
+
|
|
223
|
+
const radios = Array.from(
|
|
224
|
+
(parent as Element).querySelectorAll(
|
|
225
|
+
`input[type="radio"][name="${realName}"]`,
|
|
226
|
+
),
|
|
227
|
+
) as HTMLInputElement[];
|
|
228
|
+
|
|
229
|
+
const checked = radios.find(radio => radio.checked);
|
|
230
|
+
|
|
231
|
+
return checked !== undefined && checked !== item.element;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function _isSummarised(item: ElementWithTabIndex) {
|
|
235
|
+
return (
|
|
236
|
+
item.element instanceof HTMLDetailsElement &&
|
|
237
|
+
Array.from(item.element.children).some(child =>
|
|
238
|
+
summaryPattern.test(child.tagName),
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function _isValidElement(element: Element, filters: Filter[]): boolean {
|
|
244
|
+
const item = _getItem('focusable', element as FocusableElement);
|
|
245
|
+
|
|
246
|
+
return !filters.some(filter => filter(item));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get a list of focusable elements within a parent element
|
|
251
|
+
*/
|
|
252
|
+
export function getFocusableElements(parent: Element): FocusableElement[] {
|
|
253
|
+
return _getValidElements('focusable', parent, focusableFilters);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get a list of tabbable elements within a parent element
|
|
258
|
+
*/
|
|
259
|
+
export function getTabbableElements(parent: Element): TabbableElement[] {
|
|
260
|
+
return _getValidElements('tabbable', parent, tabbableFilters);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Is the element focusable?
|
|
265
|
+
*/
|
|
266
|
+
export function isFocusableElement(element: Element): boolean {
|
|
267
|
+
return _isValidElement(element, focusableFilters);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Is the element tabbable?
|
|
272
|
+
*/
|
|
273
|
+
export function isTabbableElement(element: Element): boolean {
|
|
274
|
+
return _isValidElement(element, tabbableFilters);
|
|
275
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
type TextDirection = 'ltr' | 'rtl';
|
|
2
|
+
|
|
3
|
+
const directionPattern = /^(ltr|rtl)$/i;
|
|
4
|
+
const headPattern = /^head$/i;
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* - Find the parent element that matches the selector
|
|
3
8
|
* - Matches may be found by a query string or a callback
|
|
@@ -38,21 +43,40 @@ export function findParentElement(
|
|
|
38
43
|
/**
|
|
39
44
|
* - Get the most specific element under the pointer
|
|
40
45
|
* - Ignores elements with `pointer-events: none` and `visibility: hidden`
|
|
41
|
-
* - If `
|
|
46
|
+
* - If `skipIgnore` is `true`, no elements are ignored
|
|
42
47
|
*/
|
|
43
|
-
export function getElementUnderPointer(
|
|
48
|
+
export function getElementUnderPointer(
|
|
49
|
+
skipIgnore?: boolean,
|
|
50
|
+
): Element | undefined {
|
|
44
51
|
const elements = Array.from(document.querySelectorAll(':hover')).filter(
|
|
45
52
|
element => {
|
|
46
|
-
|
|
53
|
+
if (headPattern.test(element.tagName)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const style = getComputedStyle(element);
|
|
47
58
|
|
|
48
59
|
return (
|
|
49
|
-
|
|
50
|
-
(
|
|
51
|
-
? true
|
|
52
|
-
: style.pointerEvents !== 'none' && style.visibility !== 'hidden')
|
|
60
|
+
(typeof skipIgnore === 'boolean' && skipIgnore) ||
|
|
61
|
+
(style.pointerEvents !== 'none' && style.visibility !== 'hidden')
|
|
53
62
|
);
|
|
54
63
|
},
|
|
55
64
|
);
|
|
56
65
|
|
|
57
66
|
return elements[elements.length - 1];
|
|
58
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the text direction of an element
|
|
71
|
+
*/
|
|
72
|
+
export function getTextDirection(element: Element): TextDirection {
|
|
73
|
+
const attribute = element.getAttribute('dir');
|
|
74
|
+
|
|
75
|
+
if (attribute !== null && directionPattern.test(attribute)) {
|
|
76
|
+
return attribute.toLowerCase() as TextDirection;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
getComputedStyle?.(element)?.direction === 'rtl' ? 'rtl' : 'ltr'
|
|
81
|
+
) as TextDirection;
|
|
82
|
+
}
|
package/src/js/event.ts
CHANGED
|
@@ -1,41 +1,8 @@
|
|
|
1
|
-
type NavigatorWithMsMaxTouchPoints = Navigator & {
|
|
2
|
-
msMaxTouchPoints: number;
|
|
3
|
-
};
|
|
4
|
-
|
|
5
1
|
type Position = {
|
|
6
2
|
x: number;
|
|
7
3
|
y: number;
|
|
8
4
|
};
|
|
9
5
|
|
|
10
|
-
/**
|
|
11
|
-
* Does the browser support touch events?
|
|
12
|
-
*/
|
|
13
|
-
export const supportsTouch = (() => {
|
|
14
|
-
let value = false;
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
if ('matchMedia' in window) {
|
|
18
|
-
const media = matchMedia('(pointer: coarse)');
|
|
19
|
-
|
|
20
|
-
if (typeof media?.matches === 'boolean') {
|
|
21
|
-
value = media.matches;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (!value) {
|
|
26
|
-
value =
|
|
27
|
-
'ontouchstart' in window ||
|
|
28
|
-
navigator.maxTouchPoints > 0 ||
|
|
29
|
-
((navigator as NavigatorWithMsMaxTouchPoints).msMaxTouchPoints ?? 0) >
|
|
30
|
-
0;
|
|
31
|
-
}
|
|
32
|
-
} catch {
|
|
33
|
-
value = false;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return value;
|
|
37
|
-
})();
|
|
38
|
-
|
|
39
6
|
/**
|
|
40
7
|
* Get the X- and Y-coordinates from a pointer event
|
|
41
8
|
*/
|
package/src/js/index.ts
CHANGED
package/src/js/touch.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
type NavigatorWithMsMaxTouchPoints = Navigator & {
|
|
2
|
+
msMaxTouchPoints: number;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Does the browser/device support touch?
|
|
7
|
+
*/
|
|
8
|
+
const supportsTouch = (() => {
|
|
9
|
+
let value = false;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
if ('matchMedia' in window) {
|
|
13
|
+
const media = matchMedia('(pointer: coarse)');
|
|
14
|
+
|
|
15
|
+
if (typeof media?.matches === 'boolean') {
|
|
16
|
+
value = media.matches;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!value) {
|
|
21
|
+
value =
|
|
22
|
+
'ontouchstart' in window ||
|
|
23
|
+
navigator.maxTouchPoints > 0 ||
|
|
24
|
+
((navigator as NavigatorWithMsMaxTouchPoints).msMaxTouchPoints ?? 0) >
|
|
25
|
+
0;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
value = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return value;
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
export default supportsTouch;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type FocusableElement = HTMLElement | SVGElement;
|
|
2
|
+
type TabbableElement = FocusableElement;
|
|
3
|
+
/**
|
|
4
|
+
* Get a list of focusable elements within a parent element
|
|
5
|
+
*/
|
|
6
|
+
export declare function getFocusableElements(parent: Element): FocusableElement[];
|
|
7
|
+
/**
|
|
8
|
+
* Get a list of tabbable elements within a parent element
|
|
9
|
+
*/
|
|
10
|
+
export declare function getTabbableElements(parent: Element): TabbableElement[];
|
|
11
|
+
/**
|
|
12
|
+
* Is the element focusable?
|
|
13
|
+
*/
|
|
14
|
+
export declare function isFocusableElement(element: Element): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Is the element tabbable?
|
|
17
|
+
*/
|
|
18
|
+
export declare function isTabbableElement(element: Element): boolean;
|
|
19
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
type TextDirection = 'ltr' | 'rtl';
|
|
1
2
|
/**
|
|
2
3
|
* - Find the parent element that matches the selector
|
|
3
4
|
* - Matches may be found by a query string or a callback
|
|
@@ -6,6 +7,11 @@ export declare function findParentElement(origin: Element, selector: string | ((
|
|
|
6
7
|
/**
|
|
7
8
|
* - Get the most specific element under the pointer
|
|
8
9
|
* - Ignores elements with `pointer-events: none` and `visibility: hidden`
|
|
9
|
-
* - If `
|
|
10
|
+
* - If `skipIgnore` is `true`, no elements are ignored
|
|
10
11
|
*/
|
|
11
|
-
export declare function getElementUnderPointer(
|
|
12
|
+
export declare function getElementUnderPointer(skipIgnore?: boolean): Element | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Get the text direction of an element
|
|
15
|
+
*/
|
|
16
|
+
export declare function getTextDirection(element: Element): TextDirection;
|
|
17
|
+
export {};
|
package/types/event.d.ts
CHANGED
package/types/index.d.ts
CHANGED