Init AxoMenuBuilder

This commit is contained in:
Jamie Kyle
2025-10-06 10:39:19 -07:00
committed by GitHub
parent 290c8fec5b
commit dea3e11379
2 changed files with 321 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react';
import React, { useState } from 'react';
import type { Meta } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { AxoMenuBuilder } from './AxoMenuBuilder.js';
import { AxoButton } from './AxoButton.js';
import { tw } from './tw.js';
export default {
title: 'Axo/AxoMenuBuilder',
} satisfies Meta;
function Template(props: {
renderer: AxoMenuBuilder.Renderer;
children: ReactNode;
}) {
const [showBookmarks, setShowBookmarks] = useState(true);
const [showFullUrls, setShowFullUrls] = useState(false);
const [selectedPerson, setSelectedPerson] = useState('jamie');
return (
<AxoMenuBuilder.Root renderer={props.renderer}>
<AxoMenuBuilder.Trigger>{props.children}</AxoMenuBuilder.Trigger>
<AxoMenuBuilder.Content>
<AxoMenuBuilder.Item
symbol="arrow-[start]"
onSelect={action('back')}
keyboardShortcut="⌘["
>
Back
</AxoMenuBuilder.Item>
<AxoMenuBuilder.Item
disabled
symbol="arrow-[end]"
onSelect={action('forward')}
keyboardShortcut="⌘]"
>
Forward
</AxoMenuBuilder.Item>
<AxoMenuBuilder.Item onSelect={action('reload')} keyboardShortcut="⌘R">
Reload
</AxoMenuBuilder.Item>
<AxoMenuBuilder.Sub>
<AxoMenuBuilder.SubTrigger>More Tools</AxoMenuBuilder.SubTrigger>
<AxoMenuBuilder.SubContent>
<AxoMenuBuilder.Item
onSelect={action('savePageAs')}
keyboardShortcut="⌘S"
>
Save Page As...
</AxoMenuBuilder.Item>
<AxoMenuBuilder.Item onSelect={action('createShortcut')}>
Create Shortcut...
</AxoMenuBuilder.Item>
<AxoMenuBuilder.Item onSelect={action('nameWindow')}>
Name Window...
</AxoMenuBuilder.Item>
<AxoMenuBuilder.Separator />
<AxoMenuBuilder.Item onSelect={action('developerTools')}>
Developer Tools
</AxoMenuBuilder.Item>
</AxoMenuBuilder.SubContent>
</AxoMenuBuilder.Sub>
<AxoMenuBuilder.Separator />
<AxoMenuBuilder.CheckboxItem
checked={showBookmarks}
onCheckedChange={setShowBookmarks}
keyboardShortcut="⌘B"
>
Show Bookmarks
</AxoMenuBuilder.CheckboxItem>
<AxoMenuBuilder.CheckboxItem
symbol="link"
checked={showFullUrls}
onCheckedChange={setShowFullUrls}
>
Show Full URLs
</AxoMenuBuilder.CheckboxItem>
<AxoMenuBuilder.Separator />
<AxoMenuBuilder.Label>People</AxoMenuBuilder.Label>
<AxoMenuBuilder.RadioGroup
value={selectedPerson}
onValueChange={setSelectedPerson}
>
<AxoMenuBuilder.RadioItem value="jamie">
Jamie
</AxoMenuBuilder.RadioItem>
<AxoMenuBuilder.RadioItem value="tyler">
Tyler
</AxoMenuBuilder.RadioItem>
</AxoMenuBuilder.RadioGroup>
</AxoMenuBuilder.Content>
</AxoMenuBuilder.Root>
);
}
export function Basic(): JSX.Element {
return (
<div className={tw('flex h-96 w-full items-center justify-center gap-8')}>
<Template renderer="AxoDropdownMenu">
<AxoButton.Root variant="secondary" size="medium">
Open Dropdown Menu
</AxoButton.Root>
</Template>
<Template renderer="AxoContextMenu">
<div className={tw('bg-fill-secondary p-12 text-color-label-primary')}>
Right-Click
</div>
</Template>
</div>
);
}

207
ts/axo/AxoMenuBuilder.tsx Normal file
View File

@@ -0,0 +1,207 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { FC } from 'react';
import React, { createContext, memo, useContext } from 'react';
import type { AxoBaseMenu } from './_internal/AxoBaseMenu.js';
import { assert, unreachable } from './_internal/assert.js';
import { AxoDropdownMenu } from './AxoDropdownMenu.js';
import { AxoContextMenu } from './AxoContextMenu.js';
const Namespace = 'AxoMenuBuilder';
export namespace AxoMenuBuilder {
export type Renderer = 'AxoDropdownMenu' | 'AxoContextMenu';
const MenuBuilderContext = createContext<Renderer | null>(null);
// eslint-disable-next-line no-inner-declarations
function useMenuBuilderContext(): Renderer {
const context = useContext(MenuBuilderContext);
return assert(context, `Must be wrapped with <${Namespace}.Root>`);
}
export type RootProps = AxoBaseMenu.MenuRootProps &
Readonly<{
renderer: Renderer;
}>;
export const Root: FC<RootProps> = memo(props => {
const { renderer, ...rest } = props;
let child: JSX.Element;
if (renderer === 'AxoDropdownMenu') {
child = <AxoDropdownMenu.Root {...rest} />;
} else if (renderer === 'AxoContextMenu') {
child = <AxoContextMenu.Root {...rest} />;
} else {
unreachable(renderer);
}
return (
<MenuBuilderContext.Provider value={props.renderer}>
{child}
</MenuBuilderContext.Provider>
);
});
Root.displayName = `${Namespace}.Root`;
export const Trigger: FC<AxoBaseMenu.MenuTriggerProps> = memo(props => {
const renderer = useMenuBuilderContext();
if (renderer === 'AxoDropdownMenu') {
return <AxoDropdownMenu.Trigger {...props} />;
}
if (renderer === 'AxoContextMenu') {
return <AxoContextMenu.Trigger {...props} />;
}
unreachable(renderer);
});
Trigger.displayName = `${Namespace}.Trigger`;
export const Content: FC<AxoBaseMenu.MenuContentProps> = memo(props => {
const renderer = useMenuBuilderContext();
if (renderer === 'AxoDropdownMenu') {
return <AxoDropdownMenu.Content {...props} />;
}
if (renderer === 'AxoContextMenu') {
return <AxoContextMenu.Content {...props} />;
}
unreachable(renderer);
});
Content.displayName = `${Namespace}.Content`;
export const Item: FC<AxoBaseMenu.MenuItemProps> = memo(props => {
const renderer = useMenuBuilderContext();
if (renderer === 'AxoDropdownMenu') {
return <AxoDropdownMenu.Item {...props} />;
}
if (renderer === 'AxoContextMenu') {
return <AxoContextMenu.Item {...props} />;
}
unreachable(renderer);
});
Item.displayName = `${Namespace}.Item`;
export const Group: FC<AxoBaseMenu.MenuGroupProps> = memo(props => {
const renderer = useMenuBuilderContext();
if (renderer === 'AxoDropdownMenu') {
return <AxoDropdownMenu.Group {...props} />;
}
if (renderer === 'AxoContextMenu') {
return <AxoContextMenu.Group {...props} />;
}
unreachable(renderer);
});
Group.displayName = `${Namespace}.Group`;
export const Label: FC<AxoBaseMenu.MenuLabelProps> = memo(props => {
const renderer = useMenuBuilderContext();
if (renderer === 'AxoDropdownMenu') {
return <AxoDropdownMenu.Label {...props} />;
}
if (renderer === 'AxoContextMenu') {
return <AxoContextMenu.Label {...props} />;
}
unreachable(renderer);
});
Label.displayName = `${Namespace}.Label`;
export const Separator: FC<AxoBaseMenu.MenuSeparatorProps> = memo(props => {
const renderer = useMenuBuilderContext();
if (renderer === 'AxoDropdownMenu') {
return <AxoDropdownMenu.Separator {...props} />;
}
if (renderer === 'AxoContextMenu') {
return <AxoContextMenu.Separator {...props} />;
}
unreachable(renderer);
});
Separator.displayName = `${Namespace}.Separator`;
export const CheckboxItem: FC<AxoBaseMenu.MenuCheckboxItemProps> = memo(
props => {
const renderer = useMenuBuilderContext();
if (renderer === 'AxoDropdownMenu') {
return <AxoDropdownMenu.CheckboxItem {...props} />;
}
if (renderer === 'AxoContextMenu') {
return <AxoContextMenu.CheckboxItem {...props} />;
}
unreachable(renderer);
}
);
CheckboxItem.displayName = `${Namespace}.CheckboxItem`;
export const RadioGroup: FC<AxoBaseMenu.MenuRadioGroupProps> = memo(props => {
const renderer = useMenuBuilderContext();
if (renderer === 'AxoDropdownMenu') {
return <AxoDropdownMenu.RadioGroup {...props} />;
}
if (renderer === 'AxoContextMenu') {
return <AxoContextMenu.RadioGroup {...props} />;
}
unreachable(renderer);
});
RadioGroup.displayName = `${Namespace}.RadioGroup`;
export const RadioItem: FC<AxoBaseMenu.MenuRadioItemProps> = memo(props => {
const renderer = useMenuBuilderContext();
if (renderer === 'AxoDropdownMenu') {
return <AxoDropdownMenu.RadioItem {...props} />;
}
if (renderer === 'AxoContextMenu') {
return <AxoContextMenu.RadioItem {...props} />;
}
unreachable(renderer);
});
RadioItem.displayName = `${Namespace}.RadioItem`;
export const Sub: FC<AxoBaseMenu.MenuSubProps> = memo(props => {
const renderer = useMenuBuilderContext();
if (renderer === 'AxoDropdownMenu') {
return <AxoDropdownMenu.Sub {...props} />;
}
if (renderer === 'AxoContextMenu') {
return <AxoContextMenu.Sub {...props} />;
}
unreachable(renderer);
});
Sub.displayName = `${Namespace}.Sub`;
export const SubTrigger: FC<AxoBaseMenu.MenuSubTriggerProps> = memo(props => {
const renderer = useMenuBuilderContext();
if (renderer === 'AxoDropdownMenu') {
return <AxoDropdownMenu.SubTrigger {...props} />;
}
if (renderer === 'AxoContextMenu') {
return <AxoContextMenu.SubTrigger {...props} />;
}
unreachable(renderer);
});
SubTrigger.displayName = `${Namespace}.SubTrigger`;
export const SubContent: FC<AxoBaseMenu.MenuSubContentProps> = memo(props => {
const renderer = useMenuBuilderContext();
if (renderer === 'AxoDropdownMenu') {
return <AxoDropdownMenu.SubContent {...props} />;
}
if (renderer === 'AxoContextMenu') {
return <AxoContextMenu.SubContent {...props} />;
}
unreachable(renderer);
});
SubContent.displayName = `${Namespace}.SubContent`;
}