diff --git a/src/components/FlexItem/FlexItem.module.css b/src/components/FlexItem/FlexItem.module.css new file mode 100644 index 00000000..fc14fea4 --- /dev/null +++ b/src/components/FlexItem/FlexItem.module.css @@ -0,0 +1,12 @@ +.flexItem { + min-width: var(--min-width); + max-width: var(--max-width); +} + +.none { + flex: none; +} + +.longhand { + flex: var(--flex-grow) var(--flex-shrink) var(--flex-basis); +} diff --git a/src/components/FlexItem/FlexItem.spec.tsx b/src/components/FlexItem/FlexItem.spec.tsx new file mode 100644 index 00000000..1541b1a1 --- /dev/null +++ b/src/components/FlexItem/FlexItem.spec.tsx @@ -0,0 +1,84 @@ +import { render, screen } from '@testing-library/react'; +import { FlexItem } from './FlexItem'; + +describe('', () => { + it('longhand class is only given if an individual flex property is specified.', () => { + render( + + Test + , + ); + const div = screen.getByTestId('flex-item'); + + expect(div).toHaveClass(/longhand/i); + }); + + it('Default values for individual properties not specified', () => { + render( +
+ + Test + + + Test + +
, + ); + const div1 = screen.getByTestId('flex-item-1'); + const div2 = screen.getByTestId('flex-item-2'); + + expect(div1).toHaveStyle('--flex-shrink: 1'); + expect(div1).toHaveStyle('--flex-basis: auto'); + expect(div2).toHaveStyle('--flex-grow: 0'); + }); + + it('If no individual flex peroperty is specified, the longhand class is not specified', () => { + render(Test); + const div = screen.getByTestId('flex-item'); + + expect(div).not.toHaveClass(/longhand/i); + }); + + it('If none is specified, the none class is given', () => { + render( + + Test + , + ); + const div = screen.getByTestId('flex-item'); + + expect(div).toHaveClass(/none/i); + }); + + it('Receive max-width', () => { + render( + + Test + , + ); + const div = screen.getByTestId('flex-item'); + + expect(div).toHaveStyle('--max-width: 100px'); + }); + + it('Receive min-width', () => { + render( + + Test + , + ); + const div = screen.getByTestId('flex-item'); + + expect(div).toHaveStyle('--min-width: 100px'); + }); +}); diff --git a/src/components/FlexItem/FlexItem.tsx b/src/components/FlexItem/FlexItem.tsx new file mode 100644 index 00000000..88741bdd --- /dev/null +++ b/src/components/FlexItem/FlexItem.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { clsx } from 'clsx'; +import { CSSProperties, forwardRef, type PropsWithChildren, type HTMLAttributes } from 'react'; +import styles from './FlexItem.module.css'; +import { CSSWitdh, CSSMaxWidth, CSSMinWidth } from '../../utils/types'; + +type FlexProperty = { + grow?: number; + shrink?: number; + basis?: CSSWitdh; +}; + +type Props = { + /** + * flexの値を指定。 growなどを指定したい場合はオブジェクトで指定 + * @defaultValue none + */ + flex?: 'none' | FlexProperty; + /** + * 最小幅 + * @defaultValue auto + */ + minWidth?: CSSMinWidth; + /** + * 最大幅 + * @defaultValue none + */ + maxWidth?: CSSMaxWidth; +} & HTMLAttributes; + +/** + * FlexやStackの子として配置し、レイアウトを調整 + */ +export const FlexItem = forwardRef>( + ({ children, flex = 'none', minWidth = 'auto', maxWidth = 'none', ...rest }, ref) => { + const flexObj: { [key: string]: string } = + typeof flex === 'object' + ? { + '--flex-grow': flex.grow != null ? flex.grow.toString() : '0', + '--flex-shrink': flex.shrink != null ? flex.shrink.toString() : '1', + '--flex-basis': flex.basis ?? 'auto', + } + : {}; + + return ( +
+ {children} +
+ ); + }, +); + +FlexItem.displayName = 'FlexItem'; diff --git a/src/index.ts b/src/index.ts index d2d97d3f..d6a12e40 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ export { Heading } from './components/Heading/Heading'; export { LinkButton } from './components/Button/LinkButton'; export { ErrorMessage } from './components/ErrorMessage/ErrorMessage'; export { Flex } from './components/Flex/Flex'; +export { FlexItem } from './components/FlexItem/FlexItem'; export { HelperMessage } from './components/HelperMessage/HelperMessage'; export { Checkbox } from './components/Checkbox/Checkbox'; export { CheckboxCard } from './components/CheckboxCard/CheckboxCard'; diff --git a/src/stories/Flex.stories.tsx b/src/stories/Flex.stories.tsx index a68c6687..46afc136 100644 --- a/src/stories/Flex.stories.tsx +++ b/src/stories/Flex.stories.tsx @@ -1,5 +1,5 @@ import { Meta, StoryObj } from '@storybook/react'; -import { Flex, Box } from '..'; +import { Flex, Box, FlexItem } from '..'; export default { component: Flex, @@ -199,3 +199,29 @@ export const CustomDataAttribute: Story = { ), }; + +export const WithFlexItem: Story = { + render: () => ( +
+ +

+ column +
+ Stretched +

+ +

+ column +
+ not +
+ stretched +

+
+ +

row grow

+
+
+
+ ), +}; diff --git a/src/utils/types.ts b/src/utils/types.ts index 25541ca2..2b6270dc 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -4,3 +4,70 @@ export type AllOrNone = T | Partial>; export type DistributiveOmit = T extends any ? Omit : never; export type HTMLTagname = keyof HTMLElementTagNameMap; + +export type CSSVariable = `var(--${string})`; + +// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis +export type CSSLength = + | `${string}cap` + | `${string}ch` + | `${string}em` + | `${string}ex` + | `${string}ic` + | `${string}lh` + | `${string}rcap` + | `${string}rem` + | `${string}rex` + | `${string}ric` + | `${string}rlh` + | `${string}vh` + | `${string}vmax` + | `${string}vmin` + | `${string}vw` + | `${string}vb` + | `${string}vi` + | `${string}cqw` + | `${string}cqh` + | `${string}cqi` + | `${string}cqb` + | `${string}cqmin` + | `${string}cqmax` + | `${string}px` + | `${string}cm` + | `${string}mm` + | `${string}q` + | `${string}in` + | `${string}pc` + | `${string}pt`; + +export type CSSPercentage = `${string}%`; + +export type CSSLengthPercentage = CSSLength | CSSPercentage; + +export type CSSWitdh = + | CSSLength + | CSSPercentage + | 'auto' + | 'fit-content' + | `fit-content(${CSSLengthPercentage})` + | 'min-content' + | 'max-content' + | CSSVariable; + +export type CSSMaxWidth = + | 'none' + | CSSLengthPercentage + | 'min-content' + | 'max-content' + | 'fit-content' + | `fit-content(${CSSLengthPercentage})` + | CSSVariable; + +export type CSSMinWidth = + | 'auto' + | CSSLengthPercentage + | 'min-content' + | 'max-content' + | 'fit-content' + | `fit-content(${CSSLengthPercentage})` + | CSSVariable;