@fluentui/react
Version:
Reusable React components for building web experiences.
278 lines (277 loc) • 15.6 kB
JavaScript
import { __assign } from "tslib";
import * as React from 'react';
import { getRTL, classNamesFunction } from '@fluentui/utilities';
import { FocusZone } from '../../FocusZone';
import { getDateRangeArray, getDayGrid, getBoundedDateRange, isRestrictedDate, DAYS_IN_WEEK, compareDates, DateRangeType, } from '@fluentui/date-time-utilities';
import { usePrevious, useId } from '@fluentui/react-hooks';
import { CalendarMonthHeaderRow } from './CalendarMonthHeaderRow';
import { CalendarGridRow } from './CalendarGridRow';
var getClassNames = classNamesFunction();
function useDayRefs() {
var daysRef = React.useRef({});
var getSetRefCallback = function (dayKey) { return function (element) {
if (element === null) {
delete daysRef.current[dayKey];
}
else {
daysRef.current[dayKey] = element;
}
}; };
return [daysRef, getSetRefCallback];
}
function useWeeks(props, onSelectDate, getSetRefCallback) {
/**
* Initial parsing of the given props to generate IDayInfo two dimensional array, which contains a representation
* of every day in the grid. Convenient for helping with conversions between day refs and Date objects in callbacks.
*/
var weeks = React.useMemo(function () {
var _a;
var weeksGrid = getDayGrid(props);
var firstVisibleDay = weeksGrid[1][0].originalDate;
var lastVisibleDay = weeksGrid[weeksGrid.length - 1][6].originalDate;
var markedDays = ((_a = props.getMarkedDays) === null || _a === void 0 ? void 0 : _a.call(props, firstVisibleDay, lastVisibleDay)) || [];
/**
* Weeks is a 2D array. Weeks[0] contains the last week of the prior range,
* Weeks[weeks.length - 1] contains first week of next range. These are for transition states.
*
* Weeks[1... weeks.length - 2] contains the actual visible data
*/
var returnValue = [];
for (var weekIndex = 0; weekIndex < weeksGrid.length; weekIndex++) {
var week = [];
var _loop_1 = function (dayIndex) {
var day = weeksGrid[weekIndex][dayIndex];
var dayInfo = __assign(__assign({ onSelected: function () { return onSelectDate(day.originalDate); }, setRef: getSetRefCallback(day.key) }, day), { isMarked: day.isMarked || (markedDays === null || markedDays === void 0 ? void 0 : markedDays.some(function (markedDay) { return compareDates(day.originalDate, markedDay); })) });
week.push(dayInfo);
};
for (var dayIndex = 0; dayIndex < DAYS_IN_WEEK; dayIndex++) {
_loop_1(dayIndex);
}
returnValue.push(week);
}
return returnValue;
// TODO: this is missing deps on getSetRefCallback and onSelectDate (and depending on the entire
// props object may not be a good idea due to likely frequent mutation). It would be easy to
// fix getSetRefCallback to not mutate every render, but onSelectDate is passed down from
// Calendar and trying to fix it requires a huge cascade of changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props]);
return weeks;
}
/**
* Hook to determine whether to animate the CalendarDayGrid forwards or backwards
* @returns true if the grid should animate backwards; false otherwise
*/
function useAnimateBackwards(weeks) {
var previousNavigatedDate = usePrevious(weeks[0][0].originalDate);
if (!previousNavigatedDate || previousNavigatedDate.getTime() === weeks[0][0].originalDate.getTime()) {
return undefined;
}
else if (previousNavigatedDate <= weeks[0][0].originalDate) {
return false;
}
else {
return true;
}
}
function useWeekCornerStyles(props) {
/**
*
* Section for setting the rounded corner styles on individual day cells. Individual day cells need different
* corners to be rounded depending on which date range type and where the cell is located in the current grid.
* If we just round all of the corners, there isn't a good overlap and we get gaps between contiguous day boxes
* in Edge browser.
*
*/
var getWeekCornerStyles = function (classNames, initialWeeks) {
var weekCornersStyled = {};
/* need to handle setting all of the corners on arbitrarily shaped blobs
__
__|A |
|B |C |__
|D |E |F |
in this case, A needs top left rounded, top right rounded
B needs top left rounded
C doesn't need any rounding
D needs bottom left rounded
E doesn't need any rounding
F needs top right rounding
*/
// cut off the animation transition weeks
var weeks = initialWeeks.slice(1, initialWeeks.length - 1);
// if there's an item above, lose both top corners. Item below, lose both bottom corners, etc.
weeks.forEach(function (week, weekIndex) {
week.forEach(function (day, dayIndex) {
var above = weeks[weekIndex - 1] &&
weeks[weekIndex - 1][dayIndex] &&
isInSameHoverRange(weeks[weekIndex - 1][dayIndex].originalDate, day.originalDate, weeks[weekIndex - 1][dayIndex].isSelected, day.isSelected);
var below = weeks[weekIndex + 1] &&
weeks[weekIndex + 1][dayIndex] &&
isInSameHoverRange(weeks[weekIndex + 1][dayIndex].originalDate, day.originalDate, weeks[weekIndex + 1][dayIndex].isSelected, day.isSelected);
var left = weeks[weekIndex][dayIndex - 1] &&
isInSameHoverRange(weeks[weekIndex][dayIndex - 1].originalDate, day.originalDate, weeks[weekIndex][dayIndex - 1].isSelected, day.isSelected);
var right = weeks[weekIndex][dayIndex + 1] &&
isInSameHoverRange(weeks[weekIndex][dayIndex + 1].originalDate, day.originalDate, weeks[weekIndex][dayIndex + 1].isSelected, day.isSelected);
var style = [];
style.push(calculateRoundedStyles(classNames, above, below, left, right));
style.push(calculateBorderStyles(classNames, above, below, left, right));
weekCornersStyled[weekIndex + '_' + dayIndex] = style.join(' ');
});
});
return weekCornersStyled;
};
var calculateRoundedStyles = function (classNames, above, below, left, right) {
var style = [];
var roundedTopLeft = !above && !left;
var roundedTopRight = !above && !right;
var roundedBottomLeft = !below && !left;
var roundedBottomRight = !below && !right;
if (roundedTopLeft) {
style.push(getRTL() ? classNames.topRightCornerDate : classNames.topLeftCornerDate);
}
if (roundedTopRight) {
style.push(getRTL() ? classNames.topLeftCornerDate : classNames.topRightCornerDate);
}
if (roundedBottomLeft) {
style.push(getRTL() ? classNames.bottomRightCornerDate : classNames.bottomLeftCornerDate);
}
if (roundedBottomRight) {
style.push(getRTL() ? classNames.bottomLeftCornerDate : classNames.bottomRightCornerDate);
}
return style.join(' ');
};
var calculateBorderStyles = function (classNames, above, below, left, right) {
var style = [];
if (!above) {
style.push(classNames.datesAbove);
}
if (!below) {
style.push(classNames.datesBelow);
}
if (!left) {
style.push(getRTL() ? classNames.datesRight : classNames.datesLeft);
}
if (!right) {
style.push(getRTL() ? classNames.datesLeft : classNames.datesRight);
}
return style.join(' ');
};
var isInSameHoverRange = function (date1, date2, date1Selected, date2Selected) {
var dateRangeType = props.dateRangeType, firstDayOfWeek = props.firstDayOfWeek, workWeekDays = props.workWeekDays;
// The hover state looks weird with non-contiguous days in work week view. In work week, show week hover state
var dateRangeHoverType = dateRangeType === DateRangeType.WorkWeek ? DateRangeType.Week : dateRangeType;
// we do not pass daysToSelectInDayView because we handle setting those styles dyanamically in onMouseOver
var dateRange = getDateRangeArray(date1, dateRangeHoverType, firstDayOfWeek, workWeekDays);
if (date1Selected !== date2Selected) {
// if one is selected and the other is not, they can't be in the same range
return false;
}
else if (date1Selected && date2Selected) {
// if they're both selected at the same time they must be in the same range
return true;
}
// otherwise, both must be unselected, so check the dateRange
return dateRange.filter(function (date) { return date.getTime() === date2.getTime(); }).length > 0;
};
return [getWeekCornerStyles, calculateRoundedStyles];
}
export var CalendarDayGridBase = function (props) {
var navigatedDayRef = React.useRef(null);
var activeDescendantId = useId();
var onSelectDate = function (selectedDate) {
var _a, _b;
var firstDayOfWeek = props.firstDayOfWeek, minDate = props.minDate, maxDate = props.maxDate, workWeekDays = props.workWeekDays, daysToSelectInDayView = props.daysToSelectInDayView, restrictedDates = props.restrictedDates;
var restrictedDatesOptions = { minDate: minDate, maxDate: maxDate, restrictedDates: restrictedDates };
var dateRange = getDateRangeArray(selectedDate, dateRangeType, firstDayOfWeek, workWeekDays, daysToSelectInDayView);
dateRange = getBoundedDateRange(dateRange, minDate, maxDate);
dateRange = dateRange.filter(function (d) {
return !isRestrictedDate(d, restrictedDatesOptions);
});
(_a = props.onSelectDate) === null || _a === void 0 ? void 0 : _a.call(props, selectedDate, dateRange);
(_b = props.onNavigateDate) === null || _b === void 0 ? void 0 : _b.call(props, selectedDate, true);
};
var _a = useDayRefs(), daysRef = _a[0], getSetRefCallback = _a[1];
var weeks = useWeeks(props, onSelectDate, getSetRefCallback);
var animateBackwards = useAnimateBackwards(weeks);
var _b = useWeekCornerStyles(props), getWeekCornerStyles = _b[0], calculateRoundedStyles = _b[1];
React.useImperativeHandle(props.componentRef, function () { return ({
focus: function () {
var _a, _b;
(_b = (_a = navigatedDayRef.current) === null || _a === void 0 ? void 0 : _a.focus) === null || _b === void 0 ? void 0 : _b.call(_a);
},
}); }, []);
/**
*
* Section for setting hover/pressed styles. Because we want arbitrary blobs of days to be selectable, to support
* highlighting every day in the month for month view, css :hover style isn't enough, so we need mouse callbacks
* to set classnames on all relevant child refs to apply the styling
*
*/
var getDayInfosInRangeOfDay = function (dayToCompare) {
// The hover state looks weird with non-contiguous days in work week view. In work week, show week hover state
var dateRangeHoverType = getDateRangeTypeToUse(props.dateRangeType, props.workWeekDays);
// gets all the dates for the given date range type that are in the same date range as the given day
var dateRange = getDateRangeArray(dayToCompare.originalDate, dateRangeHoverType, props.firstDayOfWeek, props.workWeekDays, props.daysToSelectInDayView).map(function (date) { return date.getTime(); });
// gets all the day refs for the given dates
var dayInfosInRange = weeks.reduce(function (accumulatedValue, currentWeek) {
return accumulatedValue.concat(currentWeek.filter(function (weekDay) { return dateRange.indexOf(weekDay.originalDate.getTime()) !== -1; }));
}, []);
return dayInfosInRange;
};
var getRefsFromDayInfos = function (dayInfosInRange) {
var dayRefs = [];
dayRefs = dayInfosInRange.map(function (dayInfo) { return daysRef.current[dayInfo.key]; });
return dayRefs;
};
var styles = props.styles, theme = props.theme, className = props.className, dateRangeType = props.dateRangeType, showWeekNumbers = props.showWeekNumbers, labelledBy = props.labelledBy, lightenDaysOutsideNavigatedMonth = props.lightenDaysOutsideNavigatedMonth, animationDirection = props.animationDirection;
var classNames = getClassNames(styles, {
theme: theme,
className: className,
dateRangeType: dateRangeType,
showWeekNumbers: showWeekNumbers,
lightenDaysOutsideNavigatedMonth: lightenDaysOutsideNavigatedMonth === undefined ? true : lightenDaysOutsideNavigatedMonth,
animationDirection: animationDirection,
animateBackwards: animateBackwards,
});
// When the month is highlighted get the corner dates so that styles can be added to them
var weekCorners = getWeekCornerStyles(classNames, weeks);
var partialWeekProps = {
weeks: weeks,
navigatedDayRef: navigatedDayRef,
calculateRoundedStyles: calculateRoundedStyles,
activeDescendantId: activeDescendantId,
classNames: classNames,
weekCorners: weekCorners,
getDayInfosInRangeOfDay: getDayInfosInRangeOfDay,
getRefsFromDayInfos: getRefsFromDayInfos,
};
return (React.createElement(FocusZone, { className: classNames.wrapper, preventDefaultWhenHandled: true },
React.createElement("table", { className: classNames.table, "aria-multiselectable": "false", "aria-labelledby": labelledBy, "aria-activedescendant": activeDescendantId, role: "grid" },
React.createElement("tbody", null,
React.createElement(CalendarMonthHeaderRow, __assign({}, props, { classNames: classNames, weeks: weeks })),
React.createElement(CalendarGridRow, __assign({}, props, partialWeekProps, { week: weeks[0], weekIndex: -1, rowClassName: classNames.firstTransitionWeek, ariaRole: "presentation", ariaHidden: true })),
weeks.slice(1, weeks.length - 1).map(function (week, weekIndex) { return (React.createElement(CalendarGridRow, __assign({}, props, partialWeekProps, { key: weekIndex, week: week, weekIndex: weekIndex, rowClassName: classNames.weekRow }))); }),
React.createElement(CalendarGridRow, __assign({}, props, partialWeekProps, { week: weeks[weeks.length - 1], weekIndex: -2, rowClassName: classNames.lastTransitionWeek, ariaRole: "presentation", ariaHidden: true }))))));
};
CalendarDayGridBase.displayName = 'CalendarDayGridBase';
/**
* When given work week, if the days are non-contiguous, the hover states look really weird. So for non-contiguous
* work weeks, we'll just show week view instead.
*/
function getDateRangeTypeToUse(dateRangeType, workWeekDays) {
if (workWeekDays && dateRangeType === DateRangeType.WorkWeek) {
var sortedWWDays = workWeekDays.slice().sort();
var isContiguous = true;
for (var i = 1; i < sortedWWDays.length; i++) {
if (sortedWWDays[i] !== sortedWWDays[i - 1] + 1) {
isContiguous = false;
break;
}
}
if (!isContiguous || workWeekDays.length === 0) {
return DateRangeType.Week;
}
}
return dateRangeType;
}
//# sourceMappingURL=CalendarDayGrid.base.js.map