From ea348f005aef7b2fda581a99338139f6fefcda63 Mon Sep 17 00:00:00 2001 From: Hans Krywalsky Date: Tue, 17 Aug 2021 16:45:39 +0200 Subject: [PATCH] feat(datetime): add firstDayOfWeek property (#23692) resolves #23556 Co-authored-by: Liam DeBeasi --- angular/src/directives/proxies.ts | 4 +- core/api.txt | 1 + core/src/components.d.ts | 8 +++ core/src/components/datetime/datetime.tsx | 10 ++- core/src/components/datetime/readme.md | 34 ++++++++- .../src/components/datetime/test/data.spec.ts | 4 ++ .../datetime/test/first-day-of-week/e2e.ts | 15 ++++ .../test/first-day-of-week/index.html | 71 +++++++++++++++++++ core/src/components/datetime/usage/angular.md | 5 +- .../components/datetime/usage/javascript.md | 3 + core/src/components/datetime/usage/react.md | 3 + core/src/components/datetime/usage/stencil.md | 3 + core/src/components/datetime/usage/vue.md | 3 + core/src/components/datetime/utils/data.ts | 36 ++++++++-- packages/vue/src/proxies.ts | 1 + 15 files changed, 186 insertions(+), 15 deletions(-) create mode 100644 core/src/components/datetime/test/first-day-of-week/e2e.ts create mode 100644 core/src/components/datetime/test/first-day-of-week/index.html diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index b7268898b60..172a0c7b671 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -244,8 +244,8 @@ export class IonContent { } export declare interface IonDatetime extends Components.IonDatetime { } -@ProxyCmp({ inputs: ["cancelText", "color", "dayValues", "disabled", "doneText", "hourCycle", "hourValues", "locale", "max", "min", "minuteValues", "mode", "monthValues", "name", "presentation", "readonly", "showDefaultButtons", "showDefaultTimeLabel", "showDefaultTitle", "size", "value", "yearValues"], "methods": ["confirm", "reset", "cancel"] }) -@Component({ selector: "ion-datetime", changeDetection: ChangeDetectionStrategy.OnPush, template: "", inputs: ["cancelText", "color", "dayValues", "disabled", "doneText", "hourCycle", "hourValues", "locale", "max", "min", "minuteValues", "mode", "monthValues", "name", "presentation", "readonly", "showDefaultButtons", "showDefaultTimeLabel", "showDefaultTitle", "size", "value", "yearValues"] }) +@ProxyCmp({ inputs: ["cancelText", "color", "dayValues", "disabled", "doneText", "firstDayOfWeek", "hourCycle", "hourValues", "locale", "max", "min", "minuteValues", "mode", "monthValues", "name", "presentation", "readonly", "showDefaultButtons", "showDefaultTimeLabel", "showDefaultTitle", "size", "value", "yearValues"], "methods": ["confirm", "reset", "cancel"] }) +@Component({ selector: "ion-datetime", changeDetection: ChangeDetectionStrategy.OnPush, template: "", inputs: ["cancelText", "color", "dayValues", "disabled", "doneText", "firstDayOfWeek", "hourCycle", "hourValues", "locale", "max", "min", "minuteValues", "mode", "monthValues", "name", "presentation", "readonly", "showDefaultButtons", "showDefaultTimeLabel", "showDefaultTitle", "size", "value", "yearValues"] }) export class IonDatetime { ionCancel!: EventEmitter; ionChange!: EventEmitter; diff --git a/core/api.txt b/core/api.txt index 83c3979592e..8751f33c12d 100644 --- a/core/api.txt +++ b/core/api.txt @@ -374,6 +374,7 @@ ion-datetime,prop,color,string | undefined,'primary',false,false ion-datetime,prop,dayValues,number | number[] | string | undefined,undefined,false,false ion-datetime,prop,disabled,boolean,false,false,false ion-datetime,prop,doneText,string,'Done',false,false +ion-datetime,prop,firstDayOfWeek,number,0,false,false ion-datetime,prop,hourCycle,"h12" | "h23" | undefined,undefined,false,false ion-datetime,prop,hourValues,number | number[] | string | undefined,undefined,false,false ion-datetime,prop,locale,string,'default',false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 999cc24397e..5eeb4bd5f16 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -728,6 +728,10 @@ export namespace Components { * The text to display on the picker's "Done" button. */ "doneText": string; + /** + * The first day of the week to use for `ion-datetime`. The default value is `0` and represents Sunday. + */ + "firstDayOfWeek": number; /** * The hour cycle of the `ion-datetime`. If no value is set, this is specified by the current locale. */ @@ -4296,6 +4300,10 @@ declare namespace LocalJSX { * The text to display on the picker's "Done" button. */ "doneText"?: string; + /** + * The first day of the week to use for `ion-datetime`. The default value is `0` and represents Sunday. + */ + "firstDayOfWeek"?: number; /** * The hour cycle of the `ion-datetime`. If no value is set, this is specified by the current locale. */ diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 5f0861cde90..90c0c900850 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -274,6 +274,12 @@ export class Datetime implements ComponentInterface { */ @Prop() locale = 'default'; + /** + * The first day of the week to use for `ion-datetime`. The + * default value is `0` and represents Sunday. + */ + @Prop() firstDayOfWeek = 0; + /** * The value of the datetime as a valid ISO 8601 datetime string. */ @@ -1305,7 +1311,7 @@ export class Datetime implements ComponentInterface {
- {getDaysOfWeek(this.locale, mode).map(d => { + {getDaysOfWeek(this.locale, mode, this.firstDayOfWeek % 7).map(d => { return
{d}
})}
@@ -1320,7 +1326,7 @@ export class Datetime implements ComponentInterface { return (
- {getDaysOfMonth(month, year).map((dateObject, index) => { + {getDaysOfMonth(month, year, this.firstDayOfWeek % 7).map((dateObject, index) => { const { day, dayOfWeek } = dateObject; const referenceParts = { month, day, year }; const { isActive, isToday, ariaLabel, ariaSelected, disabled } = getCalendarDayState(this.locale, referenceParts, this.activeParts, this.todayParts, this.minParts, this.maxParts, this.parsedDayValues); diff --git a/core/src/components/datetime/readme.md b/core/src/components/datetime/readme.md index 1b4ba44d433..89e9841dcdf 100644 --- a/core/src/components/datetime/readme.md +++ b/core/src/components/datetime/readme.md @@ -93,9 +93,9 @@ There are 4 primary hour cycle types: > Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle -There may be scenarios where you need to have more control over which hour cycle is used. This is where the `hour-cycle` property can help. +There may be scenarios where you need to have more control over which hour cycle is used. This is where the `hourCycle` property can help. -In the following example, we can use the `hour-cycle` property to force `ion-datetime` to use the 12 hour cycle even though the locale is `en-GB`, which uses a 24 hour cycle by default: +In the following example, we can use the `hourCycle` property to force `ion-datetime` to use the 12 hour cycle even though the locale is `en-GB`, which uses a 24 hour cycle by default: ```html @@ -111,6 +111,18 @@ For example, if you wanted to use a 12 hour cycle with the `en-GB` locale, you c `ion-datetime` currently supports the `h12` and `h23` hour cycle types. Interested in seeing support for `h11` and `h24` added to `ion-datetime`? [Let us know!](https://github.com/ionic-team/ionic-framework/issues/23750) +### Setting the First Day of the Week + +For `ion-datetime`, the default first day of the week is Sunday. As of 2021, there is no browser API that lets Ionic automatically determine the first day of the week based on a device's locale, though there is on-going work regarding this (see: [TC39 GitHub](https://github.com/tc39/ecma402/issues/6)). + +To customize the first day of the week, developers can use the `firstDayOfWeek` property. This property takes in a number between `0` and `6` where `0` represents Sunday and `6` represents Saturday. + +For example, if you wanted to have the first day of the week be Monday, you could set `firstDayOfWeek` to `1`: + +```html + +``` + ## Parsing Dates When `ionChange` is emitted, we provide an ISO-8601 string in the event payload. From there, it is the developer's responsibility to format it as they see fit. We recommend using a library like [date-fns](https://date-fns.org) to format their dates properly. @@ -210,7 +222,10 @@ dates in JavaScript. - + + + + @@ -297,6 +312,9 @@ export class MyComponent { + + +
My Custom Title
@@ -393,6 +411,9 @@ export const DateTimeExamples: React.FC = () => { {/* Custom Hour Cycle */} + + {/* Custom first day of week */} + {/* Custom title */} @@ -480,6 +501,9 @@ export class DatetimeExample { {/* Custom Hour Cycle */} , + + {/* Custom first day of week */} + , {/* Custom title */} @@ -543,6 +567,9 @@ export class DatetimeExample { + + + @@ -617,6 +644,7 @@ export class DatetimeExample { | `dayValues` | `day-values` | Values used to create the list of selectable days. By default every day is shown for the given month. However, to control exactly which days of the month to display, the `dayValues` input can take a number, an array of numbers, or a string of comma separated numbers. Note that even if the array days have an invalid number for the selected month, like `31` in February, it will correctly not show days which are not valid for the selected month. | `number \| number[] \| string \| undefined` | `undefined` | | `disabled` | `disabled` | If `true`, the user cannot interact with the datetime. | `boolean` | `false` | | `doneText` | `done-text` | The text to display on the picker's "Done" button. | `string` | `'Done'` | +| `firstDayOfWeek` | `first-day-of-week` | The first day of the week to use for `ion-datetime`. The default value is `0` and represents Sunday. | `number` | `0` | | `hourCycle` | `hour-cycle` | The hour cycle of the `ion-datetime`. If no value is set, this is specified by the current locale. | `"h12" \| "h23" \| undefined` | `undefined` | | `hourValues` | `hour-values` | Values used to create the list of selectable hours. By default the hour values range from `0` to `23` for 24-hour, or `1` to `12` for 12-hour. However, to control exactly which hours to display, the `hourValues` input can take a number, an array of numbers, or a string of comma separated numbers. | `number \| number[] \| string \| undefined` | `undefined` | | `locale` | `locale` | The locale to use for `ion-datetime`. This impacts month and day name formatting. The `'default'` value refers to the default locale set by your device. | `string` | `'default'` | diff --git a/core/src/components/datetime/test/data.spec.ts b/core/src/components/datetime/test/data.spec.ts index 23f1c903f8a..0fd92e3778e 100644 --- a/core/src/components/datetime/test/data.spec.ts +++ b/core/src/components/datetime/test/data.spec.ts @@ -30,6 +30,10 @@ describe('getDaysOfWeek()', () => { it('should return Spanish narrow names given a locale and mode', () => { expect(getDaysOfWeek('es-ES', 'md')).toEqual(['D', 'L', 'M', 'X', 'J', 'V', 'S']); }); + + it('should return English short names given a locale, mode and startOfWeek', () => { + expect(getDaysOfWeek('en-US', 'ios', 1)).toEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']); + }) }) describe('generateTime()', () => { diff --git a/core/src/components/datetime/test/first-day-of-week/e2e.ts b/core/src/components/datetime/test/first-day-of-week/e2e.ts new file mode 100644 index 00000000000..548ae2d24f7 --- /dev/null +++ b/core/src/components/datetime/test/first-day-of-week/e2e.ts @@ -0,0 +1,15 @@ +import { newE2EPage } from '@stencil/core/testing'; + +test('first-day-of-week', async () => { + const page = await newE2EPage({ + url: '/src/components/datetime/test/first-day-of-week?ionic:_testing=true' + }); + + const screenshotCompares = []; + + screenshotCompares.push(await page.compareScreenshot()); + + for (const screenshotCompare of screenshotCompares) { + expect(screenshotCompare).toMatchScreenshot(); + } +}); diff --git a/core/src/components/datetime/test/first-day-of-week/index.html b/core/src/components/datetime/test/first-day-of-week/index.html new file mode 100644 index 00000000000..cbd9479df37 --- /dev/null +++ b/core/src/components/datetime/test/first-day-of-week/index.html @@ -0,0 +1,71 @@ + + + + + Datetime - First day of week + + + + + + + + + + + + Datetime - First day of week + + + +
+
+

Default

+ + Increase firstDayOfWeek +
+ FirstDayOfWeek: 1 +
+
+
+
+
+ + + diff --git a/core/src/components/datetime/usage/angular.md b/core/src/components/datetime/usage/angular.md index 97375206d72..fc1bc64d270 100644 --- a/core/src/components/datetime/usage/angular.md +++ b/core/src/components/datetime/usage/angular.md @@ -30,7 +30,10 @@ - + + + + diff --git a/core/src/components/datetime/usage/javascript.md b/core/src/components/datetime/usage/javascript.md index 8b40cede0ae..c8cfd0704b1 100644 --- a/core/src/components/datetime/usage/javascript.md +++ b/core/src/components/datetime/usage/javascript.md @@ -32,6 +32,9 @@ + + +
My Custom Title
diff --git a/core/src/components/datetime/usage/react.md b/core/src/components/datetime/usage/react.md index dda79ca4e57..30f25c0e274 100644 --- a/core/src/components/datetime/usage/react.md +++ b/core/src/components/datetime/usage/react.md @@ -58,6 +58,9 @@ export const DateTimeExamples: React.FC = () => { {/* Custom Hour Cycle */} + + {/* Custom first day of week */} + {/* Custom title */} diff --git a/core/src/components/datetime/usage/stencil.md b/core/src/components/datetime/usage/stencil.md index 5a767caf670..26fee8d1246 100644 --- a/core/src/components/datetime/usage/stencil.md +++ b/core/src/components/datetime/usage/stencil.md @@ -56,6 +56,9 @@ export class DatetimeExample { {/* Custom Hour Cycle */} , + + {/* Custom first day of week */} + , {/* Custom title */} diff --git a/core/src/components/datetime/usage/vue.md b/core/src/components/datetime/usage/vue.md index 43fefa05e67..780da2d984e 100644 --- a/core/src/components/datetime/usage/vue.md +++ b/core/src/components/datetime/usage/vue.md @@ -32,6 +32,9 @@ + + + diff --git a/core/src/components/datetime/utils/data.ts b/core/src/components/datetime/utils/data.ts index bce2d0a7c00..6fcb37a3461 100644 --- a/core/src/components/datetime/utils/data.ts +++ b/core/src/components/datetime/utils/data.ts @@ -52,11 +52,11 @@ const hour23 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 * MD should display days such as "M" * or "T". */ -export const getDaysOfWeek = (locale: string, mode: Mode) => { +export const getDaysOfWeek = (locale: string, mode: Mode, firstDayOfWeek = 0) => { /** * Nov 1st, 2020 starts on a Sunday. - * ion-datetime assumes weeks start - * on Sunday. + * ion-datetime assumes weeks start on Sunday, + * but is configurable via `firstDayOfWeek`. */ const weekdayFormat = mode === 'ios' ? 'short' : 'narrow'; const intl = new Intl.DateTimeFormat(locale, { weekday: weekdayFormat }) @@ -67,7 +67,7 @@ export const getDaysOfWeek = (locale: string, mode: Mode) => { * For each day of the week, * get the day name. */ - for (let i = 0; i < 7; i++) { + for (let i = firstDayOfWeek; i < firstDayOfWeek + 7; i++) { const currentDate = new Date(startDate); currentDate.setDate(currentDate.getDate() + i); @@ -81,11 +81,33 @@ export const getDaysOfWeek = (locale: string, mode: Mode) => { * Returns an array containing all of the * days in a month for a given year. Values are * aligned with a week calendar starting on - * Sunday using null values. + * the firstDayOfWeek value (Sunday by default) + * using null values. */ -export const getDaysOfMonth = (month: number, year: number) => { +export const getDaysOfMonth = (month: number, year: number, firstDayOfWeek: number) => { const numDays = getNumDaysInMonth(month, year); - const offset = new Date(`${month}/1/${year}`).getDay() - 1; + const firstOfMonth = new Date(`${month}/1/${year}`).getDay(); + + /** + * To get the first day of the month aligned on the correct + * day of the week, we need to determine how many "filler" days + * to generate. These filler days as empty/disabled buttons + * that fill the space of the days of the week before the first + * of the month. + * + * There are two cases here: + * + * 1. If firstOfMonth = 4, firstDayOfWeek = 0 then the offset + * is (4 - (0 + 1)) = 3. Since the offset loop goes from 0 to 3 inclusive, + * this will generate 4 filler days (0, 1, 2, 3), and then day of week 4 will have + * the first day of the month. + * + * 2. If firstOfMonth = 2, firstDayOfWeek = 4 then the offset + * is (6 - (4 - 2)) = 4. Since the offset loop goes from 0 to 4 inclusive, + * this will generate 5 filler days (0, 1, 2, 3, 4), and then day of week 5 will have + * the first day of the month. + */ + const offset = firstOfMonth >= firstDayOfWeek ? firstOfMonth - (firstDayOfWeek + 1) : 6 - (firstDayOfWeek - firstOfMonth); let days = []; for (let i = 1; i <= numDays; i++) { diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 302c2e54e8d..ecd96d04482 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -281,6 +281,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer('ion-d 'hourValues', 'minuteValues', 'locale', + 'firstDayOfWeek', 'value', 'showDefaultTitle', 'showDefaultButtons',