Skip to content

Commit 441efab

Browse files
MichaelSun48malwilley
authored andcommitted
feat(custom-views): Add new add view button and temporary tab designs (#75206)
closes #73230 closes #73338 This PR adds the `Add View` button to the DraggableTabBar component as well as the design for temporary tabs. It also makes some refactors to the existing draggable tabs components, such as creating the dropdown menu options directly in the draggableTabBar rather than the menu component. <img width="653" alt="image" src="https://github.com/user-attachments/assets/81e18301-900d-4c07-83a8-4c413f7caaf9"> --------- Co-authored-by: Malachi Willey <[email protected]>
1 parent d85728c commit 441efab

File tree

5 files changed

+264
-123
lines changed

5 files changed

+264
-123
lines changed

static/app/components/draggableTabs/draggableTabList.tsx

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import {useTabListState} from '@react-stately/tabs';
99
import type {Node, Orientation} from '@react-types/shared';
1010
import {Reorder} from 'framer-motion';
1111

12+
import {Button} from 'sentry/components/button';
1213
import type {SelectOption} from 'sentry/components/compactSelect';
1314
import {TabsContext} from 'sentry/components/tabs';
1415
import {type BaseTabProps, Tab} from 'sentry/components/tabs/tab';
1516
import {OverflowMenu, useOverflowTabs} from 'sentry/components/tabs/tabList';
1617
import {tabsShouldForwardProp} from 'sentry/components/tabs/utils';
18+
import {IconAdd} from 'sentry/icons';
1719
import {space} from 'sentry/styles/space';
1820
import {browserHistory} from 'sentry/utils/browserHistory';
1921

@@ -29,6 +31,8 @@ function BaseDraggableTabList({
2931
className,
3032
outerWrapStyles,
3133
onReorder,
34+
onAddView,
35+
showTempTab = false,
3236
tabVariant = 'filled',
3337
...props
3438
}: BaseDraggableTabListProps) {
@@ -103,6 +107,11 @@ function BaseDraggableTabList({
103107
});
104108
}, [state.collection, overflowTabs]);
105109

110+
const persistentTabs = [...state.collection].filter(
111+
item => item.key !== 'temporary-tab'
112+
);
113+
const tempTab = [...state.collection].find(item => item.key === 'temporary-tab');
114+
106115
return (
107116
<TabListOuterWrap style={outerWrapStyles}>
108117
<Reorder.Group
@@ -115,10 +124,11 @@ function BaseDraggableTabList({
115124
{...tabListProps}
116125
orientation={orientation}
117126
hideBorder={hideBorder}
127+
borderStyle={state.selectedKey === 'temporary-tab' ? 'dashed' : 'solid'}
118128
className={className}
119129
ref={tabListRef}
120130
>
121-
{[...state.collection].map(item => (
131+
{persistentTabs.map(item => (
122132
<Reorder.Item
123133
key={item.key}
124134
value={item}
@@ -136,12 +146,32 @@ function BaseDraggableTabList({
136146
variant={tabVariant}
137147
/>
138148

139-
{state.selectedKey !== item.key &&
140-
state.collection.getKeyAfter(item.key) !== state.selectedKey && (
141-
<TabDivider />
142-
)}
149+
{(state.selectedKey === 'temporary-tab' ||
150+
(state.selectedKey !== item.key &&
151+
state.collection.getKeyAfter(item.key) !== state.selectedKey)) && (
152+
<TabDivider />
153+
)}
143154
</Reorder.Item>
144155
))}
156+
<AddViewButton borderless size="zero" onClick={onAddView}>
157+
<StyledIconAdd size="xs" />
158+
t('Add View')
159+
</AddViewButton>
160+
<TabDivider />
161+
{showTempTab && tempTab && (
162+
<Tab
163+
key={tempTab.key}
164+
item={tempTab}
165+
state={state}
166+
orientation={orientation}
167+
overflowing={
168+
orientation === 'horizontal' && overflowTabs.includes(tempTab.key)
169+
}
170+
ref={element => (tabItemsRef.current[tempTab.key] = element)}
171+
variant={tabVariant}
172+
borderStyle="dashed"
173+
/>
174+
)}
145175
</TabListWrap>
146176
</Reorder.Group>
147177

@@ -164,15 +194,22 @@ export interface DraggableTabListProps
164194
onReorder: (newOrder: Node<DraggableTabListItemProps>[]) => void;
165195
className?: string;
166196
hideBorder?: boolean;
197+
onAddView?: React.MouseEventHandler;
167198
outerWrapStyles?: React.CSSProperties;
199+
showTempTab?: boolean;
168200
tabVariant?: BaseTabProps['variant'];
169201
}
170202

171203
/**
172204
* To be used as a direct child of the <Tabs /> component. See example usage
173205
* in tabs.stories.js
174206
*/
175-
export function DraggableTabList({items, ...props}: DraggableTabListProps) {
207+
export function DraggableTabList({
208+
items,
209+
onAddView,
210+
showTempTab,
211+
...props
212+
}: DraggableTabListProps) {
176213
const collection = useCollection({items, ...props}, collectionFactory);
177214

178215
const parsedItems = useMemo(
@@ -190,7 +227,13 @@ export function DraggableTabList({items, ...props}: DraggableTabListProps) {
190227
);
191228

192229
return (
193-
<BaseDraggableTabList items={parsedItems} disabledKeys={disabledKeys} {...props}>
230+
<BaseDraggableTabList
231+
items={parsedItems}
232+
onAddView={onAddView}
233+
showTempTab={showTempTab}
234+
disabledKeys={disabledKeys}
235+
{...props}
236+
>
194237
{item => <Item {...item} />}
195238
</BaseDraggableTabList>
196239
);
@@ -213,6 +256,7 @@ const TabListOuterWrap = styled('div')`
213256
const TabListWrap = styled('ul', {
214257
shouldForwardProp: tabsShouldForwardProp,
215258
})<{
259+
borderStyle: 'dashed' | 'solid';
216260
hideBorder: boolean;
217261
orientation: Orientation;
218262
}>`
@@ -229,7 +273,7 @@ const TabListWrap = styled('ul', {
229273
? `
230274
grid-auto-flow: column;
231275
justify-content: start;
232-
${!p.hideBorder && `border-bottom: solid 1px ${p.theme.border};`}
276+
${!p.hideBorder && `border-bottom: ${p.borderStyle} 1px ${p.theme.border};`}
233277
stroke-dasharray: 4, 3;
234278
`
235279
: `
@@ -238,6 +282,18 @@ const TabListWrap = styled('ul', {
238282
align-content: start;
239283
gap: 1px;
240284
padding-right: ${space(2)};
241-
${!p.hideBorder && `border-right: solid 1px ${p.theme.border};`}
285+
${!p.hideBorder && `border-right: ${p.borderStyle} 1px ${p.theme.border};`}
242286
`};
243287
`;
288+
289+
const AddViewButton = styled(Button)`
290+
color: ${p => p.theme.gray300};
291+
padding-right: ${space(0.5)};
292+
margin: 3px 2px 2px 2px;
293+
font-weight: normal;
294+
`;
295+
296+
const StyledIconAdd = styled(IconAdd)`
297+
margin-right: 4px;
298+
margin-left: 2px;
299+
`;

static/app/components/draggableTabs/index.stories.tsx

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Fragment} from 'react';
1+
import {Fragment, useState} from 'react';
22
import styled from '@emotion/styled';
33

44
import JSXNode from 'sentry/components/stories/jsxNode';
@@ -37,24 +37,37 @@ export default storyBook(DraggableTabBar, story => {
3737
},
3838
];
3939

40-
story('Default', () => (
41-
<Fragment>
42-
<p>
43-
You should be using all of <JSXNode name="Tabs" />, <JSXNode name="TabList" />,{' '}
44-
<JSXNode name="TabList.Item" />, <JSXNode name="DroppableTabPanels" /> and
45-
<JSXNode name="DroppableTabPanels.Item" /> components.
46-
</p>
47-
<p>
48-
This will give you all kinds of accessibility and state tracking out of the box.
49-
But you will have to render all tab content, including hooks, upfront.
50-
</p>
51-
<SizingWindow>
52-
<TabBarContainer>
53-
<DraggableTabBar tabs={TABS} />
54-
</TabBarContainer>
55-
</SizingWindow>
56-
</Fragment>
57-
));
40+
story('Default', () => {
41+
const [showTempTab, setShowTempTab] = useState(false);
42+
return (
43+
<Fragment>
44+
<p>
45+
You should be using all of <JSXNode name="Tabs" />, <JSXNode name="TabList" />,{' '}
46+
<JSXNode name="TabList.Item" />, <JSXNode name="DroppableTabPanels" /> and
47+
<JSXNode name="DroppableTabPanels.Item" /> components.
48+
</p>
49+
<p>
50+
This will give you all kinds of accessibility and state tracking out of the box.
51+
But you will have to render all tab content, including hooks, upfront.
52+
</p>
53+
<SizingWindow>
54+
<TabBarContainer>
55+
<DraggableTabBar
56+
tabs={TABS}
57+
showTempTab={showTempTab}
58+
tempTabContent={
59+
<TabPanelContainer>This is a Temporary view</TabPanelContainer>
60+
}
61+
// The add view button should NOT toggle the temp tab normally.
62+
// This is a very temporary way to show off the temp tab design to PR reviewers,
63+
// and it will be removed in the very near future
64+
onAddView={() => setShowTempTab(!showTempTab)}
65+
/>
66+
</TabBarContainer>
67+
</SizingWindow>
68+
</Fragment>
69+
);
70+
});
5871
});
5972

6073
const TabBarContainer = styled('div')`

static/app/components/tabs/tab.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface TabProps extends AriaTabProps {
2828
*/
2929
overflowing: boolean;
3030
state: TabListState<any>;
31+
borderStyle?: BaseTabProps['borderStyle'];
3132
variant?: BaseTabProps['variant'];
3233
}
3334

@@ -150,7 +151,7 @@ export const BaseTab = forwardRef(
150151
*/
151152
export const Tab = forwardRef(
152153
(
153-
{item, state, orientation, overflowing, variant}: TabProps,
154+
{item, state, orientation, overflowing, variant, borderStyle = 'solid'}: TabProps,
154155
forwardedRef: React.ForwardedRef<HTMLLIElement>
155156
) => {
156157
const ref = useObjectRef(forwardedRef);
@@ -171,6 +172,7 @@ export const Tab = forwardRef(
171172
orientation={orientation}
172173
overflowing={overflowing}
173174
ref={ref}
175+
borderStyle={borderStyle}
174176
variant={variant}
175177
>
176178
{rendered}

0 commit comments

Comments
 (0)