Skip to content

Edit the whole form as one document (part 1) - #268

Closed
PhilReinking wants to merge 22 commits into
stack/bits-274-phase-1from
feat/bits-282-document-editor
Closed

PhilReinking wants to merge 22 commits into
stack/bits-274-phase-1from
feat/bits-282-document-editor

Conversation

@PhilReinking

Copy link
Copy Markdown
Contributor

The editor shows the whole form as one column of short questions. The question you work on opens in place as a card, so a phone can edit questions too. This is the first of several stacked parts. Part of BITS-282.

  • The old question list and workbench are gone. Groups are sections with an inline name, Hide in their menu and an empty hint.
  • Hidden, Randomize and Custom response moved into the card's Settings. Every answer type keeps its settings.
  • Rules still open today's rules panel. The menu keeps Add question below, Copy ID and Delete.
  • Creating a question can now take a group, for the empty group's "add a question".

Tested: Vitest, lint, vue-tsc and npm run build locally (stacked PR, no CI). By hand at 1440, 1024, 390 and 360: no sideways scroll, every answer type, drag saved.

Not tested: a screen reader by hand.

Known gap: Delete still asks with the browser dialog, and Duplicate, Move, the outline and a phone bottom sheet come in the next parts.

🤖 Generated with Claude Code

PhilReinking and others added 7 commits October 13, 2026 01:04
The question list and the workbench become one column. Each question is a
compact row (number, type icon, text on up to 2 lines, a meta line with
Required, rules, Hidden and the identifier), groups are sections. The open
question is a card in place: text with its format toolbar, choices, then
answer type, Required, Settings (this type's settings inline), Rules and a
menu. A new question opens with the cursor in its text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Delete and the drag grip showed only on hover, 96 px left of the row and
outside the tab order. Now they sit at the end of the row, always visible;
delete is a named button ("Delete choice “Yes”").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The toolbar buttons had their name only in a tooltip. They now have an
aria-label, aria-pressed and type="button", in a group named "Text format".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the duplicate endpoint (#262) and the editor loading states (#263).
The page keeps #263's loading; the document shows its skeleton and error box
instead of loading on its own. The old list (Storyboard.vue) stays removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Try again loads the questions, the cursor goes into the open
question's text (or onto Add question); when it fails again, back onto Try
again. Before, focus fell to the page. A question load that answers after
the editor closed or switched forms no longer fills the store or opens a
question (which also changed the next page's address). A question opened by
click or added now takes the cursor through one shared value, because its
text editor shows up a moment after the card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@PhilReinking PhilReinking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes at d32d1cd. No CI (stacked): Vitest, lint, vue-tsc and the build pass locally.

Must: when a question gets a type with an input field (text, email, number, file, rating, date), the field is only made once Settings is opened. Until then the live form shows nothing to answer.

Held: every setting of every answer type is still reachable, the 4 new loading tests fail without the fixes, and the closed row shows plain text only.

</div>

<QuestionSettings
v-if="settingsOpen"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Configure* parts create the answer field when they mount. Today they mount as soon as the question opens. Here they mount only with Settings open, so a new "Short text" question has no field in the live form. Keeping the panel mounted fixes it for every type.

Suggested change
v-if="settingsOpen"
v-show="settingsOpen"

Comment on lines +2 to +3
<div
role="group"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

id is a prop here, so it never lands on the element, and the Settings button's aria-controls points at nothing.

Suggested change
<div
role="group"
<div
:id="id"
role="group"

class="rounded-lg border border-grey-200 bg-grey-50 py-2 pr-2"
:class="isOpen && 'ring-1 ring-blue-500'"
>
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 pb-2 pl-3">

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Groups can't be moved any more. The list only starts a drag on .document-grip, and the group header has none. Today a group can be dragged. Can the header get a grip like the questions, or does a later part bring it?

</div>
</section>
<ChoiceRowActions
:label="label ? `choice “${label}”` : `choice ${index + 1}`"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This delete button is new to the keyboard, but after it the row is gone and focus falls to the page. Cmd+Backspace moves focus to a neighbor choice first. The same goes for the policy delete in ConsentInteraction.

PhilReinking and others added 7 commits October 13, 2026 01:32
The Configure* parts create the block's interaction when they mount.
Keep QuestionSettings mounted (v-show) so a new Short text, Email,
Number, Long text, File, Rating, Scale or Date question gets its field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row's Delete button and Cmd+Backspace now ask the list to delete.
Once the row is gone, focus moves to the row above, the new first row,
or the Add button. A failed delete leaves focus where it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@PhilReinking PhilReinking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK at e4fc5f5 (a7fa8d5 plus the updated base; this PR's own changes are the same). No CI (stacked): Vitest, lint, vue-tsc and the build pass locally.

All 5 items from the last review are fixed, and the 6 new tests fail on d32d1cd. A failed choice delete keeps focus on its button. One should inline: with a custom answer on, focus still drops after a delete.

// a failed delete keeps the row, and focus stays in it
if (editableInteractions.value?.includes(item)) return;

if (!(await focusNeighborItem(index))) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a custom answer on, the list useKeyboardNavigation gets (activeInteractions) also holds that hidden row, but index counts only the visible choices. So after a delete, focus can land on the hidden row and falls to the page. Enter on the last choice also throws when the custom answer is last, so "Enter adds a choice" adds nothing. Both work with the visible list:

-const { activeInteractions, editableInteractions } = useActiveInteractions(
+const { editableInteractions } = useActiveInteractions(
...
-} = useKeyboardNavigation(activeInteractions, async () => {
+} = useKeyboardNavigation(editableInteractions, async () => {

PhilReinking and others added 6 commits October 13, 2026 01:43
The keyboard helper got the list with the hidden custom answer row, but
row numbers count only the visible choices. Focus after a delete could
land on the hidden row, and Enter on the last choice threw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Editor gets a bare prop: no border around the toolbar and the text.
The card uses it and drops the word counter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One border around the key, the text, the grip and delete. Screen
readers now say "Choice 2" and "Reply for choice 2" instead of the
placeholder "Label".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@PhilReinking PhilReinking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK at 3938f6b. No CI (stacked): Vitest, lint, vue-tsc and the build pass locally.

The should from the last review is fixed, and the 5 new tests fail on e4fc5f5. Beyond the PR text: without bare the editor gets the same classes as before, so the completion page's editor is unchanged. A second click on Add choice makes no second choice.

@PhilReinking PhilReinking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK at 18c92f9, a merge of 3938f6b and the stack at 2f7ecf9. No CI (stacked), local runs: Vitest 294, lint, vue-tsc, build.

The diff to the stack equals the reviewed one at 3938f6b byte for byte, except #268's own changelog line. PHP is unchanged. The full PHP suite had one failure in a test outside this PR that depends on database order. It passes alone and in a feature-only run.

Brings in the delete dialogs (#269) and the save status (#267).

Conflicts: the old Workbench, Sidebar Block and Group stay deleted.
Choice and policy rows keep their in-row delete button, which now asks
first through useDeleteChoice; the list still does the delete so it can
move focus once the row is gone (useDeleteChoice takes that delete).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@PhilReinking PhilReinking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK at 0f2f881 (the stack with #269 and #267 merged in). No CI (stacked), local runs: Vitest 333, lint, vue-tsc, build.

The merge keeps part a's diff, except the choice and policy rows, which now ask through #269's step before the list deletes. Held: a choice people picked asks first, one nobody picked goes at once, a failed count asks anyway, focus lands where part a's tests say, and a failed delete keeps the row and its focus.

@PhilReinking

Copy link
Copy Markdown
Contributor Author

Landed in main with #282 (the editor rebuild as one PR); this PR's commits are part of it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant