anu-verzum 2.1.0 → 2.2.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/README.md +2 -1
- package/dist/core/components/Intl.d.ts +8 -0
- package/dist/core/components/Intl.js +75 -1
- package/dist/core/domUtils.js +15 -1
- package/dist/index.d.ts +3 -1
- package/dist/testing/events/fireEvent.d.ts +15 -13
- package/dist/testing/events/fireEvent.js +8 -1
- package/dist/testing/queries/byLabelText.js +6 -2
- package/dist/testing/queries/byRole.js +20 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
<h3>@author: <strong>Anubis-programmer</strong></h3>
|
|
6
6
|
<h3>@license: <strong>MIT</strong></h3>
|
|
7
|
-
<h3>@version: <strong>2.1.0</strong></h3>
|
|
8
7
|
|
|
9
8
|
<br>
|
|
10
9
|
|
|
@@ -196,6 +195,8 @@ The following types are exported from `anu-verzum` for use in consumer projects:
|
|
|
196
195
|
| `CreateSelectorFn` | Overloaded interface for `Anu.store.createSelector` — enables full type inference on transformation parameters |
|
|
197
196
|
| `ApiSuccessResponse<T>` | Successful HTTP response `{ status: number; response: T \| null }` |
|
|
198
197
|
| `ApiErrorResponse` | Error HTTP response `{ status: number; response: null }` |
|
|
198
|
+
| `FormatNumberOptions` | Options for `Anu.Intl.formatNumber` — `Intl.NumberFormatOptions` plus an optional `locale` |
|
|
199
|
+
| `ParseNumberOptions` | Options for `Anu.Intl.parseNumber` — `{ locale?: string }` |
|
|
199
200
|
|
|
200
201
|
#### Library development scripts
|
|
201
202
|
|
|
@@ -14,8 +14,16 @@ export interface AbbreviateNumberOptions {
|
|
|
14
14
|
decimalPlaces?: number;
|
|
15
15
|
decimalSign?: string;
|
|
16
16
|
}
|
|
17
|
+
export interface FormatNumberOptions extends Intl.NumberFormatOptions {
|
|
18
|
+
locale?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ParseNumberOptions {
|
|
21
|
+
locale?: string;
|
|
22
|
+
}
|
|
17
23
|
declare const Intl: {
|
|
18
24
|
abbreviateNumber: (value: number, options?: AbbreviateNumberOptions) => string | number;
|
|
25
|
+
formatNumber: (value: number, options?: FormatNumberOptions) => string;
|
|
26
|
+
parseNumber: (text: string, options?: ParseNumberOptions) => number | null;
|
|
19
27
|
FormattedMessage: ({ id, values, defaultMessage }: FormattedMessageProps) => AnuElement;
|
|
20
28
|
formatMessage: (id: string, values?: Record<string, string | number>, defaultMessage?: string) => string;
|
|
21
29
|
Provider: ({ locale, messages, defaultLocale, children }: IntlProviderProps) => AnuElement | null;
|
|
@@ -11,6 +11,11 @@ let __messagesContext = {
|
|
|
11
11
|
locale: undefined,
|
|
12
12
|
messages: {}
|
|
13
13
|
};
|
|
14
|
+
|
|
15
|
+
// `Intl.NumberFormatOptions` here refers to the global ECMAScript Intl namespace
|
|
16
|
+
// (type space), not the local `Intl` API object declared at the bottom of this
|
|
17
|
+
// file — a `const` introduces only a value binding, so the type reference is safe.
|
|
18
|
+
|
|
14
19
|
const IntlProvider = ({
|
|
15
20
|
locale,
|
|
16
21
|
messages,
|
|
@@ -113,8 +118,15 @@ const formatMessage = (id, values, defaultMessage) => {
|
|
|
113
118
|
}
|
|
114
119
|
return textValue;
|
|
115
120
|
};
|
|
121
|
+
|
|
122
|
+
// Reduce any BCP 47 tag to its lowercase language subtag, e.g. 'hu-HU' / 'HU' → 'hu'.
|
|
123
|
+
// Used for the exact dictionary-key lookups below (UNITS / DECIMAL_SIGN), which key
|
|
124
|
+
// on the language only — so a Provider locale of 'hu', 'HU', or 'hu-HU' all resolve
|
|
125
|
+
// to the same Hungarian entry. (formatNumber/parseNumber keep the full tag, since the
|
|
126
|
+
// region subtag is meaningful to Intl.NumberFormat — e.g. en-US vs en-GB vs en-IN.)
|
|
127
|
+
const getLanguageSubtag = locale => locale ? locale.toLowerCase().split('-')[0] : undefined;
|
|
116
128
|
const abbreviateNumber = (value, options = {}) => {
|
|
117
|
-
const getByLocale = values => values[__messagesContext.locale || 'default'] || values['default'];
|
|
129
|
+
const getByLocale = values => values[getLanguageSubtag(__messagesContext.locale) || 'default'] || values['default'];
|
|
118
130
|
const UNITS = {
|
|
119
131
|
hu: ['E', 'm', 'M', 'b'],
|
|
120
132
|
default: ['K', 'M', 'B', 'T']
|
|
@@ -162,8 +174,70 @@ const abbreviateNumber = (value, options = {}) => {
|
|
|
162
174
|
}
|
|
163
175
|
return value;
|
|
164
176
|
};
|
|
177
|
+
|
|
178
|
+
// The local `Intl` object below shadows the global ECMAScript `Intl`, so number
|
|
179
|
+
// formatting/parsing must reach the real `Intl.NumberFormat` via `globalThis.Intl`.
|
|
180
|
+
const resolveLocale = locale => locale || __messagesContext.locale || undefined;
|
|
181
|
+
const formatNumber = (value, options = {}) => {
|
|
182
|
+
const {
|
|
183
|
+
locale,
|
|
184
|
+
...numberFormatOptions
|
|
185
|
+
} = options;
|
|
186
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
187
|
+
return String(value);
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
return new globalThis.Intl.NumberFormat(resolveLocale(locale), numberFormatOptions).format(value);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error(err);
|
|
193
|
+
return String(value);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const parseNumber = (text, options = {}) => {
|
|
197
|
+
if (typeof text !== 'string') {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
// Discover the locale's grouping/decimal/minus marks from a known sample,
|
|
202
|
+
// then normalize the input back into a plain JS-parseable number string.
|
|
203
|
+
const parts = new globalThis.Intl.NumberFormat(resolveLocale(options.locale)).formatToParts(-12345.6);
|
|
204
|
+
const groupSign = parts.find(p => p.type === 'group')?.value ?? '';
|
|
205
|
+
const decimalSign = parts.find(p => p.type === 'decimal')?.value ?? '.';
|
|
206
|
+
const minusSign = parts.find(p => p.type === 'minusSign')?.value ?? '-';
|
|
207
|
+
let normalized = text.trim();
|
|
208
|
+
const isNegative = normalized.indexOf('-') > -1 || !!minusSign && normalized.indexOf(minusSign) > -1;
|
|
209
|
+
|
|
210
|
+
// Drop grouping separators: the explicit locale group sign plus any
|
|
211
|
+
// whitespace (JS `\s` covers NBSP/NNBSP, which several locales group with).
|
|
212
|
+
if (groupSign) {
|
|
213
|
+
normalized = normalized.split(groupSign).join('');
|
|
214
|
+
}
|
|
215
|
+
normalized = normalized.replace(/\s/g, '');
|
|
216
|
+
|
|
217
|
+
// Convert the locale decimal sign to a dot.
|
|
218
|
+
if (decimalSign && decimalSign !== '.') {
|
|
219
|
+
normalized = normalized.split(decimalSign).join('.');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Keep only digits and the decimal dot; sign is reapplied from isNegative.
|
|
223
|
+
normalized = normalized.replace(/[^0-9.]/g, '');
|
|
224
|
+
if (normalized === '' || normalized === '.') {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
const result = Number(normalized);
|
|
228
|
+
if (isNaN(result)) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
return isNegative ? -result : result;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.error(err);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
165
237
|
const Intl = {
|
|
166
238
|
abbreviateNumber,
|
|
239
|
+
formatNumber,
|
|
240
|
+
parseNumber,
|
|
167
241
|
FormattedMessage,
|
|
168
242
|
formatMessage,
|
|
169
243
|
Provider: IntlProvider
|
package/dist/core/domUtils.js
CHANGED
|
@@ -5,6 +5,10 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.updateDomProperties = exports.getHTMLValidSvgTag = exports.createDomElement = exports.SVG_ELEMENT_LIST = void 0;
|
|
7
7
|
var _elements = require("./elements");
|
|
8
|
+
const HTML_ATTRIBUTE_NAME_MAP = {
|
|
9
|
+
acceptCharset: 'accept-charset',
|
|
10
|
+
httpEquiv: 'http-equiv'
|
|
11
|
+
};
|
|
8
12
|
const getHTMLValidSvgTag = fiberType => {
|
|
9
13
|
switch (fiberType) {
|
|
10
14
|
case 'anchor':
|
|
@@ -46,8 +50,18 @@ const updateDomProperties = (dom, prevProps, nextProps, isSvgElement = false) =>
|
|
|
46
50
|
dom.setAttribute(name, nextProps[name]);
|
|
47
51
|
}
|
|
48
52
|
} else {
|
|
49
|
-
if (name
|
|
53
|
+
if (name === 'className' || name === 'htmlFor') {
|
|
54
|
+
// Both have working IDL aliases (el.className / el.htmlFor).
|
|
55
|
+
el[name] = nextProps[name];
|
|
56
|
+
} else if (name.includes('-') || name === 'role') {
|
|
50
57
|
dom.setAttribute(name, nextProps[name]);
|
|
58
|
+
} else if (dom.nodeType === 1 && /[A-Z]/.test(name)) {
|
|
59
|
+
// camelCase HTML attribute (inputMode, autoComplete, …): the
|
|
60
|
+
// DOM name is the lowercased key, except for the hyphenated
|
|
61
|
+
// cases captured in HTML_ATTRIBUTE_NAME_MAP. Setting these as
|
|
62
|
+
// IDL properties is unreliable (e.g. el.autoComplete is not a
|
|
63
|
+
// reflected property), so route them through setAttribute.
|
|
64
|
+
dom.setAttribute(HTML_ATTRIBUTE_NAME_MAP[name] ?? name.toLowerCase(), nextProps[name]);
|
|
51
65
|
} else {
|
|
52
66
|
el[name] = nextProps[name];
|
|
53
67
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -102,6 +102,8 @@ declare const Anu: {
|
|
|
102
102
|
};
|
|
103
103
|
Intl: {
|
|
104
104
|
abbreviateNumber: (value: number, options?: import(".").AbbreviateNumberOptions) => string | number;
|
|
105
|
+
formatNumber: (value: number, options?: import(".").FormatNumberOptions) => string;
|
|
106
|
+
parseNumber: (text: string, options?: import(".").ParseNumberOptions) => number | null;
|
|
105
107
|
FormattedMessage: ({ id, values, defaultMessage }: import("./core/components/Intl").FormattedMessageProps) => AnuElement;
|
|
106
108
|
formatMessage: (id: string, values?: Record<string, string | number>, defaultMessage?: string) => string;
|
|
107
109
|
Provider: ({ locale, messages, defaultLocale, children }: import("./core/components/Intl").IntlProviderProps) => AnuElement | null;
|
|
@@ -160,7 +162,7 @@ export type { ApiSuccessResponse, ApiErrorResponse } from './server-api/server-a
|
|
|
160
162
|
export { Component, Fragment, createElement, createRef, createPortal, createContext, render, goTo };
|
|
161
163
|
export { AnulyticsProvider, trackEvent };
|
|
162
164
|
export { store, ServerAPI, Utils, Connector, Feature, History, Intl };
|
|
163
|
-
export type { AbbreviateNumberOptions } from './core/components/Intl';
|
|
165
|
+
export type { AbbreviateNumberOptions, FormatNumberOptions, ParseNumberOptions } from './core/components/Intl';
|
|
164
166
|
declare global {
|
|
165
167
|
namespace JSX {
|
|
166
168
|
interface IntrinsicAttributes {
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
type EventInit = globalThis.EventInit &
|
|
1
|
+
type EventInit = globalThis.EventInit & {
|
|
2
|
+
target?: Record<string, any>;
|
|
3
|
+
} & Record<string, any>;
|
|
2
4
|
export declare const fireEvent: {
|
|
3
5
|
(element: Element, eventName: string, init?: EventInit): boolean;
|
|
4
|
-
click(el: Element, init?:
|
|
5
|
-
dblclick(el: Element, init?:
|
|
6
|
-
change(el: Element, init?:
|
|
7
|
-
input(el: Element, init?:
|
|
8
|
-
focus(el: Element, init?:
|
|
9
|
-
blur(el: Element, init?:
|
|
10
|
-
keyDown(el: Element, init?:
|
|
11
|
-
keyUp(el: Element, init?:
|
|
12
|
-
keyPress(el: Element, init?:
|
|
13
|
-
submit(el: Element, init?:
|
|
14
|
-
mouseDown(el: Element, init?:
|
|
15
|
-
mouseUp(el: Element, init?:
|
|
6
|
+
click(el: Element, init?: EventInit): boolean;
|
|
7
|
+
dblclick(el: Element, init?: EventInit): boolean;
|
|
8
|
+
change(el: Element, init?: EventInit): boolean;
|
|
9
|
+
input(el: Element, init?: EventInit): boolean;
|
|
10
|
+
focus(el: Element, init?: EventInit): boolean;
|
|
11
|
+
blur(el: Element, init?: EventInit): boolean;
|
|
12
|
+
keyDown(el: Element, init?: EventInit): boolean;
|
|
13
|
+
keyUp(el: Element, init?: EventInit): boolean;
|
|
14
|
+
keyPress(el: Element, init?: EventInit): boolean;
|
|
15
|
+
submit(el: Element, init?: EventInit): boolean;
|
|
16
|
+
mouseDown(el: Element, init?: EventInit): boolean;
|
|
17
|
+
mouseUp(el: Element, init?: EventInit): boolean;
|
|
16
18
|
};
|
|
17
19
|
export {};
|
|
@@ -26,7 +26,14 @@ const buildEvent = (eventName, init) => {
|
|
|
26
26
|
return new Event(eventName, opts);
|
|
27
27
|
};
|
|
28
28
|
const fireEvent = (element, eventName, init) => {
|
|
29
|
-
const
|
|
29
|
+
const {
|
|
30
|
+
target,
|
|
31
|
+
...eventInit
|
|
32
|
+
} = init ?? {};
|
|
33
|
+
if (target) {
|
|
34
|
+
Object.assign(element, target);
|
|
35
|
+
}
|
|
36
|
+
const dispatched = element.dispatchEvent(buildEvent(eventName, eventInit));
|
|
30
37
|
(0, _act.flushEffects)();
|
|
31
38
|
return dispatched;
|
|
32
39
|
};
|
|
@@ -15,8 +15,12 @@ const queryAllByLabelText = (container, label) => {
|
|
|
15
15
|
}
|
|
16
16
|
const forAttr = labelEl.getAttribute('for');
|
|
17
17
|
if (forAttr) {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// The `for` attribute is already an element id, so resolve it with
|
|
19
|
+
// getElementById — no CSS.escape needed (jsdom omits the browser-only
|
|
20
|
+
// `CSS` global), and it handles any id characters. Scope to container.
|
|
21
|
+
const doc = container.ownerDocument ?? document;
|
|
22
|
+
const input = doc.getElementById(forAttr);
|
|
23
|
+
if (input && container.contains(input)) {
|
|
20
24
|
results.push(input);
|
|
21
25
|
}
|
|
22
26
|
return;
|
|
@@ -9,9 +9,6 @@ const IMPLICIT_ROLES = {
|
|
|
9
9
|
button: ['button'],
|
|
10
10
|
link: ['a'],
|
|
11
11
|
heading: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
|
|
12
|
-
textbox: ['input'],
|
|
13
|
-
checkbox: ['input[type="checkbox"]'],
|
|
14
|
-
radio: ['input[type="radio"]'],
|
|
15
12
|
combobox: ['select'],
|
|
16
13
|
img: ['img'],
|
|
17
14
|
list: ['ul', 'ol'],
|
|
@@ -22,6 +19,21 @@ const IMPLICIT_ROLES = {
|
|
|
22
19
|
banner: ['header'],
|
|
23
20
|
contentinfo: ['footer']
|
|
24
21
|
};
|
|
22
|
+
const INPUT_TYPE_ROLES = {
|
|
23
|
+
text: 'textbox',
|
|
24
|
+
search: 'textbox',
|
|
25
|
+
email: 'textbox',
|
|
26
|
+
tel: 'textbox',
|
|
27
|
+
url: 'textbox',
|
|
28
|
+
number: 'spinbutton',
|
|
29
|
+
range: 'slider',
|
|
30
|
+
checkbox: 'checkbox',
|
|
31
|
+
radio: 'radio',
|
|
32
|
+
button: 'button',
|
|
33
|
+
submit: 'button',
|
|
34
|
+
reset: 'button',
|
|
35
|
+
image: 'button'
|
|
36
|
+
};
|
|
25
37
|
const getAccessibleName = el => {
|
|
26
38
|
const ariaLabel = el.getAttribute('aria-label');
|
|
27
39
|
if (ariaLabel) {
|
|
@@ -44,17 +56,15 @@ const hasRole = (el, role) => {
|
|
|
44
56
|
if (el.getAttribute('role') === role) {
|
|
45
57
|
return true;
|
|
46
58
|
}
|
|
59
|
+
if (el.tagName.toLowerCase() === 'input') {
|
|
60
|
+
const type = (el.getAttribute('type') ?? 'text').toLowerCase();
|
|
61
|
+
return INPUT_TYPE_ROLES[type] === role;
|
|
62
|
+
}
|
|
47
63
|
const selectors = IMPLICIT_ROLES[role];
|
|
48
64
|
if (!selectors) {
|
|
49
65
|
return false;
|
|
50
66
|
}
|
|
51
|
-
return selectors.some(sel =>
|
|
52
|
-
if (sel === 'input' && role === 'textbox') {
|
|
53
|
-
const type = el.getAttribute('type') ?? 'text';
|
|
54
|
-
return el.tagName.toLowerCase() === 'input' && ['text', 'search', 'email', 'tel', 'url'].indexOf(type) >= 0;
|
|
55
|
-
}
|
|
56
|
-
return el.matches(sel);
|
|
57
|
-
});
|
|
67
|
+
return selectors.some(sel => el.matches(sel));
|
|
58
68
|
};
|
|
59
69
|
const queryAllByRole = (container, role, options) => {
|
|
60
70
|
const results = [];
|
package/package.json
CHANGED