drawnui-react 0.1.0-preview.12 → 0.1.0-preview.13
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/SKIPPED.md +6 -5
- package/dist/controls/GridStructure.d.ts +4 -2
- package/dist/controls/GridStructure.js +2 -2
- package/dist/controls/SkiaButton.d.ts +2 -0
- package/dist/controls/SkiaButton.js +2 -0
- package/dist/controls/SkiaEditor.d.ts +7 -0
- package/dist/controls/SkiaEditor.js +9 -1
- package/dist/controls/SkiaGif.js +1 -1
- package/dist/controls/SkiaLabelFps.d.ts +16 -0
- package/dist/controls/SkiaLabelFps.js +41 -0
- package/dist/controls/SkiaLayout.d.ts +32 -6
- package/dist/controls/SkiaLayout.js +171 -10
- package/dist/controls/SkiaLottie.js +1 -1
- package/dist/controls/SkiaScroll.d.ts +5 -4
- package/dist/controls/SkiaScroll.js +17 -8
- package/dist/controls/SkiaSlider.d.ts +8 -0
- package/dist/controls/SkiaSlider.js +39 -0
- package/dist/controls/SkiaSvg.js +1 -1
- package/dist/core/Accessibility.d.ts +59 -0
- package/dist/core/Accessibility.js +303 -3
- package/dist/core/Canvas.d.ts +6 -0
- package/dist/core/Canvas.js +9 -1
- package/dist/core/SkiaControl.d.ts +21 -1
- package/dist/core/SkiaControl.js +28 -2
- package/dist/core/SkiaEffect.js +1 -1
- package/dist/core/SkiaImageManager.js +1 -1
- package/dist/core/Super.d.ts +7 -0
- package/dist/core/Super.js +15 -1
- package/dist/core/ViewsAdapter.d.ts +7 -2
- package/dist/core/ViewsAdapter.js +10 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/react/index.d.ts +4 -1
- package/dist/react/index.js +63 -9
- package/dist/react/reconciler.js +2 -1
- package/dist/vite/index.d.ts +9 -0
- package/dist/vite/index.js +36 -3
- package/package.json +3 -1
package/dist/core/SkiaControl.js
CHANGED
|
@@ -417,8 +417,12 @@ export class SkiaControl {
|
|
|
417
417
|
this.accessibilityHint = v;
|
|
418
418
|
this.AccessibilityChanged();
|
|
419
419
|
} }
|
|
420
|
-
/**
|
|
421
|
-
|
|
420
|
+
/**
|
|
421
|
+
* Whether keyboard navigation and assistive technology may focus and use this node: the explicit value, else the class
|
|
422
|
+
* default (true when the control handles Tapped), and in both cases only while a tap could reach the control
|
|
423
|
+
* (CanReceiveGesture), so Tab, Enter / Space and screen-reader activation never use a control the pointer cannot.
|
|
424
|
+
*/
|
|
425
|
+
get AccessibilityCanInteract() { return (this.accessibilityCanInteract ?? this.DefaultAccessibilityCanInteract()) && this.CanReceiveGesture("Tapped"); }
|
|
422
426
|
set AccessibilityCanInteract(v) { if (this.accessibilityCanInteract !== v) {
|
|
423
427
|
this.accessibilityCanInteract = v;
|
|
424
428
|
this.AccessibilityChanged();
|
|
@@ -503,6 +507,28 @@ export class SkiaControl {
|
|
|
503
507
|
}
|
|
504
508
|
/** Overlay focus arrives on / leaves this node (inputs may activate their sink here). */
|
|
505
509
|
OnAccessibilityFocused(_focused) { }
|
|
510
|
+
/**
|
|
511
|
+
* Keyboard navigation hands this control the keys it may use while it holds keyboard focus (ArrowLeft / Right / Up /
|
|
512
|
+
* Down, Home, End, PageUp, PageDown, DOM `event.code` names). Return true when the key was used; the default uses none,
|
|
513
|
+
* so the key moves focus inside the arrow-key group around the control.
|
|
514
|
+
*/
|
|
515
|
+
OnAccessibilityKey(_key) { return false; }
|
|
516
|
+
/**
|
|
517
|
+
* Whether a pointer gesture of this kind would reach this control, by the rules the gesture dispatch applies: the
|
|
518
|
+
* control and every ancestor draw, none of them is InputTransparent, no ancestor keeps this gesture from its children
|
|
519
|
+
* (LockChildrenGestures, PassNone included) and the control accepts input (AcceptsInput). Opacity does not count, as
|
|
520
|
+
* for the pointer. Keyboard navigation uses it: Tapped for Tab and Enter / Space, Panning for the arrow keys.
|
|
521
|
+
*/
|
|
522
|
+
CanReceiveGesture(gesture) {
|
|
523
|
+
if (!this.IsVisible || this.IsDisposed || this.InputTransparent || !this.AcceptsInput())
|
|
524
|
+
return false;
|
|
525
|
+
for (let p = this.Parent; p; p = p.Parent)
|
|
526
|
+
if (!p.IsVisible || p.IsDisposed || p.InputTransparent || p.CheckChildrenGesturesLocked(gesture))
|
|
527
|
+
return false;
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
/** False while the control ignores gestures itself, e.g. a disabled button. Default true. */
|
|
531
|
+
AcceptsInput() { return true; }
|
|
506
532
|
NotifyAccessibilityFocused(focused) { this.Superview?.AccessibilityManager.NotifyFocused(focused ? this : undefined); }
|
|
507
533
|
/** DrawnUi SmartMax: larger of two sizes, an infinite one loses. */
|
|
508
534
|
static SmartMax(a, b) {
|
package/dist/core/SkiaEffect.js
CHANGED
|
@@ -228,7 +228,7 @@ export class SkiaShaderEffect extends SkiaEffect {
|
|
|
228
228
|
static async FetchText(url) {
|
|
229
229
|
let p = sourceCache.get(url);
|
|
230
230
|
if (!p) {
|
|
231
|
-
p = fetch(url).then((r) => { if (!r.ok)
|
|
231
|
+
p = fetch(Super.ResolveAssetUrl(url)).then((r) => { if (!r.ok)
|
|
232
232
|
throw new Error(`${r.status} ${url}`); return r.text(); });
|
|
233
233
|
sourceCache.set(url, p);
|
|
234
234
|
}
|
|
@@ -39,7 +39,7 @@ export class SkiaImageManager {
|
|
|
39
39
|
return inflight;
|
|
40
40
|
}
|
|
41
41
|
async Fetch(source, signal) {
|
|
42
|
-
const response = await fetch(source, signal ? { signal } : undefined);
|
|
42
|
+
const response = await fetch(Super.ResolveAssetUrl(source), signal ? { signal } : undefined);
|
|
43
43
|
if (!response.ok)
|
|
44
44
|
throw new Error(`DrawnUi: ${response.status} loading image '${source}'`);
|
|
45
45
|
const image = Super.CK.MakeImageFromEncoded(await response.arrayBuffer());
|
package/dist/core/Super.d.ts
CHANGED
|
@@ -52,6 +52,13 @@ export declare class Super {
|
|
|
52
52
|
static UseDrawnUi(): DrawnUiBuilder;
|
|
53
53
|
/** Master switch: false makes every control render uncached (DrawnUi Super.CacheEnabled). */
|
|
54
54
|
static CacheEnabled: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Address every engine loader fetches a source from (images, sprites, fonts, SVG, GIF, Lottie, shaders); caches
|
|
57
|
+
* stay keyed by the source itself. Web-only, no C# counterpart. Default: a file listed in
|
|
58
|
+
* `globalThis.DrawnUiAssetStamps` (written into index.html by the `drawnUiAssetStamps()` Vite plugin) gets
|
|
59
|
+
* `?v=<content hash>`, so a browser never keeps an old copy of a changed file. Replace it to map sources elsewhere.
|
|
60
|
+
*/
|
|
61
|
+
static ResolveAssetUrl: (source: string) => string;
|
|
55
62
|
private static readonly colorCache;
|
|
56
63
|
/**
|
|
57
64
|
* Color string -> CanvasKit color. Hex follows the MAUI/DrawnUi convention: #RGB, #ARGB, #RRGGBB, #AARRGGBB
|
package/dist/core/Super.js
CHANGED
|
@@ -46,7 +46,7 @@ export class DrawnUiBuilder {
|
|
|
46
46
|
for (const f of this.fonts.Fonts) {
|
|
47
47
|
let face = loaded.get(f.Source);
|
|
48
48
|
if (!face) {
|
|
49
|
-
const data = await (await fetch(f.Source)).arrayBuffer();
|
|
49
|
+
const data = await (await fetch(Super.ResolveAssetUrl(f.Source))).arrayBuffer();
|
|
50
50
|
face = Super.CK.Typeface.MakeFreeTypeFaceFromData(data) ?? undefined;
|
|
51
51
|
if (!face)
|
|
52
52
|
throw new Error(`DrawnUi: cannot load font '${f.Source}'`);
|
|
@@ -122,6 +122,20 @@ export class Super {
|
|
|
122
122
|
static UseDrawnUi() { return new DrawnUiBuilder(); }
|
|
123
123
|
/** Master switch: false makes every control render uncached (DrawnUi Super.CacheEnabled). */
|
|
124
124
|
static CacheEnabled = true;
|
|
125
|
+
/**
|
|
126
|
+
* Address every engine loader fetches a source from (images, sprites, fonts, SVG, GIF, Lottie, shaders); caches
|
|
127
|
+
* stay keyed by the source itself. Web-only, no C# counterpart. Default: a file listed in
|
|
128
|
+
* `globalThis.DrawnUiAssetStamps` (written into index.html by the `drawnUiAssetStamps()` Vite plugin) gets
|
|
129
|
+
* `?v=<content hash>`, so a browser never keeps an old copy of a changed file. Replace it to map sources elsewhere.
|
|
130
|
+
*/
|
|
131
|
+
static ResolveAssetUrl = (source) => {
|
|
132
|
+
const stamps = globalThis.DrawnUiAssetStamps;
|
|
133
|
+
if (!stamps)
|
|
134
|
+
return source;
|
|
135
|
+
const q = source.indexOf("?");
|
|
136
|
+
const v = stamps[(q < 0 ? source : source.slice(0, q)).replace(/^\.?\//, "")];
|
|
137
|
+
return v ? `${source}${q < 0 ? "?" : "&"}v=${v}` : source;
|
|
138
|
+
};
|
|
125
139
|
static colorCache = new Map();
|
|
126
140
|
/**
|
|
127
141
|
* Color string -> CanvasKit color. Hex follows the MAUI/DrawnUi convention: #RGB, #ARGB, #RRGGBB, #AARRGGBB
|
|
@@ -16,6 +16,8 @@ export declare class ViewsAdapter {
|
|
|
16
16
|
Created: number;
|
|
17
17
|
constructor(parent: SkiaControl);
|
|
18
18
|
get PoolSize(): number;
|
|
19
|
+
/** The items views are bound to. */
|
|
20
|
+
get Items(): readonly unknown[];
|
|
19
21
|
get InUseCount(): number;
|
|
20
22
|
/** New template / items / mode: every realized view is dropped (DrawnUi ApplyItemsSource). */
|
|
21
23
|
Initialize(template: (() => SkiaControl) | undefined, items: readonly unknown[], recycling: RecyclingTemplate): void;
|
|
@@ -26,8 +28,11 @@ export declare class ViewsAdapter {
|
|
|
26
28
|
ShiftIndices(by: number, items: readonly unknown[]): void;
|
|
27
29
|
/** The realized view for an index, if any (no creation). */
|
|
28
30
|
GetViewForIndex(index: number): SkiaControl | undefined;
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* View bound to items[index]: existing, recycled from the pool, or freshly created. `quiet`: bound before it is
|
|
33
|
+
* attached, so the rebind invalidates the view alone and not the layout, which measures or draws it right away.
|
|
34
|
+
*/
|
|
35
|
+
GetOrCreateViewForIndex(index: number, quiet?: boolean): SkiaControl | undefined;
|
|
31
36
|
/** Index left the visible range: recycled views go back to the pool, non-recycled ones stay realized. */
|
|
32
37
|
ReleaseViewAt(index: number): void;
|
|
33
38
|
/** Releases every realized index outside [first, last]. */
|
|
@@ -16,6 +16,8 @@ export class ViewsAdapter {
|
|
|
16
16
|
this.parent = parent;
|
|
17
17
|
}
|
|
18
18
|
get PoolSize() { return this.pool.length; }
|
|
19
|
+
/** The items views are bound to. */
|
|
20
|
+
get Items() { return this.items; }
|
|
19
21
|
get InUseCount() { return this.inUse.size; }
|
|
20
22
|
/** New template / items / mode: every realized view is dropped (DrawnUi ApplyItemsSource). */
|
|
21
23
|
Initialize(template, items, recycling) {
|
|
@@ -52,8 +54,11 @@ export class ViewsAdapter {
|
|
|
52
54
|
}
|
|
53
55
|
/** The realized view for an index, if any (no creation). */
|
|
54
56
|
GetViewForIndex(index) { return this.inUse.get(index); }
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
+
/**
|
|
58
|
+
* View bound to items[index]: existing, recycled from the pool, or freshly created. `quiet`: bound before it is
|
|
59
|
+
* attached, so the rebind invalidates the view alone and not the layout, which measures or draws it right away.
|
|
60
|
+
*/
|
|
61
|
+
GetOrCreateViewForIndex(index, quiet = false) {
|
|
57
62
|
const existing = this.inUse.get(index);
|
|
58
63
|
if (existing)
|
|
59
64
|
return existing;
|
|
@@ -64,9 +69,11 @@ export class ViewsAdapter {
|
|
|
64
69
|
view = this.template();
|
|
65
70
|
this.Created++;
|
|
66
71
|
}
|
|
67
|
-
|
|
72
|
+
if (!quiet)
|
|
73
|
+
view.Parent = this.parent;
|
|
68
74
|
view.ContextIndex = index;
|
|
69
75
|
view.BindingContext = this.items[index];
|
|
76
|
+
view.Parent = this.parent;
|
|
70
77
|
this.inUse.set(index, view);
|
|
71
78
|
return view;
|
|
72
79
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./core/Accessibility";
|
|
|
12
12
|
export * from "./controls/SkiaLabel";
|
|
13
13
|
export * from "./controls/TextSpan";
|
|
14
14
|
export * from "./controls/SkiaRichLabel";
|
|
15
|
+
export * from "./controls/SkiaLabelFps";
|
|
15
16
|
export * from "./core/ControlStyle";
|
|
16
17
|
export * from "./core/ImageEffects";
|
|
17
18
|
export * from "./controls/SkiaToggle";
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./core/Accessibility";
|
|
|
12
12
|
export * from "./controls/SkiaLabel";
|
|
13
13
|
export * from "./controls/TextSpan";
|
|
14
14
|
export * from "./controls/SkiaRichLabel";
|
|
15
|
+
export * from "./controls/SkiaLabelFps";
|
|
15
16
|
export * from "./core/ControlStyle";
|
|
16
17
|
export * from "./core/ImageEffects";
|
|
17
18
|
export * from "./controls/SkiaToggle";
|
package/dist/react/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { SkiaControl } from "../core/SkiaControl";
|
|
|
4
4
|
import type { SkiaLabel as SkiaLabelCtrl } from "../controls/SkiaLabel";
|
|
5
5
|
import type { TextSpan as TextSpanCtrl } from "../controls/TextSpan";
|
|
6
6
|
import type { SkiaRichLabel as SkiaRichLabelCtrl } from "../controls/SkiaRichLabel";
|
|
7
|
+
import type { SkiaLabelFps as SkiaLabelFpsCtrl } from "../controls/SkiaLabelFps";
|
|
7
8
|
import type { SkiaSwitch as SkiaSwitchCtrl } from "../controls/SkiaSwitch";
|
|
8
9
|
import type { SkiaCheckbox as SkiaCheckboxCtrl } from "../controls/SkiaCheckbox";
|
|
9
10
|
import type { SkiaRadioButton as SkiaRadioButtonCtrl } from "../controls/SkiaRadioButton";
|
|
@@ -22,7 +23,7 @@ import type { SkiaScrollBar as SkiaScrollBarCtrl } from "../controls/SkiaScrollB
|
|
|
22
23
|
import type { RefreshIndicator as RefreshIndicatorCtrl } from "../controls/RefreshIndicator";
|
|
23
24
|
import type { SkiaSvg as SkiaSvgCtrl } from "../controls/SkiaSvg";
|
|
24
25
|
import type { SkiaBackdrop as SkiaBackdropCtrl } from "../controls/SkiaBackdrop";
|
|
25
|
-
import
|
|
26
|
+
import { SkiaEditor as SkiaEditorCtrl } from "../controls/SkiaEditor";
|
|
26
27
|
import type { SkiaSprite as SkiaSpriteCtrl } from "../controls/SkiaSprite";
|
|
27
28
|
import type { SkiaSpriteSet as SkiaSpriteSetCtrl } from "../controls/SkiaSpriteSet";
|
|
28
29
|
import type { SkiaLottie as SkiaLottieCtrl } from "../controls/SkiaLottie";
|
|
@@ -61,6 +62,8 @@ export declare const SkiaGrid: FC<LayoutProps<SkiaLayoutCtrl>>;
|
|
|
61
62
|
/** Grid drawing gradient separator lines in its spacing (C# SkiaDecoratedGrid). */
|
|
62
63
|
export declare const SkiaDecoratedGrid: FC<LayoutProps<SkiaDecoratedGridCtrl>>;
|
|
63
64
|
export declare const SkiaLabel: FC<LayoutProps<SkiaLabelCtrl>>;
|
|
65
|
+
/** Canvas FPS counter (C# SkiaLabelFps); its value changes only on frames running animators request anyway. */
|
|
66
|
+
export declare const SkiaLabelFps: FC<LeafProps<SkiaLabelFpsCtrl>>;
|
|
64
67
|
/** Markdown label (C# SkiaRichLabel): Text is markdown, rendered as spans; LinkTapped for [text](url). */
|
|
65
68
|
export declare const SkiaRichLabel: FC<LeafProps<SkiaRichLabelCtrl>>;
|
|
66
69
|
/** Child of <SkiaLabel>: a styled fragment (C# TextSpan). */
|
package/dist/react/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { SkiaAccessibilityManager } from "../core/Accessibility";
|
|
4
|
+
import { TextInputProxy } from "../core/TextInputProxy";
|
|
3
5
|
import { Canvas as CanvasView } from "../core/Canvas";
|
|
6
|
+
import { SkiaEditor as SkiaEditorCtrl } from "../controls/SkiaEditor";
|
|
4
7
|
import { SkiaScroll as SkiaScrollCtrl } from "../controls/SkiaScroll";
|
|
5
8
|
import { createDrawnRoot } from "./reconciler";
|
|
6
9
|
/** Typed JSX tags resolved by the reconciler Registry. */
|
|
@@ -13,6 +16,8 @@ export const SkiaGrid = "SkiaGrid";
|
|
|
13
16
|
/** Grid drawing gradient separator lines in its spacing (C# SkiaDecoratedGrid). */
|
|
14
17
|
export const SkiaDecoratedGrid = "SkiaDecoratedGrid";
|
|
15
18
|
export const SkiaLabel = "SkiaLabel";
|
|
19
|
+
/** Canvas FPS counter (C# SkiaLabelFps); its value changes only on frames running animators request anyway. */
|
|
20
|
+
export const SkiaLabelFps = "SkiaLabelFps";
|
|
16
21
|
/** Markdown label (C# SkiaRichLabel): Text is markdown, rendered as spans; LinkTapped for [text](url). */
|
|
17
22
|
export const SkiaRichLabel = "SkiaRichLabel";
|
|
18
23
|
/** Child of <SkiaLabel>: a styled fragment (C# TextSpan). */
|
|
@@ -114,19 +119,50 @@ const A11Y_CSS = `
|
|
|
114
119
|
.drawnui-a11y-text>span{position:absolute;white-space:pre;color:transparent;user-select:text;-webkit-user-select:text;pointer-events:auto;cursor:text;line-height:1;font-kerning:none;font-feature-settings:"kern" 0,"liga" 0,"calt" 0;text-rendering:geometricPrecision}
|
|
115
120
|
.drawnui-a11y-text>span::selection{background:rgba(110,168,254,.55);color:transparent}
|
|
116
121
|
`;
|
|
122
|
+
const NAVIGATION_KEYS = new Set(["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown"]);
|
|
117
123
|
/**
|
|
118
|
-
* Invisible ARIA elements mirroring the accessible drawn controls, positioned over the canvas.
|
|
119
|
-
* Improvement over DrawnUi.Blazor: `pointer-events:none`, so hover and every pointer gesture still reach the
|
|
120
|
-
*
|
|
124
|
+
* Invisible ARIA elements mirroring the accessible drawn controls, positioned over the canvas (the DrawnUi.Blazor
|
|
125
|
+
* overlay). Improvement over DrawnUi.Blazor: `pointer-events:none`, so hover and every pointer gesture still reach the
|
|
126
|
+
* canvas. Keyboard and screen-reader input arrive as DOM events: Enter / Space activate (a Tapped), the navigation keys
|
|
127
|
+
* go to the focused control and then to its arrow-key group; a group is one Tab stop (roving tabindex).
|
|
121
128
|
*/
|
|
122
129
|
function AccessibilityOverlay({ view }) {
|
|
123
130
|
const [nodes, setNodes] = useState(() => view.AccessibilityManager.Snapshot);
|
|
131
|
+
// keyboard focus moved by the manager (arrow keys in a group): focus that node's element once it is rendered
|
|
132
|
+
const [focusRequest, setFocusRequest] = useState();
|
|
133
|
+
// the Tab stop of a group moves with keyboard focus: re-render on focus
|
|
134
|
+
const [, setFocusTick] = useState(0);
|
|
124
135
|
useEffect(() => {
|
|
125
136
|
const mgr = view.AccessibilityManager;
|
|
126
137
|
setNodes(mgr.Snapshot);
|
|
127
138
|
const off = mgr.OnChanged(() => setNodes(mgr.Snapshot));
|
|
128
139
|
const offLive = mgr.OnLiveRegionUpdated(() => { mgr.ForceRebuildOnNextFrame(); view.Update(); });
|
|
129
|
-
|
|
140
|
+
const offFocus = mgr.OnKeyboardFocusRequested((node) => { setNodes(mgr.Snapshot); setFocusRequest({ id: node.AccessibilityId }); });
|
|
141
|
+
return () => { off(); offLive(); offFocus(); };
|
|
142
|
+
}, [view]);
|
|
143
|
+
const elementOf = (id) => overlayRef.current?.querySelector(`[data-a11y-id="${id}"]`) ?? undefined;
|
|
144
|
+
useLayoutEffect(() => {
|
|
145
|
+
if (!focusRequest)
|
|
146
|
+
return;
|
|
147
|
+
setFocusRequest(undefined);
|
|
148
|
+
elementOf(focusRequest.id)?.focus({ preventScroll: true });
|
|
149
|
+
}, [focusRequest]);
|
|
150
|
+
// Tab / Shift+Tab in a text field leave it for the next node, also after a click into it: DOM focus goes to the
|
|
151
|
+
// field's own element (without taking the caret again) and the browser's Tab moves on from there
|
|
152
|
+
const tabbingOut = useRef(0);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
const onKey = (e) => {
|
|
155
|
+
const editor = SkiaEditorCtrl.Focused;
|
|
156
|
+
if (e.key !== "Tab" || !editor || editor.Superview !== view || !TextInputProxy.IsProxyTarget(e.target))
|
|
157
|
+
return;
|
|
158
|
+
const el = elementOf(editor.AccessibilityId);
|
|
159
|
+
if (!el)
|
|
160
|
+
return;
|
|
161
|
+
tabbingOut.current = editor.AccessibilityId;
|
|
162
|
+
el.focus({ preventScroll: true });
|
|
163
|
+
};
|
|
164
|
+
document.addEventListener("keydown", onKey, true);
|
|
165
|
+
return () => document.removeEventListener("keydown", onKey, true);
|
|
130
166
|
}, [view]);
|
|
131
167
|
// selectable text spans take pointer events: the wheel over them is re-dispatched to the canvas so drawn scrolls keep working
|
|
132
168
|
const overlayRef = useRef(null);
|
|
@@ -161,11 +197,29 @@ function AccessibilityOverlay({ view }) {
|
|
|
161
197
|
const pin = (e) => { e.currentTarget.scrollTop = 0; e.currentTarget.scrollLeft = 0; };
|
|
162
198
|
return (_jsxs("div", { className: "drawnui-a11y-overlay", onScroll: pin, ref: overlayRef, children: [_jsx("style", { children: A11Y_CSS }), nodes.map((n) => {
|
|
163
199
|
const pos = { left: n.Rect.Left, top: n.Rect.Top, width: n.Rect.Width, height: n.Rect.Height };
|
|
164
|
-
const activate = () => n.Source
|
|
165
|
-
return n.CanInteract ? (_jsx("div", { role: n.Role, "aria-label": n.Label, title: n.Hint, "aria-pressed": n.Role === "button" ? n.IsPressed : undefined, "aria-checked": n.Role === "switch" || n.Role === "checkbox" || n.Role === "radio" ? n.IsPressed : undefined, "aria-live": n.Live, tabIndex: 0, className: "drawnui-a11y-node", style: { ...pos, fontSize: 0, userSelect: "none" }, onClick: activate, onKeyDown: (e) => {
|
|
166
|
-
e.
|
|
167
|
-
|
|
168
|
-
|
|
200
|
+
const activate = () => SkiaAccessibilityManager.Activate(n.Source);
|
|
201
|
+
return n.CanInteract ? (_jsx("div", { "data-a11y-id": n.Id, role: n.Role, "aria-label": n.Label, title: n.Hint, "aria-pressed": n.Role === "button" ? n.IsPressed : undefined, "aria-checked": n.Role === "switch" || n.Role === "checkbox" || n.Role === "radio" ? n.IsPressed : undefined, "aria-live": n.Live, tabIndex: view.AccessibilityManager.IsTabStop(n.Source) ? 0 : -1, className: "drawnui-a11y-node", style: { ...pos, fontSize: 0, userSelect: "none" }, onClick: activate, onKeyDown: (e) => {
|
|
202
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
activate();
|
|
205
|
+
}
|
|
206
|
+
else if (NAVIGATION_KEYS.has(e.key) && SkiaAccessibilityManager.Key(n.Source, e.key))
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
}, onFocus: (e) => {
|
|
209
|
+
pin({ currentTarget: e.currentTarget.parentElement });
|
|
210
|
+
SkiaScrollCtrl.EnsureVisible(n.Source); // inside every enclosing scroll
|
|
211
|
+
if (tabbingOut.current === n.Id)
|
|
212
|
+
tabbingOut.current = 0; // leaving a text field: it keeps no caret
|
|
213
|
+
else
|
|
214
|
+
n.Source.OnAccessibilityFocused(true); // a text field takes the caret, as after a click
|
|
215
|
+
n.Source.NotifyAccessibilityFocused(true);
|
|
216
|
+
setFocusTick((t) => t + 1);
|
|
217
|
+
}, onBlur: (e) => {
|
|
218
|
+
if (TextInputProxy.IsProxyTarget(e.relatedTarget))
|
|
219
|
+
return; // a text field moved DOM focus to its own input
|
|
220
|
+
n.Source.OnAccessibilityFocused(false);
|
|
221
|
+
n.Source.NotifyAccessibilityFocused(false);
|
|
222
|
+
}, children: n.Label }, n.Id)) : n.TextLines ? (
|
|
169
223
|
// AccessibilityTextSelectable: one positioned span per drawn line, in the drawn font, so the browser selects and copies it like HTML
|
|
170
224
|
_jsx("div", { role: n.Role, title: n.Hint, "aria-live": n.Live, className: "drawnui-a11y-node drawnui-a11y-text", style: pos, children: n.TextLines.map((l, i) => (_jsxs("span", { "data-w": l.Width, style: { left: l.Left, top: l.Top, height: l.Height, fontFamily: l.FontFamily, fontWeight: l.FontWeight, fontSize: l.FontSize, lineHeight: `${l.Height}px` }, children: [l.Text, i < n.TextLines.length - 1 ? "\n" : ""] }, i))) }, n.Id)) : (
|
|
171
225
|
// static text is exposed as real (transparent) text content; aria-label only for roles that need a name
|
package/dist/react/reconciler.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ConcurrentRoot, DefaultEventPriority } from "react-reconciler/constants
|
|
|
4
4
|
import { SkiaLabel } from "../controls/SkiaLabel";
|
|
5
5
|
import { TextSpan } from "../controls/TextSpan";
|
|
6
6
|
import { SkiaRichLabel } from "../controls/SkiaRichLabel";
|
|
7
|
+
import { SkiaLabelFps } from "../controls/SkiaLabelFps";
|
|
7
8
|
import { SkiaSwitch } from "../controls/SkiaSwitch";
|
|
8
9
|
import { SkiaCheckbox } from "../controls/SkiaCheckbox";
|
|
9
10
|
import { SkiaRadioButton } from "../controls/SkiaRadioButton";
|
|
@@ -31,7 +32,7 @@ import { SkiaScroll } from "../controls/SkiaScroll";
|
|
|
31
32
|
import { SkiaFrame, SkiaShape } from "../controls/SkiaShape";
|
|
32
33
|
/** JSX tag name -> engine class. Add a control here to expose it to React. */
|
|
33
34
|
export const Registry = {
|
|
34
|
-
SkiaLayout, SkiaStack, SkiaRow, SkiaLayer, SkiaWrap, SkiaGrid, SkiaDecoratedGrid, SkiaBackdrop, SkiaEditor, SkiaSprite, SkiaSpriteSet, SkiaLabel, SkiaRichLabel, TextSpan, SkiaHotspot, SkiaButton, SkiaImage, SkiaImageTiles, SkiaSvg, SkiaLottie, SkiaGif, SkiaScroll, SkiaScrollBar, RefreshIndicator, SkiaShape, SkiaFrame,
|
|
35
|
+
SkiaLayout, SkiaStack, SkiaRow, SkiaLayer, SkiaWrap, SkiaGrid, SkiaDecoratedGrid, SkiaBackdrop, SkiaEditor, SkiaSprite, SkiaSpriteSet, SkiaLabel, SkiaLabelFps, SkiaRichLabel, TextSpan, SkiaHotspot, SkiaButton, SkiaImage, SkiaImageTiles, SkiaSvg, SkiaLottie, SkiaGif, SkiaScroll, SkiaScrollBar, RefreshIndicator, SkiaShape, SkiaFrame,
|
|
35
36
|
SkiaSwitch, SkiaCheckbox, SkiaRadioButton, SkiaProgress, SkiaSlider, SkiaCarousel, SkiaShaderCarousel, SkiaDrawer,
|
|
36
37
|
};
|
|
37
38
|
const SKIP = new Set(["children", "key", "ref"]);
|
package/dist/vite/index.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
|
+
/**
|
|
3
|
+
* Versioned addresses for the files of the public folder (fonts, images, sprites, shaders, Lottie, favicon). Their
|
|
4
|
+
* names never change, and a browser or CDN may keep them for hours, so after a publish a returning visitor could
|
|
5
|
+
* get new code with an old file. On build every public file gets a short content hash: index.html receives the map
|
|
6
|
+
* as `globalThis.DrawnUiAssetStamps` (read by `Super.ResolveAssetUrl`, which every engine loader fetches through)
|
|
7
|
+
* and its own `href` / `src` links to public files get `?v=<hash>`. A changed file gets a new address, an unchanged
|
|
8
|
+
* one keeps its address and stays cached. Bundled files (hashed names) need nothing. The dev server is left alone.
|
|
9
|
+
*/
|
|
10
|
+
export declare function drawnUiAssetStamps(): Plugin;
|
|
2
11
|
export interface DrawnUiStaticOptions {
|
|
3
12
|
/**
|
|
4
13
|
* Pages to generate, as URL hashes / paths appended to the site root ("" = the root page, "#/shapes"...).
|
package/dist/vite/index.js
CHANGED
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Vite helpers for DrawnUi.React apps: `import { drawnUiStatic } from "drawnui-react/vite"`.
|
|
2
|
+
* Vite helpers for DrawnUi.React apps: `import { drawnUiStatic, drawnUiAssetStamps } from "drawnui-react/vite"`.
|
|
3
3
|
* Build-time only; the runtime bundle never imports this module and the frame loop knows nothing about it.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
|
+
/**
|
|
9
|
+
* Versioned addresses for the files of the public folder (fonts, images, sprites, shaders, Lottie, favicon). Their
|
|
10
|
+
* names never change, and a browser or CDN may keep them for hours, so after a publish a returning visitor could
|
|
11
|
+
* get new code with an old file. On build every public file gets a short content hash: index.html receives the map
|
|
12
|
+
* as `globalThis.DrawnUiAssetStamps` (read by `Super.ResolveAssetUrl`, which every engine loader fetches through)
|
|
13
|
+
* and its own `href` / `src` links to public files get `?v=<hash>`. A changed file gets a new address, an unchanged
|
|
14
|
+
* one keeps its address and stays cached. Bundled files (hashed names) need nothing. The dev server is left alone.
|
|
15
|
+
*/
|
|
16
|
+
export function drawnUiAssetStamps() {
|
|
17
|
+
let stamps = {};
|
|
18
|
+
return {
|
|
19
|
+
name: "drawnui-asset-stamps",
|
|
20
|
+
apply: "build",
|
|
21
|
+
configResolved(config) {
|
|
22
|
+
const dir = config.publicDir;
|
|
23
|
+
if (!dir)
|
|
24
|
+
return;
|
|
25
|
+
for (const rel of readdirSync(dir, { recursive: true })) {
|
|
26
|
+
const file = join(dir, rel);
|
|
27
|
+
if (statSync(file).isFile())
|
|
28
|
+
stamps[rel.replace(/\\/g, "/")] = createHash("sha256").update(readFileSync(file)).digest("hex").slice(0, 10);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
transformIndexHtml(html) {
|
|
32
|
+
const stamped = html.replace(/\b(href|src)="([^"?#:]+)"/g, (m, attr, url) => {
|
|
33
|
+
const v = stamps[url.replace(/^\.?\//, "")];
|
|
34
|
+
return v ? `${attr}="${url}?v=${v}"` : m;
|
|
35
|
+
});
|
|
36
|
+
return { html: stamped, tags: [{ tag: "script", children: `globalThis.DrawnUiAssetStamps=${JSON.stringify(stamps)}`, injectTo: "head-prepend" }] };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
7
40
|
/**
|
|
8
41
|
* Generates visible, semantic HTML for crawlers and AI agents from the app's own accessibility tree, at build time.
|
|
9
42
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drawnui-react",
|
|
3
|
-
"version": "0.1.0-preview.
|
|
3
|
+
"version": "0.1.0-preview.13",
|
|
4
4
|
"description": "DrawnUi for React: the DrawnUi (.NET) drawn-UI engine in TypeScript on CanvasKit (Skia WASM). Same controls, same PascalCase API as DrawnUi.Net.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nick Kovalsky (AppoMobi)",
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
"typecheck": "tsc --noEmit",
|
|
54
54
|
"build": "tsc --noEmit && node dev/build-samples.mjs",
|
|
55
55
|
"sync:skills": "node dev/sync-skills.mjs",
|
|
56
|
+
"check:recycling": "node dev/check.mjs dev/check-recycling.ts",
|
|
57
|
+
"check:keyboard": "node dev/check.mjs dev/check-keyboard.ts",
|
|
56
58
|
"site:extras": "node dev/site-extras.mjs dist/demo",
|
|
57
59
|
"build:lib": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
|
|
58
60
|
"prepublishOnly": "npm run build:lib",
|