Skip to content

Commit 41f7a00

Browse files
fix: remove setting defaults in ssr from useDefaultLocale (#8524)
* fix: remove setting defaults in ssr from useDefaultLocale * chore: revert previous useDefaultLocale commit * fix: split I18nProvider internally into two components A component uses`useDefaultLocale` and another the locale value set in the Provider. * fix tests * refactor(i18n): Replace getDefaultLocale with useDefaultLocale in useLocale function * test(i18n): Add tests for language change handling in useLocale --------- Co-authored-by: Robert Snow <[email protected]> Co-authored-by: Robert Snow <[email protected]>
1 parent b45f39a commit 41f7a00

File tree

3 files changed

+179
-19
lines changed

3 files changed

+179
-19
lines changed

packages/@react-aria/i18n/src/context.tsx

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,60 @@ export interface I18nProviderProps {
2323

2424
const I18nContext = React.createContext<Locale | null>(null);
2525

26+
interface I18nProviderWithLocaleProps extends I18nProviderProps {
27+
locale: string
28+
}
29+
2630
/**
27-
* Provides the locale for the application to all child components.
31+
* Internal component that handles the case when locale is provided.
2832
*/
29-
export function I18nProvider(props: I18nProviderProps): JSX.Element {
30-
let {locale, children} = props;
31-
let defaultLocale = useDefaultLocale();
33+
function I18nProviderWithLocale(props: I18nProviderWithLocaleProps): JSX.Element {
34+
let {locale, children} = props;
35+
let value: Locale = React.useMemo(() => ({
36+
locale,
37+
direction: isRTL(locale) ? 'rtl' : 'ltr'
38+
}), [locale]);
3239

33-
let value: Locale = React.useMemo(() => {
34-
if (!locale) {
35-
return defaultLocale;
36-
}
40+
return (
41+
<I18nContext.Provider value={value}>
42+
{children}
43+
</I18nContext.Provider>
44+
);
45+
}
3746

38-
return {
39-
locale,
40-
direction: isRTL(locale) ? 'rtl' : 'ltr'
41-
};
42-
}, [defaultLocale, locale]);
47+
interface I18nProviderWithDefaultLocaleProps {
48+
children: ReactNode
49+
}
50+
51+
/**
52+
* Internal component that handles the case when no locale is provided.
53+
*/
54+
function I18nProviderWithDefaultLocale(props: I18nProviderWithDefaultLocaleProps): JSX.Element {
55+
let {children} = props;
56+
let defaultLocale = useDefaultLocale();
4357

4458
return (
45-
<I18nContext.Provider value={value}>
59+
<I18nContext.Provider value={defaultLocale}>
4660
{children}
4761
</I18nContext.Provider>
4862
);
4963
}
5064

65+
/**
66+
* Provides the locale for the application to all child components.
67+
*/
68+
export function I18nProvider(props: I18nProviderProps): JSX.Element {
69+
let {locale, children} = props;
70+
71+
// Conditionally render different components to avoid calling useDefaultLocale.
72+
// This is necessary because useDefaultLocale triggers a re-render.
73+
if (locale) {
74+
return <I18nProviderWithLocale locale={locale} children={children} />;
75+
}
76+
77+
return <I18nProviderWithDefaultLocale children={children} />;
78+
}
79+
5180
/**
5281
* Returns the current locale and layout direction.
5382
*/
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright 2023 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import {act, render} from '@react-spectrum/test-utils-internal';
14+
import {I18nProvider, useLocale} from '../src/context';
15+
import React from 'react';
16+
17+
function TestComponent() {
18+
let locale = useLocale();
19+
return (
20+
<div>
21+
<div data-testid="locale">{locale.locale}</div>
22+
<div data-testid="direction">{locale.direction}</div>
23+
</div>
24+
);
25+
}
26+
27+
function languageProps(language) {
28+
return {
29+
value: language,
30+
writable: true,
31+
configurable: true
32+
};
33+
}
34+
35+
describe('useLocale languagechange', () => {
36+
let originalNavigator;
37+
let originalLanguage;
38+
39+
beforeEach(() => {
40+
originalNavigator = window.navigator;
41+
originalLanguage = window.navigator.language;
42+
43+
Object.defineProperty(window.navigator, 'language', languageProps('en-US'));
44+
45+
act(() => {
46+
window.dispatchEvent(new Event('languagechange'));
47+
});
48+
});
49+
50+
afterEach(() => {
51+
Object.defineProperty(window.navigator, 'language', languageProps(originalLanguage));
52+
53+
act(() => {
54+
window.dispatchEvent(new Event('languagechange'));
55+
});
56+
57+
Object.defineProperty(window, 'navigator', languageProps(originalNavigator));
58+
});
59+
60+
it('should update locale when languagechange event is triggered', () => {
61+
let {getByTestId} = render(
62+
<I18nProvider>
63+
<TestComponent />
64+
</I18nProvider>
65+
);
66+
67+
// Initial render should show en-US
68+
expect(getByTestId('locale')).toHaveTextContent('en-US');
69+
expect(getByTestId('direction')).toHaveTextContent('ltr');
70+
71+
// Change navigator.language and trigger languagechange event
72+
act(() => {
73+
Object.defineProperty(window.navigator, 'language', languageProps('pt-PT'));
74+
window.dispatchEvent(new Event('languagechange'));
75+
});
76+
77+
// Should re-render with new locale
78+
expect(getByTestId('locale')).toHaveTextContent('pt-PT');
79+
expect(getByTestId('direction')).toHaveTextContent('ltr');
80+
});
81+
82+
it('should update locale direction when changing from LTR to RTL language', () => {
83+
let {getByTestId} = render(
84+
<I18nProvider>
85+
<TestComponent />
86+
</I18nProvider>
87+
);
88+
89+
// Change to Hebrew (RTL language)
90+
act(() => {
91+
Object.defineProperty(window.navigator, 'language', languageProps('he-IL'));
92+
window.dispatchEvent(new Event('languagechange'));
93+
});
94+
95+
// Should update to Hebrew with RTL direction
96+
expect(getByTestId('locale')).toHaveTextContent('he-IL');
97+
expect(getByTestId('direction')).toHaveTextContent('rtl');
98+
99+
// Change back to Portuguese
100+
act(() => {
101+
Object.defineProperty(window.navigator, 'language', languageProps('pt-PT'));
102+
window.dispatchEvent(new Event('languagechange'));
103+
});
104+
105+
// Should update to Portuguese
106+
expect(getByTestId('locale')).toHaveTextContent('pt-PT');
107+
expect(getByTestId('direction')).toHaveTextContent('ltr');
108+
});
109+
110+
it('should not change displayed locale when explicit locale is provided via I18nProvider', () => {
111+
let {getByTestId} = render(
112+
<I18nProvider locale="fr-FR">
113+
<TestComponent />
114+
</I18nProvider>
115+
);
116+
117+
// Initial render should show fr-FR
118+
expect(getByTestId('locale')).toHaveTextContent('fr-FR');
119+
expect(getByTestId('direction')).toHaveTextContent('ltr');
120+
121+
// Change navigator.language and trigger languagechange event
122+
act(() => {
123+
Object.defineProperty(window.navigator, 'language', languageProps('ja-JP'));
124+
window.dispatchEvent(new Event('languagechange'));
125+
});
126+
127+
// Should still show fr-FR (explicit locale takes precedence)
128+
expect(getByTestId('locale')).toHaveTextContent('fr-FR');
129+
expect(getByTestId('direction')).toHaveTextContent('ltr');
130+
});
131+
});

packages/@react-spectrum/numberfield/test/NumberField.test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
jest.mock('@react-aria/live-announcer');
14-
import {act, fireEvent, pointerMap, render, within} from '@react-spectrum/test-utils-internal';
14+
import {act, fireEvent, pointerMap, render, screen, within} from '@react-spectrum/test-utils-internal';
1515
import {announce} from '@react-aria/live-announcer';
1616
import {Button} from '@react-spectrum/button';
1717
import {chain} from '@react-aria/utils';
@@ -70,7 +70,7 @@ describe('NumberField', function () {
7070
incrementButton,
7171
decrementButton,
7272
debug,
73-
rerender: (props = {}, locale) => rerender(<Provider theme={theme} locale={locale}><NumberField aria-label="labelled" {...props} /></Provider>)
73+
rerender: (props = {}, locale) => rerender(<Provider theme={theme} scale={scale} locale={locale}><NumberField aria-label="labelled" {...props} /></Provider>)
7474
};
7575
}
7676

@@ -871,7 +871,7 @@ describe('NumberField', function () {
871871

872872
expect(textField).toHaveAttribute('value', '€10.00');
873873
rerender({defaultValue: 10, formatOptions: {style: 'currency', currency: 'USD'}});
874-
expect(textField).toHaveAttribute('value', '$10.00');
874+
expect(screen.getByRole('textbox')).toHaveAttribute('value', '$10.00');
875875
});
876876

877877
it.each`
@@ -2280,7 +2280,7 @@ describe('NumberField', function () {
22802280
expect(hiddenInput).toHaveValue('30');
22812281

22822282
rerender({name: 'age', value: null});
2283-
expect(hiddenInput).toHaveValue('');
2283+
expect(document.querySelector('input[type=hidden]')).toHaveValue('');
22842284
});
22852285

22862286
it('supports form reset', async () => {
@@ -2314,7 +2314,7 @@ describe('NumberField', function () {
23142314
it('resets to defaultValue when submitting form action', async () => {
23152315
function Test() {
23162316
const [value, formAction] = React.useActionState(() => 33, 22);
2317-
2317+
23182318
return (
23192319
<Provider theme={theme}>
23202320
<form action={formAction}>

0 commit comments

Comments
 (0)