@octane-xplat/pager 0.0.0-nightly.g2e22171
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/LICENSE +21 -0
- package/README.md +56 -0
- package/package.json +82 -0
- package/src/Pager.macos.tsrx +7 -0
- package/src/Pager.parity.check.ts +19 -0
- package/src/Pager.parity.tsrx +19 -0
- package/src/Pager.tsrx +255 -0
- package/src/Pager.web.tsrx +107 -0
- package/src/escape-props.mobile.ts +65 -0
- package/src/index.macos.ts +2 -0
- package/src/index.ts +2 -0
- package/src/index.web.ts +2 -0
- package/src/layout-child.ts +57 -0
- package/src/layout-child.web.ts +108 -0
- package/src/platformProps.web.ts +14 -0
- package/src/props.ts +84 -0
- package/types/index.d.ts +5 -0
- package/types/index.macos.d.ts +4 -0
- package/types/props.d.ts +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alec Larson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# `@octane-xplat/pager`
|
|
2
|
+
|
|
3
|
+
A paged horizontal swipe container for Octane Xplat apps — onboarding flows,
|
|
4
|
+
media galleries. Each page fills the host; the user swipes between pages.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
pnpm add @octane-xplat/pager
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { useState } from 'octane'
|
|
12
|
+
import { Text } from '@octane-xplat/ui'
|
|
13
|
+
import { Pager } from '@octane-xplat/pager'
|
|
14
|
+
|
|
15
|
+
export function Onboarding() {
|
|
16
|
+
const [page, setPage] = useState(0)
|
|
17
|
+
return (
|
|
18
|
+
<Pager
|
|
19
|
+
items={['Pack', 'Travel']}
|
|
20
|
+
page={page}
|
|
21
|
+
onPageChange={setPage}
|
|
22
|
+
renderItem={(title) => <Text>{title}</Text>}
|
|
23
|
+
renderEmpty={() => <Text>No pages</Text>}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
It follows the platform-list contract — `items` + `renderItem`, no children;
|
|
30
|
+
each page is a full-host-size cell. `onPageChange` fires on settled user
|
|
31
|
+
swipes; programmatic `page` writes don't echo back. `renderEmpty` covers the
|
|
32
|
+
empty list.
|
|
33
|
+
|
|
34
|
+
There is no built-in page indicator. Compose one from `HStack` + `Text`
|
|
35
|
+
driven by the `page` state:
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { HStack, Text } from '@octane-xplat/ui'
|
|
39
|
+
|
|
40
|
+
;<HStack>
|
|
41
|
+
{titles.map((title, index) => (
|
|
42
|
+
<Text>{index === page ? '●' : '○'}</Text>
|
|
43
|
+
))}
|
|
44
|
+
</HStack>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Platform support
|
|
48
|
+
|
|
49
|
+
- **iOS/Android** — `@nativescript-community/ui-pager` (ViewPager2 on Android,
|
|
50
|
+
a paging `UICollectionView` on iOS)
|
|
51
|
+
- **Web** — a scroll-snap scroller
|
|
52
|
+
- **macOS** — an entry exists
|
|
53
|
+
|
|
54
|
+
Per-target limits are recorded in
|
|
55
|
+
[known limits](../../docs/verify/known-limits.md). Exercised by
|
|
56
|
+
[`PagerDemo`](../demos/src/PagerDemo.tsrx).
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octane-xplat/pager",
|
|
3
|
+
"version": "0.0.0-nightly.g2e22171",
|
|
4
|
+
"description": "Paged swipe container for Octane xplat — web scroll-snap row + @nativescript-community/ui-pager (ViewPager2 / paging UICollectionView)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"android",
|
|
7
|
+
"carousel",
|
|
8
|
+
"cross-platform",
|
|
9
|
+
"ios",
|
|
10
|
+
"nativescript",
|
|
11
|
+
"octane",
|
|
12
|
+
"pager",
|
|
13
|
+
"paging",
|
|
14
|
+
"swipe",
|
|
15
|
+
"viewpager",
|
|
16
|
+
"web",
|
|
17
|
+
"xplat"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/pager",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/octane-xplat/octane-xplat",
|
|
24
|
+
"directory": "packages/pager"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"types",
|
|
28
|
+
"src",
|
|
29
|
+
"!src/**/*.test.*",
|
|
30
|
+
"!src/**/*.spec.*"
|
|
31
|
+
],
|
|
32
|
+
"type": "module",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"macos": {
|
|
36
|
+
"types": "./types/index.macos.d.ts",
|
|
37
|
+
"default": "./src/index.macos.ts"
|
|
38
|
+
},
|
|
39
|
+
"types": "./types/index.d.ts",
|
|
40
|
+
"web": "./src/index.web.ts",
|
|
41
|
+
"native": "./src/index.ts",
|
|
42
|
+
"default": "./src/index.ts"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@nativescript-community/ui-pager": "14.1.42"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@nativescript-community/octane": "0.2.5",
|
|
53
|
+
"@nativescript/core": "9.1.3",
|
|
54
|
+
"@octane-xplat/macos-renderer": "0.0.0-nightly.g2e22171",
|
|
55
|
+
"@octane-xplat/ui": "0.0.0-nightly.g2e22171",
|
|
56
|
+
"@octanejs/vite-plugin": "0.1.61",
|
|
57
|
+
"octane": "0.11.0",
|
|
58
|
+
"tsrx-typegen": "0.3.0",
|
|
59
|
+
"typescript": "5.9.3",
|
|
60
|
+
"vite": "8.3.0"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@nativescript-community/octane": ">=0.2.1 <1",
|
|
64
|
+
"@nativescript/core": ">=9.1.0 <10",
|
|
65
|
+
"@octane-xplat/ui": ">=0.6.0 <1",
|
|
66
|
+
"octane": ">=0.6.3 <1"
|
|
67
|
+
},
|
|
68
|
+
"peerDependenciesMeta": {
|
|
69
|
+
"@nativescript-community/octane": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"@nativescript/core": {
|
|
73
|
+
"optional": true
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"test:native": "vitest run --config vitest.native.config.mts",
|
|
78
|
+
"typegen": "tsc -p tsconfig.types.json",
|
|
79
|
+
"test:packed": "node tests/packed-consumer.mjs",
|
|
80
|
+
"pack:check": "tsrx-typegen --pack-check"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** @jsxImportSource @octane-xplat/macos-renderer */
|
|
2
|
+
import type { PagerProps } from './props'
|
|
3
|
+
|
|
4
|
+
/** Explicit placeholder until the AppKit host has a paging surface. */
|
|
5
|
+
export function Pager(_props: PagerProps) @{
|
|
6
|
+
<label className="vx-unsupported">Pager is unsupported by the AppKit host</label>
|
|
7
|
+
}
|
|
@@ -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
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
+
import { memo, useCallback, useEffect, useRef, useState } from 'octane';
|
|
3
|
+
import {
|
|
4
|
+
registerElement,
|
|
5
|
+
} from '@nativescript-community/octane';
|
|
6
|
+
|
|
7
|
+
import type { Attributes } from '@nativescript-community/octane/intrinsics';
|
|
8
|
+
import { createPortal as createUniversalPortal, type UniversalRenderable } from 'octane/universal/native';
|
|
9
|
+
import { Pager as NSPager } from '@nativescript-community/ui-pager';
|
|
10
|
+
import { GridLayout } from '@nativescript/core';
|
|
11
|
+
import type { PagerProps } from './props';
|
|
12
|
+
import { applyEscapeProps, nativeAccessibilityRole, nativeAccessibilityState } from './escape-props.mobile';
|
|
13
|
+
import { layoutChildProps } from './layout-child';
|
|
14
|
+
|
|
15
|
+
declare module '@nativescript-community/octane/intrinsics' {
|
|
16
|
+
interface NativeScriptElements {
|
|
17
|
+
pager: Attributes<typeof NSPager>;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// @nativescript-community/ui-pager — ViewPager2 on Android, a paging
|
|
22
|
+
// UICollectionView on iOS. It is a required peer: this leaf sits in the
|
|
23
|
+
// shared native barrel, so every native app must declare the plugin for
|
|
24
|
+
// the /ns/m dev bridge to resolve it under the app root.
|
|
25
|
+
registerElement('pager', NSPager);
|
|
26
|
+
|
|
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. */
|
|
33
|
+
interface PagerCell {
|
|
34
|
+
host: any;
|
|
35
|
+
pager: any;
|
|
36
|
+
renderItem: any;
|
|
37
|
+
item: any;
|
|
38
|
+
index: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cellsByHost = new WeakMap<object, PagerCell>();
|
|
42
|
+
const cellsByPager = new WeakMap<object, Set<PagerCell>>();
|
|
43
|
+
|
|
44
|
+
function cellFor(host: any): PagerCell {
|
|
45
|
+
let cell = cellsByHost.get(host);
|
|
46
|
+
if (!cell) {
|
|
47
|
+
cell = { host, pager: null, renderItem: null, item: undefined, index: -1 };
|
|
48
|
+
cellsByHost.set(host, cell);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return cell;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `createView` runs inside the plugin's adapter — no pager instance in
|
|
55
|
+
* scope, so the cell is only linked to its pager at `itemLoading` (where
|
|
56
|
+
* `args.object` is the owner). */
|
|
57
|
+
function createPageHost() {
|
|
58
|
+
const host = new GridLayout();
|
|
59
|
+
cellFor(host);
|
|
60
|
+
return host;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const PAGER_TEMPLATES = [{ key: 'xplat-page', createView: createPageHost }];
|
|
64
|
+
// The built-in 'default' template stays first in the plugin's internal
|
|
65
|
+
// list, so the selector must name our key — a 'default' entry would match
|
|
66
|
+
// the built-in (an XML string parse, which yields nothing here).
|
|
67
|
+
const selectPageTemplate = () => 'xplat-page';
|
|
68
|
+
|
|
69
|
+
function releasePagerCells(pager: any) {
|
|
70
|
+
const cells = cellsByPager.get(pager);
|
|
71
|
+
if (!cells) return;
|
|
72
|
+
for (const cell of cells) {
|
|
73
|
+
cellsByHost.delete(cell.host);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
cellsByPager.delete(pager);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function PagerInner(props: PagerProps) @{
|
|
80
|
+
const host = useRef<any>(null);
|
|
81
|
+
const [portalCells, setPortalCells] = useState<PagerCell[]>([]);
|
|
82
|
+
// Track selectedIndex writes across synchronous and delayed echoes.
|
|
83
|
+
// The plugin raises
|
|
84
|
+
// 'selectedIndexChange' for script writes exactly as for swipes, and
|
|
85
|
+
// the driver's auto-mute only covers JSX prop writes, not ours.
|
|
86
|
+
const current = useRef(props.page ?? props.defaultPage ?? 0);
|
|
87
|
+
const pageSeeded = useRef(false);
|
|
88
|
+
|
|
89
|
+
// Stable ref identity — an inline closure re-fires ref(null)/ref(v) per
|
|
90
|
+
// render, which would bounce host.current around the effects below.
|
|
91
|
+
const bindHost = useCallback((v: any) => {
|
|
92
|
+
host.current = v;
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
const bindCell = (args: any) => {
|
|
96
|
+
const cell = cellFor(args.view);
|
|
97
|
+
cell.pager = args.object;
|
|
98
|
+
let owned = cellsByPager.get(cell.pager);
|
|
99
|
+
if (!owned) {
|
|
100
|
+
owned = new Set();
|
|
101
|
+
cellsByPager.set(cell.pager, owned);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
owned.add(cell);
|
|
105
|
+
const item = args.bindingContext ?? props.items?.[args.index];
|
|
106
|
+
if (cell.renderItem === props.renderItem && cell.index === args.index && Object.is(cell.item, item)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
cell.renderItem = props.renderItem;
|
|
111
|
+
cell.item = item;
|
|
112
|
+
cell.index = args.index;
|
|
113
|
+
setPortalCells((current) => {
|
|
114
|
+
const existing = current.some((entry) => entry === cell);
|
|
115
|
+
return existing ? [...current] : [...current, cell];
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const disposeCell = (args: any) => {
|
|
120
|
+
const cell = args.view && cellsByHost.get(args.view);
|
|
121
|
+
if (cell) {
|
|
122
|
+
cellsByHost.delete(args.view);
|
|
123
|
+
if (cell.pager) cellsByPager.get(cell.pager)?.delete(cell);
|
|
124
|
+
setPortalCells((current) => current.filter((entry) => entry !== cell));
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// One-time wiring (mount): the template set + selector are imperative —
|
|
129
|
+
// as JSX props they would be re-applied on every render. The cleanup
|
|
130
|
+
// captures the pager so cell roots release after the ref detaches.
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
const pager = host.current;
|
|
133
|
+
if (!pager) return;
|
|
134
|
+
pager.itemTemplates = PAGER_TEMPLATES;
|
|
135
|
+
pager.itemTemplateSelector = selectPageTemplate;
|
|
136
|
+
return () => releasePagerCells(pager);
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
// Items and the index are imperative writes, not JSX props: `items` must
|
|
140
|
+
// land before `selectedIndex` (the plugin coerces the index against the
|
|
141
|
+
// page count — a write ahead of items clamps to -1), and a re-assert
|
|
142
|
+
// after an items change repairs the plugin's own re-coercion.
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const pager = host.current;
|
|
145
|
+
if (!pager) return;
|
|
146
|
+
applyEscapeProps(pager, props);
|
|
147
|
+
if (pager.items !== props.items) pager.items = props.items;
|
|
148
|
+
const target = props.page ?? (pageSeeded.current ? undefined : props.defaultPage);
|
|
149
|
+
if (target !== undefined && pager.selectedIndex !== target) {
|
|
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;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
pageSeeded.current = true;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
<>
|
|
159
|
+
<pager
|
|
160
|
+
ref={bindHost}
|
|
161
|
+
id={props.id}
|
|
162
|
+
{...layoutChildProps(props)}
|
|
163
|
+
className={['vx-pager', props.className]}
|
|
164
|
+
style={props.style}
|
|
165
|
+
onItemLoading={bindCell}
|
|
166
|
+
onItemDisposing={disposeCell}
|
|
167
|
+
onSelectedIndexChange={(args: any) => {
|
|
168
|
+
if (args.value !== current.current) {
|
|
169
|
+
current.current = args.value;
|
|
170
|
+
props.onPageChange?.(args.value);
|
|
171
|
+
}
|
|
172
|
+
}}
|
|
173
|
+
accessibilityRole={nativeAccessibilityRole(props.accessibilityRole) as any}
|
|
174
|
+
accessibilityLabel={props.accessibilityLabel}
|
|
175
|
+
accessibilityHint={props.accessibilityHint}
|
|
176
|
+
accessibilityValue={props.accessibilityValue}
|
|
177
|
+
accessibilityState={nativeAccessibilityState(props.accessibilityState) as any}
|
|
178
|
+
accessible={props.accessible}
|
|
179
|
+
accessibilityLiveRegion={props.accessibilityLiveRegion as any}
|
|
180
|
+
/>
|
|
181
|
+
{portalCells.map((cell) =>
|
|
182
|
+
createPortal(
|
|
183
|
+
<gridlayout>{cell.renderItem?.(cell.item, cell.index)}</gridlayout>,
|
|
184
|
+
cell.host,
|
|
185
|
+
),
|
|
186
|
+
)}
|
|
187
|
+
</>
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// The pager subtree is memoized on `items` and configuration: the universal
|
|
191
|
+
// driver re-applies props on every render (no value diffing) and a stray
|
|
192
|
+
// `items` write would refresh the whole adapter. Constraint matches
|
|
193
|
+
// UITableView: `renderItem` should be identity-stable — a fresh closure per
|
|
194
|
+
// render is deliberately not part of the compare. (`PagerInner` must precede
|
|
195
|
+
// this — compiled components are consts, not hoisted functions.)
|
|
196
|
+
const MemoPager = memo(PagerInner, (a, b) =>
|
|
197
|
+
a.items === b.items &&
|
|
198
|
+
a.className === b.className &&
|
|
199
|
+
a.style === b.style &&
|
|
200
|
+
a.id === b.id &&
|
|
201
|
+
a.page === b.page &&
|
|
202
|
+
a.onPageChange === b.onPageChange &&
|
|
203
|
+
a.ios === b.ios &&
|
|
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
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
/** Pager — paged horizontal swiping via @nativescript-community/ui-pager
|
|
226
|
+
* (ViewPager2 / paging UICollectionView), chrome-reset: one full-size cell
|
|
227
|
+
* per item, no indicator. Pages recycle through `itemTemplates` +
|
|
228
|
+
* `itemLoading`; active cell hosts are portals from the Pager's declaring
|
|
229
|
+
* tree, so provider context follows each cell. */
|
|
230
|
+
export function Pager(props: PagerProps) @{
|
|
231
|
+
@if (props.items.length > 0) {
|
|
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}
|
|
240
|
+
id={props.id}
|
|
241
|
+
className={props.className}
|
|
242
|
+
style={props.style}
|
|
243
|
+
items={props.items}
|
|
244
|
+
renderItem={props.renderItem}
|
|
245
|
+
page={props.page}
|
|
246
|
+
defaultPage={props.defaultPage}
|
|
247
|
+
onPageChange={props.onPageChange}
|
|
248
|
+
ios={props.ios}
|
|
249
|
+
android={props.android}
|
|
250
|
+
{...layoutChildProps(props)}
|
|
251
|
+
/>
|
|
252
|
+
} @else {
|
|
253
|
+
<>{props.renderEmpty?.()}</>
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'octane';
|
|
2
|
+
import type { PagerProps } from './props';
|
|
3
|
+
import { applyWebProps } from './platformProps.web';
|
|
4
|
+
import { layoutChildProps } from './layout-child.web';
|
|
5
|
+
|
|
6
|
+
/** Paged horizontal container — web leaf: a mandatory scroll-snap row, one
|
|
7
|
+
* page per item at 100% host width. `onPageChange` fires on the settled
|
|
8
|
+
* snap page (a scroll-idle debounce reads scrollLeft after the snap lands),
|
|
9
|
+
* matching the native widget's selected-index event. Programmatic `page`
|
|
10
|
+
* writes are tracked in `current` so their own scroll never echoes back
|
|
11
|
+
* through `onPageChange` — same contract as the muted property echo on
|
|
12
|
+
* native. No indicator — compose dots from Row + View outside. */
|
|
13
|
+
export function Pager(props: PagerProps) @{
|
|
14
|
+
const host = useRef<HTMLElement | null>(null);
|
|
15
|
+
const latest = useRef(props);
|
|
16
|
+
// The page the leaf believes it is on: seeded by `page`/`defaultPage`,
|
|
17
|
+
// updated by controlled writes and settled swipes. A scroll that lands
|
|
18
|
+
// on `current` is programmatic — the settle never reports it.
|
|
19
|
+
const current = useRef(props.page ?? props.defaultPage ?? 0);
|
|
20
|
+
const hasItems = props.items.length > 0;
|
|
21
|
+
const settle = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
latest.current = props;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const el = host.current;
|
|
29
|
+
if (!el) return;
|
|
30
|
+
const onScroll = () => {
|
|
31
|
+
if (settle.current !== null) clearTimeout(settle.current);
|
|
32
|
+
settle.current = setTimeout(() => {
|
|
33
|
+
settle.current = null;
|
|
34
|
+
const el2 = host.current;
|
|
35
|
+
if (!el2 || el2.clientWidth === 0) return;
|
|
36
|
+
const max = Math.max(latest.current.items.length - 1, 0);
|
|
37
|
+
const next = Math.max(0, Math.min(max, Math.round(el2.scrollLeft / el2.clientWidth)));
|
|
38
|
+
if (next !== current.current) {
|
|
39
|
+
current.current = next;
|
|
40
|
+
latest.current.onPageChange?.(next);
|
|
41
|
+
}
|
|
42
|
+
}, 90);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
46
|
+
return () => {
|
|
47
|
+
el.removeEventListener('scroll', onScroll);
|
|
48
|
+
if (settle.current !== null) clearTimeout(settle.current);
|
|
49
|
+
settle.current = null;
|
|
50
|
+
};
|
|
51
|
+
}, [hasItems]);
|
|
52
|
+
|
|
53
|
+
// Each new host: jump (no animation) to the starting page — an initial position
|
|
54
|
+
// is not a scroll. Runs after the scroll listener attaches so the write
|
|
55
|
+
// still flows through the tracked-index path.
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const el = host.current;
|
|
58
|
+
if (!el) return;
|
|
59
|
+
current.current = props.page ?? current.current;
|
|
60
|
+
el.scrollLeft = current.current * el.clientWidth;
|
|
61
|
+
}, [hasItems]);
|
|
62
|
+
|
|
63
|
+
// Controlled writes scroll smoothly. Skipped when the page already
|
|
64
|
+
// matches `current` — a user swipe that the parent echoes back unchanged
|
|
65
|
+
// must not scroll again.
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const el = host.current;
|
|
68
|
+
if (!el || props.page === undefined || props.page === current.current) return;
|
|
69
|
+
current.current = props.page;
|
|
70
|
+
el.scrollTo({ left: props.page * el.clientWidth, behavior: 'smooth' });
|
|
71
|
+
}, [props.page]);
|
|
72
|
+
|
|
73
|
+
@if (props.items.length > 0) {
|
|
74
|
+
<div
|
|
75
|
+
ref={(el: any) => {
|
|
76
|
+
host.current = el;
|
|
77
|
+
applyWebProps(el, props.web);
|
|
78
|
+
}}
|
|
79
|
+
id={props.id}
|
|
80
|
+
className={['vx-pager', props.className]}
|
|
81
|
+
{...layoutChildProps(props)}
|
|
82
|
+
role={props.accessibilityRole ?? 'group'}
|
|
83
|
+
aria-roledescription="carousel"
|
|
84
|
+
aria-label={props.accessibilityLabel}
|
|
85
|
+
aria-hidden={props.accessible === false ? true : undefined}
|
|
86
|
+
aria-description={props.accessibilityHint}
|
|
87
|
+
aria-valuetext={props.accessibilityValue}
|
|
88
|
+
aria-disabled={props.accessibilityState?.disabled}
|
|
89
|
+
aria-selected={props.accessibilityState?.selected}
|
|
90
|
+
aria-checked={props.accessibilityState?.checked}
|
|
91
|
+
aria-live={props.accessibilityLiveRegion === 'none' ? 'off' : props.accessibilityLiveRegion}
|
|
92
|
+
>
|
|
93
|
+
@for (const item of props.items; index i; key i) {
|
|
94
|
+
<div
|
|
95
|
+
className="vx-pager-page"
|
|
96
|
+
role="group"
|
|
97
|
+
aria-roledescription="slide"
|
|
98
|
+
aria-label={`${i + 1} of ${props.items.length}`}
|
|
99
|
+
>
|
|
100
|
+
{props.renderItem(item, i)}
|
|
101
|
+
</div>
|
|
102
|
+
}
|
|
103
|
+
</div>
|
|
104
|
+
} @else {
|
|
105
|
+
<>{props.renderEmpty?.()}</>
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isAndroid, isIOS } from '@nativescript/core'
|
|
2
|
+
import type { Role } from './props'
|
|
3
|
+
|
|
4
|
+
/** Apply only the active device's escape bag after the primitive's own props. */
|
|
5
|
+
export function applyEscapeProps(view: any, props: { ios?: any; android?: any }): void {
|
|
6
|
+
if (!view) {
|
|
7
|
+
return
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const bag = isIOS ? props.ios : isAndroid ? props.android : undefined
|
|
11
|
+
if (bag) {
|
|
12
|
+
Object.assign(view, bag)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const NATIVE_ACCESSIBILITY_ROLES: Record<Role, string> = {
|
|
17
|
+
button: 'button',
|
|
18
|
+
link: 'link',
|
|
19
|
+
search: 'search',
|
|
20
|
+
image: 'image',
|
|
21
|
+
heading: 'header',
|
|
22
|
+
adjustable: 'adjustable',
|
|
23
|
+
summary: 'summary',
|
|
24
|
+
text: 'text',
|
|
25
|
+
none: 'none',
|
|
26
|
+
progressbar: 'progressBar',
|
|
27
|
+
checkbox: 'checkbox',
|
|
28
|
+
switch: 'switch',
|
|
29
|
+
radio: 'radioButton',
|
|
30
|
+
spinbutton: 'spinButton',
|
|
31
|
+
// NativeScript has no tab role; a tab is still an actionable button and
|
|
32
|
+
// its selected state is carried separately by accessibilityState.
|
|
33
|
+
tab: 'button',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Translate the shared/ARIA spelling to NativeScript's narrower role enum. */
|
|
37
|
+
export function nativeAccessibilityRole(role?: Role): string | undefined {
|
|
38
|
+
return role ? NATIVE_ACCESSIBILITY_ROLES[role] : undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** NativeScript's AccessibilityState is a single enum value, unlike the
|
|
42
|
+
* web's independent ARIA booleans. Priority follows the strongest state. */
|
|
43
|
+
export function nativeAccessibilityState(state?: {
|
|
44
|
+
disabled?: boolean
|
|
45
|
+
selected?: boolean
|
|
46
|
+
checked?: boolean
|
|
47
|
+
}): string | undefined {
|
|
48
|
+
if (state?.disabled) {
|
|
49
|
+
return 'disabled'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (state?.selected) {
|
|
53
|
+
return 'selected'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (state?.checked === true) {
|
|
57
|
+
return 'checked'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (state?.checked === false) {
|
|
61
|
+
return 'unchecked'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
package/src/index.ts
ADDED
package/src/index.web.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { LayoutChildProps, FlexContainerProps } from './props'
|
|
2
|
+
|
|
3
|
+
// start/end shorthand → flex-start/flex-end.
|
|
4
|
+
const FLEX_JUSTIFY: Record<string, string> = { start: 'flex-start', end: 'flex-end' }
|
|
5
|
+
const FLEX_ALIGN: Record<string, string> = { start: 'flex-start', end: 'flex-end' }
|
|
6
|
+
|
|
7
|
+
/** NativeScript reads these attributes from each child view during parent
|
|
8
|
+
* layout. Values are forwarded with NativeScript's own property names.
|
|
9
|
+
* The flex-container props (justifyContent/alignItems/flexWrap/gap) land on
|
|
10
|
+
* the host flexboxlayout itself — its own attributes, not child metadata. */
|
|
11
|
+
export function layoutChildProps(props: LayoutChildProps & Partial<FlexContainerProps>) {
|
|
12
|
+
const result: Record<string, any> = {}
|
|
13
|
+
for (const key of [
|
|
14
|
+
'row',
|
|
15
|
+
'col',
|
|
16
|
+
'rowSpan',
|
|
17
|
+
'colSpan',
|
|
18
|
+
'dock',
|
|
19
|
+
'left',
|
|
20
|
+
'top',
|
|
21
|
+
'flexGrow',
|
|
22
|
+
'flexShrink',
|
|
23
|
+
'alignSelf',
|
|
24
|
+
'order',
|
|
25
|
+
] as const) {
|
|
26
|
+
if (props[key] !== undefined) {
|
|
27
|
+
result[key] = props[key]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (props.justifyContent !== undefined) {
|
|
32
|
+
result.justifyContent = FLEX_JUSTIFY[props.justifyContent] ?? props.justifyContent
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (props.alignItems !== undefined) {
|
|
36
|
+
result.alignItems = FLEX_ALIGN[props.alignItems] ?? props.alignItems
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (props.flexWrap !== undefined) {
|
|
40
|
+
result.flexWrap =
|
|
41
|
+
props.flexWrap === true ? 'wrap' : props.flexWrap === false ? 'nowrap' : props.flexWrap
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (props.gap !== undefined) {
|
|
45
|
+
result.gap = props.gap
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (props.rowGap !== undefined) {
|
|
49
|
+
result.rowGap = props.rowGap
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (props.columnGap !== undefined) {
|
|
53
|
+
result.columnGap = props.columnGap
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result
|
|
57
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { LayoutChildProps, FlexContainerProps } from './props'
|
|
2
|
+
|
|
3
|
+
/** The shared contract is "numbers are dips" (RN semantics), but octane's DOM
|
|
4
|
+
* renderer keeps React's unitless list — lineHeight is unitless there, so
|
|
5
|
+
* `style={{lineHeight: 40}}` would render a 40× multiplier instead of 40px.
|
|
6
|
+
* lineHeight is the one unitless-in-React prop that is a length in the
|
|
7
|
+
* shared vocabulary; force the px suffix. */
|
|
8
|
+
export function normStyle(style: any) {
|
|
9
|
+
if (style && typeof style.lineHeight === 'number') {
|
|
10
|
+
return { ...style, lineHeight: style.lineHeight + 'px' }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return style
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// start/end shorthand → flex-start/flex-end (valid in both engines).
|
|
17
|
+
const FLEX_JUSTIFY: Record<string, string> = {
|
|
18
|
+
start: 'flex-start',
|
|
19
|
+
end: 'flex-end',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const FLEX_ALIGN: Record<string, string> = {
|
|
23
|
+
start: 'flex-start',
|
|
24
|
+
end: 'flex-end',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Convert parent-layout metadata to CSS on the web leaf. Native's `dock`
|
|
28
|
+
* attribute has no CSS equivalent and is ignored on web. Grid indices are
|
|
29
|
+
* zero-based in the shared API, matching NativeScript. */
|
|
30
|
+
export function layoutChildProps(
|
|
31
|
+
props: LayoutChildProps & Partial<FlexContainerProps> & { style?: any },
|
|
32
|
+
baseStyle = props.style,
|
|
33
|
+
) {
|
|
34
|
+
const style = { ...normStyle(baseStyle) }
|
|
35
|
+
if (props.row !== undefined) {
|
|
36
|
+
style.gridRow = `${props.row + 1} / span ${props.rowSpan ?? 1}`
|
|
37
|
+
} else if (props.rowSpan !== undefined) {
|
|
38
|
+
style.gridRow = `span ${props.rowSpan}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (props.col !== undefined) {
|
|
42
|
+
style.gridColumn = `${props.col + 1} / span ${props.colSpan ?? 1}`
|
|
43
|
+
} else if (props.colSpan !== undefined) {
|
|
44
|
+
style.gridColumn = `span ${props.colSpan}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (props.left !== undefined) {
|
|
48
|
+
style.left = props.left
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (props.top !== undefined) {
|
|
52
|
+
style.top = props.top
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (props.flexGrow !== undefined) {
|
|
56
|
+
style.flexGrow = props.flexGrow
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (props.flexShrink !== undefined) {
|
|
60
|
+
style.flexShrink = props.flexShrink
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (props.alignSelf !== undefined) {
|
|
64
|
+
style.alignSelf = props.alignSelf
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (props.order !== undefined) {
|
|
68
|
+
style.order = props.order
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Flex-container props (View/Row/Pressable hosts) — RN vocabulary on the
|
|
72
|
+
// element style. Numbers become px via octane's style normalization.
|
|
73
|
+
if (props.justifyContent !== undefined) {
|
|
74
|
+
style.justifyContent = FLEX_JUSTIFY[props.justifyContent] ?? props.justifyContent
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (props.alignItems !== undefined) {
|
|
78
|
+
style.alignItems = FLEX_ALIGN[props.alignItems] ?? props.alignItems
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (props.flexWrap !== undefined) {
|
|
82
|
+
style.flexWrap =
|
|
83
|
+
props.flexWrap === true ? 'wrap' : props.flexWrap === false ? 'nowrap' : props.flexWrap
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (props.gap !== undefined) {
|
|
87
|
+
style.gap = props.gap
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (props.rowGap !== undefined) {
|
|
91
|
+
style.rowGap = props.rowGap
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (props.columnGap !== undefined) {
|
|
95
|
+
style.columnGap = props.columnGap
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The shared contract is "numbers are dips" (RN semantics), but octane's
|
|
99
|
+
// DOM renderer keeps React's unitless list — lineHeight is unitless there,
|
|
100
|
+
// so `style={{lineHeight: 40}}` would render a 40× multiplier instead of
|
|
101
|
+
// 40px. lineHeight is the one unitless-in-React prop that is a length in
|
|
102
|
+
// the shared vocabulary; force the px suffix.
|
|
103
|
+
if (typeof style.lineHeight === 'number') {
|
|
104
|
+
style.lineHeight = style.lineHeight + 'px'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { style }
|
|
108
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Apply a web escape-hatch bag after the primitive's standard props. */
|
|
2
|
+
export function applyWebProps(el: any, props: Record<string, any> | undefined) {
|
|
3
|
+
if (!el || !props) {
|
|
4
|
+
return
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
for (const [name, value] of Object.entries(props)) {
|
|
8
|
+
if (name.includes('-')) {
|
|
9
|
+
el.setAttribute(name, String(value))
|
|
10
|
+
} else {
|
|
11
|
+
el[name] = value
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/props.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export interface LayoutChildProps {
|
|
2
|
+
row?: number
|
|
3
|
+
col?: number
|
|
4
|
+
rowSpan?: number
|
|
5
|
+
colSpan?: number
|
|
6
|
+
dock?: 'left' | 'top' | 'right' | 'bottom'
|
|
7
|
+
left?: number
|
|
8
|
+
top?: number
|
|
9
|
+
flexGrow?: number
|
|
10
|
+
flexShrink?: number
|
|
11
|
+
alignSelf?: string
|
|
12
|
+
order?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Flex-container props shared by View/Row/Pressable — RN vocabulary, applied
|
|
16
|
+
* to the host flexboxlayout natively and the element's style on web.
|
|
17
|
+
* `gap` is a dip number (px on web); NS supports it on FlexboxLayout only
|
|
18
|
+
* (GridLayout has no gap). */
|
|
19
|
+
export interface FlexContainerProps {
|
|
20
|
+
justifyContent?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly'
|
|
21
|
+
alignItems?: 'start' | 'center' | 'end' | 'stretch' | 'baseline'
|
|
22
|
+
flexWrap?: boolean | 'wrap' | 'nowrap' | 'wrap-reverse'
|
|
23
|
+
gap?: number | string
|
|
24
|
+
rowGap?: number | string
|
|
25
|
+
columnGap?: number | string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type Role =
|
|
29
|
+
| 'button'
|
|
30
|
+
| 'link'
|
|
31
|
+
| 'search'
|
|
32
|
+
| 'image'
|
|
33
|
+
| 'heading'
|
|
34
|
+
| 'adjustable'
|
|
35
|
+
| 'summary'
|
|
36
|
+
| 'text'
|
|
37
|
+
| 'none'
|
|
38
|
+
| 'progressbar'
|
|
39
|
+
| 'checkbox'
|
|
40
|
+
| 'switch'
|
|
41
|
+
| 'radio'
|
|
42
|
+
| 'spinbutton'
|
|
43
|
+
| 'tab'
|
|
44
|
+
|
|
45
|
+
export interface AccessibilityProps {
|
|
46
|
+
accessible?: boolean
|
|
47
|
+
accessibilityLabel?: string
|
|
48
|
+
accessibilityRole?: Role
|
|
49
|
+
accessibilityHint?: string
|
|
50
|
+
accessibilityValue?: string
|
|
51
|
+
accessibilityState?: {
|
|
52
|
+
disabled?: boolean
|
|
53
|
+
selected?: boolean
|
|
54
|
+
checked?: boolean
|
|
55
|
+
}
|
|
56
|
+
accessibilityLiveRegion?: 'none' | 'polite' | 'assertive'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Paged horizontal swipe container — onboarding flows, media galleries.
|
|
60
|
+
* Chrome-reset OS paging on native (ViewPager2 / UICollectionView paging via
|
|
61
|
+
* @nativescript-community/ui-pager), a scroll-snap scroller on web. Follows
|
|
62
|
+
* the platform-list contract: `items` + `renderItem`, no children — each
|
|
63
|
+
* page is a full-host-size cell. No indicator is built in; compose dots
|
|
64
|
+
* from `Row` + `View` driven by `page`/`onPageChange`. */
|
|
65
|
+
export interface PagerProps extends LayoutChildProps, AccessibilityProps {
|
|
66
|
+
className?: any
|
|
67
|
+
style?: any
|
|
68
|
+
id?: string
|
|
69
|
+
items: any[]
|
|
70
|
+
renderItem: (item: any, index: number) => any
|
|
71
|
+
renderEmpty?: () => any
|
|
72
|
+
/** Controlled page index — pass with `onPageChange` to own the page.
|
|
73
|
+
* Writes scroll/settle to the page; user swipes call `onPageChange`. */
|
|
74
|
+
page?: number
|
|
75
|
+
/** Starting page for uncontrolled use (default 0). */
|
|
76
|
+
defaultPage?: number
|
|
77
|
+
/** Fires when the settled page changes — a completed swipe on both
|
|
78
|
+
* targets. Programmatic `page` writes do not echo back through it. */
|
|
79
|
+
onPageChange?: (index: number) => void
|
|
80
|
+
/** Platform escape hatches, applied after the shared props. */
|
|
81
|
+
ios?: Record<string, any>
|
|
82
|
+
android?: Record<string, any>
|
|
83
|
+
web?: Record<string, any>
|
|
84
|
+
}
|
package/types/index.d.ts
ADDED
package/types/props.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface LayoutChildProps {
|
|
2
|
+
row?: number;
|
|
3
|
+
col?: number;
|
|
4
|
+
rowSpan?: number;
|
|
5
|
+
colSpan?: number;
|
|
6
|
+
dock?: 'left' | 'top' | 'right' | 'bottom';
|
|
7
|
+
left?: number;
|
|
8
|
+
top?: number;
|
|
9
|
+
flexGrow?: number;
|
|
10
|
+
flexShrink?: number;
|
|
11
|
+
alignSelf?: string;
|
|
12
|
+
order?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Flex-container props shared by View/Row/Pressable — RN vocabulary, applied
|
|
15
|
+
* to the host flexboxlayout natively and the element's style on web.
|
|
16
|
+
* `gap` is a dip number (px on web); NS supports it on FlexboxLayout only
|
|
17
|
+
* (GridLayout has no gap). */
|
|
18
|
+
export interface FlexContainerProps {
|
|
19
|
+
justifyContent?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly';
|
|
20
|
+
alignItems?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
|
|
21
|
+
flexWrap?: boolean | 'wrap' | 'nowrap' | 'wrap-reverse';
|
|
22
|
+
gap?: number | string;
|
|
23
|
+
rowGap?: number | string;
|
|
24
|
+
columnGap?: number | string;
|
|
25
|
+
}
|
|
26
|
+
export type Role = 'button' | 'link' | 'search' | 'image' | 'heading' | 'adjustable' | 'summary' | 'text' | 'none' | 'progressbar' | 'checkbox' | 'switch' | 'radio' | 'spinbutton' | 'tab';
|
|
27
|
+
export interface AccessibilityProps {
|
|
28
|
+
accessible?: boolean;
|
|
29
|
+
accessibilityLabel?: string;
|
|
30
|
+
accessibilityRole?: Role;
|
|
31
|
+
accessibilityHint?: string;
|
|
32
|
+
accessibilityValue?: string;
|
|
33
|
+
accessibilityState?: {
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
selected?: boolean;
|
|
36
|
+
checked?: boolean;
|
|
37
|
+
};
|
|
38
|
+
accessibilityLiveRegion?: 'none' | 'polite' | 'assertive';
|
|
39
|
+
}
|
|
40
|
+
/** Paged horizontal swipe container — onboarding flows, media galleries.
|
|
41
|
+
* Chrome-reset OS paging on native (ViewPager2 / UICollectionView paging via
|
|
42
|
+
* @nativescript-community/ui-pager), a scroll-snap scroller on web. Follows
|
|
43
|
+
* the platform-list contract: `items` + `renderItem`, no children — each
|
|
44
|
+
* page is a full-host-size cell. No indicator is built in; compose dots
|
|
45
|
+
* from `Row` + `View` driven by `page`/`onPageChange`. */
|
|
46
|
+
export interface PagerProps extends LayoutChildProps, AccessibilityProps {
|
|
47
|
+
className?: any;
|
|
48
|
+
style?: any;
|
|
49
|
+
id?: string;
|
|
50
|
+
items: any[];
|
|
51
|
+
renderItem: (item: any, index: number) => any;
|
|
52
|
+
renderEmpty?: () => any;
|
|
53
|
+
/** Controlled page index — pass with `onPageChange` to own the page.
|
|
54
|
+
* Writes scroll/settle to the page; user swipes call `onPageChange`. */
|
|
55
|
+
page?: number;
|
|
56
|
+
/** Starting page for uncontrolled use (default 0). */
|
|
57
|
+
defaultPage?: number;
|
|
58
|
+
/** Fires when the settled page changes — a completed swipe on both
|
|
59
|
+
* targets. Programmatic `page` writes do not echo back through it. */
|
|
60
|
+
onPageChange?: (index: number) => void;
|
|
61
|
+
/** Platform escape hatches, applied after the shared props. */
|
|
62
|
+
ios?: Record<string, any>;
|
|
63
|
+
android?: Record<string, any>;
|
|
64
|
+
web?: Record<string, any>;
|
|
65
|
+
}
|