From 710a920cfe18d124ad0489c56feba5fa2d6eb713 Mon Sep 17 00:00:00 2001 From: ddryo <31400297+ddryo@users.noreply.github.com> Date: Wed, 30 Sep 2026 07:26:42 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(lism-ui):=20Astro=E7=89=88Tabs/Accordio?= =?UTF-8?q?n/Modal=E3=82=92ClientRouter=E9=81=B7=E7=A7=BB=E5=BE=8C?= =?UTF-8?q?=E3=81=AB=E3=82=82=E5=88=9D=E6=9C=9F=E5=8C=96=E3=81=97=E3=80=81?= =?UTF-8?q?=E3=82=A4=E3=83=99=E3=83=B3=E3=83=88=E3=81=AE=E4=BA=8C=E9=87=8D?= =?UTF-8?q?=E7=99=BB=E9=8C=B2=E3=82=92=E9=98=B2=E3=81=90=20(#640)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011LTEGRJSW8WixGvffS3Wqj --- apps/site/src/content/en/changelog.mdx | 1 + apps/site/src/content/ja/changelog.mdx | 1 + .../src/components/Accordion/astro/Root.astro | 4 +- .../components/Accordion/setAccordion.test.ts | 98 ++++++++++++++-- .../src/components/Accordion/setAccordion.ts | 21 +++- .../src/components/Modal/astro/Modal.astro | 4 +- .../src/components/Modal/setModal.test.ts | 108 ++++++++++++++++-- .../lism-ui/src/components/Modal/setModal.ts | 57 +++++++-- .../src/components/Tabs/astro/Root.astro | 7 +- .../src/components/Tabs/setTabs.test.ts | 87 +++++++++++--- .../lism-ui/src/components/Tabs/setTabs.ts | 22 ++++ 11 files changed, 356 insertions(+), 54 deletions(-) diff --git a/apps/site/src/content/en/changelog.mdx b/apps/site/src/content/en/changelog.mdx index e5a63d778..b33a29d79 100644 --- a/apps/site/src/content/en/changelog.mdx +++ b/apps/site/src/content/en/changelog.mdx @@ -16,6 +16,7 @@ Significant changes may continue until v1.0 is released, so all notable changes - **Breaking:** Removed the deprecated component aliases `SideMain` / `FluidCols` / `SwitchCols` and the `Dummy` component from `lism-css/react` and `lism-css/astro`. Migrate to `WithSide` / `AutoColumns` / `SwitchColumns` and `DummyText` from `@lism-css/ui`, respectively (#646). - **Breaking:** Removed `_propConfig`, which overrode the Lism Props conversion settings per prop, from `getLismProps` and component props. To change the conversion settings, use [`props` in `lism.config.js`](/en/docs/customize/config/) instead (#225). - `@lism-css/ui`: Fixed `lism-base` styles taking priority over `b--*` styles when component CSS is output before `lism-css/main.css` (for example, a `Button` rendered as an `a` element kept its underline and link color). Each component's CSS and `style.css` now declare the order of `lism-base` and `lism-block` at the top (#644). +- `@lism-css/ui`: Fixed the Astro `Tabs` / `Accordion` / `Modal` not working on pages reached through `ClientRouter` navigation. They are now initialized on `astro:page-load` as well as on load, and events are not registered twice on the same element (#640). diff --git a/apps/site/src/content/ja/changelog.mdx b/apps/site/src/content/ja/changelog.mdx index 983298186..3c1529489 100644 --- a/apps/site/src/content/ja/changelog.mdx +++ b/apps/site/src/content/ja/changelog.mdx @@ -16,6 +16,7 @@ v1.0のリリースまでは大きな変更が続く可能性があるため、 - **破壊的変更:** 非推奨だったコンポーネントエイリアス`SideMain` / `FluidCols` / `SwitchCols`と`Dummy`コンポーネントを、`lism-css/react`と`lism-css/astro`から削除しました。それぞれ`WithSide` / `AutoColumns` / `SwitchColumns`と`@lism-css/ui`の`DummyText`へ移行してください(#646)。 - **破壊的変更:** Lism Propsの変換設定をprop単位で上書きする`_propConfig`を、`getLismProps`と各コンポーネントのpropsから削除しました。変換設定を変えたい場合は、[`lism.config.js`の`props`](/docs/customize/config/)で設定してください(#225)。 - `@lism-css/ui`: コンポーネントのCSSが`lism-css/main.css`より前に出力されると、`lism-base`のスタイルが`b--*`のスタイルより優先される不具合を修正しました(`a`要素の`Button`に下線とリンク色が残るなど)。各コンポーネントのCSSと`style.css`の先頭で、`lism-base`と`lism-block`の順序を宣言します(#644)。 +- `@lism-css/ui`: Astro版の`Tabs` / `Accordion` / `Modal`が、`ClientRouter`で遷移した先のページで動作しない不具合を修正しました。読み込み時に加えて`astro:page-load`でも初期化し、同じ要素にはイベントを重ねて登録しません(#640)。 diff --git a/packages/lism-ui/src/components/Accordion/astro/Root.astro b/packages/lism-ui/src/components/Accordion/astro/Root.astro index 116dfa321..7c68af045 100644 --- a/packages/lism-ui/src/components/Accordion/astro/Root.astro +++ b/packages/lism-ui/src/components/Accordion/astro/Root.astro @@ -18,6 +18,6 @@ const { class: className, as = 'div', allowMultiple, ...props } = Astro.props; diff --git a/packages/lism-ui/src/components/Accordion/setAccordion.test.ts b/packages/lism-ui/src/components/Accordion/setAccordion.test.ts index a1ce41b4a..6f1d1b7b2 100644 --- a/packages/lism-ui/src/components/Accordion/setAccordion.test.ts +++ b/packages/lism-ui/src/components/Accordion/setAccordion.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import setAccordion, { setEvent } from './setAccordion'; +import setAccordion, { setEvent, setAccordionForAstro } from './setAccordion'; vi.mock('../../helper/animation', () => ({ waitFrame: vi.fn(() => Promise.resolve(0)), @@ -9,19 +9,21 @@ vi.mock('../../helper/animation', () => ({ import { waitAnimation } from '../../helper/animation'; -beforeEach(() => { - document.body.innerHTML = ` -
-
-
- -
- +const FIXTURE = ` +
+
+
+ +
+
- `; +
+`; + +beforeEach(() => { + document.body.innerHTML = FIXTURE; vi.mocked(waitAnimation).mockResolvedValue('finished'); }); @@ -296,3 +298,75 @@ describe('setAccordion (default export)', () => { }); }); }); + +// 1回の click でハンドラが動いた回数を、preventDefault の呼び出し回数で数える +const countClickHandlerCalls = (button: HTMLElement): number => { + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + const preventDefault = vi.spyOn(event, 'preventDefault'); + button.dispatchEvent(event); + return preventDefault.mock.calls.length; +}; + +describe('二重登録の防止', () => { + it('同じ item を2回初期化しても、1回の click でハンドラは1回だけ動く', () => { + const item = document.querySelector('.b--accordion_item')!; + const button = item.querySelector('.b--accordion_button')!; + setEvent(item); + setEvent(item); + + expect(countClickHandlerCalls(button)).toBe(1); + }); + + it('クリーンアップ後の再登録は弾かれない(StrictMode の「登録→解除→登録」)', () => { + const item = document.querySelector('.b--accordion_item')!; + const button = item.querySelector('.b--accordion_button')!; + + setEvent(item)(); + const cleanup = setEvent(item); + + expect(countClickHandlerCalls(button)).toBe(1); + + cleanup(); + expect(countClickHandlerCalls(button)).toBe(0); + }); + + it('弾かれた2回目の戻り値を呼んでも、1回目の登録は解除されない', () => { + const item = document.querySelector('.b--accordion_item')!; + const button = item.querySelector('.b--accordion_button')!; + setEvent(item); + + setEvent(item)(); + + expect(countClickHandlerCalls(button)).toBe(1); + }); +}); + +describe('setAccordionForAstro', () => { + const getButton = () => document.querySelector('.b--accordion_button')!; + + it('読み込み時点の要素をすぐ初期化する', () => { + setAccordionForAstro(); + + expect(countClickHandlerCalls(getButton())).toBe(1); + }); + + it('body を差し替えて astro:page-load を発火させると、新しい要素が動く', async () => { + setAccordionForAstro(); + + document.body.innerHTML = FIXTURE; + document.dispatchEvent(new Event('astro:page-load')); + + const item = document.querySelector('.b--accordion_item')!; + getButton().click(); + await vi.waitFor(() => { + expect(item).toHaveAttribute('data-opened'); + }); + }); + + it('astro:page-load で再初期化しても、1回の click でハンドラは1回だけ動く', () => { + setAccordionForAstro(); + document.dispatchEvent(new Event('astro:page-load')); + + expect(countClickHandlerCalls(getButton())).toBe(1); + }); +}); diff --git a/packages/lism-ui/src/components/Accordion/setAccordion.ts b/packages/lism-ui/src/components/Accordion/setAccordion.ts index 76852f644..d93c04bf8 100644 --- a/packages/lism-ui/src/components/Accordion/setAccordion.ts +++ b/packages/lism-ui/src/components/Accordion/setAccordion.ts @@ -79,11 +79,17 @@ function toggleAccordion(accordionItem: HTMLElement): void { } } +// イベント登録済みのアイテム。再初期化で重ねて登録しないよう記録する。 +const registeredItems = new WeakSet(); + /** * 個別のアコーディオンにイベントをセット(React用にクリーンアップ関数を返す) */ export const setEvent = (accordionItem: HTMLElement): (() => void) => { + if (registeredItems.has(accordionItem)) return () => undefined; + const { button, panel } = getAccordionElements(accordionItem); + registeredItems.add(accordionItem); if (panel.hasAttribute('hidden')) { ACCORDION_HIDDEN_VALUE = panel.getAttribute('hidden') ?? 'until-found'; @@ -105,11 +111,13 @@ export const setEvent = (accordionItem: HTMLElement): (() => void) => { return () => { button.removeEventListener('click', _clickEvent); panel.removeEventListener('beforematch', _beforematchEvent); + // 解除後は再登録できるよう記録も消す(ReactのStrictModeは「登録→解除→登録」の順に呼ぶ) + registeredItems.delete(accordionItem); }; }; /** - * ページ内の全アコーディオンにイベントをセット(Astro用) + * ページ内の全アコーディオンにイベントをセット */ const setAccordion = (): void => { const accordionAll = document.querySelectorAll('.b--accordion_item'); @@ -117,4 +125,15 @@ const setAccordion = (): void => { setEvent(accordionItem); }); }; + +/** + * ページ内の全アコーディオンを初期化する(Astro用) + * Point: ClientRouterでの遷移後は script が再実行されないため、astro:page-load でも初期化する。 + * ClientRouterが無いページでは astro:page-load が発火しないため、すぐ初期化する処理も必要。 + */ +export const setAccordionForAstro = (): void => { + setAccordion(); + document.addEventListener('astro:page-load', setAccordion); +}; + export default setAccordion; diff --git a/packages/lism-ui/src/components/Modal/astro/Modal.astro b/packages/lism-ui/src/components/Modal/astro/Modal.astro index ffaa34d83..98a09f6b8 100644 --- a/packages/lism-ui/src/components/Modal/astro/Modal.astro +++ b/packages/lism-ui/src/components/Modal/astro/Modal.astro @@ -17,6 +17,6 @@ const mergedStyle = duration ? { ...(style as object), '--duration': duration } diff --git a/packages/lism-ui/src/components/Modal/setModal.test.ts b/packages/lism-ui/src/components/Modal/setModal.test.ts index b461dcd47..9c73e11d3 100644 --- a/packages/lism-ui/src/components/Modal/setModal.test.ts +++ b/packages/lism-ui/src/components/Modal/setModal.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import setModal, { setEvent } from './setModal'; +import setModal, { setEvent, setModalForAstro } from './setModal'; vi.mock('../../helper/animation', () => ({ waitAnimation: vi.fn(() => Promise.resolve('finished' as const)), @@ -7,13 +7,15 @@ vi.mock('../../helper/animation', () => ({ import { waitAnimation } from '../../helper/animation'; +const FIXTURE = ` + + + + +`; + beforeEach(() => { - document.body.innerHTML = ` - - - - - `; + document.body.innerHTML = FIXTURE; vi.mocked(waitAnimation).mockResolvedValue('finished'); }); @@ -342,3 +344,95 @@ describe('setModal (default export)', () => { }); }); }); + +describe('二重登録の防止', () => { + it('同じ modal を2回初期化しても、イベントを重ねて登録しない', () => { + const modal = document.querySelector('#m1')!; + const trigger = document.querySelector('[data-modal-open="m1"]')!; + const closeTrigger = document.querySelector('[data-modal-close="m1"]')!; + const spies = [modal, trigger, closeTrigger].map((el) => vi.spyOn(el, 'addEventListener')); + + setEvent(modal); + const counts = spies.map((spy) => spy.mock.calls.length); + setEvent(modal); + + expect(counts.every((count) => count > 0)).toBe(true); + expect(spies.map((spy) => spy.mock.calls.length)).toEqual(counts); + }); + + it('modal だけが残った場合(transition:persist)、新しいトリガーを登録する', async () => { + const modal = document.querySelector('#m1')!; + setEvent(modal); + + document.querySelector('[data-modal-open="m1"]')!.remove(); + document.body.insertAdjacentHTML('beforeend', ''); + const newTrigger = document.querySelector('[data-modal-open="m1"]')!; + setEvent(modal); + + newTrigger.click(); + await vi.waitFor(() => { + expect(modal.dataset.isOpen).toBe('1'); + expect(newTrigger.dataset.targetOpened).toBe('1'); + }); + + modal.close(); + expect(newTrigger.dataset.targetOpened).toBeUndefined(); + }); + + it('トリガーだけが残った場合(transition:persist)、新しい modal を開く', async () => { + const oldModal = document.querySelector('#m1')!; + const trigger = document.querySelector('[data-modal-open="m1"]')!; + const oldShowModalSpy = vi.spyOn(oldModal, 'showModal'); + setEvent(oldModal); + + oldModal.remove(); + document.body.insertAdjacentHTML('beforeend', ''); + const newModal = document.querySelector('#m1')!; + setEvent(newModal); + + trigger.click(); + await vi.waitFor(() => { + expect(newModal.dataset.isOpen).toBe('1'); + }); + expect(oldShowModalSpy).not.toHaveBeenCalled(); + }); +}); + +describe('setModalForAstro', () => { + it('読み込み時点の要素をすぐ初期化する', async () => { + setModalForAstro(); + + document.querySelector('[data-modal-open="m1"]')!.click(); + await vi.waitFor(() => { + expect(document.querySelector('#m1')).toHaveAttribute('open'); + }); + }); + + it('body を差し替えて astro:page-load を発火させると、新しい要素が動く', async () => { + setModalForAstro(); + + document.body.innerHTML = FIXTURE; + document.dispatchEvent(new Event('astro:page-load')); + + document.querySelector('[data-modal-open="m1"]')!.click(); + await vi.waitFor(() => { + expect(document.querySelector('#m1')).toHaveAttribute('open'); + }); + }); + + it('modal を開いたまま遷移しても、遷移先で scrollbar-gutter を予約できる', () => { + const root = document.documentElement; + // jsdom は clientWidth が 0 のため、スクロールバーが実幅を持つ環境として扱われる + setModalForAstro(); + document.querySelector('[data-modal-open="m1"]')!.click(); + expect(root.style.scrollbarGutter).toBe('stable'); + + // ClientRouter の遷移を模す: close イベントを経ずに body が入れ替わり、html の属性も遷移先のものになる + document.body.innerHTML = FIXTURE; + root.removeAttribute('style'); + document.dispatchEvent(new Event('astro:page-load')); + + document.querySelector('[data-modal-open="m1"]')!.click(); + expect(root.style.scrollbarGutter).toBe('stable'); + }); +}); diff --git a/packages/lism-ui/src/components/Modal/setModal.ts b/packages/lism-ui/src/components/Modal/setModal.ts index d5c7aa725..dd986d852 100644 --- a/packages/lism-ui/src/components/Modal/setModal.ts +++ b/packages/lism-ui/src/components/Modal/setModal.ts @@ -41,6 +41,21 @@ const unlockScrollbarGutter = (): void => { const isInPageLink = (link: HTMLAnchorElement): boolean => link.href.includes('#') && link.origin === location.origin && link.pathname === location.pathname && link.search === location.search; +/* + * 再初期化で重ねて登録しないよう、登録済みの要素を記録する。 + * modal外にある開くトリガーは、modalと別に記録する。 + * transition:persist で片方だけが残っても、遷移先の新しい要素を登録するため。 + */ +const modalOpeners = new WeakMap void>(); +const registeredOpenTriggers = new WeakSet(); + +// 片方だけが入れ替わっても新しいmodalを開けるよう、対象のmodalはクリック時に探す。 +function onOpenTriggerClick(e: Event): void { + const trigger = e.currentTarget as HTMLElement; + const modal = document.getElementById(trigger.getAttribute('data-modal-open') ?? ''); + if (modal instanceof HTMLDialogElement) modalOpeners.get(modal)?.(trigger); +} + /** dialogの開閉、トリガー状態の復元、背景クリック、ページ内リンク、Esc操作を設定する。 */ export function setEvent(target: HTMLElement): void { // 対象がない、またはidがない場合は処理を終了 @@ -51,11 +66,19 @@ export function setEvent(target: HTMLElement): void { const modal = target; + // openボタンにイベント登録 + document.querySelectorAll(`[data-modal-open="${modal.id}"]`).forEach((trigger) => { + if (registeredOpenTriggers.has(trigger)) return; + registeredOpenTriggers.add(trigger); + trigger.addEventListener('click', onOpenTriggerClick); + }); + + if (modalOpeners.has(modal)) return; + // オープンした時のトリガー要素を記憶する(data属性を戻すため) let theTrigger: HTMLElement | null = null; - // モーダルを開くトリガーと閉じるトリガーを取得 - const openTriggers = document.querySelectorAll(`[data-modal-open="${modal.id}"]`); + // モーダルを閉じるトリガーを取得 const closeTriggers = modal.querySelectorAll(`[data-modal-close="${modal.id}"]`); // scrollbar幅を固定してdialogを開き、次フレームで開始属性を付ける。 @@ -92,16 +115,14 @@ export function setEvent(target: HTMLElement): void { unlockScrollbarGutter(); }; - // openボタンにイベント登録 - openTriggers.forEach((trigger) => { - trigger?.addEventListener('click', () => { - // button側にもdata属性付与 - trigger.dataset.targetOpened = '1'; - theTrigger = trigger; // close() 時にdata属性削除するために記憶 + // openボタンのクリックで呼ばれる処理 + modalOpeners.set(modal, (trigger) => { + // button側にもdata属性付与 + trigger.dataset.targetOpened = '1'; + theTrigger = trigger; // close() 時にdata属性削除するために記憶 - // モーダルを開く - openDialog(); - }); + // モーダルを開く + openDialog(); }); // closeボタンにイベント登録 @@ -156,9 +177,23 @@ export function setEvent(target: HTMLElement): void { } const setModal = () => { + // modalを開いたままClientRouterで遷移すると、closeイベントが発火せず予約状態だけが残る。 + if (!document.querySelector('.b--modal[open]')) unlockScrollbarGutter(); + const modals = document.querySelectorAll('.b--modal'); modals?.forEach((target) => { setEvent(target as HTMLElement); }); }; + +/** + * ページ内の全モーダルを初期化する(Astro用) + * Point: ClientRouterでの遷移後は script が再実行されないため、astro:page-load でも初期化する。 + * ClientRouterが無いページでは astro:page-load が発火しないため、すぐ初期化する処理も必要。 + */ +export const setModalForAstro = (): void => { + setModal(); + document.addEventListener('astro:page-load', setModal); +}; + export default setModal; diff --git a/packages/lism-ui/src/components/Tabs/astro/Root.astro b/packages/lism-ui/src/components/Tabs/astro/Root.astro index 9a2b880d8..7f7b5efa6 100644 --- a/packages/lism-ui/src/components/Tabs/astro/Root.astro +++ b/packages/lism-ui/src/components/Tabs/astro/Root.astro @@ -50,9 +50,6 @@ const hasItems = btns.length > 0; diff --git a/packages/lism-ui/src/components/Tabs/setTabs.test.ts b/packages/lism-ui/src/components/Tabs/setTabs.test.ts index 23634a34b..72c5cc6e1 100644 --- a/packages/lism-ui/src/components/Tabs/setTabs.test.ts +++ b/packages/lism-ui/src/components/Tabs/setTabs.test.ts @@ -1,20 +1,22 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import setTabs from './setTabs'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import setTabs, { setTabsForAstro } from './setTabs'; // Tab.astro / Panel.astro が出力するマークアップを模したフィクスチャ -beforeEach(() => { - document.body.innerHTML = ` -
-
- - - -
-
- - +const FIXTURE = ` +
+
+ + +
- `; +
+ + +
+`; + +beforeEach(() => { + document.body.innerHTML = FIXTURE; history.replaceState({}, '', '/'); }); @@ -163,3 +165,60 @@ describe('setTabs / ディープリンク', () => { expectSelected(0); }); }); + +describe('setTabs / 二重初期化の防止', () => { + it('同じ要素を2回初期化しても、1回の click でハンドラは1回だけ動く', () => { + const { tabs, tabBtns } = getEls(); + setTabs(tabs); + setTabs(tabs); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + const preventDefault = vi.spyOn(event, 'preventDefault'); + tabBtns[1].dispatchEvent(event); + + expect(preventDefault).toHaveBeenCalledTimes(1); + expectSelected(1); + }); + + it('初期化済みの要素を再初期化しても、ディープリンクで選択を戻さない', () => { + const { tabs, tabBtns } = getEls(); + + history.replaceState({}, '', '/?lism-tab=panel2'); + setTabs(tabs); + tabBtns[2].click(); + setTabs(tabs); + + expectSelected(2); + }); +}); + +describe('setTabsForAstro', () => { + it('読み込み時点の要素をすぐ初期化する', () => { + setTabsForAstro(); + + getEls().tabBtns[1].click(); + + expectSelected(1); + }); + + it('body を差し替えて astro:page-load を発火させると、新しい要素が動く', () => { + setTabsForAstro(); + + document.body.innerHTML = FIXTURE; + document.dispatchEvent(new Event('astro:page-load')); + getEls().tabBtns[1].click(); + + expectSelected(1); + }); + + it('astro:page-load で再初期化しても、1回の click でハンドラは1回だけ動く', () => { + setTabsForAstro(); + document.dispatchEvent(new Event('astro:page-load')); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + const preventDefault = vi.spyOn(event, 'preventDefault'); + getEls().tabBtns[1].dispatchEvent(event); + + expect(preventDefault).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/lism-ui/src/components/Tabs/setTabs.ts b/packages/lism-ui/src/components/Tabs/setTabs.ts index f35758cae..10e8a4d30 100644 --- a/packages/lism-ui/src/components/Tabs/setTabs.ts +++ b/packages/lism-ui/src/components/Tabs/setTabs.ts @@ -57,7 +57,13 @@ function tabKeyControl(e: KeyboardEvent): void { nextBtn.focus(); } +// 初期化済みのタブ。再初期化でディープリンクが選択を戻さないよう記録する。 +const initializedTabs = new WeakSet(); + function setTabs(tabs: HTMLElement): void { + if (initializedTabs.has(tabs)) return; + initializedTabs.add(tabs); + const tabBtns = getTabBtns(tabs); tabBtns.forEach((tabBtn) => { tabBtn.addEventListener('click', tabControl); @@ -75,4 +81,20 @@ function setTabs(tabs: HTMLElement): void { if (target) selectTab(target); } +const setTabsAll = (): void => { + document.querySelectorAll('.b--tabs').forEach((tabs) => { + setTabs(tabs); + }); +}; + +/** + * ページ内の全タブを初期化する(Astro用) + * Point: ClientRouterでの遷移後は script が再実行されないため、astro:page-load でも初期化する。 + * ClientRouterが無いページでは astro:page-load が発火しないため、すぐ初期化する処理も必要。 + */ +export const setTabsForAstro = (): void => { + setTabsAll(); + document.addEventListener('astro:page-load', setTabsAll); +}; + export default setTabs; From 20725ad648c58a456eda5e18844436dff4344618 Mon Sep 17 00:00:00 2001 From: ddryo <31400297+ddryo@users.noreply.github.com> Date: Wed, 30 Sep 2026 07:33:40 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(lism-ui):=20Modal=E3=82=92=E9=96=8B?= =?UTF-8?q?=E3=81=84=E3=81=9F=E3=81=BE=E3=81=BEClientRouter=E3=81=A7?= =?UTF-8?q?=E9=81=B7=E7=A7=BB=E3=81=97=E3=81=9F=E9=9A=9B=E3=80=81=E9=81=B7?= =?UTF-8?q?=E7=A7=BB=E5=85=88=E3=81=AEscrollbar-gutter=E3=82=92=E9=81=B7?= =?UTF-8?q?=E7=A7=BB=E5=85=83=E3=81=AE=E5=80=A4=E3=81=A7=E4=B8=8A=E6=9B=B8?= =?UTF-8?q?=E3=81=8D=E3=81=97=E3=81=AA=E3=81=84=20(#640)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01F8f9Y4zoYQNwPMT8C1hsXd --- packages/lism-ui/src/components/Modal/setModal.test.ts | 4 +++- packages/lism-ui/src/components/Modal/setModal.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/lism-ui/src/components/Modal/setModal.test.ts b/packages/lism-ui/src/components/Modal/setModal.test.ts index 9c73e11d3..76c702c16 100644 --- a/packages/lism-ui/src/components/Modal/setModal.test.ts +++ b/packages/lism-ui/src/components/Modal/setModal.test.ts @@ -429,8 +429,10 @@ describe('setModalForAstro', () => { // ClientRouter の遷移を模す: close イベントを経ずに body が入れ替わり、html の属性も遷移先のものになる document.body.innerHTML = FIXTURE; - root.removeAttribute('style'); + root.setAttribute('style', 'scrollbar-gutter: stable both-edges'); document.dispatchEvent(new Event('astro:page-load')); + // 遷移元の値で上書きしない + expect(root.style.scrollbarGutter).toBe('stable both-edges'); document.querySelector('[data-modal-open="m1"]')!.click(); expect(root.style.scrollbarGutter).toBe('stable'); diff --git a/packages/lism-ui/src/components/Modal/setModal.ts b/packages/lism-ui/src/components/Modal/setModal.ts index dd986d852..5cfe9e0ba 100644 --- a/packages/lism-ui/src/components/Modal/setModal.ts +++ b/packages/lism-ui/src/components/Modal/setModal.ts @@ -178,7 +178,8 @@ export function setEvent(target: HTMLElement): void { const setModal = () => { // modalを開いたままClientRouterで遷移すると、closeイベントが発火せず予約状態だけが残る。 - if (!document.querySelector('.b--modal[open]')) unlockScrollbarGutter(); + // htmlの属性は遷移先の値に置き換わっているため、遷移元の値を書き戻さずに破棄する。 + if (!document.querySelector('.b--modal[open]')) savedScrollbarGutter = null; const modals = document.querySelectorAll('.b--modal'); modals?.forEach((target) => {