Skip to content

Commit

Permalink
app components board clue: use popover to show answer on click
Browse files Browse the repository at this point in the history
Instead of using the clue list at the bottom, click on an answered clue to show
a popover with its answer.

Radix popover: https://www.radix-ui.com/docs/primitives/components/popover

Use a popover instead of a tooltip because Radix tooltips do not show up on
mobile (no hover events). See discussion:
radix-ui/primitives#955

To make clue button a popover trigger, pass a ref with React.forwardRef like
so:
- radix-ui/primitives#953
- https://buildui.com/videos/do-your-react-components-compose
  • Loading branch information
cmnord committed Mar 17, 2023
1 parent d49362e commit b4cce50
Show file tree
Hide file tree
Showing 5 changed files with 569 additions and 46 deletions.
3 changes: 1 addition & 2 deletions app/components/board/board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export function ConnectedBoardComponent({
}

function handleClickClue(i: number, j: number) {
if (hasBoardControl) {
if (hasBoardControl && !isAnswered(i, j)) {
return fetcher.submit(
{ i: i.toString(), j: j.toString(), userId },
{ method: "post", action: `/room/${roomName}/choose-clue` }
Expand All @@ -140,7 +140,6 @@ export function ConnectedBoardComponent({
}

const handleKeyDown = (event: React.KeyboardEvent, i: number, j: number) => {
event.stopPropagation();
if (event.key === "Enter") {
return handleClickClue(i, j);
}
Expand Down
114 changes: 75 additions & 39 deletions app/components/board/clue.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,87 @@
import classNames from "classnames";
import * as React from "react";

import Popover from "~/components/popover";
import type { Clue } from "~/models/convert.server";

const UNREVEALED_CLUE = "unrevealed";

export function ClueComponent({
answered,
clue,
hasBoardControl,
onFocus,
onKeyDown,
onClick,
value,
}: {
interface Props {
answered: boolean;
clue: Clue;
value: number;
hasBoardControl: boolean;
onFocus: () => void;
onClick: () => void;
onClick: (e: React.MouseEvent) => void;
onKeyDown: (e: React.KeyboardEvent) => void;
}) {
const [loading, setLoading] = React.useState(false);
}

const ClueButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & Props
>(
(
{
answered,
clue,
value,
hasBoardControl,
onFocus,
onClick,
onKeyDown,
...rest
},
ref
) => {
const [loading, setLoading] = React.useState(false);

const unrevealed = clue.clue.toLowerCase() === UNREVEALED_CLUE;
const unrevealed = clue.clue.toLowerCase() === UNREVEALED_CLUE;

React.useEffect(() => {
if (answered) {
setLoading(false);
}
}, [answered]);
React.useEffect(() => {
if (answered) {
setLoading(false);
}
}, [answered]);

// TODO: daily double / wagerable text
const clueText = answered ? (
unrevealed ? (
<p className="text-sm text-gray-400">{UNREVEALED_CLUE}</p>
// TODO: daily double / wagerable text
const clueText = answered ? (
unrevealed ? (
<p className="text-sm text-gray-400">{UNREVEALED_CLUE}</p>
) : (
<p className="uppercase font-korinna break-words">{clue.answer}</p>
)
) : (
<p className="uppercase font-korinna break-words">{clue.answer}</p>
)
) : (
<p className="text-4xl lg:text-5xl text-yellow-1000 text-shadow-md font-impact">
${value}
</p>
);
<p className="text-4xl lg:text-5xl text-yellow-1000 text-shadow-md font-impact">
${value}
</p>
);

// disabled must not include `answerable` so we can focus on answered clues.
const disabled = !hasBoardControl || unrevealed || loading;
// disabled must not include `answerable` so we can focus on answered clues.
const disabled = !hasBoardControl || unrevealed || loading;

return (
<td className="p-1 h-full">
return (
<button
type="submit"
disabled={disabled}
onClick={(e) => {
e.preventDefault();
if (disabled || answered) {
onClick={(event) => {
if (disabled) {
return;
}
setLoading(true);
onClick();
if (!answered) {
setLoading(true);
}
onClick(event);
}}
onFocus={onFocus}
onKeyDown={onKeyDown}
onKeyDown={(event) => {
if (disabled) {
return;
}
if (!answered && event.key === "Enter") {
setLoading(true);
}
onKeyDown(event);
}}
className={classNames(
"px-4 py-3 relative h-full w-full bg-blue-1000 transition-colors",
{
Expand All @@ -73,9 +92,26 @@ export function ClueComponent({
"border-spin opacity-75": loading,
}
)}
ref={ref}
{...rest}
>
{clueText}
</button>
);
}
);
ClueButton.displayName = "ClueButton";

export function ClueComponent(props: Props) {
return (
<td className="p-1 h-full">
{props.answered ? (
<Popover content={props.clue.clue}>
<ClueButton {...props} />
</Popover>
) : (
<ClueButton {...props} />
)}
</td>
);
}
33 changes: 33 additions & 0 deletions app/components/popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";

export default function Popover({
children,
content,
}: {
children: React.ReactNode;
content: React.ReactNode;
}) {
return (
<PopoverPrimitive.Root>
<PopoverPrimitive.Trigger asChild>{children}</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
className={
"data-[state=open]:data-[side=top]:animate-slideDownAndFade " +
"data-[state=open]:data-[side=right]:animate-slideLeftAndFade " +
"data-[state=open]:data-[side=bottom]:animate-slideUpAndFade " +
"data-[state=open]:data-[side=left]:animate-slideRightAndFade " +
"text-white rounded-md bg-blue-600 px-3 py-2 text-sm " +
"shadow-lg max-w-xs"
}
side="top"
sideOffset={5}
>
{content}
<PopoverPrimitive.Arrow className="fill-blue-600" />
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
);
}

0 comments on commit b4cce50

Please sign in to comment.