@blario/react-native 0.1.3 → 0.1.5

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/src/fiber/text.ts CHANGED
@@ -3,6 +3,8 @@ import type { Fiber } from './types.ts';
3
3
 
4
4
  const PRIVATE_USE_GLYPHS = /[-\u{F0000}-\u{10FFFF}]/gu;
5
5
 
6
+ export const TEXT_HOST_TYPE = 'RCTText';
7
+
6
8
  export function normalizeText(value: string): string {
7
9
  return value.replace(PRIVATE_USE_GLYPHS, '').replace(/\s+/g, ' ').trim();
8
10
  }
@@ -124,6 +124,7 @@ export class SnapshotCapturer {
124
124
  rect,
125
125
  scroll: candidate.kind === 'scroll' ? describeScrollDirections(geometries.get(candidate.fiber) ?? null) : null,
126
126
  offscreen,
127
+ wheelOption: candidate.wheelOption,
127
128
  }),
128
129
  );
129
130
  if (mode === 'register') {
@@ -1,10 +1,11 @@
1
1
  import { childrenOf, componentName, firstHostDescendant, isHostComponent, propsOf } from '../fiber/reader.ts';
2
- import { collectText } from '../fiber/text.ts';
2
+ import { collectText, TEXT_HOST_TYPE } from '../fiber/text.ts';
3
3
  import type { Fiber } from '../fiber/types.ts';
4
4
  import { classify } from './classify.ts';
5
5
  import type { ElementKind } from './kinds.ts';
6
6
  import { isFocusedScreen, type ScreenRef, screenOf } from './navigation.ts';
7
7
  import { roleOf } from './properties.ts';
8
+ import { isWheel } from './scroll.ts';
8
9
 
9
10
  export const AGENT_OVERLAY_TEST_ID = 'blar-agent-overlay';
10
11
 
@@ -14,10 +15,11 @@ export interface Candidate {
14
15
  host: Fiber;
15
16
  ancestors: Fiber[];
16
17
  scrollContainers: Fiber[];
18
+ wheelOption?: boolean;
17
19
  }
18
20
 
19
21
  const MODAL_COMPONENT_NAME = 'Modal';
20
- const TEXT_HOST_TYPE = 'RCTText';
22
+ const WHEEL_OPTION_LIMIT = 24;
21
23
  const TEXT_BLOCK_LIMIT = 800;
22
24
  const HEADING_ROLES: readonly string[] = ['header', 'heading'];
23
25
 
@@ -75,6 +77,9 @@ export function collect(root: Fiber): Collected {
75
77
  const block = readTextBlock(fiber, ancestors, scrollContainers);
76
78
  if (block !== null) {
77
79
  textBlocks.push(block);
80
+ if (isWheelOption(block)) {
81
+ candidates.push({ kind: 'button', fiber, host: fiber, ancestors: block.ancestors, scrollContainers: block.scrollContainers, wheelOption: true });
82
+ }
78
83
  }
79
84
  ancestors.push(fiber);
80
85
  if (kind === 'scroll') {
@@ -118,6 +123,14 @@ function readTextBlock(fiber: Fiber, ancestors: Fiber[], scrollContainers: Fiber
118
123
  };
119
124
  }
120
125
 
126
+ function isWheelOption(block: TextBlock): boolean {
127
+ const wheel = block.scrollContainers[block.scrollContainers.length - 1];
128
+ if (wheel === undefined || !isWheel(wheel) || block.text.length > WHEEL_OPTION_LIMIT) {
129
+ return false;
130
+ }
131
+ return !block.ancestors.slice(block.ancestors.lastIndexOf(wheel)).some((ancestor) => classify(ancestor) === 'button');
132
+ }
133
+
121
134
  export function isOpenModal(fiber: Fiber): boolean {
122
135
  return componentName(fiber) === MODAL_COMPONENT_NAME && fiber.child !== null && propsOf(fiber).visible !== false;
123
136
  }
@@ -51,6 +51,7 @@ function renderButton(view: ElementView, twins: number): string {
51
51
  const tag = openTag('button', [
52
52
  ['role', view.role],
53
53
  ['testID', view.testID],
54
+ ['list', view.list],
54
55
  ['icon', view.icon],
55
56
  ['offscreen', view.offscreen],
56
57
  ...commonStateAttributes(view),
@@ -94,6 +95,7 @@ function renderSwitch(view: ElementView, twins: number): string {
94
95
  function renderScroll(view: ElementView): string {
95
96
  const tag = openTag('scroll', [
96
97
  ['testID', view.testID],
98
+ ['list', view.list],
97
99
  ['scroll', view.scroll],
98
100
  ]);
99
101
  return `${tag} />`;
@@ -113,7 +115,7 @@ function renderBody(view: ElementView, twins: number): string {
113
115
  }
114
116
 
115
117
  export function twinSignature(view: ElementView): string {
116
- return [view.kind, view.label, view.icon ?? '', view.role ?? ''].join('|');
118
+ return [view.kind, view.label, view.icon ?? '', view.role ?? '', view.list ?? ''].join('|');
117
119
  }
118
120
 
119
121
  export function countTwins(views: ElementView[]): Map<string, number> {
@@ -43,6 +43,12 @@ export function isHorizontal(fiber: Fiber): boolean {
43
43
  return propsOf(fiber).horizontal === true;
44
44
  }
45
45
 
46
+ // A wheel picker: a snapping list whose rows are plain text, where scrolling a row into the center is what selects it.
47
+ export function isWheel(fiber: Fiber): boolean {
48
+ const { snapToInterval, snapToOffsets } = propsOf(fiber);
49
+ return typeof snapToInterval === 'number' || Array.isArray(snapToOffsets);
50
+ }
51
+
46
52
  export async function measureScrollGeometry(scrollFiber: Fiber): Promise<ScrollGeometry | null> {
47
53
  const scrollHost = findScrollHost(scrollFiber);
48
54
  if (scrollHost === null) {
@@ -73,6 +79,12 @@ export function viewportExtentOf(geometry: ScrollGeometry): number {
73
79
  return geometry.horizontal ? geometry.viewport.width : geometry.viewport.height;
74
80
  }
75
81
 
82
+ export function offsetCentering(rect: Rect, geometry: ScrollGeometry): number {
83
+ const { viewport, horizontal } = geometry;
84
+ const delta = horizontal ? rect.x + rect.width / 2 - (viewport.x + viewport.width / 2) : rect.y + rect.height / 2 - (viewport.y + viewport.height / 2);
85
+ return Math.min(Math.max(scrollOffsetOf(geometry) + delta, 0), maxScrollOffsetOf(geometry));
86
+ }
87
+
76
88
  export function scrollDirectionsOf(geometry: ScrollGeometry): ScrollDirection[] {
77
89
  const offset = scrollOffsetOf(geometry);
78
90
  const canBackward = offset > SCROLL_TOLERANCE;
@@ -2,6 +2,7 @@ import { collectText, precedingText } from '../fiber/text.ts';
2
2
  import { propsOf } from '../fiber/reader.ts';
3
3
  import type { Fiber } from '../fiber/types.ts';
4
4
  import type { Rect } from '../host/rect.ts';
5
+ import { scrollInstanceOf } from './classify.ts';
5
6
  import { iconNameIn } from './icon.ts';
6
7
  import type { Offscreen } from './visibility.ts';
7
8
  import type { ElementKind } from './kinds.ts';
@@ -33,9 +34,24 @@ export interface ElementView {
33
34
  selected: boolean | null;
34
35
  scroll: string | null;
35
36
  offscreen: Offscreen | null;
37
+ list: string | null;
36
38
  rect: Rect;
37
39
  }
38
40
 
41
+ const LIST_NAME_DEPTH = 8;
42
+
43
+ // What a list is called: hosts name the view around it, as AgendaPro does with Hours and Minutes.
44
+ export function listNameOf(list: Fiber, ancestors: Fiber[]): string | null {
45
+ for (const fiber of [list, ...ancestors.slice(-LIST_NAME_DEPTH).reverse()]) {
46
+ const props = propsOf(fiber);
47
+ const name = explicitLabelOf(props) ?? testIdOf(props);
48
+ if (name !== null) {
49
+ return name;
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+
39
55
  function labelFor(kind: ElementKind, fiber: Fiber, ancestors: Fiber[]): string {
40
56
  const explicit = explicitLabelOf(propsOf(fiber));
41
57
  if (explicit !== null) {
@@ -57,6 +73,23 @@ export interface ViewSource {
57
73
  rect: Rect;
58
74
  scroll: string | null;
59
75
  offscreen?: Offscreen | null;
76
+ wheelOption?: boolean;
77
+ }
78
+
79
+ function listOfView({ kind, fiber, ancestors, wheelOption }: ViewSource): string | null {
80
+ if (kind === 'scroll') {
81
+ return listNameOf(fiber, ancestors);
82
+ }
83
+ if (wheelOption !== true) {
84
+ return null;
85
+ }
86
+ for (let index = ancestors.length - 1; index >= 0; index -= 1) {
87
+ const ancestor = ancestors[index];
88
+ if (ancestor !== undefined && scrollInstanceOf(ancestor) !== null) {
89
+ return listNameOf(ancestor, ancestors.slice(0, index));
90
+ }
91
+ }
92
+ return null;
60
93
  }
61
94
 
62
95
  export function buildView(source: ViewSource): ElementView {
@@ -82,6 +115,7 @@ export function buildView(source: ViewSource): ElementView {
82
115
  selected: flags.selected,
83
116
  scroll,
84
117
  offscreen,
118
+ list: listOfView(source),
85
119
  rect,
86
120
  };
87
121
  }
@@ -60,7 +60,8 @@ export function dropWrapperDuplicates(measured: MeasuredCandidate[]): MeasuredCa
60
60
  const OFFSCREEN_LIMIT = 30;
61
61
 
62
62
  export function limitOffscreen(measured: MeasuredCandidate[]): MeasuredCandidate[] {
63
- const hidden = measured.filter((entry) => entry.offscreen !== null && entry.candidate.kind !== 'scroll');
63
+ // Every option of a wheel stays listed: one dropped here could never be selected.
64
+ const hidden = measured.filter((entry) => entry.offscreen !== null && entry.candidate.kind !== 'scroll' && entry.candidate.wheelOption !== true);
64
65
  if (hidden.length <= OFFSCREEN_LIMIT) {
65
66
  return measured;
66
67
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '0.1.3';
1
+ export const SDK_VERSION = '0.1.5';