@octane-xplat/ui 0.7.3 → 0.9.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/README.md +4 -3
- package/dist/native/Button.tsrx.js +1 -1
- package/dist/native/KeyboardAvoiding.mobile.tsrx.js +2 -2
- package/dist/native/Markdown.tsrx.js +233 -0
- package/dist/native/Overlay.tsrx.js +15 -26
- package/dist/native/PinInput.tsrx.js +25 -7
- package/dist/native/Popover.tsrx.js +17 -18
- package/dist/native/Pressable.tsrx.js +4 -1
- package/dist/native/SearchInput.tsrx.js +6 -3
- package/dist/native/Sheet.tsrx.js +22 -37
- package/dist/native/TextArea.tsrx.js +6 -3
- package/dist/native/TextInput.tsrx.js +6 -3
- package/dist/native/VirtualList.tsrx.js +305 -121
- package/dist/native/index.shared.js +7 -5
- package/dist/native/keyboard-inset.mobile.js +5 -5
- package/dist/native/navigation-request.js +26 -0
- package/dist/native/octane-client-build.json +1 -1
- package/dist/native/overlay-lifetime.mobile.js +65 -0
- package/dist/native/pan.tsrx.js +16 -2
- package/dist/native/route-table.js +90 -10
- package/dist/native/route.js +26 -16
- package/dist/native/shared.js +7 -5
- package/dist/native/sheet-service.js +1 -1
- package/dist/native/svg.mobile.js +1 -1
- package/dist/native/text-focus.js +10 -0
- package/dist/native/text-write.js +18 -11
- package/dist/native/toast-anchor.tsrx.js +1 -1
- package/dist/native/ui.js +7 -6
- package/dist/native/vendor/ui-svg/index.android.js +2 -2
- package/dist/native/vendor/ui-svg/index.ios.js +2 -2
- package/dist/native/vendor/ui-svg/{index.common.js → src/index.common.js} +1 -1
- package/dist/native/virtual-list-cell-pool.js +63 -0
- package/dist/web/Button.web.tsrx.js +1 -1
- package/dist/web/KeyboardAvoiding.web.tsrx.js +78 -0
- package/dist/web/Markdown.tsrx.js +98 -0
- package/dist/web/Overlay.web.tsrx.js +81 -32
- package/dist/web/PinInput.web.tsrx.js +24 -10
- package/dist/web/Pressable.web.tsrx.js +169 -53
- package/dist/web/SearchInput.web.tsrx.js +2 -2
- package/dist/web/Sheet.web.tsrx.js +20 -7
- package/dist/web/TextArea.web.tsrx.js +22 -17
- package/dist/web/TextInput.web.tsrx.js +2 -2
- package/dist/web/VirtualList.web.tsrx.js +220 -109
- package/dist/web/WebView.web.tsrx.js +3 -3
- package/dist/web/{index.shared.js → index.shared.web.js} +7 -5
- package/dist/web/modal-focus.web.js +95 -0
- package/dist/web/navigation-request.js +26 -0
- package/dist/web/octane-client-build.json +1 -1
- package/dist/web/pan.web.tsrx.js +27 -10
- package/dist/web/route-table.js +90 -10
- package/dist/web/route.web.js +88 -22
- package/dist/web/sheet-service.web.js +8 -0
- package/dist/web/theme/colorScheme.web.js +31 -4
- package/dist/web/ui.js +8 -6
- package/dist/web/virtual-list-cell-pool.js +63 -0
- package/package.json +21 -19
- package/platforms/android/native-api-usage.json +7 -7
- package/src/CommandPalette.tsrx +1 -0
- package/src/Grid.tsrx +2 -0
- package/src/Icon.macos.tsrx +1 -0
- package/src/KeyboardAvoiding.macos.tsrx +7 -1
- package/src/KeyboardAvoiding.mobile.tsrx +2 -2
- package/src/KeyboardAvoiding.tsrx +15 -0
- package/src/KeyboardAvoiding.web.tsrx +15 -0
- package/src/Markdown.tsrx +121 -0
- package/src/Meter.tsrx +1 -0
- package/src/Overlay.tsrx +15 -30
- package/src/Overlay.web.tsrx +12 -3
- package/src/PinInput.macos.tsrx +20 -6
- package/src/PinInput.mobile.test.ts +107 -0
- package/src/PinInput.tsrx +20 -6
- package/src/PinInput.web.test.tsrx +42 -0
- package/src/PinInput.web.tsrx +20 -6
- package/src/Popover.tsrx +17 -17
- package/src/Pressable.tsrx +1 -1
- package/src/Pressable.web.test.tsrx +36 -0
- package/src/Pressable.web.tsrx +37 -6
- package/src/RouteHost.macos.tsrx +1 -0
- package/src/SearchInput.tsrx +4 -3
- package/src/SearchInput.web.tsrx +1 -1
- package/src/Sheet.tsrx +20 -41
- package/src/Sheet.web.tsrx +9 -1
- package/src/Text.tsrx +2 -0
- package/src/TextArea.tsrx +6 -5
- package/src/TextArea.web.test.tsrx +39 -0
- package/src/TextArea.web.tsrx +7 -5
- package/src/TextInput.tsrx +6 -6
- package/src/TextInput.web.tsrx +1 -1
- package/src/VirtualList.macos.tsrx +93 -6
- package/src/VirtualList.mobile.test.tsrx +232 -0
- package/src/VirtualList.tsrx +285 -81
- package/src/VirtualList.web.tsrx +131 -25
- package/src/WebView.tsrx +1 -0
- package/src/WebView.web.tsrx +3 -0
- package/src/android/BottomNavigationView.android.tsrx +1 -0
- package/src/android/MaterialDialog.android.tsrx +1 -0
- package/src/anim.macos.tsrx +1 -0
- package/src/apply-native-modifiers.native.ts +4 -1
- package/src/child-array.ts +12 -7
- package/src/colorScheme.macos.ts +1 -0
- package/src/controls.macos.tsrx +4 -1
- package/src/data-driver.mobile.test.ts +120 -0
- package/src/data-lifecycle.mobile.test.ts +251 -0
- package/src/escape-props.ts +2 -0
- package/src/grid-auto-placement.ts +54 -12
- package/src/grid-placement.ts +1 -1
- package/src/hosted-unsupported.macos.tsrx +1 -0
- package/src/index.macos.ts +23 -3
- package/src/index.mobile.ts +0 -1
- package/src/index.shared.ts +22 -1
- package/src/index.shared.web.ts +291 -0
- package/src/index.web.ts +1 -1
- package/src/input-readiness.web.test.tsrx +82 -0
- package/src/ios/UITabBar.ios.tsrx +1 -0
- package/src/keyboard-inset.mobile.test.ts +62 -0
- package/src/keyboard-inset.mobile.ts +63 -43
- package/src/layout-shells.macos.tsrx +1 -5
- package/src/macos-extras.macos.tsrx +2 -0
- package/src/modal-focus.web.test.tsrx +63 -0
- package/src/modal-focus.web.ts +162 -0
- package/src/navigation-request.ts +37 -0
- package/src/overlay-lifecycle.mobile.test.ts +316 -0
- package/src/overlay-lifetime.mobile.ts +107 -0
- package/src/pan.mobile.test.tsrx +116 -0
- package/src/pan.tsrx +20 -2
- package/src/pan.web.tsrx +35 -7
- package/src/platformProps.ts +1 -0
- package/src/popover.mobile.test.ts +9 -1
- package/src/pressable-accessibility.mobile.test.ts +30 -0
- package/src/props.ts +163 -3
- package/src/refresh.ts +6 -1
- package/src/refresh.web.test.ts +77 -77
- package/src/route-android.test.ts +162 -0
- package/src/route-history.web.test.ts +80 -0
- package/src/route-navigation.web.test.ts +173 -0
- package/src/route-table.test.ts +188 -9
- package/src/route-table.ts +138 -10
- package/src/route.macos.ts +56 -13
- package/src/route.ts +54 -24
- package/src/route.web.ts +141 -24
- package/src/sheet-detents.ts +5 -3
- package/src/sheet-service.macos.ts +5 -1
- package/src/sheet-service.web.ts +8 -0
- package/src/sheet-snap.test.ts +23 -23
- package/src/sheet-snap.ts +6 -1
- package/src/svg.mobile.ts +9 -3
- package/src/tap-to-blur.ios.ts +48 -32
- package/src/tap-to-blur.mobile.ts +23 -15
- package/src/text-focus.ts +12 -0
- package/src/text-input.mobile.test.ts +89 -0
- package/src/text-write.ts +21 -11
- package/src/theme/colorScheme.linux.ts +2 -4
- package/src/theme/colorScheme.web.ts +51 -4
- package/src/theme/theme-scheme.macos.ts +8 -2
- package/src/theme/tokens.css +1736 -1730
- package/src/translate.macos.ts +4 -1
- package/src/virtual-list-cell-pool.test.ts +53 -0
- package/src/virtual-list-cell-pool.ts +81 -0
- package/src/virtual-list-layout.test.ts +11 -8
- package/src/virtual-list-layout.ts +4 -8
- package/src/windows.linux.ts +7 -2
- package/src/windows.macos.ts +1 -0
- package/types/generated/.tsrx-typegen-manifest.json +13 -3
- package/types/generated/KeyboardAvoiding.d.ts +3 -0
- package/types/generated/KeyboardAvoiding.web.d.ts +2 -0
- package/types/generated/Markdown.d.ts +59 -0
- package/types/generated/TextArea.d.ts +2 -2
- package/types/generated/TextArea.web.d.ts +1 -1
- package/types/generated/TextInput.d.ts +2 -3
- package/types/generated/VirtualList.d.ts +3 -3
- package/types/generated/index.mobile.d.ts +0 -1
- package/types/generated/index.shared.d.ts +6 -3
- package/types/generated/index.shared.web.d.ts +114 -0
- package/types/generated/index.web.d.ts +1 -1
- package/types/generated/modal-focus.web.d.ts +3 -0
- package/types/generated/navigation-request.d.ts +10 -0
- package/types/generated/overlay-lifetime.mobile.d.ts +10 -0
- package/types/generated/props.d.ts +111 -3
- package/types/generated/route-table.d.ts +14 -3
- package/types/generated/svg.mobile.d.ts +2 -2
- package/types/generated/text-focus.d.ts +3 -0
- package/types/generated/text-write.d.ts +3 -1
- package/types/generated/theme/colorScheme.web.d.ts +5 -0
- package/types/generated/virtual-list-cell-pool.d.ts +18 -0
- package/types/index.macos.d.ts +15 -3
- /package/src/vendor/ui-svg/{index.android.ts → src/index.android.ts} +0 -0
- /package/src/vendor/ui-svg/{index.common.ts → src/index.common.ts} +0 -0
- /package/src/vendor/ui-svg/{index.ios.ts → src/index.ios.ts} +0 -0
- /package/src/vendor/ui-svg/{index.ts → src/index.ts} +0 -0
- /package/src/vendor/ui-svg/{typings → src/typings}/SVGKit.d.ts +0 -0
- /package/src/vendor/ui-svg/{typings → src/typings}/android.d.ts +0 -0
- /package/src/vendor/ui-svg/{typings → src/typings}/ios.d.ts +0 -0
- /package/types/generated/vendor/ui-svg/{index.common.d.ts → src/index.common.d.ts} +0 -0
- /package/types/generated/vendor/ui-svg/{index.d.ts → src/index.d.ts} +0 -0
- /package/types/generated/vendor/ui-svg/{index.ios.d.ts → src/index.ios.d.ts} +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterEach, expect, it, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
createObjectContainer,
|
|
4
|
+
createObjectDriver,
|
|
5
|
+
createUniversalRoot,
|
|
6
|
+
flushUniversalSync,
|
|
7
|
+
} from 'octane/universal/native'
|
|
8
|
+
|
|
9
|
+
vi.mock('./escape-props', () => ({ applyEscapeProps() {} }))
|
|
10
|
+
vi.mock('./TextInput.tsrx', async () => {
|
|
11
|
+
const { defineUniversalComponent, universalPlan, universalValue, useRef } =
|
|
12
|
+
await import('octane/universal/native')
|
|
13
|
+
|
|
14
|
+
const plan = universalPlan('nativescript', {
|
|
15
|
+
kind: 'host',
|
|
16
|
+
type: 'test-input',
|
|
17
|
+
bindings: [
|
|
18
|
+
['value', 0],
|
|
19
|
+
['editable', 1],
|
|
20
|
+
['cell', 2],
|
|
21
|
+
],
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
TextInput: defineUniversalComponent('nativescript', (props: any) => {
|
|
26
|
+
const handle = useRef({ focus: vi.fn(), blur: vi.fn(), native: {} })
|
|
27
|
+
props.bind?.(handle.current)
|
|
28
|
+
return universalValue(plan, [
|
|
29
|
+
props.value,
|
|
30
|
+
props.editable,
|
|
31
|
+
{ onChange: props.onChange, focus: handle.current.focus },
|
|
32
|
+
])
|
|
33
|
+
}),
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
import { PinInput } from './PinInput.tsrx'
|
|
38
|
+
const roots: any[] = []
|
|
39
|
+
function mount(props: any = {}) {
|
|
40
|
+
const container = createObjectContainer('nativescript')
|
|
41
|
+
const root = createUniversalRoot(container, createObjectDriver('nativescript'))
|
|
42
|
+
roots.push(root)
|
|
43
|
+
root.render(PinInput as any, props)
|
|
44
|
+
flushUniversalSync(() => {})
|
|
45
|
+
const fields = (): any[] => {
|
|
46
|
+
const found: any[] = []
|
|
47
|
+
const walk = (node: any) => {
|
|
48
|
+
if (node.type === 'test-input') {
|
|
49
|
+
found.push(node)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const child of node.children ?? []) {
|
|
53
|
+
walk(child)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
walk(container)
|
|
58
|
+
return found
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const enter = (i: number, value: string) =>
|
|
62
|
+
flushUniversalSync(() => {
|
|
63
|
+
fields()[i].props.cell.onChange(value)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return { fields, enter }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
for (const root of roots.splice(0)) {
|
|
71
|
+
root.unmount()
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('clears a native controlled cell and its suffix', () => {
|
|
76
|
+
const change = vi.fn()
|
|
77
|
+
const { enter } = mount({ value: '1234', onValueChange: change })
|
|
78
|
+
enter(1, '')
|
|
79
|
+
expect(change).toHaveBeenCalledWith('1')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('advances native focus after enabling the next cell and preserves handles across edits', async () => {
|
|
83
|
+
const complete = vi.fn()
|
|
84
|
+
const { fields, enter } = mount({ onComplete: complete })
|
|
85
|
+
expect(fields().map((field) => field.props.editable)).toEqual([true, false, false, false])
|
|
86
|
+
for (let i = 0; i < 4; i++) {
|
|
87
|
+
enter(i, String(i + 1))
|
|
88
|
+
await vi.waitFor(() => expect(fields()[i].props.value).toBe(String(i + 1)))
|
|
89
|
+
if (i < 3) {
|
|
90
|
+
await vi.waitFor(() => expect(fields()[i + 1].props.cell.focus).toHaveBeenCalled())
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
expect(complete).toHaveBeenCalledWith('1234')
|
|
95
|
+
enter(1, '')
|
|
96
|
+
await vi.waitFor(() => expect(fields().map((f) => f.props.value)).toEqual(['1', '', '', '']))
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('ignores disabled edits and edits beyond the next empty cell', () => {
|
|
100
|
+
const change = vi.fn()
|
|
101
|
+
const { enter } = mount({ onValueChange: change })
|
|
102
|
+
enter(3, '9')
|
|
103
|
+
expect(change).not.toHaveBeenCalled()
|
|
104
|
+
const disabled = mount({ disabled: true, value: '1234', onValueChange: change })
|
|
105
|
+
disabled.enter(1, '9')
|
|
106
|
+
expect(change).not.toHaveBeenCalled()
|
|
107
|
+
})
|
package/src/PinInput.tsrx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
-
import { useState } from 'octane';
|
|
2
|
+
import { useEffect, useRef, useState } from 'octane';
|
|
3
3
|
import type { PinInputProps, TextInputHandle } from "./props";
|
|
4
4
|
import { cx } from "./cx";
|
|
5
5
|
import { applyEscapeProps } from "./escape-props";
|
|
@@ -12,7 +12,15 @@ export function PinInput(props: PinInputProps) @{
|
|
|
12
12
|
const [internal, setInternal] = useState('');
|
|
13
13
|
const controlled = props.value !== undefined;
|
|
14
14
|
const value = controlled ? props.value! : internal;
|
|
15
|
-
const handles
|
|
15
|
+
const handles = useRef<(TextInputHandle | null)[]>([]);
|
|
16
|
+
const pendingFocus = useRef<number | null>(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const next = pendingFocus.current;
|
|
19
|
+
if (next !== null && next <= value.length) {
|
|
20
|
+
pendingFocus.current = null;
|
|
21
|
+
handles.current[next]?.focus();
|
|
22
|
+
}
|
|
23
|
+
}, [value]);
|
|
16
24
|
|
|
17
25
|
const setValue = (next: string) => {
|
|
18
26
|
const clipped = next.slice(0, length);
|
|
@@ -24,10 +32,16 @@ export function PinInput(props: PinInputProps) @{
|
|
|
24
32
|
const cellValue = (i: number) => value[i] ?? '';
|
|
25
33
|
|
|
26
34
|
const enter = (i: number, text: string) => {
|
|
35
|
+
if (props.disabled || i > value.length) return;
|
|
27
36
|
const ch = text.slice(-1);
|
|
28
|
-
const next = (value.slice(0, i) + ch + value.slice(i + 1)).slice(0, length);
|
|
37
|
+
const next = ch ? (value.slice(0, i) + ch + value.slice(i + 1)).slice(0, length) : value.slice(0, i);
|
|
38
|
+
pendingFocus.current = ch && i < length - 1 ? i + 1 : null;
|
|
29
39
|
setValue(next);
|
|
30
|
-
if (
|
|
40
|
+
if (pendingFocus.current !== null && pendingFocus.current <= value.length) {
|
|
41
|
+
const index = pendingFocus.current;
|
|
42
|
+
pendingFocus.current = null;
|
|
43
|
+
handles.current[index]?.focus();
|
|
44
|
+
}
|
|
31
45
|
};
|
|
32
46
|
|
|
33
47
|
<flexboxlayout ref={(v: any) => applyEscapeProps(v, props)} id={props.id} flexDirection="row" className={cx('vx-pininput', props.disabled && 'vx-pininput--disabled', props.className)} style={props.style}>
|
|
@@ -36,10 +50,10 @@ export function PinInput(props: PinInputProps) @{
|
|
|
36
50
|
className="vx-pininput-cell"
|
|
37
51
|
keyboardType="number"
|
|
38
52
|
secure={props.secure}
|
|
39
|
-
editable={!props.disabled}
|
|
53
|
+
editable={!props.disabled && i <= value.length}
|
|
40
54
|
value={cellValue(i)}
|
|
41
55
|
onChange={(text: string) => enter(i, text)}
|
|
42
|
-
bind={(h: TextInputHandle) => { handles[i] = h; }}
|
|
56
|
+
bind={(h: TextInputHandle) => { handles.current[i] = h; }}
|
|
43
57
|
/>
|
|
44
58
|
}
|
|
45
59
|
</flexboxlayout>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, expect, it, vi } from 'vitest'
|
|
3
|
+
import { act, createRoot } from 'octane'
|
|
4
|
+
import { PinInput } from './PinInput.web.tsrx'
|
|
5
|
+
const roots:any[]=[]
|
|
6
|
+
function mount(props:any={}) {
|
|
7
|
+
const el=document.createElement('div');document.body.append(el);const root=createRoot(el);roots.push(root)
|
|
8
|
+
const render=(next:any={}) => act(() => root.render(<PinInput {...props} {...next} />));render()
|
|
9
|
+
const inputs=() => Array.from(el.querySelectorAll('input'))
|
|
10
|
+
const enter=(i:number,value:string) => {const field=inputs()[i];field.value=value;act(() => field.dispatchEvent(new Event('input',{bubbles:true})))}
|
|
11
|
+
return {render,inputs,enter}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
afterEach(() => {for (const root of roots.splice(0)) act(() => root.unmount());document.body.innerHTML=''})
|
|
15
|
+
it('clears the edited cell and suffix of a controlled PIN', () => {
|
|
16
|
+
const change=vi.fn(),complete=vi.fn();const {enter}=mount({value:'1234',onValueChange:change,onComplete:complete});enter(1,'')
|
|
17
|
+
expect(change).toHaveBeenCalledWith('1');expect(complete).not.toHaveBeenCalled()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('keeps editing contiguous and advances focus through an uncontrolled PIN', () => {
|
|
21
|
+
const change=vi.fn(),complete=vi.fn();const {inputs,enter}=mount({onValueChange:change,onComplete:complete})
|
|
22
|
+
expect(inputs().map(i => i.readOnly)).toEqual([false,true,true,true])
|
|
23
|
+
for (let i=0;i<4;i++) {enter(i,String(i+1));if (i<3) expect(document.activeElement).toBe(inputs()[i+1])}
|
|
24
|
+
expect(inputs().map(i=>i.value)).toEqual(['1','2','3','4']);expect(complete).toHaveBeenCalledTimes(1);expect(complete).toHaveBeenCalledWith('1234')
|
|
25
|
+
enter(1,'');expect(inputs().map(i=>i.value)).toEqual(['1','','','']);expect(change).toHaveBeenLastCalledWith('1')
|
|
26
|
+
enter(1,'5');expect(document.activeElement).toBe(inputs()[2]);expect(inputs().map(i=>i.value)).toEqual(['1','5','',''])
|
|
27
|
+
expect(complete).toHaveBeenCalledTimes(1)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('replaces a filled cell without shifting later cells', () => {
|
|
31
|
+
const change=vi.fn(),complete=vi.fn();const {enter}=mount({value:'1234',onValueChange:change,onComplete:complete});enter(1,'29')
|
|
32
|
+
expect(change).toHaveBeenCalledWith('1934');expect(complete).toHaveBeenCalledWith('1934')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('does not relocate input from a later empty cell to the beginning', () => {
|
|
36
|
+
const change=vi.fn();const {enter}=mount({onValueChange:change});enter(3,'9');expect(change).not.toHaveBeenCalled()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('respects disabled cells and controlled updates', () => {
|
|
40
|
+
const {render,inputs}=mount({value:'1234',disabled:true});expect(inputs().every(i=>i.readOnly)).toBe(true)
|
|
41
|
+
render({value:'12',disabled:false});expect(inputs().map(i=>i.value)).toEqual(['1','2','','']);expect(inputs().map(i=>i.readOnly)).toEqual([false,false,false,true])
|
|
42
|
+
})
|
package/src/PinInput.web.tsrx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'octane';
|
|
1
|
+
import { useEffect, useRef, useState } from 'octane';
|
|
2
2
|
import type { PinInputProps, TextInputHandle } from "./props";
|
|
3
3
|
import { applyEscapeProps } from "./escape-props";
|
|
4
4
|
import { TextInput } from './TextInput.web.tsrx';
|
|
@@ -10,7 +10,15 @@ export function PinInput(props: PinInputProps) @{
|
|
|
10
10
|
const [internal, setInternal] = useState('');
|
|
11
11
|
const controlled = props.value !== undefined;
|
|
12
12
|
const value = controlled ? props.value! : internal;
|
|
13
|
-
const handles
|
|
13
|
+
const handles = useRef<(TextInputHandle | null)[]>([]);
|
|
14
|
+
const pendingFocus = useRef<number | null>(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const next = pendingFocus.current;
|
|
17
|
+
if (next !== null && next <= value.length) {
|
|
18
|
+
pendingFocus.current = null;
|
|
19
|
+
handles.current[next]?.focus();
|
|
20
|
+
}
|
|
21
|
+
}, [value]);
|
|
14
22
|
|
|
15
23
|
const setValue = (next: string) => {
|
|
16
24
|
const clipped = next.slice(0, length);
|
|
@@ -22,10 +30,16 @@ export function PinInput(props: PinInputProps) @{
|
|
|
22
30
|
const cellValue = (i: number) => value[i] ?? '';
|
|
23
31
|
|
|
24
32
|
const enter = (i: number, text: string) => {
|
|
33
|
+
if (props.disabled || i > value.length) return;
|
|
25
34
|
const ch = text.slice(-1);
|
|
26
|
-
const next = (value.slice(0, i) + ch + value.slice(i + 1)).slice(0, length);
|
|
35
|
+
const next = ch ? (value.slice(0, i) + ch + value.slice(i + 1)).slice(0, length) : value.slice(0, i);
|
|
36
|
+
pendingFocus.current = ch && i < length - 1 ? i + 1 : null;
|
|
27
37
|
setValue(next);
|
|
28
|
-
if (
|
|
38
|
+
if (pendingFocus.current !== null && pendingFocus.current <= value.length) {
|
|
39
|
+
const index = pendingFocus.current;
|
|
40
|
+
pendingFocus.current = null;
|
|
41
|
+
handles.current[index]?.focus();
|
|
42
|
+
}
|
|
29
43
|
};
|
|
30
44
|
|
|
31
45
|
<div ref={(e: any) => applyEscapeProps(e, props)} id={props.id} className={['vx-pininput', props.disabled ? 'vx-pininput--disabled' : undefined, props.className]} style={props.style} role="group">
|
|
@@ -35,10 +49,10 @@ export function PinInput(props: PinInputProps) @{
|
|
|
35
49
|
className="vx-pininput-cell"
|
|
36
50
|
keyboardType="number"
|
|
37
51
|
secure={props.secure}
|
|
38
|
-
editable={!props.disabled}
|
|
52
|
+
editable={!props.disabled && i <= value.length}
|
|
39
53
|
value={cellValue(i)}
|
|
40
54
|
onChange={(text: string) => enter(i, text)}
|
|
41
|
-
bind={(h: TextInputHandle) => { handles[i] = h; }}
|
|
55
|
+
bind={(h: TextInputHandle) => { handles.current[i] = h; }}
|
|
42
56
|
/>
|
|
43
57
|
))}
|
|
44
58
|
</div>
|
package/src/Popover.tsrx
CHANGED
|
@@ -9,6 +9,7 @@ import { cx } from './cx';
|
|
|
9
9
|
import { positionPopover, type PopoverPosition } from './popover-position';
|
|
10
10
|
import { applyThemeClasses, useThemeScheme } from './theme/theme-scheme';
|
|
11
11
|
import { rootLayoutFor } from './root-layout.mobile';
|
|
12
|
+
import { createOverlayLifetime } from './overlay-lifetime.mobile';
|
|
12
13
|
|
|
13
14
|
function PopoverLayer(props: PopoverProps & { anchorView: any }) @{
|
|
14
15
|
const panelRef = useRef<any>(null);
|
|
@@ -38,6 +39,7 @@ function PopoverLayer(props: PopoverProps & { anchorView: any }) @{
|
|
|
38
39
|
},
|
|
39
40
|
props.placement ?? 'bottom',
|
|
40
41
|
);
|
|
42
|
+
|
|
41
43
|
const insets = isIOS ? panelRef.current?.parent?.getSafeAreaInsets?.() : undefined;
|
|
42
44
|
const scale = isIOS ? Number(Screen.mainScreen.scale) || 1 : 1;
|
|
43
45
|
const panelPosition = {
|
|
@@ -45,6 +47,7 @@ function PopoverLayer(props: PopoverProps & { anchorView: any }) @{
|
|
|
45
47
|
left: next.left - Number(insets?.left ?? 0) / scale,
|
|
46
48
|
top: next.top - Number(insets?.top ?? 0) / scale,
|
|
47
49
|
};
|
|
50
|
+
|
|
48
51
|
setPosition((current) =>
|
|
49
52
|
current?.left === panelPosition.left &&
|
|
50
53
|
current?.top === panelPosition.top &&
|
|
@@ -125,22 +128,15 @@ function PopoverLayer(props: PopoverProps & { anchorView: any }) @{
|
|
|
125
128
|
* Overlay: a transparent host opened on the RootLayout enclosing the
|
|
126
129
|
* anchor, with PopoverLayer positioning itself over the anchor's rect. */
|
|
127
130
|
export function Popover(props: PopoverProps) @{
|
|
128
|
-
const shown = useRef<{ host: any; root: any;
|
|
131
|
+
const shown = useRef<{ host: any; root: any; lifetime: ReturnType<typeof createOverlayLifetime> } | null>(null);
|
|
132
|
+
|
|
133
|
+
const dismiss = useRef(props.onDismiss);
|
|
134
|
+
dismiss.current = props.onDismiss;
|
|
129
135
|
|
|
130
136
|
useEffect(() => {
|
|
131
137
|
const cur = shown.current;
|
|
132
138
|
if (!props.open) {
|
|
133
|
-
|
|
134
|
-
shown.current = null;
|
|
135
|
-
cur.unbindTheme?.();
|
|
136
|
-
const owner = cur.host.parent as any;
|
|
137
|
-
if (owner?.hasChild?.(cur.host)) {
|
|
138
|
-
owner.close(cur.host).then(
|
|
139
|
-
() => cur.root.unmount?.(),
|
|
140
|
-
(error: any) => { console.error('[Popover] close failed', error); cur.root.unmount?.(); },
|
|
141
|
-
);
|
|
142
|
-
} else cur.root.unmount?.();
|
|
143
|
-
|
|
139
|
+
cur?.lifetime.close();
|
|
144
140
|
return;
|
|
145
141
|
}
|
|
146
142
|
|
|
@@ -158,14 +154,18 @@ export function Popover(props: PopoverProps) @{
|
|
|
158
154
|
host.horizontalAlignment = 'stretch';
|
|
159
155
|
host.verticalAlignment = 'stretch';
|
|
160
156
|
const root = createNativeScriptRoot(host);
|
|
161
|
-
|
|
162
|
-
root.render(PopoverLayer as unknown as UniversalComponent, { ...props, anchorView: anchor });
|
|
163
|
-
rl.open(host).catch((error: unknown) => {
|
|
157
|
+
const lifetime = createOverlayLifetime(rl, host, root, 'Popover', (dismissed) => {
|
|
164
158
|
if (shown.current?.host === host) shown.current = null;
|
|
165
|
-
|
|
166
|
-
root.unmount?.();
|
|
159
|
+
if (dismissed) dismiss.current?.();
|
|
167
160
|
});
|
|
161
|
+
|
|
162
|
+
lifetime.own(unbindTheme);
|
|
163
|
+
shown.current = { host, root, lifetime };
|
|
164
|
+
root.render(PopoverLayer as unknown as UniversalComponent, { ...props, anchorView: anchor });
|
|
165
|
+
lifetime.open();
|
|
168
166
|
}, [props.open, props.children, props.placement, props.className, props.style, props.dismissOnOutsideTap]);
|
|
169
167
|
|
|
168
|
+
useEffect(() => () => shown.current?.lifetime.close(), []);
|
|
169
|
+
|
|
170
170
|
<></>
|
|
171
171
|
}
|
package/src/Pressable.tsrx
CHANGED
|
@@ -20,7 +20,7 @@ export function Pressable(props: PressableProps) @{
|
|
|
20
20
|
accessibilityRole={nativeAccessibilityRole(props.accessibilityRole) as any}
|
|
21
21
|
accessibilityHint={props.accessibilityHint}
|
|
22
22
|
accessibilityValue={props.accessibilityValue}
|
|
23
|
-
accessibilityState={nativeAccessibilityState(props.accessibilityState) as any}
|
|
23
|
+
accessibilityState={nativeAccessibilityState({ ...props.accessibilityState, disabled: props.disabled || props.accessibilityState?.disabled }) as any}
|
|
24
24
|
accessibilityLiveRegion={props.accessibilityLiveRegion as any}
|
|
25
25
|
onPan={usePan(props.disabled ? undefined : props.onPan)}
|
|
26
26
|
onSwipe={props.disabled ? undefined : props.onSwipe}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, expect, it, vi } from 'vitest'
|
|
3
|
+
import { act, createRoot } from 'octane'
|
|
4
|
+
import { Pressable } from './Pressable.web.tsrx'
|
|
5
|
+
const roots: any[] = []
|
|
6
|
+
function mount(props: any = {}) {
|
|
7
|
+
vi.useFakeTimers()
|
|
8
|
+
const el = document.createElement('div');document.body.append(el);const root=createRoot(el);roots.push(root)
|
|
9
|
+
const render = (next:any={}) => act(() => root.render(<Pressable {...props} {...next} />))
|
|
10
|
+
render();const pointer=(type:string) => act(() => el.firstElementChild!.dispatchEvent(new Event(type,{bubbles:true})))
|
|
11
|
+
const advance=() => act(() => vi.advanceTimersByTime(500))
|
|
12
|
+
return {root,render,pointer,advance}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
afterEach(() => {for (const root of roots.splice(0)) act(() => root.unmount());document.body.innerHTML='';vi.useRealTimers()})
|
|
16
|
+
it.each(['pointerup','pointerleave','pointercancel'])('cancels on %s after a rerender', (event) => {
|
|
17
|
+
const callback=vi.fn();const {render,pointer,advance}=mount({onLongPress:callback})
|
|
18
|
+
pointer('pointerdown');render({className:'updated'});pointer(event);advance();expect(callback).not.toHaveBeenCalled()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('cancels on unmount', () => {
|
|
22
|
+
const callback=vi.fn();const {root,pointer,advance}=mount({onLongPress:callback});pointer('pointerdown');act(() => root.unmount());advance();expect(callback).not.toHaveBeenCalled()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it.each([{disabled:true},{onLongPress:undefined}])('cancels when long press becomes unavailable (%j)', (next) => {
|
|
26
|
+
const callback=vi.fn();const {render,pointer,advance}=mount({onLongPress:callback});pointer('pointerdown');render(next);advance();expect(callback).not.toHaveBeenCalled()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('reports one long press through the latest callback', () => {
|
|
30
|
+
const old=vi.fn(),current=vi.fn();const {render,pointer,advance}=mount({onLongPress:old});pointer('pointerdown');render({onLongPress:current});advance();advance()
|
|
31
|
+
expect(old).not.toHaveBeenCalled();expect(current).toHaveBeenCalledTimes(1)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('does not start a disabled long press', () => {
|
|
35
|
+
const callback=vi.fn();const {pointer,advance}=mount({onLongPress:callback,disabled:true});pointer('pointerdown');advance();expect(callback).not.toHaveBeenCalled()
|
|
36
|
+
})
|
package/src/Pressable.web.tsrx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef } from 'octane';
|
|
1
|
+
import { useEffect, useRef } from 'octane';
|
|
2
2
|
import type { PressableProps } from "./props";
|
|
3
3
|
import { applyEscapeProps } from "./escape-props";
|
|
4
4
|
import { layoutChildProps } from './layout-child.web';
|
|
@@ -6,17 +6,27 @@ import { usePan } from './pan.web.tsrx';
|
|
|
6
6
|
|
|
7
7
|
export function Pressable(props: PressableProps) @{
|
|
8
8
|
const el = useRef<HTMLElement | null>(null);
|
|
9
|
-
|
|
9
|
+
const spacePressed = useRef(false);
|
|
10
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
11
|
+
const latest = useRef(props);
|
|
12
|
+
latest.current = props;
|
|
10
13
|
|
|
11
14
|
usePan(el, props.disabled ? undefined : props.onPan);
|
|
12
15
|
|
|
13
|
-
const cancel = () => { if (timer !== null) { clearTimeout(timer); timer = null; } };
|
|
16
|
+
const cancel = () => { if (timer.current !== null) { clearTimeout(timer.current); timer.current = null; } };
|
|
17
|
+
useEffect(() => { if (props.disabled) spacePressed.current = false; }, [props.disabled]);
|
|
18
|
+
const enabled = !props.disabled && Boolean(props.onLongPress);
|
|
19
|
+
useEffect(() => { if (!enabled) cancel(); }, [enabled]);
|
|
20
|
+
useEffect(() => () => cancel(), []);
|
|
14
21
|
|
|
15
22
|
// Long-press parity with native: ~500ms hold over pointerdown/up.
|
|
16
23
|
const down = () => {
|
|
17
|
-
if (
|
|
24
|
+
if (latest.current.disabled || !latest.current.onLongPress) return;
|
|
18
25
|
cancel();
|
|
19
|
-
timer = setTimeout(() => {
|
|
26
|
+
timer.current = setTimeout(() => {
|
|
27
|
+
timer.current = null;
|
|
28
|
+
if (!latest.current.disabled) latest.current.onLongPress?.();
|
|
29
|
+
}, 500);
|
|
20
30
|
};
|
|
21
31
|
|
|
22
32
|
<div
|
|
@@ -28,11 +38,32 @@ export function Pressable(props: PressableProps) @{
|
|
|
28
38
|
aria-label={props.accessibilityLabel}
|
|
29
39
|
aria-description={props.accessibilityHint}
|
|
30
40
|
aria-valuetext={props.accessibilityValue}
|
|
31
|
-
aria-disabled={props.
|
|
41
|
+
aria-disabled={props.disabled || props.accessibilityState?.disabled || undefined}
|
|
32
42
|
aria-selected={props.accessibilityState?.selected}
|
|
33
43
|
aria-checked={props.accessibilityState?.checked}
|
|
34
44
|
aria-live={props.accessibilityLiveRegion === 'none' ? 'off' : props.accessibilityLiveRegion}
|
|
35
45
|
aria-hidden={props.accessible === false ? true : undefined}
|
|
46
|
+
tabIndex={props.onPress && !props.disabled && props.accessible !== false ? 0 : -1}
|
|
47
|
+
onKeyDown={(e: KeyboardEvent) => {
|
|
48
|
+
if (e.target !== e.currentTarget || props.disabled || e.isComposing || !props.onPress) return;
|
|
49
|
+
if (e.key === 'Enter') {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
if (!e.repeat) props.onPress();
|
|
52
|
+
} else if (e.key === ' ') {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
spacePressed.current = true;
|
|
55
|
+
}
|
|
56
|
+
}}
|
|
57
|
+
onKeyUp={(e: KeyboardEvent) => {
|
|
58
|
+
if (e.target !== e.currentTarget || e.key !== ' ') return;
|
|
59
|
+
const activate = spacePressed.current;
|
|
60
|
+
spacePressed.current = false;
|
|
61
|
+
if (activate && !props.disabled && props.onPress) {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
props.onPress();
|
|
64
|
+
}
|
|
65
|
+
}}
|
|
66
|
+
onBlur={() => { spacePressed.current = false; }}
|
|
36
67
|
onClick={props.disabled ? undefined : props.onPress}
|
|
37
68
|
onDoubleClick={props.disabled ? undefined : props.onDoublePress}
|
|
38
69
|
onPointerDown={(e: any) => { if (!props.disabled) { props.onPressIn?.(); down(); } }}
|
package/src/RouteHost.macos.tsrx
CHANGED
package/src/SearchInput.tsrx
CHANGED
|
@@ -4,7 +4,8 @@ import type { SearchInputProps } from './props';
|
|
|
4
4
|
import { Icon } from './Icon.tsrx';
|
|
5
5
|
import { cx } from './cx';
|
|
6
6
|
import { applyEscapeProps } from './escape-props';
|
|
7
|
-
import { writeText } from './text-write';
|
|
7
|
+
import { isWritingText, writeText } from './text-write';
|
|
8
|
+
import { blurText } from './text-focus';
|
|
8
9
|
|
|
9
10
|
/** Chrome-reset search field — a styled TextField (not UISearchBar) with a
|
|
10
11
|
* leading glyph and a self-drawn clear button, matching the web leaf.
|
|
@@ -40,13 +41,13 @@ export function SearchInput(props: SearchInputProps) @{
|
|
|
40
41
|
ref={(v: any) => {
|
|
41
42
|
el.current = v;
|
|
42
43
|
if (!v) return;
|
|
43
|
-
props.bind?.({ focus: () => v.focus(), blur: () =>
|
|
44
|
+
props.bind?.({ focus: () => v.focus(), blur: () => blurText(v), native: v });
|
|
44
45
|
}}
|
|
45
46
|
className="vx-search-field"
|
|
46
47
|
hint={props.placeholder}
|
|
47
48
|
returnKeyType="search"
|
|
48
49
|
editable={props.editable ?? true}
|
|
49
|
-
onTextChange={(e: any) => setValue(e.value)}
|
|
50
|
+
onTextChange={(e: any) => { if (!isWritingText(e.object)) setValue(e.value); }}
|
|
50
51
|
onReturnPress={props.onSubmit}
|
|
51
52
|
onFocus={props.onFocus}
|
|
52
53
|
onBlur={props.onBlur}
|
package/src/SearchInput.web.tsrx
CHANGED
|
@@ -41,7 +41,7 @@ export function SearchInput(props: SearchInputProps) @{
|
|
|
41
41
|
enterKeyHint="search"
|
|
42
42
|
onInput={(e: any) => setValue(e.target.value)}
|
|
43
43
|
onKeyDown={(e: any) => {
|
|
44
|
-
if (e.key === 'Enter' && !e.shiftKey) props.onSubmit?.();
|
|
44
|
+
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing && e.keyCode !== 229) props.onSubmit?.();
|
|
45
45
|
}}
|
|
46
46
|
onFocus={props.onFocus}
|
|
47
47
|
onBlur={props.onBlur}
|
package/src/Sheet.tsrx
CHANGED
|
@@ -7,6 +7,7 @@ import type { SheetProps } from './props';
|
|
|
7
7
|
import { cx } from './cx';
|
|
8
8
|
import { applyThemeClasses } from './theme/theme-scheme';
|
|
9
9
|
import { rootLayoutFor } from './root-layout.mobile';
|
|
10
|
+
import { createOverlayLifetime } from './overlay-lifetime.mobile';
|
|
10
11
|
import { attachSheetDetents } from './sheet-detents';
|
|
11
12
|
import { bindBottomInsetToKeyboard } from './keyboard-inset.mobile';
|
|
12
13
|
import { attachTapToBlur } from './tap-to-blur';
|
|
@@ -23,28 +24,16 @@ export function Sheet(props: SheetProps) @{
|
|
|
23
24
|
const shown = useRef<{
|
|
24
25
|
host: GridLayout;
|
|
25
26
|
root: any;
|
|
26
|
-
|
|
27
|
-
unbindKeyboard?: () => void;
|
|
28
|
-
programmatic?: boolean;
|
|
29
|
-
detach?: () => void;
|
|
27
|
+
lifetime: ReturnType<typeof createOverlayLifetime>;
|
|
30
28
|
} | null>(null);
|
|
31
29
|
|
|
30
|
+
const dismiss = useRef(props.onDismiss);
|
|
31
|
+
dismiss.current = props.onDismiss;
|
|
32
|
+
|
|
32
33
|
useEffect(() => {
|
|
33
34
|
const cur = shown.current;
|
|
34
35
|
if (!props.open) {
|
|
35
|
-
|
|
36
|
-
shown.current = null;
|
|
37
|
-
cur.programmatic = true;
|
|
38
|
-
cur.unbindTheme?.();
|
|
39
|
-
cur.unbindKeyboard?.();
|
|
40
|
-
cur.root.unmount?.();
|
|
41
|
-
const owner = cur.host.parent as any;
|
|
42
|
-
if (owner?.hasChild?.(cur.host)) {
|
|
43
|
-
owner.close(cur.host).catch((error: unknown) => {
|
|
44
|
-
console.error('[Sheet] close failed', error);
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
36
|
+
cur?.lifetime.close();
|
|
48
37
|
return;
|
|
49
38
|
}
|
|
50
39
|
|
|
@@ -63,6 +52,12 @@ export function Sheet(props: SheetProps) @{
|
|
|
63
52
|
if (props.style) Object.assign(host.style, props.style);
|
|
64
53
|
Object.assign(host, isIOS ? props.ios : isAndroid ? props.android : undefined);
|
|
65
54
|
const root = createNativeScriptRoot(host);
|
|
55
|
+
const lifetime = createOverlayLifetime(rl, host, root, 'Sheet', (dismissed) => {
|
|
56
|
+
if (shown.current?.host === host) shown.current = null;
|
|
57
|
+
if (dismissed) dismiss.current?.();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
lifetime.own(unbindTheme);
|
|
66
61
|
// Detents: the controller owns enter/slide-out — RootLayout's open
|
|
67
62
|
// animation always lands translateY at 0, which would fight the
|
|
68
63
|
// detent offsets, so it's skipped while detents are attached.
|
|
@@ -77,23 +72,13 @@ export function Sheet(props: SheetProps) @{
|
|
|
77
72
|
|
|
78
73
|
// Lift the host above the software keyboard (todo 'Add' row inside a
|
|
79
74
|
// demo sheet is bottom-anchored and was covered on iOS).
|
|
80
|
-
|
|
81
|
-
attachTapToBlur(host);
|
|
82
|
-
|
|
83
|
-
shown.current =
|
|
75
|
+
lifetime.own(bindBottomInsetToKeyboard(host));
|
|
76
|
+
lifetime.own(attachTapToBlur(host));
|
|
77
|
+
if (detents) lifetime.own(detents.detach);
|
|
78
|
+
shown.current = { host, root, lifetime };
|
|
84
79
|
root.render(SheetBody as unknown as UniversalComponent, { children: props.children });
|
|
85
|
-
host.on('closed', () => {
|
|
86
|
-
if (shown.current === opened) shown.current = null;
|
|
87
|
-
opened.detach?.();
|
|
88
|
-
opened.unbindKeyboard?.();
|
|
89
|
-
opened.unbindTheme?.();
|
|
90
|
-
opened.root.unmount?.();
|
|
91
|
-
// 'closed' fires for programmatic closes too — onDismiss only when
|
|
92
|
-
// the platform dismissed it (shade tap or detent drag-out).
|
|
93
|
-
if (!opened.programmatic) props.onDismiss?.();
|
|
94
|
-
});
|
|
95
80
|
|
|
96
|
-
|
|
81
|
+
lifetime.open({
|
|
97
82
|
...(props.shadeCover ?? true)
|
|
98
83
|
? { shadeCover: { opacity: 0.4, tapToClose: true } }
|
|
99
84
|
: {},
|
|
@@ -105,17 +90,11 @@ export function Sheet(props: SheetProps) @{
|
|
|
105
90
|
exitTo: { translateY: 400, duration: 200 },
|
|
106
91
|
},
|
|
107
92
|
}),
|
|
108
|
-
})
|
|
109
|
-
.then(() => detents?.enter())
|
|
110
|
-
.catch((error: unknown) => {
|
|
111
|
-
if (shown.current === opened) shown.current = null;
|
|
112
|
-
opened.unbindKeyboard?.();
|
|
113
|
-
opened.unbindTheme?.();
|
|
114
|
-
opened.root.unmount?.();
|
|
115
|
-
console.error('[Sheet] open failed', error);
|
|
116
|
-
});
|
|
93
|
+
}, () => detents?.enter());
|
|
117
94
|
}, [props.open, props.children, props.className, props.style, props.shadeCover, props.ios, props.android]);
|
|
118
95
|
|
|
96
|
+
useEffect(() => () => shown.current?.lifetime.close(), []);
|
|
97
|
+
|
|
119
98
|
// Zero-size sentinel — the sheet resolves its owning RootLayout from the
|
|
120
99
|
// declaring component's ancestry, not getRootLayout() (first-mounted shell).
|
|
121
100
|
<stacklayout ref={(v: any) => { sentinel.current = v; }} style={{ width: 0, height: 0 }} />
|
package/src/Sheet.web.tsrx
CHANGED
|
@@ -4,6 +4,7 @@ import { cx } from './cx';
|
|
|
4
4
|
import { useThemeScheme } from './theme/theme-scheme';
|
|
5
5
|
import { normalizeDetents } from './sheet-snap';
|
|
6
6
|
import { attachSheetDetents } from './sheet-detents.web';
|
|
7
|
+
import { isolateModalFocus } from './modal-focus.web';
|
|
7
8
|
|
|
8
9
|
/** In-window bottom sheet — portal layer with a backdrop and a bottom-
|
|
9
10
|
* anchored panel. Same vocabulary as the native RootLayout sheet.
|
|
@@ -13,8 +14,13 @@ import { attachSheetDetents } from './sheet-detents.web';
|
|
|
13
14
|
export function Sheet(props: SheetProps) @{
|
|
14
15
|
const dark = useThemeScheme() === 'dark';
|
|
15
16
|
const panel = useRef<HTMLElement | null>(null);
|
|
17
|
+
const layer = useRef<HTMLDivElement | null>(null);
|
|
16
18
|
const dismiss = useRef(props.onDismiss);
|
|
17
19
|
dismiss.current = props.onDismiss;
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!props.open || props.shadeCover === false || !layer.current || !panel.current) return;
|
|
22
|
+
return isolateModalFocus(layer.current, panel.current, () => dismiss.current?.());
|
|
23
|
+
}, [props.open, props.shadeCover]);
|
|
18
24
|
|
|
19
25
|
const detents = normalizeDetents(props.detents);
|
|
20
26
|
const maxDetent = detents.length ? detents[detents.length - 1] : 0;
|
|
@@ -31,11 +37,13 @@ export function Sheet(props: SheetProps) @{
|
|
|
31
37
|
|
|
32
38
|
@if (props.open) {
|
|
33
39
|
<>{createPortal(
|
|
34
|
-
<div className={cx('vx-sheet-layer', dark && 'dark')}>
|
|
40
|
+
<div ref={layer} className={cx('vx-sheet-layer', dark && 'dark')}>
|
|
35
41
|
{(props.shadeCover ?? true) && <div className="vx-sheet-backdrop" onClick={props.onDismiss} />}
|
|
36
42
|
<div
|
|
37
43
|
ref={(el: any) => { panel.current = el; }}
|
|
38
44
|
className={cx('vx-sheet', detents.length && 'vx-sheet-detents', props.className)}
|
|
45
|
+
role={props.shadeCover === false ? undefined : 'dialog'}
|
|
46
|
+
aria-modal={props.shadeCover === false ? undefined : true}
|
|
39
47
|
style={{
|
|
40
48
|
...props.style,
|
|
41
49
|
...(detents.length
|
package/src/Text.tsrx
CHANGED
|
@@ -37,9 +37,11 @@ export function Text(props: TextProps) @{
|
|
|
37
37
|
'extra-bold': 800,
|
|
38
38
|
black: 900,
|
|
39
39
|
};
|
|
40
|
+
|
|
40
41
|
const fontWeight = /^\d+$/.test(fontWeightValue)
|
|
41
42
|
? Number(fontWeightValue)
|
|
42
43
|
: namedWeights[fontWeightValue];
|
|
44
|
+
|
|
43
45
|
const lineHeight = props.style?.lineHeight;
|
|
44
46
|
// UIFontDescriptor resolves variable fonts to the regular axis default on
|
|
45
47
|
// iOS. Set wght explicitly so CSS fontWeight selects the matching instance.
|