diff --git a/CHANGELOG.md b/CHANGELOG.md index dbb78fc07c..ff95eb4dc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ Legends: - Fixed timestamps always using the 12-hour clock, such as "4:41 PM" instead of "16:41" in German, in PR [#5920](https://github.com/microsoft/BotFramework-WebChat/pull/5920), by [@OEvgeny](https://github.com/OEvgeny) - Fixed screen readers not saying "Click to interact." for messages with links, in PR [#5927](https://github.com/microsoft/BotFramework-WebChat/pull/5927), by [@OEvgeny](https://github.com/OEvgeny) - Fixed carousel cards focused with TAB staying partially out of view, in PR [#5926](https://github.com/microsoft/BotFramework-WebChat/pull/5926), by [@OEvgeny](https://github.com/OEvgeny) +- Fixed LEFT/RIGHT on a button inside a carousel card scrolling the carousel, in PR [#5917](https://github.com/microsoft/BotFramework-WebChat/pull/5917), by [@OEvgeny](https://github.com/OEvgeny) ## [4.19.2] - 2026-10-08 diff --git a/__tests__/html2/carousel/navigation.arrowKeys.button.html b/__tests__/html2/carousel/navigation.arrowKeys.button.html new file mode 100644 index 0000000000..6000b3291f --- /dev/null +++ b/__tests__/html2/carousel/navigation.arrowKeys.button.html @@ -0,0 +1,236 @@ + + +
+ + + + + + + + diff --git a/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-1.png b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-1.png new file mode 100644 index 0000000000..72bc349433 Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-1.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-2.png b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-2.png new file mode 100644 index 0000000000..e9d9a6143b Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-2.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-3.png b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-3.png new file mode 100644 index 0000000000..ffda44b6c8 Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-3.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-4.png b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-4.png new file mode 100644 index 0000000000..ffda44b6c8 Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-4.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-5.png b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-5.png new file mode 100644 index 0000000000..ffda44b6c8 Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-5.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-6.png b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-6.png new file mode 100644 index 0000000000..e9d9a6143b Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-6.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-7.png b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-7.png new file mode 100644 index 0000000000..010be3ad8d Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-7.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-8.png b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-8.png new file mode 100644 index 0000000000..7ecd65c043 Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-8.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-9.png b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-9.png new file mode 100644 index 0000000000..7ecd65c043 Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.button.html.snap-9.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-4.png b/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-4.png index 57d1f8663a..1b4368cee6 100644 Binary files a/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-4.png and b/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-4.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-5.png b/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-5.png new file mode 100644 index 0000000000..f1eeba2058 Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-5.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-6.png b/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-6.png new file mode 100644 index 0000000000..f1eeba2058 Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-6.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-7.png b/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-7.png new file mode 100644 index 0000000000..f1eeba2058 Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-7.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-8.png b/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-8.png new file mode 100644 index 0000000000..57d1f8663a Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.formControls.fluent.html.snap-8.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.formControls.html b/__tests__/html2/carousel/navigation.arrowKeys.formControls.html index a9bdc4962e..802b34abea 100644 --- a/__tests__/html2/carousel/navigation.arrowKeys.formControls.html +++ b/__tests__/html2/carousel/navigation.arrowKeys.formControls.html @@ -114,11 +114,6 @@ { timeout: 5000 } ); - const waitForCarouselScrolled = async () => { - await waitFor(() => expect(carouselLayout.querySelector('.react-film__main--scrolling')).not.toBeNull()); - await waitForCarouselSettled(); - }; - // Focus should be on the card itself, not on the form controls inside it. const expectFocusOnCard = async index => { expect(document.activeElement).toBe(cards[index]); @@ -208,33 +203,53 @@ await host.sendKeys('ARROW_RIGHT'); expect(document.activeElement).toBe(submitButton); - // RIGHT on a button keeps the browser default of scrolling the carousel, wait for the scroll to start and settle. - await waitForCarouselScrolled(); - expect(document.activeElement).toBe(submitButton); + // RIGHT on a button does not scroll the carousel. + await waitForCarouselSettled(); + expect(carouselFilmstrip.scrollLeft).toBe(0); await host.snapshot('local'); - // WHEN: Press TAB twice to focus on the button inside the hero card. - await host.sendTab(2); + // WHEN: Press TAB to focus on the partially visible hero card. + await host.sendTab(); + + // THEN: The hero card should have focus and be scrolled fully into view. + await expectFocusOnCard(1); + + const heroCardScrollLeft = carouselFilmstrip.scrollLeft; + + expect(heroCardScrollLeft).toBeGreaterThan(0); + + await host.snapshot('local'); - // THEN: The button inside the hero card should have focus, TAB into the hero card scrolled it into view. + // WHEN: Press TAB to focus on the button inside the hero card. + await host.sendTab(); + + // THEN: The button inside the hero card should have focus, the carousel should not scroll. expect(document.activeElement).toBe(heroCardButton); - await waitFor(() => expect(isCardInView(cards[1])).toBe(true)); await waitForCarouselSettled(); + expect(carouselFilmstrip.scrollLeft).toBe(heroCardScrollLeft); + + await host.snapshot('local'); - // WHEN: Press LEFT and RIGHT on the button inside the hero card. - // THEN: The button inside the hero card should keep focus. + // WHEN: Press LEFT on the button inside the hero card. await host.sendKeys('ARROW_LEFT'); - expect(document.activeElement).toBe(heroCardButton); - await waitForCarouselScrolled(); + // THEN: The button inside the hero card should keep focus, the carousel should not scroll. expect(document.activeElement).toBe(heroCardButton); + await waitForCarouselSettled(); + expect(carouselFilmstrip.scrollLeft).toBe(heroCardScrollLeft); + await host.snapshot('local'); + + // WHEN: Press RIGHT on the button inside the hero card. await host.sendKeys('ARROW_RIGHT'); - expect(document.activeElement).toBe(heroCardButton); - await waitForCarouselScrolled(); + // THEN: The button inside the hero card should keep focus, the carousel should not scroll. expect(document.activeElement).toBe(heroCardButton); + await waitForCarouselSettled(); + expect(carouselFilmstrip.scrollLeft).toBe(heroCardScrollLeft); + + await host.snapshot('local'); // WHEN: Press SHIFT-TAB to focus on the hero card, then press RIGHT. await host.sendShiftTab(); diff --git a/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-4.png b/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-4.png index d7403750de..734f2733d7 100644 Binary files a/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-4.png and b/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-4.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-5.png b/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-5.png new file mode 100644 index 0000000000..fa6edac77d Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-5.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-6.png b/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-6.png new file mode 100644 index 0000000000..fa6edac77d Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-6.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-7.png b/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-7.png new file mode 100644 index 0000000000..fa6edac77d Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-7.png differ diff --git a/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-8.png b/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-8.png new file mode 100644 index 0000000000..d7403750de Binary files /dev/null and b/__tests__/html2/carousel/navigation.arrowKeys.formControls.html.snap-8.png differ diff --git a/packages/component/src/Activity/CarouselFilmStrip.js b/packages/component/src/Activity/CarouselFilmStrip.js index b25f7ba052..5812240824 100644 --- a/packages/component/src/Activity/CarouselFilmStrip.js +++ b/packages/component/src/Activity/CarouselFilmStrip.js @@ -17,6 +17,12 @@ import styles from './CarouselFilmStrip.module.css'; const { useAvatarForBot, useAvatarForUser, useDirection, useLocalizer, useStyleOptions } = hooks; +// Controls without their own LEFT/RIGHT behavior. Inputs that use arrow keys (text, number, date, radio, range, select) +// and media are left out. Adaptive Cards render a `selectAction` as `[role="button"]` or, for Action.OpenUrl, +// `[role="link"]`. +const CONTROLS_WITHOUT_ARROW_KEYS_SELECTOR = + 'a[href], button, input[type="button"], input[type="checkbox"], input[type="reset"], input[type="submit"], [role="button"], [role="link"]'; + const CarouselFilmStrip = ({ activity, className, @@ -62,7 +68,17 @@ const CarouselFilmStrip = ({ const index = cards.indexOf(target); const rtl = direction === 'rtl'; - if (altKey || ctrlKey || metaKey || shiftKey || !~index) { + if (altKey || ctrlKey || metaKey || shiftKey) { + return; + } + + if (!~index) { + // On a button, link, or checkbox inside a card, LEFT/RIGHT do nothing. Otherwise, the browser would scroll the + // carousel and react-film would center the card again, or, on a held key, scroll the control out of view. + (key === 'ArrowLeft' || key === 'ArrowRight') && + target.matches(CONTROLS_WITHOUT_ARROW_KEYS_SELECTOR) && + event.preventDefault(); + return; }