@octane-xplat/pager 0.7.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/dist/native/Pager.tsrx.js +282 -0
- package/dist/native/cx.mobile.js +11 -0
- package/dist/native/escape-props.mobile.js +39 -0
- package/dist/native/index.js +2 -0
- package/dist/native/layout-child.js +38 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/web/Pager.web.tsrx.js +337 -0
- package/dist/web/index.js +2 -0
- package/dist/web/layout-child.web.js +47 -0
- package/dist/web/octane-client-build.json +8 -0
- package/dist/web/platformProps.web.js +9 -0
- package/package.json +58 -0
- package/src/Pager.macos.tsrx +7 -0
- package/src/Pager.tsrx +231 -0
- package/src/Pager.web.test.tsrx +83 -0
- package/src/Pager.web.tsrx +104 -0
- package/src/cx.mobile.ts +8 -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
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
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 settle = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
latest.current = props;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const el = host.current;
|
|
28
|
+
if (!el) return;
|
|
29
|
+
const onScroll = () => {
|
|
30
|
+
if (settle.current !== null) clearTimeout(settle.current);
|
|
31
|
+
settle.current = setTimeout(() => {
|
|
32
|
+
settle.current = null;
|
|
33
|
+
const el2 = host.current;
|
|
34
|
+
if (!el2 || el2.clientWidth === 0) return;
|
|
35
|
+
const max = Math.max(latest.current.items.length - 1, 0);
|
|
36
|
+
const next = Math.max(0, Math.min(max, Math.round(el2.scrollLeft / el2.clientWidth)));
|
|
37
|
+
if (next !== current.current) {
|
|
38
|
+
current.current = next;
|
|
39
|
+
latest.current.onPageChange?.(next);
|
|
40
|
+
}
|
|
41
|
+
}, 90);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
45
|
+
return () => {
|
|
46
|
+
el.removeEventListener('scroll', onScroll);
|
|
47
|
+
if (settle.current !== null) clearTimeout(settle.current);
|
|
48
|
+
};
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
// Mount: jump (no animation) to the starting page — an initial position
|
|
52
|
+
// is not a scroll. Runs after the scroll listener attaches so the write
|
|
53
|
+
// still flows through the tracked-index path.
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const el = host.current;
|
|
56
|
+
if (!el || current.current <= 0) return;
|
|
57
|
+
el.scrollLeft = current.current * el.clientWidth;
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
// Controlled writes scroll smoothly. Skipped when the page already
|
|
61
|
+
// matches `current` — a user swipe that the parent echoes back unchanged
|
|
62
|
+
// must not scroll again.
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const el = host.current;
|
|
65
|
+
if (!el || props.page === undefined || props.page === current.current) return;
|
|
66
|
+
current.current = props.page;
|
|
67
|
+
el.scrollTo({ left: props.page * el.clientWidth, behavior: 'smooth' });
|
|
68
|
+
}, [props.page]);
|
|
69
|
+
|
|
70
|
+
@if (props.items.length > 0) {
|
|
71
|
+
<div
|
|
72
|
+
ref={(el: any) => {
|
|
73
|
+
host.current = el;
|
|
74
|
+
applyWebProps(el, props.web);
|
|
75
|
+
}}
|
|
76
|
+
id={props.id}
|
|
77
|
+
className={['vx-pager', props.className]}
|
|
78
|
+
{...layoutChildProps(props)}
|
|
79
|
+
role={props.accessibilityRole ?? 'group'}
|
|
80
|
+
aria-roledescription="carousel"
|
|
81
|
+
aria-label={props.accessibilityLabel}
|
|
82
|
+
aria-hidden={props.accessible === false ? true : undefined}
|
|
83
|
+
aria-description={props.accessibilityHint}
|
|
84
|
+
aria-valuetext={props.accessibilityValue}
|
|
85
|
+
aria-disabled={props.accessibilityState?.disabled}
|
|
86
|
+
aria-selected={props.accessibilityState?.selected}
|
|
87
|
+
aria-checked={props.accessibilityState?.checked}
|
|
88
|
+
aria-live={props.accessibilityLiveRegion === 'none' ? 'off' : props.accessibilityLiveRegion}
|
|
89
|
+
>
|
|
90
|
+
@for (const item of props.items; index i; key i) {
|
|
91
|
+
<div
|
|
92
|
+
className="vx-pager-page"
|
|
93
|
+
role="group"
|
|
94
|
+
aria-roledescription="slide"
|
|
95
|
+
aria-label={`${i + 1} of ${props.items.length}`}
|
|
96
|
+
>
|
|
97
|
+
{props.renderItem(item, i)}
|
|
98
|
+
</div>
|
|
99
|
+
}
|
|
100
|
+
</div>
|
|
101
|
+
} @else {
|
|
102
|
+
<>{props.renderEmpty?.()}</>
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/cx.mobile.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** NS `className` splits on ' ' only, and the universal driver applies the
|
|
2
|
+
* prop via `String(value)` — an array would arrive comma-joined and match
|
|
3
|
+
* no class at all. Native leaves normalize through cx() before handing
|
|
4
|
+
* className to an intrinsic. (The DOM renderer already flattens arrays,
|
|
5
|
+
* so web leaves don't need this.) */
|
|
6
|
+
export function cx(...parts: any[]): string {
|
|
7
|
+
return parts.flat(Infinity).filter(Boolean).join(' ')
|
|
8
|
+
}
|
|
@@ -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
|
+
}
|