From ff0eee697a3aa39493812a5ea06da0058fbf9e76 Mon Sep 17 00:00:00 2001 From: Pekka Helesuo Date: Fri, 6 Sep 2024 14:18:09 +0300 Subject: [PATCH 1/2] documentation TOC navigable by keyboard --- .../[version]/[slug]/AccordionListWrapper.tsx | 67 +++++++++++++++++++ .../docs/[version]/[slug]/page.tsx | 41 ++---------- components/Accordion/Accordion.tsx | 24 +++++-- 3 files changed, 94 insertions(+), 38 deletions(-) create mode 100644 app/documentation/docs/[version]/[slug]/AccordionListWrapper.tsx diff --git a/app/documentation/docs/[version]/[slug]/AccordionListWrapper.tsx b/app/documentation/docs/[version]/[slug]/AccordionListWrapper.tsx new file mode 100644 index 0000000..a987853 --- /dev/null +++ b/app/documentation/docs/[version]/[slug]/AccordionListWrapper.tsx @@ -0,0 +1,67 @@ +"use client"; +import styles from '@/styles/accordion.module.scss' +import { DocAnchorLinksType, MarkdownFileMetadata } from '@/types/types' +import Accordion from '../../../../../components/Accordion/Accordion'; +import { useState } from 'react'; +import Link from 'next/link'; +import { cleanTags } from '@/lib/utils'; + +export default function AccordionListWrapper({ + items, + initialOpenSections +}: { + items: MarkdownFileMetadata[] | null, + initialOpenSections: string[] +}) { + + const [openSections, setOpenSections] = useState(initialOpenSections); + const renderAccordionContent = ( + items: Array, parentSlug: string, isOpen: boolean + ) => { + return ( + + ) + } + + const updateOpenAccordion = (id: string, isOpen: boolean) => { + let newOpenSections: string[] = Object.assign([], openSections); + if (isOpen) { + if (!newOpenSections.includes(id)) { + newOpenSections.push(id); + } + } else { + newOpenSections = openSections.filter(accordionId => accordionId !== id); + } + + setOpenSections(newOpenSections); + } + + + return <> + {items?.map((item: MarkdownFileMetadata) => ( + + ))} + ; + + + +} diff --git a/app/documentation/docs/[version]/[slug]/page.tsx b/app/documentation/docs/[version]/[slug]/page.tsx index 4417146..f04cf17 100644 --- a/app/documentation/docs/[version]/[slug]/page.tsx +++ b/app/documentation/docs/[version]/[slug]/page.tsx @@ -1,21 +1,18 @@ -import Accordion from '@/components/Accordion/Accordion' import AccordionGroup from '@/components/Accordion/AccordionGroup' import VersionSidebar from '@/components/VersionSidebar' import { compareSemanticVersions } from '@/utils/misc' -import Link from 'next/link' -import styles from '@/styles/accordion.module.scss' -import { cleanTags, getVersionIndex } from '@/lib/utils' +import { getVersionIndex } from '@/lib/utils' import availableVersions from '@/_content/docs'; import Error from '@/components/Error' -import { DocAnchorLinksType, MarkdownFileMetadata } from '@/types/types' +import { MarkdownFileMetadata } from '@/types/types' import '@/styles/apidoc.scss' import '@fortawesome/fontawesome-free/js/fontawesome.min.js'; import '@fortawesome/fontawesome-free/css/fontawesome.min.css'; import '@fortawesome/fontawesome-free/css/solid.min.css'; import ApiDocContentWrapper from '@/app/documentation/api/components/ApiDocContentWrapper' +import AccordionListWrapper from '@/app/documentation/docs/[version]/[slug]/AccordionListWrapper' let indexJSON: MarkdownFileMetadata[] | null = null; - export const generateMetadata = async ({ params, }: { @@ -32,30 +29,13 @@ export const generateMetadata = async ({ } -const renderAccordionContent = ( - items: Array, parentSlug: string -) => { - return ( -
    - {items?.map((item, index) => ( -
  • - - {item.sectionNumber} {cleanTags(item.content)} - -
  • - ))} -
- ) -} - export default async function SingleDocPage({ params, }: { params: { slug: string; version: string } }) { + const { version } = params; if (!indexJSON) { indexJSON = await getVersionIndex(version, true); @@ -78,16 +58,9 @@ export default async function SingleDocPage({ return <>
- - {indexJSON?.map((item: MarkdownFileMetadata) => ( - - ))} - - + + +
{activeSection ? ( diff --git a/components/Accordion/Accordion.tsx b/components/Accordion/Accordion.tsx index a8061b7..c6f7925 100644 --- a/components/Accordion/Accordion.tsx +++ b/components/Accordion/Accordion.tsx @@ -1,16 +1,20 @@ 'use client' -import React, { useEffect, useRef, useState } from 'react' +import React, { KeyboardEvent, useEffect, useRef, useState } from 'react' import styles from '@/styles/accordion.module.scss' import Image from 'next/image' export default function Accordion({ + id, title, content, initialOpen = false, + updateIsOpen, }: { + id?: string title: string content: string | React.ReactNode - initialOpen?: boolean + initialOpen?: boolean, + updateIsOpen?: (key: string, isOpen: boolean) => void, }) { const [isOpen, setIsOpen] = useState(initialOpen) const [isAccordionGroup, setIsAccordionGroup] = useState(false) @@ -29,9 +33,19 @@ export default function Accordion({ child.classList.toggle(styles.open) }) } - return } - setIsOpen((prev) => !prev) + setIsOpen((prev) => { + if (updateIsOpen && id) { + updateIsOpen(id, !prev); + } + return !prev; + }) + } + + const accordionKeyUp = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + toggleAccordion(); + } } useEffect(() => { @@ -49,8 +63,10 @@ export default function Accordion({ >
{title} From c75a9d779edef2fb1044f707e8aaf26542c3cce2 Mon Sep 17 00:00:00 2001 From: Pekka Helesuo Date: Mon, 9 Sep 2024 16:17:01 +0300 Subject: [PATCH 2/2] accordion-item active indicator selector-inferno --- styles/accordion.module.scss | 80 ++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/styles/accordion.module.scss b/styles/accordion.module.scss index 8f07ef1..ca4eb76 100644 --- a/styles/accordion.module.scss +++ b/styles/accordion.module.scss @@ -54,16 +54,77 @@ .accordionGroup { & > * { &:first-child { + outline: none; border-bottom-left-radius: 0; border-bottom-right-radius: 0; + + div[role="button"] { + &:active, &:focus { + outline: none; + border-top-left-radius: 1.875rem; + border-top-right-radius: 1.875rem; + border: 1px solid; + } + } } + &:not(:first-child) { border-radius: 0; border-top: 1px solid #f3f3f3; } + &:last-child { border-bottom-left-radius: 1.875rem; border-bottom-right-radius: 1.875rem; + + div[role="button"] { + &:active, &:focus { + outline: none; + border: 1px solid; + } + } + + li:first-child { + margin-top: 0.225rem; + outline: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + + a:active, a:focus { + margin-top: 0.225rem; + outline: none; + border: 1px solid; + } + } + + li:last-child { + outline: none; + border-bottom-left-radius: 1.875rem; + border-bottom-right-radius: 1.875rem; + + a:active, a:focus { + border-bottom-left-radius: 1.875rem; + border-bottom-right-radius: 1.875rem; + outline: none; + border: 1px solid; + } + } + } + + &:last-child:not(.accordion.open) { + div[role="button"] { + &:active, &:focus { + outline: none; + border-bottom-left-radius: 1.875rem; + border-bottom-right-radius: 1.875rem; + border: 1px solid; + } + } + } + + /*uncover the top border of active link of the first list element*/ + li:first-child { + margin-top: 0.2rem; } } } @@ -89,10 +150,29 @@ padding: 1.25rem 2rem; width: 100%; display: block; + + &:active, &:focus { + outline: none; + border: 1px solid black; + } + } &:not(:first-child) { border-top: 1px solid #f3f3f3; } + + &:last-child { + border-bottom-left-radius: 1.875rem; + border-bottom-right-radius: 1.875rem; + } +/* + &:last-child { + a:active, a:focus { + border-bottom-left-radius: 1.875rem; + border-bottom-right-radius: 1.875rem; + } + } + */ } }