mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-22 10:47:26 +02:00
♻️ Remove headlessui (#1124)
This commit is contained in:
parent
efa4f03353
commit
e9fb86516d
12 changed files with 182 additions and 327 deletions
|
@ -21,7 +21,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^1.0.3",
|
"@auth/prisma-adapter": "^1.0.3",
|
||||||
"@floating-ui/react-dom-interactions": "^0.13.3",
|
"@floating-ui/react-dom-interactions": "^0.13.3",
|
||||||
"@headlessui/react": "^1.7.7",
|
|
||||||
"@hookform/resolvers": "^3.3.1",
|
"@hookform/resolvers": "^3.3.1",
|
||||||
"@next/bundle-analyzer": "^12.3.4",
|
"@next/bundle-analyzer": "^12.3.4",
|
||||||
"@radix-ui/react-slot": "^1.0.1",
|
"@radix-ui/react-slot": "^1.0.1",
|
||||||
|
|
|
@ -123,7 +123,7 @@ const Page = () => {
|
||||||
if (optionsToDeleteThatHaveVotes.length > 0) {
|
if (optionsToDeleteThatHaveVotes.length > 0) {
|
||||||
modalContext.render({
|
modalContext.render({
|
||||||
title: t("areYouSure"),
|
title: t("areYouSure"),
|
||||||
description: (
|
content: (
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="deletingOptionsWarning"
|
i18nKey="deletingOptionsWarning"
|
||||||
components={{ b: <strong /> }}
|
components={{ b: <strong /> }}
|
||||||
|
@ -131,7 +131,7 @@ const Page = () => {
|
||||||
),
|
),
|
||||||
onOk,
|
onOk,
|
||||||
okButtonProps: {
|
okButtonProps: {
|
||||||
type: "danger",
|
variant: "destructive",
|
||||||
},
|
},
|
||||||
okText: t("delete"),
|
okText: t("delete"),
|
||||||
cancelText: t("cancel"),
|
cancelText: t("cancel"),
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export interface CompactButtonProps {
|
|
||||||
icon?: React.ComponentType<{
|
|
||||||
className?: string;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
}>;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
onClick?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CompactButton = React.forwardRef<HTMLButtonElement, CompactButtonProps>(
|
|
||||||
({ icon: Icon, children, onClick }, ref) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
ref={ref}
|
|
||||||
type="button"
|
|
||||||
className="inline-flex size-5 items-center justify-center rounded-full bg-gray-100 text-gray-500 transition-colors hover:bg-gray-200 active:bg-gray-300 active:text-gray-500"
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{Icon ? <Icon className="size-3" /> : children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
CompactButton.displayName = "CompactButton";
|
|
||||||
|
|
||||||
export default CompactButton;
|
|
|
@ -4,8 +4,6 @@ import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@rallly/ui/dropdown-menu";
|
} from "@rallly/ui/dropdown-menu";
|
||||||
import { Icon } from "@rallly/ui/icon";
|
import { Icon } from "@rallly/ui/icon";
|
||||||
|
@ -14,6 +12,7 @@ import clsx from "clsx";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import {
|
import {
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
|
CalendarXIcon,
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
|
@ -34,7 +33,6 @@ import {
|
||||||
getDateProps,
|
getDateProps,
|
||||||
removeAllOptionsForDay,
|
removeAllOptionsForDay,
|
||||||
} from "../../../../utils/date-time-utils";
|
} from "../../../../utils/date-time-utils";
|
||||||
import CompactButton from "../../../compact-button";
|
|
||||||
import DateCard from "../../../date-card";
|
import DateCard from "../../../date-card";
|
||||||
import { useHeadlessDatePicker } from "../../../headless-date-picker";
|
import { useHeadlessDatePicker } from "../../../headless-date-picker";
|
||||||
import { DateTimeOption } from "..";
|
import { DateTimeOption } from "..";
|
||||||
|
@ -330,15 +328,20 @@ const MonthCalendar: React.FunctionComponent<DateTimePickerProps> = ({
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CompactButton
|
<Button
|
||||||
icon={XIcon}
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange([
|
onChange([
|
||||||
...options.slice(0, index),
|
...options.slice(0, index),
|
||||||
...options.slice(index + 1),
|
...options.slice(index + 1),
|
||||||
]);
|
]);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<Icon>
|
||||||
|
<XIcon />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -380,15 +383,13 @@ const MonthCalendar: React.FunctionComponent<DateTimePickerProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild={true}>
|
<DropdownMenuTrigger asChild={true}>
|
||||||
<button className="text-gray-500 hover:text-gray-800">
|
<Button variant="ghost" size="sm">
|
||||||
<MoreHorizontalIcon className="h4 w-4" />
|
<Icon>
|
||||||
</button>
|
<MoreHorizontalIcon />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<DropdownMenuLabel>
|
|
||||||
<Trans i18nKey="menu" defaults="Menu" />
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const times = optionsForDay.map(
|
const times = optionsForDay.map(
|
||||||
|
@ -430,7 +431,9 @@ const MonthCalendar: React.FunctionComponent<DateTimePickerProps> = ({
|
||||||
onChange(newOptions);
|
onChange(newOptions);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SparklesIcon className="mr-2 size-4" />
|
<Icon>
|
||||||
|
<SparklesIcon />
|
||||||
|
</Icon>
|
||||||
<Trans i18nKey="applyToAllDates" />
|
<Trans i18nKey="applyToAllDates" />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
@ -443,7 +446,9 @@ const MonthCalendar: React.FunctionComponent<DateTimePickerProps> = ({
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<XIcon className="mr-2 size-4" />
|
<Icon>
|
||||||
|
<CalendarXIcon />
|
||||||
|
</Icon>
|
||||||
<Trans i18nKey="deleteDate" />
|
<Trans i18nKey="deleteDate" />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
|
@ -1,23 +1,18 @@
|
||||||
|
import { Button } from "@rallly/ui/button";
|
||||||
import {
|
import {
|
||||||
flip,
|
Select,
|
||||||
FloatingPortal,
|
SelectContent,
|
||||||
offset,
|
SelectItem,
|
||||||
size,
|
SelectTrigger,
|
||||||
useFloating,
|
SelectValue,
|
||||||
} from "@floating-ui/react-dom-interactions";
|
} from "@rallly/ui/select";
|
||||||
import { Listbox } from "@headlessui/react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { ChevronDownIcon } from "lucide-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { getDuration } from "@/utils/date-time-utils";
|
import { getDuration } from "@/utils/date-time-utils";
|
||||||
import { stopPropagation } from "@/utils/stop-propagation";
|
|
||||||
|
|
||||||
import { styleMenuItem } from "../../../menu-styles";
|
|
||||||
|
|
||||||
export interface TimePickerProps {
|
export interface TimePickerProps {
|
||||||
value: Date;
|
value?: Date;
|
||||||
after?: Date;
|
after?: Date;
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange?: (value: Date) => void;
|
onChange?: (value: Date) => void;
|
||||||
|
@ -29,87 +24,69 @@ const TimePicker: React.FunctionComponent<TimePickerProps> = ({
|
||||||
className,
|
className,
|
||||||
after,
|
after,
|
||||||
}) => {
|
}) => {
|
||||||
const { reference, floating, x, y, strategy, refs } = useFloating({
|
const [open, setOpen] = React.useState(false);
|
||||||
placement: "bottom-start",
|
const getOptions = React.useCallback(() => {
|
||||||
strategy: "fixed",
|
if (!open) {
|
||||||
middleware: [
|
return [dayjs(value).toISOString()];
|
||||||
offset(5),
|
|
||||||
flip(),
|
|
||||||
size({
|
|
||||||
apply: ({ rects }) => {
|
|
||||||
if (refs.floating.current) {
|
|
||||||
Object.assign(refs.floating.current.style, {
|
|
||||||
minWidth: `${rects.reference.width}px`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderOptions = () => {
|
|
||||||
const startFromDate = after ? dayjs(after) : dayjs(value).startOf("day");
|
|
||||||
|
|
||||||
const options: React.ReactNode[] = [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= 96; i++) {
|
|
||||||
const optionValue = startFromDate.add(i * 15, "minutes");
|
|
||||||
options.push(
|
|
||||||
<Listbox.Option
|
|
||||||
key={i}
|
|
||||||
className={styleMenuItem}
|
|
||||||
value={optionValue.format("YYYY-MM-DDTHH:mm:ss")}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span>{optionValue.format("LT")}</span>
|
|
||||||
{after ? (
|
|
||||||
<span className="text-sm text-gray-500">
|
|
||||||
{getDuration(dayjs(after), optionValue)}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</Listbox.Option>,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return options;
|
let cursor = after
|
||||||
};
|
? dayjs(after).add(15, "minutes")
|
||||||
|
: dayjs(value).startOf("day");
|
||||||
|
|
||||||
|
const res: string[] = [];
|
||||||
|
while (cursor.isSame(value, "day")) {
|
||||||
|
res.push(cursor.toISOString());
|
||||||
|
cursor = cursor.add(15, "minutes");
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}, [after, open, value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Listbox
|
<Select
|
||||||
value={dayjs(value).format("YYYY-MM-DDTHH:mm:ss")}
|
value={value?.toISOString()}
|
||||||
onChange={(newValue) => {
|
onValueChange={(newValue) => {
|
||||||
onChange?.(new Date(newValue));
|
if (newValue) {
|
||||||
|
onChange?.(new Date(newValue));
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
>
|
>
|
||||||
{(open) => (
|
<SelectTrigger asChild>
|
||||||
<>
|
<Button className={className}>
|
||||||
<div ref={reference} className={clsx("relative", className)}>
|
<SelectValue placeholder="Select time" />
|
||||||
<Listbox.Button className="btn-default text-left">
|
</Button>
|
||||||
<span className="grow truncate">{dayjs(value).format("LT")}</span>
|
</SelectTrigger>
|
||||||
<span className="pointer-events-none ml-2 flex">
|
<SelectContent>
|
||||||
<ChevronDownIcon className="size-5" />
|
{open ? (
|
||||||
</span>
|
getOptions().map((option) => {
|
||||||
</Listbox.Button>
|
return (
|
||||||
</div>
|
<SelectItem key={option} value={dayjs(option).toISOString()}>
|
||||||
<FloatingPortal>
|
<div className="flex items-center gap-2">
|
||||||
{open ? (
|
<span>{dayjs(option).format("LT")}</span>
|
||||||
<Listbox.Options
|
{after ? (
|
||||||
style={{
|
<span className="text-sm text-gray-500">
|
||||||
position: strategy,
|
{getDuration(dayjs(after), dayjs(option))}
|
||||||
left: x ?? "",
|
</span>
|
||||||
top: y ?? "",
|
) : null}
|
||||||
}}
|
</div>
|
||||||
ref={floating}
|
</SelectItem>
|
||||||
className="z-50 max-h-52 overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
);
|
||||||
onMouseDown={stopPropagation}
|
})
|
||||||
>
|
) : (
|
||||||
{renderOptions()}
|
<SelectItem value={dayjs(value).toISOString()}>
|
||||||
</Listbox.Options>
|
<div className="flex items-center gap-2">
|
||||||
) : null}
|
<span>{dayjs(value).format("LT")}</span>
|
||||||
</FloatingPortal>
|
{after ? (
|
||||||
</>
|
<span className="text-sm text-gray-500">
|
||||||
)}
|
{getDuration(dayjs(after), dayjs(value))}
|
||||||
</Listbox>
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,14 @@ import { cn } from "@rallly/ui";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import { Card, CardDescription, CardHeader, CardTitle } from "@rallly/ui/card";
|
import { Card, CardDescription, CardHeader, CardTitle } from "@rallly/ui/card";
|
||||||
import { CommandDialog } from "@rallly/ui/command";
|
import { CommandDialog } from "@rallly/ui/command";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
useDialog,
|
||||||
|
} from "@rallly/ui/dialog";
|
||||||
import { FormField, FormMessage } from "@rallly/ui/form";
|
import { FormField, FormMessage } from "@rallly/ui/form";
|
||||||
import { Label } from "@rallly/ui/label";
|
import { Label } from "@rallly/ui/label";
|
||||||
import { Switch } from "@rallly/ui/switch";
|
import { Switch } from "@rallly/ui/switch";
|
||||||
|
@ -15,7 +23,6 @@ import { useFormContext } from "react-hook-form";
|
||||||
import { TimeZoneCommand } from "@/components/time-zone-picker/time-zone-select";
|
import { TimeZoneCommand } from "@/components/time-zone-picker/time-zone-select";
|
||||||
|
|
||||||
import { getBrowserTimeZone } from "../../../utils/date-time-utils";
|
import { getBrowserTimeZone } from "../../../utils/date-time-utils";
|
||||||
import { useModal } from "../../modal";
|
|
||||||
import { NewEventData } from "../types";
|
import { NewEventData } from "../types";
|
||||||
import MonthCalendar from "./month-calendar";
|
import MonthCalendar from "./month-calendar";
|
||||||
import { DateTimeOption } from "./types";
|
import { DateTimeOption } from "./types";
|
||||||
|
@ -73,38 +80,17 @@ const PollOptionsForm = ({
|
||||||
options.length === 0 ||
|
options.length === 0 ||
|
||||||
options.some((option) => option.type !== "timeSlot");
|
options.some((option) => option.type !== "timeSlot");
|
||||||
|
|
||||||
const [dateOrTimeRangeModal, openDateOrTimeRangeModal] = useModal({
|
const dateOrTimeRangeDialog = useDialog();
|
||||||
title: t("mixedOptionsTitle"),
|
|
||||||
description: t("mixedOptionsDescription"),
|
|
||||||
okText: t("mixedOptionsKeepTimes"),
|
|
||||||
onOk: () => {
|
|
||||||
setValue(
|
|
||||||
"options",
|
|
||||||
watchOptions.filter((option) => option.type === "timeSlot"),
|
|
||||||
);
|
|
||||||
if (!watchTimeZone) {
|
|
||||||
setValue("timeZone", getBrowserTimeZone());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cancelText: t("mixedOptionsKeepDates"),
|
|
||||||
onCancel: () => {
|
|
||||||
setValue(
|
|
||||||
"options",
|
|
||||||
watchOptions.filter((option) => option.type === "date"),
|
|
||||||
);
|
|
||||||
setValue("timeZone", "");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (watchOptions.length > 1) {
|
if (watchOptions.length > 1) {
|
||||||
const optionType = watchOptions[0].type;
|
const optionType = watchOptions[0].type;
|
||||||
// all options needs to be the same type
|
// all options needs to be the same type
|
||||||
if (watchOptions.some((option) => option.type !== optionType)) {
|
if (watchOptions.some((option) => option.type !== optionType)) {
|
||||||
openDateOrTimeRangeModal();
|
dateOrTimeRangeDialog.trigger();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [watchOptions, openDateOrTimeRangeModal]);
|
}, [watchOptions, dateOrTimeRangeDialog]);
|
||||||
|
|
||||||
const watchNavigationDate = watch("navigationDate");
|
const watchNavigationDate = watch("navigationDate");
|
||||||
const navigationDate = new Date(watchNavigationDate ?? Date.now());
|
const navigationDate = new Date(watchNavigationDate ?? Date.now());
|
||||||
|
@ -146,7 +132,47 @@ const PollOptionsForm = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{dateOrTimeRangeModal}
|
<Dialog {...dateOrTimeRangeDialog.dialogProps}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans i18nKey="mixedOptionsTitle" />
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<p className="text-sm">
|
||||||
|
<Trans i18nKey="mixedOptionsDescription" />
|
||||||
|
</p>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setValue(
|
||||||
|
"options",
|
||||||
|
watchOptions.filter((option) => option.type === "date"),
|
||||||
|
);
|
||||||
|
setValue("timeZone", "");
|
||||||
|
dateOrTimeRangeDialog.dismiss();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="mixedOptionsKeepDates" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setValue(
|
||||||
|
"options",
|
||||||
|
watchOptions.filter((option) => option.type === "timeSlot"),
|
||||||
|
);
|
||||||
|
if (!watchTimeZone) {
|
||||||
|
setValue("timeZone", getBrowserTimeZone());
|
||||||
|
}
|
||||||
|
dateOrTimeRangeDialog.dismiss();
|
||||||
|
}}
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
<Trans i18nKey="mixedOptionsKeepTimes" />
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
<div>
|
<div>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|
|
@ -70,7 +70,6 @@ const ModalProvider: React.FunctionComponent<ModalProviderProps> = ({
|
||||||
visible={true}
|
visible={true}
|
||||||
{...modal.config}
|
{...modal.config}
|
||||||
content={modal.content}
|
content={modal.content}
|
||||||
footer={null}
|
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
remove(id);
|
remove(id);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,128 +1,66 @@
|
||||||
import { Dialog } from "@headlessui/react";
|
import { Button, ButtonProps } from "@rallly/ui/button";
|
||||||
import { AnimatePresence, m } from "framer-motion";
|
import {
|
||||||
import { XIcon } from "lucide-react";
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@rallly/ui/dialog";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { ButtonProps, LegacyButton } from "../button";
|
|
||||||
|
|
||||||
export interface ModalProps {
|
export interface ModalProps {
|
||||||
description?: React.ReactNode;
|
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
okText?: string;
|
okText?: string;
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
okButtonProps?: ButtonProps;
|
okButtonProps?: ButtonProps;
|
||||||
onOk?: () => void;
|
onOk?: () => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
footer?: React.ReactNode;
|
|
||||||
content?: React.ReactNode;
|
content?: React.ReactNode;
|
||||||
overlayClosable?: boolean;
|
overlayClosable?: boolean;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
showClose?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modal: React.FunctionComponent<ModalProps> = ({
|
const Modal: React.FunctionComponent<ModalProps> = ({
|
||||||
description,
|
|
||||||
title,
|
title,
|
||||||
okText,
|
okText,
|
||||||
cancelText,
|
|
||||||
okButtonProps,
|
|
||||||
footer,
|
|
||||||
content,
|
content,
|
||||||
overlayClosable,
|
|
||||||
onCancel,
|
onCancel,
|
||||||
|
cancelText,
|
||||||
onOk,
|
onOk,
|
||||||
|
okButtonProps,
|
||||||
visible,
|
visible,
|
||||||
showClose,
|
|
||||||
}) => {
|
}) => {
|
||||||
const initialFocusRef = React.useRef<HTMLButtonElement>(null);
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<Dialog
|
||||||
{visible ? (
|
open={visible}
|
||||||
<Dialog
|
onOpenChange={(open) => {
|
||||||
open={visible}
|
if (!open) {
|
||||||
className="fixed inset-0 z-50 overflow-y-auto"
|
onCancel?.();
|
||||||
initialFocus={initialFocusRef}
|
}
|
||||||
onClose={() => {
|
}}
|
||||||
if (overlayClosable) onCancel?.();
|
>
|
||||||
}}
|
<DialogContent>
|
||||||
>
|
<DialogHeader>
|
||||||
<m.div
|
<DialogTitle>{title}</DialogTitle>
|
||||||
transition={{ duration: 0.5 }}
|
</DialogHeader>
|
||||||
className="flex min-h-screen items-center justify-center"
|
<p className="text-sm">{content}</p>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose>
|
||||||
|
<Button>{cancelText}</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
onOk?.();
|
||||||
|
}}
|
||||||
|
{...okButtonProps}
|
||||||
>
|
>
|
||||||
<Dialog.Overlay
|
{okText}
|
||||||
as={m.div}
|
</Button>
|
||||||
transition={{ duration: 0.5 }}
|
</DialogFooter>
|
||||||
initial={{ opacity: 0 }}
|
</DialogContent>
|
||||||
animate={{ opacity: 1 }}
|
</Dialog>
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
className="fixed inset-0 z-0 bg-gray-900/25"
|
|
||||||
/>
|
|
||||||
<m.div
|
|
||||||
transition={{ duration: 0.1 }}
|
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.9 }}
|
|
||||||
className="relative z-50 m-3 inline-block max-w-full transform text-left align-middle sm:m-8"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-testid="modal"
|
|
||||||
className="shadow-huge max-w-full overflow-hidden rounded-md bg-white"
|
|
||||||
>
|
|
||||||
{showClose ? (
|
|
||||||
<button
|
|
||||||
role="button"
|
|
||||||
className="absolute right-2 top-2 z-10 inline-flex size-7 items-center justify-center rounded-full text-gray-500 transition-colors hover:bg-gray-500/10 hover:text-gray-500 focus:ring-0 focus:ring-offset-0 active:bg-gray-500/20"
|
|
||||||
onClick={onCancel}
|
|
||||||
>
|
|
||||||
<XIcon className="h-4" />
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
{content ?? (
|
|
||||||
<div className="max-w-md p-6">
|
|
||||||
{title ? (
|
|
||||||
<Dialog.Title className="mb-2 font-medium">
|
|
||||||
{title}
|
|
||||||
</Dialog.Title>
|
|
||||||
) : null}
|
|
||||||
{description ? (
|
|
||||||
<Dialog.Description className="m-0">
|
|
||||||
{description}
|
|
||||||
</Dialog.Description>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{footer === undefined ? (
|
|
||||||
<div className="flex h-14 items-center justify-end gap-3 rounded-br-lg border-t bg-gray-50 p-3">
|
|
||||||
{cancelText ? (
|
|
||||||
<LegacyButton
|
|
||||||
onClick={() => {
|
|
||||||
onCancel?.();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{cancelText}
|
|
||||||
</LegacyButton>
|
|
||||||
) : null}
|
|
||||||
{okText ? (
|
|
||||||
<LegacyButton
|
|
||||||
ref={initialFocusRef}
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
onOk?.();
|
|
||||||
}}
|
|
||||||
{...okButtonProps}
|
|
||||||
>
|
|
||||||
{okText}
|
|
||||||
</LegacyButton>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</m.div>
|
|
||||||
</m.div>
|
|
||||||
</Dialog>
|
|
||||||
) : null}
|
|
||||||
</AnimatePresence>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -25,12 +25,3 @@ export const useModal = (
|
||||||
);
|
);
|
||||||
return [modal, () => setVisible(true), () => setVisible(false)];
|
return [modal, () => setVisible(true), () => setVisible(false)];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useModalState = (): [boolean, OpenModalFn, CloseModalFn] => {
|
|
||||||
const [visible, setVisible] = React.useState(false);
|
|
||||||
|
|
||||||
const hide = React.useCallback(() => setVisible(false), []);
|
|
||||||
const show = React.useCallback(() => setVisible(true), []);
|
|
||||||
|
|
||||||
return [visible, show, hide];
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
import { Switch as HeadlessSwitch } from "@headlessui/react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export interface SwitchProps {
|
|
||||||
checked?: boolean;
|
|
||||||
onChange: (checked: boolean) => void;
|
|
||||||
srDescription?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Switch: React.FunctionComponent<SwitchProps> = ({
|
|
||||||
checked = false,
|
|
||||||
onChange,
|
|
||||||
srDescription,
|
|
||||||
...rest
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<HeadlessSwitch
|
|
||||||
checked={checked}
|
|
||||||
onChange={onChange}
|
|
||||||
className={clsx(
|
|
||||||
"relative inline-flex h-6 w-10 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out",
|
|
||||||
{
|
|
||||||
"bg-gray-200": !checked,
|
|
||||||
"bg-green-500": checked,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{srDescription ? <span className="sr-only">{srDescription}</span> : null}
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className={clsx(
|
|
||||||
"pointer-events-none inline-block size-5 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out",
|
|
||||||
{
|
|
||||||
"translate-x-4": checked,
|
|
||||||
"translate-x-0": !checked,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</HeadlessSwitch>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Switch;
|
|
|
@ -26,7 +26,8 @@ test.describe("edit options", () => {
|
||||||
editOptionsPage.switchToSpecifyTimes();
|
editOptionsPage.switchToSpecifyTimes();
|
||||||
|
|
||||||
await page.click("text='12:00 PM'");
|
await page.click("text='12:00 PM'");
|
||||||
await page.click("text='1:00 PM'");
|
const listbox = page.getByRole("listbox");
|
||||||
|
listbox.getByText("1:00 PM", { exact: true }).click();
|
||||||
await page.getByRole("button", { name: "Save" }).click();
|
await page.getByRole("button", { name: "Save" }).click();
|
||||||
await expect(page.locator('text="Are you sure?"')).toBeVisible();
|
await expect(page.locator('text="Are you sure?"')).toBeVisible();
|
||||||
await page.click("text='Delete'");
|
await page.click("text='Delete'");
|
||||||
|
|
|
@ -1969,13 +1969,6 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@hapi/hoek" "^9.0.0"
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
|
||||||
"@headlessui/react@^1.7.7":
|
|
||||||
version "1.7.12"
|
|
||||||
resolved "https://registry.npmjs.org/@headlessui/react/-/react-1.7.12.tgz"
|
|
||||||
integrity sha512-FhSx5V+Qp0GvbTpaxyS+ymGDDNntCacClWsk/d8Upbr19g3AsPbjfPk4+m2CgJGcuCB5Dz7LpUIOAbvQTyjL2g==
|
|
||||||
dependencies:
|
|
||||||
client-only "^0.0.1"
|
|
||||||
|
|
||||||
"@heroicons/react@^1.0.6":
|
"@heroicons/react@^1.0.6":
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.npmjs.org/@heroicons/react/-/react-1.0.6.tgz"
|
resolved "https://registry.npmjs.org/@heroicons/react/-/react-1.0.6.tgz"
|
||||||
|
@ -5750,7 +5743,7 @@ cli-spinners@^2.5.0:
|
||||||
resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz"
|
resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz"
|
||||||
integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==
|
integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==
|
||||||
|
|
||||||
client-only@0.0.1, client-only@^0.0.1:
|
client-only@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz"
|
resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz"
|
||||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue