@octane-xplat/pager 0.14.0 → 0.15.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octane-xplat/pager",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Paged swipe container for Octane xplat — web scroll-snap row + @nativescript-community/ui-pager (ViewPager2 / paging UICollectionView)",
5
5
  "keywords": [
6
6
  "android",
@@ -24,9 +24,10 @@
24
24
  "directory": "packages/pager"
25
25
  },
26
26
  "files": [
27
- "dist",
28
27
  "types",
29
- "src"
28
+ "src",
29
+ "!src/**/*.test.*",
30
+ "!src/**/*.spec.*"
30
31
  ],
31
32
  "type": "module",
32
33
  "exports": {
@@ -36,9 +37,9 @@
36
37
  "default": "./src/index.macos.ts"
37
38
  },
38
39
  "types": "./types/index.d.ts",
39
- "web": "./dist/web/index.js",
40
- "native": "./dist/native/index.js",
41
- "default": "./dist/native/index.js"
40
+ "web": "./src/index.web.ts",
41
+ "native": "./src/index.ts",
42
+ "default": "./src/index.ts"
42
43
  }
43
44
  },
44
45
  "publishConfig": {
@@ -48,17 +49,20 @@
48
49
  "@nativescript-community/ui-pager": "14.1.42"
49
50
  },
50
51
  "devDependencies": {
51
- "@nativescript-community/octane": "0.2.4",
52
+ "@nativescript-community/octane": "0.2.5",
52
53
  "@nativescript/core": "9.1.3",
54
+ "@octane-xplat/macos-renderer": "0.15.0",
55
+ "@octane-xplat/ui": "0.15.0",
53
56
  "@octanejs/vite-plugin": "0.1.61",
54
57
  "octane": "0.11.0",
55
- "tsrx-typegen": "0.2.2",
58
+ "tsrx-typegen": "0.3.0",
56
59
  "typescript": "5.9.3",
57
60
  "vite": "8.3.0"
58
61
  },
59
62
  "peerDependencies": {
60
63
  "@nativescript-community/octane": ">=0.2.1 <1",
61
64
  "@nativescript/core": ">=9.1.0 <10",
65
+ "@octane-xplat/ui": ">=0.6.0 <1",
62
66
  "octane": ">=0.6.3 <1"
63
67
  },
64
68
  "peerDependenciesMeta": {
@@ -70,7 +74,7 @@
70
74
  }
71
75
  },
72
76
  "scripts": {
73
- "build": "vite build && vite build --mode native && pnpm typegen && node ../../scripts/check-native-dist.mjs",
77
+ "test:native": "vitest run --config vitest.native.config.mts",
74
78
  "typegen": "tsc -p tsconfig.types.json",
75
79
  "test:packed": "node tests/packed-consumer.mjs",
76
80
  "pack:check": "tsrx-typegen --pack-check"
@@ -0,0 +1,19 @@
1
+ import type { ParityCheck } from '@octane-xplat/ui/parity/check-helpers'
2
+ import { dims } from '@octane-xplat/ui/parity/check-helpers'
3
+
4
+ export const CHECKS: ParityCheck[] = [
5
+
6
+ {
7
+ fixture: 'pager-two-pages',
8
+ targets: ['web', 'ios', 'android'],
9
+ elements: { pager: 'parity-pager-root', page: 'parity-pager-page' },
10
+ equal: [
11
+ 'pager.box.w',
12
+ 'pager.box.h',
13
+ 'pager.style.backgroundColor',
14
+ 'page.box.w',
15
+ 'page.box.h',
16
+ ],
17
+ check: (m) => [dims(m('pager'), 220, 96), dims(m('page'), 220, 96)],
18
+ }
19
+ ]
@@ -0,0 +1,19 @@
1
+ // Measured parity fixtures — assertions live in Pager.parity.check.ts.
2
+ import { Pager } from './Pager'
3
+ import { View } from '@octane-xplat/ui'
4
+ import type { ParityFixture } from '@octane-xplat/ui/parity'
5
+
6
+ function PagerFixture() @{
7
+ <Pager
8
+ className="parity-pager-root"
9
+ style={{ width: 220, height: 96, backgroundColor: '#f5f5f5' }}
10
+ items={[{ key: 'first' }, { key: 'second' }]}
11
+ renderItem={() => (
12
+ <View className="parity-pager-page" style={{ width: 220, height: 96 }} />
13
+ )}
14
+ />
15
+ }
16
+
17
+ export const PARITY: ParityFixture[] = [
18
+ { name: 'pager-two-pages', component: 'Pager', renderer: PagerFixture, props: {} },
19
+ ]
package/src/Pager.tsrx CHANGED
@@ -1,18 +1,14 @@
1
1
  /** @jsxImportSource @nativescript-community/octane */
2
- import { memo, useCallback, useEffect, useRef } from 'octane';
2
+ import { memo, useCallback, useEffect, useRef, useState } from 'octane';
3
3
  import {
4
- NATIVESCRIPT_RENDERER_ID,
5
- createNativeScriptRoot,
6
- defineUniversalComponent,
7
4
  registerElement,
8
5
  } from '@nativescript-community/octane';
9
6
 
10
7
  import type { Attributes } from '@nativescript-community/octane/intrinsics';
11
- import type { UniversalRoot } from '@nativescript-community/octane';
8
+ import { createPortal as createUniversalPortal, type UniversalRenderable } from 'octane/universal/native';
12
9
  import { Pager as NSPager } from '@nativescript-community/ui-pager';
13
10
  import { GridLayout } from '@nativescript/core';
14
11
  import type { PagerProps } from './props';
15
- import { cx } from './cx.mobile';
16
12
  import { applyEscapeProps, nativeAccessibilityRole, nativeAccessibilityState } from './escape-props.mobile';
17
13
  import { layoutChildProps } from './layout-child';
18
14
 
@@ -28,12 +24,14 @@ declare module '@nativescript-community/octane/intrinsics' {
28
24
  // the /ns/m dev bridge to resolve it under the app root.
29
25
  registerElement('pager', NSPager);
30
26
 
31
- /** One recycled page: the host view the plugin's adapter asks for plus the
32
- * Octane root mounted into it. Mirrors the driver's ListView cell seam —
33
- * ui-pager fires the same `itemLoading`/`itemDisposing` pair. */
27
+ function createPortal(children: unknown, target: unknown) {
28
+ return createUniversalPortal(children as UniversalRenderable, target);
29
+ }
30
+
31
+ /** One recycled page: the adapter-owned host plus the current render data.
32
+ * Its content is a portal from Pager's declaring tree. */
34
33
  interface PagerCell {
35
34
  host: any;
36
- root: UniversalRoot;
37
35
  pager: any;
38
36
  renderItem: any;
39
37
  item: any;
@@ -43,17 +41,10 @@ interface PagerCell {
43
41
  const cellsByHost = new WeakMap<object, PagerCell>();
44
42
  const cellsByPager = new WeakMap<object, Set<PagerCell>>();
45
43
 
46
- /** Every page root renders this one component, so rebinds are diffs of the
47
- * cell's tree rather than remounts. The renderItem output wraps in a
48
- * gridlayout — page content must stay single-root. */
49
- const PagerCell = defineUniversalComponent(NATIVESCRIPT_RENDERER_ID, (props: any) =>
50
- props.renderItem(props.item, props.index),
51
- );
52
-
53
44
  function cellFor(host: any): PagerCell {
54
45
  let cell = cellsByHost.get(host);
55
46
  if (!cell) {
56
- cell = { host, root: createNativeScriptRoot(host), pager: null, renderItem: null, item: undefined, index: -1 };
47
+ cell = { host, pager: null, renderItem: null, item: undefined, index: -1 };
57
48
  cellsByHost.set(host, cell);
58
49
  }
59
50
 
@@ -80,7 +71,6 @@ function releasePagerCells(pager: any) {
80
71
  if (!cells) return;
81
72
  for (const cell of cells) {
82
73
  cellsByHost.delete(cell.host);
83
- cell.root.unmount();
84
74
  }
85
75
 
86
76
  cellsByPager.delete(pager);
@@ -88,10 +78,12 @@ function releasePagerCells(pager: any) {
88
78
 
89
79
  function PagerInner(props: PagerProps) @{
90
80
  const host = useRef<any>(null);
91
- // selectedIndex writes the leaf makes itself — the plugin raises
81
+ const [portalCells, setPortalCells] = useState<PagerCell[]>([]);
82
+ // Track selectedIndex writes across synchronous and delayed echoes.
83
+ // The plugin raises
92
84
  // 'selectedIndexChange' for script writes exactly as for swipes, and
93
85
  // the driver's auto-mute only covers JSX prop writes, not ours.
94
- const suppressEcho = useRef(false);
86
+ const current = useRef(props.page ?? props.defaultPage ?? 0);
95
87
  const pageSeeded = useRef(false);
96
88
 
97
89
  // Stable ref identity — an inline closure re-fires ref(null)/ref(v) per
@@ -118,10 +110,9 @@ function PagerInner(props: PagerProps) @{
118
110
  cell.renderItem = props.renderItem;
119
111
  cell.item = item;
120
112
  cell.index = args.index;
121
- cell.root.render(PagerCell, {
122
- renderItem: (it: any, i: number) => <gridlayout>{props.renderItem(it, i)}</gridlayout>,
123
- item,
124
- index: args.index,
113
+ setPortalCells((current) => {
114
+ const existing = current.some((entry) => entry === cell);
115
+ return existing ? [...current] : [...current, cell];
125
116
  });
126
117
  };
127
118
 
@@ -130,7 +121,7 @@ function PagerInner(props: PagerProps) @{
130
121
  if (cell) {
131
122
  cellsByHost.delete(args.view);
132
123
  if (cell.pager) cellsByPager.get(cell.pager)?.delete(cell);
133
- cell.root.unmount();
124
+ setPortalCells((current) => current.filter((entry) => entry !== cell));
134
125
  }
135
126
  };
136
127
 
@@ -156,27 +147,28 @@ function PagerInner(props: PagerProps) @{
156
147
  if (pager.items !== props.items) pager.items = props.items;
157
148
  const target = props.page ?? (pageSeeded.current ? undefined : props.defaultPage);
158
149
  if (target !== undefined && pager.selectedIndex !== target) {
159
- suppressEcho.current = true;
160
- try {
161
- pager.selectedIndex = target;
162
- } finally {
163
- suppressEcho.current = false;
164
- }
150
+ // Track the coerced target before writing: Android may echo later.
151
+ current.current = Math.max(0, Math.min(props.items.length - 1, target));
152
+ pager.selectedIndex = target;
165
153
  }
166
154
 
167
155
  pageSeeded.current = true;
168
156
  });
169
157
 
158
+ <>
170
159
  <pager
171
160
  ref={bindHost}
172
161
  id={props.id}
173
162
  {...layoutChildProps(props)}
174
- className={cx('vx-pager', props.className)}
163
+ className={['vx-pager', props.className]}
175
164
  style={props.style}
176
165
  onItemLoading={bindCell}
177
166
  onItemDisposing={disposeCell}
178
167
  onSelectedIndexChange={(args: any) => {
179
- if (!suppressEcho.current) props.onPageChange?.(args.value);
168
+ if (args.value !== current.current) {
169
+ current.current = args.value;
170
+ props.onPageChange?.(args.value);
171
+ }
180
172
  }}
181
173
  accessibilityRole={nativeAccessibilityRole(props.accessibilityRole) as any}
182
174
  accessibilityLabel={props.accessibilityLabel}
@@ -186,6 +178,13 @@ function PagerInner(props: PagerProps) @{
186
178
  accessible={props.accessible}
187
179
  accessibilityLiveRegion={props.accessibilityLiveRegion as any}
188
180
  />
181
+ {portalCells.map((cell) =>
182
+ createPortal(
183
+ <gridlayout>{cell.renderItem?.(cell.item, cell.index)}</gridlayout>,
184
+ cell.host,
185
+ ),
186
+ )}
187
+ </>
189
188
  }
190
189
 
191
190
  // The pager subtree is memoized on `items` and configuration: the universal
@@ -202,17 +201,42 @@ const MemoPager = memo(PagerInner, (a, b) =>
202
201
  a.page === b.page &&
203
202
  a.onPageChange === b.onPageChange &&
204
203
  a.ios === b.ios &&
205
- a.android === b.android
204
+ a.android === b.android &&
205
+ a.row === b.row &&
206
+ a.col === b.col &&
207
+ a.rowSpan === b.rowSpan &&
208
+ a.colSpan === b.colSpan &&
209
+ a.dock === b.dock &&
210
+ a.left === b.left &&
211
+ a.top === b.top &&
212
+ a.flexGrow === b.flexGrow &&
213
+ a.flexShrink === b.flexShrink &&
214
+ a.alignSelf === b.alignSelf &&
215
+ a.order === b.order &&
216
+ a.accessible === b.accessible &&
217
+ a.accessibilityRole === b.accessibilityRole &&
218
+ a.accessibilityLabel === b.accessibilityLabel &&
219
+ a.accessibilityHint === b.accessibilityHint &&
220
+ a.accessibilityValue === b.accessibilityValue &&
221
+ a.accessibilityState === b.accessibilityState &&
222
+ a.accessibilityLiveRegion === b.accessibilityLiveRegion
206
223
  );
207
224
 
208
225
  /** Pager — paged horizontal swiping via @nativescript-community/ui-pager
209
226
  * (ViewPager2 / paging UICollectionView), chrome-reset: one full-size cell
210
227
  * per item, no indicator. Pages recycle through `itemTemplates` +
211
- * `itemLoading`, each slot hosting its own Octane root — the same cell
212
- * machinery the driver builds for `listview`, hand-wired here. */
228
+ * `itemLoading`; active cell hosts are portals from the Pager's declaring
229
+ * tree, so provider context follows each cell. */
213
230
  export function Pager(props: PagerProps) @{
214
231
  @if (props.items.length > 0) {
215
232
  <MemoPager
233
+ accessible={props.accessible}
234
+ accessibilityRole={props.accessibilityRole}
235
+ accessibilityLabel={props.accessibilityLabel}
236
+ accessibilityHint={props.accessibilityHint}
237
+ accessibilityValue={props.accessibilityValue}
238
+ accessibilityState={props.accessibilityState}
239
+ accessibilityLiveRegion={props.accessibilityLiveRegion}
216
240
  id={props.id}
217
241
  className={props.className}
218
242
  style={props.style}
@@ -17,6 +17,7 @@ export function Pager(props: PagerProps) @{
17
17
  // updated by controlled writes and settled swipes. A scroll that lands
18
18
  // on `current` is programmatic — the settle never reports it.
19
19
  const current = useRef(props.page ?? props.defaultPage ?? 0);
20
+ const hasItems = props.items.length > 0;
20
21
  const settle = useRef<ReturnType<typeof setTimeout> | null>(null);
21
22
 
22
23
  useEffect(() => {
@@ -45,17 +46,19 @@ export function Pager(props: PagerProps) @{
45
46
  return () => {
46
47
  el.removeEventListener('scroll', onScroll);
47
48
  if (settle.current !== null) clearTimeout(settle.current);
49
+ settle.current = null;
48
50
  };
49
- }, []);
51
+ }, [hasItems]);
50
52
 
51
- // Mount: jump (no animation) to the starting page — an initial position
53
+ // Each new host: jump (no animation) to the starting page — an initial position
52
54
  // is not a scroll. Runs after the scroll listener attaches so the write
53
55
  // still flows through the tracked-index path.
54
56
  useEffect(() => {
55
57
  const el = host.current;
56
- if (!el || current.current <= 0) return;
58
+ if (!el) return;
59
+ current.current = props.page ?? current.current;
57
60
  el.scrollLeft = current.current * el.clientWidth;
58
- }, []);
61
+ }, [hasItems]);
59
62
 
60
63
  // Controlled writes scroll smoothly. Skipped when the page already
61
64
  // matches `current` — a user swipe that the parent echoes back unchanged
@@ -1,282 +0,0 @@
1
- import { cx } from "./cx.mobile.js";
2
- import { applyEscapeProps, nativeAccessibilityRole, nativeAccessibilityState } from "./escape-props.mobile.js";
3
- import { layoutChildProps } from "./layout-child.js";
4
- import { __methodDep } from "octane/universal/native";
5
- import { NATIVESCRIPT_RENDERER_ID, createNativeScriptRoot, defineUniversalComponent, hookSlots, memo, registerElement, universalComponent, universalIf, universalPlan, universalProps, universalValue, useCallback, useEffect, useRef } from "@nativescript-community/octane";
6
- import { Pager } from "@nativescript-community/ui-pager";
7
- import { GridLayout } from "@nativescript/core";
8
- //#region src/Pager.tsrx
9
- var _hs$ = /* @__PURE__ */ hookSlots(6);
10
- var _h$0 = /* @__PURE__ */ Symbol(_hs$);
11
- var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
12
- var _h$2 = /* @__PURE__ */ Symbol(_hs$ + 2);
13
- var _h$3 = /* @__PURE__ */ Symbol(_hs$ + 3);
14
- var _h$4 = /* @__PURE__ */ Symbol(_hs$ + 4);
15
- var _h$5 = /* @__PURE__ */ Symbol(_hs$ + 5);
16
- var __octaneUniversalPlan0 = universalPlan("nativescript", {
17
- "kind": "host",
18
- "type": "gridlayout",
19
- "children": [{
20
- "kind": "slot",
21
- "slot": 0
22
- }]
23
- });
24
- var __octaneUniversalPlan1 = universalPlan("nativescript", {
25
- "kind": "host",
26
- "type": "pager",
27
- "propsSlot": 0
28
- });
29
- var __octaneUniversalPlan2 = universalPlan("nativescript", {
30
- "kind": "slot",
31
- "slot": 0
32
- });
33
- var __octaneUniversalPlan3 = universalPlan("nativescript", {
34
- "kind": "range",
35
- "children": [{
36
- "kind": "slot",
37
- "slot": 0
38
- }]
39
- });
40
- var __octaneUniversalPlan4 = universalPlan("nativescript", {
41
- "kind": "slot",
42
- "slot": 0
43
- });
44
- registerElement("pager", Pager);
45
- var cellsByHost = /* @__PURE__ */ new WeakMap();
46
- var cellsByPager = /* @__PURE__ */ new WeakMap();
47
- var PagerCell = defineUniversalComponent(NATIVESCRIPT_RENDERER_ID, (props) => props.renderItem(props.item, props.index));
48
- function cellFor(host) {
49
- let cell = cellsByHost.get(host);
50
- if (!cell) {
51
- cell = {
52
- host,
53
- root: createNativeScriptRoot(host),
54
- pager: null,
55
- renderItem: null,
56
- item: void 0,
57
- index: -1
58
- };
59
- cellsByHost.set(host, cell);
60
- }
61
- return cell;
62
- }
63
- function createPageHost() {
64
- const host = new GridLayout();
65
- cellFor(host);
66
- return host;
67
- }
68
- var PAGER_TEMPLATES = [{
69
- key: "xplat-page",
70
- createView: createPageHost
71
- }];
72
- var selectPageTemplate = () => "xplat-page";
73
- function releasePagerCells(pager) {
74
- const cells = cellsByPager.get(pager);
75
- if (!cells) return;
76
- for (const cell of cells) {
77
- cellsByHost.delete(cell.host);
78
- cell.root.unmount();
79
- }
80
- cellsByPager.delete(pager);
81
- }
82
- var PagerInner = defineUniversalComponent("nativescript", function PagerInner(props) {
83
- const host = useRef(null, _h$0);
84
- const suppressEcho = useRef(false, _h$1);
85
- const pageSeeded = useRef(false, _h$2);
86
- const bindHost = useCallback((v) => {
87
- host.current = v;
88
- }, [], _h$3);
89
- const bindCell = (args) => {
90
- const cell = cellFor(args.view);
91
- cell.pager = args.object;
92
- let owned = cellsByPager.get(cell.pager);
93
- if (!owned) {
94
- owned = /* @__PURE__ */ new Set();
95
- cellsByPager.set(cell.pager, owned);
96
- }
97
- owned.add(cell);
98
- const item = args.bindingContext ?? props.items?.[args.index];
99
- if (cell.renderItem === props.renderItem && cell.index === args.index && Object.is(cell.item, item)) return;
100
- cell.renderItem = props.renderItem;
101
- cell.item = item;
102
- cell.index = args.index;
103
- cell.root.render(PagerCell, {
104
- renderItem: (it, i) => universalValue(__octaneUniversalPlan0, [props.renderItem(it, i)]),
105
- item,
106
- index: args.index
107
- });
108
- };
109
- const disposeCell = (args) => {
110
- const cell = args.view && cellsByHost.get(args.view);
111
- if (cell) {
112
- cellsByHost.delete(args.view);
113
- if (cell.pager) cellsByPager.get(cell.pager)?.delete(cell);
114
- cell.root.unmount();
115
- }
116
- };
117
- useEffect(() => {
118
- const pager = host.current;
119
- if (!pager) return;
120
- pager.itemTemplates = PAGER_TEMPLATES;
121
- pager.itemTemplateSelector = selectPageTemplate;
122
- return () => releasePagerCells(pager);
123
- }, [], _h$4);
124
- useEffect(() => {
125
- const pager = host.current;
126
- if (!pager) return;
127
- applyEscapeProps(pager, props);
128
- if (pager.items !== props.items) pager.items = props.items;
129
- const target = props.page ?? (pageSeeded.current ? void 0 : props.defaultPage);
130
- if (target !== void 0 && pager.selectedIndex !== target) {
131
- suppressEcho.current = true;
132
- try {
133
- pager.selectedIndex = target;
134
- } finally {
135
- suppressEcho.current = false;
136
- }
137
- }
138
- pageSeeded.current = true;
139
- }, [
140
- props,
141
- __methodDep(props, "items", true, true),
142
- __methodDep(props, "page", true, true),
143
- __methodDep(props, "defaultPage", true, true)
144
- ], _h$5);
145
- return universalValue(__octaneUniversalPlan1, [universalProps([
146
- [
147
- "set",
148
- "ref",
149
- bindHost
150
- ],
151
- [
152
- "set",
153
- "id",
154
- props.id
155
- ],
156
- ["spread", layoutChildProps(props)],
157
- [
158
- "set",
159
- "className",
160
- cx("vx-pager", props.className)
161
- ],
162
- [
163
- "set",
164
- "style",
165
- props.style
166
- ],
167
- [
168
- "set",
169
- "onItemLoading",
170
- bindCell
171
- ],
172
- [
173
- "set",
174
- "onItemDisposing",
175
- disposeCell
176
- ],
177
- [
178
- "set",
179
- "onSelectedIndexChange",
180
- (args) => {
181
- if (!suppressEcho.current) props.onPageChange?.(args.value);
182
- }
183
- ],
184
- [
185
- "set",
186
- "accessibilityRole",
187
- nativeAccessibilityRole(props.accessibilityRole)
188
- ],
189
- [
190
- "set",
191
- "accessibilityLabel",
192
- props.accessibilityLabel
193
- ],
194
- [
195
- "set",
196
- "accessibilityHint",
197
- props.accessibilityHint
198
- ],
199
- [
200
- "set",
201
- "accessibilityValue",
202
- props.accessibilityValue
203
- ],
204
- [
205
- "set",
206
- "accessibilityState",
207
- nativeAccessibilityState(props.accessibilityState)
208
- ],
209
- [
210
- "set",
211
- "accessible",
212
- props.accessible
213
- ],
214
- [
215
- "set",
216
- "accessibilityLiveRegion",
217
- props.accessibilityLiveRegion
218
- ]
219
- ])]);
220
- }, { "module": "@nativescript-community/octane" });
221
- var MemoPager = memo(PagerInner, (a, b) => a.items === b.items && a.className === b.className && a.style === b.style && a.id === b.id && a.page === b.page && a.onPageChange === b.onPageChange && a.ios === b.ios && a.android === b.android);
222
- var Pager$1 = defineUniversalComponent("nativescript", function Pager(props) {
223
- return universalValue(__octaneUniversalPlan4, [universalIf(props.items.length > 0, () => {
224
- return universalValue(__octaneUniversalPlan2, [universalComponent("nativescript", MemoPager, universalProps([
225
- [
226
- "set",
227
- "id",
228
- props.id
229
- ],
230
- [
231
- "set",
232
- "className",
233
- props.className
234
- ],
235
- [
236
- "set",
237
- "style",
238
- props.style
239
- ],
240
- [
241
- "set",
242
- "items",
243
- props.items
244
- ],
245
- [
246
- "set",
247
- "renderItem",
248
- props.renderItem
249
- ],
250
- [
251
- "set",
252
- "page",
253
- props.page
254
- ],
255
- [
256
- "set",
257
- "defaultPage",
258
- props.defaultPage
259
- ],
260
- [
261
- "set",
262
- "onPageChange",
263
- props.onPageChange
264
- ],
265
- [
266
- "set",
267
- "ios",
268
- props.ios
269
- ],
270
- [
271
- "set",
272
- "android",
273
- props.android
274
- ],
275
- ["spread", layoutChildProps(props)]
276
- ]))]);
277
- }, () => {
278
- return universalValue(__octaneUniversalPlan3, [props.renderEmpty?.()]);
279
- })]);
280
- }, { "module": "@nativescript-community/octane" });
281
- //#endregion
282
- export { Pager$1 as Pager };
@@ -1,12 +0,0 @@
1
- //#region src/cx.mobile.ts
2
- /** Native renderers now normalize the declarative `className` prop, but
3
- * imperative `view.className` writes still need a pre-joined string — NS
4
- * splits on ' ' only. Native leaves keep normalizing through cx() before
5
- * handing className to an intrinsic so both paths accept the same values.
6
- * (The DOM renderer already flattens arrays, so web leaves don't need
7
- * this.) */
8
- function cx(...parts) {
9
- return parts.flat(Infinity).filter(Boolean).join(" ");
10
- }
11
- //#endregion
12
- export { cx };
@@ -1,39 +0,0 @@
1
- import { isAndroid, isIOS } from "@nativescript/core";
2
- //#region src/escape-props.mobile.ts
3
- /** Apply only the active device's escape bag after the primitive's own props. */
4
- function applyEscapeProps(view, props) {
5
- if (!view) return;
6
- const bag = isIOS ? props.ios : isAndroid ? props.android : void 0;
7
- if (bag) Object.assign(view, bag);
8
- }
9
- var NATIVE_ACCESSIBILITY_ROLES = {
10
- button: "button",
11
- link: "link",
12
- search: "search",
13
- image: "image",
14
- heading: "header",
15
- adjustable: "adjustable",
16
- summary: "summary",
17
- text: "text",
18
- none: "none",
19
- progressbar: "progressBar",
20
- checkbox: "checkbox",
21
- switch: "switch",
22
- radio: "radioButton",
23
- spinbutton: "spinButton",
24
- tab: "button"
25
- };
26
- /** Translate the shared/ARIA spelling to NativeScript's narrower role enum. */
27
- function nativeAccessibilityRole(role) {
28
- return role ? NATIVE_ACCESSIBILITY_ROLES[role] : void 0;
29
- }
30
- /** NativeScript's AccessibilityState is a single enum value, unlike the
31
- * web's independent ARIA booleans. Priority follows the strongest state. */
32
- function nativeAccessibilityState(state) {
33
- if (state?.disabled) return "disabled";
34
- if (state?.selected) return "selected";
35
- if (state?.checked === true) return "checked";
36
- if (state?.checked === false) return "unchecked";
37
- }
38
- //#endregion
39
- export { applyEscapeProps, nativeAccessibilityRole, nativeAccessibilityState };
@@ -1,2 +0,0 @@
1
- import { Pager } from "./Pager.tsrx.js";
2
- export { Pager };
@@ -1,38 +0,0 @@
1
- //#region src/layout-child.ts
2
- var FLEX_JUSTIFY = {
3
- start: "flex-start",
4
- end: "flex-end"
5
- };
6
- var FLEX_ALIGN = {
7
- start: "flex-start",
8
- end: "flex-end"
9
- };
10
- /** NativeScript reads these attributes from each child view during parent
11
- * layout. Values are forwarded with NativeScript's own property names.
12
- * The flex-container props (justifyContent/alignItems/flexWrap/gap) land on
13
- * the host flexboxlayout itself — its own attributes, not child metadata. */
14
- function layoutChildProps(props) {
15
- const result = {};
16
- for (const key of [
17
- "row",
18
- "col",
19
- "rowSpan",
20
- "colSpan",
21
- "dock",
22
- "left",
23
- "top",
24
- "flexGrow",
25
- "flexShrink",
26
- "alignSelf",
27
- "order"
28
- ]) if (props[key] !== void 0) result[key] = props[key];
29
- if (props.justifyContent !== void 0) result.justifyContent = FLEX_JUSTIFY[props.justifyContent] ?? props.justifyContent;
30
- if (props.alignItems !== void 0) result.alignItems = FLEX_ALIGN[props.alignItems] ?? props.alignItems;
31
- if (props.flexWrap !== void 0) result.flexWrap = props.flexWrap === true ? "wrap" : props.flexWrap === false ? "nowrap" : props.flexWrap;
32
- if (props.gap !== void 0) result.gap = props.gap;
33
- if (props.rowGap !== void 0) result.rowGap = props.rowGap;
34
- if (props.columnGap !== void 0) result.columnGap = props.columnGap;
35
- return result;
36
- }
37
- //#endregion
38
- export { layoutChildProps };
@@ -1,8 +0,0 @@
1
- {
2
- "version": 1,
3
- "buildId": "53f3070f-3c61-4426-97a9-b21bdbd7a163",
4
- "mode": "production",
5
- "capabilities": {
6
- "independentHydration": false
7
- }
8
- }
@@ -1,334 +0,0 @@
1
- import { applyWebProps } from "./platformProps.web.js";
2
- import { layoutChildProps } from "./layout-child.web.js";
3
- import { bag4, bagOf, child, clone, drainFrag, forBlock, hookSlots, ifBlock, markDangerouslySetInnerHTMLChildren, snapshotSpread, template, useEffect, useRef } from "octane";
4
- import { bindSignalChild, bindSignalHostPropSources, enableSignalBindings, setAriaAttributeIfChanged } from "octane/internal/client";
5
- //#region src/Pager.web.tsrx
6
- enableSignalBindings(1, true);
7
- var _t$0 = /* @__PURE__ */ template("<div class=\"vx-pager-page\" role=\"group\" aria-roledescription=\"slide\"></div>");
8
- var _t$1 = /* @__PURE__ */ template("<div><!></div>");
9
- var _t$2 = /* @__PURE__ */ template("<!>", 0, 1);
10
- var _hs$ = /* @__PURE__ */ hookSlots(8);
11
- var _key$1 = (item, i) => i;
12
- function __item$2(item, __s, __extra) {
13
- const __block = __s.block;
14
- const props = __extra[0];
15
- const i = __block.itemIndex;
16
- let _b = __s.slots[0];
17
- if (_b === void 0) {
18
- const _root = clone(_t$0);
19
- _b = bag4(__s, _root, void 0, _root, null, __s);
20
- }
21
- _b.a = setAriaAttributeIfChanged(`${i + 1} of ${props.items.length}`, _b.a, _b.b, "aria-label");
22
- _b.e = bindSignalChild(__s, _b.e, 1, _b.b, props.renderItem(item, i), "b:a193c343", null, void 0, void 0, void 0, true);
23
- }
24
- var _rm$5 = [
25
- "s",
26
- "p",
27
- "o"
28
- ];
29
- function __then$0(__props, __s, __extra) {
30
- const host = __extra[0];
31
- const props = __extra[1];
32
- let _b = __s.slots[0];
33
- if (_b === void 0) {
34
- let _m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7, _m8, _m9, _m10, _m11, _m12, _m13, _m15, _m16;
35
- const _root = clone(_t$1);
36
- _m0 = (el) => {
37
- host.current = el;
38
- applyWebProps(el, props.web);
39
- };
40
- _m1 = props.id;
41
- _m2 = ["vx-pager", props.className];
42
- _m3 = snapshotSpread(layoutChildProps(props));
43
- _m4 = props.accessibilityRole ?? "group";
44
- _m5 = "carousel";
45
- _m6 = props.accessibilityLabel;
46
- _m7 = props.accessible === false ? true : void 0;
47
- _m8 = props.accessibilityHint;
48
- _m9 = props.accessibilityValue;
49
- _m10 = props.accessibilityState?.disabled;
50
- _m11 = props.accessibilityState?.selected;
51
- _m12 = props.accessibilityState?.checked;
52
- _m13 = props.accessibilityLiveRegion === "none" ? "off" : props.accessibilityLiveRegion;
53
- _m15 = bindSignalHostPropSources(__s, void 0, _root, [
54
- [
55
- false,
56
- "ref",
57
- _m0
58
- ],
59
- [
60
- false,
61
- "id",
62
- _m1
63
- ],
64
- [
65
- false,
66
- "className",
67
- _m2
68
- ],
69
- [true, _m3],
70
- [
71
- false,
72
- "role",
73
- _m4
74
- ],
75
- [
76
- false,
77
- "aria-roledescription",
78
- _m5
79
- ],
80
- [
81
- false,
82
- "aria-label",
83
- _m6
84
- ],
85
- [
86
- false,
87
- "aria-hidden",
88
- _m7
89
- ],
90
- [
91
- false,
92
- "aria-description",
93
- _m8
94
- ],
95
- [
96
- false,
97
- "aria-valuetext",
98
- _m9
99
- ],
100
- [
101
- false,
102
- "aria-disabled",
103
- _m10
104
- ],
105
- [
106
- false,
107
- "aria-selected",
108
- _m11
109
- ],
110
- [
111
- false,
112
- "aria-checked",
113
- _m12
114
- ],
115
- [
116
- false,
117
- "aria-live",
118
- _m13
119
- ]
120
- ], "b:452923c1", true);
121
- markDangerouslySetInnerHTMLChildren(_root);
122
- _m16 = child(_root);
123
- _b = bagOf(__s, _root, {
124
- a: _m0,
125
- b: _m1,
126
- c: _m2,
127
- d: _m3,
128
- e: _m4,
129
- f: _m5,
130
- g: _m6,
131
- h: _m7,
132
- i: _m8,
133
- j: _m9,
134
- k: _m10,
135
- l: _m11,
136
- m: _m12,
137
- n: _m13,
138
- o: _root,
139
- p: _m15,
140
- q: _m16
141
- });
142
- __s.refFields = _rm$5;
143
- } else {
144
- {
145
- const _v = (el) => {
146
- host.current = el;
147
- applyWebProps(el, props.web);
148
- };
149
- if (_b.a !== _v) _b.a = _v;
150
- }
151
- {
152
- const _v = props.id;
153
- if (_b.b !== _v) _b.b = _v;
154
- }
155
- {
156
- const _v = ["vx-pager", props.className];
157
- if (_b.c !== _v) _b.c = _v;
158
- }
159
- _b.d = snapshotSpread(layoutChildProps(props));
160
- {
161
- const _v = props.accessibilityRole ?? "group";
162
- if (_b.e !== _v) _b.e = _v;
163
- }
164
- {
165
- const _v = "carousel";
166
- if (_b.f !== _v) _b.f = _v;
167
- }
168
- {
169
- const _v = props.accessibilityLabel;
170
- if (_b.g !== _v) _b.g = _v;
171
- }
172
- {
173
- const _v = props.accessible === false ? true : void 0;
174
- if (_b.h !== _v) _b.h = _v;
175
- }
176
- {
177
- const _v = props.accessibilityHint;
178
- if (_b.i !== _v) _b.i = _v;
179
- }
180
- {
181
- const _v = props.accessibilityValue;
182
- if (_b.j !== _v) _b.j = _v;
183
- }
184
- {
185
- const _v = props.accessibilityState?.disabled;
186
- if (_b.k !== _v) _b.k = _v;
187
- }
188
- {
189
- const _v = props.accessibilityState?.selected;
190
- if (_b.l !== _v) _b.l = _v;
191
- }
192
- {
193
- const _v = props.accessibilityState?.checked;
194
- if (_b.m !== _v) _b.m = _v;
195
- }
196
- {
197
- const _v = props.accessibilityLiveRegion === "none" ? "off" : props.accessibilityLiveRegion;
198
- if (_b.n !== _v) _b.n = _v;
199
- }
200
- _b.p = bindSignalHostPropSources(__s, _b.p, _b.o, [
201
- [
202
- false,
203
- "ref",
204
- _b.a
205
- ],
206
- [
207
- false,
208
- "id",
209
- _b.b
210
- ],
211
- [
212
- false,
213
- "className",
214
- _b.c
215
- ],
216
- [true, _b.d],
217
- [
218
- false,
219
- "role",
220
- _b.e
221
- ],
222
- [
223
- false,
224
- "aria-roledescription",
225
- _b.f
226
- ],
227
- [
228
- false,
229
- "aria-label",
230
- _b.g
231
- ],
232
- [
233
- false,
234
- "aria-hidden",
235
- _b.h
236
- ],
237
- [
238
- false,
239
- "aria-description",
240
- _b.i
241
- ],
242
- [
243
- false,
244
- "aria-valuetext",
245
- _b.j
246
- ],
247
- [
248
- false,
249
- "aria-disabled",
250
- _b.k
251
- ],
252
- [
253
- false,
254
- "aria-selected",
255
- _b.l
256
- ],
257
- [
258
- false,
259
- "aria-checked",
260
- _b.m
261
- ],
262
- [
263
- false,
264
- "aria-live",
265
- _b.n
266
- ]
267
- ], "b:452923c1", true);
268
- }
269
- forBlock(__s, 1, _b.o, props.items, _key$1, __item$2, 50, [props], null, _b.q, true, "f3hircj");
270
- }
271
- function __else$6(__props, __s, __extra) {
272
- const __block = __s.block;
273
- const props = __extra[1];
274
- let _b = __s.slots[0];
275
- if (_b === void 0) {
276
- let _m0, _m1;
277
- const _root = clone(_t$2);
278
- _m0 = __block.parentNode;
279
- _m1 = child(_root);
280
- drainFrag(_root, __block.parentNode, __block.endMarker);
281
- _b = bag4(__s, null, _m0, _m1, null, __s);
282
- }
283
- _b.e = bindSignalChild(__s, _b.e, 1, _b.a, props.renderEmpty?.(), "b:8f0f2972", _b.b, true, void 0, true);
284
- }
285
- var Pager = function Pager(props, __s, __extra) {
286
- const __block = __s.block;
287
- const host = useRef(null, _hs$ + 0);
288
- const latest = useRef(props, _hs$ + 1);
289
- const current = useRef(props.page ?? props.defaultPage ?? 0, _hs$ + 2);
290
- const settle = useRef(null, _hs$ + 3);
291
- useEffect(() => {
292
- latest.current = props;
293
- }, [props], _hs$ + 4);
294
- useEffect(() => {
295
- const el = host.current;
296
- if (!el) return;
297
- const onScroll = () => {
298
- if (settle.current !== null) clearTimeout(settle.current);
299
- settle.current = setTimeout(() => {
300
- settle.current = null;
301
- const el2 = host.current;
302
- if (!el2 || el2.clientWidth === 0) return;
303
- const max = Math.max(latest.current.items.length - 1, 0);
304
- const next = Math.max(0, Math.min(max, Math.round(el2.scrollLeft / el2.clientWidth)));
305
- if (next !== current.current) {
306
- current.current = next;
307
- latest.current.onPageChange?.(next);
308
- }
309
- }, 90);
310
- };
311
- el.addEventListener("scroll", onScroll, { passive: true });
312
- return () => {
313
- el.removeEventListener("scroll", onScroll);
314
- if (settle.current !== null) clearTimeout(settle.current);
315
- };
316
- }, [], _hs$ + 5);
317
- useEffect(() => {
318
- const el = host.current;
319
- if (!el || current.current <= 0) return;
320
- el.scrollLeft = current.current * el.clientWidth;
321
- }, [], _hs$ + 6);
322
- useEffect(() => {
323
- const el = host.current;
324
- if (!el || props.page === void 0 || props.page === current.current) return;
325
- current.current = props.page;
326
- el.scrollTo({
327
- left: props.page * el.clientWidth,
328
- behavior: "smooth"
329
- });
330
- }, [props.page], _hs$ + 7);
331
- ifBlock(__s, 0, __block.parentNode, props.items.length > 0, __then$0, __else$6, __block.endMarker, [host, props], 2);
332
- };
333
- //#endregion
334
- export { Pager };
package/dist/web/index.js DELETED
@@ -1,2 +0,0 @@
1
- import { Pager } from "./Pager.web.tsrx.js";
2
- export { Pager };
@@ -1,47 +0,0 @@
1
- //#region src/layout-child.web.ts
2
- /** The shared contract is "numbers are dips" (RN semantics), but octane's DOM
3
- * renderer keeps React's unitless list — lineHeight is unitless there, so
4
- * `style={{lineHeight: 40}}` would render a 40× multiplier instead of 40px.
5
- * lineHeight is the one unitless-in-React prop that is a length in the
6
- * shared vocabulary; force the px suffix. */
7
- function normStyle(style) {
8
- if (style && typeof style.lineHeight === "number") return {
9
- ...style,
10
- lineHeight: style.lineHeight + "px"
11
- };
12
- return style;
13
- }
14
- var FLEX_JUSTIFY = {
15
- start: "flex-start",
16
- end: "flex-end"
17
- };
18
- var FLEX_ALIGN = {
19
- start: "flex-start",
20
- end: "flex-end"
21
- };
22
- /** Convert parent-layout metadata to CSS on the web leaf. Native's `dock`
23
- * attribute has no CSS equivalent and is ignored on web. Grid indices are
24
- * zero-based in the shared API, matching NativeScript. */
25
- function layoutChildProps(props, baseStyle = props.style) {
26
- const style = { ...normStyle(baseStyle) };
27
- if (props.row !== void 0) style.gridRow = `${props.row + 1} / span ${props.rowSpan ?? 1}`;
28
- else if (props.rowSpan !== void 0) style.gridRow = `span ${props.rowSpan}`;
29
- if (props.col !== void 0) style.gridColumn = `${props.col + 1} / span ${props.colSpan ?? 1}`;
30
- else if (props.colSpan !== void 0) style.gridColumn = `span ${props.colSpan}`;
31
- if (props.left !== void 0) style.left = props.left;
32
- if (props.top !== void 0) style.top = props.top;
33
- if (props.flexGrow !== void 0) style.flexGrow = props.flexGrow;
34
- if (props.flexShrink !== void 0) style.flexShrink = props.flexShrink;
35
- if (props.alignSelf !== void 0) style.alignSelf = props.alignSelf;
36
- if (props.order !== void 0) style.order = props.order;
37
- if (props.justifyContent !== void 0) style.justifyContent = FLEX_JUSTIFY[props.justifyContent] ?? props.justifyContent;
38
- if (props.alignItems !== void 0) style.alignItems = FLEX_ALIGN[props.alignItems] ?? props.alignItems;
39
- if (props.flexWrap !== void 0) style.flexWrap = props.flexWrap === true ? "wrap" : props.flexWrap === false ? "nowrap" : props.flexWrap;
40
- if (props.gap !== void 0) style.gap = props.gap;
41
- if (props.rowGap !== void 0) style.rowGap = props.rowGap;
42
- if (props.columnGap !== void 0) style.columnGap = props.columnGap;
43
- if (typeof style.lineHeight === "number") style.lineHeight = style.lineHeight + "px";
44
- return { style };
45
- }
46
- //#endregion
47
- export { layoutChildProps, normStyle };
@@ -1,8 +0,0 @@
1
- {
2
- "version": 1,
3
- "buildId": "a4e920b0-d311-42b3-ad90-36799b27d84f",
4
- "mode": "production",
5
- "capabilities": {
6
- "independentHydration": false
7
- }
8
- }
@@ -1,9 +0,0 @@
1
- //#region src/platformProps.web.ts
2
- /** Apply a web escape-hatch bag after the primitive's standard props. */
3
- function applyWebProps(el, props) {
4
- if (!el || !props) return;
5
- for (const [name, value] of Object.entries(props)) if (name.includes("-")) el.setAttribute(name, String(value));
6
- else el[name] = value;
7
- }
8
- //#endregion
9
- export { applyWebProps };
@@ -1,83 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { afterEach, describe, expect, it, vi } from 'vitest';
3
- import { act, createRoot } from 'octane';
4
- import { Pager } from './Pager.web.tsrx';
5
-
6
- // jsdom has no layout — clientWidth is always 0 — so the leaf's scroll math
7
- // is exercised against a stubbed page width. scrollTo is unimplemented in
8
- // jsdom; the stub records controlled-page writes.
9
-
10
- const items = ['a', 'b', 'c'];
11
- const renderItem = (item: string) => <span class="pg">{item}</span>;
12
-
13
- function mount(extra: any = {}) {
14
- const el = document.createElement('div');
15
- document.body.appendChild(el);
16
- const root = createRoot(el);
17
- act(() => root.render(<Pager items={items} renderItem={renderItem} {...extra} />));
18
- const pager = el.querySelector('.vx-pager') as any;
19
- Object.defineProperty(pager, 'clientWidth', { value: 100, configurable: true });
20
- pager.scrollTo = vi.fn((opts: any) => {
21
- pager.scrollLeft = opts.left;
22
- pager.dispatchEvent(new Event('scroll'));
23
- });
24
-
25
- return { el, root, pager };
26
- }
27
-
28
- describe('Pager (web leaf)', () => {
29
- afterEach(() => {
30
- vi.useRealTimers();
31
- });
32
-
33
- it('renders one snap page per item, plus renderEmpty', () => {
34
- const { el, root, pager } = mount();
35
- expect(pager.className).toContain('vx-pager');
36
- expect(el.querySelectorAll('.vx-pager-page')).toHaveLength(3);
37
- expect(el.querySelectorAll('.pg')).toHaveLength(3);
38
- root.unmount();
39
- el.remove();
40
- });
41
-
42
- it('fires onPageChange once the scroll settles', () => {
43
- vi.useFakeTimers();
44
- const onPageChange = vi.fn();
45
- const { el, root, pager } = mount({ onPageChange });
46
- pager.scrollLeft = 200;
47
- pager.dispatchEvent(new Event('scroll'));
48
- vi.advanceTimersByTime(200);
49
- expect(onPageChange).toHaveBeenCalledTimes(1);
50
- expect(onPageChange).toHaveBeenCalledWith(2);
51
- root.unmount();
52
- el.remove();
53
- });
54
-
55
- it('suppresses the echo of its own controlled writes', () => {
56
- vi.useFakeTimers();
57
- const onPageChange = vi.fn();
58
- const el = document.createElement('div');
59
- document.body.appendChild(el);
60
- const root = createRoot(el);
61
- const set = { page: 0 };
62
- function Host() @{
63
- <Pager items={items} renderItem={renderItem} page={set.page} onPageChange={onPageChange} />;
64
- }
65
-
66
- act(() => root.render(<Host />));
67
- const pager = el.querySelector('.vx-pager') as any;
68
- Object.defineProperty(pager, 'clientWidth', { value: 100, configurable: true });
69
- pager.scrollTo = vi.fn((opts: any) => {
70
- pager.scrollLeft = opts.left;
71
- pager.dispatchEvent(new Event('scroll'));
72
- });
73
-
74
- set.page = 1;
75
- act(() => root.render(<Host />));
76
- expect(pager.scrollTo).toHaveBeenCalledWith({ left: 100, behavior: 'smooth' });
77
- vi.advanceTimersByTime(200);
78
- // The programmatic scroll must not report back as a user swipe.
79
- expect(onPageChange).not.toHaveBeenCalled();
80
- root.unmount();
81
- el.remove();
82
- });
83
- });
package/src/cx.mobile.ts DELETED
@@ -1,9 +0,0 @@
1
- /** Native renderers now normalize the declarative `className` prop, but
2
- * imperative `view.className` writes still need a pre-joined string — NS
3
- * splits on ' ' only. Native leaves keep normalizing through cx() before
4
- * handing className to an intrinsic so both paths accept the same values.
5
- * (The DOM renderer already flattens arrays, so web leaves don't need
6
- * this.) */
7
- export function cx(...parts: any[]): string {
8
- return parts.flat(Infinity).filter(Boolean).join(' ')
9
- }