@blockquote-web-components/blockquote-dialog 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,27 +30,28 @@ Inert content outside an active dialog is typically visually obscured or dimmed
30
30
 
31
31
  ##### Fields
32
32
 
33
- | Name | Privacy | Type | Default | Description | Inherited From |
34
- | ----------------------------- | ------- | --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
35
- | `treewalker` | | | | | |
36
- | `dialogRef` | | | | | |
37
- | `open` | public | `boolean` | | Opens the dialog when set to \`true\` and closes it when set to \`false\`. | |
38
- | `_slotTpl` | | | | | |
39
- | `_labeledbyTpl` | | | | | |
40
- | `_contentTpl` | | | | | |
41
- | `_scrollerTpl` | | | | | |
42
- | `_firstNodeFocusTrapTpl` | | | | | |
43
- | `_lastNodeFocusTrapTpl` | | | | | |
44
- | `_isConnectedCallbackResolve` | | | | | |
45
- | `_isConnectedCallback` | | | | | |
46
- | `_firstFocusableChild` | | | `undefined` | | |
47
- | `_lastFocusableChild` | | | `undefined` | | |
48
- | `_nextClickIsFromContent` | | `boolean` | `false` | | |
49
- | `_overflowRoot` | | | | | |
50
- | `type` | public | `string` | `'alert'` | The type of dialog for accessibility. Set this to \`alert\` to announce a
dialog as an alert dialog. | |
51
- | `label` | public | `string` | `''` | | |
52
- | `labelledby` | public | `string` | `''` | | |
53
- | `returnValue` | public | | | Gets or sets the dialog's return value, usually to indicate which button
a user pressed to close it.

https\://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/returnValue returnValue | |
33
+ | Name | Privacy | Type | Default | Description | Inherited From |
34
+ | ----------------------------- | ------- | --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- |
35
+ | `treewalker` | | | | | |
36
+ | `dialogRef` | | | | | |
37
+ | `open` | public | `boolean` | | Opens the dialog when set to \`true\` and closes it when set to \`false\`. | |
38
+ | `_slotTpl` | | | | | |
39
+ | `_labeledbyTpl` | | | | | |
40
+ | `_contentTpl` | | | | | |
41
+ | `_scrollerTpl` | | | | | |
42
+ | `_firstNodeFocusTrapTpl` | | | | | |
43
+ | `_lastNodeFocusTrapTpl` | | | | | |
44
+ | `_isConnectedCallbackResolve` | | | | | |
45
+ | `_isConnectedCallback` | | | | | |
46
+ | `_firstFocusableChild` | | | `undefined` | | |
47
+ | `_lastFocusableChild` | | | `undefined` | | |
48
+ | `_nextClickIsFromContent` | | `boolean` | `false` | | |
49
+ | `_overflowRoot` | | | | | |
50
+ | `type` | public | `string` | `'alert'` | The type of dialog for accessibility. Set this to \`alert\` to announce a
dialog as an alert dialog. | |
51
+ | `label` | public | `string` | `''` | The 'label' attribute will be used as the 'aria-label' for the dialog | |
52
+ | `labelledby` | public | `string` | `''` | The 'labelledby' attribute will be used as the 'aria-labelledby' for the dialog.
It will also be used to create a slot with the same 'id' and 'name'.
This slot is hidden by default and its 'name' and 'id' should correspond to the 'slot' attribute of an element in the Light DOM.
This connects the 'slot', 'name', and 'id' attributes of a slot to be used with ARIA relationships. | |
53
+ | `labelledbyVisible` | public | `boolean` | `false` | The 'labelledbyVisible' attribute will control the visibility of the slot created by 'labelledby'.
By default, it is set to 'hidden'. | |
54
+ | `returnValue` | public | | | Gets or sets the dialog's return value, usually to indicate which button
a user pressed to close it.

https\://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/returnValue returnValue | |
54
55
 
55
56
  ##### Methods
56
57
 
@@ -78,12 +79,14 @@ Inert content outside an active dialog is typically visually obscured or dimmed
78
79
 
79
80
  ##### Attributes
80
81
 
81
- | Name | Field | Inherited From |
82
- | ------------ | ---------- | -------------- |
83
- | `open` | open | |
84
- | `label` | label | |
85
- | `labelledby` | labelledby | |
86
- | `type` | type | |
82
+ | Name | Field | Inherited From |
83
+ | --------------------- | ----------------- | -------------- |
84
+ | `open` | open | |
85
+ | `label` | label | |
86
+ | `labelledby` | labelledby | |
87
+ | `labelledbyVisible` | | |
88
+ | `type` | type | |
89
+ | `labelledby-visibile` | labelledbyVisible | |
87
90
 
88
91
  ##### Slots
89
92
 
@@ -109,42 +112,6 @@ Inert content outside an active dialog is typically visually obscured or dimmed
109
112
  | ---- | ------------------ | ---------------- | ----------------------- | ------- |
110
113
  | `js` | `BlockquoteDialog` | BlockquoteDialog | src/BlockquoteDialog.js | |
111
114
 
112
- ### `src/dom-utils.js`:
113
-
114
- #### Functions
115
-
116
- | Name | Description | Parameters | Return |
117
- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | ----------------------------------------------------- |
118
- | `redispatchEvent` | Re-dispatches an event from the provided element.

This function is useful for forwarding non-composed events, such as \`change\`
events. | `element: Element, event: Event\|string, options: Object` | `boolean` |
119
- | `isElementInvisible` | Checks if an element should be ignored. | `element: Element, exceptions: Array` | `boolean` |
120
- | `isFocusable` | Checks if an element is focusable. An element is considered focusable if it matches
standard focusable elements criteria (such as buttons, inputs, etc., that are not disabled
and do not have a negative tabindex) or is a custom element with a shadow root that delegates focus. | `element: Element` | `boolean` |
121
- | `getFirstAndLastFocusableChildren` | Retrieves the first and last focusable children of a node using a TreeWalker. | `walker: IterableIterator<HTMLElement>` | `[first: HTMLElement\|null, last: HTMLElement\|null]` |
122
- | `walkComposedTree` | Traverse the composed tree from the root, selecting elements that meet the provided filter criteria.&#xA;You can pass \[NodeFilter]\(https\://developer.mozilla.org/en-US/docs/Web/API/NodeFilter) or 0 to retrieve all nodes. | `node: Node, whatToShow: number, filter: function, skipNode: function` | `IterableIterator<Node>` |
123
- | `getDeepActiveElement` | Returns the deepest active element, considering Shadow DOM subtrees | `root: Document \| ShadowRoot` | `Element` |
124
- | `deepContains` | Returns true if the first node contains the second, even if the second node&#xA;is in a shadow tree.&#xA;&#xA;The standard Node.contains() function does not account for Shadow DOM, and&#xA;returns false if the supplied target node is sitting inside a shadow tree&#xA;within the container. | `container: Node, target: Node` | `boolean` |
125
- | `indexOfItemContainingTarget` | Search a list element for the item that contains the specified target.&#xA;&#xA;When dealing with UI events (e.g., mouse clicks) that may occur in&#xA;subelements inside a list item, you can use this routine to obtain the&#xA;containing list item. | `items: NodeList\|Node[], target: Node` | `number` |
126
- | `composedAncestors` | Return the ancestors of the given node in the composed tree.&#xA;&#xA;In the composed tree, the ancestor of a node assigned to a slot is that slot,&#xA;not the node's DOM ancestor. The ancestor of a shadow root is its host. | `node: Node` | `Iterable<Node>` |
127
- | `isClickInsideRect` | Checks if a click event occurred inside a given bounding rectangle. | `rect: DOMRect, ev: PointerEvent` | `boolean` |
128
- | `randomID` | | `length` | |
129
-
130
- <hr/>
131
-
132
- #### Exports
133
-
134
- | Kind | Name | Declaration | Module | Package |
135
- | ---- | ---------------------------------- | -------------------------------- | ---------------- | ------- |
136
- | `js` | `redispatchEvent` | redispatchEvent | src/dom-utils.js | |
137
- | `js` | `isElementInvisible` | isElementInvisible | src/dom-utils.js | |
138
- | `js` | `isFocusable` | isFocusable | src/dom-utils.js | |
139
- | `js` | `getFirstAndLastFocusableChildren` | getFirstAndLastFocusableChildren | src/dom-utils.js | |
140
- | `js` | `walkComposedTree` | walkComposedTree | src/dom-utils.js | |
141
- | `js` | `getDeepActiveElement` | getDeepActiveElement | src/dom-utils.js | |
142
- | `js` | `deepContains` | deepContains | src/dom-utils.js | |
143
- | `js` | `indexOfItemContainingTarget` | indexOfItemContainingTarget | src/dom-utils.js | |
144
- | `js` | `composedAncestors` | composedAncestors | src/dom-utils.js | |
145
- | `js` | `isClickInsideRect` | isClickInsideRect | src/dom-utils.js | |
146
- | `js` | `randomID` | randomID | src/dom-utils.js | |
147
-
148
115
  ### `src/index.js`:
149
116
 
150
117
  #### Exports
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockquote-web-components/blockquote-dialog",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Webcomponent blockquote-dialog following open-wc recommendations",
5
5
  "keywords": [
6
6
  "lit",
@@ -103,7 +103,7 @@
103
103
  "SwitchCase": 1,
104
104
  "ignoredNodes": [
105
105
  "PropertyDefinition",
106
- "TemplateLiteral > CallExpression"
106
+ "TemplateLiteral > *"
107
107
  ]
108
108
  }
109
109
  ],
@@ -151,6 +151,7 @@
151
151
  "dependencies": {
152
152
  "@blockquote-web-components/blockquote-directive-ariaidref-slot": "^1.0.0",
153
153
  "@blockquote-web-components/blockquote-mixin-slot-content": "^1.5.0",
154
+ "@blockquote/dev-utilities": "^1.0.0",
154
155
  "@material/web": "^1.4.1",
155
156
  "lit": "^3.1.3"
156
157
  },
@@ -164,5 +165,5 @@
164
165
  "access": "public"
165
166
  },
166
167
  "customElements": "custom-elements.json",
167
- "gitHead": "17dc7bb75989d134c948238d64c634faf035941a"
168
+ "gitHead": "d0a77e49014065e4265fa11d4736e65671f53479"
168
169
  }
@@ -7,7 +7,7 @@ import {
7
7
  isFocusable,
8
8
  getFirstAndLastFocusableChildren,
9
9
  walkComposedTree,
10
- } from './dom-utils.js';
10
+ } from '@blockquote/dev-utilities';
11
11
  import { styles } from './styles/blockquote-dialog-styles.css.js';
12
12
  import { styles as animations } from './styles/blockqoute-dialog-animations-styles.css.js';
13
13
 
@@ -44,6 +44,7 @@ import { styles as animations } from './styles/blockqoute-dialog-animations-styl
44
44
  * @attribute open
45
45
  * @attribute label
46
46
  * @attribute labelledby
47
+ * @attribute labelledbyVisible
47
48
  * @attribute type
48
49
  * @fires open
49
50
  * @fires close
@@ -84,14 +85,32 @@ export class BlockquoteDialog extends LitElement {
84
85
  attribute: false,
85
86
  },
86
87
 
88
+ /**
89
+ * The 'label' attribute will be used as the 'aria-label' for the dialog
90
+ */
87
91
  label: {
88
92
  type: String,
89
93
  },
90
94
 
95
+ /**
96
+ * The 'labelledby' attribute will be used as the 'aria-labelledby' for the dialog.
97
+ * It will also be used to create a slot with the same 'id' and 'name'.
98
+ * This slot is hidden by default and its 'name' and 'id' should correspond to the 'slot' attribute of an element in the Light DOM.
99
+ * This connects the 'slot', 'name', and 'id' attributes of a slot to be used with ARIA relationships.
100
+ */
91
101
  labelledby: {
92
102
  type: String,
93
103
  },
94
104
 
105
+ /**
106
+ * The 'labelledbyVisible' attribute will control the visibility of the slot created by 'labelledby'.
107
+ * By default, it is set to 'hidden'.
108
+ */
109
+ labelledbyVisible: {
110
+ type: String,
111
+ attribute: 'labelledby-visibile',
112
+ },
113
+
95
114
  /**
96
115
  * The type of dialog for accessibility. Set this to `alert` to announce a
97
116
  * dialog as an alert dialog.
@@ -131,6 +150,7 @@ export class BlockquoteDialog extends LitElement {
131
150
  this.type = 'alert';
132
151
  this.label = '';
133
152
  this.labelledby = '';
153
+ this.labelledbyVisible = false;
134
154
 
135
155
  if (!isServer) {
136
156
  this.addEventListener('submit', this._handleSubmit);
@@ -210,7 +230,11 @@ export class BlockquoteDialog extends LitElement {
210
230
  }
211
231
 
212
232
  get _labeledbyTpl() {
213
- return html` ${this.labelledby ? blockquoteDirectiveAriaidrefSlot(this.labelledby) : ''} `;
233
+ return html`
234
+ ${this.labelledby
235
+ ? blockquoteDirectiveAriaidrefSlot(this.labelledby, this.labelledbyVisible)
236
+ : ''}
237
+ `;
214
238
  }
215
239
 
216
240
  get _contentTpl() {
package/src/dom-utils.js DELETED
@@ -1,270 +0,0 @@
1
- /* c8 ignore start */
2
- /**
3
- * Re-dispatches an event from the provided element.
4
- * @author @material/web
5
- * @see https://github.com/material-components/material-web/blob/main/internal/events/redispatch-event.ts
6
- * @param {Element} element - The element to dispatch the event from.
7
- * @param {Event} event - The event to re-dispatch.
8
- * @param {Object} [options={}] - An object with properties to override in the new event.
9
- * @returns {boolean} - Whether or not the event was dispatched (if cancelable).
10
- */
11
-
12
- const redispatchEventFromEvent = (element, event, options = {}) => {
13
- // For bubbling events in SSR light DOM (or composed), stop their propagation
14
- // and dispatch the copy.
15
- if (event.bubbles && (!element.shadowRoot || event.composed)) {
16
- event.stopPropagation();
17
- }
18
-
19
- const copy = Reflect.construct(event.constructor, [event.type, { ...event, ...options }]);
20
- const dispatched = element.dispatchEvent(copy);
21
- if (!dispatched) {
22
- event.preventDefault();
23
- }
24
-
25
- return dispatched;
26
- };
27
-
28
- /**
29
- * Re-dispatches an event from the provided element.
30
- *
31
- * This function is useful for forwarding non-composed events, such as `change`
32
- * events.
33
- *
34
- * @example
35
- * class MyDialog extends LitElement {
36
- * render() {
37
- * return html`<dialog @close=${this.redispatchEvent}>...</dialog>`;
38
- * }
39
- *
40
- * protected redispatchEvent(ev: Event) {
41
- * redispatchEvent(this, ev, { cancelable: true });
42
- * }
43
- * }
44
- *
45
- * @param {Element} element - The element to dispatch the event from.
46
- * @param {Event|string} event - The event to re-dispatch. If it's a string, a new Event is created.
47
- * @param {Object} [options={}] - An object with properties to override in the new event.
48
- * @returns {boolean} - Whether or not the event was dispatched (if cancelable).
49
- */
50
- export const redispatchEvent = (element, event, options = {}) => {
51
- if (typeof event === 'string') {
52
- const eventType = event;
53
- const newEvent = new CustomEvent(eventType);
54
- return redispatchEventFromEvent(element, newEvent, options);
55
- }
56
- return redispatchEventFromEvent(element, event, options);
57
- };
58
-
59
- /**
60
- * Checks if an element should be ignored.
61
- * @param {Element} element - The DOM element to check.
62
- * @param {Array} [exceptions=['dialog', '[popover]']] - Array of Elements to ignore when checking the element.
63
- * @returns {boolean} True if the element should be ignored by a screen reader, false otherwise.
64
- */
65
- export const isElementInvisible = (element, exceptions = ['dialog', '[popover]']) => {
66
- if (!element || !(element instanceof HTMLElement)) {
67
- return false;
68
- }
69
-
70
- if (element.matches(exceptions.join(','))) {
71
- return false;
72
- }
73
-
74
- const computedStyle = window.getComputedStyle(element);
75
- const isStyleHidden = computedStyle.display === 'none' || computedStyle.visibility === 'hidden';
76
- const isAttributeHidden = element.matches('[disabled], [hidden], [inert], [aria-hidden="true"]');
77
-
78
- return isStyleHidden || isAttributeHidden;
79
- };
80
-
81
- /**
82
- * Checks if an element is focusable. An element is considered focusable if it matches
83
- * standard focusable elements criteria (such as buttons, inputs, etc., that are not disabled
84
- * and do not have a negative tabindex) or is a custom element with a shadow root that delegates focus.
85
- *
86
- * @param {Element} element - The DOM element to check for focusability.
87
- * @returns {boolean} True if the element is focusable, false otherwise.
88
- */
89
- export const isFocusable = element => {
90
- if (!(element instanceof HTMLElement)) {
91
- return false;
92
- }
93
-
94
- // https://stackoverflow.com/a/30753870/76472
95
- const knownFocusableElements = `a[href],area[href],button:not([disabled]),details,iframe,object,input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[contentEditable="true"],[tabindex]:not([tabindex^="-"]),audio[controls],video[controls]`;
96
-
97
- if (element.matches(knownFocusableElements)) {
98
- return true;
99
- }
100
-
101
- const isDisabledCustomElement =
102
- element.localName.includes('-') && element.matches('[disabled], [aria-disabled="true"]');
103
- if (isDisabledCustomElement) {
104
- return false;
105
- }
106
-
107
- return element.shadowRoot?.delegatesFocus ?? false;
108
- };
109
-
110
- /**
111
- * Retrieves the first and last focusable children of a node using a TreeWalker.
112
- *
113
- * @param {IterableIterator<HTMLElement>} walker - The TreeWalker object used to traverse the node's children.
114
- * @returns {[first: HTMLElement|null, last: HTMLElement|null]} An object containing the first and last focusable children. If no focusable children are found, `null` is returned for both.
115
- */
116
- export const getFirstAndLastFocusableChildren = walker => {
117
- let firstFocusableChild = null;
118
- let lastFocusableChild = null;
119
-
120
- for (const currentNode of walker) {
121
- if (!firstFocusableChild) {
122
- firstFocusableChild = currentNode;
123
- }
124
- lastFocusableChild = currentNode;
125
- }
126
-
127
- return [firstFocusableChild, lastFocusableChild];
128
- };
129
-
130
- /**
131
- * Traverse the composed tree from the root, selecting elements that meet the provided filter criteria.
132
- * You can pass [NodeFilter](https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter) or 0 to retrieve all nodes.
133
- * @author Jan Miksovsky
134
- * @see https://github.com/JanMiksovsky/elix/blob/main/src/core/dom.js
135
- * @param {Node} node - The root node for traversal.
136
- * @param {number} [whatToShow=0] - NodeFilter code for node types to include.
137
- * @param {function} [filter=(n: Node) => true] - Filters nodes. Child nodes are considered even if parent does not satisfy the filter.
138
- * @param {function} [skipNode=(n: Node) => false] - Determines whether to skip a node and its children.
139
- * @returns {IterableIterator<Node>} An iterator yielding nodes meeting the filter criteria.
140
- */
141
- export function* walkComposedTree(
142
- node,
143
- whatToShow = 0,
144
- filter = () => true,
145
- skipNode = () => false,
146
- ) {
147
- if ((whatToShow && node.nodeType !== whatToShow) || skipNode(node)) {
148
- return;
149
- }
150
-
151
- if (filter(node)) {
152
- yield node;
153
- }
154
-
155
- const children =
156
- // eslint-disable-next-line no-nested-ternary
157
- node instanceof HTMLElement && node.shadowRoot
158
- ? node.shadowRoot.children
159
- : node instanceof HTMLSlotElement
160
- ? node.assignedNodes({ flatten: true })
161
- : node.childNodes;
162
-
163
- for (const child of children) {
164
- yield* walkComposedTree(child, whatToShow, filter, skipNode);
165
- }
166
- }
167
-
168
- /**
169
- * Returns the deepest active element, considering Shadow DOM subtrees
170
- * @param {Document | ShadowRoot} root - The root element to start the search from.
171
- * @returns {Element} The deepest active element or body element if no active element is found.
172
- */
173
- export const getDeepActiveElement = (root = document) => {
174
- const activeEl = root.activeElement;
175
- if (activeEl) {
176
- if (activeEl.shadowRoot) {
177
- return getDeepActiveElement(activeEl.shadowRoot) ?? activeEl;
178
- }
179
- return activeEl;
180
- }
181
- return document.body;
182
- };
183
-
184
- /**
185
- * Returns true if the first node contains the second, even if the second node
186
- * is in a shadow tree.
187
- *
188
- * The standard Node.contains() function does not account for Shadow DOM, and
189
- * returns false if the supplied target node is sitting inside a shadow tree
190
- * within the container.
191
- *
192
- * @param {Node} container - The container to search within.
193
- * @param {Node} target - The node that may be inside the container.
194
- * @returns {boolean} - True if the container contains the target node.
195
- */
196
- export const deepContains = (container, target) => {
197
- /** @type {any} */
198
- let current = target;
199
- while (current) {
200
- const parent = current.assignedSlot || current.parentNode || current.host;
201
- if (parent === container) {
202
- return true;
203
- }
204
- current = parent;
205
- }
206
- return false;
207
- };
208
-
209
- /**
210
- * Search a list element for the item that contains the specified target.
211
- *
212
- * When dealing with UI events (e.g., mouse clicks) that may occur in
213
- * subelements inside a list item, you can use this routine to obtain the
214
- * containing list item.
215
- *
216
- * @author Jan Miksovsky
217
- * @see https://github.com/JanMiksovsky/elix/blob/main/src/core/dom.js
218
- * @param {NodeList|Node[]} items - A list element containing a set of items
219
- * @param {Node} target - A target element that may or may not be an item in the
220
- * list.
221
- * @returns {number} - The index of the list child that is or contains the
222
- * indicated target node. Returns -1 if not found.
223
- */
224
- export const indexOfItemContainingTarget = (items, target) =>
225
- Array.prototype.findIndex.call(
226
- items,
227
- (/** @type Node */ item) => item === target || deepContains(item, target),
228
- );
229
-
230
- /**
231
- * Return the ancestors of the given node in the composed tree.
232
- *
233
- * In the composed tree, the ancestor of a node assigned to a slot is that slot,
234
- * not the node's DOM ancestor. The ancestor of a shadow root is its host.
235
- *
236
- * @author Jan Miksovsky
237
- * @see https://github.com/JanMiksovsky/elix/blob/main/src/core/dom.js
238
- * @param {Node} node
239
- * @returns {Iterable<Node>}
240
- */
241
- export function* composedAncestors(node) {
242
- for (let /** @type {Node|null} */ current = node; current; ) {
243
- const next =
244
- // eslint-disable-next-line no-nested-ternary
245
- current instanceof HTMLElement && current.assignedSlot
246
- ? current.assignedSlot
247
- : current instanceof ShadowRoot
248
- ? current.host
249
- : current.parentNode;
250
- if (next) {
251
- yield next;
252
- }
253
- current = next;
254
- }
255
- }
256
-
257
- /**
258
- * Checks if a click event occurred inside a given bounding rectangle.
259
- *
260
- * @param {DOMRect} rect - The bounding rectangle, typically obtained from `element.getBoundingClientRect()`.
261
- * @param {PointerEvent} ev - The click event.
262
- * @returns {boolean} True if the click occurred inside the rectangle, false otherwise.
263
- */
264
- export const isClickInsideRect = (rect, ev) => {
265
- const { top, left, height, width } = rect;
266
- const { clientX, clientY } = ev;
267
- return clientY >= top && clientY <= top + height && clientX >= left && clientX <= left + width;
268
- };
269
-
270
- export const randomID = (length = 10) => Math.random().toString(36).substring(2, length);