web: collapse right-side block buttons into one dropdown

Three inline buttons for adding a code, metrics or diagram block
crowd the row once more block types exist. Give BottomAction a
dropdown path for two or more buttons, keeping the single-button
inline path (used by LeftSideBlock) unchanged.

Fixes oxynote#160
This commit is contained in:
Sarcastic-Soul 2026-09-22 02:21:03 +05:30
parent cdc6cd2204
commit 38dd9ce33b
2 changed files with 60 additions and 20 deletions

View File

@ -1,8 +1,10 @@
import type { VueWrapper } from "@vue/test-utils"
import { describe, it } from "vitest"
import { beforeEach, describe, it } from "vitest"
import BottomAction from "./BottomAction.vue"
import {
clearTeleportedOverlays,
findButtonByText,
menuItem,
mountUnderTooltipProvider,
renderedIconNames,
t,
@ -18,6 +20,11 @@ const CODE_KEY =
"editor.split-documentation.right-side-bottom-action-buttons.add-code"
const METRICS_KEY =
"editor.split-documentation.right-side-bottom-action-buttons.add-metrics"
const TRIGGER_KEY = "editor.split-documentation.bottom-action-trigger-label"
function openDropdown(wrapper: VueWrapper) {
return wrapper.get("[data-slot='dropdown-menu-trigger']").trigger("click")
}
function mountAction(buttons: Record<string, unknown>[]) {
return mountUnderTooltipProvider(BottomAction, { props: { buttons } })
@ -32,7 +39,9 @@ function clicks(wrapper: VueWrapper): unknown[] {
return (action.emitted("button-click") ?? []).map((args) => args[0])
}
describe("<SplitDocumentationBottomAction>", () => {
describe("<SplitDocumentationBottomAction>", { concurrent: false }, () => {
beforeEach(clearTeleportedOverlays)
it("labels a button with its text", async ({ expect }) => {
const wrapper = await mountAction([{ id: "params", text: t(PARAMS_KEY) }])
@ -61,19 +70,26 @@ describe("<SplitDocumentationBottomAction>", () => {
expect(clicks(wrapper)).toEqual(["params"])
})
it("renders one button per entry", async ({ expect }) => {
it("shows a single trigger for two or more entries", async ({ expect }) => {
const wrapper = await mountAction([
{ id: "code", text: t(CODE_KEY), icon: "lucide:code" },
{ id: "metrics", text: t(METRICS_KEY) },
])
expect(wrapper.findAll("button")).toHaveLength(2)
expect(wrapper.text()).toContain(t(CODE_KEY))
expect(wrapper.text()).toContain(t(METRICS_KEY))
expect(renderedIconNames(wrapper)).toEqual([
"lucide:code",
"lucide:circle-plus",
expect(wrapper.findAll("button")).toHaveLength(1)
expect(wrapper.text()).toContain(t(TRIGGER_KEY))
})
it("lists every entry once the trigger is opened", async ({ expect }) => {
const wrapper = await mountAction([
{ id: "code", text: t(CODE_KEY), icon: "lucide:code" },
{ id: "metrics", text: t(METRICS_KEY) },
])
await openDropdown(wrapper)
expect(menuItem(t(CODE_KEY))).not.toBeNull()
expect(menuItem(t(METRICS_KEY))).not.toBeNull()
})
it("reports a click on a later button with its own id", async ({
@ -84,7 +100,9 @@ describe("<SplitDocumentationBottomAction>", () => {
{ id: "metrics", text: t(METRICS_KEY) },
])
await findButtonByText(wrapper, t(METRICS_KEY)).trigger("click")
await openDropdown(wrapper)
menuItem(t(METRICS_KEY)).click()
await nextTick()
expect(clicks(wrapper)).toEqual(["metrics"])
})

View File

@ -14,6 +14,8 @@ const emit = defineEmits<{
(e: "button-click", id: T): void
}>()
const { t } = useI18n({ useScope: "global" })
function handleClick(id: T) {
emit("button-click", id)
}
@ -36,22 +38,42 @@ function handleClick(id: T) {
}
"
>
<template v-for="(button, index) in props.buttons" :key="button.id">
<div
v-if="index > 0"
class="h-4 w-px bg-accent-foreground/10 opacity-0 transition-opacity group-hover/right-side-extra-action:opacity-100 supports-[hover:none]:opacity-100"
/>
<ShortcutTooltip side="bottom" :shortcut="button.shortcut">
<ShortcutTooltip
v-if="props.buttons.length === 1"
side="bottom"
:shortcut="props.buttons[0]?.shortcut"
>
<ShadcnUiButton
variant="dim"
size="custom"
class="gap-1 text-2sm opacity-0 transition-opacity group-hover/right-side-extra-action:opacity-100 supports-[hover:none]:opacity-100"
@click="handleClick(props.buttons[0]!.id)"
>
<Icon :name="props.buttons[0]?.icon ?? 'lucide:circle-plus'" />
{{ props.buttons[0]?.text }}
</ShadcnUiButton>
</ShortcutTooltip>
<ShadcnUiDropdownMenu v-else>
<ShadcnUiDropdownMenuTrigger as-child>
<ShadcnUiButton
variant="dim"
size="custom"
class="gap-1 text-2sm opacity-0 transition-opacity group-hover/right-side-extra-action:opacity-100 supports-[hover:none]:opacity-100"
>
<Icon name="lucide:circle-plus" />
{{ t("editor.split-documentation.bottom-action-trigger-label") }}
</ShadcnUiButton>
</ShadcnUiDropdownMenuTrigger>
<ShadcnUiDropdownMenuContent side="bottom" align="center">
<ShadcnUiDropdownMenuItem
v-for="button in props.buttons"
:key="button.id"
@click="handleClick(button.id)"
>
<Icon :name="button.icon ?? 'lucide:circle-plus'" />
{{ button.text }}
</ShadcnUiButton>
</ShortcutTooltip>
</template>
<span>{{ button.text }}</span>
</ShadcnUiDropdownMenuItem>
</ShadcnUiDropdownMenuContent>
</ShadcnUiDropdownMenu>
</div>
</template>