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
6 changes: 5 additions & 1 deletion src/relative-time-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ const dateObserver = new (class {
this.updating = true
try {
for (const timeEl of this.elements) {
if (!timeEl.isConnected) {
this.elements.delete(timeEl)
continue
}
nearestDistance = Math.min(nearestDistance, getUnitFactor(timeEl))
timeEl.update()
}
Expand Down Expand Up @@ -752,7 +756,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor
(this.format === 'micro' && Boolean(this.#getExplicitThreshold()) && date.getTime() > now) ||
(displayUserPreferredAbsoluteTime &&
(this.#isToday(date, locale, timeZone) || this.#isCurrentYear(date, locale, timeZone)))
if (shouldObserve) {
if (shouldObserve && this.isConnected) {
dateObserver.observe(this)
} else {
dateObserver.unobserve(this)
Expand Down
115 changes: 115 additions & 0 deletions test/relative-time.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ suite('relative-time', function () {
return 0
}
try {
fixture.append(time)
time.setAttribute('format', 'duration')
time.setAttribute('datetime', '1970-01-01T00:00:00.000Z')
await Promise.resolve()
Expand Down Expand Up @@ -114,6 +115,7 @@ suite('relative-time', function () {
return 1
}
try {
fixture.append(time)
time.setAttribute('format', 'micro')
time.setAttribute('datetime', new Date(Date.now() + 65 * 1000).toISOString())
await Promise.resolve()
Expand All @@ -129,6 +131,7 @@ suite('relative-time', function () {

delays.length = 0
const pastTime = document.createElement('relative-time')
fixture.append(pastTime)
pastTime.setAttribute('format', 'micro')
pastTime.setAttribute('threshold', 'PT1H')
pastTime.setAttribute('datetime', new Date(Date.now() - 59 * 60 * 1000 - 58 * 1000).toISOString())
Expand Down Expand Up @@ -308,6 +311,7 @@ suite('relative-time', function () {
// eslint-disable-next-line @typescript-eslint/no-invalid-this
this.timeout(3000)
const el = document.createElement('relative-time')
fixture.append(el)
el.setAttribute('datetime', new Date(Date.now() + 25000).toISOString())
await Promise.resolve()
const display = el.shadowRoot?.textContent || el.textContent
Expand Down Expand Up @@ -3421,4 +3425,115 @@ suite('relative-time', function () {
assert.equal(el.shadowRoot.textContent, 'Sat, Dec 31, 2022')
})
})

suite('shared update timer', () => {
const originalSetAdd = Set.prototype.add
let observedSets

setup(() => {
observedSets = new Set()
Set.prototype.add = function (value) {
if (value instanceof RelativeTimeElement) observedSets.add(this)
return originalSetAdd.call(this, value)
}
})

teardown(() => {
Set.prototype.add = originalSetAdd
})

// The observer's Set is module-private, so detect membership by recording every Set that a
// RelativeTimeElement is added to and checking it afterwards.
function isObserved(el) {
return [...observedSets].some(set => set.has(el))
}

const recent = () => new Date(Date.now() - 3 * 60 * 1000).toISOString()

test('observes connected elements', async () => {
const el = document.createElement('relative-time')
fixture.append(el)
el.setAttribute('datetime', recent())
await Promise.resolve()
assert.isTrue(isObserved(el))
})

test('stops observing elements when they are disconnected', async () => {
const el = document.createElement('relative-time')
fixture.append(el)
el.setAttribute('datetime', recent())
await Promise.resolve()
el.remove()
assert.isFalse(isObserved(el))
})

test('does not observe elements that were never connected', async () => {
const el = document.createElement('relative-time')
el.setAttribute('datetime', recent())
await Promise.resolve()
assert.equal(el.shadowRoot.textContent, '3 minutes ago')
assert.isFalse(isObserved(el))
})

test('does not observe clones of connected elements', async () => {
const el = document.createElement('relative-time')
fixture.append(el)
el.setAttribute('datetime', recent())
await Promise.resolve()
const clone = el.cloneNode(true)
await Promise.resolve()
assert.equal(clone.shadowRoot.textContent, '3 minutes ago')
assert.isFalse(isObserved(clone))
})

test('does not observe elements removed in the same task as an attribute change', async () => {
const el = document.createElement('relative-time')
fixture.append(el)
el.setAttribute('datetime', recent())
await Promise.resolve()
el.setAttribute('datetime', new Date(Date.now() - 5 * 60 * 1000).toISOString())
el.remove()
await Promise.resolve()
assert.equal(el.shadowRoot.textContent, '5 minutes ago')
assert.isFalse(isObserved(el))
})

test('timer ticks update connected elements and drop disconnected ones', async () => {
const originalSetTimeout = window.setTimeout
const connected = document.createElement('relative-time')
const detached = document.createElement('relative-time')
let tick
// Far-future "now" so the observer always schedules a fresh timer instead of reusing a pending one.
freezeTime(new Date('2100-01-01T00:00:00.000Z'))
globalThis.setTimeout = window.setTimeout = function (callback) {
tick = callback
return 0
}
try {
fixture.append(connected)
connected.setAttribute('datetime', '2099-12-31T23:57:00.000Z')
await Promise.resolve()
assert.isFunction(tick)

// Force a detached element into the Set as if it had been retained before removal.
const set = [...observedSets].find(candidate => candidate.has(connected))
originalSetAdd.call(set, detached)
detached.setAttribute('datetime', '2099-12-31T23:57:00.000Z')
let detachedUpdates = 0
const update = detached.update
detached.update = function () {
detachedUpdates += 1
return update.call(this)
}

Date.now = () => new dateNow('2100-01-01T00:01:00.000Z').getTime()
tick()
assert.equal(connected.shadowRoot.textContent, '4 minutes ago')
assert.equal(detachedUpdates, 0)
assert.isFalse(set.has(detached))
} finally {
globalThis.setTimeout = window.setTimeout = originalSetTimeout
}
})
})
})
Loading