Skip to content

Commit

Permalink
feat(RangeDatePickerV2): new variant of component
Browse files Browse the repository at this point in the history
  • Loading branch information
marcinsawicki committed Jan 7, 2025
1 parent ae83adb commit b8099d9
Show file tree
Hide file tree
Showing 10 changed files with 433 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,40 @@ export const RangeDatePicker = ({
initialSelectedItemKey,
initialFromDate,
initialToDate,
customTempFromDate,
customTempToDate,
toMonth,
onChange,
onRangeSelect,
onSelect,
children,
}: IRangeDatePickerProps): ReactElement => {
const prevSelectedItem = useRef<string | null>(
initialSelectedItemKey || null
);
const [state, dispatch] = useRangeDatePickerState({
options,
initialSelectedItemKey,
initialFromDate,
initialToDate,
toMonth,
onChange,
options,
initialSelectedItemKey,
customTempFromDate,
customTempToDate,
children,
});
onChange,
onRangeSelect,
} as IRangeDatePickerProps);

// handle custom temp date range change (range date picker v2)
useEffect(() => {
dispatch({
type: RangeDatePickerAction.SET_CUSTOM_TEMP_RANGE,
payload: {
customTempFrom: customTempFromDate,
customTempTo: customTempToDate,
},
});
}, [customTempFromDate, customTempToDate]);

// handle initialFromDate change
useEffect(() => {
Expand Down Expand Up @@ -89,16 +106,29 @@ export const RangeDatePicker = ({
});
}, [state.from, state.to, state.selectedItem, options, onChange]);

// call onRangeSelect when valid dates selected in new date range picker v2
useEffect(() => {
const { from, to } = state;
if (!(from && to) || !onRangeSelect) {
return;
}

onRangeSelect?.({
from: from,
to: to,
});
}, [state.from, state.to, onRangeSelect]);

// handle selected option change
useEffect(() => {
const { selectedItem } = state;

if (selectedItem === prevSelectedItem.current) {
if (selectedItem === prevSelectedItem.current || !options) {
return;
}

if (!selectedItem) {
onChange(null);
onChange?.(null);

return;
}
Expand All @@ -117,7 +147,7 @@ export const RangeDatePicker = ({
{}
);

onChange(optionsHash[selectedItem]);
onChange?.(optionsHash[selectedItem]);
}, [onChange, state.selectedItem, options]);

const handleOnSelect = useCallback(() => {
Expand Down Expand Up @@ -211,7 +241,15 @@ export const RangeDatePicker = ({
}, []);

const getRangeDatePickerApi = (): IRangeDatePickerChildrenPayload => {
const { currentMonth, from, selectedItem, temporaryTo, to } = state;
const {
currentMonth,
from,
selectedItem,
temporaryTo,
to,
customTempFrom,
customTempTo,
} = state;
const selectedOption = useMemo(() => {
return getSelectedOption(selectedItem, options);
}, [options, selectedItem]);
Expand All @@ -220,6 +258,14 @@ export const RangeDatePicker = ({
return { from, to: temporaryTo };
}, [from, temporaryTo]);

const customSelectedDays = useMemo(() => {
if (!customTempFrom || !customTempTo) {
return undefined;
}

return { from: customTempFrom, to: customTempTo };
}, [customTempFrom, customTempTo]);

const disabledDays = useMemo(() => {
return toMonth ? { after: toMonth } : void 0;
}, [toMonth]);
Expand All @@ -238,7 +284,7 @@ export const RangeDatePicker = ({
month: currentMonth,
numberOfMonths: 2,
onDayClick: handleDayClick,
selected: selectedDays,
selected: customSelectedDays || selectedDays,
endMonth: toMonth,
disabled: disabledDays,
onDayMouseEnter: handleDayMouseEnter,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
$base-class: 'range-date-picker';

.#{$base-class} {
&__popover {
padding: var(--spacing-3);
max-width: 100%;
}

&__trigger {
display: flex;
transition: border-color var(--transition-duration-fast-2) ease-in-out;
border: 1px solid var(--border-basic-primary);
border-radius: var(--radius-3);
background-color: var(--surface-primary-default);
padding: var(--spacing-2);

&:hover {
border-color: var(--border-basic-hover);
cursor: pointer;
}

&__label {
flex: 1;
margin: 0;
}

&__right-node {
display: flex;
align-items: center;
margin-left: var(--spacing-2);
}

&--active,
&--active:hover {
border-color: var(--action-primary-default);
}
}

&__date-picker {
display: flex;
flex-direction: row;

&__list {
margin: 0 var(--spacing-4) 0 0;
padding: 0;
width: 160px;
list-style: none;

&__item {
display: flex;
position: relative;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-05);
border: 0;
border-radius: var(--radius-3);
background: none;
padding: 7px var(--spacing-3);
width: 100%;
min-height: 36px;
color: var(--content-basic-primary);

&:hover {
background-color: var(--picker-list-option-background-hover);
cursor: pointer;
}

&:active {
background-color: var(--picker-list-option-background-active);
}

&:focus-visible {
outline: 2px solid var(--action-primary-default);
outline-offset: -2px;
}

&--selected {
background-color: var(--picker-list-option-background-active);
}

&__icon {
margin-left: var(--spacing-2);
}
}
}

&__callendar {
display: flex;
flex-direction: row;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as React from 'react';

import { Meta } from '@storybook/react';

import { RangeDatePickerV2 } from './RangeDatePickerV2';

export default {
title: 'Components/DatePicker/RangeDatePickerV2',
component: RangeDatePickerV2,
} as Meta<typeof RangeDatePickerV2>;

export const Default = (): React.ReactElement => (
<RangeDatePickerV2 onRangeSelect={() => alert('selected')} />
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { FC, useState, useCallback } from 'react';

import { Check, Calendar } from '@livechat/design-system-icons';
import cx from 'clsx';
import { format, startOfToday } from 'date-fns';
import { DateRange } from 'react-day-picker';

import { DatePicker, RangeDatePicker } from '../../DatePicker';
import { Icon } from '../../Icon';
import { Popover } from '../../Popover';
import { Text } from '../../Typography';

import { OPTIONS } from './helpers';
import { IRangeDatePickerV2Props, RANGE_DATE_PICKER_OPTION_ID } from './types';

import styles from './RangeDatePickerV2.module.scss';

const baseClass = 'range-date-picker';

const todayDate = startOfToday();

export const RangeDatePickerV2: FC<IRangeDatePickerV2Props> = ({
selectedId,
onRangeSelect,
}) => {
const [date, setDate] = useState<DateRange | undefined>();
const [isVisible, setIsVisible] = useState(false);
const [currentSelectedId, setCurrentSelectedId] = useState<
RANGE_DATE_PICKER_OPTION_ID | undefined
>(selectedId);

const handleOnOptionMouseEnter = useCallback(({ from, to }: DateRange) => {
setDate({ from, to });
}, []);

const handleOnOptionClick = useCallback(
({ from, to }: DateRange, id: RANGE_DATE_PICKER_OPTION_ID) => {
onRangeSelect({ from, to });
setIsVisible(false);

if (!selectedId) {
setCurrentSelectedId(id);
}
},
[]
);

const handleOnRangeSelect = useCallback((range: DateRange | null) => {
if (!range) {
return;
}

setDate(range);
onRangeSelect(range);
setIsVisible(false);
}, []);

const getTriggerLavel = useCallback(() => {
if (date?.from && date?.to) {
return `${format(date.from, 'dd-MM-yy')} - ${format(
date.to,
'dd-MM-yy'
)}`;
}

return 'DD-MM-YYYY - DD-MM-YYYY';
}, [date]);

return (
<div className={styles[baseClass]}>
<Popover
isVisible={isVisible}
onOpen={() => setIsVisible(true)}
onClose={() => setIsVisible(false)}
className={styles[`${baseClass}__popover`]}
placement="bottom-start"
triggerRenderer={() => (
<div
className={cx(styles[`${baseClass}__trigger`], {
[styles[`${baseClass}__trigger--active`]]: isVisible,
})}
>
<Text className={styles[`${baseClass}__trigger__label`]}>
{getTriggerLavel()}
</Text>
<div className={styles[`${baseClass}__trigger__right-node`]}>
<Icon source={Calendar} />
</div>
</div>
)}
>
<div className={styles[`${baseClass}__date-picker`]}>
<ul role="menu" className={styles[`${baseClass}__date-picker__list`]}>
{OPTIONS.map((option) => (
<li role="none" key={option.id}>
<button
role="menuitem"
className={cx(
styles[`${baseClass}__date-picker__list__item`],
{
[styles[
`${baseClass}__date-picker__list__item--selected`
]]: currentSelectedId === option.id,
}
)}
onClick={() => handleOnOptionClick(option.value, option.id)}
onMouseEnter={() => handleOnOptionMouseEnter(option.value)}
onMouseLeave={() => setDate(undefined)}
>
{option.label}
{currentSelectedId === option.id && (
<div
// data-testid={`${option.id}-selected-icon`}
className={
styles[`${baseClass}__date-picker__list__item__icon`]
}
>
<Icon source={Check} kind="action-primary" />
</div>
)}
</button>
</li>
))}
</ul>
<div className={styles[`${baseClass}__date-picker__callendar`]}>
<RangeDatePicker
onRangeSelect={handleOnRangeSelect}
customTempFromDate={date?.from}
customTempToDate={date?.to}
initialToDate={todayDate}
>
{({ datepicker }) => <DatePicker {...datepicker} />}
</RangeDatePicker>
</div>
</div>
</Popover>
</div>
);
};
Loading

0 comments on commit b8099d9

Please sign in to comment.