Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React UI: Navigation Header Accordion component #954

Merged
merged 40 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9af062d
feat: initial NavAccordion based on original Accordion
timoheddes Sep 28, 2023
14c6681
feat: NavAccordion style and structure
timoheddes Sep 28, 2023
e3904ca
feat: add internal classname prop to Link
timoheddes Oct 2, 2023
2940021
feat: extend NavAccordion style
timoheddes Oct 2, 2023
b772dc1
chore: changeset
timoheddes Oct 2, 2023
b9b33f3
chore: update Tree description
timoheddes Oct 3, 2023
9e77776
feat: improve structure
timoheddes Oct 3, 2023
33e0570
chore: build react-ui
timoheddes Oct 3, 2023
4dcecf8
feat: update structure
timoheddes Oct 3, 2023
6ea0aad
chore: remove unused import
timoheddes Oct 3, 2023
15db1ff
feat: support asChild and update story description
timoheddes Oct 4, 2023
81047fd
chore: add noreferrer to asChild sample
timoheddes Oct 4, 2023
6243705
feat: center button wrapper items
timoheddes Oct 9, 2023
14ff28f
feat: switch to using context instead of cloneElement
timoheddes Oct 10, 2023
ea80c78
feat: refactor implemention/use of context
timoheddes Oct 11, 2023
69347cc
feat: style changes
timoheddes Oct 11, 2023
a9c483c
feat: restructure
timoheddes Oct 11, 2023
f240568
feat: style refactor
timoheddes Oct 11, 2023
92ae290
feat: semantics
timoheddes Oct 11, 2023
9adda35
feat: semantics
timoheddes Oct 11, 2023
0300f38
chore: remove console.log
timoheddes Oct 11, 2023
db3511b
chore: undo Link changes
timoheddes Oct 12, 2023
84f18a6
feat: minor style change
timoheddes Oct 12, 2023
a9d2ff2
feat: update Accordion Link component
timoheddes Oct 12, 2023
7280290
feat: update Accordion Link component
timoheddes Oct 12, 2023
544e75b
chore: update accordion heading
timoheddes Oct 12, 2023
c57f969
chore: Tag -> Element
timoheddes Oct 12, 2023
e1b55f8
feat: remove unnecessary classnames
timoheddes Oct 12, 2023
3a4a7a7
chore: set h3 fontSize
timoheddes Oct 12, 2023
b6f6f5f
chore: remove animation prep
timoheddes Oct 12, 2023
6ae52b7
chore: move rules to sprinkles
timoheddes Oct 12, 2023
b2080d4
chore: remove unused index
timoheddes Oct 12, 2023
d35e369
chore: remove initialOpenSection
timoheddes Oct 12, 2023
38c8901
chore: remove onclick prop
timoheddes Oct 12, 2023
e7e465f
chore: remove heading subcomponent
timoheddes Oct 13, 2023
534a1d5
chore: semantics
timoheddes Oct 13, 2023
571f3b0
chore: cleanup
timoheddes Oct 13, 2023
a5857ed
feat: darkMode as prop
timoheddes Oct 13, 2023
a1312dc
chore: text fix
timoheddes Oct 13, 2023
bbfea2e
chore: update lockfile
timoheddes Oct 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eighty-turkeys-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@kadena/react-ui': minor
---

Introduced Accordion variant for navigational purposes
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Dispatch, SetStateAction } from 'react';
import { createContext } from 'react';

export type OpenSections = string[];

interface IAccordionContext {
openSections: OpenSections;
setOpenSections: Dispatch<SetStateAction<OpenSections>>;
linked: boolean;
}

export const initialOpenSections: OpenSections = [];
export const AccordionContext = createContext<IAccordionContext>({
openSections: initialOpenSections,
setOpenSections: () => {},
linked: false,
});
20 changes: 15 additions & 5 deletions packages/libs/react-ui/src/components/Accordion/Accordion.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { sprinkles } from '@theme/sprinkles.css';
import { vars } from '@theme/vars.css';
import { style } from '@vanilla-extract/css';

export const accordionSectionWrapperClass = style([
export const accordionSectionClass = style([
sprinkles({
display: 'block',
marginBottom: '$6',
marginBottom: '$4',
overflow: 'hidden',
}),
{
borderBottom: `1px solid ${vars.colors.$borderDefault}`,
Expand All @@ -17,18 +18,26 @@ export const accordionSectionWrapperClass = style([
},
]);

export const accordionHeadingTitleClass = style([
sprinkles({
fontSize: '$base',
}),
]);

export const accordionButtonClass = style([
sprinkles({
alignItems: 'center',
background: 'none',
border: 'none',
color: '$neutral5',
cursor: 'pointer',
display: 'flex',
fontSize: '$base',
fontWeight: '$medium',
fontWeight: '$semiBold',
justifyContent: 'space-between',
padding: 0,
paddingBottom: '$2',
paddingRight: '$1',
textAlign: 'left',
width: '100%',
}),
Expand All @@ -43,7 +52,7 @@ export const accordionToggleIconClass = style([
transition: 'transform 0.2s ease',
selectors: {
'&.isOpen': {
transform: 'rotate(0deg)',
transform: 'rotate(180deg)',
},
},
},
Expand All @@ -53,8 +62,9 @@ export const accordionContentClass = style([
sprinkles({
color: '$neutral5',
fontSize: '$base',
margin: 0,
overflow: 'hidden',
padding: 0,
paddingBottom: '$2',
paddingTop: '$2',
}),
]);
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ const generateSection = (i: number): IAccordionSectionProps => ({
title: `Section title ${i}`,
children: (
<p>
This is the content for section {i}. The type of this content is not
restricted: any valid HTML content is allowed.
This is the content for section {i}.<br />
The type of this content is not restricted: any valid HTML content is
allowed.
</p>
),
onOpen: () => console.log(`open section ${i}`),
Expand All @@ -18,12 +19,11 @@ const generateSection = (i: number): IAccordionSectionProps => ({
const generateSections = (n: number): IAccordionSectionProps[] =>
Array.from({ length: n }, (d, i) => generateSection(i + 1));

const sampleSections: IAccordionSectionProps[] = generateSections(5);
const sampleCount: number = 3;
const sampleSections: IAccordionSectionProps[] = generateSections(sampleCount);

type StoryProps = {
sectionCount: number;
linked: boolean;
useCustomSections: boolean;
customSections: IAccordionSectionProps[];
} & IAccordionProps;

Expand All @@ -37,7 +37,7 @@ const meta: Meta<StoryProps> = {
docs: {
description: {
component:
'The Accordion component allows the user to show and hide sections of content on a page.<br />These sections can be expanded and collapsed by clicking the section headers.<br /><br /><strong>initialOpenSection</strong><br />This optional prop can be used on the Root element to set the initially opened section<br /><em>It defaults to `undefined` and has only been explcitly set to `-1` in the story code for demonstration purposes.</em><br /><br /><em>Note: this variant of the Accordion component is meant to be used to display content. For Navigation purposes, please check the other variant within the Navigation subgroup.</em>',
'The Accordion component allows the user to show and hide sections of content on a page.<br />These sections can be expanded and collapsed by clicking the section headers.<br /><br /><em>Note: this variant of the Accordion component is meant to be used to display content.<br />For Navigation purposes, please use the <strong>NavAccordion</strong> within the Navigation subgroup.</em>',
},
},
},
Expand All @@ -51,31 +51,6 @@ const meta: Meta<StoryProps> = {
type: { summary: 'boolean' },
},
},
sectionCount: {
control: { type: 'range', min: 1, max: sampleSections.length, step: 1 },
description: 'Adjust sample section items count',
if: { arg: 'useCustomContent', neq: true },
table: {
defaultValue: { summary: 3 },
type: { summary: 'number' },
},
},
useCustomSections: {
control: { type: 'boolean' },
description: 'Define your own sections instead of the sample ones?',
table: {
defaultValue: { summary: 'false' },
type: { summary: 'boolean' },
},
},
customSections: {
defaultValue: [],
description: 'Custom sections',
control: {
type: 'array',
},
if: { arg: 'useCustomSections', eq: true },
},
},
};

Expand All @@ -85,30 +60,26 @@ export const Dynamic: IStory = {
name: 'Accordion',
args: {
linked: false,
sectionCount: 3,
customSections: sampleSections,
},
render: ({ linked, sectionCount, useCustomSections, customSections }) => {
const sections = useCustomSections ? customSections : sampleSections;
render: ({ linked }) => {
const sections = sampleSections;
return (
<Accordion.Root linked={linked} initialOpenSection={-1}>
{sections
.slice(0, sectionCount)
.map(
(
{ title, children, onOpen, onClose }: IAccordionSectionProps,
index,
) => (
<Accordion.Section
onOpen={onOpen}
onClose={onClose}
title={title}
key={index}
>
{children}
</Accordion.Section>
),
)}
<Accordion.Root linked={linked}>
{sections.map(
(
{ title, children, onOpen, onClose }: IAccordionSectionProps,
index,
) => (
<Accordion.Section
onOpen={onOpen}
onClose={onClose}
title={title}
key={index}
>
{children}
</Accordion.Section>
),
)}
</Accordion.Root>
);
},
Expand Down
47 changes: 12 additions & 35 deletions packages/libs/react-ui/src/components/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,31 @@
'use client';

import type { IAccordionSectionProps } from '.';
import { accordionContentClass } from './Accordion.css';
import type { OpenSections } from './Accordion.context';
import { AccordionContext } from './Accordion.context';

import type { FC, FunctionComponentElement } from 'react';
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';

export interface IAccordionRootProps {
children?: FunctionComponentElement<IAccordionSectionProps>[];
linked?: boolean;
initialOpenSection?: number;
initialOpenSection?: OpenSections;
}

export const AccordionRoot: FC<IAccordionRootProps> = ({
children,
linked = false,
initialOpenSection = undefined,
initialOpenSection = [],
}) => {
const [openSections, setOpenSections] = useState([initialOpenSection]);

useEffect(() => {
if (linked && openSections.length > 1) {
const lastOpen = openSections.pop() || undefined;
setOpenSections([lastOpen]);
}
}, [linked]);
const [openSections, setOpenSections] =
useState<OpenSections>(initialOpenSection);

return (
<div data-testid="kda-accordion-sections">
{React.Children.map(children, (section, sectionIndex) =>
React.cloneElement(
section as React.ReactElement<
HTMLElement | IAccordionSectionProps,
React.JSXElementConstructor<JSX.Element & IAccordionSectionProps>
>,
{
index: sectionIndex,
isOpen: openSections.includes(sectionIndex),
className: accordionContentClass,
onClick: () =>
openSections.includes(sectionIndex)
? setOpenSections(
openSections.filter((i) => i !== sectionIndex),
)
: setOpenSections(
linked ? [sectionIndex] : [...openSections, sectionIndex],
),
},
),
)}
</div>
<AccordionContext.Provider
value={{ openSections, setOpenSections, linked }}
>
{children}
</AccordionContext.Provider>
);
};
Original file line number Diff line number Diff line change
@@ -1,59 +1,66 @@
'use client';

import { AccordionContext } from './Accordion.context';
import {
accordionButtonClass,
accordionContentClass,
accordionSectionWrapperClass,
accordionHeadingTitleClass,
accordionSectionClass,
accordionToggleIconClass,
} from './Accordion.css';

import { SystemIcon } from '@components/Icon';
import classNames from 'classnames';
import type { FC } from 'react';
import React from 'react';
import React, { useContext } from 'react';

export interface IAccordionSectionProps {
children?: React.ReactNode;
index?: number;
isOpen?: boolean;
onClick?: () => void;
onClose?: () => void;
onOpen?: () => void;
title: string;
}

export const AccordionSection: FC<IAccordionSectionProps> = ({
children,
isOpen,
onClick,
onClose,
onOpen,
title,
}) => {
const { openSections, setOpenSections, linked } =
useContext(AccordionContext);
const sectionId = title.replace(/\s+/g, '-').toLowerCase();
const isOpen = openSections.includes(sectionId);

const handleClick = (): void => {
if (isOpen) {
setOpenSections(
linked ? [] : [...openSections.filter((i) => i !== sectionId)],
);
onClose?.();
} else {
setOpenSections(linked ? [sectionId] : [...openSections, sectionId]);
onOpen?.();
}
onClick?.();
};
return (
<section
className={accordionSectionWrapperClass}
className={accordionSectionClass}
data-testid="kda-accordion-section"
>
<button className={accordionButtonClass} onClick={handleClick}>
{title}
<button
className={classNames([accordionButtonClass])}
onClick={handleClick}
>
<h3 className={accordionHeadingTitleClass}>{title}</h3>
<SystemIcon.Close
className={classNames(accordionToggleIconClass, {
isOpen,
})}
size="xs"
size="sm"
/>
</button>

{isOpen && children && (
{children && isOpen && (
<div className={accordionContentClass}>{children}</div>
)}
</section>
Expand Down
12 changes: 6 additions & 6 deletions packages/libs/react-ui/src/components/Link/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@ import type { FC, ReactNode } from 'react';
import React from 'react';

export interface ILinkProps {
timoheddes marked this conversation as resolved.
Show resolved Hide resolved
href?: string;
target?: '_blank' | '_self' | '_parent' | '_top';
asChild?: boolean;
block?: boolean;
timoheddes marked this conversation as resolved.
Show resolved Hide resolved
children: ReactNode;
href?: string;
icon?: keyof typeof SystemIcon;
iconAlign?: 'left' | 'right';
asChild?: boolean;
block?: boolean;
target?: '_blank' | '_self' | '_parent' | '_top';
}

export const Link: FC<ILinkProps> = ({
asChild = false,
block = false,
children,
icon,
iconAlign = 'left',
asChild = false,
block = false,
...restProps
}) => {
const Icon = icon && SystemIcon[icon];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Dispatch, SetStateAction } from 'react';
import { createContext } from 'react';

export type NavAccordionState = string[];

interface INavAccordionContext {
openSections: NavAccordionState;
setOpenSections: Dispatch<SetStateAction<NavAccordionState>>;
linked: boolean;
}

export const initialOpenSections: NavAccordionState = [];
export const NavAccordionContext = createContext<INavAccordionContext>({
openSections: initialOpenSections,
setOpenSections: () => {},
linked: false,
});
Loading