diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_Archive_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_Archive_20_N.svg index 220983c6f4a..3208c615a14 100644 --- a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_Archive_20_N.svg +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_Archive_20_N.svg @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ArrowCurved_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ArrowCurved_20_N.svg new file mode 100644 index 00000000000..25626294eb8 --- /dev/null +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ArrowCurved_20_N.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ArrowUpSend_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ArrowUpSend_20_N.svg new file mode 100644 index 00000000000..cea7a7c6999 --- /dev/null +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ArrowUpSend_20_N.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_BookmarkSingleFilled_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_BookmarkSingleFilled_20_N.svg new file mode 100644 index 00000000000..2fbe727664c --- /dev/null +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_BookmarkSingleFilled_20_N.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ClockPending_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ClockPending_20_N.svg index 58f59b11014..03041a0395a 100644 --- a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ClockPending_20_N.svg +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ClockPending_20_N.svg @@ -1,5 +1,12 @@ - - + + + + + + + + + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_HeartFilled_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_HeartFilled_20_N.svg index c08a1a19d2a..3785c307e73 100644 --- a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_HeartFilled_20_N.svg +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_HeartFilled_20_N.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_PremiumIcon_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_PremiumIcon_20_N.svg new file mode 100644 index 00000000000..30a8b79f4cc --- /dev/null +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_PremiumIcon_20_N.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_SpeedFast_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_SpeedFast_20_N.svg index 5dc0d35b2d4..c23f7c4783c 100644 --- a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_SpeedFast_20_N.svg +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_SpeedFast_20_N.svg @@ -1,5 +1,5 @@ - + diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_StopProcessing_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_StopProcessing_20_N.svg new file mode 100644 index 00000000000..d644a842f66 --- /dev/null +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_StopProcessing_20_N.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagBold_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagBold_20_N.svg index 85c7363213c..33d1e4fa926 100644 --- a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagBold_20_N.svg +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagBold_20_N.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagItalic_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagItalic_20_N.svg index 298f4f44520..2fd9fc4ab4a 100644 --- a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagItalic_20_N.svg +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagItalic_20_N.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagStrikeThrough_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagStrikeThrough_20_N.svg index a66eacb1bc9..8a38e54bdff 100644 --- a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagStrikeThrough_20_N.svg +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagStrikeThrough_20_N.svg @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagUnderline_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagUnderline_20_N.svg index ee09ce006e2..da1f35dd7d4 100644 --- a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagUnderline_20_N.svg +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_TagUnderline_20_N.svg @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_Wallet_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_Wallet_20_N.svg index c9d9cc6b7c4..3ab67d70927 100644 --- a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_Wallet_20_N.svg +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_Wallet_20_N.svg @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ZoomFitToHeight_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ZoomFitToHeight_20_N.svg new file mode 100644 index 00000000000..4c6defe3814 --- /dev/null +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ZoomFitToHeight_20_N.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ZoomFitToScreen_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ZoomFitToScreen_20_N.svg new file mode 100644 index 00000000000..53d3d632616 --- /dev/null +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ZoomFitToScreen_20_N.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ZoomFitToWidth_20_N.svg b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ZoomFitToWidth_20_N.svg new file mode 100644 index 00000000000..c9fdffdaefe --- /dev/null +++ b/packages/@react-spectrum/s2/s2wf-icons/S2_Icon_ZoomFitToWidth_20_N.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/@react-stately/combobox/src/useComboBoxState.ts b/packages/@react-stately/combobox/src/useComboBoxState.ts index d983edec580..fbe66090ac5 100644 --- a/packages/@react-stately/combobox/src/useComboBoxState.ts +++ b/packages/@react-stately/combobox/src/useComboBoxState.ts @@ -10,8 +10,8 @@ * governing permissions and limitations under the License. */ +import {ChangeValueType, ComboBoxProps, MenuTriggerAction, SelectionMode, ValueType} from '@react-types/combobox'; import {Collection, CollectionStateBase, FocusStrategy, Key, Node, Selection} from '@react-types/shared'; -import {ComboBoxProps, MenuTriggerAction, SelectionMode, ValueType} from '@react-types/combobox'; import {FormValidationState, useFormValidationState} from '@react-stately/form'; import {getChildNodes} from '@react-stately/collections'; import {ListCollection, ListState, useListState} from '@react-stately/list'; @@ -373,7 +373,7 @@ export function useComboBoxState = M extends 'single' ? Key | null : Key[]; +export type ValueType = M extends 'single' ? Key | null : readonly Key[]; +export type ChangeValueType = M extends 'single' ? Key | null : Key[]; type ValidationType = M extends 'single' ? Key : Key[]; export interface ComboBoxValidationValue { @@ -50,7 +51,7 @@ export interface ComboBoxValidationValue { inputValue: string } -export interface ComboBoxProps extends CollectionBase, InputBase, ValueBase>, TextInputBase, Validation, FocusableProps, LabelableProps, HelpTextProps { +export interface ComboBoxProps extends CollectionBase, InputBase, ValueBase, ChangeValueType>, TextInputBase, Validation, FocusableProps, LabelableProps, HelpTextProps { /** The list of ComboBox items (uncontrolled). */ defaultItems?: Iterable, /** The list of ComboBox items (controlled). */ diff --git a/packages/dev/s2-docs/src/iconAliases.js b/packages/dev/s2-docs/src/iconAliases.js index da810ff1930..005810eb101 100644 --- a/packages/dev/s2-docs/src/iconAliases.js +++ b/packages/dev/s2-docs/src/iconAliases.js @@ -3,6 +3,7 @@ export const iconAliases = { 'add': ['New', 'ProjectCreate'], 'again': ['Redo'], 'annotation': ['MusicNote', 'StickyNote'], + 'approve': ['BadgeVerified', 'CheckmarkCircle'], 'arrow': ['ChevronDoubleLeft', 'ChevronDoubleRight', 'ChevronDown', 'ChevronLeft', 'ChevronRight', 'ChevronUp'], 'attention': ['AlertDiamond', 'AlertTriangle'], 'audio': ['MusicNote', 'VolumeOff', 'VolumeOne', 'VolumeTwo'], @@ -23,6 +24,7 @@ export const iconAliases = { 'copy': ['Duplicate'], 'counterclockwise': ['RotateCCW'], 'create': ['Add', 'AddCircle', 'AddContent', 'CalendarAdd', 'DataAdd', 'FileAdd', 'FolderAdd', 'ImageAdd', 'New', 'ProjectAddInto', 'TextAdd', 'UserAdd'], + 'crown': ['PremiumIcon'], 'date': ['Calendar', 'CalendarAdd', 'CalendarDay', 'CalendarEdit', 'CalendarWeek'], 'decrease': ['ChevronDown', 'OrderOneDown', 'SortDown', 'ThumbDown'], 'delete': ['CommentRemove', 'ImageBackgroundRemove', 'RemoveCircle'], @@ -86,12 +88,13 @@ export const iconAliases = { 'plus': ['Add', 'AddCircle', 'AddContent', 'CalendarAdd', 'DataAdd', 'FileAdd', 'FolderAdd', 'ImageAdd', 'New', 'ProjectAddInto', 'TextAdd', 'UserAdd'], 'profile': ['FileUser', 'User', 'UserAdd', 'UserAvatar', 'UserAvatarCursor', 'UserEdit', 'UserFollowing', 'UserGroup', 'UserLock', 'UserSettings'], 'program': ['Code'], + 'pro': ['PremiumIcon'], 'raise': ['ChevronUp', 'OrderOneUp', 'SortUp', 'TextIncrease', 'ThumbUp'], 'reload': ['DataRefresh', 'Refresh'], 'remove': ['Delete'], 'repeat': ['Redo'], 'reverse': ['Undo'], - 'save': ['Download'], + 'save': ['Download', 'Bookmark', 'BookmarkSingleFilled'], 'schedule': ['Calendar', 'CalendarAdd', 'CalendarDay', 'CalendarEdit', 'CalendarWeek'], 'script': ['Code'], 'search': ['FindAndReplace'], @@ -110,6 +113,7 @@ export const iconAliases = { 'trash': ['Delete'], 'unlock': ['FolderOpen', 'LockOpen', 'OpenIn'], 'up': ['TextIncrease'], + 'verified': ['CheckmarkCircle'], 'versus': ['Compare'], 'vertical': ['ChartBarVert'], 'video': ['MovieCamera'], diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index b98faeaa957..15af9e858c7 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -96,7 +96,12 @@ export interface GridListProps extends Omit, 'children'> * Whether the items are arranged in a stack or grid. * @default 'stack' */ - layout?: 'stack' | 'grid' + layout?: 'stack' | 'grid', + /** + * The primary orientation of the items. Usually this is the direction that the collection scrolls. + * @default 'vertical' + */ + orientation?: 'horizontal' | 'vertical' } @@ -127,7 +132,7 @@ function GridListInner({props, collection, gridListRef: ref}: [props, ref] = useContextProps(props, ref, SelectableCollectionContext); // eslint-disable-next-line @typescript-eslint/no-unused-vars let {shouldUseVirtualFocus, filter, disallowTypeAhead, ...DOMCollectionProps} = props; - let {dragAndDropHooks, keyboardNavigationBehavior = 'arrow', layout = 'stack'} = props; + let {dragAndDropHooks, keyboardNavigationBehavior = 'arrow', layout = 'stack', orientation = 'vertical'} = props; let {CollectionRoot, isVirtualized, layoutDelegate, dropTargetDelegate: ctxDropTargetDelegate} = useContext(CollectionRendererContext); let gridlistState = useListState({ ...DOMCollectionProps, @@ -149,9 +154,10 @@ function GridListInner({props, collection, gridListRef: ref}: disabledBehavior, layoutDelegate, layout, + orientation, direction }) - ), [filteredState.collection, ref, layout, disabledKeys, disabledBehavior, layoutDelegate, collator, direction]); + ), [filteredState.collection, ref, layout, orientation, disabledKeys, disabledBehavior, layoutDelegate, collator, direction]); let {gridProps} = useGridList({ ...DOMCollectionProps, @@ -211,9 +217,11 @@ function GridListInner({props, collection, gridListRef: ref}: collection: filteredState.collection, disabledKeys: selectionManager.disabledKeys, disabledBehavior: selectionManager.disabledBehavior, - ref + ref, + orientation, + direction }); - let dropTargetDelegate = dragAndDropHooks.dropTargetDelegate || ctxDropTargetDelegate || new dragAndDropHooks.ListDropTargetDelegate(collection, ref, {layout, direction}); + let dropTargetDelegate = dragAndDropHooks.dropTargetDelegate || ctxDropTargetDelegate || new dragAndDropHooks.ListDropTargetDelegate(collection, ref, {layout, direction, orientation}); droppableCollection = dragAndDropHooks.useDroppableCollection!({ keyboardDelegate, dropTargetDelegate diff --git a/packages/react-aria-components/stories/GridList.stories.tsx b/packages/react-aria-components/stories/GridList.stories.tsx index 75addc2c794..49b042f9db6 100644 --- a/packages/react-aria-components/stories/GridList.stories.tsx +++ b/packages/react-aria-components/stories/GridList.stories.tsx @@ -59,29 +59,32 @@ export default { export type GridListStory = StoryFn; -export const GridListExample: GridListStory = (args) => ( - - 1,1 - 1,2 - 1,3 - 2,1 - 2,2 - 2,3 - 3,1 - 3,2 - 3,3 - -); +export const GridListExample: GridListStory = (args) => { + let isHorizontalStack = args.orientation === 'horizontal' && args.layout !== 'grid'; + return ( + + 1,1 + 1,2 + 1,3 + 2,1 + 2,2 + 2,3 + 3,1 + 3,2 + 3,3 + + ); +}; export const MyGridListItem = (props: GridListItemProps) => { return ( @@ -105,6 +108,7 @@ export const MyGridListItem = (props: GridListItemProps) => { GridListExample.story = { args: { layout: 'stack', + orientation: 'vertical', escapeKeyBehavior: 'clearSelection', shouldSelectOnPressUp: false, disallowTypeAhead: false @@ -114,6 +118,10 @@ GridListExample.story = { control: 'radio', options: ['stack', 'grid'] }, + orientation: { + control: 'radio', + options: ['vertical', 'horizontal'] + }, keyboardNavigationBehavior: { control: 'radio', options: ['arrow', 'tab'] @@ -133,6 +141,61 @@ GridListExample.story = { } }; +const DraggableGridListRender = (args: GridListProps) => { + let list = useListData({ + initialItems: [ + {id: '1', name: 'Item 1'}, + {id: '2', name: 'Item 2'}, + {id: '3', name: 'Item 3'}, + {id: '4', name: 'Item 4'}, + {id: '5', name: 'Item 5'} + ] + }); + + let {dragAndDropHooks} = useDragAndDrop({ + getItems: (keys) => [...keys].map(key => ({'text/plain': list.getItem(key)?.name ?? ''})), + onReorder(e) { + if (e.target.dropPosition === 'before') { + list.moveBefore(e.target.key, e.keys); + } else if (e.target.dropPosition === 'after') { + list.moveAfter(e.target.key, e.keys); + } + } + }); + + let isHorizontal = args.orientation === 'horizontal'; + + return ( + + {item => {item.name}} + + ); +}; + +export const DraggableGridListExample: StoryObj = { + render: (args) => , + args: { + orientation: 'vertical' + }, + argTypes: { + orientation: { + control: 'radio', + options: ['vertical', 'horizontal'] + } + } +}; + const MyCheckbox = ({children, ...props}: CheckboxProps) => { return ( diff --git a/packages/react-aria-components/test/GridList.test.js b/packages/react-aria-components/test/GridList.test.js index 515536d0e90..c9cde711fd5 100644 --- a/packages/react-aria-components/test/GridList.test.js +++ b/packages/react-aria-components/test/GridList.test.js @@ -64,14 +64,14 @@ let TestGridListSections = ({listBoxProps, itemProps}) => ( ); -let DraggableGridList = (props) => { +let DraggableGridList = ({orientation, ...props}) => { let {dragAndDropHooks} = useDragAndDrop({ getItems: (keys) => [...keys].map((key) => ({'text/plain': key})), ...props }); return ( - + Cat Dog Kangaroo @@ -427,6 +427,64 @@ describe('GridList', () => { expect(document.activeElement).toBe(document.body); }); + it('should support horizontal orientation', async () => { + let {getAllByRole} = render( + + Cat + Dog + Kangaroo + + ); + + let items = getAllByRole('row'); + + await user.tab(); + expect(document.activeElement).toBe(items[0]); + + await user.keyboard('{ArrowDown}'); + expect(document.activeElement).toBe(items[1]); + + await user.keyboard('{ArrowDown}'); + expect(document.activeElement).toBe(items[2]); + + await user.keyboard('{ArrowUp}'); + expect(document.activeElement).toBe(items[1]); + + await user.keyboard('{ArrowUp}'); + expect(document.activeElement).toBe(items[0]); + }); + + it('should support horizontal orientation with grid layout', async () => { + let buttonRef = React.createRef(); + let {getAllByRole} = render( + + Cat + Dog + Kangaroo + + ); + + let items = getAllByRole('row'); + + await user.tab(); + expect(document.activeElement).toBe(items[0]); + + await user.keyboard('{ArrowDown}'); + expect(document.activeElement).toBe(items[1]); + + await user.keyboard('{ArrowDown}'); + expect(document.activeElement).toBe(items[2]); + + await user.keyboard('{ArrowUp}'); + expect(document.activeElement).toBe(items[1]); + + await user.tab(); + expect(document.activeElement).toBe(buttonRef.current); + + await user.tab(); + expect(document.activeElement).toBe(document.body); + }); + it('should support selectionMode="replace" with checkboxes', async () => { let {getAllByRole} = renderGridList({selectionMode: 'multiple', selectionBehavior: 'replace'}); let items = getAllByRole('row'); @@ -996,6 +1054,32 @@ describe('GridList', () => { expect(onRootDrop).toHaveBeenCalledTimes(1); }); + + it('should support dropping with horizontal arrow keys when orientation is horizontal', async () => { + let onReorder = jest.fn(); + let {getAllByRole} = render( Test} />); + + await user.tab(); + await user.keyboard('{ArrowRight}'); + await user.keyboard('{Enter}'); + act(() => jest.runAllTimers()); + + let rows = getAllByRole('row'); + expect(rows[2]).toHaveAttribute('data-drop-target'); + expect(within(rows[2]).getByRole('button')).toHaveAttribute('aria-label', 'Insert between Cat and Dog'); + + await user.keyboard('{ArrowRight}'); + expect(rows[3]).toHaveAttribute('data-drop-target', 'true'); + expect(document.activeElement).toHaveAttribute('aria-label', 'Insert between Dog and Kangaroo'); + + await user.keyboard('{ArrowLeft}'); + expect(rows[2]).toHaveAttribute('data-drop-target', 'true'); + + await user.keyboard('{Enter}'); + act(() => jest.runAllTimers()); + + expect(onReorder).toHaveBeenCalledTimes(1); + }); }); describe('links', function () {