be-intl 0.0.50 → 0.0.52

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
@@ -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:
@@ -69,6 +97,66 @@ The semantic example above involves a lot of keyboard tapping of the letters "be
69
97
  🌐-weekday=long 🌐-year=numeric 🌐-month=long 🌐-day=numeric></time>
70
98
  ```
71
99
 
100
+ ## Programmatic attachment (no attribute)
101
+
102
+ The attribute syntax shown above shines for server-rendered HTML and progressive enhancement: the markup alone says how each value should be formatted. But most web development today renders on the client, with a framework (Lit, React, Vue, Svelte, etc.) that already has a JavaScript reference to each element it creates. In that setting, attaching be-intl programmatically is the better fit:
103
+
104
+ 1. **A less clunky API.** Frameworks tend to be awkward about setting arbitrary (let alone emoji) attributes, and quoting JSON inside an attribute, like `be-intl='{ "style": "currency", "currency": "EUR" }'`, is error prone. Setting `format` to `{style: 'currency', currency: 'EUR'}` is ordinary JavaScript, which TypeScript can check against `Intl.NumberFormatOptions` / `Intl.DateTimeFormatOptions`. You can also set `locale` directly, which has no attribute equivalent.
105
+ 2. **Less stringifying and parsing.** With an attribute, the framework serializes the format options to JSON, and be-intl then parses that JSON back into an object. Setting `format` directly skips both steps.
106
+ 3. **Less overhead monitoring attributes.** The attribute approach relies on [be-hive](https://github.com/bahrus/be-hive) / [mount-observer](https://github.com/bahrus/mount-observer) watching the DOM for elements that carry (or gain) the attribute, and for changes to its value. The programmatic approach needs none of that -- `def.js` just registers the enhancement's config, and the enhancement is attached exactly when, and to exactly the elements, your code says.
107
+
108
+ Both approaches produce the same enhancement, with the same locale resolution and formatting rules, so you can mix them in one app -- attributes for server-rendered islands, programmatic attachment inside client-rendered components.
109
+
110
+ First register the enhancement's config once:
111
+
112
+ ```JS
113
+ import { defBeIntl } from 'be-intl/def.js';
114
+ const emc = await defBeIntl(document.body); // or a shadow root's host, for a scoped registry
115
+ ```
116
+
117
+ Then set any of these properties:
118
+
119
+ | Attribute | Property | Notes |
120
+ |---------------------------------------------|-----------------------------------|----------------------------------------------------------------------------------------|
121
+ | `be-intl` / `🌐` | `format` | An `Intl.NumberFormatOptions` / `Intl.DateTimeFormatOptions` object. A bare attribute is `{}`. |
122
+ | `be-intl-style`, `-currency`, `-weekday`, `-year`, `-month`, `-day` | `style`, `currency`, `weekday`, `year`, `month`, `day` | Folded into `format`; keys set explicitly in `format` win. |
123
+ | `be-intl-observe-lang` | `observeLang` | `true` / `false`. |
124
+ | `be-intl-announce` | `announce` | `true` / `false`. |
125
+ | *(none -- `lang`)* | `locale` | A BCP-47 tag. Overrides the locale resolved from `lang`. |
126
+
127
+ ### Declarative -- via `enh.set`
128
+
129
+ ```JS
130
+ // equivalent to <data value=123456.789 lang="de-DE" be-intl='{ "style": "currency", "currency": "EUR" }'>
131
+ data.enh.set.beIntl.format = {style: 'currency', currency: 'EUR'};
132
+ ```
133
+
134
+ Only the first property needs `.set` -- it's what triggers the attachment. This can be done before or after `defBeIntl` has been called.
135
+
136
+ ### Imperative -- via `enh.get()`
137
+
138
+ ```JS
139
+ Object.assign(data.enh.get(emc), {
140
+ locale: 'de-DE',
141
+ style: 'currency',
142
+ currency: 'USD',
143
+ });
144
+ ```
145
+
146
+ `data.enh.get(emc)` with no properties at all is equivalent to a bare `be-intl` attribute.
147
+
148
+ ### Changing the formatting later
149
+
150
+ After the first render, setting `locale`, `format` or one of the semantic properties re-formats the element:
151
+
152
+ ```JS
153
+ data.enh.beIntl.currency = 'EUR';
154
+ ```
155
+
156
+ `format` is compared by reference, so assign a new object rather than mutating the existing one.
157
+
158
+ See [demo/Programmatic](demo/Programmatic/) for runnable examples.
159
+
72
160
  ## Viewing Demos Locally
73
161
 
74
162
  1. Install git
package/be-intl.js CHANGED
@@ -32,7 +32,9 @@ class BeIntl {
32
32
  * @param {PAP} initVals
33
33
  */
34
34
  async init(self, enhancedElement, ctx, initVals){
35
- const {customData} = /** @type {EMC<any, AllProps, Element, RAConfig<AllProps, Actions>>} */ (ctx.emc);
35
+ // ctx.emc is only populated on the attribute (mount-observer) path;
36
+ // enh.get() / enh.set only pass the registry item -- see def.js.
37
+ const {customData} = /** @type {EMC<any, AllProps, Element, RAConfig<AllProps, Actions>>} */ (ctx.emc || ctx.config);
36
38
  /**
37
39
  * @type {RoundaboutOptions}
38
40
  */
@@ -56,9 +58,6 @@ class BeIntl {
56
58
  */
57
59
  async hydrate(self){
58
60
  const {enhancedElement} = self;
59
- if(enhancedElement instanceof HTMLElement){
60
- enhancedElement.ariaLive = 'polite';
61
- }
62
61
 
63
62
  // Re-format whenever the element's underlying value property changes.
64
63
  // <data>/<output> reflect through `value`, <time> through `dateTime`.
@@ -111,6 +110,7 @@ class BeIntl {
111
110
 
112
111
  /**
113
112
  * @param {AP} self
113
+ * @returns {PAP | void}
114
114
  */
115
115
  formatNumber(self){
116
116
  const {enhancedElement, value, intlNumberFormat} = self;
@@ -119,15 +119,37 @@ class BeIntl {
119
119
  return;
120
120
  }
121
121
  enhancedElement.textContent = intlNumberFormat.format(/** @type {number} */ (value));
122
+ if(!self.rendered) return {rendered: true};
122
123
  }
123
124
 
124
125
  /**
125
126
  * @param {AP} self
127
+ * @returns {PAP | void}
126
128
  */
127
129
  formatDate(self){
128
130
  const {enhancedElement, value, intlDateFormat} = self;
129
131
  if(intlDateFormat === undefined || value === undefined || value === null) return;
130
132
  enhancedElement.textContent = intlDateFormat.format(/** @type {Date} */ (value));
133
+ if(!self.rendered) return {rendered: true};
134
+ }
135
+
136
+ /**
137
+ * Opt-in via `be-intl-announce`. Once the value has rendered at least once,
138
+ * mark the element as a polite ARIA live region so subsequent re-formats
139
+ * (value or locale changes) are announced by assistive tech. Deliberately
140
+ * deferred past the first render so the initial value isn't spoken on load,
141
+ * and `aria-live` is skipped for `<output>` (already an implicit polite live
142
+ * region). `aria-atomic` keeps multi-token output (e.g. a formatted date)
143
+ * announcing as one unit.
144
+ * @param {AP} self
145
+ */
146
+ armLiveRegion(self){
147
+ const {enhancedElement} = self;
148
+ if(!(enhancedElement instanceof HTMLElement)) return;
149
+ enhancedElement.setAttribute('aria-atomic', 'true');
150
+ if(enhancedElement.localName !== 'output'){
151
+ enhancedElement.setAttribute('aria-live', 'polite');
152
+ }
131
153
  }
132
154
  }
133
155
 
@@ -141,7 +163,7 @@ class BeIntl {
141
163
  */
142
164
  async function infer(from){
143
165
  return /** @type {any} */ (
144
- from.enh.get((await import('inferencer/inferencer.js')).registryItem)
166
+ from.enh.get((await import('assign-gingerly/inferencer/inferencer.js')).registryItem)
145
167
  );
146
168
  }
147
169
 
package/def.js ADDED
@@ -0,0 +1,21 @@
1
+ import 'assign-gingerly/object-extension.js';
2
+
3
+ /**
4
+ * Registers be-intl's config, so it can be attached programmatically
5
+ * via el.enh.set.beIntl / el.enh.get(emc).
6
+ * @param {Element | undefined} ref element whose customElementRegistry to register against
7
+ */
8
+ export async function defBeIntl(ref){
9
+ const {default: emc} = await import('./emc.json', {with: {type: 'json'}});
10
+ return await push(ref, emc);
11
+ }
12
+
13
+ async function push(ref, emc){
14
+ const {BeIntl} = await import('./be-intl.js');
15
+ const {enhConfig} = emc;
16
+ enhConfig.spawn = BeIntl;
17
+ enhConfig.customData = emc.customData; // the registry only stores enhConfig, not the full emc
18
+ const registry = (ref?.customElementRegistry ?? customElements).enhancementRegistry;
19
+ registry.push(enhConfig);
20
+ return enhConfig;
21
+ }
package/emc.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "enhConfig": {
3
- "enhKey": "BeIntl",
3
+ "enhKey": "beIntl",
4
4
  "spawn": "be-intl/be-intl.js",
5
5
  "withAttrs": {
6
6
  "base": "be-intl",
@@ -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
  },
@@ -45,6 +49,12 @@
45
49
  "ifKeyIn": [
46
50
  "locale",
47
51
  "format",
52
+ "style",
53
+ "currency",
54
+ "weekday",
55
+ "year",
56
+ "month",
57
+ "day",
48
58
  "initialized"
49
59
  ]
50
60
  },
@@ -65,6 +75,15 @@
65
75
  "value",
66
76
  "intlDateFormat"
67
77
  ]
78
+ },
79
+ "armLiveRegion": {
80
+ "ifAllOf": [
81
+ "announce",
82
+ "rendered"
83
+ ],
84
+ "ifKeyIn": [
85
+ "rendered"
86
+ ]
68
87
  }
69
88
  }
70
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "be-intl",
3
- "version": "0.0.50",
3
+ "version": "0.0.52",
4
4
  "keywords": [
5
5
  "web-components",
6
6
  "web-component",
@@ -14,6 +14,7 @@
14
14
  ".": "./be-intl.js",
15
15
  "./be-intl.js": "./be-intl.js",
16
16
  "./🌐.js": "./🌐.js",
17
+ "./def.js": "./def.js",
17
18
  "./emc.json": "./emc.json",
18
19
  "./🌐.json": "./🌐.json"
19
20
  },
@@ -31,15 +32,14 @@
31
32
  "update": "ncu -u && npm install"
32
33
  },
33
34
  "dependencies": {
34
- "assign-gingerly": "0.0.93",
35
+ "assign-gingerly": "0.0.97",
35
36
  "be-hive": "0.1.18",
36
- "inferencer": "0.0.13",
37
37
  "mount-observer": "0.1.53",
38
- "roundabout-lib": "0.0.36"
38
+ "roundabout-lib": "0.0.38"
39
39
  },
40
40
  "devDependencies": {
41
- "spa-ssi": "0.0.27",
42
- "@playwright/test": "1.62.1"
41
+ "spa-ssi": "0.0.28",
42
+ "@playwright/test": "1.63.0"
43
43
  },
44
44
  "author": "anderson.bruce.b@gmail.com",
45
45
  "license": "MIT"
@@ -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
  },
@@ -45,6 +49,12 @@
45
49
  "ifKeyIn": [
46
50
  "locale",
47
51
  "format",
52
+ "style",
53
+ "currency",
54
+ "weekday",
55
+ "year",
56
+ "month",
57
+ "day",
48
58
  "initialized"
49
59
  ]
50
60
  },
@@ -65,6 +75,15 @@
65
75
  "value",
66
76
  "intlDateFormat"
67
77
  ]
78
+ },
79
+ "armLiveRegion": {
80
+ "ifAllOf": [
81
+ "announce",
82
+ "rendered"
83
+ ],
84
+ "ifKeyIn": [
85
+ "rendered"
86
+ ]
68
87
  }
69
88
  }
70
89
  }