mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-08 06:36:07 +02:00
✨ Improve usability when expanding table (#827)
This commit is contained in:
parent
fc28cf105a
commit
1b100481a0
4 changed files with 214 additions and 185 deletions
|
@ -61,6 +61,7 @@
|
||||||
"react-hot-toast": "^2.4.0",
|
"react-hot-toast": "^2.4.0",
|
||||||
"react-i18next": "^12.1.4",
|
"react-i18next": "^12.1.4",
|
||||||
"react-linkify": "^1.0.0-alpha",
|
"react-linkify": "^1.0.0-alpha",
|
||||||
|
"react-remove-scroll": "^2.5.6",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
"smoothscroll-polyfill": "^0.4.4",
|
"smoothscroll-polyfill": "^0.4.4",
|
||||||
"spacetime": "^7.1.4",
|
"spacetime": "^7.1.4",
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { Button } from "@rallly/ui/button";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
||||||
import { Trans, useTranslation } from "next-i18next";
|
import { Trans, useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { RemoveScroll } from "react-remove-scroll";
|
||||||
import { useMeasure, useScroll } from "react-use";
|
import { useMeasure, useScroll } from "react-use";
|
||||||
|
|
||||||
import { TimesShownIn } from "@/components/clock";
|
import { TimesShownIn } from "@/components/clock";
|
||||||
|
@ -86,6 +87,8 @@ const DesktopPoll: React.FunctionComponent = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const collapse = () => {
|
const collapse = () => {
|
||||||
|
// enable scrolling on body
|
||||||
|
document.body.style.overflow = "";
|
||||||
setExpanded(false);
|
setExpanded(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -108,198 +111,212 @@ const DesktopPoll: React.FunctionComponent = () => {
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
expanded
|
expanded
|
||||||
? "shadow-huge absolute left-1/2 max-w-[calc(100vw-64px)] -translate-x-1/2 -translate-y-1 rounded-md border bg-white"
|
? "fixed left-0 top-0 z-50 flex h-full w-full items-center justify-center bg-gray-900/25 p-8"
|
||||||
: "",
|
: "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex h-14 shrink-0 items-center justify-between rounded-t-md border-b bg-gradient-to-b from-gray-50 to-gray-100/50 px-4 py-3">
|
<div
|
||||||
<div>
|
className={cn(
|
||||||
{mode !== "view" ? (
|
"shadow-huge flex max-h-full flex-col overflow-hidden rounded-md bg-white",
|
||||||
<div>
|
)}
|
||||||
<Trans
|
>
|
||||||
t={t}
|
<div className="flex h-14 shrink-0 items-center justify-between rounded-t-md border-b bg-gradient-to-b from-gray-50 to-gray-100/50 px-4 py-3">
|
||||||
i18nKey="saveInstruction"
|
<div>
|
||||||
values={{
|
{mode !== "view" ? (
|
||||||
action: mode === "new" ? t("continue") : t("save"),
|
<div>
|
||||||
}}
|
<Trans
|
||||||
components={{ b: <strong /> }}
|
t={t}
|
||||||
/>
|
i18nKey="saveInstruction"
|
||||||
</div>
|
values={{
|
||||||
) : (
|
action: mode === "new" ? t("continue") : t("save"),
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Users2Icon className="h-5 w-5 shrink-0" />
|
|
||||||
<div className="font-semibold">
|
|
||||||
{t("participants", { count: participants.length })} (
|
|
||||||
{participants.length})
|
|
||||||
</div>
|
|
||||||
{canAddNewParticipant ? (
|
|
||||||
<Button
|
|
||||||
className="ml-2"
|
|
||||||
size="sm"
|
|
||||||
data-testid="add-participant-button"
|
|
||||||
icon={PlusIcon}
|
|
||||||
onClick={() => {
|
|
||||||
votingForm.newParticipant();
|
|
||||||
}}
|
}}
|
||||||
|
components={{ b: <strong /> }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="text-sm font-medium">
|
|
||||||
{t("optionCount", { count: poll.options.length })}
|
|
||||||
</div>
|
|
||||||
{isOverflowing || expanded ? (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button disabled={x === 0} onClick={goToPreviousPage}>
|
|
||||||
<ArrowLeftIcon className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<Trans i18nKey="scrollLeft" defaults="Scroll Left" />
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
disabled={Boolean(
|
|
||||||
scrollRef.current &&
|
|
||||||
x + scrollRef.current.offsetWidth >=
|
|
||||||
scrollRef.current.scrollWidth,
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
goToNextPage();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ArrowRightIcon className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<Trans i18nKey="scrollRight" defaults="Scroll Right" />
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
{expanded ? (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
icon={ShrinkIcon}
|
|
||||||
onClick={() => {
|
|
||||||
collapse();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<Trans i18nKey="shrink" defaults="Shrink" />
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
icon={ExpandIcon}
|
|
||||||
onClick={() => {
|
|
||||||
expand();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<Trans i18nKey="expand" defaults="Expand" />
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{poll.options[0].duration !== 0 ? (
|
|
||||||
<div className="border-b bg-gray-50 p-3">
|
|
||||||
<TimesShownIn />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className={cn(
|
|
||||||
"pointer-events-none absolute bottom-2 left-[240px] top-0 z-30 w-4 border-l bg-gradient-to-r from-gray-800/5 via-transparent to-transparent transition-opacity",
|
|
||||||
x > 0 ? "opacity-100" : "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
ref={scrollRef}
|
|
||||||
className="scrollbar-thin hover:scrollbar-thumb-gray-400 scrollbar-thumb-gray-300 scrollbar-track-gray-100 relative z-10 flex-grow overflow-auto scroll-smooth pb-3 pr-3"
|
|
||||||
>
|
|
||||||
<table className="w-full table-auto border-separate border-spacing-0 ">
|
|
||||||
<thead>
|
|
||||||
<PollHeader />
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{mode === "new" ? (
|
|
||||||
<>
|
|
||||||
<ParticipantRowForm />
|
|
||||||
<tr aria-hidden="true">
|
|
||||||
<td colSpan={poll.options.length + 1} className="py-2" />
|
|
||||||
</tr>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
{visibleParticipants.length > 0
|
|
||||||
? visibleParticipants.map((participant, i) => {
|
|
||||||
return (
|
|
||||||
<ParticipantRow
|
|
||||||
key={i}
|
|
||||||
participant={participant}
|
|
||||||
editMode={
|
|
||||||
votingForm.watch("participantId") === participant.id
|
|
||||||
}
|
|
||||||
onChangeEditMode={(isEditing) => {
|
|
||||||
if (isEditing) {
|
|
||||||
votingForm.setEditingParticipantId(
|
|
||||||
participant.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: null}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{mode !== "view" ? (
|
|
||||||
<div className="flex shrink-0 items-center border-t bg-gray-50">
|
|
||||||
<div className="flex w-full items-center justify-between gap-3 p-3">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
votingForm.cancel();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
{mode === "new" ? (
|
|
||||||
<Button
|
|
||||||
form="voting-form"
|
|
||||||
type="submit"
|
|
||||||
variant="primary"
|
|
||||||
loading={votingForm.formState.isSubmitting}
|
|
||||||
>
|
|
||||||
{t("continue")}
|
|
||||||
</Button>
|
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
form="voting-form"
|
<Users2Icon className="h-5 w-5 shrink-0" />
|
||||||
type="submit"
|
<div className="font-semibold">
|
||||||
variant="primary"
|
{t("participants", { count: participants.length })} (
|
||||||
loading={votingForm.formState.isSubmitting}
|
{participants.length})
|
||||||
>
|
</div>
|
||||||
{t("save")}
|
{canAddNewParticipant ? (
|
||||||
</Button>
|
<Button
|
||||||
|
className="ml-2"
|
||||||
|
size="sm"
|
||||||
|
data-testid="add-participant-button"
|
||||||
|
icon={PlusIcon}
|
||||||
|
onClick={() => {
|
||||||
|
votingForm.newParticipant();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{t("optionCount", { count: poll.options.length })}
|
||||||
|
</div>
|
||||||
|
{isOverflowing || expanded ? (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button disabled={x === 0} onClick={goToPreviousPage}>
|
||||||
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<Trans i18nKey="scrollLeft" defaults="Scroll Left" />
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
disabled={Boolean(
|
||||||
|
scrollRef.current &&
|
||||||
|
x + scrollRef.current.offsetWidth >=
|
||||||
|
scrollRef.current.scrollWidth,
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
goToNextPage();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowRightIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<Trans i18nKey="scrollRight" defaults="Scroll Right" />
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
{expanded ? (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
icon={ShrinkIcon}
|
||||||
|
onClick={() => {
|
||||||
|
collapse();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<Trans i18nKey="shrink" defaults="Shrink" />
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
icon={ExpandIcon}
|
||||||
|
onClick={() => {
|
||||||
|
expand();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<Trans i18nKey="expand" defaults="Expand" />
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
{poll.options[0].duration !== 0 ? (
|
||||||
|
<div className="border-b bg-gray-50 p-3">
|
||||||
|
<TimesShownIn />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="relative flex min-h-0 flex-col">
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none absolute bottom-2 left-[240px] top-0 z-30 w-4 border-l bg-gradient-to-r from-gray-800/5 via-transparent to-transparent transition-opacity",
|
||||||
|
x > 0 ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<RemoveScroll
|
||||||
|
enabled={expanded}
|
||||||
|
ref={scrollRef}
|
||||||
|
className={cn(
|
||||||
|
"scrollbar-thin hover:scrollbar-thumb-gray-400 scrollbar-thumb-gray-300 scrollbar-track-gray-100 relative z-10 flex-grow overflow-auto scroll-smooth pb-3 pr-3",
|
||||||
|
expanded ? "" : "max-h-[600px]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<table className="w-full table-auto border-separate border-spacing-0 ">
|
||||||
|
<thead>
|
||||||
|
<PollHeader />
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{mode === "new" ? (
|
||||||
|
<>
|
||||||
|
<ParticipantRowForm />
|
||||||
|
<tr aria-hidden="true">
|
||||||
|
<td
|
||||||
|
colSpan={poll.options.length + 1}
|
||||||
|
className="py-2"
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{visibleParticipants.length > 0
|
||||||
|
? visibleParticipants.map((participant, i) => {
|
||||||
|
return (
|
||||||
|
<ParticipantRow
|
||||||
|
key={i}
|
||||||
|
participant={participant}
|
||||||
|
editMode={
|
||||||
|
votingForm.watch("participantId") ===
|
||||||
|
participant.id
|
||||||
|
}
|
||||||
|
onChangeEditMode={(isEditing) => {
|
||||||
|
if (isEditing) {
|
||||||
|
votingForm.setEditingParticipantId(
|
||||||
|
participant.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</RemoveScroll>
|
||||||
|
</div>
|
||||||
|
{mode !== "view" ? (
|
||||||
|
<div className="flex shrink-0 items-center border-t bg-gray-50">
|
||||||
|
<div className="flex w-full items-center justify-between gap-3 p-3">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
votingForm.cancel();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
{mode === "new" ? (
|
||||||
|
<Button
|
||||||
|
form="voting-form"
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
loading={votingForm.formState.isSubmitting}
|
||||||
|
>
|
||||||
|
{t("continue")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
form="voting-form"
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
loading={votingForm.formState.isSubmitting}
|
||||||
|
>
|
||||||
|
{t("save")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -31,7 +31,7 @@ const DialogOverlay = React.forwardRef<
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out animate-in data-[state=open]:fade-in fixed inset-0 z-50 bg-gray-900/25 backdrop-blur-sm transition-all duration-100",
|
"data-[state=closed]:animate-out data-[state=closed]:fade-out animate-in data-[state=open]:fade-in fixed inset-0 z-50 bg-gray-900/25 transition-all duration-100",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -8692,7 +8692,7 @@ react-overlays@^5.2.1:
|
||||||
uncontrollable "^7.2.1"
|
uncontrollable "^7.2.1"
|
||||||
warning "^4.0.3"
|
warning "^4.0.3"
|
||||||
|
|
||||||
react-remove-scroll-bar@^2.3.3:
|
react-remove-scroll-bar@^2.3.3, react-remove-scroll-bar@^2.3.4:
|
||||||
version "2.3.4"
|
version "2.3.4"
|
||||||
resolved "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz"
|
resolved "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz"
|
||||||
integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==
|
integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==
|
||||||
|
@ -8722,6 +8722,17 @@ react-remove-scroll@2.5.5:
|
||||||
use-callback-ref "^1.3.0"
|
use-callback-ref "^1.3.0"
|
||||||
use-sidecar "^1.1.2"
|
use-sidecar "^1.1.2"
|
||||||
|
|
||||||
|
react-remove-scroll@^2.5.6:
|
||||||
|
version "2.5.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.6.tgz#7510b8079e9c7eebe00e65a33daaa3aa29a10336"
|
||||||
|
integrity sha512-bO856ad1uDYLefgArk559IzUNeQ6SWH4QnrevIUjH+GczV56giDfl3h0Idptf2oIKxQmd1p9BN25jleKodTALg==
|
||||||
|
dependencies:
|
||||||
|
react-remove-scroll-bar "^2.3.4"
|
||||||
|
react-style-singleton "^2.2.1"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
use-callback-ref "^1.3.0"
|
||||||
|
use-sidecar "^1.1.2"
|
||||||
|
|
||||||
react-ssr-prepass@^1.5.0:
|
react-ssr-prepass@^1.5.0:
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz"
|
resolved "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz"
|
||||||
|
|
Loading…
Add table
Reference in a new issue