Skip to content

Commit 5d50fc0

Browse files
authored
* feat(List): Supports theme-managed colors (Tree too)
List, ListItem, Tree & TreeItem now support new theme-based color assignments. Specifically: - calculation - dimension - measure Additionally the `keyColor` prop has been marked as deprecated and now `color="key"` is the preferred approach. The color is used a background color (using the ${colorName}Accent variant) when the `*Item` is `selected` or `current` Items with `calculation` & `measure` will have a text color applied at all times unless they are marked as `disabled`
1 parent f3c36b9 commit 5d50fc0

30 files changed

+634
-292
lines changed

packages/components/src/List/List.story.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ FontFamily.args = {
6464
fontFamily: 'code',
6565
}
6666

67+
export const Color = () => <List color="key">{listItems}</List>
68+
6769
export const IconGutter = Template.bind({})
6870
IconGutter.args = {
6971
...Basic.args,

packages/components/src/List/List.test.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626

2727
import 'jest-styled-components'
2828
import React from 'react'
29-
import { screen } from '@testing-library/react'
3029
import { renderWithTheme } from '@looker/components-test-utils'
30+
import { screen } from '@testing-library/react'
3131
import { Basic, LongList } from './List.story'
32+
import { ListItem } from './ListItem'
33+
import { List } from './List'
3234

3335
/* eslint-disable-next-line @typescript-eslint/unbound-method */
3436
const globalGetBoundingClientRect = Element.prototype.getBoundingClientRect
@@ -90,4 +92,46 @@ describe('List', () => {
9092
expect(screen.getByRole('list')).not.toHaveStyle('height: 100%')
9193
})
9294
})
95+
describe('color', () => {
96+
test('displays the correct background when selected', () => {
97+
renderWithTheme(
98+
<List color="key">
99+
<ListItem selected>Mozzarella</ListItem>
100+
</List>
101+
)
102+
expect(screen.getByText('Mozzarella')).toBeInTheDocument()
103+
expect(screen.getByText('Mozzarella').closest('button')).toHaveStyle(
104+
'background: #F3F2FF;'
105+
)
106+
})
107+
108+
test('expects color="key" and keyColor have the same background-color value', () => {
109+
renderWithTheme(
110+
<>
111+
<List color="key">
112+
<ListItem selected>color</ListItem>
113+
</List>
114+
<List keyColor>
115+
<ListItem selected>keyColor</ListItem>
116+
</List>
117+
</>
118+
)
119+
expect(screen.getByText('color').closest('button')).toHaveStyle(
120+
'background: #F3F2FF;'
121+
)
122+
expect(screen.getByText('keyColor').closest('button')).toHaveStyle(
123+
'background: #F3F2FF;'
124+
)
125+
})
126+
127+
test('updates ListItem get its text color updated', () => {
128+
renderWithTheme(
129+
<List color="calculation">
130+
<ListItem selected>Mozzarella</ListItem>
131+
</List>
132+
)
133+
expect(screen.getByText('Mozzarella')).toBeInTheDocument()
134+
expect(screen.getByText('Mozzarella')).toHaveStyle('color: #319220;')
135+
})
136+
})
93137
})

packages/components/src/List/List.tsx

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -32,55 +32,52 @@ import React, {
3232
Ref,
3333
useMemo,
3434
} from 'react'
35-
import styled from 'styled-components'
36-
import { fontFamily, height, HeightProps } from 'styled-system'
3735
import {
3836
CompatibleHTMLProps,
3937
FontFamilies,
4038
shouldForwardProp,
39+
height,
4140
width,
4241
WidthProps,
4342
} from '@looker/design-tokens'
43+
import { HeightProps, fontFamily } from 'styled-system'
44+
import styled from 'styled-components'
4445
import { useArrowKeyNav, useWindow } from '../utils'
4546
import { ListItemContext } from './ListItemContext'
47+
import { DensityRamp, ListColorProps } from './types'
4648
import { listItemDimensions } from './utils'
47-
import { DensityRamp } from './types'
48-
49-
export interface ListProps
50-
extends HeightProps,
51-
WidthProps,
52-
Omit<CompatibleHTMLProps<HTMLUListElement>, 'label'> {
53-
/**
54-
* Determines how dense a list should be by affecting child ListItem
55-
* size and spacing.
56-
* @default 0
57-
*/
58-
density?: DensityRamp
59-
60-
/**
61-
* If true, all ListItem children without an icon will reserve space for an icon
62-
* for alignment purposes.
63-
*/
64-
iconGutter?: boolean
65-
66-
/**
67-
* Specify font-family. Generally will end up inheriting `theme.fonts.body` but can be specified as `brand`, `code` or `body` to explicitly specify theme-controlled font-family
68-
* @default inherit
69-
*/
70-
fontFamily?: FontFamilies
71-
72-
/**
73-
* Replace the normal uiN(1-5) color for selected and selected + hovered color with key colors
74-
*/
75-
keyColor?: boolean
76-
77-
/**
78-
* Use windowing for long lists (strongly recommended to also define a width on List or its container)
79-
* 'none' - default with children are <= 100.
80-
* 'fixed' - better performance, default when first child is a ListItem (height will default to 100%)
81-
*/
82-
windowing?: 'fixed' | 'none'
83-
}
49+
50+
export type ListProps = ListColorProps &
51+
HeightProps &
52+
WidthProps &
53+
Omit<CompatibleHTMLProps<HTMLUListElement>, 'label'> & {
54+
/**
55+
* Determines how dense a list should be by affecting child ListItem
56+
* size and spacing.
57+
* @default 0
58+
*/
59+
density?: DensityRamp
60+
61+
/**
62+
* If true, all ListItem children without an icon will reserve space for an icon
63+
* for alignment purposes.
64+
*/
65+
iconGutter?: boolean
66+
67+
/**
68+
* Specify font-family. Can be specified as `brand`, `code` or `body` to explicitly
69+
* specify theme-controlled font-family.
70+
* @default inherit
71+
*/
72+
fontFamily?: FontFamilies
73+
74+
/**
75+
* Use windowing for long lists (strongly recommended to also define a width on List or its container)
76+
* 'none' - default with children are <= 100.
77+
* 'fixed' - better performance, default when first child is a ListItem (height will default to 100%)
78+
*/
79+
windowing?: 'fixed' | 'none'
80+
}
8481

8582
const getListItemHeight = (child: ReactChild, height: number) => {
8683
if (isValidElement(child) && child.props.description) {
@@ -93,6 +90,7 @@ export const ListInternal = forwardRef(
9390
(
9491
{
9592
children,
93+
color,
9694
density = 0,
9795
disabled,
9896
height,
@@ -144,9 +142,9 @@ export const ListInternal = forwardRef(
144142
})
145143

146144
const context = {
145+
color: keyColor ? 'key' : color,
147146
density,
148147
iconGutter,
149-
keyColor,
150148
}
151149

152150
return (
@@ -180,4 +178,4 @@ const ListStyle = styled.ul
180178
padding: 0;
181179
`
182180

183-
export const List = styled(ListInternal)``
181+
export const List = styled(ListInternal)<ListProps>``

packages/components/src/List/ListItem.story.tsx

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,46 +30,75 @@ import { PersonOutline } from '@styled-icons/material/PersonOutline'
3030
import { DateRange } from '@styled-icons/material-outlined/DateRange'
3131
import { SubdirectoryArrowLeft } from '@styled-icons/material/SubdirectoryArrowLeft'
3232
import { IconButton } from '../Button'
33-
import { Text } from '../Text'
33+
import { Heading, Text } from '../Text'
3434
import { Grid } from '../Layout/Grid'
35+
import { SpaceVertical } from '../Layout/Space/SpaceVertical'
3536
import { ListItem, ListItemProps } from './ListItem'
3637
import { ListItemRole } from './types'
38+
import { List } from './List'
3739

3840
const Template: Story<ListItemProps> = (args) => <ListItem {...args} />
3941

4042
export const Basic = Template.bind({})
43+
const basicArgs = { children: 'List Item' }
4144
Basic.args = {
42-
children: 'List Item',
45+
...basicArgs,
4346
}
4447

4548
export const Icon = Template.bind({})
46-
Icon.args = {
47-
...Basic.args,
49+
const iconArgs = {
50+
...basicArgs,
4851
icon: <PersonOutline />,
4952
}
50-
51-
export const IconColor = Template.bind({})
52-
IconColor.args = {
53-
...Basic.args,
54-
color: 'warn',
55-
icon: <PersonOutline />,
53+
Icon.args = {
54+
...iconArgs,
5655
}
5756

58-
export const IconCustomColor = Template.bind({})
59-
IconCustomColor.args = {
60-
...Basic.args,
61-
color: '#cc00cc',
62-
icon: <PersonOutline />,
63-
}
57+
export const IconColor = () => <ListItem {...iconArgs} color="calculation" />
58+
export const IconCustomColor = () => <ListItem {...iconArgs} color="#cc00cc" />
59+
export const IconColorDisabled = () => (
60+
<ListItem {...iconArgs} color="warn" disabled />
61+
)
6462

65-
export const IconColorDisabled = Template.bind({})
66-
IconColorDisabled.args = {
67-
...Basic.args,
68-
color: 'warn',
69-
disabled: true,
70-
icon: <PersonOutline />,
63+
const Example: FC<ListItemProps> = ({ children, keyColor, ...props }) => {
64+
const args = {
65+
icon: <PersonOutline />,
66+
...props,
67+
}
68+
69+
return (
70+
<SpaceVertical>
71+
<Heading as="h3">{children}</Heading>
72+
<List width="100%">
73+
<ListItem {...args}>Default</ListItem>
74+
<ListItem {...args} color="key">
75+
Key
76+
</ListItem>
77+
<ListItem {...args} color="calculation">
78+
Calculation
79+
</ListItem>
80+
<ListItem {...args} color="dimension">
81+
Dimension
82+
</ListItem>
83+
<ListItem {...args} color="measure">
84+
Measure
85+
</ListItem>
86+
</List>
87+
</SpaceVertical>
88+
)
7189
}
7290

91+
export const ColorComparison = () => (
92+
<Grid columns={4}>
93+
<Example>Default</Example>
94+
<Example selected>Selected</Example>
95+
<Example selected disabled>
96+
Selected + Disabled
97+
</Example>
98+
<Example hovered>Hover</Example>
99+
</Grid>
100+
)
101+
73102
export const Detail = Template.bind({})
74103
Detail.args = {
75104
...Basic.args,
@@ -151,11 +180,7 @@ Disabled.args = {
151180
disabled: true,
152181
}
153182

154-
export const KeyColor = Template.bind({})
155-
KeyColor.args = {
156-
...Selected.args,
157-
keyColor: true,
158-
}
183+
export const KeyColor = () => <ListItem selected keyColor {...basicArgs} />
159184

160185
export const Link = () => {
161186
return (

packages/components/src/List/ListItem.test.tsx

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,88 @@ import React from 'react'
2929
import { renderWithTheme } from '@looker/components-test-utils'
3030
import { fireEvent, configure, screen } from '@testing-library/react'
3131
import { Science } from '@styled-icons/material-outlined/Science'
32-
32+
import { List } from './List'
3333
import { ListItem } from './ListItem'
3434

3535
describe('ListItem', () => {
36-
test('renders children', () => {
36+
test('children', () => {
3737
renderWithTheme(<ListItem>who!</ListItem>)
3838
expect(screen.getByText('who!')).toBeVisible()
3939
})
4040

41-
test('renders description', () => {
41+
test('description', () => {
4242
renderWithTheme(<ListItem description="are you?">who!</ListItem>)
4343
expect(screen.getByText('are you?')).toBeVisible()
4444
expect(screen.getByRole('listitem')).not.toHaveAttribute('description')
4545
})
4646

47-
test('renders detail', () => {
47+
test('detail', () => {
4848
renderWithTheme(<ListItem detail="Is an excellent question">who!</ListItem>)
4949
expect(screen.getByText('Is an excellent question')).toBeVisible()
5050
expect(screen.getByRole('listitem')).not.toHaveAttribute('detail')
5151
})
5252

53+
test('iconGutter', () => {
54+
renderWithTheme(
55+
<List iconGutter>
56+
<ListItem>who!</ListItem>
57+
</List>
58+
)
59+
expect(screen.getByText('who!')).toBeVisible()
60+
})
61+
62+
describe('color', () => {
63+
test('keyColor', () => {
64+
renderWithTheme(
65+
<ListItem selected keyColor>
66+
who!
67+
</ListItem>
68+
)
69+
expect(screen.getByText('who!')).toHaveStyle('color: #262d33')
70+
expect(screen.getByRole('listitem')).toHaveStyle('background: #f3f2ff')
71+
})
72+
73+
test('theme', () => {
74+
renderWithTheme(
75+
<ListItem color="calculation" icon={<Science data-testid="icon" />}>
76+
who!
77+
</ListItem>
78+
)
79+
expect(screen.getByText('who!')).toHaveStyle('color: #319220')
80+
expect(screen.getByTestId('icon')).toHaveStyle('color: #319220')
81+
})
82+
83+
test('theme selected', () => {
84+
renderWithTheme(
85+
<ListItem selected color="calculation">
86+
who!
87+
</ListItem>
88+
)
89+
expect(screen.getByText('who!')).toHaveStyle('color: #319220')
90+
expect(screen.getByRole('listitem')).toHaveStyle('background: #eaf4e8')
91+
})
92+
93+
test('theme + selected + hover', () => {
94+
renderWithTheme(
95+
<ListItem hovered selected color="calculation">
96+
who!
97+
</ListItem>
98+
)
99+
expect(screen.getByText('who!')).toHaveStyle('color: #319220')
100+
expect(screen.getByRole('listitem')).toHaveStyle('background: #eaf4e8')
101+
})
102+
103+
test('custom', () => {
104+
renderWithTheme(
105+
<ListItem color="#cc0000" icon={<Science data-testid="icon" />}>
106+
who!
107+
</ListItem>
108+
)
109+
expect(screen.getByText('who!')).toHaveStyle('color: #cc0000')
110+
expect(screen.getByTestId('icon')).toHaveStyle('color: #cc0000')
111+
})
112+
})
113+
53114
test('truncate', () => {
54115
renderWithTheme(
55116
<ListItem truncate>

0 commit comments

Comments
 (0)