diff --git a/package-lock.json b/package-lock.json index cdf54218f7..76aac68ad8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -98,6 +98,7 @@ "vite-plugin-electron": "0.29.0", "vite-tsconfig-paths": "5.1.2", "vitest": "2.1.2", + "vue-component-type-helpers": "2.1.6", "vue-tsc": "2.1.10", "yargs": "17.2.1" }, @@ -16205,13 +16206,20 @@ } } }, - "node_modules/vue-component-type-helpers": { + "node_modules/vue-component-meta/node_modules/vue-component-type-helpers": { "version": "2.1.10", "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.1.10.tgz", "integrity": "sha512-lfgdSLQKrUmADiSV6PbBvYgQ33KF3Ztv6gP85MfGaGaSGMTXORVaHT1EHfsqCgzRNBstPKYDmvAV9Do5CmJ07A==", "dev": true, "license": "MIT" }, + "node_modules/vue-component-type-helpers": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.1.6.tgz", + "integrity": "sha512-ng11B8B/ZADUMMOsRbqv0arc442q7lifSubD0v8oDXIFoMg/mXwAPUunrroIDkY+mcD0dHKccdaznSVp8EoX3w==", + "dev": true, + "license": "MIT" + }, "node_modules/vue-docgen-api": { "version": "4.79.2", "resolved": "https://registry.npmjs.org/vue-docgen-api/-/vue-docgen-api-4.79.2.tgz", diff --git a/package.json b/package.json index a7f9ba22d9..114b642d4b 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "vite-plugin-electron": "0.29.0", "vite-tsconfig-paths": "5.1.2", "vitest": "2.1.2", + "vue-component-type-helpers": "2.1.6", "vue-tsc": "2.1.10", "yargs": "17.2.1" } diff --git a/src/components/Menu/ContextMenu/Presentation.vue b/src/components/Menu/ContextMenu/Presentation.vue index da26802ac7..29ad0d6346 100644 --- a/src/components/Menu/ContextMenu/Presentation.vue +++ b/src/components/Menu/ContextMenu/Presentation.vue @@ -38,6 +38,9 @@ defineProps<{ uiLocked?: boolean; }>(); defineExpose({ + show: (event?: MouseEvent | undefined) => { + contextMenu.value?.show(event); + }, hide: () => { contextMenu.value?.hide(); }, diff --git a/src/components/Sing/ChangeValueDialog/CommonDialog.vue b/src/components/Sing/ChangeValueDialog/CommonDialog.vue new file mode 100644 index 0000000000..2e619dbcf8 --- /dev/null +++ b/src/components/Sing/ChangeValueDialog/CommonDialog.vue @@ -0,0 +1,91 @@ + + + + + + diff --git a/src/components/Sing/ChangeValueDialog/TempoChangeDialog.stories.ts b/src/components/Sing/ChangeValueDialog/TempoChangeDialog.stories.ts new file mode 100644 index 0000000000..1027e0e929 --- /dev/null +++ b/src/components/Sing/ChangeValueDialog/TempoChangeDialog.stories.ts @@ -0,0 +1,101 @@ +import { userEvent, within, expect, fn } from "@storybook/test"; +import { Meta, StoryObj } from "@storybook/vue3"; + +import TempoChangeDialog from "./TempoChangeDialog.vue"; + +const meta: Meta = { + component: TempoChangeDialog, + args: { + modelValue: true, + tempoChange: undefined, + mode: "add", + + onOk: fn(), + onHide: fn(), + }, + tags: ["!autodocs"], // ダイアログ系はautodocsのプレビューが正しく表示されないので無効化 +}; + +export default meta; +type Story = StoryObj; + +export const CreateOpened: Story = { + name: "開いている:追加", + args: { + modelValue: true, + mode: "add", + }, +}; +export const ChangeOpened: Story = { + name: "開いている:変更", + args: { + modelValue: true, + tempoChange: { + bpm: 120, + }, + mode: "edit", + }, +}; + +export const ClickOk: Story = { + name: "OKボタンを押す:追加", + args: { ...CreateOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const input = canvas.getByLabelText("テンポ"); + await userEvent.clear(input); + await userEvent.type(input, "100"); + + const button = canvas.getByRole("button", { name: /追加する/ }); + await userEvent.click(button); + + await expect(args["onOk"]).toBeCalledWith({ + tempoChange: { + bpm: 100, + }, + }); + }, +}; + +export const ClickDelete: Story = { + name: "OKボタンを押す:編集", + args: { ...ChangeOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const input = canvas.getByLabelText("テンポ"); + await userEvent.clear(input); + await userEvent.type(input, "100"); + + const button = canvas.getByRole("button", { name: /変更する/ }); + await userEvent.click(button); + + await expect(args["onOk"]).toBeCalledWith({ + tempoChange: { + bpm: 100, + }, + }); + }, +}; + +export const CancelClose: Story = { + name: "キャンセルボタンを押す", + args: { ...ChangeOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const button = canvas.getByRole("button", { name: /キャンセル/ }); + await userEvent.click(button); + + await expect(args["onOk"]).not.toBeCalled(); + }, +}; + +export const Closed: Story = { + name: "閉じている", + tags: ["skip-screenshot"], + args: { + modelValue: false, + }, +}; diff --git a/src/components/Sing/ChangeValueDialog/TempoChangeDialog.vue b/src/components/Sing/ChangeValueDialog/TempoChangeDialog.vue new file mode 100644 index 0000000000..6330c72ae8 --- /dev/null +++ b/src/components/Sing/ChangeValueDialog/TempoChangeDialog.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/components/Sing/ChangeValueDialog/TimeSignatureChangeDialog.stories.ts b/src/components/Sing/ChangeValueDialog/TimeSignatureChangeDialog.stories.ts new file mode 100644 index 0000000000..56be9ca2de --- /dev/null +++ b/src/components/Sing/ChangeValueDialog/TimeSignatureChangeDialog.stories.ts @@ -0,0 +1,104 @@ +import { userEvent, within, expect, fn } from "@storybook/test"; +import { Meta, StoryObj } from "@storybook/vue3"; + +import TimeSignatureChangeDialog from "./TimeSignatureChangeDialog.vue"; + +const meta: Meta = { + component: TimeSignatureChangeDialog, + args: { + modelValue: true, + timeSignatureChange: undefined, + mode: "add", + + onOk: fn(), + onHide: fn(), + }, + tags: ["!autodocs"], // ダイアログ系はautodocsのプレビューが正しく表示されないので無効化 +}; + +export default meta; +type Story = StoryObj; + +export const CreateOpened: Story = { + name: "開いている:追加", + args: { + modelValue: true, + mode: "add", + }, +}; +export const ChangeOpened: Story = { + name: "開いている:変更", + args: { + modelValue: true, + timeSignatureChange: { + beats: 4, + beatType: 4, + }, + mode: "edit", + }, +}; + +export const ClickOk: Story = { + name: "OKボタンを押す:追加", + args: { ...CreateOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const input = canvas.getByLabelText("拍子の分子"); + await userEvent.clear(input); + await userEvent.type(input, "3"); + + const button = canvas.getByRole("button", { name: /追加する/ }); + await userEvent.click(button); + + await expect(args["onOk"]).toBeCalledWith({ + timeSignatureChange: { + beats: 3, + beatType: 4, + }, + }); + }, +}; + +export const ClickDelete: Story = { + name: "OKボタンを押す:編集", + args: { ...ChangeOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const input = canvas.getByLabelText("拍子の分子"); + await userEvent.clear(input); + await userEvent.type(input, "6"); + + const button = canvas.getByRole("button", { name: /変更する/ }); + await userEvent.click(button); + + await expect(args["onOk"]).toBeCalledWith({ + timeSignatureChange: { + beats: 6, + beatType: 4, + }, + }); + }, +}; + +export const CancelClose: Story = { + name: "キャンセルボタンを押す", + args: { ...ChangeOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const button = canvas.getByRole("button", { name: /キャンセル/ }); + await userEvent.click(button); + + await expect(args["onOk"]).not.toBeCalled(); + }, +}; + +export const Closed: Story = { + name: "閉じている", + tags: ["skip-screenshot"], + args: { + modelValue: false, + }, +}; diff --git a/src/components/Sing/ChangeValueDialog/TimeSignatureChangeDialog.vue b/src/components/Sing/ChangeValueDialog/TimeSignatureChangeDialog.vue new file mode 100644 index 0000000000..a9e44e7a34 --- /dev/null +++ b/src/components/Sing/ChangeValueDialog/TimeSignatureChangeDialog.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/src/components/Sing/SequencerGrid/Presentation.vue b/src/components/Sing/SequencerGrid/Presentation.vue index 8b6f9fb5c5..ac19ace72d 100644 --- a/src/components/Sing/SequencerGrid/Presentation.vue +++ b/src/components/Sing/SequencerGrid/Presentation.vue @@ -8,9 +8,10 @@ > @@ -19,49 +20,67 @@ :key="`cell-${index}`" x="0" :y="gridCellHeight * index" - :width="beatWidth * beatsPerMeasure" - :height="gridCellHeight" + :width="gridCellWidth" + :height="gridCellHeight * 12" :class="`sequencer-grid-cell sequencer-grid-cell-${keyInfo.color}`" /> + + - + + +