@octane-xplat/ui 0.7.2 → 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/CommandPalette.tsrx.js +7 -1
- package/dist/native/Grid.tsrx.js +31 -2
- 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/Pagination.tsrx.js +15 -12
- package/dist/native/PinInput.tsrx.js +25 -7
- package/dist/native/Popover.tsrx.js +33 -23
- package/dist/native/Pressable.tsrx.js +4 -1
- package/dist/native/Screen.tsrx.js +1 -1
- package/dist/native/SearchInput.tsrx.js +6 -3
- package/dist/native/Sheet.tsrx.js +23 -38
- package/dist/native/Slider.tsrx.js +23 -14
- package/dist/native/Text.tsrx.js +16 -0
- 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/WebView.tsrx.js +17 -2
- package/dist/native/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js +9 -0
- package/dist/native/escape-props.js +2 -0
- package/dist/native/grid-auto-placement.js +83 -0
- package/dist/native/grid-placement.js +20 -0
- 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 +2 -2
- package/dist/native/svg.mobile.js +1 -1
- package/dist/native/{tap-to-blur.mobile.js → tap-to-blur.ios.js} +20 -32
- 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/useMeasure.tsrx.js +17 -18
- 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 +8 -1
- package/src/Grid.tsrx +32 -1
- 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/Pagination.tsrx +6 -4
- 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 +36 -22
- 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/Screen.tsrx +1 -1
- package/src/SearchInput.tsrx +4 -3
- package/src/SearchInput.web.tsrx +1 -1
- package/src/Sheet.tsrx +21 -42
- package/src/Sheet.web.tsrx +9 -1
- package/src/Slider.tsrx +13 -11
- package/src/Text.tsrx +36 -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 +20 -10
- 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 +13 -1
- package/src/grid-auto-placement.ts +163 -0
- package/src/grid-placement.ts +29 -0
- 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.ts +1 -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 +106 -0
- package/src/tap-to-blur.mobile.ts +21 -95
- 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 +1740 -1718
- package/src/translate.macos.ts +4 -1
- package/src/useMeasure.tsrx +20 -21
- 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 +16 -3
- package/types/generated/Grid.d.ts +0 -1
- 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/escape-props.d.ts +4 -0
- package/types/generated/grid-auto-placement.d.ts +13 -0
- package/types/generated/grid-placement.d.ts +5 -0
- 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/tap-to-blur.ios.d.ts +13 -0
- package/types/generated/tap-to-blur.mobile.d.ts +1 -15
- 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
package/src/Pagination.tsrx
CHANGED
|
@@ -36,11 +36,13 @@ export function Pagination(props: PaginationProps) @{
|
|
|
36
36
|
>
|
|
37
37
|
<flexboxlayout className="vx-page-btn" onTap={() => go(current - 1)}><label text="‹" /></flexboxlayout>
|
|
38
38
|
@for (const e of pages; key e.key) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
@if (e.page == null) {
|
|
40
|
+
<label className="vx-page-ellipsis" text="…" />
|
|
41
|
+
} @else {
|
|
42
|
+
<flexboxlayout className={cx('vx-page-btn', e.page === current && 'vx-page-btn--on')} onTap={() => go(e.page!)}>
|
|
42
43
|
<label text={String(e.page)} />
|
|
43
|
-
</flexboxlayout>
|
|
44
|
+
</flexboxlayout>
|
|
45
|
+
}
|
|
44
46
|
}
|
|
45
47
|
<flexboxlayout className="vx-page-btn" onTap={() => go(current + 1)}><label text="›" /></flexboxlayout>
|
|
46
48
|
</flexboxlayout>
|
package/src/PinInput.macos.tsrx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource @xplat/macos/renderer */
|
|
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,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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource @nativescript-community/octane */
|
|
2
2
|
import { useEffect, useRef, useState } from 'octane';
|
|
3
|
-
import { GridLayout } from '@nativescript/core';
|
|
3
|
+
import { GridLayout, isIOS, Screen } from '@nativescript/core';
|
|
4
4
|
import { createNativeScriptRoot } from '@nativescript-community/octane';
|
|
5
5
|
import type { UniversalComponent } from 'octane/universal';
|
|
6
6
|
import type { PopoverProps } from './props';
|
|
@@ -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);
|
|
@@ -39,12 +40,20 @@ function PopoverLayer(props: PopoverProps & { anchorView: any }) @{
|
|
|
39
40
|
props.placement ?? 'bottom',
|
|
40
41
|
);
|
|
41
42
|
|
|
43
|
+
const insets = isIOS ? panelRef.current?.parent?.getSafeAreaInsets?.() : undefined;
|
|
44
|
+
const scale = isIOS ? Number(Screen.mainScreen.scale) || 1 : 1;
|
|
45
|
+
const panelPosition = {
|
|
46
|
+
...next,
|
|
47
|
+
left: next.left - Number(insets?.left ?? 0) / scale,
|
|
48
|
+
top: next.top - Number(insets?.top ?? 0) / scale,
|
|
49
|
+
};
|
|
50
|
+
|
|
42
51
|
setPosition((current) =>
|
|
43
|
-
current?.left ===
|
|
44
|
-
current?.top ===
|
|
45
|
-
current?.placement ===
|
|
52
|
+
current?.left === panelPosition.left &&
|
|
53
|
+
current?.top === panelPosition.top &&
|
|
54
|
+
current?.placement === panelPosition.placement
|
|
46
55
|
? current
|
|
47
|
-
:
|
|
56
|
+
: panelPosition,
|
|
48
57
|
);
|
|
49
58
|
|
|
50
59
|
if (panelSize?.width > 0 && panelSize?.height > 0) setSettled(true);
|
|
@@ -73,6 +82,14 @@ function PopoverLayer(props: PopoverProps & { anchorView: any }) @{
|
|
|
73
82
|
};
|
|
74
83
|
}, [props.open, props.anchorView, props.placement, props.children, props.style]);
|
|
75
84
|
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (position) {
|
|
87
|
+
const panel = panelRef.current;
|
|
88
|
+
const parent = panel?.parent;
|
|
89
|
+
parent?.requestLayout?.();
|
|
90
|
+
}
|
|
91
|
+
}, [position]);
|
|
92
|
+
|
|
76
93
|
useEffect(() => {
|
|
77
94
|
applyNativeProps(panelRef.current, props);
|
|
78
95
|
}, [props.ios, props.android, settled]);
|
|
@@ -111,22 +128,15 @@ function PopoverLayer(props: PopoverProps & { anchorView: any }) @{
|
|
|
111
128
|
* Overlay: a transparent host opened on the RootLayout enclosing the
|
|
112
129
|
* anchor, with PopoverLayer positioning itself over the anchor's rect. */
|
|
113
130
|
export function Popover(props: PopoverProps) @{
|
|
114
|
-
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;
|
|
115
135
|
|
|
116
136
|
useEffect(() => {
|
|
117
137
|
const cur = shown.current;
|
|
118
138
|
if (!props.open) {
|
|
119
|
-
|
|
120
|
-
shown.current = null;
|
|
121
|
-
cur.unbindTheme?.();
|
|
122
|
-
const owner = cur.host.parent as any;
|
|
123
|
-
if (owner?.hasChild?.(cur.host)) {
|
|
124
|
-
owner.close(cur.host).then(
|
|
125
|
-
() => cur.root.unmount?.(),
|
|
126
|
-
(error: any) => { console.error('[Popover] close failed', error); cur.root.unmount?.(); },
|
|
127
|
-
);
|
|
128
|
-
} else cur.root.unmount?.();
|
|
129
|
-
|
|
139
|
+
cur?.lifetime.close();
|
|
130
140
|
return;
|
|
131
141
|
}
|
|
132
142
|
|
|
@@ -144,14 +154,18 @@ export function Popover(props: PopoverProps) @{
|
|
|
144
154
|
host.horizontalAlignment = 'stretch';
|
|
145
155
|
host.verticalAlignment = 'stretch';
|
|
146
156
|
const root = createNativeScriptRoot(host);
|
|
147
|
-
|
|
148
|
-
root.render(PopoverLayer as unknown as UniversalComponent, { ...props, anchorView: anchor });
|
|
149
|
-
rl.open(host).catch((error: unknown) => {
|
|
157
|
+
const lifetime = createOverlayLifetime(rl, host, root, 'Popover', (dismissed) => {
|
|
150
158
|
if (shown.current?.host === host) shown.current = null;
|
|
151
|
-
|
|
152
|
-
root.unmount?.();
|
|
159
|
+
if (dismissed) dismiss.current?.();
|
|
153
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();
|
|
154
166
|
}, [props.open, props.children, props.placement, props.className, props.style, props.dismissOnOutsideTap]);
|
|
155
167
|
|
|
168
|
+
useEffect(() => () => shown.current?.lifetime.close(), []);
|
|
169
|
+
|
|
156
170
|
<></>
|
|
157
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/Screen.tsrx
CHANGED
|
@@ -3,7 +3,7 @@ import type { ScreenProps } from "./props";
|
|
|
3
3
|
import { cx } from "./cx";
|
|
4
4
|
import { applyEscapeProps } from "./escape-props";
|
|
5
5
|
import { registerRootLayout, unregisterRootLayout } from "./root-layout.mobile";
|
|
6
|
-
import { attachTapToBlur } from "./tap-to-blur
|
|
6
|
+
import { attachTapToBlur } from "./tap-to-blur";
|
|
7
7
|
// The screen shell is a RootLayout so RootLayout.open() can layer overlays
|
|
8
8
|
// over it (decision #22). Self-registers on loaded/unloaded — overlay and
|
|
9
9
|
// sheet services mount on the most recently mounted shell (topRootLayout).
|
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}
|