New MenuItem components (#1389)

* wip

* Finished

* Fix from review

* Fix lint

* Fixed toggle
This commit is contained in:
Lucas Bordeau 2023-09-01 11:35:19 +02:00 committed by GitHub
parent 2538ad1c6b
commit 240edda25c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 933 additions and 39 deletions

View File

@ -8,12 +8,11 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
import { useRecoilScopedFamilyState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedFamilyState'; import { useRecoilScopedFamilyState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedFamilyState';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { HotkeyEffect } from '../../utilities/hotkey/components/HotkeyEffect';
import { useDropdownButton } from '../hooks/useDropdownButton'; import { useDropdownButton } from '../hooks/useDropdownButton';
import { dropdownButtonCustomHotkeyScopeScopedFamilyState } from '../states/dropdownButtonCustomHotkeyScopeScopedFamilyState'; import { dropdownButtonCustomHotkeyScopeScopedFamilyState } from '../states/dropdownButtonCustomHotkeyScopeScopedFamilyState';
import { DropdownRecoilScopeContext } from '../states/recoil-scope-contexts/DropdownRecoilScopeContext'; import { DropdownRecoilScopeContext } from '../states/recoil-scope-contexts/DropdownRecoilScopeContext';
import { HotkeyEffect } from './HotkeyEffect';
const StyledContainer = styled.div` const StyledContainer = styled.div`
position: relative; position: relative;
z-index: 100; z-index: 100;

View File

@ -23,6 +23,7 @@ export {
IconCheckbox, IconCheckbox,
IconChevronDown, IconChevronDown,
IconChevronLeft, IconChevronLeft,
IconChevronRight,
IconChevronsRight, IconChevronsRight,
IconCircleDot, IconCircleDot,
IconCirclePlus, IconCirclePlus,

View File

@ -0,0 +1,3 @@
import { ComponentType } from 'react';
export type IconComponent = ComponentType<{ size: number }>;

View File

@ -0,0 +1,53 @@
import { useTheme } from '@emotion/react';
import { FloatingIconButton } from '@/ui/button/components/FloatingIconButton';
import { FloatingIconButtonGroup } from '@/ui/button/components/FloatingIconButtonGroup';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
import { MenuItemAccent } from '../types/MenuItemAccent';
export type MenuItemIconButton = {
Icon: IconComponent;
onClick: () => void;
};
export type MenuItemProps = {
LeftIcon?: IconComponent;
accent: MenuItemAccent;
text: string;
iconButtons?: MenuItemIconButton[];
className: string;
onClick?: () => void;
};
export function MenuItem({
LeftIcon,
accent,
text,
iconButtons,
className,
onClick,
}: MenuItemProps) {
const theme = useTheme();
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
return (
<StyledMenuItemBase onClick={onClick} className={className} accent={accent}>
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
{showIconButtons && (
<FloatingIconButtonGroup>
{iconButtons?.map(({ Icon, onClick }, index) => (
<FloatingIconButton
icon={<Icon size={theme.icon.size.sm} />}
key={index}
onClick={onClick}
/>
))}
</FloatingIconButtonGroup>
)}
</StyledMenuItemBase>
);
}

View File

@ -0,0 +1,71 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import {
StyledMenuItemBase,
StyledMenuItemLabel,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
const StyledMenuItemLabelText = styled(StyledMenuItemLabel)`
color: ${({ theme }) => theme.font.color.primary};
`;
const StyledBigIconContainer = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.transparent.light};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
flex-direction: row;
padding: ${({ theme }) => theme.spacing(1)};
`;
const StyledCommandText = styled.div`
color: ${({ theme }) => theme.font.color.light};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
`;
const StyledMenuItemCommandContainer = styled(StyledMenuItemBase)`
height: 24px;
`;
export type MenuItemProps = {
LeftIcon?: IconComponent;
text: string;
command: string;
className: string;
onClick?: () => void;
};
export function MenuItemCommand({
LeftIcon,
text,
command,
className,
onClick,
}: MenuItemProps) {
const theme = useTheme();
return (
<StyledMenuItemCommandContainer onClick={onClick} className={className}>
<StyledMenuItemLeftContent>
{LeftIcon && (
<StyledBigIconContainer>
<LeftIcon size={theme.icon.size.sm} />
</StyledBigIconContainer>
)}
<StyledMenuItemLabelText hasLeftIcon={!!LeftIcon}>
{text}
</StyledMenuItemLabelText>
</StyledMenuItemLeftContent>
<StyledCommandText>{command}</StyledCommandText>
</StyledMenuItemCommandContainer>
);
}

View File

@ -0,0 +1,43 @@
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { Checkbox } from '@/ui/input/checkbox/components/Checkbox';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
const StyledLeftContentWithCheckboxContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
`;
type OwnProps = {
LeftIcon?: IconComponent;
selected: boolean;
text: string;
className: string;
onSelectChange?: (selected: boolean) => void;
};
export function MenuItemMultiSelect({
LeftIcon,
text,
selected,
className,
onSelectChange,
}: OwnProps) {
function handleOnClick() {
onSelectChange?.(!selected);
}
return (
<StyledMenuItemBase className={className} onClick={handleOnClick}>
<StyledLeftContentWithCheckboxContainer>
<Checkbox checked={selected} />
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
</StyledLeftContentWithCheckboxContainer>
</StyledMenuItemBase>
);
}

View File

@ -0,0 +1,30 @@
import { useTheme } from '@emotion/react';
import { IconChevronRight } from '@/ui/icon';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
export type MenuItemProps = {
LeftIcon?: IconComponent;
text: string;
onClick?: () => void;
className: string;
};
export function MenuItemNavigate({
LeftIcon,
text,
className,
onClick,
}: MenuItemProps) {
const theme = useTheme();
return (
<StyledMenuItemBase onClick={onClick} className={className}>
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
<IconChevronRight size={theme.icon.size.sm} />
</StyledMenuItemBase>
);
}

View File

@ -0,0 +1,50 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck } from '@/ui/icon';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
const StyledMenuItemSelect = styled(StyledMenuItemBase)<{ selected: boolean }>`
${({ theme, selected }) => {
if (selected) {
return css`
background: ${theme.background.transparent.light};
&:hover {
background: ${theme.background.transparent.medium};
}
`;
}
}}
`;
type OwnProps = {
LeftIcon?: IconComponent;
selected: boolean;
text: string;
className: string;
onClick?: () => void;
};
export function MenuItemSelect({
LeftIcon,
text,
selected,
className,
onClick,
}: OwnProps) {
const theme = useTheme();
return (
<StyledMenuItemSelect
onClick={onClick}
className={className}
selected={selected}
>
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
{selected && <IconCheck size={theme.icon.size.sm} />}
</StyledMenuItemSelect>
);
}

View File

@ -0,0 +1,32 @@
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { Toggle } from '@/ui/input/toggle/components/Toggle';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
type OwnProps = {
LeftIcon?: IconComponent;
toggled: boolean;
text: string;
className: string;
onToggleChange?: (toggled: boolean) => void;
};
export function MenuItemToggle({
LeftIcon,
text,
toggled,
className,
onToggleChange,
}: OwnProps) {
function handleOnClick() {
onToggleChange?.(!toggled);
}
return (
<StyledMenuItemBase className={className} onClick={handleOnClick}>
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
<Toggle value={toggled} onChange={onToggleChange} />
</StyledMenuItemBase>
);
}

View File

@ -0,0 +1,113 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconBell } from '@/ui/icon';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { MenuItemAccent } from '../../types/MenuItemAccent';
import { MenuItem } from '../MenuItem';
const meta: Meta<typeof MenuItem> = {
title: 'UI/MenuItem/MenuItem',
component: MenuItem,
};
export default meta;
type Story = StoryObj<typeof MenuItem>;
export const Default: Story = {
args: {
text: 'Menu item text',
LeftIcon: IconBell,
accent: 'default',
iconButtons: [
{ Icon: IconBell, onClick: () => console.log('Clicked') },
{ Icon: IconBell, onClick: () => console.log('Clicked') },
],
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: { ...Default.args },
argTypes: {
accent: { control: false },
className: { control: false },
iconButtons: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'withIcon',
values: [true, false],
props: (withIcon: boolean) => ({
LeftIcon: withIcon ? IconBell : undefined,
}),
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'accents',
values: ['default', 'danger'] satisfies MenuItemAccent[],
props: (accent: MenuItemAccent) => ({ accent }),
},
{
name: 'states',
values: ['default', 'hover'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: state };
default:
return {};
}
},
},
{
name: 'iconButtons',
values: ['no icon button', 'two icon buttons'],
props: (choice: string) => {
switch (choice) {
case 'no icon button': {
return {
iconButtons: [],
};
}
case 'two icon buttons': {
return {
iconButtons: [
{
Icon: IconBell,
onClick: () =>
console.log('Clicked on first icon button'),
},
{
Icon: IconBell,
onClick: () =>
console.log('Clicked on second icon button'),
},
],
};
}
}
},
},
] as CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} as CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,78 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconBell } from '@/ui/icon';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { MenuItemCommand } from '../MenuItemCommand';
const meta: Meta<typeof MenuItemCommand> = {
title: 'UI/MenuItem/MenuItemCommand',
component: MenuItemCommand,
};
export default meta;
type Story = StoryObj<typeof MenuItemCommand>;
export const Default: Story = {
args: {
text: 'First option',
command: '⌘ 1',
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: { LeftIcon: IconBell, text: 'Menu item', command: '⌘1' },
argTypes: {
className: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'withIcon',
values: [true, false],
props: (withIcon: boolean) => ({
LeftIcon: withIcon ? IconBell : undefined,
}),
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'selected',
values: [true, false],
props: (selected: boolean) => ({ selected }),
labels: (selected: boolean) =>
selected ? 'Selected' : 'Not selected',
},
{
name: 'states',
values: ['default', 'hover'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: state };
default:
return {};
}
},
},
] as CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} as CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,77 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconBell } from '@/ui/icon';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { MenuItemMultiSelect } from '../MenuItemMultiSelect';
const meta: Meta<typeof MenuItemMultiSelect> = {
title: 'UI/MenuItem/MenuItemMultiSelect',
component: MenuItemMultiSelect,
};
export default meta;
type Story = StoryObj<typeof MenuItemMultiSelect>;
export const Default: Story = {
args: {
text: 'First option',
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: { LeftIcon: IconBell, text: 'Menu item' },
argTypes: {
className: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'withIcon',
values: [true, false],
props: (withIcon: boolean) => ({
LeftIcon: withIcon ? IconBell : undefined,
}),
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'selected',
values: [true, false],
props: (selected: boolean) => ({ selected }),
labels: (selected: boolean) =>
selected ? 'Selected' : 'Not selected',
},
{
name: 'states',
values: ['default', 'hover'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: state };
default:
return {};
}
},
},
] as CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} as CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,70 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconBell } from '@/ui/icon';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { MenuItemNavigate } from '../MenuItemNavigate';
const meta: Meta<typeof MenuItemNavigate> = {
title: 'UI/MenuItem/MenuItemNavigate',
component: MenuItemNavigate,
};
export default meta;
type Story = StoryObj<typeof MenuItemNavigate>;
export const Default: Story = {
args: {
text: 'First option',
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: { LeftIcon: IconBell, text: 'Menu item' },
argTypes: {
className: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'withIcon',
values: [true, false],
props: (withIcon: boolean) => ({
LeftIcon: withIcon ? IconBell : undefined,
}),
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'states',
values: ['default', 'hover'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: state };
default:
return {};
}
},
},
] as CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} as CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,78 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconBell } from '@/ui/icon';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { MenuItemSelect } from '../MenuItemSelect';
const meta: Meta<typeof MenuItemSelect> = {
title: 'UI/MenuItem/MenuItemSelect',
component: MenuItemSelect,
};
export default meta;
type Story = StoryObj<typeof MenuItemSelect>;
export const Default: Story = {
args: {
text: 'First option',
LeftIcon: IconBell,
},
argTypes: {
className: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: { LeftIcon: IconBell, text: 'Menu item' },
argTypes: {
className: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'withIcon',
values: [true, false],
props: (withIcon: boolean) => ({
LeftIcon: withIcon ? IconBell : undefined,
}),
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'states',
values: ['default', 'hover', 'selected', 'hover+selected'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: 'hover' };
case 'selected':
return { selected: true };
case 'hover+selected':
return { className: 'hover', selected: true };
default:
return {};
}
},
},
] as CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} as CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,76 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconBell } from '@/ui/icon';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { MenuItemToggle } from '../MenuItemToggle';
const meta: Meta<typeof MenuItemToggle> = {
title: 'UI/MenuItem/MenuItemToggle',
component: MenuItemToggle,
};
export default meta;
type Story = StoryObj<typeof MenuItemToggle>;
export const Default: Story = {
args: {
text: 'First option',
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: { LeftIcon: IconBell, text: 'Menu item' },
argTypes: {
className: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'withIcon',
values: [true, false],
props: (withIcon: boolean) => ({
LeftIcon: withIcon ? IconBell : undefined,
}),
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'toggled',
values: [true, false],
props: (toggled: boolean) => ({ toggled }),
labels: (toggled: boolean) => (toggled ? 'Toggled' : 'Not toggled'),
},
{
name: 'states',
values: ['default', 'hover'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: state };
default:
return {};
}
},
},
] satisfies CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} satisfies CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,24 @@
import { useTheme } from '@emotion/react';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import {
StyledMenuItemLabel,
StyledMenuItemLeftContent,
} from './StyledMenuItemBase';
type OwnProps = {
LeftIcon?: IconComponent;
text: string;
};
export function MenuItemLeftContent({ LeftIcon, text }: OwnProps) {
const theme = useTheme();
return (
<StyledMenuItemLeftContent>
{LeftIcon && <LeftIcon size={theme.icon.size.md} />}
<StyledMenuItemLabel hasLeftIcon={!!LeftIcon}>{text}</StyledMenuItemLabel>
</StyledMenuItemLeftContent>
);
}

View File

@ -0,0 +1,85 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { hoverBackground } from '@/ui/theme/constants/effects';
import { MenuItemAccent } from '../../types/MenuItemAccent';
export type MenuItemBaseProps = {
accent?: MenuItemAccent;
};
export const StyledMenuItemBase = styled.li<MenuItemBaseProps>`
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
--vertical-padding: ${({ theme }) => theme.spacing(2)};
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.sm};
cursor: pointer;
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.sm};
gap: ${({ theme }) => theme.spacing(2)};
height: calc(32px - 2 * var(--vertical-padding));
justify-content: space-between;
padding: var(--vertical-padding) var(--horizontal-padding);
${hoverBackground};
${({ theme, accent }) => {
switch (accent) {
case 'danger': {
return css`
color: ${theme.font.color.danger};
&:hover {
background: ${theme.background.transparent.danger};
}
`;
}
case 'default':
default: {
return css`
color: ${theme.font.color.secondary};
`;
}
}
}}
position: relative;
user-select: none;
width: calc(100% - 2 * var(--horizontal-padding));
`;
export const StyledMenuItemLabel = styled.div<{ hasLeftIcon: boolean }>`
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
padding-left: ${({ theme, hasLeftIcon }) =>
hasLeftIcon ? '' : theme.spacing(1)};
`;
export const StyledNoIconFiller = styled.div`
width: ${({ theme }) => theme.spacing(1)};
`;
export const StyledMenuItemLeftContent = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(1)};
`;
export const StyledMenuItemRightContent = styled.div`
align-items: center;
display: flex;
flex-direction: row;
`;

View File

@ -0,0 +1 @@
export type MenuItemAccent = 'default' | 'danger';

View File

@ -56,8 +56,9 @@ const StyledRowContainer = styled.div`
gap: ${({ theme }) => theme.spacing(2)}; gap: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledElementContainer = styled.div` const StyledElementContainer = styled.div<{ width: number }>`
display: flex; display: flex;
${({ width }) => width && `min-width: ${width}px;`}
`; `;
const StyledCellContainer = styled.div` const StyledCellContainer = styled.div`
@ -67,64 +68,73 @@ const StyledCellContainer = styled.div`
padding: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(2)};
`; `;
const emptyVariable = { const emptyDimension = {
name: '', name: '',
values: [undefined], values: [undefined],
props: () => ({}), props: () => ({}),
} as CatalogDimension;
export type CatalogDimension = {
name: string;
values: any[];
props: (value: any) => Record<string, any>;
labels?: (value: any) => string;
};
export type CatalogOptions = {
elementContainer?: {
width?: number;
};
}; };
export const CatalogDecorator: Decorator = (Story, context) => { export const CatalogDecorator: Decorator = (Story, context) => {
const { const {
catalog: { dimensions, options }, catalog: { dimensions, options },
} = context.parameters; } = context.parameters;
const [ const [
variable1, dimension1,
variable2 = emptyVariable, dimension2 = emptyDimension,
variable3 = emptyVariable, dimension3 = emptyDimension,
variable4 = emptyVariable, dimension4 = emptyDimension,
] = dimensions; ] = dimensions as CatalogDimension[];
return ( return (
<StyledContainer> <StyledContainer>
{variable4.values.map((value4: string) => ( {dimension4.values.map((value4: any) => (
<StyledColumnContainer key={value4}> <StyledColumnContainer key={value4}>
{(variable4.labels?.(value4) || value4) && ( <StyledColumnTitle>
<StyledColumnTitle> {dimension4.labels?.(value4) ??
{variable4.labels?.(value4) || value4} (typeof value4 === 'string' ? value4 : '')}
</StyledColumnTitle> </StyledColumnTitle>
)} {dimension3.values.map((value3: any) => (
{variable3.values.map((value3: string) => (
<StyledRowsContainer key={value3}> <StyledRowsContainer key={value3}>
{(variable3.labels?.(value3) || value3) && ( <StyledRowsTitle>
<StyledRowsTitle> {dimension3.labels?.(value3) ??
{variable3.labels?.(value3) || value3} (typeof value3 === 'string' ? value3 : '')}
</StyledRowsTitle> </StyledRowsTitle>
)} {dimension2.values.map((value2: any) => (
{variable2.values.map((value2: string) => (
<StyledRowContainer key={value2}> <StyledRowContainer key={value2}>
{(variable2.labels?.(value2) || value2) && ( <StyledRowTitle>
<StyledRowTitle> {dimension2.labels?.(value2) ??
{variable2.labels?.(value2) || value2} (typeof value2 === 'string' ? value2 : '')}
</StyledRowTitle> </StyledRowTitle>
)} {dimension1.values.map((value1: any) => (
{variable1.values.map((value1: string) => (
<StyledCellContainer key={value1} id={value1}> <StyledCellContainer key={value1} id={value1}>
{(variable1.labels?.(value1) || value1) && ( <StyledElementTitle>
<StyledElementTitle> {dimension1.labels?.(value1) ??
{variable1.labels?.(value1) || value1} (typeof value1 === 'string' ? value1 : '')}
</StyledElementTitle> </StyledElementTitle>
)}
<StyledElementContainer <StyledElementContainer
{...options?.StyledelementContainer} width={options?.elementContainer?.width}
> >
<Story <Story
args={{ args={{
...context.args, ...context.args,
...variable1.props(value1), ...dimension1.props(value1),
...variable2.props(value2), ...dimension2.props(value2),
...variable3.props(value3), ...dimension3.props(value3),
...variable4.props(value4), ...dimension4.props(value4),
}} }}
/> />
</StyledElementContainer> </StyledElementContainer>