diff --git a/docs/DateUtils.md b/docs/DateUtils.md index d284d8765c..7cc9c95841 100644 --- a/docs/DateUtils.md +++ b/docs/DateUtils.md @@ -62,3 +62,7 @@ console.log(newRange.from) // 2015-05-24 ### isDayInRange `(day: Date, range: object) ⇒ Bool` Returns `true` if `day` is included in the specified range of days. See the [range example](http://react-day-picker.js.org/examples?range) for an example using this function. + +### getWeekNumber `(day: Date) ⇒ Number` + +Returns the year's week number (as per ISO, i.e. with the week starting from Monday) for the given day. diff --git a/docs/DayPickerAPI.md b/docs/DayPickerAPI.md index ceacce47d6..41f328b435 100644 --- a/docs/DayPickerAPI.md +++ b/docs/DayPickerAPI.md @@ -10,7 +10,7 @@ import DayPicker from 'react-day-picker'; **Day Modifiers**: [selectedDays](#selecteddays), [disabledDays](#disableddays), [modifiers](#modifiers), [modifiersStyles](#modifiersstyles) -**Customization**: [enableOutsideDays](#enableoutsidedays), [fixedWeeks](#fixedweeks), [todayButton](#todaybutton) +**Customization**: [enableOutsideDays](#enableoutsidedays), [fixedWeeks](#fixedweeks), [showWeekNumbers](#showweeknumbers), [todayButton](#todaybutton) **Localization**: [dir](#dir), [firstDayOfWeek](#firstdayofweek), [labels](#labels), [locale](#locale), [localeUtils](#localeUtils), [months](#months), [weekdaysLong](#weekdayslong), [weekdaysShort](#weekdaysshort) @@ -279,6 +279,12 @@ Indicate which day should appear as selected. Set a `selected` modifier. See [Modifiers](Modifiers.md) for a reference of the accepted values. +### showWeekNumbers + +**Type**: `Boolean` + +Display the year's week number next to each week ([example](../examples/?weekNumbers)). + ### todayButton **Type**: `String` @@ -386,6 +392,12 @@ Event handler when the calendar get the `keydown` event Event handler when the month is changed, i.e. clicking the navigation buttons or using the keyboard. +### onWeekClick + +**Type**: `(weekNumber: Number, days: Date[], e: SynteticEvent) ⇒ void` + +Event hander when the user clicks on a week number (when [showWeekNumbers](#showweeknumbers) is set to `true`). + ## Component methods ### showMonth diff --git a/examples/src/Examples.js b/examples/src/Examples.js index 6e044965da..4c3e6e9447 100644 --- a/examples/src/Examples.js +++ b/examples/src/Examples.js @@ -25,6 +25,7 @@ import SelectableDay from './examples/SelectableDay'; import TodayButton from './examples/TodayButton'; import SimpleCalendar from './examples/SimpleCalendar'; import SimpleInput from './examples/SimpleInput'; +import WeekNumbers from './examples/WeekNumbers'; import YearCalendar from './examples/YearCalendar'; import YearNavigation from './examples/YearNavigation'; @@ -96,6 +97,11 @@ const EXAMPLES = { description: 'Use todayButton to display a today button.', Component: TodayButton, }, + weekNumbers: { + title: 'Selecting an entire week', + description: 'Use the weekNumbers prop to display the number of each week.', + Component: WeekNumbers, + }, blocked: { title: 'Prevent navigation between months', description: 'Set canChangeMonth to false to prevent the navigation between months and years.', diff --git a/examples/src/examples/WeekNumbers.js b/examples/src/examples/WeekNumbers.js new file mode 100644 index 0000000000..f6b7a45336 --- /dev/null +++ b/examples/src/examples/WeekNumbers.js @@ -0,0 +1,37 @@ +import React from 'react'; +import DayPicker from '../../../src'; + +import '../../../src/style.css'; + +export default class WeekNumbers extends React.Component { + state = { + selectedDays: undefined, + selectedWeek: undefined, + }; + + handleWeekClick = (week, days, e) => { + e.target.blur(); + if (week === this.state.selectedWeek) { + this.setState({ + selectedWeek: undefined, + selectedDays: undefined, + }); + return; + } + this.setState({ + selectedDays: days, + selectedWeek: week, + }); + }; + + render() { + return ( + + ); + } +} diff --git a/examples/webpack.config.js b/examples/webpack.config.js index ae1255b2d6..cf23e4e2a2 100644 --- a/examples/webpack.config.js +++ b/examples/webpack.config.js @@ -53,6 +53,7 @@ module.exports = { 'SimpleCalendar', 'SimpleInput', 'TouchEvents', + 'WeekNumbers', 'YearCalendar', 'YearNavigation', ], diff --git a/src/DateUtils.js b/src/DateUtils.js index 1adb4cf2d9..0242129aba 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.js @@ -1,6 +1,7 @@ /** * Clone a date object. * + * @export * @param {Date} d The date to clone * @return {Date} The cloned date */ @@ -10,6 +11,8 @@ export function clone(d) { /** * Return `d` as a new date with `n` months added. + * + * @export * @param {[type]} d * @param {[type]} n */ @@ -22,6 +25,7 @@ export function addMonths(d, n) { /** * Return `true` if two dates are the same day, ignoring the time. * + * @export * @param {Date} d1 * @param {Date} d2 * @return {Boolean} @@ -69,6 +73,7 @@ export function isDayAfter(d1, d2) { * Return `true` if a day is in the past, e.g. yesterday or any day * before yesterday. * + * @export * @param {Date} d * @return {Boolean} */ @@ -82,6 +87,7 @@ export function isPastDay(d) { * Return `true` if a day is in the future, e.g. tomorrow or any day * after tomorrow. * + * @export * @param {Date} d * @return {Boolean} */ @@ -95,6 +101,7 @@ export function isFutureDay(d) { * Return `true` if day `d` is between days `d1` and `d2`, * without including them. * + * @export * @param {Date} d * @param {Date} d1 * @param {Date} d2 @@ -113,6 +120,7 @@ export function isDayBetween(d, d1, d2) { * Add a day to a range and return a new range. A range is an object with * `from` and `to` days. * + * @export * @param {Date} day * @param {Object} range * @return {Object} Returns a new range object @@ -143,6 +151,7 @@ export function addDayToRange(day, range = { from: null, to: null }) { /** * Return `true` if a day is included in a range of days. * + * @export * @param {Date} day * @param {Object} range * @return {Boolean} @@ -156,10 +165,28 @@ export function isDayInRange(day, range) { ); } +/** + * Return the year's week number (as per ISO, i.e. with the week starting from monday) + * for the given day. + * + * @export + * @param {Date} day + * @returns {Number} + */ +export function getWeekNumber(day) { + const date = clone(day); + date.setHours(0, 0, 0); + date.setDate(date.getDate() + 4 - (date.getDay() || 7)); + return Math.ceil( + ((date - new Date(date.getFullYear(), 0, 1)) / 8.64e7 + 1) / 7 + ); +} + export default { addDayToRange, addMonths, clone, + getWeekNumber, isDayAfter, isDayBefore, isDayBetween, diff --git a/src/DayPicker.js b/src/DayPicker.js index 03c5496e09..9550b4034b 100644 --- a/src/DayPicker.js +++ b/src/DayPicker.js @@ -28,6 +28,7 @@ export default class DayPicker extends Component { reverseMonths: PropTypes.bool, pagedNavigation: PropTypes.bool, todayButton: PropTypes.string, + showWeekNumbers: PropTypes.bool, // Modifiers selectedDays: PropTypes.oneOfType([ @@ -110,6 +111,7 @@ export default class DayPicker extends Component { onDayFocus: PropTypes.func, onMonthChange: PropTypes.func, onCaptionClick: PropTypes.func, + onWeekClick: PropTypes.func, }; static defaultProps = { @@ -128,6 +130,7 @@ export default class DayPicker extends Component { canChangeMonth: true, reverseMonths: false, pagedNavigation: false, + showWeekNumbers: false, renderDay: day => day.getDate(), weekdayElement: , navbarElement: , @@ -541,8 +544,10 @@ export default class DayPicker extends Component { locale={this.props.locale} localeUtils={this.props.localeUtils} firstDayOfWeek={firstDayOfWeek} - onCaptionClick={this.props.onCaptionClick} footer={this.props.todayButton && this.renderTodayButton()} + showWeekNumbers={this.props.showWeekNumbers} + onCaptionClick={this.props.onCaptionClick} + onWeekClick={this.props.onWeekClick} > {this.renderDayInMonth} diff --git a/src/Month.js b/src/Month.js index 16fa15e3b5..996b952946 100644 --- a/src/Month.js +++ b/src/Month.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from './PropTypes'; import Weekdays from './Weekdays'; import { getWeekArray } from './Helpers'; +import { getWeekNumber } from './DateUtils'; export default function Month({ classNames, @@ -23,6 +24,8 @@ export default function Month({ children, footer, + showWeekNumbers, + onWeekClick, }) { const captionProps = { date: month, @@ -37,6 +40,7 @@ export default function Month({ : React.createElement(captionElement, captionProps); const weeks = getWeekArray(month, firstDayOfWeek, fixedWeeks); + return (
{caption} @@ -45,16 +49,32 @@ export default function Month({ weekdaysShort={weekdaysShort} weekdaysLong={weekdaysLong} firstDayOfWeek={firstDayOfWeek} + showWeekNumbers={showWeekNumbers} locale={locale} localeUtils={localeUtils} weekdayElement={weekdayElement} />
- {weeks.map(week => ( -
- {week.map(day => children(day, month))} -
- ))} + {weeks.map(week => { + let weekNumber; + if (showWeekNumbers) { + weekNumber = getWeekNumber(week[0]); + } + return ( +
+ {showWeekNumbers && +
onWeekClick(weekNumber, week, e)} + > + {weekNumber} +
} + {week.map(day => children(day, month))} +
+ ); + })}
{footer &&
@@ -86,6 +106,10 @@ Month.propTypes = { PropTypes.instanceOf(React.Component), ]), + footer: PropTypes.node, + showWeekNumbers: PropTypes.bool, + onWeekClick: PropTypes.func, + locale: PropTypes.string.isRequired, localeUtils: PropTypes.localeUtils.isRequired, weekdaysLong: PropTypes.arrayOf(PropTypes.string), @@ -95,5 +119,4 @@ Month.propTypes = { onCaptionClick: PropTypes.func, children: PropTypes.func.isRequired, - footer: PropTypes.node, }; diff --git a/src/Weekdays.js b/src/Weekdays.js index 87d1a155fa..2ce84ce3d4 100644 --- a/src/Weekdays.js +++ b/src/Weekdays.js @@ -4,6 +4,7 @@ import PropTypes from './PropTypes'; export default function Weekdays({ classNames, firstDayOfWeek, + showWeekNumbers, weekdaysLong, weekdaysShort, locale, @@ -31,6 +32,7 @@ export default function Weekdays({ return (
+ {showWeekNumbers &&
} {days}
@@ -47,6 +49,7 @@ Weekdays.propTypes = { firstDayOfWeek: PropTypes.number.isRequired, weekdaysLong: PropTypes.arrayOf(PropTypes.string), weekdaysShort: PropTypes.arrayOf(PropTypes.string), + showWeekNumbers: PropTypes.bool, locale: PropTypes.string.isRequired, localeUtils: PropTypes.localeUtils.isRequired, weekdayElement: PropTypes.oneOfType([ diff --git a/src/classNames.js b/src/classNames.js index e186d583a1..f6c9fc67a2 100644 --- a/src/classNames.js +++ b/src/classNames.js @@ -13,6 +13,7 @@ export default { weekday: 'DayPicker-Weekday', body: 'DayPicker-Body', week: 'DayPicker-Week', + weekNumber: 'DayPicker-WeekNumber', day: 'DayPicker-Day', footer: 'DayPicker-Footer', todayButton: 'DayPicker-TodayButton', diff --git a/src/style.css b/src/style.css index dd543b5e60..787368f1f0 100644 --- a/src/style.css +++ b/src/style.css @@ -86,6 +86,17 @@ vertical-align: middle; } + .DayPicker-WeekNumber { + display: table-cell; + padding: .5rem; + text-align: right; + vertical-align: middle; + min-width: 1rem; + font-size: 0.75em; + cursor: pointer; + color: #8b9898; + } + .DayPicker--interactionDisabled .DayPicker-Day { cursor: default; } diff --git a/test/daypicker/events.js b/test/daypicker/events.js index c213146e1a..26d022d0f3 100644 --- a/test/daypicker/events.js +++ b/test/daypicker/events.js @@ -175,4 +175,17 @@ describe('DayPicker’s events handlers', () => { formatMonthTitle(new Date()) ); }); + it('should call `onWeekClick` when clicking on a week number', () => { + const onWeekClick = spy(); + const wrapper = mount( + + ); + wrapper.find('.DayPicker-WeekNumber').at(1).simulate('click'); + expect(onWeekClick.getCall(0).args[0]).to.equal(6); + expect(onWeekClick.getCall(0).args[1]).to.have.length(7); + }); }); diff --git a/test/daypicker/rendering.js b/test/daypicker/rendering.js index ceecd60a52..5392db0d05 100644 --- a/test/daypicker/rendering.js +++ b/test/daypicker/rendering.js @@ -329,6 +329,13 @@ describe('DayPicker’s rendering', () => { expect(wrapper.find('.DayPicker-Footer')).to.exists; expect(wrapper.find('button.DayPicker-TodayButton')).to.have.text('foo'); }); + it('should render the week numbers', () => { + const wrapper = mount( + + ); + expect(wrapper.find('.DayPicker-WeekNumber')).to.have.length(4); + expect(wrapper.find('.DayPicker-WeekNumber').at(1)).to.have.text('6'); + }); it('should use the specified class names', () => { const wrapper = mount( ): void; onKeyDown?(e: React.KeyboardEvent): void; onMonthChange?(month: Date): void; + onWeekClick?(weekNumber: number, days: Date[], e: React.MouseEvent): void; pagedNavigation?: boolean; renderDay?(date: Date, modifiers: Modifiers): React.ReactNode; reverseMonths?: boolean; selectedDays?: Modifier | Modifier[]; + showWeekNumbers?: boolean; todayButton?: string; toMonth?: Date; weekdayElement?: React.ReactElement> |