Skip to content

Commit

Permalink
S2 TableView custom column menus
Browse files Browse the repository at this point in the history
  • Loading branch information
snowystinger committed Jan 16, 2025
1 parent 1286a65 commit 2f2da02
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 26 deletions.
61 changes: 37 additions & 24 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 @@ -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 @@ -526,6 +527,7 @@ export const Column = forwardRef(function Column(props: ColumnProps, ref: DOMRef
let domRef = useDOMRef(ref);
let isColumnResizable = allowsResizing;


return (
<RACColumn {...props} ref={domRef} style={{borderInlineEndColor: 'transparent'}} className={renderProps => columnStyles({...renderProps, isColumnResizable, align, isQuiet})}>
{({allowsSorting, sortDirection, isFocusVisible, sort, startResize, isHovered}) => (
Expand All @@ -534,9 +536,9 @@ export const Column = forwardRef(function Column(props: ColumnProps, ref: DOMRef
(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 ?
{isColumnResizable || !!props.menu ?
(
<ResizableColumnContents allowsSorting={allowsSorting} sortDirection={sortDirection} sort={sort} startResize={startResize} isHovered={isHeaderRowHovered || isHovered} align={align}>
<ResizableColumnContents isColumnResizable={isColumnResizable} menu={props.menu} allowsSorting={allowsSorting} sortDirection={sortDirection} sort={sort} startResize={startResize} isHovered={isHeaderRowHovered || isHovered} align={align}>
{children}
</ResizableColumnContents>
) : (
Expand Down Expand Up @@ -617,7 +619,7 @@ const resizableMenuButtonWrapper = style({
},
// TODO: when align: end, the dropdown arrow is misaligned with the text, not sure how best to make the svg be flush with the end of the button other than modifying the
// paddingEnd
paddingX: 16,
paddingX: 0,
backgroundColor: 'transparent',
borderStyle: 'none',
fontSize: 'control',
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,27 @@ 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})}>
<MenuSection>
<Collection items={items}>
{(item) => <MenuItem>{item?.label}</MenuItem>}
</Collection>
</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,14 +11,15 @@
*/

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, Header, Heading, IllustratedMessage, Link, MenuItem, MenuSection, Row, TableBody, TableHeader, TableView, Text} from '../src';
import {categorizeArgTypes} from './utils';
import FolderOpen from '../spectrum-illustrations/linear/FolderOpen';
import type {Meta} from '@storybook/react';
import {SortDescriptor} from 'react-aria-components';
import {style} from '../style/spectrum-theme' with {type: 'macro'};
import {useAsyncList} from '@react-stately/data';
import {useState} from 'react';
import Filter from '../s2wf-icons/S2_Icon_Filter_20_N.svg';

let onActionFunc = action('onAction');
let noOnAction = null;
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={
<>
<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>
);

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

0 comments on commit 2f2da02

Please sign in to comment.