Skip to content

fix(ui5-color-palette): adjust keyboard navigation #12107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions packages/main/cypress/specs/ColorPalettePopover.cy.tsx
Original file line number Diff line number Diff line change
@@ -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(<>
<Button id="btnPalette">Open Palette</Button>
<ColorPalettePopover opener="btnPalette" showDefaultColor={true} showMoreColors={true}>
<ColorPaletteItem value="cyan"></ColorPaletteItem>
<ColorPaletteItem value="orange"></ColorPaletteItem>
<ColorPaletteItem value="blue"></ColorPaletteItem>
<ColorPaletteItem value="red"></ColorPaletteItem>
</ColorPalettePopover>
</>);

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(<>
<Button id="btnPalette">Open Palette</Button>
<ColorPalettePopover opener="btnPalette" showDefaultColor={true}>
<ColorPaletteItem value="cyan"></ColorPaletteItem>
<ColorPaletteItem value="orange"></ColorPaletteItem>
<ColorPaletteItem value="blue"></ColorPaletteItem>
<ColorPaletteItem value="red"></ColorPaletteItem>
</ColorPalettePopover>
</>);

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(<>
<Button id="btnPalette">Open Palette</Button>
<ColorPalettePopover opener="btnPalette" showMoreColors={true}>
<ColorPaletteItem value="cyan"></ColorPaletteItem>
<ColorPaletteItem value="orange"></ColorPaletteItem>
<ColorPaletteItem value="blue"></ColorPaletteItem>
<ColorPaletteItem value="red"></ColorPaletteItem>
</ColorPalettePopover>
</>);

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(<>
<Button id="btnPalette">Open Palette</Button>
<ColorPalettePopover opener="btnPalette">
<ColorPaletteItem value="cyan"></ColorPaletteItem>
<ColorPaletteItem value="orange"></ColorPaletteItem>
<ColorPaletteItem value="blue"></ColorPaletteItem>
<ColorPaletteItem value="yellow"></ColorPaletteItem>
<ColorPaletteItem value="green"></ColorPaletteItem>
<ColorPaletteItem value="purple"></ColorPaletteItem>
<ColorPaletteItem value="red"></ColorPaletteItem>
</ColorPalettePopover>
</>);

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");
});
});
});
1 change: 1 addition & 0 deletions packages/main/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void>
ui5PaletteOpened(): Chainable<void>
}
}
}
67 changes: 65 additions & 2 deletions packages/main/src/ColorPalette.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
}
}

Expand All @@ -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);
}
}
}

Expand All @@ -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();
Expand All @@ -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);

Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/main/src/ColorPaletteTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default function ColorPaletteTemplate(this: ColorPalette) {
onClick={this._onclick}
onKeyUp={this._onkeyup}
onKeyDown={this._onkeydown}
onMouseDown={this._onmousedown}
>
{this.showDefaultColor &&
<div class="ui5-cp-default-color-button-wrapper">
Expand Down
Loading