Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/components/AmbassadorCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { useCallback, useEffect, useRef, type Ref } from "react";
import {
useCallback,
useEffect,
useRef,
type KeyboardEventHandler,
type Ref,
} from "react";
import type { CreateTypes } from "canvas-confetti";
import Confetti from "react-canvas-confetti";

Expand Down Expand Up @@ -28,6 +34,7 @@ const stringifyLifespan = (value: number | { min: number; max: number }) => {
export interface AmbassadorCardProps {
ambassador: string;
onClose?: () => void;
onKeyDown?: KeyboardEventHandler;
className?: string;
ref?: Ref<HTMLDivElement>;
disableCardEffects?: boolean;
Expand Down Expand Up @@ -131,6 +138,7 @@ export default function AmbassadorCard(props: AmbassadorCardProps) {
className,
)}
ref={callbackRef}
tabIndex={-1} // Overlay view needs this for the onKeyDown listener
{...extras}
>
{birthday && (
Expand All @@ -141,13 +149,14 @@ export default function AmbassadorCard(props: AmbassadorCardProps) {
/>
)}
<img
className="max-h-32 w-full rounded-t-lg object-cover transition-[max-height] duration-700 ease-in-out hover:max-h-96 active:max-h-96"
className="max-h-32 w-full rounded-t-lg object-cover transition-[max-height] duration-700 ease-in-out hover:max-h-96 focus:max-h-96 active:max-h-96"
src={ambassador.image.src}
alt={ambassador.image.alt}
style={{
objectPosition: ambassador.image.position,
}}
loading="lazy"
tabIndex={0}
/>

<div className="relative flex w-full items-center justify-center bg-alveus-green px-8 py-1">
Expand All @@ -166,7 +175,10 @@ export default function AmbassadorCard(props: AmbassadorCardProps) {
{ambassador.name}
</h2>
</div>
<div className="mb-2 scrollbar-thin flex flex-auto flex-col gap-1 overflow-y-auto p-2 scrollbar-thumb-alveus-green scrollbar-track-alveus-green-900">
<div
className="mb-2 scrollbar-thin flex flex-auto flex-col gap-1 overflow-y-auto p-2 scrollbar-thumb-alveus-green scrollbar-track-alveus-green-900"
tabIndex={-1} // Prevent tabbing from the image from focusing this entire div
>
{mod && (
<div className="flex items-center gap-2">
<img
Expand Down Expand Up @@ -242,6 +254,7 @@ export default function AmbassadorCard(props: AmbassadorCardProps) {
<IconInfo
size={20}
className="rounded-full text-alveus-green-400 outline-highlight transition-[outline] hover:outline-3"
tabIndex={0}
/>
</div>
</Tooltip>
Expand Down
12 changes: 11 additions & 1 deletion src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useCallback } from "react";
import { classes } from "../utils/classes";

import Ring from "./Ring";
Expand All @@ -6,10 +7,17 @@ interface CardProps {
children?: React.ReactNode;
title?: string;
className?: string;
autoFocus?: boolean;
tabIndex?: number | undefined;
}

export default function Card(props: CardProps) {
const { children, title, className } = props;
const { children, title, className, autoFocus, tabIndex } = props;

const autoFocusCallback = useCallback((node: HTMLDivElement | null) => {
// node?.focus();
setTimeout(() => node?.focus(), 10);
}, []);

return (
<div
Expand All @@ -19,6 +27,8 @@ export default function Card(props: CardProps) {
"relative",
className,
)}
ref={autoFocus ? autoFocusCallback : undefined}
tabIndex={tabIndex}
>
{title && (
<h2 className="mb-2 text-center font-serif text-3xl font-bold text-balance">
Expand Down
1 change: 1 addition & 0 deletions src/components/TiltCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { classes } from "../utils/classes";

interface ConditionalTiltCardProps extends TiltCardProps {
disabled?: boolean;
tabIndex?: number | undefined;
}

function ConditionalTiltCard({
Expand Down
10 changes: 8 additions & 2 deletions src/components/Welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ const socialClass =

interface WelcomeProps {
className?: string;
isActiveOverlay?: boolean;
}

export default function Welcome(props: WelcomeProps) {
const { className } = props;
const { className, isActiveOverlay } = props;

const channel = useChannel();
const nonDefault = useMemo(
Expand All @@ -31,7 +32,12 @@ export default function Welcome(props: WelcomeProps) {
);

return (
<Card className={className} title="Welcome to Alveus">
<Card
className={className}
title="Welcome to Alveus"
autoFocus={isActiveOverlay}
tabIndex={-1}
>
<p className="mt-2 mb-4">
Alveus Sanctuary is a 501(c)(3) non-profit organization that functions
as a wildlife sanctuary and as a virtual education center. These
Expand Down
12 changes: 11 additions & 1 deletion src/pages/overlay/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export default function App() {
sleeping,
wake,
sleep,
caffeinate,
uncaffeinate,
on: addSleepListener,
off: removeSleepListener,
} = useSleeping();
Expand Down Expand Up @@ -52,8 +54,16 @@ export default function App() {
onMouseMove={interacted}
onWheel={interacted}
onTouchMove={interacted}
onKeyDown={interacted}
onKeyUp={(event) => {
// onFocus events capture when we click on the element in addition to
// tabbing
if (event.key === "Tab") {
caffeinate();
}
}}
onKeyDown={() => interacted()}
onMouseLeave={sleep}
onBlur={() => uncaffeinate(timeout)}
>
<Overlay />
</div>
Expand Down
7 changes: 4 additions & 3 deletions src/pages/overlay/components/Buttons.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, type JSX } from "react";
import { useMemo, type JSX, type MouseEvent } from "react";

import Tooltip from "../../../components/Tooltip";
import Ring from "../../../components/Ring";
Expand All @@ -16,7 +16,7 @@ type ButtonsOptions = Readonly<ButtonsOption[]>;

interface ButtonsProps<T extends ButtonsOptions> {
options: T;
onClick: (key: T[number]["key"] | "") => void;
onClick: (event: MouseEvent, key: T[number]["key"] | "") => void;
active?: string;
}

Expand All @@ -31,7 +31,8 @@ export default function Buttons<T extends ButtonsOptions = ButtonsOptions>(
options
.map((option) => ({
...option,
onClick: () => onClick(active === option.key ? "" : option.key),
onClick: (event: MouseEvent) =>
onClick(event, active === option.key ? "" : option.key),
active: active === option.key,
}))
.sort((a, b) => {
Expand Down
76 changes: 73 additions & 3 deletions src/pages/overlay/components/overlay/Ambassadors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
useMemo,
useRef,
type MouseEvent,
type KeyboardEventHandler,
} from "react";

import AmbassadorButton from "../../../../components/AmbassadorButton";
Expand All @@ -21,6 +22,8 @@ import type { OverlayOptionProps } from "./Overlay";
import IconChevron from "../../../../components/icons/IconChevron";
import useSettings from "../../hooks/useSettings";

import * as keyBinds from "../../../../utils/keyBinds";

const arrowClass =
"absolute border-0 cursor-pointer text-alveus-green w-full h-[var(--list-fade-padding)] z-20 transition-opacity group pt-[var(--twitch-vertical-padding)] pb-4 box-content";
const arrowSvgClass =
Expand Down Expand Up @@ -53,6 +56,8 @@ export default function Ambassadors(props: AmbassadorsProps) {
[rawAmbassadors, plants, settings.ambassadorSort.value],
);

const activeAmbassadorButtonRef = useRef<HTMLButtonElement>(null);
const activeAmbassadorRef = useRef<HTMLDivElement>(null);
const upArrowRef = useRef<HTMLButtonElement>(null);
const ambassadorList = useRef<HTMLDivElement>(null);
const downArrowRef = useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -128,6 +133,54 @@ export default function Ambassadors(props: AmbassadorsProps) {
return () => window.removeEventListener("resize", handleArrowVisibility);
}, [handleArrowVisibility, ambassadors]);

const ambassadorListCallback = useCallback(
(node: HTMLDivElement | null) => {
if (node && props.isActiveOverlay) {
// node.focus();
setTimeout(() => node.focus(), 10);
}

ambassadorList.current = node;
},
[props.isActiveOverlay],
);

const activeAmbassadorOnClose = () => {
setActiveAmbassador({});
activeAmbassadorButtonRef.current?.focus();
activeAmbassadorButtonRef.current = null;
activeAmbassadorRef.current = null;
};

const activeAmbassadorOnKeyDown: KeyboardEventHandler = (event) => {
if (event.defaultPrevented) {
return;
}

// Select keys used here to mimic default browser behavior. If you press
// enter or space, it counts as clicking the ambassador's button again to
// close it.
if (
keyBinds.BACK.includes(event.code as keyBinds.KeyCode) ||
(keyBinds.SELECT.includes(event.code as keyBinds.KeyCode) &&
event.target === activeAmbassadorRef.current)
) {
event.preventDefault();
activeAmbassadorOnClose();
return;
}
};

const activeAmbassadorRefCallback = useCallback(
(node: HTMLDivElement | null) => {
activeAmbassadorRef.current = node;

// Auto focus the ambassador that's currently active
node?.focus();
},
[],
);

return (
<div
className={classes(
Expand All @@ -137,15 +190,20 @@ export default function Ambassadors(props: AmbassadorsProps) {
>
<div className="relative z-10 flex flex-col items-center">
<div
ref={ambassadorList}
className="list-fade -my-[var(--twitch-vertical-padding)] scrollbar-none flex w-40 flex-col items-center gap-4 overflow-scroll px-4 py-[calc(var(--twitch-vertical-padding)+var(--list-fade-padding))]"
onScroll={handleArrowVisibility}
ref={ambassadorListCallback}
tabIndex={-1}
>
{ambassadors.map(([key]) => (
<AmbassadorButton
key={key}
ambassador={key}
onClick={() => {
onClick={(event) => {
if (event.target instanceof HTMLButtonElement) {
activeAmbassadorButtonRef.current = event.target;
}

setActiveAmbassador((prev) =>
prev.key === key ? {} : { key },
);
Expand All @@ -166,6 +224,7 @@ export default function Ambassadors(props: AmbassadorsProps) {
onClick={(e) => ambassadorListScroll(e, 250)}
title="Scroll up"
type="button"
tabIndex={-1}
data-transparent-clicks
>
<IconChevron className={classes(arrowSvgClass, arrowPathClass)} />
Expand All @@ -180,6 +239,7 @@ export default function Ambassadors(props: AmbassadorsProps) {
onClick={(e) => ambassadorListScroll(e, -250)}
title="Scroll down"
type="button"
tabIndex={-1}
data-transparent-clicks
>
<IconChevron className={classes(arrowSvgClass, arrowPathClass)} />
Expand All @@ -192,7 +252,17 @@ export default function Ambassadors(props: AmbassadorsProps) {
<AmbassadorCard
key={key}
ambassador={key}
onClose={() => setActiveAmbassador({})}
onClose={activeAmbassadorOnClose}
onKeyDown={
activeAmbassador.key === key
? activeAmbassadorOnKeyDown
: undefined
}
ref={
activeAmbassador.key === key
? activeAmbassadorRefCallback
: undefined
}
disableCardEffects={settings.disableCardEffects.value}
/>
</div>
Expand Down
Loading
Loading