Description
🙋 Feature Request
Users have requested the ability to resize columns in TableView by dragging a divider between them. This is a common feature in many table components, and allows users to configure tables to suit their needs. Since this is a large project, this ticket is for initial API and behavior, including pointer support. Subsequent tickets will address keyboard and screen reader accessibility.
💁 API
The Column
component should gain the following API:
export interface ColumnProps<T> {
// Already existing props relevant to the discussion below...
/** The width of the column (controlled). */
width?: number | string,
/** The minimum width of the column. */
minWidth?: number | string,
/** The maximum width of the column. */
maxWidth?: number | string,
// New props...
/** The default width of the column (uncontrolled). */
defaultWidth?: number | string,
/** Whether the user is allowed to resize the column. */
allowsResizing?: boolean,
/** A callback that is called when the column is resized. */
onResize?: (width: number) => void
}
🤔 Expected Behavior
- The
allowsResizing
prop must be set to enable the user to resize a column. - The
onResize
prop on a column will be called when the user resizes a column. It receives the new width as an argument. Widths are represented in pixels. - When the
width
prop is set on a column, the width is controlled. This means the width won’t update unless thewidth
prop changes (e.g. as a result of setting state in theonResize
callback). This is useful for storing the column widths in an external state store. - When the
defaultWidth
prop is set, the width is uncontrolled. The state is stored internally to theTableView
. - The
minWidth
andmaxWidth
props constrain the amount a user can resize a column. If unset, there is a default minimum width for a column, and no maximum width (??). - When a user hovers over a resizable column divider, the
col-resize
cursor should be shown. It should also be shown at all times while dragging, even when the cursor is not directly over the divider.
Open questions
- What should happen if the user resizes the window after resizing a column that didn’t start with a fixed size (e.g. automatically computed based on available space)? Should it stay fixed to what the user set, or adjust as it would have before?
- When resizing a column and the following columns do not have set widths (i.e. they automatically get their widths by dividing the remaining space), what should happen? Should they change size, or should they stay the same and get “pushed” over?
3 options:
- Resize as a proportion. e.g. 33%/33%/33% → 50%/25%/25%. After resizing a column, then when the width of the table changes, it grows or shrinks proportionally.
- Resizing a column overrides the width of that column, so that it becomes fixed. The other columns that haven’t been explicitly sized still change size proportionally.
- It’s weird if changing the width of one column affects the widths of others.
- All column widths are set on initial render. Resizing one column does not affect the widths of other columns. The table may end up scrolling horizontally if a column grows.
Implementation
- A new
useTableColumnResizeState
hook should be created in@react-stately/table
. This will encapsulate the state management and logic for table column resizing.- We will need to move the column width computation algorithm from the
TableLayout
into this hook to make it reusable. This is responsible for determining the widths of columns without explicitly defined widths, dividing the available space and taking constraints such as minimum and maximum widths into account. - There should be a method to update the width of a particular column, when the user resizes it.
- We will need to move the column width computation algorithm from the
- A new
useTableColumnResizeHandle
hook should be created in@react-aria/table
to encapsulate the dragging behavior.- Dragging should be implemented using the existing
useMove
hook, updating the resize state. - Eventually, this will also handle the keyboard behavior, which is out of scope for this ticket.
- Dragging should be implemented using the existing
- In React Spectrum, the computed widths will need to be passed into the layout, and updated when they change.
💻 Examples
Uncontrolled (static)
This example shows how you could have uncontrolled widths using the defaultWidth
prop. The state for the actual width is stored internally to the component. Note that the defaultWidth
is not required for a column to be resizable. The initial width will be automatically computed if not provided.
<TableView>
<TableHeader>
<Column key="name" defaultWidth={300} allowsResizing>File name</Column>
<Column key="type" allowsResizing>Type</Column>
<Column key="size" allowsResizing>Size</Column>
</TableHeader>
{/* ... */}
</TableView>
Controlled (dynamic)
This example shows how you could have the widths be controlled, i.e. stored in external state. This could be useful to allow persisting the widths to a server, local storage, etc.
Note that the width must be stored in the
column
object, not in an external state, or the column render function won’t be re-evaluated.
let [columns, setColumns] = useState([
{key: 'name', width: 100, name: 'Name'}
]);
let onResize = (key, width) => {
setColumns(columns => columns.map(col => (col.key === key ? {...col, width} : col)))
};
<TableView>
<TableHeader columns={columns}>
{column => (
<Column
width={column.width}
allowsResizing
onResize={w => onResize(column.key, w)}>
{column.name}
</Column>
)}
</TableHeader>
{/* ... */}
</TableView>
This could potentially be made slightly simpler using the useListData
hook, and potentially a new method to update an existing item, but that is out of scope for this ticket.
🧢 Your Company/Team
RSP
Metadata
Metadata
Assignees
Labels
Type
Projects
Status