From a59cfa84ef18af33522b9dca1fe0a8751bbaae56 Mon Sep 17 00:00:00 2001 From: Lina Fowler Date: Wed, 16 Jul 2025 12:34:56 -0600 Subject: [PATCH 1/4] Added accessibility fix for form validation --- .../secondary_navigation_elements.scss | 6 +- .../new/components/group_project_fields.vue | 174 +++++++++++++++++- .../views/registrations/groups/new.html.haml | 52 +++--- 3 files changed, 205 insertions(+), 27 deletions(-) diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index 5660cfa2787f47..7843ebc608611b 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -23,7 +23,11 @@ } &:focus { - @include gl-focus($color: var(--gl-action-neutral-border-color-active)); + text-decoration: none; + color: var(--gl-text-default); + outline: 2px solid var(--gl-tab-selected-indicator-color-default); + outline-offset: 2px; + box-shadow: none; } } diff --git a/ee/app/assets/javascripts/registrations/groups/new/components/group_project_fields.vue b/ee/app/assets/javascripts/registrations/groups/new/components/group_project_fields.vue index 81fb8cb4060a7a..5ae4c8ee9086a9 100644 --- a/ee/app/assets/javascripts/registrations/groups/new/components/group_project_fields.vue +++ b/ee/app/assets/javascripts/registrations/groups/new/components/group_project_fields.vue @@ -25,6 +25,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + expose: ['validateForm'], props: { importGroup: { type: Boolean, @@ -69,6 +70,13 @@ export default { currentApiRequestController: null, groupPathWithoutSuggestion: null, selectedTemplateName: this.templateName, + // Track current values for validation + currentProjectName: this.projectName, + // Validation state for accessibility + validation: { + groupName: { isValid: true, message: '' }, + projectName: { isValid: true, message: '' }, + }, }; }, computed: { @@ -90,8 +98,34 @@ export default { } if (this.projectName) { + this.currentProjectName = this.projectName; this.onProjectUpdate(this.projectName); } + + // Add form submission event listeners + this.$nextTick(() => { + this.createForm = document.querySelector('.js-groups-projects-form'); + this.importForm = document.querySelector('.js-import-project-form'); + + if (this.createForm) { + this.createForm.addEventListener('submit', this.handleFormSubmit.bind(this)); + } + + if (this.importForm) { + this.importForm.addEventListener('submit', this.handleImportFormSubmit.bind(this)); + } + }); + }, + + beforeDestroy() { + // Clean up event listeners + if (this.createForm) { + this.createForm.removeEventListener('submit', this.handleFormSubmit); + } + + if (this.importForm) { + this.importForm.removeEventListener('submit', this.handleImportFormSubmit); + } }, methods: { ...mapActions(['setStoreGroupName', 'setStoreGroupPath']), @@ -135,7 +169,78 @@ export default { debouncedOnGroupUpdate: debounce(function debouncedUpdate(slug) { this.setSuggestedSlug(slug); }, DEBOUNCE_TIMEOUT_DURATION), + + // Validation methods for accessibility + validateGroupName(value) { + if (!value || !value.trim()) { + this.validation.groupName = { + isValid: false, + message: __('Group name field is required'), + }; + return false; + } + + this.validation.groupName = { isValid: true, message: '' }; + return true; + }, + + validateProjectName(value) { + if (!value || !value.trim()) { + this.validation.projectName = { + isValid: false, + message: __('Project name field is required'), + }; + return false; + } + + this.validation.projectName = { isValid: true, message: '' }; + return true; + }, + + validateForm() { + let isValid = true; + + // Validate group name if needed + if (!this.groupPersisted || this.importGroup) { + const groupNameValue = this.storeGroupName || ''; + if (!this.validateGroupName(groupNameValue)) { + isValid = false; + } + } + + // Validate project name if needed + if (!this.importGroup) { + const projectNameValue = this.currentProjectName || ''; + if (!this.validateProjectName(projectNameValue)) { + isValid = false; + } + } + + // Hide server-side errors when client-side validation is handling it + if (!isValid) { + const errorExplanation = document.getElementById('error_explanation'); + if (errorExplanation) { + errorExplanation.style.display = 'none'; + } + + // Focus first invalid field + this.$nextTick(() => { + const firstError = this.$el.querySelector('[aria-invalid="true"]'); + if (firstError) { + firstError.focus(); + } + }); + } + + return isValid; + }, + onGroupUpdate(value) { + // Clear validation error when user starts typing + if (value && value.trim()) { + this.validation.groupName = { isValid: true, message: '' }; + } + const slug = slugify(value); this.setStoreGroupName(value); @@ -144,12 +249,46 @@ export default { this.setStoreGroupPath(slug); return this.debouncedOnGroupUpdate(slug); }, + onProjectUpdate(value) { + // Store the current value for validation + this.currentProjectName = value; + + // Clear validation error when user starts typing + if (value && value.trim()) { + this.validation.projectName = { isValid: true, message: '' }; + } + this.projectPath = slugify(convertUnicodeToAscii(value)) || DEFAULT_PROJECT_PATH; }, + selectTemplate(value) { this.selectedTemplateName = value; }, + + handleFormSubmit(event) { + const isValid = this.validateForm(); + + if (!isValid) { + event.preventDefault(); + event.stopPropagation(); + return false; + } + + return true; + }, + + handleImportFormSubmit(event) { + const isValid = this.validateForm(); + + if (!isValid) { + event.preventDefault(); + event.stopPropagation(); + return false; + } + + return true; + }, }, i18n: { groupNameLabel: s__('ProjectsNew|Group name'), @@ -163,6 +302,7 @@ export default { }, }; +