From 321296f018960d1eec9b562e3cb1c78268564503 Mon Sep 17 00:00:00 2001 From: Benjamin Seber Date: Thu, 19 Nov 2020 17:03:49 +0100 Subject: [PATCH 1/4] allow to disable days make `isDateDisabled` optional to not introduce a breaking change --- README.md | 66 ++++++++++ docs/index.html | 123 ++++++++++++++++++ .../duet-date-picker/date-adapter.ts | 1 + .../duet-date-picker/date-picker-day.tsx | 7 +- .../duet-date-picker/date-picker-month.tsx | 3 + .../duet-date-picker/date-utils.spec.ts | 15 +++ src/components/duet-date-picker/date-utils.ts | 13 +- .../duet-date-picker/duet-date-picker.tsx | 18 ++- src/index.html | 123 ++++++++++++++++++ 9 files changed, 359 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ee27f5f..3ebc978 100644 --- a/README.md +++ b/README.md @@ -599,6 +599,72 @@ Duet Date Picker offers full support for localization. This includes the text la Please note that you must provide the entirety of the localization properties in the object when overriding with your coustom localization. +## Control which days are selectable + +Duet Date Picker allows you to disable the selection of specific days. Below is an example of a date picker that is disabling weekends. + +```html + + + + +``` + ## Server side rendering Duet Date Picker package includes a hydrate app that is a bundle of the same components, but compiled so that they can be hydrated on a NodeJS server and generate static HTML and CSS. To get started, import the hydrate app into your server’s code like so: diff --git a/docs/index.html b/docs/index.html index 21f7b58..f480f2c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -529,6 +529,129 @@

Required atrribute

alert("Submitted") }) </script> + +

Disable selectable days

+ + + +
<label for="date">Choose a date</label>
+<duet-date-picker first-day-of-week="0" identifier="date"></duet-date-picker>
+
+<script>
+  const pickerDisableWeekend = document.querySelector(".picker-disabled-weekend")
+  const PICKER_DISABLED_DATE_FORMAT = /^(\d{4})-(\d{1,2})-(\d{1,2})$/
+
+  pickerDisableWeekend.dateAdapter = {
+    parse: function parse() {
+      var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""
+      var createDate = arguments.length > 1 ? arguments[1] : undefined
+      var matches = value.match(PICKER_DISABLED_DATE_FORMAT)
+
+      if (matches) {
+        return createDate(matches[3], matches[2], matches[1])
+      }
+    },
+    format: function format(date) {
+      return ""
+        .concat(date.getFullYear(), "-")
+        .concat(date.getMonth() + 1, "-")
+        .concat(date.getDate())
+    },
+    isDateDisabled: function isDateDisabled(date, focusedDay) {
+      return (
+        date.getDay() === 0 ||
+        date.getDay() === 6 ||
+        !(date.getFullYear() === focusedDay.getFullYear() && date.getMonth() === focusedDay.getMonth())
+      )
+    },
+  }
+
+  picker.localization = {
+    buttonLabel: "Choose date",
+    placeholder: "YYYY-MM-DD",
+    selectedDateMessage: "Selected date is",
+    prevMonthLabel: "Previous month",
+    nextMonthLabel: "Next month",
+    monthSelectLabel: "Month",
+    yearSelectLabel: "Year",
+    closeLabel: "Close window",
+    keyboardInstruction: "You can use arrow keys to navigate dates",
+    calendarHeading: "Choose a date",
+    dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
+    monthNames: [
+      "January",
+      "February",
+      "March",
+      "April",
+      "May",
+      "June",
+      "July",
+      "August",
+      "September",
+      "October",
+      "November",
+      "December",
+    ],
+    monthNamesShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
+  }
+</script>

© 2020 LocalTapiola Services Ltd / diff --git a/src/components/duet-date-picker/date-adapter.ts b/src/components/duet-date-picker/date-adapter.ts index 1947287..c923195 100644 --- a/src/components/duet-date-picker/date-adapter.ts +++ b/src/components/duet-date-picker/date-adapter.ts @@ -7,6 +7,7 @@ export type DuetDateFormatter = (date: Date) => string export interface DuetDateAdapter { parse: DuetDateParser format: DuetDateFormatter + isDateDisabled?: (date: Date, focusedDate: Date) => boolean } const isoAdapter: DuetDateAdapter = { parse: parseISODate, format: printISODate } diff --git a/src/components/duet-date-picker/date-picker-day.tsx b/src/components/duet-date-picker/date-picker-day.tsx index d22751b..d1d0fbe 100644 --- a/src/components/duet-date-picker/date-picker-day.tsx +++ b/src/components/duet-date-picker/date-picker-day.tsx @@ -5,6 +5,7 @@ export type DatePickerDayProps = { focusedDay: Date today: Date day: Date + disabled: boolean inRange: boolean isSelected: boolean dateFormatter: Intl.DateTimeFormat @@ -20,13 +21,13 @@ export const DatePickerDay: FunctionalComponent = ({ onDaySelect, onKeyboardNavigation, focusedDayRef, + disabled, inRange, isSelected, dateFormatter, }) => { const isToday = isEqual(day, today) const isFocused = isEqual(day, focusedDay) - const isDisabled = day.getMonth() !== focusedDay.getMonth() const isOutsideRange = !inRange function handleClick(e) { @@ -38,13 +39,13 @@ export const DatePickerDay: FunctionalComponent = ({ class={{ "duet-date__day": true, "is-outside": isOutsideRange, - "is-disabled": isDisabled, + "is-disabled": disabled, "is-today": isToday, }} tabIndex={isFocused ? 0 : -1} onClick={handleClick} onKeyDown={onKeyboardNavigation} - disabled={isOutsideRange || isDisabled} + disabled={isOutsideRange || disabled} type="button" aria-pressed={isSelected ? "true" : "false"} ref={el => { diff --git a/src/components/duet-date-picker/date-picker-month.tsx b/src/components/duet-date-picker/date-picker-month.tsx index 3baf43a..4fd8f81 100644 --- a/src/components/duet-date-picker/date-picker-month.tsx +++ b/src/components/duet-date-picker/date-picker-month.tsx @@ -29,6 +29,7 @@ type DatePickerMonthProps = { min?: Date max?: Date dateFormatter: Intl.DateTimeFormat + isDateDisabled: (date: Date) => boolean onDateSelect: DatePickerDayProps["onDaySelect"] onKeyboardNavigation: DatePickerDayProps["onKeyboardNavigation"] focusedDayRef: (element: HTMLButtonElement) => void @@ -43,6 +44,7 @@ export const DatePickerMonth: FunctionalComponent = ({ min, max, dateFormatter, + isDateDisabled, onDateSelect, onKeyboardNavigation, focusedDayRef, @@ -72,6 +74,7 @@ export const DatePickerMonth: FunctionalComponent = ({ today={today} focusedDay={focusedDate} isSelected={isEqual(day, selectedDate)} + disabled={isDateDisabled(day)} inRange={inRange(day, min, max)} onDaySelect={onDateSelect} dateFormatter={dateFormatter} diff --git a/src/components/duet-date-picker/date-utils.spec.ts b/src/components/duet-date-picker/date-utils.spec.ts index 594fe9f..c144d0b 100644 --- a/src/components/duet-date-picker/date-utils.spec.ts +++ b/src/components/duet-date-picker/date-utils.spec.ts @@ -1,5 +1,6 @@ import { isEqual, + isEqualMonth, addDays, addMonths, addYears, @@ -67,6 +68,20 @@ describe("duet-date-picker/date-utils", () => { }) }) + describe("isEqualMonth", () => { + it("compares dates", () => { + expect(isEqualMonth(new Date(2020, 0, 1), new Date(2020, 0, 1))).toBe(true) + expect(isEqualMonth(new Date(2020, 0, 1), new Date(2020, 0, 31))).toBe(true) + + expect(isEqualMonth(new Date(2020, 0, 1), new Date(2020, 1, 1))).toBe(false) + expect(isEqualMonth(new Date(2020, 0, 1), new Date(2021, 0, 1))).toBe(false) + + expect(isEqualMonth(null, new Date(2020, 0, 1))).toBe(false) + expect(isEqualMonth(new Date(2020, 0, 1), null)).toBe(false) + expect(isEqualMonth(null, null)).toBe(false) + }) + }) + describe("printISODate", () => { it("should print in format dd.mm.yyyy", () => { expect(printISODate(new Date(2020, 0, 1))).toBe("2020-01-01") diff --git a/src/components/duet-date-picker/date-utils.ts b/src/components/duet-date-picker/date-utils.ts index d113014..4ffd781 100644 --- a/src/components/duet-date-picker/date-utils.ts +++ b/src/components/duet-date-picker/date-utils.ts @@ -79,7 +79,18 @@ export function isEqual(a: Date, b: Date): boolean { return false } - return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() + return isEqualMonth(a, b) && a.getDate() === b.getDate() +} + +/** + * Compare if two dates are in the same month of the same year. + */ +export function isEqualMonth(a: Date, b: Date): boolean { + if (a == null || b == null) { + return false + } + + return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() } export function addDays(date: Date, days: number): Date { diff --git a/src/components/duet-date-picker/duet-date-picker.tsx b/src/components/duet-date-picker/duet-date-picker.tsx index 4088505..cb88eaa 100644 --- a/src/components/duet-date-picker/duet-date-picker.tsx +++ b/src/components/duet-date-picker/duet-date-picker.tsx @@ -27,6 +27,7 @@ import { createIdentifier, DaysOfWeek, createDate, + isEqualMonth, } from "./date-utils" import { DatePickerInput } from "./date-picker-input" import { DatePickerMonth } from "./date-picker-month" @@ -523,12 +524,8 @@ export class DuetDatePicker implements ComponentInterface { return } - if (day.getMonth() === this.focusedDay.getMonth()) { - this.setValue(day) - this.hide() - } else { - this.setFocusedDay(day) - } + this.setValue(day) + this.hide() } private handleMonthSelect = e => { @@ -568,6 +565,14 @@ export class DuetDatePicker implements ComponentInterface { } } + private isDateDisabled = (date: Date): boolean => { + if (this.dateAdapter.isDateDisabled) { + return this.dateAdapter.isDateDisabled(date, this.focusedDay) + } + + return !isEqualMonth(date, this.focusedDay) + } + /** * render() function * Always the last one in the class. @@ -777,6 +782,7 @@ export class DuetDatePicker implements ComponentInterface { focusedDayRef={this.processFocusedDayNode} min={minDate} max={maxDate} + isDateDisabled={this.isDateDisabled} /> diff --git a/src/index.html b/src/index.html index 810d2ba..de3f4fd 100644 --- a/src/index.html +++ b/src/index.html @@ -518,6 +518,129 @@

Required atrribute

alert("Submitted") }) </script> + +

Disable selectable days

+ + + +
<label for="date">Choose a date</label>
+<duet-date-picker first-day-of-week="0" identifier="date"></duet-date-picker>
+
+<script>
+  const pickerDisableWeekend = document.querySelector(".picker-disabled-weekend")
+  const PICKER_DISABLED_DATE_FORMAT = /^(\d{4})-(\d{1,2})-(\d{1,2})$/
+
+  pickerDisableWeekend.dateAdapter = {
+    parse: function parse() {
+      var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""
+      var createDate = arguments.length > 1 ? arguments[1] : undefined
+      var matches = value.match(PICKER_DISABLED_DATE_FORMAT)
+
+      if (matches) {
+        return createDate(matches[3], matches[2], matches[1])
+      }
+    },
+    format: function format(date) {
+      return ""
+        .concat(date.getFullYear(), "-")
+        .concat(date.getMonth() + 1, "-")
+        .concat(date.getDate())
+    },
+    isDateDisabled: function isDateDisabled(date, focusedDay) {
+      return (
+        date.getDay() === 0 ||
+        date.getDay() === 6 ||
+        !(date.getFullYear() === focusedDay.getFullYear() && date.getMonth() === focusedDay.getMonth())
+      )
+    },
+  }
+
+  picker.localization = {
+    buttonLabel: "Choose date",
+    placeholder: "YYYY-MM-DD",
+    selectedDateMessage: "Selected date is",
+    prevMonthLabel: "Previous month",
+    nextMonthLabel: "Next month",
+    monthSelectLabel: "Month",
+    yearSelectLabel: "Year",
+    closeLabel: "Close window",
+    keyboardInstruction: "You can use arrow keys to navigate dates",
+    calendarHeading: "Choose a date",
+    dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
+    monthNames: [
+      "January",
+      "February",
+      "March",
+      "April",
+      "May",
+      "June",
+      "July",
+      "August",
+      "September",
+      "October",
+      "November",
+      "December",
+    ],
+    monthNamesShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
+  }
+</script>

© 2020 LocalTapiola Services Ltd / From 40681a1556bbf14af1a842929151ff53b075f7a0 Mon Sep 17 00:00:00 2001 From: Benjamin Seber Date: Thu, 19 Nov 2020 17:50:52 +0100 Subject: [PATCH 2/4] fade days of next/prev month despite they are selectable enabling selection of every visible day in the date picker should have no impact on the visualization. days of next or previous month could be selectable but should still be rendered differently to the focused month. --- src/components/duet-date-picker/date-picker-day.tsx | 4 +++- src/components/duet-date-picker/duet-date-picker.scss | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/duet-date-picker/date-picker-day.tsx b/src/components/duet-date-picker/date-picker-day.tsx index d1d0fbe..ba949d7 100644 --- a/src/components/duet-date-picker/date-picker-day.tsx +++ b/src/components/duet-date-picker/date-picker-day.tsx @@ -1,5 +1,5 @@ import { h, FunctionalComponent } from "@stencil/core" -import { isEqual } from "./date-utils" +import { isEqual, isEqualMonth } from "./date-utils" export type DatePickerDayProps = { focusedDay: Date @@ -27,6 +27,7 @@ export const DatePickerDay: FunctionalComponent = ({ dateFormatter, }) => { const isToday = isEqual(day, today) + const isMonth = isEqualMonth(day, focusedDay) const isFocused = isEqual(day, focusedDay) const isOutsideRange = !inRange @@ -41,6 +42,7 @@ export const DatePickerDay: FunctionalComponent = ({ "is-outside": isOutsideRange, "is-disabled": disabled, "is-today": isToday, + "is-month": isMonth, }} tabIndex={isFocused ? 0 : -1} onClick={handleClick} diff --git a/src/components/duet-date-picker/duet-date-picker.scss b/src/components/duet-date-picker/duet-date-picker.scss index 55b80c4..c8fc52a 100644 --- a/src/components/duet-date-picker/duet-date-picker.scss +++ b/src/components/duet-date-picker/duet-date-picker.scss @@ -282,16 +282,17 @@ z-index: 200; } + &:not(.is-month), &.is-disabled { background: transparent; box-shadow: none; color: var(--duet-color-text); cursor: default; opacity: 0.5; + } - &::before { - display: none; - } + &.is-disabled::before { + display: none; } &.is-outside { From 2eb799dd2e8e278af2b05f152573befb1b5eeafb Mon Sep 17 00:00:00 2001 From: Benjamin Seber Date: Sat, 24 Apr 2021 22:21:57 +0200 Subject: [PATCH 3/4] render span instead of button when element is disabled --- .../duet-date-picker/date-picker-day.tsx | 32 +++++++++++++++++-- .../duet-date-picker/date-picker-month.tsx | 2 +- .../duet-date-picker/duet-date-picker.scss | 19 +++++++++-- .../duet-date-picker/duet-date-picker.tsx | 2 +- 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/components/duet-date-picker/date-picker-day.tsx b/src/components/duet-date-picker/date-picker-day.tsx index ba949d7..737dbbb 100644 --- a/src/components/duet-date-picker/date-picker-day.tsx +++ b/src/components/duet-date-picker/date-picker-day.tsx @@ -11,7 +11,7 @@ export type DatePickerDayProps = { dateFormatter: Intl.DateTimeFormat onDaySelect: (event: MouseEvent, day: Date) => void onKeyboardNavigation: (event: KeyboardEvent) => void - focusedDayRef?: (element: HTMLButtonElement) => void + focusedDayRef?: (element: HTMLElement) => void } export const DatePickerDay: FunctionalComponent = ({ @@ -35,19 +35,45 @@ export const DatePickerDay: FunctionalComponent = ({ onDaySelect(e, day) } + if (disabled) { + return ( + { + if (isFocused && el && focusedDayRef) { + focusedDayRef(el) + } + }} + > + + {dateFormatter.format(day)} + + ) + } + return (