@seed-design/react 2.3.0 → 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/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 +4 -4
- 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 +20 -0
- package/src/components/DatePicker/DatePicker.tsx +14 -2
- 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/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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { SCALE_FEEDBACK_CLASS_NAME } from "@seed-design/css/scale-feedback";
|
|
2
|
+
import { useComposedRefs } from "@radix-ui/react-compose-refs";
|
|
3
|
+
import { createSlot } from "@radix-ui/react-slot";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { useElementSizeVars } from "./elementSizeVars";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Opts an element into scale feedback: put `scaleFeedbackRef` and
|
|
10
|
+
* `scaleFeedbackClassName` on the same element, and it publishes
|
|
11
|
+
* `--seed-feedback-scale`, a ratio that shrinks it by a fixed 2px when consumed.
|
|
12
|
+
* `ScaleFeedback` pairs them up for an element you render; reach for the hook when
|
|
13
|
+
* you can't wrap one, such as inside a component already assembling its own
|
|
14
|
+
* `ref` and `className`.
|
|
15
|
+
*
|
|
16
|
+
* The two halves belong together — the size vars `useElementSizeVars` publishes,
|
|
17
|
+
* and the class that derives the ratio from them — so the hook hands them back
|
|
18
|
+
* as a pair. If one goes missing the derivation stays guaranteed-invalid and
|
|
19
|
+
* `--seed-feedback-scale` resolves to 1: a mistake costs the effect rather than
|
|
20
|
+
* producing a wrong scale.
|
|
21
|
+
*
|
|
22
|
+
* Applying the ratio is yours, because the selector that scales the element is
|
|
23
|
+
* usually the same one that changes its background, and splitting them across
|
|
24
|
+
* two rules lets them fire under different conditions:
|
|
25
|
+
*
|
|
26
|
+
* .my-button {
|
|
27
|
+
* transition: background-color 0.2s, var(--seed-feedback-scale-transition);
|
|
28
|
+
* }
|
|
29
|
+
* .my-button:active {
|
|
30
|
+
* background-color: ...;
|
|
31
|
+
* scale: var(--seed-feedback-scale);
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* The resting `scale: 1` comes with the class rather than with the rule above:
|
|
35
|
+
* it keeps the element a stacking context throughout, instead of becoming one
|
|
36
|
+
* mid-press and dragging `position: fixed` descendants with it.
|
|
37
|
+
*/
|
|
38
|
+
export function useScaleFeedback() {
|
|
39
|
+
const { sizeVarsRef } = useElementSizeVars();
|
|
40
|
+
|
|
41
|
+
return { scaleFeedbackRef: sizeVarsRef, scaleFeedbackClassName: SCALE_FEEDBACK_CLASS_NAME };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ScaleFeedbackSlot = createSlot("ScaleFeedback");
|
|
45
|
+
|
|
46
|
+
export interface ScaleFeedbackProps {
|
|
47
|
+
children: React.ReactElement;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Opts its child into scale feedback, so the ref and the class can no longer
|
|
52
|
+
* land on different elements or go missing one at a time. The child keeps its own
|
|
53
|
+
* `ref` and `className`, and applying the ratio is still yours — see
|
|
54
|
+
* `useScaleFeedback` for the rule this expects you to write.
|
|
55
|
+
*
|
|
56
|
+
* There is no `asChild` prop because rendering an element of our own is never the
|
|
57
|
+
* right answer: that element would be the one measured, and a block wrapper around
|
|
58
|
+
* an inline-block button reports the container's width, so the ratio would come
|
|
59
|
+
* out wrong rather than absent. The resting `scale: 1` would settle on the wrapper
|
|
60
|
+
* too, leaving the child to become a stacking context mid-press.
|
|
61
|
+
*/
|
|
62
|
+
export const ScaleFeedback = React.forwardRef<HTMLElement, ScaleFeedbackProps>(
|
|
63
|
+
({ children }, ref) => {
|
|
64
|
+
const { scaleFeedbackRef, scaleFeedbackClassName } = useScaleFeedback();
|
|
65
|
+
|
|
66
|
+
const childRef = React.useRef<HTMLElement | null>(null);
|
|
67
|
+
const composedRef = useComposedRefs(scaleFeedbackRef, childRef, ref);
|
|
68
|
+
|
|
69
|
+
React.useEffect(() => {
|
|
70
|
+
if (process.env.NODE_ENV === "production") return;
|
|
71
|
+
|
|
72
|
+
// Slot throws on children it can't slot onto, but a Fragment child is a valid
|
|
73
|
+
// element it silently declines to hand a ref to, and a component child is free
|
|
74
|
+
// to drop what it is handed. Both leave a ratio of 1 and no other trace.
|
|
75
|
+
const node = childRef.current;
|
|
76
|
+
if (!node) {
|
|
77
|
+
console.warn(
|
|
78
|
+
"ScaleFeedback: the child never forwarded `ref` to a DOM element, so its size is never measured. A Fragment cannot receive one.",
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!node.classList.contains(SCALE_FEEDBACK_CLASS_NAME)) {
|
|
84
|
+
console.warn(
|
|
85
|
+
"ScaleFeedback: the child dropped the `className` it was handed, so no ratio is derived. Forward `className` to the element that takes `ref`.",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<ScaleFeedbackSlot ref={composedRef} className={scaleFeedbackClassName}>
|
|
92
|
+
{children}
|
|
93
|
+
</ScaleFeedbackSlot>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
ScaleFeedback.displayName = "ScaleFeedback";
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Wraps a component so its element opts into scale feedback — for one-liner
|
|
102
|
+
* factory components (`withContext`/`withProvider` results). Hand-written
|
|
103
|
+
* forwardRef components call `useScaleFeedback` directly instead.
|
|
104
|
+
*/
|
|
105
|
+
export function withScaleFeedback<P>(
|
|
106
|
+
Component: React.ForwardRefExoticComponent<P>,
|
|
107
|
+
): React.ForwardRefExoticComponent<P>;
|
|
108
|
+
export function withScaleFeedback(
|
|
109
|
+
Component: React.ForwardRefExoticComponent<
|
|
110
|
+
React.RefAttributes<HTMLElement> & { className?: string }
|
|
111
|
+
>,
|
|
112
|
+
): React.ForwardRefExoticComponent<React.RefAttributes<HTMLElement> & { className?: string }> {
|
|
113
|
+
const Pressable = React.forwardRef<HTMLElement, { className?: string }>((props, ref) => {
|
|
114
|
+
const { scaleFeedbackRef, scaleFeedbackClassName } = useScaleFeedback();
|
|
115
|
+
const composedRef = useComposedRefs(scaleFeedbackRef, ref);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Component
|
|
119
|
+
{...props}
|
|
120
|
+
ref={composedRef}
|
|
121
|
+
className={clsx(scaleFeedbackClassName, props.className)}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
Pressable.displayName = Component.displayName;
|
|
127
|
+
return Pressable;
|
|
128
|
+
}
|