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
69 changes: 67 additions & 2 deletions packages/@react-spectrum/s2/chromatic/Menu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import {
PublishAndExport,
UnavailableMenuItem
} from '../stories/Menu.stories';
import {Button} from '../src/Button';
import {expect} from '@storybook/jest';
import {Menu} from '../src/Menu';
import {Menu, MenuItem, MenuTrigger} from '../src/Menu';
import type {Meta, StoryObj} from '@storybook/react';
import NewIcon from '../s2wf-icons/S2_Icon_New_20_N.svg';
import {userEvent, within} from 'storybook/test';

const meta: Meta<typeof Menu<any>> = {
Expand All @@ -31,7 +33,8 @@ const meta: Meta<typeof Menu<any>> = {
backgrounds: ['base'],
locales: ['en-US'],
disableAnimations: true
}
},
chromatic: {ignoreSelectors: ['[role="progressbar"]']}
},
tags: ['autodocs'],
title: 'S2 Chromatic/Menu'
Expand Down Expand Up @@ -84,3 +87,65 @@ export const WithUnavailableItem: Story = {
expect(menus).toHaveLength(2);
}
};

export const WithEmptyState: Story = {
render: () => (
<MenuTrigger>
<Button aria-label="Actions">
<NewIcon />
</Button>
<Menu aria-label="Test" items={[]}>
{() => <MenuItem>Never rendered</MenuItem>}
</Menu>
</MenuTrigger>
),
play: async ({canvasElement}) => {
await userEvent.tab();
await userEvent.keyboard('{ArrowDown}');
let body = canvasElement.ownerDocument.body;
let menu = await within(body).findByRole('menu');
await within(menu).findByText('No results');
}
};

export const WithInitialLoading: Story = {
render: () => (
<MenuTrigger>
<Button aria-label="Actions">
<NewIcon />
</Button>
<Menu aria-label="Test" items={[]} loadingState="loading">
{() => <MenuItem>Never rendered</MenuItem>}
</Menu>
</MenuTrigger>
),
play: async ({canvasElement}) => {
await userEvent.tab();
await userEvent.keyboard('{ArrowDown}');
let body = canvasElement.ownerDocument.body;
let menu = await within(body).findByRole('menu');
await within(menu).findByRole('progressbar', {hidden: true});
}
};

export const WithLoadMore: Story = {
render: () => (
<MenuTrigger>
<Button aria-label="Actions">
<NewIcon />
</Button>
<Menu aria-label="Test" loadingState="loadingMore">
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
</Menu>
</MenuTrigger>
),
play: async ({canvasElement}) => {
await userEvent.tab();
await userEvent.keyboard('{ArrowDown}');
let body = canvasElement.ownerDocument.body;
let menu = await within(body).findByRole('menu');
await within(menu).findByRole('progressbar', {hidden: true});
}
};
17 changes: 17 additions & 0 deletions packages/@react-spectrum/s2/chromatic/Picker.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,23 @@ export const ContextualHelp: Story = {
}
};

export const WithLoadMore: Story = {
render: () => (
<Picker label="Loading more" loadingState="loadingMore">
<PickerItem>Chocolate</PickerItem>
<PickerItem>Mint</PickerItem>
<PickerItem>Strawberry</PickerItem>
</Picker>
),
play: async ({canvasElement}) => {
await userEvent.tab();
await userEvent.keyboard('{ArrowDown}');
let body = canvasElement.ownerDocument.body;
let listbox = await within(body).findByRole('listbox');
await within(listbox).findByRole('progressbar', {hidden: true});
}
};

export const EmptyAndLoading: Story = {
render: () => (
<Picker label="loading" loadingState="loading">
Expand Down
19 changes: 9 additions & 10 deletions packages/@react-spectrum/s2/src/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,15 @@ import {BaseCollection, CollectionNode} from 'react-aria/private/collections/Bas
import {baseColor, centerPadding, focusRing, space, style} from '../style' with {type: 'macro'};
import {Button, ButtonRenderProps} from 'react-aria-components/Button';
import {centerBaseline} from './CenterBaseline';
import {checkmark, description, icon, iconCenterWrapper, label, sectionHeading} from './Menu';
import {
checkmark,
description,
icon,
iconCenterWrapper,
label,
loadingWrapperStyles,
sectionHeading
} from './Menu';
import CheckmarkIcon from '../ui-icons/Checkmark';
import ChevronIcon from '../ui-icons/Chevron';
import {Collection} from 'react-aria/Collection';
Expand Down Expand Up @@ -210,15 +218,6 @@ const iconStyles = style({
}
});

const loadingWrapperStyles = style({
gridColumnStart: '1',
gridColumnEnd: '-1',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginY: 8
});

const progressCircleStyles = style({
size: '1lh',
marginStart: {
Expand Down
114 changes: 110 additions & 4 deletions packages/@react-spectrum/s2/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Menu as AriaMenu,
MenuItem as AriaMenuItem,
MenuItemProps as AriaMenuItemProps,
MenuLoadMoreItem as AriaMenuLoadMoreItem,
MenuProps as AriaMenuProps,
MenuSection as AriaMenuSection,
MenuSectionProps as AriaMenuSectionProps,
Expand All @@ -24,6 +25,14 @@ import {
SubmenuTriggerProps as AriaSubmenuTriggerProps,
MenuItemRenderProps
} from 'react-aria-components/Menu';
import {
AsyncLoadable,
DOMRef,
DOMRefValue,
GlobalDOMAttributes,
LoadingState,
PressEvent
} from '@react-types/shared';
import {
baseColor,
centerPadding,
Expand All @@ -37,6 +46,7 @@ import {box, iconStyles} from './Checkbox';
import {centerBaseline} from './CenterBaseline';
import CheckmarkIcon from '../ui-icons/Checkmark';
import ChevronRightIcon from '../ui-icons/Chevron';
import {Collection} from 'react-aria/Collection';
import {ContextValue, DEFAULT_SLOT, Provider, useSlottedContext} from 'react-aria-components/slots';
import {
control,
Expand All @@ -56,7 +66,6 @@ import {
useState
} from 'react';
import {divider} from './Divider';
import {DOMRef, DOMRefValue, GlobalDOMAttributes, PressEvent} from '@react-types/shared';
import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'};
import {forwardRefType} from './types';
import {HeaderContext, HeadingContext, KeyboardContext, Text, TextContext} from './Content';
Expand All @@ -70,6 +79,7 @@ import {mergeStyles} from '../style/runtime';
import {Placement} from 'react-aria/useOverlayPosition';
import {PressResponder} from 'react-aria/private/interactions/PressResponder';
import {pressScale} from './pressScale';
import {ProgressCircle} from './ProgressCircle';
import {Separator, SeparatorProps} from 'react-aria-components/Separator';
import {ToggleButtonContext} from './ToggleButton';
import {useGlobalListeners} from 'react-aria/private/utils/useGlobalListeners';
Expand Down Expand Up @@ -107,6 +117,7 @@ export interface MenuProps<T>
AriaMenuProps<T>,
'children' | 'style' | 'className' | 'render' | 'renderEmptyState' | keyof GlobalDOMAttributes
>,
Pick<AsyncLoadable, 'onLoadMore'>,
StyleProps {
/**
* The size of the Menu.
Expand All @@ -120,6 +131,10 @@ export interface MenuProps<T>
children: ReactNode | ((item: T) => ReactNode);
/** Hides the default link out icons on menu items that open links in a new tab. */
hideLinkOutIcon?: boolean;
/**
* The current loading state of the Menu.
*/
loadingState?: LoadingState;
}

export const MenuContext =
Expand Down Expand Up @@ -413,6 +428,41 @@ let wrappingDiv = style({
size: 'full'
});

export const loadingWrapperStyles = style({
gridColumnStart: '1',
gridColumnEnd: '-1',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginY: 8
});

export const progressCircleStyles = style({
Comment thread
snowystinger marked this conversation as resolved.
size: '1lh'
});

const emptyStateText = style({
height: {
size: {
S: 24,
M: 32,
L: 40,
XL: 48
}
},
font: {
size: {
S: 'ui-sm',
M: 'ui',
L: 'ui-lg',
XL: 'ui-xl'
}
},
display: 'flex',
alignItems: 'center',
paddingX: 'edge-to-text'
});

/**
* Menus display a list of actions or options that a user can choose.
*/
Expand All @@ -428,10 +478,48 @@ export const Menu = /*#__PURE__*/ (forwardRef as forwardRefType)(function Menu<T
UNSAFE_style,
UNSAFE_className,
styles,
hideLinkOutIcon = false
hideLinkOutIcon = false,
items,
loadingState,
onLoadMore
} = props;
let ctx = useContext(InternalMenuTriggerContext);
let inPopover = useContext(InPopoverContext);
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2');

let menuLoadingCircle = (
<AriaMenuLoadMoreItem
isLoading={loadingState === 'loadingMore'}
onLoadMore={onLoadMore}
className={loadingWrapperStyles}>
<ProgressCircle
isIndeterminate
size="S"
styles={progressCircleStyles}
// Same loading string as table
aria-label={stringFormatter.format('table.loadingMore')}
/>
</AriaMenuLoadMoreItem>
);

let renderer;
if (typeof children === 'function' && items) {
renderer = (
<>
<Collection items={items} dependencies={props.dependencies}>
{children}
</Collection>
{menuLoadingCircle}
</>
);
} else {
renderer = (
<>
{children}
{menuLoadingCircle}
</>
);
}

let isPopover = (ctx || isSubmenu) && !inPopover;
let content = (
Expand All @@ -457,8 +545,26 @@ export const Menu = /*#__PURE__*/ (forwardRef as forwardRefType)(function Menu<T
],
[InPopoverContext, false]
]}>
<AriaMenu {...props} className={menu({size, isPopover}, isPopover ? null : styles)}>
{children}
<AriaMenu
{...props}
className={menu({size, isPopover}, isPopover ? null : styles)}
renderEmptyState={() =>
loadingState === 'loading' ? (
<div className={loadingWrapperStyles}>
<ProgressCircle
isIndeterminate
size="S"
styles={progressCircleStyles}
aria-label={stringFormatter.format('table.loading')}
/>
</div>
) : (
<span className={emptyStateText({size})}>
{stringFormatter.format('combobox.noResults')}
</span>
)
}>
{renderer}
</AriaMenu>
</Provider>
</InternalMenuContext.Provider>
Expand Down
26 changes: 4 additions & 22 deletions packages/@react-spectrum/s2/src/Picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import {
icon,
iconCenterWrapper,
label,
loadingWrapperStyles,
progressCircleStyles,
sectionHeading
} from './Menu';
import CheckmarkIcon from '../ui-icons/Checkmark';
Expand Down Expand Up @@ -291,26 +293,6 @@ const avatar = style({
marginEnd: 'text-to-visual'
});

const loadingWrapperStyles = style({
gridColumnStart: '1',
gridColumnEnd: '-1',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginY: 8
});

const progressCircleStyles = style({
size: {
size: {
S: 16,
M: 20,
L: 22,
XL: 26
}
}
});

let InternalPickerContext = createContext<{size: 'S' | 'M' | 'L' | 'XL'}>({size: 'M'});
let InsideSelectValueContext = createContext(false);

Expand Down Expand Up @@ -527,14 +509,14 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick
});

function PickerProgressCircle(props) {
let {id, size, 'aria-label': ariaLabel} = props;
let {id, 'aria-label': ariaLabel} = props;
return (
<ProgressCircle
id={id}
isIndeterminate
size="S"
aria-label={ariaLabel}
styles={progressCircleStyles({size})}
styles={progressCircleStyles}
/>
);
}
Expand Down
Loading