From 2f2da023e4b90dc8aedf4e0e876e4289c1e4785b Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 16 Jan 2025 15:23:37 +1100 Subject: [PATCH] S2 TableView custom column menus --- packages/@react-spectrum/s2/src/TableView.tsx | 61 ++++++---- .../s2/stories/TableView.stories.tsx | 113 +++++++++++++++++- 2 files changed, 148 insertions(+), 26 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 73663391b86..786413cce43 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -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'; @@ -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 } /** @@ -526,6 +527,7 @@ export const Column = forwardRef(function Column(props: ColumnProps, ref: DOMRef let domRef = useDOMRef(ref); let isColumnResizable = allowsResizing; + return ( columnStyles({...renderProps, isColumnResizable, align, isQuiet})}> {({allowsSorting, sortDirection, isFocusVisible, sort, startResize, isHovered}) => ( @@ -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 && } - {isColumnResizable ? + {isColumnResizable || !!props.menu ? ( - + {children} ) : ( @@ -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', @@ -709,10 +711,13 @@ const nubbin = style({ } }); -interface ResizableColumnContentProps extends Pick, Pick {} +interface ResizableColumnContentProps extends Pick, Pick { + 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,27 @@ function ResizableColumnContents(props: ResizableColumnContentProps) { - - {(item) => {item?.label}} + + + + {(item) => {item?.label}} + + + {menu} -
- resizerHandleContainer({resizableDirection, isResizing, isHovered: isInResizeMode || isHovered})}> - {({isFocusVisible, isResizing}) => ( - <> - - {(isFocusVisible || isInResizeMode) && isResizing &&
} - - )} -
-
+ {isColumnResizable && ( +
+ resizerHandleContainer({resizableDirection, isResizing, isHovered: isInResizeMode || isHovered})}> + {({isFocusVisible, isResizing}) => ( + <> + + {(isFocusVisible || isInResizeMode) && isResizing &&
} + + )} +
+
+ )} ); } diff --git a/packages/@react-spectrum/s2/stories/TableView.stories.tsx b/packages/@react-spectrum/s2/stories/TableView.stories.tsx index a8125d2946a..36db89d5b15 100644 --- a/packages/@react-spectrum/s2/stories/TableView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TableView.stories.tsx @@ -11,7 +11,7 @@ */ 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'; @@ -19,6 +19,7 @@ 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; @@ -151,6 +152,94 @@ const DynamicTable = (args: any) => ( ); + +const DynamicTableWithCustomMenus = (args: any) => ( + + + {(column) => ( + + + Filter + + + Hide column + Manage columns + + + }>{column.name} + )} + + + {item => ( + + {(column) => { + return {item[column.id]}; + }} + + )} + + +); + +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 ( + + + {(column) => ( + + + Filter + + + Hide column + Manage columns + + + }>{column.name} + )} + + + {item => ( + + {(column) => { + return {item[column.id]}; + }} + + )} + + + ); +}; + 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 ( @@ -471,7 +576,11 @@ const SortableResizableTable = (args: any) => { {(column: any) => ( - {column.name} + {column.name} )}