@seed-design/react 2.2.2 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/ActionButton/ActionButton.cjs +5 -2
- package/lib/components/ActionButton/ActionButton.d.ts.map +1 -1
- package/lib/components/ActionButton/ActionButton.js +5 -2
- package/lib/components/AttachmentDisplay/AttachmentDisplayItem.cjs +9 -6
- package/lib/components/AttachmentDisplay/AttachmentDisplayItem.d.ts.map +1 -1
- package/lib/components/AttachmentDisplay/AttachmentDisplayItem.js +9 -6
- package/lib/components/AttachmentDisplay/AttachmentDisplayTrigger.cjs +2 -1
- package/lib/components/AttachmentDisplay/AttachmentDisplayTrigger.d.ts.map +1 -1
- package/lib/components/AttachmentDisplay/AttachmentDisplayTrigger.js +2 -1
- package/lib/components/AttachmentInput/AttachmentInputItem.cjs +9 -6
- package/lib/components/AttachmentInput/AttachmentInputItem.d.ts.map +1 -1
- package/lib/components/AttachmentInput/AttachmentInputItem.js +9 -6
- package/lib/components/AttachmentInput/AttachmentInputTrigger.cjs +2 -1
- package/lib/components/AttachmentInput/AttachmentInputTrigger.d.ts.map +1 -1
- package/lib/components/AttachmentInput/AttachmentInputTrigger.js +2 -1
- package/lib/components/BottomSheet/BottomSheet.cjs +5 -3
- package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/lib/components/BottomSheet/BottomSheet.js +5 -3
- package/lib/components/Callout/Callout.cjs +3 -2
- package/lib/components/Callout/Callout.d.ts.map +1 -1
- package/lib/components/Callout/Callout.js +3 -2
- package/lib/components/Checkbox/Checkbox.cjs +2 -1
- package/lib/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/lib/components/Checkbox/Checkbox.js +2 -1
- package/lib/components/Chip/Chip.cjs +2 -1
- package/lib/components/Chip/Chip.d.ts.map +1 -1
- package/lib/components/Chip/Chip.js +2 -1
- package/lib/components/ChipTabs/ChipTabs.cjs +2 -1
- package/lib/components/ChipTabs/ChipTabs.d.ts.map +1 -1
- package/lib/components/ChipTabs/ChipTabs.js +2 -1
- package/lib/components/ContentDialog/ContentDialog.cjs +5 -3
- package/lib/components/ContentDialog/ContentDialog.d.ts.map +1 -1
- package/lib/components/ContentDialog/ContentDialog.js +5 -3
- package/lib/components/ContextualFloatingButton/ContextualFloatingButton.cjs +5 -2
- package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts.map +1 -1
- package/lib/components/ContextualFloatingButton/ContextualFloatingButton.js +5 -2
- package/lib/components/DatePicker/DatePicker.cjs +2 -2
- package/lib/components/DatePicker/DatePicker.d.ts +10 -4
- package/lib/components/DatePicker/DatePicker.d.ts.map +1 -1
- package/lib/components/DatePicker/DatePicker.js +2 -2
- package/lib/components/DatePicker/index.d.ts +1 -1
- package/lib/components/DatePicker/index.d.ts.map +1 -1
- package/lib/components/FieldButton/FieldButton.cjs +2 -1
- package/lib/components/FieldButton/FieldButton.d.ts.map +1 -1
- package/lib/components/FieldButton/FieldButton.js +2 -1
- package/lib/components/FloatingActionButton/FloatingActionButton.cjs +2 -1
- package/lib/components/FloatingActionButton/FloatingActionButton.d.ts.map +1 -1
- package/lib/components/FloatingActionButton/FloatingActionButton.js +2 -1
- package/lib/components/HelpBubble/HelpBubble.cjs +2 -1
- package/lib/components/HelpBubble/HelpBubble.d.ts.map +1 -1
- package/lib/components/HelpBubble/HelpBubble.js +2 -1
- package/lib/components/ImageFrame/ImageFrame.cjs +5 -2
- package/lib/components/ImageFrame/ImageFrame.d.ts.map +1 -1
- package/lib/components/ImageFrame/ImageFrame.js +5 -2
- package/lib/components/LoadingIndicator/usePendingButton.d.ts +57 -57
- package/lib/components/MenuSheet/MenuSheet.cjs +2 -1
- package/lib/components/MenuSheet/MenuSheet.d.ts.map +1 -1
- package/lib/components/MenuSheet/MenuSheet.js +2 -1
- package/lib/components/PageBanner/PageBanner.cjs +3 -2
- package/lib/components/PageBanner/PageBanner.d.ts.map +1 -1
- package/lib/components/PageBanner/PageBanner.js +3 -2
- package/lib/components/QuantityPicker/QuantityPicker.cjs +8 -4
- package/lib/components/QuantityPicker/QuantityPicker.d.ts.map +1 -1
- package/lib/components/QuantityPicker/QuantityPicker.js +8 -4
- package/lib/components/RadioGroup/RadioGroup.cjs +2 -1
- package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/lib/components/RadioGroup/RadioGroup.js +2 -1
- package/lib/components/ReactionButton/ReactionButton.cjs +5 -2
- package/lib/components/ReactionButton/ReactionButton.d.ts.map +1 -1
- package/lib/components/ReactionButton/ReactionButton.js +5 -2
- package/lib/components/ScrollFog/ScrollFog.d.ts +4 -6
- package/lib/components/ScrollFog/ScrollFog.d.ts.map +1 -1
- package/lib/components/ScrollFog/ScrollFog.test.d.ts +2 -0
- package/lib/components/ScrollFog/ScrollFog.test.d.ts.map +1 -0
- package/lib/components/SideNavigation/SideNavigation.cjs +1 -1
- package/lib/components/SideNavigation/SideNavigation.js +1 -1
- package/lib/components/SidePanel/SidePanel.cjs +5 -3
- package/lib/components/SidePanel/SidePanel.d.ts.map +1 -1
- package/lib/components/SidePanel/SidePanel.js +5 -3
- package/lib/components/Slider/Slider.cjs +1 -1
- package/lib/components/Slider/Slider.js +1 -1
- package/lib/components/Snackbar/Snackbar.cjs +2 -1
- package/lib/components/Snackbar/Snackbar.d.ts.map +1 -1
- package/lib/components/Snackbar/Snackbar.js +2 -1
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.cjs +2 -1
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts.map +1 -1
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.js +2 -1
- package/lib/components/Switch/Switch.cjs +2 -1
- package/lib/components/Switch/Switch.d.ts.map +1 -1
- package/lib/components/Switch/Switch.js +2 -1
- package/lib/components/Tabs/Tabs.cjs +2 -1
- package/lib/components/Tabs/Tabs.d.ts.map +1 -1
- package/lib/components/Tabs/Tabs.js +2 -1
- package/lib/components/TextField/TextField.cjs +1 -1
- package/lib/components/TextField/TextField.js +1 -1
- package/lib/components/TextField/useTextFieldWithGraphemes.cjs +2 -5
- package/lib/components/TextField/useTextFieldWithGraphemes.d.ts.map +1 -1
- package/lib/components/TextField/useTextFieldWithGraphemes.js +3 -6
- package/lib/components/ToggleButton/ToggleButton.cjs +5 -2
- package/lib/components/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/lib/components/ToggleButton/ToggleButton.js +5 -2
- package/lib/components/WheelPicker/WheelPicker.cjs +94 -0
- package/lib/components/WheelPicker/WheelPicker.d.ts +56 -0
- package/lib/components/WheelPicker/WheelPicker.d.ts.map +1 -0
- package/lib/components/WheelPicker/WheelPicker.js +89 -0
- package/lib/components/WheelPicker/WheelPicker.namespace.cjs +15 -0
- package/lib/components/WheelPicker/WheelPicker.namespace.d.ts +2 -0
- package/lib/components/WheelPicker/WheelPicker.namespace.d.ts.map +1 -0
- package/lib/components/WheelPicker/WheelPicker.namespace.js +10 -0
- package/lib/components/WheelPicker/WheelPicker.test.d.ts +2 -0
- package/lib/components/WheelPicker/WheelPicker.test.d.ts.map +1 -0
- package/lib/components/WheelPicker/index.cjs +12 -0
- package/lib/components/WheelPicker/index.d.ts +3 -0
- package/lib/components/WheelPicker/index.d.ts.map +1 -0
- package/lib/components/WheelPicker/index.js +3 -0
- package/lib/components/index.cjs +11 -0
- package/lib/components/index.d.ts +1 -0
- package/lib/components/index.d.ts.map +1 -1
- package/lib/components/index.js +3 -1
- package/lib/components/private/WheelPicker.cjs +2 -3
- package/lib/components/private/WheelPicker.d.ts +3 -3
- package/lib/components/private/WheelPicker.d.ts.map +1 -1
- package/lib/components/private/WheelPicker.js +3 -3
- package/lib/components/private/useDismissible.d.ts +123 -123
- package/lib/index.cjs +14 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -1
- package/lib/utils/elementSizeVars.cjs +76 -0
- package/lib/utils/elementSizeVars.d.ts +15 -0
- package/lib/utils/elementSizeVars.d.ts.map +1 -0
- package/lib/utils/elementSizeVars.js +74 -0
- package/lib/utils/scaleFeedback.cjs +99 -0
- package/lib/utils/scaleFeedback.d.ts +58 -0
- package/lib/utils/scaleFeedback.d.ts.map +1 -0
- package/lib/utils/scaleFeedback.js +94 -0
- package/package.json +7 -7
- package/src/components/ActionButton/ActionButton.tsx +5 -2
- package/src/components/AttachmentDisplay/AttachmentDisplayItem.tsx +14 -9
- package/src/components/AttachmentDisplay/AttachmentDisplayTrigger.tsx +7 -4
- package/src/components/AttachmentInput/AttachmentInputItem.tsx +14 -9
- package/src/components/AttachmentInput/AttachmentInputTrigger.tsx +3 -3
- package/src/components/BottomSheet/BottomSheet.tsx +5 -3
- package/src/components/Callout/Callout.tsx +9 -4
- package/src/components/Checkbox/Checkbox.tsx +3 -3
- package/src/components/Chip/Chip.tsx +2 -1
- package/src/components/ChipTabs/ChipTabs.tsx +3 -3
- package/src/components/ContentDialog/ContentDialog.tsx +5 -3
- package/src/components/ContextualFloatingButton/ContextualFloatingButton.tsx +5 -2
- package/src/components/DatePicker/DatePicker.test.tsx +70 -0
- package/src/components/DatePicker/DatePicker.tsx +17 -16
- package/src/components/DatePicker/index.ts +2 -0
- package/src/components/FieldButton/FieldButton.tsx +6 -3
- package/src/components/FloatingActionButton/FloatingActionButton.tsx +4 -4
- package/src/components/HelpBubble/HelpBubble.tsx +7 -4
- package/src/components/HelpBubbleTooltip/HelpBubbleTooltip.tsx +1 -1
- package/src/components/ImageFrame/ImageFrame.tsx +8 -1
- package/src/components/MenuSheet/MenuSheet.tsx +6 -3
- package/src/components/PageBanner/PageBanner.tsx +5 -6
- package/src/components/QuantityPicker/QuantityPicker.tsx +8 -4
- package/src/components/RadioGroup/RadioGroup.tsx +7 -4
- package/src/components/ReactionButton/ReactionButton.tsx +5 -2
- package/src/components/ScrollFog/ScrollFog.test.tsx +13 -0
- package/src/components/ScrollFog/ScrollFog.tsx +5 -7
- package/src/components/SidePanel/SidePanel.tsx +5 -3
- package/src/components/Snackbar/Snackbar.tsx +6 -3
- package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx +7 -4
- package/src/components/Switch/Switch.tsx +3 -3
- package/src/components/Tabs/Tabs.tsx +3 -3
- package/src/components/TextField/useTextFieldWithGraphemes.ts +3 -7
- package/src/components/ToggleButton/ToggleButton.tsx +5 -2
- package/src/components/WheelPicker/WheelPicker.namespace.ts +9 -0
- package/src/components/WheelPicker/WheelPicker.test.tsx +270 -0
- package/src/components/WheelPicker/WheelPicker.tsx +210 -0
- package/src/components/WheelPicker/index.ts +11 -0
- package/src/components/index.ts +1 -0
- package/src/components/private/WheelPicker.test.tsx +1 -0
- package/src/components/private/WheelPicker.tsx +6 -6
- package/src/index.ts +2 -0
- package/src/utils/elementSizeVars.tsx +112 -0
- package/src/utils/scaleFeedback.tsx +128 -0
- package/lib/components/TextField/memoize.cjs +0 -13
- package/lib/components/TextField/memoize.d.ts +0 -2
- package/lib/components/TextField/memoize.d.ts.map +0 -1
- package/lib/components/TextField/memoize.js +0 -13
- package/src/components/TextField/memoize.ts +0 -14
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { fireEvent, render } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, mock, spyOn } from "bun:test";
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
import { BottomSheet } from "../BottomSheet";
|
|
5
|
+
import { WheelPicker } from "./index";
|
|
6
|
+
|
|
7
|
+
const options = [
|
|
8
|
+
{ value: "a", label: "A" },
|
|
9
|
+
{ value: "b", label: "B" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function fireTouchPointer(type: string, element: HTMLElement, y: number) {
|
|
13
|
+
const event = new PointerEvent(type, {
|
|
14
|
+
bubbles: true,
|
|
15
|
+
cancelable: true,
|
|
16
|
+
pointerId: 1,
|
|
17
|
+
pointerType: "touch",
|
|
18
|
+
clientX: 50,
|
|
19
|
+
clientY: y,
|
|
20
|
+
});
|
|
21
|
+
Object.defineProperty(event, "pageX", { value: 50 });
|
|
22
|
+
Object.defineProperty(event, "pageY", { value: y });
|
|
23
|
+
fireEvent(element, event);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("WheelPicker", () => {
|
|
27
|
+
it("공개 기본 geometry와 중립 스타일을 적용한다", () => {
|
|
28
|
+
const { container, getByRole } = render(
|
|
29
|
+
<WheelPicker.Root aria-label="휠 피커">
|
|
30
|
+
<WheelPicker.Column aria-label="문자" options={options} defaultValue="a" />
|
|
31
|
+
</WheelPicker.Root>,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(getByRole("group")).toHaveStyle({
|
|
35
|
+
"--seed-wheel-picker-public-item-size": "44px",
|
|
36
|
+
"--seed-wheel-picker-public-visible-item-count": "5",
|
|
37
|
+
"--seed-wheel-picker-public-viewport-size": "220px",
|
|
38
|
+
"--seed-wheel-picker-public-center-offset": "88px",
|
|
39
|
+
"--seed-wheel-picker-public-scroll-fog-max-height": "132px",
|
|
40
|
+
});
|
|
41
|
+
expect(getByRole("group")).toHaveClass(
|
|
42
|
+
"seed-wheel-picker-public__root",
|
|
43
|
+
"seed-wheel-picker-public__root--size_medium",
|
|
44
|
+
);
|
|
45
|
+
expect(getByRole("group")).not.toHaveClass("seed-wheel-picker__root");
|
|
46
|
+
expect(container.querySelector("[data-wheel-picker-scroll-fog]")).toHaveClass(
|
|
47
|
+
"seed-wheel-picker-public__scrollFog",
|
|
48
|
+
);
|
|
49
|
+
expect(container.querySelector("[data-wheel-picker-columns]")).toHaveClass(
|
|
50
|
+
"seed-wheel-picker-public__columns",
|
|
51
|
+
);
|
|
52
|
+
expect(getByRole("spinbutton")).toHaveClass("seed-wheel-picker-public__column");
|
|
53
|
+
expect(container.querySelector('[data-wheel-picker-value="a"]')).toHaveClass(
|
|
54
|
+
"seed-wheel-picker-public__item",
|
|
55
|
+
);
|
|
56
|
+
expect(container.querySelector("[data-wheel-picker-item-label]")).toHaveClass(
|
|
57
|
+
"seed-wheel-picker-public__itemLabel",
|
|
58
|
+
);
|
|
59
|
+
expect(container.querySelector("[data-wheel-picker-indicator]")).toHaveClass(
|
|
60
|
+
"seed-wheel-picker-public__selectionIndicator",
|
|
61
|
+
);
|
|
62
|
+
const scrollFog = container.querySelector<HTMLElement>("[data-wheel-picker-scroll-fog]");
|
|
63
|
+
expect(scrollFog?.style.getPropertyValue("--scroll-fog-size-top")).toBe(
|
|
64
|
+
"var(--seed-wheel-picker-public-scroll-fog-size)",
|
|
65
|
+
);
|
|
66
|
+
expect(scrollFog?.style.getPropertyValue("--scroll-fog-size-bottom")).toBe(
|
|
67
|
+
"var(--seed-wheel-picker-public-scroll-fog-size)",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("Small 크기의 geometry와 타이포그래피 Variant를 적용한다", () => {
|
|
72
|
+
const { container, getByRole } = render(
|
|
73
|
+
<WheelPicker.Root aria-label="작은 휠 피커" size="small">
|
|
74
|
+
<WheelPicker.Column aria-label="문자" options={options} defaultValue="a" />
|
|
75
|
+
</WheelPicker.Root>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(getByRole("group")).toHaveStyle({
|
|
79
|
+
"--seed-wheel-picker-public-item-size": "36px",
|
|
80
|
+
"--seed-wheel-picker-public-visible-item-count": "5",
|
|
81
|
+
"--seed-wheel-picker-public-viewport-size": "180px",
|
|
82
|
+
"--seed-wheel-picker-public-center-offset": "72px",
|
|
83
|
+
"--seed-wheel-picker-public-scroll-fog-max-height": "108px",
|
|
84
|
+
});
|
|
85
|
+
expect(getByRole("group")).toHaveClass("seed-wheel-picker-public__root--size_small");
|
|
86
|
+
expect(container.querySelector("[data-wheel-picker-item-label]")).toHaveClass(
|
|
87
|
+
"seed-wheel-picker-public__itemLabel--size_small",
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("itemSize를 지정하면 Size의 Item 높이와 Scroll Fog 최대 높이만 덮어쓴다", () => {
|
|
92
|
+
const { container, getByRole } = render(
|
|
93
|
+
<WheelPicker.Root
|
|
94
|
+
aria-label="사용자 지정 휠 피커"
|
|
95
|
+
size="small"
|
|
96
|
+
itemSize={56}
|
|
97
|
+
visibleItemCount={7}
|
|
98
|
+
>
|
|
99
|
+
<WheelPicker.Column aria-label="문자" options={options} defaultValue="a" />
|
|
100
|
+
</WheelPicker.Root>,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(getByRole("group")).toHaveStyle({
|
|
104
|
+
"--seed-wheel-picker-public-item-size": "56px",
|
|
105
|
+
"--seed-wheel-picker-public-visible-item-count": "7",
|
|
106
|
+
"--seed-wheel-picker-public-viewport-size": "392px",
|
|
107
|
+
"--seed-wheel-picker-public-center-offset": "168px",
|
|
108
|
+
"--seed-wheel-picker-public-scroll-fog-max-height": "168px",
|
|
109
|
+
});
|
|
110
|
+
expect(container.querySelector("[data-wheel-picker-item-label]")).toHaveClass(
|
|
111
|
+
"seed-wheel-picker-public__itemLabel--size_small",
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("Item을 9개 표시해도 Scroll Fog 최대 높이는 Item 3개 높이로 유지한다", () => {
|
|
116
|
+
const { getByRole } = render(
|
|
117
|
+
<WheelPicker.Root aria-label="긴 휠 피커" visibleItemCount={9}>
|
|
118
|
+
<WheelPicker.Column aria-label="문자" options={options} defaultValue="a" />
|
|
119
|
+
</WheelPicker.Root>,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(getByRole("group")).toHaveStyle({
|
|
123
|
+
"--seed-wheel-picker-public-viewport-size": "396px",
|
|
124
|
+
"--seed-wheel-picker-public-scroll-fog-max-height": "132px",
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("Root와 Column의 className을 허용한다", () => {
|
|
129
|
+
const { getByRole } = render(
|
|
130
|
+
<WheelPicker.Root aria-label="휠 피커" className="custom-root">
|
|
131
|
+
<WheelPicker.Column aria-label="문자" options={options} className="custom-column" />
|
|
132
|
+
</WheelPicker.Root>,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(getByRole("group")).toHaveClass("custom-root");
|
|
136
|
+
expect(getByRole("spinbutton")).toHaveClass("custom-column");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("renderLabel로 기본 ItemLabel을 대체한다", () => {
|
|
140
|
+
const { container, getByText } = render(
|
|
141
|
+
<WheelPicker.Root aria-label="휠 피커" itemSize={56}>
|
|
142
|
+
<WheelPicker.Column
|
|
143
|
+
aria-label="문자"
|
|
144
|
+
options={options}
|
|
145
|
+
renderLabel={(option) => <span data-custom-label="">{option.label}</span>}
|
|
146
|
+
/>
|
|
147
|
+
</WheelPicker.Root>,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(getByText("A")).toHaveAttribute("data-custom-label");
|
|
151
|
+
expect(getByText("A").parentElement).toHaveClass("seed-wheel-picker-public__item");
|
|
152
|
+
expect(container.querySelector("[data-wheel-picker-item-label]")).not.toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("renderLabel에서 공개 ItemLabel을 재사용한다", () => {
|
|
156
|
+
const { getByText } = render(
|
|
157
|
+
<WheelPicker.Root aria-label="휠 피커">
|
|
158
|
+
<WheelPicker.Column
|
|
159
|
+
aria-label="문자"
|
|
160
|
+
options={options}
|
|
161
|
+
renderLabel={(option) => (
|
|
162
|
+
<WheelPicker.ItemLabel data-reused-label="">{option.label}</WheelPicker.ItemLabel>
|
|
163
|
+
)}
|
|
164
|
+
/>
|
|
165
|
+
</WheelPicker.Root>,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(getByText("A")).toHaveClass("seed-wheel-picker-public__itemLabel");
|
|
169
|
+
expect(getByText("A")).toHaveAttribute("data-wheel-picker-item-label");
|
|
170
|
+
expect(getByText("A")).toHaveAttribute("data-reused-label");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("option의 ariaLabel을 기본 접근성 텍스트로 사용한다", () => {
|
|
174
|
+
const { getByRole } = render(
|
|
175
|
+
<WheelPicker.Root aria-label="배지 선택">
|
|
176
|
+
<WheelPicker.Column
|
|
177
|
+
aria-label="배지"
|
|
178
|
+
options={[{ value: "popular", label: <span>◆ 인기</span>, ariaLabel: "인기" }]}
|
|
179
|
+
/>
|
|
180
|
+
</WheelPicker.Root>,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(getByRole("spinbutton", { name: "배지" })).toHaveAttribute("aria-valuetext", "인기");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("숫자 label을 기본 접근성 텍스트로 변환한다", () => {
|
|
187
|
+
const { getByRole } = render(
|
|
188
|
+
<WheelPicker.Root aria-label="수량 선택">
|
|
189
|
+
<WheelPicker.Column aria-label="수량" options={[{ value: "one", label: 1 }]} />
|
|
190
|
+
</WheelPicker.Root>,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(getByRole("spinbutton", { name: "수량" })).toHaveAttribute("aria-valuetext", "1");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("Root의 disabled 상태를 Column과 Item에 전파한다", () => {
|
|
197
|
+
const { container, getByRole } = render(
|
|
198
|
+
<WheelPicker.Root aria-label="비활성 휠 피커" disabled>
|
|
199
|
+
<WheelPicker.Column aria-label="문자" options={options} defaultValue="a" />
|
|
200
|
+
</WheelPicker.Root>,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(getByRole("group")).toHaveAttribute("data-disabled");
|
|
204
|
+
expect(getByRole("spinbutton")).toHaveAttribute("data-disabled");
|
|
205
|
+
expect(getByRole("spinbutton")).toHaveAttribute("aria-disabled", "true");
|
|
206
|
+
expect(container.querySelector('[data-wheel-picker-value="a"]')).toHaveAttribute(
|
|
207
|
+
"data-selected",
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("지원하지 않는 readOnly 값이 런타임에 전달되어도 읽기 전용으로 동작하지 않는다", () => {
|
|
212
|
+
const RootWithLegacyReadOnly = WheelPicker.Root as React.ComponentType<
|
|
213
|
+
WheelPicker.RootProps & { readOnly?: boolean }
|
|
214
|
+
>;
|
|
215
|
+
const { getByRole } = render(
|
|
216
|
+
<RootWithLegacyReadOnly aria-label="휠 피커" readOnly>
|
|
217
|
+
<WheelPicker.Column aria-label="문자" options={options} defaultValue="a" />
|
|
218
|
+
</RootWithLegacyReadOnly>,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
expect(getByRole("group")).not.toHaveAttribute("data-readonly");
|
|
222
|
+
expect(getByRole("spinbutton")).not.toHaveAttribute("aria-readonly");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("BottomSheet 안에서 컬럼을 스크롤할 때 시트 드래그를 시작하지 않는다", () => {
|
|
226
|
+
const originalSetPointerCapture = HTMLElement.prototype.setPointerCapture;
|
|
227
|
+
HTMLElement.prototype.setPointerCapture = mock(() => {});
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const { container, getByRole } = render(
|
|
231
|
+
<BottomSheet.Root defaultOpen modal={false} autoFocus={false} skipAnimation>
|
|
232
|
+
<BottomSheet.Content>
|
|
233
|
+
<BottomSheet.Title>값 선택</BottomSheet.Title>
|
|
234
|
+
<BottomSheet.Description>원하는 값을 선택하세요.</BottomSheet.Description>
|
|
235
|
+
<WheelPicker.Root aria-label="휠 피커">
|
|
236
|
+
<WheelPicker.Column aria-label="문자" options={options} defaultValue="b" />
|
|
237
|
+
</WheelPicker.Root>
|
|
238
|
+
</BottomSheet.Content>
|
|
239
|
+
</BottomSheet.Root>,
|
|
240
|
+
);
|
|
241
|
+
const content = container.ownerDocument.querySelector("[data-drawer]") as HTMLElement;
|
|
242
|
+
const column = getByRole("spinbutton");
|
|
243
|
+
|
|
244
|
+
content.style.transform = "matrix(1, 0, 0, 1, 0, 0)";
|
|
245
|
+
spyOn(content, "getBoundingClientRect").mockReturnValue({
|
|
246
|
+
x: 0,
|
|
247
|
+
y: 0,
|
|
248
|
+
width: 320,
|
|
249
|
+
height: 400,
|
|
250
|
+
top: 0,
|
|
251
|
+
left: 0,
|
|
252
|
+
right: 320,
|
|
253
|
+
bottom: 400,
|
|
254
|
+
toJSON: () => {},
|
|
255
|
+
});
|
|
256
|
+
Object.defineProperties(column, {
|
|
257
|
+
clientHeight: { value: 220, configurable: true },
|
|
258
|
+
scrollHeight: { value: 440, configurable: true },
|
|
259
|
+
});
|
|
260
|
+
column.scrollTop = 44;
|
|
261
|
+
|
|
262
|
+
fireTouchPointer("pointerdown", column, 100);
|
|
263
|
+
fireTouchPointer("pointermove", column, 130);
|
|
264
|
+
|
|
265
|
+
expect(content.style.transform).toBe("matrix(1, 0, 0, 1, 0, 0)");
|
|
266
|
+
} finally {
|
|
267
|
+
HTMLElement.prototype.setPointerCapture = originalSetPointerCapture;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
wheelPickerPublic,
|
|
5
|
+
type WheelPickerPublicVariantProps,
|
|
6
|
+
} from "@seed-design/css/recipes/wheel-picker-public";
|
|
7
|
+
import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
|
|
8
|
+
import {
|
|
9
|
+
WheelPicker as WheelPickerPrimitive,
|
|
10
|
+
type WheelPickerOption,
|
|
11
|
+
} from "@seed-design/react-wheel-picker";
|
|
12
|
+
import clsx from "clsx";
|
|
13
|
+
import * as React from "react";
|
|
14
|
+
import { createSlotRecipeContext } from "../../utils/createSlotRecipeContext";
|
|
15
|
+
import { ScrollFog } from "../ScrollFog/ScrollFog";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_ITEM_SIZE = {
|
|
18
|
+
small: 36,
|
|
19
|
+
medium: 44,
|
|
20
|
+
} as const;
|
|
21
|
+
const DEFAULT_VISIBLE_ITEM_COUNT = 5;
|
|
22
|
+
const { ClassNamesProvider, useClassNames } = createSlotRecipeContext(wheelPickerPublic);
|
|
23
|
+
|
|
24
|
+
type WheelPickerCssProperties = React.CSSProperties & {
|
|
25
|
+
"--seed-wheel-picker-public-item-size"?: string;
|
|
26
|
+
"--seed-wheel-picker-public-visible-item-count"?: number;
|
|
27
|
+
"--seed-wheel-picker-public-viewport-size"?: string;
|
|
28
|
+
"--seed-wheel-picker-public-center-offset"?: string;
|
|
29
|
+
"--seed-wheel-picker-public-scroll-fog-max-height"?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface WheelPickerRootProps
|
|
33
|
+
extends WheelPickerPublicVariantProps,
|
|
34
|
+
Omit<
|
|
35
|
+
WheelPickerPrimitive.RootProps,
|
|
36
|
+
"asChild" | "children" | "disabled" | "itemSize" | "readOnly" | "visibleItemCount"
|
|
37
|
+
> {
|
|
38
|
+
/** Wheel Picker를 구성하는 `WheelPicker.Column` 목록입니다. */
|
|
39
|
+
children: React.ReactNode;
|
|
40
|
+
|
|
41
|
+
/** 모든 컬럼의 포커스와 값 변경을 막습니다. */
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 한 항목의 높이입니다. 지정하지 않으면 `size`의 기본 높이를 사용합니다.
|
|
46
|
+
*/
|
|
47
|
+
itemSize?: number;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 화면에 보이는 항목 수입니다. 5 이상의 홀수를 권장합니다.
|
|
51
|
+
* @default 5
|
|
52
|
+
*/
|
|
53
|
+
visibleItemCount?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const WheelPickerRoot = React.forwardRef<HTMLDivElement, WheelPickerRootProps>(
|
|
57
|
+
(props, ref) => {
|
|
58
|
+
const [variantProps, otherProps] = wheelPickerPublic.splitVariantProps(props);
|
|
59
|
+
const {
|
|
60
|
+
children,
|
|
61
|
+
className,
|
|
62
|
+
itemSize: itemSizeProp,
|
|
63
|
+
style,
|
|
64
|
+
visibleItemCount = DEFAULT_VISIBLE_ITEM_COUNT,
|
|
65
|
+
...rootProps
|
|
66
|
+
} = otherProps;
|
|
67
|
+
const size = variantProps.size ?? "medium";
|
|
68
|
+
const itemSize = itemSizeProp ?? DEFAULT_ITEM_SIZE[size];
|
|
69
|
+
const centerOffset = ((visibleItemCount - 1) / 2) * itemSize;
|
|
70
|
+
const classNames = wheelPickerPublic({ size });
|
|
71
|
+
const wheelPickerStyle: WheelPickerCssProperties = {
|
|
72
|
+
...style,
|
|
73
|
+
"--seed-wheel-picker-public-item-size": `${itemSize}px`,
|
|
74
|
+
"--seed-wheel-picker-public-visible-item-count": visibleItemCount,
|
|
75
|
+
"--seed-wheel-picker-public-viewport-size": `${itemSize * visibleItemCount}px`,
|
|
76
|
+
"--seed-wheel-picker-public-center-offset": `${centerOffset}px`,
|
|
77
|
+
"--seed-wheel-picker-public-scroll-fog-max-height": `${itemSize * 3}px`,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<ClassNamesProvider value={classNames}>
|
|
82
|
+
<WheelPickerPrimitive.Root
|
|
83
|
+
ref={ref}
|
|
84
|
+
className={clsx(classNames.root, className)}
|
|
85
|
+
itemSize={itemSize}
|
|
86
|
+
style={wheelPickerStyle}
|
|
87
|
+
visibleItemCount={visibleItemCount}
|
|
88
|
+
{...rootProps}
|
|
89
|
+
readOnly={false}
|
|
90
|
+
>
|
|
91
|
+
<Primitive.div
|
|
92
|
+
aria-hidden
|
|
93
|
+
className={classNames.selectionIndicator}
|
|
94
|
+
data-wheel-picker-indicator=""
|
|
95
|
+
/>
|
|
96
|
+
<ScrollFog
|
|
97
|
+
className={classNames.scrollFog}
|
|
98
|
+
placement={["top", "bottom"]}
|
|
99
|
+
size="var(--seed-wheel-picker-public-scroll-fog-size)"
|
|
100
|
+
hideScrollBar
|
|
101
|
+
data-wheel-picker-scroll-fog=""
|
|
102
|
+
>
|
|
103
|
+
<Primitive.div className={classNames.columns} data-wheel-picker-columns="">
|
|
104
|
+
{children}
|
|
105
|
+
</Primitive.div>
|
|
106
|
+
</ScrollFog>
|
|
107
|
+
</WheelPickerPrimitive.Root>
|
|
108
|
+
</ClassNamesProvider>
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
WheelPickerRoot.displayName = "WheelPickerRoot";
|
|
113
|
+
|
|
114
|
+
export interface WheelPickerItemLabelProps
|
|
115
|
+
extends PrimitiveProps,
|
|
116
|
+
React.HTMLAttributes<HTMLDivElement> {}
|
|
117
|
+
|
|
118
|
+
/** Wheel Picker 항목의 기본 여백과 타이포그래피를 적용합니다. */
|
|
119
|
+
export const WheelPickerItemLabel = React.forwardRef<HTMLDivElement, WheelPickerItemLabelProps>(
|
|
120
|
+
({ className, ...props }, ref) => {
|
|
121
|
+
const classNames = useClassNames();
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Primitive.div
|
|
125
|
+
ref={ref}
|
|
126
|
+
className={clsx(classNames.itemLabel, className)}
|
|
127
|
+
{...props}
|
|
128
|
+
data-wheel-picker-item-label=""
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
WheelPickerItemLabel.displayName = "WheelPickerItemLabel";
|
|
134
|
+
|
|
135
|
+
export interface WheelPickerColumnProps
|
|
136
|
+
extends Omit<
|
|
137
|
+
WheelPickerPrimitive.ColumnProps,
|
|
138
|
+
| "asChild"
|
|
139
|
+
| "defaultValue"
|
|
140
|
+
| "getAriaValueText"
|
|
141
|
+
| "loop"
|
|
142
|
+
| "onIndexChange"
|
|
143
|
+
| "onValueChange"
|
|
144
|
+
| "options"
|
|
145
|
+
| "renderOption"
|
|
146
|
+
| "value"
|
|
147
|
+
| "valueChangeBehavior"
|
|
148
|
+
> {
|
|
149
|
+
/** 컬럼에 표시할 선택 항목입니다. 하나 이상의 항목을 제공해야 합니다. */
|
|
150
|
+
options: readonly WheelPickerOption[];
|
|
151
|
+
|
|
152
|
+
/** 제어 상태에서 현재 선택된 항목의 값입니다. */
|
|
153
|
+
value?: string;
|
|
154
|
+
|
|
155
|
+
/** 비제어 상태에서 처음 선택할 항목의 값입니다. */
|
|
156
|
+
defaultValue?: string;
|
|
157
|
+
|
|
158
|
+
/** 선택 값이 바뀔 때 호출됩니다. */
|
|
159
|
+
onValueChange?: WheelPickerPrimitive.ColumnProps["onValueChange"];
|
|
160
|
+
|
|
161
|
+
/** 사용자 조작으로 중앙 항목이 바뀔 때마다 호출됩니다. */
|
|
162
|
+
onIndexChange?: WheelPickerPrimitive.ColumnProps["onIndexChange"];
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 마지막 항목과 첫 항목을 이어 반복해서 탐색할지 여부입니다.
|
|
166
|
+
* @default false
|
|
167
|
+
*/
|
|
168
|
+
loop?: boolean;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 외부에서 `value`가 변경되었을 때 새 값으로 이동하는 스크롤 방식입니다.
|
|
172
|
+
* @default "auto"
|
|
173
|
+
*/
|
|
174
|
+
valueChangeBehavior?: ScrollBehavior;
|
|
175
|
+
|
|
176
|
+
/** 현재 값에 대응하는 접근성 텍스트를 반환합니다. */
|
|
177
|
+
getAriaValueText?: (value: string) => string;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 기본 `ItemLabel` 대신 항목에 표시할 요소를 반환합니다.
|
|
181
|
+
* 반환한 요소는 항목의 선택·비활성 색상을 적용할 수 있도록 `currentColor`를 상속해야 합니다.
|
|
182
|
+
*/
|
|
183
|
+
renderLabel?: (option: WheelPickerOption) => React.ReactNode;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const WheelPickerColumn = React.forwardRef<HTMLDivElement, WheelPickerColumnProps>(
|
|
187
|
+
({ className, renderLabel, ...props }, ref) => {
|
|
188
|
+
const classNames = useClassNames();
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<WheelPickerPrimitive.Column
|
|
192
|
+
ref={ref}
|
|
193
|
+
className={clsx(classNames.column, className)}
|
|
194
|
+
{...props}
|
|
195
|
+
renderOption={(option, optionProps) => (
|
|
196
|
+
<Primitive.div className={classNames.item} {...optionProps}>
|
|
197
|
+
{renderLabel ? (
|
|
198
|
+
renderLabel(option)
|
|
199
|
+
) : (
|
|
200
|
+
<WheelPickerItemLabel>{option.label}</WheelPickerItemLabel>
|
|
201
|
+
)}
|
|
202
|
+
</Primitive.div>
|
|
203
|
+
)}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
WheelPickerColumn.displayName = "WheelPickerColumn";
|
|
209
|
+
|
|
210
|
+
export type { WheelPickerOption };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export {
|
|
2
|
+
WheelPickerRoot,
|
|
3
|
+
WheelPickerColumn,
|
|
4
|
+
WheelPickerItemLabel,
|
|
5
|
+
type WheelPickerRootProps,
|
|
6
|
+
type WheelPickerColumnProps,
|
|
7
|
+
type WheelPickerItemLabelProps,
|
|
8
|
+
type WheelPickerOption,
|
|
9
|
+
} from "./WheelPicker";
|
|
10
|
+
|
|
11
|
+
export * as WheelPicker from "./WheelPicker.namespace";
|
package/src/components/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ describe("InternalWheelPicker", () => {
|
|
|
49
49
|
expect(columns).toHaveClass("seed-wheel-picker__columns", "columns");
|
|
50
50
|
expect(getAllByRole("spinbutton")).toHaveLength(2);
|
|
51
51
|
expect(firstColumn).toHaveClass("seed-wheel-picker__column");
|
|
52
|
+
expect(firstColumn).not.toHaveClass("seed-wheel-picker-public__column");
|
|
52
53
|
expect(firstColumn.firstElementChild).toHaveClass("seed-wheel-picker__item");
|
|
53
54
|
});
|
|
54
55
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { wheelPicker } from "@seed-design/css/recipes/wheel-picker";
|
|
4
4
|
import { Primitive } from "@seed-design/react-primitive";
|
|
5
|
-
import
|
|
5
|
+
import { WheelPicker as WheelPickerPrimitive } from "@seed-design/react-wheel-picker";
|
|
6
6
|
import clsx from "clsx";
|
|
7
7
|
import * as React from "react";
|
|
8
8
|
import { ScrollFog, type ScrollFogProps } from "../ScrollFog/ScrollFog";
|
|
@@ -17,7 +17,7 @@ type WheelPickerCssProperties = React.CSSProperties & {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
export interface InternalWheelPickerRootProps
|
|
20
|
-
extends Omit<
|
|
20
|
+
extends Omit<WheelPickerPrimitive.RootProps, "asChild" | "children"> {
|
|
21
21
|
children: React.ReactNode;
|
|
22
22
|
columnsClassName?: string;
|
|
23
23
|
scrollFogClassName?: string;
|
|
@@ -54,7 +54,7 @@ export const InternalWheelPickerRoot = React.forwardRef<
|
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
return (
|
|
57
|
-
<
|
|
57
|
+
<WheelPickerPrimitive.Root
|
|
58
58
|
ref={ref}
|
|
59
59
|
itemSize={itemSize}
|
|
60
60
|
visibleItemCount={visibleItemCount}
|
|
@@ -81,14 +81,14 @@ export const InternalWheelPickerRoot = React.forwardRef<
|
|
|
81
81
|
{children}
|
|
82
82
|
</Primitive.div>
|
|
83
83
|
</ScrollFog>
|
|
84
|
-
</
|
|
84
|
+
</WheelPickerPrimitive.Root>
|
|
85
85
|
);
|
|
86
86
|
},
|
|
87
87
|
);
|
|
88
88
|
InternalWheelPickerRoot.displayName = "InternalWheelPickerRoot";
|
|
89
89
|
|
|
90
90
|
export interface InternalWheelPickerColumnProps
|
|
91
|
-
extends Omit<
|
|
91
|
+
extends Omit<WheelPickerPrimitive.ColumnProps, "asChild" | "renderOption"> {
|
|
92
92
|
itemClassName?: string;
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -96,7 +96,7 @@ export const InternalWheelPickerColumn = React.forwardRef<
|
|
|
96
96
|
HTMLDivElement,
|
|
97
97
|
InternalWheelPickerColumnProps
|
|
98
98
|
>(({ className, itemClassName, ...props }, ref) => (
|
|
99
|
-
<
|
|
99
|
+
<WheelPickerPrimitive.Column
|
|
100
100
|
ref={ref}
|
|
101
101
|
className={clsx(classNames.column, className)}
|
|
102
102
|
renderOption={(option, optionProps) => (
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type { ResponsiveValue, UnwrapResponsive, BreakpointThreshold } from "./t
|
|
|
5
5
|
export { useBreakpoint } from "./hooks/useBreakpoint";
|
|
6
6
|
export type { UseBreakpointOptions } from "./hooks/useBreakpoint";
|
|
7
7
|
export { useBreakpointValue } from "./hooks/useBreakpointValue";
|
|
8
|
+
export { useScaleFeedback, ScaleFeedback } from "./utils/scaleFeedback";
|
|
9
|
+
export type { ScaleFeedbackProps } from "./utils/scaleFeedback";
|
|
8
10
|
export { BreakpointProvider } from "./providers/BreakpointProvider";
|
|
9
11
|
export type { BreakpointProviderProps } from "./providers/BreakpointProvider";
|
|
10
12
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Publishes an element's rendered size as CSS custom properties.
|
|
5
|
+
*
|
|
6
|
+
* The size is a plain fact about the element rather than a feature, so this
|
|
7
|
+
* util keeps unitless `--seed-element-width` / `--seed-element-height` in sync
|
|
8
|
+
* on the element and stops there. Whatever needs the size derives its own
|
|
9
|
+
* values from those vars in CSS.
|
|
10
|
+
*
|
|
11
|
+
* The names must stay in sync with `@seed-design/qvism-preset`
|
|
12
|
+
* (utils/scale-feedback.ts), which owns the formula that consumes them. Press
|
|
13
|
+
* detection stays in CSS (`:active` gates), so nothing here tracks interaction;
|
|
14
|
+
* without JS the vars are simply never set and consumers fall back to scale 1.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// One observer for every measured element; created lazily so the module stays
|
|
18
|
+
// importable during SSR.
|
|
19
|
+
let sharedObserver: ResizeObserver | null = null;
|
|
20
|
+
|
|
21
|
+
function getSharedObserver() {
|
|
22
|
+
sharedObserver ??= new ResizeObserver((entries) => {
|
|
23
|
+
// `borderBoxSize` is absent before Safari 15.4, inside the range SEED
|
|
24
|
+
// supports, so the `offsetWidth` / `offsetHeight` fallback is a live path
|
|
25
|
+
// rather than a formality. Reading every entry before writing any property
|
|
26
|
+
// is what keeps it free: layout is already clean when observations are
|
|
27
|
+
// broadcast, so the reads force nothing, while a write interleaved between
|
|
28
|
+
// them would dirty layout and make every later read reflow.
|
|
29
|
+
const measurements = entries.map((entry) => {
|
|
30
|
+
const element = entry.target as HTMLElement;
|
|
31
|
+
const size = entry.borderBoxSize?.[0];
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
element,
|
|
35
|
+
width: size ? size.inlineSize : element.offsetWidth,
|
|
36
|
+
height: size ? size.blockSize : element.offsetHeight,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
for (const { element, width, height } of measurements) {
|
|
41
|
+
element.style.setProperty("--seed-element-width", String(width));
|
|
42
|
+
element.style.setProperty("--seed-element-height", String(height));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return sharedObserver;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns a callback ref that keeps the size vars in sync on the element it is
|
|
51
|
+
* attached to. Compose it with the element's other refs however you like: being
|
|
52
|
+
* handed an element it already watches is a no-op, so the observation survives a
|
|
53
|
+
* ref whose identity changes every render and the vars are republished on resize
|
|
54
|
+
* rather than on render.
|
|
55
|
+
*
|
|
56
|
+
* Teardown still runs when the ref genuinely leaves an element — it is unobserved
|
|
57
|
+
* and its vars removed — so an element that keeps the class but loses the ref
|
|
58
|
+
* falls back to a ratio of 1 instead of holding a stale one.
|
|
59
|
+
*/
|
|
60
|
+
export function useElementSizeVars() {
|
|
61
|
+
// What React last handed the ref, against what the observer is watching. The
|
|
62
|
+
// two disagree for exactly one commit whenever a ref identity changes, because
|
|
63
|
+
// React detaches with `null` before re-attaching the very same element.
|
|
64
|
+
const attachedRef = React.useRef<HTMLElement | null>(null);
|
|
65
|
+
const observedRef = React.useRef<HTMLElement | null>(null);
|
|
66
|
+
|
|
67
|
+
const release = React.useCallback(() => {
|
|
68
|
+
const observed = observedRef.current;
|
|
69
|
+
if (!observed) return;
|
|
70
|
+
|
|
71
|
+
observedRef.current = null;
|
|
72
|
+
getSharedObserver().unobserve(observed);
|
|
73
|
+
observed.style.removeProperty("--seed-element-width");
|
|
74
|
+
observed.style.removeProperty("--seed-element-height");
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const sizeVarsRef = React.useCallback(
|
|
78
|
+
(node: HTMLElement | null) => {
|
|
79
|
+
attachedRef.current = node;
|
|
80
|
+
|
|
81
|
+
if (!node) {
|
|
82
|
+
// A commit runs to completion synchronously, so this cannot land before
|
|
83
|
+
// the re-attach that an identity change performs within the same commit.
|
|
84
|
+
// By the time it runs, the two refs tell a detached element apart from
|
|
85
|
+
// one that never actually left.
|
|
86
|
+
queueMicrotask(() => {
|
|
87
|
+
if (attachedRef.current === observedRef.current) return;
|
|
88
|
+
|
|
89
|
+
release();
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (observedRef.current === node) return;
|
|
95
|
+
|
|
96
|
+
release();
|
|
97
|
+
observedRef.current = node;
|
|
98
|
+
// `box` selects which box change marks the element active — it does not
|
|
99
|
+
// decide which sizes the entry carries (all of them are always present).
|
|
100
|
+
// Observing the content box would miss a border-box change that leaves the
|
|
101
|
+
// content box untouched, e.g. a variant that only alters padding.
|
|
102
|
+
//
|
|
103
|
+
// Before Safari 15.4 `observe` declares no options at all, so this argument
|
|
104
|
+
// is dropped and that miss is exactly what happens there. Bounded: the ratio
|
|
105
|
+
// stays valid, only derived from the last size whose content box moved.
|
|
106
|
+
getSharedObserver().observe(node, { box: "border-box" });
|
|
107
|
+
},
|
|
108
|
+
[release],
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return { sizeVarsRef };
|
|
112
|
+
}
|