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
6 changes: 5 additions & 1 deletion packages/@adobe/spectrum-css-temp/components/table/skin.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ governing permissions and limitations under the License.
}
}

&:active {
&.is-active {
Comment thread
snowystinger marked this conversation as resolved.
color: var(--spectrum-table-header-text-color-down);

.spectrum-Table-sortedIcon {
Expand Down Expand Up @@ -77,6 +77,10 @@ governing permissions and limitations under the License.
border-color: var(--spectrum-table-border-color);
background-color: var(--spectrum-table-background-color);

&:focus-ring {
box-shadow: inset 0 0 0 2px var(--spectrum-table-cell-border-color-key-focus);
}

&.is-drop-target {
border-color: var(--spectrum-alias-border-color-focus);
box-shadow: 0 0 0 1px var(--spectrum-alias-border-color-focus);
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-aria/grid/src/useGridCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
isVirtualized,
focus,
shouldSelectOnPressUp,
onAction: onCellAction ? () => onCellAction(node.key) : onAction
onAction: onCellAction ? () => onCellAction(node.key) : onAction,
isDisabled: state.collection.size === 0
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it both non-interactive and non-navigable? or should we name the prop something else? or is that what you're suggesting in the comment?
Lets only do this for Table for now? I assume we'll be asked to do this for ListView eventually, but sounds like not yet?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR also covers ListView.

Also, if the collection is empty, wouldn't there be no grid cells? I guess only in the case of tableview because of the headers?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, only TableView has grid cells when the collection is empty, CardView and TagGroup don't. Was just a bit worried that there might people using these grid hooks directly to build a table (or some other grid pattern) that wouldn't want to disable interactions when their table is empty

});

let onKeyDownCapture = (e: ReactKeyboardEvent) => {
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-aria/grid/src/useGridRow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export function useGridRow<T, C extends GridCollection<T>, S extends GridState<T
ref,
isVirtualized,
shouldSelectOnPressUp,
onAction: onRowAction ? () => onRowAction(node.key) : onAction
onAction: onRowAction ? () => onRowAction(node.key) : onAction,
isDisabled: state.collection.size === 0
});

let isSelected = state.selectionManager.isSelected(node.key);
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/gridlist/src/useGridList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export function useGridList<T>(props: AriaGridListOptions<T>, state: ListState<T
id,
'aria-multiselectable': state.selectionManager.selectionMode === 'multiple' ? 'true' : undefined
},
listProps,
state.collection.size === 0 ? {} : listProps,
descriptionProps
);

Expand Down
2 changes: 2 additions & 0 deletions packages/@react-aria/table/src/useTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ export function useTable<T>(props: AriaTableProps<T>, state: TableState<T>, ref:
gridProps: mergeProps(
gridProps,
descriptionProps,
// If table is empty, make sure the table is tabbable
state.collection.size === 0 && {tabIndex: 0},
{
// merge sort description with long press information
'aria-describedby': [descriptionProps['aria-describedby'], gridProps['aria-describedby']].filter(Boolean).join(' ')
Expand Down
9 changes: 8 additions & 1 deletion packages/@react-aria/table/src/useTableColumnHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,14 @@ export function useTableColumnHeader<T>(props: AriaTableColumnHeaderProps, state

return {
columnHeaderProps: {
...mergeProps(gridCellProps, pressProps, focusableProps, descriptionProps),
...mergeProps(
gridCellProps,
pressProps,
focusableProps,
descriptionProps,
// If the table is empty, make all column headers untabbable or programatically focusable
state.collection.size === 0 && {tabIndex: null}
),
role: 'columnheader',
id: getColumnHeaderId(state, node.key),
'aria-colspan': node.colspan && node.colspan > 1 ? node.colspan : null,
Expand Down
8 changes: 5 additions & 3 deletions packages/@react-aria/table/src/useTableColumnResize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ export interface TableColumnResizeAria {
export interface AriaTableColumnResizeProps<T> {
column: GridNode<T>,
label: string,
triggerRef: RefObject<HTMLDivElement>
triggerRef: RefObject<HTMLDivElement>,
isDisabled?: boolean
}

export function useTableColumnResize<T>(props: AriaTableColumnResizeProps<T>, state: TableState<T>, columnState: TableColumnResizeState<T>, ref: RefObject<HTMLInputElement>): TableColumnResizeAria {
let {column: item, triggerRef} = props;
let {column: item, triggerRef, isDisabled} = props;
const stateRef = useRef<TableColumnResizeState<T>>(null);
// keep track of what the cursor on the body is so it can be restored back to that when done resizing
const cursor = useRef<string | null>(null);
Expand Down Expand Up @@ -159,7 +160,8 @@ export function useTableColumnResize<T>(props: AriaTableColumnResizeProps<T>, st
stateRef.current.onColumnResizeEnd(item);
state.setKeyboardNavigationDisabled(false);
},
onChange
onChange,
disabled: isDisabled
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than disabling this, shouldn't we just not render the menu button and not display the resizer on hover?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should work, I just felt like it would be appropriate to give the user the ability to disable a column resizer but we can add that when a use case arises.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, since I ran into issues here I'm going to go back to the isDisabled approach

},
ariaProps
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function useTableSelectAllCheckbox<T>(state: TableState<T>): TableSelectA
checkboxProps: {
'aria-label': stringFormatter.format(selectionMode === 'single' ? 'select' : 'selectAll'),
isSelected: isSelectAll,
isDisabled: selectionMode !== 'multiple',
isDisabled: selectionMode !== 'multiple' || state.collection.size === 0,
isIndeterminate: !isEmpty && !isSelectAll,
onChange: () => state.selectionManager.toggleSelectAll()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {GridLayoutOptions} from '../src/GridLayout';
import {Heading, Text} from '@react-spectrum/text';
import {IllustratedMessage} from '@react-spectrum/illustratedmessage';
import {Image} from '@react-spectrum/image';
import {Link} from '@react-spectrum/link';
import React, {Key, useMemo, useState} from 'react';
import {Size} from '@react-stately/virtualizer';
import {SpectrumCardViewProps} from '@react-types/card';
Expand Down Expand Up @@ -67,7 +68,7 @@ function renderEmptyState() {
<path d="M133.7,8.5h-118c-1.9,0-3.5,1.6-3.5,3.5v27c0,0.8,0.7,1.5,1.5,1.5s1.5-0.7,1.5-1.5V23.5h119V92c0,0.3-0.2,0.5-0.5,0.5h-118c-0.3,0-0.5-0.2-0.5-0.5V69c0-0.8-0.7-1.5-1.5-1.5s-1.5,0.7-1.5,1.5v23c0,1.9,1.6,3.5,3.5,3.5h118c1.9,0,3.5-1.6,3.5-3.5V12C137.2,10.1,135.6,8.5,133.7,8.5z M15.2,21.5V12c0-0.3,0.2-0.5,0.5-0.5h118c0.3,0,0.5,0.2,0.5,0.5v9.5H15.2z M32.6,16.5c0,0.6-0.4,1-1,1h-10c-0.6,0-1-0.4-1-1s0.4-1,1-1h10C32.2,15.5,32.6,15.9,32.6,16.5z M13.6,56.1l-8.6,8.5C4.8,65,4.4,65.1,4,65.1c-0.4,0-0.8-0.1-1.1-0.4c-0.6-0.6-0.6-1.5,0-2.1l8.6-8.5l-8.6-8.5c-0.6-0.6-0.6-1.5,0-2.1c0.6-0.6,1.5-0.6,2.1,0l8.6,8.5l8.6-8.5c0.6-0.6,1.5-0.6,2.1,0c0.6,0.6,0.6,1.5,0,2.1L15.8,54l8.6,8.5c0.6,0.6,0.6,1.5,0,2.1c-0.3,0.3-0.7,0.4-1.1,0.4c-0.4,0-0.8-0.1-1.1-0.4L13.6,56.1z" />
</svg>
<Heading>No results</Heading>
<Content>No results found</Content>
<Content>No results found, press <Link onPress={action('linkPress')}>here</Link> for more info.</Content>
</IllustratedMessage>
);
}
Expand Down Expand Up @@ -527,4 +528,3 @@ export function CustomLayout(props: SpectrumCardViewProps<object> & LayoutOption
</div>
);
}

5 changes: 3 additions & 2 deletions packages/@react-spectrum/list/stories/ListView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {Image} from '@react-spectrum/image';
import Info from '@spectrum-icons/workflow/Info';
import {Item, ListView} from '../';
import {ItemDropTarget} from '@react-types/shared';
import {Link} from '@react-spectrum/link';
import NoSearchResults from '@spectrum-icons/illustrations/NoSearchResults';
import React, {useEffect, useState} from 'react';
import {storiesOf} from '@storybook/react';
Expand Down Expand Up @@ -106,14 +107,14 @@ const itemsWithThumbs = [
{key: '9', title: 'file of great boi', illustration: <File />}
];

function renderEmptyState() {
export function renderEmptyState() {
return (
<IllustratedMessage>
<svg width="150" height="103" viewBox="0 0 150 103">
<path d="M133.7,8.5h-118c-1.9,0-3.5,1.6-3.5,3.5v27c0,0.8,0.7,1.5,1.5,1.5s1.5-0.7,1.5-1.5V23.5h119V92c0,0.3-0.2,0.5-0.5,0.5h-118c-0.3,0-0.5-0.2-0.5-0.5V69c0-0.8-0.7-1.5-1.5-1.5s-1.5,0.7-1.5,1.5v23c0,1.9,1.6,3.5,3.5,3.5h118c1.9,0,3.5-1.6,3.5-3.5V12C137.2,10.1,135.6,8.5,133.7,8.5z M15.2,21.5V12c0-0.3,0.2-0.5,0.5-0.5h118c0.3,0,0.5,0.2,0.5,0.5v9.5H15.2z M32.6,16.5c0,0.6-0.4,1-1,1h-10c-0.6,0-1-0.4-1-1s0.4-1,1-1h10C32.2,15.5,32.6,15.9,32.6,16.5z M13.6,56.1l-8.6,8.5C4.8,65,4.4,65.1,4,65.1c-0.4,0-0.8-0.1-1.1-0.4c-0.6-0.6-0.6-1.5,0-2.1l8.6-8.5l-8.6-8.5c-0.6-0.6-0.6-1.5,0-2.1c0.6-0.6,1.5-0.6,2.1,0l8.6,8.5l8.6-8.5c0.6-0.6,1.5-0.6,2.1,0c0.6,0.6,0.6,1.5,0,2.1L15.8,54l8.6,8.5c0.6,0.6,0.6,1.5,0,2.1c-0.3,0.3-0.7,0.4-1.1,0.4c-0.4,0-0.8-0.1-1.1-0.4L13.6,56.1z" />
</svg>
<Heading>No results</Heading>
<Content>No results found</Content>
<Content>No results found, press <Link onPress={action('linkPress')}>here</Link> for more info.</Content>
</IllustratedMessage>
);
}
Expand Down
14 changes: 11 additions & 3 deletions packages/@react-spectrum/list/test/ListView.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {announce} from '@react-aria/live-announcer';
import {Item, ListView} from '../src';
import {Provider} from '@react-spectrum/provider';
import React from 'react';
import {renderEmptyState} from '../stories/ListView.stories';
import {Text} from '@react-spectrum/text';
import {theme} from '@react-spectrum/theme-default';
import userEvent from '@testing-library/user-event';
Expand Down Expand Up @@ -570,13 +571,20 @@ describe('ListView', function () {
});

it('should render empty state', function () {
function renderEmptyState() {
return <div>No results</div>;
}
let {getByText} = render(<ListView aria-label="List" renderEmptyState={renderEmptyState} />);
expect(getByText('No results')).toBeTruthy();
});

it('should allow you to tab into ListView body if empty', function () {
let {getByRole} = render(<ListView aria-label="List" renderEmptyState={renderEmptyState} />);
let grid = getByRole('grid');
let link = getByRole('link');
userEvent.tab();
expect(document.activeElement).toBe(grid);
userEvent.tab();
expect(document.activeElement).toBe(link);
});

it('supports custom data attributes', () => {
let {getByRole} = render(
<ListView aria-label="List" data-testid="test">
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-spectrum/table/src/Resizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ interface ResizerProps<T> {

function Resizer<T>(props: ResizerProps<T>, ref: RefObject<HTMLInputElement>) {
let {column, showResizer} = props;
let {state, columnState} = useTableContext();
let {state, columnState, isEmpty} = useTableContext();
let stringFormatter = useLocalizedStringFormatter(intlMessages);
let {direction} = useLocale();

let {inputProps, resizerProps} = useTableColumnResize({...props, label: stringFormatter.format('columnResizer')}, state, columnState, ref);
let {inputProps, resizerProps} = useTableColumnResize({...props, label: stringFormatter.format('columnResizer'), isDisabled: isEmpty}, state, columnState, ref);

let style = {
cursor: undefined,
Expand Down
48 changes: 33 additions & 15 deletions packages/@react-spectrum/table/src/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ interface TableContextValue<T> {
state: TableState<T>,
layout: TableLayout<T>,
columnState: TableColumnResizeState<T>,
headerRowHovered: boolean
headerRowHovered: boolean,
isEmpty: boolean
}

const TableContext = React.createContext<TableContextValue<unknown>>(null);
Expand Down Expand Up @@ -252,6 +253,7 @@ function TableView<T extends object>(props: SpectrumTableProps<T>, ref: DOMRef<H
return <TableSelectAllCell column={item} />;
}

// TODO: consider this case, what if we have hidden headers and a empty table
if (item.props.hideHeader) {
return (
<TooltipTrigger placement="top" trigger="focus">
Expand Down Expand Up @@ -304,11 +306,13 @@ function TableView<T extends object>(props: SpectrumTableProps<T>, ref: DOMRef<H
setHorizontalScollbarVisible(bodyRef.current.clientHeight + 2 < bodyRef.current.offsetHeight);
}
}, []);
let {isFocusVisible, focusProps} = useFocusRing();
let isEmpty = state.collection.size === 0;

return (
<TableContext.Provider value={{state, layout, columnState, headerRowHovered}}>
<TableContext.Provider value={{state, layout, columnState, headerRowHovered, isEmpty}}>
<TableVirtualizer
{...gridProps}
{...mergeProps(gridProps, focusProps)}
{...styleProps}
className={
classNames(
Expand Down Expand Up @@ -337,13 +341,14 @@ function TableView<T extends object>(props: SpectrumTableProps<T>, ref: DOMRef<H
onVisibleRectChange={onVisibleRectChange}
domRef={domRef}
bodyRef={bodyRef}
isFocusVisible={isFocusVisible}
getColumnWidth={columnState.getColumnWidth} />
</TableContext.Provider>
);
}

// This is a custom Virtualizer that also has a header that syncs its scroll position with the body.
function TableVirtualizer({layout, collection, focusedKey, renderView, renderWrapper, domRef, bodyRef, setTableWidth, getColumnWidth, onVisibleRectChange: onVisibleRectChangeProp, ...otherProps}) {
function TableVirtualizer({layout, collection, focusedKey, renderView, renderWrapper, domRef, bodyRef, setTableWidth, getColumnWidth, onVisibleRectChange: onVisibleRectChangeProp, isFocusVisible, ...otherProps}) {
let {direction} = useLocale();
let headerRef = useRef<HTMLDivElement>();
let loadingState = collection.body.props.loadingState;
Expand Down Expand Up @@ -415,7 +420,8 @@ function TableVirtualizer({layout, collection, focusedKey, renderView, renderWra

return (
<div
{...mergeProps(otherProps, virtualizerProps)}
// Override virtualizer provided tabindex if TableView is empty so it is tabbable.
{...mergeProps(otherProps, virtualizerProps, collection.size === 0 && {tabIndex: 0})}
Comment on lines +423 to +424
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useVirtualizer will return tabindex=-1 if the focused view is still in the TableView, which is problematic for the empty case because the last focused view could be a column header prior to the TableView becoming empty and thus would still be in view. Debated putting this logic into useVirtualizer but I figured that would be too wide of a change. Specifically, in case someone was using Virtualizer to create a component that would keep some focusable views even when the collection was empty but they wanted those view to still be tabbable/didn't want the virtualizer to be tabbable

ref={domRef}>
<div
role="presentation"
Expand All @@ -433,7 +439,15 @@ function TableVirtualizer({layout, collection, focusedKey, renderView, renderWra
</div>
<ScrollView
role="presentation"
className={classNames(styles, 'spectrum-Table-body')}
className={
classNames(
styles,
'spectrum-Table-body',
{
'focus-ring': isFocusVisible
}
)
}
style={{flex: 1}}
innerStyle={{overflow: 'visible', transition: state.isAnimating ? `none ${state.virtualizer.transitionDuration}ms` : undefined}}
ref={bodyRef}
Expand Down Expand Up @@ -461,17 +475,18 @@ function TableHeader({children, ...otherProps}) {
function TableColumnHeader(props) {
let {column} = props;
let ref = useRef<HTMLDivElement>(null);
let {state} = useTableContext();
let {state, isEmpty} = useTableContext();
let {pressProps, isPressed} = usePress({isDisabled: isEmpty});
let {columnHeaderProps} = useTableColumnHeader({
node: column,
isVirtualized: true
}, state, ref);

let columnProps = column.props as SpectrumColumnProps<unknown>;

let {hoverProps, isHovered} = useHover(props);
let {hoverProps, isHovered} = useHover({...props, isDisabled: isEmpty});

const allProps = [columnHeaderProps, hoverProps];
const allProps = [columnHeaderProps, hoverProps, pressProps];

return (
<FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
Expand All @@ -483,6 +498,7 @@ function TableColumnHeader(props) {
styles,
'spectrum-Table-headCell',
{
'is-active': isPressed,
'is-resizable': columnProps.allowsResizing,
'is-sortable': columnProps.allowsSorting,
'is-sorted-desc': state.sortDescriptor?.column === column.key && state.sortDescriptor?.direction === 'descending',
Expand Down Expand Up @@ -513,8 +529,9 @@ function TableColumnHeader(props) {
}

let _TableColumnHeaderButton = (props, ref: FocusableRef<HTMLDivElement>) => {
let {isEmpty} = useTableContext();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the table is empty, maybe we just render a TableColumnHeader instead of one with a menu?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we need to render the TableResizeColumnHeader if it is resizable, otherwise flipping from empty to non-empty doesn't replace the in view columns with resizable columns (caching probably?)

let domRef = useFocusableRef(ref);
let {buttonProps} = useButton({...props, elementType: 'div'}, domRef);
let {buttonProps} = useButton({...props, elementType: 'div', isDisabled: isEmpty}, domRef);
return (
<div className={classNames(styles, 'spectrum-Table-headCellContents')}>
<FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
Expand All @@ -530,16 +547,17 @@ function ResizableTableColumnHeader(props) {
let ref = useRef(null);
let triggerRef = useRef(null);
let resizingRef = useRef(null);
let {state, columnState, headerRowHovered} = useTableContext();
let {state, columnState, headerRowHovered, isEmpty} = useTableContext();
let stringFormatter = useLocalizedStringFormatter(intlMessages);
let {pressProps, isPressed} = usePress({isDisabled: isEmpty});
let {columnHeaderProps} = useTableColumnHeader({
node: column,
isVirtualized: true,
hasMenu: true
}, state, ref);
let {hoverProps, isHovered} = useHover(props);
let {hoverProps, isHovered} = useHover({...props, isDisabled: isEmpty});

const allProps = [columnHeaderProps, hoverProps];
const allProps = [columnHeaderProps, hoverProps, pressProps];

let columnProps = column.props as SpectrumColumnProps<unknown>;

Expand Down Expand Up @@ -589,7 +607,7 @@ function ResizableTableColumnHeader(props) {
}
}, [columnState.currentlyResizingColumn, column.key]);

let showResizer = headerRowHovered || columnState.currentlyResizingColumn != null;
let showResizer = !isEmpty && (headerRowHovered || columnState.currentlyResizingColumn != null);

return (
<FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
Expand All @@ -601,6 +619,7 @@ function ResizableTableColumnHeader(props) {
styles,
'spectrum-Table-headCell',
{
'is-active': isPressed,
'is-resizable': columnProps.allowsResizing,
'is-sortable': columnProps.allowsSorting,
'is-sorted-desc': state.sortDescriptor?.column === column.key && state.sortDescriptor?.direction === 'descending',
Expand Down Expand Up @@ -685,7 +704,6 @@ function TableSelectAllCell({column}) {
}
<Checkbox
{...checkboxProps}
isDisabled={isSingleSelectionMode}
isEmphasized
UNSAFE_style={isSingleSelectionMode ? {visibility: 'hidden'} : undefined}
UNSAFE_className={classNames(styles, 'spectrum-Table-checkbox')} />
Expand Down
Loading