diff --git a/package-lock.json b/package-lock.json index e72f7a76b..179e9c8e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,16 @@ "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", + "@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", "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": { @@ -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" @@ -68,10 +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==", - "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-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==", - "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-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==", - "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", @@ -700,7 +697,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 +917,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 +1416,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 +1502,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 +1674,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 +1953,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 +2550,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 +2872,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 +2883,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 +2892,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 +2907,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 +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..24966cc66 100644 --- a/package.json +++ b/package.json @@ -12,16 +12,16 @@ "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", + "@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", "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": { @@ -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" } } diff --git a/src/components/sandbox/Sandbox.tsx b/src/components/sandbox/Sandbox.tsx index f7a89edda..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 => { @@ -259,6 +269,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); @@ -269,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[] { @@ -289,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/data-grid/DataGridExamples.tsx b/src/examples/data-grid/DataGridExamples.tsx new file mode 100644 index 000000000..262b0e7f3 --- /dev/null +++ b/src/examples/data-grid/DataGridExamples.tsx @@ -0,0 +1,27 @@ +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"; + +interface DataGridExamplesProps { + isGridReady?: boolean; +} + +export function DataGridExamples({ + isGridReady = true, +}: DataGridExamplesProps) { + 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..5e62cd60b --- /dev/null +++ b/src/examples/data-grid/basic-table-with-keyboard-navigation.tsx @@ -0,0 +1,165 @@ +import { Sandbox } from "@components/sandbox"; +import { CodeSnippet } from "@components/code-snippet/CodeSnippet.tsx"; +import { + GoabBadge, + GoabButton, + GoabDataGrid, + GoabTable, +} from "@abgov/react-components"; + +interface BasicTableWithKeyboardNavigationProps { + isGridReady?: boolean; +} + +export function BasicTableWithKeyboardNavigation({ + isGridReady = true, +}: BasicTableWithKeyboardNavigationProps) { + + 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 ( + <> + {isGridReady && ( + + + + + 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..538038547 --- /dev/null +++ b/src/examples/data-grid/layout-mode-with-cards.tsx @@ -0,0 +1,358 @@ +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; +}; + +interface LayoutModeWithCardsProps { + isGridReady?: boolean; +} + +export function LayoutModeWithCards({ + isGridReady = true, +}: LayoutModeWithCardsProps) { + + 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 ( + <> + {isGridReady && ( + + + {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..a87c66d28 --- /dev/null +++ b/src/examples/data-grid/sortable-table-with-row-selection.tsx @@ -0,0 +1,384 @@ +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; +}; + +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" }, + { 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 = selectedIds.length === applications.length && applications.length > 0; + const isIndeterminate = selectedIds.length > 0 && selectedIds.length < applications.length; + + 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) => { + 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 ( + <> + {isGridReady && ( + + +
+ + + + + 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 */} + 0; + } + + get isIndeterminate(): boolean { + return this.selectedIds.length > 0 && this.selectedIds.length < this.applications.length; + } + + isSelected(id: string): boolean { + return this.selectedIds.includes(id); + } + + toggleSelection(id: string) { + if (this.selectedIds.includes(id)) { + this.selectedIds = this.selectedIds.filter(selectedId => selectedId !== id); + } else { + this.selectedIds = [...this.selectedIds, id]; + } + } + + selectAll(checked: boolean) { + 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 = selectedIds.length === applications.length && applications.length > 0; + const isIndeterminate = selectedIds.length > 0 && selectedIds.length < applications.length; + + 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) => { + 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/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/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/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..0fc5dcc4c 100644 --- a/src/routes/components/Badge.tsx +++ b/src/routes/components/Badge.tsx @@ -8,10 +8,9 @@ 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"; +import { getIconOptions } from "@utils/iconUtils"; // == Page props == @@ -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({ @@ -96,7 +89,7 @@ export default function BadgePage() { label: "Icon type", type: "combobox", name: "iconType", - options: [""].concat(ICONS), + options: getIconOptions(), value: "", }, { @@ -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/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/Container.tsx b/src/routes/components/Container.tsx index 1a1c9e493..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"; @@ -27,11 +28,9 @@ const relatedComponents = [ { link: "/components/divider", name: "Divider" } ]; type ComponentPropsType = GoabContainerProps; -type CastingType = { - [key: string]: unknown; -}; export default function ContainerPage() { + const { version } = useContext(LanguageVersionContext); const [containerProps, setContainerProps] = useState({}); const [containerBindings, setContainerBindings] = useState([ @@ -74,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[] = [ @@ -173,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", @@ -213,9 +236,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 +257,13 @@ export default function ContainerPage() {

Playground

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

Detach to use

Add things inside me

diff --git a/src/routes/components/DataGrid.tsx b/src/routes/components/DataGrid.tsx new file mode 100644 index 000000000..4a65194c2 --- /dev/null +++ b/src/routes/components/DataGrid.tsx @@ -0,0 +1,955 @@ +import { useState, useContext, useEffect, useRef } from "react"; +import { + GoabBadge, + GoabBlock, + 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", + keyboardIconPosition: "right", + }); + + // 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(() => { + 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" }, + { 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 = selectedUsers.length === users.length && users.length > 0; + const isIndeterminate = selectedUsers.length > 0 && selectedUsers.length < users.length; + + const [layoutView, setLayoutView] = useState<"table" | "card">("table"); + + const [componentBindings, setComponentBindings] = useState([ + { + label: "Layout View", + type: "dropdown", + name: "layoutView", + options: ["table", "card"], + value: "table", + }, + { + label: "Icon Position", + type: "dropdown", + name: "keyboardIconPosition", + options: ["", "left", "right"], + value: "right", + 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) => { + 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); + 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[] = [ + { + 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 */} + {isGridReady && layoutView === "table" && ( + + + + + + + selectAll(e.checked)} + /> + + + Name + + + Status + + Email + Actions + + + + {users.map(user => ( + + + toggleSelection(user.id)} + /> + + {user.name} + + + + {user.email} + + handleMenuAction(user.id, e.action)} + > + + + + + + ))} + + + + + )} + + {/* Card view */} + {isGridReady && layoutView === "card" && ( + + + {users.map(user => ( + +
+ toggleSelection(user.id)} + /> +
+ + {user.name} + + + + {user.email} + +
+ handleMenuAction(user.id, e.action)} + > + + + +
+
+ ))} +
+
+ )} + + + + + {/* Angular - Table View */} + {layoutView === "table" && ( + 0; + } + + get isIndeterminate(): boolean { + return this.selectedUsers.length > 0 && this.selectedUsers.length < this.users.length; + } + + getStatusBadgeType(status: string): GoabBadgeType { + const types: Record = { + "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.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); + } + } + }`} + /> + )} + {layoutView === "table" && ( + + + + + + + + + + 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]; + } + } + + 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 = 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) { + 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) => { + 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" && ( + + + + + + + + + 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)} + > + + + +
+
+ ))} + `} + /> + )} +
+ + + +

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/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..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, @@ -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); @@ -66,7 +60,7 @@ export default function DropdownPage() { label: "Leading icon", type: "combobox", name: "leadingIcon", - options: [""].concat(ICONS), + options: getIconOptions(), value: "", }, { @@ -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, @@ -51,7 +46,7 @@ export default function IconButtonPage() { label: "Icon", type: "combobox", name: "icon", - options: [""].concat(ICONS), + options: getIconOptions(), value: "refresh", }, { @@ -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/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 70218b81e..f4bde288c 100644 --- a/src/routes/components/MenuButton.tsx +++ b/src/routes/components/MenuButton.tsx @@ -21,8 +21,8 @@ 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"; +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"; @@ -37,15 +37,10 @@ const relatedComponents = [ ]; type ComponentPropsType = GoabMenuButtonProps; -type CastingType = { - text: string, - type: GoabButtonType, - [key: string]: unknown; -}; export default function MenuButtonPage() { const { version, language } = useContext(LanguageVersionContext); - + const [menuButtonProps, setMenuButtonProps] = useState({ text: "Menu actions", type: "primary", @@ -65,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[] = [ @@ -80,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", @@ -114,9 +133,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 +162,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/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/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. - +
& { 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); @@ -91,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: "", }, { @@ -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}> 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/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: , 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"), }, }, });