Skip to content

Commit

Permalink
feat: add selectionType prop to Calendar component (#1563)
Browse files Browse the repository at this point in the history
* feat: add selectionType prop to Calendar component

* fix: code cleanup

* fix: fix render when selection type is range

* style: fix range highlight

* style: fix range highlight

* style: fix range highlight when range start date is the same as range end date

* fix: fix range highlight when hovered

* style: fix styles

* fix: fix comments

* fix: some code enhancements

* fix: fix index.d.ts

* fix: issues in comments

* fix: fix buildNewRangeFromValue helper

* styles: range color

* styles: hover flicking

* fix: indentation

* style: fix highlight background when range starts on saturday and end in sunday

* fix: fix range selection when selected end date is previous to range start date

* fix: fix comments

Co-authored-by: Tahimi <tahimileon@gmail.com>
Co-authored-by: TahimiLeonBravo <tahimi@nexxtway.com>
Co-authored-by: Maxx Greene <56657888+maxxgreene@users.noreply.github.com>
  • Loading branch information
4 people committed May 22, 2020
1 parent aad2190 commit 18a894f
Show file tree
Hide file tree
Showing 27 changed files with 998 additions and 470 deletions.
3 changes: 2 additions & 1 deletion src/components/Calendar/context.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react';

export const { Provider, Consumer } = React.createContext();
export const CalendarContext = React.createContext();
export const { Provider, Consumer } = CalendarContext;
63 changes: 50 additions & 13 deletions src/components/Calendar/day.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,36 @@ import { Consumer } from './context';
import StyledDay from './styled/day';
import StyledDayAdjacent from './styled/dayAdjacent';
import StyledDayButton from './styled/dayButton';
import StyledRangeHighlight from './styled/rangeHighlight';
import { isSameDay, compareDates } from './helpers';
import { useRangeStartDate, useRangeEndDate } from './hooks';

function DayComponent(props) {
const { date, firstDayMonth, isSelected, minDate, maxDate, onChange } = props;
const { useAutoFocus, focusedDate, privateKeyDown, privateOnFocus, privateOnBlur } = props;
const {
date,
firstDayMonth,
isSelected,
minDate,
maxDate,
onChange,
isWithinRange,
isFirstDayOfWeek,
isLastDayOfWeek,
useAutoFocus,
focusedDate,
currentRange,
privateKeyDown,
privateOnFocus,
privateOnBlur,
privateOnHover,
} = props;
const day = date.getDate();
const isAdjacentDate = date.getMonth() !== firstDayMonth.getMonth();
const isDisabled = compareDates(date, maxDate) > 0 || compareDates(date, minDate) < 0;
const tabIndex = isSameDay(focusedDate, date) ? 0 : -1;
const buttonRef = useRef();
const isRangeStartDate = useRangeStartDate(date, currentRange);
const isRangeEndDate = useRangeEndDate(date, currentRange);

useEffect(() => {
if (!useAutoFocus || !buttonRef.current || tabIndex === -1) return;
Expand All @@ -31,18 +51,29 @@ function DayComponent(props) {

return (
<StyledDay role="gridcell">
<StyledDayButton
ref={buttonRef}
tabIndex={tabIndex}
onClick={() => onChange(new Date(date))}
isSelected={isSelected}
data-selected={isSelected}
onKeyDown={privateKeyDown}
onFocus={privateOnFocus}
onBlur={privateOnBlur}
<StyledRangeHighlight
isVisible={isWithinRange && !(isRangeStartDate && isRangeEndDate)}
isFirstInRange={isRangeStartDate}
isLastInRange={isRangeEndDate}
isFirstDayOfWeek={isFirstDayOfWeek}
isLastDayOfWeek={isLastDayOfWeek}
>
{day}
</StyledDayButton>
<StyledDayButton
ref={buttonRef}
tabIndex={tabIndex}
onClick={() => onChange(date)}
onMouseEnter={() => privateOnHover(date)}
isSelected={isSelected}
isHovered={!isSelected && isRangeEndDate}
data-selected={isSelected}
onKeyDown={privateKeyDown}
onFocus={privateOnFocus}
onBlur={privateOnBlur}
isWithinRange={isWithinRange}
>
{day}
</StyledDayButton>
</StyledRangeHighlight>
</StyledDay>
);
}
Expand All @@ -57,6 +88,9 @@ Day.propTypes = {
minDate: PropTypes.instanceOf(Date),
maxDate: PropTypes.instanceOf(Date),
isSelected: PropTypes.bool,
isWithinRange: PropTypes.bool,
isFirstDayOfWeek: PropTypes.bool,
isLastDayOfWeek: PropTypes.bool,
onChange: PropTypes.func,
};

Expand All @@ -66,5 +100,8 @@ Day.defaultProps = {
minDate: undefined,
maxDate: undefined,
isSelected: false,
isWithinRange: false,
isFirstDayOfWeek: false,
isLastDayOfWeek: false,
onChange: () => {},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import buildNewRangeFromValue from '../buildNewRangeFromValue';

describe('buildNewRangeFromValue', () => {
it('should return array with single date', () => {
const value = new Date();
const ranges = [undefined, null, []];
ranges.forEach(range => {
expect(buildNewRangeFromValue(value, range)).toEqual({
range: [value],
});
});
});
it('should return array with two dates', () => {
const date1 = new Date(2019, 2, 1);
const date2 = new Date(2019, 21, 1);
const range = [date1];
expect(buildNewRangeFromValue(date2, range)).toEqual({
range: [date1, date2],
});
});
it('should return array with two dates and date3 as first date', () => {
const date1 = new Date(2019, 0, 2);
const date2 = new Date(2019, 0, 21);
const date3 = new Date(2019, 0, 15);
const range = [date1, date2];
expect(buildNewRangeFromValue(date3, range)).toEqual({
range: [date3, date2],
nextUpdatePosition: 1,
});
});
});
17 changes: 17 additions & 0 deletions src/components/Calendar/helpers/__test__/isDateWithinRange.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import isDateWithinRange from '../isDateWithinRange';

describe('isDateWithinRange', () => {
it('should return false', () => {
const range = [new Date(2019, 2, 1, 0, 0, 0, 600), new Date(2019, 15, 1, 23, 12, 34, 600)];
expect(isDateWithinRange(null, range)).toBe(false);
expect(isDateWithinRange(undefined, range)).toBe(false);
expect(isDateWithinRange(new Date(2019, 1, 1), range)).toBe(false);
expect(isDateWithinRange(new Date(2019, 18, 1), range)).toBe(false);
});
it('should return true', () => {
const range = [new Date(2019, 2, 1), new Date(2019, 15, 1)];
expect(isDateWithinRange(new Date(2019, 2, 1), range)).toBe(true);
expect(isDateWithinRange(new Date(2019, 13, 1), range)).toBe(true);
expect(isDateWithinRange(new Date(2019, 15, 1), range)).toBe(true);
});
});
16 changes: 16 additions & 0 deletions src/components/Calendar/helpers/__test__/isEmptyRange.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import isEmptyRange from '../isEmptyRange';

describe('isEmptyRange', () => {
it('should return true', () => {
const ranges = [null, undefined, [], {}, 0, [undefined], [undefined, undefined]];
ranges.forEach(range => {
expect(isEmptyRange(range)).toBe(true);
});
});
it('should return false', () => {
const ranges = [[1], [1, 2]];
ranges.forEach(range => {
expect(isEmptyRange(range)).toBe(false);
});
});
});
44 changes: 44 additions & 0 deletions src/components/Calendar/helpers/__test__/isSameDatesRange.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import isSameDatesRange from '../isSameDatesRange';

describe('isSameMonth', () => {
it('should return true', () => {
const range1 = [
null,
undefined,
[],
[new Date(2019, 1, 1)],
[new Date(2019, 1, 1), new Date(2019, 1, 10)],
];
const range2 = [
null,
undefined,
[],
[new Date(2019, 1, 1)],
[new Date(2019, 1, 1), new Date(2019, 1, 10)],
];

range1.forEach((value, index) => {
expect(isSameDatesRange(value, range2[index])).toBe(true);
});
});
it('should return false', () => {
const range1 = [
null,
undefined,
[],
[new Date(2019, 1, 1)],
[new Date(2019, 1, 1), new Date(2019, 1, 10)],
];
const range2 = [
undefined,
[],
null,
[new Date(2019, 1, 1), new Date(2019, 1, 10)],
[new Date(2019, 1, 1)],
];

range1.forEach((value, index) => {
expect(isSameDatesRange(value, range2[index])).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import shouldDateBeSelected from '../shouldDateBeSelected';

describe('shouldDateBeSelected', () => {
it('should return true when selectionType is single and date is the same as value', () => {
const date = new Date(2019, 1, 1);
const value = new Date(2019, 1, 1);
const range = [new Date(2019, 1, 1), new Date(2019, 1, 15)];
expect(shouldDateBeSelected(date, value, 'single', undefined)).toBe(true);
expect(shouldDateBeSelected(date, value, 'single', null)).toBe(true);
expect(shouldDateBeSelected(date, value, 'single', [])).toBe(true);
expect(shouldDateBeSelected(date, value, 'single', range)).toBe(true);
});
it('should return true when selectionType is range and date is same as range boundaries', () => {
const date1 = new Date(2019, 1, 1);
const date2 = new Date(2019, 1, 15);
const value = new Date(2019, 1, 1);
const range = [new Date(2019, 1, 1), new Date(2019, 1, 15)];
expect(shouldDateBeSelected(date1, undefined, 'range', range)).toBe(true);
expect(shouldDateBeSelected(date1, null, 'range', range)).toBe(true);
expect(shouldDateBeSelected(date1, value, 'range', range)).toBe(true);
expect(shouldDateBeSelected(date2, value, 'range', range)).toBe(true);
});
it('should return false when selectionType is single and date is not the same as value', () => {
const date = new Date(2019, 1, 12);
const value = new Date(2019, 1, 1);
const range = [new Date(2019, 1, 1), new Date(2019, 1, 15)];
expect(shouldDateBeSelected(date, value, 'single', undefined)).toBe(false);
expect(shouldDateBeSelected(date, value, 'single', null)).toBe(false);
expect(shouldDateBeSelected(date, value, 'single', [])).toBe(false);
expect(shouldDateBeSelected(date, value, 'single', range)).toBe(false);
});
it('should return false when selectionType is range and date is not the same as range boundaries', () => {
const date1 = new Date(2019, 1, 2);
const date2 = new Date(2019, 1, 12);
const value = new Date(2019, 1, 1);
const range = [new Date(2019, 1, 1), new Date(2019, 1, 15)];
expect(shouldDateBeSelected(date1, undefined, 'range', range)).toBe(false);
expect(shouldDateBeSelected(date1, null, 'range', range)).toBe(false);
expect(shouldDateBeSelected(date1, value, 'range', range)).toBe(false);
expect(shouldDateBeSelected(date2, value, 'range', range)).toBe(false);
});
});
39 changes: 39 additions & 0 deletions src/components/Calendar/helpers/buildNewRangeFromValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import isDateWithinRange from './isDateWithinRange';
import compareDates from './compareDates';

export default function buildNewRangeFromValue(value, currentRange, currentUpdatePosition = 0) {
if (!currentRange || currentRange.length === 0)
return {
range: [value],
};

const [rangeStart, rangeEnd] = currentRange;

if (!rangeEnd) {
if (compareDates(value, rangeStart) >= 0) {
return {
range: [rangeStart, value],
};
}
return {
range: [value],
};
}

if (isDateWithinRange(value, currentRange)) {
if (currentUpdatePosition === 0) {
return {
range: [value, rangeEnd],
nextUpdatePosition: 1,
};
}
return {
range: [rangeStart, value],
nextUpdatePosition: 0,
};
}

return {
range: [value],
};
}
63 changes: 24 additions & 39 deletions src/components/Calendar/helpers/index.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,24 @@
import addDays from './addDays';
import addMonths from './addMonths';
import formatDate from './formatDate';
import getFirstDayMonth from './getFirstDayMonth';
import getFormattedMonth from './getFormattedMonth';
import getLastDayMonth from './getLastDayMonth';
import getYearsRange from './getYearsRange';
import isSameDay from './isSameDay';
import normalizeDate from './normalizeDate';
import getFormattedDayName from './getFormattedDayName';
import compareDates from './compareDates';
import isSameMonth from './isSameMonth';
import isSameYear from './isSameYear';
import getSign from './getSign';
import getCalendarBounds from './getCalendarBounds';
import isDateBelowLimit from './isDateBelowLimit';
import isDateBeyondLimit from './isDateBeyondLimit';
import getNextFocusedDate from './getNextFocusedDate';

export {
addDays,
addMonths,
formatDate,
getFirstDayMonth,
getFormattedMonth,
getLastDayMonth,
getYearsRange,
isSameDay,
normalizeDate,
getFormattedDayName,
compareDates,
isSameMonth,
isSameYear,
getSign,
getCalendarBounds,
isDateBelowLimit,
isDateBeyondLimit,
getNextFocusedDate,
};
export { default as addDays } from './addDays';
export { default as addMonths } from './addMonths';
export { default as formatDate } from './formatDate';
export { default as getFirstDayMonth } from './getFirstDayMonth';
export { default as getFormattedMonth } from './getFormattedMonth';
export { default as getLastDayMonth } from './getLastDayMonth';
export { default as getYearsRange } from './getYearsRange';
export { default as isSameDay } from './isSameDay';
export { default as normalizeDate } from './normalizeDate';
export { default as normalizeRange } from './normalizeRange';
export { default as getFormattedDayName } from './getFormattedDayName';
export { default as compareDates } from './compareDates';
export { default as isSameMonth } from './isSameMonth';
export { default as isSameYear } from './isSameYear';
export { default as getSign } from './getSign';
export { default as getCalendarBounds } from './getCalendarBounds';
export { default as isDateBelowLimit } from './isDateBelowLimit';
export { default as isDateBeyondLimit } from './isDateBeyondLimit';
export { default as getNextFocusedDate } from './getNextFocusedDate';
export { default as isDateWithinRange } from './isDateWithinRange';
export { default as buildNewRangeFromValue } from './buildNewRangeFromValue';
export { default as shouldDateBeSelected } from './shouldDateBeSelected';
export { default as isSameDatesRange } from './isSameDatesRange';
export { default as isEmptyRange } from './isEmptyRange';
9 changes: 9 additions & 0 deletions src/components/Calendar/helpers/isDateWithinRange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import compareDates from './compareDates';

export default function isDateWithinRange(date, range) {
if (date && Array.isArray(range) && range.length > 1) {
const [rangeStart, rangeEnd] = range;
return compareDates(date, rangeStart) >= 0 && compareDates(date, rangeEnd) <= 0;
}
return false;
}
6 changes: 6 additions & 0 deletions src/components/Calendar/helpers/isEmptyRange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default function isEmptyRange(range) {
if ([null, undefined, [], {}].includes(range)) return true;
if (!Array.isArray(range) || range.length === 0) return true;
const nullFilteredRange = range.filter(item => !!item);
return nullFilteredRange.length === 0;
}
3 changes: 3 additions & 0 deletions src/components/Calendar/helpers/isSameDatesRange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function isSameDatesRange(range1, range2) {
return JSON.stringify(range1) === JSON.stringify(range2);
}
7 changes: 7 additions & 0 deletions src/components/Calendar/helpers/normalizeRange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import normalizeDate from './normalizeDate';

export default function normalizeRange(range) {
if (!range) return [];
if (!Array.isArray(range)) return [normalizeDate(range)];
return range.map(date => normalizeDate(date));
}
Loading

0 comments on commit 18a894f

Please sign in to comment.