Skip to content

Commit

Permalink
feat: DatePicker blockedRanges (schummar#101)
Browse files Browse the repository at this point in the history
* "type" for button to avoid submit in forms; datePicker story

* more button types

* blocked ranges are coloured correctly

* show blocked color also when selected

* Cleanup

* moved css out

---------

Co-authored-by: Michael Förg <[email protected]>
  • Loading branch information
fer0n and Michael Förg authored Nov 27, 2023
1 parent 1ab6874 commit e4e954b
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 35 deletions.
1 change: 0 additions & 1 deletion docs/stories/bExternalFilters.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export const Primary = () => {
setBirthday(null);
}}
columns={(col) => [
//
col((x) => x.avatar, {
header: 'Avatar',
renderCell: (avatar) => <img width={50} height={50} src={avatar} />,
Expand Down
41 changes: 41 additions & 0 deletions docs/stories/fDatePicker.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useState } from 'react';
import { DatePicker, DatePickerProps, DateRange } from '../../src';

const today = new Date();
const nextWeek = new Date();
nextWeek.setDate(today.getDate() + 7);

export default {
title: 'Date Picker',
component: DatePicker,
argTypes: {
value: { control: 'date' },
onChange: { action: 'changed' },
blockedRanges: {
control: 'object',
},
},
};

export const Primary = (args: DatePickerProps) => {
const [date, setDate] = useState<DateRange | null>(null);

return (
<DatePicker
{...args}
value={date}
onChange={(v) => {
if (v instanceof Date) {
setDate({ min: v, max: v });
} else {
setDate(v);
}
args.onChange(v);
}}
/>
);
};

Primary.args = {
value: new Date(),
};
103 changes: 78 additions & 25 deletions src/components/datePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { gray } from '../theme/defaultTheme/defaultClasses';
import { useCssVariables } from '../theme/useCssVariables';
import { DateInput } from './dateInput';
import { Text } from './text';
import { Interpolation, Theme } from '@emotion/react';

export type DateRange = { min: Date; max: Date };

Expand Down Expand Up @@ -45,6 +46,8 @@ export type DatePickerProps = {
/** Show buttons to quickly select suggested dates or date ranges */
quickOptions?: DatePickerQuickOption[];
/** Minimum selectable date */
/** Date ranges that are visually marked as blocked */
blockedRanges?: DateRange[];
minDate?: Date;
/** Maximum selectable date */
maxDate?: Date;
Expand Down Expand Up @@ -209,6 +212,7 @@ export function DatePicker(props: DatePickerProps) {
minDate,
maxDate,
showCalendarWeek,
blockedRanges = [],
} = defaults<DatePickerProps>(props, context);

function onChange(value: Date | DateRange | null) {
Expand All @@ -228,6 +232,65 @@ export function DatePicker(props: DatePickerProps) {
const IconButton = useTheme((t) => t.components.IconButton);
const ChevronRight = useTheme((t) => t.icons.ChevronRight);
const cssVariables = useCssVariables();
function getDayCssStyles({
disabled,
prevMonth,
nextMonth,
today,
blocked,
selected,
preSelected,
}: {
disabled?: boolean;
prevMonth?: boolean;
nextMonth?: boolean;
today?: boolean;
blocked?: boolean;
selected?: boolean;
preSelected?: boolean;
}): Interpolation<Theme> {
return [
{
padding: 10,
border: 'none',
background: 'transparent',
cursor: disabled ? undefined : 'pointer',
font: 'inherit',
},
(prevMonth || nextMonth) && {
color: gray,
},
today && {
outline: '1px solid var(--secondaryMain)',
},
blocked && {
background: 'var(--blockedMain)',
color: 'var(--blockedContrastText)',
},
selected && {
background: 'var(--primaryMain)',
color: 'var(--primaryContrastText)',
},
blocked &&
selected && {
position: 'relative',
'::before': {
content: '""',
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
background: 'var(--blockedMain)',
clipPath: 'polygon(0 0, 0 50%, 50% 0)',
},
},
preSelected && {
background: 'var(--primaryLight)',
color: 'var(--primaryContrastText)',
},
];
}

const mountTime = useMemo(() => new Date(), []);
const [dateInView, setDateInView] = useState<Date>(defaultDateInView ?? mountTime);
Expand All @@ -252,6 +315,7 @@ export function DatePicker(props: DatePickerProps) {
<Button
key={index}
variant="text"
type="button"
onClick={() => {
setDirty(undefined);

Expand Down Expand Up @@ -375,15 +439,15 @@ export function DatePicker(props: DatePickerProps) {
alignItems: 'center',
}}
>
<IconButton {...getBackProps({ calendars })}>
<IconButton type="button" {...getBackProps({ calendars })}>
<ChevronRight css={{ transform: 'rotate3d(0, 0, 1, 180deg)' }} />
</IconButton>

<div css={{ display: 'flex' }}>
{formatMonth(month)} {formatYear(year)}
</div>

<IconButton {...getForwardProps({ calendars })}>
<IconButton type="button" {...getForwardProps({ calendars })}>
<ChevronRight />
</IconButton>
</div>
Expand Down Expand Up @@ -433,6 +497,7 @@ export function DatePicker(props: DatePickerProps) {
<Fragment key={index}>
{showCalendarWeek ? (
<button
type="button"
css={{
padding: 10,
border: 'none',
Expand Down Expand Up @@ -480,33 +545,21 @@ export function DatePicker(props: DatePickerProps) {
date,
min <= hovered ? { min, max: hovered } : { min: hovered, max: min },
)));
const blocked = blockedRanges.some((range) => dateIntersect(date, range));

return (
<button
type="button"
key={`${index}-${dayIndex}`}
css={[
{
padding: 10,
border: 'none',
background: 'transparent',
cursor: disabled ? undefined : 'pointer',
font: 'inherit',
},
(prevMonth || nextMonth) && {
color: gray,
},
today && {
outline: '1px solid var(--secondaryMain)',
},
selected && {
background: 'var(--primaryMain)',
color: 'var(--primaryContrastText)',
},
preSelected && {
background: 'var(--primaryLight)',
color: 'var(--primaryContrastText)',
},
]}
css={getDayCssStyles({
disabled,
prevMonth,
nextMonth,
today,
blocked,
selected,
preSelected,
})}
{...getDateProps({ dateObj: dateObject })}
onClick={() => {
if (dirty) {
Expand Down
1 change: 1 addition & 0 deletions src/theme/defaultTheme/defaultColors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import type { TableTheme } from '../../types';
export const defaultColors: TableTheme['colors'] = {
primary: { main: '#1976d2', light: '#42a5f5', contrastText: '#fff' },
secondary: { main: '#9c27b0', light: '#ba68c8', contrastText: '#fff' },
blocked: { main: '#f44336', light: '#f26257', contrastText: '#fff' },
};
1 change: 1 addition & 0 deletions src/theme/tableTheme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function mergeThemes<T>(...themes: PartialTableTheme<T>[]): PartialTableT
colors: {
primary: Object.assign({}, ...themes.map((theme) => theme.colors?.primary)),
secondary: Object.assign({}, ...themes.map((theme) => theme.colors?.secondary)),
blocked: Object.assign({}, ...themes.map((theme) => theme.colors?.blocked)),
},
spacing: themes
.map((theme) => theme.spacing)
Expand Down
23 changes: 14 additions & 9 deletions src/theme/useCssVariables.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { useTheme } from '../hooks/useTheme';

export function useCssVariables() {
return useTheme(({ spacing, colors }) => ({
'--spacing': spacing,
'--primaryMain': colors.primary.main,
'--primaryLight': colors.primary.light,
'--primaryContrastText': colors.primary.contrastText,
'--secondaryMain': colors.secondary.main,
'--secondaryLight': colors.secondary.light,
'--secondaryContrastText': colors.secondary.contrastText,
}));
return useTheme(({ spacing, colors }) => {
return {
'--spacing': spacing,
'--primaryMain': colors.primary.main,
'--primaryLight': colors.primary.light,
'--primaryContrastText': colors.primary.contrastText,
'--secondaryMain': colors.secondary.main,
'--secondaryLight': colors.secondary.light,
'--secondaryContrastText': colors.secondary.contrastText,
'--blockedMain': colors.blocked.main,
'--blockedLight': colors.blocked.light,
'--blockedContrastText': colors.blocked.contrastText,
};
});
}
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export interface TableTheme<T = unknown> {
children: ReactNode;
onClick?: (event: React.MouseEvent<Element>) => void;
className?: string;
type?: React.ButtonHTMLAttributes<HTMLButtonElement>['type'];
}>;
Button: ComponentType<{
children: ReactNode;
Expand All @@ -83,6 +84,7 @@ export interface TableTheme<T = unknown> {
variant?: 'text' | 'outlined' | 'contained';
disabled?: boolean;
className?: string;
type?: React.ButtonHTMLAttributes<HTMLButtonElement>['type'];
}>;
Checkbox: ComponentType<{
checked: boolean;
Expand Down Expand Up @@ -132,6 +134,7 @@ export interface TableTheme<T = unknown> {
colors: {
primary: { main: string; light: string; contrastText: string };
secondary: { main: string; light: string; contrastText: string };
blocked: { main: string; light: string; contrastText: string };
};
/** Spacing. */
spacing: string | number;
Expand Down

0 comments on commit e4e954b

Please sign in to comment.