diff --git a/packages/main/cypress/specs/ColorPalette.cy.tsx b/packages/main/cypress/specs/ColorPalette.cy.tsx
index 95c59f6f831c..2a9945ad89ec 100644
--- a/packages/main/cypress/specs/ColorPalette.cy.tsx
+++ b/packages/main/cypress/specs/ColorPalette.cy.tsx
@@ -101,9 +101,9 @@ describe("Color Palette tests", () => {
cy.mount();
cy.ui5ColorPaletteNavigateAndCheckSelectedColor("#cp1", 0, "ArrowRight", "pink");
- cy.ui5ColorPaletteNavigateAndCheckSelectedColor("#cp1", 0, "ArrowLeft", "orange");
- cy.ui5ColorPaletteNavigateAndCheckSelectedColor("#cp1", 0, "ArrowUp", "orange");
- cy.ui5ColorPaletteNavigateAndCheckSelectedColor("#cp1", 9, "ArrowDown", "darkblue");
+ cy.ui5ColorPaletteNavigateAndCheckSelectedColor("#cp1", 0, "ArrowLeft", "darkblue");
+ cy.ui5ColorPaletteNavigateAndCheckSelectedColor("#cp1", 0, "ArrowUp", "darkblue");
+ cy.ui5ColorPaletteNavigateAndCheckSelectedColor("#cp1", 9, "ArrowDown", "orange");
});
it("Tests show-recent-colors functionality", () => {
diff --git a/packages/main/cypress/specs/ColorPalettePopover.cy.tsx b/packages/main/cypress/specs/ColorPalettePopover.cy.tsx
index bad0c933f675..cf6febff3c5b 100644
--- a/packages/main/cypress/specs/ColorPalettePopover.cy.tsx
+++ b/packages/main/cypress/specs/ColorPalettePopover.cy.tsx
@@ -306,7 +306,7 @@ describe("Color Popover Palette events tests", () => {
});
describe("Color Popover Palette arrow keys navigation", () => {
- it("should navigate with Arrow right", () => {
+ it("should navigate with Arrow Down from last swatch to More Colors", () => {
cy.mount(
);
@@ -322,13 +322,13 @@ describe("Color Popover Palette arrow keys navigation", () => {
.and("include", "red");
cy.focused()
- .realPress("ArrowRight");
+ .realPress("ArrowDown");
cy.focused()
.should("have.attr", "aria-label", "More Colors...");
cy.focused()
- .realPress("ArrowLeft");
+ .realPress("ArrowUp");
cy.focused()
.should("have.attr", "aria-label")
@@ -366,7 +366,7 @@ describe("Color Popover Palette arrow keys navigation", () => {
.should("have.attr", "_selected-color", "hotpink");
});
- it("should navigate with Arrow left", () => {
+ it("should navigate with Arrow Down and Arrow Up between Default Color and swatches", () => {
cy.mount(
);
@@ -389,9 +389,9 @@ describe("Color Popover Palette arrow keys navigation", () => {
cy.get("@defaultColorButton")
.should("have.focus");
- // Navigate right to first color item
+ // Navigate down to first color item
cy.get("@defaultColorButton")
- .realPress("ArrowRight");
+ .realPress("ArrowDown");
cy.get("[ui5-color-palette-popover]")
.ui5GetColorPaletteItem(0)
@@ -399,9 +399,6 @@ describe("Color Popover Palette arrow keys navigation", () => {
.should("be.visible")
.and("have.attr", "value", "cyan");
- cy.get("@firstColorItem")
- .should("have.attr", "value", "cyan");
-
cy.get("@firstColorItem")
.should("have.focus")
.shadow()
@@ -411,7 +408,7 @@ describe("Color Popover Palette arrow keys navigation", () => {
.and("include", "cyan");
cy.focused()
- .realPress("ArrowLeft");
+ .realPress("ArrowUp");
cy.focused()
.should("have.attr", "aria-label", "Default Color");
@@ -420,7 +417,7 @@ describe("Color Popover Palette arrow keys navigation", () => {
.should("have.focus");
});
- it("should cycle through colors horizontally with left/right arrows", () => {
+ it("should stay at boundary when navigating horizontally past edges", () => {
cy.mount(
);
@@ -450,20 +447,33 @@ describe("Color Popover Palette arrow keys navigation", () => {
.should("have.attr", "aria-label")
.and("include", "red");
+ // At last item, ArrowRight stays (no sections to escape to)
cy.focused()
.realPress("ArrowRight");
+ cy.focused()
+ .should("have.attr", "aria-label")
+ .and("include", "red");
+
+ // Navigate back to first
+ cy.focused()
+ .realPress("ArrowLeft");
+ cy.focused()
+ .realPress("ArrowLeft");
+ cy.focused()
+ .realPress("ArrowLeft");
cy.focused()
.should("have.attr", "aria-label")
.and("include", "cyan");
+ // At first item, ArrowLeft stays (no sections to escape to)
cy.focused()
.realPress("ArrowLeft");
cy.focused()
.should("have.attr", "aria-label")
- .and("include", "red");
+ .and("include", "cyan");
});
- it("should cycle through colors vertically with up/down arrows", () => {
+ it("should navigate vertically with up/down arrows", () => {
cy.mount(
);
@@ -481,17 +491,11 @@ describe("Color Popover Palette arrow keys navigation", () => {
.should("have.attr", "aria-label")
.and("include", "yellow");
- cy.focused()
- .realPress("ArrowDown");
- cy.focused()
- .should("have.attr", "aria-label")
- .and("include", "orange");
-
cy.focused()
.realPress("ArrowUp");
cy.focused()
.should("have.attr", "aria-label")
- .and("include", "yellow");
+ .and("include", "cyan");
});
it("should navigate to More Colors from colors grid", () => {
@@ -502,19 +506,24 @@ describe("Color Popover Palette arrow keys navigation", () => {
cy.get("[ui5-color-palette-popover]")
.ui5ColorPalettePopoverOpen({ opener: "btnPalette" });
+ // Navigate to last item in second (incomplete) row
+ cy.focused()
+ .realPress("End");
cy.focused()
.realPress("End");
cy.focused()
.should("have.attr", "aria-label")
- .and("include", "green");
+ .and("include", "purple");
+ // ArrowDown from last row goes to More Colors
cy.focused()
.realPress("ArrowDown");
cy.focused()
.should("have.attr", "aria-label", "More Colors...");
+ // ArrowUp from More Colors restores column back into grid
cy.focused()
- .realPress("ArrowLeft");
+ .realPress("ArrowUp");
cy.focused()
.should("have.attr", "aria-label")
.and("include", "purple");
@@ -528,91 +537,55 @@ describe("Color Popover Palette arrow keys navigation", () => {
cy.get("[ui5-color-palette-popover]")
.ui5ColorPalettePopoverOpen({ opener: "btnPalette" });
+ // Navigate to second row item
cy.focused()
- .realPress("End");
+ .realPress("ArrowDown");
+ cy.focused()
+ .should("have.attr", "aria-label")
+ .and("include", "yellow");
+
+ // Navigate right within incomplete row
+ cy.focused()
+ .realPress("ArrowRight");
cy.focused()
.should("have.attr", "aria-label")
- .and("include", "green");
+ .and("include", "purple");
+ // ArrowUp from column 1 in last row goes to column 1 in first row
cy.focused()
.realPress("ArrowUp");
cy.focused()
.should("have.attr", "aria-label")
- .and("include", "red");
+ .and("include", "orange");
});
});
describe("Color Popover Palette Home and End keyboard navigation", () => {
- it.skip("should navigate with Home/End when showDefaultColor is set", () => {
- cy.mount(
-
- );
-
- cy.get("[ui5-color-palette-popover]")
- .ui5ColorPalettePopoverOpen({ opener: "btnPalette" });
-
- cy.get("[ui5-color-palette-popover]")
- .should("have.attr", "open");
-
- cy.get("[ui5-color-palette-popover]")
- .ui5GetColorPaletteInPopover()
- .as("colorPalette");
-
- cy.get("@colorPalette")
- .ui5GetColorPaletteDefaultButton()
- .as("defaultColorButton")
- .should("be.visible")
- .and("have.focus");
-
- cy.get("@defaultColorButton")
- .should("have.focus")
- .shadow()
- .find("button[data-sap-focus-ref]")
- .should("have.focus");
-
- cy.get("@defaultColorButton")
- .realPress("End");
-
- cy.get("[ui5-color-palette-popover]")
- .ui5GetColorPaletteItem(3)
- .as("lastColorPaletteItem")
- .should("be.visible")
- .and("have.attr", "value", "red");
-
- cy.get("@lastColorPaletteItem")
- .should("have.focus")
- .shadow()
- .find(".ui5-cp-item")
- .should("have.attr", "tabindex", "0")
- .and("have.attr", "aria-label")
- .and("include", "red");
-
- cy.focused()
- .realPress("Home");
-
- cy.get("[ui5-color-palette-popover]")
- .ui5GetColorPaletteItem(0)
- .as("firstColorPaletteItem")
- .should("be.visible")
- .and("have.attr", "value", "cyan");
-
- cy.get("@firstColorPaletteItem")
- .should("have.focus")
- .shadow()
- .find(".ui5-cp-item")
- .should("have.attr", "tabindex", "0")
- .and("have.attr", "aria-label")
- .and("include", "cyan");
-
- cy.focused()
- .realPress("Home");
-
- cy.focused()
- .should("have.attr", "aria-label", "Default Color");
-
- cy.get("@defaultColorButton")
- .should("have.focus");
- });
+ it("should navigate with Home/End when showDefaultColor is set", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-color-palette-popover]")
+ .ui5ColorPalettePopoverOpen({ opener: "btnPalette" });
+
+ cy.focused()
+ .should("have.attr", "aria-label", "Default Color")
+ .realPress("End");
+
+ cy.focused()
+ .should("have.attr", "aria-label")
+ .and("include", "red")
+ .realPress("Home");
+
+ cy.focused()
+ .should("have.attr", "aria-label")
+ .and("include", "cyan")
+ .realPress("Home");
+
+ cy.focused()
+ .should("have.attr", "aria-label", "Default Color");
+ });
it("should navigate with Home/End keys when showMoreColors is set", () => {
cy.mount(
@@ -640,43 +613,25 @@ describe("Color Popover Palette Home and End keyboard navigation", () => {
.should("have.attr", "aria-label", "More Colors...");
});
- it.skip("should navigate with Home/End when showDefaultColor & showMoreColors are set", () => {
- cy.mount(
-
- );
-
- cy.get("[ui5-color-palette-popover]")
- .as("colorPalettePopover")
- .ui5ColorPalettePopoverOpen({ opener: "btnPalette" });
-
- cy.get("@colorPalettePopover")
- .ui5GetColorPaletteInPopover()
- .as("colorPalette");
-
- cy.get("@colorPalette")
- .ui5GetColorPaletteDefaultButton()
- .as("defaultColorButton");
-
- cy.get("@defaultColorButton")
- .should("have.focus")
- .realPress("End");
+ it("should navigate with Home/End when showDefaultColor and showMoreColors are set", () => {
+ cy.mount(
+
+ );
- cy.get("@colorPalette")
- .ui5GetColorPaletteMoreColorsButton()
- .as("moreColorsButton")
- .should("be.visible");
+ cy.get("[ui5-color-palette-popover]")
+ .ui5ColorPalettePopoverOpen({ opener: "btnPalette" });
- cy.get("@moreColorsButton")
- .should("exist")
- .and("be.visible")
- .and("have.focus");
+ cy.focused()
+ .should("have.attr", "aria-label", "Default Color")
+ .realPress("End");
- cy.get("@moreColorsButton")
- .realPress("Home");
+ cy.focused()
+ .should("have.attr", "aria-label", "More Colors...")
+ .realPress("Home");
- cy.get("@defaultColorButton")
- .should("have.focus");
- });
+ cy.focused()
+ .should("have.attr", "aria-label", "Default Color");
+ });
it("should navigate with End key", () => {
cy.mount(
diff --git a/packages/main/src/ColorPalette.ts b/packages/main/src/ColorPalette.ts
index 30787ef8aa1b..a3b3a2572b21 100644
--- a/packages/main/src/ColorPalette.ts
+++ b/packages/main/src/ColorPalette.ts
@@ -210,6 +210,7 @@ class ColorPalette extends UI5Element {
_recentColors: Array;
_currentlySelected?: ColorPaletteItem;
_shouldFocusRecentColors = false;
+ _iLastFocusedColumn = 0;
@query(".ui5-cp-default-color-button")
_defaultColorButton!: Button;
@@ -225,13 +226,13 @@ class ColorPalette extends UI5Element {
this._itemNavigation = new ItemNavigation(this, {
getItemsCallback: () => this.displayedColors,
rowSize: this.rowSize,
- behavior: ItemNavigationBehavior.Cyclic,
+ behavior: ItemNavigationBehavior.Static,
});
this._itemNavigationRecentColors = new ItemNavigation(this, {
getItemsCallback: () => this.recentColorsElements,
rowSize: this.rowSize,
- behavior: ItemNavigationBehavior.Cyclic,
+ behavior: ItemNavigationBehavior.Static,
});
this._recentColors = [];
@@ -385,9 +386,12 @@ class ColorPalette extends UI5Element {
// Handle selection for items within the 'recentColorsElements'
if (this.recentColorsElements.includes(target)) {
- this.recentColorsElements[0].selected = true;
- this.recentColorsElements[0].focus();
- this._currentlySelected = this.recentColorsElements[0];
+ const firstRecentColor = this.recentColorsElements[0];
+ if (firstRecentColor) {
+ firstRecentColor.selected = true;
+ this.focusColorElement(firstRecentColor, this._itemNavigationRecentColors);
+ this._currentlySelected = firstRecentColor;
+ }
} else {
this.allColorsInPalette.forEach(item => {
item.selected = item === target;
@@ -422,29 +426,21 @@ class ColorPalette extends UI5Element {
this._handleDefaultColorClick(e);
}
- if (this._isNext(e)) {
+ if (isDown(e)) {
e.preventDefault();
e.stopPropagation();
- this._focusFirstDisplayedColor();
- } else if (isLeft(e)) {
- e.preventDefault();
- e.stopPropagation();
- this._focusFirstAvailable(
- () => this._focusLastRecentColor(),
- () => this._focusMoreColors(),
- () => this._focusLastDisplayedColor(),
- );
+ // DOWN: enter palette grid restoring column
+ this._focusPaletteGridWithColumnRestore(true);
} else if (isUp(e)) {
e.preventDefault();
e.stopPropagation();
+ // UP: navigate backward with column restore
this._focusFirstAvailable(
- () => this._focusLastRecentColor(),
+ () => this._focusRecentColorsWithColumnRestore(),
() => this._focusMoreColors(),
- () => this._focusLastSwatchOfLastFullRow(),
- () => this._focusLastDisplayedColor(),
+ () => this._focusPaletteGridWithColumnRestore(false),
);
} else if (isEnd(e)) {
- // Prevent Home/End keys from working in embedded mode - they only work in popup mode as per design
if (this._shouldPreventHomeEnd(e)) {
e.preventDefault();
e.stopPropagation();
@@ -456,36 +452,43 @@ class ColorPalette extends UI5Element {
() => this._focusMoreColors(),
() => this._focusLastDisplayedColor(),
);
+ } else if (isHome(e)) {
+ if (this._shouldPreventHomeEnd(e)) {
+ e.preventDefault();
+ e.stopPropagation();
+ return;
+ }
+ // Default Color is the first element, Home stays here
+ e.preventDefault();
+ e.stopPropagation();
}
}
_onMoreColorsKeyDown(e: KeyboardEvent) {
- if (isLeft(e)) {
+ if (this._shouldPreventHomeEnd(e)) {
e.preventDefault();
e.stopPropagation();
- this._focusLastDisplayedColor();
- } else if (isUp(e)) {
+ return;
+ }
+
+ if (isUp(e)) {
e.preventDefault();
e.stopPropagation();
+ // UP: navigate backward with column restore
this._focusFirstAvailable(
- () => this._focusLastSwatchOfLastFullRow(),
+ () => this._focusPaletteGridWithColumnRestore(false),
() => this._focusLastDisplayedColor(),
);
- } else if (this._isNext(e)) {
+ } else if (isDown(e)) {
e.preventDefault();
e.stopPropagation();
+ // DOWN: navigate forward with column restore
this._focusFirstAvailable(
- () => this._focusFirstRecentColor(),
+ () => this._focusRecentColorsWithColumnRestore(),
() => this._focusDefaultColor(),
- () => this._focusFirstDisplayedColor(),
+ () => this._focusPaletteGridWithColumnRestore(true),
);
} else if (isHome(e)) {
- // Prevent Home/End keys from working in embedded mode - they only work in popup mode as per design
- if (this._shouldPreventHomeEnd(e)) {
- e.preventDefault();
- e.stopPropagation();
- return;
- }
e.preventDefault();
e.stopPropagation();
this._focusFirstAvailable(
@@ -493,12 +496,6 @@ class ColorPalette extends UI5Element {
() => this._focusFirstDisplayedColor(),
);
} else if (isEnd(e)) {
- // Prevent Home/End keys from working in embedded mode - they only work in popup mode as per design
- if (this._shouldPreventHomeEnd(e)) {
- e.preventDefault();
- e.stopPropagation();
- return;
- }
// More Colors button is typically the last element, so END key stays here
e.preventDefault();
e.stopPropagation();
@@ -507,7 +504,6 @@ class ColorPalette extends UI5Element {
_onColorContainerKeyDown(e: KeyboardEvent) {
const target = e.target as ColorPaletteItem;
- const isLastSwatchInSingleRow = this._isSingleRow() && this._isLastSwatch(target, this.displayedColors);
// Prevent Home/End keys from working in embedded mode - they only work in popup mode as per design
if (this._shouldPreventHomeEnd(e)) {
@@ -525,47 +521,88 @@ class ColorPalette extends UI5Element {
this.selectColor(target);
}
- if (this._isPrevious(e) && this._isFirstSwatch(target, this.displayedColors)) {
+ const isAtLeftBorder = isLeft(e) && this._isFirstSwatchInRow(target);
+ const isAtRightBorder = isRight(e) && this._isLastSwatchInRow(target);
+
+ if (isAtLeftBorder || isAtRightBorder) {
e.preventDefault();
e.stopPropagation();
- this._focusFirstAvailable(
- () => this._focusDefaultColor(),
- () => this._focusLastRecentColor(),
- () => this._focusMoreColors(),
- () => this._focusLastSwatchOfLastFullRow(),
- () => this._focusLastDisplayedColor(),
- );
- } else if ((isRight(e) && this._isLastSwatch(target, this.displayedColors))
- || (isDown(e) && (this._isLastSwatchOfLastFullRow(target) || isLastSwatchInSingleRow))
- ) {
+ return;
+ }
+
+ // UP on first row → escape to previous section (popover) or stay (standalone)
+ if (isUp(e) && this._isInFirstRow(target)) {
e.preventDefault();
e.stopPropagation();
- this._focusFirstAvailable(
- () => this._focusMoreColors(),
- () => this._focusFirstRecentColor(),
- () => this._focusDefaultColor(),
- () => this._focusFirstDisplayedColor(),
- );
- } else if (isHome(e) && this._isFirstSwatchInRow(target)) {
+ if (this.popupMode) {
+ this._storeColumn(target, this.displayedColors);
+ this._focusFirstAvailable(
+ () => this._focusDefaultColor(),
+ () => this._focusRecentColorsWithColumnRestore(),
+ () => this._focusMoreColors(),
+ );
+ }
+ return;
+ }
+
+ // DOWN on last row (no item below) → escape to next section (popover) or stay (standalone)
+ if (isDown(e) && !this._hasItemBelow(target)) {
e.preventDefault();
e.stopPropagation();
- this._focusFirstAvailable(
- () => this._focusDefaultColor(),
- () => this._focusMoreColors(),
- () => this._focusFirstDisplayedColor(),
- );
- } else if (isEnd(e) && this._isLastSwatchInRow(target)) {
+ if (this.popupMode) {
+ this._storeColumn(target, this.displayedColors);
+ this._focusFirstAvailable(
+ () => this._focusMoreColors(),
+ () => this._focusRecentColorsWithColumnRestore(),
+ () => this._focusDefaultColor(),
+ () => this._focusFirstDisplayedColor(),
+ );
+ }
+ return;
+ }
+
+ // Home enhanced multi-step behavior (popover only)
+ if (isHome(e)) {
e.preventDefault();
e.stopPropagation();
- this._focusFirstAvailable(
- () => this._focusMoreColors(),
- () => this._focusDefaultColor(),
- () => this._focusLastDisplayedColor(),
- );
- } else if (isEnd(e) && this._isSwatchInLastRow(target)) {
+ const index = this.displayedColors.indexOf(target);
+ const colOffset = index % this.rowSize;
+ if (colOffset !== 0) {
+ // Step 1: go to first item in current row
+ this.focusColorElement(this.displayedColors[index - colOffset], this._itemNavigation);
+ } else if (index !== 0) {
+ // Step 2: in first column but not first item → go to first item in grid
+ this.focusColorElement(this.displayedColors[0], this._itemNavigation);
+ } else {
+ // Step 3: already on first item → escape backward
+ this._focusFirstAvailable(
+ () => this._focusDefaultColor(),
+ () => this._focusMoreColors(),
+ );
+ }
+ return;
+ }
+
+ // End enhanced multi-step behavior (popover only)
+ if (isEnd(e)) {
e.preventDefault();
e.stopPropagation();
- this._focusLastDisplayedColor();
+ const index = this.displayedColors.indexOf(target);
+ const lastIndex = this.displayedColors.length - 1;
+ const rowEnd = Math.min(index - (index % this.rowSize) + this.rowSize - 1, lastIndex);
+ if (index !== rowEnd) {
+ // Step 1: go to last item in current row
+ this.focusColorElement(this.displayedColors[rowEnd], this._itemNavigation);
+ } else if (index !== lastIndex) {
+ // Step 2: in last column but not last item → go to last item in grid
+ this.focusColorElement(this.displayedColors[lastIndex], this._itemNavigation);
+ } else {
+ // Step 3: already on last item → escape forward
+ this._focusFirstAvailable(
+ () => this._focusMoreColors(),
+ () => this._focusDefaultColor(),
+ );
+ }
}
}
@@ -583,27 +620,77 @@ class ColorPalette extends UI5Element {
this._currentlySelected = undefined;
}
- if (this._isNext(e) && this._isLastSwatch(target, this.recentColorsElements)) {
+ const isAtLeftBorder = isLeft(e) && this._isFirstSwatch(target, this.recentColorsElements);
+ const isAtRightBorder = isRight(e) && this._isLastSwatch(target, this.recentColorsElements);
+
+ if (isAtLeftBorder || isAtRightBorder) {
e.preventDefault();
e.stopPropagation();
- this._focusFirstAvailable(
- () => this._focusDefaultColor(),
- () => this._focusMoreColors(),
- () => this._focusFirstDisplayedColor(),
- );
- } else if (this._isPrevious(e) && this._isFirstSwatch(target, this.recentColorsElements)) {
+ return;
+ }
+
+ // UP from any recent color → escape to previous section (popover) or stay (standalone)
+ if (isUp(e)) {
e.preventDefault();
e.stopPropagation();
- this._focusFirstAvailable(
- () => this._focusMoreColors(),
- () => this._focusLastSwatchOfLastFullRow(),
- () => this._focusLastDisplayedColor(),
- () => this._focusDefaultColor(),
- );
- } else if (isEnd(e)) {
+ if (this.popupMode) {
+ this._storeColumn(target, this.recentColorsElements);
+ this._focusFirstAvailable(
+ () => this._focusMoreColors(),
+ () => this._focusPaletteGridWithColumnRestore(false),
+ () => this._focusDefaultColor(),
+ );
+ }
+ return;
+ }
+
+ // DOWN from any recent color → escape to next section (popover) or stay (standalone)
+ if (isDown(e)) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this.popupMode) {
+ this._storeColumn(target, this.recentColorsElements);
+ this._focusFirstAvailable(
+ () => this._focusDefaultColor(),
+ () => this._focusPaletteGridWithColumnRestore(true),
+ );
+ }
+ return;
+ }
+
+ // Home enhanced behavior (popover only)
+ if (isHome(e)) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._isFirstSwatch(target, this.recentColorsElements)) {
+ // Already at first → escape backward
+ this._focusFirstAvailable(
+ () => this._focusMoreColors(),
+ () => this._focusPaletteGridWithColumnRestore(false),
+ () => this._focusDefaultColor(),
+ );
+ } else {
+ // Go to first recent color
+ this.focusColorElement(this.recentColorsElements[0], this._itemNavigationRecentColors);
+ }
+ return;
+ }
+
+ // End enhanced behavior (popover only)
+ if (isEnd(e)) {
e.preventDefault();
e.stopPropagation();
- this._focusLastRecentColor();
+ if (this._isLastSwatch(target, this.recentColorsElements)) {
+ // Already at last → escape forward
+ this._focusFirstAvailable(
+ () => this._focusDefaultColor(),
+ () => this._focusPaletteGridWithColumnRestore(true),
+ );
+ } else {
+ // Go to last recent color
+ const lastIdx = this.recentColorsElements.length - 1;
+ this.focusColorElement(this.recentColorsElements[lastIdx], this._itemNavigationRecentColors);
+ }
}
}
@@ -620,14 +707,6 @@ class ColorPalette extends UI5Element {
|| this.recentColorsElements.includes(this._currentlySelected);
}
- _isPrevious(e: KeyboardEvent): boolean {
- return isUp(e) || isLeft(e);
- }
-
- _isNext(e: KeyboardEvent): boolean {
- return isDown(e) || isRight(e);
- }
-
_isFirstSwatch(target: ColorPaletteItem, swatches: Array): boolean {
return swatches && Boolean(swatches.length) && swatches[0] === target;
}
@@ -655,47 +734,46 @@ class ColorPalette extends UI5Element {
}
/**
- * Checks if the given color swatch is the last swatch of the last full row.
- *
- * Example 1: 12 colors with rowSize 5
- * Row 1: [0, 1, 2, 3, 4] ← Complete row
- * Row 2: [5, 6, 7, 8, 9] ← Complete row (last complete row)
- * Row 3: [10, 11] ← Incomplete row
- *
- * @param target The color swatch to check.
- * @returns True if the swatch is the last of the last full row, false otherwise.
+ * Checks if HOME/END navigation should be prevented in embedded mode.
+ * In embedded mode, HOME/END keys are blocked as they only work in popup mode per design.
+ * @private
+ * @param e The keyboard event to check
+ * @returns True if the event should be prevented, false otherwise
*/
- _isLastSwatchOfLastFullRow(target: ColorPaletteItem): boolean {
- const index = this.displayedColors.indexOf(target);
- const rowSize = this.rowSize;
- const total = this.displayedColors.length;
- const lastCompleteRowEndIndex = this._getLastCompleteRowEndIndex(total, rowSize);
- return index >= 0 && index === lastCompleteRowEndIndex;
+ _shouldPreventHomeEnd(e: KeyboardEvent): boolean {
+ return !this.popupMode && (isHome(e) || isEnd(e));
}
- _isSwatchInLastRow(target: ColorPaletteItem): boolean {
+ /**
+ * Checks if the target swatch is in the first row of the palette grid.
+ * @private
+ */
+ _isInFirstRow(target: ColorPaletteItem): boolean {
const index = this.displayedColors.indexOf(target);
- const lastRowSwatchesCount = this.displayedColors.length % this.rowSize;
- return index >= 0 && index >= this.displayedColors.length - lastRowSwatchesCount;
+ return index >= 0 && index < this.rowSize;
}
/**
- * Checks if HOME/END navigation should be prevented in embedded mode.
- * In embedded mode, HOME/END keys are blocked as they only work in popup mode per design.
+ * Checks if there's an item directly below the target in the next row.
* @private
- * @param e The keyboard event to check
- * @returns True if the event should be prevented, false otherwise
*/
- _shouldPreventHomeEnd(e: KeyboardEvent): boolean {
- return !this.popupMode && (isHome(e) || isEnd(e));
+ _hasItemBelow(target: ColorPaletteItem): boolean {
+ const index = this.displayedColors.indexOf(target);
+ if (index < 0) {
+ return false;
+ }
+ return index + this.rowSize < this.displayedColors.length;
}
/**
- * Helper to check if all displayed colors fit in a single row
+ * Stores the column position when leaving a grid vertically for cross-section navigation.
* @private
*/
- _isSingleRow(): boolean {
- return this.displayedColors.length <= this.rowSize;
+ _storeColumn(target: ColorPaletteItem, items: Array) {
+ const index = items.indexOf(target);
+ if (index >= 0) {
+ this._iLastFocusedColumn = index % this.rowSize;
+ }
}
/**
@@ -768,53 +846,48 @@ class ColorPalette extends UI5Element {
}
/**
- * Helper to focus last swatch of last full row if available
+ * Focuses a palette grid item using column memory.
+ * @param fromTop true if entering from the top (first row), false for bottom (last row)
* @private
*/
- _focusLastSwatchOfLastFullRow(): boolean {
- const rowSize = this.rowSize;
- const total = this.displayedColors.length;
- const lastCompleteRowEndIndex = this._getLastCompleteRowEndIndex(total, rowSize);
-
- // Return false if there are no full rows (less than one complete row)
- if (lastCompleteRowEndIndex < 0 || !this.displayedColors[lastCompleteRowEndIndex]) {
+ _focusPaletteGridWithColumnRestore(fromTop: boolean): boolean {
+ if (!this.displayedColors.length) {
return false;
}
-
- this.focusColorElement(this.displayedColors[lastCompleteRowEndIndex], this._itemNavigation);
+ const rowSize = this.rowSize;
+ const col = this._iLastFocusedColumn;
+ let targetRow;
+ if (fromTop) {
+ targetRow = 0;
+ } else {
+ // Find the last row that has the remembered column.
+ const lastRowWithCol = Math.floor((this.displayedColors.length - 1 - col) / rowSize);
+ targetRow = Math.max(lastRowWithCol, 0);
+ }
+ let targetIndex = targetRow * rowSize + col;
+ if (targetIndex >= this.displayedColors.length) {
+ // Fall back to the last item if the column doesn't exist at all.
+ if (targetIndex >= this.displayedColors.length) {
+ targetIndex = this.displayedColors.length - 1;
+ }
+ }
+ this.focusColorElement(this.displayedColors[targetIndex], this._itemNavigation);
return true;
}
/**
- * Returns the index of the last swatch in the last complete row.
- * @private
- */
- _getLastCompleteRowEndIndex(total: number, rowSize: number): number {
- return Math.floor(total / rowSize) * rowSize - 1;
- }
-
- /**
- * Helper to focus first recent color if available
+ * Focuses a recent colors grid item using column memory.
* @private
*/
- _focusFirstRecentColor(): boolean {
- if (this.hasRecentColors && this.recentColorsElements.length) {
- this.focusColorElement(this.recentColorsElements[0], this._itemNavigationRecentColors);
- return true;
+ _focusRecentColorsWithColumnRestore(): boolean {
+ if (!this.hasRecentColors || !this.recentColorsElements.length) {
+ return false;
}
- return false;
- }
- /**
- * Helper to focus last recent color if available
- * @private
- */
- _focusLastRecentColor(): boolean {
- if (this.hasRecentColors && this.recentColorsElements.length) {
- this.focusColorElement(this.recentColorsElements[this.recentColorsElements.length - 1], this._itemNavigationRecentColors);
- return true;
- }
- return false;
+ const targetIndex = Math.min(this._iLastFocusedColumn, this.recentColorsElements.length - 1);
+
+ this.focusColorElement(this.recentColorsElements[targetIndex], this._itemNavigationRecentColors);
+ return true;
}
focusColorElement(element: ColorPaletteNavigationItem, itemNavigation: ItemNavigation) {