Skip to content

Commit 2438b43

Browse files
committed
Add ListItem component
1 parent 33cd7ac commit 2438b43

File tree

12 files changed

+236
-0
lines changed

12 files changed

+236
-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 { Flex, FlexProps } from '../../flex';
2+
3+
export type ListIOtemFooterProps = FlexProps;
4+
5+
export const ListItemFooter = (props: ListIOtemFooterProps) => {
6+
return <Flex $padding={{ horizontal: 'medium' }} {...props} />;
7+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Flex, FlexProps } from '../../flex';
2+
3+
export type ListItemGroupProps = FlexProps;
4+
5+
export const ListItemGroup = (props: ListItemGroupProps) => {
6+
return <Flex $alignItems="center" $gap="small" {...props} />;
7+
};
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,40 @@
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+
// TODO: Colors should be replace with
9+
// with new color tokens once they are implemented.
10+
const StyledButton = styled(ButtonBase)<{ $disabled?: boolean }>`
11+
display: flex;
12+
width: 100%;
13+
${({ $disabled }) =>
14+
!$disabled &&
15+
css`
16+
&:hover ${StyledFlex} {
17+
background-color: rgba(56, 86, 116, 1);
18+
}
19+
&:active ${StyledFlex} {
20+
background-color: rgba(62, 95, 129, 1);
21+
}
22+
`}
23+
24+
&&:focus-visible {
25+
outline: 2px solid ${Colors.white};
26+
outline-offset: -1px;
27+
z-index: 10;
28+
}
29+
`;
30+
31+
export type ListItemTriggerProps = React.HtmlHTMLAttributes<HTMLButtonElement>;
32+
33+
export const ListItemTrigger = ({ children, ...props }: ListItemTriggerProps) => {
34+
const { disabled } = useListItem();
35+
return (
36+
<StyledButton $disabled={disabled} {...props}>
37+
{children}
38+
</StyledButton>
39+
);
40+
};
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,10 @@
1+
// TODO: These are colors are from the new colors tokens
2+
// which are currently not implemented, should be replaced
3+
// with the new color tokens once they are.
4+
export const levels = {
5+
0: { enabled: 'rgba(41, 77, 115, 1)', disabled: 'rgba(31, 58, 87, 1)' },
6+
1: { enabled: 'rgba(35, 65, 97, 1)', disabled: 'rgba(31, 58, 87, 1)' },
7+
2: { enabled: 'rgba(31, 58, 87, 1)', disabled: 'rgba(28, 52, 78, 1)' },
8+
3: { enabled: 'rgba(28, 52, 78, 1)', disabled: 'rgba(27, 49, 74, 1)' },
9+
4: { enabled: 'rgba(27, 49, 74, 1)', disabled: 'rgba(27, 49, 74, 1)' },
10+
} as const;

0 commit comments

Comments
 (0)