@varialkit/buttongroup 0.1.1
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/docs.md +95 -0
- package/examples.tsx +187 -0
- package/package.json +27 -0
- package/src/ButtonGroup.scss +92 -0
- package/src/ButtonGroup.tsx +133 -0
- package/src/ButtonGroup.types.ts +44 -0
- package/src/index.ts +6 -0
package/docs.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ButtonGroup
|
|
2
|
+
|
|
3
|
+
The ButtonGroup component arranges multiple `@solara/button` buttons into a unified control with shared borders and smart corner rounding. Use it for grouped actions, toolbars, and split-button patterns.
|
|
4
|
+
|
|
5
|
+
## How to Use
|
|
6
|
+
|
|
7
|
+
Import the component from `@solara/buttongroup` and pass `Button` elements or the `buttons` array prop.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { Button } from "@solara/button";
|
|
11
|
+
import { ButtonGroup } from "@solara/buttongroup";
|
|
12
|
+
|
|
13
|
+
export function Example() {
|
|
14
|
+
return (
|
|
15
|
+
<ButtonGroup>
|
|
16
|
+
<Button label="Save" variant="primary" />
|
|
17
|
+
<Button label="Duplicate" variant="default" />
|
|
18
|
+
<Button label="Delete" variant="ghost" destructive />
|
|
19
|
+
</ButtonGroup>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- **Flush layout** with overlap for seamless borders.
|
|
27
|
+
- **Smart corner rounding** that keeps the outer radius while flattening inner edges.
|
|
28
|
+
- **Orientation support** for horizontal and vertical stacks.
|
|
29
|
+
- **Shared props** to update every button consistently.
|
|
30
|
+
- **Optional spacing control** to add gaps between buttons.
|
|
31
|
+
- **Icon support** through the underlying Button props (`iconLeft`, `iconRight`, `iconOnly`).
|
|
32
|
+
|
|
33
|
+
## Props
|
|
34
|
+
|
|
35
|
+
| Prop | Type | Default | Description |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Direction of the button group layout. |
|
|
38
|
+
| `fullWidth` | `boolean` | `false` | Stretch the group to fill its container. |
|
|
39
|
+
| `gap` | `number \| string` | `0` | Spacing between buttons. Use `0` for flush borders. |
|
|
40
|
+
| `radius` | `"default" \| "none" \| "full"` | `undefined` | Radius applied to every button in the group. |
|
|
41
|
+
| `buttonProps` | `Partial<ButtonProps>` | `undefined` | Props applied to every button (child props win). |
|
|
42
|
+
| `buttons` | `ButtonProps[]` | `undefined` | Optional button definitions appended after children. |
|
|
43
|
+
| `children` | `React.ReactNode` | `undefined` | Button elements to render inside the group. |
|
|
44
|
+
| `className` | `string` | `undefined` | Additional class name for the group container. |
|
|
45
|
+
|
|
46
|
+
## Examples
|
|
47
|
+
|
|
48
|
+
### Vertical Stack with Full Width
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<ButtonGroup orientation="vertical" fullWidth>
|
|
52
|
+
<Button label="Top" />
|
|
53
|
+
<Button label="Middle" />
|
|
54
|
+
<Button label="Bottom" />
|
|
55
|
+
</ButtonGroup>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Shared Props
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
<ButtonGroup buttonProps={{ size: "small", variant: "tertiary", iconLeft: "arrow_swap_16" }}>
|
|
62
|
+
<Button label="Left" />
|
|
63
|
+
<Button label="Center" />
|
|
64
|
+
<Button label="Right" />
|
|
65
|
+
</ButtonGroup>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Icon-Only Buttons
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
<ButtonGroup gap={8} buttons={[
|
|
72
|
+
{ iconOnly: "arrow_line_up_16", "aria-label": "Upload" },
|
|
73
|
+
{ iconOnly: "arrow_line_down_16", "aria-label": "Download" },
|
|
74
|
+
]} />
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Using the `buttons` Prop
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<ButtonGroup
|
|
81
|
+
gap={8}
|
|
82
|
+
radius="full"
|
|
83
|
+
buttons={[
|
|
84
|
+
{ label: "Today", variant: "default" },
|
|
85
|
+
{ label: "Week", variant: "default" },
|
|
86
|
+
{ label: "Month", variant: "default" },
|
|
87
|
+
]}
|
|
88
|
+
/>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Accessibility
|
|
92
|
+
|
|
93
|
+
- `role="group"` is applied to the container for screen readers.
|
|
94
|
+
- Each button keeps its own `aria-*` attributes and focus styles.
|
|
95
|
+
- Provide `aria-label` for icon-only buttons.
|
package/examples.tsx
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Button } from "@solara/button";
|
|
3
|
+
import type { ButtonRadius, ButtonSize, ButtonVariant } from "@solara/button";
|
|
4
|
+
import { iconNames } from "@solara/icons";
|
|
5
|
+
import type { SolaraIconName } from "@solara/icons";
|
|
6
|
+
import { ButtonGroup } from "./src/ButtonGroup";
|
|
7
|
+
import type { ButtonGroupOrientation } from "./src/ButtonGroup.types";
|
|
8
|
+
|
|
9
|
+
type PlaygroundProps = {
|
|
10
|
+
orientation: ButtonGroupOrientation;
|
|
11
|
+
fullWidth: boolean;
|
|
12
|
+
gap: number;
|
|
13
|
+
radius: ButtonRadius;
|
|
14
|
+
size: ButtonSize;
|
|
15
|
+
variant: ButtonVariant;
|
|
16
|
+
iconLeft: SolaraIconName | "";
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ButtonGroupPlayground = ({
|
|
20
|
+
orientation,
|
|
21
|
+
fullWidth,
|
|
22
|
+
gap,
|
|
23
|
+
radius,
|
|
24
|
+
size,
|
|
25
|
+
variant,
|
|
26
|
+
iconLeft,
|
|
27
|
+
}: PlaygroundProps) => {
|
|
28
|
+
const resolvedIconLeft = iconLeft || undefined;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<ButtonGroup
|
|
32
|
+
orientation={orientation}
|
|
33
|
+
fullWidth={fullWidth}
|
|
34
|
+
gap={gap}
|
|
35
|
+
radius={radius}
|
|
36
|
+
buttonProps={{ size, variant, iconLeft: resolvedIconLeft }}
|
|
37
|
+
>
|
|
38
|
+
<Button label="Save" />
|
|
39
|
+
<Button label="Duplicate" />
|
|
40
|
+
<Button label="Delete" destructive />
|
|
41
|
+
</ButtonGroup>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const stories = {
|
|
46
|
+
playground: {
|
|
47
|
+
title: "Playground",
|
|
48
|
+
description: "Adjust orientation, spacing, and shared button props.",
|
|
49
|
+
render: (props: PlaygroundProps) => <ButtonGroupPlayground {...props} />,
|
|
50
|
+
controls: [
|
|
51
|
+
{
|
|
52
|
+
name: "orientation",
|
|
53
|
+
type: "select",
|
|
54
|
+
options: ["horizontal", "vertical"],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "fullWidth",
|
|
58
|
+
type: "boolean",
|
|
59
|
+
label: "Full Width",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "gap",
|
|
63
|
+
type: "number",
|
|
64
|
+
label: "Gap (px)",
|
|
65
|
+
min: 0,
|
|
66
|
+
max: 24,
|
|
67
|
+
step: 1,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "radius",
|
|
71
|
+
type: "select",
|
|
72
|
+
options: ["default", "none", "full"],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "size",
|
|
76
|
+
type: "select",
|
|
77
|
+
options: ["small", "medium", "large"],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "variant",
|
|
81
|
+
type: "select",
|
|
82
|
+
options: ["default", "primary", "tertiary", "ghost", "accent"],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "iconLeft",
|
|
86
|
+
label: "Icon Left",
|
|
87
|
+
type: "select",
|
|
88
|
+
options: ["", ...iconNames],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
initialProps: {
|
|
92
|
+
orientation: "horizontal",
|
|
93
|
+
fullWidth: false,
|
|
94
|
+
gap: 0,
|
|
95
|
+
radius: "default",
|
|
96
|
+
size: "medium",
|
|
97
|
+
variant: "default",
|
|
98
|
+
iconLeft: "",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
buttonsProp: {
|
|
102
|
+
title: "Buttons Prop",
|
|
103
|
+
description: "Provide button definitions without manual children.",
|
|
104
|
+
showProps: false,
|
|
105
|
+
render: () => (
|
|
106
|
+
<ButtonGroup
|
|
107
|
+
gap={8}
|
|
108
|
+
radius="full"
|
|
109
|
+
buttons={[
|
|
110
|
+
{ label: "Today", variant: "default" },
|
|
111
|
+
{ label: "Week", variant: "default" },
|
|
112
|
+
{ label: "Month", variant: "default" },
|
|
113
|
+
]}
|
|
114
|
+
/>
|
|
115
|
+
),
|
|
116
|
+
code: `import { ButtonGroup } from "@solara/buttongroup";
|
|
117
|
+
|
|
118
|
+
export function Example() {
|
|
119
|
+
return (
|
|
120
|
+
<ButtonGroup
|
|
121
|
+
gap={8}
|
|
122
|
+
radius="full"
|
|
123
|
+
buttons={[
|
|
124
|
+
{ label: "Today", variant: "default" },
|
|
125
|
+
{ label: "Week", variant: "default" },
|
|
126
|
+
{ label: "Month", variant: "default" },
|
|
127
|
+
]}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
`,
|
|
132
|
+
},
|
|
133
|
+
vertical: {
|
|
134
|
+
title: "Vertical",
|
|
135
|
+
description: "Stack buttons vertically and stretch to full width.",
|
|
136
|
+
showProps: false,
|
|
137
|
+
render: () => (
|
|
138
|
+
<div style={{ width: 260 }}>
|
|
139
|
+
<ButtonGroup orientation="vertical" fullWidth>
|
|
140
|
+
<Button label="Move" />
|
|
141
|
+
<Button label="Rename" />
|
|
142
|
+
<Button label="Archive" />
|
|
143
|
+
</ButtonGroup>
|
|
144
|
+
</div>
|
|
145
|
+
),
|
|
146
|
+
code: `import { Button } from "@solara/button";
|
|
147
|
+
import { ButtonGroup } from "@solara/buttongroup";
|
|
148
|
+
|
|
149
|
+
export function Example() {
|
|
150
|
+
return (
|
|
151
|
+
<div style={{ width: 260 }}>
|
|
152
|
+
<ButtonGroup orientation="vertical" fullWidth>
|
|
153
|
+
<Button label="Move" />
|
|
154
|
+
<Button label="Rename" />
|
|
155
|
+
<Button label="Archive" />
|
|
156
|
+
</ButtonGroup>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
`,
|
|
161
|
+
},
|
|
162
|
+
icons: {
|
|
163
|
+
title: "Icons",
|
|
164
|
+
description: "Use icon props on buttons inside the group.",
|
|
165
|
+
showProps: false,
|
|
166
|
+
render: () => (
|
|
167
|
+
<ButtonGroup gap={8} radius="full" buttonProps={{ iconLeft: "arrow_swap_16" }}>
|
|
168
|
+
<Button label="Sync" />
|
|
169
|
+
<Button label="Replace" />
|
|
170
|
+
<Button label="Merge" />
|
|
171
|
+
</ButtonGroup>
|
|
172
|
+
),
|
|
173
|
+
code: `import { Button } from "@solara/button";
|
|
174
|
+
import { ButtonGroup } from "@solara/buttongroup";
|
|
175
|
+
|
|
176
|
+
export function Example() {
|
|
177
|
+
return (
|
|
178
|
+
<ButtonGroup gap={8} radius="full" buttonProps={{ iconLeft: "arrow_swap_16" }}>
|
|
179
|
+
<Button label="Sync" />
|
|
180
|
+
<Button label="Replace" />
|
|
181
|
+
<Button label="Merge" />
|
|
182
|
+
</ButtonGroup>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
`,
|
|
186
|
+
},
|
|
187
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@varialkit/buttongroup",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./examples": "./examples.tsx"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@varialkit/button": "0.1.1",
|
|
13
|
+
"@varialkit/icons": "0.1.1"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"docs.md",
|
|
18
|
+
"examples.tsx"
|
|
19
|
+
],
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"react": "^19.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/react": "19.0.10",
|
|
25
|
+
"react": "19.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
.solara-button-group {
|
|
2
|
+
--button-group-gap: 1px;
|
|
3
|
+
--button-group-overlap: 0px;
|
|
4
|
+
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
position: relative;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.solara-button-group--horizontal {
|
|
11
|
+
flex-direction: row;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.solara-button-group--vertical {
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
align-items: stretch;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.solara-button-group--horizontal > .solara-button-group__button + .solara-button-group__button {
|
|
20
|
+
margin-left: calc(
|
|
21
|
+
(var(--button-group-gap) * var(--spacing-multiplier)) -
|
|
22
|
+
(var(--button-group-overlap) * var(--spacing-multiplier))
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.solara-button-group--vertical > .solara-button-group__button + .solara-button-group__button {
|
|
27
|
+
margin-top: calc(
|
|
28
|
+
(var(--button-group-gap) * var(--spacing-multiplier)) -
|
|
29
|
+
(var(--button-group-overlap) * var(--spacing-multiplier))
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.solara-button-group__button {
|
|
34
|
+
position: relative;
|
|
35
|
+
z-index: 1;
|
|
36
|
+
flex-shrink: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.solara-button-group__button:hover,
|
|
40
|
+
.solara-button-group__button:focus {
|
|
41
|
+
z-index: 2;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.solara-button-group__button:active {
|
|
45
|
+
z-index: 3;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.solara-button-group__button:focus-visible {
|
|
49
|
+
z-index: 4;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.solara-button-group--horizontal .solara-button-group__button--first {
|
|
53
|
+
border-top-right-radius: 0;
|
|
54
|
+
border-bottom-right-radius: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.solara-button-group--horizontal .solara-button-group__button--middle {
|
|
58
|
+
border-radius: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.solara-button-group--horizontal .solara-button-group__button--last {
|
|
62
|
+
border-top-left-radius: 0;
|
|
63
|
+
border-bottom-left-radius: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.solara-button-group--vertical .solara-button-group__button--first {
|
|
67
|
+
border-bottom-left-radius: 0;
|
|
68
|
+
border-bottom-right-radius: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.solara-button-group--vertical .solara-button-group__button--middle {
|
|
72
|
+
border-radius: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.solara-button-group--vertical .solara-button-group__button--last {
|
|
76
|
+
border-top-left-radius: 0;
|
|
77
|
+
border-top-right-radius: 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.solara-button-group--full-width {
|
|
81
|
+
width: 100%;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.solara-button-group--full-width.solara-button-group--horizontal
|
|
85
|
+
> .solara-button-group__button {
|
|
86
|
+
flex: 1 1 0%;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.solara-button-group--full-width.solara-button-group--vertical
|
|
90
|
+
> .solara-button-group__button {
|
|
91
|
+
width: 100%;
|
|
92
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React, { Children, cloneElement, isValidElement, useMemo } from "react";
|
|
2
|
+
import { Button } from "@solara/button";
|
|
3
|
+
import type { ButtonProps } from "@solara/button";
|
|
4
|
+
import type { ButtonGroupProps } from "./ButtonGroup.types";
|
|
5
|
+
import "./ButtonGroup.scss";
|
|
6
|
+
|
|
7
|
+
const classNames = (...classes: Array<string | false | null | undefined>) =>
|
|
8
|
+
classes.filter(Boolean).join(" ");
|
|
9
|
+
|
|
10
|
+
const isZeroGap = (gap: ButtonGroupProps["gap"]) => {
|
|
11
|
+
if (gap === undefined || gap === null) return true;
|
|
12
|
+
if (typeof gap === "number") return gap === 0;
|
|
13
|
+
const trimmed = gap.trim();
|
|
14
|
+
return trimmed === "0" || trimmed === "0px" || trimmed === "0rem" || trimmed === "0em";
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const resolveGapValue = (gap: ButtonGroupProps["gap"]) => {
|
|
18
|
+
if (gap === undefined || gap === null) return "0px";
|
|
19
|
+
return typeof gap === "number" ? `${gap}px` : gap;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const isButtonElement = (
|
|
23
|
+
child: React.ReactNode
|
|
24
|
+
): child is React.ReactElement<ButtonProps> => isValidElement(child) && child.type === Button;
|
|
25
|
+
|
|
26
|
+
const mergeButtonProps = (
|
|
27
|
+
sharedProps: Partial<ButtonProps>,
|
|
28
|
+
childProps: ButtonProps,
|
|
29
|
+
className: string
|
|
30
|
+
) => {
|
|
31
|
+
return {
|
|
32
|
+
...sharedProps,
|
|
33
|
+
...childProps,
|
|
34
|
+
className: classNames(sharedProps.className, childProps.className, className),
|
|
35
|
+
} as ButtonProps;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
|
|
39
|
+
(
|
|
40
|
+
{
|
|
41
|
+
orientation = "horizontal",
|
|
42
|
+
fullWidth = false,
|
|
43
|
+
gap,
|
|
44
|
+
radius,
|
|
45
|
+
buttonProps,
|
|
46
|
+
buttons,
|
|
47
|
+
className,
|
|
48
|
+
children,
|
|
49
|
+
style,
|
|
50
|
+
...props
|
|
51
|
+
},
|
|
52
|
+
ref
|
|
53
|
+
) => {
|
|
54
|
+
const sharedButtonProps = useMemo(() => {
|
|
55
|
+
if (!buttonProps && !radius) return undefined;
|
|
56
|
+
const shared: Partial<ButtonProps> = { ...(buttonProps ?? {}) };
|
|
57
|
+
if (radius) shared.radius = radius;
|
|
58
|
+
return shared;
|
|
59
|
+
}, [buttonProps, radius]);
|
|
60
|
+
|
|
61
|
+
const renderedButtons = useMemo(
|
|
62
|
+
() =>
|
|
63
|
+
(buttons ?? []).map((button, index) => (
|
|
64
|
+
<Button key={button.key ?? index} {...button} />
|
|
65
|
+
)),
|
|
66
|
+
[buttons]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const combinedChildren = useMemo(() => {
|
|
70
|
+
const baseChildren = Children.toArray(children);
|
|
71
|
+
if (renderedButtons.length === 0) return baseChildren;
|
|
72
|
+
return baseChildren.concat(renderedButtons);
|
|
73
|
+
}, [children, renderedButtons]);
|
|
74
|
+
|
|
75
|
+
const totalButtons = useMemo(
|
|
76
|
+
() => combinedChildren.filter((child) => isButtonElement(child)).length,
|
|
77
|
+
[combinedChildren]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (combinedChildren.length === 0) return null;
|
|
81
|
+
|
|
82
|
+
let buttonIndex = 0;
|
|
83
|
+
const processedChildren = combinedChildren.map((child) => {
|
|
84
|
+
if (!isButtonElement(child)) return child;
|
|
85
|
+
|
|
86
|
+
const isSingle = totalButtons === 1;
|
|
87
|
+
const isFirst = buttonIndex === 0;
|
|
88
|
+
const isLast = buttonIndex === totalButtons - 1;
|
|
89
|
+
const isMiddle = !isFirst && !isLast;
|
|
90
|
+
buttonIndex += 1;
|
|
91
|
+
|
|
92
|
+
const positionClass = classNames(
|
|
93
|
+
"solara-button-group__button",
|
|
94
|
+
!isSingle && isFirst && "solara-button-group__button--first",
|
|
95
|
+
!isSingle && isMiddle && "solara-button-group__button--middle",
|
|
96
|
+
!isSingle && isLast && "solara-button-group__button--last"
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (!sharedButtonProps) {
|
|
100
|
+
return cloneElement(child, {
|
|
101
|
+
...child.props,
|
|
102
|
+
className: classNames(child.props.className, positionClass),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return cloneElement(child, mergeButtonProps(sharedButtonProps, child.props, positionClass));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const mergedStyle = {
|
|
110
|
+
...style,
|
|
111
|
+
} as React.CSSProperties;
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
ref={ref}
|
|
116
|
+
className={classNames(
|
|
117
|
+
"solara-button-group",
|
|
118
|
+
`solara-button-group--${orientation}`,
|
|
119
|
+
fullWidth && "solara-button-group--full-width",
|
|
120
|
+
className
|
|
121
|
+
)}
|
|
122
|
+
role="group"
|
|
123
|
+
aria-orientation={orientation}
|
|
124
|
+
style={mergedStyle}
|
|
125
|
+
{...props}
|
|
126
|
+
>
|
|
127
|
+
{processedChildren}
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
ButtonGroup.displayName = "ButtonGroup";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { ButtonProps, ButtonRadius } from "@solara/button";
|
|
3
|
+
|
|
4
|
+
export type ButtonGroupOrientation = "horizontal" | "vertical";
|
|
5
|
+
|
|
6
|
+
export type ButtonGroupButton = ButtonProps & { key?: React.Key };
|
|
7
|
+
|
|
8
|
+
export interface ButtonGroupProps
|
|
9
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
10
|
+
/**
|
|
11
|
+
* The orientation of the button group.
|
|
12
|
+
* @default "horizontal"
|
|
13
|
+
*/
|
|
14
|
+
orientation?: ButtonGroupOrientation;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the button group should take up the full width of its container.
|
|
17
|
+
* @default false
|
|
18
|
+
*/
|
|
19
|
+
fullWidth?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Spacing between buttons. Use `0` for flush edges (default).
|
|
22
|
+
*/
|
|
23
|
+
gap?: number | string;
|
|
24
|
+
/**
|
|
25
|
+
* Override the radius for the grouped buttons.
|
|
26
|
+
*/
|
|
27
|
+
radius?: ButtonRadius;
|
|
28
|
+
/**
|
|
29
|
+
* Props applied to every button in the group. Individual button props win.
|
|
30
|
+
*/
|
|
31
|
+
buttonProps?: Partial<ButtonProps>;
|
|
32
|
+
/**
|
|
33
|
+
* Optional button definitions appended after children.
|
|
34
|
+
*/
|
|
35
|
+
buttons?: ButtonGroupButton[];
|
|
36
|
+
/**
|
|
37
|
+
* Button elements to render inside the group.
|
|
38
|
+
*/
|
|
39
|
+
children?: React.ReactNode;
|
|
40
|
+
/**
|
|
41
|
+
* Optional class name to add to the button group.
|
|
42
|
+
*/
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|