@vectojs/core 1.6.0 → 1.6.2
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 +10 -0
- package/dist/index.js +46 -17
- package/dist/index.mjs +46 -17
- package/dist/tree/Entity.d.ts +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,6 +112,16 @@ 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
|
+
|
|
120
|
+
Interactive projected nodes capture the active pointer on `pointerdown` and route
|
|
121
|
+
`pointermove`, `pointerup`, and `pointercancel` through normal VMT capture/bubble propagation.
|
|
122
|
+
Treat `pointercancel` as rollback: discard transient gesture state and do not create durable
|
|
123
|
+
history. Pointer capture is released safely on both completion paths.
|
|
124
|
+
|
|
115
125
|
## Static content projection
|
|
116
126
|
|
|
117
127
|
Canvas-rendered text can opt into browser-native find, translation, selection, and copy without
|
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;
|
|
@@ -816,15 +830,29 @@ var Scene = (_class2 = class _Scene {
|
|
|
816
830
|
node.dispatchEvent(new (0, _chunkDFNK6OV6js.VectoJSEvent)("pointerleave", node, e, false));
|
|
817
831
|
});
|
|
818
832
|
const capEl = el;
|
|
833
|
+
const releasePointer = (event) => {
|
|
834
|
+
if (typeof capEl.releasePointerCapture !== "function") return;
|
|
835
|
+
if (typeof capEl.hasPointerCapture === "function" && !capEl.hasPointerCapture(event.pointerId)) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
capEl.releasePointerCapture(event.pointerId);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
if (!(error instanceof DOMException) || error.name !== "NotFoundError") throw error;
|
|
842
|
+
}
|
|
843
|
+
};
|
|
819
844
|
el.addEventListener("pointerdown", (e) => {
|
|
820
845
|
if (typeof capEl.setPointerCapture === "function") capEl.setPointerCapture(e.pointerId);
|
|
821
846
|
node.dispatchEvent(new (0, _chunkDFNK6OV6js.VectoJSEvent)("pointerdown", node, e));
|
|
822
847
|
});
|
|
823
848
|
el.addEventListener("pointerup", (e) => {
|
|
824
|
-
|
|
825
|
-
capEl.releasePointerCapture(e.pointerId);
|
|
849
|
+
releasePointer(e);
|
|
826
850
|
node.dispatchEvent(new (0, _chunkDFNK6OV6js.VectoJSEvent)("pointerup", node, e));
|
|
827
851
|
});
|
|
852
|
+
el.addEventListener("pointercancel", (e) => {
|
|
853
|
+
releasePointer(e);
|
|
854
|
+
node.dispatchEvent(new (0, _chunkDFNK6OV6js.VectoJSEvent)("pointercancel", node, e));
|
|
855
|
+
});
|
|
828
856
|
el.addEventListener(
|
|
829
857
|
"pointermove",
|
|
830
858
|
(e) => node.dispatchEvent(new (0, _chunkDFNK6OV6js.VectoJSEvent)("pointermove", node, e))
|
|
@@ -896,20 +924,7 @@ var Scene = (_class2 = class _Scene {
|
|
|
896
924
|
}
|
|
897
925
|
node.emit("blur", {});
|
|
898
926
|
});
|
|
899
|
-
|
|
900
|
-
"button",
|
|
901
|
-
"switch",
|
|
902
|
-
"checkbox",
|
|
903
|
-
"radio",
|
|
904
|
-
"link",
|
|
905
|
-
"tab",
|
|
906
|
-
"menuitem",
|
|
907
|
-
"slider",
|
|
908
|
-
"combobox"
|
|
909
|
-
]);
|
|
910
|
-
const nativelyFocusable = el instanceof HTMLButtonElement || el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement || el instanceof HTMLAnchorElement && el.hasAttribute("href");
|
|
911
|
-
if (!nativelyFocusable && attrs.role && INTERACTIVE_ROLES.has(attrs.role)) {
|
|
912
|
-
el.setAttribute("tabindex", "0");
|
|
927
|
+
if (!isNativelyFocusable(el) && attrs.role && INTERACTIVE_A11Y_ROLES.has(attrs.role)) {
|
|
913
928
|
el.addEventListener("keydown", (e) => {
|
|
914
929
|
if (e.key === "Enter" || e.key === " ") {
|
|
915
930
|
e.preventDefault();
|
|
@@ -931,6 +946,13 @@ var Scene = (_class2 = class _Scene {
|
|
|
931
946
|
if (attrs.label !== void 0 && el.getAttribute("aria-label") !== attrs.label) {
|
|
932
947
|
el.setAttribute("aria-label", attrs.label);
|
|
933
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
|
+
}
|
|
934
956
|
if (attrs.inputType !== void 0 && el.getAttribute("type") !== attrs.inputType) {
|
|
935
957
|
el.setAttribute("type", attrs.inputType);
|
|
936
958
|
}
|
|
@@ -2513,7 +2535,14 @@ var DOMPortalEntity = (_class7 = class extends _chunkDFNK6OV6js.Entity {
|
|
|
2513
2535
|
});
|
|
2514
2536
|
this.resizeObserver.observe(this.domElement);
|
|
2515
2537
|
}
|
|
2516
|
-
const events = [
|
|
2538
|
+
const events = [
|
|
2539
|
+
"click",
|
|
2540
|
+
"pointerdown",
|
|
2541
|
+
"pointerup",
|
|
2542
|
+
"pointercancel",
|
|
2543
|
+
"pointermove",
|
|
2544
|
+
"wheel"
|
|
2545
|
+
];
|
|
2517
2546
|
for (const type of events) {
|
|
2518
2547
|
const handler = (e) => {
|
|
2519
2548
|
this.dispatchEvent(new (0, _chunkDFNK6OV6js.VectoJSEvent)(type, this, e));
|
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;
|
|
@@ -816,15 +830,29 @@ var Scene = class _Scene {
|
|
|
816
830
|
node.dispatchEvent(new VectoJSEvent("pointerleave", node, e, false));
|
|
817
831
|
});
|
|
818
832
|
const capEl = el;
|
|
833
|
+
const releasePointer = (event) => {
|
|
834
|
+
if (typeof capEl.releasePointerCapture !== "function") return;
|
|
835
|
+
if (typeof capEl.hasPointerCapture === "function" && !capEl.hasPointerCapture(event.pointerId)) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
capEl.releasePointerCapture(event.pointerId);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
if (!(error instanceof DOMException) || error.name !== "NotFoundError") throw error;
|
|
842
|
+
}
|
|
843
|
+
};
|
|
819
844
|
el.addEventListener("pointerdown", (e) => {
|
|
820
845
|
if (typeof capEl.setPointerCapture === "function") capEl.setPointerCapture(e.pointerId);
|
|
821
846
|
node.dispatchEvent(new VectoJSEvent("pointerdown", node, e));
|
|
822
847
|
});
|
|
823
848
|
el.addEventListener("pointerup", (e) => {
|
|
824
|
-
|
|
825
|
-
capEl.releasePointerCapture(e.pointerId);
|
|
849
|
+
releasePointer(e);
|
|
826
850
|
node.dispatchEvent(new VectoJSEvent("pointerup", node, e));
|
|
827
851
|
});
|
|
852
|
+
el.addEventListener("pointercancel", (e) => {
|
|
853
|
+
releasePointer(e);
|
|
854
|
+
node.dispatchEvent(new VectoJSEvent("pointercancel", node, e));
|
|
855
|
+
});
|
|
828
856
|
el.addEventListener(
|
|
829
857
|
"pointermove",
|
|
830
858
|
(e) => node.dispatchEvent(new VectoJSEvent("pointermove", node, e))
|
|
@@ -896,20 +924,7 @@ var Scene = class _Scene {
|
|
|
896
924
|
}
|
|
897
925
|
node.emit("blur", {});
|
|
898
926
|
});
|
|
899
|
-
|
|
900
|
-
"button",
|
|
901
|
-
"switch",
|
|
902
|
-
"checkbox",
|
|
903
|
-
"radio",
|
|
904
|
-
"link",
|
|
905
|
-
"tab",
|
|
906
|
-
"menuitem",
|
|
907
|
-
"slider",
|
|
908
|
-
"combobox"
|
|
909
|
-
]);
|
|
910
|
-
const nativelyFocusable = el instanceof HTMLButtonElement || el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement || el instanceof HTMLAnchorElement && el.hasAttribute("href");
|
|
911
|
-
if (!nativelyFocusable && attrs.role && INTERACTIVE_ROLES.has(attrs.role)) {
|
|
912
|
-
el.setAttribute("tabindex", "0");
|
|
927
|
+
if (!isNativelyFocusable(el) && attrs.role && INTERACTIVE_A11Y_ROLES.has(attrs.role)) {
|
|
913
928
|
el.addEventListener("keydown", (e) => {
|
|
914
929
|
if (e.key === "Enter" || e.key === " ") {
|
|
915
930
|
e.preventDefault();
|
|
@@ -931,6 +946,13 @@ var Scene = class _Scene {
|
|
|
931
946
|
if (attrs.label !== void 0 && el.getAttribute("aria-label") !== attrs.label) {
|
|
932
947
|
el.setAttribute("aria-label", attrs.label);
|
|
933
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
|
+
}
|
|
934
956
|
if (attrs.inputType !== void 0 && el.getAttribute("type") !== attrs.inputType) {
|
|
935
957
|
el.setAttribute("type", attrs.inputType);
|
|
936
958
|
}
|
|
@@ -2513,7 +2535,14 @@ var DOMPortalEntity = class extends Entity {
|
|
|
2513
2535
|
});
|
|
2514
2536
|
this.resizeObserver.observe(this.domElement);
|
|
2515
2537
|
}
|
|
2516
|
-
const events = [
|
|
2538
|
+
const events = [
|
|
2539
|
+
"click",
|
|
2540
|
+
"pointerdown",
|
|
2541
|
+
"pointerup",
|
|
2542
|
+
"pointercancel",
|
|
2543
|
+
"pointermove",
|
|
2544
|
+
"wheel"
|
|
2545
|
+
];
|
|
2517
2546
|
for (const type of events) {
|
|
2518
2547
|
const handler = (e) => {
|
|
2519
2548
|
this.dispatchEvent(new VectoJSEvent(type, this, e));
|
package/dist/tree/Entity.d.ts
CHANGED
|
@@ -129,6 +129,8 @@ export interface A11yAttributes {
|
|
|
129
129
|
role?: string;
|
|
130
130
|
/** Accessible name applied via `aria-label`. */
|
|
131
131
|
label?: string;
|
|
132
|
+
/** Explicit keyboard tab order for projected non-native interaction regions. */
|
|
133
|
+
tabIndex?: number;
|
|
132
134
|
/** Destination URL; only meaningful for `tag: 'a'`. */
|
|
133
135
|
href?: string;
|
|
134
136
|
/** Link target; only meaningful for `tag: 'a'`. Defaults to current window. */
|
|
@@ -162,7 +164,7 @@ export interface A11yAttributes {
|
|
|
162
164
|
/**
|
|
163
165
|
* Union of all pointer/interaction events that can be emitted by an {@link Entity}.
|
|
164
166
|
*/
|
|
165
|
-
export type VectoEvent = 'click' | 'hover' | 'pointerdown' | 'pointerup' | 'pointermove' | 'pointerleave' | 'change' | 'focus' | 'blur' | 'wheel' | 'keydown' | 'keyup';
|
|
167
|
+
export type VectoEvent = 'click' | 'hover' | 'pointerdown' | 'pointerup' | 'pointercancel' | 'pointermove' | 'pointerleave' | 'change' | 'focus' | 'blur' | 'wheel' | 'keydown' | 'keyup';
|
|
166
168
|
/** Options for {@link Entity.on} / {@link Entity.off}. */
|
|
167
169
|
export interface ListenerOptions {
|
|
168
170
|
/** Register the listener for the capture phase (root→target) instead of bubble. */
|