mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-05 01:10:49 +00:00
Init AxoMenuBuilder
This commit is contained in:
114
ts/axo/AxoMenuBuilder.stories.tsx
Normal file
114
ts/axo/AxoMenuBuilder.stories.tsx
Normal 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
207
ts/axo/AxoMenuBuilder.tsx
Normal 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`;
|
||||
}
|
||||
Reference in New Issue
Block a user