Skip to content

Commit 7e0793f

Browse files
committed
chore: add button group/heading/spinner components
1 parent 9502ec9 commit 7e0793f

File tree

11 files changed

+328
-1
lines changed

11 files changed

+328
-1
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {
2+
ButtonPropsProvider,
3+
useRecipe,
4+
type RecipeProps,
5+
} from '@chakra-ui/react';
6+
import { useMemo, type Ref } from 'react';
7+
8+
import { Group, type GroupProps } from '../group/Group';
9+
10+
export interface ButtonGroupProps
11+
extends Omit<GroupProps, 'fill'>,
12+
RecipeProps<'button'> {
13+
ref?: Ref<HTMLElement>;
14+
}
15+
16+
export function ButtonGroup(props: ButtonGroupProps) {
17+
const recipe = useRecipe({ key: 'button' });
18+
const [variantProps, otherProps] = useMemo(
19+
() => recipe.splitVariantProps(props),
20+
[props, recipe]
21+
);
22+
23+
return (
24+
<ButtonPropsProvider value={variantProps}>
25+
<Group {...otherProps} />
26+
</ButtonPropsProvider>
27+
);
28+
}

src/components/button/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './CloseButton';
22
export * from './Button';
3+
export * from './ButtonGroup';

src/components/button/recipe.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ export const buttonRecipe = defineRecipe({
407407
_hover: {
408408
color: 'text.main',
409409
bg: 'interactive.tonal.neutral.1',
410+
borderColor: 'transparent',
410411
},
411412
_active: {
412413
color: 'text.main',

src/components/group/Group.tsx

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {
2+
chakra,
3+
useRecipe,
4+
type HTMLChakraProps,
5+
type JsxStyleProps,
6+
type RecipeProps,
7+
} from '@chakra-ui/react';
8+
import {
9+
Children,
10+
cloneElement,
11+
isValidElement,
12+
useMemo,
13+
type ReactElement,
14+
type Ref,
15+
} from 'react';
16+
17+
export interface GroupProps
18+
extends HTMLChakraProps<'div', RecipeProps<'group'>> {
19+
/**
20+
* The `alignItems` style property
21+
*/
22+
align?: JsxStyleProps['alignItems'] | undefined;
23+
/**
24+
* The `justifyContent` style property
25+
*/
26+
justify?: JsxStyleProps['justifyContent'] | undefined;
27+
/**
28+
* The `flexWrap` style property
29+
*/
30+
wrap?: JsxStyleProps['flexWrap'] | undefined;
31+
/**
32+
* A function that determines if a child should be skipped
33+
*/
34+
skip?: (child: ReactElement) => boolean | undefined;
35+
/**
36+
* Ref to the underlying DOM element
37+
*/
38+
ref?: Ref<HTMLElement>;
39+
}
40+
41+
export function Group(props: GroupProps) {
42+
const recipe = useRecipe({ key: 'group' });
43+
const [variantProps, otherProps] = recipe.splitVariantProps(props);
44+
const styles = recipe(variantProps);
45+
46+
const {
47+
align = 'center',
48+
justify = 'flex-start',
49+
children,
50+
wrap,
51+
skip,
52+
ref,
53+
...rest
54+
} = otherProps;
55+
56+
// eslint-disable-next-line react-hooks/preserve-manual-memoization
57+
const _children = useMemo(() => {
58+
const childArray = Children.toArray(children).filter(isValidElement);
59+
60+
if (childArray.length === 1) {
61+
return childArray;
62+
}
63+
64+
const validChildArray = childArray.filter(child => !skip?.(child));
65+
const validChildCount = validChildArray.length;
66+
67+
if (validChildCount === 1) {
68+
return childArray;
69+
}
70+
71+
return childArray.map(child => {
72+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
73+
const childProps = child.props as any;
74+
75+
if (skip?.(child)) {
76+
return child;
77+
}
78+
79+
const index = validChildArray.indexOf(child);
80+
81+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
82+
return cloneElement(child, {
83+
...childProps,
84+
'data-group-item': '',
85+
'data-first': dataAttr(index === 0),
86+
'data-last': dataAttr(index === validChildCount - 1),
87+
'data-between': dataAttr(index > 0 && index < validChildCount - 1),
88+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
89+
style: {
90+
'--group-count': validChildCount,
91+
'--group-index': index,
92+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
93+
...(childProps?.style ?? {}),
94+
},
95+
});
96+
});
97+
// eslint-disable-next-line react-hooks/preserve-manual-memoization
98+
}, [children, skip]);
99+
100+
return (
101+
<chakra.div
102+
css={styles}
103+
ref={ref}
104+
alignItems={align}
105+
justifyContent={justify}
106+
flexWrap={wrap}
107+
{...rest}
108+
>
109+
{_children}
110+
</chakra.div>
111+
);
112+
}
113+
114+
type Booleanish = boolean | 'true' | 'false';
115+
116+
function dataAttr(condition: boolean | undefined) {
117+
return (condition ? '' : undefined) as Booleanish;
118+
}

src/components/group/recipe.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { defineRecipe } from '@chakra-ui/react';
2+
3+
export const groupRecipe = defineRecipe({
4+
base: {
5+
display: 'inline-flex',
6+
gap: 'var(--group-gap, 0.5rem)',
7+
isolation: 'isolate',
8+
position: 'relative',
9+
'& [data-group-item]': {
10+
_focusVisible: {
11+
zIndex: 1,
12+
},
13+
},
14+
},
15+
variants: {
16+
orientation: {
17+
horizontal: {
18+
flexDirection: 'row',
19+
},
20+
vertical: {
21+
flexDirection: 'column',
22+
},
23+
},
24+
attached: {
25+
true: {
26+
gap: '0!',
27+
},
28+
},
29+
grow: {
30+
true: {
31+
display: 'flex',
32+
'& > *': {
33+
flex: 1,
34+
},
35+
},
36+
},
37+
stacking: {
38+
'first-on-top': {
39+
'& > [data-group-item]': {
40+
zIndex: 'calc(var(--group-count) - var(--group-index))',
41+
},
42+
},
43+
'last-on-top': {
44+
'& > [data-group-item]': {
45+
zIndex: 'var(--group-index)',
46+
},
47+
},
48+
},
49+
},
50+
compoundVariants: [
51+
{
52+
orientation: 'horizontal',
53+
attached: true,
54+
css: {
55+
'& > *[data-first]': {
56+
borderEndRadius: '0!',
57+
},
58+
'& > *[data-between]': {
59+
borderRadius: '0!',
60+
borderLeft: 'none',
61+
},
62+
'& > *[data-last]': {
63+
borderStartRadius: '0!',
64+
borderLeft: 'none',
65+
},
66+
},
67+
},
68+
{
69+
orientation: 'vertical',
70+
attached: true,
71+
css: {
72+
'& > *[data-first]': {
73+
borderBottomRadius: '0!',
74+
marginBottom: '-1px',
75+
},
76+
'& > *[data-between]': {
77+
borderRadius: '0!',
78+
marginBottom: '-1px',
79+
},
80+
'& > *[data-last]': {
81+
borderTopRadius: '0!',
82+
},
83+
},
84+
},
85+
],
86+
defaultVariants: {
87+
orientation: 'horizontal',
88+
},
89+
});

src/components/heading/Heading.tsx

Whitespace-only changes.

src/components/heading/recipe.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { defineRecipe } from '@chakra-ui/react';
2+
3+
export const headingRecipe = defineRecipe({
4+
className: 'teleport-heading',
5+
base: {
6+
fontFamily: 'heading',
7+
fontWeight: 'bold',
8+
},
9+
variants: {
10+
size: {
11+
xs: {
12+
textStyle: 'subtitle2',
13+
},
14+
sm: {
15+
textStyle: 'subtitle1',
16+
},
17+
md: {
18+
textStyle: 'h4',
19+
},
20+
lg: {
21+
textStyle: 'h3',
22+
},
23+
xl: {
24+
textStyle: 'h2',
25+
},
26+
'2xl': {
27+
textStyle: 'h1',
28+
},
29+
'3xl': {
30+
fontSize: '32px',
31+
lineHeight: '40px',
32+
fontWeight: 'medium',
33+
},
34+
},
35+
},
36+
defaultVariants: {
37+
size: 'xl',
38+
},
39+
});

src/components/spinner/Spinner.tsx

Whitespace-only changes.

src/components/spinner/recipe.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { defineRecipe } from '@chakra-ui/react';
2+
3+
export const spinnerRecipe = defineRecipe({
4+
className: 'teleport-spinner',
5+
base: {
6+
display: 'inline-block',
7+
borderColor: 'currentColor',
8+
borderStyle: 'solid',
9+
borderWidth: '2px',
10+
borderRadius: 'full',
11+
width: 'var(--spinner-size)',
12+
height: 'var(--spinner-size)',
13+
animation: 'spin',
14+
animationDuration: 'slowest',
15+
'--spinner-track-color': 'transparent',
16+
borderBottomColor: 'var(--spinner-track-color)',
17+
borderInlineStartColor: 'var(--spinner-track-color)',
18+
},
19+
variants: {
20+
size: {
21+
inherit: {
22+
'--spinner-size': '1em',
23+
},
24+
xs: {
25+
'--spinner-size': 'sizes.3',
26+
},
27+
sm: {
28+
'--spinner-size': 'sizes.4',
29+
},
30+
md: {
31+
'--spinner-size': 'sizes.5',
32+
},
33+
lg: {
34+
'--spinner-size': 'sizes.8',
35+
},
36+
xl: {
37+
'--spinner-size': 'sizes.10',
38+
},
39+
},
40+
},
41+
defaultVariants: {
42+
size: 'md',
43+
},
44+
});

src/storybook/stories/components/button/Button.stories.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { Box, ButtonGroup, Grid, HStack, VStack } from '@chakra-ui/react';
1+
import { Box, Grid, HStack, VStack } from '@chakra-ui/react';
22
import type { Meta, StoryObj } from '@storybook/react-vite';
33
import { expect, fn } from 'storybook/test';
44

55
import {
66
Button,
77
ButtonBorder,
8+
ButtonGroup,
89
ButtonLink,
910
ButtonSecondary,
1011
ButtonText,

0 commit comments

Comments
 (0)