diff --git a/.changeset/quiet-times-agree.md b/.changeset/quiet-times-agree.md new file mode 100644 index 00000000000..f397c323028 --- /dev/null +++ b/.changeset/quiet-times-agree.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +RelativeTime: Prevent hydration errors when the server and client use different time zones \ No newline at end of file diff --git a/packages/react/src/RelativeTime/RelativeTime.test.tsx b/packages/react/src/RelativeTime/RelativeTime.test.tsx index ba9f48430f5..79e9c063a4d 100644 --- a/packages/react/src/RelativeTime/RelativeTime.test.tsx +++ b/packages/react/src/RelativeTime/RelativeTime.test.tsx @@ -1,4 +1,7 @@ -import {describe, expect, it} from 'vitest' +import {act} from 'react' +import {hydrateRoot, type Root} from 'react-dom/client' +import {renderToString} from 'react-dom/server' +import {describe, expect, it, vi} from 'vitest' import RelativeTime from '.' import {render} from '@testing-library/react' import {implementsClassName} from '../utils/testing' @@ -35,6 +38,42 @@ describe('RelativeTime', () => { expect(container.textContent).toEqual('server rendered date') }) + it('hydrates the fallback without errors when server and client time zones differ', async () => { + const date = new Date('2024-03-07T00:30:00.000Z') + const relativeTime = + const toLocaleDateStringSpy = vi.spyOn(Date.prototype, 'toLocaleDateString').mockReturnValue('Mar 7, 2024') + const container = document.createElement('div') + container.innerHTML = renderToString(relativeTime) + document.body.appendChild(container) + + toLocaleDateStringSpy.mockImplementation((_locales, options) => + options?.timeZone === 'UTC' ? 'Mar 7, 2024' : 'Mar 6, 2024', + ) + + const recoverableErrors: unknown[] = [] + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + let root: Root | undefined + + try { + expect(container.firstChild).toHaveTextContent('Mar 7, 2024') + + await act(async () => { + root = hydrateRoot(container, relativeTime, { + onRecoverableError: error => recoverableErrors.push(error), + }) + }) + + expect(recoverableErrors).toEqual([]) + expect(consoleErrorSpy).not.toHaveBeenCalled() + expect(container.firstChild).toHaveTextContent('Mar 7, 2024') + } finally { + toLocaleDateStringSpy.mockRestore() + consoleErrorSpy.mockRestore() + await act(async () => root?.unmount()) + container.remove() + } + }) + it('does not render no-title attribute by default', () => { const date = new Date('2024-03-07T12:22:48.123Z') const {container} = render() diff --git a/packages/react/src/RelativeTime/RelativeTime.tsx b/packages/react/src/RelativeTime/RelativeTime.tsx index b42612beef2..d1b55856835 100644 --- a/packages/react/src/RelativeTime/RelativeTime.tsx +++ b/packages/react/src/RelativeTime/RelativeTime.tsx @@ -4,7 +4,12 @@ import {createComponent} from '../utils/create-component' const RelativeTimeComponent = createComponent(RelativeTimeElement, 'relative-time') -const localeOptions: Intl.DateTimeFormatOptions = {month: 'short', day: 'numeric', year: 'numeric'} +const localeOptions: Intl.DateTimeFormatOptions = { + month: 'short', + day: 'numeric', + year: 'numeric', + timeZone: 'UTC', +} satisfies Intl.DateTimeFormatOptions function RelativeTime({date, datetime, children, noTitle, ...props}: RelativeTimeProps) { if (datetime) date = new Date(datetime) return (