feat(context-menu): add ReactiveCheckboxItem with useSyncExternalStore

This commit is contained in:
Jannis Braun
2026-03-24 21:47:52 +01:00
parent 2513ba10e7
commit 7a25a2df7b
@@ -1,9 +1,10 @@
import React, { useEffect, useLayoutEffect, useRef, useCallback, useState } from 'react'; import React, { useEffect, useLayoutEffect, useRef, useCallback, useState, useSyncExternalStore } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { import {
useContextMenuStore, useContextMenuStore,
filterMenuItems, filterMenuItems,
type ContextMenuItem, type ContextMenuItem,
type ContextMenuCheckbox,
type ContextMenuLeafItem, type ContextMenuLeafItem,
type ContextMenuSubmenu, type ContextMenuSubmenu,
} from '../../stores/contextMenuStore'; } from '../../stores/contextMenuStore';
@@ -41,6 +42,35 @@ function CheckboxIndicator({ checked }: { checked: boolean }) {
); );
} }
// ── Reactive checkbox wrapper ───────────────────────────────────────────────
interface ReactiveCheckboxItemProps {
item: ContextMenuCheckbox;
isMobile?: boolean;
}
function ReactiveCheckboxItem({ item, isMobile }: ReactiveCheckboxItemProps) {
const checked = useSyncExternalStore(item.subscribe, item.getChecked);
const className = isMobile
? `${MOBILE_ITEM_CLASS} text-txt-primary`
: ITEM_CLASS;
return (
<button
className={className}
style={isMobile ? undefined : ITEM_STYLE}
onClick={(e) => {
e.stopPropagation();
item.onChange(!checked);
}}
>
<span className="flex-1">{item.label}</span>
<CheckboxIndicator checked={checked} />
</button>
);
}
// ── Desktop submenu flyout ─────────────────────────────────────────────────── // ── Desktop submenu flyout ───────────────────────────────────────────────────
interface SubmenuFlyoutProps { interface SubmenuFlyoutProps {
@@ -115,19 +145,7 @@ function DesktopLeafItem({ item, close }: DesktopLeafItemProps) {
return <div>{item.render()}</div>; return <div>{item.render()}</div>;
case 'checkbox': case 'checkbox':
return ( return <ReactiveCheckboxItem item={item} />;
<button
className={ITEM_CLASS}
style={ITEM_STYLE}
onClick={(e) => {
e.stopPropagation();
item.onChange(!item.checked);
}}
>
<span className="flex-1">{item.label}</span>
<CheckboxIndicator checked={item.checked} />
</button>
);
case 'action': { case 'action': {
const className = item.disabled const className = item.disabled
@@ -413,18 +431,7 @@ function MobileLeafItem({ item, close }: MobileLeafItemProps) {
return <div>{item.render()}</div>; return <div>{item.render()}</div>;
case 'checkbox': case 'checkbox':
return ( return <ReactiveCheckboxItem item={item} isMobile />;
<button
className={`${MOBILE_ITEM_CLASS} text-txt-primary`}
onClick={(e) => {
e.stopPropagation();
item.onChange(!item.checked);
}}
>
<span className="flex-1">{item.label}</span>
<CheckboxIndicator checked={item.checked} />
</button>
);
case 'action': { case 'action': {
const colorClass = item.danger ? 'text-txt-danger' : 'text-txt-primary'; const colorClass = item.danger ? 'text-txt-danger' : 'text-txt-primary';