From 72c77446c404e4c69ec080e53b7a5be618a555bb Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Wed, 11 Nov 2020 17:31:16 -0500 Subject: [PATCH 1/5] Fix docs --- docs/components/format-number.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/components/format-number.md b/docs/components/format-number.md index 782885c92..7fc117324 100644 --- a/docs/components/format-number.md +++ b/docs/components/format-number.md @@ -4,7 +4,7 @@ Formats a number using the specified locale and options. -Localization is handled by the browser's built-in [Intl: NumberFormat API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) so there's no need to load bulky language packs. +Localization is handled by the browser's [Intl.NumberFormat API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) so there's no need to load language packs. ```html preview
From 8a08302bb758c46ec508325bea3a5f2316e0cc58 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Wed, 11 Nov 2020 17:31:53 -0500 Subject: [PATCH 2/5] Scaffold relative time component --- docs/_sidebar.md | 1 + docs/components/relative-time.md | 61 +++++++++++++++++++ docs/getting-started/changelog.md | 1 + src/components.d.ts | 45 ++++++++++++++ .../relative-time/relative-time.tsx | 45 ++++++++++++++ 5 files changed, 153 insertions(+) create mode 100644 docs/components/relative-time.md create mode 100644 src/components/relative-time/relative-time.tsx diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 58d94001c..7f71c10ea 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -52,6 +52,7 @@ - [Format Bytes](/components/format-bytes.md) - [Format Number](/components/format-number.md) - [Include](/components/include.md) + - [Relative Time](/components/relative-time.md) - [Resize Observer](/components/resize-observer.md) - [Theme](/components/theme.md) diff --git a/docs/components/relative-time.md b/docs/components/relative-time.md new file mode 100644 index 000000000..5561b70cd --- /dev/null +++ b/docs/components/relative-time.md @@ -0,0 +1,61 @@ +# Relative Time + +[component-header:sl-relative-time] + +Outputs a localized time phrase relative to the current time. + +Localization is handled by the browser's [Intl.RelativeTimeFormat API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat) so there'e no need to load language packs. + +```html preview +
+``` + +The `date` prop must be a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object or a string that [`Date.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse) can interpret. When using strings, avoid ambiguous dates such as `03/04/2020` which can be interpreted as March 4 or April 3 depending on the user's browser and locale. Instead, use a valid [ISO 8601 date time string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#Date_Time_String_Format) to ensure proper parsing. + +?> [The Intl.RelativeTimeFormat API is available in all major browsers](https://caniuse.com/mdn-javascript_builtins_intl_relativetimeformat), but it first became available to Safari in version 14. If you need to support Safari 13, you'll need to [use a polyfill](https://github.com/catamphetamine/relative-time-format). + +## Examples + +### Keeping Time in Sync + +Use the `sync` attribute to update the displayed value as time passes. + +```html preview +
+ +

+ Reset +
+ + +``` + +### Formatting Styles + +You can change the way times are formatted with the `format` attribute. Note that some locales may show the same result for `narrow` and `short` formats. + +```html preview +
+
+ +``` + +### Localization + +Use the `locale` attribute to set the desired locale. + +```html preview +English:
+Chinese:
+German:
+Russian: +``` + +[component-metadata:sl-relative-time] diff --git a/docs/getting-started/changelog.md b/docs/getting-started/changelog.md index 664cea8cc..02ab789dd 100644 --- a/docs/getting-started/changelog.md +++ b/docs/getting-started/changelog.md @@ -9,6 +9,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis ## Next - Added `sl-format-number` component +- Added `sl-relative-time` component - Added `closable` prop to `sl-tab` - Added experimental `sl-resize-observer` utility - Added experimental `sl-theme` utility and updated theming documentation diff --git a/src/components.d.ts b/src/components.d.ts index eaf719dd9..8711be7d9 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -897,6 +897,24 @@ export namespace Components { */ "value": number; } + interface SlRelativeTime { + /** + * The date from which to calculate time from. + */ + "date": Date | string; + /** + * The formatting style to use. + */ + "format": 'long' | 'short' | 'narrow'; + /** + * The locale to use when formatting the number. + */ + "locale": string; + /** + * When `auto`, values such as "yesterday" and "tomorrow" will be shown when possible. When `always`, values such as "1 day ago" and "in 1 day" will be shown. + */ + "numeric": 'always' | 'auto'; + } interface SlResizeObserver { } interface SlResponsiveEmbed { @@ -1434,6 +1452,12 @@ declare global { prototype: HTMLSlRatingElement; new (): HTMLSlRatingElement; }; + interface HTMLSlRelativeTimeElement extends Components.SlRelativeTime, HTMLStencilElement { + } + var HTMLSlRelativeTimeElement: { + prototype: HTMLSlRelativeTimeElement; + new (): HTMLSlRelativeTimeElement; + }; interface HTMLSlResizeObserverElement extends Components.SlResizeObserver, HTMLStencilElement { } var HTMLSlResizeObserverElement: { @@ -1544,6 +1568,7 @@ declare global { "sl-radio": HTMLSlRadioElement; "sl-range": HTMLSlRangeElement; "sl-rating": HTMLSlRatingElement; + "sl-relative-time": HTMLSlRelativeTimeElement; "sl-resize-observer": HTMLSlResizeObserverElement; "sl-responsive-embed": HTMLSlResponsiveEmbedElement; "sl-select": HTMLSlSelectElement; @@ -2477,6 +2502,24 @@ declare namespace LocalJSX { */ "value"?: number; } + interface SlRelativeTime { + /** + * The date from which to calculate time from. + */ + "date"?: Date | string; + /** + * The formatting style to use. + */ + "format"?: 'long' | 'short' | 'narrow'; + /** + * The locale to use when formatting the number. + */ + "locale"?: string; + /** + * When `auto`, values such as "yesterday" and "tomorrow" will be shown when possible. When `always`, values such as "1 day ago" and "in 1 day" will be shown. + */ + "numeric"?: 'always' | 'auto'; + } interface SlResizeObserver { /** * Emitted when the element is resized. @@ -2862,6 +2905,7 @@ declare namespace LocalJSX { "sl-radio": SlRadio; "sl-range": SlRange; "sl-rating": SlRating; + "sl-relative-time": SlRelativeTime; "sl-resize-observer": SlResizeObserver; "sl-responsive-embed": SlResponsiveEmbed; "sl-select": SlSelect; @@ -2912,6 +2956,7 @@ declare module "@stencil/core" { "sl-radio": LocalJSX.SlRadio & JSXBase.HTMLAttributes; "sl-range": LocalJSX.SlRange & JSXBase.HTMLAttributes; "sl-rating": LocalJSX.SlRating & JSXBase.HTMLAttributes; + "sl-relative-time": LocalJSX.SlRelativeTime & JSXBase.HTMLAttributes; "sl-resize-observer": LocalJSX.SlResizeObserver & JSXBase.HTMLAttributes; "sl-responsive-embed": LocalJSX.SlResponsiveEmbed & JSXBase.HTMLAttributes; "sl-select": LocalJSX.SlSelect & JSXBase.HTMLAttributes; diff --git a/src/components/relative-time/relative-time.tsx b/src/components/relative-time/relative-time.tsx new file mode 100644 index 000000000..336105b21 --- /dev/null +++ b/src/components/relative-time/relative-time.tsx @@ -0,0 +1,45 @@ +import { Component, Prop, State, h } from '@stencil/core'; + +/** + * @since 2.0 + * @status stable + */ + +@Component({ + tag: 'sl-relative-time', + shadow: true +}) +export class RelativeTime { + @State() displayTime = ''; + + /** The date from which to calculate time from. */ + @Prop() date: Date | string; + + /** The locale to use when formatting the number. */ + @Prop() locale: string; + + /** The formatting style to use. */ + @Prop() format: 'long' | 'short' | 'narrow' = 'long'; + + /** + * When `auto`, values such as "yesterday" and "tomorrow" will be shown when possible. When `always`, values such as + * "1 day ago" and "in 1 day" will be shown. + */ + @Prop() numeric: 'always' | 'auto' = 'auto'; + + render() { + const date = new Date(this.date); + + if (isNaN(date.getSeconds())) { + return ''; + } + + // // @ts-ignore - https://caniuse.com/mdn-javascript_builtins_intl_relativetimeformat + // new Intl.RelativeTimeFormat(this.locale, { + // numeric: this.numeric, + // style: this.type + // }).format(this.value, this.unit); + + return ; + } +} From 6e759cc50473986e00cb9b91f6b2b876886c1e0f Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Fri, 20 Nov 2020 16:37:56 -0500 Subject: [PATCH 3/5] Update docs --- docs/components/format-number.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/components/format-number.md b/docs/components/format-number.md index 7fc117324..b18e92737 100644 --- a/docs/components/format-number.md +++ b/docs/components/format-number.md @@ -4,7 +4,7 @@ Formats a number using the specified locale and options. -Localization is handled by the browser's [Intl.NumberFormat API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) so there's no need to load language packs. +Localization is handled by the browser's [Intl.NumberFormat API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat). No language packs are required. ```html preview
From 410b39ee59e6b85387638f7b5127a717d729dcd5 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Fri, 20 Nov 2020 17:02:03 -0500 Subject: [PATCH 4/5] Update docs --- docs/components/format-number.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/components/format-number.md b/docs/components/format-number.md index b18e92737..f96c3cab5 100644 --- a/docs/components/format-number.md +++ b/docs/components/format-number.md @@ -4,7 +4,7 @@ Formats a number using the specified locale and options. -Localization is handled by the browser's [Intl.NumberFormat API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat). No language packs are required. +Localization is handled by the browser's [`Intl.NumberFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat). No language packs are required. ```html preview
From 058e8fa08ed3764af88ab3c3783be7f93c8c9f64 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Fri, 20 Nov 2020 17:02:38 -0500 Subject: [PATCH 5/5] Finish up sl-relative-time --- docs/components/relative-time.md | 36 +++--- src/components.d.ts | 8 ++ .../relative-time/relative-time.tsx | 103 ++++++++++++++++-- 3 files changed, 117 insertions(+), 30 deletions(-) diff --git a/docs/components/relative-time.md b/docs/components/relative-time.md index 5561b70cd..2738a7853 100644 --- a/docs/components/relative-time.md +++ b/docs/components/relative-time.md @@ -2,49 +2,46 @@ [component-header:sl-relative-time] -Outputs a localized time phrase relative to the current time. +Outputs a localized time phrase relative to the current date and time. -Localization is handled by the browser's [Intl.RelativeTimeFormat API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat) so there'e no need to load language packs. +Localization is handled by the browser's [`Intl.RelativeTimeFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat). No language packs are required. ```html preview -
+ +
``` -The `date` prop must be a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object or a string that [`Date.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse) can interpret. When using strings, avoid ambiguous dates such as `03/04/2020` which can be interpreted as March 4 or April 3 depending on the user's browser and locale. Instead, use a valid [ISO 8601 date time string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#Date_Time_String_Format) to ensure proper parsing. +The `date` prop determines when the date/time is calculated from. It must be a string that [`Date.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse) can interpret or a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object set via JavaScript. When using strings, avoid ambiguous dates such as `03/04/2020` which can be interpreted as March 4 or April 3 depending on the user's browser and locale. Instead, always use a valid [ISO 8601 date time string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#Date_Time_String_Format) to ensure the date will be parsed properly by all clients. -?> [The Intl.RelativeTimeFormat API is available in all major browsers](https://caniuse.com/mdn-javascript_builtins_intl_relativetimeformat), but it first became available to Safari in version 14. If you need to support Safari 13, you'll need to [use a polyfill](https://github.com/catamphetamine/relative-time-format). +?> [The `Intl.RelativeTimeFormat` API is available in all major browsers](https://caniuse.com/mdn-javascript_builtins_intl_relativetimeformat), but it first became available to Safari in version 14. If you need to support Safari 13, you'll need to [use a polyfill](https://github.com/catamphetamine/relative-time-format). ## Examples ### Keeping Time in Sync -Use the `sync` attribute to update the displayed value as time passes. +Use the `sync` attribute to update the displayed value automatically as time passes. ```html preview
-

- Reset
``` ### Formatting Styles -You can change the way times are formatted with the `format` attribute. Note that some locales may show the same result for `narrow` and `short` formats. +You can change how the time is displayed using the `format` attribute. Note that some locales may display the same values for `narrow` and `short` formats. ```html preview -
-
- +
+
+ ``` ### Localization @@ -52,10 +49,11 @@ You can change the way times are formatted with the `format` attribute. Note tha Use the `locale` attribute to set the desired locale. ```html preview -English:
-Chinese:
-German:
-Russian: +English:
+Chinese:
+German:
+Greek:
+Russian: ``` [component-metadata:sl-relative-time] diff --git a/src/components.d.ts b/src/components.d.ts index 8711be7d9..63ee7713a 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -914,6 +914,10 @@ export namespace Components { * When `auto`, values such as "yesterday" and "tomorrow" will be shown when possible. When `always`, values such as "1 day ago" and "in 1 day" will be shown. */ "numeric": 'always' | 'auto'; + /** + * Keep the displayed value up to date as time passes. + */ + "sync": boolean; } interface SlResizeObserver { } @@ -2519,6 +2523,10 @@ declare namespace LocalJSX { * When `auto`, values such as "yesterday" and "tomorrow" will be shown when possible. When `always`, values such as "1 day ago" and "in 1 day" will be shown. */ "numeric"?: 'always' | 'auto'; + /** + * Keep the displayed value up to date as time passes. + */ + "sync"?: boolean; } interface SlResizeObserver { /** diff --git a/src/components/relative-time/relative-time.tsx b/src/components/relative-time/relative-time.tsx index 336105b21..1dfd9d492 100644 --- a/src/components/relative-time/relative-time.tsx +++ b/src/components/relative-time/relative-time.tsx @@ -1,4 +1,4 @@ -import { Component, Prop, State, h } from '@stencil/core'; +import { Component, Prop, State, Watch, h } from '@stencil/core'; /** * @since 2.0 @@ -10,7 +10,11 @@ import { Component, Prop, State, h } from '@stencil/core'; shadow: true }) export class RelativeTime { - @State() displayTime = ''; + updateTimeout: any; + + @State() isoTime = ''; + @State() relativeTime = ''; + @State() titleTime = ''; /** The date from which to calculate time from. */ @Prop() date: Date | string; @@ -27,19 +31,96 @@ export class RelativeTime { */ @Prop() numeric: 'always' | 'auto' = 'auto'; - render() { + /** Keep the displayed value up to date as time passes. */ + @Prop() sync = false; + + connectedCallback() { + this.updateTime(); + } + + disconnectedCallback() { + clearTimeout(this.updateTimeout); + } + + @Watch('date') + @Watch('locale') + @Watch('format') + @Watch('numeric') + @Watch('sync') + updateTime() { + const now = new Date(); const date = new Date(this.date); - if (isNaN(date.getSeconds())) { - return ''; + // Check for an invalid date + if (isNaN(date.getMilliseconds())) { + this.relativeTime = ''; + this.isoTime = ''; + return; } - // // @ts-ignore - https://caniuse.com/mdn-javascript_builtins_intl_relativetimeformat - // new Intl.RelativeTimeFormat(this.locale, { - // numeric: this.numeric, - // style: this.type - // }).format(this.value, this.unit); + const diff = +date - +now; + const availableUnits = [ + { max: 2760000, value: 60000, unit: 'minute' }, // max 46 minutes + { max: 72000000, value: 3600000, unit: 'hour' }, // max 20 hours + { max: 518400000, value: 86400000, unit: 'day' }, // max 6 days + { max: 2419200000, value: 604800000, unit: 'week' }, // max 28 days + { max: 28512000000, value: 2592000000, unit: 'month' }, // max 11 months + { max: Infinity, value: 31536000000, unit: 'year' } + ]; + const { unit, value } = availableUnits.find(unit => Math.abs(diff) < unit.max); - return ; + this.isoTime = date.toISOString(); + this.titleTime = new Intl.DateTimeFormat(this.locale, { + month: 'long', + year: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + timeZoneName: 'short' + }).format(date); + + // @ts-ignore - https://github.com/microsoft/TypeScript/issues/29129 + this.relativeTime = new Intl.RelativeTimeFormat(this.locale, { + numeric: this.numeric, + style: this.format + }).format(Math.round(diff / value), unit); + + // If sync is enabled, update as time passes + clearTimeout(this.updateTimeout); + if (this.sync) { + // Calculates the number of milliseconds until the next respective unit changes. This ensures that all components + // update at the same time which is less distracting than updating independently. + const getTimeUntilNextUnit = (unit: 'second' | 'minute' | 'hour' | 'day') => { + const units = { second: 1000, minute: 60000, hour: 3600000, day: 86400000 }; + const value = units[unit]; + return value - (now.getTime() % value); + }; + + let nextInterval: number; + + // NOTE: this could be optimized to determine when the next update should actually occur, but the size and cost of + // that logic probably isn't worth the performance benefit + if (unit === 'minute') { + nextInterval = getTimeUntilNextUnit('second'); + } else if (unit === 'hour') { + nextInterval = getTimeUntilNextUnit('minute'); + } else if (unit === 'day') { + nextInterval = getTimeUntilNextUnit('hour'); + } else { + // Cap updates at once per day. It's unlikely a user will reach this value, plus setTimeout has a limit on the + // value it can accept. https://stackoverflow.com/a/3468650/567486 + nextInterval = getTimeUntilNextUnit('day'); // next day + } + + this.updateTimeout = setTimeout(() => this.updateTime(), nextInterval); + } + } + + render() { + return ( + + ); } }