drawnui-react 0.1.0-preview.16 → 0.1.0-preview.18
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 +1 -1
- package/SKIPPED.md +7 -7
- package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
- package/dist/controls/AnimatedFramesRenderer.js +16 -1
- package/dist/controls/DrawnGame.d.ts +4 -1
- package/dist/controls/DrawnGame.js +7 -3
- package/dist/controls/GridStructure.d.ts +13 -1
- package/dist/controls/GridStructure.js +38 -2
- package/dist/controls/SkiaButton.d.ts +12 -2
- package/dist/controls/SkiaButton.js +19 -6
- package/dist/controls/SkiaCarousel.d.ts +1 -1
- package/dist/controls/SkiaCarousel.js +3 -2
- package/dist/controls/SkiaEditor.d.ts +5 -1
- package/dist/controls/SkiaEditor.js +19 -17
- package/dist/controls/SkiaGif.d.ts +3 -0
- package/dist/controls/SkiaGif.js +11 -1
- package/dist/controls/SkiaImage.js +15 -10
- package/dist/controls/SkiaLabel.d.ts +52 -3
- package/dist/controls/SkiaLabel.js +222 -50
- package/dist/controls/SkiaLayout.d.ts +5 -0
- package/dist/controls/SkiaLayout.js +7 -4
- package/dist/controls/SkiaLottie.js +3 -1
- package/dist/controls/SkiaProgress.d.ts +2 -1
- package/dist/controls/SkiaProgress.js +4 -1
- package/dist/controls/SkiaRadioButton.js +3 -1
- package/dist/controls/SkiaScroll.d.ts +17 -1
- package/dist/controls/SkiaScroll.js +84 -9
- package/dist/controls/SkiaShape.d.ts +13 -2
- package/dist/controls/SkiaShape.js +50 -10
- package/dist/controls/SkiaSlider.d.ts +10 -2
- package/dist/controls/SkiaSlider.js +15 -2
- package/dist/controls/SkiaSprite.d.ts +1 -0
- package/dist/controls/SkiaSprite.js +4 -0
- package/dist/controls/SkiaToggle.d.ts +5 -1
- package/dist/controls/SkiaToggle.js +11 -2
- package/dist/controls/SnappingLayout.d.ts +10 -1
- package/dist/controls/SnappingLayout.js +16 -1
- package/dist/core/Accessibility.d.ts +13 -0
- package/dist/core/Accessibility.js +10 -4
- package/dist/core/Animators.d.ts +28 -1
- package/dist/core/Animators.js +76 -8
- package/dist/core/Canvas.d.ts +21 -4
- package/dist/core/Canvas.js +122 -23
- package/dist/core/Easing.d.ts +12 -1
- package/dist/core/Easing.js +25 -1
- package/dist/core/Gestures.d.ts +24 -1
- package/dist/core/Gestures.js +75 -2
- package/dist/core/Hover.d.ts +14 -4
- package/dist/core/Hover.js +38 -22
- package/dist/core/ScrollAnimators.js +9 -0
- package/dist/core/SkiaControl.d.ts +27 -6
- package/dist/core/SkiaControl.js +81 -38
- package/dist/react/SkiaShell.js +33 -14
- package/dist/react/index.js +29 -2
- package/package.json +16 -1
package/dist/core/Hover.d.ts
CHANGED
|
@@ -13,15 +13,23 @@ export declare class HoverManager {
|
|
|
13
13
|
/** The hovered controls, root first. */
|
|
14
14
|
readonly Hovered: SkiaControl[];
|
|
15
15
|
private readonly next;
|
|
16
|
-
|
|
16
|
+
/** The controls under the pointer at the last hit test, root first, and the pointer in each one's own space. */
|
|
17
|
+
readonly Path: SkiaControl[];
|
|
18
|
+
readonly PathX: number[];
|
|
19
|
+
readonly PathY: number[];
|
|
17
20
|
private readonly movers;
|
|
18
21
|
private hasPointer;
|
|
19
22
|
private x;
|
|
20
23
|
private y;
|
|
21
24
|
private paused;
|
|
22
25
|
private check;
|
|
23
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* The mouse (or a pen in range) moved to (x, y), canvas pixels, nothing pressed. The hit path is taken also while
|
|
28
|
+
* hover waits (the cursor follows it), hover changes only when nothing moves.
|
|
29
|
+
*/
|
|
24
30
|
Move(root: SkiaControl | undefined, x: number, y: number): void;
|
|
31
|
+
/** Fills Path with the controls under the pointer by the routing rules of ProcessGestures. */
|
|
32
|
+
private HitTest;
|
|
25
33
|
/** The pointer left the canvas: nothing is hovered, also while content moves. */
|
|
26
34
|
Leave(): void;
|
|
27
35
|
/** The controls under a still pointer may have changed: checked once at the end of the frame. */
|
|
@@ -31,7 +39,9 @@ export declare class HoverManager {
|
|
|
31
39
|
/** End of a frame: pause while content moves, else run a pending check (at most one a frame, none while pressed). */
|
|
32
40
|
AfterFrame(root: SkiaControl | undefined, pressed: boolean): void;
|
|
33
41
|
private static Shown;
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The hovered set becomes the controls of the hit path that take hover (`fromPath`), or none: the ones that left get
|
|
44
|
+
* HoverChanged(false), then the new ones (true).
|
|
45
|
+
*/
|
|
36
46
|
private Apply;
|
|
37
47
|
}
|
package/dist/core/Hover.js
CHANGED
|
@@ -13,29 +13,50 @@ export class HoverManager {
|
|
|
13
13
|
/** The hovered controls, root first. */
|
|
14
14
|
Hovered = [];
|
|
15
15
|
next = [];
|
|
16
|
-
|
|
16
|
+
/** The controls under the pointer at the last hit test, root first, and the pointer in each one's own space. */
|
|
17
|
+
Path = [];
|
|
18
|
+
PathX = [];
|
|
19
|
+
PathY = [];
|
|
17
20
|
movers = [];
|
|
18
21
|
hasPointer = false;
|
|
19
22
|
x = 0;
|
|
20
23
|
y = 0;
|
|
21
24
|
paused = false;
|
|
22
25
|
check = false;
|
|
23
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* The mouse (or a pen in range) moved to (x, y), canvas pixels, nothing pressed. The hit path is taken also while
|
|
28
|
+
* hover waits (the cursor follows it), hover changes only when nothing moves.
|
|
29
|
+
*/
|
|
24
30
|
Move(root, x, y) {
|
|
25
31
|
this.hasPointer = true;
|
|
26
32
|
this.x = x;
|
|
27
33
|
this.y = y;
|
|
34
|
+
this.HitTest(root);
|
|
28
35
|
if (this.paused)
|
|
29
36
|
return; // content moves under the pointer: what was hovered stays, one check when it stops
|
|
30
37
|
this.check = false;
|
|
31
|
-
this.
|
|
38
|
+
this.Apply(true);
|
|
39
|
+
}
|
|
40
|
+
/** Fills Path with the controls under the pointer by the routing rules of ProcessGestures. */
|
|
41
|
+
HitTest(root) {
|
|
42
|
+
const path = this.Path;
|
|
43
|
+
path.length = 0;
|
|
44
|
+
this.PathX.length = 0;
|
|
45
|
+
this.PathY.length = 0;
|
|
46
|
+
if (root && root.IsVisible && !root.InputTransparent) {
|
|
47
|
+
const point = root.TransformPointToLocalSpace(new SKPoint(this.x, this.y));
|
|
48
|
+
if (root.HitIsInside(point.X, point.Y))
|
|
49
|
+
root.CollectHovered(point, path, this.PathX, this.PathY);
|
|
50
|
+
}
|
|
32
51
|
}
|
|
33
52
|
/** The pointer left the canvas: nothing is hovered, also while content moves. */
|
|
34
53
|
Leave() {
|
|
35
54
|
this.hasPointer = false;
|
|
36
55
|
this.check = false;
|
|
37
|
-
this.
|
|
38
|
-
this.
|
|
56
|
+
this.Path.length = 0;
|
|
57
|
+
this.PathX.length = 0;
|
|
58
|
+
this.PathY.length = 0;
|
|
59
|
+
this.Apply(false);
|
|
39
60
|
}
|
|
40
61
|
/** The controls under a still pointer may have changed: checked once at the end of the frame. */
|
|
41
62
|
RequestCheck() { if (this.hasPointer)
|
|
@@ -72,7 +93,8 @@ export class HoverManager {
|
|
|
72
93
|
this.check = false;
|
|
73
94
|
if (!this.hasPointer || pressed)
|
|
74
95
|
return;
|
|
75
|
-
this.
|
|
96
|
+
this.HitTest(root);
|
|
97
|
+
this.Apply(true);
|
|
76
98
|
}
|
|
77
99
|
static Shown(control) {
|
|
78
100
|
if (control.IsDisposed || !control.Superview)
|
|
@@ -82,23 +104,17 @@ export class HoverManager {
|
|
|
82
104
|
return false;
|
|
83
105
|
return true;
|
|
84
106
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const point = root.TransformPointToLocalSpace(new SKPoint(this.x, this.y));
|
|
91
|
-
if (root.HitIsInside(point.X, point.Y))
|
|
92
|
-
root.CollectHovered(point, path);
|
|
93
|
-
}
|
|
94
|
-
for (const c of path)
|
|
95
|
-
if (c.ReceivesHover)
|
|
96
|
-
next.push(c);
|
|
97
|
-
this.Apply();
|
|
98
|
-
}
|
|
99
|
-
/** `next` becomes the hovered set: the ones that left get HoverChanged(false), then the new ones (true). */
|
|
100
|
-
Apply() {
|
|
107
|
+
/**
|
|
108
|
+
* The hovered set becomes the controls of the hit path that take hover (`fromPath`), or none: the ones that left get
|
|
109
|
+
* HoverChanged(false), then the new ones (true).
|
|
110
|
+
*/
|
|
111
|
+
Apply(fromPath) {
|
|
101
112
|
const now = this.Hovered, next = this.next;
|
|
113
|
+
next.length = 0;
|
|
114
|
+
if (fromPath)
|
|
115
|
+
for (const c of this.Path)
|
|
116
|
+
if (c.ReceivesHover)
|
|
117
|
+
next.push(c);
|
|
102
118
|
if (now.length === next.length) {
|
|
103
119
|
let same = true;
|
|
104
120
|
for (let i = 0; i < now.length && same; i++)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Mirrors DrawnUi scrolling physics: DecelerationTimingParameters + ScrollFlingAnimator (fling),
|
|
2
2
|
// Spring + SpringTimingParameters + SpringWithVelocityAnimator (bounce), VelocityAccumulator, RubberBandUtils.
|
|
3
3
|
import { SkiaValueAnimator } from "./Animators";
|
|
4
|
+
import { VelocityTrail } from "./Gestures";
|
|
4
5
|
/** UIKit-style exponential deceleration: value(t) = v0 * (rate^(1000 t) - 1) / k, k = 1000 ln(rate). */
|
|
5
6
|
export class DecelerationTimingParameters {
|
|
6
7
|
InitialValue;
|
|
@@ -201,6 +202,14 @@ export class VelocityAccumulator {
|
|
|
201
202
|
Clear() { this.samples.length = 0; }
|
|
202
203
|
CaptureVelocity(x, y, arrivedTimeNanos = 0) {
|
|
203
204
|
const time = arrivedTimeNanos > 0 ? arrivedTimeNanos / 1e6 : performance.now();
|
|
205
|
+
// a move in the same burst as the last one replaces it: the first move of a burst carries only part of the burst's
|
|
206
|
+
// distance (DrawnUi.Rust VelocityAccumulator, 6d525f7)
|
|
207
|
+
const last = this.samples[this.samples.length - 1];
|
|
208
|
+
if (last && time - last.time < VelocityTrail.BurstMs) {
|
|
209
|
+
last.X = x;
|
|
210
|
+
last.Y = y;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
204
213
|
if (this.samples.length === VelocityAccumulator.MaxSampleSize)
|
|
205
214
|
this.samples.shift();
|
|
206
215
|
this.samples.push({ X: x, Y: y, time });
|
|
@@ -166,15 +166,15 @@ export declare class SkiaControl {
|
|
|
166
166
|
private compositeDeep?;
|
|
167
167
|
/** Set while an UpdateDraw climbs: the ancestors re-measure but an ImageComposite among them keeps its partial record. */
|
|
168
168
|
private static drawOrigin;
|
|
169
|
-
/** Nonzero while a composite redraws only some areas: layouts skip children entirely outside the clip. */
|
|
170
|
-
static CompositionCulling: number;
|
|
171
169
|
/** The children a composite record can re-record individually; layouts return their views. */
|
|
172
170
|
protected GetCompositeChildren(): readonly SkiaControl[];
|
|
173
171
|
/** C# TrackChildAsDirty (DirtyChildrenTracker): the child changed without a remeasure of this control. */
|
|
174
172
|
TrackChildAsDirty(child: SkiaControl): void;
|
|
175
173
|
private static readonly rejectRect;
|
|
176
|
-
/**
|
|
177
|
-
|
|
174
|
+
/** Its cache must be recorded before it can be drawn (none, dirty, another type, scale, size or GPU context). */
|
|
175
|
+
private CacheStale;
|
|
176
|
+
/** Its rect grown by its effects margin, through the matrix it is drawn with, lies outside the canvas clip. */
|
|
177
|
+
private OutsideClip;
|
|
178
178
|
/** Drawn bounds in canvas pixels including transforms and effects margins (C# GetTransformedDirtyBounds). */
|
|
179
179
|
GetTransformedDirtyBounds(): SKRect;
|
|
180
180
|
/** Current cache, if any. */
|
|
@@ -214,7 +214,7 @@ export declare class SkiaControl {
|
|
|
214
214
|
* top-most first, by the routing rules of ProcessGestures. True when this subtree blocks gestures below it, so the
|
|
215
215
|
* controls under it get no hover (a popup over a list).
|
|
216
216
|
*/
|
|
217
|
-
CollectHovered(point: SKPoint, into: SkiaControl[]): boolean;
|
|
217
|
+
CollectHovered(point: SKPoint, into: SkiaControl[], xs?: number[], ys?: number[]): boolean;
|
|
218
218
|
TouchEffectColor: Color;
|
|
219
219
|
/** Plays a ripple (from the tap point) when Tapped fires. */
|
|
220
220
|
AnimationTapped: SkiaTouchAnimation;
|
|
@@ -332,6 +332,8 @@ export declare class SkiaControl {
|
|
|
332
332
|
* host's pointer cursor. Default: the control as a whole (AccessibilityCanInteract); SkiaLabel adds its tappable spans.
|
|
333
333
|
*/
|
|
334
334
|
WantsPointerCursor(_x: number, _y: number): boolean;
|
|
335
|
+
/** A range control's value for screen readers (slider, progress); undefined for everything else (drawnui-cross 6c). */
|
|
336
|
+
get AccessibilityValue(): import("./Accessibility").AccessibilityValue | undefined;
|
|
335
337
|
/** aria-pressed for toggles; undefined = not a toggle. */
|
|
336
338
|
get AccessibilityIsPressed(): boolean | undefined;
|
|
337
339
|
set AccessibilityIsPressed(v: boolean | undefined);
|
|
@@ -373,8 +375,17 @@ export declare class SkiaControl {
|
|
|
373
375
|
* for the pointer. Keyboard navigation uses it: Tapped for Tab and Enter / Space, Panning for the arrow keys.
|
|
374
376
|
*/
|
|
375
377
|
CanReceiveGesture(gesture: SkiaGesturesParameters["Type"]): boolean;
|
|
376
|
-
/**
|
|
378
|
+
/**
|
|
379
|
+
* False while the control ignores input itself: RespondsToGestures = false on a button, toggle, slider, scroll...
|
|
380
|
+
* Taps, keyboard and the accessibility state (reported disabled) all follow it (drawnui-cross 6w). Default true.
|
|
381
|
+
*/
|
|
377
382
|
protected AcceptsInput(): boolean;
|
|
383
|
+
/**
|
|
384
|
+
* What a control that takes no input returns for a gesture that reached it (C# BlockedGestureResult): it keeps a
|
|
385
|
+
* press, tap or long press, so nothing under it reacts to them, and lets moves and wheel go on to the controls around
|
|
386
|
+
* it (a scroll behind it still scrolls).
|
|
387
|
+
*/
|
|
388
|
+
protected BlockedGestureResult(args: SkiaGesturesParameters): SkiaControl | null;
|
|
378
389
|
NotifyAccessibilityFocused(focused: boolean): void;
|
|
379
390
|
/** DrawnUi SmartMax: larger of two sizes, an infinite one loses. */
|
|
380
391
|
protected static SmartMax(a: number, b: number): number;
|
|
@@ -471,6 +482,16 @@ export declare class SkiaControl {
|
|
|
471
482
|
Update(): void;
|
|
472
483
|
/** Redraw without remeasure and WITHOUT dropping caches (position/overlay changes). */
|
|
473
484
|
Repaint(): void;
|
|
485
|
+
/**
|
|
486
|
+
* A parent (not the control itself) is hidden: a change here asks for no frame and the latest state shows when the
|
|
487
|
+
* parent is shown again (C# 329f6c44 HiddenUpdateTests, DrawnUi.Rust hidden_updates). The control's own visibility
|
|
488
|
+
* change still draws: it is the one that hides or shows it.
|
|
489
|
+
*/
|
|
490
|
+
UnderHiddenParent(): boolean;
|
|
491
|
+
/** The control or a parent is hidden: its animators pause until it shows again. */
|
|
492
|
+
HiddenInTree(): boolean;
|
|
493
|
+
/** The control an invalidation started at while it climbs (InvalidateMeasure): a hidden parent of it wakes no frame. */
|
|
494
|
+
private static invalidateOrigin;
|
|
474
495
|
/**
|
|
475
496
|
* Transform / Opacity changed: this control's own cache is still valid (content unchanged) but every ancestor
|
|
476
497
|
* cache holds its composited output, so those are staled before redrawing (DrawnUi RedrawCanvas + parent invalidation).
|
package/dist/core/SkiaControl.js
CHANGED
|
@@ -189,17 +189,24 @@ export class SkiaControl {
|
|
|
189
189
|
compositeDeep;
|
|
190
190
|
/** Set while an UpdateDraw climbs: the ancestors re-measure but an ImageComposite among them keeps its partial record. */
|
|
191
191
|
static drawOrigin = null;
|
|
192
|
-
/** Nonzero while a composite redraws only some areas: layouts skip children entirely outside the clip. */
|
|
193
|
-
static CompositionCulling = 0;
|
|
194
192
|
/** The children a composite record can re-record individually; layouts return their views. */
|
|
195
193
|
GetCompositeChildren() { return []; }
|
|
196
194
|
/** C# TrackChildAsDirty (DirtyChildrenTracker): the child changed without a remeasure of this control. */
|
|
197
195
|
TrackChildAsDirty(child) { if (this.IsCacheComposite)
|
|
198
196
|
this.DirtyChildrenInternal.add(child); }
|
|
199
197
|
static rejectRect = new Float32Array(4);
|
|
200
|
-
/**
|
|
201
|
-
|
|
202
|
-
const
|
|
198
|
+
/** Its cache must be recorded before it can be drawn (none, dirty, another type, scale, size or GPU context). */
|
|
199
|
+
CacheStale(scale) {
|
|
200
|
+
const cacheType = this.UsingCacheType;
|
|
201
|
+
if (cacheType === "None")
|
|
202
|
+
return false;
|
|
203
|
+
const o = this.RenderObject, r = cacheType === "Operations" ? this.ExpandedCacheRect(scale) : this.AlignedCacheRect(scale);
|
|
204
|
+
return this.cacheDirty || !o || o.Type !== cacheType || o.Epoch !== (this.Superview?.GpuEpoch ?? 0) || o.Scale !== scale
|
|
205
|
+
|| Math.round(o.Bounds.Width) !== Math.round(r.Width) || Math.round(o.Bounds.Height) !== Math.round(r.Height);
|
|
206
|
+
}
|
|
207
|
+
/** Its rect grown by its effects margin, through the matrix it is drawn with, lies outside the canvas clip. */
|
|
208
|
+
OutsideClip(canvas, scale) {
|
|
209
|
+
const b = SkiaControl.MapRect(this.RenderTransformMatrix, this.ExpandedCacheRect(scale)), q = SkiaControl.rejectRect;
|
|
203
210
|
q[0] = b.Left;
|
|
204
211
|
q[1] = b.Top;
|
|
205
212
|
q[2] = b.Right;
|
|
@@ -259,15 +266,17 @@ export class SkiaControl {
|
|
|
259
266
|
* top-most first, by the routing rules of ProcessGestures. True when this subtree blocks gestures below it, so the
|
|
260
267
|
* controls under it get no hover (a popup over a list).
|
|
261
268
|
*/
|
|
262
|
-
CollectHovered(point, into) {
|
|
269
|
+
CollectHovered(point, into, xs, ys) {
|
|
263
270
|
into.push(this);
|
|
271
|
+
xs?.push(point.X);
|
|
272
|
+
ys?.push(point.Y);
|
|
264
273
|
if (!this.CheckChildrenGesturesLocked("Pointer")) {
|
|
265
274
|
const listeners = this.GetGestureListeners();
|
|
266
275
|
for (let i = listeners.length - 1; i >= 0; i--) {
|
|
267
276
|
const child = listeners[i];
|
|
268
277
|
if (!child.IsVisible || child.InputTransparent || !this.IsGestureForChild(child, point))
|
|
269
278
|
continue;
|
|
270
|
-
if (child.CollectHovered(child.TransformPointToLocalSpace(point), into))
|
|
279
|
+
if (child.CollectHovered(child.TransformPointToLocalSpace(point), into, xs, ys))
|
|
271
280
|
return true;
|
|
272
281
|
}
|
|
273
282
|
}
|
|
@@ -528,6 +537,8 @@ export class SkiaControl {
|
|
|
528
537
|
* host's pointer cursor. Default: the control as a whole (AccessibilityCanInteract); SkiaLabel adds its tappable spans.
|
|
529
538
|
*/
|
|
530
539
|
WantsPointerCursor(_x, _y) { return this.AccessibilityCanInteract; }
|
|
540
|
+
/** A range control's value for screen readers (slider, progress); undefined for everything else (drawnui-cross 6c). */
|
|
541
|
+
get AccessibilityValue() { return undefined; }
|
|
531
542
|
/** aria-pressed for toggles; undefined = not a toggle. */
|
|
532
543
|
get AccessibilityIsPressed() { return this.accessibilityIsPressed; }
|
|
533
544
|
set AccessibilityIsPressed(v) { if (this.accessibilityIsPressed !== v) {
|
|
@@ -622,8 +633,19 @@ export class SkiaControl {
|
|
|
622
633
|
return false;
|
|
623
634
|
return true;
|
|
624
635
|
}
|
|
625
|
-
/**
|
|
636
|
+
/**
|
|
637
|
+
* False while the control ignores input itself: RespondsToGestures = false on a button, toggle, slider, scroll...
|
|
638
|
+
* Taps, keyboard and the accessibility state (reported disabled) all follow it (drawnui-cross 6w). Default true.
|
|
639
|
+
*/
|
|
626
640
|
AcceptsInput() { return true; }
|
|
641
|
+
/**
|
|
642
|
+
* What a control that takes no input returns for a gesture that reached it (C# BlockedGestureResult): it keeps a
|
|
643
|
+
* press, tap or long press, so nothing under it reacts to them, and lets moves and wheel go on to the controls around
|
|
644
|
+
* it (a scroll behind it still scrolls).
|
|
645
|
+
*/
|
|
646
|
+
BlockedGestureResult(args) {
|
|
647
|
+
return args.Type === "Down" || args.Type === "Tapped" || args.Type === "LongPressing" ? this : null;
|
|
648
|
+
}
|
|
627
649
|
NotifyAccessibilityFocused(focused) { this.Superview?.AccessibilityManager.NotifyFocused(focused ? this : undefined); }
|
|
628
650
|
/** DrawnUi SmartMax: larger of two sizes, an infinite one loses. */
|
|
629
651
|
static SmartMax(a, b) {
|
|
@@ -691,6 +713,23 @@ export class SkiaControl {
|
|
|
691
713
|
const offsetOnly = (this.Left !== 0 || this.Top !== 0) && this.UsingCacheType !== "None" && this.TranslationX === 0 && this.TranslationY === 0
|
|
692
714
|
&& this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0;
|
|
693
715
|
const needTransform = this.HasTransform && !offsetOnly;
|
|
716
|
+
let dx = 0, dy = 0;
|
|
717
|
+
if (needTransform) {
|
|
718
|
+
this.RenderTransformMatrix = this.CreateRenderTransformMatrix(this.DrawingRect, ctx.Scale);
|
|
719
|
+
}
|
|
720
|
+
else if (offsetOnly) {
|
|
721
|
+
dx = this.Left * ctx.Scale;
|
|
722
|
+
dy = this.Top * ctx.Scale;
|
|
723
|
+
this.RenderTransformMatrix = Super.CK.Matrix.translated(dx, dy); // gestures / accessibility still map through it
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
this.RenderTransformMatrix = undefined;
|
|
727
|
+
}
|
|
728
|
+
// drawn entirely outside the clip: skipped with its subtree (C# layout Virtualisation, DrawnUi.Rust); the matrix
|
|
729
|
+
// above stays current for hit tests. A cache that must be recorded is still recorded here, as before, so its cost
|
|
730
|
+
// does not move into the frame where the control scrolls in (DrawnUi.Rust records it then)
|
|
731
|
+
if (this.OutsideClip(canvas, ctx.Scale) && !this.CacheStale(ctx.Scale))
|
|
732
|
+
return;
|
|
694
733
|
let saved = false;
|
|
695
734
|
// same as DrawnUi: opacity = a layer with alpha, transforms = canvas matrix around the whole subtree (cache included)
|
|
696
735
|
if (applyOpacity) {
|
|
@@ -704,19 +743,8 @@ export class SkiaControl {
|
|
|
704
743
|
canvas.save();
|
|
705
744
|
saved = true;
|
|
706
745
|
}
|
|
707
|
-
|
|
708
|
-
if (needTransform) {
|
|
709
|
-
this.RenderTransformMatrix = this.CreateRenderTransformMatrix(this.DrawingRect, ctx.Scale);
|
|
746
|
+
if (needTransform)
|
|
710
747
|
canvas.concat(this.RenderTransformMatrix);
|
|
711
|
-
}
|
|
712
|
-
else if (offsetOnly) {
|
|
713
|
-
dx = this.Left * ctx.Scale;
|
|
714
|
-
dy = this.Top * ctx.Scale;
|
|
715
|
-
this.RenderTransformMatrix = Super.CK.Matrix.translated(dx, dy); // gestures / accessibility still map through it
|
|
716
|
-
}
|
|
717
|
-
else {
|
|
718
|
-
this.RenderTransformMatrix = undefined;
|
|
719
|
-
}
|
|
720
748
|
if (this.IsClippedToBounds) {
|
|
721
749
|
if (!saved) {
|
|
722
750
|
canvas.save();
|
|
@@ -781,10 +809,7 @@ export class SkiaControl {
|
|
|
781
809
|
const epoch = this.Superview?.GpuEpoch ?? 0;
|
|
782
810
|
if (this.RenderObjectPrevious && this.RenderObjectPrevious.Epoch !== epoch)
|
|
783
811
|
this.DisposePrevious(); // made on a lost GPU context
|
|
784
|
-
|
|
785
|
-
|| this.RenderObject.Scale !== ctx.Scale
|
|
786
|
-
|| Math.round(this.RenderObject.Bounds.Width) !== Math.round(r.Width) || Math.round(this.RenderObject.Bounds.Height) !== Math.round(r.Height);
|
|
787
|
-
if (stale)
|
|
812
|
+
if (this.CacheStale(ctx.Scale))
|
|
788
813
|
this.CreateRenderingObject({ ...ctx, Destination: this.DrawingRect }, cacheType);
|
|
789
814
|
if (this.RenderObject) {
|
|
790
815
|
// C# DrawRenderObject: with post renderers an Image cache is not blitted, the effects sample it (CachedImage)
|
|
@@ -982,13 +1007,8 @@ export class SkiaControl {
|
|
|
982
1007
|
this.DirtyChildrenInternal.add(c);
|
|
983
1008
|
for (const c of inside)
|
|
984
1009
|
this.DirtyChildrenInternal.add(c);
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
this.PaintContent({ ...ctx, Context: { Canvas: canvas, Surface: surface, Origin: { X: r.Left, Y: r.Top } } });
|
|
988
|
-
}
|
|
989
|
-
finally {
|
|
990
|
-
SkiaControl.CompositionCulling--;
|
|
991
|
-
}
|
|
1010
|
+
// children entirely outside the redrawn areas are skipped by their own clip check (Render)
|
|
1011
|
+
this.PaintContent({ ...ctx, Context: { Canvas: canvas, Surface: surface, Origin: { X: r.Left, Y: r.Top } } });
|
|
992
1012
|
this.IsRenderingWithComposition = false;
|
|
993
1013
|
const redrawn = children.filter((c) => this.DirtyChildrenInternal.has(c));
|
|
994
1014
|
this.LastCompositeRecord = { Mode: "partial", Children: redrawn.length, Dirty: redrawn, Areas: areas, Changed: changed };
|
|
@@ -1257,8 +1277,24 @@ export class SkiaControl {
|
|
|
1257
1277
|
}
|
|
1258
1278
|
/** Redraw without remeasure and WITHOUT dropping caches (position/overlay changes). */
|
|
1259
1279
|
Repaint() {
|
|
1260
|
-
this.
|
|
1280
|
+
if (!this.UnderHiddenParent())
|
|
1281
|
+
this.Superview?.Update();
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* A parent (not the control itself) is hidden: a change here asks for no frame and the latest state shows when the
|
|
1285
|
+
* parent is shown again (C# 329f6c44 HiddenUpdateTests, DrawnUi.Rust hidden_updates). The control's own visibility
|
|
1286
|
+
* change still draws: it is the one that hides or shows it.
|
|
1287
|
+
*/
|
|
1288
|
+
UnderHiddenParent() {
|
|
1289
|
+
for (let p = this.Parent; p; p = p.Parent)
|
|
1290
|
+
if (!p.IsVisible)
|
|
1291
|
+
return true;
|
|
1292
|
+
return false;
|
|
1261
1293
|
}
|
|
1294
|
+
/** The control or a parent is hidden: its animators pause until it shows again. */
|
|
1295
|
+
HiddenInTree() { return !this.IsVisible || this.UnderHiddenParent(); }
|
|
1296
|
+
/** The control an invalidation started at while it climbs (InvalidateMeasure): a hidden parent of it wakes no frame. */
|
|
1297
|
+
static invalidateOrigin = null;
|
|
1262
1298
|
/**
|
|
1263
1299
|
* Transform / Opacity changed: this control's own cache is still valid (content unchanged) but every ancestor
|
|
1264
1300
|
* cache holds its composited output, so those are staled before redrawing (DrawnUi RedrawCanvas + parent invalidation).
|
|
@@ -1356,12 +1392,19 @@ export class SkiaControl {
|
|
|
1356
1392
|
if (SkiaControl.drawOrigin === null || SkiaControl.drawOrigin === this)
|
|
1357
1393
|
this.compositeFull = true;
|
|
1358
1394
|
this.effectsMarginCache = undefined;
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
this.Parent.
|
|
1363
|
-
|
|
1364
|
-
this.
|
|
1395
|
+
const outer = SkiaControl.invalidateOrigin;
|
|
1396
|
+
SkiaControl.invalidateOrigin ??= this;
|
|
1397
|
+
try {
|
|
1398
|
+
if (this.Parent && this.IsParentIndependent)
|
|
1399
|
+
this.RepaintComposition();
|
|
1400
|
+
else if (this.Parent)
|
|
1401
|
+
this.Parent.InvalidateMeasure();
|
|
1402
|
+
else if (!SkiaControl.invalidateOrigin.UnderHiddenParent())
|
|
1403
|
+
this.Superview?.Update();
|
|
1404
|
+
}
|
|
1405
|
+
finally {
|
|
1406
|
+
SkiaControl.invalidateOrigin = outer;
|
|
1407
|
+
}
|
|
1365
1408
|
}
|
|
1366
1409
|
// ---- gestures (same names as DrawnUi) ----
|
|
1367
1410
|
/** Hit rect in pixels (no transforms in this port, so it is DrawingRect). */
|
package/dist/react/SkiaShell.js
CHANGED
|
@@ -389,6 +389,12 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
|
|
|
389
389
|
prev ? prev.TranslateToAsync(-dir * w, 0, TabsAnimationSpeed, TabsEasing) : Promise.resolve(),
|
|
390
390
|
]), TabsAnimationSpeed + 500);
|
|
391
391
|
}
|
|
392
|
+
// the tab left stays mounted (its state kept): hidden first, then back in place for its next turn
|
|
393
|
+
if (prev) {
|
|
394
|
+
prev.IsVisible = false;
|
|
395
|
+
prev.TranslationX = 0;
|
|
396
|
+
prev.TranslationY = 0;
|
|
397
|
+
}
|
|
392
398
|
setTabTransition(null);
|
|
393
399
|
}, [hasTabs, Tabs, AnimateTabs, TabsAnimationSpeed]);
|
|
394
400
|
// popstate: the browser popped OUR last entry (back) — or moved forward to a hash we can rebuild
|
|
@@ -405,7 +411,13 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
|
|
|
405
411
|
}
|
|
406
412
|
const onPop = () => {
|
|
407
413
|
const depth = window.history.state?.[HISTORY_KEY] ?? 0;
|
|
408
|
-
|
|
414
|
+
const target = parseHash();
|
|
415
|
+
// a Back lands on one of OUR entries: its hash is the stack it was pushed with (the first N pages, N = the page
|
|
416
|
+
// entries below it). A typed hash or a plain <a href="#/route"> makes a new entry with no state (depth 0):
|
|
417
|
+
// that is a navigation to the hash, not a Back to the root
|
|
418
|
+
const pagesBelow = history.current.slice(0, depth).filter((k) => k === "page").length;
|
|
419
|
+
const back = depth < history.current.length && target.join("/") === live.current.stack.slice(0, pagesBelow).join("/");
|
|
420
|
+
if (back) {
|
|
409
421
|
// back: unwind our entries down to depth
|
|
410
422
|
void (async () => {
|
|
411
423
|
while (history.current.length > depth) {
|
|
@@ -441,9 +453,8 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
|
|
|
441
453
|
})();
|
|
442
454
|
}
|
|
443
455
|
else {
|
|
444
|
-
// forward (depth grew), or
|
|
456
|
+
// forward (depth grew), or another hash = a plain <a href="#/route"> link or a typed hash:
|
|
445
457
|
// the hash names the pages, rebuild the stack without animation (overlays cannot be restored)
|
|
446
|
-
const target = parseHash();
|
|
447
458
|
if (depth === history.current.length && target.join("/") === live.current.stack.join("/"))
|
|
448
459
|
return;
|
|
449
460
|
history.current = target.map(() => "page");
|
|
@@ -466,28 +477,36 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
|
|
|
466
477
|
const transitioning = transitions > 0 || leaving !== null;
|
|
467
478
|
const rootBottom = hasTabs ? TabBarHeight + insets.Bottom : 0;
|
|
468
479
|
const navTop = NavBarHeight + insets.Top;
|
|
469
|
-
|
|
480
|
+
// every tab keeps its root and its pages mounted (state, scroll positions), only the selected one shows (C# tab
|
|
481
|
+
// views, DrawnUi.Rust "tabs keep their stacks")
|
|
482
|
+
const tabIndexes = hasTabs ? Tabs.map((_, i) => i) : [0];
|
|
470
483
|
const entering = tabTransition ? { TranslationX: tabTransition.dir * 0.75 * (typeof window !== "undefined" ? window.innerWidth : 0), Opacity: 0.001, ZIndex: 1 } : {};
|
|
471
|
-
return (_jsx(ShellContext.Provider, { value: nav, children: _jsxs(SkiaLayer, { VerticalOptions: "Fill", children: [
|
|
472
|
-
|
|
473
|
-
|
|
484
|
+
return (_jsx(ShellContext.Provider, { value: nav, children: _jsxs(SkiaLayer, { VerticalOptions: "Fill", children: [tabIndexes.map((t) => {
|
|
485
|
+
const selected = t === selectedTab;
|
|
486
|
+
return (_jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, 0, 0, rootBottom), BackgroundColor: hasTabs ? PageBackgroundColor : undefined, IsVisible: selected ? visiblePages.length === 0 || transitioning : tabTransition?.from === t, ZIndex: 0, ...(selected ? entering : {}), ref: (c) => { if (c)
|
|
487
|
+
tabRoots.current.set(t, c); }, children: hasTabs ? RenderRoute(Tabs[t]?.route ?? "") : children }, hasTabs ? `tab-${t}` : "root"));
|
|
488
|
+
}), tabIndexes.flatMap((t) => (t === selectedTab ? visiblePages : stacks[t] ?? []).map((r, i) => (_jsxs(PageHost, { route: r, speed: PagesAnimationSpeed, active: t === selectedTab, visible: t === selectedTab && (r === topRoute || transitioning), background: PageBackgroundColor, bottom: rootBottom, register: (c) => { if (c)
|
|
474
489
|
pageCtrls.current.set(r, c);
|
|
475
490
|
else
|
|
476
|
-
pageCtrls.current.delete(r); }, children: [_jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, navTop, 0, 0), children: RenderRoute(r) }), _jsxs(SkiaLayer, { HeightRequest: navTop, BackgroundColor: NavBarColor, Padding: new Thickness(0, insets.Top, 0, 0), children: [_jsx(SkiaButton, { Text: "\u2039 Back", BackgroundColor: "#00000000", TextColor: "#6EA8FE", FontSize: 16, VerticalOptions: "Center", Margin: new Thickness(8, 0), ApplyEffect: "Ripple", Tapped: () => void GoBackAsync(), AccessibilityRole: "button", AccessibilityLabel: "Back" }), _jsx(SkiaLabel, { Text: Titles?.[RouteName(r)] ?? RouteName(r), FontSize: 18, FontFamily: "FontTextBold", TextColor: Colors.White, HorizontalOptions: "Fill", HorizontalTextAlignment: "Center", VerticalOptions: "Center", MaxLines: 1, Margin: new Thickness(96, 0), AccessibilityRole: "heading" }), _jsx(SkiaButton, { Text: "Home", BackgroundColor: "#00000000", TextColor: "#6EA8FE", FontSize: 16, HorizontalOptions: "End", VerticalOptions: "Center", Margin: new Thickness(0, 0, 8, 0), ApplyEffect: "Ripple", Tapped: () => void PopToRootAsync(), AccessibilityRole: "button", AccessibilityLabel: "Home" }), _jsx(SkiaLayer, { HeightRequest: 1, VerticalOptions: "End", BackgroundColor: "#343A40" })] })] }, `${
|
|
491
|
+
pageCtrls.current.delete(r); }, children: [_jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, navTop, 0, 0), children: RenderRoute(r) }), _jsxs(SkiaLayer, { HeightRequest: navTop, BackgroundColor: NavBarColor, Padding: new Thickness(0, insets.Top, 0, 0), children: [_jsx(SkiaButton, { Text: "\u2039 Back", BackgroundColor: "#00000000", TextColor: "#6EA8FE", FontSize: 16, VerticalOptions: "Center", Margin: new Thickness(8, 0), ApplyEffect: "Ripple", Tapped: () => void GoBackAsync(), AccessibilityRole: "button", AccessibilityLabel: "Back" }), _jsx(SkiaLabel, { Text: Titles?.[RouteName(r)] ?? RouteName(r), FontSize: 18, FontFamily: "FontTextBold", TextColor: Colors.White, HorizontalOptions: "Fill", HorizontalTextAlignment: "Center", VerticalOptions: "Center", MaxLines: 1, Margin: new Thickness(96, 0), AccessibilityRole: "heading" }), _jsx(SkiaButton, { Text: "Home", BackgroundColor: "#00000000", TextColor: "#6EA8FE", FontSize: 16, HorizontalOptions: "End", VerticalOptions: "Center", Margin: new Thickness(0, 0, 8, 0), ApplyEffect: "Ripple", Tapped: () => void PopToRootAsync(), AccessibilityRole: "button", AccessibilityLabel: "Home" }), _jsx(SkiaLayer, { HeightRequest: 1, VerticalOptions: "End", BackgroundColor: "#343A40" })] })] }, `${t}:${r}#${i}`)))), hasTabs && (_jsxs(SkiaLayer, { HeightRequest: TabBarHeight + insets.Bottom, VerticalOptions: "End", BackgroundColor: TabBarColor, ZIndex: ShellDefaults.ZIndexModals - 1, BlockGesturesBelow: true, Padding: new Thickness(0, 0, 0, insets.Bottom), children: [_jsx(SkiaLayer, { HeightRequest: 1, BackgroundColor: "#343A40" }), _jsx(SkiaGrid, { ColumnDefinitions: Tabs.map(() => "*").join(","), HorizontalOptions: "Fill", VerticalOptions: "Fill", children: Tabs.map((t, i) => (_jsxs(SkiaLayer, { Column: i, HorizontalOptions: "Fill", VerticalOptions: "Fill", AnimationTapped: "Ripple", Tapped: () => void SelectTabAsync(i), AccessibilityRole: "button", AccessibilityLabel: t.title, children: [_jsx(SkiaLabel, { Text: t.title, FontSize: 13, FontFamily: i === selectedTab ? "FontTextBold" : undefined, TextColor: i === selectedTab ? ShellDefaults.TabSelectedColor : ShellDefaults.TabColor, HorizontalOptions: "Center", VerticalOptions: "Center", AccessibilityRole: "presentation" }), i === selectedTab && _jsx(SkiaShape, { Type: "Rectangle", HeightRequest: 3, WidthRequest: 36, CornerRadius: 2, BackgroundColor: ShellDefaults.TabSelectedColor, HorizontalOptions: "Center", VerticalOptions: "End" })] }, t.route))) })] })), modals.map((m) => m.node), popups.map((p) => p.node), toasts.map((t) => t.node)] }) }));
|
|
477
492
|
});
|
|
478
493
|
/** A pushed page: slides in from the right on mount (C# SkiaViewSwitcher PushView), covers what is below. */
|
|
479
|
-
function PageHost({ route, speed, visible, background, bottom, register, children }) {
|
|
494
|
+
function PageHost({ route, speed, active, visible, background, bottom, register, children }) {
|
|
480
495
|
const ctrl = useRef(null);
|
|
496
|
+
// slides in once, when pushed; a page of another tab stays mounted and is registered only while its tab is selected
|
|
481
497
|
useEffect(() => {
|
|
482
498
|
const c = ctrl.current;
|
|
483
|
-
if (
|
|
484
|
-
return;
|
|
485
|
-
register(c);
|
|
486
|
-
if (speed > 0) {
|
|
499
|
+
if (c && speed > 0) {
|
|
487
500
|
c.TranslationX = CanvasWidthPts(c);
|
|
488
501
|
void c.TranslateToAsync(0, 0, speed);
|
|
489
502
|
}
|
|
490
|
-
return () => register(null);
|
|
491
503
|
}, [route]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
const c = ctrl.current;
|
|
506
|
+
if (!c || !active)
|
|
507
|
+
return;
|
|
508
|
+
register(c);
|
|
509
|
+
return () => register(null);
|
|
510
|
+
}, [route, active]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
492
511
|
return (_jsx(SkiaLayer, { ref: (c) => { ctrl.current = c; }, VerticalOptions: "Fill", HorizontalOptions: "Fill", Margin: new Thickness(0, 0, 0, bottom), BackgroundColor: background, IsVisible: visible, BlockGesturesBelow: true, children: children }));
|
|
493
512
|
}
|
package/dist/react/index.js
CHANGED
|
@@ -126,6 +126,13 @@ const NAVIGATION_KEYS = new Set(["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDow
|
|
|
126
126
|
* canvas. Keyboard and screen-reader input arrive as DOM events: Enter / Space activate (a Tapped), the navigation keys
|
|
127
127
|
* go to the focused control and then to its arrow-key group; a group is one Tab stop (roving tabindex).
|
|
128
128
|
*/
|
|
129
|
+
/** aria-valuenow / min / max / valuetext of a range control (drawnui-cross 6c); a slider reads as horizontal. */
|
|
130
|
+
function valueAttributes(n) {
|
|
131
|
+
const v = n.Value;
|
|
132
|
+
if (!v)
|
|
133
|
+
return undefined;
|
|
134
|
+
return { "aria-valuenow": v.Now, "aria-valuemin": v.Min, "aria-valuemax": v.Max, "aria-valuetext": v.Text || undefined, "aria-orientation": n.Role === "slider" ? "horizontal" : undefined };
|
|
135
|
+
}
|
|
129
136
|
function AccessibilityOverlay({ view }) {
|
|
130
137
|
const [nodes, setNodes] = useState(() => view.AccessibilityManager.Snapshot);
|
|
131
138
|
// keyboard focus moved by the manager (arrow keys in a group): focus that node's element once it is rendered
|
|
@@ -147,6 +154,23 @@ function AccessibilityOverlay({ view }) {
|
|
|
147
154
|
setFocusRequest(undefined);
|
|
148
155
|
elementOf(focusRequest.id)?.focus({ preventScroll: true });
|
|
149
156
|
}, [focusRequest]);
|
|
157
|
+
// the focused node went away (its page or popup closed): the focus goes to the first Tab stop on screen in reading
|
|
158
|
+
// order instead of falling back to the page (drawnui-cross 6c refocus); a focus the user moved away stays away
|
|
159
|
+
const focusedId = useRef(0);
|
|
160
|
+
useLayoutEffect(() => {
|
|
161
|
+
const id = focusedId.current;
|
|
162
|
+
if (!id || nodes.some((n) => n.Id === id))
|
|
163
|
+
return;
|
|
164
|
+
focusedId.current = 0;
|
|
165
|
+
const active = document.activeElement;
|
|
166
|
+
if (active && active !== document.body)
|
|
167
|
+
return;
|
|
168
|
+
const box = overlayRef.current?.getBoundingClientRect();
|
|
169
|
+
const first = nodes.find((n) => n.CanInteract && view.AccessibilityManager.IsTabStop(n.Source)
|
|
170
|
+
&& (!box || (n.Rect.Bottom > 0 && n.Rect.Top < box.height && n.Rect.Right > 0 && n.Rect.Left < box.width)));
|
|
171
|
+
if (first)
|
|
172
|
+
elementOf(first.Id)?.focus({ preventScroll: true });
|
|
173
|
+
}, [nodes]);
|
|
150
174
|
// 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
175
|
// field's own element (without taking the caret again) and the browser's Tab moves on from there
|
|
152
176
|
const tabbingOut = useRef(0);
|
|
@@ -198,7 +222,7 @@ function AccessibilityOverlay({ view }) {
|
|
|
198
222
|
return (_jsxs("div", { className: "drawnui-a11y-overlay", onScroll: pin, ref: overlayRef, children: [_jsx("style", { children: A11Y_CSS }), nodes.map((n) => {
|
|
199
223
|
const pos = { left: n.Rect.Left, top: n.Rect.Top, width: n.Rect.Width, height: n.Rect.Height };
|
|
200
224
|
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) => {
|
|
225
|
+
return n.CanInteract ? (_jsx("div", { "data-a11y-id": n.Id, role: n.Role, "aria-label": n.Label, title: n.Hint, ...valueAttributes(n), "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
226
|
if (e.key === "Enter" || e.key === " ") {
|
|
203
227
|
e.preventDefault();
|
|
204
228
|
activate();
|
|
@@ -206,6 +230,7 @@ function AccessibilityOverlay({ view }) {
|
|
|
206
230
|
else if (NAVIGATION_KEYS.has(e.key) && SkiaAccessibilityManager.Key(n.Source, e.key))
|
|
207
231
|
e.preventDefault();
|
|
208
232
|
}, onFocus: (e) => {
|
|
233
|
+
focusedId.current = n.Id;
|
|
209
234
|
pin({ currentTarget: e.currentTarget.parentElement });
|
|
210
235
|
SkiaScrollCtrl.EnsureVisible(n.Source); // inside every enclosing scroll
|
|
211
236
|
if (tabbingOut.current === n.Id)
|
|
@@ -215,6 +240,8 @@ function AccessibilityOverlay({ view }) {
|
|
|
215
240
|
n.Source.NotifyAccessibilityFocused(true);
|
|
216
241
|
setFocusTick((t) => t + 1);
|
|
217
242
|
}, onBlur: (e) => {
|
|
243
|
+
if (e.currentTarget.isConnected && focusedId.current === n.Id)
|
|
244
|
+
focusedId.current = 0; // moved away on purpose
|
|
218
245
|
if (TextInputProxy.IsProxyTarget(e.relatedTarget))
|
|
219
246
|
return; // a text field moved DOM focus to its own input
|
|
220
247
|
n.Source.OnAccessibilityFocused(false);
|
|
@@ -223,7 +250,7 @@ function AccessibilityOverlay({ view }) {
|
|
|
223
250
|
// AccessibilityTextSelectable: one positioned span per drawn line, in the drawn font, so the browser selects and copies it like HTML
|
|
224
251
|
_jsx("div", { role: n.Role, title: n.Hint, "aria-disabled": n.Disabled || undefined, "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)) : (
|
|
225
252
|
// static text is exposed as real (transparent) text content; aria-label only for roles that need a name
|
|
226
|
-
_jsx("div", { role: n.Role, "aria-label": n.Role === "text" ? undefined : n.Label, title: n.Hint, "aria-disabled": n.Disabled || undefined, "aria-live": n.Live, className: "drawnui-a11y-node", style: pos, children: n.Label }, n.Id));
|
|
253
|
+
_jsx("div", { role: n.Role, "aria-label": n.Role === "text" ? undefined : n.Label, title: n.Hint, ...valueAttributes(n), "aria-disabled": n.Disabled || undefined, "aria-live": n.Live, className: "drawnui-a11y-node", style: pos, children: n.Label }, n.Id));
|
|
227
254
|
})] }));
|
|
228
255
|
}
|
|
229
256
|
// One import for apps: React tags + every engine type (Colors, Thickness, Super, gestures, animators...).
|