From 9d2d06b72363f170796d1de9e428adf2755f7836 Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Thu, 3 Oct 2024 16:19:53 +0200 Subject: [PATCH 1/2] WIP implementation of panels using radix accordion for a11y --- .../interfaces/name-generator/NodePanels.tsx | 71 +++++++++--- package.json | 1 + pnpm-lock.yaml | 106 +++++++++++++++++- 3 files changed, 160 insertions(+), 18 deletions(-) diff --git a/components/interview/interfaces/name-generator/NodePanels.tsx b/components/interview/interfaces/name-generator/NodePanels.tsx index 68c40baa..9aa6b247 100644 --- a/components/interview/interfaces/name-generator/NodePanels.tsx +++ b/components/interview/interfaces/name-generator/NodePanels.tsx @@ -1,7 +1,15 @@ +'use client'; + import type { Node } from '~/components/interview/NodeList'; +import * as Accordion from '@radix-ui/react-accordion'; import NodePanel from './NodePanel'; -import { forwardRef, type ForwardedRef } from 'react'; -import { useTranslations } from 'next-intl'; +import { useState, type ForwardedRef } from 'react'; +import NodeList from '~/components/interview/NodeList'; +import Surface, { MotionSurface } from '~/components/layout/Surface'; +import Heading from '~/components/typography/Heading'; +import { LayoutGroup, motion } from 'framer-motion'; + +const MotionTrigger = motion(Accordion.Trigger); type Panel = { id: string; @@ -9,19 +17,52 @@ type Panel = { nodes: Node[]; }; -export default forwardRef(function NodePanels( - { panels }: { panels: Panel[] }, - ref: ForwardedRef, -) { - const t = useTranslations(`Protocol.Panels`); +export default function NodePanels({ + panels, + ref, +}: { + panels: Panel[]; + ref: ForwardedRef; +}) { + const [values, setValues] = useState( + panels.map((panel) => panel.id), + ); return ( -
- {panels.map((panel) => { - return ( - - ); - })} -
+ + + {panels.map((panel) => ( + + + + + {panel.title} + + + + {values.includes(panel.id) ? ( + + ) : null} + + + + ))} + + ); -}); +} diff --git a/package.json b/package.json index bd7df960..bbab6ce0 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@formatjs/intl-localematcher": "^0.5.4", "@lucia-auth/adapter-prisma": "^4.0.1", "@node-rs/argon2": "^1.8.3", + "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-direction": "^1.1.0", "@radix-ui/react-label": "^2.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 144ddcba..c6cc96c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,9 @@ importers: '@node-rs/argon2': specifier: ^1.8.3 version: 1.8.3 + '@radix-ui/react-accordion': + specifier: ^1.2.1 + version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) '@radix-ui/react-dialog': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) @@ -1658,6 +1661,19 @@ packages: '@radix-ui/primitive@1.1.0': resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + '@radix-ui/react-accordion@1.2.1': + resolution: {integrity: sha512-bg/l7l5QzUjgsh8kjwDFommzAshnUsuVMV5NM56QVCm+7ZckYdd9P/ExR8xG/Oup0OajVxNLaHJ1tb8mXk+nzQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-arrow@1.1.0': resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} peerDependencies: @@ -1671,6 +1687,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collapsible@1.1.1': + resolution: {integrity: sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.0': resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} peerDependencies: @@ -1702,6 +1731,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.1.1': resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==} peerDependencies: @@ -1833,6 +1871,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.1': + resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.0.0': resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} peerDependencies: @@ -7165,6 +7216,23 @@ snapshots: '@radix-ui/primitive@1.1.0': {} + '@radix-ui/react-accordion@1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collapsible': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + react: 19.0.0-rc-e740d4b1-20240919 + react-dom: 19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) @@ -7174,6 +7242,22 @@ snapshots: '@types/react': 18.3.5 '@types/react-dom': 18.3.0 + '@radix-ui/react-collapsible@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + react: 19.0.0-rc-e740d4b1-20240919 + react-dom: 19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) @@ -7198,6 +7282,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.5 + '@radix-ui/react-context@1.1.1(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + react: 19.0.0-rc-e740d4b1-20240919 + optionalDependencies: + '@types/react': 18.3.5 + '@radix-ui/react-dialog@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -7333,6 +7423,16 @@ snapshots: '@types/react': 18.3.5 '@types/react-dom': 18.3.0 + '@radix-ui/react-presence@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + react: 19.0.0-rc-e740d4b1-20240919 + react-dom: 19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': dependencies: '@radix-ui/react-slot': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) @@ -9371,7 +9471,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.0) eslint-plugin-react: 7.36.1(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -9406,7 +9506,7 @@ snapshots: is-bun-module: 1.2.1 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -9424,7 +9524,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): + eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 From 014c19d2fa2b0590a15e6c450da7e550aa3c92a2 Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Fri, 4 Oct 2024 12:42:49 +0200 Subject: [PATCH 2/2] refactor Panels and NodePanels --- .../name-generator/NameGenerator.tsx | 2 +- .../interfaces/name-generator/NodePanel.tsx | 52 +++++++++++--- .../interfaces/name-generator/NodePanels.tsx | 70 +++++++------------ .../interfaces/name-generator/Panel.tsx | 55 --------------- 4 files changed, 69 insertions(+), 110 deletions(-) delete mode 100644 components/interview/interfaces/name-generator/Panel.tsx diff --git a/components/interview/interfaces/name-generator/NameGenerator.tsx b/components/interview/interfaces/name-generator/NameGenerator.tsx index 187f7782..c688dccf 100644 --- a/components/interview/interfaces/name-generator/NameGenerator.tsx +++ b/components/interview/interfaces/name-generator/NameGenerator.tsx @@ -86,7 +86,7 @@ function NameGenerator(_props: InterviewStage) {
- +
diff --git a/components/interview/interfaces/name-generator/NodePanel.tsx b/components/interview/interfaces/name-generator/NodePanel.tsx index d43b1a89..6adf10c9 100644 --- a/components/interview/interfaces/name-generator/NodePanel.tsx +++ b/components/interview/interfaces/name-generator/NodePanel.tsx @@ -1,16 +1,52 @@ -import Panel from './Panel'; -import NodeList, { type Node } from '../../NodeList'; +import * as Accordion from '@radix-ui/react-accordion'; +import { motion } from 'framer-motion'; +import { MotionSurface } from '~/components/layout/Surface'; +import Heading from '~/components/typography/Heading'; +import { cn } from '~/lib/utils'; -export default function NodePanel({ - nodes, +// incompatibility between framer-motion 12.x and new react types +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const MotionTrigger = motion(Accordion.Trigger); + +export default function Panel({ + id, title, + expanded, + children, + noHighlight, }: { - nodes: Node[]; + id: string; title: string; + expanded: boolean; + children: React.ReactNode; + noHighlight?: boolean; }) { + const panelClasses = cn( + 'flex flex-1 flex-col rounded-small border-b border-b-4 border-panel-1 shadow-xl', + { + 'border-b-0': noHighlight, + }, + ); + + const contentClasses = cn( + 'h-auto flex-grow border-t border-background flex flex-col overflow-hidden items-center', + ); + return ( - - - + + + + + {title} + + + + {expanded ? children : null} + + + ); } diff --git a/components/interview/interfaces/name-generator/NodePanels.tsx b/components/interview/interfaces/name-generator/NodePanels.tsx index 9aa6b247..5fba65be 100644 --- a/components/interview/interfaces/name-generator/NodePanels.tsx +++ b/components/interview/interfaces/name-generator/NodePanels.tsx @@ -2,67 +2,45 @@ import type { Node } from '~/components/interview/NodeList'; import * as Accordion from '@radix-ui/react-accordion'; -import NodePanel from './NodePanel'; -import { useState, type ForwardedRef } from 'react'; +import { useState } from 'react'; import NodeList from '~/components/interview/NodeList'; -import Surface, { MotionSurface } from '~/components/layout/Surface'; -import Heading from '~/components/typography/Heading'; -import { LayoutGroup, motion } from 'framer-motion'; - -const MotionTrigger = motion(Accordion.Trigger); +import NodePanel from './NodePanel'; +// TODO: Remove once connected to state type Panel = { id: string; title: string; nodes: Node[]; }; -export default function NodePanels({ - panels, - ref, -}: { +type NodePanels = { panels: Panel[]; - ref: ForwardedRef; -}) { +} & React.ComponentProps<'div'>; + +export default function NodePanels({ panels, ...rest }: NodePanels) { const [values, setValues] = useState( panels.map((panel) => panel.id), ); return ( - - + +
{panels.map((panel) => ( - - - - - {panel.title} - - - - {values.includes(panel.id) ? ( - - ) : null} - - - + + + ))} - - +
+
); } diff --git a/components/interview/interfaces/name-generator/Panel.tsx b/components/interview/interfaces/name-generator/Panel.tsx deleted file mode 100644 index 56ca5431..00000000 --- a/components/interview/interfaces/name-generator/Panel.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ -'use client'; - -import { useState, useCallback } from 'react'; -import Surface from '~/components/layout/Surface'; -import { cn } from '~/lib/utils'; - -export default function Panel({ - title, - children, - noCollapse, - minimize, - noHighlight, -}: { - title: string; - children: React.ReactNode; - noCollapse?: boolean; - minimize?: boolean; - noHighlight?: boolean; -}) { - const [collapsed, setCollapsed] = useState(false); - - const panelClasses = cn( - 'flex flex-1 flex-col rounded-small border-b border-b-4 border-panel-1 shadow-xl', - { - 'border-b-0': noHighlight, - 'border-b-0 opacity-0 mb-0 flex-grow-0': minimize, - }, - ); - - const contentClasses = cn('flex flex-col overflow-hidden items-center', { - 'h-0 flex-grow-0': collapsed, - 'h-auto flex-grow border-t border-background': !collapsed, - }); - - const toggleCollapsed = useCallback(() => { - if (noCollapse) { - return; - } - setCollapsed((value) => !value); - }, [noCollapse]); - - return ( - -
-

{title}

-
-
{children}
-
- ); -}