@splendidlabz/utils 1.5.0 → 1.6.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.
Files changed (169) hide show
  1. package/CHANGELOG.md +77 -1
  2. package/dist/dom/accessibility.d.ts +28 -0
  3. package/dist/dom/actions/index.d.ts +3 -0
  4. package/dist/dom/actions/masonry.d.ts +3 -0
  5. package/dist/dom/actions/prefer-horizontal-scroll.d.ts +3 -0
  6. package/dist/dom/actions/sticky.d.ts +3 -0
  7. package/dist/dom/bounding-box.d.ts +39 -0
  8. package/dist/dom/clipboard.d.ts +5 -0
  9. package/dist/dom/cookie.d.ts +5 -0
  10. package/dist/dom/css-vars.d.ts +4 -0
  11. package/dist/dom/events.d.ts +18 -0
  12. package/dist/dom/focusable.d.ts +76 -0
  13. package/dist/dom/font-size.d.ts +5 -0
  14. package/dist/dom/get-element.d.ts +100 -0
  15. package/dist/dom/hash.d.ts +1 -0
  16. package/dist/dom/index.d.ts +22 -0
  17. package/dist/dom/keyboard.d.ts +30 -0
  18. package/dist/dom/local-store.d.ts +29 -0
  19. package/dist/dom/media.d.ts +3 -0
  20. package/dist/dom/observers/index.d.ts +3 -0
  21. package/dist/dom/observers/intersection-observer.d.ts +8 -0
  22. package/dist/dom/observers/mutation-observer.d.ts +7 -0
  23. package/dist/dom/observers/observer.d.ts +1 -0
  24. package/dist/dom/observers/resize-observer.d.ts +38 -0
  25. package/dist/dom/pkce.d.ts +6 -0
  26. package/dist/dom/query-params.d.ts +4 -0
  27. package/dist/dom/random-string.d.ts +7 -0
  28. package/dist/dom/sanitize.d.ts +8 -0
  29. package/dist/dom/session-store.d.ts +5 -0
  30. package/dist/dom/trap-focus.d.ts +28 -0
  31. package/dist/dom/ui/aria-current.d.ts +5 -0
  32. package/dist/dom/ui/inconsistent-button-fix.d.ts +12 -0
  33. package/dist/dom/ui/index.d.ts +4 -0
  34. package/dist/dom/ui/scroll-container.d.ts +1 -0
  35. package/dist/dom/ui/traverse-and-scramble.d.ts +19 -0
  36. package/dist/lib/arrays/index.d.ts +5 -0
  37. package/dist/lib/arrays/item-position.d.ts +14 -0
  38. package/dist/lib/arrays/join.d.ts +13 -0
  39. package/dist/lib/arrays/shuffle.d.ts +1 -0
  40. package/dist/lib/arrays/sort.d.ts +30 -0
  41. package/dist/lib/arrays/unique.d.ts +1 -0
  42. package/dist/lib/auth/index.d.ts +1 -0
  43. package/dist/lib/auth/route-manager.d.ts +25 -0
  44. package/dist/lib/checks.d.ts +30 -0
  45. package/dist/lib/date/days.d.ts +5 -0
  46. package/dist/lib/date/index.d.ts +3 -0
  47. package/dist/lib/date/months.d.ts +9 -0
  48. package/dist/lib/date/time.d.ts +8 -0
  49. package/dist/lib/form/form-data.d.ts +7 -0
  50. package/dist/lib/form/index.d.ts +2 -0
  51. package/dist/lib/form/sanitize.d.ts +58 -0
  52. package/dist/lib/functions/debounce.d.ts +1 -0
  53. package/dist/lib/functions/env.d.ts +1 -0
  54. package/dist/lib/functions/functional.d.ts +4 -0
  55. package/dist/lib/functions/index.d.ts +5 -0
  56. package/dist/lib/functions/throttle.d.ts +1 -0
  57. package/dist/lib/functions/timeout.d.ts +3 -0
  58. package/dist/lib/hash.d.ts +1 -0
  59. package/dist/lib/index.d.ts +14 -0
  60. package/dist/lib/numbers/index.d.ts +2 -0
  61. package/dist/lib/numbers/math.d.ts +2 -0
  62. package/dist/lib/objects/camelcase-keys.d.ts +1 -0
  63. package/dist/lib/objects/empty.d.ts +2 -0
  64. package/dist/lib/objects/equal.d.ts +2 -0
  65. package/dist/lib/objects/extend.d.ts +4 -0
  66. package/dist/lib/objects/flatten.d.ts +4 -0
  67. package/dist/lib/objects/index.d.ts +13 -0
  68. package/dist/lib/objects/json.d.ts +1 -0
  69. package/dist/lib/objects/loop.d.ts +2 -0
  70. package/dist/lib/objects/mix/mix.d.ts +1 -0
  71. package/dist/lib/objects/nested-property.d.ts +22 -0
  72. package/dist/lib/objects/normalize-object.d.ts +1 -0
  73. package/dist/lib/objects/omit-empty.d.ts +1 -0
  74. package/dist/lib/objects/size.d.ts +1 -0
  75. package/dist/lib/objects/split.d.ts +12 -0
  76. package/dist/lib/promises/index.d.ts +1 -0
  77. package/dist/lib/promises/reject.d.ts +3 -0
  78. package/dist/lib/sse.d.ts +16 -0
  79. package/dist/lib/strings/convert-case/convert-case.d.ts +8 -0
  80. package/dist/lib/strings/index.d.ts +5 -0
  81. package/dist/lib/strings/markdown.d.ts +23 -0
  82. package/dist/lib/strings/name.d.ts +5 -0
  83. package/dist/lib/strings/pluralize.d.ts +1 -0
  84. package/dist/lib/strings/query-string.d.ts +8 -0
  85. package/dist/lib/style/index.d.ts +1 -0
  86. package/dist/lib/symbols/index.d.ts +1 -0
  87. package/dist/lib/symbols/symbols.d.ts +2 -0
  88. package/dist/node/common.d.ts +1 -0
  89. package/dist/node/dirname.d.ts +7 -0
  90. package/dist/node/file-cache.d.ts +54 -0
  91. package/dist/node/file.d.ts +2 -0
  92. package/dist/node/hash.d.ts +1 -0
  93. package/dist/node/index.d.ts +7 -0
  94. package/dist/node/pkce.d.ts +6 -0
  95. package/dist/node/random-string.d.ts +1 -0
  96. package/dist/node/sanitize.d.ts +8 -0
  97. package/dom/accessibility.js +46 -13
  98. package/{actions → dom/actions}/index.js +0 -2
  99. package/dom/actions/masonry.js +42 -0
  100. package/{actions → dom/actions}/prefer-horizontal-scroll.js +8 -8
  101. package/{actions → dom/actions}/sticky.js +3 -6
  102. package/dom/bounding-box.js +35 -7
  103. package/dom/cookie.js +8 -7
  104. package/dom/css-vars.js +8 -0
  105. package/dom/events.js +2 -1
  106. package/dom/focusable.js +119 -10
  107. package/dom/font-size.js +21 -10
  108. package/dom/get-element.js +112 -5
  109. package/dom/index.js +3 -0
  110. package/dom/keyboard.js +13 -8
  111. package/dom/local-store.js +37 -0
  112. package/dom/media.js +1 -0
  113. package/dom/observers/index.js +3 -0
  114. package/dom/observers/intersection-observer.js +43 -0
  115. package/dom/observers/mutation-observer.js +51 -0
  116. package/dom/observers/observer.js +18 -0
  117. package/dom/observers/resize-observer.js +64 -0
  118. package/dom/random-string.js +1 -1
  119. package/dom/random-string.test.js +13 -0
  120. package/dom/sanitize.js +16 -0
  121. package/dom/trap-focus.js +18 -3
  122. package/dom/ui/inconsistent-button-fix.js +14 -2
  123. package/dom/ui/traverse-and-scramble.js +29 -8
  124. package/eslint.config.js +2 -0
  125. package/lib/arrays/index.js +5 -62
  126. package/lib/arrays/item-position.js +23 -0
  127. package/lib/arrays/join.js +20 -0
  128. package/lib/arrays/shuffle.js +21 -0
  129. package/lib/arrays/sort.js +88 -0
  130. package/lib/arrays/sort.spec.js +123 -0
  131. package/lib/arrays/unique.js +3 -0
  132. package/lib/checks.js +42 -0
  133. package/lib/date/index.js +2 -1
  134. package/lib/date/time.js +50 -0
  135. package/lib/form/sanitize.js +68 -35
  136. package/lib/form/sanitize.spec.js +126 -0
  137. package/lib/hash.js +18 -0
  138. package/lib/hash.spec.js +163 -0
  139. package/lib/index.js +2 -0
  140. package/lib/numbers/index.js +2 -0
  141. package/lib/numbers/math.js +9 -0
  142. package/lib/objects/equal.js +9 -0
  143. package/lib/objects/index.js +1 -0
  144. package/lib/objects/mix/mix.js +2 -0
  145. package/lib/objects/mix/mix.test.js +24 -0
  146. package/lib/objects/nested-property.js +5 -0
  147. package/lib/objects/split.js +1 -1
  148. package/lib/sse.js +61 -0
  149. package/lib/strings/index.js +1 -0
  150. package/lib/strings/markdown.js +29 -10
  151. package/lib/strings/name.js +24 -0
  152. package/lib/strings/name.test.js +67 -0
  153. package/node/dirname.js +1 -1
  154. package/node/file-cache.js +46 -7
  155. package/node/index.js +1 -0
  156. package/node/random-string.js +6 -6
  157. package/node/random-string.test.js +15 -0
  158. package/node/sanitize.js +13 -0
  159. package/node/sanitize.spec.js +142 -0
  160. package/package.json +38 -13
  161. package/tsconfig.json +23 -0
  162. package/.eslintrc.cjs +0 -3
  163. package/.turbo/turbo-lint.log +0 -10
  164. package/.turbo/turbo-test.log +0 -10
  165. package/actions/intersection-observer.js +0 -37
  166. package/actions/masonry.js +0 -75
  167. package/actions/mutation-observer.js +0 -42
  168. package/actions/resize-observer.js +0 -33
  169. package/lib/index.test.js +0 -9
@@ -1,23 +1,56 @@
1
- export function stopBodyScroll() {
2
- const top = document.documentElement.scrollTop
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
- element.style.top = -1 * top + 'px'
5
- element.style.overflow = 'hidden'
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
- 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
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
- export function inertOthers() {}
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
- export function hasLabel(label, labelledBy) {
18
- if (label || labelledBy) return true
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
@@ -1,5 +1,3 @@
1
1
  export * from './masonry.js'
2
- export * from './mutation-observer.js'
3
2
  export * from './prefer-horizontal-scroll.js'
4
- export * from './resize-observer.js'
5
3
  export * from './sticky.js'
@@ -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
- addListeners,
3
- getCSSVar,
4
- removeListeners,
5
- setCSSVar,
6
- } from '../dom/index.js'
2
+ addListeners,
3
+ getCSSVar,
4
+ removeListeners,
5
+ setCSSVar,
6
+ } from '../index.js'
7
7
 
8
- import { omitEmpty } from '../lib/index.js'
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-scroll')) return
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, '--scrollSnapDelay'),
26
+ scrollSnapDelay: getCSSVar(node, '--scroll-snap-delay'),
27
27
  }),
28
28
  ...omitEmpty(props),
29
29
  }
@@ -1,12 +1,9 @@
1
- import {
2
- boundingBoxRelativeToAncestor,
3
- findScrollContainer,
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, {
@@ -1,6 +1,19 @@
1
1
  /**
2
- * Advanced boundincClientRect that contains information vertical and horizontal centers.
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
- let { paddingTop, paddingRight, paddingBottom, paddingLeft } = style
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/cookie.js CHANGED
@@ -8,15 +8,16 @@ export const cookies = {
8
8
  },
9
9
  }
10
10
 
11
- /**
12
- * Gets a cookie by name
13
- * Thanks to https://stackoverflow.com/a/21125098
14
- * @param {string} name Cookie name
15
- * @returns string Cookie value
16
- */
17
11
  export function getCookie(name) {
18
12
  const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
19
- if (match) return match[2]
13
+ if (!match) return null
14
+ const decoded = decodeURIComponent(match[2])
15
+
16
+ try {
17
+ return JSON.parse(decoded)
18
+ } catch (e) {
19
+ return decoded
20
+ }
20
21
  }
21
22
 
22
23
  // By Copilot. Needs testing before confirmation.
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
- export function Focusable(element = document) {
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
- return [
9
- ...element.querySelectorAll(
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
- ].filter(el => {
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 + 1] || null
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
- /* globals getComputedStyle */
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 unit = parseFloat(getComputedStyle(element)['font-size'])
6
- return Math.round(unit * multiple)
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 unit = parseFloat(getComputedStyle(document.body)['font-size'])
11
- return Math.round(unit * multiple)
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 [amt, unit] = splitUnit(value)
25
+ const [numeric, unit] = splitUnit(value)
16
26
  let finalValue = 0
17
27
 
18
- if (unit === 'rem') finalValue = rem(amt)
19
- if (unit === 'em') finalValue = em(element, amt)
20
- if (unit === 'px') finalValue = amt
21
- if (!unit) finalValue = amt // Default to px offsets
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
  }
@@ -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 HTMLElement) return selector
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, { searchLevels: limit - 1 })
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'