Skip to content
Merged
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
9 changes: 6 additions & 3 deletions packages/@react-aria/grid/src/useGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ export interface GridProps extends DOMProps, AriaLabelingProps {
* trigger selection clearing contextually.
* @default 'clearSelection'
*/
escapeKeyBehavior?: 'clearSelection' | 'none'
escapeKeyBehavior?: 'clearSelection' | 'none',
/** Whether selection should occur on press up instead of press down. */
shouldSelectOnPressUp?: boolean
}

export interface GridAria {
Expand All @@ -87,7 +89,8 @@ export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection<
getRowText,
onRowAction,
onCellAction,
escapeKeyBehavior = 'clearSelection'
escapeKeyBehavior = 'clearSelection',
shouldSelectOnPressUp
} = props;
let {selectionManager: manager} = state;

Expand Down Expand Up @@ -121,7 +124,7 @@ export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection<
});

let id = useId(props.id);
gridMap.set(state, {keyboardDelegate: delegate, actions: {onRowAction, onCellAction}});
gridMap.set(state, {keyboardDelegate: delegate, actions: {onRowAction, onCellAction}, shouldSelectOnPressUp});

let descriptionProps = useHighlightSelectionDescription({
selectionManager: manager,
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/grid/src/useGridRow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ export function useGridRow<T, C extends GridCollection<T>, S extends GridState<T
onAction
} = props;

let {actions} = gridMap.get(state)!;
let {actions, shouldSelectOnPressUp: gridShouldSelectOnPressUp} = gridMap.get(state)!;
let onRowAction = actions.onRowAction ? () => actions.onRowAction?.(node.key) : onAction;
let {itemProps, ...states} = useSelectableItem({
selectionManager: state.selectionManager,
key: node.key,
ref,
isVirtualized,
shouldSelectOnPressUp,
shouldSelectOnPressUp: gridShouldSelectOnPressUp || shouldSelectOnPressUp,
onAction: onRowAction || node?.props?.onAction ? chain(node?.props?.onAction, onRowAction) : undefined,
isDisabled: state.collection.size === 0
});
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-aria/grid/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ interface GridMapShared {
actions: {
onRowAction?: (key: Key) => void,
onCellAction?: (key: Key) => void
}
},
shouldSelectOnPressUp?: boolean
}

// Used to share:
Expand Down
9 changes: 6 additions & 3 deletions packages/@react-aria/gridlist/src/useGridList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export interface GridListProps<T> extends CollectionBase<T>, MultipleSelection {
*/
onAction?: (key: Key) => void,
/** Whether `disabledKeys` applies to all interactions, or only selection. */
disabledBehavior?: DisabledBehavior
disabledBehavior?: DisabledBehavior,
/** Whether selection should occur on press up instead of press down. */
shouldSelectOnPressUp?: boolean
}

export interface AriaGridListProps<T> extends GridListProps<T>, DOMProps, AriaLabelingProps {
Expand Down Expand Up @@ -115,7 +117,8 @@ export function useGridList<T>(props: AriaGridListOptions<T>, state: ListState<T
disallowTypeAhead,
linkBehavior = 'action',
keyboardNavigationBehavior = 'arrow',
escapeKeyBehavior = 'clearSelection'
escapeKeyBehavior = 'clearSelection',
shouldSelectOnPressUp
} = props;

if (!props['aria-label'] && !props['aria-labelledby']) {
Expand All @@ -139,7 +142,7 @@ export function useGridList<T>(props: AriaGridListOptions<T>, state: ListState<T
});

let id = useId(props.id);
listMap.set(state, {id, onAction, linkBehavior, keyboardNavigationBehavior});
listMap.set(state, {id, onAction, linkBehavior, keyboardNavigationBehavior, shouldSelectOnPressUp});

let descriptionProps = useHighlightSelectionDescription({
selectionManager: state.selectionManager,
Expand Down
7 changes: 3 additions & 4 deletions packages/@react-aria/gridlist/src/useGridListItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,12 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
// Copied from useGridCell + some modifications to make it not so grid specific
let {
node,
isVirtualized,
shouldSelectOnPressUp
isVirtualized
} = props;

// let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/gridlist');
let {direction} = useLocale();
let {onAction, linkBehavior, keyboardNavigationBehavior} = listMap.get(state)!;
let {onAction, linkBehavior, keyboardNavigationBehavior, shouldSelectOnPressUp} = listMap.get(state)!;
let descriptionId = useSlotId();

// We need to track the key of the item at the time it was last focused so that we force
Expand Down Expand Up @@ -125,7 +124,7 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
key: node.key,
ref,
isVirtualized,
shouldSelectOnPressUp,
shouldSelectOnPressUp: props.shouldSelectOnPressUp || shouldSelectOnPressUp,
onAction: onAction || node.props?.onAction ? chain(node.props?.onAction, onAction ? () => onAction(node.key) : undefined) : undefined,
focus,
linkBehavior
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-aria/gridlist/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ interface ListMapShared {
id: string,
onAction?: (key: Key) => void,
linkBehavior?: 'action' | 'selection' | 'override',
keyboardNavigationBehavior: 'arrow' | 'tab'
keyboardNavigationBehavior: 'arrow' | 'tab',
shouldSelectOnPressUp?: boolean
}

// Used to share:
Expand Down
3 changes: 0 additions & 3 deletions packages/@react-aria/listbox/src/useListBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ export interface AriaListBoxOptions<T> extends Omit<AriaListBoxProps<T>, 'childr
*/
shouldUseVirtualFocus?: boolean,

/** Whether selection should occur on press up instead of press down. */
shouldSelectOnPressUp?: boolean,

/** Whether options should be focused when the user hovers over them. */
shouldFocusOnHover?: boolean,

Expand Down
2 changes: 2 additions & 0 deletions packages/@react-aria/tag/src/useTagGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export interface TagGroupAria {
export interface AriaTagGroupProps<T> extends CollectionBase<T>, MultipleSelection, DOMProps, LabelableProps, AriaLabelingProps, Omit<HelpTextProps, 'errorMessage'> {
/** How multiple selection should behave in the collection. */
selectionBehavior?: SelectionBehavior,
/** Whether selection should occur on press up instead of press down. */
shouldSelectOnPressUp?: boolean,
/** Handler that is called when a user deletes a tag. */
onRemove?: (keys: Set<Key>) => void,
/** An error message for the field. */
Expand Down
2 changes: 2 additions & 0 deletions packages/@react-types/listbox/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export interface AriaListBoxProps<T> extends AriaListBoxPropsBase<T> {
label?: ReactNode,
/** How multiple selection should behave in the collection. */
selectionBehavior?: SelectionBehavior,
/** Whether selection should occur on press up instead of press down. */
shouldSelectOnPressUp?: boolean,
/**
* Handler that is called when a user performs an action on an item. The exact user event depends on
* the collection's `selectionBehavior` prop and the interaction modality.
Expand Down
4 changes: 3 additions & 1 deletion packages/@react-types/table/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export interface TableProps<T> extends MultipleSelection, Sortable {
* trigger selection clearing contextually.
* @default 'clearSelection'
*/
escapeKeyBehavior?: 'clearSelection' | 'none'
escapeKeyBehavior?: 'clearSelection' | 'none',
/** Whether selection should occur on press up instead of press down. */
shouldSelectOnPressUp?: boolean
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/react-aria-components/src/GridList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ function GridListInner<T extends object>({props, collection, gridListRef: ref}:
keyboardDelegate,
// Only tab navigation is supported in grid layout.
keyboardNavigationBehavior: layout === 'grid' ? 'tab' : keyboardNavigationBehavior,
isVirtualized
isVirtualized,
shouldSelectOnPressUp: props.shouldSelectOnPressUp
}, state, ref);

let selectionManager = state.selectionManager;
Expand Down
129 changes: 125 additions & 4 deletions packages/react-aria-components/stories/GridList.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,34 @@
*/

import {action} from '@storybook/addon-actions';
import {Button, Checkbox, CheckboxProps, DropIndicator, GridLayout, GridList, GridListItem, GridListItemProps, ListLayout, Size, Tag, TagGroup, TagList, useDragAndDrop, Virtualizer} from 'react-aria-components';
import {
Button,
Checkbox,
CheckboxProps,
Dialog,
DialogTrigger,
DropIndicator,
GridLayout,
GridList,
GridListItem,
GridListItemProps,
Heading,
ListLayout,
Modal,
ModalOverlay,
Popover,
Size,
Tag,
TagGroup,
TagList,
useDragAndDrop,
Virtualizer
} from 'react-aria-components';
import {classNames} from '@react-spectrum/utils';
import React from 'react';
import {Key, useListData} from 'react-stately';
import React, {useState} from 'react';
import styles from '../example/index.css';
import {useListData} from 'react-stately';

export default {
title: 'React Aria Components'
};
Expand Down Expand Up @@ -66,7 +89,8 @@ const MyGridListItem = (props: GridListItemProps) => {
GridListExample.story = {
args: {
layout: 'stack',
escapeKeyBehavior: 'clearSelection'
escapeKeyBehavior: 'clearSelection',
shouldSelectOnPressUp: false
},
argTypes: {
layout: {
Expand Down Expand Up @@ -218,3 +242,100 @@ export function TagGroupInsideGridList() {
</GridList>
);
}

const GridListDropdown = () => {
const [isOpen, setIsOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState<Set<Key>>(new Set([]));

const handleSelectionChange = (e) => {
setSelectedItem(e);
setIsOpen(false);
};

return (
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
<Button>Open GridList Options</Button>
<Popover>
<div>
<GridList
className={styles.menu}
selectedKeys={selectedItem}
aria-label="Favorite pokemon"
selectionMode="single"
onSelectionChange={handleSelectionChange}
shouldSelectOnPressUp
autoFocus>
<MyGridListItem textValue="Charizard">
Option 1 <Button>A</Button>
</MyGridListItem>
<MyGridListItem textValue="Blastoise">
Option 2 <Button>B</Button>
</MyGridListItem>
<MyGridListItem textValue="Venusaur">
Option 3 <Button>C</Button>
</MyGridListItem>
<MyGridListItem textValue="Pikachu">
Option 4 <Button>D</Button>
</MyGridListItem>
</GridList>
</div>
</Popover>
</DialogTrigger>
);
};

function GridListInModalPickerRender(props) {
const [mainModalOpen, setMainModalOpen] = useState(true);
return (
<>
<Button onPress={() => setMainModalOpen(true)}>
Open Modal
</Button>
<ModalOverlay
{...props}
isOpen={mainModalOpen}
onOpenChange={setMainModalOpen}
isDismissable
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'rgba(0,0,0,0.5)'
}}>
<Modal>
<Dialog>
<div
style={{
display: 'flex',
flexDirection: 'column',
padding: 8,
background: '#ccc',
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%,-50%)',
width: 'max-content',
height: 'max-content'
}}>
<Heading slot="title">Open the GridList Picker</Heading>
<GridListDropdown />
</div>
</Dialog>
</Modal>
</ModalOverlay>
</>
);
}

export const GridListInModalPicker = {
render: (args) => <GridListInModalPickerRender {...args} />,
parameters: {
docs: {
description: {
component: 'Selecting an option from the grid list over the backdrop should not result in the modal closing.'
}
}
}
};
38 changes: 38 additions & 0 deletions packages/react-aria-components/test/GridList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -820,4 +820,42 @@ describe('GridList', () => {
});
});
});

describe('shouldSelectOnPressUp', () => {
it('should select an item on pressing down when shouldSelectOnPressUp is not provided', async () => {
let onSelectionChange = jest.fn();
let {getAllByRole} = renderGridList({selectionMode: 'single', onSelectionChange});
let items = getAllByRole('row');

await user.pointer({target: items[0], keys: '[MouseLeft>]'});
expect(onSelectionChange).toBeCalledTimes(1);

await user.pointer({target: items[0], keys: '[/MouseLeft]'});
expect(onSelectionChange).toBeCalledTimes(1);
});

it('should select an item on pressing down when shouldSelectOnPressUp is false', async () => {
let onSelectionChange = jest.fn();
let {getAllByRole} = renderGridList({selectionMode: 'single', onSelectionChange, shouldSelectOnPressUp: false});
let items = getAllByRole('row');

await user.pointer({target: items[0], keys: '[MouseLeft>]'});
expect(onSelectionChange).toBeCalledTimes(1);

await user.pointer({target: items[0], keys: '[/MouseLeft]'});
expect(onSelectionChange).toBeCalledTimes(1);
});

it('should select an item on pressing up when shouldSelectOnPressUp is true', async () => {
let onSelectionChange = jest.fn();
let {getAllByRole} = renderGridList({selectionMode: 'single', onSelectionChange, shouldSelectOnPressUp: true});
let items = getAllByRole('row');

await user.pointer({target: items[0], keys: '[MouseLeft>]'});
expect(onSelectionChange).toBeCalledTimes(0);

await user.pointer({target: items[0], keys: '[/MouseLeft]'});
expect(onSelectionChange).toBeCalledTimes(1);
});
});
});
Loading