react-native-terra-ui 0.9.0 → 0.10.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 +24 -0
- package/lib/module/components/avatar/Avatar.js +7 -2
- package/lib/module/components/avatar/Avatar.js.map +1 -1
- package/lib/module/components/header/header-buttons.js +1 -1
- package/lib/module/components/header/header-buttons.js.map +1 -1
- package/lib/module/components/icon/Icon.js +2 -2
- package/lib/module/components/screen/Screen.js +87 -27
- package/lib/module/components/screen/Screen.js.map +1 -1
- package/lib/module/components/screen/ScreenContext.js +5 -1
- package/lib/module/components/screen/ScreenContext.js.map +1 -1
- package/lib/module/components/screen/ScreenList.js +11 -3
- package/lib/module/components/screen/ScreenList.js.map +1 -1
- package/lib/module/components/screen/ScreenScrollView.js +11 -3
- package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
- package/lib/module/components/screen/hooks/use-nested-container-warning.js +39 -0
- package/lib/module/components/screen/hooks/use-nested-container-warning.js.map +1 -0
- package/lib/module/components/screen/parts/ScreenFrame.js +1 -0
- package/lib/module/components/screen/parts/ScreenFrame.js.map +1 -1
- package/lib/module/components/toast/Toast.js +1 -1
- package/lib/module/components/toast/Toast.js.map +1 -1
- package/lib/module/theme/defaults/icons.js +26 -19
- package/lib/module/theme/defaults/icons.js.map +1 -1
- package/lib/typescript/src/components/avatar/Avatar.d.ts.map +1 -1
- package/lib/typescript/src/components/icon/Icon.d.ts +2 -2
- package/lib/typescript/src/components/screen/Screen.d.ts +6 -0
- package/lib/typescript/src/components/screen/Screen.d.ts.map +1 -1
- package/lib/typescript/src/components/screen/ScreenContext.d.ts +27 -1
- package/lib/typescript/src/components/screen/ScreenContext.d.ts.map +1 -1
- 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/lib/typescript/src/components/screen/hooks/use-nested-container-warning.d.ts +24 -0
- package/lib/typescript/src/components/screen/hooks/use-nested-container-warning.d.ts.map +1 -0
- package/lib/typescript/src/components/screen/parts/ScreenFrame.d.ts.map +1 -1
- package/lib/typescript/src/theme/defaults/icons.d.ts.map +1 -1
- package/lib/typescript/src/theme/types.d.ts +1 -1
- package/lib/typescript/src/theme/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/avatar/Avatar.tsx +6 -1
- package/src/components/header/header-buttons.tsx +1 -1
- package/src/components/icon/Icon.tsx +2 -2
- package/src/components/screen/Screen.tsx +81 -17
- package/src/components/screen/ScreenContext.tsx +32 -0
- package/src/components/screen/ScreenList.tsx +9 -1
- package/src/components/screen/ScreenScrollView.tsx +9 -1
- package/src/components/screen/hooks/use-nested-container-warning.ts +40 -0
- package/src/components/screen/parts/ScreenFrame.tsx +8 -1
- package/src/components/toast/Toast.tsx +1 -1
- package/src/theme/defaults/icons.tsx +22 -15
- package/src/theme/types.ts +6 -5
|
@@ -37,6 +37,28 @@ export interface ScreenContextValue {
|
|
|
37
37
|
* or a horizontal container needs.
|
|
38
38
|
*/
|
|
39
39
|
contentOwnsBottomInset: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Whether the platform's own content-inset adjustment pays the vertical insets.
|
|
42
|
+
*
|
|
43
|
+
* True on an iOS screen with no `Screen.Header`: the navigation bar above such a
|
|
44
|
+
* screen is UIKit's, and only UIKit can inset a scroll view for one — a native
|
|
45
|
+
* large title resizes on every frame of its collapse, and it refuses to collapse
|
|
46
|
+
* at all for a scroll view that does not fill the screen. The frame therefore
|
|
47
|
+
* keeps no vertical edge, the container asks for
|
|
48
|
+
* `contentInsetAdjustmentBehavior: "automatic"` rather than `"never"`, and
|
|
49
|
+
* `contentOwnsBottomInset` stays false because UIKit pays that edge too.
|
|
50
|
+
*/
|
|
51
|
+
platformAdjustsInsets: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Whether this screen *would* hand its vertical insets to the platform, if it
|
|
54
|
+
* found a vertical scroll container among its children.
|
|
55
|
+
*
|
|
56
|
+
* Everything `platformAdjustsInsets` needs except the container itself. The two
|
|
57
|
+
* differ only when the container is nested inside another component, where the
|
|
58
|
+
* root cannot see it — which is a mistake on such a screen, and the difference
|
|
59
|
+
* is how a mounting container detects that it was the one missed.
|
|
60
|
+
*/
|
|
61
|
+
platformInsetsEligible: boolean;
|
|
40
62
|
/** @deprecated Use contentPadding. */
|
|
41
63
|
margins: ScreenContentPadding;
|
|
42
64
|
/** Whether a `Screen.Header` is present. Used by scroll containers to add top inset. */
|
|
@@ -78,6 +100,10 @@ export interface ScreenScrollProviderProps {
|
|
|
78
100
|
safeArea?: ScreenSafeArea;
|
|
79
101
|
/** Whether the primary scroll container pads the bottom safe area. Default false. */
|
|
80
102
|
contentOwnsBottomInset?: boolean;
|
|
103
|
+
/** Whether the platform adjusts the scroll container's vertical insets. Default false. */
|
|
104
|
+
platformAdjustsInsets?: boolean;
|
|
105
|
+
/** Whether the screen qualifies for that, container aside. Default false. */
|
|
106
|
+
platformInsetsEligible?: boolean;
|
|
81
107
|
/** Whether a `Screen.Header` child is present. Default false. */
|
|
82
108
|
hasHeader?: boolean;
|
|
83
109
|
|
|
@@ -95,6 +121,8 @@ export function ScreenScrollProvider({
|
|
|
95
121
|
contentPadding = "all",
|
|
96
122
|
safeArea = "auto",
|
|
97
123
|
contentOwnsBottomInset = false,
|
|
124
|
+
platformAdjustsInsets = false,
|
|
125
|
+
platformInsetsEligible = false,
|
|
98
126
|
hasHeader = false,
|
|
99
127
|
}: ScreenScrollProviderProps) {
|
|
100
128
|
const scrollY = useSharedValue(0);
|
|
@@ -123,6 +151,8 @@ export function ScreenScrollProvider({
|
|
|
123
151
|
contentPadding,
|
|
124
152
|
safeArea,
|
|
125
153
|
contentOwnsBottomInset,
|
|
154
|
+
platformAdjustsInsets,
|
|
155
|
+
platformInsetsEligible,
|
|
126
156
|
margins: contentPadding,
|
|
127
157
|
hasHeader,
|
|
128
158
|
scrollY,
|
|
@@ -137,6 +167,8 @@ export function ScreenScrollProvider({
|
|
|
137
167
|
contentPadding,
|
|
138
168
|
safeArea,
|
|
139
169
|
contentOwnsBottomInset,
|
|
170
|
+
platformAdjustsInsets,
|
|
171
|
+
platformInsetsEligible,
|
|
140
172
|
hasHeader,
|
|
141
173
|
scrollY,
|
|
142
174
|
headerCollapseHeight,
|
|
@@ -16,6 +16,7 @@ import { StyleSheet } from "react-native-unistyles";
|
|
|
16
16
|
|
|
17
17
|
import { resolveScreenPadding } from "#theme/screen-padding";
|
|
18
18
|
import { PortalHost } from "../portal";
|
|
19
|
+
import { useNestedContainerWarning } from "./hooks/use-nested-container-warning";
|
|
19
20
|
import { useScreenRef } from "./hooks/use-screen-ref";
|
|
20
21
|
import { useScreenScrollHandler } from "./hooks/use-screen-scroll-handler";
|
|
21
22
|
import { ScreenBottomInset } from "./parts/ScreenBottomInset";
|
|
@@ -174,10 +175,12 @@ const ScreenListBase = forwardRef(function ScreenList(
|
|
|
174
175
|
contentPadding: screenPadding,
|
|
175
176
|
hasHeader,
|
|
176
177
|
contentOwnsBottomInset,
|
|
178
|
+
platformAdjustsInsets,
|
|
177
179
|
} = useScreen();
|
|
178
180
|
// The root yields the bottom edge only to a vertical container, so a horizontal
|
|
179
181
|
// one here is a secondary container and must not pay the inset a second time.
|
|
180
182
|
const ownsBottomInset = contentOwnsBottomInset && !horizontal;
|
|
183
|
+
useNestedContainerWarning("Screen.List", horizontal);
|
|
181
184
|
const mergedRef = useScreenRef(isDefaultFlatList ? scrollRef : undefined, ref);
|
|
182
185
|
const resolvedPadding = contentPadding ?? margins ?? screenPadding;
|
|
183
186
|
const resolvedLoading = isLoading ?? loading;
|
|
@@ -212,7 +215,12 @@ const ScreenListBase = forwardRef(function ScreenList(
|
|
|
212
215
|
renderItem={renderItem}
|
|
213
216
|
style={[{ backgroundColor: background }, style]}
|
|
214
217
|
horizontal={horizontal}
|
|
215
|
-
|
|
218
|
+
/*
|
|
219
|
+
UIKit owns the vertical insets on a header-less iOS screen — see
|
|
220
|
+
`platformAdjustsInsets`. Everywhere else the frame has already paid them,
|
|
221
|
+
so a second adjustment here would count them twice.
|
|
222
|
+
*/
|
|
223
|
+
contentInsetAdjustmentBehavior={platformAdjustsInsets ? "automatic" : "never"}
|
|
216
224
|
automaticallyAdjustContentInsets={false}
|
|
217
225
|
automaticallyAdjustsScrollIndicatorInsets={false}
|
|
218
226
|
contentContainerStyle={[
|
|
@@ -6,6 +6,7 @@ import { StyleSheet } from "react-native-unistyles";
|
|
|
6
6
|
|
|
7
7
|
import { resolveScreenPadding } from "#theme/screen-padding";
|
|
8
8
|
import { PortalHost } from "../portal";
|
|
9
|
+
import { useNestedContainerWarning } from "./hooks/use-nested-container-warning";
|
|
9
10
|
import { useScreenRef } from "./hooks/use-screen-ref";
|
|
10
11
|
import { useScreenScrollHandler } from "./hooks/use-screen-scroll-handler";
|
|
11
12
|
import { ScreenBottomInset } from "./parts/ScreenBottomInset";
|
|
@@ -72,10 +73,12 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
|
|
|
72
73
|
hasHeader,
|
|
73
74
|
background,
|
|
74
75
|
contentOwnsBottomInset,
|
|
76
|
+
platformAdjustsInsets,
|
|
75
77
|
} = useScreen();
|
|
76
78
|
// The root yields the bottom edge only to a vertical container, so a horizontal
|
|
77
79
|
// one here is a secondary container and must not pay the inset a second time.
|
|
78
80
|
const ownsBottomInset = contentOwnsBottomInset && !horizontal;
|
|
81
|
+
useNestedContainerWarning("Screen.ScrollView", horizontal);
|
|
79
82
|
const resolvedPadding = contentPadding ?? margins ?? screenPadding;
|
|
80
83
|
const mergedRef = useScreenRef(scrollRef, ref);
|
|
81
84
|
const placeholder = renderScreenPlaceholder({
|
|
@@ -91,7 +94,12 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
|
|
|
91
94
|
ref={mergedRef}
|
|
92
95
|
style={[{ backgroundColor: background }, style]}
|
|
93
96
|
horizontal={horizontal}
|
|
94
|
-
|
|
97
|
+
/*
|
|
98
|
+
UIKit owns the vertical insets on a header-less iOS screen — see
|
|
99
|
+
`platformAdjustsInsets`. Everywhere else the frame has already paid
|
|
100
|
+
them, so a second adjustment here would count them twice.
|
|
101
|
+
*/
|
|
102
|
+
contentInsetAdjustmentBehavior={platformAdjustsInsets ? "automatic" : "never"}
|
|
95
103
|
automaticallyAdjustContentInsets={false}
|
|
96
104
|
automaticallyAdjustsScrollIndicatorInsets={false}
|
|
97
105
|
showsVerticalScrollIndicator={false}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
import { useScreen } from "../ScreenContext";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Warns, in development, when this container is the one the screen root failed
|
|
7
|
+
* to find.
|
|
8
|
+
*
|
|
9
|
+
* The root inspects its own children and fragments but does not execute
|
|
10
|
+
* component children, so a container rendered inside one is invisible to it (see
|
|
11
|
+
* `hasScrollContainerOwningBottom`). On a header-less iOS screen that changes
|
|
12
|
+
* behaviour rather than just bookkeeping: the frame pads the top edge, the scroll
|
|
13
|
+
* view no longer fills the screen, and UIKit stops collapsing the native large
|
|
14
|
+
* title — silently, with the screen otherwise looking correct.
|
|
15
|
+
*
|
|
16
|
+
* The container is the only place that can tell. `platformInsetsEligible` says
|
|
17
|
+
* the screen qualified; a container mounting while `platformAdjustsInsets` is
|
|
18
|
+
* false says the root never saw it. Both true together mean exactly this mistake,
|
|
19
|
+
* so there is nothing here to false-positive on: a screen with no scroll
|
|
20
|
+
* container at all never runs this hook, and a deliberate `safeArea` opt-out is
|
|
21
|
+
* not eligible in the first place.
|
|
22
|
+
*
|
|
23
|
+
* @param container Name to report, e.g. `"Screen.List"`.
|
|
24
|
+
* @param horizontal Whether this container scrolls horizontally; those are never
|
|
25
|
+
* candidates for the vertical insets and never warn.
|
|
26
|
+
*/
|
|
27
|
+
export function useNestedContainerWarning(container: string, horizontal: boolean): void {
|
|
28
|
+
const { platformAdjustsInsets, platformInsetsEligible } = useScreen();
|
|
29
|
+
const isNested = platformInsetsEligible && !platformAdjustsInsets && !horizontal;
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!__DEV__ || !isNested) return;
|
|
33
|
+
console.warn(
|
|
34
|
+
`[react-native-terra-ui] ${container} is nested inside another component, so its Screen could not find it. ` +
|
|
35
|
+
"On iOS a Screen with no Screen.Header hands its vertical insets to UIKit, but only when the scroll " +
|
|
36
|
+
"container is among its own children — nested, the frame pads the top instead and a native large title " +
|
|
37
|
+
`will not collapse. Render ${container} as a direct child of Screen; fragments are fine.`
|
|
38
|
+
);
|
|
39
|
+
}, [container, isNested]);
|
|
40
|
+
}
|
|
@@ -19,7 +19,14 @@ export function ScreenFrame({ children, edges }: PropsWithChildren<{ edges: Scre
|
|
|
19
19
|
return (
|
|
20
20
|
<>
|
|
21
21
|
<ScreenSafeArea edges={edges}>
|
|
22
|
-
|
|
22
|
+
{/*
|
|
23
|
+
`collapsable={false}` for the same reason as the `Screen` root: whatever
|
|
24
|
+
Fabric decides about this view, its children have to stay inside it.
|
|
25
|
+
UIKit finds a screen's scroll view by walking first subviews down from
|
|
26
|
+
the view controller, and a mounted-but-childless view in that chain is a
|
|
27
|
+
dead end — the large title then never collapses.
|
|
28
|
+
*/}
|
|
29
|
+
<View collapsable={false} style={styles.content} onLayout={onLayout}>
|
|
23
30
|
{children}
|
|
24
31
|
</View>
|
|
25
32
|
</ScreenSafeArea>
|
|
@@ -300,7 +300,7 @@ const ToastClose = forwardRef<ComponentRef<typeof Button>, ToastCloseProps>(func
|
|
|
300
300
|
>
|
|
301
301
|
{children ?? (
|
|
302
302
|
<Icon
|
|
303
|
-
name="
|
|
303
|
+
name="terra.x"
|
|
304
304
|
size={iconProps?.size ?? 20}
|
|
305
305
|
color={iconProps?.color ?? colors.icon}
|
|
306
306
|
strokeWidth={iconProps?.strokeWidth}
|
|
@@ -19,6 +19,11 @@ const strokeProps = (color: string, strokeWidth: number) => ({
|
|
|
19
19
|
strokeLinejoin: "round" as const,
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
// Every registry icon draws through this: one 24×24 viewBox rendered at exactly
|
|
23
|
+
// the requested `size`, so `<Icon size={24} />` is the same optical weight
|
|
24
|
+
// whichever name it resolves to. A glyph that needs to sit smaller than its box
|
|
25
|
+
// (Avatar's person fallback) asks for a smaller `size` — it must not shrink
|
|
26
|
+
// itself here, or it would scale differently from every other icon.
|
|
22
27
|
const SvgIcon = ({ size, children }: { size: number; children: React.ReactNode }) => (
|
|
23
28
|
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
24
29
|
{children}
|
|
@@ -36,25 +41,25 @@ const PathIcon = ({ d, ...props }: TerraIconProps & { d: string }) => {
|
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
// Lucide chevron geometry scaled 20% from the 24×24 viewBox center (45° arms unchanged).
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
44
|
+
const CHEVRON_CENTER = 12;
|
|
45
|
+
const CHEVRON_OPEN = 3 * 1.2;
|
|
46
|
+
const CHEVRON_ARM = 6 * 1.2;
|
|
42
47
|
|
|
43
|
-
const
|
|
48
|
+
const ChevronLeftIcon: TerraIconComponent = (props) => (
|
|
44
49
|
<PathIcon
|
|
45
|
-
d={`M${
|
|
50
|
+
d={`M${CHEVRON_CENTER + CHEVRON_OPEN} ${CHEVRON_CENTER + CHEVRON_ARM}L${CHEVRON_CENTER - CHEVRON_OPEN} ${CHEVRON_CENTER} ${CHEVRON_CENTER + CHEVRON_OPEN} ${CHEVRON_CENTER - CHEVRON_ARM}`}
|
|
46
51
|
{...props}
|
|
47
52
|
/>
|
|
48
53
|
);
|
|
49
54
|
|
|
50
|
-
const
|
|
55
|
+
const ChevronRightIcon: TerraIconComponent = (props) => (
|
|
51
56
|
<PathIcon
|
|
52
|
-
d={`M${
|
|
57
|
+
d={`M${CHEVRON_CENTER - CHEVRON_OPEN} ${CHEVRON_CENTER + CHEVRON_ARM}L${CHEVRON_CENTER + CHEVRON_OPEN} ${CHEVRON_CENTER} ${CHEVRON_CENTER - CHEVRON_OPEN} ${CHEVRON_CENTER - CHEVRON_ARM}`}
|
|
53
58
|
{...props}
|
|
54
59
|
/>
|
|
55
60
|
);
|
|
56
61
|
|
|
57
|
-
const
|
|
62
|
+
const XIcon: TerraIconComponent = (props) => <PathIcon d="M18 6L6 18M6 6l12 12" {...props} />;
|
|
58
63
|
|
|
59
64
|
const InfoIcon: TerraIconComponent = (props) => {
|
|
60
65
|
const { color, size, strokeWidth } = resolveIconProps(props);
|
|
@@ -107,26 +112,28 @@ const DangerIcon: TerraIconComponent = (props) => {
|
|
|
107
112
|
);
|
|
108
113
|
};
|
|
109
114
|
|
|
115
|
+
const CheckIcon: TerraIconComponent = (props) => <PathIcon d="M20 6 9 17l-5-5" {...props} />;
|
|
116
|
+
|
|
110
117
|
export const PersonIcon: TerraIconComponent = (props) => {
|
|
111
118
|
const { color, size, strokeWidth } = resolveIconProps(props);
|
|
112
119
|
const stroke = strokeProps(color, strokeWidth);
|
|
113
|
-
const iconSize = size * 0.55;
|
|
114
120
|
|
|
115
121
|
return (
|
|
116
|
-
<
|
|
122
|
+
<SvgIcon size={size}>
|
|
117
123
|
<Circle cx={12} cy={8} r={4} {...stroke} />
|
|
118
124
|
<Path d="M4 20c0-4 3.582-7 8-7s8 3 8 7" {...stroke} />
|
|
119
|
-
</
|
|
125
|
+
</SvgIcon>
|
|
120
126
|
);
|
|
121
127
|
};
|
|
122
128
|
|
|
123
129
|
export const defaultIcons: Record<TerraSemanticIconName, TerraIconComponent> = {
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
130
|
+
"terra.chevron-left": ChevronLeftIcon,
|
|
131
|
+
"terra.chevron-right": ChevronRightIcon,
|
|
132
|
+
"terra.x": XIcon,
|
|
133
|
+
"terra.check": CheckIcon,
|
|
134
|
+
"terra.person": PersonIcon,
|
|
127
135
|
"status.info": InfoIcon,
|
|
128
136
|
"status.success": SuccessIcon,
|
|
129
137
|
"status.warning": WarningIcon,
|
|
130
138
|
"status.danger": DangerIcon,
|
|
131
|
-
person: PersonIcon,
|
|
132
139
|
};
|
package/src/theme/types.ts
CHANGED
|
@@ -412,14 +412,15 @@ declare global {
|
|
|
412
412
|
export interface TerraIconRegistry extends TerraUI.IconRegistry {}
|
|
413
413
|
|
|
414
414
|
export type TerraSemanticIconName =
|
|
415
|
-
| "
|
|
416
|
-
| "
|
|
417
|
-
| "
|
|
415
|
+
| "terra.chevron-left"
|
|
416
|
+
| "terra.chevron-right"
|
|
417
|
+
| "terra.x"
|
|
418
|
+
| "terra.check"
|
|
419
|
+
| "terra.person"
|
|
418
420
|
| "status.info"
|
|
419
421
|
| "status.success"
|
|
420
422
|
| "status.warning"
|
|
421
|
-
| "status.danger"
|
|
422
|
-
| "person";
|
|
423
|
+
| "status.danger";
|
|
423
424
|
|
|
424
425
|
export type TerraConfiguredIconName = keyof TerraIconRegistry & string;
|
|
425
426
|
|