@lism-css/ui 0.0.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -0
- package/package.json +75 -0
- package/src/components/Accordion/AccIcon.jsx +6 -0
- package/src/components/Accordion/Accordion.jsx +60 -0
- package/src/components/Accordion/_style.scss +54 -0
- package/src/components/Accordion/astro/AccBody.astro +13 -0
- package/src/components/Accordion/astro/AccHeader.astro +13 -0
- package/src/components/Accordion/astro/AccHeaderLabel.astro +12 -0
- package/src/components/Accordion/astro/AccIcon.astro +18 -0
- package/src/components/Accordion/astro/AccLabel.astro +11 -0
- package/src/components/Accordion/astro/Accordion.astro +21 -0
- package/src/components/Accordion/astro/__setEvent.js +2 -0
- package/src/components/Accordion/astro/index.js +8 -0
- package/src/components/Accordion/getProps.js +32 -0
- package/src/components/Accordion/index.js +4 -0
- package/src/components/Accordion/script.js +5 -0
- package/src/components/Accordion/setAccordion.js +107 -0
- package/src/components/Modal/Body.jsx +10 -0
- package/src/components/Modal/CloseBtn.jsx +20 -0
- package/src/components/Modal/Inner.jsx +6 -0
- package/src/components/Modal/Modal.jsx +20 -0
- package/src/components/Modal/OpenBtn.jsx +10 -0
- package/src/components/Modal/getProps.js +30 -0
- package/src/components/Modal/index.js +7 -0
- package/src/components/Modal/script.js +5 -0
- package/src/components/Modal/setModal.ts +107 -0
- package/src/components/Tabs/Tab.jsx +18 -0
- package/src/components/Tabs/TabItem.jsx +5 -0
- package/src/components/Tabs/TabList.jsx +6 -0
- package/src/components/Tabs/TabPanel.jsx +8 -0
- package/src/components/Tabs/Tabs.jsx +60 -0
- package/src/components/Tabs/getProps.js +8 -0
- package/src/components/Tabs/index.js +7 -0
- package/src/components/Tabs/script.js +8 -0
- package/src/components/Tabs/setEvent.js +87 -0
- package/src/components/__contexts.js +4 -0
- package/src/components/astro.ts +3 -0
- package/src/components/react.ts +3 -0
package/README.md
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lism-css/ui",
|
|
3
|
+
"version": "0.0.0-dev.1",
|
|
4
|
+
"description": "A layout-first CSS framework for websites.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "ddryo",
|
|
7
|
+
"url": "https://github.com/ddryo"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"css-framework",
|
|
12
|
+
"astro-component",
|
|
13
|
+
"react-component"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "vite",
|
|
17
|
+
"build": "vite build && pnpm build:css",
|
|
18
|
+
"build:vite": "vite build",
|
|
19
|
+
"build:css": "node bin/script-build-css.js",
|
|
20
|
+
"lint": "pnpm lint:style",
|
|
21
|
+
"lint:style": "stylelint '**/*.{css,scss}'",
|
|
22
|
+
"preview": "vite preview"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"lism-css": "./bin/cli.mjs"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"src"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"main": "./dist/index.js",
|
|
33
|
+
"exports": {
|
|
34
|
+
"./react": {
|
|
35
|
+
"import": "./src/components/react.js",
|
|
36
|
+
"types": "./dist/components/index.d.ts"
|
|
37
|
+
},
|
|
38
|
+
"./astro": {
|
|
39
|
+
"import": "./src/components/astro.js",
|
|
40
|
+
"types": "./dist/components/index.d.ts"
|
|
41
|
+
},
|
|
42
|
+
"./*.css": "./dist/css/*.css"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://www.lism.style",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/lism-css/lism-css/tree/main/packages/lism-css"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/lism-css/lism-css/issues"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"lism-css": "workspace:*"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@babel/cli": "^7.27.2",
|
|
57
|
+
"@babel/core": "^7.27.3",
|
|
58
|
+
"@babel/preset-env": "^7.27.2",
|
|
59
|
+
"@babel/preset-react": "^7.27.1",
|
|
60
|
+
"@rollup/plugin-babel": "^6.0.4",
|
|
61
|
+
"@vitejs/plugin-react-swc": "^3.10.0",
|
|
62
|
+
"glob": "^11.0.2",
|
|
63
|
+
"rollup": "^4.41.1",
|
|
64
|
+
"typescript": "~5.8.3",
|
|
65
|
+
"unplugin-dts": "1.0.0-beta.6",
|
|
66
|
+
"vite": "^6.3.5"
|
|
67
|
+
},
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"@types/react": "*",
|
|
70
|
+
"@types/react-dom": "*",
|
|
71
|
+
"react": "^18 || ^19",
|
|
72
|
+
"react-dom": "^18 || ^19"
|
|
73
|
+
},
|
|
74
|
+
"sideEffects": false
|
|
75
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Lism, Icon } from 'lism-css/react';
|
|
2
|
+
import { getAccIconProps } from './getProps';
|
|
3
|
+
|
|
4
|
+
export default function AccIcon({ icon = 'caret-down', viewBox, children = null, ...props }) {
|
|
5
|
+
return <Lism {...getAccIconProps(props)}>{children || <Icon viewBox={viewBox} icon={icon} />}</Lism>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import getLismProps from 'lism-css/lib/getLismProps';
|
|
3
|
+
import { Lism } from 'lism-css/react';
|
|
4
|
+
import { getAccProps, defaultProps } from './getProps';
|
|
5
|
+
import { setEvent } from './setAccordion';
|
|
6
|
+
import AccIcon from './AccIcon';
|
|
7
|
+
|
|
8
|
+
// import { AccContext } from './context';
|
|
9
|
+
|
|
10
|
+
// duration: [s]
|
|
11
|
+
export function Accordion({ children, ...props }) {
|
|
12
|
+
const ref = React.useRef(null);
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
if (!ref.current) return;
|
|
16
|
+
return setEvent(ref.current);
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
const lismProps = getLismProps(getAccProps(props));
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<details ref={ref} {...lismProps}>
|
|
23
|
+
{/* <AccContext.Provider value={deliverState}>{children}</AccContext.Provider> */}
|
|
24
|
+
{children}
|
|
25
|
+
</details>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function Header({ children, ...props }) {
|
|
30
|
+
return (
|
|
31
|
+
<Lism tag='summary' {...defaultProps.header} {...props}>
|
|
32
|
+
{children}
|
|
33
|
+
</Lism>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
export function Label({ children, ...props }) {
|
|
37
|
+
return (
|
|
38
|
+
<Lism {...defaultProps.label} {...props}>
|
|
39
|
+
{children}
|
|
40
|
+
</Lism>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function Body({ children, flow, innerProps, ...props }) {
|
|
45
|
+
return (
|
|
46
|
+
<Lism {...defaultProps.body} {...props}>
|
|
47
|
+
<Lism layout='flow' flow={flow} {...defaultProps.inner} {...innerProps}>
|
|
48
|
+
{children}
|
|
49
|
+
</Lism>
|
|
50
|
+
</Lism>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
export function HeaderLabel({ children, ...props }) {
|
|
54
|
+
return (
|
|
55
|
+
<Header {...props}>
|
|
56
|
+
<Label>{children}</Label>
|
|
57
|
+
<AccIcon />
|
|
58
|
+
</Header>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
.d--accordion {
|
|
2
|
+
--duration: var(--acc-duration, 0.4s);
|
|
3
|
+
&[data-opened] {
|
|
4
|
+
--_notOpen: ;
|
|
5
|
+
}
|
|
6
|
+
&:not([data-opened]) {
|
|
7
|
+
--_isOpen: ;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.d--accordion_header {
|
|
12
|
+
display: grid;
|
|
13
|
+
grid: auto / 1fr auto;
|
|
14
|
+
gap: 0.5em;
|
|
15
|
+
align-items: center;
|
|
16
|
+
outline-offset: -1px; // overflow:clip|hidden; で見えなくなってしまうのを防ぐ
|
|
17
|
+
|
|
18
|
+
/* Safariで表示されるデフォルトの三角形アイコンを消す */
|
|
19
|
+
&::-webkit-details-marker {
|
|
20
|
+
display: none;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.d--accordion_body {
|
|
25
|
+
display: grid;
|
|
26
|
+
grid: 1fr / auto;
|
|
27
|
+
transition-property: margin-block, padding-block, opacity, grid-template;
|
|
28
|
+
transition-duration: var(--duration);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ※ 正常な animation には必須
|
|
32
|
+
.d--accordion_inner {
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 閉じている時
|
|
37
|
+
.d--accordion:not([data-opened]) {
|
|
38
|
+
> .d--accordion_body {
|
|
39
|
+
grid: 0fr / auto;
|
|
40
|
+
padding-block: 0 !important;
|
|
41
|
+
margin-block: 0 !important;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// アコーディオンブロックのネスト時、別のアイコンタイプにすると表示が崩れるがそこまでは考慮しない。
|
|
46
|
+
.d--accordion_icon {
|
|
47
|
+
display: grid;
|
|
48
|
+
|
|
49
|
+
// __icon 自体にborderつけたりすると回転が見えてしまうので、 icon自体を回転させる。
|
|
50
|
+
> .a--icon {
|
|
51
|
+
transition-duration: var(--duration);
|
|
52
|
+
rotate: var(--_isOpen, -180deg);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
// import type { LismProps } from 'lism-css/types';
|
|
3
|
+
import { Lism } from 'lism-css/react';
|
|
4
|
+
import { defaultProps } from '../getProps';
|
|
5
|
+
|
|
6
|
+
const { flow, innerProps, ...props } = Astro.props || {};
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<Lism {...defaultProps.body} {...props}>
|
|
10
|
+
<Lism layout='flow' flow={flow} {...defaultProps.inner} {...innerProps}>
|
|
11
|
+
<slot />
|
|
12
|
+
</Lism>
|
|
13
|
+
</Lism>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
// import type { LismProps } from 'lism-css/types';
|
|
3
|
+
import { Lism } from 'lism-css/react';
|
|
4
|
+
import { defaultProps } from '../getProps';
|
|
5
|
+
|
|
6
|
+
// interface Props extends LismProps {}
|
|
7
|
+
|
|
8
|
+
const props = Astro.props || {};
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<Lism tag='summary' {...defaultProps.header} {...props}>
|
|
12
|
+
<slot />
|
|
13
|
+
</Lism>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Lism } from 'lism-css/react';
|
|
3
|
+
import { Icon } from 'lism-css/react';
|
|
4
|
+
import { getAccIconProps } from '../getProps';
|
|
5
|
+
|
|
6
|
+
// Propsの定義
|
|
7
|
+
// interface Props extends LismProps {
|
|
8
|
+
// icon?: string;
|
|
9
|
+
// size?: string;
|
|
10
|
+
// iconProps?: Object;
|
|
11
|
+
// }
|
|
12
|
+
|
|
13
|
+
const { viewBox, icon = 'caret-down', ...props } = Astro.props || {};
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<Lism {...getAccIconProps(props)}>
|
|
17
|
+
{Astro.slots.has('default') ? <slot /> : <Icon viewBox={viewBox} icon={icon} />}
|
|
18
|
+
</Lism>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
// import type { LismProps } from 'lism-css/types';
|
|
3
|
+
import getLismProps from 'lism-css/lib/getLismProps';
|
|
4
|
+
import { getAccProps } from '../getProps';
|
|
5
|
+
|
|
6
|
+
// Propsの定義
|
|
7
|
+
// interface Props extends LismProps {
|
|
8
|
+
// duration?: string | number;
|
|
9
|
+
// }
|
|
10
|
+
const props = Astro.props || {};
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<details {...getLismProps(getAccProps(props))}>
|
|
14
|
+
<slot />
|
|
15
|
+
</details>
|
|
16
|
+
|
|
17
|
+
<script>
|
|
18
|
+
// import setEvent from './setEvent';
|
|
19
|
+
import setAccordion from '../setAccordion';
|
|
20
|
+
setAccordion();
|
|
21
|
+
</script>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import Root from './Accordion.astro';
|
|
2
|
+
import Header from './AccHeader.astro';
|
|
3
|
+
import Label from './AccLabel.astro';
|
|
4
|
+
import Icon from './AccIcon.astro';
|
|
5
|
+
import Body from './AccBody.astro';
|
|
6
|
+
import HeaderLabel from './AccHeaderLabel.astro';
|
|
7
|
+
|
|
8
|
+
export default { Root, Header, HeaderLabel, Body, Icon, Label };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import atts from 'lism-css/lib/helper/atts';
|
|
2
|
+
|
|
3
|
+
// duration: [s]
|
|
4
|
+
export function getAccProps({ lismClass, ...props }) {
|
|
5
|
+
props.lismClass = atts(lismClass, 'd--accordion');
|
|
6
|
+
return props;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getAccIconProps({ isTrigger, ...props }) {
|
|
10
|
+
const defaultProps = {
|
|
11
|
+
lismClass: 'd--accordion_icon',
|
|
12
|
+
tag: 'span',
|
|
13
|
+
};
|
|
14
|
+
// isTrigger なら、buttun にする
|
|
15
|
+
if (isTrigger) {
|
|
16
|
+
defaultProps.tag = 'button';
|
|
17
|
+
props['data-role'] = 'trigger';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { ...defaultProps, ...props };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const defaultProps = {
|
|
24
|
+
header: { lismClass: 'd--accordion_header' },
|
|
25
|
+
label: { lismClass: 'd--accordion_label', tag: 'span' },
|
|
26
|
+
body: {
|
|
27
|
+
lismClass: 'd--accordion_body',
|
|
28
|
+
},
|
|
29
|
+
inner: {
|
|
30
|
+
lismClass: 'd--accordion_inner',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// open 属性付与からクラスの付与まで、ほんの少しだけ遅らせた方が動作が安定する
|
|
2
|
+
const DELAY = 5;
|
|
3
|
+
|
|
4
|
+
// モーダルのアニメーションが完了するのを待つ.
|
|
5
|
+
const waitAnimation = (element) => {
|
|
6
|
+
return Promise.all(element.getAnimations().map((a) => a.finished));
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// animationTime: [ms]
|
|
10
|
+
const clickedEvent = async (details, force = false) => {
|
|
11
|
+
// アニメーション中かどうか
|
|
12
|
+
if (details.dataset.animating && !force) return;
|
|
13
|
+
details.dataset.animating = '1';
|
|
14
|
+
|
|
15
|
+
const body = details.querySelector('.d--accordion_body');
|
|
16
|
+
|
|
17
|
+
// オープン / クローズ 処理
|
|
18
|
+
if (!details.open) {
|
|
19
|
+
details.open = true;
|
|
20
|
+
// 少しだけ遅らせた方が動作が安定する
|
|
21
|
+
setTimeout(async () => {
|
|
22
|
+
details.setAttribute('data-opened', ''); // クラスの追加
|
|
23
|
+
|
|
24
|
+
// アニメーション完了後に dataset を除去。
|
|
25
|
+
await waitAnimation(body);
|
|
26
|
+
delete details.dataset.animating;
|
|
27
|
+
}, DELAY);
|
|
28
|
+
} else if (details.open) {
|
|
29
|
+
details.removeAttribute('data-opened'); // クラスを削除
|
|
30
|
+
|
|
31
|
+
// アニメーション完了後に open属性 を除去。
|
|
32
|
+
await waitAnimation(body);
|
|
33
|
+
|
|
34
|
+
delete details.dataset.animating;
|
|
35
|
+
details.open = false;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const toggleEvent = (e, details) => {
|
|
40
|
+
// e.preventDefault();
|
|
41
|
+
// console.log('toggleEvent', e.target, e.currentTarget);
|
|
42
|
+
|
|
43
|
+
const hasOpen = details.open;
|
|
44
|
+
const hasOpenedClass = details.hasAttribute('data-opened');
|
|
45
|
+
|
|
46
|
+
// open はセットされたのに data-opened がついてない時
|
|
47
|
+
if (hasOpen && !hasOpenedClass) {
|
|
48
|
+
details.setAttribute('data-opened', '');
|
|
49
|
+
}
|
|
50
|
+
// open は削除されたのに data-opened がまだついている時
|
|
51
|
+
if (!hasOpen && hasOpenedClass) {
|
|
52
|
+
details.removeAttribute('data-opened');
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const setEvent = (currentRef) => {
|
|
57
|
+
const details = currentRef;
|
|
58
|
+
// トリガーが明示的に指定されていない場合は、<summary> 要素をトリガーとする
|
|
59
|
+
const clickBtn = details.querySelector(`[data-role="trigger"]`) || details.querySelector('summary');
|
|
60
|
+
|
|
61
|
+
if (!clickBtn) return;
|
|
62
|
+
|
|
63
|
+
// 複数展開を許可するかどうかを、親要素の [data-accordion-multiple] でチェック.
|
|
64
|
+
let allowMultiple = false;
|
|
65
|
+
const parent = details.parentNode;
|
|
66
|
+
if (null != parent) {
|
|
67
|
+
const dataMultiple = parent.dataset.accordionMultiple;
|
|
68
|
+
allowMultiple = 'disallow' !== dataMultiple;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const _clickedEvent = (e) => {
|
|
72
|
+
// すぐに open 属性が切り替わらないようにする
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
|
|
75
|
+
// 複数展開が禁止されている場合、(開く処理の直前で)他の開いているアイテムがあれば閉じる
|
|
76
|
+
if (!allowMultiple && !details.open) {
|
|
77
|
+
const openedItem = parent.querySelector(`[data-opened]`);
|
|
78
|
+
if (null != openedItem) clickedEvent(openedItem, true);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 自身のクリック処理
|
|
82
|
+
clickedEvent(details);
|
|
83
|
+
};
|
|
84
|
+
const _toggleEvent = (e) => {
|
|
85
|
+
toggleEvent(e, details);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// <summary> 'click' イベント
|
|
89
|
+
clickBtn.addEventListener('click', _clickedEvent);
|
|
90
|
+
|
|
91
|
+
// <details> の'toggle' イベントで、ページ内検索時にも開閉されるようにする
|
|
92
|
+
details.addEventListener('toggle', _toggleEvent);
|
|
93
|
+
|
|
94
|
+
// useEffectでアンマウントされた時にremoveEventListenerしないと2重でイベントが登録してしまう。
|
|
95
|
+
return () => {
|
|
96
|
+
clickBtn.removeEventListener('click', _clickedEvent);
|
|
97
|
+
details.removeEventListener('toggle', _toggleEvent);
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const setAccordion = () => {
|
|
102
|
+
const detailsAll = document.querySelectorAll('.d--accordion');
|
|
103
|
+
detailsAll.forEach((details) => {
|
|
104
|
+
setEvent(details);
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
export default setAccordion;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Lism } from '../Lism';
|
|
2
|
+
import { Icon } from '../atomic/Icon';
|
|
3
|
+
import { defaultProps } from './getProps';
|
|
4
|
+
// duration: [s]
|
|
5
|
+
export default function CloseBtn({ children, modalId = '', icon, srText = 'Close', ...props }) {
|
|
6
|
+
// const lismProps = getLismProps(getAccProps(props));
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<Lism data-modal-close={modalId} {...defaultProps.closeBtn} {...props}>
|
|
10
|
+
{children ? (
|
|
11
|
+
children
|
|
12
|
+
) : (
|
|
13
|
+
<>
|
|
14
|
+
<Icon icon={icon || 'x'} />
|
|
15
|
+
<span className='u-hidden'>{srText || 'Close'}</span>
|
|
16
|
+
</>
|
|
17
|
+
)}
|
|
18
|
+
</Lism>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Lism } from '../Lism';
|
|
3
|
+
import { setEvent } from './setModal';
|
|
4
|
+
import { getProps } from './getProps';
|
|
5
|
+
|
|
6
|
+
// duration: [s]
|
|
7
|
+
const Modal = ({ children, ...props }) => {
|
|
8
|
+
const ref = React.useRef(null);
|
|
9
|
+
React.useEffect(() => {
|
|
10
|
+
if (!ref?.current) return;
|
|
11
|
+
return setEvent(ref?.current);
|
|
12
|
+
}, [ref]);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Lism forwardedRef={ref} {...getProps(props)}>
|
|
16
|
+
{children}
|
|
17
|
+
</Lism>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
export default Modal;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Lism } from '../Lism';
|
|
2
|
+
import { defaultProps } from './getProps';
|
|
3
|
+
// duration: [s]
|
|
4
|
+
export default function OpenBtn({ children, modalId = '', ...props }) {
|
|
5
|
+
return (
|
|
6
|
+
<Lism data-modal-open={modalId} {...defaultProps.openBtn} {...props}>
|
|
7
|
+
{children}
|
|
8
|
+
</Lism>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import atts from '../../lib/helper/atts';
|
|
2
|
+
|
|
3
|
+
export function getProps({ lismClass = '', duration, style = {}, ...props }) {
|
|
4
|
+
const theProps = {
|
|
5
|
+
lismClass: atts(lismClass, 'd--modal'),
|
|
6
|
+
setPlain: true,
|
|
7
|
+
};
|
|
8
|
+
if (duration) {
|
|
9
|
+
style['--duration'] = duration;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return { tag: 'dialog', ...theProps, style, ...props };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getInnerProps({ lismClass = '', offset, style = {}, ...props }) {
|
|
16
|
+
if (offset) {
|
|
17
|
+
style['--offset'] = offset;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
lismClass: atts(lismClass, 'd--modal_inner'),
|
|
21
|
+
style,
|
|
22
|
+
...props,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const defaultProps = {
|
|
27
|
+
body: { lismClass: 'd--modal_body' },
|
|
28
|
+
closeBtn: { tag: 'button', setPlain: true, hov: 'o', d: 'in-flex' },
|
|
29
|
+
openBtn: { tag: 'button', setPlain: true, hov: 'o', d: 'in-flex' },
|
|
30
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// オープンした時のトリガー要素を記憶する(focusを戻す)
|
|
2
|
+
let THE_TRIGGER: HTMLElement | null = null;
|
|
3
|
+
|
|
4
|
+
// モーダルのアニメーションが完了するのを待つ.
|
|
5
|
+
const waitAnimation = async (element: HTMLElement): Promise<void> => {
|
|
6
|
+
// (子要素も取得する場合は { subtree: true } を指定)
|
|
7
|
+
const animations = element.getAnimations();
|
|
8
|
+
|
|
9
|
+
if (animations.length > 0) {
|
|
10
|
+
// allSettled を使うことで、キャンセルされた場合もrejectせずに完了扱いになる
|
|
11
|
+
await Promise.allSettled(animations.map((a) => a.finished));
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
async function modalOpen(modal: HTMLDialogElement): Promise<void> {
|
|
16
|
+
// すでに open & data-is-open が付いていれば何もしない(連打防止)
|
|
17
|
+
if (modal.open && modal.dataset.isOpen) return;
|
|
18
|
+
|
|
19
|
+
// showModal() でモーダルを開く( open 属性の付与)
|
|
20
|
+
modal.showModal();
|
|
21
|
+
|
|
22
|
+
// 次フレームで data-is-open を付与(CSS側でフェードインアニメーション開始)
|
|
23
|
+
requestAnimationFrame(() => {
|
|
24
|
+
modal.dataset.isOpen = '1';
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function modalClose(modal: HTMLDialogElement): Promise<void> {
|
|
28
|
+
// すでに閉じている場合は何もしない
|
|
29
|
+
if (undefined === modal.dataset.isOpen) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// data-open 属性を削除(CSS側でフェードアウトアニメーション開始)
|
|
34
|
+
delete modal.dataset.isOpen;
|
|
35
|
+
|
|
36
|
+
// アニメーション完了を待機
|
|
37
|
+
await waitAnimation(modal);
|
|
38
|
+
|
|
39
|
+
// アニメーション終了後、dialog を閉じる(open属性の削除)
|
|
40
|
+
modal.close();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function setEvent(modal: HTMLDialogElement): void {
|
|
44
|
+
// modalがない、またはidがない場合は処理を終了
|
|
45
|
+
if (!modal || !modal.id) return;
|
|
46
|
+
|
|
47
|
+
// モーダルを開くトリガーと閉じるトリガーを取得
|
|
48
|
+
const openTriggers: NodeListOf<HTMLElement> = document.querySelectorAll(`[data-modal-open="${modal.id}"]`);
|
|
49
|
+
const closeTriggers: NodeListOf<HTMLElement> = modal.querySelectorAll(`[data-modal-close="${modal.id}"]`);
|
|
50
|
+
|
|
51
|
+
// 自身にクローズイベントを追加(余白部分をクリックしても閉じるように)
|
|
52
|
+
modal.addEventListener('click', (e) => {
|
|
53
|
+
if (e.target === modal) {
|
|
54
|
+
modalClose(modal);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// closeボタンにイベント登録
|
|
59
|
+
modal.addEventListener('close', (e) => {
|
|
60
|
+
if (THE_TRIGGER) {
|
|
61
|
+
// THE_TRIGGER.focus(); // showModal()ではフォーカス戻す必要なし
|
|
62
|
+
|
|
63
|
+
// オープンボタンのdata属性削除
|
|
64
|
+
delete THE_TRIGGER.dataset.targetOpened;
|
|
65
|
+
THE_TRIGGER = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// モーダルを閉じる
|
|
69
|
+
modalClose(modal);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// openボタンにイベント登録追加
|
|
73
|
+
openTriggers.forEach((trigger) => {
|
|
74
|
+
trigger?.addEventListener('click', (e) => {
|
|
75
|
+
// button側にもdata属性付与
|
|
76
|
+
trigger.dataset.targetOpened = '1';
|
|
77
|
+
THE_TRIGGER = trigger; // close() 時にdata属性削除するために記憶
|
|
78
|
+
|
|
79
|
+
// モーダルを開く
|
|
80
|
+
modalOpen(modal);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 閉じるトリガーにイベントリスナーを追加
|
|
85
|
+
closeTriggers.forEach((trigger) => {
|
|
86
|
+
trigger?.addEventListener('click', (e) => {
|
|
87
|
+
modalClose(modal);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* ESCキーで閉じた時もアニメーションを実行する処理
|
|
93
|
+
*/
|
|
94
|
+
modal.addEventListener('cancel', (e) => {
|
|
95
|
+
e.preventDefault(); // デフォルトの即時 close() を防ぐ
|
|
96
|
+
modalClose(modal); // 自分で用意したクローズ処理
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const setModal = () => {
|
|
101
|
+
const modals = document.querySelectorAll('.d--modal');
|
|
102
|
+
|
|
103
|
+
modals?.forEach((target) => {
|
|
104
|
+
setEvent(target as HTMLDialogElement);
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
export default setModal;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// import React from 'react';
|
|
2
|
+
import { Lism } from '../Lism';
|
|
3
|
+
|
|
4
|
+
export default function Tab({ tabId = 'tab', index = 0, isActive = false, ...props }) {
|
|
5
|
+
const controlId = `${tabId}-${index}`;
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<Lism
|
|
9
|
+
tag='button'
|
|
10
|
+
lismClass='d--tabs_tab'
|
|
11
|
+
setPlain
|
|
12
|
+
role='tab'
|
|
13
|
+
aria-controls={controlId}
|
|
14
|
+
aria-selected={isActive ? 'true' : 'false'}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// import React from 'react';
|
|
2
|
+
import { Lism } from '../Lism';
|
|
3
|
+
|
|
4
|
+
export default function TabPanel({ tabId = 'tab', isActive = false, index = 0, ...props }) {
|
|
5
|
+
const controlId = `${tabId}-${index}`;
|
|
6
|
+
|
|
7
|
+
return <Lism id={controlId} role='tabpanel' aria-hidden={isActive ? 'false' : 'true'} lismClass='d--tabs_panel' {...props} />;
|
|
8
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Lism } from '../Lism';
|
|
3
|
+
import Tab from './Tab';
|
|
4
|
+
import TabItem from './TabItem';
|
|
5
|
+
import TabList from './TabList';
|
|
6
|
+
import TabPanel from './TabPanel';
|
|
7
|
+
import getTabsProps from './getProps';
|
|
8
|
+
// import { TabContext } from './context';
|
|
9
|
+
|
|
10
|
+
export default function Tabs({ tabId = '', defaultIndex = 1, listProps = {}, children, ...props }) {
|
|
11
|
+
const [activeIndex, setActiveIndex] = React.useState(defaultIndex);
|
|
12
|
+
const theTabId = tabId || React.useId();
|
|
13
|
+
const btns = [];
|
|
14
|
+
const panels = [];
|
|
15
|
+
|
|
16
|
+
// Tabs.Item の処理
|
|
17
|
+
React.Children.forEach(children, (child, index) => {
|
|
18
|
+
const tabIndex = index + 1; // 1 はじまり
|
|
19
|
+
// console.log('child.type', React.isValidElement(child), child.type);
|
|
20
|
+
|
|
21
|
+
if (React.isValidElement(child) && child.type === TabItem) {
|
|
22
|
+
React.Children.forEach(child.props.children, (nestedChild) => {
|
|
23
|
+
if (React.isValidElement(nestedChild)) {
|
|
24
|
+
if (nestedChild.type === Tab) {
|
|
25
|
+
const tabProps = nestedChild.props;
|
|
26
|
+
btns.push(
|
|
27
|
+
<Tab
|
|
28
|
+
{...tabProps}
|
|
29
|
+
tabId={theTabId}
|
|
30
|
+
index={tabIndex}
|
|
31
|
+
key={tabIndex}
|
|
32
|
+
isActive={tabIndex === activeIndex}
|
|
33
|
+
onClick={() => setActiveIndex(tabIndex)}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
} else if (nestedChild.type === TabPanel) {
|
|
37
|
+
const panelProps = nestedChild.props;
|
|
38
|
+
panels.push(
|
|
39
|
+
<TabPanel {...panelProps} tabId={theTabId} index={tabIndex} key={tabIndex} isActive={tabIndex === activeIndex} />
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Lism {...getTabsProps(props)}>
|
|
49
|
+
{btns.length === 0 ? (
|
|
50
|
+
// TabItemを使わず直接TabListなどを子要素に配置する場合
|
|
51
|
+
children
|
|
52
|
+
) : (
|
|
53
|
+
<>
|
|
54
|
+
<TabList {...listProps}>{btns}</TabList>
|
|
55
|
+
{panels}
|
|
56
|
+
</>
|
|
57
|
+
)}
|
|
58
|
+
</Lism>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { default as Root } from './Tabs';
|
|
2
|
+
import { default as Item } from './TabItem';
|
|
3
|
+
import { default as List } from './TabList';
|
|
4
|
+
import { default as Panel } from './TabPanel';
|
|
5
|
+
import { default as Tab } from './Tab';
|
|
6
|
+
|
|
7
|
+
export default { Root, List, Panel, Item, Tab };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* タブ
|
|
3
|
+
*/
|
|
4
|
+
function tabControl(e) {
|
|
5
|
+
e.preventDefault();
|
|
6
|
+
|
|
7
|
+
// クリックされたボタン要素
|
|
8
|
+
const clickedButton = e.currentTarget;
|
|
9
|
+
|
|
10
|
+
// クリックイベントがキー(Enter / space)によって呼び出されたかどうか
|
|
11
|
+
// const iskeyClick = 0 === e.clientX;
|
|
12
|
+
|
|
13
|
+
// マウスクリック時はフォーカスを外す
|
|
14
|
+
// if (!iskeyClick) {
|
|
15
|
+
// clickedButton.blur();
|
|
16
|
+
// }
|
|
17
|
+
|
|
18
|
+
// 属性の切り替え
|
|
19
|
+
toggleAriaData(clickedButton);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const toggleAriaData = (clickedButton) => {
|
|
23
|
+
// すでにオープンされているタブの場合はなにもしない
|
|
24
|
+
const isOpend = 'true' === clickedButton.getAttribute('aria-selected');
|
|
25
|
+
if (isOpend) return;
|
|
26
|
+
|
|
27
|
+
// 新しく表示するBodyを取得
|
|
28
|
+
const targetID = clickedButton.getAttribute('aria-controls');
|
|
29
|
+
const targetBody = document.getElementById(targetID);
|
|
30
|
+
if (null === targetBody) return;
|
|
31
|
+
|
|
32
|
+
// 親のタブリスト(ul)を取得
|
|
33
|
+
const parentTabList = clickedButton.parentNode.parentNode;
|
|
34
|
+
|
|
35
|
+
// 現在選択中のタブボタンを取得
|
|
36
|
+
const selectedButton = parentTabList.querySelector('[aria-selected="true"]');
|
|
37
|
+
|
|
38
|
+
// 展開中のBodyを取得
|
|
39
|
+
const displayedID = selectedButton.getAttribute('aria-controls');
|
|
40
|
+
const displayedBody = document.getElementById(displayedID);
|
|
41
|
+
|
|
42
|
+
// ariaの処理
|
|
43
|
+
clickedButton.setAttribute('aria-selected', 'true');
|
|
44
|
+
selectedButton.setAttribute('aria-selected', 'false');
|
|
45
|
+
displayedBody.setAttribute('aria-hidden', 'true');
|
|
46
|
+
targetBody.setAttribute('aria-hidden', 'false');
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function setEvent(tabs) {
|
|
50
|
+
const tabBtns = tabs.querySelectorAll('button[role="tab"]');
|
|
51
|
+
tabBtns.forEach((tabBtn) => {
|
|
52
|
+
tabBtn.addEventListener('click', function (e) {
|
|
53
|
+
tabControl(e);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// タブへのリンクがあるかどうか
|
|
58
|
+
const nowUrl = window?.location?.href;
|
|
59
|
+
if (!nowUrl) return;
|
|
60
|
+
|
|
61
|
+
const hasTabLink = -1 !== nowUrl.indexOf('?lism-tab=');
|
|
62
|
+
if (!hasTabLink) return;
|
|
63
|
+
|
|
64
|
+
// URLでタブを切り替える機能がONかどうか
|
|
65
|
+
|
|
66
|
+
// URLSearchParamsオブジェクトを取得
|
|
67
|
+
const url = new URL(nowUrl);
|
|
68
|
+
const params = url.searchParams;
|
|
69
|
+
|
|
70
|
+
// getメソッド
|
|
71
|
+
const targetTabId = params.get('lism-tab');
|
|
72
|
+
const target = tabs.querySelector(`[aria-controls="${targetTabId}"]`);
|
|
73
|
+
if (target) {
|
|
74
|
+
// transitionをオフにするための属性
|
|
75
|
+
tabs.dataset.hasTabLink = '1';
|
|
76
|
+
|
|
77
|
+
// タブ切り替え
|
|
78
|
+
toggleAriaData(target);
|
|
79
|
+
|
|
80
|
+
// 少し後で属性削除
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
delete tabs.dataset.hasTabLink;
|
|
83
|
+
}, 10);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default setEvent;
|