diff --git a/app/plugins/CoreEnroller/templates/element/spa-field.php b/app/plugins/CoreEnroller/templates/element/spa-field.php index d70069f92..047c480ab 100644 --- a/app/plugins/CoreEnroller/templates/element/spa-field.php +++ b/app/plugins/CoreEnroller/templates/element/spa-field.php @@ -39,7 +39,7 @@
- + Form->label($formArguments['fieldName'], $label) ?> element('form/requiredSpan') ?> diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php index e3979ab22..89beadfc8 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -264,7 +264,9 @@ public function constructSPAField(string $element, string $vueElementName): stri if(!empty($matchesId[0][1]) && !empty($matchesName[0][1])) { $vueElementProperties = [ - 'htmlId' => $matchesId[0][1] . '-picker', + // htmlId matches CakePHP's label 'for' attribute (e.g. 'field-3') so clicking + // the visible label natively focuses the interactive AutoComplete search input. + 'htmlId' => $matchesId[0][1], 'fieldName' => $matchesName[0][1], 'containerClasses' => $matchesClass[0][1], 'type' => 'field', @@ -272,6 +274,12 @@ public function constructSPAField(string $element, string $vueElementName): stri 'label' => '' ]; + // Parse the Required attribute + $regexRequired = '/\brequired(=["\']?required["\']?)?/i'; + if(preg_match($regexRequired, $element)) { + $vueElementProperties['isRequired'] = true; + } + if (isset($matchesValue[0][1])) { $vueElementProperties['inputValue'] = $matchesValue[0][1]; } diff --git a/app/templates/element/peopleAutocomplete.php b/app/templates/element/peopleAutocomplete.php index b93b53464..8977b5719 100644 --- a/app/templates/element/peopleAutocomplete.php +++ b/app/templates/element/peopleAutocomplete.php @@ -53,6 +53,15 @@ // Used by the SearchFilter Configuration $personType = $personType ?? 'person'; $htmlId = $htmlId ?? 'person-id-picker'; + + // DOM ID and Form Name separation: + // - Display Label: Assigned for="field-3" (matching $htmlId). + // - Interactive AutoComplete Input: Assigned id="field-3" (via $htmlId). Clicking the visible label natively + // focuses the interactive AutoComplete input, satisfying browser accessibility standards with zero ambiguity. + // - Hidden Submission Input: Assigned id="field-3-target" (via $targetId). + // Result: Every ID in the DOM is globally unique. + $targetId = $targetId ?? ($htmlId === $fieldName ? $fieldName . '-target' : $fieldName); + $isRequired = $isRequired ?? $vv_field_arguments["fieldOptions"]["required"] ?? $vv_field_arguments["required"] ?? false; // Does it have a value already. Default or stored // CAKEPHP automatically generates a select element if the value is an integer. This is not helpful here. $inputValue = $inputValue ?? $vv_field_arguments["fieldOptions"]["default"] ?? $vv_field_arguments["fieldOptions"]["value"] ?? ''; @@ -87,12 +96,14 @@ $searchPeople = $this->request->getAttribute('webroot') . 'api/ajax/v2/people/pick?' . $targetKey . '=' . $targetValue; - // This is the actual field that will be submitted. - print $this->Form->control($fieldName, [ - 'id' => $fieldName, + // Form Submission name Attributes: + // - Hidden Submission Input: Rendered as + // This is the actual field that will be submitted on form POST. + // - Interactive AutoComplete Input: Rendered without a native form name="field-3" attribute (it uses internal + // Vue props and dataName), preventing duplicate/conflicting payload submissions. + print $this->Form->hidden($fieldName, [ + 'id' => $targetId, 'value' => '', - 'type' => 'text', - 'class' => 'visually-hidden' ] ); } @@ -129,7 +140,8 @@ return { autocompleteOptions: { label: '', - fieldName: '', // This property hold the form input id + fieldName: '', // This property holds the form input name + targetId: '', // This property holds the hidden input id type: '', personType: '', // XXX probably should be set by config, note the backend (ApiV2Controller::pick) @@ -143,7 +155,13 @@ //name: '//', dataName: '', // This is not translated to data-personid but to datapersonid. - dataPersonid: '' + dataPersonid: '', + + required: true, + + + 'aria-describedby': '-desc', + }, personRecord: , canvasUrl: '' diff --git a/app/webroot/js/comanage/components/autocomplete/cm-autocomplete-people.js b/app/webroot/js/comanage/components/autocomplete/cm-autocomplete-people.js index 08ad3b20b..6aa4fe2c3 100644 --- a/app/webroot/js/comanage/components/autocomplete/cm-autocomplete-people.js +++ b/app/webroot/js/comanage/components/autocomplete/cm-autocomplete-people.js @@ -190,8 +190,11 @@ export default { if(['search', 'field'].includes(this.options.type)) { this.options.inputProps.dataPersonid = this.person.value; // Update the field that will be submitted - let inputElement = document.getElementById(this.options.fieldName) - inputElement.value = this.person.value; + const targetElementId = this.options.targetId || this.options.fieldName; + let inputElement = document.getElementById(targetElementId); + if(inputElement) { + inputElement.value = this.person.value; + } } else { // The picker is stand-alone, and should render the configured page in a modal on @item-select @@ -211,8 +214,11 @@ export default { }, onClear(ev) { // The user cleared the input field - so clear the real field (and the model). - let inputElement = document.getElementById(this.options.fieldName) - inputElement.value = null; + const targetElementId = this.options.targetId || this.options.fieldName; + let inputElement = document.getElementById(targetElementId); + if(inputElement) { + inputElement.value = null; + } this.person = null; }, onListNavigate(ev) { @@ -262,11 +268,14 @@ export default { mounted() { if(this.options.inputValue != undefined && this.options.inputValue != '' - && this.options.inputProps.dataName.endsWith('person-id-picker')) { + && (this.options.inputProps.dataName.endsWith('person-id') || this.options.inputProps.dataName.endsWith('person-id-picker'))) { this.person = `${this.options.personRecord.primary_name.given} ${this.options.personRecord.primary_name.family} (ID: ${this.options.personRecord.id})`; this.personUrl = `${this.options.canvasUrl}`; - let inputElement = document.getElementById(this.options.fieldName) - inputElement.value = this.options.personRecord.id; + const targetElementId = this.options.targetId || this.options.fieldName; + let inputElement = document.getElementById(targetElementId); + if(inputElement) { + inputElement.value = this.options.personRecord.id; + } } }, computed: { @@ -384,8 +393,8 @@ export default {
- info - {{ this.txt['operation.autocomplete.people.field.desc'] }} + + {{ this.txt['operation.autocomplete.people.field.desc'] }}
`