Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(table): S2 tableview custom column menu #7617

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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: 42 additions & 27 deletions packages/@react-spectrum/s2/src/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import {IconContext} from './Icon';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {LayoutNode} from '@react-stately/layout';
import {Menu, MenuItem, MenuTrigger} from './Menu';
import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu';
import {mergeStyles} from '../style/runtime';
import Nubbin from '../ui-icons/S2_MoveHorizontalTableWidget.svg';
import {ProgressCircle} from './ProgressCircle';
Expand Down Expand Up @@ -469,7 +469,7 @@ const columnStyles = style({
},
paddingX: {
default: 16,
isColumnResizable: 0
isMenu: 0
},
textAlign: {
align: {
Expand All @@ -496,7 +496,7 @@ const columnStyles = style({
borderStartWidth: 0,
borderEndWidth: {
default: 0,
isColumnResizable: 1
isMenu: 1
},
borderStyle: 'solid',
forcedColorAdjust: 'none'
Expand All @@ -513,7 +513,8 @@ export interface ColumnProps extends RACColumnProps {
*/
align?: 'start' | 'center' | 'end',
/** The content to render as the column header. */
children: ReactNode
children: ReactNode,
menu?: ReactNode
}

/**
Expand All @@ -524,19 +525,20 @@ export const Column = forwardRef(function Column(props: ColumnProps, ref: DOMRef
let {isQuiet} = useContext(InternalTableContext);
let {allowsResizing, children, align = 'start'} = props;
let domRef = useDOMRef(ref);
let isColumnResizable = allowsResizing;
let isMenu = allowsResizing || !!props.menu;


return (
<RACColumn {...props} ref={domRef} style={{borderInlineEndColor: 'transparent'}} className={renderProps => columnStyles({...renderProps, isColumnResizable, align, isQuiet})}>
<RACColumn {...props} ref={domRef} style={{borderInlineEndColor: 'transparent'}} className={renderProps => columnStyles({...renderProps, isMenu, align, isQuiet})}>
{({allowsSorting, sortDirection, isFocusVisible, sort, startResize, isHovered}) => (
<>
{/* Note this is mainly for column's without a dropdown menu. If there is a dropdown menu, the button is styled to have a focus ring for simplicity
(no need to juggle showing this focus ring if focus is on the menu button and not if it is on the resizer) */}
{/* Separate absolutely positioned element because appyling the ring on the column directly via outline means the ring's required borderRadius will cause the bottom gray border to curve as well */}
{isFocusVisible && <CellFocusRing />}
{isColumnResizable ?
{isMenu ?
(
<ResizableColumnContents allowsSorting={allowsSorting} sortDirection={sortDirection} sort={sort} startResize={startResize} isHovered={isHeaderRowHovered || isHovered} align={align}>
<ResizableColumnContents isColumnResizable={allowsResizing} menu={props.menu} allowsSorting={allowsSorting} sortDirection={sortDirection} sort={sort} startResize={startResize} isHovered={isHeaderRowHovered || isHovered} align={align}>
{children}
</ResizableColumnContents>
) : (
Expand Down Expand Up @@ -709,10 +711,13 @@ const nubbin = style({
}
});

interface ResizableColumnContentProps extends Pick<ColumnRenderProps, 'allowsSorting' | 'sort' | 'sortDirection' | 'startResize' | 'isHovered'>, Pick<ColumnProps, 'align' | 'children'> {}
interface ResizableColumnContentProps extends Pick<ColumnRenderProps, 'allowsSorting' | 'sort' | 'sortDirection' | 'startResize' | 'isHovered'>, Pick<ColumnProps, 'align' | 'children'> {
isColumnResizable?: boolean,
menu?: ReactNode
}

function ResizableColumnContents(props: ResizableColumnContentProps) {
let {allowsSorting, sortDirection, sort, startResize, children, isHovered, align} = props;
let {allowsSorting, sortDirection, sort, startResize, children, isHovered, align, isColumnResizable, menu} = props;
let {setIsInResizeMode, isInResizeMode} = useContext(InternalTableContext);
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2');
const onMenuSelect = (key) => {
Expand All @@ -731,12 +736,13 @@ function ResizableColumnContents(props: ResizableColumnContentProps) {
};

let items = useMemo(() => {
let options = [
{
let options: Array<{label: string, id: string}> = [];
if (isColumnResizable) {
options = [{
label: stringFormatter.format('table.resizeColumn'),
id: 'resize'
}
];
}];
}
if (allowsSorting) {
options = [
{
Expand All @@ -752,7 +758,7 @@ function ResizableColumnContents(props: ResizableColumnContentProps) {
}
return options;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allowsSorting]);
}, [allowsSorting, isColumnResizable]);

let buttonAlignment = 'start';
let menuAlign = 'start' as 'start' | 'end';
Expand Down Expand Up @@ -784,20 +790,29 @@ function ResizableColumnContents(props: ResizableColumnContentProps) {
</div>
<Chevron size="M" className={chevronIcon} />
</Button>
<Menu onAction={onMenuSelect} items={items} styles={style({minWidth: 128})}>
{(item) => <MenuItem>{item?.label}</MenuItem>}
<Menu onAction={onMenuSelect} styles={style({minWidth: 128})}>
{items.length > 0 && (
<MenuSection aria-label="Sort or resize?">
Copy link
Member

Choose a reason for hiding this comment

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

not translated. also probably shouldn't be a question

Copy link
Member Author

Choose a reason for hiding this comment

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

haha, it's a question because #7617 (comment)

<Collection items={items}>
{(item) => <MenuItem>{item?.label}</MenuItem>}
</Collection>
Copy link
Member Author

Choose a reason for hiding this comment

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

best way to add other sort options here (for other columns like doc cloud has?)

</MenuSection>
)}
{menu}
</Menu>
</MenuTrigger>
<div data-react-aria-prevent-focus="true">
<ColumnResizer data-react-aria-prevent-focus="true" className={({resizableDirection, isResizing}) => resizerHandleContainer({resizableDirection, isResizing, isHovered: isInResizeMode || isHovered})}>
{({isFocusVisible, isResizing}) => (
<>
<ResizerIndicator isInResizeMode={isInResizeMode} isFocusVisible={isFocusVisible} isHovered={isHovered} isResizing={isResizing} />
{(isFocusVisible || isInResizeMode) && isResizing && <div className={nubbin}><Nubbin /></div>}
</>
)}
</ColumnResizer>
</div>
{isColumnResizable && (
<div data-react-aria-prevent-focus="true">
<ColumnResizer data-react-aria-prevent-focus="true" className={({resizableDirection, isResizing}) => resizerHandleContainer({resizableDirection, isResizing, isHovered: isInResizeMode || isHovered})}>
{({isFocusVisible, isResizing}) => (
<>
<ResizerIndicator isInResizeMode={isInResizeMode} isFocusVisible={isFocusVisible} isHovered={isHovered} isResizing={isResizing} />
{(isFocusVisible || isInResizeMode) && isResizing && <div className={nubbin}><Nubbin /></div>}
</>
)}
</ColumnResizer>
</div>
)}
</>
);
}
Expand Down
113 changes: 111 additions & 2 deletions packages/@react-spectrum/s2/stories/TableView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
*/

import {action} from '@storybook/addon-actions';
import {ActionButton, Cell, Column, Content, Heading, IllustratedMessage, Link, Row, TableBody, TableHeader, TableView} from '../src';
import {ActionButton, Cell, Column, Content, Heading, IllustratedMessage, Link, MenuItem, MenuSection, Row, TableBody, TableHeader, TableView, Text} from '../src';
import {categorizeArgTypes} from './utils';
import Filter from '../s2wf-icons/S2_Icon_Filter_20_N.svg';
import FolderOpen from '../spectrum-illustrations/linear/FolderOpen';
import type {Meta} from '@storybook/react';
import {SortDescriptor} from 'react-aria-components';
Expand Down Expand Up @@ -151,6 +152,94 @@ const DynamicTable = (args: any) => (
</TableView>
);


const DynamicTableWithCustomMenus = (args: any) => (
<TableView aria-label="Dynamic table" {...args} styles={style({width: 320, height: 208})}>
<TableHeader columns={columns}>
{(column) => (
<Column
width={150}
minWidth={150}
isRowHeader={column.isRowHeader}
menu={
Copy link
Member

Choose a reason for hiding this comment

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

Bit strange that this doesn't actually accept a <Menu>. We should discuss options for the API here.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, that's another option to handle it
was trying to answer #7617 (comment) as well

if it accepted a Menu, I guess we could do a custom renderer to inject our three menu items, still not a good way to ensure groups that make sense though

maybe people have to provide a menu with specific ids on certain children if they want resize/sort?

open to other ideas, not sold on what I currently have

<>
<MenuSection>
<MenuItem onAction={action('filter')}><Filter /><Text slot="label">Filter</Text></MenuItem>
</MenuSection>
snowystinger marked this conversation as resolved.
Show resolved Hide resolved
<MenuSection>
<MenuItem onAction={action('hide column')}><Text slot="label">Hide column</Text></MenuItem>
<MenuItem onAction={action('manage columns')}><Text slot="label">Manage columns</Text></MenuItem>
</MenuSection>
</>
}>{column.name}</Column>
)}
</TableHeader>
<TableBody items={items}>
{item => (
<Row id={item.id} columns={columns}>
{(column) => {
return <Cell>{item[column.id]}</Cell>;
}}
</Row>
)}
</TableBody>
</TableView>
);

let sortItems = items;
const DynamicSortableTableWithCustomMenus = (args: any) => {
let [items, setItems] = useState(sortItems);
let [sortDescriptor, setSortDescriptor] = useState({});
let onSortChange = (sortDescriptor: SortDescriptor) => {
let {direction = 'ascending', column = 'name'} = sortDescriptor;

let sorted = items.slice().sort((a, b) => {
let cmp = a[column] < b[column] ? -1 : 1;
if (direction === 'descending') {
cmp *= -1;
}
return cmp;
});

setItems(sorted);
setSortDescriptor(sortDescriptor);
};

return (
<TableView aria-label="Dynamic table" {...args} sortDescriptor={sortDescriptor} onSortChange={onSortChange} styles={style({width: 320, height: 208})}>
<TableHeader columns={columns}>
{(column) => (
<Column
allowsSorting
width={150}
minWidth={150}
isRowHeader={column.isRowHeader}
menu={
<>
<MenuSection>
<MenuItem onAction={action('filter')}><Filter /><Text slot="label">Filter</Text></MenuItem>
</MenuSection>
<MenuSection>
<MenuItem onAction={action('hide column')}><Text slot="label">Hide column</Text></MenuItem>
<MenuItem onAction={action('manage columns')}><Text slot="label">Manage columns</Text></MenuItem>
</MenuSection>
</>
}>{column.name}</Column>
)}
</TableHeader>
<TableBody items={items}>
{item => (
<Row id={item.id} columns={columns}>
{(column) => {
return <Cell>{item[column.id]}</Cell>;
}}
</Row>
)}
</TableBody>
</TableView>
);
};

export const Dynamic = {
render: DynamicTable,
args: {
Expand All @@ -159,6 +248,22 @@ export const Dynamic = {
}
};

export const DynamicCustomMenus = {
render: DynamicTableWithCustomMenus,
args: {
...Example.args,
disabledKeys: ['Foo 5']
}
};

export const DynamicSortableCustomMenus = {
render: DynamicSortableTableWithCustomMenus,
args: {
...Example.args,
disabledKeys: ['Foo 5']
}
};

function renderEmptyState() {
return (
<IllustratedMessage>
Expand Down Expand Up @@ -471,7 +576,11 @@ const SortableResizableTable = (args: any) => {
<TableView aria-label="sortable table" {...args} sortDescriptor={isSortable ? sortDescriptor : null} onSortChange={isSortable ? onSortChange : null} styles={style({width: 384, height: 320})}>
<TableHeader columns={args.columns}>
{(column: any) => (
<Column isRowHeader={column.isRowHeader} allowsSorting={column.isSortable} allowsResizing={column.allowsResizing} align={column.align}>{column.name}</Column>
<Column
isRowHeader={column.isRowHeader}
allowsSorting={column.isSortable}
allowsResizing={column.allowsResizing}
align={column.align}>{column.name}</Column>
)}
</TableHeader>
<TableBody items={items}>
Expand Down
Loading