-
Notifications
You must be signed in to change notification settings - Fork 4
Fix/notifications #746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix/notifications #746
Changes from 13 commits
a402241
76c36c0
1f800fb
a333a12
d46f15f
3cdfd29
92d8e99
2978e5a
da9e739
684f53b
5a94f8b
657336d
0f46e63
f891b29
3f81e47
2a4ce13
71e9153
86dbc86
6bc0569
a35fc2c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import { fireEvent, render, screen, waitFor } from "@testing-library/react"; | ||
|
|
||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import type { Guild } from "@/api"; | ||
| import { GuildSwitcher } from "./guild-switcher"; | ||
|
|
||
| const mockUseGuilds = vi.fn(); | ||
| const mockUseUserPreferences = vi.fn(); | ||
|
|
||
| vi.mock("@/hooks/api/use-guilds", () => ({ | ||
| useGuilds: () => mockUseGuilds(), | ||
| })); | ||
|
|
||
| vi.mock("@/hooks/api/use-user-preferences", () => ({ | ||
| useUserPreferences: () => mockUseUserPreferences(), | ||
| })); | ||
|
|
||
| vi.mock("@/lib/game", () => ({ | ||
| Game: { | ||
| hero: { | ||
| id: 123, | ||
| }, | ||
| }, | ||
| })); | ||
|
|
||
| const createGuild = (id: string, name: string): Guild => ({ | ||
| id, | ||
| name, | ||
| icon: null, | ||
| }); | ||
|
|
||
| describe("GuildSwitcher", () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
|
|
||
| mockUseGuilds.mockReturnValue({ | ||
| data: [ | ||
| createGuild("guild-1", "Alpha"), | ||
| createGuild("guild-2", "Beta"), | ||
| createGuild("guild-3", "Gamma"), | ||
| ], | ||
| isFetched: true, | ||
| }); | ||
| mockUseUserPreferences.mockReturnValue({ | ||
| data: { | ||
| guildsOrder: [], | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it("renders guilds in the user preferences order and appends missing guilds", () => { | ||
| mockUseUserPreferences.mockReturnValue({ | ||
| data: { | ||
| guildsOrder: ["guild-2", "guild-1"], | ||
| }, | ||
| }); | ||
|
|
||
| render(<GuildSwitcher />); | ||
|
|
||
| expect( | ||
| screen.getAllByRole("button").map((button) => button.textContent), | ||
| ).toEqual(["B", "A", "G"]); | ||
| }); | ||
|
|
||
| it("falls back to the first ordered guild when the current selection is missing", async () => { | ||
| const handleChange = vi.fn(); | ||
|
|
||
| mockUseUserPreferences.mockReturnValue({ | ||
| data: { | ||
| guildsOrder: ["guild-2", "guild-1"], | ||
| }, | ||
| }); | ||
|
|
||
| render(<GuildSwitcher value="missing-guild" onChange={handleChange} />); | ||
|
|
||
| await waitFor(() => { | ||
| expect(handleChange).toHaveBeenCalledWith("guild-2"); | ||
| }); | ||
| }); | ||
|
|
||
| it("scrolls horizontally when the user uses the mouse wheel", () => { | ||
| const { container } = render(<GuildSwitcher />); | ||
| const viewport = container.querySelector( | ||
| "[data-radix-scroll-area-viewport]", | ||
| ); | ||
|
|
||
| expect(viewport).not.toBeNull(); | ||
|
|
||
| Object.defineProperty(viewport, "scrollLeft", { | ||
| configurable: true, | ||
| value: 0, | ||
| writable: true, | ||
| }); | ||
|
|
||
| fireEvent.wheel(viewport as HTMLElement, { deltaY: 48 }); | ||
|
|
||
| expect((viewport as HTMLElement).scrollLeft).toBe(48); | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,14 @@ | ||
| import { cn } from "@/lib/utils"; | ||
| import { useGuilds } from "@/hooks/api/use-guilds"; | ||
| import { useUserPreferences } from "@/hooks/api/use-user-preferences"; | ||
| import { useSettingsStore } from "@/store/settings.store"; | ||
| import { TooltipProvider } from "@/components/ui/tooltip"; | ||
| import { AvatarFallback, AvatarImage } from "@/components/ui/avatar"; | ||
| import { type FC, useEffect, useRef } from "react"; | ||
| import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; | ||
| import type { Viewport } from "@radix-ui/react-scroll-area"; | ||
| import { Game } from "@/lib/game"; | ||
| import { GuildButton } from "@/components/guild-button"; | ||
| import type { Guild } from "@/api"; | ||
|
|
||
| type GuildSwitcherProps = { | ||
| disabled?: boolean; | ||
|
|
@@ -23,6 +24,27 @@ type GuildSwitcherProps = { | |
| value?: string; | ||
| }; | ||
|
|
||
| const orderGuilds = (guilds: Guild[] | undefined, guildsOrder?: string[]) => { | ||
| if (!guilds?.length) { | ||
| return []; | ||
| } | ||
|
|
||
| if (!guildsOrder?.length) { | ||
| return guilds; | ||
| } | ||
|
|
||
| const guildsById = new Map(guilds.map((guild) => [guild.id, guild] as const)); | ||
| const orderedGuilds = guildsOrder | ||
| .map((guildId) => guildsById.get(guildId)) | ||
| .filter((guild): guild is Guild => guild !== undefined); | ||
| const orderedGuildIds = new Set(orderedGuilds.map((guild) => guild.id)); | ||
| const remainingGuilds = guilds.filter( | ||
| (guild) => !orderedGuildIds.has(guild.id), | ||
| ); | ||
|
|
||
| return [...orderedGuilds, ...remainingGuilds]; | ||
| }; | ||
|
|
||
| export const GuildSwitcher: FC<GuildSwitcherProps> = ({ | ||
| disabled = false, | ||
| allowAll = false, | ||
|
|
@@ -36,31 +58,40 @@ export const GuildSwitcher: FC<GuildSwitcherProps> = ({ | |
| selectedValues, | ||
| value, | ||
| }) => { | ||
| const scrollContainerRef = useRef<React.ElementRef<typeof Viewport>>(null); | ||
| const scrollContainerRef = useRef<HTMLDivElement | null>(null); | ||
| const characterId = String(Game.hero.id); | ||
| const { data: guilds, isFetched } = useGuilds(); | ||
| const { data: userPreferences } = useUserPreferences(); | ||
| const { setGuildId, guildIdByCharId } = useSettingsStore(); | ||
| const guildsOrder = userPreferences?.guildsOrder; | ||
| const orderedGuilds = orderGuilds(guilds, guildsOrder); | ||
|
|
||
| const guildId = guildIdByCharId[characterId]; | ||
|
|
||
| useEffect(() => { | ||
| if (!isFetched || !guilds || guilds.length === 0) return; | ||
| const orderedGuildsForSelection = orderGuilds(guilds, guildsOrder); | ||
|
|
||
| if (!isFetched || orderedGuildsForSelection.length === 0) return; | ||
| if (multiple) return; | ||
| const currentValue = value !== undefined ? value : guildId; | ||
| if (allowAll && currentValue === "all") return; | ||
| const exists = guilds.some((guild) => guild.id === currentValue); | ||
| const exists = orderedGuildsForSelection.some( | ||
| (guild) => guild.id === currentValue, | ||
| ); | ||
| if (exists) return; | ||
| if (onChange) { | ||
| onChange(guilds[0].id); | ||
| onChange(orderedGuildsForSelection[0].id); | ||
| } else { | ||
| setGuildId(characterId, guilds[0].id); | ||
| setGuildId(characterId, orderedGuildsForSelection[0].id); | ||
| } | ||
| }, [ | ||
| isFetched, | ||
| guilds, | ||
| guildsOrder, | ||
| guildId, | ||
| value, | ||
| allowAll, | ||
| multiple, | ||
| onChange, | ||
| characterId, | ||
| setGuildId, | ||
|
|
@@ -86,23 +117,19 @@ export const GuildSwitcher: FC<GuildSwitcherProps> = ({ | |
| setGuildId(characterId, newGuildId); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| if (layout !== "scroll") return; | ||
| const handleWheel = (event: React.WheelEvent<HTMLDivElement>) => { | ||
| if (layout !== "scroll" || event.deltaY === 0) { | ||
| return; | ||
| } | ||
|
|
||
| const scrollContainer = scrollContainerRef.current; | ||
| if (!scrollContainer) return; | ||
|
|
||
| const handleWheel = (e: WheelEvent) => { | ||
| e.preventDefault(); | ||
| scrollContainer.scrollLeft += e.deltaY; | ||
| }; | ||
|
|
||
| scrollContainer.addEventListener("wheel", handleWheel, { passive: false }); | ||
| if (!scrollContainer) { | ||
| return; | ||
| } | ||
|
|
||
| return () => { | ||
| scrollContainer.removeEventListener("wheel", handleWheel); | ||
| }; | ||
| }, []); | ||
| event.preventDefault(); | ||
| scrollContainer.scrollLeft += event.deltaY; | ||
| }; | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| const content = ( | ||
| <> | ||
|
|
@@ -120,7 +147,7 @@ export const GuildSwitcher: FC<GuildSwitcherProps> = ({ | |
| </AvatarFallback> | ||
| </GuildButton> | ||
| )} | ||
| {guilds?.map((guild) => ( | ||
| {orderedGuilds.map((guild) => ( | ||
| <GuildButton | ||
| key={guild.id} | ||
| isSelected={ | ||
|
|
@@ -167,9 +194,10 @@ export const GuildSwitcher: FC<GuildSwitcherProps> = ({ | |
| <ScrollArea | ||
| className={cn("ll:w-full", className)} | ||
| ref={scrollContainerRef} | ||
| onWheel={handleWheel} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| type="hover" | ||
| > | ||
| <div className="ll:flex ll:gap-1 ll:mt-1 ll:overflow-hidden"> | ||
| <div className="ll:mt-1 ll:flex ll:w-max ll:min-w-full ll:gap-1"> | ||
| {content} | ||
| </div> | ||
| <ScrollBar | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.