diff --git a/CHANGELOG.md b/CHANGELOG.md index 152d426838..44c1838e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ Legends: - Fixed screen readers announcing a single audio, video, or animation in a card as a list item, such as "1 of 1", fixes [#5722](https://github.com/microsoft/BotFramework-WebChat/issues/5722), in PR [#5903](https://github.com/microsoft/BotFramework-WebChat/pull/5903), by [@OEvgeny](https://github.com/OEvgeny) - Fixed video and animation cards disappearing in production builds when the video URL is relative or invalid, in PR [#5889](https://github.com/microsoft/BotFramework-WebChat/pull/5889), by [@OEvgeny](https://github.com/OEvgeny) - 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 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) ## [4.19.2] - 2026-10-08 diff --git a/__tests__/html2/carousel/navigation.arrowKeys.formControls.html b/__tests__/html2/carousel/navigation.arrowKeys.formControls.html index 531e140f5c..a9bdc4962e 100644 --- a/__tests__/html2/carousel/navigation.arrowKeys.formControls.html +++ b/__tests__/html2/carousel/navigation.arrowKeys.formControls.html @@ -217,14 +217,18 @@ // WHEN: Press TAB twice to focus on the button inside the hero card. await host.sendTab(2); - // THEN: The button inside the hero card should have focus. + // THEN: The button inside the hero card should have focus, TAB into the hero card scrolled it into view. expect(document.activeElement).toBe(heroCardButton); + await waitFor(() => expect(isCardInView(cards[1])).toBe(true)); + await waitForCarouselSettled(); // WHEN: Press LEFT and RIGHT on the button inside the hero card. // THEN: The button inside the hero card should keep focus. await host.sendKeys('ARROW_LEFT'); expect(document.activeElement).toBe(heroCardButton); - expect(carouselFilmstrip.scrollLeft).toBe(0); + + await waitForCarouselScrolled(); + expect(document.activeElement).toBe(heroCardButton); await host.sendKeys('ARROW_RIGHT'); expect(document.activeElement).toBe(heroCardButton); diff --git a/__tests__/html2/carousel/navigation.tab.scrollIntoView.html b/__tests__/html2/carousel/navigation.tab.scrollIntoView.html new file mode 100644 index 0000000000..48636ec437 --- /dev/null +++ b/__tests__/html2/carousel/navigation.tab.scrollIntoView.html @@ -0,0 +1,137 @@ + + +
+ + + + + + + + diff --git a/__tests__/html2/carousel/navigation.tab.scrollIntoView.html.snap-1.png b/__tests__/html2/carousel/navigation.tab.scrollIntoView.html.snap-1.png new file mode 100644 index 0000000000..72bc349433 Binary files /dev/null and b/__tests__/html2/carousel/navigation.tab.scrollIntoView.html.snap-1.png differ diff --git a/__tests__/html2/carousel/navigation.tab.scrollIntoView.html.snap-2.png b/__tests__/html2/carousel/navigation.tab.scrollIntoView.html.snap-2.png new file mode 100644 index 0000000000..78f788ada0 Binary files /dev/null and b/__tests__/html2/carousel/navigation.tab.scrollIntoView.html.snap-2.png differ diff --git a/__tests__/html2/carousel/navigation.tab.scrollIntoView.html.snap-3.png b/__tests__/html2/carousel/navigation.tab.scrollIntoView.html.snap-3.png new file mode 100644 index 0000000000..cdb27a056d Binary files /dev/null and b/__tests__/html2/carousel/navigation.tab.scrollIntoView.html.snap-3.png differ diff --git a/__tests__/html2/carousel/navigation.tab.scrollIntoView.html.snap-4.png b/__tests__/html2/carousel/navigation.tab.scrollIntoView.html.snap-4.png new file mode 100644 index 0000000000..78f788ada0 Binary files /dev/null and b/__tests__/html2/carousel/navigation.tab.scrollIntoView.html.snap-4.png differ diff --git a/__tests__/html2/carousel/navigation.tab.scrollIntoView.pointer.html b/__tests__/html2/carousel/navigation.tab.scrollIntoView.pointer.html new file mode 100644 index 0000000000..56c9d401e2 --- /dev/null +++ b/__tests__/html2/carousel/navigation.tab.scrollIntoView.pointer.html @@ -0,0 +1,112 @@ + + + + + + + + + + + diff --git a/__tests__/html2/carousel/navigation.tab.scrollIntoView.pointer.html.snap-1.png b/__tests__/html2/carousel/navigation.tab.scrollIntoView.pointer.html.snap-1.png new file mode 100644 index 0000000000..bb3dbae63c Binary files /dev/null and b/__tests__/html2/carousel/navigation.tab.scrollIntoView.pointer.html.snap-1.png differ diff --git a/packages/component/src/Activity/CarouselFilmStrip.js b/packages/component/src/Activity/CarouselFilmStrip.js index 306cdee62c..b25f7ba052 100644 --- a/packages/component/src/Activity/CarouselFilmStrip.js +++ b/packages/component/src/Activity/CarouselFilmStrip.js @@ -38,6 +38,20 @@ const CarouselFilmStrip = ({ const scrollableCallbackRef = useScrollableCallbackRef(); const scrollTo = useScrollTo(); + // When the keyboard moves the focus into a card from outside of it (TAB or SHIFT-TAB), center the card with react-film, + // as LEFT/RIGHT do, instead of leaving it partially visible. Clicking a card or button is not :focus-visible and does not + // scroll; text inputs are, so the card is centered for typing. + const handleAttachmentsFocus = useCallback( + ({ currentTarget, relatedTarget, target }) => { + const cards = Array.from(currentTarget.children); + + const index = cards.findIndex(card => card.contains(target)); + + ~index && !cards[+index].contains(relatedTarget) && target.matches(':focus-visible') && scrollTo(() => index); + }, + [scrollTo] + ); + // When a card itself has focus (not a control inside it), LEFT/RIGHT move the focus to the previous/next card // (mirrored in RTL), HOME/END to the first/last card. This is a shortcut: every card is still a tab stop. const handleAttachmentsKeyDown = useCallback( @@ -167,6 +181,7 @@ const CarouselFilmStrip = ({