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>
Analyzed whether emitting Markdown as an intermediate format would improve
lib/pdf_text_to_html.rb and app/javascript/template_builder/pdf_text_to_html.js.
Decision: no change warranted. Key reasons: no full Markdown gem on Ruby side,
snarkdown is inline-only (no block elements), dir="auto" RTL support can't be
expressed in Markdown, and PDF text contains raw * _ # characters that would
corrupt Markdown rendering.
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>
Documents the accessibility specialist review of the proposed tab
switcher feature: ARIA tablist/tab/tabpanel pattern requirements,
heuristic HTML parsing strategy for extracted text, signing form
UX recommendation (read-only text view + always-visible Vue panel),
and concrete pitfalls including DaisyUI incompatibility with APG
keyboard model, 15-page cap handling, and RTL text direction.
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.
Three translation keys added as part of a11y accessibility work
were missing from the locale file:
- preview_of: used in submissions/show.html.erb img alt text
- your_signature: used in profile/index.html.erb img alt text
- your_initials: used in profile/index.html.erb img alt text
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The submit_form/show.html.erb uses t('page') and t('of') to build
accessible alt text for document page images (e.g. "Page 1 of Contract").
These keys were added as part of a11y work but never added to the locale file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The _navbar.html.erb aria-label uses t('user_menu') but the key was
never added to the locale file, causing a MissingTranslationData error
in development.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Completed Tasks 5 and 6:
- Task 5: Added ARIA labels to 12 icon-only buttons
- Task 6: Added keyboard support to 3 custom web components
Phase 1 Status:
- 6 of 7 tasks completed (86%)
- Only Task 7 (testing) remains, blocked by Ruby version requirement
- 5 WCAG 2.2 Level A criteria now satisfied
WCAG Compliance Progress:
✅ 1.1.1 Non-text Content (Level A)
✅ 1.3.1 Info and Relationships (Level A)
✅ 2.1.1 Keyboard (Level A) - NEW
✅ 2.4.1 Bypass Blocks (Level A)
✅ 4.1.2 Name, Role, Value (Level A) - NEW
Next: Resolve Ruby blocker and complete automated testing.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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>
This file provides guidance for Claude Code when working in this repository,
including:
- Development commands for testing, linting, and database management
- Architecture overview of Rails backend and Vue frontend
- Testing structure with RSpec, Capybara, and Cuprite
- Deployment configurations for Docker and cloud platforms
- Key libraries, patterns, and conventions
- Multitenant support and environment configuration
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>