Move aria-labelledby conditional to a local variable above the tag
so the attribute uses a <%= %> expression instead of <% %> inside the tag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move flash type conditionals to local variables above the tag so
role/aria-live use <%= %> expressions instead of <% %> statements
inside the tag. Also remove unsupported aria_hidden: kwarg from
svg_icon calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
svg_icon only accepts class:; the buttons already have aria-label so the
icons are implicitly decorative.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 8-A: Keyboard alternative for drag-and-drop field placement
- Default field items: tabindex/role=button + Enter/Space to emit add-default-field
- Field type grid buttons: keyboard click (event.detail===0) adds field directly (skips draw mode)
- builder.vue: addDefaultField() handles keyboard-triggered field insertion + announces via aria-live
- 8-B: Context menu keyboard trigger on placed field areas
- area.vue: tabindex=0 + areaLabel computed + onAreaKeydown handler
- ContextMenu key / Shift+F10 synthesizes MouseEvent('contextmenu') at element center
- 8-C: Field settings dropdown focus trap
- label @focus renders dropdown for keyboard users
- @keydown.escape.stop closes dropdown
- 8-D: Live region announcements for field add/remove
- announcePolite() called in addField(), addDefaultField() (builder.vue), and removeField() (fields.vue)
- i18n: field_type_added + field_removed keys added in all 7 languages (en/es/it/pt/fr/de/nl)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
HIGH PRIORITY — Icon-only buttons with no accessible name:
- _toggle_view.html.erb: aria-label on Templates/Submissions view buttons
- _template.html.erb: aria-label on move, restore, edit, clone, archive/delete
(also fixes bug: was aria_label: t('restore') for all, now dynamic)
- _title.html.erb: aria-label on move-folder pencil link
- submissions/show.html.erb: aria-label on download options dropdown toggle,
audit log link, event log link, edit submitter pencil in parties view
- submit_form/show.html.erb: aria-label on scroll decline button (mobile icon-only)
- contenteditable.vue: span→role=button + aria-label + keyboard handlers (Enter/Space)
on edit pencil; aria-hidden on decorative icon
- signature_step.vue: aria-label + aria-hidden on minimize link
MEDIUM PRIORITY — Form inputs with missing labels:
- text_step.vue: conditional aria-label fallback on input/textarea when no
visible label rendered (showFieldNames=false or field has no name)
- area.vue: aria-label on multiple-select checkbox (matches radio pattern)
MEDIUM PRIORITY — Images with generic hardcoded alt text:
- area.vue: replace 'Image'/'Stamp'/'Knowledge-based authentication' with
t('image')/t('stamp')/t('kba') for i18n consistency
MEDIUM PRIORITY — Dropdown/menu ARIA:
- _navbar.html.erb: aria-controls="user-menu-list" on user menu trigger;
id="user-menu-list" on menu <ul>
MEDIUM PRIORITY — Form grouping:
- storage_settings/index.html.erb: wrap radio buttons in <fieldset><legend>
LOW PRIORITY — Required field indicator:
- mobile_fields.vue: replace tooltip span with <abbr title="required"> pattern
LOW PRIORITY — Keyboard accessibility:
- templates_folders/edit.html.erb: tabindex="0" on folder toggle label
i18n: add download_options key
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All focusable elements in the submission card now announce document
status, submitter identity, and action context to screen readers:
Status badges: "Awaiting – mpaiva+test@clearcompany.com"
Name links: "mpaiva+test@clearcompany.com – Awaiting"
Edit (icon): "Edit mpaiva+test@clearcompany.com"
Sign Now: "Sign Now – mpaiva+test@clearcompany.com"
View: "View – mpaiva+test@clearcompany.com"
Archive: "Archive – mpaiva+test@clearcompany.com"
Overlay link (invisible full-row cover) marked aria-hidden + tabindex=-1
to eliminate the duplicate focus stop and keyboard trap.
Applies to both single-submitter and multi-submitter row variants.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All buttons now include the template name for context so screen reader
users know which template they are acting on:
- Preferences (icon-only): aria-label added - was completely unlabelled
- Link, Archive, Clone, Edit, Preview, Restore: aria-label adds template
name (e.g. "Edit - NDA-accessible")
- Inner icon/text spans marked aria-hidden to prevent double-announcement
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pl-3 (12px) exactly matched the 8px outline + 4px offset with no buffer.
pl-4 (16px) matches pr-4 and gives 4px clearance to prevent subpixel
clipping of focus rings on left-column field type buttons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three issues fixed in the template builder field type grid:
1. Focus ring clipping: the sidebar container had pl-0.5 (2px) which caused
the 8px outline + 4px offset to be clipped on left-column buttons (Text,
Checkbox, Radio, Stamp). Increased to pl-3 (12px) to give the ring room.
2. ARIA state: add aria-pressed to each field type button so screen readers
announce when a type is active (selected for drawing).
3. Decorative content: add aria-hidden to the drag handle div and field icon
so screen readers only read the text label, not "icon name + label".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The label element used as the upload trigger had no tabindex, making it
unreachable by keyboard. Add tabindex="0" and role="button" so it is
included in Tab order and correctly announced by screen readers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Test mode toggle checkbox is small; keyboard focus should highlight
the entire row. Use .menu label:has(.toggle:focus-visible) to apply the
outline on the label container, and suppress it on the toggle itself.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Test Mode toggle (input[type="checkbox"]) was skipped by arrow key
navigation because _menuItems() only queried a[href] and button elements.
- Extend _menuItems() to include input[type="checkbox"]
- Add role="menuitemcheckbox" and aria-checked in connectedCallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All DaisyUI dropdown content elements had tabindex="0" which put the
entire menu container in the keyboard tab order, causing our :focus-visible
rule to outline the whole dropdown box rather than individual menu items.
Changed to tabindex="-1" in 14 files (3 ERB + 11 Vue):
- submissions_filters/_filter_button.html.erb
- shared/_templates_order_select.html.erb
- submissions/show.html.erb
- template_builder/{payment_settings,field_type,field,builder,
custom_field,upload,google_drive_document_settings,area,
font_modal(x2),field_submitter(x2),mobile_fields}.vue
tabindex="-1" keeps mouse-click focus working (DaisyUI :focus-within
CSS still fires) while removing the container from the Tab order.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change tabindex="0" -> tabindex="-1" on <ul class="menu"> so the
dropdown container is not in the keyboard tab order (user_menu.js
handles focus programmatically; tabindex="-1" still allows mouse
click to satisfy DaisyUI :focus-within dropdown CSS)
- Add .menu li > a:focus-visible and .menu li > button:focus-visible
to global focus rule to override DaisyUI's menu-item focus selectors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SCSS: add .input, .select, .checkbox, .radio, .toggle, .tab, .link
to :focus-visible rule so DaisyUI's higher-specificity component-level
rules are overridden by our 8px blue outline in both stylesheets.
Vue: remove !outline-0 and !ring-0 (which used !important to kill
outlines entirely) from 21 elements across 4 template_builder files:
- field_settings.vue (13 inputs/selects)
- payment_settings.vue (5 inputs/selects)
- fields.vue (2 inputs)
- formula_modal.vue (1 textarea)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add tabindex="0" to both decline <label> buttons so they enter the
tab order and :focus-visible applies (labels are not natively focusable)
- Add .btn:focus-visible alongside :focus-visible in SCSS to override
DaisyUI's higher-specificity .btn:focus-visible rule, ensuring the
global 8px blue ring applies to all DaisyUI buttons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Audit found 10 overrides across 8 files that suppressed or replaced
the global focus ring. Removed all of them:
- templates/_file_form.html.erb: remove outline-none + focus:ring-*
- templates_clone/_form.html.erb: same
- templates_preferences/_recipients.html.erb: same
- template_builder/contenteditable.vue: remove outline-none
- template_builder/document.vue: remove focus:ring-2 from tab buttons
- template_builder/area.vue: remove focus:ring-1 from 3 elements
- template_builder/font_modal.vue: remove focus:ring-2 from contenteditable
- submission_form/phone_step.vue: remove focus-within:outline from container
All interactive elements now rely solely on the global :focus-visible
rule (8px solid rgb(14,99,200), 4px offset) in application.scss/form.scss.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace input:focus with :focus-visible to cover all focusable elements
(buttons, links, inputs, selects, textareas, [tabindex] elements).
Using :focus-visible shows the ring only for keyboard navigation,
not mouse clicks — the correct accessibility behavior per WCAG 2.4.11.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Apply 8px solid blue (rgb(14,99,200)) outline with 4px offset on
input:focus for improved keyboard navigation visibility in both the
application and public form stylesheets.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Point skip links in form and plain layouts to #tab-pdf (the first
tab button, naturally focusable as a <button>)
- Add tabindex="-1" fallback <div id="tab-pdf"> when has_full_text is
false (scanned PDFs with no extractable text) so the skip link
always has a valid target
- Remove now-unused tabindex="-1" from #scrollbox and #main-content
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add tabindex="-1" to #scrollbox in submit_form/show so focus lands
there after skip link activation (not just scroll position)
- Add skip link to plain layout (used by submissions/show)
- Add id="main-content" tabindex="-1" to submissions/show outer div
as the skip link target
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bullet was warning that ActiveStorage::Attachment => [:blob] was eager
loaded but never used (Bullet doesn't detect blob access through to_json
method delegation). Remove the preload; blob lazy-loads per attachment
on demand, which is acceptable for the small attachment counts in signing forms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace raw String interpolation of aria-labelledby attribute with
inline quoted attribute interpolation, which BetterHtml permits.
Always render the attribute since the ID is always generated; a
reference to a non-existent element is harmless when no title is set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace raw string interpolation of aria-labelledby in the turbo-modal
tag with html_attributes() helper to satisfy BetterHtml security linter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- M1: submissions/show.html.erb — aria-hidden on decorative color dot; sr-only text on
colored field overlays (field name + submitter) for screen reader users (WCAG 1.4.1)
- M2: field_submitter.vue — aria-hidden on all decorative color dots; aria-label on
compact mode label (selectedSubmitter name); changed inner button to span (WCAG 1.4.1)
- M8: profile/index.html.erb — inline validation error messages (role="alert") with
aria-describedby + aria-invalid on all profile and password form fields (WCAG 3.3.1)
- M10: Add aria-label to icon-only buttons/links across 7 files:
- field.vue: draw, formula, condition, settings, remove, draw-option buttons
- preview.vue: document condition and reorder buttons
- signature_step.vue: QR toggle (with aria-pressed) and close QR buttons
- text_step.vue: toggle multiline text button
- import_list.vue: preview column data info button
- Add i18n keys: show_qr_code, close_qr_code (submission_form); preview_column_data (template_builder)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- C1: Remove maximum-scale/user-scalable=no from viewport meta (WCAG 1.4.4)
- C2: Restore focus indicators on 7 inputs — replace outline-none/ring-0 with ring (WCAG 2.4.7)
- C3: Add focus trap + dialog role to turbo_modal.js; focus on open, restore on close (WCAG 2.4.3, 2.1.2)
- C4/C6: Replace all alert()/prompt() with ARIA live regions and custom password dialog (WCAG 3.3.1, 4.1.3)
- C5: Add aria-label to signature text input, signing reason select, checkbox and radio in area.vue (WCAG 1.3.1, 4.1.2)
- C7: Replace text-gray-100 → text-white on dark code blocks in _embedding.html.erb (WCAG 1.4.3)
- H1: Change submission name div → h1 in submit_form/show.html.erb (WCAG 2.4.6)
- H2: form.html.erb already has lang attr (confirmed correct)
- H3: Add skip link to form.html.erb layout (WCAG 2.4.1)
- H4: Replace text-gray-300/400 → text-gray-600 on light backgrounds across 5 files (WCAG 1.4.3)
- H5: Replace <a> close buttons → <button> in turbo_modal partials (WCAG 4.1.2)
- H6: Fix duplicate id="decline_button" → header/scroll variants (WCAG 4.1.1)
- L10: Add role="button" tabindex="0" to html_modal label close (WCAG 4.1.2)
- Add shared aria_announce.js utility for assertive/polite live region announcements
- Add aria-labelledby to turbo modal dialog with per-instance IDs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Custom elements are display:inline by default, so margin-top on the
inner tablist div was not producing visible spacing. Move the margin
to the document-tabs element itself with an inline style (immune to
Tailwind recompile timing) and add class="block" so it participates
in the block flow and the margin takes effect.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The header has margin-bottom: -16px which pulls the next element up,
so mt-10 (40px) only produced ~24px of visible gap. mt-16 (64px) gives
~48px of actual space between the heading and the tab group.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The tablist was sticky top-[60px] so it followed the user while scrolling.
Removing sticky/z-index/background/offset classes lets it sit naturally in
the document flow directly below the header.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The signing form (submit_form/show.html.erb) loads the form.js bundle,
not application.js. DocumentTabs was only registered in application.js
so the custom element was unknown in the signing context: connectedCallback
never fired, tabs were inert, and the PDF panel content appeared blank.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
primary color (#e4e0e1) is near-white against the base-100 background
(#faf7f5), giving ~1.1:1 contrast - essentially invisible. Replace with
text-base-content + border-neutral (#291334, ~15.9:1 against base-100).
Active state distinction is now conveyed by border-neutral underline +
font-semibold (not by color alone, satisfying WCAG 1.4.1 Use of Color).
Inactive tabs retain font-medium + border-transparent.
Also remove hover:text-primary from inactive tabs - primary is near-white
so that hover would have made inactive tab text invisible on hover.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
text-base-content/60 (~4.1:1 against base-100) fails the 4.5:1 AA minimum
for normal-weight text. Change inactive tab to text-base-content (full
opacity, ~14:1) so contrast is unambiguously compliant. Visual distinction
between active/inactive now relies on the colored border underline + primary
text color, not opacity dimming. Hover updated to hover:text-primary to
preview the tab's active color before clicking.
Affects: submissions/show, submit_form/show, document_tabs.js, document.vue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create pdf_text_to_html.js: JS port of the Ruby heuristic parser
(ALL_CAPS→h2, numbered headings→h3, bullets→ul/li, body→p dir=auto)
- Add pdf_view, text_view, document_view_options keys to i18n.js (en)
- Update document.vue: tab switcher shown when all pages have extracted
text; PDF View renders the existing page images; Text View renders
heuristic HTML in a prose container with per-page sections
- ArrowLeft/ArrowRight keyboard navigation between tabs with focus management
- Tab is hidden entirely for scanned/image-only PDFs (hasFullText gate)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The page.vue component uses container-type: size for CSS container
queries. This containment context interferes with the clip: rect(0,0,0,0)
technique used by Tailwind's sr-only class, causing the hidden page text
to render visually below the PDF image.
Replace sr-only class with position: absolute; left: -9999px off-screen
technique which is robust against CSS containment contexts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create PdfTextToHtml heuristic parser (ALL_CAPS→h2, numbered→h3, bullets→ul, body→p)
- Create document-tabs custom element (ARIA APG tab pattern, roving tabindex, localStorage persistence)
- Register document-tabs element in application.js
- Add tab switcher to submissions/show and submit_form/show when all pages have extracted text
- Add text panel with per-page sections to both views
- Fix role="region" bug on sr-only page text divs (excess ARIA landmarks)
- Add 5 new i18n keys: pdf_view, text_view, document_view_options, text_view_disclaimer, signing_fields_below
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts PDF text during upload via Pdfium and stores it in attachment
metadata (pdf.pages_text), then surfaces it in visually-hidden sr-only
regions in both the signing form and submission preview views. Also adds
alt text to template builder page images and ARIA role/label to the
page-container custom element.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added new gems: axe-core-api, axe-core-rspec, axiom-types, coercible, descendants_tracker, dumb_delegator, and virtus for enhanced accessibility and data handling.
- Updated database schema comments for clarity, ensuring proper formatting for index definitions in document_generation_event.rb, email_event.rb, lock_event.rb, submission_event.rb, and submission.rb.
- Adjusted the schema version in db/schema.rb to reflect the latest changes.
These updates contribute to ongoing accessibility improvements and maintain code clarity.
Added Enter and Space key handlers to 3 custom elements for full keyboard accessibility:
1. clipboard_copy.js:
- Added keydown listener for Enter/Space keys
- Set tabindex="0" and role="button" for keyboard focus
- Refactored click handler into reusable copyToClipboard function
- Keyboard users can now copy text without a mouse
2. download_button.js:
- Added keydown listener for Enter/Space keys
- Set tabindex="0" and role="button" for keyboard focus
- Keyboard users can now trigger file downloads
3. password_input.js:
- Added keydown listener to togglePasswordVisibility element
- Set tabindex="0" and role="button" on toggle button
- Properly cleanup event listener in disconnectedCallback
- Keyboard users can now toggle password visibility
All implementations:
- Use e.preventDefault() to prevent default Space key scrolling
- Check for existing tabindex/role attributes before setting
- Follow WCAG 2.1.1 (Keyboard, Level A) guidelines
- Support both Enter and Space keys per ARIA authoring practices
This satisfies WCAG 2.2 Success Criterion 2.1.1 (Keyboard, Level A).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixed 6 images missing alt attributes across submission form components:
- signature_step.vue: Added dynamic alt text with field name and "preview"
- initials_step.vue: Added dynamic alt text with field name and "preview"
- image_step.vue: Added dynamic alt text with field name and "preview"
- area.vue: Added alt text for 5 different image types:
* Image field
* Stamp field
* Knowledge-based authentication (KBA) field
* Signature field
* Initials field
All alt text uses field.name when available, falling back to descriptive defaults.
This satisfies WCAG 2.2 Success Criterion 1.1.1 (Non-text Content, Level A).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit establishes the foundation for WCAG 2.2 Level AA compliance:
Infrastructure:
- Add axe-core-rspec gem for automated accessibility testing
- Create spec/accessibility/ directory structure
- Add accessibility_helpers.rb with custom WCAG test helpers
- Add comprehensive testing documentation (README.md, SETUP_NOTES.md)
Semantic Landmarks (WCAG 2.4.1):
- Add <main> landmark with id="main-content" to application layout
- Add <nav> landmark with aria-label to navbar
- Add skip navigation link for keyboard users
- Skip link uses focus:translate-y-0 to appear only on keyboard focus
These changes address critical barriers for screen reader and keyboard users.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>