@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 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(all) {
21
+ function getElementUnderPointer(skipIgnore) {
22
22
  const elements = Array.from(document.querySelectorAll(":hover")).filter((element) => {
23
- const style = window.getComputedStyle(element);
24
- return element.tagName !== "HEAD" && (typeof all === "boolean" && all ? true : style.pointerEvents !== "none" && style.visibility !== "hidden");
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,
@@ -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.7.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 `all` is `true`, all elements under the pointer are returned
46
+ * - If `skipIgnore` is `true`, no elements are ignored
42
47
  */
43
- export function getElementUnderPointer(all?: boolean): Element | undefined {
48
+ export function getElementUnderPointer(
49
+ skipIgnore?: boolean,
50
+ ): Element | undefined {
44
51
  const elements = Array.from(document.querySelectorAll(':hover')).filter(
45
52
  element => {
46
- const style = window.getComputedStyle(element);
53
+ if (headPattern.test(element.tagName)) {
54
+ return false;
55
+ }
56
+
57
+ const style = getComputedStyle(element);
47
58
 
48
59
  return (
49
- element.tagName !== 'HEAD' &&
50
- (typeof all === 'boolean' && all
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
@@ -1,4 +1,5 @@
1
1
  export * from './element';
2
+ export * from './element/focusable';
2
3
  export * from './event';
3
4
  export * from './number';
4
5
  export * from './string';
@@ -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 `all` is `true`, all elements under the pointer are returned
10
+ * - If `skipIgnore` is `true`, no elements are ignored
10
11
  */
11
- export declare function getElementUnderPointer(all?: boolean): Element | undefined;
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
@@ -2,10 +2,6 @@ type Position = {
2
2
  x: number;
3
3
  y: number;
4
4
  };
5
- /**
6
- * Does the browser support touch events?
7
- */
8
- export declare const supportsTouch: boolean;
9
5
  /**
10
6
  * Get the X- and Y-coordinates from a pointer event
11
7
  */
package/types/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './element';
2
+ export * from './element/focusable';
2
3
  export * from './event';
3
4
  export * from './number';
4
5
  export * from './string';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Does the browser/device support touch?
3
+ */
4
+ declare const supportsTouch: boolean;
5
+ export default supportsTouch;