feat: refactor select model

This commit is contained in:
butterfly
2024-04-29 16:29:47 +08:00
parent c34b8ab919
commit 8c28c408d8
18 changed files with 501 additions and 239 deletions

View File

@@ -2,7 +2,7 @@ import * as React from "react";
export type ButtonType = "primary" | "danger" | null;
export default function Btn(props: {
export interface BtnProps {
onClick?: () => void;
icon?: JSX.Element;
type?: ButtonType;
@@ -14,7 +14,9 @@ export default function Btn(props: {
disabled?: boolean;
tabIndex?: number;
autoFocus?: boolean;
}) {
}
export default function Btn(props: BtnProps) {
const {
onClick,
icon,

View File

@@ -1,136 +0,0 @@
import React, { useState } from "react";
import { createRoot } from "react-dom/client";
import * as AlertDialog from "@radix-ui/react-alert-dialog";
import Warning from "@/app/icons/warning.svg";
import Btn from "@/app/components/Btn";
interface ConfirmProps {
onOk?: () => Promise<void> | void;
onCancel?: () => void;
okText: string;
cancelText: string;
content: React.ReactNode;
title: React.ReactNode;
visible?: boolean;
}
const baseZIndex = 150;
const Confirm = (props: ConfirmProps) => {
const { visible, onOk, onCancel, okText, cancelText, content, title } = props;
const [open, setOpen] = useState(false);
const mergeOpen = visible ?? open;
return (
<AlertDialog.Root open={mergeOpen} onOpenChange={setOpen}>
<AlertDialog.Portal>
<AlertDialog.Overlay
className="bg-confirm-mask fixed inset-0 animate-mask "
style={{ zIndex: baseZIndex - 1 }}
/>
<AlertDialog.Content
className={`
fixed inset-0 flex flex-col item-start top-0 left-[50vw] translate-x-[-50%]
`}
style={{ zIndex: baseZIndex - 1 }}
>
<div className="flex-1">&nbsp;</div>
<div
className={`flex flex-col
bg-confirm-panel text-confirm-mask
md:p-6 md:w-confirm md:gap-6 md:rounded-lg
`}
>
<AlertDialog.Title
className={`
flex items-center justify-start gap-3 font-common
md:text-chat-header-title md:font-bold md:leading-5
`}
>
<Warning />
{title}
</AlertDialog.Title>
<AlertDialog.Description
className={`
font-common font-normal
md:text-sm-title md:leading-[158%]
`}
>
{content}
</AlertDialog.Description>
<div
style={{ display: "flex", gap: 10, justifyContent: "flex-end" }}
>
<AlertDialog.Cancel asChild>
<Btn
className={`
md:px-4 md:py-2.5 bg-delete-chat-cancel-btn border border-delete-chat-cancel-btn text-text-delete-chat-cancel-btn rounded-md
`}
onClick={() => {
setOpen(false);
onCancel?.();
}}
text={cancelText}
/>
</AlertDialog.Cancel>
<AlertDialog.Action asChild>
<Btn
className={`
md:px-4 md:py-2.5 bg-delete-chat-ok-btn text-text-delete-chat-ok-btn rounded-md
`}
onClick={() => {
const toDo = onOk?.();
if (toDo instanceof Promise) {
toDo.then(() => {
setOpen(false);
});
} else {
setOpen(false);
}
}}
text={okText}
/>
</AlertDialog.Action>
</div>
</div>
<div className="flex-1">&nbsp;</div>
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog.Root>
);
};
const div = document.createElement("div");
div.id = "confirm-root";
div.style.height = "0px";
document.body.appendChild(div);
const show = (props: Omit<ConfirmProps, "visible" | "onCancel" | "onOk">) => {
const root = createRoot(div);
const closeModal = () => {
root.unmount();
};
return new Promise<boolean>((resolve) => {
root.render(
<Confirm
{...props}
visible={true}
onCancel={() => {
closeModal();
resolve(false);
}}
onOk={() => {
closeModal();
resolve(true);
}}
/>,
);
});
};
Confirm.show = show;
export default Confirm;

View File

@@ -55,11 +55,11 @@ export default function Input(props: CommonInputProps & InputProps) {
return (
<div
className={`w-[100%] rounded-chat-input bg-input flex gap-3 items-center px-3 py-2 ${className}`}
className={` group/input w-[100%] rounded-chat-input bg-input flex gap-3 items-center px-3 py-2 ${className} hover:bg-select-hover`}
>
<input
{...rest}
className=" overflow-hidden text-text-input text-sm-title leading-input outline-none flex-1"
className=" overflow-hidden text-text-input text-sm-title leading-input outline-none flex-1 group-hover/input:bg-input-input-ele-hover"
type={internalType}
value={value}
onChange={(e) => {

View File

@@ -0,0 +1,261 @@
import React, { useState } from "react";
import { createRoot } from "react-dom/client";
import * as AlertDialog from "@radix-ui/react-alert-dialog";
import Btn, { BtnProps } from "@/app/components/Btn";
import Warning from "@/app/icons/warning.svg";
import Close from "@/app/icons/closeIcon.svg";
export interface ModalProps {
onOk?: () => void;
onCancel?: () => void;
okText?: string;
cancelText?: string;
okBtnProps?: BtnProps;
cancelBtnProps?: BtnProps;
content?: React.ReactNode;
title?: React.ReactNode;
visible?: boolean;
noFooter?: boolean;
noHeader?: boolean;
isMobile?: boolean;
closeble?: boolean;
type?: "modal" | "bottom-drawer";
headerBordered?: boolean;
}
export interface WarnProps
extends Omit<
ModalProps,
| "closeble"
| "isMobile"
| "noHeader"
| "noFooter"
| "onOk"
| "okBtnProps"
| "cancelBtnProps"
> {
onOk?: () => Promise<void> | void;
}
const baseZIndex = 150;
const Modal = (props: ModalProps) => {
const {
onOk,
onCancel,
okText,
cancelText,
content,
title,
visible,
noFooter,
noHeader,
closeble = true,
okBtnProps,
cancelBtnProps,
type = "modal",
headerBordered,
} = props;
const [open, setOpen] = useState(false);
const mergeOpen = visible ?? open;
const handleClose = () => {
setOpen(false);
onCancel?.();
};
let layoutClassName = "";
let panelClassName = "";
let titleClassName = "";
let footerClassName = "";
switch (type) {
case "bottom-drawer":
layoutClassName =
"fixed inset-0 flex flex-col item-start top-0 left-[50vw] translate-x-[-50%] md:";
panelClassName = "";
titleClassName = "";
footerClassName = "";
break;
case "modal":
default:
layoutClassName =
"fixed inset-0 flex flex-col item-start top-0 left-[50vw] translate-x-[-50%] max-sm:w-modal-modal-type-mobile";
panelClassName = "rounded-lg px-6 sm:w-modal-modal-type";
titleClassName = "py-6 max-sm:pb-3";
footerClassName = "py-6";
}
const btnCommonClass = "px-4 py-2.5 rounded-md max-sm:flex-1";
const { className: okBtnClass } = okBtnProps || {};
const { className: cancelBtnClass } = cancelBtnProps || {};
return (
<AlertDialog.Root open={mergeOpen} onOpenChange={setOpen}>
<AlertDialog.Portal>
<AlertDialog.Overlay
className="bg-modal-mask fixed inset-0 animate-mask "
style={{ zIndex: baseZIndex - 1 }}
/>
<AlertDialog.Content
className={`
${layoutClassName}
`}
style={{ zIndex: baseZIndex - 1 }}
>
<div className="flex-1">&nbsp;</div>
<div
className={`flex flex-col
bg-moda-panel text-modal-mask
${headerBordered ? " border-b border-modal-header-bottom" : ""}
${panelClassName}
`}
>
{!noHeader && (
<AlertDialog.Title
className={`
flex items-center justify-between gap-3 font-common
md:text-chat-header-title md:font-bold md:leading-5
${titleClassName}
`}
>
<div className="flex gap-3 justify-start flex-1 items-center">
{title}
</div>
{closeble && (
<div
className="items-center"
onClick={() => {
handleClose();
}}
>
<Close />
</div>
)}
</AlertDialog.Title>
)}
{content}
{!noFooter && (
<div
className={`
flex gap-3 sm:justify-end max-sm:justify-between
${footerClassName}
`}
>
<AlertDialog.Cancel asChild>
<Btn
{...cancelBtnProps}
onClick={() => handleClose()}
text={cancelText}
className={`${btnCommonClass} ${cancelBtnClass}`}
/>
</AlertDialog.Cancel>
<AlertDialog.Action asChild>
<Btn
{...okBtnProps}
onClick={() => {
setOpen(false);
onOk?.();
}}
text={okText}
className={`${btnCommonClass} ${okBtnClass}`}
/>
</AlertDialog.Action>
</div>
)}
</div>
<div className="flex-1">&nbsp;</div>
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog.Root>
);
};
export const Warn = ({
title,
onOk,
visible,
content,
...props
}: WarnProps) => {
const [internalVisible, setVisible] = useState(visible);
return (
<Modal
{...props}
title={
<>
<Warning />
{title}
</>
}
content={
<AlertDialog.Description
className={`
font-common font-normal
md:text-sm-title md:leading-[158%]
`}
>
{content}
</AlertDialog.Description>
}
closeble={false}
onOk={() => {
const toDo = onOk?.();
if (toDo instanceof Promise) {
toDo.then(() => {
setVisible(false);
});
} else {
setVisible(false);
}
}}
visible={internalVisible}
okBtnProps={{
className: `bg-delete-chat-ok-btn text-text-delete-chat-ok-btn `,
}}
cancelBtnProps={{
className: `bg-delete-chat-cancel-btn border border-delete-chat-cancel-btn text-text-delete-chat-cancel-btn`,
}}
/>
);
};
const div = document.createElement("div");
div.id = "confirm-root";
div.style.height = "0px";
document.body.appendChild(div);
Modal.warn = (props: Omit<WarnProps, "visible" | "onCancel" | "onOk">) => {
const root = createRoot(div);
const closeModal = () => {
root.unmount();
};
return new Promise<boolean>((resolve) => {
root.render(
<Warn
{...props}
visible={true}
onCancel={() => {
closeModal();
resolve(false);
}}
onOk={() => {
closeModal();
resolve(true);
}}
/>,
);
});
};
const Trigger = (props: ModalProps) => {
return <></>;
};
Modal.Trigger = Trigger;
export default Modal;

View File

@@ -1,5 +1,12 @@
import useRelativePosition from "@/app/hooks/useRelativePosition";
import { RefObject, useEffect, useMemo, useRef, useState } from "react";
import {
RefObject,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { createPortal } from "react-dom";
const ArrowIcon = ({ sibling }: { sibling: RefObject<HTMLDivElement> }) => {
@@ -55,6 +62,7 @@ export interface PopoverProps {
noArrow?: boolean;
delayClose?: number;
useGlobalRoot?: boolean;
getPopoverPanelRef?: (ref: RefObject<HTMLDivElement>) => void;
}
export default function Popover(props: PopoverProps) {
@@ -70,6 +78,7 @@ export default function Popover(props: PopoverProps) {
noArrow = false,
delayClose = 0,
useGlobalRoot,
getPopoverPanelRef,
} = props;
const [internalShow, setShow] = useState(false);
@@ -175,10 +184,14 @@ export default function Popover(props: PopoverProps) {
const popoverRef = useRef<HTMLDivElement>(null);
const closeTimer = useRef<number>(0);
useLayoutEffect(() => {
getPopoverPanelRef?.(popoverRef);
onShow?.(internalShow);
}, [internalShow]);
if (trigger === "click") {
const handleOpen = (e: { currentTarget: any }) => {
clearTimeout(closeTimer.current);
onShow?.(true);
setShow(true);
getRelativePosition(e.currentTarget, "");
window.document.documentElement.style.overflow = "hidden";
@@ -186,11 +199,9 @@ export default function Popover(props: PopoverProps) {
const handleClose = () => {
if (delayClose) {
closeTimer.current = window.setTimeout(() => {
onShow?.(false);
setShow(false);
}, delayClose);
} else {
onShow?.(false);
setShow(false);
}
window.document.documentElement.style.overflow = "auto";
@@ -219,7 +230,7 @@ export default function Popover(props: PopoverProps) {
)}
{createPortal(
<div
className={`${popoverCommonClass} ${popoverClassName} cursor-pointer`}
className={`${popoverCommonClass} ${popoverClassName} cursor-pointer overflow-auto`}
style={{ zIndex: baseZIndex + 1, ...placementStyle }}
ref={popoverRef}
>

View File

@@ -58,14 +58,22 @@ const Select = <Value extends number | string>(props: SearchProps<Value>) => {
{options?.map((o) => (
<div
key={o.value}
className={`flex items-center p-3 gap-2 rounded-action-btn hover:bg-select-option-hovered cursor-pointer`}
className={`flex items-center px-3 py-2 gap-3 rounded-action-btn hover:bg-select-option-hovered cursor-pointer`}
onClick={() => {
onSelect?.(o.value);
}}
>
{!!o.icon && <div className="">{o.icon}</div>}
<div className={`flex-1`}>{o.label}</div>
{selectedOption?.value === o.value && <Selected />}
<div className="flex gap-2 flex-1">
{!!o.icon && <div className="">{o.icon}</div>}
<div className={`flex-1`}>{o.label}</div>
</div>
<div
className={
selectedOption?.value === o.value ? "opacity-100" : "opacity-0"
}
>
<Selected />
</div>
</div>
))}
</div>
@@ -86,7 +94,7 @@ const Select = <Value extends number | string>(props: SearchProps<Value>) => {
className={selectClassName}
>
<div
className={`flex items-center gap-3 py-2 px-3 bg-select rounded-action-btn font-time text-sm-title cursor-pointer`}
className={`flex items-center gap-3 py-2 px-3 bg-select rounded-action-btn font-time text-sm-title cursor-pointer hover:bg-select-hover`}
ref={contentRef}
>
<div className={`flex items-center gap-2 flex-1`}>