Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 51 additions & 5 deletions src/relative-time-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,57 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor
return `${this.prefix} ${formatter.format(date)}`.trim()
}

#getUserPreferredAbsoluteTimeFormat(date: Date): string {
return new Intl.DateTimeFormat(this.#lang, {
day: 'numeric',
month: 'short',
#isToday(date: Date): boolean {
const now = new Date()
const formatter = new Intl.DateTimeFormat(this.#lang, {
timeZone: this.timeZone,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, having this be sensitive to user's TZ will save us many-a bug report!

year: 'numeric',
month: '2-digit',
day: '2-digit',
})
return formatter.format(now) === formatter.format(date)
}

#isCurrentYear(date: Date): boolean {
const now = new Date()
const formatter = new Intl.DateTimeFormat(this.#lang, {
timeZone: this.timeZone,
year: 'numeric',
})
return formatter.format(now) === formatter.format(date)
}

// If current day, shows "Today" + time.
// If current year, shows date without year.
// In all other scenarios, show full date.
#getUserPreferredAbsoluteTimeFormat(date: Date): string {
const timeOnlyOptions: Intl.DateTimeFormatOptions = {
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'short',
timeZone: this.timeZone,
}

if (this.#isToday(date)) {
const relativeFormatter = new Intl.RelativeTimeFormat(this.#lang, {numeric: 'auto'})
let todayText = relativeFormatter.format(0, 'day')
todayText = todayText.charAt(0).toLocaleUpperCase(this.#lang) + todayText.slice(1)
const timeOnly = new Intl.DateTimeFormat(this.#lang, timeOnlyOptions).format(date)

return `${todayText} ${timeOnly}`
}

const timeAndDateOptions: Intl.DateTimeFormatOptions = {
...timeOnlyOptions,
day: 'numeric',
month: 'short',
}
if (this.#isCurrentYear(date)) {
return new Intl.DateTimeFormat(this.#lang, timeAndDateOptions).format(date)
}
return new Intl.DateTimeFormat(this.#lang, {
...timeAndDateOptions,
year: 'numeric',
}).format(date)
}

Expand Down Expand Up @@ -525,7 +567,11 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor
this.dispatchEvent(new RelativeTimeUpdatedEvent(oldText, newText, oldTitle, newTitle))
}

if ((format === 'relative' || format === 'duration') && !displayUserPreferredAbsoluteTime) {
const shouldObserve =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for breaking out that long conditional, this is much more readable to me

format === 'relative' ||
format === 'duration' ||
(displayUserPreferredAbsoluteTime && (this.#isToday(date) || this.#isCurrentYear(date)))
if (shouldObserve) {
dateObserver.observe(this)
} else {
dateObserver.unobserve(this)
Expand Down
169 changes: 169 additions & 0 deletions test/relative-time.js
Original file line number Diff line number Diff line change
Expand Up @@ -1886,6 +1886,10 @@ suite('relative-time', function () {
})

suite('experimental: [data-prefers-absolute-time]', async () => {
teardown(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had initially wondered if we'd need a test for the boundary condition of moving from Today to the full timestamp, but this should "just work" given how we have the observable behavior set up!

document.documentElement.removeAttribute('data-prefers-absolute-time')
document.body.removeAttribute('data-prefers-absolute-time')
})
test('formats with absolute time when data-prefers-absolute-time="true"', async () => {
document.documentElement.setAttribute('data-prefers-absolute-time', 'true')
const el = document.createElement('relative-time')
Expand Down Expand Up @@ -1930,6 +1934,171 @@ suite('relative-time', function () {

assert.match(el.shadowRoot.textContent, /[A-Z][a-z]{2} \d{1,2}, \d{4}, \d{1,2}:\d{2} (AM|PM)/)
})

test('formats today dates with "Today" text', async () => {
freezeTime(new Date('2023-01-15T17:00:00.000Z'))

document.documentElement.setAttribute('data-prefers-absolute-time', 'true')
const el = document.createElement('relative-time')
el.setAttribute('lang', 'en-US')
el.setAttribute('time-zone', 'America/New_York')

el.setAttribute('datetime', '2023-01-15T17:00:00.000Z')
await Promise.resolve()

assert.equal(el.shadowRoot.textContent, 'Today 12:00 PM EST')
})

test('formats current year dates without year', async () => {
freezeTime(new Date('2023-06-15T12:00:00.000Z'))

document.documentElement.setAttribute('data-prefers-absolute-time', 'true')
const el = document.createElement('relative-time')
el.setAttribute('lang', 'en-US')
el.setAttribute('time-zone', 'America/New_York')
el.setAttribute('datetime', '2023-03-10T18:00:00.000Z')
await Promise.resolve()

assert.equal(el.shadowRoot.textContent, 'Mar 10, 1:00 PM EST')
})

test('formats different year dates as full date', async () => {
freezeTime(new Date('2023-06-15T12:00:00.000Z'))

document.documentElement.setAttribute('data-prefers-absolute-time', 'true')
const el = document.createElement('relative-time')
el.setAttribute('lang', 'en-US')
el.setAttribute('time-zone', 'America/New_York')
el.setAttribute('datetime', '2022-03-10T18:00:00.000Z')
await Promise.resolve()

assert.equal(el.shadowRoot.textContent, 'Mar 10, 2022, 1:00 PM EST')
})

test('respects locale formatting', async () => {
freezeTime(new Date('2023-01-15T17:00:00.000Z'))

document.documentElement.setAttribute('data-prefers-absolute-time', 'true')
const el = document.createElement('relative-time')
el.setAttribute('lang', 'es-ES')
el.setAttribute('time-zone', 'Europe/Madrid')

el.setAttribute('datetime', '2023-01-15T17:00:00.000Z')
await Promise.resolve()

// Spanish formatting - "hoy" = "today", 24-hour format
assert.equal(el.shadowRoot.textContent, 'Hoy 18:00 CET')
})
Comment on lines +1978 to +1991
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the future! l10n!


test('uses element time-zone attribute', async () => {
freezeTime(new Date('2023-01-15T17:00:00.000Z'))

document.documentElement.setAttribute('data-prefers-absolute-time', 'true')
const el = document.createElement('relative-time')
el.setAttribute('lang', 'en-US')
el.setAttribute('time-zone', 'Europe/Paris')
el.setAttribute('datetime', '2023-01-15T17:00:00.000Z')
await Promise.resolve()

assert.equal(el.shadowRoot.textContent, 'Today 6:00 PM GMT+1')
})

suite('format exclusions', function () {
test('does not activate for format="duration"', async () => {
freezeTime(new Date('2023-01-15T17:00:00.000Z'))
document.documentElement.setAttribute('data-prefers-absolute-time', 'true')

const el = document.createElement('relative-time')
el.setAttribute('lang', 'en-US')
el.setAttribute('datetime', '2023-01-15T16:00:00.000Z')
el.setAttribute('format', 'duration')
await Promise.resolve()

assert.equal(el.shadowRoot.textContent, '1 hour')
})

test('does not activate for format="elapsed"', async () => {
freezeTime(new Date('2023-01-15T17:00:00.000Z'))
document.documentElement.setAttribute('data-prefers-absolute-time', 'true')

const el = document.createElement('relative-time')
el.setAttribute('lang', 'en-US')
el.setAttribute('datetime', '2023-01-15T16:00:00.000Z')
el.setAttribute('format', 'elapsed')
await Promise.resolve()

assert.equal(el.shadowRoot.textContent, '1h')
})

test('does not activate for format="micro"', async () => {
freezeTime(new Date('2023-01-15T17:00:00.000Z'))
document.documentElement.setAttribute('data-prefers-absolute-time', 'true')

const el = document.createElement('relative-time')
el.setAttribute('lang', 'en-US')
el.setAttribute('datetime', '2023-01-15T16:00:00.000Z')
el.setAttribute('format', 'micro')
await Promise.resolve()

assert.equal(el.shadowRoot.textContent, '1h')
})

test('activates for format="relative" (default)', async () => {
freezeTime(new Date('2023-01-15T17:00:00.000Z'))
document.documentElement.setAttribute('data-prefers-absolute-time', 'true')

const el = document.createElement('relative-time')
el.setAttribute('lang', 'en-US')
el.setAttribute('time-zone', 'GMT')
el.setAttribute('datetime', '2023-01-15T17:00:00.000Z')
el.setAttribute('format', 'relative')
await Promise.resolve()

assert.equal(el.shadowRoot.textContent, 'Today 5:00 PM UTC')
})

test('activates for format="auto"', async () => {
freezeTime(new Date('2023-01-15T17:00:00.000Z'))
document.documentElement.setAttribute('data-prefers-absolute-time', 'true')

const el = document.createElement('relative-time')
el.setAttribute('lang', 'en-US')
el.setAttribute('time-zone', 'UTC')
el.setAttribute('datetime', '2023-01-15T17:00:00.000Z')
el.setAttribute('format', 'auto')
await Promise.resolve()

assert.equal(el.shadowRoot.textContent, 'Today 5:00 PM UTC')
})

test('activates for format="datetime" if current day', async () => {
freezeTime(new Date('2023-01-15T17:00:00.000Z'))
document.documentElement.setAttribute('data-prefers-absolute-time', 'true')

const el = document.createElement('relative-time')
el.setAttribute('lang', 'en-US')
el.setAttribute('time-zone', 'America/New_York')
el.setAttribute('datetime', '2023-01-15T17:00:00.000Z')
el.setAttribute('format', 'datetime')
await Promise.resolve()

assert.equal(el.shadowRoot.textContent, 'Today 12:00 PM EST')
})

test('activates for format="datetime" if current year but not today', async () => {
freezeTime(new Date('2023-06-15T17:00:00.000Z'))
document.documentElement.setAttribute('data-prefers-absolute-time', 'true')

const el = document.createElement('relative-time')
el.setAttribute('lang', 'en-US')
el.setAttribute('time-zone', 'America/New_York')
el.setAttribute('datetime', '2023-03-10T18:00:00.000Z') // 18:00 UTC = 1:00 PM EST
el.setAttribute('format', 'datetime')
await Promise.resolve()

assert.equal(el.shadowRoot.textContent, 'Mar 10, 1:00 PM EST')
})
})
})

suite('[aria-hidden]', async () => {
Expand Down
Loading