feat(splitter): add resizable two-pane Splitter component#1560
Draft
misama-ct wants to merge 24 commits into
Draft
feat(splitter): add resizable two-pane Splitter component#1560misama-ct wants to merge 24 commits into
misama-ct wants to merge 24 commits into
Conversation
…and context management - Added WindowSplitter component with Root, Pane, and Separator subcomponents. - Implemented context for managing splitter state and behavior. - Created associated types and recipes for styling. - Added stories for showcasing various configurations and interactions.
- Moved the `useWindowSplitterContext` hook to a new file for better structure. - Updated exports in `window-splitter.tsx` to include subcomponents with clearer naming. - Removed unused context-related code from `window-splitter.context.tsx`.
- Introduced comprehensive documentation for the Window Splitter component, detailing its functionality, usage guidelines, best practices, and accessibility features. - Included examples for both horizontal and vertical orientations, as well as controlled and uncontrolled modes. - Added tips and cautions for optimal usage scenarios and potential pitfalls.
…ests - Updated WindowSplitter stories to include data-testid attributes for better testing. - Introduced comprehensive keyboard interaction tests for both horizontal and vertical splitters, ensuring accessibility compliance. - Refactored existing stories to improve clarity and maintainability. - Adjusted imports for context management in pane and separator components.
- Updated WindowSplitter stories to utilize Chakra UI components for better styling and consistency. - Refactored content in primary and secondary panes to use Heading, Text, and List components for improved readability. - Adjusted box dimensions and border styles for a more polished appearance across all stories. - Ensured that all interactive elements are accessible and visually aligned with design standards.
…ries - Updated WindowSplitter stories to replace userEvent.tab() with direct focus calls for better focus management. - Adjusted the WindowSplitterPane component to accept an explicit ID, enhancing accessibility and usability. - Ensured all panes have a tabIndex of 0 for keyboard navigation compliance.
…ibility - Enhanced WindowSplitter stories with updated dimensions, padding, and border styles for a more cohesive look. - Replaced static values with responsive design elements, ensuring better adaptability across different screen sizes. - Improved text hierarchy by adjusting Heading sizes and margins for better readability and visual appeal. - Ensured all components maintain accessibility standards while providing a polished user experience.
…tter components - Updated the window splitter recipe to improve layout with absolute positioning and z-index adjustments. - Changed background colors for better visibility and hover effects. - Enhanced the WindowSplitterSeparator component to dynamically calculate position based on orientation, improving usability and responsiveness.
Reshape the unmerged window-splitter proof of concept into a focused N-pane primitive with predictable cascading behaviour, persistence integration, and cross-app command support. App-shell layout concerns are explicitly deferred to a follow-up pattern component.
…ples - Rename window-splitter → splitter throughout: directory, files, identifiers, displayNames, MDX frontmatter, parent barrel export - Register nimbusSplitter in theme/slot-recipes; switch Root and slots from inline-recipe to key-based recipe lookup - Replace @chakra-ui/react barrel imports with @chakra-ui/react/styled-system subpath; pull mergeRefs from @/utils instead of Chakra - Fix module resolution: @/utils/extractStyleProps → @/utils; Storybook 9 imports (@storybook/react → @storybook/react-vite, @storybook/test → storybook/test) - Remove bare Root/Pane/Separator re-exports that collided with the existing Separator component at the package barrel; add underscore- prefixed docgen exports matching the Steps pattern - Convert empty slot interface declarations to type aliases - Demonstrate the ScrollArea composition pattern in stories and MDX examples for branded scrollbars; switch demo colors to indigo + amber for better hue contrast
…uration - Reshape Splitter from N-pane + cascade to 2-pane + nesting; justify the choice on accessibility (W3C window splitter is specified for a separator between two regions), responsive design (nesting maps to real layout hierarchy), and implementation cost (no cascade algorithm needed) - Switch per-pane configuration to a `panes: Record<string, PaneConfig>` map on Root keyed by pane id; Pane carries only `id` + content - Switch sizes to `Record<string, number>` so persistence is reorder-safe and `aria-controls` resolves to a DOM id without index math - Make Handle anonymous (no id, no per-handle config); Root carries `keyboardStep`, `disableDoubleClick`, default aria-label - Drop cascading resize from spec/tasks; constraints clamp at min/max - Add Nesting requirement with a 3-region-via-nesting scenario; rewrite hook reconciliation around id mismatch instead of array-length mismatch
…rLayout Replaces the WindowSplitter scaffold with the OpenSpec-aligned 2-pane primitive: id-keyed configuration via `panes` map on Root, uncontrolled state, anonymous `Splitter.Handle`, per-pane min/max with no-cascade clamping, collapsible panes via double-click and Enter, and a companion `useSplitterLayout` hook for SSR-safe persistence and cross-component imperative commands. - Rename `Splitter.Separator` -> `Splitter.Handle` (ARIA role stays `separator`) - Recipe: `.ts`, `_focusVisible` + layerStyle focusRing/disabled, design tokens - Four-layer types with OmitInternalProps; W3C ARIA model with per-pane bounds - New `clampedResize` utility + 13 unit tests; 9 hook tests; docs.spec tests - Stories rewritten for new API with play functions covering all spec scenarios - New splitter.dev.mdx, splitter.a11y.mdx, splitter.docs.spec.tsx - i18n: resizePanes default aria-label localised via shared pipeline
…lapse toggle Decouples the mouse gesture from collapsibility. Double-click on the handle now returns the boundary to the sizes resolved on mount (from `defaultSizes` or `panes[id].defaultSize`) and works on every splitter, including ones without `collapsible: true` panes. Collapse stays on Enter (focused handle) and the imperative `useSplitterLayout.collapse(paneId)`. The previous binding (double-click = same as Enter) left double-click as a no-op on the common non-collapsible case. The new contract gives the gesture a uniform meaning across all splitters — a quick "undo a stray drag" — and documents the trade-off (consumers wanting a mouse affordance for collapse surface it themselves) in design.md Decision 10. - Spec: replace double-click-toggles-collapse scenarios with a new "Double-click restores defaults" requirement; clarify Enter scenarios. - Context + Root: add `restoreDefaults`; cache derived initial sizes in a ref. - Handle: onDoubleClick → restoreDefaults (gated by `disableDoubleClick`). - Stories: replace CollapsibleByDoubleClick with DoubleClickRestoresDefaults; switch CollapseExpandCallbacks to keyboard; update DisableDoubleClick. - Docs: clarify Collapsible-panes section, add Double-click-restores-defaults.
…avior and documentation updates
… guidelines Extract the Root sizes state machine into hooks/use-splitter-state.ts so SplitterRoot is a thin provider; pull the pure deriveInitialSizes and pickCollapseTarget helpers into utils/ with sibling specs (matching the existing clampedResize treatment). Add components/, hooks/, and utils/ barrels and route imports through them. Move the context module to the component root (.tsx->.ts) and relocate SplitterContextValue into the types file. De-duplicate the slot prop types. Add @supportsStyleProps and per-part JSDoc, and fix the __layoutRef typo in the useSplitterLayout example. No behavior change: typecheck, lint, and 37 unit tests (+14 new) pass.
… residue sweep Persistence reconciliation now treats the layout as a single boundary value (the second pane is always the complement of the first): salvage the first pane's stored size, infer the partner, or fall back to initialSizes. Replaces the generic N-pane normalize/fill logic that produced surprising rescaled values for renamed/partial stored records. Sweep remaining N-pane residue to explicit 2-pane form (deriveInitialSizes math; direct partner lookup and literal 50 in collapse/expand). Wait for the collapsed pane's ScrollArea viewport to become keyboard-focusable in the Cross App Commands story so the a11y check sees the settled state. Updates the persistence unit tests, the Graceful Persistence story assertion, and trims the contradictory spec scenarios to the 2-pane rule. Splitter suite: 38 unit + 14 storybook tests green.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 28e89d7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Contributor
|
Localization reminder: This PR adds or modifies `.i18n.ts` files. Please create a Jira ticket for the localization manager to initiate translation of any new or updated strings in Transifex. See LOC-1766 as an example. |
Contributor
Bundle Size ReportLast updated: 2026-05-29 12:33:57 UTC
Baseline source: comment-chain |
A pre-release review found the API over-parameterized a single-value boundary. Reshaped while there are no consumers: - Collapse is now controllable state (collapsedPane / defaultCollapsedPane / onCollapsedPaneChange) instead of an imperative API. - Removed the useSplitterLayout hook + __layoutRef; persistence is consumer- wired via the new onSizesChangeEnd (settled, debounce-free) + any storage. - Dropped per-pane maxSize (derived from partner minSize), per-pane defaultSize (single canonical defaultSizes), and per-pane disabled (replaced by Root-level isDisabled, matching the Nimbus convention). - Fixed restoreDefaults zero-size guard; removed dead commitCollapse path. - Documented the size variant; guaranteed full float precision in the size pipeline (only aria-valuenow rounds, for AT; added aria-valuetext). Docs, stories, unit/docs specs, OpenSpec change, and changeset updated.
…o guidelines
The three doc pages didn't follow the documented structure:
- Overview (.mdx): use lifecycleState + exportName frontmatter (was the
invalid documentState/InitialDraft), drop the in-body H1, restructure to
Overview → Resources → Variables → Guidelines, and use `jsx live` fences
(were the invalid `jsx-live`). Implementation detail moved to the dev page.
- Implementation (.dev.mdx): rewritten to the engineering-docs template —
Getting started, Usage examples (jsx live-dev), Component requirements,
Accessibility (labeling + PERSISTENT_ID + Keyboard navigation), API
reference (<PropsTable id="Splitter" />), Testing (mandatory disclaimer +
{{docs-tests}} directive), and Resources. (Previously it was a design-
rationale doc missing all of these.)
- Accessibility (.a11y.mdx): align to the standard shape — no title in
frontmatter, standard intro boilerplate, Accessibility standards bullet
list, and Resources.
The handle track is invisible at rest, so the `disabled` layerStyle (opacity 0.5 + not-allowed cursor) had nothing to attach to and only surfaced a misleading not-allowed cursor. Replace it with `cursor: default` so the disabled handle drops the resize cursor without implying a blocked interaction. Spec scenario updated to match.
Align the boolean with the house is*Disabled convention (isDisabled, isKeyboardDismissDisabled). The imperative disable* form had no precedent outside Splitter. Pure rename across source, stories, docs, and the OpenSpec proposal/design/tasks/spec.
- lifecycleState Experimental → Beta (component is stable enough for cautious non-critical production use). - Correct the SplitterHandleProps JSDoc: it claimed the handle 'accepts no id', but id (and other DOM/style props) flow through the slot and the accessibility docs explicitly recommend a persistent id for analytics. Reword to 'no per-handle behaviour config' instead.
Lead with the component name in backticks (drop the conventional-commit title line), switch to one-bullet-per-observable-change, and strip implementation mechanics (ARIA attribute names, focus-ring, hit-area) that the conventions doc says belong in the PR/commits, not the consumer-facing changelog.
Drop the prop-by-prop enumeration (API-reference material that belongs in the docs PropsTable, not the consumer changelog) in favor of a short 'what it is / what it's for' summary that links to the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
Splitterto@commercetools/nimbus— a user-resizable two-pane layout (e.g. a sidebar next to a content area) built on the W3C window splitter pattern. It supports pointer and keyboard resizing, collapsible panes, per-pane minimum sizes, both orientations, and storage-agnostic layout persistence.Motivation
We need a reusable, accessible primitive for resizable split layouts. The component is intentionally scoped to exactly two panes: a two-pane layout is a single boundary position (the second pane is always the complement of the first), which keeps the resize math and the accessibility contract simple and predictable. Layouts with three or more regions are composed by nesting a
Splitterinside aSplitter.Pane.API at a glance
What changed
Splitter.Root/Splitter.Pane/Splitter.Handlecompound component with id-keyedpanesconfig (minSize,collapsible,collapsedSize), drag + keyboard resize, both orientations, a documentedsizevariant, and an internationalized default handle label. Recipe + slots registered in the theme.defaultSizes(read once, normalized, full float precision) +onSizesChange(live) +onSizesChangeEnd(fires once per settled interaction — the persistence seam).collapsedPane/defaultCollapsedPane/onCollapsedPaneChange. A button anywhere in the app can collapse a pane with plainuseState; Enter on the focused handle toggles it too.defaultSizes, write back ononSizesChangeEnd, persist collapse via its controlled state. No bespoke hook.isDisabledon Root makes the whole splitter non-interactive (handle out of tab order,aria-disabled).minSizeper pane; the upper bound is derived (100 − partner.minSize), so there is no redundantmaxSize. Double-click restores the mount-time sizes.components/,hooks/,utils/with barrels; a thinSplitter.Rootdelegating touseSplitterState; pure sizing helpers inutils/with sibling unit specs.How it works
The handle is built on
react-ariaprimitives (useSeparator,useMove,useFocusRing) and exposesrole="separator"witharia-orientation,aria-valuenow/min/maxderived from per-paneminSize,aria-valuetext, andaria-controls. Sizes are percentages summing to 100 with full float precision preserved end-to-end (onlyaria-valuenowis rounded, for AT — layout is unaffected), so consumers can hit exact pixels at a known container width.Note on the API
This branch underwent a pre-release coherence reshape (no consumers shipped). The earlier
useSplitterLayouthook, per-panemaxSize/defaultSize/disabled, and the imperative__layoutRefwiring were removed in favor of the smaller, convention-aligned surface above. See the OpenSpec change "Revision" sections for the rationale.Test plan
isDisabled, nesting, float precision — 14 passingtypecheckandlintclean;openspec validatepassesScreenshots / recordings
Closes FEC-977