Init
This commit is contained in:
2
src/MantineBetterMenu/MantineBetterMenu.d.ts
vendored
Normal file
2
src/MantineBetterMenu/MantineBetterMenu.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import { type MenuStoreProps } from './Store';
|
||||
export declare function MantineBetterMenusProvider(props: React.PropsWithChildren<MenuStoreProps>): import("react/jsx-runtime").JSX.Element;
|
||||
51
src/MantineBetterMenu/MantineBetterMenu.stories.tsx
Normal file
51
src/MantineBetterMenu/MantineBetterMenu.stories.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { Button } from '@mantine/core';
|
||||
import { fn } from 'storybook/test';
|
||||
|
||||
import { MantineBetterMenusProvider, useMantineBetterMenus } from './';
|
||||
|
||||
|
||||
const Renderable = (props: Record<string,unknown>) => {
|
||||
return (
|
||||
<MantineBetterMenusProvider providerID='test' {...props} >
|
||||
<Menu/>
|
||||
</MantineBetterMenusProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const Menu = () => {
|
||||
const menus = useMantineBetterMenus();
|
||||
//menus.setState("menus",[{id:"test",items:[{id:"1",label:"Test",onClick:()=>{console.log("Clicked")}}]}])
|
||||
|
||||
return <Button onClick={()=> menus.show("test",{})}>Menu</Button>;
|
||||
}
|
||||
|
||||
const meta = {
|
||||
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
|
||||
args: { onClick: fn() },
|
||||
// More on argTypes: https://storybook.js.org/docs/api/argtypes
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
component: Renderable,
|
||||
parameters: {
|
||||
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: 'centered',
|
||||
},
|
||||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
tags: ['autodocs'],
|
||||
title: 'UI/Mantine Better Menu',
|
||||
} satisfies Meta<typeof Renderable>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
||||
export const BasicExample: Story = {
|
||||
args: {
|
||||
|
||||
label: 'Test',
|
||||
},
|
||||
};
|
||||
|
||||
11
src/MantineBetterMenu/MantineBetterMenu.tsx
Normal file
11
src/MantineBetterMenu/MantineBetterMenu.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { MenuRenderer } from './MenuRenderer';
|
||||
import { MantineBetterMenusStoreProvider, type MenuStoreProps } from './Store';
|
||||
|
||||
export function MantineBetterMenusProvider(props: React.PropsWithChildren<MenuStoreProps>) {
|
||||
return (
|
||||
<MantineBetterMenusStoreProvider {...props}>
|
||||
<MenuRenderer />
|
||||
{props.children}
|
||||
</MantineBetterMenusStoreProvider>
|
||||
);
|
||||
}
|
||||
1
src/MantineBetterMenu/MenuRenderer.d.ts
vendored
Normal file
1
src/MantineBetterMenu/MenuRenderer.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export declare function MenuRenderer(): import("react/jsx-runtime").JSX.Element;
|
||||
90
src/MantineBetterMenu/MenuRenderer.tsx
Normal file
90
src/MantineBetterMenu/MenuRenderer.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Menu, Portal } from '@mantine/core';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { type MantineBetterMenuInstanceItem, useMantineBetterMenus } from './Store';
|
||||
|
||||
export function MenuRenderer() {
|
||||
const { menus, providerID, setInstanceState } = useMantineBetterMenus((s) => ({
|
||||
menus: s.menus,
|
||||
providerID: s.providerID,
|
||||
setInstanceState: s.setInstanceState,
|
||||
setState: s.setState
|
||||
}));
|
||||
|
||||
return (
|
||||
<Portal id={`bmm_portal_${providerID}`} key={`bmm_portal_${providerID}`}>
|
||||
{React.Children.toArray(
|
||||
menus?.map((m, menuIndex) => {
|
||||
return (
|
||||
<Menu
|
||||
shadow="md"
|
||||
width={200}
|
||||
{...m.menuProps}
|
||||
key={`bmm_menu_${providerID}_${menuIndex}`}
|
||||
onClose={() => {
|
||||
setInstanceState(m.id, 'visible', false);
|
||||
m.menuProps?.onClose?.();
|
||||
}}
|
||||
opened={m.visible}
|
||||
>
|
||||
<Menu.Target>
|
||||
<div
|
||||
id={`bmm_${providerID}_${menuIndex}_target`}
|
||||
style={{ left: m.x, position: 'fixed', top: m.y, visibility: 'hidden' }}
|
||||
/>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
{m.renderer
|
||||
? m.renderer
|
||||
: React.Children.toArray(
|
||||
m.items?.map((item, itemIndex) => (
|
||||
<MenuItemRenderer
|
||||
key={`bmm_${providerID}_${menuIndex}_item${itemIndex}`}
|
||||
{...item}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
const MenuItemRenderer = ({ children, label, ...props }: MantineBetterMenuInstanceItem) => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
if (typeof props.renderer === 'function') {
|
||||
return props.renderer({ ...props, loading, renderer: undefined, setLoading });
|
||||
}
|
||||
if (typeof props.renderer === 'object') {
|
||||
return props.renderer;
|
||||
}
|
||||
|
||||
if (props.isDivider) {
|
||||
return <Menu.Divider />;
|
||||
}
|
||||
|
||||
if (!props.onClick && !props.onClickAsync) {
|
||||
return <Menu.Label {...(props as Record<string,unknown>)}> {children ?? label}</Menu.Label>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
{...props}
|
||||
disabled={loading}
|
||||
onClick={(e) => {
|
||||
props.onClick?.(e );
|
||||
if (props.onClickAsync) {
|
||||
setLoading(true);
|
||||
props.onClickAsync().finally(() => setLoading(false));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children ?? label}
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
||||
43
src/MantineBetterMenu/Store.d.ts
vendored
Normal file
43
src/MantineBetterMenu/Store.d.ts
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
import { type MenuItemProps, type MenuProps } from '@mantine/core';
|
||||
import { type ReactNode } from 'react';
|
||||
export interface MantineBetterMenuInstance {
|
||||
id: string;
|
||||
items?: Array<MantineBetterMenuInstanceItem>;
|
||||
menuProps?: MenuProps;
|
||||
renderer?: ReactNode;
|
||||
visible: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
export interface MantineBetterMenuInstanceItem extends Partial<MenuItemProps> {
|
||||
isDivider?: boolean;
|
||||
label?: string;
|
||||
onClick?: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onClickAsync?: () => Promise<void>;
|
||||
renderer?: ((props: MantineBetterMenuInstanceItem & Record<string, unknown>) => ReactNode) | ReactNode;
|
||||
}
|
||||
export interface MenuStoreProps {
|
||||
providerID?: string;
|
||||
}
|
||||
export type MenuStoreState = MenuStoreProps & MenuStoreStateOnly;
|
||||
export interface MenuStoreStateOnly {
|
||||
hide: (id: string) => void;
|
||||
menus: Array<MantineBetterMenuInstance>;
|
||||
setInstanceState: <K extends keyof MantineBetterMenuInstance>(instanceID: string, key: K, value: MantineBetterMenuInstance[K]) => void;
|
||||
setState: <K extends keyof MenuStoreState>(key: K, value: Partial<MenuStoreState[K]>) => void;
|
||||
show: (id: string, options?: Partial<MantineBetterMenuInstance>) => void;
|
||||
}
|
||||
declare const MantineBetterMenusStoreProvider: (props: {
|
||||
children: ReactNode;
|
||||
} & {
|
||||
firstSyncProps?: string[];
|
||||
persist?: import("zustand/middleware").PersistOptions<Partial<MenuStoreProps & MenuStoreStateOnly>, Partial<MenuStoreProps & MenuStoreStateOnly>, unknown> | undefined;
|
||||
} & MenuStoreProps) => React.ReactNode, useMantineBetterMenus: {
|
||||
(): {
|
||||
$sync?: ((props: MenuStoreProps) => void) | undefined;
|
||||
} & MenuStoreProps & MenuStoreStateOnly;
|
||||
<U>(selector: (state: {
|
||||
$sync?: ((props: MenuStoreProps) => void) | undefined;
|
||||
} & MenuStoreProps & MenuStoreStateOnly) => U, equalityFn?: ((a: U, b: U) => boolean) | undefined): U;
|
||||
};
|
||||
export { MantineBetterMenusStoreProvider, useMantineBetterMenus };
|
||||
104
src/MantineBetterMenu/Store.tsx
Normal file
104
src/MantineBetterMenu/Store.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import { type MenuItemProps, type MenuProps } from '@mantine/core';
|
||||
import { getUUID } from '@warkypublic/artemis-kit';
|
||||
import { createSyncStore } from '@warkypublic/zustandsyncstore';
|
||||
import { produce } from 'immer';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
export interface MantineBetterMenuInstance {
|
||||
id: string;
|
||||
items?: Array<MantineBetterMenuInstanceItem>;
|
||||
menuProps?: MenuProps;
|
||||
renderer?: ReactNode;
|
||||
visible: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface MantineBetterMenuInstanceItem extends Partial<MenuItemProps> {
|
||||
isDivider?: boolean;
|
||||
label?: string;
|
||||
onClick?: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onClickAsync?: () => Promise<void>;
|
||||
renderer?:
|
||||
| ((props: MantineBetterMenuInstanceItem & Record<string, unknown>) => ReactNode)
|
||||
| ReactNode;
|
||||
}
|
||||
|
||||
export interface MenuStoreProps {
|
||||
menus?: Array<MantineBetterMenuInstance>;
|
||||
providerID?: string;
|
||||
}
|
||||
|
||||
export type MenuStoreState = MenuStoreProps & MenuStoreStateOnly;
|
||||
|
||||
export interface MenuStoreStateOnly {
|
||||
hide: (id: string) => void;
|
||||
menus: Array<MantineBetterMenuInstance>;
|
||||
setInstanceState: <K extends keyof MantineBetterMenuInstance>(
|
||||
instanceID: string,
|
||||
key: K,
|
||||
value: MantineBetterMenuInstance[K]
|
||||
) => void;
|
||||
setState: <K extends keyof MenuStoreState>(key: K, value: Partial<MenuStoreState[K]>) => void;
|
||||
show: (id: string, options?: Partial<MantineBetterMenuInstance>) => void;
|
||||
}
|
||||
|
||||
const { Provider:MantineBetterMenusStoreProvider, useStore:useMantineBetterMenus } = createSyncStore<MenuStoreState, MenuStoreProps>(
|
||||
(set, get) => ({
|
||||
hide: (id: string) => {
|
||||
const s = get();
|
||||
s.setInstanceState(id, 'visible', false);
|
||||
},
|
||||
menus: [],
|
||||
setInstanceState: (id, key, value) => {
|
||||
//@ts-expect-error Type instantiation is excessively deep and possibly infinite.
|
||||
set(
|
||||
produce((state: MenuStoreState) => {
|
||||
const idx = state?.menus?.findIndex((m: MantineBetterMenuInstance) => m.id === id);
|
||||
if (idx >= 0) {
|
||||
state.menus[idx][key] = value;
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
setState: (key, value) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
state[key] = value;
|
||||
})
|
||||
);
|
||||
},
|
||||
show: (id: string, options?: Partial<Omit<MantineBetterMenuInstance, 'id'>>) => {
|
||||
const s = get();
|
||||
const menuIndex = s.menus.findIndex((m) => m.id === id);
|
||||
const menu: Partial<MantineBetterMenuInstance> = s.menus[menuIndex]
|
||||
? { ...s.menus[menuIndex] }
|
||||
: {};
|
||||
|
||||
Object.assign(menu, options);
|
||||
menu.id = menu.id ?? id;
|
||||
menu.visible = !(menu.visible ?? false);
|
||||
|
||||
if (menuIndex < 0) {
|
||||
s.setState('menus', [...s.menus, menu as MantineBetterMenuInstance]);
|
||||
} else {
|
||||
set(
|
||||
produce((state: MenuStoreState) => {
|
||||
if (!state.menus) {
|
||||
state.menus = [];
|
||||
}
|
||||
state.menus[menuIndex] = { ...state.menus[menuIndex], ...menu };
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}),
|
||||
(props) => {
|
||||
return {
|
||||
providerID: props.providerID ?? `MenuStore-${getUUID()}`
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export { MantineBetterMenusStoreProvider,useMantineBetterMenus };
|
||||
3
src/MantineBetterMenu/index.d.ts
vendored
Normal file
3
src/MantineBetterMenu/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export { MantineBetterMenusProvider } from './MantineBetterMenu';
|
||||
export { useMantineBetterMenus } from './Store';
|
||||
export type { MantineBetterMenuInstance, MantineBetterMenuInstanceItem, MenuStoreState } from './Store';
|
||||
7
src/MantineBetterMenu/index.ts
Normal file
7
src/MantineBetterMenu/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { MantineBetterMenusProvider } from './MantineBetterMenu';
|
||||
export { useMantineBetterMenus } from './Store';
|
||||
export type {
|
||||
MantineBetterMenuInstance,
|
||||
MantineBetterMenuInstanceItem,
|
||||
MenuStoreState
|
||||
} from './Store';
|
||||
Reference in New Issue
Block a user