Skip to content

Commit 59690f8

Browse files
authored
Merge pull request #66 from forumone/1.x-RC
1.0.8
2 parents 70bce90 + f23db88 commit 59690f8

File tree

16 files changed

+2683
-1595
lines changed

16 files changed

+2683
-1595
lines changed

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
18.17
1+
18.18

.storybook/main.js

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,12 @@ const StylelintWebpackPlugin = require('stylelint-webpack-plugin');
33
const YAML = require('yaml');
44

55
module.exports = {
6-
staticDirs: [path.resolve(__dirname, '../public')],
6+
staticDirs: ['../public'],
77
stories: ['../source/**/*.stories.@(js|jsx|ts|tsx)', '../source/**/*.mdx'],
88
addons: [
99
'@storybook/addon-links',
1010
'@storybook/addon-essentials',
1111
'@storybook/addon-interactions',
12-
{
13-
name: '@storybook/addon-styling',
14-
options: {
15-
postcssLoaderOptions: {
16-
implementation: require('postcss'),
17-
},
18-
},
19-
},
2012
'@storybook/addon-a11y',
2113
],
2214
framework: {
@@ -26,7 +18,7 @@ module.exports = {
2618
webpackFinal: async config => {
2719
config.plugins.push(
2820
new StylelintWebpackPlugin({
29-
exclude: ['node_modules', 'storybook'],
21+
exclude: ['node_modules', 'storybook', '.next'],
3022
}),
3123
);
3224
config.module.rules.find(

.storybook/manager.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { addons } from '@storybook/addons';
1+
import { addons } from '@storybook/manager-api';
22
import theme from './theme';
33

44
addons.setConfig({

.storybook/theme.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const storybookTheme = create({
55
appContentBg: '#fff',
66
barBg: '#19013A',
77
barSelectedColor: '#EE2737',
8+
barHoverColor: '#EE2737',
89
barTextColor: '#fff',
910
base: 'light',
1011
brandTitle: 'Forum One',

package-lock.json

Lines changed: 2070 additions & 1568 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,17 @@
3030
"react-select": "^5.7.2"
3131
},
3232
"devDependencies": {
33-
"@babel/core": "^7.20.2",
33+
"@babel/core": "^7.23.2",
3434
"@csstools/postcss-global-data": "^1.0.1",
35-
"@storybook/addon-a11y": "^7.1.0",
36-
"@storybook/addon-actions": "^7.1.0",
37-
"@storybook/addon-essentials": "^7.1.0",
38-
"@storybook/addon-interactions": "^7.1.0",
39-
"@storybook/addon-links": "^7.1.0",
40-
"@storybook/addon-styling": "^1.3.4",
41-
"@storybook/nextjs": "^7.1.0",
42-
"@storybook/react": "^7.1.0",
43-
"@storybook/testing-library": "^0.2.0",
44-
"@storybook/theming": "^7.1.0",
35+
"@storybook/addon-a11y": "^7.5.1",
36+
"@storybook/addon-actions": "^7.5.1",
37+
"@storybook/addon-essentials": "^7.5.1",
38+
"@storybook/addon-interactions": "^7.5.1",
39+
"@storybook/addon-links": "^7.5.1",
40+
"@storybook/nextjs": "^7.5.1",
41+
"@storybook/react": "^7.5.1",
42+
"@storybook/testing-library": "^0.2.2",
43+
"@storybook/theming": "^7.5.1",
4544
"@svgr/babel-plugin-add-jsx-attribute": "^8.0.0",
4645
"@svgr/babel-plugin-remove-jsx-attribute": "^8.0.0",
4746
"@svgr/cli": "^8.0.1",
@@ -51,6 +50,7 @@
5150
"@typescript-eslint/parser": "^5.20.0",
5251
"babel-loader": "^8.3.0",
5352
"clsx": "^1.2.1",
53+
"css-loader": "^6.8.1",
5454
"enhanced-resolve": "5.10.0",
5555
"eslint": "^8.13.0",
5656
"eslint-config-next": "^13.2.3",
@@ -62,13 +62,14 @@
6262
"mkdirp": "^1.0.4",
6363
"postcss": "^8.4.21",
6464
"postcss-advanced-variables": "github:kmonahan/postcss-advanced-variables",
65-
"postcss-loader": "^7.0.2",
65+
"postcss-loader": "^7.3.3",
6666
"postcss-nesting": "^11.1.0",
6767
"postcss-preset-env": "^8.0.1",
6868
"postcss-rem": "^2.0.2",
6969
"prettier": "^2.6.2",
7070
"prettier-plugin-organize-imports": "^2.3.4",
71-
"storybook": "^7.1.0",
71+
"storybook": "^7.5.1",
72+
"style-loader": "^3.3.3",
7273
"stylelint": "^15.5.0",
7374
"stylelint-config-standard": "^33.0.0",
7475
"stylelint-order": "^6.0.3",

source/00-config/constants.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
11
const MAIN_ID = 'main';
22

3-
export { MAIN_ID };
3+
const KEYCODE = {
4+
TAB: 'Tab',
5+
RETURN: 'Enter',
6+
ESC: 'Escape',
7+
SPACE: 'Space',
8+
PAGEUP: 'PageUp',
9+
PAGEDOWN: 'PageDown',
10+
END: 'End',
11+
HOME: 'Home',
12+
LEFT: 'ArrowLeft',
13+
UP: 'ArrowUp',
14+
RIGHT: 'ArrowRight',
15+
DOWN: 'ArrowDown',
16+
};
17+
18+
export { MAIN_ID, KEYCODE };
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Meta, StoryObj } from '@storybook/react';
2+
import parse from 'html-react-parser';
3+
import AccordionComponent from './Accordion';
4+
import accordionArgs from './accordion.yml';
5+
import { AccordionItemProps } from './AccordionItem';
6+
7+
const meta: Meta<typeof AccordionComponent> = {
8+
title: 'Components/Accordion',
9+
component: AccordionComponent,
10+
tags: ['autodocs'],
11+
};
12+
13+
type Story = StoryObj<typeof AccordionComponent>;
14+
15+
accordionArgs.accordionItems = accordionArgs.accordionItems.map(
16+
(
17+
item:
18+
| (Omit<AccordionItemProps, 'content'> & { content: string })
19+
| AccordionItemProps,
20+
) => {
21+
if (typeof item.content === 'string') {
22+
item.content = parse(item.content) as string;
23+
}
24+
return item;
25+
},
26+
);
27+
28+
const Accordion: Story = {
29+
render: args => <AccordionComponent {...args} />,
30+
args: {
31+
...accordionArgs,
32+
},
33+
};
34+
35+
export default meta;
36+
export { Accordion };
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import clsx from 'clsx';
2+
import { GessoComponent } from 'gesso';
3+
import { KeyboardEvent, createRef, useId, useMemo, useState } from 'react';
4+
import styles from './accordion.module.css';
5+
import getCssVar from '../../06-utility/getCssVar';
6+
import { KEYCODE } from '../../00-config/constants';
7+
import AccordionItem, { AccordionItemProps } from './AccordionItem';
8+
9+
interface AccordionProps extends GessoComponent {
10+
accordionItems: AccordionItemProps[];
11+
accordionSpeed?: string;
12+
allowMultiple?: boolean;
13+
allowToggle?: boolean;
14+
}
15+
16+
function Accordion({
17+
accordionItems,
18+
accordionSpeed = getCssVar('duration-standard'),
19+
allowMultiple,
20+
allowToggle,
21+
modifierClasses,
22+
}: AccordionProps): JSX.Element {
23+
const accordionId = useId();
24+
const [accordionItemsStatus, setAccordionItemsStatus] = useState(
25+
accordionItems.map((item, index) => ({
26+
...item,
27+
id: `${accordionId}-${index}`,
28+
})),
29+
);
30+
const accordionItemRefs = useMemo(() => {
31+
const refs: { [key: string]: React.RefObject<HTMLButtonElement> } = {};
32+
accordionItemsStatus.forEach(item => (refs[item.id] = createRef()));
33+
return refs;
34+
}, [accordionItemsStatus]);
35+
36+
const openAccordionItem = (items: AccordionItemProps[], index: number) => {
37+
return [
38+
...items.slice(0, index),
39+
{
40+
...items[index],
41+
isOpen: true,
42+
},
43+
...items.slice(index + 1),
44+
];
45+
};
46+
47+
const closeAccordionItem = (items: AccordionItemProps[], index: number) => {
48+
return [
49+
...items.slice(0, index),
50+
{
51+
...items[index],
52+
isOpen: false,
53+
},
54+
...items.slice(index + 1),
55+
];
56+
};
57+
58+
const handleClick = (id: string, isOpen = false) => {
59+
const toggleAllowed = allowMultiple ? true : allowToggle;
60+
const active = accordionItemsStatus.findIndex(item => item.isOpen);
61+
const itemIndex = accordionItemsStatus.findIndex(item => item.id === id);
62+
let itemStatusUpdated = [...accordionItemsStatus];
63+
64+
// Without allowMultiple, close the open accordion
65+
if (!allowMultiple && active !== -1 && active !== itemIndex) {
66+
itemStatusUpdated = closeAccordionItem(itemStatusUpdated, active);
67+
}
68+
69+
if (!isOpen) {
70+
itemStatusUpdated = openAccordionItem(itemStatusUpdated, itemIndex);
71+
} else if (toggleAllowed && isOpen) {
72+
itemStatusUpdated = closeAccordionItem(itemStatusUpdated, itemIndex);
73+
}
74+
75+
return setAccordionItemsStatus(itemStatusUpdated);
76+
};
77+
78+
const handleKeydown = (event: KeyboardEvent) => {
79+
const currentTarget = event.target as HTMLButtonElement;
80+
81+
// Create the array of toggle elements for the accordion group
82+
const triggers = Object.values(accordionItemRefs).map(ref => ref.current);
83+
84+
// Is this coming from an accordion header?
85+
if (triggers && currentTarget.tagName === 'BUTTON') {
86+
// Up/ Down arrow and Control + Page Up/ Page Down keyboard operations
87+
if (
88+
event.code === KEYCODE.UP ||
89+
event.code === KEYCODE.DOWN ||
90+
event.code === KEYCODE.PAGEUP ||
91+
event.code === KEYCODE.PAGEDOWN
92+
) {
93+
const index = triggers.indexOf(currentTarget);
94+
let direction;
95+
if (event.code === KEYCODE.DOWN || event.code === KEYCODE.PAGEDOWN) {
96+
direction = 1;
97+
} else {
98+
direction = -1;
99+
}
100+
const triggerLength = triggers.length;
101+
const newIndex = (index + triggerLength + direction) % triggerLength;
102+
triggers[newIndex]?.focus();
103+
event.preventDefault();
104+
} else if (event.code === KEYCODE.HOME || event.code === KEYCODE.END) {
105+
switch (event.code) {
106+
// Go to first accordion
107+
case KEYCODE.HOME:
108+
triggers[0]?.focus();
109+
break;
110+
// Go to last accordion
111+
case KEYCODE.END:
112+
triggers[triggers.length - 1]?.focus();
113+
break;
114+
default:
115+
triggers[0]?.focus();
116+
break;
117+
}
118+
event.preventDefault();
119+
}
120+
}
121+
};
122+
123+
return (
124+
<>
125+
<div
126+
className={clsx(styles.accordion, modifierClasses)}
127+
id={accordionId}
128+
onKeyDown={handleKeydown}
129+
>
130+
<div className={styles.content}>
131+
{accordionItemsStatus.map(item => {
132+
return (
133+
<AccordionItem
134+
key={item.id}
135+
{...item}
136+
accordionSpeed={accordionSpeed}
137+
toggleRef={accordionItemRefs[item.id]}
138+
handleClick={() => handleClick(item.id, item.isOpen)}
139+
/>
140+
);
141+
})}
142+
</div>
143+
</div>
144+
</>
145+
);
146+
}
147+
148+
export default Accordion;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import clsx from 'clsx';
2+
import { GessoComponent } from 'gesso';
3+
import {
4+
ElementType,
5+
MouseEventHandler,
6+
ReactElement,
7+
useEffect,
8+
useRef,
9+
} from 'react';
10+
import styles from './accordion-item.module.css';
11+
import { slideCollapse, slideExpand } from '../../06-utility/slide';
12+
13+
export interface AccordionItemProps extends GessoComponent {
14+
id: string;
15+
title: string;
16+
content: ReactElement;
17+
titleElement?: ElementType;
18+
isOpen?: boolean;
19+
accordionSpeed?: string;
20+
toggleRef?: React.RefObject<HTMLButtonElement>;
21+
handleClick: MouseEventHandler;
22+
}
23+
24+
function AccordionItem({
25+
id,
26+
title,
27+
content,
28+
titleElement: TitleElement = 'h3',
29+
isOpen,
30+
accordionSpeed,
31+
toggleRef,
32+
modifierClasses,
33+
handleClick,
34+
}: AccordionItemProps): JSX.Element {
35+
const accordionItemSectionRef = useRef(null);
36+
37+
const sectionId = `accordion-section-${id}`;
38+
const buttonId = `accordion-button-${id}`;
39+
40+
useEffect(() => {
41+
if (isOpen && accordionItemSectionRef.current) {
42+
slideExpand(accordionItemSectionRef.current, accordionSpeed);
43+
} else if (!isOpen && accordionItemSectionRef.current) {
44+
slideCollapse(accordionItemSectionRef.current, accordionSpeed);
45+
}
46+
}, [isOpen, accordionSpeed]);
47+
48+
return (
49+
<div className={clsx(styles.accordionItem, modifierClasses)}>
50+
<div className={styles.panel}>
51+
<TitleElement className={styles.heading}>
52+
<button
53+
className={styles.toggle}
54+
id={buttonId}
55+
aria-expanded={isOpen}
56+
aria-controls={sectionId}
57+
ref={toggleRef}
58+
onClick={handleClick}
59+
>
60+
{title}
61+
<span className={styles.icon}></span>
62+
</button>
63+
</TitleElement>
64+
<div
65+
ref={accordionItemSectionRef}
66+
className={styles.drawer}
67+
id={sectionId}
68+
aria-labelledby={buttonId}
69+
aria-expanded={isOpen}
70+
>
71+
<div className={styles.drawerInner}>{content}</div>
72+
</div>
73+
</div>
74+
</div>
75+
);
76+
}
77+
78+
export default AccordionItem;

0 commit comments

Comments
 (0)