Skip to content

Commit 253c151

Browse files
wip
1 parent 0f759bf commit 253c151

File tree

23 files changed

+2271
-10
lines changed

23 files changed

+2271
-10
lines changed

eslint.config.mjs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { fixupConfigRules } from "@eslint/compat"
2+
import js from "@eslint/js"
3+
import reactHooks from "eslint-plugin-react-hooks"
4+
import reactJsx from "eslint-plugin-react/configs/jsx-runtime.js"
5+
import react from "eslint-plugin-react/configs/recommended.js"
6+
// import unusedImports from "eslint-plugin-unused-imports"
7+
// import prettier from "eslint-config-prettier/flat"
8+
import globals from "globals"
9+
10+
export default [
11+
{
12+
files: [`**/*.{js,jsx,mjs}`],
13+
languageOptions: {
14+
globals: globals.browser,
15+
ecmaVersion: 2021,
16+
sourceType: `module`,
17+
parserOptions: {
18+
ecmaFeatures: {
19+
jsx: true,
20+
},
21+
},
22+
},
23+
},
24+
js.configs.recommended,
25+
...fixupConfigRules([
26+
{
27+
...react,
28+
settings: {
29+
react: { version: `detect` },
30+
},
31+
},
32+
reactJsx,
33+
]),
34+
{
35+
files: [`**/*.{js,jsx,mjs}`],
36+
plugins: {
37+
"react-hooks": reactHooks,
38+
// "unused-imports": unusedImports,
39+
},
40+
rules: {
41+
...reactHooks.configs.recommended.rules,
42+
quotes: [`error`, `backtick`],
43+
"no-undef": `error`,
44+
"prefer-const": `warn`,
45+
"react/prop-types": `off`,
46+
// "unused-imports/no-unused-imports": `error`,
47+
// "unused-imports/no-unused-vars": `warn`,
48+
},
49+
},
50+
// prettier,
51+
{ ignores: [`dist/`] },
52+
]

package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"./css/*": "./src/css/*.css",
1414
"./scss/*": "./src/scss/*.scss",
1515
"./components/*": "./src/components/*/index.js",
16-
"./icons/*": "./src/icons/*.svg"
16+
"./icons/*": "./src/icons/*.svg",
17+
"./locales/*": "./src/locales/*.yml"
1718
},
1819
"publishConfig": {
1920
"registry": "https://npm.pkg.github.com"
@@ -37,7 +38,15 @@
3738
"react-dom": ">=19"
3839
},
3940
"devDependencies": {
41+
"@eslint/compat": "^1.2.9",
42+
"@eslint/js": "^9.26.0",
4043
"express": "^4.18.2",
44+
"eslint": "^9.26.0",
45+
"eslint-config-prettier": "^10.1.5",
46+
"eslint-plugin-react": "^7.37.5",
47+
"eslint-plugin-react-hooks": "^5.2.0",
48+
"eslint-plugin-unused-imports": "^4.1.4",
49+
"globals": "^16.1.0",
4150
"path": "^0.12.7",
4251
"prettier": "^3.5.3",
4352
"react": "19.1.0",
@@ -49,9 +58,11 @@
4958
"stylelint-prettier": "^5.0.2"
5059
},
5160
"dependencies": {
61+
"@floating-ui/react": "^0.27.9",
5262
"camelize-object-keys": "^3.0.0",
5363
"clsx": "^2.1.1",
5464
"query-string": "^9.1.2",
65+
"react-use": "^17.6.0",
5566
"startijenn-rem": "^1.1.1",
5667
"string-humanize": "^1.0.1"
5768
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useCallback, useState, useRef, useEffect } from "react"
2+
3+
import Button from "../button"
4+
import Floater from "../floater"
5+
6+
export default function ButtonAndFloater({
7+
children,
8+
ref,
9+
button = {},
10+
floater = {},
11+
}) {
12+
const [open, setOpen] = useState(false)
13+
const buttonRef = useRef()
14+
const floaterRef = useRef()
15+
16+
const buttonClick = useCallback(() => setOpen((v) => !v), [])
17+
18+
const buttonBlur = useCallback(() => {
19+
floaterRef.current?.focus()
20+
}, [])
21+
22+
const floaterBlur = useCallback(() => {
23+
buttonRef.current?.focus()
24+
}, [])
25+
26+
const floaterCloseRequest = useCallback(() => {
27+
setOpen(false)
28+
buttonRef.current?.focus()
29+
}, [])
30+
31+
const floaterElementSet = (el) => (floaterRef.current = el)
32+
33+
useEffect(() => {
34+
ref.close = floaterCloseRequest
35+
}, [floaterCloseRequest, ref])
36+
37+
return (
38+
<>
39+
<Button
40+
{...button}
41+
title={
42+
typeof button.title === `function` ? button.title(open) : button.title
43+
}
44+
ref={buttonRef}
45+
type="button"
46+
aria-pressed={open}
47+
onClick={buttonClick}
48+
onBlur={buttonBlur}
49+
/>
50+
51+
{open && (
52+
<Floater
53+
{...floater}
54+
referenceRef={buttonRef}
55+
onBlur={floaterBlur}
56+
onCloseRequest={floaterCloseRequest}
57+
onElementSet={floaterElementSet}
58+
>
59+
{children}
60+
</Floater>
61+
)}
62+
</>
63+
)
64+
}

src/components/floater/index.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useEffect, useCallback, useRef } from "react"
2+
import ReactDOM from "react-dom"
3+
import { useClickAway, useKey } from "react-use"
4+
import {
5+
useFloating,
6+
autoUpdate as autoUpdateFui,
7+
offset as offsetFui,
8+
} from "@floating-ui/react"
9+
import clsx from "clsx"
10+
11+
import style from "./index.module.css"
12+
13+
export default function Floater({
14+
children,
15+
referenceRef,
16+
className,
17+
classNameInner,
18+
placement,
19+
offset,
20+
onBlur,
21+
onCloseRequest,
22+
onElementSet,
23+
}) {
24+
const { refs, floatingStyles } = useFloating({
25+
placement,
26+
middleware: [offsetFui(offset)],
27+
whileElementsMounted: autoUpdateFui,
28+
})
29+
30+
const innerRef = useRef()
31+
32+
const innerBlur = useCallback(
33+
(e) => {
34+
if (!innerRef.current.contains(e.relatedTarget)) onBlur?.()
35+
},
36+
[onBlur]
37+
)
38+
39+
useClickAway(
40+
refs.floating,
41+
(e) => !referenceRef.current.contains(e.target) && onCloseRequest?.()
42+
)
43+
44+
useKey(`Escape`, (e) => {
45+
e.preventDefault()
46+
onCloseRequest?.()
47+
})
48+
49+
useEffect(() => {
50+
refs.setReference(referenceRef.current)
51+
}, [refs.setReference, refs, referenceRef])
52+
53+
useEffect(() => {
54+
innerRef.current?.focus()
55+
onElementSet?.(innerRef.current)
56+
}, [onElementSet])
57+
58+
return ReactDOM.createPortal(
59+
<div
60+
ref={refs.setFloating}
61+
style={floatingStyles}
62+
className={clsx(style.container, className)}
63+
>
64+
<div
65+
ref={innerRef}
66+
className={clsx(style.inner, classNameInner)}
67+
tabIndex="0"
68+
onBlur={innerBlur}
69+
data-placement={placement}
70+
>
71+
{children}
72+
</div>
73+
74+
<div className="hidden-visually" tabIndex="0" />
75+
</div>,
76+
document.querySelector(`body`)
77+
)
78+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.container {
2+
/* . */
3+
}
4+
5+
.inner {
6+
/* . */
7+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import clsx from "clsx"
2+
3+
import CheckIcon from "../../icons/check.svg?react"
4+
import style from "./index.module.css"
5+
6+
export default function SelectableMenu({ children = [], onToggle, ...rest }) {
7+
if (!children.length) return null
8+
9+
const buttonClick = (e) => onToggle?.(e.currentTarget.dataset.style)
10+
11+
return (
12+
<div {...rest} className={clsx(style.container, rest.className)}>
13+
{children.map(
14+
({
15+
id: itemId,
16+
selected: itemSelected,
17+
text: itemText,
18+
Icon: ItemIcon,
19+
...itemRest
20+
}) => (
21+
<button
22+
key={itemId}
23+
{...itemRest}
24+
className={style.button}
25+
type="button"
26+
role="option"
27+
aria-selected={itemSelected}
28+
data-style={itemId}
29+
onClick={buttonClick}
30+
>
31+
{ItemIcon && <ItemIcon />}
32+
33+
<span>{itemText}</span>
34+
35+
<CheckIcon className={style.check} aria-hidden="true" />
36+
</button>
37+
)
38+
)}
39+
</div>
40+
)
41+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
.container {
2+
display: flex;
3+
flex-direction: column;
4+
gap: emify(4);
5+
}
6+
7+
.button {
8+
/* @mixin hover; */
9+
10+
padding: emify(6 10);
11+
display: flex;
12+
gap: emify(10);
13+
border-radius: var(--br-small);
14+
font-weight: var(--fw-primary-medium);
15+
font-size: emify(13);
16+
17+
&[aria-selected="true"] {
18+
background-color: var(--color-light-gray);
19+
}
20+
}
21+
22+
.check {
23+
width: emify(23);
24+
height: emify(23);
25+
margin-left: auto;
26+
27+
.button:not([aria-selected="true"]) & {
28+
visibility: hidden;
29+
}
30+
}

src/css/keyframes.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@
8383
}
8484
}
8585

86+
@keyframes slide-in-left {
87+
0% {
88+
translate: remify(10) 0;
89+
}
90+
91+
100% {
92+
translate: 0;
93+
}
94+
}
95+
8696
@keyframes slide-out-down {
8797
0% {
8898
translate: 0;

src/icons/arrows-expand.svg

Lines changed: 3 additions & 0 deletions
Loading

src/icons/expand.svg

Lines changed: 4 additions & 4 deletions
Loading

0 commit comments

Comments
 (0)