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