Skip to content

feat: carousel tabs mode#167

Open
danish17 wants to merge 15 commits into
developfrom
feat/carousel-as-tabs
Open

feat: carousel tabs mode#167
danish17 wants to merge 15 commits into
developfrom
feat/carousel-as-tabs

Conversation

@danish17

@danish17 danish17 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a tabs mode to the carousel block. Toggling Use as Tabs in the carousel inspector switches slides from a swipeable carousel to a tab panel: each slide becomes a tab panel, a carousel-tab-list block renders the tab navigation, and selection replaces scroll.

  • useTabs attribute + carousel save emits role="region" with data-is-tabs="true".
  • New rt-carousel/carousel-tab-list block renders WYSIWYG tab labels with inline colour/border controls. Save markup uses <button role="tab"> with roving tabindex, aria-controls, and aria-selected.
  • Slide save adds role="tabpanel" + aria-labelledby in tabs mode.
  • Interactivity store gains onTabKeydown (WAI-ARIA tabs keyboard pattern: Home/End/Arrows with wrap), getTabTabIndex, getTabId, getTabAriaControls, getKeyFeatureDotText.
  • Editor: clicking a tab syncs the active slide in the editor via EditorCarouselContext.setSelectedIndex. Recursive ancestor lookup (getBlockParentsByBlockName + findBlockDeep) lets tab-list/viewport live inside core/column / core/group for side-by-side vertical-tabs layouts.
  • _core.scss stacks inactive slides via :has(.embla__slide.is-active) so one panel is visible at a time without runtime is-hidden classes.
  • data-wp-context on carousel and tab-list save is now HTML-attribute encoded (encodeContext) to survive <, >, & in user-edited strings.

Test plan

  • npm run lint:js && npm run lint:js:types && npm run lint:css && npm run build && npm run test:js — all green (157 tests).
  • Editor: toggle Use as Tabs, rename a tab via the WYSIWYG input — focus must be retained across keystrokes.
  • Editor: vertical orientation renders tabs at uniform width, matching the frontend.
  • Editor: drop a core/columns block inside a carousel, then place carousel-tab-list in one column and carousel-viewport in another — tabs still report correct slide count and selecting a tab updates the visible slide.
  • Frontend: keyboard-navigate tabs with Arrow/Home/End; roving tabindex keeps only the active tab in tab order.
  • Frontend: ARIA label containing & or < does not break data-wp-context parsing.

danish17 added 14 commits July 18, 2026 02:22
- Toggle in inspector disables carousel-specific controls when enabled
- Auto-inserts carousel-tab-list block above viewport on toggle ON
- Passes duration:0 via carouselOptions for instant tab switching
- Passes useTabs to EditorCarouselContext for downstream consumers
- Adds data-is-tabs attribute to blockProps for WYSIWYG CSS hook
- viewport/edit.tsx: passes duration option through to Embla init
- slide/edit.tsx: derive slideIndex from block tree, apply is-active
  class when useTabs is true and this slide is selectedIndex
- editor.scss: hide non-.is-active slides under [data-is-tabs="true"]
  so editor matches frontend tabs behaviour
- slide/edit.tsx: add curly braces to if block (curly rule)
- view.ts: move context read after early-return guards to satisfy
  @wordpress/no-unused-vars-before-return in getSlideTabPanelId
  and getSlideTabLabelledBy
- carousel-tab-list/edit.tsx: add id props to BaseControl components
  to satisfy @wordpress/no-base-control-with-label-without-id
WPBlock recursive type replaces inline casts in carousel and tab-list
edit.tsx. findBlockDeep is a depth-first search used by editor code to
locate nested viewport/carousel ancestors when blocks are placed inside
layout containers.

encodeContext escapes JSON output for safe embedding in data-wp-context
HTML attributes. Shared between carousel and carousel-tab-list save.
Expand parent arrays to include core/column and core/group so users can
build side-by-side vertical-tabs layouts. Drops the dead 'deprecated'
block from carousel-tab-list/block.json (invalid 'saves' key, never
matched) and core/columns plural (direct parent of these blocks is
core/column, not the plural columns container).
Editor renders tabs as <button> (matches save), uses stable index-based
React key to avoid focus loss when typing, and resolves the closest
carousel ancestor via getBlockParentsByBlockName so dotCount stays
correct when nested through layout blocks.

Save markup applies alignItems: stretch + flexWrap: nowrap in vertical
mode so tabs render at uniform width on the frontend, matching the
editor (WP .is-layout-flex class defaults to align-items: flex-start).

Store (view.ts) adds WAI-ARIA tabs keyboard navigation (Home/End/Arrow
keys with wrap) and roving tabindex. data-wp-context is HTML-attribute
encoded via the shared encodeContext util.

Tests cover onTabKeydown (9), getTabTabIndex (2), getKeyFeatureDotText
(3), getTabId/getTabAriaControls (2), resolveWpValue (6).
Carousel edit drops the 'as any' block-editor select cast in favour of
the typed BlockEditorSelectors interface and uses findBlockDeep to
locate the viewport when it is nested through columns/group.

Carousel save HTML-encodes its data-wp-context via the shared
encodeContext util (was raw JSON.stringify — latent parse break if an
ariaLabel or announcementPattern contained <, >, or &).

Slide save adds tabpanel role + aria-labelledby bindings in tabs mode.
Slide index registers __experimentalLabel for cleaner block titles.
Editor context exposes setSelectedIndex; viewport edit uses it to
sync the active slide when the user clicks a tab in the editor.

_core.scss stacks inactive slides via :has(.embla__slide.is-active)
so tabs mode shows one panel at a time without runtime is-hidden
classes.

Tests: append findBlockDeep coverage to types.test.ts; remove the
deleted isSlideHiddenForTabs callback tests from view.test.ts; mock
setSelectedIndex in viewport-edit.test.tsx.
@danish17
danish17 requested a review from milindmore22 July 20, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant