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) {