Skip to content

Commit 167f82e

Browse files
platypiisevero
andauthored
Dropdown component (#204)
* Dropdown component * install storybook and add stories for Dropdown component (#207) * PR feedback * Remove unused css * Keyboard navigation --------- Co-authored-by: Sylvain Lesage <[email protected]>
1 parent 14589a9 commit 167f82e

File tree

18 files changed

+695
-18
lines changed

18 files changed

+695
-18
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ package-lock.json
3131
/coverage/
3232

3333
/lib/
34-
tsconfig.tsbuildinfo
34+
tsconfig.tsbuildinfo
35+
*storybook.log

.storybook/global.css

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/* From https://www.joshwcomeau.com/css/custom-css-reset/ */
2+
3+
/* 1. Use a more-intuitive box-sizing model */
4+
*,
5+
*::before,
6+
*::after {
7+
box-sizing: border-box;
8+
}
9+
10+
/* 2. Remove default margin */
11+
* {
12+
margin: 0;
13+
}
14+
15+
/* 3. Enable keyword animations */
16+
@media (prefers-reduced-motion: no-preference) {
17+
html {
18+
interpolate-size: allow-keywords;
19+
}
20+
}
21+
22+
body {
23+
/* 4. Add accessible line-height */
24+
line-height: 1.5;
25+
/* 5. Improve text rendering */
26+
-webkit-font-smoothing: antialiased;
27+
}
28+
29+
/* 6. Improve media defaults */
30+
img,
31+
picture,
32+
video,
33+
canvas,
34+
svg {
35+
display: block;
36+
max-width: 100%;
37+
}
38+
39+
/* 7. Inherit fonts for form controls */
40+
input,
41+
button,
42+
textarea,
43+
select {
44+
font: inherit;
45+
}
46+
47+
/* 8. Avoid text overflows */
48+
p,
49+
h1,
50+
h2,
51+
h3,
52+
h4,
53+
h5,
54+
h6 {
55+
overflow-wrap: break-word;
56+
}
57+
58+
/* 9. Improve line wrapping */
59+
p {
60+
text-wrap: pretty;
61+
}
62+
h1,
63+
h2,
64+
h3,
65+
h4,
66+
h5,
67+
h6 {
68+
text-wrap: balance;
69+
}
70+
71+
/*
72+
10. Create a root stacking context
73+
*/
74+
#root,
75+
#__next {
76+
isolation: isolate;
77+
}
78+
79+
/* Storybook */
80+
81+
* {
82+
font-family: "Mulish", "Helvetica Neue", Helvetica, Arial, sans-serif;
83+
}

.storybook/main.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { StorybookConfig } from '@storybook/react-vite'
2+
3+
const config: StorybookConfig = {
4+
stories: [
5+
'../src/**/*.mdx',
6+
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
7+
],
8+
addons: [
9+
{
10+
name: '@storybook/addon-essentials',
11+
options: {
12+
docs: false,
13+
},
14+
},
15+
'@storybook/addon-interactions',
16+
],
17+
framework: {
18+
name: '@storybook/react-vite',
19+
options: {},
20+
},
21+
core: {
22+
disableTelemetry: true,
23+
},
24+
}
25+
export default config

.storybook/preview-head.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<link rel="preconnect" href="https://fonts.gstatic.com" />
2+
<link
3+
href="https://fonts.googleapis.com/css2?family=Mulish:wght@300;600&display=swap"
4+
rel="stylesheet"
5+
/>

.storybook/preview.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Preview } from '@storybook/react'
2+
import './global.css'
3+
4+
const preview: Preview = {
5+
parameters: {
6+
controls: {
7+
matchers: {
8+
color: /(background|color)$/i,
9+
date: /Date$/i,
10+
},
11+
},
12+
},
13+
}
14+
15+
export default preview

eslint.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import javascript from '@eslint/js'
22
import react from 'eslint-plugin-react'
33
import reactHooks from 'eslint-plugin-react-hooks'
44
import reactRefresh from 'eslint-plugin-react-refresh'
5+
import storybook from 'eslint-plugin-storybook'
56
import globals from 'globals'
67
import typescript from 'typescript-eslint'
78

@@ -59,6 +60,7 @@ export default typescript.config(
5960
'no-useless-return': 'error',
6061
'no-var': 'error',
6162
'object-curly-spacing': ['error', 'always'],
63+
'object-shorthand': 'error',
6264
'prefer-const': 'warn',
6365
'prefer-destructuring': ['warn', {
6466
object: true,
@@ -104,5 +106,11 @@ export default typescript.config(
104106
...globals.node,
105107
},
106108
},
109+
},
110+
{
111+
extends: [
112+
...storybook.configs['flat/recommended'],
113+
],
114+
files: ['**/*.stories.tsx'],
107115
}
108116
)

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"prepublishOnly": "npm run build",
4242
"serve": "node bin/cli.js",
4343
"preserve": "npm run build",
44+
"storybook": "storybook dev -p 6006",
4445
"test": "vitest run",
4546
"typecheck": "tsc --noEmit",
4647
"url": "run-p -l watch:ts watch:vite watch:url",
@@ -61,6 +62,12 @@
6162
},
6263
"devDependencies": {
6364
"@eslint/js": "9.24.0",
65+
"@storybook/addon-essentials": "8.6.12",
66+
"@storybook/addon-interactions": "8.6.12",
67+
"@storybook/blocks": "8.6.12",
68+
"@storybook/react": "8.6.12",
69+
"@storybook/react-vite": "8.6.12",
70+
"@storybook/test": "8.6.12",
6471
"@testing-library/react": "16.3.0",
6572
"@types/node": "22.14.0",
6673
"@types/react": "19.1.0",
@@ -71,13 +78,20 @@
7178
"eslint-plugin-react": "7.37.5",
7279
"eslint-plugin-react-hooks": "5.2.0",
7380
"eslint-plugin-react-refresh": "0.4.19",
81+
"eslint-plugin-storybook": "0.12.0",
7482
"globals": "16.0.0",
7583
"jsdom": "26.0.0",
7684
"nodemon": "3.1.9",
7785
"npm-run-all": "4.1.5",
86+
"storybook": "8.6.12",
7887
"typescript": "5.8.3",
7988
"typescript-eslint": "8.29.1",
8089
"vite": "6.2.5",
8190
"vitest": "3.1.1"
91+
},
92+
"eslintConfig": {
93+
"extends": [
94+
"plugin:storybook/recommended"
95+
]
8296
}
8397
}

src/components/Cell/Cell.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import Breadcrumb from '../Breadcrumb/Breadcrumb.js'
77
import Layout from '../Layout/Layout.js'
88

99
interface CellProps {
10-
source: FileSource;
11-
row: number;
12-
col: number;
10+
source: FileSource
11+
row: number
12+
col: number
1313
}
1414

1515
/**
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
.dropdown {
2+
display: inline-block;
3+
position: relative;
4+
text-overflow: ellipsis;
5+
user-select: none;
6+
white-space: nowrap;
7+
}
8+
9+
.dropdownButton,
10+
.dropdownButton:active,
11+
.dropdownButton:focus,
12+
.dropdownButton:hover {
13+
align-items: center;
14+
background: inherit;
15+
border: none;
16+
color: inherit;
17+
cursor: pointer;
18+
display: flex;
19+
font-size: initial;
20+
overflow-x: hidden;
21+
padding: 0;
22+
}
23+
.dropdownButton:active,
24+
.dropdownButton:focus,
25+
.dropdownButton:hover {
26+
color: #113;
27+
}
28+
29+
/* caret */
30+
.dropdownButton::before {
31+
content: "\25bc";
32+
display: inline-block;
33+
font-size: 10px;
34+
margin-right: 4px;
35+
transform: rotate(-90deg);
36+
transition: transform 0.1s;
37+
}
38+
.dropdown:has([aria-expanded="true"]) .dropdownButton::before {
39+
transform: rotate(0deg);
40+
}
41+
42+
/* alignment */
43+
.dropdownLeft .dropdownContent {
44+
left: 0;
45+
}
46+
47+
.dropdownContent {
48+
background-color: #eee;
49+
position: absolute;
50+
right: 0;
51+
border-radius: 6px;
52+
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
53+
display: flex;
54+
flex-direction: column;
55+
max-height: 0;
56+
max-width: 300px;
57+
min-width: 160px;
58+
transition: max-height 0.1s ease-out;
59+
overflow-y: hidden;
60+
z-index: 20;
61+
}
62+
63+
.dropdown:has([aria-expanded="true"]) .dropdownContent {
64+
max-height: 170px;
65+
overflow-y: auto;
66+
}
67+
68+
.dropdownContent > * {
69+
display: block;
70+
}
71+
72+
.dropdownContent a,
73+
.dropdownContent button {
74+
background: none;
75+
border: none;
76+
border-radius: 0;
77+
color: inherit;
78+
flex-shrink: 0;
79+
font-size: 12px;
80+
overflow: hidden;
81+
text-overflow: ellipsis;
82+
padding: 8px 16px;
83+
text-align: left;
84+
text-decoration: none;
85+
width: 100%;
86+
}
87+
.dropdownContent a:active,
88+
.dropdownContent a:focus,
89+
.dropdownContent a:hover,
90+
.dropdownContent button:active,
91+
.dropdownContent button:focus,
92+
.dropdownContent button:hover {
93+
background-color: rgba(31, 30, 33, 0.1);
94+
}
95+
.dropdownContent input {
96+
margin: 4px 8px;
97+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { Meta, StoryObj } from '@storybook/react'
2+
import Dropdown from './Dropdown.js'
3+
4+
const meta: Meta<typeof Dropdown> = {
5+
component: Dropdown,
6+
}
7+
export default meta
8+
type Story = StoryObj<typeof Dropdown>;
9+
export const Default: Story = {
10+
args: {
11+
label: 'Menu',
12+
children: <>
13+
<button>Item 1</button>
14+
<button>Item 2</button>
15+
<button>Item 3</button>
16+
</>,
17+
},
18+
}
19+
20+
export const LeftAlign: Story = {
21+
args: {
22+
label: 'Menu',
23+
align: 'left',
24+
children: <>
25+
<button>Item 1</button>
26+
<button>Item 2</button>
27+
<button>Item 3</button>
28+
</>,
29+
},
30+
}
31+
32+
export const RightAlign: Story = {
33+
args: {
34+
label: 'Very long label for the menu',
35+
align: 'right',
36+
children: <>
37+
<button>Item 1</button>
38+
<button>Item 2</button>
39+
<button>Item 3</button>
40+
</>,
41+
},
42+
}

0 commit comments

Comments
 (0)