mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-23 20:18:42 +08:00
refactor(component): new Radio component (#6910)
# New `RadioGroup` component to replace `RadioButton`  ### what's new - [x] Change the API - [x] More customizable options - [x] Indicator animation - [x] Dynamic width support(responsive) - [x] Storybook - [x] JSDoc
This commit is contained in:
@@ -102,7 +102,7 @@
|
|||||||
"@vanilla-extract/css": "^1.14.2",
|
"@vanilla-extract/css": "^1.14.2",
|
||||||
"fake-indexeddb": "^6.0.0",
|
"fake-indexeddb": "^6.0.0",
|
||||||
"storybook": "^7.6.17",
|
"storybook": "^7.6.17",
|
||||||
"storybook-dark-mode": "^4.0.0",
|
"storybook-dark-mode": "4.0.1",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.2.8",
|
"vite": "^5.2.8",
|
||||||
"vitest": "1.6.0",
|
"vitest": "1.6.0",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// TODO(@catsjuice): Check `input` , `loading`, not migrated from `design`
|
|
||||||
export * from './lit-react';
|
export * from './lit-react';
|
||||||
export * from './styles';
|
export * from './styles';
|
||||||
export * from './ui/avatar';
|
export * from './ui/avatar';
|
||||||
@@ -18,6 +17,7 @@ export * from './ui/menu';
|
|||||||
export * from './ui/modal';
|
export * from './ui/modal';
|
||||||
export * from './ui/notification';
|
export * from './ui/notification';
|
||||||
export * from './ui/popover';
|
export * from './ui/popover';
|
||||||
|
export * from './ui/radio';
|
||||||
export * from './ui/scrollbar';
|
export * from './ui/scrollbar';
|
||||||
export * from './ui/skeleton';
|
export * from './ui/skeleton';
|
||||||
export * from './ui/switch';
|
export * from './ui/switch';
|
||||||
|
|||||||
@@ -2,19 +2,27 @@ import type {
|
|||||||
RadioGroupItemProps,
|
RadioGroupItemProps,
|
||||||
RadioGroupProps,
|
RadioGroupProps,
|
||||||
} from '@radix-ui/react-radio-group';
|
} from '@radix-ui/react-radio-group';
|
||||||
import * as RadioGroup from '@radix-ui/react-radio-group';
|
import * as RadixRadioGroup from '@radix-ui/react-radio-group';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
import { RadioGroup } from '../radio';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
|
// for reference
|
||||||
|
RadioGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* use {@link RadioGroup } instead
|
||||||
|
*/
|
||||||
export const RadioButton = forwardRef<
|
export const RadioButton = forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
RadioGroupItemProps & { spanStyle?: string }
|
RadioGroupItemProps & { spanStyle?: string }
|
||||||
>(({ children, className, spanStyle, ...props }, ref) => {
|
>(({ children, className, spanStyle, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<RadioGroup.Item
|
<RadixRadioGroup.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
className={clsx(styles.radioButton, className)}
|
className={clsx(styles.radioButton, className)}
|
||||||
@@ -22,27 +30,31 @@ export const RadioButton = forwardRef<
|
|||||||
<span className={clsx(styles.radioUncheckedButton, spanStyle)}>
|
<span className={clsx(styles.radioUncheckedButton, spanStyle)}>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
<RadioGroup.Indicator
|
<RadixRadioGroup.Indicator
|
||||||
className={clsx(styles.radioButtonContent, spanStyle)}
|
className={clsx(styles.radioButtonContent, spanStyle)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</RadioGroup.Indicator>
|
</RadixRadioGroup.Indicator>
|
||||||
</RadioGroup.Item>
|
</RadixRadioGroup.Item>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
RadioButton.displayName = 'RadioButton';
|
RadioButton.displayName = 'RadioButton';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* use {@link RadioGroup} instead
|
||||||
|
*/
|
||||||
export const RadioButtonGroup = forwardRef<
|
export const RadioButtonGroup = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
RadioGroupProps & { width?: CSSProperties['width'] }
|
RadioGroupProps & { width?: CSSProperties['width'] }
|
||||||
>(({ className, style, width, ...props }, ref) => {
|
>(({ className, style, width, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<RadioGroup.Root
|
<RadixRadioGroup.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clsx(styles.radioButtonGroup, className)}
|
className={clsx(styles.radioButtonGroup, className)}
|
||||||
style={{ width, ...style }}
|
style={{ width, ...style }}
|
||||||
{...props}
|
{...props}
|
||||||
></RadioGroup.Root>
|
></RadixRadioGroup.Root>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
RadioButtonGroup.displayName = 'RadioButtonGroup';
|
RadioButtonGroup.displayName = 'RadioButtonGroup';
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './radio';
|
||||||
|
export type { RadioItem, RadioProps } from './types';
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { AiIcon, FrameIcon, TocIcon, TodayIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { ResizePanel } from '../resize-panel/resize-panel';
|
||||||
|
import { RadioGroup } from './radio';
|
||||||
|
import type { RadioItem } from './types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'UI/RadioGroup',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FixedWidth = () => {
|
||||||
|
const [value, setValue] = useState('Radio 1');
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p style={{ marginBottom: 10, fontSize: cssVar('fontXs') }}>
|
||||||
|
width:
|
||||||
|
<code
|
||||||
|
style={{
|
||||||
|
padding: '2px 4px',
|
||||||
|
borderRadius: 3,
|
||||||
|
background: cssVar('hoverColorFilled'),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
300px
|
||||||
|
</code>
|
||||||
|
</p>
|
||||||
|
<RadioGroup
|
||||||
|
width={300}
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
items={['Radio 1', 'Radio 2, Longer', 'S3']}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AutoWidth = () => {
|
||||||
|
const [value, setValue] = useState('Radio 1');
|
||||||
|
return (
|
||||||
|
<RadioGroup
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
items={['Radio 1', 'Radio 2, Longer', 'S3']}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DynamicWidth = () => {
|
||||||
|
const [value, setValue] = useState('Radio 1');
|
||||||
|
return (
|
||||||
|
<ResizePanel
|
||||||
|
horizontal
|
||||||
|
vertical={false}
|
||||||
|
maxWidth={1080}
|
||||||
|
minWidth={235}
|
||||||
|
width={250}
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
width="100%"
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
items={['Radio 1', 'Radio 2, Longer', 'S3']}
|
||||||
|
/>
|
||||||
|
</ResizePanel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IconTabs = () => {
|
||||||
|
const [value, setValue] = useState('ai');
|
||||||
|
const items: RadioItem[] = [
|
||||||
|
{
|
||||||
|
value: 'ai',
|
||||||
|
label: <AiIcon width={20} height={20} />,
|
||||||
|
style: { width: 28 },
|
||||||
|
testId: 'ai-radio',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'calendar',
|
||||||
|
label: <TodayIcon width={20} height={20} />,
|
||||||
|
style: { width: 28 },
|
||||||
|
testId: 'calendar-radio',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'outline',
|
||||||
|
label: <TocIcon width={20} height={20} />,
|
||||||
|
style: { width: 28 },
|
||||||
|
testId: 'outline-radio',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'frame',
|
||||||
|
label: <FrameIcon width={20} height={20} />,
|
||||||
|
style: { width: 28 },
|
||||||
|
testId: 'frame-radio',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<RadioGroup
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
items={items}
|
||||||
|
padding={4}
|
||||||
|
borderRadius={12}
|
||||||
|
gap={8}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomizeActiveStyle = () => {
|
||||||
|
const [value, setValue] = useState('ai');
|
||||||
|
const items: RadioItem[] = [
|
||||||
|
{
|
||||||
|
value: 'ai',
|
||||||
|
label: <AiIcon width={20} height={20} />,
|
||||||
|
style: { width: 28 },
|
||||||
|
testId: 'ai-radio',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'calendar',
|
||||||
|
label: <TodayIcon width={20} height={20} />,
|
||||||
|
style: { width: 28 },
|
||||||
|
testId: 'calendar-radio',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'outline',
|
||||||
|
label: <TocIcon width={20} height={20} />,
|
||||||
|
style: { width: 28 },
|
||||||
|
testId: 'outline-radio',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'frame',
|
||||||
|
label: <FrameIcon width={20} height={20} />,
|
||||||
|
style: { width: 28 },
|
||||||
|
testId: 'frame-radio',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<RadioGroup
|
||||||
|
activeItemStyle={{ color: cssVar('primaryColor') }}
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
items={items}
|
||||||
|
padding={4}
|
||||||
|
borderRadius={12}
|
||||||
|
gap={8}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import * as RadixRadioGroup from '@radix-ui/react-radio-group';
|
||||||
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { createRef, memo, useCallback, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
import { withUnit } from '../../utils/with-unit';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
import type { RadioItem, RadioProps } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ### Radio button group (Tabs)
|
||||||
|
* A tab-like radio button group
|
||||||
|
*
|
||||||
|
* #### 1. Basic usage with fixed width
|
||||||
|
* ```tsx
|
||||||
|
* <RadioGroup
|
||||||
|
* width={300}
|
||||||
|
* value={value}
|
||||||
|
* onChange={setValue}
|
||||||
|
* items={['Radio 1', 'Radio 2, Longer', 'S3']}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* #### 2. Dynamic width
|
||||||
|
* ```tsx
|
||||||
|
* <RadioGroup
|
||||||
|
* width="100%"
|
||||||
|
* value={value}
|
||||||
|
* onChange={setValue}
|
||||||
|
* items={['Radio 1', 'Radio 2, Longer', 'S3']}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* #### 3. `ReactNode` as label
|
||||||
|
* ```tsx
|
||||||
|
* const [value, setValue] = useState('ai');
|
||||||
|
* const items: RadioItem[] = [
|
||||||
|
* {
|
||||||
|
* value: 'ai',
|
||||||
|
* label: <AiIcon width={20} height={20} />,
|
||||||
|
* style: { width: 28 },
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* value: 'calendar',
|
||||||
|
* label: <TodayIcon width={20} height={20} />,
|
||||||
|
* style: { width: 28 },
|
||||||
|
* },
|
||||||
|
* ];
|
||||||
|
* return (
|
||||||
|
* <RadioGroup
|
||||||
|
* value={value}
|
||||||
|
* onChange={setValue}
|
||||||
|
* items={items}
|
||||||
|
* padding={4}
|
||||||
|
* borderRadius={12}
|
||||||
|
* gap={8}
|
||||||
|
* />
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const RadioGroup = memo(function RadioGroup({
|
||||||
|
items,
|
||||||
|
value,
|
||||||
|
width,
|
||||||
|
style,
|
||||||
|
padding = 2,
|
||||||
|
gap = 4,
|
||||||
|
borderRadius = 10,
|
||||||
|
itemHeight = 28,
|
||||||
|
animationDuration = 250,
|
||||||
|
animationEasing = 'cubic-bezier(.18,.22,0,1)',
|
||||||
|
activeItemClassName,
|
||||||
|
activeItemStyle,
|
||||||
|
onChange,
|
||||||
|
}: RadioProps) {
|
||||||
|
const animationTImerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const finalItems = useMemo(() => {
|
||||||
|
return items
|
||||||
|
.map(value =>
|
||||||
|
typeof value === 'string' ? ({ value } as RadioItem) : value
|
||||||
|
)
|
||||||
|
.map(item => ({
|
||||||
|
...item,
|
||||||
|
ref: createRef<HTMLButtonElement>(),
|
||||||
|
indicatorRef: createRef<HTMLDivElement>(),
|
||||||
|
}));
|
||||||
|
}, [items]);
|
||||||
|
const finalStyle = useMemo(
|
||||||
|
() => ({
|
||||||
|
width,
|
||||||
|
...style,
|
||||||
|
...assignInlineVars({
|
||||||
|
[styles.outerPadding]: withUnit(padding, 'px'),
|
||||||
|
[styles.outerRadius]: withUnit(borderRadius, 'px'),
|
||||||
|
[styles.itemGap]: withUnit(gap, 'px'),
|
||||||
|
[styles.itemHeight]: withUnit(itemHeight, 'px'),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
[width, style, padding, borderRadius, gap, itemHeight]
|
||||||
|
);
|
||||||
|
|
||||||
|
const animate = useCallback(
|
||||||
|
(oldValue?: string, newValue?: string) => {
|
||||||
|
if (!oldValue || !newValue) return;
|
||||||
|
const oldItem = finalItems.find(item => item.value === oldValue);
|
||||||
|
const newItem = finalItems.find(item => item.value === newValue);
|
||||||
|
if (!oldItem || !newItem) return;
|
||||||
|
const oldRect = oldItem.ref.current?.getBoundingClientRect();
|
||||||
|
const newRect = newItem.ref.current?.getBoundingClientRect();
|
||||||
|
if (!oldRect || !newRect) return;
|
||||||
|
const activeIndicator = newItem.indicatorRef.current;
|
||||||
|
if (!activeIndicator) return;
|
||||||
|
|
||||||
|
activeIndicator.style.transform = `translate3d(${oldRect.left - newRect.left}px,0,0)`;
|
||||||
|
activeIndicator.style.transition = 'none';
|
||||||
|
activeIndicator.style.width = `${oldRect.width}px`;
|
||||||
|
|
||||||
|
const animation = `${withUnit(animationDuration, 'ms')} ${animationEasing}`;
|
||||||
|
|
||||||
|
if (animationTImerRef.current) clearTimeout(animationTImerRef.current);
|
||||||
|
animationTImerRef.current = setTimeout(() => {
|
||||||
|
animationTImerRef.current = null;
|
||||||
|
activeIndicator.style.transition = `transform ${animation}, width ${animation}`;
|
||||||
|
activeIndicator.style.transform = 'none';
|
||||||
|
activeIndicator.style.width = '';
|
||||||
|
}, 50);
|
||||||
|
},
|
||||||
|
[animationDuration, animationEasing, finalItems]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onValueChange = useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
|
const oldValue = value;
|
||||||
|
if (oldValue !== newValue) {
|
||||||
|
onChange(newValue);
|
||||||
|
animate(oldValue, newValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[animate, onChange, value]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadixRadioGroup.Root
|
||||||
|
value={value}
|
||||||
|
onValueChange={onValueChange}
|
||||||
|
className={styles.radioButtonGroup}
|
||||||
|
style={finalStyle}
|
||||||
|
>
|
||||||
|
{finalItems.map(({ customRender, ...item }, index) => {
|
||||||
|
const testId = item.testId ? { 'data-testid': item.testId } : {};
|
||||||
|
const active = item.value === value;
|
||||||
|
|
||||||
|
const classMap = { [styles.radioButton]: true };
|
||||||
|
if (activeItemClassName) classMap[activeItemClassName] = active;
|
||||||
|
if (item.className) classMap[item.className] = true;
|
||||||
|
|
||||||
|
const style = { ...item.style };
|
||||||
|
if (activeItemStyle && active) Object.assign(style, activeItemStyle);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadixRadioGroup.Item
|
||||||
|
ref={item.ref}
|
||||||
|
key={item.value}
|
||||||
|
value={item.value}
|
||||||
|
className={clsx(classMap)}
|
||||||
|
style={style}
|
||||||
|
{...testId}
|
||||||
|
{...item.attrs}
|
||||||
|
>
|
||||||
|
<RadixRadioGroup.Indicator
|
||||||
|
forceMount
|
||||||
|
className={styles.indicator}
|
||||||
|
ref={item.indicatorRef}
|
||||||
|
/>
|
||||||
|
<span className={styles.radioButtonContent}>
|
||||||
|
{customRender
|
||||||
|
? customRender(item, index)
|
||||||
|
: item.label ?? item.value}
|
||||||
|
</span>
|
||||||
|
</RadixRadioGroup.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</RadixRadioGroup.Root>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { createVar, globalStyle, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const outerPadding = createVar('radio-outer-padding');
|
||||||
|
export const outerRadius = createVar('radio-outer-radius');
|
||||||
|
export const itemGap = createVar('radio-item-gap');
|
||||||
|
export const itemHeight = createVar('radio-item-height');
|
||||||
|
|
||||||
|
export const radioButton = style({
|
||||||
|
flex: 1,
|
||||||
|
position: 'relative',
|
||||||
|
borderRadius: `calc(${outerRadius} - ${outerPadding})`,
|
||||||
|
height: itemHeight,
|
||||||
|
padding: '4px 8px',
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: cssVar('textSecondaryColor'),
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
userSelect: 'none',
|
||||||
|
fontWeight: 600,
|
||||||
|
selectors: {
|
||||||
|
'&[data-state="checked"]': {
|
||||||
|
color: cssVar('textPrimaryColor'),
|
||||||
|
},
|
||||||
|
'&[data-state="unchecked"]:hover': {
|
||||||
|
background: cssVar('hoverColor'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const radioButtonContent = style({
|
||||||
|
zIndex: 1,
|
||||||
|
display: 'block',
|
||||||
|
});
|
||||||
|
globalStyle(`${radioButtonContent} > svg`, { display: 'block' });
|
||||||
|
export const radioButtonGroup = style({
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
background: cssVar('hoverColorFilled'),
|
||||||
|
|
||||||
|
borderRadius: outerRadius,
|
||||||
|
padding: outerPadding,
|
||||||
|
gap: itemGap,
|
||||||
|
|
||||||
|
// @ts-expect-error - fix electron drag
|
||||||
|
WebkitAppRegion: 'no-drag',
|
||||||
|
});
|
||||||
|
export const indicator = style({
|
||||||
|
position: 'absolute',
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
background: cssVar('white'),
|
||||||
|
filter: 'drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.1))',
|
||||||
|
opacity: 0,
|
||||||
|
transformOrigin: 'left',
|
||||||
|
selectors: {
|
||||||
|
'[data-state="checked"] > &': {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import type { RadioGroupItemProps } from '@radix-ui/react-radio-group';
|
||||||
|
import type { CSSProperties, ReactNode } from 'react';
|
||||||
|
|
||||||
|
type SimpleRadioItem = string;
|
||||||
|
|
||||||
|
export interface RadioProps extends RadioGroupItemProps {
|
||||||
|
items: RadioItem[] | SimpleRadioItem[];
|
||||||
|
value: any;
|
||||||
|
onChange: (value: any) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total width of the radio group, items will be evenly distributed
|
||||||
|
*/
|
||||||
|
width?: CSSProperties['width'];
|
||||||
|
/**
|
||||||
|
* Distance between outer wrapper and items (in pixels)
|
||||||
|
* @default 2
|
||||||
|
*/
|
||||||
|
padding?: number;
|
||||||
|
/**
|
||||||
|
* Distance between items (in pixels)
|
||||||
|
* @default 4
|
||||||
|
*/
|
||||||
|
gap?: number;
|
||||||
|
/**
|
||||||
|
* Outer border radius (in pixels), the inner item's border radius will be calculated based on `padding` and `borderRadius`
|
||||||
|
* @default 10
|
||||||
|
*/
|
||||||
|
borderRadius?: number;
|
||||||
|
/**
|
||||||
|
* Height of the inner item (in pixels)
|
||||||
|
* @default 28
|
||||||
|
*/
|
||||||
|
itemHeight?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom duration for the indicator animation
|
||||||
|
* @default 250
|
||||||
|
*/
|
||||||
|
animationDuration?: number | string;
|
||||||
|
/**
|
||||||
|
* Custom easing function for the indicator animation
|
||||||
|
* @default 'cubic-bezier(.18,.22,0,1)'
|
||||||
|
*/
|
||||||
|
animationEasing?: string;
|
||||||
|
/** Customize active item's className */
|
||||||
|
activeItemClassName?: string;
|
||||||
|
/** Customize active item's style */
|
||||||
|
activeItemStyle?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RadioItem {
|
||||||
|
value: string;
|
||||||
|
label?: ReactNode;
|
||||||
|
style?: CSSProperties;
|
||||||
|
className?: string;
|
||||||
|
/** bind `data-testid` */
|
||||||
|
testId?: string;
|
||||||
|
/** Customize button-element's html attributes */
|
||||||
|
attrs?: Partial<
|
||||||
|
Omit<React.HTMLAttributes<HTMLButtonElement>, 'className' | 'style'>
|
||||||
|
> &
|
||||||
|
Record<`data-${string}`, string>;
|
||||||
|
customRender?: (
|
||||||
|
item: Omit<RadioItem, 'customRender'>,
|
||||||
|
index: number
|
||||||
|
) => ReactNode;
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ export const ResizePanel = ({
|
|||||||
}: ResizePanelProps) => {
|
}: ResizePanelProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const cornerHandleRef = useRef<HTMLDivElement | null>(null);
|
const cornerHandleRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const displayRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current || !cornerHandleRef.current) return;
|
if (!containerRef.current || !cornerHandleRef.current) return;
|
||||||
@@ -48,6 +49,7 @@ export const ResizePanel = ({
|
|||||||
let startSize: [number, number] = [0, 0];
|
let startSize: [number, number] = [0, 0];
|
||||||
|
|
||||||
const onDragStart = (e: MouseEvent) => {
|
const onDragStart = (e: MouseEvent) => {
|
||||||
|
containerEl.dataset.resizing = 'true';
|
||||||
startPos = [e.clientX, e.clientY];
|
startPos = [e.clientX, e.clientY];
|
||||||
startSize = [containerEl.offsetWidth, containerEl.offsetHeight];
|
startSize = [containerEl.offsetWidth, containerEl.offsetHeight];
|
||||||
document.addEventListener('mousemove', onDrag);
|
document.addEventListener('mousemove', onDrag);
|
||||||
@@ -62,6 +64,7 @@ export const ResizePanel = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onDragEnd = () => {
|
const onDragEnd = () => {
|
||||||
|
containerEl.dataset.resizing = 'false';
|
||||||
document.removeEventListener('mousemove', onDrag);
|
document.removeEventListener('mousemove', onDrag);
|
||||||
document.removeEventListener('mouseup', onDragEnd);
|
document.removeEventListener('mouseup', onDragEnd);
|
||||||
};
|
};
|
||||||
@@ -84,6 +87,10 @@ export const ResizePanel = ({
|
|||||||
);
|
);
|
||||||
containerEl.style.height = `${height}px`;
|
containerEl.style.height = `${height}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (displayRef.current) {
|
||||||
|
displayRef.current.textContent = `${containerEl.offsetWidth}px * ${containerEl.offsetHeight}px`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updateSize([width ?? 400, height ?? 200]);
|
updateSize([width ?? 400, height ?? 200]);
|
||||||
@@ -112,7 +119,9 @@ export const ResizePanel = ({
|
|||||||
{...attrs}
|
{...attrs}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<div ref={cornerHandleRef} className={styles.cornerHandle}></div>
|
<div ref={cornerHandleRef} className={styles.cornerHandle}>
|
||||||
|
<div ref={displayRef} className={styles.display}></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
const HANDLE_SIZE = 24;
|
const HANDLE_SIZE = 24;
|
||||||
export const container = style({
|
export const container = style({
|
||||||
@@ -19,3 +20,29 @@ export const cornerHandle = style({
|
|||||||
transform: 'rotate(45deg)',
|
transform: 'rotate(45deg)',
|
||||||
cursor: 'nwse-resize',
|
cursor: 'nwse-resize',
|
||||||
});
|
});
|
||||||
|
export const display = style({
|
||||||
|
position: 'absolute',
|
||||||
|
left: 32,
|
||||||
|
top: 12,
|
||||||
|
transform: 'rotate(-45deg)',
|
||||||
|
transformOrigin: '0 0',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
borderRadius: 6,
|
||||||
|
background: cssVar('black'),
|
||||||
|
color: cssVar('white'),
|
||||||
|
borderTopLeftRadius: 0,
|
||||||
|
|
||||||
|
maxWidth: 0,
|
||||||
|
maxHeight: 0,
|
||||||
|
padding: 0,
|
||||||
|
transition: 'all 0.23s ease',
|
||||||
|
overflow: 'hidden',
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'[data-resizing="true"] &': {
|
||||||
|
padding: '4px 8px',
|
||||||
|
maxWidth: 200,
|
||||||
|
maxHeight: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
type AllowedUnits = 'px' | 'ms';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get value with unit
|
||||||
|
*/
|
||||||
|
export const withUnit = (
|
||||||
|
value: string | number,
|
||||||
|
unit: AllowedUnits
|
||||||
|
): string => {
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return `${value}${unit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\d+(\.\d+)?$/.test(value)) {
|
||||||
|
return `${value}${unit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
@@ -354,7 +354,7 @@ __metadata:
|
|||||||
rxjs: "npm:^7.8.1"
|
rxjs: "npm:^7.8.1"
|
||||||
sonner: "npm:^1.4.41"
|
sonner: "npm:^1.4.41"
|
||||||
storybook: "npm:^7.6.17"
|
storybook: "npm:^7.6.17"
|
||||||
storybook-dark-mode: "npm:^4.0.0"
|
storybook-dark-mode: "npm:4.0.1"
|
||||||
swr: "npm:^2.2.5"
|
swr: "npm:^2.2.5"
|
||||||
typescript: "npm:^5.4.5"
|
typescript: "npm:^5.4.5"
|
||||||
uuid: "npm:^10.0.0"
|
uuid: "npm:^10.0.0"
|
||||||
@@ -36220,9 +36220,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"storybook-dark-mode@npm:^4.0.0":
|
"storybook-dark-mode@npm:4.0.1":
|
||||||
version: 4.0.2
|
version: 4.0.1
|
||||||
resolution: "storybook-dark-mode@npm:4.0.2"
|
resolution: "storybook-dark-mode@npm:4.0.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@storybook/components": "npm:^8.0.0"
|
"@storybook/components": "npm:^8.0.0"
|
||||||
"@storybook/core-events": "npm:^8.0.0"
|
"@storybook/core-events": "npm:^8.0.0"
|
||||||
@@ -36232,7 +36232,7 @@ __metadata:
|
|||||||
"@storybook/theming": "npm:^8.0.0"
|
"@storybook/theming": "npm:^8.0.0"
|
||||||
fast-deep-equal: "npm:^3.1.3"
|
fast-deep-equal: "npm:^3.1.3"
|
||||||
memoizerific: "npm:^1.11.3"
|
memoizerific: "npm:^1.11.3"
|
||||||
checksum: 10/c9ef7bc6734df7486ff763c9da3c69505269eaf5fd7b5b489553f023b363ea892862241e6d701ad647ca5d1e64fd9a2646b8985c7ea8ac97a3bca87891db6fe5
|
checksum: 10/3225e5bdaba0ea76b65d642202d9712d7de234e3b5673fb46e444892ab114be207dd287778e2002b662ec35bb8153d2624ff280ce51c5299fb13c711431dad40
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user