Skip to content

Commit

Permalink
imple FlexItem
Browse files Browse the repository at this point in the history
  • Loading branch information
8845musign committed Jul 4, 2024
1 parent 9bc2042 commit 380c9f3
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 1 deletion.
12 changes: 12 additions & 0 deletions src/components/FlexItem/FlexItem.module.css
Original file line number Diff line number Diff line change
@@ -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);
}
84 changes: 84 additions & 0 deletions src/components/FlexItem/FlexItem.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { render, screen } from '@testing-library/react';
import { FlexItem } from './FlexItem';

describe('<FlexItem>', () => {
it('longhand class is only given if an individual flex property is specified.', () => {
render(
<FlexItem flex={{ grow: 1 }} data-testid="flex-item">
Test
</FlexItem>,
);
const div = screen.getByTestId('flex-item');

expect(div).toHaveClass(/longhand/i);
});

it('Default values for individual properties not specified', () => {
render(
<div>
<FlexItem
flex={{
grow: 0,
}}
data-testid="flex-item-1"
>
Test
</FlexItem>
<FlexItem
flex={{
shrink: 1,
}}
data-testid="flex-item-2"
>
Test
</FlexItem>
</div>,
);
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(<FlexItem data-testid="flex-item">Test</FlexItem>);
const div = screen.getByTestId('flex-item');

expect(div).not.toHaveClass(/longhand/i);
});

it('If none is specified, the none class is given', () => {
render(
<FlexItem flex="none" data-testid="flex-item">
Test
</FlexItem>,
);
const div = screen.getByTestId('flex-item');

expect(div).toHaveClass(/none/i);
});

it('Receive max-width', () => {
render(
<FlexItem maxWidth="100px" data-testid="flex-item">
Test
</FlexItem>,
);
const div = screen.getByTestId('flex-item');

expect(div).toHaveStyle('--max-width: 100px');
});

it('Receive min-width', () => {
render(
<FlexItem minWidth="100px" data-testid="flex-item">
Test
</FlexItem>,
);
const div = screen.getByTestId('flex-item');

expect(div).toHaveStyle('--min-width: 100px');
});
});
65 changes: 65 additions & 0 deletions src/components/FlexItem/FlexItem.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>;

/**
* FlexやStackの子として配置し、レイアウトを調整
*/
export const FlexItem = forwardRef<HTMLDivElement, PropsWithChildren<Props>>(
({ 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 (
<div
className={clsx(styles.flexItem, flex === 'none' && styles.none, typeof flex === 'object' && styles.longhand)}
ref={ref}
style={
{
'--min-width': minWidth,
'--max-width': maxWidth,
...flexObj,
} as CSSProperties
}
{...rest}
>
{children}
</div>
);
},
);

FlexItem.displayName = 'FlexItem';
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
28 changes: 27 additions & 1 deletion src/stories/Flex.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Meta, StoryObj } from '@storybook/react';
import { Flex, Box } from '..';
import { Flex, Box, FlexItem } from '..';

export default {
component: Flex,
Expand Down Expand Up @@ -199,3 +199,29 @@ export const CustomDataAttribute: Story = {
</Flex>
),
};

export const WithFlexItem: Story = {
render: () => (
<div style={{ height: 500 }}>
<Flex spacing="lg" alignItems="stretch" height="full">
<p style={{ margin: 0, background: '#DDD' }}>
column
<br />
Stretched
</p>
<FlexItem>
<p style={{ margin: 0, background: '#DDD' }}>
column
<br />
not
<br />
stretched
</p>
</FlexItem>
<FlexItem flex={{ grow: 1 }}>
<p style={{ margin: 0, background: '#DDD' }}>row grow</p>
</FlexItem>
</Flex>
</div>
),
};
67 changes: 67 additions & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,70 @@ export type AllOrNone<T> = T | Partial<Record<keyof T, undefined>>;
export type DistributiveOmit<T, TOmitted extends PropertyKey> = T extends any ? Omit<T, TOmitted> : 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;

0 comments on commit 380c9f3

Please sign in to comment.