mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(component): checkbox (#4665)
This commit is contained in:
@@ -2,6 +2,7 @@ export * from './components/list-skeleton';
|
||||
export * from './styles';
|
||||
export * from './ui/breadcrumbs';
|
||||
export * from './ui/button';
|
||||
export * from './ui/checkbox';
|
||||
export * from './ui/empty';
|
||||
export * from './ui/input';
|
||||
export * from './ui/layout';
|
||||
|
||||
100
packages/frontend/component/src/ui/checkbox/checkbox.tsx
Normal file
100
packages/frontend/component/src/ui/checkbox/checkbox.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
// components/checkbox.tsx
|
||||
import clsx from 'clsx';
|
||||
import { type HTMLAttributes, useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import * as icons from './icons';
|
||||
import * as styles from './index.css';
|
||||
|
||||
type CheckboxProps = Omit<HTMLAttributes<HTMLInputElement>, 'onChange'> & {
|
||||
checked: boolean;
|
||||
onChange: (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
checked: boolean
|
||||
) => void;
|
||||
disabled?: boolean;
|
||||
intermediate?: boolean;
|
||||
};
|
||||
|
||||
export const Checkbox = ({
|
||||
checked,
|
||||
onChange,
|
||||
intermediate,
|
||||
disabled,
|
||||
...otherProps
|
||||
}: CheckboxProps) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const handleChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newChecked = event.target.checked;
|
||||
onChange(event, newChecked);
|
||||
const inputElement = inputRef.current;
|
||||
if (newChecked && inputElement) {
|
||||
playCheckAnimation(inputElement.parentElement as Element).catch(
|
||||
console.error
|
||||
);
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.indeterminate = !!intermediate;
|
||||
}
|
||||
}, [intermediate]);
|
||||
|
||||
const icon = intermediate
|
||||
? icons.intermediate
|
||||
: checked
|
||||
? icons.checked
|
||||
: icons.unchecked;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.root, disabled && styles.disabled)}
|
||||
{...otherProps}
|
||||
>
|
||||
{icon}
|
||||
<input
|
||||
ref={inputRef}
|
||||
data-testid="affine-checkbox"
|
||||
className={clsx(styles.input)}
|
||||
type="checkbox"
|
||||
value={checked ? 'on' : 'off'}
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const playCheckAnimation = async (refElement: Element) => {
|
||||
const sparkingEl = document.createElement('div');
|
||||
sparkingEl.classList.add('affine-check-animation');
|
||||
sparkingEl.style.cssText = `
|
||||
position: absolute;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border-radius: 50%;
|
||||
font-size: inherit;
|
||||
`;
|
||||
refElement.appendChild(sparkingEl);
|
||||
|
||||
await sparkingEl.animate(
|
||||
[
|
||||
{
|
||||
offset: 0.5,
|
||||
boxShadow:
|
||||
'0 -18px 0 -8px #1e96eb, 16px -8px 0 -8px #1e96eb, 16px 8px 0 -8px #1e96eb, 0 18px 0 -8px #1e96eb, -16px 8px 0 -8px #1e96eb, -16px -8px 0 -8px #1e96eb',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
boxShadow:
|
||||
'0 -32px 0 -10px transparent, 32px -16px 0 -10px transparent, 32px 16px 0 -10px transparent, 0 36px 0 -10px transparent, -32px 16px 0 -10px transparent, -32px -16px 0 -10px transparent',
|
||||
},
|
||||
],
|
||||
{ duration: 500, easing: 'ease', fill: 'forwards' }
|
||||
).finished;
|
||||
|
||||
sparkingEl.remove();
|
||||
};
|
||||
52
packages/frontend/component/src/ui/checkbox/icons.tsx
Normal file
52
packages/frontend/component/src/ui/checkbox/icons.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
const unchecked = (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6 3.25C4.48122 3.25 3.25 4.48122 3.25 6V18C3.25 19.5188 4.48122 20.75 6 20.75H18C19.5188 20.75 20.75 19.5188 20.75 18V6C20.75 4.48122 19.5188 3.25 18 3.25H6ZM4.75 6C4.75 5.30964 5.30964 4.75 6 4.75H18C18.6904 4.75 19.25 5.30964 19.25 6V18C19.25 18.6904 18.6904 19.25 18 19.25H6C5.30964 19.25 4.75 18.6904 4.75 18V6Z"
|
||||
fill="var(--affine-icon-color)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const checked = (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.25 6C3.25 4.48122 4.48122 3.25 6 3.25H18C19.5188 3.25 20.75 4.48122 20.75 6V18C20.75 19.5188 19.5188 20.75 18 20.75H6C4.48122 20.75 3.25 19.5188 3.25 18V6ZM16.5303 9.53033C16.8232 9.23744 16.8232 8.76256 16.5303 8.46967C16.2374 8.17678 15.7626 8.17678 15.4697 8.46967L10.5 13.4393L9.03033 11.9697C8.73744 11.6768 8.26256 11.6768 7.96967 11.9697C7.67678 12.2626 7.67678 12.7374 7.96967 13.0303L9.96967 15.0303C10.2626 15.3232 10.7374 15.3232 11.0303 15.0303L16.5303 9.53033Z"
|
||||
fill="var(--affine-blue-600)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const intermediate = (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6 3.25C4.48122 3.25 3.25 4.48122 3.25 6V18C3.25 19.5188 4.48122 20.75 6 20.75H18C19.5188 20.75 20.75 19.5188 20.75 18V6C20.75 4.48122 19.5188 3.25 18 3.25H6ZM8.54 11.25C8.12579 11.25 7.79 11.5858 7.79 12C7.79 12.4142 8.12579 12.75 8.54 12.75H15.54C15.9542 12.75 16.29 12.4142 16.29 12C16.29 11.5858 15.9542 11.25 15.54 11.25H8.54Z"
|
||||
fill="var(--affine-icon-color)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { checked, intermediate, unchecked };
|
||||
28
packages/frontend/component/src/ui/checkbox/index.css.ts
Normal file
28
packages/frontend/component/src/ui/checkbox/index.css.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
':hover': {
|
||||
opacity: 0.8,
|
||||
},
|
||||
':active': {
|
||||
opacity: 0.9,
|
||||
},
|
||||
});
|
||||
|
||||
export const disabled = style({
|
||||
opacity: 0.5,
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
export const input = style({
|
||||
opacity: 0,
|
||||
position: 'absolute',
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
inset: 0,
|
||||
cursor: 'pointer',
|
||||
fontSize: 'inherit',
|
||||
});
|
||||
1
packages/frontend/component/src/ui/checkbox/index.ts
Normal file
1
packages/frontend/component/src/ui/checkbox/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './checkbox';
|
||||
@@ -1,39 +0,0 @@
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
|
||||
import { Input } from '.';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Input',
|
||||
component: Input,
|
||||
} satisfies Meta<typeof Input>;
|
||||
|
||||
export const Basic: StoryFn<typeof Input> = () => {
|
||||
return <Input data-testid="test-input" defaultValue="test" />;
|
||||
};
|
||||
|
||||
Basic.play = async ({ canvasElement }) => {
|
||||
const element = within(canvasElement);
|
||||
const item = element.getByTestId('test-input') as HTMLInputElement;
|
||||
expect(item).toBeTruthy();
|
||||
expect(item.value).toBe('test');
|
||||
userEvent.clear(item);
|
||||
userEvent.type(item, 'test 2');
|
||||
expect(item.value).toBe('test 2');
|
||||
};
|
||||
|
||||
export const DynamicHeight: StoryFn<typeof Input> = () => {
|
||||
return <Input width={200} data-testid="test-input" />;
|
||||
};
|
||||
|
||||
DynamicHeight.play = async ({ canvasElement }) => {
|
||||
const element = within(canvasElement);
|
||||
const item = element.getByTestId('test-input') as HTMLInputElement;
|
||||
expect(item).toBeTruthy();
|
||||
expect(item.getBoundingClientRect().width).toBe(200);
|
||||
};
|
||||
|
||||
export const NoBorder: StoryFn<typeof Input> = () => {
|
||||
return <Input noBorder={true} data-testid="test-input" />;
|
||||
};
|
||||
Reference in New Issue
Block a user