@@ -136,49 +139,40 @@ function Calendar(props) {
The `CalendarGrid` component will be responsible for rendering an individual month. It is a separate component so that you can render more than one month at a time if you like. It's rendered as an HTML `
` element, and React Aria takes care of adding the proper ARIA roles and event handlers to make it behave as an ARIA grid. You can use the arrow keys to navigate between cells, and the Enter key to select a date.
+The `state.getDatesInWeek` function returns the dates in each week of the month. Note that this always includes 7 values, but some of them may be null, which indicates that the date doesn't exist within the calendar system. You should render a placeholder `` element in this case so that the cells line up correctly.
+
**Note**: this component is the same as the `CalendarGrid` component shown in the [useRangeCalendar](useRangeCalendar.html) docs, and you can reuse it between both `Calendar` and `RangeCalendar`.
```tsx example render=false export=true
import {useCalendarGrid} from '@react-aria/calendar';
-import {VisuallyHidden} from '@react-aria/visually-hidden';
-import {startOfWeek, getWeeksInMonth} from '@internationalized/date';
+import {getWeeksInMonth} from '@internationalized/date';
function CalendarGrid({state, ...props}) {
let {locale} = useLocale();
- let {gridProps, weekDays} = useCalendarGrid(props, state);
+ let {gridProps, headerProps, weekDays} = useCalendarGrid(props, state);
- // Find the start date of the grid, which is the beginning
- // of the week the month starts in. Also get the number of
- // weeks in the month so we can render the proper number of rows.
- let monthStart = startOfWeek(state.visibleRange.start, locale);
+ // Get the number of weeks in the month so we can render the proper number of rows.
let weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale);
return (
-
+
- {weekDays.map((day, index) => {
- return (
-
- {/* Make sure screen readers read the full day name,
- but we show an abbreviation visually. */}
- {day.long}
-
- {day.narrow}
-
-
- );
- })}
+ {weekDays.map((day, index) =>
+ {day}
+ )}
{[...new Array(weeksInMonth).keys()].map(weekIndex => (
- {[...new Array(7).keys()].map(dayIndex => (
-
+ {state.getDatesInWeek(weekIndex).map((date, i) => (
+ date ? (
+
+ ) :
))}
))}
@@ -198,7 +192,7 @@ Finally, the `CalendarCell` component renders an individual cell in a calendar.
import {useCalendarCell} from '@react-aria/calendar';
function CalendarCell({state, date}) {
- let ref = React.useRef();
+ let ref = React.useRef(null);
let {
cellProps,
buttonProps,
@@ -240,12 +234,13 @@ That's it! Now we can render an example of our `Calendar` component in action.
.header {
display: flex;
align-items: center;
+ gap: 4px;
+ margin: 0 8px;
}
.header h2 {
flex: 1;
margin: 0;
- text-align: center;
}
.calendar table {
@@ -284,7 +279,7 @@ The `Button` component is used in the above example to navigate between months.
import {useButton} from '@react-aria/button';
function Button(props) {
- let ref = React.useRef();
+ let ref = React.useRef(null);
let {buttonProps} = useButton(props, ref);
return {props.children} ;
}
@@ -306,6 +301,12 @@ function Button(props) {
title="Styled Components"
description="A week view component, built with Styled Components." />
+
+
## Usage
The following examples show how to use the `Calendar` component created in the above example.
@@ -402,7 +403,7 @@ This example includes multiple unavailable date ranges, e.g. dates when no appoi
```tsx example
import {today, isWeekend} from '@internationalized/date';
-import {useLocale} from '@adobe/react-spectrum';
+import {useLocale} from '@react-aria/i18n';
function Example() {
let now = today(getLocalTimeZone());
diff --git a/packages/@react-aria/calendar/docs/useRangeCalendar.mdx b/packages/@react-aria/calendar/docs/useRangeCalendar.mdx
index 1c167a66edb..3900cdc08a5 100644
--- a/packages/@react-aria/calendar/docs/useRangeCalendar.mdx
+++ b/packages/@react-aria/calendar/docs/useRangeCalendar.mdx
@@ -24,7 +24,6 @@ import tailwindExample from 'url:./tailwind.png';
---
category: Date and Time
keywords: [input, form, field, date, time]
-after_version: 3.0.0
---
# useRangeCalendar
@@ -51,9 +50,13 @@ There is no standalone range calendar element in HTML. Two separate `
+
A range calendar consists of a grouping element containing one or more date grids (e.g. months), and a previous and next button for navigating through time. Each calendar grid consists of cells containing button elements that can be pressed and navigated to using the arrow keys to select a date range. Once a start date is selected, the user can navigate to another date using the keyboard or by hovering over it, and clicking it or pressing the Enter key commits the selected date range.
@@ -116,14 +119,14 @@ function RangeCalendar(props) {
createCalendar
});
- let ref = React.useRef();
+ let ref = React.useRef(null);
let {calendarProps, prevButtonProps, nextButtonProps, title} = useRangeCalendar(props, state, ref);
return (
- <
{title}
+ <
>
@@ -136,49 +139,40 @@ function RangeCalendar(props) {
The `CalendarGrid` component will be responsible for rendering an individual month. It is a separate component so that you can render more than one month at a time if you like. It's rendered as an HTML `
` element, and React Aria takes care of adding the proper ARIA roles and event handlers to make it behave as an ARIA grid. You can use the arrow keys to navigate between cells, and the Enter key to select a date.
+The `state.getDatesInWeek` function returns the dates in each week of the month. Note that this always includes 7 values, but some of them may be null, which indicates that the date doesn't exist within the calendar system. You should render a placeholder `` element in this case so that the cells line up correctly.
+
**Note**: this component is the same as the `CalendarGrid` component shown in the [useCalendar](useCalendar.html) docs, and you can reuse it between both `Calendar` and `RangeCalendar`.
```tsx example render=false export=true
import {useCalendarGrid} from '@react-aria/calendar';
-import {VisuallyHidden} from '@react-aria/visually-hidden';
-import {startOfWeek, getWeeksInMonth} from '@internationalized/date';
+import {getWeeksInMonth} from '@internationalized/date';
function CalendarGrid({state, ...props}) {
let {locale} = useLocale();
- let {gridProps, weekDays} = useCalendarGrid(props, state);
+ let {gridProps, headerProps, weekDays} = useCalendarGrid(props, state);
- // Find the start date of the grid, which is the beginning
- // of the week the month starts in. Also get the number of
- // weeks in the month so we can render the proper number of rows.
- let startDate = startOfWeek(state.visibleRange.start, locale);
+ // Get the number of weeks in the month so we can render the proper number of rows.
let weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale);
return (
-
+
- {weekDays.map((day, index) => {
- return (
-
- {/* Make sure screen readers read the full day name,
- but we show an abbreviation visually. */}
- {day.long}
-
- {day.narrow}
-
-
- );
- })}
+ {weekDays.map((day, index) =>
+ {day}
+ )}
{[...new Array(weeksInMonth).keys()].map(weekIndex => (
- {[...new Array(7).keys()].map(dayIndex => (
-
+ {state.getDatesInWeek(weekIndex).map((date, i) => (
+ date ? (
+
+ ) :
))}
))}
@@ -198,7 +192,7 @@ Finally, the `CalendarCell` component renders an individual cell in a calendar.
import {useCalendarCell} from '@react-aria/calendar';
function CalendarCell({state, date}) {
- let ref = React.useRef();
+ let ref = React.useRef(null);
let {
cellProps,
buttonProps,
@@ -240,12 +234,13 @@ That's it! Now we can render an example of our `RangeCalendar` component in acti
.header {
display: flex;
align-items: center;
+ gap: 4px;
+ margin: 0 8px;
}
.header h2 {
flex: 1;
margin: 0;
- text-align: center;
}
.calendar table {
@@ -284,7 +279,7 @@ The `Button` component is used in the above example to navigate between months.
import {useButton} from '@react-aria/button';
function Button(props) {
- let ref = React.useRef();
+ let ref = React.useRef(null);
let {buttonProps} = useButton(props, ref);
return {props.children} ;
}
@@ -413,7 +408,6 @@ This example includes multiple unavailable date ranges, e.g. dates when a rental
```tsx example
import {today} from '@internationalized/date';
-import {useLocale} from '@react-aria/i18n';
function Example() {
let now = today(getLocalTimeZone());
@@ -423,7 +417,6 @@ function Example() {
[now.add({days: 23}), now.add({days: 24})],
];
- let {locale} = useLocale();
let isDateUnavailable = (date) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0);
return
@@ -448,7 +441,7 @@ function Example() {
### Controlling the focused date
-By default, the first selected date is focused when a `RangeCalendar` first mounts. If no `value` or `defaultValue` prop is provided, then the current date is focused. However, `useRangeCalendar` supports controlling which date is focused using the `focusedValue` and `onFocusChange` props. This also determines which month is visible. The `defaultFocusedValue` prop allows setting the initial focused date when the `Calendar` first mounts, without controlling it.
+By default, the first selected date is focused when a `RangeCalendar` first mounts. If no `value` or `defaultValue` prop is provided, then the current date is focused. However, `useRangeCalendar` supports controlling which date is focused using the `focusedValue` and `onFocusChange` props. This also determines which month is visible. The `defaultFocusedValue` prop allows setting the initial focused date when the `RangeCalendar` first mounts, without controlling it.
This example focuses July 1, 2021 by default. The user may change the focused date, and the `onFocusChange` event updates the state. Clicking the button resets the focused date back to the initial value.
diff --git a/packages/@react-aria/calendar/intl/ar-AE.json b/packages/@react-aria/calendar/intl/ar-AE.json
index 2839db97d66..4c83d705bdf 100644
--- a/packages/@react-aria/calendar/intl/ar-AE.json
+++ b/packages/@react-aria/calendar/intl/ar-AE.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "تم تحديد {date, date, full}",
+ "dateRange": "{startDate} إلى {endDate}",
+ "dateSelected": "{date} المحدد",
"finishRangeSelectionPrompt": "انقر لإنهاء عملية تحديد نطاق التاريخ",
+ "maximumDate": "آخر تاريخ متاح",
+ "minimumDate": "أول تاريخ متاح",
"next": "التالي",
"previous": "السابق",
- "selectedDateDescription": "التاريخ المحدد: {date, date, full}",
- "selectedRangeDescription": "النطاق المحدد: {start, date, long} إلى {end, date, long}",
+ "selectedDateDescription": "تاريخ محدد: {date}",
+ "selectedRangeDescription": "المدى الزمني المحدد: {dateRange}",
"startRangeSelectionPrompt": "انقر لبدء عملية تحديد نطاق التاريخ",
- "todayDate": "اليوم، {date, date, full}",
- "todayDateSelected": "اليوم، تم تحديد {date, date, full}"
+ "todayDate": "اليوم، {date}",
+ "todayDateSelected": "اليوم، {date} محدد"
}
diff --git a/packages/@react-aria/calendar/intl/bg-BG.json b/packages/@react-aria/calendar/intl/bg-BG.json
index 3844e9c5bb0..80d9a9117df 100644
--- a/packages/@react-aria/calendar/intl/bg-BG.json
+++ b/packages/@react-aria/calendar/intl/bg-BG.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "избрани {date, date, full}",
+ "dateRange": "{startDate} до {endDate}",
+ "dateSelected": "Избрано е {date}",
"finishRangeSelectionPrompt": "Натиснете, за да довършите избора на времеви интервал",
+ "maximumDate": "Последна налична дата",
+ "minimumDate": "Първа налична дата",
"next": "Напред",
"previous": "Назад",
- "selectedDateDescription": "Избрана дата: {date, date, full}",
- "selectedRangeDescription": "Избран интервал: от {start, date, long} до {end, date, long}",
+ "selectedDateDescription": "Избрана дата: {date}",
+ "selectedRangeDescription": "Избран диапазон: {dateRange}",
"startRangeSelectionPrompt": "Натиснете, за да пристъпите към избора на времеви интервал",
- "todayDate": "Днес {date, date, full}",
- "todayDateSelected": "Днес са избрани {date, date, full}"
+ "todayDate": "Днес, {date}",
+ "todayDateSelected": "Днес, {date} са избрани"
}
diff --git a/packages/@react-aria/calendar/intl/cs-CZ.json b/packages/@react-aria/calendar/intl/cs-CZ.json
index 2e78aa140b2..10c20396e16 100644
--- a/packages/@react-aria/calendar/intl/cs-CZ.json
+++ b/packages/@react-aria/calendar/intl/cs-CZ.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "Vybráno: {date, date, full}",
+ "dateRange": "{startDate} až {endDate}",
+ "dateSelected": "Vybráno {date}",
"finishRangeSelectionPrompt": "Kliknutím dokončíte výběr rozsahu dat",
+ "maximumDate": "Poslední dostupné datum",
+ "minimumDate": "První dostupné datum",
"next": "Další",
"previous": "Předchozí",
- "selectedDateDescription": "Zvolené datum: {date, date, full}",
- "selectedRangeDescription": "Zvolený rozsah: {start, date, long} až {end, date, long}",
+ "selectedDateDescription": "Vybrané datum: {date}",
+ "selectedRangeDescription": "Vybrané období: {dateRange}",
"startRangeSelectionPrompt": "Kliknutím zahájíte výběr rozsahu dat",
- "todayDate": "Dnes, {date, date, full}",
- "todayDateSelected": "Vybrán dnešek: {date, date, full}"
+ "todayDate": "Dnes, {date}",
+ "todayDateSelected": "Dnes, vybráno {date}"
}
diff --git a/packages/@react-aria/calendar/intl/da-DK.json b/packages/@react-aria/calendar/intl/da-DK.json
index c12a82392a4..7651e21be5e 100644
--- a/packages/@react-aria/calendar/intl/da-DK.json
+++ b/packages/@react-aria/calendar/intl/da-DK.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} valgt",
+ "dateRange": "{startDate} til {endDate}",
+ "dateSelected": "{date} valgt",
"finishRangeSelectionPrompt": "Klik for at fuldføre valg af datoområde",
+ "maximumDate": "Sidste ledige dato",
+ "minimumDate": "Første ledige dato",
"next": "Næste",
"previous": "Forrige",
- "selectedDateDescription": "Valgt dato: {date, date, full}",
- "selectedRangeDescription": "Valgt område: {start, date, long} til {end, date, long}",
+ "selectedDateDescription": "Valgt dato: {date}",
+ "selectedRangeDescription": "Valgt interval: {dateRange}",
"startRangeSelectionPrompt": "Klik for at starte valg af datoområde",
- "todayDate": "I dag, {date, date, full}",
- "todayDateSelected": "I dag, {date, date, full} valgt"
+ "todayDate": "I dag, {date}",
+ "todayDateSelected": "I dag, {date} valgt"
}
diff --git a/packages/@react-aria/calendar/intl/de-DE.json b/packages/@react-aria/calendar/intl/de-DE.json
index c9205519785..0d2c832b393 100644
--- a/packages/@react-aria/calendar/intl/de-DE.json
+++ b/packages/@react-aria/calendar/intl/de-DE.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} ausgewählt",
+ "dateRange": "{startDate} bis {endDate}",
+ "dateSelected": "{date} ausgewählt",
"finishRangeSelectionPrompt": "Klicken, um die Auswahl des Datumsbereichs zu beenden",
+ "maximumDate": "Letztes verfügbares Datum",
+ "minimumDate": "Erstes verfügbares Datum",
"next": "Weiter",
"previous": "Zurück",
- "selectedDateDescription": "Ausgewähltes Datum: {date, date, full}",
- "selectedRangeDescription": "Bereich auswählen: {start, date, long} bis {end, date, long}",
+ "selectedDateDescription": "Ausgewähltes Datum: {date}",
+ "selectedRangeDescription": "Ausgewählter Bereich: {dateRange}",
"startRangeSelectionPrompt": "Klicken, um die Auswahl des Datumsbereichs zu beginnen",
- "todayDate": "Heute, {date, date, full}",
- "todayDateSelected": "Heute, {date, date, full} ausgewählt"
+ "todayDate": "Heute, {date}",
+ "todayDateSelected": "Heute, {date} ausgewählt"
}
diff --git a/packages/@react-aria/calendar/intl/el-GR.json b/packages/@react-aria/calendar/intl/el-GR.json
index 16e56541d7b..9f0caa88a59 100644
--- a/packages/@react-aria/calendar/intl/el-GR.json
+++ b/packages/@react-aria/calendar/intl/el-GR.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "Επιλέχτηκε {date, date, full}",
+ "dateRange": "{startDate} έως {endDate}",
+ "dateSelected": "Επιλέχθηκε {date}",
"finishRangeSelectionPrompt": "Κάντε κλικ για να ολοκληρώσετε την επιλογή εύρους ημερομηνιών",
+ "maximumDate": "Τελευταία διαθέσιμη ημερομηνία",
+ "minimumDate": "Πρώτη διαθέσιμη ημερομηνία",
"next": "Επόμενο",
"previous": "Προηγούμενο",
- "selectedDateDescription": "Επιλεγμένη ημερομηνία: {date, date, full}",
- "selectedRangeDescription": "Επιλεγμένο εύρος: {start, date, long} έως {end, date, long}",
+ "selectedDateDescription": "Επιλεγμένη ημερομηνία: {date}",
+ "selectedRangeDescription": "Επιλεγμένο εύρος: {dateRange}",
"startRangeSelectionPrompt": "Κάντε κλικ για να ξεκινήσετε την επιλογή εύρους ημερομηνιών",
- "todayDate": "Σήμερα, {date, date, full}",
- "todayDateSelected": "Σήμερα, επιλέχτηκε {date, date, full}"
+ "todayDate": "Σήμερα, {date}",
+ "todayDateSelected": "Σήμερα, επιλέχτηκε {date}"
}
diff --git a/packages/@react-aria/calendar/intl/en-US.json b/packages/@react-aria/calendar/intl/en-US.json
index 9fa85df8034..ae03f79f1dc 100644
--- a/packages/@react-aria/calendar/intl/en-US.json
+++ b/packages/@react-aria/calendar/intl/en-US.json
@@ -1,11 +1,14 @@
{
"previous": "Previous",
"next": "Next",
- "selectedDateDescription": "Selected Date: {date, date, full}",
- "selectedRangeDescription": "Selected Range: {start, date, long} to {end, date, long}",
- "todayDate": "Today, {date, date, full}",
- "todayDateSelected": "Today, {date, date, full} selected",
- "dateSelected": "{date, date, full} selected",
+ "selectedDateDescription": "Selected Date: {date}",
+ "selectedRangeDescription": "Selected Range: {dateRange}",
+ "todayDate": "Today, {date}",
+ "todayDateSelected": "Today, {date} selected",
+ "dateSelected": "{date} selected",
"startRangeSelectionPrompt": "Click to start selecting date range",
- "finishRangeSelectionPrompt": "Click to finish selecting date range"
+ "finishRangeSelectionPrompt": "Click to finish selecting date range",
+ "minimumDate": "First available date",
+ "maximumDate": "Last available date",
+ "dateRange": "{startDate} to {endDate}"
}
diff --git a/packages/@react-aria/calendar/intl/es-ES.json b/packages/@react-aria/calendar/intl/es-ES.json
index 021047e832d..9797d00d52c 100644
--- a/packages/@react-aria/calendar/intl/es-ES.json
+++ b/packages/@react-aria/calendar/intl/es-ES.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} seleccionado",
+ "dateRange": "{startDate} a {endDate}",
+ "dateSelected": "{date} seleccionado",
"finishRangeSelectionPrompt": "Haga clic para terminar de seleccionar rango de fechas",
+ "maximumDate": "Última fecha disponible",
+ "minimumDate": "Primera fecha disponible",
"next": "Siguiente",
"previous": "Anterior",
- "selectedDateDescription": "Fecha seleccionada: {date, date, full}",
- "selectedRangeDescription": "Seleccionar rango: {start, date, long} a {end, date, long}",
+ "selectedDateDescription": "Fecha seleccionada: {date}",
+ "selectedRangeDescription": "Intervalo seleccionado: {dateRange}",
"startRangeSelectionPrompt": "Haga clic para comenzar a seleccionar un rango de fechas",
- "todayDate": "Hoy, {date, date, full}",
- "todayDateSelected": "Hoy, {date, date, full} seleccionado"
+ "todayDate": "Hoy, {date}",
+ "todayDateSelected": "Hoy, {date} seleccionado"
}
diff --git a/packages/@react-aria/calendar/intl/et-EE.json b/packages/@react-aria/calendar/intl/et-EE.json
index f41a29ac9e8..286c86ca859 100644
--- a/packages/@react-aria/calendar/intl/et-EE.json
+++ b/packages/@react-aria/calendar/intl/et-EE.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} valitud",
+ "dateRange": "{startDate} kuni {endDate}",
+ "dateSelected": "{date} valitud",
"finishRangeSelectionPrompt": "Klõpsake kuupäevavahemiku valimise lõpetamiseks",
+ "maximumDate": "Viimane saadaolev kuupäev",
+ "minimumDate": "Esimene saadaolev kuupäev",
"next": "Järgmine",
"previous": "Eelmine",
- "selectedDateDescription": "Valitud kuupäev: {date, date, full}",
- "selectedRangeDescription": "Valitud vahemik: {start, date, long} kuni {end, date, long}",
+ "selectedDateDescription": "Valitud kuupäev: {date}",
+ "selectedRangeDescription": "Valitud vahemik: {dateRange}",
"startRangeSelectionPrompt": "Klõpsake kuupäevavahemiku valimiseks",
- "todayDate": "Täna {date, date, full}",
- "todayDateSelected": "Täna {date, date, full} valitud"
+ "todayDate": "Täna, {date}",
+ "todayDateSelected": "Täna, {date} valitud"
}
diff --git a/packages/@react-aria/calendar/intl/fi-FI.json b/packages/@react-aria/calendar/intl/fi-FI.json
index d5bd0c71eb4..99b6161245d 100644
--- a/packages/@react-aria/calendar/intl/fi-FI.json
+++ b/packages/@react-aria/calendar/intl/fi-FI.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} valittu",
+ "dateRange": "{startDate} – {endDate}",
+ "dateSelected": "{date} valittu",
"finishRangeSelectionPrompt": "Lopeta päivämääräalueen valinta napsauttamalla tätä.",
+ "maximumDate": "Viimeinen varattavissa oleva päivämäärä",
+ "minimumDate": "Ensimmäinen varattavissa oleva päivämäärä",
"next": "Seuraava",
"previous": "Edellinen",
- "selectedDateDescription": "Valittu päivämäärä: {date, date, full}",
- "selectedRangeDescription": "Valittu alue: {start, date, long} - {end, date, long}",
+ "selectedDateDescription": "Valittu päivämäärä: {date}",
+ "selectedRangeDescription": "Valittu aikaväli: {dateRange}",
"startRangeSelectionPrompt": "Aloita päivämääräalueen valinta napsauttamalla tätä.",
- "todayDate": "Tänään, {date, date, full}",
- "todayDateSelected": "Tänään, {date, date, full} valittu"
+ "todayDate": "Tänään, {date}",
+ "todayDateSelected": "Tänään, {date} valittu"
}
diff --git a/packages/@react-aria/calendar/intl/fr-FR.json b/packages/@react-aria/calendar/intl/fr-FR.json
index 7b7e1472ef6..b3880330852 100644
--- a/packages/@react-aria/calendar/intl/fr-FR.json
+++ b/packages/@react-aria/calendar/intl/fr-FR.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} sélectionné",
+ "dateRange": "{startDate} à {endDate}",
+ "dateSelected": "{date} sélectionné",
"finishRangeSelectionPrompt": "Cliquer pour finir de sélectionner la plage de dates",
+ "maximumDate": "Dernière date disponible",
+ "minimumDate": "Première date disponible",
"next": "Suivant",
"previous": "Précédent",
- "selectedDateDescription": "Date sélectionnée : {date, date, full}",
- "selectedRangeDescription": "Plage sélectionnée : {start, date, long} à {end, date, long}",
+ "selectedDateDescription": "Date sélectionnée : {date}",
+ "selectedRangeDescription": "Plage sélectionnée : {dateRange}",
"startRangeSelectionPrompt": "Cliquer pour commencer à sélectionner la plage de dates",
- "todayDate": "Aujourd’hui, {date, date, full}",
- "todayDateSelected": "Aujourd’hui, {date, date, full} sélectionné"
+ "todayDate": "Aujourd'hui, {date}",
+ "todayDateSelected": "Aujourd’hui, {date} sélectionné"
}
diff --git a/packages/@react-aria/calendar/intl/he-IL.json b/packages/@react-aria/calendar/intl/he-IL.json
index eddcf8aea56..edf26959811 100644
--- a/packages/@react-aria/calendar/intl/he-IL.json
+++ b/packages/@react-aria/calendar/intl/he-IL.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "התאריך {date, date, full} שנבחר",
+ "dateRange": "{startDate} עד {endDate}",
+ "dateSelected": "{date} נבחר",
"finishRangeSelectionPrompt": "חץ כדי לסיים את בחירת טווח התאריכים",
+ "maximumDate": "תאריך פנוי אחרון",
+ "minimumDate": "תאריך פנוי ראשון",
"next": "הבא",
"previous": "הקודם",
- "selectedDateDescription": "התאריך שנבחר: {date, date, full}",
- "selectedRangeDescription": "הטווח שנבחר: מ-{start, date, long} ועד {end, date, long}",
+ "selectedDateDescription": "תאריך נבחר: {date}",
+ "selectedRangeDescription": "טווח נבחר: {dateRange}",
"startRangeSelectionPrompt": "לחץ כדי להתחיל בבחירת טווח התאריכים",
- "todayDate": "היום, {date, date, full}",
- "todayDateSelected": "היום, התאריך {date, date, full} שנבחר"
+ "todayDate": "היום, {date}",
+ "todayDateSelected": "היום, {date} נבחר"
}
diff --git a/packages/@react-aria/calendar/intl/hr-HR.json b/packages/@react-aria/calendar/intl/hr-HR.json
index 093592f96d9..821cf49f24c 100644
--- a/packages/@react-aria/calendar/intl/hr-HR.json
+++ b/packages/@react-aria/calendar/intl/hr-HR.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "Odabran {date, date, full}",
+ "dateRange": "{startDate} do {endDate}",
+ "dateSelected": "{date} odabran",
"finishRangeSelectionPrompt": "Kliknite da dovršite raspon odabranih datuma",
+ "maximumDate": "Posljednji raspoloživi datum",
+ "minimumDate": "Prvi raspoloživi datum",
"next": "Sljedeći",
"previous": "Prethodni",
- "selectedDateDescription": "Odabrani datum: {date, date, full}",
- "selectedRangeDescription": "Odabrani raspon: od {start, date, long} do {end, date, long}",
+ "selectedDateDescription": "Odabrani datum: {date}",
+ "selectedRangeDescription": "Odabrani raspon: {dateRange}",
"startRangeSelectionPrompt": "Kliknite da započnete raspon odabranih datuma",
- "todayDate": "Danas, {date, date, full}",
- "todayDateSelected": "Danas, odabran {date, date, full}"
+ "todayDate": "Danas, {date}",
+ "todayDateSelected": "Danas, odabran {date}"
}
diff --git a/packages/@react-aria/calendar/intl/hu-HU.json b/packages/@react-aria/calendar/intl/hu-HU.json
index d2435e33794..16f116435b3 100644
--- a/packages/@react-aria/calendar/intl/hu-HU.json
+++ b/packages/@react-aria/calendar/intl/hu-HU.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} kijelölve",
+ "dateRange": "{startDate}–{endDate}",
+ "dateSelected": "{date} kiválasztva",
"finishRangeSelectionPrompt": "Kattintson a dátumtartomány kijelölésének befejezéséhez",
+ "maximumDate": "Utolsó elérhető dátum",
+ "minimumDate": "Az első elérhető dátum",
"next": "Következő",
"previous": "Előző",
- "selectedDateDescription": "Kijelölt dátum: {date, date, full}",
- "selectedRangeDescription": "Kijelölt tartomány: {start, date, long} – {end, date, long}",
+ "selectedDateDescription": "Kijelölt dátum: {date}",
+ "selectedRangeDescription": "Kijelölt tartomány: {dateRange}",
"startRangeSelectionPrompt": "Kattintson a dátumtartomány kijelölésének indításához",
- "todayDate": "Ma, {date, date, full}",
- "todayDateSelected": "Ma, {date, date, full} kijelölve"
+ "todayDate": "Ma, {date}",
+ "todayDateSelected": "Ma, {date} kijelölve"
}
diff --git a/packages/@react-aria/calendar/intl/it-IT.json b/packages/@react-aria/calendar/intl/it-IT.json
index 65dc8093d84..2b96bf3a4d0 100644
--- a/packages/@react-aria/calendar/intl/it-IT.json
+++ b/packages/@react-aria/calendar/intl/it-IT.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} selezionato",
+ "dateRange": "Da {startDate} a {endDate}",
+ "dateSelected": "{date} selezionata",
"finishRangeSelectionPrompt": "Fai clic per completare la selezione dell’intervallo di date",
+ "maximumDate": "Ultima data disponibile",
+ "minimumDate": "Prima data disponibile",
"next": "Successivo",
"previous": "Precedente",
- "selectedDateDescription": "Data selezionata: {date, date, full}",
- "selectedRangeDescription": "Intervallo selezionato: da {start, date, long} a {end, date, long}",
+ "selectedDateDescription": "Data selezionata: {date}",
+ "selectedRangeDescription": "Intervallo selezionato: {dateRange}",
"startRangeSelectionPrompt": "Fai clic per selezionare l’intervallo di date",
- "todayDate": "Oggi, {date, date, full}",
- "todayDateSelected": "Oggi, {date, date, full} selezionato"
+ "todayDate": "Oggi, {date}",
+ "todayDateSelected": "Oggi, {date} selezionata"
}
diff --git a/packages/@react-aria/calendar/intl/ja-JP.json b/packages/@react-aria/calendar/intl/ja-JP.json
index bba5859d6ec..4e01fa23ccd 100644
--- a/packages/@react-aria/calendar/intl/ja-JP.json
+++ b/packages/@react-aria/calendar/intl/ja-JP.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} を選択",
+ "dateRange": "{startDate} から {endDate}",
+ "dateSelected": "{date} を選択",
"finishRangeSelectionPrompt": "クリックして日付範囲の選択を終了",
+ "maximumDate": "最終利用可能日",
+ "minimumDate": "最初の利用可能日",
"next": "次へ",
"previous": "前へ",
- "selectedDateDescription": "日付を選択:{date, date, full}",
- "selectedRangeDescription": "範囲を選択:{start, date, long} から {end, date, long}",
+ "selectedDateDescription": "選択した日付 : {date}",
+ "selectedRangeDescription": "選択範囲 : {dateRange}",
"startRangeSelectionPrompt": "クリックして日付範囲の選択を開始",
- "todayDate": "本日、{date, date, full}",
- "todayDateSelected": "本日、{date, date, full} を選択"
+ "todayDate": "本日、{date}",
+ "todayDateSelected": "本日、{date} を選択"
}
diff --git a/packages/@react-aria/calendar/intl/ko-KR.json b/packages/@react-aria/calendar/intl/ko-KR.json
index b039a7ef94d..0f3633b911d 100644
--- a/packages/@react-aria/calendar/intl/ko-KR.json
+++ b/packages/@react-aria/calendar/intl/ko-KR.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} 선택함",
+ "dateRange": "{startDate} ~ {endDate}",
+ "dateSelected": "{date} 선택됨",
"finishRangeSelectionPrompt": "날짜 범위 선택을 완료하려면 클릭하십시오.",
+ "maximumDate": "마지막으로 사용 가능한 일자",
+ "minimumDate": "처음으로 사용 가능한 일자",
"next": "다음",
"previous": "이전",
- "selectedDateDescription": "선택한 날짜: {date, date, full}",
- "selectedRangeDescription": "선택한 범위: {start, date, long} ~ {end, date, long}",
+ "selectedDateDescription": "선택 일자: {date}",
+ "selectedRangeDescription": "선택 범위: {dateRange}",
"startRangeSelectionPrompt": "날짜 범위 선택을 시작하려면 클릭하십시오.",
- "todayDate": "오늘, {date, date, full}",
- "todayDateSelected": "오늘, {date, date, full} 선택함"
+ "todayDate": "오늘, {date}",
+ "todayDateSelected": "오늘, {date} 선택됨"
}
diff --git a/packages/@react-aria/calendar/intl/lt-LT.json b/packages/@react-aria/calendar/intl/lt-LT.json
index 189ee31db38..ea3cd0a3b7f 100644
--- a/packages/@react-aria/calendar/intl/lt-LT.json
+++ b/packages/@react-aria/calendar/intl/lt-LT.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "Pasirinkta {date, date, full}",
+ "dateRange": "Nuo {startDate} iki {endDate}",
+ "dateSelected": "Pasirinkta {date}",
"finishRangeSelectionPrompt": "Spustelėkite, kad baigtumėte pasirinkti datų intervalą",
+ "maximumDate": "Paskutinė galima data",
+ "minimumDate": "Pirmoji galima data",
"next": "Paskesnis",
"previous": "Ankstesnis",
- "selectedDateDescription": "Pasirinkta data: {date, date, full}",
- "selectedRangeDescription": "Pasirinktas intervalas: nuo {start, date, long} iki {end, date, long}",
+ "selectedDateDescription": "Pasirinkta data: {date}",
+ "selectedRangeDescription": "Pasirinktas intervalas: {dateRange}",
"startRangeSelectionPrompt": "Spustelėkite, kad pradėtumėte pasirinkti datų intervalą",
- "todayDate": "Šiandien, {date, date, full}",
- "todayDateSelected": "Šiandien, pasirinkta {date, date, full}"
+ "todayDate": "Šiandien, {date}",
+ "todayDateSelected": "Šiandien, pasirinkta {date}"
}
diff --git a/packages/@react-aria/calendar/intl/lv-LV.json b/packages/@react-aria/calendar/intl/lv-LV.json
index f73caa7bfbe..659a44485bd 100644
--- a/packages/@react-aria/calendar/intl/lv-LV.json
+++ b/packages/@react-aria/calendar/intl/lv-LV.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "Atlasīts {date, date, full}",
+ "dateRange": "No {startDate} līdz {endDate}",
+ "dateSelected": "Atlasīts: {date}",
"finishRangeSelectionPrompt": "Noklikšķiniet, lai pabeigtu datumu diapazona atlasi",
+ "maximumDate": "Pēdējais pieejamais datums",
+ "minimumDate": "Pirmais pieejamais datums",
"next": "Tālāk",
"previous": "Atpakaļ",
- "selectedDateDescription": "Atlasītais datums: {date, date, full}",
- "selectedRangeDescription": "Atlasītais diapazons: {start, date, long} līdz {end, date, long}",
+ "selectedDateDescription": "Atlasītais datums: {date}",
+ "selectedRangeDescription": "Atlasītais diapazons: {dateRange}",
"startRangeSelectionPrompt": "Noklikšķiniet, lai sāktu datumu diapazona atlasi",
- "todayDate": "Šodiena, {date, date, full}",
- "todayDateSelected": "Atlasīta šodiena, {date, date, full}"
+ "todayDate": "Šodien, {date}",
+ "todayDateSelected": "Atlasīta šodiena, {date}"
}
diff --git a/packages/@react-aria/calendar/intl/nb-NO.json b/packages/@react-aria/calendar/intl/nb-NO.json
index 955ff755cfd..a5e7cdd482b 100644
--- a/packages/@react-aria/calendar/intl/nb-NO.json
+++ b/packages/@react-aria/calendar/intl/nb-NO.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} valgt",
+ "dateRange": "{startDate} til {endDate}",
+ "dateSelected": "{date} valgt",
"finishRangeSelectionPrompt": "Klikk for å fullføre valg av datoområde",
+ "maximumDate": "Siste tilgjengelige dato",
+ "minimumDate": "Første tilgjengelige dato",
"next": "Neste",
"previous": "Forrige",
- "selectedDateDescription": "Valgt dato: {date, date, full}",
- "selectedRangeDescription": "Valgt område: {start, date, long} til {end, date, long}",
+ "selectedDateDescription": "Valgt dato: {date}",
+ "selectedRangeDescription": "Valgt område: {dateRange}",
"startRangeSelectionPrompt": "Klikk for å starte valg av datoområde",
- "todayDate": "I dag, {date, date, full}",
- "todayDateSelected": "I dag, {date, date, full} valgt"
+ "todayDate": "I dag, {date}",
+ "todayDateSelected": "I dag, {date} valgt"
}
diff --git a/packages/@react-aria/calendar/intl/nl-NL.json b/packages/@react-aria/calendar/intl/nl-NL.json
index 072b2d80358..012285024af 100644
--- a/packages/@react-aria/calendar/intl/nl-NL.json
+++ b/packages/@react-aria/calendar/intl/nl-NL.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} geselecteerd",
+ "dateRange": "{startDate} tot {endDate}",
+ "dateSelected": "{date} geselecteerd",
"finishRangeSelectionPrompt": "Klik om de selectie van het datumbereik te voltooien",
+ "maximumDate": "Laatste beschikbare datum",
+ "minimumDate": "Eerste beschikbare datum",
"next": "Volgende",
"previous": "Vorige",
- "selectedDateDescription": "Geselecteerde datum: {date, date, full}",
- "selectedRangeDescription": "Geselecteerd bereik: {start, date, long} t/m {end, date, long}",
+ "selectedDateDescription": "Geselecteerde datum: {date}",
+ "selectedRangeDescription": "Geselecteerd bereik: {dateRange}",
"startRangeSelectionPrompt": "Klik om het datumbereik te selecteren",
- "todayDate": "Vandaag, {date, date, full}",
- "todayDateSelected": "Vandaag, {date, date, full} geselecteerd"
+ "todayDate": "Vandaag, {date}",
+ "todayDateSelected": "Vandaag, {date} geselecteerd"
}
diff --git a/packages/@react-aria/calendar/intl/pl-PL.json b/packages/@react-aria/calendar/intl/pl-PL.json
index 0020fad9172..de7527236f6 100644
--- a/packages/@react-aria/calendar/intl/pl-PL.json
+++ b/packages/@react-aria/calendar/intl/pl-PL.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "Wybrano {date, date, full}",
+ "dateRange": "{startDate} do {endDate}",
+ "dateSelected": "Wybrano {date}",
"finishRangeSelectionPrompt": "Kliknij, aby zakończyć wybór zakresu dat",
+ "maximumDate": "Ostatnia dostępna data",
+ "minimumDate": "Pierwsza dostępna data",
"next": "Dalej",
"previous": "Wstecz",
- "selectedDateDescription": "Wybrana data: {date, date, full}",
- "selectedRangeDescription": "Wybrany zakres: {start, date, long} do {end, date, long}",
+ "selectedDateDescription": "Wybrana data: {date}",
+ "selectedRangeDescription": "Wybrany zakres: {dateRange}",
"startRangeSelectionPrompt": "Kliknij, aby rozpocząć wybór zakresu dat",
- "todayDate": "Dzisiaj {date, date, full}",
- "todayDateSelected": "Dzisiaj wybrano {date, date, full}"
+ "todayDate": "Dzisiaj, {date}",
+ "todayDateSelected": "Dzisiaj wybrano {date}"
}
diff --git a/packages/@react-aria/calendar/intl/pt-BR.json b/packages/@react-aria/calendar/intl/pt-BR.json
index 44ebef4440d..95580ead432 100644
--- a/packages/@react-aria/calendar/intl/pt-BR.json
+++ b/packages/@react-aria/calendar/intl/pt-BR.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} selecionado",
+ "dateRange": "{startDate} a {endDate}",
+ "dateSelected": "{date} selecionado",
"finishRangeSelectionPrompt": "Clique para concluir a seleção do intervalo de datas",
+ "maximumDate": "Última data disponível",
+ "minimumDate": "Primeira data disponível",
"next": "Próximo",
"previous": "Anterior",
- "selectedDateDescription": "Data selecionada: {date, date, full}",
- "selectedRangeDescription": "Intervalo selecionado: {start, date, long} até {end, date, long}",
+ "selectedDateDescription": "Data selecionada: {date}",
+ "selectedRangeDescription": "Intervalo selecionado: {dateRange}",
"startRangeSelectionPrompt": "Clique para iniciar a seleção do intervalo de datas",
- "todayDate": "Hoje, {date, date, full}",
- "todayDateSelected": "Hoje, {date, date, full} selecionado"
+ "todayDate": "Hoje, {date}",
+ "todayDateSelected": "Hoje, {date} selecionado"
}
diff --git a/packages/@react-aria/calendar/intl/pt-PT.json b/packages/@react-aria/calendar/intl/pt-PT.json
index d7cd7c83a62..07d09f31d96 100644
--- a/packages/@react-aria/calendar/intl/pt-PT.json
+++ b/packages/@react-aria/calendar/intl/pt-PT.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{data, data, completo} selecionado",
+ "dateRange": "{startDate} a {endDate}",
+ "dateSelected": "{date} selecionado",
"finishRangeSelectionPrompt": "Clique para terminar de selecionar o intervalo de datas",
+ "maximumDate": "Última data disponível",
+ "minimumDate": "Primeira data disponível",
"next": "Próximo",
"previous": "Anterior",
- "selectedDateDescription": "Data Selecionada: {data, data, completo}",
- "selectedRangeDescription": "Selecionar Intervalo: {início, data, longo} a {término, data, longo}",
+ "selectedDateDescription": "Data selecionada: {date}",
+ "selectedRangeDescription": "Intervalo selecionado: {dateRange}",
"startRangeSelectionPrompt": "Clique para começar a selecionar o intervalo de datas",
- "todayDate": "Hoje, {date, date, full}",
- "todayDateSelected": "Hoje, {date, date, full} selecionado"
+ "todayDate": "Hoje, {date}",
+ "todayDateSelected": "Hoje, {date} selecionado"
}
diff --git a/packages/@react-aria/calendar/intl/ro-RO.json b/packages/@react-aria/calendar/intl/ro-RO.json
index 0f9042ac75e..b8d349c8fd4 100644
--- a/packages/@react-aria/calendar/intl/ro-RO.json
+++ b/packages/@react-aria/calendar/intl/ro-RO.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} selectată",
+ "dateRange": "De la {startDate} până la {endDate}",
+ "dateSelected": "{date} selectată",
"finishRangeSelectionPrompt": "Apăsaţi pentru a finaliza selecţia razei pentru dată",
+ "maximumDate": "Ultima dată disponibilă",
+ "minimumDate": "Prima dată disponibilă",
"next": "Următorul",
"previous": "Înainte",
- "selectedDateDescription": "Dată selectată: {date, date, full}",
- "selectedRangeDescription": "Selectaţi raza: {start, date, long} la {end, date, long}",
+ "selectedDateDescription": "Dată selectată: {date}",
+ "selectedRangeDescription": "Interval selectat: {dateRange}",
"startRangeSelectionPrompt": "Apăsaţi pentru a începe selecţia razei pentru dată",
- "todayDate": "Astăzi, {date, date, full}",
- "todayDateSelected": "Dată, {date, date, full} selectată"
+ "todayDate": "Astăzi, {date}",
+ "todayDateSelected": "Azi, {date} selectată"
}
diff --git a/packages/@react-aria/calendar/intl/ru-RU.json b/packages/@react-aria/calendar/intl/ru-RU.json
index 69b40675455..d070344cc7e 100644
--- a/packages/@react-aria/calendar/intl/ru-RU.json
+++ b/packages/@react-aria/calendar/intl/ru-RU.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "выбрано {date, date, full}",
+ "dateRange": "С {startDate} по {endDate}",
+ "dateSelected": "Выбрано {date}",
"finishRangeSelectionPrompt": "Щелкните, чтобы завершить выбор диапазона дат",
+ "maximumDate": "Последняя доступная дата",
+ "minimumDate": "Первая доступная дата",
"next": "Далее",
"previous": "Назад",
- "selectedDateDescription": "Выбранная дата: {date, date, full}",
- "selectedRangeDescription": "Выбранный диапазон: {start, date, long} – {end, date, long}",
+ "selectedDateDescription": "Выбранная дата: {date}",
+ "selectedRangeDescription": "Выбранный диапазон: {dateRange}",
"startRangeSelectionPrompt": "Щелкните, чтобы начать выбор диапазона дат",
- "todayDate": "Сегодня, {date, date, full}",
- "todayDateSelected": "Сегодня, выбрано {date, date, full}"
+ "todayDate": "Сегодня, {date}",
+ "todayDateSelected": "Сегодня, выбрано {date}"
}
diff --git a/packages/@react-aria/calendar/intl/sk-SK.json b/packages/@react-aria/calendar/intl/sk-SK.json
index cc76594974e..5e19d563a32 100644
--- a/packages/@react-aria/calendar/intl/sk-SK.json
+++ b/packages/@react-aria/calendar/intl/sk-SK.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "Vybratý dátum {date, date, full}",
+ "dateRange": "Od {startDate} do {endDate}",
+ "dateSelected": "Vybratý dátum {date}",
"finishRangeSelectionPrompt": "Kliknutím dokončíte výber rozsahu dátumov",
+ "maximumDate": "Posledný dostupný dátum",
+ "minimumDate": "Prvý dostupný dátum",
"next": "Nasledujúce",
"previous": "Predchádzajúce",
- "selectedDateDescription": "Vybratý dátum: {date, date, full}",
- "selectedRangeDescription": "Vybratý rozsah dátumov: {start, date, long} do {end, date, long}",
+ "selectedDateDescription": "Vybratý dátum: {date}",
+ "selectedRangeDescription": "Vybratý rozsah: {dateRange}",
"startRangeSelectionPrompt": "Kliknutím spustíte výber rozsahu dátumov",
- "todayDate": "Dnešný dátum, {date, date, full}",
- "todayDateSelected": "Vybratý dnešný dátum, {date, date, full}"
+ "todayDate": "Dnes {date}",
+ "todayDateSelected": "Vybratý dnešný dátum {date}"
}
diff --git a/packages/@react-aria/calendar/intl/sl-SI.json b/packages/@react-aria/calendar/intl/sl-SI.json
index 1d88f119348..0bb5783cb03 100644
--- a/packages/@react-aria/calendar/intl/sl-SI.json
+++ b/packages/@react-aria/calendar/intl/sl-SI.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "izbrano: {date, date, full}",
+ "dateRange": "{startDate} do {endDate}",
+ "dateSelected": "{date} izbrano",
"finishRangeSelectionPrompt": "Kliknite za dokončanje izbire datumskega obsega",
+ "maximumDate": "Zadnji razpoložljivi datum",
+ "minimumDate": "Prvi razpoložljivi datum",
"next": "Naprej",
"previous": "Nazaj",
- "selectedDateDescription": "Izbrani datum: {date, date, full}",
- "selectedRangeDescription": "Izbrani razpon: {start, date, long} do {end, date, long}",
+ "selectedDateDescription": "Izbrani datum: {date}",
+ "selectedRangeDescription": "Izbrano območje: {dateRange}",
"startRangeSelectionPrompt": "Kliknite za začetek izbire datumskega obsega",
- "todayDate": "Danes, {date, date, full}",
- "todayDateSelected": "Danes, izbrano: {date, date, full}"
+ "todayDate": "Danes, {date}",
+ "todayDateSelected": "Danes, {date} izbrano"
}
diff --git a/packages/@react-aria/calendar/intl/sr-SP.json b/packages/@react-aria/calendar/intl/sr-SP.json
index 487a47b0173..15503641821 100644
--- a/packages/@react-aria/calendar/intl/sr-SP.json
+++ b/packages/@react-aria/calendar/intl/sr-SP.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "Izabran {date, date, full}",
+ "dateRange": "{startDate} do {endDate}",
+ "dateSelected": "{date} izabran",
"finishRangeSelectionPrompt": "Kliknite da dovršite opseg izabranih datuma",
+ "maximumDate": "Zadnji raspoloživi datum",
+ "minimumDate": "Prvi raspoloživi datum",
"next": "Sledeći",
"previous": "Prethodni",
- "selectedDateDescription": "Izabrani datum: {date, date, full}",
- "selectedRangeDescription": "Izabrani opseg: od {start, date, long} do {end, date, long}",
+ "selectedDateDescription": "Izabrani datum: {date}",
+ "selectedRangeDescription": "Izabrani period: {dateRange}",
"startRangeSelectionPrompt": "Kliknite da započnete opseg izabranih datuma",
- "todayDate": "Danas, {date, date, full}",
- "todayDateSelected": "Danas, izabran {date, date, full}"
+ "todayDate": "Danas, {date}",
+ "todayDateSelected": "Danas, izabran {date}"
}
diff --git a/packages/@react-aria/calendar/intl/sv-SE.json b/packages/@react-aria/calendar/intl/sv-SE.json
index c5f816ee643..329048f7ede 100644
--- a/packages/@react-aria/calendar/intl/sv-SE.json
+++ b/packages/@react-aria/calendar/intl/sv-SE.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} har valts",
+ "dateRange": "{startDate} till {endDate}",
+ "dateSelected": "{date} har valts",
"finishRangeSelectionPrompt": "Klicka för att avsluta val av datumintervall",
+ "maximumDate": "Sista tillgängliga datum",
+ "minimumDate": "Första tillgängliga datum",
"next": "Nästa",
"previous": "Föregående",
- "selectedDateDescription": "Valt datum: {date, date, full}",
- "selectedRangeDescription": "Valt intervall: {start, date, long} till {end, date, long}",
+ "selectedDateDescription": "Valt datum: {date}",
+ "selectedRangeDescription": "Valt intervall: {dateRange}",
"startRangeSelectionPrompt": "Klicka för att välja datumintervall",
- "todayDate": "I dag, {date, date, full}",
- "todayDateSelected": "I dag, {date, date, full} har valts"
+ "todayDate": "Idag, {date}",
+ "todayDateSelected": "Idag, {date} har valts"
}
diff --git a/packages/@react-aria/calendar/intl/tr-TR.json b/packages/@react-aria/calendar/intl/tr-TR.json
index b114ee88427..a784738e452 100644
--- a/packages/@react-aria/calendar/intl/tr-TR.json
+++ b/packages/@react-aria/calendar/intl/tr-TR.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "{date, date, full} seçildi",
+ "dateRange": "{startDate} - {endDate}",
+ "dateSelected": "{date} seçildi",
"finishRangeSelectionPrompt": "Tarih aralığı seçimini tamamlamak için tıklayın",
+ "maximumDate": "Son müsait tarih",
+ "minimumDate": "İlk müsait tarih",
"next": "Sonraki",
"previous": "Önceki",
- "selectedDateDescription": "Seçili Tarih: {date, date, full}",
- "selectedRangeDescription": "Seçili Aralık: {start, date, long} - {end, date, long}",
+ "selectedDateDescription": "Seçilen Tarih: {date}",
+ "selectedRangeDescription": "Seçilen Aralık: {dateRange}",
"startRangeSelectionPrompt": "Tarih aralığı seçimini başlatmak için tıklayın",
- "todayDate": "Bugün, {date, date, full}",
- "todayDateSelected": "Bugün, {date, date, full} seçildi"
+ "todayDate": "Bugün, {date}",
+ "todayDateSelected": "Bugün, {date} seçildi"
}
diff --git a/packages/@react-aria/calendar/intl/uk-UA.json b/packages/@react-aria/calendar/intl/uk-UA.json
index cd08b0fef98..40aac39533e 100644
--- a/packages/@react-aria/calendar/intl/uk-UA.json
+++ b/packages/@react-aria/calendar/intl/uk-UA.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "Вибрано: {date, date, full}",
+ "dateRange": "{startDate} — {endDate}",
+ "dateSelected": "Вибрано {date}",
"finishRangeSelectionPrompt": "Натисніть, щоб завершити вибір діапазону дат",
+ "maximumDate": "Остання доступна дата",
+ "minimumDate": "Перша доступна дата",
"next": "Наступний",
"previous": "Попередній",
- "selectedDateDescription": "Вибрана дата: {date, date, full}",
- "selectedRangeDescription": "Вибраний діапазон: від {start, date, long} до {end, date, long}",
+ "selectedDateDescription": "Вибрана дата: {date}",
+ "selectedRangeDescription": "Вибраний діапазон: {dateRange}",
"startRangeSelectionPrompt": "Натисніть, щоб почати вибір діапазону дат",
- "todayDate": "Сьогодні: {date, date, full}",
- "todayDateSelected": "Сьогодні вибрано: {date, date, full}"
+ "todayDate": "Сьогодні, {date}",
+ "todayDateSelected": "Сьогодні, вибрано {date}"
}
diff --git a/packages/@react-aria/calendar/intl/zh-CN.json b/packages/@react-aria/calendar/intl/zh-CN.json
index 28327115ba1..2b9d8285ffd 100644
--- a/packages/@react-aria/calendar/intl/zh-CN.json
+++ b/packages/@react-aria/calendar/intl/zh-CN.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "已选择 {date, date, full}",
+ "dateRange": "{startDate} 至 {endDate}",
+ "dateSelected": "已选定 {date}",
"finishRangeSelectionPrompt": "单击以完成选择日期范围",
+ "maximumDate": "最后一个可用日期",
+ "minimumDate": "第一个可用日期",
"next": "下一页",
"previous": "上一页",
- "selectedDateDescription": "选定的日期: {date, date, full}",
- "selectedRangeDescription": "选定的范围: {start, date, long} 到 {end, date, long}",
+ "selectedDateDescription": "选定的日期:{date}",
+ "selectedRangeDescription": "选定的范围:{dateRange}",
"startRangeSelectionPrompt": "单击以开始选择日期范围",
- "todayDate": "今天({date, date, full})",
- "todayDateSelected": "已选择今天({date, date, full})"
+ "todayDate": "今天,即 {date}",
+ "todayDateSelected": "已选定今天,即 {date}"
}
diff --git a/packages/@react-aria/calendar/intl/zh-TW.json b/packages/@react-aria/calendar/intl/zh-TW.json
index 0e0b178aba8..5b0dbd837b4 100644
--- a/packages/@react-aria/calendar/intl/zh-TW.json
+++ b/packages/@react-aria/calendar/intl/zh-TW.json
@@ -1,11 +1,14 @@
{
- "dateSelected": "已選取 {date, date, full}",
+ "dateRange": "{startDate} 至 {endDate}",
+ "dateSelected": "已選取 {date}",
"finishRangeSelectionPrompt": "按一下以完成選取日期範圍",
+ "maximumDate": "最後一個可用日期",
+ "minimumDate": "第一個可用日期",
"next": "下一頁",
"previous": "上一頁",
- "selectedDateDescription": "選取日期: {date, date, full}",
- "selectedRangeDescription": "選取範圍: {start, date, long} 至 {end, date, long}",
+ "selectedDateDescription": "選定的日期:{date}",
+ "selectedRangeDescription": "選定的範圍:{dateRange}",
"startRangeSelectionPrompt": "按一下以開始選取日期範圍",
- "todayDate": "今日,{date, date, full}",
- "todayDateSelected": "今日,已選取 {date, date, full}"
+ "todayDate": "今天,{date}",
+ "todayDateSelected": "已選取今天,{date}"
}
diff --git a/packages/@react-aria/calendar/package.json b/packages/@react-aria/calendar/package.json
index 0d7b78afdfc..acabb77f6a4 100644
--- a/packages/@react-aria/calendar/package.json
+++ b/packages/@react-aria/calendar/package.json
@@ -1,10 +1,15 @@
{
"name": "@react-aria/calendar",
- "version": "3.0.0-alpha.4",
+ "version": "3.2.0",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
"module": "dist/module.js",
+ "exports": {
+ "types": "./dist/types.d.ts",
+ "import": "./dist/import.mjs",
+ "require": "./dist/main.js"
+ },
"types": "dist/types.d.ts",
"source": "src/index.ts",
"files": [
@@ -17,21 +22,20 @@
"url": "https://github.com/adobe/react-spectrum"
},
"dependencies": {
- "@babel/runtime": "^7.6.2",
- "@internationalized/date": "3.0.0-alpha.4",
- "@react-aria/i18n": "^3.3.8",
- "@react-aria/interactions": "^3.8.3",
- "@react-aria/live-announcer": "^3.0.5",
- "@react-aria/utils": "^3.11.2",
- "@react-stately/calendar": "3.0.0-alpha.4",
- "@react-types/button": "^3.4.3",
- "@react-types/calendar": "3.0.0-alpha.4",
- "@react-types/shared": "^3.11.1",
- "date-fns": "^1.30.1"
+ "@internationalized/date": "^3.2.0",
+ "@react-aria/i18n": "^3.7.1",
+ "@react-aria/interactions": "^3.15.0",
+ "@react-aria/live-announcer": "^3.3.0",
+ "@react-aria/utils": "^3.16.0",
+ "@react-stately/calendar": "^3.2.0",
+ "@react-types/button": "^3.7.2",
+ "@react-types/calendar": "^3.2.0",
+ "@react-types/shared": "^3.18.0",
+ "@swc/helpers": "^0.4.14"
},
"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 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-aria/calendar/src/index.ts b/packages/@react-aria/calendar/src/index.ts
index a190826aba7..1676e603a42 100644
--- a/packages/@react-aria/calendar/src/index.ts
+++ b/packages/@react-aria/calendar/src/index.ts
@@ -10,8 +10,12 @@
* governing permissions and limitations under the License.
*/
-export * from './useCalendar';
-export * from './useRangeCalendar';
-export * from './useCalendarGrid';
-export * from './useCalendarCell';
-export * from './types';
+export {useCalendar} from './useCalendar';
+export {useRangeCalendar} from './useRangeCalendar';
+export {useCalendarGrid} from './useCalendarGrid';
+export {useCalendarCell} from './useCalendarCell';
+
+export type {AriaCalendarProps, AriaRangeCalendarProps, CalendarProps, DateValue, RangeCalendarProps} from '@react-types/calendar';
+export type {CalendarAria} from './useCalendarBase';
+export type {AriaCalendarGridProps, CalendarGridAria} from './useCalendarGrid';
+export type {AriaCalendarCellProps, CalendarCellAria} from './useCalendarCell';
diff --git a/packages/@react-aria/calendar/src/types.ts b/packages/@react-aria/calendar/src/types.ts
deleted file mode 100644
index a68bfe65814..00000000000
--- a/packages/@react-aria/calendar/src/types.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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 {AriaButtonProps} from '@react-types/button';
-import {HTMLAttributes} from 'react';
-
-export interface CalendarAria {
- /** Props for the calendar grouping element. */
- calendarProps: HTMLAttributes,
- /** Props for the next button. */
- nextButtonProps: AriaButtonProps,
- /** Props for the previous button. */
- prevButtonProps: AriaButtonProps,
- /** A description of the visible date range, for use in the calendar title. */
- title: string
-}
-
-
-export interface CalendarGridAria {
- /** Props for the date grid element (e.g. ``). */
- gridProps: HTMLAttributes,
- /** A list of week days formatted for the current locale, typically used in column headers. */
- weekDays: WeekDay[]
-}
-
-interface WeekDay {
- /** A short name (e.g. single letter) for the day. */
- narrow: string,
- /** The full day name. If not displayed visually, it should be used as the accessiblity name. */
- long: string
-}
diff --git a/packages/@react-aria/calendar/src/useCalendar.ts b/packages/@react-aria/calendar/src/useCalendar.ts
index 1c9af8f480b..40ac2395117 100644
--- a/packages/@react-aria/calendar/src/useCalendar.ts
+++ b/packages/@react-aria/calendar/src/useCalendar.ts
@@ -10,15 +10,14 @@
* governing permissions and limitations under the License.
*/
-import {CalendarAria} from './types';
-import {CalendarProps, DateValue} from '@react-types/calendar';
+import {AriaCalendarProps, DateValue} from '@react-types/calendar';
+import {CalendarAria, useCalendarBase} from './useCalendarBase';
import {CalendarState} from '@react-stately/calendar';
-import {useCalendarBase} from './useCalendarBase';
/**
* Provides the behavior and accessibility implementation for a calendar component.
* A calendar displays one or more date grids and allows users to select a single date.
*/
-export function useCalendar(props: CalendarProps, state: CalendarState): CalendarAria {
+export function useCalendar(props: AriaCalendarProps, state: CalendarState): CalendarAria {
return useCalendarBase(props, state);
}
diff --git a/packages/@react-aria/calendar/src/useCalendarBase.ts b/packages/@react-aria/calendar/src/useCalendarBase.ts
index 3fd8892d80a..3c7ba57b9d6 100644
--- a/packages/@react-aria/calendar/src/useCalendarBase.ts
+++ b/packages/@react-aria/calendar/src/useCalendarBase.ts
@@ -11,22 +11,37 @@
*/
import {announce} from '@react-aria/live-announcer';
-import {CalendarAria} from './types';
-import {calendarIds, useSelectedDateDescription, useVisibleRangeDescription} from './utils';
+import {AriaButtonProps} from '@react-types/button';
+import {AriaLabelingProps, DOMAttributes} from '@react-types/shared';
import {CalendarPropsBase} from '@react-types/calendar';
import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
import {DOMProps} from '@react-types/shared';
+import {filterDOMProps, mergeProps, useLabels, useSlotId, useUpdateEffect} from '@react-aria/utils';
+import {hookData, useSelectedDateDescription, useVisibleRangeDescription} from './utils';
// @ts-ignore
import intlMessages from '../intl/*.json';
-import {mergeProps, useDescription, useId, useUpdateEffect} from '@react-aria/utils';
-import {useMessageFormatter} from '@react-aria/i18n';
+import {useLocalizedStringFormatter} from '@react-aria/i18n';
import {useRef} from 'react';
-export function useCalendarBase(props: CalendarPropsBase & DOMProps, state: CalendarState | RangeCalendarState): CalendarAria {
- let formatMessage = useMessageFormatter(intlMessages);
- let calendarId = useId(props.id);
+export interface CalendarAria {
+ /** Props for the calendar grouping element. */
+ calendarProps: DOMAttributes,
+ /** Props for the next button. */
+ nextButtonProps: AriaButtonProps,
+ /** Props for the previous button. */
+ prevButtonProps: AriaButtonProps,
+ /** Props for the error message element, if any. */
+ errorMessageProps: DOMAttributes,
+ /** A description of the visible date range, for use in the calendar title. */
+ title: string
+}
+
+export function useCalendarBase(props: CalendarPropsBase & DOMProps & AriaLabelingProps, state: CalendarState | RangeCalendarState): CalendarAria {
+ let stringFormatter = useLocalizedStringFormatter(intlMessages);
+ let domProps = filterDOMProps(props);
- let visibleRangeDescription = useVisibleRangeDescription(state.visibleRange.start, state.visibleRange.end, state.timeZone);
+ let title = useVisibleRangeDescription(state.visibleRange.start, state.visibleRange.end, state.timeZone, false);
+ let visibleRangeDescription = useVisibleRangeDescription(state.visibleRange.start, state.visibleRange.end, state.timeZone, true);
// Announce when the visible date range changes
useUpdateEffect(() => {
@@ -45,10 +60,15 @@ export function useCalendarBase(props: CalendarPropsBase & DOMProps, state: Cale
// handle an update to the caption that describes the currently selected range, to announce the new value
}, [selectedDateDescription]);
- let descriptionProps = useDescription(visibleRangeDescription);
+ let errorMessageId = useSlotId([Boolean(props.errorMessage), props.validationState]);
- // Label the child grid elements by the group element if it is labelled.
- calendarIds.set(state, props['aria-label'] || props['aria-labelledby'] ? calendarId : null);
+ // Pass the label to the child grid elements.
+ hookData.set(state, {
+ ariaLabel: props['aria-label'],
+ ariaLabelledBy: props['aria-labelledby'],
+ errorMessageId,
+ selectedDateDescription
+ });
// If the next or previous buttons become disabled while they are focused, move focus to the calendar body.
let nextFocused = useRef(false);
@@ -65,27 +85,34 @@ export function useCalendarBase(props: CalendarPropsBase & DOMProps, state: Cale
state.setFocused(true);
}
+ let labelProps = useLabels({
+ id: props['id'],
+ 'aria-label': [props['aria-label'], visibleRangeDescription].filter(Boolean).join(', '),
+ 'aria-labelledby': props['aria-labelledby']
+ });
+
return {
- calendarProps: mergeProps(descriptionProps, {
+ calendarProps: mergeProps(domProps, labelProps, {
role: 'group',
- id: calendarId,
- 'aria-label': props['aria-label'],
- 'aria-labelledby': props['aria-labelledby']
+ 'aria-describedby': props['aria-describedby'] || undefined
}),
nextButtonProps: {
onPress: () => state.focusNextPage(),
- 'aria-label': formatMessage('next'),
+ 'aria-label': stringFormatter.format('next'),
isDisabled: nextDisabled,
onFocus: () => nextFocused.current = true,
onBlur: () => nextFocused.current = false
},
prevButtonProps: {
onPress: () => state.focusPreviousPage(),
- 'aria-label': formatMessage('previous'),
+ 'aria-label': stringFormatter.format('previous'),
isDisabled: previousDisabled,
onFocus: () => previousFocused.current = true,
onBlur: () => previousFocused.current = false
},
- title: visibleRangeDescription
+ errorMessageProps: {
+ id: errorMessageId
+ },
+ title
};
}
diff --git a/packages/@react-aria/calendar/src/useCalendarCell.ts b/packages/@react-aria/calendar/src/useCalendarCell.ts
index f52b0100c30..6d93e064bf3 100644
--- a/packages/@react-aria/calendar/src/useCalendarCell.ts
+++ b/packages/@react-aria/calendar/src/useCalendarCell.ts
@@ -12,13 +12,15 @@
import {CalendarDate, isEqualDay, isSameDay, isToday} from '@internationalized/date';
import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
-import {focusWithoutScrolling} from '@react-aria/utils';
-import {HTMLAttributes, RefObject, useEffect, useMemo, useRef} from 'react';
+import {DOMAttributes} from '@react-types/shared';
+import {focusWithoutScrolling, getScrollParent, scrollIntoViewport, useDescription} from '@react-aria/utils';
+import {getEraFormat, hookData} from './utils';
+import {getInteractionModality, usePress} from '@react-aria/interactions';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {mergeProps} from '@react-aria/utils';
-import {useDateFormatter, useMessageFormatter} from '@react-aria/i18n';
-import {usePress} from '@react-aria/interactions';
+import {RefObject, useEffect, useMemo, useRef} from 'react';
+import {useDateFormatter, useLocalizedStringFormatter} from '@react-aria/i18n';
export interface AriaCalendarCellProps {
/** The date that this cell represents. */
@@ -30,11 +32,11 @@ export interface AriaCalendarCellProps {
isDisabled?: boolean
}
-interface CalendarCellAria {
+export interface CalendarCellAria {
/** Props for the grid cell element (e.g. ``). */
- cellProps: HTMLAttributes,
+ cellProps: DOMAttributes,
/** Props for the button element within the cell. */
- buttonProps: HTMLAttributes,
+ buttonProps: DOMAttributes,
/** Whether the cell is currently being pressed. */
isPressed: boolean,
/** Whether the cell is selected. */
@@ -61,6 +63,8 @@ interface CalendarCellAria {
* For example, dates before the first day of a month in the same week.
*/
isOutsideVisibleRange: boolean,
+ /** Whether the cell is part of an invalid selection. */
+ isInvalid: boolean,
/** The day number formatted according to the current locale. */
formattedDate: string
}
@@ -71,13 +75,14 @@ interface CalendarCellAria {
*/
export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarState | RangeCalendarState, ref: RefObject): CalendarCellAria {
let {date, isDisabled} = props;
- let formatMessage = useMessageFormatter(intlMessages);
+ let {errorMessageId, selectedDateDescription} = hookData.get(state);
+ let stringFormatter = useLocalizedStringFormatter(intlMessages);
let dateFormatter = useDateFormatter({
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
- era: date.calendar.identifier !== 'gregory' ? 'long' : undefined,
+ era: getEraFormat(date),
timeZone: state.timeZone
});
let isSelected = state.isSelected(date);
@@ -85,6 +90,15 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
isDisabled = isDisabled || state.isCellDisabled(date);
let isUnavailable = state.isCellUnavailable(date);
let isSelectable = !isDisabled && !isUnavailable;
+ let isInvalid = state.validationState === 'invalid' && (
+ 'highlightedRange' in state
+ ? !state.anchorDate && state.highlightedRange && date.compare(state.highlightedRange.start) >= 0 && date.compare(state.highlightedRange.end) <= 0
+ : state.value && isSameDay(state.value, date)
+ );
+
+ if (isInvalid) {
+ isSelected = true;
+ }
// For performance, reuse the same date object as before if the new date prop is the same.
// This allows subsequent useMemo results to be reused.
@@ -100,40 +114,56 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
// aria-label should be localize Day of week, Month, Day and Year without Time.
let isDateToday = isToday(date, state.timeZone);
let label = useMemo(() => {
+ let label = '';
+
+ // If this is a range calendar, add a description of the full selected range
+ // to the first and last selected date.
+ if (
+ 'highlightedRange' in state &&
+ state.value &&
+ !state.anchorDate &&
+ (isSameDay(date, state.value.start) || isSameDay(date, state.value.end))
+ ) {
+ label = selectedDateDescription + ', ';
+ }
+
+ label += dateFormatter.format(nativeDate);
if (isDateToday) {
// If date is today, set appropriate string depending on selected state:
- return formatMessage(isSelected ? 'todayDateSelected' : 'todayDate', {
- date: nativeDate
+ label = stringFormatter.format(isSelected ? 'todayDateSelected' : 'todayDate', {
+ date: label
});
} else if (isSelected) {
// If date is selected but not today:
- return formatMessage('dateSelected', {
- date: nativeDate
+ label = stringFormatter.format('dateSelected', {
+ date: label
});
}
- return dateFormatter.format(nativeDate);
- }, [dateFormatter, nativeDate, formatMessage, isSelected, isDateToday]);
+ if (state.minValue && isSameDay(date, state.minValue)) {
+ label += ', ' + stringFormatter.format('minimumDate');
+ } else if (state.maxValue && isSameDay(date, state.maxValue)) {
+ label += ', ' + stringFormatter.format('maximumDate');
+ }
+
+ return label;
+ }, [dateFormatter, nativeDate, stringFormatter, isSelected, isDateToday, date, state, selectedDateDescription]);
// When a cell is focused and this is a range calendar, add a prompt to help
// screenreader users know that they are in a range selection mode.
+ let rangeSelectionPrompt = '';
if ('anchorDate' in state && isFocused && !state.isReadOnly && isSelectable) {
- let rangeSelectionPrompt = '';
-
// If selection has started add "click to finish selecting range"
if (state.anchorDate) {
- rangeSelectionPrompt = formatMessage('finishRangeSelectionPrompt');
+ rangeSelectionPrompt = stringFormatter.format('finishRangeSelectionPrompt');
// Otherwise, add "click to start selecting range" prompt
} else {
- rangeSelectionPrompt = formatMessage('startRangeSelectionPrompt');
- }
-
- // Append to aria-label
- if (rangeSelectionPrompt) {
- label = `${label} (${rangeSelectionPrompt})`;
+ rangeSelectionPrompt = stringFormatter.format('startRangeSelectionPrompt');
}
}
+ let descriptionProps = useDescription(rangeSelectionPrompt);
+
let isAnchorPressed = useRef(false);
let isRangeBoundaryPressed = useRef(false);
let touchDragTimerRef = useRef(null);
@@ -142,7 +172,7 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
// again to trigger onPressStart. Cancel presses immediately when the pointer exits.
shouldCancelOnPointerExit: 'anchorDate' in state && !!state.anchorDate,
preventFocusOnPress: true,
- isDisabled: !isSelectable,
+ isDisabled: !isSelectable || state.isReadOnly,
onPressStart(e) {
if (state.isReadOnly) {
state.setFocusedDate(date);
@@ -152,7 +182,9 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
if ('highlightedRange' in state && !state.anchorDate && (e.pointerType === 'mouse' || e.pointerType === 'touch')) {
// Allow dragging the start or end date of a range to modify it
// rather than starting a new selection.
- if (state.highlightedRange) {
+ // Don't allow dragging when invalid, or weird jumping behavior may occur as date ranges
+ // are constrained to available dates. The user will need to select a new range in this case.
+ if (state.highlightedRange && !isInvalid) {
if (isSameDay(date, state.highlightedRange.start)) {
state.setAnchorDate(state.highlightedRange.end);
state.setFocusedDate(date);
@@ -253,6 +285,14 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
useEffect(() => {
if (isFocused && ref.current) {
focusWithoutScrolling(ref.current);
+
+ // Scroll into view if navigating with a keyboard, otherwise
+ // try not to shift the view under the user's mouse/finger.
+ // If in a overlay, scrollIntoViewport will only cause scrolling
+ // up to the overlay scroll body to prevent overlay shifting
+ if (getInteractionModality() !== 'pointer') {
+ scrollIntoViewport(ref.current, {containingElement: getScrollParent(ref.current)});
+ }
}
}, [isFocused, ref]);
@@ -262,13 +302,14 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
calendar: date.calendar.identifier
});
- let formattedDate = useMemo(() => cellDateFormatter.format(nativeDate), [cellDateFormatter, nativeDate]);
+ let formattedDate = useMemo(() => cellDateFormatter.formatToParts(nativeDate).find(part => part.type === 'day').value, [cellDateFormatter, nativeDate]);
return {
cellProps: {
role: 'gridcell',
'aria-disabled': !isSelectable || null,
- 'aria-selected': isSelectable ? isSelected : null
+ 'aria-selected': isSelected || null,
+ 'aria-invalid': isInvalid || null
},
buttonProps: mergeProps(pressProps, {
onFocus() {
@@ -280,6 +321,11 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
role: 'button',
'aria-disabled': !isSelectable || null,
'aria-label': label,
+ 'aria-invalid': isInvalid || null,
+ 'aria-describedby': [
+ isInvalid ? errorMessageId : null,
+ descriptionProps['aria-describedby']
+ ].filter(Boolean).join(' ') || undefined,
onPointerEnter(e) {
// Highlight the date on hover or drag over a date when selecting a range.
if ('highlightDate' in state && (e.pointerType !== 'touch' || state.isDragging) && isSelectable) {
@@ -305,6 +351,7 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
isDisabled,
isUnavailable,
isOutsideVisibleRange: date.compare(state.visibleRange.start) < 0 || date.compare(state.visibleRange.end) > 0,
+ isInvalid,
formattedDate
};
}
diff --git a/packages/@react-aria/calendar/src/useCalendarGrid.ts b/packages/@react-aria/calendar/src/useCalendarGrid.ts
index 546da036287..671924b2bbb 100644
--- a/packages/@react-aria/calendar/src/useCalendarGrid.ts
+++ b/packages/@react-aria/calendar/src/useCalendarGrid.ts
@@ -10,15 +10,15 @@
* governing permissions and limitations under the License.
*/
-import {CalendarDate, startOfWeek} from '@internationalized/date';
-import {CalendarGridAria} from './types';
-import {calendarIds, useSelectedDateDescription, useVisibleRangeDescription} from './utils';
+import {CalendarDate, startOfWeek, today} from '@internationalized/date';
import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
-import {KeyboardEvent} from 'react';
-import {mergeProps, useDescription, useLabels} from '@react-aria/utils';
+import {DOMAttributes} from '@react-types/shared';
+import {hookData, useVisibleRangeDescription} from './utils';
+import {KeyboardEvent, useMemo} from 'react';
+import {mergeProps, useLabels} from '@react-aria/utils';
import {useDateFormatter, useLocale} from '@react-aria/i18n';
-interface CalendarGridProps {
+export interface AriaCalendarGridProps {
/**
* The first date displayed in the calendar grid.
* Defaults to the first visible date in the calendar.
@@ -33,12 +33,21 @@ interface CalendarGridProps {
endDate?: CalendarDate
}
+export interface CalendarGridAria {
+ /** Props for the date grid element (e.g. ``). */
+ gridProps: DOMAttributes,
+ /** Props for the grid header element (e.g. ``). */
+ headerProps: DOMAttributes,
+ /** A list of week day abbreviations formatted for the current locale, typically used in column headers. */
+ weekDays: string[]
+}
+
/**
* Provides the behavior and accessibility implementation for a calendar grid component.
* A calendar grid displays a single grid of days within a calendar or range calendar which
* can be keyboard navigated and selected by the user.
*/
-export function useCalendarGrid(props: CalendarGridProps, state: CalendarState | RangeCalendarState): CalendarGridAria {
+export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarState | RangeCalendarState): CalendarGridAria {
let {
startDate = state.visibleRange.start,
endDate = state.visibleRange.end
@@ -55,30 +64,27 @@ export function useCalendarGrid(props: CalendarGridProps, state: CalendarState |
break;
case 'PageUp':
e.preventDefault();
- if (e.shiftKey) {
- state.focusPreviousSection();
- } else {
- state.focusPreviousPage();
- }
+ e.stopPropagation();
+ state.focusPreviousSection(e.shiftKey);
break;
case 'PageDown':
e.preventDefault();
- if (e.shiftKey) {
- state.focusNextSection();
- } else {
- state.focusNextPage();
- }
+ e.stopPropagation();
+ state.focusNextSection(e.shiftKey);
break;
case 'End':
e.preventDefault();
- state.focusPageEnd();
+ e.stopPropagation();
+ state.focusSectionEnd();
break;
case 'Home':
e.preventDefault();
- state.focusPageStart();
+ e.stopPropagation();
+ state.focusSectionStart();
break;
case 'ArrowLeft':
e.preventDefault();
+ e.stopPropagation();
if (direction === 'rtl') {
state.focusNextDay();
} else {
@@ -87,10 +93,12 @@ export function useCalendarGrid(props: CalendarGridProps, state: CalendarState |
break;
case 'ArrowUp':
e.preventDefault();
+ e.stopPropagation();
state.focusPreviousRow();
break;
case 'ArrowRight':
e.preventDefault();
+ e.stopPropagation();
if (direction === 'rtl') {
state.focusPreviousDay();
} else {
@@ -99,6 +107,7 @@ export function useCalendarGrid(props: CalendarGridProps, state: CalendarState |
break;
case 'ArrowDown':
e.preventDefault();
+ e.stopPropagation();
state.focusNextRow();
break;
case 'Escape':
@@ -111,32 +120,27 @@ export function useCalendarGrid(props: CalendarGridProps, state: CalendarState |
}
};
- let selectedDateDescription = useSelectedDateDescription(state);
- let descriptionProps = useDescription(selectedDateDescription);
- let visibleRangeDescription = useVisibleRangeDescription(startDate, endDate, state.timeZone);
+ let visibleRangeDescription = useVisibleRangeDescription(startDate, endDate, state.timeZone, true);
+ let {ariaLabel, ariaLabelledBy} = hookData.get(state);
let labelProps = useLabels({
- 'aria-label': visibleRangeDescription,
- 'aria-labelledby': calendarIds.get(state)
+ 'aria-label': [ariaLabel, visibleRangeDescription].filter(Boolean).join(', '),
+ 'aria-labelledby': ariaLabelledBy
});
let dayFormatter = useDateFormatter({weekday: 'narrow', timeZone: state.timeZone});
- let dayFormatterLong = useDateFormatter({weekday: 'long', timeZone: state.timeZone});
let {locale} = useLocale();
- let weekStart = startOfWeek(state.visibleRange.start, locale);
- let weekDays = [...new Array(7).keys()].map((index) => {
- let date = weekStart.add({days: index});
- let dateDay = date.toDate(state.timeZone);
- let narrow = dayFormatter.format(dateDay);
- let long = dayFormatterLong.format(dateDay);
- return {
- narrow,
- long
- };
- });
+ let weekDays = useMemo(() => {
+ let weekStart = startOfWeek(today(state.timeZone), locale);
+ return [...new Array(7).keys()].map((index) => {
+ let date = weekStart.add({days: index});
+ let dateDay = date.toDate(state.timeZone);
+ return dayFormatter.format(dateDay);
+ });
+ }, [locale, state.timeZone, dayFormatter]);
return {
- gridProps: mergeProps(descriptionProps, labelProps, {
+ gridProps: mergeProps(labelProps, {
role: 'grid',
'aria-readonly': state.isReadOnly || null,
'aria-disabled': state.isDisabled || null,
@@ -145,6 +149,11 @@ export function useCalendarGrid(props: CalendarGridProps, state: CalendarState |
onFocus: () => state.setFocused(true),
onBlur: () => state.setFocused(false)
}),
+ headerProps: {
+ // Column headers are hidden to screen readers to make navigating with a touch screen reader easier.
+ // The day names are already included in the label of each cell, so there's no need to announce them twice.
+ 'aria-hidden': true
+ },
weekDays
};
}
diff --git a/packages/@react-aria/calendar/src/useRangeCalendar.ts b/packages/@react-aria/calendar/src/useRangeCalendar.ts
index 806ae3e0ea2..603a380d283 100644
--- a/packages/@react-aria/calendar/src/useRangeCalendar.ts
+++ b/packages/@react-aria/calendar/src/useRangeCalendar.ts
@@ -10,21 +10,19 @@
* governing permissions and limitations under the License.
*/
-import {CalendarAria} from './types';
-import {DateValue, RangeCalendarProps} from '@react-types/calendar';
+import {AriaRangeCalendarProps, DateValue} from '@react-types/calendar';
+import {CalendarAria, useCalendarBase} from './useCalendarBase';
+import {FocusableElement} from '@react-types/shared';
import {RangeCalendarState} from '@react-stately/calendar';
import {RefObject, useRef} from 'react';
-import {useCalendarBase} from './useCalendarBase';
-import {useEvent, useId} from '@react-aria/utils';
+import {useEvent} from '@react-aria/utils';
/**
* Provides the behavior and accessibility implementation for a range calendar component.
* A range calendar displays one or more date grids and allows users to select a contiguous range of dates.
*/
-export function useRangeCalendar(props: RangeCalendarProps, state: RangeCalendarState, ref: RefObject): CalendarAria {
+export function useRangeCalendar(props: AriaRangeCalendarProps, state: RangeCalendarState, ref: RefObject): CalendarAria {
let res = useCalendarBase(props, state);
- res.nextButtonProps.id = useId();
- res.prevButtonProps.id = useId();
// We need to ignore virtual pointer events from VoiceOver due to these bugs.
// https://bugs.webkit.org/show_bug.cgi?id=222627
@@ -33,13 +31,14 @@ export function useRangeCalendar(props: RangeCalendarProps<
// We need to match that here otherwise this will fire before the press event in
// useCalendarCell, causing range selection to not work properly.
let isVirtualClick = useRef(false);
- useEvent(useRef(window), 'pointerdown', e => {
+ let windowRef = useRef(typeof window !== 'undefined' ? window : null);
+ useEvent(windowRef, 'pointerdown', e => {
isVirtualClick.current = e.width === 0 && e.height === 0;
});
// Stop range selection when pressing or releasing a pointer outside the calendar body,
// except when pressing the next or previous buttons to switch months.
- let endDragging = e => {
+ let endDragging = (e: PointerEvent) => {
if (isVirtualClick.current) {
isVirtualClick.current = false;
return;
@@ -50,19 +49,26 @@ export function useRangeCalendar(props: RangeCalendarProps<
return;
}
- let target = e.target as HTMLElement;
+ let target = e.target as Element;
let body = document.getElementById(res.calendarProps.id);
if (
- (!body.contains(target) || !target.closest('[role="button"]')) &&
- !document.getElementById(res.nextButtonProps.id)?.contains(target) &&
- !document.getElementById(res.prevButtonProps.id)?.contains(target)
+ body &&
+ body.contains(document.activeElement) &&
+ (!body.contains(target) || !target.closest('button, [role="button"]'))
) {
state.selectFocusedDate();
}
};
- useEvent(useRef(window), 'pointerup', endDragging);
- useEvent(useRef(window), 'pointercancel', endDragging);
+ useEvent(windowRef, 'pointerup', endDragging);
+ useEvent(windowRef, 'pointercancel', endDragging);
+
+ // Also stop range selection on blur, e.g. tabbing away from the calendar.
+ res.calendarProps.onBlur = e => {
+ if ((!e.relatedTarget || !ref.current.contains(e.relatedTarget)) && state.anchorDate) {
+ state.selectFocusedDate();
+ }
+ };
// Prevent touch scrolling while dragging
useEvent(ref, 'touchmove', e => {
diff --git a/packages/@react-aria/calendar/src/utils.ts b/packages/@react-aria/calendar/src/utils.ts
index 73af0c468c3..273dfd44442 100644
--- a/packages/@react-aria/calendar/src/utils.ts
+++ b/packages/@react-aria/calendar/src/utils.ts
@@ -10,17 +10,29 @@
* governing permissions and limitations under the License.
*/
-import {CalendarDate, endOfMonth, isSameDay, startOfMonth} from '@internationalized/date';
+import {CalendarDate, DateFormatter, endOfMonth, isSameDay, startOfMonth} from '@internationalized/date';
import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
// @ts-ignore
import intlMessages from '../intl/*.json';
-import {useDateFormatter, useMessageFormatter} from '@react-aria/i18n';
+import type {LocalizedStringFormatter} from '@internationalized/string';
+import {useDateFormatter, useLocalizedStringFormatter} from '@react-aria/i18n';
import {useMemo} from 'react';
-export const calendarIds = new WeakMap();
+interface HookData {
+ ariaLabel: string,
+ ariaLabelledBy: string,
+ errorMessageId: string,
+ selectedDateDescription: string
+}
+
+export const hookData = new WeakMap();
+
+export function getEraFormat(date: CalendarDate): 'short' | undefined {
+ return date?.calendar.identifier === 'gregory' && date.era === 'BC' ? 'short' : undefined;
+}
export function useSelectedDateDescription(state: CalendarState | RangeCalendarState) {
- let formatMessage = useMessageFormatter(intlMessages);
+ let stringFormatter = useLocalizedStringFormatter(intlMessages);
let start: CalendarDate, end: CalendarDate;
if ('highlightedRange' in state) {
@@ -29,6 +41,15 @@ export function useSelectedDateDescription(state: CalendarState | RangeCalendarS
start = end = state.value;
}
+ let dateFormatter = useDateFormatter({
+ weekday: 'long',
+ month: 'long',
+ year: 'numeric',
+ day: 'numeric',
+ era: getEraFormat(start) || getEraFormat(end),
+ timeZone: state.timeZone
+ });
+
let anchorDate = 'anchorDate' in state ? state.anchorDate : null;
return useMemo(() => {
// No message if currently selecting a range, or there is nothing highlighted.
@@ -36,26 +57,34 @@ export function useSelectedDateDescription(state: CalendarState | RangeCalendarS
// Use a single date message if the start and end dates are the same day,
// otherwise include both dates.
if (isSameDay(start, end)) {
- return formatMessage('selectedDateDescription', {date: start.toDate(state.timeZone)});
+ let date = dateFormatter.format(start.toDate(state.timeZone));
+ return stringFormatter.format('selectedDateDescription', {date});
} else {
- return formatMessage('selectedRangeDescription', {start: start.toDate(state.timeZone), end: end.toDate(state.timeZone)});
+ let dateRange = formatRange(dateFormatter, stringFormatter, start, end, state.timeZone);
+
+ return stringFormatter.format('selectedRangeDescription', {dateRange});
}
}
return '';
- }, [start, end, anchorDate, state.timeZone, formatMessage]);
+ }, [start, end, anchorDate, state.timeZone, stringFormatter, dateFormatter]);
}
-export function useVisibleRangeDescription(startDate: CalendarDate, endDate: CalendarDate, timeZone: string) {
+export function useVisibleRangeDescription(startDate: CalendarDate, endDate: CalendarDate, timeZone: string, isAria: boolean) {
+ let stringFormatter = useLocalizedStringFormatter(intlMessages);
+ let era: any = getEraFormat(startDate) || getEraFormat(endDate);
let monthFormatter = useDateFormatter({
month: 'long',
year: 'numeric',
- era: startDate.calendar.identifier !== 'gregory' ? 'long' : undefined,
+ era,
calendar: startDate.calendar.identifier,
timeZone
});
let dateFormatter = useDateFormatter({
- dateStyle: 'long',
+ month: 'long',
+ year: 'numeric',
+ day: 'numeric',
+ era,
calendar: startDate.calendar.identifier,
timeZone
});
@@ -67,10 +96,43 @@ export function useVisibleRangeDescription(startDate: CalendarDate, endDate: Cal
if (isSameDay(endDate, endOfMonth(startDate))) {
return monthFormatter.format(startDate.toDate(timeZone));
} else if (isSameDay(endDate, endOfMonth(endDate))) {
- return monthFormatter.formatRange(startDate.toDate(timeZone), endDate.toDate(timeZone));
+ return isAria
+ ? formatRange(monthFormatter, stringFormatter, startDate, endDate, timeZone)
+ : monthFormatter.formatRange(startDate.toDate(timeZone), endDate.toDate(timeZone));
}
}
- return dateFormatter.formatRange(startDate.toDate(timeZone), endDate.toDate(timeZone));
- }, [startDate, endDate, monthFormatter, dateFormatter, timeZone]);
+ return isAria
+ ? formatRange(dateFormatter, stringFormatter, startDate, endDate, timeZone)
+ : dateFormatter.formatRange(startDate.toDate(timeZone), endDate.toDate(timeZone));
+ }, [startDate, endDate, monthFormatter, dateFormatter, stringFormatter, timeZone, isAria]);
+}
+
+function formatRange(dateFormatter: DateFormatter, stringFormatter: LocalizedStringFormatter, start: CalendarDate, end: CalendarDate, timeZone: string) {
+ let parts = dateFormatter.formatRangeToParts(start.toDate(timeZone), end.toDate(timeZone));
+
+ // Find the separator between the start and end date. This is determined
+ // by finding the last shared literal before the end range.
+ let separatorIndex = -1;
+ for (let i = 0; i < parts.length; i++) {
+ let part = parts[i];
+ if (part.source === 'shared' && part.type === 'literal') {
+ separatorIndex = i;
+ } else if (part.source === 'endRange') {
+ break;
+ }
+ }
+
+ // Now we can combine the parts into start and end strings.
+ let startValue = '';
+ let endValue = '';
+ for (let i = 0; i < parts.length; i++) {
+ if (i < separatorIndex) {
+ startValue += parts[i].value;
+ } else if (i > separatorIndex) {
+ endValue += parts[i].value;
+ }
+ }
+
+ return stringFormatter.format('dateRange', {startDate: startValue, endDate: endValue});
}
diff --git a/packages/@react-aria/calendar/test/useCalendar.test.js b/packages/@react-aria/calendar/test/useCalendar.test.js
index f61b9523e68..b1acb428672 100644
--- a/packages/@react-aria/calendar/test/useCalendar.test.js
+++ b/packages/@react-aria/calendar/test/useCalendar.test.js
@@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/
-import {act, fireEvent, render} from '@testing-library/react';
+import {act, fireEvent, render} from '@react-spectrum/test-utils';
import {CalendarDate} from '@internationalized/date';
import {Example} from '../stories/Example';
import React from 'react';
@@ -56,90 +56,90 @@ describe('useCalendar', () => {
describe('visibleDuration: 3 days', () => {
it('should move the focused date by one day with the left/right arrows', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 – 6, 2019', 'ArrowLeft', 1, 'Tuesday, June 4, 2019', 'June 4 – 6, 2019', {visibleDuration: {days: 3}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 – 6, 2019', 'ArrowLeft', 2, 'Monday, June 3, 2019', 'June 1 – 3, 2019', {visibleDuration: {days: 3}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 – 6, 2019', 'ArrowRight', 1, 'Thursday, June 6, 2019', 'June 4 – 6, 2019', {visibleDuration: {days: 3}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 – 6, 2019', 'ArrowRight', 2, 'Friday, June 7, 2019', 'June 7 – 9, 2019', {visibleDuration: {days: 3}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 to 6, 2019', 'ArrowLeft', 1, 'Tuesday, June 4, 2019', 'June 4 to 6, 2019', {visibleDuration: {days: 3}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 to 6, 2019', 'ArrowLeft', 2, 'Monday, June 3, 2019', 'June 1 to 3, 2019', {visibleDuration: {days: 3}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 to 6, 2019', 'ArrowRight', 1, 'Thursday, June 6, 2019', 'June 4 to 6, 2019', {visibleDuration: {days: 3}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 to 6, 2019', 'ArrowRight', 2, 'Friday, June 7, 2019', 'June 7 to 9, 2019', {visibleDuration: {days: 3}});
});
it('should move the focused date by one row with the up/down arrows', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 – 6, 2019', 'ArrowUp', 1, 'Sunday, June 2, 2019', 'June 1 – 3, 2019', {visibleDuration: {days: 3}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 – 6, 2019', 'ArrowDown', 1, 'Saturday, June 8, 2019', 'June 7 – 9, 2019', {visibleDuration: {days: 3}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 to 6, 2019', 'ArrowUp', 1, 'Sunday, June 2, 2019', 'June 1 to 3, 2019', {visibleDuration: {days: 3}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 to 6, 2019', 'ArrowDown', 1, 'Saturday, June 8, 2019', 'June 7 to 9, 2019', {visibleDuration: {days: 3}});
});
it('should move the focused date by one row with the page up/page down arrows', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 – 6, 2019', 'PageUp', 1, 'Sunday, June 2, 2019', 'June 1 – 3, 2019', {visibleDuration: {days: 3}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 – 6, 2019', 'PageDown', 1, 'Saturday, June 8, 2019', 'June 7 – 9, 2019', {visibleDuration: {days: 3}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 to 6, 2019', 'PageUp', 1, 'Sunday, June 2, 2019', 'June 1 to 3, 2019', {visibleDuration: {days: 3}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 to 6, 2019', 'PageDown', 1, 'Saturday, June 8, 2019', 'June 7 to 9, 2019', {visibleDuration: {days: 3}});
});
it('should move the focused date by one row with the shift + page up/page down arrows', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 – 6, 2019', 'PageUp', 1, 'Sunday, June 2, 2019', 'June 1 – 3, 2019', {visibleDuration: {days: 3}}, {shiftKey: true});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 – 6, 2019', 'PageDown', 1, 'Saturday, June 8, 2019', 'June 7 – 9, 2019', {visibleDuration: {days: 3}}, {shiftKey: true});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 to 6, 2019', 'PageUp', 1, 'Sunday, June 2, 2019', 'June 1 to 3, 2019', {visibleDuration: {days: 3}}, {shiftKey: true});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 to 6, 2019', 'PageDown', 1, 'Saturday, June 8, 2019', 'June 7 to 9, 2019', {visibleDuration: {days: 3}}, {shiftKey: true});
});
it('should move the focused date to the start/end of the visible range with the home/end keys', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 – 6, 2019', 'Home', 1, 'Tuesday, June 4, 2019', 'June 4 – 6, 2019', {visibleDuration: {days: 3}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 – 6, 2019', 'End', 1, 'Thursday, June 6, 2019', 'June 4 – 6, 2019', {visibleDuration: {days: 3}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 to 6, 2019', 'Home', 1, 'Tuesday, June 4, 2019', 'June 4 to 6, 2019', {visibleDuration: {days: 3}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 to 6, 2019', 'End', 1, 'Thursday, June 6, 2019', 'June 4 to 6, 2019', {visibleDuration: {days: 3}});
});
});
describe('visibleDuration: 1 week', () => {
it('should move the focused date by one day with the left/right arrows', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 8, 2019', 'ArrowLeft', 1, 'Tuesday, June 4, 2019', 'June 2 – 8, 2019', {visibleDuration: {weeks: 1}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 8, 2019', 'ArrowLeft', 4, 'Saturday, June 1, 2019', 'May 26 – June 1, 2019', {visibleDuration: {weeks: 1}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 8, 2019', 'ArrowRight', 1, 'Thursday, June 6, 2019', 'June 2 – 8, 2019', {visibleDuration: {weeks: 1}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 8, 2019', 'ArrowRight', 4, 'Sunday, June 9, 2019', 'June 9 – 15, 2019', {visibleDuration: {weeks: 1}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 8, 2019', 'ArrowLeft', 1, 'Tuesday, June 4, 2019', 'June 2 to 8, 2019', {visibleDuration: {weeks: 1}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 8, 2019', 'ArrowLeft', 4, 'Saturday, June 1, 2019', 'May 26 to June 1, 2019', {visibleDuration: {weeks: 1}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 8, 2019', 'ArrowRight', 1, 'Thursday, June 6, 2019', 'June 2 to 8, 2019', {visibleDuration: {weeks: 1}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 8, 2019', 'ArrowRight', 4, 'Sunday, June 9, 2019', 'June 9 to 15, 2019', {visibleDuration: {weeks: 1}});
});
it('should move the focused date by one week with the up/down arrows', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 8, 2019', 'ArrowUp', 1, 'Wednesday, May 29, 2019', 'May 26 – June 1, 2019', {visibleDuration: {weeks: 1}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 8, 2019', 'ArrowDown', 1, 'Wednesday, June 12, 2019', 'June 9 – 15, 2019', {visibleDuration: {weeks: 1}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 8, 2019', 'ArrowUp', 1, 'Wednesday, May 29, 2019', 'May 26 to June 1, 2019', {visibleDuration: {weeks: 1}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 8, 2019', 'ArrowDown', 1, 'Wednesday, June 12, 2019', 'June 9 to 15, 2019', {visibleDuration: {weeks: 1}});
});
it('should move the focused date by one week with the page up/page down arrows', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 8, 2019', 'PageUp', 1, 'Wednesday, May 29, 2019', 'May 26 – June 1, 2019', {visibleDuration: {weeks: 1}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 8, 2019', 'PageDown', 1, 'Wednesday, June 12, 2019', 'June 9 – 15, 2019', {visibleDuration: {weeks: 1}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 8, 2019', 'PageUp', 1, 'Wednesday, May 29, 2019', 'May 26 to June 1, 2019', {visibleDuration: {weeks: 1}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 8, 2019', 'PageDown', 1, 'Wednesday, June 12, 2019', 'June 9 to 15, 2019', {visibleDuration: {weeks: 1}});
});
it('should move the focused date by one month with the shift + page up/page down arrows', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 8, 2019', 'PageUp', 1, 'Sunday, May 5, 2019', 'May 5 – 11, 2019', {visibleDuration: {weeks: 1}}, {shiftKey: true});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 8, 2019', 'PageDown', 1, 'Friday, July 5, 2019', 'June 30 – July 6, 2019', {visibleDuration: {weeks: 1}}, {shiftKey: true});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 8, 2019', 'PageUp', 1, 'Sunday, May 5, 2019', 'May 5 to 11, 2019', {visibleDuration: {weeks: 1}}, {shiftKey: true});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 8, 2019', 'PageDown', 1, 'Friday, July 5, 2019', 'June 30 to July 6, 2019', {visibleDuration: {weeks: 1}}, {shiftKey: true});
});
it('should move the focused date to the start/end of the week with the home/end keys', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 8, 2019', 'Home', 1, 'Sunday, June 2, 2019', 'June 2 – 8, 2019', {visibleDuration: {weeks: 1}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 8, 2019', 'End', 1, 'Saturday, June 8, 2019', 'June 2 – 8, 2019', {visibleDuration: {weeks: 1}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 8, 2019', 'Home', 1, 'Sunday, June 2, 2019', 'June 2 to 8, 2019', {visibleDuration: {weeks: 1}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 8, 2019', 'End', 1, 'Saturday, June 8, 2019', 'June 2 to 8, 2019', {visibleDuration: {weeks: 1}});
});
});
describe('visibleDuration: 2 weeks', () => {
it('should move the focused date by one day with the left/right arrows', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 15, 2019', 'ArrowLeft', 1, 'Tuesday, June 4, 2019', 'June 2 – 15, 2019', {visibleDuration: {weeks: 2}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 15, 2019', 'ArrowLeft', 4, 'Saturday, June 1, 2019', 'May 19 – June 1, 2019', {visibleDuration: {weeks: 2}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 15, 2019', 'ArrowRight', 1, 'Thursday, June 6, 2019', 'June 2 – 15, 2019', {visibleDuration: {weeks: 2}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 15, 2019', 'ArrowRight', 4, 'Sunday, June 9, 2019', 'June 2 – 15, 2019', {visibleDuration: {weeks: 2}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 15, 2019', 'ArrowRight', 11, 'Sunday, June 16, 2019', 'June 16 – 29, 2019', {visibleDuration: {weeks: 2}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 15, 2019', 'ArrowLeft', 1, 'Tuesday, June 4, 2019', 'June 2 to 15, 2019', {visibleDuration: {weeks: 2}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 15, 2019', 'ArrowLeft', 4, 'Saturday, June 1, 2019', 'May 19 to June 1, 2019', {visibleDuration: {weeks: 2}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 15, 2019', 'ArrowRight', 1, 'Thursday, June 6, 2019', 'June 2 to 15, 2019', {visibleDuration: {weeks: 2}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 15, 2019', 'ArrowRight', 4, 'Sunday, June 9, 2019', 'June 2 to 15, 2019', {visibleDuration: {weeks: 2}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 15, 2019', 'ArrowRight', 11, 'Sunday, June 16, 2019', 'June 16 to 29, 2019', {visibleDuration: {weeks: 2}});
});
it('should move the focused date by one week with the up/down arrows', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 15, 2019', 'ArrowUp', 1, 'Wednesday, May 29, 2019', 'May 19 – June 1, 2019', {visibleDuration: {weeks: 2}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 15, 2019', 'ArrowDown', 1, 'Wednesday, June 12, 2019', 'June 2 – 15, 2019', {visibleDuration: {weeks: 2}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 15, 2019', 'ArrowDown', 2, 'Wednesday, June 19, 2019', 'June 16 – 29, 2019', {visibleDuration: {weeks: 2}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 15, 2019', 'ArrowUp', 1, 'Wednesday, May 29, 2019', 'May 19 to June 1, 2019', {visibleDuration: {weeks: 2}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 15, 2019', 'ArrowDown', 1, 'Wednesday, June 12, 2019', 'June 2 to 15, 2019', {visibleDuration: {weeks: 2}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 15, 2019', 'ArrowDown', 2, 'Wednesday, June 19, 2019', 'June 16 to 29, 2019', {visibleDuration: {weeks: 2}});
});
- it('should move the focused date by one page with the page up/page down arrows', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 15, 2019', 'PageUp', 1, 'Wednesday, May 22, 2019', 'May 19 – June 1, 2019', {visibleDuration: {weeks: 2}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 15, 2019', 'PageDown', 1, 'Wednesday, June 19, 2019', 'June 16 – 29, 2019', {visibleDuration: {weeks: 2}});
+ it('should move the focused date by one week with the page up/page down arrows', async () => {
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 15, 2019', 'PageUp', 1, 'Wednesday, May 29, 2019', 'May 19 to June 1, 2019', {visibleDuration: {weeks: 2}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 15, 2019', 'PageDown', 1, 'Wednesday, June 12, 2019', 'June 2 to 15, 2019', {visibleDuration: {weeks: 2}});
});
it('should move the focused date by one month with the shift + page up/page down arrows', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 15, 2019', 'PageUp', 1, 'Sunday, May 5, 2019', 'April 28 – May 11, 2019', {visibleDuration: {weeks: 2}}, {shiftKey: true});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 15, 2019', 'PageDown', 1, 'Friday, July 5, 2019', 'June 30 – July 13, 2019', {visibleDuration: {weeks: 2}}, {shiftKey: true});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 15, 2019', 'PageUp', 1, 'Sunday, May 5, 2019', 'April 28 to May 11, 2019', {visibleDuration: {weeks: 2}}, {shiftKey: true});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 15, 2019', 'PageDown', 1, 'Friday, July 5, 2019', 'June 30 to July 13, 2019', {visibleDuration: {weeks: 2}}, {shiftKey: true});
});
it('should move the focused date to the start/end of the visible range with the home/end keys', async () => {
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 15, 2019', 'Home', 1, 'Sunday, June 2, 2019', 'June 2 – 15, 2019', {visibleDuration: {weeks: 2}});
- await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 – 15, 2019', 'End', 1, 'Saturday, June 15, 2019', 'June 2 – 15, 2019', {visibleDuration: {weeks: 2}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 15, 2019', 'Home', 1, 'Sunday, June 2, 2019', 'June 2 to 15, 2019', {visibleDuration: {weeks: 2}});
+ await testKeyboard(new CalendarDate(2019, 6, 5), 'June 2 to 15, 2019', 'End', 1, 'Saturday, June 8, 2019', 'June 2 to 15, 2019', {visibleDuration: {weeks: 2}});
});
});
});
diff --git a/packages/@react-aria/checkbox/docs/buttongroup-example.png b/packages/@react-aria/checkbox/docs/buttongroup-example.png
new file mode 100644
index 00000000000..91b6e9a8df2
Binary files /dev/null and b/packages/@react-aria/checkbox/docs/buttongroup-example.png differ
diff --git a/packages/@react-aria/checkbox/docs/tailwind-example.png b/packages/@react-aria/checkbox/docs/tailwind-example.png
new file mode 100644
index 00000000000..533cebc207b
Binary files /dev/null and b/packages/@react-aria/checkbox/docs/tailwind-example.png differ
diff --git a/packages/@react-aria/checkbox/docs/useCheckbox.mdx b/packages/@react-aria/checkbox/docs/useCheckbox.mdx
index 1069ae2dd36..d3672d08e8d 100644
--- a/packages/@react-aria/checkbox/docs/useCheckbox.mdx
+++ b/packages/@react-aria/checkbox/docs/useCheckbox.mdx
@@ -18,6 +18,8 @@ import {HeaderInfo, FunctionAPI, TypeContext, InterfaceType, TypeLink, PageDescr
import {Keyboard} from '@react-spectrum/text';
import packageData from '@react-aria/checkbox/package.json';
import Anatomy from './checkbox-anatomy.svg';
+import {ExampleCard} from '@react-spectrum/docs/src/ExampleCard';
+import tailwindPreview from 'url:./tailwind-example.png';
---
category: Forms
@@ -32,7 +34,7 @@ keywords: [checkbox, input, aria]
packageData={packageData}
componentNames={['useCheckbox']}
sourceData={[
- {type: 'W3C', url: 'https://www.w3.org/TR/wai-aria-practices/#checkbox'}
+ {type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/'}
]} />
## API
@@ -82,7 +84,7 @@ import {useToggleState} from '@react-stately/toggle';
function Checkbox(props) {
let {children} = props;
let state = useToggleState(props);
- let ref = React.useRef();
+ let ref = React.useRef(null);
let {inputProps} = useCheckbox(props, state, ref);
return (
@@ -93,8 +95,7 @@ function Checkbox(props) {
);
}
-Test
-Test
+Unsubscribe
```
## Styling
@@ -111,18 +112,19 @@ hook from `@react-aria/focus`. When `isFocusVisible` is true, an extra SVG eleme
rendered to indicate focus. The focus ring is only visible when the user is interacting
with a keyboard, not with a mouse or touch.
-```tsx example
+```tsx example export=true
import {VisuallyHidden} from '@react-aria/visually-hidden';
import {useFocusRing} from '@react-aria/focus';
function Checkbox(props) {
let state = useToggleState(props);
- let ref = React.useRef();
+ let ref = React.useRef(null);
let {inputProps} = useCheckbox(props, state, ref);
let {isFocusVisible, focusProps} = useFocusRing();
+ let isSelected = state.isSelected && !props.isIndeterminate;
return (
-
+
@@ -132,20 +134,23 @@ function Checkbox(props) {
aria-hidden="true"
style={{marginRight: 4}}>
- {state.isSelected &&
+ x={isSelected ? 4 : 5}
+ y={isSelected ? 4 : 5}
+ width={isSelected ? 16 : 14}
+ height={isSelected ? 16 : 14}
+ fill={isSelected ? 'orange' : 'none'}
+ stroke={isSelected ? 'none' : 'gray'}
+ strokeWidth={2}/>
+ {isSelected &&
}
+ {props.isIndeterminate &&
+
+ }
{isFocusVisible &&
Foo
+Unsubscribe
+```
+
+## Styled examples
+
+
+
+## Usage
+
+The following examples show how to use the `Checkbox` component created in the above example.
+
+### Default value
+
+Checkboxes are not selected by default. The `defaultSelected` prop can be used to set the default state.
+
+```tsx example
+Subscribe
+```
+
+### Controlled value
+
+The `isSelected` prop can be used to make the selected state controlled. The `onChange` event is fired when the user presses the checkbox, and receives the new value.
+
+```tsx example
+function Example() {
+ let [selected, setSelection] = React.useState(false);
+
+ return (
+ <>
+
+ Subscribe
+
+ {`You are ${selected ? 'subscribed' : 'unsubscribed'}`}
+ >
+ );
+ }
+```
+
+### Indeterminate
+
+A Checkbox can be in an indeterminate state, controlled using the `isIndeterminate` prop.
+This overrides the appearance of the Checkbox, whether selection is controlled or uncontrolled.
+The Checkbox will visually remain indeterminate until the `isIndeterminate` prop is set to false, regardless of user interaction.
+
+```tsx example
+Subscribe
+```
+
+### Disabled
+
+Checkboxes can be disabled using the `isDisabled` prop.
+
+```tsx example
+Subscribe
+```
+
+### Read only
+
+The `isReadOnly` prop makes the selection immutable. Unlike `isDisabled`, the Checkbox remains focusable.
+See the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/readonly) for more information.
+
+```tsx example
+Agree
+```
+
+
+### HTML forms
+
+Checkbox supports the `name` and `value` props for integration with HTML forms.
+
+```tsx example
+Subscribe
```
## Internationalization
diff --git a/packages/@react-aria/checkbox/docs/useCheckboxGroup.mdx b/packages/@react-aria/checkbox/docs/useCheckboxGroup.mdx
index 792a6966cf1..966891dafae 100644
--- a/packages/@react-aria/checkbox/docs/useCheckboxGroup.mdx
+++ b/packages/@react-aria/checkbox/docs/useCheckboxGroup.mdx
@@ -18,6 +18,8 @@ import {HeaderInfo, FunctionAPI, TypeContext, InterfaceType, TypeLink, PageDescr
import {Keyboard} from '@react-spectrum/text';
import packageData from '@react-aria/checkbox/package.json';
import Anatomy from './checkboxgroup-anatomy.svg';
+import {ExampleCard} from '@react-spectrum/docs/src/ExampleCard';
+import buttongroupPreview from 'url:./buttongroup-example.png';
---
category: Forms
@@ -33,7 +35,7 @@ after_version: 3.1.0
packageData={packageData}
componentNames={['useCheckboxGroup', 'useCheckboxGroupItem']}
sourceData={[
- {type: 'W3C', url: 'https://www.w3.org/TR/wai-aria-practices/#checkbox'}
+ {type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/'}
]} />
## API
@@ -94,18 +96,18 @@ standalone checkbox, use the [useCheckbox](useCheckbox.html) hook instead.
This example uses native input elements for the checkboxes, and React context to share state from the group
to each checkbox. An HTML `` element wraps the native input and the text to provide an implicit label
-for the radio.
+for the checkbox.
-```tsx example
+```tsx example export=true
import {useCheckboxGroup, useCheckboxGroupItem} from '@react-aria/checkbox';
import {useCheckboxGroupState} from '@react-stately/checkbox';
let CheckboxGroupContext = React.createContext(null);
function CheckboxGroup(props) {
- let {children, label} = props;
+ let {children, label, description, errorMessage, validationState} = props;
let state = useCheckboxGroupState(props);
- let {groupProps, labelProps} = useCheckboxGroup(props, state);
+ let {groupProps, labelProps, descriptionProps, errorMessageProps} = useCheckboxGroup(props, state);
return (
@@ -113,6 +115,10 @@ function CheckboxGroup(props) {
{children}
+ {description &&
{description}
}
+ {errorMessage && validationState === 'invalid' &&
+
{errorMessage}
+ }
);
}
@@ -120,7 +126,7 @@ function CheckboxGroup(props) {
function Checkbox(props) {
let {children} = props;
let state = React.useContext(CheckboxGroupContext);
- let ref = React.useRef();
+ let ref = React.useRef(null);
let {inputProps} = useCheckboxGroupItem(props, state, ref);
let isDisabled = state.isDisabled || props.isDisabled;
@@ -148,3 +154,120 @@ function Checkbox(props) {
## Styling
See the [useCheckbox](useCheckbox.html#styling) docs for details on how to customize the styling of checkbox elements.
+
+## Styled examples
+
+
+
+## Usage
+
+The following examples show how to use the `CheckboxGroup` component created in the above example.
+
+### Default value
+
+An initial, uncontrolled value can be provided to the CheckboxGroup using the `defaultValue` prop, which accepts an array of selected items that map to the
+`value` prop on each Checkbox.
+
+```tsx example
+
+ Soccer
+ Baseball
+ Basketball
+
+```
+
+### Controlled value
+
+A controlled value can be provided using the `value` prop, which accepts an array of selected items, which map to the
+`value` prop on each Checkbox. The `onChange` event is fired when the user checks or unchecks an option. It receives a new array
+containing the updated selected values.
+
+```tsx example
+function Example() {
+ let [selected, setSelected] = React.useState(['soccer', 'baseball']);
+
+ return (
+
+ Soccer
+ Baseball
+ Basketball
+
+ );
+}
+```
+
+### Description
+
+The `description` prop can be used to associate additional help text with a checkbox group.
+
+```tsx example
+
+ Soccer
+ Baseball
+ Basketball
+
+```
+
+### Error message
+
+The `errorMessage` prop can be used to help the user fix a validation error. It should be combined with the `validationState` prop to
+semantically mark the checkbox group as invalid for assistive technologies.
+
+```tsx example
+
+ Soccer
+ Baseball
+ Basketball
+
+```
+
+### Disabled
+
+The entire CheckboxGroup can be disabled with the `isDisabled` prop.
+
+```tsx example
+
+ Soccer
+ Baseball
+ Basketball
+
+```
+
+To disable an individual checkbox, pass `isDisabled` to the `Checkbox` instead.
+
+```tsx example
+
+ Soccer
+ Baseball
+ Basketball
+
+```
+
+### Read only
+
+The `isReadOnly` prop makes the selection immutable. Unlike `isDisabled`, the CheckboxGroup remains focusable.
+See the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/readonly) for more information.
+
+```tsx example
+
+ Soccer
+ Baseball
+ Basketball
+
+```
+
+### HTML forms
+
+CheckboxGroup supports the `name` prop, paired with the Checkbox `value` prop, for integration with HTML forms.
+
+```tsx example
+
+ Soccer
+ Baseball
+ Basketball
+
+```
diff --git a/packages/@react-aria/checkbox/package.json b/packages/@react-aria/checkbox/package.json
index 9f0a5d335bf..20deea97c74 100644
--- a/packages/@react-aria/checkbox/package.json
+++ b/packages/@react-aria/checkbox/package.json
@@ -1,10 +1,15 @@
{
"name": "@react-aria/checkbox",
- "version": "3.3.3",
+ "version": "3.9.0",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
"module": "dist/module.js",
+ "exports": {
+ "types": "./dist/types.d.ts",
+ "import": "./dist/import.mjs",
+ "require": "./dist/main.js"
+ },
"types": "dist/types.d.ts",
"source": "src/index.ts",
"files": [
@@ -17,16 +22,17 @@
"url": "https://github.com/adobe/react-spectrum"
},
"dependencies": {
- "@babel/runtime": "^7.6.2",
- "@react-aria/label": "^3.2.4",
- "@react-aria/toggle": "^3.2.3",
- "@react-aria/utils": "^3.11.3",
- "@react-stately/checkbox": "^3.0.6",
- "@react-stately/toggle": "^3.2.6",
- "@react-types/checkbox": "^3.2.6"
+ "@react-aria/label": "^3.5.1",
+ "@react-aria/toggle": "^3.6.0",
+ "@react-aria/utils": "^3.16.0",
+ "@react-stately/checkbox": "^3.4.1",
+ "@react-stately/toggle": "^3.5.1",
+ "@react-types/checkbox": "^3.4.3",
+ "@react-types/shared": "^3.18.0",
+ "@swc/helpers": "^0.4.14"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0-rc.1"
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-aria/checkbox/src/index.ts b/packages/@react-aria/checkbox/src/index.ts
index 63e2a88c34d..536cafb0826 100644
--- a/packages/@react-aria/checkbox/src/index.ts
+++ b/packages/@react-aria/checkbox/src/index.ts
@@ -9,7 +9,9 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
-
-export * from './useCheckbox';
-export * from './useCheckboxGroup';
-export * from './useCheckboxGroupItem';
+export type {CheckboxAria} from './useCheckbox';
+export {useCheckbox} from './useCheckbox';
+export {useCheckboxGroup} from './useCheckboxGroup';
+export {useCheckboxGroupItem} from './useCheckboxGroupItem';
+export type {AriaCheckboxGroupItemProps, AriaCheckboxGroupProps, AriaCheckboxProps} from '@react-types/checkbox';
+export type {CheckboxGroupAria} from './useCheckboxGroup';
diff --git a/packages/@react-aria/checkbox/src/useCheckbox.ts b/packages/@react-aria/checkbox/src/useCheckbox.ts
index bf0babde6cf..1b258eb0f8b 100644
--- a/packages/@react-aria/checkbox/src/useCheckbox.ts
+++ b/packages/@react-aria/checkbox/src/useCheckbox.ts
@@ -17,7 +17,15 @@ import {useToggle} from '@react-aria/toggle';
export interface CheckboxAria {
/** Props for the input element. */
- inputProps: InputHTMLAttributes
+ inputProps: InputHTMLAttributes,
+ /** Whether the checkbox is selected. */
+ isSelected: boolean,
+ /** Whether the checkbox is in a pressed state. */
+ isPressed: boolean,
+ /** Whether the checkbox is disabled. */
+ isDisabled: boolean,
+ /** Whether the checkbox is read only. */
+ isReadOnly: boolean
}
/**
@@ -29,8 +37,7 @@ export interface CheckboxAria {
* @param inputRef - A ref for the HTML input element.
*/
export function useCheckbox(props: AriaCheckboxProps, state: ToggleState, inputRef: RefObject): CheckboxAria {
- let {inputProps} = useToggle(props, state, inputRef);
- let {isSelected} = state;
+ let {inputProps, isSelected, isPressed, isDisabled, isReadOnly} = useToggle(props, state, inputRef);
let {isIndeterminate} = props;
useEffect(() => {
@@ -44,8 +51,11 @@ export function useCheckbox(props: AriaCheckboxProps, state: ToggleState, inputR
return {
inputProps: {
...inputProps,
- checked: isSelected,
- 'aria-checked': isIndeterminate ? 'mixed' : isSelected
- }
+ checked: isSelected
+ },
+ isSelected,
+ isPressed,
+ isDisabled,
+ isReadOnly
};
}
diff --git a/packages/@react-aria/checkbox/src/useCheckboxGroup.ts b/packages/@react-aria/checkbox/src/useCheckboxGroup.ts
index ecf2e2f20aa..2d4cf119b2a 100644
--- a/packages/@react-aria/checkbox/src/useCheckboxGroup.ts
+++ b/packages/@react-aria/checkbox/src/useCheckboxGroup.ts
@@ -11,17 +11,21 @@
*/
import {AriaCheckboxGroupProps} from '@react-types/checkbox';
-import {checkboxGroupNames} from './utils';
+import {checkboxGroupDescriptionIds, checkboxGroupErrorMessageIds, checkboxGroupNames} from './utils';
import {CheckboxGroupState} from '@react-stately/checkbox';
+import {DOMAttributes} from '@react-types/shared';
import {filterDOMProps, mergeProps} from '@react-aria/utils';
-import {HTMLAttributes} from 'react';
-import {useLabel} from '@react-aria/label';
+import {useField} from '@react-aria/label';
-interface CheckboxGroupAria {
+export interface CheckboxGroupAria {
/** Props for the checkbox group wrapper element. */
- groupProps: HTMLAttributes,
+ groupProps: DOMAttributes,
/** Props for the checkbox group's visible label (if any). */
- labelProps: HTMLAttributes
+ labelProps: DOMAttributes,
+ /** Props for the checkbox group description element, if any. */
+ descriptionProps: DOMAttributes,
+ /** Props for the checkbox group error message element, if any. */
+ errorMessageProps: DOMAttributes
}
/**
@@ -33,12 +37,14 @@ interface CheckboxGroupAria {
export function useCheckboxGroup(props: AriaCheckboxGroupProps, state: CheckboxGroupState): CheckboxGroupAria {
let {isDisabled, name} = props;
- let {labelProps, fieldProps} = useLabel({
+ let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField({
...props,
// Checkbox group is not an HTML input element so it
// shouldn't be labeled by a element.
labelElementType: 'span'
});
+ checkboxGroupDescriptionIds.set(state, descriptionProps.id);
+ checkboxGroupErrorMessageIds.set(state, errorMessageProps.id);
let domProps = filterDOMProps(props, {labelable: true});
@@ -51,6 +57,8 @@ export function useCheckboxGroup(props: AriaCheckboxGroupProps, state: CheckboxG
'aria-disabled': isDisabled || undefined,
...fieldProps
}),
- labelProps
+ labelProps,
+ descriptionProps,
+ errorMessageProps
};
}
diff --git a/packages/@react-aria/checkbox/src/useCheckboxGroupItem.ts b/packages/@react-aria/checkbox/src/useCheckboxGroupItem.ts
index de7c42f9082..2d547d92692 100644
--- a/packages/@react-aria/checkbox/src/useCheckboxGroupItem.ts
+++ b/packages/@react-aria/checkbox/src/useCheckboxGroupItem.ts
@@ -12,7 +12,7 @@
import {AriaCheckboxGroupItemProps} from '@react-types/checkbox';
import {CheckboxAria, useCheckbox} from './useCheckbox';
-import {checkboxGroupNames} from './utils';
+import {checkboxGroupDescriptionIds, checkboxGroupErrorMessageIds, checkboxGroupNames} from './utils';
import {CheckboxGroupState} from '@react-stately/checkbox';
import {RefObject} from 'react';
import {useToggleState} from '@react-stately/toggle';
@@ -41,12 +41,21 @@ export function useCheckboxGroupItem(props: AriaCheckboxGroupItemProps, state: C
}
});
- let {inputProps} = useCheckbox({
+ let res = useCheckbox({
...props,
isReadOnly: props.isReadOnly || state.isReadOnly,
isDisabled: props.isDisabled || state.isDisabled,
name: props.name || checkboxGroupNames.get(state)
}, toggleState, inputRef);
- return {inputProps};
+ return {
+ ...res,
+ inputProps: {
+ ...res.inputProps,
+ 'aria-describedby': [
+ state.validationState === 'invalid' ? checkboxGroupErrorMessageIds.get(state) : null,
+ checkboxGroupDescriptionIds.get(state)
+ ].filter(Boolean).join(' ') || undefined
+ }
+ };
}
diff --git a/packages/@react-aria/checkbox/src/utils.ts b/packages/@react-aria/checkbox/src/utils.ts
index 3161b05cd4c..159411bfdc6 100644
--- a/packages/@react-aria/checkbox/src/utils.ts
+++ b/packages/@react-aria/checkbox/src/utils.ts
@@ -13,3 +13,5 @@
import {CheckboxGroupState} from '@react-stately/checkbox';
export const checkboxGroupNames = new WeakMap();
+export const checkboxGroupDescriptionIds = new WeakMap();
+export const checkboxGroupErrorMessageIds = new WeakMap();
diff --git a/packages/@react-aria/checkbox/test/useCheckboxGroup.test.tsx b/packages/@react-aria/checkbox/test/useCheckboxGroup.test.tsx
index d0a4df7bae3..2572fb37653 100644
--- a/packages/@react-aria/checkbox/test/useCheckboxGroup.test.tsx
+++ b/packages/@react-aria/checkbox/test/useCheckboxGroup.test.tsx
@@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/
-import {act, render} from '@testing-library/react';
+import {act, render} from '@react-spectrum/test-utils';
import {AriaCheckboxGroupItemProps, AriaCheckboxGroupProps} from '@react-types/checkbox';
import {CheckboxGroupState, useCheckboxGroupState} from '@react-stately/checkbox';
import React, {useRef} from 'react';
diff --git a/packages/@react-aria/color/docs/ColorAreaAnatomy.svg b/packages/@react-aria/color/docs/ColorAreaAnatomy.svg
new file mode 100644
index 00000000000..bc9ddd64e6a
--- /dev/null
+++ b/packages/@react-aria/color/docs/ColorAreaAnatomy.svg
@@ -0,0 +1,32 @@
+
+ Color area anatomy diagram
+ Shows a color area component with labels pointing to its parts, including the area, and thumb elements.
+
+
+
+
+
+
+
+
+
+
+ Area
+
+
+
+
+
+
+
+ Thumb
+
+
+
+
+
+
diff --git a/packages/@react-aria/color/docs/useColorArea.mdx b/packages/@react-aria/color/docs/useColorArea.mdx
new file mode 100644
index 00000000000..cc50f8400ee
--- /dev/null
+++ b/packages/@react-aria/color/docs/useColorArea.mdx
@@ -0,0 +1,745 @@
+{/* Copyright 2022 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 {Layout} from '@react-spectrum/docs';
+export default Layout;
+
+import ChevronRight from '@spectrum-icons/workflow/ChevronRight';
+import docs from 'docs:@react-aria/color';
+import statelyDocs from 'docs:@react-stately/color';
+import {HeaderInfo, FunctionAPI, TypeContext, InterfaceType, TypeLink, PageDescription} from '@react-spectrum/docs';
+import packageData from '@react-aria/color/package.json';
+import Anatomy from './ColorAreaAnatomy.svg';
+
+---
+category: Color
+keywords: [color area, color picker, aria]
+---
+
+# useColorArea
+
+{docs.exports.useColorArea.description}
+
+
+
+## API
+
+
+
+## Features
+
+The [<input type="color">](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/color) HTML element
+can be used to build a color picker, however it is very inconsistent across browsers and operating systems and consists
+of a complete color picker rather than a color area. `useColorArea` helps achieve accessible and
+touch-friendly color areas that can be styled as needed.
+
+* Support for adjusting two-channel values of an HSL, HSB or RGB color value
+* Support for mouse, touch, and keyboard via the [useMove](useMove.html) hook
+* Multi-touch support
+* Pressing on the color area background moves the thumb to that position
+* Supports using the arrow keys, for changing value by step, as well as shift + arrow key, page up/down, home, and end keys, for changing the value by page step.
+* Support for disabling the color area
+* Prevents text selection while dragging
+* Exposed to assistive technology as a `2D slider` element via ARIA
+* Uses two hidden native input elements within a group to support touch screen readers
+* Automatic ARIA labeling using the localized channel names by default
+* Support for mirroring in RTL locales
+
+## Anatomy
+
+
+
+A color area consists of a rectangular background area that provides, using a two-dimensional gradient,
+a visual representation of the range of color values from which a user can select, and a thumb that the user can drag
+to change the selected color value. Two visually hidden ` ` elements are used to represent the color channel
+values to assistive technologies.
+
+`useColorArea` returns five sets of props that you should spread onto the appropriate elements:
+
+
+
+
+
+State is managed by the
+hook from `@react-stately/color`. The state object should be passed as an option to `useColorArea`.
+
+By default, `useColorArea` provides an `aria-label` for the localized string "Color Picker",
+which labels the visually hidden ` ` elements for the two color channels, or on mobile devices,
+the group containing them. If you wish to override this with a more specific label, an `aria-label` or
+`aria-labelledby` prop may be passed to further to identify the element to assistive technologies.
+
+The `aria-valuetext` for each ` ` will include the localized color channel name and current value for each
+channel.
+
+## Example
+
+This example shows how to build a color area with a draggable thumb to adjust two color channel values of a color. Styling for
+the background gradient and positioning of the thumb are provided by `useColorArea` in the returned props for each element.
+The two ` ` elements inside the thumb represent the color channel values to assistive technologies, and are hidden
+from view. The thumb also uses the
+[useFocusRing](useFocusRing.html) hook to grow in size when it is keyboard focused (try tabbing to it).
+
+```tsx example export=true
+import {useColorArea} from '@react-aria/color';
+import {useColorAreaState} from '@react-stately/color';
+import {useFocusRing} from '@react-aria/focus';
+
+const SIZE = 192;
+const FOCUSED_THUMB_SIZE = 28;
+const THUMB_SIZE = 20;
+const BORDER_RADIUS = 4;
+
+function ColorArea(props) {
+ let inputXRef = React.useRef(null);
+ let inputYRef = React.useRef(null);
+ let containerRef = React.useRef(null);
+
+ let state = useColorAreaState(props);
+
+ let {isDisabled} = props;
+
+ let {
+ colorAreaProps,
+ gradientProps,
+ xInputProps,
+ yInputProps,
+ thumbProps
+ } = useColorArea({...props, inputXRef, inputYRef, containerRef}, state);
+
+ let {focusProps, isFocusVisible} = useFocusRing();
+
+ return (
+
+ );
+};
+
+
+```
+
+## Usage
+
+The following examples show how to use the `ColorArea` component created in the above example.
+
+### Uncontrolled
+
+By default, color area is uncontrolled, with a default value of white using the RGB color space (`rgb(255, 255, 255)`).
+You can change the default value using the `defaultValue` prop, and the color area will use the color space of the provided value.
+If no `xChannel` or `yChannel` is provided, for the RGB color space, the `red` color channel maps to the horizontal axis or `xChannel`,
+and the `green` color channel maps to the vertical axis or `yChannel`. Similarly, for the HSL and HSB color spaces, the `hue` color
+channel maps to the horizontal axis or `xChannel`, and the `saturation` color channel maps to the vertical axis or `yChannel`.
+
+```tsx example
+x: Hue, y: Saturation
+
+```
+
+### Controlled
+
+A color area can be made controlled using the `value` prop.
+The
+function is used to parse the initial color from an RGB, HSL or HSB string, stored in state.
+
+The `onChange` prop is used to update the value in the state when the user drags the thumb.
+This is the more common usage because it allows to adjust the third color channel using a separate control,
+like a color slider using the [useColorSlider](useColorSlider.html) hook or a color wheel using the
+[useColorWheel](useColorWheel.html) hook, or to display the color value stored in a state using a preview swatch.
+
+The `onChangeEnd` prop can be used to handle when a user stops dragging the color area.
+
+```tsx example
+import {parseColor} from '@react-stately/color';
+
+function Example() {
+ let [
+ color,
+ setColor
+ ] = React.useState(parseColor('hsba(219, 58%, 93%, 0.75)'));
+ let [
+ endColor,
+ setEndColor
+ ] = React.useState(color);
+ let [
+ xChannel,
+ yChannel,
+ zChannel
+ ] = color.getColorChannels();
+ return (
+ <>
+ x: {color.getChannelName(xChannel, 'en-US')}, y: {color.getChannelName(yChannel, 'en-US')}
+
+ Current color value: {color.toString('hsba')}
+ End color value: {endColor.toString('hsba')}
+ >
+ );
+}
+```
+
+### ColorSlider
+
+The `ColorSlider` component used in the example above controls the channel value not controlled by the `ColorArea`, in this case, the `brightness` channel, or the `alpha` channel.
+It is built using the [useColorSlider](useColorSlider.html) hook, and can be shared with other color components.
+
+
+ Show code
+
+```tsx example export=true render=false
+ import {useColorSlider} from '@react-aria/color';
+ import {useColorSliderState} from '@react-stately/color';
+ import {VisuallyHidden} from '@react-aria/visually-hidden';
+ import {useLocale} from '@react-aria/i18n';
+ import {useFocusRing} from '@react-aria/focus';
+
+ function ColorSlider(props) {
+ let {locale} = useLocale();
+ let state = useColorSliderState({...props, locale});
+ let trackRef = React.useRef(null);
+ let inputRef = React.useRef(null);
+
+ // Default label to the channel name in the current locale
+ let label = props.label || state.value.getChannelName(props.channel, locale);
+
+ let {trackProps, thumbProps, inputProps, labelProps, outputProps} = useColorSlider({
+ ...props,
+ label,
+ trackRef,
+ inputRef
+ }, state);
+
+ let {focusProps, isFocusVisible} = useFocusRing();
+
+ return (
+
+ {/* Create a flex container for the label and output element. */}
+
+ {label}
+
+ {state.value.formatChannelValue(props.channel, locale)}
+
+
+ {/* The track element holds the visible track line and the thumb. */}
+
+
+ );
+ }
+```
+
+ Show CSS
+
+```css
+ .color-slider-track,
+ .color-slider-track-background,
+ .color-slider-track-color {
+ width: 100%;
+ border-radius: 4px;
+ forced-color-adjust: none;
+ position: relative;
+ }
+
+ .color-slider-track-background,
+ .color-slider-track-color {
+ position: absolute;
+ height: 100%;
+ }
+
+ .color-slider-thumb {
+ position: absolute;
+ top: 14px;
+ border: 2px solid white;
+ box-shadow:
+ 0 0 0 1px black,
+ inset 0 0 0 1px black;
+ border-radius: 50%;
+ box-sizing: border-box;
+ width: 20px;
+ height: 20px;
+ }
+
+ .color-slider-thumb.is-focused {
+ width: 32px;
+ height: 32px;
+ }
+
+ .color-slider-track-background,
+ .color-slider-thumb-background {
+ background-size: 16px 16px;
+ background-position:
+ -2px -2px,
+ -2px 6px,
+ 6px -10px,
+ -10px -2px;
+ background-color: white;
+ background-image:
+ linear-gradient(-45deg, transparent 75.5%, rgb(188, 188, 188) 75.5%),
+ linear-gradient(45deg, transparent 75.5%, rgb(188, 188, 188) 75.5%),
+ linear-gradient(-45deg, rgb(188, 188, 188) 25.5%, transparent 25.5%),
+ linear-gradient(45deg, rgb(188, 188, 188) 25.5%, transparent 25.5%);
+ }
+
+ .color-slider-thumb-background,
+ .color-slider-thumb-color {
+ position: absolute;
+ border-radius: 50%;
+ width: 100%;
+ height: 100%;
+ }
+```
+
+
+
+### ColorSwatch
+
+The `ColorSwatch` component used in the example above implements an image preview of the color.
+
+
+ Show code
+
+```tsx example export=true render=false
+ function ColorSwatch (props) {
+ let {
+ value,
+ ...otherProps
+ } = props;
+ return (
+
+ );
+ }
+```
+
+ Show CSS
+
+```css
+ .color-preview-swatch {
+ display: inline-block;
+ border-radius: 4px;
+ forced-color-adjust: none;
+ position: relative;
+ width: 40px;
+ height: 40px;
+ overflow: hidden;
+ }
+
+ .color-preview-swatch-background {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-size: 16px 16px;
+ background-position:
+ -2px -2px,
+ -2px 6px,
+ 6px -10px,
+ -10px -2px;
+ background-color: white;
+ background-image:
+ linear-gradient(-45deg, transparent 75.5%, rgb(188, 188, 188) 75.5%),
+ linear-gradient(45deg, transparent 75.5%, rgb(188, 188, 188) 75.5%),
+ linear-gradient(-45deg, rgb(188, 188, 188) 25.5%, transparent 25.5%),
+ linear-gradient(45deg, rgb(188, 188, 188) 25.5%, transparent 25.5%);
+ }
+
+ .color-preview-swatch-color {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ }
+```
+
+
+
+### xChannel and yChannel
+
+The color channel for each axis of a color area can be specified using the `xChannel` and `yChannel` props.
+An array of channel names for a color can be returned using the `color.getColorChannels` method.
+To get a localized channel name to use as a label, you can use the `color.getChannelName` method.
+
+#### RGB
+```tsx example
+import {parseColor} from '@react-stately/color';
+
+function Example() {
+ let [
+ color,
+ setColor
+ ] = React.useState(parseColor('rgb(100, 149, 237)'));
+ let [
+ rChannel,
+ gChannel,
+ bChannel
+ ] = color.getColorChannels();
+ return (
+ <>
+
+
+ x: {color.getChannelName(gChannel, 'en-US')}, y: {color.getChannelName(bChannel, 'en-US')}
+
+
+
+
+ x: {color.getChannelName(bChannel, 'en-US')}, y: {color.getChannelName(rChannel, 'en-US')}
+
+
+
+
+ x: {color.getChannelName(rChannel, 'en-US')}, y: {color.getChannelName(gChannel, 'en-US')}
+
+
+
+
+ Current RGB color value: {color.toString('rgb')}
+ >
+ );
+}
+```
+#### HSL
+```tsx example
+import {parseColor} from '@react-stately/color';
+
+function Example() {
+ let [
+ color,
+ setColor
+ ] = React.useState(parseColor('hsl(219, 79%, 66%)'));
+ let [
+ hChannel,
+ sChannel,
+ lChannel
+ ] = color.getColorChannels();
+ return (
+ <>
+
+
+ x: {color.getChannelName(sChannel, 'en-US')}, y: {color.getChannelName(lChannel, 'en-US')}
+
+
+
+
+ x: {color.getChannelName(hChannel, 'en-US')}, y: {color.getChannelName(lChannel, 'en-US')}
+
+
+
+
+ x: {color.getChannelName(hChannel, 'en-US')}, y: {color.getChannelName(sChannel, 'en-US')}
+
+
+
+
+ Current HSL color value: {color.toString('hsl')}
+ >
+ );
+}
+```
+#### HSB
+```tsx example
+import {parseColor} from '@react-stately/color';
+
+function Example() {
+ let [
+ color,
+ setColor
+ ] = React.useState(parseColor('hsb(219, 58%, 93%)'));
+ let [
+ hChannel,
+ sChannel,
+ bChannel
+ ] = color.getColorChannels();
+ return (
+ <>
+
+
+ x: {color.getChannelName(sChannel, 'en-US')}, y: {color.getChannelName(bChannel, 'en-US')}
+
+
+
+
+ x: {color.getChannelName(hChannel, 'en-US')}, y: {color.getChannelName(bChannel, 'en-US')}
+
+
+
+
+ x: {color.getChannelName(hChannel, 'en-US')}, y: {color.getChannelName(sChannel, 'en-US')}
+
+
+
+
+ Current HSB color value: {color.toString('hsb')}
+ >
+ );
+}
+```
+
+### Disabled
+
+A color area can be disabled using the `isDisabled` prop. This prevents the thumb from being focused or dragged.
+It's up to you to style your color area to appear disabled accordingly.
+
+```tsx example
+
+```
+
+## Internationalization
+
+### Labeling
+
+By default, `useColorArea` provides an `aria-label` for the localized string "Color Picker",
+which labels the visually hidden ` ` elements for the two color channels, or on mobile devices,
+the group containing them. If you wish to override this with a more specific label, an `aria-label` or
+`aria-labelledby` prop may be passed to further to identify the element to assistive technologies.
+For example, for a color area that adjusts a text color you might pass the `aria-label` prop,
+"Text color", which `useColorArea` will return as the `aria-label` prop "Text color, Color picker". When a custom `aria-label`
+is provided, it should be localized accordingly.
+
+### Role description
+
+In order to communicate to a screen reader user that the color area adjusts in two dimensions,
+`useColorArea` provides an `aria-roledescription`, using the localized string "2D Slider", on each of the
+visually hidden ` ` elements.
+
+### Value formatting
+
+The `aria-valuetext` of each ` ` element is formatted according to the user's locale automatically.
+It includes the localized color channel name and current value for each channel, with the channel name
+and value that the ` ` element controls coming before the channel name and value for the adjacent
+` ` element. For example, for an RGB color area where the `xChannel` is "blue", the `yChannel`
+is "green", when the current selected color is yellow (`rgb(255, 255, 0)`), the ` ` representing the
+blue channel will have `aria-valuetext` to announce as "Blue: 0, Green: 255", and the ` `
+representing the green channel will have `aria-valuetext` to announce as "Green: 255, Blue: 0".
+
+### RTL
+
+In right-to-left languages, color areas should be mirrored.
+Orientation of the gradient background, positioning of the thumb,
+and dragging behavior is automatically mirrored by `useColorArea`.
diff --git a/packages/@react-aria/color/docs/useColorField.mdx b/packages/@react-aria/color/docs/useColorField.mdx
index b292eb02445..af46caf1cb6 100644
--- a/packages/@react-aria/color/docs/useColorField.mdx
+++ b/packages/@react-aria/color/docs/useColorField.mdx
@@ -29,7 +29,7 @@ keywords: [color, input, color picker, aria]
packageData={packageData}
componentNames={['useColorField']}
sourceData={[
- {type: 'W3C', url: 'https://www.w3.org/TR/wai-aria-practices-1.2/#spinbutton'}
+ {type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/'}
]} />
## API
@@ -48,13 +48,15 @@ color fields that can be styled as needed.
* Supports using the arrow keys to increment and decrement the value
* Exposed to assistive technology as a `textbox` via ARIA
* Visual and ARIA labeling support
-* Follows the [spinbutton](https://www.w3.org/TR/wai-aria-practices-1.2/#spinbutton) ARIA pattern
+* Follows the [spinbutton](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/) ARIA pattern
* Works around bugs in VoiceOver with the spinbutton role
* Uses an ARIA live region to ensure that value changes are announced
## Anatomy
-
+
A color field consists of an input element and a label. `useColorField` automatically manages
the relationship between the two elements using the `for` attribute on the `` element
@@ -80,7 +82,7 @@ import {useColorFieldState} from '@react-stately/color';
function ColorField(props) {
let state = useColorFieldState(props);
- let inputRef = React.useRef();
+ let inputRef = React.useRef(null);
let {
labelProps,
inputProps
diff --git a/packages/@react-aria/color/docs/useColorSlider.mdx b/packages/@react-aria/color/docs/useColorSlider.mdx
index 6ba3f315e39..bb6f8f60b6e 100644
--- a/packages/@react-aria/color/docs/useColorSlider.mdx
+++ b/packages/@react-aria/color/docs/useColorSlider.mdx
@@ -30,7 +30,7 @@ keywords: [color slider, color picker, aria]
packageData={packageData}
componentNames={['useColorSlider']}
sourceData={[
- {type: 'W3C', url: 'https://www.w3.org/TR/wai-aria-practices-1.2/#slider'}
+ {type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/slider/'}
]} />
## API
@@ -62,7 +62,9 @@ touch-friendly color sliders that can be styled as needed.
## Anatomy
-
+
A color slider consists of a track element and a thumb that the user can drag to change a single channel of a color value.
It may also include optional label and `` elements to display the color channel name and current numeric value, respectively.
@@ -107,8 +109,8 @@ const THUMB_SIZE = 20;
function ColorSlider(props) {
let {locale} = useLocale();
let state = useColorSliderState({...props, locale});
- let trackRef = React.useRef();
- let inputRef = React.useRef();
+ let trackRef = React.useRef(null);
+ let inputRef = React.useRef(null);
// Default label to the channel name in the current locale
let label = props.label || state.value.getChannelName(props.channel, locale);
@@ -184,9 +186,9 @@ by passing an `aria-label` prop to `useColorSlider`.
function ColorSlider(props) {
let {locale} = useLocale();
let state = useColorSliderState({...props, locale});
- let trackRef = React.useRef();
- let inputRef = React.useRef();
- let {groupProps, trackProps, thumbProps, inputProps} = useColorSlider({
+ let trackRef = React.useRef(null);
+ let inputRef = React.useRef(null);
+ let {trackProps, thumbProps, inputProps} = useColorSlider({
...props,
orientation: 'vertical',
trackRef,
@@ -197,7 +199,6 @@ function ColorSlider(props) {
return (
diff --git a/packages/@react-aria/color/docs/useColorWheel.mdx b/packages/@react-aria/color/docs/useColorWheel.mdx
index c133c269002..5b2e54e6f54 100644
--- a/packages/@react-aria/color/docs/useColorWheel.mdx
+++ b/packages/@react-aria/color/docs/useColorWheel.mdx
@@ -29,7 +29,7 @@ keywords: [color wheel, color picker, aria]
packageData={packageData}
componentNames={['useColorWheel']}
sourceData={[
- {type: 'W3C', url: 'https://www.w3.org/TR/wai-aria-practices-1.2/#slider'}
+ {type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/slider/'}
]} />
## API
@@ -56,7 +56,9 @@ touch-friendly color wheels that can be styled as needed.
## Anatomy
-
+
A color wheel consists of a circular track and a thumb that the user can drag to change the color hue.
A visually hidden `
` element is used to represent the value to assistive technologies.
diff --git a/packages/@react-aria/color/intl/ar-AE.json b/packages/@react-aria/color/intl/ar-AE.json
index e3adbfc6707..27699d20277 100644
--- a/packages/@react-aria/color/intl/ar-AE.json
+++ b/packages/@react-aria/color/intl/ar-AE.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "شريط تمرير ثنائي الأبعاد",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "أداة انتقاء اللون",
+ "twoDimensionalSlider": "مُنزلق 2D"
}
diff --git a/packages/@react-aria/color/intl/bg-BG.json b/packages/@react-aria/color/intl/bg-BG.json
index 8299f4af79a..09a7ae7221b 100644
--- a/packages/@react-aria/color/intl/bg-BG.json
+++ b/packages/@react-aria/color/intl/bg-BG.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D плъзгач",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Средство за избиране на цвят",
+ "twoDimensionalSlider": "2D плъзгач"
}
diff --git a/packages/@react-aria/color/intl/cs-CZ.json b/packages/@react-aria/color/intl/cs-CZ.json
index d676773682e..34e3c070777 100644
--- a/packages/@react-aria/color/intl/cs-CZ.json
+++ b/packages/@react-aria/color/intl/cs-CZ.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D posuvník",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Výběr barvy",
+ "twoDimensionalSlider": "2D posuvník"
}
diff --git a/packages/@react-aria/color/intl/da-DK.json b/packages/@react-aria/color/intl/da-DK.json
index 02bf3d40a29..e961edaf72a 100644
--- a/packages/@react-aria/color/intl/da-DK.json
+++ b/packages/@react-aria/color/intl/da-DK.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D-skyder",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Farvevælger",
+ "twoDimensionalSlider": "2D-skyder"
}
diff --git a/packages/@react-aria/color/intl/de-DE.json b/packages/@react-aria/color/intl/de-DE.json
index 0face0024ec..d3e2a83190d 100644
--- a/packages/@react-aria/color/intl/de-DE.json
+++ b/packages/@react-aria/color/intl/de-DE.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D-Schieberegler",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Farbwähler",
+ "twoDimensionalSlider": "2D-Schieberegler"
}
diff --git a/packages/@react-aria/color/intl/el-GR.json b/packages/@react-aria/color/intl/el-GR.json
index 754e53316a3..1b38659d3a1 100644
--- a/packages/@react-aria/color/intl/el-GR.json
+++ b/packages/@react-aria/color/intl/el-GR.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "Ρυθμιστικό 2D",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Επιλογέας χρωμάτων",
+ "twoDimensionalSlider": "Ρυθμιστικό 2D"
}
diff --git a/packages/@react-aria/color/intl/en-US.json b/packages/@react-aria/color/intl/en-US.json
index c3d72686b82..11f22be29b5 100644
--- a/packages/@react-aria/color/intl/en-US.json
+++ b/packages/@react-aria/color/intl/en-US.json
@@ -1,5 +1,6 @@
{
+ "colorPicker": "Color picker",
"twoDimensionalSlider": "2D slider",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorInputLabel": "{label}, {channelLabel}"
}
diff --git a/packages/@react-aria/color/intl/es-ES.json b/packages/@react-aria/color/intl/es-ES.json
index ee0bd0f7a58..541978f189c 100644
--- a/packages/@react-aria/color/intl/es-ES.json
+++ b/packages/@react-aria/color/intl/es-ES.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "Control deslizante en 2D",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Selector de color",
+ "twoDimensionalSlider": "Regulador 2D"
}
diff --git a/packages/@react-aria/color/intl/et-EE.json b/packages/@react-aria/color/intl/et-EE.json
index 2859d654c80..e0a1819beed 100644
--- a/packages/@react-aria/color/intl/et-EE.json
+++ b/packages/@react-aria/color/intl/et-EE.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D-liugur",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Värvivalija",
+ "twoDimensionalSlider": "2D-liugur"
}
diff --git a/packages/@react-aria/color/intl/fi-FI.json b/packages/@react-aria/color/intl/fi-FI.json
index ca3b11689ce..7c87655a518 100644
--- a/packages/@react-aria/color/intl/fi-FI.json
+++ b/packages/@react-aria/color/intl/fi-FI.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D-liukusäädin",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Värimuokkain",
+ "twoDimensionalSlider": "2D-liukusäädin"
}
diff --git a/packages/@react-aria/color/intl/fr-FR.json b/packages/@react-aria/color/intl/fr-FR.json
index 29fc2c866e1..85b88f186ba 100644
--- a/packages/@react-aria/color/intl/fr-FR.json
+++ b/packages/@react-aria/color/intl/fr-FR.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "Curseur 2D",
- "colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorInputLabel": "{label}, {channelLabel}",
+ "colorNameAndValue": "{name} : {value}",
+ "colorPicker": "Sélecteur de couleurs",
+ "twoDimensionalSlider": "Curseur 2D"
}
diff --git a/packages/@react-aria/color/intl/he-IL.json b/packages/@react-aria/color/intl/he-IL.json
index d3affd743a4..c50a1d7de84 100644
--- a/packages/@react-aria/color/intl/he-IL.json
+++ b/packages/@react-aria/color/intl/he-IL.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "מחוון דו-ממדי",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "בוחר הצבעים",
+ "twoDimensionalSlider": "מחוון דו מימדי"
}
diff --git a/packages/@react-aria/color/intl/hr-HR.json b/packages/@react-aria/color/intl/hr-HR.json
index 1eed1913084..b2fbd6936cc 100644
--- a/packages/@react-aria/color/intl/hr-HR.json
+++ b/packages/@react-aria/color/intl/hr-HR.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D kliznik",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Odabir boje",
+ "twoDimensionalSlider": "2D klizač"
}
diff --git a/packages/@react-aria/color/intl/hu-HU.json b/packages/@react-aria/color/intl/hu-HU.json
index efed81ba033..9187515d216 100644
--- a/packages/@react-aria/color/intl/hu-HU.json
+++ b/packages/@react-aria/color/intl/hu-HU.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D csúszka",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Színválasztó",
+ "twoDimensionalSlider": "2D-csúszka"
}
diff --git a/packages/@react-aria/color/intl/it-IT.json b/packages/@react-aria/color/intl/it-IT.json
index 561daf2fb94..bb2ea21aedb 100644
--- a/packages/@react-aria/color/intl/it-IT.json
+++ b/packages/@react-aria/color/intl/it-IT.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "Dispositivo di scorrimento 2D",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Selettore colore",
+ "twoDimensionalSlider": "Cursore 2D"
}
diff --git a/packages/@react-aria/color/intl/ja-JP.json b/packages/@react-aria/color/intl/ja-JP.json
index 76f16b8bccb..1ec646d234e 100644
--- a/packages/@react-aria/color/intl/ja-JP.json
+++ b/packages/@react-aria/color/intl/ja-JP.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D スライダー",
- "colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorInputLabel": "{label}、{channelLabel}",
+ "colorNameAndValue": "{name} : {value}",
+ "colorPicker": "カラーピッカー",
+ "twoDimensionalSlider": "2D スライダー"
}
diff --git a/packages/@react-aria/color/intl/ko-KR.json b/packages/@react-aria/color/intl/ko-KR.json
index 58e94a866c3..731eeda32a3 100644
--- a/packages/@react-aria/color/intl/ko-KR.json
+++ b/packages/@react-aria/color/intl/ko-KR.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D 슬라이더",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "색상 피커",
+ "twoDimensionalSlider": "2D 슬라이더"
}
diff --git a/packages/@react-aria/color/intl/lt-LT.json b/packages/@react-aria/color/intl/lt-LT.json
index d13d94a0663..27db8176a56 100644
--- a/packages/@react-aria/color/intl/lt-LT.json
+++ b/packages/@react-aria/color/intl/lt-LT.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D slankiklis",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Spalvų parinkiklis",
+ "twoDimensionalSlider": "2D slankiklis"
}
diff --git a/packages/@react-aria/color/intl/lv-LV.json b/packages/@react-aria/color/intl/lv-LV.json
index 402af4930f8..7862b95533e 100644
--- a/packages/@react-aria/color/intl/lv-LV.json
+++ b/packages/@react-aria/color/intl/lv-LV.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "Plaknes slīdnis",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Krāsu atlasītājs",
+ "twoDimensionalSlider": "2D slīdnis"
}
diff --git a/packages/@react-aria/color/intl/nb-NO.json b/packages/@react-aria/color/intl/nb-NO.json
index b3a77570622..f67d14442cd 100644
--- a/packages/@react-aria/color/intl/nb-NO.json
+++ b/packages/@react-aria/color/intl/nb-NO.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D-glidebryter",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Fargevelger",
+ "twoDimensionalSlider": "2D-glidebryter"
}
diff --git a/packages/@react-aria/color/intl/nl-NL.json b/packages/@react-aria/color/intl/nl-NL.json
index e2baafafd32..24ca3ff6462 100644
--- a/packages/@react-aria/color/intl/nl-NL.json
+++ b/packages/@react-aria/color/intl/nl-NL.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D-schuifregelaar",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Kleurkiezer",
+ "twoDimensionalSlider": "2D-schuifregelaar"
}
diff --git a/packages/@react-aria/color/intl/pl-PL.json b/packages/@react-aria/color/intl/pl-PL.json
index 4053c546cf3..a7cdc2e6a4a 100644
--- a/packages/@react-aria/color/intl/pl-PL.json
+++ b/packages/@react-aria/color/intl/pl-PL.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "Suwak 2D",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Próbnik kolorów",
+ "twoDimensionalSlider": "Suwak 2D"
}
diff --git a/packages/@react-aria/color/intl/pt-BR.json b/packages/@react-aria/color/intl/pt-BR.json
index 550bbb505bc..198e2fd3979 100644
--- a/packages/@react-aria/color/intl/pt-BR.json
+++ b/packages/@react-aria/color/intl/pt-BR.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "Controle deslizante 2D",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Seletor de cores",
+ "twoDimensionalSlider": "Controle deslizante 2D"
}
diff --git a/packages/@react-aria/color/intl/pt-PT.json b/packages/@react-aria/color/intl/pt-PT.json
index 33d9da707d2..198e2fd3979 100644
--- a/packages/@react-aria/color/intl/pt-PT.json
+++ b/packages/@react-aria/color/intl/pt-PT.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "Controlo de deslize 2D",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Seletor de cores",
+ "twoDimensionalSlider": "Controle deslizante 2D"
}
diff --git a/packages/@react-aria/color/intl/ro-RO.json b/packages/@react-aria/color/intl/ro-RO.json
index 90eba883c3c..b8bbc64eb95 100644
--- a/packages/@react-aria/color/intl/ro-RO.json
+++ b/packages/@react-aria/color/intl/ro-RO.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "Cursor 2D",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Selector de culori",
+ "twoDimensionalSlider": "Glisor 2D"
}
diff --git a/packages/@react-aria/color/intl/ru-RU.json b/packages/@react-aria/color/intl/ru-RU.json
index 52bd24d769e..32bde268462 100644
--- a/packages/@react-aria/color/intl/ru-RU.json
+++ b/packages/@react-aria/color/intl/ru-RU.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "Двумерный ползунок",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Палитра цветов",
+ "twoDimensionalSlider": "Ползунок 2D"
}
diff --git a/packages/@react-aria/color/intl/sk-SK.json b/packages/@react-aria/color/intl/sk-SK.json
index 8aa9d695694..54c43231e9c 100644
--- a/packages/@react-aria/color/intl/sk-SK.json
+++ b/packages/@react-aria/color/intl/sk-SK.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D jazdec",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Výber farieb",
+ "twoDimensionalSlider": "2D jazdec"
}
diff --git a/packages/@react-aria/color/intl/sl-SI.json b/packages/@react-aria/color/intl/sl-SI.json
index 02845036219..4ea49d1254f 100644
--- a/packages/@react-aria/color/intl/sl-SI.json
+++ b/packages/@react-aria/color/intl/sl-SI.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D-drsnik",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Izbirnik barv",
+ "twoDimensionalSlider": "2D drsnik"
}
diff --git a/packages/@react-aria/color/intl/sr-SP.json b/packages/@react-aria/color/intl/sr-SP.json
index a4b9bc4b6cb..20c43dbb962 100644
--- a/packages/@react-aria/color/intl/sr-SP.json
+++ b/packages/@react-aria/color/intl/sr-SP.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D клизач",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Birač boja",
+ "twoDimensionalSlider": "2D klizač"
}
diff --git a/packages/@react-aria/color/intl/sv-SE.json b/packages/@react-aria/color/intl/sv-SE.json
index b0732a46bcf..51e045afd75 100644
--- a/packages/@react-aria/color/intl/sv-SE.json
+++ b/packages/@react-aria/color/intl/sv-SE.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D-reglage",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Färgväljaren",
+ "twoDimensionalSlider": "2D-reglage"
}
diff --git a/packages/@react-aria/color/intl/tr-TR.json b/packages/@react-aria/color/intl/tr-TR.json
index 0a69975643a..a2669bd264b 100644
--- a/packages/@react-aria/color/intl/tr-TR.json
+++ b/packages/@react-aria/color/intl/tr-TR.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2B slayt gösterisi",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Renk Seçici",
+ "twoDimensionalSlider": "2D sürgü"
}
diff --git a/packages/@react-aria/color/intl/uk-UA.json b/packages/@react-aria/color/intl/uk-UA.json
index 6bd3ac3b2bc..89cfaebe80d 100644
--- a/packages/@react-aria/color/intl/uk-UA.json
+++ b/packages/@react-aria/color/intl/uk-UA.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "Повзунок 2D",
+ "colorInputLabel": "{label}, {channelLabel}",
"colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorPicker": "Палітра кольорів",
+ "twoDimensionalSlider": "Повзунок 2D"
}
diff --git a/packages/@react-aria/color/intl/zh-CN.json b/packages/@react-aria/color/intl/zh-CN.json
index 171ba64dcb0..a57de6f3c9b 100644
--- a/packages/@react-aria/color/intl/zh-CN.json
+++ b/packages/@react-aria/color/intl/zh-CN.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D 滑块",
- "colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorInputLabel": "{label}、{channelLabel}",
+ "colorNameAndValue": "{name}:{value}",
+ "colorPicker": "拾色器",
+ "twoDimensionalSlider": "2D 滑块"
}
diff --git a/packages/@react-aria/color/intl/zh-TW.json b/packages/@react-aria/color/intl/zh-TW.json
index d8c87cc8bd8..b7c4e9066e8 100644
--- a/packages/@react-aria/color/intl/zh-TW.json
+++ b/packages/@react-aria/color/intl/zh-TW.json
@@ -1,5 +1,6 @@
{
- "twoDimensionalSlider": "2D 滑桿",
- "colorNameAndValue": "{name}: {value}",
- "x/y": "{x} / {y}"
+ "colorInputLabel": "{label},{channelLabel}",
+ "colorNameAndValue": "{name}:{value}",
+ "colorPicker": "檢色器",
+ "twoDimensionalSlider": "2D 滑桿"
}
diff --git a/packages/@react-aria/color/package.json b/packages/@react-aria/color/package.json
index bb26979ed7e..080e20783f5 100644
--- a/packages/@react-aria/color/package.json
+++ b/packages/@react-aria/color/package.json
@@ -1,10 +1,15 @@
{
"name": "@react-aria/color",
- "version": "3.0.0-beta.10",
+ "version": "3.0.0-beta.20",
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
"module": "dist/module.js",
+ "exports": {
+ "types": "./dist/types.d.ts",
+ "import": "./dist/import.mjs",
+ "require": "./dist/main.js"
+ },
"types": "dist/types.d.ts",
"source": "src/index.ts",
"files": [
@@ -17,23 +22,22 @@
"url": "https://github.com/adobe/react-spectrum"
},
"dependencies": {
- "@babel/runtime": "^7.6.2",
- "@internationalized/message": "^3.0.2",
- "@react-aria/i18n": "^3.3.8",
- "@react-aria/interactions": "^3.8.3",
- "@react-aria/slider": "^3.0.7",
- "@react-aria/spinbutton": "^3.0.5",
- "@react-aria/textfield": "^3.5.4",
- "@react-aria/utils": "^3.11.3",
- "@react-aria/visually-hidden": "^3.2.7",
- "@react-stately/color": "3.0.0-beta.9",
- "@react-types/color": "3.0.0-beta.7",
- "@react-types/shared": "^3.11.2",
- "@react-types/slider": "^3.0.5"
+ "@react-aria/i18n": "^3.7.1",
+ "@react-aria/interactions": "^3.15.0",
+ "@react-aria/slider": "^3.4.0",
+ "@react-aria/spinbutton": "^3.4.0",
+ "@react-aria/textfield": "^3.9.1",
+ "@react-aria/utils": "^3.16.0",
+ "@react-aria/visually-hidden": "^3.8.0",
+ "@react-stately/color": "^3.3.1",
+ "@react-types/color": "3.0.0-beta.16",
+ "@react-types/shared": "^3.18.0",
+ "@react-types/slider": "^3.5.0",
+ "@swc/helpers": "^0.4.14"
},
"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 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
},
"publishConfig": {
"access": "public"
diff --git a/packages/@react-aria/color/src/index.ts b/packages/@react-aria/color/src/index.ts
index 64b356d1a53..dd5423790b6 100644
--- a/packages/@react-aria/color/src/index.ts
+++ b/packages/@react-aria/color/src/index.ts
@@ -9,8 +9,12 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
-
-export * from './useColorArea';
-export * from './useColorSlider';
-export * from './useColorWheel';
-export * from './useColorField';
+export {useColorArea} from './useColorArea';
+export {useColorSlider} from './useColorSlider';
+export {useColorWheel} from './useColorWheel';
+export {useColorField} from './useColorField';
+export type {AriaColorAreaOptions, ColorAreaAria} from './useColorArea';
+export type {AriaColorSliderOptions, ColorSliderAria} from './useColorSlider';
+export type {AriaColorWheelOptions, ColorWheelAria} from './useColorWheel';
+export type {AriaColorFieldProps} from '@react-types/color';
+export type {ColorFieldAria} from './useColorField';
diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts
index f86116d08f6..e3c08e21d5e 100644
--- a/packages/@react-aria/color/src/useColorArea.ts
+++ b/packages/@react-aria/color/src/useColorArea.ts
@@ -12,57 +12,59 @@
import {AriaColorAreaProps, ColorChannel} from '@react-types/color';
import {ColorAreaState} from '@react-stately/color';
+import {DOMAttributes} from '@react-types/shared';
import {focusWithoutScrolling, isAndroid, isIOS, mergeProps, useGlobalListeners, useLabels} from '@react-aria/utils';
// @ts-ignore
import intlMessages from '../intl/*.json';
-import React, {ChangeEvent, HTMLAttributes, InputHTMLAttributes, RefObject, useCallback, useRef} from 'react';
+import React, {ChangeEvent, InputHTMLAttributes, RefObject, useCallback, useRef} from 'react';
import {useColorAreaGradient} from './useColorAreaGradient';
-import {useKeyboard, useMove} from '@react-aria/interactions';
-import {useLocale, useMessageFormatter} from '@react-aria/i18n';
+import {useFocus, useFocusWithin, useKeyboard, useMove} from '@react-aria/interactions';
+import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n';
import {useVisuallyHidden} from '@react-aria/visually-hidden';
-interface ColorAreaAria {
+export interface ColorAreaAria {
/** Props for the color area container element. */
- colorAreaProps: HTMLAttributes
,
+ colorAreaProps: DOMAttributes,
/** Props for the color area gradient foreground element. */
- gradientProps: HTMLAttributes,
+ gradientProps: DOMAttributes,
/** Props for the thumb element. */
- thumbProps: HTMLAttributes,
+ thumbProps: DOMAttributes,
/** Props for the visually hidden horizontal range input element. */
xInputProps: InputHTMLAttributes,
/** Props for the visually hidden vertical range input element. */
yInputProps: InputHTMLAttributes
}
-interface ColorAreaAriaProps extends AriaColorAreaProps {
+export interface AriaColorAreaOptions extends AriaColorAreaProps {
/** A ref to the input that represents the x axis of the color area. */
- inputXRef: RefObject,
+ inputXRef: RefObject,
/** A ref to the input that represents the y axis of the color area. */
- inputYRef: RefObject,
+ inputYRef: RefObject,
/** A ref to the color area containing element. */
- containerRef: RefObject
+ containerRef: RefObject
}
/**
- * Provides the behavior and accessibility implementation for a color wheel component.
- * Color wheels allow users to adjust the hue of an HSL or HSB color value on a circular track.
+ * Provides the behavior and accessibility implementation for a color area component.
+ * Color area allows users to adjust two channels of an RGB, HSL or HSB color value against a two-dimensional gradient background.
*/
-export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState): ColorAreaAria {
+export function useColorArea(props: AriaColorAreaOptions, state: ColorAreaState): ColorAreaAria {
let {
isDisabled,
inputXRef,
inputYRef,
- containerRef
+ containerRef,
+ 'aria-label': ariaLabel
} = props;
- let formatMessage = useMessageFormatter(intlMessages);
+ let stringFormatter = useLocalizedStringFormatter(intlMessages);
let {addGlobalListener, removeGlobalListener} = useGlobalListeners();
let {direction, locale} = useLocale();
- let focusedInputRef = useRef(null);
+ let focusedInputRef = useRef(null);
- let focusInput = useCallback((inputRef:RefObject = inputXRef) => {
+ let focusInput = useCallback((inputRef:RefObject = inputXRef) => {
if (inputRef.current) {
focusWithoutScrolling(inputRef.current);
}
@@ -87,6 +89,7 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
e.preventDefault();
// remember to set this and unset it so that onChangeEnd is fired
stateRef.current.setDragging(true);
+ valueChangedViaKeyboard.current = true;
switch (e.key) {
case 'PageUp':
stateRef.current.incrementY(stateRef.current.yChannelPageStep);
@@ -108,7 +111,6 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
stateRef.current.setDragging(false);
if (focusedInputRef.current) {
focusInput(focusedInputRef.current ? focusedInputRef : inputXRef);
- focusedInputRef.current = undefined;
}
}
});
@@ -135,6 +137,7 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
currentPosition.current = getThumbPosition();
}
let {width, height} = containerRef.current.getBoundingClientRect();
+ let valueChanged = deltaX !== 0 || deltaY !== 0;
if (pointerType === 'keyboard') {
let deltaXValue = shiftKey && xChannelPageStep > xChannelStep ? xChannelPageStep : xChannelStep;
let deltaYValue = shiftKey && yChannelPageStep > yChannelStep ? yChannelPageStep : yChannelStep;
@@ -147,8 +150,9 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
} else if (deltaY < 0) {
incrementY(deltaYValue);
}
+ valueChangedViaKeyboard.current = valueChanged;
// set the focused input based on which axis has the greater delta
- focusedInputRef.current = (deltaX !== 0 || deltaY !== 0) && Math.abs(deltaY) > Math.abs(deltaX) ? inputYRef.current : inputXRef.current;
+ focusedInputRef.current = valueChanged && Math.abs(deltaY) > Math.abs(deltaX) ? inputYRef.current : inputXRef.current;
} else {
currentPosition.current.x += (direction === 'rtl' ? -1 : 1) * deltaX / width ;
currentPosition.current.y += deltaY / height;
@@ -159,11 +163,20 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
isOnColorArea.current = undefined;
stateRef.current.setDragging(false);
focusInput(focusedInputRef.current ? focusedInputRef : inputXRef);
- focusedInputRef.current = undefined;
}
};
let {moveProps: movePropsThumb} = useMove(moveHandler);
+ let valueChangedViaKeyboard = useRef(false);
+ let {focusWithinProps} = useFocusWithin({
+ onFocusWithinChange: (focusWithin:boolean) => {
+ if (!focusWithin) {
+ valueChangedViaKeyboard.current = false;
+ focusedInputRef.current === undefined;
+ }
+ }
+ });
+
let currentPointer = useRef(undefined);
let isOnColorArea = useRef(false);
let {moveProps: movePropsContainer} = useMove({
@@ -187,6 +200,7 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
let onThumbDown = (id: number | null) => {
if (!state.isDragging) {
currentPointer.current = id;
+ valueChangedViaKeyboard.current = false;
focusInput();
state.setDragging(true);
if (typeof PointerEvent !== 'undefined') {
@@ -201,6 +215,7 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
let onThumbUp = (e) => {
let id = e.pointerId ?? e.changedTouches?.[0].identifier;
if (id === currentPointer.current) {
+ valueChangedViaKeyboard.current = false;
focusInput();
state.setDragging(false);
currentPointer.current = undefined;
@@ -225,6 +240,7 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
}
if (x >= 0 && x <= 1 && y >= 0 && y <= 1 && !state.isDragging && currentPointer.current === undefined) {
isOnColorArea.current = true;
+ valueChangedViaKeyboard.current = false;
currentPointer.current = id;
state.setColorFromPoint(x, y);
@@ -244,6 +260,7 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
let id = e.pointerId ?? e.changedTouches?.[0].identifier;
if (isOnColorArea.current && id === currentPointer.current) {
isOnColorArea.current = false;
+ valueChangedViaKeyboard.current = false;
currentPointer.current = undefined;
state.setDragging(false);
focusInput();
@@ -295,34 +312,55 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
onThumbDown(e.changedTouches[0].identifier);
}
})
- }, keyboardProps, movePropsThumb);
+ }, focusWithinProps, keyboardProps, movePropsThumb);
+
+ let {focusProps: xInputFocusProps} = useFocus({
+ onFocus: () => {
+ focusedInputRef.current = inputXRef.current;
+ }
+ });
+
+ let {focusProps: yInputFocusProps} = useFocus({
+ onFocus: () => {
+ focusedInputRef.current = inputYRef.current;
+ }
+ });
let isMobile = isIOS() || isAndroid();
+ function getAriaValueTextForChannel(channel:ColorChannel) {
+ return (
+ valueChangedViaKeyboard.current ?
+ stringFormatter.format('colorNameAndValue', {name: state.value.getChannelName(channel, locale), value: state.value.formatChannelValue(channel, locale)})
+ :
+ [
+ stringFormatter.format('colorNameAndValue', {name: state.value.getChannelName(channel, locale), value: state.value.formatChannelValue(channel, locale)}),
+ stringFormatter.format('colorNameAndValue', {name: state.value.getChannelName(channel === yChannel ? xChannel : yChannel, locale), value: state.value.formatChannelValue(channel === yChannel ? xChannel : yChannel, locale)})
+ ].join(', ')
+ );
+ }
+
+ let colorPickerLabel = stringFormatter.format('colorPicker');
+
let xInputLabellingProps = useLabels({
...props,
- 'aria-label': isMobile ? state.value.getChannelName(xChannel, locale) : formatMessage('x/y', {x: state.value.getChannelName(xChannel, locale), y: state.value.getChannelName(yChannel, locale)})
+ 'aria-label': ariaLabel ? stringFormatter.format('colorInputLabel', {label: ariaLabel, channelLabel: colorPickerLabel}) : colorPickerLabel
});
let yInputLabellingProps = useLabels({
...props,
- 'aria-label': isMobile ? state.value.getChannelName(yChannel, locale) : formatMessage('x/y', {x: state.value.getChannelName(xChannel, locale), y: state.value.getChannelName(yChannel, locale)})
+ 'aria-label': ariaLabel ? stringFormatter.format('colorInputLabel', {label: ariaLabel, channelLabel: colorPickerLabel}) : colorPickerLabel
});
- let colorAriaLabellingProps = useLabels(props);
-
- let getValueTitle = () => {
- const channels: [ColorChannel, ColorChannel, ColorChannel] = state.value.getColorChannels();
- const colorNamesAndValues = [];
- channels.forEach(channel =>
- colorNamesAndValues.push(
- formatMessage('colorNameAndValue', {name: state.value.getChannelName(channel, locale), value: state.value.formatChannelValue(channel, locale)})
- )
- );
- return colorNamesAndValues.length ? colorNamesAndValues.join(', ') : null;
- };
+ let colorAreaLabellingProps = useLabels(
+ {
+ ...props,
+ 'aria-label': ariaLabel ? `${ariaLabel}, ${colorPickerLabel}` : undefined
+ },
+ isMobile ? colorPickerLabel : undefined
+ );
- let ariaRoleDescription = isMobile ? null : formatMessage('twoDimensionalSlider');
+ let ariaRoleDescription = stringFormatter.format('twoDimensionalSlider');
let {visuallyHiddenProps} = useVisuallyHidden({style: {
opacity: '0.0001',
@@ -343,10 +381,9 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
isDisabled: props.isDisabled
});
-
return {
colorAreaProps: {
- ...colorAriaLabellingProps,
+ ...colorAreaLabellingProps,
...colorAreaInteractions,
...colorAreaStyleProps,
role: 'group'
@@ -363,24 +400,22 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
xInputProps: {
...xInputLabellingProps,
...visuallyHiddenProps,
+ ...xInputFocusProps,
type: 'range',
min: state.value.getChannelRange(xChannel).minValue,
max: state.value.getChannelRange(xChannel).maxValue,
step: xChannelStep,
'aria-roledescription': ariaRoleDescription,
- 'aria-valuetext': (
- isMobile ?
- formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)})
- :
- [
- formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)}),
- formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)})
- ].join(', ')
- ),
- title: getValueTitle(),
+ 'aria-valuetext': getAriaValueTextForChannel(xChannel),
disabled: isDisabled,
value: state.value.getChannelValue(xChannel),
- tabIndex: 0,
+ tabIndex: (isMobile || !focusedInputRef.current || focusedInputRef.current === inputXRef.current ? undefined : -1),
+ /*
+ So that only a single "2d slider" control shows up when listing form elements for screen readers,
+ add aria-hidden="true" to the unfocused control when the value has not changed via the keyboard,
+ but remove aria-hidden to reveal the input for each channel when the value has changed with the keyboard.
+ */
+ 'aria-hidden': (isMobile || !focusedInputRef.current || focusedInputRef.current === inputXRef.current || valueChangedViaKeyboard.current ? undefined : 'true'),
onChange: (e: ChangeEvent) => {
state.setXValue(parseFloat(e.target.value));
}
@@ -388,25 +423,23 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
yInputProps: {
...yInputLabellingProps,
...visuallyHiddenProps,
+ ...yInputFocusProps,
type: 'range',
min: state.value.getChannelRange(yChannel).minValue,
max: state.value.getChannelRange(yChannel).maxValue,
step: yChannelStep,
'aria-roledescription': ariaRoleDescription,
- 'aria-valuetext': (
- isMobile ?
- formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)})
- :
- [
- formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)}),
- formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)})
- ].join(', ')
- ),
+ 'aria-valuetext': getAriaValueTextForChannel(yChannel),
'aria-orientation': 'vertical',
- title: getValueTitle(),
disabled: isDisabled,
value: state.value.getChannelValue(yChannel),
- tabIndex: -1,
+ tabIndex: (isMobile || (focusedInputRef.current && focusedInputRef.current === inputYRef.current) ? undefined : -1),
+ /*
+ So that only a single "2d slider" control shows up when listing form elements for screen readers,
+ add aria-hidden="true" to the unfocused input when the value has not changed via the keyboard,
+ but remove aria-hidden to reveal the input for each channel when the value has changed with the keyboard.
+ */
+ 'aria-hidden': (isMobile || (focusedInputRef.current && focusedInputRef.current === inputYRef.current) || valueChangedViaKeyboard.current ? undefined : 'true'),
onChange: (e: ChangeEvent) => {
state.setYValue(parseFloat(e.target.value));
}
diff --git a/packages/@react-aria/color/src/useColorAreaGradient.ts b/packages/@react-aria/color/src/useColorAreaGradient.ts
index 4206be8a128..50b59fd46cc 100644
--- a/packages/@react-aria/color/src/useColorAreaGradient.ts
+++ b/packages/@react-aria/color/src/useColorAreaGradient.ts
@@ -218,17 +218,21 @@ export function useColorAreaGradient({direction, state, zChannel, xChannel, isDi
x = 1 - x;
}
+ let forcedColorAdjustNoneStyle = {forcedColorAdjust: 'none'};
+
return {
colorAreaStyleProps: {
style: {
position: 'relative',
touchAction: 'none',
+ ...forcedColorAdjustNoneStyle,
...background.colorAreaStyles
}
},
gradientStyleProps: {
style: {
touchAction: 'none',
+ ...forcedColorAdjustNoneStyle,
...background.gradientStyles
}
},
@@ -238,7 +242,8 @@ export function useColorAreaGradient({direction, state, zChannel, xChannel, isDi
left: `${x * 100}%`,
top: `${y * 100}%`,
transform: 'translate(0%, 0%)',
- touchAction: 'none'
+ touchAction: 'none',
+ ...forcedColorAdjustNoneStyle
}
}
};
diff --git a/packages/@react-aria/color/src/useColorField.ts b/packages/@react-aria/color/src/useColorField.ts
index 70908094fea..9e1b04608b3 100644
--- a/packages/@react-aria/color/src/useColorField.ts
+++ b/packages/@react-aria/color/src/useColorField.ts
@@ -24,7 +24,7 @@ import {useFocusWithin, useScrollWheel} from '@react-aria/interactions';
import {useFormattedTextField} from '@react-aria/textfield';
import {useSpinButton} from '@react-aria/spinbutton';
-interface ColorFieldAria {
+export interface ColorFieldAria {
/** Props for the label element. */
labelProps: LabelHTMLAttributes,
/** Props for the input element. */
diff --git a/packages/@react-aria/color/src/useColorSlider.ts b/packages/@react-aria/color/src/useColorSlider.ts
index 4c15b946223..aeaa59e8ba2 100644
--- a/packages/@react-aria/color/src/useColorSlider.ts
+++ b/packages/@react-aria/color/src/useColorSlider.ts
@@ -12,36 +12,37 @@
import {AriaColorSliderProps} from '@react-types/color';
import {ColorSliderState} from '@react-stately/color';
-import {HTMLAttributes, RefObject} from 'react';
+import {DOMAttributes} from '@react-types/shared';
+import {InputHTMLAttributes, RefObject} from 'react';
import {mergeProps} from '@react-aria/utils';
import {useLocale} from '@react-aria/i18n';
import {useSlider, useSliderThumb} from '@react-aria/slider';
-interface ColorSliderAriaOptions extends AriaColorSliderProps {
+export interface AriaColorSliderOptions extends AriaColorSliderProps {
/** A ref for the track element. */
- trackRef: RefObject,
+ trackRef: RefObject,
/** A ref for the input element. */
inputRef: RefObject
}
-interface ColorSliderAria {
+export interface ColorSliderAria {
/** Props for the label element. */
- labelProps: HTMLAttributes,
+ labelProps: DOMAttributes,
/** Props for the track element. */
- trackProps: HTMLAttributes,
+ trackProps: DOMAttributes,
/** Props for the thumb element. */
- thumbProps: HTMLAttributes,
+ thumbProps: DOMAttributes,
/** Props for the visually hidden range input element. */
- inputProps: HTMLAttributes,
+ inputProps: InputHTMLAttributes,
/** Props for the output element, displaying the value of the color slider. */
- outputProps: HTMLAttributes
+ outputProps: DOMAttributes
}
/**
* Provides the behavior and accessibility implementation for a color slider component.
* Color sliders allow users to adjust an individual channel of a color value.
*/
-export function useColorSlider(props: ColorSliderAriaOptions, state: ColorSliderState): ColorSliderAria {
+export function useColorSlider(props: AriaColorSliderOptions, state: ColorSliderState): ColorSliderAria {
let {trackRef, inputRef, orientation, channel, 'aria-label': ariaLabel} = props;
let {locale, direction} = useLocale();
@@ -99,17 +100,14 @@ export function useColorSlider(props: ColorSliderAriaOptions, state: ColorSlider
}
};
- let thumbPosition = state.getThumbPercent(0);
- if (orientation === 'vertical' || direction === 'rtl') {
- thumbPosition = 1 - thumbPosition;
- }
+ let forcedColorAdjustNoneStyle = {forcedColorAdjust: 'none'};
return {
trackProps: {
...mergeProps(groupProps, trackProps),
style: {
- position: 'relative',
- touchAction: 'none',
+ ...trackProps.style,
+ ...forcedColorAdjustNoneStyle,
background: generateBackground()
}
},
@@ -117,10 +115,8 @@ export function useColorSlider(props: ColorSliderAriaOptions, state: ColorSlider
thumbProps: {
...thumbProps,
style: {
- touchAction: 'none',
- position: 'absolute',
- [orientation === 'vertical' ? 'top' : 'left']: `${thumbPosition * 100}%`,
- transform: 'translate(-50%, -50%)'
+ ...thumbProps.style,
+ ...forcedColorAdjustNoneStyle
}
},
labelProps,
diff --git a/packages/@react-aria/color/src/useColorWheel.ts b/packages/@react-aria/color/src/useColorWheel.ts
index 1c3ea44d93d..202c726df58 100644
--- a/packages/@react-aria/color/src/useColorWheel.ts
+++ b/packages/@react-aria/color/src/useColorWheel.ts
@@ -12,23 +12,24 @@
import {AriaColorWheelProps} from '@react-types/color';
import {ColorWheelState} from '@react-stately/color';
+import {DOMAttributes} from '@react-types/shared';
import {focusWithoutScrolling, mergeProps, useGlobalListeners, useLabels} from '@react-aria/utils';
-import React, {ChangeEvent, HTMLAttributes, InputHTMLAttributes, RefObject, useCallback, useRef} from 'react';
+import React, {ChangeEvent, InputHTMLAttributes, RefObject, useCallback, useRef} from 'react';
import {useKeyboard, useMove} from '@react-aria/interactions';
import {useLocale} from '@react-aria/i18n';
-interface ColorWheelAriaProps extends AriaColorWheelProps {
+export interface AriaColorWheelOptions extends AriaColorWheelProps {
/** The outer radius of the color wheel. */
outerRadius: number,
/** The inner radius of the color wheel. */
innerRadius: number
}
-interface ColorWheelAria {
+export interface ColorWheelAria {
/** Props for the track element. */
- trackProps: HTMLAttributes,
+ trackProps: DOMAttributes,
/** Props for the thumb element. */
- thumbProps: HTMLAttributes,
+ thumbProps: DOMAttributes,
/** Props for the visually hidden range input element. */
inputProps: InputHTMLAttributes
}
@@ -37,7 +38,7 @@ interface ColorWheelAria {
* Provides the behavior and accessibility implementation for a color wheel component.
* Color wheels allow users to adjust the hue of an HSL or HSB color value on a circular track.
*/
-export function useColorWheel(props: ColorWheelAriaProps, state: ColorWheelState, inputRef: RefObject): ColorWheelAria {
+export function useColorWheel(props: AriaColorWheelOptions, state: ColorWheelState, inputRef: RefObject): ColorWheelAria {
let {
isDisabled,
innerRadius,
@@ -257,6 +258,11 @@ export function useColorWheel(props: ColorWheelAriaProps, state: ColorWheelState
});
let {minValue, maxValue, step} = state.value.getChannelRange('hue');
+
+ let forcedColorAdjustNoneStyle = {
+ forcedColorAdjust: 'none'
+ };
+
return {
trackProps: {
...trackInteractions,
@@ -283,7 +289,8 @@ export function useColorWheel(props: ColorWheelAriaProps, state: ColorWheelState
hsl(360, 100%, 50%)
)
`,
- clipPath: `path(evenodd, "${circlePath(outerRadius, outerRadius, outerRadius)} ${circlePath(outerRadius, outerRadius, innerRadius)}")`
+ clipPath: `path(evenodd, "${circlePath(outerRadius, outerRadius, outerRadius)} ${circlePath(outerRadius, outerRadius, innerRadius)}")`,
+ ...forcedColorAdjustNoneStyle
}
},
thumbProps: {
@@ -293,7 +300,8 @@ export function useColorWheel(props: ColorWheelAriaProps, state: ColorWheelState
left: '50%',
top: '50%',
transform: `translate(calc(${x}px - 50%), calc(${y}px - 50%))`,
- touchAction: 'none'
+ touchAction: 'none',
+ ...forcedColorAdjustNoneStyle
}
},
inputProps: mergeProps(
diff --git a/packages/@react-aria/color/test/useColorField.test.js b/packages/@react-aria/color/test/useColorField.test.js
index b42624136a7..c559d37ed9b 100644
--- a/packages/@react-aria/color/test/useColorField.test.js
+++ b/packages/@react-aria/color/test/useColorField.test.js
@@ -12,7 +12,7 @@
import {parseColor} from '@react-stately/color';
import React from 'react';
-import {renderHook} from '@testing-library/react-hooks';
+import {renderHook} from '@react-spectrum/test-utils';
import {useColorField} from '../';
describe('useColorField', function () {
diff --git a/packages/@react-aria/color/test/useColorWheel.test.tsx b/packages/@react-aria/color/test/useColorWheel.test.tsx
index 2026c88fd35..65545975ac0 100644
--- a/packages/@react-aria/color/test/useColorWheel.test.tsx
+++ b/packages/@react-aria/color/test/useColorWheel.test.tsx
@@ -10,9 +10,8 @@
* governing permissions and limitations under the License.
*/
-import {act, fireEvent, render} from '@testing-library/react';
+import {act, fireEvent, installMouseEvent, installPointerEvent, render} from '@react-spectrum/test-utils';
import {ColorWheelProps} from '@react-types/color';
-import {installMouseEvent, installPointerEvent} from '@react-spectrum/test-utils';
import {parseColor, useColorWheelState} from '@react-stately/color';
import React, {useRef} from 'react';
import {useColorWheel} from '../';
diff --git a/packages/@react-aria/combobox/docs/useComboBox.mdx b/packages/@react-aria/combobox/docs/useComboBox.mdx
index 8f880356303..d1bef58b2ac 100644
--- a/packages/@react-aria/combobox/docs/useComboBox.mdx
+++ b/packages/@react-aria/combobox/docs/useComboBox.mdx
@@ -42,7 +42,7 @@ after_version: 3.0.0-alpha.0
packageData={packageData}
componentNames={['useComboBox']}
sourceData={[
- {type: 'W3C', url: 'https://www.w3.org/TR/wai-aria-practices-1.2/#combobox'}
+ {type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/combobox/'}
]} />
## API
@@ -76,6 +76,8 @@ A combo box can be built using the [<datalist>](https://developer.mozilla.
* Custom localized announcements for option focusing, filtering, and selection using an ARIA live region to work around VoiceOver bugs
* Support for description and error message help text linked to the input via ARIA
+Read our [blog post](../blog/building-a-combobox.html) for more details about the interactions and accessibility features implemented by `useComboBox`.
+
## Anatomy
@@ -129,15 +131,11 @@ is included within the `` to display the dropdown arrow icon (hidden fro
A "contains" filter function is obtained from and is passed to so
that the list box can be filtered based on the option text and the current input text.
-The list box popup should use the same `Popover` and `ListBox` components created with [useOverlay](useOverlay.html)
-and [useListBox](useListBox.html) that you may already have in your component library or application. These can be shared with other
+The same `Popover`, `ListBox`, and `Button` components created with [usePopover](usePopover.html), [useListBox](useListBox.html),
+and [useButton](useButton.html) that you may already have in your component library or application should be reused. These can be shared with other
components such as a `Select` created with [useSelect](useSelect.html) or a `Dialog` popover created with [useDialog](useDialog.html).
The code for these components is also included below in the collapsed sections.
-This example does not do any advanced popover positioning or portaling to escape its visual container.
-See [useOverlayTrigger](useOverlayTrigger.html) for an example of how to implement this
-using .
-
In addition, see [useListBox](useListBox.html) for examples of sections (option groups), and more complex
options. For an example of the description and error message elements, see [useTextField](useTextField.html).
@@ -148,8 +146,8 @@ import {useComboBoxState} from '@react-stately/combobox'
import {useComboBox} from '@react-aria/combobox';
import {useFilter} from '@react-aria/i18n';
-// Reuse the ListBox and Popover from your component library. See below for details.
-import {ListBox, Popover} from 'your-component-library';
+// Reuse the ListBox, Popover, and Button from your component library. See below for details.
+import {ListBox, Popover, Button} from 'your-component-library';
function ComboBox(props) {
// Setup filter function and state.
@@ -162,7 +160,7 @@ function ComboBox(props) {
let listBoxRef = React.useRef(null);
let popoverRef = React.useRef(null);
- let {buttonProps: triggerProps, inputProps, listBoxProps, labelProps} = useComboBox(
+ let {buttonProps, inputProps, listBoxProps, labelProps} = useComboBox(
{
...props,
inputRef,
@@ -173,15 +171,10 @@ function ComboBox(props) {
state
);
- // Call useButton to get props for the button element. Alternatively, you can
- // pass the triggerProps to a separate Button component using useButton
- // that you might already have in your component library.
- let {buttonProps} = useButton(triggerProps, buttonRef);
-
return (