nexxtway/react-rainbow

View on GitHub
src/components/Calendar/singleCalendar.js

Summary

Maintainability
D
3 days
Test Coverage
/* eslint-disable react/sort-comp */
/* eslint-disable react/prop-types */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import DaysOfWeek from './daysOfWeek';
import Month from './month';
import {
    normalizeDate,
    addDays,
    addMonths,
    formatDate,
    getFirstDayMonth,
    getFormattedMonth,
    getLastDayMonth,
    getYearsRange,
    isSameMonth,
    getSign,
    getCalendarBounds,
    isDateBelowLimit,
    isDateBeyondLimit,
    getNextFocusedDate,
    isSameDatesRange,
    isEmptyRange,
    isSameDay,
} from './helpers';
import CalendarHeader from './calendarHeader';
import StyledTable from './styled/table';
import {
    UP_KEY,
    DOWN_KEY,
    RIGHT_KEY,
    LEFT_KEY,
    HOME_KEY,
    END_KEY,
    PAGEUP_KEY,
    PAGEDN_KEY,
    SPACE_KEY,
    ENTER_KEY,
} from '../../libs/constants';
import { uniqueId } from '../../libs/utils';
import { Provider } from './context';
import normalizeDates from './helpers/normalizeDates';

class SingleCalendar extends Component {
    constructor(props) {
        super(props);
        this.state = {
            focusedDate: normalizeDate(props.value),
            currentMonth: getFirstDayMonth(normalizeDate(props.value)),
            currentRange: props.selectedRange,
            maxRangeEnd: undefined,
        };
        this.enableNavKeys = false;
        this.monthLabelId = uniqueId('month');
        this.previousMonth = this.previousMonth.bind(this);
        this.nextMonth = this.nextMonth.bind(this);
        this.handleYearChange = this.handleYearChange.bind(this);
        this.handleKeyDown = this.handleKeyDown.bind(this);
        this.handleKeyUpPressed = this.handleKeyUpPressed.bind(this);
        this.handleKeyDownPressed = this.handleKeyDownPressed.bind(this);
        this.handleKeyLeftPressed = this.handleKeyLeftPressed.bind(this);
        this.handleKeyRightPressed = this.handleKeyRightPressed.bind(this);
        this.handleKeyHomePressed = this.handleKeyHomePressed.bind(this);
        this.handleKeyEndPressed = this.handleKeyEndPressed.bind(this);
        this.handleKeyPageUpPressed = this.handleKeyPageUpPressed.bind(this);
        this.handleKeyPageDownPressed = this.handleKeyPageDownPressed.bind(this);
        this.handleKeyEnterPressed = this.handleKeyEnterPressed.bind(this);
        this.handleKeyAltPageUpPressed = this.handleKeyAltPageUpPressed.bind(this);
        this.handleKeyAltPageDownPressed = this.handleKeyAltPageDownPressed.bind(this);
        this.keyHandlerMap = {
            [UP_KEY]: this.handleKeyUpPressed,
            [DOWN_KEY]: this.handleKeyDownPressed,
            [LEFT_KEY]: this.handleKeyLeftPressed,
            [RIGHT_KEY]: this.handleKeyRightPressed,
            [HOME_KEY]: this.handleKeyHomePressed,
            [END_KEY]: this.handleKeyEndPressed,
            [PAGEUP_KEY]: this.handleKeyPageUpPressed,
            [PAGEDN_KEY]: this.handleKeyPageDownPressed,
            [SPACE_KEY]: this.handleKeyEnterPressed,
            [ENTER_KEY]: this.handleKeyEnterPressed,
        };
        this.keyHandlerMapAlt = {
            [HOME_KEY]: this.handleKeyHomePressed,
            [END_KEY]: this.handleKeyEndPressed,
            [PAGEUP_KEY]: this.handleKeyAltPageUpPressed,
            [PAGEDN_KEY]: this.handleKeyAltPageDownPressed,
        };

        this.onDayFocus = this.onDayFocus.bind(this);
        this.onDayBlur = this.onDayBlur.bind(this);
        this.onDayHover = this.onDayHover.bind(this);
    }

    componentDidUpdate(prevProps) {
        const { selectedRange: prevSelectedRange, value: prevValue } = prevProps;
        const { selectedRange, value } = this.props;
        const normalizedDate = normalizeDate(value);
        if (formatDate(normalizeDate(prevValue)) !== formatDate(normalizedDate)) {
            this.updateCurrentMonth(normalizedDate);
            this.updateFocusedDate(normalizedDate);
        }
        if (!isSameDatesRange(prevSelectedRange, selectedRange)) {
            this.updateCurrentRange(selectedRange);
        }
    }

    onDayFocus() {
        this.enableNavKeys = true;
    }

    onDayBlur() {
        this.enableNavKeys = false;
    }

    onDayHover(date) {
        const { selectionType, selectedRange } = this.props;
        if (selectionType === 'single' || isEmptyRange(selectedRange)) return;

        const [rangeStart, rangeEnd] = selectedRange;
        if (rangeEnd !== undefined) return;

        const { currentRange } = this.state;
        const [currentRangeStart] = currentRange;

        if (isDateBelowLimit(date, rangeStart)) {
            this.setState({
                currentRange: [currentRangeStart],
            });
        } else {
            this.setState({
                currentRange: [rangeStart, date],
            });
        }
    }

    getContext() {
        const { focusedDate, currentRange, maxRangeEnd } = this.state;
        const { selectionType, selectedRange, disabledDays, highlightedDays } = this.props;
        return {
            focusedDate,
            useAutoFocus: this.enableNavKeys,
            selectionType,
            selectedRange,
            currentRange,
            disabledDays,
            highlightedDays,
            maxRangeEnd,
            privateKeyDown: this.handleKeyDown,
            privateOnFocus: this.onDayFocus,
            privateOnBlur: this.onDayBlur,
            privateOnHover: this.onDayHover,
        };
    }

    moveFocusedDay(increment) {
        const { currentMonth, focusedDate } = this.state;
        let nextFocusedDate = addDays(focusedDate, increment);
        let nextFocusedMonth = currentMonth;

        if (!isSameMonth(nextFocusedDate, currentMonth)) {
            nextFocusedMonth = getFirstDayMonth(addMonths(currentMonth, getSign(increment)));
        }

        const { minDate, maxDate } = this.props;
        const { minCalendarDate, maxCalendarDate } = getCalendarBounds(minDate, maxDate);

        if (isDateBelowLimit(nextFocusedDate, minCalendarDate)) {
            nextFocusedDate = minCalendarDate;
            nextFocusedMonth = getFirstDayMonth(minCalendarDate);
        } else if (isDateBeyondLimit(nextFocusedDate, maxCalendarDate)) {
            nextFocusedDate = maxCalendarDate;
            nextFocusedMonth = getFirstDayMonth(maxCalendarDate);
        }

        this.setState({
            focusedDate: nextFocusedDate,
            currentMonth: nextFocusedMonth,
        });
    }

    moveFocusedMonth(increment) {
        const { focusedDate } = this.state;
        let nextFocusedDate = addMonths(focusedDate, increment);

        const { minDate, maxDate } = this.props;
        const { minCalendarDate, maxCalendarDate } = getCalendarBounds(minDate, maxDate);
        if (isDateBelowLimit(nextFocusedDate, minCalendarDate)) {
            nextFocusedDate = minCalendarDate;
        } else if (isDateBeyondLimit(nextFocusedDate, maxCalendarDate)) {
            nextFocusedDate = maxCalendarDate;
        }

        this.setState({
            focusedDate: nextFocusedDate,
            currentMonth: getFirstDayMonth(nextFocusedDate),
        });
    }

    updateCurrentMonth(value) {
        this.setState({
            currentMonth: getFirstDayMonth(value),
        });
    }

    updateFocusedDate(value) {
        this.setState({
            focusedDate: value,
        });
    }

    updateCurrentRange(value) {
        const { disabledDays: disabledDaysInProps } = this.props;
        const disabledDays = normalizeDates(disabledDaysInProps);

        const maxRangeEnd =
            value.length === 1
                ? Math.min(...disabledDays.filter(day => isDateBeyondLimit(day, value[0])))
                : undefined;
        this.setState({
            currentRange: value,
            maxRangeEnd,
        });
    }

    nextMonth() {
        const { currentMonth } = this.state;
        const newMonth = addMonths(currentMonth, 1);
        const { value } = this.props;
        const focusedDate = getNextFocusedDate(value, newMonth);

        this.setState({
            focusedDate,
            currentMonth: newMonth,
        });
    }

    previousMonth() {
        const { currentMonth } = this.state;
        const newMonth = addMonths(currentMonth, -1);
        const { value } = this.props;
        const focusedDate = getNextFocusedDate(value, newMonth);

        this.setState({
            focusedDate,
            currentMonth: newMonth,
        });
    }

    handleYearChange(event) {
        const { currentMonth } = this.state;
        const year = +event.target.value;
        const newMonth = new Date(currentMonth);
        newMonth.setFullYear(year);

        const { value } = this.props;
        const focusedDate = getNextFocusedDate(value, newMonth);

        this.setState({
            focusedDate,
            currentMonth: newMonth,
        });
    }

    handleKeyDown(event) {
        if (!this.enableNavKeys) return;
        const { keyCode, altKey } = event;
        const keyHandler = altKey ? this.keyHandlerMapAlt : this.keyHandlerMap;
        if (keyHandler[keyCode]) {
            event.preventDefault();
            event.stopPropagation();
            keyHandler[keyCode]();
        }
    }

    handleKeyUpPressed() {
        this.moveFocusedDay(-7);
    }

    handleKeyDownPressed() {
        this.moveFocusedDay(7);
    }

    handleKeyLeftPressed() {
        this.moveFocusedDay(-1);
    }

    handleKeyRightPressed() {
        this.moveFocusedDay(1);
    }

    handleKeyHomePressed() {
        const { focusedDate } = this.state;
        this.moveFocusedDay(-focusedDate.getDay());
    }

    handleKeyEndPressed() {
        const { focusedDate } = this.state;
        this.moveFocusedDay(6 - focusedDate.getDay());
    }

    handleKeyPageUpPressed() {
        this.moveFocusedMonth(-1);
    }

    handleKeyPageDownPressed() {
        this.moveFocusedMonth(1);
    }

    handleKeyAltPageUpPressed() {
        this.moveFocusedMonth(-12);
    }

    handleKeyAltPageDownPressed() {
        this.moveFocusedMonth(12);
    }

    handleKeyEnterPressed() {
        const { onChange, disabledDays } = this.props;
        const { focusedDate } = this.state;
        if (disabledDays.some(day => isSameDay(day, focusedDate))) return;
        onChange(new Date(focusedDate));
    }

    render() {
        const { currentMonth, currentRange } = this.state;
        const { id, value, onChange, minDate, maxDate, className, style, locale } = this.props;
        const formattedMonth = getFormattedMonth(currentMonth, locale);
        const currentYear = currentMonth.getFullYear();
        const yearsRange = getYearsRange({
            minDate,
            maxDate,
            currentMonth: currentMonth.getMonth(),
        });
        const lastYearItem = yearsRange[yearsRange.length - 1];
        const maxSelectableDate = maxDate || new Date(lastYearItem.value, 11, 31);
        const disableNextMonth = addMonths(currentMonth, 1) > maxSelectableDate;
        const minSelectableDate = minDate || new Date(yearsRange[0].value, 0, 1);
        const prevDate = getLastDayMonth(addMonths(currentMonth, -1));
        const disablePreviousMonth = prevDate < minSelectableDate;

        return (
            <section id={id} className={className} style={style} data-calendar-type="single">
                <CalendarHeader
                    variant="single"
                    monthLabelId={this.monthLabelId}
                    formattedMonth={formattedMonth}
                    currentYear={currentYear}
                    yearsRange={yearsRange}
                    disablePreviousMonth={disablePreviousMonth}
                    disableNextMonth={disableNextMonth}
                    onPrevMonthClick={this.previousMonth}
                    onNextMonthClick={this.nextMonth}
                    onYearChange={this.handleYearChange}
                />
                <StyledTable role="grid" aria-labelledby={this.monthLabelId}>
                    <DaysOfWeek locale={locale} />
                    <Provider value={this.getContext()}>
                        <Month
                            value={value}
                            firstDayMonth={currentMonth}
                            minDate={minDate}
                            maxDate={maxDate}
                            onChange={onChange}
                            selectedRange={currentRange}
                        />
                    </Provider>
                </StyledTable>
            </section>
        );
    }
}

SingleCalendar.propTypes = {
    value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]),
    maxDate: PropTypes.instanceOf(Date),
    minDate: PropTypes.instanceOf(Date),
    onChange: PropTypes.func,
    className: PropTypes.string,
    style: PropTypes.object,
    id: PropTypes.string,
    locale: PropTypes.string,
    selectionType: PropTypes.oneOf(['single', 'range']),
    selectedRange: PropTypes.arrayOf(
        PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]),
    ),
    disabledDays: PropTypes.arrayOf(
        PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
    ),
    highlightedDays: PropTypes.arrayOf(
        PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
    ),
};

SingleCalendar.defaultProps = {
    value: undefined,
    minDate: undefined,
    maxDate: undefined,
    onChange: () => {},
    className: undefined,
    style: undefined,
    id: undefined,
    locale: undefined,
    selectionType: 'single',
    selectedRange: undefined,
    disabledDays: [],
    highlightedDays: [],
};

export default SingleCalendar;