diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 9cc6791d954..a12694692dd 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -122,7 +122,7 @@ export interface TableViewProps extends Omit, setIsInResizeMode?:(val: boolean) => void, isInResizeMode?: boolean}>({}); +let InternalTableContext = createContext, setIsInResizeMode?:(val: boolean) => void, isInResizeMode?: boolean, selectionMode?: 'none' | 'single' | 'multiple'}>({}); const tableWrapper = style({ minHeight: 0, @@ -291,6 +291,7 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re onResizeEnd: propsOnResizeEnd, onAction, onLoadMore, + selectionMode = 'none', ...otherProps } = props; @@ -315,11 +316,12 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re loadingState, onLoadMore, isInResizeMode, - setIsInResizeMode - }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode]); + setIsInResizeMode, + selectionMode + }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode, selectionMode]); let scrollRef = useRef(null); - let isCheckboxSelection = props.selectionMode === 'multiple' || props.selectionMode === 'single'; + let isCheckboxSelection = selectionMode === 'multiple' || selectionMode === 'single'; let {selectedKeys, onSelectionChange, actionBar, actionBarHeight} = useActionBarContainer({...props, scrollRef}); @@ -362,6 +364,7 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re isQuiet })} selectionBehavior="toggle" + selectionMode={selectionMode} onRowAction={onAction} {...otherProps} selectedKeys={selectedKeys} @@ -1053,6 +1056,45 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef({ + ...commonCellStyles, + color: { + default: baseColor('neutral'), + isSaving: baseColor('neutral-subdued') + }, + paddingY: centerPadding(), + boxSizing: 'border-box', + height: 'calc(100% - 1px)', // so we don't overlap the border of the next cell + width: 'full', + fontSize: controlFont(), + alignItems: 'center', + display: 'flex', + borderStyle: { + default: 'none', + isDivider: 'solid' + }, + borderEndWidth: { + default: 0, + isDivider: 1 + }, + borderColor: { + default: 'gray-300', + forcedColors: 'ButtonBorder' + }, + backgroundColor: { + default: 'transparent', + ':is([role="rowheader"]:hover, [role="gridcell"]:hover)': { + selectionMode: { + none: colorMix('gray-25', 'gray-900', 7), + single: 'gray-25', + multiple: 'gray-25' + } + }, + ':is([role="row"][data-focus-visible-within] [role="rowheader"]:focus-within, [role="row"][data-focus-visible-within] [role="gridcell"]:focus-within)': 'gray-25' + } +}); + let editPopover = style({ ...colorScheme(), '--s2-container-bg': { @@ -1083,17 +1125,19 @@ let editPopover = style({ }, getAllowedOverrides()); interface EditableCellProps extends Omit { + /** The component which will handle editing the cell. For example, a `TextField` or a `Picker`. */ renderEditing: () => ReactNode, + /** Whether the cell is currently being saved. */ isSaving?: boolean, - onSubmit: () => void, - onCancel: () => void + /** Handler that is called when the value has been changed and is ready to be saved. */ + onSubmit: () => void } /** - * An exditable cell within a table row. + * An editable cell within a table row. */ export const EditableCell = forwardRef(function EditableCell(props: EditableCellProps, ref: ForwardedRef) { - let {children, showDivider = false, textValue, ...otherProps} = props; + let {children, showDivider = false, textValue, isSaving, ...otherProps} = props; let tableVisualOptions = useContext(InternalTableContext); let domRef = useObjectRef(ref); textValue ||= typeof children === 'string' ? children : undefined; @@ -1101,10 +1145,11 @@ export const EditableCell = forwardRef(function EditableCell(props: EditableCell return ( cell({ + className={renderProps => editableCell({ ...renderProps, ...tableVisualOptions, - isDivider: showDivider + isDivider: showDivider, + isSaving })} textValue={textValue} {...otherProps}> @@ -1128,7 +1173,7 @@ const nonTextInputTypes = new Set([ ]); function EditableCellInner(props: EditableCellProps & {isFocusVisible: boolean, cellRef: RefObject}) { - let {children, align, renderEditing, isSaving, onSubmit, onCancel, isFocusVisible, cellRef} = props; + let {children, align, renderEditing, isSaving, onSubmit, isFocusVisible, cellRef} = props; let [isOpen, setIsOpen] = useState(false); let popoverRef = useRef(null); let formRef = useRef(null); @@ -1180,10 +1225,8 @@ function EditableCellInner(props: EditableCellProps & {isFocusVisible: boolean, } }, [isOpen]); - // Cancel, don't save the value let cancel = () => { setIsOpen(false); - onCancel(); }; return ( @@ -1202,6 +1245,7 @@ function EditableCellInner(props: EditableCellProps & {isFocusVisible: boolean, styles: style({ // TODO: really need access to display here instead, but not possible right now // will be addressable with displayOuter + // Could use `hidden` attribute instead of css, but I don't have access to much of this state at the moment visibility: { default: 'hidden', isForcedVisible: 'visible', diff --git a/packages/@react-spectrum/s2/stories/TableView.stories.tsx b/packages/@react-spectrum/s2/stories/TableView.stories.tsx index 045b6a4ecf6..2fd5b40815b 100644 --- a/packages/@react-spectrum/s2/stories/TableView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TableView.stories.tsx @@ -1497,7 +1497,6 @@ export const EditableTable: StoryObj = { align={column.align} showDivider={column.showDivider} onSubmit={() => onChange(item.id, column.id!)} - onCancel={() => {}} isSaving={item.isSaving[column.id!]} renderEditing={() => ( = { align={column.align} showDivider={column.showDivider} onSubmit={() => onChange(item.id, column.id!)} - onCancel={() => {}} isSaving={item.isSaving[column.id!]} renderEditing={() => ( = { align={column.align} showDivider={column.showDivider} onSubmit={() => onChange(item.id, column.id!)} - onCancel={() => {}} isSaving={item.isSaving[column.id!]} renderEditing={() => ( = { align={column.align} showDivider={column.showDivider} onSubmit={() => onChange(item.id, column.id!)} - onCancel={() => {}} isSaving={item.isSaving[column.id!]} renderEditing={() => ( { align={column.align} showDivider={column.showDivider} onSubmit={() => onChange(item.id, column.id!)} - onCancel={() => {}} isSaving={item.isSaving[column.id!]} renderEditing={() => ( { align={column.align} showDivider={column.showDivider} onSubmit={() => onChange(item.id, column.id!)} - onCancel={() => {}} isSaving={item.isSaving[column.id!]} renderEditing={() => ( & {name: string}> = [ + {name: 'Fruits', id: 'fruits', isRowHeader: true, width: '6fr', minWidth: 300}, + {name: 'Task', id: 'task', width: '2fr', minWidth: 100}, + {name: 'Farmer', id: 'farmer', width: '2fr', minWidth: 150} +]; +///- end collapse -/// + +export default function EditableTable(props) { + let columns = editableColumns; + let [editableItems, setEditableItems] = useState(defaultItems); + let intermediateValue = useRef(null); + + let onChange = useCallback((id: Key, columnId: Key) => { + let value = intermediateValue.current; + if (value === null) { + return; + } + intermediateValue.current = null; + setEditableItems(prev => { + let newItems = prev.map(i => i.id === id && i[columnId] !== value ? {...i, [columnId]: value} : i); + return newItems; + }); + }, []); + + let onIntermediateChange = useCallback((value: any) => { + intermediateValue.current = value; + }, []); + + return ( + + + {(column) => ( + {column.name} + )} + + + {item => ( + + {(column) => { + if (column.id === 'fruits') { + ///- begin highlight -/// + return ( + onChange(item.id, column.id!)} + renderEditing={() => ( + value.length > 0 ? null : 'Fruit name is required'} + styles={style({flexGrow: 1, flexShrink: 1, minWidth: 0})} + defaultValue={item[column.id!]} + onChange={value => onIntermediateChange(value)} /> + )}> +
+ {item[column.id]} + + +
+
+ ); + ///- end highlight -/// + } + if (column.id === 'farmer') { + ///- begin highlight -/// + return ( + onChange(item.id, column.id!)} + renderEditing={() => ( + onIntermediateChange(value)}> + + + Eva + + + + Steven + + + + Michael + + + + Sara + + + + Karina + + + + Otto + + + + Matt + + + + Emily + + + + Amelia + + + + Isla + + + )}> +
+ {item[column.id]} + +
+
+ ); + ///- end highlight -/// + } + return {item[column.id!]}; + }} +
+ )} +
+
+ ); +} +``` + ## API ```tsx links={{TableView: '#tableview', TableHeader: '#tableheader', Column: '#column', TableBody: '#tablebody', Row: '#row', Cell: '#cell'}} @@ -724,3 +894,7 @@ function subscribe(fn) { ### Cell + +### EditableCell + + \ No newline at end of file