@fusion.dev/helix-core 0.164.0 → 0.165.0

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.
@@ -21908,6 +21908,61 @@
21908
21908
  }
21909
21909
  ]
21910
21910
  },
21911
+ {
21912
+ "kind": "method",
21913
+ "name": "edit",
21914
+ "privacy": "private",
21915
+ "return": {
21916
+ "type": {
21917
+ "text": "boolean"
21918
+ }
21919
+ },
21920
+ "parameters": [
21921
+ {
21922
+ "name": "element",
21923
+ "type": {
21924
+ "text": "HTMLElement"
21925
+ }
21926
+ },
21927
+ {
21928
+ "name": "key",
21929
+ "type": {
21930
+ "text": "string"
21931
+ }
21932
+ }
21933
+ ],
21934
+ "description": "Applies an editing key to the segment, the arrows, Backspace, a digit or\na letter, and says whether it edited anything."
21935
+ },
21936
+ {
21937
+ "kind": "field",
21938
+ "name": "composing",
21939
+ "type": {
21940
+ "text": "[Node, string | null][]"
21941
+ },
21942
+ "privacy": "private",
21943
+ "default": "[]",
21944
+ "description": "What the segment's text was when a composition started to write into it."
21945
+ },
21946
+ {
21947
+ "kind": "field",
21948
+ "name": "handleSegmentBeforeInput",
21949
+ "privacy": "private",
21950
+ "readonly": true,
21951
+ "description": "Text a touch keyboard writes into a segment, which arrives as input and\nnot as a key: an on-screen keyboard sends `Unidentified` for its keys,\nso a digit tapped on a phone is read here, as React Aria's segments read\nit. Every input is canceled, so the segment holds only what the field\ndraws in it."
21952
+ },
21953
+ {
21954
+ "kind": "field",
21955
+ "name": "handleSegmentInput",
21956
+ "privacy": "private",
21957
+ "readonly": true
21958
+ },
21959
+ {
21960
+ "kind": "field",
21961
+ "name": "collapseSelection",
21962
+ "privacy": "private",
21963
+ "readonly": true,
21964
+ "description": "Keeps the caret at the start of the segment with focus, where Chromium\nfires input for a typed key, and where Android's keyboard does not\ncompose over the segment's text. A selection already there, or outside\nthe segment, is left alone."
21965
+ },
21911
21966
  {
21912
21967
  "kind": "field",
21913
21968
  "name": "handleSegmentKeydown",
@@ -28842,6 +28897,24 @@
28842
28897
  ],
28843
28898
  "description": "A reading in words: `value` written with `options` where they say another\nstyle, and otherwise `share`, from 0 to 1, as a percentage, whole unless\n`options` say more.\n\nMeter and Progress each wrote this, with the value each holds to its range\nand the share of it that is filled."
28844
28899
  },
28900
+ {
28901
+ "kind": "function",
28902
+ "name": "latinDigits",
28903
+ "return": {
28904
+ "type": {
28905
+ "text": "string"
28906
+ }
28907
+ },
28908
+ "parameters": [
28909
+ {
28910
+ "name": "text",
28911
+ "type": {
28912
+ "text": "string"
28913
+ }
28914
+ }
28915
+ ],
28916
+ "description": "Text with every decimal digit written in ASCII: Arabic-Indic `١٢٣`, Persian\n`۱۲۳` and Devanagari `१२३` read as `123`. A keyboard in one of those\nlocales types its own digits, and `Number` reads only ASCII."
28917
+ },
28845
28918
  {
28846
28919
  "kind": "function",
28847
28920
  "name": "parseNumber",
@@ -28897,6 +28970,14 @@
28897
28970
  "module": "src/utils/number-format/number-format.ts"
28898
28971
  }
28899
28972
  },
28973
+ {
28974
+ "kind": "js",
28975
+ "name": "latinDigits",
28976
+ "declaration": {
28977
+ "name": "latinDigits",
28978
+ "module": "src/utils/number-format/number-format.ts"
28979
+ }
28980
+ },
28900
28981
  {
28901
28982
  "kind": "js",
28902
28983
  "name": "parseNumber",
@@ -33183,7 +33264,7 @@
33183
33264
  "type": {
33184
33265
  "text": "string"
33185
33266
  },
33186
- "default": "'0.164.0'",
33267
+ "default": "'0.165.0'",
33187
33268
  "description": "The version of the package this file ships in.\n\n**Written by `fusion/scripts/version.mjs`** wherever it writes the package's\n`package.json`, and stamped back into the repository beside it after a\nrelease, so the two never disagree; a spec beside this holds them to it.\nNothing else in the running code knows which release it is."
33188
33269
  }
33189
33270
  ],
@@ -33743,6 +33824,6 @@
33743
33824
  ],
33744
33825
  "helix": {
33745
33826
  "package": "@fusion.dev/helix-core",
33746
- "version": "0.164.0"
33827
+ "version": "0.165.0"
33747
33828
  }
33748
33829
  }
@@ -12,6 +12,7 @@ const STATES = [
12
12
  'aria-controls',
13
13
  'aria-invalid',
14
14
  'aria-pressed',
15
+ 'aria-required',
15
16
  ];
16
17
  export class AriaForwardController {
17
18
  get label() {
@@ -8,6 +8,7 @@ import { html, nothing } from 'lit';
8
8
  import { property, state } from 'lit/decorators.js';
9
9
  import { isPartial, isTwelveHourLayout, segmentKey, segmentLayout, segmentName, segmentRange, segmentsOf, segmentsValue, segmentText, segmentValueText, shownValue, } from '../../utils/date-segments/date-segments.js';
10
10
  import { inlineStep } from '../../utils/inline-step/inline-step.js';
11
+ import { latinDigits } from '../../utils/number-format/number-format.js';
11
12
  import { appleTouch } from '../../utils/platform/platform.js';
12
13
  import { verdictFlags } from '../../utils/verdict-flags/verdict-flags.js';
13
14
  import { formatWords, words } from '../../words/words.js';
@@ -27,11 +28,59 @@ export const MixinSegmentedField = (Base) => {
27
28
  this.touchApple = appleTouch(typeof navigator === 'undefined' ? undefined : navigator);
28
29
  this.entered = undefined;
29
30
  this.opener = undefined;
31
+ this.composing = [];
32
+ this.handleSegmentBeforeInput = (event) => {
33
+ const element = event.currentTarget;
34
+ if (event.inputType === 'insertCompositionText') {
35
+ this.composing = [...element.childNodes].map((node) => [node, node.nodeValue]);
36
+ return;
37
+ }
38
+ event.preventDefault();
39
+ if (event.inputType === 'deleteContentBackward' ||
40
+ event.inputType === 'deleteContentForward') {
41
+ this.typed = '';
42
+ this.edit(element, 'Backspace');
43
+ return;
44
+ }
45
+ for (const character of latinDigits(event.data ?? '')) {
46
+ this.edit(element, character);
47
+ }
48
+ };
49
+ this.handleSegmentInput = (event) => {
50
+ const element = event.currentTarget;
51
+ const { data, inputType } = event;
52
+ if (inputType !== 'insertCompositionText') {
53
+ return;
54
+ }
55
+ const kept = new Set(this.composing.map(([node]) => node));
56
+ for (const node of [...element.childNodes]) {
57
+ if (!kept.has(node)) {
58
+ node.remove();
59
+ }
60
+ }
61
+ for (const [node, text] of this.composing) {
62
+ node.nodeValue = text;
63
+ }
64
+ this.composing = [];
65
+ if (element.dataset.segment === 'dayPeriod' && data) {
66
+ this.edit(element, data.slice(-1));
67
+ }
68
+ };
69
+ this.collapseSelection = () => {
70
+ const root = this.renderRoot;
71
+ const active = this.segmentElements.find((one) => one === root?.activeElement);
72
+ const selection = document.getSelection();
73
+ const at = selection?.anchorNode;
74
+ if (active &&
75
+ selection &&
76
+ at &&
77
+ active.contains(at) &&
78
+ !(at === active && selection.isCollapsed && selection.anchorOffset === 0)) {
79
+ selection.collapse(active, 0);
80
+ }
81
+ };
30
82
  this.handleSegmentKeydown = (event) => {
31
83
  const element = event.currentTarget;
32
- const kind = element.dataset.segment;
33
- const row = Number(element.dataset.row ?? 0);
34
- const twelve = isTwelveHourLayout(this.layout);
35
84
  if (event.key === 'ArrowDown' && event.altKey) {
36
85
  event.preventDefault();
37
86
  this.opener = element;
@@ -56,36 +105,23 @@ export const MixinSegmentedField = (Base) => {
56
105
  this.typed = '';
57
106
  return;
58
107
  }
59
- if (this.readOnly || this.isDisabled) {
60
- return;
61
- }
62
- const edit = segmentKey(event.key, {
63
- kind,
64
- locale: this.segmentLocale,
65
- segments: this.parts[row] ?? {},
66
- system: this.segmentSystem,
67
- twelve,
68
- typed: this.typed,
69
- });
70
- if (edit) {
108
+ if (this.edit(element, latinDigits(event.key))) {
71
109
  event.preventDefault();
72
- this.typed = edit.typed;
73
- this.take(row, edit.segments);
74
- if (edit.advance) {
75
- this.moveFocus(element, 1);
76
- }
77
110
  }
78
111
  };
79
112
  this.handleSegmentsFocusIn = (event) => {
80
113
  this.typed = '';
114
+ document.getSelection()?.collapse(event.target, 0);
81
115
  if (!this.segmentElements.includes(event.relatedTarget)) {
82
116
  this.entered = this.value;
117
+ document.addEventListener('selectionchange', this.collapseSelection);
83
118
  }
84
119
  };
85
120
  this.handleSegmentsFocusOut = (event) => {
86
121
  if (this.segmentElements.includes(event.relatedTarget)) {
87
122
  return;
88
123
  }
124
+ document.removeEventListener('selectionchange', this.collapseSelection);
89
125
  if (this.entered !== undefined && this.entered !== this.value) {
90
126
  this.announceChange();
91
127
  }
@@ -174,6 +210,29 @@ export const MixinSegmentedField = (Base) => {
174
210
  const all = this.segmentElements;
175
211
  all[all.indexOf(from) + by]?.focus();
176
212
  }
213
+ edit(element, key) {
214
+ if (this.readOnly || this.isDisabled) {
215
+ return false;
216
+ }
217
+ const row = Number(element.dataset.row ?? 0);
218
+ const edit = segmentKey(key, {
219
+ kind: element.dataset.segment,
220
+ locale: this.segmentLocale,
221
+ segments: this.parts[row] ?? {},
222
+ system: this.segmentSystem,
223
+ twelve: isTwelveHourLayout(this.layout),
224
+ typed: this.typed,
225
+ });
226
+ if (!edit) {
227
+ return false;
228
+ }
229
+ this.typed = edit.typed;
230
+ this.take(row, edit.segments);
231
+ if (edit.advance) {
232
+ this.moveFocus(element, 1);
233
+ }
234
+ return true;
235
+ }
177
236
  willUpdate(changed) {
178
237
  super.willUpdate(changed);
179
238
  const own = this.own;
@@ -196,6 +255,7 @@ export const MixinSegmentedField = (Base) => {
196
255
  disconnectedCallback() {
197
256
  super.disconnectedCallback();
198
257
  this.removeEventListener('click', this.handleLabelPress);
258
+ document.removeEventListener('selectionchange', this.collapseSelection);
199
259
  }
200
260
  get blocksImplicitSubmission() {
201
261
  return this.segmentsShown || super.blocksImplicitSubmission;
@@ -260,7 +320,10 @@ export const MixinSegmentedField = (Base) => {
260
320
  data-segment=${kind}
261
321
  role=${spin ? 'spinbutton' : 'textbox'}
262
322
  tabindex=${this.isDisabled ? '-1' : '0'}
263
- inputmode=${kind === 'dayPeriod' ? nothing : 'numeric'}
323
+ contenteditable=${editable ? 'true' : nothing}
324
+ spellcheck=${editable ? 'false' : nothing}
325
+ autocorrect=${editable ? 'off' : nothing}
326
+ inputmode=${kind === 'dayPeriod' || !editable ? nothing : 'numeric'}
264
327
  enterkeyhint=${editable ? 'next' : nothing}
265
328
  aria-label=${named === id ? segmentName(kind, locale) : `${segmentName(kind, locale)},`}
266
329
  aria-labelledby=${named === id ? nothing : named}
@@ -273,6 +336,8 @@ export const MixinSegmentedField = (Base) => {
273
336
  aria-required=${this.required ? 'true' : nothing}
274
337
  aria-readonly=${this.readOnly ? 'true' : nothing}
275
338
  @keydown=${this.handleSegmentKeydown}
339
+ @beforeinput=${this.handleSegmentBeforeInput}
340
+ @input=${this.handleSegmentInput}
276
341
  >${text}</span
277
342
  >`;
278
343
  })}`;
@@ -25,6 +25,12 @@ export declare const formatPercent: (share: number, locale?: string) => string;
25
25
  * and the share of it that is filled.
26
26
  */
27
27
  export declare const readingText: (value: number, share: number, locale?: string, options?: Intl.NumberFormatOptions) => string;
28
+ /**
29
+ * Text with every decimal digit written in ASCII: Arabic-Indic `١٢٣`, Persian
30
+ * `۱۲۳` and Devanagari `१२३` read as `123`. A keyboard in one of those
31
+ * locales types its own digits, and `Number` reads only ASCII.
32
+ */
33
+ export declare const latinDigits: (text: string) => string;
28
34
  /**
29
35
  * Reads a number written in a locale with the same options it is shown with,
30
36
  * or `null` where the text is no number.
@@ -14,6 +14,14 @@ export const formatPercent = (share, locale = '') => formatNumber(share, locale,
14
14
  export const readingText = (value, share, locale = '', options) => options && options.style !== 'percent'
15
15
  ? formatNumber(value, locale, options)
16
16
  : formatNumber(share, locale, options ?? WHOLE_PERCENT);
17
+ export const latinDigits = (text) => text.replace(/\p{Nd}/gu, (digit) => {
18
+ const point = digit.codePointAt(0) ?? 0;
19
+ let zero = point;
20
+ while (/\p{Nd}/u.test(String.fromCodePoint(zero - 1))) {
21
+ zero -= 1;
22
+ }
23
+ return String((point - zero) % 10);
24
+ });
17
25
  const grouped = (text, separator, decimal) => {
18
26
  const whole = (text.split(decimal)[0] ?? '').replace(/^[+-]/u, '');
19
27
  return new RegExp(`^\\d{1,3}(${literal(separator)}\\d{3})+$`, 'u').test(whole);
@@ -36,7 +44,7 @@ export const parseNumber = (text, locale = '', options) => {
36
44
  .filter(Boolean)
37
45
  .sort((a, b) => b.length - a.length);
38
46
  const percent = options?.style === 'percent' && /[%\u066a\u2030]/u.test(text);
39
- let read = text.replace(/[\s\u00a0\u202f]/gu, '');
47
+ let read = latinDigits(text).replace(/[\s\u00a0\u202f\u061c\u200e\u200f]/gu, '');
40
48
  for (const one of dropped) {
41
49
  read = read.replace(new RegExp(literal(one), 'gu'), '');
42
50
  }
package/dist/version.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * release, so the two never disagree; a spec beside this holds them to it.
7
7
  * Nothing else in the running code knows which release it is.
8
8
  */
9
- export declare const VERSION = "0.164.0";
9
+ export declare const VERSION = "0.165.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.164.0';
1
+ export const VERSION = '0.165.0';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-core",
3
3
  "description": "Helix element primitives: base classes, mixins, decorators, utilities.",
4
- "version": "0.164.0",
4
+ "version": "0.165.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -56,11 +56,11 @@
56
56
  "bugs": "https://github.com/project-fusion/fusion/issues",
57
57
  "dependencies": {
58
58
  "@floating-ui/dom": "^1.8.0",
59
- "@fusion.dev/helix-icons": "0.164.0"
59
+ "@fusion.dev/helix-icons": "0.165.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@fusion.dev/helix-cem": "0.164.0",
63
- "@fusion.dev/helix-testing": "0.164.0",
62
+ "@fusion.dev/helix-cem": "0.165.0",
63
+ "@fusion.dev/helix-testing": "0.165.0",
64
64
  "@lit-labs/ssr": "^4.1.0",
65
65
  "lit": "^3.2.1"
66
66
  },