Fix WCAG 2.1 AA critical and high issues: viewport zoom, focus indicators, modals, alerts, labels

- 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>
pull/599/head
Marcelo Paiva 3 weeks ago
parent 692341dd2d
commit e41dd55716

@ -0,0 +1,18 @@
export function announceError (message, timeout = 7000) {
const el = document.createElement('div')
el.setAttribute('role', 'alert')
el.setAttribute('aria-live', 'assertive')
el.className = 'sr-only'
el.textContent = message
document.body.append(el)
setTimeout(() => el.remove(), timeout)
}
export function announcePolite (message, timeout = 5000) {
const el = document.createElement('div')
el.setAttribute('aria-live', 'polite')
el.className = 'sr-only'
el.textContent = message
document.body.append(el)
setTimeout(() => el.remove(), timeout)
}

@ -1,3 +1,5 @@
import { announceError } from './aria_announce'
export default class extends HTMLElement {
connectedCallback () {
this.clearChecked()
@ -17,7 +19,7 @@ export default class extends HTMLElement {
navigator.clipboard.writeText(text)
} else {
if (e.target.tagName !== 'INPUT') {
alert(`Clipboard not available. Make sure you're using https://\nCopy text: ${text}`)
announceError(`Clipboard not available. Please use HTTPS. Copy text: ${text}`)
}
}
}

@ -1,4 +1,5 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
import { announceError } from './aria_announce'
export default targetable(class extends HTMLElement {
static [target.static] = ['defaultButton', 'loadingButton']
@ -45,7 +46,8 @@ export default targetable(class extends HTMLElement {
this.downloadUrls(urls)
}
} else {
alert('Failed to download files')
announceError('Failed to download files')
this.toggleState()
}
})
}

@ -1,3 +1,5 @@
import { announceError } from './aria_announce'
export default class extends HTMLElement {
connectedCallback () {
this.form.addEventListener('submit', (e) => {
@ -21,7 +23,7 @@ export default class extends HTMLElement {
const data = JSON.parse(await resp.text())
if (data.error) {
alert(data.error)
announceError(data.error)
}
} catch (err) {
console.error(err)

@ -1,16 +1,68 @@
export default class extends HTMLElement {
connectedCallback () {
const input = document.createElement('input')
this.showDialog()
}
showDialog () {
const dialogId = `prompt-password-dialog-${Math.random().toString(36).slice(2)}`
const inputId = `prompt-password-input-${Math.random().toString(36).slice(2)}`
const dialog = document.createElement('div')
dialog.setAttribute('role', 'dialog')
dialog.setAttribute('aria-modal', 'true')
dialog.setAttribute('aria-labelledby', `${dialogId}-title`)
dialog.id = dialogId
dialog.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black/50'
dialog.innerHTML = `
<div class="bg-base-100 rounded-xl shadow-xl p-6 w-full max-w-sm mx-4">
<h2 id="${dialogId}-title" class="text-lg font-semibold mb-4">PDF Password Required</h2>
<div class="form-control mb-4">
<label for="${inputId}" class="label label-text mb-1">Enter PDF password</label>
<input id="${inputId}" type="password" class="base-input w-full" autocomplete="current-password" />
</div>
<div class="flex justify-end gap-2">
<button type="button" class="btn btn-ghost" data-action="cancel">Cancel</button>
<button type="button" class="btn btn-neutral" data-action="confirm">Open</button>
</div>
</div>
`
document.body.append(dialog)
const input = dialog.querySelector(`#${inputId}`)
const cancelBtn = dialog.querySelector('[data-action="cancel"]')
const confirmBtn = dialog.querySelector('[data-action="confirm"]')
requestAnimationFrame(() => input.focus())
const confirm = () => {
const passwordInput = document.createElement('input')
passwordInput.type = 'hidden'
passwordInput.name = 'password'
passwordInput.value = input.value
this.form.append(passwordInput)
dialog.remove()
this.form.requestSubmit()
this.remove()
}
input.type = 'hidden'
input.name = 'password'
input.value = prompt('Enter PDF password')
const cancel = () => {
dialog.remove()
this.remove()
}
this.form.append(input)
confirmBtn.addEventListener('click', confirm)
cancelBtn.addEventListener('click', cancel)
this.form.requestSubmit()
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') confirm()
if (e.key === 'Escape') cancel()
})
this.remove()
dialog.addEventListener('keydown', (e) => {
if (e.key === 'Escape') cancel()
})
}
get form () {

@ -1,24 +1,39 @@
import { actionable } from '@github/catalyst/lib/actionable'
const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
export default actionable(class extends HTMLElement {
connectedCallback () {
document.body.classList.add('overflow-hidden')
document.addEventListener('keyup', this.onEscKey)
this.setAttribute('role', 'dialog')
this.setAttribute('aria-modal', 'true')
this._previousFocus = document.activeElement
document.addEventListener('keyup', this.onEscKey)
document.addEventListener('keydown', this.onTabKey)
document.addEventListener('turbo:before-cache', this.close)
if (this.dataset.closeAfterSubmit !== 'false') {
document.addEventListener('turbo:submit-end', this.onSubmit)
}
requestAnimationFrame(() => {
const first = this.querySelector(FOCUSABLE)
first?.focus()
})
}
disconnectedCallback () {
document.body.classList.remove('overflow-hidden')
document.removeEventListener('keyup', this.onEscKey)
document.removeEventListener('keydown', this.onTabKey)
document.removeEventListener('turbo:submit-end', this.onSubmit)
document.removeEventListener('turbo:before-cache', this.close)
this._previousFocus?.focus()
}
onSubmit = (e) => {
@ -33,6 +48,28 @@ export default actionable(class extends HTMLElement {
}
}
onTabKey = (e) => {
if (e.key !== 'Tab') return
const focusable = Array.from(this.querySelectorAll(FOCUSABLE))
if (focusable.length === 0) return
const first = focusable[0]
const last = focusable[focusable.length - 1]
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault()
last.focus()
}
} else {
if (document.activeElement === last) {
e.preventDefault()
first.focus()
}
}
}
close = (e) => {
e?.preventDefault()

@ -134,6 +134,7 @@
v-if="submittable"
type="checkbox"
:value="false"
:aria-label="field.title || field.name || fieldNames[field.type]"
class="aspect-square base-checkbox"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue"
@ -154,6 +155,7 @@
v-if="submittable"
type="radio"
:value="false"
:aria-label="(field.title || field.name || fieldNames[field.type]) + (option?.value ? ': ' + option.value : '')"
class="aspect-square checked:checkbox checked:checkbox-xs"
:class="{ 'base-radio': !modelValue || modelValue !== optionValue(option), '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue && modelValue === optionValue(option)"
@ -208,7 +210,7 @@
>
<span
v-if="field && field.name && withFieldPlaceholder && !modelValue && modelValue !== 0"
class="whitespace-pre-wrap text-gray-400"
class="whitespace-pre-wrap text-gray-600"
:class="{ 'w-full': field.preferences?.align }"
>{{ field.name }}</span>
<span

@ -204,7 +204,7 @@
dir="auto"
:required="currentField.required"
class="select base-input !text-2xl w-full text-center font-normal"
:class="{ 'text-gray-300': !values[currentField.uuid] }"
:class="{ 'text-gray-600': !values[currentField.uuid] }"
:name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value"
@focus="scrollIntoField(currentField)"
@ -212,7 +212,7 @@
<option
value=""
:selected="!values[currentField.uuid]"
class="text-gray-300"
class="text-gray-600"
>
{{ t('select_your_option') }}
</option>

@ -102,7 +102,7 @@
ref="phone"
:value="defaultValue && detectedPhoneValueDialCode ? phoneValue.split('+' + detectedPhoneValueDialCode).slice(-1).join('') : phoneValue"
:readonly="!!defaultValue"
class="base-input !text-2xl !rounded-l-none !border-l-0 !outline-none w-full"
class="base-input !text-2xl !rounded-l-none !border-l-0 w-full"
autocomplete="tel"
type="tel"
inputmode="tel"

@ -205,6 +205,7 @@
</div>
</div>
</div>
<label for="signature_text_input" class="sr-only">{{ t('signature') }}</label>
<input
v-if="isTextSignature && !modelValue && !computedPreviousValue"
id="signature_text_input"
@ -217,8 +218,9 @@
>
<select
v-if="withSigningReason && !isOtherReason"
:aria-label="t('require_signing_reason')"
class="select base-input !text-2xl w-full mt-6 text-center"
:class="{ 'text-gray-300': !reason }"
:class="{ 'text-gray-600': !reason }"
required
:name="`values[${field.preferences.reason_field_uuid}]`"
@change="$event.target.value === 'other' ? [reason = '', isOtherReason = true] : $emit('update:reason', $event.target.value)"
@ -227,7 +229,7 @@
value=""
disabled
:selected="!reason"
class="text-gray-300"
class="text-gray-600"
>
{{ t('select_a_reason') }}
</option>
@ -304,6 +306,14 @@
v-else
class="mt-5 md:mt-7"
/>
<div
v-if="signatureError"
role="alert"
aria-live="assertive"
class="text-error text-sm mt-2 px-1"
>
{{ signatureError }}
</div>
</div>
</template>
@ -424,7 +434,8 @@ export default {
isUsePreviousValue: true,
isTouchAttachment: false,
isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload',
uploadImageInputKey: Math.random().toString()
uploadImageInputKey: Math.random().toString(),
signatureError: null
}
},
computed: {
@ -764,7 +775,7 @@ export default {
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
if (this.field.required === true || this.pad.toData().length > 0) {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
this.signatureError = this.t('signature_is_too_small_or_simple_please_redraw')
return Promise.reject(new Error('Image too small or simple'))
} else {
@ -820,7 +831,7 @@ export default {
}
}).catch((error) => {
if (this.field.required === true) {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
this.signatureError = this.t('signature_is_too_small_or_simple_please_redraw')
return reject(error)
} else {

@ -64,7 +64,7 @@
ref="name"
:contenteditable="editable && !defaultField && field.type !== 'heading'"
dir="auto"
class="pr-1 cursor-text outline-none block"
class="pr-1 cursor-text block focus:ring-1 focus:ring-base-content/40 focus:rounded-sm"
style="min-width: 2px"
@paste.prevent="onPaste"
@keydown.enter.prevent="onNameEnter"
@ -275,7 +275,7 @@
<select
v-else-if="isSelectInput"
ref="defaultValueSelect"
class="bg-transparent outline-none focus:outline-none w-full"
class="bg-transparent w-full focus:ring-1 focus:ring-base-content/40 focus:rounded-sm"
@change="[field.default_value = $event.target.value, field.readonly = !!field.default_value?.length, save()]"
@focus="selectedAreasRef.value = [area]"
@keydown.enter="onDefaultValueEnter"
@ -300,7 +300,7 @@
v-else
ref="defaultValue"
:contenteditable="isValueInput"
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-base-content/30"
class="whitespace-pre-wrap empty:before:content-[attr(placeholder)] before:text-base-content/30 focus:ring-1 focus:ring-base-content/40 focus:rounded-sm"
:class="{ 'cursor-text': isValueInput }"
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))"
@blur="onDefaultValueBlur"

@ -61,7 +61,7 @@
</div>
<select
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
:class="{ 'text-gray-300': !condition.field_uuid }"
:class="{ 'text-gray-600': !condition.field_uuid }"
required
@change="[
condition.field_uuid = $event.target.value,
@ -103,7 +103,7 @@
<select
v-if="['radio', 'select', 'multiple'].includes(conditionField(condition)?.type) && conditionField(condition)?.options"
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
:class="{ 'text-gray-300': !condition.value }"
:class="{ 'text-gray-600': !condition.value }"
required
@change="condition.value = $event.target.value"
>
@ -130,7 +130,7 @@
type="number"
step="any"
class="input input-bordered input-sm w-full bg-white h-11 pl-4 text-base font-normal"
:class="{ 'text-gray-300': !condition.value }"
:class="{ 'text-gray-600': !condition.value }"
:placeholder="t('type_value')"
required
>

@ -11,7 +11,7 @@
type="button"
:aria-selected="!textViewActive ? 'true' : 'false'"
:tabindex="!textViewActive ? 0 : -1"
:class="['px-4 py-2 text-sm border-b-2 -mb-px focus:outline-none', !textViewActive ? 'border-neutral text-base-content font-semibold' : 'border-transparent text-base-content font-medium']"
:class="['px-4 py-2 text-sm border-b-2 -mb-px focus:ring-2 focus:ring-inset focus:ring-base-content/50', !textViewActive ? 'border-neutral text-base-content font-semibold' : 'border-transparent text-base-content font-medium']"
@click="textViewActive = false"
@keydown="onTabKeydown($event, false)"
>
@ -22,7 +22,7 @@
type="button"
:aria-selected="textViewActive ? 'true' : 'false'"
:tabindex="textViewActive ? 0 : -1"
:class="['px-4 py-2 text-sm border-b-2 -mb-px focus:outline-none', textViewActive ? 'border-neutral text-base-content font-semibold' : 'border-transparent text-base-content font-medium']"
:class="['px-4 py-2 text-sm border-b-2 -mb-px focus:ring-2 focus:ring-inset focus:ring-base-content/50', textViewActive ? 'border-neutral text-base-content font-semibold' : 'border-transparent text-base-content font-medium']"
@click="textViewActive = true"
@keydown="onTabKeydown($event, true)"
>

@ -169,7 +169,7 @@
>
<span
contenteditable="true"
class="outline-none whitespace-nowrap truncate"
class="whitespace-nowrap truncate focus:ring-2 focus:ring-base-content focus:ring-inset focus:rounded-sm"
>
{{ field.default_value || field.name || buildDefaultName(field) }}
</span>

@ -50,7 +50,7 @@
<div class="flex">
<select
class="base-select !select-sm !h-10"
:class="{ '!text-gray-300': !mapping.field_name }"
:class="{ '!text-gray-600': !mapping.field_name }"
required
@change="mapping.field_name = $event.target.value"
>
@ -77,7 +77,7 @@
<div class="w-full relative">
<select
class="base-select !select-sm !h-10"
:class="{ '!text-gray-300': !mapping.column_index && mapping.column_index != 0 }"
:class="{ '!text-gray-600': !mapping.column_index && mapping.column_index != 0 }"
required
@change="mapping.column_index = parseInt($event.target.value)"
>

@ -5,7 +5,7 @@
<% if Docuseal.enable_pwa? %>
<link rel="manifest" href="/manifest.json">
<% end %>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<%= csrf_meta_tags %>
<% if ENV['ROLLBAR_CLIENT_TOKEN'] %>
<meta name="rollbar-token" content="<%= ENV.fetch('ROLLBAR_CLIENT_TOKEN', nil) %>">

@ -13,6 +13,9 @@
<%= stylesheet_pack_tag 'form', media: 'all' %>
</head>
<body>
<a href="#scrollbox" class="absolute left-0 top-0 -translate-y-full focus:translate-y-0 z-50 p-4 bg-base-100 text-base-content border-2 border-neutral" tabindex="0">
Skip to main content
</a>
<%= yield %>
</body>
</html>

@ -7,7 +7,7 @@
<span>
<%= local_assigns[:title] %>
</span>
<label for="<%= uuid %>" class="text-xl" aria-label="<%= t('close') %>">&times;</label>
<label for="<%= uuid %>" class="text-xl" role="button" tabindex="0" aria-label="<%= t('close') %>">&times;</label>
</div>
<% end %>
<div>

@ -1,13 +1,14 @@
<turbo-frame id="modal">
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<% modal_title_id = "modal_title_#{SecureRandom.hex(4)}" %>
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>" <%= "aria-labelledby=\"#{modal_title_id}\"" if local_assigns[:title] %>>
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
<% if local_assigns[:title] %>
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span>
<span id="<%= modal_title_id %>">
<%= local_assigns[:title] %>
</span>
<a href="#" class="text-xl" data-action="click:turbo-modal#close" aria-label="<%= t('close') %>">&times;</a>
<button type="button" class="text-xl" data-action="click:turbo-modal#close" aria-label="<%= t('close') %>">&times;</button>
</div>
<% end %>
<div>

@ -1,13 +1,14 @@
<turbo-frame id="modal">
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto !justify-normal md:!justify-center" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<% modal_title_id = "modal_title_#{SecureRandom.hex(4)}" %>
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto !justify-normal md:!justify-center" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>" <%= "aria-labelledby=\"#{modal_title_id}\"" if local_assigns[:title] %>>
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
<div class="bg-base-100 min-h-screen max-h-screen md:min-h-fit md:mt-3 md:rounded-2xl relative w-full relative overflow-y-auto">
<% if local_assigns[:title] %>
<div class="flex justify-between bg-base-100 py-2 px-5 items-center border-b pb-2 font-medium mt-0.5">
<span>
<span id="<%= modal_title_id %>">
<%= local_assigns[:title] %>
</span>
<a href="#" class="text-xl" data-action="click:turbo-modal#close" aria-label="<%= t('close') %>">&times;</a>
<button type="button" class="text-xl" data-action="click:turbo-modal#close" aria-label="<%= t('close') %>">&times;</button>
</div>
<% end %>
<div class="w-full md:w-[590px] overflow-y-auto" style="max-height: calc(100vh - 14px - 45px)">

@ -1,5 +1,5 @@
<div class="mt-2 mb-1">
<div class="tooltip w-full" data-tip="<%= t('unlock_with_docuseal_pro') %>">
<%= link_to submitter.sent_at? ? t('re_send_sms') : t('send_sms'), Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'btn btn-sm btn-primary text-gray-400 w-full' %>
<%= link_to submitter.sent_at? ? t('re_send_sms') : t('send_sms'), Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'btn btn-sm btn-primary text-gray-600 w-full' %>
</div>
</div>

@ -16,12 +16,12 @@
<%= local_assigns[:banner_html] || capture do %>
<%= render('submit_form/banner') %>
<div id="signing_form_header" class="sticky min-[1230px]:static top-0 z-50 bg-base-100 py-2 px-2 flex items-center md:-mx-[8px]" style="margin-bottom: -16px">
<div class="text-xl md:text-2xl font-medium focus:text-clip" style="width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<h1 class="text-xl md:text-2xl font-medium focus:text-clip" style="width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<%= @submitter.submission.name || @submitter.submission.template.name %>
</div>
</h1>
<div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0">
<% if @form_configs[:with_decline] %>
<label id="decline_button" for="<%= decline_modal_checkbox_uuid = SecureRandom.uuid %>" class="btn btn-sm !px-5"><%= t(:decline) %></label>
<label id="decline_button_header" for="<%= decline_modal_checkbox_uuid = SecureRandom.uuid %>" class="btn btn-sm !px-5"><%= t(:decline) %></label>
<% end %>
<% if @form_configs[:with_partial_download] %>
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4">
@ -39,7 +39,7 @@
</div>
<scroll-buttons class="fixed right-5 top-2 hidden md:flex gap-1 z-50 ease-in-out opacity-0 -translate-y-10">
<% if @form_configs[:with_decline] %>
<label id="decline_button" for="<%= decline_modal_checkbox_uuid %>" class="btn btn-sm px-0">
<label id="decline_button_scroll" for="<%= decline_modal_checkbox_uuid %>" class="btn btn-sm px-0">
<span class="min-[1366px]:inline hidden px-3">
<%= t(:decline) %>
</span>

@ -55,7 +55,7 @@
<div id="js_1" class="block my-4">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to t('learn_more'), console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<%= link_to t('learn_more'), console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-white flex', rel: 'noopener' %>
<clipboard-copy data-text="<script src=&quot;<%= Docuseal::CDN_URL %>/js/form.js&quot;></script>
<docuseal-form data-src=&quot;<%= start_form_url(slug: template.slug) %>&quot;></docuseal-form>
@ -88,7 +88,7 @@
<div id="react_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to t('learn_more'), console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<%= link_to t('learn_more'), console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-white flex', rel: 'noopener' %>
<clipboard-copy data-text="import React from &quot;react&quot;
import { DocusealForm } from '@docuseal/react'
@ -135,7 +135,7 @@ export function App() {
<div id="vue_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to t('learn_more'), console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<%= link_to t('learn_more'), console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-white flex', rel: 'noopener' %>
<clipboard-copy data-text="<template>
<DocusealForm
:src=&quot;'<%= start_form_url(slug: template.slug) %>'&quot;
@ -193,7 +193,7 @@ export default {
<div id="angular_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to t('learn_more'), console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<%= link_to t('learn_more'), console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-white flex', rel: 'noopener' %>
<clipboard-copy data-text="import { Component } from '@angular/core';
import { DocusealFormComponent } from '@docuseal/angular';

@ -8,7 +8,7 @@
</label>
<folder-autocomplete class="flex justify-between w-full">
<set-value data-on="blur" data-value="<%= TemplateFolder::DEFAULT_NAME %>" data-empty-only="true" class="peer w-full whitespace-nowrap">
<input id="folder_name" placeholder="<%= t('folder_name') %>" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1" name="folder_name" value="<%= params[:folder_name].presence || TemplateFolder::DEFAULT_NAME %>" autocomplete="off">
<input id="folder_name" placeholder="<%= t('folder_name') %>" type="text" class="w-full outline-none border-transparent focus:border-base-content/50 focus:ring-1 focus:ring-base-content/20 bg-base-100 px-1 focus:rounded-sm" name="folder_name" value="<%= params[:folder_name].presence || TemplateFolder::DEFAULT_NAME %>" autocomplete="off">
</set-value>
<set-value data-on="click" data-value="" data-input-id="folder_name" class="peer-focus-within:hidden whitespace-nowrap">
<label for="folder_name" data-clear-on-focus="true" class="shrink-0 link mr-1.5 cursor-pointer">

@ -14,7 +14,7 @@
</label>
<folder-autocomplete class="flex justify-between w-full">
<set-value data-on="blur" data-value="<%= TemplateFolder::DEFAULT_NAME %>" data-empty-only="true" class="peer w-full whitespace-nowrap">
<input id="folder_name" placeholder="<%= t('folder_name') %>" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.full_name || TemplateFolder::DEFAULT_NAME %>" autocomplete="off">
<input id="folder_name" placeholder="<%= t('folder_name') %>" type="text" class="w-full outline-none border-transparent focus:border-base-content/50 focus:ring-1 focus:ring-base-content/20 bg-base-100 px-1 focus:rounded-sm" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.full_name || TemplateFolder::DEFAULT_NAME %>" autocomplete="off">
</set-value>
<set-value data-on="click" data-value="" data-input-id="folder_name" class="peer-focus-within:hidden whitespace-nowrap">
<label for="folder_name" data-clear-on-focus="true" class="shrink-0 link mr-1.5 cursor-pointer">

@ -12,7 +12,7 @@
<%= ff.hidden_field :uuid %>
<div class="form-control">
<div class="flex justify-between">
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-base-content/50 focus:ring-1 focus:ring-base-content/20 bg-base-100 px-1 peer mb-2 focus:rounded-sm', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
<% if template.submitters.size > 2 %>
<div id="order_<%= submitter['uuid'] %>" class="mr-0.5">
<% if is_order_set %>

Loading…
Cancel
Save