@arraypress/extension-kit 1.10.0 → 1.11.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.
- package/README.md +97 -20
- package/bin/extension-kit.js +2 -1
- package/package.json +1 -1
- package/src/ai/host.d.ts +183 -0
- package/src/ai/host.js +225 -0
- package/src/ai/model.d.ts +2 -1
- package/src/ai/model.js +10 -2
- package/src/build/vendor.d.ts +29 -8
- package/src/build/vendor.js +136 -40
- package/src/content/highlight.d.ts +15 -6
- package/src/content/highlight.js +42 -17
- package/src/content/overlay.d.ts +19 -4
- package/src/content/overlay.js +44 -6
- package/src/content/slices.d.ts +3 -1
- package/src/content/slices.js +29 -14
- package/src/content/watch.d.ts +4 -2
- package/src/content/watch.js +17 -3
- package/src/dom/focus.d.ts +11 -0
- package/src/dom/focus.js +15 -0
- package/src/frame/window.d.ts +1 -1
- package/src/frame/window.js +8 -7
- package/src/index.d.ts +10 -3
- package/src/index.js +8 -3
- package/src/panel/show-on-page.d.ts +82 -0
- package/src/panel/show-on-page.js +83 -0
- package/src/scripting/call.d.ts +29 -9
- package/src/scripting/call.js +35 -11
- package/src/scripting/context-menus.d.ts +45 -0
- package/src/scripting/context-menus.js +53 -0
- package/src/scripting/registration.d.ts +2 -1
- package/src/scripting/registration.js +2 -1
- package/src/scripting/tab.d.ts +17 -5
- package/src/scripting/tab.js +15 -7
- package/src/settings/saver.js +16 -6
- package/src/settings/storage.d.ts +33 -2
- package/src/settings/storage.js +73 -3
- package/src/settings/store.d.ts +17 -0
- package/src/settings/store.js +87 -20
- package/src/sites/sites.d.ts +6 -1
- package/src/sites/sites.js +20 -3
- package/src/styles/chrome-ui.css +56 -30
- package/src/testing/e2e.d.ts +35 -4
- package/src/testing/e2e.js +67 -5
- package/src/testing/fake-model.d.ts +9 -1
- package/src/testing/fake-model.js +44 -23
- package/src/testing/fake-storage.d.ts +41 -13
- package/src/testing/fake-storage.js +163 -51
- package/src/testing/index.d.ts +3 -3
- package/src/testing/index.js +5 -3
- package/src/testing/live-run.d.ts +23 -2
- package/src/testing/live-run.js +64 -18
- package/src/testing/messages.d.ts +7 -3
- package/src/testing/messages.js +50 -9
- package/src/testing/package.d.ts +56 -7
- package/src/testing/package.js +57 -18
- package/src/testing/prompt.d.ts +4 -2
- package/src/testing/prompt.js +7 -3
- package/src/time/pool.d.ts +2 -1
- package/src/time/pool.js +4 -2
- package/src/toolbar/theme.d.ts +10 -2
- package/src/toolbar/theme.js +12 -4
- package/src/ui/menu.d.ts +13 -3
- package/src/ui/menu.js +31 -11
- package/src/ui/model-row.js +5 -1
- package/src/ui/site-list.js +12 -3
- package/src/ui/text-dialog.d.ts +3 -2
- package/src/ui/text-dialog.js +10 -3
- package/src/ui/toast.js +2 -0
package/src/toolbar/theme.d.ts
CHANGED
|
@@ -132,9 +132,12 @@ export type OffscreenApi = {
|
|
|
132
132
|
*
|
|
133
133
|
* @param {OffscreenApi} api - `chrome.offscreen` and `chrome.runtime`.
|
|
134
134
|
* @param {string} [url] - The document, from the extension's root. Default {@link OFFSCREEN_PATH}.
|
|
135
|
+
* @param {string} [justification] - What the document is for, when it does more than watch the
|
|
136
|
+
* scheme (it answers for Chrome's on-device model too, `serveModelOffscreen`): Chrome keeps it
|
|
137
|
+
* with the document, so it has to be the whole truth.
|
|
135
138
|
* @returns {Promise<boolean>} Whether it opened one (false: already open).
|
|
136
139
|
*/
|
|
137
|
-
export declare function watchFromOffscreen({ offscreen, runtime }: OffscreenApi, url?: string): Promise<boolean>;
|
|
140
|
+
export declare function watchFromOffscreen({ offscreen, runtime }: OffscreenApi, url?: string, justification?: string): Promise<boolean>;
|
|
138
141
|
export type ToolbarWorkerApi = {
|
|
139
142
|
/**
|
|
140
143
|
* `chrome.storage`.
|
|
@@ -179,9 +182,14 @@ export type ToolbarWorkerApi = {
|
|
|
179
182
|
* worker wakes.
|
|
180
183
|
*
|
|
181
184
|
* @param {ToolbarWorkerApi} api - `chrome`, or a fake.
|
|
185
|
+
* @param {{url?: string, justification?: string}} [document] - The offscreen document and what
|
|
186
|
+
* it's for, when it isn't the kit's defaults ({@link watchFromOffscreen}).
|
|
182
187
|
* @returns {ToolbarTheme} The theme, for tests.
|
|
183
188
|
* @example
|
|
184
189
|
* // background.js
|
|
185
190
|
* startToolbarTheme(chrome);
|
|
186
191
|
*/
|
|
187
|
-
export declare function startToolbarTheme(api: ToolbarWorkerApi
|
|
192
|
+
export declare function startToolbarTheme(api: ToolbarWorkerApi, { url, justification }?: {
|
|
193
|
+
url?: string;
|
|
194
|
+
justification?: string;
|
|
195
|
+
}): ToolbarTheme;
|
package/src/toolbar/theme.js
CHANGED
|
@@ -116,6 +116,9 @@ export function createToolbarTheme({ storage, action, extensionId }) {
|
|
|
116
116
|
/** @type {Promise<boolean> | null} */
|
|
117
117
|
let opening = null;
|
|
118
118
|
|
|
119
|
+
/** Why the offscreen document is open, as Chrome is told. */
|
|
120
|
+
const SCHEME_JUSTIFICATION = 'Reads whether Chrome is in light or dark mode, to show the matching toolbar icon.';
|
|
121
|
+
|
|
119
122
|
/**
|
|
120
123
|
* Open the offscreen document that watches the scheme (reason MATCH_MEDIA),
|
|
121
124
|
* unless it's open. It stays open: it's one small page in the extension's own
|
|
@@ -125,16 +128,19 @@ let opening = null;
|
|
|
125
128
|
*
|
|
126
129
|
* @param {OffscreenApi} api - `chrome.offscreen` and `chrome.runtime`.
|
|
127
130
|
* @param {string} [url] - The document, from the extension's root. Default {@link OFFSCREEN_PATH}.
|
|
131
|
+
* @param {string} [justification] - What the document is for, when it does more than watch the
|
|
132
|
+
* scheme (it answers for Chrome's on-device model too, `serveModelOffscreen`): Chrome keeps it
|
|
133
|
+
* with the document, so it has to be the whole truth.
|
|
128
134
|
* @returns {Promise<boolean>} Whether it opened one (false: already open).
|
|
129
135
|
*/
|
|
130
|
-
export function watchFromOffscreen({ offscreen, runtime }, url = OFFSCREEN_PATH) {
|
|
136
|
+
export function watchFromOffscreen({ offscreen, runtime }, url = OFFSCREEN_PATH, justification = SCHEME_JUSTIFICATION) {
|
|
131
137
|
opening ??= (async () => {
|
|
132
138
|
const open = await runtime.getContexts({ contextTypes: ['OFFSCREEN_DOCUMENT'] });
|
|
133
139
|
if (open.length) return false;
|
|
134
140
|
await offscreen.createDocument({
|
|
135
141
|
url,
|
|
136
142
|
reasons: [offscreen.Reason.MATCH_MEDIA],
|
|
137
|
-
justification
|
|
143
|
+
justification,
|
|
138
144
|
});
|
|
139
145
|
return true;
|
|
140
146
|
})().finally(() => { opening = null; });
|
|
@@ -159,15 +165,17 @@ export function watchFromOffscreen({ offscreen, runtime }, url = OFFSCREEN_PATH)
|
|
|
159
165
|
* worker wakes.
|
|
160
166
|
*
|
|
161
167
|
* @param {ToolbarWorkerApi} api - `chrome`, or a fake.
|
|
168
|
+
* @param {{url?: string, justification?: string}} [document] - The offscreen document and what
|
|
169
|
+
* it's for, when it isn't the kit's defaults ({@link watchFromOffscreen}).
|
|
162
170
|
* @returns {ToolbarTheme} The theme, for tests.
|
|
163
171
|
* @example
|
|
164
172
|
* // background.js
|
|
165
173
|
* startToolbarTheme(chrome);
|
|
166
174
|
*/
|
|
167
|
-
export function startToolbarTheme(api) {
|
|
175
|
+
export function startToolbarTheme(api, { url, justification } = {}) {
|
|
168
176
|
const theme = createToolbarTheme({ storage: api.storage.local, action: api.action, extensionId: api.runtime.id });
|
|
169
177
|
theme.restore();
|
|
170
|
-
watchFromOffscreen(api).catch((error) => console.error('Opening the colour scheme watcher:', error));
|
|
178
|
+
watchFromOffscreen(api, url, justification).catch((error) => console.error('Opening the colour scheme watcher:', error));
|
|
171
179
|
api.runtime.onMessage.addListener((message, sender) => {
|
|
172
180
|
theme.onMessage(message, sender);
|
|
173
181
|
return false;
|
package/src/ui/menu.d.ts
CHANGED
|
@@ -99,8 +99,10 @@ export type Box = {
|
|
|
99
99
|
*
|
|
100
100
|
* @param {{width: number, height: number}} size - The menu's size.
|
|
101
101
|
* @param {{width: number, height: number}} viewport - The window's (`innerWidth`, `innerHeight`).
|
|
102
|
-
* @param {{anchor?: Box, x?: number, y?: number, placement?: 'over' | 'below'}} where - The
|
|
103
|
-
* box, or a point; `placement` says where it goes from an anchor (default `'over'`)
|
|
102
|
+
* @param {{anchor?: Box, x?: number, y?: number, placement?: 'over' | 'below', rtl?: boolean}} where - The
|
|
103
|
+
* anchor's box, or a point; `placement` says where it goes from an anchor (default `'over'`); `rtl`
|
|
104
|
+
* (a right-to-left page) lines its start up with the anchor's left edge and opens leftwards from a
|
|
105
|
+
* point, as Chrome mirrors its own.
|
|
104
106
|
* @returns {{left: number, top: number}} The menu's top-left, in px.
|
|
105
107
|
* @example
|
|
106
108
|
* menuPosition({width: 160, height: 120}, {width: 800, height: 600}, {anchor: button.getBoundingClientRect()});
|
|
@@ -112,11 +114,12 @@ export declare function menuPosition(size: {
|
|
|
112
114
|
}, viewport: {
|
|
113
115
|
width: number;
|
|
114
116
|
height: number;
|
|
115
|
-
}, { anchor, x, y, placement }: {
|
|
117
|
+
}, { anchor, x, y, placement, rtl }: {
|
|
116
118
|
anchor?: Box;
|
|
117
119
|
x?: number;
|
|
118
120
|
y?: number;
|
|
119
121
|
placement?: 'over' | 'below';
|
|
122
|
+
rtl?: boolean;
|
|
120
123
|
}): {
|
|
121
124
|
left: number;
|
|
122
125
|
top: number;
|
|
@@ -163,6 +166,11 @@ export type ActionMenu = {
|
|
|
163
166
|
* Close it, if open, and return the focus.
|
|
164
167
|
*/
|
|
165
168
|
close: () => void;
|
|
169
|
+
/**
|
|
170
|
+
* Take it off the page and stop listening to the window (a content
|
|
171
|
+
* script's overlay being removed).
|
|
172
|
+
*/
|
|
173
|
+
destroy: () => void;
|
|
166
174
|
};
|
|
167
175
|
/**
|
|
168
176
|
* What {@link createMenu} takes.
|
|
@@ -182,6 +190,8 @@ export type ActionMenu = {
|
|
|
182
190
|
* @property {(entries: readonly MenuEntry[], where: MenuPlace) => void} open Fill it with `entries`
|
|
183
191
|
* and open it (closing it first if it's open).
|
|
184
192
|
* @property {() => void} close Close it, if open, and return the focus.
|
|
193
|
+
* @property {() => void} destroy Take it off the page and stop listening to the window (a content
|
|
194
|
+
* script's overlay being removed).
|
|
185
195
|
*/
|
|
186
196
|
/**
|
|
187
197
|
* Make a page's action menu: one `<dialog class="menu">` that any number of
|
package/src/ui/menu.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @module ui/menu
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { focusedElement, rootDocument } from '../dom/focus.js';
|
|
17
|
+
import { focusedElement, focusedIn, rootDocument } from '../dom/focus.js';
|
|
18
18
|
|
|
19
19
|
/** The gap kept between a menu and the window's edges, in px. */
|
|
20
20
|
export const MENU_EDGE = 8;
|
|
@@ -67,19 +67,21 @@ const BELOW_GAP = 4;
|
|
|
67
67
|
*
|
|
68
68
|
* @param {{width: number, height: number}} size - The menu's size.
|
|
69
69
|
* @param {{width: number, height: number}} viewport - The window's (`innerWidth`, `innerHeight`).
|
|
70
|
-
* @param {{anchor?: Box, x?: number, y?: number, placement?: 'over' | 'below'}} where - The
|
|
71
|
-
* box, or a point; `placement` says where it goes from an anchor (default `'over'`)
|
|
70
|
+
* @param {{anchor?: Box, x?: number, y?: number, placement?: 'over' | 'below', rtl?: boolean}} where - The
|
|
71
|
+
* anchor's box, or a point; `placement` says where it goes from an anchor (default `'over'`); `rtl`
|
|
72
|
+
* (a right-to-left page) lines its start up with the anchor's left edge and opens leftwards from a
|
|
73
|
+
* point, as Chrome mirrors its own.
|
|
72
74
|
* @returns {{left: number, top: number}} The menu's top-left, in px.
|
|
73
75
|
* @example
|
|
74
76
|
* menuPosition({width: 160, height: 120}, {width: 800, height: 600}, {anchor: button.getBoundingClientRect()});
|
|
75
77
|
* // → {left: anchor.right - 160, top: anchor.top}
|
|
76
78
|
*/
|
|
77
|
-
export function menuPosition(size, viewport, { anchor, x = 0, y = 0, placement = 'over' }) {
|
|
79
|
+
export function menuPosition(size, viewport, { anchor, x = 0, y = 0, placement = 'over', rtl = false }) {
|
|
78
80
|
const { width, height } = size;
|
|
79
81
|
const left = (/** @type {number} */ start) => Math.max(MENU_EDGE, Math.min(start, viewport.width - width - MENU_EDGE));
|
|
80
82
|
const room = (/** @type {number} */ top) => top + height <= viewport.height - MENU_EDGE;
|
|
81
|
-
if (!anchor) return { left: left(x), top: Math.max(MENU_EDGE, room(y) ? y : y - height) };
|
|
82
|
-
const start = left(anchor.right - width);
|
|
83
|
+
if (!anchor) return { left: left(rtl ? x - width : x), top: Math.max(MENU_EDGE, room(y) ? y : y - height) };
|
|
84
|
+
const start = left(rtl ? anchor.left : anchor.right - width);
|
|
83
85
|
if (placement === 'below') {
|
|
84
86
|
const below = anchor.bottom + BELOW_GAP;
|
|
85
87
|
return { left: start, top: Math.max(MENU_EDGE, room(below) ? below : anchor.top - height - BELOW_GAP) };
|
|
@@ -118,6 +120,8 @@ export function isInside(box, x, y) {
|
|
|
118
120
|
* @property {(entries: readonly MenuEntry[], where: MenuPlace) => void} open Fill it with `entries`
|
|
119
121
|
* and open it (closing it first if it's open).
|
|
120
122
|
* @property {() => void} close Close it, if open, and return the focus.
|
|
123
|
+
* @property {() => void} destroy Take it off the page and stop listening to the window (a content
|
|
124
|
+
* script's overlay being removed).
|
|
121
125
|
*/
|
|
122
126
|
|
|
123
127
|
/**
|
|
@@ -168,21 +172,32 @@ export function createMenu({ id, placement = 'over', root, doc = rootDocument(ro
|
|
|
168
172
|
|
|
169
173
|
menu.addEventListener('keydown', (event) => {
|
|
170
174
|
const list = items();
|
|
171
|
-
|
|
175
|
+
// Asked of the menu's own root: a closed shadow root's focused element can't be seen from the document.
|
|
176
|
+
const at = list.indexOf(/** @type {HTMLButtonElement} */ (focusedIn(menu)));
|
|
172
177
|
/** @param {number} index */
|
|
173
178
|
const go = (index) => {
|
|
174
|
-
|
|
179
|
+
handled();
|
|
175
180
|
list[(index + list.length) % list.length]?.focus();
|
|
176
181
|
};
|
|
182
|
+
/** The key is the menu's: the page under a content script's menu doesn't act on it too (its own shortcuts). */
|
|
183
|
+
const handled = () => {
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
event.stopPropagation();
|
|
186
|
+
};
|
|
177
187
|
if (event.key === 'ArrowDown') go(at + 1);
|
|
178
188
|
else if (event.key === 'ArrowUp') go(at < 0 ? -1 : at - 1);
|
|
179
189
|
else if (event.key === 'Home') go(0);
|
|
180
190
|
else if (event.key === 'End') go(-1);
|
|
181
191
|
else if (event.key === 'Escape' || event.key === 'Tab') {
|
|
182
|
-
|
|
192
|
+
handled();
|
|
183
193
|
close();
|
|
184
194
|
}
|
|
185
195
|
});
|
|
196
|
+
// Closed any other way (the browser's own close, another script): the button no longer says it's open.
|
|
197
|
+
menu.addEventListener('close', () => {
|
|
198
|
+
expanded?.setAttribute('aria-expanded', 'false');
|
|
199
|
+
expanded = null;
|
|
200
|
+
});
|
|
186
201
|
// Esc the browser handles itself (when the keydown above didn't see it).
|
|
187
202
|
menu.addEventListener('cancel', (event) => {
|
|
188
203
|
event.preventDefault();
|
|
@@ -219,7 +234,7 @@ export function createMenu({ id, placement = 'over', root, doc = rootDocument(ro
|
|
|
219
234
|
if (label) menu.setAttribute('aria-label', label);
|
|
220
235
|
else menu.removeAttribute('aria-label');
|
|
221
236
|
|
|
222
|
-
const active = focusedElement(doc);
|
|
237
|
+
const active = focusedIn(menu) ?? focusedElement(doc);
|
|
223
238
|
returnFocus = focusTo ?? anchor ?? (active instanceof view.HTMLElement ? active : null);
|
|
224
239
|
if (anchor?.hasAttribute('aria-haspopup')) {
|
|
225
240
|
anchor.setAttribute('aria-expanded', 'true');
|
|
@@ -228,13 +243,18 @@ export function createMenu({ id, placement = 'over', root, doc = rootDocument(ro
|
|
|
228
243
|
menu.showModal();
|
|
229
244
|
const { width, height } = menu.getBoundingClientRect();
|
|
230
245
|
const { left, top } = menuPosition({ width, height }, { width: view.innerWidth, height: view.innerHeight }, {
|
|
231
|
-
anchor: anchor?.getBoundingClientRect(), x, y, placement,
|
|
246
|
+
anchor: anchor?.getBoundingClientRect(), x, y, placement, rtl: view.getComputedStyle(menu).direction === 'rtl',
|
|
232
247
|
});
|
|
233
248
|
menu.style.left = `${left}px`;
|
|
234
249
|
menu.style.top = `${top}px`;
|
|
235
250
|
items()[0]?.focus();
|
|
236
251
|
},
|
|
237
252
|
close,
|
|
253
|
+
destroy() {
|
|
254
|
+
shut();
|
|
255
|
+
view.removeEventListener('blur', close);
|
|
256
|
+
menu.remove();
|
|
257
|
+
},
|
|
238
258
|
};
|
|
239
259
|
}
|
|
240
260
|
|
package/src/ui/model-row.js
CHANGED
|
@@ -94,7 +94,11 @@ export function createModelRow({ status, help, download, progress }, { options,
|
|
|
94
94
|
progress.value = loaded;
|
|
95
95
|
status.textContent = t(MODEL_MESSAGES.downloading, [String(Math.round(loaded * 100))]);
|
|
96
96
|
},
|
|
97
|
-
}).then(
|
|
97
|
+
}).then(
|
|
98
|
+
// The state as the code that uses it sees it; if it can't be asked (the worker didn't answer), as this page does.
|
|
99
|
+
async (here) => show(await state().catch(() => here)),
|
|
100
|
+
() => show('downloadable', t(MODEL_MESSAGES.downloadFailed)),
|
|
101
|
+
);
|
|
98
102
|
});
|
|
99
103
|
|
|
100
104
|
return {
|
package/src/ui/site-list.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* @module ui/site-list
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
import { focusedIn } from '../dom/focus.js';
|
|
30
31
|
import { createMenu, moreActionsButton } from './menu.js';
|
|
31
32
|
import { askText } from './text-dialog.js';
|
|
32
33
|
import { MAX_HOST_LENGTH, siteLabel } from '../sites/sites.js';
|
|
@@ -177,9 +178,11 @@ export function createSiteList({
|
|
|
177
178
|
form?.addEventListener('submit', async (event) => {
|
|
178
179
|
event.preventDefault();
|
|
179
180
|
const input = /** @type {HTMLInputElement} */ (form.querySelector('input'));
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
const typed = input.value;
|
|
182
|
+
const site = parse(typed);
|
|
183
|
+
if (!site) invalid?.(typed);
|
|
184
|
+
// Cleared only if it's still what was added: adding can wait on Chrome (a permission prompt), and the next site may be typed meanwhile.
|
|
185
|
+
else if (await add(site) !== false && input.value === typed) input.value = '';
|
|
183
186
|
});
|
|
184
187
|
addButton?.addEventListener('click', async () => {
|
|
185
188
|
const site = await ask(w.addSite, w.add);
|
|
@@ -219,6 +222,11 @@ export function createSiteList({
|
|
|
219
222
|
|
|
220
223
|
return {
|
|
221
224
|
render(sites) {
|
|
225
|
+
// The focus stays in the list across a redraw: on the same row's button (the one after a removed
|
|
226
|
+
// row, the last after the last), or on the way to add when no row is left. Without this, the
|
|
227
|
+
// row's button a dialog or menu gave the focus back to is replaced and the focus falls to the page.
|
|
228
|
+
const focused = focusedIn(list);
|
|
229
|
+
const at = focused && list.contains(focused) ? [...list.children].findIndex((row) => row.contains(focused)) : -1;
|
|
222
230
|
list.replaceChildren(...sites.map((site) => {
|
|
223
231
|
const row = doc.createElement('li');
|
|
224
232
|
row.className = 'row';
|
|
@@ -229,6 +237,7 @@ export function createSiteList({
|
|
|
229
237
|
return row;
|
|
230
238
|
}));
|
|
231
239
|
if (empty) empty.hidden = sites.length > 0;
|
|
240
|
+
if (at >= 0) (list.children[Math.min(at, sites.length - 1)]?.querySelector('button') ?? addButton ?? form?.querySelector('input'))?.focus();
|
|
232
241
|
},
|
|
233
242
|
};
|
|
234
243
|
}
|
package/src/ui/text-dialog.d.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* folder's name (`askText`); several, as chrome://settings/searchEngines asks
|
|
5
5
|
* for a site search (`askFields`). The action button stays disabled while a
|
|
6
6
|
* field is blank or isn't valid, with the reason under that field. Enter
|
|
7
|
-
* means the action button, Esc and Cancel cancel
|
|
8
|
-
*
|
|
7
|
+
* means the action button, Esc and Cancel cancel (as does the dialog being
|
|
8
|
+
* closed any other way), and the focus goes back to what opened it. A reason
|
|
9
|
+
* is announced as it appears, and its field marked invalid. The page needs no markup: the dialog (`#text-dialog`) is
|
|
9
10
|
* made the first time, its fields each time it's asked.
|
|
10
11
|
*
|
|
11
12
|
* @module ui/text-dialog
|
package/src/ui/text-dialog.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* folder's name (`askText`); several, as chrome://settings/searchEngines asks
|
|
5
5
|
* for a site search (`askFields`). The action button stays disabled while a
|
|
6
6
|
* field is blank or isn't valid, with the reason under that field. Enter
|
|
7
|
-
* means the action button, Esc and Cancel cancel
|
|
8
|
-
*
|
|
7
|
+
* means the action button, Esc and Cancel cancel (as does the dialog being
|
|
8
|
+
* closed any other way), and the focus goes back to what opened it. A reason
|
|
9
|
+
* is announced as it appears, and its field marked invalid. The page needs no markup: the dialog (`#text-dialog`) is
|
|
9
10
|
* made the first time, its fields each time it's asked.
|
|
10
11
|
*
|
|
11
12
|
* @module ui/text-dialog
|
|
@@ -178,7 +179,7 @@ function fieldFor(doc, request, only) {
|
|
|
178
179
|
input.value = typeof request.value === 'string' ? request.value : '';
|
|
179
180
|
if (request.placeholder) input.placeholder = request.placeholder;
|
|
180
181
|
if (request.maxLength !== undefined) input.maxLength = request.maxLength;
|
|
181
|
-
const error = element(doc, 'span', { id: ids.error, class: 'hint error' });
|
|
182
|
+
const error = element(doc, 'span', { id: ids.error, class: 'hint error', 'aria-live': 'polite' });
|
|
182
183
|
error.hidden = true;
|
|
183
184
|
const hint = element(doc, 'span', { id: ids.hint, class: 'hint secondary' });
|
|
184
185
|
hint.hidden = true;
|
|
@@ -226,6 +227,9 @@ export function askFields({
|
|
|
226
227
|
const { reason } = waiting ? { reason: '' } : checkText(input.value, request.validate);
|
|
227
228
|
error.textContent = reason;
|
|
228
229
|
error.hidden = !reason;
|
|
230
|
+
// Said to a screen reader as it appears, and the field marked (the stylesheet turns its underline red).
|
|
231
|
+
if (reason) input.setAttribute('aria-invalid', 'true');
|
|
232
|
+
else input.removeAttribute('aria-invalid');
|
|
229
233
|
hint.textContent = waiting ? request.waiting ?? '' : typeof request.hint === 'function' ? request.hint(input.value) : request.hint ?? '';
|
|
230
234
|
hint.hidden = !hint.textContent;
|
|
231
235
|
};
|
|
@@ -291,6 +295,9 @@ export function askFields({
|
|
|
291
295
|
event.preventDefault();
|
|
292
296
|
settle(null);
|
|
293
297
|
}, { signal });
|
|
298
|
+
// Closed any other way (the browser's own close after a second Esc, another script): cancelled, not left unanswered.
|
|
299
|
+
// (The event comes a task after the closing: one from the ask before this, already answered, finds the dialog open again.)
|
|
300
|
+
parts.dialog.addEventListener('close', () => { if (!parts.dialog.open) settle(null); }, { signal });
|
|
294
301
|
|
|
295
302
|
parts.dialog.showModal();
|
|
296
303
|
// The first field still to fill in, or the first, its text selected to type over.
|
package/src/ui/toast.js
CHANGED
|
@@ -93,6 +93,8 @@ export function createToast({ undoLabel = 'Undo', duration = TOAST_MS, undoDurat
|
|
|
93
93
|
text.textContent = message;
|
|
94
94
|
undoButton.hidden = !undo;
|
|
95
95
|
undoLast = undo;
|
|
96
|
+
// Taken off the page since (a single-page site replaced its body): put back, or showing it would throw.
|
|
97
|
+
if (!toast.isConnected) (root?.isConnected === false ? doc.body : root ?? doc.body).append(toast);
|
|
96
98
|
// Hidden and shown again, so it's the newest in the top layer (over a popover since).
|
|
97
99
|
toast.togglePopover(false);
|
|
98
100
|
toast.togglePopover(true);
|