From a762c2629fd0e22929875b59be6a48792f8edc43 Mon Sep 17 00:00:00 2001 From: Marshall Peterson Date: Tue, 4 Jan 2022 15:49:23 -0700 Subject: [PATCH 01/11] initial work --- .../components/table/index.css | 34 +++++++++++++ .../@react-aria/slider/src/useSliderThumb.ts | 6 ++- packages/@react-aria/table/package.json | 3 ++ .../table/src/useTableColumnResize.ts | 51 +++++++++++++++++++ .../slider/src/SliderThumb.tsx | 2 + .../@react-spectrum/table/src/Resizer.tsx | 16 ++++++ .../@react-spectrum/table/src/TableView.tsx | 16 ++++-- .../slider/src/useSliderState.ts | 2 +- packages/@react-stately/table/package.json | 1 + .../@react-stately/table/src/useTableState.ts | 23 +++++++-- packages/@react-types/table/src/index.d.ts | 7 ++- 11 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 packages/@react-aria/table/src/useTableColumnResize.ts create mode 100644 packages/@react-spectrum/table/src/Resizer.tsx diff --git a/packages/@adobe/spectrum-css-temp/components/table/index.css b/packages/@adobe/spectrum-css-temp/components/table/index.css index 9afc14fbad3..e51a7a8364f 100644 --- a/packages/@adobe/spectrum-css-temp/components/table/index.css +++ b/packages/@adobe/spectrum-css-temp/components/table/index.css @@ -86,6 +86,32 @@ svg.spectrum-Table-sortedIcon { } } +.spectrum-Table-columnResizer { + display: flex; + align-items: center; + justify-content: end; + box-sizing: border-box; + position: absolute; + top: 0; + right: 0px; + width: 6px; + height: 100%; + cursor: col-resize; + user-select: none; + z-index: 3; + + &::after { + content: ""; + position: absolute; + z-index: 2; + display: block; + box-sizing: border-box; + width: 1px; + height: 100%; + background-color: red; + } +} + .spectrum-Table-cell--alignCenter { text-align: center; } @@ -236,6 +262,14 @@ svg.spectrum-Table-sortedIcon { border-inline-end-width: var(--spectrum-table-divider-border-size); } +.spectrum-Table-cell--divider { + &.is-resizable { + &:hover { + border-inline-end-width: 3px; + } + } +} + .spectrum-Table-row { position: relative; cursor: default; diff --git a/packages/@react-aria/slider/src/useSliderThumb.ts b/packages/@react-aria/slider/src/useSliderThumb.ts index 7aafaf8d6b2..0ae302b512e 100644 --- a/packages/@react-aria/slider/src/useSliderThumb.ts +++ b/packages/@react-aria/slider/src/useSliderThumb.ts @@ -80,11 +80,12 @@ export function useSliderThumb( let {moveProps} = useMove({ onMoveStart() { currentPosition.current = null; + console.log('start', stateRef.current.isThumbDragging(index)); state.setThumbDragging(index, true); }, onMove({deltaX, deltaY, pointerType}) { let size = isVertical ? trackRef.current.offsetHeight : trackRef.current.offsetWidth; - + if (currentPosition.current == null) { currentPosition.current = stateRef.current.getThumbPercent(index) * size; } @@ -98,12 +99,13 @@ export function useSliderThumb( if (isVertical || reverseX) { delta = -delta; } - + currentPosition.current += delta; stateRef.current.setThumbPercent(index, clamp(currentPosition.current / size, 0, 1)); } }, onMoveEnd() { + console.log('end', stateRef.current.isThumbDragging(index)); state.setThumbDragging(index, false); } }); diff --git a/packages/@react-aria/table/package.json b/packages/@react-aria/table/package.json index d256d4baf12..d82b1cc6638 100644 --- a/packages/@react-aria/table/package.json +++ b/packages/@react-aria/table/package.json @@ -22,14 +22,17 @@ "@react-aria/grid": "^3.0.0", "@react-aria/i18n": "^3.3.2", "@react-aria/interactions": "^3.6.0", + "@react-aria/label": "^3.2.0", "@react-aria/live-announcer": "^3.0.1", "@react-aria/selection": "^3.6.0", "@react-aria/utils": "^3.9.0", + "@react-stately/slider": "^3.0.3", "@react-stately/table": "^3.0.0", "@react-stately/virtualizer": "^3.1.5", "@react-types/checkbox": "^3.2.3", "@react-types/grid": "^3.0.0", "@react-types/shared": "^3.9.0", + "@react-types/slider": "^3.0.2", "@react-types/table": "^3.0.0" }, "peerDependencies": { diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts new file mode 100644 index 00000000000..1632d939646 --- /dev/null +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// import {snapValueToStep} from '@react-aria/utils'; +import {useMove} from '@react-aria/interactions'; +import {useRef} from 'react'; + + +export function useTableColumnResize(state): any { + const stateRef = useRef(null); + stateRef.current = state; + let currentPosition = useRef(null); + const {moveProps} = useMove({ + onMoveStart() { + currentPosition.current = null; + console.log('start resizing - isResizingColumn:', stateRef.current.isResizingColumn()); + stateRef.current.setResizingColumn(true); + }, + onMove({deltaX}) { + + if (currentPosition.current == null) { + // currentPosition.current = state; + currentPosition.current = 100; + } + + currentPosition.current += deltaX; + // console.log(currentPosition.current, state); + // setState(currentPosition.current); + // setState(snapValueToStep(currentPosition.current, 75, 500, 1)); + }, + onMoveEnd() { + console.log('end resizing - isResizingColumn:', stateRef.current.isResizingColumn()); + stateRef.current.setResizingColumn(false); + } + }); + + + return { + resizerProps: moveProps + }; + +} diff --git a/packages/@react-spectrum/slider/src/SliderThumb.tsx b/packages/@react-spectrum/slider/src/SliderThumb.tsx index e6e5a591ffc..ce886610b4c 100644 --- a/packages/@react-spectrum/slider/src/SliderThumb.tsx +++ b/packages/@react-spectrum/slider/src/SliderThumb.tsx @@ -46,6 +46,8 @@ export function SliderThumb(props: SliderThumbProps) { let {direction} = useLocale(); let cssDirection = direction === 'rtl' ? 'right' : 'left'; + console.log('SliderThumb.tsx', state.isThumbDragging(index)); + return (
+ ); +} diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 2b76ebb877e..282a54535e0 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -23,6 +23,7 @@ import {mergeProps, useLayoutEffect} from '@react-aria/utils'; import {ProgressCircle} from '@react-spectrum/progress'; import React, {ReactElement, useCallback, useContext, useMemo, useRef, useState} from 'react'; import {Rect, ReusableView, useVirtualizerState} from '@react-stately/virtualizer'; +import Resizer from './Resizer'; import {SpectrumColumnProps, SpectrumTableProps} from '@react-types/table'; import styles from '@adobe/spectrum-css-temp/components/table/vars.css'; import stylesOverrides from './table.css'; @@ -83,6 +84,8 @@ function TableView(props: SpectrumTableProps, ref: DOMRef(props: SpectrumTableProps, ref: DOMRef) => { + let renderView = (tableState?: any) => (type: string, item: GridNode) => { switch (type) { case 'header': case 'rowgroup': @@ -234,13 +237,18 @@ function TableView(props: SpectrumTableProps, ref: DOMRef; + return ( + <> + + + + ); case 'loader': return ( 0 ? formatMessage('loadingMore') : formatMessage('loading')} /> + aria-label={tableState.collection.size > 0 ? formatMessage('loadingMore') : formatMessage('loading')} /> ); case 'empty': { @@ -281,7 +289,7 @@ function TableView(props: SpectrumTableProps, ref: DOMRef renderView(state)} renderWrapper={renderWrapper} domRef={domRef} /> diff --git a/packages/@react-stately/slider/src/useSliderState.ts b/packages/@react-stately/slider/src/useSliderState.ts index 15686e46ddb..b187495af48 100644 --- a/packages/@react-stately/slider/src/useSliderState.ts +++ b/packages/@react-stately/slider/src/useSliderState.ts @@ -225,7 +225,7 @@ export function useSliderState(props: SliderStateOptions): SliderState { getThumbValue: (index: number) => values[index], setThumbValue: updateValue, setThumbPercent, - isThumbDragging: (index: number) => isDraggings[index], + isThumbDragging: (index: number) => isDraggingsRef.current[index], setThumbDragging: updateDragging, focusedThumb: focusedIndex, setFocusedThumb: setFocusedIndex, diff --git a/packages/@react-stately/table/package.json b/packages/@react-stately/table/package.json index eb66c910b96..83fcb39fc44 100644 --- a/packages/@react-stately/table/package.json +++ b/packages/@react-stately/table/package.json @@ -21,6 +21,7 @@ "@react-stately/collections": "^3.3.3", "@react-stately/grid": "^3.0.0", "@react-stately/selection": "^3.7.0", + "@react-stately/utils": "^3.2.2", "@react-types/grid": "^3.0.0", "@react-types/shared": "^3.8.0", "@react-types/table": "^3.0.0" diff --git a/packages/@react-stately/table/src/useTableState.ts b/packages/@react-stately/table/src/useTableState.ts index a0ffb553574..0f053c47771 100644 --- a/packages/@react-stately/table/src/useTableState.ts +++ b/packages/@react-stately/table/src/useTableState.ts @@ -13,7 +13,7 @@ import {CollectionBase, Node, SelectionMode, Sortable, SortDescriptor, SortDirection} from '@react-types/shared'; import {GridState, useGridState} from '@react-stately/grid'; import {TableCollection as ITableCollection} from '@react-types/table'; -import {Key, useMemo} from 'react'; +import {Key, useMemo, useRef, useState} from 'react'; import {MultipleSelectionStateProps} from '@react-stately/selection'; import {TableCollection} from './TableCollection'; import {useCollection} from '@react-stately/collections'; @@ -26,7 +26,12 @@ export interface TableState extends GridState> { /** The current sorted column and direction. */ sortDescriptor: SortDescriptor, /** Calls the provided onSortChange handler with the provided column key and sort direction. */ - sort(columnKey: Key): void + sort(columnKey: Key): void, + + /** Test that something was resized. */ + isResizingColumn(): boolean, + /** Set the value of isResizing. */ + setResizingColumn(resizing: boolean): void } export interface CollectionBuilderContext { @@ -52,6 +57,16 @@ const OPPOSITE_SORT_DIRECTION = { export function useTableState(props: TableStateProps): TableState { let {selectionMode = 'none'} = props; + const [isResizing, setResizing] = useState(false); + + const isResizingRef = useRef(null); + isResizingRef.current = isResizing; + + function updateResizing(resizing: boolean) { + isResizingRef.current = resizing; + setResizing(isResizingRef.current); + } + let context = useMemo(() => ({ showSelectionCheckboxes: props.showSelectionCheckboxes && selectionMode !== 'none', selectionMode, @@ -78,6 +93,8 @@ export function useTableState(props: TableStateProps): Tabl ? OPPOSITE_SORT_DIRECTION[props.sortDescriptor.direction] : 'ascending' }); - } + }, + isResizingColumn: () => isResizing, + setResizingColumn: updateResizing }; } diff --git a/packages/@react-types/table/src/index.d.ts b/packages/@react-types/table/src/index.d.ts index 7d548a8b45b..64e54840427 100644 --- a/packages/@react-types/table/src/index.d.ts +++ b/packages/@react-types/table/src/index.d.ts @@ -62,7 +62,12 @@ export interface ColumnProps { minWidth?: number | string, /** The maximum width of the column. */ maxWidth?: number | string, - // defaultWidth?: number | string + /** The default width of the column. */ + defaultWidth?: number | string, + /** Whether the column allows resizing. */ + allowsResizing?: boolean, + /** Whether the column allows resizing. */ + onResize?: (width: number) => void, /** Whether the column allows sorting. */ allowsSorting?: boolean, /** Whether a column is a [row header](https://www.w3.org/TR/wai-aria-1.1/#rowheader) and should be announced by assistive technology during row navigation. */ From 1d4e93d84ce44c8016261cd8af6c98e8752bd331 Mon Sep 17 00:00:00 2001 From: Danny North Date: Thu, 6 Jan 2022 13:40:44 -0700 Subject: [PATCH 02/11] really hacky column resizing to prove flow --- .../@react-aria/slider/src/useSliderThumb.ts | 59 +- .../table/src/useTableColumnResize.ts | 24 +- .../@react-spectrum/table/src/TableView.tsx | 573 ++++++++++-------- .../@react-stately/layout/src/TableLayout.ts | 17 +- .../slider/src/useSliderState.ts | 30 +- .../@react-stately/table/src/useTableState.ts | 64 +- 6 files changed, 459 insertions(+), 308 deletions(-) diff --git a/packages/@react-aria/slider/src/useSliderThumb.ts b/packages/@react-aria/slider/src/useSliderThumb.ts index 0ae302b512e..23a0c89571f 100644 --- a/packages/@react-aria/slider/src/useSliderThumb.ts +++ b/packages/@react-aria/slider/src/useSliderThumb.ts @@ -1,7 +1,21 @@ import {AriaSliderThumbProps} from '@react-types/slider'; -import {clamp, focusWithoutScrolling, mergeProps, useGlobalListeners} from '@react-aria/utils'; +import { + clamp, + focusWithoutScrolling, + mergeProps, + useGlobalListeners +} from '@react-aria/utils'; import {getSliderThumbId, sliderIds} from './utils'; -import React, {ChangeEvent, HTMLAttributes, InputHTMLAttributes, LabelHTMLAttributes, RefObject, useCallback, useEffect, useRef} from 'react'; +import React, { + ChangeEvent, + HTMLAttributes, + InputHTMLAttributes, + LabelHTMLAttributes, + RefObject, + useCallback, + useEffect, + useRef +} from 'react'; import {SliderState} from '@react-stately/slider'; import {useFocusable} from '@react-aria/focus'; import {useLabel} from '@react-aria/label'; @@ -80,32 +94,41 @@ export function useSliderThumb( let {moveProps} = useMove({ onMoveStart() { currentPosition.current = null; - console.log('start', stateRef.current.isThumbDragging(index)); state.setThumbDragging(index, true); }, onMove({deltaX, deltaY, pointerType}) { - let size = isVertical ? trackRef.current.offsetHeight : trackRef.current.offsetWidth; - + let size = isVertical + ? trackRef.current.offsetHeight + : trackRef.current.offsetWidth; + if (currentPosition.current == null) { - currentPosition.current = stateRef.current.getThumbPercent(index) * size; + currentPosition.current = + stateRef.current.getThumbPercent(index) * size; } if (pointerType === 'keyboard') { // (invert left/right according to language direction) + (according to vertical) - let delta = ((reverseX ? -deltaX : deltaX) + (isVertical ? -deltaY : -deltaY)) * stateRef.current.step; + let delta = + ((reverseX ? -deltaX : deltaX) + (isVertical ? -deltaY : -deltaY)) * + stateRef.current.step; currentPosition.current += delta * size; - stateRef.current.setThumbValue(index, stateRef.current.getThumbValue(index) + delta); + stateRef.current.setThumbValue( + index, + stateRef.current.getThumbValue(index) + delta + ); } else { let delta = isVertical ? deltaY : deltaX; if (isVertical || reverseX) { delta = -delta; } - + currentPosition.current += delta; - stateRef.current.setThumbPercent(index, clamp(currentPosition.current / size, 0, 1)); + stateRef.current.setThumbPercent( + index, + clamp(currentPosition.current / size, 0, 1) + ); } }, onMoveEnd() { - console.log('end', stateRef.current.isThumbDragging(index)); state.setThumbDragging(index, false); } }); @@ -130,7 +153,6 @@ export function useSliderThumb( addGlobalListener(window, 'mouseup', onUp, false); addGlobalListener(window, 'touchend', onUp, false); addGlobalListener(window, 'pointerup', onUp, false); - }; let onUp = (e) => { @@ -166,9 +188,8 @@ export function useSliderThumb( state.setThumbValue(index, parseFloat(e.target.value)); } }), - thumbProps: !isDisabled ? mergeProps( - moveProps, - { + thumbProps: !isDisabled + ? mergeProps(moveProps, { onMouseDown: (e: React.MouseEvent) => { if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) { return; @@ -181,9 +202,11 @@ export function useSliderThumb( } onDown(e.pointerId); }, - onTouchStart: (e: React.TouchEvent) => {onDown(e.changedTouches[0].identifier);} - } - ) : {}, + onTouchStart: (e: React.TouchEvent) => { + onDown(e.changedTouches[0].identifier); + } + }) + : {}, labelProps }; } diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 1632d939646..bb506db6035 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -14,38 +14,26 @@ import {useMove} from '@react-aria/interactions'; import {useRef} from 'react'; - export function useTableColumnResize(state): any { const stateRef = useRef(null); stateRef.current = state; - let currentPosition = useRef(null); + let currentPosition = useRef(0); const {moveProps} = useMove({ onMoveStart() { - currentPosition.current = null; - console.log('start resizing - isResizingColumn:', stateRef.current.isResizingColumn()); - stateRef.current.setResizingColumn(true); + currentPosition.current = 0; + stateRef.current.setColumnResizeWidth(0); }, onMove({deltaX}) { - - if (currentPosition.current == null) { - // currentPosition.current = state; - currentPosition.current = 100; - } - + console.log('current resize width: ', currentPosition.current, ' moved from that position: ', deltaX); currentPosition.current += deltaX; - // console.log(currentPosition.current, state); - // setState(currentPosition.current); - // setState(snapValueToStep(currentPosition.current, 75, 500, 1)); + stateRef.current.setColumnResizeWidth(currentPosition.current); }, onMoveEnd() { - console.log('end resizing - isResizingColumn:', stateRef.current.isResizingColumn()); - stateRef.current.setResizingColumn(false); + currentPosition.current = 0; } }); - return { resizerProps: moveProps }; - } diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 282a54535e0..a4c64883a20 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -18,11 +18,28 @@ import {FocusRing, useFocusRing} from '@react-aria/focus'; import {GridNode} from '@react-types/grid'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {layoutInfoToStyle, ScrollView, setScrollLeft, useVirtualizer, VirtualizerItem} from '@react-aria/virtualizer'; +import { + layoutInfoToStyle, + ScrollView, + setScrollLeft, + useVirtualizer, + VirtualizerItem +} from '@react-aria/virtualizer'; import {mergeProps, useLayoutEffect} from '@react-aria/utils'; import {ProgressCircle} from '@react-spectrum/progress'; -import React, {ReactElement, useCallback, useContext, useMemo, useRef, useState} from 'react'; -import {Rect, ReusableView, useVirtualizerState} from '@react-stately/virtualizer'; +import React, { + ReactElement, + useCallback, + useContext, + useMemo, + useRef, + useState +} from 'react'; +import { + Rect, + ReusableView, + useVirtualizerState +} from '@react-stately/virtualizer'; import Resizer from './Resizer'; import {SpectrumColumnProps, SpectrumTableProps} from '@react-types/table'; import styles from '@adobe/spectrum-css-temp/components/table/vars.css'; @@ -34,7 +51,16 @@ import {useHover} from '@react-aria/interactions'; import {useLocale, useMessageFormatter} from '@react-aria/i18n'; import {usePress} from '@react-aria/interactions'; import {useProvider, useProviderProps} from '@react-spectrum/provider'; -import {useTable, useTableCell, useTableColumnHeader, useTableHeaderRow, useTableRow, useTableRowGroup, useTableSelectAllCheckbox, useTableSelectionCheckbox} from '@react-aria/table'; +import { + useTable, + useTableCell, + useTableColumnHeader, + useTableHeaderRow, + useTableRow, + useTableRowGroup, + useTableSelectAllCheckbox, + useTableSelectionCheckbox +} from '@react-aria/table'; import {VisuallyHidden} from '@react-aria/visually-hidden'; const DEFAULT_HEADER_HEIGHT = { @@ -72,22 +98,30 @@ function useTableContext() { return useContext(TableContext); } -function TableView(props: SpectrumTableProps, ref: DOMRef) { +function TableView( + props: SpectrumTableProps, + ref: DOMRef +) { props = useProviderProps(props); let {isQuiet, onAction} = props; let {styleProps} = useStyleProps(props); - let [showSelectionCheckboxes, setShowSelectionCheckboxes] = useState(props.selectionStyle !== 'highlight'); + let [showSelectionCheckboxes, setShowSelectionCheckboxes] = useState( + props.selectionStyle !== 'highlight' + ); let state = useTableState({ ...props, showSelectionCheckboxes, - selectionBehavior: props.selectionStyle === 'highlight' ? 'replace' : 'toggle' + selectionBehavior: + props.selectionStyle === 'highlight' ? 'replace' : 'toggle' }); - console.log('from TableView:', state.isResizingColumn()); + let columnResizeWidth = state.columnResizeWidth(); + console.log('from TableView:', state.columnResizeWidth()); // If the selection behavior changes in state, we need to update showSelectionCheckboxes here due to the circular dependency... - let shouldShowCheckboxes = state.selectionManager.selectionBehavior !== 'replace'; + let shouldShowCheckboxes = + state.selectionManager.selectionBehavior !== 'replace'; if (shouldShowCheckboxes !== showSelectionCheckboxes) { setShowSelectionCheckboxes(shouldShowCheckboxes); } @@ -97,51 +131,65 @@ function TableView(props: SpectrumTableProps, ref: DOMRef new TableLayout({ - // If props.rowHeight is auto, then use estimated heights based on scale, otherwise use fixed heights. - rowHeight: props.overflowMode === 'wrap' - ? null - : ROW_HEIGHTS[density][scale], - estimatedRowHeight: props.overflowMode === 'wrap' - ? ROW_HEIGHTS[density][scale] - : null, - headingHeight: props.overflowMode === 'wrap' - ? null - : DEFAULT_HEADER_HEIGHT[scale], - estimatedHeadingHeight: props.overflowMode === 'wrap' - ? DEFAULT_HEADER_HEIGHT[scale] - : null, - getDefaultWidth: ({hideHeader, isSelectionCell, showDivider}) => { - if (hideHeader) { - let width = DEFAULT_HIDE_HEADER_CELL_WIDTH[scale]; - return showDivider ? width + 1 : width; - } else if (isSelectionCell) { - return SELECTION_CELL_DEFAULT_WIDTH[scale]; - } - } - }), [props.overflowMode, scale, density]); + let layout = useMemo( + () => { + console.log('re-rendering layout'); + return new TableLayout({ + // If props.rowHeight is auto, then use estimated heights based on scale, otherwise use fixed heights. + rowHeight: + props.overflowMode === 'wrap' ? null : ROW_HEIGHTS[density][scale], + estimatedRowHeight: + props.overflowMode === 'wrap' ? ROW_HEIGHTS[density][scale] : null, + headingHeight: + props.overflowMode === 'wrap' ? null : DEFAULT_HEADER_HEIGHT[scale], + estimatedHeadingHeight: + props.overflowMode === 'wrap' ? DEFAULT_HEADER_HEIGHT[scale] : null, + getDefaultWidth: ({hideHeader, isSelectionCell, showDivider}) => { + if (hideHeader) { + let width = DEFAULT_HIDE_HEADER_CELL_WIDTH[scale]; + return showDivider ? width + 1 : width; + } else if (isSelectionCell) { + return SELECTION_CELL_DEFAULT_WIDTH[scale]; + } + }, + columnResizeWidth + }); + }, + [props.overflowMode, scale, density, columnResizeWidth] + ); let {direction} = useLocale(); layout.collection = state.collection; - let {gridProps} = useTable({ - ...props, - isVirtualized: true, - layout - }, state, domRef); + let {gridProps} = useTable( + { + ...props, + isVirtualized: true, + layout + }, + state, + domRef + ); // This overrides collection view's renderWrapper to support DOM heirarchy. type View = ReusableView, unknown>; - let renderWrapper = (parent: View, reusableView: View, children: View[], renderChildren: (views: View[]) => ReactElement[]) => { - let style = layoutInfoToStyle(reusableView.layoutInfo, direction, parent && parent.layoutInfo); + let renderWrapper = ( + parent: View, + reusableView: View, + children: View[], + renderChildren: (views: View[]) => ReactElement[] + ) => { + let style = layoutInfoToStyle( + reusableView.layoutInfo, + direction, + parent && parent.layoutInfo + ); if (style.overflow === 'hidden') { style.overflow = 'visible'; // needed to support position: sticky } if (reusableView.viewType === 'rowgroup') { return ( - + {renderChildren(children)} ); @@ -149,9 +197,7 @@ function TableView(props: SpectrumTableProps, ref: DOMRef + {renderChildren(children)} ); @@ -185,22 +231,18 @@ function TableView(props: SpectrumTableProps, ref: DOMRef + className={classNames( + styles, + 'spectrum-Table-cellWrapper', + classNames(stylesOverrides, { + 'react-spectrum-Table-cellWrapper': !reusableView.layoutInfo + .estimatedSize + }) + )} /> ); }; - let renderView = (tableState?: any) => (type: string, item: GridNode) => { + let renderView = (type: string, item: GridNode) => { switch (type) { case 'header': case 'rowgroup': @@ -240,7 +282,7 @@ function TableView(props: SpectrumTableProps, ref: DOMRef - + ); case 'loader': @@ -248,20 +290,22 @@ function TableView(props: SpectrumTableProps, ref: DOMRef 0 ? formatMessage('loadingMore') : formatMessage('loading')} /> + aria-label={ + state.collection.size > 0 + ? formatMessage('loadingMore') + : formatMessage('loading') + } /> ); case 'empty': { - let emptyState = props.renderEmptyState ? props.renderEmptyState() : null; + let emptyState = props.renderEmptyState + ? props.renderEmptyState() + : null; if (emptyState == null) { return null; } - return ( - - {emptyState} - - ); + return {emptyState}; } } }; @@ -271,25 +315,20 @@ function TableView(props: SpectrumTableProps, ref: DOMRef renderView(state)} + renderView={renderView} renderWrapper={renderWrapper} domRef={domRef} /> @@ -297,7 +336,15 @@ function TableView(props: SpectrumTableProps, ref: DOMRef(); let bodyRef = useRef(); @@ -316,23 +363,27 @@ function TableVirtualizer({layout, collection, focusedKey, renderView, renderWra transitionDuration: isLoading ? 160 : 220 }); - let {virtualizerProps} = useVirtualizer({ - focusedKey, - scrollToItem(key) { - let item = collection.getItem(key); - let column = collection.columns[0]; - state.virtualizer.scrollToItem(key, { - duration: 0, - // Prevent scrolling to the top when clicking on column headers. - shouldScrollY: item?.type !== 'column', - // Offset scroll position by width of selection cell - // (which is sticky and will overlap the cell we're scrolling to). - offsetX: column.props.isSelectionCell - ? layout.columnWidths.get(column.key) - : 0 - }); - } - }, state, domRef); + let {virtualizerProps} = useVirtualizer( + { + focusedKey, + scrollToItem(key) { + let item = collection.getItem(key); + let column = collection.columns[0]; + state.virtualizer.scrollToItem(key, { + duration: 0, + // Prevent scrolling to the top when clicking on column headers. + shouldScrollY: item?.type !== 'column', + // Offset scroll position by width of selection cell + // (which is sticky and will overlap the cell we're scrolling to). + offsetX: column.props.isSelectionCell + ? layout.columnWidths.get(column.key) + : 0 + }); + } + }, + state, + domRef + ); let headerHeight = layout.getLayoutInfo('header')?.rect.height || 0; let visibleRect = state.virtualizer.visibleRect; @@ -342,16 +393,20 @@ function TableVirtualizer({layout, collection, focusedKey, renderView, renderWra headerRef.current.scrollLeft = bodyRef.current.scrollLeft; }, [bodyRef]); - let onVisibleRectChange = useCallback((rect: Rect) => { - state.setVisibleRect(rect); + let onVisibleRectChange = useCallback( + (rect: Rect) => { + state.setVisibleRect(rect); - if (!isLoading && onLoadMore) { - let scrollOffset = state.virtualizer.contentSize.height - rect.height * 2; - if (rect.y > scrollOffset) { - onLoadMore(); + if (!isLoading && onLoadMore) { + let scrollOffset = + state.virtualizer.contentSize.height - rect.height * 2; + if (rect.y > scrollOffset) { + onLoadMore(); + } } - } - }, [onLoadMore, isLoading, state.setVisibleRect, state.virtualizer]); + }, + [onLoadMore, isLoading, state.setVisibleRect, state.virtualizer] + ); useLayoutEffect(() => { if (!isLoading && onLoadMore && !state.isAnimating) { @@ -359,12 +414,16 @@ function TableVirtualizer({layout, collection, focusedKey, renderView, renderWra onLoadMore(); } } - }, [state.contentSize, state.virtualizer, state.isAnimating, onLoadMore, isLoading]); + }, [ + state.contentSize, + state.virtualizer, + state.isAnimating, + onLoadMore, + isLoading + ]); return ( -
+
{state.visibleViews[0]} @@ -383,7 +444,12 @@ function TableVirtualizer({layout, collection, focusedKey, renderView, renderWra role="presentation" className={classNames(styles, 'spectrum-Table-body')} style={{flex: 1}} - innerStyle={{overflow: 'visible', transition: state.isAnimating ? `none ${state.virtualizer.transitionDuration}ms` : undefined}} + innerStyle={{ + overflow: 'visible', + transition: state.isAnimating + ? `none ${state.virtualizer.transitionDuration}ms` + : undefined + }} ref={bodyRef} contentSize={state.contentSize} onVisibleRectChange={onVisibleRectChange} @@ -400,7 +466,10 @@ function TableHeader({children, ...otherProps}) { let {rowGroupProps} = useTableRowGroup(); return ( -
+
{children}
); @@ -409,10 +478,14 @@ function TableHeader({children, ...otherProps}) { function TableColumnHeader({column}) { let ref = useRef(); let state = useTableContext(); - let {columnHeaderProps} = useTableColumnHeader({ - node: column, - isVirtualized: true - }, state, ref); + let {columnHeaderProps} = useTableColumnHeader( + { + node: column, + isVirtualized: true + }, + state, + ref + ); let columnProps = column.props as SpectrumColumnProps; let {hoverProps, isHovered} = useHover({}); @@ -422,35 +495,38 @@ function TableColumnHeader({column}) {
1, - 'react-spectrum-Table-cell--alignEnd': columnProps.align === 'end' - } - ) - ) - }> - {columnProps.hideHeader ? - {column.rendered} : -
{column.rendered}
- } - {columnProps.allowsSorting && - - } - + className={classNames( + styles, + 'spectrum-Table-headCell', + { + 'is-sortable': columnProps.allowsSorting, + 'is-sorted-desc': + state.sortDescriptor?.column === column.key && + state.sortDescriptor?.direction === 'descending', + 'is-sorted-asc': + state.sortDescriptor?.column === column.key && + state.sortDescriptor?.direction === 'ascending', + 'is-hovered': isHovered, + 'spectrum-Table-cell--hideHeader': columnProps.hideHeader + }, + classNames(stylesOverrides, 'react-spectrum-Table-cell', { + 'react-spectrum-Table-cell--alignCenter': + columnProps.align === 'center' || column.colspan > 1, + 'react-spectrum-Table-cell--alignEnd': columnProps.align === 'end' + }) + )}> + {columnProps.hideHeader ? ( + {column.rendered} + ) : ( +
+ {column.rendered} +
+ )} + {columnProps.allowsSorting && ( + + )}
); @@ -460,10 +536,14 @@ function TableSelectAllCell({column}) { let ref = useRef(); let state = useTableContext(); let isSingleSelectionMode = state.selectionManager.selectionMode === 'single'; - let {columnHeaderProps} = useTableColumnHeader({ - node: column, - isVirtualized: true - }, state, ref); + let {columnHeaderProps} = useTableColumnHeader( + { + node: column, + isVirtualized: true + }, + state, + ref + ); let {checkboxProps} = useTableSelectAllCheckbox(state); let {hoverProps, isHovered} = useHover({}); @@ -473,16 +553,14 @@ function TableSelectAllCell({column}) {
+ className={classNames( + styles, + 'spectrum-Table-headCell', + 'spectrum-Table-checkboxCell', + { + 'is-hovered': isHovered + } + )}> { /* In single selection mode, the checkbox will be hidden. @@ -490,14 +568,17 @@ function TableSelectAllCell({column}) { we use a VisuallyHidden component to include the aria-label from the checkbox, which for single selection will be "Select." */ - isSingleSelectionMode && - {checkboxProps['aria-label']} + isSingleSelectionMode && ( + {checkboxProps['aria-label']} + ) }
@@ -517,14 +598,19 @@ function TableRowGroup({children, ...otherProps}) { function TableRow({item, children, onAction, ...otherProps}) { let ref = useRef(); let state = useTableContext(); - let allowsInteraction = state.selectionManager.selectionMode !== 'none' || onAction; + let allowsInteraction = + state.selectionManager.selectionMode !== 'none' || onAction; let isDisabled = !allowsInteraction || state.disabledKeys.has(item.key); let isSelected = state.selectionManager.isSelected(item.key); - let {rowProps} = useTableRow({ - node: item, - isVirtualized: true, - onAction: onAction ? () => onAction(item.key) : null - }, state, ref); + let {rowProps} = useTableRow( + { + node: item, + isVirtualized: true, + onAction: onAction ? () => onAction(item.key) : null + }, + state, + ref + ); let {pressProps, isPressed} = usePress({isDisabled}); @@ -549,21 +635,17 @@ function TableRow({item, children, onAction, ...otherProps}) {
+ className={classNames(styles, 'spectrum-Table-row', { + 'is-active': isPressed, + 'is-selected': isSelected, + 'spectrum-Table-row--highlightSelection': + state.selectionManager.selectionBehavior === 'replace' && + (isSelected || state.selectionManager.isSelected(item.nextKey)), + 'is-focused': isFocusVisibleWithin, + 'focus-ring': isFocusVisible, + 'is-hovered': isHovered, + 'is-disabled': isDisabled + })}> {children}
); @@ -572,7 +654,11 @@ function TableRow({item, children, onAction, ...otherProps}) { function TableHeaderRow({item, children, style}) { let state = useTableContext(); let ref = useRef(); - let {rowProps} = useTableHeaderRow({node: item, isVirtualized: true}, state, ref); + let {rowProps} = useTableHeaderRow( + {node: item, isVirtualized: true}, + state, + ref + ); return (
@@ -585,38 +671,41 @@ function TableCheckboxCell({cell}) { let ref = useRef(); let state = useTableContext(); let isDisabled = state.disabledKeys.has(cell.parentKey); - let {gridCellProps} = useTableCell({ - node: cell, - isVirtualized: true - }, state, ref); + let {gridCellProps} = useTableCell( + { + node: cell, + isVirtualized: true + }, + state, + ref + ); - let {checkboxProps} = useTableSelectionCheckbox({key: cell.parentKey}, state); + let {checkboxProps} = useTableSelectionCheckbox( + {key: cell.parentKey}, + state + ); return (
- {state.selectionManager.selectionMode !== 'none' && + className={classNames( + styles, + 'spectrum-Table-cell', + 'spectrum-Table-checkboxCell', + { + 'is-disabled': isDisabled + }, + classNames(stylesOverrides, 'react-spectrum-Table-cell') + )}> + {state.selectionManager.selectionMode !== 'none' && ( - } + )}
); @@ -627,43 +716,38 @@ function TableCell({cell}) { let ref = useRef(); let columnProps = cell.column.props as SpectrumColumnProps; let isDisabled = state.disabledKeys.has(cell.parentKey); - let {gridCellProps} = useTableCell({ - node: cell, - isVirtualized: true - }, state, ref); + let {gridCellProps} = useTableCell( + { + node: cell, + isVirtualized: true + }, + state, + ref + ); return (
- + className={classNames( + styles, + 'spectrum-Table-cell', + { + 'spectrum-Table-cell--divider': + columnProps.showDivider && cell.column.nextKey !== null, + 'spectrum-Table-cell--hideHeader': columnProps.hideHeader, + 'is-disabled': isDisabled + }, + classNames(stylesOverrides, 'react-spectrum-Table-cell', { + 'react-spectrum-Table-cell--alignStart': + columnProps.align === 'start', + 'react-spectrum-Table-cell--alignCenter': + columnProps.align === 'center', + 'react-spectrum-Table-cell--alignEnd': columnProps.align === 'end' + }) + )}> + {cell.rendered}
@@ -676,8 +760,13 @@ function CenteredWrapper({children}) { return (
+ aria-rowindex={ + state.collection.headerRows.length + state.collection.size + 1 + } + className={classNames( + stylesOverrides, + 'react-spectrum-Table-centeredWrapper' + )}>
{children}
@@ -688,5 +777,7 @@ function CenteredWrapper({children}) { /** * Tables are containers for displaying information. They allow users to quickly scan, sort, compare, and take action on large amounts of data. */ -const _TableView = React.forwardRef(TableView) as (props: SpectrumTableProps & {ref?: DOMRef}) => ReactElement; +const _TableView = React.forwardRef(TableView) as ( + props: SpectrumTableProps & { ref?: DOMRef } +) => ReactElement; export {_TableView as TableView}; diff --git a/packages/@react-stately/layout/src/TableLayout.ts b/packages/@react-stately/layout/src/TableLayout.ts index 99bf5760662..0e059e6d0e6 100644 --- a/packages/@react-stately/layout/src/TableLayout.ts +++ b/packages/@react-stately/layout/src/TableLayout.ts @@ -18,7 +18,8 @@ import {LayoutNode, ListLayout, ListLayoutOptions} from './ListLayout'; type TableLayoutOptions = ListLayoutOptions & { - getDefaultWidth: (props) => string | number + getDefaultWidth: (props) => string | number, + columnResizeWidth: number } export class TableLayout extends ListLayout { @@ -26,6 +27,7 @@ export class TableLayout extends ListLayout { lastCollection: TableCollection; columnWidths: Map; stickyColumnIndices: number[]; + columnResizeWidth: number; getDefaultWidth: (props) => string | number; wasLoading = false; isLoading = false; @@ -33,6 +35,7 @@ export class TableLayout extends ListLayout { constructor(options: TableLayoutOptions) { super(options); this.getDefaultWidth = options.getDefaultWidth; + this.columnResizeWidth = options.columnResizeWidth; } @@ -73,6 +76,10 @@ export class TableLayout extends ListLayout { for (let column of this.collection.columns) { let props = column.props as ColumnProps; let width = props.width ?? this.getDefaultWidth(props); + if (width && this.collection.columns[0] === column) { + // @ts-ignore + width = width + this.columnResizeWidth; + } if (width != null) { let w = this.parseWidth(width); this.columnWidths.set(column.key, w); @@ -90,9 +97,13 @@ export class TableLayout extends ListLayout { // Pass 2: if there are remaining columns, then distribute the remaining space evenly. if (remainingColumns.size > 0) { - let columnWidth = remainingSpace / (this.collection.columns.length - this.columnWidths.size); - + let fakeWidth = remainingSpace / (this.collection.columns.length - this.columnWidths.size); + let columnWidth = fakeWidth; for (let column of remainingColumns) { + if (this.collection.columns[0] === column) { + // @ts-ignore + columnWidth = columnWidth + this.columnResizeWidth; + } let props = column.props as ColumnProps; let minWidth = props.minWidth != null ? this.parseWidth(props.minWidth) : 75; let maxWidth = props.maxWidth != null ? this.parseWidth(props.maxWidth) : Infinity; diff --git a/packages/@react-stately/slider/src/useSliderState.ts b/packages/@react-stately/slider/src/useSliderState.ts index b187495af48..d11f41a0628 100644 --- a/packages/@react-stately/slider/src/useSliderState.ts +++ b/packages/@react-stately/slider/src/useSliderState.ts @@ -140,16 +140,26 @@ interface SliderStateOptions extends SliderProps { * @param props */ export function useSliderState(props: SliderStateOptions): SliderState { - const {isDisabled, minValue = DEFAULT_MIN_VALUE, maxValue = DEFAULT_MAX_VALUE, numberFormatter: formatter, step = DEFAULT_STEP_VALUE} = props; + const { + isDisabled, + minValue = DEFAULT_MIN_VALUE, + maxValue = DEFAULT_MAX_VALUE, + numberFormatter: formatter, + step = DEFAULT_STEP_VALUE + } = props; const [values, setValues] = useControlledState( props.value as any, - props.defaultValue ?? [minValue] as any, + props.defaultValue ?? ([minValue] as any), props.onChange as any ); - const [isDraggings, setDraggings] = useState(new Array(values.length).fill(false)); + const [isDraggings, setDraggings] = useState( + new Array(values.length).fill(false) + ); const isEditablesRef = useRef(new Array(values.length).fill(true)); - const [focusedIndex, setFocusedIndex] = useState(undefined); + const [focusedIndex, setFocusedIndex] = useState( + undefined + ); const valuesRef = useRef(null); valuesRef.current = values; @@ -194,11 +204,19 @@ export function useSliderState(props: SliderStateOptions): SliderState { } const wasDragging = isDraggingsRef.current[index]; - isDraggingsRef.current = replaceIndex(isDraggingsRef.current, index, dragging); + isDraggingsRef.current = replaceIndex( + isDraggingsRef.current, + index, + dragging + ); setDraggings(isDraggingsRef.current); // Call onChangeEnd if no handles are dragging. - if (props.onChangeEnd && wasDragging && !isDraggingsRef.current.some(Boolean)) { + if ( + props.onChangeEnd && + wasDragging && + !isDraggingsRef.current.some(Boolean) + ) { props.onChangeEnd(valuesRef.current); } } diff --git a/packages/@react-stately/table/src/useTableState.ts b/packages/@react-stately/table/src/useTableState.ts index 0f053c47771..762b19d4213 100644 --- a/packages/@react-stately/table/src/useTableState.ts +++ b/packages/@react-stately/table/src/useTableState.ts @@ -10,7 +10,14 @@ * governing permissions and limitations under the License. */ -import {CollectionBase, Node, SelectionMode, Sortable, SortDescriptor, SortDirection} from '@react-types/shared'; +import { + CollectionBase, + Node, + SelectionMode, + Sortable, + SortDescriptor, + SortDirection +} from '@react-types/shared'; import {GridState, useGridState} from '@react-stately/grid'; import {TableCollection as ITableCollection} from '@react-types/table'; import {Key, useMemo, useRef, useState} from 'react'; @@ -29,9 +36,9 @@ export interface TableState extends GridState> { sort(columnKey: Key): void, /** Test that something was resized. */ - isResizingColumn(): boolean, + columnResizeWidth(): number, /** Set the value of isResizing. */ - setResizingColumn(resizing: boolean): void + setColumnResizeWidth(size: number): void } export interface CollectionBuilderContext { @@ -40,7 +47,10 @@ export interface CollectionBuilderContext { columns: Node[] } -export interface TableStateProps extends CollectionBase, MultipleSelectionStateProps, Sortable { +export interface TableStateProps + extends CollectionBase, + MultipleSelectionStateProps, + Sortable { /** Whether the row selection checkboxes should be displayed. */ showSelectionCheckboxes?: boolean } @@ -54,31 +64,40 @@ const OPPOSITE_SORT_DIRECTION = { * Provides state management for a table component. Handles building a collection * of columns and rows from props. In addition, it tracks row selection and manages sort order changes. */ -export function useTableState(props: TableStateProps): TableState { +export function useTableState( + props: TableStateProps +): TableState { let {selectionMode = 'none'} = props; - const [isResizing, setResizing] = useState(false); + const [columnWidth, setColumnWidth] = useState(0); - const isResizingRef = useRef(null); - isResizingRef.current = isResizing; + const columnWidthRef = useRef(null); + columnWidthRef.current = columnWidth; - function updateResizing(resizing: boolean) { - isResizingRef.current = resizing; - setResizing(isResizingRef.current); + function updateColumnWidth(newWidth: number) { + columnWidthRef.current = newWidth; + setColumnWidth(columnWidthRef.current); } - let context = useMemo(() => ({ - showSelectionCheckboxes: props.showSelectionCheckboxes && selectionMode !== 'none', - selectionMode, - columns: [] - }), [props.children, props.showSelectionCheckboxes, selectionMode]); + let context = useMemo( + () => ({ + showSelectionCheckboxes: + props.showSelectionCheckboxes && selectionMode !== 'none', + selectionMode, + columns: [] + }), + [props.children, props.showSelectionCheckboxes, selectionMode] + ); let collection = useCollection>( props, (nodes, prev) => new TableCollection(nodes, prev, context), context ); - let {disabledKeys, selectionManager} = useGridState({...props, collection}); + let {disabledKeys, selectionManager} = useGridState({ + ...props, + collection + }); return { collection, @@ -89,12 +108,13 @@ export function useTableState(props: TableStateProps): Tabl sort(columnKey: Key) { props.onSortChange({ column: columnKey, - direction: props.sortDescriptor?.column === columnKey - ? OPPOSITE_SORT_DIRECTION[props.sortDescriptor.direction] - : 'ascending' + direction: + props.sortDescriptor?.column === columnKey + ? OPPOSITE_SORT_DIRECTION[props.sortDescriptor.direction] + : 'ascending' }); }, - isResizingColumn: () => isResizing, - setResizingColumn: updateResizing + columnResizeWidth: () => columnWidth, + setColumnResizeWidth: updateColumnWidth }; } From 3a4645a6e8628229e5fd8061c45563c2013d1615 Mon Sep 17 00:00:00 2001 From: Marshall Peterson Date: Tue, 11 Jan 2022 10:24:09 -0700 Subject: [PATCH 03/11] add resize algo --- .../table/src/useTableColumnResize.ts | 9 +- .../@react-spectrum/table/src/Resizer.tsx | 6 +- .../@react-spectrum/table/src/TableView.tsx | 37 +++-- .../table/stories/Table.stories.tsx | 50 ++++++- .../@react-stately/layout/src/TableLayout.ts | 136 +++++++++++++++--- 5 files changed, 190 insertions(+), 48 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index bb506db6035..02aa0aff632 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -14,17 +14,18 @@ import {useMove} from '@react-aria/interactions'; import {useRef} from 'react'; -export function useTableColumnResize(state): any { +export function useTableColumnResize(state, layout, item): any { const stateRef = useRef(null); stateRef.current = state; let currentPosition = useRef(0); const {moveProps} = useMove({ onMoveStart() { - currentPosition.current = 0; - stateRef.current.setColumnResizeWidth(0); + const width = layout.columnWidths.get(item.key); + currentPosition.current = width; + stateRef.current.setColumnResizeWidth(width); }, onMove({deltaX}) { - console.log('current resize width: ', currentPosition.current, ' moved from that position: ', deltaX); + // console.log('current resize width: ', currentPosition.current, ' moved from that position: ', deltaX); currentPosition.current += deltaX; stateRef.current.setColumnResizeWidth(currentPosition.current); }, diff --git a/packages/@react-spectrum/table/src/Resizer.tsx b/packages/@react-spectrum/table/src/Resizer.tsx index 2b408eb83f7..1ac3934782d 100644 --- a/packages/@react-spectrum/table/src/Resizer.tsx +++ b/packages/@react-spectrum/table/src/Resizer.tsx @@ -5,10 +5,8 @@ import {useTableColumnResize} from '@react-aria/table/src/useTableColumnResize'; export default function Resizer(props) { - const {state} = props; - let {resizerProps} = useTableColumnResize(state); - - console.log(state); + const {state, layout, item} = props; + let {resizerProps} = useTableColumnResize(state, layout, item); // console.log('from react-spectrum', state.isResizingColumn()); return (
diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index a4c64883a20..90ec564105c 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -117,7 +117,7 @@ function TableView( }); let columnResizeWidth = state.columnResizeWidth(); - console.log('from TableView:', state.columnResizeWidth()); + // console.log('from TableView:', state.columnResizeWidth()); // If the selection behavior changes in state, we need to update showSelectionCheckboxes here due to the circular dependency... let shouldShowCheckboxes = @@ -132,29 +132,26 @@ function TableView( let {scale} = useProvider(); let density = props.density || 'regular'; let layout = useMemo( - () => { - console.log('re-rendering layout'); - return new TableLayout({ + () => new TableLayout({ // If props.rowHeight is auto, then use estimated heights based on scale, otherwise use fixed heights. - rowHeight: + rowHeight: props.overflowMode === 'wrap' ? null : ROW_HEIGHTS[density][scale], - estimatedRowHeight: + estimatedRowHeight: props.overflowMode === 'wrap' ? ROW_HEIGHTS[density][scale] : null, - headingHeight: + headingHeight: props.overflowMode === 'wrap' ? null : DEFAULT_HEADER_HEIGHT[scale], - estimatedHeadingHeight: + estimatedHeadingHeight: props.overflowMode === 'wrap' ? DEFAULT_HEADER_HEIGHT[scale] : null, - getDefaultWidth: ({hideHeader, isSelectionCell, showDivider}) => { - if (hideHeader) { - let width = DEFAULT_HIDE_HEADER_CELL_WIDTH[scale]; - return showDivider ? width + 1 : width; - } else if (isSelectionCell) { - return SELECTION_CELL_DEFAULT_WIDTH[scale]; - } - }, - columnResizeWidth - }); - }, + getDefaultWidth: ({hideHeader, isSelectionCell, showDivider}) => { + if (hideHeader) { + let width = DEFAULT_HIDE_HEADER_CELL_WIDTH[scale]; + return showDivider ? width + 1 : width; + } else if (isSelectionCell) { + return SELECTION_CELL_DEFAULT_WIDTH[scale]; + } + }, + columnResizeWidth + }), [props.overflowMode, scale, density, columnResizeWidth] ); let {direction} = useLocale(); @@ -282,7 +279,7 @@ function TableView( return ( <> - + ); case 'loader': diff --git a/packages/@react-spectrum/table/stories/Table.stories.tsx b/packages/@react-spectrum/table/stories/Table.stories.tsx index bbbb4bd5c1c..c98c6337a74 100644 --- a/packages/@react-spectrum/table/stories/Table.stories.tsx +++ b/packages/@react-spectrum/table/stories/Table.stories.tsx @@ -1022,7 +1022,55 @@ storiesOf('TableView', module) ) ) - .add('table with breadcrumb navigation', () => ); + .add('table with breadcrumb navigation', () => ) + .add( + 'resizable columns', + () => ( + + + File Name + Type + Size + + + + 2018 Proposal + PDF + 214 KB + + + Budget + XLS + 120 KB + + + + ) + ) + .add( + 'resizable columns, flex', + () => ( + + + File Name + Type + Size + + + + 2018 Proposal + PDF + 214 KB + + + Budget + XLS + 120 KB + + + + ) + ); function AsyncLoadingExample() { interface Item { diff --git a/packages/@react-stately/layout/src/TableLayout.ts b/packages/@react-stately/layout/src/TableLayout.ts index 0e059e6d0e6..c497979b337 100644 --- a/packages/@react-stately/layout/src/TableLayout.ts +++ b/packages/@react-stately/layout/src/TableLayout.ts @@ -70,17 +70,19 @@ export class TableLayout extends ListLayout { this.columnWidths = new Map(); this.stickyColumnIndices = []; + // if there was a column resized, set it's width and mark it as static somehow. + // Pass 1: set widths for all explicitly defined columns. let remainingColumns = new Set>(); let remainingSpace = this.virtualizer.visibleRect.width; for (let column of this.collection.columns) { let props = column.props as ColumnProps; let width = props.width ?? this.getDefaultWidth(props); - if (width && this.collection.columns[0] === column) { + if (this.collection.columns[0] === column && this.columnResizeWidth > 0) { // @ts-ignore - width = width + this.columnResizeWidth; + width = this.columnResizeWidth; } - if (width != null) { + if (this.getIsStatic(width)) { let w = this.parseWidth(width); this.columnWidths.set(column.key, w); remainingSpace -= w; @@ -97,25 +99,120 @@ export class TableLayout extends ListLayout { // Pass 2: if there are remaining columns, then distribute the remaining space evenly. if (remainingColumns.size > 0) { - let fakeWidth = remainingSpace / (this.collection.columns.length - this.columnWidths.size); - let columnWidth = fakeWidth; + const remCols = this.getDynamicColumnWidths( + Array.from(remainingColumns), + remainingSpace + ); + let i = 0; for (let column of remainingColumns) { - if (this.collection.columns[0] === column) { - // @ts-ignore - columnWidth = columnWidth + this.columnResizeWidth; - } - let props = column.props as ColumnProps; - let minWidth = props.minWidth != null ? this.parseWidth(props.minWidth) : 75; - let maxWidth = props.maxWidth != null ? this.parseWidth(props.maxWidth) : Infinity; - let width = Math.max(minWidth, Math.min(maxWidth, columnWidth)); - - this.columnWidths.set(column.key, width); - remainingSpace -= width; - if (width !== columnWidth) { - columnWidth = remainingSpace / (this.collection.columns.length - this.columnWidths.size); - } + this.columnWidths.set(column.key, remCols[i].columnWidth); + i++; } + + + // let fakeWidth = remainingSpace / (this.collection.columns.length - this.columnWidths.size); + // let columnWidth = fakeWidth; + // for (let column of remainingColumns) { + // if (this.collection.columns[0] === column) { + // // @ts-ignore + // columnWidth = columnWidth + this.columnResizeWidth; + // } + // let props = column.props as ColumnProps; + // let minWidth = props.minWidth != null ? this.parseWidth(props.minWidth) : 75; + // let maxWidth = props.maxWidth != null ? this.parseWidth(props.maxWidth) : Infinity; + // let width = Math.max(minWidth, Math.min(maxWidth, columnWidth)); + + // this.columnWidths.set(column.key, width); + // remainingSpace -= width; + // if (width !== columnWidth) { + // columnWidth = remainingSpace / (this.collection.columns.length - this.columnWidths.size); + // } + // } + } + + } + + getIsStatic(width: number | string): boolean { + return ( + width != null && (typeof width === 'number' || width.match(/^(\d+)%$/) !== null) + ); + } + + getDynamicColumnWidths(remainingColumns, remainingSpace) { + let columns = this.mapColumns(remainingColumns, remainingSpace); + + columns.sort((a, b) => b.delta - a.delta); + columns = this.solveWidths(columns, remainingSpace); + columns.sort((a, b) => a.index - b.index); + + return columns; + } + + mapColumns(remainingColumns, remainingSpace) { + let remainingFractions = remainingColumns.reduce( + (sum, column) => sum + this.parseFractionalUnit(column.props.width), + 0 + ); + + let columns = [...remainingColumns].map((column, index) => { + const targetWidth = + (this.parseFractionalUnit(column.props.width) * remainingSpace) / remainingFractions; + + return { + ...column, + index, + delta: Math.max( + 0, + this.getMinWidth(column.minWidth) - targetWidth, + targetWidth - this.getMaxWidth(column.maxWidth) + ) + }; + }); + + return columns; + } + + solveWidths(remainingColumns, remainingSpace) { + let remainingFractions = remainingColumns.reduce( + (sum, col) => sum + this.parseFractionalUnit(col.props.width), + 0 + ); + + for (let i = 0; i < remainingColumns.length; i++) { + const column = remainingColumns[i]; + + const targetWidth = + (this.parseFractionalUnit(column.props.width) * remainingSpace) / remainingFractions; + + let width = Math.max( + this.getMinWidth(column.minWidth), + Math.min(targetWidth, this.getMaxWidth(column.maxWidth)) + ); + column.columnWidth = width; + remainingSpace -= width; + remainingFractions -= this.parseFractionalUnit(column.props.width); } + + return remainingColumns; + } + + parseFractionalUnit(width: string): number { + if (!width) { + return 1; + } + return parseInt(width.match(/(?<=^flex-)(\d+)/g)[0], 10); + } + + getMinWidth(minWidth: number | string): number { + return minWidth !== undefined && minWidth !== null + ? this.parseWidth(minWidth) + : 75; + } + + getMaxWidth(maxWidth: number | string): number { + return maxWidth !== undefined && maxWidth !== null + ? this.parseWidth(maxWidth) + : Infinity; } parseWidth(width: number | string): number { @@ -194,6 +291,7 @@ export class TableLayout extends ListLayout { } } + // used to get the column widths when rendering to the DOM getColumnWidth(node: GridNode) { let colspan = node.colspan ?? 1; let width = 0; From e8dda059f04a2f92b657c4e25412cfcf9d4fd236 Mon Sep 17 00:00:00 2001 From: Marshall Peterson Date: Thu, 13 Jan 2022 10:57:08 -0700 Subject: [PATCH 04/11] resize any column, not just first --- .../table/src/useTableColumnResize.ts | 9 +++++- .../@react-spectrum/table/src/TableView.tsx | 8 +++-- .../@react-stately/layout/src/TableLayout.ts | 31 +++++-------------- .../@react-stately/table/src/useTableState.ts | 20 ++++++++++-- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 02aa0aff632..520c7b716c8 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -18,16 +18,23 @@ export function useTableColumnResize(state, layout, item): any { const stateRef = useRef(null); stateRef.current = state; let currentPosition = useRef(0); + let prevColumns = useRef<{key: string, width: number}[]>([]); const {moveProps} = useMove({ onMoveStart() { const width = layout.columnWidths.get(item.key); currentPosition.current = width; - stateRef.current.setColumnResizeWidth(width); + const columns = layout.collection.columns; + prevColumns.current = columns.slice(0, columns.findIndex(column => column === item) + 1).map(column => ({key: column.key, width: layout.columnWidths.get(column.key)})); + + // let prevColumns = layout.collection.columns.slice(0, ); + // stateRef.current.setColumnResizeWidth(width); }, onMove({deltaX}) { // console.log('current resize width: ', currentPosition.current, ' moved from that position: ', deltaX); currentPosition.current += deltaX; + prevColumns.current[prevColumns.current.length - 1].width = currentPosition.current; stateRef.current.setColumnResizeWidth(currentPosition.current); + stateRef.current.setResizeColumns(prevColumns.current); }, onMoveEnd() { currentPosition.current = 0; diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 90ec564105c..9e48aa4625d 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -117,7 +117,8 @@ function TableView( }); let columnResizeWidth = state.columnResizeWidth(); - // console.log('from TableView:', state.columnResizeWidth()); + let resizeColumns = state.resizeColumns(); + // If the selection behavior changes in state, we need to update showSelectionCheckboxes here due to the circular dependency... let shouldShowCheckboxes = @@ -150,9 +151,10 @@ function TableView( return SELECTION_CELL_DEFAULT_WIDTH[scale]; } }, - columnResizeWidth + columnResizeWidth, + resizeColumns }), - [props.overflowMode, scale, density, columnResizeWidth] + [props.overflowMode, scale, density, columnResizeWidth, resizeColumns] ); let {direction} = useLocale(); layout.collection = state.collection; diff --git a/packages/@react-stately/layout/src/TableLayout.ts b/packages/@react-stately/layout/src/TableLayout.ts index c497979b337..c151b1ea9df 100644 --- a/packages/@react-stately/layout/src/TableLayout.ts +++ b/packages/@react-stately/layout/src/TableLayout.ts @@ -19,7 +19,8 @@ import {LayoutNode, ListLayout, ListLayoutOptions} from './ListLayout'; type TableLayoutOptions = ListLayoutOptions & { getDefaultWidth: (props) => string | number, - columnResizeWidth: number + columnResizeWidth: number, + resizeColumns: {key: string, width: number}[] } export class TableLayout extends ListLayout { @@ -28,6 +29,7 @@ export class TableLayout extends ListLayout { columnWidths: Map; stickyColumnIndices: number[]; columnResizeWidth: number; + resizeColumns: {key: string, width: number}[]; getDefaultWidth: (props) => string | number; wasLoading = false; isLoading = false; @@ -36,6 +38,7 @@ export class TableLayout extends ListLayout { super(options); this.getDefaultWidth = options.getDefaultWidth; this.columnResizeWidth = options.columnResizeWidth; + this.resizeColumns = options.resizeColumns; } @@ -78,9 +81,9 @@ export class TableLayout extends ListLayout { for (let column of this.collection.columns) { let props = column.props as ColumnProps; let width = props.width ?? this.getDefaultWidth(props); - if (this.collection.columns[0] === column && this.columnResizeWidth > 0) { - // @ts-ignore - width = this.columnResizeWidth; + + if (this.resizeColumns.findIndex(col => col.key === column.key) !== -1) { + width = this.resizeColumns.find(col => col.key === column.key).width; } if (this.getIsStatic(width)) { let w = this.parseWidth(width); @@ -108,26 +111,6 @@ export class TableLayout extends ListLayout { this.columnWidths.set(column.key, remCols[i].columnWidth); i++; } - - - // let fakeWidth = remainingSpace / (this.collection.columns.length - this.columnWidths.size); - // let columnWidth = fakeWidth; - // for (let column of remainingColumns) { - // if (this.collection.columns[0] === column) { - // // @ts-ignore - // columnWidth = columnWidth + this.columnResizeWidth; - // } - // let props = column.props as ColumnProps; - // let minWidth = props.minWidth != null ? this.parseWidth(props.minWidth) : 75; - // let maxWidth = props.maxWidth != null ? this.parseWidth(props.maxWidth) : Infinity; - // let width = Math.max(minWidth, Math.min(maxWidth, columnWidth)); - - // this.columnWidths.set(column.key, width); - // remainingSpace -= width; - // if (width !== columnWidth) { - // columnWidth = remainingSpace / (this.collection.columns.length - this.columnWidths.size); - // } - // } } } diff --git a/packages/@react-stately/table/src/useTableState.ts b/packages/@react-stately/table/src/useTableState.ts index 762b19d4213..086cd2aa9f9 100644 --- a/packages/@react-stately/table/src/useTableState.ts +++ b/packages/@react-stately/table/src/useTableState.ts @@ -38,7 +38,12 @@ export interface TableState extends GridState> { /** Test that something was resized. */ columnResizeWidth(): number, /** Set the value of isResizing. */ - setColumnResizeWidth(size: number): void + setColumnResizeWidth(size: number): void, + + /** Colums that are set by the resize. */ + resizeColumns(): {key: string, width: number}[], + /** Set the columns that are fixed by the resize. */ + setResizeColumns(columns: {key: string, width: number}[]): void } export interface CollectionBuilderContext { @@ -70,15 +75,24 @@ export function useTableState( let {selectionMode = 'none'} = props; const [columnWidth, setColumnWidth] = useState(0); + const [resizedColumns, setResizedColumns] = useState<{key: string, width: number}[]>([]); const columnWidthRef = useRef(null); columnWidthRef.current = columnWidth; + const resizedColumnsRef = useRef<{key: string, width: number}[]>(null); + resizedColumnsRef.current = resizedColumns; + function updateColumnWidth(newWidth: number) { columnWidthRef.current = newWidth; setColumnWidth(columnWidthRef.current); } + function updateResizedColumns(columns: {key: string, width: number}[]) { + resizedColumnsRef.current = columns; + setResizedColumns(resizedColumnsRef.current); + } + let context = useMemo( () => ({ showSelectionCheckboxes: @@ -115,6 +129,8 @@ export function useTableState( }); }, columnResizeWidth: () => columnWidth, - setColumnResizeWidth: updateColumnWidth + setColumnResizeWidth: updateColumnWidth, + resizeColumns: () => resizedColumns, + setResizeColumns: updateResizedColumns }; } From b5fcedc343385716f6095acea5bbfb97be68e5cd Mon Sep 17 00:00:00 2001 From: Marshall Peterson Date: Fri, 14 Jan 2022 14:13:31 -0700 Subject: [PATCH 05/11] column resize working --- .../table/src/useTableColumnResize.ts | 20 +--- .../@react-spectrum/table/src/TableView.tsx | 15 ++- .../@react-stately/layout/src/TableLayout.ts | 51 +++++++--- packages/@react-stately/table/package.json | 1 + .../@react-stately/table/src/useTableState.ts | 92 ++++++++++++++----- 5 files changed, 120 insertions(+), 59 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 520c7b716c8..5d963c20417 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -17,27 +17,17 @@ import {useRef} from 'react'; export function useTableColumnResize(state, layout, item): any { const stateRef = useRef(null); stateRef.current = state; - let currentPosition = useRef(0); - let prevColumns = useRef<{key: string, width: number}[]>([]); const {moveProps} = useMove({ onMoveStart() { - const width = layout.columnWidths.get(item.key); - currentPosition.current = width; - const columns = layout.collection.columns; - prevColumns.current = columns.slice(0, columns.findIndex(column => column === item) + 1).map(column => ({key: column.key, width: layout.columnWidths.get(column.key)})); - - // let prevColumns = layout.collection.columns.slice(0, ); - // stateRef.current.setColumnResizeWidth(width); + stateRef.current.setCurrentResizeColumn(item.key); + stateRef.current.addResizedColumn(item.key); }, onMove({deltaX}) { - // console.log('current resize width: ', currentPosition.current, ' moved from that position: ', deltaX); - currentPosition.current += deltaX; - prevColumns.current[prevColumns.current.length - 1].width = currentPosition.current; - stateRef.current.setColumnResizeWidth(currentPosition.current); - stateRef.current.setResizeColumns(prevColumns.current); + stateRef.current.setResizeDelta(deltaX); }, onMoveEnd() { - currentPosition.current = 0; + stateRef.current.setCurrentResizeColumn(); + stateRef.current.setResizeDelta(0); } }); diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 7850592cccd..1df948b9744 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -116,8 +116,9 @@ function TableView( props.selectionStyle === 'highlight' ? 'replace' : 'toggle' }); - let columnResizeWidth = state.columnResizeWidth(); - let resizeColumns = state.resizeColumns(); + let columnWidths = state.columnWidths(); + let currentResizeColumn = state.currentResizeColumn(); + let resizeDelta = state.resizeDelta(); // If the selection behavior changes in state, we need to update showSelectionCheckboxes here due to the circular dependency... @@ -151,10 +152,14 @@ function TableView( return SELECTION_CELL_DEFAULT_WIDTH[scale]; } }, - columnResizeWidth, - resizeColumns + columnWidths, + getColumnWidth: state.getColumnWidth, + setColumnWidth: state.setColumnWidth, + hasResizedColumn: state.hasResizedColumn, + currentResizeColumn, + resizeDelta }), - [props.overflowMode, scale, density, columnResizeWidth, resizeColumns] + [props.overflowMode, scale, density, resizeDelta] ); let {direction} = useLocale(); layout.collection = state.collection; diff --git a/packages/@react-stately/layout/src/TableLayout.ts b/packages/@react-stately/layout/src/TableLayout.ts index c151b1ea9df..1aceb2921b3 100644 --- a/packages/@react-stately/layout/src/TableLayout.ts +++ b/packages/@react-stately/layout/src/TableLayout.ts @@ -19,26 +19,39 @@ import {LayoutNode, ListLayout, ListLayoutOptions} from './ListLayout'; type TableLayoutOptions = ListLayoutOptions & { getDefaultWidth: (props) => string | number, - columnResizeWidth: number, - resizeColumns: {key: string, width: number}[] + columnWidths: Map, + getColumnWidth: (key: Key) => number, + setColumnWidth: (key: Key, width: number) => void, + hasResizedColumn: (key: Key) => boolean, + currentResizeColumn: Key, + resizeDelta: number } export class TableLayout extends ListLayout { collection: TableCollection; lastCollection: TableCollection; columnWidths: Map; + columnWidthsRef: Map; stickyColumnIndices: number[]; - columnResizeWidth: number; - resizeColumns: {key: string, width: number}[]; getDefaultWidth: (props) => string | number; + getColumnWidth_: (key: Key) => number; + setColumnWidth: (key: Key, width: number) => void; + hasResizedColumn: (key: Key) => boolean; + currentResizeColumn: Key; + resizeDelta: number; wasLoading = false; isLoading = false; constructor(options: TableLayoutOptions) { super(options); this.getDefaultWidth = options.getDefaultWidth; - this.columnResizeWidth = options.columnResizeWidth; - this.resizeColumns = options.resizeColumns; + this.columnWidths = options.columnWidths; + this.getColumnWidth_ = options.getColumnWidth; + this.setColumnWidth = options.setColumnWidth; + this.hasResizedColumn = options.hasResizedColumn; + this.currentResizeColumn = options.currentResizeColumn; + this.columnWidthsRef = this.columnWidths; + this.resizeDelta = options.resizeDelta; } @@ -58,6 +71,7 @@ export class TableLayout extends ListLayout { this.wasLoading = this.isLoading; this.isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; + // TODO: switch this to only pass in the columns that we want to calculate this.buildColumnWidths(); let header = this.buildHeader(); let body = this.buildBody(0); @@ -70,7 +84,6 @@ export class TableLayout extends ListLayout { } buildColumnWidths() { - this.columnWidths = new Map(); this.stickyColumnIndices = []; // if there was a column resized, set it's width and mark it as static somehow. @@ -78,16 +91,22 @@ export class TableLayout extends ListLayout { // Pass 1: set widths for all explicitly defined columns. let remainingColumns = new Set>(); let remainingSpace = this.virtualizer.visibleRect.width; + let isAfterResizeColumn = this.currentResizeColumn === null; for (let column of this.collection.columns) { let props = column.props as ColumnProps; - let width = props.width ?? this.getDefaultWidth(props); - - if (this.resizeColumns.findIndex(col => col.key === column.key) !== -1) { - width = this.resizeColumns.find(col => col.key === column.key).width; + let width; + if (!isAfterResizeColumn) { + width = this.getColumnWidth_(column.key); + if (column.key === this.currentResizeColumn) { + width += this.resizeDelta; + } + } else { + width = this.hasResizedColumn(column.key) ? this.getColumnWidth_(column.key) : props.width ?? this.getDefaultWidth(props); } if (this.getIsStatic(width)) { let w = this.parseWidth(width); - this.columnWidths.set(column.key, w); + this.columnWidthsRef.set(column.key, w); + this.setColumnWidth(column.key, w); remainingSpace -= w; } else { remainingColumns.add(column); @@ -98,6 +117,8 @@ export class TableLayout extends ListLayout { if (column.props.isSelectionCell || this.collection.rowHeaderColumnKeys.has(column.key)) { this.stickyColumnIndices.push(column.index); } + + isAfterResizeColumn = isAfterResizeColumn || column.key === this.currentResizeColumn; } // Pass 2: if there are remaining columns, then distribute the remaining space evenly. @@ -108,7 +129,8 @@ export class TableLayout extends ListLayout { ); let i = 0; for (let column of remainingColumns) { - this.columnWidths.set(column.key, remCols[i].columnWidth); + this.columnWidthsRef.set(column.key, remCols[i].columnWidth); + this.setColumnWidth(column.key, remCols[i].columnWidth); i++; } } @@ -280,7 +302,8 @@ export class TableLayout extends ListLayout { let width = 0; for (let i = 0; i < colspan; i++) { let column = this.collection.columns[node.index + i]; - width += this.columnWidths.get(column.key); + // width += this.columnWidths.get(column.key); + width += this.getColumnWidth_(column.key); } return width; diff --git a/packages/@react-stately/table/package.json b/packages/@react-stately/table/package.json index ea894ecf443..d0184ab5790 100644 --- a/packages/@react-stately/table/package.json +++ b/packages/@react-stately/table/package.json @@ -21,6 +21,7 @@ "@react-stately/collections": "^3.3.3", "@react-stately/grid": "^3.1.0", "@react-stately/selection": "^3.8.0", + "@react-stately/utils": "^3.3.0", "@react-types/grid": "^3.0.0", "@react-types/shared": "^3.10.0", "@react-types/table": "^3.1.0" diff --git a/packages/@react-stately/table/src/useTableState.ts b/packages/@react-stately/table/src/useTableState.ts index 086cd2aa9f9..351c016de2a 100644 --- a/packages/@react-stately/table/src/useTableState.ts +++ b/packages/@react-stately/table/src/useTableState.ts @@ -35,15 +35,18 @@ export interface TableState extends GridState> { /** Calls the provided onSortChange handler with the provided column key and sort direction. */ sort(columnKey: Key): void, - /** Test that something was resized. */ - columnResizeWidth(): number, - /** Set the value of isResizing. */ - setColumnResizeWidth(size: number): void, - - /** Colums that are set by the resize. */ - resizeColumns(): {key: string, width: number}[], - /** Set the columns that are fixed by the resize. */ - setResizeColumns(columns: {key: string, width: number}[]): void + columnWidths(): Map, + getColumnWidth(key: Key): number, + setColumnWidth(key: Key, width: number), + + hasResizedColumn(key: Key): boolean, + addResizedColumn(key: Key), + + currentResizeColumn(): Key, + setCurrentResizeColumn(key: Key), + + resizeDelta(): number, + setResizeDelta(deltaX: number) } export interface CollectionBuilderContext { @@ -74,23 +77,57 @@ export function useTableState( ): TableState { let {selectionMode = 'none'} = props; - const [columnWidth, setColumnWidth] = useState(0); - const [resizedColumns, setResizedColumns] = useState<{key: string, width: number}[]>([]); + // map of the columns and their width, key is the column key, value is the width + // TODO: switch to useControlledState + const [columnWidths, setColumnWidths] = useState>(new Map()); + // set of all the column keys that have been resized + const [resizedColumns, setResizedColumns] = useState>(new Set()); + // current column key that is being resized + const [currentResizeColumn, setCurrentResizeColumn] = useState(null); + // resize delta + const [resizeDelta, setResizeDelta] = useState(0); + + + // map of the columns and their width, key is the column key, value is the width + const columnWidthsRef = useRef>(null); + columnWidthsRef.current = columnWidths; + const resizedColumnsRef = useRef>(null); + resizedColumnsRef.current = resizedColumns; + const currentResizeColumnRef = useRef(null); + currentResizeColumnRef.current = currentResizeColumn; + const resizeDeltaRef = useRef(null); + resizeDeltaRef.current = resizeDelta; - const columnWidthRef = useRef(null); - columnWidthRef.current = columnWidth; + function getColumnWidth(key: Key): number { + return columnWidths.get(key); + } - const resizedColumnsRef = useRef<{key: string, width: number}[]>(null); - resizedColumnsRef.current = resizedColumns; + function setColumnWidthNew(key: Key, width: number) { + columnWidthsRef.current.set(key, width); + // new map so that change detection is triggered + setColumnWidths(new Map(columnWidthsRef.current)); + } + + function hasResizedColumn(key: Key): boolean { + return resizedColumns.has(key); + } + + function addResizedColumn(key: Key) { + if (resizedColumnsRef.current.has(key)) { + return; + } + resizedColumnsRef.current.add(key); + setResizedColumns(new Set(resizedColumnsRef.current)); + } - function updateColumnWidth(newWidth: number) { - columnWidthRef.current = newWidth; - setColumnWidth(columnWidthRef.current); + function updatedCurrentResizeColumn(key: Key) { + currentResizeColumnRef.current = key; + setCurrentResizeColumn(currentResizeColumnRef.current); } - function updateResizedColumns(columns: {key: string, width: number}[]) { - resizedColumnsRef.current = columns; - setResizedColumns(resizedColumnsRef.current); + function updateResizeDelta(deltaX: number) { + resizeDeltaRef.current = deltaX; + setResizeDelta(resizeDeltaRef.current); } let context = useMemo( @@ -128,9 +165,14 @@ export function useTableState( : 'ascending' }); }, - columnResizeWidth: () => columnWidth, - setColumnResizeWidth: updateColumnWidth, - resizeColumns: () => resizedColumns, - setResizeColumns: updateResizedColumns + columnWidths: () => columnWidths, + getColumnWidth, + setColumnWidth: setColumnWidthNew, + hasResizedColumn, + addResizedColumn, + currentResizeColumn: () => currentResizeColumn, + setCurrentResizeColumn: updatedCurrentResizeColumn, + resizeDelta: () => resizeDelta, + setResizeDelta: updateResizeDelta }; } From 5c562b971d469382659eaa9fa0af3d7352200110 Mon Sep 17 00:00:00 2001 From: Marshall Peterson Date: Fri, 21 Jan 2022 14:16:27 -0700 Subject: [PATCH 06/11] support allowsResizing prop --- packages/@react-spectrum/table/src/TableView.tsx | 2 +- .../@react-spectrum/table/stories/Table.stories.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 1df948b9744..92d322ffc9d 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -283,7 +283,7 @@ function TableView( return ( <> - + {item.props.allowsResizing && } ); case 'loader': diff --git a/packages/@react-spectrum/table/stories/Table.stories.tsx b/packages/@react-spectrum/table/stories/Table.stories.tsx index b8d6519ed63..b5597a5c61b 100644 --- a/packages/@react-spectrum/table/stories/Table.stories.tsx +++ b/packages/@react-spectrum/table/stories/Table.stories.tsx @@ -1029,8 +1029,8 @@ storiesOf('TableView', module) File Name - Type - Size + Type + Size @@ -1052,9 +1052,9 @@ storiesOf('TableView', module) () => ( - File Name - Type - Size + File Name + Type + Size From 28cd3ba328b842f02c63e67c2f5feeefd225612f Mon Sep 17 00:00:00 2001 From: Danny North Date: Fri, 21 Jan 2022 15:08:54 -0700 Subject: [PATCH 07/11] Fixed issue where mouse was not correctly tracking column sizing --- packages/@react-aria/table/src/useTableColumnResize.ts | 8 +++++++- packages/@react-spectrum/table/src/TableView.tsx | 1 - packages/@react-stately/layout/src/TableLayout.ts | 2 +- packages/@react-stately/table/src/useTableState.ts | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 5d963c20417..405a44f09fa 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -17,17 +17,23 @@ import {useRef} from 'react'; export function useTableColumnResize(state, layout, item): any { const stateRef = useRef(null); stateRef.current = state; + + const columnResizeWidthRef = useRef(null); const {moveProps} = useMove({ onMoveStart() { stateRef.current.setCurrentResizeColumn(item.key); stateRef.current.addResizedColumn(item.key); + columnResizeWidthRef.current = stateRef.current.getColumnWidth(item.key); }, onMove({deltaX}) { - stateRef.current.setResizeDelta(deltaX); + columnResizeWidthRef.current += deltaX; + let widthRespectingBoundaries = Math.max(item.props.minWidth || 75, Math.min(columnResizeWidthRef.current, item.props.maxWidth || Infinity)); + stateRef.current.setResizeDelta(widthRespectingBoundaries); }, onMoveEnd() { stateRef.current.setCurrentResizeColumn(); stateRef.current.setResizeDelta(0); + columnResizeWidthRef.current = 0; } }); diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 92d322ffc9d..1bec015d61f 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -279,7 +279,6 @@ function TableView( ); } - return ( <> diff --git a/packages/@react-stately/layout/src/TableLayout.ts b/packages/@react-stately/layout/src/TableLayout.ts index 1aceb2921b3..0318f30ff14 100644 --- a/packages/@react-stately/layout/src/TableLayout.ts +++ b/packages/@react-stately/layout/src/TableLayout.ts @@ -98,7 +98,7 @@ export class TableLayout extends ListLayout { if (!isAfterResizeColumn) { width = this.getColumnWidth_(column.key); if (column.key === this.currentResizeColumn) { - width += this.resizeDelta; + width = this.resizeDelta; } } else { width = this.hasResizedColumn(column.key) ? this.getColumnWidth_(column.key) : props.width ?? this.getDefaultWidth(props); diff --git a/packages/@react-stately/table/src/useTableState.ts b/packages/@react-stately/table/src/useTableState.ts index 351c016de2a..967954c3bd0 100644 --- a/packages/@react-stately/table/src/useTableState.ts +++ b/packages/@react-stately/table/src/useTableState.ts @@ -99,7 +99,7 @@ export function useTableState( resizeDeltaRef.current = resizeDelta; function getColumnWidth(key: Key): number { - return columnWidths.get(key); + return columnWidthsRef.current.get(key); } function setColumnWidthNew(key: Key, width: number) { From 6146c8c4604ede0d1e92ad2c88f4f310689f7ef4 Mon Sep 17 00:00:00 2001 From: Danny North Date: Mon, 24 Jan 2022 12:06:34 -0700 Subject: [PATCH 08/11] use defaultWidth for uncontrolled resizeable columns --- .../table/stories/Table.stories.tsx | 4 ++-- .../@react-stately/layout/src/TableLayout.ts | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/@react-spectrum/table/stories/Table.stories.tsx b/packages/@react-spectrum/table/stories/Table.stories.tsx index b5597a5c61b..86c402efe64 100644 --- a/packages/@react-spectrum/table/stories/Table.stories.tsx +++ b/packages/@react-spectrum/table/stories/Table.stories.tsx @@ -1048,13 +1048,13 @@ storiesOf('TableView', module) ) ) .add( - 'resizable columns, flex', + 'resizable columns, uncontrolled, flex', () => ( File Name Type - Size + Size diff --git a/packages/@react-stately/layout/src/TableLayout.ts b/packages/@react-stately/layout/src/TableLayout.ts index 0318f30ff14..4bec2f9d9ea 100644 --- a/packages/@react-stately/layout/src/TableLayout.ts +++ b/packages/@react-stately/layout/src/TableLayout.ts @@ -95,13 +95,15 @@ export class TableLayout extends ListLayout { for (let column of this.collection.columns) { let props = column.props as ColumnProps; let width; - if (!isAfterResizeColumn) { + if (props.width) { + width = props.width; + } else if (!isAfterResizeColumn) { width = this.getColumnWidth_(column.key); if (column.key === this.currentResizeColumn) { width = this.resizeDelta; } } else { - width = this.hasResizedColumn(column.key) ? this.getColumnWidth_(column.key) : props.width ?? this.getDefaultWidth(props); + width = this.hasResizedColumn(column.key) ? this.getColumnWidth_(column.key) : props.defaultWidth ?? this.getDefaultWidth(props); } if (this.getIsStatic(width)) { let w = this.parseWidth(width); @@ -155,13 +157,13 @@ export class TableLayout extends ListLayout { mapColumns(remainingColumns, remainingSpace) { let remainingFractions = remainingColumns.reduce( - (sum, column) => sum + this.parseFractionalUnit(column.props.width), + (sum, column) => sum + this.parseFractionalUnit(column.props.defaultWidth), 0 ); let columns = [...remainingColumns].map((column, index) => { const targetWidth = - (this.parseFractionalUnit(column.props.width) * remainingSpace) / remainingFractions; + (this.parseFractionalUnit(column.props.defaultWidth) * remainingSpace) / remainingFractions; return { ...column, @@ -179,7 +181,7 @@ export class TableLayout extends ListLayout { solveWidths(remainingColumns, remainingSpace) { let remainingFractions = remainingColumns.reduce( - (sum, col) => sum + this.parseFractionalUnit(col.props.width), + (sum, col) => sum + this.parseFractionalUnit(col.props.defaultWidth), 0 ); @@ -187,7 +189,7 @@ export class TableLayout extends ListLayout { const column = remainingColumns[i]; const targetWidth = - (this.parseFractionalUnit(column.props.width) * remainingSpace) / remainingFractions; + (this.parseFractionalUnit(column.props.defaultWidth) * remainingSpace) / remainingFractions; let width = Math.max( this.getMinWidth(column.minWidth), @@ -195,7 +197,7 @@ export class TableLayout extends ListLayout { ); column.columnWidth = width; remainingSpace -= width; - remainingFractions -= this.parseFractionalUnit(column.props.width); + remainingFractions -= this.parseFractionalUnit(column.props.defaultWidth); } return remainingColumns; From 1499f75cd25b36c00465bff29f1a2edb75d42c8a Mon Sep 17 00:00:00 2001 From: Marshall Peterson Date: Tue, 25 Jan 2022 11:59:44 -0700 Subject: [PATCH 09/11] clean up unused props and only calculate widths of affected columns --- .../table/src/useTableColumnResize.ts | 6 +++--- .../@react-spectrum/table/src/Resizer.tsx | 5 ++--- .../table/stories/Table.stories.tsx | 2 +- .../@react-stately/layout/src/TableLayout.ts | 20 +++++++------------ 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 405a44f09fa..ec6b1d439ed 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -14,7 +14,7 @@ import {useMove} from '@react-aria/interactions'; import {useRef} from 'react'; -export function useTableColumnResize(state, layout, item): any { +export function useTableColumnResize(state, item): any { const stateRef = useRef(null); stateRef.current = state; @@ -29,10 +29,10 @@ export function useTableColumnResize(state, layout, item): any { columnResizeWidthRef.current += deltaX; let widthRespectingBoundaries = Math.max(item.props.minWidth || 75, Math.min(columnResizeWidthRef.current, item.props.maxWidth || Infinity)); stateRef.current.setResizeDelta(widthRespectingBoundaries); + stateRef.current.setColumnWidth(item.key, widthRespectingBoundaries); }, onMoveEnd() { - stateRef.current.setCurrentResizeColumn(); - stateRef.current.setResizeDelta(0); + stateRef.current.setCurrentResizeColumn(); columnResizeWidthRef.current = 0; } }); diff --git a/packages/@react-spectrum/table/src/Resizer.tsx b/packages/@react-spectrum/table/src/Resizer.tsx index 1ac3934782d..9579f6e1b14 100644 --- a/packages/@react-spectrum/table/src/Resizer.tsx +++ b/packages/@react-spectrum/table/src/Resizer.tsx @@ -5,9 +5,8 @@ import {useTableColumnResize} from '@react-aria/table/src/useTableColumnResize'; export default function Resizer(props) { - const {state, layout, item} = props; - let {resizerProps} = useTableColumnResize(state, layout, item); - // console.log('from react-spectrum', state.isResizingColumn()); + const {state, item} = props; + let {resizerProps} = useTableColumnResize(state, item); return (
); diff --git a/packages/@react-spectrum/table/stories/Table.stories.tsx b/packages/@react-spectrum/table/stories/Table.stories.tsx index 86c402efe64..513b5808ec6 100644 --- a/packages/@react-spectrum/table/stories/Table.stories.tsx +++ b/packages/@react-spectrum/table/stories/Table.stories.tsx @@ -1028,7 +1028,7 @@ storiesOf('TableView', module) () => ( - File Name + File Name Type Size diff --git a/packages/@react-stately/layout/src/TableLayout.ts b/packages/@react-stately/layout/src/TableLayout.ts index 4bec2f9d9ea..f96a6fb1ab2 100644 --- a/packages/@react-stately/layout/src/TableLayout.ts +++ b/packages/@react-stately/layout/src/TableLayout.ts @@ -71,8 +71,11 @@ export class TableLayout extends ListLayout { this.wasLoading = this.isLoading; this.isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; - // TODO: switch this to only pass in the columns that we want to calculate - this.buildColumnWidths(); + const resizeIndex = this.collection.columns.findIndex(column => column.key === this.currentResizeColumn); + let affectedResizeColumns = this.collection.columns.slice(resizeIndex + 1, this.collection.columns.length); + let remainingSpace = this.virtualizer.visibleRect.width; + remainingSpace = this.collection.columns.slice(0, resizeIndex + 1).reduce((acc, column) => acc - this.getColumnWidth_(column.key), this.virtualizer.visibleRect.width); + this.buildColumnWidths(affectedResizeColumns, remainingSpace); let header = this.buildHeader(); let body = this.buildBody(0); body.layoutInfo.rect.width = Math.max(header.layoutInfo.rect.width, body.layoutInfo.rect.width); @@ -83,25 +86,18 @@ export class TableLayout extends ListLayout { ]; } - buildColumnWidths() { + buildColumnWidths(affectedResizeColumns: GridNode[], remainingSpace: number) { this.stickyColumnIndices = []; // if there was a column resized, set it's width and mark it as static somehow. // Pass 1: set widths for all explicitly defined columns. let remainingColumns = new Set>(); - let remainingSpace = this.virtualizer.visibleRect.width; - let isAfterResizeColumn = this.currentResizeColumn === null; - for (let column of this.collection.columns) { + for (let column of affectedResizeColumns) { let props = column.props as ColumnProps; let width; if (props.width) { width = props.width; - } else if (!isAfterResizeColumn) { - width = this.getColumnWidth_(column.key); - if (column.key === this.currentResizeColumn) { - width = this.resizeDelta; - } } else { width = this.hasResizedColumn(column.key) ? this.getColumnWidth_(column.key) : props.defaultWidth ?? this.getDefaultWidth(props); } @@ -119,8 +115,6 @@ export class TableLayout extends ListLayout { if (column.props.isSelectionCell || this.collection.rowHeaderColumnKeys.has(column.key)) { this.stickyColumnIndices.push(column.index); } - - isAfterResizeColumn = isAfterResizeColumn || column.key === this.currentResizeColumn; } // Pass 2: if there are remaining columns, then distribute the remaining space evenly. From 14531b4c73ca839227ba557d3780254d4480125c Mon Sep 17 00:00:00 2001 From: Marshall Peterson Date: Tue, 25 Jan 2022 12:26:44 -0700 Subject: [PATCH 10/11] move the min/max check to TableLayout so that it will correctly support percentage values for min/max --- .../table/src/useTableColumnResize.ts | 6 +- .../table/stories/Table.stories.tsx | 65 ++++++++++++++++++- .../@react-stately/layout/src/TableLayout.ts | 42 ++++++++---- .../@react-stately/table/src/useTableState.ts | 6 +- 4 files changed, 100 insertions(+), 19 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index ec6b1d439ed..74823af8c97 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -21,15 +21,13 @@ export function useTableColumnResize(state, item): any { const columnResizeWidthRef = useRef(null); const {moveProps} = useMove({ onMoveStart() { - stateRef.current.setCurrentResizeColumn(item.key); + stateRef.current.setCurrentResizeColumn(item); stateRef.current.addResizedColumn(item.key); columnResizeWidthRef.current = stateRef.current.getColumnWidth(item.key); }, onMove({deltaX}) { columnResizeWidthRef.current += deltaX; - let widthRespectingBoundaries = Math.max(item.props.minWidth || 75, Math.min(columnResizeWidthRef.current, item.props.maxWidth || Infinity)); - stateRef.current.setResizeDelta(widthRespectingBoundaries); - stateRef.current.setColumnWidth(item.key, widthRespectingBoundaries); + stateRef.current.setResizeDelta(columnResizeWidthRef.current); }, onMoveEnd() { stateRef.current.setCurrentResizeColumn(); diff --git a/packages/@react-spectrum/table/stories/Table.stories.tsx b/packages/@react-spectrum/table/stories/Table.stories.tsx index 513b5808ec6..7d5a76dcf2a 100644 --- a/packages/@react-spectrum/table/stories/Table.stories.tsx +++ b/packages/@react-spectrum/table/stories/Table.stories.tsx @@ -28,7 +28,7 @@ import {IllustratedMessage} from '@react-spectrum/illustratedmessage'; import {Link} from '@react-spectrum/link'; import {LoadingState, SelectionMode} from '@react-types/shared'; import {Radio, RadioGroup} from '@react-spectrum/radio'; -import React, {Key, useState} from 'react'; +import React, {Key, useMemo, useState} from 'react'; import {SearchField} from '@react-spectrum/searchfield'; import {storiesOf} from '@storybook/react'; import {Switch} from '@react-spectrum/switch'; @@ -1070,6 +1070,12 @@ storiesOf('TableView', module) ) + ) + .add( + 'resizable columns, controlled', + () => ( + + ) ); function AsyncLoadingExample() { @@ -1419,3 +1425,60 @@ export function TableWithBreadcrumbs() { ); } + +function ResizableColumnsControlled() { + let columns = [ + { + key: 0, + label: 'File Name', + allowsResizing: true, + width: 300 + }, + { + key: 1, + label: 'Type', + allowsResizing: true, + width: 200 + }, + { + key: 2, + label: 'Size', + allowsResizing: true, + width: 96 + } + ]; + + let [columnState, setColumnState] = useState(columns); + + let onResize = (key, prevWidth, newWidth) => { + if (newWidth !== prevWidth) { + let updatedColumnState = columnState.map(c => key === c.key ? {...c, width: newWidth} : c); + setColumnState(updatedColumnState); + + } + + }; + + + return ( + + + { + columnState.map(c => useMemo(() => ( onResize(c.key, c.width, newWidth)}>{c.label}), [c.width])) + } + + + + 2018 Proposal + PDF + 214 KB + + + Budget + XLS + 120 KB + + + + ); +} diff --git a/packages/@react-stately/layout/src/TableLayout.ts b/packages/@react-stately/layout/src/TableLayout.ts index f96a6fb1ab2..9c95f4bc8b8 100644 --- a/packages/@react-stately/layout/src/TableLayout.ts +++ b/packages/@react-stately/layout/src/TableLayout.ts @@ -21,9 +21,9 @@ type TableLayoutOptions = ListLayoutOptions & { getDefaultWidth: (props) => string | number, columnWidths: Map, getColumnWidth: (key: Key) => number, - setColumnWidth: (key: Key, width: number) => void, + setColumnWidth: (column: any, width: number) => void, hasResizedColumn: (key: Key) => boolean, - currentResizeColumn: Key, + currentResizeColumn: any, resizeDelta: number } @@ -35,9 +35,9 @@ export class TableLayout extends ListLayout { stickyColumnIndices: number[]; getDefaultWidth: (props) => string | number; getColumnWidth_: (key: Key) => number; - setColumnWidth: (key: Key, width: number) => void; + setColumnWidth_: (column: any, width: number) => void; hasResizedColumn: (key: Key) => boolean; - currentResizeColumn: Key; + currentResizeColumn: any; resizeDelta: number; wasLoading = false; isLoading = false; @@ -47,7 +47,7 @@ export class TableLayout extends ListLayout { this.getDefaultWidth = options.getDefaultWidth; this.columnWidths = options.columnWidths; this.getColumnWidth_ = options.getColumnWidth; - this.setColumnWidth = options.setColumnWidth; + this.setColumnWidth_ = options.setColumnWidth; this.hasResizedColumn = options.hasResizedColumn; this.currentResizeColumn = options.currentResizeColumn; this.columnWidthsRef = this.columnWidths; @@ -71,11 +71,26 @@ export class TableLayout extends ListLayout { this.wasLoading = this.isLoading; this.isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; - const resizeIndex = this.collection.columns.findIndex(column => column.key === this.currentResizeColumn); - let affectedResizeColumns = this.collection.columns.slice(resizeIndex + 1, this.collection.columns.length); - let remainingSpace = this.virtualizer.visibleRect.width; - remainingSpace = this.collection.columns.slice(0, resizeIndex + 1).reduce((acc, column) => acc - this.getColumnWidth_(column.key), this.virtualizer.visibleRect.width); + // only rebuild columns that come after the column being resized, if no column is being resized, they will all be built + const resizeIndex = this.collection.columns.findIndex(column => column.key === this.currentResizeColumn?.key); + // if resizing, set the column width for the resized column to the delta bounded by it's min/max + if (resizeIndex > -1) { + const columnProps = this.collection.columns[resizeIndex].props; + const boundedResizeDelta = Math.max(this.getMinWidth(columnProps?.minWidth), Math.min(this.getMaxWidth(columnProps.maxWidth), this.resizeDelta)); + + if (columnProps.width) { + // Controlled component - explicit width is defined and should always win. + // Unsure: Set column width to current width but call onResize with the new width? + this.setColumnWidth_(this.currentResizeColumn, columnProps.width); + columnProps.onResize && columnProps.onResize(boundedResizeDelta); + } else { + this.setColumnWidth(this.currentResizeColumn, boundedResizeDelta); + } + } + const affectedResizeColumns = this.collection.columns.slice(resizeIndex + 1, this.collection.columns.length); + const remainingSpace = this.collection.columns.slice(0, resizeIndex + 1).reduce((acc, column) => acc - this.getColumnWidth_(column.key), this.virtualizer.visibleRect.width); this.buildColumnWidths(affectedResizeColumns, remainingSpace); + let header = this.buildHeader(); let body = this.buildBody(0); body.layoutInfo.rect.width = Math.max(header.layoutInfo.rect.width, body.layoutInfo.rect.width); @@ -86,6 +101,11 @@ export class TableLayout extends ListLayout { ]; } + setColumnWidth(column, newWidth) { + this.setColumnWidth_(column, newWidth); + column.props.onResize && column.props.onResize(newWidth); + } + buildColumnWidths(affectedResizeColumns: GridNode[], remainingSpace: number) { this.stickyColumnIndices = []; @@ -104,7 +124,7 @@ export class TableLayout extends ListLayout { if (this.getIsStatic(width)) { let w = this.parseWidth(width); this.columnWidthsRef.set(column.key, w); - this.setColumnWidth(column.key, w); + this.setColumnWidth(column, w); remainingSpace -= w; } else { remainingColumns.add(column); @@ -126,7 +146,7 @@ export class TableLayout extends ListLayout { let i = 0; for (let column of remainingColumns) { this.columnWidthsRef.set(column.key, remCols[i].columnWidth); - this.setColumnWidth(column.key, remCols[i].columnWidth); + this.setColumnWidth(column, remCols[i].columnWidth); i++; } } diff --git a/packages/@react-stately/table/src/useTableState.ts b/packages/@react-stately/table/src/useTableState.ts index 967954c3bd0..baad374c009 100644 --- a/packages/@react-stately/table/src/useTableState.ts +++ b/packages/@react-stately/table/src/useTableState.ts @@ -37,7 +37,7 @@ export interface TableState extends GridState> { columnWidths(): Map, getColumnWidth(key: Key): number, - setColumnWidth(key: Key, width: number), + setColumnWidth(column: any, width: number), hasResizedColumn(key: Key): boolean, addResizedColumn(key: Key), @@ -102,8 +102,8 @@ export function useTableState( return columnWidthsRef.current.get(key); } - function setColumnWidthNew(key: Key, width: number) { - columnWidthsRef.current.set(key, width); + function setColumnWidthNew(column: any, width: number) { + columnWidthsRef.current.set(column.key, width); // new map so that change detection is triggered setColumnWidths(new Map(columnWidthsRef.current)); } From 56a402afffa4f5bc18ccb06aeb4fb6fd28841bf7 Mon Sep 17 00:00:00 2001 From: Marshall Peterson Date: Tue, 1 Feb 2022 12:52:50 -0700 Subject: [PATCH 11/11] remove prettier formatting --- .../@react-aria/slider/src/useSliderThumb.ts | 53 +++++-------------- .../@react-stately/layout/src/TableLayout.ts | 1 - .../slider/src/useSliderState.ts | 32 +++-------- .../@react-stately/table/src/useTableState.ts | 21 ++------ 4 files changed, 26 insertions(+), 81 deletions(-) diff --git a/packages/@react-aria/slider/src/useSliderThumb.ts b/packages/@react-aria/slider/src/useSliderThumb.ts index 23a0c89571f..7aafaf8d6b2 100644 --- a/packages/@react-aria/slider/src/useSliderThumb.ts +++ b/packages/@react-aria/slider/src/useSliderThumb.ts @@ -1,21 +1,7 @@ import {AriaSliderThumbProps} from '@react-types/slider'; -import { - clamp, - focusWithoutScrolling, - mergeProps, - useGlobalListeners -} from '@react-aria/utils'; +import {clamp, focusWithoutScrolling, mergeProps, useGlobalListeners} from '@react-aria/utils'; import {getSliderThumbId, sliderIds} from './utils'; -import React, { - ChangeEvent, - HTMLAttributes, - InputHTMLAttributes, - LabelHTMLAttributes, - RefObject, - useCallback, - useEffect, - useRef -} from 'react'; +import React, {ChangeEvent, HTMLAttributes, InputHTMLAttributes, LabelHTMLAttributes, RefObject, useCallback, useEffect, useRef} from 'react'; import {SliderState} from '@react-stately/slider'; import {useFocusable} from '@react-aria/focus'; import {useLabel} from '@react-aria/label'; @@ -97,24 +83,16 @@ export function useSliderThumb( state.setThumbDragging(index, true); }, onMove({deltaX, deltaY, pointerType}) { - let size = isVertical - ? trackRef.current.offsetHeight - : trackRef.current.offsetWidth; + let size = isVertical ? trackRef.current.offsetHeight : trackRef.current.offsetWidth; if (currentPosition.current == null) { - currentPosition.current = - stateRef.current.getThumbPercent(index) * size; + currentPosition.current = stateRef.current.getThumbPercent(index) * size; } if (pointerType === 'keyboard') { // (invert left/right according to language direction) + (according to vertical) - let delta = - ((reverseX ? -deltaX : deltaX) + (isVertical ? -deltaY : -deltaY)) * - stateRef.current.step; + let delta = ((reverseX ? -deltaX : deltaX) + (isVertical ? -deltaY : -deltaY)) * stateRef.current.step; currentPosition.current += delta * size; - stateRef.current.setThumbValue( - index, - stateRef.current.getThumbValue(index) + delta - ); + stateRef.current.setThumbValue(index, stateRef.current.getThumbValue(index) + delta); } else { let delta = isVertical ? deltaY : deltaX; if (isVertical || reverseX) { @@ -122,10 +100,7 @@ export function useSliderThumb( } currentPosition.current += delta; - stateRef.current.setThumbPercent( - index, - clamp(currentPosition.current / size, 0, 1) - ); + stateRef.current.setThumbPercent(index, clamp(currentPosition.current / size, 0, 1)); } }, onMoveEnd() { @@ -153,6 +128,7 @@ export function useSliderThumb( addGlobalListener(window, 'mouseup', onUp, false); addGlobalListener(window, 'touchend', onUp, false); addGlobalListener(window, 'pointerup', onUp, false); + }; let onUp = (e) => { @@ -188,8 +164,9 @@ export function useSliderThumb( state.setThumbValue(index, parseFloat(e.target.value)); } }), - thumbProps: !isDisabled - ? mergeProps(moveProps, { + thumbProps: !isDisabled ? mergeProps( + moveProps, + { onMouseDown: (e: React.MouseEvent) => { if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) { return; @@ -202,11 +179,9 @@ export function useSliderThumb( } onDown(e.pointerId); }, - onTouchStart: (e: React.TouchEvent) => { - onDown(e.changedTouches[0].identifier); - } - }) - : {}, + onTouchStart: (e: React.TouchEvent) => {onDown(e.changedTouches[0].identifier);} + } + ) : {}, labelProps }; } diff --git a/packages/@react-stately/layout/src/TableLayout.ts b/packages/@react-stately/layout/src/TableLayout.ts index 9c95f4bc8b8..3ac463dbe83 100644 --- a/packages/@react-stately/layout/src/TableLayout.ts +++ b/packages/@react-stately/layout/src/TableLayout.ts @@ -318,7 +318,6 @@ export class TableLayout extends ListLayout { let width = 0; for (let i = 0; i < colspan; i++) { let column = this.collection.columns[node.index + i]; - // width += this.columnWidths.get(column.key); width += this.getColumnWidth_(column.key); } diff --git a/packages/@react-stately/slider/src/useSliderState.ts b/packages/@react-stately/slider/src/useSliderState.ts index d11f41a0628..15686e46ddb 100644 --- a/packages/@react-stately/slider/src/useSliderState.ts +++ b/packages/@react-stately/slider/src/useSliderState.ts @@ -140,26 +140,16 @@ interface SliderStateOptions extends SliderProps { * @param props */ export function useSliderState(props: SliderStateOptions): SliderState { - const { - isDisabled, - minValue = DEFAULT_MIN_VALUE, - maxValue = DEFAULT_MAX_VALUE, - numberFormatter: formatter, - step = DEFAULT_STEP_VALUE - } = props; + const {isDisabled, minValue = DEFAULT_MIN_VALUE, maxValue = DEFAULT_MAX_VALUE, numberFormatter: formatter, step = DEFAULT_STEP_VALUE} = props; const [values, setValues] = useControlledState( props.value as any, - props.defaultValue ?? ([minValue] as any), + props.defaultValue ?? [minValue] as any, props.onChange as any ); - const [isDraggings, setDraggings] = useState( - new Array(values.length).fill(false) - ); + const [isDraggings, setDraggings] = useState(new Array(values.length).fill(false)); const isEditablesRef = useRef(new Array(values.length).fill(true)); - const [focusedIndex, setFocusedIndex] = useState( - undefined - ); + const [focusedIndex, setFocusedIndex] = useState(undefined); const valuesRef = useRef(null); valuesRef.current = values; @@ -204,19 +194,11 @@ export function useSliderState(props: SliderStateOptions): SliderState { } const wasDragging = isDraggingsRef.current[index]; - isDraggingsRef.current = replaceIndex( - isDraggingsRef.current, - index, - dragging - ); + isDraggingsRef.current = replaceIndex(isDraggingsRef.current, index, dragging); setDraggings(isDraggingsRef.current); // Call onChangeEnd if no handles are dragging. - if ( - props.onChangeEnd && - wasDragging && - !isDraggingsRef.current.some(Boolean) - ) { + if (props.onChangeEnd && wasDragging && !isDraggingsRef.current.some(Boolean)) { props.onChangeEnd(valuesRef.current); } } @@ -243,7 +225,7 @@ export function useSliderState(props: SliderStateOptions): SliderState { getThumbValue: (index: number) => values[index], setThumbValue: updateValue, setThumbPercent, - isThumbDragging: (index: number) => isDraggingsRef.current[index], + isThumbDragging: (index: number) => isDraggings[index], setThumbDragging: updateDragging, focusedThumb: focusedIndex, setFocusedThumb: setFocusedIndex, diff --git a/packages/@react-stately/table/src/useTableState.ts b/packages/@react-stately/table/src/useTableState.ts index baad374c009..f03789b86e8 100644 --- a/packages/@react-stately/table/src/useTableState.ts +++ b/packages/@react-stately/table/src/useTableState.ts @@ -10,14 +10,7 @@ * governing permissions and limitations under the License. */ -import { - CollectionBase, - Node, - SelectionMode, - Sortable, - SortDescriptor, - SortDirection -} from '@react-types/shared'; +import {CollectionBase, Node, SelectionMode, Sortable, SortDescriptor, SortDirection} from '@react-types/shared'; import {GridState, useGridState} from '@react-stately/grid'; import {TableCollection as ITableCollection} from '@react-types/table'; import {Key, useMemo, useRef, useState} from 'react'; @@ -145,10 +138,7 @@ export function useTableState( (nodes, prev) => new TableCollection(nodes, prev, context), context ); - let {disabledKeys, selectionManager} = useGridState({ - ...props, - collection - }); + let {disabledKeys, selectionManager} = useGridState({...props, collection}); return { collection, @@ -159,10 +149,9 @@ export function useTableState( sort(columnKey: Key) { props.onSortChange({ column: columnKey, - direction: - props.sortDescriptor?.column === columnKey - ? OPPOSITE_SORT_DIRECTION[props.sortDescriptor.direction] - : 'ascending' + direction: props.sortDescriptor?.column === columnKey + ? OPPOSITE_SORT_DIRECTION[props.sortDescriptor.direction] + : 'ascending' }); }, columnWidths: () => columnWidths,