Skip to content

Commit

Permalink
feat(datetime): add firstDayOfWeek property (#23692)
Browse files Browse the repository at this point in the history
resolves #23556

Co-authored-by: Liam DeBeasi <[email protected]>
  • Loading branch information
EinfachHans and liamdebeasi authored Aug 17, 2021
1 parent bc4e826 commit ea348f0
Show file tree
Hide file tree
Showing 15 changed files with 186 additions and 15 deletions.
4 changes: 2 additions & 2 deletions angular/src/directives/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<ng-content></ng-content>", 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: "<ng-content></ng-content>", 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<CustomEvent>;
ionChange!: EventEmitter<CustomEvent>;
Expand Down
1 change: 1 addition & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*/
Expand Down
10 changes: 8 additions & 2 deletions core/src/components/datetime/datetime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -1305,7 +1311,7 @@ export class Datetime implements ComponentInterface {
</div>
</div>
<div class="calendar-days-of-week">
{getDaysOfWeek(this.locale, mode).map(d => {
{getDaysOfWeek(this.locale, mode, this.firstDayOfWeek % 7).map(d => {
return <div class="day-of-week">{d}</div>
})}
</div>
Expand All @@ -1320,7 +1326,7 @@ export class Datetime implements ComponentInterface {
return (
<div class="calendar-month">
<div class="calendar-month-grid">
{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);
Expand Down
34 changes: 31 additions & 3 deletions core/src/components/datetime/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<ion-datetime hour-cycle="h12" locale="en-GB"></ion-datetime>
Expand All @@ -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
<ion-datetime first-day-of-week="1"></ion-datetime>
```

## 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.
Expand Down Expand Up @@ -210,7 +222,10 @@ dates in JavaScript.
<ion-datetime size="cover"></ion-datetime>

<!-- Custom Hour Cycle -->
<ion-datetime hour-cycle="h23"></ion-datetime>
<ion-datetime hourCycle="h23"></ion-datetime>

<!-- Custom first day of week -->
<ion-datetime [firstDayOfWeek]="1"></ion-datetime>

<!-- Custom title -->
<ion-datetime>
Expand Down Expand Up @@ -297,6 +312,9 @@ export class MyComponent {
<!-- Custom Hour Cycle -->
<ion-datetime hour-cycle="h23"></ion-datetime>

<!-- Custom first day of week -->
<ion-datetime first-day-of-week="1"></ion-datetime>

<!-- Custom title -->
<ion-datetime>
<div slot="title">My Custom Title</div>
Expand Down Expand Up @@ -393,6 +411,9 @@ export const DateTimeExamples: React.FC = () => {

{/* Custom Hour Cycle */}
<IonDatetime hourCycle="h23"></IonDatetime>

{/* Custom first day of week */}
<IonDatetime firstDayOfWeek={1}></IonDatetime>

{/* Custom title */}
<IonDatetime>
Expand Down Expand Up @@ -480,6 +501,9 @@ export class DatetimeExample {

{/* Custom Hour Cycle */}
<ion-datetime hourCycle="h23"></ion-datetime>,

{/* Custom first day of week */}
<ion-datetime firstDayOfWeek={1}></ion-datetime>,

{/* Custom title */}
<ion-datetime>
Expand Down Expand Up @@ -543,6 +567,9 @@ export class DatetimeExample {

<!-- Custom Hour Cycle -->
<ion-datetime hour-cycle="h23"></ion-datetime>

<!-- Custom first day of week -->
<ion-datetime first-day-of-week="1"></ion-datetime>

<!-- Custom title -->
<ion-datetime>
Expand Down Expand Up @@ -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'` |
Expand Down
4 changes: 4 additions & 0 deletions core/src/components/datetime/test/data.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()', () => {
Expand Down
15 changes: 15 additions & 0 deletions core/src/components/datetime/test/first-day-of-week/e2e.ts
Original file line number Diff line number Diff line change
@@ -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();
}
});
71 changes: 71 additions & 0 deletions core/src/components/datetime/test/first-day-of-week/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Datetime - First day of week</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(1, minmax(250px, 1fr));
grid-gap: 60px 20px;
}
h2 {
font-size: 12px;
font-weight: normal;

color: #6f7378;

margin-top: 10px;
margin-left: 5px;
}

@media screen and (max-width: 800px) {
.grid {
grid-template-columns: 1fr;
padding: 0;
}
}

ion-datetime {
box-shadow: 0px 16px 32px rgba(0, 0, 0, 0.25), 0px 8px 16px rgba(0, 0, 0, 0.25);
border-radius: 8px;
}
</style>
</head>
<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Datetime - First day of week</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>Default</h2>
<ion-datetime first-day-of-week="1"></ion-datetime>
<ion-button onclick="increase()">Increase firstDayOfWeek</ion-button>
<div>
<span>FirstDayOfWeek: <span id="start-of-week">1</span></span>
</div>
</div>
</div>
</ion-content>
</ion-app>
</body>
<script>
function increase() {
const datetime = document.querySelector('ion-datetime');
datetime.firstDayOfWeek = datetime.firstDayOfWeek + 1;

const span = document.getElementById('start-of-week');
span.innerText = datetime.firstDayOfWeek;
}
</script>
</html>
5 changes: 4 additions & 1 deletion core/src/components/datetime/usage/angular.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
<ion-datetime size="cover"></ion-datetime>

<!-- Custom Hour Cycle -->
<ion-datetime hour-cycle="h23"></ion-datetime>
<ion-datetime hourCycle="h23"></ion-datetime>

<!-- Custom first day of week -->
<ion-datetime [firstDayOfWeek]="1"></ion-datetime>

<!-- Custom title -->
<ion-datetime>
Expand Down
3 changes: 3 additions & 0 deletions core/src/components/datetime/usage/javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
<!-- Custom Hour Cycle -->
<ion-datetime hour-cycle="h23"></ion-datetime>

<!-- Custom first day of week -->
<ion-datetime first-day-of-week="1"></ion-datetime>

<!-- Custom title -->
<ion-datetime>
<div slot="title">My Custom Title</div>
Expand Down
3 changes: 3 additions & 0 deletions core/src/components/datetime/usage/react.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ export const DateTimeExamples: React.FC = () => {

{/* Custom Hour Cycle */}
<IonDatetime hourCycle="h23"></IonDatetime>

{/* Custom first day of week */}
<IonDatetime firstDayOfWeek={1}></IonDatetime>

{/* Custom title */}
<IonDatetime>
Expand Down
3 changes: 3 additions & 0 deletions core/src/components/datetime/usage/stencil.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export class DatetimeExample {

{/* Custom Hour Cycle */}
<ion-datetime hourCycle="h23"></ion-datetime>,

{/* Custom first day of week */}
<ion-datetime firstDayOfWeek={1}></ion-datetime>,

{/* Custom title */}
<ion-datetime>
Expand Down
3 changes: 3 additions & 0 deletions core/src/components/datetime/usage/vue.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@

<!-- Custom Hour Cycle -->
<ion-datetime hour-cycle="h23"></ion-datetime>

<!-- Custom first day of week -->
<ion-datetime first-day-of-week="1"></ion-datetime>

<!-- Custom title -->
<ion-datetime>
Expand Down
36 changes: 29 additions & 7 deletions core/src/components/datetime/utils/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand All @@ -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);

Expand All @@ -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++) {
Expand Down
1 change: 1 addition & 0 deletions packages/vue/src/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer<JSX.IonDatetime>('ion-d
'hourValues',
'minuteValues',
'locale',
'firstDayOfWeek',
'value',
'showDefaultTitle',
'showDefaultButtons',
Expand Down

0 comments on commit ea348f0

Please sign in to comment.