From 1fb8fe66808fd0e2ec88fb0643b029e504e81c3d Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 22 Jun 2026 19:17:13 -0600 Subject: [PATCH] feat: add generic ToolbarMenuButton dropdown component --- src/components/ToolbarMenuButton.test.ts | 72 ++++++++++++++++++++++++ src/components/ToolbarMenuButton.vue | 58 +++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 src/components/ToolbarMenuButton.test.ts create mode 100644 src/components/ToolbarMenuButton.vue diff --git a/src/components/ToolbarMenuButton.test.ts b/src/components/ToolbarMenuButton.test.ts new file mode 100644 index 0000000..727bdd9 --- /dev/null +++ b/src/components/ToolbarMenuButton.test.ts @@ -0,0 +1,72 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import ToolbarMenuButton from './ToolbarMenuButton.vue' + +function mountMenu(props: { label: string; toggleTestid: string; disabled?: boolean }) { + return mount(ToolbarMenuButton, { + props, + attachTo: document.body, + slots: { + default: ``, + }, + }) +} + +describe('ToolbarMenuButton', () => { + it('renders the toggle button with the given label and closed menu by default', () => { + const wrapper = mountMenu({ label: '导出 ▾', toggleTestid: 'export-menu-toggle' }) + expect(wrapper.get('button[data-testid="export-menu-toggle"]').text()).toBe('导出 ▾') + expect(wrapper.find('[data-testid="item-a"]').exists()).toBe(false) + wrapper.unmount() + }) + + it('opens the menu when the toggle button is clicked', async () => { + const wrapper = mountMenu({ label: '导出 ▾', toggleTestid: 'export-menu-toggle' }) + await wrapper.get('button[data-testid="export-menu-toggle"]').trigger('click') + expect(wrapper.get('[data-testid="item-a"]').isVisible()).toBe(true) + expect(wrapper.get('[data-testid="item-b"]').isVisible()).toBe(true) + wrapper.unmount() + }) + + it('closes the menu when a slot item calls close', async () => { + const wrapper = mountMenu({ label: '导出 ▾', toggleTestid: 'export-menu-toggle' }) + await wrapper.get('button[data-testid="export-menu-toggle"]').trigger('click') + await wrapper.get('button[data-testid="item-a"]').trigger('click') + expect(wrapper.find('[data-testid="item-a"]').exists()).toBe(false) + wrapper.unmount() + }) + + it('closes the menu when clicking outside the component', async () => { + const wrapper = mountMenu({ label: '导出 ▾', toggleTestid: 'export-menu-toggle' }) + await wrapper.get('button[data-testid="export-menu-toggle"]').trigger('click') + expect(wrapper.find('[data-testid="item-a"]').exists()).toBe(true) + + document.body.click() + await wrapper.vm.$nextTick() + + expect(wrapper.find('[data-testid="item-a"]').exists()).toBe(false) + wrapper.unmount() + }) + + it('closes the menu when Escape is pressed', async () => { + const wrapper = mountMenu({ label: '导出 ▾', toggleTestid: 'export-menu-toggle' }) + await wrapper.get('button[data-testid="export-menu-toggle"]').trigger('click') + expect(wrapper.find('[data-testid="item-a"]').exists()).toBe(true) + + await wrapper.get('div.toolbar-menu').trigger('keydown', { key: 'Escape' }) + + expect(wrapper.find('[data-testid="item-a"]').exists()).toBe(false) + wrapper.unmount() + }) + + it('disables the toggle button and never opens the menu when disabled is true', async () => { + const wrapper = mountMenu({ label: '导出 ▾', toggleTestid: 'export-menu-toggle', disabled: true }) + expect(wrapper.get('button[data-testid="export-menu-toggle"]').attributes('disabled')).toBeDefined() + await wrapper.get('button[data-testid="export-menu-toggle"]').trigger('click') + expect(wrapper.find('[data-testid="item-a"]').exists()).toBe(false) + wrapper.unmount() + }) +}) diff --git a/src/components/ToolbarMenuButton.vue b/src/components/ToolbarMenuButton.vue new file mode 100644 index 0000000..3a300bb --- /dev/null +++ b/src/components/ToolbarMenuButton.vue @@ -0,0 +1,58 @@ + + +