Skip to content

Commit

Permalink
Focus on first visible field in form and in navigable layouts
Browse files Browse the repository at this point in the history
  • Loading branch information
Pavitra Khatri authored and ci-build committed Feb 4, 2025
1 parent 07135c6 commit b77c7d1
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
}
}


/**
* Returns all expanded items.
*
Expand Down Expand Up @@ -225,6 +225,21 @@
panel.classList.add(this.constructor.cssClasses.panel.expanded);
panel.classList.remove(this.constructor.cssClasses.panel.hidden);
panel.setAttribute("aria-hidden", false);

if (document.activeElement === button) {
setTimeout(() => {
let id = panel.id.replace(this.constructor.idSuffixes.panel, "");
const form = this.formContainer.getModel();
const tab = this.getModel()._jsonModel.items.find(item => item.id === id);

if (tab && Array.isArray(tab.items) && tab.items.length > 0) {
const field = form.getElement(tab.id);
if (field) {
form.setFocus(field);
}
}
}, 0)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,6 @@
return this.element.querySelector(Accordion.selectors.qm);
}

setFocus(id) {
super.setFocus(id);
this.setActive();
this.#collapseAllItems();
const item = this.getItemById(id + '-item');
this.expandItem(item)
}


#collapseAllItems() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,68 @@
let formContainer = e.detail;
let formEl = formContainer.getFormElement();
formContainer.initialiseHamburgerMenu();

function findFirstVisibleEnabledChild(children) {
for (const child of children) {
// Skip if not visible or not enabled
if (!child._jsonModel.visible || !child._jsonModel.enabled) {
continue;
}

// If it's not a panel, return the field directly
if (child._jsonModel.fieldType !== "panel") {
return child;
}

// Handle panel case
if (child._jsonModel.fieldType === "panel") {
// Check if panel has items
if (Array.isArray(child._jsonModel.items) && child._jsonModel.items.length > 0) {
// Find first non-panel field or panel with children
const firstField = child._jsonModel.items.find(item =>
item.fieldType !== "panel" ||
(item.fieldType === "panel" && Array.isArray(item.items) && item.items.length > 0)
);
if (firstField) {
return firstField;
}
}
// Skip empty panels
continue;
}
}
return null;
}

// function findFirstVisibleEnabledChild(children) {
// for (const child of children) {
// if (child._jsonModel.visible && child._jsonModel.enabled) {
// if (Array.isArray(child._jsonModel.items)) {
// const validField = child._jsonModel.items.find(
// item => item.fieldType !== "panel" ||
// (item.fieldType === "panel" && Array.isArray(item.items) && item.items.length > 0)
// );
// if (validField) return validField;
// } else {
// return child;
// }
// }
// }
// return null;
// }





const firstVisibleChild = findFirstVisibleEnabledChild(formContainer.getModel()._children)
const form = formContainer.getModel();
if (form && firstVisibleChild) {
const field = form?.getElement(firstVisibleChild.id);
if (field) {
form.setFocus(field);
}
}
setTimeout(() => {
let loaderToRemove = document.querySelector("[data-cmp-adaptiveform-container-loader='"+ formEl.id + "']");
if(loaderToRemove){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,12 @@
}


getFocusElementId() {
const activeIndex = this._active;
const activeTabElement = this.getCachedTabs()[activeIndex];
return activeTabElement.id.substring(0, activeTabElement.id.lastIndexOf(Wizard.#tabIdSuffix));
}

#navigateToNextTab() {
const activeIndex = this._active;
const activeTabElement = this.getCachedTabs()[activeIndex];
Expand Down Expand Up @@ -334,6 +340,8 @@
#navigateAndFocusTab(index) {
this.navigate(index);
this.focusWithoutScroll(this.getCachedTabs()[index]);
const id = this.getFocusElementId();
this.focusToFirstVisibleField(id);
}

#syncWizardNavLabels() {
Expand Down
82 changes: 82 additions & 0 deletions ui.frontend/src/view/FormPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,88 @@ class FormPanel extends FormFieldBase {
}
}

// focusToFirstVisibleField(id) {
// const form = this.formContainer.getModel();
// const activeTab = this.getModel()._jsonModel.items.find(item => item.id === id);

// if (activeTab && Array.isArray(activeTab.items) && activeTab.items.length > 0) {
// const field = form.getElement(activeTab.id);
// if (field) {
// form.setFocus(field);
// }
// }
// else if (activeTab.fieldType !== "panel") {
// const field = form.getElement(activeTab.id);
// form.setFocus(field);
// }

// }

// ... existing code ...

// ... existing code ...

focusToFirstVisibleField(id) {
const form = this.formContainer.getModel();
const activeTab = this.getModel()._jsonModel.items.find(item => item.id === id);

if (!activeTab) {
return;
}

// Check if it's a panel
if (activeTab.fieldType === "panel") {
const model = form.getElement(activeTab.id);

// Handle repeatable panel case
// if (model && model._children) {
// // Check if there are any instances
// const children = Object.values(model._children);
// if (children.length === 0) {
// return; // No instances, don't set focus
// }

// // Get the active instance (last instance) if multiple instances exist
// const activeInstance = children[children.length - 1];
// if (!activeInstance) {
// return;
// }

// // Get the first field from the active instance's items
// const firstField = form.getElement(activeInstance.items?.[0]?.id);
// if (firstField) {
// form.setFocus(firstField);
// }
// return;
// }

// Handle regular panel case
if (!activeTab.items || !Array.isArray(activeTab.items) || activeTab.items.length === 0) {
return; // Empty panel, don't set focus
}

// Find first field in regular panel
for (let item of activeTab.items) {
const field = form.getElement(item.id);
if (field) {
form.setFocus(field);
break;
}
}
return;
}

// For non-panel elements, set focus directly
const field = form.getElement(activeTab.id);
if (field) {
form.setFocus(field);
}
}





/**
* Adds a child view to the FormPanel.
* @param {Object} childView - The child view to be added.
Expand Down
4 changes: 2 additions & 2 deletions ui.frontend/src/view/FormTabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,12 @@ class FormTabs extends FormPanel {
if (this.#_active !== tabId) {
this.navigate(tabId);
this.focusWithoutScroll(this.#getTabNavElementById(tabId));
const id = this.getActiveTabId(this.#getCachedTabs()).replace(this.#tabIdSuffix,"");
this.focusToFirstVisibleField(id);
}
}



#getTabNavElementById(tabId) {
var tabs = this.#getCachedTabs();
if (tabs) {
Expand Down Expand Up @@ -328,7 +329,6 @@ class FormTabs extends FormPanel {
}
}


/**
* Synchronizes tab labels with their corresponding tab panels.
* Updates the ID and aria-controls attribute of each tab label.
Expand Down
22 changes: 21 additions & 1 deletion ui.tests/test-module/specs/accordion/accordion.runtime.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,6 @@ describe("Form with Accordion Layout Container with focus", () => {
cy.get(`#${secondChildComponentButtonId}`).should('have.class', 'cmp-accordion__button--expanded');
cy.get(`#${firstChildComponentButtonId}`).should('not.have.class', 'cmp-accordion__button--expanded');
cy.get(`#${firstChildComponentPanelId}`).should('not.have.class', 'cmp-accordion__panel--expanded');

cy.get(`#${firstChildComponentButtonId}`).then(() => {
formContainer.setFocus(id);
cy.get(`#${firstChildComponentButtonId}`).isElementInViewport().should("eq", true);
Expand All @@ -212,6 +211,27 @@ describe("Form with Accordion Layout Container with focus", () => {
});
});
});

it("on clicking of expand button, focus should be visible on first component in the current tab ", () => {
const [id, fieldView] = Object.entries(formContainer._fields)[0];
const firstChildComponentId = formContainer._model.items[0].items[0].id;
const firstChildComponentButtonId = firstChildComponentId + "-button";
const firstChildComponentPanelId = firstChildComponentId + "-panel";

const secondChildComponentId = formContainer._model.items[0].items[1].id;
const secondChildComponentButtonId = secondChildComponentId + "-button";
const secondChildComponentPanelId = secondChildComponentId + "-panel";

cy.get(`#${secondChildComponentButtonId}`).click({force: true}).then(() => {
cy.get('input[name="textinputfa2"]').should('be.focused');
cy.get(`#${secondChildComponentPanelId}`).should('have.class', 'cmp-accordion__panel--expanded');
})

cy.get(`#${firstChildComponentButtonId}`).click({force: true}).then(() => {
cy.get('input[name="textinputfa1"]').should('be.focused');
cy.get(`#${firstChildComponentPanelId}`).should('have.class', 'cmp-accordion__panel--expanded');
})
})
});

describe("Form with Accordion Layout Container with Hidden Children", () => {
Expand Down
24 changes: 24 additions & 0 deletions ui.tests/test-module/specs/tabsontop/tabsontop.runtime.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,30 @@ describe("Form with Tabsontop Layout Container with focus", () => {

});

it("first component should always be focused in the form", () => {
const [id, fieldView] = Object.entries(formContainer._fields)[0];
tab1().should('have.class', 'cmp-tabs__tab--active');
tab1().should('have.attr', 'aria-selected', 'true');
tab2().should('have.attr', 'aria-selected', 'false');
cy.get('input[name="textinputft1"]').should('be.focused');
})

it("focus should be on the first component when navigated to the next tab", () => {
const [id, fieldView] = Object.entries(formContainer._fields)[0];
tab2().click().then(() => {
tab2().should('have.class', 'cmp-tabs__tab--active');
tab2().should('have.attr', 'aria-selected', 'true');
cy.get('input[name="textinputft2"]').should('be.focused');
tab1().should('have.attr', 'aria-selected', 'false');
});
tab1().click().then(() => {
tab1().should('have.class', 'cmp-tabs__tab--active');
tab1().should('have.attr', 'aria-selected', 'true');
cy.get('input[name="textinputft1"]').should('be.focused');
tab2().should('have.attr', 'aria-selected', 'false');
});
})


});
describe("Form with TabsOnTop Layout Container with Hidden Children", () => {
Expand Down
23 changes: 23 additions & 0 deletions ui.tests/test-module/specs/wizard/wizard.runtime.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,29 @@ describe("Form with Wizard Layout Container with focus", () => {
});
});

it("First component should always have focus", () => {
const [id, fieldView] = Object.entries(formContainer._fields)[0];
cy.get(".cmp-adaptiveform-wizard__tab").eq(0).should('have.class', 'cmp-adaptiveform-wizard__tab--active');
cy.get(".cmp-adaptiveform-wizard__tab").eq(1).should('not.have.class', 'cmp-adaptiveform-wizard__tab--active');
cy.get(".cmp-adaptiveform-wizard__wizardpanel").eq(0).should('have.class', 'cmp-adaptiveform-wizard__wizardpanel--active');
cy.get(".cmp-adaptiveform-wizard__wizardpanel").eq(1).should('not.have.class', 'cmp-adaptiveform-wizard__wizardpanel--active');
cy.get('input[name="textinputfw1"]').should('be.focused');
cy.get(".cmp-adaptiveform-wizard__nav--next").click({force: true}).then(() => {
cy.get(".cmp-adaptiveform-wizard__tab").eq(0).should('not.have.class', 'cmp-adaptiveform-wizard__tab--active');
cy.get(".cmp-adaptiveform-wizard__tab").eq(1).should('have.class', 'cmp-adaptiveform-wizard__tab--active');
cy.get(".cmp-adaptiveform-wizard__wizardpanel").eq(0).should('not.have.class', 'cmp-adaptiveform-wizard__wizardpanel--active');
cy.get(".cmp-adaptiveform-wizard__wizardpanel").eq(1).should('have.class', 'cmp-adaptiveform-wizard__wizardpanel--active');
cy.get('input[name="textinputfw2"]').should('be.focused');
});
cy.get(".cmp-adaptiveform-wizard__nav--previous").click({force: true}).then(() => {
cy.get(".cmp-adaptiveform-wizard__tab").eq(0).should('have.class', 'cmp-adaptiveform-wizard__tab--active');
cy.get(".cmp-adaptiveform-wizard__tab").eq(1).should('not.have.class', 'cmp-adaptiveform-wizard__tab--active');
cy.get(".cmp-adaptiveform-wizard__wizardpanel").eq(0).should('have.class', 'cmp-adaptiveform-wizard__wizardpanel--active');
cy.get(".cmp-adaptiveform-wizard__wizardpanel").eq(1).should('not.have.class', 'cmp-adaptiveform-wizard__wizardpanel--active');
cy.get('input[name="textinputfw1"]').should('be.focused');
});
})

});

describe("Form with wizard Layout Container with Hidden Children", () => {
Expand Down

0 comments on commit b77c7d1

Please sign in to comment.