diff --git a/CHANGELOG.md b/CHANGELOG.md index e730a4e2..80654d07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ Lines marked **Upgrade step** need you to act after updating. - The form list loads faster when a team has many forms. It now needs the same few database queries for one form or a hundred. Before, every form added 3 queries. (#256) - On screens narrower than 1280 px, a form's editor bar now sits in two rows under the app header: the form with Dashboard and Preview, then Editor, Settings, Integrations and Submissions as tabs. Nothing in it scrolls sideways any more, and the header keeps the logo and your team name. On every screen, keyboard focus in the editor bar and the account menu now shows a blue ring. (#257) - A form's pages have one top bar instead of two: the form's name with its menu, a Draft or Live label, the pages, Preview, Publish and your account. Publish first checks for questions without text, rules that point to a deleted question and a missing privacy link. Once the form is live, Share has its link, Open form and Unpublish. On a phone the pages sit in a tab bar at the bottom. (#265) -- The form editor shows your whole form as one column. Each question shows its number, answer type, text, and if it is required, has rules or is hidden. The question you work on opens right there, also on a phone, where you couldn't edit questions before. Hiding a question, random order and custom answers are now under the question's Settings. An outline next to the form jumps to any question, and on a phone a question's Settings open from the bottom of the screen. (#268, #272) +- The form editor shows your whole form as one column. Each question shows its number, answer type, text, and if it is required, has rules or is hidden. The question you work on opens right there, also on a phone, where you couldn't edit questions before. Hiding a question, random order and custom answers are now under the question's Settings. An outline next to the form jumps to any question, and on a phone a question's Settings open from the bottom of the screen. Move a question with Alt+Up/Down, with Move up and Move down in its menu, or by dragging. (#268, #272, #275) - Deleting a question, a group, an answer choice, a consent policy or a logic rule in the form editor now asks first in the app's own dialog. It names the question, how many answers will be deleted with it, and the questions whose rules will stop working. A choice nobody picked, or a policy nobody agreed to, is still deleted at once. (#269) ### Fixed diff --git a/resources/js/app.ts b/resources/js/app.ts index 4fcc4e6a..29da4271 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -178,6 +178,8 @@ createInertiaApp({ autoHide: true, placement: "auto", html: true, + // a tooltip shown on hover must not take the focus, e.g. from the question text + noAutoFocus: true, }, }, }) diff --git a/resources/js/components/Factory/Document/BlockMenu.test.ts b/resources/js/components/Factory/Document/BlockMenu.test.ts new file mode 100644 index 00000000..df16bedb --- /dev/null +++ b/resources/js/components/Factory/Document/BlockMenu.test.ts @@ -0,0 +1,65 @@ +import { flushPromises, mount, VueWrapper } from "@vue/test-utils"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { useForm, useWorkbench } from "@/stores"; +import BlockMenu from "./BlockMenu.vue"; + +const blocks = [ + { id: 1, uuid: "q1", type: "radio" }, + { id: 2, uuid: "q2", type: "radio" }, +] as FormBlockModel[]; + +let wrapper: VueWrapper | undefined; + +const openMenu = async (block: FormBlockModel) => { + wrapper = mount(BlockMenu, { + props: { block, label: "Options for question 1" }, + attachTo: document.body, + }); + await wrapper.find("button").trigger("click"); + await flushPromises(); +}; + +// the menu opens in a portal +const item = (label: string) => + [...document.querySelectorAll("[role=menuitem]")].find( + (element) => element.textContent?.trim() === label, + ); + +describe("a block's menu", () => { + beforeEach(() => { + vi.clearAllMocks(); + useWorkbench().$reset(); + useForm().$reset(); + useForm().blocks = blocks; + }); + + afterEach(() => { + wrapper?.unmount(); + document.body.innerHTML = ""; + }); + + it("moves a question down, for touch and pointer users", async () => { + await openMenu(blocks[0]); + + item("Move down")!.click(); + await flushPromises(); + + expect(useForm().changeBlockSequence).toHaveBeenCalledWith( + false, + 1, + blocks[0], + ); + }); + + it("can't move the first block up or the last one down", async () => { + await openMenu(blocks[0]); + expect(item("Move up")?.getAttribute("aria-disabled")).toBe("true"); + expect(item("Move down")?.getAttribute("aria-disabled")).toBeNull(); + wrapper!.unmount(); + document.body.innerHTML = ""; + + await openMenu(blocks[1]); + expect(item("Move up")?.getAttribute("aria-disabled")).toBeNull(); + expect(item("Move down")?.getAttribute("aria-disabled")).toBe("true"); + }); +}); diff --git a/resources/js/components/Factory/Document/BlockMenu.vue b/resources/js/components/Factory/Document/BlockMenu.vue index 6bfb93ec..230b0bfe 100644 --- a/resources/js/components/Factory/Document/BlockMenu.vue +++ b/resources/js/components/Factory/Document/BlockMenu.vue @@ -34,6 +34,20 @@ :label="block.is_disabled ? 'Show group' : 'Hide group'" @click="toggleHidden" /> + + (); const store = useForm(); const workbench = useWorkbench(); const { addBlock } = useAddBlock(); +const { canMove, moveBlock } = useMoveBlock(); const item = "w-full min-h-11 text-left text-sm lg:min-h-0"; const isGroup = computed(() => props.block.type === "group"); diff --git a/resources/js/components/Factory/Document/DocumentGroup.vue b/resources/js/components/Factory/Document/DocumentGroup.vue index 715801ff..36a11dd4 100644 --- a/resources/js/components/Factory/Document/DocumentGroup.vue +++ b/resources/js/components/Factory/Document/DocumentGroup.vue @@ -7,6 +7,7 @@ >