From d158545ec96241ec4092c6974b687a506b3e7121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulius=20Puod=C5=BEi=C5=ABnas?= Date: Tue, 26 Oct 2021 17:30:21 +0300 Subject: [PATCH] Added a "week" date picker type, when clicking on any date it auto selects all dates which are in the same week as the date depending on the firstDayOfWeek prop. --- src/components/calendar/calendar.tsx | 6 +- src/index.tsx | 20 +++-- src/locales/en-US.json | 1 + src/pickers/index.tsx | 3 +- src/pickers/utils.ts | 27 ++++++ src/pickers/week.tsx | 122 +++++++++++++++++++++++++++ src/types/index.ts | 7 +- stories/data.ts | 2 +- stories/index.stories.tsx | 21 +++++ 9 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 src/pickers/week.tsx diff --git a/src/components/calendar/calendar.tsx b/src/components/calendar/calendar.tsx index 073f7251..b93359c9 100644 --- a/src/components/calendar/calendar.tsx +++ b/src/components/calendar/calendar.tsx @@ -22,6 +22,8 @@ interface CalendarProps extends RenderProps { previousMonth: string; previousYear: string; showToday: SemanticDatepickerProps['showToday']; + type: SemanticDatepickerProps['type']; + thisWeekButton: string; todayButton: string; weekdays: Locale['weekdays']; } @@ -55,7 +57,9 @@ const Calendar: React.FC = ({ previousMonth, previousYear, showToday, + type, todayButton, + thisWeekButton, weekdays, pointing, }) => { @@ -210,7 +214,7 @@ const Calendar: React.FC = ({ onClick: onPressBtn, })} > - {todayButton} + {(type === 'week' && thisWeekButton) ? thisWeekButton : todayButton} )} diff --git a/src/index.tsx b/src/index.tsx index 456b2708..d3a4de54 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -13,7 +13,7 @@ import { parseRangeOnBlur, pick, } from './utils'; -import { BasicDatePicker, RangeDatePicker } from './pickers'; +import { BasicDatePicker, RangeDatePicker, WeekDatePicker } from './pickers'; import { Locale, SemanticDatepickerProps } from './types'; import Calendar from './components/calendar'; import Input from './components/input'; @@ -125,9 +125,17 @@ class SemanticDatepicker extends React.Component< return this.props.type === 'range'; } + get isWeekInput() { + return this.props.type === 'week'; + } + + get isWeekOrRangeInput() { + return this.isWeekInput || this.isRangeInput; + } + get initialState() { const { format, value, formatOptions } = this.props; - const initialSelectedDate = this.isRangeInput ? [] : null; + const initialSelectedDate = this.isWeekOrRangeInput ? [] : null; return { isVisible: false, @@ -161,7 +169,7 @@ class SemanticDatepicker extends React.Component< return date; } - return this.isRangeInput ? selectedDate[0] : selectedDate; + return this.isWeekOrRangeInput ? selectedDate[0] : selectedDate; } get locale() { @@ -188,7 +196,9 @@ class SemanticDatepicker extends React.Component< state = this.initialState; - Component: React.ElementType = this.isRangeInput + Component: React.ElementType = this.isWeekInput + ? WeekDatePicker + : this.isRangeInput ? RangeDatePicker : BasicDatePicker; @@ -343,7 +353,7 @@ class SemanticDatepicker extends React.Component< return; } - if (this.isRangeInput) { + if (this.isWeekOrRangeInput) { const parsedValue = parseRangeOnBlur(String(typedValue), format); const areDatesValid = parsedValue.every(isValid); diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 5b25e1fe..ffeae0ce 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -1,5 +1,6 @@ { "todayButton": "Today", + "thisWeekButton" : "This Week", "nextMonth": "Next month", "previousMonth": "Previous month", "nextYear": "Next year", diff --git a/src/pickers/index.tsx b/src/pickers/index.tsx index 9373b0f5..964cfeeb 100644 --- a/src/pickers/index.tsx +++ b/src/pickers/index.tsx @@ -4,5 +4,6 @@ He didn't publish the components to npm, so I copied his work */ import BasicDatePicker from './basic'; import RangeDatePicker from './range'; +import WeekDatePicker from './week'; -export { BasicDatePicker, RangeDatePicker }; +export { BasicDatePicker, RangeDatePicker, WeekDatePicker }; diff --git a/src/pickers/utils.ts b/src/pickers/utils.ts index f9bcc9b2..7128a56a 100644 --- a/src/pickers/utils.ts +++ b/src/pickers/utils.ts @@ -47,3 +47,30 @@ export function getArrowKeyHandlers(config) { export function isInRange(range, date) { return range.length === 2 && range[0] <= date && range[1] >= date; } + +/** + * Generates an array of all week dates in the same week for a given date + * @param {Date} date a given date + * @param {number} firstDayOfWeek first day of the week (e.g. 1 for Monday) + */ +export function findWeekDatesForDate( + date: Date, + firstDayOfWeek: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +) { + firstDayOfWeek = firstDayOfWeek !== undefined ? firstDayOfWeek : 0; + let weekStartDay = new Date(date.getTime()); + let dayOfWeek = date.getDay() - firstDayOfWeek; + + if (dayOfWeek < 0) { + dayOfWeek = dayOfWeek + 7; + } + weekStartDay.setDate(date.getDate() - dayOfWeek); + + let dates = [weekStartDay]; + while (dates.length !== 7) { + let nextDay = new Date(dates[dates.length - 1]); + nextDay.setDate(nextDay.getDate() + 1); + dates.push(nextDay); + } + return dates; +} diff --git a/src/pickers/week.tsx b/src/pickers/week.tsx new file mode 100644 index 00000000..088c4444 --- /dev/null +++ b/src/pickers/week.tsx @@ -0,0 +1,122 @@ +import compareAsc from 'date-fns/compareAsc'; +import isSameDay from 'date-fns/isSameDay'; +import React from 'react'; +import { WeekDataPickerProps } from '../types'; +import BaseDatePicker from './base'; +import { composeEventHandlers, isInRange, findWeekDatesForDate } from './utils'; + +type WeekDatePickerState = { + hoveredDates: Date[] | null; +}; + +class WeekDatePicker extends React.Component< + WeekDataPickerProps, + WeekDatePickerState +> { + static defaultProps = { + selected: [], + }; + + state: WeekDatePickerState = { hoveredDates: null }; + + setHoveredDates = (dates: Date[] | null) => { + this.setState((state) => + state.hoveredDates === dates ? null : { hoveredDates: dates } + ); + }; + + // Calendar level + onMouseLeave = () => { + this.setHoveredDates(null); + }; + + // Date level + onHoverFocusDate(date: Date | null) { + const { firstDayOfWeek } = this.props; + + if (date === null) return; + this.setHoveredDates(findWeekDatesForDate(date, firstDayOfWeek)); + } + + _handleOnDateSelected = ( + { selectable, date }, + event: React.SyntheticEvent + ) => { + const { onChange, firstDayOfWeek } = this.props; + + if (!selectable) { + return; + } + + let newDates = findWeekDatesForDate( + date, + firstDayOfWeek ? firstDayOfWeek : 0 + ); + + if (onChange) { + onChange(event, newDates); + } + + if (newDates.length === 2) { + this.setHoveredDates(null); + } + }; + + getEnhancedDateProps = ( + getDateProps, + dateBounds, + { onMouseEnter, onFocus, ...restProps } + ) => { + const { hoveredDates } = this.state; + const { date } = restProps.dateObj; + return getDateProps({ + ...restProps, + inRange: isInRange(dateBounds, date), + start: dateBounds[0] && isSameDay(dateBounds[0], date), + end: dateBounds[1] && isSameDay(dateBounds[1], date), + // @ts-ignore + hovered: hoveredDates && isSameDay(hoveredDates, date), + onMouseEnter: composeEventHandlers(onMouseEnter, () => { + this.onHoverFocusDate(date); + }), + onFocus: composeEventHandlers(onFocus, () => { + this.onHoverFocusDate(date); + }), + }); + }; + + getEnhancedRootProps = (getRootProps, props) => + getRootProps({ + ...props, + onMouseLeave: this.onMouseLeave, + }); + + render() { + const { children, ...rest } = this.props; + const { hoveredDates } = this.state; + + const dateBounds = hoveredDates + ? [hoveredDates[0], hoveredDates[hoveredDates.length - 1]].sort( + compareAsc + ) + : []; + + return ( + + {({ getRootProps, getDateProps, ...renderProps }) => + children({ + ...renderProps, + getRootProps: this.getEnhancedRootProps.bind(this, getRootProps), + getDateProps: this.getEnhancedDateProps.bind( + this, + getDateProps, + dateBounds + ), + }) + } + + ); + } +} + +export default WeekDatePicker; diff --git a/src/types/index.ts b/src/types/index.ts index 641648b3..98327c7b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -89,7 +89,7 @@ export type SemanticDatepickerProps = PickedDayzedProps & pointing: 'left' | 'right' | 'top left' | 'top right'; required?: boolean; showToday: boolean; - type: 'basic' | 'range'; + type: 'basic' | 'range' | 'week'; datePickerOnly: boolean; value: DayzedProps['selected'] | null; }; @@ -108,4 +108,9 @@ export interface RangeDatePickerProps extends BaseDatePickerProps { selected: Date[]; } +export interface WeekDataPickerProps extends BaseDatePickerProps { + onChange: (event: React.SyntheticEvent, dates: Date[] | null) => void; + selected: Date[]; +} + export { DayzedProps, RenderProps }; diff --git a/stories/data.ts b/stories/data.ts index 13a0ed92..cbfa237d 100644 --- a/stories/data.ts +++ b/stories/data.ts @@ -1,7 +1,7 @@ import { ALL_ICONS_IN_ALL_CONTEXTS } from 'semantic-ui-react/src/lib/SUI'; import { SemanticICONS } from 'semantic-ui-react'; -const types = ['basic', 'range']; +const types = ['basic', 'range', 'week']; const pointing = ['left', 'right', 'top left', 'top right']; const locale = [ 'bg-BG', diff --git a/stories/index.stories.tsx b/stories/index.stories.tsx index e1d2370b..f211ca88 100644 --- a/stories/index.stories.tsx +++ b/stories/index.stories.tsx @@ -140,3 +140,24 @@ export const inverted = () => { ); }; + +export const week = () => { + const firstDayOfWeek = number('First Day Of Week', 1, { max: 6, min: 0 }); + const disabledWeekend = boolean('Disabled Weekend', false); + + return ( + + date.getDay() != 0 && date.getDay() !== 6 + : undefined + } + firstDayOfWeek={firstDayOfWeek as 0 | 1 | 2 | 3 | 4 | 5 | 6} + onChange={onChange} + type="week" + /> + + ); +};