feat(context-menu): add ReactiveCheckboxItem with useSyncExternalStore
This commit is contained in:
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user