From 96ce024b7eb8d60c564ec700ccd5712178d2e97c Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 13 Oct 2026 15:17:09 +0000 Subject: [PATCH 1/5] Connect display label to Person Picker input and decouple submission field (CO-3018) --- .../templates/element/spa-field.php | 2 +- app/src/View/Helper/FieldHelper.php | 2 +- app/templates/element/peopleAutocomplete.php | 10 ++++---- .../autocomplete/cm-autocomplete-people.js | 23 +++++++++++++------ 4 files changed, 23 insertions(+), 14 deletions(-) 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..6c16a19d6 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -264,7 +264,7 @@ public function constructSPAField(string $element, string $vueElementName): stri if(!empty($matchesId[0][1]) && !empty($matchesName[0][1])) { $vueElementProperties = [ - 'htmlId' => $matchesId[0][1] . '-picker', + 'htmlId' => $matchesId[0][1], 'fieldName' => $matchesName[0][1], 'containerClasses' => $matchesClass[0][1], 'type' => 'field', diff --git a/app/templates/element/peopleAutocomplete.php b/app/templates/element/peopleAutocomplete.php index b93b53464..20d0c15f0 100644 --- a/app/templates/element/peopleAutocomplete.php +++ b/app/templates/element/peopleAutocomplete.php @@ -53,6 +53,7 @@ // Used by the SearchFilter Configuration $personType = $personType ?? 'person'; $htmlId = $htmlId ?? 'person-id-picker'; + $targetId = $targetId ?? ($htmlId === $fieldName ? $fieldName . '-target' : $fieldName); // 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"] ?? ''; @@ -88,11 +89,9 @@ $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, + print $this->Form->hidden($fieldName, [ + 'id' => $targetId, 'value' => '', - 'type' => 'text', - 'class' => 'visually-hidden' ] ); } @@ -129,7 +128,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) 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..e33fe6860 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: { From 3bedb56a57be8e78d9f48e8968bd69de798588c1 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 13 Oct 2026 15:26:55 +0000 Subject: [PATCH 2/5] Add comments --- app/src/View/Helper/FieldHelper.php | 2 ++ app/templates/element/peopleAutocomplete.php | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php index 6c16a19d6..fb17b94c4 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -264,6 +264,8 @@ public function constructSPAField(string $element, string $vueElementName): stri if(!empty($matchesId[0][1]) && !empty($matchesName[0][1])) { $vueElementProperties = [ + // 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], diff --git a/app/templates/element/peopleAutocomplete.php b/app/templates/element/peopleAutocomplete.php index 20d0c15f0..d99d442ee 100644 --- a/app/templates/element/peopleAutocomplete.php +++ b/app/templates/element/peopleAutocomplete.php @@ -53,6 +53,13 @@ // 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); // 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. @@ -88,7 +95,11 @@ $searchPeople = $this->request->getAttribute('webroot') . 'api/ajax/v2/people/pick?' . $targetKey . '=' . $targetValue; - // This is the actual field that will be submitted. + // 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' => '', From 08842a193372a799ffe9769a3b8c212cd311ed38 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 13 Oct 2026 15:48:07 +0000 Subject: [PATCH 3/5] Hide decorative info icon in Person Picker from screen readers via aria-hidden --- .../comanage/components/autocomplete/cm-autocomplete-people.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e33fe6860..b2c90c3fd 100644 --- a/app/webroot/js/comanage/components/autocomplete/cm-autocomplete-people.js +++ b/app/webroot/js/comanage/components/autocomplete/cm-autocomplete-people.js @@ -393,7 +393,7 @@ export default {
- info + {{ this.txt['operation.autocomplete.people.field.desc'] }}
From 5359ce53c3d80ac2a6ed5ff79aba934d5481ceee Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 13 Oct 2026 15:55:18 +0000 Subject: [PATCH 4/5] Set required attribute on Person Picker input for mandatory fields --- app/src/View/Helper/FieldHelper.php | 6 ++++++ app/templates/element/peopleAutocomplete.php | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php index fb17b94c4..89beadfc8 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -274,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 d99d442ee..833ecf964 100644 --- a/app/templates/element/peopleAutocomplete.php +++ b/app/templates/element/peopleAutocomplete.php @@ -61,6 +61,7 @@ // - 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"] ?? ''; @@ -154,7 +155,10 @@ //name: '//', dataName: '', // This is not translated to data-personid but to datapersonid. - dataPersonid: '' + dataPersonid: '', + + required: true, + }, personRecord: , canvasUrl: '' From d796dc6290ecfe99b8aa52721ebdeca04bcbf1eb Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 13 Oct 2026 16:00:04 +0000 Subject: [PATCH 5/5] Link Person Picker description to input via aria-describedby --- app/templates/element/peopleAutocomplete.php | 3 +++ .../comanage/components/autocomplete/cm-autocomplete-people.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/templates/element/peopleAutocomplete.php b/app/templates/element/peopleAutocomplete.php index 833ecf964..8977b5719 100644 --- a/app/templates/element/peopleAutocomplete.php +++ b/app/templates/element/peopleAutocomplete.php @@ -159,6 +159,9 @@ 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 b2c90c3fd..6aa4fe2c3 100644 --- a/app/webroot/js/comanage/components/autocomplete/cm-autocomplete-people.js +++ b/app/webroot/js/comanage/components/autocomplete/cm-autocomplete-people.js @@ -394,7 +394,7 @@ export default {
- {{ this.txt['operation.autocomplete.people.field.desc'] }} + {{ this.txt['operation.autocomplete.people.field.desc'] }}
`