Skip to content

Improve attachment accessibility#1053

Open
brunoprietog wants to merge 1 commit into
mainfrom
attachments-a11y
Open

Improve attachment accessibility#1053
brunoprietog wants to merge 1 commit into
mainfrom
attachments-a11y

Conversation

@brunoprietog
Copy link
Copy Markdown
Collaborator

@brunoprietog brunoprietog commented May 15, 2026

Make attachments and galleries usable by keyboard and screen-reader users while keeping the existing markup and serialized HTML unchanged.

Selection announcements

  • Move the per-attachment Remove control out of the contenteditable into a contextual sibling, reachable with Alt+F10 and dismissed with Esc. The inline is gone.
  • Park the DOM range on a visually hidden span inside the figure when an attachment receives a NodeSelection, so the screen reader announces the widget label in focus mode. Lexical would otherwise clear the DOM range and leave nothing to read.

Caret behaviour around decorator nodes

  • One DecoratorNodeCaret class keeps every figure atomic for the caret: arrow keys collapse the visually-identical offset adjacent to the decorator node into its NodeSelection (including cross-block entry into galleries and mention-leading paragraphs), stepping off lands the caret on the inline neighbour or the adjacent block (and otherwise stays on the figure rather than letting Chromium drop the caret inside), and any non-arrow key drops the NodeSelection back to a normal RangeSelection so typing, Ctrl+Home / Ctrl+End, Escape and the rest of the keyboard keep working on a familiar selection.
  • Decorative <img>s inside a custom attachment carry an empty alt so screen readers don't read them as a graphic on top of the decorator node's own accessible label.

Caption

  • Label the <textarea> with aria-label and let Esc exit caption editing by re-selecting the attachment.

Live region

  • Add a polite aria-live region on <lexxy-editor> with a debounced announce() helper. Used to announce keyboard reorder moves.

Keyboard reordering (issue #877)

  • Alt+Shift+Arrow moves the selected attachment up/down and creates or extracts galleries when adjacent images line up. When the attachment is already at the start or end of its container, the screen reader is told instead of the move silently failing.

Copilot AI review requested due to automatic review settings May 15, 2026 04:24
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves accessibility and keyboard ergonomics for attachments (including galleries) by moving destructive controls out of the contenteditable, improving screen-reader announcements for NodeSelections, and adding keyboard-based attachment reordering while aiming to keep serialized HTML stable.

Changes:

  • Introduces a contextual <lexxy-attachment-toolbar> (Alt+F10 / Esc) and removes the inline delete button element.
  • Adds a live region + announce() API and uses it to announce keyboard reordering actions (Alt+Shift+Arrow…).
  • Improves caret/selection behavior around decorator nodes (atomic caret stepping + “fake” DOM selection anchoring) and enhances caption accessibility/keyboard exit.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/javascript/unit/helpers/direction.test.js Adds unit coverage for the new Direction helper.
test/browser/tests/prompts/mention_navigation.test.js Adds browser coverage for keyboard navigation/select behavior around mentions.
test/browser/tests/formatting/horizontal_divider.test.js Updates divider deletion to use the new contextual toolbar.
test/browser/tests/attachments/gallery.test.js Refactors duplicated gallery helpers into shared helpers.
test/browser/tests/attachments/gallery_navigation.test.js Adds keyboard navigation tests for moving selection within galleries.
test/browser/tests/attachments/drop_in_gallery.test.js Uses shared gallery helpers instead of local duplicates.
test/browser/tests/attachments/attachments.test.js Updates attachment deletion to use the new contextual toolbar.
test/browser/tests/attachments/attachment_toolbar.test.js Adds coverage for toolbar visibility, shortcut focusing, and delete action.
test/browser/tests/attachments/attachment_keyboard_move.test.js Adds coverage for Alt+Shift+Arrow attachment move/reorder + announcements.
test/browser/tests/attachments/attachment_fake_selection.test.js Adds coverage that the fake selection span is attached and labeled.
test/browser/tests/attachments/attachment_caption.test.js Adds coverage for Tab-to-caption and Esc-to-exit caption editing.
test/browser/helpers/gallery_test_helpers.js Centralizes gallery helper utilities used by multiple tests.
test/browser/helpers/attachment_helpers.js Adds shared helpers for selecting attachments and building attachment HTML.
src/nodes/horizontal_divider_node.js Removes inline delete UI and adds a label for accessibility tooling.
src/nodes/custom_action_text_attachment_node.js Removes inline delete UI, adds decorative alt="" for inner images, adds label.
src/nodes/action_text_attachment_node.js Adds label, caption focus helper, caption label mirroring, Esc-to-exit caption behavior.
src/helpers/lexical_helper.js Adds selection helpers for labelled decorator nodes + editor announce helper.
src/helpers/direction.js Introduces a forward/backward direction abstraction used by caret + reorder logic.
src/extensions/attachments_extension.js Registers new attachment accessibility behaviors (tab-to-caption, fake selection, caret, keyboard move).
src/elements/node_delete_button.js Removes the old inline delete button custom element.
src/elements/live_region.js Adds a debounced polite live region element for announcements.
src/elements/index.js Registers new custom elements (live region + attachment toolbar) and removes old one.
src/elements/editor.js Installs live region and attachment toolbar; exposes announce().
src/elements/attachment_toolbar.js Adds contextual toolbar UI for removing the currently selected labelled decorator node.
src/editor/attachments/keyboard_move.js Implements Alt+Shift+Arrow attachment moves + gallery creation/extraction + announcements.
src/editor/attachments/fake_selection.js Implements the hidden span + DOM-range parking for SR focus-mode announcements.
src/editor/attachments/decorator_node_caret.js Adds atomic caret behavior around decorator nodes and NodeSelection drop-back logic.
app/assets/stylesheets/lexxy-editor.css Updates floating-controls styling for the new attachment toolbar and visually-hidden helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/nodes/action_text_attachment_node.js
Comment thread src/elements/attachment_toolbar.js Outdated
Comment thread src/elements/attachment_toolbar.js Outdated
Copilot AI review requested due to automatic review settings May 15, 2026 06:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 3 comments.

Comment thread src/editor/attachments/fake_selection.js
Comment thread src/editor/attachments/decorator_node_caret.js Outdated
Comment on lines +17 to +27
connectedCallback() {
this.classList.add("lexxy-floating-controls")
this.role = "toolbar"
this.ariaLabel = "Attachment actions"
this.hidden = true

if (this.#editor) {
this.#setUpButtons()
this.#monitorSelection()
this.#registerKeyboardShortcut()
}
@samuelpecher samuelpecher self-requested a review May 18, 2026 13:36
Copy link
Copy Markdown
Collaborator

@samuelpecher samuelpecher left a comment

Choose a reason for hiding this comment

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

@brunoprietog I've made a few comments to start threads on the various workarounds proposed. I do want to note the high quality of the execution of the workarounds; my questions are on overall approach.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is the Alt+F10 shortcut tied to a floating delete button? Could the same shortcut be mapped to jumping on/off an individual DecoratorNode's deletion button?

I ask as floating buttons are more difficult to style onto the node and can end up mis-aligned with the node. The best positional code is the one we don't have to write. I think it's better to have more complexity jumping onto/off each node's button rather than moving a singleton button around.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Alt+F10 is used to focus the toolbar, which could have any button actually. The problem is that we can't have those buttons inside the contenteditable element, which is why I left them as siblings. I improved the code there by using a ResizeObserver.

Comment thread src/editor/attachments/keyboard_move.js Outdated
Comment thread src/editor/attachments/keyboard_move.js Outdated
Comment thread src/helpers/direction.js
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is the change which gives me the most pause and seems like it will need the most continuing maintenance to keep working.

What's the viability of manually announcing what's in the NodeSelection accessibility-wise?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This was the trickiest part for sure. The reason for this fake selection is that we need to make the screen reader believe that something has changed in the cursor itself. If we use a live region or anything else, even if we can announce the correct information, the cursor will also be read, with incorrect information about the cursor, such as the first letter of the previous paragraph. In other words, the live region is just an additional announcement, but it doesn’t fix the incorrect announcements about the cursor.

Copilot AI review requested due to automatic review settings May 23, 2026 13:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 4 comments.

Comment thread test/javascript/unit/helpers/direction.test.js Outdated
Comment thread src/elements/attachment_toolbar.js
Comment thread src/elements/attachment_toolbar.js
Comment thread src/nodes/action_text_attachment_node.js
Copilot AI review requested due to automatic review settings May 23, 2026 14:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 2 comments.

Comment thread src/editor/attachments/keyboard_move.js Outdated
Comment thread src/elements/attachment_toolbar.js
Copilot AI review requested due to automatic review settings May 23, 2026 16:30
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 29 out of 29 changed files in this pull request and generated 3 comments.

Comment thread src/elements/attachment_toolbar.js
Comment thread src/editor/attachments/keyboard_move.js
Comment thread src/editor/attachments/decorator_node_caret.js
Make attachments and galleries usable by keyboard and screen-reader
users while keeping the existing markup and serialized HTML unchanged.

Selection announcements
- Move the per-attachment Remove control out of the contenteditable into
  a contextual <lexxy-attachment-toolbar> sibling, reachable with Alt+F10
  and dismissed with Esc. The inline <lexxy-node-delete-button> is gone.
- Park the DOM range on a visually hidden span inside the figure when an
  attachment receives a NodeSelection, so the screen reader announces
  the widget label in focus mode. Lexical would otherwise clear the
  DOM range and leave nothing to read.

Caret behaviour around decorator nodes
- One DecoratorNodeCaret class keeps every figure atomic for the caret:
  arrow keys collapse the visually-identical offset adjacent to the
  decorator node into its NodeSelection (including cross-block entry
  into galleries and mention-leading paragraphs), stepping off lands
  the caret on the inline neighbour or the adjacent block (and
  otherwise stays on the figure rather than letting Chromium drop the
  caret inside), and any non-arrow key drops the NodeSelection back to
  a normal RangeSelection so typing, Ctrl+Home / Ctrl+End, Escape and
  the rest of the keyboard keep working on a familiar selection.
- Decorative <img>s inside a custom attachment carry an empty alt so
  screen readers don't read them as a graphic on top of the decorator
  node's own accessible label.

Caption
- Label the <textarea> with aria-label and let Esc exit caption editing
  by re-selecting the attachment.

Live region
- Add a polite aria-live region on <lexxy-editor> with a debounced
  announce() helper. Used to announce keyboard reorder moves.

Keyboard reordering (issue #877)
- Alt+Shift+Arrow moves the selected attachment up/down and creates or
  extracts galleries when adjacent images line up. When the attachment
  is already at the start or end of its container, the screen reader is
  told instead of the move silently failing.
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.

3 participants