@splendidlabz/utils 1.5.0 → 1.5.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/CHANGELOG.md +71 -1
- package/dist/dom/accessibility.d.ts +28 -0
- package/dist/dom/actions/index.d.ts +3 -0
- package/dist/dom/actions/masonry.d.ts +3 -0
- package/dist/dom/actions/prefer-horizontal-scroll.d.ts +3 -0
- package/dist/dom/actions/sticky.d.ts +3 -0
- package/dist/dom/bounding-box.d.ts +39 -0
- package/dist/dom/clipboard.d.ts +5 -0
- package/dist/dom/cookie.d.ts +11 -0
- package/dist/dom/css-vars.d.ts +4 -0
- package/dist/dom/events.d.ts +18 -0
- package/dist/dom/focusable.d.ts +76 -0
- package/dist/dom/font-size.d.ts +5 -0
- package/dist/dom/get-element.d.ts +100 -0
- package/dist/dom/hash.d.ts +1 -0
- package/dist/dom/index.d.ts +22 -0
- package/dist/dom/keyboard.d.ts +30 -0
- package/dist/dom/local-store.d.ts +29 -0
- package/dist/dom/media.d.ts +3 -0
- package/dist/dom/observers/index.d.ts +3 -0
- package/dist/dom/observers/intersection-observer.d.ts +8 -0
- package/dist/dom/observers/mutation-observer.d.ts +7 -0
- package/dist/dom/observers/observer.d.ts +1 -0
- package/dist/dom/observers/resize-observer.d.ts +38 -0
- package/dist/dom/pkce.d.ts +6 -0
- package/dist/dom/query-params.d.ts +4 -0
- package/dist/dom/random-string.d.ts +7 -0
- package/dist/dom/sanitize.d.ts +8 -0
- package/dist/dom/session-store.d.ts +5 -0
- package/dist/dom/trap-focus.d.ts +28 -0
- package/dist/dom/ui/aria-current.d.ts +5 -0
- package/dist/dom/ui/inconsistent-button-fix.d.ts +12 -0
- package/dist/dom/ui/index.d.ts +4 -0
- package/dist/dom/ui/scroll-container.d.ts +1 -0
- package/dist/dom/ui/traverse-and-scramble.d.ts +19 -0
- package/dist/lib/arrays/index.d.ts +5 -0
- package/dist/lib/arrays/item-position.d.ts +14 -0
- package/dist/lib/arrays/join.d.ts +13 -0
- package/dist/lib/arrays/shuffle.d.ts +1 -0
- package/dist/lib/arrays/sort.d.ts +30 -0
- package/dist/lib/arrays/unique.d.ts +1 -0
- package/dist/lib/auth/index.d.ts +1 -0
- package/dist/lib/auth/route-manager.d.ts +25 -0
- package/dist/lib/checks.d.ts +29 -0
- package/dist/lib/date/days.d.ts +5 -0
- package/dist/lib/date/index.d.ts +3 -0
- package/dist/lib/date/months.d.ts +9 -0
- package/dist/lib/date/time.d.ts +8 -0
- package/dist/lib/form/form-data.d.ts +7 -0
- package/dist/lib/form/index.d.ts +2 -0
- package/dist/lib/form/sanitize.d.ts +58 -0
- package/dist/lib/functions/debounce.d.ts +1 -0
- package/dist/lib/functions/env.d.ts +1 -0
- package/dist/lib/functions/functional.d.ts +4 -0
- package/dist/lib/functions/index.d.ts +5 -0
- package/dist/lib/functions/throttle.d.ts +1 -0
- package/dist/lib/functions/timeout.d.ts +3 -0
- package/dist/lib/index.d.ts +13 -0
- package/dist/lib/numbers/index.d.ts +2 -0
- package/dist/lib/numbers/math.d.ts +2 -0
- package/dist/lib/objects/camelcase-keys.d.ts +1 -0
- package/dist/lib/objects/empty.d.ts +2 -0
- package/dist/lib/objects/equal.d.ts +2 -0
- package/dist/lib/objects/extend.d.ts +4 -0
- package/dist/lib/objects/flatten.d.ts +4 -0
- package/dist/lib/objects/index.d.ts +13 -0
- package/dist/lib/objects/json.d.ts +1 -0
- package/dist/lib/objects/loop.d.ts +2 -0
- package/dist/lib/objects/mix/mix.d.ts +1 -0
- package/dist/lib/objects/nested-property.d.ts +22 -0
- package/dist/lib/objects/normalize-object.d.ts +1 -0
- package/dist/lib/objects/omit-empty.d.ts +1 -0
- package/dist/lib/objects/size.d.ts +1 -0
- package/dist/lib/objects/split.d.ts +10 -0
- package/dist/lib/promises/index.d.ts +1 -0
- package/dist/lib/promises/reject.d.ts +3 -0
- package/dist/lib/sse.d.ts +16 -0
- package/dist/lib/strings/convert-case/convert-case.d.ts +8 -0
- package/dist/lib/strings/index.d.ts +4 -0
- package/dist/lib/strings/markdown.d.ts +23 -0
- package/dist/lib/strings/pluralize.d.ts +1 -0
- package/dist/lib/strings/query-string.d.ts +8 -0
- package/dist/lib/style/index.d.ts +1 -0
- package/dist/lib/symbols/index.d.ts +1 -0
- package/dist/lib/symbols/symbols.d.ts +2 -0
- package/dist/node/common.d.ts +1 -0
- package/dist/node/dirname.d.ts +7 -0
- package/dist/node/file-cache.d.ts +54 -0
- package/dist/node/file.d.ts +2 -0
- package/dist/node/hash.d.ts +1 -0
- package/dist/node/index.d.ts +7 -0
- package/dist/node/pkce.d.ts +6 -0
- package/dist/node/random-string.d.ts +2 -0
- package/dist/node/sanitize.d.ts +8 -0
- package/dom/accessibility.js +46 -13
- package/{actions → dom/actions}/index.js +0 -2
- package/dom/actions/masonry.js +42 -0
- package/{actions → dom/actions}/prefer-horizontal-scroll.js +8 -8
- package/{actions → dom/actions}/sticky.js +3 -6
- package/dom/bounding-box.js +35 -7
- package/dom/css-vars.js +8 -0
- package/dom/events.js +2 -1
- package/dom/focusable.js +119 -10
- package/dom/font-size.js +21 -10
- package/dom/get-element.js +112 -5
- package/dom/index.js +3 -0
- package/dom/keyboard.js +13 -8
- package/dom/local-store.js +37 -0
- package/dom/media.js +1 -0
- package/dom/observers/index.js +3 -0
- package/dom/observers/intersection-observer.js +43 -0
- package/dom/observers/mutation-observer.js +51 -0
- package/dom/observers/observer.js +18 -0
- package/dom/observers/resize-observer.js +64 -0
- package/dom/sanitize.js +16 -0
- package/dom/trap-focus.js +18 -3
- package/dom/ui/inconsistent-button-fix.js +14 -2
- package/dom/ui/traverse-and-scramble.js +29 -8
- package/eslint.config.js +2 -0
- package/lib/arrays/index.js +5 -62
- package/lib/arrays/item-position.js +23 -0
- package/lib/arrays/join.js +20 -0
- package/lib/arrays/shuffle.js +21 -0
- package/lib/arrays/sort.js +88 -0
- package/lib/arrays/sort.spec.js +123 -0
- package/lib/arrays/unique.js +3 -0
- package/lib/checks.js +26 -0
- package/lib/date/index.js +2 -1
- package/lib/date/time.js +50 -0
- package/lib/form/sanitize.js +68 -35
- package/lib/form/sanitize.spec.js +126 -0
- package/lib/index.js +1 -0
- package/lib/numbers/index.js +2 -0
- package/lib/numbers/math.js +9 -0
- package/lib/objects/equal.js +9 -0
- package/lib/objects/index.js +1 -0
- package/lib/objects/mix/mix.js +2 -0
- package/lib/objects/mix/mix.test.js +16 -0
- package/lib/objects/nested-property.js +5 -0
- package/lib/sse.js +61 -0
- package/lib/strings/markdown.js +29 -10
- package/node/dirname.js +1 -1
- package/node/file-cache.js +46 -7
- package/node/index.js +1 -0
- package/node/sanitize.js +13 -0
- package/node/sanitize.spec.js +142 -0
- package/package.json +38 -13
- package/tsconfig.json +23 -0
- package/.eslintrc.cjs +0 -3
- package/.turbo/turbo-lint.log +0 -10
- package/.turbo/turbo-test.log +0 -10
- package/actions/intersection-observer.js +0 -37
- package/actions/masonry.js +0 -75
- package/actions/mutation-observer.js +0 -42
- package/actions/resize-observer.js +0 -33
- package/lib/index.test.js +0 -9
package/dom/accessibility.js
CHANGED
|
@@ -1,23 +1,56 @@
|
|
|
1
|
-
export function stopBodyScroll() {
|
|
2
|
-
|
|
1
|
+
// export function stopBodyScroll(element) {
|
|
2
|
+
// if (!element) element = document.body
|
|
3
|
+
// const top = document.documentElement.scrollTop
|
|
4
|
+
// element.style.top = -1 * top + 'px'
|
|
5
|
+
// element.style.overflow = 'hidden'
|
|
6
|
+
// }
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
// export function allowBodyScroll() {
|
|
9
|
+
// const top = parseFloat(document.body.style.top) * -1
|
|
10
|
+
// document.body.style.top = null
|
|
11
|
+
// document.body.style.overflow = null
|
|
12
|
+
// document.documentElement.scrollTop = top
|
|
13
|
+
// }
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Checks if an element has an accessible name through label or aria-labelledby
|
|
17
|
+
* @param {string} [label] - The label text
|
|
18
|
+
* @param {string} [labelledBy] - The aria-labelledby value
|
|
19
|
+
* @returns {boolean} True if the element has an accessible name
|
|
20
|
+
*/
|
|
21
|
+
export function hasAccessibleName(label, labelledBy) {
|
|
22
|
+
if (label || labelledBy) return true
|
|
23
|
+
return false
|
|
13
24
|
}
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Checks if an element has an accessible name and logs a warning/error if it doesn't
|
|
28
|
+
* @param {Object} [options] - Options for checking accessible name
|
|
29
|
+
* @param {string} [options.name] - The name of the element being checked
|
|
30
|
+
* @param {string} [options.ariaLabel] - The aria-label value
|
|
31
|
+
* @param {string} [options.ariaLabelledBy] - The aria-labelledby value
|
|
32
|
+
* @param {'warn'|'error'} [options.severity='warn'] - Severity level for the message
|
|
33
|
+
* @returns {boolean} True if the element has an accessible name
|
|
34
|
+
*/
|
|
35
|
+
export function checkForAccessibleName({
|
|
36
|
+
name,
|
|
37
|
+
ariaLabel,
|
|
38
|
+
ariaLabelledBy,
|
|
39
|
+
severity = 'warn',
|
|
40
|
+
} = {}) {
|
|
41
|
+
if (ariaLabel || ariaLabelledBy) return true
|
|
16
42
|
|
|
17
|
-
|
|
18
|
-
if (
|
|
43
|
+
const message = `${name} is missing an accessible name. Please provide an accessible name with 'aria-label' or 'aria-labelledby'.`
|
|
44
|
+
if (severity === 'warn') console.warn(message)
|
|
45
|
+
if (severity === 'error') console.error(message)
|
|
46
|
+
return false
|
|
19
47
|
}
|
|
20
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Checks if an ARIA popup value is valid
|
|
51
|
+
* @param {string} value - The ARIA popup value to check
|
|
52
|
+
* @returns {boolean} True if the value is invalid
|
|
53
|
+
*/
|
|
21
54
|
export function isInvalidAriaPopup(value) {
|
|
22
55
|
const allowedPopoupValues = ['dialog', 'menu', 'listbox', 'grid', 'tree']
|
|
23
56
|
if (!allowedPopoupValues.includes(value)) return true
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getChildrenElements } from '../get-element.js'
|
|
2
|
+
import { mediaLoaded } from '../media.js'
|
|
3
|
+
import { resizeObserver } from '../observers/resize-observer.js'
|
|
4
|
+
|
|
5
|
+
export async function masonry(node) {
|
|
6
|
+
if (nativeMasonrySupport(node)) return
|
|
7
|
+
|
|
8
|
+
const colGap = parseFloat(getComputedStyle(node).columnGap)
|
|
9
|
+
const items = getChildrenElements(node)
|
|
10
|
+
|
|
11
|
+
node.style.setProperty('row-gap', '1px', 'important')
|
|
12
|
+
node.style.gridAutoRows = '0px'
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
await mediaLoaded(node)
|
|
16
|
+
} catch (e) {}
|
|
17
|
+
|
|
18
|
+
layout({ colGap, items })
|
|
19
|
+
|
|
20
|
+
const obs = resizeObserver(node, {
|
|
21
|
+
async callback(opts) {
|
|
22
|
+
layout({ colGap, items })
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
async function layout({ colGap, items }) {
|
|
27
|
+
items.forEach(item => {
|
|
28
|
+
const ib = item.getBoundingClientRect()
|
|
29
|
+
item.style.gridRowEnd = `span ${Math.round(ib.height + colGap)}`
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
destroy() {
|
|
35
|
+
obs.disconnect()
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function nativeMasonrySupport(node) {
|
|
41
|
+
return getComputedStyle(node).gridTemplateRows === 'masonry'
|
|
42
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from '../
|
|
2
|
+
addListeners,
|
|
3
|
+
getCSSVar,
|
|
4
|
+
removeListeners,
|
|
5
|
+
setCSSVar,
|
|
6
|
+
} from '../index.js'
|
|
7
7
|
|
|
8
|
-
import { omitEmpty } from '
|
|
8
|
+
import { omitEmpty } from '../../lib/index.js'
|
|
9
9
|
|
|
10
10
|
const DEFAULT_OPTIONS = {
|
|
11
11
|
scrollSnapDelay: 1000,
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function preferHorizontalScroll(node, props = {}) {
|
|
15
|
-
if (!node.classList.contains('scrollable-prefer-horizontal
|
|
15
|
+
if (!node.classList.contains('scrollable-prefer-horizontal')) return
|
|
16
16
|
|
|
17
17
|
const state = {
|
|
18
18
|
origSnapType: null,
|
|
@@ -23,7 +23,7 @@ export function preferHorizontalScroll(node, props = {}) {
|
|
|
23
23
|
const options = {
|
|
24
24
|
...DEFAULT_OPTIONS,
|
|
25
25
|
...omitEmpty({
|
|
26
|
-
scrollSnapDelay: getCSSVar(node, '--
|
|
26
|
+
scrollSnapDelay: getCSSVar(node, '--scroll-snap-delay'),
|
|
27
27
|
}),
|
|
28
28
|
...omitEmpty(props),
|
|
29
29
|
}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from '../dom/index.js'
|
|
5
|
-
import { resizeObserver } from './resize-observer.js'
|
|
1
|
+
import { boundingBoxRelativeToAncestor } from '../bounding-box.js'
|
|
2
|
+
import { resizeObserver } from '../observers/resize-observer.js'
|
|
3
|
+
import { findScrollContainer } from '../ui/scroll-container.js'
|
|
6
4
|
|
|
7
5
|
export function sticky(node, props = {}) {
|
|
8
6
|
const scrollContainer = findScrollContainer(node)
|
|
9
|
-
|
|
10
7
|
let stickyTBLR = getComputedTBLR(node)
|
|
11
8
|
|
|
12
9
|
const obs = resizeObserver(node, {
|
package/dom/bounding-box.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Advanced
|
|
3
|
-
* @param {HTMLElement} element
|
|
2
|
+
* Advanced boundingClientRect that contains information vertical and horizontal centers.
|
|
3
|
+
* @param {HTMLElement} element - The element to get the bounding box for
|
|
4
|
+
* @param {Object} [options] - Options for the bounding box calculation
|
|
5
|
+
* @param {boolean} [options.containsPadding=true] - Whether to include padding in the calculations
|
|
6
|
+
* @returns {Object} The bounding box object with additional center coordinates
|
|
7
|
+
* @property {number} x - The x coordinate
|
|
8
|
+
* @property {number} y - The y coordinate
|
|
9
|
+
* @property {number} width - The width
|
|
10
|
+
* @property {number} height - The height
|
|
11
|
+
* @property {number} top - The top coordinate
|
|
12
|
+
* @property {number} right - The right coordinate
|
|
13
|
+
* @property {number} bottom - The bottom coordinate
|
|
14
|
+
* @property {number} left - The left coordinate
|
|
15
|
+
* @property {number} xCenter - The horizontal center coordinate
|
|
16
|
+
* @property {number} yCenter - The vertical center coordinate
|
|
4
17
|
*/
|
|
5
18
|
export function boundingBox(element, { containsPadding = true } = {}) {
|
|
6
19
|
const box = element.getBoundingClientRect()
|
|
@@ -12,11 +25,11 @@ export function boundingBox(element, { containsPadding = true } = {}) {
|
|
|
12
25
|
// Gets boundingClientRect value without padding
|
|
13
26
|
if (!containsPadding) {
|
|
14
27
|
const style = getComputedStyle(element)
|
|
15
|
-
|
|
16
|
-
paddingTop = parseFloat(paddingTop)
|
|
17
|
-
paddingRight = parseFloat(paddingRight)
|
|
18
|
-
paddingBottom = parseFloat(paddingBottom)
|
|
19
|
-
paddingLeft = parseFloat(paddingLeft)
|
|
28
|
+
// Parse padding values immediately to numbers
|
|
29
|
+
const paddingTop = parseFloat(style.paddingTop)
|
|
30
|
+
const paddingRight = parseFloat(style.paddingRight)
|
|
31
|
+
const paddingBottom = parseFloat(style.paddingBottom)
|
|
32
|
+
const paddingLeft = parseFloat(style.paddingLeft)
|
|
20
33
|
|
|
21
34
|
ret.x = ret.x + paddingLeft
|
|
22
35
|
ret.y = ret.y + paddingTop
|
|
@@ -33,6 +46,21 @@ export function boundingBox(element, { containsPadding = true } = {}) {
|
|
|
33
46
|
return ret
|
|
34
47
|
}
|
|
35
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Gets the bounding box of an element relative to its ancestor
|
|
51
|
+
* @param {Object} params - Parameters for the calculation
|
|
52
|
+
* @param {HTMLElement} params.element - The element to get the bounding box for
|
|
53
|
+
* @param {HTMLElement} [params.ancestorElement] - The ancestor element to calculate position relative to. Defaults to parent element
|
|
54
|
+
* @returns {Object} The relative bounding box with center coordinates
|
|
55
|
+
* @property {number} top - Distance from ancestor's top
|
|
56
|
+
* @property {number} right - Distance from ancestor's right
|
|
57
|
+
* @property {number} bottom - Distance from ancestor's bottom
|
|
58
|
+
* @property {number} left - Distance from ancestor's left
|
|
59
|
+
* @property {number} width - The width
|
|
60
|
+
* @property {number} height - The height
|
|
61
|
+
* @property {number} xCenter - The horizontal center coordinate relative to ancestor
|
|
62
|
+
* @property {number} yCenter - The vertical center coordinate relative to ancestor
|
|
63
|
+
*/
|
|
36
64
|
export function boundingBoxRelativeToAncestor({ element, ancestorElement }) {
|
|
37
65
|
ancestorElement = ancestorElement || element.parentElement
|
|
38
66
|
|
package/dom/css-vars.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { getElement } from './get-element.js'
|
|
2
2
|
|
|
3
|
+
export function getCSSValue(element, prop) {
|
|
4
|
+
return getCSSVar(element, prop)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function setCSSValue(element, prop, value) {
|
|
8
|
+
setCSSVar(element, prop, value)
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
export function getCSSVar(element, prop) {
|
|
4
12
|
element = getElement(element)
|
|
5
13
|
return getComputedStyle(element).getPropertyValue(prop)
|
package/dom/events.js
CHANGED
|
@@ -10,12 +10,13 @@ export function removeListeners(listeners) {
|
|
|
10
10
|
})
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// Dispatch a custom event from a Node.
|
|
13
14
|
export function dispatchEvent(node, eventName, detail, options = {}) {
|
|
14
15
|
node.dispatchEvent(
|
|
15
16
|
new CustomEvent(eventName, {
|
|
16
17
|
...options,
|
|
17
18
|
detail,
|
|
18
|
-
})
|
|
19
|
+
})
|
|
19
20
|
)
|
|
20
21
|
}
|
|
21
22
|
|
package/dom/focusable.js
CHANGED
|
@@ -1,12 +1,72 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} Focusables
|
|
3
|
+
* @property {number} length - Number of keyboard focusable elements
|
|
4
|
+
* @property {Array} all - All focusable elements that are visible and enabled
|
|
5
|
+
* @property {Array} keyboard - Elements that can be focused via keyboard (tabIndex > -1)
|
|
6
|
+
* @property {Array} keyboardOnly - Alias for keyboard focusable elements
|
|
7
|
+
* @property {HTMLElement|null} first - First keyboard focusable element
|
|
8
|
+
* @property {HTMLElement|null} last - Last keyboard focusable element
|
|
9
|
+
* @property {function(number): HTMLElement|null} next - Get next focusable element after the given index
|
|
10
|
+
* @property {function(number): HTMLElement|null} prev - Get previous focusable element before the given index
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Gets all focusable elements within a container and provides methods to navigate between them.
|
|
15
|
+
* This is useful for implementing keyboard navigation, focus trapping, and accessibility features.
|
|
16
|
+
*
|
|
17
|
+
* @param {HTMLElement} [container=document.body] - The container element to search within. Only elements inside this container will be included.
|
|
18
|
+
* @returns {Focusables} Object containing focusable elements and navigation methods
|
|
19
|
+
* @example
|
|
20
|
+
* // Get focusable elements in a modal
|
|
21
|
+
* const modal = document.querySelector('.modal')
|
|
22
|
+
* const focusable = getFocusableElements(modal)
|
|
23
|
+
*
|
|
24
|
+
* // Get all focusable elements in document
|
|
25
|
+
* const allFocusable = getFocusableElements()
|
|
26
|
+
*
|
|
27
|
+
* // Focus the first element when modal opens
|
|
28
|
+
* focusable.first?.focus()
|
|
29
|
+
*
|
|
30
|
+
* // Handle tab key to trap focus
|
|
31
|
+
* modal.addEventListener('keydown', e => {
|
|
32
|
+
* if (e.key === 'Tab') {
|
|
33
|
+
* e.preventDefault()
|
|
34
|
+
* const currentIndex = focusable.keyboard.indexOf(document.activeElement)
|
|
35
|
+
* const nextElement = e.shiftKey
|
|
36
|
+
* ? focusable.prev(currentIndex)
|
|
37
|
+
* : focusable.next(currentIndex)
|
|
38
|
+
* nextElement?.focus()
|
|
39
|
+
* }
|
|
40
|
+
* })
|
|
41
|
+
*/
|
|
42
|
+
export function getFocusableElements(container = document.body) {
|
|
2
43
|
return {
|
|
44
|
+
/**
|
|
45
|
+
* Gets the number of keyboard focusable elements.
|
|
46
|
+
* This is useful for checking if there are any focusable elements
|
|
47
|
+
* before attempting to focus them.
|
|
48
|
+
*
|
|
49
|
+
* @returns {number} The number of keyboard focusable elements
|
|
50
|
+
*/
|
|
3
51
|
get length() {
|
|
4
52
|
return this.keyboard.length
|
|
5
53
|
},
|
|
6
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Gets all focusable elements within the container that are visible and enabled.
|
|
57
|
+
* This includes elements that can be focused via mouse or programmatically,
|
|
58
|
+
* but may not be keyboard focusable.
|
|
59
|
+
*
|
|
60
|
+
* Elements are considered focusable if they:
|
|
61
|
+
* - Are not disabled
|
|
62
|
+
* - Are not hidden
|
|
63
|
+
* - Are not display:none
|
|
64
|
+
*
|
|
65
|
+
* @returns {HTMLElement[]} Array of focusable elements that are visible and enabled
|
|
66
|
+
*/
|
|
7
67
|
get all() {
|
|
8
|
-
|
|
9
|
-
|
|
68
|
+
const elements = Array.from(
|
|
69
|
+
container.querySelectorAll(
|
|
10
70
|
`a,
|
|
11
71
|
button,
|
|
12
72
|
input,
|
|
@@ -22,35 +82,84 @@ export function Focusable(element = document) {
|
|
|
22
82
|
video[controls],
|
|
23
83
|
[contenteditable],
|
|
24
84
|
[tabindex]
|
|
25
|
-
|
|
85
|
+
`,
|
|
26
86
|
),
|
|
27
|
-
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
// @ts-ignore - we check instanceof HTMLElement at runtime
|
|
90
|
+
return elements.filter(el => {
|
|
28
91
|
if (el.hasAttribute('disabled')) return false
|
|
29
92
|
if (el.hasAttribute('hidden')) return false
|
|
30
93
|
if (window.getComputedStyle(el).display === 'none') return false
|
|
31
|
-
|
|
32
94
|
return true
|
|
33
95
|
})
|
|
34
96
|
},
|
|
35
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Gets elements that can be focused via keyboard (tabIndex > -1).
|
|
100
|
+
* These are the elements that will be focused when using the Tab key.
|
|
101
|
+
*
|
|
102
|
+
* @returns {HTMLElement[]} Array of keyboard focusable elements
|
|
103
|
+
*/
|
|
36
104
|
get keyboard() {
|
|
37
|
-
return this.all.filter(el => el.tabIndex > -1)
|
|
105
|
+
return this.all.filter(el => el instanceof HTMLElement && el.tabIndex > -1)
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Alias for keyboard focusable elements.
|
|
110
|
+
* This is provided for semantic clarity in certain contexts.
|
|
111
|
+
*
|
|
112
|
+
* @returns {HTMLElement[]} Array of keyboard focusable elements
|
|
113
|
+
*/
|
|
114
|
+
get keyboardOnly() {
|
|
115
|
+
return this.keyboard
|
|
38
116
|
},
|
|
39
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Gets the first keyboard focusable element.
|
|
120
|
+
* This is useful for setting initial focus when opening a modal or dialog.
|
|
121
|
+
*
|
|
122
|
+
* @returns {HTMLElement|null} The first focusable element or null if none exist
|
|
123
|
+
*/
|
|
40
124
|
get first() {
|
|
41
|
-
return this.keyboard[0]
|
|
125
|
+
return this.keyboard[0] || null
|
|
42
126
|
},
|
|
43
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Gets the last keyboard focusable element.
|
|
130
|
+
* This is useful for focus trapping when using Shift+Tab.
|
|
131
|
+
*
|
|
132
|
+
* @returns {HTMLElement|null} The last focusable element or null if none exist
|
|
133
|
+
*/
|
|
44
134
|
get last() {
|
|
45
|
-
return this.keyboard[this.length - 1]
|
|
135
|
+
return this.keyboard[this.length - 1] || null
|
|
46
136
|
},
|
|
47
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Gets the next focusable element after the given index.
|
|
140
|
+
* This is useful for implementing custom tab order or focus management.
|
|
141
|
+
*
|
|
142
|
+
* @param {number} index - The current element's index in the keyboard focusable array
|
|
143
|
+
* @returns {HTMLElement|null} The next focusable element or null if none exists
|
|
144
|
+
*/
|
|
48
145
|
next(index) {
|
|
49
146
|
return this.keyboard[index + 1] || null
|
|
50
147
|
},
|
|
51
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Gets the previous focusable element before the given index.
|
|
151
|
+
* This is useful for implementing custom tab order or focus management.
|
|
152
|
+
*
|
|
153
|
+
* @param {number} index - The current element's index in the keyboard focusable array
|
|
154
|
+
* @returns {HTMLElement|null} The previous focusable element or null if none exists
|
|
155
|
+
*/
|
|
52
156
|
prev(index) {
|
|
53
|
-
return this.keyboard[index
|
|
157
|
+
return this.keyboard[index - 1] || null
|
|
54
158
|
},
|
|
55
159
|
}
|
|
56
160
|
}
|
|
161
|
+
|
|
162
|
+
// Alias of getFocusableElements
|
|
163
|
+
export function Focusable(container = document.body) {
|
|
164
|
+
return getFocusableElements(container)
|
|
165
|
+
}
|
package/dom/font-size.js
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
1
1
|
import { splitUnit } from '../lib/index.js'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export function getUnit(value) {
|
|
4
|
+
const [, unit] = splitUnit(value)
|
|
5
|
+
return unit
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
export function em(element = document.body, multiple = 1) {
|
|
5
|
-
const
|
|
6
|
-
return Math.round(
|
|
9
|
+
const value = parseFloat(getComputedStyle(element)['font-size'])
|
|
10
|
+
return Math.round(value * multiple)
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
export function rem(multiple = 1) {
|
|
10
|
-
const
|
|
11
|
-
return Math.round(
|
|
14
|
+
const value = parseFloat(getComputedStyle(document.body)['font-size'])
|
|
15
|
+
return Math.round(value * multiple)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function lh(element = document.body, multiple = 1) {
|
|
19
|
+
const lineHeight = getComputedStyle(element)['line-height']
|
|
20
|
+
const value = parseFloat(lineHeight)
|
|
21
|
+
return Math.round(value * multiple)
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
export function toPx(value, element = null) {
|
|
15
|
-
const [
|
|
25
|
+
const [numeric, unit] = splitUnit(value)
|
|
16
26
|
let finalValue = 0
|
|
17
27
|
|
|
18
|
-
if (unit === 'rem') finalValue = rem(
|
|
19
|
-
if (unit === 'em') finalValue = em(element,
|
|
20
|
-
if (unit === '
|
|
21
|
-
if (
|
|
28
|
+
if (unit === 'rem') finalValue = rem(numeric)
|
|
29
|
+
if (unit === 'em') finalValue = em(element, numeric)
|
|
30
|
+
if (unit === 'lh') finalValue = lh(element, numeric)
|
|
31
|
+
if (unit === 'px') finalValue = numeric
|
|
32
|
+
if (!unit) finalValue = numeric // Default to px offsets
|
|
22
33
|
|
|
23
34
|
return finalValue
|
|
24
35
|
}
|
package/dom/get-element.js
CHANGED
|
@@ -2,12 +2,54 @@
|
|
|
2
2
|
// If the selector is already an HTMLElement, it will return the selector itself.
|
|
3
3
|
// Otherwise, it will return the first element that matches the selector.
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* List of Astro-specific node names that need special handling
|
|
7
|
+
* @type {string[]}
|
|
8
|
+
*/
|
|
9
|
+
const astroNodes = ['ASTRO-SLOT', 'ASTRO-ISLAND']
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Gets the type of a DOM node
|
|
13
|
+
* @param {Node|NodeList|Array} node - The node to check
|
|
14
|
+
* @returns {'element'|'nodelist'|'array'|undefined} The type of the node
|
|
15
|
+
*/
|
|
16
|
+
export function getNodeType(node) {
|
|
17
|
+
if (node instanceof Element) return 'element'
|
|
18
|
+
if (node instanceof NodeList) return 'nodelist'
|
|
19
|
+
if (Array.isArray(node)) return 'array'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Gets an element from a selector or returns the element if it's already an Element
|
|
24
|
+
* @param {string|Element} selector - CSS selector or Element
|
|
25
|
+
* @returns {Element|undefined} The matching element or undefined if not found
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // Get element by selector
|
|
29
|
+
* const button = getElement('.button')
|
|
30
|
+
*
|
|
31
|
+
* // Pass through Element
|
|
32
|
+
* const element = document.querySelector('.button')
|
|
33
|
+
* const sameElement = getElement(element) // Returns element directly
|
|
34
|
+
*/
|
|
5
35
|
export function getElement(selector) {
|
|
6
36
|
if (!selector) return
|
|
7
|
-
if (selector instanceof
|
|
37
|
+
if (selector instanceof Element) return selector
|
|
8
38
|
return document.querySelector(selector)
|
|
9
39
|
}
|
|
10
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Gets all child elements of a node, handling Astro slots
|
|
43
|
+
* @param {Element} node - The parent node
|
|
44
|
+
* @returns {Element[]} Array of child elements
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Get children of a div
|
|
48
|
+
* const children = getChildrenElements(div)
|
|
49
|
+
*
|
|
50
|
+
* // Works with Astro slots
|
|
51
|
+
* const slotChildren = getChildrenElements(astroSlot)
|
|
52
|
+
*/
|
|
11
53
|
export function getChildrenElements(node) {
|
|
12
54
|
let children = node.children
|
|
13
55
|
|
|
@@ -16,16 +58,64 @@ export function getChildrenElements(node) {
|
|
|
16
58
|
return Array.from(children)
|
|
17
59
|
}
|
|
18
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Gets the parent element, skipping Astro-specific nodes
|
|
63
|
+
* @param {Element} element - The element to get the parent of
|
|
64
|
+
* @returns {Element|null} The parent element, or null if no parent exists
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* // Get parent, skipping Astro slots
|
|
68
|
+
* const parent = getParentElement(element)
|
|
69
|
+
*/
|
|
70
|
+
export function getParentElement(element) {
|
|
71
|
+
const parent = element.parentElement
|
|
72
|
+
if (astroNodes.includes(parent.nodeName)) return getParentElement(parent)
|
|
73
|
+
return parent
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Gets all sibling elements of an element (excluding the element itself)
|
|
78
|
+
* @param {Element} element - The element to get siblings of
|
|
79
|
+
* @returns {Element[]} Array of sibling elements
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* // Get all siblings of an element
|
|
83
|
+
* const siblings = getSiblingElements(element)
|
|
84
|
+
*/
|
|
19
85
|
export function getSiblingElements(element) {
|
|
20
86
|
return Array.from(element.parentElement.children).filter(
|
|
21
|
-
child => child !== element
|
|
87
|
+
child => child !== element
|
|
22
88
|
)
|
|
23
89
|
}
|
|
24
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Gets the index of an element among its siblings
|
|
93
|
+
* @param {Element} element - The element to get the index of
|
|
94
|
+
* @returns {number} The index of the element (0-based)
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* // Get position of element among siblings
|
|
98
|
+
* const index = getSelfIndex(element)
|
|
99
|
+
*/
|
|
25
100
|
export function getSelfIndex(element) {
|
|
26
101
|
return Array.from(element.parentElement.children).indexOf(element)
|
|
27
102
|
}
|
|
28
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Checks if an element is an ancestor of another element
|
|
106
|
+
* @param {Element} ancestor - The potential ancestor element
|
|
107
|
+
* @param {Element} element - The element to check
|
|
108
|
+
* @param {Object} [options] - Options for the search
|
|
109
|
+
* @param {number} [options.searchLevels=0] - Number of levels to search up, 0 for unlimited
|
|
110
|
+
* @returns {boolean} True if ancestor is an ancestor of element
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* // Check if div is an ancestor of button
|
|
114
|
+
* const isParent = isAncestor(div, button)
|
|
115
|
+
*
|
|
116
|
+
* // Check if div is an ancestor within 2 levels
|
|
117
|
+
* const isCloseAncestor = isAncestor(div, button, { searchLevels: 2 })
|
|
118
|
+
*/
|
|
29
119
|
export function isAncestor(ancestor, element, { searchLevels = 0 } = {}) {
|
|
30
120
|
if (ancestor === element) return true
|
|
31
121
|
|
|
@@ -47,6 +137,22 @@ export function isAncestor(ancestor, element, { searchLevels = 0 } = {}) {
|
|
|
47
137
|
return false
|
|
48
138
|
}
|
|
49
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Gets ancestor elements with their siblings, optionally limiting the search depth.
|
|
142
|
+
* Useful for finding the nearest ancestor that has siblings.
|
|
143
|
+
*
|
|
144
|
+
* @param {Element} element - The element to start searching from
|
|
145
|
+
* @param {Object} [options] - Options for the search
|
|
146
|
+
* @param {number|boolean} [options.limit=false] - Number of levels to search up, or false for unlimited
|
|
147
|
+
* @returns {Element[]|null} Array of sibling elements, or null if none found
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* // Get siblings of nearest ancestor that has siblings
|
|
151
|
+
* const ancestorSiblings = getAncestorWithSiblings(element)
|
|
152
|
+
*
|
|
153
|
+
* // Search up to 2 levels
|
|
154
|
+
* const limitedSiblings = getAncestorWithSiblings(element, { limit: 2 })
|
|
155
|
+
*/
|
|
50
156
|
export function getAncestorWithSiblings(element, { limit = false } = {}) {
|
|
51
157
|
const ancestor = element.parentElement
|
|
52
158
|
const siblings = getSiblingElements(ancestor)
|
|
@@ -61,9 +167,10 @@ export function getAncestorWithSiblings(element, { limit = false } = {}) {
|
|
|
61
167
|
if (limit === false) return getAncestorWithSiblings(ancestor)
|
|
62
168
|
|
|
63
169
|
// Limited search
|
|
64
|
-
if (limit > 0)
|
|
65
|
-
return getAncestorWithSiblings(ancestor, {
|
|
170
|
+
if (typeof limit === 'number' && limit > 0) {
|
|
171
|
+
return getAncestorWithSiblings(ancestor, { limit: limit - 1 })
|
|
172
|
+
}
|
|
66
173
|
|
|
67
|
-
// Limit reached, end search
|
|
174
|
+
// Limit reached, end search
|
|
68
175
|
if (limit === 0) return null
|
|
69
176
|
}
|
package/dom/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './accessibility.js'
|
|
2
|
+
export * from './actions/index.js'
|
|
2
3
|
export * from './bounding-box.js'
|
|
3
4
|
export * from './clipboard.js'
|
|
4
5
|
export * from './cookie.js'
|
|
@@ -11,9 +12,11 @@ export * from './hash.js'
|
|
|
11
12
|
export * from './keyboard.js'
|
|
12
13
|
export * from './local-store.js'
|
|
13
14
|
export * from './media.js'
|
|
15
|
+
export * from './observers/index.js'
|
|
14
16
|
export * from './pkce.js'
|
|
15
17
|
export * from './query-params.js'
|
|
16
18
|
export * from './random-string.js'
|
|
19
|
+
export * from './sanitize.js'
|
|
17
20
|
export * from './session-store.js'
|
|
18
21
|
export * from './trap-focus.js'
|
|
19
22
|
export * from './ui/index.js'
|