react-native-terra-ui 0.13.7 → 0.13.9
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 +18 -0
- package/lib/module/components/header/parts/HeaderBar.js +3 -1
- package/lib/module/components/header/parts/HeaderBar.js.map +1 -1
- package/lib/module/components/header/parts/HeaderGradientOverlay.js +1 -2
- package/lib/module/components/header/parts/HeaderGradientOverlay.js.map +1 -1
- package/lib/module/components/header/parts/HeaderSurface.js +9 -4
- package/lib/module/components/header/parts/HeaderSurface.js.map +1 -1
- package/lib/module/components/header/variants/LargeTitleHeader/index.js +1 -0
- package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
- package/lib/module/components/header/variants/TitleHeader/index.js +1 -0
- package/lib/module/components/header/variants/TitleHeader/index.js.map +1 -1
- package/lib/module/components/input/hooks/use-focus-state.js +25 -0
- package/lib/module/components/input/hooks/use-focus-state.js.map +1 -0
- package/lib/module/components/input/parts/InputField.js +53 -31
- package/lib/module/components/input/parts/InputField.js.map +1 -1
- package/lib/module/components/input/variants/SearchInput/SearchCancelButton.js +93 -0
- package/lib/module/components/input/variants/SearchInput/SearchCancelButton.js.map +1 -0
- package/lib/module/components/input/variants/SearchInput/SearchClearButton.js +52 -0
- package/lib/module/components/input/variants/SearchInput/SearchClearButton.js.map +1 -0
- package/lib/module/components/input/variants/SearchInput/index.js +85 -30
- package/lib/module/components/input/variants/SearchInput/index.js.map +1 -1
- package/lib/module/components/input/variants/SearchInput/utils.js +9 -0
- package/lib/module/components/input/variants/SearchInput/utils.js.map +1 -0
- package/lib/module/components/screen/ScreenList.js +5 -1
- package/lib/module/components/screen/ScreenList.js.map +1 -1
- package/lib/module/components/screen/ScreenScrollView.js +5 -1
- package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
- package/lib/module/theme/tokens/light.js +1 -1
- package/lib/typescript/src/components/header/parts/HeaderBar.d.ts +3 -1
- package/lib/typescript/src/components/header/parts/HeaderBar.d.ts.map +1 -1
- package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts +3 -3
- package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts.map +1 -1
- package/lib/typescript/src/components/header/parts/HeaderSurface.d.ts +12 -3
- package/lib/typescript/src/components/header/parts/HeaderSurface.d.ts.map +1 -1
- package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
- package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input/hooks/use-focus-state.d.ts +15 -0
- package/lib/typescript/src/components/input/hooks/use-focus-state.d.ts.map +1 -0
- package/lib/typescript/src/components/input/parts/InputField.d.ts +3 -1
- package/lib/typescript/src/components/input/parts/InputField.d.ts.map +1 -1
- package/lib/typescript/src/components/input/types.d.ts +11 -1
- package/lib/typescript/src/components/input/types.d.ts.map +1 -1
- package/lib/typescript/src/components/input/variants/SearchInput/SearchCancelButton.d.ts +19 -0
- package/lib/typescript/src/components/input/variants/SearchInput/SearchCancelButton.d.ts.map +1 -0
- package/lib/typescript/src/components/input/variants/SearchInput/SearchClearButton.d.ts +13 -0
- package/lib/typescript/src/components/input/variants/SearchInput/SearchClearButton.d.ts.map +1 -0
- package/lib/typescript/src/components/input/variants/SearchInput/index.d.ts +15 -1
- package/lib/typescript/src/components/input/variants/SearchInput/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input/variants/SearchInput/utils.d.ts +4 -0
- package/lib/typescript/src/components/input/variants/SearchInput/utils.d.ts.map +1 -0
- package/lib/typescript/src/components/screen/ScreenList.d.ts.map +1 -1
- package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/header/parts/HeaderBar.tsx +4 -1
- package/src/components/header/parts/HeaderGradientOverlay.tsx +3 -4
- package/src/components/header/parts/HeaderSurface.tsx +14 -8
- package/src/components/header/variants/LargeTitleHeader/index.tsx +1 -0
- package/src/components/header/variants/TitleHeader/index.tsx +1 -0
- package/src/components/input/hooks/use-focus-state.ts +32 -0
- package/src/components/input/parts/InputField.tsx +45 -31
- package/src/components/input/types.ts +11 -1
- package/src/components/input/variants/SearchInput/SearchCancelButton.tsx +109 -0
- package/src/components/input/variants/SearchInput/SearchClearButton.tsx +58 -0
- package/src/components/input/variants/SearchInput/index.tsx +92 -24
- package/src/components/input/variants/SearchInput/utils.ts +4 -0
- package/src/components/screen/ScreenList.tsx +3 -0
- package/src/components/screen/ScreenScrollView.tsx +3 -0
- package/src/theme/tokens/light.ts +1 -1
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import { type ComponentRef, forwardRef, useCallback, useState } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { type ComponentRef, forwardRef, useCallback, useImperativeHandle, useRef, useState } from "react";
|
|
2
|
+
import { type TextInput, View } from "react-native";
|
|
3
3
|
|
|
4
4
|
import { StyleSheet } from "react-native-unistyles";
|
|
5
5
|
|
|
6
6
|
import { Icon } from "../../../icon";
|
|
7
|
+
import { useFocusState } from "../../hooks/use-focus-state";
|
|
7
8
|
import { InputField } from "../../parts/InputField";
|
|
8
9
|
import type { InputBaseProps } from "../../types";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
/** Touch expansion around the clear button, in dp — it is smaller than a cell. */
|
|
14
|
-
const CLEAR_HIT_SLOP = 8;
|
|
10
|
+
import { SearchCancelButton } from "./SearchCancelButton";
|
|
11
|
+
import { SearchClearButton } from "./SearchClearButton";
|
|
12
|
+
import { ICON_SIZE } from "./utils";
|
|
15
13
|
|
|
16
14
|
export interface SearchInputProps extends Omit<InputBaseProps, "leading" | "trailing"> {
|
|
17
15
|
/**
|
|
@@ -23,16 +21,30 @@ export interface SearchInputProps extends Omit<InputBaseProps, "leading" | "trai
|
|
|
23
21
|
isClearable?: boolean;
|
|
24
22
|
/** Accessible name of the clear button. Defaults to `'Clear search'`. */
|
|
25
23
|
clearAccessibilityLabel?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Show a round cancel button beside the field while it is focused. Pressing it
|
|
26
|
+
* blurs the field and calls `onCancelPress`. Defaults to `false`.
|
|
27
|
+
*/
|
|
28
|
+
hasCancelButton?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Called when the cancel button is pressed, after the field has blurred. The
|
|
31
|
+
* text is left alone; clear it here if cancelling should also reset the query.
|
|
32
|
+
*/
|
|
33
|
+
onCancelPress?: () => void;
|
|
34
|
+
/** Accessible name of the cancel button. Defaults to `'Cancel search'`. */
|
|
35
|
+
cancelAccessibilityLabel?: string;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
/**
|
|
29
39
|
* A search field: a leading magnifier, and a clear button that appears as soon
|
|
30
40
|
* as there is text. Controlled through `value` or left to manage its own, and
|
|
31
|
-
* it defaults to the search return key so the keyboard reads correctly.
|
|
41
|
+
* it defaults to the search return key so the keyboard reads correctly. With
|
|
42
|
+
* `hasCancelButton`, a round cancel button slides in beside it while focused.
|
|
32
43
|
*
|
|
33
44
|
* @example
|
|
34
45
|
* ```tsx
|
|
35
46
|
* <Input.Search value={query} onChangeText={setQuery} onSubmitEditing={run} />
|
|
47
|
+
* <Input.Search hasCancelButton onCancelPress={() => setQuery("")} />
|
|
36
48
|
* ```
|
|
37
49
|
*/
|
|
38
50
|
export const SearchInput = forwardRef<ComponentRef<typeof TextInput>, SearchInputProps>(function SearchInput(
|
|
@@ -43,16 +55,29 @@ export const SearchInput = forwardRef<ComponentRef<typeof TextInput>, SearchInpu
|
|
|
43
55
|
onClear,
|
|
44
56
|
isClearable = true,
|
|
45
57
|
clearAccessibilityLabel = "Clear search",
|
|
58
|
+
hasCancelButton = false,
|
|
59
|
+
onCancelPress,
|
|
60
|
+
cancelAccessibilityLabel = "Cancel search",
|
|
46
61
|
placeholder = "Search",
|
|
47
62
|
isDisabled = false,
|
|
48
63
|
size = "md",
|
|
49
64
|
radius = "full",
|
|
65
|
+
background = "surface.sunken",
|
|
66
|
+
hasFocusGlow = true,
|
|
50
67
|
returnKeyType = "search",
|
|
51
68
|
clearButtonMode = "never",
|
|
69
|
+
style,
|
|
70
|
+
onFocus,
|
|
71
|
+
onBlur,
|
|
52
72
|
...rest
|
|
53
73
|
},
|
|
54
74
|
ref
|
|
55
75
|
) {
|
|
76
|
+
// The cancel button needs the native input to blur it, and the caller still
|
|
77
|
+
// gets the same instance through the forwarded ref.
|
|
78
|
+
const inputRef = useRef<ComponentRef<typeof TextInput>>(null);
|
|
79
|
+
useImperativeHandle(ref, () => inputRef.current as ComponentRef<typeof TextInput>);
|
|
80
|
+
|
|
56
81
|
// The clear button only exists while there is text, so an uncontrolled field
|
|
57
82
|
// still has to be tracked. That state also has to drive the native input:
|
|
58
83
|
// resetting it alone would hide the button while the text stayed on screen,
|
|
@@ -61,6 +86,20 @@ export const SearchInput = forwardRef<ComponentRef<typeof TextInput>, SearchInpu
|
|
|
61
86
|
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? "");
|
|
62
87
|
const isControlled = value !== undefined;
|
|
63
88
|
const text = isControlled ? value : uncontrolledValue;
|
|
89
|
+
// Ending the edit makes iOS commit any pending autocorrect suggestion as one
|
|
90
|
+
// last `onChangeText`. `onCancelPress` waits for the blur event, which comes
|
|
91
|
+
// after it, so a query cleared there is not overwritten by the late commit.
|
|
92
|
+
const isCancellingRef = useRef(false);
|
|
93
|
+
const handleBlurAfterCancel = useCallback<NonNullable<SearchInputProps["onBlur"]>>(
|
|
94
|
+
(event) => {
|
|
95
|
+
onBlur?.(event);
|
|
96
|
+
if (!isCancellingRef.current) return;
|
|
97
|
+
isCancellingRef.current = false;
|
|
98
|
+
onCancelPress?.();
|
|
99
|
+
},
|
|
100
|
+
[onBlur, onCancelPress]
|
|
101
|
+
);
|
|
102
|
+
const { isFocused, handleFocus, handleBlur } = useFocusState(onFocus, handleBlurAfterCancel);
|
|
64
103
|
|
|
65
104
|
const handleChangeText = useCallback(
|
|
66
105
|
(next: string) => {
|
|
@@ -76,14 +115,28 @@ export const SearchInput = forwardRef<ComponentRef<typeof TextInput>, SearchInpu
|
|
|
76
115
|
onClear?.();
|
|
77
116
|
}, [isControlled, onChangeText, onClear]);
|
|
78
117
|
|
|
79
|
-
const
|
|
118
|
+
const handleCancel = useCallback(() => {
|
|
119
|
+
const input = inputRef.current;
|
|
120
|
+
// No blur event will come for a field that is not focused, so call through.
|
|
121
|
+
if (!input?.isFocused()) {
|
|
122
|
+
onCancelPress?.();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
isCancellingRef.current = true;
|
|
126
|
+
input.blur();
|
|
127
|
+
}, [onCancelPress]);
|
|
80
128
|
|
|
81
|
-
|
|
129
|
+
const showsClear = isClearable && text.length > 0 && !isDisabled;
|
|
130
|
+
const showsCancel = isFocused && !isDisabled;
|
|
131
|
+
|
|
132
|
+
const field = (
|
|
82
133
|
<InputField
|
|
83
|
-
ref={
|
|
134
|
+
ref={inputRef}
|
|
84
135
|
size={size}
|
|
85
136
|
radius={radius}
|
|
137
|
+
background={background}
|
|
86
138
|
isDisabled={isDisabled}
|
|
139
|
+
hasFocusGlow={hasFocusGlow}
|
|
87
140
|
placeholder={placeholder}
|
|
88
141
|
returnKeyType={returnKeyType}
|
|
89
142
|
// iOS's native clear button would sit beside ours; ours is the
|
|
@@ -91,28 +144,43 @@ export const SearchInput = forwardRef<ComponentRef<typeof TextInput>, SearchInpu
|
|
|
91
144
|
clearButtonMode={clearButtonMode}
|
|
92
145
|
value={text}
|
|
93
146
|
onChangeText={handleChangeText}
|
|
147
|
+
onFocus={handleFocus}
|
|
148
|
+
onBlur={handleBlur}
|
|
94
149
|
leading={<Icon name="terra.search" size={ICON_SIZE[size]} color="text.tertiary" />}
|
|
95
150
|
trailing={
|
|
96
|
-
|
|
97
|
-
<
|
|
98
|
-
accessibilityLabel={clearAccessibilityLabel}
|
|
99
|
-
accessibilityRole="button"
|
|
100
|
-
hitSlop={CLEAR_HIT_SLOP}
|
|
101
|
-
onPress={handleClear}
|
|
102
|
-
style={styles.clear}
|
|
103
|
-
>
|
|
104
|
-
<Icon name="terra.x" size={ICON_SIZE[size]} color="text.tertiary" />
|
|
105
|
-
</Pressable>
|
|
151
|
+
showsClear ? (
|
|
152
|
+
<SearchClearButton size={size} accessibilityLabel={clearAccessibilityLabel} onPress={handleClear} />
|
|
106
153
|
) : undefined
|
|
107
154
|
}
|
|
155
|
+
// With a cancel button the row is the outer element, so `style` moves there.
|
|
156
|
+
style={hasCancelButton ? undefined : style}
|
|
108
157
|
{...rest}
|
|
109
158
|
/>
|
|
110
159
|
);
|
|
160
|
+
|
|
161
|
+
// Without a cancel button the field renders bare, so its layout is unchanged.
|
|
162
|
+
if (!hasCancelButton) return field;
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<View style={[styles.row, style]}>
|
|
166
|
+
<View style={styles.fieldSlot}>{field}</View>
|
|
167
|
+
<SearchCancelButton
|
|
168
|
+
isVisible={showsCancel}
|
|
169
|
+
size={size}
|
|
170
|
+
background={background}
|
|
171
|
+
accessibilityLabel={cancelAccessibilityLabel}
|
|
172
|
+
onPress={handleCancel}
|
|
173
|
+
/>
|
|
174
|
+
</View>
|
|
175
|
+
);
|
|
111
176
|
});
|
|
112
177
|
|
|
113
178
|
const styles = StyleSheet.create(() => ({
|
|
114
|
-
|
|
179
|
+
row: {
|
|
180
|
+
flexDirection: "row",
|
|
115
181
|
alignItems: "center",
|
|
116
|
-
|
|
182
|
+
},
|
|
183
|
+
fieldSlot: {
|
|
184
|
+
flex: 1,
|
|
117
185
|
},
|
|
118
186
|
}));
|
|
@@ -245,6 +245,9 @@ const ScreenListBase = forwardRef(function ScreenList(
|
|
|
245
245
|
contentInsetAdjustmentBehavior={platformAdjustsInsets ? "automatic" : "never"}
|
|
246
246
|
automaticallyAdjustContentInsets={false}
|
|
247
247
|
automaticallyAdjustsScrollIndicatorInsets={false}
|
|
248
|
+
// iOS: inset by the keyboard so a focused field can scroll above it.
|
|
249
|
+
automaticallyAdjustKeyboardInsets
|
|
250
|
+
keyboardShouldPersistTaps="handled"
|
|
248
251
|
contentContainerStyle={[
|
|
249
252
|
styles.content(
|
|
250
253
|
resolvedPadding,
|
|
@@ -117,6 +117,9 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
|
|
|
117
117
|
contentInsetAdjustmentBehavior={platformAdjustsInsets ? "automatic" : "never"}
|
|
118
118
|
automaticallyAdjustContentInsets={false}
|
|
119
119
|
automaticallyAdjustsScrollIndicatorInsets={false}
|
|
120
|
+
// iOS: inset by the keyboard so a focused field can scroll above it.
|
|
121
|
+
automaticallyAdjustKeyboardInsets
|
|
122
|
+
keyboardShouldPersistTaps="handled"
|
|
120
123
|
showsVerticalScrollIndicator={false}
|
|
121
124
|
scrollEventThrottle={16}
|
|
122
125
|
contentContainerStyle={[
|
|
@@ -11,7 +11,7 @@ export const light = {
|
|
|
11
11
|
|
|
12
12
|
// Text
|
|
13
13
|
"color.text.primary": "{palette.neutral.950}",
|
|
14
|
-
"color.text.secondary": "#
|
|
14
|
+
"color.text.secondary": "#55555d",
|
|
15
15
|
"color.text.tertiary": "#6e6e76",
|
|
16
16
|
"color.text.disabled": "#d4d4d8",
|
|
17
17
|
"color.text.inverse": "{palette.white}",
|