diff --git a/src/relative-time-element.ts b/src/relative-time-element.ts index 45d0390..58ff3e0 100644 --- a/src/relative-time-element.ts +++ b/src/relative-time-element.ts @@ -423,6 +423,16 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor } else if (span.hasAttribute('aria-hidden')) { span.removeAttribute('aria-hidden') } + // Tabular numerals keep every digit the same width, so a ticking + // duration/elapsed timer doesn't visibly jump around as it counts (#383). + // Other formats get no inline style; `font-variant-numeric: normal !important` + // on `::part(root)` still overrides this for opt-out. + const tabular = this.format === 'duration' || this.format === 'elapsed' + const fontVariantNumeric = tabular ? 'tabular-nums' : '' + const spanStyle = (span as HTMLElement).style + if (spanStyle.fontVariantNumeric !== fontVariantNumeric) { + spanStyle.fontVariantNumeric = fontVariantNumeric + } if (span.textContent !== content) { span.textContent = content } diff --git a/test/relative-time.js b/test/relative-time.js index 1523051..00ba522 100644 --- a/test/relative-time.js +++ b/test/relative-time.js @@ -64,6 +64,30 @@ suite('relative-time', function () { } }) + test('renders duration and elapsed formats with tabular numerals', async () => { + freezeTime(new Date(5000)) + for (const format of ['duration', 'elapsed']) { + const time = document.createElement('relative-time') + time.setAttribute('format', format) + time.setAttribute('datetime', '1970-01-01T00:00:00.000Z') + await Promise.resolve() + assert.equal( + time.shadowRoot.firstElementChild.style.fontVariantNumeric, + 'tabular-nums', + `${format} keeps each tick's digits the same width`, + ) + time.disconnectedCallback() + } + }) + + test('renders other formats without inline numeric styling', async () => { + const time = document.createElement('relative-time') + time.setAttribute('datetime', '2026-10-30T00:00:00.000Z') + await Promise.resolve() + assert.equal(time.shadowRoot.firstElementChild.style.fontVariantNumeric, '') + time.disconnectedCallback() + }) + test('reschedules future micro datetime updates at the explicit threshold boundary', async () => { const originalSetTimeout = window.setTimeout const delays = []