diff --git a/.changeset/date-picker-translation-truncation.md b/.changeset/date-picker-translation-truncation.md new file mode 100644 index 00000000000..9fa87713e33 --- /dev/null +++ b/.changeset/date-picker-translation-truncation.md @@ -0,0 +1,5 @@ +--- +'@siemens/ix': patch +--- + +Fixed weekday label rendering in `ix-date-picker` to avoid truncation in locales with longer translations. diff --git a/.changeset/date-picker-week-start.md b/.changeset/date-picker-week-start.md new file mode 100644 index 00000000000..45dac129cdf --- /dev/null +++ b/.changeset/date-picker-week-start.md @@ -0,0 +1,5 @@ +--- +'@siemens/ix': patch +--- + +Fix `ix-date-picker` calendar rendering when `weekStartIndex` is set to a non-default value. diff --git a/.changeset/date-picker-year-dropdown-keyboard.md b/.changeset/date-picker-year-dropdown-keyboard.md new file mode 100644 index 00000000000..e5c8b07a497 --- /dev/null +++ b/.changeset/date-picker-year-dropdown-keyboard.md @@ -0,0 +1,5 @@ +--- +'@siemens/ix': patch +--- + +Fix `ix-date-picker` year dropdown keyboard navigation so pressing ArrowDown on the last year extends the list and moves focus to the newly added year instead of wrapping back to the start. diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 2fc9cf13946..b9adc3eb3b9 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -33,6 +33,7 @@ import { DateTimeDateChangeEvent, DateTimeSelectEvent } from "./components/datet import { ElementReference } from "./components/utils/element-reference"; import { CloseBehavior } from "./components/dropdown/dropdown-controller"; import { AlignedPlacement } from "./components/dropdown/placement"; +import { KeyboardNavigationBoundaryContext } from "./components/dropdown/dropdown-focus"; import { FocusTrapOptions } from "./components/utils/focus/focus-trap"; import { DropdownButtonVariant } from "./components/dropdown-button/dropdown-button.types"; import { IxDropdownItemRole } from "./components/dropdown-item/dropdown-item.types"; @@ -88,6 +89,7 @@ export { DateTimeDateChangeEvent, DateTimeSelectEvent } from "./components/datet export { ElementReference } from "./components/utils/element-reference"; export { CloseBehavior } from "./components/dropdown/dropdown-controller"; export { AlignedPlacement } from "./components/dropdown/placement"; +export { KeyboardNavigationBoundaryContext } from "./components/dropdown/dropdown-focus"; export { FocusTrapOptions } from "./components/utils/focus/focus-trap"; export { DropdownButtonVariant } from "./components/dropdown-button/dropdown-button.types"; export { IxDropdownItemRole } from "./components/dropdown-item/dropdown-item.types"; @@ -1517,6 +1519,10 @@ export namespace Components { * @default false */ "disableFocusTrap": boolean; + /** + * @default false + */ + "disableWrapFocusNavigation": boolean; /** * @default false */ @@ -1563,6 +1569,9 @@ export namespace Components { crossAxis?: number; alignmentAxis?: number; }; + "onBoundaryFocus"?: ( + context: KeyboardNavigationBoundaryContext + ) => Promise; "overwriteDropdownStyle"?: (delegate: { dropdownRef: HTMLElement; triggerRef?: HTMLElement; @@ -1618,6 +1627,10 @@ export namespace Components { * @default 'both' */ "closeBehavior": 'inside' | 'outside' | 'both' | boolean; + /** + * @default false + */ + "disableWrapFocusNavigation": boolean; /** * Disable button * @default false @@ -1644,6 +1657,9 @@ export namespace Components { * Set label */ "label"?: string | null; + "onBoundaryFocus"?: ( + context: KeyboardNavigationBoundaryContext + ) => Promise; /** * Placement of the dropdown */ @@ -8134,6 +8150,10 @@ declare namespace LocalJSX { * @default false */ "disableFocusTrap"?: boolean; + /** + * @default false + */ + "disableWrapFocusNavigation"?: boolean; /** * @default false */ @@ -8179,6 +8199,9 @@ declare namespace LocalJSX { crossAxis?: number; alignmentAxis?: number; }; + "onBoundaryFocus"?: ( + context: KeyboardNavigationBoundaryContext + ) => Promise; "onExperimentalFocusNextElement"?: (event: IxDropdownCustomEvent) => void; /** * Will be fired only after dropdown changed visibility to "true" @@ -8244,6 +8267,10 @@ declare namespace LocalJSX { * @default 'both' */ "closeBehavior"?: 'inside' | 'outside' | 'both' | boolean; + /** + * @default false + */ + "disableWrapFocusNavigation"?: boolean; /** * Disable button * @default false @@ -8269,6 +8296,9 @@ declare namespace LocalJSX { * Set label */ "label"?: string | null; + "onBoundaryFocus"?: ( + context: KeyboardNavigationBoundaryContext + ) => Promise; /** * Fire event before visibility of dropdown has changed, preventing event will cancel showing dropdown */ @@ -11879,6 +11909,7 @@ declare namespace LocalJSX { "disableFocusTrap": boolean; "enableTopLayer": boolean; "focusCheckedItem": boolean; + "disableWrapFocusNavigation": boolean; "discoverAllSubmenus": boolean; "ignoreRelatedSubmenu": boolean; "suppressOverflowBehavior": boolean; @@ -11893,6 +11924,7 @@ declare namespace LocalJSX { "placement": AlignedPlacement; "ariaLabelDropdownButton": string; "focusCheckedItem": boolean; + "disableWrapFocusNavigation": boolean; "enableTopLayer": boolean; "suppressAriaActiveDescendant": boolean; } diff --git a/packages/core/src/components/date-picker/date-picker.scss b/packages/core/src/components/date-picker/date-picker.scss index bd67ecd03e6..f7baa84a3fb 100644 --- a/packages/core/src/components/date-picker/date-picker.scss +++ b/packages/core/src/components/date-picker/date-picker.scss @@ -265,10 +265,6 @@ border: none; background: none; cursor: initial; - - .overflow { - @include text-truncation.ellipsis; - } } &.empty-day { diff --git a/packages/core/src/components/date-picker/date-picker.tsx b/packages/core/src/components/date-picker/date-picker.tsx index 98962cc8a44..2c9f98a0cb1 100644 --- a/packages/core/src/components/date-picker/date-picker.tsx +++ b/packages/core/src/components/date-picker/date-picker.tsx @@ -26,6 +26,10 @@ import { } from '@stencil/core'; import { DateTime, Info } from 'luxon'; import type { DateTimeCardCorners } from '../date-time-card/date-time-card.types'; +import type { + KeyboardNavigationBoundaryContext, + KeyboardNavigationBoundaryDirection, +} from '../dropdown/dropdown-focus'; import { queryElements } from '../utils/focus/focus-utilities'; import { DefaultMixins } from '../utils/internal/component'; import { makeRef } from '../utils/make-ref'; @@ -263,10 +267,14 @@ export class DatePicker private readonly yearDropdownButtonRef = makeRef(); + private readonly monthDropdownButtonRef = + makeRef(); + private readonly yearMonthSelectionDropdownRef = makeRef(); @State() dayNames!: string[]; + @State() dayNamesFull!: string[]; @State() monthNames!: string[]; @State() focusedDay: number = 1; @@ -489,18 +497,56 @@ export class DatePicker } private setTranslations() { - this.dayNames = this.rotateWeekDayNames( + const shortDayNames = this.rotateWeekDayNames( + Info.weekdays('short', { + locale: this.locale, + }), + this.weekStartIndex + ); + + const narrowDayNames = this.rotateWeekDayNames( + Info.weekdays('narrow', { + locale: this.locale, + }), + this.weekStartIndex + ); + + this.dayNamesFull = this.rotateWeekDayNames( Info.weekdays('long', { locale: this.locale, }), this.weekStartIndex ); + this.dayNames = this.useNarrowWeekdayLabels(shortDayNames) + ? narrowDayNames + : shortDayNames; + this.monthNames = Info.months('long', { locale: this.locale, }); } + private useNarrowWeekdayLabels(weekdays: string[]): boolean { + const maxGraphemeLength = 6; + + return weekdays.some( + (weekday) => this.getGraphemeLength(weekday) > maxGraphemeLength + ); + } + + private getGraphemeLength(value: string): number { + if (Intl.Segmenter === undefined) { + return [...value].length; + } + + const segmenter = new Intl.Segmenter(this.locale, { + granularity: 'grapheme', + }); + + return [...segmenter.segment(value)].length; + } + /** * Rotate the WeekdayNames array. * Based on the position that should be the new 0-index. @@ -524,86 +570,47 @@ export class DatePicker this.dateSelect.emit(date); } + private getWeekdayIndex(date: DateTime) { + const weekStartIndex = + ((this.weekStartIndex % this.DAYS_IN_WEEK) + this.DAYS_IN_WEEK) % + this.DAYS_IN_WEEK; + + return ( + (date.weekday - 1 - weekStartIndex + this.DAYS_IN_WEEK) % + this.DAYS_IN_WEEK + ); + } + private calculateCalendar() { const calendar: CalendarWeek[] = []; const month = DateTime.utc(this.selectedYear, this.selectedMonth + 1); const monthStart = month.startOf('month'); - const monthEnd = month.endOf('month'); - let startWeek = monthStart.weekNumber; - let endWeek = monthEnd.weekNumber; - let monthStartWeekDayIndex = monthStart.weekday - 1; - let monthEndWeekDayIndex = monthEnd.weekday - 1; - - if (this.weekStartIndex !== 0) { - // Find the positions where to start/stop counting the day-numbers based on which day the week starts - const weekdays = Info.weekdays(); - const monthStartWeekDayName = weekdays[monthStart.weekday]; - - monthStartWeekDayIndex = this.dayNames.findIndex( - (d) => d === monthStartWeekDayName - ); - const monthEndWeekDayName = weekdays[monthEnd.weekday]; - monthEndWeekDayIndex = this.dayNames.findIndex( - (d) => d === monthEndWeekDayName - ); - } - - let correctLastWeek = false; - if (endWeek === 1) { - endWeek = monthEnd.weeksInWeekYear + 1; - correctLastWeek = true; - } - - let correctFirstWeek = false; - if (startWeek === monthStart.weeksInWeekYear) { - startWeek = 1; - endWeek++; - - correctFirstWeek = true; - } + const daysInMonth = month.daysInMonth ?? 0; + const monthStartWeekDayIndex = this.getWeekdayIndex(monthStart); + const weekCount = Math.ceil( + (monthStartWeekDayIndex + daysInMonth) / this.DAYS_IN_WEEK + ); - let currDayNumber = 1; - for ( - let weekIndex = startWeek; - weekIndex <= endWeek && currDayNumber <= 31; - weekIndex++ - ) { + for (let weekIndex = 0; weekIndex < weekCount; weekIndex++) { const daysArr: (number | undefined)[] = []; - for (let j = 0; j < this.DAYS_IN_WEEK && currDayNumber <= 31; j++) { - // Display empty cells until the calender starts/has ended - if ( - (weekIndex === startWeek && j < monthStartWeekDayIndex) || - (weekIndex === endWeek && j > monthEndWeekDayIndex) - ) { - daysArr.push(undefined); - } else { - daysArr.push(currDayNumber++); - } - } + for (let dayIndex = 0; dayIndex < this.DAYS_IN_WEEK; dayIndex++) { + const dayNumber = + weekIndex * this.DAYS_IN_WEEK + dayIndex - monthStartWeekDayIndex + 1; - if (correctFirstWeek || correctLastWeek) { - if (weekIndex === 1) { - calendar.push({ - weekNumber: monthStart.weeksInWeekYear, - dayNumbers: daysArr, - }); - } else if (weekIndex === monthEnd.weekNumber) { - calendar.push({ - weekNumber: 1, - dayNumbers: daysArr, - }); - } else { - calendar.push({ - weekNumber: weekIndex - 1, - dayNumbers: daysArr, - }); - } - continue; + daysArr.push( + dayNumber < 1 || dayNumber > daysInMonth ? undefined : dayNumber + ); } + const firstDayOfWeek = daysArr.find((day) => day !== undefined) ?? 1; + calendar.push({ - weekNumber: weekIndex, + weekNumber: DateTime.utc( + this.selectedYear, + this.selectedMonth + 1, + firstDayOfWeek + ).weekNumber, dayNumbers: daysArr, }); } @@ -908,6 +915,62 @@ export class DatePicker }); } + private findYearDropdownItem(container: HTMLElement, year: number) { + return queryElements(container, 'ix-dropdown-item').find( + (item) => item.textContent?.trim() === `${year}` + ) as HTMLElement | undefined; + } + + private scrollToSelectedDropdownItem(dropdownElement: HTMLElement) { + const selectedItem = dropdownElement.querySelector( + 'ix-dropdown-item[checked]' + ) as HTMLElement; + + if (!selectedItem) { + return; + } + + requestAnimationFrameNoNgZone(() => { + selectedItem.scrollIntoView({ + block: 'center', + }); + }); + } + + private async waitForNextAnimationFrame() { + await new Promise((resolve) => { + requestAnimationFrameNoNgZone(() => resolve()); + }); + } + + private readonly onYearDropdownBoundaryFocus = async ({ + direction, + }: KeyboardNavigationBoundaryContext) => { + const container = this.yearDropdownButtonRef.current; + + if (!container) { + return undefined; + } + + const targetYear = this.getBoundaryTargetYear(direction); + + if (direction === 'next') { + this.endYear += 5; + } else { + this.startYear -= 5; + } + + // Wait for the updated year range to render before resolving next focus target. + await this.waitForNextAnimationFrame(); + return this.findYearDropdownItem(container, targetYear); + }; + + private getBoundaryTargetYear( + direction: KeyboardNavigationBoundaryDirection + ) { + return direction === 'next' ? this.endYear + 1 : this.startYear - 1; + } + private skipFirstScrollOffset = true; private intersectStart = new IntersectionObserver( (entries) => this.intersect(entries), @@ -944,9 +1007,18 @@ export class DatePicker aria-label={this.ariaLabelMonthSelection} variant="tertiary" label={null} + ref={this.monthDropdownButtonRef} onShowChanged={(event) => { // Need to stop event propagation to trigger initial focus handling of the calendar days event.stopPropagation(); + + if (event.detail) { + requestAnimationFrameNoNgZone(() => { + this.scrollToSelectedDropdownItem( + this.monthDropdownButtonRef.current! + ); + }); + } }} > @@ -958,6 +1030,8 @@ export class DatePicker { - selectedYearItem.scrollIntoView({ - block: 'center', - }); - }); + this.scrollToSelectedDropdownItem( + this.yearDropdownButtonRef.current! + ); }); } else { this.intersectStart.disconnect(); @@ -1030,13 +1093,15 @@ export class DatePicker {this.showWeekNumbers && (
)} - {this.dayNames.map((name) => ( + {this.dayNames.map((name, index) => (
-
{name.slice(0, 3)}
+ {name}
))} diff --git a/packages/core/src/components/date-picker/test/date-picker.ct.ts b/packages/core/src/components/date-picker/test/date-picker.ct.ts index 36329a0d5a4..aa743fcf826 100644 --- a/packages/core/src/components/date-picker/test/date-picker.ct.ts +++ b/packages/core/src/components/date-picker/test/date-picker.ct.ts @@ -16,6 +16,106 @@ const getDateObj = async (page: Page) => { }); }; +const getCalendarRows = async (page: Page) => { + return page.locator('[role="grid"] [role="row"]').evaluateAll((rows) => + rows.slice(1).map((row) => + Array.from(row.querySelectorAll('[role="gridcell"]')).map((cell) => { + const day = (cell as HTMLElement).dataset.calendarDay; + return day ? Number(day) : undefined; + }) + ) + ); +}; + +type CalendarLayoutTestState = { + name: string; + from: string; + weekStartIndex?: number; + expectedRows: (number | undefined)[][]; +}; + +const calendarLayoutTestStates: CalendarLayoutTestState[] = [ + { + name: 'default week start', + from: '2023/08/01', + expectedRows: [ + [undefined, 1, 2, 3, 4, 5, 6], + [7, 8, 9, 10, 11, 12, 13], + [14, 15, 16, 17, 18, 19, 20], + [21, 22, 23, 24, 25, 26, 27], + [28, 29, 30, 31, undefined, undefined, undefined], + ], + }, + { + name: 'custom week start', + from: '2023/08/01', + weekStartIndex: 2, + expectedRows: [ + [undefined, undefined, undefined, undefined, undefined, undefined, 1], + [2, 3, 4, 5, 6, 7, 8], + [9, 10, 11, 12, 13, 14, 15], + [16, 17, 18, 19, 20, 21, 22], + [23, 24, 25, 26, 27, 28, 29], + [30, 31, undefined, undefined, undefined, undefined, undefined], + ], + }, + { + name: 'four-row non-leap February', + from: '2021/02/01', + expectedRows: [ + [1, 2, 3, 4, 5, 6, 7], + [8, 9, 10, 11, 12, 13, 14], + [15, 16, 17, 18, 19, 20, 21], + [22, 23, 24, 25, 26, 27, 28], + ], + }, + { + name: 'leap February', + from: '2024/02/01', + expectedRows: [ + [undefined, undefined, undefined, 1, 2, 3, 4], + [5, 6, 7, 8, 9, 10, 11], + [12, 13, 14, 15, 16, 17, 18], + [19, 20, 21, 22, 23, 24, 25], + [26, 27, 28, 29, undefined, undefined, undefined], + ], + }, + { + name: 'six-row month starting in the last column', + from: '2026/03/01', + expectedRows: [ + [undefined, undefined, undefined, undefined, undefined, undefined, 1], + [2, 3, 4, 5, 6, 7, 8], + [9, 10, 11, 12, 13, 14, 15], + [16, 17, 18, 19, 20, 21, 22], + [23, 24, 25, 26, 27, 28, 29], + [30, 31, undefined, undefined, undefined, undefined, undefined], + ], + }, + { + name: 'negative week start wrapping to the last column', + from: '2023/08/01', + weekStartIndex: -1, + expectedRows: [ + [undefined, undefined, 1, 2, 3, 4, 5], + [6, 7, 8, 9, 10, 11, 12], + [13, 14, 15, 16, 17, 18, 19], + [20, 21, 22, 23, 24, 25, 26], + [27, 28, 29, 30, 31, undefined, undefined], + ], + }, +]; + +const getDatePickerMarkup = ({ + from, + weekStartIndex, +}: CalendarLayoutTestState) => { + const weekStartIndexAttribute = + weekStartIndex !== undefined ? ` week-start-index="${weekStartIndex}"` : ''; + + return ``; +}; + regressionTest('renders', async ({ mount, page }) => { await mount(``); const datePicker = page.locator(DatePickerSelector); @@ -31,6 +131,21 @@ regressionTest('translation', async ({ mount, page }) => { await expect(header).toHaveCount(1); }); +regressionTest.describe('calendar layout', () => { + for (const testState of calendarLayoutTestStates) { + regressionTest( + `renders accurate rows with ${testState.name}`, + async ({ mount, page }) => { + await mount(getDatePickerMarkup(testState)); + + const datePicker = page.locator(DatePickerSelector); + await expect(datePicker).toHaveClass(/\bhydrated\b/); + expect(await getCalendarRows(page)).toEqual(testState.expectedRows); + } + ); + } +}); + regressionTest.describe('date picker tests single', () => { regressionTest.beforeEach(async ({ mount }) => { await mount( @@ -160,6 +275,30 @@ regressionTest.describe('date picker tests single', () => { } ); + regressionTest( + 'ArrowDown on the last year extends the list without wrapping', + async ({ page }) => { + await page.waitForSelector('ix-date-time-card'); + + const yearSelection = page.getByLabel('Select year'); + const activeYear = yearSelection.locator('ix-dropdown-item.ix-focused'); + + await yearSelection.focus(); + await page.keyboard.press('Enter'); + + await page.keyboard.press('End'); + await expect + .poll(async () => (await activeYear.textContent())?.trim()) + .toBe('2124'); + + await page.keyboard.press('ArrowDown'); + + await expect + .poll(async () => (await activeYear.textContent())?.trim()) + .toBe('2125'); + } + ); + regressionTest( 'select different date fires dateChange event', async ({ page }) => { diff --git a/packages/core/src/components/dropdown-button/dropdown-button.tsx b/packages/core/src/components/dropdown-button/dropdown-button.tsx index 7cf27901c11..fe8448843ce 100644 --- a/packages/core/src/components/dropdown-button/dropdown-button.tsx +++ b/packages/core/src/components/dropdown-button/dropdown-button.tsx @@ -42,6 +42,7 @@ import { ComponentIdMixinContract, } from '../utils/internal/mixins/id.mixin'; import { closestPassShadow } from '../utils/shadow-dom'; +import type { KeyboardNavigationBoundaryContext } from '../dropdown/dropdown-focus'; @Component({ tag: 'ix-dropdown-button', @@ -101,6 +102,14 @@ export class DropdownButton */ @Prop() focusCheckedItem: boolean = false; + /** @internal */ + @Prop() disableWrapFocusNavigation = false; + + /** @internal */ + @Prop() onBoundaryFocus?: ( + context: KeyboardNavigationBoundaryContext + ) => Promise; + /** * Enable Popover API rendering for dropdown. * @@ -305,6 +314,8 @@ export class DropdownButton enableTopLayer={this.enableTopLayer} disableFocusTrap={true} focusCheckedItem={this.focusCheckedItem} + disableWrapFocusNavigation={this.disableWrapFocusNavigation} + onBoundaryFocus={this.onBoundaryFocus} onShowChanged={(event) => this.onDropdownShowChanged(event)} onScroll={(event) => { // Need to dispatch the event again to handle infinite scroll of ix-date-picker, diff --git a/packages/core/src/components/dropdown/dropdown-focus.ts b/packages/core/src/components/dropdown/dropdown-focus.ts index afc619a5021..fd2ce906052 100644 --- a/packages/core/src/components/dropdown/dropdown-focus.ts +++ b/packages/core/src/components/dropdown/dropdown-focus.ts @@ -36,6 +36,14 @@ export const QUERY_CURRENT_VISIBLE_FOCUS = VALID_FOCUS_ELEMENTS.map( `${selector}.${IX_FOCUS_VISIBLE_ACTIVE}:not([tabindex^="-"]):not([disabled]):not([hidden])` ).join(', '); +export type KeyboardNavigationBoundaryDirection = 'next' | 'previous'; + +export interface KeyboardNavigationBoundaryContext { + direction: KeyboardNavigationBoundaryDirection; + items: HTMLElement[]; + activeElement: HTMLElement | null; +} + export const getIndexOfDropdownItem = ( items: HTMLElement[], item: HTMLElement | null, @@ -104,6 +112,10 @@ export const configureKeyboardInteraction = ( querySelector?: string; activeQuerySelector?: string; itemTriggerKeys?: string[]; + wrapNavigation?: boolean; + onBoundaryFocus?: ( + context: KeyboardNavigationBoundaryContext + ) => Promise; beforeKeydown?: (ev: KeyboardEvent) => void; onItemActivation?: ( event: KeyboardEvent, @@ -120,6 +132,7 @@ export const configureKeyboardInteraction = ( 'Enter', ' ', ]; + const wrapNavigation = options.wrapNavigation ?? true; const getActiveElement = options.getActiveElement ?? @@ -136,6 +149,39 @@ export const configureKeyboardInteraction = ( const getEventListenerTarget = options.getEventListenerTarget ?? (() => getItemsHost()); + const handleBoundaryFocus = async ( + direction: KeyboardNavigationBoundaryDirection, + items: HTMLElement[], + activeElement: HTMLElement | null + ) => { + const currentItemIndex = getIndexOfDropdownItem( + items, + activeElement, + activeQuerySelector + ); + const boundaryIndex = direction === 'next' ? items.length - 1 : 0; + + if ( + wrapNavigation || + items.length === 0 || + currentItemIndex !== boundaryIndex + ) { + return false; + } + + const boundaryItem = await options.onBoundaryFocus?.({ + direction, + items, + activeElement, + }); + + if (boundaryItem !== undefined) { + setItemActive(boundaryItem); + } + + return true; + }; + const callback = async (event: KeyboardEvent) => { const activeElement = getActiveElement(); let items: HTMLElement[] = []; @@ -195,6 +241,11 @@ export const configureKeyboardInteraction = ( // Disable movement/scroll with keyboard event.preventDefault(); + + if (await handleBoundaryFocus('next', items, activeElement)) { + break; + } + const nextItem = getNextFocusableDropdownItem( items, activeElement, @@ -214,6 +265,11 @@ export const configureKeyboardInteraction = ( } // Disable movement/scroll with keyboard event.preventDefault(); + + if (await handleBoundaryFocus('previous', items, activeElement)) { + break; + } + const prevItem = getPreviousFocusableItem( items, activeElement, diff --git a/packages/core/src/components/dropdown/dropdown.tsx b/packages/core/src/components/dropdown/dropdown.tsx index e11add48c04..40a4c8cf48b 100644 --- a/packages/core/src/components/dropdown/dropdown.tsx +++ b/packages/core/src/components/dropdown/dropdown.tsx @@ -61,7 +61,10 @@ import { DropdownInterface, hasDropdownItemWrapperImplemented, } from './dropdown-controller'; -import { configureKeyboardInteraction } from './dropdown-focus'; +import { + configureKeyboardInteraction, + type KeyboardNavigationBoundaryContext, +} from './dropdown-focus'; import { AlignedPlacement } from './placement'; let sequenceId = 0; @@ -176,6 +179,14 @@ export class Dropdown */ @Prop() keyboardItemTriggerKeys: string[] = ['Enter', ' ']; + /** @internal */ + @Prop() disableWrapFocusNavigation = false; + + /** @internal */ + @Prop() onBoundaryFocus?: ( + context: KeyboardNavigationBoundaryContext + ) => Promise; + /** * Move dropdown along main axis of alignment * @@ -638,6 +649,8 @@ export class Dropdown getEventListenerTarget: () => (this.triggerElement as HTMLElement) ?? (this.anchorElement as HTMLElement), + wrapNavigation: !this.disableWrapFocusNavigation, + onBoundaryFocus: this.onBoundaryFocus, onItemActivation: (event, activeElement) => { event.preventDefault(); activeElement?.click(); diff --git a/packages/react/src/components/components.server.ts b/packages/react/src/components/components.server.ts index ed52886b6f8..35506dc9a98 100644 --- a/packages/react/src/components/components.server.ts +++ b/packages/react/src/components/components.server.ts @@ -717,6 +717,7 @@ export const IxDropdown: StencilReactComponent = /*@__ 'placement', 'ariaLabelDropdownButton', 'focusCheckedItem', + 'disableWrapFocusNavigation', + 'onBoundaryFocus', 'enableTopLayer', 'suppressAriaActiveDescendant', 'showChange', diff --git a/packages/vue/src/components/ix-dropdown.ts b/packages/vue/src/components/ix-dropdown.ts index 875054416c5..f3ddfa5c57b 100644 --- a/packages/vue/src/components/ix-dropdown.ts +++ b/packages/vue/src/components/ix-dropdown.ts @@ -22,6 +22,8 @@ export const IxDropdown: StencilVueComponent = /*@__PURE__*/ def 'focusCheckedItem', 'keyboardActivationKeys', 'keyboardItemTriggerKeys', + 'disableWrapFocusNavigation', + 'onBoundaryFocus', 'offset', 'overwriteDropdownStyle', 'discoverAllSubmenus', diff --git a/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-locales-1-chromium---classic-dark-linux.png b/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-locales-1-chromium---classic-dark-linux.png index db9387ed952..7e80ccf223c 100644 Binary files a/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-locales-1-chromium---classic-dark-linux.png and b/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-locales-1-chromium---classic-dark-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-locales-1-chromium---classic-light-linux.png b/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-locales-1-chromium---classic-light-linux.png index 15bc937a1b6..c49d0e3f699 100644 Binary files a/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-locales-1-chromium---classic-light-linux.png and b/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-locales-1-chromium---classic-light-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-week-start-1-chromium---classic-dark-linux.png b/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-week-start-1-chromium---classic-dark-linux.png index 59728225d5d..fa26f9afab4 100644 Binary files a/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-week-start-1-chromium---classic-dark-linux.png and b/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-week-start-1-chromium---classic-dark-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-week-start-1-chromium---classic-light-linux.png b/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-week-start-1-chromium---classic-light-linux.png index 245ab17ef66..6dade2dad86 100644 Binary files a/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-week-start-1-chromium---classic-light-linux.png and b/testing/visual-testing/__screenshots__/tests/date-picker/date-picker.e2e.ts/date-picker-week-start-1-chromium---classic-light-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-basic-1-chromium---classic-dark-linux.png b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-basic-1-chromium---classic-dark-linux.png index bccfbe14e13..67833e5bf3d 100644 Binary files a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-basic-1-chromium---classic-dark-linux.png and b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-basic-1-chromium---classic-dark-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-basic-1-chromium---classic-light-linux.png b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-basic-1-chromium---classic-light-linux.png index 7955526805c..3e5f2565b3d 100644 Binary files a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-basic-1-chromium---classic-light-linux.png and b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-basic-1-chromium---classic-light-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-mobile-1-chromium---classic-dark-linux.png b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-mobile-1-chromium---classic-dark-linux.png index 9eeeb4dcf51..df98b2b4640 100644 Binary files a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-mobile-1-chromium---classic-dark-linux.png and b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-mobile-1-chromium---classic-dark-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-mobile-1-chromium---classic-light-linux.png b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-mobile-1-chromium---classic-light-linux.png index ce7fd2d7c32..7b7e7dcb0c0 100644 Binary files a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-mobile-1-chromium---classic-light-linux.png and b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-mobile-1-chromium---classic-light-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-show-week-numbers-1-chromium---classic-dark-linux.png b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-show-week-numbers-1-chromium---classic-dark-linux.png index 7591ea476ed..2aa4d12bb32 100644 Binary files a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-show-week-numbers-1-chromium---classic-dark-linux.png and b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-show-week-numbers-1-chromium---classic-dark-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-show-week-numbers-1-chromium---classic-light-linux.png b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-show-week-numbers-1-chromium---classic-light-linux.png index 1d532f681ad..4ab7946c7fc 100644 Binary files a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-show-week-numbers-1-chromium---classic-light-linux.png and b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-show-week-numbers-1-chromium---classic-light-linux.png differ