be-intl 0.0.49 → 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 CHANGED
@@ -36,6 +36,58 @@ We can also employ more semantic syntax:
36
36
  <data value=123456.789 lang="de-DE" be-intl-style=currency be-intl-currency=EUR></data>
37
37
  ```
38
38
 
39
+ ## Locale resolution
40
+
41
+ The examples above put `lang` right on the formatted element, but that isn't required.
42
+ `be-intl` uses the element's **effective language**, resolved the same way the browser's
43
+ `:lang()` selector works:
44
+
45
+ 1. the nearest ancestor with a `lang` (or `xml:lang`) attribute — crossing shadow-root
46
+ boundaries via the host element;
47
+ 2. otherwise `<html lang>`;
48
+ 3. otherwise the browser's own `navigator.language`.
49
+
50
+ So in practice you set `lang` once, high up:
51
+
52
+ ```html
53
+ <html lang="de-DE">
54
+ ...
55
+ <data value=123456.789 be-intl-style=currency be-intl-currency=EUR></data>
56
+ <!-- emits 123.456,79 € -->
57
+ ```
58
+
59
+ Changing the formatted element's **own** `lang` after it has been enhanced re-formats it only
60
+ if you opt in with `be-intl-observe-lang` (`🌐-observe-lang`); changes to an ancestor's `lang`
61
+ after enhancement are not tracked.
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
+
39
91
  ## Alternative names
40
92
 
41
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
- if(enhancedElement instanceof HTMLElement){
60
- enhancedElement.ariaLive = 'polite';
61
- }
62
-
63
- // Re-format whenever the element's underlying value property changes.
64
- // <data>/<output> reflect through `value`, <time> through `dateTime`.
65
- const valueProp = enhancedElement.localName === 'time' ? 'dateTime' : 'value';
66
- const inference = await infer(enhancedElement);
67
- const propagator = await inference.getPropagator();
68
- propagator.addEventListener(valueProp, () => {
69
- self.value = inference.value;
70
- });
71
-
72
- // Opt-in: track lang changes.
73
- if(self.observeLang && enhancedElement instanceof HTMLElement){
74
- const langObserver = new MutationObserver(() => {
75
- self.locale = enhancedElement.lang || defaultLocale;
76
- });
77
- langObserver.observe(enhancedElement, {attributes: true, attributeFilter: ['lang']});
78
- }
79
-
80
- const locale = self.locale
81
- || (enhancedElement instanceof HTMLElement ? enhancedElement.lang : '')
82
- || defaultLocale;
83
- return {locale, value: inference.value};
84
- }
85
-
86
- /**
87
- * (Re)build the Intl formatter. `value` is seeded by `hydrate` and kept
88
- * fresh by the propagator, so it isn't read here.
89
- * @param {AP} self
90
- * @returns {PAP}
91
- */
92
- onFormattingChange(self){
93
- const {enhancedElement, locale} = self;
94
- // Explicit JSON (`self.format`) wins; semantic sub-attributes fill the gaps.
95
- const format = {.../** @type {Record<string, any>} */ (self.format || {})};
96
- for(const key of SEMANTIC_KEYS){
97
- const val = /** @type {Record<string, any>} */ (self)[key];
98
- if(val !== undefined && format[key] === undefined) format[key] = val;
99
- }
100
- if(enhancedElement.localName === 'time'){
101
- return {
102
- intlDateFormat: new Intl.DateTimeFormat(locale, /** @type {Intl.DateTimeFormatOptions} */ (format)),
103
- resolved: true,
104
- };
105
- }
106
- return {
107
- intlNumberFormat: new Intl.NumberFormat(locale, /** @type {Intl.NumberFormatOptions} */ (format)),
108
- resolved: true,
109
- };
110
- }
111
-
112
- /**
113
- * @param {AP} self
114
- */
115
- formatNumber(self){
116
- const {enhancedElement, value, intlNumberFormat} = self;
117
- if(intlNumberFormat === undefined || value === undefined || value === null){
118
- enhancedElement.textContent = '';
119
- return;
120
- }
121
- enhancedElement.textContent = intlNumberFormat.format(/** @type {number} */ (value));
122
- }
123
-
124
- /**
125
- * @param {AP} self
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
- * Resolve the `inferencer` enhancement instance for an element. Typed loosely
136
- * (`Promise<any>`) so the class doesn't couple to the `Infer` shape; `.value`
137
- * is a live, type-coerced read (`Date` for `<time>`, `number` for `<data>`),
138
- * `getPropagator()` emits on value-property changes.
139
- * @param {Element & ElementEnhancementGateway} from
140
- * @returns {Promise<any>}
141
- */
142
- async function infer(from){
143
- return /** @type {any} */ (
144
- from.enh.get((await import('inferencer/inferencer.js')).registryItem)
145
- );
146
- }
147
-
148
- export { BeIntl };
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.49",
3
+ "version": "0.0.51",
4
4
  "keywords": [
5
5
  "web-components",
6
6
  "web-component",
@@ -31,14 +31,13 @@
31
31
  "update": "ncu -u && npm install"
32
32
  },
33
33
  "dependencies": {
34
- "assign-gingerly": "0.0.92",
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.27",
40
+ "spa-ssi": "0.0.28",
42
41
  "@playwright/test": "1.62.1"
43
42
  },
44
43
  "author": "anderson.bruce.b@gmail.com",
@@ -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
  }