Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): add the contextmenu component #2552

Merged
merged 11 commits into from
Oct 9, 2024
5 changes: 5 additions & 0 deletions .changeset/young-ants-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@kadena/kode-ui': minor
---

add the contextMenu component
4 changes: 2 additions & 2 deletions packages/apps/explorer/src/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ export const NavBar: FC<
)}

<Media greaterThanOrEqual="md">
<SelectNetwork />
<SelectNetwork placement="bottom end" />
</Media>
</Stack>
<Stack flex={1}>{children}</Stack>

<Stack alignItems="center">
<Media lessThan="md">
<SelectNetwork />
<SelectNetwork placement="bottom start" />
</Media>
<ThemeToggle />
<GraphQLQueryDialog />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import { useNetwork } from '@/context/networksContext';
import { EVENT_NAMES, analyticsEvent } from '@/utils/analytics';
import { MonoSettings } from '@kadena/kode-icons/system';
import { Button, Select, SelectItem, Stack } from '@kadena/kode-ui';
import { MonoMoreVert, MonoSettings } from '@kadena/kode-icons/system';
import type { IContextMenuProps } from '@kadena/kode-ui';
import {
Button,
ContextMenu,
ContextMenuItem,
Stack,
Text,
} from '@kadena/kode-ui';
import type { FC } from 'react';
import React, { useState } from 'react';
import { Media } from '../Layout/media';
import { ConfigNetwork } from './ConfigNetwork';
import {
selectorButtonClass,
selectorClass,
selectorClassWrapper,
} from './style.css';

export const SelectNetwork: FC = () => {
interface IProps {
placement?: IContextMenuProps['placement'];
}

export const SelectNetwork: FC<IProps> = ({ placement = 'bottom end' }) => {
const { networks, activeNetwork, setActiveNetwork } = useNetwork();
const [isOpen, setIsOpen] = useState(false);

Expand All @@ -24,34 +40,34 @@ export const SelectNetwork: FC = () => {

return (
<>
<Stack>
<Stack className={selectorClassWrapper}>
<Media greaterThanOrEqual="md">
<Select
size="lg"
aria-label="Select network"
selectedKey={activeNetwork!.slug}
fontType="code"
onSelectionChange={handleSelectNetwork}
>
{
networks.map((network) => (
<SelectItem
key={network.slug ?? network.label}
textValue={network.label}
>
<Stack paddingInlineEnd="md" style={{ whiteSpace: 'nowrap' }}>
{network.label}
</Stack>
</SelectItem>
)) as any
}
</Select>
<Text className={selectorClass}>{activeNetwork.label}</Text>
</Media>
<Button
onPress={handlePress}
variant="transparent"
endVisual={<MonoSettings />}
/>
<ContextMenu
placement={placement}
trigger={
<Button
variant="transparent"
endVisual={<MonoMoreVert />}
className={selectorButtonClass}
/>
}
>
{networks.map((network) => (
<ContextMenuItem
aria-label={network.label}
key={network.slug ?? network.label}
label={network.label}
onClick={() => handleSelectNetwork(network.slug)}
/>
))}
<ContextMenuItem
label="Settings"
endVisual={<MonoSettings />}
onClick={handlePress}
/>
</ContextMenu>
</Stack>
{isOpen && <ConfigNetwork handleOpen={setIsOpen} />}
</>
Expand Down
38 changes: 37 additions & 1 deletion packages/apps/explorer/src/components/SelectNetwork/style.css.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,44 @@
import { token, tokens } from '@kadena/kode-ui/styles';
import { atoms, responsiveStyle, token, tokens } from '@kadena/kode-ui/styles';
import { style } from '@vanilla-extract/css';

export const cardVisualClass = style({
width: tokens.kda.foundation.size.n16,
height: tokens.kda.foundation.size.n16,
color: token('color.icon.brand.primary.default'),
});

export const selectorClassWrapper = style([
atoms({
display: 'flex',
alignItems: 'center',
borderRadius: 'xs',
borderStyle: 'solid',
paddingInlineStart: 'no',
borderColor: 'base.subtle',
}),
{ paddingInlineEnd: '1px', borderWidth: 0, direction: 'rtl' },
responsiveStyle({
md: {
paddingInlineStart: token('spacing.md'),
borderWidth: token('border.hairline'),
},
}),
]);

export const selectorClass = style([
{
display: 'flex',
alignItems: 'center',
height: '46px',
paddingInlineEnd: token('spacing.md'),
borderStyle: 'solid',
borderWidth: 0,
borderColor: token('color.border.base.subtle'),
borderInlineEndWidth: token('border.hairline'),
},
]);

export const selectorButtonClass = style({
height: '46px',
aspectRatio: '1/1',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import { MonoChevronRight, MonoMoreVert } from '@kadena/kode-icons/system';
import { Button } from '../Button';
import { Stack } from '../Layout';
import { iconControl } from './../../storyDecorators/iconControl';
import type { IContextMenuProps } from './ContextMenu';
import { ContextMenu } from './ContextMenu';
import type { IContextMenuItemProps } from './ContextMenuItem';
import { ContextMenuItem } from './ContextMenuItem';

const meta: Meta<IContextMenuItemProps & IContextMenuProps> = {
title: 'Overlays/ContextMenu',
parameters: {
status: { type: 'valid' },
docs: {
description: {
component: '',
},
},
},
argTypes: {
endVisual: iconControl,
label: {
type: 'string',
},
isDisabled: {
type: 'boolean',
},
placement: {
options: ['bottom start', 'bottom end', 'top start', 'top end'],
control: {
type: 'select',
},
},
},
};

export default meta;
type Story = StoryObj<IContextMenuItemProps & IContextMenuProps>;

export const Primary: Story = {
name: 'ContextMenu',
args: {
label: 'Hello world',
endVisual: <MonoChevronRight />,
isDisabled: false,
placement: 'bottom end',
},
render: ({ ...props }) => {
return (
<Stack
justifyContent="center"
alignItems="center"
width="100%"
style={{ height: '90dvh' }}
>
<ContextMenu
placement={props.placement}
trigger={<Button endVisual={<MonoMoreVert />} />}
>
<ContextMenuItem onClick={() => alert('click')} label="menu item" />
<ContextMenuItem onClick={() => alert('click 1')} {...props} />
<ContextMenuItem
onClick={() => alert('click 2')}
isDisabled
label="longer menu item 3"
/>
<ContextMenuItem
onClick={() => alert('click 3')}
label="very very long title menu item 4"
endVisual={<MonoMoreVert />}
/>
</ContextMenu>
</Stack>
);
},
};
61 changes: 61 additions & 0 deletions packages/libs/kode-ui/src/components/ContextMenu/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { FC, PropsWithChildren } from 'react';
import React, { useRef } from 'react';
import type { AriaMenuProps } from 'react-aria';
import { FocusScope, useMenu, useMenuTrigger } from 'react-aria';
import { useMenuTriggerState, useTreeState } from 'react-stately';
import { Popover } from './Popover';
import { contextMenuClass } from './style.css';

export type IContextMenuProps = PropsWithChildren & {
trigger: React.ReactElement;
placement?: 'bottom start' | 'bottom end' | 'top start' | 'top end';
};

export const ContextMenu: FC<IContextMenuProps> = ({
children,
trigger,
...props
}) => {
const ref = useRef(null);
const menuReref = useRef(null);
const state = useMenuTriggerState({});

const { menuTriggerProps, menuProps: menuWrapperProps } = useMenuTrigger(
{},
state,
ref,
);

const newMenuWrapperProps = { ...menuWrapperProps, autoFocus: false };

const treeState = useTreeState({
...newMenuWrapperProps,
} as AriaMenuProps<{}>);
const { menuProps } = useMenu({ ...newMenuWrapperProps }, treeState, ref);

return (
<>
{React.cloneElement(trigger, {
...trigger.props,
...menuTriggerProps,
ref,
})}
{state.isOpen && (
<>
<Popover {...props} triggerRef={ref} state={state}>
<div
ref={menuReref}
{...menuProps}
className={contextMenuClass}
onClick={state.close}
sstraatemans marked this conversation as resolved.
Show resolved Hide resolved
>
<FocusScope contain autoFocus restoreFocus>
{children}
</FocusScope>
</div>
</Popover>
</>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { FC, ReactElement } from 'react';
import React from 'react';
import type { AriaButtonProps } from 'react-aria';
import { Stack, Text } from '../';
import {
menuItemClass,
menuItemIconClass,
menuItemLabelClass,
} from './style.css';

export type IContextMenuItemProps = Pick<
AriaButtonProps<'button'>,
'aria-label'
> & {
label: string;
isDisabled?: boolean;
endVisual?: ReactElement;
onClick?: React.MouseEventHandler<HTMLDivElement>;
};

export const ContextMenuItem: FC<IContextMenuItemProps> = ({
label,
isDisabled = false,
endVisual,
...props
}) => {
return (
<Stack
as="button"
alignItems="center"
data-disabled={isDisabled}
className={menuItemClass}
{...props}
>
<Text transform="capitalize" className={menuItemLabelClass}>
{label}
</Text>
<Stack className={menuItemIconClass}>{endVisual ?? endVisual}</Stack>
</Stack>
);
};
39 changes: 39 additions & 0 deletions packages/libs/kode-ui/src/components/ContextMenu/Popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { FC } from 'react';
import React, { useRef } from 'react';
import type { AriaPopoverProps } from 'react-aria';
import { Overlay, usePopover } from 'react-aria';
import type { OverlayTriggerState } from 'react-stately';
import { underlayClass } from './style.css';

interface PopoverProps extends Omit<AriaPopoverProps, 'popoverRef'> {
children: React.ReactNode;
state: OverlayTriggerState;
}

export const Popover: FC<PopoverProps> = ({
children,
state,
offset = 8,
...props
}) => {
const popoverRef = useRef(null);
const { popoverProps, underlayProps } = usePopover(
{
...props,
offset,
popoverRef,
},
state,
);

return (
<>
<Overlay>
<div {...underlayProps} className={underlayClass} />
<div {...popoverProps} ref={popoverRef} className="popover">
{children}
</div>
</Overlay>
</>
);
};
2 changes: 2 additions & 0 deletions packages/libs/kode-ui/src/components/ContextMenu/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ContextMenu, IContextMenuProps } from './ContextMenu';
export { ContextMenuItem, IContextMenuItemProps } from './ContextMenuItem';
Loading
Loading