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.
Files changed (68) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/lib/module/components/header/parts/HeaderBar.js +3 -1
  3. package/lib/module/components/header/parts/HeaderBar.js.map +1 -1
  4. package/lib/module/components/header/parts/HeaderGradientOverlay.js +1 -2
  5. package/lib/module/components/header/parts/HeaderGradientOverlay.js.map +1 -1
  6. package/lib/module/components/header/parts/HeaderSurface.js +9 -4
  7. package/lib/module/components/header/parts/HeaderSurface.js.map +1 -1
  8. package/lib/module/components/header/variants/LargeTitleHeader/index.js +1 -0
  9. package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
  10. package/lib/module/components/header/variants/TitleHeader/index.js +1 -0
  11. package/lib/module/components/header/variants/TitleHeader/index.js.map +1 -1
  12. package/lib/module/components/input/hooks/use-focus-state.js +25 -0
  13. package/lib/module/components/input/hooks/use-focus-state.js.map +1 -0
  14. package/lib/module/components/input/parts/InputField.js +53 -31
  15. package/lib/module/components/input/parts/InputField.js.map +1 -1
  16. package/lib/module/components/input/variants/SearchInput/SearchCancelButton.js +93 -0
  17. package/lib/module/components/input/variants/SearchInput/SearchCancelButton.js.map +1 -0
  18. package/lib/module/components/input/variants/SearchInput/SearchClearButton.js +52 -0
  19. package/lib/module/components/input/variants/SearchInput/SearchClearButton.js.map +1 -0
  20. package/lib/module/components/input/variants/SearchInput/index.js +85 -30
  21. package/lib/module/components/input/variants/SearchInput/index.js.map +1 -1
  22. package/lib/module/components/input/variants/SearchInput/utils.js +9 -0
  23. package/lib/module/components/input/variants/SearchInput/utils.js.map +1 -0
  24. package/lib/module/components/screen/ScreenList.js +5 -1
  25. package/lib/module/components/screen/ScreenList.js.map +1 -1
  26. package/lib/module/components/screen/ScreenScrollView.js +5 -1
  27. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  28. package/lib/module/theme/tokens/light.js +1 -1
  29. package/lib/typescript/src/components/header/parts/HeaderBar.d.ts +3 -1
  30. package/lib/typescript/src/components/header/parts/HeaderBar.d.ts.map +1 -1
  31. package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts +3 -3
  32. package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts.map +1 -1
  33. package/lib/typescript/src/components/header/parts/HeaderSurface.d.ts +12 -3
  34. package/lib/typescript/src/components/header/parts/HeaderSurface.d.ts.map +1 -1
  35. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
  36. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts.map +1 -1
  37. package/lib/typescript/src/components/input/hooks/use-focus-state.d.ts +15 -0
  38. package/lib/typescript/src/components/input/hooks/use-focus-state.d.ts.map +1 -0
  39. package/lib/typescript/src/components/input/parts/InputField.d.ts +3 -1
  40. package/lib/typescript/src/components/input/parts/InputField.d.ts.map +1 -1
  41. package/lib/typescript/src/components/input/types.d.ts +11 -1
  42. package/lib/typescript/src/components/input/types.d.ts.map +1 -1
  43. package/lib/typescript/src/components/input/variants/SearchInput/SearchCancelButton.d.ts +19 -0
  44. package/lib/typescript/src/components/input/variants/SearchInput/SearchCancelButton.d.ts.map +1 -0
  45. package/lib/typescript/src/components/input/variants/SearchInput/SearchClearButton.d.ts +13 -0
  46. package/lib/typescript/src/components/input/variants/SearchInput/SearchClearButton.d.ts.map +1 -0
  47. package/lib/typescript/src/components/input/variants/SearchInput/index.d.ts +15 -1
  48. package/lib/typescript/src/components/input/variants/SearchInput/index.d.ts.map +1 -1
  49. package/lib/typescript/src/components/input/variants/SearchInput/utils.d.ts +4 -0
  50. package/lib/typescript/src/components/input/variants/SearchInput/utils.d.ts.map +1 -0
  51. package/lib/typescript/src/components/screen/ScreenList.d.ts.map +1 -1
  52. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/src/components/header/parts/HeaderBar.tsx +4 -1
  55. package/src/components/header/parts/HeaderGradientOverlay.tsx +3 -4
  56. package/src/components/header/parts/HeaderSurface.tsx +14 -8
  57. package/src/components/header/variants/LargeTitleHeader/index.tsx +1 -0
  58. package/src/components/header/variants/TitleHeader/index.tsx +1 -0
  59. package/src/components/input/hooks/use-focus-state.ts +32 -0
  60. package/src/components/input/parts/InputField.tsx +45 -31
  61. package/src/components/input/types.ts +11 -1
  62. package/src/components/input/variants/SearchInput/SearchCancelButton.tsx +109 -0
  63. package/src/components/input/variants/SearchInput/SearchClearButton.tsx +58 -0
  64. package/src/components/input/variants/SearchInput/index.tsx +92 -24
  65. package/src/components/input/variants/SearchInput/utils.ts +4 -0
  66. package/src/components/screen/ScreenList.tsx +3 -0
  67. package/src/components/screen/ScreenScrollView.tsx +3 -0
  68. package/src/theme/tokens/light.ts +1 -1
@@ -1,17 +1,15 @@
1
- import { type ComponentRef, forwardRef, useCallback, useState } from "react";
2
- import { Pressable, type TextInput } from "react-native";
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
- /** Glyph size per field size, in dp. */
11
- const ICON_SIZE = { sm: 16, md: 18, lg: 20 } as const;
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 hasText = text.length > 0;
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
- return (
129
+ const showsClear = isClearable && text.length > 0 && !isDisabled;
130
+ const showsCancel = isFocused && !isDisabled;
131
+
132
+ const field = (
82
133
  <InputField
83
- ref={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
- isClearable && hasText && !isDisabled ? (
97
- <Pressable
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
- clear: {
179
+ row: {
180
+ flexDirection: "row",
115
181
  alignItems: "center",
116
- justifyContent: "center",
182
+ },
183
+ fieldSlot: {
184
+ flex: 1,
117
185
  },
118
186
  }));
@@ -0,0 +1,4 @@
1
+ import type { InputSize } from "../../types";
2
+
3
+ /** Glyph size per field size, in dp. Shared by the magnifier and both buttons. */
4
+ export const ICON_SIZE: Record<InputSize, number> = { sm: 14, md: 16, lg: 18 };
@@ -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": "#4a4a52",
14
+ "color.text.secondary": "#55555d",
15
15
  "color.text.tertiary": "#6e6e76",
16
16
  "color.text.disabled": "#d4d4d8",
17
17
  "color.text.inverse": "{palette.white}",