feat: add date picker (#2644)

Co-authored-by: himself65 <himself65@outlook.com>
This commit is contained in:
JimmFly
2023-06-08 17:55:16 +08:00
committed by GitHub
parent bedf838fe5
commit 29d8f61c90
15 changed files with 706 additions and 22 deletions

View File

@@ -43,6 +43,7 @@
"lit": "^2.7.5",
"lottie-web": "^5.12.0",
"react": "18.3.0-canary-16d053d59-20230506",
"react-datepicker": "^4.12.0",
"react-dom": "18.3.0-canary-16d053d59-20230506",
"react-error-boundary": "^4.0.9",
"react-is": "^18.2.0",
@@ -56,6 +57,7 @@
"@blocksuite/lit": "0.0.0-20230606130340-805f430b-nightly",
"@blocksuite/store": "0.0.0-20230606130340-805f430b-nightly",
"@types/react": "^18.2.6",
"@types/react-datepicker": "^4.11.2",
"@types/react-dnd": "^3.0.2",
"@types/react-dom": "18.2.4",
"@vanilla-extract/css": "^1.11.0",

View File

@@ -0,0 +1,191 @@
import {
ArrowDownSmallIcon,
ArrowLeftSmallIcon,
ArrowRightSmallIcon,
} from '@blocksuite/icons';
import dayjs from 'dayjs';
import { useCallback, useState } from 'react';
import DatePicker from 'react-datepicker';
import * as styles from './index.css';
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
type DatePickerProps = {
value?: string;
onChange: (value: string) => void;
};
export const AFFiNEDatePicker = (props: DatePickerProps) => {
const { value, onChange } = props;
const [openMonthPicker, setOpenMonthPicker] = useState(false);
const [selectedDate, setSelectedDate] = useState<Date | null>(
value ? dayjs(value).toDate() : null
);
const handleOpenMonthPicker = useCallback(() => {
setOpenMonthPicker(true);
}, []);
const handleCloseMonthPicker = useCallback(() => {
setOpenMonthPicker(false);
}, []);
const handleSelectDate = (date: Date | null) => {
if (date) {
setSelectedDate(date);
onChange(dayjs(date).format('YYYY-MM-DD'));
setOpenMonthPicker(false);
}
};
const renderCustomHeader = ({
date,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}: {
date: Date;
decreaseMonth: () => void;
increaseMonth: () => void;
prevMonthButtonDisabled: boolean;
nextMonthButtonDisabled: boolean;
}) => {
const selectedYear = dayjs(date).year();
const selectedMonth = dayjs(date).month();
return (
<div className={styles.headerStyle}>
<div
data-testid="date-picker-current-month"
className={styles.mouthStyle}
>
{months[selectedMonth]}
</div>
<div
data-testid="date-picker-current-year"
className={styles.yearStyle}
>
{selectedYear}
</div>
<div
data-testid="month-picker-button"
className={styles.arrowDownStyle}
onClick={handleOpenMonthPicker}
>
<ArrowDownSmallIcon />
</div>
<button
data-testid="date-picker-prev-button"
className={styles.arrowLeftStyle}
onClick={decreaseMonth}
disabled={prevMonthButtonDisabled}
>
<ArrowLeftSmallIcon />
</button>
<button
data-testid="date-picker-next-button"
className={styles.arrowRightStyle}
onClick={increaseMonth}
disabled={nextMonthButtonDisabled}
>
<ArrowRightSmallIcon />
</button>
</div>
);
};
const renderCustomMonthHeader = ({
date,
decreaseYear,
increaseYear,
prevYearButtonDisabled,
nextYearButtonDisabled,
}: {
date: Date;
decreaseYear: () => void;
increaseYear: () => void;
prevYearButtonDisabled: boolean;
nextYearButtonDisabled: boolean;
}) => {
const selectedYear = dayjs(date).year();
return (
<div className={styles.monthHeaderStyle}>
<div
data-testid="month-picker-current-year"
className={styles.monthTitleStyle}
>
{selectedYear}
</div>
<button
data-testid="month-picker-prev-button"
className={styles.arrowLeftStyle}
onClick={decreaseYear}
disabled={prevYearButtonDisabled}
>
<ArrowLeftSmallIcon />
</button>
<button
data-testid="month-picker-next-button"
className={styles.arrowRightStyle}
onClick={increaseYear}
disabled={nextYearButtonDisabled}
>
<ArrowRightSmallIcon />
</button>
</div>
);
};
return (
<DatePicker
onClickOutside={handleCloseMonthPicker}
className={styles.inputStyle}
calendarClassName={styles.calendarStyle}
weekDayClassName={() => styles.weekStyle}
dayClassName={() => styles.dayStyle}
popperClassName={styles.popperStyle}
monthClassName={() => styles.mouthsStyle}
selected={selectedDate}
onChange={handleSelectDate}
showPopperArrow={false}
dateFormat="MMM dd"
showMonthYearPicker={openMonthPicker}
shouldCloseOnSelect={!openMonthPicker}
renderCustomHeader={({
date,
decreaseYear,
increaseYear,
decreaseMonth,
increaseMonth,
prevYearButtonDisabled,
nextYearButtonDisabled,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) =>
openMonthPicker
? renderCustomMonthHeader({
date,
decreaseYear,
increaseYear,
prevYearButtonDisabled,
nextYearButtonDisabled,
})
: renderCustomHeader({
date,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
})
}
/>
);
};
export default AFFiNEDatePicker;

View File

@@ -0,0 +1,200 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const inputStyle = style({
fontSize: 'var(--affine-font-xs)',
width: '50px',
fontWeight: '600',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '22px',
marginLeft: '10px',
marginRight: '10px',
});
export const popperStyle = style({
boxShadow: 'var(--affine-shadow-2)',
padding: '0 10px',
marginTop: '16px',
background: 'var(--affine-background-overlay-panel-color)',
borderRadius: '12px',
width: '300px',
});
globalStyle('.react-datepicker__header', {
background: 'var(--affine-background-overlay-panel-color)',
border: 'none',
marginBottom: '6px',
});
export const headerStyle = style({
background: 'var(--affine-background-overlay-panel-color)',
border: 'none',
display: 'flex',
width: '100%',
alignItems: 'center',
marginBottom: '12px',
padding: '0 14px',
position: 'relative',
});
export const monthHeaderStyle = style({
background: 'var(--affine-background-overlay-panel-color)',
border: 'none',
display: 'flex',
width: '100%',
alignItems: 'center',
marginBottom: '18px',
padding: '0 14px',
position: 'relative',
'::after': {
content: '""',
position: 'absolute',
width: 'calc(100% - 24px)',
height: '1px',
background: 'var(--affine-border-color)',
bottom: '-18px',
left: '12px',
},
});
export const monthTitleStyle = style({
color: 'var(--affine-text-primary-color)',
fontWeight: '600',
fontSize: 'var(--affine-font-sm)',
marginLeft: '12px',
});
export const yearStyle = style({
marginLeft: '8px',
color: 'var(--affine-text-primary-color)',
fontWeight: '600',
fontSize: 'var(--affine-font-sm)',
});
export const mouthStyle = style({
color: 'var(--affine-text-primary-color)',
fontWeight: '600',
fontSize: 'var(--affine-font-sm)',
});
export const arrowLeftStyle = style({
width: '16px',
height: '16px',
textAlign: 'right',
position: 'absolute',
right: '50px',
});
export const arrowRightStyle = style({
width: '16px',
height: '16px',
right: '14px',
position: 'absolute',
});
export const weekStyle = style({
fontSize: 'var(--affine-font-xs)',
color: 'var(--affine-text-secondary-color)',
display: 'inline-block',
width: '28px',
height: '28px',
lineHeight: '28px',
padding: '0 4px',
margin: '0px 6px',
verticalAlign: 'middle',
});
export const calendarStyle = style({
background: 'var(--affine-background-overlay-panel-color)',
border: 'none',
width: '100%',
});
export const dayStyle = style({
fontSize: 'var(--affine-font-xs)',
color: 'var(--affine-text-primary-color)',
display: 'inline-block',
width: '28px',
height: '28px',
lineHeight: '28px',
padding: '0 4px',
margin: '6px 12px 6px 0px',
verticalAlign: 'middle',
fontWeight: '400',
borderRadius: '8px',
selectors: {
'&:hover': {
background: 'var(--affine-hover-color)',
borderRadius: '8px',
transition: 'background-color 0.3s ease-in-out',
},
'&[aria-selected="true"]': {
color: 'var(--affine-black)',
background: 'var(--affine-hover-color)',
},
'&[aria-selected="true"]:hover': {
background: 'var(--affine-hover-color)',
},
'&[tabindex="0"][aria-selected="false"]': {
background: 'var(--affine-background-overlay-panel-color)',
},
'&.react-datepicker__day--today[aria-selected="false"]': {
background: 'var(--affine-primary-color)',
color: 'var(--affine-palette-line-white)',
},
'&.react-datepicker__day--today[aria-selected="false"]:hover': {
color: 'var(--affine-black)',
background: 'var(--affine-hover-color)',
},
'&.react-datepicker__day--outside-month[aria-selected="false"]': {
color: 'var(--affine-text-disable-color)',
},
},
});
export const arrowDownStyle = style({
width: '16px',
height: '16px',
marginLeft: '4px',
color: 'var(--affine-icon-color)',
fontSize: 'var(--affine-font-sm)',
cursor: 'pointer',
});
export const mouthsStyle = style({
fontSize: 'var(--affine-font-base)',
color: 'var(--affine-text-primary-color)',
display: 'inline-block',
lineHeight: '22px',
padding: '6px 16px',
fontWeight: '400',
borderRadius: '8px',
selectors: {
'&:hover': {
background: 'var(--affine-hover-color)',
transition: 'background-color 0.3s ease-in-out',
borderRadius: '8px',
},
'&[aria-selected="true"]': {
color: 'var(--affine-black)',
background: 'var(--affine-hover-color)',
fontWeight: '400',
},
'&[aria-selected="true"]:hover': {
background: 'var(--affine-hover-color)',
},
'&[tabindex="0"][aria-selected="false"]': {
background: 'var(--affine-background-overlay-panel-color)',
},
'&.react-datepicker__month-text--today[aria-selected="false"]': {
background: 'var(--affine-primary-color)',
color: 'var(--affine-palette-line-white)',
},
'&.react-datepicker__month-text--today[aria-selected="false"]:hover': {
background: 'var(--affine-hover-color)',
color: 'var(--affine-black)',
},
},
});
globalStyle(`${calendarStyle} .react-datepicker__month-container`, {
float: 'none',
width: '100%',
});
globalStyle(`${calendarStyle} .react-datepicker__month-wrapper`, {
display: 'flex',
justifyContent: 'space-between',
marginBottom: '18px',
});
globalStyle(`${calendarStyle} .react-datepicker__month-text`, {
margin: '0',
width: '64px',
});

View File

@@ -0,0 +1 @@
export * from './date-picker';

View File

@@ -6,6 +6,7 @@ import { Menu, MenuItem } from '../../../ui/menu';
import * as styles from './index.css';
import { literalMatcher } from './literal-matcher';
import type { TFunction, TType } from './logical/typesystem';
import { variableDefineMap } from './shared-types';
import { filterMatcher, VariableSelect, vars } from './vars';
export const Condition = ({
@@ -35,7 +36,10 @@ export const Condition = ({
content={<VariableSelect selected={[]} onSelect={onChange} />}
>
<div data-testid="variable-name" className={styles.filterTypeStyle}>
{ast.left.name}
<div className={styles.filterTypeIconStyle}>
{variableDefineMap[ast.left.name].icon}
</div>
<div>{ast.left.name}</div>
</div>
</Menu>
<Menu
@@ -101,7 +105,7 @@ export const Arg = ({
return null;
}
return (
<div data-testid="filter-arg" style={{ marginLeft: 4 }}>
<div data-testid="filter-arg" style={{ marginLeft: 4, fontWeight: 600 }}>
{data.render({ type, value, onChange })}
</div>
);

View File

@@ -41,12 +41,21 @@ export const filterItemCloseStyle = style({
});
export const inputStyle = style({
fontSize: 'var(--affine-font-xs)',
margin: '0 10px',
});
export const switchStyle = style({
fontSize: 'var(--affine-font-xs)',
color: 'var(--affine-text-secondary-color)',
marginLeft: '4px',
margin: '0 10px',
});
export const filterTypeStyle = style({
fontSize: 'var(--affine-font-sm)',
display: 'flex',
marginRight: '10px',
});
export const filterTypeIconStyle = style({
fontSize: 'var(--affine-font-base)',
marginRight: '6px',
display: 'flex',
color: 'var(--affine-icon-color)',
});

View File

@@ -2,6 +2,7 @@ import type { Literal } from '@affine/env/filter';
import dayjs from 'dayjs';
import type { ReactNode } from 'react';
import { AFFiNEDatePicker } from '../../date-picker';
import { inputStyle } from './index.css';
import { tBoolean, tDate } from './logical/custom-type';
import { Matcher } from './logical/matcher';
@@ -33,14 +34,12 @@ literalMatcher.register(tBoolean.create(), {
});
literalMatcher.register(tDate.create(), {
render: ({ value, onChange }) => (
<input
className={inputStyle}
<AFFiNEDatePicker
value={dayjs(value.value as number).format('YYYY-MM-DD')}
type="date"
onChange={e => {
onChange({
type: 'literal',
value: dayjs(e.target.value, 'YYYY-MM-DD').valueOf(),
value: dayjs(e, 'YYYY-MM-DD').valueOf(),
});
}}
/>

View File

@@ -1,6 +1,5 @@
import type { Literal, LiteralValue, VariableMap } from '@affine/env/filter';
import { DateTimeIcon, FavoritedIcon } from '@blocksuite/icons';
import type { ReactElement } from 'react';
import { tBoolean, tDate } from './logical/custom-type';
import type { TType } from './logical/typesystem';
@@ -13,7 +12,6 @@ export const toLiteral = (value: LiteralValue): Literal => ({
export type FilterVariable = {
name: keyof VariableMap;
type: TType;
icon: ReactElement;
};
export const variableDefineMap = {

View File

@@ -63,7 +63,7 @@ export const VariableSelect = ({
// .filter(v => !selected.find(filter => filter.left.name === v.name))
.map(v => (
<MenuItem
icon={v.icon}
icon={variableDefineMap[v.name].icon}
key={v.name}
onClick={() => {
onSelect(createDefaultFilter(v));

View File

@@ -3,7 +3,7 @@ import { SaveIcon } from '@blocksuite/icons';
import { uuidv4 } from '@blocksuite/store';
import { useState } from 'react';
import { Button, Input, Modal, ModalWrapper } from '../../..';
import { Button, Input, Modal, ModalCloseButton, ModalWrapper } from '../../..';
import { FilterList } from '../filter';
import * as styles from './view-list.css';
@@ -25,10 +25,10 @@ const CreateView = ({
return (
<div>
<h1>Save As New View</h1>
<div className={styles.saveTitle}>Save As New View</div>
<div
style={{
backgroundColor: 'rgba(0, 0, 0, 0.04)',
backgroundColor: 'var(--affine-hover-color)',
borderRadius: 8,
padding: 20,
marginTop: 20,
@@ -47,11 +47,13 @@ const CreateView = ({
/>
</div>
<div
style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 20 }}
style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 40 }}
>
<Button onClick={onCancel}>Cancel</Button>
<Button className={styles.cancelButton} onClick={onCancel}>
Cancel
</Button>
<Button
style={{ marginLeft: 20 }}
style={{ marginLeft: 20, borderRadius: '8px' }}
type="primary"
onClick={() => {
if (value.name.trim().length > 0) {
@@ -82,7 +84,19 @@ export const SaveViewButton = ({ init, onConfirm }: CreateViewProps) => {
</div>
</Button>
<Modal open={show} onClose={() => changeShow(false)}>
<ModalWrapper width={560} style={{ padding: '40px' }}>
<ModalWrapper
width={560}
style={{
padding: '40px',
background: 'var(--affine-background-primary-color)',
}}
>
<ModalCloseButton
top={12}
right={12}
onClick={() => changeShow(false)}
hoverColor="var(--affine-icon-color)"
/>
<CreateView
init={init}
onCancel={() => changeShow(false)}

View File

@@ -60,3 +60,17 @@ export const saveText = style({
justifyContent: 'center',
fontSize: 'var(--affine-font-sm)',
});
export const cancelButton = style({
background: 'var(--affine-hover-color)',
borderRadius: '8px',
':hover': {
background: 'var(--affine-hover-color)',
color: 'var(--affine-text-primary-color)',
border: '1px solid var(--affine-border-color)',
},
});
export const saveTitle = style({
fontSize: 'var(--affine-font-h-6)',
fontWeight: '600',
lineHeight: '24px',
});

View File

@@ -1,3 +1,4 @@
@import 'react-datepicker/dist/react-datepicker.css';
* {
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);

View File

@@ -0,0 +1,13 @@
import { AFFiNEDatePicker } from '@affine/component/date-picker';
import type { StoryFn } from '@storybook/react';
import { useState } from 'react';
export default {
title: 'AFFiNE/AFFiNEDatePicker',
component: AFFiNEDatePicker,
};
export const Default: StoryFn = () => {
const [value, setValue] = useState<string>(new Date().toString());
return <AFFiNEDatePicker value={value} onChange={setValue} />;
};