diff --git a/README.md b/README.md index 4f835ae..4810707 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ So, a relative date phrase is used for up to a month and then the actual date is | `month` | `month` | `'numeric'\|'2-digit'\|'short'\|'long'\|'narrow'\|undefined` | *** | | `year` | `year` | `'numeric'\|'2-digit'\|undefined` | **** | | `timeZoneName` | `time-zone-name` | `'long'\|'short'\|'shortOffset'\|'longOffset'` `\|'shortGeneric'\|'longGeneric'\|undefined` | `undefined` | +| `timeZone` | `time-zone` | `string\|undefined` | Browser default time zone | | `noTitle` | `no-title` | `-` | `-` | *: If unspecified, `formatStyle` will return `'narrow'` if `format` is `'elapsed'` or `'micro'`, `'short'` if the format is `'relative'` or `'datetime'`, otherwise it will be `'long'`. @@ -139,6 +140,19 @@ The `duration` format will display the time remaining (or elapsed time) from the - `4 hours` - `8 days, 30 minutes, 1 second` +##### time-zone (`string`) + +The`time-zone` attribute allows you to specify the IANA time zone name (e.g., `America/New_York`, `Europe/London`) used for formatting the date and time. + +You can set the time zone either as an attribute or property: +```html + + June 1, 2024 8:00am EDT + +``` + +If the individual element does not have a `time-zone` attribute then it will traverse upwards in the tree to find the closest element that does, or default the `time-zone` to the browsers default. + ###### Deprecated Formats ###### `format=elapsed` diff --git a/src/relative-time-element.ts b/src/relative-time-element.ts index 80dc977..7af34a9 100644 --- a/src/relative-time-element.ts +++ b/src/relative-time-element.ts @@ -90,6 +90,14 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor } } + get timeZone() { + // Prefer attribute, then closest, then document + const tz = + this.closest('[time-zone]')?.getAttribute('time-zone') || + this.ownerDocument.documentElement.getAttribute('time-zone') + return tz || undefined + } + #renderRoot: Node = this.shadowRoot ? this.shadowRoot : this.attachShadow ? this.attachShadow({mode: 'open'}) : this static get observedAttributes() { @@ -113,6 +121,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor 'lang', 'title', 'aria-hidden', + 'time-zone', ] } @@ -129,6 +138,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor hour: 'numeric', minute: '2-digit', timeZoneName: 'short', + timeZone: this.timeZone, }).format(date) } @@ -198,6 +208,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor month: this.month, year: this.year, timeZoneName: this.timeZoneName, + timeZone: this.timeZone, }) return `${this.prefix} ${formatter.format(date)}`.trim() } diff --git a/test/relative-time.js b/test/relative-time.js index de5c102..67e5cd7 100644 --- a/test/relative-time.js +++ b/test/relative-time.js @@ -2586,4 +2586,62 @@ suite('relative-time', function () { }) } }) + + suite('[timeZone]', function () { + test('updates when the time-zone attribute is set', async () => { + const el = document.createElement('relative-time') + el.setAttribute('datetime', '2020-01-01T12:00:00.000Z') + el.setAttribute('time-zone', 'America/New_York') + el.setAttribute('format', 'datetime') + el.setAttribute('hour', 'numeric') + el.setAttribute('minute', '2-digit') + el.setAttribute('second', '2-digit') + el.setAttribute('time-zone-name', 'longGeneric') + await Promise.resolve() + assert.equal(el.shadowRoot.textContent, 'Wed, Jan 1, 2020, 7:00:00 AM Eastern Time') + }) + + test('updates when the time-zone attribute changes', async () => { + const el = document.createElement('relative-time') + el.setAttribute('datetime', '2020-01-01T12:00:00.000Z') + el.setAttribute('time-zone', 'America/New_York') + el.setAttribute('format', 'datetime') + el.setAttribute('hour', 'numeric') + el.setAttribute('minute', '2-digit') + el.setAttribute('second', '2-digit') + await Promise.resolve() + const initial = el.shadowRoot.textContent + el.setAttribute('time-zone', 'Asia/Tokyo') + await Promise.resolve() + assert.notEqual(el.shadowRoot.textContent, initial) + assert.equal(el.shadowRoot.textContent, 'Wed, Jan 1, 2020, 9:00:00 PM') + }) + + test('ignores empty time-zone attributes', async () => { + const el = document.createElement('relative-time') + el.setAttribute('datetime', '2020-01-01T12:00:00.000Z') + el.setAttribute('time-zone', '') + el.setAttribute('format', 'datetime') + el.setAttribute('hour', 'numeric') + el.setAttribute('minute', '2-digit') + el.setAttribute('second', '2-digit') + await Promise.resolve() + // Should fallback to default or system time zone + assert.equal(el.shadowRoot.textContent, 'Wed, Jan 1, 2020, 4:00:00 PM') + }) + + test('uses html time-zone if element time-zone is empty', async () => { + const time = document.createElement('relative-time') + time.setAttribute('datetime', '2020-01-01T12:00:00.000Z') + time.setAttribute('time-zone', '') + document.documentElement.setAttribute('time-zone', 'Asia/Tokyo') + time.setAttribute('format', 'datetime') + time.setAttribute('hour', 'numeric') + time.setAttribute('minute', '2-digit') + time.setAttribute('second', '2-digit') + await Promise.resolve() + assert.equal(time.shadowRoot.textContent, 'Wed, Jan 1, 2020, 9:00:00 PM') + document.documentElement.removeAttribute('time-zone') + }) + }) })