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