Skip to content

Commit b65541c

Browse files
daltonesMaja Wichrowska
authored andcommitted
Feature: prop firstDayOfWeek
1 parent 19a10f7 commit b65541c

File tree

10 files changed

+199
-37
lines changed

10 files changed

+199
-37
lines changed

src/components/CalendarMonth.jsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const propTypes = forbidExtraProps({
3838
onDayMouseLeave: PropTypes.func,
3939
renderMonth: PropTypes.func,
4040
renderDay: PropTypes.func,
41+
firstDayOfWeek: PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6]),
4142

4243
focusedDate: momentPropTypes.momentObj, // indicates focusable day
4344
isFocused: PropTypes.bool, // indicates whether or not to move focus to focusable day
@@ -59,6 +60,7 @@ const defaultProps = {
5960
onDayMouseLeave() {},
6061
renderMonth: null,
6162
renderDay: null,
63+
firstDayOfWeek: null,
6264

6365
focusedDate: null,
6466
isFocused: false,
@@ -71,16 +73,27 @@ const defaultProps = {
7173
export default class CalendarMonth extends React.Component {
7274
constructor(props) {
7375
super(props);
76+
7477
this.state = {
75-
weeks: getCalendarMonthWeeks(props.month, props.enableOutsideDays),
78+
weeks: getCalendarMonthWeeks(
79+
props.month,
80+
props.enableOutsideDays,
81+
props.firstDayOfWeek === null ? moment.localeData().firstDayOfWeek() : props.firstDayOfWeek,
82+
),
7683
};
7784
}
7885

7986
componentWillReceiveProps(nextProps) {
80-
const { month, enableOutsideDays } = nextProps;
81-
if (!month.isSame(this.props.month)) {
87+
const { month, enableOutsideDays, firstDayOfWeek } = nextProps;
88+
if (!month.isSame(this.props.month)
89+
|| enableOutsideDays !== this.props.enableOutsideDays
90+
|| firstDayOfWeek !== this.props.firstDayOfWeek) {
8291
this.setState({
83-
weeks: getCalendarMonthWeeks(month, enableOutsideDays),
92+
weeks: getCalendarMonthWeeks(
93+
month,
94+
enableOutsideDays,
95+
firstDayOfWeek === null ? moment.localeData().firstDayOfWeek() : firstDayOfWeek,
96+
),
8497
});
8598
}
8699
}

src/components/CalendarMonthGrid.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22
import PropTypes from 'prop-types';
33
import shallowCompare from 'react-addons-shallow-compare';
44
import momentPropTypes from 'react-moment-proptypes';
5-
import { forbidExtraProps, nonNegativeInteger } from 'airbnb-prop-types';
5+
import { forbidExtraProps, nonNegativeInteger, range } from 'airbnb-prop-types';
66
import moment from 'moment';
77
import cx from 'classnames';
88
import { addEventListener, removeEventListener } from 'consolidated-events';
@@ -45,6 +45,7 @@ const propTypes = forbidExtraProps({
4545
daySize: nonNegativeInteger,
4646
focusedDate: momentPropTypes.momentObj, // indicates focusable day
4747
isFocused: PropTypes.bool, // indicates whether or not to move focus to focusable day
48+
firstDayOfWeek: range(0, 7),
4849

4950
// i18n
5051
monthFormat: PropTypes.string,
@@ -69,6 +70,7 @@ const defaultProps = {
6970
daySize: DAY_SIZE,
7071
focusedDate: null,
7172
isFocused: false,
73+
firstDayOfWeek: null,
7274

7375
// i18n
7476
monthFormat: 'MMMM YYYY', // english locale
@@ -175,6 +177,7 @@ export default class CalendarMonthGrid extends React.Component {
175177
renderMonth,
176178
renderDay,
177179
onMonthTransitionEnd,
180+
firstDayOfWeek,
178181
focusedDate,
179182
isFocused,
180183
phrases,
@@ -228,6 +231,7 @@ export default class CalendarMonthGrid extends React.Component {
228231
onDayClick={onDayClick}
229232
renderMonth={renderMonth}
230233
renderDay={renderDay}
234+
firstDayOfWeek={firstDayOfWeek}
231235
daySize={daySize}
232236
focusedDate={isVisible ? focusedDate : null}
233237
isFocused={isFocused}

src/components/DateRangePicker.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const defaultProps = {
7070
hideKeyboardShortcutsPanel: false,
7171
daySize: DAY_SIZE,
7272
isRTL: false,
73+
firstDayOfWeek: null,
7374

7475
// navigation related props
7576
navPrev: null,
@@ -305,6 +306,7 @@ export default class DateRangePicker extends React.Component {
305306
keepOpenOnDateSelect,
306307
renderDay,
307308
renderCalendarInfo,
309+
firstDayOfWeek,
308310
initialVisibleMonth,
309311
hideKeyboardShortcutsPanel,
310312
customCloseIcon,
@@ -362,6 +364,7 @@ export default class DateRangePicker extends React.Component {
362364
onBlur={this.onDayPickerBlur}
363365
phrases={phrases}
364366
isRTL={isRTL}
367+
firstDayOfWeek={firstDayOfWeek}
365368
/>
366369

367370
{withFullScreenPortal && (

src/components/DayPicker.jsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22
import PropTypes from 'prop-types';
33
import shallowCompare from 'react-addons-shallow-compare';
44
import ReactDOM from 'react-dom';
5-
import { forbidExtraProps, nonNegativeInteger } from 'airbnb-prop-types';
5+
import { forbidExtraProps, nonNegativeInteger, range } from 'airbnb-prop-types';
66
import moment from 'moment';
77
import cx from 'classnames';
88
import throttle from 'lodash/throttle';
@@ -48,6 +48,7 @@ const propTypes = forbidExtraProps({
4848
onOutsideClick: PropTypes.func,
4949
hidden: PropTypes.bool,
5050
initialVisibleMonth: PropTypes.func,
51+
firstDayOfWeek: range(0, 7),
5152
renderCalendarInfo: PropTypes.func,
5253
hideKeyboardShortcutsPanel: PropTypes.bool,
5354
daySize: nonNegativeInteger,
@@ -90,6 +91,7 @@ export const defaultProps = {
9091
onOutsideClick() {},
9192
hidden: false,
9293
initialVisibleMonth: () => moment(),
94+
firstDayOfWeek: null,
9395
renderCalendarInfo: null,
9496
hideKeyboardShortcutsPanel: false,
9597
daySize: DAY_SIZE,
@@ -682,11 +684,16 @@ export default class DayPicker extends React.Component {
682684
style = verticalStyle;
683685
}
684686

687+
let { firstDayOfWeek } = this.props;
688+
if (firstDayOfWeek === null) {
689+
firstDayOfWeek = moment.localeData().firstDayOfWeek();
690+
}
691+
685692
const header = [];
686693
for (let i = 0; i < 7; i += 1) {
687694
header.push(
688695
<li key={i} style={{ width: daySize }}>
689-
<small>{moment().weekday(i).format('dd')}</small>
696+
<small>{moment().day((i + firstDayOfWeek) % 7).format('dd')}</small>
690697
</li>,
691698
);
692699
}
@@ -725,6 +732,7 @@ export default class DayPicker extends React.Component {
725732
onDayClick,
726733
onDayMouseEnter,
727734
onDayMouseLeave,
735+
firstDayOfWeek,
728736
renderMonth,
729737
renderDay,
730738
renderCalendarInfo,
@@ -842,6 +850,7 @@ export default class DayPicker extends React.Component {
842850
onMonthTransitionEnd={this.updateStateAfterMonthTransition}
843851
monthFormat={monthFormat}
844852
daySize={daySize}
853+
firstDayOfWeek={firstDayOfWeek}
845854
isFocused={shouldFocusDate}
846855
focusedDate={focusedDate}
847856
phrases={phrases}

src/components/DayPickerRangeController.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const propTypes = forbidExtraProps({
6767
onOutsideClick: PropTypes.func,
6868
renderDay: PropTypes.func,
6969
renderCalendarInfo: PropTypes.func,
70+
firstDayOfWeek: PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6]),
7071

7172
// accessibility
7273
onBlur: PropTypes.func,
@@ -114,6 +115,7 @@ const defaultProps = {
114115

115116
renderDay: null,
116117
renderCalendarInfo: null,
118+
firstDayOfWeek: null,
117119

118120
// accessibility
119121
onBlur() {},
@@ -806,6 +808,7 @@ export default class DayPickerRangeController extends React.Component {
806808
onOutsideClick,
807809
withPortal,
808810
enableOutsideDays,
811+
firstDayOfWeek,
809812
hideKeyboardShortcutsPanel,
810813
daySize,
811814
focusedInput,
@@ -843,6 +846,7 @@ export default class DayPickerRangeController extends React.Component {
843846
navNext={navNext}
844847
renderDay={renderDay}
845848
renderCalendarInfo={renderCalendarInfo}
849+
firstDayOfWeek={firstDayOfWeek}
846850
hideKeyboardShortcutsPanel={hideKeyboardShortcutsPanel}
847851
isFocused={isFocused}
848852
getFirstFocusableDay={this.getFirstFocusableDay}

src/components/SingleDatePicker.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const defaultProps = {
5757
withPortal: false,
5858
withFullScreenPortal: false,
5959
initialVisibleMonth: null,
60+
firstDayOfWeek: null,
6061
numberOfMonths: 2,
6162
keepOpenOnDateSelect: false,
6263
reopenPickerOnClearDate: false,
@@ -315,6 +316,9 @@ export default class SingleDatePicker extends React.Component {
315316
renderDay,
316317
renderCalendarInfo,
317318
hideKeyboardShortcutsPanel,
319+
firstDayOfWeek,
320+
date,
321+
initialVisibleMonth,
318322
customCloseIcon,
319323
phrases,
320324
daySize,
@@ -360,6 +364,7 @@ export default class SingleDatePicker extends React.Component {
360364
isOutsideRange={isOutsideRange}
361365
isDayBlocked={isDayBlocked}
362366
isDayHighlighted={isDayHighlighted}
367+
firstDayOfWeek={firstDayOfWeek}
363368
/>
364369

365370
{withFullScreenPortal && (

src/shapes/DateRangePickerShape.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import PropTypes from 'prop-types';
22
import momentPropTypes from 'react-moment-proptypes';
3-
import { nonNegativeInteger } from 'airbnb-prop-types';
3+
import { nonNegativeInteger, range } from 'airbnb-prop-types';
44

55
import { DateRangePickerPhrases } from '../defaultPhrases';
66
import getPhrasePropTypes from '../utils/getPhrasePropTypes';
@@ -44,7 +44,7 @@ export default {
4444
withFullScreenPortal: PropTypes.bool,
4545
daySize: nonNegativeInteger,
4646
isRTL: PropTypes.bool,
47-
47+
firstDayOfWeek: range(0, 7),
4848
initialVisibleMonth: PropTypes.func,
4949
numberOfMonths: PropTypes.number,
5050
keepOpenOnDateSelect: PropTypes.bool,

src/shapes/SingleDatePickerShape.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import PropTypes from 'prop-types';
22
import momentPropTypes from 'react-moment-proptypes';
3-
import { nonNegativeInteger } from 'airbnb-prop-types';
3+
import { nonNegativeInteger, range } from 'airbnb-prop-types';
44

55
import { SingleDatePickerPhrases } from '../defaultPhrases';
66
import getPhrasePropTypes from '../utils/getPhrasePropTypes';
@@ -36,6 +36,7 @@ export default {
3636
withPortal: PropTypes.bool,
3737
withFullScreenPortal: PropTypes.bool,
3838
initialVisibleMonth: PropTypes.func,
39+
firstDayOfWeek: range(0,7),
3940
numberOfMonths: PropTypes.number,
4041
keepOpenOnDateSelect: PropTypes.bool,
4142
reopenPickerOnClearDate: PropTypes.bool,

src/utils/getCalendarMonthWeeks.js

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,48 @@
1-
export default function getCalendarMonthWeeks(month, enableOutsideDays) {
2-
// set utc offset to get correct dates in future (when timezone changes)
3-
const baseDate = month.clone();
4-
const firstOfMonth = baseDate.clone().startOf('month').hour(12);
5-
const lastOfMonth = baseDate.clone().endOf('month').hour(12);
1+
import moment from 'moment';
62

7-
const currentDay = firstOfMonth.clone();
8-
let currentWeek = [];
9-
const weeksInMonth = [];
3+
const WEEKDAYS = [0, 1, 2, 3, 4, 5, 6];
104

11-
// days belonging to the previous month
12-
for (let i = 0; i < currentDay.weekday(); i += 1) {
13-
const prevDay = enableOutsideDays ? currentDay.clone().subtract(i + 1, 'day') : null;
14-
currentWeek.unshift(prevDay);
5+
export default function getCalendarMonthWeeks(
6+
month,
7+
enableOutsideDays,
8+
firstDayOfWeek = moment.localeData().firstDayOfWeek(),
9+
) {
10+
if (!moment.isMoment(month) || !month.isValid()) {
11+
throw new TypeError('`month` must be a valid moment object');
12+
}
13+
if (WEEKDAYS.indexOf(firstDayOfWeek) === -1) {
14+
throw new TypeError('`firstDayOfWeek` must be an integer between 0 and 6');
1515
}
1616

17-
while (currentDay < lastOfMonth) {
18-
currentWeek.push(currentDay.clone());
19-
currentDay.add(1, 'd');
17+
// set utc offset to get correct dates in future (when timezone changes)
18+
const firstOfMonth = month.clone().startOf('month').hour(12);
19+
const lastOfMonth = month.clone().endOf('month').hour(12);
20+
21+
// calculate the exact first and last days to fill the entire matrix
22+
// (considering days outside month)
23+
const prevDays = ((firstOfMonth.day() + 7 - firstDayOfWeek) % 7);
24+
const nextDays = ((firstDayOfWeek + 6 - lastOfMonth.day()) % 7);
25+
const firstDay = firstOfMonth.clone().subtract(prevDays, 'day');
26+
const lastDay = lastOfMonth.clone().add(nextDays, 'day');
2027

21-
if (currentDay.weekday() === 0) {
22-
weeksInMonth.push(currentWeek);
23-
currentWeek = [];
28+
const totalDays = lastDay.diff(firstDay, 'days') + 1;
29+
30+
const currentDay = firstDay.clone();
31+
const weeksInMonth = [];
32+
33+
for (let i = 0; i < totalDays; i += 1) {
34+
if (i % 7 === 0) {
35+
weeksInMonth.push([]);
2436
}
25-
}
2637

27-
// weekday() returns the index of the day of the week according to the locale
28-
// this means if the week starts on Monday, weekday() will return 0 for a Monday date, not 1
29-
if (currentDay.weekday() !== 0) {
30-
// days belonging to the next month
31-
for (let k = currentDay.weekday(), count = 0; k < 7; k += 1, count += 1) {
32-
const nextDay = enableOutsideDays ? currentDay.clone().add(count, 'day') : null;
33-
currentWeek.push(nextDay);
38+
let day = null;
39+
if ((i >= prevDays && i < (totalDays - nextDays)) || enableOutsideDays) {
40+
day = currentDay.clone();
3441
}
3542

36-
weeksInMonth.push(currentWeek);
43+
weeksInMonth[weeksInMonth.length - 1].push(day);
44+
45+
currentDay.add(1, 'day');
3746
}
3847

3948
return weeksInMonth;

0 commit comments

Comments
 (0)