@aiquants/menu-bar 1.18.2 → 1.19.0
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 +159 -30
- package/dist/MenuBar.d.ts +2 -29
- package/dist/components/SessionInfo.d.ts +1 -26
- package/dist/contents/MenuContent.d.ts +3 -14
- package/dist/contents/MenuList.d.ts +3 -38
- package/dist/contents/OverlayShadow.d.ts +1 -20
- package/dist/elements/MenuSwitch.d.ts +8 -22
- package/dist/events/pinEvent.d.ts +2 -37
- package/dist/hooks/useMenuPinState.d.ts +1 -16
- package/dist/index.d.ts +11 -21
- package/dist/index.es.js +478 -475
- package/dist/index.umd.js +2 -2
- package/dist/styles/menu-bar.css +1 -1
- package/dist/styles/menu-bar.standalone.css +1 -1
- package/dist/types/user.d.ts +0 -7
- package/dist/types.d.ts +1 -15
- package/package.json +8 -6
- package/dist/__tests__/pinEvent.test.d.ts +0 -1
- package/src/MenuBar.tsx +0 -248
- package/src/__tests__/pinEvent.test.tsx +0 -68
- package/src/components/SessionInfo.tsx +0 -95
- package/src/contents/MenuContent.tsx +0 -79
- package/src/contents/MenuList.tsx +0 -400
- package/src/contents/OverlayShadow.tsx +0 -30
- package/src/elements/MenuSwitch.tsx +0 -134
- package/src/events/pinEvent.ts +0 -129
- package/src/hooks/useMenuPinState.ts +0 -58
- package/src/index.ts +0 -29
- package/src/lucide-icons.d.ts +0 -6
- package/src/styles/components.entry.css +0 -11
- package/src/styles/menu-bar.css +0 -224
- package/src/styles/standalone.entry.css +0 -12
- package/src/types/user.ts +0 -12
- package/src/types.ts +0 -24
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module MenuList
|
|
3
|
-
* @description This module provides the list of menu items.
|
|
4
|
-
* @description このモジュールは、メニュー項目のリストを提供します。
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import BarChart from "lucide-react/dist/esm/icons/bar-chart.js"
|
|
8
|
-
import Download from "lucide-react/dist/esm/icons/download.js"
|
|
9
|
-
import FileText from "lucide-react/dist/esm/icons/file-text.js"
|
|
10
|
-
import LinkIcon from "lucide-react/dist/esm/icons/link.js"
|
|
11
|
-
import Lock from "lucide-react/dist/esm/icons/lock.js"
|
|
12
|
-
import Pin from "lucide-react/dist/esm/icons/pin.js"
|
|
13
|
-
import PinOff from "lucide-react/dist/esm/icons/pin-off.js"
|
|
14
|
-
import Table from "lucide-react/dist/esm/icons/table.js"
|
|
15
|
-
import { type ReactNode, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
|
|
16
|
-
import { Link, useLocation } from "react-router-dom"
|
|
17
|
-
|
|
18
|
-
// SSR 環境では useLayoutEffect が警告を出すため useEffect にフォールバックする
|
|
19
|
-
const useSafeLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect
|
|
20
|
-
|
|
21
|
-
// メニューバーのスクロール位置を永続化する sessionStorage のキー
|
|
22
|
-
const SCROLL_STORAGE_KEY = "aiquants-menu-scroll"
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @interface ISubmenu
|
|
26
|
-
* @description Interface for a submenu item.
|
|
27
|
-
* @description サブメニュー項目のインターフェース。
|
|
28
|
-
* @property {number} subid - The unique ID of the submenu item. サブメニュー項目の一意の ID。
|
|
29
|
-
* @property {string} [title] - The title of the submenu item. サブメニュー項目のタイトル。
|
|
30
|
-
* @property {string} href - The URL for the submenu item. サブメニュー項目の URL。
|
|
31
|
-
* @property {string} [mark] - The type of mark to display next to the item. 項目の横に表示するマークの種類。
|
|
32
|
-
* @property {boolean} [new] - Whether the item is a new link. 新しいリンクかどうか。
|
|
33
|
-
*/
|
|
34
|
-
export interface ISubmenu {
|
|
35
|
-
subid: number
|
|
36
|
-
title?: string
|
|
37
|
-
href: string
|
|
38
|
-
mark?: string
|
|
39
|
-
new?: boolean
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @interface IMenu
|
|
44
|
-
* @description Interface for a menu item.
|
|
45
|
-
* @description メニュー項目のインターフェース。
|
|
46
|
-
* @property {number} id - The unique ID of the menu item. メニュー項目の一意の ID。
|
|
47
|
-
* @property {string} title - The title of the menu item. メニュー項目のタイトル。
|
|
48
|
-
* @property {string} [href] - The URL for the menu item. メニュー項目の URL。
|
|
49
|
-
* @property {ISubmenu[]} submenu - The list of submenu items. サブメニュー項目のリスト。
|
|
50
|
-
*/
|
|
51
|
-
export interface IMenu {
|
|
52
|
-
id: number
|
|
53
|
-
title: string
|
|
54
|
-
href?: string
|
|
55
|
-
submenu: ISubmenu[]
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* @typedef MenuNavigationPayload
|
|
60
|
-
* @description Payload emitted when a menu or submenu item is selected.
|
|
61
|
-
* @description メニューまたはサブメニューが選択された際に発火するペイロード。
|
|
62
|
-
* @property {{ id: number; title: string; href?: string }} menu - Selected menu metadata.
|
|
63
|
-
* @property {{ subid: number; title?: string; href: string; mark?: string; new?: boolean }} [submenu] - Selected submenu metadata.
|
|
64
|
-
*/
|
|
65
|
-
export type MenuNavigationPayload = {
|
|
66
|
-
menu: Pick<IMenu, "id" | "title" | "href">
|
|
67
|
-
submenu?: Pick<ISubmenu, "subid" | "title" | "href" | "mark" | "new">
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* @interface IMenuListProps
|
|
72
|
-
* @description Props for the MenuList component.
|
|
73
|
-
* @description MenuList コンポーネントの Props。
|
|
74
|
-
* @property {IMenu[]} menuList - The list of menu items. メニュー項目のリスト。
|
|
75
|
-
* @property {(payload: MenuNavigationPayload) => void} [onMenuSelected] - Function to handle menu item selection. メニュー項目選択を処理する関数。
|
|
76
|
-
*/
|
|
77
|
-
interface IMenuListProps {
|
|
78
|
-
menuList: IMenu[]
|
|
79
|
-
onMenuSelected?(payload: MenuNavigationPayload): void
|
|
80
|
-
pinnedSubmenuIds?: number[]
|
|
81
|
-
onToggleSubmenuPin?(id: number): void
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const regexp = new RegExp(/(^https?:\/\/)|(\.(html|htm)?$)/)
|
|
85
|
-
|
|
86
|
-
const markIcons: Record<string, React.ElementType> = {
|
|
87
|
-
report: FileText,
|
|
88
|
-
slip: FileText,
|
|
89
|
-
table: Table,
|
|
90
|
-
chart: BarChart,
|
|
91
|
-
download: Download,
|
|
92
|
-
lock: Lock,
|
|
93
|
-
href: LinkIcon,
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* @function StyledLinkText
|
|
98
|
-
* @description A component that renders a styled link with an optional icon.
|
|
99
|
-
* @description オプションのアイコンを持つスタイル付きリンクを描画するコンポーネント。
|
|
100
|
-
* @param {{ mark?: string; children: ReactNode }} props - The props for the component.
|
|
101
|
-
* @returns {React.JSX.Element} The rendered styled link text.
|
|
102
|
-
*/
|
|
103
|
-
const StyledLinkText = ({ mark, children }: { mark?: string; children: ReactNode }) => {
|
|
104
|
-
const Icon = mark ? markIcons[mark] : undefined
|
|
105
|
-
if (!Icon) {
|
|
106
|
-
return <span className="mx-1 text-sky-600 hover:underline dark:text-sky-400">{children}</span>
|
|
107
|
-
}
|
|
108
|
-
return (
|
|
109
|
-
<div className="flex flex-nowrap items-center">
|
|
110
|
-
<Icon className="h-3.5 w-3.5 shrink-0" />
|
|
111
|
-
<div className="aiquants-menu-link mx-1 hover:underline">{children}</div>
|
|
112
|
-
</div>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* @function buildMenuPayload
|
|
118
|
-
* @description Creates a navigation payload from menu metadata.
|
|
119
|
-
* @description メニューのメタデータからナビゲーション用ペイロードを生成します。
|
|
120
|
-
* @param {IMenu} menu - The parent menu item. 親メニュー項目。
|
|
121
|
-
* @param {ISubmenu} [submenu] - The selected submenu item if any. 選択されたサブメニュー項目。
|
|
122
|
-
* @returns {MenuNavigationPayload} The constructed payload.
|
|
123
|
-
*/
|
|
124
|
-
const buildMenuPayload = (menu: IMenu, submenu?: ISubmenu): MenuNavigationPayload => {
|
|
125
|
-
if (!submenu) {
|
|
126
|
-
return {
|
|
127
|
-
menu: {
|
|
128
|
-
id: menu.id,
|
|
129
|
-
title: menu.title,
|
|
130
|
-
href: menu.href,
|
|
131
|
-
},
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return {
|
|
135
|
-
menu: {
|
|
136
|
-
id: menu.id,
|
|
137
|
-
title: menu.title,
|
|
138
|
-
href: menu.href,
|
|
139
|
-
},
|
|
140
|
-
submenu: {
|
|
141
|
-
subid: submenu.subid,
|
|
142
|
-
title: submenu.title,
|
|
143
|
-
href: submenu.href,
|
|
144
|
-
mark: submenu.mark,
|
|
145
|
-
new: submenu.new,
|
|
146
|
-
},
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* @function SubMenuItem
|
|
152
|
-
* @description A component that renders a single submenu item.
|
|
153
|
-
* @description 単一のサブメニュー項目を描画するコンポーネント。
|
|
154
|
-
* @param {{ item: ISubmenu; parentMenu: IMenu; onMenuSelected?: (payload: MenuNavigationPayload) => void }} props - The props for the component.
|
|
155
|
-
* @returns {React.JSX.Element} The rendered submenu item.
|
|
156
|
-
*/
|
|
157
|
-
const SubMenuItem = ({ item, parentMenu, onMenuSelected }: { item: ISubmenu; parentMenu: IMenu; onMenuSelected?: (payload: MenuNavigationPayload) => void }) => {
|
|
158
|
-
if (item.href === "separator") {
|
|
159
|
-
return <hr className="aiquants-menu-separator mx-2 my-1 h-px" />
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const linkContent = <StyledLinkText mark={item.mark}>{item.title}</StyledLinkText>
|
|
163
|
-
|
|
164
|
-
const commonProps = {
|
|
165
|
-
onClick: () => {
|
|
166
|
-
if (!onMenuSelected) {
|
|
167
|
-
return
|
|
168
|
-
}
|
|
169
|
-
onMenuSelected(buildMenuPayload(parentMenu, item))
|
|
170
|
-
},
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (item.new) {
|
|
174
|
-
return (
|
|
175
|
-
<a href={item.href} rel="noopener noreferrer" target="_blank" {...commonProps}>
|
|
176
|
-
{linkContent}
|
|
177
|
-
</a>
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (regexp.test(item.href)) {
|
|
182
|
-
return (
|
|
183
|
-
<a href={item.href} {...commonProps}>
|
|
184
|
-
{linkContent}
|
|
185
|
-
</a>
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return (
|
|
190
|
-
<Link to={item.href} {...commonProps}>
|
|
191
|
-
{linkContent}
|
|
192
|
-
</Link>
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* @function MenuList
|
|
198
|
-
* @description A component that renders the list of menu items.
|
|
199
|
-
* @description メニュー項目のリストを描画するコンポーネント。
|
|
200
|
-
* @param {IMenuListProps} props - The props for the component.
|
|
201
|
-
* @returns {React.JSX.Element} The rendered menu list.
|
|
202
|
-
*/
|
|
203
|
-
/**
|
|
204
|
-
* href からクエリパラメータやハッシュ、末尾のスラッシュを除去して正規化します。
|
|
205
|
-
*/
|
|
206
|
-
const normalizeHref = (href: string | undefined): string => {
|
|
207
|
-
if (!href) return ""
|
|
208
|
-
return href.split(/[?#]/)[0].replace(/(?!^\/)\/$/, "")
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* 現在のパス名に合致するメニュー項目またはサブメニュー項目の親メニューIDを検索します。
|
|
213
|
-
*/
|
|
214
|
-
const getActiveMenuId = (menuList: IMenu[], pathname: string): number | null => {
|
|
215
|
-
const normalizedPath = pathname !== "/" && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname
|
|
216
|
-
for (const item of menuList) {
|
|
217
|
-
if (normalizeHref(item.href) === normalizedPath) {
|
|
218
|
-
return item.id
|
|
219
|
-
}
|
|
220
|
-
if (item.submenu) {
|
|
221
|
-
for (const sub of item.submenu) {
|
|
222
|
-
if (normalizeHref(sub.href) === normalizedPath) {
|
|
223
|
-
return item.id
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return null
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export const MenuList = (props: IMenuListProps): React.JSX.Element => {
|
|
232
|
-
const { menuList, onMenuSelected, pinnedSubmenuIds = [], onToggleSubmenuPin } = props
|
|
233
|
-
|
|
234
|
-
// 現在のパス名を取得 (非Router環境のテスト用フォールバック付き)
|
|
235
|
-
let locationPathname = ""
|
|
236
|
-
try {
|
|
237
|
-
const location = useLocation()
|
|
238
|
-
locationPathname = location.pathname
|
|
239
|
-
} catch {
|
|
240
|
-
if (typeof window !== "undefined") {
|
|
241
|
-
locationPathname = window.location.pathname
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// 初回の開閉状態をパス名から特定
|
|
246
|
-
const initialOpenMenuId = useMemo(() => {
|
|
247
|
-
return getActiveMenuId(menuList, locationPathname)
|
|
248
|
-
}, [menuList, locationPathname])
|
|
249
|
-
|
|
250
|
-
const [openMenuId, setOpenMenuId] = useState<number | null>(initialOpenMenuId)
|
|
251
|
-
|
|
252
|
-
// 自動展開済みのパス名と状態を追跡するRef。
|
|
253
|
-
// これにより、非同期メニュー読込による menuList 更新時に、ユーザーが手動で閉じたアコーディオンが勝手に再展開されるのを防ぎます。
|
|
254
|
-
const lastPathnameRef = useRef<string | null>(locationPathname)
|
|
255
|
-
const hasAutoOpenedRef = useRef<boolean>(initialOpenMenuId !== null)
|
|
256
|
-
|
|
257
|
-
useEffect(() => {
|
|
258
|
-
// パス名変更時にフラグをリセットし、メニューを一旦閉じる
|
|
259
|
-
if (lastPathnameRef.current !== locationPathname) {
|
|
260
|
-
lastPathnameRef.current = locationPathname
|
|
261
|
-
hasAutoOpenedRef.current = false
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// 現在のパスに対して自動展開が未完了の場合のみ実行
|
|
265
|
-
if (!hasAutoOpenedRef.current) {
|
|
266
|
-
const activeId = getActiveMenuId(menuList, locationPathname)
|
|
267
|
-
if (activeId !== null) {
|
|
268
|
-
setOpenMenuId(activeId)
|
|
269
|
-
hasAutoOpenedRef.current = true
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}, [locationPathname, menuList])
|
|
273
|
-
|
|
274
|
-
const handleToggle = useCallback(
|
|
275
|
-
(id: number) => {
|
|
276
|
-
const isPinned = pinnedSubmenuIds.includes(id)
|
|
277
|
-
if (isPinned) {
|
|
278
|
-
// Keep the expanded state of the submenu when it is pinned
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
setOpenMenuId((prevId) => (prevId === id ? null : id))
|
|
282
|
-
},
|
|
283
|
-
[pinnedSubmenuIds],
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
// スクロールコンテナへの参照。ページ遷移で再マウントされてもスクロール位置を維持するために使用する。
|
|
287
|
-
const scrollRef = useRef<HTMLDivElement>(null)
|
|
288
|
-
|
|
289
|
-
// マウント時に保存済みのスクロール位置を復元する。
|
|
290
|
-
// サブメニューのピン留め展開状態と同様に、ページ遷移をまたいでもメニューバーのスクロール位置を保持する。
|
|
291
|
-
// ペイント前に復元してスクロール位置のちらつきを防ぐため useLayoutEffect を使用する。
|
|
292
|
-
useSafeLayoutEffect(() => {
|
|
293
|
-
const el = scrollRef.current
|
|
294
|
-
if (!el) {
|
|
295
|
-
return
|
|
296
|
-
}
|
|
297
|
-
try {
|
|
298
|
-
const saved = sessionStorage.getItem(SCROLL_STORAGE_KEY)
|
|
299
|
-
if (saved !== null) {
|
|
300
|
-
const top = Number.parseFloat(saved)
|
|
301
|
-
if (!Number.isNaN(top)) {
|
|
302
|
-
el.scrollTop = top
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
} catch {}
|
|
306
|
-
}, [])
|
|
307
|
-
|
|
308
|
-
// スクロール時に現在位置を sessionStorage へ保存する。
|
|
309
|
-
const handleScroll = useCallback(() => {
|
|
310
|
-
const el = scrollRef.current
|
|
311
|
-
if (!el) {
|
|
312
|
-
return
|
|
313
|
-
}
|
|
314
|
-
try {
|
|
315
|
-
sessionStorage.setItem(SCROLL_STORAGE_KEY, String(el.scrollTop))
|
|
316
|
-
} catch {}
|
|
317
|
-
}, [])
|
|
318
|
-
|
|
319
|
-
const MenuListContent = useMemo(() => {
|
|
320
|
-
return (
|
|
321
|
-
<>
|
|
322
|
-
{menuList.map((item) => (
|
|
323
|
-
<div key={item.id}>
|
|
324
|
-
{item.href ? (
|
|
325
|
-
<div className="aiquants-menu-list-item relative mb-0.5 ml-1 flex items-center rounded-l-xl px-2.5 py-1 text-xs before:mr-2 before:inline-block before:h-2 before:w-2 before:shrink-0 before:rounded-full before:content-[''] sm:text-sm">
|
|
326
|
-
{regexp.test(item.href) ? (
|
|
327
|
-
<a
|
|
328
|
-
href={item.href}
|
|
329
|
-
onClick={() => {
|
|
330
|
-
if (!onMenuSelected) {
|
|
331
|
-
return
|
|
332
|
-
}
|
|
333
|
-
onMenuSelected(buildMenuPayload(item))
|
|
334
|
-
}}>
|
|
335
|
-
<span className="aiquants-menu-link hover:underline">{item.title}</span>
|
|
336
|
-
</a>
|
|
337
|
-
) : (
|
|
338
|
-
<Link
|
|
339
|
-
to={item.href}
|
|
340
|
-
onClick={() => {
|
|
341
|
-
if (!onMenuSelected) {
|
|
342
|
-
return
|
|
343
|
-
}
|
|
344
|
-
onMenuSelected(buildMenuPayload(item))
|
|
345
|
-
}}>
|
|
346
|
-
<span className="aiquants-menu-link hover:underline">{item.title}</span>
|
|
347
|
-
</Link>
|
|
348
|
-
)}
|
|
349
|
-
</div>
|
|
350
|
-
) : (
|
|
351
|
-
<details className="aiquants-menu-accordion-details" open={item.id === openMenuId || pinnedSubmenuIds.includes(item.id)}>
|
|
352
|
-
{/** biome-ignore lint/a11y/noStaticElementInteractions: summary toggles submenu visibility */}
|
|
353
|
-
<summary
|
|
354
|
-
className="aiquants-menu-list-item aiquants-menu-accordion-summary relative mb-0.5 ml-1 list-none rounded-l-xl px-2.5 py-1 text-xs outline-none before:mr-2 before:inline-block before:h-2 before:w-2 before:shrink-0 before:rounded-full before:content-[''] sm:text-sm"
|
|
355
|
-
onClick={(e) => {
|
|
356
|
-
e.preventDefault()
|
|
357
|
-
handleToggle(item.id)
|
|
358
|
-
}}
|
|
359
|
-
onKeyDown={(e) => {
|
|
360
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
361
|
-
e.preventDefault()
|
|
362
|
-
handleToggle(item.id)
|
|
363
|
-
}
|
|
364
|
-
}}>
|
|
365
|
-
<span>{item.title}</span>
|
|
366
|
-
<button
|
|
367
|
-
type="button"
|
|
368
|
-
onClick={(e) => {
|
|
369
|
-
e.stopPropagation()
|
|
370
|
-
e.preventDefault()
|
|
371
|
-
if (onToggleSubmenuPin) {
|
|
372
|
-
onToggleSubmenuPin(item.id)
|
|
373
|
-
}
|
|
374
|
-
}}
|
|
375
|
-
className={`aiquants-submenu-pin-button ${pinnedSubmenuIds.includes(item.id) ? "pinned" : ""}`}
|
|
376
|
-
title={pinnedSubmenuIds.includes(item.id) ? "ピン留め解除" : "ピン留め"}>
|
|
377
|
-
{pinnedSubmenuIds.includes(item.id) ? <Pin size={13} /> : <PinOff size={13} />}
|
|
378
|
-
</button>
|
|
379
|
-
</summary>
|
|
380
|
-
<div className="aiquants-menu-submenu pointer-events-auto my-0.5 mr-1 ml-3.5 rounded-lg px-2 py-1 shadow-sm">
|
|
381
|
-
{item.submenu.map((subitem) => (
|
|
382
|
-
<div key={subitem.subid} className="aiquants-menu-accordion-container block w-full whitespace-nowrap py-0.5 text-xs">
|
|
383
|
-
<SubMenuItem item={subitem} parentMenu={item} onMenuSelected={onMenuSelected} />
|
|
384
|
-
</div>
|
|
385
|
-
))}
|
|
386
|
-
</div>
|
|
387
|
-
</details>
|
|
388
|
-
)}
|
|
389
|
-
</div>
|
|
390
|
-
))}
|
|
391
|
-
</>
|
|
392
|
-
)
|
|
393
|
-
}, [menuList, onMenuSelected, openMenuId, handleToggle, pinnedSubmenuIds])
|
|
394
|
-
|
|
395
|
-
return (
|
|
396
|
-
<div ref={scrollRef} onScroll={handleScroll} className="hidden-scrollbar h-0 grow overflow-y-scroll">
|
|
397
|
-
{MenuListContent}
|
|
398
|
-
</div>
|
|
399
|
-
)
|
|
400
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module OverlayShadow
|
|
3
|
-
* @description This module provides an overlay shadow component.
|
|
4
|
-
* @description このモジュールは、オーバーレイシャドウコンポーネントを提供します。
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @interface IOverlayProps
|
|
9
|
-
* @description Props for the OverlayShadow component.
|
|
10
|
-
* @description OverlayShadow コンポーネントの Props。
|
|
11
|
-
* @property {boolean} isOverlay - Whether the overlay is visible. オーバーレイが表示されているかどうか。
|
|
12
|
-
* @property {() => void} [onOverlayClick] - Function to handle overlay click. オーバーレイのクリックを処理する関数。
|
|
13
|
-
*/
|
|
14
|
-
interface IOverlayProps {
|
|
15
|
-
isOverlay: boolean
|
|
16
|
-
onOverlayClick?(): void
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @function OverlayShadow
|
|
21
|
-
* @description A component that renders an overlay shadow.
|
|
22
|
-
* @description オーバーレイシャドウを描画するコンポーネント。
|
|
23
|
-
* @param {IOverlayProps} props - The props for the component.
|
|
24
|
-
* @returns {React.JSX.Element} The rendered overlay shadow.
|
|
25
|
-
*/
|
|
26
|
-
export const OverlayShadow = (props: IOverlayProps): React.JSX.Element => {
|
|
27
|
-
const { isOverlay, onOverlayClick } = props
|
|
28
|
-
|
|
29
|
-
return <button type="button" className={`fixed inset-0 z-2147483640 cursor-default bg-black/50 transition-opacity duration-500 ${isOverlay ? "opacity-100" : "pointer-events-none opacity-0"}`} onClick={onOverlayClick} />
|
|
30
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef } from "react"
|
|
2
|
-
import type { Theme } from "../types"
|
|
3
|
-
|
|
4
|
-
export interface IMenuSwitchProps {
|
|
5
|
-
/**
|
|
6
|
-
* MenuBar コンポーネントからの指示:
|
|
7
|
-
* open === true (または !isOpen): メニューは「閉じている」状態 ➔ 4本線ハンバーガーを表示
|
|
8
|
-
* open === false (または isOpen): メニューは「開いている」状態 ➔ ✕印を表示
|
|
9
|
-
*/
|
|
10
|
-
open?: boolean
|
|
11
|
-
isOpen?: boolean
|
|
12
|
-
onMenuOpen?: () => void
|
|
13
|
-
onMenuClose?: () => void
|
|
14
|
-
onToggle?: (nextOpen: boolean) => void
|
|
15
|
-
theme?: Theme
|
|
16
|
-
pinned?: boolean
|
|
17
|
-
isPinned?: boolean
|
|
18
|
-
className?: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 4本線バーと✕印アニメーションの従前の仕様の MenuSwitch コンポーネント。
|
|
23
|
-
*/
|
|
24
|
-
export const MenuSwitch = (props: IMenuSwitchProps): React.JSX.Element => {
|
|
25
|
-
const { open, isOpen, onMenuOpen, onMenuClose, onToggle, theme = "light", className } = props
|
|
26
|
-
|
|
27
|
-
// メニューが閉じた状態(=4本線表示)かどうか
|
|
28
|
-
// open が指定されている場合は open (open === !isMenuOpen)
|
|
29
|
-
// isOpen が指定されている場合は !isOpen
|
|
30
|
-
const isClosed = open ?? (isOpen !== undefined ? !isOpen : true)
|
|
31
|
-
|
|
32
|
-
const stateRef = useRef(isClosed)
|
|
33
|
-
|
|
34
|
-
const refSpan1 = useRef<HTMLSpanElement>(null)
|
|
35
|
-
const refSpan2 = useRef<HTMLSpanElement>(null)
|
|
36
|
-
const refSpan3 = useRef<HTMLSpanElement>(null)
|
|
37
|
-
const refSpan4 = useRef<HTMLSpanElement>(null)
|
|
38
|
-
const refMenuString = useRef<HTMLDivElement>(null)
|
|
39
|
-
|
|
40
|
-
// 4本線ハンバーガーを表示(メニューが閉じている時)
|
|
41
|
-
const showHamburgerStyle = useCallback(() => {
|
|
42
|
-
if (refSpan1.current && refSpan2.current && refSpan3.current && refSpan4.current && refMenuString.current) {
|
|
43
|
-
const color = theme === "dark" ? "#fff" : "#000"
|
|
44
|
-
refSpan1.current.style.backgroundColor = color
|
|
45
|
-
refSpan1.current.style.width = "100%"
|
|
46
|
-
refSpan1.current.style.height = "3px"
|
|
47
|
-
refSpan1.current.style.transform = "none"
|
|
48
|
-
|
|
49
|
-
refSpan2.current.style.opacity = "1"
|
|
50
|
-
refSpan2.current.style.backgroundColor = color
|
|
51
|
-
|
|
52
|
-
refSpan3.current.style.opacity = "1"
|
|
53
|
-
refSpan3.current.style.backgroundColor = color
|
|
54
|
-
|
|
55
|
-
refSpan4.current.style.backgroundColor = color
|
|
56
|
-
refSpan4.current.style.width = "100%"
|
|
57
|
-
refSpan4.current.style.height = "3px"
|
|
58
|
-
refSpan4.current.style.transform = "none"
|
|
59
|
-
|
|
60
|
-
refMenuString.current.style.color = color
|
|
61
|
-
refMenuString.current.classList.remove("after:content-['close']")
|
|
62
|
-
refMenuString.current.classList.remove("after:ml-0.5")
|
|
63
|
-
refMenuString.current.classList.add("after:content-['menu']")
|
|
64
|
-
refMenuString.current.classList.add("after:-ml-0.5")
|
|
65
|
-
stateRef.current = true
|
|
66
|
-
}
|
|
67
|
-
}, [theme])
|
|
68
|
-
|
|
69
|
-
// ✕印を表示(メニューが開いている時)
|
|
70
|
-
const showCrossStyle = useCallback(() => {
|
|
71
|
-
if (refSpan1.current && refSpan2.current && refSpan3.current && refSpan4.current && refMenuString.current) {
|
|
72
|
-
// 開いている時は背景オーバーレイが暗いため常に白色
|
|
73
|
-
refSpan1.current.style.backgroundColor = "#fff"
|
|
74
|
-
refSpan1.current.style.width = "32px"
|
|
75
|
-
refSpan1.current.style.height = "4px"
|
|
76
|
-
refSpan1.current.style.transform = "translateY(9px) rotate(-45deg)"
|
|
77
|
-
|
|
78
|
-
refSpan2.current.style.opacity = "0"
|
|
79
|
-
refSpan3.current.style.opacity = "0"
|
|
80
|
-
|
|
81
|
-
refSpan4.current.style.backgroundColor = "#fff"
|
|
82
|
-
refSpan4.current.style.width = "32px"
|
|
83
|
-
refSpan4.current.style.height = "4px"
|
|
84
|
-
refSpan4.current.style.transform = "translateY(-9px) rotate(45deg)"
|
|
85
|
-
|
|
86
|
-
refMenuString.current.style.color = "#fff"
|
|
87
|
-
refMenuString.current.classList.remove("after:content-['menu']")
|
|
88
|
-
refMenuString.current.classList.remove("after:-ml-0.5")
|
|
89
|
-
refMenuString.current.classList.add("after:content-['close']")
|
|
90
|
-
refMenuString.current.classList.add("after:ml-0.5")
|
|
91
|
-
stateRef.current = false
|
|
92
|
-
}
|
|
93
|
-
}, [])
|
|
94
|
-
|
|
95
|
-
useEffect(() => {
|
|
96
|
-
if (isClosed) {
|
|
97
|
-
showHamburgerStyle()
|
|
98
|
-
} else {
|
|
99
|
-
showCrossStyle()
|
|
100
|
-
}
|
|
101
|
-
}, [isClosed, showHamburgerStyle, showCrossStyle])
|
|
102
|
-
|
|
103
|
-
const handleToggle = useCallback(() => {
|
|
104
|
-
if (stateRef.current) {
|
|
105
|
-
// 現在 4本線(閉じている状態) ➔ クリックでメニューを開く
|
|
106
|
-
showCrossStyle()
|
|
107
|
-
onMenuOpen?.()
|
|
108
|
-
onToggle?.(true)
|
|
109
|
-
} else {
|
|
110
|
-
// 現在 ✕印(開いている状態) ➔ クリックでメニューを閉じる
|
|
111
|
-
showHamburgerStyle()
|
|
112
|
-
onMenuClose?.()
|
|
113
|
-
onToggle?.(false)
|
|
114
|
-
}
|
|
115
|
-
}, [showCrossStyle, showHamburgerStyle, onMenuOpen, onMenuClose, onToggle])
|
|
116
|
-
|
|
117
|
-
const isMenuOpened = !isClosed
|
|
118
|
-
|
|
119
|
-
return (
|
|
120
|
-
<button
|
|
121
|
-
type="button"
|
|
122
|
-
data-aqmb-menu-switch="true"
|
|
123
|
-
aria-label={isMenuOpened ? "Close menu" : "Open menu"}
|
|
124
|
-
aria-expanded={isMenuOpened}
|
|
125
|
-
className={`aiquants-menu-switch z-[2147483647] inline-block h-[27px] w-[27px] cursor-pointer align-middle outline-hidden ${className || ""}`}
|
|
126
|
-
onClick={handleToggle}>
|
|
127
|
-
<span ref={refSpan1} className="absolute top-0 left-0 box-border inline-block h-[3px] w-full select-none transition-all duration-500" />
|
|
128
|
-
<span ref={refSpan2} className="absolute top-[6px] left-0 box-border inline-block h-[3px] w-full select-none transition-all duration-500" />
|
|
129
|
-
<span ref={refSpan3} className="absolute top-[12px] left-0 box-border inline-block h-[3px] w-full select-none transition-all duration-500" />
|
|
130
|
-
<span ref={refSpan4} className="absolute top-[18px] left-0 box-border inline-block h-[3px] w-full select-none transition-all duration-500" />
|
|
131
|
-
<div ref={refMenuString} className="absolute w-full text-xs transition-all duration-500 after:mt-2 after:block" />
|
|
132
|
-
</button>
|
|
133
|
-
)
|
|
134
|
-
}
|