From 844b8505805fbb8eea2d48aeca41ab900844d06f Mon Sep 17 00:00:00 2001 From: eirikbacker Date: Mon, 29 Jul 2024 15:29:45 +0200 Subject: [PATCH] fix: use u-details element --- packages/css/accordion.css | 28 +++--- packages/react/package.json | 3 +- .../Accordion/Accordion.stories.tsx | 16 ++-- .../components/Accordion/Accordion.test.tsx | 16 ---- .../components/Accordion/AccordionContent.tsx | 43 +++------- .../components/Accordion/AccordionHeading.tsx | 78 +++++------------ .../components/Accordion/AccordionItem.tsx | 85 +++++++++++-------- yarn.lock | 8 ++ 8 files changed, 112 insertions(+), 165 deletions(-) diff --git a/packages/css/accordion.css b/packages/css/accordion.css index a944ef9ca5..8bb545726c 100644 --- a/packages/css/accordion.css +++ b/packages/css/accordion.css @@ -29,6 +29,8 @@ } .ds-accordion__header { + cursor: pointer; + padding: var(--ds-spacing-4); margin: 0; width: 100%; display: flex; @@ -41,21 +43,7 @@ background-color: var(--dsc-accordion-button-background); } -.ds-accordion__button { - cursor: pointer; - width: 100%; - display: flex; - justify-content: flex-start; - align-items: center; - gap: var(--ds-spacing-2); - margin: 0; - padding: var(--ds-spacing-4); - background-color: transparent; - border: none; - font-family: inherit; -} - -.ds-accordion__item--open .ds-accordion__header { +.ds-accordion__item[open] .ds-accordion__header { background-color: var(--dsc-accordion-button-background-open); } @@ -63,10 +51,14 @@ position: relative; } -.ds-accordion__item:where(.ds-accordion__item--open) .ds-accordion__expand-icon { +.ds-accordion__item[open] .ds-accordion__expand-icon { transform: rotateZ(180deg); } +.ds-accordion__item--controlled:not([open]) .ds-accordion__content { + display: none; /* Turn off search-in-page-to-open when state is controlled */ +} + .ds-accordion__item:not(:first-child) .ds-accordion__header { border-top: 1px solid var(--dsc-accordion-border-color); } @@ -80,7 +72,7 @@ border-top-right-radius: var(--dsc-accordion-border-radius); } -.ds-accordion--border .ds-accordion__item:last-of-type:not(.ds-accordion__item--open) .ds-accordion__header:first-of-type { +.ds-accordion--border .ds-accordion__item:last-of-type:not([open]) .ds-accordion__header:first-of-type { border-bottom-left-radius: var(--dsc-accordion-border-radius); border-bottom-right-radius: var(--dsc-accordion-border-radius); } @@ -90,7 +82,7 @@ background-color: var(--dsc-accordion-icon-background-hover); } - .ds-accordion__item--open .ds-accordion__header:hover .ds-accordion__expand-icon { + .ds-accordion__item[open] .ds-accordion__header:hover .ds-accordion__expand-icon { background-color: var(--dsc-accordion-icon-background-active); } } diff --git a/packages/react/package.json b/packages/react/package.json index d835579f8f..46752bebc1 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -35,7 +35,8 @@ "@floating-ui/react": "0.26.12", "@navikt/aksel-icons": "^5.12.2", "@radix-ui/react-slot": "^1.0.2", - "@tanstack/react-virtual": "^3.5.1" + "@tanstack/react-virtual": "^3.5.1", + "@u-elements/u-details": "^0.0.5" }, "devDependencies": { "copyfiles": "^2.4.1", diff --git a/packages/react/src/components/Accordion/Accordion.stories.tsx b/packages/react/src/components/Accordion/Accordion.stories.tsx index d92e185fd8..c3601512c8 100644 --- a/packages/react/src/components/Accordion/Accordion.stories.tsx +++ b/packages/react/src/components/Accordion/Accordion.stories.tsx @@ -16,7 +16,7 @@ export default { export const Preview: StoryFn = (args) => ( - + Hvem kan registrere seg i Frivillighetsregisteret? @@ -27,7 +27,7 @@ export const Preview: StoryFn = (args) => ( - + Hvordan går jeg fram for å registrere i Frivillighetsregisteret? @@ -45,7 +45,7 @@ export const AccordionBorder: StoryFn = () => ( color='subtle' > - Vedlegg + Vedlegg Vedlegg 1, vedlegg 2, vedlegg 3 @@ -57,7 +57,7 @@ export const AccordionColor: StoryFn = () => ( color='brand2' > - + Hvordan får jeg tildelt et jegernummer? @@ -66,7 +66,7 @@ export const AccordionColor: StoryFn = () => ( - + Jeg har glemt jegernummeret mitt. Hvor finner jeg dette? @@ -92,7 +92,7 @@ export const Controlled: StoryFn = () => {
- setOpen(!open)}> + setOpen(!open)}> Enkeltpersonforetak @@ -104,7 +104,7 @@ export const Controlled: StoryFn = () => { - setOpen(!open)}> + setOpen(!open)}> Aksjeselskap (AS) @@ -116,7 +116,7 @@ export const Controlled: StoryFn = () => { - setOpen(!open)}> + setOpen(!open)}> Ansvarlig selskap (ANS/DA) diff --git a/packages/react/src/components/Accordion/Accordion.test.tsx b/packages/react/src/components/Accordion/Accordion.test.tsx index accd02117f..f712c70478 100644 --- a/packages/react/src/components/Accordion/Accordion.test.tsx +++ b/packages/react/src/components/Accordion/Accordion.test.tsx @@ -27,11 +27,6 @@ describe('Accordion', () => { render(); const accordionExpandButton = screen.getByRole('button'); - expect( - screen.getByRole('heading', { - name: 'Accordion Header Title Text', - }), - ).toBeInTheDocument(); expect(screen.getByText('The fantastic accordion content text')); expect(screen.getByText('Accordion Header Title Text')); expect(accordionExpandButton).toHaveAttribute('aria-expanded', 'false'); @@ -56,17 +51,6 @@ describe('Accordion', () => { const accordionExpandButton = screen.getByRole('button'); expect(accordionExpandButton).toHaveAttribute('aria-expanded', 'true'); }); - - test('should render heading as level 1 by default', () => { - render(); - - expect( - screen.getByRole('heading', { - name: 'Accordion Header Title Text', - level: 1, - }), - ); - }); }); describe('Accordion Accessibility', () => { diff --git a/packages/react/src/components/Accordion/AccordionContent.tsx b/packages/react/src/components/Accordion/AccordionContent.tsx index a817262db7..7bad5bc6cc 100644 --- a/packages/react/src/components/Accordion/AccordionContent.tsx +++ b/packages/react/src/components/Accordion/AccordionContent.tsx @@ -1,11 +1,8 @@ import cl from 'clsx/lite'; import type { HTMLAttributes } from 'react'; -import { forwardRef, useContext } from 'react'; +import { forwardRef } from 'react'; -import { AnimateHeight } from '../../utilities/AnimateHeight'; -import { Paragraph } from '../Typography'; - -import { AccordionItemContext } from './AccordionItem'; +import { Paragraph } from '..'; export type AccordionContentProps = HTMLAttributes; @@ -17,34 +14,18 @@ export type AccordionContentProps = HTMLAttributes; export const AccordionContent = forwardRef< HTMLDivElement, AccordionContentProps ->(({ children, className, ...rest }, ref) => { - const context = useContext(AccordionItemContext); - - if (context === null) { - console.error( - ' has to be used within an ', - ); - return null; - } - +>(({ className, ...rest }, ref) => { return ( - - -
- {children} -
-
-
+
+ ); }); diff --git a/packages/react/src/components/Accordion/AccordionHeading.tsx b/packages/react/src/components/Accordion/AccordionHeading.tsx index 633640b441..ff89edb9fb 100644 --- a/packages/react/src/components/Accordion/AccordionHeading.tsx +++ b/packages/react/src/components/Accordion/AccordionHeading.tsx @@ -1,76 +1,40 @@ import { ChevronDownIcon } from '@navikt/aksel-icons'; import cl from 'clsx/lite'; -import type { ReactNode, MouseEventHandler, HTMLAttributes } from 'react'; -import { forwardRef, useContext } from 'react'; +import type { ReactNode, HTMLAttributes } from 'react'; +import { forwardRef } from 'react'; -import { Paragraph, Heading } from '../Typography'; - -import { AccordionItemContext } from './AccordionItem'; +import { Paragraph } from '..'; export type AccordionHeaderProps = { - /** - * Heading level. Use this to make sure the heading is correct according to you page heading levels - * @default 1 - */ - level?: 1 | 2 | 3 | 4 | 5 | 6; - /** Handle when clicked on header */ - onHeaderClick?: MouseEventHandler | undefined; /** Heading text */ children: ReactNode; -} & HTMLAttributes; +} & HTMLAttributes; /** * Accordion header component, contains a button to toggle the content. * @example * Header */ -export const AccordionHeading = forwardRef< - HTMLHeadingElement, - AccordionHeaderProps ->(({ level = 1, children, className, onHeaderClick, ...rest }, ref) => { - const context = useContext(AccordionItemContext); - - if (context === null) { - console.error( - ' has to be used within an ', - ); - return null; - } - - const handleClick: MouseEventHandler = (e) => { - context.toggleOpen(); - onHeaderClick && onHeaderClick(e); - }; - - return ( - ( + ({ children, className, ...rest }, ref) => ( + - - - ); -}); + {children} + + + ), +); AccordionHeading.displayName = 'AccordionHeading'; diff --git a/packages/react/src/components/Accordion/AccordionItem.tsx b/packages/react/src/components/Accordion/AccordionItem.tsx index 5e4c0e1db2..66a4bbc638 100644 --- a/packages/react/src/components/Accordion/AccordionItem.tsx +++ b/packages/react/src/components/Accordion/AccordionItem.tsx @@ -1,6 +1,8 @@ import cl from 'clsx/lite'; import type { ReactNode, HTMLAttributes } from 'react'; -import { createContext, forwardRef, useState, useId } from 'react'; +import { useMergeRefs } from '@floating-ui/react'; +import { forwardRef, useEffect, useRef } from 'react'; +import '@u-elements/u-details'; export type AccordionItemProps = { /** @@ -18,16 +20,7 @@ export type AccordionItemProps = { defaultOpen?: boolean; /** Content should be one `` and `` */ children: ReactNode; -} & HTMLAttributes; - -type AccordionItemContextProps = { - open: boolean; - toggleOpen: () => void; - contentId: string; -}; - -export const AccordionItemContext = - createContext(null); +} & HTMLAttributes; /** * Accordion item component, contains `Accordion.Header` and `Accordion.Content` components. @@ -37,37 +30,61 @@ export const AccordionItemContext = * Content * */ -export const AccordionItem = forwardRef( - ({ children, className, open, defaultOpen = false, ...rest }, ref) => { - const [internalOpen, setInternalOpen] = useState(defaultOpen); - const contentId = useId(); +export const AccordionItem = forwardRef( + ({ className, open, defaultOpen = false, ...rest }, ref) => { + const internalOpen = useRef(open ?? defaultOpen); // Only render open state on server, let
handle state in browser + const detailsRef = useRef(null); + const mergedRefs = useMergeRefs([detailsRef, ref]); + + // Control state with a useEffect to animate on prop change and prevent native
toggle + useEffect(() => { + const details = detailsRef.current; + const summary = details?.querySelector(':scope > u-summary'); + const isControlled = open !== undefined; + const handleSummaryClick = (event: Event) => { + event?.preventDefault(); // Prevent native
toggle so we can animate + if (!isControlled) animateToggle(details!); + }; + + summary?.addEventListener('click', handleSummaryClick); + if (details && isControlled) animateToggle(details, open); + return () => summary?.removeEventListener('click', handleSummaryClick); + }, [open]); return ( -
- { - if (open === undefined) { - setInternalOpen((iOpen) => !iOpen); - } - }, - contentId: contentId, - }} - > - {children} - -
+ /> ); }, ); AccordionItem.displayName = 'AccordionItem'; + +const REDUCED_MOTION = '(prefers-reduced-motion: reduce)'; +function animateToggle(details: HTMLDetailsElement, open = !details.open) { + const isAnimateSupported = 'animate' in details; + const isReducedMotion = window.matchMedia?.(REDUCED_MOTION).matches; + + if (details.open === open) return; + if (isReducedMotion || !isAnimateSupported) return (details.open = open); + + const closed = (details.open = false) || `${details.scrollHeight}px`; + const opened = (details.open = true) && `${details.scrollHeight}px`; + + details.style.overflow = 'clip'; // Clip content while animating + details.animate( + { height: [open ? closed : opened, open ? opened : closed] }, + { duration: 400, easing: 'ease-in-out' }, + ).onfinish = () => { + details.style.removeProperty('overflow'); // Restore overlow + details.open = open; + }; +} diff --git a/yarn.lock b/yarn.lock index 4da062c1d5..65508c6355 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2640,6 +2640,7 @@ __metadata: "@navikt/aksel-icons": "npm:^5.12.2" "@radix-ui/react-slot": "npm:^1.0.2" "@tanstack/react-virtual": "npm:^3.5.1" + "@u-elements/u-details": "npm:^0.0.5" copyfiles: "npm:^2.4.1" rimraf: "npm:^5.0.5" rollup: "npm:^4.12.1" @@ -7225,6 +7226,13 @@ __metadata: languageName: node linkType: hard +"@u-elements/u-details@npm:^0.0.5": + version: 0.0.5 + resolution: "@u-elements/u-details@npm:0.0.5" + checksum: 10/1bbfa9c1fa2fadf55a77d5cd6065d1a27202628f3567e88b239792602fd5df900bb3e2175e7f5b15509d8f94a33ae5a95a61ed335a66b7b21b9fa41a1ad4f4a5 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.0.0, @ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0"