diff --git a/apps/prs/angular/src/app/app.component.html b/apps/prs/angular/src/app/app.component.html
index dff506719..e7dae74f1 100644
--- a/apps/prs/angular/src/app/app.component.html
+++ b/apps/prs/angular/src/app/app.component.html
@@ -85,6 +85,7 @@
3306
3370
v2 header icons
+ 3344 Table Multi-Sort
3396 Text heading-2xs size
3407 Skip Focus on Tab
3407 Tabs Orientation
diff --git a/apps/prs/angular/src/app/app.routes.ts b/apps/prs/angular/src/app/app.routes.ts
index 7c300d9ee..b96375774 100644
--- a/apps/prs/angular/src/app/app.routes.ts
+++ b/apps/prs/angular/src/app/app.routes.ts
@@ -64,6 +64,7 @@ import { Feat2722Component } from "../routes/features/feat2722/feat2722.componen
import { Feat2730Component } from "../routes/features/feat2730/feat2730.component";
import { Feat2829Component } from "../routes/features/feat2829/feat2829.component";
import { Feat3102Component } from "../routes/features/feat3102/feat3102.component";
+import { Feat3344Component } from "../routes/features/feat3344/feat3344.component";
import { Feat3137Component } from "../routes/features/feat3137/feat3137.component";
import { Feat3241Component } from "../routes/features/feat3241/feat3241.component";
import { Feat3229Component } from "../routes/features/feat3229/feat3229.component";
@@ -142,6 +143,7 @@ export const appRoutes: Route[] = [
{ path: "features/3137", component: Feat3137Component },
{ path: "features/3241", component: Feat3241Component },
{ path: "features/v2-icons", component: FeatV2IconsComponent },
+ { path: "features/3344", component: Feat3344Component },
{ path: "features/3137", component: Feat3137Component },
{ path: "features/3229", component: Feat3229Component },
{ path: "features/3306", component: Feat3306Component },
diff --git a/apps/prs/angular/src/routes/features/feat3344/feat3344.component.html b/apps/prs/angular/src/routes/features/feat3344/feat3344.component.html
new file mode 100644
index 000000000..0eda15483
--- /dev/null
+++ b/apps/prs/angular/src/routes/features/feat3344/feat3344.component.html
@@ -0,0 +1,111 @@
+
PR 4: Table & TableSortHeader V2
+
+
+
+
+ TableSortHeader:
+ - Always-visible sort icon (chevron-expand when unsorted)
+ - Arrow-up/arrow-down for sorted states
+ - sortOrder prop shows "1", "2" for multi-column sort
+
+ Table:
+ - sortMode="single" (default) or "multi" (up to 2 columns)
+ - Initial sort declared on headers via direction + sortOrder
+ - onSort callback with sortBy, sortDir, and sorts array
+
+
+
+
+
+
+Test 1: Single-Column Sort (Default)
+Click column headers to sort. Only one column sorts at a time (default behavior).
+Current sort: {{ formatSorts(currentSorts) }}
+
+
+
+
+
+ Name
+
+
+ Department
+
+
+ Salary
+
+
+
+
+ @for (row of singleSorted; track row.id) {
+
+ {{ row.name }}
+ {{ row.department }}
+ {{ formatCurrency(row.salary) }}
+
+ }
+
+
+
+
+
+Test 2: Multi-Column Sort
+With sortMode="multi", click columns to add them to sort order (up to 2).
+Current sort: {{ formatSorts(multiSorts) }}
+
+
+
+
+
+ Name
+
+
+ Department
+
+
+ Salary
+
+
+
+
+ @for (row of multiSorted; track row.id) {
+
+ {{ row.name }}
+ {{ row.department }}
+ {{ formatCurrency(row.salary) }}
+
+ }
+
+
+
+
+
+Test 3: Multi Initial Sort (declarative)
+Two initial sorts declared on headers: Department ascending (primary), Salary descending (secondary).
+Use direction + sortOrder on each header to set priority.
+Current sort: {{ formatSorts(test3Sorts) }}
+
+
+
+
+
+ Name
+
+
+ Department
+
+
+ Salary
+
+
+
+
+ @for (row of test3Sorted; track row.id) {
+
+ {{ row.name }}
+ {{ row.department }}
+ {{ formatCurrency(row.salary) }}
+
+ }
+
+
diff --git a/apps/prs/angular/src/routes/features/feat3344/feat3344.component.ts b/apps/prs/angular/src/routes/features/feat3344/feat3344.component.ts
new file mode 100644
index 000000000..722af099e
--- /dev/null
+++ b/apps/prs/angular/src/routes/features/feat3344/feat3344.component.ts
@@ -0,0 +1,100 @@
+import { Component, OnInit, OnDestroy } from "@angular/core";
+import {
+ GoabBlock,
+ GoabDivider,
+ GoabDetails,
+ GoabxTable,
+ GoabxTableSortHeader,
+} from "@abgov/angular-components";
+import { GoabTableOnSortDetail, GoabTableSortEntry } from "@abgov/ui-components-common";
+
+@Component({
+ standalone: true,
+ selector: "abgov-feat3344",
+ templateUrl: "./feat3344.component.html",
+ imports: [
+ GoabBlock,
+ GoabDivider,
+ GoabDetails,
+ GoabxTable,
+ GoabxTableSortHeader,
+ ],
+})
+export class Feat3344Component implements OnInit, OnDestroy {
+ private v2TokensLink: HTMLLinkElement | null = null;
+
+ currentSorts: GoabTableSortEntry[] = [];
+ multiSorts: GoabTableSortEntry[] = [];
+ test3Sorts: GoabTableSortEntry[] = [];
+
+ data = [
+ { id: 1, name: "Alice Johnson", department: "Engineering", salary: 95000 },
+ { id: 2, name: "Bob Smith", department: "Marketing", salary: 72000 },
+ { id: 3, name: "Carol Williams", department: "Engineering", salary: 105000 },
+ { id: 4, name: "David Brown", department: "Sales", salary: 68000 },
+ { id: 5, name: "Eve Davis", department: "Marketing", salary: 78000 },
+ ];
+
+ singleSorted = [...this.data];
+ multiSorted = [...this.data];
+ test3Sorted = [...this.data];
+
+ ngOnInit() {
+ this.v2TokensLink = document.createElement("link");
+ this.v2TokensLink.rel = "stylesheet";
+ this.v2TokensLink.href = "/v2-tokens/tokens.css";
+ document.head.appendChild(this.v2TokensLink);
+ }
+
+ ngOnDestroy() {
+ if (this.v2TokensLink) {
+ document.head.removeChild(this.v2TokensLink);
+ this.v2TokensLink = null;
+ }
+ }
+
+ onSingleSortChange(detail: GoabTableOnSortDetail) {
+ this.currentSorts = detail.sorts ?? [];
+ this.singleSorted = this.sortData(this.data, this.currentSorts);
+ }
+
+ onMultiSortChange(detail: GoabTableOnSortDetail) {
+ this.multiSorts = detail.sorts ?? [];
+ this.multiSorted = this.sortData(this.data, this.multiSorts);
+ }
+
+ onTest3SortChange(detail: GoabTableOnSortDetail) {
+ this.test3Sorts = detail.sorts ?? [];
+ this.test3Sorted = this.sortData(this.data, this.test3Sorts);
+ }
+
+ formatSorts(sorts: GoabTableSortEntry[]): string {
+ if (sorts.length === 0) return "None";
+ return sorts.map((s, i) => `${i + 1}. ${s.column} (${s.direction})`).join(", ");
+ }
+
+ formatCurrency(value: number): string {
+ return "$" + value.toLocaleString();
+ }
+
+ private sortData(
+ data: typeof this.data,
+ sorts: GoabTableSortEntry[],
+ ): typeof this.data {
+ if (sorts.length === 0) return [...data];
+ return [...data].sort((a, b) => {
+ for (const { column, direction } of sorts) {
+ const aVal = a[column as keyof typeof a];
+ const bVal = b[column as keyof typeof b];
+ let cmp = 0;
+ if (typeof aVal === "string" && typeof bVal === "string") {
+ cmp = aVal.localeCompare(bVal);
+ } else {
+ cmp = (aVal as number) - (bVal as number);
+ }
+ if (cmp !== 0) return direction === "asc" ? cmp : -cmp;
+ }
+ return 0;
+ });
+ }
+}
diff --git a/apps/prs/react/src/app/app.tsx b/apps/prs/react/src/app/app.tsx
index 9ea799e24..6d8e441f8 100644
--- a/apps/prs/react/src/app/app.tsx
+++ b/apps/prs/react/src/app/app.tsx
@@ -92,12 +92,13 @@ export function App() {
3137 Work Side Menu Group
3241 V2 Experimental Wrappers
v2 header icons
- 3407 Skip Focus on Tab
- 3407 Tabs Orientation
3229 V2 Menu Button vs size and icon-only
+ 3344 Table Multi-Sort
3306 Custom slug value for tabs
3370 Clear calendar day selection
3396 Text heading-2xs size
+ 3407 Skip Focus on Tab
+ 3407 Tabs Orientation
A
diff --git a/apps/prs/react/src/main.tsx b/apps/prs/react/src/main.tsx
index 4cf795a65..3bd4a5f75 100644
--- a/apps/prs/react/src/main.tsx
+++ b/apps/prs/react/src/main.tsx
@@ -72,6 +72,7 @@ import { Feat3241Route } from "./routes/features/feat3241";
import { FeatV2IconsRoute } from "./routes/features/featV2Icons";
import { Feat3407SkipOnFocusTabRoute } from "./routes/features/feat3407SkipOnFocusTab";
import { Feat3407StackOnMobileRoute } from "./routes/features/feat3407StackOnMobile";
+import { Feat3344Route } from "./routes/features/feat3344";
import { Feat3137Route } from "./routes/features/feat3137";
import { Feat3306Route } from "./routes/features/feat3306";
import { Feat2469Route } from "./routes/features/feat2469";
@@ -158,11 +159,18 @@ root.render(
} />
} />
} />
+ } />
} />
} />
} />
- } />
- } />
+ }
+ />
+ }
+ />
diff --git a/apps/prs/react/src/routes/features/feat3344.tsx b/apps/prs/react/src/routes/features/feat3344.tsx
new file mode 100644
index 000000000..354e8adc9
--- /dev/null
+++ b/apps/prs/react/src/routes/features/feat3344.tsx
@@ -0,0 +1,200 @@
+/**
+ * PR 4: Table & TableSortHeader V2 Updates
+ *
+ * Tests:
+ * - TableSortHeader with chevron-expand icon (unsorted state)
+ * - TableSortHeader with arrow-up/arrow-down (sorted states)
+ * - Built-in single and multi-column sorting
+ * - sortOrder numbers for multi-column sort priority
+ * - V2 focus ring on icon only (not whole button)
+ */
+
+import { useEffect, useMemo, useState } from "react";
+import {
+ GoabBlock,
+ GoabDivider,
+ GoabDetails,
+} from "@abgov/react-components";
+// ?url suffix tells Vite to resolve the path without injecting the CSS
+import v2TokensUrl from "@abgov/design-tokens-v2/dist/tokens.css?url";
+import { GoabxTable, GoabxTableSortHeader } from "@abgov/react-components/experimental";
+import type { GoabTableSortEntry } from "@abgov/ui-components-common";
+
+type RowData = { id: number; name: string; department: string; salary: number };
+
+const initialData: RowData[] = [
+ { id: 1, name: "Alice Johnson", department: "Engineering", salary: 95000 },
+ { id: 2, name: "Bob Smith", department: "Marketing", salary: 72000 },
+ { id: 3, name: "Carol Williams", department: "Engineering", salary: 105000 },
+ { id: 4, name: "David Brown", department: "Sales", salary: 68000 },
+ { id: 5, name: "Eve Davis", department: "Marketing", salary: 78000 },
+];
+
+function sortData(data: RowData[], sorts: GoabTableSortEntry[]): RowData[] {
+ if (sorts.length === 0) return data;
+ return [...data].sort((a, b) => {
+ for (const { column, direction } of sorts) {
+ const aVal = a[column as keyof RowData];
+ const bVal = b[column as keyof RowData];
+ let cmp = 0;
+ if (typeof aVal === "string" && typeof bVal === "string") {
+ cmp = aVal.localeCompare(bVal);
+ } else {
+ cmp = (aVal as number) - (bVal as number);
+ }
+ if (cmp !== 0) return direction === "asc" ? cmp : -cmp;
+ }
+ return 0;
+ });
+}
+
+export function Feat3344Route() {
+ useEffect(() => {
+ const link = document.createElement("link");
+ link.rel = "stylesheet";
+ link.href = v2TokensUrl;
+ document.head.appendChild(link);
+ return () => {
+ document.head.removeChild(link);
+ };
+ }, []);
+
+ const [currentSorts, setCurrentSorts] = useState([]);
+ const [multiSorts, setMultiSorts] = useState([]);
+ const [test3Sorts, setTest3Sorts] = useState([]);
+
+ const singleSorted = useMemo(() => sortData(initialData, currentSorts), [currentSorts]);
+ const multiSorted = useMemo(() => sortData(initialData, multiSorts), [multiSorts]);
+ const test3Sorted = useMemo(() => sortData(initialData, test3Sorts), [test3Sorts]);
+
+ const formatSorts = (sorts: GoabTableSortEntry[]): string => {
+ if (sorts.length === 0) return "None";
+ return sorts.map((s, i) => `${i + 1}. ${s.column} (${s.direction})`).join(", ");
+ };
+
+ return (
+
+
PR 4: Table & TableSortHeader V2
+
+
+
+
+ TableSortHeader:
+ - Always-visible sort icon (chevron-expand when unsorted)
+ - Arrow-up/arrow-down for sorted states
+ - sortOrder prop shows "1", "2" for multi-column sort
+
+ Table:
+ - sortMode="single" (default) or "multi" (up to 2 columns)
+ - Initial sort declared on headers via direction + sortOrder
+ - onSort callback with sortBy, sortDir, and sorts array
+
+
+
+
+
+
+
Test 1: Single-Column Sort (Default)
+
Click column headers to sort. Only one column sorts at a time (default behavior).
+
Current sort: {formatSorts(currentSorts)}
+
+
setCurrentSorts(detail.sorts ?? [])}>
+
+
+
+ Name
+
+
+ Department
+
+
+ Salary
+
+
+
+
+ {singleSorted.map((row) => (
+
+ {row.name}
+ {row.department}
+ ${row.salary.toLocaleString()}
+
+ ))}
+
+
+
+
+
+
Test 2: Multi-Column Sort
+
With sortMode="multi", click columns to add them to sort order (up to 2).
+
Current sort: {formatSorts(multiSorts)}
+
+
setMultiSorts(detail.sorts ?? [])}>
+
+
+
+ Name
+
+
+ Department
+
+
+ Salary
+
+
+
+
+ {multiSorted.map((row) => (
+
+ {row.name}
+ {row.department}
+ ${row.salary.toLocaleString()}
+
+ ))}
+
+
+
+
+
+
Test 3: Multi Initial Sort (declarative)
+
Two initial sorts declared on headers: Department ascending (primary), Salary
+ descending (secondary). Use direction + sortOrder on each header to set priority.
+
Current sort: {formatSorts(test3Sorts)}
+
+
setTest3Sorts(detail.sorts ?? [])}
+ >
+
+
+
+ Name
+
+
+
+ Department
+
+
+
+
+ Salary
+
+
+
+
+
+ {test3Sorted.map((row) => (
+
+ {row.name}
+ {row.department}
+ ${row.salary.toLocaleString()}
+
+ ))}
+
+
+
+ );
+}
+
+export default Feat3344Route;
diff --git a/apps/prs/react/src/styles.css b/apps/prs/react/src/styles.css
index 90d4ee007..2450e3000 100644
--- a/apps/prs/react/src/styles.css
+++ b/apps/prs/react/src/styles.css
@@ -1 +1,8 @@
/* You can add global styles to this file, and also import other style files */
+
+/*
+ * V2 Design Tokens
+ * Uncomment the line below to test V2 component styling.
+ * V2 tokens override V1 CSS custom properties with updated values.
+ */
+/* @import "@abgov/design-tokens-v2/dist/tokens.css"; */
diff --git a/docs/generated/component-apis/table-sort-header.json b/docs/generated/component-apis/table-sort-header.json
new file mode 100644
index 000000000..dce036035
--- /dev/null
+++ b/docs/generated/component-apis/table-sort-header.json
@@ -0,0 +1,41 @@
+{
+ "componentSlug": "table-sort-header",
+ "extractedFrom": "libs/web-components/src/components/table/TableSortHeader.svelte",
+ "extractedAt": "2026-02-26T00:00:00.000Z",
+ "props": [
+ {
+ "name": "name",
+ "type": "string",
+ "required": false,
+ "default": "",
+ "description": "Column name identifier for sorting."
+ },
+ {
+ "name": "direction",
+ "type": "\"asc\" | \"desc\" | \"none\"",
+ "typeLabel": "GoabTableSortDirection",
+ "values": [
+ "asc",
+ "desc",
+ "none"
+ ],
+ "required": false,
+ "default": "none",
+ "description": "Sets the sort direction indicator."
+ },
+ {
+ "name": "sortOrder",
+ "type": "number",
+ "required": false,
+ "default": 0,
+ "description": "Sort order number for multi-column sort display (1, 2, etc)."
+ }
+ ],
+ "events": [],
+ "slots": [
+ {
+ "name": "default",
+ "description": ""
+ }
+ ]
+}
diff --git a/docs/generated/component-apis/table.json b/docs/generated/component-apis/table.json
index 15e3104de..3a17eb9c3 100644
--- a/docs/generated/component-apis/table.json
+++ b/docs/generated/component-apis/table.json
@@ -36,6 +36,18 @@
"default": "normal",
"description": "A relaxed variant of the table with more vertical padding for the cells."
},
+ {
+ "name": "sortMode",
+ "type": "\"single\" | \"multi\"",
+ "typeLabel": "GoabTableSortMode",
+ "values": [
+ "single",
+ "multi"
+ ],
+ "required": false,
+ "default": "single",
+ "description": "Sort mode: \"single\" allows one column, \"multi\" allows up to 2 columns."
+ },
{
"name": "testId",
"type": "string",
diff --git a/docs/src/data/configurations/index.ts b/docs/src/data/configurations/index.ts
index 37cb8943a..7376dea4d 100644
--- a/docs/src/data/configurations/index.ts
+++ b/docs/src/data/configurations/index.ts
@@ -73,6 +73,7 @@ export { cardContentConfigurations } from './card-content';
export { cardImageConfigurations } from './card-image';
export { cardActionsConfigurations } from './card-actions';
export { tableConfigurations } from './table';
+export { tableSortHeaderConfigurations } from './table-sort-header';
export { dataGridConfigurations } from './data-grid';
export { modalConfigurations } from './modal';
export { drawerConfigurations } from './drawer';
@@ -147,6 +148,7 @@ import { cardContentConfigurations } from './card-content';
import { cardImageConfigurations } from './card-image';
import { cardActionsConfigurations } from './card-actions';
import { tableConfigurations } from './table';
+import { tableSortHeaderConfigurations } from './table-sort-header';
import { dataGridConfigurations } from './data-grid';
import { modalConfigurations } from './modal';
import { drawerConfigurations } from './drawer';
@@ -231,6 +233,7 @@ export const configurationRegistry: ConfigurationRegistry = {
'card-image': cardImageConfigurations,
'card-actions': cardActionsConfigurations,
table: tableConfigurations,
+ 'table-sort-header': tableSortHeaderConfigurations,
'data-grid': dataGridConfigurations,
modal: modalConfigurations,
drawer: drawerConfigurations,
diff --git a/docs/src/data/configurations/table-sort-header.ts b/docs/src/data/configurations/table-sort-header.ts
new file mode 100644
index 000000000..f3b7c2897
--- /dev/null
+++ b/docs/src/data/configurations/table-sort-header.ts
@@ -0,0 +1,48 @@
+/**
+ * TableSortHeader Component Configurations
+ *
+ * Sort headers for table columns, used within Table's thead.
+ */
+
+import type { ComponentConfigurations } from './types';
+
+export const tableSortHeaderConfigurations: ComponentConfigurations = {
+ componentSlug: 'table-sort-header',
+ componentName: 'TableSortHeader',
+ defaultConfigurationId: 'basic',
+
+ configurations: [
+ {
+ id: 'basic',
+ name: 'Basic sort header',
+ description: 'Sortable column header with name and direction',
+ code: {
+ react: `
+ Name
+ `,
+ angular: `
+ Name
+ `,
+ webComponents: `
+ Name
+ `,
+ },
+ },
+ {
+ id: 'sort-order',
+ name: 'With sort order',
+ description: 'Sort header with sortOrder for multi-column sort priority display',
+ code: {
+ react: `
+ Name
+ `,
+ angular: `
+ Name
+ `,
+ webComponents: `
+ Name
+ `,
+ },
+ },
+ ],
+};
diff --git a/docs/src/data/configurations/table.ts b/docs/src/data/configurations/table.ts
index a1f7fa7fb..3d3c93ca5 100644
--- a/docs/src/data/configurations/table.ts
+++ b/docs/src/data/configurations/table.ts
@@ -399,6 +399,174 @@ export const tableConfigurations: ComponentConfigurations = {
+`,
+ },
+ },
+ {
+ id: 'single-sort',
+ name: 'Single-column sorting',
+ description: 'Sortable columns using TableSortHeader (default single sort mode)',
+ code: {
+ react: ` console.log(detail)}>
+
+
+
+ Name
+
+
+ Status
+
+
+ Date
+
+
+
+
+
+ John Smith
+ Active
+ Jan 15, 2024
+
+
+ Jane Doe
+ Pending
+ Jan 16, 2024
+
+
+ `,
+ angular: `
+
+
+
+ Name
+
+
+ Status
+
+
+ Date
+
+
+
+
+
+ John Smith
+ Active
+ Jan 15, 2024
+
+
+ Jane Doe
+ Pending
+ Jan 16, 2024
+
+
+ `,
+ webComponents: `
+
+
+
+ Name
+ Status
+ Date
+
+
+
+
+ John Smith
+ Active
+ Jan 15, 2024
+
+
+ Jane Doe
+ Pending
+ Jan 16, 2024
+
+
+
+ `,
+ },
+ },
+ {
+ id: 'multi-sort',
+ name: 'Multi-column sorting',
+ description: 'Sort by multiple columns with sortMode="multi" and sortOrder for priority',
+ code: {
+ react: ` console.log(detail)}>
+
+
+
+ Name
+
+
+ Status
+
+
+ Date
+
+
+
+
+
+ John Smith
+ Active
+ Jan 15, 2024
+
+
+ Jane Doe
+ Pending
+ Jan 16, 2024
+
+
+ `,
+ angular: `
+
+
+
+ Name
+
+
+ Status
+
+
+ Date
+
+
+
+
+
+ John Smith
+ Active
+ Jan 15, 2024
+
+
+ Jane Doe
+ Pending
+ Jan 16, 2024
+
+
+ `,
+ webComponents: `
+
+
+
+ Name
+ Status
+ Date
+
+
+
+
+ John Smith
+ Active
+ Jan 15, 2024
+
+
+ Jane Doe
+ Pending
+ Jan 16, 2024
+
+
+
`,
},
},
diff --git a/libs/angular-components/src/experimental/table-sort-header/table-sort-header.spec.ts b/libs/angular-components/src/experimental/table-sort-header/table-sort-header.spec.ts
index 91463dee5..8c82df241 100644
--- a/libs/angular-components/src/experimental/table-sort-header/table-sort-header.spec.ts
+++ b/libs/angular-components/src/experimental/table-sort-header/table-sort-header.spec.ts
@@ -17,6 +17,21 @@ class TestTableSortHeaderComponent {
/** do nothing **/
}
+@Component({
+ standalone: true,
+ imports: [GoabxTableSortHeader],
+ template: `
+
+
+ Salary
+
+
+ `,
+})
+class TestTableSortHeaderWithSortOrderComponent {
+ /** do nothing **/
+}
+
describe("GoABTableSortHeader", () => {
let fixture: ComponentFixture;
@@ -39,3 +54,26 @@ describe("GoABTableSortHeader", () => {
expect(el?.textContent).toContain("First name and really long header");
});
});
+
+describe("GoABTableSortHeader with sortOrder", () => {
+ let fixture: ComponentFixture;
+
+ beforeEach(fakeAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [GoabxTableSortHeader, TestTableSortHeaderWithSortOrderComponent],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(TestTableSortHeaderWithSortOrderComponent);
+ fixture.detectChanges();
+ tick();
+ fixture.detectChanges();
+ }));
+
+ it("should render sort-order attribute", () => {
+ const el = fixture.nativeElement.querySelector("goa-table-sort-header");
+ expect(el?.getAttribute("sort-order")).toBe("2");
+ expect(el?.getAttribute("direction")).toBe("desc");
+ expect(el?.getAttribute("name")).toBe("salary");
+ });
+});
diff --git a/libs/angular-components/src/experimental/table-sort-header/table-sort-header.ts b/libs/angular-components/src/experimental/table-sort-header/table-sort-header.ts
index 70b4a83a3..5b8ba41cf 100644
--- a/libs/angular-components/src/experimental/table-sort-header/table-sort-header.ts
+++ b/libs/angular-components/src/experimental/table-sort-header/table-sort-header.ts
@@ -1,4 +1,4 @@
-import { GoabTableSortDirection } from "@abgov/ui-components-common";
+import { GoabTableSortDirection, GoabTableSortOrder } from "@abgov/ui-components-common";
import {
CUSTOM_ELEMENTS_SCHEMA,
Component,
@@ -16,6 +16,7 @@ import { CommonModule } from "@angular/common";
*ngIf="isReady"
[attr.name]="name"
[attr.direction]="direction"
+ [attr.sort-order]="sortOrder"
>
@@ -27,6 +28,7 @@ export class GoabxTableSortHeader implements OnInit {
isReady = false;
@Input() name?: string;
@Input() direction?: GoabTableSortDirection = "none";
+ @Input() sortOrder?: GoabTableSortOrder;
constructor(private cdr: ChangeDetectorRef) {}
diff --git a/libs/angular-components/src/experimental/table/table.spec.ts b/libs/angular-components/src/experimental/table/table.spec.ts
index 11ce0650b..224fd0667 100644
--- a/libs/angular-components/src/experimental/table/table.spec.ts
+++ b/libs/angular-components/src/experimental/table/table.spec.ts
@@ -3,6 +3,7 @@ import { GoabxTable } from "./table";
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import {
GoabTableOnSortDetail,
+ GoabTableSortMode,
GoabTableVariant,
Spacing,
} from "@abgov/ui-components-common";
@@ -15,6 +16,8 @@ import { fireEvent } from "@testing-library/dom";
{
component.width = "200px";
component.variant = "relaxed";
+ component.sortMode = "multi";
+ component.striped = true;
component.testId = "foo";
component.mt = "s" as Spacing;
component.mb = "xl" as Spacing;
@@ -77,8 +84,11 @@ describe("GoabxTable", () => {
it("should render", () => {
const el = fixture.nativeElement.querySelector("goa-table");
+ expect(el?.getAttribute("version")).toBe("2");
expect(el?.getAttribute("width")).toBe(component.width);
expect(el?.getAttribute("variant")).toBe(component.variant);
+ expect(el?.getAttribute("sort-mode")).toBe(component.sortMode);
+ expect(el?.getAttribute("striped")).toBe("true");
expect(el?.getAttribute("testid")).toBe(component.testId);
expect(el?.getAttribute("mt")).toBe(component.mt);
expect(el?.getAttribute("mb")).toBe(component.mb);
@@ -95,13 +105,16 @@ describe("GoabxTable", () => {
it("should dispatch _sort", () => {
const onSort = jest.spyOn(component, "onSort");
const el = fixture.nativeElement.querySelector("goa-table");
+ const detail = {
+ sortBy: "column1",
+ sortDir: 1,
+ sorts: [{ column: "column1", direction: "asc" }],
+ };
fireEvent(
el,
- new CustomEvent("_sort", {
- detail: { sortBy: "column1", sortDir: 1 },
- }),
+ new CustomEvent("_sort", { detail }),
);
- expect(onSort).toHaveBeenCalledWith({ sortBy: "column1", sortDir: 1 });
+ expect(onSort).toHaveBeenCalledWith(detail);
});
});
diff --git a/libs/angular-components/src/experimental/table/table.ts b/libs/angular-components/src/experimental/table/table.ts
index e57e05cb1..be621400c 100644
--- a/libs/angular-components/src/experimental/table/table.ts
+++ b/libs/angular-components/src/experimental/table/table.ts
@@ -1,4 +1,8 @@
-import { GoabTableOnSortDetail, GoabTableVariant } from "@abgov/ui-components-common";
+import {
+ GoabTableOnSortDetail,
+ GoabTableSortMode,
+ GoabTableVariant,
+} from "@abgov/ui-components-common";
import {
CUSTOM_ELEMENTS_SCHEMA,
Component,
@@ -21,6 +25,7 @@ import { GoabBaseComponent } from "../base.component";
[attr.version]="version"
[attr.width]="width"
[attr.variant]="variant"
+ [attr.sort-mode]="sortMode"
[attr.striped]="striped"
[attr.testid]="testId"
[attr.mt]="mt"
@@ -42,6 +47,7 @@ export class GoabxTable extends GoabBaseComponent implements OnInit {
version = "2";
@Input() width?: string;
@Input() variant?: GoabTableVariant;
+ @Input() sortMode?: GoabTableSortMode;
@Input({ transform: booleanAttribute }) striped?: boolean;
constructor(private cdr: ChangeDetectorRef) {
diff --git a/libs/angular-components/src/lib/components/table-sort-header/table-sort-header.ts b/libs/angular-components/src/lib/components/table-sort-header/table-sort-header.ts
index 3d59b5347..bf2ccbd36 100644
--- a/libs/angular-components/src/lib/components/table-sort-header/table-sort-header.ts
+++ b/libs/angular-components/src/lib/components/table-sort-header/table-sort-header.ts
@@ -21,7 +21,6 @@ export class GoabTableSortHeader implements OnInit {
isReady = false;
@Input() name?: string;
@Input() direction?: GoabTableSortDirection = "none";
-
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit(): void {
diff --git a/libs/common/src/lib/common.ts b/libs/common/src/lib/common.ts
index 929677f21..3511922c3 100644
--- a/libs/common/src/lib/common.ts
+++ b/libs/common/src/lib/common.ts
@@ -242,9 +242,18 @@ export interface GoabTableProps extends Margins {
testId?: string;
}
+export type GoabTableSortMode = "single" | "multi";
+export type GoabTableSortOrder = 1 | 2;
+
+export type GoabTableSortEntry = {
+ column: string;
+ direction: "asc" | "desc";
+};
+
export type GoabTableOnSortDetail = {
sortBy: string;
sortDir: number;
+ sorts?: GoabTableSortEntry[];
};
// Spacer
diff --git a/libs/react-components/specs/table.browser.spec.tsx b/libs/react-components/specs/table.browser.spec.tsx
new file mode 100644
index 000000000..29b86aaaf
--- /dev/null
+++ b/libs/react-components/specs/table.browser.spec.tsx
@@ -0,0 +1,531 @@
+import { render } from "vitest-browser-react";
+import { vi, describe, it, expect } from "vitest";
+import { useState, useMemo } from "react";
+import { GoabxTable, GoabxTableSortHeader } from "../src/experimental";
+
+type SortEntry = { column: string; direction: "asc" | "desc" };
+type RowData = { id: number; name: string; department: string; salary: number };
+
+const initialData: RowData[] = [
+ { id: 1, name: "Alice Johnson", department: "Engineering", salary: 95000 },
+ { id: 2, name: "Bob Smith", department: "Marketing", salary: 72000 },
+ { id: 3, name: "Carol Williams", department: "Engineering", salary: 105000 },
+ { id: 4, name: "David Brown", department: "Sales", salary: 68000 },
+ { id: 5, name: "Eve Davis", department: "Marketing", salary: 78000 },
+];
+
+function sortData(data: RowData[], sorts: SortEntry[]): RowData[] {
+ if (sorts.length === 0) return data;
+ return [...data].sort((a, b) => {
+ for (const { column, direction } of sorts) {
+ const aVal = a[column as keyof RowData];
+ const bVal = b[column as keyof RowData];
+ let cmp = 0;
+ if (typeof aVal === "string" && typeof bVal === "string") {
+ cmp = aVal.localeCompare(bVal);
+ } else {
+ cmp = (aVal as number) - (bVal as number);
+ }
+ if (cmp !== 0) return direction === "asc" ? cmp : -cmp;
+ }
+ return 0;
+ });
+}
+
+/** Query row names from the light DOM render container (not shadow DOM). */
+function getRowNames(container: HTMLElement): string[] {
+ const rows = container.querySelectorAll("tbody tr");
+ return Array.from(rows).map((row) => row.querySelector("td")!.textContent!.trim());
+}
+
+/** Get the shadow DOM button inside a goa-table-sort-header, queried from the light DOM container. */
+function getSortHeaderButton(container: HTMLElement, name: string): HTMLElement {
+ const header = container.querySelector(`goa-table-sort-header[name="${name}"]`);
+ return header!.shadowRoot!.querySelector("button")!;
+}
+
+/** Wait for sort headers to be mounted and their shadow roots to be ready. */
+async function waitForHeaders(container: HTMLElement, count: number) {
+ await vi.waitFor(
+ () => {
+ const headers = container.querySelectorAll("goa-table-sort-header");
+ expect(headers.length).toBe(count);
+ // Ensure shadow roots are ready
+ headers.forEach((h) => {
+ expect(h.shadowRoot).toBeTruthy();
+ expect(h.shadowRoot!.querySelector("button")).toBeTruthy();
+ });
+ },
+ { timeout: 3000 },
+ );
+}
+
+describe("GoabxTable Browser Tests", () => {
+ describe("single-column sorting", () => {
+ it("should sort by name ascending on first click", async () => {
+ const onSort = vi.fn();
+
+ const Component = () => {
+ const [sorts, setSorts] = useState([]);
+ const sorted = useMemo(() => sortData(initialData, sorts), [sorts]);
+
+ return (
+ {
+ setSorts(detail.sorts ?? []);
+ onSort(detail);
+ }}
+ >
+
+
+
+ Name
+
+
+ Department
+
+
+ Salary
+
+
+
+
+ {sorted.map((row) => (
+
+ {row.name}
+ {row.department}
+ ${row.salary.toLocaleString()}
+
+ ))}
+
+
+ );
+ };
+
+ const { container } = render( );
+ await waitForHeaders(container, 3);
+
+ // Initial order should be the original data order
+ expect(getRowNames(container)).toEqual([
+ "Alice Johnson",
+ "Bob Smith",
+ "Carol Williams",
+ "David Brown",
+ "Eve Davis",
+ ]);
+
+ // Click Name header to sort ascending
+ getSortHeaderButton(container, "name").click();
+
+ await vi.waitFor(() => {
+ expect(onSort).toHaveBeenCalled();
+ expect(getRowNames(container)).toEqual([
+ "Alice Johnson",
+ "Bob Smith",
+ "Carol Williams",
+ "David Brown",
+ "Eve Davis",
+ ]);
+ });
+ });
+
+ it("should toggle sort direction on subsequent clicks", async () => {
+ const Component = () => {
+ const [sorts, setSorts] = useState([]);
+ const sorted = useMemo(() => sortData(initialData, sorts), [sorts]);
+
+ return (
+ {
+ setSorts(detail.sorts ?? []);
+ }}
+ >
+
+
+
+ Name
+
+
+ Salary
+
+
+
+
+ {sorted.map((row) => (
+
+ {row.name}
+ ${row.salary.toLocaleString()}
+
+ ))}
+
+
+ );
+ };
+
+ const { container } = render( );
+ await waitForHeaders(container, 2);
+
+ // First click — sort ascending
+ getSortHeaderButton(container, "name").click();
+ await vi.waitFor(() => {
+ expect(getRowNames(container)).toEqual([
+ "Alice Johnson",
+ "Bob Smith",
+ "Carol Williams",
+ "David Brown",
+ "Eve Davis",
+ ]);
+ });
+
+ // Second click — sort descending
+ getSortHeaderButton(container, "name").click();
+ await vi.waitFor(() => {
+ expect(getRowNames(container)).toEqual([
+ "Eve Davis",
+ "David Brown",
+ "Carol Williams",
+ "Bob Smith",
+ "Alice Johnson",
+ ]);
+ });
+ });
+
+ it("should switch sort column when clicking a different header", async () => {
+ const Component = () => {
+ const [sorts, setSorts] = useState([]);
+ const sorted = useMemo(() => sortData(initialData, sorts), [sorts]);
+
+ return (
+ {
+ setSorts(detail.sorts ?? []);
+ }}
+ >
+
+
+
+ Name
+
+
+ Salary
+
+
+
+
+ {sorted.map((row) => (
+
+ {row.name}
+ ${row.salary.toLocaleString()}
+
+ ))}
+
+
+ );
+ };
+
+ const { container } = render( );
+ await waitForHeaders(container, 2);
+
+ // Sort by name first
+ getSortHeaderButton(container, "name").click();
+ await vi.waitFor(() => {
+ expect(getRowNames(container)[0]).toBe("Alice Johnson");
+ });
+
+ // Now click salary — should sort by salary ascending, replacing name sort
+ getSortHeaderButton(container, "salary").click();
+ await vi.waitFor(() => {
+ // Salary asc: 68000, 72000, 78000, 95000, 105000
+ expect(getRowNames(container)).toEqual([
+ "David Brown",
+ "Bob Smith",
+ "Eve Davis",
+ "Alice Johnson",
+ "Carol Williams",
+ ]);
+ });
+ });
+ });
+
+ describe("multi-column sorting", () => {
+ it("should sort by two columns when clicking sequentially", async () => {
+ const Component = () => {
+ const [sorts, setSorts] = useState([]);
+ const sorted = useMemo(() => sortData(initialData, sorts), [sorts]);
+
+ return (
+ {
+ setSorts(detail.sorts ?? []);
+ }}
+ >
+
+
+
+ Name
+
+
+ Department
+
+
+ Salary
+
+
+
+
+ {sorted.map((row) => (
+
+ {row.name}
+ {row.department}
+ ${row.salary.toLocaleString()}
+
+ ))}
+
+
+ );
+ };
+
+ const { container } = render( );
+ await waitForHeaders(container, 3);
+
+ // Click department to sort ascending
+ getSortHeaderButton(container, "department").click();
+ await vi.waitFor(() => {
+ // Department asc: Engineering (Alice, Carol), Marketing (Bob, Eve), Sales (David)
+ const names = getRowNames(container);
+ expect(names[0]).toBe("Alice Johnson"); // Engineering
+ expect(names[4]).toBe("David Brown"); // Sales
+ });
+
+ // Click salary as secondary sort
+ getSortHeaderButton(container, "salary").click();
+ await vi.waitFor(() => {
+ // Dept asc then salary asc within each dept
+ // Engineering: Alice (95k), Carol (105k)
+ // Marketing: Bob (72k), Eve (78k)
+ // Sales: David (68k)
+ expect(getRowNames(container)).toEqual([
+ "Alice Johnson",
+ "Carol Williams",
+ "Bob Smith",
+ "Eve Davis",
+ "David Brown",
+ ]);
+ });
+ });
+ });
+
+ describe("declarative initial sort (multi)", () => {
+ it("should apply initial sort from direction and sortOrder props", async () => {
+ const onSort = vi.fn();
+
+ const Component = () => {
+ const [sorts, setSorts] = useState([]);
+ const sorted = useMemo(() => sortData(initialData, sorts), [sorts]);
+
+ return (
+ {
+ setSorts(detail.sorts ?? []);
+ onSort(detail);
+ }}
+ >
+
+
+
+ Name
+
+
+
+ Department
+
+
+
+
+ Salary
+
+
+
+
+
+ {sorted.map((row) => (
+
+ {row.name}
+ {row.department}
+ ${row.salary.toLocaleString()}
+
+ ))}
+
+
+ );
+ };
+
+ const { container } = render( );
+
+ // Wait for initial sort to be applied
+ await vi.waitFor(
+ () => {
+ // Dept asc, then salary desc within each dept
+ // Engineering: Carol (105k), Alice (95k)
+ // Marketing: Eve (78k), Bob (72k)
+ // Sales: David (68k)
+ expect(getRowNames(container)).toEqual([
+ "Carol Williams",
+ "Alice Johnson",
+ "Eve Davis",
+ "Bob Smith",
+ "David Brown",
+ ]);
+ },
+ { timeout: 3000 },
+ );
+
+ // Verify the initial onSort was dispatched
+ expect(onSort).toHaveBeenCalled();
+ const lastCall = onSort.mock.calls[onSort.mock.calls.length - 1][0];
+ expect(lastCall.sorts).toEqual([
+ { column: "department", direction: "asc" },
+ { column: "salary", direction: "desc" },
+ ]);
+ });
+
+ it("should update sort when clicking a header after initial sort", async () => {
+ const Component = () => {
+ const [sorts, setSorts] = useState([]);
+ const sorted = useMemo(() => sortData(initialData, sorts), [sorts]);
+
+ return (
+ {
+ setSorts(detail.sorts ?? []);
+ }}
+ >
+
+
+
+ Name
+
+
+
+ Department
+
+
+
+
+ Salary
+
+
+
+
+
+ {sorted.map((row) => (
+
+ {row.name}
+ {row.department}
+ ${row.salary.toLocaleString()}
+
+ ))}
+
+
+ );
+ };
+
+ const { container } = render( );
+
+ // Wait for initial sort to apply
+ await vi.waitFor(
+ () => {
+ expect(getRowNames(container)[0]).toBe("Carol Williams");
+ },
+ { timeout: 3000 },
+ );
+
+ // Click Name header — should add name as a sort column (replacing secondary since max is 2)
+ getSortHeaderButton(container, "name").click();
+
+ await vi.waitFor(() => {
+ // Department asc is primary, name asc replaces salary as secondary
+ // Engineering: Alice, Carol
+ // Marketing: Bob, Eve
+ // Sales: David
+ expect(getRowNames(container)).toEqual([
+ "Alice Johnson",
+ "Carol Williams",
+ "Bob Smith",
+ "Eve Davis",
+ "David Brown",
+ ]);
+ });
+ });
+ });
+
+ describe("sort header direction attribute", () => {
+ it("should update direction attribute on sort header after clicking", async () => {
+ const Component = () => {
+ return (
+ { /* noop */ }}>
+
+
+
+ Name
+
+
+ Department
+
+
+
+
+
+ Alice
+ Engineering
+
+
+
+ );
+ };
+
+ const { container } = render( );
+ await waitForHeaders(container, 2);
+
+ const nameHeader = container.querySelector('goa-table-sort-header[name="name"]')!;
+ const deptHeader = container.querySelector('goa-table-sort-header[name="department"]')!;
+
+ // Initially both should be "none"
+ expect(nameHeader.getAttribute("direction")).toBe("none");
+ expect(deptHeader.getAttribute("direction")).toBe("none");
+
+ // Click name header
+ getSortHeaderButton(container, "name").click();
+
+ await vi.waitFor(() => {
+ expect(nameHeader.getAttribute("direction")).toBe("asc");
+ expect(deptHeader.getAttribute("direction")).toBe("none");
+ });
+
+ // Click again for descending
+ getSortHeaderButton(container, "name").click();
+
+ await vi.waitFor(() => {
+ expect(nameHeader.getAttribute("direction")).toBe("desc");
+ expect(deptHeader.getAttribute("direction")).toBe("none");
+ });
+
+ // Click department — name should reset to none
+ getSortHeaderButton(container, "department").click();
+
+ await vi.waitFor(() => {
+ expect(nameHeader.getAttribute("direction")).toBe("none");
+ expect(deptHeader.getAttribute("direction")).toBe("asc");
+ });
+ });
+ });
+});
diff --git a/libs/react-components/src/experimental/index.ts b/libs/react-components/src/experimental/index.ts
index 84f2eb6b0..7f50fb313 100644
--- a/libs/react-components/src/experimental/index.ts
+++ b/libs/react-components/src/experimental/index.ts
@@ -26,7 +26,7 @@ export * from "./side-menu/side-menu";
export * from "./side-menu-group/side-menu-group";
export * from "./side-menu-heading/side-menu-heading";
export * from "./table/table";
-export * from "./table/table-sort-header";
+export * from "./table-sort-header/table-sort-header";
export * from "./tab/tab";
export * from "./tabs/tabs";
export * from "./textarea/textarea";
diff --git a/libs/react-components/src/experimental/table/table-sort-header.spec.tsx b/libs/react-components/src/experimental/table-sort-header/table-sort-header.spec.tsx
similarity index 67%
rename from libs/react-components/src/experimental/table/table-sort-header.spec.tsx
rename to libs/react-components/src/experimental/table-sort-header/table-sort-header.spec.tsx
index 8e0d0ed40..f866f8241 100644
--- a/libs/react-components/src/experimental/table/table-sort-header.spec.tsx
+++ b/libs/react-components/src/experimental/table-sort-header/table-sort-header.spec.tsx
@@ -19,4 +19,12 @@ describe("GoabxTableSortHeader", () => {
const el = document.querySelector("goa-table-sort-header");
expect(el?.getAttribute("data-grid")).toBe("cell");
});
+
+ it("should render sort-order attribute", () => {
+ render( );
+ const el = document.querySelector("goa-table-sort-header");
+ expect(el?.getAttribute("sort-order")).toBe("2");
+ expect(el?.getAttribute("name")).toBe("salary");
+ expect(el?.getAttribute("direction")).toBe("desc");
+ });
});
diff --git a/libs/react-components/src/experimental/table/table-sort-header.tsx b/libs/react-components/src/experimental/table-sort-header/table-sort-header.tsx
similarity index 63%
rename from libs/react-components/src/experimental/table/table-sort-header.tsx
rename to libs/react-components/src/experimental/table-sort-header/table-sort-header.tsx
index a1845a9f7..e44c2c3a5 100644
--- a/libs/react-components/src/experimental/table/table-sort-header.tsx
+++ b/libs/react-components/src/experimental/table-sort-header/table-sort-header.tsx
@@ -1,11 +1,11 @@
-import { DataAttributes, GoabTableSortDirection } from "@abgov/ui-components-common";
+import { DataAttributes, GoabTableSortDirection, GoabTableSortOrder } from "@abgov/ui-components-common";
import type { JSX } from "react";
-import { transformProps, lowercase } from "../../lib/common/extract-props";
interface WCProps {
name?: string;
direction?: GoabTableSortDirection;
+ "sort-order"?: GoabTableSortOrder;
}
declare module "react" {
@@ -21,16 +21,22 @@ declare module "react" {
export interface GoabxTableSortProps extends DataAttributes {
name?: string;
direction?: GoabTableSortDirection;
+ sortOrder?: GoabTableSortOrder;
children?: React.ReactNode;
}
export function GoabxTableSortHeader({
+ name,
+ direction = "none",
+ sortOrder,
children,
...rest
}: GoabxTableSortProps): JSX.Element {
- const _props = transformProps(rest, lowercase);
-
- return {children} ;
+ return (
+
+ {children}
+
+ );
}
export default GoabxTableSortHeader;
diff --git a/libs/react-components/src/experimental/table/table.spec.tsx b/libs/react-components/src/experimental/table/table.spec.tsx
index 30c9effc7..2ac994f28 100644
--- a/libs/react-components/src/experimental/table/table.spec.tsx
+++ b/libs/react-components/src/experimental/table/table.spec.tsx
@@ -12,13 +12,11 @@ describe("GoabxTable", () => {
});
it("should render with properties", () => {
- const { baseElement } = render( );
+ const { baseElement } = render( );
const table = baseElement.querySelector("goa-table");
- expect(table?.getAttribute("stickyHeader")).toBeNull();
expect(table?.getAttribute("striped")).toBe("true");
- // TODO: Enable this later if needed
- // expect(table?.getAttribute("stickyHeader")).toBe("true");
+ expect(table?.getAttribute("sort-mode")).toBe("multi");
});
it("should call onSort when _sort event is triggered", () => {
@@ -26,11 +24,11 @@ describe("GoabxTable", () => {
const { baseElement } = render( );
const table = baseElement.querySelector("goa-table");
- const event: GoabTableOnSortDetail = { sortBy: "name", sortDir: 1 };
+ const detail: GoabTableOnSortDetail = { sortBy: "name", sortDir: 1 };
expect(table).toBeTruthy();
- table && fireEvent(table, new CustomEvent("_sort", { detail: event }));
- expect(onSort).toHaveBeenCalledWith(event);
+ table && fireEvent(table, new CustomEvent("_sort", { detail }));
+ expect(onSort).toHaveBeenCalledWith(detail);
});
it("should handle _sort event gracefully when no onSort prop is passed", () => {
diff --git a/libs/react-components/src/experimental/table/table.tsx b/libs/react-components/src/experimental/table/table.tsx
index 33552ab7f..96299c3b7 100644
--- a/libs/react-components/src/experimental/table/table.tsx
+++ b/libs/react-components/src/experimental/table/table.tsx
@@ -1,5 +1,6 @@
import {
GoabTableOnSortDetail,
+ GoabTableSortMode,
GoabTableVariant,
Margins,
} from "@abgov/ui-components-common";
@@ -10,6 +11,7 @@ interface WCProps extends Margins {
width?: string;
stickyheader?: string;
variant?: GoabTableVariant;
+ "sort-mode"?: GoabTableSortMode;
testid?: string;
striped?: string;
version?: string;
@@ -28,6 +30,7 @@ declare module "react" {
export interface GoabxTableProps extends Margins {
width?: string;
onSort?: (detail: GoabTableOnSortDetail) => void;
+ sortMode?: GoabTableSortMode;
// stickyHeader?: boolean; TODO: enable this later
variant?: GoabTableVariant;
striped?: boolean;
@@ -39,7 +42,7 @@ export interface GoabxTableProps extends Margins {
// legacy name
export type TableProps = GoabxTableProps;
-export function GoabxTable({ onSort, version = "2", ...props }: GoabxTableProps) {
+export function GoabxTable({ onSort, sortMode, version = "2", ...props }: GoabxTableProps) {
const ref = useRef(null);
useEffect(() => {
if (!ref.current) {
@@ -64,6 +67,7 @@ export function GoabxTable({ onSort, version = "2", ...props }: GoabxTableProps)
// TODO: Enable this later if needed
// stickyheader={props.stickyHeader ? "true" : undefined}
variant={props.variant}
+ sort-mode={sortMode}
striped={props.striped ? "true" : undefined}
testid={props.testId}
mt={props.mt}
diff --git a/libs/web-components/src/components/table/Table.svelte b/libs/web-components/src/components/table/Table.svelte
index cbc0135f8..508378800 100644
--- a/libs/web-components/src/components/table/Table.svelte
+++ b/libs/web-components/src/components/table/Table.svelte
@@ -1,19 +1,26 @@
-
+
diff --git a/libs/web-components/src/components/table/TableSortHeader.spec.ts b/libs/web-components/src/components/table/TableSortHeader.spec.ts
index 61c327b6c..d7b4fbb9a 100644
--- a/libs/web-components/src/components/table/TableSortHeader.spec.ts
+++ b/libs/web-components/src/components/table/TableSortHeader.spec.ts
@@ -16,7 +16,7 @@ describe("GoATableSortHeader", () => {
const icon = container.querySelector("goa-icon");
expect(button.classList.contains("asc"));
- expect(icon.getAttribute("type")).toBe("caret-up")
+ expect(icon.getAttribute("type")).toBe("arrow-up")
})
it('binds desc direction param', async () => {
@@ -26,6 +26,6 @@ describe("GoATableSortHeader", () => {
const icon = container.querySelector("goa-icon");
expect(button.classList.contains("desc"));
- expect(icon.getAttribute("type")).toBe("caret-down")
+ expect(icon.getAttribute("type")).toBe("arrow-down")
})
})
diff --git a/libs/web-components/src/components/table/TableSortHeader.svelte b/libs/web-components/src/components/table/TableSortHeader.svelte
index f4a751f4a..bb865e1a0 100644
--- a/libs/web-components/src/components/table/TableSortHeader.svelte
+++ b/libs/web-components/src/components/table/TableSortHeader.svelte
@@ -1,48 +1,85 @@
-
+
-
+
- {#if direction === "desc"}
-
- {:else if direction === "asc"}
-
- {:else}
-
-
-
-
- {/if}
+
+ {sortOrder}
+ {#if direction === "desc"}
+
+ {:else if direction === "asc"}
+
+ {:else}
+
+ {/if}
+