diff --git a/src/Calendar.js b/src/Calendar.js index 3150f534..4ad9184c 100644 --- a/src/Calendar.js +++ b/src/Calendar.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useCallback, useState } from 'react' import { bool, func, instanceOf, object, objectOf, string } from 'prop-types' import { startOfMonth } from 'date-fns' import { isSelectable, mergeModifiers } from './utils' @@ -6,6 +6,8 @@ import useControllableState from './useControllableState' import CalendarNavigation from './CalendarNavigation' import CalendarWeekHeader from './CalendarWeekHeader' import CalendarGrid from './CalendarGrid' +import Popover from './Popover' +import MonthPicker from './MonthPicker' export default function Calendar({ locale, @@ -18,15 +20,20 @@ export default function Calendar({ onDayHover, onDayClick, weekdayFormat, - touchDragEnabled + touchDragEnabled, + monthModifiers, + monthModifiersClassNames }) { const [month, setMonth] = useControllableState(receivedMonth, onMonthChange, startOfMonth(new Date())) + const [show, setShow] = useState(true) const modifiers = mergeModifiers( { disabled: date => !isSelectable(date, { minimumDate, maximumDate }) }, receivedModifiers ) + const handleToggle = useCallback(() => setShow(state => !state), []) + return (
- + + + + {}, onClick: () => {} } diff --git a/src/CalendarMonth.js b/src/CalendarMonth.js new file mode 100644 index 00000000..00e26932 --- /dev/null +++ b/src/CalendarMonth.js @@ -0,0 +1,84 @@ +import React from 'react' +import { bool, instanceOf, func, number, object, objectOf, string } from 'prop-types' +import { getMonth, format, getYear } from 'date-fns' +import classNames from 'classnames' + +const defaultModifiersClassNames = { + today: '-today', + outside: '-outside', + wide: '-wide', + disabled: '-disabled', + selected: '-selected', + selectedStart: '-selected-start', + selectedMiddle: '-selected-middle', + selectedEnd: '-selected-end' +} + +const isSameMonth = (date, actualDate) => { + return (getMonth(date) === getMonth(actualDate) && getYear(date) === getYear(actualDate)) +} + +export default function CalendarMonth({ + date, + height, + locale, + modifiers: receivedModifiers, + modifiersClassNames: receivedModifiersClassNames, + onClick, + onHover, + showGrid, + actualDate +}) { + const monthClassNames = {} + const modifiers = { today: isSameMonth(date, actualDate), ...receivedModifiers } + const modifiersClassNames = { ...defaultModifiersClassNames, ...receivedModifiersClassNames } + + Object.keys(modifiers).forEach(name => { + monthClassNames[modifiersClassNames[name]] = modifiers[name] + }) + + const handleClick = event => { + onClick(date) + showGrid() + event.preventDefault() + } + + const handleMouseEnter = () => { + onHover(date) + } + + const handleMouseLeave = () => { + onHover(null) + } + + return ( + + {format(date, 'LLLL', { locale })} + + ) +} + +CalendarMonth.propTypes = { + date: instanceOf(Date).isRequired, + height: number.isRequired, + locale: object.isRequired, + modifiers: objectOf(bool), + modifiersClassNames: objectOf(string), + onHover: func, + onClick: func, + showGrid: func, + actualDate: instanceOf(Date) +} + +CalendarMonth.defaultProps = { + modifiers: {}, + onHover: () => { }, + onClick: () => { } +} diff --git a/src/CalendarNavigation.js b/src/CalendarNavigation.js index c077641c..0617384c 100644 --- a/src/CalendarNavigation.js +++ b/src/CalendarNavigation.js @@ -1,9 +1,9 @@ import React from 'react' -import { func, instanceOf, object } from 'prop-types' +import { bool, func, instanceOf, object } from 'prop-types' import classNames from 'classnames' -import { addMonths, getYear, startOfMonth, subMonths, format, isSameMonth } from 'date-fns' +import { startOfMonth, format, isSameMonth, subMonths, addMonths } from 'date-fns' -export default function CalendarNavigation({ locale, month, minimumDate, maximumDate, onMonthChange }) { +export default function CalendarNavigation({ locale, month, minimumDate, maximumDate, onMonthChange, showMonthPicker, show }) { const handlePrevious = event => { onMonthChange(startOfMonth(subMonths(month, 1))) event.preventDefault() @@ -18,19 +18,22 @@ export default function CalendarNavigation({ locale, month, minimumDate, maximum
- - {format(month, getYear(month) === getYear(new Date()) ? 'LLLL' : 'LLLL yyyy', { locale })} + + {show + ? format(month, 'LLLL yyyy', { locale }) + : format(month, 'yyyy', { locale }) + } @@ -95,7 +99,9 @@ DatePicker.propTypes = { modifiers: objectOf(func), modifiersClassNames: objectOf(string), weekdayFormat: string, - touchDragEnabled: bool + touchDragEnabled: bool, + monthModifiers: objectOf(func), + monthModifiersClassNames: objectOf(string) } DatePicker.defaultProps = { diff --git a/src/DatePickerCalendar.js b/src/DatePickerCalendar.js index aec742ae..6c6a9252 100644 --- a/src/DatePickerCalendar.js +++ b/src/DatePickerCalendar.js @@ -16,7 +16,9 @@ export default function DatePickerCalendar({ modifiers: receivedModifiers, modifiersClassNames, weekdayFormat, - touchDragEnabled + touchDragEnabled, + monthModifiers, + monthModifiersClassNames }) { const isSelected = date => isSameDay(date, selectedDate) && isSelectable(date, { minimumDate, maximumDate }) const modifiers = mergeModifiers({ selected: isSelected, disabled: isSelected }, receivedModifiers) @@ -38,6 +40,8 @@ export default function DatePickerCalendar({ modifiersClassNames={modifiersClassNames} weekdayFormat={weekdayFormat} touchDragEnabled={touchDragEnabled} + monthModifiers={monthModifiers} + monthModifiersClassNames={monthModifiersClassNames} /> ) } @@ -53,5 +57,7 @@ DatePickerCalendar.propTypes = { modifiers: objectOf(func), modifiersClassNames: objectOf(string), weekdayFormat: string, - touchDragEnabled: bool + touchDragEnabled: bool, + monthModifiers: objectOf(func), + monthModifiersClassNames: objectOf(string) } diff --git a/src/MonthPicker.js b/src/MonthPicker.js new file mode 100644 index 00000000..040c210e --- /dev/null +++ b/src/MonthPicker.js @@ -0,0 +1,101 @@ +import React, { useEffect, useState } from 'react' +import { bool, instanceOf, func, number, object, objectOf, string } from 'prop-types' +import { lightFormat, eachMonthOfInterval, getYear } from 'date-fns' +import CalendarMonth from './CalendarMonth' +import useGrid from './useGrid' +import getMonth from 'date-fns/getMonth' +import classNames from 'classnames' +import { ORIGIN_BOTTOM, ORIGIN_TOP } from './constants' + +const computeModifiers = (modifiers, date) => { + const computedModifiers = {} + + Object.keys(modifiers).map(key => { + computedModifiers[key] = modifiers[key](date) + }) + + return computedModifiers +} + +export default function MonthPicker({ + locale, + modifiers, + actualDate, + modifiersClassNames, + onDayHover, + onClick, + transitionDuration, + touchDragEnabled, + showGrid +}) { + const grid = useGrid({ locale, month: getMonth(actualDate), onMonthChange: onClick, transitionDuration, touchDragEnabled }) + const { cellHeight, offset, containerElementRef, isWide, origin, transition } = grid + const [months, setMonths] = useState([]) + + useEffect(() => { + const allMonths = eachMonthOfInterval({ + start: new Date(getYear(actualDate), 0, 1), + end: new Date(getYear(actualDate), 11, 1) + }).map(date => { + return ( + + ) + }) + + setMonths(allMonths) + }, [actualDate, cellHeight, locale, modifiers, modifiersClassNames, onClick, onDayHover, showGrid, isWide]) + + return ( + <> +
+
+ {months} +
+
+ + ) +} + +MonthPicker.propTypes = { + locale: object.isRequired, + actualDate: instanceOf(Date).isRequired, + modifiers: objectOf(func), + modifiersClassNames: objectOf(string), + onDayHover: func, + transitionDuration: number.isRequired, + touchDragEnabled: bool, + onClick: func, + showGrid: func +} + +MonthPicker.defaultProps = { + modifiers: {}, + transitionDuration: 800, + touchDragEnabled: true +} diff --git a/src/style.scss b/src/style.scss index 89928137..9ab954a1 100644 --- a/src/style.scss +++ b/src/style.scss @@ -22,6 +22,10 @@ $nice-dates-cell-width: calc(100% / 7); &_current { flex-grow: 1; font-size: $nice-dates-font-size-big; + cursor: pointer; + &:hover { + font-weight: bolder; + } } &_previous, &_next { @@ -103,6 +107,10 @@ $nice-dates-cell-width: calc(100% / 7); transition: 300ms color; } + &.-moving .nice-dates-month { + transition: 300ms color; + } + &.-origin-bottom { top: auto; bottom: 0; @@ -119,6 +127,93 @@ $nice-dates-cell-width: calc(100% / 7); } } + &-month { + border-top: 1px solid transparent; + box-sizing: border-box; + color: $nice-dates-color-gray-dark; + cursor: pointer; + font-size: $nice-dates-font-size-big; + position: relative; + text-align: center; + margin-top: 20px; + width: calc(100% / 3); + align-items: center; + display: flex; + justify-content: center; + flex-direction: column; + transition: 150ms color; + will-change: color; + z-index: 3; + + &:before, &:after { + border-radius: 999px; + bottom: 0; + box-sizing: border-box; + content: ''; + display: block; + left: 0; + opacity: 0; + position: absolute; + right: 0; + top: 0; + } + + &:before { + background-color: $nice-dates-color-accent; + z-index: 1; + } + + &:after { + border: 2px solid $nice-dates-color-accent; + transform: scale(.95); + transition-duration: 150ms; + transition-property: transform, opacity; + z-index: 2; + } + + &:hover { + &:after { + opacity: 1; + transform: scale(1); + } + } + + &:not(.-disabled):hover { + &:after { + opacity: 1; + transform: scale(1); + } + } + + &.-today { + font-weight: bolder; + } + + &.-wide { + &:before, &:after { + left: 12.5%; + right: 12.5%; + } + } + + @media (hover: none) { + &:after { + content: none; + } + + &.-selected * { + color: #fff; + } + } + + &.-disabled { + cursor: default; + pointer-events: none; + color: $nice-dates-color-gray-light; + } + + } + &-day { border-top: 1px solid transparent; box-sizing: border-box; @@ -271,8 +366,8 @@ $nice-dates-cell-width: calc(100% / 7); &-popover { background-color: #fff; border-radius: 8px; - box-shadow: 0 1px 8px rgba(#000, .12); margin: 8px 0; + box-shadow: 0 1px 8px rgba(#000, .12); max-width: 600px; position: absolute; transform-origin: top; diff --git a/src/useGrid.js b/src/useGrid.js index cf2b7bd7..f1af1c84 100644 --- a/src/useGrid.js +++ b/src/useGrid.js @@ -107,7 +107,7 @@ export default function useGrid({ locale, month: currentMonth, onMonthChange, tr containerElement.classList.add('-transition') clearTimeout(timeoutRef.current) - if (Math.abs(differenceInCalendarMonths(currentMonth, lastCurrentMonth)) <= 3) { + if (Math.abs(differenceInCalendarMonths(currentMonth, lastCurrentMonth)) <= 12) { dispatch({ type: 'transitionToCurrentMonth', currentMonth }) timeoutRef.current = setTimeout(() => {