update popover

This commit is contained in:
Nate Kelley 2025-03-03 20:21:13 -07:00
parent b433361929
commit 132c858951
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
4 changed files with 98 additions and 288 deletions

View File

@ -0,0 +1,79 @@
import type { Meta, StoryObj } from '@storybook/react';
import { PolicyCheck } from './PolicyCheck';
import { fn } from '@storybook/test';
const meta = {
title: 'Features/Auth/PolicyCheck',
component: PolicyCheck,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
password: { control: 'text' },
show: { control: 'boolean' },
placement: {
control: 'select',
options: ['top', 'right', 'bottom', 'left']
},
onCheckChange: { action: 'onCheckChange' }
}
} satisfies Meta<typeof PolicyCheck>;
export default meta;
type Story = StoryObj<typeof PolicyCheck>;
export const Default: Story = {
args: {
password: '',
show: true,
placement: 'left',
onCheckChange: fn()
}
};
export const ValidPassword: Story = {
args: {
password: 'Test123!@#',
show: true,
placement: 'left',
onCheckChange: fn()
}
};
export const InvalidPassword: Story = {
args: {
password: 'weak',
show: true,
placement: 'left',
onCheckChange: fn()
}
};
export const Hidden: Story = {
args: {
password: 'Test123!@#',
show: false,
placement: 'left',
onCheckChange: fn()
}
};
export const DifferentPlacement: Story = {
args: {
password: 'Test123!@#',
show: true,
placement: 'right',
onCheckChange: fn()
}
};
export const WithCustomChildren: Story = {
args: {
password: 'Test123!@#',
show: true,
placement: 'left',
onCheckChange: fn(),
children: <span className="text-blue-500">Custom trigger element</span>
}
};

View File

@ -1,6 +1,8 @@
import { AppMaterialIcons } from '@/components/ui';
import { CircleCheck, CircleXmark, CircleInfo } from '@/components/ui/icons';
import React, { useEffect, useMemo } from 'react';
import { AppPopover, Text } from '@/components/ui';
import { Text } from '@/components/ui/typography';
import { Popover } from '@/components/ui/tooltip/Popover';
import { Button } from '@/components/ui/buttons/Button';
export const PolicyCheck: React.FC<{
password: string;
@ -72,9 +74,13 @@ export const PolicyCheck: React.FC<{
return (
<div className="flex items-center space-x-1">
{passwordGood ? (
<AppMaterialIcons className="text-green-600" icon={'check_circle'} />
<div className="text-success-foreground">
<CircleCheck />
</div>
) : (
<AppMaterialIcons className="text-red-600" icon={'close'} />
<div className="text-danger-foreground">
<CircleXmark />
</div>
)}
<Text size="sm">{text}</Text>
</div>
@ -82,25 +88,21 @@ export const PolicyCheck: React.FC<{
};
return (
<AppPopover
<Popover
open={show === false ? false : undefined}
placement={placement}
side={'left'}
align={'start'}
content={
<div className="flex flex-col p-1.5">
<div className="flex flex-col gap-y-1 p-1.5">
{items.map((item, index) => (
<PasswordCheck key={index} passwordGood={item.check} text={item.text} />
))}
</div>
}>
<div className="flex w-full cursor-pointer items-center space-x-1">
{children ? (
children
) : allCompleted ? (
<AppMaterialIcons icon={'check_circle'} size={12} />
) : (
<AppMaterialIcons icon={'info'} size={12} />
{!children && (
<Button variant={'ghost'} prefix={allCompleted ? <CircleCheck /> : <CircleInfo />}></Button>
)}
</div>
</AppPopover>
{children && children}
</Popover>
);
};

View File

@ -1,270 +0,0 @@
'use client';
import React, { useLayoutEffect, useMemo } from 'react';
import { Divider, Dropdown, MenuProps, DropdownProps, Input } from 'antd';
import { useAntToken } from '@/styles/useAntToken';
import { createStyles } from 'antd-style';
import { useMemoizedFn } from 'ahooks';
type Item = {
label: React.ReactNode;
key?: string;
index?: number;
onClick?: () => void;
icon?: React.ReactNode;
disabled?: boolean;
link?: string;
};
export interface AppPopoverMenuProps extends DropdownProps {
headerContent?: React.ReactNode | string;
children: React.ReactNode;
footerContent?: React.ReactNode;
items?: Item[];
contentWidth?: number;
selectedItems?: string[];
hideCheckbox?: boolean;
doNotSortSelected?: boolean;
disabled?: boolean;
}
const useAppPopoverMenuStyles = createStyles(({ css, token }) => ({
container: css`
.busterv2-dropdown-menu-item {
&:hover {
.checkbox-container {
opacity: 1 !important;
}
}
}
`
}));
export const AppPopoverMenu: React.FC<AppPopoverMenuProps> = React.memo(
({
items,
children,
headerContent,
footerContent,
contentWidth = 205,
selectedItems = [],
hideCheckbox = false,
destroyPopupOnHide = true,
doNotSortSelected = false,
disabled = false,
...props
}) => {
const token = useAntToken();
const { styles, cx } = useAppPopoverMenuStyles();
const [filterText, setFilterText] = React.useState('');
const footerContentContainer = !!footerContent ? (
<div className="px-2 py-1.5">{footerContent}</div>
) : null;
const filterItems = useMemoizedFn((items: Item[]) => {
const filterTextLowerCase = filterText.toLowerCase();
return items.filter((item) => {
if (filterText === '') return true;
const keyLowerCase = item.key?.toString().toLowerCase();
const labelLowerCase = item.label?.toString().toLowerCase();
return (
keyLowerCase?.includes(filterTextLowerCase) ||
labelLowerCase?.includes(filterTextLowerCase)
);
});
});
const _selectedItems = useMemo(
() =>
filterItems(
doNotSortSelected
? []
: items?.filter((item) => selectedItems.includes(item.key as string)) || []
),
[doNotSortSelected, filterText, items, selectedItems]
);
const notSelectedItems = useMemo(
() =>
filterItems(
doNotSortSelected
? items || []
: items?.filter((item) => !selectedItems.includes(item.key as string)) || []
),
[doNotSortSelected, filterText, items, selectedItems]
);
const createItem = useMemoizedFn((item: Item, index: number) => ({
disabled: item.disabled,
icon: item.icon,
index,
label: (
<></>
// <AppSelectItem
// disabled={item.disabled}
// index={item.index}
// content={item?.label}
// hideCheckbox={hideCheckbox}
// link={item.link}
// onClick={() => {
// item.onClick && item.onClick();
// }}
// selected={selectedItems.includes(item.key as string)}
// />
),
key: (item?.key as string) || (index.toString() as string)
}));
const selectedItemsInternal: Item[] = useMemo(
() => _selectedItems.map(createItem),
[_selectedItems, createItem]
);
const internalItems: MenuProps['items'] = useMemo(
() => notSelectedItems.map(createItem),
[notSelectedItems, createItem]
);
const dropdownRender = useMemoizedFn((menu: React.ReactNode) => {
return (
<div
style={{
minWidth: contentWidth,
backgroundColor: token.colorBgElevated,
borderRadius: token.borderRadiusLG,
boxShadow: token.boxShadowSecondary
}}>
<HeaderContentContainer
headerContent={headerContent}
filterText={filterText}
setFilterText={setFilterText}
/>
{!!headerContent && <Divider />}
{!!selectedItemsInternal.length && (
<>
<div className="p-1">
{selectedItemsInternal.map((item, index) => (
<SelectedItem {...item} key={String(index)} />
))}
</div>
{!!internalItems.length && <Divider />}
</>
)}
{!!menu &&
React.cloneElement(menu as React.ReactElement, {
style: {
boxShadow: 'none'
}
})}
{!!footerContentContainer && !!items?.length && <Divider />}
{footerContentContainer}
</div>
);
});
const memoizedMenu = useMemo(() => {
return {
className: cx(styles.container),
rootClassName: '',
items: internalItems,
selectable: true,
selectedKeys: selectedItems
};
}, [internalItems, selectedItems]);
useLayoutEffect(() => {
if (!props.open) {
setFilterText('');
}
}, [props.open]);
return (
<Dropdown
{...props}
disabled={disabled}
destroyPopupOnHide={destroyPopupOnHide}
open={props.open}
menu={memoizedMenu}
dropdownRender={dropdownRender}>
{children}
</Dropdown>
);
}
);
AppPopoverMenu.displayName = 'AppPopoverMenu';
const useSelectedItemStyles = createStyles(({ css, token }) => ({
container: css`
cursor: pointer;
display: flex;
items-center: center;
height: 28px;
padding: 0 0 0 12px;
border-radius: ${token.borderRadius}px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
background: ${token.controlItemBgHover};
}
`,
icon: {
color: token.colorIcon,
fontSize: token.fontSizeIcon
}
}));
const SelectedItem: React.FC<Omit<Item, 'key'>> = ({ label, onClick, icon }) => {
const { styles, cx } = useSelectedItemStyles();
return (
<div onClick={onClick} className={cx(styles.container, 'relative w-full')}>
{icon && <span className={cx(styles.icon, 'mr-2 flex items-center')}>{icon}</span>}
{label}
</div>
);
};
const HeaderContentContainer: React.FC<{
headerContent?: React.ReactNode | string;
filterText: string;
setFilterText: (text: string) => void;
}> = ({ headerContent, setFilterText, filterText }) => {
const token = useAntToken();
const onChange = useMemoizedFn((e: React.ChangeEvent<HTMLInputElement>) => {
e.stopPropagation();
e.preventDefault();
setFilterText(e.target.value);
});
if (!headerContent) return;
const headerIsText = typeof headerContent === 'string';
return (
<div
className="flex items-center px-3 py-1"
style={{
color: token.colorTextDescription,
height: 38
}}>
{headerIsText ? (
<Input
className="pl-0!"
variant="borderless"
placeholder={headerContent as string}
value={filterText}
onChange={onChange}
/>
) : (
headerContent
)}
</div>
);
};

View File

@ -1,5 +1,4 @@
export * from './AppPopover';
export * from './AppPopoverMenu';
export * from './Tooltip';
import { Tooltip } from './Tooltip';