`) for layout flexibility
+* Virtualized scrolling support for performance with large tables
+
+## Anatomy
+
+
+
+A table consists of a container element, with columns and rows of cells containing data inside. The cells within a table may contain focusable elements or plain text content.
+If the table supports row selection, each row can optionally include a selection checkbox in the first column. Additionally, a "select all" checkbox is displayed as the first column header if the table supports multiple row selection.
+
+The
,
,
, and
hooks handle keyboard, mouse, and other interactions to support
+row selection, in table navigation, and overall focus behavior. Those hooks, along with
and
, also handle exposing the table and its contents
+to assistive technology using ARIA.
and
handle row selection and associating each checkbox with its respective rows
+for assistive technology. Each of these hooks returns props to be spread onto the appropriate HTML element.
+
+State is managed by the
+hook from `@react-stately/table`. The state object should be passed as an option to each of the above hooks where applicable.
+
+Note that an `aria-label` or `aria-labelledby` must be passed to the table to identify the element to assistive technology.
+
+## State management
+
+`useTable` requires knowledge of the rows, cells, and columns in the table in order to handle keyboard
+navigation and other interactions. It does this using
+the
+interface, which is a generic interface to access sequential unique keyed data. You can
+implement this interface yourself, e.g. by using a prop to pass a list of item objects,
+but
from
+`@react-stately/table` implements a JSX based interface for building collections instead.
+See [Collection Components](/react-stately/collections.html) for more information,
+and [Collection Interface](/react-stately/Collection.html) for internal details.
+
+Data is defined using the
,
,
,
, and
components, which support both static and dynamic data.
+See the examples in the [usage](#usage) section below for details on how to use these components.
+
+In addition,
+manages the state necessary for multiple selection and exposes
+a
,
+which makes use of the collection to provide an interface to update the selection state.
+For more information, see [Selection](/react-stately/selection.html).
+
+## Example
+
+Tables are complex [collection components](../react-stately/collections.html) that are built up from many child elements
+including columns, rows, and cells. In this example, we'll use the standard HTML table elements along with hooks from React
+Aria for each child. You may also use other elements like `
` to render these components as appropriate.
+Since there are many pieces, we'll walk through each of them one by one.
+
+The
hook will be used to render the outer most table element. It uses
+the
hook to construct the table's collection of rows and columns,
+and manage state such as the focused row/cell, selection, and sort column/direction. We'll use the collection to iterate through
+the rows and cells of the table and render the relevant components, which we'll define below.
+
+```tsx example export=true render=false
+import {Cell, Column, Row, TableBody, TableHeader, useTableState} from '@react-stately/table';
+import {mergeProps} from '@react-aria/utils';
+import {useRef} from 'react';
+import {useFocusRing} from '@react-aria/focus';
+
+function Table(props) {
+ let state = useTableState({...props, showSelectionCheckboxes: props.selectionMode === 'multiple'});
+ let ref = useRef();
+ let {collection} = state;
+ let {gridProps} = useTable(props, state, ref);
+
+ return (
+
+
+ {collection.headerRows.map(headerRow => (
+
+ {[...headerRow.childNodes].map(column =>
+ column.props.isSelectionCell
+ ?
+ :
+ )}
+
+ ))}
+
+
+ {[...collection.body.childNodes].map(row => (
+
+ {[...row.childNodes].map(cell =>
+ cell.props.isSelectionCell
+ ?
+ :
+ )}
+
+ ))}
+
+
+ );
+}
+```
+
+### Table header
+
+A
hook will be used to group the rows in the table header and table body. In this example,
+we're using HTML table elements, so this will be either a `
` or `` element, as passed from the
+above `Table` component via the `type` prop.
+
+```tsx example export=true render=false
+function TableRowGroup({type: Element, style, children}) {
+ let {rowGroupProps} = useTableRowGroup();
+ return (
+
+ {children}
+
+ );
+}
+```
+
+The hook will be used to render a header row. Header rows are similar to other rows,
+but they don't support user interaction like selection. In this example, there's only one header
+row, but there could be multiple in the case of nested columns. See the [example below](#nested-columns) for details.
+
+```tsx example export=true render=false
+function TableHeaderRow({item, state, children}) {
+ let ref = useRef();
+ let {rowProps} = useTableHeaderRow({node: item}, state, ref);
+
+ return (
+
+ {children}
+
+ );
+}
+```
+
+The hook will be used to render each column header. Column headers act as a label
+for all of the cells in that column, and can optionally support user interaction to sort by the column
+and change the sort order.
+
+The `allowsSorting` property of the column object can be used to determine
+if the column supports sorting at all.
+
+The `sortDescriptor` object stored in the `state` object indicates which column the table is currently sorted by,
+as well as the sort direction (ascending or descending). This is used to render an arrow icon to visually
+indicate the sort direction. When not sorted by this column, we use `visibility: hidden` to ensure that
+we reserve space for this icon at all times. That way the table's layout doesn't shift when we change the
+column we're sorting by. See the [example below](#sorting) of all of this in action.
+
+Finally, we use the hook to ensure that a focus ring is rendered when
+the cell is navigated to with the keyboard.
+
+```tsx example export=true render=false
+function TableColumnHeader({column, state}) {
+ let ref = useRef();
+ let {columnHeaderProps} = useTableColumnHeader({node: column}, state, ref);
+ let {isFocusVisible, focusProps} = useFocusRing();
+ let arrowIcon = state.sortDescriptor?.direction === 'ascending' ? '▲' : '▼';
+
+ return (
+ 1 ? 'center' : 'left',
+ padding: '5px 10px',
+ outline: isFocusVisible ? '2px solid orange' : 'none',
+ cursor: 'default'
+ }}
+ ref={ref}>
+ {column.rendered}
+ {column.props.allowsSorting &&
+
+ {arrowIcon}
+
+ }
+ |
+ );
+}
+```
+
+### Table body
+
+Now that we've covered the table header, let's move on to the body. We'll use
+the hook to render each row in the table.
+Table rows can be focused and navigated to using the keyboard via the arrow keys. In addition, table rows
+can optionally support selection via mouse, touch, or keyboard. Clicking, tapping, or pressing the Space
+key anywhere in the row selects it.
+
+We'll use the object exposed
+by the `state` to determine if a row is selected, and render a pink background if so. We'll also use the
+hook to render a focus ring when the user navigates to the row with the keyboard.
+
+```tsx example export=true render=false
+function TableRow({item, children, state}) {
+ let ref = useRef();
+ let isSelected = state.selectionManager.isSelected(item.key);
+ let {rowProps} = useTableRow({node: item}, state, ref);
+ let {isFocusVisible, focusProps} = useFocusRing();
+
+ return (
+
+ {children}
+
+ );
+}
+```
+
+Finally, we'll use the hook to render each cell.
+Users can use the left and right arrow keys to navigate to each cell in a row, as well as any focusable elements
+within a cell. This is indicated by the focus ring, as created with the
+hook. The cell's contents are available in the `rendered` property of the cell
+object.
+
+```tsx example export=true render=false
+function TableCell({cell, state}) {
+ let ref = useRef();
+ let {gridCellProps} = useTableCell({node: cell}, state, ref);
+ let {isFocusVisible, focusProps} = useFocusRing();
+
+ return (
+
+ {cell.rendered}
+ |
+ );
+}
+```
+
+With all of the above components in place, we can render an example of our Table in action.
+This example shows a static collection, where all of the data is hard coded. [See below](#dynamic-collections)
+for examples of using this Table component with dynamic collections (e.g. from a server).
+
+Try tabbing into the table and navigating using the arrow keys.
+
+```tsx example
+
+
+ Name
+ Type
+ Date Modified
+
+
+
+ Games |
+ File folder |
+ 6/7/2020 |
+
+
+ Program Files |
+ File folder |
+ 4/7/2021 |
+
+
+ bootmgr |
+ System file |
+ 11/20/2010 |
+
+
+ log.txt |
+ Text Document |
+ 1/18/2016 |
+
+
+
+```
+
+### Adding selection
+
+Next, let's add support for selection. For multiple selection, we'll want to add a column of checkboxes to the left
+of the table to allow the user to select rows. This is done using the
+hook. It is passed the `parentKey` of the cell, which refers to the row the cell is contained within. When the user
+checks or unchecks the checkbox, the row will be added or removed from the Table's selection.
+
+In this example, we pass the result of the `checkboxProps` into the
+hook and render an `` element directly, but it's likely you'll have a `Checkbox` component in your component library that uses these hooks already.
+See the [useCheckbox docs](useCheckbox.html) for more information.
+
+```tsx example export=true render=false
+import {useToggleState} from '@react-stately/toggle';
+import {useCheckbox} from '@react-aria/checkbox';
+
+function TableCheckboxCell({cell, state}) {
+ let ref = useRef();
+ let {gridCellProps} = useTableCell({node: cell}, state, ref);
+ let {checkboxProps} = useTableSelectionCheckbox({key: cell.parentKey}, state);
+
+ let inputRef = useRef(null);
+ let {inputProps} = useCheckbox(checkboxProps, useToggleState(checkboxProps), inputRef);
+
+ return (
+
+
+ |
+ );
+}
+```
+
+We also want the user to be able to select all rows in the table at once. This is possible using the ⌘ Cmd + A
+keyboard shortcut, but we'll also add a checkbox into the table header to do this and represent the selection state visually.
+This is done using the hook. When all rows are selected,
+the checkbox will be shown as checked, and when only some rows are selected, the checkbox will be rendered in an indeterminate state.
+The user can check or uncheck the checkbox to select all or clear the selection, respectively.
+
+```tsx example export=true render=false
+function TableSelectAllCell({column, state}) {
+ let ref = useRef();
+ let isSingleSelectionMode = state.selectionManager.selectionMode === 'single';
+ let {columnHeaderProps} = useTableColumnHeader({node: column}, state, ref);
+
+ let {checkboxProps} = useTableSelectAllCheckbox(state);
+ let inputRef = useRef(null);
+ let {inputProps} = useCheckbox(checkboxProps, useToggleState(checkboxProps), inputRef);
+
+ return (
+
+ {
+ /*
+ In single selection mode, the checkbox will be hidden.
+ So to avoid leaving a column header with no accessible content,
+ use a VisuallyHidden component to include the aria-label from the checkbox,
+ which for single selection will be "Select."
+ */
+ isSingleSelectionMode &&
+ {inputProps['aria-label']}
+ }
+
+ |
+ );
+}
+```
+
+The following example shows how to enable multiple selection support using the Table component we built above.
+It's as simple as setting the `selectionMode` prop to `"multiple"`. Because we set the `showSelectionCheckboxes`
+option of `useTableState` to true when multiple selection is enabled, an extra column for these checkboxes is
+automatically added for us.
+
+```tsx example
+
+
+ Name
+ Type
+ Level
+
+
+
+ Charizard |
+ Fire, Flying |
+ 67 |
+
+
+ Blastoise |
+ Water |
+ 56 |
+
+
+ Venusaur |
+ Grass, Poison |
+ 83 |
+
+
+ Pikachu |
+ Electric |
+ 100 |
+
+
+
+```
+
+And that's it! We now have a fully interactive table component that can support keyboard navigation, single or multiple selection,
+as well as column sorting. In addition, it is fully accessible for screen readers and other assistive technology. See below for more
+examples of how to use the Table component that we've built.
+
+## Usage
+
+### Dynamic collections
+
+So far, our examples have shown static collections, where the data is hard coded.
+Dynamic collections, as shown below, can be used when the table data comes from an external data source such as an API, or updates over time.
+In the example below, both the columns and the rows are provided to the table via a render function. You can also make the columns static and
+only the rows dynamic.
+
+```tsx example export=true
+function ExampleTable(props) {
+ let columns = [
+ {name: 'Name', key: 'name'},
+ {name: 'Type', key: 'type'},
+ {name: 'Date Modified', key: 'date'}
+ ];
+
+ let rows = [
+ {id: 1, name: 'Games', date: '6/7/2020', type: 'File folder'},
+ {id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder'},
+ {id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file'},
+ {id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document'}
+ ];
+
+ return (
+
+
+ {column => (
+
+ {column.name}
+
+ )}
+
+
+ {item => (
+
+ {columnKey => {item[columnKey]} | }
+
+ )}
+
+
+ );
+}
+```
+
+### Single selection
+
+By default, `useTableState` doesn't allow row selection but this can be enabled using the `selectionMode` prop. Use `defaultSelectedKeys` to provide a default set of selected rows.
+Note that the value of the selected keys must match the `key` prop of the row.
+
+The example below enables single selection mode, and uses `defaultSelectedKeys` to select the row with key equal to "2".
+A user can click on a different row to change the selection, or click on the same row again to deselect it entirely.
+
+```tsx example
+// Using the example above
+
+```
+
+### Multiple selection
+
+Multiple selection can be enabled by setting `selectionMode` to `multiple`.
+
+```tsx example
+// Using the example above
+
+```
+
+### Disallow empty selection
+
+Table also supports a `disallowEmptySelection` prop which forces the user to have at least one row in the Table selected at all times.
+In this mode, if a single row is selected and the user presses it, it will not be deselected.
+
+```tsx example
+// Using the example above
+
+```
+
+### Controlled selection
+
+To programmatically control row selection, use the `selectedKeys` prop paired with the `onSelectionChange` callback. The `key` prop from the selected rows will
+be passed into the callback when the row is pressed, allowing you to update state accordingly.
+
+```tsx example export=true
+function PokemonTable(props) {
+ let columns = [
+ {name: 'Name', uid: 'name'},
+ {name: 'Type', uid: 'type'},
+ {name: 'Level', uid: 'level'}
+ ];
+
+ let rows = [
+ {id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67'},
+ {id: 2, name: 'Blastoise', type: 'Water', level: '56'},
+ {id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83'},
+ {id: 4, name: 'Pikachu', type: 'Electric', level: '100'}
+ ];
+
+ let [selectedKeys, setSelectedKeys] = React.useState(new Set([2]));
+
+ return (
+
+
+ {column => (
+
+ {column.name}
+
+ )}
+
+
+ {item => (
+
+ {columnKey => {item[columnKey]} | }
+
+ )}
+
+
+ );
+}
+```
+
+### Disabled rows
+
+You can disable specific rows by providing an array of keys to `useTableState` via the `disabledKeys` prop. This will prevent rows from being selectable as shown in the example below.
+Note that you are responsible for the styling of disabled rows, however, the selection checkbox will be automatically disabled.
+
+```tsx example
+// Using the same table as above
+
+```
+
+### Sorting
+
+Table supports sorting its data when a column header is pressed. To designate that a Column should support sorting, provide it with
+the `allowsSorting` prop. The Table accepts a `sortDescriptor` prop that defines the current column key to sort by and the sort direction (ascending/descending).
+When the user presses a sortable column header, the column's key and sort direction is passed into the `onSortChange` callback, allowing you to update
+the `sortDescriptor` appropriately.
+
+This example performs client side sorting by passing a `sort` function to the [useAsyncList](../react-stately/useAsyncList.html) hook.
+See the docs for more information on how to perform server side sorting.
+
+```tsx example
+import {useAsyncList} from '@react-stately/data';
+
+function AsyncSortTable() {
+ let list = useAsyncList({
+ async load({signal}) {
+ let res = await fetch(`https://swapi.dev/api/people/?search`, {signal});
+ let json = await res.json();
+ return {
+ items: json.results
+ };
+ },
+ async sort({items, sortDescriptor}) {
+ return {
+ items: items.sort((a, b) => {
+ let first = a[sortDescriptor.column];
+ let second = b[sortDescriptor.column];
+ let cmp = (parseInt(first) || first) < (parseInt(second) || second) ? -1 : 1;
+ if (sortDescriptor.direction === 'descending') {
+ cmp *= -1;
+ }
+ return cmp;
+ })
+ };
+ }
+ });
+
+ return (
+
+
+ Name
+ Height
+ Mass
+ Birth Year
+
+
+ {item => (
+
+ {columnKey => {item[columnKey]} | }
+
+ )}
+
+
+ );
+}
+```
+
+### Nested columns
+
+Columns can be nested to create column groups. This will result in more than one header row to be created, with the `colspan`
+attribute of each column header cell set to the appropriate value so that the columns line up. Data for the leaf columns
+appears in each row of the table body.
+
+This example also shows the use of the `isRowHeader` prop for `Column`, which controls which columns are included in the
+accessibility name for each row. By default, only the first column is included, but in some cases more than one column may
+be used to represent the row. In this example, the first and last name columns are combined to form the ARIA label for the row.
+Only leaf columns may be marked as row headers.
+
+```tsx example
+
+
+
+ First Name
+ Last Name
+
+
+ Age
+ Birthday
+
+
+
+
+ Sam |
+ Smith |
+ 36 |
+ May 3 |
+
+
+ Julia |
+ Jones |
+ 24 |
+ February 10 |
+
+
+ Peter |
+ Parker |
+ 28 |
+ September 7 |
+
+
+ Bruce |
+ Wayne |
+ 32 |
+ December 18 |
+
+
+
+```
+
+### Dynamic nested columns
+
+Nested columns can also be defined dynamically using the function syntax and the `childColumns` prop.
+The following example is the same as the example above, but defined dynamically.
+
+```tsx example
+let columns = [
+ {name: 'Name', key: 'name', children: [
+ {name: 'First Name', key: 'first', isRowHeader: true},
+ {name: 'Last Name', key: 'last', isRowHeader: true}
+ ]},
+ {name: 'Information', key: 'info', children: [
+ {name: 'Age', key: 'age'},
+ {name: 'Birthday', key: 'birthday'}
+ ]}
+];
+
+let rows = [
+ {id: 1, first: 'Sam', last: 'Smith', age: 36, birthday: 'May 3'},
+ {id: 2, first: 'Julia', last: 'Jones', age: 24, birthday: 'February 10'},
+ {id: 3, first: 'Peter', last: 'Parker', age: 28, birthday: 'September 7'},
+ {id: 4, first: 'Bruce', last: 'Wayne', age: 32, birthday: 'December 18'}
+];
+
+
+
+ {column => (
+
+ {column.name}
+
+ )}
+
+
+ {item => (
+
+ {columnKey => {item[columnKey]} | }
+
+ )}
+
+
+```
+
+## Internationalization
+
+`useTable` handles some aspects of internationalization automatically.
+For example, type to select is implemented with an
+[Intl.Collator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Collator)
+for internationalized string matching, and keyboard navigation is mirrored in right-to-left languages.
+You are responsible for localizing all text content within the table.
+
+### RTL
+
+In right-to-left languages, the table layout should be mirrored. The columns should be ordered from right to left and the
+individual column text alignment should be inverted. Ensure that your CSS accounts for this.
diff --git a/packages/@react-aria/table/intl/ar-AE.json b/packages/@react-aria/table/intl/ar-AE.json
new file mode 100644
index 00000000000..b113a16c271
--- /dev/null
+++ b/packages/@react-aria/table/intl/ar-AE.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "تصاعدي",
+ "ascendingSort": "ترتيب حسب العمود {columnName} بترتيب تصاعدي",
+ "descending": "تنازلي",
+ "descendingSort": "ترتيب حسب العمود {columnName} بترتيب تنازلي",
+ "select": "تحديد",
+ "selectAll": "تحديد الكل",
+ "sortable": "عمود قابل للترتيب"
+}
diff --git a/packages/@react-aria/table/intl/bg-BG.json b/packages/@react-aria/table/intl/bg-BG.json
new file mode 100644
index 00000000000..79c7e6594e1
--- /dev/null
+++ b/packages/@react-aria/table/intl/bg-BG.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "възходящ",
+ "ascendingSort": "сортирано по колона {columnName} във възходящ ред",
+ "descending": "низходящ",
+ "descendingSort": "сортирано по колона {columnName} в низходящ ред",
+ "select": "Изберете",
+ "selectAll": "Изберете всичко",
+ "sortable": "сортираща колона"
+}
diff --git a/packages/@react-aria/table/intl/cs-CZ.json b/packages/@react-aria/table/intl/cs-CZ.json
new file mode 100644
index 00000000000..2cf4c03d89c
--- /dev/null
+++ b/packages/@react-aria/table/intl/cs-CZ.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "vzestupně",
+ "ascendingSort": "řazeno vzestupně podle sloupce {columnName}",
+ "descending": "sestupně",
+ "descendingSort": "řazeno sestupně podle sloupce {columnName}",
+ "select": "Vybrat",
+ "selectAll": "Vybrat vše",
+ "sortable": "sloupec s možností řazení"
+}
diff --git a/packages/@react-aria/table/intl/da-DK.json b/packages/@react-aria/table/intl/da-DK.json
new file mode 100644
index 00000000000..f2269ab098e
--- /dev/null
+++ b/packages/@react-aria/table/intl/da-DK.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "stigende",
+ "ascendingSort": "sorteret efter kolonne {columnName} i stigende rækkefølge",
+ "descending": "faldende",
+ "descendingSort": "sorteret efter kolonne {columnName} i faldende rækkefølge",
+ "select": "Vælg",
+ "selectAll": "Vælg alle",
+ "sortable": "sorterbar kolonne"
+}
diff --git a/packages/@react-aria/table/intl/de-DE.json b/packages/@react-aria/table/intl/de-DE.json
new file mode 100644
index 00000000000..aff34133ef9
--- /dev/null
+++ b/packages/@react-aria/table/intl/de-DE.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "aufsteigend",
+ "ascendingSort": "sortiert nach Spalte {columnName} in aufsteigender Reihenfolge",
+ "descending": "absteigend",
+ "descendingSort": "sortiert nach Spalte {columnName} in absteigender Reihenfolge",
+ "select": "Auswählen",
+ "selectAll": "Alles auswählen",
+ "sortable": "sortierbare Spalte"
+}
diff --git a/packages/@react-aria/table/intl/el-GR.json b/packages/@react-aria/table/intl/el-GR.json
new file mode 100644
index 00000000000..184b3d14854
--- /dev/null
+++ b/packages/@react-aria/table/intl/el-GR.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "αύξουσα",
+ "ascendingSort": "διαλογή ανά στήλη {columnName} σε αύξουσα σειρά",
+ "descending": "φθίνουσα",
+ "descendingSort": "διαλογή ανά στήλη {columnName} σε φθίνουσα σειρά",
+ "select": "Επιλογή",
+ "selectAll": "Επιλογή όλων",
+ "sortable": "Στήλη διαλογής"
+}
diff --git a/packages/@react-aria/table/intl/en-US.json b/packages/@react-aria/table/intl/en-US.json
new file mode 100644
index 00000000000..165348b0d0b
--- /dev/null
+++ b/packages/@react-aria/table/intl/en-US.json
@@ -0,0 +1,9 @@
+{
+ "select": "Select",
+ "selectAll": "Select All",
+ "sortable": "sortable column",
+ "ascending": "ascending",
+ "descending": "descending",
+ "ascendingSort": "sorted by column {columnName} in ascending order",
+ "descendingSort": "sorted by column {columnName} in descending order"
+}
diff --git a/packages/@react-aria/table/intl/es-ES.json b/packages/@react-aria/table/intl/es-ES.json
new file mode 100644
index 00000000000..4ca7209cb9c
--- /dev/null
+++ b/packages/@react-aria/table/intl/es-ES.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "de subida",
+ "ascendingSort": "ordenado por columna {columnName} en orden de subida",
+ "descending": "de bajada",
+ "descendingSort": "ordenado por columna {columnName} en orden de bajada",
+ "select": "Seleccionar",
+ "selectAll": "Seleccionar todos",
+ "sortable": "columna ordenable"
+}
diff --git a/packages/@react-aria/table/intl/et-EE.json b/packages/@react-aria/table/intl/et-EE.json
new file mode 100644
index 00000000000..d26411d7cdc
--- /dev/null
+++ b/packages/@react-aria/table/intl/et-EE.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "tõusev järjestus",
+ "ascendingSort": "sorditud veeru järgi {columnName} tõusvas järjestuses",
+ "descending": "laskuv järjestus",
+ "descendingSort": "sorditud veeru järgi {columnName} laskuvas järjestuses",
+ "select": "Vali",
+ "selectAll": "Vali kõik",
+ "sortable": "sorditav veerg"
+}
diff --git a/packages/@react-aria/table/intl/fi-FI.json b/packages/@react-aria/table/intl/fi-FI.json
new file mode 100644
index 00000000000..8cbc20f7385
--- /dev/null
+++ b/packages/@react-aria/table/intl/fi-FI.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "nouseva",
+ "ascendingSort": "lajiteltu sarakkeen {columnName} mukaan nousevassa järjestyksessä",
+ "descending": "laskeva",
+ "descendingSort": "lajiteltu sarakkeen {columnName} mukaan laskevassa järjestyksessä",
+ "select": "Valitse",
+ "selectAll": "Valitse kaikki",
+ "sortable": "lajiteltava sarake"
+}
diff --git a/packages/@react-aria/table/intl/fr-FR.json b/packages/@react-aria/table/intl/fr-FR.json
new file mode 100644
index 00000000000..b8486b3217d
--- /dev/null
+++ b/packages/@react-aria/table/intl/fr-FR.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "croissant",
+ "ascendingSort": "trié en fonction de la colonne {columnName} par ordre croissant",
+ "descending": "décroissant",
+ "descendingSort": "trié en fonction de la colonne {columnName} par ordre décroissant",
+ "select": "Sélectionner",
+ "selectAll": "Sélectionner tout",
+ "sortable": "colonne triable"
+}
diff --git a/packages/@react-aria/table/intl/he-IL.json b/packages/@react-aria/table/intl/he-IL.json
new file mode 100644
index 00000000000..8564cb1105d
--- /dev/null
+++ b/packages/@react-aria/table/intl/he-IL.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "עולה",
+ "ascendingSort": "מוין לפי עמודה {columnName} בסדר עולה",
+ "descending": "יורד",
+ "descendingSort": "מוין לפי עמודה {columnName} בסדר יורד",
+ "select": "בחר",
+ "selectAll": "בחר הכול",
+ "sortable": "עמודה שניתן למיין"
+}
diff --git a/packages/@react-aria/table/intl/hr-HR.json b/packages/@react-aria/table/intl/hr-HR.json
new file mode 100644
index 00000000000..07df6a6e677
--- /dev/null
+++ b/packages/@react-aria/table/intl/hr-HR.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "rastući",
+ "ascendingSort": "razvrstano po stupcima {columnName} rastućem redoslijedom",
+ "descending": "padajući",
+ "descendingSort": "razvrstano po stupcima {columnName} padajućim redoslijedom",
+ "select": "Odaberite",
+ "selectAll": "Odaberite sve",
+ "sortable": "stupac koji se može razvrstati"
+}
diff --git a/packages/@react-aria/table/intl/hu-HU.json b/packages/@react-aria/table/intl/hu-HU.json
new file mode 100644
index 00000000000..8c4c0dabb8d
--- /dev/null
+++ b/packages/@react-aria/table/intl/hu-HU.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "növekvő",
+ "ascendingSort": "rendezve a(z) {columnName} oszlop szerint, növekvő sorrendben",
+ "descending": "csökkenő",
+ "descendingSort": "rendezve a(z) {columnName} oszlop szerint, csökkenő sorrendben",
+ "select": "Kijelölés",
+ "selectAll": "Összes kijelölése",
+ "sortable": "rendezendő oszlop"
+}
diff --git a/packages/@react-aria/table/intl/it-IT.json b/packages/@react-aria/table/intl/it-IT.json
new file mode 100644
index 00000000000..5bc848e5981
--- /dev/null
+++ b/packages/@react-aria/table/intl/it-IT.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "crescente",
+ "ascendingSort": "in ordine crescente in base alla colonna {columnName}",
+ "descending": "decrescente",
+ "descendingSort": "in ordine decrescente in base alla colonna {columnName}",
+ "select": "Seleziona",
+ "selectAll": "Seleziona tutto",
+ "sortable": "colonna ordinabile"
+}
diff --git a/packages/@react-aria/table/intl/ja-JP.json b/packages/@react-aria/table/intl/ja-JP.json
new file mode 100644
index 00000000000..6dedc21b000
--- /dev/null
+++ b/packages/@react-aria/table/intl/ja-JP.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "昇順",
+ "ascendingSort": "列 {columnName} を昇順で並べ替え",
+ "descending": "降順",
+ "descendingSort": "列 {columnName} を降順で並べ替え",
+ "select": "選択",
+ "selectAll": "すべて選択",
+ "sortable": "並べ替え可能な列"
+}
diff --git a/packages/@react-aria/table/intl/ko-KR.json b/packages/@react-aria/table/intl/ko-KR.json
new file mode 100644
index 00000000000..f37b5677060
--- /dev/null
+++ b/packages/@react-aria/table/intl/ko-KR.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "오름차순",
+ "ascendingSort": "{columnName} 열을 기준으로 오름차순으로 정렬됨",
+ "descending": "내림차순",
+ "descendingSort": "{columnName} 열을 기준으로 내림차순으로 정렬됨",
+ "select": "선택",
+ "selectAll": "모두 선택",
+ "sortable": "정렬 가능한 열"
+}
diff --git a/packages/@react-aria/table/intl/lt-LT.json b/packages/@react-aria/table/intl/lt-LT.json
new file mode 100644
index 00000000000..7657adf8460
--- /dev/null
+++ b/packages/@react-aria/table/intl/lt-LT.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "didėjančia tvarka",
+ "ascendingSort": "surikiuota pagal stulpelį {columnName} didėjančia tvarka",
+ "descending": "mažėjančia tvarka",
+ "descendingSort": "surikiuota pagal stulpelį {columnName} mažėjančia tvarka",
+ "select": "Pasirinkti",
+ "selectAll": "Pasirinkti viską",
+ "sortable": "rikiuojamas stulpelis"
+}
diff --git a/packages/@react-aria/table/intl/lv-LV.json b/packages/@react-aria/table/intl/lv-LV.json
new file mode 100644
index 00000000000..b591f7bb5d1
--- /dev/null
+++ b/packages/@react-aria/table/intl/lv-LV.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "augošā secībā",
+ "ascendingSort": "kārtots pēc kolonnas {columnName} augošā secībā",
+ "descending": "dilstošā secībā",
+ "descendingSort": "kārtots pēc kolonnas {columnName} dilstošā secībā",
+ "select": "Atlasīt",
+ "selectAll": "Atlasīt visu",
+ "sortable": "kārtojamā kolonna"
+}
diff --git a/packages/@react-aria/table/intl/nb-NO.json b/packages/@react-aria/table/intl/nb-NO.json
new file mode 100644
index 00000000000..a524b7c46af
--- /dev/null
+++ b/packages/@react-aria/table/intl/nb-NO.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "stigende",
+ "ascendingSort": "sortert etter kolonne {columnName} i stigende rekkefølge",
+ "descending": "synkende",
+ "descendingSort": "sortert etter kolonne {columnName} i synkende rekkefølge",
+ "select": "Velg",
+ "selectAll": "Velg alle",
+ "sortable": "kolonne som kan sorteres"
+}
diff --git a/packages/@react-aria/table/intl/nl-NL.json b/packages/@react-aria/table/intl/nl-NL.json
new file mode 100644
index 00000000000..0368f85e367
--- /dev/null
+++ b/packages/@react-aria/table/intl/nl-NL.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "oplopend",
+ "ascendingSort": "gesorteerd in oplopende volgorde in kolom {columnName}",
+ "descending": "aflopend",
+ "descendingSort": "gesorteerd in aflopende volgorde in kolom {columnName}",
+ "select": "Selecteren",
+ "selectAll": "Alles selecteren",
+ "sortable": "sorteerbare kolom"
+}
diff --git a/packages/@react-aria/table/intl/pl-PL.json b/packages/@react-aria/table/intl/pl-PL.json
new file mode 100644
index 00000000000..dec39e6ea69
--- /dev/null
+++ b/packages/@react-aria/table/intl/pl-PL.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "rosnąco",
+ "ascendingSort": "posortowano według kolumny {columnName} w porządku rosnącym",
+ "descending": "malejąco",
+ "descendingSort": "posortowano według kolumny {columnName} w porządku malejącym",
+ "select": "Zaznacz",
+ "selectAll": "Zaznacz wszystko",
+ "sortable": "kolumna z możliwością sortowania"
+}
diff --git a/packages/@react-aria/table/intl/pt-BR.json b/packages/@react-aria/table/intl/pt-BR.json
new file mode 100644
index 00000000000..db99b8ec0c8
--- /dev/null
+++ b/packages/@react-aria/table/intl/pt-BR.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "crescente",
+ "ascendingSort": "classificado pela coluna {columnName} em ordem crescente",
+ "descending": "decrescente",
+ "descendingSort": "classificado pela coluna {columnName} em ordem decrescente",
+ "select": "Selecionar",
+ "selectAll": "Selecionar tudo",
+ "sortable": "coluna classificável"
+}
diff --git a/packages/@react-aria/table/intl/pt-PT.json b/packages/@react-aria/table/intl/pt-PT.json
new file mode 100644
index 00000000000..1c21ac328c4
--- /dev/null
+++ b/packages/@react-aria/table/intl/pt-PT.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "ascendente",
+ "ascendingSort": "Ordenar por coluna {columnName} em ordem ascendente",
+ "descending": "descendente",
+ "descendingSort": "Ordenar por coluna {columnName} em ordem descendente",
+ "select": "Selecionar",
+ "selectAll": "Selecionar tudo",
+ "sortable": "Coluna ordenável"
+}
diff --git a/packages/@react-aria/table/intl/ro-RO.json b/packages/@react-aria/table/intl/ro-RO.json
new file mode 100644
index 00000000000..a453ec231f6
--- /dev/null
+++ b/packages/@react-aria/table/intl/ro-RO.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "crescătoare",
+ "ascendingSort": "sortate după coloana {columnName} în ordine crescătoare",
+ "descending": "descrescătoare",
+ "descendingSort": "sortate după coloana {columnName} în ordine descrescătoare",
+ "select": "Selectare",
+ "selectAll": "Selectare totală",
+ "sortable": "coloană sortabilă"
+}
diff --git a/packages/@react-aria/table/intl/ru-RU.json b/packages/@react-aria/table/intl/ru-RU.json
new file mode 100644
index 00000000000..11de78f0db8
--- /dev/null
+++ b/packages/@react-aria/table/intl/ru-RU.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "возрастание",
+ "ascendingSort": "сортировать столбец {columnName} в порядке возрастания",
+ "descending": "убывание",
+ "descendingSort": "сортировать столбец {columnName} в порядке убывания",
+ "select": "Выбрать",
+ "selectAll": "Выбрать все",
+ "sortable": "сортируемый столбец"
+}
diff --git a/packages/@react-aria/table/intl/sk-SK.json b/packages/@react-aria/table/intl/sk-SK.json
new file mode 100644
index 00000000000..3f7fa857900
--- /dev/null
+++ b/packages/@react-aria/table/intl/sk-SK.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "vzostupne",
+ "ascendingSort": "zoradené zostupne podľa stĺpca {columnName}",
+ "descending": "zostupne",
+ "descendingSort": "zoradené zostupne podľa stĺpca {columnName}",
+ "select": "Vybrať",
+ "selectAll": "Vybrať všetko",
+ "sortable": "zoraditeľný stĺpec"
+}
diff --git a/packages/@react-aria/table/intl/sl-SI.json b/packages/@react-aria/table/intl/sl-SI.json
new file mode 100644
index 00000000000..34f2c291018
--- /dev/null
+++ b/packages/@react-aria/table/intl/sl-SI.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "naraščajoče",
+ "ascendingSort": "razvrščeno po stolpcu {columnName} v naraščajočem vrstnem redu",
+ "descending": "padajoče",
+ "descendingSort": "razvrščeno po stolpcu {columnName} v padajočem vrstnem redu",
+ "select": "Izberite",
+ "selectAll": "Izberite vse",
+ "sortable": "razvrstljivi stolpec"
+}
diff --git a/packages/@react-aria/table/intl/sr-SP.json b/packages/@react-aria/table/intl/sr-SP.json
new file mode 100644
index 00000000000..4d2491e1afc
--- /dev/null
+++ b/packages/@react-aria/table/intl/sr-SP.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "rastući",
+ "ascendingSort": "sortirano po kolonama {columnName} rastućim redosledom",
+ "descending": "padajući",
+ "descendingSort": "sortirano po kolonama {columnName} padajućim redosledom",
+ "select": "Izaberite",
+ "selectAll": "Izaberite sve",
+ "sortable": "kolona koja se može sortirati"
+}
diff --git a/packages/@react-aria/table/intl/sv-SE.json b/packages/@react-aria/table/intl/sv-SE.json
new file mode 100644
index 00000000000..cd79ff392f1
--- /dev/null
+++ b/packages/@react-aria/table/intl/sv-SE.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "stigande",
+ "ascendingSort": "sorterat på kolumn {columnName} i stigande ordning",
+ "descending": "fallande",
+ "descendingSort": "sorterat på kolumn {columnName} i fallande ordning",
+ "select": "Markera",
+ "selectAll": "Markera allt",
+ "sortable": "sorterbar kolumn"
+}
diff --git a/packages/@react-aria/table/intl/tr-TR.json b/packages/@react-aria/table/intl/tr-TR.json
new file mode 100644
index 00000000000..f8255b6e858
--- /dev/null
+++ b/packages/@react-aria/table/intl/tr-TR.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "artan sırada",
+ "ascendingSort": "{columnName} sütuna göre artan düzende sırala",
+ "descending": "azalan sırada",
+ "descendingSort": "{columnName} sütuna göre azalan düzende sırala",
+ "select": "Seç",
+ "selectAll": "Tümünü Seç",
+ "sortable": "Sıralanabilir sütun"
+}
diff --git a/packages/@react-aria/table/intl/uk-UA.json b/packages/@react-aria/table/intl/uk-UA.json
new file mode 100644
index 00000000000..4566164563b
--- /dev/null
+++ b/packages/@react-aria/table/intl/uk-UA.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "висхідний",
+ "ascendingSort": "відсортовано за стовпцем {columnName} у висхідному порядку",
+ "descending": "низхідний",
+ "descendingSort": "відсортовано за стовпцем {columnName} у низхідному порядку",
+ "select": "Вибрати",
+ "selectAll": "Вибрати все",
+ "sortable": "сортувальний стовпець"
+}
diff --git a/packages/@react-aria/table/intl/zh-CN.json b/packages/@react-aria/table/intl/zh-CN.json
new file mode 100644
index 00000000000..24dea2998c0
--- /dev/null
+++ b/packages/@react-aria/table/intl/zh-CN.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "升序",
+ "ascendingSort": "按列 {columnName} 升序排序",
+ "descending": "降序",
+ "descendingSort": "按列 {columnName} 降序排序",
+ "select": "选择",
+ "selectAll": "全选",
+ "sortable": "可排序的列"
+}
diff --git a/packages/@react-aria/table/intl/zh-TW.json b/packages/@react-aria/table/intl/zh-TW.json
new file mode 100644
index 00000000000..98d97950ee4
--- /dev/null
+++ b/packages/@react-aria/table/intl/zh-TW.json
@@ -0,0 +1,9 @@
+{
+ "ascending": "遞增",
+ "ascendingSort": "已依據「{columnName}」欄遞增排序",
+ "descending": "遞減",
+ "descendingSort": "已依據「{columnName}」欄遞減排序",
+ "select": "選取",
+ "selectAll": "全選",
+ "sortable": "可排序的欄"
+}
diff --git a/packages/@react-aria/table/package.json b/packages/@react-aria/table/package.json
index 41b6e5c132b..3f6ba8918ba 100644
--- a/packages/@react-aria/table/package.json
+++ b/packages/@react-aria/table/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-aria/table",
- "version": "3.0.0-beta.0",
+ "version": "3.0.0-rc.0",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -18,21 +18,23 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/focus": "^3.4.0",
- "@react-aria/grid": "3.0.0-beta.0",
- "@react-aria/i18n": "^3.3.1",
- "@react-aria/interactions": "^3.5.0",
- "@react-aria/selection": "^3.5.0",
- "@react-aria/utils": "^3.8.1",
- "@react-stately/table": "3.0.0-beta.0",
- "@react-stately/virtualizer": "^3.1.4",
- "@react-types/checkbox": "^3.2.1",
- "@react-types/grid": "3.0.0-beta.0",
- "@react-types/shared": "^3.7.0",
- "@react-types/table": "3.0.0-beta.0"
+ "@react-aria/focus": "^3.4.1",
+ "@react-aria/grid": "3.0.0-rc.0",
+ "@react-aria/i18n": "^3.3.2",
+ "@react-aria/interactions": "^3.5.1",
+ "@react-aria/live-announcer": "^3.0.1",
+ "@react-aria/selection": "^3.5.1",
+ "@react-aria/utils": "^3.8.2",
+ "@react-stately/table": "3.0.0-rc.0",
+ "@react-stately/virtualizer": "^3.1.5",
+ "@react-types/checkbox": "^3.2.3",
+ "@react-types/grid": "3.0.0-rc.0",
+ "@react-types/shared": "^3.8.0",
+ "@react-types/table": "3.0.0-rc.0"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0-rc.1"
+ "react": "^16.8.0 || ^17.0.0-rc.1",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-aria/table/src/useTable.ts b/packages/@react-aria/table/src/useTable.ts
index 7f884b49135..4f5743db0bb 100644
--- a/packages/@react-aria/table/src/useTable.ts
+++ b/packages/@react-aria/table/src/useTable.ts
@@ -10,15 +10,19 @@
* governing permissions and limitations under the License.
*/
+import {announce} from '@react-aria/live-announcer';
import {GridAria, GridProps, useGrid} from '@react-aria/grid';
import {gridIds} from './utils';
+// @ts-ignore
+import intlMessages from '../intl/*.json';
import {Layout} from '@react-stately/virtualizer';
+import {mergeProps, useDescription, useId, useUpdateEffect} from '@react-aria/utils';
import {Node} from '@react-types/shared';
import {RefObject, useMemo} from 'react';
import {TableKeyboardDelegate} from './TableKeyboardDelegate';
import {TableState} from '@react-stately/table';
import {useCollator, useLocale} from '@react-aria/i18n';
-import {useId} from '@react-aria/utils';
+import {useMessageFormatter} from '@react-aria/i18n';
interface TableProps extends GridProps {
/** The layout object for the table. Computes what content is visible and how to position and style them. */
@@ -95,7 +99,21 @@ export function useTable(props: TableProps, state: TableState, ref: Ref
gridProps['aria-rowcount'] = state.collection.size + state.collection.headerRows.length;
}
+ let {column, direction: sortDirection} = state.sortDescriptor || {};
+ let formatMessage = useMessageFormatter(intlMessages);
+ let sortDescription = useMemo(() => {
+ let columnName = state.collection.columns.find(c => c.key === column)?.textValue;
+ return sortDirection && column ? formatMessage(`${sortDirection}Sort`, {columnName}) : undefined;
+ }, [sortDirection, column, state.collection.columns, formatMessage]);
+
+ let descriptionProps = useDescription(sortDescription);
+
+ // Only announce after initial render, tabbing to the table will tell you the initial sort info already
+ useUpdateEffect(() => {
+ announce(sortDescription, 'assertive', 500);
+ }, [sortDescription]);
+
return {
- gridProps
+ gridProps: mergeProps(gridProps, descriptionProps)
};
}
diff --git a/packages/@react-aria/table/src/useTableColumnHeader.ts b/packages/@react-aria/table/src/useTableColumnHeader.ts
index 56b1c936c4d..a7a5611a093 100644
--- a/packages/@react-aria/table/src/useTableColumnHeader.ts
+++ b/packages/@react-aria/table/src/useTableColumnHeader.ts
@@ -13,13 +13,15 @@
import {getColumnHeaderId} from './utils';
import {GridNode} from '@react-types/grid';
import {HTMLAttributes, RefObject} from 'react';
-import {mergeProps} from '@react-aria/utils';
+// @ts-ignore
+import intlMessages from '../intl/*.json';
+import {isAndroid, mergeProps, useDescription} from '@react-aria/utils';
import {TableState} from '@react-stately/table';
import {useFocusable} from '@react-aria/focus';
import {useGridCell} from '@react-aria/grid';
+import {useMessageFormatter} from '@react-aria/i18n';
import {usePress} from '@react-aria/interactions';
-
interface ColumnHeaderProps {
/** An object representing the [column header](https://www.w3.org/TR/wai-aria-1.1/#columnheader). Contains all the relevant information that makes up the column header. */
node: GridNode,
@@ -40,11 +42,12 @@ interface ColumnHeaderAria {
*/
export function useTableColumnHeader(props: ColumnHeaderProps, state: TableState, ref: RefObject): ColumnHeaderAria {
let {node} = props;
+ let allowsSorting = node.props.allowsSorting;
let {gridCellProps} = useGridCell(props, state, ref);
let isSelectionCellDisabled = node.props.isSelectionCell && state.selectionManager.selectionMode === 'single';
let {pressProps} = usePress({
- isDisabled: !node.props.allowsSorting || isSelectionCellDisabled,
+ isDisabled: !allowsSorting || isSelectionCellDisabled,
onPress() {
state.sort(node.key);
}
@@ -52,19 +55,34 @@ export function useTableColumnHeader(props: ColumnHeaderProps, state: TableSt
// Needed to pick up the focusable context, enabling things like Tooltips for example
let {focusableProps} = useFocusable({}, ref);
+
let ariaSort: HTMLAttributes['aria-sort'] = null;
- if (node.props.allowsSorting) {
- ariaSort = state.sortDescriptor?.column === node.key ? state.sortDescriptor.direction : 'none';
+ let isSortedColumn = state.sortDescriptor?.column === node.key;
+ let sortDirection = state.sortDescriptor?.direction;
+ // aria-sort not supported in Android Talkback
+ if (node.props.allowsSorting && !isAndroid()) {
+ ariaSort = isSortedColumn ? sortDirection : 'none';
+ }
+
+ let formatMessage = useMessageFormatter(intlMessages);
+ let sortDescription;
+ if (allowsSorting) {
+ sortDescription = `${formatMessage('sortable')}`;
+ // Android Talkback doesn't support aria-sort so we add sort order details to the aria-described by here
+ if (isSortedColumn && sortDirection && isAndroid()) {
+ sortDescription = `${sortDescription}, ${formatMessage(sortDirection)}`;
+ }
}
+ let descriptionProps = useDescription(sortDescription);
+
return {
columnHeaderProps: {
- ...mergeProps(gridCellProps, pressProps, focusableProps),
+ ...mergeProps(gridCellProps, pressProps, focusableProps, descriptionProps),
role: 'columnheader',
id: getColumnHeaderId(state, node.key),
'aria-colspan': node.colspan && node.colspan > 1 ? node.colspan : null,
- 'aria-sort': ariaSort,
- 'aria-disabled': isSelectionCellDisabled || undefined
+ 'aria-sort': ariaSort
}
};
}
diff --git a/packages/@react-aria/table/src/useTableSelectionCheckbox.ts b/packages/@react-aria/table/src/useTableSelectionCheckbox.ts
index 77a105d0336..375a0aa844e 100644
--- a/packages/@react-aria/table/src/useTableSelectionCheckbox.ts
+++ b/packages/@react-aria/table/src/useTableSelectionCheckbox.ts
@@ -12,9 +12,12 @@
import {AriaCheckboxProps} from '@react-types/checkbox';
import {getRowLabelledBy} from './utils';
+// @ts-ignore
+import intlMessages from '../intl/*.json';
import {Key} from 'react';
import {TableState} from '@react-stately/table';
-import {useId} from '@react-aria/utils';
+import {useGridSelectionCheckbox} from '@react-aria/grid';
+import {useMessageFormatter} from '@react-aria/i18n';
interface SelectionCheckboxProps {
/** A unique key for the checkbox. */
@@ -38,22 +41,12 @@ interface SelectAllCheckboxAria {
*/
export function useTableSelectionCheckbox(props: SelectionCheckboxProps, state: TableState): SelectionCheckboxAria {
let {key} = props;
-
- let manager = state.selectionManager;
- let checkboxId = useId();
- let isDisabled = state.disabledKeys.has(key);
- let isSelected = state.selectionManager.isSelected(key);
-
- let onChange = () => manager.select(key);
+ const {checkboxProps} = useGridSelectionCheckbox(props, state);
return {
checkboxProps: {
- id: checkboxId,
- 'aria-label': 'Select',
- 'aria-labelledby': `${checkboxId} ${getRowLabelledBy(state, key)}`,
- isSelected,
- isDisabled: isDisabled || manager.selectionMode === 'none',
- onChange
+ ...checkboxProps,
+ 'aria-labelledby': `${checkboxProps.id} ${getRowLabelledBy(state, key)}`
}
};
}
@@ -65,9 +58,11 @@ export function useTableSelectionCheckbox(props: SelectionCheckboxProps, stat
*/
export function useTableSelectAllCheckbox(state: TableState): SelectAllCheckboxAria {
let {isEmpty, isSelectAll, selectionMode} = state.selectionManager;
+ const formatMessage = useMessageFormatter(intlMessages);
+
return {
checkboxProps: {
- 'aria-label': 'Select All',
+ 'aria-label': formatMessage(selectionMode === 'single' ? 'select' : 'selectAll'),
isSelected: isSelectAll,
isDisabled: selectionMode !== 'multiple',
isIndeterminate: !isEmpty && !isSelectAll,
diff --git a/packages/@react-aria/table/stories/example.tsx b/packages/@react-aria/table/stories/example.tsx
new file mode 100644
index 00000000000..e426d793589
--- /dev/null
+++ b/packages/@react-aria/table/stories/example.tsx
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2021 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 {mergeProps} from '@react-aria/utils';
+import React from 'react';
+import {useCheckbox} from '@react-aria/checkbox';
+import {useFocusRing} from '@react-aria/focus';
+import {useRef} from 'react';
+import {useTable, useTableCell, useTableColumnHeader, useTableHeaderRow, useTableRow, useTableRowGroup, useTableSelectAllCheckbox, useTableSelectionCheckbox} from '@react-aria/table';
+import {useTableState} from '@react-stately/table';
+import {useToggleState} from '@react-stately/toggle';
+import {VisuallyHidden} from '@react-aria/visually-hidden';
+
+export function Table(props) {
+ let state = useTableState({...props, showSelectionCheckboxes: props.selectionMode === 'multiple'});
+ let ref = useRef();
+ let bodyRef = useRef();
+ let {collection} = state;
+ let {gridProps} = useTable({...props, scrollRef: bodyRef}, state, ref);
+
+ return (
+
+
+ {collection.headerRows.map(headerRow => (
+
+ {[...headerRow.childNodes].map(column =>
+ column.props.isSelectionCell
+ ?
+ :
+ )}
+
+ ))}
+
+
+ {[...collection.body.childNodes].map(row => (
+
+ {[...row.childNodes].map(cell =>
+ cell.props.isSelectionCell
+ ?
+ :
+ )}
+
+ ))}
+
+
+ );
+}
+
+const TableRowGroup = React.forwardRef((props: any, ref) => {
+ let {type: Element, style, children} = props;
+ let {rowGroupProps} = useTableRowGroup();
+ return (
+
+ {children}
+
+ );
+});
+
+function TableHeaderRow({item, state, children}) {
+ let ref = useRef();
+ let {rowProps} = useTableHeaderRow({node: item}, state, ref);
+
+ return (
+
+ {children}
+
+ );
+}
+
+function TableColumnHeader({column, state}) {
+ let ref = useRef();
+ let {columnHeaderProps} = useTableColumnHeader({node: column}, state, ref);
+ let {isFocusVisible, focusProps} = useFocusRing();
+ let arrowIcon = state.sortDescriptor?.direction === 'ascending' ? '▲' : '▼';
+
+ return (
+ 1 ? 'center' : 'left',
+ padding: '5px 10px',
+ outline: isFocusVisible ? '2px solid orange' : 'none',
+ cursor: 'default'
+ }}
+ ref={ref}>
+ {column.rendered}
+ {column.props.allowsSorting &&
+
+ {arrowIcon}
+
+ }
+ |
+ );
+}
+
+function TableRow({item, children, state}) {
+ let ref = useRef();
+ let isSelected = state.selectionManager.isSelected(item.key);
+ let {rowProps} = useTableRow({node: item}, state, ref);
+ let {isFocusVisible, focusProps} = useFocusRing();
+
+ return (
+
+ {children}
+
+ );
+}
+
+function TableCell({cell, state}) {
+ let ref = useRef();
+ let {gridCellProps} = useTableCell({node: cell}, state, ref);
+ let {isFocusVisible, focusProps} = useFocusRing();
+
+ return (
+
+ {cell.rendered}
+ |
+ );
+}
+
+function TableCheckboxCell({cell, state}) {
+ let ref = useRef();
+ let {gridCellProps} = useTableCell({node: cell}, state, ref);
+ let {checkboxProps} = useTableSelectionCheckbox({key: cell.parentKey}, state);
+
+ let inputRef = useRef(null);
+ let {inputProps} = useCheckbox(checkboxProps, useToggleState(checkboxProps), inputRef);
+
+ return (
+
+
+ |
+ );
+}
+
+function TableSelectAllCell({column, state}) {
+ let ref = useRef();
+ let isSingleSelectionMode = state.selectionManager.selectionMode === 'single';
+ let {columnHeaderProps} = useTableColumnHeader({node: column}, state, ref);
+
+ let {checkboxProps} = useTableSelectAllCheckbox(state);
+ let inputRef = useRef(null);
+ let {inputProps} = useCheckbox(checkboxProps, useToggleState(checkboxProps), inputRef);
+
+ return (
+
+ {
+ /*
+ In single selection mode, the checkbox will be hidden.
+ So to avoid leaving a column header with no accessible content,
+ use a VisuallyHidden component to include the aria-label from the checkbox,
+ which for single selection will be "Select."
+ */
+ isSingleSelectionMode &&
+ {inputProps['aria-label']}
+ }
+
+ |
+ );
+}
diff --git a/packages/@react-aria/table/stories/useTable.stories.tsx b/packages/@react-aria/table/stories/useTable.stories.tsx
new file mode 100644
index 00000000000..cb6bec37c15
--- /dev/null
+++ b/packages/@react-aria/table/stories/useTable.stories.tsx
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021 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 {Cell, Column, Row, TableBody, TableHeader} from '@react-stately/table';
+import React from 'react';
+import {Table} from './example';
+
+const meta = {
+ title: 'useTable'
+};
+
+export default meta;
+
+let columns = [
+ {name: 'Name', uid: 'name'},
+ {name: 'Type', uid: 'type'},
+ {name: 'Level', uid: 'level'}
+];
+
+let rows = [
+ {id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67'},
+ {id: 2, name: 'Blastoise', type: 'Water', level: '56'},
+ {id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83'},
+ {id: 4, name: 'Pikachu', type: 'Electric', level: '100'},
+ {id: 5, name: 'Charizard', type: 'Fire, Flying', level: '67'},
+ {id: 6, name: 'Blastoise', type: 'Water', level: '56'},
+ {id: 7, name: 'Venusaur', type: 'Grass, Poison', level: '83'},
+ {id: 8, name: 'Pikachu', type: 'Electric', level: '100'},
+ {id: 9, name: 'Charizard', type: 'Fire, Flying', level: '67'},
+ {id: 10, name: 'Blastoise', type: 'Water', level: '56'},
+ {id: 11, name: 'Venusaur', type: 'Grass, Poison', level: '83'},
+ {id: 12, name: 'Pikachu', type: 'Electric', level: '100'}
+];
+
+const Template = () => () => (
+ <>
+
+
+
+ {column => (
+
+ {column.name}
+
+ )}
+
+
+ {item => (
+
+ {columnKey => {item[columnKey]} | }
+
+ )}
+
+
+
+ >
+);
+
+export const ScrollTesting = Template().bind({});
+ScrollTesting.args = {};
diff --git a/packages/@react-aria/tabs/docs/useTabList.mdx b/packages/@react-aria/tabs/docs/useTabList.mdx
index 9a9ea00a2c3..3e668ddf37b 100644
--- a/packages/@react-aria/tabs/docs/useTabList.mdx
+++ b/packages/@react-aria/tabs/docs/useTabList.mdx
@@ -91,8 +91,6 @@ This example displays a basic list of tabs. The currently selected tab receives
```tsx example export=true
import {Item} from '@react-stately/collections';
-import {useFocus} from '@react-aria/interactions';
-import {mergeProps} from '@react-aria/utils';
function Tabs(props) {
let state = useTabListState(props);
@@ -125,7 +123,7 @@ function Tab({item, state}) {
borderBottom: isSelected ? '3px solid var(--blue)' : undefined,
opacity: isDisabled ? '0.5' : undefined
}}>
- {item.rendered}
+ {rendered}
);
}
diff --git a/packages/@react-aria/tabs/package.json b/packages/@react-aria/tabs/package.json
index d8b2775c4df..1c0e8999efd 100644
--- a/packages/@react-aria/tabs/package.json
+++ b/packages/@react-aria/tabs/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-aria/tabs",
- "version": "3.0.0",
+ "version": "3.0.1",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -18,15 +18,15 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/focus": "^3.4.0",
- "@react-aria/i18n": "^3.3.1",
- "@react-aria/interactions": "^3.5.0",
- "@react-aria/selection": "^3.5.0",
- "@react-aria/utils": "^3.8.1",
- "@react-stately/list": "^3.2.3",
- "@react-stately/tabs": "^3.0.0",
- "@react-types/shared": "^3.7.0",
- "@react-types/tabs": "^3.0.0"
+ "@react-aria/focus": "^3.4.1",
+ "@react-aria/i18n": "^3.3.2",
+ "@react-aria/interactions": "^3.5.1",
+ "@react-aria/selection": "^3.5.1",
+ "@react-aria/utils": "^3.8.2",
+ "@react-stately/list": "^3.3.0",
+ "@react-stately/tabs": "^3.0.1",
+ "@react-types/shared": "^3.8.0",
+ "@react-types/tabs": "^3.0.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1"
diff --git a/packages/@react-aria/tabs/src/useTabList.ts b/packages/@react-aria/tabs/src/useTabList.ts
index 52ef4df1ecd..cdb721cd50d 100644
--- a/packages/@react-aria/tabs/src/useTabList.ts
+++ b/packages/@react-aria/tabs/src/useTabList.ts
@@ -51,7 +51,8 @@ export function useTabList(props: AriaTabListProps, state: TabListState
selectionManager: manager,
keyboardDelegate: delegate,
selectOnFocus: keyboardActivation === 'automatic',
- disallowEmptySelection: true
+ disallowEmptySelection: true,
+ scrollRef: ref
});
// Compute base id for all tabs
diff --git a/packages/@react-aria/tabs/stories/example.tsx b/packages/@react-aria/tabs/stories/example.tsx
new file mode 100644
index 00000000000..42c118fec6e
--- /dev/null
+++ b/packages/@react-aria/tabs/stories/example.tsx
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2021 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 React from 'react';
+import {useTab, useTabList, useTabPanel} from '@react-aria/tabs';
+import {useTabListState} from '@react-stately/tabs';
+
+export function Tabs(props) {
+ let state = useTabListState(props);
+ let ref = React.useRef();
+ let {tabListProps} = useTabList(props, state, ref);
+ return (
+
+
+ {[...state.collection].map((item) => (
+
+ ))}
+
+
+
+ );
+}
+
+function Tab({item, state}) {
+ let {key, rendered} = item;
+ let ref = React.useRef();
+ let {tabProps} = useTab({key}, state, ref);
+ let isSelected = state.selectedKey === key;
+ let isDisabled = state.disabledKeys.has(key);
+ return (
+
+ {rendered}
+
+ );
+}
+
+function TabPanel({state, ...props}) {
+ let ref = React.useRef();
+ let {tabPanelProps} = useTabPanel(props, state, ref);
+ return (
+
+ {state.selectedItem?.props.children}
+
+ );
+}
diff --git a/packages/@react-aria/tabs/stories/useTabList.stories.tsx b/packages/@react-aria/tabs/stories/useTabList.stories.tsx
new file mode 100644
index 00000000000..8450c9f4705
--- /dev/null
+++ b/packages/@react-aria/tabs/stories/useTabList.stories.tsx
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 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 {Item} from '@react-stately/collections';
+import React from 'react';
+import {Tabs} from './example';
+
+const meta = {
+ title: 'useTabList'
+};
+
+export default meta;
+
+let lotsOfItems: any[] = [];
+for (let i = 0; i < 50; i++) {
+ lotsOfItems.push({name: 'Item ' + i, contents: 'Contents ' + i});
+}
+
+const Template = () => () => (
+
+ {(item) => (
+ -
+ {item.contents}
+
+ )}
+
+);
+
+export const ScrollTesting = Template().bind({});
+ScrollTesting.args = {};
diff --git a/packages/@react-aria/textfield/package.json b/packages/@react-aria/textfield/package.json
index 902fabba47d..847033ff4f3 100644
--- a/packages/@react-aria/textfield/package.json
+++ b/packages/@react-aria/textfield/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-aria/textfield",
- "version": "3.3.0",
+ "version": "3.3.1",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -18,11 +18,11 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/focus": "^3.3.0",
- "@react-aria/label": "^3.1.2",
- "@react-aria/utils": "^3.8.0",
- "@react-types/shared": "^3.6.0",
- "@react-types/textfield": "^3.2.2"
+ "@react-aria/focus": "^3.4.1",
+ "@react-aria/label": "^3.1.3",
+ "@react-aria/utils": "^3.8.2",
+ "@react-types/shared": "^3.8.0",
+ "@react-types/textfield": "^3.2.3"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1"
diff --git a/packages/@react-aria/textfield/src/useFormattedTextField.ts b/packages/@react-aria/textfield/src/useFormattedTextField.ts
index 5e44f185ef4..27809eff638 100644
--- a/packages/@react-aria/textfield/src/useFormattedTextField.ts
+++ b/packages/@react-aria/textfield/src/useFormattedTextField.ts
@@ -77,6 +77,10 @@ export function useFormattedTextField(props: AriaTextFieldProps, state: Formatte
? input.value.slice(0, input.selectionStart - 1) + input.value.slice(input.selectionStart)
: input.value.slice(0, input.selectionStart) + input.value.slice(input.selectionEnd);
break;
+ case 'deleteSoftLineBackward':
+ case 'deleteHardLineBackward':
+ nextValue = input.value.slice(input.selectionStart);
+ break;
default:
if (e.data != null) {
nextValue =
@@ -114,7 +118,7 @@ export function useFormattedTextField(props: AriaTextFieldProps, state: Formatte
}
: null;
- let {labelProps, inputProps: textFieldProps} = useTextField(props, inputRef);
+ let {labelProps, inputProps: textFieldProps, descriptionProps, errorMessageProps} = useTextField(props, inputRef);
let compositionStartState = useRef(null);
return {
@@ -150,6 +154,8 @@ export function useFormattedTextField(props: AriaTextFieldProps, state: Formatte
}
}
),
- labelProps
+ labelProps,
+ descriptionProps,
+ errorMessageProps
};
}
diff --git a/packages/@react-aria/textfield/src/useTextField.ts b/packages/@react-aria/textfield/src/useTextField.ts
index b971e744fad..236919a2503 100644
--- a/packages/@react-aria/textfield/src/useTextField.ts
+++ b/packages/@react-aria/textfield/src/useTextField.ts
@@ -12,16 +12,20 @@
import {AriaTextFieldProps} from '@react-types/textfield';
import {ChangeEvent, InputHTMLAttributes, LabelHTMLAttributes, RefObject, TextareaHTMLAttributes} from 'react';
-import {ElementType} from 'react';
+import {ElementType, HTMLAttributes} from 'react';
import {filterDOMProps, mergeProps} from '@react-aria/utils';
+import {useField} from '@react-aria/label';
import {useFocusable} from '@react-aria/focus';
-import {useLabel} from '@react-aria/label';
export interface TextFieldAria {
/** Props for the input element. */
inputProps: InputHTMLAttributes | TextareaHTMLAttributes,
- /** Props for the text field's visible label element (if any). */
- labelProps: LabelHTMLAttributes
+ /** Props for the text field's visible label element, if any. */
+ labelProps: LabelHTMLAttributes,
+ /** Props for the text field's description element, if any. */
+ descriptionProps: HTMLAttributes,
+ /** Props for the text field's error message element, if any. */
+ errorMessageProps: HTMLAttributes
}
interface AriaTextFieldOptions extends AriaTextFieldProps {
@@ -53,7 +57,7 @@ export function useTextField(
onChange = () => {}
} = props;
let {focusableProps} = useFocusable(props, ref);
- let {labelProps, fieldProps} = useLabel(props);
+ let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField(props);
let domProps = filterDOMProps(props, {labelable: true});
const inputOnlyProps = {
@@ -104,6 +108,8 @@ export function useTextField(
...focusableProps,
...fieldProps
}
- )
+ ),
+ descriptionProps,
+ errorMessageProps
};
}
diff --git a/packages/@react-aria/toggle/package.json b/packages/@react-aria/toggle/package.json
index 80d1b09811f..8d31690097c 100644
--- a/packages/@react-aria/toggle/package.json
+++ b/packages/@react-aria/toggle/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-aria/toggle",
- "version": "3.1.3",
+ "version": "3.1.4",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -18,13 +18,13 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/focus": "^3.3.0",
- "@react-aria/interactions": "^3.4.0",
- "@react-aria/utils": "^3.8.0",
- "@react-stately/toggle": "^3.2.2",
- "@react-types/checkbox": "^3.2.2",
- "@react-types/shared": "^3.6.0",
- "@react-types/switch": "^3.1.1"
+ "@react-aria/focus": "^3.4.1",
+ "@react-aria/interactions": "^3.5.1",
+ "@react-aria/utils": "^3.8.2",
+ "@react-stately/toggle": "^3.2.3",
+ "@react-types/checkbox": "^3.2.3",
+ "@react-types/shared": "^3.8.0",
+ "@react-types/switch": "^3.1.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1"
diff --git a/packages/@react-aria/tooltip/package.json b/packages/@react-aria/tooltip/package.json
index dd9f3765574..c63be5da553 100644
--- a/packages/@react-aria/tooltip/package.json
+++ b/packages/@react-aria/tooltip/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-aria/tooltip",
- "version": "3.1.2",
+ "version": "3.1.3",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -18,17 +18,15 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/focus": "^3.3.0",
- "@react-aria/interactions": "^3.4.0",
- "@react-aria/overlays": "^3.6.3",
- "@react-aria/utils": "^3.8.0",
- "@react-stately/tooltip": "^3.0.4",
- "@react-types/shared": "^3.6.0",
- "@react-types/tooltip": "^3.1.1"
+ "@react-aria/focus": "^3.4.1",
+ "@react-aria/interactions": "^3.5.1",
+ "@react-aria/utils": "^3.8.2",
+ "@react-stately/tooltip": "^3.0.5",
+ "@react-types/shared": "^3.8.0",
+ "@react-types/tooltip": "^3.1.2"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0-rc.1",
- "react-dom": "^16.8.0 || ^17.0.0-rc.1"
+ "react": "^16.8.0 || ^17.0.0-rc.1"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-aria/utils/package.json b/packages/@react-aria/utils/package.json
index c5fba6803da..27bc75b4498 100644
--- a/packages/@react-aria/utils/package.json
+++ b/packages/@react-aria/utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-aria/utils",
- "version": "3.8.1",
+ "version": "3.8.2",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -18,9 +18,9 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/ssr": "^3.0.2",
- "@react-stately/utils": "^3.2.1",
- "@react-types/shared": "^3.7.0",
+ "@react-aria/ssr": "^3.0.3",
+ "@react-stately/utils": "^3.2.2",
+ "@react-types/shared": "^3.8.0",
"clsx": "^1.1.1"
},
"peerDependencies": {
diff --git a/packages/@react-aria/utils/src/index.ts b/packages/@react-aria/utils/src/index.ts
index dc83b8cc20e..da877ae0d3f 100644
--- a/packages/@react-aria/utils/src/index.ts
+++ b/packages/@react-aria/utils/src/index.ts
@@ -29,3 +29,4 @@ export * from './getScrollParent';
export * from './useViewportSize';
export * from './useDescription';
export * from './platform';
+export * from './useEvent';
diff --git a/packages/@react-aria/utils/src/useEvent.ts b/packages/@react-aria/utils/src/useEvent.ts
new file mode 100644
index 00000000000..f7c1a6564f3
--- /dev/null
+++ b/packages/@react-aria/utils/src/useEvent.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 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 {RefObject, useEffect, useRef} from 'react';
+
+export function useEvent(
+ ref: RefObject,
+ event: K,
+ handler: (this: Document, ev: GlobalEventHandlersEventMap[K]) => any,
+ options?: boolean | AddEventListenerOptions
+) {
+ let handlerRef = useRef(handler);
+ handlerRef.current = handler;
+
+ let isDisabled = handler == null;
+
+ useEffect(() => {
+ if (isDisabled) {
+ return;
+ }
+
+ let element = ref.current;
+ let handler = (e: GlobalEventHandlersEventMap[K]) => handlerRef.current.call(this, e);
+
+ element.addEventListener(event, handler, options);
+ return () => {
+ element.removeEventListener(event, handler, options);
+ };
+ }, [ref, event, options, isDisabled]);
+}
diff --git a/packages/@react-aria/utils/src/useId.ts b/packages/@react-aria/utils/src/useId.ts
index 56f4eda18c8..8ad71ac6b3f 100644
--- a/packages/@react-aria/utils/src/useId.ts
+++ b/packages/@react-aria/utils/src/useId.ts
@@ -25,6 +25,9 @@ export function useId(defaultId?: string): string {
isRendering.current = true;
let [value, setValue] = useState(defaultId);
let nextId = useRef(null);
+
+ let res = useSSRSafeId(value);
+
// don't memo this, we want it new each render so that the Effects always run
// eslint-disable-next-line react-hooks/exhaustive-deps
let updateValue = (val) => {
@@ -35,10 +38,19 @@ export function useId(defaultId?: string): string {
}
};
+ idsUpdaterMap.set(res, updateValue);
+
useLayoutEffect(() => {
isRendering.current = false;
}, [updateValue]);
+ useLayoutEffect(() => {
+ let r = res;
+ return () => {
+ idsUpdaterMap.delete(r);
+ };
+ }, [res]);
+
useEffect(() => {
let newId = nextId.current;
if (newId) {
@@ -46,9 +58,6 @@ export function useId(defaultId?: string): string {
nextId.current = null;
}
}, [setValue, updateValue]);
-
- let res = useSSRSafeId(value);
- idsUpdaterMap.set(res, updateValue);
return res;
}
@@ -81,13 +90,16 @@ export function mergeIds(idA: string, idB: string): string {
* if we can use it in places such as labelledby.
*/
export function useSlotId(): string {
- let [id, setId] = useState(useId());
+ let id = useId();
+ let [resolvedId, setResolvedId] = useState(id);
useLayoutEffect(() => {
let setCurr = idsUpdaterMap.get(id);
if (setCurr && !document.getElementById(id)) {
- setId(null);
+ setResolvedId(null);
+ } else {
+ setResolvedId(id);
}
}, [id]);
- return id;
+ return resolvedId;
}
diff --git a/packages/@react-aria/virtualizer/package.json b/packages/@react-aria/virtualizer/package.json
index 45c4112cfdb..d6f2fdb8cbd 100644
--- a/packages/@react-aria/virtualizer/package.json
+++ b/packages/@react-aria/virtualizer/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-aria/virtualizer",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -18,10 +18,10 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/i18n": "^3.3.1",
- "@react-aria/utils": "^3.8.0",
- "@react-stately/virtualizer": "^3.1.4",
- "@react-types/shared": "^3.6.0"
+ "@react-aria/i18n": "^3.3.2",
+ "@react-aria/utils": "^3.8.2",
+ "@react-stately/virtualizer": "^3.1.5",
+ "@react-types/shared": "^3.8.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1",
diff --git a/packages/@react-aria/virtualizer/src/Virtualizer.tsx b/packages/@react-aria/virtualizer/src/Virtualizer.tsx
index 5fcaca82557..7ad6b1a1f4a 100644
--- a/packages/@react-aria/virtualizer/src/Virtualizer.tsx
+++ b/packages/@react-aria/virtualizer/src/Virtualizer.tsx
@@ -141,14 +141,16 @@ export function useVirtualizer(props: VirtualizerOptions
let onFocus = useCallback((e: FocusEvent) => {
// If the focused item is scrolled out of view and is not in the DOM, the collection
// will have tabIndex={0}. When tabbing in from outside, scroll the focused item into view.
- // We only want to do this if the collection itself is receiving focus, not a child
- // element, and we aren't moving focus to the collection from within (see below).
- if (e.target === ref.current && !isFocusWithin.current) {
- virtualizer.scrollToItem(focusedKey, {duration: 0});
+ if (!isFocusWithin.current) {
+ if (scrollToItem) {
+ scrollToItem(focusedKey);
+ } else {
+ virtualizer.scrollToItem(focusedKey, {duration: 0});
+ }
}
isFocusWithin.current = e.target !== ref.current;
- }, [ref, virtualizer, focusedKey]);
+ }, [ref, virtualizer, focusedKey, scrollToItem]);
let onBlur = useCallback((e: FocusEvent) => {
isFocusWithin.current = ref.current.contains(e.relatedTarget as Element);
diff --git a/packages/@react-aria/visually-hidden/package.json b/packages/@react-aria/visually-hidden/package.json
index e61f964e468..c65d0f9f0c4 100644
--- a/packages/@react-aria/visually-hidden/package.json
+++ b/packages/@react-aria/visually-hidden/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-aria/visually-hidden",
- "version": "3.2.2",
+ "version": "3.2.3",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -20,8 +20,8 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/interactions": "^3.4.0",
- "@react-aria/utils": "^3.8.0",
+ "@react-aria/interactions": "^3.5.1",
+ "@react-aria/utils": "^3.8.2",
"clsx": "^1.1.1"
},
"peerDependencies": {
diff --git a/packages/@react-spectrum/accordion/package.json b/packages/@react-spectrum/accordion/package.json
index 63be74b3596..43f9966bc63 100644
--- a/packages/@react-spectrum/accordion/package.json
+++ b/packages/@react-spectrum/accordion/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-spectrum/accordion",
- "version": "3.0.0-alpha.1",
+ "version": "3.0.0-alpha.2",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -32,24 +32,24 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/accordion": "3.0.0-alpha.1",
- "@react-aria/focus": "^3.3.0",
- "@react-aria/i18n": "^3.3.1",
- "@react-aria/interactions": "^3.4.0",
- "@react-aria/utils": "^3.8.0",
- "@react-spectrum/utils": "^3.5.2",
- "@react-stately/collections": "^3.3.2",
- "@react-stately/tree": "^3.1.4",
- "@react-types/accordion": "3.0.0-alpha.0",
- "@react-types/shared": "^3.6.0",
- "@spectrum-icons/ui": "^3.2.0"
+ "@react-aria/accordion": "3.0.0-alpha.2",
+ "@react-aria/focus": "^3.4.1",
+ "@react-aria/i18n": "^3.3.2",
+ "@react-aria/interactions": "^3.5.1",
+ "@react-aria/utils": "^3.8.2",
+ "@react-spectrum/utils": "^3.6.2",
+ "@react-stately/collections": "^3.3.3",
+ "@react-stately/tree": "^3.2.0",
+ "@react-types/accordion": "3.0.0-alpha.1",
+ "@react-types/shared": "^3.8.0",
+ "@spectrum-icons/ui": "^3.2.1"
},
"devDependencies": {
"@adobe/spectrum-css-temp": "3.0.0-alpha.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1",
- "@react-spectrum/provider": "^3.0.0-rc.1"
+ "@react-spectrum/provider": "^3.0.0"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-spectrum/actionbar/intl/ar-AE.json b/packages/@react-spectrum/actionbar/intl/ar-AE.json
new file mode 100644
index 00000000000..be9c7577455
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/ar-AE.json
@@ -0,0 +1,7 @@
+{
+ "actions": "الإجراءات",
+ "actionsAvailable": "الإجراءات المتاحة.",
+ "clearSelection": "إزالة التحديد",
+ "selected": "{count, plural, =0 {غير محدد} other {# محدد}}",
+ "selectedAll": "تم تحديد الكل"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/bg-BG.json b/packages/@react-spectrum/actionbar/intl/bg-BG.json
new file mode 100644
index 00000000000..4e479b3d679
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/bg-BG.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Действия",
+ "actionsAvailable": "Налични действия.",
+ "clearSelection": "Изчистване на избора",
+ "selected": "{count, plural, =0 {Няма избрани} other {# избрани}}",
+ "selectedAll": "Всички избрани"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/cs-CZ.json b/packages/@react-spectrum/actionbar/intl/cs-CZ.json
new file mode 100644
index 00000000000..47c7614d9b9
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/cs-CZ.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Akce",
+ "actionsAvailable": "Dostupné akce.",
+ "clearSelection": "Vymazat výběr",
+ "selected": "{count, plural, =0 {Nic není vybráno} other {Vybráno #}}",
+ "selectedAll": "Vybráno vše"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/da-DK.json b/packages/@react-spectrum/actionbar/intl/da-DK.json
new file mode 100644
index 00000000000..862484ba8d7
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/da-DK.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Handlinger",
+ "actionsAvailable": "Tilgængelige handlinger.",
+ "clearSelection": "Ryd markering",
+ "selected": "{count, plural, =0 {Ingen valgt} other {# valgt}}",
+ "selectedAll": "Alle valgt"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/de-DE.json b/packages/@react-spectrum/actionbar/intl/de-DE.json
new file mode 100644
index 00000000000..acf371d164c
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/de-DE.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Aktionen",
+ "actionsAvailable": "Aktionen verfügbar.",
+ "clearSelection": "Auswahl löschen",
+ "selected": "{count, plural, =0 {Nichts ausgewählt} other {# ausgewählt}}",
+ "selectedAll": "Alles ausgewählt"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/el-GR.json b/packages/@react-spectrum/actionbar/intl/el-GR.json
new file mode 100644
index 00000000000..dbb7f4f21d6
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/el-GR.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Ενέργειες",
+ "actionsAvailable": "Υπάρχουν διαθέσιμες ενέργειες.",
+ "clearSelection": "Εκκαθάριση επιλογής",
+ "selected": "{count, plural, =0 {Δεν επιλέχθηκε κανένα} other {Επιλέχθηκαν #}}",
+ "selectedAll": "Επιλέχθηκαν όλα"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/es-ES.json b/packages/@react-spectrum/actionbar/intl/es-ES.json
new file mode 100644
index 00000000000..b28b2d7eac3
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/es-ES.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Acciones",
+ "actionsAvailable": "Acciones disponibles.",
+ "clearSelection": "Borrar selección",
+ "selected": "{count, plural, =0 {Nada seleccionado} other {# seleccionado}}",
+ "selectedAll": "Todo seleccionado"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/et-EE.json b/packages/@react-spectrum/actionbar/intl/et-EE.json
new file mode 100644
index 00000000000..a8c2597417b
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/et-EE.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Toimingud",
+ "actionsAvailable": "Toimingud saadaval.",
+ "clearSelection": "Puhasta valik",
+ "selected": "{count, plural, =0 {Pole valitud} other {# valitud}}",
+ "selectedAll": "Kõik valitud"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/fi-FI.json b/packages/@react-spectrum/actionbar/intl/fi-FI.json
new file mode 100644
index 00000000000..24dab06be96
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/fi-FI.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Toiminnot",
+ "actionsAvailable": "Toiminnot käytettävissä.",
+ "clearSelection": "Poista valinta",
+ "selected": "{count, plural, =0 {Ei mitään valittu} other {# valittu}}",
+ "selectedAll": "Kaikki valittu"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/fr-FR.json b/packages/@react-spectrum/actionbar/intl/fr-FR.json
new file mode 100644
index 00000000000..27f3cf3b6f9
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/fr-FR.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Actions",
+ "actionsAvailable": "Actions disponibles.",
+ "clearSelection": "Supprimer la sélection",
+ "selected": "{count, plural, =0 {Aucun élément sélectionné} other {# sélectionnés}}",
+ "selectedAll": "Toute la sélection"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/he-IL.json b/packages/@react-spectrum/actionbar/intl/he-IL.json
new file mode 100644
index 00000000000..d175abb76a1
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/he-IL.json
@@ -0,0 +1,7 @@
+{
+ "actions": "פעולות",
+ "actionsAvailable": "פעולות זמינות.",
+ "clearSelection": "נקה בחירה",
+ "selected": "{count, plural, =0 {לא בוצעה בחירה} other {# נבחרו}}",
+ "selectedAll": "כל הפריטים שנבחרו"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/hr-HR.json b/packages/@react-spectrum/actionbar/intl/hr-HR.json
new file mode 100644
index 00000000000..cdb7951af8d
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/hr-HR.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Radnje",
+ "actionsAvailable": "Dostupne radnje.",
+ "clearSelection": "Poništi odabir",
+ "selected": "{count, plural, =0 {Nijedna nije odabrana} other {# je odabrano}}",
+ "selectedAll": "Sve je odabrano"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/hu-HU.json b/packages/@react-spectrum/actionbar/intl/hu-HU.json
new file mode 100644
index 00000000000..390354f2780
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/hu-HU.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Műveletek",
+ "actionsAvailable": "Műveletek állnak rendelkezésre.",
+ "clearSelection": "Kijelölés törlése",
+ "selected": "{count, plural, =0 {Egy sincs kijelölve} other {# kijelölve}}",
+ "selectedAll": "Mind kijelölve"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/it-IT.json b/packages/@react-spectrum/actionbar/intl/it-IT.json
new file mode 100644
index 00000000000..2e2edc1cf51
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/it-IT.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Azioni",
+ "actionsAvailable": "Azioni disponibili.",
+ "clearSelection": "Annulla selezione",
+ "selected": "{count, plural, =0 {Nessuno selezionato} other {# selezionato/i}}",
+ "selectedAll": "Tutti selezionati"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/ja-JP.json b/packages/@react-spectrum/actionbar/intl/ja-JP.json
new file mode 100644
index 00000000000..1e8514eac94
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/ja-JP.json
@@ -0,0 +1,7 @@
+{
+ "actions": "アクション",
+ "actionsAvailable": "アクションを利用できます。",
+ "clearSelection": "選択をクリア",
+ "selected": "{count, plural, =0 {選択されていません} other {# 個を選択しました}}",
+ "selectedAll": "すべてを選択"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/ko-KR.json b/packages/@react-spectrum/actionbar/intl/ko-KR.json
new file mode 100644
index 00000000000..6136957e26e
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/ko-KR.json
@@ -0,0 +1,7 @@
+{
+ "actions": "액션",
+ "actionsAvailable": "사용 가능한 액션",
+ "clearSelection": "선택 항목 지우기",
+ "selected": "{count, plural, =0 {선택된 항목 없음} other {#개 선택됨}}",
+ "selectedAll": "모두 선택됨"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/lt-LT.json b/packages/@react-spectrum/actionbar/intl/lt-LT.json
new file mode 100644
index 00000000000..58442363c69
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/lt-LT.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Veiksmai",
+ "actionsAvailable": "Galimi veiksmai.",
+ "clearSelection": "Išvalyti pasirinkimą",
+ "selected": "{count, plural, =0 {Nieko nepasirinkta} other {Pasirinkta #}}",
+ "selectedAll": "Pasirinkta viskas"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/lv-LV.json b/packages/@react-spectrum/actionbar/intl/lv-LV.json
new file mode 100644
index 00000000000..b82efb16e19
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/lv-LV.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Darbības",
+ "actionsAvailable": "Pieejamas darbības.",
+ "clearSelection": "Notīrīt atlasi",
+ "selected": "{count, plural, =0 {Nav atlasīts nekas} other {Atlasīts: #}}",
+ "selectedAll": "Atlasīts viss"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/nb-NO.json b/packages/@react-spectrum/actionbar/intl/nb-NO.json
new file mode 100644
index 00000000000..62fd10b1505
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/nb-NO.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Handlinger",
+ "actionsAvailable": "Tilgjengelige handlinger.",
+ "clearSelection": "Tøm utvalg",
+ "selected": "{count, plural, =0 {Ingen er valgt} other {# er valgt}}",
+ "selectedAll": "Alle er valgt"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/nl-NL.json b/packages/@react-spectrum/actionbar/intl/nl-NL.json
new file mode 100644
index 00000000000..68ee0e0e5fb
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/nl-NL.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Acties",
+ "actionsAvailable": "Acties beschikbaar.",
+ "clearSelection": "Selectie wissen",
+ "selected": "{count, plural, =0 {Niets geselecteerd} other {# geselecteerd}}",
+ "selectedAll": "Alles geselecteerd"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/pl-PL.json b/packages/@react-spectrum/actionbar/intl/pl-PL.json
new file mode 100644
index 00000000000..98d99635a9d
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/pl-PL.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Działania",
+ "actionsAvailable": "Dostępne działania.",
+ "clearSelection": "Wyczyść zaznaczenie",
+ "selected": "{count, plural, =0 {Nie zaznaczono żadnego elementu} other {# zaznaczonych}}",
+ "selectedAll": "Wszystkie zaznaczone"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/pt-BR.json b/packages/@react-spectrum/actionbar/intl/pt-BR.json
new file mode 100644
index 00000000000..48b16208bc1
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/pt-BR.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Ações",
+ "actionsAvailable": "Ações disponíveis.",
+ "clearSelection": "Limpar seleção",
+ "selected": "{count, plural, =0 {Nada selecionado} other {# selecionado}}",
+ "selectedAll": "Todos selecionados"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/pt-PT.json b/packages/@react-spectrum/actionbar/intl/pt-PT.json
new file mode 100644
index 00000000000..677e4dd52b2
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/pt-PT.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Ações",
+ "actionsAvailable": "Ações disponíveis.",
+ "clearSelection": "Limpar seleção",
+ "selected": "{count, plural, =0 {Nenhum selecionado} other {# selecionado}}",
+ "selectedAll": "Tudo selecionado"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/ro-RO.json b/packages/@react-spectrum/actionbar/intl/ro-RO.json
new file mode 100644
index 00000000000..19f81c7db37
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/ro-RO.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Acțiuni",
+ "actionsAvailable": "Acțiuni disponibile.",
+ "clearSelection": "Goliți selecția",
+ "selected": "{count, plural, =0 {Niciunul selectat} other {# selectate}}",
+ "selectedAll": "Toate selectate"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/ru-RU.json b/packages/@react-spectrum/actionbar/intl/ru-RU.json
new file mode 100644
index 00000000000..17b991e35e5
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/ru-RU.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Действия",
+ "actionsAvailable": "Возможно выполнение действий.",
+ "clearSelection": "Очистить выбор",
+ "selected": "{count, plural, =0 {Ничего не выбоано} other {# выбрано}}",
+ "selectedAll": "Выбрано все"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/sk-SK.json b/packages/@react-spectrum/actionbar/intl/sk-SK.json
new file mode 100644
index 00000000000..515bcb75fc8
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/sk-SK.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Akcie",
+ "actionsAvailable": "Dostupné akcie.",
+ "clearSelection": "Vymazať výber",
+ "selected": "{count, plural, =0 {Žiadne vybraté položky} other {Počet vybratých položiek: #}}",
+ "selectedAll": "Všetky vybraté položky"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/sl-SI.json b/packages/@react-spectrum/actionbar/intl/sl-SI.json
new file mode 100644
index 00000000000..eb948dc71ac
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/sl-SI.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Dejanja",
+ "actionsAvailable": "Na voljo so dejanja.",
+ "clearSelection": "Počisti izbor",
+ "selected": "{count, plural, =0 {Nič izbranih} other {# izbranih}}",
+ "selectedAll": "Vsi izbrani"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/sr-SP.json b/packages/@react-spectrum/actionbar/intl/sr-SP.json
new file mode 100644
index 00000000000..b27fcb0d4f9
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/sr-SP.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Radnje",
+ "actionsAvailable": "Dostupne su radnje.",
+ "clearSelection": "Poništi izbor",
+ "selected": "{count, plural, =0 {Ništa nije izabrano} other {# je izabrano}}",
+ "selectedAll": "Sve je izabrano"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/sv-SE.json b/packages/@react-spectrum/actionbar/intl/sv-SE.json
new file mode 100644
index 00000000000..6d21118d811
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/sv-SE.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Åtgärder",
+ "actionsAvailable": "Åtgärder finns.",
+ "clearSelection": "Rensa markering",
+ "selected": "{count, plural, =0 {Inga markerade} other {# markerade}}",
+ "selectedAll": "Alla markerade"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/tr-TR.json b/packages/@react-spectrum/actionbar/intl/tr-TR.json
new file mode 100644
index 00000000000..8243b768ee3
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/tr-TR.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Eylemler",
+ "actionsAvailable": "Eylemler mevcut.",
+ "clearSelection": "Seçimi temizle",
+ "selected": "{count, plural, =0 {Hiçbiri seçilmedi} other {# seçildi}}",
+ "selectedAll": "Tümü seçildi"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/uk-UA.json b/packages/@react-spectrum/actionbar/intl/uk-UA.json
new file mode 100644
index 00000000000..b2e0206ab9a
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/uk-UA.json
@@ -0,0 +1,7 @@
+{
+ "actions": "Дії",
+ "actionsAvailable": "Доступні дії.",
+ "clearSelection": "Очистити вибір",
+ "selected": "{count, plural, =0 {Нічого не вибрано} other {Вибрано: #}}",
+ "selectedAll": "Усе вибрано"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/zh-CN.json b/packages/@react-spectrum/actionbar/intl/zh-CN.json
new file mode 100644
index 00000000000..4d418ddb553
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/zh-CN.json
@@ -0,0 +1,7 @@
+{
+ "actions": "操作",
+ "actionsAvailable": "有可用操作。",
+ "clearSelection": "清除选择",
+ "selected": "{count, plural, =0 {无选择} other {已选择 # 个}}",
+ "selectedAll": "全选"
+}
diff --git a/packages/@react-spectrum/actionbar/intl/zh-TW.json b/packages/@react-spectrum/actionbar/intl/zh-TW.json
new file mode 100644
index 00000000000..625231005fa
--- /dev/null
+++ b/packages/@react-spectrum/actionbar/intl/zh-TW.json
@@ -0,0 +1,7 @@
+{
+ "actions": "動作",
+ "actionsAvailable": "可執行的動作。",
+ "clearSelection": "清除選取項目",
+ "selected": "{count, plural, =0 {未選取任何項目} other {已選取 # 個}}",
+ "selectedAll": "已選取所有項目"
+}
diff --git a/packages/@react-spectrum/actionbar/package.json b/packages/@react-spectrum/actionbar/package.json
index 983ce5ea43b..cf2a7d0b3f2 100644
--- a/packages/@react-spectrum/actionbar/package.json
+++ b/packages/@react-spectrum/actionbar/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-spectrum/actionbar",
- "version": "3.0.0-alpha.0",
+ "version": "3.0.0-alpha.2",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -31,31 +31,32 @@
"url": "https://github.com/adobe/react-spectrum"
},
"dependencies": {
- "@adobe/react-spectrum": "^3.11.0",
+ "@adobe/react-spectrum": "^3.13.0",
"@babel/runtime": "^7.6.2",
- "@react-aria/focus": "^3.4.0",
- "@react-aria/i18n": "^3.3.0",
- "@react-aria/interactions": "^3.5.0",
- "@react-aria/live-announcer": "^3.0.0",
- "@react-aria/utils": "^3.8.1",
- "@react-spectrum/actiongroup": "^3.2.0",
- "@react-spectrum/button": "^3.5.0",
- "@react-spectrum/layout": "^3.2.0",
- "@react-spectrum/overlays": "^3.4.2",
- "@react-spectrum/text": "^3.1.1",
- "@react-spectrum/utils": "^3.6.0",
- "@react-stately/collections": "^3.3.1",
- "@react-types/actionbar": "3.0.0-alpha.0",
- "@react-types/shared": "^3.7.0",
- "@spectrum-icons/ui": "^3.2.0",
- "@spectrum-icons/workflow": "^3.2.0"
+ "@react-aria/focus": "^3.4.1",
+ "@react-aria/i18n": "^3.3.2",
+ "@react-aria/interactions": "^3.5.1",
+ "@react-aria/live-announcer": "^3.0.1",
+ "@react-aria/utils": "^3.8.2",
+ "@react-spectrum/actiongroup": "^3.2.2",
+ "@react-spectrum/button": "^3.5.1",
+ "@react-spectrum/layout": "^3.2.1",
+ "@react-spectrum/overlays": "^3.4.4",
+ "@react-spectrum/text": "^3.1.3",
+ "@react-spectrum/utils": "^3.6.2",
+ "@react-stately/collections": "^3.3.3",
+ "@react-types/actionbar": "3.0.0-alpha.1",
+ "@react-types/shared": "^3.8.0",
+ "@spectrum-icons/ui": "^3.2.1",
+ "@spectrum-icons/workflow": "^3.2.1"
},
"devDependencies": {
"@adobe/spectrum-css-temp": "3.0.0-alpha.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1",
- "@react-spectrum/provider": "^3.0.0-rc.1"
+ "@react-spectrum/provider": "^3.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-spectrum/actiongroup/package.json b/packages/@react-spectrum/actiongroup/package.json
index 22b4bcc60b3..97f208adc71 100644
--- a/packages/@react-spectrum/actiongroup/package.json
+++ b/packages/@react-spectrum/actiongroup/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-spectrum/actiongroup",
- "version": "3.2.1",
+ "version": "3.2.2",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -32,33 +32,33 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/actiongroup": "^3.2.0",
- "@react-aria/button": "^3.3.2",
- "@react-aria/focus": "^3.4.0",
- "@react-aria/interactions": "^3.5.0",
- "@react-aria/selection": "^3.5.0",
- "@react-aria/utils": "^3.8.1",
- "@react-spectrum/button": "^3.5.0",
- "@react-spectrum/form": "^3.2.2",
- "@react-spectrum/menu": "^3.3.0",
- "@react-spectrum/provider": "^3.2.0",
- "@react-spectrum/text": "^3.1.2",
- "@react-spectrum/tooltip": "^3.1.3",
- "@react-spectrum/utils": "^3.6.0",
- "@react-stately/collections": "^3.3.2",
- "@react-stately/list": "^3.2.3",
- "@react-types/actiongroup": "^3.2.0",
- "@react-types/button": "^3.4.0",
- "@react-types/shared": "^3.7.0",
- "@spectrum-icons/ui": "^3.2.0",
- "@spectrum-icons/workflow": "^3.2.0"
+ "@react-aria/actiongroup": "^3.2.1",
+ "@react-aria/button": "^3.3.3",
+ "@react-aria/focus": "^3.4.1",
+ "@react-aria/interactions": "^3.5.1",
+ "@react-aria/selection": "^3.5.1",
+ "@react-aria/utils": "^3.8.2",
+ "@react-spectrum/button": "^3.5.1",
+ "@react-spectrum/form": "^3.2.3",
+ "@react-spectrum/menu": "^3.4.0",
+ "@react-spectrum/text": "^3.1.3",
+ "@react-spectrum/tooltip": "^3.1.4",
+ "@react-spectrum/utils": "^3.6.2",
+ "@react-stately/collections": "^3.3.3",
+ "@react-stately/list": "^3.3.0",
+ "@react-types/actiongroup": "^3.2.1",
+ "@react-types/button": "^3.4.1",
+ "@react-types/shared": "^3.8.0",
+ "@spectrum-icons/ui": "^3.2.1",
+ "@spectrum-icons/workflow": "^3.2.1"
},
"devDependencies": {
"@adobe/spectrum-css-temp": "3.0.0-alpha.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1",
- "@react-spectrum/provider": "^3.0.0-rc.1"
+ "@react-spectrum/provider": "^3.2.0",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-spectrum/actiongroup/src/ActionGroup.tsx b/packages/@react-spectrum/actiongroup/src/ActionGroup.tsx
index bf9e5641ccb..34d4546c38e 100644
--- a/packages/@react-spectrum/actiongroup/src/ActionGroup.tsx
+++ b/packages/@react-spectrum/actiongroup/src/ActionGroup.tsx
@@ -14,7 +14,15 @@ import {ActionButton} from '@react-spectrum/button';
import {AriaLabelingProps, DOMProps, DOMRef, Node, StyleProps} from '@react-types/shared';
import buttonStyles from '@adobe/spectrum-css-temp/components/button/vars.css';
import ChevronDownMedium from '@spectrum-icons/ui/ChevronDownMedium';
-import {classNames, SlotProvider, useDOMRef, useStyleProps, useValueEffect} from '@react-spectrum/utils';
+import {
+ classNames,
+ ClearSlots,
+ SlotProvider,
+ useDOMRef,
+ useSlotProps,
+ useStyleProps,
+ useValueEffect
+} from '@react-spectrum/utils';
import {filterDOMProps, mergeProps, useId, useLayoutEffect, useResizeObserver} from '@react-aria/utils';
import {Item, Menu, MenuTrigger} from '@react-spectrum/menu';
import {ListState, useListState} from '@react-stately/list';
@@ -32,6 +40,7 @@ import {useProviderProps} from '@react-spectrum/provider';
function ActionGroup(props: SpectrumActionGroupProps, ref: DOMRef) {
props = useProviderProps(props);
+ props = useSlotProps(props, 'actionGroup');
let {
isEmphasized,
@@ -284,40 +293,42 @@ function ActionGroupItem({item, state, isDisabled, isEmphasized, staticColor,
// Use a PressResponder to send DOM props through.
// ActionButton doesn't allow overriding the role by default.
-
-
+
+
- {item.rendered}
-
-
+ }
+ isDisabled={isDisabled}
+ staticColor={staticColor}
+ aria-label={item['aria-label']}
+ aria-labelledby={item['aria-label'] == null && hideButtonText ? textId : undefined}>
+ {item.rendered}
+
+
+
);
diff --git a/packages/@react-spectrum/alert/package.json b/packages/@react-spectrum/alert/package.json
index 5573ded5d0b..3e3de037fd5 100644
--- a/packages/@react-spectrum/alert/package.json
+++ b/packages/@react-spectrum/alert/package.json
@@ -43,7 +43,7 @@
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1",
- "@react-spectrum/provider": "^3.0.0-rc.1"
+ "@react-spectrum/provider": "^3.0.0"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-spectrum/avatar/README.md b/packages/@react-spectrum/avatar/README.md
new file mode 100644
index 00000000000..3815de7eda2
--- /dev/null
+++ b/packages/@react-spectrum/avatar/README.md
@@ -0,0 +1,3 @@
+# @react-spectrum/avatar
+
+This package is part of [react-spectrum](https://github.com/adobe/react-spectrum). See the repo for more details.
diff --git a/packages/@react-spectrum/avatar/chromatic/Avatar.chromatic.tsx b/packages/@react-spectrum/avatar/chromatic/Avatar.chromatic.tsx
new file mode 100644
index 00000000000..01f7c3d0994
--- /dev/null
+++ b/packages/@react-spectrum/avatar/chromatic/Avatar.chromatic.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 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 {Avatar} from '../';
+import {Meta, Story} from '@storybook/react';
+import React from 'react';
+import {SpectrumAvatarProps} from '@react-types/avatar';
+
+const SRC_URL_1 = 'https://mir-s3-cdn-cf.behance.net/project_modules/disp/690bc6105945313.5f84bfc9de488.png';
+const SRC_URL_2 = 'https://i.imgur.com/xIe7Wlb.png';
+
+const meta: Meta = {
+ title: 'Avatar',
+ component: Avatar
+};
+
+export default meta;
+
+const AvatarTemplate: Story = (args) => (
+
+);
+
+export const Default = AvatarTemplate.bind({});
+Default.args = {src: SRC_URL_1};
+Default.storyName = 'default';
+
+export const Disabled = AvatarTemplate.bind({});
+Disabled.args = {isDisabled: true, src: SRC_URL_1};
+Disabled.storyName = 'isDisabled';
+
+export const CustomSize = AvatarTemplate.bind({});
+CustomSize.args = {size: 'avatar-size-700', src: SRC_URL_2};
+CustomSize.storyName = 'with custom size';
diff --git a/packages/@react-spectrum/avatar/index.ts b/packages/@react-spectrum/avatar/index.ts
new file mode 100644
index 00000000000..b1a5da03bc4
--- /dev/null
+++ b/packages/@react-spectrum/avatar/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2021 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.
+ */
+
+export * from './src';
diff --git a/packages/@react-spectrum/avatar/package.json b/packages/@react-spectrum/avatar/package.json
new file mode 100644
index 00000000000..09ea7d45816
--- /dev/null
+++ b/packages/@react-spectrum/avatar/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@react-spectrum/avatar",
+ "version": "3.0.0-alpha.0",
+ "description": "Spectrum UI components in React",
+ "license": "Apache-2.0",
+ "private": true,
+ "main": "dist/main.js",
+ "module": "dist/module.js",
+ "types": "dist/types.d.ts",
+ "source": "src/index.ts",
+ "files": [
+ "dist",
+ "src"
+ ],
+ "sideEffects": [
+ "*.css"
+ ],
+ "targets": {
+ "main": {
+ "includeNodeModules": [
+ "@adobe/spectrum-css-temp"
+ ]
+ },
+ "module": {
+ "includeNodeModules": [
+ "@adobe/spectrum-css-temp"
+ ]
+ }
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/adobe/react-spectrum"
+ },
+ "dependencies": {
+ "@babel/runtime": "^7.6.2",
+ "@react-aria/utils": "^3.8.2",
+ "@react-spectrum/utils": "^3.6.1",
+ "@react-types/avatar": "3.0.0-alpha.0",
+ "@react-types/shared": "^3.7.1"
+ },
+ "devDependencies": {
+ "@adobe/spectrum-css-temp": "3.0.0-alpha.1"
+ },
+ "peerDependencies": {
+ "@react-spectrum/provider": "^3.2.1",
+ "react": "^16.8.0 || ^17.0.0-rc.1"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/@react-spectrum/avatar/src/Avatar.tsx b/packages/@react-spectrum/avatar/src/Avatar.tsx
new file mode 100644
index 00000000000..045309c14f3
--- /dev/null
+++ b/packages/@react-spectrum/avatar/src/Avatar.tsx
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2021 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 {classNames, dimensionValue, useDOMRef, useStyleProps} from '@react-spectrum/utils';
+import {DOMRef} from '@react-types/shared';
+import {filterDOMProps} from '@react-aria/utils';
+import React, {forwardRef} from 'react';
+import {SpectrumAvatarProps} from '@react-types/avatar';
+import styles from '@adobe/spectrum-css-temp/components/avatar/vars.css';
+import {useProviderProps} from '@react-spectrum/provider';
+
+const DEFAULT_SIZE = 'avatar-size-100';
+const SIZE_RE = /^size-\d+/;
+
+function Avatar(props: SpectrumAvatarProps, ref: DOMRef) {
+ const {
+ alt = '',
+ isDisabled,
+ size,
+ src,
+ ...otherProps
+ } = useProviderProps(props);
+
+ const {styleProps} = useStyleProps(otherProps);
+ const domRef = useDOMRef(ref);
+
+ const domProps = filterDOMProps(otherProps);
+
+ // Casting `size` as `any` since `isNaN` expects a `number`, but we want it
+ // to handle `string` numbers; e.g. '300' as opposed to 300
+ const sizeValue = typeof size !== 'number' && (SIZE_RE.test(size) || !isNaN(size as any))
+ ? dimensionValue(DEFAULT_SIZE) // override disallowed size values
+ : dimensionValue(size || DEFAULT_SIZE);
+
+ return (
+
+ );
+}
+
+/**
+ * An avatar is a thumbnail representation of an entity, such as a user or an organization.
+ */
+const _Avatar = forwardRef(Avatar);
+export {_Avatar as Avatar};
diff --git a/packages/@react-spectrum/avatar/src/index.ts b/packages/@react-spectrum/avatar/src/index.ts
new file mode 100644
index 00000000000..c612f78a480
--- /dev/null
+++ b/packages/@react-spectrum/avatar/src/index.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2021 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.
+ */
+
+///
+
+export * from './Avatar';
diff --git a/packages/@react-spectrum/avatar/stories/Avatar.stories.tsx b/packages/@react-spectrum/avatar/stories/Avatar.stories.tsx
new file mode 100644
index 00000000000..86c9c1c7be3
--- /dev/null
+++ b/packages/@react-spectrum/avatar/stories/Avatar.stories.tsx
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2021 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 {Avatar} from '../';
+import {Meta, Story} from '@storybook/react';
+import React from 'react';
+import {SpectrumAvatarProps} from '@react-types/avatar';
+
+const SRC_URL_1 = 'https://mir-s3-cdn-cf.behance.net/project_modules/disp/690bc6105945313.5f84bfc9de488.png';
+const SRC_URL_2 = 'https://i.imgur.com/xIe7Wlb.png';
+
+const meta: Meta = {
+ title: 'Avatar',
+ component: Avatar
+};
+
+export default meta;
+
+const AvatarTemplate: Story = (args) => (
+
+);
+
+export const Default = AvatarTemplate.bind({});
+Default.args = {src: SRC_URL_1};
+Default.storyName = 'default';
+
+export const Disabled = AvatarTemplate.bind({});
+Disabled.args = {isDisabled: true, src: SRC_URL_1};
+Disabled.storyName = 'isDisabled';
+
+export const WithAltText = AvatarTemplate.bind({});
+WithAltText.args = {alt: 'Pensive', src: SRC_URL_2};
+WithAltText.storyName = 'with alt text';
+
+export const CustomSize = AvatarTemplate.bind({});
+CustomSize.args = {...WithAltText.args, size: 'avatar-size-700'};
+CustomSize.storyName = 'with custom size';
diff --git a/packages/@react-spectrum/avatar/test/Avatar.test.js b/packages/@react-spectrum/avatar/test/Avatar.test.js
new file mode 100644
index 00000000000..21c23e1519b
--- /dev/null
+++ b/packages/@react-spectrum/avatar/test/Avatar.test.js
@@ -0,0 +1,77 @@
+import {Avatar} from '../';
+import React from 'react';
+import {render, screen} from '@testing-library/react';
+import * as Utils from '@react-spectrum/utils';
+
+describe('Avatar', () => {
+ it('renders an avatar image', () => {
+ render();
+ expect(screen.getByRole('img')).toBeInTheDocument();
+ expect(screen.getByRole('img')).toHaveAttribute('src', 'http://localhost/some_image.png');
+ });
+
+ it('can render an avatar image with an alt', () => {
+ render();
+ expect(screen.getByAltText(/test avatar/i)).toBeInTheDocument();
+ });
+
+ describe('when given a custom size', () => {
+ it('supports custom sizes in units, such as pixels', () => {
+ render();
+ expect(screen.getByRole('img')).toHaveStyle({
+ height: '80px',
+ width: '80px'
+ });
+ });
+
+ it('supports custom sizes in numbers', () => {
+ render();
+ expect(screen.getByRole('img')).toHaveStyle({
+ height: '80px',
+ width: '80px'
+ });
+ });
+
+ // Spying on dimensionValue since we're unable to use toHaveStyle effectively with CSS vars
+ // See https://github.com/testing-library/jest-dom/issues/322
+ it('supports predefined avatar sizes', () => {
+ const dimensionValueSpy = jest.spyOn(Utils, 'dimensionValue');
+ render();
+ expect(dimensionValueSpy).not.toHaveBeenCalledWith('avatar-size-100');
+ });
+
+ it('defaults to default size when size is size-XXXX', () => {
+ const dimensionValueSpy = jest.spyOn(Utils, 'dimensionValue');
+ render();
+ expect(dimensionValueSpy).toHaveBeenCalledWith('avatar-size-100');
+ });
+
+ it('defaults to default size when size is a string number', () => {
+ const dimensionValueSpy = jest.spyOn(Utils, 'dimensionValue');
+ render();
+ expect(dimensionValueSpy).toHaveBeenCalledWith('avatar-size-100');
+ });
+ });
+
+ it('supports custom class names', () => {
+ render();
+ expect(screen.getByRole('img')).toHaveAttribute('class', expect.stringContaining('my-class'));
+ });
+
+ it('supports style props', () => {
+ render();
+ expect(screen.getByRole('img', {hidden: true})).toBeInTheDocument();
+ });
+
+ it('supports custom DOM props', () => {
+ render();
+ expect(screen.getByTestId(/test avatar/i)).toBeInTheDocument();
+ });
+
+ describe('when isDisabled = true', () => {
+ it('renders a disabled avatar image', () => {
+ render();
+ expect(screen.getByRole('img')).toHaveAttribute('class', expect.stringMatching(/disabled/i));
+ });
+ });
+});
diff --git a/packages/@react-spectrum/breadcrumbs/docs/Breadcrumbs.mdx b/packages/@react-spectrum/breadcrumbs/docs/Breadcrumbs.mdx
index 782fa4c946b..53b9c3f1263 100644
--- a/packages/@react-spectrum/breadcrumbs/docs/Breadcrumbs.mdx
+++ b/packages/@react-spectrum/breadcrumbs/docs/Breadcrumbs.mdx
@@ -131,7 +131,7 @@ This variation keeps the root visible when other items are truncated into the me
```tsx example
-
+
- Home
- Trendy
- March 2020 Assets
@@ -175,7 +175,7 @@ Resize your browser window to see the above behavior in the examples below.
```
```tsx example
-
+
- My Shared Documents
- North America Spring Catalogue
- March 2020
diff --git a/packages/@react-spectrum/breadcrumbs/package.json b/packages/@react-spectrum/breadcrumbs/package.json
index f7d4602f75c..117cd370653 100644
--- a/packages/@react-spectrum/breadcrumbs/package.json
+++ b/packages/@react-spectrum/breadcrumbs/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-spectrum/breadcrumbs",
- "version": "3.2.2",
+ "version": "3.2.3",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -32,25 +32,26 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/breadcrumbs": "^3.1.4",
- "@react-aria/focus": "^3.3.0",
- "@react-aria/i18n": "^3.3.1",
- "@react-aria/interactions": "^3.4.0",
- "@react-aria/utils": "^3.8.0",
- "@react-spectrum/button": "^3.4.1",
- "@react-spectrum/menu": "^3.2.3",
- "@react-spectrum/utils": "^3.5.2",
- "@react-stately/collections": "^3.3.2",
- "@react-types/breadcrumbs": "^3.1.1",
- "@react-types/shared": "^3.6.0",
- "@spectrum-icons/ui": "^3.2.0"
+ "@react-aria/breadcrumbs": "^3.1.5",
+ "@react-aria/focus": "^3.4.1",
+ "@react-aria/i18n": "^3.3.2",
+ "@react-aria/interactions": "^3.5.1",
+ "@react-aria/utils": "^3.8.2",
+ "@react-spectrum/button": "^3.5.1",
+ "@react-spectrum/menu": "^3.4.0",
+ "@react-spectrum/utils": "^3.6.2",
+ "@react-stately/collections": "^3.3.3",
+ "@react-types/breadcrumbs": "^3.2.1",
+ "@react-types/shared": "^3.8.0",
+ "@spectrum-icons/ui": "^3.2.1"
},
"devDependencies": {
"@adobe/spectrum-css-temp": "3.0.0-alpha.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1",
- "@react-spectrum/provider": "^3.0.0-rc.1"
+ "@react-spectrum/provider": "^3.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-spectrum/breadcrumbs/src/Breadcrumbs.tsx b/packages/@react-spectrum/breadcrumbs/src/Breadcrumbs.tsx
index fe2ab11e1dd..fdb3aae61f6 100644
--- a/packages/@react-spectrum/breadcrumbs/src/Breadcrumbs.tsx
+++ b/packages/@react-spectrum/breadcrumbs/src/Breadcrumbs.tsx
@@ -56,8 +56,14 @@ function Breadcrumbs(props: SpectrumBreadcrumbsProps, ref: DOMRef) {
let updateOverflow = useCallback(() => {
let computeVisibleItems = (visibleItems: number) => {
- let listItems = Array.from(listRef.current.children) as HTMLLIElement[];
- let containerWidth = listRef.current.offsetWidth;
+ // Refs can be null at runtime.
+ let currListRef: HTMLUListElement | null = listRef.current;
+ if (!currListRef) {
+ return;
+ }
+
+ let listItems = Array.from(currListRef.children) as HTMLLIElement[];
+ let containerWidth = currListRef.offsetWidth;
let isShowingMenu = childArray.length > visibleItems;
let calculatedWidth = 0;
let newVisibleItems = 0;
diff --git a/packages/@react-spectrum/button/chromatic/ToggleButton.chromatic.tsx b/packages/@react-spectrum/button/chromatic/ToggleButton.chromatic.tsx
index 4b47b77512e..ba5c528d743 100644
--- a/packages/@react-spectrum/button/chromatic/ToggleButton.chromatic.tsx
+++ b/packages/@react-spectrum/button/chromatic/ToggleButton.chromatic.tsx
@@ -11,8 +11,8 @@
*/
import {classNames} from '@react-spectrum/utils';
+import {generatePowerset} from '@react-spectrum/story-utils';
import {Grid, repeat, View} from '@adobe/react-spectrum';
-import {mergeProps} from '@react-aria/utils';
import React from 'react';
import {storiesOf} from '@storybook/react';
import styles from '@adobe/spectrum-css-temp/components/button/vars.css';
@@ -28,21 +28,7 @@ let states = [
{UNSAFE_className: classNames(styles, 'focus-ring')}
];
-// Generate a powerset of the options
-let combinations: any[] = [{}];
-for (let i = 0; i < states.length; i++) {
- let len = combinations.length;
- for (let j = 0; j < len; j++) {
- let merged = mergeProps(combinations[j], states[i]);
-
- // Ignore disabled combined with interactive states.
- if (merged.isDisabled && merged.UNSAFE_className) {
- continue;
- }
-
- combinations.push(merged);
- }
-}
+let combinations = generatePowerset(states, (merged) => merged.isDisabled && merged.UNSAFE_className);
storiesOf('Button/ToggleButton', module)
.addParameters({providerSwitcher: {status: 'positive'}})
diff --git a/packages/@react-spectrum/button/package.json b/packages/@react-spectrum/button/package.json
index 118127c0253..923cb18834b 100644
--- a/packages/@react-spectrum/button/package.json
+++ b/packages/@react-spectrum/button/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-spectrum/button",
- "version": "3.5.0",
+ "version": "3.5.1",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -32,16 +32,16 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/button": "^3.3.2",
- "@react-aria/focus": "^3.4.0",
- "@react-aria/interactions": "^3.5.0",
- "@react-aria/utils": "^3.8.1",
- "@react-spectrum/text": "^3.1.2",
- "@react-spectrum/utils": "^3.6.0",
- "@react-stately/toggle": "^3.2.2",
- "@react-types/button": "^3.4.0",
- "@react-types/shared": "^3.7.0",
- "@spectrum-icons/ui": "^3.2.0"
+ "@react-aria/button": "^3.3.3",
+ "@react-aria/focus": "^3.4.1",
+ "@react-aria/interactions": "^3.5.1",
+ "@react-aria/utils": "^3.8.2",
+ "@react-spectrum/text": "^3.1.3",
+ "@react-spectrum/utils": "^3.6.2",
+ "@react-stately/toggle": "^3.2.3",
+ "@react-types/button": "^3.4.1",
+ "@react-types/shared": "^3.8.0",
+ "@spectrum-icons/ui": "^3.2.1"
},
"devDependencies": {
"@adobe/spectrum-css-temp": "3.0.0-alpha.1",
@@ -49,7 +49,7 @@
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1",
- "@react-spectrum/provider": "^3.0.0-rc.1"
+ "@react-spectrum/provider": "^3.0.0"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-spectrum/button/src/ActionButton.tsx b/packages/@react-spectrum/button/src/ActionButton.tsx
index eb0d69e8fef..2404310f0fb 100644
--- a/packages/@react-spectrum/button/src/ActionButton.tsx
+++ b/packages/@react-spectrum/button/src/ActionButton.tsx
@@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/
-import {classNames, SlotProvider, useFocusableRef, useStyleProps} from '@react-spectrum/utils';
+import {classNames, SlotProvider, useFocusableRef, useSlotProps, useStyleProps} from '@react-spectrum/utils';
import {FocusableRef} from '@react-types/shared';
import {FocusRing} from '@react-aria/focus';
import {mergeProps} from '@react-aria/utils';
@@ -24,6 +24,7 @@ import {useProviderProps} from '@react-spectrum/provider';
function ActionButton(props: SpectrumActionButtonProps, ref: FocusableRef) {
props = useProviderProps(props);
+ props = useSlotProps(props, 'actionButton');
let {
isQuiet,
isDisabled,
diff --git a/packages/@react-spectrum/buttongroup/package.json b/packages/@react-spectrum/buttongroup/package.json
index b2eef76f61f..8166b66d1d6 100644
--- a/packages/@react-spectrum/buttongroup/package.json
+++ b/packages/@react-spectrum/buttongroup/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-spectrum/buttongroup",
- "version": "3.2.1",
+ "version": "3.2.2",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -32,12 +32,12 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/utils": "^3.8.0",
- "@react-spectrum/utils": "^3.5.2",
- "@react-spectrum/button": "^3.4.1",
- "@react-spectrum/text": "^3.1.2",
- "@react-types/buttongroup": "^3.1.1",
- "@react-types/shared": "^3.6.0"
+ "@react-aria/utils": "^3.8.2",
+ "@react-spectrum/utils": "^3.6.2",
+ "@react-spectrum/button": "^3.5.1",
+ "@react-spectrum/text": "^3.1.3",
+ "@react-types/buttongroup": "^3.1.2",
+ "@react-types/shared": "^3.8.0"
},
"devDependencies": {
"@adobe/spectrum-css-temp": "3.0.0-alpha.1",
@@ -45,7 +45,7 @@
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1",
- "@react-spectrum/provider": "^3.0.0-rc.1"
+ "@react-spectrum/provider": "^3.0.0"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-spectrum/buttongroup/src/ButtonGroup.tsx b/packages/@react-spectrum/buttongroup/src/ButtonGroup.tsx
index f4aee52091c..aad9e7e7165 100644
--- a/packages/@react-spectrum/buttongroup/src/ButtonGroup.tsx
+++ b/packages/@react-spectrum/buttongroup/src/ButtonGroup.tsx
@@ -21,10 +21,10 @@ import {
} from '@react-spectrum/utils';
import {DOMRef} from '@react-types/shared';
import {filterDOMProps} from '@react-aria/utils';
+import {Provider, useProvider, useProviderProps} from '@react-spectrum/provider';
import React, {useCallback, useEffect, useRef} from 'react';
import {SpectrumButtonGroupProps} from '@react-types/buttongroup';
import styles from '@adobe/spectrum-css-temp/components/buttongroup/vars.css';
-import {useProvider, useProviderProps} from '@react-spectrum/provider';
function ButtonGroup(props: SpectrumButtonGroupProps, ref: DOMRef) {
let {scale} = useProvider();
@@ -102,11 +102,12 @@ function ButtonGroup(props: SpectrumButtonGroupProps, ref: DOMRef
- {children}
+
+ {children}
+
);
diff --git a/packages/@react-spectrum/calendar/package.json b/packages/@react-spectrum/calendar/package.json
index 4e899f20461..f381dde3773 100644
--- a/packages/@react-spectrum/calendar/package.json
+++ b/packages/@react-spectrum/calendar/package.json
@@ -33,10 +33,12 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
+ "@internationalized/date": "3.0.0-alpha.1",
"@react-aria/calendar": "3.0.0-alpha.1",
"@react-aria/focus": "^3.1.0",
"@react-aria/i18n": "^3.1.0",
"@react-aria/interactions": "^3.1.0",
+ "@react-aria/utils": "^3.8.2",
"@react-aria/visually-hidden": "^3.1.0",
"@react-spectrum/button": "^3.1.0",
"@react-spectrum/utils": "^3.1.0",
@@ -52,7 +54,8 @@
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1",
- "@react-spectrum/provider": "^3.0.0-rc.1"
+ "@react-spectrum/provider": "^3.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-spectrum/calendar/src/Calendar.tsx b/packages/@react-spectrum/calendar/src/Calendar.tsx
index d448b6cabce..6afa9bea2f8 100644
--- a/packages/@react-spectrum/calendar/src/Calendar.tsx
+++ b/packages/@react-spectrum/calendar/src/Calendar.tsx
@@ -11,13 +11,17 @@
*/
import {CalendarBase} from './CalendarBase';
+import {createCalendar} from '@internationalized/date';
+import {DateValue, SpectrumCalendarProps} from '@react-types/calendar';
import React from 'react';
-import {SpectrumCalendarProps} from '@react-types/calendar';
import {useCalendar} from '@react-aria/calendar';
import {useCalendarState} from '@react-stately/calendar';
-export function Calendar(props: SpectrumCalendarProps) {
- let state = useCalendarState(props);
+export function Calendar(props: SpectrumCalendarProps) {
+ let state = useCalendarState({
+ ...props,
+ createCalendar
+ });
let aria = useCalendar(props, state);
return (
- {monthDateFormatter.format(state.currentMonth)}
+ {monthDateFormatter.format(toDate(state.currentMonth, state.timeZone))}
();
let {cellProps, buttonProps} = useCalendarCell(props, state, ref);
let {hoverProps, isHovered} = useHover({});
- let dateFormatter = useDateFormatter({day: 'numeric'});
+ let dateFormatter = useDateFormatter({
+ day: 'numeric',
+ timeZone: state.timeZone,
+ calendar: state.currentMonth.calendar.identifier
+ });
let isSelected = state.isSelected(props.date);
let highlightedRange = 'highlightedRange' in state && state.highlightedRange;
let isSelectionStart = highlightedRange && isSameDay(props.date, highlightedRange.start);
let isSelectionEnd = highlightedRange && isSameDay(props.date, highlightedRange.end);
- let isRangeStart = isSelected && (props.date.getDay() === 0 || props.date.getDate() === 1);
- let isRangeEnd = isSelected && (props.date.getDay() === 6 || props.date.getDate() === state.daysInMonth);
+ let dayOfWeek = getDayOfWeek(props.date);
+ let isRangeStart = isSelected && (dayOfWeek === 0 || props.date.day === 1);
+ let isRangeEnd = isSelected && (dayOfWeek === 6 || props.date.day === state.daysInMonth);
+ let {focusProps, isFocusVisible} = useFocusRing();
return (
- {dateFormatter.format(props.date)}
+ {dateFormatter.format(toDate(props.date, state.timeZone))}
|
);
diff --git a/packages/@react-spectrum/calendar/src/CalendarTableBody.tsx b/packages/@react-spectrum/calendar/src/CalendarTableBody.tsx
index 089cd022698..2e25655f2c5 100644
--- a/packages/@react-spectrum/calendar/src/CalendarTableBody.tsx
+++ b/packages/@react-spectrum/calendar/src/CalendarTableBody.tsx
@@ -23,11 +23,12 @@ export function CalendarTableBody({state}: CalendarTableBodyProps) {
{
[...new Array(state.weeksInMonth).keys()].map(weekIndex => (
-
+
{
[...new Array(7).keys()].map(dayIndex => (
)
diff --git a/packages/@react-spectrum/calendar/src/CalendarTableHeader.tsx b/packages/@react-spectrum/calendar/src/CalendarTableHeader.tsx
index b26563a8ffb..118eadb0862 100644
--- a/packages/@react-spectrum/calendar/src/CalendarTableHeader.tsx
+++ b/packages/@react-spectrum/calendar/src/CalendarTableHeader.tsx
@@ -10,29 +10,38 @@
* governing permissions and limitations under the License.
*/
+import {CalendarDate, toDate} from '@internationalized/date';
import {classNames} from '@react-spectrum/utils';
import React from 'react';
import styles from '@adobe/spectrum-css-temp/components/calendar/vars.css';
+import {useCalendarTableHeader} from '@react-aria/calendar';
import {useDateFormatter} from '@react-aria/i18n';
import {VisuallyHidden} from '@react-aria/visually-hidden';
interface CalendarTableHeaderProps {
- weekDays: Array
+ weekDays: Array
}
export function CalendarTableHeader({weekDays}: CalendarTableHeaderProps) {
+ const {
+ columnHeaderProps
+ } = useCalendarTableHeader();
let dayFormatter = useDateFormatter({weekday: 'narrow'});
let dayFormatterLong = useDateFormatter({weekday: 'long'});
return (
-
+
{
- weekDays.map((dateDay, index) => {
+ weekDays.map((date, index) => {
+ // Timezone doesn't matter here, assuming all days are formatted in the same zone.
+ let dateDay = toDate(date, 'America/Los_Angeles');
let day = dayFormatter.format(dateDay);
let dayLong = dayFormatterLong.format(dateDay);
return (
{/* Make sure screen readers read the full day name, but we show an abbreviation visually. */}
{dayLong}
diff --git a/packages/@react-spectrum/calendar/src/RangeCalendar.tsx b/packages/@react-spectrum/calendar/src/RangeCalendar.tsx
index 26275bc95b7..ccf0ab325e6 100644
--- a/packages/@react-spectrum/calendar/src/RangeCalendar.tsx
+++ b/packages/@react-spectrum/calendar/src/RangeCalendar.tsx
@@ -11,13 +11,17 @@
*/
import {CalendarBase} from './CalendarBase';
+import {createCalendar} from '@internationalized/date';
+import {DateValue, SpectrumRangeCalendarProps} from '@react-types/calendar';
import React from 'react';
-import {SpectrumRangeCalendarProps} from '@react-types/calendar';
import {useRangeCalendar} from '@react-aria/calendar';
import {useRangeCalendarState} from '@react-stately/calendar';
-export function RangeCalendar(props: SpectrumRangeCalendarProps) {
- let state = useRangeCalendarState(props);
+export function RangeCalendar(props: SpectrumRangeCalendarProps) {
+ let state = useRangeCalendarState({
+ ...props,
+ createCalendar
+ });
let aria = useRangeCalendar(props, state);
return (
render()
)
.add(
'defaultValue',
- () => render({defaultValue: new Date(2019, 5, 5)})
+ () => render({defaultValue: new CalendarDate(2019, 6, 5)})
)
.add(
'controlled value',
- () => render({value: new Date(2019, 5, 5)})
+ () => render({value: new CalendarDate(2019, 5, 5)})
+ )
+ .add(
+ 'with time',
+ () =>
+ )
+ .add(
+ 'with zoned time',
+ () =>
)
.add(
'minValue: today, maxValue: 1 week from now',
- () => render({minValue: new Date(), maxValue: addWeeks(new Date(), 1)})
+ () => render({minValue: today(getLocalTimeZone()), maxValue: today(getLocalTimeZone()).add({weeks: 1})})
)
.add(
'defaultValue + minValue + maxValue',
- () => render({defaultValue: new Date(2019, 5, 10), minValue: new Date(2019, 5, 5), maxValue: new Date(2019, 5, 20)})
+ () => render({defaultValue: new CalendarDate(2019, 6, 10), minValue: new CalendarDate(2019, 6, 5), maxValue: new CalendarDate(2019, 6, 20)})
)
.add(
'isDisabled',
- () => render({defaultValue: new Date(2019, 5, 5), isDisabled: true})
+ () => render({defaultValue: new CalendarDate(2019, 6, 5), isDisabled: true})
)
.add(
'isReadOnly',
- () => render({defaultValue: new Date(2019, 5, 5), isReadOnly: true})
+ () => render({defaultValue: new CalendarDate(2019, 6, 5), isReadOnly: true})
)
.add(
'autoFocus',
- () => render({defaultValue: new Date(2019, 5, 5), autoFocus: true})
+ () => render({defaultValue: new CalendarDate(2019, 6, 5), autoFocus: true})
);
function render(props = {}) {
- return ;
+ return ;
+}
+
+// https://github.com/unicode-org/cldr/blob/22af90ae3bb04263f651323ce3d9a71747a75ffb/common/supplemental/supplementalData.xml#L4649-L4664
+const preferences = [
+ {locale: '', label: 'Default', ordering: 'gregory'},
+ {label: 'Arabic (Algeria)', locale: 'ar-DZ', territories: 'DJ DZ EH ER IQ JO KM LB LY MA MR OM PS SD SY TD TN YE', ordering: 'gregory islamic islamic-civil islamic-tbla'},
+ {label: 'Arabic (United Arab Emirates)', locale: 'ar-AE', territories: 'AE BH KW QA', ordering: 'gregory islamic-umalqura islamic islamic-civil islamic-tbla'},
+ {label: 'Arabic (Egypt)', locale: 'AR-EG', territories: 'EG', ordering: 'gregory coptic islamic islamic-civil islamic-tbla'},
+ {label: 'Arabic (Saudi Arabia)', locale: 'ar-SA', territories: 'SA', ordering: 'islamic-umalqura gregory islamic islamic-rgsa'},
+ {label: 'Farsi (Afghanistan)', locale: 'fa-AF', territories: 'AF IR', ordering: 'persian gregory islamic islamic-civil islamic-tbla'},
+ // {territories: 'CN CX HK MO SG', ordering: 'gregory chinese'},
+ {label: 'Amharic (Ethiopia)', locale: 'am-ET', territories: 'ET', ordering: 'gregory ethiopic ethioaa'},
+ {label: 'Hebrew (Israel)', locale: 'he-IL', territories: 'IL', ordering: 'gregory hebrew islamic islamic-civil islamic-tbla'},
+ {label: 'Hindi (India)', locale: 'hi-IN', territories: 'IN', ordering: 'gregory indian'},
+ {label: 'Japanese (Japan)', locale: 'ja-JP', territories: 'JP', ordering: 'gregory japanese'},
+ // {territories: 'KR', ordering: 'gregory dangi'},
+ {label: 'Thai (Thailand)', locale: 'th-TH', territories: 'TH', ordering: 'buddhist gregory'},
+ {label: 'Chinese (Taiwan)', locale: 'zh-TW', territories: 'TW', ordering: 'gregory roc chinese'}
+];
+
+const calendars = [
+ {key: 'gregory', name: 'Gregorian'},
+ {key: 'japanese', name: 'Japanese'},
+ {key: 'buddhist', name: 'Buddhist'},
+ {key: 'roc', name: 'Taiwan'},
+ {key: 'persian', name: 'Persian'},
+ {key: 'indian', name: 'Indian'},
+ {key: 'islamic-umalqura', name: 'Islamic (Umm al-Qura)'},
+ {key: 'islamic-civil', name: 'Islamic Civil'},
+ {key: 'islamic-tbla', name: 'Islamic Tabular'},
+ {key: 'hebrew', name: 'Hebrew'},
+ {key: 'coptic', name: 'Coptic'},
+ {key: 'ethiopic', name: 'Ethiopic'},
+ {key: 'ethioaa', name: 'Ethiopic (Amete Alem)'}
+];
+
+function Example(props) {
+ let [locale, setLocale] = React.useState('');
+ let [calendar, setCalendar] = React.useState(calendars[0].key);
+ let {locale: defaultLocale} = useLocale();
+
+ let pref = preferences.find(p => p.locale === locale);
+ let preferredCalendars = React.useMemo(() => pref ? pref.ordering.split(' ').map(p => calendars.find(c => c.key === p)).filter(Boolean) : [calendars[0]], [pref]);
+ let otherCalendars = React.useMemo(() => calendars.filter(c => !preferredCalendars.some(p => p.key === c.key)), [preferredCalendars]);
+
+ let updateLocale = locale => {
+ setLocale(locale);
+ let pref = preferences.find(p => p.locale === locale);
+ setCalendar(pref.ordering.split(' ')[0]);
+ };
+
+ return (
+
+
+
+ {item => - {item.label}
}
+
+
+
+ {item => - {item.name}
}
+
+
+ {item => - {item.name}
}
+
+
+
+
+
+
+
+ );
+}
+
+function CalendarWithTime() {
+ let [value, setValue] = useState(new CalendarDateTime(2019, 6, 5, 8));
+ let onChange = (v: CalendarDateTime) => {
+ setValue(v);
+ action('onChange')(v);
+ };
+
+ return (
+
+
+
+
+ );
+}
+
+function CalendarWithZonedTime() {
+ let [value, setValue] = useState(parseZonedDateTime('2021-03-14T00:45-08:00[America/Los_Angeles]'));
+ let onChange = (v: ZonedDateTime) => {
+ setValue(v);
+ action('onChange')(v);
+ };
+
+ return (
+
+
+
+
+ );
}
diff --git a/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx b/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx
index e7c1f13ca0b..1bcfea12901 100644
--- a/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx
+++ b/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx
@@ -11,45 +11,83 @@
*/
import {action} from '@storybook/addon-actions';
-import {addWeeks} from 'date-fns';
+import {CalendarDate, CalendarDateTime, getLocalTimeZone, parseZonedDateTime, today} from '@internationalized/date';
+import {Flex} from '@react-spectrum/layout';
import {RangeCalendar} from '../';
-import React from 'react';
+import React, {useState} from 'react';
import {storiesOf} from '@storybook/react';
+import {TimeField} from '@react-spectrum/datepicker';
-storiesOf('RangeCalendar', module)
+storiesOf('Date and Time/RangeCalendar', module)
.add(
'Default',
() => render()
)
.add(
'defaultValue',
- () => render({defaultValue: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 10)}})
+ () => render({defaultValue: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 10)}})
)
.add(
'controlled value',
- () => render({value: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 10)}})
+ () => render({value: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 10)}})
+ )
+ .add(
+ 'with time',
+ () =>
+ )
+ .add(
+ 'with zoned time',
+ () =>
)
.add(
'minValue: today, maxValue: 1 week from now',
- () => render({minValue: new Date(), maxValue: addWeeks(new Date(), 1)})
+ () => render({minValue: today(getLocalTimeZone()), maxValue: today(getLocalTimeZone()).add({weeks: 1})})
)
.add(
'defaultValue + minValue + maxValue',
- () => render({defaultValue: {start: new Date(2019, 5, 10), end: new Date(2019, 5, 12)}, minValue: new Date(2019, 5, 5), maxValue: new Date(2019, 5, 20)})
+ () => render({defaultValue: {start: new CalendarDate(2019, 6, 10), end: new CalendarDate(2019, 6, 12)}, minValue: new CalendarDate(2019, 6, 5), maxValue: new CalendarDate(2019, 6, 20)})
)
.add(
'isDisabled',
- () => render({defaultValue: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 10)}, isDisabled: true})
+ () => render({defaultValue: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 10)}, isDisabled: true})
)
.add(
'isReadOnly',
- () => render({defaultValue: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 10)}, isReadOnly: true})
+ () => render({defaultValue: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 10)}, isReadOnly: true})
)
.add(
'autoFocus',
- () => render({defaultValue: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 10)}, autoFocus: true})
+ () => render({defaultValue: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 10)}, autoFocus: true})
);
function render(props = {}) {
return ;
}
+
+function RangeCalendarWithTime() {
+ let [value, setValue] = useState({start: new CalendarDateTime(2019, 6, 5, 8), end: new CalendarDateTime(2019, 6, 10, 12)});
+
+ return (
+
+
+
+ setValue({...value, start: v})} />
+ setValue({...value, end: v})} />
+
+
+ );
+}
+
+function RangeCalendarWithZonedTime() {
+ let [value, setValue] = useState({start: parseZonedDateTime('2021-03-10T00:45-05:00[America/New_York]'), end: parseZonedDateTime('2021-03-26T18:05-07:00[America/Los_Angeles]')});
+
+ return (
+
+
+
+ setValue({...value, start: v})} />
+ setValue({...value, end: v})} />
+
+
+ );
+}
diff --git a/packages/@react-spectrum/calendar/test/Calendar.test.js b/packages/@react-spectrum/calendar/test/Calendar.test.js
index 665d52a472f..ee113fb5d26 100644
--- a/packages/@react-spectrum/calendar/test/Calendar.test.js
+++ b/packages/@react-spectrum/calendar/test/Calendar.test.js
@@ -13,10 +13,10 @@
jest.mock('@react-aria/live-announcer');
import {announce} from '@react-aria/live-announcer';
import {Calendar} from '../';
+import {CalendarDate} from '@internationalized/date';
import {fireEvent, render} from '@testing-library/react';
import React from 'react';
import {triggerPress} from '@react-spectrum/test-utils';
-import V2Calendar from '@react/react-spectrum/Calendar';
let keyCodes = {'Enter': 13, ' ': 32, 'PageUp': 33, 'PageDown': 34, 'End': 35, 'Home': 36, 'ArrowLeft': 37, 'ArrowUp': 38, 'ArrowRight': 39, 'ArrowDown': 40};
@@ -33,10 +33,8 @@ describe('Calendar', () => {
it.each`
Name | Calendar
${'v3'} | ${Calendar}
- ${'v2'} | ${V2Calendar}
`('$Name should render a calendar with a defaultValue', ({Calendar}) => {
- let isV2 = Calendar === V2Calendar;
- let {getByLabelText, getByRole, getAllByRole} = render();
+ let {getByLabelText, getByRole, getAllByRole} = render();
let heading = getByRole('heading');
expect(heading).toHaveTextContent('June 2019');
@@ -45,18 +43,16 @@ describe('Calendar', () => {
expect(gridCells.length).toBe(30);
let selectedDate = getByLabelText('Selected', {exact: false});
- expect(isV2 ? selectedDate : selectedDate.parentElement).toHaveAttribute('role', 'gridcell');
- expect(isV2 ? selectedDate : selectedDate.parentElement).toHaveAttribute('aria-selected', 'true');
+ expect(selectedDate.parentElement).toHaveAttribute('role', 'gridcell');
+ expect(selectedDate.parentElement).toHaveAttribute('aria-selected', 'true');
expect(selectedDate).toHaveAttribute('aria-label', 'Wednesday, June 5, 2019 selected');
});
it.each`
Name | Calendar
${'v3'} | ${Calendar}
- ${'v2'} | ${V2Calendar}
`('$Name should render a calendar with a value', ({Calendar}) => {
- let isV2 = Calendar === V2Calendar;
- let {getByLabelText, getByRole, getAllByRole} = render();
+ let {getByLabelText, getByRole, getAllByRole} = render();
let heading = getByRole('heading');
expect(heading).toHaveTextContent('June 2019');
@@ -65,32 +61,24 @@ describe('Calendar', () => {
expect(gridCells.length).toBe(30);
let selectedDate = getByLabelText('Selected', {exact: false});
- expect(isV2 ? selectedDate : selectedDate.parentElement).toHaveAttribute('role', 'gridcell');
- expect(isV2 ? selectedDate : selectedDate.parentElement).toHaveAttribute('aria-selected', 'true');
+ expect(selectedDate.parentElement).toHaveAttribute('role', 'gridcell');
+ expect(selectedDate.parentElement).toHaveAttribute('aria-selected', 'true');
expect(selectedDate).toHaveAttribute('aria-label', 'Wednesday, June 5, 2019 selected');
});
it.each`
Name | Calendar
${'v3'} | ${Calendar}
- ${'v2'} | ${V2Calendar}
`('$Name should focus the selected date if autoFocus is set', ({Calendar}) => {
- let {getByRole, getByLabelText} = render();
+ let {getByRole, getByLabelText} = render();
let cell = getByLabelText('selected', {exact: false});
let grid = getByRole('grid');
- if (Calendar === V2Calendar) {
- expect(cell).toHaveAttribute('role', 'gridcell');
- expect(cell).toHaveAttribute('aria-selected', 'true');
- expect(grid).toHaveFocus();
- expect(grid).toHaveAttribute('aria-activedescendant', cell.id);
- } else {
- expect(cell.parentElement).toHaveAttribute('role', 'gridcell');
- expect(cell.parentElement).toHaveAttribute('aria-selected', 'true');
- expect(cell).toHaveFocus();
- expect(grid).not.toHaveAttribute('aria-activedescendant');
- }
+ expect(cell.parentElement).toHaveAttribute('role', 'gridcell');
+ expect(cell.parentElement).toHaveAttribute('aria-selected', 'true');
+ expect(cell).toHaveFocus();
+ expect(grid).not.toHaveAttribute('aria-activedescendant');
});
});
@@ -98,12 +86,11 @@ describe('Calendar', () => {
it.each`
Name | Calendar
${'v3'} | ${Calendar}
- ${'v2'} | ${V2Calendar}
`('$Name selects a date on keyDown Enter/Space (uncontrolled)', ({Calendar}) => {
let onChange = jest.fn();
let {getByLabelText, getByRole} = render(
);
@@ -118,25 +105,24 @@ describe('Calendar', () => {
selectedDate = getByLabelText('selected', {exact: false});
expect(selectedDate.textContent).toBe('4');
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange.mock.calls[0][0].valueOf()).toBe(new Date(2019, 5, 4).valueOf()); // v2 returns a moment object
+ expect(onChange.mock.calls[0][0]).toEqual(new CalendarDate(2019, 6, 4));
fireEvent.keyDown(grid, {key: 'ArrowLeft', keyCode: keyCodes.ArrowLeft});
fireEvent.keyDown(grid, {key: ' ', keyCode: keyCodes[' ']});
selectedDate = getByLabelText('selected', {exact: false});
expect(selectedDate.textContent).toBe('3');
expect(onChange).toHaveBeenCalledTimes(2);
- expect(onChange.mock.calls[1][0].valueOf()).toBe(new Date(2019, 5, 3).valueOf()); // v2 returns a moment object
+ expect(onChange.mock.calls[1][0]).toEqual(new CalendarDate(2019, 6, 3));
});
it.each`
Name | Calendar
${'v3'} | ${Calendar}
- ${'v2'} | ${V2Calendar}
`('$Name selects a date on keyDown Enter/Space (controlled)', ({Calendar}) => {
let onChange = jest.fn();
let {getByLabelText, getByRole} = render(
);
@@ -151,21 +137,16 @@ describe('Calendar', () => {
selectedDate = getByLabelText('selected', {exact: false});
expect(selectedDate.textContent).toBe('5'); // controlled
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange.mock.calls[0][0].valueOf()).toBe(new Date(2019, 5, 4).valueOf()); // v2 returns a moment object
+ expect(onChange.mock.calls[0][0]).toEqual(new CalendarDate(2019, 6, 4));
fireEvent.keyDown(grid, {key: 'ArrowLeft', keyCode: keyCodes.ArrowLeft});
fireEvent.keyDown(grid, {key: ' ', keyCode: keyCodes[' ']});
selectedDate = getByLabelText('selected', {exact: false});
expect(selectedDate.textContent).toBe('5'); // controlled
expect(onChange).toHaveBeenCalledTimes(2);
- expect(onChange.mock.calls[1][0].valueOf()).toBe(new Date(2019, 5, 3).valueOf()); // v2 returns a moment object
+ expect(onChange.mock.calls[1][0]).toEqual(new CalendarDate(2019, 6, 3));
});
- // v2 tests disabled until next release
- // it.each`
- // Name | Calendar | props
- // ${'v3'} | ${Calendar} | ${{isReadOnly: true}}
- // ${'v2'} | ${V2Calendar} | ${{readOnly: true}}
it.each`
Name | Calendar | props
${'v3'} | ${Calendar} | ${{isReadOnly: true}}
@@ -173,7 +154,7 @@ describe('Calendar', () => {
let onChange = jest.fn();
let {getByLabelText} = render(
@@ -195,11 +176,6 @@ describe('Calendar', () => {
expect(onChange).not.toHaveBeenCalled();
});
- // v2 tests disabled until next release
- // it.each`
- // Name | Calendar
- // ${'v3'} | ${Calendar}
- // ${'v2'} | ${V2Calendar}
it.each`
Name | Calendar
${'v3'} | ${Calendar}
@@ -207,7 +183,7 @@ describe('Calendar', () => {
let onChange = jest.fn();
let {getByLabelText, getByText} = render(
);
@@ -217,18 +193,17 @@ describe('Calendar', () => {
let selectedDate = getByLabelText('selected', {exact: false});
expect(selectedDate.textContent).toBe('17');
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange.mock.calls[0][0].valueOf()).toBe(new Date(2019, 5, 17).valueOf()); // v2 returns a moment object
+ expect(onChange.mock.calls[0][0]).toEqual(new CalendarDate(2019, 6, 17));
});
it.each`
Name | Calendar
${'v3'} | ${Calendar}
- ${'v2'} | ${V2Calendar}
`('$Name selects a date on click (controlled)', ({Calendar}) => {
let onChange = jest.fn();
let {getByLabelText, getByText} = render(
);
@@ -238,18 +213,17 @@ describe('Calendar', () => {
let selectedDate = getByLabelText('selected', {exact: false});
expect(selectedDate.textContent).toBe('5');
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange.mock.calls[0][0].valueOf()).toBe(new Date(2019, 5, 17).valueOf()); // v2 returns a moment object
+ expect(onChange.mock.calls[0][0]).toEqual(new CalendarDate(2019, 6, 17));
});
it.each`
Name | Calendar | props
${'v3'} | ${Calendar} | ${{isDisabled: true}}
- ${'v2'} | ${V2Calendar} | ${{disabled: true}}
`('$Name does not select a date on click if isDisabled', ({Calendar, props}) => {
let onChange = jest.fn();
let {getByLabelText, getByText} = render(
);
@@ -262,11 +236,6 @@ describe('Calendar', () => {
expect(onChange).not.toHaveBeenCalled();
});
- // v2 tests disabled until next release
- // it.each`
- // Name | Calendar | props
- // ${'v3'} | ${Calendar} | ${{isReadOnly: true}}
- // ${'v2'} | ${V2Calendar} | ${{readOnly: true}}
it.each`
Name | Calendar | props
${'v3'} | ${Calendar} | ${{isReadOnly: true}}
@@ -274,7 +243,7 @@ describe('Calendar', () => {
let onChange = jest.fn();
let {getByLabelText, getByText} = render(
);
@@ -289,8 +258,7 @@ describe('Calendar', () => {
it.each`
Name | Calendar | props
- ${'v3'} | ${Calendar} | ${{defaultValue: new Date(2019, 1, 8), minValue: new Date(2019, 1, 5), maxValue: new Date(2019, 1, 15)}}
- ${'v2'} | ${V2Calendar} | ${{defaultValue: new Date(2019, 1, 8), min: new Date(2019, 1, 5), max: new Date(2019, 1, 15)}}
+ ${'v3'} | ${Calendar} | ${{defaultValue: new CalendarDate(2019, 2, 8), minValue: new CalendarDate(2019, 2, 5), maxValue: new CalendarDate(2019, 2, 15)}}
`('$Name does not select a date on click if outside the valid date range', ({Calendar, props}) => {
let onChange = jest.fn();
let {getByLabelText} = render(
@@ -328,7 +296,7 @@ describe('Calendar', () => {
// These tests only work against v3
describe('announcing', () => {
it('announces when the current month changes', () => {
- let {getByLabelText} = render();
+ let {getByLabelText} = render();
let nextButton = getByLabelText('Next');
triggerPress(nextButton);
@@ -338,17 +306,17 @@ describe('Calendar', () => {
});
it('announces when the selected date changes', () => {
- let {getByText} = render();
+ let {getByText} = render();
let newDate = getByText('17');
triggerPress(newDate);
expect(announce).toHaveBeenCalledTimes(1);
- expect(announce).toHaveBeenCalledWith('Selected Date: Monday, June 17, 2019');
+ expect(announce).toHaveBeenCalledWith('Selected Date: Monday, June 17, 2019', 'polite', 4000);
});
it('ensures that the active descendant is announced when the focused date changes', () => {
- let {getByRole, getByLabelText} = render();
+ let {getByRole, getByLabelText} = render();
let grid = getByRole('grid');
let selectedDate = getByLabelText('selected', {exact: false});
@@ -359,7 +327,7 @@ describe('Calendar', () => {
});
it('renders a caption with the selected date', () => {
- let {getByText, getByRole} = render();
+ let {getByText, getByRole} = render();
let grid = getByRole('grid');
let caption = document.getElementById(grid.getAttribute('aria-describedby'));
diff --git a/packages/@react-spectrum/calendar/test/CalendarBase.test.js b/packages/@react-spectrum/calendar/test/CalendarBase.test.js
index 33026c12f7e..b7f0ba84760 100644
--- a/packages/@react-spectrum/calendar/test/CalendarBase.test.js
+++ b/packages/@react-spectrum/calendar/test/CalendarBase.test.js
@@ -12,12 +12,12 @@
import {act, fireEvent, render} from '@testing-library/react';
import {Calendar, RangeCalendar} from '../';
+import {CalendarDate} from '@internationalized/date';
import {getDaysInMonth} from 'date-fns';
import {Provider} from '@react-spectrum/provider';
import React from 'react';
import {theme} from '@react-spectrum/theme-default';
import {triggerPress} from '@react-spectrum/test-utils';
-import V2Calendar from '@react/react-spectrum/Calendar';
let cellFormatter = new Intl.DateTimeFormat('en-US', {weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'});
let headingFormatter = new Intl.DateTimeFormat('en-US', {month: 'long', year: 'numeric'});
@@ -46,10 +46,7 @@ describe('CalendarBase', () => {
Name | Calendar | props
${'v3 Calendar'} | ${Calendar} | ${{}}
${'v3 RangeCalendar'} | ${RangeCalendar} | ${{}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range'}}
`('$Name shows the current month by default', ({Calendar, props}) => {
- const isV2 = Calendar === V2Calendar;
let {getByLabelText, getByRole, getAllByRole} = render();
let calendar = getByRole('group');
@@ -59,17 +56,12 @@ describe('CalendarBase', () => {
expect(heading).toHaveTextContent(headingFormatter.format(new Date()));
let grid = getByRole('grid');
-
- if (isV2) {
- expect(grid).toHaveAttribute('tabIndex', '0');
- } else {
- expect(grid).not.toHaveAttribute('tabIndex');
- }
+ expect(grid).not.toHaveAttribute('tabIndex');
let today = getByLabelText('today', {exact: false});
- expect(isV2 ? today : today.parentElement).toHaveAttribute('role', 'gridcell');
+ expect(today.parentElement).toHaveAttribute('role', 'gridcell');
expect(today).toHaveAttribute('aria-label', `Today, ${cellFormatter.format(new Date())}`);
- expect(today).toHaveAttribute('tabIndex', !isV2 ? '0' : '-1');
+ expect(today).toHaveAttribute('tabIndex', '0');
expect(getByLabelText('Previous')).toBeVisible();
expect(getByLabelText('Next')).toBeVisible();
@@ -77,7 +69,7 @@ describe('CalendarBase', () => {
let gridCells = getAllByRole('gridcell').filter(cell => cell.getAttribute('aria-disabled') !== 'true');
expect(gridCells.length).toBe(getDaysInMonth(new Date()));
for (let cell of gridCells) {
- expect(isV2 ? cell : cell.children[0]).toHaveAttribute('aria-label');
+ expect(cell.children[0]).toHaveAttribute('aria-label');
}
});
@@ -85,8 +77,6 @@ describe('CalendarBase', () => {
Name | Calendar | props
${'v3 Calendar'} | ${Calendar} | ${{isDisabled: true}}
${'v3 RangeCalendar'} | ${RangeCalendar} | ${{isDisabled: true}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{disabled: true}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range', disabled: true}}
`('$Name should set aria-disabled when isDisabled', ({Calendar, props}) => {
let {getByRole, getAllByRole, getByLabelText} = render();
@@ -107,46 +97,29 @@ describe('CalendarBase', () => {
Name | Calendar | props
${'v3 Calendar'} | ${Calendar} | ${{isReadOnly: true}}
${'v3 RangeCalendar'} | ${RangeCalendar} | ${{isReadOnly: true}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{readOnly: true}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range', readOnly: true}}
`('$Name should set aria-readonly when isReadOnly', ({Calendar, props}) => {
- const isV2 = Calendar === V2Calendar;
let {getByRole} = render();
let grid = getByRole('grid');
expect(grid).toHaveAttribute('aria-readonly', 'true');
- if (isV2) {
- expect(grid).toHaveAttribute('tabIndex', '0');
- } else {
- expect(grid).not.toHaveAttribute('tabIndex');
- }
+ expect(grid).not.toHaveAttribute('tabIndex');
});
it.each`
Name | Calendar | props
${'v3 Calendar'} | ${Calendar} | ${{}}
${'v3 RangeCalendar'} | ${RangeCalendar} | ${{}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range'}}
`('$Name should focus today if autoFocus is set and there is no selected value', ({Name, Calendar}) => {
- const isV2 = Calendar === V2Calendar;
- let {getByRole, getByLabelText} = render();
+ let {getByLabelText} = render();
let cell = getByLabelText('today', {exact: false});
- expect(isV2 ? cell : cell.parentElement).toHaveAttribute('role', 'gridcell');
-
- let grid = getByRole('grid');
- expect(isV2 ? grid : cell).toHaveFocus();
- if (isV2) {
- expect(grid).toHaveAttribute('aria-activedescendant', cell.id);
- }
+ expect(cell.parentElement).toHaveAttribute('role', 'gridcell');
+ expect(cell).toHaveFocus();
});
it.each`
Name | Calendar | props
- ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new Date(2019, 1, 10), minValue: new Date(2019, 1, 3), maxValue: new Date(2019, 1, 20)}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{defaultValue: new Date(2019, 1, 10), min: new Date(2019, 1, 3), max: new Date(2019, 1, 20)}}
- ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new Date(2019, 1, 10), end: new Date(2019, 1, 15)}, minValue: new Date(2019, 1, 3), maxValue: new Date(2019, 1, 20)}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range', defaultValue: [new Date(2019, 1, 10), new Date(2019, 1, 15)], min: new Date(2019, 1, 3), max: new Date(2019, 1, 20)}}
+ ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2019, 2, 10), minValue: new CalendarDate(2019, 2, 3), maxValue: new CalendarDate(2019, 2, 20)}}
+ ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2019, 2, 10), end: new CalendarDate(2019, 2, 15)}, minValue: new CalendarDate(2019, 2, 3), maxValue: new CalendarDate(2019, 2, 20)}}
`('$Name should set aria-disabled on cells outside the valid date range', ({Calendar, props}) => {
let {getAllByRole} = render();
@@ -156,8 +129,8 @@ describe('CalendarBase', () => {
it.each`
Name | Calendar | props
- ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new Date(2019, 1, 10), minValue: new Date(2019, 1, 3), maxValue: new Date(2019, 2, 20)}}
- ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new Date(2019, 1, 10), end: new Date(2019, 1, 15)}, minValue: new Date(2019, 1, 3), maxValue: new Date(2019, 2, 20)}}
+ ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2019, 2, 10), minValue: new CalendarDate(2019, 2, 3), maxValue: new CalendarDate(2019, 3, 20)}}
+ ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2019, 2, 10), end: new CalendarDate(2019, 2, 15)}, minValue: new CalendarDate(2019, 2, 3), maxValue: new CalendarDate(2019, 3, 20)}}
`('$Name should disable the previous button if outside valid date range', ({Calendar, props}) => {
let {getByLabelText} = render();
@@ -167,8 +140,8 @@ describe('CalendarBase', () => {
it.each`
Name | Calendar | props
- ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new Date(2019, 2, 10), minValue: new Date(2019, 1, 3), maxValue: new Date(2019, 2, 20)}}
- ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new Date(2019, 2, 10), end: new Date(2019, 2, 15)}, minValue: new Date(2019, 1, 3), maxValue: new Date(2019, 2, 20)}}
+ ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2019, 3, 10), minValue: new CalendarDate(2019, 2, 3), maxValue: new CalendarDate(2019, 3, 20)}}
+ ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2019, 3, 10), end: new CalendarDate(2019, 3, 15)}, minValue: new CalendarDate(2019, 2, 3), maxValue: new CalendarDate(2019, 3, 20)}}
`('$Name should disable the next button if outside valid date range', ({Calendar, props}) => {
let {getByLabelText} = render();
@@ -178,8 +151,8 @@ describe('CalendarBase', () => {
it.each`
Name | Calendar | props
- ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new Date(2019, 2, 10), minValue: new Date(2019, 1, 3), maxValue: new Date(2019, 1, 20)}}
- ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new Date(2019, 2, 10), end: new Date(2019, 2, 15)}, minValue: new Date(2019, 1, 3), maxValue: new Date(2019, 1, 20)}}
+ ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2019, 3, 10), minValue: new CalendarDate(2019, 2, 3), maxValue: new CalendarDate(2019, 2, 20)}}
+ ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2019, 3, 10), end: new CalendarDate(2019, 3, 15)}, minValue: new CalendarDate(2019, 2, 3), maxValue: new CalendarDate(2019, 2, 20)}}
`('$Name should disable both the next and previous buttons if outside valid date range', ({Calendar, props}) => {
let {getByLabelText} = render();
@@ -189,10 +162,8 @@ describe('CalendarBase', () => {
it.each`
Name | Calendar | props
- ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new Date(2019, 5, 5)}}
- ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 10)}}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{defaultValue: new Date(2019, 5, 5)}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range', defaultValue: [new Date(2019, 5, 5), new Date(2019, 5, 10)]}}
+ ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2019, 6, 5)}}
+ ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 10)}}}
`('$Name should change the month when previous or next buttons are clicked', ({Calendar, props}) => {
let {getByRole, getByLabelText, getAllByLabelText, getAllByRole} = render();
@@ -230,10 +201,8 @@ describe('CalendarBase', () => {
describe('labeling', () => {
it.each`
Name | Calendar | props
- ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new Date(2019, 5, 5)}}}
- ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 5)}}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{defaultValue: new Date(2019, 5, 5)}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{defaultValue: new Date(2019, 5, 5), selectionType: 'range'}}
+ ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2019, 6, 5)}}}
+ ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 5)}}}
`('$Name should be labeled by month heading by default', async ({Calendar, props}) => {
let {getByRole} = render();
let calendar = getByRole('group');
@@ -248,8 +217,6 @@ describe('CalendarBase', () => {
Name | Calendar | props
${'v3 Calendar'} | ${Calendar} | ${{}}
${'v3 RangeCalendar'} | ${RangeCalendar} | ${{}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range'}}
`('$Name should support labeling with aria-label', ({Calendar, props}) => {
let {getByRole} = render();
let calendar = getByRole('group');
@@ -265,8 +232,6 @@ describe('CalendarBase', () => {
Name | Calendar | props
${'v3 Calendar'} | ${Calendar} | ${{}}
${'v3 RangeCalendar'} | ${RangeCalendar} | ${{}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range'}}
`('$Name should support labeling with aria-labelledby', ({Calendar, props}) => {
let {getByRole} = render();
let calendar = getByRole('group');
@@ -281,8 +246,6 @@ describe('CalendarBase', () => {
Name | Calendar | props
${'v3 Calendar'} | ${Calendar} | ${{}}
${'v3 RangeCalendar'} | ${RangeCalendar} | ${{}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range'}}
`('$Name should support labeling with aria-labelledby and aria-label', ({Calendar, props}) => {
let {getByRole} = render();
let calendar = getByRole('group');
@@ -298,8 +261,6 @@ describe('CalendarBase', () => {
Name | Calendar | props
${'v3 Calendar'} | ${Calendar} | ${{}}
${'v3 RangeCalendar'} | ${RangeCalendar} | ${{}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range'}}
`('$Name should support labeling with a custom id', ({Calendar, props}) => {
let {getByRole} = render();
let calendar = getByRole('group');
@@ -314,36 +275,24 @@ describe('CalendarBase', () => {
describe('keyboard navigation', () => {
async function testKeyboard(Calendar, defaultValue, key, value, month, props, opts) {
- let isV2 = Calendar === V2Calendar;
-
// For range calendars, convert the value to a range of one day
if (Calendar === RangeCalendar) {
defaultValue = {start: defaultValue, end: defaultValue};
- } else if (isV2 && props && props.selectionType === 'range') {
- defaultValue = [defaultValue, defaultValue];
}
let {getByRole, getAllByRole, getByLabelText, getAllByLabelText, unmount} = render();
let grid = getAllByRole('grid')[0]; // get by role will see two, role=grid and implicit which also has role=grid
let cell = getAllByLabelText('selected', {exact: false}).filter(cell => cell.role !== 'grid')[0];
- if (isV2) {
- expect(grid).toHaveAttribute('aria-activedescendant', cell.id);
- } else {
- expect(grid).not.toHaveAttribute('aria-activedescendant');
- expect(document.activeElement).toBe(cell);
- }
+ expect(grid).not.toHaveAttribute('aria-activedescendant');
+ expect(document.activeElement).toBe(cell);
fireEvent.keyDown(document.activeElement, {key, keyCode: keyCodes[key], ...opts});
fireEvent.keyUp(document.activeElement, {key, keyCode: keyCodes[key], ...opts});
cell = getByLabelText(value, {exact: false});
- if (isV2) {
- expect(grid).toHaveAttribute('aria-activedescendant', cell.id);
- } else {
- expect(grid).not.toHaveAttribute('aria-activedescendant');
- expect(document.activeElement).toBe(cell);
- }
+ expect(grid).not.toHaveAttribute('aria-activedescendant');
+ expect(document.activeElement).toBe(cell);
let heading = getByRole('heading');
expect(heading).toHaveTextContent(month);
@@ -360,85 +309,68 @@ describe('CalendarBase', () => {
Name | Calendar | props
${'v3 Calendar'} | ${Calendar} | ${{}}
${'v3 RangeCalendar'} | ${RangeCalendar} | ${{}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range'}}
`('$Name should move the focused date by one day with the left/right arrows', async ({Calendar, props}) => {
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'ArrowLeft', 'Tuesday, June 4, 2019', 'June 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'ArrowRight', 'Thursday, June 6, 2019', 'June 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'ArrowLeft', 'Tuesday, June 4, 2019', 'June 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'ArrowRight', 'Thursday, June 6, 2019', 'June 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 1), 'ArrowLeft', 'Friday, May 31, 2019', 'May 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 30), 'ArrowRight', 'Monday, July 1, 2019', 'July 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 1), 'ArrowLeft', 'Friday, May 31, 2019', 'May 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 30), 'ArrowRight', 'Monday, July 1, 2019', 'July 2019', props);
});
it.each`
Name | Calendar | props
${'v3 Calendar'} | ${Calendar} | ${{}}
${'v3 RangeCalendar'} | ${RangeCalendar} | ${{}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range'}}
`('$Name should move the focused date by one week with the up/down arrows', async ({Calendar, props}) => {
- await testKeyboard(Calendar, new Date(2019, 5, 12), 'ArrowUp', 'Wednesday, June 5, 2019', 'June 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 12), 'ArrowDown', 'Wednesday, June 19, 2019', 'June 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 12), 'ArrowUp', 'Wednesday, June 5, 2019', 'June 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 12), 'ArrowDown', 'Wednesday, June 19, 2019', 'June 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'ArrowUp', 'Wednesday, May 29, 2019', 'May 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 26), 'ArrowDown', 'Wednesday, July 3, 2019', 'July 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'ArrowUp', 'Wednesday, May 29, 2019', 'May 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 26), 'ArrowDown', 'Wednesday, July 3, 2019', 'July 2019', props);
});
it.each`
Name | Calendar | props
${'v3 Calendar'} | ${Calendar} | ${{}}
${'v3 RangeCalendar'} | ${RangeCalendar} | ${{}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range'}}
`('$Name should move the focused date to the start or end of the month with the home/end keys', async ({Calendar, props}) => {
- await testKeyboard(Calendar, new Date(2019, 5, 12), 'Home', 'Saturday, June 1, 2019', 'June 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 12), 'End', 'Sunday, June 30, 2019', 'June 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 12), 'Home', 'Saturday, June 1, 2019', 'June 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 12), 'End', 'Sunday, June 30, 2019', 'June 2019', props);
});
it.each`
Name | Calendar | props
${'v3 Calendar'} | ${Calendar} | ${{}}
${'v3 RangeCalendar'} | ${RangeCalendar} | ${{}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range'}}
`('$Name should move the focused date by one month with the page up/page down keys', async ({Calendar, props}) => {
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'PageUp', 'Sunday, May 5, 2019', 'May 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'PageDown', 'Friday, July 5, 2019', 'July 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'PageUp', 'Sunday, May 5, 2019', 'May 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'PageDown', 'Friday, July 5, 2019', 'July 2019', props);
});
- // v2 tests disabled until next release
- // it.each`
- // Name | Calendar | props
- // ${'v3 Calendar'} | ${Calendar} | ${{}}
- // ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{}}
- // ${'v2 Calendar'} | ${V2Calendar} | ${{}}
- // ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range'}}
it.each`
Name | Calendar | props
${'v3 Calendar'} | ${Calendar} | ${{}}
${'v3 RangeCalendar'} | ${RangeCalendar} | ${{}}
`('$Name should move the focused date by one year with the shift + page up/shift + page down keys', async ({Calendar, props}) => {
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'PageUp', 'Tuesday, June 5, 2018', 'June 2018', props, {shiftKey: true});
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'PageDown', 'Friday, June 5, 2020', 'June 2020', props, {shiftKey: true});
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'PageUp', 'Tuesday, June 5, 2018', 'June 2018', props, {shiftKey: true});
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'PageDown', 'Friday, June 5, 2020', 'June 2020', props, {shiftKey: true});
});
it.each`
Name | Calendar | props
- ${'v3 Calendar'} | ${Calendar} | ${{minValue: new Date(2019, 5, 2), maxValue: new Date(2019, 5, 8)}}
- ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{minValue: new Date(2019, 5, 2), maxValue: new Date(2019, 5, 8)}}
- ${'v2 Calendar'} | ${V2Calendar} | ${{min: new Date(2019, 5, 5), max: new Date(2019, 5, 8)}}
- ${'v2 range Calendar'} | ${V2Calendar} | ${{selectionType: 'range', min: new Date(2019, 5, 5), max: new Date(2019, 5, 8)}}
+ ${'v3 Calendar'} | ${Calendar} | ${{minValue: new CalendarDate(2019, 6, 2), maxValue: new CalendarDate(2019, 6, 8)}}
+ ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{minValue: new CalendarDate(2019, 6, 2), maxValue: new CalendarDate(2019, 6, 8)}}
`('$Name should not move the focused date outside the valid range', async ({Calendar, props}) => {
- await testKeyboard(Calendar, new Date(2019, 5, 2), 'ArrowLeft', 'Sunday, June 2, 2019 selected', 'June 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 8), 'ArrowRight', 'Saturday, June 8, 2019 selected', 'June 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'ArrowUp', 'Wednesday, June 5, 2019 selected', 'June 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'ArrowDown', 'Wednesday, June 5, 2019 selected', 'June 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'Home', 'Wednesday, June 5, 2019 selected', 'June 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'End', 'Wednesday, June 5, 2019 selected', 'June 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'PageUp', 'Wednesday, June 5, 2019 selected', 'June 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'PageDown', 'Wednesday, June 5, 2019 selected', 'June 2019', props);
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'PageUp', 'Wednesday, June 5, 2019 selected', 'June 2019', props, {shiftKey: true});
- await testKeyboard(Calendar, new Date(2019, 5, 5), 'PageDown', 'Wednesday, June 5, 2019 selected', 'June 2019', props, {shiftKey: true});
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 2), 'ArrowLeft', 'Sunday, June 2, 2019 selected', 'June 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 8), 'ArrowRight', 'Saturday, June 8, 2019 selected', 'June 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'ArrowUp', 'Wednesday, June 5, 2019 selected', 'June 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'ArrowDown', 'Wednesday, June 5, 2019 selected', 'June 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'Home', 'Wednesday, June 5, 2019 selected', 'June 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'End', 'Wednesday, June 5, 2019 selected', 'June 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'PageUp', 'Wednesday, June 5, 2019 selected', 'June 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'PageDown', 'Wednesday, June 5, 2019 selected', 'June 2019', props);
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'PageUp', 'Wednesday, June 5, 2019 selected', 'June 2019', props, {shiftKey: true});
+ await testKeyboard(Calendar, new CalendarDate(2019, 6, 5), 'PageDown', 'Wednesday, June 5, 2019 selected', 'June 2019', props, {shiftKey: true});
});
});
@@ -470,8 +402,8 @@ describe('CalendarBase', () => {
it.each`
Name | Calendar | props
- ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new Date(2019, 5, 5)}}
- ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 10)}}}
+ ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2019, 6, 5)}}
+ ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 10)}}}
`('$Name should mirror arrow key movement in an RTL locale', ({Calendar, props}) => {
// LTR
let {getByRole, getAllByRole, rerender} = render(
diff --git a/packages/@react-spectrum/calendar/test/RangeCalendar.test.js b/packages/@react-spectrum/calendar/test/RangeCalendar.test.js
index 4a5d185d9de..62e547a8d91 100644
--- a/packages/@react-spectrum/calendar/test/RangeCalendar.test.js
+++ b/packages/@react-spectrum/calendar/test/RangeCalendar.test.js
@@ -12,12 +12,11 @@
jest.mock('@react-aria/live-announcer');
import {announce} from '@react-aria/live-announcer';
+import {CalendarDate} from '@internationalized/date';
import {fireEvent, render} from '@testing-library/react';
import {RangeCalendar} from '../';
import React from 'react';
-import {startOfDay} from 'date-fns';
import {triggerPress} from '@react-spectrum/test-utils';
-import V2Calendar from '@react/react-spectrum/Calendar';
let cellFormatter = new Intl.DateTimeFormat('en-US', {weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'});
let keyCodes = {'Enter': 13, ' ': 32, 'PageUp': 33, 'PageDown': 34, 'End': 35, 'Home': 36, 'ArrowLeft': 37, 'ArrowUp': 38, 'ArrowRight': 39, 'ArrowDown': 40, Escape: 27};
@@ -34,10 +33,8 @@ describe('RangeCalendar', () => {
describe('basics', () => {
it.each`
Name | RangeCalendar | props
- ${'v3'} | ${RangeCalendar} | ${{defaultValue: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 10)}}}
- ${'v2'} | ${V2Calendar} | ${{selectionType: 'range', defaultValue: [new Date(2019, 5, 5), new Date(2019, 5, 10)]}}
+ ${'v3'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 10)}}}
`('$Name should render a calendar with a defaultValue', ({RangeCalendar, props}) => {
- let isV2 = RangeCalendar === V2Calendar;
let {getAllByLabelText, getByRole, getAllByRole} = render();
let heading = getByRole('heading');
@@ -59,18 +56,16 @@ describe('RangeCalendar', () => {
let i = 0;
for (let cell of selectedDates) {
- expect(isV2 ? cell : cell.parentElement).toHaveAttribute('role', 'gridcell');
- expect(isV2 ? cell : cell.parentElement).toHaveAttribute('aria-selected', 'true');
+ expect(cell.parentElement).toHaveAttribute('role', 'gridcell');
+ expect(cell.parentElement).toHaveAttribute('aria-selected', 'true');
expect(cell).toHaveAttribute('aria-label', labels[i++]);
}
});
it.each`
Name | RangeCalendar | props
- ${'v3'} | ${RangeCalendar} | ${{value: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 10)}}}
- ${'v2'} | ${V2Calendar} | ${{selectionType: 'range', value: [new Date(2019, 5, 5), new Date(2019, 5, 10)]}}
+ ${'v3'} | ${RangeCalendar} | ${{value: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 10)}}}
`('$Name should render a calendar with a value', ({RangeCalendar, props}) => {
- let isV2 = RangeCalendar === V2Calendar;
let {getAllByLabelText, getByRole, getAllByRole} = render();
let heading = getByRole('heading');
@@ -92,38 +87,30 @@ describe('RangeCalendar', () => {
let i = 0;
for (let cell of selectedDates) {
- expect(isV2 ? cell : cell.parentElement).toHaveAttribute('role', 'gridcell');
- expect(isV2 ? cell : cell.parentElement).toHaveAttribute('aria-selected', 'true');
+ expect(cell.parentElement).toHaveAttribute('role', 'gridcell');
+ expect(cell.parentElement).toHaveAttribute('aria-selected', 'true');
expect(cell).toHaveAttribute('aria-label', labels[i++]);
}
});
it.each`
Name | RangeCalendar | props
- ${'v3'} | ${RangeCalendar} | ${{value: {start: new Date(2019, 1, 3), end: new Date(2019, 1, 18)}}}
- ${'v2'} | ${V2Calendar} | ${{selectionType: 'range', value: [new Date(2019, 1, 3), new Date(2019, 1, 18)]}}
+ ${'v3'} | ${RangeCalendar} | ${{value: {start: new CalendarDate(2019, 2, 3), end: new CalendarDate(2019, 2, 18)}}}
`('$Name should focus the first selected date if autoFocus is set', ({RangeCalendar, props}) => {
let {getByRole, getAllByLabelText} = render();
let cells = getAllByLabelText('selected', {exact: false});
let grid = getByRole('grid');
- if (RangeCalendar === V2Calendar) {
- expect(cells[0]).toHaveAttribute('role', 'gridcell');
- expect(cells[0]).toHaveAttribute('aria-selected', 'true');
- expect(grid).toHaveFocus();
- expect(grid).toHaveAttribute('aria-activedescendant', cells[0].id);
- } else {
- expect(cells[0].parentElement).toHaveAttribute('role', 'gridcell');
- expect(cells[0].parentElement).toHaveAttribute('aria-selected', 'true');
- expect(cells[0]).toHaveFocus();
- expect(grid).not.toHaveAttribute('aria-activedescendant');
- }
+ expect(cells[0].parentElement).toHaveAttribute('role', 'gridcell');
+ expect(cells[0].parentElement).toHaveAttribute('aria-selected', 'true');
+ expect(cells[0]).toHaveFocus();
+ expect(grid).not.toHaveAttribute('aria-activedescendant');
});
// v2 doesn't pass this test - it starts by showing the end date instead of the start date.
it('should show selected dates across multiple months', () => {
- let {getByRole, getByLabelText, getAllByLabelText, getAllByRole} = render();
+ let {getByRole, getByLabelText, getAllByLabelText, getAllByRole} = render();
let heading = getByRole('heading');
expect(heading).toHaveTextContent('June 2019');
@@ -206,35 +193,24 @@ describe('RangeCalendar', () => {
it.each`
Name | RangeCalendar | props
${'v3'} | ${RangeCalendar} | ${{}}
- ${'v2'} | ${V2Calendar} | ${{selectionType: 'range'}}
`('$Name adds a range selection prompt to the focused cell', ({RangeCalendar, props}) => {
- const isV2 = RangeCalendar === V2Calendar;
let {getByRole, getByLabelText} = render();
let grid = getByRole('grid');
let cell = getByLabelText('today', {exact: false});
- if (isV2) {
- expect(grid).toHaveAttribute('aria-activedescendant', cell.id);
- } else {
- expect(grid).not.toHaveAttribute('aria-activedescendant');
- }
+ expect(grid).not.toHaveAttribute('aria-activedescendant');
expect(cell).toHaveAttribute('aria-label', `Today, ${cellFormatter.format(new Date())} (Click to start selecting date range)`);
// enter selection mode
fireEvent.keyDown(grid, {key: 'Enter', keyCode: keyCodes.Enter});
- if (isV2) {
- expect(grid).toHaveAttribute('aria-activedescendant', cell.id);
- } else {
- expect(grid).not.toHaveAttribute('aria-activedescendant');
- }
- expect(isV2 ? cell : cell.parentElement).toHaveAttribute('aria-selected');
+ expect(grid).not.toHaveAttribute('aria-activedescendant');
+ expect(cell.parentElement).toHaveAttribute('aria-selected');
expect(cell).toHaveAttribute('aria-label', `Today, ${cellFormatter.format(new Date())} selected (Click to finish selecting date range)`);
});
it.each`
Name | RangeCalendar | props
- ${'v3'} | ${RangeCalendar} | ${{defaultValue: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 10)}}}
- ${'v2'} | ${V2Calendar} | ${{selectionType: 'range', defaultValue: [new Date(2019, 5, 5), new Date(2019, 5, 10)]}}
+ ${'v3'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 10)}}}
`('$Name can select a range with the keyboard (uncontrolled)', ({RangeCalendar, props}) => {
let onChange = jest.fn();
let {getAllByLabelText, getByRole} = render(
@@ -273,22 +249,14 @@ describe('RangeCalendar', () => {
expect(selectedDates[selectedDates.length - 1].textContent).toBe('8');
expect(onChange).toHaveBeenCalledTimes(1);
- let value = onChange.mock.calls[0][0];
- let start, end;
- if (Array.isArray(value)) { // v2
- [start, end] = value;
- } else { // v3
- ({start, end} = value);
- }
-
- expect(start.valueOf()).toBe(new Date(2019, 5, 4).valueOf()); // v2 returns a moment object
- expect(startOfDay(end).valueOf()).toBe(new Date(2019, 5, 8).valueOf()); // v2 returns a moment object
+ let {start, end} = onChange.mock.calls[0][0];
+ expect(start).toEqual(new CalendarDate(2019, 6, 4));
+ expect(end).toEqual(new CalendarDate(2019, 6, 8));
});
it.each`
Name | RangeCalendar | props
- ${'v3'} | ${RangeCalendar} | ${{value: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 10)}}}
- ${'v2'} | ${V2Calendar} | ${{selectionType: 'range', value: [new Date(2019, 5, 5), new Date(2019, 5, 10)]}}
+ ${'v3'} | ${RangeCalendar} | ${{value: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 10)}}}
`('$Name can select a range with the keyboard (controlled)', ({RangeCalendar, props}) => {
let onChange = jest.fn();
let {getAllByLabelText, getByRole} = render(
@@ -327,19 +295,11 @@ describe('RangeCalendar', () => {
expect(selectedDates[selectedDates.length - 1].textContent).toBe('10');
expect(onChange).toHaveBeenCalledTimes(1);
- let value = onChange.mock.calls[0][0];
- let start, end;
- if (Array.isArray(value)) { // v2
- [start, end] = value;
- } else { // v3
- ({start, end} = value);
- }
-
- expect(start.valueOf()).toBe(new Date(2019, 5, 4).valueOf()); // v2 returns a moment object
- expect(startOfDay(end).valueOf()).toBe(new Date(2019, 5, 8).valueOf()); // v2 returns a moment object
+ let {start, end} = onChange.mock.calls[0][0];
+ expect(start).toEqual(new CalendarDate(2019, 6, 4));
+ expect(end).toEqual(new CalendarDate(2019, 6, 8));
});
- // v2 does not pass this test.
it('does not enter selection mode with the keyboard if isReadOnly', () => {
let {getByRole, getByLabelText} = render();
@@ -359,8 +319,7 @@ describe('RangeCalendar', () => {
it.each`
Name | RangeCalendar | props
- ${'v3'} | ${RangeCalendar} | ${{defaultValue: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 10)}}}
- ${'v2'} | ${V2Calendar} | ${{selectionType: 'range', defaultValue: [new Date(2019, 5, 5), new Date(2019, 5, 10)]}}
+ ${'v3'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 10)}}}
`('$Name selects a range with the mouse (uncontrolled)', ({RangeCalendar, props}) => {
let onChange = jest.fn();
let {getAllByLabelText, getByText} = render(
@@ -391,22 +350,14 @@ describe('RangeCalendar', () => {
expect(selectedDates[selectedDates.length - 1].textContent).toBe('17');
expect(onChange).toHaveBeenCalledTimes(1);
- let value = onChange.mock.calls[0][0];
- let start, end;
- if (Array.isArray(value)) { // v2
- [start, end] = value;
- } else { // v3
- ({start, end} = value);
- }
-
- expect(start.valueOf()).toBe(new Date(2019, 5, 7).valueOf()); // v2 returns a moment object
- expect(startOfDay(end).valueOf()).toBe(new Date(2019, 5, 17).valueOf()); // v2 returns a moment object
+ let {start, end} = onChange.mock.calls[0][0];
+ expect(start).toEqual(new CalendarDate(2019, 6, 7));
+ expect(end).toEqual(new CalendarDate(2019, 6, 17));
});
it.each`
Name | RangeCalendar | props
- ${'v3'} | ${RangeCalendar} | ${{value: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 10)}}}
- ${'v2'} | ${V2Calendar} | ${{selectionType: 'range', value: [new Date(2019, 5, 5), new Date(2019, 5, 10)]}}
+ ${'v3'} | ${RangeCalendar} | ${{value: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 10)}}}
`('$Name selects a range with the mouse (controlled)', ({RangeCalendar, props}) => {
let onChange = jest.fn();
let {getAllByLabelText, getByText} = render(
@@ -437,19 +388,11 @@ describe('RangeCalendar', () => {
expect(selectedDates[selectedDates.length - 1].textContent).toBe('10');
expect(onChange).toHaveBeenCalledTimes(1);
- let value = onChange.mock.calls[0][0];
- let start, end;
- if (Array.isArray(value)) { // v2
- [start, end] = value;
- } else { // v3
- ({start, end} = value);
- }
-
- expect(start.valueOf()).toBe(new Date(2019, 5, 7).valueOf()); // v2 returns a moment object
- expect(startOfDay(end).valueOf()).toBe(new Date(2019, 5, 17).valueOf()); // v2 returns a moment object
+ let {start, end} = onChange.mock.calls[0][0];
+ expect(start).toEqual(new CalendarDate(2019, 6, 7));
+ expect(end).toEqual(new CalendarDate(2019, 6, 17));
});
- // v2 does not pass this test.
it('does not enter selection mode with the mouse if isReadOnly', () => {
let {getByRole, getByLabelText, getByText} = render();
@@ -469,7 +412,6 @@ describe('RangeCalendar', () => {
it.each`
Name | RangeCalendar | props
${'v3'} | ${RangeCalendar} | ${{isDisabled: true}}
- ${'v2'} | ${V2Calendar} | ${{selectionType: 'range', disabled: true}}
`('$Name does not select a date on click if isDisabled', ({RangeCalendar, props}) => {
let onChange = jest.fn();
let {getAllByLabelText, getByText} = render(
@@ -489,8 +431,7 @@ describe('RangeCalendar', () => {
it.each`
Name | RangeCalendar | props
- ${'v3'} | ${RangeCalendar} | ${{defaultValue: {start: new Date(2019, 1, 8), end: new Date(2019, 1, 15)}, minValue: new Date(2019, 1, 5), maxValue: new Date(2019, 1, 15)}}
- ${'v2'} | ${V2Calendar} | ${{selectionType: 'range', defaultValue: [new Date(2019, 1, 8), new Date(2019, 1, 15)], min: new Date(2019, 1, 5), max: new Date(2019, 1, 15)}}
+ ${'v3'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2019, 2, 8), end: new CalendarDate(2019, 2, 15)}, minValue: new CalendarDate(2019, 2, 5), maxValue: new CalendarDate(2019, 2, 15)}}
`('$Name does not select a date on click if outside the valid date range', ({RangeCalendar, props}) => {
let onChange = jest.fn();
let {getByLabelText, getAllByLabelText} = render(
@@ -530,8 +471,7 @@ describe('RangeCalendar', () => {
it.each`
Name | RangeCalendar | props
- ${'v3'} | ${RangeCalendar} | ${{value: {start: new Date(2019, 5, 5), end: new Date(2019, 5, 10)}}}
- ${'v2'} | ${V2Calendar} | ${{selectionType: 'range', value: [new Date(2019, 5, 5), new Date(2019, 5, 10)]}}
+ ${'v3'} | ${RangeCalendar} | ${{value: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 6, 10)}}}
`('$Name cancels the selection when the escape key is pressed', ({RangeCalendar, props}) => {
let onChange = jest.fn();
let {getByText, getAllByLabelText} = render(
@@ -570,7 +510,7 @@ describe('RangeCalendar', () => {
// These tests only work against v3
describe('announcing', () => {
it('announces when the current month changes', () => {
- let {getByLabelText} = render();
+ let {getByLabelText} = render();
let nextButton = getByLabelText('Next');
triggerPress(nextButton);
@@ -580,17 +520,17 @@ describe('RangeCalendar', () => {
});
it('announces when the selected date range changes', () => {
- let {getByText} = render();
+ let {getByText} = render();
triggerPress(getByText('17'));
triggerPress(getByText('10'));
expect(announce).toHaveBeenCalledTimes(1);
- expect(announce).toHaveBeenCalledWith('Selected Range: June 10, 2019 to June 17, 2019');
+ expect(announce).toHaveBeenCalledWith('Selected Range: June 10, 2019 to June 17, 2019', 'polite', 4000);
});
it('ensures that the active descendant is announced when the focused date changes', () => {
- let {getByRole, getAllByLabelText} = render();
+ let {getByRole, getAllByLabelText} = render();
let grid = getByRole('grid');
@@ -603,7 +543,7 @@ describe('RangeCalendar', () => {
});
it('renders a caption with the selected date range', () => {
- let {getByText, getByRole} = render();
+ let {getByText, getByRole} = render();
let grid = getByRole('grid');
let caption = document.getElementById(grid.getAttribute('aria-describedby'));
diff --git a/packages/@react-spectrum/checkbox/package.json b/packages/@react-spectrum/checkbox/package.json
index 8574f2eca92..87d12fed2d4 100644
--- a/packages/@react-spectrum/checkbox/package.json
+++ b/packages/@react-spectrum/checkbox/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-spectrum/checkbox",
- "version": "3.2.3",
+ "version": "3.2.4",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -32,24 +32,24 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/checkbox": "^3.2.2",
- "@react-aria/focus": "^3.3.0",
- "@react-aria/interactions": "^3.4.0",
- "@react-spectrum/form": "^3.2.2",
- "@react-spectrum/label": "^3.3.3",
- "@react-spectrum/utils": "^3.5.2",
- "@react-stately/checkbox": "^3.0.2",
- "@react-stately/toggle": "^3.2.2",
- "@react-types/checkbox": "^3.2.2",
- "@react-types/shared": "^3.6.0",
- "@spectrum-icons/ui": "^3.2.0"
+ "@react-aria/checkbox": "^3.2.3",
+ "@react-aria/focus": "^3.4.1",
+ "@react-aria/interactions": "^3.5.1",
+ "@react-spectrum/form": "^3.2.3",
+ "@react-spectrum/label": "^3.3.4",
+ "@react-spectrum/utils": "^3.6.2",
+ "@react-stately/checkbox": "^3.0.3",
+ "@react-stately/toggle": "^3.2.3",
+ "@react-types/checkbox": "^3.2.3",
+ "@react-types/shared": "^3.8.0",
+ "@spectrum-icons/ui": "^3.2.1"
},
"devDependencies": {
"@adobe/spectrum-css-temp": "3.0.0-alpha.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1",
- "@react-spectrum/provider": "^3.0.0-rc.1"
+ "@react-spectrum/provider": "^3.0.0"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-spectrum/color/chromatic/ColorField.chromatic.tsx b/packages/@react-spectrum/color/chromatic/ColorField.chromatic.tsx
new file mode 100644
index 00000000000..09a8c735712
--- /dev/null
+++ b/packages/@react-spectrum/color/chromatic/ColorField.chromatic.tsx
@@ -0,0 +1,121 @@
+/*
+ * 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 {ColorField} from '../';
+import {generatePowerset} from '@react-spectrum/story-utils';
+import {Grid, repeat} from '@react-spectrum/layout';
+import {Meta, Story} from '@storybook/react';
+import React from 'react';
+import {SpectrumColorFieldProps} from '@react-types/color';
+
+// Ignore read only because it doesn't apply any distingishable visual features
+let states = [
+ {isQuiet: true},
+ {isDisabled: true},
+ {validationState: ['valid', 'invalid']},
+ {isRequired: true},
+ {necessityIndicator: 'label'}
+];
+
+let combinations = generatePowerset(states);
+
+function shortName(key, value) {
+ let returnVal = '';
+ switch (key) {
+ case 'isQuiet':
+ returnVal = 'quiet';
+ break;
+ case 'isDisabled':
+ returnVal = 'disable';
+ break;
+ case 'validationState':
+ returnVal = `vs ${value}`;
+ break;
+ case 'isRequired':
+ returnVal = 'req';
+ break;
+ case 'necessityIndicator':
+ returnVal = 'necInd=label';
+ break;
+ }
+ return returnVal;
+}
+
+const meta: Meta = {
+ title: 'ColorField'
+};
+
+export default meta;
+
+const Template: Story = (args) => (
+
+ {combinations.map(c => {
+ let key = Object.keys(c).map(k => shortName(k, c[k])).join(' ');
+ if (!key) {
+ key = 'empty';
+ }
+ return ;
+ })}
+
+);
+
+const TemplateSmall: Story = (args) => (
+
+ {combinations.map(c => {
+ let key = Object.keys(c).map(k => shortName(k, c[k])).join(' ');
+ if (!key) {
+ key = 'empty';
+ }
+ return ;
+ })}
+
+);
+
+const NoLabelTemplate: Story = (args) => (
+
+ {combinations.filter(combo => combo.isRequired == null && combo.necessityIndicator == null).map(c => {
+ let key = Object.keys(c).map(k => shortName(k, c[k])).join(' ');
+ if (!key) {
+ key = 'empty';
+ }
+ return ;
+ })}
+
+);
+
+export const PropDefaults = Template.bind({});
+PropDefaults.storyName = 'default';
+PropDefaults.args = {};
+
+export const PropDefaultValue = Template.bind({});
+PropDefaultValue.storyName = 'default value';
+PropDefaultValue.args = {...PropDefaults.args, defaultValue: '#abcdef'};
+
+export const PropPlaceholder = Template.bind({});
+PropPlaceholder.storyName = 'placeholder';
+PropPlaceholder.args = {...PropDefaults.args, placeholder: 'Enter a hex color'};
+
+export const PropAriaLabelled = NoLabelTemplate.bind({});
+PropAriaLabelled.storyName = 'aria-label';
+PropAriaLabelled.args = {'aria-label': 'Label'};
+
+export const PropLabelEnd = Template.bind({});
+PropLabelEnd.storyName = 'label end';
+PropLabelEnd.args = {...PropDefaults.args, labelAlign: 'end', defaultValue: '#abcdef'};
+
+export const PropLabelSide = TemplateSmall.bind({});
+PropLabelSide.storyName = 'label side';
+PropLabelSide.args = {...PropDefaults.args, labelPosition: 'side', defaultValue: '#abcdef'};
+
+export const PropCustomWidth = Template.bind({});
+PropCustomWidth.storyName = 'custom width';
+PropCustomWidth.args = {...PropDefaults.args, width: 'size-3000'};
diff --git a/packages/@react-spectrum/color/chromatic/ColorSlider.chromatic.tsx b/packages/@react-spectrum/color/chromatic/ColorSlider.chromatic.tsx
new file mode 100644
index 00000000000..e334417ac14
--- /dev/null
+++ b/packages/@react-spectrum/color/chromatic/ColorSlider.chromatic.tsx
@@ -0,0 +1,103 @@
+/*
+ * 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 {ColorSlider} from '../';
+import {generatePowerset} from '@react-spectrum/story-utils';
+import {Grid, repeat} from '@react-spectrum/layout';
+import {Meta, Story} from '@storybook/react';
+import React from 'react';
+import {SpectrumColorSliderProps} from '@react-types/color';
+
+let states = [
+ {isDisabled: true},
+ {label: [null, 'custom label']},
+ {showValueLabel: false}
+];
+
+let combinations = generatePowerset(states, (merged) => merged.label === null && merged.showValueLabel === false);
+
+function shortName(key, value) {
+ let returnVal = '';
+ switch (key) {
+ case 'isDisabled':
+ returnVal = 'disable';
+ break;
+ case 'label':
+ returnVal = `${value === null ? 'no label' : value}`;
+ break;
+ case 'orientation':
+ returnVal = 'vertical';
+ break;
+ case 'showValueLabel':
+ returnVal = 'noValLabel';
+ break;
+ }
+ return returnVal;
+}
+
+const meta: Meta = {
+ title: 'ColorSlider'
+};
+
+export default meta;
+
+const Template: Story = (args) => (
+
+ {combinations.map(c => {
+ let key = Object.keys(c).map(k => shortName(k, c[k])).join(' ');
+ if (!key) {
+ key = 'empty';
+ }
+ return ;
+ })}
+
+);
+
+const VerticalTemplate: Story = (args) => (
+
+ {combinations.map(c => {
+ let key = Object.keys(c).map(k => shortName(k, c[k])).join(' ');
+ if (!key) {
+ key = 'empty';
+ }
+ return ;
+ })}
+
+);
+
+export const PropChannelRed = Template.bind({});
+PropChannelRed.storyName = 'channel: red';
+PropChannelRed.args = {channel: 'red', defaultValue: '#7f0000'};
+
+export const PropChannelAlpha = Template.bind({});
+PropChannelAlpha.storyName = 'channel: alpha';
+PropChannelAlpha.args = {channel: 'alpha', defaultValue: '#7f0000'};
+
+export const PropChannelLightness = Template.bind({});
+PropChannelLightness.storyName = 'channel: lightness';
+PropChannelLightness.args = {channel: 'lightness', defaultValue: 'hsla(0, 100%, 50%, 0.5)'};
+
+export const PropChannelBrightness = Template.bind({});
+PropChannelBrightness.storyName = 'channel: brightness';
+PropChannelBrightness.args = {channel: 'brightness', defaultValue: 'hsba(0, 100%, 50%, 0.5)'};
+
+export const PropVertical = VerticalTemplate.bind({});
+PropVertical.storyName = 'orientation: vertical';
+PropVertical.args = {channel: 'red', defaultValue: '#7f0000'};
+
+export const PropCustomWidth = Template.bind({});
+PropCustomWidth.storyName = 'custom width';
+PropCustomWidth.args = {channel: 'red', defaultValue: '#7f0000', width: 'size-3600'};
+
+export const PropCustomHeight = VerticalTemplate.bind({});
+PropCustomHeight.storyName = 'custom height';
+PropCustomHeight.args = {channel: 'red', defaultValue: '#7f0000', height: 'size-3600'};
diff --git a/packages/@react-spectrum/color/package.json b/packages/@react-spectrum/color/package.json
index 4aef00e77fb..8199da04547 100644
--- a/packages/@react-spectrum/color/package.json
+++ b/packages/@react-spectrum/color/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-spectrum/color",
- "version": "3.0.0-beta.2",
+ "version": "3.0.0-beta.3",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -32,30 +32,31 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/color": "3.0.0-beta.2",
- "@react-aria/focus": "^3.3.0",
- "@react-aria/i18n": "^3.3.1",
- "@react-aria/interactions": "^3.4.0",
- "@react-aria/slider": "^3.0.2",
- "@react-aria/utils": "^3.8.0",
- "@react-spectrum/label": "^3.3.3",
- "@react-spectrum/layout": "^3.1.4",
- "@react-spectrum/textfield": "^3.1.6",
- "@react-spectrum/utils": "^3.5.2",
- "@react-stately/color": "3.0.0-beta.2",
- "@react-stately/slider": "^3.0.2",
- "@react-stately/utils": "^3.2.1",
- "@react-types/color": "3.0.0-beta.1",
- "@react-types/shared": "^3.6.0",
- "@react-types/slider": "^3.0.1",
- "@react-types/textfield": "^3.1.0"
+ "@react-aria/color": "3.0.0-beta.3",
+ "@react-aria/focus": "^3.4.1",
+ "@react-aria/i18n": "^3.3.2",
+ "@react-aria/interactions": "^3.5.1",
+ "@react-aria/slider": "^3.0.3",
+ "@react-aria/utils": "^3.8.2",
+ "@react-spectrum/label": "^3.3.4",
+ "@react-spectrum/layout": "^3.2.1",
+ "@react-spectrum/textfield": "^3.1.7",
+ "@react-spectrum/utils": "^3.6.2",
+ "@react-stately/color": "3.0.0-beta.3",
+ "@react-stately/slider": "^3.0.3",
+ "@react-stately/utils": "^3.2.2",
+ "@react-types/color": "3.0.0-beta.2",
+ "@react-types/shared": "^3.8.0",
+ "@react-types/slider": "^3.0.2",
+ "@react-types/textfield": "^3.2.3"
},
"devDependencies": {
"@adobe/spectrum-css-temp": "3.0.0-alpha.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1",
- "@react-spectrum/provider": "^3.0.0-rc.1"
+ "@react-spectrum/provider": "^3.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-spectrum/color/test/ColorSlider.test.tsx b/packages/@react-spectrum/color/test/ColorSlider.test.tsx
index 0145382ed9f..2e7d40893ba 100644
--- a/packages/@react-spectrum/color/test/ColorSlider.test.tsx
+++ b/packages/@react-spectrum/color/test/ColorSlider.test.tsx
@@ -163,27 +163,27 @@ describe('ColorSlider', () => {
});
it('hides value label with showValueLabel=false', () => {
- let {getByRole} = render();
- expect(() => getByRole('status')).toThrow();
+ let {queryByRole} = render();
+ expect(queryByRole('status')).toBeNull();
});
it('hides value label when no visible label', () => {
- let {getByRole} = render();
- expect(() => getByRole('status')).toThrow();
+ let {queryByRole} = render();
+ expect(queryByRole('status')).toBeNull();
});
it('hides value label when aria-label is specified', () => {
- let {getByRole} = render();
- expect(() => getByRole('status')).toThrow();
+ let {queryByRole} = render();
+ expect(queryByRole('status')).toBeNull();
});
it('hides value label when aria-labelledby is specified', () => {
- let {getByRole} = render();
- expect(() => getByRole('status')).toThrow();
+ let {queryByRole} = render();
+ expect(queryByRole('status')).toBeNull();
});
it('hides label and value label and has default aria-label when orientation=vertical', () => {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let slider = getByRole('slider');
let group = getByRole('group');
@@ -192,11 +192,11 @@ describe('ColorSlider', () => {
expect(group).toHaveAttribute('id');
expect(slider).toHaveAttribute('aria-labelledby', group.id);
- expect(() => getByRole('status')).toThrow();
+ expect(queryByRole('status')).toBeNull();
});
it('uses custom label as aria-label orientation=vertical', () => {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let slider = getByRole('slider');
let group = getByRole('group');
@@ -205,11 +205,11 @@ describe('ColorSlider', () => {
expect(group).toHaveAttribute('id');
expect(slider).toHaveAttribute('aria-labelledby', group.id);
- expect(() => getByRole('status')).toThrow();
+ expect(queryByRole('status')).toBeNull();
});
it('supports custom aria-label with orientation=vertical', () => {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let slider = getByRole('slider');
let group = getByRole('group');
@@ -218,11 +218,11 @@ describe('ColorSlider', () => {
expect(group).toHaveAttribute('id');
expect(slider).toHaveAttribute('aria-labelledby', group.id);
- expect(() => getByRole('status')).toThrow();
+ expect(queryByRole('status')).toBeNull();
});
it('supports custom aria-labelledby with orientation=vertical', () => {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let slider = getByRole('slider');
let group = getByRole('group');
@@ -231,7 +231,7 @@ describe('ColorSlider', () => {
expect(group).toHaveAttribute('id');
expect(slider).toHaveAttribute('aria-labelledby', group.id);
- expect(() => getByRole('status')).toThrow();
+ expect(queryByRole('status')).toBeNull();
});
});
diff --git a/packages/@react-spectrum/combobox/chromatic/ComboBox.chromatic.tsx b/packages/@react-spectrum/combobox/chromatic/ComboBox.chromatic.tsx
new file mode 100644
index 00000000000..65cc9e591d6
--- /dev/null
+++ b/packages/@react-spectrum/combobox/chromatic/ComboBox.chromatic.tsx
@@ -0,0 +1,134 @@
+/*
+ * 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 {ComboBox, Item} from '../';
+import {generatePowerset} from '@react-spectrum/story-utils';
+import {Grid, repeat} from '@react-spectrum/layout';
+import {Meta, Story} from '@storybook/react';
+import React from 'react';
+import {SpectrumComboBoxProps} from '@react-types/combobox';
+
+// Skipping focus styles because don't have a way of applying it via classnames
+// No controlled open state also means no menu
+let states = [
+ {isQuiet: true},
+ {isReadOnly: true},
+ {isDisabled: true},
+ {validationState: ['valid', 'invalid']},
+ {isRequired: true},
+ {necessityIndicator: 'label'}
+];
+
+let combinations = generatePowerset(states);
+
+function shortName(key, value) {
+ let returnVal = '';
+ switch (key) {
+ case 'isQuiet':
+ returnVal = 'quiet';
+ break;
+ case 'isReadOnly':
+ returnVal = 'ro';
+ break;
+ case 'isDisabled':
+ returnVal = 'disable';
+ break;
+ case 'validationState':
+ returnVal = `vs ${value}`;
+ break;
+ case 'isRequired':
+ returnVal = 'req';
+ break;
+ case 'necessityIndicator':
+ returnVal = 'necInd=label';
+ break;
+ }
+ return returnVal;
+}
+
+const meta: Meta> = {
+ title: 'ComboBox',
+ parameters: {
+ chromaticProvider: {colorSchemes: ['light', 'dark', 'lightest', 'darkest'], locales: ['en-US'], scales: ['medium', 'large']}
+ }
+};
+
+export default meta;
+
+let items = [
+ {name: 'Aardvark', id: '1'},
+ {name: 'Kangaroo', id: '2'},
+ {name: 'Snake', id: '3'}
+];
+
+const Template: Story> = (args) => (
+
+ {combinations.map(c => {
+ let key = Object.keys(c).map(k => shortName(k, c[k])).join(' ');
+ if (!key) {
+ key = 'empty';
+ }
+
+ return (
+
+ {(item: any) => - {item.name}
}
+
+ );
+ })}
+
+);
+
+// Chromatic can't handle the size of the side label story so removed some extraneous props that don't matter for side label case.
+const TemplateSideLabel: Story> = (args) => (
+
+ {combinations.filter(combo => !(combo.isReadOnly || combo.isDisabled)).map(c => {
+ let key = Object.keys(c).map(k => shortName(k, c[k])).join(' ');
+ if (!key) {
+ key = 'empty';
+ }
+
+ return (
+
+ {(item: any) => - {item.name}
}
+
+ );
+ })}
+
+);
+
+export const PropDefaults = Template.bind({});
+PropDefaults.storyName = 'default';
+PropDefaults.args = {};
+
+export const PropSelectedKey = Template.bind({});
+PropSelectedKey.storyName = 'selectedKey: 2';
+PropSelectedKey.args = {selectedKey: '2'};
+
+export const PropInputValue = Template.bind({});
+PropInputValue.storyName = 'inputValue: Blah';
+PropInputValue.args = {inputValue: 'Blah'};
+
+export const PropAriaLabelled = Template.bind({});
+PropAriaLabelled.storyName = 'aria-label';
+PropAriaLabelled.args = {'aria-label': 'Label'};
+
+export const PropLabelEnd = Template.bind({});
+PropLabelEnd.storyName = 'label end';
+PropLabelEnd.args = {...PropDefaults.args, labelAlign: 'end'};
+
+export const PropLabelSide = TemplateSideLabel.bind({});
+PropLabelSide.storyName = 'label side';
+PropLabelSide.args = {...PropDefaults.args, labelPosition: 'side'};
+
+export const PropCustomWidth = Template.bind({});
+PropCustomWidth.storyName = 'custom width';
+PropCustomWidth.args = {...PropDefaults.args, width: 'size-1600'};
diff --git a/packages/@react-spectrum/combobox/chromatic/ComboBoxRTL.chromatic.tsx b/packages/@react-spectrum/combobox/chromatic/ComboBoxRTL.chromatic.tsx
new file mode 100644
index 00000000000..45dd7753b6b
--- /dev/null
+++ b/packages/@react-spectrum/combobox/chromatic/ComboBoxRTL.chromatic.tsx
@@ -0,0 +1,25 @@
+/*
+ * 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 {Meta} from '@storybook/react';
+
+// Original ComboBox chromatic story was too large to be processed
+const meta: Meta = {
+ title: 'ComboBoxRTL',
+ parameters: {
+ chromaticProvider: {colorSchemes: ['light', 'dark', 'lightest', 'darkest'], locales: ['ar-AE'], scales: ['medium', 'large']}
+ }
+};
+
+export default meta;
+
+export {PropDefaults, PropSelectedKey, PropInputValue, PropAriaLabelled, PropLabelEnd, PropLabelSide, PropCustomWidth} from './ComboBox.chromatic';
diff --git a/packages/@react-spectrum/combobox/package.json b/packages/@react-spectrum/combobox/package.json
index 716bde9d373..9b2a4ee8bf7 100644
--- a/packages/@react-spectrum/combobox/package.json
+++ b/packages/@react-spectrum/combobox/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-spectrum/combobox",
- "version": "3.0.0-rc.0",
+ "version": "3.0.1",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -32,37 +32,38 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/button": "^3.3.2",
- "@react-aria/combobox": "3.0.0-rc.0",
- "@react-aria/dialog": "^3.1.3",
- "@react-aria/focus": "^3.4.0",
- "@react-aria/i18n": "^3.3.1",
- "@react-aria/interactions": "^3.5.0",
- "@react-aria/label": "^3.1.2",
- "@react-aria/overlays": "^3.7.0",
- "@react-aria/utils": "^3.8.1",
- "@react-spectrum/button": "^3.5.0",
- "@react-spectrum/label": "^3.3.3",
- "@react-spectrum/listbox": "^3.4.3",
- "@react-spectrum/overlays": "^3.4.2",
- "@react-spectrum/progress": "^3.1.2",
- "@react-spectrum/textfield": "^3.1.6",
- "@react-spectrum/utils": "^3.6.0",
- "@react-stately/collections": "^3.3.2",
- "@react-stately/combobox": "3.0.0-rc.0",
- "@react-types/button": "^3.4.0",
- "@react-types/combobox": "3.0.0-rc.0",
- "@react-types/overlays": "^3.5.0",
- "@react-types/shared": "^3.7.0",
- "@react-types/textfield": "^3.2.2",
- "@spectrum-icons/ui": "^3.1.0"
+ "@react-aria/button": "^3.3.3",
+ "@react-aria/combobox": "^3.0.1",
+ "@react-aria/dialog": "^3.1.4",
+ "@react-aria/focus": "^3.4.1",
+ "@react-aria/i18n": "^3.3.2",
+ "@react-aria/interactions": "^3.5.1",
+ "@react-aria/label": "^3.1.3",
+ "@react-aria/overlays": "^3.7.2",
+ "@react-aria/utils": "^3.8.2",
+ "@react-spectrum/button": "^3.5.1",
+ "@react-spectrum/label": "^3.3.4",
+ "@react-spectrum/listbox": "^3.5.1",
+ "@react-spectrum/overlays": "^3.4.4",
+ "@react-spectrum/progress": "^3.1.3",
+ "@react-spectrum/textfield": "^3.1.7",
+ "@react-spectrum/utils": "^3.6.2",
+ "@react-stately/collections": "^3.3.3",
+ "@react-stately/combobox": "^3.0.1",
+ "@react-types/button": "^3.4.1",
+ "@react-types/combobox": "^3.0.1",
+ "@react-types/overlays": "^3.5.1",
+ "@react-types/shared": "^3.8.0",
+ "@react-types/textfield": "^3.2.3",
+ "@spectrum-icons/ui": "^3.2.1"
},
"devDependencies": {
"@adobe/spectrum-css-temp": "3.0.0-alpha.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1",
- "@react-spectrum/provider": "^3.0.0-rc.1"
+ "@react-spectrum/provider": "^3.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-spectrum/combobox/src/ComboBox.tsx b/packages/@react-spectrum/combobox/src/ComboBox.tsx
index 9f2088a1181..e27eda335cb 100644
--- a/packages/@react-spectrum/combobox/src/ComboBox.tsx
+++ b/packages/@react-spectrum/combobox/src/ComboBox.tsx
@@ -108,7 +108,7 @@ const ComboBoxBase = React.forwardRef(function ComboBoxBase(pr
state
);
- let {overlayProps, placement} = useOverlayPosition({
+ let {overlayProps, placement, updatePosition} = useOverlayPosition({
targetRef: unwrappedButtonRef,
overlayRef: unwrappedPopoverRef,
scrollRef: listBoxRef,
@@ -137,6 +137,17 @@ const ComboBoxBase = React.forwardRef(function ComboBoxBase(pr
useLayoutEffect(onResize, [scale, onResize]);
+ // Update position once the ListBox has rendered. This ensures that
+ // it flips properly when it doesn't fit in the available space.
+ // TODO: add ResizeObserver to useOverlayPosition so we don't need this.
+ useLayoutEffect(() => {
+ if (state.isOpen) {
+ requestAnimationFrame(() => {
+ updatePosition();
+ });
+ }
+ }, [state.isOpen, updatePosition]);
+
let style = {
...overlayProps.style,
width: isQuiet ? null : menuWidth,
@@ -145,7 +156,10 @@ const ComboBoxBase = React.forwardRef(function ComboBoxBase(pr
return (
<>
-
+
(pr
isNonModal
isDismissable={false}>
getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onOpenChange).not.toHaveBeenCalled();
expect(onFocus).not.toHaveBeenCalled();
@@ -303,7 +303,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onOpenChange).not.toHaveBeenCalled();
let button = getByRole('button');
@@ -312,13 +312,13 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onOpenChange).not.toHaveBeenCalled();
expect(onInputChange).not.toHaveBeenCalled();
});
it('can be readonly', function () {
- let {getByRole} = renderComboBox({isReadOnly: true, defaultInputValue: 'Blargh'});
+ let {getByRole, queryByRole} = renderComboBox({isReadOnly: true, defaultInputValue: 'Blargh'});
let combobox = getByRole('combobox');
typeText(combobox, 'One');
@@ -326,7 +326,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(combobox.value).toBe('Blargh');
expect(onOpenChange).not.toHaveBeenCalled();
expect(onFocus).toHaveBeenCalled();
@@ -337,7 +337,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onOpenChange).not.toHaveBeenCalled();
let button = getByRole('button');
@@ -346,7 +346,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onOpenChange).not.toHaveBeenCalled();
expect(onInputChange).not.toHaveBeenCalled();
});
@@ -444,11 +444,11 @@ describe('ComboBox', function () {
describe('button click', function () {
it('keeps focus within the textfield after opening the menu', function () {
- let {getByRole} = renderComboBox();
+ let {getByRole, queryByRole} = renderComboBox();
let button = getByRole('button');
let combobox = getByRole('combobox');
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
act(() => {
combobox.focus();
@@ -465,7 +465,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
});
it('doesn\'t focus first item if there are items loaded', function () {
@@ -485,7 +485,7 @@ describe('ComboBox', function () {
});
it('opens for touch', () => {
- let {getByRole} = renderComboBox({});
+ let {getByRole, queryByRole} = renderComboBox({});
let button = getByRole('button');
let combobox = getByRole('combobox');
@@ -508,7 +508,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
});
it('resets the focused item when re-opening the menu', function () {
@@ -544,11 +544,11 @@ describe('ComboBox', function () {
});
it('shows all items', function () {
- let {getByRole} = renderComboBox({defaultInputValue: 'gibberish'});
+ let {getByRole, queryByRole} = renderComboBox({defaultInputValue: 'gibberish'});
let button = getByRole('button');
let combobox = getByRole('combobox');
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
act(() => {
combobox.focus();
@@ -565,12 +565,12 @@ describe('ComboBox', function () {
describe('keyboard input', function () {
it('opens the menu on down arrow press', function () {
- let {getByRole} = renderComboBox();
+ let {getByRole, queryByRole} = renderComboBox();
let button = getByRole('button');
let combobox = getByRole('combobox');
act(() => {combobox.focus();});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onOpenChange).not.toHaveBeenCalled();
act(() => {
@@ -586,12 +586,12 @@ describe('ComboBox', function () {
});
it('opens the menu on up arrow press', function () {
- let {getByRole} = renderComboBox();
+ let {getByRole, queryByRole} = renderComboBox();
let button = getByRole('button');
let combobox = getByRole('combobox');
act(() => {combobox.focus();});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onOpenChange).not.toHaveBeenCalled();
act(() => {
@@ -607,12 +607,12 @@ describe('ComboBox', function () {
});
it('opens the menu on user typing', function () {
- let {getByRole} = renderComboBox();
+ let {getByRole, queryByRole} = renderComboBox();
let button = getByRole('button');
let combobox = getByRole('combobox');
act(() => {combobox.focus();});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onOpenChange).not.toHaveBeenCalled();
typeText(combobox, 'Two');
@@ -666,7 +666,7 @@ describe('ComboBox', function () {
});
it('closes the menu if there are no matching items', function () {
- let {getByRole} = renderComboBox();
+ let {getByRole, queryByRole} = renderComboBox();
let button = getByRole('button');
let combobox = getByRole('combobox');
@@ -684,7 +684,7 @@ describe('ComboBox', function () {
typeText(combobox, 'z');
act(() => jest.runAllTimers());
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(button).toHaveAttribute('aria-expanded', 'false');
expect(button).not.toHaveAttribute('aria-controls');
expect(combobox).not.toHaveAttribute('aria-controls');
@@ -692,7 +692,7 @@ describe('ComboBox', function () {
});
it('doesn\'t open the menu on user typing if menuTrigger=manual', function () {
- let {getByRole} = renderComboBox({menuTrigger: 'manual'});
+ let {getByRole, queryByRole} = renderComboBox({menuTrigger: 'manual'});
let combobox = getByRole('combobox');
// Need to focus and skip click so combobox doesn't open for virtual click
@@ -702,7 +702,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onOpenChange).not.toHaveBeenCalled();
let button = getByRole('button');
@@ -719,7 +719,7 @@ describe('ComboBox', function () {
});
it('doesn\'t open the menu if no items match', function () {
- let {getByRole} = renderComboBox();
+ let {getByRole, queryByRole} = renderComboBox();
let combobox = getByRole('combobox');
act(() => combobox.focus());
@@ -728,7 +728,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onOpenChange).not.toHaveBeenCalled();
});
});
@@ -822,7 +822,7 @@ describe('ComboBox', function () {
});
it('allows the user to select an item via Enter', function () {
- let {getByRole} = renderComboBox();
+ let {getByRole, queryByRole} = renderComboBox();
let button = getByRole('button');
let combobox = getByRole('combobox');
@@ -851,14 +851,14 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(combobox.value).toBe('One');
expect(onSelectionChange).toHaveBeenCalledWith('1');
expect(onSelectionChange).toHaveBeenCalledTimes(1);
});
it('resets input text if reselecting a selected option with Enter', function () {
- let {getByRole} = renderComboBox({defaultSelectedKey: '2'});
+ let {getByRole, queryByRole} = renderComboBox({defaultSelectedKey: '2'});
let combobox = getByRole('combobox');
expect(combobox.value).toBe('Two');
@@ -891,7 +891,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(combobox.value).toBe('Two');
expect(onSelectionChange).toHaveBeenCalledTimes(0);
expect(onInputChange).toHaveBeenCalledTimes(3);
@@ -899,7 +899,7 @@ describe('ComboBox', function () {
});
it('resets input text if reselecting a selected option with click', function () {
- let {getByRole} = renderComboBox({defaultSelectedKey: '2'});
+ let {getByRole, queryByRole} = renderComboBox({defaultSelectedKey: '2'});
let combobox = getByRole('combobox');
expect(combobox.value).toBe('Two');
@@ -924,7 +924,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(combobox.value).toBe('Two');
// selectionManager.select from useSingleSelectListState always calls onSelectionChange even if the key is the same
expect(onSelectionChange).toHaveBeenCalledTimes(1);
@@ -934,7 +934,7 @@ describe('ComboBox', function () {
});
it('closes menu and resets selected key if allowsCustomValue=true and no item is focused', function () {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let combobox = getByRole('combobox');
act(() => combobox.focus());
@@ -955,7 +955,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange).toHaveBeenCalledWith(null);
expect(onOpenChange).toHaveBeenCalledTimes(2);
@@ -995,7 +995,7 @@ describe('ComboBox', function () {
});
it('closes menu when pressing Enter on an already selected item', function () {
- let {getByRole} = renderComboBox({selectedKey: '2'});
+ let {getByRole, queryByRole} = renderComboBox({selectedKey: '2'});
let combobox = getByRole('combobox');
act(() => combobox.focus());
@@ -1014,7 +1014,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onSelectionChange).not.toHaveBeenCalled();
});
@@ -1042,7 +1042,7 @@ describe('ComboBox', function () {
});
it('displays all items when opened via arrow keys', function () {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let combobox = getByRole('combobox');
let button = getByRole('button');
@@ -1065,7 +1065,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
act(() => {
fireEvent.keyDown(combobox, {key: 'ArrowUp', code: 38, charCode: 38});
fireEvent.keyUp(combobox, {key: 'ArrowDown', code: 38, charCode: 38});
@@ -1100,7 +1100,7 @@ describe('ComboBox', function () {
});
it('displays filtered list when input value is changed', function () {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let combobox = getByRole('combobox');
let button = getByRole('button');
act(() => {
@@ -1143,7 +1143,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
combobox = getByRole('combobox');
act(() => {
// Not sure why, test blows up for controlled items combobox when trying to fire arrow down here
@@ -1163,7 +1163,7 @@ describe('ComboBox', function () {
// separate test since controlled items case above blows up
it('controlled items combobox doesn\'t display all items when menu is opened', function () {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let combobox = getByRole('combobox');
let button = getByRole('button');
@@ -1191,7 +1191,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
});
});
});
@@ -1278,7 +1278,7 @@ describe('ComboBox', function () {
});
it('should not match any items if input is just a space', function () {
- let {getByRole} = renderComboBox();
+ let {getByRole, queryByRole} = renderComboBox();
let combobox = getByRole('combobox');
typeText(combobox, ' ');
@@ -1287,7 +1287,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
});
it('doesn\'t focus the first item in combobox menu if you completely clear your textfield and menuTrigger = focus', function () {
@@ -1373,7 +1373,7 @@ describe('ComboBox', function () {
});
it('should close the menu when no items match', function () {
- let {getByRole} = renderComboBox();
+ let {getByRole, queryByRole} = renderComboBox();
let combobox = getByRole('combobox');
typeText(combobox, 'O');
@@ -1394,7 +1394,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onOpenChange).toHaveBeenCalledTimes(2);
expect(onOpenChange).toHaveBeenLastCalledWith(false, undefined);
});
@@ -1504,7 +1504,7 @@ describe('ComboBox', function () {
});
it('closes and commits custom value', function () {
- let {getByRole} = render(
+ let {getByRole, queryByRole} = render(
- Bulbasaur
@@ -1531,7 +1531,7 @@ describe('ComboBox', function () {
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange).toHaveBeenCalledWith(null);
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
});
it('retains selected key on blur if input value matches', function () {
@@ -1564,7 +1564,7 @@ describe('ComboBox', function () {
});
it('clears the input field if value doesn\'t match a combobox option and no item is focused (menuTrigger=manual case)', function () {
- let {getByRole, getAllByRole} = render(
+ let {getByRole, queryByRole, getAllByRole} = render(
- Bulbasaur
@@ -1585,7 +1585,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(combobox.value).toBe('Charm');
act(() => {
@@ -1677,7 +1677,7 @@ describe('ComboBox', function () {
});
it('tab and shift tab move focus away from the combobox and select the focused item', function () {
- let {getByRole, getAllByRole} = render(
+ let {getByRole, queryByRole, getAllByRole} = render(
@@ -1722,7 +1722,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onInputChange).toHaveBeenLastCalledWith('Bulbasaur');
expect(onSelectionChange).toHaveBeenLastCalledWith('1');
expect(combobox.value).toBe('Bulbasaur');
@@ -1758,12 +1758,12 @@ describe('ComboBox', function () {
expect(onInputChange).toHaveBeenLastCalledWith('Bulbasaur');
expect(onSelectionChange).toHaveBeenLastCalledWith('1');
expect(combobox.value).toBe('Bulbasaur');
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(document.activeElement).toBe(shiftTabButton);
});
it('doesn\'t select the focused item on blur', function () {
- let {getByRole} = render(
+ let {getByRole, queryByRole} = render(
- Bulbasaur
@@ -1799,7 +1799,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onInputChange).not.toHaveBeenCalled();
expect(onSelectionChange).not.toHaveBeenCalled();
expect(combobox.value).toBe('');
@@ -2050,7 +2050,7 @@ describe('ComboBox', function () {
});
it('closes when selecting an item', function () {
- let {getByRole, rerender} = render();
+ let {getByRole, queryByRole, rerender} = render();
let combobox = getByRole('combobox');
let button = getByRole('button');
expect(combobox.value).toBe('T');
@@ -2076,11 +2076,11 @@ describe('ComboBox', function () {
});
expect(combobox.value).toBe('Three');
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
});
it('calls onOpenChange when clicking on a selected item if selectedKey is controlled but open state isn\'t ', function () {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let combobox = getByRole('combobox');
let button = getByRole('button');
@@ -2102,7 +2102,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onOpenChange).toHaveBeenCalledTimes(2);
expect(onOpenChange).toHaveBeenLastCalledWith(false, undefined);
});
@@ -2317,7 +2317,7 @@ describe('ComboBox', function () {
`('$Name ComboBox', ({Name, Component}) => {
describe('blur and commit flows', function () {
it('should reset the input text and close the menu on committing a previously selected option', () => {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let combobox = getByRole('combobox');
let button = getByRole('button');
act(() => {
@@ -2336,7 +2336,7 @@ describe('ComboBox', function () {
});
expect(combobox.value).toBe('One');
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
act(() => {
fireEvent.change(combobox, {target: {value: 'On'}});
@@ -2358,7 +2358,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(combobox.value).toBe('One');
if (!Name.includes('value') && !Name.includes('all')) {
@@ -2392,7 +2392,7 @@ describe('ComboBox', function () {
});
it('should update the input field with the selected item and close the menu on commit', () => {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let combobox = getByRole('combobox');
typeText(combobox, 'On');
act(() => jest.runAllTimers());
@@ -2411,7 +2411,7 @@ describe('ComboBox', function () {
});
expect(combobox.value).toBe('One');
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
if (!Name.includes('value') && !Name.includes('all')) {
// Check that onInputChange is firing appropriately for the comboboxes w/o user defined onInputChange handlers
@@ -2445,7 +2445,7 @@ describe('ComboBox', function () {
});
it('should reset the input value and close the menu on blur', () => {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let combobox = getByRole('combobox');
typeText(combobox, 'On');
act(() => jest.runAllTimers());
@@ -2460,7 +2460,7 @@ describe('ComboBox', function () {
});
expect(combobox.value).toBe('One');
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
act(() => {
fireEvent.change(combobox, {target: {value: 'On'}});
@@ -2475,7 +2475,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(combobox.value).toBe('One');
if (!Name.includes('value') && !Name.includes('all')) {
@@ -2498,7 +2498,7 @@ describe('ComboBox', function () {
});
it('should not open the menu on blur when an invalid input is entered', () => {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let combobox = getByRole('combobox');
typeText(combobox, 'On');
act(() => jest.runAllTimers());
@@ -2513,7 +2513,7 @@ describe('ComboBox', function () {
typeText(combobox, 'z');
act(() => jest.runAllTimers());
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(combobox.value).toBe('z');
act(() => {
@@ -2521,7 +2521,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(combobox.value).toBe('');
if (!Name.includes('value') && !Name.includes('all')) {
@@ -2546,7 +2546,7 @@ describe('ComboBox', function () {
describe('controlled items', function () {
it('should update the input value when items update and selectedKey textValue does\'t match', function () {
- let {getByRole, rerender} = render();
+ let {getByRole, queryByRole, rerender} = render();
let combobox = getByRole('combobox');
act(() => {
@@ -2571,7 +2571,7 @@ describe('ComboBox', function () {
expect(onSelectionChange).toBeCalledTimes(1);
expect(onSelectionChange).toHaveBeenLastCalledWith('1');
}
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
act(() => {
combobox.blur();
@@ -2600,7 +2600,7 @@ describe('ComboBox', function () {
expect(combobox.value).toBe('New Text');
}
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
if (!Name.includes('value') && !Name.includes('all')) {
expect(onInputChange).toBeCalledTimes(5);
@@ -2609,7 +2609,7 @@ describe('ComboBox', function () {
});
it('doesn\'t update the input value when items update but the combobox is focused', function () {
- let {getByRole, rerender} = render();
+ let {getByRole, queryByRole, rerender} = render();
let combobox = getByRole('combobox');
act(() => {
@@ -2634,7 +2634,7 @@ describe('ComboBox', function () {
expect(onSelectionChange).toBeCalledTimes(1);
expect(onSelectionChange).toHaveBeenLastCalledWith('1');
}
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
let newItems = [
{name: 'New Text', id: '1'},
@@ -2645,7 +2645,7 @@ describe('ComboBox', function () {
rerender();
expect(combobox.value).toBe('Aardvark');
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
});
});
});
@@ -2763,7 +2763,7 @@ describe('ComboBox', function () {
describe('uncontrolled combobox', function () {
it('should update both input value and selected item freely', function () {
- let {getByRole} = renderComboBox();
+ let {getByRole, queryByRole} = renderComboBox();
let combobox = getByRole('combobox');
let button = getByRole('button');
expect(combobox.value).toBe('');
@@ -2811,7 +2811,7 @@ describe('ComboBox', function () {
});
expect(combobox.value).toBe('One');
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onInputChange).toHaveBeenCalledTimes(5);
expect(onInputChange).toHaveBeenCalledWith('One');
expect(onSelectionChange).toHaveBeenCalledTimes(1);
@@ -2926,7 +2926,7 @@ describe('ComboBox', function () {
});
it('should close the menu if user clicks on a already selected item', function () {
- let {getByRole} = renderComboBox({defaultSelectedKey: '2'});
+ let {getByRole, queryByRole} = renderComboBox({defaultSelectedKey: '2'});
let combobox = getByRole('combobox');
let button = getByRole('button');
expect(combobox.value).toBe('Two');
@@ -2948,7 +2948,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onOpenChange).toHaveBeenCalledTimes(2);
expect(onOpenChange).toHaveBeenLastCalledWith(false, undefined);
@@ -2957,7 +2957,7 @@ describe('ComboBox', function () {
describe('combobox with sections', function () {
it('supports rendering sections', function () {
- let {getByRole, getByText} = renderSectionComboBox();
+ let {getByRole, queryByRole, getByText} = renderSectionComboBox();
let combobox = getByRole('combobox');
let button = getByRole('button');
@@ -3016,7 +3016,7 @@ describe('ComboBox', function () {
});
expect(combobox.value).toBe('Four');
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('listbox')).toBeNull();
expect(onInputChange).toHaveBeenCalledTimes(1);
expect(onInputChange).toHaveBeenCalledWith('Four');
expect(onSelectionChange).toHaveBeenCalledTimes(1);
@@ -3058,7 +3058,7 @@ describe('ComboBox', function () {
let groups = within(listbox).getAllByRole('group');
expect(groups[0]).not.toHaveAttribute('aria-selected');
- expect(() => within(listbox).getAllByRole('img')).toThrow();
+ expect(within(listbox).queryAllByRole('img')).toEqual([]);
});
});
@@ -3119,7 +3119,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => queryByRole('listbox')).toBeNull;
+ expect(queryByRole('listbox')).toBeNull();
// If allowCustomValue then the value shouldn't be reset (for Escape, only if there isn't a selected key)
if (Name.includes('allows custom value')) {
@@ -3168,7 +3168,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => queryByRole('listbox')).toBeNull;
+ expect(queryByRole('listbox')).toBeNull();
if (Name.includes('allows custom value') && Name.includes('Enter')) {
expect(combobox).toHaveAttribute('value', 'T');
} else {
@@ -3186,20 +3186,20 @@ describe('ComboBox', function () {
describe('loadingState', function () {
it('combobox should not render a loading circle if menu is not open', function () {
- let {getByRole, rerender} = render();
+ let {getByRole, queryByRole, rerender} = render();
act(() => {jest.advanceTimersByTime(500);});
// First time load will show progress bar so user can know that items are being fetched
expect(getByRole('progressbar')).toBeTruthy();
rerender();
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
});
it('combobox should render a loading circle if menu is not open but menuTrigger is "manual"', function () {
- let {getByRole, rerender} = render();
+ let {getByRole, queryByRole, rerender} = render();
let combobox = getByRole('combobox');
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
act(() => {jest.advanceTimersByTime(500);});
expect(() => within(combobox).getByRole('progressbar')).toBeTruthy();
@@ -3208,15 +3208,15 @@ describe('ComboBox', function () {
expect(() => within(combobox).getByRole('progressbar')).toBeTruthy();
rerender();
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
});
it('combobox should not render a loading circle until a delay of 500ms passes (loadingState: loading)', function () {
- let {getByRole} = renderComboBox({loadingState: 'loading'});
+ let {getByRole, queryByRole} = renderComboBox({loadingState: 'loading'});
let combobox = getByRole('combobox');
act(() => {jest.advanceTimersByTime(250);});
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
act(() => {jest.advanceTimersByTime(250);});
expect(() => within(combobox).getByRole('progressbar')).toBeTruthy();
@@ -3231,11 +3231,11 @@ describe('ComboBox', function () {
});
it('combobox should not render a loading circle until a delay of 500ms passes and the menu is open (loadingState: filtering)', function () {
- let {getByRole} = renderComboBox({loadingState: 'filtering'});
+ let {getByRole, queryByRole} = renderComboBox({loadingState: 'filtering'});
let combobox = getByRole('combobox');
act(() => {jest.advanceTimersByTime(500);});
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
let button = getByRole('button');
@@ -3247,10 +3247,10 @@ describe('ComboBox', function () {
});
it('combobox should hide the loading circle when loadingState changes to a non-loading state', function () {
- let {getByRole, rerender} = render();
+ let {getByRole, queryByRole, rerender} = render();
let combobox = getByRole('combobox');
let button = getByRole('button');
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
act(() => {
triggerPress(button);
@@ -3262,14 +3262,14 @@ describe('ComboBox', function () {
rerender();
let listbox = getByRole('listbox');
expect(listbox).toBeVisible();
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
});
it('combobox should hide the loading circle when if the menu closes', function () {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let combobox = getByRole('combobox');
let button = getByRole('button');
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
act(() => {
triggerPress(button);
@@ -3284,44 +3284,44 @@ describe('ComboBox', function () {
triggerPress(button);
jest.runAllTimers();
});
- expect(() => getByRole('progressbar')).toThrow();
- expect(() => getByRole('listbox')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
+ expect(queryByRole('listbox')).toBeNull();
});
it('combobox cancels the 500ms progress circle delay timer if the loading finishes first', function () {
- let {getByRole, rerender} = render();
- expect(() => getByRole('progressbar')).toThrow();
+ let {queryByRole, rerender} = render();
+ expect(queryByRole('progressbar')).toBeNull();
act(() => {jest.advanceTimersByTime(250);});
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
rerender();
act(() => {jest.advanceTimersByTime(250);});
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
});
it('combobox should not reset the 500ms progress circle delay timer when loadingState changes from loading to filtering', function () {
- let {getByRole, rerender} = render();
+ let {getByRole, queryByRole, rerender} = render();
let combobox = getByRole('combobox');
act(() => {jest.advanceTimersByTime(250);});
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
rerender();
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
act(() => {jest.advanceTimersByTime(250);});
expect(() => within(combobox).getByRole('progressbar')).toBeTruthy();
});
it('combobox should reset the 500ms progress circle delay timer when input text changes', function () {
- let {getByRole} = render();
+ let {getByRole, queryByRole} = render();
let combobox = getByRole('combobox');
act(() => {jest.advanceTimersByTime(250);});
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
typeText(combobox, 'O');
act(() => {jest.advanceTimersByTime(250);});
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
act(() => {jest.advanceTimersByTime(250);});
expect(() => within(combobox).getByRole('progressbar')).toBeTruthy();
@@ -3361,10 +3361,10 @@ describe('ComboBox', function () {
});
it('should render the loading swirl in the listbox when loadingState="loadingMore"', function () {
- let {getByRole} = renderComboBox({loadingState: 'loadingMore'});
+ let {getByRole, queryByRole} = renderComboBox({loadingState: 'loadingMore'});
let button = getByRole('button');
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
act(() => {
triggerPress(button);
@@ -3655,6 +3655,10 @@ describe('ComboBox', function () {
triggerPress(clearButton);
});
+ act(() => {
+ jest.runAllTimers();
+ });
+
expect(document.activeElement).toBe(trayInput);
expect(trayInput.value).toBe('');
});
@@ -4245,10 +4249,10 @@ describe('ComboBox', function () {
describe('isLoading', function () {
it('tray input should render a loading circle after a delay of 500ms if loadingState="filtering"', function () {
- let {getByRole, getByTestId, rerender} = render();
+ let {getByRole, queryByRole, getByTestId, rerender} = render();
let button = getByRole('button');
act(() => {jest.advanceTimersByTime(500);});
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
act(() => {
triggerPress(button);
@@ -4269,10 +4273,10 @@ describe('ComboBox', function () {
});
it('tray input should hide the loading circle if loadingState is no longer "filtering"', function () {
- let {getByRole, getByTestId, rerender} = render();
+ let {getByRole, queryByRole, getByTestId, rerender} = render();
let button = getByRole('button');
act(() => {jest.advanceTimersByTime(500);});
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
act(() => {
triggerPress(button);
@@ -4367,10 +4371,10 @@ describe('ComboBox', function () {
});
it('should render the loading swirl in the listbox when loadingState="loadingMore"', function () {
- let {getByRole, getByTestId} = renderComboBox({loadingState: 'loadingMore', validationState: 'invalid'});
+ let {getByRole, queryByRole, getByTestId} = renderComboBox({loadingState: 'loadingMore', validationState: 'invalid'});
let button = getByRole('button');
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
act(() => {
triggerPress(button);
@@ -4403,7 +4407,7 @@ describe('ComboBox', function () {
describe('mobile async loading', function () {
it('async combobox works with useAsyncList', async () => {
- let {getByRole, getByTestId} = render(
+ let {getByRole, queryByRole, getByTestId} = render(
@@ -4422,14 +4426,14 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(() => getByRole('progressbar')).toThrow();
+ expect(queryByRole('progressbar')).toBeNull();
let tray = getByTestId('tray');
expect(tray).toBeVisible();
expect(within(tray).queryByRole('progressbar')).toBeNull();
let listbox = getByRole('listbox');
- expect(within(listbox).queryByRole('progressbar')).toBeNull;
+ expect(within(listbox).queryByRole('progressbar')).toBeNull();
let items = within(listbox).getAllByRole('option');
expect(items).toHaveLength(3);
expect(items[0]).toHaveTextContent('Aardvark');
@@ -4444,7 +4448,7 @@ describe('ComboBox', function () {
jest.advanceTimersByTime(500);
let trayInputProgress = within(tray).getByRole('progressbar', {hidden: true});
expect(trayInputProgress).toBeTruthy();
- expect(within(listbox).queryByRole('progressbar')).toBeNull;
+ expect(within(listbox).queryByRole('progressbar')).toBeNull();
jest.runAllTimers();
});
@@ -4681,7 +4685,7 @@ describe('ComboBox', function () {
describe('hiding surrounding content', function () {
it('should hide elements outside the combobox with aria-hidden', function () {
- let {getByRole, getAllByRole} = render(
+ let {getByRole, queryAllByRole, getAllByRole} = render(
<>
@@ -4708,12 +4712,12 @@ describe('ComboBox', function () {
expect(outside[0]).toHaveAttribute('aria-hidden', 'true');
expect(outside[1]).toHaveAttribute('aria-hidden', 'true');
- expect(() => getAllByRole('checkbox')).toThrow();
+ expect(queryAllByRole('checkbox')).toEqual([]);
expect(getByRole('combobox')).toBeVisible();
});
it('should not traverse into a hidden container', function () {
- let {getByRole, getAllByRole} = render(
+ let {getByRole, queryAllByRole, getAllByRole} = render(
<>
@@ -4743,7 +4747,7 @@ describe('ComboBox', function () {
expect(outside[0]).not.toHaveAttribute('aria-hidden', 'true');
expect(outside[1]).toHaveAttribute('aria-hidden', 'true');
- expect(() => getAllByRole('checkbox')).toThrow();
+ expect(queryAllByRole('checkbox')).toEqual([]);
expect(getByRole('combobox')).toBeVisible();
});
@@ -4779,7 +4783,7 @@ describe('ComboBox', function () {
);
- let {getByRole, getAllByRole, rerender} = render();
+ let {getByRole, getAllByRole, queryAllByRole, rerender} = render();
let combobox = getByRole('combobox');
let button = getByRole('button');
@@ -4797,7 +4801,7 @@ describe('ComboBox', function () {
rerender();
- await waitFor(() => expect(() => getAllByRole('checkbox')).toThrow());
+ await waitFor(() => expect(queryAllByRole('checkbox')).toEqual([]));
expect(getByRole('combobox')).toBeVisible();
expect(getByRole('listbox')).toBeVisible();
@@ -4817,7 +4821,7 @@ describe('ComboBox', function () {
);
- let {getByRole, getAllByRole, getByTestId, rerender} = render();
+ let {getByRole, getAllByRole, queryAllByRole, getByTestId, rerender} = render();
let combobox = getByRole('combobox');
let button = getByRole('button');
@@ -4831,13 +4835,14 @@ describe('ComboBox', function () {
});
let listbox = getByRole('listbox');
+
expect(listbox).toBeVisible();
expect(button).toHaveAttribute('aria-hidden', 'true');
expect(outer).toHaveAttribute('aria-hidden', 'true');
rerender();
- await waitFor(() => expect(() => getAllByRole('checkbox')).toThrow());
+ await waitFor(() => expect(queryAllByRole('checkbox')).toEqual([]));
expect(getByRole('combobox')).toBeVisible();
expect(getByRole('listbox')).toBeVisible();
@@ -4860,7 +4865,7 @@ describe('ComboBox', function () {
);
- let {getByRole, getAllByRole, rerender} = render(
+ let {getByRole, queryAllByRole, rerender} = render(
);
@@ -4876,7 +4881,7 @@ describe('ComboBox', function () {
let listbox = getByRole('listbox');
let options = within(listbox).getAllByRole('option');
expect(options).toHaveLength(1);
- expect(() => getAllByRole('checkbox')).toThrow();
+ expect(queryAllByRole('checkbox')).toEqual([]);
rerender();
@@ -4886,7 +4891,7 @@ describe('ComboBox', function () {
options = within(listbox).getAllByRole('option');
expect(options).toHaveLength(2);
- expect(() => getAllByRole('checkbox')).toThrow();
+ expect(queryAllByRole('checkbox')).toEqual([]);
expect(getByRole('combobox')).toBeVisible();
expect(getByRole('listbox')).toBeVisible();
});
diff --git a/packages/@react-spectrum/datepicker/package.json b/packages/@react-spectrum/datepicker/package.json
index de6d31cc836..1c153913a26 100644
--- a/packages/@react-spectrum/datepicker/package.json
+++ b/packages/@react-spectrum/datepicker/package.json
@@ -33,6 +33,8 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
+ "@internationalized/date": "3.0.0-alpha.1",
+ "@internationalized/number": "^3.0.2",
"@react-aria/datepicker": "3.0.0-alpha.1",
"@react-aria/focus": "^3.1.0",
"@react-aria/i18n": "^3.1.0",
@@ -41,9 +43,14 @@
"@react-spectrum/button": "^3.1.0",
"@react-spectrum/calendar": "3.0.0-alpha.1",
"@react-spectrum/dialog": "^3.1.0",
+ "@react-spectrum/label": "^3.3.4",
+ "@react-spectrum/layout": "^3.2.1",
"@react-spectrum/utils": "^3.1.0",
+ "@react-spectrum/view": "^3.1.1",
"@react-stately/datepicker": "3.0.0-alpha.1",
+ "@react-stately/utils": "^3.2.1",
"@react-types/datepicker": "3.0.0-alpha.1",
+ "@react-types/shared": "^3.3.0",
"@spectrum-icons/ui": "^3.1.0",
"@spectrum-icons/workflow": "^3.1.0",
"date-fns": "^1.30.1"
@@ -54,7 +61,8 @@
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1",
- "@react-spectrum/provider": "^3.0.0-rc.1"
+ "@react-spectrum/provider": "^3.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-spectrum/datepicker/src/DateField.tsx b/packages/@react-spectrum/datepicker/src/DateField.tsx
new file mode 100644
index 00000000000..9aca860eab3
--- /dev/null
+++ b/packages/@react-spectrum/datepicker/src/DateField.tsx
@@ -0,0 +1,30 @@
+/*
+ * 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 {DatePickerField} from './DatePickerField';
+import {DateValue, SpectrumDatePickerProps} from '@react-types/datepicker';
+import {FocusScope} from '@react-aria/focus';
+import React from 'react';
+import {useProviderProps} from '@react-spectrum/provider';
+
+export function DateField(props: SpectrumDatePickerProps) {
+ props = useProviderProps(props);
+ let {
+ autoFocus
+ } = props;
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/@react-spectrum/datepicker/src/DatePicker.tsx b/packages/@react-spectrum/datepicker/src/DatePicker.tsx
index 592e188777c..ece6d9fd703 100644
--- a/packages/@react-spectrum/datepicker/src/DatePicker.tsx
+++ b/packages/@react-spectrum/datepicker/src/DatePicker.tsx
@@ -12,80 +12,113 @@
import {Calendar} from '@react-spectrum/calendar';
import CalendarIcon from '@spectrum-icons/workflow/Calendar';
-import {classNames, useStyleProps} from '@react-spectrum/utils';
+import {classNames} from '@react-spectrum/utils';
+import {Content} from '@react-spectrum/view';
import {DatePickerField} from './DatePickerField';
import datepickerStyles from './index.css';
+import {DateValue, SpectrumDatePickerProps} from '@react-types/datepicker';
import {Dialog, DialogTrigger} from '@react-spectrum/dialog';
+import {Field} from '@react-spectrum/label';
import {FieldButton} from '@react-spectrum/button';
-import {FocusRing, FocusScope} from '@react-aria/focus';
+import {FocusScope, useFocusRing} from '@react-aria/focus';
import {mergeProps} from '@react-aria/utils';
import React, {useRef} from 'react';
-import {SpectrumDatePickerProps} from '@react-types/datepicker';
import '@adobe/spectrum-css-temp/components/textfield/vars.css'; // HACK: must be included BEFORE inputgroup
import styles from '@adobe/spectrum-css-temp/components/inputgroup/vars.css';
+import {TimeField} from './TimeField';
import {useDatePicker} from '@react-aria/datepicker';
import {useDatePickerState} from '@react-stately/datepicker';
import {useHover} from '@react-aria/interactions';
import {useLocale} from '@react-aria/i18n';
import {useProviderProps} from '@react-spectrum/provider';
-export function DatePicker(props: SpectrumDatePickerProps) {
+export function DatePicker(props: SpectrumDatePickerProps) {
props = useProviderProps(props);
let {
autoFocus,
- formatOptions,
isQuiet,
isDisabled,
isReadOnly,
isRequired,
- placeholderDate,
- ...otherProps
+ placeholderValue
+ // showFormatHelpText,
} = props;
- let {styleProps} = useStyleProps(otherProps);
let {hoverProps, isHovered} = useHover({isDisabled});
- let state = useDatePickerState(props);
- let {comboboxProps, fieldProps, buttonProps, dialogProps} = useDatePicker(props, state);
- let {value, setValue, selectDate, isOpen, setOpen} = state;
let targetRef = useRef();
+ let state = useDatePickerState(props);
+ let {groupProps, labelProps, fieldProps, buttonProps, dialogProps} = useDatePicker(props, state, targetRef);
+ let {value, setValue, isOpen, setOpen} = state;
let {direction} = useLocale();
+ let {isFocused, isFocusVisible, focusProps} = useFocusRing({
+ within: true,
+ isTextInput: true,
+ autoFocus
+ });
+
let className = classNames(
styles,
'spectrum-InputGroup',
{
'spectrum-InputGroup--quiet': isQuiet,
- 'is-invalid': state.validationState === 'invalid',
+ 'spectrum-InputGroup--invalid': state.validationState === 'invalid',
+ 'is-disabled': isDisabled,
+ 'is-hovered': isHovered,
+ 'is-focused': isFocused,
+ 'focus-ring': isFocusVisible
+ }
+ );
+
+ let fieldClassName = classNames(
+ styles,
+ 'spectrum-InputGroup-input',
+ {
'is-disabled': isDisabled,
- 'is-hovered': isHovered
- },
- styleProps.className
+ 'is-invalid': state.validationState === 'invalid'
+ }
);
+
+ // TODO: format help text
+ // let formatter = useDateFormatter({dateStyle: 'short'});
+ // let segments = showFormatHelpText ? formatter.formatToParts(new Date()).map(s => {
+ // if (s.type === 'literal') {
+ // return s.value;
+ // }
+
+ // return s.type;
+ // }).join(' ') : '';
+
+ let v = state.value || props.placeholderValue;
+ let placeholder: DateValue = placeholderValue;
+ let timePlaceholder = placeholder && 'hour' in placeholder ? placeholder : null;
+ let timeMinValue = props.minValue && 'hour' in props.minValue ? props.minValue : null;
+ let timeMaxValue = props.maxValue && 'hour' in props.maxValue ? props.maxValue : null;
+ let timeGranularity = props.granularity === 'hour' || props.granularity === 'minute' || props.granularity === 'second' || props.granularity === 'millisecond' ? props.granularity : null;
+ let showTimeField = (v && 'hour' in v) || !!timeGranularity;
+
return (
-
+
+ granularity={props.granularity}
+ hourCycle={props.hourCycle}
+ inputClassName={fieldClassName}
+ UNSAFE_className={classNames(styles, 'spectrum-InputGroup-field')}
+ hideTimeZone={props.hideTimeZone} />
-
+
);
}
diff --git a/packages/@react-spectrum/datepicker/src/DatePickerField.tsx b/packages/@react-spectrum/datepicker/src/DatePickerField.tsx
index bfd85c04ed5..ec866cafcab 100644
--- a/packages/@react-spectrum/datepicker/src/DatePickerField.tsx
+++ b/packages/@react-spectrum/datepicker/src/DatePickerField.tsx
@@ -13,40 +13,53 @@
import Alert from '@spectrum-icons/ui/AlertMedium';
import Checkmark from '@spectrum-icons/ui/CheckmarkMedium';
import {classNames} from '@react-spectrum/utils';
+import {createCalendar} from '@internationalized/date';
import {DatePickerSegment} from './DatePickerSegment';
import datepickerStyles from './index.css';
-import inputgroupStyles from '@adobe/spectrum-css-temp/components/inputgroup/vars.css';
-import React from 'react';
-import {SpectrumDatePickerProps} from '@react-types/datepicker';
+import {DateValue, SpectrumDatePickerProps} from '@react-types/datepicker';
+import {Field} from '@react-spectrum/label';
+import {FocusRing} from '@react-aria/focus';
+import React, {useRef} from 'react';
import textfieldStyles from '@adobe/spectrum-css-temp/components/textfield/vars.css';
import {useDateField} from '@react-aria/datepicker';
import {useDatePickerFieldState} from '@react-stately/datepicker';
-import {useStyleProps} from '@react-spectrum/utils';
+import {useLocale} from '@react-aria/i18n';
-export function DatePickerField(props: SpectrumDatePickerProps) {
- let state = useDatePickerFieldState(props);
+interface DatePickerFieldProps extends SpectrumDatePickerProps {
+ inputClassName?: string,
+ hideValidationIcon?: boolean,
+ maxGranularity?: SpectrumDatePickerProps['granularity']
+}
+
+export function DatePickerField(props: DatePickerFieldProps) {
let {
isDisabled,
isReadOnly,
isRequired,
isQuiet,
- validationState,
- ...otherProps
+ inputClassName,
+ hideValidationIcon
} = props;
- let {styleProps} = useStyleProps(otherProps);
- let {fieldProps, segmentProps} = useDateField(props);
+ let ref = useRef();
+ let {locale} = useLocale();
+ let state = useDatePickerFieldState({
+ ...props,
+ locale,
+ createCalendar
+ });
+
+ let {labelProps, fieldProps} = useDateField(props, state, ref);
- let isInvalid = validationState === 'invalid';
+ let isInvalid = state.validationState === 'invalid';
let textfieldClass = classNames(
textfieldStyles,
'spectrum-Textfield',
{
- 'is-invalid': isInvalid,
- 'is-valid': validationState === 'valid',
+ 'spectrum-Textfield--invalid': isInvalid && !hideValidationIcon,
+ 'spectrum-Textfield--valid': state.validationState === 'valid' && !hideValidationIcon,
'spectrum-Textfield--quiet': isQuiet
},
- classNames(datepickerStyles, 'react-spectrum-Datepicker-field'),
- styleProps.className
+ classNames(datepickerStyles, 'react-spectrum-Datepicker-field')
);
let inputClass = classNames(
@@ -56,48 +69,42 @@ export function DatePickerField(props: SpectrumDatePickerProps) {
'is-disabled': isDisabled,
'is-invalid': isInvalid
},
- classNames(
- inputgroupStyles,
- 'spectrum-InputGroup-input',
- {
- 'is-disabled': isDisabled,
- 'is-invalid': isInvalid
- }
- ),
- classNames(datepickerStyles, 'react-spectrum-Datepicker-input')
+ classNames(datepickerStyles, 'react-spectrum-Datepicker-input'),
+ inputClassName
);
let iconClass = classNames(
textfieldStyles,
- 'spectrum-Textfield-validationIcon',
- {
- 'is-invalid': isInvalid,
- 'is-valid': validationState === 'valid'
- }
+ 'spectrum-Textfield-validationIcon'
);
let validationIcon = null;
- if (validationState === 'invalid') {
- validationIcon = ;
- } else if (validationState === 'valid') {
- validationIcon = ;
+ if (!hideValidationIcon) {
+ if (state.validationState === 'invalid') {
+ validationIcon = ;
+ } else if (state.validationState === 'valid') {
+ validationIcon = ;
+ }
}
return (
-
-
- {state.segments.map((segment, i) =>
- ( )
- )}
+
+
+
+
+ {state.segments.map((segment, i) =>
+ ()
+ )}
+
+
+ {validationIcon}
- {validationIcon}
-
+
);
}
diff --git a/packages/@react-spectrum/datepicker/src/DatePickerSegment.tsx b/packages/@react-spectrum/datepicker/src/DatePickerSegment.tsx
index 64051b119a2..d59bf39812d 100644
--- a/packages/@react-spectrum/datepicker/src/DatePickerSegment.tsx
+++ b/packages/@react-spectrum/datepicker/src/DatePickerSegment.tsx
@@ -11,14 +11,17 @@
*/
import {classNames} from '@react-spectrum/utils';
-import {DatePickerBase} from '@react-types/datepicker';
+import {DatePickerBase, DateValue} from '@react-types/datepicker';
import {DatePickerFieldState, DateSegment} from '@react-stately/datepicker';
-import React from 'react';
+import {NumberParser} from '@internationalized/number';
+import React, {useMemo, useRef} from 'react';
import styles from './index.css';
import {useDateSegment} from '@react-aria/datepicker';
import {useFocusManager} from '@react-aria/focus';
+import {useLocale} from '@react-aria/i18n';
+import {usePress} from '@react-aria/interactions';
-interface DatePickerSegmentProps extends DatePickerBase {
+interface DatePickerSegmentProps extends DatePickerBase {
segment: DateSegment,
state: DatePickerFieldState
}
@@ -37,7 +40,6 @@ export function DatePickerSegment({segment, state, ...otherProps}: DatePickerSeg
// These segments cannot be directly edited by the user.
case 'weekday':
case 'timeZoneName':
- case 'era':
return ;
// Editable segment
@@ -48,19 +50,22 @@ export function DatePickerSegment({segment, state, ...otherProps}: DatePickerSeg
function LiteralSegment({segment, isPlaceholder}: LiteralSegmentProps) {
let focusManager = useFocusManager();
- let onMouseDown = (e) => {
- let node = focusManager.focusNext({from: e.target});
- if (node) {
- e.preventDefault();
- e.stopPropagation();
+ let {pressProps} = usePress({
+ onPressStart: (e) => {
+ if (e.pointerType === 'mouse') {
+ let res = focusManager.focusNext({from: e.target as HTMLElement});
+ if (!res) {
+ focusManager.focusPrevious({from: e.target as HTMLElement});
+ }
+ }
}
- };
+ });
return (
-
{segment.text}
@@ -68,10 +73,18 @@ function LiteralSegment({segment, isPlaceholder}: LiteralSegmentProps) {
}
function EditableSegment({segment, state, ...otherProps}: DatePickerSegmentProps) {
- let {segmentProps} = useDateSegment(otherProps, segment, state);
+ let ref = useRef();
+ let {segmentProps} = useDateSegment(otherProps, segment, state, ref);
+ let {locale} = useLocale();
+ let parser = useMemo(() => new NumberParser(locale), [locale]);
+ let isNumeric = useMemo(() => parser.isValidPartialNumber(segment.text), [segment.text, parser]);
return (
{segment.text}
diff --git a/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx b/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx
index d6177127693..05040729c02 100644
--- a/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx
+++ b/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx
@@ -12,23 +12,27 @@
import CalendarIcon from '@spectrum-icons/workflow/Calendar';
import {classNames, useStyleProps} from '@react-spectrum/utils';
+import {Content} from '@react-spectrum/view';
import {DatePickerField} from './DatePickerField';
import datepickerStyles from './index.css';
+import {DateValue, SpectrumDateRangePickerProps} from '@react-types/datepicker';
import {Dialog, DialogTrigger} from '@react-spectrum/dialog';
+import {Field} from '@react-spectrum/label';
import {FieldButton} from '@react-spectrum/button';
-import {FocusRing, FocusScope, useFocusManager} from '@react-aria/focus';
+import {Flex} from '@react-spectrum/layout';
+import {FocusScope, useFocusManager, useFocusRing} from '@react-aria/focus';
import {mergeProps} from '@react-aria/utils';
import {RangeCalendar} from '@react-spectrum/calendar';
import React, {useRef} from 'react';
-import {SpectrumDateRangePickerProps} from '@react-types/datepicker';
import styles from '@adobe/spectrum-css-temp/components/inputgroup/vars.css';
+import {TimeField} from './TimeField';
import {useDateRangePicker} from '@react-aria/datepicker';
import {useDateRangePickerState} from '@react-stately/datepicker';
-import {useHover} from '@react-aria/interactions';
+import {useHover, usePress} from '@react-aria/interactions';
import {useLocale} from '@react-aria/i18n';
import {useProviderProps} from '@react-spectrum/provider';
-export function DateRangePicker(props: SpectrumDateRangePickerProps) {
+export function DateRangePicker (props: SpectrumDateRangePickerProps) {
props = useProviderProps(props);
let {
isQuiet,
@@ -36,67 +40,94 @@ export function DateRangePicker(props: SpectrumDateRangePickerProps) {
isReadOnly,
isRequired,
autoFocus,
- formatOptions,
- placeholderDate,
+ placeholderValue,
...otherProps
} = props;
let {styleProps} = useStyleProps(otherProps);
let {hoverProps, isHovered} = useHover({isDisabled});
- let state = useDateRangePickerState(props);
- let {comboboxProps, buttonProps, dialogProps, startFieldProps, endFieldProps} = useDateRangePicker(props, state);
- let {value, setDate, selectDateRange, isOpen, setOpen} = state;
let targetRef = useRef();
+ let state = useDateRangePickerState(props);
+ let {labelProps, groupProps, buttonProps, dialogProps, startFieldProps, endFieldProps} = useDateRangePicker(props, state, targetRef);
+ let {value, isOpen, setOpen} = state;
let {direction} = useLocale();
+ let {isFocused, isFocusVisible, focusProps} = useFocusRing({
+ within: true,
+ isTextInput: true,
+ autoFocus
+ });
+
+
let className = classNames(
styles,
'spectrum-InputGroup',
'spectrum-Datepicker--range',
{
'spectrum-InputGroup--quiet': isQuiet,
- 'is-invalid': state.validationState === 'invalid',
+ 'spectrum-InputGroup--invalid': state.validationState === 'invalid',
'is-disabled': isDisabled,
- 'is-hovered': isHovered
+ 'is-hovered': isHovered,
+ 'is-focused': isFocused,
+ 'focus-ring': isFocusVisible
},
styleProps.className
);
+ let fieldClassName = classNames(
+ styles,
+ 'spectrum-InputGroup-input',
+ {
+ 'is-disabled': isDisabled,
+ 'is-invalid': state.validationState === 'invalid'
+ }
+ );
+
+ let v = state.value?.start || props.placeholderValue;
+ let placeholder: DateValue = placeholderValue;
+ let timePlaceholder = placeholder && 'hour' in placeholder ? placeholder : null;
+ let timeMinValue = props.minValue && 'hour' in props.minValue ? props.minValue : null;
+ let timeMaxValue = props.maxValue && 'hour' in props.maxValue ? props.maxValue : null;
+ let timeGranularity = props.granularity === 'hour' || props.granularity === 'minute' || props.granularity === 'second' || props.granularity === 'millisecond' ? props.granularity : null;
+ let showTimeField = (v && 'hour' in v) || !!timeGranularity;
+
return (
-
+
setDate('start', start)}
- UNSAFE_className={classNames(styles, 'spectrum-Datepicker-startField')} />
+ defaultValue={null}
+ onChange={start => state.setValue({...value, start})}
+ granularity={props.granularity}
+ hourCycle={props.hourCycle}
+ UNSAFE_className={classNames(datepickerStyles, 'react-spectrum-Datepicker-startField')}
+ inputClassName={fieldClassName} />
setDate('end', end)}
+ defaultValue={null}
+ onChange={end => state.setValue({...value, end})}
+ granularity={props.granularity}
+ hourCycle={props.hourCycle}
UNSAFE_className={classNames(
styles,
'spectrum-Datepicker-endField',
@@ -104,7 +135,8 @@ export function DateRangePicker(props: SpectrumDateRangePickerProps) {
datepickerStyles,
'react-spectrum-Datepicker-endField'
)
- )} />
+ )}
+ inputClassName={fieldClassName} />
-
+
);
}
function DateRangeDash() {
let focusManager = useFocusManager();
- let onMouseDown = (e) => {
- e.preventDefault();
- focusManager.focusNext({from: e.target});
- };
+ let {pressProps} = usePress({
+ onPressStart: (e) => {
+ if (e.pointerType === 'mouse') {
+ focusManager.focusNext({from: e.target as HTMLElement});
+ }
+ }
+ });
return (
+ {...pressProps} />
);
}
diff --git a/packages/@react-spectrum/datepicker/src/TimeField.tsx b/packages/@react-spectrum/datepicker/src/TimeField.tsx
new file mode 100644
index 00000000000..3a3b32beace
--- /dev/null
+++ b/packages/@react-spectrum/datepicker/src/TimeField.tsx
@@ -0,0 +1,73 @@
+/*
+ * 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 {DatePickerField} from './DatePickerField';
+import {DateValue, SpectrumTimePickerProps, TimeValue} from '@react-types/datepicker';
+import {FocusScope} from '@react-aria/focus';
+import {getLocalTimeZone, Time, toCalendarDateTime, today, toTime} from '@internationalized/date';
+import React, {useMemo} from 'react';
+import {useControlledState} from '@react-stately/utils';
+import {useProviderProps} from '@react-spectrum/provider';
+
+export function TimeField(props: SpectrumTimePickerProps) {
+ props = useProviderProps(props);
+ let {
+ autoFocus,
+ placeholderValue = new Time(12),
+ minValue,
+ maxValue
+ } = props;
+
+ let [value, setValue] = useControlledState(
+ props.value,
+ props.defaultValue,
+ props.onChange
+ );
+
+ let v = value || placeholderValue;
+ let day = v && 'day' in v ? v : undefined;
+ let placeholderDate = useMemo(() => convertValue(placeholderValue), [placeholderValue]);
+ let minDate = useMemo(() => convertValue(minValue, day), [minValue, day]);
+ let maxDate = useMemo(() => convertValue(maxValue, day), [maxValue, day]);
+
+ let dateTime = useMemo(() => value == null ? null : convertValue(value), [value]);
+ let onChange = newValue => {
+ setValue(v && 'day' in v ? newValue : toTime(newValue));
+ };
+
+ return (
+
+
+
+ );
+}
+
+function convertValue(value: TimeValue, date: DateValue = today(getLocalTimeZone())) {
+ if (!value) {
+ return null;
+ }
+
+ if ('day' in value) {
+ return value;
+ }
+
+ return toCalendarDateTime(date, value);
+}
diff --git a/packages/@react-spectrum/datepicker/src/index.css b/packages/@react-spectrum/datepicker/src/index.css
index 0e4041add74..3200a68bd25 100644
--- a/packages/@react-spectrum/datepicker/src/index.css
+++ b/packages/@react-spectrum/datepicker/src/index.css
@@ -10,30 +10,47 @@
* governing permissions and limitations under the License.
*/
-.react-spectrum-Datepicker-field.react-spectrum-Datepicker-field {
- /* needs to override the style from Textfield */
+.react-spectrum-Datepicker-startField.react-spectrum-Datepicker-startField {
width: auto;
}
.react-spectrum-Datepicker-endField {
+ width: auto;
flex: 1;
}
+.react-spectrum-Datepicker-field ~ .react-spectrum-Datepicker-endField > .react-spectrum-Datepicker-input {
+ border-inline-start-width: 0;
+ border-start-start-radius: 0;
+ border-end-start-radius: 0;
+}
+
+.react-spectrum-Datepicker-field {
+ /* overflow: hidden; */
+}
+
.react-spectrum-Datepicker-input {
display: flex;
align-items: center;
- /* Override default InputGroup styling in spectrum-css which assumes the input is an actual element */
- width: auto !important;
- min-width: 0 !important;
+ /* width: auto; */
+ overflow-x: auto;
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* Internet Explorer 10+ */
+
+ &::-webkit-scrollbar { /* WebKit */
+ width: 0;
+ height: 0;
+ }
}
.react-spectrum-Datepicker-literal {
white-space: pre;
user-select: none;
+ color: var(--spectrum-textfield-text-color);
}
.react-spectrum-Datepicker-literal.is-placeholder {
- color: var(--spectrum-textfield-placeholder-text-color, var(--spectrum-global-color-gray-600));
+ color: var(--spectrum-global-color-gray-700);
}
.react-spectrum-DatePicker-cell {
@@ -41,12 +58,17 @@
background: none;
padding: 0 2px;
border-radius: 2px;
- user-select: none;
- -webkit-user-select: none;
+ font-variant-numeric: tabular-nums;
+ text-align: end;
+ box-sizing: content-box;
+ white-space: nowrap;
+ color: var(--spectrum-textfield-text-color);
}
.react-spectrum-DatePicker-cell:focus {
- background: rgba(38, 128, 235, 0.25);
+ background-color: var(--spectrum-global-color-static-blue);
+ color: white;
+ caret-color: transparent;
outline: none;
}
@@ -54,6 +76,18 @@
color: var(--spectrum-textfield-placeholder-text-color, var(--spectrum-global-color-gray-600));
}
+.react-spectrum-DatePicker-cell.is-placeholder ~ .react-spectrum-Datepicker-literal {
+ color: var(--spectrum-textfield-placeholder-text-color, var(--spectrum-global-color-gray-600));
+}
+
+.react-spectrum-DatePicker-cell.is-placeholder:focus {
+ color: var(--spectrum-global-color-static-gray-400);
+}
+
+.react-spectrum-Datepicker-dialog {
+ width: calc(var(--spectrum-dialog-padding) * 2 +(var(--spectrum-calendar-day-width, var(--spectrum-global-dimension-size-400)) + var(--spectrum-calendar-day-padding, 4px) * 2) * 7);
+}
+
/* When displayed in a tray (aria-modal), center the dialog rather than using fixed padding. */
.react-spectrum-Datepicker-dialog[aria-modal] {
margin: 0 auto;
diff --git a/packages/@react-spectrum/datepicker/src/index.ts b/packages/@react-spectrum/datepicker/src/index.ts
index ec737723c9a..55bdd660e05 100644
--- a/packages/@react-spectrum/datepicker/src/index.ts
+++ b/packages/@react-spectrum/datepicker/src/index.ts
@@ -14,3 +14,5 @@
export * from './DatePicker';
export * from './DateRangePicker';
+export * from './TimeField';
+export * from './DateField';
diff --git a/packages/@react-spectrum/datepicker/stories/DateField.stories.tsx b/packages/@react-spectrum/datepicker/stories/DateField.stories.tsx
new file mode 100644
index 00000000000..bc3a52c80e7
--- /dev/null
+++ b/packages/@react-spectrum/datepicker/stories/DateField.stories.tsx
@@ -0,0 +1,253 @@
+/*
+ * 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 {action} from '@storybook/addon-actions';
+import {CalendarDate, CalendarDateTime, parseAbsolute, parseAbsoluteToLocal, parseDate, parseDateTime, parseZonedDateTime, toZoned} from '@internationalized/date';
+import {DateField} from '../';
+import {Flex} from '@react-spectrum/layout';
+import {Item, Picker, Section} from '@react-spectrum/picker';
+import {Provider} from '@react-spectrum/provider';
+import React from 'react';
+import {storiesOf} from '@storybook/react';
+import {useLocale} from '@react-aria/i18n';
+
+const BlockDecorator = storyFn => {storyFn()} ;
+
+storiesOf('Date and Time/DateField', module)
+ .addDecorator(BlockDecorator)
+ .add(
+ 'default',
+ () => render()
+ )
+ .add(
+ 'defaultValue',
+ () => render({defaultValue: parseDate('2020-02-03')})
+ )
+ .add(
+ 'controlled value',
+ () => render({value: new CalendarDate(2020, 2, 3)})
+ )
+ .add(
+ 'defaultValue, zoned',
+ () => render({defaultValue: toZoned(parseDate('2020-02-03'), 'America/Los_Angeles')})
+ )
+ .add(
+ 'granularity: minute',
+ () => render({granularity: 'minute'})
+ )
+ .add(
+ 'granularity: second',
+ () => render({granularity: 'second'})
+ )
+ .add(
+ 'hourCycle: 12',
+ () => render({granularity: 'minute', hourCycle: 12})
+ )
+ .add(
+ 'hourCycle: 24',
+ () => render({granularity: 'minute', hourCycle: 24})
+ )
+ .add(
+ 'granularity: minute, defaultValue',
+ () => render({granularity: 'minute', defaultValue: parseDateTime('2021-03-14T08:45')})
+ )
+ .add(
+ 'granularity: minute, defaultValue, zoned',
+ () => render({granularity: 'minute', defaultValue: parseZonedDateTime('2021-11-07T00:45-07:00[America/Los_Angeles]')})
+ )
+ .add('granularity: minute, defaultValue, zoned, absolute',
+ () => render({granularity: 'minute', defaultValue: parseAbsoluteToLocal('2021-11-07T07:45:00Z')})
+ )
+ .add('granularity: minute, defaultValue, zoned, absolute, timeZone',
+ () => render({granularity: 'minute', defaultValue: parseAbsolute('2021-11-07T07:45:00Z', 'America/New_York')})
+ )
+ .add(
+ 'defaultValue with time, granularity: day',
+ () => render({granularity: 'day', defaultValue: parseDateTime('2021-03-14T08:45')})
+ )
+ .add(
+ 'hideTimeZone',
+ () => render({granularity: 'minute', defaultValue: parseZonedDateTime('2021-11-07T00:45-07:00[America/Los_Angeles]'), hideTimeZone: true})
+ )
+ .add(
+ 'isDisabled',
+ () => render({isDisabled: true, value: new CalendarDate(2020, 2, 3)})
+ )
+ .add(
+ 'isQuiet, isDisabled',
+ () => render({isQuiet: true, isDisabled: true, value: new CalendarDate(2020, 2, 3)})
+ )
+ .add(
+ 'isReadOnly',
+ () => render({isReadOnly: true, value: new CalendarDate(2020, 2, 3)})
+ )
+ .add(
+ 'autoFocus',
+ () => render({autoFocus: true})
+ )
+ .add(
+ 'validationState: invalid',
+ () => render({validationState: 'invalid', value: new CalendarDate(2020, 2, 3)})
+ )
+ .add(
+ 'validationState: valid',
+ () => render({validationState: 'valid', value: new CalendarDate(2020, 2, 3)})
+ )
+ .add(
+ 'minValue: 2010/1/1, maxValue: 2020/1/1',
+ () => render({minValue: new CalendarDate(2010, 0, 1), maxValue: new CalendarDate(2020, 0, 1)})
+ )
+ .add(
+ 'placeholderValue: 1980/1/1',
+ () => render({placeholderValue: new CalendarDate(1980, 1, 1)})
+ )
+ .add(
+ 'placeholderValue: 1980/1/1 8 AM',
+ () => render({placeholderValue: new CalendarDateTime(1980, 1, 1, 8)})
+ )
+ .add(
+ 'placeholderValue: 1980/1/1, zoned',
+ () => render({placeholderValue: toZoned(new CalendarDate(1980, 1, 1), 'America/Los_Angeles')})
+ );
+
+storiesOf('Date and Time/DateField/styling', module)
+ .addDecorator(BlockDecorator)
+ .add(
+ 'isQuiet',
+ () => render({isQuiet: true})
+ )
+ .add(
+ 'labelPosition: side',
+ () => render({labelPosition: 'side'})
+ )
+ .add(
+ 'labelAlign: end',
+ () => render({labelPosition: 'top', labelAlign: 'end'})
+ )
+ .add(
+ 'required',
+ () => render({isRequired: true})
+ )
+ .add(
+ 'required with label',
+ () => render({isRequired: true, necessityIndicator: 'label'})
+ )
+ .add(
+ 'optional',
+ () => render({necessityIndicator: 'label'})
+ )
+ .add(
+ 'no visible label',
+ () => render({'aria-label': 'Date', label: null})
+ )
+ .add(
+ 'quiet no visible label',
+ () => render({isQuiet: true, 'aria-label': 'Date', label: null})
+ )
+ .add(
+ 'custom width',
+ () => render({width: 'size-3000'})
+ )
+ .add(
+ 'quiet custom width',
+ () => render({isQuiet: true, width: 'size-3000'})
+ )
+ .add(
+ 'custom width no visible label',
+ () => render({width: 'size-3000', label: null, 'aria-label': 'Date'})
+ )
+ .add(
+ 'custom width, labelPosition=side',
+ () => render({width: 'size-3000', labelPosition: 'side'})
+ );
+
+function render(props = {}) {
+ return (
+
+ );
+}
+
+// https://github.com/unicode-org/cldr/blob/22af90ae3bb04263f651323ce3d9a71747a75ffb/common/supplemental/supplementalData.xml#L4649-L4664
+const preferences = [
+ {locale: '', label: 'Default', ordering: 'gregory'},
+ {label: 'Arabic (Algeria)', locale: 'ar-DZ', territories: 'DJ DZ EH ER IQ JO KM LB LY MA MR OM PS SD SY TD TN YE', ordering: 'gregory islamic islamic-civil islamic-tbla'},
+ {label: 'Arabic (United Arab Emirates)', locale: 'ar-AE', territories: 'AE BH KW QA', ordering: 'gregory islamic-umalqura islamic islamic-civil islamic-tbla'},
+ {label: 'Arabic (Egypt)', locale: 'AR-EG', territories: 'EG', ordering: 'gregory coptic islamic islamic-civil islamic-tbla'},
+ {label: 'Arabic (Saudi Arabia)', locale: 'ar-SA', territories: 'SA', ordering: 'islamic-umalqura gregory islamic islamic-rgsa'},
+ {label: 'Farsi (Afghanistan)', locale: 'fa-AF', territories: 'AF IR', ordering: 'persian gregory islamic islamic-civil islamic-tbla'},
+ // {territories: 'CN CX HK MO SG', ordering: 'gregory chinese'},
+ {label: 'Amharic (Ethiopia)', locale: 'am-ET', territories: 'ET', ordering: 'gregory ethiopic ethioaa'},
+ {label: 'Hebrew (Israel)', locale: 'he-IL', territories: 'IL', ordering: 'gregory hebrew islamic islamic-civil islamic-tbla'},
+ {label: 'Hindi (India)', locale: 'hi-IN', territories: 'IN', ordering: 'gregory indian'},
+ // {label: 'Marathi (India)', locale: 'mr-IN', territories: 'IN', ordering: 'gregory indian'},
+ {label: 'Bengali (India)', locale: 'bn-IN', territories: 'IN', ordering: 'gregory indian'},
+ {label: 'Japanese (Japan)', locale: 'ja-JP', territories: 'JP', ordering: 'gregory japanese'},
+ // {territories: 'KR', ordering: 'gregory dangi'},
+ {label: 'Thai (Thailand)', locale: 'th-TH', territories: 'TH', ordering: 'buddhist gregory'},
+ {label: 'Chinese (Taiwan)', locale: 'zh-TW', territories: 'TW', ordering: 'gregory roc chinese'}
+];
+
+const calendars = [
+ {key: 'gregory', name: 'Gregorian'},
+ {key: 'japanese', name: 'Japanese'},
+ {key: 'buddhist', name: 'Buddhist'},
+ {key: 'roc', name: 'Taiwan'},
+ {key: 'persian', name: 'Persian'},
+ {key: 'indian', name: 'Indian'},
+ {key: 'islamic-umalqura', name: 'Islamic (Umm al-Qura)'},
+ {key: 'islamic-civil', name: 'Islamic Civil'},
+ {key: 'islamic-tbla', name: 'Islamic Tabular'},
+ {key: 'hebrew', name: 'Hebrew'},
+ {key: 'coptic', name: 'Coptic'},
+ {key: 'ethiopic', name: 'Ethiopic'},
+ {key: 'ethioaa', name: 'Ethiopic (Amete Alem)'}
+];
+
+function Example(props) {
+ let [locale, setLocale] = React.useState('');
+ let [calendar, setCalendar] = React.useState(calendars[0].key);
+ let {locale: defaultLocale} = useLocale();
+
+ let pref = preferences.find(p => p.locale === locale);
+ let preferredCalendars = React.useMemo(() => pref ? pref.ordering.split(' ').map(p => calendars.find(c => c.key === p)).filter(Boolean) : [calendars[0]], [pref]);
+ let otherCalendars = React.useMemo(() => calendars.filter(c => !preferredCalendars.some(p => p.key === c.key)), [preferredCalendars]);
+
+ let updateLocale = locale => {
+ setLocale(locale);
+ let pref = preferences.find(p => p.locale === locale);
+ setCalendar(pref.ordering.split(' ')[0]);
+ };
+
+ return (
+
+
+
+ {item => - {item.label}
}
+
+
+
+ {item => - {item.name}
}
+
+
+ {item => - {item.name}
}
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx
index b1c5f6a8f18..4ba179dea16 100644
--- a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx
+++ b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx
@@ -11,58 +11,84 @@
*/
import {action} from '@storybook/addon-actions';
+import {CalendarDate, CalendarDateTime, parseAbsolute, parseAbsoluteToLocal, parseDate, parseDateTime, parseZonedDateTime, toZoned} from '@internationalized/date';
import {DatePicker} from '../';
+import {Flex} from '@react-spectrum/layout';
+import {Item, Picker, Section} from '@react-spectrum/picker';
+import {Provider} from '@react-spectrum/provider';
import React from 'react';
import {storiesOf} from '@storybook/react';
+import {useLocale} from '@react-aria/i18n';
const BlockDecorator = storyFn => {storyFn()} ;
-storiesOf('DatePicker', module)
+storiesOf('Date and Time/DatePicker', module)
.addDecorator(BlockDecorator)
.add(
'default',
() => render()
)
- .add(
- 'isQuiet',
- () => render({isQuiet: true})
- )
.add(
'defaultValue',
- () => render({defaultValue: new Date(2020, 2, 3)})
+ () => render({defaultValue: parseDate('2020-02-03')})
)
.add(
'controlled value',
- () => render({value: new Date(2020, 2, 3)})
+ () => render({value: new CalendarDate(2020, 2, 3)})
+ )
+ .add(
+ 'defaultValue, zoned',
+ () => render({defaultValue: toZoned(parseDate('2020-02-03'), 'America/Los_Angeles')})
+ )
+ .add(
+ 'granularity: minute',
+ () => render({granularity: 'minute'})
+ )
+ .add(
+ 'granularity: second',
+ () => render({granularity: 'second'})
+ )
+ .add(
+ 'hourCycle: 12',
+ () => render({granularity: 'minute', hourCycle: 12})
+ )
+ .add(
+ 'hourCycle: 24',
+ () => render({granularity: 'minute', hourCycle: 24})
+ )
+ .add(
+ 'granularity: minute, defaultValue',
+ () => render({granularity: 'minute', defaultValue: parseDateTime('2021-03-14T08:45')})
+ )
+ .add(
+ 'granularity: minute, defaultValue, zoned',
+ () => render({granularity: 'minute', defaultValue: parseZonedDateTime('2021-11-07T00:45-07:00[America/Los_Angeles]')})
+ )
+ .add('granularity: minute, defaultValue, zoned, absolute',
+ () => render({granularity: 'minute', defaultValue: parseAbsoluteToLocal('2021-11-07T07:45:00Z')})
+ )
+ .add('granularity: minute, defaultValue, zoned, absolute, timeZone',
+ () => render({granularity: 'minute', defaultValue: parseAbsolute('2021-11-07T07:45:00Z', 'America/New_York')})
+ )
+ .add(
+ 'defaultValue with time, granularity: day',
+ () => render({granularity: 'day', defaultValue: parseDateTime('2021-03-14T08:45')})
)
.add(
- 'custom date format',
- () => render({
- formatOptions: {
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- hour: 'numeric',
- minute: 'numeric',
- second: 'numeric'
- }
- })
+ 'hideTimeZone',
+ () => render({granularity: 'minute', defaultValue: parseZonedDateTime('2021-11-07T00:45-07:00[America/Los_Angeles]'), hideTimeZone: true})
)
.add(
'isDisabled',
- () => render({isDisabled: true, value: new Date(2020, 2, 3)})
+ () => render({isDisabled: true, value: new CalendarDate(2020, 2, 3)})
)
.add(
'isQuiet, isDisabled',
- () => render({isQuiet: true, isDisabled: true, value: new Date(2020, 2, 3)})
+ () => render({isQuiet: true, isDisabled: true, value: new CalendarDate(2020, 2, 3)})
)
.add(
'isReadOnly',
- () => render({isReadOnly: true, value: new Date(2020, 2, 3)})
- )
- .add(
- 'isRequired',
- () => render({isRequired: true})
+ () => render({isReadOnly: true, value: new CalendarDate(2020, 2, 3)})
)
.add(
'autoFocus',
@@ -70,25 +96,158 @@ storiesOf('DatePicker', module)
)
.add(
'validationState: invalid',
- () => render({validationState: 'invalid', value: new Date(2020, 2, 3)})
+ () => render({validationState: 'invalid', value: new CalendarDate(2020, 2, 3)})
)
.add(
'validationState: valid',
- () => render({validationState: 'valid', value: new Date(2020, 2, 3)})
+ () => render({validationState: 'valid', value: new CalendarDate(2020, 2, 3)})
)
.add(
- 'minDate: 2010/1/1, maxDate: 2020/1/1',
- () => render({minValue: new Date(2010, 0, 1), maxValue: new Date(2020, 0, 1)})
+ 'minValue: 2010/1/1, maxValue: 2020/1/1',
+ () => render({minValue: new CalendarDate(2010, 0, 1), maxValue: new CalendarDate(2020, 0, 1)})
)
.add(
- 'placeholderDate: 1980/1/1',
- () => render({placeholderDate: new Date(1980, 0, 1)})
+ 'placeholderValue: 1980/1/1',
+ () => render({placeholderValue: new CalendarDate(1980, 1, 1)})
+ )
+ .add(
+ 'placeholderValue: 1980/1/1 8 AM',
+ () => render({placeholderValue: new CalendarDateTime(1980, 1, 1, 8)})
+ )
+ .add(
+ 'placeholderValue: 1980/1/1, zoned',
+ () => render({placeholderValue: toZoned(new CalendarDate(1980, 1, 1), 'America/Los_Angeles')})
+ );
+
+storiesOf('Date and Time/DatePicker/styling', module)
+ .addDecorator(BlockDecorator)
+ .add(
+ 'isQuiet',
+ () => render({isQuiet: true})
+ )
+ .add(
+ 'labelPosition: side',
+ () => render({labelPosition: 'side'})
+ )
+ .add(
+ 'labelAlign: end',
+ () => render({labelPosition: 'top', labelAlign: 'end'})
+ )
+ .add(
+ 'required',
+ () => render({isRequired: true})
+ )
+ .add(
+ 'required with label',
+ () => render({isRequired: true, necessityIndicator: 'label'})
+ )
+ .add(
+ 'optional',
+ () => render({necessityIndicator: 'label'})
+ )
+ .add(
+ 'no visible label',
+ () => render({'aria-label': 'Date', label: null})
+ )
+ .add(
+ 'quiet no visible label',
+ () => render({isQuiet: true, 'aria-label': 'Date', label: null})
+ )
+ .add(
+ 'custom width',
+ () => render({width: 'size-3000'})
+ )
+ .add(
+ 'quiet custom width',
+ () => render({isQuiet: true, width: 'size-3000'})
+ )
+ .add(
+ 'custom width no visible label',
+ () => render({width: 'size-3000', label: null, 'aria-label': 'Date'})
+ )
+ .add(
+ 'custom width, labelPosition=side',
+ () => render({width: 'size-3000', labelPosition: 'side'})
);
function render(props = {}) {
return (
-
);
}
+
+// https://github.com/unicode-org/cldr/blob/22af90ae3bb04263f651323ce3d9a71747a75ffb/common/supplemental/supplementalData.xml#L4649-L4664
+const preferences = [
+ {locale: '', label: 'Default', ordering: 'gregory'},
+ {label: 'Arabic (Algeria)', locale: 'ar-DZ', territories: 'DJ DZ EH ER IQ JO KM LB LY MA MR OM PS SD SY TD TN YE', ordering: 'gregory islamic islamic-civil islamic-tbla'},
+ {label: 'Arabic (United Arab Emirates)', locale: 'ar-AE', territories: 'AE BH KW QA', ordering: 'gregory islamic-umalqura islamic islamic-civil islamic-tbla'},
+ {label: 'Arabic (Egypt)', locale: 'AR-EG', territories: 'EG', ordering: 'gregory coptic islamic islamic-civil islamic-tbla'},
+ {label: 'Arabic (Saudi Arabia)', locale: 'ar-SA', territories: 'SA', ordering: 'islamic-umalqura gregory islamic islamic-rgsa'},
+ {label: 'Farsi (Afghanistan)', locale: 'fa-AF', territories: 'AF IR', ordering: 'persian gregory islamic islamic-civil islamic-tbla'},
+ // {territories: 'CN CX HK MO SG', ordering: 'gregory chinese'},
+ {label: 'Amharic (Ethiopia)', locale: 'am-ET', territories: 'ET', ordering: 'gregory ethiopic ethioaa'},
+ {label: 'Hebrew (Israel)', locale: 'he-IL', territories: 'IL', ordering: 'gregory hebrew islamic islamic-civil islamic-tbla'},
+ {label: 'Hindi (India)', locale: 'hi-IN', territories: 'IN', ordering: 'gregory indian'},
+ // {label: 'Marathi (India)', locale: 'mr-IN', territories: 'IN', ordering: 'gregory indian'},
+ {label: 'Bengali (India)', locale: 'bn-IN', territories: 'IN', ordering: 'gregory indian'},
+ {label: 'Japanese (Japan)', locale: 'ja-JP', territories: 'JP', ordering: 'gregory japanese'},
+ // {territories: 'KR', ordering: 'gregory dangi'},
+ {label: 'Thai (Thailand)', locale: 'th-TH', territories: 'TH', ordering: 'buddhist gregory'},
+ {label: 'Chinese (Taiwan)', locale: 'zh-TW', territories: 'TW', ordering: 'gregory roc chinese'}
+];
+
+const calendars = [
+ {key: 'gregory', name: 'Gregorian'},
+ {key: 'japanese', name: 'Japanese'},
+ {key: 'buddhist', name: 'Buddhist'},
+ {key: 'roc', name: 'Taiwan'},
+ {key: 'persian', name: 'Persian'},
+ {key: 'indian', name: 'Indian'},
+ {key: 'islamic-umalqura', name: 'Islamic (Umm al-Qura)'},
+ {key: 'islamic-civil', name: 'Islamic Civil'},
+ {key: 'islamic-tbla', name: 'Islamic Tabular'},
+ {key: 'hebrew', name: 'Hebrew'},
+ {key: 'coptic', name: 'Coptic'},
+ {key: 'ethiopic', name: 'Ethiopic'},
+ {key: 'ethioaa', name: 'Ethiopic (Amete Alem)'}
+];
+
+function Example(props) {
+ let [locale, setLocale] = React.useState('');
+ let [calendar, setCalendar] = React.useState(calendars[0].key);
+ let {locale: defaultLocale} = useLocale();
+
+ let pref = preferences.find(p => p.locale === locale);
+ let preferredCalendars = React.useMemo(() => pref ? pref.ordering.split(' ').map(p => calendars.find(c => c.key === p)).filter(Boolean) : [calendars[0]], [pref]);
+ let otherCalendars = React.useMemo(() => calendars.filter(c => !preferredCalendars.some(p => p.key === c.key)), [preferredCalendars]);
+
+ let updateLocale = locale => {
+ setLocale(locale);
+ let pref = preferences.find(p => p.locale === locale);
+ setCalendar(pref.ordering.split(' ')[0]);
+ };
+
+ return (
+
+
+
+ {item => - {item.label}
}
+
+
+
+ {item => - {item.name}
}
+
+
+ {item => - {item.name}
}
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/@react-spectrum/datepicker/stories/DateRangePicker.stories.tsx b/packages/@react-spectrum/datepicker/stories/DateRangePicker.stories.tsx
index 21d576fa64f..c95ee5e1dfc 100644
--- a/packages/@react-spectrum/datepicker/stories/DateRangePicker.stories.tsx
+++ b/packages/@react-spectrum/datepicker/stories/DateRangePicker.stories.tsx
@@ -11,63 +11,58 @@
*/
import {action} from '@storybook/addon-actions';
+import {CalendarDate, parseDate, toZoned} from '@internationalized/date';
import {DateRangePicker} from '../';
import React from 'react';
import {storiesOf} from '@storybook/react';
const BlockDecorator = storyFn => {storyFn()} ;
-storiesOf('DateRangePicker', module)
+storiesOf('Date and Time/DateRangePicker', module)
.addDecorator(BlockDecorator)
.add(
'default',
() => render()
)
- .add(
- 'isQuiet',
- () => render({isQuiet: true})
- )
.add(
'defaultValue',
- () => render({defaultValue: {start: new Date(2020, 2, 3), end: new Date(2020, 5, 4)}})
+ () => render({defaultValue: {start: new CalendarDate(2020, 2, 3), end: new CalendarDate(2020, 5, 4)}})
)
.add(
'controlled value',
- () => render({value: {start: new Date(2020, 2, 3), end: new Date(2020, 5, 4)}})
- )
- .add(
- 'custom date format',
- () => render({
- formatOptions: {
- // weekday: 'long',
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- hour: 'numeric',
- minute: 'numeric',
- second: 'numeric'
- // hour12: false,
- // // timeZoneName: 'short',
- // // timeZone: 'America/New_York'
- // // era: 'long'
- }
- })
+ () => render({value: {start: new CalendarDate(2020, 2, 3), end: new CalendarDate(2020, 5, 4)}})
+ )
+ .add(
+ 'defaultValue, zoned',
+ () => render({defaultValue: {start: toZoned(parseDate('2020-02-03'), 'America/New_York'), end: toZoned(parseDate('2020-02-05'), 'America/Los_Angeles')}})
+ )
+ .add(
+ 'granularity: minute',
+ () => render({granularity: 'minute'})
+ )
+ .add(
+ 'granularity: second',
+ () => render({granularity: 'second'})
+ )
+ .add(
+ 'hourCycle: 12',
+ () => render({granularity: 'minute', hourCycle: 12})
+ )
+ .add(
+ 'hourCycle: 24',
+ () => render({granularity: 'minute', hourCycle: 24})
)
.add(
'isDisabled',
- () => render({isDisabled: true, value: {start: new Date(2020, 2, 3), end: new Date(2020, 5, 4)}})
+ () => render({isDisabled: true, value: {start: new CalendarDate(2020, 2, 3), end: new CalendarDate(2020, 5, 4)}})
)
.add(
'isQuiet, isDisabled',
- () => render({isQuiet: true, isDisabled: true, value: {start: new Date(2020, 2, 3), end: new Date(2020, 5, 4)}})
+ () => render({isQuiet: true, isDisabled: true, value: {start: new CalendarDate(2020, 2, 3), end: new CalendarDate(2020, 5, 4)}})
)
.add(
'isReadOnly',
- () => render({isReadOnly: true, value: {start: new Date(2020, 2, 3), end: new Date(2020, 5, 4)}})
- )
- .add(
- 'isRequired',
- () => render({isRequired: true})
+ () => render({isReadOnly: true, value: {start: new CalendarDate(2020, 2, 3), end: new CalendarDate(2020, 5, 4)}})
)
.add(
'autoFocus',
@@ -75,25 +70,77 @@ storiesOf('DateRangePicker', module)
)
.add(
'validationState: invalid',
- () => render({validationState: 'invalid', value: {start: new Date(2020, 2, 3), end: new Date(2020, 5, 4)}})
+ () => render({validationState: 'invalid', value: {start: new CalendarDate(2020, 2, 3), end: new CalendarDate(2020, 5, 4)}})
)
.add(
'validationState: valid',
- () => render({validationState: 'valid', value: {start: new Date(2020, 2, 3), end: new Date(2020, 5, 4)}})
+ () => render({validationState: 'valid', value: {start: new CalendarDate(2020, 2, 3), end: new CalendarDate(2020, 5, 4)}})
)
.add(
'minDate: 2010/1/1, maxDate: 2020/1/1',
- () => render({minValue: new Date(2010, 1, 1), maxValue: new Date(2020, 1, 1)})
+ () => render({minValue: new CalendarDate(2010, 1, 1), maxValue: new CalendarDate(2020, 1, 1)})
+ )
+ .add(
+ 'placeholderValue: 1980/1/1',
+ () => render({placeholderValue: new CalendarDate(1980, 1, 1)})
+ );
+
+storiesOf('Date and Time/DateRangePicker/styling', module)
+ .addDecorator(BlockDecorator)
+ .add(
+ 'isQuiet',
+ () => render({isQuiet: true})
+ )
+ .add(
+ 'labelPosition: side',
+ () => render({labelPosition: 'side'})
+ )
+ .add(
+ 'labelAlign: end',
+ () => render({labelPosition: 'top', labelAlign: 'end'})
+ )
+ .add(
+ 'required',
+ () => render({isRequired: true})
+ )
+ .add(
+ 'required with label',
+ () => render({isRequired: true, necessityIndicator: 'label'})
+ )
+ .add(
+ 'optional',
+ () => render({necessityIndicator: 'label'})
+ )
+ .add(
+ 'no visible label',
+ () => render({'aria-label': 'Date range', label: null})
+ )
+ .add(
+ 'quiet no visible label',
+ () => render({isQuiet: true, 'aria-label': 'Date range', label: null})
+ )
+ .add(
+ 'custom width',
+ () => render({width: 'size-3600'})
+ )
+ .add(
+ 'quiet custom width',
+ () => render({isQuiet: true, width: 'size-3600'})
+ )
+ .add(
+ 'custom width no visible label',
+ () => render({width: 'size-3600', label: null, 'aria-label': 'Date range'})
)
.add(
- 'placeholderDate: 1980/1/1',
- () => render({placeholderDate: new Date(1980, 0, 1)})
+ 'custom width, labelPosition=side',
+ () => render({width: 'size-3600', labelPosition: 'side'})
);
function render(props = {}) {
return (
diff --git a/packages/@react-spectrum/datepicker/stories/TimeField.stories.tsx b/packages/@react-spectrum/datepicker/stories/TimeField.stories.tsx
new file mode 100644
index 00000000000..c0833d94e39
--- /dev/null
+++ b/packages/@react-spectrum/datepicker/stories/TimeField.stories.tsx
@@ -0,0 +1,166 @@
+/*
+ * 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 {action} from '@storybook/addon-actions';
+import {CalendarDateTime, parseTime, parseZonedDateTime, Time, toZoned} from '@internationalized/date';
+import React from 'react';
+import {storiesOf} from '@storybook/react';
+import {TimeField} from '../';
+
+const BlockDecorator = storyFn => {storyFn()} ;
+
+storiesOf('Date and Time/TimeField', module)
+ .addDecorator(BlockDecorator)
+ .add(
+ 'default',
+ () => render()
+ )
+ .add(
+ 'defaultValue',
+ () => render({defaultValue: parseTime('20:24')})
+ )
+ .add(
+ 'controlled value',
+ () => render({value: new Time(2, 35)})
+ )
+ .add(
+ 'granularity: second',
+ () => render({granularity: 'second'})
+ )
+ .add(
+ 'hourCycle: 12',
+ () => render({hourCycle: 12, defaultValue: parseTime('00:00')})
+ )
+ .add(
+ 'hourCycle: 24',
+ () => render({hourCycle: 24, defaultValue: parseTime('00:00')})
+ )
+ .add(
+ 'hourCycle: 12, granularity: hour',
+ () => render({hourCycle: 12, granularity: 'hour'})
+ )
+ .add(
+ 'hourCycle: 24, granularity: hour',
+ () => render({hourCycle: 24, granularity: 'hour'})
+ )
+ .add(
+ 'zoned',
+ () => render({defaultValue: parseZonedDateTime('2021-11-07T00:45-07:00[America/Los_Angeles]')})
+ )
+ .add(
+ 'hideTimeZone',
+ () => render({defaultValue: parseZonedDateTime('2021-11-07T00:45-07:00[America/Los_Angeles]'), hideTimeZone: true})
+ )
+ .add(
+ 'isDisabled',
+ () => render({isDisabled: true, value: new Time(2, 35)})
+ )
+ .add(
+ 'isQuiet, isDisabled',
+ () => render({isQuiet: true, isDisabled: true, value: new Time(2, 35)})
+ )
+ .add(
+ 'isReadOnly',
+ () => render({isReadOnly: true, value: new Time(2, 35)})
+ )
+ .add(
+ 'autoFocus',
+ () => render({autoFocus: true})
+ )
+ .add(
+ 'validationState: invalid',
+ () => render({validationState: 'invalid', value: new Time(2, 35)})
+ )
+ .add(
+ 'validationState: valid',
+ () => render({validationState: 'valid', value: new Time(2, 35)})
+ )
+ .add(
+ 'placeholderValue: 8 AM',
+ () => render({placeholderValue: new Time(8)})
+ )
+ .add(
+ 'placeholderValue: 1980/1/1 8AM, zoned',
+ () => render({placeholderValue: toZoned(new CalendarDateTime(1980, 1, 1, 8), 'America/Los_Angeles')})
+ )
+ .add(
+ 'minValue: 8 AM',
+ () => render({minValue: new Time(8)})
+ )
+ .add(
+ 'maxValue: 8 PM',
+ () => render({maxValue: new Time(20)})
+ )
+ .add(
+ 'minValue: 8 AM, maxValue: 8 PM',
+ () => render({minValue: new Time(8), maxValue: new Time(20)})
+ );
+
+storiesOf('Date and Time/TimeField/styling', module)
+ .addDecorator(BlockDecorator)
+ .add(
+ 'isQuiet',
+ () => render({isQuiet: true})
+ )
+ .add(
+ 'labelPosition: side',
+ () => render({labelPosition: 'side'})
+ )
+ .add(
+ 'labelAlign: end',
+ () => render({labelPosition: 'top', labelAlign: 'end'})
+ )
+ .add(
+ 'required',
+ () => render({isRequired: true})
+ )
+ .add(
+ 'required with label',
+ () => render({isRequired: true, necessityIndicator: 'label'})
+ )
+ .add(
+ 'optional',
+ () => render({necessityIndicator: 'label'})
+ )
+ .add(
+ 'no visible label',
+ () => render({'aria-label': 'Time', label: null})
+ )
+ .add(
+ 'quiet no visible label',
+ () => render({isQuiet: true, 'aria-label': 'Time', label: null})
+ )
+ .add(
+ 'custom width',
+ () => render({width: 'size-3000'})
+ )
+ .add(
+ 'quiet custom width',
+ () => render({isQuiet: true, width: 'size-3000'})
+ )
+ .add(
+ 'custom width no visible label',
+ () => render({width: 'size-3000', label: null, 'aria-label': 'Time'})
+ )
+ .add(
+ 'custom width, labelPosition=side',
+ () => render({width: 'size-3000', labelPosition: 'side'})
+ );
+
+function render(props = {}) {
+ return (
+
+ );
+}
diff --git a/packages/@react-spectrum/datepicker/test/DatePicker.test.js b/packages/@react-spectrum/datepicker/test/DatePicker.test.js
index 6cd2ca621c0..677a0f01567 100644
--- a/packages/@react-spectrum/datepicker/test/DatePicker.test.js
+++ b/packages/@react-spectrum/datepicker/test/DatePicker.test.js
@@ -10,13 +10,21 @@
* governing permissions and limitations under the License.
*/
-import {act, fireEvent, render} from '@testing-library/react';
+import {act, fireEvent, render, within} from '@testing-library/react';
+import {CalendarDate, CalendarDateTime, getLocalTimeZone, toCalendarDateTime, today} from '@internationalized/date';
import {DatePicker} from '../';
import {Provider} from '@react-spectrum/provider';
import React from 'react';
import {theme} from '@react-spectrum/theme-default';
import {triggerPress} from '@react-spectrum/test-utils';
+function beforeInput(target, key) {
+ // JSDOM doesn't support the beforeinput event
+ let e = new InputEvent('beforeinput', {cancelable: true, data: key});
+ e.inputType = 'insertText';
+ fireEvent(target, e);
+}
+
describe('DatePicker', function () {
beforeAll(() => {
jest.useFakeTimers();
@@ -28,9 +36,9 @@ describe('DatePicker', function () {
});
describe('basics', function () {
it('should render a datepicker with a specified date', function () {
- let {getByRole, getAllByRole} = render();
+ let {getAllByRole} = render();
- let combobox = getByRole('combobox');
+ let combobox = getAllByRole('group')[0];
expect(combobox).toBeVisible();
expect(combobox).not.toHaveAttribute('aria-disabled');
expect(combobox).not.toHaveAttribute('aria-invalid');
@@ -41,7 +49,7 @@ describe('DatePicker', function () {
expect(segments[0].textContent).toBe('2');
expect(segments[0].getAttribute('aria-label')).toBe('Month');
expect(segments[0].getAttribute('aria-valuenow')).toBe('2');
- expect(segments[0].getAttribute('aria-valuetext')).toBe('February');
+ expect(segments[0].getAttribute('aria-valuetext')).toBe('2 − February');
expect(segments[0].getAttribute('aria-valuemin')).toBe('1');
expect(segments[0].getAttribute('aria-valuemax')).toBe('12');
@@ -60,18 +68,10 @@ describe('DatePicker', function () {
expect(segments[2].getAttribute('aria-valuemax')).toBe('9999');
});
- it('should render a datepicker with a custom date format', function () {
- let format = {
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- hour: 'numeric',
- minute: 'numeric',
- second: 'numeric'
- };
- let {getByRole, getAllByRole} = render();
-
- let combobox = getByRole('combobox');
+ it('should render a datepicker with granularity="second"', function () {
+ let {getAllByRole} = render();
+
+ let combobox = getAllByRole('group')[0];
expect(combobox).toBeVisible();
expect(combobox).not.toHaveAttribute('aria-disabled');
expect(combobox).not.toHaveAttribute('aria-invalid');
@@ -79,10 +79,10 @@ describe('DatePicker', function () {
let segments = getAllByRole('spinbutton');
expect(segments.length).toBe(7);
- expect(segments[0].textContent).toBe('February');
+ expect(segments[0].textContent).toBe('2');
expect(segments[0].getAttribute('aria-label')).toBe('Month');
expect(segments[0].getAttribute('aria-valuenow')).toBe('2');
- expect(segments[0].getAttribute('aria-valuetext')).toBe('February');
+ expect(segments[0].getAttribute('aria-valuetext')).toBe('2 − February');
expect(segments[0].getAttribute('aria-valuemin')).toBe('1');
expect(segments[0].getAttribute('aria-valuemax')).toBe('12');
@@ -130,13 +130,13 @@ describe('DatePicker', function () {
describe('calendar popover', function () {
it('should emit onChange when selecting a date in the calendar in controlled mode', function () {
let onChange = jest.fn();
- let {getByRole, getAllByRole} = render(
+ let {getByRole, getAllByRole, queryByLabelText} = render(
-
+
);
- let combobox = getByRole('combobox');
+ let combobox = getAllByRole('group')[0];
expect(combobox).toHaveTextContent('2/3/2019');
let button = getByRole('button');
@@ -145,6 +145,8 @@ describe('DatePicker', function () {
let dialog = getByRole('dialog');
expect(dialog).toBeVisible();
+ expect(queryByLabelText('Time')).toBeNull();
+
let cells = getAllByRole('gridcell');
let selected = cells.find(cell => cell.getAttribute('aria-selected') === 'true');
expect(selected.children[0]).toHaveAttribute('aria-label', 'Sunday, February 3, 2019 selected');
@@ -153,7 +155,7 @@ describe('DatePicker', function () {
expect(dialog).not.toBeInTheDocument();
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange).toHaveBeenCalledWith(new Date(2019, 1, 4));
+ expect(onChange).toHaveBeenCalledWith(new CalendarDate(2019, 2, 4));
expect(combobox).toHaveTextContent('2/3/2019'); // controlled
});
@@ -161,11 +163,11 @@ describe('DatePicker', function () {
let onChange = jest.fn();
let {getByRole, getAllByRole} = render(
-
+
);
- let combobox = getByRole('combobox');
+ let combobox = getAllByRole('group')[0];
expect(combobox).toHaveTextContent('2/3/2019');
let button = getByRole('button');
@@ -182,41 +184,170 @@ describe('DatePicker', function () {
expect(dialog).not.toBeInTheDocument();
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange).toHaveBeenCalledWith(new Date(2019, 1, 4));
+ expect(onChange).toHaveBeenCalledWith(new CalendarDate(2019, 2, 4));
expect(combobox).toHaveTextContent('2/4/2019'); // uncontrolled
});
+
+ it('should display a time field when a CalendarDateTime value is used', function () {
+ let onChange = jest.fn();
+ let {getByRole, getAllByRole, getAllByLabelText} = render(
+
+
+
+ );
+
+ let combobox = getAllByRole('group')[0];
+ expect(combobox).toHaveTextContent('2/3/2019, 8:45 AM');
+
+ let button = getByRole('button');
+ triggerPress(button);
+
+ let dialog = getByRole('dialog');
+ expect(dialog).toBeVisible();
+
+ let cells = getAllByRole('gridcell');
+ let selected = cells.find(cell => cell.getAttribute('aria-selected') === 'true');
+ expect(selected.children[0]).toHaveAttribute('aria-label', 'Sunday, February 3, 2019 selected');
+
+ let timeField = getAllByLabelText('Time')[0];
+ expect(timeField).toHaveTextContent('8:45 AM');
+
+ // selecting a date should not close the popover
+ triggerPress(selected.nextSibling.children[0]);
+
+ expect(dialog).toBeVisible();
+ expect(onChange).toHaveBeenCalledTimes(1);
+ expect(onChange).toHaveBeenCalledWith(new CalendarDateTime(2019, 2, 4, 8, 45));
+ expect(combobox).toHaveTextContent('2/4/2019, 8:45 AM');
+
+ let hour = within(timeField).getByLabelText('Hour');
+ expect(hour).toHaveAttribute('role', 'spinbutton');
+ expect(hour).toHaveAttribute('aria-valuetext', '8 AM');
+
+ act(() => hour.focus());
+ fireEvent.keyDown(hour, {key: 'ArrowUp'});
+ fireEvent.keyUp(hour, {key: 'ArrowUp'});
+
+ expect(hour).toHaveAttribute('aria-valuetext', '9 AM');
+
+ expect(dialog).toBeVisible();
+ expect(onChange).toHaveBeenCalledTimes(2);
+ expect(onChange).toHaveBeenCalledWith(new CalendarDateTime(2019, 2, 4, 9, 45));
+ expect(combobox).toHaveTextContent('2/4/2019, 9:45 AM');
+ });
+
+ it('should not fire onChange until both date and time are selected', function () {
+ let onChange = jest.fn();
+ let {getByRole, getAllByRole, getAllByLabelText} = render(
+
+
+
+ );
+
+ let combobox = getAllByRole('group')[0];
+ let formatter = new Intl.DateTimeFormat('en-US', {year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric'});
+ let placeholder = formatter.format(toCalendarDateTime(today(getLocalTimeZone())).set({hour: 12, minute: 0}).toDate(getLocalTimeZone()));
+ expect(combobox).toHaveTextContent(placeholder);
+
+ let button = getByRole('button');
+ triggerPress(button);
+
+ let dialog = getByRole('dialog');
+ expect(dialog).toBeVisible();
+
+ let cells = getAllByRole('gridcell');
+ let selected = cells.find(cell => cell.getAttribute('aria-selected') === 'true');
+ expect(selected).toBeUndefined();
+
+ let timeField = getAllByLabelText('Time')[0];
+ expect(timeField).toHaveTextContent('12:00 PM');
+
+ // selecting a date should not close the popover
+ let todayCell = cells.find(cell => cell.firstChild.getAttribute('aria-label')?.startsWith('Today'));
+ triggerPress(todayCell.firstChild);
+
+ expect(todayCell).toHaveAttribute('aria-selected', 'true');
+
+ expect(dialog).toBeVisible();
+ expect(onChange).not.toHaveBeenCalled();
+ expect(combobox).toHaveTextContent(placeholder);
+
+ let hour = within(timeField).getByLabelText('Hour');
+ expect(hour).toHaveAttribute('role', 'spinbutton');
+ expect(hour).toHaveAttribute('aria-valuetext', '12 PM');
+
+ act(() => hour.focus());
+ fireEvent.keyDown(hour, {key: 'ArrowUp'});
+ fireEvent.keyUp(hour, {key: 'ArrowUp'});
+
+ expect(hour).toHaveAttribute('aria-valuetext', '1 PM');
+
+ expect(onChange).not.toHaveBeenCalled();
+ expect(combobox).toHaveTextContent(placeholder);
+
+ fireEvent.keyDown(hour, {key: 'ArrowRight'});
+ fireEvent.keyUp(hour, {key: 'ArrowRight'});
+
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Minute');
+ expect(document.activeElement).toHaveAttribute('aria-valuetext', '00');
+ fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'});
+ fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'});
+
+ expect(document.activeElement).toHaveAttribute('aria-valuetext', '01');
+
+ expect(onChange).not.toHaveBeenCalled();
+ expect(combobox).toHaveTextContent(placeholder);
+
+ fireEvent.keyDown(hour, {key: 'ArrowRight'});
+ fireEvent.keyUp(hour, {key: 'ArrowRight'});
+
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Day Period');
+ expect(document.activeElement).toHaveAttribute('aria-valuetext', '1 PM');
+
+ fireEvent.keyDown(document.activeElement, {key: 'Enter'});
+ fireEvent.keyUp(document.activeElement, {key: 'Enter'});
+
+ expect(dialog).toBeVisible();
+ expect(onChange).toHaveBeenCalledTimes(1);
+ let value = toCalendarDateTime(today(getLocalTimeZone())).set({hour: 13, minute: 1});
+ expect(onChange).toHaveBeenCalledWith(value);
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
+ });
});
describe('labeling', function () {
- it('should support labeling with a default label', function () {
- let {getByRole, getAllByRole} = render();
+ it('should support labeling', function () {
+ let {getByRole, getAllByRole, getByText} = render();
+
+ let label = getByText('Date');
- let combobox = getByRole('combobox');
- expect(combobox).toHaveAttribute('aria-label', 'Date');
- expect(combobox).toHaveAttribute('id');
- let comboboxId = combobox.getAttribute('id');
+ let combobox = getAllByRole('group')[0];
+ expect(combobox).toHaveAttribute('aria-labelledby', label.id);
let button = getByRole('button');
expect(button).toHaveAttribute('aria-label', 'Calendar');
expect(button).toHaveAttribute('id');
let buttonId = button.getAttribute('id');
- expect(button).toHaveAttribute('aria-labelledby', `${comboboxId} ${buttonId}`);
+ expect(button).toHaveAttribute('aria-labelledby', `${label.id} ${buttonId}`);
let segments = getAllByRole('spinbutton');
for (let segment of segments) {
expect(segment).toHaveAttribute('id');
let segmentId = segment.getAttribute('id');
- expect(segment).toHaveAttribute('aria-labelledby', `${comboboxId} ${segmentId}`);
+ expect(segment).toHaveAttribute('aria-labelledby', `${label.id} ${segmentId}`);
}
});
it('should support labeling with aria-label', function () {
let {getByRole, getAllByRole} = render();
- let combobox = getByRole('combobox');
- expect(combobox).toHaveAttribute('aria-label', 'Birth date');
- expect(combobox).toHaveAttribute('id');
- let comboboxId = combobox.getAttribute('id');
+ let field = getAllByRole('group')[1];
+ expect(field).toHaveAttribute('aria-label', 'Birth date');
+ expect(field).toHaveAttribute('id');
+ let comboboxId = field.getAttribute('id');
+
+ let combobox = getAllByRole('group')[0];
+ expect(combobox).toHaveAttribute('aria-labelledby', comboboxId);
let button = getByRole('button');
expect(button).toHaveAttribute('aria-label', 'Calendar');
@@ -235,10 +366,13 @@ describe('DatePicker', function () {
it('should support labeling with aria-labelledby', function () {
let {getByRole, getAllByRole} = render();
- let combobox = getByRole('combobox');
+ let combobox = getAllByRole('group')[0];
expect(combobox).not.toHaveAttribute('aria-label');
expect(combobox).toHaveAttribute('aria-labelledby', 'foo');
+ let field = getAllByRole('group')[1];
+ expect(field).toHaveAttribute('aria-labelledby', 'foo');
+
let button = getByRole('button');
expect(button).toHaveAttribute('aria-label', 'Calendar');
expect(button).toHaveAttribute('id');
@@ -256,12 +390,12 @@ describe('DatePicker', function () {
describe('focus management', function () {
it('should focus the last segment on mouse down in the field', function () {
- let {getAllByRole, getByTestId} = render();
- let field = getByTestId('date-field');
+ let {getAllByRole} = render();
+ let field = getAllByRole('group')[1];
let segments = getAllByRole('spinbutton');
- fireEvent.mouseDown(field);
- expect(segments[0]).toHaveFocus();
+ triggerPress(field);
+ expect(segments[segments.length - 1]).toHaveFocus();
});
});
@@ -269,18 +403,9 @@ describe('DatePicker', function () {
describe('arrow keys', function () {
function testArrows(label, value, incremented, decremented, options = {}) {
let onChange = jest.fn();
- let format = {
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- hour: 'numeric',
- minute: 'numeric',
- second: 'numeric',
- ...options.format
- };
// Test controlled mode
- let {getByLabelText, unmount} = render();
+ let {getByLabelText, unmount} = render();
let segment = getByLabelText(label);
let textContent = segment.textContent;
act(() => {segment.focus();});
@@ -298,7 +423,7 @@ describe('DatePicker', function () {
// Test uncontrolled mode (increment)
onChange = jest.fn();
- ({getByLabelText, unmount} = render());
+ ({getByLabelText, unmount} = render());
segment = getByLabelText(label);
textContent = segment.textContent;
act(() => {segment.focus();});
@@ -311,7 +436,7 @@ describe('DatePicker', function () {
// Test uncontrolled mode (decrement)
onChange = jest.fn();
- ({getByLabelText, unmount} = render());
+ ({getByLabelText, unmount} = render());
segment = getByLabelText(label);
textContent = segment.textContent;
act(() => {segment.focus();});
@@ -324,7 +449,7 @@ describe('DatePicker', function () {
// Test read only mode (increment)
onChange = jest.fn();
- ({getByLabelText, unmount} = render());
+ ({getByLabelText, unmount} = render());
segment = getByLabelText(label);
textContent = segment.textContent;
act(() => {segment.focus();});
@@ -336,7 +461,7 @@ describe('DatePicker', function () {
// Test read only mode (decrement)
onChange = jest.fn();
- ({getByLabelText, unmount} = render());
+ ({getByLabelText, unmount} = render());
segment = getByLabelText(label);
textContent = segment.textContent;
act(() => {segment.focus();});
@@ -349,158 +474,149 @@ describe('DatePicker', function () {
describe('month', function () {
it('should support using the arrow keys to increment and decrement the month', function () {
- testArrows('Month', new Date(2019, 1, 3), new Date(2019, 2, 3), new Date(2019, 0, 3));
+ testArrows('Month', new CalendarDate(2019, 2, 3), new CalendarDate(2019, 3, 3), new CalendarDate(2019, 1, 3));
});
it('should wrap around when incrementing and decrementing the month', function () {
- testArrows('Month', new Date(2019, 11, 3), new Date(2019, 0, 3), new Date(2019, 10, 3));
- testArrows('Month', new Date(2019, 0, 3), new Date(2019, 1, 3), new Date(2019, 11, 3));
+ testArrows('Month', new CalendarDate(2019, 12, 3), new CalendarDate(2019, 1, 3), new CalendarDate(2019, 11, 3));
+ testArrows('Month', new CalendarDate(2019, 1, 3), new CalendarDate(2019, 2, 3), new CalendarDate(2019, 12, 3));
});
it('should support using the page up and down keys to increment and decrement the month by 2', function () {
- testArrows('Month', new Date(2019, 0, 3), new Date(2019, 2, 3), new Date(2019, 10, 3), {upKey: 'PageUp', downKey: 'PageDown'});
- testArrows('Month', new Date(2019, 1, 3), new Date(2019, 3, 3), new Date(2019, 11, 3), {upKey: 'PageUp', downKey: 'PageDown'});
+ testArrows('Month', new CalendarDate(2019, 1, 3), new CalendarDate(2019, 3, 3), new CalendarDate(2019, 11, 3), {upKey: 'PageUp', downKey: 'PageDown'});
+ testArrows('Month', new CalendarDate(2019, 2, 3), new CalendarDate(2019, 4, 3), new CalendarDate(2019, 12, 3), {upKey: 'PageUp', downKey: 'PageDown'});
});
it('should support using the home and end keys to jump to the min and max month', function () {
- testArrows('Month', new Date(2019, 5, 3), new Date(2019, 11, 3), new Date(2019, 0, 3), {upKey: 'End', downKey: 'Home'});
+ testArrows('Month', new CalendarDate(2019, 6, 3), new CalendarDate(2019, 12, 3), new CalendarDate(2019, 1, 3), {upKey: 'End', downKey: 'Home'});
});
});
describe('day', function () {
it('should support using the arrow keys to increment and decrement the day', function () {
- testArrows('Day', new Date(2019, 1, 3), new Date(2019, 1, 4), new Date(2019, 1, 2));
+ testArrows('Day', new CalendarDate(2019, 2, 3), new CalendarDate(2019, 2, 4), new CalendarDate(2019, 2, 2));
});
it('should wrap around when incrementing and decrementing the day', function () {
- testArrows('Day', new Date(2019, 1, 28), new Date(2019, 1, 1), new Date(2019, 1, 27));
- testArrows('Day', new Date(2019, 1, 1), new Date(2019, 1, 2), new Date(2019, 1, 28));
+ testArrows('Day', new CalendarDate(2019, 2, 28), new CalendarDate(2019, 2, 1), new CalendarDate(2019, 2, 27));
+ testArrows('Day', new CalendarDate(2019, 2, 1), new CalendarDate(2019, 2, 2), new CalendarDate(2019, 2, 28));
});
it('should support using the page up and down keys to increment and decrement the day by 7', function () {
- testArrows('Day', new Date(2019, 1, 3), new Date(2019, 1, 10), new Date(2019, 1, 24), {upKey: 'PageUp', downKey: 'PageDown'});
+ testArrows('Day', new CalendarDate(2019, 2, 3), new CalendarDate(2019, 2, 10), new CalendarDate(2019, 2, 24), {upKey: 'PageUp', downKey: 'PageDown'});
});
it('should support using the home and end keys to jump to the min and max day', function () {
- testArrows('Day', new Date(2019, 1, 5), new Date(2019, 1, 28), new Date(2019, 1, 1), {upKey: 'End', downKey: 'Home'});
+ testArrows('Day', new CalendarDate(2019, 2, 5), new CalendarDate(2019, 2, 28), new CalendarDate(2019, 2, 1), {upKey: 'End', downKey: 'Home'});
});
});
describe('year', function () {
it('should support using the arrow keys to increment and decrement the year', function () {
- testArrows('Year', new Date(2019, 1, 3), new Date(2020, 1, 3), new Date(2018, 1, 3));
+ testArrows('Year', new CalendarDate(2019, 2, 3), new CalendarDate(2020, 2, 3), new CalendarDate(2018, 2, 3));
});
it('should support using the page up and down keys to increment and decrement the year to the nearest 5', function () {
- testArrows('Year', new Date(2019, 1, 3), new Date(2020, 1, 3), new Date(2015, 1, 3), {upKey: 'PageUp', downKey: 'PageDown'});
+ testArrows('Year', new CalendarDate(2019, 2, 3), new CalendarDate(2020, 2, 3), new CalendarDate(2015, 2, 3), {upKey: 'PageUp', downKey: 'PageDown'});
});
});
describe('hour', function () {
it('should support using the arrow keys to increment and decrement the hour', function () {
- testArrows('Hour', new Date(2019, 1, 3, 8), new Date(2019, 1, 3, 9), new Date(2019, 1, 3, 7));
+ testArrows('Hour', new CalendarDateTime(2019, 2, 3, 8), new CalendarDateTime(2019, 2, 3, 9), new CalendarDateTime(2019, 2, 3, 7));
});
it('should wrap around when incrementing and decrementing the hour in 12 hour time', function () {
// AM
- testArrows('Hour', new Date(2019, 1, 3, 11), new Date(2019, 1, 3, 0), new Date(2019, 1, 3, 10));
- testArrows('Hour', new Date(2019, 1, 3, 0), new Date(2019, 1, 3, 1), new Date(2019, 1, 3, 11));
+ testArrows('Hour', new CalendarDateTime(2019, 2, 3, 11), new CalendarDateTime(2019, 2, 3, 0), new CalendarDateTime(2019, 2, 3, 10));
+ testArrows('Hour', new CalendarDateTime(2019, 2, 3, 0), new CalendarDateTime(2019, 2, 3, 1), new CalendarDateTime(2019, 2, 3, 11));
// PM
- testArrows('Hour', new Date(2019, 1, 3, 23), new Date(2019, 1, 3, 12), new Date(2019, 1, 3, 22));
- testArrows('Hour', new Date(2019, 1, 3, 12), new Date(2019, 1, 3, 13), new Date(2019, 1, 3, 23));
+ testArrows('Hour', new CalendarDateTime(2019, 2, 3, 23), new CalendarDateTime(2019, 2, 3, 12), new CalendarDateTime(2019, 2, 3, 22));
+ testArrows('Hour', new CalendarDateTime(2019, 2, 3, 12), new CalendarDateTime(2019, 2, 3, 13), new CalendarDateTime(2019, 2, 3, 23));
});
it('should wrap around when incrementing and decrementing the hour in 24 hour time', function () {
- testArrows('Hour', new Date(2019, 1, 3, 23), new Date(2019, 1, 3, 0), new Date(2019, 1, 3, 22), {format: {hour12: false}});
- testArrows('Hour', new Date(2019, 1, 3, 0), new Date(2019, 1, 3, 1), new Date(2019, 1, 3, 23), {format: {hour12: false}});
+ testArrows('Hour', new CalendarDateTime(2019, 2, 3, 23), new CalendarDateTime(2019, 2, 3, 0), new CalendarDateTime(2019, 2, 3, 22), {props: {hourCycle: 24}});
+ testArrows('Hour', new CalendarDateTime(2019, 2, 3, 0), new CalendarDateTime(2019, 2, 3, 1), new CalendarDateTime(2019, 2, 3, 23), {props: {hourCycle: 24}});
});
it('should support using the page up and down keys to increment and decrement the hour by 2', function () {
- testArrows('Hour', new Date(2019, 1, 3, 8), new Date(2019, 1, 3, 10), new Date(2019, 1, 3, 6), {upKey: 'PageUp', downKey: 'PageDown'});
+ testArrows('Hour', new CalendarDateTime(2019, 2, 3, 8), new CalendarDateTime(2019, 2, 3, 10), new CalendarDateTime(2019, 2, 3, 6), {upKey: 'PageUp', downKey: 'PageDown'});
});
it('should support using the home and end keys to jump to the min and max hour in 12 hour time', function () {
// AM
- testArrows('Hour', new Date(2019, 1, 3, 8), new Date(2019, 1, 3, 11), new Date(2019, 1, 3, 0), {upKey: 'End', downKey: 'Home'});
+ testArrows('Hour', new CalendarDateTime(2019, 2, 3, 8), new CalendarDateTime(2019, 2, 3, 11), new CalendarDateTime(2019, 2, 3, 0), {upKey: 'End', downKey: 'Home'});
// PM
- testArrows('Hour', new Date(2019, 1, 3, 16), new Date(2019, 1, 3, 23), new Date(2019, 1, 3, 12), {upKey: 'End', downKey: 'Home'});
+ testArrows('Hour', new CalendarDateTime(2019, 2, 3, 16), new CalendarDateTime(2019, 2, 3, 23), new CalendarDateTime(2019, 2, 3, 12), {upKey: 'End', downKey: 'Home'});
});
it('should support using the home and end keys to jump to the min and max hour in 24 hour time', function () {
- testArrows('Hour', new Date(2019, 1, 3, 8), new Date(2019, 1, 3, 23), new Date(2019, 1, 3, 0), {upKey: 'End', downKey: 'Home', format: {hour12: false}});
+ testArrows('Hour', new CalendarDateTime(2019, 2, 3, 8), new CalendarDateTime(2019, 2, 3, 23), new CalendarDateTime(2019, 2, 3, 0), {upKey: 'End', downKey: 'Home', props: {hourCycle: 24}});
});
});
describe('minute', function () {
it('should support using the arrow keys to increment and decrement the minute', function () {
- testArrows('Minute', new Date(2019, 1, 3, 8, 5), new Date(2019, 1, 3, 8, 6), new Date(2019, 1, 3, 8, 4));
+ testArrows('Minute', new CalendarDateTime(2019, 2, 3, 8, 5), new CalendarDateTime(2019, 2, 3, 8, 6), new CalendarDateTime(2019, 2, 3, 8, 4));
});
it('should wrap around when incrementing and decrementing the minute', function () {
- testArrows('Minute', new Date(2019, 1, 3, 8, 59), new Date(2019, 1, 3, 8, 0), new Date(2019, 1, 3, 8, 58));
- testArrows('Minute', new Date(2019, 1, 3, 8, 0), new Date(2019, 1, 3, 8, 1), new Date(2019, 1, 3, 8, 59));
+ testArrows('Minute', new CalendarDateTime(2019, 2, 3, 8, 59), new CalendarDateTime(2019, 2, 3, 8, 0), new CalendarDateTime(2019, 2, 3, 8, 58));
+ testArrows('Minute', new CalendarDateTime(2019, 2, 3, 8, 0), new CalendarDateTime(2019, 2, 3, 8, 1), new CalendarDateTime(2019, 2, 3, 8, 59));
});
it('should support using the page up and down keys to increment and decrement the minute to the nearest 15', function () {
- testArrows('Minute', new Date(2019, 1, 3, 8, 22), new Date(2019, 1, 3, 8, 30), new Date(2019, 1, 3, 8, 15), {upKey: 'PageUp', downKey: 'PageDown'});
+ testArrows('Minute', new CalendarDateTime(2019, 2, 3, 8, 22), new CalendarDateTime(2019, 2, 3, 8, 30), new CalendarDateTime(2019, 2, 3, 8, 15), {upKey: 'PageUp', downKey: 'PageDown'});
});
it('should support using the home and end keys to jump to the min and max minute', function () {
- testArrows('Minute', new Date(2019, 1, 3, 8, 22), new Date(2019, 1, 3, 8, 59), new Date(2019, 1, 3, 8, 0), {upKey: 'End', downKey: 'Home', format: {hour12: false}});
+ testArrows('Minute', new CalendarDateTime(2019, 2, 3, 8, 22), new CalendarDateTime(2019, 2, 3, 8, 59), new CalendarDateTime(2019, 2, 3, 8, 0), {upKey: 'End', downKey: 'Home', props: {hourCycle: 24}});
});
});
describe('second', function () {
it('should support using the arrow keys to increment and decrement the second', function () {
- testArrows('Second', new Date(2019, 1, 3, 8, 5, 10), new Date(2019, 1, 3, 8, 5, 11), new Date(2019, 1, 3, 8, 5, 9));
+ testArrows('Second', new CalendarDateTime(2019, 2, 3, 8, 5, 10), new CalendarDateTime(2019, 2, 3, 8, 5, 11), new CalendarDateTime(2019, 2, 3, 8, 5, 9), {props: {granularity: 'second'}});
});
it('should wrap around when incrementing and decrementing the second', function () {
- testArrows('Second', new Date(2019, 1, 3, 8, 5, 59), new Date(2019, 1, 3, 8, 5, 0), new Date(2019, 1, 3, 8, 5, 58));
- testArrows('Second', new Date(2019, 1, 3, 8, 5, 0), new Date(2019, 1, 3, 8, 5, 1), new Date(2019, 1, 3, 8, 5, 59));
+ testArrows('Second', new CalendarDateTime(2019, 2, 3, 8, 5, 59), new CalendarDateTime(2019, 2, 3, 8, 5, 0), new CalendarDateTime(2019, 2, 3, 8, 5, 58), {props: {granularity: 'second'}});
+ testArrows('Second', new CalendarDateTime(2019, 2, 3, 8, 5, 0), new CalendarDateTime(2019, 2, 3, 8, 5, 1), new CalendarDateTime(2019, 2, 3, 8, 5, 59), {props: {granularity: 'second'}});
});
it('should support using the page up and down keys to increment and decrement the second to the nearest 15', function () {
- testArrows('Second', new Date(2019, 1, 3, 8, 5, 22), new Date(2019, 1, 3, 8, 5, 30), new Date(2019, 1, 3, 8, 5, 15), {upKey: 'PageUp', downKey: 'PageDown'});
+ testArrows('Second', new CalendarDateTime(2019, 2, 3, 8, 5, 22), new CalendarDateTime(2019, 2, 3, 8, 5, 30), new CalendarDateTime(2019, 2, 3, 8, 5, 15), {upKey: 'PageUp', downKey: 'PageDown', props: {granularity: 'second'}});
});
it('should support using the home and end keys to jump to the min and max second', function () {
- testArrows('Second', new Date(2019, 1, 3, 8, 5, 22), new Date(2019, 1, 3, 8, 5, 59), new Date(2019, 1, 3, 8, 5, 0), {upKey: 'End', downKey: 'Home', format: {hour12: false}});
+ testArrows('Second', new CalendarDateTime(2019, 2, 3, 8, 5, 22), new CalendarDateTime(2019, 2, 3, 8, 5, 59), new CalendarDateTime(2019, 2, 3, 8, 5, 0), {upKey: 'End', downKey: 'Home', props: {granularity: 'second', hourCycle: 24}});
});
});
describe('day period', function () {
it('should support using the arrow keys to increment and decrement the day period', function () {
- testArrows('Day Period', new Date(2019, 1, 3, 8), new Date(2019, 1, 3, 20), new Date(2019, 1, 3, 20));
- testArrows('Day Period', new Date(2019, 1, 3, 20), new Date(2019, 1, 3, 8), new Date(2019, 1, 3, 8));
+ testArrows('Day Period', new CalendarDateTime(2019, 2, 3, 8), new CalendarDateTime(2019, 2, 3, 20), new CalendarDateTime(2019, 2, 3, 20));
+ testArrows('Day Period', new CalendarDateTime(2019, 2, 3, 20), new CalendarDateTime(2019, 2, 3, 8), new CalendarDateTime(2019, 2, 3, 8));
});
});
});
describe('text input', function () {
- function testInput(label, value, keys, newValue, moved, options) {
+ function testInput(label, value, keys, newValue, moved, props) {
let onChange = jest.fn();
- let format = {
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- hour: 'numeric',
- minute: 'numeric',
- second: 'numeric',
- ...options
- };
-
// Test controlled mode
- let {getByLabelText, getAllByRole, unmount} = render();
+ let {getByLabelText, getAllByRole, unmount} = render();
let segment = getByLabelText(label);
let textContent = segment.textContent;
act(() => {segment.focus();});
let i = 0;
for (let key of keys) {
- fireEvent.keyDown(segment, {key});
+ beforeInput(segment, key);
+
expect(onChange).toHaveBeenCalledTimes(++i);
expect(segment.textContent).toBe(textContent);
@@ -523,14 +639,15 @@ describe('DatePicker', function () {
// Test uncontrolled mode
onChange = jest.fn();
- ({getByLabelText, getAllByRole, unmount} = render());
+ ({getByLabelText, getAllByRole, unmount} = render());
segment = getByLabelText(label);
textContent = segment.textContent;
act(() => {segment.focus();});
i = 0;
for (let key of keys) {
- fireEvent.keyDown(segment, {key});
+ beforeInput(segment, key);
+
expect(onChange).toHaveBeenCalledTimes(++i);
expect(segment.textContent).not.toBe(textContent);
@@ -553,13 +670,14 @@ describe('DatePicker', function () {
// Test read only mode
onChange = jest.fn();
- ({getByLabelText, getAllByRole, unmount} = render());
+ ({getByLabelText, getAllByRole, unmount} = render());
segment = getByLabelText(label);
textContent = segment.textContent;
act(() => {segment.focus();});
for (let key of keys) {
- fireEvent.keyDown(segment, {key});
+ beforeInput(segment, key);
+
expect(onChange).not.toHaveBeenCalled();
expect(segment.textContent).toBe(textContent);
expect(segment).toHaveFocus();
@@ -571,82 +689,74 @@ describe('DatePicker', function () {
}
it('should support typing into the month segment', function () {
- testInput('Month', new Date(2019, 1, 3), '1', new Date(2019, 0, 3), false);
- testInput('Month', new Date(2019, 1, 3), '12', new Date(2019, 11, 3), true);
- testInput('Month', new Date(2019, 1, 3), '4', new Date(2019, 3, 3), true);
+ testInput('Month', new CalendarDate(2019, 2, 3), '1', new CalendarDate(2019, 1, 3), false);
+ testInput('Month', new CalendarDate(2019, 2, 3), '12', new CalendarDate(2019, 12, 3), true);
+ testInput('Month', new CalendarDate(2019, 2, 3), '4', new CalendarDate(2019, 4, 3), true);
});
it('should support typing into the day segment', function () {
- testInput('Day', new Date(2019, 1, 3), '1', new Date(2019, 1, 1), false);
- testInput('Day', new Date(2019, 1, 3), '12', new Date(2019, 1, 12), true);
- testInput('Day', new Date(2019, 1, 3), '4', new Date(2019, 1, 4), true);
+ testInput('Day', new CalendarDate(2019, 2, 3), '1', new CalendarDate(2019, 2, 1), false);
+ testInput('Day', new CalendarDate(2019, 2, 3), '12', new CalendarDate(2019, 2, 12), true);
+ testInput('Day', new CalendarDate(2019, 2, 3), '4', new CalendarDate(2019, 2, 4), true);
});
it('should support typing into the year segment', function () {
- testInput('Year', new Date(2019, 1, 3), '1993', new Date(1993, 1, 3), true);
+ testInput('Year', new CalendarDate(2019, 2, 3), '1993', new CalendarDate(1993, 2, 3), false);
+ testInput('Year', new CalendarDateTime(2019, 2, 3, 8), '1993', new CalendarDateTime(1993, 2, 3, 8), true);
});
it('should support typing into the hour segment in 12 hour time', function () {
// AM
- testInput('Hour', new Date(2019, 1, 3, 8), '1', new Date(2019, 1, 3, 1), false);
- testInput('Hour', new Date(2019, 1, 3, 8), '11', new Date(2019, 1, 3, 11), true);
- testInput('Hour', new Date(2019, 1, 3, 8), '12', new Date(2019, 1, 3, 0), true);
- testInput('Hour', new Date(2019, 1, 3, 8), '4', new Date(2019, 1, 3, 4), true);
+ testInput('Hour', new CalendarDateTime(2019, 2, 3, 8), '1', new CalendarDateTime(2019, 2, 3, 1), false);
+ testInput('Hour', new CalendarDateTime(2019, 2, 3, 8), '11', new CalendarDateTime(2019, 2, 3, 11), true);
+ testInput('Hour', new CalendarDateTime(2019, 2, 3, 8), '12', new CalendarDateTime(2019, 2, 3, 0), true);
+ testInput('Hour', new CalendarDateTime(2019, 2, 3, 8), '4', new CalendarDateTime(2019, 2, 3, 4), true);
// PM
- testInput('Hour', new Date(2019, 1, 3, 20), '1', new Date(2019, 1, 3, 13), false);
- testInput('Hour', new Date(2019, 1, 3, 20), '11', new Date(2019, 1, 3, 23), true);
- testInput('Hour', new Date(2019, 1, 3, 20), '12', new Date(2019, 1, 3, 12), true);
- testInput('Hour', new Date(2019, 1, 3, 20), '4', new Date(2019, 1, 3, 16), true);
+ testInput('Hour', new CalendarDateTime(2019, 2, 3, 20), '1', new CalendarDateTime(2019, 2, 3, 13), false);
+ testInput('Hour', new CalendarDateTime(2019, 2, 3, 20), '11', new CalendarDateTime(2019, 2, 3, 23), true);
+ testInput('Hour', new CalendarDateTime(2019, 2, 3, 20), '12', new CalendarDateTime(2019, 2, 3, 12), true);
+ testInput('Hour', new CalendarDateTime(2019, 2, 3, 20), '4', new CalendarDateTime(2019, 2, 3, 16), true);
});
it('should support typing into the hour segment in 24 hour time', function () {
- testInput('Hour', new Date(2019, 1, 3, 8), '1', new Date(2019, 1, 3, 1), false, {hour12: false});
- testInput('Hour', new Date(2019, 1, 3, 8), '11', new Date(2019, 1, 3, 11), true, {hour12: false});
- testInput('Hour', new Date(2019, 1, 3, 8), '23', new Date(2019, 1, 3, 23), true, {hour12: false});
+ testInput('Hour', new CalendarDateTime(2019, 2, 3, 8), '1', new CalendarDateTime(2019, 2, 3, 1), false, {hourCycle: 24});
+ testInput('Hour', new CalendarDateTime(2019, 2, 3, 8), '11', new CalendarDateTime(2019, 2, 3, 11), true, {hourCycle: 24});
+ testInput('Hour', new CalendarDateTime(2019, 2, 3, 8), '23', new CalendarDateTime(2019, 2, 3, 23), true, {hourCycle: 24});
});
it('should support typing into the minute segment', function () {
- testInput('Minute', new Date(2019, 1, 3, 8, 8), '1', new Date(2019, 1, 3, 8, 1), false);
- testInput('Minute', new Date(2019, 1, 3, 8, 8), '2', new Date(2019, 1, 3, 8, 2), false);
- testInput('Minute', new Date(2019, 1, 3, 8, 8), '5', new Date(2019, 1, 3, 8, 5), false);
- testInput('Minute', new Date(2019, 1, 3, 8, 8), '6', new Date(2019, 1, 3, 8, 6), true);
- testInput('Minute', new Date(2019, 1, 3, 8, 8), '59', new Date(2019, 1, 3, 8, 59), true);
+ testInput('Minute', new CalendarDateTime(2019, 2, 3, 8, 8), '1', new CalendarDateTime(2019, 2, 3, 8, 1), false);
+ testInput('Minute', new CalendarDateTime(2019, 2, 3, 8, 8), '2', new CalendarDateTime(2019, 2, 3, 8, 2), false);
+ testInput('Minute', new CalendarDateTime(2019, 2, 3, 8, 8), '5', new CalendarDateTime(2019, 2, 3, 8, 5), false);
+ testInput('Minute', new CalendarDateTime(2019, 2, 3, 8, 8), '6', new CalendarDateTime(2019, 2, 3, 8, 6), true);
+ testInput('Minute', new CalendarDateTime(2019, 2, 3, 8, 8), '59', new CalendarDateTime(2019, 2, 3, 8, 59), true);
});
it('should support typing into the second segment', function () {
- testInput('Second', new Date(2019, 1, 3, 8, 5, 8), '1', new Date(2019, 1, 3, 8, 5, 1), false);
- testInput('Second', new Date(2019, 1, 3, 8, 5, 8), '2', new Date(2019, 1, 3, 8, 5, 2), false);
- testInput('Second', new Date(2019, 1, 3, 8, 5, 8), '5', new Date(2019, 1, 3, 8, 5, 5), false);
- testInput('Second', new Date(2019, 1, 3, 8, 5, 8), '6', new Date(2019, 1, 3, 8, 5, 6), true);
- testInput('Second', new Date(2019, 1, 3, 8, 5, 8), '59', new Date(2019, 1, 3, 8, 5, 59), true);
+ testInput('Second', new CalendarDateTime(2019, 2, 3, 8, 5, 8), '1', new CalendarDateTime(2019, 2, 3, 8, 5, 1), false, {granularity: 'second'});
+ testInput('Second', new CalendarDateTime(2019, 2, 3, 8, 5, 8), '2', new CalendarDateTime(2019, 2, 3, 8, 5, 2), false, {granularity: 'second'});
+ testInput('Second', new CalendarDateTime(2019, 2, 3, 8, 5, 8), '5', new CalendarDateTime(2019, 2, 3, 8, 5, 5), false, {granularity: 'second'});
+ testInput('Second', new CalendarDateTime(2019, 2, 3, 8, 5, 8), '6', new CalendarDateTime(2019, 2, 3, 8, 5, 6), true, {granularity: 'second'});
+ testInput('Second', new CalendarDateTime(2019, 2, 3, 8, 5, 8), '59', new CalendarDateTime(2019, 2, 3, 8, 5, 59), true, {granularity: 'second'});
});
it('should support typing into the day period segment', function () {
- testInput('Day Period', new Date(2019, 1, 3, 8), 'p', new Date(2019, 1, 3, 20), false);
- testInput('Day Period', new Date(2019, 1, 3, 20), 'a', new Date(2019, 1, 3, 8), false);
+ testInput('Day Period', new CalendarDateTime(2019, 2, 3, 8), 'p', new CalendarDateTime(2019, 2, 3, 20), false);
+ testInput('Day Period', new CalendarDateTime(2019, 2, 3, 20), 'a', new CalendarDateTime(2019, 2, 3, 8), false);
});
it('should support entering arabic digits', function () {
- testInput('Year', new Date(2019, 1, 3), '٢٠٢٤', new Date(2024, 1, 3), true);
+ testInput('Year', new CalendarDate(2019, 2, 3), '٢٠٢٤', new CalendarDate(2024, 2, 3), false);
});
});
describe('backspace', function () {
function testBackspace(label, value, newValue, options) {
let onChange = jest.fn();
- let format = {
- year: 'numeric',
- month: 'numeric',
- day: 'numeric',
- hour: 'numeric',
- minute: 'numeric',
- second: 'numeric',
- ...options
- };
// Test controlled mode
- let {getByLabelText, unmount} = render();
+ let {getByLabelText, unmount} = render();
let segment = getByLabelText(label);
let textContent = segment.textContent;
act(() => {segment.focus();});
@@ -659,7 +769,7 @@ describe('DatePicker', function () {
// Test uncontrolled mode
onChange = jest.fn();
- ({getByLabelText, unmount} = render());
+ ({getByLabelText, unmount} = render());
segment = getByLabelText(label);
textContent = segment.textContent;
act(() => {segment.focus();});
@@ -672,54 +782,54 @@ describe('DatePicker', function () {
}
it('should support backspace in the month segment', function () {
- testBackspace('Month', new Date(2019, 1, 3), new Date(2019, 0, 3));
- testBackspace('Month', new Date(2019, 5, 3), new Date(2019, 0, 3));
- testBackspace('Month', new Date(2019, 11, 3), new Date(2019, 0, 3));
+ testBackspace('Month', new CalendarDate(2019, 2, 3), new CalendarDate(2019, 1, 3));
+ testBackspace('Month', new CalendarDate(2019, 6, 3), new CalendarDate(2019, 1, 3));
+ testBackspace('Month', new CalendarDate(2019, 12, 3), new CalendarDate(2019, 1, 3));
});
it('should support backspace in the day segment', function () {
- testBackspace('Day', new Date(2019, 1, 3), new Date(2019, 1, 1));
- testBackspace('Day', new Date(2019, 1, 20), new Date(2019, 1, 2));
+ testBackspace('Day', new CalendarDate(2019, 2, 3), new CalendarDate(2019, 2, 1));
+ testBackspace('Day', new CalendarDate(2019, 2, 20), new CalendarDate(2019, 2, 2));
});
it('should support backspace in the year segment', function () {
- testBackspace('Year', new Date(2019, 1, 3), new Date(201, 1, 3));
+ testBackspace('Year', new CalendarDate(2019, 2, 3), new CalendarDate(201, 2, 3));
});
it('should support backspace in the hour segment in 12 hour time', function () {
// AM
- testBackspace('Hour', new Date(2019, 1, 3, 8), new Date(2019, 1, 3, 0));
- testBackspace('Hour', new Date(2019, 1, 3, 11), new Date(2019, 1, 3, 1));
+ testBackspace('Hour', new CalendarDateTime(2019, 2, 3, 8), new CalendarDateTime(2019, 2, 3, 0));
+ testBackspace('Hour', new CalendarDateTime(2019, 2, 3, 11), new CalendarDateTime(2019, 2, 3, 1));
// PM
- testBackspace('Hour', new Date(2019, 1, 3, 16), new Date(2019, 1, 3, 12));
- testBackspace('Hour', new Date(2019, 1, 3, 23), new Date(2019, 1, 3, 13));
+ testBackspace('Hour', new CalendarDateTime(2019, 2, 3, 16), new CalendarDateTime(2019, 2, 3, 12));
+ testBackspace('Hour', new CalendarDateTime(2019, 2, 3, 23), new CalendarDateTime(2019, 2, 3, 13));
});
it('should support backspace in the hour segment in 24 hour time', function () {
- testBackspace('Hour', new Date(2019, 1, 3, 8), new Date(2019, 1, 3, 0), {hour12: false});
- testBackspace('Hour', new Date(2019, 1, 3, 11), new Date(2019, 1, 3, 1), {hour12: false});
- testBackspace('Hour', new Date(2019, 1, 3, 16), new Date(2019, 1, 3, 1), {hour12: false});
- testBackspace('Hour', new Date(2019, 1, 3, 23), new Date(2019, 1, 3, 2), {hour12: false});
+ testBackspace('Hour', new CalendarDateTime(2019, 2, 3, 8), new CalendarDateTime(2019, 2, 3, 0), {hourCycle: 24});
+ testBackspace('Hour', new CalendarDateTime(2019, 2, 3, 11), new CalendarDateTime(2019, 2, 3, 1), {hourCycle: 24});
+ testBackspace('Hour', new CalendarDateTime(2019, 2, 3, 16), new CalendarDateTime(2019, 2, 3, 1), {hourCycle: 24});
+ testBackspace('Hour', new CalendarDateTime(2019, 2, 3, 23), new CalendarDateTime(2019, 2, 3, 2), {hourCycle: 24});
});
it('should support backspace in the minute segment', function () {
- testBackspace('Minute', new Date(2019, 1, 3, 5, 8), new Date(2019, 1, 3, 5, 0));
- testBackspace('Minute', new Date(2019, 1, 3, 5, 25), new Date(2019, 1, 3, 5, 2));
- testBackspace('Minute', new Date(2019, 1, 3, 5, 59), new Date(2019, 1, 3, 5, 5));
+ testBackspace('Minute', new CalendarDateTime(2019, 2, 3, 5, 8), new CalendarDateTime(2019, 2, 3, 5, 0));
+ testBackspace('Minute', new CalendarDateTime(2019, 2, 3, 5, 25), new CalendarDateTime(2019, 2, 3, 5, 2));
+ testBackspace('Minute', new CalendarDateTime(2019, 2, 3, 5, 59), new CalendarDateTime(2019, 2, 3, 5, 5));
});
it('should support second in the minute segment', function () {
- testBackspace('Second', new Date(2019, 1, 3, 5, 5, 8), new Date(2019, 1, 3, 5, 5, 0));
- testBackspace('Second', new Date(2019, 1, 3, 5, 5, 25), new Date(2019, 1, 3, 5, 5, 2));
- testBackspace('Second', new Date(2019, 1, 3, 5, 5, 59), new Date(2019, 1, 3, 5, 5, 5));
+ testBackspace('Second', new CalendarDateTime(2019, 2, 3, 5, 5, 8), new CalendarDateTime(2019, 2, 3, 5, 5, 0), {granularity: 'second'});
+ testBackspace('Second', new CalendarDateTime(2019, 2, 3, 5, 5, 25), new CalendarDateTime(2019, 2, 3, 5, 5, 2), {granularity: 'second'});
+ testBackspace('Second', new CalendarDateTime(2019, 2, 3, 5, 5, 59), new CalendarDateTime(2019, 2, 3, 5, 5, 5), {granularity: 'second'});
});
it('should support backspace with arabic digits', function () {
let onChange = jest.fn();
let {getByLabelText} = render(
-
+
);
let segment = getByLabelText('العام');
@@ -728,7 +838,7 @@ describe('DatePicker', function () {
fireEvent.keyDown(segment, {key: 'Backspace'});
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange).toHaveBeenCalledWith(new Date(201, 1, 3));
+ expect(onChange).toHaveBeenCalledWith(new CalendarDate(201, 2, 3));
expect(segment).toHaveTextContent('٢٠١');
});
});
@@ -736,13 +846,13 @@ describe('DatePicker', function () {
describe('validation', function () {
it('should display an error icon when date is less than the minimum (controlled)', function () {
- let {getByTestId} = render();
+ let {getByTestId} = render();
expect(getByTestId('invalid-icon')).toBeVisible();
});
it('should display an error icon when date is less than the minimum (uncontrolled)', function () {
- let {getByTestId, getByLabelText} = render();
- expect(() => getByTestId('invalid-icon')).toThrow();
+ let {getByTestId, getByLabelText, queryByTestId} = render();
+ expect(queryByTestId('invalid-icon')).toBeNull();
let year = getByLabelText('Year');
fireEvent.keyDown(year, {key: 'ArrowDown'});
@@ -750,17 +860,17 @@ describe('DatePicker', function () {
expect(getByTestId('invalid-icon')).toBeVisible();
fireEvent.keyDown(year, {key: 'ArrowUp'});
- expect(() => getByTestId('invalid-icon')).toThrow();
+ expect(queryByTestId('invalid-icon')).toBeNull();
});
it('should display an error icon when date is greater than the maximum (controlled)', function () {
- let {getByTestId} = render();
+ let {getByTestId} = render();
expect(getByTestId('invalid-icon')).toBeVisible();
});
it('should display an error icon when date is greater than the maximum (uncontrolled)', function () {
- let {getByTestId, getByLabelText} = render();
- expect(() => getByTestId('invalid-icon')).toThrow();
+ let {getByTestId, getByLabelText, queryByTestId} = render();
+ expect(queryByTestId('invalid-icon')).toBeNull();
let year = getByLabelText('Year');
fireEvent.keyDown(year, {key: 'ArrowUp'});
@@ -768,41 +878,44 @@ describe('DatePicker', function () {
expect(getByTestId('invalid-icon')).toBeVisible();
fireEvent.keyDown(year, {key: 'ArrowDown'});
- expect(() => getByTestId('invalid-icon')).toThrow();
+ expect(queryByTestId('invalid-icon')).toBeNull();
});
});
describe('placeholder', function () {
it('should display a placeholder date if no value is provided', function () {
let onChange = jest.fn();
- let {getByRole} = render();
+ let {getAllByRole} = render();
- let combobox = getByRole('combobox');
- expect(combobox).toHaveTextContent(`1/1/${new Date().getFullYear()}`);
+ let combobox = getAllByRole('group')[0];
+ let today = new Intl.DateTimeFormat('en-US').format(new Date());
+ expect(combobox).toHaveTextContent(today);
});
it('should display a placeholder date if the value prop is null', function () {
let onChange = jest.fn();
- let {getByRole} = render();
+ let {getAllByRole} = render();
- let combobox = getByRole('combobox');
- expect(combobox).toHaveTextContent(`1/1/${new Date().getFullYear()}`);
+ let combobox = getAllByRole('group')[0];
+ let today = new Intl.DateTimeFormat('en-US').format(new Date());
+ expect(combobox).toHaveTextContent(today);
});
- it('should use the placeholderDate prop if provided', function () {
+ it('should use the placeholderValue prop if provided', function () {
let onChange = jest.fn();
- let {getByRole} = render();
+ let {getAllByRole} = render();
- let combobox = getByRole('combobox');
+ let combobox = getAllByRole('group')[0];
expect(combobox).toHaveTextContent('1/1/1980');
});
it('should confirm placeholder value with the enter key', function () {
let onChange = jest.fn();
- let {getByRole, getAllByRole} = render();
+ let {getAllByRole} = render();
- let combobox = getByRole('combobox');
- expect(combobox).toHaveTextContent(`1/1/${new Date().getFullYear()}`);
+ let combobox = getAllByRole('group')[0];
+ let todayStr = new Intl.DateTimeFormat('en-US').format(new Date());
+ expect(combobox).toHaveTextContent(todayStr);
let segments = getAllByRole('spinbutton');
act(() => {segments[0].focus();});
@@ -818,16 +931,17 @@ describe('DatePicker', function () {
fireEvent.keyDown(document.activeElement, {key: 'Enter'});
expect(segments[2]).toHaveFocus();
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange).toHaveBeenCalledWith(new Date(new Date().getFullYear(), 0, 1));
- expect(combobox).toHaveTextContent(`1/1/${new Date().getFullYear()}`);
+ expect(onChange).toHaveBeenCalledWith(today(getLocalTimeZone()));
+ expect(combobox).toHaveTextContent(todayStr);
});
it('should use arrow keys to modify placeholder (uncontrolled)', function () {
let onChange = jest.fn();
- let {getByRole, getAllByRole} = render();
+ let {getAllByRole} = render();
- let combobox = getByRole('combobox');
- expect(combobox).toHaveTextContent(`1/1/${new Date().getFullYear()}`);
+ let combobox = getAllByRole('group')[0];
+ let formatter = new Intl.DateTimeFormat('en-US');
+ expect(combobox).toHaveTextContent(formatter.format(new Date()));
let segments = getAllByRole('spinbutton');
act(() => {segments[0].focus();});
@@ -836,26 +950,30 @@ describe('DatePicker', function () {
fireEvent.keyDown(document.activeElement, {key: 'ArrowRight'});
expect(segments[1]).toHaveFocus();
expect(onChange).not.toHaveBeenCalled();
- expect(combobox).toHaveTextContent(`2/1/${new Date().getFullYear()}`);
+ let value = today(getLocalTimeZone()).cycle('month', 1);
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'});
fireEvent.keyDown(document.activeElement, {key: 'ArrowRight'});
expect(segments[2]).toHaveFocus();
expect(onChange).not.toHaveBeenCalled();
- expect(combobox).toHaveTextContent(`2/2/${new Date().getFullYear()}`);
+ value = value.cycle('day', 1);
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'});
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange).toHaveBeenCalledWith(new Date(new Date().getFullYear() + 1, 1, 2));
- expect(combobox).toHaveTextContent(`2/2/${new Date().getFullYear() + 1}`);
+ value = value.cycle('year', 1);
+ expect(onChange).toHaveBeenCalledWith(value);
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
});
it('should use arrow keys to modify placeholder (controlled)', function () {
let onChange = jest.fn();
- let {getByRole, getAllByRole, rerender} = render();
+ let {getAllByRole, rerender} = render();
- let combobox = getByRole('combobox');
- expect(combobox).toHaveTextContent(`1/1/${new Date().getFullYear()}`);
+ let combobox = getAllByRole('group')[0];
+ let formatter = new Intl.DateTimeFormat('en-US');
+ expect(combobox).toHaveTextContent(formatter.format(new Date()));
let segments = getAllByRole('spinbutton');
act(() => {segments[0].focus();});
@@ -864,88 +982,99 @@ describe('DatePicker', function () {
fireEvent.keyDown(document.activeElement, {key: 'ArrowRight'});
expect(segments[1]).toHaveFocus();
expect(onChange).not.toHaveBeenCalled();
- expect(combobox).toHaveTextContent(`2/1/${new Date().getFullYear()}`);
+ let value = today(getLocalTimeZone()).cycle('month', 1);
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'});
fireEvent.keyDown(document.activeElement, {key: 'ArrowRight'});
expect(segments[2]).toHaveFocus();
expect(onChange).not.toHaveBeenCalled();
- expect(combobox).toHaveTextContent(`2/2/${new Date().getFullYear()}`);
+ value = value.cycle('day', 1);
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'});
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange).toHaveBeenCalledWith(new Date(new Date().getFullYear() + 1, 1, 2));
- expect(combobox).toHaveTextContent(`2/2/${new Date().getFullYear()}`); // controlled
+ expect(onChange).toHaveBeenCalledWith(value.cycle('year', 1));
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone()))); // controlled
- rerender();
- expect(combobox).toHaveTextContent(`2/2/${new Date().getFullYear() + 1}`);
+ value = value.cycle('year', 1);
+ rerender();
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
});
it('should enter a date to modify placeholder (uncontrolled)', function () {
let onChange = jest.fn();
- let {getByRole, getAllByRole} = render();
+ let {getAllByRole} = render();
- let combobox = getByRole('combobox');
- expect(combobox).toHaveTextContent(`1/1/${new Date().getFullYear()}`);
+ let combobox = getAllByRole('group')[0];
+ let formatter = new Intl.DateTimeFormat('en-US');
+ expect(combobox).toHaveTextContent(formatter.format(new Date()));
let segments = getAllByRole('spinbutton');
act(() => {segments[0].focus();});
- fireEvent.keyDown(document.activeElement, {key: '4'});
+ beforeInput(document.activeElement, '4');
expect(segments[1]).toHaveFocus();
expect(onChange).not.toHaveBeenCalled();
- expect(combobox).toHaveTextContent(`4/1/${new Date().getFullYear()}`);
+ let value = today(getLocalTimeZone()).set({month: 4});
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
- fireEvent.keyDown(document.activeElement, {key: '5'});
+ beforeInput(document.activeElement, '5');
expect(segments[2]).toHaveFocus();
expect(onChange).not.toHaveBeenCalled();
- expect(combobox).toHaveTextContent(`4/5/${new Date().getFullYear()}`);
+ value = today(getLocalTimeZone()).set({month: 4, day: 5});
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
- fireEvent.keyDown(document.activeElement, {key: '2'});
+ beforeInput(document.activeElement, '2');
expect(onChange).toHaveBeenCalledTimes(1);
- fireEvent.keyDown(document.activeElement, {key: '0'});
+ beforeInput(document.activeElement, '0');
expect(onChange).toHaveBeenCalledTimes(2);
- fireEvent.keyDown(document.activeElement, {key: '2'});
+ beforeInput(document.activeElement, '2');
expect(onChange).toHaveBeenCalledTimes(3);
- fireEvent.keyDown(document.activeElement, {key: '0'});
+ beforeInput(document.activeElement, '0');
expect(segments[2]).toHaveFocus();
expect(onChange).toHaveBeenCalledTimes(4);
- expect(onChange).toHaveBeenCalledWith(new Date(2020, 3, 5));
- expect(combobox).toHaveTextContent('4/5/2020');
+ value = new CalendarDate(2020, 4, 5);
+ expect(onChange).toHaveBeenCalledWith(value);
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
});
it('should enter a date to modify placeholder (controlled)', function () {
let onChange = jest.fn();
- let {getByRole, getAllByRole, rerender} = render();
+ let {getAllByRole, rerender} = render();
- let combobox = getByRole('combobox');
- expect(combobox).toHaveTextContent(`1/1/${new Date().getFullYear()}`);
+ let combobox = getAllByRole('group')[0];
+ let formatter = new Intl.DateTimeFormat('en-US');
+ expect(combobox).toHaveTextContent(formatter.format(new Date()));
let segments = getAllByRole('spinbutton');
act(() => {segments[0].focus();});
- fireEvent.keyDown(document.activeElement, {key: '4'});
+ beforeInput(document.activeElement, '4');
expect(segments[1]).toHaveFocus();
expect(onChange).not.toHaveBeenCalled();
- expect(combobox).toHaveTextContent(`4/1/${new Date().getFullYear()}`);
+ let value = today(getLocalTimeZone()).set({month: 4});
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
- fireEvent.keyDown(document.activeElement, {key: '5'});
+ beforeInput(document.activeElement, '5');
expect(segments[2]).toHaveFocus();
expect(onChange).not.toHaveBeenCalled();
- expect(combobox).toHaveTextContent(`4/5/${new Date().getFullYear()}`);
+ value = today(getLocalTimeZone()).set({month: 4, day: 5});
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
- fireEvent.keyDown(document.activeElement, {key: '2'});
+ beforeInput(document.activeElement, '2');
expect(onChange).toHaveBeenCalledTimes(1);
- fireEvent.keyDown(document.activeElement, {key: '0'});
- fireEvent.keyDown(document.activeElement, {key: '2'});
- fireEvent.keyDown(document.activeElement, {key: '0'});
+ beforeInput(document.activeElement, '0');
+ beforeInput(document.activeElement, '2');
+ beforeInput(document.activeElement, '0');
expect(segments[2]).toHaveFocus();
expect(onChange).toHaveBeenCalledTimes(4);
- expect(onChange).toHaveBeenCalledWith(new Date(2020, 3, 5));
- expect(combobox).toHaveTextContent(`4/5/${new Date().getFullYear()}`); // controlled
+ expect(onChange).toHaveBeenCalledWith(new CalendarDate(2020, 4, 5));
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone()))); // controlled
- rerender();
- expect(combobox).toHaveTextContent('4/5/2020');
+ value = new CalendarDate(2020, 4, 5);
+ rerender();
+ expect(combobox).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
});
});
});
diff --git a/packages/@react-spectrum/datepicker/test/DatePickerBase.test.js b/packages/@react-spectrum/datepicker/test/DatePickerBase.test.js
index 498f22f8496..a1d51fe73dd 100644
--- a/packages/@react-spectrum/datepicker/test/DatePickerBase.test.js
+++ b/packages/@react-spectrum/datepicker/test/DatePickerBase.test.js
@@ -11,12 +11,25 @@
*/
import {act, fireEvent, render} from '@testing-library/react';
+import {CalendarDate, parseZonedDateTime} from '@internationalized/date';
import {DatePicker, DateRangePicker} from '../';
+import {installPointerEvent} from '@react-spectrum/test-utils';
import {Provider} from '@react-spectrum/provider';
import React from 'react';
import {theme} from '@react-spectrum/theme-default';
import {triggerPress} from '@react-spectrum/test-utils';
+function pointerEvent(type, opts) {
+ let evt = new Event(type, {bubbles: true, cancelable: true});
+ Object.assign(evt, {
+ ctrlKey: false,
+ metaKey: false,
+ shiftKey: false,
+ button: opts.button || 0
+ }, opts);
+ return evt;
+}
+
describe('DatePickerBase', function () {
describe('basics', function () {
it.each`
@@ -24,17 +37,22 @@ describe('DatePickerBase', function () {
${'DatePicker'} | ${DatePicker} | ${3}
${'DateRangePicker'} | ${DateRangePicker} | ${6}
`('$Name should render a default datepicker', ({Component, numSegments}) => {
- let {getByRole, getAllByRole} = render();
+ let {getAllByRole} = render();
- let combobox = getByRole('combobox');
+ let combobox = getAllByRole('group')[0];
expect(combobox).toBeVisible();
expect(combobox).not.toHaveAttribute('aria-disabled');
expect(combobox).not.toHaveAttribute('aria-invalid');
let segments = getAllByRole('spinbutton');
expect(segments.length).toBe(numSegments);
+ for (let segment of segments) {
+ expect(segment).not.toHaveAttribute('aria-disabled');
+ expect(segment).toHaveAttribute('contentEditable', 'true');
+ expect(segment).toHaveAttribute('inputMode', 'numeric');
+ }
- let button = getByRole('button');
+ let button = getAllByRole('button')[0];
expect(button).toBeVisible();
});
@@ -43,17 +61,19 @@ describe('DatePickerBase', function () {
${'DatePicker'} | ${DatePicker}
${'DateRangePicker'} | ${DateRangePicker}
`('$Name should set aria-disabled when isDisabled', ({Component}) => {
- let {getByRole, getAllByRole} = render();
+ let {getAllByRole} = render();
- let combobox = getByRole('combobox');
+ let combobox = getAllByRole('group')[0];
expect(combobox).toHaveAttribute('aria-disabled', 'true');
let segments = getAllByRole('spinbutton');
for (let segment of segments) {
expect(segment).toHaveAttribute('aria-disabled', 'true');
+ expect(segment).not.toHaveAttribute('contentEditable', 'true');
+ expect(segment).not.toHaveAttribute('inputMode', 'numeric');
}
- let button = getByRole('button');
+ let button = getAllByRole('button')[0];
expect(button).toHaveAttribute('disabled');
});
@@ -62,17 +82,17 @@ describe('DatePickerBase', function () {
${'DatePicker'} | ${DatePicker}
${'DateRangePicker'} | ${DateRangePicker}
`('$Name should set aria-readonly when isReadOnly', ({Component}) => {
- let {getByRole, getAllByRole} = render();
+ let {getAllByRole} = render();
- let combobox = getByRole('combobox');
- expect(combobox).toHaveAttribute('aria-readonly', 'true');
+ let combobox = getAllByRole('group')[0];
+ expect(combobox).not.toHaveAttribute('aria-readonly', 'true');
let segments = getAllByRole('spinbutton');
for (let segment of segments) {
expect(segment).toHaveAttribute('aria-readonly', 'true');
}
- let button = getByRole('button');
+ let button = getAllByRole('button')[0];
expect(button).toHaveAttribute('disabled');
});
@@ -81,10 +101,10 @@ describe('DatePickerBase', function () {
${'DatePicker'} | ${DatePicker}
${'DateRangePicker'} | ${DateRangePicker}
`('$Name should set aria-required when isRequired', ({Component}) => {
- let {getByRole, getAllByRole} = render();
+ let {getAllByRole} = render();
- let combobox = getByRole('combobox');
- expect(combobox).toHaveAttribute('aria-required', 'true');
+ let combobox = getAllByRole('group')[0];
+ expect(combobox).not.toHaveAttribute('aria-required', 'true');
let segments = getAllByRole('spinbutton');
for (let segment of segments) {
@@ -97,10 +117,15 @@ describe('DatePickerBase', function () {
${'DatePicker'} | ${DatePicker}
${'DateRangePicker'} | ${DateRangePicker}
`('$Name should set aria-invalid when validationState="invalid"', ({Component}) => {
- let {getByRole} = render();
+ let {getAllByRole} = render();
+
+ let combobox = getAllByRole('group')[0];
+ expect(combobox).not.toHaveAttribute('aria-invalid', 'true');
- let combobox = getByRole('combobox');
- expect(combobox).toHaveAttribute('aria-invalid', 'true');
+ let segments = getAllByRole('spinbutton');
+ for (let segment of segments) {
+ expect(segment).toHaveAttribute('aria-invalid', 'true');
+ }
});
});
@@ -110,29 +135,38 @@ describe('DatePickerBase', function () {
${'DatePicker'} | ${DatePicker}
${'DateRangePicker'} | ${DateRangePicker}
`('$Name should open a calendar popover when clicking the button', ({Component}) => {
- let {getByRole} = render(
+ let {getAllByRole} = render(
-
+
);
- let combobox = getByRole('combobox');
- expect(combobox).toHaveAttribute('aria-haspopup', 'dialog');
+ let combobox = getAllByRole('group')[0];
+ expect(combobox).not.toHaveAttribute('aria-haspopup', 'dialog');
expect(combobox).not.toHaveAttribute('aria-owns');
- let button = getByRole('button');
+ let segments = getAllByRole('spinbutton');
+ for (let segment of segments) {
+ // expect(segment).toHaveAttribute('aria-haspopup', 'dialog');
+ expect(segment).not.toHaveAttribute('aria-expanded');
+ expect(segment).not.toHaveAttribute('aria-controls');
+ }
+
+ let button = getAllByRole('button')[0];
expect(button).toHaveAttribute('aria-haspopup', 'dialog');
+ expect(button).toHaveAttribute('aria-expanded', 'false');
expect(button).not.toHaveAttribute('aria-controls');
triggerPress(button);
- let dialog = getByRole('dialog');
+ let dialog = getAllByRole('dialog')[0];
expect(dialog).toBeVisible();
expect(dialog).toHaveAttribute('id');
let dialogId = dialog.getAttribute('id');
- expect(combobox).toHaveAttribute('aria-expanded', 'true');
- expect(combobox).toHaveAttribute('aria-owns', dialogId);
+ // for (let segment of segments) {
+ // expect(segment).toHaveAttribute('aria-controls', dialogId);
+ // }
expect(button).toHaveAttribute('aria-expanded', 'true');
expect(button).toHaveAttribute('aria-controls', dialogId);
@@ -146,29 +180,37 @@ describe('DatePickerBase', function () {
${'DatePicker'} | ${DatePicker}
${'DateRangePicker'} | ${DateRangePicker}
`('$Name should open a calendar popover pressing Alt + ArrowDown on the keyboard', ({Component}) => {
- let {getByRole} = render(
+ let {getAllByRole} = render(
-
+
);
- let combobox = getByRole('combobox');
- expect(combobox).toHaveAttribute('aria-haspopup', 'dialog');
+ let combobox = getAllByRole('group')[0];
+ expect(combobox).not.toHaveAttribute('aria-haspopup', 'dialog');
expect(combobox).not.toHaveAttribute('aria-owns');
- let button = getByRole('button');
+ let segments = getAllByRole('spinbutton');
+ for (let segment of segments) {
+ // expect(segment).toHaveAttribute('aria-haspopup', 'dialog');
+ expect(segment).not.toHaveAttribute('aria-expanded');
+ expect(segment).not.toHaveAttribute('aria-controls');
+ }
+
+ let button = getAllByRole('button')[0];
expect(button).toHaveAttribute('aria-haspopup', 'dialog');
expect(button).not.toHaveAttribute('aria-controls');
fireEvent.keyDown(combobox, {key: 'ArrowDown', altKey: true});
- let dialog = getByRole('dialog');
+ let dialog = getAllByRole('dialog')[0];
expect(dialog).toBeVisible();
expect(dialog).toHaveAttribute('id');
let dialogId = dialog.getAttribute('id');
- expect(combobox).toHaveAttribute('aria-expanded', 'true');
- expect(combobox).toHaveAttribute('aria-owns', dialogId);
+ // for (let segment of segments) {
+ // expect(segment).toHaveAttribute('aria-controls', dialogId);
+ // }
expect(button).toHaveAttribute('aria-expanded', 'true');
expect(button).toHaveAttribute('aria-controls', dialogId);
@@ -177,35 +219,43 @@ describe('DatePickerBase', function () {
expect(document.activeElement.parentElement).toHaveAttribute('role', 'gridcell');
});
- it.each`
+ it.skip.each`
Name | Component
${'DatePicker'} | ${DatePicker}
${'DateRangePicker'} | ${DateRangePicker}
`('$Name should open a calendar popover when tapping on the date field with a touch device', ({Component}) => {
- let {getByRole} = render(
+ let {getAllByRole} = render(
-
+
);
- let combobox = getByRole('combobox');
- expect(combobox).toHaveAttribute('aria-haspopup', 'dialog');
+ let combobox = getAllByRole('group')[0];
+ expect(combobox).not.toHaveAttribute('aria-haspopup', 'dialog');
expect(combobox).not.toHaveAttribute('aria-owns');
- let button = getByRole('button');
+ let segments = getAllByRole('spinbutton');
+ for (let segment of segments) {
+ // expect(segment).toHaveAttribute('aria-haspopup', 'dialog');
+ expect(segment).not.toHaveAttribute('aria-expanded');
+ expect(segment).not.toHaveAttribute('aria-controls');
+ }
+
+ let button = getAllByRole('button')[0];
expect(button).toHaveAttribute('aria-haspopup', 'dialog');
expect(button).not.toHaveAttribute('aria-controls');
fireEvent.touchStart(combobox, {targetTouches: [{identifier: 1}]});
fireEvent.touchEnd(combobox, {changedTouches: [{identifier: 1, clientX: 0, clientY: 0}]});
- let dialog = getByRole('dialog');
+ let dialog = getAllByRole('dialog')[0];
expect(dialog).toBeVisible();
expect(dialog).toHaveAttribute('id');
let dialogId = dialog.getAttribute('id');
- expect(combobox).toHaveAttribute('aria-expanded', 'true');
- expect(combobox).toHaveAttribute('aria-owns', dialogId);
+ // for (let segment of segments) {
+ // expect(segment).toHaveAttribute('aria-controls', dialogId);
+ // }
expect(button).toHaveAttribute('aria-expanded', 'true');
expect(button).toHaveAttribute('aria-controls', dialogId);
@@ -216,12 +266,14 @@ describe('DatePickerBase', function () {
});
describe('focus management', function () {
+ installPointerEvent();
+
it.each`
Name | Component
${'DatePicker'} | ${DatePicker}
${'DateRangePicker'} | ${DateRangePicker}
`('$Name should support arrow keys to move between segments', ({Component}) => {
- let {getAllByRole} = render();
+ let {getAllByRole} = render();
let segments = getAllByRole('spinbutton');
act(() => {segments[0].focus();});
@@ -244,7 +296,7 @@ describe('DatePickerBase', function () {
`('$Name should support arrow keys to move between segments in an RTL locale', ({Component}) => {
let {getAllByRole} = render(
-
+
);
@@ -267,11 +319,11 @@ describe('DatePickerBase', function () {
${'DatePicker'} | ${DatePicker}
${'DateRangePicker'} | ${DateRangePicker}
`('$Name should focus the next segment on mouse down on a literal segment', ({Component}) => {
- let {getAllByRole, getAllByText} = render();
+ let {getAllByRole, getAllByText} = render();
let literals = getAllByText('/');
let segments = getAllByRole('spinbutton');
- fireEvent.mouseDown(literals[0]);
+ fireEvent(literals[0], pointerEvent('pointerdown', {pointerId: 1, pointerType: 'mouse'}));
expect(segments[1]).toHaveFocus();
});
@@ -280,25 +332,10 @@ describe('DatePickerBase', function () {
${'DatePicker'} | ${DatePicker}
${'DateRangePicker'} | ${DateRangePicker}
`('$Name should focus the next segment on mouse down on a non-editable segment', ({Component}) => {
- let format = {
- weekday: 'long',
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- hour: 'numeric',
- minute: 'numeric',
- second: 'numeric',
- timeZoneName: 'short',
- era: 'short'
- };
-
- let {getAllByTestId} = render();
-
- fireEvent.mouseDown(getAllByTestId('weekday')[0]);
- expect(getAllByTestId('month')[0]).toHaveFocus();
-
- fireEvent.mouseDown(getAllByTestId('era')[0]);
- expect(getAllByTestId('hour')[0]).toHaveFocus();
+ let {getAllByTestId} = render();
+
+ fireEvent(getAllByTestId('timeZoneName').pop(), pointerEvent('pointerdown', {pointerId: 1, pointerType: 'mouse'}));
+ expect(getAllByTestId('dayPeriod').pop()).toHaveFocus();
});
it.each`
@@ -306,7 +343,7 @@ describe('DatePickerBase', function () {
${'DatePicker'} | ${DatePicker}
${'DateRangePicker'} | ${DateRangePicker}
`('$Name should not be focusable when isDisabled', ({Component}) => {
- let {getAllByRole} = render();
+ let {getAllByRole} = render();
let segments = getAllByRole('spinbutton');
for (let segment of segments) {
expect(segment).not.toHaveAttribute('tabIndex');
@@ -318,7 +355,7 @@ describe('DatePickerBase', function () {
${'DatePicker'} | ${DatePicker}
${'DateRangePicker'} | ${DateRangePicker}
`('$Name should focus the first segment by default if autoFocus is set', ({Component}) => {
- let {getAllByRole} = render();
+ let {getAllByRole} = render();
let segments = getAllByRole('spinbutton');
expect(segments[0]).toHaveFocus();
@@ -331,7 +368,7 @@ describe('DatePickerBase', function () {
${'DatePicker'} | ${DatePicker}
${'DateRangePicker'} | ${DateRangePicker}
`('$Name should display an error icon when validationState="invalid"', ({Component}) => {
- let {getByTestId} = render();
+ let {getByTestId} = render();
expect(getByTestId('invalid-icon')).toBeVisible();
});
@@ -340,7 +377,7 @@ describe('DatePickerBase', function () {
${'DatePicker'} | ${DatePicker}
${'DateRangePicker'} | ${DateRangePicker}
`('$Name should display an checkmark icon when validationState="valid"', ({Component}) => {
- let {getByTestId} = render();
+ let {getByTestId} = render();
expect(getByTestId('valid-icon')).toBeVisible();
});
});
diff --git a/packages/@react-spectrum/datepicker/test/DateRangePicker.test.js b/packages/@react-spectrum/datepicker/test/DateRangePicker.test.js
index 178279ab9cc..a32379eb14f 100644
--- a/packages/@react-spectrum/datepicker/test/DateRangePicker.test.js
+++ b/packages/@react-spectrum/datepicker/test/DateRangePicker.test.js
@@ -10,13 +10,32 @@
* governing permissions and limitations under the License.
*/
-import {act, fireEvent, getAllByRole as getAllByRoleInContainer, render} from '@testing-library/react';
+import {act, fireEvent, getAllByRole as getAllByRoleInContainer, render, within} from '@testing-library/react';
+import {CalendarDate, CalendarDateTime, getLocalTimeZone, toCalendarDateTime, today} from '@internationalized/date';
import {DateRangePicker} from '../';
import {Provider} from '@react-spectrum/provider';
import React from 'react';
import {theme} from '@react-spectrum/theme-default';
import {triggerPress} from '@react-spectrum/test-utils';
+function pointerEvent(type, opts) {
+ let evt = new Event(type, {bubbles: true, cancelable: true});
+ Object.assign(evt, {
+ ctrlKey: false,
+ metaKey: false,
+ shiftKey: false,
+ button: opts.button || 0
+ }, opts);
+ return evt;
+}
+
+function beforeInput(target, key) {
+ // JSDOM doesn't support the beforeinput event
+ let e = new InputEvent('beforeinput', {cancelable: true, data: key});
+ e.inputType = 'insertText';
+ fireEvent(target, e);
+}
+
describe('DateRangePicker', function () {
// there are live announcers, we need to be able to get rid of them after each test or get a warning in the console about act()
beforeAll(() => jest.useFakeTimers());
@@ -28,9 +47,9 @@ describe('DateRangePicker', function () {
});
describe('basics', function () {
it('should render a DateRangePicker with a specified date range', function () {
- let {getByRole, getAllByRole} = render();
+ let {getAllByRole} = render();
- let combobox = getByRole('combobox');
+ let combobox = getAllByRole('group')[0];
expect(combobox).toBeVisible();
expect(combobox).not.toHaveAttribute('aria-disabled');
expect(combobox).not.toHaveAttribute('aria-invalid');
@@ -41,7 +60,7 @@ describe('DateRangePicker', function () {
expect(segments[0].textContent).toBe('2');
expect(segments[0].getAttribute('aria-label')).toBe('Month');
expect(segments[0].getAttribute('aria-valuenow')).toBe('2');
- expect(segments[0].getAttribute('aria-valuetext')).toBe('February');
+ expect(segments[0].getAttribute('aria-valuetext')).toBe('2 − February');
expect(segments[0].getAttribute('aria-valuemin')).toBe('1');
expect(segments[0].getAttribute('aria-valuemax')).toBe('12');
@@ -62,7 +81,7 @@ describe('DateRangePicker', function () {
expect(segments[3].textContent).toBe('5');
expect(segments[3].getAttribute('aria-label')).toBe('Month');
expect(segments[3].getAttribute('aria-valuenow')).toBe('5');
- expect(segments[3].getAttribute('aria-valuetext')).toBe('May');
+ expect(segments[3].getAttribute('aria-valuetext')).toBe('5 − May');
expect(segments[3].getAttribute('aria-valuemin')).toBe('1');
expect(segments[3].getAttribute('aria-valuemax')).toBe('12');
@@ -81,18 +100,10 @@ describe('DateRangePicker', function () {
expect(segments[5].getAttribute('aria-valuemax')).toBe('9999');
});
- it('should render a DateRangePicker with a custom date format', function () {
- let format = {
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- hour: 'numeric',
- minute: 'numeric',
- second: 'numeric'
- };
- let {getByRole, getAllByRole} = render();
-
- let combobox = getByRole('combobox');
+ it('should render a DateRangePicker granularity="second"', function () {
+ let {getAllByRole} = render();
+
+ let combobox = getAllByRole('group')[0];
expect(combobox).toBeVisible();
expect(combobox).not.toHaveAttribute('aria-disabled');
expect(combobox).not.toHaveAttribute('aria-invalid');
@@ -100,10 +111,10 @@ describe('DateRangePicker', function () {
let segments = getAllByRole('spinbutton');
expect(segments.length).toBe(14);
- expect(segments[0].textContent).toBe('February');
+ expect(segments[0].textContent).toBe('2');
expect(segments[0].getAttribute('aria-label')).toBe('Month');
expect(segments[0].getAttribute('aria-valuenow')).toBe('2');
- expect(segments[0].getAttribute('aria-valuetext')).toBe('February');
+ expect(segments[0].getAttribute('aria-valuetext')).toBe('2 − February');
expect(segments[0].getAttribute('aria-valuemin')).toBe('1');
expect(segments[0].getAttribute('aria-valuemax')).toBe('12');
@@ -146,10 +157,10 @@ describe('DateRangePicker', function () {
expect(segments[6].getAttribute('aria-label')).toBe('Day Period');
expect(segments[6].getAttribute('aria-valuetext')).toBe('12 AM');
- expect(segments[7].textContent).toBe('May');
+ expect(segments[7].textContent).toBe('5');
expect(segments[7].getAttribute('aria-label')).toBe('Month');
expect(segments[7].getAttribute('aria-valuenow')).toBe('5');
- expect(segments[7].getAttribute('aria-valuetext')).toBe('May');
+ expect(segments[7].getAttribute('aria-valuetext')).toBe('5 − May');
expect(segments[7].getAttribute('aria-valuemin')).toBe('1');
expect(segments[7].getAttribute('aria-valuemax')).toBe('12');
@@ -199,7 +210,7 @@ describe('DateRangePicker', function () {
let onChange = jest.fn();
let {getByRole, getAllByRole, getByLabelText} = render(
-
+
);
@@ -223,81 +234,242 @@ describe('DateRangePicker', function () {
expect(dialog).not.toBeInTheDocument();
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange).toHaveBeenCalledWith({start: new Date(2019, 1, 10), end: new Date(2019, 1, 17)});
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2019, 2, 10), end: new CalendarDate(2019, 2, 17)});
expect(startDate).toHaveTextContent('2/10/2019'); // uncontrolled
expect(endDate).toHaveTextContent('2/17/2019');
});
+
+ it('should display time fields when a CalendarDateTime value is used', function () {
+ let onChange = jest.fn();
+ let {getByRole, getAllByRole, getByLabelText, getAllByLabelText} = render(
+
+
+
+ );
+
+ let startDate = getByLabelText('Start Date');
+ let endDate = getByLabelText('End Date');
+ expect(startDate).toHaveTextContent('2/3/2019, 8:45 AM');
+ expect(endDate).toHaveTextContent('5/6/2019, 10:45 AM');
+
+ let button = getByRole('button');
+ triggerPress(button);
+
+ let dialog = getByRole('dialog');
+ expect(dialog).toBeVisible();
+
+ let cells = getAllByRole('gridcell');
+ let selected = cells.find(cell => cell.getAttribute('aria-selected') === 'true');
+ expect(selected.children[0]).toHaveAttribute('aria-label', 'Sunday, February 3, 2019 selected (Click to start selecting date range)');
+
+ let startTimeField = getAllByLabelText('Start time')[0];
+ expect(startTimeField).toHaveTextContent('8:45 AM');
+
+ let endTimeField = getAllByLabelText('End time')[0];
+ expect(endTimeField).toHaveTextContent('10:45 AM');
+
+ // selecting a date should not close the popover
+ triggerPress(getByLabelText('Sunday, February 10, 2019 selected'));
+ triggerPress(getByLabelText('Sunday, February 17, 2019'));
+
+ expect(dialog).toBeVisible();
+ expect(onChange).toHaveBeenCalledTimes(1);
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDateTime(2019, 2, 10, 8, 45), end: new CalendarDateTime(2019, 2, 17, 10, 45)});
+ expect(startDate).toHaveTextContent('2/10/2019, 8:45 AM');
+ expect(endDate).toHaveTextContent('2/17/2019, 10:45 AM');
+
+ let hour = within(startTimeField).getByLabelText('Hour');
+ expect(hour).toHaveAttribute('role', 'spinbutton');
+ expect(hour).toHaveAttribute('aria-valuetext', '8 AM');
+
+ act(() => hour.focus());
+ fireEvent.keyDown(hour, {key: 'ArrowUp'});
+ fireEvent.keyUp(hour, {key: 'ArrowUp'});
+
+ expect(hour).toHaveAttribute('aria-valuetext', '9 AM');
+
+ expect(dialog).toBeVisible();
+ expect(onChange).toHaveBeenCalledTimes(2);
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDateTime(2019, 2, 10, 9, 45), end: new CalendarDateTime(2019, 2, 17, 10, 45)});
+ expect(startDate).toHaveTextContent('2/10/2019, 9:45 AM');
+ expect(endDate).toHaveTextContent('2/17/2019, 10:45 AM');
+
+ hour = within(endTimeField).getByLabelText('Hour');
+ expect(hour).toHaveAttribute('role', 'spinbutton');
+ expect(hour).toHaveAttribute('aria-valuetext', '10 AM');
+
+ act(() => hour.focus());
+ fireEvent.keyDown(hour, {key: 'ArrowUp'});
+ fireEvent.keyUp(hour, {key: 'ArrowUp'});
+
+ expect(hour).toHaveAttribute('aria-valuetext', '11 AM');
+
+ expect(dialog).toBeVisible();
+ expect(onChange).toHaveBeenCalledTimes(3);
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDateTime(2019, 2, 10, 9, 45), end: new CalendarDateTime(2019, 2, 17, 11, 45)});
+ expect(startDate).toHaveTextContent('2/10/2019, 9:45 AM');
+ expect(endDate).toHaveTextContent('2/17/2019, 11:45 AM');
+ });
+
+ it('should not fire onChange until both date range and time range are selected', function () {
+ let onChange = jest.fn();
+ let {getByRole, getAllByRole, getByLabelText, getAllByLabelText} = render(
+
+
+
+ );
+
+ let formatter = new Intl.DateTimeFormat('en-US', {year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric'});
+ let placeholder = formatter.format(toCalendarDateTime(today(getLocalTimeZone())).set({hour: 12, minute: 0}).toDate(getLocalTimeZone()));
+ let startDate = getByLabelText('Start Date');
+ let endDate = getByLabelText('End Date');
+ expect(startDate).toHaveTextContent(placeholder);
+ expect(endDate).toHaveTextContent(placeholder);
+
+ let button = getByRole('button');
+ triggerPress(button);
+
+ let dialog = getByRole('dialog');
+ expect(dialog).toBeVisible();
+
+ let cells = getAllByRole('gridcell');
+ let selected = cells.find(cell => cell.getAttribute('aria-selected') === 'true');
+ expect(selected).toBeUndefined();
+
+ let startTimeField = getAllByLabelText('Start time')[0];
+ expect(startTimeField).toHaveTextContent('12:00 PM');
+
+ let endTimeField = getAllByLabelText('End time')[0];
+ expect(endTimeField).toHaveTextContent('12:00 PM');
+
+ // selecting a date should not close the popover
+ let enabledCells = cells.filter(cell => !cell.hasAttribute('aria-disabled'));
+ triggerPress(enabledCells[0].firstChild);
+ triggerPress(enabledCells[1].firstChild);
+
+ expect(dialog).toBeVisible();
+ expect(onChange).not.toHaveBeenCalled();
+ expect(startDate).toHaveTextContent(placeholder);
+ expect(endDate).toHaveTextContent(placeholder);
+
+ for (let timeField of [startTimeField, endTimeField]) {
+ let hour = within(timeField).getByLabelText('Hour');
+ expect(hour).toHaveAttribute('role', 'spinbutton');
+ expect(hour).toHaveAttribute('aria-valuetext', '12 PM');
+
+ act(() => hour.focus());
+ fireEvent.keyDown(hour, {key: 'ArrowUp'});
+ fireEvent.keyUp(hour, {key: 'ArrowUp'});
+
+ expect(hour).toHaveAttribute('aria-valuetext', '1 PM');
+
+ expect(onChange).not.toHaveBeenCalled();
+ expect(startDate).toHaveTextContent(placeholder);
+ expect(endDate).toHaveTextContent(placeholder);
+
+ fireEvent.keyDown(hour, {key: 'ArrowRight'});
+ fireEvent.keyUp(hour, {key: 'ArrowRight'});
+
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Minute');
+ expect(document.activeElement).toHaveAttribute('aria-valuetext', '00');
+ fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'});
+ fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'});
+
+ expect(document.activeElement).toHaveAttribute('aria-valuetext', '01');
+
+ expect(onChange).not.toHaveBeenCalled();
+ expect(startDate).toHaveTextContent(placeholder);
+ expect(endDate).toHaveTextContent(placeholder);
+
+ fireEvent.keyDown(hour, {key: 'ArrowRight'});
+ fireEvent.keyUp(hour, {key: 'ArrowRight'});
+
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Day Period');
+ expect(document.activeElement).toHaveAttribute('aria-valuetext', '1 PM');
+
+ fireEvent.keyDown(document.activeElement, {key: 'Enter'});
+ fireEvent.keyUp(document.activeElement, {key: 'Enter'});
+ }
+
+ expect(dialog).toBeVisible();
+ expect(onChange).toHaveBeenCalledTimes(1);
+ let startValue = toCalendarDateTime(today(getLocalTimeZone())).set({day: 1, hour: 13, minute: 1});
+ let endValue = toCalendarDateTime(today(getLocalTimeZone())).set({day: 2, hour: 13, minute: 1});
+ expect(onChange).toHaveBeenCalledWith({start: startValue, end: endValue});
+ expect(startDate).toHaveTextContent(formatter.format(startValue.toDate(getLocalTimeZone())));
+ expect(endDate).toHaveTextContent(formatter.format(endValue.toDate(getLocalTimeZone())));
+ });
});
describe('labeling', function () {
- it('should support labeling with a default label', function () {
- let {getByRole, getByLabelText} = render();
+ it('should support labeling', function () {
+ let {getAllByRole, getByLabelText, getByText} = render();
+
+ let label = getByText('Date range');
- let combobox = getByRole('combobox');
- expect(combobox).toHaveAttribute('aria-label', 'Date Range');
- expect(combobox).toHaveAttribute('id');
+ let combobox = getAllByRole('group')[0];
+ expect(combobox).toHaveAttribute('aria-labelledby', label.id);
let startDate = getByLabelText('Start Date');
- expect(startDate).toHaveAttribute('aria-labelledby', `${combobox.id} ${startDate.id}`);
+ expect(startDate).toHaveAttribute('aria-labelledby', `${label.id} ${startDate.id}`);
let endDate = getByLabelText('End Date');
- expect(endDate).toHaveAttribute('aria-labelledby', `${combobox.id} ${endDate.id}`);
+ expect(endDate).toHaveAttribute('aria-labelledby', `${label.id} ${endDate.id}`);
- let button = getByRole('button');
+ let button = getAllByRole('button')[0];
expect(button).toHaveAttribute('aria-label', 'Calendar');
expect(button).toHaveAttribute('id');
- expect(button).toHaveAttribute('aria-labelledby', `${combobox.id} ${button.id}`);
+ expect(button).toHaveAttribute('aria-labelledby', `${label.id} ${button.id}`);
let startSegments = getAllByRoleInContainer(startDate, 'spinbutton');
for (let segment of startSegments) {
expect(segment).toHaveAttribute('id');
- expect(segment).toHaveAttribute('aria-labelledby', `${combobox.id} ${startDate.id} ${segment.id}`);
+ expect(segment).toHaveAttribute('aria-labelledby', `${label.id} ${startDate.id} ${segment.id}`);
}
let endSegments = getAllByRoleInContainer(endDate, 'spinbutton');
for (let segment of endSegments) {
expect(segment).toHaveAttribute('id');
- expect(segment).toHaveAttribute('aria-labelledby', `${combobox.id} ${endDate.id} ${segment.id}`);
+ expect(segment).toHaveAttribute('aria-labelledby', `${label.id} ${endDate.id} ${segment.id}`);
}
});
it('should support labeling with aria-label', function () {
- let {getByRole, getByLabelText} = render();
+ let {getAllByRole, getByLabelText} = render();
- let combobox = getByRole('combobox');
- expect(combobox).toHaveAttribute('aria-label', 'Birth date');
- expect(combobox).toHaveAttribute('id');
+ let field = getAllByRole('group')[0];
+ expect(field).toHaveAttribute('aria-label', 'Birth date');
+ expect(field).toHaveAttribute('id');
let startDate = getByLabelText('Start Date');
- expect(startDate).toHaveAttribute('aria-labelledby', `${combobox.id} ${startDate.id}`);
+ expect(startDate).toHaveAttribute('aria-labelledby', `${field.id} ${startDate.id}`);
let endDate = getByLabelText('End Date');
- expect(endDate).toHaveAttribute('aria-labelledby', `${combobox.id} ${endDate.id}`);
+ expect(endDate).toHaveAttribute('aria-labelledby', `${field.id} ${endDate.id}`);
- let button = getByRole('button');
+ let button = getAllByRole('button')[0];
expect(button).toHaveAttribute('aria-label', 'Calendar');
expect(button).toHaveAttribute('id');
- expect(button).toHaveAttribute('aria-labelledby', `${combobox.id} ${button.id}`);
+ expect(button).toHaveAttribute('aria-labelledby', `${field.id} ${button.id}`);
let startSegments = getAllByRoleInContainer(startDate, 'spinbutton');
for (let segment of startSegments) {
expect(segment).toHaveAttribute('id');
- expect(segment).toHaveAttribute('aria-labelledby', `${combobox.id} ${startDate.id} ${segment.id}`);
+ expect(segment).toHaveAttribute('aria-labelledby', `${field.id} ${startDate.id} ${segment.id}`);
}
let endSegments = getAllByRoleInContainer(endDate, 'spinbutton');
for (let segment of endSegments) {
expect(segment).toHaveAttribute('id');
- expect(segment).toHaveAttribute('aria-labelledby', `${combobox.id} ${endDate.id} ${segment.id}`);
+ expect(segment).toHaveAttribute('aria-labelledby', `${field.id} ${endDate.id} ${segment.id}`);
}
});
it('should support labeling with aria-labelledby', function () {
- let {getByRole, getByLabelText} = render();
+ let {getAllByRole, getByLabelText} = render();
- let combobox = getByRole('combobox');
- expect(combobox).not.toHaveAttribute('aria-label');
- expect(combobox).toHaveAttribute('aria-labelledby', 'foo');
+ let field = getAllByRole('group')[0];
+ expect(field).toHaveAttribute('aria-labelledby', 'foo');
let startDate = getByLabelText('Start Date');
expect(startDate).toHaveAttribute('aria-labelledby', `foo ${startDate.id}`);
@@ -305,7 +477,7 @@ describe('DateRangePicker', function () {
let endDate = getByLabelText('End Date');
expect(endDate).toHaveAttribute('aria-labelledby', `foo ${endDate.id}`);
- let button = getByRole('button');
+ let button = getAllByRole('button')[0];
expect(button).toHaveAttribute('aria-label', 'Calendar');
expect(button).toHaveAttribute('id');
expect(button).toHaveAttribute('aria-labelledby', `foo ${button.id}`);
@@ -326,26 +498,29 @@ describe('DateRangePicker', function () {
describe('focus management', function () {
it('should focus the first segment of each field on mouse down', function () {
- let {getByLabelText} = render();
+ let {getByLabelText} = render();
let startDate = getByLabelText('Start Date');
let endDate = getByLabelText('End Date');
let startSegments = getAllByRoleInContainer(startDate, 'spinbutton');
let endSegments = getAllByRoleInContainer(endDate, 'spinbutton');
- fireEvent.mouseDown(startDate);
- expect(startSegments[0]).toHaveFocus();
+ triggerPress(startDate);
+ expect(startSegments[startSegments.length - 1]).toHaveFocus();
- fireEvent.mouseDown(endDate);
- expect(endSegments[0]).toHaveFocus();
+ act(() => document.activeElement.blur());
+
+ triggerPress(endDate);
+ expect(endSegments[endSegments.length - 1]).toHaveFocus();
});
it('should focus the first segment of the end date on mouse down on the dash', function () {
- let {getByTestId, getByLabelText} = render();
+ let {getByTestId, getByLabelText} = render();
let rangeDash = getByTestId('date-range-dash');
let endDate = getByLabelText('End Date');
let endSegments = getAllByRoleInContainer(endDate, 'spinbutton');
- fireEvent.mouseDown(rangeDash);
+ fireEvent(rangeDash, pointerEvent('pointerdown', {pointerId: 1, pointerType: 'mouse'}));
+ triggerPress(rangeDash);
expect(endSegments[0]).toHaveFocus();
});
});
@@ -358,7 +533,8 @@ describe('DateRangePicker', function () {
let onChange = jest.fn();
let {getAllByLabelText} = render(
);
@@ -369,7 +545,7 @@ describe('DateRangePicker', function () {
expect(startMonth).toHaveTextContent('1'); // uncontrolled
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange).toHaveBeenCalledWith({start: new Date(2019, 0, 3), end: new Date(2019, 4, 6)});
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2019, 1, 3), end: new CalendarDate(2019, 5, 6)});
let endYear = getAllByLabelText('Year')[1];
expect(endYear).toHaveTextContent('2019');
@@ -378,14 +554,15 @@ describe('DateRangePicker', function () {
expect(endYear).toHaveTextContent('2020'); // uncontrolled
expect(onChange).toHaveBeenCalledTimes(2);
- expect(onChange).toHaveBeenCalledWith({start: new Date(2019, 0, 3), end: new Date(2020, 4, 6)});
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2019, 1, 3), end: new CalendarDate(2020, 5, 6)});
});
it('should edit a date range with the arrow keys (controlled)', function () {
let onChange = jest.fn();
let {getAllByLabelText} = render(
);
@@ -396,7 +573,7 @@ describe('DateRangePicker', function () {
expect(startMonth).toHaveTextContent('2'); // controlled
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange).toHaveBeenCalledWith({start: new Date(2019, 0, 3), end: new Date(2019, 4, 6)});
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2019, 1, 3), end: new CalendarDate(2019, 5, 6)});
let endYear = getAllByLabelText('Year')[1];
expect(endYear).toHaveTextContent('2019');
@@ -405,73 +582,76 @@ describe('DateRangePicker', function () {
expect(endYear).toHaveTextContent('2019'); // controlled
expect(onChange).toHaveBeenCalledTimes(2);
- expect(onChange).toHaveBeenCalledWith({start: new Date(2019, 1, 3), end: new Date(2020, 4, 6)});
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2019, 2, 3), end: new CalendarDate(2020, 5, 6)});
});
it('should edit a date range by entering text (uncontrolled)', function () {
let onChange = jest.fn();
let {getAllByLabelText} = render(
);
let startMonth = getAllByLabelText('Month')[0];
act(() => {startMonth.focus();});
- fireEvent.keyDown(startMonth, {key: '8'});
+ beforeInput(startMonth, '8');
expect(startMonth).toHaveTextContent('8'); // uncontrolled
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange).toHaveBeenCalledWith({start: new Date(2019, 7, 3), end: new Date(2019, 4, 6)});
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2019, 8, 3), end: new CalendarDate(2019, 5, 6)});
expect(getAllByLabelText('Day')[0]).toHaveFocus();
let endYear = getAllByLabelText('Year')[1];
expect(endYear).toHaveTextContent('2019');
act(() => {endYear.focus();});
- fireEvent.keyDown(endYear, {key: '2'});
- fireEvent.keyDown(endYear, {key: '0'});
- fireEvent.keyDown(endYear, {key: '2'});
- fireEvent.keyDown(endYear, {key: '2'});
+ beforeInput(endYear, '2');
+ beforeInput(endYear, '0');
+ beforeInput(endYear, '2');
+ beforeInput(endYear, '2');
expect(endYear).toHaveTextContent('2022'); // uncontrolled
expect(onChange).toHaveBeenCalledTimes(5);
- expect(onChange).toHaveBeenCalledWith({start: new Date(2019, 7, 3), end: new Date(2022, 4, 6)});
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2019, 8, 3), end: new CalendarDate(2022, 5, 6)});
});
it('should edit a date range by entering text (controlled)', function () {
let onChange = jest.fn();
let {getAllByLabelText} = render(
);
let startMonth = getAllByLabelText('Month')[0];
act(() => {startMonth.focus();});
- fireEvent.keyDown(startMonth, {key: '8'});
+ beforeInput(startMonth, '8');
expect(startMonth).toHaveTextContent('2'); // controlled
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange).toHaveBeenCalledWith({start: new Date(2019, 7, 3), end: new Date(2019, 4, 6)});
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2019, 8, 3), end: new CalendarDate(2019, 5, 6)});
expect(getAllByLabelText('Day')[0]).toHaveFocus();
let endDay = getAllByLabelText('Day')[1];
expect(endDay).toHaveTextContent('6');
act(() => {endDay.focus();});
- fireEvent.keyDown(endDay, {key: '4'});
+ beforeInput(endDay, '4');
expect(endDay).toHaveTextContent('6'); // controlled
expect(onChange).toHaveBeenCalledTimes(2);
- expect(onChange).toHaveBeenCalledWith({start: new Date(2019, 1, 3), end: new Date(2019, 4, 4)});
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2019, 2, 3), end: new CalendarDate(2019, 5, 4)});
});
it('should support backspace (uncontrolled)', function () {
let onChange = jest.fn();
let {getAllByLabelText} = render(
);
@@ -482,14 +662,15 @@ describe('DateRangePicker', function () {
expect(endYear).toHaveTextContent('201'); // uncontrolled
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange).toHaveBeenCalledWith({start: new Date(2019, 1, 3), end: new Date(201, 4, 6)});
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2019, 2, 3), end: new CalendarDate(201, 5, 6)});
});
it('should support backspace (controlled)', function () {
let onChange = jest.fn();
let {getAllByLabelText} = render(
);
@@ -500,7 +681,7 @@ describe('DateRangePicker', function () {
expect(endYear).toHaveTextContent('2019'); // controlled
expect(onChange).toHaveBeenCalledTimes(1);
- expect(onChange).toHaveBeenCalledWith({start: new Date(2019, 1, 3), end: new Date(201, 4, 6)});
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2019, 2, 3), end: new CalendarDate(201, 5, 6)});
});
});
@@ -508,8 +689,9 @@ describe('DateRangePicker', function () {
it('should display an error icon when the start date is less than the minimum (controlled)', function () {
let {getByTestId} = render(
+ label="Date range"
+ value={{start: new CalendarDate(1980, 1, 1), end: new CalendarDate(1999, 2, 3)}}
+ minValue={new CalendarDate(1985, 1, 1)} />
);
expect(getByTestId('invalid-icon')).toBeVisible();
});
@@ -517,8 +699,9 @@ describe('DateRangePicker', function () {
it('should display an error icon when the start date is less than the minimum (uncontrolled)', function () {
let {getByTestId, getAllByLabelText} = render(
+ label="Date range"
+ defaultValue={{start: new CalendarDate(1985, 1, 1), end: new CalendarDate(1999, 2, 3)}}
+ minValue={new CalendarDate(1985, 1, 1)} />
);
expect(() => getByTestId('invalid-icon')).toThrow();
@@ -534,8 +717,9 @@ describe('DateRangePicker', function () {
it('should display an error icon when the start date is greater than the maximum (controlled)', function () {
let {getByTestId} = render(
+ label="Date range"
+ value={{start: new CalendarDate(1990, 1, 1), end: new CalendarDate(1999, 2, 3)}}
+ maxValue={new CalendarDate(1985, 1, 1)} />
);
expect(getByTestId('invalid-icon')).toBeVisible();
});
@@ -543,8 +727,9 @@ describe('DateRangePicker', function () {
it('should display an error icon when the start date is greater than the maximum (uncontrolled)', function () {
let {getByTestId, getAllByLabelText} = render(
+ label="Date range"
+ defaultValue={{start: new CalendarDate(1984, 2, 1), end: new CalendarDate(1984, 2, 3)}}
+ maxValue={new CalendarDate(1985, 1, 1)} />
);
expect(() => getByTestId('invalid-icon')).toThrow();
@@ -560,8 +745,9 @@ describe('DateRangePicker', function () {
it('should display an error icon when the end date is greater than the maximum (controlled)', function () {
let {getByTestId} = render(
+ label="Date range"
+ value={{start: new CalendarDate(1980, 1, 1), end: new CalendarDate(1999, 2, 3)}}
+ maxValue={new CalendarDate(1985, 1, 1)} />
);
expect(getByTestId('invalid-icon')).toBeVisible();
});
@@ -569,8 +755,9 @@ describe('DateRangePicker', function () {
it('should display an error icon when the end date is greater than the maximum (uncontrolled)', function () {
let {getByTestId, getAllByLabelText} = render(
+ label="Date range"
+ defaultValue={{start: new CalendarDate(1980, 2, 1), end: new CalendarDate(1984, 2, 3)}}
+ maxValue={new CalendarDate(1985, 1, 1)} />
);
expect(() => getByTestId('invalid-icon')).toThrow();
@@ -586,7 +773,8 @@ describe('DateRangePicker', function () {
it('should display an error icon when the end date is less than the start date (controlled)', function () {
let {getByTestId} = render(
+ label="Date range"
+ value={{start: new CalendarDate(1990, 1, 1), end: new CalendarDate(1980, 2, 3)}} />
);
expect(getByTestId('invalid-icon')).toBeVisible();
});
@@ -594,7 +782,8 @@ describe('DateRangePicker', function () {
it('should display an error icon when the end date is less than the start date (uncontrolled)', function () {
let {getByTestId, getAllByLabelText} = render(
+ label="Date range"
+ defaultValue={{start: new CalendarDate(1980, 2, 1), end: new CalendarDate(1980, 2, 3)}} />
);
expect(() => getByTestId('invalid-icon')).toThrow();
@@ -611,27 +800,29 @@ describe('DateRangePicker', function () {
describe('placeholder', function () {
it('should display a placeholder date if no value is provided', function () {
let onChange = jest.fn();
- let {getByLabelText} = render();
+ let {getByLabelText} = render();
let startDate = getByLabelText('Start Date');
let endDate = getByLabelText('End Date');
- expect(startDate).toHaveTextContent(`1/1/${new Date().getFullYear()}`);
- expect(endDate).toHaveTextContent(`1/1/${new Date().getFullYear()}`);
+ let today = new Intl.DateTimeFormat('en-US').format(new Date());
+ expect(startDate).toHaveTextContent(today);
+ expect(endDate).toHaveTextContent(today);
});
it('should display a placeholder date if the value prop is null', function () {
let onChange = jest.fn();
- let {getByLabelText} = render();
+ let {getByLabelText} = render();
let startDate = getByLabelText('Start Date');
let endDate = getByLabelText('End Date');
- expect(startDate).toHaveTextContent(`1/1/${new Date().getFullYear()}`);
- expect(endDate).toHaveTextContent(`1/1/${new Date().getFullYear()}`);
+ let today = new Intl.DateTimeFormat('en-US').format(new Date());
+ expect(startDate).toHaveTextContent(today);
+ expect(endDate).toHaveTextContent(today);
});
- it('should use the placeholderDate prop if provided', function () {
+ it('should use the placeholderValue prop if provided', function () {
let onChange = jest.fn();
- let {getByLabelText} = render();
+ let {getByLabelText} = render();
let startDate = getByLabelText('Start Date');
let endDate = getByLabelText('End Date');
@@ -641,55 +832,60 @@ describe('DateRangePicker', function () {
it('should not fire onChange until both start and end dates have been entered', function () {
let onChange = jest.fn();
- let {getByLabelText, getAllByRole} = render();
+ let {getByLabelText, getAllByRole} = render();
let startDate = getByLabelText('Start Date');
let endDate = getByLabelText('End Date');
- expect(startDate).toHaveTextContent(`1/1/${new Date().getFullYear()}`);
- expect(endDate).toHaveTextContent(`1/1/${new Date().getFullYear()}`);
+ let formatter = new Intl.DateTimeFormat('en-US');
+ expect(startDate).toHaveTextContent(formatter.format(new Date()));
+ expect(endDate).toHaveTextContent(formatter.format(new Date()));
let segments = getAllByRole('spinbutton');
act(() => {segments[0].focus();});
- fireEvent.keyDown(document.activeElement, {key: '2'});
- expect(startDate).toHaveTextContent(`2/1/${new Date().getFullYear()}`);
+ beforeInput(document.activeElement, '2');
+ let value = today(getLocalTimeZone()).set({month: 2});
+ expect(startDate).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
expect(segments[1]).toHaveFocus();
expect(onChange).not.toHaveBeenCalled();
- fireEvent.keyDown(document.activeElement, {key: '3'});
- expect(startDate).toHaveTextContent(`2/3/${new Date().getFullYear()}`);
+ beforeInput(document.activeElement, '3');
+ value = today(getLocalTimeZone()).set({month: 2, day: 3});
+ expect(startDate).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
expect(segments[2]).toHaveFocus();
expect(onChange).not.toHaveBeenCalled();
- fireEvent.keyDown(document.activeElement, {key: '2'});
- fireEvent.keyDown(document.activeElement, {key: '0'});
- fireEvent.keyDown(document.activeElement, {key: '2'});
- fireEvent.keyDown(document.activeElement, {key: '0'});
+ beforeInput(document.activeElement, '2');
+ beforeInput(document.activeElement, '0');
+ beforeInput(document.activeElement, '2');
+ beforeInput(document.activeElement, '0');
expect(startDate).toHaveTextContent('2/3/2020');
expect(segments[3]).toHaveFocus();
expect(onChange).not.toHaveBeenCalled();
- fireEvent.keyDown(document.activeElement, {key: '4'});
- expect(endDate).toHaveTextContent(`4/1/${new Date().getFullYear()}`);
+ beforeInput(document.activeElement, '4');
+ value = today(getLocalTimeZone()).set({month: 4});
+ expect(endDate).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
expect(segments[4]).toHaveFocus();
expect(onChange).not.toHaveBeenCalled();
- fireEvent.keyDown(document.activeElement, {key: '8'});
- expect(endDate).toHaveTextContent(`4/8/${new Date().getFullYear()}`);
+ beforeInput(document.activeElement, '8');
+ value = today(getLocalTimeZone()).set({month: 4, day: 8});
+ expect(endDate).toHaveTextContent(formatter.format(value.toDate(getLocalTimeZone())));
expect(segments[5]).toHaveFocus();
expect(onChange).not.toHaveBeenCalled();
- fireEvent.keyDown(document.activeElement, {key: '2'});
+ beforeInput(document.activeElement, '2');
expect(onChange).toHaveBeenCalledTimes(1);
- fireEvent.keyDown(document.activeElement, {key: '0'});
+ beforeInput(document.activeElement, '0');
expect(onChange).toHaveBeenCalledTimes(2);
- fireEvent.keyDown(document.activeElement, {key: '2'});
+ beforeInput(document.activeElement, '2');
expect(onChange).toHaveBeenCalledTimes(3);
- fireEvent.keyDown(document.activeElement, {key: '2'});
+ beforeInput(document.activeElement, '2');
expect(onChange).toHaveBeenCalledTimes(4);
- expect(onChange).toHaveBeenCalledWith({start: new Date(2020, 1, 3), end: new Date(2022, 3, 8)});
+ expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2020, 2, 3), end: new CalendarDate(2022, 4, 8)});
});
});
});
diff --git a/packages/@react-spectrum/dialog/package.json b/packages/@react-spectrum/dialog/package.json
index 9179ed90f22..ca3885820ba 100644
--- a/packages/@react-spectrum/dialog/package.json
+++ b/packages/@react-spectrum/dialog/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-spectrum/dialog",
- "version": "3.3.2",
+ "version": "3.3.3",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
@@ -32,26 +32,26 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
- "@react-aria/dialog": "^3.1.3",
- "@react-aria/focus": "^3.3.0",
- "@react-aria/i18n": "^3.3.1",
- "@react-aria/interactions": "^3.4.0",
- "@react-aria/overlays": "^3.6.3",
- "@react-aria/utils": "^3.8.0",
- "@react-spectrum/button": "^3.4.1",
- "@react-spectrum/buttongroup": "^3.2.1",
- "@react-spectrum/divider": "^3.1.2",
- "@react-spectrum/layout": "^3.1.4",
- "@react-spectrum/overlays": "^3.4.1",
- "@react-spectrum/text": "^3.1.2",
- "@react-spectrum/utils": "^3.5.2",
- "@react-spectrum/view": "^3.1.2",
- "@react-stately/overlays": "^3.1.2",
- "@react-stately/utils": "^3.2.1",
- "@react-types/button": "^3.2.1",
- "@react-types/dialog": "^3.3.0",
- "@react-types/shared": "^3.6.0",
- "@spectrum-icons/ui": "^3.2.0"
+ "@react-aria/dialog": "^3.1.4",
+ "@react-aria/focus": "^3.4.1",
+ "@react-aria/i18n": "^3.3.2",
+ "@react-aria/interactions": "^3.5.1",
+ "@react-aria/overlays": "^3.7.2",
+ "@react-aria/utils": "^3.8.2",
+ "@react-spectrum/button": "^3.5.1",
+ "@react-spectrum/buttongroup": "^3.2.2",
+ "@react-spectrum/divider": "^3.1.3",
+ "@react-spectrum/layout": "^3.2.1",
+ "@react-spectrum/overlays": "^3.4.4",
+ "@react-spectrum/text": "^3.1.3",
+ "@react-spectrum/utils": "^3.6.2",
+ "@react-spectrum/view": "^3.1.3",
+ "@react-stately/overlays": "^3.1.3",
+ "@react-stately/utils": "^3.2.2",
+ "@react-types/button": "^3.4.1",
+ "@react-types/dialog": "^3.3.1",
+ "@react-types/shared": "^3.8.0",
+ "@spectrum-icons/ui": "^3.2.1"
},
"devDependencies": {
"@adobe/spectrum-css-temp": "3.0.0-alpha.1",
@@ -59,7 +59,8 @@
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1",
- "@react-spectrum/provider": "^3.0.0-rc.1"
+ "@react-spectrum/provider": "^3.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-spectrum/dialog/stories/DialogTrigger.stories.tsx b/packages/@react-spectrum/dialog/stories/DialogTrigger.stories.tsx
index 88be1688a33..a8d2ef078c6 100644
--- a/packages/@react-spectrum/dialog/stories/DialogTrigger.stories.tsx
+++ b/packages/@react-spectrum/dialog/stories/DialogTrigger.stories.tsx
@@ -364,7 +364,7 @@ function render({width = 'auto', ...props}) {
Trigger
{(close) => (
|