src/datepicker/styled-components.ts (584 lines of code) (raw):
/*
Copyright (c) Uber Technologies, Inc.
This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/
import { styled } from '../styles';
import getDayStateCode from './utils/day-state';
import type { SharedStyleProps, CalendarProps } from './types';
import { ORIENTATION, DENSITY, INPUT_ROLE } from './constants';
import type { StyleObject } from 'styletron-standard';
/**
* Main component container element
*/
export const StyledInputWrapper = styled<
'div',
{
$separateRangeInputs: boolean;
} & SharedStyleProps
>('div', (props) => {
const { $separateRangeInputs } = props;
return {
width: '100%',
...($separateRangeInputs ? { display: 'flex', justifyContent: 'center' } : {}),
};
});
StyledInputWrapper.displayName = 'StyledInputWrapper';
export const StyledInputLabel = styled('div', ({ $theme }) => ({
...$theme.typography.LabelMedium,
marginBottom: $theme.sizing.scale300,
}));
StyledInputLabel.displayName = 'StyledInputLabel';
export const StyledStartDate = styled('div', ({ $theme }) => ({
width: '100%',
marginRight: $theme.sizing.scale300,
}));
StyledStartDate.displayName = 'StyledStartDate';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const StyledEndDate = styled('div', ({ $theme }) => ({
width: '100%',
}));
StyledEndDate.displayName = 'StyledEndDate';
/**
* Main component container element
*/
export const StyledRoot = styled<'div', SharedStyleProps>('div', (props) => {
const {
$theme: { typography, colors, borders, sizing },
} = props;
return {
...typography.font200,
color: colors.calendarForeground,
backgroundColor: colors.calendarBackground,
textAlign: 'center',
borderTopLeftRadius: borders.radius400,
borderTopRightRadius: borders.radius400,
borderBottomRightRadius: borders.radius400,
borderBottomLeftRadius: borders.radius400,
display: 'inline-block',
paddingTop: sizing.scale500,
paddingBottom: sizing.scale500,
paddingLeft: sizing.scale500,
paddingRight: sizing.scale500,
};
});
StyledRoot.displayName = 'StyledRoot';
export const StyledMonthContainer = styled<
'div',
{
$orientation: CalendarProps<Date>['orientation'];
}
>('div', (props) => {
const { $orientation } = props;
return {
display: 'flex',
flexDirection: $orientation === ORIENTATION.vertical ? 'column' : 'row',
gap: '16px',
};
});
StyledMonthContainer.displayName = 'StyledMonthContainer';
export const StyledCalendarContainer = styled<'div', SharedStyleProps>('div', {});
StyledCalendarContainer.displayName = 'StyledCalendarContainer';
export const StyledSelectorContainer = styled<'div', SharedStyleProps>('div', ({ $theme }) => {
const textAlign = $theme.direction === 'rtl' ? 'right' : 'left';
return {
marginBottom: $theme.sizing.scale600,
paddingLeft: $theme.sizing.scale600,
paddingRight: $theme.sizing.scale600,
textAlign,
};
});
StyledSelectorContainer.displayName = 'StyledSelectorContainer';
export const StyledCalendarHeader = styled<'div', SharedStyleProps>('div', (props) => {
const {
$theme: { typography, borders, colors, sizing },
$density,
} = props;
return {
...($density === DENSITY.high ? typography.LabelMedium : typography.LabelLarge),
boxSizing: 'border-box',
color: colors.calendarHeaderForeground,
display: 'grid',
gridTemplateColumns: '1fr auto auto 1fr',
alignItems: 'center',
backgroundColor: colors.calendarHeaderBackground,
borderTopLeftRadius: borders.surfaceBorderRadius,
borderTopRightRadius: borders.surfaceBorderRadius,
borderBottomRightRadius: 0,
borderBottomLeftRadius: 0,
// account for the left/right arrow heights
minHeight:
$density === DENSITY.high ? `calc(${sizing.scale800} + ${sizing.scale0})` : sizing.scale950,
width: $density === DENSITY.high ? '100%' : '392px',
};
});
StyledCalendarHeader.displayName = 'StyledCalendarHeader';
export const StyledMonthHeader = styled<'div', SharedStyleProps>('div', (props) => {
return {
display: 'flex',
justifyContent: 'space-around',
color: props.$theme.colors.calendarHeaderForeground,
backgroundColor: props.$theme.colors.calendarHeaderBackground,
whiteSpace: 'nowrap',
width: '100%',
};
});
StyledMonthHeader.displayName = 'StyledMonthHeader';
export const StyledMonthYearSelectButton = styled<'button', SharedStyleProps>('button', (props) => {
const {
$theme: { typography, colors, sizing },
$isFocusVisible,
$density,
} = props;
return {
...($density === DENSITY.high ? typography.LabelMedium : typography.LabelLarge),
alignItems: 'center',
backgroundColor: 'transparent',
borderLeftWidth: 0,
borderRightWidth: 0,
borderTopWidth: 0,
borderBottomWidth: 0,
color: colors.calendarHeaderForeground,
cursor: 'pointer',
display: 'flex',
height: $density === DENSITY.high ? '48px' : '56px',
paddingTop: $density === DENSITY.high ? sizing.scale400 : sizing.scale550,
paddingBottom: $density === DENSITY.high ? sizing.scale400 : sizing.scale550,
paddingLeft: $density === DENSITY.high ? sizing.scale500 : sizing.scale600,
paddingRight: $density === DENSITY.high ? sizing.scale500 : sizing.scale600,
outline: 'none',
':focus': {
boxShadow: $isFocusVisible ? `0 0 0 3px ${colors.borderAccent}` : 'none',
},
};
});
StyledMonthYearSelectButton.displayName = 'StyledMonthYearSelectButton';
export const StyledMonthYearSelectIconContainer = styled('span', (props) => {
const marginDirection: string = props.$theme.direction === 'rtl' ? 'marginRight' : 'marginLeft';
return {
alignItems: 'center',
display: 'flex',
[marginDirection]: props.$theme.sizing.scale500,
};
});
StyledMonthYearSelectIconContainer.displayName = 'StyledMonthYearSelectIconContainer';
// @ts-ignore
function getArrowBtnStyle({
$theme,
$disabled,
$isFocusVisible,
$density,
$isTrailing,
}): StyleObject {
return {
alignItems: 'center',
boxSizing: 'border-box',
display: 'flex',
color: $disabled
? $theme.colors.calendarHeaderForegroundDisabled
: $theme.colors.calendarHeaderForeground,
cursor: $disabled ? 'default' : 'pointer',
backgroundColor: 'transparent',
borderLeftWidth: 0,
borderRightWidth: 0,
borderTopWidth: 0,
borderBottomWidth: 0,
height: '48px',
justifyContent: 'center',
justifySelf: $isTrailing ? 'end' : 'start',
paddingTop: $density === DENSITY.high ? '6px' : 0,
paddingBottom: $density === DENSITY.high ? '6px' : 0,
paddingLeft: $density === DENSITY.high ? '6px' : 0,
paddingRight: $density === DENSITY.high ? '6px' : 0,
marginBottom: 0,
marginTop: 0,
outline: 'none',
':focus': $disabled
? {}
: {
boxShadow: $isFocusVisible ? `0 0 0 3px ${$theme.colors.borderAccent}` : 'none',
},
width: '48px',
};
}
export const StyledPrevButton = styled<'button', SharedStyleProps & { $isTrailing: boolean }>(
'button',
getArrowBtnStyle
);
StyledPrevButton.displayName = 'StyledPrevButton';
export const StyledNextButton = styled<'button', SharedStyleProps & { $isTrailing: boolean }>(
'button',
getArrowBtnStyle
);
StyledNextButton.displayName = 'StyledNextButton';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const StyledMonth = styled<'div', SharedStyleProps>('div', (props: SharedStyleProps) => {
return {
display: 'inline-block',
width: '100%',
};
});
StyledMonth.displayName = 'StyledMonth';
export const StyledWeek = styled<'div', SharedStyleProps>('div', (props) => {
const {
$theme: { sizing },
} = props;
return {
whiteSpace: 'nowrap',
display: 'flex',
marginBottom: sizing.scale0,
justifyContent: 'space-around',
width: '100%',
};
});
StyledWeek.displayName = 'StyledWeek';
// @ts-ignore
function generateDayStyles(defaultCode: string, defaultStyle) {
const codeForSM = defaultCode.substr(0, 12) + '1' + defaultCode.substr(12 + 1);
const codeForEM = defaultCode.substr(0, 13) + '1' + defaultCode.substr(13 + 1);
return {
[defaultCode]: defaultStyle,
[codeForSM]: defaultStyle,
[codeForEM]: defaultStyle,
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getDayStyles(code, { colors }): any {
const undefinedDayStyle = {
// @ts-ignore
':before': { content: null },
// @ts-ignore
':after': { content: null },
};
let defaultDayStyle = undefinedDayStyle;
const disabledDateStyle = {
color: colors.calendarForegroundDisabled,
// @ts-ignore
':before': { content: null },
// @ts-ignore
':after': { content: null },
};
const outsideMonthDateStyle = {
color: colors.calendarForegroundDisabled,
':before': {
borderTopStyle: 'none',
borderBottomStyle: 'none',
borderLeftStyle: 'none',
borderRightStyle: 'none',
backgroundColor: 'transparent',
},
':after': {
borderTopLeftRadius: '0%',
borderTopRightRadius: '0%',
borderBottomLeftRadius: '0%',
borderBottomRightRadius: '0%',
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
borderRightColor: 'transparent',
borderLeftColor: 'transparent',
},
};
const highlightedStyle = {
// @ts-ignore
':before': { content: null },
};
const CODE_DISABLED_INDEX = 1;
if (code && code[CODE_DISABLED_INDEX] === '1') {
defaultDayStyle = disabledDateStyle;
}
// See the ./utils/day-state.js file for the description of all available states
// rdhsrSsDeDpSrHpHrRrLsMeMoM
// '000000000000000'
const dayStateStyle = Object.assign(
{},
// highlighted date
generateDayStyles('001000000000000', {
color: colors.calendarDayForegroundPseudoSelected,
}),
// selected date
generateDayStyles('000100000000000', {
color: colors.calendarDayForegroundSelected,
}),
// selected highlighted date
generateDayStyles('001100000000000', {
color: colors.calendarDayForegroundSelectedHighlighted,
}),
// disabled date
{
'010000000000000': {
color: colors.calendarForegroundDisabled,
':after': { content: null },
},
},
// disabled highlighted date
{
'011000000000000': {
color: colors.calendarForegroundDisabled,
':after': { content: null },
},
},
// date outside of the currently displayed month (when peekNextMonth is true)
generateDayStyles('000000000000001', outsideMonthDateStyle),
// Range Datepicker
// range: highlighted date outside of a selected range
generateDayStyles('101000000000000', highlightedStyle),
generateDayStyles('101010000000000', highlightedStyle),
// range: selected date
generateDayStyles('100100000000000', {
color: colors.calendarDayForegroundSelected,
}),
// range: selected highlighted date
// when single date selected in a range
generateDayStyles('101100000000000', {
color: colors.calendarDayForegroundSelectedHighlighted,
':before': { content: null },
}),
// range: selected start and end dates are the same
generateDayStyles('100111100000000', {
color: colors.calendarDayForegroundSelected,
':before': { content: null },
}),
generateDayStyles('101111100000000', {
color: colors.calendarDayForegroundSelectedHighlighted,
':before': { content: null },
}),
// range: selected start date
generateDayStyles('100111000000000', {
color: colors.calendarDayForegroundSelected,
}),
// range: selected end date
generateDayStyles('100110100000000', {
color: colors.calendarDayForegroundSelected,
':before': { left: null, right: '50%' },
}),
// range: first selected date while a range is highlighted but no second date selected yet
// highlighted range on the right from the selected
generateDayStyles('100100001010000', {
color: colors.calendarDayForegroundSelected,
}),
// highlighted range on the left from the selected
generateDayStyles('100100001001000', {
color: colors.calendarDayForegroundSelected,
':before': { left: null, right: '50%' },
}),
// range: second date in a range that is highlighted but not selected
generateDayStyles('101000001010000', {
':before': { left: null, right: '50%' },
}),
{ '101000001001000': {} },
{ '101000001001100': {} },
{ '101000001001010': {} },
// range: pseudo-selected date
generateDayStyles('100010010000000', {
color: colors.calendarDayForegroundPseudoSelected,
':before': { left: '0', width: '100%' },
':after': { content: null },
}),
// range: pseudo-highlighted date (in a range where only one date is
// selected and second date is highlighted)
{
'101000001100000': {
color: colors.calendarDayForegroundPseudoSelected,
':before': {
left: '0',
width: '100%',
},
':after': {
content: null,
},
},
},
generateDayStyles('100000001100000', {
color: colors.calendarDayForegroundPseudoSelected,
':before': {
left: '0',
width: '100%',
},
':after': {
content: null,
},
}),
// highlighted start date in a range
generateDayStyles('101111000000000', {
color: colors.calendarDayForegroundSelectedHighlighted,
}),
// highlighted end date in a range
generateDayStyles('101110100000000', {
color: colors.calendarDayForegroundSelectedHighlighted,
':before': { left: null, right: '50%' },
}),
// range: pseudo-selected date
generateDayStyles('101010010000000', {
color: colors.calendarDayForegroundPseudoSelectedHighlighted,
':before': { left: '0', width: '100%' },
}),
// Range is true Date outside current month (when peekNextMonth is true)
generateDayStyles('100000000000001', outsideMonthDateStyle),
// peekNextMonth is true, date is outside month, start date is selected and range is highlighted is on right
generateDayStyles('100000001010001', outsideMonthDateStyle),
// peekNextMonth is true, date is outside month, start date is selected and range is highlighted is on left
generateDayStyles('100000001001001', outsideMonthDateStyle),
// peekNextMonth is true, date is outside month, range is selected
generateDayStyles('100010000000001', outsideMonthDateStyle)
);
return dayStateStyle[code] || defaultDayStyle;
}
export const StyledDay = styled<'div', SharedStyleProps>('div', (props) => {
const {
$disabled,
$isFocusVisible,
$isHighlighted,
$peekNextMonth,
$pseudoSelected,
$range,
$selected,
$outsideMonth,
$outsideMonthWithinRange,
$hasDateLabel,
$density,
$hasLockedBehavior,
$selectedInput,
$value,
$theme: { colors, typography, sizing },
} = props;
const code = getDayStateCode(props);
let height;
if ($hasDateLabel) {
if ($density === DENSITY.high) {
height = '64px';
} else {
height = '72px';
}
} else {
if ($density === DENSITY.high) {
height = '44px';
} else {
height = '52px';
}
}
let circleHeight;
if ($hasDateLabel) {
if ($density === DENSITY.high) {
circleHeight = '60px';
} else {
circleHeight = '70px';
}
} else {
if ($density === DENSITY.high) {
circleHeight = '40px';
} else {
circleHeight = '48px';
}
}
const [startDate, endDate] = Array.isArray($value) ? $value : [$value, null];
const oppositeInputIsPopulated =
$selectedInput === INPUT_ROLE.startDate
? endDate !== null && typeof endDate !== 'undefined'
: startDate !== null && typeof startDate !== 'undefined';
const shouldHighlightRange = $range && !($hasLockedBehavior && !oppositeInputIsPopulated);
return {
...($density === DENSITY.high ? typography.ParagraphSmall : typography.ParagraphMedium),
boxSizing: 'border-box',
position: 'relative',
cursor: $disabled || (!$peekNextMonth && $outsideMonth) ? 'default' : 'pointer',
color: colors.calendarForeground,
display: 'inline-block',
width: $density === DENSITY.high ? '52px' : '56px',
height: height,
// setting lineHeight equal to the contents height to vertically center the text
lineHeight: $density === DENSITY.high ? sizing.scale850 : sizing.scale950,
textAlign: 'center',
paddingTop: sizing.scale300,
paddingBottom: sizing.scale300,
paddingLeft: sizing.scale300,
paddingRight: sizing.scale300,
marginTop: 0,
marginBottom: 0,
marginLeft: 0,
marginRight: 0,
outline: 'none',
backgroundColor: 'transparent',
// `transform` creates a stacking context so
// a z-index used on its' children doesn't
// interfere with anything outside the component
transform: 'scale(1)',
...getDayStyles(code, props.$theme),
// :after pseudo element defines the selected
// or highlighted day's circle styles
':after': {
zIndex: -1,
content: '""',
boxSizing: 'border-box',
display: 'inline-block',
boxShadow:
$isFocusVisible && (!$outsideMonth || $peekNextMonth)
? `0 0 0 3px ${colors.borderAccent}`
: 'none',
backgroundColor: $selected
? colors.calendarDayBackgroundSelectedHighlighted
: $pseudoSelected && $isHighlighted
? colors.calendarDayBackgroundPseudoSelectedHighlighted
: colors.calendarBackground,
height: circleHeight,
width: $density === DENSITY.high ? '40px' : '48px',
position: 'absolute',
top: '2px',
left: $density === DENSITY.high ? '6px' : '4px',
paddingTop: sizing.scale200,
paddingBottom: sizing.scale200,
borderLeftWidth: '2px',
borderRightWidth: '2px',
borderTopWidth: '2px',
borderBottomWidth: '2px',
borderLeftStyle: 'solid',
borderRightStyle: 'solid',
borderTopStyle: 'solid',
borderBottomStyle: 'solid',
borderTopColor: colors.borderSelected,
borderBottomColor: colors.borderSelected,
borderRightColor: colors.borderSelected,
borderLeftColor: colors.borderSelected,
borderTopLeftRadius: $hasDateLabel ? sizing.scale850 : '100%',
borderTopRightRadius: $hasDateLabel ? sizing.scale850 : '100%',
borderBottomLeftRadius: $hasDateLabel ? sizing.scale850 : '100%',
borderBottomRightRadius: $hasDateLabel ? sizing.scale850 : '100%',
...(getDayStyles(code, props.$theme)[':after'] || {}),
...($outsideMonthWithinRange ? { content: null } : {}),
},
...(shouldHighlightRange
? {
// :before pseudo element defines a grey background style that extends
// the selected/highlighted day's circle and spans through a range
':before': {
zIndex: -1,
content: '""',
boxSizing: 'border-box',
display: 'inline-block',
backgroundColor: colors.backgroundTertiary,
position: 'absolute',
height: circleHeight,
width: '50%',
top: '2px',
left: '50%',
borderTopWidth: '2px',
borderBottomWidth: '2px',
borderLeftWidth: '0',
borderRightWidth: '0',
borderTopStyle: 'solid',
borderBottomStyle: 'solid',
borderLeftStyle: 'solid',
borderRightStyle: 'solid',
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
...(getDayStyles(code, props.$theme)[':before'] || {}),
...($outsideMonthWithinRange
? {
backgroundColor: colors.backgroundTertiary,
left: '0',
width: '100%',
content: '""',
}
: {}),
},
}
: // a hack to make flow happy, otherwise it complains about complexity
// eslint-disable-next-line @typescript-eslint/no-explicit-any
({} as any)),
};
});
StyledDay.displayName = 'StyledDay';
export const StyledDayLabel = styled<'div', SharedStyleProps>('div', (props) => {
const {
$theme: { typography, colors },
$selected,
} = props;
return {
...typography.ParagraphXSmall,
color: $selected ? colors.contentInverseTertiary : colors.contentTertiary,
};
});
StyledDayLabel.displayName = 'StyledDayLabel';
export const StyledWeekdayHeader = styled<'div', SharedStyleProps>('div', (props) => {
const {
$theme: { typography, colors, sizing },
$density,
} = props;
return {
...typography.LabelMedium,
color: colors.contentTertiary,
boxSizing: 'border-box',
position: 'relative',
cursor: 'default',
display: 'inline-block',
width: $density === DENSITY.high ? '44px' : '52px',
height: $density === DENSITY.high ? '44px' : '52px',
textAlign: 'center',
// setting lineHeight equal to the contents height to vertically center the text
lineHeight: sizing.scale900,
paddingTop: sizing.scale300,
paddingBottom: sizing.scale300,
paddingLeft: sizing.scale200,
paddingRight: sizing.scale200,
marginTop: 0,
marginBottom: 0,
marginLeft: 0,
marginRight: 0,
backgroundColor: 'transparent',
};
});
StyledWeekdayHeader.displayName = 'StyledWeekdayHeader';
export const StyledInputContainer = styled<
'div',
{
$separateRangeInputs: boolean;
} & SharedStyleProps
>('div', (props) => {
const { $theme, $separateRangeInputs } = props;
return {
width: '100%',
...($separateRangeInputs ? { display: 'flex', justifyContent: 'center' } : {}),
backgroundColor: $theme.colors.backgroundPrimary,
outline: 'none',
paddingInlineStart: 'unset',
};
});
StyledInputContainer.displayName = 'StyledInputContainer';