be-intl 0.0.50 → 0.0.51
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 +28 -0
- package/be-intl.js +168 -148
- package/emc.json +13 -0
- package/package.json +2 -3
- package//360/237/214/220.json +13 -0
package/README.md
CHANGED
|
@@ -60,6 +60,34 @@ Changing the formatted element's **own** `lang` after it has been enhanced re-fo
|
|
|
60
60
|
if you opt in with `be-intl-observe-lang` (`🌐-observe-lang`); changes to an ancestor's `lang`
|
|
61
61
|
after enhancement are not tracked.
|
|
62
62
|
|
|
63
|
+
## Announcing updates to assistive technology
|
|
64
|
+
|
|
65
|
+
`be-intl` writes the formatted string into `textContent` on first render and again whenever the
|
|
66
|
+
bound value or the effective locale changes. By default it leaves ARIA untouched — most
|
|
67
|
+
formatted `<data>` / `<time>` elements are static readouts, and turning every one into a live
|
|
68
|
+
region (especially for a locale switch that only changes *presentation*, not the value) is
|
|
69
|
+
usually just screen-reader noise.
|
|
70
|
+
|
|
71
|
+
When a particular value *is* something the user is watching change, opt that element in with
|
|
72
|
+
`be-intl-announce` (`🌐-announce`):
|
|
73
|
+
|
|
74
|
+
```html
|
|
75
|
+
<output be-intl be-intl-announce></output>
|
|
76
|
+
<data be-intl be-intl-announce value="0" id="unread"></data>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
With the attribute set, `be-intl` — **after** the element's first render, so the initial value
|
|
80
|
+
isn't spoken on load — marks it as a polite live region:
|
|
81
|
+
|
|
82
|
+
- `aria-live="polite"` (skipped for `<output>`, which is already an implicit polite live
|
|
83
|
+
region), and
|
|
84
|
+
- `aria-atomic="true"`, so multi-token output such as a formatted date is announced as one
|
|
85
|
+
phrase rather than word-by-word.
|
|
86
|
+
|
|
87
|
+
Later re-formats then mutate an already-registered region and are announced. `assertive` is
|
|
88
|
+
intentionally not offered — interrupting the user to read out a reformatted number is almost
|
|
89
|
+
never the right call.
|
|
90
|
+
|
|
63
91
|
## Alternative names
|
|
64
92
|
|
|
65
93
|
The semantic example above involves a lot of keyboard tapping of the letters "be-intl". To avoid blisters on your itty bitty fingers, we provide an alternative base attribute you can use:
|
package/be-intl.js
CHANGED
|
@@ -1,148 +1,168 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
/** @import {Actions, PAP, AllProps, AP} from './types/be-intl/types' */;
|
|
3
|
-
/** @import {RoundaboutOptions} from './types/roundabout/types' */;
|
|
4
|
-
/** @import {ElementEnhancementGateway, SpawnContext} from './types/assign-gingerly/types' */;
|
|
5
|
-
/** @import {EMC} from './types/mount-observer/types' */;
|
|
6
|
-
/** @import {RAConfig} from './types/roundabout/types' */;
|
|
7
|
-
|
|
8
|
-
const defaultLocale = new Intl.NumberFormat().resolvedOptions().locale;
|
|
9
|
-
|
|
10
|
-
/** Semantic sugar attributes folded into `format`. */
|
|
11
|
-
const SEMANTIC_KEYS = ['style', 'currency', 'weekday', 'year', 'month', 'day'];
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @implements {Actions}
|
|
15
|
-
*/
|
|
16
|
-
class BeIntl {
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @this {AllProps & Actions}
|
|
20
|
-
* @param {Element & ElementEnhancementGateway} enhancedElement
|
|
21
|
-
* @param {SpawnContext} ctx
|
|
22
|
-
* @param {PAP} initVals
|
|
23
|
-
*/
|
|
24
|
-
constructor(enhancedElement, ctx, initVals){
|
|
25
|
-
this.init(this, enhancedElement, ctx, initVals);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {AllProps} self
|
|
30
|
-
* @param {Element & ElementEnhancementGateway} enhancedElement
|
|
31
|
-
* @param {SpawnContext} ctx
|
|
32
|
-
* @param {PAP} initVals
|
|
33
|
-
*/
|
|
34
|
-
async init(self, enhancedElement, ctx, initVals){
|
|
35
|
-
const {customData} = /** @type {EMC<any, AllProps, Element, RAConfig<AllProps, Actions>>} */ (ctx.emc);
|
|
36
|
-
/**
|
|
37
|
-
* @type {RoundaboutOptions}
|
|
38
|
-
*/
|
|
39
|
-
const raOptions = {
|
|
40
|
-
...customData,
|
|
41
|
-
vm: self,
|
|
42
|
-
initialPropVals: {
|
|
43
|
-
enhancedElement,
|
|
44
|
-
...customData?.defaultPropVals,
|
|
45
|
-
...initVals
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
await (await import('roundabout-lib/roundabout.js')).roundabout(raOptions);
|
|
49
|
-
self.initialized = true;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Wire up live-update observation, resolve the locale, and seed the value.
|
|
54
|
-
* @param {AP} self
|
|
55
|
-
* @returns {Promise<PAP>}
|
|
56
|
-
*/
|
|
57
|
-
async hydrate(self){
|
|
58
|
-
const {enhancedElement} = self;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
* @
|
|
126
|
-
*/
|
|
127
|
-
formatDate(self){
|
|
128
|
-
const {enhancedElement, value, intlDateFormat} = self;
|
|
129
|
-
if(intlDateFormat === undefined || value === undefined || value === null) return;
|
|
130
|
-
enhancedElement.textContent = intlDateFormat.format(/** @type {Date} */ (value));
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import {Actions, PAP, AllProps, AP} from './types/be-intl/types' */;
|
|
3
|
+
/** @import {RoundaboutOptions} from './types/roundabout/types' */;
|
|
4
|
+
/** @import {ElementEnhancementGateway, SpawnContext} from './types/assign-gingerly/types' */;
|
|
5
|
+
/** @import {EMC} from './types/mount-observer/types' */;
|
|
6
|
+
/** @import {RAConfig} from './types/roundabout/types' */;
|
|
7
|
+
|
|
8
|
+
const defaultLocale = new Intl.NumberFormat().resolvedOptions().locale;
|
|
9
|
+
|
|
10
|
+
/** Semantic sugar attributes folded into `format`. */
|
|
11
|
+
const SEMANTIC_KEYS = ['style', 'currency', 'weekday', 'year', 'month', 'day'];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @implements {Actions}
|
|
15
|
+
*/
|
|
16
|
+
class BeIntl {
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @this {AllProps & Actions}
|
|
20
|
+
* @param {Element & ElementEnhancementGateway} enhancedElement
|
|
21
|
+
* @param {SpawnContext} ctx
|
|
22
|
+
* @param {PAP} initVals
|
|
23
|
+
*/
|
|
24
|
+
constructor(enhancedElement, ctx, initVals){
|
|
25
|
+
this.init(this, enhancedElement, ctx, initVals);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {AllProps} self
|
|
30
|
+
* @param {Element & ElementEnhancementGateway} enhancedElement
|
|
31
|
+
* @param {SpawnContext} ctx
|
|
32
|
+
* @param {PAP} initVals
|
|
33
|
+
*/
|
|
34
|
+
async init(self, enhancedElement, ctx, initVals){
|
|
35
|
+
const {customData} = /** @type {EMC<any, AllProps, Element, RAConfig<AllProps, Actions>>} */ (ctx.emc);
|
|
36
|
+
/**
|
|
37
|
+
* @type {RoundaboutOptions}
|
|
38
|
+
*/
|
|
39
|
+
const raOptions = {
|
|
40
|
+
...customData,
|
|
41
|
+
vm: self,
|
|
42
|
+
initialPropVals: {
|
|
43
|
+
enhancedElement,
|
|
44
|
+
...customData?.defaultPropVals,
|
|
45
|
+
...initVals
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
await (await import('roundabout-lib/roundabout.js')).roundabout(raOptions);
|
|
49
|
+
self.initialized = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Wire up live-update observation, resolve the locale, and seed the value.
|
|
54
|
+
* @param {AP} self
|
|
55
|
+
* @returns {Promise<PAP>}
|
|
56
|
+
*/
|
|
57
|
+
async hydrate(self){
|
|
58
|
+
const {enhancedElement} = self;
|
|
59
|
+
|
|
60
|
+
// Re-format whenever the element's underlying value property changes.
|
|
61
|
+
// <data>/<output> reflect through `value`, <time> through `dateTime`.
|
|
62
|
+
const valueProp = enhancedElement.localName === 'time' ? 'dateTime' : 'value';
|
|
63
|
+
const inference = await infer(enhancedElement);
|
|
64
|
+
const propagator = await inference.getPropagator();
|
|
65
|
+
propagator.addEventListener(valueProp, () => {
|
|
66
|
+
self.value = inference.value;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Opt-in: track lang changes on the element itself. Container-`lang`
|
|
70
|
+
// changes after mount aren't observed (rare); `inference.lang` still
|
|
71
|
+
// walks ancestors + shadow hosts on each read.
|
|
72
|
+
if(self.observeLang && enhancedElement instanceof HTMLElement){
|
|
73
|
+
const langObserver = new MutationObserver(() => {
|
|
74
|
+
self.locale = inference.lang || defaultLocale;
|
|
75
|
+
});
|
|
76
|
+
langObserver.observe(enhancedElement, {attributes: true, attributeFilter: ['lang']});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const locale = self.locale || inference.lang || defaultLocale;
|
|
80
|
+
return {locale, value: inference.value};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* (Re)build the Intl formatter. `value` is seeded by `hydrate` and kept
|
|
85
|
+
* fresh by the propagator, so it isn't read here.
|
|
86
|
+
* @param {AP} self
|
|
87
|
+
* @returns {PAP}
|
|
88
|
+
*/
|
|
89
|
+
onFormattingChange(self){
|
|
90
|
+
const {enhancedElement, locale} = self;
|
|
91
|
+
// Explicit JSON (`self.format`) wins; semantic sub-attributes fill the gaps.
|
|
92
|
+
const format = {.../** @type {Record<string, any>} */ (self.format || {})};
|
|
93
|
+
for(const key of SEMANTIC_KEYS){
|
|
94
|
+
const val = /** @type {Record<string, any>} */ (self)[key];
|
|
95
|
+
if(val !== undefined && format[key] === undefined) format[key] = val;
|
|
96
|
+
}
|
|
97
|
+
if(enhancedElement.localName === 'time'){
|
|
98
|
+
return {
|
|
99
|
+
intlDateFormat: new Intl.DateTimeFormat(locale, /** @type {Intl.DateTimeFormatOptions} */ (format)),
|
|
100
|
+
resolved: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
intlNumberFormat: new Intl.NumberFormat(locale, /** @type {Intl.NumberFormatOptions} */ (format)),
|
|
105
|
+
resolved: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {AP} self
|
|
111
|
+
* @returns {PAP | void}
|
|
112
|
+
*/
|
|
113
|
+
formatNumber(self){
|
|
114
|
+
const {enhancedElement, value, intlNumberFormat} = self;
|
|
115
|
+
if(intlNumberFormat === undefined || value === undefined || value === null){
|
|
116
|
+
enhancedElement.textContent = '';
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
enhancedElement.textContent = intlNumberFormat.format(/** @type {number} */ (value));
|
|
120
|
+
if(!self.rendered) return {rendered: true};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {AP} self
|
|
125
|
+
* @returns {PAP | void}
|
|
126
|
+
*/
|
|
127
|
+
formatDate(self){
|
|
128
|
+
const {enhancedElement, value, intlDateFormat} = self;
|
|
129
|
+
if(intlDateFormat === undefined || value === undefined || value === null) return;
|
|
130
|
+
enhancedElement.textContent = intlDateFormat.format(/** @type {Date} */ (value));
|
|
131
|
+
if(!self.rendered) return {rendered: true};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Opt-in via `be-intl-announce`. Once the value has rendered at least once,
|
|
136
|
+
* mark the element as a polite ARIA live region so subsequent re-formats
|
|
137
|
+
* (value or locale changes) are announced by assistive tech. Deliberately
|
|
138
|
+
* deferred past the first render so the initial value isn't spoken on load,
|
|
139
|
+
* and `aria-live` is skipped for `<output>` (already an implicit polite live
|
|
140
|
+
* region). `aria-atomic` keeps multi-token output (e.g. a formatted date)
|
|
141
|
+
* announcing as one unit.
|
|
142
|
+
* @param {AP} self
|
|
143
|
+
*/
|
|
144
|
+
armLiveRegion(self){
|
|
145
|
+
const {enhancedElement} = self;
|
|
146
|
+
if(!(enhancedElement instanceof HTMLElement)) return;
|
|
147
|
+
enhancedElement.setAttribute('aria-atomic', 'true');
|
|
148
|
+
if(enhancedElement.localName !== 'output'){
|
|
149
|
+
enhancedElement.setAttribute('aria-live', 'polite');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the `inferencer` enhancement instance for an element. Typed loosely
|
|
156
|
+
* (`Promise<any>`) so the class doesn't couple to the `Infer` shape; `.value`
|
|
157
|
+
* is a live, type-coerced read (`Date` for `<time>`, `number` for `<data>`),
|
|
158
|
+
* `getPropagator()` emits on value-property changes.
|
|
159
|
+
* @param {Element & ElementEnhancementGateway} from
|
|
160
|
+
* @returns {Promise<any>}
|
|
161
|
+
*/
|
|
162
|
+
async function infer(from){
|
|
163
|
+
return /** @type {any} */ (
|
|
164
|
+
from.enh.get((await import('assign-gingerly/inferencer/inferencer.js')).registryItem)
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export { BeIntl };
|
package/emc.json
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"observeLang": "${base}-observe-lang",
|
|
19
19
|
"_observeLang": {
|
|
20
20
|
"instanceOf": "Boolean"
|
|
21
|
+
},
|
|
22
|
+
"announce": "${base}-announce",
|
|
23
|
+
"_announce": {
|
|
24
|
+
"instanceOf": "Boolean"
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
27
|
},
|
|
@@ -65,6 +69,15 @@
|
|
|
65
69
|
"value",
|
|
66
70
|
"intlDateFormat"
|
|
67
71
|
]
|
|
72
|
+
},
|
|
73
|
+
"armLiveRegion": {
|
|
74
|
+
"ifAllOf": [
|
|
75
|
+
"announce",
|
|
76
|
+
"rendered"
|
|
77
|
+
],
|
|
78
|
+
"ifKeyIn": [
|
|
79
|
+
"rendered"
|
|
80
|
+
]
|
|
68
81
|
}
|
|
69
82
|
}
|
|
70
83
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "be-intl",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.51",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"web-components",
|
|
6
6
|
"web-component",
|
|
@@ -33,12 +33,11 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"assign-gingerly": "0.0.93",
|
|
35
35
|
"be-hive": "0.1.18",
|
|
36
|
-
"inferencer": "0.0.13",
|
|
37
36
|
"mount-observer": "0.1.53",
|
|
38
37
|
"roundabout-lib": "0.0.36"
|
|
39
38
|
},
|
|
40
39
|
"devDependencies": {
|
|
41
|
-
"spa-ssi": "0.0.
|
|
40
|
+
"spa-ssi": "0.0.28",
|
|
42
41
|
"@playwright/test": "1.62.1"
|
|
43
42
|
},
|
|
44
43
|
"author": "anderson.bruce.b@gmail.com",
|
package//360/237/214/220.json
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"observeLang": "${base}-observe-lang",
|
|
19
19
|
"_observeLang": {
|
|
20
20
|
"instanceOf": "Boolean"
|
|
21
|
+
},
|
|
22
|
+
"announce": "${base}-announce",
|
|
23
|
+
"_announce": {
|
|
24
|
+
"instanceOf": "Boolean"
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
27
|
},
|
|
@@ -65,6 +69,15 @@
|
|
|
65
69
|
"value",
|
|
66
70
|
"intlDateFormat"
|
|
67
71
|
]
|
|
72
|
+
},
|
|
73
|
+
"armLiveRegion": {
|
|
74
|
+
"ifAllOf": [
|
|
75
|
+
"announce",
|
|
76
|
+
"rendered"
|
|
77
|
+
],
|
|
78
|
+
"ifKeyIn": [
|
|
79
|
+
"rendered"
|
|
80
|
+
]
|
|
68
81
|
}
|
|
69
82
|
}
|
|
70
83
|
}
|