Add visual indicators for required fields and ARIA attributes in shift forms

This commit is contained in:
Ross
2025-12-17 15:25:39 +00:00
parent ea38a25fa1
commit 32886da33d
2 changed files with 74 additions and 6 deletions
@@ -8,3 +8,34 @@
</div>
</div>
</form>
<script>
// Mark required fields with a visual asterisk and set ARIA attributes
(function(){
try{
const form = document.currentScript.previousElementSibling;
if(!form) return;
const requiredEls = form.querySelectorAll('input[required], select[required], textarea[required]');
requiredEls.forEach(el=>{
el.setAttribute('aria-required','true');
// try to find a label: either <label for="id_x"> or a sibling .label
const id = el.id;
let lbl = id ? form.querySelector('label[for="'+id+'"]') : null;
if(!lbl){
// look for nearest label element in the same .field
const field = el.closest('.field');
if(field) lbl = field.querySelector('label');
}
if(lbl){
const already = lbl.querySelector('.required-indicator') || lbl.querySelector('.has-text-danger') || /\*\s*$/.test(lbl.textContent);
if(!already){
const span = document.createElement('span');
span.className = 'required-indicator has-text-danger';
span.textContent = ' *';
lbl.appendChild(span);
}
}
});
}catch(e){ /* ignore */ }
})();
</script>
@@ -60,12 +60,12 @@ function _close_shift_modal_targets(){
{name: "exclude_days", label: "Exclude days (comma-separated)", type: "list"},
{name: "allow_self", label: "Allow self", type: "bool"}
]},
"min_summed_grade_by_shifts_per_day": {label: "Min summed grade by shifts per day", fields: [ {name: "shifts", label: "Shifts (comma-separated)", type: "list"}, {name: "min_grade_sum", label: "Minimum grade sum", type: "int"}, {name: "days", label: "Days (comma-separated)", type: "list"} ]},
"limit_grade_number": {label: "Limit grade number", fields: [ {name: "grade", label: "Grade", type: "int"}, {name: "max_number", label: "Max number", type: "int"} ]},
"minimum_grade_number": {label: "Minimum grade number", fields: [ {name: "grade", label: "Grade", type: "int"}, {name: "min_number", label: "Min number", type: "int"} ]},
"require_remote_site_presence": {label: "Require remote site presence", fields: [ {name: "site", label: "Site", type: "text"}, {name: "required_number", label: "Required number", type: "int"} ]},
"max_shifts_per_week": {label: "Max shifts per week", fields: [ {name: "max_shifts", label: "Max shifts", type: "int"} ]},
"max_shifts_per_week_block": {label: "Max shifts per week block", fields: [ {name: "week_block", label: "Week block", type: "int"}, {name: "max_shifts", label: "Max shifts", type: "int"} ]}
"min_summed_grade_by_shifts_per_day": {label: "Min summed grade by shifts per day", fields: [ {name: "shifts", label: "Shifts (comma-separated)", type: "list", required: true}, {name: "min_grade_sum", label: "Minimum grade sum", type: "int", required: true}, {name: "days", label: "Days (comma-separated)", type: "list", required: true} ]},
"limit_grade_number": {label: "Limit grade number", fields: [ {name: "grade", label: "Grade", type: "int", required: true}, {name: "max_number", label: "Max number", type: "int", required: true} ]},
"minimum_grade_number": {label: "Minimum grade number", fields: [ {name: "grade", label: "Grade", type: "int", required: true}, {name: "min_number", label: "Min number", type: "int", required: true} ]},
"require_remote_site_presence": {label: "Require remote site presence", fields: [ {name: "site", label: "Site", type: "text", required: true}, {name: "required_number", label: "Required number", type: "int", required: true} ]},
"max_shifts_per_week": {label: "Max shifts per week", fields: [ {name: "max_shifts", label: "Max shifts", type: "int", required: true} ]},
"max_shifts_per_week_block": {label: "Max shifts per week block", fields: [ {name: "week_block", label: "Week block", type: "int", required: true}, {name: "max_shifts", label: "Max shifts", type: "int", required: true} ]}
};
const textarea = document.getElementById('id_constraints');
@@ -148,6 +148,13 @@ function _close_shift_modal_targets(){
SCHEMAS[t].fields.forEach(f=>{
const fieldWrap = document.createElement('div'); fieldWrap.className='field';
const label = document.createElement('label'); label.className='label'; label.textContent = f.label;
// append required indicator if schema marks this field required
if(f.required){
if(!label.querySelector('.required-indicator')){
const span = document.createElement('span'); span.className='required-indicator has-text-danger'; span.textContent=' *';
label.appendChild(span);
}
}
fieldWrap.appendChild(label);
const control = document.createElement('div'); control.className='control';
let input;
@@ -168,6 +175,7 @@ function _close_shift_modal_targets(){
cb.checked = !!curb;
cb.setAttribute('data-field-name', f.name);
cb.onchange = syncToData;
if(f.required){ cb.required = true; cb.setAttribute('aria-required','true'); }
control.appendChild(cb);
fieldWrap.appendChild(control);
fieldsContainer.appendChild(fieldWrap);
@@ -181,6 +189,7 @@ function _close_shift_modal_targets(){
input = document.createElement('input'); input.className='input'; input.type='text'; input.oninput = syncToData;
}
input.setAttribute('data-field-name', f.name);
if(f.required){ input.required = true; input.setAttribute('aria-required','true'); }
control.appendChild(input);
fieldWrap.appendChild(control);
fieldsContainer.appendChild(fieldWrap);
@@ -248,6 +257,34 @@ function _close_shift_modal_targets(){
})();
</script>
<script>
// Mark required fields in the modal shift form with a visible asterisk and add ARIA attributes
(function(){
try{
const modalForm = document.querySelector('#shift-modal form');
if(!modalForm) return;
const requiredEls = modalForm.querySelectorAll('input[required], select[required], textarea[required]');
requiredEls.forEach(el=>{
el.setAttribute('aria-required','true');
const id = el.id;
let lbl = id ? modalForm.querySelector('label[for="'+id+'"]') : null;
if(!lbl){
const field = el.closest('.field');
if(field) lbl = field.querySelector('label');
}
if(lbl){
const already = lbl.querySelector('.required-indicator') || lbl.querySelector('.has-text-danger') || /\*\s*$/.test(lbl.textContent);
if(!already){
const span = document.createElement('span');
span.className = 'required-indicator has-text-danger';
span.textContent = ' *';
lbl.appendChild(span);
}
}
});
}catch(e){ /* ignore */ }
})();
</script>
<div class="field">
<div class="control">
<button class="button is-primary" type="submit">Save</button>