Skip to content

Commit d69127e

Browse files
committed
Add ListItem component
1 parent 33cd7ac commit d69127e

File tree

12 files changed

+232
-0
lines changed

12 files changed

+232
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Flex } from '../flex';
2+
import {
3+
ListItemContent,
4+
ListItemFooter,
5+
ListItemGroup,
6+
ListItemItem,
7+
ListItemLabel,
8+
ListItemText,
9+
ListItemTrigger,
10+
} from './components';
11+
import { levels } from './levels';
12+
import { ListItemProvider } from './ListItemContext';
13+
14+
export interface ListItemProps {
15+
level?: keyof typeof levels;
16+
disabled?: boolean;
17+
children: React.ReactNode;
18+
}
19+
20+
const ListItem = ({ level = 0, disabled, children }: ListItemProps) => {
21+
return (
22+
<ListItemProvider level={level} disabled={disabled}>
23+
<Flex $flexDirection="column" $gap="small">
24+
{children}
25+
</Flex>
26+
</ListItemProvider>
27+
);
28+
};
29+
30+
const ListItemNamespace = Object.assign(ListItem, {
31+
Content: ListItemContent,
32+
Label: ListItemLabel,
33+
Group: ListItemGroup,
34+
Text: ListItemText,
35+
Trigger: ListItemTrigger,
36+
Item: ListItemItem,
37+
Footer: ListItemFooter,
38+
});
39+
40+
export { ListItemNamespace as ListItem };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { createContext, ReactNode, useContext } from 'react';
2+
3+
import { levels } from './levels';
4+
5+
interface ListItemContextType {
6+
level: keyof typeof levels;
7+
disabled?: boolean;
8+
}
9+
10+
const ListItemContext = createContext<ListItemContextType | undefined>(undefined);
11+
12+
interface ListItemProviderProps extends ListItemContextType {
13+
children: ReactNode;
14+
}
15+
16+
export const ListItemProvider = ({ level, disabled, children }: ListItemProviderProps) => {
17+
return (
18+
<ListItemContext.Provider value={{ level, disabled }}>{children}</ListItemContext.Provider>
19+
);
20+
};
21+
22+
export const useListItem = (): ListItemContextType => {
23+
const context = useContext(ListItemContext);
24+
if (!context) {
25+
throw new Error('useListItem must be used within a ListItemProvider');
26+
}
27+
return context;
28+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import styled from 'styled-components';
2+
3+
import { Flex, FlexProps } from '../../flex';
4+
import { levels } from '../levels';
5+
import { useListItem } from '../ListItemContext';
6+
7+
const sizes = {
8+
full: '100%',
9+
small: '44px',
10+
};
11+
12+
export const StyledFlex = styled(Flex)<{
13+
$level: keyof typeof levels;
14+
$disabled?: boolean;
15+
$size: 'full' | 'small';
16+
}>`
17+
width: ${({ $size }) => sizes[$size]};
18+
height: 100%;
19+
background-color: ${({ $disabled, $level }) =>
20+
$disabled ? levels[$level].disabled : levels[$level].enabled};
21+
&&:has(> :last-child:nth-child(1)) {
22+
&&:has(img) {
23+
justify-content: center;
24+
}
25+
}
26+
`;
27+
28+
export interface ListItemContainerProps extends FlexProps {
29+
size?: 'full' | 'small';
30+
}
31+
32+
export const ListItemContent = ({ size = 'full', ...props }: ListItemContainerProps) => {
33+
const { level } = useListItem();
34+
return (
35+
<StyledFlex
36+
$size={size}
37+
$level={level}
38+
$alignItems="center"
39+
$justifyContent="space-between"
40+
$gap="small"
41+
$padding={{
42+
horizontal: 'medium',
43+
}}
44+
{...props}
45+
/>
46+
);
47+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import styled from 'styled-components';
2+
3+
import { Flex } from '../../flex';
4+
5+
export const ListItemFooter = styled(Flex).attrs({
6+
$padding: { horizontal: 'medium' },
7+
})``;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import styled from 'styled-components';
2+
3+
import { Flex } from '../../flex';
4+
5+
export const ListItemGroup = styled(Flex).attrs({
6+
$alignItems: 'center',
7+
$gap: 'small',
8+
})``;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import styled from 'styled-components';
2+
3+
export interface ListItemItemProps {
4+
children: React.ReactNode;
5+
}
6+
7+
const StyledDiv = styled.div`
8+
min-height: 44px;
9+
width: 100%;
10+
display: grid;
11+
grid-template-columns: 1fr;
12+
&&:has(> :last-child:nth-child(2)) {
13+
grid-template-columns: 1fr 44px;
14+
}
15+
`;
16+
17+
export const ListItemItem = ({ children }: ListItemItemProps) => {
18+
return <StyledDiv>{children}</StyledDiv>;
19+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Colors } from '../../../foundations';
2+
import { LabelTinyProps, TitleMedium } from '../../typography';
3+
import { useListItem } from '../ListItemContext';
4+
5+
export type ListItemLabelProps = LabelTinyProps;
6+
7+
export const ListItemLabel = ({ children, ...props }: ListItemLabelProps) => {
8+
const { disabled } = useListItem();
9+
return (
10+
<TitleMedium color={disabled ? Colors.white40 : Colors.white} {...props}>
11+
{children}
12+
</TitleMedium>
13+
);
14+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Colors } from '../../../foundations';
2+
import { Text, TextProps } from '../../typography';
3+
import { useListItem } from '../ListItemContext';
4+
5+
export type ListItemProps<E extends React.ElementType = 'span'> = Omit<TextProps<E>, 'variant'> & {
6+
variant?: Extract<TextProps<E>['variant'], 'labelTiny' | 'footnoteMini'>;
7+
};
8+
9+
export const ListItemText = ({ variant = 'labelTiny', children, ...props }: ListItemProps) => {
10+
const { disabled } = useListItem();
11+
return (
12+
<Text variant={variant} color={disabled ? Colors.white40 : Colors.white60} {...props}>
13+
{children}
14+
</Text>
15+
);
16+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import styled, { css } from 'styled-components';
2+
3+
import { Colors } from '../../../foundations';
4+
import { ButtonBase } from '../../button';
5+
import { useListItem } from '../ListItemContext';
6+
import { StyledFlex } from './ListItemContent';
7+
8+
const StyledButton = styled(ButtonBase)<{ $disabled?: boolean }>`
9+
display: flex;
10+
width: 100%;
11+
${({ $disabled }) =>
12+
!$disabled &&
13+
css`
14+
&:hover ${StyledFlex} {
15+
background-color: rgba(56, 86, 116, 1);
16+
}
17+
&:active ${StyledFlex} {
18+
background-color: rgba(62, 95, 129, 1);
19+
}
20+
`}
21+
22+
&&:focus-visible {
23+
outline: 2px solid ${Colors.white};
24+
outline-offset: -1px;
25+
z-index: 10;
26+
}
27+
`;
28+
29+
export type ListItemTriggerProps = React.HtmlHTMLAttributes<HTMLButtonElement>;
30+
31+
export const ListItemTrigger = ({ children, ...props }: ListItemTriggerProps) => {
32+
const { disabled } = useListItem();
33+
return (
34+
<StyledButton $disabled={disabled} {...props}>
35+
{children}
36+
</StyledButton>
37+
);
38+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export * from './ListItemContent';
2+
export * from './ListItemItem';
3+
export * from './ListItemGroup';
4+
export * from './ListItemLabel';
5+
export * from './ListItemText';
6+
export * from './ListItemTrigger';
7+
export * from './ListItemFooter';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './ListItem';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const levels = {
2+
0: { enabled: 'rgba(41, 77, 115, 1)', disabled: 'rgba(31, 58, 87, 1)' },
3+
1: { enabled: 'rgba(35, 65, 97, 1)', disabled: 'rgba(31, 58, 87, 1)' },
4+
2: { enabled: 'rgba(31, 58, 87, 1)', disabled: 'rgba(28, 52, 78, 1)' },
5+
3: { enabled: 'rgba(28, 52, 78, 1)', disabled: 'rgba(27, 49, 74, 1)' },
6+
4: { enabled: 'rgba(27, 49, 74, 1)', disabled: 'rgba(27, 49, 74, 1)' },
7+
} as const;

0 commit comments

Comments
 (0)