@delacour/react-native-ui 0.1.0-alpha.3
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 +192 -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 +435 -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 +232 -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 +245 -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 +148 -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 +95 -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 +276 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +199 -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 +100 -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 +33 -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 +78 -0
- package/src/components/field/field.tsx +144 -0
- package/src/components/field/field.types.ts +15 -0
- package/src/components/field/field.variants.test.ts +216 -0
- package/src/components/field/field.variants.ts +136 -0
- package/src/components/field/index.ts +21 -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 +64 -0
- package/src/components/pressable/index.ts +9 -0
- package/src/components/pressable/pressable.tsx +256 -0
- package/src/components/pressable/pressable.variants.test.ts +99 -0
- package/src/components/pressable/pressable.variants.ts +63 -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 +247 -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 +581 -0
- package/src/components/radio/radio.variants.ts +272 -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 +48 -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 +161 -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 +804 -0
- package/src/components/slider/slider.variants.ts +607 -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 +286 -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 +672 -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,570 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { declarationCount } from "../../styles/theme-tokens.test";
|
|
5
|
+
import { ICON_SIZES } from "../icon/icon.variants";
|
|
6
|
+
import {
|
|
7
|
+
hasThumbChild,
|
|
8
|
+
resolveSwitchAxes,
|
|
9
|
+
resolveSwitchContentTreatment,
|
|
10
|
+
resolveSwitchRelease,
|
|
11
|
+
resolveSwitchThumbTokens,
|
|
12
|
+
resolveSwitchTrackTokens,
|
|
13
|
+
SWITCH_COLORS,
|
|
14
|
+
SWITCH_CONTENT_ICON_STEP,
|
|
15
|
+
SWITCH_CONTENT_REST_TEXT_CLASS,
|
|
16
|
+
SWITCH_CONTENT_REST_TOKEN,
|
|
17
|
+
SWITCH_CONTENT_TEXT_CLASS,
|
|
18
|
+
SWITCH_DEFAULT_COLOR,
|
|
19
|
+
SWITCH_DEFAULT_SIZE,
|
|
20
|
+
SWITCH_FLING_VELOCITY,
|
|
21
|
+
SWITCH_INVALID_CONTENT_TEXT_CLASS,
|
|
22
|
+
SWITCH_INVALID_THUMB_TOKEN,
|
|
23
|
+
SWITCH_INVALID_TRACK_TOKEN,
|
|
24
|
+
SWITCH_PRESS_ANIMATION,
|
|
25
|
+
SWITCH_SIZES,
|
|
26
|
+
SWITCH_TAP_SLOP,
|
|
27
|
+
SWITCH_THUMB_INSET,
|
|
28
|
+
SWITCH_THUMB_REST_TOKEN,
|
|
29
|
+
SWITCH_THUMB_SPRING,
|
|
30
|
+
SWITCH_THUMB_TOKEN,
|
|
31
|
+
SWITCH_TRACK_REST_TOKEN,
|
|
32
|
+
SWITCH_TRACK_TOKEN,
|
|
33
|
+
switchTravel,
|
|
34
|
+
switchVariants,
|
|
35
|
+
} from "./switch.variants";
|
|
36
|
+
|
|
37
|
+
const TOKENS_CSS = readFileSync(join(import.meta.dirname, "../../styles/tokens.css"), "utf-8");
|
|
38
|
+
|
|
39
|
+
/** The minimum a touch target may be, in points. Apple's number, and Android's. */
|
|
40
|
+
const MINIMUM_TARGET = 44;
|
|
41
|
+
|
|
42
|
+
/** Tailwind's spacing step, in points — `h-6` is six of them. */
|
|
43
|
+
const SPACING_STEP = 4;
|
|
44
|
+
|
|
45
|
+
/** A `--spacing-*` token's value in points, read from `tokens.css`. */
|
|
46
|
+
function spacingPx(token: string): number {
|
|
47
|
+
const match = TOKENS_CSS.match(new RegExp(`--spacing-${token}:\\s*(\\d+)px`));
|
|
48
|
+
if (!match) throw new Error(`tokens.css declares no --spacing-${token}`);
|
|
49
|
+
return Number(match[1]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A slot's class string, with `tv`'s empty-slot `undefined` flattened.
|
|
54
|
+
*
|
|
55
|
+
* A slot that emits nothing returns `undefined` rather than `""` — correct, but
|
|
56
|
+
* it makes a bare `not.toMatch()` throw rather than pass.
|
|
57
|
+
*/
|
|
58
|
+
function cls(value: string | undefined): string {
|
|
59
|
+
return value ?? "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The points a `h-6` / `py-2.5` / `left-0.5` utility resolves to. */
|
|
63
|
+
function utilityPx(value: string, prefix: string): number {
|
|
64
|
+
const match = value.match(new RegExp(`\\b${prefix}-(\\d+(?:\\.\\d+)?)\\b`));
|
|
65
|
+
if (!match) throw new Error(`no \`${prefix}-*\` in "${value}"`);
|
|
66
|
+
return Number(match[1]) * SPACING_STEP;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Every slot for one size, disabled off. */
|
|
70
|
+
function slotsFor(size: (typeof SWITCH_SIZES)[number]) {
|
|
71
|
+
return switchVariants({ size, isDisabled: false });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("switchVariants defaults", () => {
|
|
75
|
+
test("the tv's defaults are the constants the resolver falls back to", () => {
|
|
76
|
+
// A drift between the two is a switch that renders at one size and reports
|
|
77
|
+
// another, which nothing on screen would reveal.
|
|
78
|
+
expect(cls(switchVariants({}).track())).toBe(cls(slotsFor(SWITCH_DEFAULT_SIZE).track()));
|
|
79
|
+
expect(resolveSwitchAxes({}).size).toBe(SWITCH_DEFAULT_SIZE);
|
|
80
|
+
expect(resolveSwitchAxes({}).color).toBe(SWITCH_DEFAULT_COLOR);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("switch geometry", () => {
|
|
85
|
+
test("every content step is a size the icon scale has, and fits inside the knob", () => {
|
|
86
|
+
for (const size of SWITCH_SIZES) {
|
|
87
|
+
const step = SWITCH_CONTENT_ICON_STEP[size];
|
|
88
|
+
expect(ICON_SIZES).toContain(step);
|
|
89
|
+
// A glyph in a knob, not a second knob.
|
|
90
|
+
expect(spacingPx(`icon-${step}`)).toBeLessThan(utilityPx(cls(slotsFor(size).thumb()), "h"));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("the thumb is wider than it is tall at every size", () => {
|
|
95
|
+
// The shape of the control: a rounded rectangle lying on its side, not a
|
|
96
|
+
// disc. A size that came out square would be a size nobody noticed.
|
|
97
|
+
for (const size of SWITCH_SIZES) {
|
|
98
|
+
const thumb = cls(slotsFor(size).thumb());
|
|
99
|
+
expect(utilityPx(thumb, "w")).toBeGreaterThan(utilityPx(thumb, "h"));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("the thumb is as wide as the track is tall", () => {
|
|
104
|
+
for (const size of SWITCH_SIZES) {
|
|
105
|
+
const slots = slotsFor(size);
|
|
106
|
+
expect(utilityPx(cls(slots.thumb()), "w")).toBe(utilityPx(cls(slots.track()), "h"));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("the thumb's height is the track's less twice the vertical inset", () => {
|
|
111
|
+
// Never written as a class — the track is `justify-center`, so the parent
|
|
112
|
+
// centres it. This is the relationship that inset actually describes.
|
|
113
|
+
for (const size of SWITCH_SIZES) {
|
|
114
|
+
const slots = slotsFor(size);
|
|
115
|
+
const track = utilityPx(cls(slots.track()), "h");
|
|
116
|
+
expect(utilityPx(cls(slots.thumb()), "h")).toBe(track - SWITCH_THUMB_INSET * 2);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("the two capsules are concentric", () => {
|
|
121
|
+
// Both are `rounded-full`, so each radius is half its own height and the
|
|
122
|
+
// difference is the vertical inset — the subtraction `Checkbox`'s fill
|
|
123
|
+
// makes against its border, arrived at by construction rather than a number.
|
|
124
|
+
for (const size of SWITCH_SIZES) {
|
|
125
|
+
const slots = slotsFor(size);
|
|
126
|
+
expect(cls(slots.track())).toMatch(/\brounded-full\b/);
|
|
127
|
+
expect(cls(slots.thumb())).toMatch(/\brounded-full\b/);
|
|
128
|
+
|
|
129
|
+
const trackRadius = utilityPx(cls(slots.track()), "h") / 2;
|
|
130
|
+
const thumbRadius = utilityPx(cls(slots.thumb()), "h") / 2;
|
|
131
|
+
expect(trackRadius - thumbRadius).toBe(SWITCH_THUMB_INSET);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("the horizontal inset written into the slots is the one the travel subtracts", () => {
|
|
136
|
+
// The maths takes it off twice. A slot and the constant disagreeing is a
|
|
137
|
+
// thumb that stops a point short of the far edge, at every size.
|
|
138
|
+
for (const size of SWITCH_SIZES) {
|
|
139
|
+
const slots = slotsFor(size);
|
|
140
|
+
expect(utilityPx(cls(slots.thumb()), "left")).toBe(SWITCH_THUMB_INSET);
|
|
141
|
+
expect(utilityPx(cls(slots.startContent()), "left")).toBe(SWITCH_THUMB_INSET);
|
|
142
|
+
expect(utilityPx(cls(slots.endContent()), "right")).toBe(SWITCH_THUMB_INSET);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("the thumb travels the length the track leaves it, and it is a real distance", () => {
|
|
147
|
+
for (const size of SWITCH_SIZES) {
|
|
148
|
+
const slots = slotsFor(size);
|
|
149
|
+
const track = cls(slots.track());
|
|
150
|
+
const thumbWidth = utilityPx(cls(slots.thumb()), "w");
|
|
151
|
+
const travel = switchTravel({
|
|
152
|
+
inset: SWITCH_THUMB_INSET,
|
|
153
|
+
thumbWidth,
|
|
154
|
+
trackWidth: utilityPx(track, "w"),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(travel).toBe(utilityPx(track, "w") - thumbWidth - SWITCH_THUMB_INSET * 2);
|
|
158
|
+
// Short enough and the drag has no room; longer than the thumb and the
|
|
159
|
+
// knob reads as lost in the track rather than filling it.
|
|
160
|
+
expect(travel).toBeGreaterThan(0);
|
|
161
|
+
expect(travel).toBeLessThanOrEqual(thumbWidth);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("the track and its touch padding sum to a real target at every size", () => {
|
|
166
|
+
// The sum, not the parts, so the ladder can be retuned without the test
|
|
167
|
+
// becoming a transcript of it — and so a shorter track cannot silently
|
|
168
|
+
// shrink the target.
|
|
169
|
+
for (const size of SWITCH_SIZES) {
|
|
170
|
+
const slots = slotsFor(size);
|
|
171
|
+
const height = utilityPx(cls(slots.track()), "h");
|
|
172
|
+
const padding = utilityPx(cls(slots.touchArea()), "py");
|
|
173
|
+
expect(height + padding * 2).toBe(MINIMUM_TARGET);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("the glyph slot is the step the map names", () => {
|
|
178
|
+
// Two places name it — the slot, because Tailwind's scanner is static, and
|
|
179
|
+
// the map, because the comparison above needs a value.
|
|
180
|
+
for (const size of SWITCH_SIZES) {
|
|
181
|
+
expect(cls(slotsFor(size).glyph())).toBe(`size-icon-${SWITCH_CONTENT_ICON_STEP[size]}`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("a content layer is exactly the space the knob vacates", () => {
|
|
186
|
+
// The travel, not the thumb's width. Size them like the knob and the two
|
|
187
|
+
// layers plus the thumb are wider than the track: the far layer reaches
|
|
188
|
+
// under the knob, and its text is clipped by a knob drawn on top of it.
|
|
189
|
+
for (const size of SWITCH_SIZES) {
|
|
190
|
+
const slots = slotsFor(size);
|
|
191
|
+
const thumb = cls(slots.thumb());
|
|
192
|
+
const travel = switchTravel({
|
|
193
|
+
inset: SWITCH_THUMB_INSET,
|
|
194
|
+
thumbWidth: utilityPx(thumb, "w"),
|
|
195
|
+
trackWidth: utilityPx(cls(slots.track()), "w"),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
for (const layer of [cls(slots.startContent()), cls(slots.endContent())]) {
|
|
199
|
+
expect(utilityPx(layer, "w")).toBe(travel);
|
|
200
|
+
// The knob's height, so a glyph at either end sits on its centre line.
|
|
201
|
+
expect(utilityPx(layer, "h")).toBe(utilityPx(thumb, "h"));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("a resting thumb never overlaps the layer at the other end", () => {
|
|
207
|
+
// The one that has to hold: the visible layer is always the far one, and a
|
|
208
|
+
// knob reaching into it clips whatever it holds — a glyph is small enough to
|
|
209
|
+
// survive that, so it only shows up at the size where the text is longest.
|
|
210
|
+
// The near layer is under the knob by design, and faded to nothing anyway.
|
|
211
|
+
for (const size of SWITCH_SIZES) {
|
|
212
|
+
const slots = slotsFor(size);
|
|
213
|
+
const trackWidth = utilityPx(cls(slots.track()), "w");
|
|
214
|
+
const thumbWidth = utilityPx(cls(slots.thumb()), "w");
|
|
215
|
+
const layerWidth = utilityPx(cls(slots.startContent()), "w");
|
|
216
|
+
|
|
217
|
+
const thumbFarEdge = SWITCH_THUMB_INSET + thumbWidth;
|
|
218
|
+
const layerNearEdge = trackWidth - SWITCH_THUMB_INSET - layerWidth;
|
|
219
|
+
expect(thumbFarEdge).toBeLessThanOrEqual(layerNearEdge);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("the size ladder ascends", () => {
|
|
224
|
+
const tracks = SWITCH_SIZES.map((size) => utilityPx(cls(slotsFor(size).track()), "h"));
|
|
225
|
+
expect(tracks).toEqual([...tracks].sort((a, b) => a - b));
|
|
226
|
+
expect(new Set(tracks).size).toBe(tracks.length);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("switchVariants slots", () => {
|
|
231
|
+
test("the tv describes no colour beyond the resting appearance", () => {
|
|
232
|
+
// Every colour on this control is interpolated, so a `bg-*` per colour here
|
|
233
|
+
// would be a second source for one surface — the drift a class and a style
|
|
234
|
+
// produce for a frame on every toggle.
|
|
235
|
+
for (const size of SWITCH_SIZES) {
|
|
236
|
+
const slots = slotsFor(size);
|
|
237
|
+
expect(cls(slots.track())).toMatch(/\bbg-input\b/);
|
|
238
|
+
expect(cls(slots.thumb())).toMatch(/\bbg-background\b/);
|
|
239
|
+
expect(cls(slots.track())).not.toMatch(/\bbg-(primary|success|warning|destructive|info)\b/);
|
|
240
|
+
expect(cls(slots.thumb())).not.toMatch(/\bbg-(primary|success|warning|destructive|info)\b/);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("no slot carries a text treatment", () => {
|
|
245
|
+
// Every slot is worn by a View, and a View does not cascade colour to a
|
|
246
|
+
// Text descendant. A glyph's colour is a token, not a class. Rule 1.
|
|
247
|
+
for (const size of SWITCH_SIZES) {
|
|
248
|
+
const slots = slotsFor(size);
|
|
249
|
+
for (const value of [slots.touchArea(), slots.track(), slots.thumb(), slots.startContent(), slots.endContent()]) {
|
|
250
|
+
expect(cls(value)).not.toMatch(/\b(text|font)-/);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("every slot that can hold content centres it", () => {
|
|
256
|
+
// A View lays a child out at flex-start by default, so a glyph dropped into
|
|
257
|
+
// an uncentred knob sits against its top-left corner inside a circle — which
|
|
258
|
+
// reads as a mispositioned icon rather than as a missing class.
|
|
259
|
+
for (const size of SWITCH_SIZES) {
|
|
260
|
+
const slots = slotsFor(size);
|
|
261
|
+
for (const value of [slots.thumb(), slots.startContent(), slots.endContent()]) {
|
|
262
|
+
expect(cls(value)).toMatch(/\bitems-center\b/);
|
|
263
|
+
expect(cls(value)).toMatch(/\bjustify-center\b/);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("the track clips, so composed content cannot paint over the capsule's edge", () => {
|
|
269
|
+
for (const size of SWITCH_SIZES) {
|
|
270
|
+
expect(cls(slotsFor(size).track())).toMatch(/\boverflow-hidden\b/);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("the touch area does not stretch", () => {
|
|
275
|
+
// Without this a switch in a gap column spans the screen and a tap far from
|
|
276
|
+
// the pill toggles it.
|
|
277
|
+
for (const size of SWITCH_SIZES) {
|
|
278
|
+
expect(cls(slotsFor(size).touchArea())).toMatch(/\bself-start\b/);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("neither animated node carries the disabled fade", () => {
|
|
283
|
+
// `track` and `thumb` both run a useAnimatedStyle. A treatment that would
|
|
284
|
+
// break the moment one of them wrote `opacity` is one waiting to fail
|
|
285
|
+
// silently — the failure Radio records for a class on Pressable's own node.
|
|
286
|
+
for (const size of SWITCH_SIZES) {
|
|
287
|
+
const slots = switchVariants({ size, isDisabled: true });
|
|
288
|
+
expect(cls(slots.touchArea())).toMatch(/\bopacity-50\b/);
|
|
289
|
+
expect(cls(slots.track())).not.toMatch(/\bopacity-/);
|
|
290
|
+
expect(cls(slots.thumb())).not.toMatch(/\bopacity-/);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("the knob draws no border", () => {
|
|
295
|
+
// A second line where there is already a boundary. Against a saturated track
|
|
296
|
+
// it reads as a dark ring rather than as definition.
|
|
297
|
+
for (const size of SWITCH_SIZES) {
|
|
298
|
+
expect(cls(slotsFor(size).thumb())).not.toMatch(/\bborder\b|\bborder-/);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("nothing draws a shadow", () => {
|
|
303
|
+
// Nothing else in this package does, and React Native's shadow props
|
|
304
|
+
// diverge between platforms in a way a one-pixel border does not.
|
|
305
|
+
for (const size of SWITCH_SIZES) {
|
|
306
|
+
const slots = slotsFor(size);
|
|
307
|
+
for (const value of [slots.track(), slots.thumb()]) {
|
|
308
|
+
expect(cls(value)).not.toMatch(/\bshadow/);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("switch colour tokens", () => {
|
|
315
|
+
test("every thumb token is the foreground of its own track token", () => {
|
|
316
|
+
// Two maps that can drift is how a switch ends up with a pale knob on a
|
|
317
|
+
// near-white track. `default` is the exception the theme forces: there is
|
|
318
|
+
// no `--foreground-foreground`.
|
|
319
|
+
for (const color of SWITCH_COLORS) {
|
|
320
|
+
if (color === "default") {
|
|
321
|
+
expect(SWITCH_TRACK_TOKEN[color]).toBe("foreground");
|
|
322
|
+
expect(SWITCH_THUMB_TOKEN[color]).toBe("background");
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
expect(SWITCH_THUMB_TOKEN[color]).toBe(`${SWITCH_TRACK_TOKEN[color]}-foreground`);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("every token named is declared in both variants of theme.css", () => {
|
|
330
|
+
// A token no theme emits resolves to undefined, is dropped, and the surface
|
|
331
|
+
// silently keeps whatever it started as.
|
|
332
|
+
const tokens = [
|
|
333
|
+
...Object.values(SWITCH_TRACK_TOKEN),
|
|
334
|
+
...Object.values(SWITCH_THUMB_TOKEN),
|
|
335
|
+
SWITCH_TRACK_REST_TOKEN,
|
|
336
|
+
SWITCH_THUMB_REST_TOKEN,
|
|
337
|
+
SWITCH_INVALID_TRACK_TOKEN,
|
|
338
|
+
SWITCH_INVALID_THUMB_TOKEN,
|
|
339
|
+
SWITCH_CONTENT_REST_TOKEN,
|
|
340
|
+
];
|
|
341
|
+
for (const token of tokens) expect(declarationCount(token)).toBe(2);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("the four semantic colours stay distinct from each other and from the neutrals", () => {
|
|
345
|
+
// Two cells collapsing means a caller can set the axis and see nothing move.
|
|
346
|
+
const tracks = SWITCH_COLORS.map((color) => SWITCH_TRACK_TOKEN[color]);
|
|
347
|
+
expect(new Set(tracks).size).toBe(SWITCH_COLORS.length);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("the track and the thumb never resolve to the same token", () => {
|
|
351
|
+
for (const color of SWITCH_COLORS) {
|
|
352
|
+
expect(SWITCH_THUMB_TOKEN[color]).not.toBe(SWITCH_TRACK_TOKEN[color]);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe("resolveSwitchTrackTokens / resolveSwitchThumbTokens", () => {
|
|
358
|
+
test("an off switch wears the field chrome at every colour", () => {
|
|
359
|
+
for (const color of SWITCH_COLORS) {
|
|
360
|
+
expect(resolveSwitchTrackTokens({ color, isInvalid: false }).rest).toBe(SWITCH_TRACK_REST_TOKEN);
|
|
361
|
+
expect(resolveSwitchThumbTokens({ color, isInvalid: false }).rest).toBe(SWITCH_THUMB_REST_TOKEN);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("an on switch fades to its own colour", () => {
|
|
366
|
+
for (const color of SWITCH_COLORS) {
|
|
367
|
+
expect(resolveSwitchTrackTokens({ color, isInvalid: false }).active).toBe(SWITCH_TRACK_TOKEN[color]);
|
|
368
|
+
expect(resolveSwitchThumbTokens({ color, isInvalid: false }).active).toBe(SWITCH_THUMB_TOKEN[color]);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("invalid outranks the colour at both ends, so there is nothing to fade", () => {
|
|
373
|
+
// The colour is the signal the value is wrong, and it has to be there before
|
|
374
|
+
// the switch is turned on as much as after.
|
|
375
|
+
for (const color of SWITCH_COLORS) {
|
|
376
|
+
const track = resolveSwitchTrackTokens({ color, isInvalid: true });
|
|
377
|
+
const thumb = resolveSwitchThumbTokens({ color, isInvalid: true });
|
|
378
|
+
expect(track.rest).toBe(SWITCH_INVALID_TRACK_TOKEN);
|
|
379
|
+
expect(track.active).toBe(SWITCH_INVALID_TRACK_TOKEN);
|
|
380
|
+
expect(thumb.rest).toBe(SWITCH_INVALID_THUMB_TOKEN);
|
|
381
|
+
expect(thumb.active).toBe(SWITCH_INVALID_THUMB_TOKEN);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe("resolveSwitchAxes", () => {
|
|
387
|
+
test("the switch's own props win over the field", () => {
|
|
388
|
+
const axes = resolveSwitchAxes({
|
|
389
|
+
own: { color: "success", isDisabled: true, isInvalid: false, size: "lg" },
|
|
390
|
+
field: { isDisabled: false, isInvalid: true },
|
|
391
|
+
});
|
|
392
|
+
expect(axes).toEqual({ color: "success", size: "lg", isDisabled: true, isInvalid: false });
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("a field reaches the state axes when the switch names neither", () => {
|
|
396
|
+
const axes = resolveSwitchAxes({ field: { isDisabled: true, isInvalid: true } });
|
|
397
|
+
expect(axes.isDisabled).toBe(true);
|
|
398
|
+
expect(axes.isInvalid).toBe(true);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("an explicit false opts out of a disabled field", () => {
|
|
402
|
+
// `??` and never `||`, so `false` is a value rather than an absence.
|
|
403
|
+
const axes = resolveSwitchAxes({ own: { isDisabled: false }, field: { isDisabled: true } });
|
|
404
|
+
expect(axes.isDisabled).toBe(false);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("a field cannot acquire a paint axis", () => {
|
|
408
|
+
const axes = resolveSwitchAxes({ field: { isDisabled: true, isInvalid: true } });
|
|
409
|
+
expect(axes.color).toBe(SWITCH_DEFAULT_COLOR);
|
|
410
|
+
expect(axes.size).toBe(SWITCH_DEFAULT_SIZE);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("nothing named anywhere settles on the defaults", () => {
|
|
414
|
+
expect(resolveSwitchAxes({ own: {}, field: null })).toEqual({
|
|
415
|
+
color: SWITCH_DEFAULT_COLOR,
|
|
416
|
+
size: SWITCH_DEFAULT_SIZE,
|
|
417
|
+
isDisabled: false,
|
|
418
|
+
isInvalid: false,
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe("switchTravel", () => {
|
|
424
|
+
test("a track that has not been measured yet has no travel, never a negative one", () => {
|
|
425
|
+
expect(switchTravel({ trackWidth: 0, thumbWidth: 0, inset: SWITCH_THUMB_INSET })).toBe(0);
|
|
426
|
+
expect(switchTravel({ trackWidth: 0, thumbWidth: 24, inset: SWITCH_THUMB_INSET })).toBe(0);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("the inset comes off both ends", () => {
|
|
430
|
+
expect(switchTravel({ trackWidth: 52, thumbWidth: 24, inset: 2 })).toBe(24);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe("resolveSwitchRelease", () => {
|
|
435
|
+
test("a release that barely moved is a tap, and toggles", () => {
|
|
436
|
+
// This is what lets one Gesture.Pan() serve both gestures rather than
|
|
437
|
+
// racing a Tap against it.
|
|
438
|
+
expect(resolveSwitchRelease({ progress: 0, distance: 0, velocity: 0, wasSelected: false })).toBe(true);
|
|
439
|
+
expect(resolveSwitchRelease({ progress: 1, distance: 0, velocity: 0, wasSelected: true })).toBe(false);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("a vertical swipe over the switch is not a tap and changes nothing", () => {
|
|
443
|
+
// The finger moved nothing along the track, so the position is untouched —
|
|
444
|
+
// but it did move, so reading the along-track translation alone would have
|
|
445
|
+
// called this a tap and toggled on every attempt to scroll past.
|
|
446
|
+
const scroll = SWITCH_TAP_SLOP + 30;
|
|
447
|
+
expect(resolveSwitchRelease({ progress: 0, distance: scroll, velocity: 0, wasSelected: false })).toBe(false);
|
|
448
|
+
expect(resolveSwitchRelease({ progress: 1, distance: scroll, velocity: 0, wasSelected: true })).toBe(true);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("the tap slop is a tolerance, and a hair past it is not a tap", () => {
|
|
452
|
+
expect(resolveSwitchRelease({ progress: 0, distance: SWITCH_TAP_SLOP - 1, velocity: 0, wasSelected: false })).toBe(
|
|
453
|
+
true
|
|
454
|
+
);
|
|
455
|
+
expect(resolveSwitchRelease({ progress: 0, distance: SWITCH_TAP_SLOP, velocity: 0, wasSelected: false })).toBe(
|
|
456
|
+
false
|
|
457
|
+
);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("a drag past the slop is settled by position, not by the state it started in", () => {
|
|
461
|
+
const past = SWITCH_TAP_SLOP + 1;
|
|
462
|
+
expect(resolveSwitchRelease({ progress: 0.2, distance: past, velocity: 0, wasSelected: false })).toBe(false);
|
|
463
|
+
expect(resolveSwitchRelease({ progress: 0.8, distance: past, velocity: 0, wasSelected: false })).toBe(true);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("a drag out and back commits nothing", () => {
|
|
467
|
+
// It travelled, so it is not a tap; it came back, so position says off.
|
|
468
|
+
expect(resolveSwitchRelease({ progress: 0.05, distance: 40, velocity: 0, wasSelected: false })).toBe(false);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("a flick beats the position it stopped at", () => {
|
|
472
|
+
// The one case where position is the wrong question: let go fast enough and
|
|
473
|
+
// the switch goes the way the finger was going.
|
|
474
|
+
const fling = SWITCH_FLING_VELOCITY + 1;
|
|
475
|
+
expect(resolveSwitchRelease({ progress: 0.1, distance: 10, velocity: fling, wasSelected: false })).toBe(true);
|
|
476
|
+
expect(resolveSwitchRelease({ progress: 0.9, distance: 10, velocity: -fling, wasSelected: true })).toBe(false);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("a slow release below the fling threshold is left to the position", () => {
|
|
480
|
+
const slow = SWITCH_FLING_VELOCITY - 1;
|
|
481
|
+
expect(resolveSwitchRelease({ progress: 0.1, distance: 10, velocity: slow, wasSelected: false })).toBe(false);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("exactly half way settles off, so the midpoint is not a coin toss", () => {
|
|
485
|
+
expect(resolveSwitchRelease({ progress: 0.5, distance: 20, velocity: 0, wasSelected: false })).toBe(false);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
describe("hasThumbChild", () => {
|
|
490
|
+
test("no children at all means the root composes one in", () => {
|
|
491
|
+
expect(hasThumbChild([])).toBe(false);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("content parts alone are not a thumb", () => {
|
|
495
|
+
expect(hasThumbChild([false, false])).toBe(false);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("a thumb anywhere among the children is the caller's own", () => {
|
|
499
|
+
expect(hasThumbChild([false, true, false])).toBe(true);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
describe("SWITCH_THUMB_SPRING", () => {
|
|
504
|
+
test("it does not overshoot", () => {
|
|
505
|
+
// A wider thumb travels a shorter distance inside a track that clips it, so
|
|
506
|
+
// an overshoot has nowhere to go — the knob would visibly squash against the
|
|
507
|
+
// end of its own capsule on every toggle.
|
|
508
|
+
const critical = 2 * Math.sqrt(SWITCH_THUMB_SPRING.stiffness * SWITCH_THUMB_SPRING.mass);
|
|
509
|
+
expect(SWITCH_THUMB_SPRING.damping / critical).toBeGreaterThanOrEqual(1);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("it is not so overdamped that it crawls", () => {
|
|
513
|
+
const critical = 2 * Math.sqrt(SWITCH_THUMB_SPRING.stiffness * SWITCH_THUMB_SPRING.mass);
|
|
514
|
+
expect(SWITCH_THUMB_SPRING.damping / critical).toBeLessThan(1.3);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
describe("SWITCH_PRESS_ANIMATION", () => {
|
|
519
|
+
test("a press shrinks the control without collapsing it", () => {
|
|
520
|
+
expect(SWITCH_PRESS_ANIMATION.restScale).toBe(1);
|
|
521
|
+
expect(SWITCH_PRESS_ANIMATION.pressedScale).toBeLessThan(1);
|
|
522
|
+
expect(SWITCH_PRESS_ANIMATION.pressedScale).toBeGreaterThan(0.9);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
describe("content treatment", () => {
|
|
527
|
+
test("only the slot handed to a TextClassProvider carries a text treatment", () => {
|
|
528
|
+
// It is never worn by a View, which is the condition rule 1 sets — the five
|
|
529
|
+
// View slots are swept for its absence above.
|
|
530
|
+
for (const size of SWITCH_SIZES) {
|
|
531
|
+
const value = cls(switchVariants({ size, isDisabled: false }).contentText());
|
|
532
|
+
expect(value).toMatch(/\btext-/);
|
|
533
|
+
expect(value).toMatch(/\bfont-/);
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test("every content text class names the token the glyph beside it uses", () => {
|
|
538
|
+
// Tailwind's scanner is static, so this cannot be derived at runtime — which
|
|
539
|
+
// makes two tables that could drift. The pin is what stops them.
|
|
540
|
+
for (const color of SWITCH_COLORS) {
|
|
541
|
+
expect(SWITCH_CONTENT_TEXT_CLASS[color]).toBe(`text-${SWITCH_THUMB_TOKEN[color]}`);
|
|
542
|
+
}
|
|
543
|
+
expect(SWITCH_CONTENT_REST_TEXT_CLASS).toBe(`text-${SWITCH_CONTENT_REST_TOKEN}`);
|
|
544
|
+
expect(SWITCH_INVALID_CONTENT_TEXT_CLASS).toBe(`text-${SWITCH_INVALID_THUMB_TOKEN}`);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("the start layer is drawn on the coloured track, the end layer on the resting one", () => {
|
|
548
|
+
// Start is revealed when the switch is on; end when it is off. The surface
|
|
549
|
+
// each sits on is the whole of the decision.
|
|
550
|
+
for (const color of SWITCH_COLORS) {
|
|
551
|
+
const start = resolveSwitchContentTreatment({ color, isInvalid: false, placement: "start" });
|
|
552
|
+
const end = resolveSwitchContentTreatment({ color, isInvalid: false, placement: "end" });
|
|
553
|
+
|
|
554
|
+
expect(start.color).toBe(SWITCH_THUMB_TOKEN[color]);
|
|
555
|
+
expect(start.textClass).toBe(SWITCH_CONTENT_TEXT_CLASS[color]);
|
|
556
|
+
expect(end.color).toBe(SWITCH_CONTENT_REST_TOKEN);
|
|
557
|
+
expect(end.textClass).toBe(SWITCH_CONTENT_REST_TEXT_CLASS);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test("invalid outranks the colour at both ends", () => {
|
|
562
|
+
for (const color of SWITCH_COLORS) {
|
|
563
|
+
for (const placement of ["start", "end"] as const) {
|
|
564
|
+
const treatment = resolveSwitchContentTreatment({ color, isInvalid: true, placement });
|
|
565
|
+
expect(treatment.color).toBe(SWITCH_INVALID_THUMB_TOKEN);
|
|
566
|
+
expect(treatment.textClass).toBe(SWITCH_INVALID_CONTENT_TEXT_CLASS);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
});
|