Merge branch 'relative-time' into next

This commit is contained in:
Cory LaViska
2020-11-20 17:02:59 -05:00
6 changed files with 241 additions and 1 deletions

View File

@@ -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)

View File

@@ -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). No language packs are required.
```html preview
<div class="format-number-overview">

View File

@@ -0,0 +1,59 @@
# Relative Time
[component-header:sl-relative-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). No language packs are required.
```html preview
<!-- Shoelace 2 release date 🎉 -->
<sl-relative-time date="2020-07-15T09:17:00-04:00"></sl-relative-time><br>
```
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).
## Examples
### Keeping Time in Sync
Use the `sync` attribute to update the displayed value automatically as time passes.
```html preview
<div class="relative-time-sync">
<sl-relative-time sync></sl-relative-time>
</div>
<script>
const container = document.querySelector('.relative-time-sync');
const relativeTime = container.querySelector('sl-relative-time');
relativeTime.date = new Date(new Date().getTime() - 60000);
</script>
```
### Formatting Styles
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
<sl-relative-time date="2020-07-15T09:17:00-04:00" format="narrow"></sl-relative-time><br>
<sl-relative-time date="2020-07-15T09:17:00-04:00" format="short"></sl-relative-time><br>
<sl-relative-time date="2020-07-15T09:17:00-04:00" format="long"></sl-relative-time>
```
### Localization
Use the `locale` attribute to set the desired locale.
```html preview
English: <sl-relative-time date="2020-07-15T09:17:00-04:00" locale="en-US"></sl-relative-time><br>
Chinese: <sl-relative-time date="2020-07-15T09:17:00-04:00" locale="zh-CN"></sl-relative-time><br>
German: <sl-relative-time date="2020-07-15T09:17:00-04:00" locale="de"></sl-relative-time><br>
Greek: <sl-relative-time date="2020-07-15T09:17:00-04:00" locale="el"></sl-relative-time><br>
Russian: <sl-relative-time date="2020-07-15T09:17:00-04:00" locale="ru"></sl-relative-time>
```
[component-metadata:sl-relative-time]

View File

@@ -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

53
src/components.d.ts vendored
View File

@@ -897,6 +897,28 @@ 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';
/**
* Keep the displayed value up to date as time passes.
*/
"sync": boolean;
}
interface SlResizeObserver {
}
interface SlResponsiveEmbed {
@@ -1434,6 +1456,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 +1572,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 +2506,28 @@ 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';
/**
* Keep the displayed value up to date as time passes.
*/
"sync"?: boolean;
}
interface SlResizeObserver {
/**
* Emitted when the element is resized.
@@ -2862,6 +2913,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 +2964,7 @@ declare module "@stencil/core" {
"sl-radio": LocalJSX.SlRadio & JSXBase.HTMLAttributes<HTMLSlRadioElement>;
"sl-range": LocalJSX.SlRange & JSXBase.HTMLAttributes<HTMLSlRangeElement>;
"sl-rating": LocalJSX.SlRating & JSXBase.HTMLAttributes<HTMLSlRatingElement>;
"sl-relative-time": LocalJSX.SlRelativeTime & JSXBase.HTMLAttributes<HTMLSlRelativeTimeElement>;
"sl-resize-observer": LocalJSX.SlResizeObserver & JSXBase.HTMLAttributes<HTMLSlResizeObserverElement>;
"sl-responsive-embed": LocalJSX.SlResponsiveEmbed & JSXBase.HTMLAttributes<HTMLSlResponsiveEmbedElement>;
"sl-select": LocalJSX.SlSelect & JSXBase.HTMLAttributes<HTMLSlSelectElement>;

View File

@@ -0,0 +1,126 @@
import { Component, Prop, State, Watch, h } from '@stencil/core';
/**
* @since 2.0
* @status stable
*/
@Component({
tag: 'sl-relative-time',
shadow: true
})
export class RelativeTime {
updateTimeout: any;
@State() isoTime = '';
@State() relativeTime = '';
@State() titleTime = '';
/** 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';
/** 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);
// Check for an invalid date
if (isNaN(date.getMilliseconds())) {
this.relativeTime = '';
this.isoTime = '';
return;
}
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);
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 (
<time dateTime={this.isoTime} title={this.titleTime}>
{this.relativeTime}
</time>
);
}
}