diff --git a/src/blocks/carousel/carousel-tab-list/__tests__/view.test.ts b/src/blocks/carousel/carousel-tab-list/__tests__/view.test.ts index d1523ce..0ce4cf5 100644 --- a/src/blocks/carousel/carousel-tab-list/__tests__/view.test.ts +++ b/src/blocks/carousel/carousel-tab-list/__tests__/view.test.ts @@ -209,6 +209,29 @@ describe( 'getTabTabIndex (roving tabindex)', () => { ); expect( storeConfig!.callbacks.getTabTabIndex() ).toBe( '-1' ); } ); + + it( 'returns "0" for the first tab when selectedIndex is -1', () => { + ( getContext as jest.Mock ).mockReturnValue( + mockContext( { selectedIndex: -1, snap: { index: 0 } } ), + ); + expect( storeConfig!.callbacks.getTabTabIndex() ).toBe( '0' ); + } ); + + it( 'returns "0" for the first tab when selectedIndex is undefined (uninitialized)', () => { + ( getContext as jest.Mock ).mockReturnValue( + mockContext( { selectedIndex: undefined as unknown as number, snap: { index: 0 } } ), + ); + expect( storeConfig!.callbacks.getTabTabIndex() ).toBe( '0' ); + } ); + + it( 'returns "-1" when snap or snap.index is undefined', () => { + ( getContext as jest.Mock ).mockReturnValue( { + carouselId: 'c1', + selectedIndex: 0, + snap: undefined, + } ); + expect( storeConfig!.callbacks.getTabTabIndex() ).toBe( '-1' ); + } ); } ); describe( 'getKeyFeatureDotText', () => { diff --git a/src/blocks/carousel/carousel-tab-list/style.scss b/src/blocks/carousel/carousel-tab-list/style.scss index 1753b0a..ef29650 100644 --- a/src/blocks/carousel/carousel-tab-list/style.scss +++ b/src/blocks/carousel/carousel-tab-list/style.scss @@ -32,7 +32,9 @@ align-items: center; justify-content: center; padding: 0.5em 1em; - border: var(--rt-tab-border-width) var(--rt-tab-border-style) var(--rt-tab-border-color); + border-width: var(--rt-tab-border-width); + border-style: var(--rt-tab-border-style); + border-color: var(--rt-tab-border-color); background: var(--rt-tab-inactive-bg); color: var(--rt-tab-inactive-color); cursor: pointer; @@ -45,11 +47,9 @@ &:focus-visible { - /* Visible by default (WCAG 2.4.7). Use currentColor unconditionally - * because --rt-tab-border-color defaults to transparent. */ - outline-color: currentcolor; - outline-style: solid; - outline-width: 2px; + /* Visible by default (WCAG 2.4.7). High contrast outline for keyboard + * navigation. */ + outline: 2px solid var(--wp-admin-theme-color, #007cba); outline-offset: 2px; } @@ -61,9 +61,42 @@ } } +@media (max-width: 1024px) { + + .wp-block-rt-carousel-carousel-tab-list, + .wp-block-rt-carousel-carousel-tab-list.is-layout-flex { + max-width: 100%; + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.2) transparent; + padding-bottom: 4px; + + &::-webkit-scrollbar { + height: 4px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + border-radius: 2px; + } + + .wp-block-rt-carousel-carousel-tab-list__tab { + flex: 0 0 auto; + white-space: nowrap; + } + } +} + @media (prefers-reduced-motion: reduce) { .wp-block-rt-carousel-carousel-tab-list__tab { transition: none; } } + diff --git a/src/blocks/carousel/carousel-tab-list/view.ts b/src/blocks/carousel/carousel-tab-list/view.ts index 4da4c14..7fd58cc 100644 --- a/src/blocks/carousel/carousel-tab-list/view.ts +++ b/src/blocks/carousel/carousel-tab-list/view.ts @@ -94,8 +94,17 @@ store( 'rt-carousel/carousel', { * inactive tabs are focusable only via arrow keys (tabindex=-1). */ getTabTabIndex: (): string => { const context = getContext< TabContext >(); - const isActive = context.selectedIndex === context.snap?.index; + const snapIndex = context.snap?.index; + if ( typeof snapIndex !== 'number' ) { + return '-1'; + } + const selectedIndex = + typeof context.selectedIndex === 'number' && context.selectedIndex >= 0 + ? context.selectedIndex + : 0; + const isActive = selectedIndex === snapIndex; return isActive ? '0' : '-1'; }, }, } as CarouselStore ); + diff --git a/src/blocks/carousel/save.tsx b/src/blocks/carousel/save.tsx index 686eca1..e05edd6 100644 --- a/src/blocks/carousel/save.tsx +++ b/src/blocks/carousel/save.tsx @@ -56,7 +56,7 @@ export default function Save( { : false, isPlaying: !! autoplay, // Initially true if autoplay is enabled timerIterationId: 0, - selectedIndex: -1, + selectedIndex: 0, scrollSnaps: [], canScrollPrev: false, canScrollNext: false, diff --git a/src/blocks/carousel/view.ts b/src/blocks/carousel/view.ts index 0f93cf2..0aa7328 100644 --- a/src/blocks/carousel/view.ts +++ b/src/blocks/carousel/view.ts @@ -285,7 +285,14 @@ store( 'rt-carousel/carousel', { const { snap } = context as CarouselContext & { snap?: { index?: number }; }; - return context.selectedIndex === snap?.index; + if ( typeof snap?.index !== 'number' ) { + return false; + } + const selectedIndex = + typeof context.selectedIndex === 'number' && context.selectedIndex >= 0 + ? context.selectedIndex + : 0; + return selectedIndex === snap.index; }, getDotLabel: () => { const context = getContext();