Skip to content

Commit

Permalink
feat(Tooltip): 追加 (#1486)
Browse files Browse the repository at this point in the history
* Add Tooltip component

* Fix a11y issues

* Fix Text component to receive ref prop

* Add PropsCascader utility component

* Add prepareForSlot util from @mui/base

* Add exports tooltip

* Update deps lock file

* Fmt

* Add change log
  • Loading branch information
Qs-F authored Jan 4, 2024
1 parent 7c5495f commit e12d109
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 46 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-games-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@4design/for-ui": patch
---

feat: Tooltipを追加
5 changes: 4 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 packages/for-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from './textArea';
export * from './textField';
export * from './tabs';
export * from './table';
export * from './tooltip';
2 changes: 1 addition & 1 deletion packages/for-ui/src/skeleton/Skeleton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Text } from '../text';
import { Skeleton, SkeletonX } from './Skeleton';

export default {
title: 'Data Display / Skeleton',
title: 'Feedback / Skeleton',
component: Skeleton,
decorators: [
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
21 changes: 21 additions & 0 deletions packages/for-ui/src/system/PropsCascader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Children, cloneElement, FC, ForwardedRef, forwardRef, HTMLAttributes, isValidElement, ReactNode } from 'react';

type PropsCascaderProps<T extends HTMLAttributes<HTMLElement>> = T & {
children: ReactNode;
};

type PropsCascaderComponent = <T extends HTMLAttributes<HTMLElement>>(props: PropsCascaderProps<T>) => ReturnType<FC>;

export const PropsCascader: PropsCascaderComponent = forwardRef(
<T extends HTMLAttributes<HTMLElement>>(
{ children, ...props }: PropsCascaderProps<T>,
ref: ForwardedRef<HTMLElement>,
) => {
return Children.map(children, (child: ReactNode) => {
if (!isValidElement(child)) {
return child;
}
return cloneElement(child, { ...props, ...child.props, ref });
});
},
);
85 changes: 41 additions & 44 deletions packages/for-ui/src/text/Text.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react';
import { ElementType, FC, forwardRef, ReactNode } from 'react';
import { fsx } from '../system/fsx';
import { ComponentProps, Ref } from '../system/polyComponent';

type WithInherit<T> = T | 'inherit';

Expand Down Expand Up @@ -33,52 +34,48 @@ const style = (size: WithInherit<Size>, weight: WithInherit<Weight>, typeface: W
);
};

export type TextProps<P extends ElementType> = ComponentPropsWithoutRef<P> & {
/**
* レンダリングするコンポーネントを指定 (例: h1, p, strong)
* @default span
*/
as?: P;
export type TextProps<As extends ElementType = 'span'> = ComponentProps<
{
/**
* テキストのサイズを指定
* @default inherit
*/
size?: WithInherit<Size>;

/**
* テキストのサイズを指定
* @default inherit
*/
size?: WithInherit<Size>;
/**
* 表示するテキストのウェイトを指定
* @default inherit
*/
weight?: WithInherit<Weight>;

/**
* 表示するテキストのウェイトを指定
* @default inherit
*/
weight?: WithInherit<Weight>;
/**
* 表示する書体の種別を指定
* @default inherit
*/
typeface?: WithInherit<Typeface>;

/**
* 表示する書体の種別を指定
* @default inherit
*/
typeface?: WithInherit<Typeface>;
className?: string;

className?: string;
/**
* 文字列またはstrong等のコンポーネント (HTML的にvalidになるようにしてください)
*/
children: ReactNode;
},
As
>;

/**
* 文字列またはstrong等のコンポーネント (HTML的にvalidになるようにしてください)
*/
children: ReactNode;
};
type TextComponent = <As extends ElementType = 'span'>(props: TextProps<As>) => ReturnType<FC>;

export const Text = <P extends ElementType = 'span'>({
as,
size = 'inherit',
weight = 'inherit',
typeface = 'inherit',
className,
children,
...rests
}: TextProps<P>): JSX.Element => {
const Component = as || 'span';
return (
<Component className={fsx(style(size, weight, typeface), className)} {...rests}>
{children}
</Component>
);
};
export const Text: TextComponent = forwardRef(
<As extends ElementType = 'span'>(
{ as, size = 'inherit', weight = 'inherit', typeface = 'inherit', className, children, ...rests }: TextProps<As>,
ref: Ref<As>,
) => {
const Component = as || 'span';
return (
<Component ref={ref} className={fsx(style(size, weight, typeface), className)} {...rests}>
{children}
</Component>
);
},
);
31 changes: 31 additions & 0 deletions packages/for-ui/src/tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { forwardRef } from 'react';
import { MdOutlineInfo } from 'react-icons/md';
import { Meta, StoryObj } from '@storybook/react/types-6-0';
import { Tooltip, TooltipFrame, TooltipProps } from './Tooltip';

type Story = StoryObj<TooltipProps>;

const TooltipIcon = forwardRef<HTMLSpanElement, object>((props, ref) => (
<span ref={ref} {...props} className="items-self-end inline-flex w-fit">
<MdOutlineInfo />
</span>
));

export default {
title: 'Feedback / Tooltip',
component: Tooltip,
} as Meta<typeof Tooltip>;

export const Playground: Story = {
args: {
title: 'テキスト',
children: <TooltipIcon />,
},
};

export const FrameOnly: Story = {
args: {
title: 'テキスト',
},
render: (props) => <TooltipFrame {...props} />,
};
38 changes: 38 additions & 0 deletions packages/for-ui/src/tooltip/Tooltip.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ComponentPropsWithRef, forwardRef } from 'react';
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Tooltip } from './Tooltip';

const Trigger = forwardRef<HTMLSpanElement, ComponentPropsWithRef<'span'>>((props, ref) => (
<span ref={ref} {...props} />
));

describe('Tooltip', () => {
it('has rendered children', async () => {
render(
<Tooltip title="description">
<Trigger>trigger</Trigger>
</Tooltip>,
);
expect(await screen.findByText('trigger')).toBeInTheDocument();
});
it('has children with accessible description', async () => {
render(
<Tooltip title="description">
<Trigger>trigger</Trigger>
</Tooltip>,
);
expect(await screen.findByText('trigger')).toHaveAccessibleDescription('description');
});
it('is appeared when focusing trigger', async () => {
const user = userEvent.setup();
render(
<Tooltip title="description">
<Trigger>trigger</Trigger>
</Tooltip>,
);
await user.tab();
expect(await screen.findByRole('tooltip')).toBeInTheDocument();
});
});
54 changes: 54 additions & 0 deletions packages/for-ui/src/tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ComponentPropsWithRef, FC, forwardRef, useId } from 'react';
import MuiTooltip, { TooltipProps as MuiTooltipProps } from '@mui/material/Tooltip';
import { fsx } from '../system/fsx';
import { PropsCascader } from '../system/PropsCascader';
import { Text } from '../text';
import { prepareForSlot } from '../utils/prepareForSlot';

export type TooltipProps = Pick<MuiTooltipProps, 'placement' | 'children'> & {
title: string;
};

export const TooltipFrame = forwardRef<HTMLSpanElement, ComponentPropsWithRef<'span'>>(
({ children, ...props }, ref) => (
<Text
{...props}
ref={ref}
size="r"
weight="regular"
className={fsx(`text-shade-white-default bg-shade-dark-default inline-flex rounded px-2`)}
>
{children}
</Text>
),
);

export const Tooltip: FC<TooltipProps> = ({ children, ...props }) => {
const internalId = useId();
return (
<MuiTooltip
id={internalId}
slots={{
tooltip: prepareForSlot(TooltipFrame),
}}
slotProps={{
popper: {
keepMounted: true,
style: {
display: undefined,
},
},
tooltip: {
style: {
visibility: 'visible',
},
},
}}
{...props}
>
<PropsCascader tabIndex={0} aria-describedby={internalId} aria-label={undefined}>
{children}
</PropsCascader>
</MuiTooltip>
);
};
1 change: 1 addition & 0 deletions packages/for-ui/src/tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Tooltip';
14 changes: 14 additions & 0 deletions packages/for-ui/src/utils/prepareForSlot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Retrieved from https://github.com/mui/material-ui/blob/master/packages/mui-base/src/utils/prepareForSlot.tsx
import * as React from 'react';

export function prepareForSlot<ComponentType extends React.ElementType>(Component: ComponentType) {
type Props = React.ComponentProps<ComponentType>;

return React.forwardRef<HTMLElement, Props>(function Slot(props, ref) {
const { ownerState: _, ...other } = props;
return React.createElement<Props>(Component, {
...(other as Props),
ref,
});
});
}

0 comments on commit e12d109

Please sign in to comment.