@splendidlabz/utils 1.5.0-beta.6 → 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.
Files changed (117) hide show
  1. package/CHANGELOG.md +28 -0
  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 +11 -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 +29 -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/index.d.ts +13 -0
  59. package/dist/lib/numbers/index.d.ts +2 -0
  60. package/dist/lib/numbers/math.d.ts +2 -0
  61. package/dist/lib/objects/camelcase-keys.d.ts +1 -0
  62. package/dist/lib/objects/empty.d.ts +2 -0
  63. package/dist/lib/objects/equal.d.ts +2 -0
  64. package/dist/lib/objects/extend.d.ts +4 -0
  65. package/dist/lib/objects/flatten.d.ts +4 -0
  66. package/dist/lib/objects/index.d.ts +13 -0
  67. package/dist/lib/objects/json.d.ts +1 -0
  68. package/dist/lib/objects/loop.d.ts +2 -0
  69. package/dist/lib/objects/mix/mix.d.ts +1 -0
  70. package/dist/lib/objects/nested-property.d.ts +22 -0
  71. package/dist/lib/objects/normalize-object.d.ts +1 -0
  72. package/dist/lib/objects/omit-empty.d.ts +1 -0
  73. package/dist/lib/objects/size.d.ts +1 -0
  74. package/dist/lib/objects/split.d.ts +10 -0
  75. package/dist/lib/promises/index.d.ts +1 -0
  76. package/dist/lib/promises/reject.d.ts +3 -0
  77. package/dist/lib/sse.d.ts +16 -0
  78. package/dist/lib/strings/convert-case/convert-case.d.ts +8 -0
  79. package/dist/lib/strings/index.d.ts +4 -0
  80. package/dist/lib/strings/markdown.d.ts +23 -0
  81. package/dist/lib/strings/pluralize.d.ts +1 -0
  82. package/dist/lib/strings/query-string.d.ts +8 -0
  83. package/dist/lib/style/index.d.ts +1 -0
  84. package/dist/lib/symbols/index.d.ts +1 -0
  85. package/dist/lib/symbols/symbols.d.ts +2 -0
  86. package/dist/node/common.d.ts +1 -0
  87. package/dist/node/dirname.d.ts +7 -0
  88. package/dist/node/file-cache.d.ts +54 -0
  89. package/dist/node/file.d.ts +2 -0
  90. package/dist/node/hash.d.ts +1 -0
  91. package/dist/node/index.d.ts +7 -0
  92. package/dist/node/pkce.d.ts +6 -0
  93. package/dist/node/random-string.d.ts +2 -0
  94. package/dist/node/sanitize.d.ts +8 -0
  95. package/dom/accessibility.js +22 -0
  96. package/dom/actions/prefer-horizontal-scroll.js +4 -4
  97. package/dom/bounding-box.js +35 -7
  98. package/dom/focusable.js +119 -10
  99. package/dom/get-element.js +97 -5
  100. package/dom/keyboard.js +13 -8
  101. package/dom/observers/intersection-observer.js +1 -1
  102. package/dom/observers/mutation-observer.js +1 -1
  103. package/dom/observers/resize-observer.js +2 -2
  104. package/dom/sanitize.js +1 -1
  105. package/dom/trap-focus.js +18 -3
  106. package/dom/ui/inconsistent-button-fix.js +14 -2
  107. package/dom/ui/traverse-and-scramble.js +29 -8
  108. package/lib/arrays/join.js +7 -4
  109. package/lib/arrays/sort.js +37 -7
  110. package/lib/checks.js +26 -0
  111. package/lib/form/sanitize.js +34 -11
  112. package/lib/sse.js +4 -7
  113. package/lib/strings/markdown.js +12 -4
  114. package/node/file-cache.js +46 -7
  115. package/node/sanitize.js +1 -1
  116. package/package.json +32 -10
  117. package/tsconfig.json +23 -0
@@ -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/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
+ }
@@ -2,21 +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
+ */
5
9
  const astroNodes = ['ASTRO-SLOT', 'ASTRO-ISLAND']
6
10
 
7
- // Returns the type of the node.
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
+ */
8
16
  export function getNodeType(node) {
9
17
  if (node instanceof Element) return 'element'
10
18
  if (node instanceof NodeList) return 'nodelist'
11
19
  if (Array.isArray(node)) return 'array'
12
20
  }
13
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
+ */
14
35
  export function getElement(selector) {
15
36
  if (!selector) return
16
- if (selector instanceof HTMLElement) return selector
37
+ if (selector instanceof Element) return selector
17
38
  return document.querySelector(selector)
18
39
  }
19
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
+ */
20
53
  export function getChildrenElements(node) {
21
54
  let children = node.children
22
55
 
@@ -25,22 +58,64 @@ export function getChildrenElements(node) {
25
58
  return Array.from(children)
26
59
  }
27
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
+ */
28
70
  export function getParentElement(element) {
29
71
  const parent = element.parentElement
30
72
  if (astroNodes.includes(parent.nodeName)) return getParentElement(parent)
31
73
  return parent
32
74
  }
33
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
+ */
34
85
  export function getSiblingElements(element) {
35
86
  return Array.from(element.parentElement.children).filter(
36
87
  child => child !== element
37
88
  )
38
89
  }
39
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
+ */
40
100
  export function getSelfIndex(element) {
41
101
  return Array.from(element.parentElement.children).indexOf(element)
42
102
  }
43
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
+ */
44
119
  export function isAncestor(ancestor, element, { searchLevels = 0 } = {}) {
45
120
  if (ancestor === element) return true
46
121
 
@@ -62,6 +137,22 @@ export function isAncestor(ancestor, element, { searchLevels = 0 } = {}) {
62
137
  return false
63
138
  }
64
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
+ */
65
156
  export function getAncestorWithSiblings(element, { limit = false } = {}) {
66
157
  const ancestor = element.parentElement
67
158
  const siblings = getSiblingElements(ancestor)
@@ -76,9 +167,10 @@ export function getAncestorWithSiblings(element, { limit = false } = {}) {
76
167
  if (limit === false) return getAncestorWithSiblings(ancestor)
77
168
 
78
169
  // Limited search
79
- if (limit > 0)
80
- return getAncestorWithSiblings(ancestor, { searchLevels: limit - 1 })
170
+ if (typeof limit === 'number' && limit > 0) {
171
+ return getAncestorWithSiblings(ancestor, { limit: limit - 1 })
172
+ }
81
173
 
82
- // Limit reached, end search.
174
+ // Limit reached, end search
83
175
  if (limit === 0) return null
84
176
  }
package/dom/keyboard.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Event is Tab (without shift)
3
- * @param {Object} event - The event object
4
- * @returns {Boolean}
3
+ * @param {KeyboardEvent} event - The keyboard event object
4
+ * @returns {boolean}
5
5
  */
6
6
  export function isTab(event) {
7
7
  return !event.shiftKey && event.key === 'Tab'
@@ -9,8 +9,8 @@ export function isTab(event) {
9
9
 
10
10
  /**
11
11
  * Event is shift + Tab
12
- * @param {Object} event - The event object
13
- * @returns {Boolean}
12
+ * @param {KeyboardEvent} event - The keyboard event object
13
+ * @returns {boolean}
14
14
  */
15
15
  export function isShiftTab(event) {
16
16
  return event.shiftKey && event.key === 'Tab'
@@ -18,8 +18,8 @@ export function isShiftTab(event) {
18
18
 
19
19
  /**
20
20
  * Matches both Tab and Shift+Tab
21
- * @param {Object} event - The event object
22
- * @returns
21
+ * @param {string} key - The keyboard key to check
22
+ * @returns {boolean} Whether the key is Tab
23
23
  */
24
24
  export function isTabKey(key) {
25
25
  return key === 'Tab'
@@ -27,13 +27,18 @@ export function isTabKey(key) {
27
27
 
28
28
  /**
29
29
  * Event is an arrow key
30
- * @param {Object} event - The event object
31
- * @returns {Boolean}
30
+ * @param {string} key - The keyboard key to check
31
+ * @returns {boolean} Whether the key is an arrow key (Up, Down, Left, Right)
32
32
  */
33
33
  export function isArrowKey(key) {
34
34
  return ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)
35
35
  }
36
36
 
37
+ /**
38
+ * Event is Escape key
39
+ * @param {string} key - The keyboard key to check
40
+ * @returns {boolean} Whether the key is Escape
41
+ */
37
42
  export function isEscapeKey(key) {
38
43
  return key === 'Escape'
39
44
  }
@@ -17,7 +17,7 @@ export function intersectionObserver(target, options = {}) {
17
17
 
18
18
  return {
19
19
  observer,
20
- observe(target, options) {
20
+ observe(target) {
21
21
  useObserverMethodOnTarget(target, observer, 'observe', options)
22
22
  },
23
23
 
@@ -28,7 +28,7 @@ export function mutationObserver(target, options) {
28
28
 
29
29
  return {
30
30
  observer,
31
- observe(target, options) {
31
+ observe(target) {
32
32
  useObserverMethodOnTarget(target, observer, 'observe', options)
33
33
  },
34
34
 
@@ -12,7 +12,7 @@ import { useObserverMethodOnTarget } from './observer.js'
12
12
  * @param {boolean} [options.observe=true] - Whether to start observing immediately. If false, the observer won't be created.
13
13
  * @param {Function} [options.callback] - Optional callback function that will be called when resize changes are detected.
14
14
  * If not provided, a 'resize-obs' event will be dispatched on the target.
15
- * @param {Object} [options...] - Additional options to pass to ResizeObserver.observe()
15
+ * @param {Object} [options.observerOptions] - Additional options to pass to ResizeObserver.observe()
16
16
  *
17
17
  * @returns {Object} An object with methods to control the observer:
18
18
  * - observe(target, options): Start observing a new target element
@@ -50,7 +50,7 @@ export function resizeObserver(target, options) {
50
50
  }
51
51
 
52
52
  return {
53
- observe(target, options) {
53
+ observe(target) {
54
54
  useObserverMethodOnTarget(target, observer, 'observe', options)
55
55
  },
56
56
 
package/dom/sanitize.js CHANGED
@@ -12,5 +12,5 @@ import { sanitize as sanitizeLib } from '../lib/form/sanitize.js'
12
12
  * @throws {Error} If input is a string but DOMPurify fails
13
13
  */
14
14
  export function sanitize(value, options = {}) {
15
- return sanitizeLib(value, { sanitizer: DOMPurify.sanitize })
15
+ return sanitizeLib(value, { sanitizer: DOMPurify.sanitize, ...options })
16
16
  }
package/dom/trap-focus.js CHANGED
@@ -1,6 +1,12 @@
1
- import { Focusable } from './focusable.js'
1
+ import { getFocusableElements } from './focusable.js'
2
2
  import { isShiftTab, isTab } from './keyboard.js'
3
3
 
4
+ /**
5
+ * Traps focus within a set of focusable elements, creating a circular tab order
6
+ * @param {Object} params
7
+ * @param {KeyboardEvent} params.event - The keyboard event to handle
8
+ * @param {import('./focusable.js').Focusables} params.focusables - Object containing focusable elements
9
+ */
4
10
  export function trapFocus({ event, focusables }) {
5
11
  if (event.key !== 'Tab') return
6
12
  if (isTab(event) && document.activeElement === focusables.last) {
@@ -16,7 +22,16 @@ export function trapFocus({ event, focusables }) {
16
22
  }
17
23
  }
18
24
 
19
- /* Returns focus back to trigger node when user is done tabbing through the content */
25
+ /**
26
+ * Manages focus behavior when tabbing through content, with options to control focus on exit
27
+ * @param {Object} params
28
+ * @param {KeyboardEvent} params.event - The keyboard event to handle
29
+ * @param {HTMLElement} params.triggerNode - Element that triggered the focus trap
30
+ * @param {HTMLElement} params.contentNode - Container element that holds focusable content
31
+ * @param {import('./focusable.js').Focusables} params.focusables - Object containing focusable elements
32
+ * @param {'next'|'trigger'} [params.tabOutFocus='next'] - Where to focus when tabbing out: 'next' element or back to trigger
33
+ * @param {Function} [params.onLeave=()=>{}] - Callback when focus leaves content, receives 'forward' or 'back' direction
34
+ */
20
35
  export function manageFocus({
21
36
  event,
22
37
  triggerNode,
@@ -44,7 +59,7 @@ export function manageFocus({
44
59
  if (tabOutFocus === 'trigger') triggerNode.focus()
45
60
  if (tabOutFocus === 'next') {
46
61
  event.preventDefault()
47
- const DOMFocusables = Focusable().keyboard
62
+ const DOMFocusables = getFocusableElements().keyboard
48
63
  const index = DOMFocusables.findIndex(e => e === triggerNode)
49
64
  const next = DOMFocusables[index + 1]
50
65
  if (next) next.focus()
@@ -1,7 +1,19 @@
1
+ /**
2
+ * Fixes inconsistent button focus behavior across browsers.
3
+ *
4
+ * In some browsers (like Firefox), clicking a button doesn't focus it.
5
+ * This function ensures buttons are focused when clicked, making behavior consistent.
6
+ *
7
+ * @see {@link https://zellwk.com/blog/inconsistent-button-behavior/|Source}
8
+ * @example
9
+ * // Add the fix to your app
10
+ * inconsistentButtonFix()
11
+ */
1
12
  export function inconsistentButtonFix() {
2
13
  // https://zellwk.com/blog/inconsistent-button-behavior/
3
- document.addEventListener('click', function (event) {
4
- if (event.target.matches('button')) {
14
+ /** @param {MouseEvent} event */
15
+ document.addEventListener('click', event => {
16
+ if (event.target instanceof HTMLButtonElement) {
5
17
  event.target.focus()
6
18
  }
7
19
  })
@@ -1,10 +1,22 @@
1
- // Adjusted function to take excludes as an argument
2
- export function traverseAndScramble(node, { excludes } = {}) {
1
+ /**
2
+ * Traverses a DOM node and scrambles its text content, excluding specified elements.
3
+ * Useful for creating placeholder/loading states or privacy features.
4
+ *
5
+ * @param {Element|ChildNode} node - The root element or node to start traversal from
6
+ * @param {Object} [options] - Options for traversal
7
+ * @param {string[]} [options.excludes=[]] - Array of CSS selectors to exclude from scrambling
8
+ * @example
9
+ * // Scramble all text in a container
10
+ * traverseAndScramble(document.querySelector('.container'))
11
+ *
12
+ * // Scramble text but exclude certain elements
13
+ * traverseAndScramble(document.body, {
14
+ * excludes: ['.no-scramble', '[data-no-scramble]']
15
+ * })
16
+ */
17
+ export function traverseAndScramble(node, { excludes = [] } = {}) {
3
18
  // Check if the current node matches any of the exclude selectors
4
- if (
5
- excludes &&
6
- excludes.some(selector => node.matches && node.matches(selector))
7
- ) {
19
+ if (node instanceof Element && excludes.some(selector => node.matches(selector))) {
8
20
  return // Skip this node and its children
9
21
  }
10
22
 
@@ -12,14 +24,23 @@ export function traverseAndScramble(node, { excludes } = {}) {
12
24
  if (child.nodeType === 3) {
13
25
  // Node.TEXT_NODE
14
26
  child.nodeValue = scrambleText(child.nodeValue)
15
- } else if (child.nodeType === 1) {
27
+ } else if (child.nodeType === 1 && child instanceof Element) {
16
28
  // Node.ELEMENT_NODE
17
29
  traverseAndScramble(child, { excludes }) // Recurse into child elements with excludes
18
30
  }
19
31
  })
20
32
  }
21
33
 
22
- // Function to scramble text remains unchanged
34
+ /**
35
+ * Scrambles text by replacing alphanumeric characters with random ones.
36
+ * Preserves case, punctuation, and other symbols.
37
+ *
38
+ * @param {string} text - The text to scramble
39
+ * @returns {string} The scrambled text
40
+ * @private
41
+ * @example
42
+ * scrambleText('Hello123!') // Returns something like 'Xk9mP2q!'
43
+ */
23
44
  function scrambleText(text) {
24
45
  return text
25
46
  .split('')
@@ -1,10 +1,13 @@
1
1
  import { lastArrayItem } from './item-position.js'
2
2
  /**
3
- * Joins an array of strings with commas and a conjunction for the last item
4
- * @param {string[]} array - Array of strings to join
5
- * @param {Object} options - Join options
6
- * @param {string} options.conjunction - Conjunction to use ('and' or 'or')
3
+ * Joins an array with a conjunction (and/or)
4
+ * @param {Array<string>} array - Array of strings to join
5
+ * @param {Object} [options] - Options for joining
6
+ * @param {string} [options.conjunction='and'] - Conjunction to use ('and' or 'or')
7
7
  * @returns {string} Joined string
8
+ * @example
9
+ * joinWithConjunction(['a', 'b', 'c']) // 'a, b, and c'
10
+ * joinWithConjunction(['a', 'b'], { conjunction: 'or' }) // 'a or b'
8
11
  */
9
12
  export function joinWithConjunction(array, { conjunction = 'and' } = {}) {
10
13
  if (!array?.length) return ''