Add custom actions to the header
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -119,11 +119,6 @@
|
||||
padding-inline: var(--canon-space-3);
|
||||
}
|
||||
|
||||
.canon-MenuTrigger {
|
||||
all: unset;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.canon-MenuPopup {
|
||||
box-sizing: border-box;
|
||||
max-width: 21.25rem;
|
||||
|
||||
@@ -9601,7 +9601,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.canon-HeaderToolbarOptions {
|
||||
.canon-HeaderToolbarControls {
|
||||
right: var(--canon-space-2);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
||||
@@ -513,7 +513,7 @@ export const componentDefinitions: {
|
||||
readonly toolbar: 'canon-HeaderToolbar';
|
||||
readonly toolbarWrapper: 'canon-HeaderToolbarWrapper';
|
||||
readonly toolbarContent: 'canon-HeaderToolbarContent';
|
||||
readonly toolbarOptions: 'canon-HeaderToolbarOptions';
|
||||
readonly toolbarControls: 'canon-HeaderToolbarControls';
|
||||
readonly toolbarIcon: 'canon-HeaderToolbarIcon';
|
||||
readonly toolbarName: 'canon-HeaderToolbarName';
|
||||
readonly breadcrumbs: 'canon-HeaderBreadcrumbs';
|
||||
@@ -995,9 +995,11 @@ export interface HeaderProps {
|
||||
// (undocumented)
|
||||
breadcrumbs?: HeaderBreadcrumb[];
|
||||
// (undocumented)
|
||||
customActions?: React.ReactNode;
|
||||
// (undocumented)
|
||||
icon?: React.ReactNode;
|
||||
// (undocumented)
|
||||
options?: HeaderOption[];
|
||||
menuItems?: HeaderOption[];
|
||||
// (undocumented)
|
||||
tabs?: HeaderTab[];
|
||||
// (undocumented)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import type { Meta, StoryObj, StoryFn } from '@storybook/react';
|
||||
import { Header } from './Header';
|
||||
import { HeaderBreadcrumb, HeaderOption, HeaderTab } from './types';
|
||||
import { Button } from '../Button';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Header',
|
||||
@@ -59,7 +60,7 @@ const breadcrumbs: HeaderBreadcrumb[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const options: HeaderOption[] = [
|
||||
const menuItems: HeaderOption[] = [
|
||||
{
|
||||
label: 'Settings',
|
||||
value: 'settings',
|
||||
@@ -124,7 +125,13 @@ export const WithTabs: Story = {
|
||||
|
||||
export const WithOptions: Story = {
|
||||
args: {
|
||||
options,
|
||||
menuItems,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCustomActions: Story = {
|
||||
args: {
|
||||
customActions: <Button>Custom action</Button>,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -136,7 +143,7 @@ export const WithBreadcrumbs: Story = {
|
||||
|
||||
export const WithAllComponents: Story = {
|
||||
args: {
|
||||
options,
|
||||
menuItems,
|
||||
tabs,
|
||||
breadcrumbs,
|
||||
},
|
||||
@@ -144,7 +151,7 @@ export const WithAllComponents: Story = {
|
||||
|
||||
export const WithLayout: Story = {
|
||||
args: {
|
||||
options,
|
||||
menuItems,
|
||||
tabs,
|
||||
breadcrumbs,
|
||||
},
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.canon-HeaderToolbarOptions {
|
||||
.canon-HeaderToolbarControls {
|
||||
position: absolute;
|
||||
right: var(--canon-space-2);
|
||||
top: 50%;
|
||||
|
||||
@@ -24,15 +24,16 @@ import { HeaderTabs } from './HeaderTabs';
|
||||
* @public
|
||||
*/
|
||||
export const Header = (props: HeaderProps) => {
|
||||
const { tabs, icon, title, options, breadcrumbs } = props;
|
||||
const { tabs, icon, title, menuItems, breadcrumbs, customActions } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderToolbar
|
||||
icon={icon}
|
||||
title={title}
|
||||
options={options}
|
||||
menuItems={menuItems}
|
||||
breadcrumbs={breadcrumbs}
|
||||
customActions={customActions}
|
||||
/>
|
||||
<HeaderTabs tabs={tabs} />
|
||||
</>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { Link } from 'react-aria-components';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import { RiArrowRightSLine, RiMore2Line, RiShapesLine } from '@remixicon/react';
|
||||
import { RiArrowRightSLine, RiMore2Line } from '@remixicon/react';
|
||||
import type { HeaderProps } from './types';
|
||||
import { ButtonIcon } from '../ButtonIcon';
|
||||
import { Menu } from '../Menu';
|
||||
@@ -29,7 +29,7 @@ import { motion, useScroll, useTransform } from 'motion/react';
|
||||
* @internal
|
||||
*/
|
||||
export const HeaderToolbar = (props: HeaderProps) => {
|
||||
const { icon, title, options, breadcrumbs } = props;
|
||||
const { icon, title, menuItems, breadcrumbs, customActions } = props;
|
||||
const { classNames } = useStyles('Header');
|
||||
|
||||
const { scrollY } = useScroll();
|
||||
@@ -38,7 +38,7 @@ export const HeaderToolbar = (props: HeaderProps) => {
|
||||
// Refs for collision detection
|
||||
const toolbarWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarContentRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarOptionsRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarControlsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// State for breadcrumb visibility
|
||||
const [showBreadcrumbs, setShowBreadcrumbs] = useState(true);
|
||||
@@ -51,7 +51,7 @@ export const HeaderToolbar = (props: HeaderProps) => {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const wrapper = toolbarWrapperRef.current;
|
||||
const content = toolbarContentRef.current;
|
||||
const options = toolbarOptionsRef.current;
|
||||
const options = toolbarControlsRef.current;
|
||||
|
||||
if (!wrapper || !content) return;
|
||||
|
||||
@@ -91,9 +91,7 @@ export const HeaderToolbar = (props: HeaderProps) => {
|
||||
<div className={classNames.toolbarWrapper} ref={toolbarWrapperRef}>
|
||||
<div className={classNames.toolbarContent} ref={toolbarContentRef}>
|
||||
<div className={classNames.toolbarName}>
|
||||
<div className={classNames.toolbarIcon}>
|
||||
{icon || <RiShapesLine />}
|
||||
</div>
|
||||
{icon && <div className={classNames.toolbarIcon}>{icon}</div>}
|
||||
{title || 'Your plugin'}
|
||||
</div>
|
||||
{breadcrumbs && (
|
||||
@@ -128,23 +126,24 @@ export const HeaderToolbar = (props: HeaderProps) => {
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
<div className={classNames.toolbarOptions} ref={toolbarOptionsRef}>
|
||||
{options && (
|
||||
<div className={classNames.toolbarControls} ref={toolbarControlsRef}>
|
||||
{customActions}
|
||||
{menuItems && (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
render={props => (
|
||||
<ButtonIcon
|
||||
{...props}
|
||||
size="small"
|
||||
icon={<RiMore2Line />}
|
||||
variant="secondary"
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Menu.Portal>
|
||||
<Menu.Positioner sideOffset={4} align="end">
|
||||
<Menu.Popup>
|
||||
{options.map(option => (
|
||||
{menuItems.map(option => (
|
||||
<Menu.Item
|
||||
key={option.value}
|
||||
onClick={() => option.onClick?.()}
|
||||
|
||||
@@ -24,9 +24,10 @@ import { MutableRefObject } from 'react';
|
||||
export interface HeaderProps {
|
||||
icon?: React.ReactNode;
|
||||
title?: string;
|
||||
tabs?: HeaderTab[];
|
||||
options?: HeaderOption[];
|
||||
breadcrumbs?: HeaderBreadcrumb[];
|
||||
customActions?: React.ReactNode;
|
||||
menuItems?: HeaderOption[];
|
||||
tabs?: HeaderTab[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -240,7 +240,7 @@ export const componentDefinitions = {
|
||||
toolbar: 'canon-HeaderToolbar',
|
||||
toolbarWrapper: 'canon-HeaderToolbarWrapper',
|
||||
toolbarContent: 'canon-HeaderToolbarContent',
|
||||
toolbarOptions: 'canon-HeaderToolbarOptions',
|
||||
toolbarControls: 'canon-HeaderToolbarControls',
|
||||
toolbarIcon: 'canon-HeaderToolbarIcon',
|
||||
toolbarName: 'canon-HeaderToolbarName',
|
||||
breadcrumbs: 'canon-HeaderBreadcrumbs',
|
||||
|
||||
Reference in New Issue
Block a user