@vectojs/core 1.6.1 → 1.7.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 +5 -0
- package/dist/index.js +30 -18
- package/dist/index.mjs +30 -18
- package/dist/tree/Entity.d.ts +10 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,6 +112,11 @@ This projection is intentionally thin. Applications still own accessible names,
|
|
|
112
112
|
focus order, contrast, and correct control semantics. See the
|
|
113
113
|
[accessibility guide](https://vectojs.org/learn/accessibility/).
|
|
114
114
|
|
|
115
|
+
Non-control workspaces that own keyboard shortcuts can opt into focus order
|
|
116
|
+
explicitly: return `{ role: 'region', label: 'Canvas workspace', tabIndex: 0 }`.
|
|
117
|
+
The Scene refreshes the projected `tabindex` when attributes change. Keep native
|
|
118
|
+
text inputs and editable content in charge of their own editing shortcuts.
|
|
119
|
+
|
|
115
120
|
Interactive projected nodes capture the active pointer on `pointerdown` and route
|
|
116
121
|
`pointermove`, `pointerup`, and `pointercancel` through normal VMT capture/bubble propagation.
|
|
117
122
|
Treat `pointercancel` as rollback: discard transient gesture state and do not create durable
|
package/dist/index.js
CHANGED
|
@@ -297,6 +297,20 @@ var ComputeParticleEntity = (_class = class extends _chunkDFNK6OV6js.Entity {
|
|
|
297
297
|
}, _class);
|
|
298
298
|
|
|
299
299
|
// src/tree/Scene.ts
|
|
300
|
+
var INTERACTIVE_A11Y_ROLES = /* @__PURE__ */ new Set([
|
|
301
|
+
"button",
|
|
302
|
+
"switch",
|
|
303
|
+
"checkbox",
|
|
304
|
+
"radio",
|
|
305
|
+
"link",
|
|
306
|
+
"tab",
|
|
307
|
+
"menuitem",
|
|
308
|
+
"slider",
|
|
309
|
+
"combobox"
|
|
310
|
+
]);
|
|
311
|
+
function isNativelyFocusable(element) {
|
|
312
|
+
return element instanceof HTMLButtonElement || element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement || element instanceof HTMLAnchorElement && element.hasAttribute("href");
|
|
313
|
+
}
|
|
300
314
|
var REDUCED_MOTION_FPS = 30;
|
|
301
315
|
function parseInlinePx(value) {
|
|
302
316
|
if (!value || !value.endsWith("px")) return null;
|
|
@@ -910,20 +924,7 @@ var Scene = (_class2 = class _Scene {
|
|
|
910
924
|
}
|
|
911
925
|
node.emit("blur", {});
|
|
912
926
|
});
|
|
913
|
-
|
|
914
|
-
"button",
|
|
915
|
-
"switch",
|
|
916
|
-
"checkbox",
|
|
917
|
-
"radio",
|
|
918
|
-
"link",
|
|
919
|
-
"tab",
|
|
920
|
-
"menuitem",
|
|
921
|
-
"slider",
|
|
922
|
-
"combobox"
|
|
923
|
-
]);
|
|
924
|
-
const nativelyFocusable = el instanceof HTMLButtonElement || el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement || el instanceof HTMLAnchorElement && el.hasAttribute("href");
|
|
925
|
-
if (!nativelyFocusable && attrs.role && INTERACTIVE_ROLES.has(attrs.role)) {
|
|
926
|
-
el.setAttribute("tabindex", "0");
|
|
927
|
+
if (!isNativelyFocusable(el) && attrs.role && INTERACTIVE_A11Y_ROLES.has(attrs.role)) {
|
|
927
928
|
el.addEventListener("keydown", (e) => {
|
|
928
929
|
if (e.key === "Enter" || e.key === " ") {
|
|
929
930
|
e.preventDefault();
|
|
@@ -945,6 +946,13 @@ var Scene = (_class2 = class _Scene {
|
|
|
945
946
|
if (attrs.label !== void 0 && el.getAttribute("aria-label") !== attrs.label) {
|
|
946
947
|
el.setAttribute("aria-label", attrs.label);
|
|
947
948
|
}
|
|
949
|
+
const implicitTabIndex = !isNativelyFocusable(el) && attrs.role && INTERACTIVE_A11Y_ROLES.has(attrs.role) ? 0 : null;
|
|
950
|
+
const desiredTabIndex = _nullishCoalesce(attrs.tabIndex, () => ( implicitTabIndex));
|
|
951
|
+
if (desiredTabIndex === null) {
|
|
952
|
+
if (el.hasAttribute("tabindex")) el.removeAttribute("tabindex");
|
|
953
|
+
} else if (el.getAttribute("tabindex") !== String(desiredTabIndex)) {
|
|
954
|
+
el.setAttribute("tabindex", String(desiredTabIndex));
|
|
955
|
+
}
|
|
948
956
|
if (attrs.inputType !== void 0 && el.getAttribute("type") !== attrs.inputType) {
|
|
949
957
|
el.setAttribute("type", attrs.inputType);
|
|
950
958
|
}
|
|
@@ -1069,6 +1077,8 @@ var Scene = (_class2 = class _Scene {
|
|
|
1069
1077
|
s.margin = "0";
|
|
1070
1078
|
s.padding = "0";
|
|
1071
1079
|
s.color = "transparent";
|
|
1080
|
+
s.forcedColorAdjust = "none";
|
|
1081
|
+
s.setProperty("-webkit-text-fill-color", "transparent");
|
|
1072
1082
|
s.whiteSpace = "pre-wrap";
|
|
1073
1083
|
s.overflow = "hidden";
|
|
1074
1084
|
s.zIndex = "0";
|
|
@@ -1098,24 +1108,26 @@ var Scene = (_class2 = class _Scene {
|
|
|
1098
1108
|
const lineFont = _nullishCoalesce(_nullishCoalesce(line.font, () => ( projection.font)), () => ( ""));
|
|
1099
1109
|
const lineHeight2 = _nullishCoalesce(_nullishCoalesce(line.lineHeight, () => ( projection.lineHeight)), () => ( 16));
|
|
1100
1110
|
lineElement.style.position = "absolute";
|
|
1111
|
+
lineElement.dir = "auto";
|
|
1101
1112
|
lineElement.style.left = `${line.x}px`;
|
|
1102
1113
|
lineElement.style.top = `${line.y + line.baseline - _chunkDFNK6OV6js.cssLineBoxBaseline.call(void 0, lineFont, lineHeight2)}px`;
|
|
1103
1114
|
lineElement.style.whiteSpace = "pre";
|
|
1104
1115
|
if (lineFont) lineElement.style.font = lineFont;
|
|
1105
1116
|
lineElement.style.lineHeight = `${lineHeight2}px`;
|
|
1117
|
+
const separator = _nullishCoalesce(line.separatorAfter, () => ( (index < lines.length - 1 ? "\n" : "")));
|
|
1106
1118
|
if (line.runs && line.runs.length > 0) {
|
|
1107
|
-
for (
|
|
1119
|
+
for (let runIndex = 0; runIndex < line.runs.length; runIndex++) {
|
|
1120
|
+
const run = line.runs[runIndex];
|
|
1108
1121
|
const runElement = document.createElement("span");
|
|
1109
|
-
runElement.textContent = run.text;
|
|
1122
|
+
runElement.textContent = run.text + (runIndex === line.runs.length - 1 ? separator : "");
|
|
1110
1123
|
if (run.font) runElement.style.font = run.font;
|
|
1111
1124
|
runElement.style.lineHeight = `${lineHeight2}px`;
|
|
1112
1125
|
lineElement.appendChild(runElement);
|
|
1113
1126
|
}
|
|
1114
1127
|
} else {
|
|
1115
|
-
lineElement.textContent = line.text;
|
|
1128
|
+
lineElement.textContent = line.text + separator;
|
|
1116
1129
|
}
|
|
1117
1130
|
el.appendChild(lineElement);
|
|
1118
|
-
if (index < lines.length - 1) el.appendChild(document.createTextNode("\n"));
|
|
1119
1131
|
}
|
|
1120
1132
|
el.dataset.vectoProjectionLines = signature;
|
|
1121
1133
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -297,6 +297,20 @@ var ComputeParticleEntity = class extends Entity {
|
|
|
297
297
|
};
|
|
298
298
|
|
|
299
299
|
// src/tree/Scene.ts
|
|
300
|
+
var INTERACTIVE_A11Y_ROLES = /* @__PURE__ */ new Set([
|
|
301
|
+
"button",
|
|
302
|
+
"switch",
|
|
303
|
+
"checkbox",
|
|
304
|
+
"radio",
|
|
305
|
+
"link",
|
|
306
|
+
"tab",
|
|
307
|
+
"menuitem",
|
|
308
|
+
"slider",
|
|
309
|
+
"combobox"
|
|
310
|
+
]);
|
|
311
|
+
function isNativelyFocusable(element) {
|
|
312
|
+
return element instanceof HTMLButtonElement || element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement || element instanceof HTMLAnchorElement && element.hasAttribute("href");
|
|
313
|
+
}
|
|
300
314
|
var REDUCED_MOTION_FPS = 30;
|
|
301
315
|
function parseInlinePx(value) {
|
|
302
316
|
if (!value || !value.endsWith("px")) return null;
|
|
@@ -910,20 +924,7 @@ var Scene = class _Scene {
|
|
|
910
924
|
}
|
|
911
925
|
node.emit("blur", {});
|
|
912
926
|
});
|
|
913
|
-
|
|
914
|
-
"button",
|
|
915
|
-
"switch",
|
|
916
|
-
"checkbox",
|
|
917
|
-
"radio",
|
|
918
|
-
"link",
|
|
919
|
-
"tab",
|
|
920
|
-
"menuitem",
|
|
921
|
-
"slider",
|
|
922
|
-
"combobox"
|
|
923
|
-
]);
|
|
924
|
-
const nativelyFocusable = el instanceof HTMLButtonElement || el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement || el instanceof HTMLAnchorElement && el.hasAttribute("href");
|
|
925
|
-
if (!nativelyFocusable && attrs.role && INTERACTIVE_ROLES.has(attrs.role)) {
|
|
926
|
-
el.setAttribute("tabindex", "0");
|
|
927
|
+
if (!isNativelyFocusable(el) && attrs.role && INTERACTIVE_A11Y_ROLES.has(attrs.role)) {
|
|
927
928
|
el.addEventListener("keydown", (e) => {
|
|
928
929
|
if (e.key === "Enter" || e.key === " ") {
|
|
929
930
|
e.preventDefault();
|
|
@@ -945,6 +946,13 @@ var Scene = class _Scene {
|
|
|
945
946
|
if (attrs.label !== void 0 && el.getAttribute("aria-label") !== attrs.label) {
|
|
946
947
|
el.setAttribute("aria-label", attrs.label);
|
|
947
948
|
}
|
|
949
|
+
const implicitTabIndex = !isNativelyFocusable(el) && attrs.role && INTERACTIVE_A11Y_ROLES.has(attrs.role) ? 0 : null;
|
|
950
|
+
const desiredTabIndex = attrs.tabIndex ?? implicitTabIndex;
|
|
951
|
+
if (desiredTabIndex === null) {
|
|
952
|
+
if (el.hasAttribute("tabindex")) el.removeAttribute("tabindex");
|
|
953
|
+
} else if (el.getAttribute("tabindex") !== String(desiredTabIndex)) {
|
|
954
|
+
el.setAttribute("tabindex", String(desiredTabIndex));
|
|
955
|
+
}
|
|
948
956
|
if (attrs.inputType !== void 0 && el.getAttribute("type") !== attrs.inputType) {
|
|
949
957
|
el.setAttribute("type", attrs.inputType);
|
|
950
958
|
}
|
|
@@ -1069,6 +1077,8 @@ var Scene = class _Scene {
|
|
|
1069
1077
|
s.margin = "0";
|
|
1070
1078
|
s.padding = "0";
|
|
1071
1079
|
s.color = "transparent";
|
|
1080
|
+
s.forcedColorAdjust = "none";
|
|
1081
|
+
s.setProperty("-webkit-text-fill-color", "transparent");
|
|
1072
1082
|
s.whiteSpace = "pre-wrap";
|
|
1073
1083
|
s.overflow = "hidden";
|
|
1074
1084
|
s.zIndex = "0";
|
|
@@ -1098,24 +1108,26 @@ var Scene = class _Scene {
|
|
|
1098
1108
|
const lineFont = line.font ?? projection.font ?? "";
|
|
1099
1109
|
const lineHeight2 = line.lineHeight ?? projection.lineHeight ?? 16;
|
|
1100
1110
|
lineElement.style.position = "absolute";
|
|
1111
|
+
lineElement.dir = "auto";
|
|
1101
1112
|
lineElement.style.left = `${line.x}px`;
|
|
1102
1113
|
lineElement.style.top = `${line.y + line.baseline - cssLineBoxBaseline(lineFont, lineHeight2)}px`;
|
|
1103
1114
|
lineElement.style.whiteSpace = "pre";
|
|
1104
1115
|
if (lineFont) lineElement.style.font = lineFont;
|
|
1105
1116
|
lineElement.style.lineHeight = `${lineHeight2}px`;
|
|
1117
|
+
const separator = line.separatorAfter ?? (index < lines.length - 1 ? "\n" : "");
|
|
1106
1118
|
if (line.runs && line.runs.length > 0) {
|
|
1107
|
-
for (
|
|
1119
|
+
for (let runIndex = 0; runIndex < line.runs.length; runIndex++) {
|
|
1120
|
+
const run = line.runs[runIndex];
|
|
1108
1121
|
const runElement = document.createElement("span");
|
|
1109
|
-
runElement.textContent = run.text;
|
|
1122
|
+
runElement.textContent = run.text + (runIndex === line.runs.length - 1 ? separator : "");
|
|
1110
1123
|
if (run.font) runElement.style.font = run.font;
|
|
1111
1124
|
runElement.style.lineHeight = `${lineHeight2}px`;
|
|
1112
1125
|
lineElement.appendChild(runElement);
|
|
1113
1126
|
}
|
|
1114
1127
|
} else {
|
|
1115
|
-
lineElement.textContent = line.text;
|
|
1128
|
+
lineElement.textContent = line.text + separator;
|
|
1116
1129
|
}
|
|
1117
1130
|
el.appendChild(lineElement);
|
|
1118
|
-
if (index < lines.length - 1) el.appendChild(document.createTextNode("\n"));
|
|
1119
1131
|
}
|
|
1120
1132
|
el.dataset.vectoProjectionLines = signature;
|
|
1121
1133
|
}
|
package/dist/tree/Entity.d.ts
CHANGED
|
@@ -68,6 +68,13 @@ export interface ContentProjectionRun {
|
|
|
68
68
|
export interface ContentProjectionLine {
|
|
69
69
|
/** Text for browser find-in-page and native selection. */
|
|
70
70
|
text: string;
|
|
71
|
+
/**
|
|
72
|
+
* Logical source content between this visual line and the next one. Use a
|
|
73
|
+
* space for a consumed soft-wrap separator, `"\n"` for a hard break, or an
|
|
74
|
+
* empty string for a space-less wrap. Omit to retain the legacy newline
|
|
75
|
+
* fallback between non-final lines.
|
|
76
|
+
*/
|
|
77
|
+
separatorAfter?: string;
|
|
71
78
|
/** Local origin of the visual line inside the entity. */
|
|
72
79
|
x: number;
|
|
73
80
|
y: number;
|
|
@@ -81,7 +88,7 @@ export interface ContentProjectionLine {
|
|
|
81
88
|
runs?: ContentProjectionRun[];
|
|
82
89
|
}
|
|
83
90
|
export interface ContentProjection {
|
|
84
|
-
/** The
|
|
91
|
+
/** The logical source text exposed to find, selection, copy, and assistive technology. */
|
|
85
92
|
text: string;
|
|
86
93
|
/** CSS font shorthand matching the drawn glyphs, e.g. `'24px sans-serif'`. */
|
|
87
94
|
font?: string;
|
|
@@ -129,6 +136,8 @@ export interface A11yAttributes {
|
|
|
129
136
|
role?: string;
|
|
130
137
|
/** Accessible name applied via `aria-label`. */
|
|
131
138
|
label?: string;
|
|
139
|
+
/** Explicit keyboard tab order for projected non-native interaction regions. */
|
|
140
|
+
tabIndex?: number;
|
|
132
141
|
/** Destination URL; only meaningful for `tag: 'a'`. */
|
|
133
142
|
href?: string;
|
|
134
143
|
/** Link target; only meaningful for `tag: 'a'`. Defaults to current window. */
|