diff --git a/.gitignore b/.gitignore
index 178fe850797..63448f766af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
.idea
.package-lock.json
.parcel-cache
+.vscode
build-storybook.log
coverage
dist
diff --git a/packages/@internationalized/number/src/NumberParser.ts b/packages/@internationalized/number/src/NumberParser.ts
index dfeb7c6c24f..6311f920787 100644
--- a/packages/@internationalized/number/src/NumberParser.ts
+++ b/packages/@internationalized/number/src/NumberParser.ts
@@ -274,9 +274,9 @@ class NumberParserImpl {
const nonLiteralParts = new Set(['decimal', 'fraction', 'integer', 'minusSign', 'plusSign', 'group']);
-// This list is derived from https://www.unicode.org/cldr/charts/43/supplemental/language_plural_rules.html#comparison and includes
+// This list is derived from https://www.unicode.org/cldr/charts/49/supplemental/language_plural_rules.html#comparison and includes
// all unique numbers which we need to check in order to determine all the plural forms for a given locale.
-// See: https://github.com/adobe/react-spectrum/pull/5134/files#r1337037855 for used script
+// Run scripts/generateAllPlurals.mjs to generate this list.
const pluralNumbers = [
0, 4, 2, 1, 11, 20, 3, 7, 100, 21, 0.1, 1.1
];
diff --git a/packages/react-aria-components/docs/Modal.mdx b/packages/react-aria-components/docs/Modal.mdx
index f9e6dbd8210..d17bd684aad 100644
--- a/packages/react-aria-components/docs/Modal.mdx
+++ b/packages/react-aria-components/docs/Modal.mdx
@@ -82,7 +82,7 @@ import {DialogTrigger, Modal, Dialog, Button, Heading, TextField, Label, Input}
position: absolute;
top: 0;
left: 0;
- width: 100vw;
+ width: var(--page-width);
height: var(--page-height);
background: rgba(0 0 0 / .5);
z-index: 100;
@@ -246,10 +246,11 @@ import {ModalOverlay} from 'react-aria-components';
position: absolute;
top: 0;
left: 0;
- width: 100%;
+ width: var(--page-width);
height: var(--page-height);
background: rgba(45 0 0 / .3);
backdrop-filter: blur(10px);
+ overflow: clip;
&[data-entering] {
animation: mymodal-blur 300ms;
@@ -421,11 +422,12 @@ A `Modal` can be targeted with the `.react-aria-Modal` CSS selector, or by overr
By default, `Modal` includes a builtin `ModalOverlay`, which renders a backdrop over the page when a modal is open. This can be targeted using the `.react-aria-ModalOverlay` CSS selector. To customize the `ModalOverlay` with a different class name or other attributes, render a `ModalOverlay` and place a `Modal` inside.
-The `--page-height` and `--visual-viewport-height` CSS custom property will be set on the `ModalOverlay`, the latter of which you can use to set the height of the Modal to account for the virtual keyboard on mobile.
+The `--page-height`, `--page-width`, `--visual-viewport-height` and `--visual-viewport-width` CSS custom properties will be set on the `ModalOverlay`, the latter of which you can use to set the size of the Modal to account for the virtual keyboard on mobile.
```css render=false
.react-aria-ModalOverlay {
position: absolute;
+ width: var(--page-width);
height: var(--page-height);
}
diff --git a/packages/react-aria-components/stories/Modal.stories.tsx b/packages/react-aria-components/stories/Modal.stories.tsx
index 0bb9dd808df..e84ff65d815 100644
--- a/packages/react-aria-components/stories/Modal.stories.tsx
+++ b/packages/react-aria-components/stories/Modal.stories.tsx
@@ -28,7 +28,6 @@ import styles from '../example/index.css';
import {TextField} from '../src/TextField';
import './styles.css';
-
export default {
title: 'React Aria Components/Modal',
component: Modal
@@ -80,6 +79,63 @@ export const ModalExample: ModalStory = () => (
);
+export const SheetExample: ModalStory = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
function InertTest() {
return (
diff --git a/packages/react-stately/src/numberfield/useNumberFieldState.ts b/packages/react-stately/src/numberfield/useNumberFieldState.ts
index b1f1f45a9eb..d5a46978ad7 100644
--- a/packages/react-stately/src/numberfield/useNumberFieldState.ts
+++ b/packages/react-stately/src/numberfield/useNumberFieldState.ts
@@ -155,7 +155,7 @@ export function useNumberFieldState(
let [prevValue, setPrevValue] = useState(numberValue);
let [prevLocale, setPrevLocale] = useState(locale);
let [prevFormatOptions, setPrevFormatOptions] = useState(formatOptions);
- if (!Object.is(numberValue, prevValue) || locale !== prevLocale || formatOptions !== prevFormatOptions) {
+ if (!Object.is(numberValue, prevValue) || locale !== prevLocale || !isEqualFormatOptions(formatOptions, prevFormatOptions)) {
setInputValue(format(numberValue));
setPrevValue(numberValue);
setPrevLocale(locale);
@@ -304,6 +304,27 @@ export function useNumberFieldState(
};
}
+// Shallow equality is sufficient here because all values in Intl.NumberFormatOptions are primitives.
+function isEqualFormatOptions(a: Intl.NumberFormatOptions | undefined, b: Intl.NumberFormatOptions | undefined) {
+ if (a === b) {
+ return true;
+ }
+ if (!a || !b) {
+ return false;
+ }
+ let aKeys = Object.keys(a);
+ let bKeys = Object.keys(b);
+ if (aKeys.length !== bKeys.length) {
+ return false;
+ }
+ for (let key of aKeys) {
+ if (b[key] !== a[key]) {
+ return false;
+ }
+ }
+ return true;
+}
+
function handleDecimalOperation(operator: '-' | '+', value1: number, value2: number): number {
let result = operator === '+' ? value1 + value2 : value1 - value2;
diff --git a/packages/react-stately/test/numberfield/useNumberFieldState.test.ts b/packages/react-stately/test/numberfield/useNumberFieldState.test.ts
new file mode 100644
index 00000000000..e9f54df8b20
--- /dev/null
+++ b/packages/react-stately/test/numberfield/useNumberFieldState.test.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2020 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {actHook as act, renderHook} from '@react-spectrum/test-utils-internal';
+import {useNumberFieldState} from '../../src/numberfield/useNumberFieldState';
+
+describe('useNumberFieldState', () => {
+ describe('formatOptions stability', () => {
+ it('does not reset inputValue when re-rendered with same formatOptions content', () => {
+ let initialFormatOptions = {style: 'unit', unit: 'millisecond', useGrouping: false} as Intl.NumberFormatOptions;
+ let {result, rerender} = renderHook(
+ ({formatOptions}) => useNumberFieldState({defaultValue: 1, formatOptions, locale: 'en-US'}),
+ {initialProps: {formatOptions: initialFormatOptions}}
+ );
+
+ // Simulate user typing '2' (without committing)
+ act(() => {
+ result.current.setInputValue('2');
+ });
+ expect(result.current.inputValue).toBe('2');
+
+ // Re-render with new-reference but same-content formatOptions (simulates inline object literal)
+ rerender({formatOptions: {style: 'unit', unit: 'millisecond', useGrouping: false} as Intl.NumberFormatOptions});
+
+ // inputValue should NOT be reset to '1' (the formatted defaultValue)
+ expect(result.current.inputValue).toBe('2');
+ });
+
+ it('resets inputValue when formatOptions content actually changes', () => {
+ let {result, rerender} = renderHook(
+ ({formatOptions}) => useNumberFieldState({defaultValue: 1024, formatOptions, locale: 'en-US'}),
+ {initialProps: {formatOptions: {style: 'currency', currency: 'EUR'} as Intl.NumberFormatOptions}}
+ );
+
+ expect(result.current.inputValue).toBe('€1,024.00');
+
+ rerender({formatOptions: {style: 'currency', currency: 'USD'}});
+
+ expect(result.current.inputValue).toBe('$1,024.00');
+ });
+
+ it('does not reset inputValue when re-rendered with undefined formatOptions', () => {
+ let {result, rerender} = renderHook(
+ ({formatOptions}) => useNumberFieldState({defaultValue: 1, formatOptions, locale: 'en-US'}),
+ {initialProps: {formatOptions: undefined as Intl.NumberFormatOptions | undefined}}
+ );
+
+ act(() => {
+ result.current.setInputValue('2');
+ });
+ expect(result.current.inputValue).toBe('2');
+
+ rerender({formatOptions: undefined});
+
+ expect(result.current.inputValue).toBe('2');
+ });
+ });
+});
diff --git a/scripts/generateAllPlurals.mjs b/scripts/generateAllPlurals.mjs
index feb395767e4..12ceb29937b 100644
--- a/scripts/generateAllPlurals.mjs
+++ b/scripts/generateAllPlurals.mjs
@@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/
-/* Scrapes data on CLDR https://www.unicode.org/cldr/charts/44/supplemental/language_plural_rules.html#comparison
+/* Scrapes data on CLDR https://www.unicode.org/cldr/charts/49/supplemental/language_plural_rules.html#comparison
* and generates a list of all possible values needed between all locales for plural rules.
* It is used by our NumberParser to generate all literal strings, ex units 1 foot, 2 feet, but other locales have more than 2 forms.
*/
@@ -150,7 +150,7 @@ function extractTable(dom, integerTable, fractionTable) {
return Array.from(values);
}
-fetch('https://www.unicode.org/cldr/charts/44/supplemental/language_plural_rules.html#comparison')
+fetch('https://www.unicode.org/cldr/charts/49/supplemental/language_plural_rules.html#comparison')
.then(async (response) => {
let data = await response.text();
const dom = new JSDOM(data);
diff --git a/starters/docs/src/Modal.css b/starters/docs/src/Modal.css
index 848e09bb8c7..96db3275429 100644
--- a/starters/docs/src/Modal.css
+++ b/starters/docs/src/Modal.css
@@ -4,9 +4,10 @@
position: absolute;
top: 0;
left: 0;
- width: 100vw;
+ width: var(--page-width);
height: var(--page-height);
background: rgba(0 0 0 / .5);
+ overflow: clip;
z-index: 100;
font-family: system-ui;
font-size: var(--font-size);
diff --git a/starters/docs/src/Sheet.css b/starters/docs/src/Sheet.css
index aa316fb74d1..9d417df9e96 100644
--- a/starters/docs/src/Sheet.css
+++ b/starters/docs/src/Sheet.css
@@ -2,11 +2,12 @@
position: absolute;
top: 0;
left: 0;
- width: 100%;
+ width: var(--page-width);
height: var(--page-height);
background: rgba(0 0 0 / .3);
backdrop-filter: blur(10px);
z-index: 100;
+ overflow: clip;
&[data-entering] {
animation: sheet-blur 300ms;