diff --git a/src/components/Datepicker/CalendarComponents/Calendar.js b/src/components/Datepicker/CalendarComponents/Calendar.js new file mode 100644 index 0000000..3b94e55 --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/Calendar.js @@ -0,0 +1,245 @@ +import React, { useState, useRef, useEffect } from "react"; + +import { + getDateAccordingToMonth, + shallowClone, + getValueType, +} from "./shared/generalUtils"; +import { + TYPE_SINGLE_DATE, + TYPE_RANGE, + TYPE_MUTLI_DATE, +} from "./shared/constants"; +import { useLocaleUtils, useLocaleLanguage } from "./shared/hooks"; + +import { Header, MonthSelector, YearSelector, DaysList } from "./components"; + +const Calendar = ({ + value, + onChange, + onDisabledDayError, + calendarClassName, + calendarTodayClassName, + calendarSelectedDayClassName, + calendarRangeStartClassName, + calendarRangeBetweenClassName, + calendarRangeEndClassName, + disabledDays, + colorPrimary, + colorPrimaryLight, + slideAnimationDuration, + minimumDate, + maximumDate, + selectorStartingYear, + selectorEndingYear, + locale, + shouldHighlightWeekends, + renderFooter, + customDaysClassName, + events, + renderEvents, + type, +}) => { + const calendarElement = useRef(null); + const [mainState, setMainState] = useState({ + activeDate: null, + monthChangeDirection: "", + isMonthSelectorOpen: false, + isYearSelectorOpen: false, + }); + const [eventInFooter, setEventInFooter] = useState(null); + useEffect(() => { + const handleKeyUp = ({ key }) => { + /* istanbul ignore else */ + if (key === "Tab") + calendarElement.current.classList.remove("-noFocusOutline"); + }; + calendarElement.current.addEventListener("keyup", handleKeyUp, false); + return () => { + // calendarElement.current.removeEventListener('keyup', handleKeyUp, false); + }; + }); + + const { getToday } = useLocaleUtils(locale); + const { weekDays: weekDaysList, isRtl } = useLocaleLanguage(locale); + const today = getToday(); + + const createStateToggler = (property) => () => { + setMainState({ ...mainState, [property]: !mainState[property] }); + }; + + const toggleMonthSelector = createStateToggler("isMonthSelectorOpen"); + const toggleYearSelector = createStateToggler("isYearSelectorOpen"); + + const getComputedActiveDate = () => { + const valueType = getValueType(value); + if (valueType === TYPE_MUTLI_DATE && value.length) + return shallowClone(value[0]); + if (valueType === TYPE_SINGLE_DATE && value) return shallowClone(value); + if (valueType === TYPE_RANGE && value.from) return shallowClone(value.from); + return shallowClone(today); + }; + + const activeDate = mainState.activeDate + ? shallowClone(mainState.activeDate) + : getComputedActiveDate(); + + const weekdays = weekDaysList.map((weekDay) => ( + + {type == "dashboard" ? weekDay.name : weekDay.short} + + )); + + const handleMonthChange = (direction) => { + setMainState({ + ...mainState, + monthChangeDirection: direction, + }); + }; + + const updateDate = () => { + setMainState({ + ...mainState, + activeDate: getDateAccordingToMonth( + activeDate, + mainState.monthChangeDirection + ), + monthChangeDirection: "", + }); + }; + + const selectMonth = (newMonthNumber) => { + setMainState({ + ...mainState, + activeDate: { ...activeDate, month: newMonthNumber }, + isMonthSelectorOpen: false, + }); + }; + + const selectYear = (year) => { + setMainState({ + ...mainState, + activeDate: { ...activeDate, year }, + isYearSelectorOpen: false, + }); + }; + + return ( +
+
+ + + + + +
{weekdays}
+ + setEventInFooter(event)} + type={type} + /> +
+
+ {type == "dashboard" ? ( + <> +
+ رویداد ها:{" "} +
+

+ {eventInFooter + ? eventInFooter + : "رویدادی در این روز وجود ندارد!"} +

+ + ) : ( + eventInFooter + )} +
+ {renderFooter()} +
+
+ ); +}; + +Calendar.defaultProps = { + minimumDate: null, + maximumDate: null, + colorPrimary: "#0eca2d", + colorPrimaryLight: "#cff4d5", + slideAnimationDuration: "0.4s", + calendarClassName: "", + locale: "en", + value: null, + renderFooter: () => null, + renderEvents: () => null, + customDaysClassName: [], + events: [], +}; + +export default Calendar; diff --git a/src/components/Datepicker/CalendarComponents/DatePicker.css b/src/components/Datepicker/CalendarComponents/DatePicker.css new file mode 100644 index 0000000..c7010fb --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/DatePicker.css @@ -0,0 +1,672 @@ +.DatePicker { + position: relative; + display: inline-block; + z-index: 100; +} + +.DatePicker__input { + background: #fff; + border: 1px solid #ddd; + padding: 0.4em 0.8em; + font-family: inherit; + text-align: center; + font-size: 12px; +} + +.DatePicker__input.-rtl { + direction: rtl; +} + +.DatePicker__input::placeholder { + color: #979797; +} + +.DatePicker__calendarContainer.-top + .DatePicker__calendarArrow { + top: auto; + bottom: calc(100% + 10px); + transform: translateY(-2.5rem) rotate(180deg); + animation: fadeArrowFlipped 0.3s forwards; +} + +.DatePicker__calendarContainer { + position: absolute; + top: calc(100% + 20px); + left: 50%; + transform: translateX(-50%); +} + +.DatePicker__calendarContainer.-top { + top: auto; + bottom: calc(100% + 20px); +} + +.Calendar, +.Calendar * { + margin: 0; + padding: 0; + box-sizing: border-box; + direction: ltr; +} + +.Calendar, +.Calendar.-rtl * { + direction: rtl; +} + +.DatePicker__calendarArrow { + position: absolute; + width: 0; + height: 0; + top: calc(100% + 10px); + left: 0; + right: 0; + margin: 0 auto; + border-style: solid; + z-index: 10; + border-width: 0 10px 10px 10px; + border-color: transparent transparent #fff transparent; +} + +.Calendar { + --cl-color-black: #444444; + --cl-color-disabled: #d4d4d4; + --cl-color-error: #ff2929; + font-size: 10px; + background: #fff; + box-shadow: 0 1em 4em rgba(0, 0, 0, 0.07); + border-radius: 1em; + position: relative; + user-select: none; + padding-top: 1.2em; + display: flex; + flex-direction: column; + width: 33em; + z-index: 10; + max-width: 90vw; + min-height: 36.7em; +} + +.DatePicker .Calendar, +.DatePicker__calendarArrow { + transform: translateY(2.5em); + opacity: 0; + animation: fadeCalendar 0.3s forwards; +} + +.DatePicker__calendarContainer.-top .Calendar { + transform: translateY(-2.5em); +} + +.Calendar.-noFocusOutline *:focus { + outline: none !important; +} + +.Calendar > :not(.Calendar__footer) button { + font-family: inherit; + background: transparent; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + outline: none; +} + +.Calendar__header { + display: flex; + color: var(--cl-color-black); + padding: 2em 2.9em; + align-items: center; + overflow: hidden; +} + +.Calendar__monthArrowWrapper { + line-height: 0; + font-size: 1em; + padding: 3px; + position: relative; + border: none; + z-index: 1; + opacity: 1; + transition: 0.2s; +} + +.Calendar__monthArrowWrapper:focus { + outline: 1px dashed rgba(0, 0, 0, 0.4); + outline-offset: 2px; +} + +.Calendar__monthArrowWrapper:disabled, +.Calendar__monthArrowWrapper.-hidden { + opacity: 0; + pointer-events: none; +} + +.Calendar__monthArrowWrapper.-left { + transform: rotate(90deg); +} +.Calendar.-rtl .Calendar__monthArrowWrapper.-left { + transform: rotate(-90deg); +} + +.Calendar__monthArrowWrapper.-right { + transform: rotate(-90deg); +} +.Calendar.-rtl .Calendar__monthArrowWrapper.-right { + transform: rotate(90deg); +} + +.Calendar__monthArrowWrapper:active .Calendar__monthArrow { + transform: scale(0.7); +} + +.Calendar__monthArrow { + border-radius: 50%; + transition: var(--animation-duration) transform; + pointer-events: none; + background-repeat: no-repeat; + display: block; + width: 1.7em; + height: 1.7em; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cg class='nc-icon-wrapper' fill='%23000000'%3E%3Cdefs stroke='none'%3E%3C/defs%3E%3Cpath class='cls-1' d='M12 23.25V.75' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5px'%3E%3C/path%3E%3Cpath class='cls-2' d='M22.5 11.25L12 .75 1.5 11.25' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5px' fill-rule='evenodd'%3E%3C/path%3E%3C/g%3E%3C/svg%3E"); + background-size: 100% 100%; +} + +.Calendar__monthYearContainer { + flex: 1; + position: relative; +} + +.Calendar__monthYear { + font-size: 1.6em; + font-weight: 500; + display: flex; + align-items: center; + position: absolute; + top: 0; + bottom: 0; + left: 50%; + will-change: transform, opacity; + backface-visibility: hidden; + transform: translateZ(0); + transition: var(--animation-duration); + line-height: 1; +} + +.Calendar__monthYear.-hiddenNext { + opacity: 0; + transform: translateX(50%); +} + +.Calendar.-rtl .Calendar__monthYear.-hiddenNext { + transform: translateX(-150%); +} + +.Calendar__monthYear.-hiddenPrevious { + opacity: 0; + transform: translateX(-150%); +} + +.Calendar.-rtl .Calendar__monthYear.-hiddenPrevious { + transform: translateX(50%); +} + +.Calendar__monthYear.-shown { + opacity: 1; + margin-top: auto; + margin-bottom: auto; + transform: translateX(-50%); +} + +.Calendar__monthYear.-shownAnimated { + animation: var(--animation-duration) fadeTextToCenter forwards; +} + +.Calendar__monthYear > * { + padding: 0.2em 0.5em; + border: 1px solid transparent; + transition: var(--animation-duration); + font-size: 1.05em; + display: flex; + justify-content: center; + align-items: center; + transform: translateX(0) scale(0.95); + will-change: transform; + border-radius: 5px; +} + +.Calendar__monthYear:not(.-shown) > *, +.Calendar__monthYear > *.-hidden { + cursor: default; + pointer-events: none; +} + +.Calendar__monthText { + margin-left: -0.3em; +} +.Calendar__yearText:last-child { + margin-right: -0.3em; +} + +.Calendar__monthYear.-shown > *:hover, +.Calendar:not(.-noFocusOutline) .Calendar__monthYear.-shown > *:focus, +.Calendar__monthYear > *.-activeBackground { + background: #f5f5f5; +} + +.Calendar__monthText:hover { + transform: translateX(-0.2em) scale(0.95); +} +.Calendar.-rtl .Calendar__monthText:hover { + transform: translateX(0.2em) scale(0.95); +} + +.Calendar__yearText:hover { + transform: translateX(0.2em) scale(0.95); +} +.Calendar.-rtl .Calendar__yearText:hover { + transform: translateX(-0.2em) scale(0.95); +} + +.Calendar__monthYear .Calendar__yearText.-hidden { + transform: translateX(50%); + opacity: 0; +} + +.Calendar.-rtl .Calendar__monthYear .Calendar__yearText.-hidden { + transform: translateX(-50%); +} + +.Calendar__monthYear .Calendar__monthText.-hidden { + transform: translateX(-50%); + opacity: 0; +} + +.Calendar.-rtl .Calendar__monthYear .Calendar__monthText.-hidden { + transform: translateX(50%); +} + +.Calendar__monthYear:not(.-shown) > * { + pointer-events: none; +} + +.Calendar__monthSelectorAnimationWrapper, +.Calendar__yearSelectorAnimationWrapper { + position: absolute; + width: 100%; + height: 80%; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; +} + +.Calendar__monthSelectorWrapper { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.Calendar__monthSelector { + padding: 0 2.5em; + align-content: center; + padding-bottom: 2em; +} + +.Calendar__monthSelector, +.Calendar__yearSelector { + display: flex; + flex-wrap: wrap; + position: relative; + z-index: 2; + background-color: #fff; + transform: translateY(-150%); + will-change: transform; + transition: 0.6s; + height: 100%; +} + +.Calendar__yearSelectorWrapper { + width: 100%; + height: 100%; +} + +.Calendar__yearSelectorWrapper::after, +.Calendar__yearSelectorWrapper::before { + content: ''; + width: 100%; + height: 5em; + position: absolute; + left: 0; + opacity: 0; + transition: 0.4s; + transition-delay: 0.2s; +} + +.Calendar__yearSelectorWrapper::after { + background-image: linear-gradient(to bottom, #fff, #fff 10%, rgba(245, 245, 245, 0)); + top: -0.1em; +} + +.Calendar__yearSelectorWrapper::before { + background-image: linear-gradient(to top, #fff, #fff 10%, rgba(245, 245, 245, 0)); + bottom: 0; +} + +.Calendar__yearSelectorWrapper.-faded::after, +.Calendar__yearSelectorWrapper.-faded::before { + opacity: 1; + z-index: 3; +} + +.Calendar__yearSelector { + align-content: flex-start; + scrollbar-width: 0; + overflow: scroll; + position: relative; + width: 100%; + padding: 5em 2em; + -ms-overflow-style: none; +} + +.Calendar__yearSelector::-webkit-scrollbar { + display: none; +} + +.Calendar__yearSelectorItem { + width: 25%; + display: flex; + justify-content: center; +} + +.Calendar__yearSelectorItem:not(:nth-child(-n + 4)) { + margin-top: 1.5em; +} + +.Calendar__yearSelectorText { + border: none; + font-size: 1.4em; + min-width: 85%; + padding: 0.2em 0.5em; + border-radius: 8.5px; +} + +.Calendar__monthSelector.-open, +.Calendar__yearSelector.-open { + transform: translateY(0); +} + +.Calendar__yearSelectorText:focus, +.Calendar__monthSelectorItemText:focus { + outline: 1px dashed rgba(0, 0, 0, 0.4); + outline-offset: 2px; +} + +.Calendar__monthSelectorItem { + width: calc(100% / 3); + display: flex; + justify-content: center; +} + +.Calendar__monthSelectorItem:not(:nth-child(-n + 3)) { + margin-top: 2em; +} + +.Calendar__monthSelectorItemText { + border: none; + padding: 0.4em 0.4em; + border-radius: 8.5px; + font-size: 1.3em; + min-width: 70%; + transition: 0.3s; +} + +.Calendar__monthSelectorItem:not(.-active) .Calendar__monthSelectorItemText:not(:disabled):hover, +.Calendar__yearSelectorItem:not(.-active) .Calendar__yearSelectorText:not(:disabled):hover { + background: #f5f5f5; +} + +.Calendar__monthSelectorItemText:disabled, +.Calendar__yearSelectorText:disabled { + opacity: 0.5; + cursor: default; +} + +.Calendar__monthSelectorItem.-active .Calendar__monthSelectorItemText, +.Calendar__yearSelectorItem.-active .Calendar__yearSelectorText { + background-color: var(--cl-color-primary); + color: #fff; +} + +.Calendar__weekDays { + display: flex; + justify-content: space-between; + color: var(--cl-color-disabled); + font-size: 1.2em; + margin-bottom: 0.7em; + padding: 0 2.6em; + position: relative; +} + +.Calendar__weekDay { + display: block; + width: calc(100% / 7); + text-align: center; + text-decoration: none; +} + +.Calendar__sectionWrapper { + position: relative; + min-height: 25.8em; + overflow: hidden; +} + +.Calendar__section { + display: flex; + flex-direction: column; + padding: 0 3.2em; + position: absolute; + color: var(--cl-color-black); + top: 0; + padding-top: 0.5em; + left: 0; + width: 100%; + will-change: transform, opacity; + transform: translateZ(0); + backface-visibility: hidden; + transition: var(--animation-duration); +} + +.Calendar__section.-hiddenPrevious { + opacity: 0.5; + transform: translateX(-90%); +} + +.Calendar.-rtl .Calendar__section.-hiddenPrevious { + transform: translateX(90%); +} + +.Calendar__section.-hiddenNext { + opacity: 0.5; + transform: translateX(90%); +} + +.Calendar.-rtl .Calendar__section.-hiddenNext { + transform: translateX(-90%); +} + +.Calendar__section.-shown { + opacity: 1; + transform: translateX(0); +} + +.Calendar__section.-shownAnimated { + animation: var(--animation-duration) FadeContentToCenter forwards; +} + +.Calendar__weekRow { + display: flex; + width: 100%; +} + +.Calendar__day { + display: block; + width: calc(100% / 7); + text-align: center; + padding: calc(0.25em - 1px) 0; + font-size: 1.6em; + border-radius: 50%; + transition: 0.2s; + border: 1px solid transparent; + margin-bottom: 0.3em; + color: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.Calendar__day:focus { + outline: 1px dashed rgba(0, 0, 0, 0.4); + outline-offset: 2px; +} + +.Calendar__day.-ltr { + min-height: 2.6em; + font-size: 1.45em; +} + +.Calendar__day.-rtl { + font-size: 1.55em; + height: 2.45em; +} + +.Calendar__day:not(.-blank):not(.-selectedStart):not(.-selectedEnd):not(.-selectedBetween):not(.-selected):hover { + background: #eaeaea; + border-radius: 50%; + color: var(--cl-color-black); + border-color: transparent; +} + +.Calendar__day.-selected, +.Calendar__day.-selectedStart, +.Calendar__day.-selectedEnd { + background: var(--cl-color-primary); + color: #fff; +} + +.Calendar__day.-ltr.-selectedStart { + border-radius: 0; + border-top-left-radius: 100em; + border-bottom-left-radius: 100em; +} + +.Calendar__day.-rtl.-selectedStart { + border-radius: 0; + border-top-right-radius: 100em; + border-bottom-right-radius: 100em; +} + +.Calendar__day.-selectedBetween { + background: var(--cl-color-primary-light); + color: var(--cl-color-primary); + border-radius: 0; +} + +.Calendar__day.-ltr.-selectedEnd { + border-top-right-radius: 100em; + border-bottom-right-radius: 100em; +} + +.Calendar__day.-rtl.-selectedEnd { + border-top-left-radius: 100em; + border-bottom-left-radius: 100em; +} + +.Calendar__day.-weekend:not(.-selected):not(.-blank):not(.-selectedStart):not(.-selectedEnd):not(.-selectedBetween) { + color: var(--cl-color-error); +} + +.Calendar__day.-weekend.-today:not(.-selectedStart):not(.-selectedEnd):not(.-selectedBetween)::after { + background: var(--cl-color-error); +} + +.Calendar__day.-disabled { + color: var(--cl-color-disabled) !important; + background: transparent !important; + cursor: default !important; +} +.Calendar__day.-selected { + border-radius: 50%; +} +.Calendar__day.-today:not(.-selectedStart):not(.-selectedEnd):not(.-selectedBetween) { + font-weight: 600; + color: var(--cl-color-black); + color: #000; + position: relative; +} + +.Calendar__day.-today:not(.-selectedStart):not(.-selectedEnd):not(.-selectedBetween)::after { + content: ''; + position: absolute; + bottom: 0.2em; + display: block; + width: 0.6em; + height: 1px; + background: #000; + left: 50%; + opacity: 0.5; + transform: translateX(-50%); + transition: 0.2s; +} + +.Calendar__day.-today:hover:not(.-selectedStart):not(.-selectedEnd):not(.-selectedBetween)::after { + opacity: 0; +} + +.Calendar__day.-blank { + color: transparent; + cursor: default; + pointer-events: none; +} + +.Calendar__footer { + position: relative; + z-index: 1; +} + +@keyframes fadeCalendar { + from { + opacity: 0; + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeArrowFlipped { + from { + opacity: 0; + } + + to { + opacity: 1; + transform: translateY(0) rotate(180deg); + } +} + +@keyframes fadeTextToCenter { + to { + opacity: 1; + transform: translateX(-50%); + } +} + +@keyframes FadeContentToCenter { + to { + opacity: 1; + transform: translateX(0); + } +} diff --git a/src/components/Datepicker/CalendarComponents/DatePicker.js b/src/components/Datepicker/CalendarComponents/DatePicker.js new file mode 100644 index 0000000..6f81669 --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/DatePicker.js @@ -0,0 +1,203 @@ +import React, { useState, useEffect, useRef, useLayoutEffect } from 'react'; + +import Calendar from './Calendar'; +import DatePickerInput from './DatePickerInput'; +import { getValueType } from './shared/generalUtils'; +import { TYPE_SINGLE_DATE, TYPE_MUTLI_DATE, TYPE_RANGE } from './shared/constants'; + +const DatePicker = ({ + value, + onChange, + formatInputText, + inputPlaceholder, + inputClassName, + inputName, + renderInput, + wrapperClassName, + calendarClassName, + calendarTodayClassName, + calendarSelectedDayClassName, + calendarRangeStartClassName, + calendarRangeBetweenClassName, + calendarRangeEndClassName, + calendarPopperPosition, + disabledDays, + onDisabledDayError, + colorPrimary, + colorPrimaryLight, + slideAnimationDuration, + minimumDate, + maximumDate, + selectorStartingYear, + selectorEndingYear, + locale, + shouldHighlightWeekends, + renderFooter, + customDaysClassName, +}) => { + const calendarContainerElement = useRef(null); + const inputElement = useRef(null); + const shouldPreventToggle = useRef(false); + const [isCalendarOpen, setCalendarVisiblity] = useState(false); + + useEffect(() => { + const handleBlur = () => { + setCalendarVisiblity(false); + }; + window.addEventListener('blur', handleBlur, false); + return () => { + window.removeEventListener('blur', handleBlur, false); + }; + }, []); + + // handle input focus/blur + useEffect(() => { + const valueType = getValueType(value); + if (valueType === TYPE_MUTLI_DATE) return; // no need to close the calendar + const shouldCloseCalendar = + valueType === TYPE_SINGLE_DATE ? !isCalendarOpen : !isCalendarOpen && value.from && value.to; + if (shouldCloseCalendar) inputElement.current.blur(); + }, [value, isCalendarOpen]); + + const handleBlur = e => { + e.persist(); + if (!isCalendarOpen) return; + const isInnerElementFocused = calendarContainerElement.current.contains(e.relatedTarget); + if (shouldPreventToggle.current) { + shouldPreventToggle.current = false; + inputElement.current.focus(); + } else if (isInnerElementFocused && e.relatedTarget) { + e.relatedTarget.focus(); + } else { + setCalendarVisiblity(false); + } + }; + + const openCalendar = () => { + if (!shouldPreventToggle.current) setCalendarVisiblity(true); + }; + + // Keep the calendar in the screen bounds if input is near the window edges + useLayoutEffect(() => { + if (!isCalendarOpen) return; + const { left, width, height, top } = calendarContainerElement.current.getBoundingClientRect(); + const { clientWidth, clientHeight } = document.documentElement; + const isOverflowingFromRight = left + width > clientWidth; + const isOverflowingFromLeft = left < 0; + const isOverflowingFromBottom = top + height > clientHeight; + + const getLeftStyle = () => { + const overflowFromRightDistance = left + width - clientWidth; + + if (!isOverflowingFromRight && !isOverflowingFromLeft) return; + const overflowFromLeftDistance = Math.abs(left); + const rightPosition = isOverflowingFromLeft ? overflowFromLeftDistance : 0; + + const leftStyle = isOverflowingFromRight + ? `calc(50% - ${overflowFromRightDistance}px)` + : `calc(50% + ${rightPosition}px)`; + return leftStyle; + }; + + calendarContainerElement.current.style.left = getLeftStyle(); + if ( + (calendarPopperPosition === 'auto' && isOverflowingFromBottom) || + calendarPopperPosition === 'top' + ) { + calendarContainerElement.current.classList.add('-top'); + } + }, [isCalendarOpen]); + + const handleCalendarChange = newValue => { + const valueType = getValueType(value); + onChange(newValue); + if (valueType === TYPE_SINGLE_DATE) setCalendarVisiblity(false); + else if (valueType === TYPE_RANGE && newValue.from && newValue.to) setCalendarVisiblity(false); + }; + + const handleKeyUp = ({ key }) => { + switch (key) { + case 'Enter': + setCalendarVisiblity(true); + break; + case 'Escape': + setCalendarVisiblity(false); + shouldPreventToggle.current = true; + break; + } + }; + + useEffect(() => { + if (!isCalendarOpen && shouldPreventToggle.current) { + inputElement.current.focus(); + shouldPreventToggle.current = false; + } + }, [shouldPreventToggle, isCalendarOpen]); + + return ( +
+ + {isCalendarOpen && ( + <> +
{ + shouldPreventToggle.current = true; + }} + > + +
+
+ + )} +
+ ); +}; + +DatePicker.defaultProps = { + wrapperClassName: '', + locale: 'en', + calendarPopperPosition: 'auto', +}; + +export default DatePicker; diff --git a/src/components/Datepicker/CalendarComponents/DatePickerInput.js b/src/components/Datepicker/CalendarComponents/DatePickerInput.js new file mode 100644 index 0000000..30d3ee7 --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/DatePickerInput.js @@ -0,0 +1,93 @@ +import React from 'react'; + +import { useLocaleUtils, useLocaleLanguage } from './shared/hooks'; +import { putZero, getValueType } from './shared/generalUtils'; +import { TYPE_SINGLE_DATE, TYPE_RANGE, TYPE_MUTLI_DATE } from './shared/constants'; + +const DatePickerInput = React.forwardRef( + ( + { value, inputPlaceholder, inputClassName, inputName, formatInputText, renderInput, locale }, + ref, + ) => { + const { getLanguageDigits } = useLocaleUtils(locale); + const { + from: fromWord, + to: toWord, + yearLetterSkip, + digitSeparator, + defaultPlaceholder, + isRtl, + } = useLocaleLanguage(locale); + + const getSingleDayValue = () => { + if (!value) return ''; + const year = getLanguageDigits(value.year); + const month = getLanguageDigits(putZero(value.month)); + const day = getLanguageDigits(putZero(value.day)); + return `${year}/${month}/${day}`; + }; + + const getDayRangeValue = () => { + if (!value.from || !value.to) return ''; + const { from, to } = value; + const fromText = `${getLanguageDigits(putZero(from.year)) + .toString() + .slice(yearLetterSkip)}/${getLanguageDigits(putZero(from.month))}/${getLanguageDigits( + putZero(from.day), + )}`; + const toText = `${getLanguageDigits(putZero(to.year)) + .toString() + .slice(yearLetterSkip)}/${getLanguageDigits(putZero(to.month))}/${getLanguageDigits( + putZero(to.day), + )}`; + return `${fromWord} ${fromText} ${toWord} ${toText}`; + }; + + const getMultiDateValue = () => { + return value.map(date => getLanguageDigits(date.day)).join(`${digitSeparator} `); + }; + + const getValue = () => { + if (formatInputText()) return formatInputText(); + const valueType = getValueType(value); + switch (valueType) { + case TYPE_SINGLE_DATE: + return getSingleDayValue(); + case TYPE_RANGE: + return getDayRangeValue(); + case TYPE_MUTLI_DATE: + return getMultiDateValue(); + } + }; + + const placeholderValue = inputPlaceholder || defaultPlaceholder; + const render = () => { + return ( + renderInput({ ref }) || ( + + ) + ); + }; + + return render(); + }, +); + +DatePickerInput.defaultProps = { + formatInputText: () => '', + renderInput: () => null, + inputPlaceholder: '', + inputClassName: '', + inputName: '', +}; + +export default DatePickerInput; diff --git a/src/components/Datepicker/CalendarComponents/components/DaysList.js b/src/components/Datepicker/CalendarComponents/components/DaysList.js new file mode 100644 index 0000000..bb5e55d --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/components/DaysList.js @@ -0,0 +1,399 @@ +import React, { useRef, useEffect } from 'react'; + +import { + getSlideDate, + handleSlideAnimationEnd, + animateContent, +} from '../shared/sliderHelpers'; +import { + deepCloneObject, + isSameDay, + createUniqueRange, + getValueType, +} from '../shared/generalUtils'; +import { + TYPE_SINGLE_DATE, + TYPE_RANGE, + TYPE_MUTLI_DATE, +} from '../shared/constants'; +import handleKeyboardNavigation from '../shared/keyboardNavigation'; +import { useLocaleUtils, useLocaleLanguage } from '../shared/hooks'; + +const DaysList = ({ + activeDate, + value, + monthChangeDirection, + onSlideChange, + disabledDays, + onDisabledDayError, + minimumDate, + maximumDate, + onChange, + locale, + calendarTodayClassName, + calendarSelectedDayClassName, + calendarRangeStartClassName, + calendarRangeEndClassName, + calendarRangeBetweenClassName, + shouldHighlightWeekends, + isQuickSelectorOpen, + customDaysClassName, + events, + showEvent, + type +}) => { + const calendarSectionWrapper = useRef(null); + const { isRtl, weekDays: weekDaysList } = useLocaleLanguage(locale); + const { + getToday, + isBeforeDate, + checkDayInDayRange, + getMonthFirstWeekday, + getMonthLength, + getLanguageDigits, + getMonthName, + } = useLocaleUtils(locale); + const today = getToday(); + + useEffect(() => { + if (!monthChangeDirection) return; + animateContent({ + direction: monthChangeDirection, + parent: calendarSectionWrapper.current, + }); + }, [monthChangeDirection]); + + const getDayRangeValue = (day) => { + const clonedDayRange = deepCloneObject(value); + const dayRangeValue = + clonedDayRange.from && clonedDayRange.to + ? { from: null, to: null } + : clonedDayRange; + const dayRangeProp = !dayRangeValue.from ? 'from' : 'to'; + dayRangeValue[dayRangeProp] = day; + const { from, to } = dayRangeValue; + + // swap from and to values if from is later than to + if (isBeforeDate(dayRangeValue.to, dayRangeValue.from)) { + dayRangeValue.from = to; + dayRangeValue.to = from; + } + + const checkIncludingDisabledDay = (disabledDay) => { + return checkDayInDayRange({ + day: disabledDay, + from: dayRangeValue.from, + to: dayRangeValue.to, + }); + }; + const includingDisabledDay = disabledDays.find(checkIncludingDisabledDay); + if (includingDisabledDay) { + onDisabledDayError(includingDisabledDay); + return value; + } + + return dayRangeValue; + }; + + const getMultiDateValue = (day) => { + const isAlreadyExisting = value.some((valueDay) => + isSameDay(valueDay, day) + ); + const addedToValue = [...value, day]; + const removedFromValue = value.filter( + (valueDay) => !isSameDay(valueDay, day) + ); + return isAlreadyExisting ? removedFromValue : addedToValue; + }; + + const handleDayClick = (day) => { + const getNewValue = () => { + const valueType = getValueType(value); + switch (valueType) { + case TYPE_SINGLE_DATE: + return day; + case TYPE_RANGE: + return getDayRangeValue(day); + case TYPE_MUTLI_DATE: + return getMultiDateValue(day); + } + }; + const newValue = getNewValue(); + onChange(newValue); + }; + + const isSingleDateSelected = (day) => { + const valueType = getValueType(value); + if (valueType === TYPE_SINGLE_DATE) return isSameDay(day, value); + if (valueType === TYPE_MUTLI_DATE) + return value.some((valueDay) => isSameDay(valueDay, day)); + }; + + const getDayStatus = (dayItem) => { + const isToday = isSameDay(dayItem, today); + const isSelected = isSingleDateSelected(dayItem); + const { from: startingDay, to: endingDay } = value || {}; + const isStartingDayRange = isSameDay(dayItem, startingDay); + const isEndingDayRange = isSameDay(dayItem, endingDay); + const isWithinRange = checkDayInDayRange({ + day: dayItem, + from: startingDay, + to: endingDay, + }); + return { + isToday, + isSelected, + isStartingDayRange, + isEndingDayRange, + isWithinRange, + }; + }; + + const getDayClassNames = (dayItem) => { + const { + isToday, + isSelected, + isStartingDayRange, + isEndingDayRange, + isWithinRange, + } = getDayStatus(dayItem); + const customDayItemClassName = customDaysClassName.find((day) => + isSameDay(dayItem, day) + ); + + const classNames = '' + .concat(isToday && !isSelected ? ` -today ${calendarTodayClassName}` : '') + .concat(!dayItem.isStandard ? ' -blank' : '') + .concat(dayItem.isWeekend && shouldHighlightWeekends ? ' -weekend' : '') + .concat( + customDayItemClassName ? ` ${customDayItemClassName.className}` : '' + ) + .concat(isSelected ? ` -selected ${calendarSelectedDayClassName}` : '') + .concat( + isStartingDayRange + ? ` -selectedStart ${calendarRangeStartClassName}` + : '' + ) + .concat( + isEndingDayRange ? ` -selectedEnd ${calendarRangeEndClassName}` : '' + ) + .concat( + isWithinRange + ? ` -selectedBetween ${calendarRangeBetweenClassName}` + : '' + ) + .concat(dayItem.isDisabled ? ' -disabled' : ''); + return classNames; + }; + + const getViewMonthDays = (date) => { + // to match month starting date with the correct weekday label + const prependingBlankDays = createUniqueRange( + getMonthFirstWeekday(date), + 'starting-blank' + ); + const standardDays = createUniqueRange(getMonthLength(date)).map((day) => ({ + ...day, + isStandard: true, + month: date.month, + year: date.year, + })); + const allDays = [...prependingBlankDays, ...standardDays]; + return allDays; + }; + + const handleDayPress = ({ isDisabled, ...dayItem }) => { + if (isDisabled) { + onDisabledDayError(dayItem); // good for showing error messages + } else handleDayClick(dayItem); + }; + + const isDayReachableByKeyboard = ({ + isOnActiveSlide, + isStandard, + isSelected, + isStartingDayRange, + isToday, + day, + }) => { + if (isQuickSelectorOpen || !isOnActiveSlide || !isStandard) return false; + if (isSelected || isStartingDayRange || isToday || day === 1) return true; + }; + + const renderEachWeekDays = ( + { id, value: day, month, year, isStandard }, + index + ) => { + const dayItem = { day, month, year }; + const isInDisabledDaysRange = disabledDays.some((disabledDay) => + isSameDay(dayItem, disabledDay) + ); + const isBeforeMinimumDate = isBeforeDate(dayItem, minimumDate); + const isAfterMaximumDate = isBeforeDate(maximumDate, dayItem); + const isNotInValidRange = + isStandard && (isBeforeMinimumDate || isAfterMaximumDate); + const isDisabled = isInDisabledDaysRange || isNotInValidRange; + const isWeekend = weekDaysList.some( + (weekDayItem, weekDayItemIndex) => + weekDayItem.isWeekend && weekDayItemIndex === index + ); + const additionalClass = getDayClassNames({ + ...dayItem, + isWeekend, + isStandard, + isDisabled, + }); + const dayLabel = `${weekDaysList[index].name}, ${day} ${getMonthName( + month + )} ${year}`; + const isOnActiveSlide = month === activeDate.month; + const dayStatus = getDayStatus(dayItem); + const { isSelected, isStartingDayRange, isEndingDayRange, isWithinRange } = + dayStatus; + const shouldEnableKeyboardNavigation = isDayReachableByKeyboard({ + ...dayItem, + ...dayStatus, + isOnActiveSlide, + isStandard, + }); + + // showing Dots Of Events + const specialDate = (sDay, sMonth, sYear) => { + return events.map((e) => { + if (sDay == e.day && sMonth == e.month && sYear == e.year) { + return .; + + } + }) + + }; + + const showEventDescription = (sDay, sMonth, sYear) => { + + return events.map((e) => { + + if (sDay == e.day && sMonth == e.month && sYear == e.year) { + + return (

{e.description}

) + + } + + }) + + + } + return ( +
{ + handleDayPress({ ...dayItem, isDisabled }); + // console.log(specialDate(day)); + // console.log(showEventDescription(day)) + showEvent(showEventDescription(day, month, year)) + + }} + onKeyDown={({ key }) => { + /* istanbul ignore else */ + if (key === 'Enter') handleDayPress({ ...dayItem, isDisabled }); + }} + aria-disabled={isDisabled} + aria-label={dayLabel} + aria-selected={ + isSelected || isStartingDayRange || isEndingDayRange || isWithinRange + } + {...(!isStandard || !isOnActiveSlide || isQuickSelectorOpen + ? { 'aria-hidden': true } + : {})} + role="gridcell" + data-is-default-selectable={shouldEnableKeyboardNavigation} + > +

{!isStandard ? '' : getLanguageDigits(day)}

+ {/* Dots Of Events */} + {specialDate(day, month, year)} +
+ ); + }; + + const renderMonthDays = (isInitialActiveChild) => { + const date = getSlideDate({ + activeDate, + isInitialActiveChild, + monthChangeDirection, + parent: calendarSectionWrapper.current, + }); + const allDays = getViewMonthDays(date); + const renderSingleWeekRow = (weekRowIndex) => { + const eachWeekDays = allDays + .slice(weekRowIndex * 7, weekRowIndex * 7 + 7) + .map(renderEachWeekDays); + return ( +
+ {eachWeekDays} +
+ ); + }; + return Array.from(Array(6).keys()).map(renderSingleWeekRow); + }; + + const handleKeyDown = (e) => { + handleKeyboardNavigation(e, { allowVerticalArrows: true }); + }; + + return ( +
+
{ + handleSlideAnimationEnd(e); + onSlideChange(); + }} + className="Calendar__section -shown" + role="rowgroup" + > + {renderMonthDays(true)} +
+
{ + handleSlideAnimationEnd(e); + onSlideChange(); + }} + className="Calendar__section -hiddenNext" + role="rowgroup" + > + {renderMonthDays(false)} +
+
+ ); +}; + +DaysList.defaultProps = { + onChange: () => { }, + onDisabledDayError: () => { }, + disabledDays: [], + calendarTodayClassName: '', + calendarSelectedDayClassName: '', + calendarRangeStartClassName: '', + calendarRangeBetweenClassName: '', + calendarRangeEndClassName: '', + shouldHighlightWeekends: false, +}; + +export default DaysList; diff --git a/src/components/Datepicker/CalendarComponents/components/Header.js b/src/components/Datepicker/CalendarComponents/components/Header.js new file mode 100644 index 0000000..5677b94 --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/components/Header.js @@ -0,0 +1,205 @@ +import React, { useEffect, useRef } from 'react'; + +import { isSameDay } from '../shared/generalUtils'; +import { getSlideDate, animateContent, handleSlideAnimationEnd } from '../shared/sliderHelpers'; +import { useLocaleUtils, useLocaleLanguage } from '../shared/hooks'; + +const Header = ({ + maximumDate, + minimumDate, + onMonthChange, + activeDate, + monthChangeDirection, + onMonthSelect, + onYearSelect, + isMonthSelectorOpen, + isYearSelectorOpen, + locale, + type +}) => { + const headerElement = useRef(null); + const monthYearWrapperElement = useRef(null); + + const { getMonthName, isBeforeDate, getLanguageDigits } = useLocaleUtils(locale); + const { + isRtl, + nextMonth, + previousMonth, + openMonthSelector, + closeMonthSelector, + openYearSelector, + closeYearSelector, + } = useLocaleLanguage(locale); + + useEffect(() => { + if (!monthChangeDirection) return; + animateContent({ + direction: monthChangeDirection, + parent: monthYearWrapperElement.current, + }); + }, [monthChangeDirection]); + + useEffect(() => { + const isOpen = isMonthSelectorOpen || isYearSelectorOpen; + const monthText = headerElement.current.querySelector( + '.Calendar__monthYear.-shown .Calendar__monthText', + ); + const yearText = monthText.nextSibling; + const hasActiveBackground = element => element.classList.contains('-activeBackground'); + const isInitialRender = + !isOpen && !hasActiveBackground(monthText) && !hasActiveBackground(yearText); + if (isInitialRender) return; + + const arrows = [...headerElement.current.querySelectorAll('.Calendar__monthArrowWrapper')]; + const hasMonthSelectorToggled = isMonthSelectorOpen || hasActiveBackground(monthText); + const primaryElement = hasMonthSelectorToggled ? monthText : yearText; + const secondaryElement = hasMonthSelectorToggled ? yearText : monthText; + + let translateXDirection = hasMonthSelectorToggled ? 1 : -1; + if (isRtl) translateXDirection *= -1; + const scale = !isOpen ? 0.95 : 1; + const translateX = !isOpen ? 0 : `${(translateXDirection * secondaryElement.offsetWidth) / 2}`; + if (!isOpen) { + secondaryElement.removeAttribute('aria-hidden'); + } else { + secondaryElement.setAttribute('aria-hidden', true); + } + secondaryElement.setAttribute('tabindex', isOpen ? '-1' : '0'); + secondaryElement.style.transform = ''; + primaryElement.style.transform = `scale(${scale}) ${ + translateX ? `translateX(${translateX}px)` : '' + }`; + primaryElement.classList.toggle('-activeBackground'); + secondaryElement.classList.toggle('-hidden'); + arrows.forEach(arrow => { + const isHidden = arrow.classList.contains('-hidden'); + arrow.classList.toggle('-hidden'); + if (isHidden) { + arrow.removeAttribute('aria-hidden'); + arrow.setAttribute('tabindex', '0'); + } else { + arrow.setAttribute('aria-hidden', true); + arrow.setAttribute('tabindex', '-1'); + } + }); + }, [isMonthSelectorOpen, isYearSelectorOpen]); + + const getMonthYearText = isInitialActiveChild => { + const date = getSlideDate({ + isInitialActiveChild, + monthChangeDirection, + activeDate, + parent: monthYearWrapperElement.current, + }); + const year = getLanguageDigits(date.year); + const month = getMonthName(date.month); + return { month, year }; + }; + + const isNextMonthArrowDisabled = + maximumDate && + isBeforeDate(maximumDate, { ...activeDate, month: activeDate.month + 1, day: 1 }); + const isPreviousMonthArrowDisabled = + minimumDate && + (isBeforeDate({ ...activeDate, day: 1 }, minimumDate) || + isSameDay(minimumDate, { ...activeDate, day: 1 })); + + const onMonthChangeTrigger = direction => { + const isMonthChanging = Array.from(monthYearWrapperElement.current.children).some(child => + child.classList.contains('-shownAnimated'), + ); + if (isMonthChanging) return; + onMonthChange(direction); + }; + + // first button text is the one who shows the current month and year(initial active child) + const monthYearButtons = [true, false].map(isInitialActiveChild => { + const { month, year } = getMonthYearText(isInitialActiveChild); + const isActiveMonth = month === getMonthName(activeDate.month); + const hiddenStatus = { + ...(isActiveMonth ? {} : { 'aria-hidden': true }), + }; + return ( +
+ + +
+ ); + }); + + return ( +
+ {type == 'dashboard' ? + +
+   + {monthYearButtons} +
+ : + <> + +
+   + {monthYearButtons} +
+ + + + } +
+ ); +}; + +export default Header; diff --git a/src/components/Datepicker/CalendarComponents/components/MonthSelector.js b/src/components/Datepicker/CalendarComponents/components/MonthSelector.js new file mode 100644 index 0000000..aafbd2c --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/components/MonthSelector.js @@ -0,0 +1,74 @@ +import React, { useRef, useEffect } from 'react'; + +import { isSameDay } from '../shared/generalUtils'; +import handleKeyboardNavigation from '../shared/keyboardNavigation'; +import { useLocaleUtils, useLocaleLanguage } from '../shared/hooks'; + +const MonthSelector = ({ activeDate, maximumDate, minimumDate, onMonthSelect, isOpen, locale }) => { + const monthSelector = useRef(null); + + useEffect(() => { + const classToggleMethod = isOpen ? 'add' : 'remove'; + monthSelector.current.classList[classToggleMethod]('-open'); + }, [isOpen]); + + const { getMonthNumber, isBeforeDate } = useLocaleUtils(locale); + const { months: monthsList } = useLocaleLanguage(locale); + + const handleKeyDown = e => { + handleKeyboardNavigation(e, { allowVerticalArrows: false }); + }; + + const renderMonthSelectorItems = () => + monthsList.map(persianMonth => { + const monthNumber = getMonthNumber(persianMonth); + const monthDate = { day: 1, month: monthNumber, year: activeDate.year }; + const isAfterMaximumDate = + maximumDate && isBeforeDate(maximumDate, { ...monthDate, month: monthNumber }); + const isBeforeMinimumDate = + minimumDate && + (isBeforeDate({ ...monthDate, month: monthNumber + 1 }, minimumDate) || + isSameDay({ ...monthDate, month: monthNumber + 1 }, minimumDate)); + const isSelected = monthNumber === activeDate.month; + return ( +
  • + +
  • + ); + }); + return ( +
    +
    +
      + {renderMonthSelectorItems()} +
    +
    +
    + ); +}; + +export default MonthSelector; diff --git a/src/components/Datepicker/CalendarComponents/components/YearSelector.js b/src/components/Datepicker/CalendarComponents/components/YearSelector.js new file mode 100644 index 0000000..115bad6 --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/components/YearSelector.js @@ -0,0 +1,99 @@ +import React, { useRef, useEffect } from 'react'; + +import { MINIMUM_SELECTABLE_YEAR_SUBTRACT, MAXIMUM_SELECTABLE_YEAR_SUM } from '../shared/constants'; +import handleKeyboardNavigation from '../shared/keyboardNavigation'; +import { useLocaleUtils } from '../shared/hooks'; + +const YearSelector = ({ + isOpen, + activeDate, + onYearSelect, + selectorStartingYear, + selectorEndingYear, + maximumDate, + minimumDate, + locale, +}) => { + const wrapperElement = useRef(null); + const yearListElement = useRef(null); + + const { getLanguageDigits, getToday } = useLocaleUtils(locale); + const startingYearValue = + selectorStartingYear || getToday().year - MINIMUM_SELECTABLE_YEAR_SUBTRACT; + const endingYearValue = selectorEndingYear || getToday().year + MAXIMUM_SELECTABLE_YEAR_SUM; + const allYears = []; + for (let i = startingYearValue; i <= endingYearValue; i += 1) { + allYears.push(i); + } + useEffect(() => { + const classToggleMethod = isOpen ? 'add' : 'remove'; + const activeSelectorYear = wrapperElement.current.querySelector( + '.Calendar__yearSelectorItem.-active', + ); + if (!activeSelectorYear) { + throw new RangeError( + `Provided value for year is out of selectable year range. You're probably using a wrong locale prop value or your provided value's locale is different from the date picker locale. Try changing the 'locale' prop or the value you've provided.`, + ); + } + wrapperElement.current.classList[classToggleMethod]('-faded'); + yearListElement.current.scrollTop = + activeSelectorYear.offsetTop - activeSelectorYear.offsetHeight * 5; + yearListElement.current.classList[classToggleMethod]('-open'); + }, [isOpen]); + + const renderSelectorYears = () => { + return allYears.map(item => { + const isAfterMaximumDate = maximumDate && item > maximumDate.year; + const isBeforeMinimumDate = minimumDate && item < minimumDate.year; + const isSelected = activeDate.year === item; + return ( +
  • + +
  • + ); + }); + }; + + const handleKeyDown = e => { + handleKeyboardNavigation(e, { allowVerticalArrows: false }); + }; + + return ( +
    +
    +
      + {renderSelectorYears()} +
    +
    +
    + ); +}; + +YearSelector.defaultProps = { + selectorStartingYear: 0, + selectorEndingYear: 0, +}; + +export default YearSelector; diff --git a/src/components/Datepicker/CalendarComponents/components/index.js b/src/components/Datepicker/CalendarComponents/components/index.js new file mode 100644 index 0000000..7cc422f --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/components/index.js @@ -0,0 +1,4 @@ +export { default as Header } from './Header'; +export { default as MonthSelector } from './MonthSelector'; +export { default as YearSelector } from './YearSelector'; +export { default as DaysList } from './DaysList'; diff --git a/src/components/Datepicker/CalendarComponents/index.js b/src/components/Datepicker/CalendarComponents/index.js new file mode 100644 index 0000000..a983b77 --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/index.js @@ -0,0 +1,3 @@ +export { default} from './DatePicker'; +export * from './Calendar'; +export { default as utils } from './shared/localeUtils'; diff --git a/src/components/Datepicker/CalendarComponents/shared/constants.js b/src/components/Datepicker/CalendarComponents/shared/constants.js new file mode 100644 index 0000000..bceb3a1 --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/shared/constants.js @@ -0,0 +1,104 @@ +export const PERSIAN_NUMBERS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']; + +export const PERSIAN_MONTHS = [ + 'فروردین', + 'اردیبهشت', + 'خرداد', + 'تیر', + 'مرداد', + 'شهریور', + 'مهر', + 'آبان', + 'آذر', + 'دی', + 'بهمن', + 'اسفند', +]; + +export const GREGORIAN_MONTHS = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +]; + +export const PERSIAN_WEEK_DAYS = [ + { + name: 'شنبه', + short: 'ش', + }, + { + name: 'یکشنبه', + short: 'ی', + }, + { + name: 'دوشنبه', + short: 'د', + }, + { + name: 'سه شنبه', + short: 'س', + }, + { + name: 'چهارشنبه', + short: 'چ', + }, + { + name: 'پنجشنبه', + short: 'پ', + }, + { + name: 'جمعه', + short: 'ج', + isWeekend: true, + }, +]; + +export const GREGORIAN_WEEK_DAYS = [ + { + name: 'Sunday', + short: 'S', + isWeekend: true, + }, + { + name: 'Monday', + short: 'M', + }, + { + name: 'Tuesday', + short: 'T', + }, + { + name: 'Wednesday', + short: 'W', + }, + { + name: 'Thursday', + short: 'T', + }, + { + name: 'Friday', + short: 'F', + }, + { + name: 'Saturday', + short: 'S', + isWeekend: true, + }, +]; + +export const MINIMUM_SELECTABLE_YEAR_SUBTRACT = 100; + +export const MAXIMUM_SELECTABLE_YEAR_SUM = 50; + +export const TYPE_SINGLE_DATE = 'SINGLE_DATE'; +export const TYPE_RANGE = 'RANGE'; +export const TYPE_MUTLI_DATE = 'MUTLI_DATE'; diff --git a/src/components/Datepicker/CalendarComponents/shared/generalUtils.js b/src/components/Datepicker/CalendarComponents/shared/generalUtils.js new file mode 100644 index 0000000..fd379b6 --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/shared/generalUtils.js @@ -0,0 +1,69 @@ +import { TYPE_SINGLE_DATE, TYPE_RANGE, TYPE_MUTLI_DATE } from './constants'; + +/* + These utility functions don't depend on locale of the date picker(Persian or Gregorian) +*/ + +const createUniqueRange = (number, startingId) => + Array.from(Array(number).keys()).map(key => ({ + value: key + 1, + id: `${startingId}-${key}`, + })); + +const isSameDay = (day1, day2) => { + if (!day1 || !day2) return false; + return day1.day === day2.day && day1.month === day2.month && day1.year === day2.year; +}; + +const putZero = number => (number.toString().length === 1 ? `0${number}` : number); + +const toExtendedDay = date => [date.year, date.month, date.day]; + +const shallowClone = value => ({ ...value }); + +const deepCloneObject = obj => + JSON.parse(JSON.stringify(obj, (key, value) => (typeof value === 'undefined' ? null : value))); + +const getDateAccordingToMonth = (date, direction) => { + const toSum = direction === 'NEXT' ? 1 : -1; + let newMonthIndex = date.month + toSum; + let newYear = date.year; + if (newMonthIndex < 1) { + newMonthIndex = 12; + newYear -= 1; + } + if (newMonthIndex > 12) { + newMonthIndex = 1; + newYear += 1; + } + const newDate = { year: newYear, month: newMonthIndex, day: 1 }; + return newDate; +}; + +const hasProperty = (object, propertyName) => + Object.prototype.hasOwnProperty.call(object || {}, propertyName); + +const getValueType = value => { + if (Array.isArray(value)) return TYPE_MUTLI_DATE; + if (hasProperty(value, 'from') && hasProperty(value, 'to')) return TYPE_RANGE; + if ( + !value || + (hasProperty(value, 'year') && hasProperty(value, 'month') && hasProperty(value, 'day')) + ) { + return TYPE_SINGLE_DATE; + } + throw new TypeError( + `The passed value is malformed! Please make sure you're using one of the valid value types for date picker.`, + ); +}; + +export { + createUniqueRange, + isSameDay, + putZero, + toExtendedDay, + shallowClone, + deepCloneObject, + getDateAccordingToMonth, + getValueType, +}; diff --git a/src/components/Datepicker/CalendarComponents/shared/hooks.js b/src/components/Datepicker/CalendarComponents/shared/hooks.js new file mode 100644 index 0000000..1656e5b --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/shared/hooks.js @@ -0,0 +1,10 @@ +import { useMemo } from 'react'; + +import utils from './localeUtils'; +import getLanguageText from './localeLanguages'; + +const useLocaleUtils = locale => useMemo(() => utils(locale), [locale]); + +const useLocaleLanguage = locale => useMemo(() => getLanguageText(locale), [locale]); + +export { useLocaleUtils, useLocaleLanguage }; diff --git a/src/components/Datepicker/CalendarComponents/shared/keyboardNavigation.js b/src/components/Datepicker/CalendarComponents/shared/keyboardNavigation.js new file mode 100644 index 0000000..f72d0ae --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/shared/keyboardNavigation.js @@ -0,0 +1,44 @@ +const handleArrowKeys = (e, { allowVerticalArrows }) => { + const { activeElement } = document; + const getNthChildSafe = (element, index) => (element ? element.children[index] : null); + const getStandardItem = item => item && (item.hasAttribute('aria-hidden') ? null : item); + const { nextSibling: nextRow, previousSibling: previousRow } = activeElement.parentElement; + const nextSibling = getStandardItem(activeElement.nextSibling || getNthChildSafe(nextRow, 0)); + const previousRowLength = previousRow ? previousRow.children.length - 1 : 0; + const previousSibling = getStandardItem( + activeElement.previousSibling || getNthChildSafe(previousRow, previousRowLength), + ); + const getVerticalSibling = row => + getNthChildSafe(row, Array.from(activeElement.parentElement.children).indexOf(activeElement)); + const downSibling = getStandardItem(getVerticalSibling(nextRow)); + const upSibling = getStandardItem(getVerticalSibling(previousRow)); + const isDefaultSelectable = activeElement.dataset.isDefaultSelectable === 'true'; + + if (!isDefaultSelectable) activeElement.tabIndex = '-1'; + const focusIfAvailable = element => { + e.preventDefault(); + /* istanbul ignore else */ + if (element) { + element.setAttribute('tabindex', '0'); + element.focus(); + } + }; + switch (e.key) { + case 'ArrowRight': + focusIfAvailable(nextSibling); + break; + case 'ArrowLeft': + focusIfAvailable(previousSibling); + break; + case 'ArrowDown': + /* istanbul ignore else */ + if (allowVerticalArrows) focusIfAvailable(downSibling); + break; + case 'ArrowUp': + /* istanbul ignore else */ + if (allowVerticalArrows) focusIfAvailable(upSibling); + break; + } +}; + +export default handleArrowKeys; diff --git a/src/components/Datepicker/CalendarComponents/shared/localeLanguages.js b/src/components/Datepicker/CalendarComponents/shared/localeLanguages.js new file mode 100644 index 0000000..806b1f5 --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/shared/localeLanguages.js @@ -0,0 +1,85 @@ +import jalaali from 'jalaali-js'; + +import { + GREGORIAN_MONTHS, + PERSIAN_MONTHS, + GREGORIAN_WEEK_DAYS, + PERSIAN_WEEK_DAYS, + PERSIAN_NUMBERS, +} from './constants'; +import { toExtendedDay } from './generalUtils'; + +const localeLanguages = { + en: { + months: GREGORIAN_MONTHS, + weekDays: GREGORIAN_WEEK_DAYS, + weekStartingIndex: 0, + getToday(gregorainTodayObject) { + return gregorainTodayObject; + }, + toNativeDate(date) { + return new Date(date.year, date.month - 1, date.day); + }, + getMonthLength(date) { + return new Date(date.year, date.month, 0).getDate(); + }, + transformDigit(digit) { + return digit; + }, + nextMonth: 'Next Month', + previousMonth: 'Previous Month', + openMonthSelector: 'Open Month Selector', + openYearSelector: 'Open Year Selector', + closeMonthSelector: 'Close Month Selector', + closeYearSelector: 'Close Year Selector', + from: 'from', + to: 'to', + defaultPlaceholder: 'Select...', + digitSeparator: ',', + yearLetterSkip: 0, + isRtl: false, + }, + fa: { + months: PERSIAN_MONTHS, + weekDays: PERSIAN_WEEK_DAYS, + weekStartingIndex: 1, + getToday({ year, month, day }) { + const { jy, jm, jd } = jalaali.toJalaali(year, month, day); + return { year: jy, month: jm, day: jd }; + }, + toNativeDate(date) { + const gregorian = jalaali.toGregorian(...toExtendedDay(date)); + return new Date(gregorian.gy, gregorian.gm - 1, gregorian.gd); + }, + getMonthLength(date) { + return jalaali.jalaaliMonthLength(date.year, date.month); + }, + transformDigit(digit) { + return digit + .toString() + .split('') + .map(letter => PERSIAN_NUMBERS[Number(letter)]) + .join(''); + }, + nextMonth: 'ماه بعد', + previousMonth: 'ماه قبل', + openMonthSelector: 'نمایش انتخابگر ماه', + openYearSelector: 'نمایش انتخابگر سال', + closeMonthSelector: 'بستن انتخابگر ماه', + closeYearSelector: 'بستن انتخابگر ماه', + from: 'از', + to: 'تا', + defaultPlaceholder: 'انتخاب...', + digitSeparator: '،', + yearLetterSkip: -2, + isRtl: true, + }, +}; + +const getLocaleDetails = locale => { + if (typeof locale === 'string') return localeLanguages[locale]; + return locale; +}; + +export { localeLanguages }; +export default getLocaleDetails; diff --git a/src/components/Datepicker/CalendarComponents/shared/localeUtils.js b/src/components/Datepicker/CalendarComponents/shared/localeUtils.js new file mode 100644 index 0000000..1055f94 --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/shared/localeUtils.js @@ -0,0 +1,61 @@ +/* + These utility functions highly depend on locale of the date picker(Persian or Gregorian) +*/ + +import getLocaleDetails from './localeLanguages'; + +const utils = (locale = 'en') => { + const { + months: monthsList, + getToday: localeGetToday, + toNativeDate, + getMonthLength, + weekStartingIndex, + transformDigit: getLanguageDigits, + } = typeof locale === 'string' ? getLocaleDetails(locale) : locale; + + const getToday = () => { + const todayDate = new Date(); + const year = todayDate.getFullYear(); + const month = todayDate.getMonth() + 1; + const day = todayDate.getDate(); + return localeGetToday({ year, month, day }); + }; + + const getMonthName = month => monthsList[month - 1]; + + const getMonthNumber = monthName => monthsList.indexOf(monthName) + 1; + + const getMonthFirstWeekday = date => { + const gregorianDate = toNativeDate({ ...date, day: 1 }); + const weekday = gregorianDate.getDay(); + const dayIndex = weekday + weekStartingIndex; + return dayIndex % 7; + }; + + const isBeforeDate = (day1, day2) => { + if (!day1 || !day2) return false; + return toNativeDate(day1) < toNativeDate(day2); + }; + + const checkDayInDayRange = ({ day, from, to }) => { + if (!day || !from || !to) return false; + const nativeDay = toNativeDate(day); + const nativeFrom = toNativeDate(from); + const nativeTo = toNativeDate(to); + return nativeDay > nativeFrom && nativeDay < nativeTo; + }; + + return { + getToday, + getMonthName, + getMonthNumber, + getMonthLength, + getMonthFirstWeekday, + isBeforeDate, + checkDayInDayRange, + getLanguageDigits, + }; +}; + +export default utils; diff --git a/src/components/Datepicker/CalendarComponents/shared/sliderHelpers.js b/src/components/Datepicker/CalendarComponents/shared/sliderHelpers.js new file mode 100644 index 0000000..524e49a --- /dev/null +++ b/src/components/Datepicker/CalendarComponents/shared/sliderHelpers.js @@ -0,0 +1,33 @@ +import { getDateAccordingToMonth } from './generalUtils'; + +const getSlideDate = ({ parent, isInitialActiveChild, activeDate, monthChangeDirection }) => { + if (!parent) { + return isInitialActiveChild ? activeDate : getDateAccordingToMonth(activeDate, 'NEXT'); + } + const child = parent.children[isInitialActiveChild ? 0 : 1]; + const isActiveSlide = + child.classList.contains('-shown') || child.classList.contains('-shownAnimated'); // check -shownAnimated for Safari bug + return isActiveSlide ? activeDate : getDateAccordingToMonth(activeDate, monthChangeDirection); +}; + +const animateContent = ({ parent, direction }) => { + const wrapperChildren = Array.from(parent.children); + const shownItem = wrapperChildren.find(child => child.classList.contains('-shown')); + const hiddenItem = wrapperChildren.find(child => child !== shownItem); + const baseClass = shownItem.classList[0]; + const isNextMonth = direction === 'NEXT'; + const getAnimationClass = value => (value ? '-hiddenNext' : '-hiddenPrevious'); + hiddenItem.style.transition = 'none'; + shownItem.style.transition = ''; + shownItem.className = `${baseClass} ${getAnimationClass(!isNextMonth)}`; + hiddenItem.className = `${baseClass} ${getAnimationClass(isNextMonth)}`; + hiddenItem.classList.add('-shownAnimated'); +}; + +const handleSlideAnimationEnd = ({ target }) => { + target.classList.remove('-hiddenNext'); + target.classList.remove('-hiddenPrevious'); + target.classList.replace('-shownAnimated', '-shown'); +}; + +export { animateContent, getSlideDate, handleSlideAnimationEnd }; diff --git a/src/components/Calender/DateRangePicker/index.js b/src/components/Datepicker/DateRangePicker/index.js similarity index 100% rename from src/components/Calender/DateRangePicker/index.js rename to src/components/Datepicker/DateRangePicker/index.js diff --git a/src/components/Calender/RangeDatePicker/index.js b/src/components/Datepicker/RangeDatePicker/index.js similarity index 89% rename from src/components/Calender/RangeDatePicker/index.js rename to src/components/Datepicker/RangeDatePicker/index.js index 1f56e5d..c3a7dd3 100644 --- a/src/components/Calender/RangeDatePicker/index.js +++ b/src/components/Datepicker/RangeDatePicker/index.js @@ -1,6 +1,7 @@ import React, { useState } from "react"; import "./index.scss"; -import DatePicker from "../Calendar/CalendarComponents"; + +import DatePicker from "../CalendarComponents"; const RangeDatePicker = () => { const [selectedDayRange, setSelectedDayRange] = useState({ diff --git a/src/components/Calender/RangeDatePicker/index.scss b/src/components/Datepicker/RangeDatePicker/index.scss similarity index 100% rename from src/components/Calender/RangeDatePicker/index.scss rename to src/components/Datepicker/RangeDatePicker/index.scss diff --git a/src/components/Calender/index.js b/src/components/Datepicker/index.js similarity index 88% rename from src/components/Calender/index.js rename to src/components/Datepicker/index.js index 90d8431..66cf8d4 100644 --- a/src/components/Calender/index.js +++ b/src/components/Datepicker/index.js @@ -1,10 +1,12 @@ import React, { useState } from "react"; import DateRangePicker from "./DateRangePicker"; +import RangeDatePicker from "./RangeDatePicker"; function Calender() { const list = { 1: , + 1: , }; const [type, setType] = useState(1); return ( diff --git a/src/redux/data.js b/src/redux/data.js index 7f5dd5d..7b1f027 100644 --- a/src/redux/data.js +++ b/src/redux/data.js @@ -30,7 +30,7 @@ import ProductDescriptionTable from "../components/ProductDescriptionTable"; import QuestionAnswer from "../components/QuestionAnswer"; import RadioButton from "../components/RadioButton"; import Range from "../components/Range"; -import Calender from "../components/Calender"; +import Datepicker from "../components/Datepicker"; // mini components import MiniComponents from "../components/MiniComponents"; @@ -645,12 +645,12 @@ const components = [ }, }, { - name: "Calender", + name: "Datepicker", author: "Andalib/Keshavarzi", props: [], content: { - name: "Calender", - component: , + name: "Datepicker", + component: , code: null, }, },