Skip to content

feat(#3344): add multi-column sorting to Table and TableSortHeader#3409

Draft
twjeffery wants to merge 16 commits intodevfrom
tom/table-fixes
Draft

feat(#3344): add multi-column sorting to Table and TableSortHeader#3409
twjeffery wants to merge 16 commits intodevfrom
tom/table-fixes

Conversation

@twjeffery
Copy link
Collaborator

@twjeffery twjeffery commented Feb 10, 2026

Summary

Adds built-in multi-column sorting to Table and TableSortHeader components.

  • sortMode prop: "single" (default) or "multi" (up to 2 columns)
  • initialSort prop: Pre-configure sort state
  • onSortChange callback: Receives array of current sorts
  • sortOrder prop on TableSortHeader: Shows "1", "2" priority badges
  • Table manages sort state internally, headers update automatically
  • New icons: chevron-expand (unsorted), arrow-up/arrow-down (sorted)
  • V2 focus ring on icon only

Multi sort

image

Single sort

image

Test plan

  • Visit React playground at /features/3344
  • Test single-column sort (Test 1) - click headers, verify only one sorts at a time
  • Test multi-column sort (Test 2) - click multiple headers, verify "1" and "2" badges appear
  • Test initial sort (Test 3) - verify Department starts sorted
  • Test V2 experimental wrappers (Test 4) - verify focus ring on icon only
  • Visit Angular playground at /features/3344 and repeat tests

Closes #3344

@twjeffery twjeffery force-pushed the tom/table-fixes branch 3 times, most recently from c1514d7 to 7097784 Compare February 10, 2026 01:49
Copy link
Collaborator

@bdfranck bdfranck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sorting interactions are looking good! I've left some comments that can be grouped into three categories:

  1. Try to re-use and expand on existing variables and functions.
  2. Avoid using JSON because it can lead to increased errors. Use Types instead.
  3. Make sure that you're giving devs the data they need for their sort functions.

/** Sort mode: "single" allows one column, "multi" allows up to 2 columns. */
export let sortmode: SortMode = "single";
/** Initial sort configuration as JSON string: [{"column":"name","direction":"asc"}] */
export let initialsort: string = "";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a JSON string here could introduce errors. We can take advantage of TypeScript's error prevention by using an array of SortEntries instead. Then we can remove _sorts as it will be redundant.

The checkbox list is a good reference. It uses an array of strings for keeping track of multiple selected values.

/** Array of currently selected checkbox values. */
export let value: string[] = [];

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree vs the point that we shouldn' use json.
I think instead of using initialsort as a JSON string on the table (which is a new property), We could declare the initial sort state directly on each goa-table-sort-header (which we do nowadays)
For example:

<goa-table sortmode="multi">
... <goa-table-sort-header name="name" direction="asc" sortorder="1">Name</goa-table-sort-header>
... <goa-table-sort-header name="department" direction="desc" sortorder="2">Department</goa-table-sort-header>
...<goa-table-sort-header name="salary">Salary</goa-table-sort-header>
</goa-table>

So we only add a new property sortorder to the goa-table-sort-header. This approach will be consistent with how the old single-sort already worked. The table just scans headers onMount to build its internal sort state from direction + sortorder attribute.

}
}

function initializeSorts() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove this function when you switch to using an array of SortEntries for the sorts.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we rely on GoabTableSortHeader then refer this comment #3409 (comment), we can keep this function to "scan" header and then initialize the table sort state.

import type { Spacing } from "../../common/styling";

// Types
type SortDirection = "asc" | "desc";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an existing type that you can use. It's already imported into this component:

<script context="module" lang="ts">
export type GoATableSortDirection = "asc" | "desc" | "none";
</script>

/** @deprecated Use onSortChange for new implementations */
onSort?: (detail: GoabTableOnSortDetail) => void;
/** Called when sort state changes. Receives array of current sorts. */
onSortChange?: (sorts: GoabTableSortEntry[]) => void;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of building a parallel function that does the same thing as onSort, why not expand onSort to support either a single item or an array?

GoabTableOnSortDetail[] | GoabTableOnSortDetail

This would allow you to add new functionality while preserving the original structure.


// New API: onSortChange receives sorts array
if (onSortChange && "sorts" in detail) {
onSortChange(detail.sorts);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that this will send a "asc" or "desc" string for direction instead of 1 or -1. Is this intentional? Devs will need a number for their sort function like we have in our example:
https://design.alberta.ca/components/table#tab-1#table-with-sortable-columns

@Input() initialSort?: GoabTableSortEntry[];
@Input({ transform: booleanAttribute }) striped?: boolean;

get initialSortJson(): string | undefined {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid using JSON here too. The initial sort should be an array to reduce errors.

.direction--none goa-icon {
height: 0.625rem;
/* Sort order number badge */
.sort-order {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested and see there is a small shift in a column:
https://jam.dev/c/8bafde76-cd5d-4d17-8e1e-68912f31a9b1

I think we should add a hidden class for this .sort-order, for example:
<span class='sort-order' class:hidden={!sortorder || direction === 'none'>

Then we can make sure width:0 and margin:0 to prevent the shifting.

@netlify
Copy link

netlify bot commented Feb 24, 2026

Deploy Preview for goa-design-2 failed.

Name Link
🔨 Latest commit c82cfcf
🔍 Latest deploy log https://app.netlify.com/projects/goa-design-2/deploys/699f5992ae49af0007477236

twjeffery and others added 2 commits February 25, 2026 11:07
- Add sortMode prop ("single" | "multi") to Table for multi-column sorting
- Add initialSort prop to pre-configure sort state
- Add onSortChange callback returning array of current sorts
- Add sortOrder prop to TableSortHeader for priority display ("1", "2")
- Table manages sort state internally, updates headers automatically
- Change sort icons: chevron-expand (unsorted), arrow-up/down (sorted)
- Add V2 focus styling (ring on icon only)
- Update React and Angular wrappers (lib + experimental)
- Add test pages for React and Angular playgrounds

Closes #3344
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add optional multi-column sorting (primary + secondary) to the table component

3 participants