Skip to content

Conversation

@jamiehenson
Copy link
Member

@jamiehenson jamiehenson commented Nov 11, 2025

Human preamble

This PR does two things.

Firstly, implements this (with the exception of the Markdown buttons, since the functionality is not yet available and the buttons are easily addable):
Screenshot 2025-11-12 at 16 40 24

Secondly, it refactors the LanguageSelector component to be based around Radix primitives versus react-select. The motivation for this is that it's a) more syntactically inkeeping with other components that also use Radix as a foundation, b) it's lighter and more customisable than react-select. Not critical to the outcome of this PR but worthwhile doing whilst I was in here.

Review link: https://ably-docs-web-4686-docs-7kp8gz.herokuapp.com/docs/chat/rooms

Acceptance criteria:

  • resembles the design
  • works responsively
  • the llm provider links work

Summary

  • Created new PageHeader MDX component with title, description, language selector, and LLM integration links
  • Migrated LanguageSelector from react-select to Radix UI primitives for better accessibility and consistency
  • Added IBM Plex Serif font family for typography enhancements

Changes

PageHeader Component

  • Created src/components/Layout/mdx/PageHeader.tsx
  • Displays page title and description from MDX frontmatter
  • Integrates LanguageSelector component
  • Includes "Open in" links for ChatGPT and Claude with tooltips
  • Features tracking analytics for LLM link clicks
  • Responsive layout with proper spacing and borders

LanguageSelector Migration

  • Replaced react-select with @radix-ui/react-select
  • Maintained all existing functionality and visual appearance
  • Benefits:
    • Better accessibility (WAI-ARIA ListBox pattern)
    • Smaller bundle size
    • Consistency with other Radix primitives (Accordion, DropdownMenu, Tooltip)
    • Built-in keyboard navigation
    • Automatic click-outside and portal management
  • Updated tests to work with Radix structure
  • All 7 tests passing ✓

Typography

  • Added IBM Plex Serif font as font-serif
  • Configured font weights and display settings

Testing

  • ✅ LanguageSelector tests: 7/7 passed
  • ✅ All Layout component tests: 32/32 passed
  • ✅ TypeScript compilation successful
  • ✅ No unused variables or ESLint warnings

Notes

  • Only migrated LanguageSelector to Radix - other react-select usage (ApiKeyMenu, Select wrapper) remains unchanged
  • PageHeader LLM URLs use dynamic prompt generation based on product, page, and language context
  • Added scrollIntoView mock in tests for Radix Select compatibility with JSDOM

🤖 Generated with Claude Code

Co-Authored-By: Claude [email protected]

Summary by CodeRabbit

Release Notes

  • New Features

    • Added page header with quick links to ChatGPT and Claude for accessing pages in AI assistants with one click.
    • Enhanced language selector interface with improved visual design.
  • UI/UX Improvements

    • Updated typography with IBM Plex Serif font support.
    • Refined breadcrumbs navigation for better clarity.
    • Optimized spacing and layout consistency across pages.
  • Tests

    • Expanded test coverage for breadcrumbs and language selector functionality.

@coderabbitai
Copy link

coderabbitai bot commented Nov 11, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR introduces a new utility strip feature by adding Radix UI Select-based components (PageHeader, Select wrapper, Skeleton), refactoring LanguageSelector to use the new Select component, simplifying Breadcrumbs logic, consolidating styling utilities, and adding IBM Plex Serif font support across the application layout.

Changes

Cohort / File(s) Change Summary
Dependencies
package.json
Added "@radix-ui/react-select" ^2.2.6 dependency
Typography & Theming
src/components/Head.tsx, tailwind.config.js
Added IBM Plex Serif font stylesheet URL and Tailwind font-family configuration (serif: ['IBM Plex Serif', 'Georgia', ...])
Styling Utilities
src/components/Layout/utils/styles.ts
New utility file exporting tooltipContentClassName, secondaryButtonClassName, and iconButtonClassName for reuse across components
Layout Container Padding
src/components/Article/index.tsx, src/components/Layout/Layout.tsx
Added horizontal padding (px-4) and negative margin (-mx-4) to Article and Layout containers for edge-to-edge responsive spacing
Breadcrumbs Refactoring
src/components/Layout/Breadcrumbs.tsx, src/components/Layout/Breadcrumbs.test.tsx
Removed useMemo-based filtering logic; simplified to direct activePage.tree mapping; added lastActiveNodeIndex tracking for mobile visibility; updated test assertions to verify full tree rendering and disabled state styling
Header Style Consolidation
src/components/Layout/Header.tsx
Replaced inline style constant definitions (tooltipContentClassName, secondaryButtonClassName, iconButtonClassName) with imports from ./utils/styles
Select Component Wrapper
src/components/ui/Select.tsx
New Radix UI Select wrapper component with useScrollUnlock hook to unlock body scrolling when dropdown is open; re-exports Radix exports and adds Root as named export
LanguageSelector Migration
src/components/Layout/LanguageSelector.tsx, src/components/Layout/LanguageSelector.test.tsx
Replaced custom implementation with Radix UI Select wrapper; added value state, language change tracking, URL query navigation, and skeleton fallback; updated tests with async waitFor, role-based selectors, and navigation verification
PageHeader Component
src/components/Layout/mdx/PageHeader.tsx, src/components/Layout/MDXWrapper.tsx
New PageHeader component rendering title, description, conditional LanguageSelector, and LLM links (ChatGPT, Claude) with tooltips and telemetry tracking; replaced PageTitle usage in MDXWrapper with PageHeader
Skeleton Component
src/components/ui/Skeleton.tsx
New UI skeleton component with data-slot="skeleton" attribute, accepting className and spreading remaining div props

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant LanguageSelector
    participant Select as Radix Select
    participant Navigate
    participant UrlQuery as URL Query
    
    User->>LanguageSelector: Open language dropdown
    activate LanguageSelector
    LanguageSelector->>Select: Render Select.Root (via wrapper)
    Select->>LanguageSelector: Scroll unlock on open
    LanguageSelector->>LanguageSelector: Display skeleton if loading
    User->>Select: Select language
    LanguageSelector->>LanguageSelector: Update value state
    LanguageSelector->>LanguageSelector: track('language_changed')
    LanguageSelector->>Navigate: navigate(path?lang=code)
    Navigate->>UrlQuery: Update query parameter
    deactivate LanguageSelector
Loading
sequenceDiagram
    participant User
    participant PageHeader
    participant LayoutContext
    participant Tooltip
    participant LlmService as External LLM
    
    Note over PageHeader: Component Renders
    PageHeader->>LayoutContext: useLayoutContext (activePage)
    LayoutContext-->>PageHeader: Receive product, page, language
    PageHeader->>PageHeader: useMemo compute llmLinks
    PageHeader->>PageHeader: Render title + description
    alt activePage.languages.length > 0
        PageHeader->>PageHeader: Render LanguageSelector
    end
    PageHeader->>PageHeader: Render LLM link buttons
    User->>PageHeader: Hover on LLM link
    PageHeader->>Tooltip: Show tooltip (model + login note)
    User->>PageHeader: Click LLM link
    PageHeader->>PageHeader: track('llm_link_clicked')
    PageHeader->>LlmService: window.open(llmUrl, '_blank')
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • LanguageSelector refactoring: Substantial logic change from custom implementation to Radix UI Select; verify state management, navigation behavior, and scroll unlock interaction
  • New PageHeader component: Review LLM link generation logic, telemetry tracking, context dependencies, and conditional rendering of LanguageSelector
  • Breadcrumbs simplification: Ensure removal of useMemo filtering doesn't introduce performance regressions; verify lastActiveNodeIndex logic for mobile visibility
  • Select wrapper scroll unlock: Complex useLayoutEffect and MutationObserver logic; verify robustness of scroll-lock attribute detection and cleanup
  • Test coverage updates: Verify LanguageSelector tests adequately cover new Select-based behavior and navigation query parameter handling
  • Cross-component styling consolidation: Ensure re-exported styles from utils/styles.ts match original definitions and maintain visual consistency

Poem

🐰 A hop, skip, and scroll—our Select takes flight,
With PageHeader crowned in serif delight,
Breadcrumbs now lean, the UI grows bright,
Tooltips spin tales of Claude and ChatGPT's might,
The utility strip hops ever upright! 🎀✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Multiple changes appear unrelated to PageHeader and LanguageSelector migration: Article/Layout styling adjustments, Breadcrumbs refactor, Head font changes, and new UI utilities suggest scope expansion beyond stated objectives. Justify or move out-of-scope changes (Article padding, Breadcrumbs logic, Head fonts, MDXWrapper updates) into separate PRs or update PR description to document all intended changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Linked Issues check ❓ Inconclusive Linked issue WEB-4686 has no defined coding requirements or acceptance criteria provided; cannot validate PR completion against undefined objectives. Clarify the coding requirements and acceptance criteria for WEB-4686 to enable proper validation of PR compliance.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly matches the two main changes: adding PageHeader component and migrating LanguageSelector to Radix UI, both clearly implemented in the changeset.

Comment @coderabbitai help to get the list of available commands and usage tips.

@jamiehenson jamiehenson changed the title feat: Add PageHeader component and migrate LanguageSelector to Radix UI [WEB-4686] Add PageHeader component and migrate LanguageSelector to Radix UI Nov 11, 2025
Base automatically changed from web-4684-docs-nav-update to web-4684-docs-redesign November 12, 2025 13:46
@jamiehenson jamiehenson force-pushed the web-4684-docs-redesign branch from c967a27 to 8aec4e5 Compare November 12, 2025 13:47
@jamiehenson jamiehenson force-pushed the web-4686-docs-header branch 2 times, most recently from fc79806 to 7e18063 Compare November 12, 2025 14:01
@jamiehenson jamiehenson added the review-app Create a Heroku review app label Nov 12, 2025
@ably-ci ably-ci temporarily deployed to ably-docs-web-4686-docs-7kp8gz November 12, 2025 16:28 Inactive
@jamiehenson jamiehenson marked this pull request as ready for review November 12, 2025 16:43
@jamiehenson jamiehenson temporarily deployed to ably-docs-web-4686-docs-7kp8gz November 12, 2025 16:49 Inactive

const Article: FC<{ children: React.ReactNode }> = ({ children }) => (
<article className="flex-1 overflow-x-hidden relative z-10">
<article className="flex-1 overflow-x-hidden px-4 -mx-4 relative z-10">
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows focus outlines to be visible when otherwise they would be cut off by the container's overflow property

@@ -0,0 +1,12 @@
import cn from '@ably/ui/core/utils/cn';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as was in Header, just moved out so it could be used in the assets added here

}, [activePage?.tree]);

if (breadcrumbNodes.length === 0) {
if (!activePage?.tree || activePage.tree.length === 0) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need to do this processing anymore, we no longer care about eliminating duplicates

import cn from '@ably/ui/core/utils/cn';

import { getRandomChannelName } from '../blocks/software/Code/get-random-channel-name';
import Aside from '../blocks/dividers/Aside';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same imports, just reorganised into logical groups

@jamiehenson jamiehenson temporarily deployed to ably-docs-web-4686-docs-7kp8gz November 12, 2025 16:55 Inactive
@jamiehenson jamiehenson temporarily deployed to ably-docs-web-4686-docs-7kp8gz November 12, 2025 17:08 Inactive
@@ -0,0 +1,13 @@
import cn from '@ably/ui/core/utils/cn';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extend: {
...ablyUIConfig.theme.extend,
fontFamily: {
serif: ['IBM Plex Serif', 'Georgia', 'Cambria', 'Times New Roman', 'Times', 'serif'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohh new one

Copy link
Contributor

@aralovelace aralovelace left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much nicer language selector!

One thing I notice when dropdown is active the right side nav moved as well? is it expected? or fixed on another PR?

https://www.loom.com/share/75e2ae0803824da2b4e09b757b21814b

@@ -0,0 +1,13 @@
import cn from '@ably/ui/core/utils/cn';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import { useEffect, useMemo, useState } from 'react';
import { useLocation } from '@reach/router';
import Select from 'react-select';
import * as Select from '@radix-ui/react-select';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yey more radix!

@jamiehenson jamiehenson temporarily deployed to ably-docs-web-4686-docs-7kp8gz November 13, 2025 15:42 Inactive
@jamiehenson jamiehenson had a problem deploying to ably-docs-web-4686-docs-7kp8gz November 13, 2025 16:03 Failure
@jamiehenson jamiehenson temporarily deployed to ably-docs-web-4686-docs-7kp8gz November 13, 2025 16:03 Inactive
@jamiehenson
Copy link
Member Author

jamiehenson commented Nov 13, 2025

One thing I notice when dropdown is active the right side nav moved as well? is it expected? or fixed on another PR?

@aralovelace interesting one, I hadn't noticed this - you actually unearthed something undesirable about this radix select component that I've spent a bit of time getting my head around.

For whatever reason, opening the dropdown locks scrolling on the page and there's no way to disable that - this is the thing that makes the right sidebar shift because when the scrolling is locked, the scrollbar is taken away. This is solvable via CSS, but the locking is the worst part as it's somewhat bizarre behaviour for a dropdown box to force.

What I've done is repackaged Radix select in a local UI file that also includes a workaround that disables this scroll locking until Radix offers a way to turn it off. I briefly tried reverting back to react-select which doesn't have this quirk but it's a greater loss as it's a step back accessibility-wise.

@jamiehenson
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 13, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@@ -0,0 +1,50 @@
import { useLayoutEffect } from 'react';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TLDR of this file is, it reexports Radix, but bundles in a useEffect in with Root that forces scrolling to be unlocked as Radix itself insists on locking it

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/components/Layout/utils/styles.ts (1)

9-12: Consider using cn consistently for all composite class strings.

The secondaryButtonClassName is defined as a plain string literal, while iconButtonClassName uses cn to compose classes. For consistency and to leverage cn's deduplication and conditional class handling, consider wrapping secondaryButtonClassName in cn as well.

-export const secondaryButtonClassName =
-  'focus-base flex items-center justify-center gap-2 px-4 py-[7px] h-9 ui-text-label4 text-neutral-1300 dark:text-neutral-000 rounded border border-neutral-400 dark:border-neutral-900 hover:border-neutral-600 dark:hover:border-neutral-700';
+export const secondaryButtonClassName = cn(
+  'focus-base flex items-center justify-center gap-2 px-4 py-[7px] h-9 ui-text-label4 text-neutral-1300 dark:text-neutral-000 rounded border border-neutral-400 dark:border-neutral-900 hover:border-neutral-600 dark:hover:border-neutral-700'
+);
src/components/Layout/LanguageSelector.tsx (1)

50-61: Query parameter handling may lose existing parameters.

Line 59 replaces the entire query string with ?lang=${option.label}. If the URL contains other query parameters, they will be lost. Consider preserving existing parameters by parsing and updating the search params.

 const handleValueChange = (newValue: string) => {
   setValue(newValue);

   const option = options.find((opt) => opt.value === newValue);
   if (option) {
     track('language_selector_changed', {
       language: option.label,
       location: location.pathname,
     });
-    navigate(`${location.pathname}?lang=${option.label}`);
+    const params = new URLSearchParams(location.search);
+    params.set('lang', option.label);
+    navigate(`${location.pathname}?${params.toString()}`);
   }
 };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8aec4e5 and a01cb40.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (15)
  • package.json (1 hunks)
  • src/components/Article/index.tsx (1 hunks)
  • src/components/Head.tsx (1 hunks)
  • src/components/Layout/Breadcrumbs.test.tsx (1 hunks)
  • src/components/Layout/Breadcrumbs.tsx (2 hunks)
  • src/components/Layout/Header.tsx (1 hunks)
  • src/components/Layout/LanguageSelector.test.tsx (6 hunks)
  • src/components/Layout/LanguageSelector.tsx (3 hunks)
  • src/components/Layout/Layout.tsx (1 hunks)
  • src/components/Layout/MDXWrapper.tsx (4 hunks)
  • src/components/Layout/mdx/PageHeader.tsx (1 hunks)
  • src/components/Layout/utils/styles.ts (1 hunks)
  • src/components/ui/Select.tsx (1 hunks)
  • src/components/ui/Skeleton.tsx (1 hunks)
  • tailwind.config.js (1 hunks)
🔇 Additional comments (17)
src/components/Article/index.tsx (1)

5-5: LGTM!

The padding/negative margin technique prevents focus outlines from being clipped by overflow, and the pattern is consistent with similar changes in Layout.tsx.

src/components/Head.tsx (1)

30-30: Verify font weight requirements.

The IBM Plex Serif addition aligns with the Tailwind config update. Note that only italic weights (400 and 700) are being loaded.

If you plan to use IBM Plex Serif in non-italic (roman) styles, update the URL to include those weights:

-href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Manrope:[email protected]&family=Source+Code+Pro:wght@600&family=IBM+Plex+Serif:ital,wght@1,400;1,700&display=swap"
+href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Manrope:[email protected]&family=Source+Code+Pro:wght@600&family=IBM+Plex+Serif:ital,wght@0,400;0,700;1,400;1,700&display=swap"
tailwind.config.js (1)

15-17: LGTM!

The serif font family addition is well-structured with appropriate fallbacks and aligns with the font loading in Head.tsx.

src/components/Layout/Layout.tsx (1)

45-45: LGTM!

The padding/negative margin addition is consistent with the Article component change and maintains the conditional overflow behavior.

src/components/Layout/Header.tsx (1)

16-16: LGTM!

Centralizing styling constants in a shared module is good practice and reduces duplication. The runtime behavior remains identical.

src/components/ui/Skeleton.tsx (1)

1-13: LGTM!

Clean skeleton component implementation following standard patterns. The data-slot attribute provides a useful hook for testing and styling.

package.json (1)

52-52: Version 2.2.6 is current and appropriate for this use case.

The latest stable version of @radix-ui/react-select is 2.2.6, and no direct security vulnerabilities are recorded for this version. The known issues (#2706 on resetting optional Selects and #2192 on imports) do not affect this implementation—LanguageSelector does not expose reset functionality, and the custom wrapper properly re-exports Radix UI components. The addition is sound and ready.

src/components/Layout/MDXWrapper.tsx (2)

55-56: LGTM: Improved type safety for code block processing.

The new ElementProps type provides clearer typing for the code block processing logic and makes the element property contract explicit.


124-129: LGTM: Stronger validation for code element className.

The updated logic now requires className to be truthy (not just present), which prevents processing when the className is an empty string. This is a good defensive improvement.

src/components/Layout/Breadcrumbs.tsx (1)

8-9: LGTM: Centralized link styles improve maintainability.

Extracting the link styles into a constant makes it easier to maintain consistent styling across all breadcrumb links.

src/components/Layout/mdx/PageHeader.tsx (2)

35-37: Nice use of semantic typography for the description.

The italic serif font styling for the description provides good visual hierarchy and readability.


66-66: No issues found. Both icon names are available in @ably/ui 17.9.3.

The icons icon-tech-openai and icon-tech-claude-mono are defined in the @ably/ui package at node_modules/@ably/ui/core/icons/tech/. They exist as SVG assets and will load correctly at runtime without any failures.

src/components/Layout/Breadcrumbs.test.tsx (1)

40-96: LGTM: Comprehensive test coverage for the refactored breadcrumb logic.

The updated tests cover:

  • Rendering of all tree nodes including hash-link nodes
  • Correct href attributes
  • Disabled states for last item and non-linked nodes
  • Mobile visibility behavior
  • Edge cases like non-linked current pages

This provides strong validation of the new breadcrumb rendering logic.

src/components/Layout/LanguageSelector.tsx (2)

71-153: LGTM: Clean Radix UI implementation with good accessibility.

The Radix Select implementation includes:

  • Proper ARIA labels for the trigger
  • Keyboard navigation support via Radix primitives
  • Portal rendering for proper layering
  • Item indicators for selected state
  • Responsive styling and proper focus states

14-14: Select wrapper properly implements the scroll-lock workaround.

The verification confirms that src/components/ui/Select.tsx contains a fully implemented useScrollUnlock hook that addresses the Radix Select scroll-locking issue. The wrapper correctly detects the data-scroll-locked attribute via MutationObserver and removes it to restore scrolling, preventing the layout shifts mentioned in the PR.

src/components/Layout/LanguageSelector.test.tsx (2)

36-49: LGTM: Mock data structure updated to match implementation.

The mock for src/data/languages correctly provides languageData with string versions and languageInfo with label mappings, matching the expected structure in the refactored component.


81-161: LGTM: Comprehensive test coverage for Radix-based implementation.

The updated tests properly cover:

  • Async dropdown interactions with waitFor
  • Combobox role-based selection for accessibility
  • Escape key handling for dismissing the dropdown
  • Language filtering based on activePage.languages
  • Default language selection
  • Navigation behavior with query parameters

The use of waitFor handles the async nature of Radix UI components correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-app Create a Heroku review app

Development

Successfully merging this pull request may close these issues.

4 participants