@arraypress/extension-kit 1.2.0 → 1.4.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 +147 -17
- package/bin/extension-kit.js +46 -10
- package/package.json +1 -1
- package/src/build/index.d.ts +4 -2
- package/src/build/index.js +6 -2
- package/src/build/store-frame.d.ts +176 -0
- package/src/build/store-frame.js +241 -0
- package/src/build/test-build.d.ts +85 -0
- package/src/build/test-build.js +82 -0
- package/src/build/toolbar-icons.d.ts +36 -18
- package/src/build/toolbar-icons.js +91 -24
- package/src/build/vendor.d.ts +78 -8
- package/src/build/vendor.js +133 -10
- package/src/build/zip.d.ts +3 -2
- package/src/build/zip.js +6 -4
- package/src/dom/focus.d.ts +28 -0
- package/src/dom/focus.js +36 -0
- package/src/dom/i18n.d.ts +11 -3
- package/src/dom/i18n.js +17 -4
- package/src/dom/setup.d.ts +55 -0
- package/src/dom/setup.js +47 -0
- package/src/index.d.ts +9 -1
- package/src/index.js +10 -1
- package/src/sites/sites.d.ts +114 -0
- package/src/sites/sites.js +169 -0
- package/src/styles/chrome-ui.css +13 -6
- package/src/testing/browser.d.ts +52 -0
- package/src/testing/browser.js +79 -0
- package/src/testing/e2e.d.ts +18 -5
- package/src/testing/e2e.js +17 -5
- package/src/testing/index.d.ts +7 -3
- package/src/testing/index.js +9 -3
- package/src/testing/pool.d.ts +23 -0
- package/src/testing/pool.js +33 -0
- package/src/time/time.d.ts +36 -0
- package/src/time/time.js +47 -0
- package/src/ui/menu.d.ts +13 -6
- package/src/ui/menu.js +11 -7
- package/src/ui/site-list.d.ts +216 -0
- package/src/ui/site-list.js +234 -0
- package/src/ui/text-dialog.js +3 -1
- package/src/ui/toast.d.ts +25 -9
- package/src/ui/toast.js +25 -11
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A settings card's list of sites (“Never on”, “Allowed sites”), in one of
|
|
3
|
+
* Chrome's two ways:
|
|
4
|
+
*
|
|
5
|
+
* - **With an add row** (`form`), as a short list in a chrome://settings
|
|
6
|
+
* card: a row per site with a Remove button, and below them a field and an
|
|
7
|
+
* Add button.
|
|
8
|
+
* - **With an Add button and a dialog** (`addButton`), as
|
|
9
|
+
* chrome://settings/performance lists the sites Memory Saver keeps active:
|
|
10
|
+
* the button asks for a site in the one-field dialog (“Add site”), and each
|
|
11
|
+
* row has a ⋮ menu with Edit and Remove.
|
|
12
|
+
*
|
|
13
|
+
* Either way, an optional “No sites added” row shows while there are none.
|
|
14
|
+
* The page's markup holds the list, the empty row and the form or button (so
|
|
15
|
+
* they're localised with the rest of the page); this fills the list and wires
|
|
16
|
+
* them up.
|
|
17
|
+
*
|
|
18
|
+
* ```html
|
|
19
|
+
* <ul class="list" id="never-sites"></ul>
|
|
20
|
+
* <p class="row text" id="no-sites"><span class="label secondary">No sites added</span></p>
|
|
21
|
+
* <form class="row add" id="add-never">
|
|
22
|
+
* <input type="text" placeholder="example.com" aria-label="Site" autocomplete="off" spellcheck="false">
|
|
23
|
+
* <button type="submit">Add</button>
|
|
24
|
+
* </form>
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @module ui/site-list
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { createMenu, moreActionsButton } from './menu.js';
|
|
31
|
+
import { askText } from './text-dialog.js';
|
|
32
|
+
import { MAX_HOST_LENGTH, siteLabel } from '../sites/sites.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The list's words, Chrome's English by default; a page passes its translations.
|
|
36
|
+
*
|
|
37
|
+
* @typedef {object} SiteListWords
|
|
38
|
+
* @property {string} add The Add button in the dialog.
|
|
39
|
+
* @property {string} addSite The dialog's title when adding.
|
|
40
|
+
* @property {string} editSite The dialog's title when editing.
|
|
41
|
+
* @property {string} edit The ⋮ menu's Edit item.
|
|
42
|
+
* @property {string} remove The Remove button, or the ⋮ menu's Remove item.
|
|
43
|
+
* @property {string} save The dialog's action when editing.
|
|
44
|
+
* @property {string} cancel The dialog's Cancel button.
|
|
45
|
+
* @property {string} site The dialog's field label.
|
|
46
|
+
* @property {string} invalid Under the dialog's field while what's typed isn't a site.
|
|
47
|
+
* @property {(label: string) => string} removeLabel The Remove button's name for screen readers.
|
|
48
|
+
* @property {(label: string) => string} moreActions The ⋮ button's name for screen readers.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/** @type {Readonly<SiteListWords>} */
|
|
52
|
+
export const SITE_LIST_WORDS = Object.freeze({
|
|
53
|
+
add: 'Add',
|
|
54
|
+
addSite: 'Add site',
|
|
55
|
+
editSite: 'Edit site',
|
|
56
|
+
edit: 'Edit',
|
|
57
|
+
remove: 'Remove',
|
|
58
|
+
save: 'Save',
|
|
59
|
+
cancel: 'Cancel',
|
|
60
|
+
site: 'Site',
|
|
61
|
+
invalid: 'Enter a site, like example.com',
|
|
62
|
+
removeLabel: (/** @type {string} */ label) => `Remove “${label}”`,
|
|
63
|
+
moreActions: (/** @type {string} */ label) => `More actions for “${label}”`,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The message key each word is read from when a page passes its `t`, so
|
|
68
|
+
* every extension's `_locales` names them alike. `removeLabel` and
|
|
69
|
+
* `moreActions` take the site as `$1` (`"More actions for $SITE$"`).
|
|
70
|
+
*
|
|
71
|
+
* @type {Readonly<Record<keyof SiteListWords, string>>}
|
|
72
|
+
*/
|
|
73
|
+
export const SITE_LIST_MESSAGES = Object.freeze({
|
|
74
|
+
add: 'optionsAdd',
|
|
75
|
+
addSite: 'optionsAddSite',
|
|
76
|
+
editSite: 'optionsEditSite',
|
|
77
|
+
edit: 'optionsEdit',
|
|
78
|
+
remove: 'optionsRemove',
|
|
79
|
+
save: 'optionsSave',
|
|
80
|
+
cancel: 'optionsCancel',
|
|
81
|
+
site: 'optionsSiteLabel',
|
|
82
|
+
invalid: 'optionsSiteInvalid',
|
|
83
|
+
removeLabel: 'optionsRemoveLabel',
|
|
84
|
+
moreActions: 'optionsMoreActions',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The list's words from a page's messages, under {@link SITE_LIST_MESSAGES}.
|
|
89
|
+
*
|
|
90
|
+
* @param {(key: string, substitutions?: string | string[]) => string} t - The page's message lookup.
|
|
91
|
+
* @returns {SiteListWords} The words.
|
|
92
|
+
*/
|
|
93
|
+
function translatedWords(t) {
|
|
94
|
+
const m = SITE_LIST_MESSAGES;
|
|
95
|
+
return {
|
|
96
|
+
add: t(m.add), addSite: t(m.addSite), editSite: t(m.editSite), edit: t(m.edit), remove: t(m.remove),
|
|
97
|
+
save: t(m.save), cancel: t(m.cancel), site: t(m.site), invalid: t(m.invalid),
|
|
98
|
+
removeLabel: (label) => t(m.removeLabel, [label]),
|
|
99
|
+
moreActions: (label) => t(m.moreActions, [label]),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* What {@link createSiteList} takes: the list, one way to add to it, and
|
|
105
|
+
* what adding, removing and editing do.
|
|
106
|
+
*
|
|
107
|
+
* @typedef {object} SiteListOptions
|
|
108
|
+
* @property {HTMLElement} list The `<ul class="list">` to fill.
|
|
109
|
+
* @property {HTMLElement} [empty] Shown while the list is empty.
|
|
110
|
+
* @property {HTMLFormElement} [form] An add row: a form with one text field and a submit button.
|
|
111
|
+
* @property {HTMLElement} [addButton] Or an Add button that asks in the dialog; each row then has a
|
|
112
|
+
* ⋮ menu (Edit, Remove) instead of a Remove button.
|
|
113
|
+
* @property {(text: string) => string | null} parse What was typed as the site the list keeps
|
|
114
|
+
* (`siteHost`, `siteOrigin`), or null when it isn't one.
|
|
115
|
+
* @property {(site: string) => unknown} add Adds a parsed site (asking Chrome for access first, if
|
|
116
|
+
* the list needs it). Resolve to false to keep the text in the add row's field (access refused).
|
|
117
|
+
* @property {(site: string) => unknown} remove Removes a site.
|
|
118
|
+
* @property {(site: string, edited: string) => unknown} [replace] Puts an edited site in place of
|
|
119
|
+
* the old one (the ⋮ menu's Edit; without it, the menu has only Remove).
|
|
120
|
+
* @property {(text: string) => void} [invalid] The add row: called with what was typed when it isn't a site.
|
|
121
|
+
* @property {(site: string) => string} [label] How a site reads. Default {@link siteLabel}.
|
|
122
|
+
* @property {number} [maxLength] The dialog field's `maxlength`. Default {@link MAX_HOST_LENGTH}.
|
|
123
|
+
* @property {(key: string, substitutions?: string | string[]) => string} [t] The page's message lookup
|
|
124
|
+
* (`setUpPage`'s): the words are then its messages under {@link SITE_LIST_MESSAGES}. Without it,
|
|
125
|
+
* Chrome's English ({@link SITE_LIST_WORDS}).
|
|
126
|
+
* @property {Partial<SiteListWords>} [words] Words that differ from those, by name.
|
|
127
|
+
*/
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A site list.
|
|
131
|
+
*
|
|
132
|
+
* @typedef {object} SiteList
|
|
133
|
+
* @property {(sites: readonly string[]) => void} render Show these sites (call it whenever they change).
|
|
134
|
+
*/
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Fill a site list and wire its way of adding: an add row (typing a site and
|
|
138
|
+
* pressing Enter or Add) or an Add button (the dialog, which keeps its Add off
|
|
139
|
+
* until what's typed is a site). A row's Remove (or its ⋮ menu's Remove and
|
|
140
|
+
* Edit) hands its site to `remove` (or `replace`).
|
|
141
|
+
*
|
|
142
|
+
* @param {SiteListOptions} options - The markup, the parser and what adding and removing do.
|
|
143
|
+
* @returns {SiteList} The list.
|
|
144
|
+
* @example
|
|
145
|
+
* const never = createSiteList({
|
|
146
|
+
* list: byId('never-sites'), form: byId('add-never', HTMLFormElement), empty: byId('no-sites'),
|
|
147
|
+
* parse: siteHost,
|
|
148
|
+
* add: (host) => save({ neverSites: withSite(settings.neverSites, host) }),
|
|
149
|
+
* remove: (host) => save({ neverSites: withoutSite(settings.neverSites, host) }),
|
|
150
|
+
* t, // its words from optionsAdd, optionsRemove, …
|
|
151
|
+
* });
|
|
152
|
+
* never.render(settings.neverSites);
|
|
153
|
+
*/
|
|
154
|
+
export function createSiteList({
|
|
155
|
+
list, empty, form, addButton, parse, add, remove, replace, invalid, label = siteLabel, maxLength = MAX_HOST_LENGTH, t, words,
|
|
156
|
+
}) {
|
|
157
|
+
const doc = list.ownerDocument;
|
|
158
|
+
const w = { ...(t ? translatedWords(t) : SITE_LIST_WORDS), ...words };
|
|
159
|
+
const menu = addButton ? createMenu({ doc }) : null;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Ask for a site in the dialog.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} title - Its title.
|
|
165
|
+
* @param {string} action - Its action button.
|
|
166
|
+
* @param {string} [value] - The site to start with.
|
|
167
|
+
* @returns {Promise<string | null>} The site, parsed, or null when cancelled.
|
|
168
|
+
*/
|
|
169
|
+
const ask = async (title, action, value) => {
|
|
170
|
+
const text = await askText({
|
|
171
|
+
title, label: w.site, value, action, cancel: w.cancel, maxLength, doc,
|
|
172
|
+
validate: (typed) => (parse(typed) ? null : w.invalid),
|
|
173
|
+
});
|
|
174
|
+
return text === null ? null : parse(text);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
form?.addEventListener('submit', async (event) => {
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
const input = /** @type {HTMLInputElement} */ (form.querySelector('input'));
|
|
180
|
+
const site = parse(input.value);
|
|
181
|
+
if (!site) invalid?.(input.value);
|
|
182
|
+
else if (await add(site) !== false) input.value = '';
|
|
183
|
+
});
|
|
184
|
+
addButton?.addEventListener('click', async () => {
|
|
185
|
+
const site = await ask(w.addSite, w.add);
|
|
186
|
+
if (site) await add(site);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The end of a site's row: its ⋮ button, or its Remove button.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} site - The site.
|
|
193
|
+
* @param {string} name - How it reads.
|
|
194
|
+
* @returns {HTMLButtonElement} The button.
|
|
195
|
+
*/
|
|
196
|
+
const rowEnd = (site, name) => {
|
|
197
|
+
if (menu) {
|
|
198
|
+
return moreActionsButton(name, {
|
|
199
|
+
label: w.moreActions(name), doc,
|
|
200
|
+
onClick: (anchor) => menu.open([
|
|
201
|
+
...replace ? [{
|
|
202
|
+
label: w.edit,
|
|
203
|
+
run: async () => {
|
|
204
|
+
const edited = await ask(w.editSite, w.save, name);
|
|
205
|
+
if (edited && edited !== site) await replace(site, edited);
|
|
206
|
+
},
|
|
207
|
+
}] : [],
|
|
208
|
+
{ label: w.remove, run: () => remove(site) },
|
|
209
|
+
], { anchor }),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const button = doc.createElement('button');
|
|
213
|
+
button.type = 'button';
|
|
214
|
+
button.textContent = w.remove;
|
|
215
|
+
button.setAttribute('aria-label', w.removeLabel(name));
|
|
216
|
+
button.addEventListener('click', () => remove(site));
|
|
217
|
+
return button;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
render(sites) {
|
|
222
|
+
list.replaceChildren(...sites.map((site) => {
|
|
223
|
+
const row = doc.createElement('li');
|
|
224
|
+
row.className = 'row';
|
|
225
|
+
const name = doc.createElement('span');
|
|
226
|
+
name.className = 'label';
|
|
227
|
+
name.textContent = label(site);
|
|
228
|
+
row.append(name, rowEnd(site, name.textContent));
|
|
229
|
+
return row;
|
|
230
|
+
}));
|
|
231
|
+
if (empty) empty.hidden = sites.length > 0;
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
package/src/ui/text-dialog.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* @module ui/text-dialog
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { focusedElement } from '../dom/focus.js';
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* What the dialog asks.
|
|
14
16
|
*
|
|
@@ -126,7 +128,7 @@ export function askText({
|
|
|
126
128
|
}) {
|
|
127
129
|
const parts = partsFor(doc);
|
|
128
130
|
pending.get(doc)?.(null);
|
|
129
|
-
const active = doc
|
|
131
|
+
const active = focusedElement(doc);
|
|
130
132
|
const back = returnTo === undefined ? (active instanceof /** @type {Window & typeof globalThis} */ (doc.defaultView).HTMLElement ? active : null) : returnTo;
|
|
131
133
|
|
|
132
134
|
parts.title.textContent = title;
|
package/src/ui/toast.d.ts
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* after a delete: "Deleted “Research” Undo". One per page, made by
|
|
4
4
|
* {@link createToast}; a new message replaces the last and restarts the wait.
|
|
5
5
|
*
|
|
6
|
+
* It's a manual `popover`, so it shows in the top layer: over anything the
|
|
7
|
+
* page stacks (a content script's toast over a site's own fixed bars), and
|
|
8
|
+
* never clipped by an ancestor.
|
|
9
|
+
*
|
|
6
10
|
* @module ui/toast
|
|
7
11
|
*/
|
|
8
12
|
/** How long chrome://bookmarks shows a toast (its cr-toast-manager's `duration`), in ms. */
|
|
@@ -21,13 +25,19 @@ export type ToastOptions = {
|
|
|
21
25
|
*/
|
|
22
26
|
undoDuration?: number;
|
|
23
27
|
/**
|
|
24
|
-
*
|
|
28
|
+
* Where the toast goes: an element, or a shadow root (a content
|
|
29
|
+
* script's). Default the page's `body`.
|
|
30
|
+
*/
|
|
31
|
+
root?: ParentNode;
|
|
32
|
+
/**
|
|
33
|
+
* The page. Default `root`'s document, else `document`.
|
|
25
34
|
*/
|
|
26
35
|
doc?: Document;
|
|
27
36
|
};
|
|
28
37
|
export type Toast = {
|
|
29
38
|
/**
|
|
30
|
-
* The `#toast` element (`role="status"`), in
|
|
39
|
+
* The `#toast` element (`role="status"`, `popover="manual"`), in
|
|
40
|
+
* `root`; `:popover-open` while it shows.
|
|
31
41
|
*/
|
|
32
42
|
element: HTMLElement;
|
|
33
43
|
/**
|
|
@@ -47,27 +57,33 @@ export type Toast = {
|
|
|
47
57
|
* @property {string} [undoLabel] The undo button's text. Default "Undo".
|
|
48
58
|
* @property {number} [duration] How long a toast stays, in ms. Default {@link TOAST_MS}.
|
|
49
59
|
* @property {number} [undoDuration] How long one with Undo stays. Default `duration`.
|
|
50
|
-
* @property {
|
|
60
|
+
* @property {ParentNode} [root] Where the toast goes: an element, or a shadow root (a content
|
|
61
|
+
* script's). Default the page's `body`.
|
|
62
|
+
* @property {Document} [doc] The page. Default `root`'s document, else `document`.
|
|
51
63
|
*/
|
|
52
64
|
/**
|
|
53
65
|
* A page's toast.
|
|
54
66
|
*
|
|
55
67
|
* @typedef {object} Toast
|
|
56
|
-
* @property {HTMLElement} element The `#toast` element (`role="status"`), in
|
|
68
|
+
* @property {HTMLElement} element The `#toast` element (`role="status"`, `popover="manual"`), in
|
|
69
|
+
* `root`; `:popover-open` while it shows.
|
|
57
70
|
* @property {(text: string, undo?: (() => unknown) | null) => void} show Show `text`, with an Undo
|
|
58
71
|
* button that hides the toast and runs `undo` when one is given.
|
|
59
72
|
* @property {() => void} hide Take it down (and its Undo with it).
|
|
60
73
|
*/
|
|
61
74
|
/**
|
|
62
|
-
* Make the page's toast: `<div id="toast" class="toast" role="status"
|
|
63
|
-
* `#toast-text` and an Undo button `#toast-undo`,
|
|
64
|
-
* when the page loads, so screen readers know the
|
|
75
|
+
* Make the page's toast: `<div id="toast" class="toast" role="status"
|
|
76
|
+
* popover="manual">` with `#toast-text` and an Undo button `#toast-undo`,
|
|
77
|
+
* closed until shown. Make it when the page loads, so screen readers know the
|
|
78
|
+
* live region before it speaks.
|
|
65
79
|
*
|
|
66
|
-
* @param {ToastOptions} [options] - The Undo text, how long it stays, and
|
|
80
|
+
* @param {ToastOptions} [options] - The Undo text, how long it stays, and where it goes.
|
|
67
81
|
* @returns {Toast} The toast.
|
|
68
82
|
* @example
|
|
69
83
|
* const toast = createToast();
|
|
70
84
|
* await deleteWorkspace(workspace);
|
|
71
85
|
* toast.show(`Deleted “${workspace.name}”`, () => restore(workspace));
|
|
86
|
+
* // In a content script's shadow root:
|
|
87
|
+
* createToast({ root: host.attachShadow({ mode: 'open' }), duration: 4000 });
|
|
72
88
|
*/
|
|
73
|
-
export declare function createToast({ undoLabel, duration, undoDuration, doc }?: ToastOptions): Toast;
|
|
89
|
+
export declare function createToast({ undoLabel, duration, undoDuration, root, doc }?: ToastOptions): Toast;
|
package/src/ui/toast.js
CHANGED
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
* after a delete: "Deleted “Research” Undo". One per page, made by
|
|
4
4
|
* {@link createToast}; a new message replaces the last and restarts the wait.
|
|
5
5
|
*
|
|
6
|
+
* It's a manual `popover`, so it shows in the top layer: over anything the
|
|
7
|
+
* page stacks (a content script's toast over a site's own fixed bars), and
|
|
8
|
+
* never clipped by an ancestor.
|
|
9
|
+
*
|
|
6
10
|
* @module ui/toast
|
|
7
11
|
*/
|
|
8
12
|
|
|
13
|
+
import { rootDocument } from '../dom/focus.js';
|
|
14
|
+
|
|
9
15
|
/** How long chrome://bookmarks shows a toast (its cr-toast-manager's `duration`), in ms. */
|
|
10
16
|
export const TOAST_MS = 10_000;
|
|
11
17
|
|
|
@@ -16,38 +22,44 @@ export const TOAST_MS = 10_000;
|
|
|
16
22
|
* @property {string} [undoLabel] The undo button's text. Default "Undo".
|
|
17
23
|
* @property {number} [duration] How long a toast stays, in ms. Default {@link TOAST_MS}.
|
|
18
24
|
* @property {number} [undoDuration] How long one with Undo stays. Default `duration`.
|
|
19
|
-
* @property {
|
|
25
|
+
* @property {ParentNode} [root] Where the toast goes: an element, or a shadow root (a content
|
|
26
|
+
* script's). Default the page's `body`.
|
|
27
|
+
* @property {Document} [doc] The page. Default `root`'s document, else `document`.
|
|
20
28
|
*/
|
|
21
29
|
|
|
22
30
|
/**
|
|
23
31
|
* A page's toast.
|
|
24
32
|
*
|
|
25
33
|
* @typedef {object} Toast
|
|
26
|
-
* @property {HTMLElement} element The `#toast` element (`role="status"`), in
|
|
34
|
+
* @property {HTMLElement} element The `#toast` element (`role="status"`, `popover="manual"`), in
|
|
35
|
+
* `root`; `:popover-open` while it shows.
|
|
27
36
|
* @property {(text: string, undo?: (() => unknown) | null) => void} show Show `text`, with an Undo
|
|
28
37
|
* button that hides the toast and runs `undo` when one is given.
|
|
29
38
|
* @property {() => void} hide Take it down (and its Undo with it).
|
|
30
39
|
*/
|
|
31
40
|
|
|
32
41
|
/**
|
|
33
|
-
* Make the page's toast: `<div id="toast" class="toast" role="status"
|
|
34
|
-
* `#toast-text` and an Undo button `#toast-undo`,
|
|
35
|
-
* when the page loads, so screen readers know the
|
|
42
|
+
* Make the page's toast: `<div id="toast" class="toast" role="status"
|
|
43
|
+
* popover="manual">` with `#toast-text` and an Undo button `#toast-undo`,
|
|
44
|
+
* closed until shown. Make it when the page loads, so screen readers know the
|
|
45
|
+
* live region before it speaks.
|
|
36
46
|
*
|
|
37
|
-
* @param {ToastOptions} [options] - The Undo text, how long it stays, and
|
|
47
|
+
* @param {ToastOptions} [options] - The Undo text, how long it stays, and where it goes.
|
|
38
48
|
* @returns {Toast} The toast.
|
|
39
49
|
* @example
|
|
40
50
|
* const toast = createToast();
|
|
41
51
|
* await deleteWorkspace(workspace);
|
|
42
52
|
* toast.show(`Deleted “${workspace.name}”`, () => restore(workspace));
|
|
53
|
+
* // In a content script's shadow root:
|
|
54
|
+
* createToast({ root: host.attachShadow({ mode: 'open' }), duration: 4000 });
|
|
43
55
|
*/
|
|
44
|
-
export function createToast({ undoLabel = 'Undo', duration = TOAST_MS, undoDuration = duration, doc =
|
|
56
|
+
export function createToast({ undoLabel = 'Undo', duration = TOAST_MS, undoDuration = duration, root, doc = rootDocument(root) } = {}) {
|
|
45
57
|
const toast = doc.createElement('div');
|
|
46
58
|
toast.id = 'toast';
|
|
47
59
|
toast.className = 'toast';
|
|
48
60
|
toast.setAttribute('role', 'status');
|
|
49
61
|
toast.setAttribute('aria-live', 'polite');
|
|
50
|
-
toast.
|
|
62
|
+
toast.popover = 'manual';
|
|
51
63
|
const text = doc.createElement('span');
|
|
52
64
|
text.id = 'toast-text';
|
|
53
65
|
const undoButton = doc.createElement('button');
|
|
@@ -56,7 +68,7 @@ export function createToast({ undoLabel = 'Undo', duration = TOAST_MS, undoDurat
|
|
|
56
68
|
undoButton.textContent = undoLabel;
|
|
57
69
|
undoButton.hidden = true;
|
|
58
70
|
toast.append(text, undoButton);
|
|
59
|
-
doc.body.append(toast);
|
|
71
|
+
(root ?? doc.body).append(toast);
|
|
60
72
|
|
|
61
73
|
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
62
74
|
let timer;
|
|
@@ -65,7 +77,7 @@ export function createToast({ undoLabel = 'Undo', duration = TOAST_MS, undoDurat
|
|
|
65
77
|
|
|
66
78
|
const hide = () => {
|
|
67
79
|
clearTimeout(timer);
|
|
68
|
-
toast.
|
|
80
|
+
toast.togglePopover(false);
|
|
69
81
|
undoLast = null;
|
|
70
82
|
};
|
|
71
83
|
undoButton.addEventListener('click', () => {
|
|
@@ -81,7 +93,9 @@ export function createToast({ undoLabel = 'Undo', duration = TOAST_MS, undoDurat
|
|
|
81
93
|
text.textContent = message;
|
|
82
94
|
undoButton.hidden = !undo;
|
|
83
95
|
undoLast = undo;
|
|
84
|
-
|
|
96
|
+
// Hidden and shown again, so it's the newest in the top layer (over a popover since).
|
|
97
|
+
toast.togglePopover(false);
|
|
98
|
+
toast.togglePopover(true);
|
|
85
99
|
timer = setTimeout(hide, undo ? undoDuration : duration);
|
|
86
100
|
},
|
|
87
101
|
hide,
|