From eb1637798041f500debd483f419cfe0a8e50b0f4 Mon Sep 17 00:00:00 2001 From: Greg Tyler Date: Mon, 8 Jan 2024 09:47:22 +0000 Subject: [PATCH 01/58] feat(date-picker): add date picker --- docs/examples/date-picker/index.njk | 19 + src/moj/all.js | 5 + src/moj/components/_all.scss | 1 + .../components/date-picker/_date-picker.scss | 278 ++++++++ src/moj/components/date-picker/date-picker.js | 596 ++++++++++++++++++ src/moj/components/date-picker/macro.njk | 3 + src/moj/components/date-picker/template.njk | 64 ++ 7 files changed, 966 insertions(+) create mode 100644 docs/examples/date-picker/index.njk create mode 100644 src/moj/components/date-picker/_date-picker.scss create mode 100644 src/moj/components/date-picker/date-picker.js create mode 100644 src/moj/components/date-picker/macro.njk create mode 100644 src/moj/components/date-picker/template.njk diff --git a/docs/examples/date-picker/index.njk b/docs/examples/date-picker/index.njk new file mode 100644 index 00000000..ca0d19f6 --- /dev/null +++ b/docs/examples/date-picker/index.njk @@ -0,0 +1,19 @@ +--- +layout: layouts/example.njk +title: Date Picker (example) +arguments: date-picker +--- + +{%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} + +{{ mojDatePicker({ + id: "submisison-date", + name: "submisison-date", + label: { + text: "Submission date" + }, + hint: { + text: "For example, 03/01/2024" + }, + value: "03/01/2024" +}) }} diff --git a/src/moj/all.js b/src/moj/all.js index a4090064..f7c41dda 100644 --- a/src/moj/all.js +++ b/src/moj/all.js @@ -66,4 +66,9 @@ MOJFrontend.initAll = function (options) { table: $table }); }); + + const $datepickers = document.querySelectorAll('[data-module="moj-date-picker"]') + MOJFrontend.nodeListForEach($datepickers, function ($datepicker) { + new MOJFrontend.DatePicker($datepicker, {}).init(); + }) } diff --git a/src/moj/components/_all.scss b/src/moj/components/_all.scss index ffa08e9f..19812f8e 100755 --- a/src/moj/components/_all.scss +++ b/src/moj/components/_all.scss @@ -5,6 +5,7 @@ @import "button-menu/button-menu"; @import "cookie-banner/cookie-banner"; @import "currency-input/currency-input"; +@import "date-picker/date-picker"; @import "filter/filter"; @import "header/header"; @import "identity-bar/identity-bar"; diff --git a/src/moj/components/date-picker/_date-picker.scss b/src/moj/components/date-picker/_date-picker.scss new file mode 100644 index 00000000..fd28c20d --- /dev/null +++ b/src/moj/components/date-picker/_date-picker.scss @@ -0,0 +1,278 @@ +.moj-datepicker { + position: relative; + + &--fixed-width { + .moj-datepicker-input__wrapper { + width: 215px; + } + } + + &__dialog { + box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.15); + + background-color: govuk-colour('white'); + clear: both; + display: none; + padding: 8px; + outline: 1px solid $govuk-border-colour; + outline-offset: -1px; + position: static; + top: 0; + transition: background-color 0.2s, outline-color 0.2s; + width: 280px; + z-index: 2; + + &__header { + position: relative; + text-align: center; + margin-bottom: 5px; + + > :nth-child(1) { + position: absolute; + left: 5px; + top: -2px; + + > :nth-child(2) { + margin-left: 4px; + } + } + + > :nth-child(3) { + position: absolute; + right: 5px; + top: -2px; + + > :nth-child(1) { + margin-right: 4px; + } + } + } + + &__title { + font-size: 16px; + padding: 8px 0; + margin: 0 !important; + } + + &__navbuttons { + button { + background-color: transparent; + color: $govuk-text-colour !important; + min-height: 40px; + margin: 0; + padding: 4px 4px 0 4px; + min-width: 32px; + border: none; + display: inline-block; + cursor: pointer; + outline: none; + + .moj-datepicker-icon { + height: 32px; + padding: 0; + position: static; + width: 24px; + } + + &:hover { + background-color: rgba(govuk-colour('yellow'), .5); + } + + &:focus { + background-color: $govuk-focus-colour; + border-bottom: 4px solid govuk-colour('black'); + } + } + } + + &__table { + border-collapse: collapse; + + tbody:focus-within { + outline: 2px solid $govuk-focus-colour; + } + + td { + border: 0; + margin: 0; + outline: 0; + padding: 0; + } + + th { + font-size: 16px; + color: $govuk-text-colour; + } + + button { + background-color: transparent; + border-width: 0; + color: $govuk-text-colour; + min-height: 40px; + margin: 0; + padding: 0; + min-width: 40px; + + font-size: 16px; + + &:hover { + outline: 3px solid rgba(0,0,0,0); + color: $govuk-text-colour; + background-color: rgba(govuk-colour('yellow'), .5); + box-shadow: none; + text-decoration: none; + -webkit-box-decoration-break: clone; + box-decoration-break: clone; + cursor: pointer; + } + + &:focus { + outline: 3px solid rgba(0,0,0,0); + color: $govuk-focus-text-colour; + background-color: $govuk-focus-colour; + border-bottom: 4px solid govuk-colour('black'); + padding-top: 4px; + text-decoration: none; + -webkit-box-decoration-break: clone; + box-decoration-break: clone; + } + + &[disabled="true"] { + background-color: govuk-colour('light-grey'); + color: $govuk-text-colour; + } + + &.moj-datepicker__current { + $moj-current-outline-width: 2px; + outline: $moj-current-outline-width solid govuk-colour('black') !important; + outline-offset: #{$moj-current-outline-width * -1}; + } + + &.moj-datepicker__current[tabindex="-1"] { + background: transparent; + color: currentColor; + + &:hover { + background-color: rgba(govuk-colour('yellow'), .5); + cursor: pointer; + } + } + + &.moj-datepicker__today { + font-weight: 700; + + &::after { + background-color: currentColor; + border-radius: 4px; + content: ''; + height: 4px; + margin-top: -1px; + margin-left: 1px; + position: absolute; + width: 4px; + } + } + + &.moj-datepicker-selected:not(:focus) { + background-color: govuk-colour('black'); + color: govuk-colour('white'); + } + } + } + + &__table-caption { + font-size: 14px; + caption-side: bottom; + line-height: 2; + margin-top: 8px; + } + + &__buttongroup { + display: grid; + grid-gap: 0 8px; + margin-right: 0; + overflow: visible; + grid-template-columns: 1fr 1fr; + + > * { + margin-left: 0; + margin-right: 0; + width: auto !important; + } + + .govuk-button { + margin-bottom: 0; + } + } + } + + .govuk-label--m { + margin-bottom: 5px; + } + + .govuk-hint { + margin-bottom: 10px; + } +} + +.moj-datepicker-input__wrapper { + display: flex; + position: relative; + margin-bottom: 24px; + overflow: visible; + + .govukInput { + float: left; + margin-bottom: 0; + margin-right: -48px; + padding-right: 56px; + } + + .moj-datepicker-icon { + height: 24px; + width: 32px; + } + + .govuk-form-group { + width: 100%; + } +} + +@media (min-width: 768px) { + .moj-datepicker { + &__dialog { + position: absolute; + width: auto; + } + } +} + +.moj-datepicker-button { + background-color: govuk-colour('black'); + fill: govuk-colour('white'); + position: absolute; + right: 0; + bottom: 0; + height: 40px; + padding-top: 6px; + border: none; + border-bottom: 4px solid govuk-colour('black'); + outline: none; + cursor: pointer; + + &:hover { + background-color: govuk-colour('mid-grey'); + fill: govuk-colour('black'); + border-bottom: 4px solid govuk-colour('mid-grey'); + } + + &:focus { + background-color: $govuk-focus-colour; + fill: $govuk-focus-text-colour; + border-bottom: 4px solid govuk-colour('black'); + } + + @media (max-width: 768px) { + bottom: unset; + } +} diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js new file mode 100644 index 00000000..2d418815 --- /dev/null +++ b/src/moj/components/date-picker/date-picker.js @@ -0,0 +1,596 @@ +/** + * Datepicker config + * + * @typedef {object} DatepickerConfig + * + * @property {string} [imagePath] - The path to image assets. + * @property {string} [id] - . + * @property {string} [name] - . + * @property {string} [label] - . + * @property {string} [hint] - . + * @property {string} [minDate] - . + * @property {string} [maxDate] - . + */ + +/** + * Datepicker component + * + * @param {HTMLElement} $module - HTML element + * @param {DatepickerConfig} config - Datepicker config + * @constructor + */ +function Datepicker($module, config) { + if (!$module) { + return this + } + const defaultConfig = { + imagePath: '/assets/images/', + } + this.config = { ...defaultConfig, ...config } + + this.dayLabels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + this.monthLabels = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ] + + this.currentDate = new Date() + this.currentDate.setHours(0, 0, 0, 0) + this.calendarDays = [] + + this.keycodes = { + tab: 9, + esc: 27, + pageup: 33, + pagedown: 34, + end: 35, + home: 36, + left: 37, + up: 38, + right: 39, + down: 40, + } + + this.$module = $module + this.$input = $module.querySelector('.moj-js-datepicker-input') + this.$calendarButton = $module.querySelector('.moj-js-datepicker-button') +} + +/** + * Initialise Datepicker + */ +Datepicker.prototype.init = function () { + // Check that required elements are present + if (!this.$input) { + return + } + + this.initControls() +} + +/** + * Initialise controls and set attributes + */ +Datepicker.prototype.initControls = function () { + // Create datepicker popup dialog + const titleId = `datepicker-title-${this.$input.id}` + const dialog = document.createElement('div') + dialog.id = `datepicker-${this.$input.id}` + dialog.setAttribute('class', 'moj-datepicker__dialog datepickerDialog') + dialog.setAttribute('role', 'dialog') + dialog.setAttribute('aria-modal', 'true') + dialog.setAttribute('aria-labelledby', titleId) + dialog.innerHTML = this.createDialogMarkup(titleId) + + this.dialogElement = dialog + this.$input.insertAdjacentElement('afterend', this.dialogElement) + + this.dialogTitleNode = this.dialogElement.querySelector('.js-datepicker-month-year') + + this.setMinAndMaxDatesOnCalendar() + + // create calendar + const tbody = this.dialogElement.querySelector('tbody') + let dayCount = 0 + for (let i = 0; i < 6; i++) { + // create row + const row = tbody.insertRow(i) + + for (let j = 0; j < 7; j++) { + // create cell (day) + const cell = document.createElement('td') + const dateButton = document.createElement('button') + dateButton.dataset.form = 'date-select' + + cell.appendChild(dateButton) + row.appendChild(cell) + + const calendarDay = new DSCalendarDay(dateButton, dayCount, i, j, this) + calendarDay.init() + this.calendarDays.push(calendarDay) + dayCount++ + } + } + + // add event listeners + this.prevMonthButton = this.dialogElement.querySelector('.js-datepicker-prev-month') + this.prevYearButton = this.dialogElement.querySelector('.js-datepicker-prev-year') + this.nextMonthButton = this.dialogElement.querySelector('.js-datepicker-next-month') + this.nextYearButton = this.dialogElement.querySelector('.js-datepicker-next-year') + this.prevMonthButton.addEventListener('click', event => this.focusPreviousMonth(event, false)) + this.prevYearButton.addEventListener('click', event => this.focusPreviousYear(event, false)) + this.nextMonthButton.addEventListener('click', event => this.focusNextMonth(event, false)) + this.nextYearButton.addEventListener('click', event => this.focusNextYear(event, false)) + + this.cancelButton = this.dialogElement.querySelector('.js-datepicker-cancel') + this.okButton = this.dialogElement.querySelector('.js-datepicker-ok') + this.cancelButton.addEventListener('click', event => { + event.preventDefault() + this.closeDialog(event) + }) + this.okButton.addEventListener('click', () => this.selectDate(this.currentDate)) + + const dialogButtons = this.dialogElement.querySelectorAll('button:not([disabled="true"])') + // eslint-disable-next-line prefer-destructuring + this.firstButtonInDialog = dialogButtons[0] + this.lastButtonInDialog = dialogButtons[dialogButtons.length - 1] + this.firstButtonInDialog.addEventListener('keydown', event => this.firstButtonKeyup(event)) + this.lastButtonInDialog.addEventListener('keydown', event => this.lastButtonKeyup(event)) + + this.$calendarButton.addEventListener('click', event => this.toggleDialog(event)) + + document.body.addEventListener('mouseup', event => this.backgroundClick(event)) + + // populates calendar with initial dates, avoids Wave errors about null buttons + this.updateCalendar() +} + +Datepicker.prototype.createDialogMarkup = function (titleId) { + return `
+
+ + + +
+ +

June 2020

+ +
+ + + +
+
+ + + + + + + + + + + + + + + + +
You can use the arrow keys to select a date
MoTuWeThFrSaSu
+ +
+ + +
` +} + +Datepicker.prototype.leadingZeroes = function (value, length = 2) { + let ret = value.toString() + + while (ret.length < length) { + ret = `0${ret.toString()}` + } + + return ret +} + +Datepicker.prototype.setMinAndMaxDatesOnCalendar = function () { + if (this.$input.dataset.mindate) { + this.minDate = this.formattedDateFromString(this.$input.dataset.mindate, null) + if (this.minDate && this.currentDate < this.minDate) { + this.currentDate = this.minDate + } + } + + if (this.$input.dataset.maxdate) { + this.maxDate = this.formattedDateFromString(this.$input.dataset.maxdate, null) + if (this.maxDate && this.currentDate > this.maxDate) { + this.currentDate = this.maxDate + } + } +} + +Datepicker.prototype.formattedDateFromString = function (dateString, fallback = new Date()) { + let formattedDate = null + const dateFormatPattern = /(\d{1,2})([-/,. ])(\d{1,2})[-/,. ](\d{4})/ + + if (!dateFormatPattern.test(dateString)) return fallback + + const match = dateString.match(dateFormatPattern) + const separator = match[2] + const day = match[1] + const month = match[3] + const year = match[4] + + formattedDate = new Date(`${month}${separator}${day}${separator}${year}`) + if (formattedDate instanceof Date && !isNaN(formattedDate)) { + return formattedDate + } + return fallback +} + +Datepicker.prototype.formattedDateFromDate = function (date) { + return `${this.leadingZeroes(date.getDate())}/${this.leadingZeroes(date.getMonth() + 1)}/${date.getFullYear()}` +} + +Datepicker.prototype.backgroundClick = function (event) { + if ( + this.isOpen() && + !this.dialogElement.contains(event.target) && + !this.$input.contains(event.target) && + !this.$calendarButton.contains(event.target) + ) { + event.preventDefault() + this.closeDialog() + } +} + +Datepicker.prototype.formattedDateHuman = function (date) { + return `${this.dayLabels[date.getDay()]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}` +} + +Datepicker.prototype.firstButtonKeyup = function (event) { + if (event.keyCode === this.keycodes.tab && event.shiftKey) { + this.lastButtonInDialog.focus() + event.preventDefault() + } +} + +Datepicker.prototype.lastButtonKeyup = function (event) { + if (event.keyCode === this.keycodes.tab && !event.shiftKey) { + this.firstButtonInDialog.focus() + event.preventDefault() + } +} + +// render calendar +Datepicker.prototype.updateCalendar = function () { + this.dialogTitleNode.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}` + + const day = this.currentDate + + const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1) + const dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1 // Change logic to make Monday first day of week, i.e. 0 + + firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek) + + const thisDay = new Date(firstOfMonth) + + // loop through our days + for (let i = 0; i < this.calendarDays.length; i++) { + const hidden = thisDay.getMonth() !== day.getMonth() + + let disabled + + if (thisDay < this.minDate) { + disabled = true + } + if (thisDay > this.maxDate) { + disabled = true + } + + this.calendarDays[i].update(thisDay, hidden, disabled) + + thisDay.setDate(thisDay.getDate() + 1) + } +} + +Datepicker.prototype.setCurrentDate = function (focus = true) { + const { currentDate } = this + + this.calendarDays.forEach(calendarDay => { + calendarDay.button.setAttribute('tabindex', -1) + calendarDay.button.classList.remove('moj-datepicker-selected') + const calendarDayDate = calendarDay.date + calendarDayDate.setHours(0, 0, 0, 0) + + const today = new Date() + today.setHours(0, 0, 0, 0) + + if (calendarDayDate.getTime() === currentDate.getTime() && !calendarDay.disabled) { + if (focus) { + calendarDay.button.setAttribute('tabindex', 0) + calendarDay.button.focus() + calendarDay.button.classList.add('moj-datepicker-selected') + } + } + + if (this.inputDate && calendarDayDate.getTime() === this.inputDate.getTime()) { + calendarDay.button.classList.add('moj-datepicker__current') + calendarDay.button.setAttribute('aria-selected', true) + } else { + calendarDay.button.classList.remove('moj-datepicker__current') + calendarDay.button.removeAttribute('aria-selected') + } + + if (calendarDayDate.getTime() === today.getTime()) { + calendarDay.button.classList.add('moj-datepicker__today') + } else { + calendarDay.button.classList.remove('moj-datepicker__today') + } + }) + + // if no date is tab-able, make the first non-disabled date tab-able + if (!focus) { + const enabledDays = this.calendarDays.filter(calendarDay => { + return window.getComputedStyle(calendarDay.button).display === 'block' && !calendarDay.button.disabled + }) + + enabledDays[0].button.setAttribute('tabindex', 0) + + this.currentDate = enabledDays[0].date + } +} + +Datepicker.prototype.selectDate = function (date) { + this.$calendarButton.querySelector('span').innerText = `Choose date. Selected date is ${this.formattedDateHuman( + date, + )}` + this.$input.value = this.formattedDateFromDate(date) + + const changeEvent = new Event('change', { bubbles: true, cancelable: true }) + this.$input.dispatchEvent(changeEvent) + + this.closeDialog() +} + +Datepicker.prototype.isOpen = function () { + return this.dialogElement.classList.contains('moj-datepicker__dialog--open') +} + +Datepicker.prototype.toggleDialog = function (event) { + event.preventDefault() + if (this.isOpen()) { + this.closeDialog() + } else { + this.setMinAndMaxDatesOnCalendar() + this.openDialog() + } +} + +Datepicker.prototype.openDialog = function () { + // display the dialog + this.dialogElement.style.display = 'block' + this.dialogElement.classList.add('moj-datepicker__dialog--open') + + // position the dialog + this.dialogElement.style.left = `${this.$input.offsetWidth + 16}px` + + // get the date from the input element + if (this.$input.value.match(/^(\d{1,2})([-/,. ])(\d{1,2})[-/,. ](\d{4})$/)) { + this.inputDate = this.formattedDateFromString(this.$input.value) + this.currentDate = this.inputDate + } + + this.updateCalendar() + this.setCurrentDate() +} + +Datepicker.prototype.closeDialog = function () { + this.dialogElement.style.display = 'none' + this.dialogElement.classList.remove('moj-datepicker__dialog--open') + this.$calendarButton.focus() +} + +Datepicker.prototype.goToDate = function (date, focus) { + const current = this.currentDate + this.currentDate = date + + if (this.minDate && this.minDate > date) { + this.currentDate = this.minDate + } else if (this.maxDate && this.maxDate < date) { + this.currentDate = this.maxDate + } + + if (current.getMonth() !== this.currentDate.getMonth() || current.getFullYear() !== this.currentDate.getFullYear()) { + this.updateCalendar() + } + + this.setCurrentDate(focus) +} + +// day navigation +Datepicker.prototype.focusNextDay = function () { + const date = new Date(this.currentDate) + date.setDate(date.getDate() + 1) + this.goToDate(date) +} + +Datepicker.prototype.focusPreviousDay = function () { + const date = new Date(this.currentDate) + date.setDate(date.getDate() - 1) + this.goToDate(date) +} + +// week navigation +Datepicker.prototype.focusNextWeek = function () { + const date = new Date(this.currentDate) + date.setDate(date.getDate() + 7) + this.goToDate(date) +} + +Datepicker.prototype.focusPreviousWeek = function () { + const date = new Date(this.currentDate) + date.setDate(date.getDate() - 7) + this.goToDate(date) +} + +Datepicker.prototype.focusFirstDayOfWeek = function () { + const date = new Date(this.currentDate) + date.setDate(date.getDate() - date.getDay()) + this.goToDate(date) +} + +Datepicker.prototype.focusLastDayOfWeek = function () { + const date = new Date(this.currentDate) + date.setDate(date.getDate() - date.getDay() + 6) + this.goToDate(date) +} + +// month navigation +Datepicker.prototype.focusNextMonth = function (event, focus = true) { + event.preventDefault() + const date = new Date(this.currentDate) + date.setMonth(date.getMonth() + 1, 1) + this.goToDate(date, focus) +} + +Datepicker.prototype.focusPreviousMonth = function (event, focus = true) { + event.preventDefault() + const date = new Date(this.currentDate) + date.setMonth(date.getMonth() - 1, 1) + this.goToDate(date, focus) +} + +// year navigation +Datepicker.prototype.focusNextYear = function (event, focus = true) { + event.preventDefault() + const date = new Date(this.currentDate) + date.setFullYear(date.getFullYear() + 1, date.getMonth(), 1) + this.goToDate(date, focus) +} + +Datepicker.prototype.focusPreviousYear = function (event, focus = true) { + event.preventDefault() + const date = new Date(this.currentDate) + date.setFullYear(date.getFullYear() - 1, date.getMonth(), 1) + this.goToDate(date, focus) +} + +/** + * + * @param button + * @param index + * @param row + * @param column + * @param picker + * @constructor + */ +function DSCalendarDay(button, index, row, column, picker) { + this.index = index + this.row = row + this.column = column + this.button = button + this.picker = picker + + this.date = new Date() +} + +DSCalendarDay.prototype.init = function () { + this.button.addEventListener('keydown', this.keyPress.bind(this)) + this.button.addEventListener('click', this.click.bind(this)) +} + +DSCalendarDay.prototype.update = function (day, hidden, disabled) { + this.button.innerHTML = day.getDate() + this.date = new Date(day) + + if (disabled) { + this.button.setAttribute('disabled', true) + } else { + this.button.removeAttribute('disabled') + } + + if (hidden) { + this.button.style.display = 'none' + } else { + this.button.style.display = 'block' + } +} + +DSCalendarDay.prototype.click = function (event) { + this.picker.goToDate(this.date) + this.picker.selectDate(this.date) + + event.stopPropagation() + event.preventDefault() +} + +DSCalendarDay.prototype.keyPress = function (event) { + let calendarNavKey = true + + switch (event.keyCode) { + case this.picker.keycodes.left: + this.picker.focusPreviousDay() + break + case this.picker.keycodes.right: + this.picker.focusNextDay() + break + case this.picker.keycodes.up: + this.picker.focusPreviousWeek() + break + case this.picker.keycodes.down: + this.picker.focusNextWeek() + break + case this.picker.keycodes.home: + this.picker.focusFirstDayOfWeek() + break + case this.picker.keycodes.end: + this.picker.focusLastDayOfWeek() + break + case this.picker.keycodes.pageup: + // eslint-disable-next-line no-unused-expressions + event.shiftKey ? this.picker.focusPreviousYear(event) : this.picker.focusPreviousMonth(event) + break + case this.picker.keycodes.pagedown: + // eslint-disable-next-line no-unused-expressions + event.shiftKey ? this.picker.focusNextYear(event) : this.picker.focusNextMonth(event) + break + case this.picker.keycodes.esc: + this.picker.closeDialog() + break + default: + calendarNavKey = false + break + } + + if (calendarNavKey) { + event.preventDefault() + event.stopPropagation() + } +} + +MOJFrontend.DatePicker = Datepicker; diff --git a/src/moj/components/date-picker/macro.njk b/src/moj/components/date-picker/macro.njk new file mode 100644 index 00000000..edff875a --- /dev/null +++ b/src/moj/components/date-picker/macro.njk @@ -0,0 +1,3 @@ +{% macro mojDatePicker(params) %} + {%- include "./template.njk" -%} +{% endmacro %} diff --git a/src/moj/components/date-picker/template.njk b/src/moj/components/date-picker/template.njk new file mode 100644 index 00000000..a384258d --- /dev/null +++ b/src/moj/components/date-picker/template.njk @@ -0,0 +1,64 @@ +{% from "govuk/components/input/macro.njk" import govukInput %} +{% from "govuk/components/label/macro.njk" import govukLabel %} +{% from "govuk/components/hint/macro.njk" import govukHint %} +{% from "govuk/components/error-message/macro.njk" import govukErrorMessage %} + +
+ {{ govukLabel({ + html: params.label.html, + text: params.label.text, + classes: params.label.classes, + isPageHeading: params.label.isPageHeading, + attributes: params.label.attributes, + for: params.id + }) | indent(2) | trim }} + {% if params.hint %} + {% set hintId = params.id + "-hint" %} + {% set describedBy = describedBy + " " + hintId if describedBy else hintId %} + {{ govukHint({ + id: hintId, + classes: params.hint.classes, + attributes: params.hint.attributes, + html: params.hint.html, + text: params.hint.text + }) | indent(2) | trim }} + {% endif %} + {% if params.errorMessage %} + {% set errorId = params.id + "-error" %} + {% set describedBy = describedBy + " " + errorId if describedBy else errorId %} + {{ govukErrorMessage({ + id: errorId, + classes: params.errorMessage.classes, + attributes: params.errorMessage.attributes, + html: params.errorMessage.html, + text: params.errorMessage.text, + visuallyHiddenText: params.errorMessage.visuallyHiddenText + }) | indent(2) | trim }} + {% endif %} +
+ {{ govukInput({ + classes: "govuk-input moj-js-datepicker-input", + id: params.id, + name: params.name, + value: params.value, + autocomplete: "off", + attributes: { + "maxlength": "10", + "data-mindate": params.minDate, + "data-maxdate": params.maxDate + } + }) }} + +
+
From 83cc9e10657f2d2d403fde8b8d2597c8c92df1a7 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 26 Jun 2024 16:54:58 +0100 Subject: [PATCH 02/58] refactor(date picker): updates to submitted datepicker component code Chnages to submitted code to use gov.uk styles where possible. Updates component to being fixed width by default, allowing users to either provide an alternative width class or remove it to have a fluid input. --- docs/_includes/arguments/date-picker.md | 0 src/moj/components/date-picker/README.md | 68 +++++++++++++++++++ .../components/date-picker/_date-picker.scss | 42 +++++------- src/moj/components/date-picker/date-picker.js | 61 +++++++++-------- src/moj/components/date-picker/template.njk | 19 ++++-- 5 files changed, 132 insertions(+), 58 deletions(-) create mode 100644 docs/_includes/arguments/date-picker.md create mode 100644 src/moj/components/date-picker/README.md diff --git a/docs/_includes/arguments/date-picker.md b/docs/_includes/arguments/date-picker.md new file mode 100644 index 00000000..e69de29b diff --git a/src/moj/components/date-picker/README.md b/src/moj/components/date-picker/README.md new file mode 100644 index 00000000..56c7a40b --- /dev/null +++ b/src/moj/components/date-picker/README.md @@ -0,0 +1,68 @@ +## params + +id +classes (these are for the container) +name +value +minDate +maxDate + +label { + html + text + classes + attributes +} +hint { + classes + attributes + html + text +} +errorMessage { + html + text + classes + attributes + visuallyHiddenText +} + + +## Questions / Issues + +### Input width +Possibly need a param to set the width (govuk-width-class) of the text input? +The css has classes for a --fixed class. +This is tricky in terms of API vs what exists. For consistency we could have an +`input` param, but currently all the attrs for the input are not namespaced (id, +value, name), but the non-prefixed `classes` param gets assigned to the container +not the input. +A 'breaking' change would be to use the `classes` param on the input, allowing +users to assign any of the govuk input width modifier classes. And then have a +`containerClasses` param for the container element. +A non-breaking solution would be to have a new param. e.g. `width` but this is +confusing if we expect a css class string. Could have `widthClass` or +`inputWidthClass`... none of these feel ideal though. + +### Header abbreviations +Currently the calendar headers contain abbreviated days (e.g. Mo, Tu) and have +an `abbr` attribute set with the full text. Technically this should be the +other way round, the `abbr` attribute should be for the short version. Need to +check screen reader handling here. Alternative would be an `aria-label` with +the full name. + +### Translations +Do we have a standardised way of doing this yet? +We need welsh days of the week and months - these are static and shouldn;t need +to be provided by the user, so I guess we should pick them up from the `lang` +attribute. + +### Width +Current component is 300px wide (280px + padding) +this matches old iPhone small screens +Figma component is 354px which is probably a more modern +smallest screen size... +Days in Figma are 44px wide - presume WCAG improvemnt? (nope 24*24 is minumum) + +### Label +Label takes the param 'isPageHeading' diff --git a/src/moj/components/date-picker/_date-picker.scss b/src/moj/components/date-picker/_date-picker.scss index fd28c20d..70b01d92 100644 --- a/src/moj/components/date-picker/_date-picker.scss +++ b/src/moj/components/date-picker/_date-picker.scss @@ -1,19 +1,15 @@ .moj-datepicker { position: relative; + @include govuk-font(16); + - &--fixed-width { - .moj-datepicker-input__wrapper { - width: 215px; - } - } &__dialog { box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.15); - background-color: govuk-colour('white'); clear: both; display: none; - padding: 8px; + padding: govuk-spacing(4); outline: 1px solid $govuk-border-colour; outline-offset: -1px; position: static; @@ -187,18 +183,8 @@ margin-top: 8px; } - &__buttongroup { - display: grid; - grid-gap: 0 8px; - margin-right: 0; - overflow: visible; - grid-template-columns: 1fr 1fr; - - > * { - margin-left: 0; - margin-right: 0; - width: auto !important; - } + > .govuk-button-group { + margin-bottom: 0; .govuk-button { margin-bottom: 0; @@ -221,6 +207,16 @@ margin-bottom: 24px; overflow: visible; + .govuk-form-group { + width: 100%; + } + + &--fixed { + .govuk-form-group { + width: auto; + } + } + .govukInput { float: left; margin-bottom: 0; @@ -233,11 +229,10 @@ width: 32px; } - .govuk-form-group { - width: 100%; - } + } + @media (min-width: 768px) { .moj-datepicker { &__dialog { @@ -250,9 +245,6 @@ .moj-datepicker-button { background-color: govuk-colour('black'); fill: govuk-colour('white'); - position: absolute; - right: 0; - bottom: 0; height: 40px; padding-top: 6px; border: none; diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 2d418815..b3901bb5 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -95,7 +95,7 @@ Datepicker.prototype.initControls = function () { this.dialogElement = dialog this.$input.insertAdjacentElement('afterend', this.dialogElement) - this.dialogTitleNode = this.dialogElement.querySelector('.js-datepicker-month-year') + this.dialogTitleNode = this.dialogElement.querySelector('.moj-js-datepicker-month-year') this.setMinAndMaxDatesOnCalendar() @@ -123,17 +123,17 @@ Datepicker.prototype.initControls = function () { } // add event listeners - this.prevMonthButton = this.dialogElement.querySelector('.js-datepicker-prev-month') - this.prevYearButton = this.dialogElement.querySelector('.js-datepicker-prev-year') - this.nextMonthButton = this.dialogElement.querySelector('.js-datepicker-next-month') - this.nextYearButton = this.dialogElement.querySelector('.js-datepicker-next-year') + this.prevMonthButton = this.dialogElement.querySelector('.moj-js-datepicker-prev-month') + this.prevYearButton = this.dialogElement.querySelector('.moj-js-datepicker-prev-year') + this.nextMonthButton = this.dialogElement.querySelector('.moj-js-datepicker-next-month') + this.nextYearButton = this.dialogElement.querySelector('.moj-js-datepicker-next-year') this.prevMonthButton.addEventListener('click', event => this.focusPreviousMonth(event, false)) this.prevYearButton.addEventListener('click', event => this.focusPreviousYear(event, false)) this.nextMonthButton.addEventListener('click', event => this.focusNextMonth(event, false)) this.nextYearButton.addEventListener('click', event => this.focusNextYear(event, false)) - this.cancelButton = this.dialogElement.querySelector('.js-datepicker-cancel') - this.okButton = this.dialogElement.querySelector('.js-datepicker-ok') + this.cancelButton = this.dialogElement.querySelector('.moj-js-datepicker-cancel') + this.okButton = this.dialogElement.querySelector('.moj-js-datepicker-ok') this.cancelButton.addEventListener('click', event => { event.preventDefault() this.closeDialog(event) @@ -144,8 +144,8 @@ Datepicker.prototype.initControls = function () { // eslint-disable-next-line prefer-destructuring this.firstButtonInDialog = dialogButtons[0] this.lastButtonInDialog = dialogButtons[dialogButtons.length - 1] - this.firstButtonInDialog.addEventListener('keydown', event => this.firstButtonKeyup(event)) - this.lastButtonInDialog.addEventListener('keydown', event => this.lastButtonKeyup(event)) + this.firstButtonInDialog.addEventListener('keydown', event => this.firstButtonKeydown(event)) + this.lastButtonInDialog.addEventListener('keydown', event => this.lastButtonKeydown(event)) this.$calendarButton.addEventListener('click', event => this.toggleDialog(event)) @@ -158,52 +158,51 @@ Datepicker.prototype.initControls = function () { Datepicker.prototype.createDialogMarkup = function (titleId) { return `
- -
-

June 2020

+

June 2020

- -
- - +
You can use the arrow keys to select a date
- - - - - - - + + + + + + +
MoTuWeThFrSaSuMonTueWedThuFriSatSun
-
- - +
+ +
` } @@ -272,14 +271,14 @@ Datepicker.prototype.formattedDateHuman = function (date) { return `${this.dayLabels[date.getDay()]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}` } -Datepicker.prototype.firstButtonKeyup = function (event) { +Datepicker.prototype.firstButtonKeydown = function (event) { if (event.keyCode === this.keycodes.tab && event.shiftKey) { this.lastButtonInDialog.focus() event.preventDefault() } } -Datepicker.prototype.lastButtonKeyup = function (event) { +Datepicker.prototype.lastButtonKeydown = function (event) { if (event.keyCode === this.keycodes.tab && !event.shiftKey) { this.firstButtonInDialog.focus() event.preventDefault() @@ -397,7 +396,11 @@ Datepicker.prototype.openDialog = function () { this.dialogElement.classList.add('moj-datepicker__dialog--open') // position the dialog - this.dialogElement.style.left = `${this.$input.offsetWidth + 16}px` + // if input is wider than dialog pin it to the right + if(this.$input.offsetWidth > this.dialogElement.offsetWidth) { + this.dialogElement.style.right = `0px` + } + this.dialogElement.style.top = `${this.$input.offsetHeight + 16}px` // get the date from the input element if (this.$input.value.match(/^(\d{1,2})([-/,. ])(\d{1,2})[-/,. ](\d{4})$/)) { diff --git a/src/moj/components/date-picker/template.njk b/src/moj/components/date-picker/template.njk index a384258d..a658cf03 100644 --- a/src/moj/components/date-picker/template.njk +++ b/src/moj/components/date-picker/template.njk @@ -3,7 +3,7 @@ {% from "govuk/components/hint/macro.njk" import govukHint %} {% from "govuk/components/error-message/macro.njk" import govukErrorMessage %} -
+
{{ govukLabel({ html: params.label.html, text: params.label.text, @@ -35,15 +35,26 @@ visuallyHiddenText: params.errorMessage.visuallyHiddenText }) | indent(2) | trim }} {% endif %} -
+ + {% set fixedWidth = true %} + {% set widthClass = 'govuk-input--width-10' %} + + {% if params.inputWidthClass == '' %} + {% set fixedWidth = false %} + {% set widthClass = '' %} + {% elif params.inputWidthClass %} + {% set fixedWidth = true %} + {% set widthClass = params.inputWidthClass %} + {% endif %} +
{{ govukInput({ - classes: "govuk-input moj-js-datepicker-input", + classes: "govuk-input moj-js-datepicker-input " + widthClass, id: params.id, name: params.name, value: params.value, autocomplete: "off", attributes: { - "maxlength": "10", "data-mindate": params.minDate, "data-maxdate": params.maxDate } From 611c50b7ea1966c1e246f7dc4add515eba02b110 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Thu, 27 Jun 2024 23:18:12 +0100 Subject: [PATCH 03/58] refactor(date picker): update styles to match design system design Amends the styles of the submitted datepicker to match the tweaked design for inclusion in the Design System. Renames some classes to be slightly more consistent with BEM style. Reformat css file to remove nesting to conform to GDS recommended style. --- docs/examples/date-picker/index.njk | 2 +- .../components/date-picker/_date-picker.scss | 358 ++++++++---------- src/moj/components/date-picker/date-picker.js | 63 +-- src/moj/components/date-picker/template.njk | 13 +- 4 files changed, 196 insertions(+), 240 deletions(-) diff --git a/docs/examples/date-picker/index.njk b/docs/examples/date-picker/index.njk index ca0d19f6..0414beb3 100644 --- a/docs/examples/date-picker/index.njk +++ b/docs/examples/date-picker/index.njk @@ -15,5 +15,5 @@ arguments: date-picker hint: { text: "For example, 03/01/2024" }, - value: "03/01/2024" + value: "14/06/2024" }) }} diff --git a/src/moj/components/date-picker/_date-picker.scss b/src/moj/components/date-picker/_date-picker.scss index 70b01d92..0121a537 100644 --- a/src/moj/components/date-picker/_date-picker.scss +++ b/src/moj/components/date-picker/_date-picker.scss @@ -1,270 +1,214 @@ .moj-datepicker { position: relative; @include govuk-font(16); +} +.moj-datepicker-dialog { + display: none; + position: absolute; + top: 0; + min-width: 280px; + padding: govuk-spacing(4); + box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.15); + outline: 1px solid $govuk-border-colour; + outline-offset: -1px; + background-color: govuk-colour('white'); + transition: background-color 0.2s, outline-color 0.2s; + z-index: 2; +} +.moj-datepicker-dialog__header { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: govuk-spacing(2); +} - &__dialog { - box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.15); - background-color: govuk-colour('white'); - clear: both; - display: none; - padding: govuk-spacing(4); - outline: 1px solid $govuk-border-colour; - outline-offset: -1px; - position: static; - top: 0; - transition: background-color 0.2s, outline-color 0.2s; - width: 280px; - z-index: 2; +.moj-datepicker-dialog__title { + @include govuk-font(16); + font-weight: bold; + margin-top: 0; + margin-bottom: 0; +} - &__header { - position: relative; - text-align: center; - margin-bottom: 5px; +.moj-datepicker-dialog__navbuttons { + display: flex; + align-items: center; +} - > :nth-child(1) { - position: absolute; - left: 5px; - top: -2px; +.moj-datepicker-calendar { + border-collapse: collapse; + margin-bottom: govuk-spacing(4); - > :nth-child(2) { - margin-left: 4px; - } - } - - > :nth-child(3) { - position: absolute; - right: 5px; - top: -2px; + tbody:focus-within { + outline: 2px solid $govuk-focus-colour; + } - > :nth-child(1) { - margin-right: 4px; - } - } - } + td { + border: 0; + margin: 0; + outline: 0; + padding: 0; + } - &__title { - font-size: 16px; - padding: 8px 0; - margin: 0 !important; - } + th { + @include govuk-font(16); + font-weight: bold; + color: $govuk-text-colour; + } - &__navbuttons { - button { - background-color: transparent; - color: $govuk-text-colour !important; - min-height: 40px; - margin: 0; - padding: 4px 4px 0 4px; - min-width: 32px; - border: none; - display: inline-block; - cursor: pointer; - outline: none; +} - .moj-datepicker-icon { - height: 32px; - padding: 0; - position: static; - width: 24px; - } +.moj-datepicker-dialog > .govuk-button-group { + margin-bottom: 0; - &:hover { - background-color: rgba(govuk-colour('yellow'), .5); - } + > * { + margin-bottom: 0; + } +} - &:focus { - background-color: $govuk-focus-colour; - border-bottom: 4px solid govuk-colour('black'); - } +.moj-datepicker-button { + @include govuk-font(16); + background-color: transparent; + outline: 3px solid rgba(0, 0, 0, 0); + outline-offset: -3px; + border-width: 0; + color: $govuk-text-colour; + height: 40px; + margin: 0; + padding: 0; + width: 44px; + position: relative; + + @media (forced-colors: active) { + // Don't show the bottom bar in forced-color modes as it blocks the outline + &:after { + display: none } } - &__table { - border-collapse: collapse; - - tbody:focus-within { - outline: 2px solid $govuk-focus-colour; - } + &:after { + content: ""; + position: absolute; + bottom: 0px; + height: 4px; + left: 0; + right: 0; + background-color: transparent; + } - td { - border: 0; - margin: 0; - outline: 0; - padding: 0; - } + &:hover { + color: $govuk-text-colour; + background-color: $govuk-border-colour; + text-decoration: none; + -webkit-box-decoration-break: clone; + box-decoration-break: clone; + cursor: pointer; + } - th { - font-size: 16px; - color: $govuk-text-colour; + &:focus { + color: govuk-colour('black'); + background-color: govuk-colour('yellow'); + outline-color: govuk-colour('yellow'); + text-decoration: none; + -webkit-box-decoration-break: clone; + box-decoration-break: clone; + &:after { + background-color: govuk-colour('black'); } + } - button { - background-color: transparent; - border-width: 0; - color: $govuk-text-colour; - min-height: 40px; - margin: 0; - padding: 0; - min-width: 40px; - - font-size: 16px; - - &:hover { - outline: 3px solid rgba(0,0,0,0); - color: $govuk-text-colour; - background-color: rgba(govuk-colour('yellow'), .5); - box-shadow: none; - text-decoration: none; - -webkit-box-decoration-break: clone; - box-decoration-break: clone; - cursor: pointer; - } - - &:focus { - outline: 3px solid rgba(0,0,0,0); - color: $govuk-focus-text-colour; - background-color: $govuk-focus-colour; - border-bottom: 4px solid govuk-colour('black'); - padding-top: 4px; - text-decoration: none; - -webkit-box-decoration-break: clone; - box-decoration-break: clone; - } - - &[disabled="true"] { - background-color: govuk-colour('light-grey'); - color: $govuk-text-colour; - } - - &.moj-datepicker__current { - $moj-current-outline-width: 2px; - outline: $moj-current-outline-width solid govuk-colour('black') !important; - outline-offset: #{$moj-current-outline-width * -1}; - } - - &.moj-datepicker__current[tabindex="-1"] { - background: transparent; - color: currentColor; - - &:hover { - background-color: rgba(govuk-colour('yellow'), .5); - cursor: pointer; - } - } - - &.moj-datepicker__today { - font-weight: 700; + &:focus:hover { + background-color: govuk-colour('mid-grey'); + } - &::after { - background-color: currentColor; - border-radius: 4px; - content: ''; - height: 4px; - margin-top: -1px; - margin-left: 1px; - position: absolute; - width: 4px; - } - } + &[disabled], + &[disabled]:hover { + background-color: govuk-colour('light-grey'); + color: govuk-colour('black'); + opacity: 0.5; + cursor: not-allowed; + } - &.moj-datepicker-selected:not(:focus) { - background-color: govuk-colour('black'); - color: govuk-colour('white'); - } + &.current { + background: govuk-colour('blue'); + color: govuk-colour('white'); + outline-color: govuk-colour('blue'); + &:after { + background-color: govuk-colour('blue'); } } - &__table-caption { - font-size: 14px; - caption-side: bottom; - line-height: 2; - margin-top: 8px; + &.current[tabindex="-1"] { + background: transparent; + color: currentColor; + outline-color: transparent; + &:after { + background-color: transparent; + } } - > .govuk-button-group { - margin-bottom: 0; + &.today { + border: 1px solid govuk-colour('black'); + } - .govuk-button { - margin-bottom: 0; + &.selected:not(:focus) { + background-color: govuk-colour('blue'); + color: govuk-colour('white'); + &:after { + background-color: govuk-colour('blue'); } } - } - - .govuk-label--m { - margin-bottom: 5px; - } - - .govuk-hint { - margin-bottom: 10px; - } } -.moj-datepicker-input__wrapper { - display: flex; +.moj-datepicker-input-wrapper { position: relative; + display: flex; margin-bottom: 24px; overflow: visible; - .govuk-form-group { + > .govuk-form-group { width: 100%; } - &--fixed { - .govuk-form-group { + &--fixed > .govuk-form-group { width: auto; - } } - - .govukInput { - float: left; - margin-bottom: 0; - margin-right: -48px; - padding-right: 56px; - } - - .moj-datepicker-icon { - height: 24px; - width: 32px; - } - - } - @media (min-width: 768px) { - .moj-datepicker { - &__dialog { - position: absolute; - width: auto; - } + .moj-datepicker-dialog { + width: auto; } } -.moj-datepicker-button { +.moj-datepicker-toggle { background-color: govuk-colour('black'); - fill: govuk-colour('white'); + color: govuk-colour('white'); + outline: 3px solid rgba(0, 0, 0, 0); + outline-offset: -3px; height: 40px; padding-top: 6px; border: none; - border-bottom: 4px solid govuk-colour('black'); - outline: none; + border-bottom: 4px solid rgba(0, 0, 0, 0); cursor: pointer; + &:focus { + background-color: govuk-colour('yellow'); + color: govuk-colour('black'); + border-bottom: 4px solid govuk-colour('black'); + } + &:hover { background-color: govuk-colour('mid-grey'); - fill: govuk-colour('black'); + color: govuk-colour('black'); border-bottom: 4px solid govuk-colour('mid-grey'); } - &:focus { - background-color: $govuk-focus-colour; - fill: $govuk-focus-text-colour; + &:focus:hover { + background-color: govuk-colour('mid-grey'); + color: govuk-colour('black'); border-bottom: 4px solid govuk-colour('black'); } - - @media (max-width: 768px) { - bottom: unset; - } } diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index b3901bb5..62513aff 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -63,7 +63,7 @@ function Datepicker($module, config) { this.$module = $module this.$input = $module.querySelector('.moj-js-datepicker-input') - this.$calendarButton = $module.querySelector('.moj-js-datepicker-button') + this.$calendarButton = $module.querySelector('.moj-js-datepicker-toggle') } /** @@ -86,7 +86,7 @@ Datepicker.prototype.initControls = function () { const titleId = `datepicker-title-${this.$input.id}` const dialog = document.createElement('div') dialog.id = `datepicker-${this.$input.id}` - dialog.setAttribute('class', 'moj-datepicker__dialog datepickerDialog') + dialog.setAttribute('class', 'moj-datepicker-dialog datepickerDialog') dialog.setAttribute('role', 'dialog') dialog.setAttribute('aria-modal', 'true') dialog.setAttribute('aria-labelledby', titleId) @@ -110,7 +110,6 @@ Datepicker.prototype.initControls = function () { // create cell (day) const cell = document.createElement('td') const dateButton = document.createElement('button') - dateButton.dataset.form = 'date-select' cell.appendChild(dateButton) row.appendChild(cell) @@ -134,7 +133,7 @@ Datepicker.prototype.initControls = function () { this.cancelButton = this.dialogElement.querySelector('.moj-js-datepicker-cancel') this.okButton = this.dialogElement.querySelector('.moj-js-datepicker-ok') - this.cancelButton.addEventListener('click', event => { + this.cancelButton.addEventListener('click', (event) => { event.preventDefault() this.closeDialog(event) }) @@ -156,35 +155,45 @@ Datepicker.prototype.initControls = function () { } Datepicker.prototype.createDialogMarkup = function (titleId) { - return `
-
- -
-

June 2020

+

June 2020

-
- -
- +
@@ -321,8 +330,10 @@ Datepicker.prototype.setCurrentDate = function (focus = true) { const { currentDate } = this this.calendarDays.forEach(calendarDay => { + calendarDay.button.classList.add('moj-datepicker-button') + calendarDay.button.classList.add('moj-datepicker-calendar__day') calendarDay.button.setAttribute('tabindex', -1) - calendarDay.button.classList.remove('moj-datepicker-selected') + calendarDay.button.classList.remove('selected') const calendarDayDate = calendarDay.date calendarDayDate.setHours(0, 0, 0, 0) @@ -333,22 +344,22 @@ Datepicker.prototype.setCurrentDate = function (focus = true) { if (focus) { calendarDay.button.setAttribute('tabindex', 0) calendarDay.button.focus() - calendarDay.button.classList.add('moj-datepicker-selected') + calendarDay.button.classList.add('selected') } } if (this.inputDate && calendarDayDate.getTime() === this.inputDate.getTime()) { - calendarDay.button.classList.add('moj-datepicker__current') + calendarDay.button.classList.add('current') calendarDay.button.setAttribute('aria-selected', true) } else { - calendarDay.button.classList.remove('moj-datepicker__current') + calendarDay.button.classList.remove('current') calendarDay.button.removeAttribute('aria-selected') } if (calendarDayDate.getTime() === today.getTime()) { - calendarDay.button.classList.add('moj-datepicker__today') + calendarDay.button.classList.add('today') } else { - calendarDay.button.classList.remove('moj-datepicker__today') + calendarDay.button.classList.remove('today') } }) @@ -377,7 +388,7 @@ Datepicker.prototype.selectDate = function (date) { } Datepicker.prototype.isOpen = function () { - return this.dialogElement.classList.contains('moj-datepicker__dialog--open') + return this.dialogElement.classList.contains('moj-datepicker-dialog--open') } Datepicker.prototype.toggleDialog = function (event) { @@ -393,7 +404,7 @@ Datepicker.prototype.toggleDialog = function (event) { Datepicker.prototype.openDialog = function () { // display the dialog this.dialogElement.style.display = 'block' - this.dialogElement.classList.add('moj-datepicker__dialog--open') + this.dialogElement.classList.add('moj-datepicker-dialog--open') // position the dialog // if input is wider than dialog pin it to the right @@ -414,7 +425,7 @@ Datepicker.prototype.openDialog = function () { Datepicker.prototype.closeDialog = function () { this.dialogElement.style.display = 'none' - this.dialogElement.classList.remove('moj-datepicker__dialog--open') + this.dialogElement.classList.remove('moj-datepicker-dialog--open') this.$calendarButton.focus() } diff --git a/src/moj/components/date-picker/template.njk b/src/moj/components/date-picker/template.njk index a658cf03..7e6bb945 100644 --- a/src/moj/components/date-picker/template.njk +++ b/src/moj/components/date-picker/template.njk @@ -46,8 +46,8 @@ {% set fixedWidth = true %} {% set widthClass = params.inputWidthClass %} {% endif %} -
+
{{ govukInput({ classes: "govuk-input moj-js-datepicker-input " + widthClass, id: params.id, @@ -59,16 +59,17 @@ "data-maxdate": params.maxDate } }) }} -
From 606dd43afee400282886aa92c93ecbe567101c20 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Fri, 28 Jun 2024 11:43:06 +0100 Subject: [PATCH 04/58] refactor(date picker): add calendar button via JS for pregressive enhancement This PR moves the calendar popup toggle from in the template to within the JS, as the button shouldn't show up unless JS is available to toggle the popup dialog. Improvements were made to the accessible labelling of the table headers and the individual day buttons to aid screenreader users. --- .../components/date-picker/_date-picker.scss | 2 +- src/moj/components/date-picker/date-picker.js | 41 +++++++++++++++---- src/moj/components/date-picker/template.njk | 16 +------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/moj/components/date-picker/_date-picker.scss b/src/moj/components/date-picker/_date-picker.scss index 0121a537..60d09c37 100644 --- a/src/moj/components/date-picker/_date-picker.scss +++ b/src/moj/components/date-picker/_date-picker.scss @@ -150,7 +150,7 @@ } &.today { - border: 1px solid govuk-colour('black'); + border: 2px solid govuk-colour('black'); } &.selected:not(:focus) { diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 62513aff..73d0a58f 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -63,7 +63,6 @@ function Datepicker($module, config) { this.$module = $module this.$input = $module.querySelector('.moj-js-datepicker-input') - this.$calendarButton = $module.querySelector('.moj-js-datepicker-toggle') } /** @@ -94,7 +93,9 @@ Datepicker.prototype.initControls = function () { this.dialogElement = dialog this.$input.insertAdjacentElement('afterend', this.dialogElement) + this.$input.parentElement.insertAdjacentHTML('afterend', this.createToggleMarkup() ) + this.$calendarButton = this.$module.querySelector('.moj-js-datepicker-toggle') this.dialogTitleNode = this.dialogElement.querySelector('.moj-js-datepicker-month-year') this.setMinAndMaxDatesOnCalendar() @@ -154,6 +155,22 @@ Datepicker.prototype.initControls = function () { this.updateCalendar() } +Datepicker.prototype.createToggleMarkup = function() { + return `` +} + Datepicker.prototype.createDialogMarkup = function (titleId) { return `
@@ -196,13 +213,13 @@ Datepicker.prototype.createDialogMarkup = function (titleId) {
Mon
- - - - - - - + + + + + + + @@ -539,7 +556,13 @@ DSCalendarDay.prototype.init = function () { } DSCalendarDay.prototype.update = function (day, hidden, disabled) { - this.button.innerHTML = day.getDate() + const dateOptions = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + } + this.button.innerHTML = `${day.toLocaleDateString('en-GB', dateOptions )}` this.date = new Date(day) if (disabled) { diff --git a/src/moj/components/date-picker/template.njk b/src/moj/components/date-picker/template.njk index 7e6bb945..042f1244 100644 --- a/src/moj/components/date-picker/template.njk +++ b/src/moj/components/date-picker/template.njk @@ -3,7 +3,7 @@ {% from "govuk/components/hint/macro.njk" import govukHint %} {% from "govuk/components/error-message/macro.njk" import govukErrorMessage %} -
+
{{ govukLabel({ html: params.label.html, text: params.label.text, @@ -53,24 +53,12 @@ id: params.id, name: params.name, value: params.value, + describedBy: hintId if params.hint, autocomplete: "off", attributes: { "data-mindate": params.minDate, "data-maxdate": params.maxDate } }) }} -
From 0998c4933591ba43197b4daaaceee4fc442bf437 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Fri, 28 Jun 2024 11:47:53 +0100 Subject: [PATCH 05/58] refactor(date picker): remove unnecessary data-button attributes --- src/moj/components/date-picker/date-picker.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 73d0a58f..88495b70 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -174,7 +174,7 @@ Datepicker.prototype.createToggleMarkup = function() { Datepicker.prototype.createDialogMarkup = function (titleId) { return `
- - -
MonTueWedThuFriSatSunMondayTuesdayWednesdayThursdayFridaySaturdaySunday
- - + +
` } From 2f8830e97019f9ff5fdbee366f2afcd5f6efdbb3 Mon Sep 17 00:00:00 2001 From: Rob McCarthy Date: Mon, 1 Jul 2024 09:29:09 +0100 Subject: [PATCH 06/58] docs(date picker component): adding guidance for date picker component --- docs/components/date-picker.md | 122 +++++++++++++++++++++++++-------- 1 file changed, 94 insertions(+), 28 deletions(-) diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index 988c7261..2cc6ec83 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -3,51 +3,117 @@ layout: layouts/component.njk title: Date picker --- -This component has recently been contributed to the MoJ Design System and is being developed. +The date picker component allows users to pick a date by entering a date or choosing from a calendar. -## Status of development +{% example "/examples/date-picker", 220 %} -The below criteria all need to be met for a component to be considered as fully developed for use within the MoJ Design System. +## When to use -This page will be updated as the component is developed. +A date picker helps users choose a date by using a calendar view. This may help users to choose: +- a relative date - for example, last Tuesday or next Wednesday +- a date they don’t commonly use +- today’s date +- available dates only + +The calendar view is not mandatory - users can still input a date into the text field. + +## When not to use + +Do not use if users need to enter a memorable date (e.g. their date of birth) or a date they can easily look up (e.g. an appointment date on a letter they have received). Instead, use the [date input component](https://design-system.service.gov.uk/components/date-input/) from the GOV.UK Design System. + +## Similar or linked components + +The GOV.UK Design System has a [date input component](https://design-system.service.gov.uk/components/date-input/) and a [pattern for asking users for dates](https://design-system.service.gov.uk/patterns/dates/). + +## Hint text +INCLUDE SOMETHING HERE === The date picker default hint text is 17/05/2024. Always include a full-stop at the end. + +INLUDE SOMETHING HERE === If using hint text that is different from the default date, use a date that is within the context of your service. And, consider using numbers that are visually different to avoid confusion for some users. For example, 08/03/2023 can be confusing for some users. + +## Disabled dates + +You can set allowed date ranges if you need a user to pick a date within a date range. Individual dates and date ranges can also be disabled in the calendar view. + +Users may type unavailable or disabled dates in the input field, so error messages will be necessary. + +{% example "/examples/date-picker", 220 %} + +INCLUDE SOMETHING HERE === You can disable specific dates (e.g. 17/10/2024, 18/10/2024, 19/10/2024). + +INCLUDE SOMETHING HERE === You can disable days of the week (e.g. every Saturday and Sunday). + +## From and to dates + +Allow users to pick to and from dates by stacking 2 date pickers together. + +When stacking 2 date pickers horizontally or vertically, apply padding that is consistent with the rest of your product. + +### Vertically stacked + +Multiple date picker components can be vertically stacked. This is useful when used in vertical filters or forms. + +{% example "/examples/date-picker", 220 %} + +### Horizontally stacked + +Multiple date picker components can be horizontally displayed. This is useful when used in horizontal filters. + +{% example "/examples/date-picker", 220 %} + +## Errors + +Follow the guidance in the [GOV.UK Design System](https://design-system.service.gov.uk/components/error-message/) for error messages. + +{% example "/examples/date-picker", 220 %} + +### Error messages in English and Welsh - - + + + - - - - - - + + + - - + + + - - + + + - - + + +
Development criteriaStatusError stateEnglish error messageWelsh error message
WCAG 2.2 compliant - Being reviewed -
HTML / Nunjucks version - In progress - If no date is entered or picked from the calendarEnter or pick a dateNodwch neu dewiswch ddyddiad
Figma version - In progress - If the date entered is in the wrong formatEnter the date in the correct format, for example, 17/5/2024Rhowch y dyddiad yn y fformat cywir, er enghraifft, 17/5/2024
Documentation - Being reviewed - If the date entered does not existThe date you entered must be a real dateRhaid i'r dyddiad a roesoch fod yn ddyddiad go iawn
Researched and tested - Not started - If the date entered is incompleteEnter a complete date, for example, 17/5/2024Nodwch ddyddiad cyflawn, er enghraifft, 17/5/2024
+ +### If you are using multiple date pickers + +Make sure you use error summaries and error messages for each text field. Even if the same errors occur for multiple date pickers. + +## Examples + +Text + +## Considerations + +Whilst the date picker is fully navigable using a keyboard, date pickers can be slow and difficult to use for keyboard only users. + +Another challenge for users, especially those with poor vision or colour blindness, is seeing the unavailable or disabled dates. + +## Contributors + +Thanks to **Dom Billington**, **Eddie Shannon**, **David Middleton**, and the **DPS Connect team** for contributing this component. From 6f9178e9503a82f934f4358d593eedb608c3ae1d Mon Sep 17 00:00:00 2001 From: Rob McCarthy Date: Mon, 1 Jul 2024 10:31:16 +0100 Subject: [PATCH 07/58] docs(date picker component): updating hint text on component example --- docs/examples/date-picker/index.njk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/date-picker/index.njk b/docs/examples/date-picker/index.njk index 0414beb3..8155fb1f 100644 --- a/docs/examples/date-picker/index.njk +++ b/docs/examples/date-picker/index.njk @@ -13,7 +13,7 @@ arguments: date-picker text: "Submission date" }, hint: { - text: "For example, 03/01/2024" + text: "For example, 17/5/2024." }, - value: "14/06/2024" + value: "14/6/2024" }) }} From 5456d6a39e982aa59676e86b9b173cef1bfc844f Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Mon, 1 Jul 2024 11:46:07 +0100 Subject: [PATCH 08/58] feat(date picker): add support for disabling dates and days Allow for disabling of arbitrary dates using the data-disableddays param. Also disable specific days of the week using the data-disableddays param. e.g. disable all weekends with `data-disableddays="saturday sunday"` --- docs/examples/date-picker/index.njk | 8 ++- src/moj/components/date-picker/date-picker.js | 63 ++++++++++++++++--- src/moj/components/date-picker/template.njk | 4 +- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/docs/examples/date-picker/index.njk b/docs/examples/date-picker/index.njk index 0414beb3..c0433435 100644 --- a/docs/examples/date-picker/index.njk +++ b/docs/examples/date-picker/index.njk @@ -13,7 +13,11 @@ arguments: date-picker text: "Submission date" }, hint: { - text: "For example, 03/01/2024" + text: "For example, 17/05/2024." }, - value: "14/06/2024" + minDate: "05/05/2024", + maxDate: "06/07/2024", + value: "17/05/2024", + disabledDates: "20/05/2024 21/05/2024", + disabledDays: "Saturday Sunday" }) }} diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 88495b70..c3d1ccef 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -47,6 +47,8 @@ function Datepicker($module, config) { this.currentDate = new Date() this.currentDate.setHours(0, 0, 0, 0) this.calendarDays = [] + this.disabledDates = [] + this.disabledDays = [] this.keycodes = { tab: 9, @@ -99,6 +101,8 @@ Datepicker.prototype.initControls = function () { this.dialogTitleNode = this.dialogElement.querySelector('.moj-js-datepicker-month-year') this.setMinAndMaxDatesOnCalendar() + this.setDisabledDates() + this.setDisabledDays() // create calendar const tbody = this.dialogElement.querySelector('tbody') @@ -258,6 +262,55 @@ Datepicker.prototype.setMinAndMaxDatesOnCalendar = function () { } } +Datepicker.prototype.setDisabledDates = function() { + if(this.$input.dataset.disableddates) { + this.disabledDates = this.$input.dataset.disableddates + .replace(/\s+/, ' ') + .split(' ') + .map(item => this.formattedDateFromString(item, null)) + .filter(item => item) + } +} + +Datepicker.prototype.setDisabledDays = function () { + if (this.$input.dataset.disableddays) { + // lowercase and arrange dayLabels to put indexOf sunday == 0 for comparison + // with getDay() function + let weekDays = this.dayLabels.map(item => item.toLowerCase()) + weekDays.unshift(weekDays.pop()) + + this.disabledDays = this.$input.dataset.disableddays + .replace(/\s+/, ' ') + .toLowerCase() + .split(' ') + .map(item => weekDays.indexOf(item)) + .filter(item => item !== -1) + } +} + +Datepicker.prototype.isDisabledDate = function (date) { + + if (this.minDate && this.minDate > date) { + return true + } + + if (this.maxDate && this.maxDate < date) { + return true + } + + for (const disabledDate of this.disabledDates) { + if (date.toDateString() === disabledDate.toDateString()) { + return true + } + } + + if (this.disabledDays.includes(date.getDay())) { + return true + } + + return false; +} + Datepicker.prototype.formattedDateFromString = function (dateString, fallback = new Date()) { let formattedDate = null const dateFormatPattern = /(\d{1,2})([-/,. ])(\d{1,2})[-/,. ](\d{4})/ @@ -327,15 +380,7 @@ Datepicker.prototype.updateCalendar = function () { // loop through our days for (let i = 0; i < this.calendarDays.length; i++) { const hidden = thisDay.getMonth() !== day.getMonth() - - let disabled - - if (thisDay < this.minDate) { - disabled = true - } - if (thisDay > this.maxDate) { - disabled = true - } + const disabled = this.isDisabledDate(thisDay) this.calendarDays[i].update(thisDay, hidden, disabled) diff --git a/src/moj/components/date-picker/template.njk b/src/moj/components/date-picker/template.njk index 042f1244..fd3cd092 100644 --- a/src/moj/components/date-picker/template.njk +++ b/src/moj/components/date-picker/template.njk @@ -57,7 +57,9 @@ autocomplete: "off", attributes: { "data-mindate": params.minDate, - "data-maxdate": params.maxDate + "data-maxdate": params.maxDate, + "data-disableddates": params.disabledDates, + "data-disableddays": params.disabledDays } }) }}
From c6b513c325a5781c7bfe297145a21299f782f8b4 Mon Sep 17 00:00:00 2001 From: Rob McCarthy Date: Mon, 1 Jul 2024 11:26:05 +0100 Subject: [PATCH 09/58] docs(date picker component): change to input field and example height --- docs/components/date-picker.md | 10 +++++----- docs/examples/date-picker/index.njk | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index 2cc6ec83..d486088d 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -5,7 +5,7 @@ title: Date picker The date picker component allows users to pick a date by entering a date or choosing from a calendar. -{% example "/examples/date-picker", 220 %} +{% example "/examples/date-picker", 590 %} ## When to use @@ -36,7 +36,7 @@ You can set allowed date ranges if you need a user to pick a date within a date Users may type unavailable or disabled dates in the input field, so error messages will be necessary. -{% example "/examples/date-picker", 220 %} +{% example "/examples/date-picker", 590 %} INCLUDE SOMETHING HERE === You can disable specific dates (e.g. 17/10/2024, 18/10/2024, 19/10/2024). @@ -52,19 +52,19 @@ When stacking 2 date pickers horizontally or vertically, apply padding that is c Multiple date picker components can be vertically stacked. This is useful when used in vertical filters or forms. -{% example "/examples/date-picker", 220 %} +{% example "/examples/date-picker", 590 %} ### Horizontally stacked Multiple date picker components can be horizontally displayed. This is useful when used in horizontal filters. -{% example "/examples/date-picker", 220 %} +{% example "/examples/date-picker", 590 %} ## Errors Follow the guidance in the [GOV.UK Design System](https://design-system.service.gov.uk/components/error-message/) for error messages. -{% example "/examples/date-picker", 220 %} +{% example "/examples/date-picker", 590 %} ### Error messages in English and Welsh diff --git a/docs/examples/date-picker/index.njk b/docs/examples/date-picker/index.njk index 8155fb1f..c236b15b 100644 --- a/docs/examples/date-picker/index.njk +++ b/docs/examples/date-picker/index.njk @@ -10,7 +10,7 @@ arguments: date-picker id: "submisison-date", name: "submisison-date", label: { - text: "Submission date" + text: "Date" }, hint: { text: "For example, 17/5/2024." From 28bdb2faf2dc38d83d65047555a829935f17ee00 Mon Sep 17 00:00:00 2001 From: Rob McCarthy Date: Thu, 4 Jul 2024 08:42:42 +0100 Subject: [PATCH 10/58] docs(documentation change): updating guidance for date picker component --- docs/components/date-picker.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index d486088d..22cc2c6b 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -26,9 +26,9 @@ Do not use if users need to enter a memorable date (e.g. their date of birth) or The GOV.UK Design System has a [date input component](https://design-system.service.gov.uk/components/date-input/) and a [pattern for asking users for dates](https://design-system.service.gov.uk/patterns/dates/). ## Hint text -INCLUDE SOMETHING HERE === The date picker default hint text is 17/05/2024. Always include a full-stop at the end. +ADD SOMETHING HERE === The date picker default hint text is 17/05/2024. Always include a full-stop at the end. -INLUDE SOMETHING HERE === If using hint text that is different from the default date, use a date that is within the context of your service. And, consider using numbers that are visually different to avoid confusion for some users. For example, 08/03/2023 can be confusing for some users. +ADD SOMETHING HERE === If using hint text that is different from the default date, use a date that is within the context of your service. And, consider using numbers that are visually different to avoid confusion for some users. For example, 08/03/2023 can be confusing for some users. ## Disabled dates @@ -38,9 +38,11 @@ Users may type unavailable or disabled dates in the input field, so error messag {% example "/examples/date-picker", 590 %} -INCLUDE SOMETHING HERE === You can disable specific dates (e.g. 17/10/2024, 18/10/2024, 19/10/2024). +ADD SOMETHING HERE === You can disable specific dates (e.g. 17/10/2024, 18/10/2024, 19/10/2024). -INCLUDE SOMETHING HERE === You can disable days of the week (e.g. every Saturday and Sunday). +ADD SOMETHING HERE === You can disable days of the week (e.g. every Saturday and Sunday). + +ADD SOMETHING HERE === Date pickers with lots of disabled dates isn't a good experience for a user. For example, if using a date picker to book an appointment, it may be easier for users to show them a list of available appointments as radio buttons. ## From and to dates @@ -97,6 +99,11 @@ Follow the guidance in the [GOV.UK Design System](https://design-system.service. Enter a complete date, for example, 17/5/2024 Nodwch ddyddiad cyflawn, er enghraifft, 17/5/2024 + + If the date entered is a disabled date + ADD SOMETHING HERE + Nodwch ddyddiad cyflawn, er enghraifft, 17/5/2024 + From f4cff19303717ce62af8066e25e9f9cff9814f3b Mon Sep 17 00:00:00 2001 From: Rob McCarthy Date: Thu, 4 Jul 2024 15:32:51 +0100 Subject: [PATCH 11/58] docs(update to component documentation): adding examples to date picker component guidance --- .../images/date-picker-filter-example.svg | 15 +++++++++++++ .../images/date-picker-question-example.svg | 15 +++++++++++++ docs/components/date-picker.md | 22 ++++++++++++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 docs/assets/images/date-picker-filter-example.svg create mode 100644 docs/assets/images/date-picker-question-example.svg diff --git a/docs/assets/images/date-picker-filter-example.svg b/docs/assets/images/date-picker-filter-example.svg new file mode 100644 index 00000000..aa7882fb --- /dev/null +++ b/docs/assets/images/date-picker-filter-example.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/docs/assets/images/date-picker-question-example.svg b/docs/assets/images/date-picker-question-example.svg new file mode 100644 index 00000000..a69d561c --- /dev/null +++ b/docs/assets/images/date-picker-question-example.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index 22cc2c6b..10234e6a 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -113,7 +113,27 @@ Make sure you use error summaries and error messages for each text field. Even i ## Examples -Text +### Filtering information with a date picker + +

#

+ +Date pickers can be used as a way to filter information on a page. + +Use one date picker to show information related to a single date, or use two date pickers to show information within a date range. + +[View example](#) + +### Asking a question with a date picker + +

#

+ +Date pickers can be used within the conventional one-question-per-page approach for GOV.UK services. + +There are a number of ways that dates can be asked for and provided, inc. using a date input field, a date picker, or even as a list of radio buttons. + +Test your product or service with users to see which work best for their needs. + +[View example](#) ## Considerations From f486c8b84084d760196fb5bb3d4e1907bc55c096 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 3 Jul 2024 12:35:40 +0100 Subject: [PATCH 12/58] feat(date picker): updates to styling, and WIP changes to how disabled dates are handles --- .../components/date-picker/_date-picker.scss | 25 ++++++++++--------- src/moj/components/date-picker/date-picker.js | 20 ++++++++++----- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/moj/components/date-picker/_date-picker.scss b/src/moj/components/date-picker/_date-picker.scss index 60d09c37..75c11bd2 100644 --- a/src/moj/components/date-picker/_date-picker.scss +++ b/src/moj/components/date-picker/_date-picker.scss @@ -9,9 +9,10 @@ top: 0; min-width: 280px; padding: govuk-spacing(4); - box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.15); - outline: 1px solid $govuk-border-colour; - outline-offset: -1px; + /* box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.15); */ + outline: 2px solid $govuk-border-colour; + outline: 2px solid govuk-colour('black'); + outline-offset: -2px; background-color: govuk-colour('white'); transition: background-color 0.2s, outline-color 0.2s; z-index: 2; @@ -98,6 +99,14 @@ background-color: transparent; } + &[aria-disabled="true"], + &[aria-disabled="true"]:hover { + background-color: govuk-colour('light-grey'); + color: govuk-colour('black'); + cursor: not-allowed; + pointer-events: none; + } + &:hover { color: $govuk-text-colour; background-color: $govuk-border-colour; @@ -123,15 +132,7 @@ background-color: govuk-colour('mid-grey'); } - &[disabled], - &[disabled]:hover { - background-color: govuk-colour('light-grey'); - color: govuk-colour('black'); - opacity: 0.5; - cursor: not-allowed; - } - - &.current { + &.current:not(:focus) { background: govuk-colour('blue'); color: govuk-colour('white'); outline-color: govuk-colour('blue'); diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index c3d1ccef..ead42288 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -142,7 +142,9 @@ Datepicker.prototype.initControls = function () { event.preventDefault() this.closeDialog(event) }) - this.okButton.addEventListener('click', () => this.selectDate(this.currentDate)) + this.okButton.addEventListener('click', () => { + this.selectDate(this.currentDate) + }) const dialogButtons = this.dialogElement.querySelectorAll('button:not([disabled="true"])') // eslint-disable-next-line prefer-destructuring @@ -371,7 +373,8 @@ Datepicker.prototype.updateCalendar = function () { const day = this.currentDate const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1) - const dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1 // Change logic to make Monday first day of week, i.e. 0 + // const dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1 // Change logic to make Monday first day of week, i.e. 0 + const dayOfWeek = firstOfMonth.getDay() firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek) @@ -389,6 +392,7 @@ Datepicker.prototype.updateCalendar = function () { } Datepicker.prototype.setCurrentDate = function (focus = true) { + console.log('setCurrentDate') const { currentDate } = this this.calendarDays.forEach(calendarDay => { @@ -402,7 +406,7 @@ Datepicker.prototype.setCurrentDate = function (focus = true) { const today = new Date() today.setHours(0, 0, 0, 0) - if (calendarDayDate.getTime() === currentDate.getTime() && !calendarDay.disabled) { + if (calendarDayDate.getTime() === currentDate.getTime() /* && !calendarDay.button.disabled */) { if (focus) { calendarDay.button.setAttribute('tabindex', 0) calendarDay.button.focus() @@ -438,6 +442,10 @@ Datepicker.prototype.setCurrentDate = function (focus = true) { } Datepicker.prototype.selectDate = function (date) { + if (this.isDisabledDate(date)) { + return + } + this.$calendarButton.querySelector('span').innerText = `Choose date. Selected date is ${this.formattedDateHuman( date, )}` @@ -473,7 +481,7 @@ Datepicker.prototype.openDialog = function () { if(this.$input.offsetWidth > this.dialogElement.offsetWidth) { this.dialogElement.style.right = `0px` } - this.dialogElement.style.top = `${this.$input.offsetHeight + 16}px` + this.dialogElement.style.top = `${this.$input.offsetHeight + 3}px` // get the date from the input element if (this.$input.value.match(/^(\d{1,2})([-/,. ])(\d{1,2})[-/,. ](\d{4})$/)) { @@ -611,9 +619,9 @@ DSCalendarDay.prototype.update = function (day, hidden, disabled) { this.date = new Date(day) if (disabled) { - this.button.setAttribute('disabled', true) + this.button.setAttribute('aria-disabled', true) } else { - this.button.removeAttribute('disabled') + this.button.removeAttribute('aria-disabled') } if (hidden) { From 7fdecfbb474e4b3f3aff886f2429317e58591b2e Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Mon, 8 Jul 2024 10:59:54 +0100 Subject: [PATCH 13/58] feat(date picker): update disabled and hover styles to match figma designs --- .../components/date-picker/_date-picker.scss | 26 ++++++++++++++----- src/moj/components/date-picker/date-picker.js | 10 +++---- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/moj/components/date-picker/_date-picker.scss b/src/moj/components/date-picker/_date-picker.scss index 75c11bd2..d1ea5082 100644 --- a/src/moj/components/date-picker/_date-picker.scss +++ b/src/moj/components/date-picker/_date-picker.scss @@ -9,8 +9,6 @@ top: 0; min-width: 280px; padding: govuk-spacing(4); - /* box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.15); */ - outline: 2px solid $govuk-border-colour; outline: 2px solid govuk-colour('black'); outline-offset: -2px; background-color: govuk-colour('white'); @@ -72,8 +70,8 @@ .moj-datepicker-button { @include govuk-font(16); background-color: transparent; - outline: 3px solid rgba(0, 0, 0, 0); - outline-offset: -3px; + outline: 2px solid rgba(0, 0, 0, 0); + outline-offset: -2px; border-width: 0; color: $govuk-text-colour; height: 40px; @@ -104,7 +102,7 @@ background-color: govuk-colour('light-grey'); color: govuk-colour('black'); cursor: not-allowed; - pointer-events: none; + /* pointer-events: none; */ } &:hover { @@ -130,10 +128,14 @@ &:focus:hover { background-color: govuk-colour('mid-grey'); + outline-color: govuk-colour('yellow'); + &:after { + background-color: transparent; + } } &.current:not(:focus) { - background: govuk-colour('blue'); + background-color: govuk-colour('blue'); color: govuk-colour('white'); outline-color: govuk-colour('blue'); &:after { @@ -157,10 +159,22 @@ &.selected:not(:focus) { background-color: govuk-colour('blue'); color: govuk-colour('white'); + &:after { background-color: govuk-colour('blue'); } + + &:hover { + outline-color: govuk-colour('blue'); + background-color: govuk-colour('mid-grey'); + color: govuk-colour('black'); + + &:after { + background-color: transparent; + } + } } + } .moj-datepicker-input-wrapper { diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index ead42288..dc8ba4a4 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -503,11 +503,11 @@ Datepicker.prototype.goToDate = function (date, focus) { const current = this.currentDate this.currentDate = date - if (this.minDate && this.minDate > date) { - this.currentDate = this.minDate - } else if (this.maxDate && this.maxDate < date) { - this.currentDate = this.maxDate - } + // if (this.minDate && this.minDate > date) { + // this.currentDate = this.minDate + // } else if (this.maxDate && this.maxDate < date) { + // this.currentDate = this.maxDate + // } if (current.getMonth() !== this.currentDate.getMonth() || current.getFullYear() !== this.currentDate.getFullYear()) { this.updateCalendar() From 5b52feeca8a99bbf401a7bf4a5490c46f1977682 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Mon, 8 Jul 2024 12:31:54 +0100 Subject: [PATCH 14/58] feat(date picker): add leadingzeros config parameter Add config option for whether or not the date inserted into the field on selection has leadingzeros for days and months --- src/moj/components/date-picker/date-picker.js | 41 +++++++++++++++---- src/moj/components/date-picker/template.njk | 3 +- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index dc8ba4a4..5d13e7a7 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -10,6 +10,7 @@ * @property {string} [hint] - . * @property {string} [minDate] - . * @property {string} [maxDate] - . + * @property {Boolean} [leadingZeroes] - Whether to add leading zeroes when populating the field */ /** @@ -25,6 +26,7 @@ function Datepicker($module, config) { } const defaultConfig = { imagePath: '/assets/images/', + leadingZeros: false, } this.config = { ...defaultConfig, ...config } @@ -100,9 +102,7 @@ Datepicker.prototype.initControls = function () { this.$calendarButton = this.$module.querySelector('.moj-js-datepicker-toggle') this.dialogTitleNode = this.dialogElement.querySelector('.moj-js-datepicker-month-year') - this.setMinAndMaxDatesOnCalendar() - this.setDisabledDates() - this.setDisabledDays() + this.setOptions() // create calendar const tbody = this.dialogElement.querySelector('tbody') @@ -238,16 +238,23 @@ Datepicker.prototype.createDialogMarkup = function (titleId) {
` } -Datepicker.prototype.leadingZeroes = function (value, length = 2) { +Datepicker.prototype.leadingZeros = function (value, length = 2) { let ret = value.toString() while (ret.length < length) { - ret = `0${ret.toString()}` + ret = `0${ret}` } return ret } +Datepicker.prototype.setOptions = function() { + this.setMinAndMaxDatesOnCalendar() + this.setDisabledDates() + this.setDisabledDays() + this.setLeadingZeros() +} + Datepicker.prototype.setMinAndMaxDatesOnCalendar = function () { if (this.$input.dataset.mindate) { this.minDate = this.formattedDateFromString(this.$input.dataset.mindate, null) @@ -290,6 +297,17 @@ Datepicker.prototype.setDisabledDays = function () { } } +Datepicker.prototype.setLeadingZeros = function() { + if (this.$input.dataset.leadingzeros) { + if(this.$input.dataset.leadingzeros.toLowerCase() === 'true') { + this.config.leadingZeros = true; + } + if(this.$input.dataset.leadingzeros.toLowerCase() === 'false') { + this.config.leadingZeros = false; + } + } +} + Datepicker.prototype.isDisabledDate = function (date) { if (this.minDate && this.minDate > date) { @@ -333,7 +351,11 @@ Datepicker.prototype.formattedDateFromString = function (dateString, fallback = } Datepicker.prototype.formattedDateFromDate = function (date) { - return `${this.leadingZeroes(date.getDate())}/${this.leadingZeroes(date.getMonth() + 1)}/${date.getFullYear()}` + if(this.config.leadingZeros) { + return `${this.leadingZeros(date.getDate())}/${this.leadingZeros(date.getMonth() + 1)}/${date.getFullYear()}` + } else { + return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}` + } } Datepicker.prototype.backgroundClick = function (event) { @@ -373,8 +395,8 @@ Datepicker.prototype.updateCalendar = function () { const day = this.currentDate const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1) - // const dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1 // Change logic to make Monday first day of week, i.e. 0 - const dayOfWeek = firstOfMonth.getDay() + const dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1 // Change logic to make Monday first day of week, i.e. 0 + // const dayOfWeek = firstOfMonth.getDay() firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek) @@ -392,7 +414,6 @@ Datepicker.prototype.updateCalendar = function () { } Datepicker.prototype.setCurrentDate = function (focus = true) { - console.log('setCurrentDate') const { currentDate } = this this.calendarDays.forEach(calendarDay => { @@ -489,6 +510,8 @@ Datepicker.prototype.openDialog = function () { this.currentDate = this.inputDate } + console.log(this.currentDate) + this.updateCalendar() this.setCurrentDate() } diff --git a/src/moj/components/date-picker/template.njk b/src/moj/components/date-picker/template.njk index fd3cd092..37989615 100644 --- a/src/moj/components/date-picker/template.njk +++ b/src/moj/components/date-picker/template.njk @@ -59,7 +59,8 @@ "data-mindate": params.minDate, "data-maxdate": params.maxDate, "data-disableddates": params.disabledDates, - "data-disableddays": params.disabledDays + "data-disableddays": params.disabledDays, + "data-leadingzeros": params.leadingZeros } }) }}
From c942b2eb07800fc09907acbffa244637e4a6b60a Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Mon, 8 Jul 2024 15:21:13 +0100 Subject: [PATCH 15/58] feat(date picker): add config option for week start day Allows a weekStartDay option to be passed to the component to configure whether weeks start on a monday or sunday --- src/moj/components/date-picker/date-picker.js | 50 ++++++++++++++----- src/moj/components/date-picker/template.njk | 3 +- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 5d13e7a7..8752b51c 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -27,10 +27,12 @@ function Datepicker($module, config) { const defaultConfig = { imagePath: '/assets/images/', leadingZeros: false, + weekStartDay: 'monday' } this.config = { ...defaultConfig, ...config } this.dayLabels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + this.monthLabels = [ 'January', 'February', @@ -78,6 +80,7 @@ Datepicker.prototype.init = function () { return } + this.setOptions() this.initControls() } @@ -95,14 +98,15 @@ Datepicker.prototype.initControls = function () { dialog.setAttribute('aria-labelledby', titleId) dialog.innerHTML = this.createDialogMarkup(titleId) + this.dialogElement = dialog + this.createCalendarHeaders() this.$input.insertAdjacentElement('afterend', this.dialogElement) this.$input.parentElement.insertAdjacentHTML('afterend', this.createToggleMarkup() ) this.$calendarButton = this.$module.querySelector('.moj-js-datepicker-toggle') this.dialogTitleNode = this.dialogElement.querySelector('.moj-js-datepicker-month-year') - this.setOptions() // create calendar const tbody = this.dialogElement.querySelector('tbody') @@ -218,15 +222,7 @@ Datepicker.prototype.createDialogMarkup = function (titleId) { - - - - - - - - - + @@ -238,6 +234,15 @@ Datepicker.prototype.createDialogMarkup = function (titleId) { ` } +Datepicker.prototype.createCalendarHeaders = function() { + console.log(this.dayLabels) + this.dayLabels.forEach( (day) => { + const html = `` + const headerRow = this.dialogElement.querySelector('thead > tr') + headerRow.insertAdjacentHTML('beforeend', html) + }) +} + Datepicker.prototype.leadingZeros = function (value, length = 2) { let ret = value.toString() @@ -253,6 +258,7 @@ Datepicker.prototype.setOptions = function() { this.setDisabledDates() this.setDisabledDays() this.setLeadingZeros() + this.setWeekStartDay() } Datepicker.prototype.setMinAndMaxDatesOnCalendar = function () { @@ -286,7 +292,9 @@ Datepicker.prototype.setDisabledDays = function () { // lowercase and arrange dayLabels to put indexOf sunday == 0 for comparison // with getDay() function let weekDays = this.dayLabels.map(item => item.toLowerCase()) - weekDays.unshift(weekDays.pop()) + if(this.config.weekStartDay === 'monday') { + weekDays.unshift(weekDays.pop()) + } this.disabledDays = this.$input.dataset.disableddays .replace(/\s+/, ' ') @@ -308,6 +316,17 @@ Datepicker.prototype.setLeadingZeros = function() { } } +Datepicker.prototype.setWeekStartDay = function() { + const weekStartDayParam = this.$input.dataset.weekstartday; + if(weekStartDayParam.toLowerCase() === 'sunday' ) { + this.config.weekStartDay = 'sunday' + this.dayLabels.unshift(this.dayLabels.pop()) + } + if(weekStartDayParam.toLowerCase() === 'monday' ) { + this.config.weekStartDay = 'monday' + } +} + Datepicker.prototype.isDisabledDate = function (date) { if (this.minDate && this.minDate > date) { @@ -395,8 +414,13 @@ Datepicker.prototype.updateCalendar = function () { const day = this.currentDate const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1) - const dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1 // Change logic to make Monday first day of week, i.e. 0 - // const dayOfWeek = firstOfMonth.getDay() + let dayOfWeek; + + if ( this.config.weekStartDay === 'monday') { + dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1 // Change logic to make Monday first day of week, i.e. 0 + } else { + dayOfWeek = firstOfMonth.getDay() + } firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek) diff --git a/src/moj/components/date-picker/template.njk b/src/moj/components/date-picker/template.njk index 37989615..9524e90d 100644 --- a/src/moj/components/date-picker/template.njk +++ b/src/moj/components/date-picker/template.njk @@ -60,7 +60,8 @@ "data-maxdate": params.maxDate, "data-disableddates": params.disabledDates, "data-disableddays": params.disabledDays, - "data-leadingzeros": params.leadingZeros + "data-leadingzeros": params.leadingZeros, + "data-weekstartday": params.weekstartday } }) }} From 347aced2174962ed3c5127b23a854e14577b7439 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Mon, 8 Jul 2024 16:38:14 +0100 Subject: [PATCH 16/58] feat(date picker): allow passing date ranges to disabledDates In order to make it easier to disabled a block of dates it is now possible to pass date ranges in the format "19/7/2024-26/7/24" in the disabledDates parameter --- .../components/date-picker/_date-picker.scss | 2 +- src/moj/components/date-picker/date-picker.js | 22 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/moj/components/date-picker/_date-picker.scss b/src/moj/components/date-picker/_date-picker.scss index d1ea5082..384239d8 100644 --- a/src/moj/components/date-picker/_date-picker.scss +++ b/src/moj/components/date-picker/_date-picker.scss @@ -117,7 +117,7 @@ &:focus { color: govuk-colour('black'); background-color: govuk-colour('yellow'); - outline-color: govuk-colour('yellow'); + outline-color: transparent; text-decoration: none; -webkit-box-decoration-break: clone; box-decoration-break: clone; diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 8752b51c..f44c4628 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -235,7 +235,6 @@ Datepicker.prototype.createDialogMarkup = function (titleId) { } Datepicker.prototype.createCalendarHeaders = function() { - console.log(this.dayLabels) this.dayLabels.forEach( (day) => { const html = `` const headerRow = this.dialogElement.querySelector('thead > tr') @@ -282,9 +281,26 @@ Datepicker.prototype.setDisabledDates = function() { this.disabledDates = this.$input.dataset.disableddates .replace(/\s+/, ' ') .split(' ') - .map(item => this.formattedDateFromString(item, null)) + .map((item) => { + if (item.includes('-')) { + const [startDate, endDate] = item.split('-').map(d => this.formattedDateFromString(d, null)) + if (startDate && endDate) { + const date = new Date(startDate.getTime()); + const dates = []; + while (date <= endDate) { + dates.push(new Date(date)); + date.setDate(date.getDate() + 1); + } + return dates + } + } else { + return this.formattedDateFromString(item, null) + } + }) + .flat() .filter(item => item) } + } Datepicker.prototype.setDisabledDays = function () { @@ -534,8 +550,6 @@ Datepicker.prototype.openDialog = function () { this.currentDate = this.inputDate } - console.log(this.currentDate) - this.updateCalendar() this.setCurrentDate() } From aa4e78c771e78c464076c91ae7170650253844c3 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Mon, 8 Jul 2024 17:34:53 +0100 Subject: [PATCH 17/58] feat(date picker): add examples to date picker guidance page --- .eleventy.js | 2 ++ docs/components/date-picker.md | 6 ++++- .../date-picker-disabled-dates/index.njk | 20 +++++++++++++++++ .../date-picker-disabled-days/index.njk | 18 +++++++++++++++ docs/examples/date-picker-min-max/index.njk | 22 +++++++++++++++++++ docs/examples/date-picker/index.njk | 7 +++--- 6 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 docs/examples/date-picker-disabled-dates/index.njk create mode 100644 docs/examples/date-picker-disabled-days/index.njk create mode 100644 docs/examples/date-picker-min-max/index.njk diff --git a/.eleventy.js b/.eleventy.js index a4cdd54e..996107d5 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -98,6 +98,8 @@ module.exports = function (eleventyConfig) { }); }); + eleventyConfig.addShortcode("dateInCurrentMonth", (day) => `${day}/${new Date().getMonth()+1}/${new Date().getFullYear()}`); + eleventyConfig.addShortcode("lastUpdated", function (component) { if (process.env.STAGING) return ''; diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index 10234e6a..72a0cb84 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -36,12 +36,16 @@ You can set allowed date ranges if you need a user to pick a date within a date Users may type unavailable or disabled dates in the input field, so error messages will be necessary. -{% example "/examples/date-picker", 590 %} +{% example "/examples/date-picker-min-max", 590 %} ADD SOMETHING HERE === You can disable specific dates (e.g. 17/10/2024, 18/10/2024, 19/10/2024). +{% example "/examples/date-picker-disabled-dates", 590 %} + ADD SOMETHING HERE === You can disable days of the week (e.g. every Saturday and Sunday). +{% example "/examples/date-picker-disabled-days", 590 %} + ADD SOMETHING HERE === Date pickers with lots of disabled dates isn't a good experience for a user. For example, if using a date picker to book an appointment, it may be easier for users to show them a list of available appointments as radio buttons. ## From and to dates diff --git a/docs/examples/date-picker-disabled-dates/index.njk b/docs/examples/date-picker-disabled-dates/index.njk new file mode 100644 index 00000000..5c25bcb2 --- /dev/null +++ b/docs/examples/date-picker-disabled-dates/index.njk @@ -0,0 +1,20 @@ +--- +layout: layouts/example.njk +title: Date Picker Disabled Dates (example) +--- + +{%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} + +{% set disabledDates %}{% dateInCurrentMonth 05 %} {% dateInCurrentMonth 12 %} {% dateInCurrentMonth 18 %}-{% dateInCurrentMonth 25 %} {% endset %} + +{{ mojDatePicker({ + id: "date", + name: "date", + label: { + text: "Date" + }, + hint: { + text: "For example, 17/5/2024." + }, + disabledDates: disabledDates +}) }} diff --git a/docs/examples/date-picker-disabled-days/index.njk b/docs/examples/date-picker-disabled-days/index.njk new file mode 100644 index 00000000..d80303a7 --- /dev/null +++ b/docs/examples/date-picker-disabled-days/index.njk @@ -0,0 +1,18 @@ +--- +layout: layouts/example.njk +title: Date Picker Disabled Days (example) +--- + +{%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} + +{{ mojDatePicker({ + id: "date", + name: "date", + label: { + text: "Date" + }, + hint: { + text: "For example, 17/5/2024." + }, + disabledDays: "saturday sunday" +}) }} diff --git a/docs/examples/date-picker-min-max/index.njk b/docs/examples/date-picker-min-max/index.njk new file mode 100644 index 00000000..4a88ae51 --- /dev/null +++ b/docs/examples/date-picker-min-max/index.njk @@ -0,0 +1,22 @@ +--- +layout: layouts/example.njk +title: Date Picker Min and Max Date (example) +--- + +{%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} + +{% set minDate %}{% dateInCurrentMonth 05 %}{% endset %} +{% set maxDate %}{% dateInCurrentMonth 25 %}{% endset %} + +{{ mojDatePicker({ + id: "date", + name: "date", + label: { + text: "Date" + }, + hint: { + text: "For example, 17/5/2024." + }, + minDate: minDate, + maxDate: maxDate +}) }} diff --git a/docs/examples/date-picker/index.njk b/docs/examples/date-picker/index.njk index c236b15b..5efa828f 100644 --- a/docs/examples/date-picker/index.njk +++ b/docs/examples/date-picker/index.njk @@ -7,13 +7,12 @@ arguments: date-picker {%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} {{ mojDatePicker({ - id: "submisison-date", - name: "submisison-date", + id: "date", + name: "date", label: { text: "Date" }, hint: { text: "For example, 17/5/2024." - }, - value: "14/6/2024" + } }) }} From 10d57c550e77d5c0952217a47934efee3bbc3dfb Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 10 Jul 2024 09:44:16 +0100 Subject: [PATCH 18/58] ci(dockerfile): remove asset copy directive from the dockerfile Copying the assets directory is no longer needed as the assets are now within the docs directory --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a4990e4c..c9726233 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,6 @@ COPY package.json package.json COPY package-lock.json package-lock.json RUN npm ci -COPY assets assets COPY docs docs COPY src src COPY package package From 01b498ec946670d3aa2c910e4c5d55a51e2844ec Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 10 Jul 2024 13:47:16 +0100 Subject: [PATCH 19/58] feat(date picker): add horizontal and vertical pairs example --- docs/components/date-picker.md | 4 +-- .../date-picker-horizontal-pair/index.njk | 34 +++++++++++++++++++ .../date-picker-vertical-pair/index.njk | 28 +++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 docs/examples/date-picker-horizontal-pair/index.njk create mode 100644 docs/examples/date-picker-vertical-pair/index.njk diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index 72a0cb84..a47b62dc 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -58,13 +58,13 @@ When stacking 2 date pickers horizontally or vertically, apply padding that is c Multiple date picker components can be vertically stacked. This is useful when used in vertical filters or forms. -{% example "/examples/date-picker", 590 %} +{% example "/examples/date-picker-vertical-pair", 650 %} ### Horizontally stacked Multiple date picker components can be horizontally displayed. This is useful when used in horizontal filters. -{% example "/examples/date-picker", 590 %} +{% example "/examples/date-picker-horizontal-pair", 590 %} ## Errors diff --git a/docs/examples/date-picker-horizontal-pair/index.njk b/docs/examples/date-picker-horizontal-pair/index.njk new file mode 100644 index 00000000..9374144b --- /dev/null +++ b/docs/examples/date-picker-horizontal-pair/index.njk @@ -0,0 +1,34 @@ +--- +layout: layouts/example.njk +title: Date Picker Vertical Pair (example) +--- + +{%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} + +
+
+{{ mojDatePicker({ + id: "from-date", + name: "from-date", + label: { + text: "From" + }, + hint: { + text: "For example, 17/5/2024." + } +}) }} +
+
+ +{{ mojDatePicker({ + id: "to-date", + name: "to-date", + label: { + text: "To" + }, + hint: { + text: "For example, 17/5/2024." + } +}) }} +
+
diff --git a/docs/examples/date-picker-vertical-pair/index.njk b/docs/examples/date-picker-vertical-pair/index.njk new file mode 100644 index 00000000..c35818a3 --- /dev/null +++ b/docs/examples/date-picker-vertical-pair/index.njk @@ -0,0 +1,28 @@ +--- +layout: layouts/example.njk +title: Date Picker Vertical Pair (example) +--- + +{%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} + +{{ mojDatePicker({ + id: "from-date", + name: "from-date", + label: { + text: "From" + }, + hint: { + text: "For example, 17/5/2024." + } +}) }} + +{{ mojDatePicker({ + id: "to-date", + name: "to-date", + label: { + text: "To" + }, + hint: { + text: "For example, 17/5/2024." + } +}) }} From 3fa2db0c543c1ea8494fef517df37f5ecd7290e6 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Thu, 11 Jul 2024 14:50:58 +0100 Subject: [PATCH 20/58] refactor(date picker): refactor date picker template to be cleaner The previous template resulted in nested moj-form-group elements causing issues with error states. The template has now been refactored to more fully utilise the govuk-input macro and use the govuk-attributes macro too to impriove and simplify the external api to the component. --- docs/components/date-picker.md | 2 +- docs/examples/date-picker-error/index.njk | 21 ++++ package-lock.json | 14 +-- package.json | 2 +- .../components/date-picker/_date-picker.scss | 75 ++++++++++++-- src/moj/components/date-picker/date-picker.js | 42 +++++--- src/moj/components/date-picker/template.njk | 98 ++++++++----------- 7 files changed, 162 insertions(+), 92 deletions(-) create mode 100644 docs/examples/date-picker-error/index.njk diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index a47b62dc..d5df1a73 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -70,7 +70,7 @@ Multiple date picker components can be horizontally displayed. This is useful wh Follow the guidance in the [GOV.UK Design System](https://design-system.service.gov.uk/components/error-message/) for error messages. -{% example "/examples/date-picker", 590 %} +{% example "/examples/date-picker-error", 590 %} ### Error messages in English and Welsh diff --git a/docs/examples/date-picker-error/index.njk b/docs/examples/date-picker-error/index.njk new file mode 100644 index 00000000..cc368b6b --- /dev/null +++ b/docs/examples/date-picker-error/index.njk @@ -0,0 +1,21 @@ +--- +layout: layouts/example.njk +title: Date Picker (example) +arguments: date-picker +--- + +{%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} + +{{ mojDatePicker({ + id: "date", + name: "date", + label: { + text: "Date" + }, + hint: { + text: "For example, 17/5/2024." + }, + errorMessage: { + text: 'Enter or pick a date' + } +}) }} diff --git a/package-lock.json b/package-lock.json index cfacd208..c9d8df06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "clipboard": "^2.0.8", "del": "^7.0.0", "esbuild": "^0.23.0", - "govuk-frontend": "^5.0.0", + "govuk-frontend": "^5.4.0", "gulp": "^4.0.2", "gulp-cache": "^1.1.3", "gulp-concat": "^2.6.1", @@ -10947,9 +10947,9 @@ } }, "node_modules/govuk-frontend": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-5.1.0.tgz", - "integrity": "sha512-Dc3J+uOI4i2VR3BVyfxbf6qVjTT4n4bBqbD0/Io6feP8pt/4IfKdP1vWimZf+BwMKKMXacw10hmdy5UcD6Cr8w==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-5.4.0.tgz", + "integrity": "sha512-F3YwQYrYQqIPfNxsoph6O78Ey1unCB6cy6omx8KeWY9G504lWZFBSIaiUCma1jNLw9bOUU7Ui+tXG09jjqy0Mw==", "engines": { "node": ">= 4.2.0" } @@ -32054,9 +32054,9 @@ } }, "govuk-frontend": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-5.1.0.tgz", - "integrity": "sha512-Dc3J+uOI4i2VR3BVyfxbf6qVjTT4n4bBqbD0/Io6feP8pt/4IfKdP1vWimZf+BwMKKMXacw10hmdy5UcD6Cr8w==" + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-5.4.0.tgz", + "integrity": "sha512-F3YwQYrYQqIPfNxsoph6O78Ey1unCB6cy6omx8KeWY9G504lWZFBSIaiUCma1jNLw9bOUU7Ui+tXG09jjqy0Mw==" }, "graceful-fs": { "version": "4.2.10", diff --git a/package.json b/package.json index 704aa426..820feb99 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "clipboard": "^2.0.8", "del": "^7.0.0", "esbuild": "^0.23.0", - "govuk-frontend": "^5.0.0", + "govuk-frontend": "^5.4.0", "gulp": "^4.0.2", "gulp-cache": "^1.1.3", "gulp-concat": "^2.6.1", diff --git a/src/moj/components/date-picker/_date-picker.scss b/src/moj/components/date-picker/_date-picker.scss index 384239d8..48b58bb7 100644 --- a/src/moj/components/date-picker/_date-picker.scss +++ b/src/moj/components/date-picker/_date-picker.scss @@ -177,21 +177,78 @@ } -.moj-datepicker-input-wrapper { - position: relative; - display: flex; - margin-bottom: 24px; - overflow: visible; +/* + Default input with to .govuk-input--width-10 (10 chars) + Allow that to be overriden by the input width modifiers or global width overrides. + Width classes less than 10ch not included as that is narrower than a date. +*/ +.moj-datepicker input { + max-width: 11.5em; // govuk-input--width-10 + + &.govuk-input--width-30 { + max-width: 29.5em; + } + + &.govuk-input--width-20 { + max-width: 20.5em; + } + + &.govuk-\!-width-full { + width: 100% !important; + max-width: none; + } - > .govuk-form-group { - width: 100%; + &.govuk-\!-width-three-quarters { + width: 100% !important; + max-width: none; + + @include govuk-media-query($from: tablet) { + width: 75% !important; + } } - &--fixed > .govuk-form-group { - width: auto; + &.govuk-\!-width-two-thirds { + width: 100% !important; + max-width: none; + + @include govuk-media-query($from: tablet) { + width: 66.66% !important; + } } + + &.govuk-\!-width-one-half { + width: 100% !important; + max-width: none; + + @include govuk-media-query($from: tablet) { + width: 50% !important; + } + } + + &.govuk-\!-width-one-third { + width: 100% !important; + max-width: none; + + @include govuk-media-query($from: tablet) { + width: 33.33% !important; + } + } + + &.govuk-\!-width-one-quarter { + width: 100% !important; + max-width: none; + + @include govuk-media-query($from: tablet) { + width: 25% !important; + } + } +} + +.moj-datepicker__wrapper { + position: relative; } + @media (min-width: 768px) { .moj-datepicker-dialog { width: auto; diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index f44c4628..c7ed43cb 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -101,8 +101,18 @@ Datepicker.prototype.initControls = function () { this.dialogElement = dialog this.createCalendarHeaders() - this.$input.insertAdjacentElement('afterend', this.dialogElement) - this.$input.parentElement.insertAdjacentHTML('afterend', this.createToggleMarkup() ) + + const pickerWrapper = document.createElement('div') + const inputWrapper = document.createElement('div') + pickerWrapper.classList.add('moj-datepicker__wrapper') + inputWrapper.classList.add('govuk-input__wrapper') + + this.$input.parentNode.insertBefore(pickerWrapper, this.$input) + pickerWrapper.appendChild(inputWrapper) + inputWrapper.appendChild(this.$input) + + inputWrapper.insertAdjacentHTML('beforeend', this.createToggleMarkup() ) + pickerWrapper.insertAdjacentElement('beforeend', this.dialogElement) this.$calendarButton = this.$module.querySelector('.moj-js-datepicker-toggle') this.dialogTitleNode = this.dialogElement.querySelector('.moj-js-datepicker-month-year') @@ -261,15 +271,15 @@ Datepicker.prototype.setOptions = function() { } Datepicker.prototype.setMinAndMaxDatesOnCalendar = function () { - if (this.$input.dataset.mindate) { - this.minDate = this.formattedDateFromString(this.$input.dataset.mindate, null) + if (this.$module.dataset.mindate) { + this.minDate = this.formattedDateFromString(this.$module.dataset.mindate, null) if (this.minDate && this.currentDate < this.minDate) { this.currentDate = this.minDate } } - if (this.$input.dataset.maxdate) { - this.maxDate = this.formattedDateFromString(this.$input.dataset.maxdate, null) + if (this.$module.dataset.maxdate) { + this.maxDate = this.formattedDateFromString(this.$module.dataset.maxdate, null) if (this.maxDate && this.currentDate > this.maxDate) { this.currentDate = this.maxDate } @@ -277,8 +287,8 @@ Datepicker.prototype.setMinAndMaxDatesOnCalendar = function () { } Datepicker.prototype.setDisabledDates = function() { - if(this.$input.dataset.disableddates) { - this.disabledDates = this.$input.dataset.disableddates + if(this.$module.dataset.disableddates) { + this.disabledDates = this.$module.dataset.disableddates .replace(/\s+/, ' ') .split(' ') .map((item) => { @@ -304,7 +314,7 @@ Datepicker.prototype.setDisabledDates = function() { } Datepicker.prototype.setDisabledDays = function () { - if (this.$input.dataset.disableddays) { + if (this.$module.dataset.disableddays) { // lowercase and arrange dayLabels to put indexOf sunday == 0 for comparison // with getDay() function let weekDays = this.dayLabels.map(item => item.toLowerCase()) @@ -312,7 +322,7 @@ Datepicker.prototype.setDisabledDays = function () { weekDays.unshift(weekDays.pop()) } - this.disabledDays = this.$input.dataset.disableddays + this.disabledDays = this.$module.dataset.disableddays .replace(/\s+/, ' ') .toLowerCase() .split(' ') @@ -322,23 +332,23 @@ Datepicker.prototype.setDisabledDays = function () { } Datepicker.prototype.setLeadingZeros = function() { - if (this.$input.dataset.leadingzeros) { - if(this.$input.dataset.leadingzeros.toLowerCase() === 'true') { + if (this.$module.dataset.leadingzeros) { + if(this.$module.dataset.leadingzeros.toLowerCase() === 'true') { this.config.leadingZeros = true; } - if(this.$input.dataset.leadingzeros.toLowerCase() === 'false') { + if(this.$module.dataset.leadingzeros.toLowerCase() === 'false') { this.config.leadingZeros = false; } } } Datepicker.prototype.setWeekStartDay = function() { - const weekStartDayParam = this.$input.dataset.weekstartday; - if(weekStartDayParam.toLowerCase() === 'sunday' ) { + const weekStartDayParam = this.$module.dataset.weekstartday; + if(weekStartDayParam?.toLowerCase() === 'sunday' ) { this.config.weekStartDay = 'sunday' this.dayLabels.unshift(this.dayLabels.pop()) } - if(weekStartDayParam.toLowerCase() === 'monday' ) { + if(weekStartDayParam?.toLowerCase() === 'monday' ) { this.config.weekStartDay = 'monday' } } diff --git a/src/moj/components/date-picker/template.njk b/src/moj/components/date-picker/template.njk index 9524e90d..507f32c8 100644 --- a/src/moj/components/date-picker/template.njk +++ b/src/moj/components/date-picker/template.njk @@ -1,68 +1,50 @@ {% from "govuk/components/input/macro.njk" import govukInput %} -{% from "govuk/components/label/macro.njk" import govukLabel %} -{% from "govuk/components/hint/macro.njk" import govukHint %} -{% from "govuk/components/error-message/macro.njk" import govukErrorMessage %} +{% from "govuk/macros/attributes.njk" import govukAttributes %} -
- {{ govukLabel({ - html: params.label.html, - text: params.label.text, - classes: params.label.classes, - isPageHeading: params.label.isPageHeading, - attributes: params.label.attributes, - for: params.id - }) | indent(2) | trim }} - {% if params.hint %} - {% set hintId = params.id + "-hint" %} - {% set describedBy = describedBy + " " + hintId if describedBy else hintId %} - {{ govukHint({ - id: hintId, - classes: params.hint.classes, - attributes: params.hint.attributes, - html: params.hint.html, - text: params.hint.text - }) | indent(2) | trim }} - {% endif %} - {% if params.errorMessage %} - {% set errorId = params.id + "-error" %} - {% set describedBy = describedBy + " " + errorId if describedBy else errorId %} - {{ govukErrorMessage({ - id: errorId, - classes: params.errorMessage.classes, - attributes: params.errorMessage.attributes, - html: params.errorMessage.html, - text: params.errorMessage.text, - visuallyHiddenText: params.errorMessage.visuallyHiddenText - }) | indent(2) | trim }} - {% endif %} - - {% set fixedWidth = true %} - {% set widthClass = 'govuk-input--width-10' %} +{% set classNames = "moj-js-datepicker-input " %} - {% if params.inputWidthClass == '' %} - {% set fixedWidth = false %} - {% set widthClass = '' %} - {% elif params.inputWidthClass %} - {% set fixedWidth = true %} - {% set widthClass = params.inputWidthClass %} - {% endif %} -
+{%- if params.classes %} + {% set classNames = classNames + " " + params.classes %} +{% endif %} + +{% set attributes = { + "data-module": 'moj-date-picker', + "data-mindate": { + value: params.mindate, + optional: true + }, + "data-maxdate": { + value: params.maxdate, + optional: true + }, + "data-disableddates": { + value: params.disabledDates, + optional: true + }, + "data-disableddays": { + value: params.disabledDays, + optional: true + }, + "data-leadingzeros": { + value: params.leadingZeros, + optional: true + }, + "data-weekstartday": { + value: params.weekStartDay, + optional: true + } +} %} + +
{{ govukInput({ - classes: "govuk-input moj-js-datepicker-input " + widthClass, + classes: classNames, id: params.id, name: params.name, value: params.value, - describedBy: hintId if params.hint, autocomplete: "off", - attributes: { - "data-mindate": params.minDate, - "data-maxdate": params.maxDate, - "data-disableddates": params.disabledDates, - "data-disableddays": params.disabledDays, - "data-leadingzeros": params.leadingZeros, - "data-weekstartday": params.weekstartday - } + formGroup: params.formGroup, + label: params.label, + hint: params.hint, + errorMessage: params.errorMessage }) }} -
From 4ad17455bd65b2a663377b4168ed163eb42c6e24 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Thu, 11 Jul 2024 15:32:22 +0100 Subject: [PATCH 21/58] docs(date picker): remove from-to stacked examples --- docs/components/date-picker.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index d5df1a73..13f8de09 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -48,24 +48,6 @@ ADD SOMETHING HERE === You can disable days of the week (e.g. every Saturday and ADD SOMETHING HERE === Date pickers with lots of disabled dates isn't a good experience for a user. For example, if using a date picker to book an appointment, it may be easier for users to show them a list of available appointments as radio buttons. -## From and to dates - -Allow users to pick to and from dates by stacking 2 date pickers together. - -When stacking 2 date pickers horizontally or vertically, apply padding that is consistent with the rest of your product. - -### Vertically stacked - -Multiple date picker components can be vertically stacked. This is useful when used in vertical filters or forms. - -{% example "/examples/date-picker-vertical-pair", 650 %} - -### Horizontally stacked - -Multiple date picker components can be horizontally displayed. This is useful when used in horizontal filters. - -{% example "/examples/date-picker-horizontal-pair", 590 %} - ## Errors Follow the guidance in the [GOV.UK Design System](https://design-system.service.gov.uk/components/error-message/) for error messages. From bdebcc037c6834cfd54922e7bc3c03dfaa16563e Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Thu, 11 Jul 2024 15:54:03 +0100 Subject: [PATCH 22/58] refactor(date picker): refactor date picker JS Extract some js into functions for slightly improved readability --- src/moj/components/date-picker/date-picker.js | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index c7ed43cb..102ab18e 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -89,17 +89,7 @@ Datepicker.prototype.init = function () { */ Datepicker.prototype.initControls = function () { // Create datepicker popup dialog - const titleId = `datepicker-title-${this.$input.id}` - const dialog = document.createElement('div') - dialog.id = `datepicker-${this.$input.id}` - dialog.setAttribute('class', 'moj-datepicker-dialog datepickerDialog') - dialog.setAttribute('role', 'dialog') - dialog.setAttribute('aria-modal', 'true') - dialog.setAttribute('aria-labelledby', titleId) - dialog.innerHTML = this.createDialogMarkup(titleId) - - - this.dialogElement = dialog + this.dialogElement = this.createDialog() this.createCalendarHeaders() const pickerWrapper = document.createElement('div') @@ -111,47 +101,26 @@ Datepicker.prototype.initControls = function () { pickerWrapper.appendChild(inputWrapper) inputWrapper.appendChild(this.$input) - inputWrapper.insertAdjacentHTML('beforeend', this.createToggleMarkup() ) + inputWrapper.insertAdjacentHTML('beforeend', this.toggleTemplate() ) pickerWrapper.insertAdjacentElement('beforeend', this.dialogElement) this.$calendarButton = this.$module.querySelector('.moj-js-datepicker-toggle') this.dialogTitleNode = this.dialogElement.querySelector('.moj-js-datepicker-month-year') + this.createCalendar() - // create calendar - const tbody = this.dialogElement.querySelector('tbody') - let dayCount = 0 - for (let i = 0; i < 6; i++) { - // create row - const row = tbody.insertRow(i) - - for (let j = 0; j < 7; j++) { - // create cell (day) - const cell = document.createElement('td') - const dateButton = document.createElement('button') - - cell.appendChild(dateButton) - row.appendChild(cell) - - const calendarDay = new DSCalendarDay(dateButton, dayCount, i, j, this) - calendarDay.init() - this.calendarDays.push(calendarDay) - dayCount++ - } - } - - // add event listeners this.prevMonthButton = this.dialogElement.querySelector('.moj-js-datepicker-prev-month') this.prevYearButton = this.dialogElement.querySelector('.moj-js-datepicker-prev-year') this.nextMonthButton = this.dialogElement.querySelector('.moj-js-datepicker-next-month') this.nextYearButton = this.dialogElement.querySelector('.moj-js-datepicker-next-year') + this.cancelButton = this.dialogElement.querySelector('.moj-js-datepicker-cancel') + this.okButton = this.dialogElement.querySelector('.moj-js-datepicker-ok') + + // add event listeners this.prevMonthButton.addEventListener('click', event => this.focusPreviousMonth(event, false)) this.prevYearButton.addEventListener('click', event => this.focusPreviousYear(event, false)) this.nextMonthButton.addEventListener('click', event => this.focusNextMonth(event, false)) this.nextYearButton.addEventListener('click', event => this.focusNextYear(event, false)) - - this.cancelButton = this.dialogElement.querySelector('.moj-js-datepicker-cancel') - this.okButton = this.dialogElement.querySelector('.moj-js-datepicker-ok') this.cancelButton.addEventListener('click', (event) => { event.preventDefault() this.closeDialog(event) @@ -175,7 +144,44 @@ Datepicker.prototype.initControls = function () { this.updateCalendar() } -Datepicker.prototype.createToggleMarkup = function() { +Datepicker.prototype.createDialog = function() { + const titleId = `datepicker-title-${this.$input.id}` + const dialog = document.createElement('div') + + dialog.id = `datepicker-${this.$input.id}` + dialog.setAttribute('class', 'moj-datepicker-dialog datepickerDialog') + dialog.setAttribute('role', 'dialog') + dialog.setAttribute('aria-modal', 'true') + dialog.setAttribute('aria-labelledby', titleId) + dialog.innerHTML = this.dialogTemplate(titleId) + + return dialog +} + +Datepicker.prototype.createCalendar = function() { + const tbody = this.dialogElement.querySelector('tbody') + let dayCount = 0 + for (let i = 0; i < 6; i++) { + // create row + const row = tbody.insertRow(i) + + for (let j = 0; j < 7; j++) { + // create cell (day) + const cell = document.createElement('td') + const dateButton = document.createElement('button') + + cell.appendChild(dateButton) + row.appendChild(cell) + + const calendarDay = new DSCalendarDay(dateButton, dayCount, i, j, this) + calendarDay.init() + this.calendarDays.push(calendarDay) + dayCount++ + } + } +} + +Datepicker.prototype.toggleTemplate = function() { return `` } -Datepicker.prototype.createDialogMarkup = function (titleId) { +Datepicker.prototype.dialogTemplate = function (titleId) { return `
MondayTuesdayWednesdayThursdayFridaySaturdaySunday
${day}${day}
- - + - - - + + - - - + + - - - + + - - - + + - - - + +
Error stateEnglish error messageWelsh error messageError message
If no date is entered or picked from the calendarEnter or pick a dateNodwch neu dewiswch ddyddiadNo date is entered or selected from the calendarEnter or select a date
If the date entered is in the wrong formatEnter the date in the correct format, for example, 17/5/2024Rhowch y dyddiad yn y fformat cywir, er enghraifft, 17/5/2024The date is in the wrong formatEnter the date in the correct format, for example, 17/5/2024
If the date entered does not existThe date you entered must be a real dateRhaid i'r dyddiad a roesoch fod yn ddyddiad go iawnThe date does not existEnter a real date
If the date entered is incompleteEnter a complete date, for example, 17/5/2024Nodwch ddyddiad cyflawn, er enghraifft, 17/5/2024The date is incompleteEnter a full date, for example 17/5/2024
If the date entered is a disabled dateADD SOMETHING HERENodwch ddyddiad cyflawn, er enghraifft, 17/5/2024The date is disabledSelect an available date from the calendar
-### If you are using multiple date pickers +### Using multiple date pickers -Make sure you use error summaries and error messages for each text field. Even if the same errors occur for multiple date pickers. +Each text field needs its own error summary and message. This applies even if the error is the same. ## Examples @@ -103,30 +120,21 @@ Make sure you use error summaries and error messages for each text field. Even i

#

-Date pickers can be used as a way to filter information on a page. - -Use one date picker to show information related to a single date, or use two date pickers to show information within a date range. - -[View example](#) +[View example](#) (opens in a new window). ### Asking a question with a date picker

#

-Date pickers can be used within the conventional one-question-per-page approach for GOV.UK services. +[View example](#) (opens in a new window). -There are a number of ways that dates can be asked for and provided, inc. using a date input field, a date picker, or even as a list of radio buttons. +## Future changes -Test your product or service with users to see which work best for their needs. +In future versions of this documentation, there will be: -[View example](#) - -## Considerations - -Whilst the date picker is fully navigable using a keyboard, date pickers can be slow and difficult to use for keyboard only users. - -Another challenge for users, especially those with poor vision or colour blindness, is seeing the unavailable or disabled dates. +- Guidance examples on using this component for date ranges +- Welsh content for the designs, including error messages ## Contributors -Thanks to **Dom Billington**, **Eddie Shannon**, **David Middleton**, and the **DPS Connect team** for contributing this component. +Thanks to Dom Billington, Eddie Shannon, David Middleton, and the DPS Connect team for contributing this component. This component was based on the date picker in the Scottish Government Design System. diff --git a/package-lock.json b/package-lock.json index c9d8df06..45a82620 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15157,48 +15157,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", "dev": true, @@ -15214,87 +15172,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/npm/node_modules/@isaacs/string-locale-compare": { "version": "1.1.0", "dev": true, @@ -18029,6 +17906,21 @@ "node": ">=8" } }, + "node_modules/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/npm/node_modules/strip-ansi": { "version": "6.0.1", "dev": true, @@ -18041,6 +17933,19 @@ "node": ">=8" } }, + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/npm/node_modules/supports-color": { "version": "9.4.0", "dev": true, @@ -18255,6 +18160,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.0.1", "dev": true, @@ -34885,36 +34808,6 @@ "strip-ansi": "^7.0.1" } }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "bundled": true, - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "bundled": true, - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, "strip-ansi": { "version": "7.1.0", "bundled": true, @@ -34922,61 +34815,6 @@ "requires": { "ansi-regex": "^6.0.1" } - }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "bundled": true, - "dev": true - } - } - }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "bundled": true, - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "bundled": true, - "dev": true - }, - "string-width": { - "version": "4.2.3", - "bundled": true, - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } } } }, @@ -36835,6 +36673,16 @@ "strip-ansi": "^6.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "bundled": true, + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "strip-ansi": { "version": "6.0.1", "bundled": true, @@ -36843,6 +36691,14 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "supports-color": { "version": "9.4.0", "bundled": true, @@ -37030,6 +36886,16 @@ } } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "bundled": true, From 2f2def642099b8dfda012a974817722a2cbddbcb Mon Sep 17 00:00:00 2001 From: Rob McCarthy Date: Wed, 17 Jul 2024 15:07:11 +0100 Subject: [PATCH 25/58] docs(date picker component): changing example image on date picker guidance --- .../images/date-picker-filter-example.svg | 289 +++++++++++++++++- 1 file changed, 280 insertions(+), 9 deletions(-) diff --git a/docs/assets/images/date-picker-filter-example.svg b/docs/assets/images/date-picker-filter-example.svg index aa7882fb..cc833834 100644 --- a/docs/assets/images/date-picker-filter-example.svg +++ b/docs/assets/images/date-picker-filter-example.svg @@ -1,15 +1,286 @@ - - - - + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 35bb9482c997b5ef9641a90f4359a3ea97560ac3 Mon Sep 17 00:00:00 2001 From: Rob McCarthy Date: Wed, 17 Jul 2024 16:54:14 +0100 Subject: [PATCH 26/58] docs(date picker component): updating example image for filtering with the date picker --- .../images/date-picker-filter-example.svg | 289 +----------------- 1 file changed, 9 insertions(+), 280 deletions(-) diff --git a/docs/assets/images/date-picker-filter-example.svg b/docs/assets/images/date-picker-filter-example.svg index cc833834..70642425 100644 --- a/docs/assets/images/date-picker-filter-example.svg +++ b/docs/assets/images/date-picker-filter-example.svg @@ -1,286 +1,15 @@ - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + From 087b03ae4426b51d30d81af22d9e4c84acf99fda Mon Sep 17 00:00:00 2001 From: helennickols <94117270+helennickols@users.noreply.github.com> Date: Mon, 22 Jul 2024 14:27:18 +0100 Subject: [PATCH 27/58] docs(date picker component): update Changes to content --- docs/components/date-picker.md | 2 +- package-lock.json | 15 ++++++++------- package.json | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index 9c2967d5..bf7a85b5 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -37,7 +37,7 @@ Users with poor vision or colour blindness may find it harder to see disabled da Whilst the date picker is fully navigable using a keyboard, date pickers can be slow and difficult to use for keyboard only users. -Another challenge for users, especially those with poor vision or colour blindness, is seeing the unavailable or disabled dates. +Another great challenge for users, especially those with poor vision or colour blindness, is seeing the unavailable or disabled dates. ## Similar or linked components diff --git a/package-lock.json b/package-lock.json index 45a82620..0b1800a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "clipboard": "^2.0.8", "del": "^7.0.0", "esbuild": "^0.23.0", - "govuk-frontend": "^5.4.0", + "govuk-frontend": "^5.4.1", "gulp": "^4.0.2", "gulp-cache": "^1.1.3", "gulp-concat": "^2.6.1", @@ -10947,9 +10947,10 @@ } }, "node_modules/govuk-frontend": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-5.4.0.tgz", - "integrity": "sha512-F3YwQYrYQqIPfNxsoph6O78Ey1unCB6cy6omx8KeWY9G504lWZFBSIaiUCma1jNLw9bOUU7Ui+tXG09jjqy0Mw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-5.4.1.tgz", + "integrity": "sha512-Gmd8LV++TRh9OF6tA+9KQTpwvlsLcri7qRjViz9ji4YuwZvX+c9TD7tyE+dnJcqsQsJfhr9Fp38m3Hu3H7EIcQ==", + "license": "MIT", "engines": { "node": ">= 4.2.0" } @@ -31977,9 +31978,9 @@ } }, "govuk-frontend": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-5.4.0.tgz", - "integrity": "sha512-F3YwQYrYQqIPfNxsoph6O78Ey1unCB6cy6omx8KeWY9G504lWZFBSIaiUCma1jNLw9bOUU7Ui+tXG09jjqy0Mw==" + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-5.4.1.tgz", + "integrity": "sha512-Gmd8LV++TRh9OF6tA+9KQTpwvlsLcri7qRjViz9ji4YuwZvX+c9TD7tyE+dnJcqsQsJfhr9Fp38m3Hu3H7EIcQ==" }, "graceful-fs": { "version": "4.2.10", diff --git a/package.json b/package.json index 820feb99..b4403a10 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "clipboard": "^2.0.8", "del": "^7.0.0", "esbuild": "^0.23.0", - "govuk-frontend": "^5.4.0", + "govuk-frontend": "^5.4.1", "gulp": "^4.0.2", "gulp-cache": "^1.1.3", "gulp-concat": "^2.6.1", From b8ad0e4bffb702773b8918d5bc6c68ba02a214c8 Mon Sep 17 00:00:00 2001 From: helennickols <94117270+helennickols@users.noreply.github.com> Date: Mon, 22 Jul 2024 16:46:55 +0100 Subject: [PATCH 28/58] docs(date picker component): update content Refine content ready for release. --- docs/components/date-picker.md | 57 ++++++++++++++++------------------ 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index bf7a85b5..80e8154a 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -7,47 +7,43 @@ title: Date picker {% example "/examples/date-picker", 590 %} -## About the date picker +## Overview -When users first open the calendar view it'll show today's date. This is the calendar view. +When users first open the calendar it'll show today's date in the calendar view. Users do not have to use this - they can also enter a date directly into the text field. -Users do not have to use the calendar view. They can also enter a date directly into the text field. +### When to use -## When to use +Users might want to use the calendar view: -The date picker enables users to select a date from a calendar. This may help them to select: +- for a relative date or one they need to look up, for example last Thursday or next Wednesday +- to enter today's date more quickly +- for available dates only -- a relative date or one they need to look up, for example last Thursday or next Wednesday -- today's date, more quickly -- available dates only - -## When not to use +### When not to use Do not use the date picker: - for a memorable date, for example a user's date of birth - for a date that users know or can easily look up, for example an appointment on a letter -- when only an approximate date is needed, for example month and year - -Instead use the [date input component](https://design-system.service.gov.uk/components/date-input/) from the GOV.UK Design System. +- when only a rough date is needed, for example the month and year -## Considerations +Use the [GOV.UK Design System's date input component](https://design-system.service.gov.uk/components/date-input/) instead. -Users with poor vision or colour blindness may find it harder to see disabled dates. This does not affect the performance of the component. +### Things to consider -Whilst the date picker is fully navigable using a keyboard, date pickers can be slow and difficult to use for keyboard only users. +Date pickers are fully navigable using a keyboard, but can be slow and difficult to use for keyboard-only users. -Another great challenge for users, especially those with poor vision or colour blindness, is seeing the unavailable or disabled dates. +### Similar or linked components -## Similar or linked components +There's also the [GOV.UK Design System's date input component](https://design-system.service.gov.uk/components/date-input/) and [pattern for asking users for dates](https://design-system.service.gov.uk/patterns/dates/). -The GOV.UK Design System has a [date input component](https://design-system.service.gov.uk/components/date-input/) and a [pattern for asking users for dates](https://design-system.service.gov.uk/patterns/dates/). +## How to use -## Hint text +### Hint text -The date picker hint text is set to 17/5/2024. It can be changed if another date would help users, for example the date of the start of a scheme. Add a full-stop at the end. +The date picker hint text is set to 17/5/2024. This can be changed to a more helpful date, for example the start of a scheme. Add a full-stop at the end. -## Disabled dates +### Excluding dates -You can disable specific dates and days of the week, for example bank holidays or every weekend. +You can exclude (or disable) specific dates and days of the week from the date picker, for example bank holidays or every weekend. {% example "/examples/date-picker-disabled-dates", 590 %} {% example "/examples/date-picker-disabled-days", 590 %} -Disabled states: +Excluded dates: - are not focusable - are not read by screen readers - do not need the right visual contrast +- may be harder to view for users with poor vision or colour blindness -This means they are not accessible should they be needed for anything. +They are not accessible should they be needed for something. This does not affect the performance of the component. -If there are a lot of unavailable dates, the date picker may not provide a good user experience. This is because users will have to navigate through a lot of months to find a date, for example for an appointment. If there are only a few available dates, consider showing them in a list with radio buttons. +If there are a lot of unavailable dates, users will have to navigate through a a lot of months to find a date. If there are only a few available dates (for example for an appointment) consider showing them in a list with radio buttons. -## Error messages +### Error messages -Follow the guidance in the [GOV.UK Design System](https://design-system.service.gov.uk/components/error-message/) for error messages. +Follow the [GOV.UK Design System guidance on error messages](https://design-system.service.gov.uk/components/error-message/). {% example "/examples/date-picker-error", 590 %} @@ -112,7 +109,7 @@ Follow the guidance in the [GOV.UK Design System](https://design-system.service. ### Using multiple date pickers -Each text field needs its own error summary and message. This applies even if the error is the same. +Each text field needs its own error summary and message, even if the error is the same. ## Examples @@ -132,7 +129,7 @@ Each text field needs its own error summary and message. This applies even if th In future versions of this documentation, there will be: -- Guidance examples on using this component for date ranges +- guidance on using date ranges with this component - Welsh content for the designs, including error messages ## Contributors From 73f707188ab22bac8af5a69a73bf2314a620eab4 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 24 Jul 2024 14:44:56 +0100 Subject: [PATCH 29/58] feat(date picker): code formatting and updates following accessibility review * add aria-expanded attribute onto calendar toggle button * add "excluded date" assisteive text to excluded dates * update excluded dates example to show both individual dates and days * reformat code to follow convention of element variables having a $ prefix --- .../date-picker-disabled-dates/index.njk | 6 +- src/moj/components/date-picker/date-picker.js | 177 +++++++++--------- src/moj/components/date-picker/template.njk | 8 +- 3 files changed, 99 insertions(+), 92 deletions(-) diff --git a/docs/examples/date-picker-disabled-dates/index.njk b/docs/examples/date-picker-disabled-dates/index.njk index 5c25bcb2..256bd6a9 100644 --- a/docs/examples/date-picker-disabled-dates/index.njk +++ b/docs/examples/date-picker-disabled-dates/index.njk @@ -5,8 +5,6 @@ title: Date Picker Disabled Dates (example) {%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} -{% set disabledDates %}{% dateInCurrentMonth 05 %} {% dateInCurrentMonth 12 %} {% dateInCurrentMonth 18 %}-{% dateInCurrentMonth 25 %} {% endset %} - {{ mojDatePicker({ id: "date", name: "date", @@ -16,5 +14,7 @@ title: Date Picker Disabled Dates (example) hint: { text: "For example, 17/5/2024." }, - disabledDates: disabledDates + value: "10/04/2025", + excludedDates: "02/04/2025 18/04/2025 21/04/2025", + excludedDays: "saturday sunday" }) }} diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 102ab18e..ee3a695b 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -51,8 +51,8 @@ function Datepicker($module, config) { this.currentDate = new Date() this.currentDate.setHours(0, 0, 0, 0) this.calendarDays = [] - this.disabledDates = [] - this.disabledDays = [] + this.excludedDates = [] + this.excludedDays = [] this.keycodes = { tab: 9, @@ -88,53 +88,55 @@ Datepicker.prototype.init = function () { * Initialise controls and set attributes */ Datepicker.prototype.initControls = function () { + this.id = `datepicker-${this.$input.id}` + // Create datepicker popup dialog - this.dialogElement = this.createDialog() + this.$dialog = this.createDialog() this.createCalendarHeaders() - const pickerWrapper = document.createElement('div') - const inputWrapper = document.createElement('div') - pickerWrapper.classList.add('moj-datepicker__wrapper') - inputWrapper.classList.add('govuk-input__wrapper') + const $componentWrapper = document.createElement('div') + const $inputWrapper = document.createElement('div') + $componentWrapper.classList.add('moj-datepicker__wrapper') + $inputWrapper.classList.add('govuk-input__wrapper') - this.$input.parentNode.insertBefore(pickerWrapper, this.$input) - pickerWrapper.appendChild(inputWrapper) - inputWrapper.appendChild(this.$input) + this.$input.parentNode.insertBefore($componentWrapper, this.$input) + $componentWrapper.appendChild($inputWrapper) + $inputWrapper.appendChild(this.$input) - inputWrapper.insertAdjacentHTML('beforeend', this.toggleTemplate() ) - pickerWrapper.insertAdjacentElement('beforeend', this.dialogElement) + $inputWrapper.insertAdjacentHTML('beforeend', this.toggleTemplate() ) + $componentWrapper.insertAdjacentElement('beforeend', this.$dialog) this.$calendarButton = this.$module.querySelector('.moj-js-datepicker-toggle') - this.dialogTitleNode = this.dialogElement.querySelector('.moj-js-datepicker-month-year') + this.$dialogTitle = this.$dialog.querySelector('.moj-js-datepicker-month-year') this.createCalendar() - this.prevMonthButton = this.dialogElement.querySelector('.moj-js-datepicker-prev-month') - this.prevYearButton = this.dialogElement.querySelector('.moj-js-datepicker-prev-year') - this.nextMonthButton = this.dialogElement.querySelector('.moj-js-datepicker-next-month') - this.nextYearButton = this.dialogElement.querySelector('.moj-js-datepicker-next-year') - this.cancelButton = this.dialogElement.querySelector('.moj-js-datepicker-cancel') - this.okButton = this.dialogElement.querySelector('.moj-js-datepicker-ok') + this.$prevMonthButton = this.$dialog.querySelector('.moj-js-datepicker-prev-month') + this.$prevYearButton = this.$dialog.querySelector('.moj-js-datepicker-prev-year') + this.$nextMonthButton = this.$dialog.querySelector('.moj-js-datepicker-next-month') + this.$nextYearButton = this.$dialog.querySelector('.moj-js-datepicker-next-year') + this.$cancelButton = this.$dialog.querySelector('.moj-js-datepicker-cancel') + this.$okButton = this.$dialog.querySelector('.moj-js-datepicker-ok') // add event listeners - this.prevMonthButton.addEventListener('click', event => this.focusPreviousMonth(event, false)) - this.prevYearButton.addEventListener('click', event => this.focusPreviousYear(event, false)) - this.nextMonthButton.addEventListener('click', event => this.focusNextMonth(event, false)) - this.nextYearButton.addEventListener('click', event => this.focusNextYear(event, false)) - this.cancelButton.addEventListener('click', (event) => { + this.$prevMonthButton.addEventListener('click', event => this.focusPreviousMonth(event, false)) + this.$prevYearButton.addEventListener('click', event => this.focusPreviousYear(event, false)) + this.$nextMonthButton.addEventListener('click', event => this.focusNextMonth(event, false)) + this.$nextYearButton.addEventListener('click', event => this.focusNextYear(event, false)) + this.$cancelButton.addEventListener('click', (event) => { event.preventDefault() this.closeDialog(event) }) - this.okButton.addEventListener('click', () => { + this.$okButton.addEventListener('click', () => { this.selectDate(this.currentDate) }) - const dialogButtons = this.dialogElement.querySelectorAll('button:not([disabled="true"])') + const dialogButtons = this.$dialog.querySelectorAll('button:not([disabled="true"])') // eslint-disable-next-line prefer-destructuring - this.firstButtonInDialog = dialogButtons[0] - this.lastButtonInDialog = dialogButtons[dialogButtons.length - 1] - this.firstButtonInDialog.addEventListener('keydown', event => this.firstButtonKeydown(event)) - this.lastButtonInDialog.addEventListener('keydown', event => this.lastButtonKeydown(event)) + this.$firstButtonInDialog = dialogButtons[0] + this.$lastButtonInDialog = dialogButtons[dialogButtons.length - 1] + this.$firstButtonInDialog.addEventListener('keydown', event => this.firstButtonKeydown(event)) + this.$lastButtonInDialog.addEventListener('keydown', event => this.lastButtonKeydown(event)) this.$calendarButton.addEventListener('click', event => this.toggleDialog(event)) @@ -144,36 +146,36 @@ Datepicker.prototype.initControls = function () { this.updateCalendar() } -Datepicker.prototype.createDialog = function() { +Datepicker.prototype.createDialog = function () { const titleId = `datepicker-title-${this.$input.id}` - const dialog = document.createElement('div') + const $dialog = document.createElement('div') - dialog.id = `datepicker-${this.$input.id}` - dialog.setAttribute('class', 'moj-datepicker-dialog datepickerDialog') - dialog.setAttribute('role', 'dialog') - dialog.setAttribute('aria-modal', 'true') - dialog.setAttribute('aria-labelledby', titleId) - dialog.innerHTML = this.dialogTemplate(titleId) + $dialog.id = this.id + $dialog.setAttribute('class', 'moj-datepicker-dialog datepickerDialog') + $dialog.setAttribute('role', 'dialog') + $dialog.setAttribute('aria-modal', 'true') + $dialog.setAttribute('aria-labelledby', titleId) + $dialog.innerHTML = this.dialogTemplate(titleId) - return dialog + return $dialog } -Datepicker.prototype.createCalendar = function() { - const tbody = this.dialogElement.querySelector('tbody') +Datepicker.prototype.createCalendar = function () { + const $tbody = this.$dialog.querySelector('tbody') let dayCount = 0 for (let i = 0; i < 6; i++) { // create row - const row = tbody.insertRow(i) + const $row = $tbody.insertRow(i) for (let j = 0; j < 7; j++) { // create cell (day) - const cell = document.createElement('td') - const dateButton = document.createElement('button') + const $cell = document.createElement('td') + const $dateButton = document.createElement('button') - cell.appendChild(dateButton) - row.appendChild(cell) + $cell.appendChild($dateButton) + $row.appendChild($cell) - const calendarDay = new DSCalendarDay(dateButton, dayCount, i, j, this) + const calendarDay = new DSCalendarDay($dateButton, dayCount, i, j, this) calendarDay.init() this.calendarDays.push(calendarDay) dayCount++ @@ -181,8 +183,8 @@ Datepicker.prototype.createCalendar = function() { } } -Datepicker.prototype.toggleTemplate = function() { - return `Choose date ${day}` - const headerRow = this.dialogElement.querySelector('thead > tr') + const headerRow = this.$dialog.querySelector('thead > tr') headerRow.insertAdjacentHTML('beforeend', html) }) } @@ -270,8 +272,8 @@ Datepicker.prototype.leadingZeros = function (value, length = 2) { Datepicker.prototype.setOptions = function() { this.setMinAndMaxDatesOnCalendar() - this.setDisabledDates() - this.setDisabledDays() + this.setExcludedDates() + this.setExcludedDays() this.setLeadingZeros() this.setWeekStartDay() } @@ -292,13 +294,14 @@ Datepicker.prototype.setMinAndMaxDatesOnCalendar = function () { } } -Datepicker.prototype.setDisabledDates = function() { - if(this.$module.dataset.disableddates) { - this.disabledDates = this.$module.dataset.disableddates +Datepicker.prototype.setExcludedDates = function() { + if(this.$module.dataset.excludeddates) { + this.excludedDates = this.$module.dataset.excludeddates .replace(/\s+/, ' ') .split(' ') .map((item) => { if (item.includes('-')) { + // parse the date range from the format "dd/mm/yyyy-dd/mm/yyyy" const [startDate, endDate] = item.split('-').map(d => this.formattedDateFromString(d, null)) if (startDate && endDate) { const date = new Date(startDate.getTime()); @@ -319,8 +322,8 @@ Datepicker.prototype.setDisabledDates = function() { } -Datepicker.prototype.setDisabledDays = function () { - if (this.$module.dataset.disableddays) { +Datepicker.prototype.setExcludedDays = function () { + if (this.$module.dataset.excludeddays) { // lowercase and arrange dayLabels to put indexOf sunday == 0 for comparison // with getDay() function let weekDays = this.dayLabels.map(item => item.toLowerCase()) @@ -328,7 +331,7 @@ Datepicker.prototype.setDisabledDays = function () { weekDays.unshift(weekDays.pop()) } - this.disabledDays = this.$module.dataset.disableddays + this.excludedDays = this.$module.dataset.excludeddays .replace(/\s+/, ' ') .toLowerCase() .split(' ') @@ -359,8 +362,7 @@ Datepicker.prototype.setWeekStartDay = function() { } } -Datepicker.prototype.isDisabledDate = function (date) { - +Datepicker.prototype.isExcludedDate = function (date) { if (this.minDate && this.minDate > date) { return true } @@ -369,13 +371,13 @@ Datepicker.prototype.isDisabledDate = function (date) { return true } - for (const disabledDate of this.disabledDates) { - if (date.toDateString() === disabledDate.toDateString()) { + for (const excludedDate of this.excludedDates) { + if (date.toDateString() === excludedDate.toDateString()) { return true } } - if (this.disabledDays.includes(date.getDay())) { + if (this.excludedDays.includes(date.getDay())) { return true } @@ -412,7 +414,7 @@ Datepicker.prototype.formattedDateFromDate = function (date) { Datepicker.prototype.backgroundClick = function (event) { if ( this.isOpen() && - !this.dialogElement.contains(event.target) && + !this.$dialog.contains(event.target) && !this.$input.contains(event.target) && !this.$calendarButton.contains(event.target) ) { @@ -427,21 +429,21 @@ Datepicker.prototype.formattedDateHuman = function (date) { Datepicker.prototype.firstButtonKeydown = function (event) { if (event.keyCode === this.keycodes.tab && event.shiftKey) { - this.lastButtonInDialog.focus() + this.$lastButtonInDialog.focus() event.preventDefault() } } Datepicker.prototype.lastButtonKeydown = function (event) { if (event.keyCode === this.keycodes.tab && !event.shiftKey) { - this.firstButtonInDialog.focus() + this.$firstButtonInDialog.focus() event.preventDefault() } } // render calendar Datepicker.prototype.updateCalendar = function () { - this.dialogTitleNode.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}` + this.$dialogTitle.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}` const day = this.currentDate @@ -461,7 +463,7 @@ Datepicker.prototype.updateCalendar = function () { // loop through our days for (let i = 0; i < this.calendarDays.length; i++) { const hidden = thisDay.getMonth() !== day.getMonth() - const disabled = this.isDisabledDate(thisDay) + const disabled = this.isExcludedDate(thisDay) this.calendarDays[i].update(thisDay, hidden, disabled) @@ -519,7 +521,7 @@ Datepicker.prototype.setCurrentDate = function (focus = true) { } Datepicker.prototype.selectDate = function (date) { - if (this.isDisabledDate(date)) { + if (this.isExcludedDate(date)) { return } @@ -535,7 +537,7 @@ Datepicker.prototype.selectDate = function (date) { } Datepicker.prototype.isOpen = function () { - return this.dialogElement.classList.contains('moj-datepicker-dialog--open') + return this.$dialog.classList.contains('moj-datepicker-dialog--open') } Datepicker.prototype.toggleDialog = function (event) { @@ -550,15 +552,16 @@ Datepicker.prototype.toggleDialog = function (event) { Datepicker.prototype.openDialog = function () { // display the dialog - this.dialogElement.style.display = 'block' - this.dialogElement.classList.add('moj-datepicker-dialog--open') + this.$dialog.style.display = 'block' + this.$dialog.classList.add('moj-datepicker-dialog--open') + this.$calendarButton.setAttribute('aria-expanded', 'true') // position the dialog // if input is wider than dialog pin it to the right - if(this.$input.offsetWidth > this.dialogElement.offsetWidth) { - this.dialogElement.style.right = `0px` + if(this.$input.offsetWidth > this.$dialog.offsetWidth) { + this.$dialog.style.right = `0px` } - this.dialogElement.style.top = `${this.$input.offsetHeight + 3}px` + this.$dialog.style.top = `${this.$input.offsetHeight + 3}px` // get the date from the input element if (this.$input.value.match(/^(\d{1,2})([-/,. ])(\d{1,2})[-/,. ](\d{4})$/)) { @@ -571,8 +574,9 @@ Datepicker.prototype.openDialog = function () { } Datepicker.prototype.closeDialog = function () { - this.dialogElement.style.display = 'none' - this.dialogElement.classList.remove('moj-datepicker-dialog--open') + this.$dialog.style.display = 'none' + this.$dialog.classList.remove('moj-datepicker-dialog--open') + this.$calendarButton.setAttribute('aria-expanded', 'false') this.$calendarButton.focus() } @@ -680,17 +684,17 @@ DSCalendarDay.prototype.init = function () { } DSCalendarDay.prototype.update = function (day, hidden, disabled) { - const dateOptions = { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - } - this.button.innerHTML = `${day.toLocaleDateString('en-GB', dateOptions )}` - this.date = new Date(day) + let label = day.getDate() + let accessibleLabel = day.toLocaleDateString('en-GB', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + } ) if (disabled) { this.button.setAttribute('aria-disabled', true) + accessibleLabel = 'Excluded date, ' + accessibleLabel } else { this.button.removeAttribute('aria-disabled') } @@ -700,6 +704,9 @@ DSCalendarDay.prototype.update = function (day, hidden, disabled) { } else { this.button.style.display = 'block' } + + this.button.innerHTML = `${accessibleLabel}` + this.date = new Date(day) } DSCalendarDay.prototype.click = function (event) { diff --git a/src/moj/components/date-picker/template.njk b/src/moj/components/date-picker/template.njk index 1eacea2c..405841fd 100644 --- a/src/moj/components/date-picker/template.njk +++ b/src/moj/components/date-picker/template.njk @@ -17,12 +17,12 @@ value: params.maxDate, optional: true }, - "data-disableddates": { - value: params.disabledDates, + "data-excludeddates": { + value: params.excludedDates, optional: true }, - "data-disableddays": { - value: params.disabledDays, + "data-excludeddays": { + value: params.excludedDays, optional: true }, "data-leadingzeros": { From 59dad3a7ebad312a9260d6b97664c4115f66a956 Mon Sep 17 00:00:00 2001 From: helennickols <94117270+helennickols@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:55:43 +0100 Subject: [PATCH 30/58] docs(date picker component): update content --- .../partials/suggest-a-change-and-help.njk | 24 ++++++------ docs/community/suggest-a-change.md | 3 +- docs/components/date-picker.md | 37 +++++++------------ docs/examples/date-picker-error/index.njk | 2 +- 4 files changed, 29 insertions(+), 37 deletions(-) diff --git a/docs/_includes/layouts/partials/suggest-a-change-and-help.njk b/docs/_includes/layouts/partials/suggest-a-change-and-help.njk index c6f26d1e..91a42fb8 100644 --- a/docs/_includes/layouts/partials/suggest-a-change-and-help.njk +++ b/docs/_includes/layouts/partials/suggest-a-change-and-help.njk @@ -3,25 +3,25 @@
-
+
-

- Suggest a change -

-

- To help improve the MoJ Design System, you can suggest changes. -

+

+ Suggest a change +

- Tell us about the change you're proposing by using the suggest a change form. The MoJ Design System Group will be notified of your suggestion and will review it. + You can suggest a change to improve the MoJ Design System. +

+

+ The MoJ Design System team will review it.


-

- Need help? -

+

+ Get help +

- The MoJ Design System Group provides support for users of the MoJ Design System. Contact us to ask for help. + Contact the MoJ Design System team for support.

diff --git a/docs/community/suggest-a-change.md b/docs/community/suggest-a-change.md index 8a5b136d..bdffcb96 100644 --- a/docs/community/suggest-a-change.md +++ b/docs/community/suggest-a-change.md @@ -1,8 +1,9 @@ --- layout: layouts/community.njk -title: Suggest a change --- +# Suggest a change + To help improve the MoJ Design System, you can suggest changes to components and patterns. Useful suggestions may look like: diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index 80e8154a..5eabd366 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -9,7 +9,7 @@ title: Date picker ## Overview -When users first open the calendar it'll show today's date in the calendar view. Users do not have to use this - they can also enter a date directly into the text field. +When users first open the date picker's calendar it'll show today's date. Users do not have to use the calendar view to select a date - they can also enter one directly into the text field. ### When to use @@ -17,7 +17,7 @@ Users might want to use the calendar view: - for a relative date or one they need to look up, for example last Thursday or next Wednesday - to enter today's date more quickly -- for available dates only +- for available dates only, for example for prison visits ### When not to use @@ -25,23 +25,23 @@ Do not use the date picker: - for a memorable date, for example a user's date of birth - for a date that users know or can easily look up, for example an appointment on a letter -- when only a rough date is needed, for example the month and year +- when only a rough date is needed, for example a month and year Use the [GOV.UK Design System's date input component](https://design-system.service.gov.uk/components/date-input/) instead. ### Things to consider -Date pickers are fully navigable using a keyboard, but can be slow and difficult to use for keyboard-only users. +Date pickers are fully navigable using a keyboard, but can be slow for keyboard-only and screen reader users. ### Similar or linked components -There's also the [GOV.UK Design System's date input component](https://design-system.service.gov.uk/components/date-input/) and [pattern for asking users for dates](https://design-system.service.gov.uk/patterns/dates/). +There's also the ['Ask users for dates' pattern in the GOV.UK Design System](https://design-system.service.gov.uk/patterns/dates/). ## How to use ### Hint text -The date picker hint text is set to 17/5/2024. This can be changed to a more helpful date, for example the start of a scheme. Add a full-stop at the end. +The date picker hint text is set to 17/5/2024. This can be changed to a more helpful date, for example the start of a scheme. Add a full stop at the end. ### Excluding dates @@ -57,16 +57,9 @@ You can exclude (or disable) specific dates and days of the week from the date p {% example "/examples/date-picker-disabled-days", 590 %} -Excluded dates: +Excluded dates have the right visual contrast but may be harder to view for users with low vision or colour blindness. -- are not focusable -- are not read by screen readers -- do not need the right visual contrast -- may be harder to view for users with poor vision or colour blindness - -They are not accessible should they be needed for something. This does not affect the performance of the component. - -If there are a lot of unavailable dates, users will have to navigate through a a lot of months to find a date. If there are only a few available dates (for example for an appointment) consider showing them in a list with radio buttons. +If there are a lot of unavailable dates, users will have to navigate through a lot of months to find a date. If there are only a few available dates (for example for an appointment) consider showing them in a list with radio buttons. ### Error messages @@ -109,29 +102,27 @@ Follow the [GOV.UK Design System guidance on error messages](https://design-syst ### Using multiple date pickers -Each text field needs its own error summary and message, even if the error is the same. +If you're using more than one date picker, give each text field its own error summary and message (even if the error is the same). ## Examples ### Filtering information with a date picker -

#

- -[View example](#) (opens in a new window). +

A screenshot with the title 'Attended appointments'. In a grey box is the title Filter, underneath is the title Date and then a text input field. The calendar icon and a green 'Apply filter' button is on the right. Below this element is the text '7 appointments'. Details of these appointments are shown.

### Asking a question with a date picker

#

-[View example](#) (opens in a new window). - ## Future changes In future versions of this documentation, there will be: - guidance on using date ranges with this component -- Welsh content for the designs, including error messages +- Welsh language content for the designs, including error messages ## Contributors -Thanks to Dom Billington, Eddie Shannon, David Middleton, and the DPS Connect team for contributing this component. This component was based on the date picker in the Scottish Government Design System. +Thanks to Dom Billington, Eddie Shannon, David Middleton, and the DPS Connect team for contributing this component. + +This component was based on the [Scottish Government Design System date picker](https://designsystem.gov.scot/components/date-picker). diff --git a/docs/examples/date-picker-error/index.njk b/docs/examples/date-picker-error/index.njk index cc368b6b..8b959232 100644 --- a/docs/examples/date-picker-error/index.njk +++ b/docs/examples/date-picker-error/index.njk @@ -16,6 +16,6 @@ arguments: date-picker text: "For example, 17/5/2024." }, errorMessage: { - text: 'Enter or pick a date' + text: 'Enter or select a date' } }) }} From 4516b63aea79a3fb7455661d422289c9d69e6328 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Thu, 25 Jul 2024 14:02:16 +0100 Subject: [PATCH 31/58] feat(date picker): allow component to be configured via JS as well as via data-attributes This change updates the component to allow for component properties such as minDate, maxDate and excludedDates/Days to be passed in via the JS config object. This follows the GDS convention where component defaults are overridden by the JS config, which is overridden by data attributes. The code to do this is largely borrowed from GOV.UK frontend. Also added in this commit are JSDoc comments for all functions with arguments. --- src/moj/components/date-picker/date-picker.js | 1064 ++++++++++------- src/moj/components/date-picker/template.njk | 12 +- 2 files changed, 630 insertions(+), 446 deletions(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index ee3a695b..452e2b4a 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -2,57 +2,77 @@ * Datepicker config * * @typedef {object} DatepickerConfig - * - * @property {string} [imagePath] - The path to image assets. - * @property {string} [id] - . - * @property {string} [name] - . - * @property {string} [label] - . - * @property {string} [hint] - . - * @property {string} [minDate] - . - * @property {string} [maxDate] - . - * @property {Boolean} [leadingZeroes] - Whether to add leading zeroes when populating the field + * @property {string} [excludedDates] - Dates that cannot be selected + * @property {string} [excludedDays] - Days that cannot be selected + * @property {boolean} [leadingZeroes] - Whether to add leading zeroes when populating the field + * @property {string} [minDate] - The earliest available date + * @property {string} [maxDate] - The latest available date + * @property {string} [weekStartDay] - First day of the week in calendar view */ /** - * Datepicker component - * * @param {HTMLElement} $module - HTML element - * @param {DatepickerConfig} config - Datepicker config + * @param {DatepickerConfig} config - config object * @constructor */ function Datepicker($module, config) { if (!$module) { - return this + return this; } - const defaultConfig = { - imagePath: '/assets/images/', - leadingZeros: false, - weekStartDay: 'monday' - } - this.config = { ...defaultConfig, ...config } - this.dayLabels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + const schema = Object.freeze({ + properties: { + excludedDates: { type: "string" }, + excludedDays: { type: "string" }, + leadingZeros: { type: "string" }, + maxDate: { type: "string" }, + minDate: { type: "string" }, + weekStartDay: { type: "string" }, + }, + }); + + const defaults = { + leadingZeros: false, + weekStartDay: "monday", + }; + + // data attributes override JS config, which overrides defaults + this.config = this.mergeConfigs( + defaults, + config, + this.parseDataset(schema, $module.dataset), + ); + + this.dayLabels = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ]; this.monthLabels = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', - ] - - this.currentDate = new Date() - this.currentDate.setHours(0, 0, 0, 0) - this.calendarDays = [] - this.excludedDates = [] - this.excludedDays = [] + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + + this.currentDate = new Date(); + this.currentDate.setHours(0, 0, 0, 0); + this.calendarDays = []; + this.excludedDates = []; + this.excludedDays = []; this.keycodes = { tab: 9, @@ -65,123 +85,148 @@ function Datepicker($module, config) { up: 38, right: 39, down: 40, - } + }; - this.$module = $module - this.$input = $module.querySelector('.moj-js-datepicker-input') + this.$module = $module; + this.$input = $module.querySelector(".moj-js-datepicker-input"); } -/** - * Initialise Datepicker - */ + Datepicker.prototype.init = function () { // Check that required elements are present if (!this.$input) { - return + return; } - this.setOptions() - this.initControls() -} + this.setOptions(); + this.initControls(); +}; -/** - * Initialise controls and set attributes - */ Datepicker.prototype.initControls = function () { - this.id = `datepicker-${this.$input.id}` + this.id = `datepicker-${this.$input.id}`; // Create datepicker popup dialog - this.$dialog = this.createDialog() - this.createCalendarHeaders() - - const $componentWrapper = document.createElement('div') - const $inputWrapper = document.createElement('div') - $componentWrapper.classList.add('moj-datepicker__wrapper') - $inputWrapper.classList.add('govuk-input__wrapper') - - this.$input.parentNode.insertBefore($componentWrapper, this.$input) - $componentWrapper.appendChild($inputWrapper) - $inputWrapper.appendChild(this.$input) - - $inputWrapper.insertAdjacentHTML('beforeend', this.toggleTemplate() ) - $componentWrapper.insertAdjacentElement('beforeend', this.$dialog) - - this.$calendarButton = this.$module.querySelector('.moj-js-datepicker-toggle') - this.$dialogTitle = this.$dialog.querySelector('.moj-js-datepicker-month-year') - - this.createCalendar() - - this.$prevMonthButton = this.$dialog.querySelector('.moj-js-datepicker-prev-month') - this.$prevYearButton = this.$dialog.querySelector('.moj-js-datepicker-prev-year') - this.$nextMonthButton = this.$dialog.querySelector('.moj-js-datepicker-next-month') - this.$nextYearButton = this.$dialog.querySelector('.moj-js-datepicker-next-year') - this.$cancelButton = this.$dialog.querySelector('.moj-js-datepicker-cancel') - this.$okButton = this.$dialog.querySelector('.moj-js-datepicker-ok') + this.$dialog = this.createDialog(); + this.createCalendarHeaders(); + + const $componentWrapper = document.createElement("div"); + const $inputWrapper = document.createElement("div"); + $componentWrapper.classList.add("moj-datepicker__wrapper"); + $inputWrapper.classList.add("govuk-input__wrapper"); + + this.$input.parentNode.insertBefore($componentWrapper, this.$input); + $componentWrapper.appendChild($inputWrapper); + $inputWrapper.appendChild(this.$input); + + $inputWrapper.insertAdjacentHTML("beforeend", this.toggleTemplate()); + $componentWrapper.insertAdjacentElement("beforeend", this.$dialog); + + this.$calendarButton = this.$module.querySelector( + ".moj-js-datepicker-toggle", + ); + this.$dialogTitle = this.$dialog.querySelector( + ".moj-js-datepicker-month-year", + ); + + this.createCalendar(); + + this.$prevMonthButton = this.$dialog.querySelector( + ".moj-js-datepicker-prev-month", + ); + this.$prevYearButton = this.$dialog.querySelector( + ".moj-js-datepicker-prev-year", + ); + this.$nextMonthButton = this.$dialog.querySelector( + ".moj-js-datepicker-next-month", + ); + this.$nextYearButton = this.$dialog.querySelector( + ".moj-js-datepicker-next-year", + ); + this.$cancelButton = this.$dialog.querySelector(".moj-js-datepicker-cancel"); + this.$okButton = this.$dialog.querySelector(".moj-js-datepicker-ok"); // add event listeners - this.$prevMonthButton.addEventListener('click', event => this.focusPreviousMonth(event, false)) - this.$prevYearButton.addEventListener('click', event => this.focusPreviousYear(event, false)) - this.$nextMonthButton.addEventListener('click', event => this.focusNextMonth(event, false)) - this.$nextYearButton.addEventListener('click', event => this.focusNextYear(event, false)) - this.$cancelButton.addEventListener('click', (event) => { - event.preventDefault() - this.closeDialog(event) - }) - this.$okButton.addEventListener('click', () => { - this.selectDate(this.currentDate) - }) - - const dialogButtons = this.$dialog.querySelectorAll('button:not([disabled="true"])') + this.$prevMonthButton.addEventListener("click", (event) => + this.focusPreviousMonth(event, false), + ); + this.$prevYearButton.addEventListener("click", (event) => + this.focusPreviousYear(event, false), + ); + this.$nextMonthButton.addEventListener("click", (event) => + this.focusNextMonth(event, false), + ); + this.$nextYearButton.addEventListener("click", (event) => + this.focusNextYear(event, false), + ); + this.$cancelButton.addEventListener("click", (event) => { + event.preventDefault(); + this.closeDialog(event); + }); + this.$okButton.addEventListener("click", () => { + this.selectDate(this.currentDate); + }); + + const dialogButtons = this.$dialog.querySelectorAll( + 'button:not([disabled="true"])', + ); // eslint-disable-next-line prefer-destructuring - this.$firstButtonInDialog = dialogButtons[0] - this.$lastButtonInDialog = dialogButtons[dialogButtons.length - 1] - this.$firstButtonInDialog.addEventListener('keydown', event => this.firstButtonKeydown(event)) - this.$lastButtonInDialog.addEventListener('keydown', event => this.lastButtonKeydown(event)) - - this.$calendarButton.addEventListener('click', event => this.toggleDialog(event)) - - document.body.addEventListener('mouseup', event => this.backgroundClick(event)) + this.$firstButtonInDialog = dialogButtons[0]; + this.$lastButtonInDialog = dialogButtons[dialogButtons.length - 1]; + this.$firstButtonInDialog.addEventListener("keydown", (event) => + this.firstButtonKeydown(event), + ); + this.$lastButtonInDialog.addEventListener("keydown", (event) => + this.lastButtonKeydown(event), + ); + + this.$calendarButton.addEventListener("click", (event) => + this.toggleDialog(event), + ); + + document.body.addEventListener("mouseup", (event) => + this.backgroundClick(event), + ); // populates calendar with initial dates, avoids Wave errors about null buttons - this.updateCalendar() -} + this.updateCalendar(); +}; Datepicker.prototype.createDialog = function () { - const titleId = `datepicker-title-${this.$input.id}` - const $dialog = document.createElement('div') + const titleId = `datepicker-title-${this.$input.id}`; + const $dialog = document.createElement("div"); - $dialog.id = this.id - $dialog.setAttribute('class', 'moj-datepicker-dialog datepickerDialog') - $dialog.setAttribute('role', 'dialog') - $dialog.setAttribute('aria-modal', 'true') - $dialog.setAttribute('aria-labelledby', titleId) - $dialog.innerHTML = this.dialogTemplate(titleId) + $dialog.id = this.id; + $dialog.setAttribute("class", "moj-datepicker-dialog datepickerDialog"); + $dialog.setAttribute("role", "dialog"); + $dialog.setAttribute("aria-modal", "true"); + $dialog.setAttribute("aria-labelledby", titleId); + $dialog.innerHTML = this.dialogTemplate(titleId); - return $dialog -} + return $dialog; +}; Datepicker.prototype.createCalendar = function () { - const $tbody = this.$dialog.querySelector('tbody') - let dayCount = 0 + const $tbody = this.$dialog.querySelector("tbody"); + let dayCount = 0; for (let i = 0; i < 6; i++) { // create row - const $row = $tbody.insertRow(i) + const $row = $tbody.insertRow(i); for (let j = 0; j < 7; j++) { // create cell (day) - const $cell = document.createElement('td') - const $dateButton = document.createElement('button') + const $cell = document.createElement("td"); + const $dateButton = document.createElement("button"); - $cell.appendChild($dateButton) - $row.appendChild($cell) + $cell.appendChild($dateButton); + $row.appendChild($cell); - const calendarDay = new DSCalendarDay($dateButton, dayCount, i, j, this) - calendarDay.init() - this.calendarDays.push(calendarDay) - dayCount++ + const calendarDay = new DSCalendarDay($dateButton, dayCount, i, j, this); + calendarDay.init(); + this.calendarDays.push(calendarDay); + dayCount++; } } -} +}; Datepicker.prototype.toggleTemplate = function () { return `` -} - + `; +}; +/** + * HTML template for calendar dialog + * + * @param {string} [titleId] - Id attribute for dialog title + * @return {string} + */ Datepicker.prototype.dialogTemplate = function (titleId) { return `
@@ -248,168 +298,217 @@ Datepicker.prototype.dialogTemplate = function (titleId) {
- -
` -} - -Datepicker.prototype.createCalendarHeaders = function() { - this.dayLabels.forEach( (day) => { - const html = `${day}` - const headerRow = this.$dialog.querySelector('thead > tr') - headerRow.insertAdjacentHTML('beforeend', html) - }) -} + +
`; +}; + +Datepicker.prototype.createCalendarHeaders = function () { + this.dayLabels.forEach((day) => { + const html = `${day}`; + const $headerRow = this.$dialog.querySelector("thead > tr"); + $headerRow.insertAdjacentHTML("beforeend", html); + }); +}; +/** + * Pads given number with leading zeros + * + * @param {number} value - The value to be padded + * @param {number} length - The length in characters of the output + * @return {string} + */ Datepicker.prototype.leadingZeros = function (value, length = 2) { - let ret = value.toString() + let ret = value.toString(); while (ret.length < length) { - ret = `0${ret}` + ret = `0${ret}`; } - return ret -} + return ret; +}; -Datepicker.prototype.setOptions = function() { - this.setMinAndMaxDatesOnCalendar() - this.setExcludedDates() - this.setExcludedDays() - this.setLeadingZeros() - this.setWeekStartDay() -} +Datepicker.prototype.setOptions = function () { + this.setMinAndMaxDatesOnCalendar(); + this.setExcludedDates(); + this.setExcludedDays(); + this.setLeadingZeros(); + this.setWeekStartDay(); +}; Datepicker.prototype.setMinAndMaxDatesOnCalendar = function () { - if (this.$module.dataset.mindate) { - this.minDate = this.formattedDateFromString(this.$module.dataset.mindate, null) + if (this.config.minDate) { + this.minDate = this.formattedDateFromString( + this.$module.dataset.mindate, + null, + ); if (this.minDate && this.currentDate < this.minDate) { - this.currentDate = this.minDate + this.currentDate = this.minDate; } } - if (this.$module.dataset.maxdate) { - this.maxDate = this.formattedDateFromString(this.$module.dataset.maxdate, null) + if (this.config.maxDate) { + this.maxDate = this.formattedDateFromString( + this.$module.dataset.maxdate, + null, + ); if (this.maxDate && this.currentDate > this.maxDate) { - this.currentDate = this.maxDate + this.currentDate = this.maxDate; } } -} - -Datepicker.prototype.setExcludedDates = function() { - if(this.$module.dataset.excludeddates) { - this.excludedDates = this.$module.dataset.excludeddates - .replace(/\s+/, ' ') - .split(' ') - .map((item) => { - if (item.includes('-')) { - // parse the date range from the format "dd/mm/yyyy-dd/mm/yyyy" - const [startDate, endDate] = item.split('-').map(d => this.formattedDateFromString(d, null)) - if (startDate && endDate) { - const date = new Date(startDate.getTime()); - const dates = []; - while (date <= endDate) { - dates.push(new Date(date)); - date.setDate(date.getDate() + 1); - } - return dates - } - } else { - return this.formattedDateFromString(item, null) - } - }) - .flat() - .filter(item => item) +}; + +Datepicker.prototype.setExcludedDates = function () { + if (this.config.excludedDates) { + this.excludedDates = this.config.excludedDates + .replace(/\s+/, " ") + .split(" ") + .map((item) => { + if (item.includes("-")) { + // parse the date range from the format "dd/mm/yyyy-dd/mm/yyyy" + const [startDate, endDate] = item + .split("-") + .map((d) => this.formattedDateFromString(d, null)); + if (startDate && endDate) { + const date = new Date(startDate.getTime()); + const dates = []; + while (date <= endDate) { + dates.push(new Date(date)); + date.setDate(date.getDate() + 1); + } + return dates; + } + } else { + return this.formattedDateFromString(item, null); + } + }) + .flat() + .filter((item) => item); } - -} +}; Datepicker.prototype.setExcludedDays = function () { - if (this.$module.dataset.excludeddays) { + if (this.config.excludedDays) { // lowercase and arrange dayLabels to put indexOf sunday == 0 for comparison // with getDay() function - let weekDays = this.dayLabels.map(item => item.toLowerCase()) - if(this.config.weekStartDay === 'monday') { - weekDays.unshift(weekDays.pop()) + let weekDays = this.dayLabels.map((item) => item.toLowerCase()); + if (this.config.weekStartDay === "monday") { + weekDays.unshift(weekDays.pop()); } - this.excludedDays = this.$module.dataset.excludeddays - .replace(/\s+/, ' ') + this.excludedDays = this.config.excludedDays + .replace(/\s+/, " ") .toLowerCase() - .split(' ') - .map(item => weekDays.indexOf(item)) - .filter(item => item !== -1) + .split(" ") + .map((item) => weekDays.indexOf(item)) + .filter((item) => item !== -1); } -} +}; -Datepicker.prototype.setLeadingZeros = function() { - if (this.$module.dataset.leadingzeros) { - if(this.$module.dataset.leadingzeros.toLowerCase() === 'true') { +Datepicker.prototype.setLeadingZeros = function () { + if (typeof this.config.leadingZeros !== "boolean") { + if (this.config.leadingZeros.toLowerCase() === "true") { this.config.leadingZeros = true; } - if(this.$module.dataset.leadingzeros.toLowerCase() === 'false') { + if (this.config.leadingzeros.toLowerCase() === "false") { this.config.leadingZeros = false; } } -} - -Datepicker.prototype.setWeekStartDay = function() { - const weekStartDayParam = this.$module.dataset.weekstartday; - if(weekStartDayParam?.toLowerCase() === 'sunday' ) { - this.config.weekStartDay = 'sunday' - this.dayLabels.unshift(this.dayLabels.pop()) +}; + +Datepicker.prototype.setWeekStartDay = function () { + const weekStartDayParam = this.config.weekStartDay; + if (weekStartDayParam?.toLowerCase() === "sunday") { + this.config.weekStartDay = "sunday"; + // Rotate dayLabels array to put Sunday as the first item + this.dayLabels.unshift(this.dayLabels.pop()); } - if(weekStartDayParam?.toLowerCase() === 'monday' ) { - this.config.weekStartDay = 'monday' + if (weekStartDayParam?.toLowerCase() === "monday") { + this.config.weekStartDay = "monday"; } -} +}; +/** + * Determine if a date is selecteable + * + * @param {Date} date - the date to check + * @return {boolean} + * + */ Datepicker.prototype.isExcludedDate = function (date) { - if (this.minDate && this.minDate > date) { - return true - } - - if (this.maxDate && this.maxDate < date) { - return true - } + if (this.minDate && this.minDate > date) { + return true; + } - for (const excludedDate of this.excludedDates) { - if (date.toDateString() === excludedDate.toDateString()) { - return true - } - } + if (this.maxDate && this.maxDate < date) { + return true; + } - if (this.excludedDays.includes(date.getDay())) { - return true + for (const excludedDate of this.excludedDates) { + if (date.toDateString() === excludedDate.toDateString()) { + return true; } + } - return false; -} - -Datepicker.prototype.formattedDateFromString = function (dateString, fallback = new Date()) { - let formattedDate = null - const dateFormatPattern = /(\d{1,2})([-/,. ])(\d{1,2})[-/,. ](\d{4})/ - - if (!dateFormatPattern.test(dateString)) return fallback + if (this.excludedDays.includes(date.getDay())) { + return true; + } - const match = dateString.match(dateFormatPattern) - const separator = match[2] - const day = match[1] - const month = match[3] - const year = match[4] + return false; +}; - formattedDate = new Date(`${month}${separator}${day}${separator}${year}`) +/** + * Get a Date object from a string + * + * @param {string} dateString - string in the format d/m/yyyy dd/mm/yyyy + * @param {Date} fallback - date object to return if formatting fails + * @return {Date} + */ +Datepicker.prototype.formattedDateFromString = function ( + dateString, + fallback = new Date(), +) { + let formattedDate = null; + // Accepts d/m/yyyy and dd/mm/yyyy + const dateFormatPattern = /(\d{1,2})([-/,. ])(\d{1,2})[-/,. ](\d{4})/; + + if (!dateFormatPattern.test(dateString)) return fallback; + + const match = dateString.match(dateFormatPattern); + const separator = match[2]; + const day = match[1]; + const month = match[3]; + const year = match[4]; + + formattedDate = new Date(`${month}${separator}${day}${separator}${year}`); if (formattedDate instanceof Date && !isNaN(formattedDate)) { - return formattedDate + return formattedDate; } - return fallback -} + return fallback; +}; +/** + * Get a formatted date string from a Date object + * + * @param {Date} date - date to format to a string + * @return {string} + */ Datepicker.prototype.formattedDateFromDate = function (date) { - if(this.config.leadingZeros) { - return `${this.leadingZeros(date.getDate())}/${this.leadingZeros(date.getMonth() + 1)}/${date.getFullYear()}` + if (this.config.leadingZeros) { + return `${this.leadingZeros(date.getDate())}/${this.leadingZeros(date.getMonth() + 1)}/${date.getFullYear()}`; } else { - return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}` + return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`; } -} +}; + +/** + * Get a huma readabel date in the format Monday 2 March 2024 + * + * @param {Date} - date to format + * @return {string} + */ +Datepicker.prototype.formattedDateHuman = function (date) { + return `${this.dayLabels[date.getDay()]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}`; +}; Datepicker.prototype.backgroundClick = function (event) { if ( @@ -418,347 +517,432 @@ Datepicker.prototype.backgroundClick = function (event) { !this.$input.contains(event.target) && !this.$calendarButton.contains(event.target) ) { - event.preventDefault() - this.closeDialog() + event.preventDefault(); + this.closeDialog(); } -} - -Datepicker.prototype.formattedDateHuman = function (date) { - return `${this.dayLabels[date.getDay()]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}` -} +}; Datepicker.prototype.firstButtonKeydown = function (event) { if (event.keyCode === this.keycodes.tab && event.shiftKey) { - this.$lastButtonInDialog.focus() - event.preventDefault() + this.$lastButtonInDialog.focus(); + event.preventDefault(); } -} +}; Datepicker.prototype.lastButtonKeydown = function (event) { if (event.keyCode === this.keycodes.tab && !event.shiftKey) { - this.$firstButtonInDialog.focus() - event.preventDefault() + this.$firstButtonInDialog.focus(); + event.preventDefault(); } -} +}; // render calendar Datepicker.prototype.updateCalendar = function () { - this.$dialogTitle.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}` + this.$dialogTitle.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}`; - const day = this.currentDate - - const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1) + const day = this.currentDate; + const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1); let dayOfWeek; - if ( this.config.weekStartDay === 'monday') { - dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1 // Change logic to make Monday first day of week, i.e. 0 + if (this.config.weekStartDay === "monday") { + dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1; // Change logic to make Monday first day of week, i.e. 0 } else { - dayOfWeek = firstOfMonth.getDay() + dayOfWeek = firstOfMonth.getDay(); } - firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek) + firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek); - const thisDay = new Date(firstOfMonth) + const thisDay = new Date(firstOfMonth); // loop through our days for (let i = 0; i < this.calendarDays.length; i++) { - const hidden = thisDay.getMonth() !== day.getMonth() - const disabled = this.isExcludedDate(thisDay) + const hidden = thisDay.getMonth() !== day.getMonth(); + const disabled = this.isExcludedDate(thisDay); - this.calendarDays[i].update(thisDay, hidden, disabled) + this.calendarDays[i].update(thisDay, hidden, disabled); - thisDay.setDate(thisDay.getDate() + 1) + thisDay.setDate(thisDay.getDate() + 1); } -} +}; Datepicker.prototype.setCurrentDate = function (focus = true) { - const { currentDate } = this - - this.calendarDays.forEach(calendarDay => { - calendarDay.button.classList.add('moj-datepicker-button') - calendarDay.button.classList.add('moj-datepicker-calendar__day') - calendarDay.button.setAttribute('tabindex', -1) - calendarDay.button.classList.remove('selected') - const calendarDayDate = calendarDay.date - calendarDayDate.setHours(0, 0, 0, 0) - - const today = new Date() - today.setHours(0, 0, 0, 0) - - if (calendarDayDate.getTime() === currentDate.getTime() /* && !calendarDay.button.disabled */) { + const { currentDate } = this; + + this.calendarDays.forEach((calendarDay) => { + calendarDay.button.classList.add("moj-datepicker-button"); + calendarDay.button.classList.add("moj-datepicker-calendar__day"); + calendarDay.button.setAttribute("tabindex", -1); + calendarDay.button.classList.remove("selected"); + const calendarDayDate = calendarDay.date; + calendarDayDate.setHours(0, 0, 0, 0); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if ( + calendarDayDate.getTime() === + currentDate.getTime() /* && !calendarDay.button.disabled */ + ) { if (focus) { - calendarDay.button.setAttribute('tabindex', 0) - calendarDay.button.focus() - calendarDay.button.classList.add('selected') + calendarDay.button.setAttribute("tabindex", 0); + calendarDay.button.focus(); + calendarDay.button.classList.add("selected"); } } - if (this.inputDate && calendarDayDate.getTime() === this.inputDate.getTime()) { - calendarDay.button.classList.add('current') - calendarDay.button.setAttribute('aria-selected', true) + if ( + this.inputDate && + calendarDayDate.getTime() === this.inputDate.getTime() + ) { + calendarDay.button.classList.add("current"); + calendarDay.button.setAttribute("aria-selected", true); } else { - calendarDay.button.classList.remove('current') - calendarDay.button.removeAttribute('aria-selected') + calendarDay.button.classList.remove("current"); + calendarDay.button.removeAttribute("aria-selected"); } if (calendarDayDate.getTime() === today.getTime()) { - calendarDay.button.classList.add('today') + calendarDay.button.classList.add("today"); } else { - calendarDay.button.classList.remove('today') + calendarDay.button.classList.remove("today"); } - }) + }); // if no date is tab-able, make the first non-disabled date tab-able if (!focus) { - const enabledDays = this.calendarDays.filter(calendarDay => { - return window.getComputedStyle(calendarDay.button).display === 'block' && !calendarDay.button.disabled - }) + const enabledDays = this.calendarDays.filter((calendarDay) => { + return ( + window.getComputedStyle(calendarDay.button).display === "block" && + !calendarDay.button.disabled + ); + }); - enabledDays[0].button.setAttribute('tabindex', 0) + enabledDays[0].button.setAttribute("tabindex", 0); - this.currentDate = enabledDays[0].date + this.currentDate = enabledDays[0].date; } -} +}; Datepicker.prototype.selectDate = function (date) { if (this.isExcludedDate(date)) { - return + return; } - this.$calendarButton.querySelector('span').innerText = `Choose date. Selected date is ${this.formattedDateHuman( - date, - )}` - this.$input.value = this.formattedDateFromDate(date) + this.$calendarButton.querySelector("span").innerText = + `Choose date. Selected date is ${this.formattedDateHuman(date)}`; + this.$input.value = this.formattedDateFromDate(date); - const changeEvent = new Event('change', { bubbles: true, cancelable: true }) - this.$input.dispatchEvent(changeEvent) + const changeEvent = new Event("change", { bubbles: true, cancelable: true }); + this.$input.dispatchEvent(changeEvent); - this.closeDialog() -} + this.closeDialog(); +}; Datepicker.prototype.isOpen = function () { - return this.$dialog.classList.contains('moj-datepicker-dialog--open') -} + return this.$dialog.classList.contains("moj-datepicker-dialog--open"); +}; Datepicker.prototype.toggleDialog = function (event) { - event.preventDefault() + event.preventDefault(); if (this.isOpen()) { - this.closeDialog() + this.closeDialog(); } else { - this.setMinAndMaxDatesOnCalendar() - this.openDialog() + this.setMinAndMaxDatesOnCalendar(); + this.openDialog(); } -} +}; Datepicker.prototype.openDialog = function () { // display the dialog - this.$dialog.style.display = 'block' - this.$dialog.classList.add('moj-datepicker-dialog--open') - this.$calendarButton.setAttribute('aria-expanded', 'true') + this.$dialog.style.display = "block"; + this.$dialog.classList.add("moj-datepicker-dialog--open"); + this.$calendarButton.setAttribute("aria-expanded", "true"); // position the dialog // if input is wider than dialog pin it to the right - if(this.$input.offsetWidth > this.$dialog.offsetWidth) { - this.$dialog.style.right = `0px` + if (this.$input.offsetWidth > this.$dialog.offsetWidth) { + this.$dialog.style.right = `0px`; } - this.$dialog.style.top = `${this.$input.offsetHeight + 3}px` + this.$dialog.style.top = `${this.$input.offsetHeight + 3}px`; // get the date from the input element if (this.$input.value.match(/^(\d{1,2})([-/,. ])(\d{1,2})[-/,. ](\d{4})$/)) { - this.inputDate = this.formattedDateFromString(this.$input.value) - this.currentDate = this.inputDate + this.inputDate = this.formattedDateFromString(this.$input.value); + this.currentDate = this.inputDate; } - this.updateCalendar() - this.setCurrentDate() -} + this.updateCalendar(); + this.setCurrentDate(); +}; Datepicker.prototype.closeDialog = function () { - this.$dialog.style.display = 'none' - this.$dialog.classList.remove('moj-datepicker-dialog--open') - this.$calendarButton.setAttribute('aria-expanded', 'false') - this.$calendarButton.focus() -} + this.$dialog.style.display = "none"; + this.$dialog.classList.remove("moj-datepicker-dialog--open"); + this.$calendarButton.setAttribute("aria-expanded", "false"); + this.$calendarButton.focus(); +}; Datepicker.prototype.goToDate = function (date, focus) { - const current = this.currentDate - this.currentDate = date + const current = this.currentDate; + this.currentDate = date; - if (current.getMonth() !== this.currentDate.getMonth() || current.getFullYear() !== this.currentDate.getFullYear()) { - this.updateCalendar() + if ( + current.getMonth() !== this.currentDate.getMonth() || + current.getFullYear() !== this.currentDate.getFullYear() + ) { + this.updateCalendar(); } - this.setCurrentDate(focus) -} + this.setCurrentDate(focus); +}; // day navigation Datepicker.prototype.focusNextDay = function () { - const date = new Date(this.currentDate) - date.setDate(date.getDate() + 1) - this.goToDate(date) -} + const date = new Date(this.currentDate); + date.setDate(date.getDate() + 1); + this.goToDate(date); +}; Datepicker.prototype.focusPreviousDay = function () { - const date = new Date(this.currentDate) - date.setDate(date.getDate() - 1) - this.goToDate(date) -} + const date = new Date(this.currentDate); + date.setDate(date.getDate() - 1); + this.goToDate(date); +}; // week navigation Datepicker.prototype.focusNextWeek = function () { - const date = new Date(this.currentDate) - date.setDate(date.getDate() + 7) - this.goToDate(date) -} + const date = new Date(this.currentDate); + date.setDate(date.getDate() + 7); + this.goToDate(date); +}; Datepicker.prototype.focusPreviousWeek = function () { - const date = new Date(this.currentDate) - date.setDate(date.getDate() - 7) - this.goToDate(date) -} + const date = new Date(this.currentDate); + date.setDate(date.getDate() - 7); + this.goToDate(date); +}; Datepicker.prototype.focusFirstDayOfWeek = function () { - const date = new Date(this.currentDate) - date.setDate(date.getDate() - date.getDay()) - this.goToDate(date) -} + const date = new Date(this.currentDate); + date.setDate(date.getDate() - date.getDay()); + this.goToDate(date); +}; Datepicker.prototype.focusLastDayOfWeek = function () { - const date = new Date(this.currentDate) - date.setDate(date.getDate() - date.getDay() + 6) - this.goToDate(date) -} + const date = new Date(this.currentDate); + date.setDate(date.getDate() - date.getDay() + 6); + this.goToDate(date); +}; // month navigation Datepicker.prototype.focusNextMonth = function (event, focus = true) { - event.preventDefault() - const date = new Date(this.currentDate) - date.setMonth(date.getMonth() + 1, 1) - this.goToDate(date, focus) -} + event.preventDefault(); + const date = new Date(this.currentDate); + date.setMonth(date.getMonth() + 1, 1); + this.goToDate(date, focus); +}; Datepicker.prototype.focusPreviousMonth = function (event, focus = true) { - event.preventDefault() - const date = new Date(this.currentDate) - date.setMonth(date.getMonth() - 1, 1) - this.goToDate(date, focus) -} + event.preventDefault(); + const date = new Date(this.currentDate); + date.setMonth(date.getMonth() - 1, 1); + this.goToDate(date, focus); +}; // year navigation Datepicker.prototype.focusNextYear = function (event, focus = true) { - event.preventDefault() - const date = new Date(this.currentDate) - date.setFullYear(date.getFullYear() + 1, date.getMonth(), 1) - this.goToDate(date, focus) -} + event.preventDefault(); + const date = new Date(this.currentDate); + date.setFullYear(date.getFullYear() + 1, date.getMonth(), 1); + this.goToDate(date, focus); +}; Datepicker.prototype.focusPreviousYear = function (event, focus = true) { - event.preventDefault() - const date = new Date(this.currentDate) - date.setFullYear(date.getFullYear() - 1, date.getMonth(), 1) - this.goToDate(date, focus) -} + event.preventDefault(); + const date = new Date(this.currentDate); + date.setFullYear(date.getFullYear() - 1, date.getMonth(), 1); + this.goToDate(date, focus); +}; + +/** + * Parse dataset + * + * Loop over an object and normalise each value using {@link normaliseString}, + * optionally expanding nested `i18n.field` + * + * @param {{ schema: Schema }} Component - Component class + * @param {DOMStringMap} dataset - HTML element dataset + * @returns {Object} Normalised dataset + */ +Datepicker.prototype.parseDataset = function (schema, dataset) { + const parsed = {}; + + for (const [field, attributes] of Object.entries(schema.properties)) { + if (field in dataset) { + parsed[field] = dataset[field]; + } + } + + return parsed; +}; + +/** + * Config merging function + * + * Takes any number of objects and combines them together, with + * greatest priority on the LAST item passed in. + * + * @param {...{ [key: string]: unknown }} configObjects - Config objects to merge + * @returns {{ [key: string]: unknown }} A merged config object + */ +Datepicker.prototype.mergeConfigs = function (...configObjects) { + const formattedConfigObject = {}; + + // Loop through each of the passed objects + for (const configObject of configObjects) { + for (const key of Object.keys(configObject)) { + const option = formattedConfigObject[key]; + const override = configObject[key]; + + // Push their keys one-by-one into formattedConfigObject. Any duplicate + // keys with object values will be merged, otherwise the new value will + // override the existing value. + if (typeof option === "object" && typeof override === "object") { + // @ts-expect-error Index signature for type 'string' is missing + formattedConfigObject[key] = this.mergeConfigs(option, override); + } else { + formattedConfigObject[key] = override; + } + } + } + + return formattedConfigObject; +}; + /** * - * @param button - * @param index - * @param row - * @param column - * @param picker + * @param {HTMLElement} button + * @param {number} index + * @param {number} row + * @param {number} column + * @param {Datepicker} picker * @constructor */ function DSCalendarDay(button, index, row, column, picker) { - this.index = index - this.row = row - this.column = column - this.button = button - this.picker = picker + this.index = index; + this.row = row; + this.column = column; + this.button = button; + this.picker = picker; - this.date = new Date() + this.date = new Date(); } DSCalendarDay.prototype.init = function () { - this.button.addEventListener('keydown', this.keyPress.bind(this)) - this.button.addEventListener('click', this.click.bind(this)) -} + this.button.addEventListener("keydown", this.keyPress.bind(this)); + this.button.addEventListener("click", this.click.bind(this)); +}; +/** + * @param {Date} day - the Date for the calendar day + * @param {boolean} hidden - visibility of the day + * @param {boolean} disabled - is the day selectable or excluded + */ DSCalendarDay.prototype.update = function (day, hidden, disabled) { - let label = day.getDate() - let accessibleLabel = day.toLocaleDateString('en-GB', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - } ) + let label = day.getDate(); + let accessibleLabel = day.toLocaleDateString("en-GB", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }); if (disabled) { - this.button.setAttribute('aria-disabled', true) - accessibleLabel = 'Excluded date, ' + accessibleLabel + this.button.setAttribute("aria-disabled", true); + accessibleLabel = "Excluded date, " + accessibleLabel; } else { - this.button.removeAttribute('aria-disabled') + this.button.removeAttribute("aria-disabled"); } if (hidden) { - this.button.style.display = 'none' + this.button.style.display = "none"; } else { - this.button.style.display = 'block' + this.button.style.display = "block"; } - this.button.innerHTML = `${accessibleLabel}` - this.date = new Date(day) -} + this.button.innerHTML = `${accessibleLabel}`; + this.date = new Date(day); +}; DSCalendarDay.prototype.click = function (event) { - this.picker.goToDate(this.date) - this.picker.selectDate(this.date) + this.picker.goToDate(this.date); + this.picker.selectDate(this.date); - event.stopPropagation() - event.preventDefault() -} + event.stopPropagation(); + event.preventDefault(); +}; DSCalendarDay.prototype.keyPress = function (event) { - let calendarNavKey = true + let calendarNavKey = true; switch (event.keyCode) { case this.picker.keycodes.left: - this.picker.focusPreviousDay() - break + this.picker.focusPreviousDay(); + break; case this.picker.keycodes.right: - this.picker.focusNextDay() - break + this.picker.focusNextDay(); + break; case this.picker.keycodes.up: - this.picker.focusPreviousWeek() - break + this.picker.focusPreviousWeek(); + break; case this.picker.keycodes.down: - this.picker.focusNextWeek() - break + this.picker.focusNextWeek(); + break; case this.picker.keycodes.home: - this.picker.focusFirstDayOfWeek() - break + this.picker.focusFirstDayOfWeek(); + break; case this.picker.keycodes.end: - this.picker.focusLastDayOfWeek() - break + this.picker.focusLastDayOfWeek(); + break; case this.picker.keycodes.pageup: // eslint-disable-next-line no-unused-expressions - event.shiftKey ? this.picker.focusPreviousYear(event) : this.picker.focusPreviousMonth(event) - break + event.shiftKey + ? this.picker.focusPreviousYear(event) + : this.picker.focusPreviousMonth(event); + break; case this.picker.keycodes.pagedown: // eslint-disable-next-line no-unused-expressions - event.shiftKey ? this.picker.focusNextYear(event) : this.picker.focusNextMonth(event) - break + event.shiftKey + ? this.picker.focusNextYear(event) + : this.picker.focusNextMonth(event); + break; case this.picker.keycodes.esc: - this.picker.closeDialog() - break + this.picker.closeDialog(); + break; default: - calendarNavKey = false - break + calendarNavKey = false; + break; } if (calendarNavKey) { - event.preventDefault() - event.stopPropagation() + event.preventDefault(); + event.stopPropagation(); } -} +}; MOJFrontend.DatePicker = Datepicker; + +/** + * Schema for component config + * + * @typedef {object} Schema + * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties + */ + +/** + * Schema property for component config + * + * @typedef {object} SchemaProperty + * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type + */ diff --git a/src/moj/components/date-picker/template.njk b/src/moj/components/date-picker/template.njk index 405841fd..f3164c59 100644 --- a/src/moj/components/date-picker/template.njk +++ b/src/moj/components/date-picker/template.njk @@ -9,27 +9,27 @@ {% set attributes = { "data-module": 'moj-date-picker', - "data-mindate": { + "data-min-date": { value: params.minDate, optional: true }, - "data-maxdate": { + "data-max-date": { value: params.maxDate, optional: true }, - "data-excludeddates": { + "data-excluded-dates": { value: params.excludedDates, optional: true }, - "data-excludeddays": { + "data-excluded-days": { value: params.excludedDays, optional: true }, - "data-leadingzeros": { + "data-leading-zeros": { value: params.leadingZeros, optional: true }, - "data-weekstartday": { + "data-week-start-day": { value: params.weekStartDay, optional: true } From 638975e671762060b01465b73b0072ca441b2c6b Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Thu, 25 Jul 2024 14:39:31 +0100 Subject: [PATCH 32/58] feat(date picker): update hover cover remove second excluded dates example --- docs/components/date-picker.md | 4 +--- .../index.njk | 2 +- .../index.njk | 2 +- src/moj/components/date-picker/_date-picker.scss | 4 ++-- 4 files changed, 5 insertions(+), 7 deletions(-) rename docs/examples/{date-picker-disabled-dates => date-picker-excluded-dates}/index.njk (89%) rename docs/examples/{date-picker-disabled-days => date-picker-excluded-days}/index.njk (86%) diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index 5eabd366..f9ba86f3 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -53,9 +53,7 @@ Users may type unavailable or disabled dates in the input field, so error messag You can exclude (or disable) specific dates and days of the week from the date picker, for example bank holidays or every weekend. -{% example "/examples/date-picker-disabled-dates", 590 %} - -{% example "/examples/date-picker-disabled-days", 590 %} +{% example "/examples/date-picker-excluded-dates", 590 %} Excluded dates have the right visual contrast but may be harder to view for users with low vision or colour blindness. diff --git a/docs/examples/date-picker-disabled-dates/index.njk b/docs/examples/date-picker-excluded-dates/index.njk similarity index 89% rename from docs/examples/date-picker-disabled-dates/index.njk rename to docs/examples/date-picker-excluded-dates/index.njk index 256bd6a9..0b12090c 100644 --- a/docs/examples/date-picker-disabled-dates/index.njk +++ b/docs/examples/date-picker-excluded-dates/index.njk @@ -1,6 +1,6 @@ --- layout: layouts/example.njk -title: Date Picker Disabled Dates (example) +title: Date Picker Excluded Dates (example) --- {%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} diff --git a/docs/examples/date-picker-disabled-days/index.njk b/docs/examples/date-picker-excluded-days/index.njk similarity index 86% rename from docs/examples/date-picker-disabled-days/index.njk rename to docs/examples/date-picker-excluded-days/index.njk index d80303a7..8b86c2dc 100644 --- a/docs/examples/date-picker-disabled-days/index.njk +++ b/docs/examples/date-picker-excluded-days/index.njk @@ -1,6 +1,6 @@ --- layout: layouts/example.njk -title: Date Picker Disabled Days (example) +title: Date Picker Excluded Days (example) --- {%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} diff --git a/src/moj/components/date-picker/_date-picker.scss b/src/moj/components/date-picker/_date-picker.scss index 48b58bb7..160d9695 100644 --- a/src/moj/components/date-picker/_date-picker.scss +++ b/src/moj/components/date-picker/_date-picker.scss @@ -107,7 +107,7 @@ &:hover { color: $govuk-text-colour; - background-color: $govuk-border-colour; + background-color: #949494; text-decoration: none; -webkit-box-decoration-break: clone; box-decoration-break: clone; @@ -127,7 +127,7 @@ } &:focus:hover { - background-color: govuk-colour('mid-grey'); + background-color: #949494; outline-color: govuk-colour('yellow'); &:after { background-color: transparent; From fde3653310c154fb135a930b34a99a35a24ed216 Mon Sep 17 00:00:00 2001 From: helennickols Date: Thu, 25 Jul 2024 15:05:01 +0100 Subject: [PATCH 33/58] docs(date picker component): update content --- docs/components/date-picker.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index f9ba86f3..c653c77a 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -7,6 +7,7 @@ title: Date picker {% example "/examples/date-picker", 590 %} + ## Overview When users first open the date picker's calendar it'll show today's date. Users do not have to use the calendar view to select a date - they can also enter one directly into the text field. @@ -37,6 +38,7 @@ Date pickers are fully navigable using a keyboard, but can be slow for keyboard- There's also the ['Ask users for dates' pattern in the GOV.UK Design System](https://design-system.service.gov.uk/patterns/dates/). + ## How to use ### Hint text @@ -65,8 +67,6 @@ Follow the [GOV.UK Design System guidance on error messages](https://design-syst {% example "/examples/date-picker-error", 590 %} -### Error messages - @@ -102,6 +102,7 @@ Follow the [GOV.UK Design System guidance on error messages](https://design-syst If you're using more than one date picker, give each text field its own error summary and message (even if the error is the same). + ## Examples ### Filtering information with a date picker @@ -110,14 +111,8 @@ If you're using more than one date picker, give each text field its own error su ### Asking a question with a date picker -

#

- -## Future changes - -In future versions of this documentation, there will be: +

A screenshot with the title 'What date do you want to view appointments for?' Underneath is the title 'Date' and then a text input field with the calendar icon. Underneath that is a green 'Continue' button.

-- guidance on using date ranges with this component -- Welsh language content for the designs, including error messages ## Contributors From 656e1c9fdb36f9c3d49e3b4761c072fd857e0c85 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Thu, 25 Jul 2024 16:13:09 +0100 Subject: [PATCH 34/58] feat(date picker): update diasbled dates with strikethrough and hover colors to darker grey Ensure metting WCAG color contrast rules by amending hover color, and add strikethrough for clarity of meaning on excluded dates --- src/moj/components/date-picker/_date-picker.scss | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/moj/components/date-picker/_date-picker.scss b/src/moj/components/date-picker/_date-picker.scss index 160d9695..ff79fb68 100644 --- a/src/moj/components/date-picker/_date-picker.scss +++ b/src/moj/components/date-picker/_date-picker.scss @@ -102,7 +102,7 @@ background-color: govuk-colour('light-grey'); color: govuk-colour('black'); cursor: not-allowed; - /* pointer-events: none; */ + text-decoration: line-through; } &:hover { @@ -118,7 +118,6 @@ color: govuk-colour('black'); background-color: govuk-colour('yellow'); outline-color: transparent; - text-decoration: none; -webkit-box-decoration-break: clone; box-decoration-break: clone; &:after { @@ -166,7 +165,7 @@ &:hover { outline-color: govuk-colour('blue'); - background-color: govuk-colour('mid-grey'); + background-color: #949494; color: govuk-colour('black'); &:after { @@ -273,13 +272,13 @@ } &:hover { - background-color: govuk-colour('mid-grey'); + background-color: #949494; color: govuk-colour('black'); - border-bottom: 4px solid govuk-colour('mid-grey'); + border-bottom: 4px solid #949494; } &:focus:hover { - background-color: govuk-colour('mid-grey'); + background-color: #949494; color: govuk-colour('black'); border-bottom: 4px solid govuk-colour('black'); } From d4ba814b39df71871ce448505372b3d122e2c2d4 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Fri, 26 Jul 2024 09:50:37 +0100 Subject: [PATCH 35/58] feat(date picker): fix example open in new window link styling --- docs/_includes/arguments/date-picker.md | 16 ++++++++++++++++ docs/assets/stylesheets/components/_example.scss | 10 +++++----- gulpfile.js | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/docs/_includes/arguments/date-picker.md b/docs/_includes/arguments/date-picker.md index e69de29b..57f1a897 100644 --- a/docs/_includes/arguments/date-picker.md +++ b/docs/_includes/arguments/date-picker.md @@ -0,0 +1,16 @@ +| Name | Type | Required | Description | +| ------------ | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- | +| id | string | Yes | The ID of the input. | +| name | string | Yes | The name of the input, which is submitted with the form data. | +| value | string | No | Optional initial value of the input. | +| formGroup | object | No | Additional options for the form group containing the text input component. See [formGroup](#options-date-picker-form-group). | +| label | object | Yes | The label used by the text input component. See [GOV.UK text input documentation](https://design-system.service.gov.uk/components/text-input/) for label options. | +| hint | object | No | Can be used to add a hint to a text input component. See [GOV.UK text input documentation](https://design-system.service.gov.uk/components/text-input/) for hint options. | +| errorMessage | object | No | Can be used to add an error message to the text input component. The error message component will not display if you use a falsy value for `errorMessage`, for example `false` or `null`. See [GOV.UK text input documentation](https://design-system.service.gov.uk/components/text-input/) for errorMessage options. | +| minDate | string | No | Earliest date that can be selected (format dd/mm/yyyy) | +| maxDate | string | No | Latest date that can be selected (format dd/mm/yyyy) | +| exludedDates | string | No | String of pace separated dates that cannot be selected | +| excludedDays | string | No | String of space separated days of the week that cannot be selected | +| weekStartDay | string | No | Day of the week the calendar starts on. Either 'monday' or 'sunday'. Defaults to 'monday' | + + diff --git a/docs/assets/stylesheets/components/_example.scss b/docs/assets/stylesheets/components/_example.scss index cc7c8ceb..b904bce0 100644 --- a/docs/assets/stylesheets/components/_example.scss +++ b/docs/assets/stylesheets/components/_example.scss @@ -24,7 +24,7 @@ } .app-example__new-window { - @include govuk-font($size: 14); + @include govuk-font($size: 16); border: 1px solid $govuk-border-colour; position: absolute; top: -1px; left: -1px; @@ -34,18 +34,18 @@ background-color: white; color: govuk-colour("blue"); display: block; - padding: 5px 10px; + margin: 8px; text-decoration: none; } a:hover { - color: govuk-colour("light-blue"); + color: $govuk-link-hover-colour; } a:focus { - // color: $govuk-focus-text-colour; + color: $govuk-focus-text-colour; background-color: $govuk-focus-colour; - // box-shadow: 0 -2px $govuk-focus-colour, 0 4px $govuk-focus-text-colour; + box-shadow: 0 -2px $govuk-focus-colour, 0 4px $govuk-focus-text-colour; // border-color: $govuk-focus-text-colour; } diff --git a/gulpfile.js b/gulpfile.js index c55fadf7..3899269c 100755 --- a/gulpfile.js +++ b/gulpfile.js @@ -38,7 +38,7 @@ gulp.task( gulp.task( "watch:styles", () => { gulp.watch( - ["docs/assets/**.*.scss", "src/moj/components/**/*.scss"], + ["docs/assets/**/*.scss", "src/moj/components/**/*.scss"], gulp.series(["docs:styles"]), ) } From a004155a453c7f1d4b917b9071eb77504a282347 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Fri, 26 Jul 2024 11:40:25 +0100 Subject: [PATCH 36/58] refactor(date picker): small final tidying tweaks to datepicker --- package-lock.json | 214 +++++++++++++++++- src/moj/components/date-picker/date-picker.js | 4 +- 2 files changed, 210 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0b1800a9..12095db5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15158,6 +15158,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", "dev": true, @@ -15173,6 +15215,87 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/npm/node_modules/@isaacs/string-locale-compare": { "version": "1.1.0", "dev": true, @@ -17911,7 +18034,6 @@ "name": "string-width", "version": "4.2.3", "dev": true, - "inBundle": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -17938,7 +18060,6 @@ "name": "strip-ansi", "version": "6.0.1", "dev": true, - "inBundle": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -18165,7 +18286,6 @@ "name": "wrap-ansi", "version": "7.0.0", "dev": true, - "inBundle": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -34809,6 +34929,36 @@ "strip-ansi": "^7.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "bundled": true, + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "bundled": true, + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "bundled": true, + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, "strip-ansi": { "version": "7.1.0", "bundled": true, @@ -34816,6 +34966,61 @@ "requires": { "ansi-regex": "^6.0.1" } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "bundled": true, + "dev": true + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "bundled": true, + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "bundled": true, + "dev": true + }, + "string-width": { + "version": "4.2.3", + "bundled": true, + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } } } }, @@ -36676,7 +36881,6 @@ }, "string-width-cjs": { "version": "npm:string-width@4.2.3", - "bundled": true, "dev": true, "requires": { "emoji-regex": "^8.0.0", @@ -36694,7 +36898,6 @@ }, "strip-ansi-cjs": { "version": "npm:strip-ansi@6.0.1", - "bundled": true, "dev": true, "requires": { "ansi-regex": "^5.0.1" @@ -36889,7 +37092,6 @@ }, "wrap-ansi-cjs": { "version": "npm:wrap-ansi@7.0.0", - "bundled": true, "dev": true, "requires": { "ansi-styles": "^4.0.0", diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 452e2b4a..8fcd81d9 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -105,7 +105,6 @@ Datepicker.prototype.init = function () { Datepicker.prototype.initControls = function () { this.id = `datepicker-${this.$input.id}`; - // Create datepicker popup dialog this.$dialog = this.createDialog(); this.createCalendarHeaders(); @@ -243,6 +242,7 @@ Datepicker.prototype.toggleTemplate = function () { `; }; + /** * HTML template for calendar dialog * @@ -501,7 +501,7 @@ Datepicker.prototype.formattedDateFromDate = function (date) { }; /** - * Get a huma readabel date in the format Monday 2 March 2024 + * Get a human readable date in the format Monday 2 March 2024 * * @param {Date} - date to format * @return {string} From 354afc7b4f59eb1ea8c104d11056379ba59f6d88 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Fri, 26 Jul 2024 11:51:50 +0100 Subject: [PATCH 37/58] fix(examples): adds example title to example tabs links to prevent many redundant links on the page This was raised in an accessibility review by Ben Proctor-Rogers. Each of the example links on the page has the same label, which is not a good experience for screen reader users. This change adds the example title to the link as visually hidden text. --- docs/_includes/example.njk | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/_includes/example.njk b/docs/_includes/example.njk index cb7f1763..24a3f222 100644 --- a/docs/_includes/example.njk +++ b/docs/_includes/example.njk @@ -6,10 +6,22 @@
From 8d16275356a88b9c3dbf803f38fb2d0f67b41aa3 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Fri, 26 Jul 2024 12:18:34 +0100 Subject: [PATCH 38/58] docs(date picker): add figma link to first example --- docs/examples/date-picker/index.njk | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/examples/date-picker/index.njk b/docs/examples/date-picker/index.njk index 5efa828f..8c5012eb 100644 --- a/docs/examples/date-picker/index.njk +++ b/docs/examples/date-picker/index.njk @@ -2,6 +2,7 @@ layout: layouts/example.njk title: Date Picker (example) arguments: date-picker +figma_link: https://www.figma.com/design/N2xqOFkyehXwcD9DxU1gEq/MoJ-Figma-Kit?node-id=792-861&t=6DfPOX7RAnjrVE0j-0 --- {%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} From 30a7644169edb4b6fe69832ce2effbcbd7b4ad3f Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Fri, 26 Jul 2024 13:46:52 +0100 Subject: [PATCH 39/58] Revert "refactor(date picker): small final tidying tweaks to datepicker" This reverts commit a004155a453c7f1d4b917b9071eb77504a282347. --- package-lock.json | 214 +----------------- src/moj/components/date-picker/date-picker.js | 4 +- 2 files changed, 8 insertions(+), 210 deletions(-) diff --git a/package-lock.json b/package-lock.json index 12095db5..0b1800a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15158,48 +15158,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", "dev": true, @@ -15215,87 +15173,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/npm/node_modules/@isaacs/string-locale-compare": { "version": "1.1.0", "dev": true, @@ -18034,6 +17911,7 @@ "name": "string-width", "version": "4.2.3", "dev": true, + "inBundle": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -18060,6 +17938,7 @@ "name": "strip-ansi", "version": "6.0.1", "dev": true, + "inBundle": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -18286,6 +18165,7 @@ "name": "wrap-ansi", "version": "7.0.0", "dev": true, + "inBundle": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -34929,36 +34809,6 @@ "strip-ansi": "^7.0.1" } }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "bundled": true, - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "bundled": true, - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, "strip-ansi": { "version": "7.1.0", "bundled": true, @@ -34966,61 +34816,6 @@ "requires": { "ansi-regex": "^6.0.1" } - }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "bundled": true, - "dev": true - } - } - }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "bundled": true, - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "bundled": true, - "dev": true - }, - "string-width": { - "version": "4.2.3", - "bundled": true, - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } } } }, @@ -36881,6 +36676,7 @@ }, "string-width-cjs": { "version": "npm:string-width@4.2.3", + "bundled": true, "dev": true, "requires": { "emoji-regex": "^8.0.0", @@ -36898,6 +36694,7 @@ }, "strip-ansi-cjs": { "version": "npm:strip-ansi@6.0.1", + "bundled": true, "dev": true, "requires": { "ansi-regex": "^5.0.1" @@ -37092,6 +36889,7 @@ }, "wrap-ansi-cjs": { "version": "npm:wrap-ansi@7.0.0", + "bundled": true, "dev": true, "requires": { "ansi-styles": "^4.0.0", diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 8fcd81d9..452e2b4a 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -105,6 +105,7 @@ Datepicker.prototype.init = function () { Datepicker.prototype.initControls = function () { this.id = `datepicker-${this.$input.id}`; + // Create datepicker popup dialog this.$dialog = this.createDialog(); this.createCalendarHeaders(); @@ -242,7 +243,6 @@ Datepicker.prototype.toggleTemplate = function () { `; }; - /** * HTML template for calendar dialog * @@ -501,7 +501,7 @@ Datepicker.prototype.formattedDateFromDate = function (date) { }; /** - * Get a human readable date in the format Monday 2 March 2024 + * Get a huma readabel date in the format Monday 2 March 2024 * * @param {Date} - date to format * @return {string} From faad0f3bc9a685abdb14d42b0d5f669223027d6a Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Fri, 26 Jul 2024 13:53:51 +0100 Subject: [PATCH 40/58] refactor(date picker): small formatting nits --- src/moj/components/date-picker/date-picker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 452e2b4a..8fcd81d9 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -105,7 +105,6 @@ Datepicker.prototype.init = function () { Datepicker.prototype.initControls = function () { this.id = `datepicker-${this.$input.id}`; - // Create datepicker popup dialog this.$dialog = this.createDialog(); this.createCalendarHeaders(); @@ -243,6 +242,7 @@ Datepicker.prototype.toggleTemplate = function () { `; }; + /** * HTML template for calendar dialog * @@ -501,7 +501,7 @@ Datepicker.prototype.formattedDateFromDate = function (date) { }; /** - * Get a huma readabel date in the format Monday 2 March 2024 + * Get a human readable date in the format Monday 2 March 2024 * * @param {Date} - date to format * @return {string} From f0e687f482de9cea1c355d3dca132a81689b38d1 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Fri, 26 Jul 2024 14:17:56 +0100 Subject: [PATCH 41/58] docs(date picker): update date-picker component README within package --- src/moj/components/date-picker/README.md | 100 ++++++++--------------- 1 file changed, 34 insertions(+), 66 deletions(-) diff --git a/src/moj/components/date-picker/README.md b/src/moj/components/date-picker/README.md index 56c7a40b..3b912ac4 100644 --- a/src/moj/components/date-picker/README.md +++ b/src/moj/components/date-picker/README.md @@ -1,68 +1,36 @@ -## params +# Date picker + +- [Guidance](https://design-patterns.service.justice.gov.uk/components/date +picker) + +## Example + +``` +{{ mojDatePicker({ + id: "appointment-date", + name: "appointment-date" + label: "Appointment date" + hint: For example, 17/5/2024. +}) }} +``` + +## Arguments + +This component accepts the following arguments. + +| Name | Type | Required | Description | +| ------------ | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- | +| id | string | Yes | The ID of the input. | +| name | string | Yes | The name of the input, which is submitted with the form data. | +| value | string | No | Optional initial value of the input. | +| formGroup | object | No | Additional options for the form group containing the text input component. See [formGroup](#options-date-picker-form-group). | +| label | object | Yes | The label used by the text input component. See [GOV.UK text input documentation](https://design-system.service.gov.uk/components/text-input/) for label options. | +| hint | object | No | Can be used to add a hint to a text input component. See [GOV.UK text input documentation](https://design-system.service.gov.uk/components/text-input/) for hint options. | +| errorMessage | object | No | Can be used to add an error message to the text input component. The error message component will not display if you use a falsy value for `errorMessage`, for example `false` or `null`. See [GOV.UK text input documentation](https://design-system.service.gov.uk/components/text-input/) for errorMessage options. | +| minDate | string | No | Earliest date that can be selected (format dd/mm/yyyy) | +| maxDate | string | No | Latest date that can be selected (format dd/mm/yyyy) | +| exludedDates | string | No | String of pace separated dates that cannot be selected | +| excludedDays | string | No | String of space separated days of the week that cannot be selected | +| weekStartDay | string | No | Day of the week the calendar starts on. Either 'monday' or 'sunday'. Defaults to 'monday' | -id -classes (these are for the container) -name -value -minDate -maxDate -label { - html - text - classes - attributes -} -hint { - classes - attributes - html - text -} -errorMessage { - html - text - classes - attributes - visuallyHiddenText -} - - -## Questions / Issues - -### Input width -Possibly need a param to set the width (govuk-width-class) of the text input? -The css has classes for a --fixed class. -This is tricky in terms of API vs what exists. For consistency we could have an -`input` param, but currently all the attrs for the input are not namespaced (id, -value, name), but the non-prefixed `classes` param gets assigned to the container -not the input. -A 'breaking' change would be to use the `classes` param on the input, allowing -users to assign any of the govuk input width modifier classes. And then have a -`containerClasses` param for the container element. -A non-breaking solution would be to have a new param. e.g. `width` but this is -confusing if we expect a css class string. Could have `widthClass` or -`inputWidthClass`... none of these feel ideal though. - -### Header abbreviations -Currently the calendar headers contain abbreviated days (e.g. Mo, Tu) and have -an `abbr` attribute set with the full text. Technically this should be the -other way round, the `abbr` attribute should be for the short version. Need to -check screen reader handling here. Alternative would be an `aria-label` with -the full name. - -### Translations -Do we have a standardised way of doing this yet? -We need welsh days of the week and months - these are static and shouldn;t need -to be provided by the user, so I guess we should pick them up from the `lang` -attribute. - -### Width -Current component is 300px wide (280px + padding) -this matches old iPhone small screens -Figma component is 354px which is probably a more modern -smallest screen size... -Days in Figma are 44px wide - presume WCAG improvemnt? (nope 24*24 is minumum) - -### Label -Label takes the param 'isPageHeading' From 5dbf16d11b54edb0157e7a2e1199be1670045371 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Mon, 29 Jul 2024 14:24:01 +0100 Subject: [PATCH 42/58] feat(date picker): fixes following code review Adjustments in response to code review --- docs/_includes/arguments/date-picker.md | 2 +- docs/community/suggest-a-change.md | 3 +- .../components/date-picker/_date-picker.scss | 14 ++++--- src/moj/components/date-picker/date-picker.js | 41 ++++++++----------- 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/docs/_includes/arguments/date-picker.md b/docs/_includes/arguments/date-picker.md index 57f1a897..0789071e 100644 --- a/docs/_includes/arguments/date-picker.md +++ b/docs/_includes/arguments/date-picker.md @@ -9,7 +9,7 @@ | errorMessage | object | No | Can be used to add an error message to the text input component. The error message component will not display if you use a falsy value for `errorMessage`, for example `false` or `null`. See [GOV.UK text input documentation](https://design-system.service.gov.uk/components/text-input/) for errorMessage options. | | minDate | string | No | Earliest date that can be selected (format dd/mm/yyyy) | | maxDate | string | No | Latest date that can be selected (format dd/mm/yyyy) | -| exludedDates | string | No | String of pace separated dates that cannot be selected | +| exludedDates | string | No | String of space separated dates that cannot be selected | | excludedDays | string | No | String of space separated days of the week that cannot be selected | | weekStartDay | string | No | Day of the week the calendar starts on. Either 'monday' or 'sunday'. Defaults to 'monday' | diff --git a/docs/community/suggest-a-change.md b/docs/community/suggest-a-change.md index bdffcb96..1866c21b 100644 --- a/docs/community/suggest-a-change.md +++ b/docs/community/suggest-a-change.md @@ -1,9 +1,8 @@ --- +title: Suggest a Change layout: layouts/community.njk --- -# Suggest a change - To help improve the MoJ Design System, you can suggest changes to components and patterns. Useful suggestions may look like: diff --git a/src/moj/components/date-picker/_date-picker.scss b/src/moj/components/date-picker/_date-picker.scss index ff79fb68..0102920f 100644 --- a/src/moj/components/date-picker/_date-picker.scss +++ b/src/moj/components/date-picker/_date-picker.scss @@ -16,6 +16,10 @@ z-index: 2; } +.moj-datepicker-dialog--open { + display: block; +} + .moj-datepicker-dialog__header { position: relative; display: flex; @@ -67,7 +71,7 @@ } } -.moj-datepicker-button { +.moj-datepicker__button { @include govuk-font(16); background-color: transparent; outline: 2px solid rgba(0, 0, 0, 0); @@ -133,7 +137,7 @@ } } - &.current:not(:focus) { + &--current:not(:focus) { background-color: govuk-colour('blue'); color: govuk-colour('white'); outline-color: govuk-colour('blue'); @@ -142,7 +146,7 @@ } } - &.current[tabindex="-1"] { + &--current[tabindex="-1"] { background: transparent; color: currentColor; outline-color: transparent; @@ -151,11 +155,11 @@ } } - &.today { + &--today { border: 2px solid govuk-colour('black'); } - &.selected:not(:focus) { + &--selected:not(:focus) { background-color: govuk-colour('blue'); color: govuk-colour('white'); diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 8fcd81d9..4d3a1aef 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -87,11 +87,15 @@ function Datepicker($module, config) { down: 40, }; + this.buttonClass = 'moj-datepicker__button' + this.selectedDayButtonClass = 'moj-datepicker__button--selected' + this.currentDayButtonClass = 'moj-datepicker__button--current' + this.todayButtonClass = 'moj-datepicker__button--today' + this.$module = $module; this.$input = $module.querySelector(".moj-js-datepicker-input"); } - Datepicker.prototype.init = function () { // Check that required elements are present if (!this.$input) { @@ -252,7 +256,7 @@ Datepicker.prototype.toggleTemplate = function () { Datepicker.prototype.dialogTemplate = function (titleId) { return `
- - -
- + From 68d18f42cc86d471d900d71602315ccbc9406b4d Mon Sep 17 00:00:00 2001 From: helennickols Date: Tue, 30 Jul 2024 13:24:17 +0100 Subject: [PATCH 46/58] docs(date picker component): update content Correcting typo --- docs/components/date-picker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index 34cb5ff1..8b96ee9f 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -59,7 +59,7 @@ You can exclude (or disable) specific dates and days of the week from the date p Excluded dates have the correct colour contrast ratio with the date text and calendar background. This is WCAG 2.2 compliant. However, these dates may be harder to view for users with low vision or colour blindness, so there’s also a strikethrough. Numbers with a strikethrough can be harder for people with dyscalculia to read. -If there are not many available dates, users will have to navigate a lot to find one. Consider listing these dates with radio buttons instead. options, such as appointments. +If there are not many available dates, users will have to navigate a lot to find one. Consider listing these dates with radio buttons instead. ### Error messages From 6f03b16087ce57ec4d6b36fc667eff4cf973b7bb Mon Sep 17 00:00:00 2001 From: helennickols Date: Tue, 30 Jul 2024 14:14:17 +0100 Subject: [PATCH 47/58] docs(date picker component): update content Remove greyed out content which is a comment. --- docs/components/date-picker.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index 8b96ee9f..ae1756ec 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -47,12 +47,6 @@ The date picker hint text is set to 17/5/2024. This can be changed to a more hel ### Excluding dates - - You can exclude (or disable) specific dates and days of the week from the date picker, for example bank holidays or every weekend. {% example "/examples/date-picker-excluded-dates", 590 %} From f4557f317b65ed898e5569d15974ce8f52a309a7 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 31 Jul 2024 11:20:36 +0100 Subject: [PATCH 48/58] refactor(date picker): update sass to follow GOV.UK BEM conventions Updates the CSS classes to have only one block root per component. Also updates to follw GOV.UK recomended convention of preferring sass variables instead of colour functions where available. --- .../components/date-picker/_date-picker.scss | 76 ++++++++++--------- src/moj/components/date-picker/date-picker.js | 22 +++--- 2 files changed, 51 insertions(+), 47 deletions(-) diff --git a/src/moj/components/date-picker/_date-picker.scss b/src/moj/components/date-picker/_date-picker.scss index 0102920f..c218e57b 100644 --- a/src/moj/components/date-picker/_date-picker.scss +++ b/src/moj/components/date-picker/_date-picker.scss @@ -1,26 +1,30 @@ +// Custom colour required for passing WCAG 2.2 AA contrast text/background and +// background/surrounding +$moj-datepicker-mid-grey: #949494; + .moj-datepicker { position: relative; @include govuk-font(16); } -.moj-datepicker-dialog { +.moj-datepicker__dialog { display: none; position: absolute; top: 0; min-width: 280px; padding: govuk-spacing(4); - outline: 2px solid govuk-colour('black'); + outline: 2px solid $govuk-text-colour; outline-offset: -2px; background-color: govuk-colour('white'); transition: background-color 0.2s, outline-color 0.2s; z-index: 2; } -.moj-datepicker-dialog--open { +.moj-datepicker__dialog--open { display: block; } -.moj-datepicker-dialog__header { +.moj-datepicker__dialog-header { position: relative; display: flex; align-items: center; @@ -28,19 +32,19 @@ margin-bottom: govuk-spacing(2); } -.moj-datepicker-dialog__title { +.moj-datepicker__dialog-title { @include govuk-font(16); font-weight: bold; margin-top: 0; margin-bottom: 0; } -.moj-datepicker-dialog__navbuttons { +.moj-datepicker__dialog-navbuttons { display: flex; align-items: center; } -.moj-datepicker-calendar { +.moj-datepicker__calendar { border-collapse: collapse; margin-bottom: govuk-spacing(4); @@ -63,7 +67,7 @@ } -.moj-datepicker-dialog > .govuk-button-group { +.moj-datepicker__dialog > .govuk-button-group { margin-bottom: 0; > * { @@ -104,14 +108,14 @@ &[aria-disabled="true"], &[aria-disabled="true"]:hover { background-color: govuk-colour('light-grey'); - color: govuk-colour('black'); + color: $govuk-text-colour; cursor: not-allowed; text-decoration: line-through; } &:hover { color: $govuk-text-colour; - background-color: #949494; + background-color: $moj-datepicker-mid-grey; text-decoration: none; -webkit-box-decoration-break: clone; box-decoration-break: clone; @@ -119,30 +123,30 @@ } &:focus { - color: govuk-colour('black'); - background-color: govuk-colour('yellow'); + color: $govuk-text-colour; + background-color: $govuk-focus-colour; outline-color: transparent; -webkit-box-decoration-break: clone; box-decoration-break: clone; &:after { - background-color: govuk-colour('black'); + background-color: $govuk-text-colour; } } &:focus:hover { - background-color: #949494; - outline-color: govuk-colour('yellow'); + background-color: $moj-datepicker-mid-grey; + outline-color: $govuk-focus-colour; &:after { background-color: transparent; } } &--current:not(:focus) { - background-color: govuk-colour('blue'); + background-color: $govuk-link-colour; color: govuk-colour('white'); - outline-color: govuk-colour('blue'); + outline-color: $govuk-link-colour; &:after { - background-color: govuk-colour('blue'); + background-color: $govuk-link-colour; } } @@ -156,21 +160,21 @@ } &--today { - border: 2px solid govuk-colour('black'); + border: 2px solid $govuk-text-colour; } &--selected:not(:focus) { - background-color: govuk-colour('blue'); + background-color: $govuk-link-colour; color: govuk-colour('white'); &:after { - background-color: govuk-colour('blue'); + background-color: $govuk-link-colour; } &:hover { - outline-color: govuk-colour('blue'); - background-color: #949494; - color: govuk-colour('black'); + outline-color: $govuk-link-colour; + background-color: $moj-datepicker-mid-grey; + color: $govuk-text-colour; &:after { background-color: transparent; @@ -253,13 +257,13 @@ @media (min-width: 768px) { - .moj-datepicker-dialog { + .moj-datepicker__dialog { width: auto; } } -.moj-datepicker-toggle { - background-color: govuk-colour('black'); +.moj-datepicker__toggle { + background-color: $govuk-text-colour; color: govuk-colour('white'); outline: 3px solid rgba(0, 0, 0, 0); outline-offset: -3px; @@ -270,20 +274,20 @@ cursor: pointer; &:focus { - background-color: govuk-colour('yellow'); - color: govuk-colour('black'); - border-bottom: 4px solid govuk-colour('black'); + background-color: $govuk-focus-colour; + color: $govuk-text-colour; + border-bottom: 4px solid $govuk-text-colour; } &:hover { - background-color: #949494; - color: govuk-colour('black'); - border-bottom: 4px solid #949494; + background-color: $moj-datepicker-mid-grey; + color: $govuk-text-colour; + border-bottom: 4px solid $moj-datepicker-mid-grey; } &:focus:hover { - background-color: #949494; - color: govuk-colour('black'); - border-bottom: 4px solid govuk-colour('black'); + background-color: $moj-datepicker-mid-grey; + color: $govuk-text-colour; + border-bottom: 4px solid $govuk-text-colour; } } diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 4d3a1aef..328e4a55 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -199,7 +199,7 @@ Datepicker.prototype.createDialog = function () { const $dialog = document.createElement("div"); $dialog.id = this.id; - $dialog.setAttribute("class", "moj-datepicker-dialog datepickerDialog"); + $dialog.setAttribute("class", "moj-datepicker__dialog datepickerDialog"); $dialog.setAttribute("role", "dialog"); $dialog.setAttribute("aria-modal", "true"); $dialog.setAttribute("aria-labelledby", titleId); @@ -232,7 +232,7 @@ Datepicker.prototype.createCalendar = function () { }; Datepicker.prototype.toggleTemplate = function () { - return ` -

June 2020

+

June 2020

-
+
-
The date is disabledThe date is excluded Select an available date from the calendar
+
@@ -574,7 +574,7 @@ Datepicker.prototype.setCurrentDate = function (focus = true) { this.calendarDays.forEach((calendarDay) => { calendarDay.button.classList.add("moj-datepicker__button"); - calendarDay.button.classList.add("moj-datepicker-calendar__day"); + calendarDay.button.classList.add("moj-datepicker__calendar-day"); calendarDay.button.setAttribute("tabindex", -1); calendarDay.button.classList.remove(this.selectedDayButtonClass); const calendarDayDate = calendarDay.date; @@ -643,7 +643,7 @@ Datepicker.prototype.selectDate = function (date) { }; Datepicker.prototype.isOpen = function () { - return this.$dialog.classList.contains("moj-datepicker-dialog--open"); + return this.$dialog.classList.contains("moj-datepicker__dialog--open"); }; Datepicker.prototype.toggleDialog = function (event) { @@ -657,7 +657,7 @@ Datepicker.prototype.toggleDialog = function (event) { }; Datepicker.prototype.openDialog = function () { - this.$dialog.classList.add("moj-datepicker-dialog--open"); + this.$dialog.classList.add("moj-datepicker__dialog--open"); this.$calendarButton.setAttribute("aria-expanded", "true"); // position the dialog @@ -678,7 +678,7 @@ Datepicker.prototype.openDialog = function () { }; Datepicker.prototype.closeDialog = function () { - this.$dialog.classList.remove("moj-datepicker-dialog--open"); + this.$dialog.classList.remove("moj-datepicker__dialog--open"); this.$calendarButton.setAttribute("aria-expanded", "false"); this.$calendarButton.focus(); }; From 8a214d052ff1c80e155607e6b435bbfb49559f77 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 31 Jul 2024 11:43:50 +0100 Subject: [PATCH 49/58] docs(date picker): add date picker to what's new section on homepage --- docs/_includes/layouts/home.njk | 17 +++++++++++++++-- docs/index.md | 2 -- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/_includes/layouts/home.njk b/docs/_includes/layouts/home.njk index f68bb7ed..92c41ed4 100644 --- a/docs/_includes/layouts/home.njk +++ b/docs/_includes/layouts/home.njk @@ -24,6 +24,17 @@ +
+
+
+

What’s new

+

29 July 2024: We’ve released a new date picker component to help users select a date quickly and easily.

+

Sign up to get emails about the MoJ Design System.

+
+
+
+
+
@@ -32,7 +43,7 @@

Components

Save time with reusable, accessible components for forms, navigation, panels, tables and more.

- Find a component + Find a component

@@ -44,13 +55,15 @@

Patterns

Help users complete common tasks like uploading files, filtering lists, and getting help.

- Find a pattern + Find a pattern

+
+ {{ content | safe }}
diff --git a/docs/index.md b/docs/index.md index 17042dc9..6b6bc623 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,8 +3,6 @@ layout: layouts/home.njk title: Design, build, and deliver accessible and consistent services --- ---- - ## Contribute to the MoJ Design System Anyone can contribute to the MoJ Design System by proposing a new style, component, or pattern. From 088cc37a3ed023e2acd82b4bd9b06f620887792a Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 31 Jul 2024 12:49:51 +0100 Subject: [PATCH 50/58] refactor(date picker): remove need for duplicate date regex The regex for a date was duplicated in the code. It wasn't necessary to wrap getting the date in the input in a conditional due to the fact that fomattedDateFromString falls back to todays date meaning if there is no date in the input or the input contains an invalid date, it will fall back to setting the currentDate for the calendar to today, which is what is required. --- src/moj/components/date-picker/date-picker.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 328e4a55..b3fb6520 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -583,6 +583,7 @@ Datepicker.prototype.setCurrentDate = function (focus = true) { const today = new Date(); today.setHours(0, 0, 0, 0); + if ( calendarDayDate.getTime() === currentDate.getTime() /* && !calendarDay.button.disabled */ @@ -668,10 +669,9 @@ Datepicker.prototype.openDialog = function () { this.$dialog.style.top = `${this.$input.offsetHeight + 3}px`; // get the date from the input element - if (this.$input.value.match(/^(\d{1,2})([-/,. ])(\d{1,2})[-/,. ](\d{4})$/)) { - this.inputDate = this.formattedDateFromString(this.$input.value); - this.currentDate = this.inputDate; - } + this.inputDate = this.formattedDateFromString(this.$input.value); + this.currentDate = this.inputDate; + this.currentDate.setHours(0, 0, 0, 0); this.updateCalendar(); this.setCurrentDate(); From b5308fd4caeabcda4a698f16a8c36c432327423e Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 31 Jul 2024 13:04:49 +0100 Subject: [PATCH 51/58] fix(date picker): move escape key event listener to dialog element The 'esc' keydown event listener was attached to the calendarDayButton instance(s) meaning that escape would only close the modal if one of the calendar days was focused. Escape should clode the dialog wherever you are focused within it. Moving the listener onto the parent element fixes this bug. --- src/moj/components/date-picker/date-picker.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index b3fb6520..ea55f9c1 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -186,6 +186,14 @@ Datepicker.prototype.initControls = function () { this.toggleDialog(event), ); + this.$dialog.addEventListener("keydown", (event) => { + if (event.keyCode == this.keycodes.esc) { + this.closeDialog(); + event.preventDefault(); + event.stopPropagation(); + } + }) + document.body.addEventListener("mouseup", (event) => this.backgroundClick(event), ); @@ -912,9 +920,6 @@ DSCalendarDay.prototype.keyPress = function (event) { ? this.picker.focusNextYear(event) : this.picker.focusNextMonth(event); break; - case this.picker.keycodes.esc: - this.picker.closeDialog(); - break; default: calendarNavKey = false; break; From f63509bcc00fc02167e8d4f4beaf4bd61303a951 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 31 Jul 2024 13:15:28 +0100 Subject: [PATCH 52/58] refactor(date picker): refactor event.keycode to event.key --- src/moj/components/date-picker/date-picker.js | 37 ++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index ea55f9c1..f4d25cda 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -74,19 +74,6 @@ function Datepicker($module, config) { this.excludedDates = []; this.excludedDays = []; - this.keycodes = { - tab: 9, - esc: 27, - pageup: 33, - pagedown: 34, - end: 35, - home: 36, - left: 37, - up: 38, - right: 39, - down: 40, - }; - this.buttonClass = 'moj-datepicker__button' this.selectedDayButtonClass = 'moj-datepicker__button--selected' this.currentDayButtonClass = 'moj-datepicker__button--current' @@ -187,7 +174,7 @@ Datepicker.prototype.initControls = function () { ); this.$dialog.addEventListener("keydown", (event) => { - if (event.keyCode == this.keycodes.esc) { + if (event.key == 'Escape') { this.closeDialog(); event.preventDefault(); event.stopPropagation(); @@ -535,14 +522,14 @@ Datepicker.prototype.backgroundClick = function (event) { }; Datepicker.prototype.firstButtonKeydown = function (event) { - if (event.keyCode === this.keycodes.tab && event.shiftKey) { + if (event.key === 'Tab' && event.shiftKey) { this.$lastButtonInDialog.focus(); event.preventDefault(); } }; Datepicker.prototype.lastButtonKeydown = function (event) { - if (event.keyCode === this.keycodes.tab && !event.shiftKey) { + if (event.key === 'Tab' && !event.shiftKey) { this.$firstButtonInDialog.focus(); event.preventDefault(); } @@ -889,32 +876,32 @@ DSCalendarDay.prototype.click = function (event) { DSCalendarDay.prototype.keyPress = function (event) { let calendarNavKey = true; - switch (event.keyCode) { - case this.picker.keycodes.left: + switch (event.key) { + case 'ArrowLeft': this.picker.focusPreviousDay(); break; - case this.picker.keycodes.right: + case 'ArrowRight': this.picker.focusNextDay(); break; - case this.picker.keycodes.up: + case 'ArrowUp': this.picker.focusPreviousWeek(); break; - case this.picker.keycodes.down: + case 'ArrowDown': this.picker.focusNextWeek(); break; - case this.picker.keycodes.home: + case 'Home': this.picker.focusFirstDayOfWeek(); break; - case this.picker.keycodes.end: + case 'End': this.picker.focusLastDayOfWeek(); break; - case this.picker.keycodes.pageup: + case 'PageUp': // eslint-disable-next-line no-unused-expressions event.shiftKey ? this.picker.focusPreviousYear(event) : this.picker.focusPreviousMonth(event); break; - case this.picker.keycodes.pagedown: + case 'PageDown': // eslint-disable-next-line no-unused-expressions event.shiftKey ? this.picker.focusNextYear(event) From 5346361166dedf55997106d2d14d0c0fb71e5e84 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 31 Jul 2024 13:26:53 +0100 Subject: [PATCH 53/58] fix(date picker): fixes and changes based on code review --- src/moj/components/date-picker/date-picker.js | 94 +++++++++---------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index f4d25cda..e91e0501 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -74,10 +74,10 @@ function Datepicker($module, config) { this.excludedDates = []; this.excludedDays = []; - this.buttonClass = 'moj-datepicker__button' - this.selectedDayButtonClass = 'moj-datepicker__button--selected' - this.currentDayButtonClass = 'moj-datepicker__button--current' - this.todayButtonClass = 'moj-datepicker__button--today' + this.buttonClass = "moj-datepicker__button"; + this.selectedDayButtonClass = "moj-datepicker__button--selected"; + this.currentDayButtonClass = "moj-datepicker__button--current"; + this.todayButtonClass = "moj-datepicker__button--today"; this.$module = $module; this.$input = $module.querySelector(".moj-js-datepicker-input"); @@ -174,12 +174,12 @@ Datepicker.prototype.initControls = function () { ); this.$dialog.addEventListener("keydown", (event) => { - if (event.key == 'Escape') { + if (event.key == "Escape") { this.closeDialog(); event.preventDefault(); event.stopPropagation(); } - }) + }); document.body.addEventListener("mouseup", (event) => this.backgroundClick(event), @@ -250,55 +250,55 @@ Datepicker.prototype.toggleTemplate = function () { */ Datepicker.prototype.dialogTemplate = function (titleId) { return `
-
- + - -
+ + +
-

June 2020

+

June 2020

-
- + - -
- + + + -
- - - +
+ + + - -
+ + -
- - -
`; +
+ + +
`; }; Datepicker.prototype.createCalendarHeaders = function () { @@ -408,7 +408,7 @@ Datepicker.prototype.setLeadingZeros = function () { if (this.config.leadingZeros.toLowerCase() === "true") { this.config.leadingZeros = true; } - if (this.config.leadingzeros.toLowerCase() === "false") { + if (this.config.leadingZeros.toLowerCase() === "false") { this.config.leadingZeros = false; } } @@ -468,17 +468,16 @@ Datepicker.prototype.formattedDateFromString = function ( ) { let formattedDate = null; // Accepts d/m/yyyy and dd/mm/yyyy - const dateFormatPattern = /(\d{1,2})([-/,. ])(\d{1,2})[-/,. ](\d{4})/; + const dateFormatPattern = /(\d{1,2})([-/,. ])(\d{1,2})\2(\d{4})/; if (!dateFormatPattern.test(dateString)) return fallback; const match = dateString.match(dateFormatPattern); - const separator = match[2]; const day = match[1]; const month = match[3]; const year = match[4]; - formattedDate = new Date(`${month}${separator}${day}${separator}${year}`); + formattedDate = new Date(`${month}-${day}-${year}`); if (formattedDate instanceof Date && !isNaN(formattedDate)) { return formattedDate; } @@ -522,14 +521,14 @@ Datepicker.prototype.backgroundClick = function (event) { }; Datepicker.prototype.firstButtonKeydown = function (event) { - if (event.key === 'Tab' && event.shiftKey) { + if (event.key === "Tab" && event.shiftKey) { this.$lastButtonInDialog.focus(); event.preventDefault(); } }; Datepicker.prototype.lastButtonKeydown = function (event) { - if (event.key === 'Tab' && !event.shiftKey) { + if (event.key === "Tab" && !event.shiftKey) { this.$firstButtonInDialog.focus(); event.preventDefault(); } @@ -578,7 +577,6 @@ Datepicker.prototype.setCurrentDate = function (focus = true) { const today = new Date(); today.setHours(0, 0, 0, 0); - if ( calendarDayDate.getTime() === currentDate.getTime() /* && !calendarDay.button.disabled */ @@ -877,31 +875,31 @@ DSCalendarDay.prototype.keyPress = function (event) { let calendarNavKey = true; switch (event.key) { - case 'ArrowLeft': + case "ArrowLeft": this.picker.focusPreviousDay(); break; - case 'ArrowRight': + case "ArrowRight": this.picker.focusNextDay(); break; - case 'ArrowUp': + case "ArrowUp": this.picker.focusPreviousWeek(); break; - case 'ArrowDown': + case "ArrowDown": this.picker.focusNextWeek(); break; - case 'Home': + case "Home": this.picker.focusFirstDayOfWeek(); break; - case 'End': + case "End": this.picker.focusLastDayOfWeek(); break; - case 'PageUp': + case "PageUp": // eslint-disable-next-line no-unused-expressions event.shiftKey ? this.picker.focusPreviousYear(event) : this.picker.focusPreviousMonth(event); break; - case 'PageDown': + case "PageDown": // eslint-disable-next-line no-unused-expressions event.shiftKey ? this.picker.focusNextYear(event) From be4ff59dbb653a5e72f57e042291444e5dc18669 Mon Sep 17 00:00:00 2001 From: helennickols Date: Wed, 31 Jul 2024 13:27:24 +0100 Subject: [PATCH 54/58] docs(date picker component): update content New content about server side validation for text inputs. --- docs/components/date-picker.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index ae1756ec..a20a4456 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -51,6 +51,8 @@ You can exclude (or disable) specific dates and days of the week from the date p {% example "/examples/date-picker-excluded-dates", 590 %} +You need to add server-side validation for when users enter an unavailable date directly into the text field (rather than in the calendar). This will show them an error message. + Excluded dates have the correct colour contrast ratio with the date text and calendar background. This is WCAG 2.2 compliant. However, these dates may be harder to view for users with low vision or colour blindness, so there’s also a strikethrough. Numbers with a strikethrough can be harder for people with dyscalculia to read. If there are not many available dates, users will have to navigate a lot to find one. Consider listing these dates with radio buttons instead. From 7c746f57a4b015217ccc0ebe496a478576dfee34 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 31 Jul 2024 13:31:37 +0100 Subject: [PATCH 55/58] refactor(date picker): remove unused css class on the dialog --- src/moj/components/date-picker/date-picker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index e91e0501..173d1fde 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -194,7 +194,7 @@ Datepicker.prototype.createDialog = function () { const $dialog = document.createElement("div"); $dialog.id = this.id; - $dialog.setAttribute("class", "moj-datepicker__dialog datepickerDialog"); + $dialog.setAttribute("class", "moj-datepicker__dialog"); $dialog.setAttribute("role", "dialog"); $dialog.setAttribute("aria-modal", "true"); $dialog.setAttribute("aria-labelledby", titleId); From 9c15a4f3f73b113407de77f86b2cad9fcb7ed732 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 31 Jul 2024 14:33:38 +0100 Subject: [PATCH 56/58] docs(date picker): add figma link to all examples --- docs/examples/date-picker-error/index.njk | 1 + docs/examples/date-picker-excluded-dates/index.njk | 1 + docs/examples/date-picker-excluded-days/index.njk | 1 + docs/examples/date-picker-min-max/index.njk | 1 + 4 files changed, 4 insertions(+) diff --git a/docs/examples/date-picker-error/index.njk b/docs/examples/date-picker-error/index.njk index 8b959232..c45ad008 100644 --- a/docs/examples/date-picker-error/index.njk +++ b/docs/examples/date-picker-error/index.njk @@ -2,6 +2,7 @@ layout: layouts/example.njk title: Date Picker (example) arguments: date-picker +figma_link: https://www.figma.com/design/N2xqOFkyehXwcD9DxU1gEq/MoJ-Figma-Kit?node-id=792-861&t=6DfPOX7RAnjrVE0j-0 --- {%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} diff --git a/docs/examples/date-picker-excluded-dates/index.njk b/docs/examples/date-picker-excluded-dates/index.njk index 0b12090c..1df7f969 100644 --- a/docs/examples/date-picker-excluded-dates/index.njk +++ b/docs/examples/date-picker-excluded-dates/index.njk @@ -1,6 +1,7 @@ --- layout: layouts/example.njk title: Date Picker Excluded Dates (example) +figma_link: https://www.figma.com/design/N2xqOFkyehXwcD9DxU1gEq/MoJ-Figma-Kit?node-id=792-861&t=6DfPOX7RAnjrVE0j-0 --- {%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} diff --git a/docs/examples/date-picker-excluded-days/index.njk b/docs/examples/date-picker-excluded-days/index.njk index 8b86c2dc..9c04a9c3 100644 --- a/docs/examples/date-picker-excluded-days/index.njk +++ b/docs/examples/date-picker-excluded-days/index.njk @@ -1,6 +1,7 @@ --- layout: layouts/example.njk title: Date Picker Excluded Days (example) +figma_link: https://www.figma.com/design/N2xqOFkyehXwcD9DxU1gEq/MoJ-Figma-Kit?node-id=792-861&t=6DfPOX7RAnjrVE0j-0 --- {%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} diff --git a/docs/examples/date-picker-min-max/index.njk b/docs/examples/date-picker-min-max/index.njk index 4a88ae51..cf34768a 100644 --- a/docs/examples/date-picker-min-max/index.njk +++ b/docs/examples/date-picker-min-max/index.njk @@ -1,6 +1,7 @@ --- layout: layouts/example.njk title: Date Picker Min and Max Date (example) +figma_link: https://www.figma.com/design/N2xqOFkyehXwcD9DxU1gEq/MoJ-Figma-Kit?node-id=792-861&t=6DfPOX7RAnjrVE0j-0 --- {%- from "moj/components/date-picker/macro.njk" import mojDatePicker -%} From 4d96d23a32917530917659f91b99033035bfc641 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 31 Jul 2024 14:34:10 +0100 Subject: [PATCH 57/58] fix(date picker): fix mindate and maxdate functionality --- src/moj/components/date-picker/date-picker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 173d1fde..48fc6976 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -337,7 +337,7 @@ Datepicker.prototype.setOptions = function () { Datepicker.prototype.setMinAndMaxDatesOnCalendar = function () { if (this.config.minDate) { this.minDate = this.formattedDateFromString( - this.$module.dataset.mindate, + this.config.minDate, null, ); if (this.minDate && this.currentDate < this.minDate) { @@ -347,7 +347,7 @@ Datepicker.prototype.setMinAndMaxDatesOnCalendar = function () { if (this.config.maxDate) { this.maxDate = this.formattedDateFromString( - this.$module.dataset.maxdate, + this.config.maxDate, null, ); if (this.maxDate && this.currentDate > this.maxDate) { From ce51f88a9cf928a4de7e2efeb142bbdec68287c5 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 31 Jul 2024 15:43:40 +0100 Subject: [PATCH 58/58] docs(date picker): update excluded dates example to also include min and max date --- docs/examples/date-picker-excluded-dates/index.njk | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/examples/date-picker-excluded-dates/index.njk b/docs/examples/date-picker-excluded-dates/index.njk index 1df7f969..1f0e1444 100644 --- a/docs/examples/date-picker-excluded-dates/index.njk +++ b/docs/examples/date-picker-excluded-dates/index.njk @@ -16,6 +16,8 @@ figma_link: https://www.figma.com/design/N2xqOFkyehXwcD9DxU1gEq/MoJ-Figma-Kit?no text: "For example, 17/5/2024." }, value: "10/04/2025", + minDate: "01/04/2025", + maxDate: "30/04/2025", excludedDates: "02/04/2025 18/04/2025 21/04/2025", excludedDays: "saturday sunday" }) }}