@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,634 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { declaredTokens, RADIUS_BASE_PX, radiusMultiplier, radiusPx } from "../../styles/theme-tokens.test";
|
|
3
|
+
import { ICON_SIZE_TOKENS } from "../../styles/tokens";
|
|
4
|
+
import { TEXT_SIZES } from "../text/text.variants";
|
|
5
|
+
import {
|
|
6
|
+
CHECKBOX_ALIGNMENTS,
|
|
7
|
+
CHECKBOX_BORDER_WIDTH,
|
|
8
|
+
CHECKBOX_COLORS,
|
|
9
|
+
CHECKBOX_GLYPH_TOKEN,
|
|
10
|
+
CHECKBOX_HIT_SLOP,
|
|
11
|
+
CHECKBOX_INDICATOR_ANIMATION,
|
|
12
|
+
CHECKBOX_INVALID_BORDER_TOKEN,
|
|
13
|
+
CHECKBOX_INVALID_GLYPH_TOKEN,
|
|
14
|
+
CHECKBOX_RADIUS_MULTIPLIER,
|
|
15
|
+
CHECKBOX_RADIUS_STEP,
|
|
16
|
+
CHECKBOX_REST_BORDER_TOKEN,
|
|
17
|
+
CHECKBOX_SIZES,
|
|
18
|
+
CHECKBOX_SURFACE_TOKEN,
|
|
19
|
+
checkboxVariants,
|
|
20
|
+
resolveCheckboxAxes,
|
|
21
|
+
resolveCheckboxBorderTokens,
|
|
22
|
+
resolveCheckboxFilled,
|
|
23
|
+
resolveCheckboxFillRadius,
|
|
24
|
+
resolveCheckboxHitSlop,
|
|
25
|
+
resolveCheckboxLabelColor,
|
|
26
|
+
resolveCheckboxLabelSize,
|
|
27
|
+
toggleCheckedValue,
|
|
28
|
+
} from "./checkbox.variants";
|
|
29
|
+
|
|
30
|
+
const LIGHT = declaredTokens("light");
|
|
31
|
+
const DARK = declaredTokens("dark");
|
|
32
|
+
|
|
33
|
+
/** Position of a class string's `size-icon-*` token on the shared icon scale. */
|
|
34
|
+
function sizeStep(cls: string): number {
|
|
35
|
+
const token = cls.match(/\bsize-(icon-[\w-]+)\b/)?.[1];
|
|
36
|
+
return ICON_SIZE_TOKENS.indexOf(token as (typeof ICON_SIZE_TOKENS)[number]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Tailwind's spacing step a class string sets its gap from. */
|
|
40
|
+
function gapOf(cls: string): number {
|
|
41
|
+
return Number(cls.match(/\bgap-([\d.]+)\b/)?.[1]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The `bg-*` token a class string paints its surface with, minus the prefix. */
|
|
45
|
+
function fillToken(cls: string): string | undefined {
|
|
46
|
+
return cls.match(/\bbg-([\w-]+)\b/)?.[1];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The `border-*` colour token a class string resolves to, minus the prefix. */
|
|
50
|
+
function borderToken(cls: string): string | undefined {
|
|
51
|
+
return cls.match(/\bborder-((?:[a-z]+-)*[a-z]+)\b/)?.[1];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("the theme.css reader", () => {
|
|
55
|
+
// The token assertions below are only worth anything if the parse found something.
|
|
56
|
+
test("finds both variants", () => {
|
|
57
|
+
expect(LIGHT.size).toBeGreaterThan(0);
|
|
58
|
+
expect(DARK.size).toBeGreaterThan(0);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("checkboxVariants box slot", () => {
|
|
63
|
+
test("defaults to the unchecked default md box", () => {
|
|
64
|
+
const cls = checkboxVariants().box();
|
|
65
|
+
expect(cls).toContain("size-icon-lg");
|
|
66
|
+
expect(cls).toContain("border-input");
|
|
67
|
+
expect(cls).toContain("bg-card");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("is square at every size, from the shared icon scale", () => {
|
|
71
|
+
for (const size of CHECKBOX_SIZES) {
|
|
72
|
+
const cls = checkboxVariants({ size }).box();
|
|
73
|
+
// No scale of its own. A checkbox is a glyph in a box and both
|
|
74
|
+
// measurements already live on `--spacing-icon-*`.
|
|
75
|
+
expect(sizeStep(cls)).toBeGreaterThanOrEqual(0);
|
|
76
|
+
// `size-*` and never `w-*` with `h-*`: tailwind-merge conflicts `size`
|
|
77
|
+
// into `w`/`h` but not the reverse, so a caller's `size-8` has to be
|
|
78
|
+
// able to clear whatever is here.
|
|
79
|
+
expect(cls).not.toMatch(/\bw-\d/);
|
|
80
|
+
expect(cls).not.toMatch(/\bh-\d/);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("steps up with the size, on that same scale", () => {
|
|
85
|
+
const steps = CHECKBOX_SIZES.map((size) => sizeStep(checkboxVariants({ size }).box()));
|
|
86
|
+
expect(steps).toEqual([...steps].sort((a, b) => a - b));
|
|
87
|
+
expect(new Set(steps).size).toBe(CHECKBOX_SIZES.length);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("sits exactly two steps above its own glyph, at every size", () => {
|
|
91
|
+
for (const size of CHECKBOX_SIZES) {
|
|
92
|
+
const slots = checkboxVariants({ size });
|
|
93
|
+
// The gap that leaves the tick breathing room inside the box. Pinned as
|
|
94
|
+
// an offset rather than as points, so the icon scale can be retuned
|
|
95
|
+
// without this test becoming a transcript of it.
|
|
96
|
+
expect(sizeStep(slots.box()) - sizeStep(slots.glyph())).toBe(2);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("clips its own fill at every size", () => {
|
|
101
|
+
for (const size of CHECKBOX_SIZES) {
|
|
102
|
+
// The indicator is an absolute layer under a rounded border. Without
|
|
103
|
+
// this it paints square corners over the box's own.
|
|
104
|
+
expect(checkboxVariants({ size }).box()).toContain("overflow-hidden");
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("reserves the border on every colour, filled or not", () => {
|
|
109
|
+
for (const color of CHECKBOX_COLORS) {
|
|
110
|
+
for (const isFilled of [true, false]) {
|
|
111
|
+
// A border declared only where it shows would move the glyph by a
|
|
112
|
+
// point the moment the box was ticked.
|
|
113
|
+
expect(checkboxVariants({ color, isFilled }).box()).toMatch(/\bborder\b/);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("rests on the field's border, at every colour and either state", () => {
|
|
119
|
+
for (const color of CHECKBOX_COLORS) {
|
|
120
|
+
for (const isFilled of [true, false]) {
|
|
121
|
+
// The class is the resting appearance only. The live colour is an
|
|
122
|
+
// interpolated style, because a colour that fades cannot be a class
|
|
123
|
+
// — see `resolveCheckboxBorderTokens`.
|
|
124
|
+
expect(borderToken(checkboxVariants({ color, isFilled }).box())).toBe(CHECKBOX_REST_BORDER_TOKEN);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("names no colour of its own in a class, at any colour", () => {
|
|
130
|
+
for (const color of CHECKBOX_COLORS) {
|
|
131
|
+
// Two sources for one border is how the class and the animated style
|
|
132
|
+
// end up disagreeing for a frame on every toggle.
|
|
133
|
+
expect(borderToken(checkboxVariants({ color, isFilled: true }).box())).not.toBe(color);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("offsets itself onto the label's first line at every size", () => {
|
|
138
|
+
for (const size of CHECKBOX_SIZES) {
|
|
139
|
+
// Half the difference between the label's line box and the box, so
|
|
140
|
+
// `items-start` on the row still reads as centred on one line. Without
|
|
141
|
+
// this the box would sit a point or two high on every single-line
|
|
142
|
+
// checkbox in the library.
|
|
143
|
+
expect(checkboxVariants({ size }).box()).toMatch(/\bmt-(px|[\d.]+)\b/);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("merges an incoming className last", () => {
|
|
148
|
+
expect(checkboxVariants().box({ className: "size-8" })).toContain("size-8");
|
|
149
|
+
expect(checkboxVariants().box({ className: "size-8" })).not.toContain("size-icon-lg");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("checkboxVariants indicator slot", () => {
|
|
154
|
+
test("fills the box past its edges, so its own edge coincides with nothing", () => {
|
|
155
|
+
const cls = checkboxVariants().indicator();
|
|
156
|
+
expect(cls).toContain("absolute");
|
|
157
|
+
// An absolute child is positioned against the padding box, so `inset-0`
|
|
158
|
+
// stops the fill exactly on the border's inner curve. Two coincident
|
|
159
|
+
// antialiased curves under-cover where they meet, and the box's `bg-card`
|
|
160
|
+
// bleeds through as an arc at each corner — the whole reason this is a
|
|
161
|
+
// negative inset. Do not tidy it back to `inset-0`.
|
|
162
|
+
expect(cls).toContain("-inset-px");
|
|
163
|
+
expect(cls).not.toMatch(/\binset-0\b/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("holds no layout for the glyph, which the tick layers own", () => {
|
|
167
|
+
// The fill scales, and a glyph inside it would scale with it. The tick is
|
|
168
|
+
// a sibling drawn on top so the two can move as different gestures.
|
|
169
|
+
expect(checkboxVariants().indicator()).not.toContain("justify-center");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("carries no corner radius of its own, at any size", () => {
|
|
173
|
+
for (const size of CHECKBOX_SIZES) {
|
|
174
|
+
// A class cannot read the live `--radius`, and the fill's corner has to
|
|
175
|
+
// track it — see `resolveCheckboxFillRadius`. It is an inline style
|
|
176
|
+
// instead, so a `rounded-*` here would be a second, stale definition
|
|
177
|
+
// tailwind-merge has no reason to drop.
|
|
178
|
+
expect(checkboxVariants({ size }).indicator()).not.toMatch(/\brounded-/);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("paints a distinct surface for every colour", () => {
|
|
183
|
+
const fills = CHECKBOX_COLORS.map((color) => fillToken(checkboxVariants({ color }).indicator()));
|
|
184
|
+
// Two colours resolving to one fill means a caller can set the axis and
|
|
185
|
+
// see nothing change.
|
|
186
|
+
expect(new Set(fills).size).toBe(CHECKBOX_COLORS.length);
|
|
187
|
+
expect(fills.every((fill) => fill !== undefined)).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("turns destructive when invalid, at every colour", () => {
|
|
191
|
+
for (const color of CHECKBOX_COLORS) {
|
|
192
|
+
expect(fillToken(checkboxVariants({ color, isInvalid: true }).indicator())).toBe("destructive");
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("resolveCheckboxFillRadius", () => {
|
|
198
|
+
test("is the box's own radius, at every size", () => {
|
|
199
|
+
for (const size of CHECKBOX_SIZES) {
|
|
200
|
+
// The fill spans the border box, so it wears the box's corner rather than
|
|
201
|
+
// the border's inner one. Subtracting the border here is what made the two
|
|
202
|
+
// curves coincide, and coincident curves are what left a seam at each
|
|
203
|
+
// corner — see `resolveCheckboxFillRadius`.
|
|
204
|
+
expect(resolveCheckboxFillRadius(size, RADIUS_BASE_PX)).toBe(radiusPx(CHECKBOX_RADIUS_STEP[size]));
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// The multipliers are restated in TypeScript because the corner scale is
|
|
209
|
+
// `@theme inline` and no `--radius-*` variable reaches the runtime. Retuning
|
|
210
|
+
// the scale in `tokens.css` has to fail here rather than quietly leave the
|
|
211
|
+
// fill on the old curve.
|
|
212
|
+
test("multiplies --radius by what tokens.css says that step multiplies it by", () => {
|
|
213
|
+
for (const [step, multiplier] of Object.entries(CHECKBOX_RADIUS_MULTIPLIER)) {
|
|
214
|
+
expect(multiplier).toBe(radiusMultiplier(step));
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("follows --radius, so a pasted theme moves the fill with the border", () => {
|
|
219
|
+
for (const size of CHECKBOX_SIZES) {
|
|
220
|
+
const doubled = resolveCheckboxFillRadius(size, RADIUS_BASE_PX * 2);
|
|
221
|
+
expect(doubled).toBe(radiusPx(CHECKBOX_RADIUS_STEP[size]) * 2);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("never goes negative, so a square-cornered theme draws a square fill", () => {
|
|
226
|
+
for (const size of CHECKBOX_SIZES) expect(resolveCheckboxFillRadius(size, 0)).toBe(0);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("names the radius step the box actually wears", () => {
|
|
230
|
+
for (const size of CHECKBOX_SIZES) {
|
|
231
|
+
// Two places naming a radius is how the fill ends up wearing a curve the
|
|
232
|
+
// box stopped using.
|
|
233
|
+
expect(checkboxVariants({ size }).box()).toContain(`rounded-${CHECKBOX_RADIUS_STEP[size]}`);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("assumes the bare border width the box actually sets", () => {
|
|
238
|
+
// Two things restate this number and neither can read a class: the
|
|
239
|
+
// `indicator` slot's `-inset-px`, which is how far the fill has to reach to
|
|
240
|
+
// clear the border ring, and `CheckboxBox`'s `onLayout`, which subtracts it
|
|
241
|
+
// twice to recover the padding box the tick clip sits in. A `border-2` here
|
|
242
|
+
// would leave the fill a point short of the ring on every side and put the
|
|
243
|
+
// glyph two points off centre.
|
|
244
|
+
expect(CHECKBOX_BORDER_WIDTH).toBe(1);
|
|
245
|
+
for (const size of CHECKBOX_SIZES) {
|
|
246
|
+
const cls = checkboxVariants({ size }).box();
|
|
247
|
+
expect(cls).toMatch(/(^|\s)border(\s|$)/);
|
|
248
|
+
expect(cls).not.toMatch(/\bborder-\d/);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("checkboxVariants tick slots", () => {
|
|
254
|
+
test("clips from the box's left edge, spanning its full height", () => {
|
|
255
|
+
const cls = checkboxVariants().tick();
|
|
256
|
+
expect(cls).toContain("absolute");
|
|
257
|
+
expect(cls).toContain("left-0");
|
|
258
|
+
expect(cls).toContain("inset-y-0");
|
|
259
|
+
// Without this the glyph is not revealed across, it just sits there while
|
|
260
|
+
// a container of no consequence changes width around it.
|
|
261
|
+
expect(cls).toContain("overflow-hidden");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("keeps the glyph on the box's centre line behind that clip", () => {
|
|
265
|
+
const cls = checkboxVariants().tickInner();
|
|
266
|
+
expect(cls).toContain("items-center");
|
|
267
|
+
expect(cls).toContain("justify-center");
|
|
268
|
+
// Full height, and a width the component sets from the measured box — so
|
|
269
|
+
// the glyph stays put while the clip in front of it opens rather than
|
|
270
|
+
// sliding across with it.
|
|
271
|
+
expect(cls).toContain("h-full");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("leaves the clip no width of its own", () => {
|
|
275
|
+
// The width is animated from the measured box, so a `w-*` here would be a
|
|
276
|
+
// second answer that tailwind-merge resolves and Reanimated then ignores.
|
|
277
|
+
expect(checkboxVariants().tick()).not.toMatch(/\bw-/);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("checkboxVariants root slot", () => {
|
|
282
|
+
test("lays the box and its label out as a row, aligned to the first line", () => {
|
|
283
|
+
expect(checkboxVariants().root()).toContain("flex-row");
|
|
284
|
+
// Not `items-center`. A wrapped label centred against its own paragraph
|
|
285
|
+
// drifts the box down the side of it — what `Input` refuses for a
|
|
286
|
+
// multiline field's decorators. The box's own margin puts a single-line
|
|
287
|
+
// label back where centring had it.
|
|
288
|
+
expect(checkboxVariants().root()).toContain("items-start");
|
|
289
|
+
expect(checkboxVariants().root()).not.toContain("items-center");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("carries no text utility at any size", () => {
|
|
293
|
+
for (const size of CHECKBOX_SIZES) {
|
|
294
|
+
// A React Native View does not cascade colour to a Text descendant, so
|
|
295
|
+
// a `text-*` here would be a class that does nothing. See AGENTS.md
|
|
296
|
+
// rule 1 — the label's treatment is the `Text.Label` preset's.
|
|
297
|
+
expect(checkboxVariants({ size }).root()).not.toMatch(/\btext-/);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("steps its gap up with the size", () => {
|
|
302
|
+
const gaps = CHECKBOX_SIZES.map((size) => gapOf(checkboxVariants({ size }).root()));
|
|
303
|
+
expect(gaps).toEqual([...gaps].sort((a, b) => a - b));
|
|
304
|
+
expect(new Set(gaps).size).toBe(CHECKBOX_SIZES.length);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("fades the whole row when disabled, so the label goes with the box", () => {
|
|
308
|
+
expect(checkboxVariants({ isDisabled: true }).root()).toContain("opacity-50");
|
|
309
|
+
expect(checkboxVariants({ isDisabled: false }).root()).not.toContain("opacity-50");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe("checkboxVariants label slot", () => {
|
|
314
|
+
test("carries no type scale, weight or colour at any size", () => {
|
|
315
|
+
for (const size of CHECKBOX_SIZES) {
|
|
316
|
+
for (const isInvalid of [true, false]) {
|
|
317
|
+
const cls = checkboxVariants({ isInvalid, size }).label();
|
|
318
|
+
// The scale belongs to `Text.Label` and the colour to `Text`'s own
|
|
319
|
+
// axis. A `text-sm font-medium` here would be a second definition
|
|
320
|
+
// of the preset that could drift from it — Field's rule, and the
|
|
321
|
+
// reason `Input` ships no label part at all.
|
|
322
|
+
expect(cls).not.toMatch(/\btext-/);
|
|
323
|
+
expect(cls).not.toMatch(/\bfont-/);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("shrinks so a long label wraps instead of pushing the box off the row", () => {
|
|
329
|
+
expect(checkboxVariants().label()).toContain("shrink");
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("checkboxVariants glyph slot", () => {
|
|
334
|
+
test("steps up with the size, on the shared icon scale", () => {
|
|
335
|
+
const steps = CHECKBOX_SIZES.map((size) => sizeStep(checkboxVariants({ size }).glyph()));
|
|
336
|
+
expect(steps.every((step) => step >= 0)).toBe(true);
|
|
337
|
+
expect(steps).toEqual([...steps].sort((a, b) => a - b));
|
|
338
|
+
expect(new Set(steps).size).toBe(CHECKBOX_SIZES.length);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("checkboxVariants alignment", () => {
|
|
343
|
+
test("puts the box first by default", () => {
|
|
344
|
+
expect(checkboxVariants({ alignment: "start" }).root()).not.toContain("flex-row-reverse");
|
|
345
|
+
expect(checkboxVariants({ alignment: "start" }).label()).not.toContain("grow");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("pushes the box to the far edge and fills the row with the label", () => {
|
|
349
|
+
expect(checkboxVariants({ alignment: "end" }).root()).toContain("flex-row-reverse");
|
|
350
|
+
expect(checkboxVariants({ alignment: "end" }).label()).toContain("grow");
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe("CHECKBOX_GLYPH_TOKEN", () => {
|
|
355
|
+
test("names a token for every colour", () => {
|
|
356
|
+
expect(Object.keys(CHECKBOX_GLYPH_TOKEN).sort()).toEqual([...CHECKBOX_COLORS].sort());
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("pairs each glyph with the fill its own indicator paints", () => {
|
|
360
|
+
for (const color of CHECKBOX_COLORS) {
|
|
361
|
+
// Two maps that can drift is how a checkbox ends up with a white tick
|
|
362
|
+
// on a pale fill. The foreground is always the fill's own.
|
|
363
|
+
expect(CHECKBOX_GLYPH_TOKEN[color]).toBe(`${fillToken(checkboxVariants({ color }).indicator())}-foreground`);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("declares every token it names in both themes", () => {
|
|
368
|
+
for (const token of [...Object.values(CHECKBOX_GLYPH_TOKEN), CHECKBOX_INVALID_GLYPH_TOKEN]) {
|
|
369
|
+
expect(LIGHT.has(token)).toBe(true);
|
|
370
|
+
expect(DARK.has(token)).toBe(true);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("pairs the invalid glyph with the invalid fill", () => {
|
|
375
|
+
expect(CHECKBOX_INVALID_GLYPH_TOKEN).toBe(
|
|
376
|
+
`${fillToken(checkboxVariants({ isInvalid: true }).indicator())}-foreground`
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe("CHECKBOX_SURFACE_TOKEN", () => {
|
|
382
|
+
test("names a token for every colour", () => {
|
|
383
|
+
expect(Object.keys(CHECKBOX_SURFACE_TOKEN).sort()).toEqual([...CHECKBOX_COLORS].sort());
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("matches the fill its own indicator paints", () => {
|
|
387
|
+
for (const color of CHECKBOX_COLORS) {
|
|
388
|
+
// The border interpolates to this token, so a drift here is a border
|
|
389
|
+
// settling on a shade the surface behind it never uses.
|
|
390
|
+
expect(fillToken(checkboxVariants({ color }).indicator())).toBe(CHECKBOX_SURFACE_TOKEN[color]);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("declares every border token it can resolve to in both themes", () => {
|
|
395
|
+
const tokens = [
|
|
396
|
+
...Object.values(CHECKBOX_SURFACE_TOKEN),
|
|
397
|
+
CHECKBOX_REST_BORDER_TOKEN,
|
|
398
|
+
CHECKBOX_INVALID_BORDER_TOKEN,
|
|
399
|
+
];
|
|
400
|
+
for (const token of tokens) {
|
|
401
|
+
// An unresolved token yields `undefined`, which the component falls
|
|
402
|
+
// back to `transparent` for — a border that silently vanishes in one
|
|
403
|
+
// theme. This is the test that keeps that fallback unreachable.
|
|
404
|
+
expect(LIGHT.has(token)).toBe(true);
|
|
405
|
+
expect(DARK.has(token)).toBe(true);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe("resolveCheckboxBorderTokens", () => {
|
|
411
|
+
test("travels from the field chrome to the colour's own fill", () => {
|
|
412
|
+
for (const color of CHECKBOX_COLORS) {
|
|
413
|
+
expect(resolveCheckboxBorderTokens({ color, isInvalid: false })).toEqual({
|
|
414
|
+
active: CHECKBOX_SURFACE_TOKEN[color],
|
|
415
|
+
rest: CHECKBOX_REST_BORDER_TOKEN,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("holds destructive at both ends when invalid, so there is nothing to fade", () => {
|
|
421
|
+
for (const color of CHECKBOX_COLORS) {
|
|
422
|
+
// The border is the signal that the value is wrong, and it has to be
|
|
423
|
+
// there before the box is ticked as much as after — so an invalid box
|
|
424
|
+
// never fades into or out of it.
|
|
425
|
+
const tokens = resolveCheckboxBorderTokens({ color, isInvalid: true });
|
|
426
|
+
expect(tokens.rest).toBe(CHECKBOX_INVALID_BORDER_TOKEN);
|
|
427
|
+
expect(tokens.active).toBe(CHECKBOX_INVALID_BORDER_TOKEN);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe("resolveCheckboxFilled", () => {
|
|
433
|
+
test("paints the surface when checked", () => {
|
|
434
|
+
expect(resolveCheckboxFilled({ isChecked: true, isIndeterminate: false })).toBe(true);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("paints the surface when indeterminate, checked or not", () => {
|
|
438
|
+
// Indeterminate fills exactly as checked does — only the glyph differs.
|
|
439
|
+
expect(resolveCheckboxFilled({ isChecked: false, isIndeterminate: true })).toBe(true);
|
|
440
|
+
expect(resolveCheckboxFilled({ isChecked: true, isIndeterminate: true })).toBe(true);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("leaves it empty otherwise", () => {
|
|
444
|
+
expect(resolveCheckboxFilled({ isChecked: false, isIndeterminate: false })).toBe(false);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe("resolveCheckboxAxes", () => {
|
|
449
|
+
const GROUP = { alignment: "end", color: "success", isDisabled: false, isInvalid: false, size: "lg" } as const;
|
|
450
|
+
const FIELD = { isDisabled: true, isInvalid: true } as const;
|
|
451
|
+
|
|
452
|
+
test("falls back to the defaults with nothing around it", () => {
|
|
453
|
+
expect(resolveCheckboxAxes({ own: {} })).toEqual({
|
|
454
|
+
alignment: "start",
|
|
455
|
+
color: "default",
|
|
456
|
+
isDisabled: false,
|
|
457
|
+
isInvalid: false,
|
|
458
|
+
size: "md",
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("takes the group's axes when the checkbox names none", () => {
|
|
463
|
+
const axes = resolveCheckboxAxes({ group: GROUP, own: {} });
|
|
464
|
+
expect(axes.color).toBe("success");
|
|
465
|
+
expect(axes.size).toBe("lg");
|
|
466
|
+
expect(axes.alignment).toBe("end");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("lets the checkbox's own axis beat the group's", () => {
|
|
470
|
+
// Unlike Input.Group, which owns one box and therefore owns the axes that
|
|
471
|
+
// draw it. Checkbox.Group owns no box — it is a wrapper supplying
|
|
472
|
+
// defaults, the same kind of thing as Field, so a control overrides it.
|
|
473
|
+
const axes = resolveCheckboxAxes({ group: GROUP, own: { color: "destructive", size: "sm" } });
|
|
474
|
+
expect(axes.color).toBe("destructive");
|
|
475
|
+
expect(axes.size).toBe("sm");
|
|
476
|
+
expect(axes.alignment).toBe("end");
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("reads the field last, behind both", () => {
|
|
480
|
+
expect(resolveCheckboxAxes({ field: FIELD, own: {} }).isInvalid).toBe(true);
|
|
481
|
+
expect(resolveCheckboxAxes({ field: FIELD, own: {} }).isDisabled).toBe(true);
|
|
482
|
+
expect(
|
|
483
|
+
resolveCheckboxAxes({ field: FIELD, group: { isDisabled: false, isInvalid: false }, own: {} }).isInvalid
|
|
484
|
+
).toBe(false);
|
|
485
|
+
expect(resolveCheckboxAxes({ field: FIELD, own: { isInvalid: false } }).isInvalid).toBe(false);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("lets an explicit false opt out of an invalid field", () => {
|
|
489
|
+
// `??` and never `||`, so `false` is an answer rather than an absence.
|
|
490
|
+
expect(resolveCheckboxAxes({ field: FIELD, own: { isDisabled: false } }).isDisabled).toBe(false);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
describe("toggleCheckedValue", () => {
|
|
495
|
+
test("appends a value that is absent", () => {
|
|
496
|
+
expect(toggleCheckedValue(["a"], "b")).toEqual(["a", "b"]);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("removes a value that is present", () => {
|
|
500
|
+
expect(toggleCheckedValue(["a", "b", "c"], "b")).toEqual(["a", "c"]);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("preserves the order the rest were checked in", () => {
|
|
504
|
+
expect(toggleCheckedValue(["c", "a"], "b")).toEqual(["c", "a", "b"]);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("returns a new array either way", () => {
|
|
508
|
+
// React bails out of a re-render on an unchanged reference, so a mutated
|
|
509
|
+
// array would toggle the state and leave the screen alone.
|
|
510
|
+
const current = ["a"];
|
|
511
|
+
expect(toggleCheckedValue(current, "b")).not.toBe(current);
|
|
512
|
+
expect(toggleCheckedValue(current, "a")).not.toBe(current);
|
|
513
|
+
expect(current).toEqual(["a"]);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("removes every copy of a value that somehow appears twice", () => {
|
|
517
|
+
expect(toggleCheckedValue(["a", "a"], "a")).toEqual([]);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
describe("resolveCheckboxLabelSize", () => {
|
|
522
|
+
test("names a Text step for every checkbox size", () => {
|
|
523
|
+
for (const size of CHECKBOX_SIZES) {
|
|
524
|
+
expect(TEXT_SIZES).toContain(resolveCheckboxLabelSize(size));
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test("steps up with the box", () => {
|
|
529
|
+
const steps = CHECKBOX_SIZES.map((size) => TEXT_SIZES.indexOf(resolveCheckboxLabelSize(size)));
|
|
530
|
+
expect(steps).toEqual([...steps].sort((a, b) => a - b));
|
|
531
|
+
expect(new Set(steps).size).toBe(CHECKBOX_SIZES.length);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
describe("resolveCheckboxLabelColor", () => {
|
|
536
|
+
test("turns the label destructive with the box it names", () => {
|
|
537
|
+
expect(resolveCheckboxLabelColor(true)).toBe("destructive");
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("leaves the preset's own colour alone otherwise", () => {
|
|
541
|
+
// `undefined` is the answer, not a gap: Text's colour axis emits nothing
|
|
542
|
+
// when unnamed, so the label falls through to `Text.Label`'s own token.
|
|
543
|
+
expect(resolveCheckboxLabelColor(false)).toBeUndefined();
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
describe("resolveCheckboxHitSlop", () => {
|
|
548
|
+
test("pads a bare box out toward the 44pt minimum target", () => {
|
|
549
|
+
for (const size of CHECKBOX_SIZES) {
|
|
550
|
+
expect(resolveCheckboxHitSlop({ hasLabel: false, size })).toBe(CHECKBOX_HIT_SLOP[size]);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("leaves a labelled row alone", () => {
|
|
555
|
+
for (const size of CHECKBOX_SIZES) {
|
|
556
|
+
// The row is the target once there is a label, and it is already wide.
|
|
557
|
+
// Slop on top of it would overlap the next row's and make a tap between
|
|
558
|
+
// two checkboxes ambiguous.
|
|
559
|
+
expect(resolveCheckboxHitSlop({ hasLabel: true, size })).toBeUndefined();
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
describe("CHECKBOX_HIT_SLOP", () => {
|
|
565
|
+
test("brings every bare box to at least 44 points", () => {
|
|
566
|
+
const BOX_PX = { sm: 18, md: 20, lg: 24 } as const;
|
|
567
|
+
for (const size of CHECKBOX_SIZES) {
|
|
568
|
+
expect(BOX_PX[size] + CHECKBOX_HIT_SLOP[size] * 2).toBeGreaterThanOrEqual(44);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
describe("CHECKBOX_INDICATOR_ANIMATION", () => {
|
|
574
|
+
test("moves both of the fill's tracks", () => {
|
|
575
|
+
for (const [from, to] of [CHECKBOX_INDICATOR_ANIMATION.opacity, CHECKBOX_INDICATOR_ANIMATION.scale]) {
|
|
576
|
+
expect(from).not.toBe(to);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test("names no corner radius, because the fill's never changes", () => {
|
|
581
|
+
// The radius that keeps the fill concentric with the border is a fixed
|
|
582
|
+
// geometric fact, not a track. Animating it means it is only correct at
|
|
583
|
+
// one end — see resolveCheckboxFillRadius.
|
|
584
|
+
expect(CHECKBOX_INDICATOR_ANIMATION).not.toHaveProperty("borderRadius");
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test("ends filled at the values a painted box needs", () => {
|
|
588
|
+
// The fill has to land fully opaque and unscaled, or a checked box is
|
|
589
|
+
// permanently stopped mid-animation.
|
|
590
|
+
expect(CHECKBOX_INDICATOR_ANIMATION.opacity[1]).toBe(1);
|
|
591
|
+
expect(CHECKBOX_INDICATOR_ANIMATION.scale[1]).toBe(1);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("starts invisible, so an unchecked box shows none of the fill", () => {
|
|
595
|
+
expect(CHECKBOX_INDICATOR_ANIMATION.opacity[0]).toBe(0);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("scales the fill up rather than down, so it grows into the box", () => {
|
|
599
|
+
expect(CHECKBOX_INDICATOR_ANIMATION.scale[0]).toBeLessThan(CHECKBOX_INDICATOR_ANIMATION.scale[1]);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test("names no translation, so the fill arrives from the centre and not an edge", () => {
|
|
603
|
+
// A checkbox is filled, not slid into. A `translateX` here would make the
|
|
604
|
+
// surface enter from one side, which reads as a panel rather than a fill.
|
|
605
|
+
expect(CHECKBOX_INDICATOR_ANIMATION).not.toHaveProperty("translateX");
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test("holds the tick back, but not past the end of the fill", () => {
|
|
609
|
+
// At 0 the stroke and the surface it is drawn on start together and read
|
|
610
|
+
// as one blurred event; at 1 the tick would never draw at all.
|
|
611
|
+
expect(CHECKBOX_INDICATOR_ANIMATION.tickDelay).toBeGreaterThan(0);
|
|
612
|
+
expect(CHECKBOX_INDICATOR_ANIMATION.tickDelay).toBeLessThan(1);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("starts the border after the tick, so it reads as the fill arriving", () => {
|
|
616
|
+
// The fill scales 0.8 -> 1, so it is only near the edge late in its
|
|
617
|
+
// travel. A border that changed at the same moment as the tick would read
|
|
618
|
+
// as a second thing happening rather than as the surface reaching it.
|
|
619
|
+
expect(CHECKBOX_INDICATOR_ANIMATION.borderDelay).toBeGreaterThan(CHECKBOX_INDICATOR_ANIMATION.tickDelay);
|
|
620
|
+
expect(CHECKBOX_INDICATOR_ANIMATION.borderDelay).toBeLessThan(1);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test("is quick enough to read as a state change rather than a transition", () => {
|
|
624
|
+
expect(CHECKBOX_INDICATOR_ANIMATION.durationMs).toBeLessThanOrEqual(200);
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
describe("the exported axis arrays", () => {
|
|
629
|
+
test("list every key the tv() declares", () => {
|
|
630
|
+
expect([...CHECKBOX_COLORS].sort()).toEqual([...CHECKBOX_COLORS].sort());
|
|
631
|
+
expect(CHECKBOX_SIZES.length).toBe(3);
|
|
632
|
+
expect([...CHECKBOX_ALIGNMENTS]).toEqual(["start", "end"]);
|
|
633
|
+
});
|
|
634
|
+
});
|