@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.
Files changed (38) hide show
  1. package/README.md +0 -0
  2. package/package.json +75 -0
  3. package/src/components/Accordion/AccIcon.jsx +6 -0
  4. package/src/components/Accordion/Accordion.jsx +60 -0
  5. package/src/components/Accordion/_style.scss +54 -0
  6. package/src/components/Accordion/astro/AccBody.astro +13 -0
  7. package/src/components/Accordion/astro/AccHeader.astro +13 -0
  8. package/src/components/Accordion/astro/AccHeaderLabel.astro +12 -0
  9. package/src/components/Accordion/astro/AccIcon.astro +18 -0
  10. package/src/components/Accordion/astro/AccLabel.astro +11 -0
  11. package/src/components/Accordion/astro/Accordion.astro +21 -0
  12. package/src/components/Accordion/astro/__setEvent.js +2 -0
  13. package/src/components/Accordion/astro/index.js +8 -0
  14. package/src/components/Accordion/getProps.js +32 -0
  15. package/src/components/Accordion/index.js +4 -0
  16. package/src/components/Accordion/script.js +5 -0
  17. package/src/components/Accordion/setAccordion.js +107 -0
  18. package/src/components/Modal/Body.jsx +10 -0
  19. package/src/components/Modal/CloseBtn.jsx +20 -0
  20. package/src/components/Modal/Inner.jsx +6 -0
  21. package/src/components/Modal/Modal.jsx +20 -0
  22. package/src/components/Modal/OpenBtn.jsx +10 -0
  23. package/src/components/Modal/getProps.js +30 -0
  24. package/src/components/Modal/index.js +7 -0
  25. package/src/components/Modal/script.js +5 -0
  26. package/src/components/Modal/setModal.ts +107 -0
  27. package/src/components/Tabs/Tab.jsx +18 -0
  28. package/src/components/Tabs/TabItem.jsx +5 -0
  29. package/src/components/Tabs/TabList.jsx +6 -0
  30. package/src/components/Tabs/TabPanel.jsx +8 -0
  31. package/src/components/Tabs/Tabs.jsx +60 -0
  32. package/src/components/Tabs/getProps.js +8 -0
  33. package/src/components/Tabs/index.js +7 -0
  34. package/src/components/Tabs/script.js +8 -0
  35. package/src/components/Tabs/setEvent.js +87 -0
  36. package/src/components/__contexts.js +4 -0
  37. package/src/components/astro.ts +3 -0
  38. 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,12 @@
1
+ ---
2
+ import AccHeader from './AccHeader.astro';
3
+ import AccLabel from './AccLabel.astro';
4
+ import AccIcon from './AccIcon.astro';
5
+ ---
6
+
7
+ <AccHeader {...Astro.props || {}}>
8
+ <AccLabel>
9
+ <slot />
10
+ </AccLabel>
11
+ <AccIcon />
12
+ </AccHeader>
@@ -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,11 @@
1
+ ---
2
+ // import type { LismProps } from 'lism-css/types';
3
+ import { Lism } from 'lism-css/react';
4
+ import { defaultProps } from '../getProps';
5
+
6
+ const props = Astro.props || {};
7
+ ---
8
+
9
+ <Lism {...defaultProps.label} {...props}>
10
+ <slot />
11
+ </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,2 @@
1
+ import setEvent from '@lism-ui/core/components/Accordion/setEvent';
2
+ export default setEvent;
@@ -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,4 @@
1
+ import { Accordion as Root, Header, Label, Body, HeaderLabel } from './Accordion';
2
+ import { default as Icon } from './AccIcon';
3
+
4
+ export default { Root, Header, Label, Icon, Body, HeaderLabel };
@@ -0,0 +1,5 @@
1
+ import setAccordion from './setAccordion.js';
2
+
3
+ document.addEventListener('DOMContentLoaded', function () {
4
+ setAccordion();
5
+ });
@@ -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,10 @@
1
+ import { Lism } from '../Lism';
2
+ import { defaultProps } from './getProps';
3
+
4
+ export default function ModalBody({ children, ...props }) {
5
+ return (
6
+ <Lism {...defaultProps.body} {...props}>
7
+ {children}
8
+ </Lism>
9
+ );
10
+ }
@@ -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,6 @@
1
+ import { Lism } from '../Lism';
2
+ import { getInnerProps } from './getProps';
3
+
4
+ export default function ModalInner({ children, ...props }) {
5
+ return <Lism {...getInnerProps(props)}>{children}</Lism>;
6
+ }
@@ -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,7 @@
1
+ import Root from './Modal';
2
+ import Inner from './Inner';
3
+ import Body from './Body';
4
+ import CloseBtn from './CloseBtn';
5
+ import OpenBtn from './OpenBtn';
6
+
7
+ export default { Root, Inner, Body, CloseBtn, OpenBtn };
@@ -0,0 +1,5 @@
1
+ import setModal from './setModal.ts';
2
+
3
+ document.addEventListener('DOMContentLoaded', function () {
4
+ setModal();
5
+ });
@@ -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,5 @@
1
+ // Note: <Tabs>側でループして色々処理される。
2
+ // 引数でちゃんと処理したいpropを書いておかないとだめ。
3
+ export default function TabItem({ isTabItem = true, children }) {
4
+ return <div>{children}</div>;
5
+ }
@@ -0,0 +1,6 @@
1
+ import { Lism } from '../Lism';
2
+
3
+ export default function TabList(props) {
4
+ // jc: 's',
5
+ return <Lism lismClass='d--tabs_list' role='tablist' {...props} />;
6
+ }
@@ -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,8 @@
1
+ import atts from '../../lib/helper/atts';
2
+
3
+ export default function getTabsProps({ lismClass, ...props }) {
4
+ return {
5
+ lismClass: atts(lismClass, 'd--tabs'),
6
+ ...props,
7
+ };
8
+ }
@@ -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,8 @@
1
+ import setEvent from './setEvent.js';
2
+
3
+ document.addEventListener('DOMContentLoaded', function () {
4
+ const tabsAll = document.querySelectorAll('.d--tabs');
5
+ tabsAll.forEach((tabs) => {
6
+ setEvent(tabs);
7
+ });
8
+ });
@@ -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;
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ export const LayoutContext = React.createContext(null);
3
+
4
+ // import {createContext} from "react"; で 使うと、関係ないコンポーネントも use client が必要になってしまう。
@@ -0,0 +1,3 @@
1
+ export { default as Accordion } from './Accordion/astro';
2
+ // export { default as Tabs } from './Tabs';
3
+ // export { default as Modal } from './Modal';
@@ -0,0 +1,3 @@
1
+ export { default as Accordion } from './Accordion';
2
+ // export { default as Tabs } from './Tabs';
3
+ // export { default as Modal } from './Modal';