Skip to content

Commit

Permalink
Merge pull request #94 from 8845musign/imple-icon-component
Browse files Browse the repository at this point in the history
Imple `<Icon>` component
  • Loading branch information
takanorip committed Jun 3, 2024
2 parents ab21211 + 5466913 commit 5b6c85e
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 26 deletions.
17 changes: 9 additions & 8 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,15 @@ module.exports = {
'unused-imports/no-unused-imports': 'error',
'import/no-unresolved': 'off',
'no-console': 'error',
"@typescript-eslint/no-unused-vars": [
"error",
'@typescript-eslint/no-unused-vars': [
'error',
{
"argsIgnorePattern": "_",
"varsIgnorePattern": "_",
"caughtErrorsIgnorePattern": "_",
"destructuredArrayIgnorePattern": "_"
}
]
argsIgnorePattern: '_',
varsIgnorePattern: '_',
caughtErrorsIgnorePattern: '_',
destructuredArrayIgnorePattern: '_',
},
],
'import/namespace': [2, { allowComputed: true }],
},
};
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
"peerDependencies": {
"@headlessui/react": ">1.7.0 <2.0.0",
"@ubie/design-tokens": "^0.0.10 || ^0.1.0",
"@ubie/ubie-icons": ">0.5.0",
"clsx": ">1.2.0",
"react": "^17 || ^18",
"react-dom": "^17 || ^18"
Expand Down
2 changes: 1 addition & 1 deletion src/components/Button/Button.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@

.icon > * {
width: var(--icon-size);
height: var(--sicon-size);
height: var(--icon-size);
}

.fixedIcon {
Expand Down
6 changes: 6 additions & 0 deletions src/components/Icon/Icon.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.icon {
width: var(--size);
height: var(--size);
color: var(--text-color);
vertical-align: bottom;
}
24 changes: 24 additions & 0 deletions src/components/Icon/Icon.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { composeStory } from '@storybook/react';
import { render, screen } from '@testing-library/react';
import Meta, { WithCustomDataAttribute, WithId } from './Icon.stories';

const WithCustomDataAttributeStory = composeStory(WithCustomDataAttribute, Meta);
const WithIdStory = composeStory(WithId, Meta);

describe('Icon', () => {
test('Add custom data attributes', async () => {
render(<WithCustomDataAttributeStory />);

const iconElement = await screen.findByTestId('testid');

expect(iconElement).toBeInTheDocument();
});

test('Add id', async () => {
render(<WithIdStory />);

const iconElement = await screen.findByTestId('testid');

expect(iconElement).toHaveAttribute('id', 'icon-id');
});
});
87 changes: 87 additions & 0 deletions src/components/Icon/Icon.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentProps } from 'react';
import { Icon, Flex, Box, Stack } from '../../index';

export default {
component: Icon,
} satisfies Meta<typeof Icon>;

const defaultArgs: Partial<ComponentProps<typeof Icon>> = {
icon: 'UbieIcon',
};

type Story = StoryObj<typeof Icon>;

export const Default: Story = {
render: (args) => <Icon {...args} />,
args: defaultArgs,
};

export const Size: Story = {
render: (args) => (
<>
<Flex alignItems="center" spacing="sm">
<Icon {...args} size="xs" />
<Icon {...args} size="sm" />
<Icon {...args} size="md" />
<Icon {...args} size="lg" />
<Icon {...args} size="xl" />
<Icon {...args} size="2xl" />
<Icon {...args} size="3xl" />
<Icon {...args} size="4xl" />
</Flex>
</>
),
args: defaultArgs,
};

export const Color: Story = {
render: (args) => (
<Flex alignItems="center" spacing="sm">
<Icon {...args} color="primary" />
<Icon {...args} color="accent" />
<Icon {...args} color="main" />
<Icon {...args} color="sub" />
<Icon {...args} color="alert" />
<Icon {...args} color="link" />
<Icon {...args} color="linkSub" />
<Icon {...args} color="disabled" />
<Box pt="xxs" pr="xxs" pb="xxs" pl="xxs" backgroundColor="primaryDarken">
<Icon {...args} color="white" />
</Box>
</Flex>
),
args: defaultArgs,
};

export const CaseOnlyIcon: Story = {
render: () => (
<Stack spacing="sm">
<p>
ラベルや付随する文章を伴わずにアイコン単独で使う場合、アイコンが保つ意味を<code>label</code> propで付与します
<br />
例: アイコンボタンなど
</p>
<div>
<Icon icon="SetupIcon" label="設定" />
</div>
</Stack>
),
};

export const WithCustomDataAttribute: Story = {
render: (args) => <Icon {...args} />,
args: {
...defaultArgs,
'data-testid': 'testid',
},
};

export const WithId: Story = {
render: (args) => <Icon {...args} />,
args: {
...defaultArgs,
id: 'icon-id',
'data-testid': 'testid',
},
};
83 changes: 83 additions & 0 deletions src/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as Icons from '@ubie/ubie-icons';
import styles from './Icon.module.css';
import { CustomDataAttributeProps } from '../../types/attributes';
import { TextColor } from '../../types/style';
import { colorVariable } from '../../utils/style';
import type { FC, CSSProperties } from 'react';

type Icon = keyof typeof Icons;

type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';

const toIconSizeEmValue = (size: IconSize): string => {
switch (size) {
case 'xs':
return '1em';
case 'sm':
return '1.25em';
case 'md':
return '1.5em';
case 'lg':
return '1.75em';
case 'xl':
return '2em';
case '2xl':
return '4em';
case '3xl':
return '5em';
case '4xl':
return '6.5em';
default:
// eslint-disable-next-line no-case-declarations
const _: never = size;
throw new Error(`Unknown size: ${_}`);
}
};

type Props = {
/**
* アイコンの種類
*/
icon: Icon;
/**
* 色。指定しない場合はinheritとなり、親要素のfont-colorを継承します
*/
color?: TextColor;
/**
* サイズ
* @default md
*/
size?: IconSize;
/**
* ネイティブの`id`属性。ページで固有のIDを指定
*/
id?: string;
/**
* アイコンが何を表すかを説明するテキスト
* 単に装飾的なアイコンの場合は指定しない
*/
label?: string;
} & CustomDataAttributeProps;

/**
* アイコンコンポーネント。labelを指定しない場合は単に装飾的なアイコンであるとみなされ、aria-hiddenが付与されます
*/
export const Icon: FC<Props> = ({ icon, color, size = 'md', label, ...otherProps }) => {
const IconComponent = Icons[icon];
const _sizeValue = toIconSizeEmValue(size);
return (
<IconComponent
role="img"
aria-hidden={label === undefined || label === '' ? true : undefined}
aria-label={label}
className={styles.icon}
style={
{
...colorVariable(color),
'--size': _sizeValue,
} as CSSProperties
}
{...otherProps}
/>
);
};
7 changes: 6 additions & 1 deletion src/components/LinkCard/LinkCard.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@

.icon {
flex: none;
font-size: var(--icon-size);
color: var(--color-primary);
}

.icon > * {
width: var(--icon-size);
height: var(--icon-size);
vertical-align: bottom;
}

.text {
display: flex;
flex: 1;
Expand Down
28 changes: 23 additions & 5 deletions src/components/LinkCard/LinkCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import { ArrowBRightIcon } from '@ubie/ubie-icons';
import clsx from 'clsx';
import { cloneElement, forwardRef } from 'react';
import { cloneElement, forwardRef, isValidElement } from 'react';
import styles from './LinkCard.module.css';
import { CustomDataAttributeProps } from '../../types/attributes'; // 追加したインポート
import { CustomDataAttributeProps } from '../../types/attributes';
import type { ComponentType, ReactElement, ReactNode } from 'react';

type Props = {
Expand Down Expand Up @@ -37,11 +37,29 @@ type Props = {
/**
* アイコン
*/
icon?: ComponentType<{ className?: string }>;
icon?: ComponentType | ReactElement;
} & CustomDataAttributeProps;

// ref https://github.com/microsoft/TypeScript/issues/53178
const _isValidElement = (el: ComponentType | ReactElement): el is ReactElement => {
return isValidElement(el);
};

const renderPropIcon = (icon: ComponentType | ReactElement) => {
if (icon == null) {
return null;
}

if (_isValidElement(icon)) {
return icon;
}

const IconComponent = icon;
return <IconComponent />;
};

export const LinkCard = forwardRef<HTMLAnchorElement, Props>(
({ title, size = 'medium', className, icon: IconComponent, description, render, ...props }, forwardedRef) => {
({ title, size = 'medium', className, icon, description, render, ...props }, forwardedRef) => {
const cls = clsx(styles[size], styles.card, className);

/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
Expand All @@ -56,7 +74,7 @@ export const LinkCard = forwardRef<HTMLAnchorElement, Props>(
ref: forwardedRef,
},
<>
{IconComponent && <IconComponent className={styles.icon} />}
{icon != null && <span className={styles.icon}>{renderPropIcon(icon)}</span>}
<div className={styles.text}>
<p className={styles.title}>{title}</p>
{description && <p className={styles.description}>{description}</p>}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { Flex } from './components/Flex/Flex';
export { HelperMessage } from './components/HelperMessage/HelperMessage';
export { Checkbox } from './components/Checkbox/Checkbox';
export { CheckboxGroup } from './components/CheckboxGroup/CheckboxGroup';
export { Icon } from './components/Icon/Icon';
export { Input } from './components/Input/Input';
export { Label } from './components/Label/Label';
export { LinkCard } from './components/LinkCard/LinkCard';
Expand Down
16 changes: 8 additions & 8 deletions src/stories/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Meta, StoryObj } from '@storybook/react';
import { BlankLinkIcon, UbieIcon, TrashIcon } from '@ubie/ubie-icons';
import { Button, Stack } from '../';
import { BlankLinkIcon, TrashIcon } from '@ubie/ubie-icons';
import { Button, Stack, Icon } from '../';
import type { ComponentProps } from 'react';

export default {
Expand Down Expand Up @@ -55,12 +55,12 @@ export const WithIcon: Story = {
<dt style={{ fontWeight: 'bold' }}>Default Position</dt>
<dd style={{ margin: 0 }}>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-start', gap: '32px' }}>
<Button icon={<UbieIcon />} {...defaultArgs} />
<Button icon={<UbieIcon />} {...defaultArgs} variant="secondary" />
<Button icon={<UbieIcon />} {...defaultArgs} variant="accent" />
<Button icon={<UbieIcon />} {...defaultArgs} variant="alert" />
<Button icon={<UbieIcon />} {...defaultArgs} variant="text" />
<Button icon={<TrashIcon />} {...defaultArgs} variant="textAlert" />
<Button icon={<Icon icon="UbieIcon" />} {...defaultArgs} />
<Button icon={<Icon icon="UbieIcon" />} {...defaultArgs} variant="secondary" />
<Button icon={<Icon icon="UbieIcon" />} {...defaultArgs} variant="accent" />
<Button icon={<Icon icon="UbieIcon" />} {...defaultArgs} variant="alert" />
<Button icon={<Icon icon="UbieIcon" />} {...defaultArgs} variant="text" />
<Button icon={<Icon icon="TrashIcon" />} {...defaultArgs} variant="textAlert" />
</div>
</dd>
</Stack>
Expand Down
9 changes: 7 additions & 2 deletions src/stories/LinkCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Meta, StoryObj } from '@storybook/react';
import { HospitalIcon } from '@ubie/ubie-icons';
import { LinkCard, Stack } from '..';
import { LinkCard, Stack, Icon } from '..';

export default {
component: LinkCard,
Expand All @@ -23,7 +23,12 @@ export const Default: Story = {
};

export const WithIcon: Story = {
render: (args) => <LinkCard {...args} href="https://vitals.ubie.life/" icon={HospitalIcon} />,
render: (args) => (
<Stack spacing="md">
<LinkCard {...args} href="https://vitals.ubie.life/" icon={HospitalIcon} />
<LinkCard {...args} href="https://vitals.ubie.life/" icon={<Icon icon="HospitalIcon" />} />
</Stack>
),
args: defaultArgs,
};

Expand Down

0 comments on commit 5b6c85e

Please sign in to comment.