Skip to content

TableView column resizing #2555

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
devongovett opened this issue Nov 16, 2021 · 3 comments · Fixed by #2883
Closed

TableView column resizing #2555

devongovett opened this issue Nov 16, 2021 · 3 comments · Fixed by #2883
Assignees
Labels
enhancement New feature or request rsp:TableView

Comments

@devongovett
Copy link
Member

devongovett commented Nov 16, 2021

🙋 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 the width prop changes (e.g. as a result of setting state in the onResize 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 the TableView.
  • The minWidth and maxWidth 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:

  1. 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.
  2. 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.
    1. It’s weird if changing the width of one column affects the widths of others.
  3. 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.
  • 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.
  • 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

@chuckdries
Copy link

Does anyone have any specific plans to implement this? If not, our team needs it and has bandwidth to do it; I just don't want to duplicate work.

@matthewdeutsch
Copy link
Collaborator

@chuckdries yes we have a team in the exploration phase here - I'll reach out to you privately with details

@marshallpete
Copy link
Member

Accessibility Proposal

  • Using arrow keys, users can navigate to the column header
  • While the column header is focussed, pressing return/enter or space will activate a dropdown
  • One of the options in the dropdown will be to resize the column (if column is resizable)
  • User can navigate through the dropdown using arrow keys
  • Pressing return/enter or space on the dropdown item will activate the resize mode, closing the dropdown
  • When in resize mode, the user can use the left and right arrow keys to adjust the width of the column in increments (probably 10px?)
  • Pressing Esc will exit resize mode and return focus to the column header
  • Pressing Tab will also exit resize mode and tab out of the table entirely
  • If user tries to return focus to the table using Shift + Tab, the column header will be focussed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request rsp:TableView
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

5 participants