Skip to content

Commit 3fc9608

Browse files
te6-inclaude
andcommitted
feat(react): render pressed content-scale layout layers
Wrap slot children in the layout layer for SelectBox roots, MenuItem, MenuSheetItem, SwipeableMenuSheetItem, AccordionTrigger, and SegmentedControlItem, and add the public FieldButton.Layout slot (input-button composes content next to the background button overlay, so the wrapper is an explicit composition point). asChild is preserved via wrapSlotChildren: the consumer element stays the slot target and the layer lands inside it, bailing out (no scale) for chained asChild, childless/void, and render-prop children. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5fe25c2 commit 3fc9608

11 files changed

Lines changed: 297 additions & 32 deletions

File tree

packages/react/src/components/Accordion/Accordion.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import {
66
useAccordionItemContext,
77
} from "@seed-design/react-accordion";
88
import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
9+
import clsx from "clsx";
910
import type * as React from "react";
11+
import { forwardRef } from "react";
1012
import { createSlotRecipeContext } from "../../utils/createSlotRecipeContext";
1113
import { createWithStateProps } from "../../utils/createWithStateProps";
14+
import { wrapSlotChildren } from "../../utils/wrapSlotChildren";
1215

13-
const { withProvider, withContext } = createSlotRecipeContext(accordion);
16+
const { withProvider, withContext, useClassNames } = createSlotRecipeContext(accordion);
1417
const withStateProps = createWithStateProps([useAccordionItemContext]);
1518

1619
////////////////////////////////////////////////////////////////////////////////////
@@ -47,9 +50,25 @@ AccordionHeader.displayName = "AccordionHeader";
4750

4851
export interface AccordionTriggerProps extends AccordionPrimitive.TriggerProps {}
4952

50-
export const AccordionTrigger = withContext<HTMLButtonElement, AccordionTriggerProps>(
51-
AccordionPrimitive.Trigger,
52-
"trigger",
53+
export const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
54+
({ className, children, ...props }, ref) => {
55+
const classNames = useClassNames();
56+
57+
return (
58+
<AccordionPrimitive.Trigger
59+
ref={ref}
60+
className={clsx(classNames.trigger, className)}
61+
{...props}
62+
>
63+
{/* layout layer — scales the trigger's content as a whole on press while the
64+
pressed background stays on trigger::before. With asChild it is injected
65+
inside the consumer's element instead. */}
66+
{wrapSlotChildren(props.asChild, children, (layoutChildren) => (
67+
<div className={classNames.layout}>{layoutChildren}</div>
68+
))}
69+
</AccordionPrimitive.Trigger>
70+
);
71+
},
5372
);
5473
AccordionTrigger.displayName = "AccordionTrigger";
5574

packages/react/src/components/FieldButton/FieldButton.namespace.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export {
1414
FieldButtonPrefixText as PrefixText,
1515
FieldButtonRequiredIndicator as RequiredIndicator,
1616
FieldButtonControl as Control,
17+
FieldButtonLayout as Layout,
1718
FieldButtonSuffixIcon as SuffixIcon,
1819
FieldButtonSuffixText as SuffixText,
1920
FieldButtonValue as Value,
@@ -32,6 +33,7 @@ export {
3233
type FieldButtonPrefixTextProps as PrefixTextProps,
3334
type FieldButtonRequiredIndicatorProps as RequiredIndicatorProps,
3435
type FieldButtonControlProps as ControlProps,
36+
type FieldButtonLayoutProps as LayoutProps,
3537
type FieldButtonSuffixIconProps as SuffixIconProps,
3638
type FieldButtonSuffixTextProps as SuffixTextProps,
3739
type FieldButtonValueProps as ValueProps,

packages/react/src/components/FieldButton/FieldButton.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,20 @@ export const FieldButtonControl = withProvider<HTMLDivElement, FieldButtonContro
225225
"root",
226226
);
227227

228+
export interface FieldButtonLayoutProps
229+
extends PrimitiveProps,
230+
React.HTMLAttributes<HTMLDivElement> {}
231+
232+
/**
233+
* Wraps the control's visible content (everything except `FieldButton.Button`) so it
234+
* scales as a whole on press while the background stays on the button overlay.
235+
* Compositions without it simply skip the pressed scale.
236+
*/
237+
export const FieldButtonLayout = withContext<HTMLDivElement, FieldButtonLayoutProps>(
238+
Primitive.div,
239+
"layout",
240+
);
241+
228242
export interface FieldButtonClearButtonProps extends FieldButton.ClearButtonProps {}
229243

230244
export const FieldButtonClearButton = withContext<HTMLButtonElement, FieldButtonClearButtonProps>(

packages/react/src/components/Menu/Menu.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import clsx from "clsx";
88
import * as React from "react";
99
import { createSlotRecipeContext } from "../../utils/createSlotRecipeContext";
1010
import { createWithStateProps } from "../../utils/createWithStateProps";
11+
import { wrapSlotChildren } from "../../utils/wrapSlotChildren";
1112

1213
const { ClassNamesProvider, withContext, useClassNames } = createSlotRecipeContext(menu);
1314
const {
@@ -108,7 +109,7 @@ export const MenuGroupLabel = withContext<HTMLDivElement, MenuGroupLabelProps>(
108109
export interface MenuItemProps extends MenuItemVariantProps, MenuPrimitive.ItemProps {}
109110

110111
export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
111-
({ className: propClassName, ...props }, ref) => {
112+
({ className: propClassName, children, ...props }, ref) => {
112113
const [variantProps, otherProps] = menuItem.splitVariantProps(props);
113114
const parentProps = useItemProps();
114115

@@ -120,7 +121,14 @@ export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
120121
ref={ref}
121122
className={clsx(classNames.root, propClassName)}
122123
{...otherProps}
123-
/>
124+
>
125+
{/* layout layer — scales as a whole on press while the pressed background
126+
stays on root::before. With asChild it is injected inside the
127+
consumer's element instead. */}
128+
{wrapSlotChildren(otherProps.asChild, children, (layoutChildren) => (
129+
<div className={classNames.layout}>{layoutChildren}</div>
130+
))}
131+
</MenuPrimitive.Item>
124132
</ItemClassNamesProvider>
125133
);
126134
},

packages/react/src/components/MenuSheet/MenuSheet.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import * as React from "react";
99
import { createSlotRecipeContext } from "../../utils/createSlotRecipeContext";
1010
import { createWithStateProps } from "../../utils/createWithStateProps";
11+
import { wrapSlotChildren } from "../../utils/wrapSlotChildren";
1112
import clsx from "clsx";
1213

1314
const { withRootProvider, withContext, useClassNames } = createSlotRecipeContext(menuSheet);
@@ -227,7 +228,7 @@ export interface MenuSheetItemProps
227228
* @deprecated Use `SwipeableMenuSheet` instead.
228229
*/
229230
export const MenuSheetItem = React.forwardRef<HTMLButtonElement, MenuSheetItemProps>(
230-
({ className: propClassName, ...props }, ref) => {
231+
({ className: propClassName, children, ...props }, ref) => {
231232
const [variantProps, otherProps] = menuSheetItem.splitVariantProps(props);
232233
const parentProps = useItemProps();
233234

@@ -241,7 +242,14 @@ export const MenuSheetItem = React.forwardRef<HTMLButtonElement, MenuSheetItemPr
241242
className={clsx(classNames.root, propClassName)}
242243
{...stateProps}
243244
{...otherProps}
244-
/>
245+
>
246+
{/* layout layer — scales as a whole on press while the pressed background
247+
stays on root. With asChild it is injected inside the consumer's
248+
element instead. */}
249+
{wrapSlotChildren(otherProps.asChild, children, (layoutChildren) => (
250+
<div className={classNames.layout}>{layoutChildren}</div>
251+
))}
252+
</Primitive.button>
245253
</ItemClassNamesProvider>
246254
);
247255
},

packages/react/src/components/SegmentedControl/SegmentedControl.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import {
44
} from "@seed-design/css/recipes/segmented-control";
55
import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
66
import { SegmentedControl as SegmentedControlPrimitive } from "@seed-design/react-segmented-control";
7+
import clsx from "clsx";
8+
import { forwardRef } from "react";
79
import { createSlotRecipeContext } from "../../utils/createSlotRecipeContext";
10+
import { wrapSlotChildren } from "../../utils/wrapSlotChildren";
811

9-
const { withProvider, withContext } = createSlotRecipeContext(segmentedControl);
12+
const { withProvider, withContext, useClassNames } = createSlotRecipeContext(segmentedControl);
1013

1114
export interface SegmentedControlRootProps
1215
extends SegmentedControlVariantProps,
@@ -28,10 +31,27 @@ export const SegmentedControlIndicator = withContext<
2831

2932
export interface SegmentedControlItemProps extends SegmentedControlPrimitive.ItemProps {}
3033

31-
export const SegmentedControlItem = withContext<HTMLLabelElement, SegmentedControlItemProps>(
32-
SegmentedControlPrimitive.Item,
33-
"item",
34+
export const SegmentedControlItem = forwardRef<HTMLLabelElement, SegmentedControlItemProps>(
35+
({ className, children, ...props }, ref) => {
36+
const classNames = useClassNames();
37+
38+
return (
39+
<SegmentedControlPrimitive.Item
40+
ref={ref}
41+
className={clsx(classNames.item, className)}
42+
{...props}
43+
>
44+
{/* layout layer — scales the item's content as a whole on press while the
45+
pressed background stays on item. With asChild it is injected inside the
46+
consumer's element instead. */}
47+
{wrapSlotChildren(props.asChild, children, (layoutChildren) => (
48+
<div className={classNames.itemLayout}>{layoutChildren}</div>
49+
))}
50+
</SegmentedControlPrimitive.Item>
51+
);
52+
},
3453
);
54+
SegmentedControlItem.displayName = "SegmentedControlItem";
3555

3656
export interface SegmentedControlItemHiddenInputProps
3757
extends SegmentedControlPrimitive.ItemHiddenInputProps {}

packages/react/src/components/SelectBox/CheckSelectBox.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,20 +122,27 @@ export const CheckSelectBoxRoot = forwardRef<HTMLLabelElement, CheckSelectBoxRoo
122122
...variantProps,
123123
});
124124

125+
const content =
126+
footerVisibility === "always" ? (
127+
children
128+
) : (
129+
<FooterVisibilityProvider footerVisibility={footerVisibility}>
130+
{children}
131+
</FooterVisibilityProvider>
132+
);
133+
125134
return (
126135
<ClassNamesProvider value={classNames}>
127136
<CheckboxPrimitive.Root
128137
ref={ref}
129138
className={clsx(classNames.root, className)}
130139
{...otherProps}
131140
>
132-
{footerVisibility === "always" ? (
133-
children
134-
) : (
135-
<FooterVisibilityProvider footerVisibility={footerVisibility}>
136-
{children}
137-
</FooterVisibilityProvider>
138-
)}
141+
{/* inner layer — scales trigger + footer as one unit on press while the
142+
pressed background stays on root. asChild replaces the root element
143+
with the consumer's child, so skip the layer (no pressed scale)
144+
instead of breaking Slot's contract. */}
145+
{otherProps.asChild ? content : <div className={classNames.inner}>{content}</div>}
139146
</CheckboxPrimitive.Root>
140147
</ClassNamesProvider>
141148
);

packages/react/src/components/SelectBox/RadioSelectBox.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,20 +121,27 @@ export const RadioSelectBoxItem = forwardRef<HTMLLabelElement, RadioSelectBoxIte
121121
...variantProps,
122122
});
123123

124+
const content =
125+
footerVisibility === "always" ? (
126+
children
127+
) : (
128+
<FooterVisibilityProvider footerVisibility={footerVisibility}>
129+
{children}
130+
</FooterVisibilityProvider>
131+
);
132+
124133
return (
125134
<ClassNamesProvider value={classNames}>
126135
<RadioGroupPrimitive.Item
127136
ref={ref}
128137
className={clsx(classNames.root, className)}
129138
{...otherProps}
130139
>
131-
{footerVisibility === "always" ? (
132-
children
133-
) : (
134-
<FooterVisibilityProvider footerVisibility={footerVisibility}>
135-
{children}
136-
</FooterVisibilityProvider>
137-
)}
140+
{/* inner layer — scales trigger + footer as one unit on press while the
141+
pressed background stays on root. asChild replaces the root element
142+
with the consumer's child, so skip the layer (no pressed scale)
143+
instead of breaking Slot's contract. */}
144+
{otherProps.asChild ? content : <div className={classNames.inner}>{content}</div>}
138145
</RadioGroupPrimitive.Item>
139146
</ClassNamesProvider>
140147
);

packages/react/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
99
import clsx from "clsx";
1010
import * as React from "react";
1111
import { createSlotRecipeContext } from "../../utils/createSlotRecipeContext";
12+
import { wrapSlotChildren } from "../../utils/wrapSlotChildren";
1213

1314
const { withContext, useClassNames, ClassNamesProvider } = createSlotRecipeContext(menuSheet);
1415
const {
@@ -200,18 +201,21 @@ export interface SwipeableMenuSheetItemProps
200201
export const SwipeableMenuSheetItem = React.forwardRef<
201202
HTMLButtonElement,
202203
SwipeableMenuSheetItemProps
203-
>(({ className: propClassName, ...props }, ref) => {
204+
>(({ className: propClassName, children, ...props }, ref) => {
204205
const [variantProps, otherProps] = menuSheetItem.splitVariantProps(props);
205206
const parentProps = useItemProps();
206207
const classNames = menuSheetItem({ ...parentProps, ...variantProps });
207208

208209
return (
209210
<ItemClassNamesProvider value={classNames}>
210-
<Primitive.button
211-
ref={ref}
212-
className={clsx(classNames.root, propClassName)}
213-
{...otherProps}
214-
/>
211+
<Primitive.button ref={ref} className={clsx(classNames.root, propClassName)} {...otherProps}>
212+
{/* layout layer — scales as a whole on press while the pressed background
213+
stays on root. With asChild it is injected inside the consumer's
214+
element instead. */}
215+
{wrapSlotChildren(otherProps.asChild, children, (layoutChildren) => (
216+
<div className={classNames.layout}>{layoutChildren}</div>
217+
))}
218+
</Primitive.button>
215219
</ItemClassNamesProvider>
216220
);
217221
});

0 commit comments

Comments
 (0)