-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
base: main
Are you sure you want to change the base?
Changes from all commits
2f2da02
4fc2b6e
e6c04fe
1682ec4
d027c92
f975d06
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -469,7 +469,7 @@ const columnStyles = style({ | |
}, | ||
paddingX: { | ||
default: 16, | ||
isColumnResizable: 0 | ||
isMenu: 0 | ||
}, | ||
textAlign: { | ||
align: { | ||
|
@@ -496,7 +496,7 @@ const columnStyles = style({ | |
borderStartWidth: 0, | ||
borderEndWidth: { | ||
default: 0, | ||
isColumnResizable: 1 | ||
isMenu: 1 | ||
}, | ||
borderStyle: 'solid', | ||
forcedColorAdjust: 'none' | ||
|
@@ -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 | ||
} | ||
|
||
/** | ||
|
@@ -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> | ||
) : ( | ||
|
@@ -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) => { | ||
|
@@ -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 = [ | ||
{ | ||
|
@@ -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'; | ||
|
@@ -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?"> | ||
<Collection items={items}> | ||
{(item) => <MenuItem>{item?.label}</MenuItem>} | ||
</Collection> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
)} | ||
</> | ||
); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -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={ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bit strange that this doesn't actually accept a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, that's another option to handle it 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: { | ||
|
@@ -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> | ||
|
@@ -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}> | ||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)