@pie-players/pie-players-shared 1.0.0-beta.2 → 1.0.0-beta.4
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/dist/i18n/catalogs.d.ts +5 -0
- package/dist/i18n/catalogs.js +9 -0
- package/dist/i18n/content-language.d.ts +35 -0
- package/dist/i18n/content-language.js +51 -0
- package/dist/i18n/index.d.ts +6 -2
- package/dist/i18n/index.js +5 -2
- package/dist/i18n/language-tags.d.ts +9 -0
- package/dist/i18n/language-tags.js +21 -0
- package/dist/i18n/locale-load-error.d.ts +34 -0
- package/dist/i18n/locale-load-error.js +29 -0
- package/dist/i18n/messages/en-US.d.ts +113 -3
- package/dist/i18n/messages/en-US.js +128 -3
- package/dist/i18n/messages/nl-NL.d.ts +103 -2
- package/dist/i18n/messages/nl-NL.js +109 -8
- package/dist/i18n/provider.d.ts +20 -1
- package/dist/i18n/provider.js +71 -31
- package/dist/i18n/pseudo.d.ts +34 -0
- package/dist/i18n/pseudo.js +106 -0
- package/dist/loaders/esm-adapter.d.ts +10 -0
- package/dist/loaders/esm-adapter.js +88 -15
- package/dist/loaders/index.d.ts +1 -1
- package/dist/loaders/index.js +1 -1
- package/dist/loaders/preloaded-registration.d.ts +18 -6
- package/dist/loaders/preloaded-registration.js +44 -9
- package/dist/pie/index.d.ts +1 -0
- package/dist/pie/math-rendering.d.ts +13 -1
- package/dist/pie/math-rendering.js +54 -1
- package/dist/security/overwide-regions.d.ts +20 -0
- package/dist/security/overwide-regions.js +65 -0
- package/dist/security/wrap-overwide-images.d.ts +6 -2
- package/dist/security/wrap-overwide-images.js +5 -2
- package/dist/security/wrap-overwide-tables.d.ts +6 -2
- package/dist/security/wrap-overwide-tables.js +5 -2
- package/dist/security/wrap-overwide.d.ts +13 -0
- package/dist/security/wrap-overwide.js +68 -3
- package/dist/types/index.d.ts +12 -0
- package/dist/ui/focus-trap.d.ts +12 -0
- package/dist/ui/focus-trap.js +39 -1
- package/dist/ui/overlay-containment.d.ts +13 -0
- package/dist/ui/overlay-containment.js +23 -0
- package/dist/ui/overlay-placement.d.ts +28 -0
- package/dist/ui/overlay-placement.js +17 -2
- package/package.json +4 -4
|
@@ -13,6 +13,16 @@ export interface MathRenderingAPI {
|
|
|
13
13
|
unWrapMath?: (wrapped: string) => string;
|
|
14
14
|
mmlToLatex?: (mathml: string) => string;
|
|
15
15
|
}
|
|
16
|
+
/** How the default renderer `initializeMathRendering` installs loads its assets. */
|
|
17
|
+
export interface MathRenderingOptions {
|
|
18
|
+
/**
|
|
19
|
+
* The npm root the renderer's speech rules load from when the page's math
|
|
20
|
+
* options set no `assetRoot`. With neither, jsDelivr. A page that names the
|
|
21
|
+
* rules' directory itself, in `SREfeature.json` or a `text/x-sre-config`
|
|
22
|
+
* block, keeps it.
|
|
23
|
+
*/
|
|
24
|
+
assetRoot?: string;
|
|
25
|
+
}
|
|
16
26
|
/**
|
|
17
27
|
* The page's renderer: the one `initializeMathRendering` installed, or a
|
|
18
28
|
* host's. IIFE elements render with it, and so do ESM elements when the page
|
|
@@ -27,6 +37,8 @@ export declare const getMathRenderer: () => MathRenderingAPI | null;
|
|
|
27
37
|
* - window["@pie-lib/math-rendering"] (standard key)
|
|
28
38
|
* - window["_dll_pie_lib__math_rendering"] (SystemJS/DLL key for IIFE bundles)
|
|
29
39
|
*
|
|
40
|
+
* The first call installs the renderer for the page, so its `options` apply.
|
|
41
|
+
*
|
|
30
42
|
* @example
|
|
31
43
|
* ```typescript
|
|
32
44
|
* // Default MathJax
|
|
@@ -37,7 +49,7 @@ export declare const getMathRenderer: () => MathRenderingAPI | null;
|
|
|
37
49
|
* await initializeMathRendering();
|
|
38
50
|
* ```
|
|
39
51
|
*/
|
|
40
|
-
export declare function initializeMathRendering(): Promise<void>;
|
|
52
|
+
export declare function initializeMathRendering(options?: MathRenderingOptions): Promise<void>;
|
|
41
53
|
/**
|
|
42
54
|
* Set custom math renderer programmatically
|
|
43
55
|
*
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
/// <reference path="../shims.d.ts" />
|
|
10
10
|
import { createPieLogger, isGlobalDebugEnabled } from "./logger.js";
|
|
11
|
+
import { pageMathAssetRoot } from "./math-assets.js";
|
|
11
12
|
const GLOBAL_KEY = "@pie-lib/math-rendering";
|
|
12
13
|
const GLOBAL_DLL_KEY = "_dll_pie_lib__math_rendering";
|
|
13
14
|
// Every copy of @pie-lib/math-rendering on a page typesets through the MathJax
|
|
@@ -15,6 +16,52 @@ const GLOBAL_DLL_KEY = "_dll_pie_lib__math_rendering";
|
|
|
15
16
|
const SHARED_INSTANCE_KEY = "@pie-lib/math-rendering@2";
|
|
16
17
|
let initPromise = null;
|
|
17
18
|
const logger = createPieLogger("math-rendering", () => isGlobalDebugEnabled());
|
|
19
|
+
/**
|
|
20
|
+
* The speech-rule-engine version the default renderer bundles, whose rules it
|
|
21
|
+
* loads from `speech-rule-engine@<version>/lib/mathmaps` under an npm root. A
|
|
22
|
+
* test pins it to the module.
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_RENDERER_SRE_VERSION = "4.1.2";
|
|
26
|
+
const SRE_FEATURE_KEY = "SREfeature";
|
|
27
|
+
const pageNamesSpeechRules = () => {
|
|
28
|
+
const feature = window[SRE_FEATURE_KEY];
|
|
29
|
+
if (feature && typeof feature === "object" && feature.json)
|
|
30
|
+
return true;
|
|
31
|
+
return Array.from(document.querySelectorAll('script[type="text/x-sre-config"]')).some((block) => {
|
|
32
|
+
try {
|
|
33
|
+
return !!JSON.parse(block.textContent || "")?.json;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Points the module's speech-rule-engine at the rules under `root` and returns
|
|
42
|
+
* the undo. SRE reads `SREfeature` as it sets up, synchronously while the
|
|
43
|
+
* module evaluates, and its later setups keep the path, so the global is
|
|
44
|
+
* restored afterwards: another copy of SRE on the page, as a legacy bundle
|
|
45
|
+
* carries, keeps the rules of its own version.
|
|
46
|
+
*/
|
|
47
|
+
const pointSpeechRulesAt = (root) => {
|
|
48
|
+
if (!root || typeof document === "undefined" || pageNamesSpeechRules()) {
|
|
49
|
+
return () => { };
|
|
50
|
+
}
|
|
51
|
+
const host = window;
|
|
52
|
+
const had = Object.hasOwn(host, SRE_FEATURE_KEY);
|
|
53
|
+
const previous = host[SRE_FEATURE_KEY];
|
|
54
|
+
host[SRE_FEATURE_KEY] = {
|
|
55
|
+
...(previous && typeof previous === "object" ? previous : {}),
|
|
56
|
+
json: `${root.replace(/\/+$/, "")}/speech-rule-engine@${DEFAULT_RENDERER_SRE_VERSION}/lib/mathmaps`,
|
|
57
|
+
};
|
|
58
|
+
return () => {
|
|
59
|
+
if (had)
|
|
60
|
+
host[SRE_FEATURE_KEY] = previous;
|
|
61
|
+
else
|
|
62
|
+
delete host[SRE_FEATURE_KEY];
|
|
63
|
+
};
|
|
64
|
+
};
|
|
18
65
|
/**
|
|
19
66
|
* The page's renderer: the one `initializeMathRendering` installed, or a
|
|
20
67
|
* host's. IIFE elements render with it, and so do ESM elements when the page
|
|
@@ -66,6 +113,8 @@ const createSharedInstance = (renderer) => {
|
|
|
66
113
|
* - window["@pie-lib/math-rendering"] (standard key)
|
|
67
114
|
* - window["_dll_pie_lib__math_rendering"] (SystemJS/DLL key for IIFE bundles)
|
|
68
115
|
*
|
|
116
|
+
* The first call installs the renderer for the page, so its `options` apply.
|
|
117
|
+
*
|
|
69
118
|
* @example
|
|
70
119
|
* ```typescript
|
|
71
120
|
* // Default MathJax
|
|
@@ -76,7 +125,7 @@ const createSharedInstance = (renderer) => {
|
|
|
76
125
|
* await initializeMathRendering();
|
|
77
126
|
* ```
|
|
78
127
|
*/
|
|
79
|
-
export async function initializeMathRendering() {
|
|
128
|
+
export async function initializeMathRendering(options = {}) {
|
|
80
129
|
// Only run in browser
|
|
81
130
|
if (typeof window === "undefined") {
|
|
82
131
|
return;
|
|
@@ -90,6 +139,7 @@ export async function initializeMathRendering() {
|
|
|
90
139
|
return;
|
|
91
140
|
}
|
|
92
141
|
initPromise = (async () => {
|
|
142
|
+
const restoreSpeechRules = pointSpeechRulesAt(pageMathAssetRoot() ?? options.assetRoot);
|
|
93
143
|
try {
|
|
94
144
|
// The package has no exports map, so the file is named in full for
|
|
95
145
|
// resolvers that do not complete a directory, such as webpack's
|
|
@@ -112,6 +162,9 @@ export async function initializeMathRendering() {
|
|
|
112
162
|
logger.error("Failed to initialize math rendering:", error);
|
|
113
163
|
throw error;
|
|
114
164
|
}
|
|
165
|
+
finally {
|
|
166
|
+
restoreSpeechRules();
|
|
167
|
+
}
|
|
115
168
|
})();
|
|
116
169
|
try {
|
|
117
170
|
await initPromise;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps the image and table scroll wrappers under a live root in step with
|
|
3
|
+
* whether they scroll. See {@link syncOverwideRegions}.
|
|
4
|
+
*
|
|
5
|
+
* Overflow changes without any mutation: zoom and a resized column change the
|
|
6
|
+
* wrapper's width, an image load or a dropped drag tile changes its content's.
|
|
7
|
+
* A `ResizeObserver` on each wrapper and its content catches both, and its first
|
|
8
|
+
* delivery for a newly observed target is the initial sync. A wrapper that is
|
|
9
|
+
* not laid out measures zero and syncs as non-scrolling; showing it resizes it,
|
|
10
|
+
* which syncs it again.
|
|
11
|
+
*
|
|
12
|
+
* Without `ResizeObserver` nothing is synced, so every wrapper stays a region —
|
|
13
|
+
* the reachable default.
|
|
14
|
+
*/
|
|
15
|
+
export interface OverwideRegionTracker {
|
|
16
|
+
/** Starts observing every wrapper under the root not yet observed. */
|
|
17
|
+
track(): void;
|
|
18
|
+
disconnect(): void;
|
|
19
|
+
}
|
|
20
|
+
export declare function createOverwideRegionTracker(root: Element): OverwideRegionTracker;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps the image and table scroll wrappers under a live root in step with
|
|
3
|
+
* whether they scroll. See {@link syncOverwideRegions}.
|
|
4
|
+
*
|
|
5
|
+
* Overflow changes without any mutation: zoom and a resized column change the
|
|
6
|
+
* wrapper's width, an image load or a dropped drag tile changes its content's.
|
|
7
|
+
* A `ResizeObserver` on each wrapper and its content catches both, and its first
|
|
8
|
+
* delivery for a newly observed target is the initial sync. A wrapper that is
|
|
9
|
+
* not laid out measures zero and syncs as non-scrolling; showing it resizes it,
|
|
10
|
+
* which syncs it again.
|
|
11
|
+
*
|
|
12
|
+
* Without `ResizeObserver` nothing is synced, so every wrapper stays a region —
|
|
13
|
+
* the reachable default.
|
|
14
|
+
*/
|
|
15
|
+
import { IMAGE_SPEC } from "./wrap-overwide-images.js";
|
|
16
|
+
import { TABLE_SPEC } from "./wrap-overwide-tables.js";
|
|
17
|
+
import { syncOverwideRegions } from "./wrap-overwide.js";
|
|
18
|
+
const SPECS = [IMAGE_SPEC, TABLE_SPEC];
|
|
19
|
+
const WRAPPER_SELECTOR = SPECS.map((spec) => `.${spec.wrapperClass}`).join(",");
|
|
20
|
+
export function createOverwideRegionTracker(root) {
|
|
21
|
+
if (typeof ResizeObserver === "undefined") {
|
|
22
|
+
return { track() { }, disconnect() { } };
|
|
23
|
+
}
|
|
24
|
+
// Wrapper to the content observed with it. Held strongly, as the observer
|
|
25
|
+
// holds its targets, so each `track()` releases wrappers that have left the
|
|
26
|
+
// document — an element re-rendering over its subtree drops them every time.
|
|
27
|
+
const observed = new Map();
|
|
28
|
+
const observer = new ResizeObserver((entries) => {
|
|
29
|
+
const wrappers = new Set();
|
|
30
|
+
for (const { target } of entries) {
|
|
31
|
+
const wrapper = target.matches(WRAPPER_SELECTOR)
|
|
32
|
+
? target
|
|
33
|
+
: target.parentElement;
|
|
34
|
+
if (wrapper)
|
|
35
|
+
wrappers.add(wrapper);
|
|
36
|
+
}
|
|
37
|
+
for (const spec of SPECS)
|
|
38
|
+
syncOverwideRegions(wrappers, spec);
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
track() {
|
|
42
|
+
for (const [wrapper, content] of observed) {
|
|
43
|
+
if (wrapper.isConnected)
|
|
44
|
+
continue;
|
|
45
|
+
observer.unobserve(wrapper);
|
|
46
|
+
if (content)
|
|
47
|
+
observer.unobserve(content);
|
|
48
|
+
observed.delete(wrapper);
|
|
49
|
+
}
|
|
50
|
+
for (const wrapper of Array.from(root.querySelectorAll(WRAPPER_SELECTOR))) {
|
|
51
|
+
if (observed.has(wrapper))
|
|
52
|
+
continue;
|
|
53
|
+
const content = wrapper.firstElementChild;
|
|
54
|
+
observed.set(wrapper, content);
|
|
55
|
+
observer.observe(wrapper);
|
|
56
|
+
if (content)
|
|
57
|
+
observer.observe(content);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
disconnect() {
|
|
61
|
+
observer.disconnect();
|
|
62
|
+
observed.clear();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* The wrapper is rendered as
|
|
7
7
|
* `<span class="pie-image-scroll" tabindex="0" role="region" aria-label="...">`
|
|
8
|
-
* and receives the accompanying CSS from `@pie-players/pie-theme`.
|
|
8
|
+
* and receives the accompanying CSS from `@pie-players/pie-theme`.
|
|
9
|
+
* The live player keeps the region attributes only while the wrapper actually
|
|
10
|
+
* scrolls (see `./overwide-regions.js`), so a image that fits adds no tab stop. The CSS uses
|
|
9
11
|
* `overflow-x: auto` so small images stay visually unchanged: a scrollbar only
|
|
10
12
|
* appears when the image's intrinsic width exceeds the wrapper's available
|
|
11
13
|
* space (including at higher browser-zoom levels, which is the original driver
|
|
@@ -26,7 +28,9 @@
|
|
|
26
28
|
* The wrapping itself lives in `./wrap-overwide.js`, shared with the table
|
|
27
29
|
* wrapper: only the spec below differs.
|
|
28
30
|
*/
|
|
29
|
-
import { type WrapOverwideOptions } from "./wrap-overwide.js";
|
|
31
|
+
import { type OverwideWrapSpec, type WrapOverwideOptions } from "./wrap-overwide.js";
|
|
32
|
+
/** Internal: shared with the region tracker, not re-exported. */
|
|
33
|
+
export declare const IMAGE_SPEC: OverwideWrapSpec;
|
|
30
34
|
export type WrapOverwideImagesInElementOptions = WrapOverwideOptions;
|
|
31
35
|
/**
|
|
32
36
|
* Wrap every unwrapped `<img>` descendant of `root` with the shared
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* The wrapper is rendered as
|
|
7
7
|
* `<span class="pie-image-scroll" tabindex="0" role="region" aria-label="...">`
|
|
8
|
-
* and receives the accompanying CSS from `@pie-players/pie-theme`.
|
|
8
|
+
* and receives the accompanying CSS from `@pie-players/pie-theme`.
|
|
9
|
+
* The live player keeps the region attributes only while the wrapper actually
|
|
10
|
+
* scrolls (see `./overwide-regions.js`), so a image that fits adds no tab stop. The CSS uses
|
|
9
11
|
* `overflow-x: auto` so small images stay visually unchanged: a scrollbar only
|
|
10
12
|
* appears when the image's intrinsic width exceeds the wrapper's available
|
|
11
13
|
* space (including at higher browser-zoom levels, which is the original driver
|
|
@@ -50,7 +52,8 @@ function imageLayoutClass(image) {
|
|
|
50
52
|
display !== "contents";
|
|
51
53
|
return blockLevel ? "pie-image-scroll-block" : null;
|
|
52
54
|
}
|
|
53
|
-
|
|
55
|
+
/** Internal: shared with the region tracker, not re-exported. */
|
|
56
|
+
export const IMAGE_SPEC = {
|
|
54
57
|
selector: "img",
|
|
55
58
|
wrapperTag: "span",
|
|
56
59
|
wrapperClass: "pie-image-scroll",
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* The wrapper is rendered as
|
|
7
7
|
* `<div class="pie-table-scroll" tabindex="0" role="region" aria-label="...">`
|
|
8
|
-
* and receives the accompanying CSS from `@pie-players/pie-theme`.
|
|
8
|
+
* and receives the accompanying CSS from `@pie-players/pie-theme`.
|
|
9
|
+
* The live player keeps the region attributes only while the wrapper actually
|
|
10
|
+
* scrolls (see `./overwide-regions.js`), so a table that fits adds no tab stop. The CSS uses
|
|
9
11
|
* `overflow-x: auto` so narrow tables stay visually unchanged: a scrollbar only
|
|
10
12
|
* appears when the table's intrinsic width exceeds the wrapper's available
|
|
11
13
|
* space (including at higher browser-zoom levels — WCAG 1.4.10 Reflow at 400%
|
|
@@ -26,7 +28,9 @@
|
|
|
26
28
|
* The wrapping itself lives in `./wrap-overwide.js`, shared with the image
|
|
27
29
|
* wrapper: only the four values below and the accessible name differ.
|
|
28
30
|
*/
|
|
29
|
-
import { type WrapOverwideOptions } from "./wrap-overwide.js";
|
|
31
|
+
import { type OverwideWrapSpec, type WrapOverwideOptions } from "./wrap-overwide.js";
|
|
32
|
+
/** Internal: shared with the region tracker, not re-exported. */
|
|
33
|
+
export declare const TABLE_SPEC: OverwideWrapSpec;
|
|
30
34
|
export type WrapOverwideTablesInElementOptions = WrapOverwideOptions;
|
|
31
35
|
/**
|
|
32
36
|
* Wrap every unwrapped `<table>` descendant of `root` with the shared
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* The wrapper is rendered as
|
|
7
7
|
* `<div class="pie-table-scroll" tabindex="0" role="region" aria-label="...">`
|
|
8
|
-
* and receives the accompanying CSS from `@pie-players/pie-theme`.
|
|
8
|
+
* and receives the accompanying CSS from `@pie-players/pie-theme`.
|
|
9
|
+
* The live player keeps the region attributes only while the wrapper actually
|
|
10
|
+
* scrolls (see `./overwide-regions.js`), so a table that fits adds no tab stop. The CSS uses
|
|
9
11
|
* `overflow-x: auto` so narrow tables stay visually unchanged: a scrollbar only
|
|
10
12
|
* appears when the table's intrinsic width exceeds the wrapper's available
|
|
11
13
|
* space (including at higher browser-zoom levels — WCAG 1.4.10 Reflow at 400%
|
|
@@ -57,7 +59,8 @@ function buildAriaLabel(table) {
|
|
|
57
59
|
}
|
|
58
60
|
return "Scrollable table";
|
|
59
61
|
}
|
|
60
|
-
|
|
62
|
+
/** Internal: shared with the region tracker, not re-exported. */
|
|
63
|
+
export const TABLE_SPEC = {
|
|
61
64
|
selector: "table",
|
|
62
65
|
wrapperTag: "div",
|
|
63
66
|
wrapperClass: "pie-table-scroll",
|
|
@@ -55,6 +55,19 @@ export interface WrapOverwideOptions {
|
|
|
55
55
|
* the layout class this pass would have given it.
|
|
56
56
|
*/
|
|
57
57
|
export declare function wrapOverwideInElement(root: Element, spec: OverwideWrapSpec, options?: WrapOverwideOptions): number;
|
|
58
|
+
/**
|
|
59
|
+
* Makes each wrapper a named, focusable region while its content overflows it
|
|
60
|
+
* horizontally, and a plain container otherwise.
|
|
61
|
+
*
|
|
62
|
+
* A wrapper is built as a region, because a wrapper that scrolls must be
|
|
63
|
+
* reachable by keyboard (WCAG 2.1.1) and markup wrapped off-document cannot be
|
|
64
|
+
* measured. Most never scroll: a table that fits its column, a drag tile, a
|
|
65
|
+
* layout table. Left as regions they add a tab stop and a landmark each, so the
|
|
66
|
+
* live player measures every wrapper and keeps the region only where there is
|
|
67
|
+
* something to scroll. Measurements are read for every wrapper before the first
|
|
68
|
+
* write. A wrapper holding focus keeps its region, so a sync never drops focus.
|
|
69
|
+
*/
|
|
70
|
+
export declare function syncOverwideRegions(wrappers: Iterable<Element>, spec: OverwideWrapSpec): void;
|
|
58
71
|
/**
|
|
59
72
|
* String-in / string-out wrapping, for the post-sanitization step.
|
|
60
73
|
*
|
|
@@ -40,6 +40,29 @@ function isInsidePieCustomElement(node, root) {
|
|
|
40
40
|
}
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
|
+
const VERTICAL_MARGINS = ["margin-top", "margin-bottom"];
|
|
44
|
+
/**
|
|
45
|
+
* Moves the node's inline vertical margins onto its wrapper. The wrapper clips
|
|
46
|
+
* vertically — `overflow-x: auto` forces `overflow-y` off `visible` — so a
|
|
47
|
+
* negative margin left on the node pulls its edge into the clipped band, and
|
|
48
|
+
* the wrapper's own block formatting context stops a positive one collapsing
|
|
49
|
+
* with its neighbours. On the wrapper, both lay out as they did unwrapped.
|
|
50
|
+
* Horizontal margins stay: `margin: 0 auto` centres the node inside the wrapper.
|
|
51
|
+
*/
|
|
52
|
+
function liftVerticalMargins(node, wrapper) {
|
|
53
|
+
const style = node.style;
|
|
54
|
+
if (!style)
|
|
55
|
+
return;
|
|
56
|
+
for (const property of VERTICAL_MARGINS) {
|
|
57
|
+
const value = style.getPropertyValue(property);
|
|
58
|
+
if (!value)
|
|
59
|
+
continue;
|
|
60
|
+
wrapper.style.setProperty(property, value, style.getPropertyPriority(property));
|
|
61
|
+
style.removeProperty(property);
|
|
62
|
+
}
|
|
63
|
+
if (!style.length)
|
|
64
|
+
node.removeAttribute("style");
|
|
65
|
+
}
|
|
43
66
|
/**
|
|
44
67
|
* Wrappers whose layout class was read from the live cascade. A wrapper built
|
|
45
68
|
* off-document saw only its node's inline style, so the first live pass to meet
|
|
@@ -109,17 +132,59 @@ export function wrapOverwideInElement(root, spec, options = {}) {
|
|
|
109
132
|
wrapper.className = layoutClass
|
|
110
133
|
? `${spec.wrapperClass} ${layoutClass}`
|
|
111
134
|
: spec.wrapperClass;
|
|
112
|
-
wrapper
|
|
113
|
-
wrapper.setAttribute("role", "region");
|
|
114
|
-
wrapper.setAttribute("aria-label", spec.buildAriaLabel(node));
|
|
135
|
+
setRegion(wrapper, node, spec);
|
|
115
136
|
if (readsLiveLayout)
|
|
116
137
|
liveLayoutRead.add(wrapper);
|
|
138
|
+
liftVerticalMargins(node, wrapper);
|
|
117
139
|
parent.insertBefore(wrapper, node);
|
|
118
140
|
wrapper.appendChild(node);
|
|
119
141
|
wrapped += 1;
|
|
120
142
|
}
|
|
121
143
|
return wrapped;
|
|
122
144
|
}
|
|
145
|
+
function setRegion(wrapper, node, spec) {
|
|
146
|
+
wrapper.setAttribute("tabindex", "0");
|
|
147
|
+
wrapper.setAttribute("role", "region");
|
|
148
|
+
wrapper.setAttribute("aria-label", spec.buildAriaLabel(node));
|
|
149
|
+
}
|
|
150
|
+
function clearRegion(wrapper) {
|
|
151
|
+
wrapper.removeAttribute("tabindex");
|
|
152
|
+
wrapper.removeAttribute("role");
|
|
153
|
+
wrapper.removeAttribute("aria-label");
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Makes each wrapper a named, focusable region while its content overflows it
|
|
157
|
+
* horizontally, and a plain container otherwise.
|
|
158
|
+
*
|
|
159
|
+
* A wrapper is built as a region, because a wrapper that scrolls must be
|
|
160
|
+
* reachable by keyboard (WCAG 2.1.1) and markup wrapped off-document cannot be
|
|
161
|
+
* measured. Most never scroll: a table that fits its column, a drag tile, a
|
|
162
|
+
* layout table. Left as regions they add a tab stop and a landmark each, so the
|
|
163
|
+
* live player measures every wrapper and keeps the region only where there is
|
|
164
|
+
* something to scroll. Measurements are read for every wrapper before the first
|
|
165
|
+
* write. A wrapper holding focus keeps its region, so a sync never drops focus.
|
|
166
|
+
*/
|
|
167
|
+
export function syncOverwideRegions(wrappers, spec) {
|
|
168
|
+
const measured = [];
|
|
169
|
+
for (const wrapper of wrappers) {
|
|
170
|
+
if (!wrapper.isConnected)
|
|
171
|
+
continue;
|
|
172
|
+
if (!wrapper.classList.contains(spec.wrapperClass))
|
|
173
|
+
continue;
|
|
174
|
+
measured.push([wrapper, wrapper.scrollWidth > wrapper.clientWidth]);
|
|
175
|
+
}
|
|
176
|
+
for (const [wrapper, scrolls] of measured) {
|
|
177
|
+
const node = wrapper.firstElementChild;
|
|
178
|
+
if (scrolls && node?.matches(spec.selector))
|
|
179
|
+
setRegion(wrapper, node, spec);
|
|
180
|
+
else if (!scrolls && !isFocused(wrapper))
|
|
181
|
+
clearRegion(wrapper);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function isFocused(element) {
|
|
185
|
+
const root = element.getRootNode();
|
|
186
|
+
return root.activeElement === element;
|
|
187
|
+
}
|
|
123
188
|
/**
|
|
124
189
|
* String-in / string-out wrapping, for the post-sanitization step.
|
|
125
190
|
*
|
package/dist/types/index.d.ts
CHANGED
|
@@ -557,6 +557,10 @@ export interface ToolParameterMap {
|
|
|
557
557
|
pitch?: number;
|
|
558
558
|
volume?: number;
|
|
559
559
|
};
|
|
560
|
+
/** `zoom` is the lens's starting magnification, 2 meaning twice the size. */
|
|
561
|
+
magnification: {
|
|
562
|
+
zoom?: number;
|
|
563
|
+
};
|
|
560
564
|
}
|
|
561
565
|
/** The parameter shape of tool `K`: its {@link ToolParameterMap} entry, else open. */
|
|
562
566
|
export type ToolParametersFor<K extends string> = K extends keyof ToolParameterMap ? ToolParameterMap[K] : Record<string, unknown>;
|
|
@@ -582,6 +586,14 @@ export interface Env {
|
|
|
582
586
|
mode: PlayerMode;
|
|
583
587
|
role: PlayerRole;
|
|
584
588
|
partialScoring?: boolean;
|
|
589
|
+
/**
|
|
590
|
+
* BCP 47 tag of the language the item's content is written in, when the
|
|
591
|
+
* delivery knows it. The player reflects it onto the content subtree as
|
|
592
|
+
* `lang`/`dir`; a host that emits nothing leaves the content unstamped.
|
|
593
|
+
* The interface locale never travels here — it is the player's `locale`
|
|
594
|
+
* attribute and the `pie.interfaceLocale` context.
|
|
595
|
+
*/
|
|
596
|
+
locale?: string;
|
|
585
597
|
}
|
|
586
598
|
/** The `detail` of the `player-error` event `pie-item-player` dispatches. */
|
|
587
599
|
export interface PieItemPlayerErrorDetail {
|
package/dist/ui/focus-trap.d.ts
CHANGED
|
@@ -24,4 +24,16 @@ type FocusTrapOptions = {
|
|
|
24
24
|
* and restores prior focus when the trap is removed.
|
|
25
25
|
*/
|
|
26
26
|
export declare function createFocusTrap(container: HTMLElement, options?: FocusTrapOptions): () => void;
|
|
27
|
+
/**
|
|
28
|
+
* Returns focus to where it was before a tool took it, for a tool that closes
|
|
29
|
+
* itself: the tool's element leaves the DOM on close, which drops focus to the
|
|
30
|
+
* document body. `capture` runs as the tool takes focus and `restore` as it
|
|
31
|
+
* closes, before its element goes.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createFocusReturn(): {
|
|
34
|
+
/** Records the focused element, unless focus is already inside `container`. */
|
|
35
|
+
capture: (container: Node) => void;
|
|
36
|
+
/** Focuses the recorded element, if it is still in the document. */
|
|
37
|
+
restore: () => void;
|
|
38
|
+
};
|
|
27
39
|
export {};
|
package/dist/ui/focus-trap.js
CHANGED
|
@@ -44,7 +44,11 @@ export function createFocusTrap(container, options = {}) {
|
|
|
44
44
|
const wrap = options.wrap ?? true;
|
|
45
45
|
const onKeydown = (event) => {
|
|
46
46
|
if (event.key === "Escape") {
|
|
47
|
-
options.onEscape
|
|
47
|
+
if (!options.onEscape)
|
|
48
|
+
return;
|
|
49
|
+
// Claimed, so a page-wide Escape listener does not also act on it.
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
options.onEscape();
|
|
48
52
|
return;
|
|
49
53
|
}
|
|
50
54
|
if (event.key !== "Tab")
|
|
@@ -94,3 +98,37 @@ export function createFocusTrap(container, options = {}) {
|
|
|
94
98
|
}
|
|
95
99
|
};
|
|
96
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Returns focus to where it was before a tool took it, for a tool that closes
|
|
103
|
+
* itself: the tool's element leaves the DOM on close, which drops focus to the
|
|
104
|
+
* document body. `capture` runs as the tool takes focus and `restore` as it
|
|
105
|
+
* closes, before its element goes.
|
|
106
|
+
*/
|
|
107
|
+
export function createFocusReturn() {
|
|
108
|
+
let opener = null;
|
|
109
|
+
return {
|
|
110
|
+
capture(container) {
|
|
111
|
+
if (typeof document === "undefined")
|
|
112
|
+
return;
|
|
113
|
+
const active = getDeepActiveElement(document);
|
|
114
|
+
if (active && container.contains(active))
|
|
115
|
+
return;
|
|
116
|
+
opener =
|
|
117
|
+
active instanceof HTMLElement && active !== document.body
|
|
118
|
+
? active
|
|
119
|
+
: null;
|
|
120
|
+
},
|
|
121
|
+
restore() {
|
|
122
|
+
const target = opener;
|
|
123
|
+
opener = null;
|
|
124
|
+
if (!target?.isConnected)
|
|
125
|
+
return;
|
|
126
|
+
try {
|
|
127
|
+
target.focus();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// ignore
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -17,6 +17,11 @@ export interface Point {
|
|
|
17
17
|
x: number;
|
|
18
18
|
y: number;
|
|
19
19
|
}
|
|
20
|
+
/** A box on screen: its top-left corner and its extent, as a `DOMRect` gives them. */
|
|
21
|
+
export interface ScreenBox extends Size {
|
|
22
|
+
left: number;
|
|
23
|
+
top: number;
|
|
24
|
+
}
|
|
20
25
|
/**
|
|
21
26
|
* The box `element` resolves its `left`/`top` against: its containing block, or
|
|
22
27
|
* the viewport when that is the initial containing block.
|
|
@@ -77,3 +82,11 @@ export declare function uprightDockCentre(box: Size, strip: Size, degrees: numbe
|
|
|
77
82
|
* where a box exceeds its block and centres on the offending axis.
|
|
78
83
|
*/
|
|
79
84
|
export declare function clampPointWithinBlock(point: Point, box: Size, block: Size, gutter?: number): Point;
|
|
85
|
+
/**
|
|
86
|
+
* The top-left corner, in viewport coordinates, of a control strip docked
|
|
87
|
+
* beside an upright `tool`: centred below it, or above it when only the space
|
|
88
|
+
* above holds the strip, and slid along the tool's edge to stay inside the
|
|
89
|
+
* block. A strip that fits on neither side takes the roomier one; a strip wider
|
|
90
|
+
* than the block centres on it.
|
|
91
|
+
*/
|
|
92
|
+
export declare function uprightDockCorner(tool: ScreenBox, strip: Size, block: ScreenBox, gap: number, gutter?: number): Point;
|
|
@@ -126,3 +126,26 @@ export function clampPointWithinBlock(point, box, block, gutter = DEFAULT_CONTAI
|
|
|
126
126
|
const offset = clampOffsetWithinBlock({ x: point.x - centre.x, y: point.y - centre.y }, box, block, gutter);
|
|
127
127
|
return { x: centre.x + offset.x, y: centre.y + offset.y };
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* The top-left corner, in viewport coordinates, of a control strip docked
|
|
131
|
+
* beside an upright `tool`: centred below it, or above it when only the space
|
|
132
|
+
* above holds the strip, and slid along the tool's edge to stay inside the
|
|
133
|
+
* block. A strip that fits on neither side takes the roomier one; a strip wider
|
|
134
|
+
* than the block centres on it.
|
|
135
|
+
*/
|
|
136
|
+
export function uprightDockCorner(tool, strip, block, gap, gutter = DEFAULT_CONTAINMENT_GUTTER) {
|
|
137
|
+
const top = block.top + gutter;
|
|
138
|
+
const bottom = block.top + block.height - gutter;
|
|
139
|
+
const below = tool.top + tool.height + gap;
|
|
140
|
+
const above = tool.top - gap - strip.height;
|
|
141
|
+
const roomBelow = bottom - below;
|
|
142
|
+
const roomAbove = tool.top - gap - top;
|
|
143
|
+
const y = roomBelow >= strip.height || (roomAbove < strip.height && roomBelow >= roomAbove)
|
|
144
|
+
? below
|
|
145
|
+
: above;
|
|
146
|
+
const travel = block.width - gutter * 2 - strip.width;
|
|
147
|
+
const x = travel <= 0
|
|
148
|
+
? block.left + (block.width - strip.width) / 2
|
|
149
|
+
: Math.max(block.left + gutter, Math.min(block.left + gutter + travel, tool.left + (tool.width - strip.width) / 2));
|
|
150
|
+
return { x, y };
|
|
151
|
+
}
|
|
@@ -53,6 +53,34 @@ export type OverlayPlacementNudge = keyof typeof OVERLAY_PLACEMENT_NUDGES;
|
|
|
53
53
|
export type OverlayPlacementAnnouncement = (typeof OVERLAY_PLACEMENT_NUDGES)[OverlayPlacementNudge]["announceKey"] | "toolkit.announce.rotatedTo";
|
|
54
54
|
/** The tap controls' rotations, the keyboard's two steps each way. */
|
|
55
55
|
export declare const OVERLAY_PLACEMENT_ROTATIONS: readonly [number, number, 1, 5];
|
|
56
|
+
/**
|
|
57
|
+
* What the tap controls drive: a step for each arrow, and a turn for a tool
|
|
58
|
+
* that rotates. An `OverlayPlacementController` is one; an upright tool that
|
|
59
|
+
* keeps its own position supplies `nudge` alone.
|
|
60
|
+
*/
|
|
61
|
+
export interface OverlayTapTarget {
|
|
62
|
+
nudge: (direction: OverlayPlacementNudge) => void;
|
|
63
|
+
rotateBy?: (degrees: number) => void;
|
|
64
|
+
}
|
|
65
|
+
/** A shrink-and-grow pair on the tap controls, labelled by the tool. */
|
|
66
|
+
export interface OverlaySizeControl {
|
|
67
|
+
shrink: {
|
|
68
|
+
glyph: string;
|
|
69
|
+
label: string;
|
|
70
|
+
};
|
|
71
|
+
grow: {
|
|
72
|
+
glyph: string;
|
|
73
|
+
label: string;
|
|
74
|
+
};
|
|
75
|
+
step: (direction: 1 | -1) => void;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Docks an upright tool's tap controls beside it, as `uprightDockCorner`
|
|
79
|
+
* places them. `OverlayPlacementControls` lays the controls out at the tool's
|
|
80
|
+
* top-left corner, so the offset is taken from there. A strip wider than the
|
|
81
|
+
* block wraps onto more rows, so a phone-width pane does not clip it.
|
|
82
|
+
*/
|
|
83
|
+
export declare function dockUprightControls(element: HTMLElement, controls: HTMLElement): void;
|
|
56
84
|
export interface OverlayPlacementOptions {
|
|
57
85
|
/** The tool's positioned element, which receives the transform. */
|
|
58
86
|
getElement: () => HTMLElement | undefined;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* its `style.transform`, so a drag, an arrow key and a tap control always agree
|
|
5
5
|
* about where the tool is.
|
|
6
6
|
*/
|
|
7
|
-
import { clampOffsetOverlappingBlock, resolveContainingBlockRect, rotatedExtent, rotationCentreShift, uprightDockCentre, } from "./overlay-containment.js";
|
|
7
|
+
import { clampOffsetOverlappingBlock, DEFAULT_CONTAINMENT_GUTTER, resolveContainingBlockRect, rotatedExtent, rotationCentreShift, uprightDockCentre, uprightDockCorner, } from "./overlay-containment.js";
|
|
8
8
|
import { createPointerDragController } from "./pointer-drag.js";
|
|
9
9
|
import { createPointerGesture } from "./pointer-gesture.js";
|
|
10
10
|
import { createPointerRotateController } from "./pointer-rotate.js";
|
|
@@ -60,8 +60,23 @@ const ARROW_KEYS = {
|
|
|
60
60
|
};
|
|
61
61
|
/** Pixels of the tool kept inside its containing block on each axis. */
|
|
62
62
|
const MIN_VISIBLE = 100;
|
|
63
|
-
/** Space between the tool
|
|
63
|
+
/** Space between the tool and its tap controls. */
|
|
64
64
|
const CONTROLS_GAP = 8;
|
|
65
|
+
/**
|
|
66
|
+
* Docks an upright tool's tap controls beside it, as `uprightDockCorner`
|
|
67
|
+
* places them. `OverlayPlacementControls` lays the controls out at the tool's
|
|
68
|
+
* top-left corner, so the offset is taken from there. A strip wider than the
|
|
69
|
+
* block wraps onto more rows, so a phone-width pane does not clip it.
|
|
70
|
+
*/
|
|
71
|
+
export function dockUprightControls(element, controls) {
|
|
72
|
+
const block = resolveContainingBlockRect(element);
|
|
73
|
+
if (!block)
|
|
74
|
+
return;
|
|
75
|
+
controls.style.maxWidth = `${Math.max(0, block.width - DEFAULT_CONTAINMENT_GUTTER * 2)}px`;
|
|
76
|
+
const tool = element.getBoundingClientRect();
|
|
77
|
+
const corner = uprightDockCorner(tool, { width: controls.offsetWidth, height: controls.offsetHeight }, block, CONTROLS_GAP);
|
|
78
|
+
controls.style.transform = `translate(${corner.x - tool.left - element.clientLeft}px, ${corner.y - tool.top - element.clientTop}px)`;
|
|
79
|
+
}
|
|
65
80
|
export function createOverlayPlacement(options) {
|
|
66
81
|
let placement = { x: 0, y: 0, rotation: 0 };
|
|
67
82
|
const sizeOf = (element) => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pie-players/pie-players-shared",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared runtime + UI utilities for PIE players",
|
|
6
6
|
"license": "MIT",
|
|
@@ -106,9 +106,9 @@
|
|
|
106
106
|
"dist"
|
|
107
107
|
],
|
|
108
108
|
"dependencies": {
|
|
109
|
-
"@pie-element/shared-utils": "0.1.
|
|
109
|
+
"@pie-element/shared-utils": "0.1.4",
|
|
110
110
|
"@pie-lib/math-rendering-module": "6.0.0",
|
|
111
|
-
"dompurify": "^3.4.
|
|
111
|
+
"dompurify": "^3.4.17",
|
|
112
112
|
"semver": "^7.8.5"
|
|
113
113
|
},
|
|
114
114
|
"scripts": {
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"@biomejs/biome": "^2.5.15",
|
|
126
126
|
"@happy-dom/global-registrator": "^20.14.5",
|
|
127
127
|
"@pie-element/shared-math-rendering-mathjax": "0.1.4",
|
|
128
|
-
"@playwright/test": "^1.64.
|
|
128
|
+
"@playwright/test": "^1.64.1",
|
|
129
129
|
"@types/semver": "^7.8.0",
|
|
130
130
|
"es-module-shims": "^2.8.4",
|
|
131
131
|
"esbuild": "^0.28.2",
|