From d0cc0a16c785eaecd6b9e21b7b9e17e30871de01 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 22 Aug 2022 18:51:25 -0700 Subject: [PATCH 1/2] useGridList docs refinements --- .../@react-aria/gridlist/docs/anatomy.svg | 24 +- .../@react-aria/gridlist/docs/useGridList.mdx | 294 ++++++++++++------ .../selection/src/useSelectableItem.ts | 2 +- .../selection/src/useSelectableList.ts | 5 +- .../@react-spectrum/list/docs/anatomy.svg | 12 +- 5 files changed, 217 insertions(+), 120 deletions(-) diff --git a/packages/@react-aria/gridlist/docs/anatomy.svg b/packages/@react-aria/gridlist/docs/anatomy.svg index 0d7693b8e5b..cd889dc2816 100644 --- a/packages/@react-aria/gridlist/docs/anatomy.svg +++ b/packages/@react-aria/gridlist/docs/anatomy.svg @@ -20,7 +20,7 @@ - + @@ -32,11 +32,11 @@ List item List item List item - - - + + + - + @@ -46,7 +46,7 @@ - + @@ -56,16 +56,16 @@ - + - + - + @@ -73,7 +73,7 @@ - + @@ -87,14 +87,14 @@ List item row - + List item grid cell - + - {[...collection].map((item) => ( +
    + {[...state.collection].map((item) => ( ))}
@@ -157,54 +145,20 @@ function List(props) { function ListItem({ item, state }) { let ref = React.useRef(); - - let { - rowProps, - gridCellProps, - isPressed, - isSelected, - isFocused, - isDisabled - } = useGridListItem( - { - node: item - }, + let {rowProps, gridCellProps, isPressed} = useGridListItem( + {node: item}, state, ref ); + let {isFocusVisible, focusProps} = useFocusRing(); let showCheckbox = state.selectionManager.selectionMode !== 'none' && state.selectionManager.selectionBehavior === 'toggle'; - let backgroundColor; - let color = "black"; - - if (isSelected) { - backgroundColor = "blueviolet"; - color = "white"; - } else if (isFocused) { - backgroundColor = "gray"; - } else if (isDisabled) { - backgroundColor = "transparent"; - color = "gray"; - } - return (
  • + className={`${isPressed ? 'pressed' : ''} ${isFocusVisible ? 'focus-visible' : ''}`}>
    {showCheckbox && } {item.rendered} @@ -212,28 +166,109 @@ function ListItem({ item, state }) {
  • ); } +``` + +Now we can render a basic example list, with multiple selection and interactive children in each item. + +```tsx example +import {Item} from "@react-stately/collections"; + +// Reuse the Button from your component library. See below. +import {Button} from 'your-component-library'; - + Charizard - Blastoise - Venusaur - Pikachu + + Blastoise + + + + Venusaur + + + + Pikachu + + ``` +
    + Show CSS + +```css +.list { + padding: 0; + list-style: none; + background: var(--page-background); + border: 1px solid var(--spectrum-global-color-gray-400); + max-width: 400px; + min-width: 200px; + max-height: 250px; + overflow: auto; +} + +.list li { + padding: 8px; + outline: none; + cursor: default; +} + +.list li:nth-child(2n) { + background: var(--spectrum-alias-highlight-hover); +} + +.list li.pressed { + background: var(--spectrum-global-color-gray-200); +} + +.list li[aria-selected=true] { + background: slateblue; + color: white; +} + +.list li.focus-visible { + outline: 2px solid slateblue; + outline-offset: -3px; +} + +.list li.focus-visible[aria-selected=true] { + outline-color: white; +} + +.list li[aria-disabled] { + opacity: 0.4; +} + +.list [role=gridcell] { + display: flex; + align-items: center; + gap: 4px; +} + +.list li button { + margin-left: auto; +} + +.list li input[type=checkbox] { + accent-color: white; +} +``` + +
    + ### Adding selection checkboxes -Next, let's add support for selection checkboxes. For multiple selection, we'll want to add a checkbox to the row to allow the user to select rows. +Next, let's add support for selection checkboxes to allow the user to select items explicitly. This is done using the hook. It is passed the `key` of the item it is contained within. When the user checks or unchecks the checkbox, the row will be added or removed from the List's selection. -In this example, we pass the result of the `checkboxProps` into our checkbox defined below. -It's likely you'll have a `Checkbox` component in your component library that uses these hooks already. -See below for an example. +The `Checkbox` component used in this example is independent and can be used separately from `useGridList`. The code is available below. + ```tsx example export=true render=false import {useGridListSelectionCheckbox} from '@react-aria/gridlist'; @@ -242,23 +277,30 @@ import {Checkbox} from 'your-component-library'; function ListCheckbox({ item, state }) { let { checkboxProps } = useGridListSelectionCheckbox({ key: item.key }, state); - return ; } ``` -The following example shows how to enable multiple selection support using the List component we built above. -It's as simple as setting the `selectionMode` prop to `"multiple"`. +The following example shows an example list with multiple selection using checkboxes and the default `toggle` [selection behavior](#selection-behavior). ```tsx example - + Charizard - Blastoise - Venusaur - Pikachu + + Blastoise + + + + Venusaur + + + + Pikachu + + ``` @@ -303,7 +345,7 @@ import {useButton} from '@react-aria/button'; function Button(props) { let ref = React.useRef(); let {buttonProps} = useButton(props, ref); - return ; + return ; } ``` @@ -314,7 +356,7 @@ function Button(props) { ### Dynamic collections So far, our examples have shown static collections, where the data is hard coded. -Dynamic collections, as shown below, can be used when the List data comes from an external data source such as an API, or updates over time. +Dynamic collections, as shown below, can be used when the data comes from an external data source such as an API, or updates over time. In the example below, the rows are provided to the List via a render function. ```tsx example export=true @@ -328,7 +370,14 @@ function ExampleList(props) { return ( - {item => {item.name}} + {item => ( + + {item.name} + + + )} ); } @@ -352,17 +401,15 @@ A user can click on a different row to change the selection, or click on the sam Multiple selection can be enabled by setting `selectionMode` to `multiple`. ```tsx example -// Using the example above ``` ### Disallow empty selection -List also supports a `disallowEmptySelection` prop which forces the user to have at least one row in the List selected at all times. +`useGridList` also supports a `disallowEmptySelection` prop which forces the user to have at least one row in the List selected at all times. In this mode, if a single row is selected and the user presses it, it will not be deselected. ```tsx example -// Using the example above ``` @@ -392,14 +439,21 @@ function PokemonList(props) { ### Disabled rows -You can disable specific rows by providing an array of keys to `useListState` via the `disabledKeys` prop. This will prevent rows from being selectable as shown in the example below. +You can disable specific rows by providing an array of keys to `useListState` via the `disabledKeys` prop. This will disable all interactions on disabled rows, +unless the `disabledBehavior` prop is used to change this behavior. Note that you are responsible for the styling of disabled rows, however, the selection checkbox will be automatically disabled. ```tsx example -// Using the same List as above +// Using the example above ``` +When `disabledBehavior` is set to `selection`, interactions such as focus, dragging, or actions can still be performed on disabled rows. + +```tsx example + +``` + ### Selection behavior By default, `useGridList` uses the `"toggle"` selection behavior, which behaves like a checkbox group: clicking, tapping, or pressing the Space or Enter keys toggles selection for the focused row. Using the arrow keys moves focus but does not change selection. The `"toggle"` selection mode is often paired with a checkbox in each row as an explicit affordance for selection. @@ -421,8 +475,48 @@ be triggered via the Enter key, and selection using the alert(`Opening item ${key}...`)} /> - alert(`Opening item ${key}...`)} /> +
    + alert(`Opening item ${key}...`)} /> + alert(`Opening item ${key}...`)} /> +
    +``` + +### Asynchronous loading + +This example uses the [useAsyncList](../react-stately/useAsyncList.html) hook to handle loadiasynchronous loading of data from a server. You may additionally want to display a spinner to indicate the loading state to the user, or support features like infinite scroll to load more data. + +```tsx example +import {useAsyncList} from '@react-stately/data'; + +function AsyncList() { + + let list = useAsyncList({ + async load({signal, cursor}) { + if (cursor) { + cursor = cursor.replace(/^http:\/\//i, 'https://'); + } + + let res = await fetch(cursor || `https://swapi.py4e.com/api/people/?search=`, {signal}); + let json = await res.json(); + + return { + items: json.results, + cursor: json.next + }; + } + }); + + return ( + + {(item) => ( + {item.name} + )} + + ); +} ``` ## Internationalization diff --git a/packages/@react-aria/selection/src/useSelectableItem.ts b/packages/@react-aria/selection/src/useSelectableItem.ts index 7dcd9faa885..84a29d5d329 100644 --- a/packages/@react-aria/selection/src/useSelectableItem.ts +++ b/packages/@react-aria/selection/src/useSelectableItem.ts @@ -78,7 +78,7 @@ export interface SelectableItemStates { allowsSelection: boolean, /** * Whether the item has an action, dependent on `onAction`, `disabledKeys`, - * and `disabledBehavior. It may also change depending on the current selection state + * and `disabledBehavior`. It may also change depending on the current selection state * of the list (e.g. when selection is primary). This can be used to enable or disable hover * styles or other visual indications of interactivity. */ diff --git a/packages/@react-aria/selection/src/useSelectableList.ts b/packages/@react-aria/selection/src/useSelectableList.ts index af7a2e4687e..0c51a578f88 100644 --- a/packages/@react-aria/selection/src/useSelectableList.ts +++ b/packages/@react-aria/selection/src/useSelectableList.ts @@ -107,7 +107,10 @@ export function useSelectableList(props: AriaSelectableListOptions): SelectableL // By default, a KeyboardDelegate is provided which uses the DOM to query layout information (e.g. for page up/page down). // When virtualized, the layout object will be passed in as a prop and override this. let collator = useCollator({usage: 'search', sensitivity: 'base'}); - let delegate = useMemo(() => keyboardDelegate || new ListKeyboardDelegate(collection, disabledKeys, ref, collator), [keyboardDelegate, collection, disabledKeys, ref, collator]); + let disabledBehavior = selectionManager.disabledBehavior; + let delegate = useMemo(() => ( + keyboardDelegate || new ListKeyboardDelegate(collection, disabledBehavior === 'selection' ? new Set() : disabledKeys, ref, collator) + ), [keyboardDelegate, collection, disabledKeys, ref, collator, disabledBehavior]); let {collectionProps} = useSelectableCollection({ ref, diff --git a/packages/@react-spectrum/list/docs/anatomy.svg b/packages/@react-spectrum/list/docs/anatomy.svg index 763db89a1d5..0cac6818db2 100644 --- a/packages/@react-spectrum/list/docs/anatomy.svg +++ b/packages/@react-spectrum/list/docs/anatomy.svg @@ -11,11 +11,11 @@ - + - + @@ -65,18 +65,18 @@ - + - + Label area Description area (optional) - + @@ -88,7 +88,7 @@ - + From 88c2c71892a8fe73c2bba606deecc2b8c117b5f6 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 22 Aug 2022 19:26:35 -0700 Subject: [PATCH 2/2] workaround iOS bug --- packages/@react-aria/gridlist/docs/useGridList.mdx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/gridlist/docs/useGridList.mdx b/packages/@react-aria/gridlist/docs/useGridList.mdx index 7b7a8f07fb4..5389fa30fdd 100644 --- a/packages/@react-aria/gridlist/docs/useGridList.mdx +++ b/packages/@react-aria/gridlist/docs/useGridList.mdx @@ -253,8 +253,11 @@ import {Button} from 'your-component-library'; margin-left: auto; } -.list li input[type=checkbox] { - accent-color: white; +/* iOS Safari has a bug that prevents accent-color: white from working. */ +@supports not (-webkit-touch-callout: none) { + .list li input[type=checkbox] { + accent-color: white; + } } ```