@twip/web 0.0.0-canary-092e560 → 0.0.0-canary-abcb2d5

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.
@@ -1,5 +1,5 @@
1
1
  import type { RichText } from './review.js';
2
- import type { ControlKind, ControlOptions, DataBinding } from './services/elements.js';
2
+ import type { ControlKind, ControlOptions, DataBinding, ReferenceRule } from './services/elements.js';
3
3
  import type { TabAlignment, TabLeader, TabSelector } from './services/ruler.js';
4
4
  import type { ViewOptions } from './services/view.js';
5
5
  import type { AutoCorrectChange, HighlightColor, HorizontalPosition, LineSpacing, MarkupView, ObjectWrap, RowHeight, TablePosition, TableWidth, UnderlineStyle, VerticalPosition, WrapSide } from './state.js';
@@ -476,6 +476,40 @@ export interface Commands {
476
476
  labels: Record<string, string>;
477
477
  merge?: boolean;
478
478
  }): Promise<void>;
479
+ /** Which fields are sub-template references: replaces the rules; an empty list turns sub-templates off. View. */
480
+ setSubTemplateRules(options: {
481
+ rules: ReferenceRule[];
482
+ }): Promise<void>;
483
+ /** Shows references' sub-template bodies in their place, or their fields (default: every reference). View. */
484
+ setSubTemplateView(options: {
485
+ ids?: ElementId[];
486
+ expanded: boolean;
487
+ }): Promise<void>;
488
+ /** Edits a sub-template's body in this editor, on its own pages. View. */
489
+ editSubTemplate(options: {
490
+ name: string;
491
+ category?: string;
492
+ }): Promise<void>;
493
+ /** Back to the template from a sub-template, at the template's selection from before. View. */
494
+ closeSubTemplate(): Promise<void>;
495
+ /** A new sub-template, empty or with a DOCX's body (`category`: default the first rule's). */
496
+ createSubTemplate(options: {
497
+ name: string;
498
+ category?: string;
499
+ from?: Uint8Array;
500
+ }): Promise<void>;
501
+ /** Renames a sub-template, and with `updateReferences` every reference to it. */
502
+ renameSubTemplate(options: {
503
+ name: string;
504
+ to: string;
505
+ category?: string;
506
+ updateReferences?: boolean;
507
+ }): Promise<void>;
508
+ /** Removes a sub-template; its references no longer resolve. */
509
+ removeSubTemplate(options: {
510
+ name: string;
511
+ category?: string;
512
+ }): Promise<void>;
479
513
  /** Inserts a content control, optionally around the selection. */
480
514
  insertControl(options: {
481
515
  kind: ControlKind;
@@ -646,6 +680,8 @@ export interface Commands {
646
680
  showAutoCorrectOptions(): Promise<void>;
647
681
  /** Dismisses the popover asked for (`popoverClosed`), for an application that closes its own on Esc or a press elsewhere; with none open, nothing. View. */
648
682
  closePopover(): Promise<void>;
683
+ /** Says the formatting at the caret to screen readers (an `announcement` of kind `formatting`). Shift+F1. */
684
+ describeFormatting(): Promise<void>;
649
685
  }
650
686
  /** A command id: the method name, the batch op's `type`, the catalog id and the key in `state.commands`. */
651
687
  export type CommandId = keyof Commands;
package/dist/commands.js CHANGED
@@ -143,6 +143,13 @@ const encodings = {
143
143
  unlinkFields: 'spread',
144
144
  showFieldCodes: 'spread',
145
145
  setFieldLabels: 'spread',
146
+ setSubTemplateRules: 'spread',
147
+ setSubTemplateView: 'spread',
148
+ editSubTemplate: 'spread',
149
+ closeSubTemplate: 'none',
150
+ createSubTemplate: 'spread',
151
+ renameSubTemplate: 'spread',
152
+ removeSubTemplate: 'spread',
146
153
  insertControl: 'spread',
147
154
  setControl: 'spread',
148
155
  removeControl: 'spread',
@@ -184,6 +191,7 @@ const encodings = {
184
191
  neverAutoCorrect: 'none',
185
192
  showAutoCorrectOptions: 'none',
186
193
  closePopover: 'none',
194
+ describeFormatting: 'none',
187
195
  };
188
196
  /** Builds a command's op from its method arguments. */
189
197
  export function toOp(id, arg) {
package/dist/editor.d.ts CHANGED
@@ -4,7 +4,7 @@ import { FontLibrary } from './fonts/library.js';
4
4
  import { type MountedReview, type Review } from './review.js';
5
5
  import { type Accessibility, type Shortcuts } from './services/accessibility.js';
6
6
  import { type Dialogs } from './services/dialogs.js';
7
- import { type DataParts, type Elements, type MountedElements } from './services/elements.js';
7
+ import { type DataParts, type Elements, type MountedElements, type SubTemplates } from './services/elements.js';
8
8
  import { type Find } from './services/find.js';
9
9
  import { type Menus } from './services/menus.js';
10
10
  import { type Navigation, type Styles } from './services/navigation.js';
@@ -74,6 +74,8 @@ export interface MountOptions extends SessionOptions {
74
74
  * `leaders` colour, so the application draws only the cards. Default false: the application draws them.
75
75
  */
76
76
  drawLeaders?: boolean;
77
+ /** The editor's accessible name, which screen readers say when it gets the focus. Default `'Document'`. */
78
+ label?: string;
77
79
  /** Aborts the mount; everything it created is released. */
78
80
  signal?: AbortSignal;
79
81
  }
@@ -151,6 +153,8 @@ export declare class HeadlessEditor {
151
153
  readonly elements: Elements;
152
154
  /** The custom XML data parts. */
153
155
  readonly data: DataParts;
156
+ /** The template's sub-templates, and editing one in this editor. */
157
+ readonly subTemplates: SubTemplates;
154
158
  /** The ruler model. */
155
159
  readonly ruler: RulerModelSource;
156
160
  /** The accessibility tree. */
package/dist/editor.js CHANGED
@@ -6,7 +6,7 @@ import { mountHost } from './host/mount.js';
6
6
  import { createMountedReview, createReview } from './review.js';
7
7
  import { createAccessibility, createShortcuts } from './services/accessibility.js';
8
8
  import { createDialogs } from './services/dialogs.js';
9
- import { createDataParts, createElements } from './services/elements.js';
9
+ import { createDataParts, createElements, createSubTemplates, } from './services/elements.js';
10
10
  import { createFind } from './services/find.js';
11
11
  import { createMenus } from './services/menus.js';
12
12
  import { createNavigation, createStyles } from './services/navigation.js';
@@ -270,6 +270,8 @@ export class HeadlessEditor {
270
270
  elements;
271
271
  /** The custom XML data parts. */
272
272
  data;
273
+ /** The template's sub-templates, and editing one in this editor. */
274
+ subTemplates;
273
275
  /** The ruler model. */
274
276
  ruler;
275
277
  /** The accessibility tree. */
@@ -295,6 +297,7 @@ export class HeadlessEditor {
295
297
  this.review = createReview(session);
296
298
  this.elements = createElements(session);
297
299
  this.data = createDataParts(session);
300
+ this.subTemplates = createSubTemplates(session);
298
301
  this.ruler = { model: () => session.state.ruler };
299
302
  this.accessibility = createAccessibility(session);
300
303
  this.shortcuts = createShortcuts(session);
package/dist/events.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import type { EditorError } from './errors.js';
2
2
  import type { BalloonPlacement, CommentAnchor } from './review.js';
3
+ import type { AnnouncementKind } from './services/accessibility.js';
3
4
  import type { DialogKind } from './services/dialogs.js';
4
5
  import type { ElementInfo } from './services/elements.js';
5
6
  import type { ContextMenuEvent } from './services/menus.js';
6
7
  import type { RulerView } from './services/ruler.js';
7
8
  import type { GestureEvent, Popover } from './services/view.js';
8
- import type { SelectionSummary } from './state.js';
9
+ import type { EditingTarget, SelectionSummary } from './state.js';
9
10
  import type { Author, ClientPoint, ClientRect, CommentId, ElementId, RevisionId, Warning } from './types.js';
10
11
  /** Every event of `editor.on(type, listener)`, keyed by type. */
11
12
  export interface TwipEvents {
@@ -13,6 +14,8 @@ export interface TwipEvents {
13
14
  documentChanged: TransactionRecord;
14
15
  /** The selection changed; coalesced per frame. */
15
16
  selectionChanged: SelectionSummary;
17
+ /** The editor switched between the template and a sub-template, or between sub-templates: where it is now. */
18
+ editingTargetChanged: EditingTarget;
16
19
  /** A link was activated: a click when read only, Ctrl+click otherwise. */
17
20
  linkActivated: {
18
21
  address: string;
@@ -23,6 +26,19 @@ export interface TwipEvents {
23
26
  key: string;
24
27
  element: ElementId | null;
25
28
  };
29
+ /**
30
+ * Typing, pasting or dropping was refused: into a sub-template's body shown in place of its reference (edit the
31
+ * sub-template instead), or into a locked control.
32
+ */
33
+ editRefused: {
34
+ reason: {
35
+ kind: 'subTemplate';
36
+ name: string;
37
+ } | {
38
+ kind: 'locked';
39
+ };
40
+ element?: ElementId;
41
+ };
26
42
  /** The user asked for a context menu. */
27
43
  contextMenu: ContextMenuEvent;
28
44
  /**
@@ -86,9 +102,10 @@ export interface TwipEvents {
86
102
  rulerChanged: RulerView;
87
103
  /** Mounted only: hover over a link, element mark, revision or comment, or `null` when it ends. */
88
104
  tooltip: Tooltip | null;
89
- /** What the editor said to screen readers. */
105
+ /** What the editor said to screen readers, and what it is about (when one change says several things, the first). */
90
106
  announcement: {
91
107
  text: string;
108
+ kind: AnnouncementKind;
92
109
  };
93
110
  /** A fidelity warning outside an operation. */
94
111
  warning: Warning;
@@ -1,3 +1,9 @@
1
1
  import type { Session } from '../editor.js';
2
2
  import type { Dom, Ledger } from './dom.js';
3
- export declare function startA11y(s: Session, dom: Dom, ledger: Ledger): void;
3
+ /** The mirror's part in pointer input. */
4
+ export interface A11y {
5
+ /** Whether a mirror node was just activated: a press now is the screen reader's click, and places no caret. */
6
+ activating(): boolean;
7
+ }
8
+ /** Mirrors the tree and speaks announcements. `label` is the editor's accessible name; `focus` focuses the editing element. */
9
+ export declare function startA11y(s: Session, dom: Dom, ledger: Ledger, label: string, focus: () => void, report: (e: unknown) => void): A11y;
package/dist/host/a11y.js CHANGED
@@ -1,42 +1,156 @@
1
- import { Kind } from '../transport/protocol.js';
2
- const roles = {
3
- document: 'document',
4
- page: 'region',
5
- paragraph: 'paragraph',
6
- heading: 'heading',
7
- list: 'list',
8
- listItem: 'listitem',
9
- table: 'table',
10
- row: 'row',
11
- cell: 'cell',
12
- image: 'img',
13
- link: 'link',
14
- textBox: 'group',
15
- note: 'note',
16
- header: 'region',
17
- footer: 'region',
1
+ import { Kind, Method } from '../transport/protocol.js';
2
+ /** Each role's element and ARIA role. Inline nodes are spans; the rest are divs. */
3
+ const elements = {
4
+ document: { tag: 'div', role: 'document' },
5
+ paragraph: { tag: 'div', role: 'paragraph' },
6
+ heading: { tag: 'div', role: 'heading' },
7
+ list: { tag: 'div', role: 'list' },
8
+ listItem: { tag: 'div', role: 'listitem' },
9
+ table: { tag: 'div', role: 'table' },
10
+ row: { tag: 'div', role: 'row' },
11
+ cell: { tag: 'div', role: 'cell' },
12
+ columnHeader: { tag: 'div', role: 'columnheader' },
13
+ rowHeader: { tag: 'div', role: 'rowheader' },
14
+ image: { tag: 'span', role: 'img' },
15
+ link: { tag: 'span', role: 'link' },
16
+ textBox: { tag: 'div', role: 'group' },
17
+ note: { tag: 'div', role: 'note' },
18
+ noteRef: { tag: 'span', role: 'link' },
19
+ comment: { tag: 'div', role: 'comment' },
20
+ // Named groups inside the headers and footers region: the landmark key goes from region to region, not into each.
21
+ header: { tag: 'div', role: 'group' },
22
+ footer: { tag: 'div', role: 'group' },
23
+ region: { tag: 'div', role: 'region' },
24
+ text: { tag: 'span' },
18
25
  };
19
- const hidden = 'position:absolute;left:0;top:0;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;';
20
- function build(node) {
21
- const e = document.createElement('div');
22
- e.setAttribute('role', roles[node.role] ?? 'group');
23
- if (node.name)
24
- e.setAttribute('aria-label', node.name);
25
- if (node.level !== undefined)
26
- e.setAttribute('aria-level', String(node.level));
27
- const marks = node.marks ?? [];
28
- if (marks.includes('misspelling'))
29
- e.setAttribute('aria-invalid', 'spelling');
30
- const review = marks.filter((m) => m !== 'misspelling');
31
- if (review.length)
32
- e.setAttribute('aria-description', review.join(' '));
33
- if (node.text)
34
- e.append(node.text);
35
- for (const child of node.children)
36
- e.append(build(child));
37
- return e;
26
+ /** Nodes that hold no caret position of their own: the rest take the focus, and focusing one starts editing there. */
27
+ const unfocused = new Set(['document', 'region', 'header', 'footer', 'list', 'table', 'row', 'text']);
28
+ // Out of sight, one line per block: wrapped at 1px, a screen reader using the screen's layout reads a word a line.
29
+ const hidden = 'position:absolute;left:0;top:0;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:pre;';
30
+ /** Mounts on one page get distinct element ids: `aria-details` refers to them. */
31
+ let mounts = 0;
32
+ /** The mirror of one mount's tree. */
33
+ class Mirror {
34
+ #byId = new Map();
35
+ #prefix = `twip-${++mounts}-`;
36
+ root;
37
+ label;
38
+ constructor(root, label) {
39
+ this.root = root;
40
+ this.label = label;
41
+ }
42
+ /** The element id of node `id`, for `aria-details`. */
43
+ #elementId(id) {
44
+ return this.#prefix + id;
45
+ }
46
+ #build(node) {
47
+ const { tag, role } = elements[node.role] ?? elements.paragraph;
48
+ // Insertions and deletions are ins and del, which screen readers report as such.
49
+ const marks = node.marks ?? [];
50
+ const e = document.createElement(marks.includes('insertion') ? 'ins' : marks.includes('deletion') ? 'del' : tag);
51
+ e.dataset.twipId = String(node.id);
52
+ e.id = this.#elementId(node.id);
53
+ if (role)
54
+ e.setAttribute('role', role);
55
+ const name = node.role === 'document' ? this.label : node.name;
56
+ // A note reference holds no text of its own: its name is its text (a name alone, Chrome's NVDA reads as a letter).
57
+ if (name && node.role === 'noteRef' && !node.text && !node.children.length)
58
+ e.append(name);
59
+ else if (name && node.role !== 'text')
60
+ e.setAttribute('aria-label', name);
61
+ if (node.level !== undefined && (node.role === 'heading' || node.role === 'listItem'))
62
+ e.setAttribute('aria-level', String(node.level));
63
+ if (node.position) {
64
+ e.setAttribute('aria-posinset', String(node.position.index));
65
+ e.setAttribute('aria-setsize', String(node.position.count));
66
+ }
67
+ if (node.cell) {
68
+ e.setAttribute('aria-colindex', String(node.cell.col));
69
+ if (node.cell.colSpan > 1)
70
+ e.setAttribute('aria-colspan', String(node.cell.colSpan));
71
+ if (node.cell.rowSpan > 1)
72
+ e.setAttribute('aria-rowspan', String(node.cell.rowSpan));
73
+ }
74
+ if (marks.includes('misspelling'))
75
+ e.setAttribute('aria-invalid', 'spelling');
76
+ if (node.author && (marks.includes('insertion') || marks.includes('deletion')))
77
+ e.setAttribute('aria-description', `by ${node.author}`);
78
+ if (node.lang)
79
+ e.lang = node.lang;
80
+ if (node.details !== undefined)
81
+ e.setAttribute('aria-details', this.#elementId(node.details));
82
+ // Focusable, as a screen reader's activation (browse mode Enter) focuses the element before it clicks.
83
+ if (!unfocused.has(node.role))
84
+ e.tabIndex = -1;
85
+ if (node.label)
86
+ e.append(Object.assign(document.createElement('span'), { textContent: `${node.label} ` }));
87
+ if (node.text)
88
+ e.append(node.text);
89
+ for (const child of node.children)
90
+ e.append(this.#build(child));
91
+ // A row's index: its cells'.
92
+ if (node.role === 'row') {
93
+ const row = node.children.find((c) => c.cell)?.cell?.row;
94
+ if (row !== undefined)
95
+ e.setAttribute('aria-rowindex', String(row));
96
+ }
97
+ this.#byId.set(node.id, e);
98
+ return e;
99
+ }
100
+ #forget(e) {
101
+ for (const x of [e, ...e.querySelectorAll('[data-twip-id]')]) {
102
+ const id = Number(x.dataset.twipId);
103
+ if (this.#byId.get(id) === x)
104
+ this.#byId.delete(id);
105
+ }
106
+ }
107
+ update(update) {
108
+ if ('full' in update) {
109
+ this.#byId.clear();
110
+ this.root.replaceChildren(this.#build(update.full));
111
+ return;
112
+ }
113
+ // Replaced nodes not in the tree yet wait for the `children` change that places them.
114
+ const detached = new Map();
115
+ for (const change of update.patch) {
116
+ if (change.type === 'replace') {
117
+ const old = this.#byId.get(change.node.id);
118
+ if (old)
119
+ this.#forget(old);
120
+ const e = this.#build(change.node);
121
+ if (old)
122
+ old.replaceWith(e);
123
+ else
124
+ detached.set(change.node.id, e);
125
+ continue;
126
+ }
127
+ const parent = this.#byId.get(change.parent);
128
+ if (!parent)
129
+ continue;
130
+ const wanted = change.ids.map((id) => detached.get(id) ?? this.#byId.get(id)).filter((e) => !!e);
131
+ const keep = new Set(wanted);
132
+ for (const child of [...parent.children]) {
133
+ if (!keep.has(child)) {
134
+ this.#forget(child);
135
+ child.remove();
136
+ }
137
+ }
138
+ // Only elements out of place move: the others stay where the screen reader may be reading.
139
+ wanted.forEach((e, i) => {
140
+ if (parent.children[i] !== e)
141
+ parent.insertBefore(e, parent.children[i] ?? null);
142
+ });
143
+ }
144
+ }
38
145
  }
39
- export function startA11y(s, dom, ledger) {
146
+ /** How long an announcement waits after emptying the live region. */
147
+ const LIVE_MS = 50;
148
+ /** How long after the focus comes into the editing element an announcement waits: past the focus's own speech. */
149
+ const AFTER_FOCUS_MS = 600;
150
+ /** How long after a mirror node's activation a press on the editor is the activation's own mouse click. */
151
+ const ACTIVATION_MS = 500;
152
+ /** Mirrors the tree and speaks announcements. `label` is the editor's accessible name; `focus` focuses the editing element. */
153
+ export function startA11y(s, dom, ledger, label, focus, report) {
40
154
  const tree = ledger.node(dom.layer, document.createElement('div'));
41
155
  tree.dataset.twip = 'a11y';
42
156
  tree.style.cssText = hidden;
@@ -45,18 +159,42 @@ export function startA11y(s, dom, ledger) {
45
159
  live.setAttribute('aria-live', 'polite');
46
160
  live.setAttribute('role', 'status');
47
161
  live.style.cssText = hidden;
162
+ const mirror = new Mirror(tree, label);
48
163
  const onMessage = (m) => {
49
164
  const r = m.payload;
50
165
  if (m.kind === Kind.HostRequest && r.type === 'a11y')
51
- tree.replaceChildren(build(r.tree));
166
+ mirror.update(r.update);
52
167
  };
53
168
  s.hostListeners.add(onMessage);
54
169
  s.disposers.push(() => s.hostListeners.delete(onMessage));
55
- // A repeated announcement is read again: the region empties first.
170
+ // Activating a node starts editing there. A screen reader's activation focuses the node, then clicks it, or clicks
171
+ // with the mouse where it is: that lands on the canvas, and is the activation's, not a click to place the caret.
172
+ let activated = -Infinity;
173
+ const activate = (e) => {
174
+ // Regions and the document hold no caret position: the nearest node inside one that does.
175
+ const at = e.target?.closest('[data-twip-id][tabindex]');
176
+ if (!at || s.destroyed)
177
+ return;
178
+ e.preventDefault();
179
+ activated = performance.now();
180
+ s.call(Method.AccessibleGo, Number(at.dataset.twipId)).then(focus, report);
181
+ };
182
+ ledger.listen(tree, 'click', activate);
183
+ ledger.listen(tree, 'focusin', activate);
184
+ // A repeated announcement is read again: the region empties first. One made as the focus comes into the editing
185
+ // element waits for the screen reader to start saying the focus (NVDA does about 150 ms later, and cancels what it
186
+ // was saying): it is then said after it, not cut off by it ("Editing sub-template Violation" from a button).
56
187
  let cancel;
188
+ let focusedAt = -Infinity;
189
+ ledger.listen(dom.layer, 'focusin', (e) => {
190
+ if (e.target.matches('[data-twip="input"]'))
191
+ focusedAt = performance.now();
192
+ });
57
193
  s.disposers.push(s.on('announcement', ({ text }) => {
58
194
  live.textContent = '';
59
195
  cancel?.();
60
- cancel = ledger.timeout(() => (live.textContent = text), 50);
196
+ const wait = Math.max(LIVE_MS, focusedAt + AFTER_FOCUS_MS - performance.now());
197
+ cancel = ledger.timeout(() => (live.textContent = text), wait);
61
198
  }));
199
+ return { activating: () => performance.now() - activated < ACTIVATION_MS };
62
200
  }
@@ -7,7 +7,7 @@ export interface Clipboard {
7
7
  as?: 'formatted' | 'text';
8
8
  }): Promise<void>;
9
9
  }
10
- export declare function startClipboard(s: Session, dom: Dom, input: HTMLTextAreaElement, ledger: Ledger, report: (e: unknown) => void, toViewport: (e: {
10
+ export declare function startClipboard(s: Session, dom: Dom, input: HTMLElement, ledger: Ledger, report: (e: unknown) => void, toViewport: (e: {
11
11
  clientX: number;
12
12
  clientY: number;
13
13
  }) => {
@@ -1,5 +1,6 @@
1
1
  import { EditorError } from '../errors.js';
2
2
  import { Kind, Method } from '../transport/protocol.js';
3
+ import { codeOf } from './keys.js';
3
4
  /** The last copy on the page: the system clipboard has no place for its token, so a paste brings it back (the engine
4
5
  * pastes the copy it names only while the text is still that copy's). */
5
6
  let lastCopy;
@@ -71,7 +72,7 @@ export function startClipboard(s, dom, input, ledger, report, toViewport) {
71
72
  // Ctrl+Shift+V pastes as text.
72
73
  let plain = false;
73
74
  ledger.listen(input, 'keydown', (e) => {
74
- plain = (e.ctrlKey || e.metaKey) && e.shiftKey && e.code === 'KeyV';
75
+ plain = (e.ctrlKey || e.metaKey) && e.shiftKey && codeOf(e) === 'KeyV';
75
76
  });
76
77
  ledger.listen(input, 'paste', (e) => {
77
78
  e.preventDefault();
package/dist/host/dom.js CHANGED
@@ -113,6 +113,8 @@ export function createDom(container, ledger) {
113
113
  // extent, in both directions.
114
114
  const scroller = element('div', 'editor', 'position:relative;width:100%;height:100%;overflow:auto;display:grid;outline:none;');
115
115
  const canvas = element('canvas', 'canvas', 'grid-area:1/1;position:sticky;left:0;top:0;align-self:start;justify-self:start;display:block;touch-action:none;');
116
+ // The pixels say nothing to assistive technology: the editing element and the accessibility DOM do.
117
+ canvas.setAttribute('aria-hidden', 'true');
116
118
  const content = element('div', 'content', 'grid-area:1/1;position:relative;pointer-events:none;');
117
119
  const layer = element('div', 'layer', 'position:absolute;inset:0;overflow:hidden;');
118
120
  content.append(layer);
@@ -1,3 +1,3 @@
1
1
  import type { Session } from '../editor.js';
2
2
  import type { Ledger } from './dom.js';
3
- export declare function startIme(s: Session, input: HTMLTextAreaElement, ledger: Ledger, report: (e: unknown) => void): void;
3
+ export declare function startIme(s: Session, input: HTMLElement, ledger: Ledger, report: (e: unknown) => void): void;
package/dist/host/ime.js CHANGED
@@ -3,9 +3,6 @@ export function startIme(s, input, ledger, report) {
3
3
  const compose = (phase, text) => s.input({ type: 'composition', phase, text }).catch(report);
4
4
  ledger.listen(input, 'compositionstart', () => compose(Phase.start, ''));
5
5
  ledger.listen(input, 'compositionupdate', (e) => compose(Phase.update, e.data));
6
- ledger.listen(input, 'compositionend', (e) => {
7
- compose(Phase.end, e.data);
8
- // The element only ever holds the composition.
9
- input.value = '';
10
- });
6
+ // The editing element puts its text back after the composition (host/input.ts).
7
+ ledger.listen(input, 'compositionend', (e) => compose(Phase.end, e.data));
11
8
  }
@@ -1,12 +1,15 @@
1
1
  import type { Session } from '../editor.js';
2
2
  import type { Dom, Ledger } from './dom.js';
3
+ /** What the editing element tells screen readers about leaving it. */
4
+ export declare const LEAVE_HINT = "Press F6, or Escape then Tab, to leave the editor";
3
5
  /** The input element of a mounted editor. */
4
6
  export interface Input {
5
- readonly element: HTMLTextAreaElement;
7
+ readonly element: HTMLElement;
6
8
  focus(): void;
7
9
  }
8
10
  /**
9
11
  * Starts keyboard and text input. `claims` says whether a chord is a command's, an application shortcut or a host
10
12
  * key (`shortcuts.lookup`): those keys are the editor's, and their browser defaults (Save, Print, Find) don't run.
13
+ * `label` is the editor's accessible name.
11
14
  */
12
- export declare function startInput(s: Session, dom: Dom, ledger: Ledger, report: (e: unknown) => void, claims: (chord: string) => boolean): Input;
15
+ export declare function startInput(s: Session, dom: Dom, ledger: Ledger, report: (e: unknown) => void, claims: (chord: string) => boolean, label: string): Input;