mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-10 15:36:49 +02:00
First public commit
This commit is contained in:
commit
e05cd62e53
228 changed files with 17717 additions and 0 deletions
178
components/poll/participant-row-form.tsx
Normal file
178
components/poll/participant-row-form.tsx
Normal file
|
@ -0,0 +1,178 @@
|
|||
import { Option } from "@prisma/client";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { requiredString } from "../../utils/form-validation";
|
||||
import Button from "../button";
|
||||
import CheckCircle from "../icons/check-circle.svg";
|
||||
import NameInput from "../name-input";
|
||||
import { ControlledScrollDiv } from "./poll";
|
||||
import { usePollContext } from "./poll-context";
|
||||
import { ParticipantForm } from "./types";
|
||||
|
||||
export interface ParticipantRowFormProps {
|
||||
defaultValues?: Partial<ParticipantForm>;
|
||||
onSubmit: (data: ParticipantForm) => Promise<void>;
|
||||
className?: string;
|
||||
options: Option[];
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
const ParticipantRowForm: React.VoidFunctionComponent<ParticipantRowFormProps> =
|
||||
({ defaultValues, onSubmit, className, options, onCancel }) => {
|
||||
const {
|
||||
setActiveOptionId,
|
||||
activeOptionId,
|
||||
columnWidth,
|
||||
scrollPosition,
|
||||
sidebarWidth,
|
||||
numberOfColumns,
|
||||
goToNextPage,
|
||||
setScrollPosition,
|
||||
} = usePollContext();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
control,
|
||||
formState: { errors, submitCount, isSubmitting },
|
||||
reset,
|
||||
} = useForm<ParticipantForm>({
|
||||
defaultValues: { name: "", votes: [], ...defaultValues },
|
||||
});
|
||||
|
||||
const isColumnVisible = (index: number) => {
|
||||
return (
|
||||
scrollPosition + numberOfColumns * columnWidth > columnWidth * index
|
||||
);
|
||||
};
|
||||
|
||||
const checkboxRefs = React.useRef<HTMLInputElement[]>([]);
|
||||
const isAnimatingRef = React.useRef(false);
|
||||
// This hack is necessary because when there is only one checkbox,
|
||||
// react-hook-form does not know to format the value into an array.
|
||||
// See: https://github.com/react-hook-form/react-hook-form/issues/7834
|
||||
|
||||
const checkboxProps = register("votes", {
|
||||
onBlur: () => setActiveOptionId(null),
|
||||
});
|
||||
|
||||
const checkboxGroupHack = (
|
||||
<input type="checkbox" className="hidden" {...checkboxProps} />
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(async ({ name, votes }) => {
|
||||
await onSubmit({
|
||||
name,
|
||||
// if there is only one checkbox then we get a string rather than array
|
||||
// See this issue with using dot notation: https://github.com/react-hook-form/react-hook-form/issues/7834
|
||||
votes: Array.isArray(votes) ? votes : [votes],
|
||||
});
|
||||
reset();
|
||||
})}
|
||||
className={clsx("flex shrink-0 h-14", className)}
|
||||
>
|
||||
{checkboxGroupHack}
|
||||
<div className="flex items-center px-4" style={{ width: sidebarWidth }}>
|
||||
<Controller
|
||||
name="name"
|
||||
rules={{
|
||||
validate: requiredString,
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<div className="-ml-2">
|
||||
<NameInput
|
||||
autoFocus={true}
|
||||
className={clsx("w-full", {
|
||||
"animate-wiggle input-error":
|
||||
errors.name && submitCount > 0,
|
||||
})}
|
||||
placeholder="Your name"
|
||||
{...field}
|
||||
onKeyDown={(e) => {
|
||||
if (e.code === "Tab" && scrollPosition > 0) {
|
||||
e.preventDefault();
|
||||
setScrollPosition(0);
|
||||
setTimeout(() => {
|
||||
checkboxRefs.current[0].focus();
|
||||
}, 800);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
control={control}
|
||||
/>
|
||||
</div>
|
||||
<ControlledScrollDiv>
|
||||
{options.map((option, index) => {
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
className={clsx(
|
||||
"flex justify-center items-center shrink-0 transition-colors",
|
||||
{
|
||||
"bg-slate-50": activeOptionId === option.id,
|
||||
},
|
||||
)}
|
||||
style={{ width: columnWidth }}
|
||||
onMouseOver={() => setActiveOptionId(option.id)}
|
||||
onMouseOut={() => setActiveOptionId(null)}
|
||||
>
|
||||
<input
|
||||
className="checkbox"
|
||||
type="checkbox"
|
||||
value={option.id}
|
||||
onKeyDown={(e) => {
|
||||
if (isAnimatingRef.current) {
|
||||
return e.preventDefault();
|
||||
}
|
||||
if (
|
||||
e.code === "Tab" &&
|
||||
index < options.length - 1 &&
|
||||
!isColumnVisible(index + 1)
|
||||
) {
|
||||
isAnimatingRef.current = true;
|
||||
e.preventDefault();
|
||||
goToNextPage();
|
||||
setTimeout(() => {
|
||||
checkboxRefs.current[index + 1].focus();
|
||||
isAnimatingRef.current = false;
|
||||
}, 500);
|
||||
}
|
||||
}}
|
||||
{...checkboxProps}
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
checkboxRefs.current[index] = el;
|
||||
checkboxProps.ref(el);
|
||||
}
|
||||
}}
|
||||
onFocus={() => {
|
||||
setActiveOptionId(option.id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ControlledScrollDiv>
|
||||
<div className="flex items-center px-2 space-x-2 transition-all">
|
||||
<Button
|
||||
htmlType="submit"
|
||||
icon={<CheckCircle />}
|
||||
type="primary"
|
||||
loading={isSubmitting}
|
||||
data-testid="submitNewParticipant"
|
||||
/>
|
||||
<Button onClick={onCancel} type="default">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParticipantRowForm;
|
Loading…
Add table
Add a link
Reference in a new issue