From cb7c528f8d90dd06afd22f62beff98937e50418e Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Sun, 4 Oct 2026 20:38:23 -0400 Subject: [PATCH] feature: matchmaking bottom sheets close with a swipe down --- .../matchmaking/MatchmakingSettingsPanel.vue | 45 +++++++------- components/matchmaking/PlayWherePrompt.vue | 41 +++++++------ .../matchmaking/RegionLatencySettings.vue | 1 + .../MatchmakingSettingsPanel.spec.ts | 61 +++++++++++++++++++ 4 files changed, 106 insertions(+), 42 deletions(-) create mode 100644 tests/components/matchmaking/MatchmakingSettingsPanel.spec.ts diff --git a/components/matchmaking/MatchmakingSettingsPanel.vue b/components/matchmaking/MatchmakingSettingsPanel.vue index 957850432..3e0bab4e8 100644 --- a/components/matchmaking/MatchmakingSettingsPanel.vue +++ b/components/matchmaking/MatchmakingSettingsPanel.vue @@ -6,16 +6,16 @@ import { PopoverTrigger, } from "~/components/ui/popover"; import { - Sheet, - SheetContent, - SheetDescription, - SheetTitle, -} from "~/components/ui/sheet"; + Drawer, + DrawerContent, + DrawerDescription, + DrawerTitle, +} from "~/components/ui/drawer"; import MatchmakingSettings from "~/components/matchmaking/MatchmakingSettings.vue"; // Every way into the matchmaking settings (the top-bar globe, the /play gear) -// opens this: the same sections everywhere, a popover on desktop and a bottom -// sheet on phones, where a popover this tall flips up and clips. +// opens this: the same sections everywhere, a popover on desktop and a +// swipe-down drawer on phones, where a popover this tall flips up and clips. withDefaults(defineProps<{ align?: "start" | "center" | "end" }>(), { align: "start", }); @@ -44,23 +44,22 @@ const isPhone = useMediaQuery("(max-width: 767px)"); - - - {{ - $t("pages.play.matchmaking.settings") - }} - {{ - $t("pages.play.matchmaking.settings") - }} - - - - - + +
+ {{ + $t("pages.play.matchmaking.settings") + }} + {{ + $t("pages.play.matchmaking.settings") + }} + + + +
+
+ diff --git a/components/matchmaking/PlayWherePrompt.vue b/components/matchmaking/PlayWherePrompt.vue index 72909c271..dc2a88d31 100644 --- a/components/matchmaking/PlayWherePrompt.vue +++ b/components/matchmaking/PlayWherePrompt.vue @@ -10,18 +10,19 @@ import { DialogTitle, } from "~/components/ui/dialog"; import { - Sheet, - SheetContent, - SheetDescription, - SheetTitle, -} from "~/components/ui/sheet"; + Drawer, + DrawerContent, + DrawerDescription, + DrawerTitle, +} from "~/components/ui/drawer"; import PlayWhereChoices from "~/components/matchmaking/PlayWhereChoices.vue"; import { useMatchmakingStore } from "~/stores/MatchmakingStore"; import { roundedPing } from "~/components/play/matchmakingHero"; // Asks a player on a LAN whether to play there or online, the first time they // queue or host this session. Anchored to the button that asked on desktop; a -// bottom sheet on phones; a small dialog when nothing anchors it (a menu item). +// swipe-down drawer on phones; a small dialog when nothing anchors it (a menu +// item). const { t } = useI18n(); const matchmaking = useMatchmakingStore(); const isMobile = useMediaQuery("(max-width: 767px)"); @@ -99,19 +100,21 @@ function onOpenChange(open: boolean) { - - - {{ title }} - {{ title }} - - - + + +
+ {{ title }} + {{ title }} + +
+
+
diff --git a/components/matchmaking/RegionLatencySettings.vue b/components/matchmaking/RegionLatencySettings.vue index f8b315f0a..437819f8e 100644 --- a/components/matchmaking/RegionLatencySettings.vue +++ b/components/matchmaking/RegionLatencySettings.vue @@ -200,6 +200,7 @@ const wordClasses = step="5" :max="sliderMax" :value="maxLatency" + data-vaul-no-drag class="min-w-0 flex-1 cursor-pointer accent-[hsl(var(--tac-amber))]" @input="onLatencyInput" /> diff --git a/tests/components/matchmaking/MatchmakingSettingsPanel.spec.ts b/tests/components/matchmaking/MatchmakingSettingsPanel.spec.ts new file mode 100644 index 000000000..de2ecf9af --- /dev/null +++ b/tests/components/matchmaking/MatchmakingSettingsPanel.spec.ts @@ -0,0 +1,61 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { flushPromises } from "@vue/test-utils"; +import { mountSuspended } from "@nuxt/test-utils/runtime"; +import MatchmakingSettingsPanel from "~/components/matchmaking/MatchmakingSettingsPanel.vue"; + +function screen({ phone }: { phone: boolean }) { + vi.spyOn(window, "matchMedia").mockImplementation( + (query: string) => + ({ + matches: phone && query === "(max-width: 767px)", + media: query, + onchange: null, + addEventListener: () => {}, + removeEventListener: () => {}, + addListener: () => {}, + removeListener: () => {}, + dispatchEvent: () => false, + }) as MediaQueryList, + ); +} + +async function mountPanel(open: boolean) { + const wrapper = await mountSuspended(MatchmakingSettingsPanel, { + props: { open }, + slots: { trigger: '' }, + global: { + stubs: { MatchmakingSettings: { template: "
settings body
" } }, + }, + attachTo: document.body, + }); + await flushPromises(); + return wrapper; +} + +afterEach(() => { + vi.restoreAllMocks(); + document.body.innerHTML = ""; +}); + +describe("MatchmakingSettingsPanel", () => { + it("opens as a drawer you can swipe down on a phone", async () => { + screen({ phone: true }); + const wrapper = await mountPanel(true); + + expect(document.querySelector("[data-vaul-drawer]")).not.toBeNull(); + expect(document.body.textContent).toContain("settings body"); + wrapper.unmount(); + }); + + it("opens as a popover on desktop", async () => { + screen({ phone: false }); + const wrapper = await mountPanel(false); + await wrapper.find("button").trigger("click"); + await wrapper.setProps({ open: true }); + await flushPromises(); + + expect(document.querySelector("[data-vaul-drawer]")).toBeNull(); + expect(document.body.textContent).toContain("settings body"); + wrapper.unmount(); + }); +});