@wordpress/components 23.7.0 → 23.8.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/CHANGELOG.md +17 -6
- package/build/custom-gradient-picker/serializer.js +0 -4
- package/build/custom-gradient-picker/serializer.js.map +1 -1
- package/build/drop-zone/index.js +8 -8
- package/build/drop-zone/index.js.map +1 -1
- package/build/index.js.map +1 -1
- package/build/mobile/keyboard-aware-flat-list/index.android.js +0 -4
- package/build/mobile/keyboard-aware-flat-list/index.android.js.map +1 -1
- package/build/mobile/keyboard-aware-flat-list/index.ios.js +100 -55
- package/build/mobile/keyboard-aware-flat-list/index.ios.js.map +1 -1
- package/build/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js +82 -0
- package/build/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js.map +1 -0
- package/build/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js +85 -0
- package/build/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js.map +1 -0
- package/build/mobile/keyboard-aware-flat-list/use-text-input-caret-position.native.js +44 -0
- package/build/mobile/keyboard-aware-flat-list/use-text-input-caret-position.native.js.map +1 -0
- package/build/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js +53 -0
- package/build/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js.map +1 -0
- package/build/navigator/navigator-provider/component.js +4 -2
- package/build/navigator/navigator-provider/component.js.map +1 -1
- package/build/navigator/navigator-screen/component.js +4 -3
- package/build/navigator/navigator-screen/component.js.map +1 -1
- package/build/private-apis.js.map +1 -1
- package/build/query-controls/author-select.js +2 -1
- package/build/query-controls/author-select.js.map +1 -1
- package/build/query-controls/category-select.js +3 -1
- package/build/query-controls/category-select.js.map +1 -1
- package/build/query-controls/index.js +7 -1
- package/build/query-controls/index.js.map +1 -1
- package/build/sandbox/index.native.js +51 -28
- package/build/sandbox/index.native.js.map +1 -1
- package/build-module/custom-gradient-picker/serializer.js +0 -4
- package/build-module/custom-gradient-picker/serializer.js.map +1 -1
- package/build-module/drop-zone/index.js +8 -8
- package/build-module/drop-zone/index.js.map +1 -1
- package/build-module/index.js.map +1 -1
- package/build-module/mobile/keyboard-aware-flat-list/index.android.js +0 -4
- package/build-module/mobile/keyboard-aware-flat-list/index.android.js.map +1 -1
- package/build-module/mobile/keyboard-aware-flat-list/index.ios.js +97 -54
- package/build-module/mobile/keyboard-aware-flat-list/index.ios.js.map +1 -1
- package/build-module/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js +73 -0
- package/build-module/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js.map +1 -0
- package/build-module/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js +76 -0
- package/build-module/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js.map +1 -0
- package/build-module/mobile/keyboard-aware-flat-list/use-text-input-caret-position.native.js +33 -0
- package/build-module/mobile/keyboard-aware-flat-list/use-text-input-caret-position.native.js.map +1 -0
- package/build-module/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js +40 -0
- package/build-module/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js.map +1 -0
- package/build-module/navigator/navigator-provider/component.js +4 -2
- package/build-module/navigator/navigator-provider/component.js.map +1 -1
- package/build-module/navigator/navigator-screen/component.js +4 -3
- package/build-module/navigator/navigator-screen/component.js.map +1 -1
- package/build-module/private-apis.js.map +1 -1
- package/build-module/query-controls/author-select.js +2 -1
- package/build-module/query-controls/author-select.js.map +1 -1
- package/build-module/query-controls/category-select.js +3 -1
- package/build-module/query-controls/category-select.js.map +1 -1
- package/build-module/query-controls/index.js +7 -2
- package/build-module/query-controls/index.js.map +1 -1
- package/build-module/sandbox/index.native.js +52 -30
- package/build-module/sandbox/index.native.js.map +1 -1
- package/build-style/style-rtl.css +1 -1
- package/build-style/style.css +1 -1
- package/build-types/angle-picker-control/styles/angle-picker-control-styles.d.ts +1 -1
- package/build-types/border-box-control/border-box-control/hook.d.ts +2 -2
- package/build-types/border-box-control/border-box-control-linked-button/hook.d.ts +2 -2
- package/build-types/border-box-control/border-box-control-split-controls/hook.d.ts +2 -2
- package/build-types/border-box-control/border-box-control-visualizer/hook.d.ts +2 -2
- package/build-types/border-control/border-control/hook.d.ts +2 -2
- package/build-types/border-control/border-control-dropdown/hook.d.ts +2 -2
- package/build-types/border-control/border-control-style-picker/hook.d.ts +2 -2
- package/build-types/box-control/styles/box-control-styles.d.ts +5 -5
- package/build-types/button/deprecated.d.ts +2 -2
- package/build-types/card/card/hook.d.ts +2 -2
- package/build-types/card/card-body/hook.d.ts +2 -2
- package/build-types/card/card-divider/hook.d.ts +2 -2
- package/build-types/card/card-footer/hook.d.ts +2 -2
- package/build-types/card/card-header/hook.d.ts +2 -2
- package/build-types/card/card-media/hook.d.ts +2 -2
- package/build-types/color-palette/styles.d.ts +1 -1
- package/build-types/color-picker/styles.d.ts +5 -5
- package/build-types/combobox-control/styles.d.ts +1 -1
- package/build-types/custom-gradient-picker/serializer.d.ts +1 -5
- package/build-types/custom-gradient-picker/serializer.d.ts.map +1 -1
- package/build-types/custom-gradient-picker/types.d.ts +0 -2
- package/build-types/custom-gradient-picker/types.d.ts.map +1 -1
- package/build-types/date-time/date/styles.d.ts +2 -2
- package/build-types/date-time/date-time/styles.d.ts +1 -1
- package/build-types/date-time/time/styles.d.ts +8 -8
- package/build-types/drop-zone/index.d.ts.map +1 -1
- package/build-types/elevation/hook.d.ts +2 -2
- package/build-types/external-link/styles/external-link-styles.d.ts +1 -1
- package/build-types/flex/flex/hook.d.ts +2 -2
- package/build-types/flex/flex-block/hook.d.ts +2 -2
- package/build-types/flex/flex-item/hook.d.ts +2 -2
- package/build-types/focal-point-picker/styles/focal-point-picker-style.d.ts +2 -2
- package/build-types/form-token-field/styles.d.ts +1 -1
- package/build-types/grid/hook.d.ts +2 -2
- package/build-types/h-stack/hook.d.ts +2 -2
- package/build-types/heading/hook.d.ts +2 -2
- package/build-types/index.d.ts +128 -0
- package/build-types/index.d.ts.map +1 -0
- package/build-types/input-control/styles/input-control-styles.d.ts +2 -2
- package/build-types/item-group/item/hook.d.ts +2 -2
- package/build-types/item-group/item-group/hook.d.ts +2 -2
- package/build-types/navigation/styles/navigation-styles.d.ts +2 -2
- package/build-types/navigator/navigator-back-button/hook.d.ts +2 -2
- package/build-types/navigator/navigator-button/hook.d.ts +2 -2
- package/build-types/navigator/navigator-provider/component.d.ts.map +1 -1
- package/build-types/navigator/navigator-screen/component.d.ts +1 -1
- package/build-types/navigator/navigator-screen/component.d.ts.map +1 -1
- package/build-types/navigator/stories/index.d.ts +1 -0
- package/build-types/navigator/stories/index.d.ts.map +1 -1
- package/build-types/navigator/types.d.ts +2 -2
- package/build-types/navigator/types.d.ts.map +1 -1
- package/build-types/number-control/index.d.ts +2 -2
- package/build-types/number-control/stories/index.d.ts +2 -2
- package/build-types/palette-edit/styles.d.ts +3 -3
- package/build-types/popover/index.d.ts +1 -1
- package/build-types/popover/stories/e2e/index.d.ts +1 -1
- package/build-types/private-apis.d.ts +2 -3
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/query-controls/author-select.d.ts.map +1 -1
- package/build-types/query-controls/category-select.d.ts.map +1 -1
- package/build-types/query-controls/index.d.ts.map +1 -1
- package/build-types/range-control/index.d.ts +1 -1
- package/build-types/range-control/styles/range-control-styles.d.ts +2 -2
- package/build-types/resizable-box/index.d.ts +1 -1
- package/build-types/resizable-box/resize-tooltip/index.d.ts +1 -1
- package/build-types/resizable-box/stories/index.d.ts +2 -2
- package/build-types/scrollable/hook.d.ts +2 -2
- package/build-types/search-control/index.d.ts +1 -1
- package/build-types/search-control/stories/index.d.ts +2 -2
- package/build-types/spacer/hook.d.ts +2 -2
- package/build-types/spinner/index.d.ts +1 -1
- package/build-types/surface/hook.d.ts +2 -2
- package/build-types/text/hook.d.ts +2 -2
- package/build-types/text-control/index.d.ts +1 -1
- package/build-types/toolbar/toolbar-button/index.d.ts +2 -2
- package/build-types/tools-panel/tools-panel/hook.d.ts +2 -2
- package/build-types/tools-panel/tools-panel-header/hook.d.ts +2 -2
- package/build-types/tools-panel/tools-panel-item/hook.d.ts +2 -2
- package/build-types/truncate/hook.d.ts +2 -2
- package/build-types/ui/control-group/hook.d.ts +2 -2
- package/build-types/ui/control-label/hook.d.ts +2 -2
- package/build-types/ui/form-group/form-group.d.ts +2 -2
- package/build-types/ui/form-group/use-form-group.d.ts +2 -2
- package/build-types/unit-control/index.d.ts +1 -1
- package/build-types/unit-control/styles/unit-control-styles.d.ts +2 -2
- package/build-types/v-stack/hook.d.ts +2 -2
- package/package.json +20 -19
- package/src/custom-gradient-picker/serializer.ts +2 -6
- package/src/custom-gradient-picker/types.ts +0 -18
- package/src/drop-zone/index.tsx +12 -8
- package/src/drop-zone/style.scss +1 -1
- package/src/mobile/keyboard-aware-flat-list/index.android.js +0 -4
- package/src/mobile/keyboard-aware-flat-list/index.ios.js +118 -67
- package/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js +203 -0
- package/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js +140 -0
- package/src/mobile/keyboard-aware-flat-list/test/use-text-input-caret-position.native.js +82 -0
- package/src/mobile/keyboard-aware-flat-list/test/use-text-input-offset.native.js +147 -0
- package/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js +87 -0
- package/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js +105 -0
- package/src/mobile/keyboard-aware-flat-list/use-text-input-caret-position.native.js +36 -0
- package/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js +54 -0
- package/src/navigator/navigator-provider/component.tsx +2 -0
- package/src/navigator/navigator-screen/component.tsx +5 -2
- package/src/navigator/stories/index.tsx +68 -0
- package/src/navigator/test/index.tsx +52 -0
- package/src/navigator/types.ts +2 -1
- package/src/query-controls/author-select.tsx +1 -0
- package/src/query-controls/category-select.tsx +1 -0
- package/src/query-controls/index.tsx +4 -2
- package/src/sandbox/index.native.js +70 -36
- package/tsconfig.json +1 -2
- package/tsconfig.tsbuildinfo +1 -1
- /package/src/{index.js → index.ts} +0 -0
- /package/src/{private-apis.js → private-apis.ts} +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { renderHook } from '@testing-library/react-native';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* WordPress dependencies
|
|
8
|
+
*/
|
|
9
|
+
import RCTAztecView from '@wordpress/react-native-aztec';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal dependencies
|
|
13
|
+
*/
|
|
14
|
+
import useTextInputOffset from '../use-text-input-offset';
|
|
15
|
+
|
|
16
|
+
jest.mock( '@wordpress/react-native-aztec', () => ( {
|
|
17
|
+
InputState: {
|
|
18
|
+
getCurrentFocusedElement: jest.fn(),
|
|
19
|
+
},
|
|
20
|
+
} ) );
|
|
21
|
+
|
|
22
|
+
describe( 'useTextInputOffset', () => {
|
|
23
|
+
afterEach( () => {
|
|
24
|
+
jest.clearAllMocks();
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
it( 'should return a function', () => {
|
|
28
|
+
// Arrange
|
|
29
|
+
const scrollViewRef = { current: {} };
|
|
30
|
+
const scrollEnabled = true;
|
|
31
|
+
|
|
32
|
+
// Act
|
|
33
|
+
const { result } = renderHook( () =>
|
|
34
|
+
useTextInputOffset( scrollEnabled, scrollViewRef )
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Assert
|
|
38
|
+
expect( result.current[ 0 ] ).toBeInstanceOf( Function );
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
it( 'should return null when scrollViewRef.current is null', async () => {
|
|
42
|
+
// Arrange
|
|
43
|
+
const scrollViewRef = { current: null };
|
|
44
|
+
const scrollEnabled = true;
|
|
45
|
+
|
|
46
|
+
// Act
|
|
47
|
+
const { result } = renderHook( () =>
|
|
48
|
+
useTextInputOffset( scrollEnabled, scrollViewRef )
|
|
49
|
+
);
|
|
50
|
+
const getTextInputOffset = result.current[ 0 ];
|
|
51
|
+
|
|
52
|
+
// Assert
|
|
53
|
+
const offset = await getTextInputOffset();
|
|
54
|
+
expect( offset ).toBeNull();
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
it( 'should return null when textInput is null', async () => {
|
|
58
|
+
// Arrange
|
|
59
|
+
const scrollViewRef = { current: {} };
|
|
60
|
+
const scrollEnabled = true;
|
|
61
|
+
RCTAztecView.InputState.getCurrentFocusedElement.mockReturnValue(
|
|
62
|
+
null
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Act
|
|
66
|
+
const { result } = renderHook( () =>
|
|
67
|
+
useTextInputOffset( scrollEnabled, scrollViewRef )
|
|
68
|
+
);
|
|
69
|
+
const getTextInputOffset = result.current[ 0 ];
|
|
70
|
+
|
|
71
|
+
// Assert
|
|
72
|
+
const offset = await getTextInputOffset();
|
|
73
|
+
expect( offset ).toBeNull();
|
|
74
|
+
} );
|
|
75
|
+
|
|
76
|
+
it( 'should return null when scroll is not enabled', async () => {
|
|
77
|
+
// Arrange
|
|
78
|
+
const scrollViewRef = { current: {} };
|
|
79
|
+
const scrollEnabled = false;
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
const { result } = renderHook( () =>
|
|
83
|
+
useTextInputOffset( scrollEnabled, scrollViewRef )
|
|
84
|
+
);
|
|
85
|
+
const getTextInputOffset = result.current[ 0 ];
|
|
86
|
+
|
|
87
|
+
// Assert
|
|
88
|
+
const offset = await getTextInputOffset();
|
|
89
|
+
expect( offset ).toBeNull();
|
|
90
|
+
} );
|
|
91
|
+
|
|
92
|
+
it( 'should return correct offset value when caretY is not null', async () => {
|
|
93
|
+
// Arrange
|
|
94
|
+
const scrollViewRef = { current: {} };
|
|
95
|
+
const scrollEnabled = true;
|
|
96
|
+
const x = 0;
|
|
97
|
+
const y = 10;
|
|
98
|
+
const width = 0;
|
|
99
|
+
const height = 100;
|
|
100
|
+
const textInput = {
|
|
101
|
+
measureLayout: jest.fn( ( _, callback ) => {
|
|
102
|
+
callback( x, y, width, height );
|
|
103
|
+
} ),
|
|
104
|
+
};
|
|
105
|
+
RCTAztecView.InputState.getCurrentFocusedElement.mockReturnValue(
|
|
106
|
+
textInput
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Act
|
|
110
|
+
const { result } = renderHook( () =>
|
|
111
|
+
useTextInputOffset( scrollEnabled, scrollViewRef )
|
|
112
|
+
);
|
|
113
|
+
const getTextInputOffset = result.current[ 0 ];
|
|
114
|
+
|
|
115
|
+
// Assert
|
|
116
|
+
const offset = await getTextInputOffset( { caretY: 10 } );
|
|
117
|
+
expect( offset ).toBe( 20 );
|
|
118
|
+
} );
|
|
119
|
+
|
|
120
|
+
it( 'should return correct offset value when caretY is -1', async () => {
|
|
121
|
+
// Arrange
|
|
122
|
+
const scrollViewRef = { current: {} };
|
|
123
|
+
const scrollEnabled = true;
|
|
124
|
+
const x = 0;
|
|
125
|
+
const y = 10;
|
|
126
|
+
const width = 0;
|
|
127
|
+
const height = 100;
|
|
128
|
+
const textInput = {
|
|
129
|
+
measureLayout: jest.fn( ( _, callback ) => {
|
|
130
|
+
callback( x, y, width, height );
|
|
131
|
+
} ),
|
|
132
|
+
};
|
|
133
|
+
RCTAztecView.InputState.getCurrentFocusedElement.mockReturnValue(
|
|
134
|
+
textInput
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Act
|
|
138
|
+
const { result } = renderHook( () =>
|
|
139
|
+
useTextInputOffset( scrollEnabled, scrollViewRef )
|
|
140
|
+
);
|
|
141
|
+
const getTextInputOffset = result.current[ 0 ];
|
|
142
|
+
|
|
143
|
+
// Assert
|
|
144
|
+
const offset = await getTextInputOffset( { caretY: -1 } );
|
|
145
|
+
expect( offset ).toBe( 110 );
|
|
146
|
+
} );
|
|
147
|
+
} );
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Keyboard } from 'react-native';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* WordPress dependencies
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useCallback, useState, useRef } from '@wordpress/element';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hook that adds Keyboard listeners to get the offset space
|
|
14
|
+
* when the keyboard is opened, taking into account focused AztecViews.
|
|
15
|
+
*
|
|
16
|
+
* @param {boolean} scrollEnabled Whether the scroll is enabled or not.
|
|
17
|
+
* @param {Function} shouldPreventAutomaticScroll Whether to prevent scrolling when there's a Keyboard offset set.
|
|
18
|
+
* @return {[number]} Keyboard offset.
|
|
19
|
+
*/
|
|
20
|
+
export default function useKeyboardOffset(
|
|
21
|
+
scrollEnabled,
|
|
22
|
+
shouldPreventAutomaticScroll
|
|
23
|
+
) {
|
|
24
|
+
const [ keyboardOffset, setKeyboardOffset ] = useState( 0 );
|
|
25
|
+
const timeoutRef = useRef();
|
|
26
|
+
|
|
27
|
+
const onKeyboardDidHide = useCallback( () => {
|
|
28
|
+
if ( shouldPreventAutomaticScroll() ) {
|
|
29
|
+
clearTimeout( timeoutRef.current );
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A timeout is being used to delay resetting the offset in cases
|
|
34
|
+
// where the focus is changed to a different TextInput.
|
|
35
|
+
clearTimeout( timeoutRef.current );
|
|
36
|
+
timeoutRef.current = setTimeout( () => {
|
|
37
|
+
setKeyboardOffset( 0 );
|
|
38
|
+
}, 200 );
|
|
39
|
+
}, [ shouldPreventAutomaticScroll ] );
|
|
40
|
+
|
|
41
|
+
const onKeyboardDidShow = useCallback( ( { endCoordinates } ) => {
|
|
42
|
+
clearTimeout( timeoutRef.current );
|
|
43
|
+
setKeyboardOffset( endCoordinates.height );
|
|
44
|
+
}, [] );
|
|
45
|
+
|
|
46
|
+
const onKeyboardWillShow = useCallback( () => {
|
|
47
|
+
clearTimeout( timeoutRef.current );
|
|
48
|
+
}, [] );
|
|
49
|
+
|
|
50
|
+
useEffect( () => {
|
|
51
|
+
let willShowSubscription;
|
|
52
|
+
let showSubscription;
|
|
53
|
+
let hideSubscription;
|
|
54
|
+
|
|
55
|
+
if ( scrollEnabled ) {
|
|
56
|
+
willShowSubscription = Keyboard.addListener(
|
|
57
|
+
'keyboardWillShow',
|
|
58
|
+
onKeyboardWillShow
|
|
59
|
+
);
|
|
60
|
+
showSubscription = Keyboard.addListener(
|
|
61
|
+
'keyboardDidShow',
|
|
62
|
+
onKeyboardDidShow
|
|
63
|
+
);
|
|
64
|
+
hideSubscription = Keyboard.addListener(
|
|
65
|
+
'keyboardDidHide',
|
|
66
|
+
onKeyboardDidHide
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
willShowSubscription?.remove();
|
|
70
|
+
showSubscription?.remove();
|
|
71
|
+
hideSubscription?.remove();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
clearTimeout( timeoutRef.current );
|
|
76
|
+
willShowSubscription?.remove();
|
|
77
|
+
showSubscription?.remove();
|
|
78
|
+
hideSubscription?.remove();
|
|
79
|
+
};
|
|
80
|
+
}, [
|
|
81
|
+
onKeyboardDidHide,
|
|
82
|
+
onKeyboardDidShow,
|
|
83
|
+
onKeyboardWillShow,
|
|
84
|
+
scrollEnabled,
|
|
85
|
+
] );
|
|
86
|
+
return [ keyboardOffset ];
|
|
87
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* WordPress dependencies
|
|
8
|
+
*/
|
|
9
|
+
import { useCallback } from '@wordpress/element';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_FONT_SIZE = 16;
|
|
12
|
+
|
|
13
|
+
/** @typedef {import('@wordpress/element').RefObject} RefObject */
|
|
14
|
+
/** @typedef {import('react-native-reanimated').SharedValue} SharedValue */
|
|
15
|
+
/**
|
|
16
|
+
* Hook to scroll to the currently focused TextInput
|
|
17
|
+
* depending on where the caret is placed taking into
|
|
18
|
+
* account the Keyboard and the Header.
|
|
19
|
+
*
|
|
20
|
+
* @param {number} extraScrollHeight Extra space to not overlap the content.
|
|
21
|
+
* @param {number} keyboardOffset Keyboard space offset.
|
|
22
|
+
* @param {boolean} scrollEnabled Whether the scroll is enabled or not.
|
|
23
|
+
* @param {RefObject} scrollViewMeasurements ScrollView Layout measurements.
|
|
24
|
+
* @param {RefObject} scrollViewRef ScrollView reference.
|
|
25
|
+
* @param {SharedValue} scrollViewYOffset Current offset position of the ScrollView.
|
|
26
|
+
* @return {Function[]} Function to scroll to the current TextInput's offset.
|
|
27
|
+
*/
|
|
28
|
+
export default function useScrollToTextInput(
|
|
29
|
+
extraScrollHeight,
|
|
30
|
+
keyboardOffset,
|
|
31
|
+
scrollEnabled,
|
|
32
|
+
scrollViewMeasurements,
|
|
33
|
+
scrollViewRef,
|
|
34
|
+
scrollViewYOffset
|
|
35
|
+
) {
|
|
36
|
+
const { top, bottom } = useSafeAreaInsets();
|
|
37
|
+
const insets = top + bottom;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Function to scroll to the current TextInput's offset.
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} caret The caret position data of the currently focused TextInput.
|
|
43
|
+
* @param {number} caret.caretHeight The height of the caret.
|
|
44
|
+
* @param {number} textInputOffset The offset calculated with the caret's Y coordinate + the
|
|
45
|
+
* TextInput's Y coord or height value.
|
|
46
|
+
*/
|
|
47
|
+
const scrollToTextInputOffset = useCallback(
|
|
48
|
+
( caret, textInputOffset ) => {
|
|
49
|
+
const { caretHeight = DEFAULT_FONT_SIZE } = caret ?? {};
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
! scrollViewRef.current ||
|
|
53
|
+
! scrollEnabled ||
|
|
54
|
+
! scrollViewMeasurements.current
|
|
55
|
+
) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const currentScrollViewYOffset = Math.max(
|
|
59
|
+
0,
|
|
60
|
+
scrollViewYOffset.value
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Scroll up.
|
|
64
|
+
if ( textInputOffset < currentScrollViewYOffset ) {
|
|
65
|
+
scrollViewRef.current.scrollTo( {
|
|
66
|
+
y: textInputOffset,
|
|
67
|
+
animated: true,
|
|
68
|
+
} );
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const availableScreenSpace = Math.abs(
|
|
73
|
+
Math.floor(
|
|
74
|
+
scrollViewMeasurements.current.height -
|
|
75
|
+
( keyboardOffset + extraScrollHeight + caretHeight )
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
const maxOffset = Math.floor(
|
|
79
|
+
currentScrollViewYOffset + availableScreenSpace
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const isAtTheTop =
|
|
83
|
+
textInputOffset < scrollViewMeasurements.current.y + insets;
|
|
84
|
+
|
|
85
|
+
// Scroll down.
|
|
86
|
+
if ( textInputOffset > maxOffset && ! isAtTheTop ) {
|
|
87
|
+
scrollViewRef.current.scrollTo( {
|
|
88
|
+
y: textInputOffset - availableScreenSpace,
|
|
89
|
+
animated: true,
|
|
90
|
+
} );
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[
|
|
94
|
+
extraScrollHeight,
|
|
95
|
+
insets,
|
|
96
|
+
keyboardOffset,
|
|
97
|
+
scrollEnabled,
|
|
98
|
+
scrollViewMeasurements,
|
|
99
|
+
scrollViewRef,
|
|
100
|
+
scrollViewYOffset,
|
|
101
|
+
]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return [ scrollToTextInputOffset ];
|
|
105
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import RCTAztecView from '@wordpress/react-native-aztec';
|
|
5
|
+
import { useCallback, useEffect, useState } from '@wordpress/element';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook that listens to caret changes from AztecView TextInputs.
|
|
9
|
+
*
|
|
10
|
+
* @param {boolean} scrollEnabled Whether the scroll is enabled or not.
|
|
11
|
+
* @return {[number]} Current caret's data.
|
|
12
|
+
*/
|
|
13
|
+
export default function useTextInputCaretPosition( scrollEnabled ) {
|
|
14
|
+
const [ currentCaretData, setCurrentCaretData ] = useState();
|
|
15
|
+
|
|
16
|
+
const onCaretChange = useCallback( ( caret ) => {
|
|
17
|
+
setCurrentCaretData( caret );
|
|
18
|
+
}, [] );
|
|
19
|
+
|
|
20
|
+
useEffect( () => {
|
|
21
|
+
if ( scrollEnabled ) {
|
|
22
|
+
RCTAztecView.InputState.addCaretChangeListener( onCaretChange );
|
|
23
|
+
} else {
|
|
24
|
+
RCTAztecView.InputState.removeCaretChangeListener( onCaretChange );
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
if ( scrollEnabled ) {
|
|
29
|
+
RCTAztecView.InputState.removeCaretChangeListener(
|
|
30
|
+
onCaretChange
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}, [ scrollEnabled, onCaretChange ] );
|
|
35
|
+
return [ currentCaretData ];
|
|
36
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import RCTAztecView from '@wordpress/react-native-aztec';
|
|
5
|
+
import { useCallback } from '@wordpress/element';
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('@wordpress/element').RefObject} RefObject */
|
|
8
|
+
/**
|
|
9
|
+
* Hook that calculates the currently focused TextInput's current
|
|
10
|
+
* caret Y coordinate position.
|
|
11
|
+
*
|
|
12
|
+
* @param {boolean} scrollEnabled Whether the scroll is enabled or not.
|
|
13
|
+
* @param {RefObject} scrollViewRef ScrollView reference.
|
|
14
|
+
* @return {[Function]} Function to get the current TextInput's offset.
|
|
15
|
+
*/
|
|
16
|
+
export default function useTextInputOffset( scrollEnabled, scrollViewRef ) {
|
|
17
|
+
const getTextInputOffset = useCallback(
|
|
18
|
+
async ( caret ) => {
|
|
19
|
+
const { caretY = null } = caret ?? {};
|
|
20
|
+
const textInput =
|
|
21
|
+
RCTAztecView.InputState.getCurrentFocusedElement();
|
|
22
|
+
|
|
23
|
+
return new Promise( ( resolve ) => {
|
|
24
|
+
if (
|
|
25
|
+
scrollViewRef.current &&
|
|
26
|
+
textInput &&
|
|
27
|
+
scrollEnabled &&
|
|
28
|
+
caretY !== null
|
|
29
|
+
) {
|
|
30
|
+
textInput.measureLayout(
|
|
31
|
+
scrollViewRef.current,
|
|
32
|
+
( _x, y, _width, height ) => {
|
|
33
|
+
const caretYOffset =
|
|
34
|
+
// For cases where the caretY value is -1
|
|
35
|
+
// we use the y + height value, e.g the current
|
|
36
|
+
// character index is not valid or out of bounds
|
|
37
|
+
// see https://github.com/wordpress-mobile/AztecEditor-iOS/blob/4d0522d67b0056ac211466caaa76936cc5b4f947/Aztec/Classes/TextKit/TextView.swift#L762
|
|
38
|
+
caretY >= 0 && caretY < height
|
|
39
|
+
? y + caretY
|
|
40
|
+
: y + height;
|
|
41
|
+
resolve( Math.round( Math.abs( caretYOffset ) ) );
|
|
42
|
+
},
|
|
43
|
+
() => resolve( null )
|
|
44
|
+
);
|
|
45
|
+
} else {
|
|
46
|
+
resolve( null );
|
|
47
|
+
}
|
|
48
|
+
} );
|
|
49
|
+
},
|
|
50
|
+
[ scrollEnabled, scrollViewRef ]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return [ getTextInputOffset ];
|
|
54
|
+
}
|
|
@@ -147,6 +147,7 @@ function UnconnectedNavigatorProvider(
|
|
|
147
147
|
const {
|
|
148
148
|
focusTargetSelector,
|
|
149
149
|
isBack = false,
|
|
150
|
+
skipFocus = false,
|
|
150
151
|
...restOptions
|
|
151
152
|
} = options;
|
|
152
153
|
|
|
@@ -168,6 +169,7 @@ function UnconnectedNavigatorProvider(
|
|
|
168
169
|
path,
|
|
169
170
|
isBack,
|
|
170
171
|
hasRestoredFocus: false,
|
|
172
|
+
skipFocus,
|
|
171
173
|
};
|
|
172
174
|
|
|
173
175
|
if ( prevLocationHistory.length < 1 ) {
|
|
@@ -43,7 +43,7 @@ const animationExitDelay = 0;
|
|
|
43
43
|
// as some of them would overlap with HTML props (e.g. `onAnimationStart`, ...)
|
|
44
44
|
type Props = Omit<
|
|
45
45
|
WordPressComponentProps< NavigatorScreenProps, 'div', false >,
|
|
46
|
-
keyof MotionProps
|
|
46
|
+
Exclude< keyof MotionProps, 'style' >
|
|
47
47
|
>;
|
|
48
48
|
|
|
49
49
|
function UnconnectedNavigatorScreen(
|
|
@@ -100,11 +100,13 @@ function UnconnectedNavigatorScreen(
|
|
|
100
100
|
// - when the screen becomes visible
|
|
101
101
|
// - if the wrapper ref has been assigned
|
|
102
102
|
// - if focus hasn't already been restored for the current location
|
|
103
|
+
// - if the `skipFocus` option is not set to `true`. This is useful when we trigger the navigation outside of NavigatorScreen.
|
|
103
104
|
if (
|
|
104
105
|
isInitialLocation ||
|
|
105
106
|
! isMatch ||
|
|
106
107
|
! wrapperRef.current ||
|
|
107
|
-
locationRef.current.hasRestoredFocus
|
|
108
|
+
locationRef.current.hasRestoredFocus ||
|
|
109
|
+
location.skipFocus
|
|
108
110
|
) {
|
|
109
111
|
return;
|
|
110
112
|
}
|
|
@@ -143,6 +145,7 @@ function UnconnectedNavigatorScreen(
|
|
|
143
145
|
isMatch,
|
|
144
146
|
location.isBack,
|
|
145
147
|
location.focusTargetSelector,
|
|
148
|
+
location.skipFocus,
|
|
146
149
|
] );
|
|
147
150
|
|
|
148
151
|
const mergedWrapperRef = useMergeRefs( [ forwardedRef, wrapperRef ] );
|
|
@@ -298,3 +298,71 @@ export const NestedNavigator: ComponentStory< typeof NavigatorProvider > =
|
|
|
298
298
|
NestedNavigator.args = {
|
|
299
299
|
initialPath: '/child2/grandchild',
|
|
300
300
|
};
|
|
301
|
+
|
|
302
|
+
const NavigatorButtonWithSkipFocus = ( {
|
|
303
|
+
path,
|
|
304
|
+
onClick,
|
|
305
|
+
...props
|
|
306
|
+
}: React.ComponentProps< typeof NavigatorButton > ) => {
|
|
307
|
+
const { goTo } = useNavigator();
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<Button
|
|
311
|
+
{ ...props }
|
|
312
|
+
onClick={ ( e: React.MouseEvent< HTMLButtonElement > ) => {
|
|
313
|
+
goTo( path, { skipFocus: true } );
|
|
314
|
+
onClick?.( e );
|
|
315
|
+
} }
|
|
316
|
+
/>
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export const SkipFocus: ComponentStory< typeof NavigatorProvider > = (
|
|
321
|
+
args
|
|
322
|
+
) => {
|
|
323
|
+
return <NavigatorProvider { ...args } />;
|
|
324
|
+
};
|
|
325
|
+
SkipFocus.args = {
|
|
326
|
+
initialPath: '/',
|
|
327
|
+
children: (
|
|
328
|
+
<>
|
|
329
|
+
<div
|
|
330
|
+
style={ {
|
|
331
|
+
height: 250,
|
|
332
|
+
border: '1px solid black',
|
|
333
|
+
} }
|
|
334
|
+
>
|
|
335
|
+
<NavigatorScreen
|
|
336
|
+
path="/"
|
|
337
|
+
style={ {
|
|
338
|
+
height: '100%',
|
|
339
|
+
} }
|
|
340
|
+
>
|
|
341
|
+
<h1>Home screen</h1>
|
|
342
|
+
<NavigatorButton variant="secondary" path="/child">
|
|
343
|
+
Go to child screen.
|
|
344
|
+
</NavigatorButton>
|
|
345
|
+
</NavigatorScreen>
|
|
346
|
+
<NavigatorScreen
|
|
347
|
+
path="/child"
|
|
348
|
+
style={ {
|
|
349
|
+
height: '100%',
|
|
350
|
+
} }
|
|
351
|
+
>
|
|
352
|
+
<h2>Child screen</h2>
|
|
353
|
+
<NavigatorToParentButton variant="secondary">
|
|
354
|
+
Go to parent screen.
|
|
355
|
+
</NavigatorToParentButton>
|
|
356
|
+
</NavigatorScreen>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<NavigatorButtonWithSkipFocus
|
|
360
|
+
variant="secondary"
|
|
361
|
+
path="/child"
|
|
362
|
+
style={ { margin: '1rem 2rem' } }
|
|
363
|
+
>
|
|
364
|
+
Go to child screen, but keep focus on this button
|
|
365
|
+
</NavigatorButtonWithSkipFocus>
|
|
366
|
+
</>
|
|
367
|
+
),
|
|
368
|
+
};
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
NavigatorToParentButton,
|
|
23
23
|
useNavigator,
|
|
24
24
|
} from '..';
|
|
25
|
+
import type { NavigateOptions } from '../types';
|
|
25
26
|
|
|
26
27
|
const INVALID_HTML_ATTRIBUTE = {
|
|
27
28
|
raw: ' "\'><=invalid_path',
|
|
@@ -57,6 +58,7 @@ const BUTTON_TEXT = {
|
|
|
57
58
|
'Navigate to screen with an invalid HTML value as a path.',
|
|
58
59
|
back: 'Go back',
|
|
59
60
|
backUsingGoTo: 'Go back using goTo',
|
|
61
|
+
goToWithSkipFocus: 'Go to with skipFocus',
|
|
60
62
|
};
|
|
61
63
|
|
|
62
64
|
type CustomTestOnClickHandler = (
|
|
@@ -64,6 +66,7 @@ type CustomTestOnClickHandler = (
|
|
|
64
66
|
| {
|
|
65
67
|
type: 'goTo';
|
|
66
68
|
path: string;
|
|
69
|
+
options?: NavigateOptions;
|
|
67
70
|
}
|
|
68
71
|
| { type: 'goBack' }
|
|
69
72
|
| { type: 'goToParent' }
|
|
@@ -108,6 +111,26 @@ function CustomNavigatorGoToBackButton( {
|
|
|
108
111
|
);
|
|
109
112
|
}
|
|
110
113
|
|
|
114
|
+
function CustomNavigatorGoToSkipFocusButton( {
|
|
115
|
+
path,
|
|
116
|
+
onClick,
|
|
117
|
+
...props
|
|
118
|
+
}: Omit< ComponentPropsWithoutRef< typeof NavigatorButton >, 'onClick' > & {
|
|
119
|
+
onClick?: CustomTestOnClickHandler;
|
|
120
|
+
} ) {
|
|
121
|
+
const { goTo } = useNavigator();
|
|
122
|
+
return (
|
|
123
|
+
<Button
|
|
124
|
+
onClick={ () => {
|
|
125
|
+
goTo( path, { skipFocus: true } );
|
|
126
|
+
// Used to spy on the values passed to `navigator.goTo`.
|
|
127
|
+
onClick?.( { type: 'goTo', path } );
|
|
128
|
+
} }
|
|
129
|
+
{ ...props }
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
111
134
|
function CustomNavigatorBackButton( {
|
|
112
135
|
onClick,
|
|
113
136
|
...props
|
|
@@ -342,6 +365,12 @@ const MyHierarchicalNavigation = ( {
|
|
|
342
365
|
{ BUTTON_TEXT.backUsingGoTo }
|
|
343
366
|
</CustomNavigatorGoToBackButton>
|
|
344
367
|
</NavigatorScreen>
|
|
368
|
+
<CustomNavigatorGoToSkipFocusButton
|
|
369
|
+
path={ PATHS.NESTED }
|
|
370
|
+
onClick={ onNavigatorButtonClick }
|
|
371
|
+
>
|
|
372
|
+
{ BUTTON_TEXT.goToWithSkipFocus }
|
|
373
|
+
</CustomNavigatorGoToSkipFocusButton>
|
|
345
374
|
</NavigatorProvider>
|
|
346
375
|
</>
|
|
347
376
|
);
|
|
@@ -716,6 +745,29 @@ describe( 'Navigator', () => {
|
|
|
716
745
|
await user.click( getNavigationButton( 'back' ) );
|
|
717
746
|
expect( getNavigationButton( 'toChildScreen' ) ).toHaveFocus();
|
|
718
747
|
} );
|
|
748
|
+
|
|
749
|
+
it( 'should skip focus based on location `skipFocus` option', async () => {
|
|
750
|
+
const user = userEvent.setup();
|
|
751
|
+
render( <MyHierarchicalNavigation /> );
|
|
752
|
+
|
|
753
|
+
// Navigate to child screen with skipFocus.
|
|
754
|
+
await user.click( getNavigationButton( 'goToWithSkipFocus' ) );
|
|
755
|
+
expect( queryScreen( 'home' ) ).not.toBeInTheDocument();
|
|
756
|
+
expect( getScreen( 'nested' ) ).toBeInTheDocument();
|
|
757
|
+
|
|
758
|
+
// The clicked button should remain focused.
|
|
759
|
+
expect( getNavigationButton( 'goToWithSkipFocus' ) ).toHaveFocus();
|
|
760
|
+
|
|
761
|
+
// Navigate back to parent screen.
|
|
762
|
+
await user.click( getNavigationButton( 'back' ) );
|
|
763
|
+
expect( getScreen( 'child' ) ).toBeInTheDocument();
|
|
764
|
+
// The first tabbable element receives focus.
|
|
765
|
+
expect(
|
|
766
|
+
screen.getByRole( 'button', {
|
|
767
|
+
name: 'First tabbable child screen button',
|
|
768
|
+
} )
|
|
769
|
+
).toHaveFocus();
|
|
770
|
+
} );
|
|
719
771
|
} );
|
|
720
772
|
|
|
721
773
|
describe( 'animation', () => {
|
package/src/navigator/types.ts
CHANGED
|
@@ -10,9 +10,10 @@ import type { ButtonAsButtonProps } from '../button/types';
|
|
|
10
10
|
|
|
11
11
|
export type MatchParams = Record< string, string | string[] >;
|
|
12
12
|
|
|
13
|
-
type NavigateOptions = {
|
|
13
|
+
export type NavigateOptions = {
|
|
14
14
|
focusTargetSelector?: string;
|
|
15
15
|
isBack?: boolean;
|
|
16
|
+
skipFocus?: boolean;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
export type NavigatorLocation = NavigateOptions & {
|