diff --git a/packages/main/cypress/specs/ColorPalettePopover.cy.tsx b/packages/main/cypress/specs/ColorPalettePopover.cy.tsx new file mode 100644 index 000000000000..24dfe038319f --- /dev/null +++ b/packages/main/cypress/specs/ColorPalettePopover.cy.tsx @@ -0,0 +1,133 @@ +import Button from "../../src/Button.js"; +import ColorPalettePopover from "../../src/ColorPalettePopover.js"; +import ColorPaletteItem from "../../src/ColorPaletteItem.js"; + +describe("Color Popover Palette tests", () => { + describe("Home and End keyboard navigation", () => { + it("showDefaultColor & showMoreColors", () => { + cy.mount(<> + + + + + + + + ); + + cy.get("[ui5-color-palette-popover]") + .ui5PaletteOpen(); + + cy.focused() + .should("have.attr", "aria-label", "Default Color"); + + cy.focused() + .realPress("End"); + + cy.focused() + .should("have.attr", "aria-label", "More Colors..."); + + cy.focused() + .realPress("Home"); + + cy.focused() + .should("have.attr", "aria-label", "Default Color"); + }); + + it("showDefaultColor", () => { + cy.mount(<> + + + + + + + + ); + + cy.get("[ui5-color-palette-popover]") + .ui5PaletteOpen(); + + cy.focused() + .realPress("End"); + + cy.focused() + .should("have.attr", "aria-label", "Color - 4: red"); + + cy.focused() + .realPress("Home"); + + cy.focused() + .should("have.attr", "aria-label", "Color - 1: cyan"); + + cy.focused() + .realPress("Home"); + + cy.focused() + .should("have.attr", "aria-label", "Default Color"); + }); + + it("showMoreColors", () => { + cy.mount(<> + + + + + + + + ); + + cy.get("[ui5-color-palette-popover]") + .ui5PaletteOpen(); + + cy.focused() + .should("have.attr", "aria-label", "Color - 1: cyan"); + + cy.focused() + .realPress("End"); + + cy.focused() + .should("have.attr", "aria-label", "Color - 4: red"); + + cy.focused() + .realPress("End"); + + cy.focused() + .should("have.attr", "aria-label", "More Colors..."); + }); + + it("Item navigation End", () => { + cy.mount(<> + + + + + + + + + + + ); + + cy.get("[ui5-color-palette-popover]") + .ui5PaletteOpen(); + + cy.focused() + .should("have.attr", "aria-label", "Color - 1: cyan"); + + cy.focused() + .realPress("ArrowDown"); + + cy.focused() + .should("have.attr", "aria-label", "Color - 6: purple"); + + cy.focused() + .realPress("End"); + + cy.focused() + .should("have.attr", "aria-label", "Color - 7: red"); + }); + }); +}); \ No newline at end of file diff --git a/packages/main/cypress/support/commands.ts b/packages/main/cypress/support/commands.ts index ec83e0f4d043..c74d0c439ed2 100644 --- a/packages/main/cypress/support/commands.ts +++ b/packages/main/cypress/support/commands.ts @@ -43,6 +43,7 @@ import { ModifierKey } from "./commands/common/types.js"; // Please keep this list in alphabetical order import "./commands/Calendar.commands.js"; import "./commands/ColorPalette.commands.js"; +import "./commands/ColorPalettePopover.commands.ts"; import "./commands/ColorPicker.commands.js"; import "./commands/DateTimePicker.commands.js"; import "./commands/DateRangePicker.commands.js"; diff --git a/packages/main/cypress/support/commands/ColorPalettePopover.commands.ts b/packages/main/cypress/support/commands/ColorPalettePopover.commands.ts new file mode 100644 index 000000000000..72e280b5b82e --- /dev/null +++ b/packages/main/cypress/support/commands/ColorPalettePopover.commands.ts @@ -0,0 +1,44 @@ +Cypress.Commands.add("ui5PaletteOpen", { prevSubject: true }, (prevSubject, options) => { + cy.wrap(prevSubject) + .as("palette") + .then($palette => { + if (options?.opener) { + cy.wrap($palette) + .invoke("attr", "opener", options.opener); + } + + cy.wrap($palette) + .invoke("attr", "open", true); + }); + + cy.get("@palette") + .ui5PaletteOpened(); +}); + +Cypress.Commands.add("ui5PaletteOpened", { prevSubject: true }, subject => { + cy.wrap(subject) + .as("palette"); + + cy.get("@palette") + .should("have.attr", "open"); + + cy.get("@palette") + .shadow() + .find("[ui5-responsive-popover]") + .should($rp => { + expect($rp.is(":popover-open")).to.be.true; + expect($rp.width()).to.not.equal(0); + expect($rp.height()).to.not.equal(0); + }) + .and("have.attr", "open"); +}); + + +declare global { + namespace Cypress { + interface Chainable { + ui5PaletteOpen(options?: { opener?: string }): Chainable + ui5PaletteOpened(): Chainable + } + } +} \ No newline at end of file diff --git a/packages/main/src/ColorPalette.ts b/packages/main/src/ColorPalette.ts index c6ca6b5c12a4..fc4253b41105 100644 --- a/packages/main/src/ColorPalette.ts +++ b/packages/main/src/ColorPalette.ts @@ -2,6 +2,7 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; +import query from "@ui5/webcomponents-base/dist/decorators/query.js"; import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; @@ -17,6 +18,8 @@ import { isDown, isUp, isTabNext, + isHome, + isEnd, } from "@ui5/webcomponents-base/dist/Keys.js"; import ColorPaletteTemplate from "./ColorPaletteTemplate.js"; import type ColorPaletteItem from "./ColorPaletteItem.js"; @@ -187,6 +190,12 @@ class ColorPalette extends UI5Element { _currentlySelected?: ColorPaletteItem; _shouldFocusRecentColors = false; + @query(".ui5-cp-default-color-button") + _defaultColorButton!: Button; + + @query(".ui5-cp-more-colors") + _moreColorsButton!: Button; + @i18n("@ui5/webcomponents") static i18nBundle: I18nBundle; @@ -309,6 +318,16 @@ class ColorPalette extends UI5Element { this.handleSelection(e.target as ColorPaletteItem); } + _onmousedown(e: MouseEvent) { + const target = e.target as ColorPaletteItem; + + if (this.displayedColors.includes(target)) { + this._itemNavigation.setCurrentItem(target); + } else if (this.recentColorsElements.includes(target)) { + this._itemNavigationRecentColors.setCurrentItem(target); + } + } + _onkeyup(e: KeyboardEvent) { const target = e.target as ColorPaletteItem; if (isSpace(e)) { @@ -326,6 +345,11 @@ class ColorPalette extends UI5Element { if (isSpace(e)) { e.preventDefault(); } + + if (!this.popupMode && (isHome(e) || isEnd(e))) { + e.preventDefault(); + e.stopPropagation(); + } } handleSelection(target: ColorPaletteItem) { @@ -393,6 +417,14 @@ class ColorPalette extends UI5Element { this.focusColorElement(this.displayedColors[colorPaletteFocusIndex], this._itemNavigation); } + } else if (isEnd(e)) { + e.stopPropagation(); + + if (this.showMoreColors) { + this._moreColorsButton?.focus(); + } else if (this.displayedColors.length) { + this.focusColorElement(this.displayedColors[this.displayedColors.length - 1], this._itemNavigation); + } } } @@ -415,6 +447,14 @@ class ColorPalette extends UI5Element { } else { this.focusColorElement(this.displayedColors[0], this._itemNavigation); } + } else if (isHome(e)) { + e.stopPropagation(); + + if (this.showDefaultColor) { + this._defaultColorButton?.focus(); + } else if (this.displayedColors.length) { + this.focusColorElement(this.displayedColors[0], this._itemNavigation); + } } } @@ -435,7 +475,7 @@ class ColorPalette extends UI5Element { this.selectColor(target); } - if (isUp(e) && target === this.displayedColors[0] && this.colorPaletteNavigationElements.length > 1) { + if (isUp(e) && this._isFirstDisplayedSwatch(target) && this.colorPaletteNavigationElements.length > 1) { e.stopPropagation(); if (this.showDefaultColor) { this.firstFocusableElement.focus(); @@ -444,7 +484,7 @@ class ColorPalette extends UI5Element { } else if (!this.showDefaultColor && this.showMoreColors) { lastElementInNavigation.focus(); } - } else if (isDown(e) && target === this.displayedColors[this.displayedColors.length - 1] && this.colorPaletteNavigationElements.length > 1) { + } else if (isDown(e) && this._isLastDisplayedSwatch(target) && this.colorPaletteNavigationElements.length > 1) { e.stopPropagation(); const isRecentColorsNextElement = (this.showDefaultColor && !this.showMoreColors && this.hasRecentColors) || (!this.showDefaultColor && !this.showMoreColors && this.hasRecentColors); @@ -457,9 +497,32 @@ class ColorPalette extends UI5Element { } else if (!this.showDefaultColor && this.showMoreColors) { this.colorPaletteNavigationElements[1].focus(); } + } else if (isHome(e) && this.showDefaultColor && this._isFirstDisplayedSwatch(target)) { + e.stopPropagation(); + this._defaultColorButton?.focus(); + } else if (isEnd(e) && this.showMoreColors && this._isLastDisplayedSwatch(target)) { + e.stopPropagation(); + this._moreColorsButton?.focus(); + } else if (isEnd(e) && this._isElementInLastRow(target)) { + e.stopPropagation(); + this.focusColorElement(this.displayedColors[this.displayedColors.length - 1], this._itemNavigation); } } + _isFirstDisplayedSwatch(target: ColorPaletteItem) { + return this.displayedColors[0] === target; + } + + _isLastDisplayedSwatch(target: ColorPaletteItem) { + return this.displayedColors[this.displayedColors.length - 1] === target; + } + + _isElementInLastRow(target: ColorPaletteItem) { + const index = this.displayedColors.indexOf(target); + const length = this.displayedColors.length; + return index >= length - (length % this.rowSize); + } + _onRecentColorsContainerKeyDown(e: KeyboardEvent) { if (this._isUpOrDownNavigatableColorPaletteItem(e)) { this._currentlySelected = undefined; diff --git a/packages/main/src/ColorPaletteTemplate.tsx b/packages/main/src/ColorPaletteTemplate.tsx index 7d726ac84f1e..38030e8911fb 100644 --- a/packages/main/src/ColorPaletteTemplate.tsx +++ b/packages/main/src/ColorPaletteTemplate.tsx @@ -15,6 +15,7 @@ export default function ColorPaletteTemplate(this: ColorPalette) { onClick={this._onclick} onKeyUp={this._onkeyup} onKeyDown={this._onkeydown} + onMouseDown={this._onmousedown} > {this.showDefaultColor &&