From 4a74340ec9d37cefb043a5fe6286b16652ec6bd9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:52:04 +0000 Subject: [PATCH 01/15] chore(deps): bump react-router and react-router-dom Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) to 6.30.3 and updates ancestor dependency [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom). These dependencies need to be updated together. Updates `react-router` from 6.22.3 to 6.30.3 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/react-router@6.30.3/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@6.30.3/packages/react-router) Updates `react-router-dom` from 6.22.3 to 6.30.3 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.30.3/packages/react-router-dom) --- updated-dependencies: - dependency-name: react-router dependency-version: 6.30.3 dependency-type: indirect - dependency-name: react-router-dom dependency-version: 6.30.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 38 ++++++++++++++++---------------------- package.json | 2 +- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 00f7ab208..37a484c31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "octokit": "^4.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.13.0", + "react-router-dom": "^6.30.3", "use-debounce": "^10.0.4" }, "devDependencies": { @@ -700,7 +700,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", - "peer": true, "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.0.0", @@ -921,9 +920,10 @@ } }, "node_modules/@remix-run/router": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", - "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -1419,7 +1419,6 @@ "version": "18.2.67", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.67.tgz", "integrity": "sha512-vkIE2vTIMHQ/xL0rgmuoECBCkZFZeHr49HeWSc24AptMbNRo7pwSBvj73rlJJs9fGKj0koS+V7kQB1jHS0uCgw==", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1506,7 +1505,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -1679,7 +1677,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1959,7 +1956,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2878,7 +2874,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2890,7 +2885,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -2900,11 +2894,12 @@ } }, "node_modules/react-router": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", - "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.15.3" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -2914,12 +2909,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", - "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.15.3", - "react-router": "6.22.3" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" @@ -3221,7 +3217,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3266,7 +3261,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/package.json b/package.json index f8749177a..9f6db0209 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "octokit": "^4.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.13.0", + "react-router-dom": "^6.30.3", "use-debounce": "^10.0.4" }, "devDependencies": { From 06299d99b39fc68013eac4e1c6795c89bfb0d792 Mon Sep 17 00:00:00 2001 From: Vanessa Tran Date: Mon, 12 Jan 2026 10:56:22 -0700 Subject: [PATCH 02/15] feat(#2609): add doc for GoabDataGrid --- package-lock.json | 33 +- package.json | 6 +- src/components/sandbox/Sandbox.tsx | 6 + src/examples/data-grid/DataGridExamples.tsx | 21 + .../basic-table-with-keyboard-navigation.tsx | 159 +++++ .../data-grid/layout-mode-with-cards.tsx | 349 ++++++++++ .../sortable-table-with-row-selection.tsx | 362 ++++++++++ src/global-constants.ts | 1 + src/routes/components/Components.tsx | 1 + src/routes/components/DataGrid.tsx | 652 ++++++++++++++++++ src/routes/components/Table.tsx | 1 + src/versioned-router.tsx | 4 +- 12 files changed, 1570 insertions(+), 25 deletions(-) create mode 100644 src/examples/data-grid/DataGridExamples.tsx create mode 100644 src/examples/data-grid/basic-table-with-keyboard-navigation.tsx create mode 100644 src/examples/data-grid/layout-mode-with-cards.tsx create mode 100644 src/examples/data-grid/sortable-table-with-row-selection.tsx create mode 100644 src/routes/components/DataGrid.tsx diff --git a/package-lock.json b/package-lock.json index e72f7a76b..20a04c064 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,9 @@ "name": "code-sandbox", "version": "0.0.0", "dependencies": { - "@abgov/react-components": "6.10.0-dev.8", - "@abgov/ui-components-common": "1.10.0-dev.2", - "@abgov/web-components": "1.40.0-dev.17", + "@abgov/react-components": "6.10.0-dev.11", + "@abgov/ui-components-common": "1.10.0-dev.4", + "@abgov/web-components": "1.40.0-dev.21", "@faker-js/faker": "^8.3.1", "highlight.js": "^11.8.0", "js-cookie": "^3.0.5", @@ -68,9 +68,9 @@ } }, "node_modules/@abgov/react-components": { - "version": "6.10.0-dev.8", - "resolved": "https://registry.npmjs.org/@abgov/react-components/-/react-components-6.10.0-dev.8.tgz", - "integrity": "sha512-2acGDt2Hzw9QBYYgcdofL4k8b4WbFebj/DfPFTRP7nMpiTaAGaapw391c6qBHVm17WBpZAPAibOscoLvbnYSwA==", + "version": "6.10.0-dev.11", + "resolved": "https://registry.npmjs.org/@abgov/react-components/-/react-components-6.10.0-dev.11.tgz", + "integrity": "sha512-m7kCQvJ2l5ScDoyOxWF4h9Jx2V65GVy2aWBZR5C6fnaoFRDQSre3ml7+DwnnlAd3jNuDW7u8uyh3BhL0lpqD8A==", "license": "Apache-2.0", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", @@ -79,15 +79,15 @@ } }, "node_modules/@abgov/ui-components-common": { - "version": "1.10.0-dev.2", - "resolved": "https://registry.npmjs.org/@abgov/ui-components-common/-/ui-components-common-1.10.0-dev.2.tgz", - "integrity": "sha512-hta2YqaQfUD5PYg0lumsndI94NtQea7D8wtPYg52Pjf/StZEhT0qWST3LaK9KFNp3libhJ0lDb9j1/t5L+z0ug==", + "version": "1.10.0-dev.4", + "resolved": "https://registry.npmjs.org/@abgov/ui-components-common/-/ui-components-common-1.10.0-dev.4.tgz", + "integrity": "sha512-z4xl26B9qbzY74tPj5H+6YJYpDBaz2J5k9pKb9uJKn2PJR61tgX0iDeLtaUf1I/qvLcnJV2Tk/6zX/h8qX948Q==", "license": "Apache-2.0" }, "node_modules/@abgov/web-components": { - "version": "1.40.0-dev.17", - "resolved": "https://registry.npmjs.org/@abgov/web-components/-/web-components-1.40.0-dev.17.tgz", - "integrity": "sha512-wbxPI/yBmUw9BuTdYJTrsU41OXU3FFvEXMr8+KCQmX+KIEztUpQqWCcZ1FEyhfSXUmcAK+MIAfkj2B/CGrmO+w==", + "version": "1.40.0-dev.21", + "resolved": "https://registry.npmjs.org/@abgov/web-components/-/web-components-1.40.0-dev.21.tgz", + "integrity": "sha512-bRsJqSW6ZLRoW9AtGQs/vQHNrSv9Rd5Kv1ZD5oQqeooqZQSJLVgpyeXMqPqwDnALfZiLhZ6QB/6erziVlGy7cA==", "license": "Apache-2.0" }, "node_modules/@esbuild/aix-ppc64": { @@ -700,7 +700,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", - "peer": true, "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.0.0", @@ -1419,7 +1418,6 @@ "version": "18.2.67", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.67.tgz", "integrity": "sha512-vkIE2vTIMHQ/xL0rgmuoECBCkZFZeHr49HeWSc24AptMbNRo7pwSBvj73rlJJs9fGKj0koS+V7kQB1jHS0uCgw==", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1506,7 +1504,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -1679,7 +1676,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1959,7 +1955,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2878,7 +2873,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2890,7 +2884,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -3221,7 +3214,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3266,7 +3258,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/package.json b/package.json index 0370f34f3..49dc4f30d 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "prettier": "npx prettier . --write" }, "dependencies": { - "@abgov/react-components": "6.10.0-dev.8", - "@abgov/ui-components-common": "1.10.0-dev.2", - "@abgov/web-components": "1.40.0-dev.17", + "@abgov/react-components": "6.10.0-dev.11", + "@abgov/ui-components-common": "1.10.0-dev.4", + "@abgov/web-components": "1.40.0-dev.21", "@faker-js/faker": "^8.3.1", "highlight.js": "^11.8.0", "js-cookie": "^3.0.5", diff --git a/src/components/sandbox/Sandbox.tsx b/src/components/sandbox/Sandbox.tsx index f7a89edda..dbaf8ab37 100644 --- a/src/components/sandbox/Sandbox.tsx +++ b/src/components/sandbox/Sandbox.tsx @@ -259,6 +259,12 @@ function AdditionalCodeSnippets(props: AdditionalCodeSnippetsProps) { Array.isArray(el.props.tags) ? el.props.tags : [el.props.tags]; + + const isSharedSnippet = componentTags.includes("angular") && componentTags.includes("react"); + if (isSharedSnippet) { + return props.tags.some(tag => componentTags.includes(tag)); + } + if (props.tags.length !== componentTags.length) return false; return matches(componentTags); diff --git a/src/examples/data-grid/DataGridExamples.tsx b/src/examples/data-grid/DataGridExamples.tsx new file mode 100644 index 000000000..763d95cce --- /dev/null +++ b/src/examples/data-grid/DataGridExamples.tsx @@ -0,0 +1,21 @@ +import { SandboxHeader } from "@components/sandbox/sandbox-header/sandboxHeader.tsx"; +import { BasicTableWithKeyboardNavigation } from "./basic-table-with-keyboard-navigation.tsx"; +import { SortableTableWithRowSelection } from "./sortable-table-with-row-selection.tsx"; +import { LayoutModeWithCards } from "./layout-mode-with-cards.tsx"; + +export function DataGridExamples() { + return ( + <> + + + + + + + + + + ); +} + +export default DataGridExamples; diff --git a/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx b/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx new file mode 100644 index 000000000..1fe12608c --- /dev/null +++ b/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx @@ -0,0 +1,159 @@ +import { Sandbox } from "@components/sandbox"; +import { CodeSnippet } from "@components/code-snippet/CodeSnippet.tsx"; +import { + GoabBadge, + GoabButton, + GoabContainer, + GoabDataGrid, + GoabTable, +} from "@abgov/react-components"; + +export function BasicTableWithKeyboardNavigation() { + const users = [ + { id: "1", name: "Alice Johnson", role: "Developer", status: "Active" }, + { id: "2", name: "Bob Smith", role: "Designer", status: "Active" }, + { id: "3", name: "Carol White", role: "Manager", status: "Away" }, + { id: "4", name: "David Brown", role: "Analyst", status: "Active" }, + ]; + + return ( + <> + + + + + + Name + Role + Status + Actions + + + + {users.map((user) => ( + + {user.name} + {user.role} + + + + + View + + + ))} + + + + + + + {/* Angular Code */} + + + + + Name + Role + Status + Actions + + + + @for (user of users; track user.id) { + + {{ user.name }} + {{ user.role }} + + + + + View + + + } + + + `} + /> + + + + {/* React Code */} + + + + + + + Name + Role + Status + Actions + + + + {users.map((user) => ( + + {user.name} + {user.role} + + + + + View + + + ))} + + + `} + /> + + + ); +} + +export default BasicTableWithKeyboardNavigation; diff --git a/src/examples/data-grid/layout-mode-with-cards.tsx b/src/examples/data-grid/layout-mode-with-cards.tsx new file mode 100644 index 000000000..b64bb23a3 --- /dev/null +++ b/src/examples/data-grid/layout-mode-with-cards.tsx @@ -0,0 +1,349 @@ +import { Sandbox } from "@components/sandbox"; +import { CodeSnippet } from "@components/code-snippet/CodeSnippet.tsx"; +import { + GoabBadge, + GoabBlock, + GoabCheckbox, + GoabContainer, + GoabDataGrid, + GoabMenuAction, + GoabMenuButton, +} from "@abgov/react-components"; +import { GoabBadgeType } from "@abgov/ui-components-common"; + +type User = { + id: string; + name: string; + status: string; + updated: string; + email: string; + program: string; + programId: string; + serviceAccess: string; +}; + +export function LayoutModeWithCards() { + const users: User[] = [ + { + id: "1", + name: "Mike Zwei", + status: "Removed", + updated: "Jun 30, 2022 at 2:30 PM", + email: "mike.zwei@gmail.com", + program: "Wee Wild Ones Curry", + programId: "74528567", + serviceAccess: "Claims Adjustments", + }, + { + id: "2", + name: "Emma Stroman", + status: "To be removed", + updated: "Nov 28, 2021 at 1:30 PM", + email: "emma.stroman@gmail.com", + program: "Fort McMurray", + programId: "74522643", + serviceAccess: "Claims Adjustments", + }, + ]; + + const getStatusBadgeType = (status: string): GoabBadgeType => { + switch (status) { + case "Removed": + return "success"; + case "To be removed": + return "emergency"; + default: + return "information"; + } + }; + + return ( + <> + + + {users.map(user => ( + + + + + + + {user.name} + + + + + + Updated + {user.updated} + + + Email + {user.email} + + + Program + {user.program} + + + + + + Program ID + {user.programId} + + + Service access + {user.serviceAccess} + + + + + + + + + + + ))} + + + + + {/* Angular Code */} + + + + @for (user of users; track user.id) { + + + + + + + {{ user.name }} + + + + + + + + + Updated + {{ user.updated }} + + + Email + {{ user.email }} + + + Program + {{ user.program }} + + + + + + Program ID + {{ user.programId }} + + + Service access + {{ user.serviceAccess }} + + + + + + + + + + + } + `} + /> + + {/* React Code */} + { + switch (status) { + case "Removed": + return "success"; + case "To be removed": + return "emergency"; + default: + return "information"; + } + }; + + const handleMenuAction = (userId: string, event: GoabMenuButtonOnActionDetail) => { + if (event.action === "open") { + console.log("Open user:", userId); + } else if (event.action === "delete") { + console.log("Delete user:", userId); + } + };`} + /> + + + {users.map((user) => ( + + + + + + + {user.name} + + + + + + Updated + {user.updated} + + + Email + {user.email} + + + Program + {user.program} + + + + + + Program ID + {user.programId} + + + Service access + {user.serviceAccess} + + + + + handleMenuAction(user.id, e)}> + + + + + + ))} + `} + /> + + + ); +} + +export default LayoutModeWithCards; diff --git a/src/examples/data-grid/sortable-table-with-row-selection.tsx b/src/examples/data-grid/sortable-table-with-row-selection.tsx new file mode 100644 index 000000000..b5009964b --- /dev/null +++ b/src/examples/data-grid/sortable-table-with-row-selection.tsx @@ -0,0 +1,362 @@ +import { useState } from "react"; +import { Sandbox } from "@components/sandbox"; +import { CodeSnippet } from "@components/code-snippet/CodeSnippet.tsx"; +import { + GoabBadge, + GoabCheckbox, + GoabContainer, + GoabDataGrid, + GoabTable, + GoabTableSortHeader, + GoabMenuButton, + GoabMenuAction, +} from "@abgov/react-components"; +import { GoabTableOnSortDetail } from "@abgov/ui-components-common"; + +type Application = { + id: string; + applicant: string; + dateSubmitted: string; + status: string; + amount: string; +}; + +export function SortableTableWithRowSelection() { + const initialApplications: Application[] = [ + { id: "APP-001", applicant: "John Doe", dateSubmitted: "2024-01-15", status: "Approved", amount: "$5,000" }, + { id: "APP-002", applicant: "Jane Smith", dateSubmitted: "2024-01-18", status: "Pending", amount: "$3,500" }, + { id: "APP-003", applicant: "Bob Wilson", dateSubmitted: "2024-01-20", status: "In Review", amount: "$7,200" }, + { id: "APP-004", applicant: "Alice Brown", dateSubmitted: "2024-01-22", status: "Approved", amount: "$4,800" }, + { id: "APP-005", applicant: "Charlie Davis", dateSubmitted: "2024-01-25", status: "Denied", amount: "$2,500" }, + ]; + + const getStatusBadgeType = (status: string): "success" | "important" | "information" | "emergency" => { + switch (status) { + case "Approved": + return "success"; + case "Pending": + return "important"; + case "In Review": + return "information"; + case "Denied": + return "emergency"; + default: + return "information"; + } + }; + + const [applications, setApplications] = useState(initialApplications); + const [selectedIds, setSelectedIds] = useState([]); + const [isSelectedAll, setIsSelectedAll] = useState(false); + + const isSelected = (id: string): boolean => selectedIds.includes(id); + + const toggleSelection = (id: string) => { + if (selectedIds.includes(id)) { + setSelectedIds(selectedIds.filter((selectedId) => selectedId !== id)); + } else { + setSelectedIds([...selectedIds, id]); + } + }; + + const selectAll = (checked: boolean) => { + setIsSelectedAll(checked); + if (checked) { + setSelectedIds(applications.map((app) => app.id)); + } else { + setSelectedIds([]); + } + }; + + const handleSort = (event: GoabTableOnSortDetail) => { + const { sortBy, sortDir } = event; + const sorted = [...applications].sort((a: any, b: any) => + (a[sortBy] > b[sortBy] ? 1 : -1) * sortDir + ); + setApplications(sorted); + }; + + return ( + <> + + + + + + + selectAll(e.checked)} + /> + + + ID + + + Applicant + + + Date Submitted + + Status + Amount + Actions + + + + {applications.map((app) => ( + + + toggleSelection(app.id)} + /> + + {app.id} + {app.applicant} + {app.dateSubmitted} + + + + {app.amount} + + + + + + + + ))} + + + + + + + {/* Angular Code */} + selectedId !== id); + } else { + this.selectedIds = [...this.selectedIds, id]; + } + } + + selectAll(checked: boolean) { + this.isSelectedAll = checked; + this.selectedIds = checked ? this.applications.map(app => app.id) : []; + } + + handleSort(event: GoabTableOnSortDetail) { + const { sortBy, sortDir } = event; + this.applications = [...this.applications].sort((a: any, b: any) => + (a[sortBy] > b[sortBy] ? 1 : -1) * sortDir + ); + } + + getStatusBadgeType(status: string): GoabBadgeType { + const types: Record = { + "Approved": "success", + "Pending": "important", + "In Review": "information", + "Denied": "emergency" + }; + return types[status] || "information"; + } + }`} + /> + + + + + + + + + + ID + + + Applicant + + + Date Submitted + + Status + Amount + Actions + + + + @for (app of applications; track app.id) { + + + + + {{ app.id }} + {{ app.applicant }} + {{ app.dateSubmitted }} + + + + {{ app.amount }} + + + + + + + + } + + + `} + /> + + {/* React Code */} + ([]); + const [isSelectedAll, setIsSelectedAll] = useState(false); + + const isSelected = (id: string): boolean => selectedIds.includes(id); + + const toggleSelection = (id: string) => { + if (selectedIds.includes(id)) { + setSelectedIds(selectedIds.filter((selectedId) => selectedId !== id)); + } else { + setSelectedIds([...selectedIds, id]); + } + }; + + const selectAll = (checked: boolean) => { + setIsSelectedAll(checked); + setSelectedIds(checked ? applications.map((app) => app.id) : []); + }; + + const handleSort = (event: GoabTableOnSortDetail) => { + const { sortBy, sortDir } = event; + const sorted = [...applications].sort((a: any, b: any) => + (a[sortBy] > b[sortBy] ? 1 : -1) * sortDir + ); + setApplications(sorted); + }; + + const getStatusBadgeType = (status: string) => { + const types: Record = { + "Approved": "success", + "Pending": "important", + "In Review": "information", + "Denied": "emergency" + }; + return types[status] || "information"; + };`} + /> + + + + + + + selectAll(e.checked)} + /> + + + ID + + + Applicant + + + Date Submitted + + Status + Amount + Actions + + + + {applications.map((app) => ( + + + toggleSelection(app.id)} + /> + + {app.id} + {app.applicant} + {app.dateSubmitted} + + + + {app.amount} + + + + + + + + ))} + + + `} + /> + + + ); +} + +export default SortableTableWithRowSelection; diff --git a/src/global-constants.ts b/src/global-constants.ts index b33cc83e9..d1f05e75d 100644 --- a/src/global-constants.ts +++ b/src/global-constants.ts @@ -4,6 +4,7 @@ export const DEFAULT_LANGUAGE = "react"; // Array of 'New' components export const NEW_COMPONENTS = [ + "Data grid", "Drawer", "Temporary notification", "Checkbox list", diff --git a/src/routes/components/Components.tsx b/src/routes/components/Components.tsx index fc4c1aae4..e60e43bdb 100644 --- a/src/routes/components/Components.tsx +++ b/src/routes/components/Components.tsx @@ -66,6 +66,7 @@ export function Components() { Accordion Callout Container + {newComponentLabel("Data grid")} Details Hero banner List diff --git a/src/routes/components/DataGrid.tsx b/src/routes/components/DataGrid.tsx new file mode 100644 index 000000000..1e9753c78 --- /dev/null +++ b/src/routes/components/DataGrid.tsx @@ -0,0 +1,652 @@ +import { useState, useContext } from "react"; +import { + GoabBadge, + GoabCheckbox, + GoabContainer, + GoabDataGrid, + GoabDataGridProps, + GoabMenuAction, + GoabMenuButton, + GoabTab, + GoabTable, + GoabTableSortHeader, + GoabTabs, + GoabText, +} from "@abgov/react-components"; +import { Category, ComponentHeader } from "@components/component-header/ComponentHeader.tsx"; +import { + ComponentProperties, + ComponentProperty, +} from "@components/component-properties/ComponentProperties.tsx"; +import { ComponentContent } from "@components/component-content/ComponentContent"; +import { DesignEmpty } from "@components/empty-states/design-empty/DesignEmpty.tsx"; +import { AccessibilityEmpty } from "@components/empty-states/accessibility-empty/AccessibilityEmpty.tsx"; +import { CodeSnippet } from "@components/code-snippet/CodeSnippet.tsx"; +import { Sandbox, ComponentBinding } from "@components/sandbox"; +import { LanguageVersionContext } from "@contexts/LanguageVersionContext.tsx"; +import { OldComponentBanner } from "@components/old-component-banner/OldComponentBanner.tsx"; +import { DataGridExamples } from "@examples/data-grid/DataGridExamples.tsx"; + +// == Page props == + +const componentName = "Data grid"; +const description = + "A wrapper component that adds keyboard navigation and accessibility features to tables and layout grids. It implements the WAI-ARIA grid pattern for navigating through cells using arrow keys."; +const category = Category.CONTENT_AND_LAYOUT; +const relatedComponents = [{ link: "/components/table", name: "Table" }]; +const FIGMA_LINK = + "https://www.figma.com/design/pMvlCYzvrNw63lD5D6JpKA/Component---Data-Card-and-Data-Table?node-id=3632-932585&m=dev"; +const ACCESSIBILITY_FIGMA_LINK = + "https://www.figma.com/design/pMvlCYzvrNw63lD5D6JpKA/Component---Data-Card-and-Data-Table?node-id=2735-80003&m=dev"; + +type User = { + id: string; + name: string; + status: string; + email: string; +}; + +type ComponentPropsType = GoabDataGridProps; + +export default function DataGridPage() { + const { version, language } = useContext(LanguageVersionContext); + const [dataGridProps, setDataGridProps] = useState({ + keyboardNav: "table", + }); + + const initialUsers: User[] = [ + { id: "1", name: "Alice Johnson", status: "Active", email: "alice@example.com" }, + { id: "2", name: "Bob Smith", status: "Pending", email: "bob@example.com" }, + { id: "3", name: "Carol White", status: "Active", email: "carol@example.com" }, + ]; + + const getStatusBadgeType = (status: string): "success" | "important" | "information" => { + switch (status) { + case "Active": + return "success"; + case "Pending": + return "important"; + default: + return "information"; + } + }; + + const [users, setUsers] = useState(initialUsers); + const [selectedUsers, setSelectedUsers] = useState([]); + const [isSelectedAll, setIsSelectedAll] = useState(false); + + const [componentBindings, setComponentBindings] = useState([ + { + label: "Keyboard Navigation", + type: "dropdown", + name: "keyboardNav", + options: ["table", "layout"], + value: "table", + }, + { + label: "Icon Position", + type: "dropdown", + name: "keyboardIconPosition", + options: ["", "left", "right"], + value: "", + defaultValue: "left", + }, + { + label: "Icon Visibility", + type: "dropdown", + name: "keyboardIconVisibility", + options: ["", "visible", "hidden"], + value: "", + defaultValue: "visible", + }, + ]); + + const isSelected = (userId: string): boolean => { + return selectedUsers.includes(userId); + }; + + const toggleSelection = (userId: string) => { + if (selectedUsers.includes(userId)) { + setSelectedUsers(selectedUsers.filter(id => id !== userId)); + } else { + setSelectedUsers([...selectedUsers, userId]); + } + }; + + const selectAll = (checked: boolean) => { + setIsSelectedAll(checked); + if (checked) { + setSelectedUsers(users.map(u => u.id)); + } else { + setSelectedUsers([]); + } + }; + + const handleSort = (event: { sortBy: string; sortDir: number }) => { + const { sortBy, sortDir } = event; + const sortedUsers = [...users].sort( + (a: any, b: any) => (a[sortBy] > b[sortBy] ? 1 : -1) * sortDir + ); + setUsers(sortedUsers); + }; + + const handleMenuAction = (userId: string, action: string) => { + if (action === "view") { + alert(`Viewing user ${userId}`); + } else if (action === "delete") { + setUsers(users.filter(u => u.id !== userId)); + } + }; + + function onSandboxChange(bindings: ComponentBinding[], props: Record) { + setComponentBindings(bindings); + setDataGridProps({ keyboardNav: "table", ...props } as ComponentPropsType); + } + + const componentProperties: ComponentProperty[] = [ + { + name: "keyboardNav", + type: "GoabDataGridKeyboardNav (table | layout)", + required: true, + description: + "Defines the keyboard navigation mode. Use 'table' for traditional table structures where navigation stops at row boundaries. Use 'layout' for grid layouts where navigation wraps between rows.", + }, + { + name: "keyboardIconVisibility", + type: "GoabDataGridIconVisibility (visible | hidden)", + defaultValue: "visible", + description: + "Controls whether the keyboard navigation indicator icon is displayed when navigating with arrow keys.", + }, + { + name: "keyboardIconPosition", + type: "GoabDataGridIconPosition (left | right)", + defaultValue: "left", + description: "Sets the position of the keyboard navigation indicator icon.", + }, + ]; + + return ( + <> + + + {version === "old" && ( + + )} + + {version === "new" && ( + + + +

+ Component +

+ + {/* Don't render inside Sandbox because the table with interactive elements (arrow right left cannot focus on action button directly inside sandbox */} + + + + + + + selectAll(e.checked)} + /> + + + Name + + + Status + + Email + Actions + + + + {users.map(user => ( + + + toggleSelection(user.id)} + /> + + {user.name} + + + + {user.email} + + handleMenuAction(user.id, e.action)}> + + + + + + ))} + + + + + + + + + {/* Angular */} + = { + "Active": "success", + "Pending": "important" + }; + return types[status] || "information"; + } + + isSelected(userId: string): boolean { + return this.selectedUsers.includes(userId); + } + + handleSort(event: GoabTableOnSortDetail) { + const { sortBy, sortDir } = event; + this.users.sort((a: any, b: any) => (a[sortBy] > b[sortBy] ? 1 : -1) * sortDir); + } + + selectAll(event: GoabCheckboxOnChangeDetail) { + this.isSelectedAll = event.checked; + this.selectedUsers = event.checked ? this.users.map(u => u.id) : []; + } + + toggleSelection(userId: string, event: GoabCheckboxOnChangeDetail) { + if (event.checked) { + this.selectedUsers.push(userId); + } else { + this.selectedUsers = this.selectedUsers.filter(id => id !== userId); + } + } + + handleMenuAction(userId: string, event: GoabMenuButtonOnActionDetail) { + if (event.action === "view") { + console.log("View user:", userId); + } else if (event.action === "delete") { + this.users = this.users.filter(u => u.id !== userId); + } + } + }`} + /> + + + + + + + + + + + Name + + + Status + + Email + Actions + + + + @for (user of users; track user.id) { + + + + + + {{ user.name }} + + + + {{ user.email }} + + + + + + + + } + + + `} + /> + + {/* React */} + ([ + { id: "1", name: "Alice Johnson", status: "Active", email: "alice@example.com" }, + { id: "2", name: "Bob Smith", status: "Pending", email: "bob@example.com" }, + ]); + const [selectedUsers, setSelectedUsers] = useState([]); + const [isSelectedAll, setIsSelectedAll] = useState(false); + + const getStatusBadgeType = (status: string): "success" | "important" | "information" => { + switch (status) { + case "Active": + return "success"; + case "Pending": + return "important"; + default: + return "information"; + } + }; + + const isSelected = (userId: string): boolean => { + return selectedUsers.includes(userId); + }; + + const handleSort = (event: GoabTableOnSortDetail) => { + const { sortBy, sortDir } = event; + const sortedUsers = [...users].sort( + (a: any, b: any) => (a[sortBy] > b[sortBy] ? 1 : -1) * sortDir + ); + setUsers(sortedUsers); + }; + + const selectAll = (event: GoabCheckboxOnChangeDetail) => { + setIsSelectedAll(event.checked); + setSelectedUsers(event.checked ? users.map(u => u.id) : []); + }; + + const toggleSelection = (userId: string, event: GoabCheckboxOnChangeDetail) => { + if (event.checked) { + setSelectedUsers([...selectedUsers, userId]); + } else { + setSelectedUsers(selectedUsers.filter(id => id !== userId)); + } + }; + + const handleMenuAction = (userId: string, action: string) => { + if (action === "view") { + console.log("View user:", userId); + } else if (action === "delete") { + setUsers(users.filter(u => u.id !== userId)); + } + };`} + /> + + + + + + + + + + Name + + + Status + + Email + Actions + + + + {users.map((user) => ( + + + toggleSelection(user.id, e)} + /> + + {user.name} + + + + {user.email} + + handleMenuAction(user.id, e.action)} + > + + + + + + ))} + + + `} + /> + + + + +

Data attributes

+ + The Data Grid component uses data-grid attributes to identify rows and + cells for keyboard navigation. + + + + + + Attribute + Description + Usage + + + + + + data-grid="row" + + Marks an element as a row in the grid + + Apply to <tr> elements or container elements representing + rows + + + + + data-grid="cell" + + Marks an element as a cell in the grid + + Apply to <td>, <th>, or any element + representing a cell + + + + + data-grid="cell-N" + + Marks a cell with explicit ordering (where N is a number) + + Use in layout mode to control cell order when HTML order differs from visual + order + + + + + +

Keyboard navigation

+ + The Data Grid implements the WAI-ARIA grid pattern for keyboard navigation: + + + + + + Key + Action + + + + + + Arrow Right + + + Move focus one cell to the right. In table mode, stops at row end. In layout + mode, wraps to next row. + + + + + Arrow Left + + + Move focus one cell to the left. In table mode, stops at row start. In layout + mode, wraps to previous row. + + + + + Arrow Down + + Move focus one row down in the same column position. + + + + Arrow Up + + Move focus one row up in the same column position. + + + + Home + + Move focus to the first cell in the current row. + + + + End + + Move focus to the last cell in the current row. + + + + Tab + + + Move focus to the next interactive element within the current cell, or exit + the grid if at the last element. + + + + +
+ + + Examples + + + }> + + + + + {FIGMA_LINK ? ( + + ) : ( + + Design guidelines for this component are coming soon. + + )} + + + + + +
+
+ )} + + ); +} diff --git a/src/routes/components/Table.tsx b/src/routes/components/Table.tsx index e54a2960b..6af9d4d90 100644 --- a/src/routes/components/Table.tsx +++ b/src/routes/components/Table.tsx @@ -114,6 +114,7 @@ export default function TablePage() { description="A set of structured data that is easy for a user to scan, examine, and compare." relatedComponents={[ { link: "/components/button", name: "Button" }, + { link: "/components/data-grid", name: "Data grid" }, { link: "/components/dropdown", name: "Dropdown" }, { link: "/components/filter-chip", name: "Filter chip" }, { link: "/components/pagination", name: "Pagination" }, diff --git a/src/versioned-router.tsx b/src/versioned-router.tsx index 38e86020e..34927735c 100644 --- a/src/versioned-router.tsx +++ b/src/versioned-router.tsx @@ -58,6 +58,7 @@ import PublicForm from "@examples/public-form.tsx"; import FilterChipPage from "@routes/components/FilterChip.tsx"; import TextPage from "@routes/components/Text.tsx"; import { DrawerPage } from "@routes/components/Drawer.tsx"; +import DataGridPage from "@routes/components/DataGrid.tsx"; import LinkPage from "@routes/components/Link.tsx"; import TemporaryNotificationPage from "@routes/components/TemporaryNotification.tsx"; @@ -97,7 +98,8 @@ export const ComponentsRouter = () => { callout: , checkbox: , "checkbox-list": , - container: , + "container": , + "data-grid": , "date-picker": , details: , divider: , From c089aba8b031ab6eb1d376f970ae0867f2324bea Mon Sep 17 00:00:00 2001 From: Vanessa Tran Date: Tue, 13 Jan 2026 09:40:37 -0700 Subject: [PATCH 03/15] fix: make Sandbox generic to resolve typescript vs data attributes --- src/components/sandbox/Sandbox.tsx | 34 +++++++++++------ .../show-links-to-navigation-items.tsx | 8 +--- src/hooks/useSandboxFormItem.tsx | 2 +- src/routes/components/Accordion.tsx | 13 ++----- src/routes/components/AppFooter.tsx | 11 ++---- src/routes/components/AppHeader.tsx | 10 ++--- src/routes/components/Badge.tsx | 13 ++----- src/routes/components/Callout.tsx | 11 ++---- src/routes/components/Checkbox.tsx | 11 ++---- src/routes/components/Container.tsx | 9 ++--- src/routes/components/DatePicker.tsx | 11 ++---- src/routes/components/Dropdown.tsx | 14 ++----- src/routes/components/FileUploader.tsx | 38 +++++-------------- src/routes/components/FilterChip.tsx | 11 ++---- src/routes/components/IconButton.tsx | 11 ++---- src/routes/components/MenuButton.tsx | 12 ++---- src/routes/components/Radio.tsx | 13 ++----- src/routes/components/TextField.tsx | 12 ++---- src/routes/components/Tooltip.tsx | 10 ++--- 19 files changed, 80 insertions(+), 174 deletions(-) diff --git a/src/components/sandbox/Sandbox.tsx b/src/components/sandbox/Sandbox.tsx index dbaf8ab37..134c82d64 100644 --- a/src/components/sandbox/Sandbox.tsx +++ b/src/components/sandbox/Sandbox.tsx @@ -21,12 +21,12 @@ type Flag = "reactive" | "template-driven" | "event"; type ComponentType = "goa" | "codesnippet"; type Serializer = (el: any, properties: ComponentBinding[]) => string; -interface SandboxProps { +interface SandboxProps> { properties?: ComponentBinding[]; formItemProperties?: ComponentBinding[]; note?: string | { type?: GoabCalloutType; heading?: string; content: string }; fullWidth?: boolean; - onChange?: (bindings: ComponentBinding[], props: Record) => void; + onChange?: (bindings: ComponentBinding[], props: T) => void; onChangeFormItemBindings?: (bindings: ComponentBinding[], props: Record) => void; flags?: Flag[]; skipRender?: boolean; // prevent rendering the snippet, to allow custom code to be shown @@ -41,11 +41,11 @@ interface SandboxProps { type SandboxViewProps = { fullWidth?: boolean; - sandboxProps: SandboxProps; + sandboxProps: SandboxProps; background?: string; }; -export const Sandbox = (props: SandboxProps) => { +export const Sandbox = ,>(props: SandboxProps) => { const {language: lang, version} = useContext(LanguageVersionContext); const [formatLang, setFormatLang] = useState(""); @@ -92,10 +92,20 @@ export const Sandbox = (props: SandboxProps) => { } function onChangeFormItemBindings(bindings: ComponentBinding[]) { - props.onChangeFormItemBindings?.(bindings, toKeyValue(bindings)); + props.onChangeFormItemBindings?.(bindings, toFormItemKeyValue(bindings)); } - function toKeyValue(bindings: ComponentBinding[]) { + function toKeyValue(bindings: ComponentBinding[]): T { + return bindings.reduce((acc: Record, prop: ComponentBinding) => { + if (typeof prop.value === "string" && prop.value === "") { + return acc; + } + acc[prop.name] = prop.value; + return acc; + }, {}) as unknown as T; + } + + function toFormItemKeyValue(bindings: ComponentBinding[]): Record { return bindings.reduce((acc: Record, prop: ComponentBinding) => { if (typeof prop.value === "string" && prop.value === "") { return acc; @@ -117,7 +127,7 @@ export const Sandbox = (props: SandboxProps) => { return ( <> - {props.skipRenderDom ? null : } + {props.skipRenderDom ? null : } background={props.background} />} {/* Only render the GoAAccordion if props.properties is provided */} {props.properties && props.properties.length > 0 && ( @@ -141,7 +151,7 @@ export const Sandbox = (props: SandboxProps) => { /> )} - + } formatLang={formatLang} lang={lang} serializers={serializers} version={version} /> {props.note && (typeof props.note === "string" ? (

{props.note}

@@ -162,7 +172,7 @@ export const Sandbox = (props: SandboxProps) => { }; type SandboxCodeProps = { - props: SandboxProps & { children: ReactNode }; + props: SandboxProps & { children: ReactNode }; lang: string; formatLang: string; serializers: Record; @@ -246,7 +256,7 @@ function SandboxCode(p: SandboxCodeProps) { // to be displayed, while hiding the non-reactive ones type AdditionalCodeSnippetsProps = { tags: string[]; - sandboxProps: SandboxProps; + sandboxProps: SandboxProps; } function AdditionalCodeSnippets(props: AdditionalCodeSnippetsProps) { const matches = (list: string[]): boolean => { @@ -275,7 +285,7 @@ function AdditionalCodeSnippets(props: AdditionalCodeSnippetsProps) { // Filters components from within the Sandbox children // i.e. Get all the components type ComponentListProps = { - sandboxProps: SandboxProps; + sandboxProps: SandboxProps; type: ComponentType; } function ComponentList(props: ComponentListProps): ReactElement[] { @@ -295,7 +305,7 @@ function ComponentList(props: ComponentListProps): ReactElement[] { type ComponentOutputProps = { formatLang: string; type: "angular" | "angular-reactive" | "angular-template-driven" | "react"; - sandboxProps: SandboxProps; + sandboxProps: SandboxProps; serializer: Serializer; } diff --git a/src/examples/show-links-to-navigation-items.tsx b/src/examples/show-links-to-navigation-items.tsx index 9234e49e3..1b024fa00 100644 --- a/src/examples/show-links-to-navigation-items.tsx +++ b/src/examples/show-links-to-navigation-items.tsx @@ -12,10 +12,6 @@ import { propsToString } from "@components/sandbox/BaseSerializer.ts"; type FooterNavPropsType = GoabFooterNavSectionProps; type FooterPropsType = GoabAppFooterProps; -type CastingType = { - // add any required props here - [key: string]: unknown; -}; export const ShowLinksToNavigationItems = () => { const {version} = useContext(LanguageVersionContext); @@ -55,8 +51,8 @@ export const ShowLinksToNavigationItems = () => { maxColumnCount: props.maxColumnCount || 1 }; - setFooterProps(footerProps as CastingType); - setFooterNavSectionProps(footerNavSectionProps as CastingType); + setFooterProps(footerProps as FooterPropsType); + setFooterNavSectionProps(footerNavSectionProps as FooterNavPropsType); setAppFooterNavBindings(bindings); } diff --git a/src/hooks/useSandboxFormItem.tsx b/src/hooks/useSandboxFormItem.tsx index 8c5c96dff..c9c09038c 100644 --- a/src/hooks/useSandboxFormItem.tsx +++ b/src/hooks/useSandboxFormItem.tsx @@ -43,7 +43,7 @@ export const useSandboxFormItem = (initialProps: GoabFormItemProps) => { function onFormItemChange(bindings: ComponentBinding[], props: Record) { setFormItemBindings(bindings); - setFormItemProps(props); + setFormItemProps(props as GoabFormItemProps); } return { formItemBindings, formItemProps, onFormItemChange }; diff --git a/src/routes/components/Accordion.tsx b/src/routes/components/Accordion.tsx index 6390402b5..dbefe481f 100644 --- a/src/routes/components/Accordion.tsx +++ b/src/routes/components/Accordion.tsx @@ -14,7 +14,6 @@ import { Category, ComponentHeader } from "@components/component-header/Componen import { useState } from "react"; import AccordionExamples from "@examples/accordion/AccordionExamples.tsx"; import { ComponentContent } from "@components/component-content/ComponentContent"; -import { GoabAccordionHeadingSize } from "@abgov/ui-components-common"; import { LegacyMarginProperty, LegacyTestIdProperties, @@ -35,12 +34,6 @@ const FIGMA_LINK = "https://www.figma.com/design/3pb2IK8s2QUqWieH79KdN7/%E2%9D%9 type ComponentPropsType = GoabAccordionProps; -type CastingType = { - heading: string; - headingSize: GoabAccordionHeadingSize; - children: React.ReactNode; - [key: string]: unknown; -}; export default function AccordionPage() { @@ -215,9 +208,9 @@ export default function AccordionPage() { MarginProperty, ]; - function onSandboxChange(bindings: ComponentBinding[], props: Record) { + function onSandboxChange(bindings: ComponentBinding[], props: ComponentPropsType) { setAccordionBindings(bindings); - setAccordionProps(props as CastingType); + setAccordionProps(props); } return ( @@ -237,7 +230,7 @@ export default function AccordionPage() {

Playground

- + properties={accordionBindings} onChange={onSandboxChange} fullWidth> This is the content in an accordion item. This content can be anything that you want including rich text, components, and more. diff --git a/src/routes/components/AppFooter.tsx b/src/routes/components/AppFooter.tsx index 2c71b9c19..a8b735df7 100644 --- a/src/routes/components/AppFooter.tsx +++ b/src/routes/components/AppFooter.tsx @@ -30,11 +30,6 @@ const relatedComponents = [ ]; type ComponentPropsType = GoabAppFooterProps; -type CastingType = { - // add any required props here - [key: string]: unknown; -}; - export default function AppFooterPage() { const {language} = useContext(LanguageVersionContext); @@ -88,8 +83,8 @@ export default function AppFooterPage() { }, ]; - function onSandbox1Change(bindings: ComponentBinding[], props: Record) { - setSandbox1Props(props as CastingType); + function onSandbox1Change(bindings: ComponentBinding[], props: ComponentPropsType) { + setSandbox1Props(props); setAppFooterBindings(bindings); } @@ -110,7 +105,7 @@ export default function AppFooterPage() { Playground

Basic Footer

- + properties={appFooterBindings} onChange={onSandbox1Change} fullWidth> diff --git a/src/routes/components/AppHeader.tsx b/src/routes/components/AppHeader.tsx index 0818fb92d..923ec9df3 100644 --- a/src/routes/components/AppHeader.tsx +++ b/src/routes/components/AppHeader.tsx @@ -29,10 +29,6 @@ const relatedComponents = [ { link: "/components/microsite-header", name: "Microsite header" } ]; type ComponentPropsType = GoabAppHeaderProps; -type CastingType = { - // add any required props here - [key: string]: unknown; -}; export default function AppHeaderPage() { const [appHeaderProps, setAppHeaderProps] = useState({ url: "www.alberta.ca", @@ -185,8 +181,8 @@ export default function AppHeaderPage() { ]; - function onSandboxChange(bindings: ComponentBinding[], props: Record) { - setAppHeaderProps(props as CastingType); + function onSandboxChange(bindings: ComponentBinding[], props: ComponentPropsType) { + setAppHeaderProps(props); setAppHeaderBindings(bindings); } @@ -207,7 +203,7 @@ export default function AppHeaderPage() {

Playground

- + properties={appHeaderBindings} onChange={onSandboxChange} fullWidth> diff --git a/src/routes/components/Badge.tsx b/src/routes/components/Badge.tsx index c333ec495..611c974a8 100644 --- a/src/routes/components/Badge.tsx +++ b/src/routes/components/Badge.tsx @@ -8,7 +8,6 @@ import { } from "@components/component-properties/ComponentProperties.tsx"; import { ComponentContent } from "@components/component-content/ComponentContent"; import BadgeExamples from "@examples/badge/BadgeExamples.tsx"; -import { GoabBadgeType } from "@abgov/ui-components-common"; import { DesignEmpty } from "@components/empty-states/design-empty/DesignEmpty.tsx"; import { AccessibilityEmpty } from "@components/empty-states/accessibility-empty/AccessibilityEmpty.tsx"; import ICONS from "@routes/components/icons.json"; @@ -37,12 +36,6 @@ const relatedComponents = [ ]; type ComponentPropsType = GoabBadgeProps; -type CastingType = { - // add any required props here - type: GoabBadgeType; - content: string; - [key: string]: unknown; -}; export default function BadgePage() { const [badgeProps, setBadgeProps] = useState({ @@ -203,9 +196,9 @@ export default function BadgePage() { }, ]; - function onSandboxChange(badgeBindings: ComponentBinding[], props: Record) { + function onSandboxChange(badgeBindings: ComponentBinding[], props: ComponentPropsType) { setBadgeBindings(badgeBindings); - setBadgeProps(props as CastingType); + setBadgeProps(props); } return ( @@ -224,7 +217,7 @@ export default function BadgePage() {

Playground

- + properties={badgeBindings} onChange={onSandboxChange}> ({ @@ -163,9 +158,9 @@ export default function CalloutPage() { MarginProperty, ]; - function onSandboxChange(bindings: ComponentBinding[], props: Record) { + function onSandboxChange(bindings: ComponentBinding[], props: ComponentPropsType) { setComponentBindings(bindings); - setComponentProps(props as CastingType); + setComponentProps(props); } return ( @@ -185,7 +180,7 @@ export default function CalloutPage() {

Playground

- + properties={componentBindings} onChange={onSandboxChange}> Callout important information for the user. diff --git a/src/routes/components/Checkbox.tsx b/src/routes/components/Checkbox.tsx index 113fe77b2..ac6e5cabf 100644 --- a/src/routes/components/Checkbox.tsx +++ b/src/routes/components/Checkbox.tsx @@ -30,11 +30,6 @@ const relatedComponents = [ ]; const FIGMA_LINK = "https://www.figma.com/design/3pb2IK8s2QUqWieH79KdN7/%E2%9D%96-Component-library-%7C-DDD?node-id=183-219"; type ComponentPropsType = GoabCheckboxProps; -type CastingType = { - name: string; - checked: boolean; - [key: string]: unknown; -}; export default function CheckboxPage() { const {version} = useContext(LanguageVersionContext); const [checkboxProps, setCheckboxProps] = useState({ @@ -241,7 +236,7 @@ export default function CheckboxPage() { const noop = () => {}; - function onChange(bindings: ComponentBinding[], props: Record) { + function onChange(bindings: ComponentBinding[], props: ComponentPropsType) { const missingProps = { name: "item", checked: false, @@ -250,7 +245,7 @@ export default function CheckboxPage() { const updatedProps = { ...missingProps, ...props }; setCheckboxBindings(bindings); - setCheckboxProps(updatedProps as CastingType); + setCheckboxProps(updatedProps); } return ( @@ -268,7 +263,7 @@ export default function CheckboxPage() {

Playground

- properties={checkboxBindings} formItemProperties={formItemBindings} onChange={onChange} diff --git a/src/routes/components/Container.tsx b/src/routes/components/Container.tsx index 1a1c9e493..820966d8e 100644 --- a/src/routes/components/Container.tsx +++ b/src/routes/components/Container.tsx @@ -27,9 +27,6 @@ const relatedComponents = [ { link: "/components/divider", name: "Divider" } ]; type ComponentPropsType = GoabContainerProps; -type CastingType = { - [key: string]: unknown; -}; export default function ContainerPage() { const [containerProps, setContainerProps] = useState({}); @@ -213,9 +210,9 @@ export default function ContainerPage() { }, ]; - function onSandboxChange(bindings: ComponentBinding[], props: Record) { + function onSandboxChange(bindings: ComponentBinding[], props: ComponentPropsType) { setContainerBindings(bindings); - setContainerProps(props as CastingType); + setContainerProps(props); } return ( @@ -234,7 +231,7 @@ export default function ContainerPage() {

Playground

- + properties={containerBindings} onChange={onSandboxChange} fullWidth>

Detach to use

Add things inside me

diff --git a/src/routes/components/DatePicker.tsx b/src/routes/components/DatePicker.tsx index e7fa0f57d..284db843f 100644 --- a/src/routes/components/DatePicker.tsx +++ b/src/routes/components/DatePicker.tsx @@ -17,7 +17,6 @@ import { import { useSandboxFormItem } from "@hooks/useSandboxFormItem.tsx"; import { CodeSnippet } from "@components/code-snippet/CodeSnippet.tsx"; import { ComponentContent } from "@components/component-content/ComponentContent"; -import { GoabDatePickerOnChangeDetail } from "@abgov/ui-components-common"; import { LanguageVersionContext } from "@contexts/LanguageVersionContext.tsx"; import { LegacyMarginProperty, @@ -40,10 +39,6 @@ const relatedComponents = [ const description = "Lets users select a date through a calendar without the need to manually type it in a field."; type ComponentPropsType = GoabDatePickerProps; -type CastingType = { - [key: string]: unknown; - onChange: (event: GoabDatePickerOnChangeDetail) => void; -}; export default function DatePickerPage() { const { version } = useContext(LanguageVersionContext); @@ -217,9 +212,9 @@ export default function DatePickerPage() { }, ]; - function onSandboxChange(bindings: ComponentBinding[], props: Record) { + function onSandboxChange(bindings: ComponentBinding[], props: ComponentPropsType) { setComponentBindings(bindings); - setComponentProps(props as CastingType); + setComponentProps(props); } const noop = () => {}; @@ -241,7 +236,7 @@ export default function DatePickerPage() {

Playground

- properties={componentBindings} formItemProperties={formItemBindings} onChange={onSandboxChange} diff --git a/src/routes/components/Dropdown.tsx b/src/routes/components/Dropdown.tsx index da729e00e..acbb213d7 100644 --- a/src/routes/components/Dropdown.tsx +++ b/src/routes/components/Dropdown.tsx @@ -41,12 +41,6 @@ const relatedComponents = [ { link: "/components/radio", name: "Radio" }, ]; type ComponentPropsType = GoabDropdownProps; -type CastingType = { - name: string; - value: string; - [key: string]: unknown; - onChange: (event: GoabDropdownOnChangeDetail) => void; -}; export default function DropdownPage() { const { version } = useContext(LanguageVersionContext); @@ -397,9 +391,9 @@ export default function DropdownPage() { }, ]; - function onSandboxChange(bindings: ComponentBinding[], props: Record) { + function onSandboxChange(bindings: ComponentBinding[], props: ComponentPropsType) { setDropdownBindings(bindings); - setDropdownProps(props as CastingType); + setDropdownProps(props); } // Demo @@ -407,7 +401,7 @@ export default function DropdownPage() { function onChange(event: GoabDropdownOnChangeDetail) { setColor(event.value || ""); - setDropdownProps({ ...dropdownProps, value: event.value || "" } as CastingType); + setDropdownProps({ ...dropdownProps, value: event.value || "" }); } return ( @@ -426,7 +420,7 @@ export default function DropdownPage() {

Playground

- properties={dropdownBindings} formItemProperties={formItemBindings} onChange={onSandboxChange} diff --git a/src/routes/components/FileUploader.tsx b/src/routes/components/FileUploader.tsx index 22f5ce6c2..e823ec64c 100644 --- a/src/routes/components/FileUploader.tsx +++ b/src/routes/components/FileUploader.tsx @@ -74,10 +74,6 @@ const relatedComponents = [ const FIGMA_LINK = "https://www.figma.com/design/3pb2IK8s2QUqWieH79KdN7/%E2%9D%96-Component-library-%7C-DDD?node-id=804-5767"; type ComponentPropsType = Omit; -type CastingType = { - maxFileSize: string; - [key: string]: unknown; -}; export default function FileUploaderPage() { const { version } = useContext(LanguageVersionContext); @@ -272,9 +268,10 @@ export default function FileUploaderPage() { }, ]; - function onSandboxChange(bindings: ComponentBinding[], props: Record) { + + function onSandboxChange(bindings: ComponentBinding[], props: ComponentPropsType) { setFileUploaderBindings(bindings); - setFileUploaderProps(props as CastingType); + setFileUploaderProps(props); } // For file uploader demo @@ -321,14 +318,9 @@ export default function FileUploaderPage() { -

- Playground -

- +

Playground

+ properties={fileUploaderBindings} onChange={onSandboxChange} fullWidth skipRender> + {/* ******* */} {/* Angular */} {/* ******* */} @@ -414,11 +406,7 @@ export default function FileUploaderPage() { allowCopy={true} code={` - + , "angular", version)}> @for (upload of uploads; track $index) { - + , "react", version)} /> {uploads.map(upload => ( - uploadFile(event.file)} ${propsToString( - fileUploaderProps, - "react", - version - )} /> + uploadFile(event.file)} ${propsToString(fileUploaderProps as Record, "react", version)} /> {uploads.map(upload => ( ({ content: "Chip text", @@ -108,9 +103,9 @@ export default function FilterChipPage() { MarginProperty, ]; - function onSandboxChange(bindings: ComponentBinding[], props: Record) { + function onSandboxChange(bindings: ComponentBinding[], props: ComponentPropsType) { setComponentBindings(bindings); - setComponentProps(props as CastingType); + setComponentProps(props); } return ( @@ -127,7 +122,7 @@ export default function FilterChipPage() {

Playground

- + properties={componentBindings} onChange={onSandboxChange}> ({ icon: "refresh" as GoabIconType, @@ -200,8 +195,8 @@ export default function IconButtonPage() { }, ]; - function onSandboxChange(bindings: ComponentBinding[], props: Record) { - setIconButtonProps(props as CastingType); + function onSandboxChange(bindings: ComponentBinding[], props: ComponentPropsType) { + setIconButtonProps(props); setIconButtonBindings(bindings); } return ( @@ -224,7 +219,7 @@ export default function IconButtonPage() {

Playground

- + properties={iconButtonBindings} onChange={onSandboxChange}> diff --git a/src/routes/components/MenuButton.tsx b/src/routes/components/MenuButton.tsx index 70218b81e..13950a418 100644 --- a/src/routes/components/MenuButton.tsx +++ b/src/routes/components/MenuButton.tsx @@ -21,7 +21,6 @@ import { ExamplesEmpty } from "@components/empty-states/examples-empty/ExamplesE import { AccessibilityEmpty } from "@components/empty-states/accessibility-empty/AccessibilityEmpty.tsx"; import { DesignEmpty } from "@components/empty-states/design-empty/DesignEmpty.tsx"; import { ComponentBinding, Sandbox } from "@components/sandbox"; -import { GoabButtonType } from "@abgov/ui-components-common"; import { CodeSnippet } from "@components/code-snippet/CodeSnippet"; const FIGMA_LINK = @@ -37,11 +36,6 @@ const relatedComponents = [ ]; type ComponentPropsType = GoabMenuButtonProps; -type CastingType = { - text: string, - type: GoabButtonType, - [key: string]: unknown; -}; export default function MenuButtonPage() { const { version, language } = useContext(LanguageVersionContext); @@ -114,9 +108,9 @@ export default function MenuButtonPage() { }, ]; - function onSandboxChange(bindings: ComponentBinding[], props: Record) { + function onSandboxChange(bindings: ComponentBinding[], props: ComponentPropsType) { setMenuButtonBindings(bindings); - setMenuButtonProps(props as CastingType); + setMenuButtonProps(props); } function handleAction(detail: GoabMenuButtonOnActionDetail) { @@ -143,7 +137,7 @@ export default function MenuButtonPage() {

Playground

- + properties={menuButtonBindings} onChange={onSandboxChange}> void; -}; export default function RadioPage() { const { version } = useContext(LanguageVersionContext); @@ -297,8 +290,8 @@ export default function RadioPage() { MarginProperty, ]; - function onSandboxChange(bindings: ComponentBinding[], props: Record) { - setRadioProps(props as CastingType); + function onSandboxChange(bindings: ComponentBinding[], props: ComponentPropsType) { + setRadioProps(props); setRadioBindings(bindings); } @@ -322,7 +315,7 @@ export default function RadioPage() {

Playground

- properties={radioBindings} formItemProperties={formItemBindings} onChange={onSandboxChange} diff --git a/src/routes/components/TextField.tsx b/src/routes/components/TextField.tsx index c4ff08682..986dee54c 100644 --- a/src/routes/components/TextField.tsx +++ b/src/routes/components/TextField.tsx @@ -45,12 +45,6 @@ const relatedComponents = [ type ComponentPropsType = Omit & { onChange?: (event: GoabInputOnChangeDetail) => void; }; -type CastingType = { - name: string; - value: string; - [key: string]: unknown; - onChange: (event: GoabInputOnChangeDetail) => void; -}; export default function TextFieldPage() { const { version } = useContext(LanguageVersionContext); @@ -628,9 +622,9 @@ export default function TextFieldPage() { MarginProperty, ]; - function onSandboxChange(bindings: ComponentBinding[], props: Record) { + function onSandboxChange(bindings: ComponentBinding[], props: ComponentPropsType) { setComponentBindings(bindings); - setComponentProps(props as CastingType); + setComponentProps(props); } // For sandbox demo function @@ -654,7 +648,7 @@ export default function TextFieldPage() {

Playground

- properties={componentBindings} formItemProperties={formItemBindings} onChange={onSandboxChange} diff --git a/src/routes/components/Tooltip.tsx b/src/routes/components/Tooltip.tsx index 1b1f35838..9ab4b49ba 100644 --- a/src/routes/components/Tooltip.tsx +++ b/src/routes/components/Tooltip.tsx @@ -37,10 +37,6 @@ const relatedComponents = [ { link: "/components/popover", name: "Popover" } ]; type ComponentPropsType = GoabTooltipProps; -type CastingType = { - content: string; - [key: string]: unknown; -}; export default function TooltipPage() { const { version } = useContext(LanguageVersionContext); @@ -161,9 +157,9 @@ export default function TooltipPage() { MarginProperty ]; - function onSandboxChange(bindings: ComponentBinding[], props: Record) { + function onSandboxChange(bindings: ComponentBinding[], props: ComponentPropsType) { setComponentBindings(bindings); - setComponentProps(props as CastingType); + setComponentProps(props); } return ( @@ -181,7 +177,7 @@ export default function TooltipPage() {

Playground

- + properties={componentBindings} onChange={onSandboxChange}> From c19550daa1bdabda4b380106979c0f5c565c9b3c Mon Sep 17 00:00:00 2001 From: Vanessa Tran Date: Mon, 19 Jan 2026 16:39:01 -0700 Subject: [PATCH 04/15] fix: add delay render for GoabDataGrid to fix keyboard navigation --- package-lock.json | 24 ++-- package.json | 6 +- .../basic-table-with-keyboard-navigation.tsx | 9 ++ .../data-grid/layout-mode-with-cards.tsx | 9 ++ .../sortable-table-with-row-selection.tsx | 10 +- src/routes/components/DataGrid.tsx | 124 ++++++++++-------- 6 files changed, 112 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index 20a04c064..881db2086 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,9 @@ "name": "code-sandbox", "version": "0.0.0", "dependencies": { - "@abgov/react-components": "6.10.0-dev.11", - "@abgov/ui-components-common": "1.10.0-dev.4", - "@abgov/web-components": "1.40.0-dev.21", + "@abgov/react-components": "6.10.0-next.1", + "@abgov/ui-components-common": "1.10.0-next.1", + "@abgov/web-components": "1.40.0-next.1", "@faker-js/faker": "^8.3.1", "highlight.js": "^11.8.0", "js-cookie": "^3.0.5", @@ -68,9 +68,9 @@ } }, "node_modules/@abgov/react-components": { - "version": "6.10.0-dev.11", - "resolved": "https://registry.npmjs.org/@abgov/react-components/-/react-components-6.10.0-dev.11.tgz", - "integrity": "sha512-m7kCQvJ2l5ScDoyOxWF4h9Jx2V65GVy2aWBZR5C6fnaoFRDQSre3ml7+DwnnlAd3jNuDW7u8uyh3BhL0lpqD8A==", + "version": "6.10.0-next.1", + "resolved": "https://registry.npmjs.org/@abgov/react-components/-/react-components-6.10.0-next.1.tgz", + "integrity": "sha512-82N8l6CQ/wI/CIyQQaYb4YvJ8pbvGAn4d9phnVAKcaJW7UBdsBjxYd97GDceGh4d+QKqxgLxREG2v+1Wslc1Qw==", "license": "Apache-2.0", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", @@ -79,15 +79,15 @@ } }, "node_modules/@abgov/ui-components-common": { - "version": "1.10.0-dev.4", - "resolved": "https://registry.npmjs.org/@abgov/ui-components-common/-/ui-components-common-1.10.0-dev.4.tgz", - "integrity": "sha512-z4xl26B9qbzY74tPj5H+6YJYpDBaz2J5k9pKb9uJKn2PJR61tgX0iDeLtaUf1I/qvLcnJV2Tk/6zX/h8qX948Q==", + "version": "1.10.0-next.1", + "resolved": "https://registry.npmjs.org/@abgov/ui-components-common/-/ui-components-common-1.10.0-next.1.tgz", + "integrity": "sha512-mrm9oTGwNmlBj2c6q+MATSu9DOhVpIIVK24YKi+9oFbR4XNZyc5YNGm2BIjN0h3+POVY0BeevAAr47A0K/ht3g==", "license": "Apache-2.0" }, "node_modules/@abgov/web-components": { - "version": "1.40.0-dev.21", - "resolved": "https://registry.npmjs.org/@abgov/web-components/-/web-components-1.40.0-dev.21.tgz", - "integrity": "sha512-bRsJqSW6ZLRoW9AtGQs/vQHNrSv9Rd5Kv1ZD5oQqeooqZQSJLVgpyeXMqPqwDnALfZiLhZ6QB/6erziVlGy7cA==", + "version": "1.40.0-next.1", + "resolved": "https://registry.npmjs.org/@abgov/web-components/-/web-components-1.40.0-next.1.tgz", + "integrity": "sha512-BF6N3K+XF9AKjq7CBKl91nMb+uTzn1Yz91fCD5jVmMXW8BIPr7LgPW/vllhB338bEcGnEbXx2m0G/4iVxqc79A==", "license": "Apache-2.0" }, "node_modules/@esbuild/aix-ppc64": { diff --git a/package.json b/package.json index 49dc4f30d..d9a76784e 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "prettier": "npx prettier . --write" }, "dependencies": { - "@abgov/react-components": "6.10.0-dev.11", - "@abgov/ui-components-common": "1.10.0-dev.4", - "@abgov/web-components": "1.40.0-dev.21", + "@abgov/react-components": "6.10.0-next.1", + "@abgov/ui-components-common": "1.10.0-next.1", + "@abgov/web-components": "1.40.0-next.1", "@faker-js/faker": "^8.3.1", "highlight.js": "^11.8.0", "js-cookie": "^3.0.5", diff --git a/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx b/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx index 1fe12608c..f4d551f4f 100644 --- a/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx +++ b/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from "react"; import { Sandbox } from "@components/sandbox"; import { CodeSnippet } from "@components/code-snippet/CodeSnippet.tsx"; import { @@ -9,6 +10,12 @@ import { } from "@abgov/react-components"; export function BasicTableWithKeyboardNavigation() { + const [isGridReady, setIsGridReady] = useState(false); + useEffect(() => { + const timer = setTimeout(() => setIsGridReady(true), 200); + return () => clearTimeout(timer); + }, []); + const users = [ { id: "1", name: "Alice Johnson", role: "Developer", status: "Active" }, { id: "2", name: "Bob Smith", role: "Designer", status: "Active" }, @@ -18,6 +25,7 @@ export function BasicTableWithKeyboardNavigation() { return ( <> + {isGridReady && ( @@ -49,6 +57,7 @@ export function BasicTableWithKeyboardNavigation() { + )} {/* Angular Code */} diff --git a/src/examples/data-grid/layout-mode-with-cards.tsx b/src/examples/data-grid/layout-mode-with-cards.tsx index b64bb23a3..a9740c180 100644 --- a/src/examples/data-grid/layout-mode-with-cards.tsx +++ b/src/examples/data-grid/layout-mode-with-cards.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from "react"; import { Sandbox } from "@components/sandbox"; import { CodeSnippet } from "@components/code-snippet/CodeSnippet.tsx"; import { @@ -23,6 +24,12 @@ type User = { }; export function LayoutModeWithCards() { + const [isGridReady, setIsGridReady] = useState(false); + useEffect(() => { + const timer = setTimeout(() => setIsGridReady(true), 200); + return () => clearTimeout(timer); + }, []); + const users: User[] = [ { id: "1", @@ -59,6 +66,7 @@ export function LayoutModeWithCards() { return ( <> + {isGridReady && ( {users.map(user => ( @@ -112,6 +120,7 @@ export function LayoutModeWithCards() { ))} + )} {/* Angular Code */} diff --git a/src/examples/data-grid/sortable-table-with-row-selection.tsx b/src/examples/data-grid/sortable-table-with-row-selection.tsx index b5009964b..47206a305 100644 --- a/src/examples/data-grid/sortable-table-with-row-selection.tsx +++ b/src/examples/data-grid/sortable-table-with-row-selection.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Sandbox } from "@components/sandbox"; import { CodeSnippet } from "@components/code-snippet/CodeSnippet.tsx"; import { @@ -22,6 +22,12 @@ type Application = { }; export function SortableTableWithRowSelection() { + const [isGridReady, setIsGridReady] = useState(false); + useEffect(() => { + const timer = setTimeout(() => setIsGridReady(true), 200); + return () => clearTimeout(timer); + }, []); + const initialApplications: Application[] = [ { id: "APP-001", applicant: "John Doe", dateSubmitted: "2024-01-15", status: "Approved", amount: "$5,000" }, { id: "APP-002", applicant: "Jane Smith", dateSubmitted: "2024-01-18", status: "Pending", amount: "$3,500" }, @@ -78,6 +84,7 @@ export function SortableTableWithRowSelection() { return ( <> + {isGridReady && ( @@ -134,6 +141,7 @@ export function SortableTableWithRowSelection() { + )} {/* Angular Code */} diff --git a/src/routes/components/DataGrid.tsx b/src/routes/components/DataGrid.tsx index 1e9753c78..212ba7588 100644 --- a/src/routes/components/DataGrid.tsx +++ b/src/routes/components/DataGrid.tsx @@ -1,4 +1,4 @@ -import { useState, useContext } from "react"; +import { useState, useContext, useEffect } from "react"; import { GoabBadge, GoabCheckbox, @@ -54,6 +54,13 @@ export default function DataGridPage() { keyboardNav: "table", }); + const [isGridReady, setIsGridReady] = useState(false); + useEffect(() => { + // add small delay for all nested doms fully render + const timer = setTimeout(() => setIsGridReady(true), 200); + return () => clearTimeout(timer); + }, []); + const initialUsers: User[] = [ { id: "1", name: "Alice Johnson", status: "Active", email: "alice@example.com" }, { id: "2", name: "Bob Smith", status: "Pending", email: "bob@example.com" }, @@ -188,65 +195,73 @@ export default function DataGridPage() { {/* Don't render inside Sandbox because the table with interactive elements (arrow right left cannot focus on action button directly inside sandbox */} - - - - - - - selectAll(e.checked)} - /> - - - Name - - - Status - - Email - Actions - - - - {users.map(user => ( - - + {isGridReady && ( + + + + + + toggleSelection(user.id)} + name="selectAll" + mt="2xs" + checked={isSelectedAll} + onChange={e => selectAll(e.checked)} /> - - {user.name} - - - - {user.email} - - handleMenuAction(user.id, e.action)}> - - - - + + + Name + + + Status + + Email + Actions - ))} - - - - + + + {users.map(user => ( + + + toggleSelection(user.id)} + /> + + {user.name} + + + + {user.email} + + handleMenuAction(user.id, e.action)} + > + + + + + + ))} + + + + + )} + skipRenderDom + skipRender + fullWidth + > - }> + } + >
From 13568acbc77668e08ea5b468b15902f1023fa41d Mon Sep 17 00:00:00 2001 From: Vanessa Tran Date: Mon, 19 Jan 2026 17:40:57 -0700 Subject: [PATCH 05/15] chore: allow to switch table/card layout under sandbox --- src/routes/components/DataGrid.tsx | 716 +++++++++++++++++++---------- 1 file changed, 484 insertions(+), 232 deletions(-) diff --git a/src/routes/components/DataGrid.tsx b/src/routes/components/DataGrid.tsx index 212ba7588..8c63336c3 100644 --- a/src/routes/components/DataGrid.tsx +++ b/src/routes/components/DataGrid.tsx @@ -1,6 +1,7 @@ import { useState, useContext, useEffect } from "react"; import { GoabBadge, + GoabBlock, GoabCheckbox, GoabContainer, GoabDataGrid, @@ -82,12 +83,14 @@ export default function DataGridPage() { const [selectedUsers, setSelectedUsers] = useState([]); const [isSelectedAll, setIsSelectedAll] = useState(false); + const [layoutView, setLayoutView] = useState<"table" | "card">("table"); + const [componentBindings, setComponentBindings] = useState([ { - label: "Keyboard Navigation", + label: "Layout View", type: "dropdown", - name: "keyboardNav", - options: ["table", "layout"], + name: "layoutView", + options: ["table", "card"], value: "table", }, { @@ -147,7 +150,11 @@ export default function DataGridPage() { function onSandboxChange(bindings: ComponentBinding[], props: Record) { setComponentBindings(bindings); - setDataGridProps({ keyboardNav: "table", ...props } as ComponentPropsType); + const newLayoutView = (props.layoutView as "table" | "card") || "table"; + setLayoutView(newLayoutView); + const keyboardNav = newLayoutView === "card" ? "layout" : "table"; + const { layoutView: _, ...restProps } = props; + setDataGridProps({ keyboardNav, ...restProps } as ComponentPropsType); } const componentProperties: ComponentProperty[] = [ @@ -195,7 +202,7 @@ export default function DataGridPage() { {/* Don't render inside Sandbox because the table with interactive elements (arrow right left cannot focus on action button directly inside sandbox */} - {isGridReady && ( + {isGridReady && layoutView === "table" && ( @@ -255,12 +262,57 @@ export default function DataGridPage() { )} + {/* Card view */} + {isGridReady && layoutView === "card" && ( + + + {users.map(user => ( + +
+ toggleSelection(user.id)} + /> +
+ + {user.name} + + + + {user.email} + +
+ handleMenuAction(user.id, e.action)} + > + + + +
+
+ ))} +
+
+ )} + - {/* Angular */} - = { - "Active": "success", - "Pending": "important" - }; - return types[status] || "information"; - } - - isSelected(userId: string): boolean { - return this.selectedUsers.includes(userId); - } - - handleSort(event: GoabTableOnSortDetail) { - const { sortBy, sortDir } = event; - this.users.sort((a: any, b: any) => (a[sortBy] > b[sortBy] ? 1 : -1) * sortDir); - } + {/* Angular - Table View */} + {layoutView === "table" && ( + = { + "Active": "success", + "Pending": "important" + }; + return types[status] || "information"; + } - selectAll(event: GoabCheckboxOnChangeDetail) { - this.isSelectedAll = event.checked; - this.selectedUsers = event.checked ? this.users.map(u => u.id) : []; - } + isSelected(userId: string): boolean { + return this.selectedUsers.includes(userId); + } - toggleSelection(userId: string, event: GoabCheckboxOnChangeDetail) { - if (event.checked) { - this.selectedUsers.push(userId); - } else { - this.selectedUsers = this.selectedUsers.filter(id => id !== userId); + handleSort(event: GoabTableOnSortDetail) { + const { sortBy, sortDir } = event; + this.users.sort((a: any, b: any) => (a[sortBy] > b[sortBy] ? 1 : -1) * sortDir); } - } - handleMenuAction(userId: string, event: GoabMenuButtonOnActionDetail) { - if (event.action === "view") { - console.log("View user:", userId); - } else if (event.action === "delete") { - this.users = this.users.filter(u => u.id !== userId); + selectAll(event: GoabCheckboxOnChangeDetail) { + this.isSelectedAll = event.checked; + this.selectedUsers = event.checked ? this.users.map(u => u.id) : []; } - } - }`} - /> - - - - - - - - - - Name - - - Status - - Email - Actions - - - - @for (user of users; track user.id) { + toggleSelection(userId: string, event: GoabCheckboxOnChangeDetail) { + if (event.checked) { + this.selectedUsers.push(userId); + } else { + this.selectedUsers = this.selectedUsers.filter(id => id !== userId); + } + } + + handleMenuAction(userId: string, event: GoabMenuButtonOnActionDetail) { + if (event.action === "view") { + console.log("View user:", userId); + } else if (event.action === "delete") { + this.users = this.users.filter(u => u.id !== userId); + } + } + }`} + /> + )} + {layoutView === "table" && ( + + + - + + name="selectAll" + mt="2xs" + [checked]="isSelectedAll" + (onChange)="selectAll($event)"> - - {{ user.name }} - - - - {{ user.email }} - - - - - - + + + Name + + + Status + + Email + Actions + + + @for (user of users; track user.id) { + + + + + + {{ user.name }} + + + + {{ user.email }} + + + + + + + + } + + + `} + /> + )} + + {/* Angular - Card View */} + {layoutView === "card" && ( + = { + "Active": "success", + "Pending": "important" + }; + return types[status] || "information"; + } + + isSelected(userId: string): boolean { + return this.selectedUsers.includes(userId); + } + + toggleSelection(userId: string) { + if (this.selectedUsers.includes(userId)) { + this.selectedUsers = this.selectedUsers.filter(id => id !== userId); + } else { + this.selectedUsers = [...this.selectedUsers, userId]; } - - - `} - /> + } - {/* React */} - ([ - { id: "1", name: "Alice Johnson", status: "Active", email: "alice@example.com" }, - { id: "2", name: "Bob Smith", status: "Pending", email: "bob@example.com" }, - ]); - const [selectedUsers, setSelectedUsers] = useState([]); - const [isSelectedAll, setIsSelectedAll] = useState(false); - - const getStatusBadgeType = (status: string): "success" | "important" | "information" => { - switch (status) { - case "Active": - return "success"; - case "Pending": - return "important"; - default: - return "information"; - } - }; - - const isSelected = (userId: string): boolean => { - return selectedUsers.includes(userId); - }; - - const handleSort = (event: GoabTableOnSortDetail) => { - const { sortBy, sortDir } = event; - const sortedUsers = [...users].sort( - (a: any, b: any) => (a[sortBy] > b[sortBy] ? 1 : -1) * sortDir - ); - setUsers(sortedUsers); - }; - - const selectAll = (event: GoabCheckboxOnChangeDetail) => { - setIsSelectedAll(event.checked); - setSelectedUsers(event.checked ? users.map(u => u.id) : []); - }; - - const toggleSelection = (userId: string, event: GoabCheckboxOnChangeDetail) => { - if (event.checked) { - setSelectedUsers([...selectedUsers, userId]); - } else { - setSelectedUsers(selectedUsers.filter(id => id !== userId)); - } - }; - - const handleMenuAction = (userId: string, action: string) => { - if (action === "view") { - console.log("View user:", userId); - } else if (action === "delete") { - setUsers(users.filter(u => u.id !== userId)); - } - };`} - /> + handleMenuAction(userId: string, event: GoabMenuButtonOnActionDetail) { + if (event.action === "view") { + console.log("View user:", userId); + } else if (event.action === "delete") { + this.users = this.users.filter(u => u.id !== userId); + } + } + }`} + /> + )} + {layoutView === "card" && ( + + @for (user of users; track user.id) { + +
+ + +
+ + {{ user.name }} + + + + + {{ user.email }} + +
+ + + + +
+
+ } + `} + /> + )} + + {/* React - Table View */} + {layoutView === "table" && ( + ([ + { id: "1", name: "Alice Johnson", status: "Active", email: "alice@example.com" }, + { id: "2", name: "Bob Smith", status: "Pending", email: "bob@example.com" }, + ]); + const [selectedUsers, setSelectedUsers] = useState([]); + const [isSelectedAll, setIsSelectedAll] = useState(false); + + const getStatusBadgeType = (status: string): "success" | "important" | "information" => { + switch (status) { + case "Active": + return "success"; + case "Pending": + return "important"; + default: + return "information"; + } + }; - - - - - - - - - Name - - - Status - - Email - Actions - - - - {users.map((user) => ( - - + const isSelected = (userId: string): boolean => { + return selectedUsers.includes(userId); + }; + + const handleSort = (event: GoabTableOnSortDetail) => { + const { sortBy, sortDir } = event; + const sortedUsers = [...users].sort( + (a: any, b: any) => (a[sortBy] > b[sortBy] ? 1 : -1) * sortDir + ); + setUsers(sortedUsers); + }; + + const selectAll = (event: GoabCheckboxOnChangeDetail) => { + setIsSelectedAll(event.checked); + setSelectedUsers(event.checked ? users.map(u => u.id) : []); + }; + + const toggleSelection = (userId: string, event: GoabCheckboxOnChangeDetail) => { + if (event.checked) { + setSelectedUsers([...selectedUsers, userId]); + } else { + setSelectedUsers(selectedUsers.filter(id => id !== userId)); + } + }; + + const handleMenuAction = (userId: string, action: string) => { + if (action === "view") { + console.log("View user:", userId); + } else if (action === "delete") { + setUsers(users.filter(u => u.id !== userId)); + } + };`} + /> + )} + {layoutView === "table" && ( + + + + + toggleSelection(user.id, e)} + name="selectAll" + mt="2xs" + checked={isSelectedAll} + onChange={selectAll} /> - - {user.name} - - - - {user.email} - - handleMenuAction(user.id, e.action)} - > - - - - + + + Name + + + Status + + Email + Actions - ))} - - - `} - /> + + + {users.map((user) => ( + + + toggleSelection(user.id, e)} + /> + + {user.name} + + + + {user.email} + + handleMenuAction(user.id, e.action)} + > + + + + + + ))} + + + `} + /> + )} + + {/* React - Card View */} + {layoutView === "card" && ( + ([ + { id: "1", name: "Alice Johnson", status: "Active", email: "alice@example.com" }, + { id: "2", name: "Bob Smith", status: "Pending", email: "bob@example.com" }, + ]); + const [selectedUsers, setSelectedUsers] = useState([]); + + const getStatusBadgeType = (status: string): "success" | "important" | "information" => { + switch (status) { + case "Active": + return "success"; + case "Pending": + return "important"; + default: + return "information"; + } + }; + + const isSelected = (userId: string): boolean => { + return selectedUsers.includes(userId); + }; + + const toggleSelection = (userId: string) => { + if (selectedUsers.includes(userId)) { + setSelectedUsers(selectedUsers.filter(id => id !== userId)); + } else { + setSelectedUsers([...selectedUsers, userId]); + } + }; + + const handleMenuAction = (userId: string, action: string) => { + if (action === "view") { + console.log("View user:", userId); + } else if (action === "delete") { + setUsers(users.filter(u => u.id !== userId)); + } + };`} + /> + )} + {layoutView === "card" && ( + + {users.map((user) => ( + +
+ toggleSelection(user.id)} + /> +
+ + {user.name} + + + + {user.email} + +
+ handleMenuAction(user.id, e.action)} + > + + + +
+
+ ))} + `} + /> + )}
From c69f65d4b6b144933113427094f2ecc61ee20c1a Mon Sep 17 00:00:00 2001 From: Vanessa Tran Date: Mon, 19 Jan 2026 18:00:28 -0700 Subject: [PATCH 06/15] chore: add indeterminate checkbox to sandbox and example --- .../sortable-table-with-row-selection.tsx | 23 ++++++++++++++----- src/routes/components/DataGrid.tsx | 23 ++++++++++++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/examples/data-grid/sortable-table-with-row-selection.tsx b/src/examples/data-grid/sortable-table-with-row-selection.tsx index 47206a305..07492e5c6 100644 --- a/src/examples/data-grid/sortable-table-with-row-selection.tsx +++ b/src/examples/data-grid/sortable-table-with-row-selection.tsx @@ -53,7 +53,9 @@ export function SortableTableWithRowSelection() { const [applications, setApplications] = useState(initialApplications); const [selectedIds, setSelectedIds] = useState([]); - const [isSelectedAll, setIsSelectedAll] = useState(false); + + const isSelectedAll = selectedIds.length === applications.length && applications.length > 0; + const isIndeterminate = selectedIds.length > 0 && selectedIds.length < applications.length; const isSelected = (id: string): boolean => selectedIds.includes(id); @@ -66,7 +68,6 @@ export function SortableTableWithRowSelection() { }; const selectAll = (checked: boolean) => { - setIsSelectedAll(checked); if (checked) { setSelectedIds(applications.map((app) => app.id)); } else { @@ -95,6 +96,7 @@ export function SortableTableWithRowSelection() { name="selectAll" mt="2xs" checked={isSelectedAll} + indeterminate={isIndeterminate} onChange={(e) => selectAll(e.checked)} /> @@ -158,7 +160,14 @@ export function SortableTableWithRowSelection() { ]; selectedIds: string[] = []; - isSelectedAll = false; + + get isSelectedAll(): boolean { + return this.selectedIds.length === this.applications.length && this.applications.length > 0; + } + + get isIndeterminate(): boolean { + return this.selectedIds.length > 0 && this.selectedIds.length < this.applications.length; + } isSelected(id: string): boolean { return this.selectedIds.includes(id); @@ -173,7 +182,6 @@ export function SortableTableWithRowSelection() { } selectAll(checked: boolean) { - this.isSelectedAll = checked; this.selectedIds = checked ? this.applications.map(app => app.id) : []; } @@ -209,6 +217,7 @@ export function SortableTableWithRowSelection() { @@ -267,7 +276,9 @@ export function SortableTableWithRowSelection() { ]); const [selectedIds, setSelectedIds] = useState([]); - const [isSelectedAll, setIsSelectedAll] = useState(false); + + const isSelectedAll = selectedIds.length === applications.length && applications.length > 0; + const isIndeterminate = selectedIds.length > 0 && selectedIds.length < applications.length; const isSelected = (id: string): boolean => selectedIds.includes(id); @@ -280,7 +291,6 @@ export function SortableTableWithRowSelection() { }; const selectAll = (checked: boolean) => { - setIsSelectedAll(checked); setSelectedIds(checked ? applications.map((app) => app.id) : []); }; @@ -316,6 +326,7 @@ export function SortableTableWithRowSelection() { selectAll(e.checked)} /> diff --git a/src/routes/components/DataGrid.tsx b/src/routes/components/DataGrid.tsx index 8c63336c3..a00ebde0c 100644 --- a/src/routes/components/DataGrid.tsx +++ b/src/routes/components/DataGrid.tsx @@ -81,7 +81,9 @@ export default function DataGridPage() { const [users, setUsers] = useState(initialUsers); const [selectedUsers, setSelectedUsers] = useState([]); - const [isSelectedAll, setIsSelectedAll] = useState(false); + + const isSelectedAll = selectedUsers.length === users.length && users.length > 0; + const isIndeterminate = selectedUsers.length > 0 && selectedUsers.length < users.length; const [layoutView, setLayoutView] = useState<"table" | "card">("table"); @@ -124,7 +126,6 @@ export default function DataGridPage() { }; const selectAll = (checked: boolean) => { - setIsSelectedAll(checked); if (checked) { setSelectedUsers(users.map(u => u.id)); } else { @@ -213,6 +214,7 @@ export default function DataGridPage() { name="selectAll" mt="2xs" checked={isSelectedAll} + indeterminate={isIndeterminate} onChange={e => selectAll(e.checked)} /> @@ -340,7 +342,14 @@ export default function DataGridPage() { { id: "2", name: "Bob Smith", status: "Pending", email: "bob@example.com" }, ]; selectedUsers: string[] = []; - isSelectedAll = false; + + get isSelectedAll(): boolean { + return this.selectedUsers.length === this.users.length && this.users.length > 0; + } + + get isIndeterminate(): boolean { + return this.selectedUsers.length > 0 && this.selectedUsers.length < this.users.length; + } getStatusBadgeType(status: string): GoabBadgeType { const types: Record = { @@ -360,7 +369,6 @@ export default function DataGridPage() { } selectAll(event: GoabCheckboxOnChangeDetail) { - this.isSelectedAll = event.checked; this.selectedUsers = event.checked ? this.users.map(u => u.id) : []; } @@ -405,6 +413,7 @@ export default function DataGridPage() { name="selectAll" mt="2xs" [checked]="isSelectedAll" + [indeterminate]="isIndeterminate" (onChange)="selectAll($event)"> @@ -555,7 +564,9 @@ export default function DataGridPage() { { id: "2", name: "Bob Smith", status: "Pending", email: "bob@example.com" }, ]); const [selectedUsers, setSelectedUsers] = useState([]); - const [isSelectedAll, setIsSelectedAll] = useState(false); + + const isSelectedAll = selectedUsers.length === users.length && users.length > 0; + const isIndeterminate = selectedUsers.length > 0 && selectedUsers.length < users.length; const getStatusBadgeType = (status: string): "success" | "important" | "information" => { switch (status) { @@ -581,7 +592,6 @@ export default function DataGridPage() { }; const selectAll = (event: GoabCheckboxOnChangeDetail) => { - setIsSelectedAll(event.checked); setSelectedUsers(event.checked ? users.map(u => u.id) : []); }; @@ -625,6 +635,7 @@ export default function DataGridPage() { name="selectAll" mt="2xs" checked={isSelectedAll} + indeterminate={isIndeterminate} onChange={selectAll} /> From 37e4ed23e8006186be6dc9b58382a5c1d8f3c8c8 Mon Sep 17 00:00:00 2001 From: Vanessa Tran Date: Tue, 20 Jan 2026 17:26:02 -0700 Subject: [PATCH 07/15] fix: tab double focus when switching tabs on data-grid --- src/examples/data-grid/DataGridExamples.tsx | 14 +++++-- .../basic-table-with-keyboard-navigation.tsx | 17 ++++----- .../data-grid/layout-mode-with-cards.tsx | 14 +++---- .../sortable-table-with-row-selection.tsx | 15 ++++---- src/routes/components/DataGrid.tsx | 37 +++++++++++++++---- 5 files changed, 62 insertions(+), 35 deletions(-) diff --git a/src/examples/data-grid/DataGridExamples.tsx b/src/examples/data-grid/DataGridExamples.tsx index 763d95cce..262b0e7f3 100644 --- a/src/examples/data-grid/DataGridExamples.tsx +++ b/src/examples/data-grid/DataGridExamples.tsx @@ -3,17 +3,23 @@ import { BasicTableWithKeyboardNavigation } from "./basic-table-with-keyboard-na import { SortableTableWithRowSelection } from "./sortable-table-with-row-selection.tsx"; import { LayoutModeWithCards } from "./layout-mode-with-cards.tsx"; -export function DataGridExamples() { +interface DataGridExamplesProps { + isGridReady?: boolean; +} + +export function DataGridExamples({ + isGridReady = true, +}: DataGridExamplesProps) { return ( <> - + - + - + ); } diff --git a/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx b/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx index f4d551f4f..e7504ef7d 100644 --- a/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx +++ b/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx @@ -1,20 +1,19 @@ -import { useState, useEffect } from "react"; import { Sandbox } from "@components/sandbox"; import { CodeSnippet } from "@components/code-snippet/CodeSnippet.tsx"; import { GoabBadge, GoabButton, - GoabContainer, GoabDataGrid, GoabTable, } from "@abgov/react-components"; -export function BasicTableWithKeyboardNavigation() { - const [isGridReady, setIsGridReady] = useState(false); - useEffect(() => { - const timer = setTimeout(() => setIsGridReady(true), 200); - return () => clearTimeout(timer); - }, []); +interface BasicTableWithKeyboardNavigationProps { + isGridReady?: boolean; +} + +export function BasicTableWithKeyboardNavigation({ + isGridReady = true, +}: BasicTableWithKeyboardNavigationProps) { const users = [ { id: "1", name: "Alice Johnson", role: "Developer", status: "Active" }, @@ -26,7 +25,6 @@ export function BasicTableWithKeyboardNavigation() { return ( <> {isGridReady && ( - @@ -56,7 +54,6 @@ export function BasicTableWithKeyboardNavigation() { - )} diff --git a/src/examples/data-grid/layout-mode-with-cards.tsx b/src/examples/data-grid/layout-mode-with-cards.tsx index a9740c180..019e91137 100644 --- a/src/examples/data-grid/layout-mode-with-cards.tsx +++ b/src/examples/data-grid/layout-mode-with-cards.tsx @@ -1,4 +1,3 @@ -import { useState, useEffect } from "react"; import { Sandbox } from "@components/sandbox"; import { CodeSnippet } from "@components/code-snippet/CodeSnippet.tsx"; import { @@ -23,12 +22,13 @@ type User = { serviceAccess: string; }; -export function LayoutModeWithCards() { - const [isGridReady, setIsGridReady] = useState(false); - useEffect(() => { - const timer = setTimeout(() => setIsGridReady(true), 200); - return () => clearTimeout(timer); - }, []); +interface LayoutModeWithCardsProps { + isGridReady?: boolean; +} + +export function LayoutModeWithCards({ + isGridReady = true, +}: LayoutModeWithCardsProps) { const users: User[] = [ { diff --git a/src/examples/data-grid/sortable-table-with-row-selection.tsx b/src/examples/data-grid/sortable-table-with-row-selection.tsx index 07492e5c6..1002785b0 100644 --- a/src/examples/data-grid/sortable-table-with-row-selection.tsx +++ b/src/examples/data-grid/sortable-table-with-row-selection.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState } from "react"; import { Sandbox } from "@components/sandbox"; import { CodeSnippet } from "@components/code-snippet/CodeSnippet.tsx"; import { @@ -21,12 +21,13 @@ type Application = { amount: string; }; -export function SortableTableWithRowSelection() { - const [isGridReady, setIsGridReady] = useState(false); - useEffect(() => { - const timer = setTimeout(() => setIsGridReady(true), 200); - return () => clearTimeout(timer); - }, []); +interface SortableTableWithRowSelectionProps { + isGridReady?: boolean; +} + +export function SortableTableWithRowSelection({ + isGridReady = true, +}: SortableTableWithRowSelectionProps) { const initialApplications: Application[] = [ { id: "APP-001", applicant: "John Doe", dateSubmitted: "2024-01-15", status: "Approved", amount: "$5,000" }, diff --git a/src/routes/components/DataGrid.tsx b/src/routes/components/DataGrid.tsx index a00ebde0c..95031ec5d 100644 --- a/src/routes/components/DataGrid.tsx +++ b/src/routes/components/DataGrid.tsx @@ -1,4 +1,4 @@ -import { useState, useContext, useEffect } from "react"; +import { useState, useContext, useEffect, useRef } from "react"; import { GoabBadge, GoabBlock, @@ -54,14 +54,37 @@ export default function DataGridPage() { const [dataGridProps, setDataGridProps] = useState({ keyboardNav: "table", }); - + + // We render GoabDataGrid inside GoabTabs, so when switching tabs the grid needs time to mount. + // This setTimeout defers initialization to the next event loop tick, giving React enough time + // to complete the mount/unmount cycle before the DataGrid initializes its keyboard navigation. const [isGridReady, setIsGridReady] = useState(false); + const gridTimerRef = useRef | null>(null); + + const scheduleGridReady = () => { + if (gridTimerRef.current) clearTimeout(gridTimerRef.current); + gridTimerRef.current = setTimeout(() => setIsGridReady(true), 0); + }; + useEffect(() => { - // add small delay for all nested doms fully render - const timer = setTimeout(() => setIsGridReady(true), 200); - return () => clearTimeout(timer); + scheduleGridReady(); + return () => { + if (gridTimerRef.current) clearTimeout(gridTimerRef.current); + }; }, []); + const resetGridReady = () => { + setIsGridReady(false); + scheduleGridReady(); + }; + + const handleTabChange = (event: { tab: number }) => { + // Only reset for tabs with DataGrids (tab 1 = Code playground, tab 2 = Examples) + if (event.tab === 1 || event.tab === 2) { + resetGridReady(); + } + }; + const initialUsers: User[] = [ { id: "1", name: "Alice Johnson", status: "Active", email: "alice@example.com" }, { id: "2", name: "Bob Smith", status: "Pending", email: "bob@example.com" }, @@ -196,7 +219,7 @@ export default function DataGridPage() { {version === "new" && ( - +

Component @@ -907,7 +930,7 @@ export default function DataGridPage() { } > - + From e44d968bfbf4c6f325cd38c0281b67ee66fcda6f Mon Sep 17 00:00:00 2001 From: Vanessa Tran Date: Tue, 20 Jan 2026 17:33:58 -0700 Subject: [PATCH 08/15] chore: move icon position to right --- .../data-grid/basic-table-with-keyboard-navigation.tsx | 6 +++--- src/examples/data-grid/layout-mode-with-cards.tsx | 6 +++--- .../data-grid/sortable-table-with-row-selection.tsx | 6 +++--- src/routes/components/DataGrid.tsx | 3 ++- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx b/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx index e7504ef7d..5e62cd60b 100644 --- a/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx +++ b/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx @@ -25,7 +25,7 @@ export function BasicTableWithKeyboardNavigation({ return ( <> {isGridReady && ( - + @@ -63,7 +63,7 @@ export function BasicTableWithKeyboardNavigation({ tags="angular" allowCopy={true} code={` - + @@ -127,7 +127,7 @@ export function BasicTableWithKeyboardNavigation({ tags="react" allowCopy={true} code={` - + diff --git a/src/examples/data-grid/layout-mode-with-cards.tsx b/src/examples/data-grid/layout-mode-with-cards.tsx index 019e91137..09267a9e4 100644 --- a/src/examples/data-grid/layout-mode-with-cards.tsx +++ b/src/examples/data-grid/layout-mode-with-cards.tsx @@ -68,7 +68,7 @@ export function LayoutModeWithCards({ <> {isGridReady && ( - + {users.map(user => ( @@ -179,7 +179,7 @@ export function LayoutModeWithCards({ tags="angular" allowCopy={true} code={` - + @for (user of users; track user.id) { @@ -298,7 +298,7 @@ export function LayoutModeWithCards({ tags="react" allowCopy={true} code={` - + {users.map((user) => ( diff --git a/src/examples/data-grid/sortable-table-with-row-selection.tsx b/src/examples/data-grid/sortable-table-with-row-selection.tsx index 1002785b0..6fb601ae0 100644 --- a/src/examples/data-grid/sortable-table-with-row-selection.tsx +++ b/src/examples/data-grid/sortable-table-with-row-selection.tsx @@ -88,7 +88,7 @@ export function SortableTableWithRowSelection({ <> {isGridReady && ( - + @@ -210,7 +210,7 @@ export function SortableTableWithRowSelection({ tags="angular" allowCopy={true} code={` - + @@ -319,7 +319,7 @@ export function SortableTableWithRowSelection({ tags="react" allowCopy={true} code={` - + diff --git a/src/routes/components/DataGrid.tsx b/src/routes/components/DataGrid.tsx index 95031ec5d..4a65194c2 100644 --- a/src/routes/components/DataGrid.tsx +++ b/src/routes/components/DataGrid.tsx @@ -53,6 +53,7 @@ export default function DataGridPage() { const { version, language } = useContext(LanguageVersionContext); const [dataGridProps, setDataGridProps] = useState({ keyboardNav: "table", + keyboardIconPosition: "right", }); // We render GoabDataGrid inside GoabTabs, so when switching tabs the grid needs time to mount. @@ -123,7 +124,7 @@ export default function DataGridPage() { type: "dropdown", name: "keyboardIconPosition", options: ["", "left", "right"], - value: "", + value: "right", defaultValue: "left", }, { From d8260993a75f64d6b9c46b6193f6915a04773764 Mon Sep 17 00:00:00 2001 From: Vanessa Tran Date: Tue, 20 Jan 2026 18:05:51 -0700 Subject: [PATCH 09/15] fix: sticky menu overlap vs 2 examples --- .../data-grid/layout-mode-with-cards.tsx | 56 +++++++++---------- .../sortable-table-with-row-selection.tsx | 8 ++- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/examples/data-grid/layout-mode-with-cards.tsx b/src/examples/data-grid/layout-mode-with-cards.tsx index 09267a9e4..538038547 100644 --- a/src/examples/data-grid/layout-mode-with-cards.tsx +++ b/src/examples/data-grid/layout-mode-with-cards.tsx @@ -67,14 +67,14 @@ export function LayoutModeWithCards({ return ( <> {isGridReady && ( - + {users.map(user => ( - - + +
- +
{user.name} - +
Updated {user.updated} @@ -97,9 +97,9 @@ export function LayoutModeWithCards({ Program {user.program} - +
- +
Program ID {user.programId} @@ -108,14 +108,14 @@ export function LayoutModeWithCards({ Service access {user.serviceAccess} - - +
+
-
+
))}
@@ -181,11 +181,11 @@ export function LayoutModeWithCards({ code={` @for (user of users; track user.id) { - - + +
- +
{{ user.name }} @@ -196,7 +196,7 @@ export function LayoutModeWithCards({ - +
Updated {{ user.updated }} @@ -209,9 +209,9 @@ export function LayoutModeWithCards({ Program {{ user.program }} - +
- +
Program ID {{ user.programId }} @@ -220,14 +220,14 @@ export function LayoutModeWithCards({ Service access {{ user.serviceAccess }} - - +
+
-
+
}
`} @@ -300,11 +300,11 @@ export function LayoutModeWithCards({ code={` {users.map((user) => ( - - + +
- +
{user.name} - +
Updated {user.updated} @@ -327,9 +327,9 @@ export function LayoutModeWithCards({ Program {user.program} - +
- +
Program ID {user.programId} @@ -338,14 +338,14 @@ export function LayoutModeWithCards({ Service access {user.serviceAccess} - - +
+
handleMenuAction(user.id, e)}> -
+
))}
`} diff --git a/src/examples/data-grid/sortable-table-with-row-selection.tsx b/src/examples/data-grid/sortable-table-with-row-selection.tsx index 6fb601ae0..a87c66d28 100644 --- a/src/examples/data-grid/sortable-table-with-row-selection.tsx +++ b/src/examples/data-grid/sortable-table-with-row-selection.tsx @@ -87,9 +87,10 @@ export function SortableTableWithRowSelection({ return ( <> {isGridReady && ( - + - +
+ @@ -141,7 +142,8 @@ export function SortableTableWithRowSelection({ ))} - + +
)} From 0e9fdfc4a16e2e49f3311eaef6e9c4559c4ca3f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:46:04 +0000 Subject: [PATCH 10/15] chore(deps-dev): bump lodash from 4.17.21 to 4.17.23 Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23) --- updated-dependencies: - dependency-name: lodash dependency-version: 4.17.23 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package-lock.json | 20 ++++++-------------- package.json | 2 +- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index e72f7a76b..c4322b0f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "eslint": "^8.43.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.3.5", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "prettier": "2.4.1", "typescript": "^5.4.2", "vite": "^5.4.21" @@ -700,7 +700,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", - "peer": true, "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.0.0", @@ -1419,7 +1418,6 @@ "version": "18.2.67", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.67.tgz", "integrity": "sha512-vkIE2vTIMHQ/xL0rgmuoECBCkZFZeHr49HeWSc24AptMbNRo7pwSBvj73rlJJs9fGKj0koS+V7kQB1jHS0uCgw==", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1506,7 +1504,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -1679,7 +1676,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1959,7 +1955,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2557,10 +2552,11 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -2878,7 +2874,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2890,7 +2885,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -3221,7 +3215,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3266,7 +3259,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/package.json b/package.json index 0370f34f3..e17a951fc 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "prettier": "2.4.1", "typescript": "^5.4.2", "vite": "^5.4.21", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "@types/lodash": "^4.17.16" } } From 7bf7d904b9cd368ca94300a3d6c87f4d33b9f573 Mon Sep 17 00:00:00 2001 From: Vanessa Tran Date: Fri, 23 Jan 2026 11:58:58 -0700 Subject: [PATCH 11/15] chore: update GoabIconType --- src/routes/components/Badge.tsx | 4 +- src/routes/components/Button.tsx | 6 +-- src/routes/components/Dropdown.tsx | 4 +- src/routes/components/IconButton.tsx | 4 +- src/routes/components/Icons.tsx | 48 +++++++------------ src/routes/components/Link.tsx | 6 +-- src/routes/components/MenuButton.tsx | 17 ++++++- src/routes/components/TextField.tsx | 6 +-- .../icons.json => utils/iconUtils.ts} | 23 +++++++-- tsconfig.json | 3 ++ vite.config.ts | 1 + 11 files changed, 70 insertions(+), 52 deletions(-) rename src/{routes/components/icons.json => utils/iconUtils.ts} (93%) diff --git a/src/routes/components/Badge.tsx b/src/routes/components/Badge.tsx index 611c974a8..0fc5dcc4c 100644 --- a/src/routes/components/Badge.tsx +++ b/src/routes/components/Badge.tsx @@ -10,7 +10,7 @@ import { ComponentContent } from "@components/component-content/ComponentContent import BadgeExamples from "@examples/badge/BadgeExamples.tsx"; import { DesignEmpty } from "@components/empty-states/design-empty/DesignEmpty.tsx"; import { AccessibilityEmpty } from "@components/empty-states/accessibility-empty/AccessibilityEmpty.tsx"; -import ICONS from "@routes/components/icons.json"; +import { getIconOptions } from "@utils/iconUtils"; // == Page props == @@ -89,7 +89,7 @@ export default function BadgePage() { label: "Icon type", type: "combobox", name: "iconType", - options: [""].concat(ICONS), + options: getIconOptions(), value: "", }, { diff --git a/src/routes/components/Button.tsx b/src/routes/components/Button.tsx index 88b0ae7b6..b221c866f 100644 --- a/src/routes/components/Button.tsx +++ b/src/routes/components/Button.tsx @@ -12,7 +12,7 @@ import { ComponentProperties, ComponentProperty, } from "@components/component-properties/ComponentProperties"; -import ICONS from "@routes/components/icons.json"; +import { getIconOptions } from "@utils/iconUtils"; import { ComponentContent } from "@components/component-content/ComponentContent"; import { ButtonExamples } from "@examples/button/ButtonExamples.tsx"; import { @@ -55,14 +55,14 @@ export default function ButtonPage() { label: "Leading icon", type: "combobox", name: "leadingIcon", - options: [""].concat(ICONS), + options: getIconOptions(), value: "", }, { label: "Trailing Icon", type: "combobox", name: "trailingIcon", - options: [""].concat(ICONS), + options: getIconOptions(), value: "", }, {label: "Width", type: "string", name: "width", value: ""}, diff --git a/src/routes/components/Dropdown.tsx b/src/routes/components/Dropdown.tsx index acbb213d7..ebbe31495 100644 --- a/src/routes/components/Dropdown.tsx +++ b/src/routes/components/Dropdown.tsx @@ -9,7 +9,7 @@ import { GoabTabs, } from "@abgov/react-components"; import { ComponentBinding, Sandbox } from "@components/sandbox"; -import ICONS from "./icons.json"; +import { getIconOptions } from "@utils/iconUtils"; import { Category, ComponentHeader } from "@components/component-header/ComponentHeader.tsx"; import { ComponentProperties, @@ -60,7 +60,7 @@ export default function DropdownPage() { label: "Leading icon", type: "combobox", name: "leadingIcon", - options: [""].concat(ICONS), + options: getIconOptions(), value: "", }, { diff --git a/src/routes/components/IconButton.tsx b/src/routes/components/IconButton.tsx index d5174638a..8149a5eb6 100644 --- a/src/routes/components/IconButton.tsx +++ b/src/routes/components/IconButton.tsx @@ -12,7 +12,7 @@ import { } from "@components/component-properties/ComponentProperties.tsx"; import { ComponentBinding, Sandbox } from "@components/sandbox"; import { useState } from "react"; -import ICONS from "./icons.json"; +import { getIconOptions } from "@utils/iconUtils"; import { ComponentContent } from "@components/component-content/ComponentContent"; import { GoabIconType } from "@abgov/ui-components-common"; import { DesignEmpty } from "@components/empty-states/design-empty/DesignEmpty.tsx"; @@ -46,7 +46,7 @@ export default function IconButtonPage() { label: "Icon", type: "combobox", name: "icon", - options: [""].concat(ICONS), + options: getIconOptions(), value: "refresh", }, { diff --git a/src/routes/components/Icons.tsx b/src/routes/components/Icons.tsx index 1c83db032..7b8be9382 100644 --- a/src/routes/components/Icons.tsx +++ b/src/routes/components/Icons.tsx @@ -1,4 +1,3 @@ -import ICONS from "./icons.json"; import { useState } from "react"; import { ComponentBinding, Sandbox } from "@components/sandbox"; import { @@ -15,20 +14,22 @@ import { LegacyTestIdProperties, MarginProperty, TestIdProperty } from "@components/component-properties/common-properties.ts"; +import { getIconOptions } from "@utils/iconUtils"; const FIGMA_LINK = "https://www.figma.com/design/3pb2IK8s2QUqWieH79KdN7/%E2%9D%96-Component-library-%7C-DDD?node-id=24019-471310"; export default function IconsPage() { + const iconOptions = getIconOptions(false); const [iconsProps, setIconsProps] = useState({ - type: ICONS[0] as GoabIconType, + type: iconOptions[0] as GoabIconType, }); const [iconsBindings, setIconsBindings] = useState([ { label: "Type", type: "combobox", name: "type", - options: ICONS, - value: ICONS[0], + options: iconOptions, + value: iconOptions[0], }, { label: "Size", @@ -38,14 +39,6 @@ export default function IconsPage() { value: "", defaultValue: "medium", }, - { - label: "Theme", - type: "list", - name: "theme", - options: ["", "outline", "filled"], - value: "", - defaultValue: "outline", - }, { label: "Opacity", type: "number", @@ -91,12 +84,6 @@ export default function IconsPage() { description: "Sets the size of icon.", defaultValue: "medium", }, - { - name: "theme", - type: "outline | filled", - description: "Styles the icon to show outline or filled.", - defaultValue: "outline", - }, { name: "opacity", type: "number", @@ -147,7 +134,7 @@ export default function IconsPage() { { name: "type", type: "GoabIconType", - description: "Sets the icon.", + description: "Sets the icon. You can optionally append a theme suffix to control the icon style (e.g. search:filled or search:outline). Defaults to outline if no theme is specified.", required: true, }, { @@ -156,12 +143,6 @@ export default function IconsPage() { description: "Sets the size of icon.", defaultValue: "medium", }, - { - name: "theme", - type: "GoabIconTheme (outline | filled)", - description: "Styles the icon to show outline or filled.", - defaultValue: "outline", - }, { name: "inverted", type: "boolean", @@ -195,9 +176,9 @@ export default function IconsPage() { ]; - function onSandboxChange(iconsBindings: ComponentBinding[], props: Record) { - setIconsBindings(iconsBindings); - setIconsProps(props as { type: GoabIconType; [key: string]: unknown }); + function onSandboxChange(bindings: ComponentBinding[], props: Record) { + setIconsBindings(bindings); + setIconsProps(props as { type: GoabIconType;[key: string]: unknown }); } return ( @@ -223,7 +204,10 @@ export default function IconsPage() {

Playground

- + @@ -305,9 +289,9 @@ export default function IconsPage() { The extended icon set includes the full {" "} - Ionicons library. - {" "} + target="_blank" rel="noreferrer"> + Ionicons library. + {" "} When you need additional icons outside of the core icon set, use these icons to maintain a consistent visual language. diff --git a/src/routes/components/Link.tsx b/src/routes/components/Link.tsx index 70f5ad8c0..f42f0966f 100644 --- a/src/routes/components/Link.tsx +++ b/src/routes/components/Link.tsx @@ -9,7 +9,7 @@ import { GoabBadge, GoabTab, GoabTabs, GoabLink } from "@abgov/react-components" import LinkExamples from "@examples/link/LinkExamples.tsx"; import { DesignEmpty } from "@components/empty-states/design-empty/DesignEmpty.tsx"; import { AccessibilityEmpty } from "@components/empty-states/accessibility-empty/AccessibilityEmpty.tsx"; -import ICONS from "@routes/components/icons.json"; +import { getIconOptions } from "@utils/iconUtils"; export default function LinkPage() { @@ -21,14 +21,14 @@ export default function LinkPage() { label: "Leading Icon", type: "combobox", name: "leadingIcon", - options: [""].concat(ICONS), + options: getIconOptions(), value: "", }, { label: "Trailing Icon", type: "combobox", name: "trailingIcon", - options: [""].concat(ICONS), + options: getIconOptions(), value: "", }, { diff --git a/src/routes/components/MenuButton.tsx b/src/routes/components/MenuButton.tsx index 13950a418..c969f76c3 100644 --- a/src/routes/components/MenuButton.tsx +++ b/src/routes/components/MenuButton.tsx @@ -22,6 +22,7 @@ import { AccessibilityEmpty } from "@components/empty-states/accessibility-empty import { DesignEmpty } from "@components/empty-states/design-empty/DesignEmpty.tsx"; import { ComponentBinding, Sandbox } from "@components/sandbox"; import { CodeSnippet } from "@components/code-snippet/CodeSnippet"; +import { getIconOptions } from "@utils/iconUtils"; const FIGMA_LINK = "https://www.figma.com/design/3pb2IK8s2QUqWieH79KdN7/%E2%9D%96-Component-library-%7C-DDD?node-id=69366-164803"; @@ -39,7 +40,7 @@ type ComponentPropsType = GoabMenuButtonProps; export default function MenuButtonPage() { const { version, language } = useContext(LanguageVersionContext); - + const [menuButtonProps, setMenuButtonProps] = useState({ text: "Menu actions", type: "primary", @@ -59,6 +60,20 @@ export default function MenuButtonPage() { options: ["primary", "secondary", "tertiary"], value: "primary", }, + { + label: "Leading icon", + type: "combobox", + name: "leadingIcon", + options: getIconOptions(), + value: "", + }, + { + label: "Max width", + type: "string", + name: "maxWidth", + helpText: "Sets the maximum width of the dropdown menu options.", + value: "", + }, ]); const menuButtonProperties: ComponentProperty[] = [ diff --git a/src/routes/components/TextField.tsx b/src/routes/components/TextField.tsx index 986dee54c..15eb51cb9 100644 --- a/src/routes/components/TextField.tsx +++ b/src/routes/components/TextField.tsx @@ -13,7 +13,7 @@ import { GoabTab, GoabTabs, } from "@abgov/react-components"; -import ICONS from "./icons.json"; +import { getIconOptions } from "@utils/iconUtils"; import { CodeSnippet } from "@components/code-snippet/CodeSnippet.tsx"; import { useSandboxFormItem } from "@hooks/useSandboxFormItem.tsx"; import { ComponentContent } from "@components/component-content/ComponentContent"; @@ -85,14 +85,14 @@ export default function TextFieldPage() { label: "Leading Icon", type: "combobox", name: "leadingIcon", - options: [""].concat(ICONS), + options: getIconOptions(), value: "", }, { label: "Trailing Icon", type: "combobox", name: "trailingIcon", - options: [""].concat(ICONS), + options: getIconOptions(), value: "", }, { diff --git a/src/routes/components/icons.json b/src/utils/iconUtils.ts similarity index 93% rename from src/routes/components/icons.json rename to src/utils/iconUtils.ts index e96b8f45a..aa543a0a3 100644 --- a/src/routes/components/icons.json +++ b/src/utils/iconUtils.ts @@ -1,4 +1,5 @@ -[ +// All available icons from Ionicons library +export const ICONS = [ "accessibility", "add-circle", "add", @@ -167,7 +168,6 @@ "file-tray-full", "file-tray", "file-tray-stacked", - "filenames.ps1", "film", "filter-circle", "filter", @@ -503,5 +503,20 @@ "logo-xing", "logo-yahoo", "logo-yen", - "logo-youtube;" -] + "logo-youtube", +]; + +/** + * @param includeEmpty - Whether to include an empty string as the first option (default: true) + * @returns Array of icon options including :filled variants + */ +export function getIconOptions(includeEmpty: boolean = true): string[] { + const options: string[] = includeEmpty ? [""] : []; + + for (const icon of ICONS) { + options.push(icon); + options.push(`${icon}:filled`); + } + + return options; +} diff --git a/tsconfig.json b/tsconfig.json index c85970eb3..deffbc015 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -41,6 +41,9 @@ ], "@examples/*": [ "./src/examples/*" + ], + "@utils/*": [ + "./src/utils/*" ] } }, diff --git a/vite.config.ts b/vite.config.ts index 78e4ecc37..58322d0bf 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,6 +16,7 @@ export default defineConfig({ "@hooks": path.resolve(__dirname, "./src/hooks"), "@contexts": path.resolve(__dirname, "./src/contexts"), "@examples": path.resolve(__dirname, "./src/examples"), + "@utils": path.resolve(__dirname, "./src/utils"), }, }, }); From 476793feaac7783165996ff83f0cc0e38d105b9f Mon Sep 17 00:00:00 2001 From: Vanessa Tran Date: Fri, 23 Jan 2026 12:53:36 -0700 Subject: [PATCH 12/15] chore: doc minHeight and maxHeight for container --- src/routes/components/Container.tsx | 36 +++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/routes/components/Container.tsx b/src/routes/components/Container.tsx index 820966d8e..6439d761a 100644 --- a/src/routes/components/Container.tsx +++ b/src/routes/components/Container.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useContext, useState } from "react"; import { ComponentBinding, Sandbox } from "@components/sandbox"; import { ComponentProperties, @@ -16,6 +16,7 @@ import { ComponentContent } from "@components/component-content/ComponentContent const FIGMA_LINK = "https://www.figma.com/design/3pb2IK8s2QUqWieH79KdN7/%E2%9D%96-Component-library-%7C-DDD?node-id=1789-12623"; import { DesignEmpty } from "@components/empty-states/design-empty/DesignEmpty.tsx"; import { AccessibilityEmpty } from "@components/empty-states/accessibility-empty/AccessibilityEmpty.tsx"; +import { LanguageVersionContext } from "@contexts/LanguageVersionContext.tsx"; // == Page props == const componentName = "Container"; @@ -29,6 +30,7 @@ const relatedComponents = [ type ComponentPropsType = GoabContainerProps; export default function ContainerPage() { + const { version } = useContext(LanguageVersionContext); const [containerProps, setContainerProps] = useState({}); const [containerBindings, setContainerBindings] = useState([ @@ -71,6 +73,20 @@ export default function ContainerPage() { requirement: "optional", value: "", }, + { + label: "Min Height", + type: "string", + name: "minHeight", + requirement: "optional", + value: "", + }, + { + label: "Max Height", + type: "string", + name: "maxHeight", + requirement: "optional", + value: "", + }, ]); const oldComponentProperties: ComponentProperty[] = [ @@ -170,6 +186,16 @@ export default function ContainerPage() { type: "string", description: "Sets the maximum width of the container.", }, + { + name: "minHeight", + type: "string", + description: "Sets the minimum height of the container.", + }, + { + name: "maxHeight", + type: "string", + description: "Sets the maximum height of the container.", + }, { name: "title", lang: "angular", @@ -231,7 +257,13 @@ export default function ContainerPage() {

Playground

- properties={containerBindings} onChange={onSandboxChange} fullWidth> + + properties={version === "new" + ? containerBindings + : containerBindings.filter(b => b.name !== "minHeight" && b.name !== "maxHeight")} + onChange={onSandboxChange} + fullWidth + >

Detach to use

Add things inside me

From b1a39e31d1ece26ea25169afe4aeb7ef6b6d37e9 Mon Sep 17 00:00:00 2001 From: Vanessa Tran Date: Fri, 23 Jan 2026 13:03:32 -0700 Subject: [PATCH 13/15] chore: add leadingIcon and maxWidth to MenuButton --- src/routes/components/MenuButton.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/routes/components/MenuButton.tsx b/src/routes/components/MenuButton.tsx index c969f76c3..f4bde288c 100644 --- a/src/routes/components/MenuButton.tsx +++ b/src/routes/components/MenuButton.tsx @@ -89,6 +89,16 @@ export default function MenuButtonPage() { description: "Controls the visual style of the trigger button.", defaultValue: "primary", }, + { + name: "leadingIcon", + type: "GoabIconType", + description: "Optional leading icon appearing within the button.", + }, + { + name: "maxWidth", + type: "string", + description: "Sets the maximum width of the dropdown menu.", + }, TestIdProperty, { name: "onAction", From fa2d04d5ba2509885048bbb3f10aff7448c2529a Mon Sep 17 00:00:00 2001 From: Vanessa Tran Date: Fri, 23 Jan 2026 13:24:17 -0700 Subject: [PATCH 14/15] chore: add id to GoabText --- src/routes/components/Text.tsx | 75 ++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/src/routes/components/Text.tsx b/src/routes/components/Text.tsx index aa00b559d..cecb93864 100644 --- a/src/routes/components/Text.tsx +++ b/src/routes/components/Text.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useContext, useState } from "react"; import { ComponentBinding, Sandbox } from "@components/sandbox"; import { ComponentProperties, @@ -9,10 +9,12 @@ import { GoabText, GoabTab, GoabTabs, GoabBadge } from "@abgov/react-components" import { DesignEmpty } from "@components/empty-states/design-empty/DesignEmpty.tsx"; import { AccessibilityEmpty } from "@components/empty-states/accessibility-empty/AccessibilityEmpty.tsx"; import { ExamplesEmpty } from "@components/empty-states/examples-empty/ExamplesEmpty.tsx"; +import { LanguageVersionContext } from "@contexts/LanguageVersionContext.tsx"; const FIGMA_LINK = "https://www.figma.com/design/3pb2IK8s2QUqWieH79KdN7/%E2%9D%96-Component-library-%7C-DDD?node-id=27301-303449"; export default function TextPage() { + const { version } = useContext(LanguageVersionContext); const [textProps, setTextProps] = useState({}); const [textBindings, setTextBindings] = useState([ @@ -57,8 +59,53 @@ export default function TextPage() { options: ["none", "3xs", "2xs", "xs", "s", "m", "l", "xl", "2xl", "3xl", "4xl"], value: "none", }, + { + label: "Id", + type: "string", + name: "id", + value: "", + }, ]); + const oldComponentProperties: ComponentProperty[] = [ + { + name: "tag", + type: "h1 | h2 | h3 | h4 | h5 | span | div | p", + description: "Sets the tag and text size.", + defaultValue: "div" + }, + { + name: "size", + type: "heading-xl | heading-l | heading-m | heading-s | heading-xs | body-l | body-m | body-s | body-xs", + description: "Overrides the text size from the 'as' property." + }, + { + name: "maxWidth", + type: "string", + description: "Sets the max width.", + defaultValue: "65ch", + lang: "react", + }, + { + name: "maxwidth", + type: "string", + description: "Sets the max width.", + defaultValue: "65ch", + lang: "angular", + }, + { + name: "color", + type: "primary | secondary", + description: "Sets the text colour.", + defaultValue: "primary" + }, + { + name: "mt,mr,mb,ml", + type: "none | 3xs | 2xs | xs | s | m | l | xl | 2xl | 3xl | 4xl", + description: "Apply margin to the top, right, bottom, and/or left of the component.", + }, + ]; + const componentProperties: ComponentProperty[] = [ { name: "tag", @@ -72,17 +119,22 @@ export default function TextPage() { description: "Overrides the text size from the 'as' property." }, { - name: "maxWidth", - type: "string", - description: "Sets the max width.", - defaultValue: "65ch" + name: "maxWidth", + type: "string", + description: "Sets the max width.", + defaultValue: "65ch" }, - { + { name: "color", type: "primary | secondary", description: "Sets the text colour.", defaultValue: "primary" - }, + }, + { + name: "id", + type: "string", + description: "Sets the HTML id attribute on the text element.", + }, { name: "mt,mr,mb,ml", type: "none | 3xs | 2xs | xs | s | m | l | xl | 2xl | 3xl | 4xl", @@ -108,13 +160,18 @@ export default function TextPage() {

Playground

- + b.name !== "id")} + onChange={onSandboxChange} + > Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - +
Date: Tue, 3 Feb 2026 11:42:45 -0700 Subject: [PATCH 15/15] Updating package versions --- package-lock.json | 27 ++++++++++++--------------- package.json | 6 +++--- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index b59ab47e2..179e9c8e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,9 @@ "name": "code-sandbox", "version": "0.0.0", "dependencies": { - "@abgov/react-components": "6.10.0-next.1", - "@abgov/ui-components-common": "1.10.0-next.1", - "@abgov/web-components": "1.40.0-next.1", + "@abgov/react-components": "6.10.0", + "@abgov/ui-components-common": "1.10.0", + "@abgov/web-components": "1.40.0", "@faker-js/faker": "^8.3.1", "highlight.js": "^11.8.0", "js-cookie": "^3.0.5", @@ -68,10 +68,9 @@ } }, "node_modules/@abgov/react-components": { - "version": "6.10.0-next.1", - "resolved": "https://registry.npmjs.org/@abgov/react-components/-/react-components-6.10.0-next.1.tgz", - "integrity": "sha512-82N8l6CQ/wI/CIyQQaYb4YvJ8pbvGAn4d9phnVAKcaJW7UBdsBjxYd97GDceGh4d+QKqxgLxREG2v+1Wslc1Qw==", - "license": "Apache-2.0", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@abgov/react-components/-/react-components-6.10.0.tgz", + "integrity": "sha512-+srxI/D50YvVIxcuuGIzE3Rm+XQmwrkhCQ6wSzPz6o+hN6SNprgH9aM5YefuaDqaDtLWIRitSNJgaoUrdqMgFA==", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", @@ -79,16 +78,14 @@ } }, "node_modules/@abgov/ui-components-common": { - "version": "1.10.0-next.1", - "resolved": "https://registry.npmjs.org/@abgov/ui-components-common/-/ui-components-common-1.10.0-next.1.tgz", - "integrity": "sha512-mrm9oTGwNmlBj2c6q+MATSu9DOhVpIIVK24YKi+9oFbR4XNZyc5YNGm2BIjN0h3+POVY0BeevAAr47A0K/ht3g==", - "license": "Apache-2.0" + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@abgov/ui-components-common/-/ui-components-common-1.10.0.tgz", + "integrity": "sha512-XQXRnoGay2H1ApEP7ZqUV9TpqDlktjD2tuwY1WL7iBCGPPuXgSWFpd0BAl4gMsvIt1S0VLM45wsCNULMy8adWQ==" }, "node_modules/@abgov/web-components": { - "version": "1.40.0-next.1", - "resolved": "https://registry.npmjs.org/@abgov/web-components/-/web-components-1.40.0-next.1.tgz", - "integrity": "sha512-BF6N3K+XF9AKjq7CBKl91nMb+uTzn1Yz91fCD5jVmMXW8BIPr7LgPW/vllhB338bEcGnEbXx2m0G/4iVxqc79A==", - "license": "Apache-2.0" + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@abgov/web-components/-/web-components-1.40.0.tgz", + "integrity": "sha512-bVPCxXxsEYILXN18fCS95NvKoy5pQ1tFruXleSW2jEN0d/80sXhaxN5rH8n+dYOG//qJgheziTbMpSOGqgTVUQ==" }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", diff --git a/package.json b/package.json index 32cb59e2b..24966cc66 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "prettier": "npx prettier . --write" }, "dependencies": { - "@abgov/react-components": "6.10.0-next.1", - "@abgov/ui-components-common": "1.10.0-next.1", - "@abgov/web-components": "1.40.0-next.1", + "@abgov/react-components": "6.10.0", + "@abgov/ui-components-common": "1.10.0", + "@abgov/web-components": "1.40.0", "@faker-js/faker": "^8.3.1", "highlight.js": "^11.8.0", "js-cookie": "^3.0.5",