@delacour/react-native-ui 0.1.0-alpha.20260925053522
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/README.md +113 -0
- package/package.json +125 -0
- package/src/components/accordion/AGENTS.md +193 -0
- package/src/components/accordion/accordion-content.tsx +125 -0
- package/src/components/accordion/accordion-description.tsx +17 -0
- package/src/components/accordion/accordion-indicator.tsx +104 -0
- package/src/components/accordion/accordion-item.tsx +78 -0
- package/src/components/accordion/accordion-title.tsx +18 -0
- package/src/components/accordion/accordion-trigger.tsx +148 -0
- package/src/components/accordion/accordion.context.tsx +153 -0
- package/src/components/accordion/accordion.tsx +253 -0
- package/src/components/accordion/accordion.types.ts +11 -0
- package/src/components/accordion/accordion.variants.test.ts +434 -0
- package/src/components/accordion/accordion.variants.ts +358 -0
- package/src/components/accordion/index.ts +45 -0
- package/src/components/badge/AGENTS.md +83 -0
- package/src/components/badge/badge-close-button.tsx +48 -0
- package/src/components/badge/badge-end-content.tsx +15 -0
- package/src/components/badge/badge-label.tsx +24 -0
- package/src/components/badge/badge-start-content.tsx +16 -0
- package/src/components/badge/badge.context.tsx +64 -0
- package/src/components/badge/badge.tsx +192 -0
- package/src/components/badge/badge.types.ts +10 -0
- package/src/components/badge/badge.variants.test.ts +269 -0
- package/src/components/badge/badge.variants.ts +238 -0
- package/src/components/badge/index.ts +17 -0
- package/src/components/bottom-sheet/AGENTS.md +242 -0
- package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
- package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +246 -0
- package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
- package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +150 -0
- package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
- package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
- package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +105 -0
- package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
- package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
- package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
- package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +293 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +220 -0
- package/src/components/bottom-sheet/index.ts +40 -0
- package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
- package/src/components/button/AGENTS.md +217 -0
- package/src/components/button/button-end-content.tsx +15 -0
- package/src/components/button/button-group-separator.tsx +37 -0
- package/src/components/button/button-group-text.tsx +101 -0
- package/src/components/button/button-group.tsx +158 -0
- package/src/components/button/button-label.tsx +24 -0
- package/src/components/button/button-start-content.tsx +15 -0
- package/src/components/button/button.context.tsx +187 -0
- package/src/components/button/button.tsx +273 -0
- package/src/components/button/button.types.ts +10 -0
- package/src/components/button/button.variants.test.ts +811 -0
- package/src/components/button/button.variants.ts +480 -0
- package/src/components/button/index.ts +50 -0
- package/src/components/chart/AGENTS.md +210 -0
- package/src/components/chart/chart-area.tsx +93 -0
- package/src/components/chart/chart-bar.tsx +103 -0
- package/src/components/chart/chart-bars.tsx +75 -0
- package/src/components/chart/chart-candlestick.tsx +60 -0
- package/src/components/chart/chart-grid.tsx +33 -0
- package/src/components/chart/chart-legend.tsx +41 -0
- package/src/components/chart/chart-line.tsx +50 -0
- package/src/components/chart/chart-scatter.tsx +35 -0
- package/src/components/chart/chart-tooltip-dot.tsx +70 -0
- package/src/components/chart/chart-tooltip-x.tsx +121 -0
- package/src/components/chart/chart-tooltip-y.tsx +55 -0
- package/src/components/chart/chart-tooltip.tsx +133 -0
- package/src/components/chart/chart-x-axis.tsx +27 -0
- package/src/components/chart/chart-y-axis.tsx +21 -0
- package/src/components/chart/chart.context.tsx +127 -0
- package/src/components/chart/chart.tsx +448 -0
- package/src/components/chart/chart.types.ts +108 -0
- package/src/components/chart/chart.variants.test.ts +893 -0
- package/src/components/chart/chart.variants.ts +726 -0
- package/src/components/chart/index.ts +103 -0
- package/src/components/chart/pie-chart-center.tsx +40 -0
- package/src/components/chart/pie-chart-label.tsx +56 -0
- package/src/components/chart/pie-chart-slice.tsx +45 -0
- package/src/components/chart/pie-chart-tooltip.tsx +85 -0
- package/src/components/chart/pie-chart.context.tsx +77 -0
- package/src/components/chart/pie-chart.tsx +247 -0
- package/src/components/chart/use-chart-font.ts +25 -0
- package/src/components/chart/use-chart-palette.ts +38 -0
- package/src/components/checkbox/AGENTS.md +185 -0
- package/src/components/checkbox/checkbox-box.tsx +152 -0
- package/src/components/checkbox/checkbox-group.tsx +107 -0
- package/src/components/checkbox/checkbox-label.tsx +34 -0
- package/src/components/checkbox/checkbox.context.tsx +136 -0
- package/src/components/checkbox/checkbox.tsx +258 -0
- package/src/components/checkbox/checkbox.types.ts +14 -0
- package/src/components/checkbox/checkbox.variants.test.ts +634 -0
- package/src/components/checkbox/checkbox.variants.ts +484 -0
- package/src/components/checkbox/index.ts +42 -0
- package/src/components/field/AGENTS.md +112 -0
- package/src/components/field/field-content.tsx +24 -0
- package/src/components/field/field-description.tsx +29 -0
- package/src/components/field/field-error.tsx +61 -0
- package/src/components/field/field-group.tsx +22 -0
- package/src/components/field/field-label.tsx +47 -0
- package/src/components/field/field-legend.tsx +34 -0
- package/src/components/field/field-separator.tsx +81 -0
- package/src/components/field/field-set.tsx +22 -0
- package/src/components/field/field.context.tsx +95 -0
- package/src/components/field/field.tsx +157 -0
- package/src/components/field/field.types.ts +15 -0
- package/src/components/field/field.variants.test.ts +240 -0
- package/src/components/field/field.variants.ts +156 -0
- package/src/components/field/index.ts +22 -0
- package/src/components/icon/AGENTS.md +104 -0
- package/src/components/icon/icon.context.tsx +29 -0
- package/src/components/icon/icon.tsx +92 -0
- package/src/components/icon/icon.variants.test.ts +111 -0
- package/src/components/icon/icon.variants.ts +79 -0
- package/src/components/icon/index.ts +11 -0
- package/src/components/input/AGENTS.md +126 -0
- package/src/components/input/index.ts +27 -0
- package/src/components/input/input-group-decorator.tsx +84 -0
- package/src/components/input/input-group-prefix.tsx +16 -0
- package/src/components/input/input-group-suffix.tsx +15 -0
- package/src/components/input/input-group.tsx +140 -0
- package/src/components/input/input.context.tsx +87 -0
- package/src/components/input/input.tsx +184 -0
- package/src/components/input/input.types.ts +10 -0
- package/src/components/input/input.variants.test.ts +369 -0
- package/src/components/input/input.variants.ts +329 -0
- package/src/components/list-group/AGENTS.md +67 -0
- package/src/components/list-group/index.ts +20 -0
- package/src/components/list-group/list-group-item-content.tsx +10 -0
- package/src/components/list-group/list-group-item-description.tsx +12 -0
- package/src/components/list-group/list-group-item-prefix.tsx +30 -0
- package/src/components/list-group/list-group-item-suffix.tsx +55 -0
- package/src/components/list-group/list-group-item-title.tsx +17 -0
- package/src/components/list-group/list-group-item.tsx +87 -0
- package/src/components/list-group/list-group.context.tsx +65 -0
- package/src/components/list-group/list-group.tsx +116 -0
- package/src/components/list-group/list-group.types.ts +14 -0
- package/src/components/list-group/list-group.variants.test.ts +223 -0
- package/src/components/list-group/list-group.variants.ts +107 -0
- package/src/components/pressable/AGENTS.md +74 -0
- package/src/components/pressable/index.ts +9 -0
- package/src/components/pressable/pressable.tsx +261 -0
- package/src/components/pressable/pressable.variants.test.ts +128 -0
- package/src/components/pressable/pressable.variants.ts +80 -0
- package/src/components/provider/AGENTS.md +90 -0
- package/src/components/provider/index.ts +1 -0
- package/src/components/provider/provider.tsx +105 -0
- package/src/components/radio/AGENTS.md +245 -0
- package/src/components/radio/index.ts +35 -0
- package/src/components/radio/radio-group.tsx +126 -0
- package/src/components/radio/radio-indicator.tsx +91 -0
- package/src/components/radio/radio-label.tsx +39 -0
- package/src/components/radio/radio.context.tsx +132 -0
- package/src/components/radio/radio.tsx +215 -0
- package/src/components/radio/radio.variants.test.ts +580 -0
- package/src/components/radio/radio.variants.ts +271 -0
- package/src/components/screen/AGENTS.md +287 -0
- package/src/components/screen/index.ts +85 -0
- package/src/components/screen/screen-chat-list.tsx +485 -0
- package/src/components/screen/screen-content.tsx +69 -0
- package/src/components/screen/screen-debug.ts +43 -0
- package/src/components/screen/screen-error.tsx +61 -0
- package/src/components/screen/screen-flat-list.tsx +88 -0
- package/src/components/screen/screen-footer-background.tsx +60 -0
- package/src/components/screen/screen-footer.tsx +170 -0
- package/src/components/screen/screen-header.tsx +36 -0
- package/src/components/screen/screen-legend-list.tsx +105 -0
- package/src/components/screen/screen-list-component.tsx +23 -0
- package/src/components/screen/screen-loading.tsx +50 -0
- package/src/components/screen/screen-navbar-back-button.tsx +79 -0
- package/src/components/screen/screen-navbar-background.tsx +45 -0
- package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
- package/src/components/screen/screen-navbar-title.tsx +31 -0
- package/src/components/screen/screen-navbar.tsx +154 -0
- package/src/components/screen/screen-root.tsx +38 -0
- package/src/components/screen/screen-scroll-area.tsx +116 -0
- package/src/components/screen/screen-scroll-shadow.tsx +186 -0
- package/src/components/screen/screen-section-list.tsx +81 -0
- package/src/components/screen/screen-view.tsx +59 -0
- package/src/components/screen/screen.context.tsx +167 -0
- package/src/components/screen/screen.tsx +114 -0
- package/src/components/screen/screen.types.ts +66 -0
- package/src/components/screen/screen.variants.test.ts +602 -0
- package/src/components/screen/screen.variants.ts +475 -0
- package/src/components/screen/use-screen-scroll-insets.ts +218 -0
- package/src/components/separator/AGENTS.md +30 -0
- package/src/components/separator/index.ts +8 -0
- package/src/components/separator/separator.tsx +85 -0
- package/src/components/slider/AGENTS.md +274 -0
- package/src/components/slider/index.ts +50 -0
- package/src/components/slider/slider-fill.tsx +68 -0
- package/src/components/slider/slider-output.tsx +55 -0
- package/src/components/slider/slider-thumb.tsx +193 -0
- package/src/components/slider/slider-track.tsx +233 -0
- package/src/components/slider/slider.context.tsx +118 -0
- package/src/components/slider/slider.tsx +321 -0
- package/src/components/slider/slider.types.ts +26 -0
- package/src/components/slider/slider.variants.test.ts +856 -0
- package/src/components/slider/slider.variants.ts +661 -0
- package/src/components/spinner/AGENTS.md +73 -0
- package/src/components/spinner/index.ts +17 -0
- package/src/components/spinner/spinner-arc.tsx +90 -0
- package/src/components/spinner/spinner-content.tsx +63 -0
- package/src/components/spinner/spinner.context.tsx +52 -0
- package/src/components/spinner/spinner.tsx +128 -0
- package/src/components/spinner/spinner.variants.test.ts +273 -0
- package/src/components/spinner/spinner.variants.ts +187 -0
- package/src/components/switch/AGENTS.md +213 -0
- package/src/components/switch/index.ts +42 -0
- package/src/components/switch/switch-content.tsx +111 -0
- package/src/components/switch/switch-end-content.tsx +18 -0
- package/src/components/switch/switch-start-content.tsx +20 -0
- package/src/components/switch/switch-thumb.tsx +102 -0
- package/src/components/switch/switch.context.tsx +79 -0
- package/src/components/switch/switch.tsx +423 -0
- package/src/components/switch/switch.types.ts +14 -0
- package/src/components/switch/switch.variants.test.ts +570 -0
- package/src/components/switch/switch.variants.ts +511 -0
- package/src/components/tabs/AGENTS.md +287 -0
- package/src/components/tabs/index.ts +69 -0
- package/src/components/tabs/tabs-content.tsx +65 -0
- package/src/components/tabs/tabs-indicator.tsx +97 -0
- package/src/components/tabs/tabs-label.tsx +62 -0
- package/src/components/tabs/tabs-list.tsx +139 -0
- package/src/components/tabs/tabs-pager.tsx +59 -0
- package/src/components/tabs/tabs-scroll-view.tsx +171 -0
- package/src/components/tabs/tabs-separator.tsx +73 -0
- package/src/components/tabs/tabs-trigger.tsx +210 -0
- package/src/components/tabs/tabs.context.tsx +294 -0
- package/src/components/tabs/tabs.tsx +435 -0
- package/src/components/tabs/tabs.types.ts +13 -0
- package/src/components/tabs/tabs.variants.test.ts +1020 -0
- package/src/components/tabs/tabs.variants.ts +670 -0
- package/src/components/text/AGENTS.md +95 -0
- package/src/components/text/index.ts +25 -0
- package/src/components/text/text.context.tsx +60 -0
- package/src/components/text/text.tsx +251 -0
- package/src/components/text/text.variants.test.ts +422 -0
- package/src/components/text/text.variants.ts +282 -0
- package/src/display-name.test.ts +145 -0
- package/src/docs.test.ts +98 -0
- package/src/expo/navigation-theme.tsx +58 -0
- package/src/hooks/use-controllable-state.ts +45 -0
- package/src/hooks/use-keyboard-state-sync.tsx +147 -0
- package/src/hooks/use-navigation-theme.ts +78 -0
- package/src/hooks/use-theme-color.ts +44 -0
- package/src/icons/central.ts +1 -0
- package/src/lib/cn.test.ts +136 -0
- package/src/lib/cn.ts +29 -0
- package/src/lib/color.test.ts +80 -0
- package/src/lib/color.ts +79 -0
- package/src/lib/compose-refs.test.ts +64 -0
- package/src/lib/compose-refs.ts +37 -0
- package/src/lib/keyboard-animation.test.ts +33 -0
- package/src/lib/keyboard-animation.ts +28 -0
- package/src/lib/merge-props.test.ts +82 -0
- package/src/lib/merge-props.ts +47 -0
- package/src/lib/navigation-theme.test.ts +49 -0
- package/src/lib/navigation-theme.ts +53 -0
- package/src/lib/slot.tsx +45 -0
- package/src/lib/tv.ts +20 -0
- package/src/styles/base.css +2 -0
- package/src/styles/geometry.test.ts +117 -0
- package/src/styles/index.css +3 -0
- package/src/styles/theme-tokens.test.ts +252 -0
- package/src/styles/theme.css +447 -0
- package/src/styles/tokens.css +132 -0
- package/src/styles/tokens.test.ts +181 -0
- package/src/styles/tokens.ts +86 -0
- package/src/uniwind-env.d.ts +1 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
CHAT_COMPOSER_GAP,
|
|
4
|
+
footerAboveKeyboard,
|
|
5
|
+
footerOccupancy,
|
|
6
|
+
resolveFooterBorderOpacity,
|
|
7
|
+
resolveNavbarBorderOpacity,
|
|
8
|
+
resolveScreenEdgePadding,
|
|
9
|
+
resolveScreenViewPadding,
|
|
10
|
+
resolveScrollBottomInset,
|
|
11
|
+
resolveScrollTopInset,
|
|
12
|
+
SCREEN_BORDER_FADE_DISTANCE,
|
|
13
|
+
SCREEN_EDGES,
|
|
14
|
+
SCREEN_FLOATING_BOTTOM_GAP,
|
|
15
|
+
SCREEN_FOOTER_PADDING,
|
|
16
|
+
SCREEN_PLACEMENTS,
|
|
17
|
+
SCREEN_SCROLL_INSET_MODES,
|
|
18
|
+
screenVariants,
|
|
19
|
+
} from "./screen.variants";
|
|
20
|
+
|
|
21
|
+
/** Top padding + bottom gap — the footer's own chrome around its measured content. */
|
|
22
|
+
const CHROME = SCREEN_FOOTER_PADDING + SCREEN_FLOATING_BOTTOM_GAP;
|
|
23
|
+
|
|
24
|
+
/** Slots that are containers: a `View` cannot cascade colour to a `Text` child. */
|
|
25
|
+
const CONTAINER_SLOTS = [
|
|
26
|
+
"root",
|
|
27
|
+
"content",
|
|
28
|
+
"view",
|
|
29
|
+
"header",
|
|
30
|
+
"navbar",
|
|
31
|
+
"navbarRow",
|
|
32
|
+
"navbarStart",
|
|
33
|
+
"navbarActions",
|
|
34
|
+
"navbarCenter",
|
|
35
|
+
"navbarBackground",
|
|
36
|
+
"navbarBorder",
|
|
37
|
+
"backButton",
|
|
38
|
+
"footer",
|
|
39
|
+
"footerBackground",
|
|
40
|
+
"footerBorder",
|
|
41
|
+
"footerContent",
|
|
42
|
+
"loading",
|
|
43
|
+
"errorContent",
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
/** Slots that render a `Text` and must therefore carry their own colour. */
|
|
47
|
+
const TEXT_SLOTS = ["navbarTitle", "navbarSubtitle", "errorTitle", "errorMessage"] as const;
|
|
48
|
+
|
|
49
|
+
describe("footerOccupancy", () => {
|
|
50
|
+
test("wraps the measured content in the footer's own chrome and the safe-area band", () => {
|
|
51
|
+
expect(footerOccupancy(58, 34)).toBe(120);
|
|
52
|
+
expect(footerOccupancy(58, 0)).toBe(86);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("gaps the bottom by the floating gap, not the footer's own padding", () => {
|
|
56
|
+
const inset = 34;
|
|
57
|
+
const pillBottomEdge = footerOccupancy(58, inset) - SCREEN_FOOTER_PADDING - 58;
|
|
58
|
+
expect(pillBottomEdge).toBe(inset + SCREEN_FLOATING_BOTTOM_GAP);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("tracks the safe-area inset instead of a hardcoded stand-in", () => {
|
|
62
|
+
for (const inset of [0, 20, 21, 34, 48]) {
|
|
63
|
+
expect(footerOccupancy(58, inset) - footerOccupancy(58, 0)).toBe(inset);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("tracks the measured content height 1:1 — an attachment strip must be reserved", () => {
|
|
68
|
+
expect(footerOccupancy(58 + 88, 34) - footerOccupancy(58, 34)).toBe(88);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("counts both vertical edges, not one", () => {
|
|
72
|
+
expect(footerOccupancy(0, 0)).toBe(CHROME);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("footerAboveKeyboard", () => {
|
|
77
|
+
// The whole reason two functions exist. The sticky shift parks the footer's
|
|
78
|
+
// safe-area band behind the keyboard, so counting it in a `bottomOffset`
|
|
79
|
+
// would scroll the focused input further than it needs to go.
|
|
80
|
+
test("differs from occupancy by exactly the safe-area band", () => {
|
|
81
|
+
for (const inset of [0, 21, 34]) {
|
|
82
|
+
expect(footerOccupancy(58, inset) - footerAboveKeyboard(58)).toBe(inset);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("still covers the footer's own chrome — the input must clear the composer", () => {
|
|
87
|
+
expect(footerAboveKeyboard(58)).toBe(86);
|
|
88
|
+
expect(footerAboveKeyboard(0)).toBe(CHROME);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("the layout constants", () => {
|
|
93
|
+
test("are positive lengths, so a reserve is never negative", () => {
|
|
94
|
+
for (const value of [SCREEN_FOOTER_PADDING, SCREEN_FLOATING_BOTTOM_GAP, CHAT_COMPOSER_GAP]) {
|
|
95
|
+
expect(value).toBeGreaterThan(0);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("screenVariants placement", () => {
|
|
101
|
+
test("defaults to overlay, the placement a screen's chrome floats at", () => {
|
|
102
|
+
expect(screenVariants().navbar()).toContain("absolute");
|
|
103
|
+
expect(screenVariants().footer()).toContain("absolute");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("pins the navbar to the top and the footer to the bottom", () => {
|
|
107
|
+
const navbar = screenVariants({ placement: "overlay" }).navbar();
|
|
108
|
+
expect(navbar).toContain("top-0");
|
|
109
|
+
expect(navbar).not.toContain("bottom-0");
|
|
110
|
+
|
|
111
|
+
const footer = screenVariants({ placement: "overlay" }).footer();
|
|
112
|
+
expect(footer).toContain("bottom-0");
|
|
113
|
+
expect(footer).not.toContain("top-0");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("stretches both across the full width, or an overlay would shrink to its content", () => {
|
|
117
|
+
for (const slot of ["navbar", "footer"] as const) {
|
|
118
|
+
const cls = screenVariants({ placement: "overlay" })[slot]();
|
|
119
|
+
expect(cls).toContain("right-0");
|
|
120
|
+
expect(cls).toContain("left-0");
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("backs a static footer, so content lifted under it cannot show through", () => {
|
|
125
|
+
// The backing travels with the keyboard translation. Without it a static
|
|
126
|
+
// footer pushed up over the content is legible only where the content
|
|
127
|
+
// happens to be blank.
|
|
128
|
+
expect(screenVariants({ placement: "static" }).footerBackground()).toContain("bg-background");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("leaves an overlay footer transparent, since content is meant to scroll under it", () => {
|
|
132
|
+
expect(screenVariants({ placement: "overlay" }).footerBackground()).not.toContain("bg-background");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("rules a static footer along its TOP edge, mirroring the navbar's bottom one", () => {
|
|
136
|
+
const cls = screenVariants({ placement: "static" }).footerBorder();
|
|
137
|
+
expect(cls).toContain("top-0");
|
|
138
|
+
expect(cls).not.toContain("bottom-0");
|
|
139
|
+
expect(cls).toContain("h-px");
|
|
140
|
+
expect(cls).toContain("bg-border");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("draws no hairline on an overlay footer, which floats rather than divides", () => {
|
|
144
|
+
expect(screenVariants({ placement: "overlay" }).footerBorder()).not.toContain("bg-border");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("puts the two hairlines on opposite edges", () => {
|
|
148
|
+
expect(screenVariants().navbarBorder()).toContain("bottom-0");
|
|
149
|
+
expect(screenVariants({ placement: "static" }).footerBorder()).toContain("top-0");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("fills its box at either placement, so the backing cannot be a partial band", () => {
|
|
153
|
+
for (const placement of SCREEN_PLACEMENTS) {
|
|
154
|
+
const cls = screenVariants({ placement }).footerBackground();
|
|
155
|
+
expect(cls).toContain("absolute");
|
|
156
|
+
for (const edge of ["top-0", "right-0", "bottom-0", "left-0"]) {
|
|
157
|
+
expect(cls).toContain(edge);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("returns a static navbar and footer to the flow", () => {
|
|
163
|
+
for (const slot of ["navbar", "footer"] as const) {
|
|
164
|
+
const cls = screenVariants({ placement: "static" })[slot]();
|
|
165
|
+
expect(cls).toContain("relative");
|
|
166
|
+
expect(cls).not.toContain("absolute");
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("gives every placement a distinct treatment on both slots", () => {
|
|
171
|
+
for (const slot of ["navbar", "footer"] as const) {
|
|
172
|
+
const seen = new Set(SCREEN_PLACEMENTS.map((placement) => screenVariants({ placement })[slot]()));
|
|
173
|
+
expect(seen.size).toBe(SCREEN_PLACEMENTS.length);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("raises the navbar above the content it overlays at either placement", () => {
|
|
178
|
+
// The navbar is a screen's FIRST child, so document order alone would
|
|
179
|
+
// paint the content over it. The footer needs no such help — it is last.
|
|
180
|
+
for (const placement of SCREEN_PLACEMENTS) {
|
|
181
|
+
expect(screenVariants({ placement }).navbar()).toContain("z-50");
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("every slot", () => {
|
|
187
|
+
// `tv()` returns undefined for a slot with no classes at all, not "". Blanking
|
|
188
|
+
// a slot therefore hands `className={undefined}` to a View and reads as a bug
|
|
189
|
+
// at the call site — it has already happened twice in this component, to
|
|
190
|
+
// `footerSafeArea` and then to `header`. A slot that genuinely needs no
|
|
191
|
+
// classes should be deleted, not emptied.
|
|
192
|
+
// `tv()` synthesises a `base` slot for the whole component whether or not one
|
|
193
|
+
// was declared. Nothing here renders it, and it is undefined by construction.
|
|
194
|
+
const declared = (slots: Record<string, () => string | undefined>) =>
|
|
195
|
+
Object.entries(slots).filter(([name]) => name !== "base");
|
|
196
|
+
|
|
197
|
+
test("resolves to a string, never undefined", () => {
|
|
198
|
+
for (const [name, slot] of declared(screenVariants())) {
|
|
199
|
+
expect(typeof slot(), `slot "${name}"`).toBe("string");
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("resolves to a string at either placement", () => {
|
|
204
|
+
for (const placement of SCREEN_PLACEMENTS) {
|
|
205
|
+
for (const [name, slot] of declared(screenVariants({ placement }))) {
|
|
206
|
+
expect(typeof slot(), `slot "${name}" at placement "${placement}"`).toBe("string");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("screenVariants colour", () => {
|
|
213
|
+
test("keeps text colour off every container", () => {
|
|
214
|
+
const slots = screenVariants();
|
|
215
|
+
for (const slot of CONTAINER_SLOTS) {
|
|
216
|
+
expect(slots[slot]()).not.toMatch(/\btext-/);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("puts a colour on every text slot, since a View cannot cascade one", () => {
|
|
221
|
+
const slots = screenVariants();
|
|
222
|
+
for (const slot of TEXT_SLOTS) {
|
|
223
|
+
expect(slots[slot]()).toMatch(/\btext-(foreground|muted-foreground)\b/);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("leaves the navbar's title and subtitle unaligned, so a stacked pair reads left", () => {
|
|
228
|
+
// The `center` slot centres them by flex; forcing `text-center` here would
|
|
229
|
+
// centre a single line inside a leading column that should read left.
|
|
230
|
+
for (const slot of ["navbarTitle", "navbarSubtitle"] as const) {
|
|
231
|
+
expect(screenVariants()[slot]()).not.toMatch(/\btext-(center|right|left)\b/);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("paints the navbar background and its hairline from theme tokens, never a literal", () => {
|
|
236
|
+
expect(screenVariants().navbarBackground()).toContain("bg-background");
|
|
237
|
+
expect(screenVariants().navbarBorder()).toContain("bg-border");
|
|
238
|
+
for (const slot of ["navbarBackground", "navbarBorder"] as const) {
|
|
239
|
+
expect(screenVariants()[slot]()).not.toMatch(/#[0-9a-f]{3,8}|rgba?\(/i);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("clips the navbar background, so the hairline stays inside the navbar's box", () => {
|
|
244
|
+
expect(screenVariants().navbarBackground()).toContain("overflow-hidden");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("screenVariants sizing", () => {
|
|
249
|
+
test("puts every horizontal edge of the screen on one gutter token", () => {
|
|
250
|
+
// The navbar's row, a scrollable's content and the footer's content all
|
|
251
|
+
// line up. A footer button used to sit at 16 while the content above it
|
|
252
|
+
// sat at 20, on every screen with a footer.
|
|
253
|
+
for (const slot of ["navbarRow", "scrollContent", "footerContent"] as const) {
|
|
254
|
+
expect(screenVariants()[slot]()).toContain("screen-gutter");
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("keeps the gutter off the header, whose container already has one", () => {
|
|
259
|
+
// A header adding its own would double it, which is why every call site
|
|
260
|
+
// used to need `px-0` to undo it.
|
|
261
|
+
expect(screenVariants().header()).not.toContain("screen-gutter");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("pads a scrollable on all four sides, not just horizontally", () => {
|
|
265
|
+
// Vertical breathing room is the half that went missing: the animated
|
|
266
|
+
// spacers clear the chrome exactly, leaving content flush against it.
|
|
267
|
+
expect(screenVariants().scrollContent()).toBe("p-screen-gutter");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("sizes the navbar's control row from its token, not a raw utility", () => {
|
|
271
|
+
expect(screenVariants().navbarRow()).toContain("h-navbar-row");
|
|
272
|
+
expect(screenVariants().navbarRow()).not.toMatch(/\bh-\[/);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("lets the navbar's start slot shrink, so a long title truncates rather than pushing the actions out", () => {
|
|
276
|
+
expect(screenVariants().navbarStart()).toContain("min-w-0");
|
|
277
|
+
expect(screenVariants().navbarStart()).toContain("flex-1");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("fills the screen from the root down, so a child's flex-1 has a height to resolve against", () => {
|
|
281
|
+
for (const slot of ["root", "content", "view"] as const) {
|
|
282
|
+
expect(screenVariants()[slot]()).toContain("flex-1");
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("screenVariants className", () => {
|
|
288
|
+
test("merges the caller's class last, so an override wins", () => {
|
|
289
|
+
expect(screenVariants().root({ className: "bg-card" })).toContain("bg-card");
|
|
290
|
+
expect(screenVariants().root({ className: "bg-card" })).not.toContain("bg-background");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("lets a caller re-gutter the header", () => {
|
|
294
|
+
const cls = screenVariants().header({ className: "px-8" });
|
|
295
|
+
expect(cls).toContain("px-8");
|
|
296
|
+
expect(cls).not.toContain("px-screen-gutter");
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("resolveScreenEdgePadding", () => {
|
|
301
|
+
const INSETS = { bottom: 34, left: 0, right: 0, top: 59 };
|
|
302
|
+
// A landscape notch, where the horizontal insets are the ones that matter.
|
|
303
|
+
const LANDSCAPE = { bottom: 21, left: 59, right: 59, top: 0 };
|
|
304
|
+
|
|
305
|
+
test("insets nothing by default — a container is not safe-area aware unless asked", () => {
|
|
306
|
+
expect(resolveScreenEdgePadding(undefined, INSETS)).toEqual({});
|
|
307
|
+
expect(resolveScreenEdgePadding([], INSETS)).toEqual({});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("pads only the edges it was given, and omits the rest entirely", () => {
|
|
311
|
+
// Absent, never 0. A 0 is not a harmless absence — uniwind puts a class's
|
|
312
|
+
// style first and the `style` prop second, and Yoga resolves a longhand
|
|
313
|
+
// edge ahead of the `padding` a class compiles to, treating 0 as defined.
|
|
314
|
+
// Four unconditional zeroes silently erased every side of a caller's p-5.
|
|
315
|
+
expect(resolveScreenEdgePadding(["bottom"], INSETS)).toEqual({ paddingBottom: 34 });
|
|
316
|
+
expect(resolveScreenEdgePadding(["top"], INSETS)).toEqual({ paddingTop: 59 });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("takes each edge's own inset, not one number for all four", () => {
|
|
320
|
+
expect(resolveScreenEdgePadding(SCREEN_EDGES, LANDSCAPE)).toEqual({
|
|
321
|
+
paddingBottom: 21,
|
|
322
|
+
paddingLeft: 59,
|
|
323
|
+
paddingRight: 59,
|
|
324
|
+
paddingTop: 0,
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("names no key at all for an edge left out", () => {
|
|
329
|
+
expect("paddingTop" in resolveScreenEdgePadding(["bottom"], INSETS)).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("does not care what order the edges are named in", () => {
|
|
333
|
+
expect(resolveScreenEdgePadding(["bottom", "top"], INSETS)).toEqual(
|
|
334
|
+
resolveScreenEdgePadding(["top", "bottom"], INSETS)
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("pads an edge once even when it is named twice", () => {
|
|
339
|
+
expect(resolveScreenEdgePadding(["top", "top"], INSETS).paddingTop).toBe(59);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("covers every edge in SCREEN_EDGES", () => {
|
|
343
|
+
for (const edge of SCREEN_EDGES) {
|
|
344
|
+
const padding = resolveScreenEdgePadding([edge], LANDSCAPE);
|
|
345
|
+
const total = Object.values(padding).reduce((sum, value) => sum + value, 0);
|
|
346
|
+
expect(total).toBe(LANDSCAPE[edge]);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
/** A closed keyboard over a 34pt inset, with an overlay footer of 58pt content. */
|
|
352
|
+
const CLOSED = {
|
|
353
|
+
footerHeight: 58,
|
|
354
|
+
footerPlacement: "overlay",
|
|
355
|
+
keyboardHeight: 0,
|
|
356
|
+
keyboardProgress: 0,
|
|
357
|
+
safeAreaBottom: 34,
|
|
358
|
+
} as const;
|
|
359
|
+
|
|
360
|
+
describe("resolveScrollBottomInset", () => {
|
|
361
|
+
test("reserves what the footer COVERS, not what it measured", () => {
|
|
362
|
+
// The measured height is content only. Reserving that alone leaves the last
|
|
363
|
+
// row sitting behind the footer's own chrome — the bug this replaces.
|
|
364
|
+
const reserve = resolveScrollBottomInset({ ...CLOSED, mode: "standard" });
|
|
365
|
+
expect(reserve).toBe(footerOccupancy(58, 34));
|
|
366
|
+
expect(reserve).toBeGreaterThan(58 + 34);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("reserves nothing for a static footer, which took its own space in the flow", () => {
|
|
370
|
+
expect(resolveScrollBottomInset({ ...CLOSED, footerPlacement: "static", mode: "standard" })).toBe(0);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("falls back to the bare safe-area band with no footer mounted", () => {
|
|
374
|
+
expect(resolveScrollBottomInset({ ...CLOSED, footerHeight: 0, mode: "standard" })).toBe(34);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("never invents a footer's chrome when there is no footer", () => {
|
|
378
|
+
const reserve = resolveScrollBottomInset({ ...CLOSED, footerHeight: 0, mode: "standard" });
|
|
379
|
+
expect(reserve).toBeLessThan(SCREEN_FOOTER_PADDING + SCREEN_FLOATING_BOTTOM_GAP + 34);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("adds the keyboard band in standard mode and omits it in keyboard-aware", () => {
|
|
383
|
+
const open = { ...CLOSED, keyboardHeight: -336, keyboardProgress: 1 };
|
|
384
|
+
// `KeyboardAwareScrollView` adds that padding itself; adding it twice
|
|
385
|
+
// leaves a keyboard-sized gap under the content.
|
|
386
|
+
const standard = resolveScrollBottomInset({ ...open, mode: "standard" });
|
|
387
|
+
const aware = resolveScrollBottomInset({ ...open, mode: "keyboard-aware" });
|
|
388
|
+
expect(standard - aware).toBe(336);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("fades the safe-area band out as the keyboard covers it", () => {
|
|
392
|
+
const half = resolveScrollBottomInset({ ...CLOSED, keyboardProgress: 0.5, mode: "keyboard-aware" });
|
|
393
|
+
const shut = resolveScrollBottomInset({ ...CLOSED, mode: "keyboard-aware" });
|
|
394
|
+
expect(shut - half).toBe(17);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("chat mode adds the composer gap and keeps the full safe-area band", () => {
|
|
398
|
+
// The band is correct in BOTH keyboard states for a chat list — it is real
|
|
399
|
+
// padding while closed and list-lift compensation while open.
|
|
400
|
+
const shut = resolveScrollBottomInset({ ...CLOSED, mode: "chat" });
|
|
401
|
+
const open = resolveScrollBottomInset({ ...CLOSED, keyboardHeight: -336, keyboardProgress: 1, mode: "chat" });
|
|
402
|
+
expect(shut).toBe(footerOccupancy(58, 34) + CHAT_COMPOSER_GAP);
|
|
403
|
+
expect(open).toBe(shut);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("chat mode never pads for the keyboard — a chat list is lifted, not padded", () => {
|
|
407
|
+
for (const progress of [0, 0.5, 1]) {
|
|
408
|
+
const reserve = resolveScrollBottomInset({
|
|
409
|
+
...CLOSED,
|
|
410
|
+
keyboardHeight: -336 * progress,
|
|
411
|
+
keyboardProgress: progress,
|
|
412
|
+
mode: "chat",
|
|
413
|
+
});
|
|
414
|
+
expect(reserve).toBe(footerOccupancy(58, 34) + CHAT_COMPOSER_GAP);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("returns a non-negative reserve for every mode and placement", () => {
|
|
419
|
+
for (const mode of SCREEN_SCROLL_INSET_MODES) {
|
|
420
|
+
for (const footerPlacement of SCREEN_PLACEMENTS) {
|
|
421
|
+
for (const footerHeight of [0, 58]) {
|
|
422
|
+
const reserve = resolveScrollBottomInset({ ...CLOSED, footerHeight, footerPlacement, mode });
|
|
423
|
+
expect(reserve).toBeGreaterThanOrEqual(0);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe("resolveScreenViewPadding", () => {
|
|
431
|
+
const BASE = {
|
|
432
|
+
footerHeight: 58,
|
|
433
|
+
footerPlacement: "overlay",
|
|
434
|
+
navbarHeight: 96,
|
|
435
|
+
navbarPlacement: "overlay",
|
|
436
|
+
safeAreaBottom: 34,
|
|
437
|
+
safeAreaTop: 59,
|
|
438
|
+
} as const;
|
|
439
|
+
|
|
440
|
+
test("clears an overlay navbar by its measured height, safe-area band included", () => {
|
|
441
|
+
expect(resolveScreenViewPadding(BASE).paddingTop).toBe(96);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("clears an overlay footer by what it covers, not by what it measured", () => {
|
|
445
|
+
expect(resolveScreenViewPadding(BASE).paddingBottom).toBe(footerOccupancy(58, 34));
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("adds nothing for static chrome, which took its own space in the flow", () => {
|
|
449
|
+
const padding = resolveScreenViewPadding({
|
|
450
|
+
...BASE,
|
|
451
|
+
footerPlacement: "static",
|
|
452
|
+
navbarPlacement: "static",
|
|
453
|
+
});
|
|
454
|
+
expect(padding).toEqual({ paddingBottom: 0, paddingTop: 0 });
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("falls back to the raw safe-area inset on an edge with no chrome", () => {
|
|
458
|
+
const padding = resolveScreenViewPadding({ ...BASE, footerHeight: 0, navbarHeight: 0 });
|
|
459
|
+
expect(padding).toEqual({ paddingBottom: 34, paddingTop: 59 });
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("agrees with a scrollable's reserve on what an overlay footer covers", () => {
|
|
463
|
+
// The two run in different components; a screen that swaps a scroll area
|
|
464
|
+
// for a static body must not shift its content.
|
|
465
|
+
expect(resolveScreenViewPadding(BASE).paddingBottom).toBe(
|
|
466
|
+
resolveScrollBottomInset({ ...CLOSED, mode: "standard" })
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
describe("resolveNavbarBorderOpacity", () => {
|
|
472
|
+
test("draws the hairline at rest when the fade is off", () => {
|
|
473
|
+
// The default. A screen whose content starts flush against the bar wants
|
|
474
|
+
// the line from the first frame — screens used to re-add one by hand
|
|
475
|
+
// precisely because it faded.
|
|
476
|
+
for (const scrollY of [0, 10, 200]) {
|
|
477
|
+
expect(resolveNavbarBorderOpacity(scrollY, false)).toBe(1);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("starts invisible at rest when the fade is on", () => {
|
|
482
|
+
expect(resolveNavbarBorderOpacity(0, true)).toBe(0);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test("reaches full strength by the fade distance", () => {
|
|
486
|
+
expect(resolveNavbarBorderOpacity(SCREEN_BORDER_FADE_DISTANCE, true)).toBe(1);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("ramps linearly across the distance", () => {
|
|
490
|
+
expect(resolveNavbarBorderOpacity(SCREEN_BORDER_FADE_DISTANCE / 2, true)).toBe(0.5);
|
|
491
|
+
expect(resolveNavbarBorderOpacity(SCREEN_BORDER_FADE_DISTANCE / 4, true)).toBe(0.25);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("clamps past the distance rather than overshooting", () => {
|
|
495
|
+
expect(resolveNavbarBorderOpacity(SCREEN_BORDER_FADE_DISTANCE * 50, true)).toBe(1);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("clamps a rubber-banded overscroll, which reports a negative offset", () => {
|
|
499
|
+
// iOS reports a negative contentOffset while the user drags past the top.
|
|
500
|
+
// Unclamped that drives the opacity below zero, which some backends treat
|
|
501
|
+
// as garbage rather than as transparent.
|
|
502
|
+
for (const scrollY of [-1, -80]) {
|
|
503
|
+
expect(resolveNavbarBorderOpacity(scrollY, true)).toBe(0);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("never leaves the 0…1 range for any offset", () => {
|
|
508
|
+
for (const scrollY of [-500, -20, 0, 1, 19, 20, 21, 5000]) {
|
|
509
|
+
const opacity = resolveNavbarBorderOpacity(scrollY, true);
|
|
510
|
+
expect(opacity).toBeGreaterThanOrEqual(0);
|
|
511
|
+
expect(opacity).toBeLessThanOrEqual(1);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
describe("resolveFooterBorderOpacity", () => {
|
|
517
|
+
/** Scrolled to the middle of content twice the viewport's height. */
|
|
518
|
+
const MIDDLE = { contentHeight: 1600, layoutHeight: 800, scrollY: 400 };
|
|
519
|
+
|
|
520
|
+
test("draws the hairline at rest when the fade is off", () => {
|
|
521
|
+
expect(resolveFooterBorderOpacity({ ...MIDDLE, fadeOnScroll: false })).toBe(1);
|
|
522
|
+
expect(resolveFooterBorderOpacity({ ...MIDDLE, fadeOnScroll: false, scrollY: 800 })).toBe(1);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("reads the FAR end of the scroll, not the near one", () => {
|
|
526
|
+
// The navbar's line asks "is there content above?" and brightens as you
|
|
527
|
+
// leave the top. This one asks "is there content below?", so it is already
|
|
528
|
+
// full at the top and fades as the content runs out — a footer line driven
|
|
529
|
+
// by scrollY would be brightest where there is nothing left to scroll to.
|
|
530
|
+
expect(resolveFooterBorderOpacity({ ...MIDDLE, fadeOnScroll: true, scrollY: 0 })).toBe(1);
|
|
531
|
+
expect(resolveFooterBorderOpacity({ ...MIDDLE, fadeOnScroll: true, scrollY: 800 })).toBe(0);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("fades across the last stretch of the scroll", () => {
|
|
535
|
+
const atEnd = 800;
|
|
536
|
+
expect(
|
|
537
|
+
resolveFooterBorderOpacity({
|
|
538
|
+
...MIDDLE,
|
|
539
|
+
fadeOnScroll: true,
|
|
540
|
+
scrollY: atEnd - SCREEN_BORDER_FADE_DISTANCE / 2,
|
|
541
|
+
})
|
|
542
|
+
).toBe(0.5);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("clamps a rubber-banded overscroll past the end", () => {
|
|
546
|
+
expect(resolveFooterBorderOpacity({ ...MIDDLE, fadeOnScroll: true, scrollY: 900 })).toBe(0);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test("draws the hairline while the content is still unmeasured", () => {
|
|
550
|
+
// Both heights are published by the scrollable's own scroll events, so they
|
|
551
|
+
// are 0 until the first one lands. A screen tall enough to scroll does have
|
|
552
|
+
// content below at that point, and one too short never contradicts it.
|
|
553
|
+
expect(resolveFooterBorderOpacity({ contentHeight: 0, fadeOnScroll: true, layoutHeight: 0, scrollY: 0 })).toBe(1);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("never leaves the 0…1 range", () => {
|
|
557
|
+
for (const scrollY of [-500, 0, 400, 799, 800, 5000]) {
|
|
558
|
+
const opacity = resolveFooterBorderOpacity({ ...MIDDLE, fadeOnScroll: true, scrollY });
|
|
559
|
+
expect(opacity).toBeGreaterThanOrEqual(0);
|
|
560
|
+
expect(opacity).toBeLessThanOrEqual(1);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
describe("resolveScrollTopInset", () => {
|
|
566
|
+
const NAVBAR = { navbarHeight: 96, navbarPlacement: "overlay", safeAreaTop: 59 } as const;
|
|
567
|
+
|
|
568
|
+
test("clears an overlay navbar by its measured height, safe-area band included", () => {
|
|
569
|
+
expect(resolveScrollTopInset(NAVBAR)).toBe(96);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("reserves nothing for a static navbar, which took its own space in the flow", () => {
|
|
573
|
+
expect(resolveScrollTopInset({ ...NAVBAR, navbarPlacement: "static" })).toBe(0);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("falls back to the safe-area inset with no navbar, so content clears the notch", () => {
|
|
577
|
+
// The failure this exists for: a screen that skips the navbar had no top
|
|
578
|
+
// reserve at all and ran its first row under the status bar, while
|
|
579
|
+
// Screen.View on the same screen padded itself correctly.
|
|
580
|
+
expect(resolveScrollTopInset({ ...NAVBAR, navbarHeight: 0 })).toBe(59);
|
|
581
|
+
expect(resolveScrollTopInset({ ...NAVBAR, navbarHeight: 0, navbarPlacement: "static" })).toBe(59);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("agrees with a static body on the same screen", () => {
|
|
585
|
+
// The two run in different components; a screen swapping a scroll area for
|
|
586
|
+
// Screen.View must not shift its content.
|
|
587
|
+
for (const navbarHeight of [0, 96]) {
|
|
588
|
+
for (const navbarPlacement of SCREEN_PLACEMENTS) {
|
|
589
|
+
const scroll = resolveScrollTopInset({ navbarHeight, navbarPlacement, safeAreaTop: 59 });
|
|
590
|
+
const view = resolveScreenViewPadding({
|
|
591
|
+
footerHeight: 0,
|
|
592
|
+
footerPlacement: "overlay",
|
|
593
|
+
navbarHeight,
|
|
594
|
+
navbarPlacement,
|
|
595
|
+
safeAreaBottom: 34,
|
|
596
|
+
safeAreaTop: 59,
|
|
597
|
+
});
|
|
598
|
+
expect(scroll).toBe(view.paddingTop);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
});
|