From 09f0a0a1e92bbe37d51ad4c2d7f360ca4804fe66 Mon Sep 17 00:00:00 2001
From: Lauren Hitchon
Date: Wed, 28 Feb 2024 16:45:57 +1100
Subject: [PATCH] feature/date-picker (#385)
* Create base files
* Add base guidance content
* Create date picker base functionality
* Add multiple input setDate functionality
* Add accessibility features. Cancel and accept buttons and keyboard navigation styles
* Add disable dates functionality
* Add min date and max date functionality
* Remove comments and refactor disabled dates functionality
* Add autocomplete attributes
* Add theming and inverted examples
* Add card carousel theming and inverted examples
* Add links from date input to date picker
* Add date picker and input inverted styles
---
src/components/_all.scss | 1 +
src/components/card-carousel/_carousel.scss | 45 +-
src/components/card-carousel/blank.hbs | 11 +
src/components/card-carousel/index.hbs | 1 +
src/components/card-carousel/theme.hbs | 33 +
src/components/date-input/_date-input.hbs | 6 +-
src/components/date-input/_guidance.hbs | 4 +
src/components/date-input/blank.hbs | 41 +-
src/components/date-input/index.hbs | 4 +-
src/components/date-input/theme.hbs | 81 +++
src/components/date-picker/_date-picker.hbs | 138 +++++
src/components/date-picker/_date-picker.scss | 324 ++++++++++
src/components/date-picker/_guidance.hbs | 226 +++++++
src/components/date-picker/blank.hbs | 37 ++
src/components/date-picker/date-picker.js | 578 ++++++++++++++++++
src/components/date-picker/index.hbs | 30 +
.../date-picker/json/date-picker.json | 1 +
src/components/date-picker/theme.hbs | 39 ++
src/components/form/_form.scss | 68 ++-
src/components/form/blank.hbs | 126 ++--
src/components/form/theme.hbs | 124 ++--
src/docs/content/design/theming.hbs | 5 +-
src/global/scss/helpers/_visibility.scss | 2 -
src/main.js | 10 +-
src/main.scss | 1 +
25 files changed, 1772 insertions(+), 164 deletions(-)
create mode 100644 src/components/card-carousel/theme.hbs
create mode 100644 src/components/date-input/theme.hbs
create mode 100644 src/components/date-picker/_date-picker.hbs
create mode 100644 src/components/date-picker/_date-picker.scss
create mode 100644 src/components/date-picker/_guidance.hbs
create mode 100644 src/components/date-picker/blank.hbs
create mode 100644 src/components/date-picker/date-picker.js
create mode 100644 src/components/date-picker/index.hbs
create mode 100644 src/components/date-picker/json/date-picker.json
create mode 100644 src/components/date-picker/theme.hbs
diff --git a/src/components/_all.scss b/src/components/_all.scss
index dd9d5808..6c3f362f 100644
--- a/src/components/_all.scss
+++ b/src/components/_all.scss
@@ -7,6 +7,7 @@
@import 'card-carousel/carousel';
@import 'content-block/content-block';
@import 'date-input/date-input';
+@import 'date-picker/date-picker';
@import 'dialog/dialog';
@import 'file-upload/file-upload';
@import 'filters/filters';
diff --git a/src/components/card-carousel/_carousel.scss b/src/components/card-carousel/_carousel.scss
index f279fc83..a55fa540 100644
--- a/src/components/card-carousel/_carousel.scss
+++ b/src/components/card-carousel/_carousel.scss
@@ -1,3 +1,5 @@
+/* stylelint-disable max-nesting-depth */
+
.nsw-carousel {
--carousel-item-auto-size: 300px;
position: relative;
@@ -174,6 +176,28 @@
display: block;
margin: auto;
}
+
+ .nsw-section--invert & {
+ border: 2px solid var(--nsw-white);
+
+ &:hover {
+ @include nsw-hover-light();
+
+ .nsw-icon {
+ color: var(--nsw-white);
+ }
+ }
+
+ &:focus {
+ @include nsw-focus($color: var(--nsw-focus-light));
+ }
+
+ .nsw-icon {
+ &:hover {
+ color: var(--nsw-white);
+ }
+ }
+ }
}
&__navigation {
@@ -206,8 +230,8 @@
.nsw-carousel__nav-item button {
background-color: transparent;
- width: 1.5rem;
- height: 1.5rem;
+ width: 1.6rem;
+ height: 1.6rem;
color: var(--nsw-brand-dark);
font-size: 12px;
border-radius: var(--nsw-border-radius);
@@ -218,12 +242,27 @@
@include nsw-focus(false);
outline-offset: 2px;
}
+
+ .nsw-section--invert & {
+ background-color: var(--nsw-white);
+ border: 1px solid var(--nsw-white);
+
+ &:focus {
+ @include nsw-focus($color: var(--nsw-focus-light));
+ outline-offset: 2px;
+ }
+ }
}
.nsw-carousel__nav-item--selected {
button {
background-color: var(--nsw-brand-dark);
color: var(--nsw-white);
+
+ .nsw-section--invert & {
+ background-color: transparent;
+ border: 2px solid var(--nsw-white);
+ }
}
}
}
@@ -236,7 +275,7 @@
height: 1.2em;
padding: 0;
border-radius: 50%;
- border: 1px solid var(--nsw-brand-dark);
+ border: 2px solid var(--nsw-brand-dark);
transition: all 300ms ease-in-out;
cursor: pointer;
line-height: 0;
diff --git a/src/components/card-carousel/blank.hbs b/src/components/card-carousel/blank.hbs
index 48ad7159..016b6587 100644
--- a/src/components/card-carousel/blank.hbs
+++ b/src/components/card-carousel/blank.hbs
@@ -19,3 +19,14 @@ page: true
{{/_layout-container}}
+{{#>_layout-container brand-dark="true" invert="true"}}
+5 cards
+{{>_carousel model.carousel-five default=true loop=true}}
+
+9 cards
+{{>_carousel model.carousel-nine default=true loop=true}}
+
+Pagination
+{{>_carousel model.carousel-nine pagination=true default=true}}
+{{/_layout-container}}
+
diff --git a/src/components/card-carousel/index.hbs b/src/components/card-carousel/index.hbs
index 49bbc705..18c6f79b 100644
--- a/src/components/card-carousel/index.hbs
+++ b/src/components/card-carousel/index.hbs
@@ -4,6 +4,7 @@ width: wide
tabs: true
directory: card-carousel
intro: A card carousel displays multiple related cards horizontally, allowing users to swipe or navigate through the content.
+theme: true
model:
carousel-five: ../../components/card-carousel/json/carousel-five.json
carousel-nine: ../../components/card-carousel/json/carousel-nine.json
diff --git a/src/components/card-carousel/theme.hbs b/src/components/card-carousel/theme.hbs
new file mode 100644
index 00000000..99a6757a
--- /dev/null
+++ b/src/components/card-carousel/theme.hbs
@@ -0,0 +1,33 @@
+---
+title: Card carousel
+width: wide
+model:
+ carousel-five: ../../components/card-carousel/json/carousel-five.json
+ carousel-nine: ../../components/card-carousel/json/carousel-nine.json
+page: true
+---
+
+{{#>_theme}}
+{{#>_layout-container}}
+5 cards
+{{>_carousel model.carousel-five default=true loop=true}}
+
+9 cards
+{{>_carousel model.carousel-nine default=true loop=true}}
+
+Pagination
+{{>_carousel model.carousel-nine pagination=true default=true}}
+
+{{/_layout-container}}
+
+{{#>_layout-container brand-dark="true" invert="true"}}
+5 cards
+{{>_carousel model.carousel-five default=true loop=true}}
+
+9 cards
+{{>_carousel model.carousel-nine default=true loop=true}}
+
+Pagination
+{{>_carousel model.carousel-nine pagination=true default=true}}
+{{/_layout-container}}
+{{/_theme}}
diff --git a/src/components/date-input/_date-input.hbs b/src/components/date-input/_date-input.hbs
index 96aa834b..39719759 100644
--- a/src/components/date-input/_date-input.hbs
+++ b/src/components/date-input/_date-input.hbs
@@ -6,15 +6,15 @@
diff --git a/src/components/date-input/_guidance.hbs b/src/components/date-input/_guidance.hbs
index 6a251b14..632ebd1c 100644
--- a/src/components/date-input/_guidance.hbs
+++ b/src/components/date-input/_guidance.hbs
@@ -31,6 +31,10 @@ layout: blank-layout.hbs
use in instances where only part of a date is needed, like a month or a year without a specific day.
+Helping users pick a date
+
+Users might need to pick a date from a selection, for example, to book an appointment. To do this, you can present dates in a calendar format using a date picker .
+
How this component works
The three fields in the date input component are grouped together in a fieldset
with a legend
that describes them, as shown in the examples on this page. The legend is usually a question, such as 'What is your date of birth?'.
diff --git a/src/components/date-input/blank.hbs b/src/components/date-input/blank.hbs
index ea0e7f56..3a60ab77 100644
--- a/src/components/date-input/blank.hbs
+++ b/src/components/date-input/blank.hbs
@@ -1,8 +1,10 @@
---
title: Date input
-width: wide
+width: narrow
page: true
---
+
+
{{#>_layout-container}}
@@ -39,3 +41,40 @@ page: true
{{/_layout-container}}
+
+{{#>_layout-container brand-dark="true" invert="true"}}
+
+
+
+
+
+
+
+{{/_layout-container}}
\ No newline at end of file
diff --git a/src/components/date-input/index.hbs b/src/components/date-input/index.hbs
index 9d2e44f2..a46e8d4c 100644
--- a/src/components/date-input/index.hbs
+++ b/src/components/date-input/index.hbs
@@ -1,10 +1,10 @@
---
title: Date input
-width: wide
+width: narrow
tabs: true
directory: date-input
intro: 'A date input allows users to enter a date.'
-figma: 'URL'
+theme: true
meta-description: 'A date input allows users to enter a date.'
meta-index: true
---
diff --git a/src/components/date-input/theme.hbs b/src/components/date-input/theme.hbs
new file mode 100644
index 00000000..06dbca02
--- /dev/null
+++ b/src/components/date-input/theme.hbs
@@ -0,0 +1,81 @@
+---
+title: Date input
+width: narrow
+page: true
+---
+
+{{#>_theme}}
+{{#>_layout-container}}
+
+
+
+
+
+
+
+{{/_layout-container}}
+
+{{#>_layout-container brand-dark="true" invert="true"}}
+
+
+
+
+
+
+
+{{/_layout-container}}
+{{/_theme}}
diff --git a/src/components/date-picker/_date-picker.hbs b/src/components/date-picker/_date-picker.hbs
new file mode 100644
index 00000000..64a55164
--- /dev/null
+++ b/src/components/date-picker/_date-picker.hbs
@@ -0,0 +1,138 @@
+{{#if single}}
+{{/if}}{{#if multiple}}
+
+
+ Start date
+ For example 28 10 2024
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+ OK
+
+
+
+{{/if}}
\ No newline at end of file
diff --git a/src/components/date-picker/_date-picker.scss b/src/components/date-picker/_date-picker.scss
new file mode 100644
index 00000000..0574d2df
--- /dev/null
+++ b/src/components/date-picker/_date-picker.scss
@@ -0,0 +1,324 @@
+:root {
+ --date-picker-calendar-gap: 4px;
+ --date-picker-calendar-item-size: 2.6em;
+}
+
+@media (min-width: 48rem) {
+ :root {
+ --date-picker-calendar-item-size: 3em;
+ }
+}
+
+.nsw-date-input {
+ position: relative;
+
+ &__button {
+ display: flex;
+
+ button {
+ height: rem(48px);
+ align-self: flex-end;
+ }
+ }
+
+ &__wrapper {
+ position: relative;
+
+ button {
+ .nsw-material-icons {
+ font-size: 1.25rem;
+ }
+ }
+ }
+}
+
+.nsw-date-picker {
+ display: inline-block;
+ position: absolute;
+ left: 0;
+ top: calc(var(--date-picker-calendar-gap) + 100%);
+ background-color: var(--nsw-white);
+ border-radius: var(--nsw-border-radius);
+ box-shadow: var(--nsw-box-shadow);
+ padding: 0.375rem;
+ z-index: 5;
+ user-select: none;
+ overflow: hidden;
+ visibility: hidden;
+ opacity: 0;
+ transition: visibility 0s 0.2s, opacity 0.2s;
+
+ @include breakpoint('lg') {
+ padding: 0.5625rem;
+ }
+
+ * {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ }
+
+ ol,
+ ul {
+ list-style: none;
+ }
+
+ .nsw-section--invert & {
+ .nsw-icon-button {
+ color: rgba(var(--nsw-palette-grey-01-rgb), 0.6);
+
+ &:hover {
+ @include nsw-hover;
+ outline-width: 0;
+ }
+
+ &:focus {
+ @include nsw-focus();
+ }
+ }
+
+ .nsw-button--dark-outline-solid {
+ background-color: var(--nsw-white);
+ border-color: var(--nsw-brand-dark);
+ color: var(--nsw-brand-dark);
+
+ &:focus {
+ @include nsw-focus();
+ }
+
+ &:hover {
+ background-color: var(--nsw-brand-dark);
+ border-color: transparent;
+ color: var(--nsw-text-light);
+ }
+ }
+
+ .nsw-button--dark {
+ background-color: var(--nsw-brand-dark);
+ color: var(--nsw-text-light);
+
+ &:focus {
+ @include nsw-focus();
+ }
+
+ &:hover {
+ color: var(--nsw-text-light);
+ background-color: var(--nsw-brand-dark);
+ background-image: linear-gradient(rgba(var(--nsw-white-rgb), 0.15), rgba(var(--nsw-white-rgb), 0.15));
+ border-color: transparent;
+ }
+
+ &:active {
+ background-color: var(--nsw-brand-dark);
+ background-image: linear-gradient(rgba(var(--nsw-white-rgb), 0.075), rgba(var(--nsw-white-rgb), 0.075));
+ border-color: transparent;
+ }
+ }
+ }
+
+ &--is-visible {
+ visibility: visible;
+ opacity: 1;
+ transition: opacity 0.2s;
+ }
+
+ &__title {
+ position: relative;
+
+ &-label {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-weight: 600;
+ color: var(--nsw-text-dark);
+ }
+
+ &-nav {
+ display: flex;
+ flex-wrap: wrap;
+ position: relative;
+ z-index: 1;
+ justify-content: space-between;
+
+ li {
+ display: flex;
+ }
+
+ &-btn {
+ width: var(--date-picker-calendar-item-size);
+ height: var(--date-picker-calendar-item-size);
+ border-radius: var(--nsw-border-radius);
+ color: rgba(var(--nsw-palette-grey-01-rgb), 0.6);
+ transition: transform 0.2s;
+
+ &:hover {
+ background-color: rgba(var(--nsw-palette-grey-01-rgb), 0.075);
+ color: var(--nsw-text-dark);
+ }
+ }
+ }
+ }
+
+ &__week,
+ &__dates {
+ display: flex;
+ flex-wrap: wrap;
+
+ li {
+ width: var(--date-picker-calendar-item-size);
+ height: var(--date-picker-calendar-item-size);
+ }
+ }
+
+ &__day {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 0.833rem;
+ color: rgba(var(--nsw-palette-grey-01-rgb), 0.6);
+ }
+
+ &__dates {
+ width: calc(var(--date-picker-calendar-item-size) * 7);
+ }
+
+ &__date {
+ background-color: transparent;
+ padding: 0;
+ border: 0;
+ border-radius: 0;
+ color: var(--nsw-text-dark);
+ line-height: inherit;
+ appearance: none;
+ position: relative;
+ z-index: 1;
+ width: 100%;
+ height: 100%;
+ text-align: center;
+ font-size: 1rem;
+
+ &:focus {
+ @include nsw-focus(false);
+ }
+
+ &:focus,
+ &:hover {
+ border-radius: var(--nsw-border-radius);
+ }
+
+ &:hover {
+ box-shadow: inset 0 0 0 2px var(--nsw-focus);
+ }
+
+ &:focus:not(:hover) {
+ box-shadow: 0 0 0 2px rgba(var(--nsw-palette-blue-01-rgb), 0.2), 0 2px 4px rgba(var(--nsw-palette-blue-01-rgb), 0.3);
+ }
+
+ &--today {
+ color: var(--nsw-brand-dark);
+ border-radius: var(--nsw-border-radius);
+
+ @include nsw-hover();
+
+ &::after {
+ content: '';
+ background-color: var(--nsw-brand-dark);
+ border-radius: 4px;
+ bottom: 6px;
+ height: 4px;
+ left: 50%;
+ margin-left: -2px;
+ position: absolute;
+ width: 4px;
+ }
+ }
+
+ &--keyboard-focus {
+ background-color: rgba(var(--nsw-palette-grey-01-rgb), 0.2);
+ border-radius: var(--nsw-border-radius);
+ }
+
+ &::-moz-focus-inner {
+ border: 0;
+ }
+
+ &--selected {
+ border-radius: var(--nsw-border-radius);
+ background-color: var(--nsw-brand-dark);
+ box-shadow: 0 2px 4px rgba(var(--nsw-palette-blue-01-rgb), 0.3);
+ color: var(--nsw-white);
+ z-index: 2;
+ }
+
+ &[disabled='true'],
+ &[aria-disabled='true'] {
+ background-color: rgba(var(--nsw-palette-grey-03-rgb), 0.5);
+ color: rgba(var(--nsw-palette-grey-01-rgb), 0.7);
+ border-radius: var(--nsw-border-radius);
+ pointer-events: none;
+ }
+
+ &.nsw-date-picker__date--range {
+ background-color: rgba(var(--nsw-palette-blue-01-rgb), 0.2);
+ color: var(--nsw-text-dark);
+
+ &:focus,
+ &:hover {
+ border-radius: 0;
+ }
+
+ &:focus {
+ background-color: var(--nsw-focus);
+ }
+
+ &-start,
+ &-end {
+ background-color: var(--nsw-brand-dark);
+ box-shadow: 0 2px 4px rgba(var(--nsw-palette-blue-01-rgb), 0.3);
+ color: var(--nsw-white);
+ z-index: 2;
+
+ &:focus:not(:hover) {
+ box-shadow: 0 0 0 2px rgba(var(--nsw-palette-blue-01-rgb), 0.2), 0 2px 4px rgba(var(--nsw-palette-blue-01-rgb), 0.3);
+ }
+ }
+
+ &-start {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ &-end {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ }
+ }
+
+ &__buttongroup {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: 0.375rem 0;
+ gap: 0.5rem;
+
+ @include breakpoint('lg') {
+ margin: 0.5625rem 0;
+ }
+
+ button {
+ height: var(--date-picker-calendar-item-size);
+ line-height: 1;
+ padding: 0;
+ flex-basis: 100%;
+ flex: 1;
+ }
+ }
+}
diff --git a/src/components/date-picker/_guidance.hbs b/src/components/date-picker/_guidance.hbs
new file mode 100644
index 00000000..d13cfc33
--- /dev/null
+++ b/src/components/date-picker/_guidance.hbs
@@ -0,0 +1,226 @@
+---
+title: Date picker
+layout: blank-layout.hbs
+---
+
+Usage
+
+A date picker simplifies the process of entering dates by providing a visual calendar interface helping users to understand a date’s relationship to other days, such as the day of the week or how far away a date is from today.
+
+The date picker is a progressive enhancement to text inputs that lets users choose a date from a calendar interface or enter the date as text. To help with accessibility, it can be used with a keyboard, as well as mouse or touchscreen.
+
+Do:
+
+ display the date format above the text input
+ ensure users can enter dates via the calendar or text input
+ disable dates that aren’t available
+
+
+When to use
+Use a date picker:
+
+ when the date options are relatively close to the present day.
+ when users need to know the day of the week, or the week of the month, as well as the date.
+ when knowing the day of the week helps users choose a specific date.
+
+
+When to avoid
+Don't use a date picker:
+
+ when the user needs to select a set date that they know, like their birthday — use date input instead
+ when having a calendar is not likely to help the user - such as a date several years in the past
+ when screen size is limited, a full calendar view might not be practical.
+
+
+How this component works
+
+The date picker component relies on JavaScript so should be treated as an enhancement. Users should always be able to enter the date into a text field as well as use the control.
+
+Users select dates from a visual representation of the month and can skip through months and years. This allows them to easily see what day of the week and week of the month a particular date is in, which is particularly useful for tasks like appointment booking.
+
+The Date Picker component comes with the following customizations:
+
+Month labels
+
+By default, the calendar widget shows the full English name of the months; if you wish to change this default (e.g., passing a short version of the label or using a different language), add a data-months attribute to the .nsw-date-input element with the comma-separated list of the labels you want to use:
+
+{{#>_docs-code open=true}}
+
+
+
+{{/_docs-code}}
+
+Date format
+
+By default, the date format of the text input field is 'dd/mm/yyyy'; if you want to change this order (e.g., yyyy/mm/dd), add a data-date-format attribute to the .nsw-date-input element with the new order you want to use:
+
+{{#>_docs-code open=true}}
+
+
+
+{{/_docs-code}}
+
+Date separator
+
+By default, the slash ('/') is used as date separator; if you want to use a different character (e.g., '-'), add a data-date-separator attribute to the .nsw-date-input element:
+
+{{#>_docs-code open=true}}
+
+
+
+{{/_docs-code}}
+
+Preselected date
+
+If you want to prefill your Date Picker component with a date, use the .js-date-input__text
input element and set its value
equal to the date you want to use. Make sure to use the date format you specify in data-date-format or the default, d-m-y.
+
+Date ranges and disabled dates
+
+Allowed date ranges for a date picker can be set by specifying earliest and latest allowed dates. Individual dates can also be disabled.
+
+If you want to specify an earliest possible date for the calendar, add a data-min-date attribute to the .nsw-date-input element:
+
+{{#>_docs-code open=true}}
+
+
+
+{{/_docs-code}}
+
+If you want to specify a latest possible date for the calendar, add a data-max-date attribute to the .nsw-date-input element:
+
+
+{{#>_docs-code open=true}}
+
+
+
+{{/_docs-code}}
+
+Use an attribute of data-dates-disabled to specify a list of dates that the user will not be able to select.
+
+The value of this attribute should be a space-separated list of dates in the format you specify in data-date-format or the default, d-m-y.
+
+{{#>_docs-code open=true}}
+
+
+
+{{/_docs-code}}
+
+Accessibility
+All components are responsive and meet WCAG 2.1 AA accessibility guidelines.
+
+The dialog contains a calendar that uses the grid pattern to present buttons that enable the user to choose a day from the calendar. Choosing a date from the calendar closes the dialog and populates the date input field. When the dialog is opened, if the input field is empty, or does not contain a valid date, then the current date is focused in the calendar. Otherwise, the focus is placed on the day in the calendar that matches the value of the date input field.
+
+Keyboard support
+
+Users can navigate the calendar by using the cursor keys to move around the calendar, and can use the enter key or spacebar to select a date.
+
+The following table lists the keyboard commands that the date picker supports.
+
+
+
+
+
+ Element
+ Key
+ Action
+
+
+
+
+ Calendar button
+ Space
,
+ Enter
+
+ Opens the date picker. If there is a current date set in the text input, that date is focussed in the date picker. If not, today's date is focussed.
+
+
+ Date picker
+ Tab
+ Moves focus to the next element in the tab order. If tabbing away from the last focusable element in the tab order, moves focus to the first focusable element in the date picker.
+
+
+ Date picker
+ Shift + Tab
+ Moves focus to the previous element in the tab order. If tabbing away from the first focusable element in the tab order, moves focus to the last focusable element in the date picker.
+
+
+ Month and year buttons
+ Space
,
+ Enter
+
+ Change the current month or year displayed in the date picker.
+
+
+ Dates
+ Space
,
+ Enter
+
+ Selects the focussed date, closes the date picker and moves focus back to the calendar button. Updates the accessible name of the calendar button to indicate the selected date.
+
+
+ Dates
+ Up
+ Moves focus to the same day of the previous week, changing the displayed month if necessary.
+
+
+ Dates
+ Down
+ Moves focus to the same day of the next week, changing the displayed month if necessary.
+
+
+ Dates
+ Left
+ Moves focus to the previous day, changing the displayed month if necessary.
+
+
+ Dates
+ Right
+ Moves focus to the next day, changing the displayed month if necessary.
+
+
+ Dates
+ Home
+ Moves focus to the first day of the current week.
+
+
+ Dates
+ End
+ Moves focus to the last day of the current week.
+
+
+ Dates
+ Page Up
+ Shows the previous month and focuses on the same day of the month.
+
+
+ Dates
+ Shift + Page Up
+ Shows same month in the previous year and focuses on the same day of the month.
+
+
+ Dates
+ Page Down
+ Shows the next month and focuses on the same day of the month.
+
+
+ Dates
+ Shift + Page Down
+ Shows same month in the next year and focuses on the same day of the month.
+
+
+ Cancel button
+ Space
,
+ Enter
+
+ Closes the date picker and makes no change to the date in the text input. Focus is returned to the calendar button.
+
+
+ OK button
+ Space
,
+ Enter
+
+ Closes the date picker and updates the date in the text input with the chosen date in the date picker. Focus is returned to the calendar button.
+
+
+
+
\ No newline at end of file
diff --git a/src/components/date-picker/blank.hbs b/src/components/date-picker/blank.hbs
new file mode 100644
index 00000000..862fac7a
--- /dev/null
+++ b/src/components/date-picker/blank.hbs
@@ -0,0 +1,37 @@
+---
+title: Date picker
+width: narrow
+page: true
+---
+
+{{#>_layout-container}}
+
+{{>_date-picker single=true id="1"}}
+
+
+
+
+{{>_date-picker single=true disabled=true id="2"}}
+
+
+
+
+{{>_date-picker multiple=true id="3"}}
+
+{{/_layout-container}}
+
+{{#>_layout-container brand-dark="true" invert="true"}}
+
+{{>_date-picker single=true id="1"}}
+
+
+
+
+{{>_date-picker single=true disabled=true id="2"}}
+
+
+
+
+{{>_date-picker multiple=true id="3"}}
+
+{{/_layout-container}}
diff --git a/src/components/date-picker/date-picker.js b/src/components/date-picker/date-picker.js
new file mode 100644
index 00000000..d0b4698c
--- /dev/null
+++ b/src/components/date-picker/date-picker.js
@@ -0,0 +1,578 @@
+/* eslint-disable max-len */
+class DatePicker {
+ constructor(element) {
+ this.element = element
+ this.prefix = 'nsw-'
+ this.class = 'date-picker'
+ this.dateClass = `${this.prefix}${this.class}__date`
+ this.todayClass = `${this.dateClass}--today`
+ this.selectedClass = `${this.dateClass}--selected`
+ this.keyboardFocusClass = `${this.dateClass}--keyboard-focus`
+ this.visibleClass = `${this.prefix}${this.class}--is-visible`
+ this.months = this.element.getAttribute('data-months') ? this.element.getAttribute('data-months') : ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
+ this.dateFormat = this.element.getAttribute('data-date-format') ? this.element.getAttribute('data-date-format') : 'd-m-y'
+ this.dateSeparator = this.element.getAttribute('data-date-separator') ? this.element.getAttribute('data-date-separator') : '/'
+ this.datesDisabled = this.element.getAttribute('data-dates-disabled') ? this.element.getAttribute('data-dates-disabled') : ''
+ this.minDate = this.element.getAttribute('data-min-date') ? this.element.getAttribute('data-min-date') : ''
+ this.maxDate = this.element.getAttribute('data-max-date') ? this.element.getAttribute('data-max-date') : ''
+ this.input = this.element.querySelector('.js-date-input__text')
+ this.trigger = this.element.querySelector('.js-date-input__trigger')
+ this.triggerLabel = this.trigger.getAttribute('aria-label')
+ this.datePicker = this.element.querySelector('.js-date-picker')
+ this.body = this.datePicker.querySelector('.js-date-picker__dates')
+ this.navigation = this.datePicker.querySelector('.js-date-picker__title-nav')
+ this.heading = this.datePicker.querySelector('.js-date-picker__title-label')
+ this.close = this.datePicker.querySelector('.js-date-picker__close')
+ this.accept = this.datePicker.querySelector('.js-date-picker__accept')
+ this.multipleInput = this.element.querySelector('.js-date-input-multiple')
+ this.dateInput = this.multipleInput && this.multipleInput.querySelector('.js-date-picker-date')
+ this.monthInput = this.multipleInput && this.multipleInput.querySelector('.js-date-picker-month')
+ this.yearInput = this.multipleInput && this.multipleInput.querySelector('.js-date-picker-year')
+ this.multiDateArray = [this.dateInput, this.monthInput, this.yearInput]
+ this.dateIndexes = this.getDateIndexes()
+ this.pickerVisible = false
+ this.dateSelected = false
+ this.selectedDay = false
+ this.selectedMonth = false
+ this.selectedYear = false
+ this.firstFocusable = false
+ this.lastFocusable = false
+ this.disabledArray = false
+ }
+
+ init() {
+ this.disabledDates()
+ this.resetCalendar()
+ this.initCalendarAria()
+ this.initCalendarEvents()
+ this.placeCalendar()
+ }
+
+ initCalendarAria() {
+ this.resetLabelCalendarTrigger()
+
+ const srLiveReagion = document.createElement('div')
+ srLiveReagion.setAttribute('aria-live', 'polite')
+ srLiveReagion.classList.add('sr-only', 'js-date-input__sr-live')
+ this.element.appendChild(srLiveReagion)
+ this.srLiveReagion = this.element.querySelector('.js-date-input__sr-live')
+ }
+
+ initCalendarEvents() {
+ if (this.input) {
+ this.input.addEventListener('focus', () => {
+ this.toggleCalendar(true)
+ })
+ }
+
+ if (this.multipleInput) {
+ this.multiDateArray.forEach((element) => {
+ element.addEventListener('focus', () => {
+ this.hideCalendar()
+ })
+ })
+ }
+
+ if (this.trigger) {
+ this.trigger.addEventListener('click', (event) => {
+ event.preventDefault()
+ this.pickerVisible = false
+ this.toggleCalendar()
+ this.trigger.setAttribute('aria-expanded', 'true')
+ })
+ }
+
+ if (this.close) {
+ this.close.addEventListener('click', (event) => {
+ event.preventDefault()
+ this.hideCalendar()
+ })
+ }
+
+ if (this.accept) {
+ this.accept.addEventListener('click', (event) => {
+ event.preventDefault()
+ const day = this.body.querySelector('button[tabindex="0"]')
+ if (day) {
+ this.dateSelected = true
+ this.selectedDay = day.innerText
+ this.selectedMonth = this.currentMonth
+ this.selectedYear = this.currentYear
+ this.setInputValue()
+ if (this.input) {
+ this.input.focus()
+ } else if (this.multipleInput) {
+ this.trigger.focus()
+ this.hideCalendar()
+ }
+
+ this.resetLabelCalendarTrigger()
+ }
+ })
+ }
+
+ this.body.addEventListener('click', (event) => {
+ event.preventDefault()
+ const day = event.target.closest('button')
+ if (day) {
+ this.dateSelected = true
+ this.selectedDay = day.innerText
+ this.selectedMonth = this.currentMonth
+ this.selectedYear = this.currentYear
+ this.setInputValue()
+ if (this.input) {
+ this.input.focus()
+ } else if (this.multipleInput) {
+ this.trigger.focus()
+ this.hideCalendar()
+ }
+
+ this.resetLabelCalendarTrigger()
+ }
+ })
+
+ this.navigation.addEventListener('click', (event) => {
+ event.preventDefault()
+ const monthBtn = event.target.closest('.js-date-picker__month-nav-btn')
+ const yearBtn = event.target.closest('.js-date-picker__year-nav-btn')
+
+ if (monthBtn && monthBtn.classList.contains('js-date-picker__month-nav-btn--prev')) {
+ this.showPrevMonth(true)
+ } else if (monthBtn && monthBtn.classList.contains('js-date-picker__month-nav-btn--next')) {
+ this.showNextMonth(true)
+ } else if (yearBtn && yearBtn.classList.contains('js-date-picker__year-nav-btn--prev')) {
+ this.showPrevYear(true)
+ } else if (yearBtn && yearBtn.classList.contains('js-date-picker__year-nav-btn--next')) {
+ this.showNextYear(true)
+ }
+ })
+
+ window.addEventListener('keydown', (event) => {
+ if ((event.code && event.code === 27) || (event.key && event.key.toLowerCase() === 'escape')) {
+ if (document.activeElement.closest('.js-date-picker')) {
+ const activeInput = document.activeElement.closest('.js-date-input').querySelector('input')
+ activeInput.focus()
+ } else {
+ this.hideCalendar()
+ }
+ }
+ })
+
+ window.addEventListener('click', (event) => {
+ if (!event.target.closest('.js-date-picker') && !event.target.closest('.js-date-input') && this.pickerVisible) {
+ this.hideCalendar()
+ }
+ })
+
+ this.body.addEventListener('keydown', (event) => {
+ let day = this.currentDay
+ if ((event.code && event.code === 40) || (event.key && event.key.toLowerCase() === 'arrowdown')) {
+ day += 7
+ this.resetDayValue(day)
+ } else if ((event.code && event.code === 39) || (event.key && event.key.toLowerCase() === 'arrowright')) {
+ day += 1
+ this.resetDayValue(day)
+ } else if ((event.code && event.code === 37) || (event.key && event.key.toLowerCase() === 'arrowleft')) {
+ day -= 1
+ this.resetDayValue(day)
+ } else if ((event.code && event.code === 38) || (event.key && event.key.toLowerCase() === 'arrowup')) {
+ day -= 7
+ this.resetDayValue(day)
+ } else if ((event.code && event.code === 35) || (event.key && event.key.toLowerCase() === 'end')) {
+ event.preventDefault()
+ day = day + 6 - this.getDayOfWeek(this.currentYear, this.currentMonth, day)
+ this.resetDayValue(day)
+ } else if ((event.code && event.code === 36) || (event.key && event.key.toLowerCase() === 'home')) {
+ event.preventDefault()
+ day -= this.getDayOfWeek(this.currentYear, this.currentMonth, day)
+ this.resetDayValue(day)
+ } else if ((event.code && event.code === 34) || (event.key && event.key.toLowerCase() === 'pagedown')) {
+ event.preventDefault()
+ this.showNextMonth()
+ } else if ((event.code && event.code === 33) || (event.key && event.key.toLowerCase() === 'pageup')) {
+ event.preventDefault()
+ this.showPrevMonth()
+ }
+ })
+
+ this.datePicker.addEventListener('keydown', (event) => {
+ if ((event.code && event.code === 9) || (event.key && event.key === 'Tab')) {
+ this.trapFocus(event)
+ }
+ })
+
+ if (this.input) {
+ this.input.addEventListener('keydown', (event) => {
+ if ((event.code && event.code === 13) || (event.key && event.key.toLowerCase() === 'enter')) {
+ this.resetCalendar()
+ this.resetLabelCalendarTrigger()
+ this.hideCalendar()
+ } else if ((event.code && event.code === 40) || (event.key && event.key.toLowerCase() === 'arrowdown' && this.pickerVisible)) {
+ this.body.querySelector('button[tabindex="0"]').focus()
+ }
+ })
+ }
+
+ if (this.multipleInput) {
+ this.multiDateArray.forEach((element) => {
+ element.addEventListener('keydown', (event) => {
+ if ((event.code && event.code === 13) || (event.key && event.key.toLowerCase() === 'enter')) {
+ this.resetCalendar()
+ this.resetLabelCalendarTrigger()
+ this.hideCalendar()
+ } else if ((event.code && event.code === 40) || (event.key && event.key.toLowerCase() === 'arrowdown' && this.pickerVisible)) {
+ this.body.querySelector('button[tabindex="0"]').focus()
+ }
+ })
+ })
+ }
+ }
+
+ getCurrentDay(date) {
+ return (date)
+ ? this.getDayFromDate(date)
+ : new Date().getDate()
+ }
+
+ getCurrentMonth(date) {
+ return (date)
+ ? this.getMonthFromDate(date)
+ : new Date().getMonth()
+ }
+
+ getCurrentYear(date) {
+ return (date)
+ ? this.getYearFromDate(date)
+ : new Date().getFullYear()
+ }
+
+ getDayFromDate(date) {
+ const day = parseInt(date.split('-')[2], 10)
+ return Number.isNaN(day) ? this.getCurrentDay(false) : day
+ }
+
+ getMonthFromDate(date) {
+ const month = parseInt(date.split('-')[1], 10) - 1
+ return Number.isNaN(month) ? this.getCurrentMonth(false) : month
+ }
+
+ getYearFromDate(date) {
+ const year = parseInt(date.split('-')[0], 10)
+ return Number.isNaN(year) ? this.getCurrentYear(false) : year
+ }
+
+ showNextMonth(bool) {
+ this.currentYear = (this.currentMonth === 11) ? this.currentYear + 1 : this.currentYear
+ this.currentMonth = (this.currentMonth + 1) % 12
+ this.currentDay = this.checkDayInMonth()
+ this.showCalendar(bool)
+ this.srLiveReagion.textContent = `${this.months[this.currentMonth]} ${this.currentYear}`
+ }
+
+ showPrevMonth(bool) {
+ this.currentYear = (this.currentMonth === 0) ? this.currentYear - 1 : this.currentYear
+ this.currentMonth = (this.currentMonth === 0) ? 11 : this.currentMonth - 1
+ this.currentDay = this.checkDayInMonth()
+ this.showCalendar(bool)
+ this.srLiveReagion.textContent = `${this.months[this.currentMonth]} ${this.currentYear}`
+ }
+
+ showNextYear(bool) {
+ this.currentYear += 1
+ this.currentMonth %= 12
+ this.currentDay = this.checkDayInMonth()
+ this.showCalendar(bool)
+ this.srLiveReagion.textContent = `${this.months[this.currentMonth]} ${this.currentYear}`
+ }
+
+ showPrevYear(bool) {
+ this.currentYear -= 1
+ this.currentMonth %= 12
+ this.currentDay = this.checkDayInMonth()
+ this.showCalendar(bool)
+ this.srLiveReagion.textContent = `${this.months[this.currentMonth]} ${this.currentYear}`
+ }
+
+ checkDayInMonth() {
+ return (this.currentDay > this.constructor.daysInMonth(this.currentYear, this.currentMonth)) ? 1 : this.currentDay
+ }
+
+ static daysInMonth(year, month) {
+ return 32 - new Date(year, month, 32).getDate()
+ }
+
+ resetCalendar() {
+ let currentDate = false
+ let selectedDate
+
+ if (this.input) {
+ selectedDate = this.input.value
+ } else if (this.multipleInput) {
+ if (this.dateInput.value !== '' && this.monthInput.value !== '' && this.yearInput.value !== '') {
+ selectedDate = `${this.dateInput.value}/${this.monthInput.value}/${this.yearInput.value}`
+ } else {
+ selectedDate = ''
+ }
+ }
+
+ this.dateSelected = false
+ if (selectedDate !== '') {
+ const date = this.getDateFromInput()
+ this.dateSelected = true
+ currentDate = date
+ }
+ this.currentDay = this.getCurrentDay(currentDate)
+ this.currentMonth = this.getCurrentMonth(currentDate)
+ this.currentYear = this.getCurrentYear(currentDate)
+
+ this.selectedDay = this.dateSelected ? this.currentDay : false
+ this.selectedMonth = this.dateSelected ? this.currentMonth : false
+ this.selectedYear = this.dateSelected ? this.currentYear : false
+ }
+
+ disabledDates() {
+ this.disabledArray = []
+
+ if (this.datesDisabled) {
+ const disabledDates = this.datesDisabled.split(' ')
+
+ disabledDates.forEach((element) => {
+ this.disabledArray.push(element)
+ })
+ }
+ }
+
+ convertDateToParse(date) {
+ const dateArray = date.split(this.dateSeparator)
+ return `${dateArray[this.dateIndexes[2]]}, ${dateArray[this.dateIndexes[1]]}, ${dateArray[this.dateIndexes[0]]}`
+ }
+
+ isDisabledDate(day, month, year) {
+ let disabled = false
+
+ const dateParse = new Date(year, month, day)
+ const minDate = new Date(this.convertDateToParse(this.minDate))
+ const maxDate = new Date(this.convertDateToParse(this.maxDate))
+
+ if (this.minDate && minDate > dateParse) {
+ disabled = true
+ }
+
+ if (this.maxDate && maxDate < dateParse) {
+ disabled = true
+ }
+
+ if (this.disabledArray.length > 0) {
+ this.disabledArray.forEach((element) => {
+ const disabledDate = new Date(this.convertDateToParse(element))
+ if (dateParse.getTime() === disabledDate.getTime()) {
+ disabled = true
+ }
+ })
+ }
+
+ return disabled
+ }
+
+ showCalendar(bool) {
+ const firstDay = this.constructor.getDayOfWeek(this.currentYear, this.currentMonth, '01')
+ this.body.innerHTML = ''
+ this.heading.innerHTML = `${this.months[this.currentMonth]} ${this.currentYear}`
+
+ let date = 1
+ let calendar = ''
+ for (let i = 0; i < 6; i += 1) {
+ for (let j = 0; j < 7; j += 1) {
+ if (i === 0 && j < firstDay) {
+ calendar += ' '
+ } else if (date > this.constructor.daysInMonth(this.currentYear, this.currentMonth)) {
+ break
+ } else {
+ let classListDate = ''
+ let tabindexValue = '-1'
+ let disabled
+ if (date === this.currentDay) {
+ tabindexValue = '0'
+ }
+ if (this.getCurrentMonth() === this.currentMonth && this.getCurrentYear() === this.currentYear && date === this.getCurrentDay()) {
+ classListDate += ` ${this.todayClass}`
+ }
+
+ if (this.isDisabledDate(date, this.currentMonth, this.currentYear)) {
+ classListDate += ` ${this.dateClass}--disabled`
+ disabled = 'aria-disabled="true"'
+ }
+ if (this.dateSelected && date === this.selectedDay && this.currentYear === this.selectedYear && this.currentMonth === this.selectedMonth) {
+ classListDate += ` ${this.selectedClass}`
+ }
+ calendar = `${calendar}${date} `
+ date += 1
+ }
+ }
+ }
+ this.body.innerHTML = calendar
+
+ if (!this.pickerVisible) this.datePicker.classList.add(this.visibleClass)
+ this.pickerVisible = true
+
+ if (!bool) this.body.querySelector('button[tabindex="0"]').focus()
+
+ this.getFocusableElements()
+
+ this.placeCalendar()
+ }
+
+ hideCalendar() {
+ this.datePicker.classList.remove(this.visibleClass)
+ this.pickerVisible = false
+
+ this.firstFocusable = false
+ this.lastFocusable = false
+
+ if (this.trigger) this.trigger.setAttribute('aria-expanded', 'false')
+ }
+
+ toggleCalendar(bool) {
+ if (!this.pickerVisible) {
+ this.resetCalendar()
+ this.showCalendar(bool)
+ } else {
+ this.hideCalendar()
+ }
+ }
+
+ static getDayOfWeek(year, month, day) {
+ let weekDay = (new Date(year, month, day)).getDay() - 1
+ if (weekDay < 0) weekDay = 6
+ return weekDay
+ }
+
+ getDateIndexes() {
+ const dateFormat = this.dateFormat.toLowerCase().replace(/-/g, '')
+ return [dateFormat.indexOf('d'), dateFormat.indexOf('m'), dateFormat.indexOf('y')]
+ }
+
+ setInputValue() {
+ if (this.input) {
+ this.input.value = this.getDateForInput(this.selectedDay, this.selectedMonth, this.selectedYear)
+ } else if (this.multipleInput) {
+ this.dateInput.value = this.constructor.getReadableDate(this.selectedDay)
+ this.monthInput.value = this.constructor.getReadableDate(this.selectedMonth + 1)
+ this.yearInput.value = this.selectedYear
+ }
+ }
+
+ getDateForInput(day, month, year) {
+ const dateArray = []
+ dateArray[this.dateIndexes[0]] = this.constructor.getReadableDate(day)
+ dateArray[this.dateIndexes[1]] = this.constructor.getReadableDate(month + 1)
+ dateArray[this.dateIndexes[2]] = year
+ return dateArray[0] + this.dateSeparator + dateArray[1] + this.dateSeparator + dateArray[2]
+ }
+
+ getDateFromInput() {
+ let dateArray
+
+ if (this.input) {
+ dateArray = this.input.value.split(this.dateSeparator)
+ } else if (this.multipleInput) {
+ dateArray = [this.dateInput.value, this.monthInput.value, this.yearInput.value]
+ }
+ return `${dateArray[this.dateIndexes[2]]}-${dateArray[this.dateIndexes[1]]}-${dateArray[this.dateIndexes[0]]}`
+ }
+
+ static getReadableDate(date) {
+ return (date < 10) ? `0${date}` : date
+ }
+
+ resetDayValue(day) {
+ const totDays = this.constructor.daysInMonth(this.currentYear, this.currentMonth)
+ if (day > totDays) {
+ this.currentDay = day - totDays
+ this.showNextMonth(false)
+ } else if (day < 1) {
+ const newMonth = this.currentMonth === 0 ? 11 : this.currentMonth - 1
+ this.currentDay = this.constructor.daysInMonth(this.currentYear, newMonth) + day
+ this.showPrevMonth(false)
+ } else {
+ this.currentDay = day
+ const focusItem = this.body.querySelector('button[tabindex="0"]')
+ focusItem.setAttribute('tabindex', '-1')
+ focusItem.classList.remove(this.keyboardFocusClass)
+
+ const buttons = this.body.getElementsByTagName('button')
+ for (let i = 0; i < buttons.length; i += 1) {
+ if (parseInt(buttons[i].textContent, 10) === this.currentDay) {
+ buttons[i].setAttribute('tabindex', '0')
+ buttons[i].classList.add(this.keyboardFocusClass)
+ buttons[i].focus()
+ break
+ }
+ }
+ this.getFocusableElements()
+ }
+ }
+
+ resetLabelCalendarTrigger() {
+ if (!this.trigger) return
+
+ if (this.selectedYear && this.selectedMonth !== false && this.selectedDay) {
+ this.trigger.setAttribute('aria-label', `${this.triggerLabel}, selected date is ${new Date(this.selectedYear, this.selectedMonth, this.selectedDay).toDateString()}`)
+ } else {
+ this.trigger.setAttribute('aria-label', this.triggerLabel)
+ }
+ }
+
+ getFocusableElements() {
+ const allFocusable = this.datePicker.querySelectorAll('[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex]:not([tabindex="-1"]), [contenteditable], audio[controls], video[controls], summary')
+ this.getFirstFocusable(allFocusable)
+ this.getLastFocusable(allFocusable)
+ }
+
+ getFirstFocusable(elements) {
+ for (let i = 0; i < elements.length; i += 1) {
+ if ((elements[i].offsetWidth || elements[i].offsetHeight || elements[i].getClientRects().length) && elements[i].getAttribute('tabindex') !== '-1') {
+ this.firstFocusable = elements[i]
+ return true
+ }
+ }
+
+ return false
+ }
+
+ getLastFocusable(elements) {
+ for (let i = elements.length - 1; i >= 0; i -= 1) {
+ if ((elements[i].offsetWidth || elements[i].offsetHeight || elements[i].getClientRects().length) && elements[i].getAttribute('tabindex') !== '-1') {
+ this.lastFocusable = elements[i]
+ return true
+ }
+ }
+
+ return false
+ }
+
+ trapFocus(event) {
+ if (this.firstFocusable === document.activeElement && event.shiftKey) {
+ event.preventDefault()
+ this.lastFocusable.focus()
+ }
+ if (this.lastFocusable === document.activeElement && !event.shiftKey) {
+ event.preventDefault()
+ this.firstFocusable.focus()
+ }
+ }
+
+ placeCalendar() {
+ this.datePicker.style.left = '0px'
+ this.datePicker.style.right = 'auto'
+
+ const pickerBoundingRect = this.datePicker.getBoundingClientRect()
+
+ if (pickerBoundingRect.right > window.innerWidth) {
+ this.datePicker.style.left = 'auto'
+ this.datePicker.style.right = '0px'
+ }
+ }
+}
+
+export default DatePicker
diff --git a/src/components/date-picker/index.hbs b/src/components/date-picker/index.hbs
new file mode 100644
index 00000000..11d5b199
--- /dev/null
+++ b/src/components/date-picker/index.hbs
@@ -0,0 +1,30 @@
+---
+title: Date picker
+width: narrow
+tabs: true
+directory: date-picker
+theme: true
+intro: 'Use a date picker to let users pick a date from a calendar'
+meta-description: 'Use a date picker to let users pick a date from a calendar'
+meta-index: true
+---
+
+{{#>_docs-example}}
+{{>_date-picker single=true id="1"}}
+{{/_docs-example}}
+
+Date ranges and disabled dates
+
+Allowed date ranges for a date picker can be set by specifying earliest and latest allowed dates. Individual dates can also be disabled.
+
+In this example, a max date of 28 January 2024, a min date of 02 January 2024 has been set and three days in January 2024 have been disabled.
+
+{{#>_docs-example separated=true}}
+{{>_date-picker single=true disabled=true id="2"}}
+{{/_docs-example}}
+
+Individual date inputs for day, month and year
+
+{{#>_docs-example}}
+{{>_date-picker multiple=true id="3"}}
+{{/_docs-example}}
\ No newline at end of file
diff --git a/src/components/date-picker/json/date-picker.json b/src/components/date-picker/json/date-picker.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/src/components/date-picker/json/date-picker.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/components/date-picker/theme.hbs b/src/components/date-picker/theme.hbs
new file mode 100644
index 00000000..3490b420
--- /dev/null
+++ b/src/components/date-picker/theme.hbs
@@ -0,0 +1,39 @@
+---
+title: Date picker
+width: narrow
+page: true
+---
+
+{{#>_theme}}
+{{#>_layout-container}}
+
+{{>_date-picker single=true id="1"}}
+
+
+
+
+{{>_date-picker single=true disabled=true id="2"}}
+
+
+
+
+{{>_date-picker multiple=true id="3"}}
+
+{{/_layout-container}}
+
+{{#>_layout-container brand-dark="true" invert="true"}}
+
+{{>_date-picker single=true id="1"}}
+
+
+
+
+{{>_date-picker single=true disabled=true id="2"}}
+
+
+
+
+{{>_date-picker multiple=true id="3"}}
+
+{{/_layout-container}}
+{{/_theme}}
\ No newline at end of file
diff --git a/src/components/form/_form.scss b/src/components/form/_form.scss
index 0c2f68ea..cb00f562 100644
--- a/src/components/form/_form.scss
+++ b/src/components/form/_form.scss
@@ -1,3 +1,5 @@
+/* stylelint-disable max-nesting-depth */
+
$nsw-form-tick: ' ';
.nsw-form {
@@ -93,6 +95,10 @@ $nsw-form-tick: '
{{>_text-input
- id="form-text-1"
+ id="form-text-11"
label="Name"
}}
{{>_textarea
- id="form-textarea-1"
+ id="form-textarea-11"
label="Comment"
}}
{{>_input-group
- id="form-input-group-1"
+ id="form-input-group-11"
label="Search"
}}
{{>_input-group
- id="form-input-group-icon-1"
+ id="form-input-group-icon-11"
label="Search"
icon="search"
}}
{{>_input-group
- id="form-input-group-icon-2"
+ id="form-input-group-icon-12"
label="Search"
icon="search"
white=true
@@ -418,9 +419,9 @@ model: