Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/dev/docs/pages/blog/building-a-combobox.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ export default BlogPostLayout;

---
keywords: [combobox, accessibility, mobile, react spectrum, react, spectrum, interactions, touch]
description: After many months of research, development, and testing, we’re excited to announce that the React Spectrum [ComboBox](../react-spectrum/ComboBox.html) component and React Aria [useComboBox](../react-aria/useComboBox.html) hook are now available! In this post we'll take a deeper look into some of the challenges we faced when building an accessible and mobile friendly ComboBox.
description: After many months of research, development, and testing, we’re excited to announce that the React Spectrum [ComboBox](../../s2/ComboBox.html) component and React Aria [useComboBox](../useComboBox.html) hook are now available! In this post we'll take a deeper look into some of the challenges we faced when building an accessible and mobile friendly ComboBox.
date: 2021-07-13
author: '[Daniel Lu](https://github.com/LFDanLu)'
---

# Creating an accessible autocomplete experience

After many months of research, development, and extensive testing across browsers, devices, and assistive technology, we’re excited to announce that the React Spectrum [ComboBox](../react-spectrum/ComboBox.html) component and React Aria [useComboBox](../react-aria/useComboBox.html) hook are now available! We’ve focused on the following areas to help you build quality autocomplete experiences.
After many months of research, development, and extensive testing across browsers, devices, and assistive technology, we’re excited to announce that the React Spectrum [ComboBox](../../s2/ComboBox.html) component and React Aria [useComboBox](../useComboBox.html) hook are now available! We’ve focused on the following areas to help you build quality autocomplete experiences.

- **Accessibility** — Our ComboBox has been tested with screen readers across desktop and mobile devices, and with many different input methods including mouse, touch, and keyboard. We encountered many screen reader differences, and worked hard to ensure announcements are clear and consistent.
- **Mobile** — On small screens, the React Spectrum ComboBox is automatically displayed in a tray, which improves the user experience by giving them a larger area to scroll. We also optimized our experience for touch screen interactions and on screen keyboards.
Expand Down Expand Up @@ -133,7 +133,7 @@ Special care was taken such that the messages themselves only contained relevant
When the user then moves to a different option in the same section, only the newly focused item name is announced. Similarly, the total option count is only announced when number of options available in the listbox changes. Since many of these messages were added to fill in gaps in VoiceOver's announcement,
we only trigger the `LiveAnnouncer` on Apple devices to avoid announcement overlap on other screen readers.

If you are interested in using this `LiveAnnouncer` yourself, check out [LiveAnnouncer](https://github.com/adobe/react-spectrum/blob/main/packages/@react-aria/live-announcer/src/LiveAnnouncer.tsx) in `@react-aria/live-announcer`. Otherwise, the [useComboBox](../react-aria/useComboBox.html) hook provides you with all of the custom messaging out of the box. See the video below for a sneak peek!
If you are interested in using this `LiveAnnouncer` yourself, check out [LiveAnnouncer](https://github.com/adobe/react-spectrum/blob/main/packages/@react-aria/live-announcer/src/LiveAnnouncer.tsx) in `@react-aria/live-announcer`. Otherwise, the [useComboBox](../useComboBox.html) hook provides you with all of the custom messaging out of the box. See the video below for a sneak peek!

<Video src={comboboxAccessibilityUrl} alt="Demo of VoiceOver announcement in ComboBox" style={{maxWidth: 'min(100%, 700px)', display: 'block', margin: '20px auto'}} controls />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Numbers can be formatted in many different ways, including percentages, units, d

Parsing numbers while taking into account all locale-specific detail is quite complex and error-prone. `NumberParser` uses information about the locale and expected format for a number in order to parse it correctly. This means it is somewhat strict about the accepted formats. It is not designed to handle use cases where the user can enter numbers in an unknown format (e.g. either a unit value _or_ a percentage), this must be known up front or selected via an external UI control.

Read our [blog post](https://react-spectrum.adobe.com/blog/how-we-internationalized-our-numberfield.html) for more details on how the number parser is implemented.
Read our [blog post](how-we-internationalized-our-numberfield.html) for more details on how the number parser is implemented.

### Example

Expand Down
2 changes: 1 addition & 1 deletion packages/dev/s2-docs/pages/react-aria/Button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const tags = ['btn'];

## Events

Use the `onPress` prop to handle interactions via mouse, keyboard, and touch. This is similar to `onClick`, but normalized for consistency across browsers, devices, and interaction methods. Read our [blog post](https://react-spectrum.adobe.com/blog/building-a-button-part-1.html) to learn more.
Use the `onPress` prop to handle interactions via mouse, keyboard, and touch. This is similar to `onClick`, but normalized for consistency across browsers, devices, and interaction methods. Read our [blog post](building-a-button-part-1.html) to learn more.

The `onPressStart`, `onPressEnd`, and `onPressChange` events are also emitted as the user interacts with the button. Each of these handlers receives a <TypeLink links={typesDocs.links} type={typesDocs.exports.PressEvent} />, which provides information about the target and interaction method. See [usePress](https://react-spectrum.adobe.com/react-aria/usePress.html) for more details.

Expand Down
37 changes: 37 additions & 0 deletions packages/dev/s2-docs/pages/react-aria/blog/CalendarSystems.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';

import {Calendar, Picker, PickerItem, Provider} from '@react-spectrum/s2';
import React from 'react';
import {useLocale} from '@react-aria/i18n';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};

export default function CalendarSystems() {
let [calendar, setCalendar] = React.useState('gregory');
let {locale} = useLocale();
const calendars = [
{key: 'gregory', name: 'Gregorian'},
{key: 'japanese', name: 'Japanese'},
{key: 'buddhist', name: 'Buddhist'},
{key: 'roc', name: 'Taiwan'},
{key: 'persian', name: 'Persian'},
{key: 'indian', name: 'Indian'},
{key: 'islamic-umalqura', name: 'Islamic (Umm al-Qura)'},
{key: 'islamic-civil', name: 'Islamic Civil'},
{key: 'islamic-tbla', name: 'Islamic Tabular'},
{key: 'hebrew', name: 'Hebrew'},
{key: 'coptic', name: 'Coptic'},
{key: 'ethiopic', name: 'Ethiopic'},
{key: 'ethioaa', name: 'Ethiopic (Amete Alem)'}
];

return (
<div className={style({display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8})}>
<Picker label="Calendar system" items={calendars} value={calendar} onChange={setCalendar}>
{item => <PickerItem>{item.name}</PickerItem>}
</Picker>
<Provider locale={`${locale}-u-ca-${calendar}`}>
<Calendar aria-label="Date" />
</Provider>
</div>
)
}
128 changes: 128 additions & 0 deletions packages/dev/s2-docs/pages/react-aria/blog/DragBetweenListsExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
'use client';

import {GridList, GridListItem} from 'vanilla-starter/GridList';
import {useDragAndDrop, isTextDropItem} from 'react-aria-components';
import {useListData} from 'react-stately';
import File from '@react-spectrum/s2/icons/File';
import Folder from '@react-spectrum/s2/icons/Folder';
import React from 'react';

function BidirectionalDnDGridList(props) {
let {list} = props;
let {dragAndDropHooks} = useDragAndDrop({
acceptedDragTypes: ['custom-app-type-bidirectional'],
// Only allow move operations
getAllowedDropOperations: () => ['move'],
getItems(keys) {
return [...keys].map(key => {
let item = list.getItem(key);
// Setup the drag types and associated info for each dragged item.
return {
'custom-app-type-bidirectional': JSON.stringify(item),
'text/plain': item.name
};
});
},
onInsert: async (e) => {
let {
items,
target
} = e;
let processedItems = await Promise.all(
items
.filter(isTextDropItem)
.map(async item => JSON.parse(await item.getText('custom-app-type-bidirectional')))
);
if (target.dropPosition === 'before') {
list.insertBefore(target.key, ...processedItems);
} else if (target.dropPosition === 'after') {
list.insertAfter(target.key, ...processedItems);
}
},
onReorder: (e) => {
let {
keys,
target
} = e;

if (target.dropPosition === 'before') {
list.moveBefore(target.key, [...keys]);
} else if (target.dropPosition === 'after') {
list.moveAfter(target.key, [...keys]);
}
},
onRootDrop: async (e) => {
let {
items
} = e;
let processedItems = await Promise.all(
items
.filter(isTextDropItem)
.map(async item => JSON.parse(await item.getText('custom-app-type-bidirectional')))
);
list.append(...processedItems);
},
/*- begin highlight -*/
onDragEnd: (e) => {
let {
dropOperation,
keys,
isInternal
} = e;
// Only remove the dragged items if they aren't dropped inside the source list
if (dropOperation === 'move' && !isInternal) {
list.remove(...keys);
}
}
/*- end highlight -*/
});

return (
<GridList
aria-label={props['aria-label']}
selectionMode="multiple"
layout="list"
items={list.items}
dragAndDropHooks={dragAndDropHooks}
style={{width: 300, height: 300}}>
{item => (
<GridListItem textValue={item.name}>
{item.type === 'folder' ? <Folder /> : <File />}
<span>{item.name}</span>
</GridListItem>
)}
</GridList>
);
}

export default function DragBetweenListsExample() {
let list1 = useListData({
initialItems: [
{id: '1', type: 'file', name: 'Adobe Photoshop'},
{id: '2', type: 'file', name: 'Adobe XD'},
{id: '3', type: 'folder', name: 'Documents'},
{id: '4', type: 'file', name: 'Adobe InDesign'},
{id: '5', type: 'folder', name: 'Utilities'},
{id: '6', type: 'file', name: 'Adobe AfterEffects'}
]
});

let list2 = useListData({
initialItems: [
{id: '7', type: 'folder', name: 'Pictures'},
{id: '8', type: 'file', name: 'Adobe Fresco'},
{id: '9', type: 'folder', name: 'Apps'},
{id: '10', type: 'file', name: 'Adobe Illustrator'},
{id: '11', type: 'file', name: 'Adobe Lightroom'},
{id: '12', type: 'file', name: 'Adobe Dreamweaver'}
]
});


return (
<div style={{display: 'flex', justifyContent: 'center', flexWrap: 'wrap', gap: 8}}>
<BidirectionalDnDGridList list={list1} aria-label="First GridList in drag between list example" />
<BidirectionalDnDGridList list={list2} aria-label="Second GridList in drag between list example" />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import {today, getLocalTimeZone} from '@internationalized/date';
import {RangeCalendar} from '@react-spectrum/s2';
import React from 'react';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};

export default function RangeCalendarExample() {
let now = today(getLocalTimeZone()).set({day: 8});
let disabledRanges = [
[now, now.add({days: 2})],
[now.add({days: 10}), now.add({days: 14})],
[now.add({days: 23}), now.add({days: 28})],
];

let isDateUnavailable = (date) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0);

return (
<div className={style({display: 'flex', flexDirection: 'column', alignItems: 'center'})}>
<RangeCalendar
aria-label="Trip dates"
minValue={now}
isDateUnavailable={isDateUnavailable}
defaultValue={{start: now.add({days: 5}), end: now.add({days: 8})}} />
</div>
);
}
Loading