Skip to content

Commit 0e78432

Browse files
authored
feat(ui): chonkify select (#86984)
This is for single select only - multi select will be a follow-up PR
1 parent e1cbf1d commit 0e78432

File tree

9 files changed

+931
-311
lines changed

9 files changed

+931
-311
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import isPropValid from '@emotion/is-prop-valid';
2+
import type {DO_NOT_USE_ChonkTheme} from '@emotion/react';
3+
4+
import {space} from 'sentry/styles/space';
5+
import type {FormSize} from 'sentry/utils/theme';
6+
import {chonkStyled} from 'sentry/utils/theme/theme.chonk';
7+
8+
/**
9+
* Menu item priority. Determines the text and background color.
10+
*/
11+
export type Priority = 'primary' | 'danger' | 'default';
12+
13+
function getTextColor({
14+
theme,
15+
priority,
16+
disabled,
17+
}: {
18+
disabled: boolean;
19+
priority: Priority;
20+
theme: DO_NOT_USE_ChonkTheme;
21+
}) {
22+
if (disabled) {
23+
return theme.subText;
24+
}
25+
switch (priority) {
26+
case 'primary':
27+
return theme.colors.content.accent;
28+
case 'danger':
29+
return theme.errorText;
30+
case 'default':
31+
default:
32+
return theme.textColor;
33+
}
34+
}
35+
36+
export const ChonkInnerWrap = chonkStyled('div', {
37+
shouldForwardProp: prop =>
38+
typeof prop === 'string' &&
39+
isPropValid(prop) &&
40+
!['disabled', 'isFocused', 'priority'].includes(prop),
41+
})<{
42+
disabled: boolean;
43+
isFocused: boolean;
44+
priority: Priority;
45+
size: FormSize;
46+
}>`
47+
display: flex;
48+
position: relative;
49+
padding: 0 ${space(1)} 0 ${space(1.5)};
50+
border-radius: ${p => p.theme.borderRadius};
51+
box-sizing: border-box;
52+
53+
font-size: ${p => p.theme.form[p.size ?? 'md'].fontSize};
54+
55+
&,
56+
&:hover {
57+
color: ${getTextColor};
58+
}
59+
${p => p.disabled && `cursor: default;`}
60+
61+
&::before {
62+
content: '';
63+
position: absolute;
64+
top: 0;
65+
left: 0;
66+
width: 100%;
67+
height: 100%;
68+
z-index: -1;
69+
}
70+
71+
${p =>
72+
p.isFocused &&
73+
`
74+
z-index: 1;
75+
/* Background to hide the previous item's divider */
76+
::before {
77+
background: ${p.theme.backgroundElevated};
78+
}
79+
`}
80+
`;
81+
82+
/**
83+
* Returns the appropriate vertical padding based on the size prop. To be used
84+
* as top/bottom padding/margin in ContentWrap and LeadingItems.
85+
*/
86+
const getVerticalPadding = (size: FormSize) => {
87+
switch (size) {
88+
case 'xs':
89+
return space(0.5);
90+
case 'sm':
91+
return space(0.75);
92+
case 'md':
93+
default:
94+
return space(1);
95+
}
96+
};
97+
98+
export const ChonkContentWrap = chonkStyled('div')<{
99+
isFocused: boolean;
100+
showDivider: boolean;
101+
size: FormSize;
102+
}>`
103+
position: relative;
104+
width: 100%;
105+
height: ${p => p.theme.form[p.size ?? 'md'].height};
106+
min-width: 0;
107+
display: flex;
108+
gap: ${space(1)};
109+
justify-content: space-between;
110+
padding: ${p => getVerticalPadding(p.size)} 0;
111+
112+
${p =>
113+
p.showDivider &&
114+
!p.isFocused &&
115+
`
116+
li:not(:last-child) &::after {
117+
content: '';
118+
position: absolute;
119+
left: 0;
120+
bottom: 0;
121+
width: 100%;
122+
height: 1px;
123+
box-shadow: 0 1px 0 0 ${p.theme.innerBorder};
124+
}
125+
`}
126+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import styled from '@emotion/styled';
2+
3+
import {MenuListItem} from 'sentry/components/core/menuListItem/index';
4+
import {Grid} from 'sentry/components/stories/sideBySide';
5+
import storyBook from 'sentry/stories/storyBook';
6+
import {space} from 'sentry/styles/space';
7+
8+
// eslint-disable-next-line import/no-webpack-loader-syntax
9+
import types from '!!type-loader!sentry/components/core/menuListItem';
10+
11+
export default storyBook('MenuListItem', (story, APIReference) => {
12+
APIReference(types.MenuListItem);
13+
14+
story('focused', () => {
15+
return <SizeVariants isFocused />;
16+
});
17+
18+
story('selected', () => {
19+
return <SizeVariants isSelected priority="primary" />;
20+
});
21+
22+
story('focused & selected', () => {
23+
return <SizeVariants isFocused isSelected priority="primary" />;
24+
});
25+
26+
story('disabled', () => {
27+
return <SizeVariants disabled />;
28+
});
29+
30+
story('with trailing items', () => {
31+
return <SizeVariants trailingItems="🚀" />;
32+
});
33+
});
34+
35+
const Container = styled('div')`
36+
margin-top: ${space(0.5)};
37+
border: 1px solid ${p => p.theme.border};
38+
border-radius: ${p => p.theme.borderRadius};
39+
`;
40+
41+
const leadingItems: React.ComponentProps<typeof MenuListItem>['leadingItems'] = state => {
42+
return state.isSelected ? '✅' : '⬜';
43+
};
44+
45+
function SizeVariants(props: Partial<React.ComponentProps<typeof MenuListItem>>) {
46+
return (
47+
<Grid>
48+
<div>
49+
Medium:
50+
<Container>
51+
<MenuListItem size="md" label="hello" leadingItems={leadingItems} />
52+
<MenuListItem size="md" label="sentry" leadingItems={leadingItems} {...props} />
53+
<MenuListItem size="md" label="world" leadingItems={leadingItems} />
54+
</Container>
55+
</div>
56+
<div>
57+
Small:
58+
<Container>
59+
<MenuListItem size="sm" label="hello" leadingItems={leadingItems} />
60+
<MenuListItem size="sm" label="sentry" leadingItems={leadingItems} {...props} />
61+
<MenuListItem size="sm" label="world" leadingItems={leadingItems} />
62+
</Container>
63+
</div>
64+
<div>
65+
X-Small:
66+
<Container>
67+
<MenuListItem size="xs" label="hello" leadingItems={leadingItems} />
68+
<MenuListItem size="xs" label="sentry" leadingItems={leadingItems} {...props} />
69+
<MenuListItem size="xs" label="world" leadingItems={leadingItems} />
70+
</Container>
71+
</div>
72+
</Grid>
73+
);
74+
}

static/app/components/core/menuListItem/index.tsx

+69-62
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import isPropValid from '@emotion/is-prop-valid';
55
import {type Theme, useTheme} from '@emotion/react';
66
import styled from '@emotion/styled';
77

8+
import {
9+
ChonkContentWrap,
10+
ChonkInnerWrap,
11+
type Priority,
12+
} from 'sentry/components/core/menuListItem/index.chonk';
813
import InteractionStateLayer from 'sentry/components/interactionStateLayer';
914
import {Overlay, PositionWrapper} from 'sentry/components/overlay';
1015
import type {TooltipProps} from 'sentry/components/tooltip';
@@ -13,11 +18,7 @@ import {space} from 'sentry/styles/space';
1318
import domId from 'sentry/utils/domId';
1419
import mergeRefs from 'sentry/utils/mergeRefs';
1520
import type {FormSize} from 'sentry/utils/theme';
16-
17-
/**
18-
* Menu item priority. Determines the text and background color.
19-
*/
20-
type Priority = 'primary' | 'danger' | 'default';
21+
import {withChonk} from 'sentry/utils/theme/withChonk';
2122

2223
/**
2324
* Leading/trailing items to be rendered alongside the main text label.
@@ -294,51 +295,54 @@ function getTextColor({
294295
}
295296
}
296297

297-
export const InnerWrap = styled('div', {
298-
shouldForwardProp: prop =>
299-
typeof prop === 'string' &&
300-
isPropValid(prop) &&
301-
!['disabled', 'isFocused', 'priority'].includes(prop),
302-
})<{
303-
disabled: boolean;
304-
isFocused: boolean;
305-
priority: Priority;
306-
size: Props['size'];
307-
}>`
308-
display: flex;
309-
position: relative;
310-
padding: 0 ${space(1)} 0 ${space(1.5)};
311-
border-radius: ${p => p.theme.borderRadius};
312-
box-sizing: border-box;
313-
314-
font-size: ${p => p.theme.form[p.size ?? 'md'].fontSize};
315-
316-
&,
317-
&:hover {
318-
color: ${getTextColor};
319-
}
320-
${p => p.disabled && `cursor: default;`}
321-
322-
&::before {
323-
content: '';
324-
position: absolute;
325-
top: 0;
326-
left: 0;
327-
width: 100%;
328-
height: 100%;
329-
z-index: -1;
330-
}
331-
332-
${p =>
333-
p.isFocused &&
334-
`
298+
export const InnerWrap = withChonk(
299+
styled('div', {
300+
shouldForwardProp: prop =>
301+
typeof prop === 'string' &&
302+
isPropValid(prop) &&
303+
!['disabled', 'isFocused', 'priority'].includes(prop),
304+
})<{
305+
disabled: boolean;
306+
isFocused: boolean;
307+
priority: Priority;
308+
size: Props['size'];
309+
}>`
310+
display: flex;
311+
position: relative;
312+
padding: 0 ${space(1)} 0 ${space(1.5)};
313+
border-radius: ${p => p.theme.borderRadius};
314+
box-sizing: border-box;
315+
316+
font-size: ${p => p.theme.form[p.size ?? 'md'].fontSize};
317+
318+
&,
319+
&:hover {
320+
color: ${getTextColor};
321+
}
322+
${p => p.disabled && `cursor: default;`}
323+
324+
&::before {
325+
content: '';
326+
position: absolute;
327+
top: 0;
328+
left: 0;
329+
width: 100%;
330+
height: 100%;
331+
z-index: -1;
332+
}
333+
334+
${p =>
335+
p.isFocused &&
336+
`
335337
z-index: 1;
336338
/* Background to hide the previous item's divider */
337339
::before {
338340
background: ${p.theme.backgroundElevated};
339341
}
340342
`}
341-
`;
343+
`,
344+
ChonkInnerWrap
345+
);
342346

343347
const StyledInteractionStateLayer = styled(InteractionStateLayer)`
344348
z-index: -1;
@@ -360,23 +364,24 @@ const getVerticalPadding = (size: Props['size']) => {
360364
}
361365
};
362366

363-
const ContentWrap = styled('div')<{
364-
isFocused: boolean;
365-
showDivider: boolean;
366-
size: Props['size'];
367-
}>`
368-
position: relative;
369-
width: 100%;
370-
min-width: 0;
371-
display: flex;
372-
gap: ${space(1)};
373-
justify-content: space-between;
374-
padding: ${p => getVerticalPadding(p.size)} 0;
375-
376-
${p =>
377-
p.showDivider &&
378-
!p.isFocused &&
379-
`
367+
const ContentWrap = withChonk(
368+
styled('div')<{
369+
isFocused: boolean;
370+
showDivider: boolean;
371+
size: Props['size'];
372+
}>`
373+
position: relative;
374+
width: 100%;
375+
min-width: 0;
376+
display: flex;
377+
gap: ${space(1)};
378+
justify-content: space-between;
379+
padding: ${p => getVerticalPadding(p.size)} 0;
380+
381+
${p =>
382+
p.showDivider &&
383+
!p.isFocused &&
384+
`
380385
li:not(:last-child) &::after {
381386
content: '';
382387
position: absolute;
@@ -387,7 +392,9 @@ const ContentWrap = styled('div')<{
387392
box-shadow: 0 1px 0 0 ${p.theme.innerBorder};
388393
}
389394
`}
390-
`;
395+
`,
396+
ChonkContentWrap
397+
);
391398

392399
export const LeadingItems = styled('div')<{
393400
disabled: boolean;

0 commit comments

Comments
 (0)