From 1550868947ba4bed7b5b436d56e4829f22bff12b Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Oct 2025 00:56:28 +0100 Subject: [PATCH 1/9] 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 353655b..23ac184 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() } @@ -226,6 +237,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor minute: '2-digit', timeZoneName: 'short', timeZone: this.timeZone, + hour12: isBrowser12hCycle(), }).format(date) } From 4583cbf550bcf50f252d158c7d30fb012e296145 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Oct 2025 00:59:38 +0100 Subject: [PATCH 2/9] 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 23ac184..7a76927 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 fd4f9d0ab5aef59b6505047035d80091774affac Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Oct 2025 01:03:20 +0100 Subject: [PATCH 3/9] 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 7a76927..e569a2b 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 2921fd72e30e3f150bd6124d67cba03eebd8779a Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Oct 2025 01:15:12 +0100 Subject: [PATCH 4/9] 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 e569a2b..842358e 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 0a6f358e6b441f755f18ff24333566fdd0079f11 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Oct 2025 01:39:04 +0100 Subject: [PATCH 5/9] add override option --- README.md | 3 ++- examples/index.html | 14 ++++++++++++++ src/relative-time-element.ts | 14 +++++++++++--- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 59ff70c..e7dcd09 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,8 @@ 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 | +| `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 842358e..9cf67b0 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() } @@ -236,7 +244,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) } From 6b00b7ab2b58454cf6d8f65efe0147dec4baa6e5 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Oct 2025 01:44:38 +0100 Subject: [PATCH 6/9] 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 e7dcd09..b60e744 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,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 9cf67b0..ecd5903 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 80e253a25682ca2985ed3f5440eaa287906f2eed Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Oct 2025 01:53:26 +0100 Subject: [PATCH 7/9] 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

- - + + - + +