Skip to content

Commit 420d956

Browse files
authored
feat: add month range picker (#560)
1 parent 687aea4 commit 420d956

File tree

8 files changed

+606
-0
lines changed

8 files changed

+606
-0
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import styled, { css } from 'styled-components';
2+
3+
import { getSemanticValue } from '../../../utils/cssVariables';
4+
import { get } from '../../../utils/themeGet';
5+
6+
interface MonthButtonProps {
7+
isSelectedStartOrEnd: boolean;
8+
disabled: boolean;
9+
isInRange: boolean;
10+
}
11+
12+
const getColor = ({ isSelectedStartOrEnd, isInRange, disabled }: MonthButtonProps) => {
13+
if (isSelectedStartOrEnd) {
14+
return css`
15+
color: ${getSemanticValue('foreground-on-background-accent')};
16+
background: ${getSemanticValue('background-element-accent-emphasized')};
17+
box-shadow: 0 0 0 0.0625rem ${getSemanticValue('border-accent-default')};
18+
z-index: 2;
19+
`;
20+
}
21+
22+
if (isInRange) {
23+
return css`
24+
color: ${getSemanticValue('foreground-accent-default')};
25+
background: ${getSemanticValue('background-element-accent-faded')};
26+
box-shadow: 0 0 0 0.0625rem ${getSemanticValue('border-accent-faded')};
27+
z-index: 1;
28+
29+
&:hover {
30+
cursor: pointer;
31+
background: ${getSemanticValue('background-element-accent-default')};
32+
color: ${getSemanticValue('foreground-accent-emphasized')};
33+
}
34+
`;
35+
}
36+
37+
if (disabled) {
38+
return css`
39+
color: ${getSemanticValue('foreground-disabled')};
40+
box-shadow: 0 0 0 0.0625rem ${getSemanticValue('border-disabled')};
41+
background: ${getSemanticValue('transparent')};
42+
43+
&:hover {
44+
cursor: not-allowed;
45+
}
46+
`;
47+
}
48+
49+
return css`
50+
color: ${getSemanticValue('foreground-primary')};
51+
background: ${getSemanticValue('transparent')};
52+
53+
&:hover {
54+
cursor: pointer;
55+
background: ${getSemanticValue('background-element-accent-default')};
56+
color: ${getSemanticValue('foreground-accent-emphasized')};
57+
}
58+
`;
59+
};
60+
61+
const MonthButton = styled.button.attrs({ type: 'button' })<MonthButtonProps>`
62+
font-family: ${get('fonts.normal')};
63+
font-weight: ${get('fontWeights.normal')};
64+
font-size: ${get('fontSizes.0')};
65+
border: 0;
66+
padding: 0.5rem;
67+
box-shadow: 0 0 0 0.0625rem ${getSemanticValue('border-neutral-default')};
68+
outline: none;
69+
70+
transition-property: background, box-shadow, color;
71+
transition-duration: 200ms;
72+
transition-timing-function: ease;
73+
74+
&:hover {
75+
cursor: pointer;
76+
}
77+
78+
${getColor}
79+
`;
80+
81+
export { MonthButton };
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React, { FC } from 'react';
2+
import { format } from 'date-fns';
3+
import styled from 'styled-components';
4+
import { Text } from '../../Text/Text';
5+
import { MonthButton } from './MonthButton';
6+
7+
const MonthGrid = styled.div`
8+
display: grid;
9+
grid-template-columns: repeat(4, 1fr);
10+
gap: 8px;
11+
`;
12+
13+
const YearSection = styled.div`
14+
flex: 1;
15+
`;
16+
17+
export interface MonthCalendarProps {
18+
year: number;
19+
onClick: (monthIndex: number, year: number) => void;
20+
onHover: (monthIndex: number, year: number) => void;
21+
isMonthDisabled: (year: number, month: number) => boolean;
22+
isInRange: (date: Date) => boolean;
23+
isSelectedStartOrEnd: (date: Date) => boolean;
24+
locale: Locale;
25+
}
26+
27+
export const MonthCalendar: FC<MonthCalendarProps> = ({
28+
year,
29+
onClick,
30+
onHover,
31+
isMonthDisabled,
32+
isSelectedStartOrEnd,
33+
isInRange,
34+
locale
35+
}) => (
36+
<YearSection>
37+
<Text as="p" fontWeight="bold" mb={2} textAlign="center">
38+
{year}
39+
</Text>
40+
<MonthGrid>
41+
{Array.from({ length: 12 }).map((_, index) => {
42+
const date = new Date(year, index, 1);
43+
const monthName = format(date, 'MMM', { locale });
44+
const isDisabled = isMonthDisabled(year, index);
45+
46+
return (
47+
<MonthButton
48+
key={monthName}
49+
onClick={() => onClick(index, year)}
50+
onMouseEnter={() => onHover(index, year)}
51+
disabled={isDisabled}
52+
isInRange={isInRange(date)}
53+
isSelectedStartOrEnd={isSelectedStartOrEnd(date)}
54+
aria-label={`${monthName} ${year}`}
55+
aria-pressed={isSelectedStartOrEnd(date)}
56+
>
57+
{monthName}
58+
</MonthButton>
59+
);
60+
})}
61+
</MonthGrid>
62+
</YearSection>
63+
);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import styled, { css } from 'styled-components';
2+
import { Elevation, MediaQueries } from '../../essentials';
3+
import { getSemanticValue } from '../../utils/cssVariables';
4+
5+
const baseArrowStyles = css`
6+
width: 1.25rem;
7+
height: 1.25rem;
8+
position: absolute;
9+
background: inherit;
10+
`;
11+
12+
export const Arrow = styled.div`
13+
visibility: hidden;
14+
${baseArrowStyles};
15+
16+
&::before {
17+
${baseArrowStyles};
18+
visibility: visible;
19+
content: '';
20+
transform: rotate(45deg);
21+
}
22+
`;
23+
24+
export const MonthPickerContentContainer = styled.div`
25+
background: ${getSemanticValue('background-surface-neutral-default')};
26+
box-shadow: 0 0 0.5rem 0.1875rem ${getSemanticValue('border-neutral-faded')};
27+
z-index: ${Elevation.DATEPICKER};
28+
29+
&[data-popper-placement^='top'] > ${Arrow} {
30+
bottom: -0.625rem;
31+
&::before {
32+
box-shadow: 0.25rem 0.25rem 0.5rem -0.125rem ${getSemanticValue('border-neutral-faded')};
33+
}
34+
}
35+
36+
&[data-popper-placement^='bottom'] > ${Arrow} {
37+
top: -0.625rem;
38+
&::before {
39+
box-shadow: -0.25rem -0.25rem 0.5rem -0.125rem ${getSemanticValue('border-neutral-faded')};
40+
}
41+
}
42+
43+
${MediaQueries.small} {
44+
padding: 1.5rem;
45+
margin-left: 0;
46+
}
47+
`;

0 commit comments

Comments
 (0)