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')
+ })
+ })
})