@butternutbox/pawprint-native 0.23.2 → 0.24.1
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/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +22 -0
- package/dist/index.cjs +91 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +92 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/atoms/Button/Button.tsx +20 -27
- package/src/components/atoms/Tag/Tag.tsx +14 -6
- package/src/components/atoms/Typography/Typography.tsx +6 -0
- package/src/components/molecules/Progress/Progress.stories.tsx +37 -0
- package/src/components/molecules/Progress/Progress.tsx +37 -2
- package/src/components/molecules/Progress/index.ts +1 -1
- package/src/components/molecules/StackedNotifications/StackedNotifications.test.tsx +58 -1
- package/src/components/molecules/StackedNotifications/StackedNotifications.tsx +83 -17
- package/src/components/molecules/TabNavigation/TabNavigation.tsx +6 -2
package/package.json
CHANGED
|
@@ -83,18 +83,6 @@ const StyledButton = styled(Pressable)<{
|
|
|
83
83
|
})
|
|
84
84
|
)
|
|
85
85
|
|
|
86
|
-
const StyledTextWrapper = styled(View)<{
|
|
87
|
-
textOpacity: number
|
|
88
|
-
fullWidth?: boolean
|
|
89
|
-
}>(({ textOpacity, fullWidth }) => ({
|
|
90
|
-
opacity: textOpacity,
|
|
91
|
-
// `flexShrink` (not `flex: 1`) so the text wrapper can shrink to let long /
|
|
92
|
-
// translated copy wrap under the button's `minHeight`, WITHOUT growing to
|
|
93
|
-
// fill the row on short copy — which would push a `startIcon`/`endIcon` out
|
|
94
|
-
// to the button edge instead of sitting next to the centred text.
|
|
95
|
-
...(fullWidth ? { flexShrink: 1 } : {})
|
|
96
|
-
}))
|
|
97
|
-
|
|
98
86
|
const StyledSpinnerWrapper = styled(View)({
|
|
99
87
|
position: "absolute",
|
|
100
88
|
alignItems: "center",
|
|
@@ -233,21 +221,26 @@ const Button = React.forwardRef<View, ButtonProps>(
|
|
|
233
221
|
{...rest}
|
|
234
222
|
>
|
|
235
223
|
{startIcon && <IconWrapper>{startIcon}</IconWrapper>}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
224
|
+
{/* `Typography` is a DIRECT child of the button row (no wrapping View)
|
|
225
|
+
so its `<Text>` participates in flex shrink and wraps long /
|
|
226
|
+
translated labels to multiple lines under the button's `minHeight`.
|
|
227
|
+
A `<Text>` nested inside an intermediate flex `<View>` does not
|
|
228
|
+
re-wrap once the wrapper resolves its width. Loading hides the label
|
|
229
|
+
via a transparent colour (keeping its footprint) while the spinner
|
|
230
|
+
overlays it. */}
|
|
231
|
+
<Typography
|
|
232
|
+
token={{
|
|
233
|
+
fontFamily: typography.fontFamily,
|
|
234
|
+
fontWeight: typography.fontWeight,
|
|
235
|
+
fontSize: typography.fontSize,
|
|
236
|
+
lineHeight: typography.lineHeight,
|
|
237
|
+
letterSpacing: typography.letterSpacing
|
|
238
|
+
}}
|
|
239
|
+
color={(loading ? "transparent" : variantStyles.textColor) as never}
|
|
240
|
+
align="center"
|
|
241
|
+
>
|
|
242
|
+
{children}
|
|
243
|
+
</Typography>
|
|
251
244
|
{endIcon && <IconWrapper>{endIcon}</IconWrapper>}
|
|
252
245
|
{loading && (
|
|
253
246
|
<StyledSpinnerWrapper>
|
|
@@ -66,6 +66,12 @@ const StyledTag = styled(View)<{
|
|
|
66
66
|
}
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
+
// Lets the label shrink so a long tag wraps within its container instead of
|
|
70
|
+
// growing past it (e.g. long translated copy overflowing a card's padding).
|
|
71
|
+
const StyledLabel = styled(View)({
|
|
72
|
+
flexShrink: 1
|
|
73
|
+
})
|
|
74
|
+
|
|
69
75
|
/**
|
|
70
76
|
* Tag component for labelling and categorising content.
|
|
71
77
|
*
|
|
@@ -118,12 +124,14 @@ export const Tag = React.forwardRef<View, TagProps>(
|
|
|
118
124
|
colour={iconColourMap[variant]}
|
|
119
125
|
/>
|
|
120
126
|
)}
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
<StyledLabel>
|
|
128
|
+
<Typography
|
|
129
|
+
token={typography[size].default}
|
|
130
|
+
color={textColorMap[variant]}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</Typography>
|
|
134
|
+
</StyledLabel>
|
|
127
135
|
</StyledTag>
|
|
128
136
|
)
|
|
129
137
|
}
|
|
@@ -217,6 +217,12 @@ export const Typography = React.forwardRef<Text, TypographyProps>(
|
|
|
217
217
|
} = props
|
|
218
218
|
|
|
219
219
|
const style: TextStyle = {
|
|
220
|
+
// Token-mode text is component-owned and lives inside flex rows
|
|
221
|
+
// (buttons, tabs, tags). `flexShrink` lets it wrap to multiple lines
|
|
222
|
+
// when the row is narrower than the text, instead of laying out on one
|
|
223
|
+
// line and overflowing/clipping — a `<Text>` nested in a flex `<View>`
|
|
224
|
+
// won't re-wrap on its own after the parent's width is resolved.
|
|
225
|
+
flexShrink: 1,
|
|
220
226
|
...nativeTokenToStyle(token),
|
|
221
227
|
...(color ? { color } : {}),
|
|
222
228
|
...getCommonStyles(align, textTransform, textDecoration)
|
|
@@ -3,6 +3,7 @@ import { View, StyleSheet } from "react-native"
|
|
|
3
3
|
import { Progress } from "./Progress"
|
|
4
4
|
import type { ProgressProps } from "./Progress"
|
|
5
5
|
import { Typography } from "../../atoms/Typography"
|
|
6
|
+
import { Button } from "../../atoms/Button"
|
|
6
7
|
|
|
7
8
|
export default {
|
|
8
9
|
title: "Molecules/Progress",
|
|
@@ -33,6 +34,10 @@ export default {
|
|
|
33
34
|
description: {
|
|
34
35
|
control: { type: "text" },
|
|
35
36
|
description: "Helper text rendered above the track (right)"
|
|
37
|
+
},
|
|
38
|
+
animate: {
|
|
39
|
+
control: { type: "boolean" },
|
|
40
|
+
description: "Ease the indicator to its new width instead of snapping"
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
43
|
}
|
|
@@ -53,6 +58,38 @@ export const Playground = {
|
|
|
53
58
|
)
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
const DEMO_MAX = 6
|
|
62
|
+
|
|
63
|
+
export const Stepped = {
|
|
64
|
+
name: "Stepped (animation)",
|
|
65
|
+
render: () => {
|
|
66
|
+
const [step, setStep] = React.useState(1)
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<View style={styles.column}>
|
|
70
|
+
<Progress
|
|
71
|
+
value={step}
|
|
72
|
+
max={DEMO_MAX}
|
|
73
|
+
label="Animated"
|
|
74
|
+
description={`${step}/${DEMO_MAX}`}
|
|
75
|
+
/>
|
|
76
|
+
<Progress
|
|
77
|
+
value={step}
|
|
78
|
+
max={DEMO_MAX}
|
|
79
|
+
animate={false}
|
|
80
|
+
label="Snapping"
|
|
81
|
+
description={`${step}/${DEMO_MAX}`}
|
|
82
|
+
/>
|
|
83
|
+
<View style={styles.section}>
|
|
84
|
+
<Button onPress={() => setStep((s) => (s % DEMO_MAX) + 1)}>
|
|
85
|
+
Next step
|
|
86
|
+
</Button>
|
|
87
|
+
</View>
|
|
88
|
+
</View>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
56
93
|
export const Small = {
|
|
57
94
|
name: "Small",
|
|
58
95
|
render: () => (
|
|
@@ -3,6 +3,13 @@ import { View, ViewProps } from "react-native"
|
|
|
3
3
|
import styled from "@emotion/native"
|
|
4
4
|
import { useTheme } from "@emotion/react"
|
|
5
5
|
import * as ProgressPrimitive from "@rn-primitives/progress"
|
|
6
|
+
import Reanimated, {
|
|
7
|
+
Easing,
|
|
8
|
+
useAnimatedStyle,
|
|
9
|
+
useReducedMotion,
|
|
10
|
+
useSharedValue,
|
|
11
|
+
withTiming
|
|
12
|
+
} from "react-native-reanimated"
|
|
6
13
|
import { Typography } from "../../atoms/Typography"
|
|
7
14
|
|
|
8
15
|
type ProgressSize = "sm" | "lg"
|
|
@@ -16,11 +23,22 @@ type ProgressOwnProps = {
|
|
|
16
23
|
label?: React.ReactNode
|
|
17
24
|
description?: React.ReactNode
|
|
18
25
|
getValueLabel?: (value: number, max: number) => string
|
|
26
|
+
animate?: boolean
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
export type ProgressProps = ProgressOwnProps &
|
|
22
30
|
Omit<ViewProps, keyof ProgressOwnProps>
|
|
23
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Timing the indicator eases with. Exported so a consumer overlaying something
|
|
34
|
+
* on the track (a marker, a mascot) can run its own animation off the same
|
|
35
|
+
* config and stay pinned to the leading edge of the fill.
|
|
36
|
+
*/
|
|
37
|
+
export const PROGRESS_ANIMATION = {
|
|
38
|
+
duration: 450,
|
|
39
|
+
easing: Easing.inOut(Easing.cubic)
|
|
40
|
+
} as const
|
|
41
|
+
|
|
24
42
|
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
25
43
|
|
|
26
44
|
const StyledRoot = styled(View)<{ rootGap: number }>(({ rootGap }) => ({
|
|
@@ -52,7 +70,7 @@ const StyledTrack = styled(View)<{
|
|
|
52
70
|
overflow: "hidden"
|
|
53
71
|
}))
|
|
54
72
|
|
|
55
|
-
const StyledIndicator = styled(View)<{
|
|
73
|
+
const StyledIndicator = styled(Reanimated.View)<{
|
|
56
74
|
indicatorBorderRadius: number
|
|
57
75
|
indicatorBgColor: string
|
|
58
76
|
}>(({ indicatorBorderRadius, indicatorBgColor }) => ({
|
|
@@ -78,6 +96,8 @@ const clamp = (value: number, min: number, max: number) =>
|
|
|
78
96
|
* @param {React.ReactNode} [label] - Label rendered in the top-left above the track.
|
|
79
97
|
* @param {React.ReactNode} [description] - Helper text rendered in the top-right above the track.
|
|
80
98
|
* @param {(value: number, max: number) => string} [getValueLabel] - Accessible value label.
|
|
99
|
+
* @param {boolean} [animate=true] - Ease the indicator to its new width instead of
|
|
100
|
+
* snapping. Ignored when the OS has reduced motion enabled.
|
|
81
101
|
*
|
|
82
102
|
* @example
|
|
83
103
|
* ```tsx
|
|
@@ -96,6 +116,7 @@ export const Progress = React.forwardRef<View, ProgressProps>(
|
|
|
96
116
|
label,
|
|
97
117
|
description,
|
|
98
118
|
getValueLabel,
|
|
119
|
+
animate = true,
|
|
99
120
|
...rest
|
|
100
121
|
},
|
|
101
122
|
ref
|
|
@@ -117,6 +138,20 @@ export const Progress = React.forwardRef<View, ProgressProps>(
|
|
|
117
138
|
const typographyForSize =
|
|
118
139
|
size === "lg" ? typography.large : typography.small
|
|
119
140
|
|
|
141
|
+
const reducedMotion = useReducedMotion()
|
|
142
|
+
const shouldAnimate = animate && !reducedMotion
|
|
143
|
+
const fill = useSharedValue(percent)
|
|
144
|
+
|
|
145
|
+
React.useEffect(() => {
|
|
146
|
+
fill.value = shouldAnimate
|
|
147
|
+
? withTiming(percent, PROGRESS_ANIMATION)
|
|
148
|
+
: percent
|
|
149
|
+
}, [fill, percent, shouldAnimate])
|
|
150
|
+
|
|
151
|
+
const indicatorStyle = useAnimatedStyle(() => ({
|
|
152
|
+
width: `${fill.value}%`
|
|
153
|
+
}))
|
|
154
|
+
|
|
120
155
|
const trackSize = { lg: track.size.large, sm: track.size.small }[size]
|
|
121
156
|
const trackHeight = parseTokenValue(trackSize.height)
|
|
122
157
|
const trackMinWidth = parseTokenValue(trackSize.mindWidth)
|
|
@@ -171,7 +206,7 @@ export const Progress = React.forwardRef<View, ProgressProps>(
|
|
|
171
206
|
<StyledIndicator
|
|
172
207
|
indicatorBorderRadius={indicatorBorderRadius}
|
|
173
208
|
indicatorBgColor={indicatorBgColor}
|
|
174
|
-
style={
|
|
209
|
+
style={indicatorStyle}
|
|
175
210
|
/>
|
|
176
211
|
</ProgressPrimitive.Indicator>
|
|
177
212
|
</StyledTrack>
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { Progress } from "./Progress"
|
|
1
|
+
export { Progress, PROGRESS_ANIMATION } from "./Progress"
|
|
2
2
|
export type { ProgressProps } from "./Progress"
|
|
@@ -3,7 +3,10 @@ import { screen } from "@testing-library/react"
|
|
|
3
3
|
import userEvent from "@testing-library/user-event"
|
|
4
4
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
|
5
5
|
import { renderWithTheme } from "../../../test-utils"
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
StackedNotifications,
|
|
8
|
+
computeBackfillIds
|
|
9
|
+
} from "./StackedNotifications"
|
|
7
10
|
import type { StackedNotificationItem } from "./StackedNotifications"
|
|
8
11
|
|
|
9
12
|
const A: StackedNotificationItem = {
|
|
@@ -200,3 +203,57 @@ describe("StackedNotifications", () => {
|
|
|
200
203
|
})
|
|
201
204
|
})
|
|
202
205
|
})
|
|
206
|
+
|
|
207
|
+
describe("computeBackfillIds", () => {
|
|
208
|
+
// This decides whether a card slides in or fades in. Tested separately
|
|
209
|
+
// here because the tests above use a mocked animation library, so they
|
|
210
|
+
// can't actually check which animation a card gets.
|
|
211
|
+
|
|
212
|
+
it("flags an item that was queued but not yet visible as a backfill", () => {
|
|
213
|
+
const result = computeBackfillIds(
|
|
214
|
+
[C, B, A],
|
|
215
|
+
new Set(["a", "b", "c"]),
|
|
216
|
+
new Set(["c", "b"])
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
expect(result).toEqual(new Set(["a"]))
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it("does not flag a genuinely new item that was never queued before", () => {
|
|
223
|
+
const result = computeBackfillIds(
|
|
224
|
+
[C, B, A],
|
|
225
|
+
new Set(["b", "c"]),
|
|
226
|
+
new Set(["c", "b"])
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
expect(result).toEqual(new Set())
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it("does not flag an item that was already visible", () => {
|
|
233
|
+
const result = computeBackfillIds(
|
|
234
|
+
[C, B, A],
|
|
235
|
+
new Set(["a", "b", "c"]),
|
|
236
|
+
new Set(["a", "b", "c"])
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
expect(result).toEqual(new Set())
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it("only considers items present in the current layers", () => {
|
|
243
|
+
// "a" was queued and hidden before, but it's not shown this time, so
|
|
244
|
+
// it shouldn't show up in the result at all.
|
|
245
|
+
const result = computeBackfillIds(
|
|
246
|
+
[C, B],
|
|
247
|
+
new Set(["a", "b", "c"]),
|
|
248
|
+
new Set(["c"])
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
expect(result).toEqual(new Set(["b"]))
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it("flags nothing on an initial mount, where nothing was previously queued", () => {
|
|
255
|
+
const result = computeBackfillIds([C, B, A], new Set(), new Set())
|
|
256
|
+
|
|
257
|
+
expect(result).toEqual(new Set())
|
|
258
|
+
})
|
|
259
|
+
})
|
|
@@ -44,15 +44,16 @@ const SPRING_CONFIG = { damping: 20, stiffness: 260, mass: 0.5 }
|
|
|
44
44
|
const CARD_ENTERING = FadeInDown.duration(200).easing(Easing.inOut(Easing.quad))
|
|
45
45
|
const CARD_EXITING = FadeOutDown.duration(200)
|
|
46
46
|
|
|
47
|
-
// Bottom-anchored so scaling never moves a card's bottom edge — only
|
|
48
|
-
// translateY does. That's what makes the peek reveal an exact, constant
|
|
49
|
-
// distance regardless of a card's own message length.
|
|
50
47
|
const CARD_BASE_STYLE = {
|
|
51
48
|
position: "absolute" as const,
|
|
52
49
|
bottom: 0,
|
|
53
50
|
left: 0,
|
|
54
|
-
right: 0
|
|
55
|
-
|
|
51
|
+
right: 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const DEPTH_LAYER_STYLE = {
|
|
55
|
+
transformOrigin: "50% 100%" as const,
|
|
56
|
+
width: "100%" as const
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
// Floats over content pinned to the bottom. `box-none` lets touches pass
|
|
@@ -79,11 +80,35 @@ const StyledStack = styled(View)({
|
|
|
79
80
|
width: "100%"
|
|
80
81
|
})
|
|
81
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Works out which cards are only now becoming visible because they were
|
|
85
|
+
* already queued (a "backfill"), versus cards that are genuinely brand new.
|
|
86
|
+
* A backfilled card should slide into place; a brand new one should fade in.
|
|
87
|
+
*
|
|
88
|
+
* This lives outside the component as its own function so it can be tested
|
|
89
|
+
* directly — the animation library is mocked in tests, so testing this logic
|
|
90
|
+
* through the rendered component wouldn't actually prove it works.
|
|
91
|
+
*/
|
|
92
|
+
export const computeBackfillIds = (
|
|
93
|
+
layers: StackedNotificationItem[],
|
|
94
|
+
prevActiveIds: Set<string>,
|
|
95
|
+
prevLayerIds: Set<string>
|
|
96
|
+
): Set<string> => {
|
|
97
|
+
const ids = new Set<string>()
|
|
98
|
+
layers.forEach((item) => {
|
|
99
|
+
if (prevActiveIds.has(item.id) && !prevLayerIds.has(item.id)) {
|
|
100
|
+
ids.add(item.id)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
return ids
|
|
104
|
+
}
|
|
105
|
+
|
|
82
106
|
type StackedCardProps = {
|
|
83
107
|
item: StackedNotificationItem
|
|
84
108
|
depth: number
|
|
85
109
|
revealStep: number
|
|
86
110
|
onDismiss?: (id: string) => void
|
|
111
|
+
isBackfill: boolean
|
|
87
112
|
}
|
|
88
113
|
|
|
89
114
|
/**
|
|
@@ -97,15 +122,32 @@ const StackedCard = ({
|
|
|
97
122
|
item,
|
|
98
123
|
depth,
|
|
99
124
|
revealStep,
|
|
100
|
-
onDismiss
|
|
125
|
+
onDismiss,
|
|
126
|
+
isBackfill
|
|
101
127
|
}: StackedCardProps) => {
|
|
102
|
-
|
|
128
|
+
// Remember whether this card was a backfill when it first appeared, and
|
|
129
|
+
// ignore any later changes to the `isBackfill` prop. The parent only
|
|
130
|
+
// reports `isBackfill: true` for the one render where the card shows up —
|
|
131
|
+
// if we kept using the live value, the card's animation would stop
|
|
132
|
+
// partway through as soon as that value flips back to false.
|
|
133
|
+
const isBackfillRef = React.useRef(isBackfill)
|
|
134
|
+
const wasBackfill = isBackfillRef.current
|
|
135
|
+
|
|
136
|
+
const depthSV = useSharedValue(wasBackfill ? depth + 1 : depth)
|
|
137
|
+
const opacitySV = useSharedValue(wasBackfill ? 0 : 1)
|
|
103
138
|
|
|
104
139
|
React.useEffect(() => {
|
|
105
140
|
depthSV.value = withSpring(depth, SPRING_CONFIG)
|
|
106
141
|
}, [depth, depthSV])
|
|
107
142
|
|
|
108
|
-
|
|
143
|
+
// Only a backfilled card needs to fade in here. A brand new card already
|
|
144
|
+
// fades in via the outer view's `entering` animation instead.
|
|
145
|
+
React.useEffect(() => {
|
|
146
|
+
if (wasBackfill) opacitySV.value = withSpring(1, SPRING_CONFIG)
|
|
147
|
+
}, [])
|
|
148
|
+
|
|
149
|
+
const depthStyle = useAnimatedStyle(() => ({
|
|
150
|
+
opacity: opacitySV.value,
|
|
109
151
|
transform: [
|
|
110
152
|
{ translateY: depthSV.value * revealStep },
|
|
111
153
|
{ scale: 1 - depthSV.value * SCALE_STEP }
|
|
@@ -116,20 +158,22 @@ const StackedCard = ({
|
|
|
116
158
|
|
|
117
159
|
return (
|
|
118
160
|
<Reanimated.View
|
|
119
|
-
entering={CARD_ENTERING}
|
|
161
|
+
entering={wasBackfill ? undefined : CARD_ENTERING}
|
|
120
162
|
exiting={CARD_EXITING}
|
|
121
163
|
pointerEvents={isFront ? "auto" : "none"}
|
|
122
164
|
accessibilityElementsHidden={!isFront}
|
|
123
165
|
importantForAccessibility={isFront ? "auto" : "no-hide-descendants"}
|
|
124
|
-
style={[CARD_BASE_STYLE, { zIndex: -depth }
|
|
166
|
+
style={[CARD_BASE_STYLE, { zIndex: -depth }]}
|
|
125
167
|
>
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
168
|
+
<Reanimated.View style={[DEPTH_LAYER_STYLE, depthStyle]}>
|
|
169
|
+
<Notification
|
|
170
|
+
variant="toast"
|
|
171
|
+
type={item.type}
|
|
172
|
+
onClose={isFront ? () => onDismiss?.(item.id) : undefined}
|
|
173
|
+
>
|
|
174
|
+
{item.message}
|
|
175
|
+
</Notification>
|
|
176
|
+
</Reanimated.View>
|
|
133
177
|
</Reanimated.View>
|
|
134
178
|
)
|
|
135
179
|
}
|
|
@@ -207,6 +251,27 @@ export const StackedNotifications = React.forwardRef<
|
|
|
207
251
|
[activeItems]
|
|
208
252
|
)
|
|
209
253
|
|
|
254
|
+
// Keep track of which ids were queued, and which were actually shown,
|
|
255
|
+
// last time we rendered. That's how we tell "already queued, only now
|
|
256
|
+
// shown" apart from "brand new" below.
|
|
257
|
+
const prevActiveIdsRef = React.useRef<Set<string>>(new Set())
|
|
258
|
+
const prevLayerIdsRef = React.useRef<Set<string>>(new Set())
|
|
259
|
+
|
|
260
|
+
const backfillIds = React.useMemo(
|
|
261
|
+
() =>
|
|
262
|
+
computeBackfillIds(
|
|
263
|
+
layers,
|
|
264
|
+
prevActiveIdsRef.current,
|
|
265
|
+
prevLayerIdsRef.current
|
|
266
|
+
),
|
|
267
|
+
[layers]
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
React.useEffect(() => {
|
|
271
|
+
prevActiveIdsRef.current = new Set(activeItems.map((item) => item.id))
|
|
272
|
+
prevLayerIdsRef.current = new Set(layers.map((item) => item.id))
|
|
273
|
+
}, [activeItems, layers])
|
|
274
|
+
|
|
210
275
|
const handleDismiss = (id: string) => {
|
|
211
276
|
setDismissed((prev) => {
|
|
212
277
|
const next = new Set(prev)
|
|
@@ -235,6 +300,7 @@ export const StackedNotifications = React.forwardRef<
|
|
|
235
300
|
depth={depth}
|
|
236
301
|
revealStep={peekStep}
|
|
237
302
|
onDismiss={handleDismiss}
|
|
303
|
+
isBackfill={backfillIds.has(item.id)}
|
|
238
304
|
/>
|
|
239
305
|
))}
|
|
240
306
|
</StyledStack>
|
|
@@ -114,7 +114,11 @@ const StyledTab = styled(Pressable)<{
|
|
|
114
114
|
alignItems: "center",
|
|
115
115
|
justifyContent: "center",
|
|
116
116
|
gap: tabGap,
|
|
117
|
-
height
|
|
117
|
+
// `minHeight` (not a fixed `height`) so a tab whose label wraps to more
|
|
118
|
+
// than one line — e.g. long translated copy in `fixed` layout — grows to
|
|
119
|
+
// fit instead of clipping/overflowing. The list's `alignItems: "stretch"`
|
|
120
|
+
// then matches every tab to the tallest, keeping labels vertically aligned.
|
|
121
|
+
minHeight: tabHeight,
|
|
118
122
|
minWidth: tabMinWidth,
|
|
119
123
|
paddingTop: tabPaddingTop,
|
|
120
124
|
paddingBottom: tabPaddingBottom,
|
|
@@ -223,7 +227,7 @@ const TabNavigationTab = React.forwardRef<View, TabNavigationTabProps>(
|
|
|
223
227
|
{icon && (
|
|
224
228
|
<Icon icon={icon} customColour={textColour as string} aria-hidden />
|
|
225
229
|
)}
|
|
226
|
-
<Typography token={token} color={textColour}>
|
|
230
|
+
<Typography token={token} color={textColour} align="center">
|
|
227
231
|
{children}
|
|
228
232
|
</Typography>
|
|
229
233
|
</StyledTab>
|