drawnui-react 0.1.0-preview.15 → 0.1.0-preview.17

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.
Files changed (64) hide show
  1. package/README.md +1 -1
  2. package/SKIPPED.md +9 -8
  3. package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
  4. package/dist/controls/AnimatedFramesRenderer.js +16 -1
  5. package/dist/controls/DrawnGame.d.ts +4 -1
  6. package/dist/controls/DrawnGame.js +7 -3
  7. package/dist/controls/GridStructure.d.ts +13 -1
  8. package/dist/controls/GridStructure.js +38 -2
  9. package/dist/controls/SkiaBackdrop.d.ts +2 -0
  10. package/dist/controls/SkiaBackdrop.js +2 -0
  11. package/dist/controls/SkiaButton.d.ts +2 -0
  12. package/dist/controls/SkiaButton.js +2 -0
  13. package/dist/controls/SkiaCarousel.d.ts +3 -1
  14. package/dist/controls/SkiaCarousel.js +5 -2
  15. package/dist/controls/SkiaDrawer.d.ts +2 -0
  16. package/dist/controls/SkiaDrawer.js +2 -0
  17. package/dist/controls/SkiaEditor.d.ts +5 -1
  18. package/dist/controls/SkiaEditor.js +29 -18
  19. package/dist/controls/SkiaGif.d.ts +3 -0
  20. package/dist/controls/SkiaGif.js +11 -1
  21. package/dist/controls/SkiaImage.js +15 -10
  22. package/dist/controls/SkiaLabel.d.ts +54 -3
  23. package/dist/controls/SkiaLabel.js +235 -52
  24. package/dist/controls/SkiaLayout.d.ts +20 -0
  25. package/dist/controls/SkiaLayout.js +78 -6
  26. package/dist/controls/SkiaLottie.js +3 -1
  27. package/dist/controls/SkiaProgress.d.ts +2 -1
  28. package/dist/controls/SkiaProgress.js +6 -3
  29. package/dist/controls/SkiaScroll.d.ts +16 -0
  30. package/dist/controls/SkiaScroll.js +85 -8
  31. package/dist/controls/SkiaShape.d.ts +13 -2
  32. package/dist/controls/SkiaShape.js +50 -10
  33. package/dist/controls/SkiaSlider.d.ts +7 -1
  34. package/dist/controls/SkiaSlider.js +12 -4
  35. package/dist/controls/SkiaSprite.d.ts +1 -0
  36. package/dist/controls/SkiaSprite.js +4 -0
  37. package/dist/controls/SkiaToggle.d.ts +2 -0
  38. package/dist/controls/SkiaToggle.js +3 -1
  39. package/dist/controls/SnappingLayout.d.ts +7 -0
  40. package/dist/controls/SnappingLayout.js +11 -0
  41. package/dist/core/Accessibility.d.ts +21 -0
  42. package/dist/core/Accessibility.js +66 -9
  43. package/dist/core/Animators.d.ts +28 -1
  44. package/dist/core/Animators.js +76 -8
  45. package/dist/core/Canvas.d.ts +26 -4
  46. package/dist/core/Canvas.js +136 -23
  47. package/dist/core/Easing.d.ts +12 -1
  48. package/dist/core/Easing.js +25 -1
  49. package/dist/core/Gestures.d.ts +24 -1
  50. package/dist/core/Gestures.js +75 -2
  51. package/dist/core/Hover.d.ts +47 -0
  52. package/dist/core/Hover.js +134 -0
  53. package/dist/core/KeyboardManager.d.ts +13 -0
  54. package/dist/core/KeyboardManager.js +18 -0
  55. package/dist/core/ScrollAnimators.js +9 -0
  56. package/dist/core/SkiaControl.d.ts +97 -1
  57. package/dist/core/SkiaControl.js +271 -38
  58. package/dist/index.d.ts +1 -0
  59. package/dist/index.js +1 -0
  60. package/dist/react/SkiaShell.js +33 -14
  61. package/dist/react/index.d.ts +1 -1
  62. package/dist/react/index.js +30 -3
  63. package/dist/react/reconciler.js +29 -10
  64. package/package.json +18 -1
@@ -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,15 +240,17 @@ 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);
221
248
  n.Source.NotifyAccessibilityFocused(false);
222
249
  }, children: n.Label }, n.Id)) : n.TextLines ? (
223
250
  // AccessibilityTextSelectable: one positioned span per drawn line, in the drawn font, so the browser selects and copies it like HTML
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)) : (
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-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...).
@@ -1,6 +1,7 @@
1
1
  import { createContext } from "react";
2
2
  import Reconciler from "react-reconciler";
3
3
  import { ConcurrentRoot, DefaultEventPriority } from "react-reconciler/constants";
4
+ import { SkiaControl } from "../core/SkiaControl";
4
5
  import { SkiaLabel } from "../controls/SkiaLabel";
5
6
  import { TextSpan } from "../controls/TextSpan";
6
7
  import { SkiaRichLabel } from "../controls/SkiaRichLabel";
@@ -38,12 +39,14 @@ export const Registry = {
38
39
  const SKIP = new Set(["children", "key", "ref"]);
39
40
  /** Props that change how the subtree is composited, not what it contains: repaint, keep caches (DrawnUi RedrawCanvas). */
40
41
  const REPAINT_ONLY = new Set(["TranslationX", "TranslationY", "Rotation", "ScaleX", "ScaleY", "Scale", "SkewX", "SkewY", "AnchorX", "AnchorY", "Opacity"]);
42
+ /** Props that change only how a control looks, never its size (C# NeedDraw): an ImageComposite above redraws only its area. */
43
+ const isDrawOnly = (k) => k.endsWith("Color") || k === "FillGradient" || k === "StrokeGradient";
41
44
  /**
42
45
  * Assigns changed props straight onto the control (same names as the C# properties).
43
46
  * Handler props (functions) are swapped without invalidating: inline arrows change identity every render.
44
47
  */
45
48
  function applyProps(inst, prev, next) {
46
- let changed = false, repaint = false;
49
+ let changed = false, repaint = false, layout = false;
47
50
  for (const k in next) {
48
51
  if (SKIP.has(k) || (prev && prev[k] === next[k]))
49
52
  continue;
@@ -52,23 +55,36 @@ function applyProps(inst, prev, next) {
52
55
  continue;
53
56
  if (REPAINT_ONLY.has(k))
54
57
  repaint = true;
55
- else
58
+ else {
56
59
  changed = true;
60
+ if (!isDrawOnly(k))
61
+ layout = true;
62
+ }
57
63
  }
58
64
  if (prev)
59
65
  for (const k in prev)
60
66
  if (!SKIP.has(k) && !(k in next)) {
61
67
  inst[k] = new inst.constructor()[k];
62
- changed = true;
68
+ changed = layout = true;
63
69
  }
64
- if (changed && prev)
65
- inst.Update();
70
+ if (changed && prev) {
71
+ if (layout || !(inst instanceof SkiaControl))
72
+ inst.Update();
73
+ else
74
+ inst.UpdateDraw();
75
+ }
66
76
  else if (repaint && prev)
67
77
  inst.RepaintComposition?.();
68
78
  }
69
79
  let currentUpdatePriority = DefaultEventPriority;
70
80
  const noop = () => { };
71
81
  const hostContext = {};
82
+ function appendChild(parent, child) {
83
+ if (parent instanceof SkiaLayout)
84
+ return parent.InsertChild(child);
85
+ child.IsChildrenItem = true;
86
+ parent.AddSubView(child);
87
+ }
72
88
  // Record<string, unknown> absorbs members that react-reconciler 0.33 reads but @types 0.32 does not declare yet.
73
89
  const hostConfig = {
74
90
  supportsMutation: true,
@@ -94,12 +110,15 @@ const hostConfig = {
94
110
  createTextInstance(text) {
95
111
  throw new Error(`DrawnUi: raw text "${text}" is not allowed, use <SkiaLabel Text="..." />`);
96
112
  },
97
- appendInitialChild: (parent, child) => parent.AddSubView(child),
98
- appendChild: (parent, child) => parent.AddSubView(child),
113
+ // JSX children are the app's children (C# IsChildrenItem): a layout keeps them in JSX order inside the slots they
114
+ // occupy, so its own parts keep their place; an existing child passed again is a move
115
+ appendInitialChild: (parent, child) => appendChild(parent, child),
116
+ appendChild: (parent, child) => appendChild(parent, child),
99
117
  insertBefore: (parent, child, before) => {
100
- const index = parent instanceof SkiaLayout ? parent.Views.indexOf(before)
101
- : parent instanceof SkiaLabel ? parent.Spans.indexOf(before) : 0;
102
- parent.InsertSubView(index, child);
118
+ if (parent instanceof SkiaLayout)
119
+ return parent.InsertChild(child, before);
120
+ child.IsChildrenItem = true;
121
+ parent.InsertSubView(parent instanceof SkiaLabel ? parent.Spans.indexOf(before) : 0, child);
103
122
  },
104
123
  removeChild: (parent, child) => parent.RemoveSubView(child),
105
124
  appendChildToContainer: (canvas, child) => { canvas.Content = child; },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drawnui-react",
3
- "version": "0.1.0-preview.15",
3
+ "version": "0.1.0-preview.17",
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)",
@@ -56,6 +56,23 @@
56
56
  "check:recycling": "node dev/check.mjs dev/check-recycling.ts",
57
57
  "check:keyboard": "node dev/check.mjs dev/check-keyboard.ts",
58
58
  "check:wheel": "node dev/check.mjs dev/check-wheel.ts",
59
+ "check:children": "node dev/check.mjs dev/check-children.tsx",
60
+ "check:composite": "node dev/check.mjs dev/check-composite.tsx",
61
+ "check:hover": "node dev/check.mjs dev/check-hover.ts",
62
+ "check:grid-label": "node dev/check.mjs dev/check-grid-label.ts",
63
+ "check:snap": "node dev/check.mjs dev/check-snap.ts",
64
+ "check:game": "node dev/check.mjs dev/check-game.ts",
65
+ "check:animation": "node dev/check.mjs dev/check-animation.ts",
66
+ "check:shape": "node dev/check.mjs dev/check-shape.ts",
67
+ "check:label": "node dev/check.mjs dev/check-label.ts",
68
+ "check:image": "node dev/check.mjs dev/check-image.ts",
69
+ "check:longpress": "node dev/check.mjs dev/check-longpress.ts",
70
+ "check:hidden": "node dev/check.mjs dev/check-hidden.ts",
71
+ "check:shell": "node dev/check.mjs dev/check-shell.tsx",
72
+ "check:wheel-routing": "node dev/check.mjs dev/check-wheel-routing.ts",
73
+ "check:velocity": "node dev/check.mjs dev/check-velocity.ts",
74
+ "check:scroll-index": "node dev/check.mjs dev/check-scroll-index.ts",
75
+ "check:culling": "node dev/check.mjs dev/check-culling.ts",
59
76
  "site:extras": "node dev/site-extras.mjs dist/demo",
60
77
  "build:lib": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
61
78
  "prepublishOnly": "npm run build:lib",