- = $label ?>
+ = $this->Form->label($formArguments['fieldName'], $label) ?>
= $this->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: '= $label ?>',
- fieldName: '= $fieldName ?>', // This property hold the form input id
+ fieldName: '= $fieldName ?>', // This property holds the form input name
+ targetId: '= $targetId ?>', // This property holds the hidden input id
type: '= $type ?>',
personType: '= $personType ?>',
// XXX probably should be set by config, note the backend (ApiV2Controller::pick)
@@ -143,7 +155,13 @@
//name: '//',
dataName: '= $htmlId ?>',
// This is not translated to data-personid but to datapersonid.
- dataPersonid: '= $inputValue ?>'
+ dataPersonid: '= $inputValue ?>',
+
+ required: true,
+
+
+ 'aria-describedby': '= $htmlId ?>-desc',
+
},
personRecord: = json_encode($personRecord ?? [])?>,
canvasUrl: '= $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'] }}
+ info
+ {{ this.txt['operation.autocomplete.people.field.desc'] }}