diff --git a/.changeset/rare-ties-find.md b/.changeset/rare-ties-find.md
new file mode 100644
index 000000000..3c7de54c2
--- /dev/null
+++ b/.changeset/rare-ties-find.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Standardise toggle chevrons across the app
diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts
index e3e1aa076..ad403bae4 100644
--- a/packages/gitbook/e2e/internal.spec.ts
+++ b/packages/gitbook/e2e/internal.spec.ts
@@ -273,10 +273,10 @@ const testCases: TestsCase[] = [
await expect(navigationLink).not.toBeVisible();
// Find and click the chevron element that is next to "Editor" in the TOC
- // It is a span inside the link
+ // It is a button inside the link
const editorChevron = page
.getByRole('link', { name: 'Editor' })
- .locator('span');
+ .locator('button');
await editorChevron.click();
// Verify "Navigation" link becomes visible after expansion
@@ -295,10 +295,10 @@ const testCases: TestsCase[] = [
await expect(navigationLink).not.toBeVisible();
// Find and click the chevron element that is next to "Editor" in the TOC
- // It is a span inside the link
+ // It is a button inside the link
const editorChevron = page
.getByRole('link', { name: 'Editor' })
- .locator('span');
+ .locator('button');
await editorChevron.click();
// At this stage the link should still not be visible
@@ -307,7 +307,7 @@ const testCases: TestsCase[] = [
// Then we click 'Content Structure' chevron to expand further
const contentStructureChevron = page
.getByRole('link', { name: 'Content Structure' })
- .locator('span');
+ .locator('button');
await contentStructureChevron.click();
// Verify "Spaces" link becomes visible after expansion
diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx
index b9d79be41..09219f191 100644
--- a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx
+++ b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx
@@ -6,7 +6,7 @@ import { useEffect, useId, useMemo, useRef, useState } from 'react';
import { useAdaptiveVisitor } from '@/components/Adaptive';
import { useInViewportListener } from '@/components/hooks/useInViewportListener';
import { useScrollListener } from '@/components/hooks/useScrollListener';
-import { Button } from '@/components/primitives';
+import { Button, ToggleChevron } from '@/components/primitives';
import { t, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { useDebounceCallback } from 'usehooks-ts';
@@ -183,7 +183,7 @@ function CodeBlockExpandable(props: {
}
size="xsmall"
variant="blank"
type="button"
diff --git a/packages/gitbook/src/components/DocumentView/Expandable/Expandable.tsx b/packages/gitbook/src/components/DocumentView/Expandable/Expandable.tsx
index 8a3de8f41..cee18e19a 100644
--- a/packages/gitbook/src/components/DocumentView/Expandable/Expandable.tsx
+++ b/packages/gitbook/src/components/DocumentView/Expandable/Expandable.tsx
@@ -4,6 +4,7 @@ import { Icon } from '@gitbook/icons';
import { getNodeFragmentByType } from '@/lib/document';
import { tcls } from '@/lib/tailwind';
+import { ToggleChevron } from '@/components/primitives';
import type { BlockProps } from '../Block';
import { Blocks } from '../Blocks';
import { Inlines } from '../Inlines';
@@ -46,14 +47,14 @@ export function Expandable(props: BlockProps) {
'[&::-webkit-details-marker]:hidden'
)}
>
- ) {
{...rest}
>
{title}
- {isDropdown ? : null}
+ {isDropdown ? : null}
);
}
@@ -195,7 +194,7 @@ function HeaderItemDropdown(
{...rest}
>
{title}
-
+
);
}
diff --git a/packages/gitbook/src/components/Header/HeaderLinkMore.tsx b/packages/gitbook/src/components/Header/HeaderLinkMore.tsx
index 073ba34af..5e526cca8 100644
--- a/packages/gitbook/src/components/Header/HeaderLinkMore.tsx
+++ b/packages/gitbook/src/components/Header/HeaderLinkMore.tsx
@@ -10,12 +10,8 @@ import type React from 'react';
import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
-import {
- DropdownChevron,
- DropdownMenu,
- DropdownMenuItem,
- DropdownSubMenu,
-} from '../primitives/DropdownMenu';
+import { ToggleChevron } from '../primitives';
+import { DropdownMenu, DropdownMenuItem, DropdownSubMenu } from '../primitives/DropdownMenu';
import styles from './headerLinks.module.css';
/**
@@ -44,7 +40,7 @@ export function HeaderLinkMore(props: {
>
{label}
-
+
);
diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx
index 4224ea26f..d4a628dbe 100644
--- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx
+++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx
@@ -4,8 +4,8 @@ import { useMemo } from 'react';
import type { GitBookSiteContext } from '@/lib/context';
import { getSiteSpaceURL } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
-import { Button, type ButtonProps } from '../primitives';
-import { DropdownChevron, DropdownMenu } from '../primitives/DropdownMenu';
+import { Button, type ButtonProps, ToggleChevron } from '../primitives';
+import { DropdownMenu } from '../primitives/DropdownMenu';
import { SpacesDropdownMenuItems } from './SpacesDropdownMenuItem';
// Memoized regex for checking if a string starts with an emoji
@@ -37,7 +37,7 @@ export function SpacesDropdown(props: {
data-testid="space-dropdown-button"
size="small"
variant={variant}
- trailing={}
+ trailing={}
className={tcls('bg-tint-base', className)}
>
{siteSpace.title}
diff --git a/packages/gitbook/src/components/PageActions/PageActionsDropdown.tsx b/packages/gitbook/src/components/PageActions/PageActionsDropdown.tsx
index f713d38ba..2a041ea74 100644
--- a/packages/gitbook/src/components/PageActions/PageActionsDropdown.tsx
+++ b/packages/gitbook/src/components/PageActions/PageActionsDropdown.tsx
@@ -4,9 +4,9 @@ import { Button, ButtonGroup } from '@/components/primitives/Button';
import { DropdownMenu, DropdownMenuSeparator } from '@/components/primitives/DropdownMenu';
import { tString, useLanguage } from '@/intl/client';
import type { GitSyncState, SiteCustomizationSettings } from '@gitbook/api';
-import { Icon } from '@gitbook/icons';
import React, { useRef } from 'react';
import { useAI } from '../AI';
+import { ToggleChevron } from '../primitives';
import {
ActionCopyMCPURL,
ActionCopyMarkdown,
@@ -57,12 +57,7 @@ export function PageActionsDropdown(props: PageActionsDropdownProps) {
className="!min-w-60 max-w-max"
button={
- }
+ icon={}
label={tString(language, defaultAction ? 'more' : 'actions')}
iconOnly={!!defaultAction}
size="xsmall"
diff --git a/packages/gitbook/src/components/Search/SearchScopeControl.tsx b/packages/gitbook/src/components/Search/SearchScopeControl.tsx
index 3545b1147..c95706a3c 100644
--- a/packages/gitbook/src/components/Search/SearchScopeControl.tsx
+++ b/packages/gitbook/src/components/Search/SearchScopeControl.tsx
@@ -2,7 +2,7 @@
import { t, tString, useLanguage } from '@/intl/client';
import type { SiteSection } from '@gitbook/api';
-import { Button, DropdownChevron, DropdownMenu, DropdownMenuItem } from '../primitives';
+import { Button, DropdownMenu, DropdownMenuItem, ToggleChevron } from '../primitives';
import { useSearch } from './useSearch';
interface SearchScopeControlProps {
@@ -64,7 +64,7 @@ function SearchScopeSectionControl(props: SearchScopeControlProps & { isExtended
isExtended ? 'search_scope_section_all' : 'search_scope_section_current',
section?.title ?? ''
)}
- trailing={}
+ trailing={}
/>
}
>
@@ -123,7 +123,7 @@ function SearchScopeVariantControl(props: SearchScopeControlProps & { isExtended
isExtended ? 'search_scope_variant_all' : 'search_scope_variant_current',
spaceTitle ?? ''
)}
- trailing={}
+ trailing={}
/>
}
>
diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx
index 5254a7606..9dc149203 100644
--- a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx
+++ b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx
@@ -1,13 +1,13 @@
'use client';
-import { Icon, type IconName } from '@gitbook/icons';
+import type { IconName } from '@gitbook/icons';
import { motion } from 'motion/react';
import React from 'react';
import { type ClassValue, tcls } from '@/lib/tailwind';
import { findSectionInGroup } from '@/lib/utils';
import { useToggleAnimation } from '../hooks';
-import { Link } from '../primitives';
+import { Link, ToggleChevron } from '../primitives';
import { ScrollContainer } from '../primitives/ScrollContainer';
import { SectionIcon } from './SectionIcon';
import type {
@@ -199,21 +199,18 @@ export function SiteSectionGroupItem(props: {
isActiveGroup && 'hover:bg-tint-hover'
)}
>
-
diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
index 0e26edeac..3f98fe3cf 100644
--- a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
+++ b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
@@ -4,7 +4,7 @@ import type { IconName } from '@gitbook/icons';
import * as NavigationMenu from '@radix-ui/react-navigation-menu';
import React from 'react';
-import { Button, DropdownChevron, Link } from '@/components/primitives';
+import { Button, Link, ToggleChevron } from '@/components/primitives';
import { tcls } from '@/lib/tailwind';
import { findSectionInGroup } from '@/lib/utils';
import { useIsMobile } from '../hooks/useIsMobile';
@@ -188,7 +188,7 @@ const SectionTab = React.forwardRef(function SectionTab(
{...rest}
icon={icon ? : null}
label={title}
- trailing={isGroup ? : null}
+ trailing={isGroup ? : null}
active={isActive}
className={tcls(
'group/dropdown relative my-1.5 overflow-visible',
diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
index b2843aca8..ad132d6fd 100644
--- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
@@ -1,13 +1,8 @@
'use client';
-
-import { Icon } from '@gitbook/icons';
import { AnimatePresence, motion } from 'motion/react';
import React, { useRef } from 'react';
-
-import { tcls } from '@/lib/tailwind';
-
import { useCurrentPagePath } from '../hooks';
-import { Link, type LinkInsightsProps, type LinkProps } from '../primitives';
+import { Button, Link, type LinkInsightsProps, type LinkProps, ToggleChevron } from '../primitives';
import { useScrollToActiveTOCItem } from './TOCScroller';
/**
@@ -138,43 +133,28 @@ function Toggler(props: {
isOpen: boolean;
onToggle: () => void;
}) {
- const { isLinkActive, isOpen, onToggle } = props;
+ const { isOpen, onToggle } = props;
return (
-
+ }
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onToggle();
}}
- >
-
-
+ label={undefined}
+ size="xsmall"
+ iconOnly
+ variant="blank"
+ className="ml-auto text-current hover:bg-tint-base"
+ tabIndex={-1} // Prevent focus on the button since it's already inside a clickable link that performs the same toggle action.
+ />
);
}
diff --git a/packages/gitbook/src/components/primitives/DropdownMenu.tsx b/packages/gitbook/src/components/primitives/DropdownMenu.tsx
index 87160de36..130a1e138 100644
--- a/packages/gitbook/src/components/primitives/DropdownMenu.tsx
+++ b/packages/gitbook/src/components/primitives/DropdownMenu.tsx
@@ -10,6 +10,7 @@ import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
import { assert } from 'ts-essentials';
import { Link, type LinkInsightsProps } from '.';
+import { ToggleChevron } from './ToggleChevron';
export type DropdownButtonProps = Omit<
Partial, E>>,
@@ -99,26 +100,6 @@ export function DropdownMenu(props: {
);
}
-/**
- * Animated chevron to display in the dropdown button.
- */
-export function DropdownChevron() {
- return (
-
- );
-}
-
/**
* Button with a chevron for use in dropdowns.
*/
@@ -131,7 +112,7 @@ export function DropdownButton(props: {
return (
{children}
-
+
);
}
diff --git a/packages/gitbook/src/components/primitives/ToggleChevron.tsx b/packages/gitbook/src/components/primitives/ToggleChevron.tsx
new file mode 100644
index 000000000..0ef35f520
--- /dev/null
+++ b/packages/gitbook/src/components/primitives/ToggleChevron.tsx
@@ -0,0 +1,44 @@
+import { type ClassValue, tcls } from '@/lib/tailwind';
+import { Icon, type IconName } from '@gitbook/icons';
+
+/**
+ * Animated chevron to display in dropdowns and other toggleable elements.
+ * Uses the `data-[state=open]` attribute to animate the chevron, or you can pass it an `open` prop to control the state.
+ */
+export function ToggleChevron(props: {
+ open?: boolean;
+ orientation?: 'down-to-up' | 'right-to-down'; // The direction of the chevron when open.
+ className?: ClassValue;
+}) {
+ const {
+ open,
+ orientation = 'down-to-up',
+ className = 'opacity-6 group-hover/dropdown:opacity-11',
+ } = props;
+
+ const classes = {
+ 'down-to-up': {
+ icon: 'chevron-down',
+ animation: 'rotate-180',
+ autoAnimation: 'group-data-[state=open]/dropdown:rotate-180 group-open:rotate-180',
+ },
+ 'right-to-down': {
+ icon: 'chevron-right',
+ animation: 'rotate-90',
+ autoAnimation: 'group-data-[state=open]/dropdown:rotate-90 group-open:rotate-90',
+ },
+ };
+
+ return (
+
+ );
+}
diff --git a/packages/gitbook/src/components/primitives/index.ts b/packages/gitbook/src/components/primitives/index.ts
index 74927a29b..6da921669 100644
--- a/packages/gitbook/src/components/primitives/index.ts
+++ b/packages/gitbook/src/components/primitives/index.ts
@@ -14,3 +14,4 @@ export * from './LoadingStateProvider';
export * from './HoverCard';
export * from './DropdownMenu';
export * from './Input';
+export * from './ToggleChevron';