@oscarpalmer/toretto 0.48.0 → 0.49.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/src/aria.ts CHANGED
@@ -1,13 +1,11 @@
1
1
  import {setElementValues, updateElementValue} from './internal/element-value';
2
- import type {AriaAttribute, AriaAttributeUnprefixed, AriaRole} from './models';
2
+ import type {AnyAriaAttribute, AnyAriaBooleanAttribute, AriaRole} from './models';
3
3
 
4
4
  // #region Types
5
5
 
6
- type AnyAriaAttribute = AriaAttribute | AriaAttributeUnprefixed;
7
-
8
- type AriaAttributeItem = {
9
- name: AnyAriaAttribute;
10
- value?: string;
6
+ type AriaAttributeItem<Name extends AnyAriaAttribute = AnyAriaAttribute> = {
7
+ name: Name;
8
+ value?: Name extends AnyAriaBooleanAttribute ? boolean | string : string;
11
9
  };
12
10
 
13
11
  // #endregion
@@ -18,10 +16,22 @@ type AriaAttributeItem = {
18
16
  * Get the value of a specific _ARIA_ attribute from an element
19
17
  *
20
18
  * @param element Element to get _ARIA_ attribute from
21
- * @param name _ARIA_ name
19
+ * @param name _ARIA_ attribute name
22
20
  * @returns _ARIA_ value _(or `undefined`)_
23
21
  */
24
- export function getAria(element: Element, attribute: AnyAriaAttribute): string | undefined;
22
+ export function getAria(
23
+ element: Element,
24
+ name: AnyAriaBooleanAttribute,
25
+ ): boolean | string | undefined;
26
+
27
+ /**
28
+ * Get the value of a specific _ARIA_ attribute from an element
29
+ *
30
+ * @param element Element to get _ARIA_ attribute from
31
+ * @param name _ARIA_ attribute name
32
+ * @returns _ARIA_ value _(or `undefined`)_
33
+ */
34
+ export function getAria(element: Element, name: AnyAriaAttribute): string | undefined;
25
35
 
26
36
  /**
27
37
  * Get specific _ARIA_ attributes from an element
@@ -32,8 +42,12 @@ export function getAria(element: Element, attribute: AnyAriaAttribute): string |
32
42
  */
33
43
  export function getAria<Attribute extends AnyAriaAttribute>(
34
44
  element: Element,
35
- attributes: Attribute[],
36
- ): Record<Attribute, string | undefined>;
45
+ names: Attribute[],
46
+ ): {
47
+ [Key in Attribute as Key extends `aria-${infer Name}`
48
+ ? Name
49
+ : Key]: Key extends AnyAriaBooleanAttribute ? boolean | string | undefined : string | undefined;
50
+ };
37
51
 
38
52
  export function getAria(element: Element, value: string | string[]): unknown {
39
53
  if (!(element instanceof Element)) {
@@ -44,7 +58,7 @@ export function getAria(element: Element, value: string | string[]): unknown {
44
58
  return typeof value === 'string' ? getAriaValue(element, value) : undefined;
45
59
  }
46
60
 
47
- const arias = {} as Record<string, string | undefined>;
61
+ const arias = {} as Record<string, unknown>;
48
62
 
49
63
  const {length} = value;
50
64
 
@@ -59,8 +73,20 @@ export function getAria(element: Element, value: string | string[]): unknown {
59
73
  return arias;
60
74
  }
61
75
 
62
- function getAriaValue(element: Element, attribute: string): string | undefined {
63
- return element.getAttribute(getName(attribute)) ?? undefined;
76
+ function getAriaValue(element: Element, attribute: string): unknown {
77
+ const name = getName(attribute);
78
+
79
+ const value = element.getAttribute(name) ?? undefined;
80
+
81
+ if (
82
+ ariaBooleanAttributesSet.has(name as never) &&
83
+ typeof value === 'string' &&
84
+ EXPRESSION_BOOLEAN.test(value)
85
+ ) {
86
+ return value.toLowerCase() === 'true';
87
+ }
88
+
89
+ return value;
64
90
  }
65
91
 
66
92
  function getName(value: string): string {
@@ -88,7 +114,22 @@ export function getRole(element: Element): string | undefined {
88
114
  * @param attribute _ARIA_ attribute to set
89
115
  * @param value _ARIA_ attribute value
90
116
  */
91
- export function setAria(element: Element, attribute: AnyAriaAttribute, value?: unknown): void;
117
+ export function setAria(
118
+ element: Element,
119
+ attribute: AnyAriaBooleanAttribute,
120
+ value?: boolean | string,
121
+ ): void;
122
+
123
+ /**
124
+ * Set an _ARIA_ attribute on an element
125
+ *
126
+ * _(Or remove it, if value is `null` or `undefined`)_
127
+ *
128
+ * @param element Element for _ARIA_ attribute
129
+ * @param attribute _ARIA_ attribute to set
130
+ * @param value _ARIA_ attribute value
131
+ */
132
+ export function setAria(element: Element, attribute: AnyAriaAttribute, value?: string): void;
92
133
 
93
134
  /**
94
135
  * Set one or more _ARIA_ attributes on an element
@@ -108,16 +149,9 @@ export function setAria(element: Element, attributes: AriaAttributeItem[]): void
108
149
  * @param element Element for _ARIA_ attributes
109
150
  * @param attributes _ARIA_ attributes to set
110
151
  */
111
- export function setAria(
112
- element: Element,
113
- attributes: Partial<Record<AnyAriaAttribute, unknown>>,
114
- ): void;
152
+ export function setAria(element: Element, attributes: Record<string, unknown>): void;
115
153
 
116
- export function setAria(
117
- element: Element,
118
- first: AnyAriaAttribute | AriaAttributeItem[] | Partial<Record<AnyAriaAttribute, unknown>>,
119
- second?: unknown,
120
- ): void {
154
+ export function setAria(element: Element, first: unknown, second?: unknown): void {
121
155
  setElementValues(element, first, second, null, updateAriaAttribute);
122
156
  }
123
157
 
@@ -140,10 +174,22 @@ export function setRole(element: Element, role?: AriaRole): void {
140
174
  }
141
175
 
142
176
  function updateAriaAttribute(element: Element, key: string, value: unknown): void {
177
+ const name = getName(key);
178
+
179
+ let actual = value;
180
+
181
+ if (
182
+ ariaBooleanAttributesSet.has(name as never) &&
183
+ typeof value === 'string' &&
184
+ EXPRESSION_BOOLEAN.test(value)
185
+ ) {
186
+ actual = value.toLowerCase() === 'true';
187
+ }
188
+
143
189
  updateElementValue(
144
190
  element,
145
- getName(key),
146
- value,
191
+ name,
192
+ actual,
147
193
  // Using `.call` in `updateElementValue`
148
194
  // oxlint-disable-next-line typescript/unbound-method
149
195
  element.setAttribute,
@@ -151,7 +197,6 @@ function updateAriaAttribute(element: Element, key: string, value: unknown): voi
151
197
  // oxlint-disable-next-line typescript/unbound-method
152
198
  element.removeAttribute,
153
199
  false,
154
- false,
155
200
  );
156
201
  }
157
202
 
@@ -163,4 +208,33 @@ const ATTRIBUTE_ARIA_PREFIX = 'aria-';
163
208
 
164
209
  const EXPRESSION_ARIA_PREFIX = /^aria-/i;
165
210
 
211
+ const EXPRESSION_BOOLEAN = /^(true|false)$/i;
212
+
213
+ /**
214
+ * List of _ARIA_ attributes that can be treated as boolean values
215
+ */
216
+ export const ariaBooleanAttributes: readonly AnyAriaBooleanAttribute[] = Object.freeze([
217
+ 'aria-atomic',
218
+ 'aria-busy',
219
+ 'aria-checked',
220
+ 'aria-current',
221
+ 'aria-disabled',
222
+ 'aria-expanded',
223
+ 'aria-haspopup',
224
+ 'aria-hidden',
225
+ 'aria-invalid',
226
+ 'aria-modal',
227
+ 'aria-multiline',
228
+ 'aria-multiselectable',
229
+ 'aria-pressed',
230
+ 'aria-readonly',
231
+ 'aria-required',
232
+ 'aria-selected',
233
+ ]);
234
+
235
+ /**
236
+ * Set of _ARIA_ attributes that can be treated as boolean values
237
+ */
238
+ export const ariaBooleanAttributesSet = new Set(ariaBooleanAttributes);
239
+
166
240
  // #endregion
package/src/create.ts CHANGED
@@ -1,15 +1,29 @@
1
+ import {isPlainObject} from '@oscarpalmer/atoms/is';
1
2
  import type {Primitive} from '@oscarpalmer/atoms/models';
3
+ import {setAria} from './aria';
2
4
  import {setAttributes} from './attribute';
5
+ import {setData} from './data';
6
+ import type {AnyAriaAttribute, AnyAriaBooleanAttribute, CSSValues} from './models';
3
7
  import {setProperties} from './property';
4
8
  import {setStyles} from './style';
5
9
 
6
10
  // #region Types
7
11
 
8
- type Properties<Target extends Element> = {
9
- [Property in keyof Target]?: Target[Property] extends Primitive ? Target[Property] : never;
12
+ type CreateElementValues<Target extends Element> = {
13
+ aria?: CreateElementValuesAria;
14
+ attribute?: Record<string, unknown>;
15
+ data?: Record<string, unknown>;
16
+ property?: CreateElementValuesProperties<Target>;
17
+ style?: Partial<CSSValues>;
10
18
  };
11
19
 
12
- type Styles = Partial<Record<keyof CSSStyleDeclaration, unknown>>;
20
+ type CreateElementValuesAria = {
21
+ [Key in AnyAriaAttribute]?: Key extends AnyAriaBooleanAttribute ? boolean | string : string;
22
+ };
23
+
24
+ type CreateElementValuesProperties<Target extends Element> = {
25
+ [Property in keyof Target]?: Target[Property] extends Primitive ? Target[Property] : never;
26
+ };
13
27
 
14
28
  // #endregion
15
29
 
@@ -19,59 +33,60 @@ type Styles = Partial<Record<keyof CSSStyleDeclaration, unknown>>;
19
33
  * Creates an _HTML_ element with the specified tag name together with optional properties, attributes, and styles
20
34
  *
21
35
  * @param tag Tag name
22
- * @param properties Element properties
23
- * @param attributes Element attributes
24
- * @param styles Element styles
36
+ * @param values Element values
25
37
  * @returns Created element
26
38
  */
27
39
  export function createElement<TagName extends keyof HTMLElementTagNameMap>(
28
40
  tag: TagName,
29
- properties?: Properties<HTMLElementTagNameMap[TagName]>,
30
- attributes?: Record<string, unknown>,
31
- styles?: Styles,
41
+ values?: CreateElementValues<HTMLElementTagNameMap[TagName]>,
32
42
  ): HTMLElementTagNameMap[TagName];
33
43
 
34
44
  /**
35
45
  * Creates an _HTML_ element with the specified tag name together with optional properties, attributes, and styles
36
46
  *
37
47
  * @param tag Tag name
38
- * @param properties Element properties
39
- * @param attributes Element attributes
40
- * @param styles Element styles
48
+ * @param values Element values
41
49
  * @returns Created element
42
50
  */
43
51
  export function createElement(
44
52
  tag: string,
45
- properties?: Properties<HTMLElement>,
46
- attributes?: Record<string, unknown>,
47
- styles?: Styles,
53
+ values?: CreateElementValues<HTMLElement>,
48
54
  ): HTMLUnknownElement;
49
55
 
50
- export function createElement(
51
- tag: string,
52
- properties?: Properties<HTMLElement>,
53
- attributes?: Record<string, unknown>,
54
- styles?: Styles,
55
- ): HTMLElement {
56
+ export function createElement(tag: string, values?: CreateElementValues<HTMLElement>): HTMLElement {
56
57
  if (typeof tag !== 'string') {
57
58
  throw new TypeError(MESSAGE);
58
59
  }
59
60
 
60
61
  const element = document.createElement(tag);
61
62
 
62
- if (properties != null) {
63
- setProperties(element, properties);
64
- }
63
+ const {aria, attribute, data, property, style} = getElementValues<HTMLElement>(values);
65
64
 
66
- if (attributes != null) {
67
- setAttributes(element, attributes);
68
- }
65
+ setAria(element, aria ?? {});
66
+ setAttributes(element, attribute ?? {});
67
+ setData(element, data ?? {});
68
+ setProperties(element, property ?? {});
69
+ setStyles(element, style ?? {});
70
+
71
+ return element;
72
+ }
69
73
 
70
- if (styles != null) {
71
- setStyles(element, styles);
74
+ function getElementValues<Target extends Element>(input?: unknown): CreateElementValues<Target> {
75
+ if (!isPlainObject(input)) {
76
+ return {};
72
77
  }
73
78
 
74
- return element;
79
+ return {
80
+ aria: isPlainObject(input.aria) ? input.aria : undefined,
81
+ attribute: isPlainObject(input.attribute)
82
+ ? (input.attribute as Record<string, unknown>)
83
+ : undefined,
84
+ data: isPlainObject(input.data) ? input.data : undefined,
85
+ property: isPlainObject(input.property)
86
+ ? (input.property as CreateElementValuesProperties<Target>)
87
+ : undefined,
88
+ style: isPlainObject(input.style) ? input.style : undefined,
89
+ };
75
90
  }
76
91
 
77
92
  // #endregion
package/src/data.ts CHANGED
@@ -7,28 +7,46 @@ import {EXPRESSION_DATA_PREFIX} from './internal/get-value';
7
7
  // #region Functions
8
8
 
9
9
  /**
10
- * Get a keyed data value from an element
10
+ * Get a keyed data value from an element, without parsing the value
11
11
  *
12
12
  * @param element Element to get data from
13
13
  * @param key Data key
14
- * @param parse Parse values? _(defaults to `true`)_
14
+ * @param parse Parse the value?
15
15
  * @returns Data value
16
16
  */
17
- export function getData(element: Element, key: string, parse?: boolean): unknown;
17
+ export function getData(element: Element, key: string, parse: false): string | undefined;
18
18
 
19
19
  /**
20
- * Get keyed data values from an element
20
+ * Get a keyed data value from an element and parse the value
21
+ *
22
+ * @param element Element to get data from
23
+ * @param key Data key
24
+ * @returns Data value
25
+ */
26
+ export function getData(element: Element, key: string): unknown;
27
+
28
+ /**
29
+ * Get keyed data values from an element, without parsing the values
21
30
  *
22
31
  * @param element Element to get data from
23
32
  * @param keys Keys of the data values to get
24
- * @param parse Parse values? _(defaults to `true`)_
33
+ * @param parse Parse the values?
25
34
  * @returns Keyed data values
26
35
  */
27
36
  export function getData<Key extends string>(
28
37
  element: Element,
29
38
  keys: Key[],
30
- parse?: boolean,
31
- ): Record<Key, unknown>;
39
+ parse: false,
40
+ ): Record<Key, string | undefined>;
41
+
42
+ /**
43
+ * Get keyed data values from an element and parse the values
44
+ *
45
+ * @param element Element to get data from
46
+ * @param keys Keys of the data values to get
47
+ * @returns Keyed data values
48
+ */
49
+ export function getData<Key extends string>(element: Element, keys: Key[]): Record<Key, unknown>;
32
50
 
33
51
  export function getData(element: Element, keys: string | string[], parseValues?: boolean): unknown {
34
52
  if (!(element instanceof Element)) {
@@ -38,13 +56,7 @@ export function getData(element: Element, keys: string | string[], parseValues?:
38
56
  const noParse = parseValues === false;
39
57
 
40
58
  if (typeof keys === 'string') {
41
- const value = (element as HTMLElement).dataset[camelCase(keys)];
42
-
43
- if (value === undefined) {
44
- return;
45
- }
46
-
47
- return noParse ? value : parse(value);
59
+ return getDataValue(element, keys, noParse);
48
60
  }
49
61
 
50
62
  const {length} = keys;
@@ -53,18 +65,27 @@ export function getData(element: Element, keys: string | string[], parseValues?:
53
65
 
54
66
  for (let index = 0; index < length; index += 1) {
55
67
  const key = keys[index];
56
- const value = (element as HTMLElement).dataset[camelCase(key)];
57
68
 
58
- if (value == null) {
59
- data[key] = undefined;
60
- } else {
61
- data[key] = noParse ? value : parse(value);
62
- }
69
+ data[key] = getDataValue(element, key, noParse);
63
70
  }
64
71
 
65
72
  return data;
66
73
  }
67
74
 
75
+ function getDataValue(element: Element, key: string, noParse: boolean): unknown {
76
+ const value = (element as HTMLElement).dataset[camelCase(key)];
77
+
78
+ if (value == null) {
79
+ return;
80
+ }
81
+
82
+ if (noParse) {
83
+ return value;
84
+ }
85
+
86
+ return parse(value) ?? value;
87
+ }
88
+
68
89
  function getName(original: string): string {
69
90
  return `${ATTRIBUTE_DATA_PREFIX}${kebabCase(original.replace(EXPRESSION_DATA_PREFIX, ''))}`;
70
91
  }
@@ -101,7 +122,6 @@ function updateDataAttribute(element: Element, key: string, value: unknown): voi
101
122
  // Using `.call` in `updateElementValue`
102
123
  // oxlint-disable-next-line typescript/unbound-method
103
124
  element.removeAttribute,
104
- false,
105
125
  true,
106
126
  );
107
127
  }
@@ -148,7 +148,6 @@ export function updateAttribute(
148
148
  // Using `.call` in `updateElementValue`
149
149
  // oxlint-disable-next-line typescript/unbound-method
150
150
  element.removeAttribute,
151
- isBoolean,
152
151
  false,
153
152
  );
154
153
  }
@@ -4,6 +4,14 @@ import {isAttribute} from './attribute';
4
4
 
5
5
  // #region Functions
6
6
 
7
+ function getValueForAttribute(value: unknown, json: boolean): string | undefined {
8
+ if (typeof value === 'string') {
9
+ return value;
10
+ }
11
+
12
+ return json ? JSON.stringify(value) : getString(value);
13
+ }
14
+
7
15
  function ignoreSetAttribute(element: Element, name: string): boolean {
8
16
  if (element instanceof HTMLTextAreaElement && name === 'value') {
9
17
  return true;
@@ -79,13 +87,12 @@ export function updateElementValue(
79
87
  value: unknown,
80
88
  set: (key: string, value: string) => void,
81
89
  remove: (key: string) => void,
82
- isBoolean: boolean,
83
90
  json: boolean,
84
91
  ): void {
85
92
  if (value == null) {
86
93
  remove.call(element, key);
87
94
  } else if (!ignoreSetAttribute(element, key)) {
88
- set.call(element, key, json ? JSON.stringify(value) : getString(value));
95
+ set.call(element, key, getValueForAttribute(value, json) as string);
89
96
  }
90
97
  }
91
98
 
package/src/models.ts CHANGED
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Any _ARIA_ attribute for an element _(both prefixed and unprefixed)_
3
+ */
4
+ export type AnyAriaAttribute = AriaAttribute | AriaAttributeUnprefixed;
5
+
6
+ /**
7
+ * Any _ARIA_ attribute for an element that can be set to a boolean value _(both prefixed and unprefixed)_
8
+ */
9
+ export type AnyAriaBooleanAttribute = AriaBooleanAttribute | AriaBooleanAttributeUnprefixed;
10
+
1
11
  /**
2
12
  * _ARIA_ attribute for an element
3
13
  *
@@ -11,11 +21,39 @@ export type AriaAttribute = keyof AriaAttributes;
11
21
  * _(https://www.w3.org/TR/wai-aria-1.3/#aria-attributes)_
12
22
  */
13
23
  export type AriaAttributeUnprefixed = keyof {
14
- [Key in AriaAttribute as Key extends `aria-${infer Name}` ? Name : never]: string | null;
24
+ [Key in AriaAttribute as Key extends `aria-${infer Name}` ? Name : never]: unknown;
15
25
  };
16
26
 
17
27
  type AriaAttributes = {
18
- [Key in keyof ARIAMixin as NormalizedName<Key>]: string | null;
28
+ [Key in keyof ARIAMixin as NormalizedName<Key>]: unknown;
29
+ };
30
+
31
+ /**
32
+ * _ARIA_ attribute for an element that can be set to a boolean value
33
+ */
34
+ export type AriaBooleanAttribute =
35
+ | 'aria-atomic'
36
+ | 'aria-busy'
37
+ | 'aria-checked'
38
+ | 'aria-current'
39
+ | 'aria-disabled'
40
+ | 'aria-expanded'
41
+ | 'aria-haspopup'
42
+ | 'aria-hidden'
43
+ | 'aria-invalid'
44
+ | 'aria-modal'
45
+ | 'aria-multiline'
46
+ | 'aria-multiselectable'
47
+ | 'aria-pressed'
48
+ | 'aria-readonly'
49
+ | 'aria-required'
50
+ | 'aria-selected';
51
+
52
+ /**
53
+ * _ARIA_ attribute for an element that can be set to a boolean value, without the `aria-` prefix
54
+ */
55
+ export type AriaBooleanAttributeUnprefixed = keyof {
56
+ [Key in AriaBooleanAttribute as Key extends `aria-${infer Name}` ? Name : never]: string | null;
19
57
  };
20
58
 
21
59
  type NormalizedName<Key extends string> = Key extends `aria${infer Name}`
@@ -128,6 +166,23 @@ export type Attribute = {
128
166
  value: unknown;
129
167
  };
130
168
 
169
+ /**
170
+ * CSS styles for an element
171
+ */
172
+ export type CSSStyles = Record<keyof CSSStyleDeclaration, unknown>;
173
+
174
+ /**
175
+ * CSSS values for an element _(both styles and variables)_
176
+ */
177
+ export type CSSValues = CSSVariables & CSSStyles;
178
+
179
+ /**
180
+ * CSS variables for an element
181
+ */
182
+ export type CSSVariables<Value extends Record<string, unknown> = Record<string, unknown>> = {
183
+ [Property in keyof Value as `--${string & Property}`]?: unknown;
184
+ };
185
+
131
186
  /**
132
187
  * Event listener for custom events
133
188
  */