From e12d10926080851e4ecd5bccfe36b1e316d12866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=81=B5=E3=81=BF?= Date: Thu, 4 Jan 2024 17:17:18 +0900 Subject: [PATCH] =?UTF-8?q?feat(Tooltip):=20=E8=BF=BD=E5=8A=A0=20(#1486)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .changeset/orange-games-work.md | 5 ++ package-lock.json | 5 +- packages/for-ui/src/index.ts | 1 + .../for-ui/src/skeleton/Skeleton.stories.tsx | 2 +- packages/for-ui/src/system/PropsCascader.tsx | 21 +++++ packages/for-ui/src/text/Text.tsx | 85 +++++++++---------- .../for-ui/src/tooltip/Tooltip.stories.tsx | 31 +++++++ packages/for-ui/src/tooltip/Tooltip.test.tsx | 38 +++++++++ packages/for-ui/src/tooltip/Tooltip.tsx | 54 ++++++++++++ packages/for-ui/src/tooltip/index.ts | 1 + packages/for-ui/src/utils/prepareForSlot.tsx | 14 +++ 11 files changed, 211 insertions(+), 46 deletions(-) create mode 100644 .changeset/orange-games-work.md create mode 100644 packages/for-ui/src/system/PropsCascader.tsx create mode 100644 packages/for-ui/src/tooltip/Tooltip.stories.tsx create mode 100644 packages/for-ui/src/tooltip/Tooltip.test.tsx create mode 100644 packages/for-ui/src/tooltip/Tooltip.tsx create mode 100644 packages/for-ui/src/tooltip/index.ts create mode 100644 packages/for-ui/src/utils/prepareForSlot.tsx diff --git a/.changeset/orange-games-work.md b/.changeset/orange-games-work.md new file mode 100644 index 000000000..ae939ff31 --- /dev/null +++ b/.changeset/orange-games-work.md @@ -0,0 +1,5 @@ +--- +"@4design/for-ui": patch +--- + +feat: Tooltipを追加 diff --git a/package-lock.json b/package-lock.json index 8404300cf..91413d3b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5709,11 +5709,14 @@ } }, "node_modules/@storybook/builder-webpack4/node_modules/watchpack/chokidar2": { - "version": "0.0.1", + "version": "2.0.0", "dev": true, "optional": true, "dependencies": { "chokidar": "^2.1.8" + }, + "engines": { + "node": "<8.10.0" } }, "node_modules/@storybook/builder-webpack4/node_modules/webpack": { diff --git a/packages/for-ui/src/index.ts b/packages/for-ui/src/index.ts index bb36427b0..b62974b3f 100644 --- a/packages/for-ui/src/index.ts +++ b/packages/for-ui/src/index.ts @@ -19,3 +19,4 @@ export * from './textArea'; export * from './textField'; export * from './tabs'; export * from './table'; +export * from './tooltip'; diff --git a/packages/for-ui/src/skeleton/Skeleton.stories.tsx b/packages/for-ui/src/skeleton/Skeleton.stories.tsx index 4dce51c98..19f726a48 100644 --- a/packages/for-ui/src/skeleton/Skeleton.stories.tsx +++ b/packages/for-ui/src/skeleton/Skeleton.stories.tsx @@ -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 diff --git a/packages/for-ui/src/system/PropsCascader.tsx b/packages/for-ui/src/system/PropsCascader.tsx new file mode 100644 index 000000000..5d1a74bd9 --- /dev/null +++ b/packages/for-ui/src/system/PropsCascader.tsx @@ -0,0 +1,21 @@ +import { Children, cloneElement, FC, ForwardedRef, forwardRef, HTMLAttributes, isValidElement, ReactNode } from 'react'; + +type PropsCascaderProps> = T & { + children: ReactNode; +}; + +type PropsCascaderComponent = >(props: PropsCascaderProps) => ReturnType; + +export const PropsCascader: PropsCascaderComponent = forwardRef( + >( + { children, ...props }: PropsCascaderProps, + ref: ForwardedRef, + ) => { + return Children.map(children, (child: ReactNode) => { + if (!isValidElement(child)) { + return child; + } + return cloneElement(child, { ...props, ...child.props, ref }); + }); + }, +); diff --git a/packages/for-ui/src/text/Text.tsx b/packages/for-ui/src/text/Text.tsx index d65901a47..94f75d76d 100644 --- a/packages/for-ui/src/text/Text.tsx +++ b/packages/for-ui/src/text/Text.tsx @@ -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 | 'inherit'; @@ -33,52 +34,48 @@ const style = (size: WithInherit, weight: WithInherit, typeface: W ); }; -export type TextProps

= ComponentPropsWithoutRef

& { - /** - * レンダリングするコンポーネントを指定 (例: h1, p, strong) - * @default span - */ - as?: P; +export type TextProps = ComponentProps< + { + /** + * テキストのサイズを指定 + * @default inherit + */ + size?: WithInherit; - /** - * テキストのサイズを指定 - * @default inherit - */ - size?: WithInherit; + /** + * 表示するテキストのウェイトを指定 + * @default inherit + */ + weight?: WithInherit; - /** - * 表示するテキストのウェイトを指定 - * @default inherit - */ - weight?: WithInherit; + /** + * 表示する書体の種別を指定 + * @default inherit + */ + typeface?: WithInherit; - /** - * 表示する書体の種別を指定 - * @default inherit - */ - typeface?: WithInherit; + className?: string; - className?: string; + /** + * 文字列またはstrong等のコンポーネント (HTML的にvalidになるようにしてください) + */ + children: ReactNode; + }, + As +>; - /** - * 文字列またはstrong等のコンポーネント (HTML的にvalidになるようにしてください) - */ - children: ReactNode; -}; +type TextComponent = (props: TextProps) => ReturnType; -export const Text =

({ - as, - size = 'inherit', - weight = 'inherit', - typeface = 'inherit', - className, - children, - ...rests -}: TextProps

): JSX.Element => { - const Component = as || 'span'; - return ( - - {children} - - ); -}; +export const Text: TextComponent = forwardRef( + ( + { as, size = 'inherit', weight = 'inherit', typeface = 'inherit', className, children, ...rests }: TextProps, + ref: Ref, + ) => { + const Component = as || 'span'; + return ( + + {children} + + ); + }, +); diff --git a/packages/for-ui/src/tooltip/Tooltip.stories.tsx b/packages/for-ui/src/tooltip/Tooltip.stories.tsx new file mode 100644 index 000000000..81a24f739 --- /dev/null +++ b/packages/for-ui/src/tooltip/Tooltip.stories.tsx @@ -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; + +const TooltipIcon = forwardRef((props, ref) => ( + + + +)); + +export default { + title: 'Feedback / Tooltip', + component: Tooltip, +} as Meta; + +export const Playground: Story = { + args: { + title: 'テキスト', + children: , + }, +}; + +export const FrameOnly: Story = { + args: { + title: 'テキスト', + }, + render: (props) => , +}; diff --git a/packages/for-ui/src/tooltip/Tooltip.test.tsx b/packages/for-ui/src/tooltip/Tooltip.test.tsx new file mode 100644 index 000000000..ed3e31efa --- /dev/null +++ b/packages/for-ui/src/tooltip/Tooltip.test.tsx @@ -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>((props, ref) => ( + +)); + +describe('Tooltip', () => { + it('has rendered children', async () => { + render( + + trigger + , + ); + expect(await screen.findByText('trigger')).toBeInTheDocument(); + }); + it('has children with accessible description', async () => { + render( + + trigger + , + ); + expect(await screen.findByText('trigger')).toHaveAccessibleDescription('description'); + }); + it('is appeared when focusing trigger', async () => { + const user = userEvent.setup(); + render( + + trigger + , + ); + await user.tab(); + expect(await screen.findByRole('tooltip')).toBeInTheDocument(); + }); +}); diff --git a/packages/for-ui/src/tooltip/Tooltip.tsx b/packages/for-ui/src/tooltip/Tooltip.tsx new file mode 100644 index 000000000..d7f8f21c1 --- /dev/null +++ b/packages/for-ui/src/tooltip/Tooltip.tsx @@ -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 & { + title: string; +}; + +export const TooltipFrame = forwardRef>( + ({ children, ...props }, ref) => ( + + {children} + + ), +); + +export const Tooltip: FC = ({ children, ...props }) => { + const internalId = useId(); + return ( + + + {children} + + + ); +}; diff --git a/packages/for-ui/src/tooltip/index.ts b/packages/for-ui/src/tooltip/index.ts new file mode 100644 index 000000000..7594a8f06 --- /dev/null +++ b/packages/for-ui/src/tooltip/index.ts @@ -0,0 +1 @@ +export * from './Tooltip'; diff --git a/packages/for-ui/src/utils/prepareForSlot.tsx b/packages/for-ui/src/utils/prepareForSlot.tsx new file mode 100644 index 000000000..99b78054c --- /dev/null +++ b/packages/for-ui/src/utils/prepareForSlot.tsx @@ -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(Component: ComponentType) { + type Props = React.ComponentProps; + + return React.forwardRef(function Slot(props, ref) { + const { ownerState: _, ...other } = props; + return React.createElement(Component, { + ...(other as Props), + ref, + }); + }); +}