From e3a6c3306492f72f7c6b884aebecc3924ef4e18a Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Oct 2025 00:56:28 +0100 Subject: [PATCH 01/11] Render 12/24 hour format according to user's preference Fixes: https://github.com/github/relative-time-element/issues/276 This has no direct opt-out but I think it may not need one because user preferences should always be respected imho. --- src/relative-time-element.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/relative-time-element.ts b/src/relative-time-element.ts index e3612ef..ec5ccb7 100644 --- a/src/relative-time-element.ts +++ b/src/relative-time-element.ts @@ -32,6 +32,14 @@ function getUnitFactor(el: RelativeTimeElement): number { return 60 * 60 * 1000 } +// Determine whether the user has a 12 (vs. 24) hour cycle preference. This relies on the hour formatting in +// a 12 hour preference being formatted like "1 AM" including a space, while with a 24 hour preference, the +// same is formatted as "01" without a space. In the future `Intl.Locale.prototype.getHourCycles()` could be +// used but in my testing it incorrectly returned a 12 hour preference with MacOS set to 24 hour format. +function isBrowser12hCycle() { + return Boolean(new Intl.DateTimeFormat([], {hour: 'numeric'}).format(0).match(/\s/)) +} + const dateObserver = new (class { elements: Set = new Set() time = Infinity @@ -130,6 +138,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor // value takes precedence over this custom format. // // Returns a formatted time String. + #getFormattedTitle(date: Date): string | undefined { return new Intl.DateTimeFormat(this.#lang, { day: 'numeric', @@ -139,6 +148,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor minute: '2-digit', timeZoneName: 'short', timeZone: this.timeZone, + hour12: isBrowser12hCycle(), }).format(date) } @@ -213,6 +223,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor year: this.year, timeZoneName: this.timeZoneName, timeZone: this.timeZone, + hour12: isBrowser12hCycle(), }) return `${this.prefix} ${formatter.format(date)}`.trim() } @@ -246,6 +257,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor minute: '2-digit', timeZoneName: 'short', timeZone: this.timeZone, + hour12: isBrowser12hCycle(), } if (this.#isToday(date)) { From 2942825c1a68d0e8866408c4d71453a126ef82d2 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Oct 2025 00:59:38 +0100 Subject: [PATCH 02/11] format --- src/relative-time-element.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/relative-time-element.ts b/src/relative-time-element.ts index ec5ccb7..9221a1f 100644 --- a/src/relative-time-element.ts +++ b/src/relative-time-element.ts @@ -138,7 +138,6 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor // value takes precedence over this custom format. // // Returns a formatted time String. - #getFormattedTitle(date: Date): string | undefined { return new Intl.DateTimeFormat(this.#lang, { day: 'numeric', From 85a9943ffbb026a380efcbf45dee67be9c6398fb Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Oct 2025 01:03:20 +0100 Subject: [PATCH 03/11] prefer RegExp.prototype.exec --- src/relative-time-element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/relative-time-element.ts b/src/relative-time-element.ts index 9221a1f..d1180a6 100644 --- a/src/relative-time-element.ts +++ b/src/relative-time-element.ts @@ -37,7 +37,7 @@ function getUnitFactor(el: RelativeTimeElement): number { // same is formatted as "01" without a space. In the future `Intl.Locale.prototype.getHourCycles()` could be // used but in my testing it incorrectly returned a 12 hour preference with MacOS set to 24 hour format. function isBrowser12hCycle() { - return Boolean(new Intl.DateTimeFormat([], {hour: 'numeric'}).format(0).match(/\s/)) + return Boolean(/\s/.exec(new Intl.DateTimeFormat([], {hour: 'numeric'}).format(0))) } const dateObserver = new (class { From 96cb9c583f9fadd5c7afe44733215dc171d34f26 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Oct 2025 01:15:12 +0100 Subject: [PATCH 04/11] update comment --- src/relative-time-element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/relative-time-element.ts b/src/relative-time-element.ts index d1180a6..67ff6f0 100644 --- a/src/relative-time-element.ts +++ b/src/relative-time-element.ts @@ -35,7 +35,7 @@ function getUnitFactor(el: RelativeTimeElement): number { // Determine whether the user has a 12 (vs. 24) hour cycle preference. This relies on the hour formatting in // a 12 hour preference being formatted like "1 AM" including a space, while with a 24 hour preference, the // same is formatted as "01" without a space. In the future `Intl.Locale.prototype.getHourCycles()` could be -// used but in my testing it incorrectly returned a 12 hour preference with MacOS set to 24 hour format. +// used but it is not as well-supported as this method. function isBrowser12hCycle() { return Boolean(/\s/.exec(new Intl.DateTimeFormat([], {hour: 'numeric'}).format(0))) } From b2e15aad634febc8437f2854fc30e5b3d9457d3a Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Oct 2025 01:39:04 +0100 Subject: [PATCH 05/11] add override option --- README.md | 41 ++++++++++++++++++------------------ examples/index.html | 14 ++++++++++++ src/relative-time-element.ts | 14 +++++++++--- 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 5c399f7..430d56e 100644 --- a/README.md +++ b/README.md @@ -56,26 +56,27 @@ So, a relative date phrase is used for up to a month and then the actual date is #### Attributes -| Property Name | Attribute Name | Possible Values | Default Value | -| :------------- | :--------------- | :------------------------------------------------------------------------------------------ | :------------------------ | -| `datetime` | `datetime` | `string` | - | -| `format` | `format` | `'datetime'\|'relative'\|'duration'` | `'auto'` | -| `date` | - | `Date \| null` | - | -| `tense` | `tense` | `'auto'\|'past'\|'future'` | `'auto'` | -| `precision` | `precision` | `'year'\|'month'\|'day'\|'hour'\|'minute'\|'second'` | `'second'` | -| `threshold` | `threshold` | `string` | `'P30D'` | -| `prefix` | `prefix` | `string` | `'on'` | -| `formatStyle` | `format-style` | `'long'\|'short'\|'narrow'` | \* | -| `second` | `second` | `'numeric'\|'2-digit'\|undefined` | `undefined` | -| `minute` | `minute` | `'numeric'\|'2-digit'\|undefined` | `undefined` | -| `hour` | `hour` | `'numeric'\|'2-digit'\|undefined` | `undefined` | -| `weekday` | `weekday` | `'short'\|'long'\|'narrow'\|undefined` | \*\* | -| `day` | `day` | `'numeric'\|'2-digit'\|undefined` | `'numeric'` | -| `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` | `-` | `-` | +| Property Name | Attribute Name | Possible Values | Default Value | +| :------------- | :--------------- | :------------------------------------------------------------------------------------------ | :------------------------------- | +| `datetime` | `datetime` | `string` | - | +| `format` | `format` | `'datetime'\|'relative'\|'duration'` | `'auto'` | +| `date` | - | `Date \| null` | - | +| `tense` | `tense` | `'auto'\|'past'\|'future'` | `'auto'` | +| `precision` | `precision` | `'year'\|'month'\|'day'\|'hour'\|'minute'\|'second'` | `'second'` | +| `threshold` | `threshold` | `string` | `'P30D'` | +| `prefix` | `prefix` | `string` | `'on'` | +| `formatStyle` | `format-style` | `'long'\|'short'\|'narrow'` | \* | +| `second` | `second` | `'numeric'\|'2-digit'\|undefined` | `undefined` | +| `minute` | `minute` | `'numeric'\|'2-digit'\|undefined` | `undefined` | +| `hour` | `hour` | `'numeric'\|'2-digit'\|undefined` | `undefined` | +| `weekday` | `weekday` | `'short'\|'long'\|'narrow'\|undefined` | \*\* | +| `day` | `day` | `'numeric'\|'2-digit'\|undefined` | `'numeric'` | +| `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 | +| `hourCycle` | `hour-cycle` | `'h11'\|'h12'\|'h23'\|'h24'\|undefined` | 'h12' or 'h24' based on browser | +| `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'`. diff --git a/examples/index.html b/examples/index.html index 886af5c..2cc0103 100644 --- a/examples/index.html +++ b/examples/index.html @@ -29,6 +29,20 @@

Format DateTime

+

+ h12 cycle: + + Jan 1 1970 + +

+ +

+ h24 cycle: + + Jan 1 1970 + +

+

Customised options: diff --git a/src/relative-time-element.ts b/src/relative-time-element.ts index 67ff6f0..db70041 100644 --- a/src/relative-time-element.ts +++ b/src/relative-time-element.ts @@ -106,6 +106,14 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor return tz || undefined } + get hourCycle() { + // Prefer attribute, then closest, then document + const hc = + this.closest('[hour-cycle]')?.getAttribute('hour-cycle') || + this.ownerDocument.documentElement.getAttribute('hour-cycle') + return (hc || (isBrowser12hCycle() ? 'h12' : 'h24')) as Intl.DateTimeFormatOptions['hourCycle'] + } + #renderRoot: Node = this.shadowRoot ? this.shadowRoot : this.attachShadow ? this.attachShadow({mode: 'open'}) : this static get observedAttributes() { @@ -147,7 +155,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor minute: '2-digit', timeZoneName: 'short', timeZone: this.timeZone, - hour12: isBrowser12hCycle(), + hour12: this.hourCycle === 'h12', }).format(date) } @@ -222,7 +230,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor year: this.year, timeZoneName: this.timeZoneName, timeZone: this.timeZone, - hour12: isBrowser12hCycle(), + hour12: this.hourCycle === 'h12', }) return `${this.prefix} ${formatter.format(date)}`.trim() } @@ -256,7 +264,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor minute: '2-digit', timeZoneName: 'short', timeZone: this.timeZone, - hour12: isBrowser12hCycle(), + hour12: this.hourCycle === 'h12', } if (this.#isToday(date)) { From e52f34b096e58ea28f55b01acef4745843424a9e Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Oct 2025 01:44:38 +0100 Subject: [PATCH 06/11] use correct h23 --- README.md | 2 +- examples/index.html | 4 ++-- src/relative-time-element.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 430d56e..294c99d 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ So, a relative date phrase is used for up to a month and then the actual date is | `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 | -| `hourCycle` | `hour-cycle` | `'h11'\|'h12'\|'h23'\|'h24'\|undefined` | 'h12' or 'h24' based on browser | +| `hourCycle` | `hour-cycle` | `'h11'\|'h12'\|'h23'\|'h24'\|undefined` | 'h12' or 'h23' based on browser | | `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'`. diff --git a/examples/index.html b/examples/index.html index 2cc0103..b190940 100644 --- a/examples/index.html +++ b/examples/index.html @@ -37,8 +37,8 @@

Format DateTime

- h24 cycle: - + h23 cycle: + Jan 1 1970

diff --git a/src/relative-time-element.ts b/src/relative-time-element.ts index db70041..7b16295 100644 --- a/src/relative-time-element.ts +++ b/src/relative-time-element.ts @@ -111,7 +111,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor const hc = this.closest('[hour-cycle]')?.getAttribute('hour-cycle') || this.ownerDocument.documentElement.getAttribute('hour-cycle') - return (hc || (isBrowser12hCycle() ? 'h12' : 'h24')) as Intl.DateTimeFormatOptions['hourCycle'] + return (hc || (isBrowser12hCycle() ? 'h12' : 'h23')) as Intl.DateTimeFormatOptions['hourCycle'] } #renderRoot: Node = this.shadowRoot ? this.shadowRoot : this.attachShadow ? this.attachShadow({mode: 'open'}) : this From 343328cd52a916f5df689a2f014a364e82f1fc3c Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Oct 2025 01:53:26 +0100 Subject: [PATCH 07/11] add isHour12 function to support h11 --- examples/index.html | 4 ++-- src/relative-time-element.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/index.html b/examples/index.html index b190940..0c4caf3 100644 --- a/examples/index.html +++ b/examples/index.html @@ -222,8 +222,8 @@

With Aria Hidden

- - + + - + +