Breadcrumb responsive improvements.
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -61,6 +61,7 @@
|
||||
gap: var(--canon-space-2);
|
||||
font-size: var(--canon-font-size-3);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.canon-ToolbarIcon {
|
||||
@@ -74,6 +75,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
.canon-ToolbarBreadcrumbs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--canon-space-2);
|
||||
font-size: var(--canon-font-size-3);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
}
|
||||
|
||||
.canon-ToolbarBreadcrumb {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--canon-space-2);
|
||||
}
|
||||
|
||||
.canon-ToolbarBreadcrumbLink {
|
||||
color: var(--canon-fg-secondary);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
&[data-active='true'] {
|
||||
color: var(--canon-fg-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.canon-ToolbarBreadcrumbSeparator {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--canon-fg-secondary);
|
||||
}
|
||||
|
||||
.canon-ToolbarOptions {
|
||||
position: absolute;
|
||||
right: var(--canon-space-2);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.canon-ToolbarTabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -84,7 +124,6 @@
|
||||
border-radius: var(--canon-radius-2);
|
||||
border: 1px solid var(--canon-border);
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
/* Initialize CSS variables */
|
||||
--active-tab-left: 0px;
|
||||
|
||||
@@ -14,14 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Tabs, TabList, Tab as AriaTab } from 'react-aria-components';
|
||||
import { Tabs, TabList, Tab as AriaTab, Link } from 'react-aria-components';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import { useId, useRef, useState } from 'react';
|
||||
import { useId, useRef, useState, useEffect } from 'react';
|
||||
import { ToolbarTabsIndicators } from './Indicators';
|
||||
import { RiMore2Line, RiShapesLine } from '@remixicon/react';
|
||||
import { RiArrowRightSLine, RiMore2Line, RiShapesLine } from '@remixicon/react';
|
||||
import type { ToolbarProps, ToolbarTabProps } from './types';
|
||||
import { ButtonIcon } from '../ButtonIcon';
|
||||
import { Menu } from '../Menu';
|
||||
import { motion, useScroll, useTransform } from 'framer-motion';
|
||||
|
||||
/**
|
||||
* A component that renders a toolbar.
|
||||
@@ -29,12 +30,22 @@ import { Menu } from '../Menu';
|
||||
* @public
|
||||
*/
|
||||
export const Toolbar = (props: ToolbarProps) => {
|
||||
const { tabs, icon, name, options } = props;
|
||||
const { tabs, icon, name, options, breadcrumbs } = props;
|
||||
const { classNames } = useStyles('Toolbar');
|
||||
const tabsRef = useRef<HTMLDivElement>(null);
|
||||
const tabRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const [hoveredKey, setHoveredKey] = useState<string | null>(null);
|
||||
const prevHoveredKey = useRef<string | null>(null);
|
||||
const { scrollY } = useScroll();
|
||||
const breadcrumbOpacity = useTransform(scrollY, [80, 120], [0, 1]);
|
||||
|
||||
// Refs for collision detection
|
||||
const toolbarWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarContentRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarOptionsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// State for breadcrumb visibility
|
||||
const [showBreadcrumbs, setShowBreadcrumbs] = useState(true);
|
||||
|
||||
const setTabRef = (key: string, element: HTMLDivElement | null) => {
|
||||
if (element) {
|
||||
@@ -44,17 +55,94 @@ export const Toolbar = (props: ToolbarProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Set up resize observer
|
||||
useEffect(() => {
|
||||
const wrapper = toolbarWrapperRef.current;
|
||||
if (!wrapper) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const wrapper = toolbarWrapperRef.current;
|
||||
const content = toolbarContentRef.current;
|
||||
const options = toolbarOptionsRef.current;
|
||||
|
||||
if (!wrapper || !content) return;
|
||||
|
||||
// Get dimensions
|
||||
const wrapperRect = wrapper.getBoundingClientRect();
|
||||
const wrapperWidth = wrapperRect.width;
|
||||
const wrapperPadding = 24; // 12px on each side (var(--canon-space-3))
|
||||
const availableWidth = wrapperWidth - wrapperPadding;
|
||||
|
||||
// Calculate required width for content
|
||||
const contentRect = content.getBoundingClientRect();
|
||||
const contentWidth = contentRect?.width || 0;
|
||||
|
||||
// Calculate options width (if exists)
|
||||
const optionsRect = options?.getBoundingClientRect();
|
||||
const optionsWidth = optionsRect?.width || 0;
|
||||
|
||||
// Check if we need to hide breadcrumbs
|
||||
const shouldShowBreadcrumbs =
|
||||
contentWidth + optionsWidth + 32 <= availableWidth;
|
||||
|
||||
// Only update state if the value actually changed to prevent flickering
|
||||
setShowBreadcrumbs(prev =>
|
||||
prev !== shouldShowBreadcrumbs ? shouldShowBreadcrumbs : prev,
|
||||
);
|
||||
});
|
||||
|
||||
resizeObserver.observe(wrapper);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classNames.toolbar}>
|
||||
<div className={classNames.toolbarContentWrapper}>
|
||||
<div className={classNames.toolbarContent}>
|
||||
<div
|
||||
className={classNames.toolbarContentWrapper}
|
||||
ref={toolbarWrapperRef}
|
||||
>
|
||||
<div className={classNames.toolbarContent} ref={toolbarContentRef}>
|
||||
<div className={classNames.name}>
|
||||
<div className={classNames.icon}>{icon || <RiShapesLine />}</div>
|
||||
{name || 'Your plugin'}
|
||||
</div>
|
||||
{breadcrumbs && (
|
||||
<motion.div
|
||||
className={classNames.breadcrumbs}
|
||||
style={{
|
||||
opacity: breadcrumbOpacity,
|
||||
visibility: showBreadcrumbs ? 'visible' : 'hidden',
|
||||
}}
|
||||
>
|
||||
<RiArrowRightSLine
|
||||
size={16}
|
||||
className={classNames.breadcrumbSeparator}
|
||||
/>
|
||||
{breadcrumbs.map((breadcrumb, index) => (
|
||||
<div key={breadcrumb.label} className={classNames.breadcrumb}>
|
||||
<Link
|
||||
href={breadcrumb.href}
|
||||
className={classNames.breadcrumbLink}
|
||||
data-active={index === breadcrumbs.length - 1}
|
||||
>
|
||||
{breadcrumb.label}
|
||||
</Link>
|
||||
{index < breadcrumbs.length - 1 && (
|
||||
<RiArrowRightSLine
|
||||
size={16}
|
||||
className={classNames.breadcrumbSeparator}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
<div className={classNames.toolbarOptions}>
|
||||
<div className={classNames.toolbarOptions} ref={toolbarOptionsRef}>
|
||||
{options && (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
@@ -88,7 +176,7 @@ export const Toolbar = (props: ToolbarProps) => {
|
||||
</div>
|
||||
{tabs && (
|
||||
<Tabs className={classNames.tabs} ref={tabsRef}>
|
||||
<TabList className={classNames.tabList}>
|
||||
<TabList className={classNames.tabList} aria-label="Toolbar tabs">
|
||||
{tabs.map((tab, index) => {
|
||||
return (
|
||||
<Tab
|
||||
@@ -129,6 +217,7 @@ const Tab = (props: ToolbarTabProps) => {
|
||||
ref={el => setTabRef(id, el as HTMLDivElement)}
|
||||
onHoverStart={() => setHoveredKey(id)}
|
||||
onHoverEnd={() => setHoveredKey(null)}
|
||||
href={tab.href}
|
||||
>
|
||||
{tab.label}
|
||||
</AriaTab>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import type { Meta, StoryObj, StoryFn } from '@storybook/react';
|
||||
import { Toolbar } from './Toolbar';
|
||||
import { ToolbarBreadcrumb, ToolbarOption, ToolbarTab } from './types';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Toolbar',
|
||||
@@ -25,7 +26,7 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const tabs = [
|
||||
const tabs: ToolbarTab[] = [
|
||||
{
|
||||
label: 'Overview',
|
||||
},
|
||||
@@ -43,7 +44,22 @@ const tabs = [
|
||||
},
|
||||
];
|
||||
|
||||
const options = [
|
||||
const breadcrumbs: ToolbarBreadcrumb[] = [
|
||||
{
|
||||
label: 'Home',
|
||||
href: '/',
|
||||
},
|
||||
{
|
||||
label: 'Dashboard',
|
||||
href: '/dashboard',
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
href: '/settings',
|
||||
},
|
||||
];
|
||||
|
||||
const options: ToolbarOption[] = [
|
||||
{
|
||||
label: 'Settings',
|
||||
value: 'settings',
|
||||
@@ -112,10 +128,17 @@ export const WithOptions: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithOptionsAndTabs: Story = {
|
||||
export const WithBreadcrumbs: Story = {
|
||||
args: {
|
||||
breadcrumbs,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAllComponents: Story = {
|
||||
args: {
|
||||
options,
|
||||
tabs,
|
||||
breadcrumbs,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -123,6 +146,7 @@ export const WithLayout: Story = {
|
||||
args: {
|
||||
options,
|
||||
tabs,
|
||||
breadcrumbs,
|
||||
},
|
||||
decorators: layoutDecorator,
|
||||
parameters: {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { MutableRefObject } from 'react';
|
||||
*/
|
||||
export interface ToolbarTab {
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,6 +37,16 @@ export interface ToolbarOption {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a breadcrumb item in the toolbar.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ToolbarBreadcrumb {
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the main Toolbar component.
|
||||
*
|
||||
@@ -46,6 +57,7 @@ export interface ToolbarProps {
|
||||
name?: string;
|
||||
tabs?: ToolbarTab[];
|
||||
options?: ToolbarOption[];
|
||||
breadcrumbs?: ToolbarBreadcrumb[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -244,6 +244,10 @@ export const componentDefinitions = {
|
||||
toolbarOptions: 'canon-ToolbarOptions',
|
||||
icon: 'canon-ToolbarIcon',
|
||||
name: 'canon-ToolbarName',
|
||||
breadcrumbs: 'canon-ToolbarBreadcrumbs',
|
||||
breadcrumb: 'canon-ToolbarBreadcrumb',
|
||||
breadcrumbLink: 'canon-ToolbarBreadcrumbLink',
|
||||
breadcrumbSeparator: 'canon-ToolbarBreadcrumbSeparator',
|
||||
tabs: 'canon-ToolbarTabs',
|
||||
tabList: 'canon-ToolbarTabList',
|
||||
tab: 'canon-ToolbarTab',
|
||||
|
||||
Reference in New Issue
Block a user