From d0bc8fea7ce430c847416f434f5330fc5e59474c Mon Sep 17 00:00:00 2001 From: MichaelSun48 Date: Mon, 24 Jun 2024 16:22:57 -0700 Subject: [PATCH 1/7] Copy tabs dir to draggableTabs dir --- .../components/draggableTabs/draggableTab.tsx | 236 ++++++++++++++ .../draggableTabs/draggableTabList.tsx | 300 ++++++++++++++++++ .../draggableTabs/draggableTabPanels.tsx | 99 ++++++ static/app/components/draggableTabs/index.tsx | 91 ++++++ static/app/components/draggableTabs/item.tsx | 12 + static/app/components/draggableTabs/utils.tsx | 4 + 6 files changed, 742 insertions(+) create mode 100644 static/app/components/draggableTabs/draggableTab.tsx create mode 100644 static/app/components/draggableTabs/draggableTabList.tsx create mode 100644 static/app/components/draggableTabs/draggableTabPanels.tsx create mode 100644 static/app/components/draggableTabs/index.tsx create mode 100644 static/app/components/draggableTabs/item.tsx create mode 100644 static/app/components/draggableTabs/utils.tsx diff --git a/static/app/components/draggableTabs/draggableTab.tsx b/static/app/components/draggableTabs/draggableTab.tsx new file mode 100644 index 00000000000000..9f5cd4bfdf1c7b --- /dev/null +++ b/static/app/components/draggableTabs/draggableTab.tsx @@ -0,0 +1,236 @@ +import {forwardRef, useCallback} from 'react'; +import type {Theme} from '@emotion/react'; +import styled from '@emotion/styled'; +import type {AriaTabProps} from '@react-aria/tabs'; +import {useTab} from '@react-aria/tabs'; +import {useObjectRef} from '@react-aria/utils'; +import type {TabListState} from '@react-stately/tabs'; +import type {Node, Orientation} from '@react-types/shared'; + +import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import Link from 'sentry/components/links/link'; +import {space} from 'sentry/styles/space'; + +import {tabsShouldForwardProp} from './utils'; + +interface TabProps extends AriaTabProps { + item: Node; + orientation: Orientation; + /** + * Whether this tab is overflowing the TabList container. If so, the tab + * needs to be visually hidden. Users can instead select it via an overflow + * menu. + */ + overflowing: boolean; + state: TabListState; +} + +/** + * Stops event propagation if the command/ctrl/shift key is pressed, in effect + * preventing any state change. This is useful because when a user + * command/ctrl/shift-clicks on a tab link, the intention is to view the tab + * in a new browser tab/window, not to update the current view. + */ +function handleLinkClick(e: React.PointerEvent) { + if (e.metaKey || e.ctrlKey || e.shiftKey) { + e.stopPropagation(); + } +} + +/** + * Renders a single tab item. This should not be imported directly into any + * page/view – it's only meant to be used by . See the correct + * usage in tabs.stories.js + */ +function BaseTab( + {item, state, orientation, overflowing}: TabProps, + forwardedRef: React.ForwardedRef +) { + const ref = useObjectRef(forwardedRef); + + const { + key, + rendered, + props: {to, hidden}, + } = item; + const {tabProps, isSelected} = useTab({key, isDisabled: hidden}, state, ref); + + const InnerWrap = useCallback( + ({children}) => + to ? ( + + {children} + + ) : ( + {children} + ), + [to, orientation] + ); + + return ( + + ); +} + +export const Tab = forwardRef(BaseTab); + +const TabWrap = styled('li', {shouldForwardProp: tabsShouldForwardProp})<{ + overflowing: boolean; + selected: boolean; +}>` + color: ${p => (p.selected ? p.theme.activeText : p.theme.textColor)}; + white-space: nowrap; + cursor: pointer; + + &:hover { + color: ${p => (p.selected ? p.theme.activeText : p.theme.headingColor)}; + } + + &:focus { + outline: none; + } + + &[aria-disabled], + &[aria-disabled]:hover { + color: ${p => p.theme.subText}; + pointer-events: none; + cursor: default; + } + + ${p => + p.overflowing && + ` + opacity: 0; + pointer-events: none; + `} +`; + +const innerWrapStyles = ({ + theme, + orientation, +}: { + orientation: Orientation; + theme: Theme; +}) => ` + display: flex; + align-items: center; + position: relative; + height: calc( + ${theme.form.sm.height}px + + ${orientation === 'horizontal' ? space(0.75) : '0px'} + ); + border-radius: ${theme.borderRadius}; + transform: translateY(1px); + + ${ + orientation === 'horizontal' + ? ` + /* Extra padding + negative margin trick, to expand click area */ + padding: ${space(0.75)} ${space(1)} ${space(1.5)}; + margin-left: -${space(1)}; + margin-right: -${space(1)}; + ` + : `padding: ${space(0.75)} ${space(2)};` + }; +`; + +const TabLink = styled(Link)<{orientation: Orientation}>` + ${innerWrapStyles} + + &, + &:hover { + color: inherit; + } +`; + +const TabInnerWrap = styled('span')<{orientation: Orientation}>` + ${innerWrapStyles} +`; + +const StyledInteractionStateLayer = styled(InteractionStateLayer)<{ + orientation: Orientation; +}>` + position: absolute; + width: auto; + height: auto; + transform: none; + left: 0; + right: 0; + top: 0; + bottom: ${p => (p.orientation === 'horizontal' ? space(0.75) : 0)}; +`; + +const FocusLayer = styled('div')<{orientation: Orientation}>` + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: ${p => (p.orientation === 'horizontal' ? space(0.75) : 0)}; + + pointer-events: none; + border-radius: inherit; + z-index: 0; + transition: box-shadow 0.1s ease-out; + + li:focus-visible & { + box-shadow: + ${p => p.theme.focusBorder} 0 0 0 1px, + inset ${p => p.theme.focusBorder} 0 0 0 1px; + } +`; + +const TabSelectionIndicator = styled('div')<{ + orientation: Orientation; + selected: boolean; +}>` + position: absolute; + border-radius: 2px; + pointer-events: none; + background: ${p => (p.selected ? p.theme.active : 'transparent')}; + transition: background 0.1s ease-out; + + li[aria-disabled='true'] & { + background: ${p => (p.selected ? p.theme.subText : 'transparent')}; + } + + ${p => + p.orientation === 'horizontal' + ? ` + width: calc(100% - ${space(2)}); + height: 3px; + + bottom: 0; + left: 50%; + transform: translateX(-50%); + ` + : ` + width: 3px; + height: 50%; + + left: 0; + top: 50%; + transform: translateY(-50%); + `}; +`; diff --git a/static/app/components/draggableTabs/draggableTabList.tsx b/static/app/components/draggableTabs/draggableTabList.tsx new file mode 100644 index 00000000000000..2ef4d52a21c287 --- /dev/null +++ b/static/app/components/draggableTabs/draggableTabList.tsx @@ -0,0 +1,300 @@ +import {useContext, useEffect, useMemo, useRef, useState} from 'react'; +import styled from '@emotion/styled'; +import type {AriaTabListOptions} from '@react-aria/tabs'; +import {useTabList} from '@react-aria/tabs'; +import {useCollection} from '@react-stately/collections'; +import {ListCollection} from '@react-stately/list'; +import type {TabListStateOptions} from '@react-stately/tabs'; +import {useTabListState} from '@react-stately/tabs'; +import type {Node, Orientation} from '@react-types/shared'; + +import type {SelectOption} from 'sentry/components/compactSelect'; +import {CompactSelect} from 'sentry/components/compactSelect'; +import DropdownButton from 'sentry/components/dropdownButton'; +import {IconEllipsis} from 'sentry/icons'; +import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import {browserHistory} from 'sentry/utils/browserHistory'; + +import {Tab} from './draggableTab'; +import {TabsContext} from './index'; +import type {TabListItemProps} from './item'; +import {Item} from './item'; +import {tabsShouldForwardProp} from './utils'; + +/** + * Uses IntersectionObserver API to detect overflowing tabs. Returns an array + * containing of keys of overflowing tabs. + */ +function useOverflowTabs({ + tabListRef, + tabItemsRef, + tabItems, +}: { + tabItems: TabListItemProps[]; + tabItemsRef: React.RefObject>; + tabListRef: React.RefObject; +}) { + const [overflowTabs, setOverflowTabs] = useState>([]); + + useEffect(() => { + const options = { + root: tabListRef.current, + // Nagative right margin to account for overflow menu's trigger button + rootMargin: `0px -42px 1px ${space(1)}`, + // Use 0.95 rather than 1 because of a bug in Edge (Windows) where the intersection + // ratio may unexpectedly drop to slightly below 1 (0.999…) on page scroll. + threshold: 0.95, + }; + + const callback: IntersectionObserverCallback = entries => { + entries.forEach(entry => { + const {target} = entry; + const {key} = (target as HTMLElement).dataset; + if (!key) { + return; + } + + if (!entry.isIntersecting) { + setOverflowTabs(prev => prev.concat([key])); + return; + } + + setOverflowTabs(prev => prev.filter(k => k !== key)); + }); + }; + + const observer = new IntersectionObserver(callback, options); + Object.values(tabItemsRef.current ?? {}).forEach( + element => element && observer.observe(element) + ); + + return () => observer.disconnect(); + }, [tabListRef, tabItemsRef]); + + const tabItemKeyToHiddenMap = tabItems.reduce( + (acc, next) => ({ + ...acc, + [next.key]: next.hidden, + }), + {} + ); + + // Tabs that are hidden will be rendered with display: none so won't intersect, + // but we don't want to show them in the overflow menu + return overflowTabs.filter(tabKey => !tabItemKeyToHiddenMap[tabKey]); +} + +export interface TabListProps + extends AriaTabListOptions, + TabListStateOptions { + className?: string; + hideBorder?: boolean; + outerWrapStyles?: React.CSSProperties; +} + +interface BaseTabListProps extends TabListProps { + items: TabListItemProps[]; +} + +function BaseTabList({ + hideBorder = false, + className, + outerWrapStyles, + ...props +}: BaseTabListProps) { + const tabListRef = useRef(null); + const {rootProps, setTabListState} = useContext(TabsContext); + const { + value, + defaultValue, + onChange, + disabled, + orientation = 'horizontal', + keyboardActivation = 'manual', + ...otherRootProps + } = rootProps; + + // Load up list state + const ariaProps = { + selectedKey: value, + defaultSelectedKey: defaultValue, + onSelectionChange: key => { + onChange?.(key); + + // If the newly selected tab is a tab link, then navigate to the specified link + const linkTo = [...(props.items ?? [])].find(item => item.key === key)?.to; + if (!linkTo) { + return; + } + browserHistory.push(linkTo); + }, + isDisabled: disabled, + keyboardActivation, + ...otherRootProps, + ...props, + }; + + const state = useTabListState(ariaProps); + const {tabListProps} = useTabList({orientation, ...ariaProps}, state, tabListRef); + useEffect(() => { + setTabListState(state); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state.disabledKeys, state.selectedItem, state.selectedKey, props.children]); + + // Detect tabs that overflow from the wrapper and put them in an overflow menu + const tabItemsRef = useRef>({}); + const overflowTabs = useOverflowTabs({ + tabListRef, + tabItemsRef, + tabItems: props.items, + }); + + const overflowMenuItems = useMemo(() => { + // Sort overflow items in the order that they appear in TabList + const sortedKeys = [...state.collection].map(item => item.key); + const sortedOverflowTabs = overflowTabs.sort( + (a, b) => sortedKeys.indexOf(a) - sortedKeys.indexOf(b) + ); + + return sortedOverflowTabs.flatMap>(key => { + const item = state.collection.getItem(key); + + if (!item) { + return []; + } + + return { + value: key, + label: item.props.children, + disabled: item.props.disabled, + textValue: item.textValue, + }; + }); + }, [state.collection, overflowTabs]); + + return ( + + + {[...state.collection].map(item => ( + (tabItemsRef.current[item.key] = element)} + /> + ))} + + + {orientation === 'horizontal' && overflowMenuItems.length > 0 && ( + + state.setSelectedKey(opt.value)} + disabled={disabled} + position="bottom-end" + size="sm" + offset={4} + trigger={triggerProps => ( + } + aria-label={t('More tabs')} + /> + )} + /> + + )} + + ); +} + +const collectionFactory = (nodes: Iterable>) => new ListCollection(nodes); + +/** + * To be used as a direct child of the component. See example usage + * in tabs.stories.js + */ +export function TabList({items, ...props}: TabListProps) { + /** + * Initial, unfiltered list of tab items. + */ + const collection = useCollection({items, ...props}, collectionFactory); + + const parsedItems = useMemo( + () => [...collection].map(({key, props: itemProps}) => ({key, ...itemProps})), + [collection] + ); + + /** + * List of keys of disabled items (those with a `disbled` prop) to be passed + * into `BaseTabList`. + */ + const disabledKeys = useMemo( + () => parsedItems.filter(item => item.disabled).map(item => item.key), + [parsedItems] + ); + + return ( + + {item => } + + ); +} + +TabList.Item = Item; + +const TabListOuterWrap = styled('div')` + position: relative; +`; + +const TabListWrap = styled('ul', {shouldForwardProp: tabsShouldForwardProp})<{ + hideBorder: boolean; + orientation: Orientation; +}>` + position: relative; + display: grid; + padding: 0; + margin: 0; + list-style-type: none; + flex-shrink: 0; + + ${p => + p.orientation === 'horizontal' + ? ` + grid-auto-flow: column; + justify-content: start; + gap: ${space(2)}; + ${!p.hideBorder && `border-bottom: solid 1px ${p.theme.border};`} + ` + : ` + height: 100%; + grid-auto-flow: row; + align-content: start; + gap: 1px; + padding-right: ${space(2)}; + ${!p.hideBorder && `border-right: solid 1px ${p.theme.border};`} + `}; +`; + +const TabListOverflowWrap = styled('div')` + position: absolute; + right: 0; + bottom: ${space(0.75)}; +`; +const OverflowMenuTrigger = styled(DropdownButton)` + padding-left: ${space(1)}; + padding-right: ${space(1)}; +`; diff --git a/static/app/components/draggableTabs/draggableTabPanels.tsx b/static/app/components/draggableTabs/draggableTabPanels.tsx new file mode 100644 index 00000000000000..38741ed667a49b --- /dev/null +++ b/static/app/components/draggableTabs/draggableTabPanels.tsx @@ -0,0 +1,99 @@ +import {useContext, useRef} from 'react'; +import styled from '@emotion/styled'; +import type {AriaTabPanelProps} from '@react-aria/tabs'; +import {useTabPanel} from '@react-aria/tabs'; +import {useCollection} from '@react-stately/collections'; +import {ListCollection} from '@react-stately/list'; +import type {TabListState} from '@react-stately/tabs'; +import type {CollectionBase, Node, Orientation} from '@react-types/shared'; + +import {TabsContext} from './index'; +import {Item} from './item'; +import {tabsShouldForwardProp} from './utils'; + +const collectionFactory = (nodes: Iterable>) => new ListCollection(nodes); + +interface TabPanelsProps extends AriaTabPanelProps, CollectionBase { + className?: string; +} + +/** + * To be used as a direct child of the component. See example usage + * in tabs.stories.js + */ +export function TabPanels(props: TabPanelsProps) { + const { + rootProps: {orientation, items}, + tabListState, + } = useContext(TabsContext); + + // Parse child tab panels from props and identify the selected panel + const collection = useCollection({items, ...props}, collectionFactory, { + suppressTextValueWarning: true, + }); + const selectedPanel = tabListState + ? collection.getItem(tabListState.selectedKey) + : null; + + if (!tabListState) { + return null; + } + + return ( + + {selectedPanel?.props.children} + + ); +} + +TabPanels.Item = Item; + +interface TabPanelProps extends AriaTabPanelProps { + state: TabListState; + children?: React.ReactNode; + className?: string; + orientation?: Orientation; +} + +function TabPanel({ + state, + orientation = 'horizontal', + className, + children, + ...props +}: TabPanelProps) { + const ref = useRef(null); + const {tabPanelProps} = useTabPanel(props, state, ref); + + return ( + + {children} + + ); +} + +const TabPanelWrap = styled('div', {shouldForwardProp: tabsShouldForwardProp})<{ + orientation: Orientation; +}>` + border-radius: ${p => p.theme.borderRadius}; + + ${p => (p.orientation === 'horizontal' ? `height: 100%;` : `width: 100%;`)}; + + &:focus-visible { + outline: none; + box-shadow: + inset ${p => p.theme.focusBorder} 0 0 0 1px, + ${p => p.theme.focusBorder} 0 0 0 1px; + z-index: 1; + } +`; diff --git a/static/app/components/draggableTabs/index.tsx b/static/app/components/draggableTabs/index.tsx new file mode 100644 index 00000000000000..7ded52b54ed766 --- /dev/null +++ b/static/app/components/draggableTabs/index.tsx @@ -0,0 +1,91 @@ +import 'intersection-observer'; // polyfill + +import {createContext, useState} from 'react'; +import styled from '@emotion/styled'; +import type {AriaTabListOptions} from '@react-aria/tabs'; +import type {TabListState, TabListStateOptions} from '@react-stately/tabs'; +import type {Orientation} from '@react-types/shared'; + +import {tabsShouldForwardProp} from './utils'; + +export interface TabsProps + extends Omit< + AriaTabListOptions, + 'selectedKey' | 'defaultSelectedKey' | 'onSelectionChange' | 'isDisabled' + >, + Omit< + TabListStateOptions, + | 'children' + | 'selectedKey' + | 'defaultSelectedKey' + | 'onSelectionChange' + | 'isDisabled' + > { + children?: React.ReactNode; + className?: string; + /** + * [Uncontrolled] Default selected tab. Must match the `key` prop on the + * selected tab item. + */ + defaultValue?: T; + disabled?: boolean; + /** + * Callback when the selected tab changes. + */ + onChange?: (key: T) => void; + /** + * [Controlled] Selected tab . Must match the `key` prop on the selected tab + * item. + */ + value?: T; +} + +interface TabContext { + rootProps: Omit, 'children' | 'className'>; + setTabListState: (state: TabListState) => void; + tabListState?: TabListState; +} + +export const TabsContext = createContext({ + rootProps: {orientation: 'horizontal'}, + setTabListState: () => {}, +}); + +/** + * Root tabs component. Provides the necessary data (via React context) for + * child components (TabList and TabPanels) to work together. See example + * usage in tabs.stories.js + */ +export function Tabs({ + orientation = 'horizontal', + className, + children, + ...props +}: TabsProps) { + const [tabListState, setTabListState] = useState>(); + + return ( + + + {children} + + + ); +} + +const TabsWrap = styled('div', {shouldForwardProp: tabsShouldForwardProp})<{ + orientation: Orientation; +}>` + display: flex; + flex-direction: ${p => (p.orientation === 'horizontal' ? 'column' : 'row')}; + flex-grow: 1; + + ${p => + p.orientation === 'vertical' && + ` + height: 100%; + align-items: stretch; + `}; +`; diff --git a/static/app/components/draggableTabs/item.tsx b/static/app/components/draggableTabs/item.tsx new file mode 100644 index 00000000000000..a8aac0aa157cd9 --- /dev/null +++ b/static/app/components/draggableTabs/item.tsx @@ -0,0 +1,12 @@ +import {Item as _Item} from '@react-stately/collections'; +import type {ItemProps} from '@react-types/shared'; +import type {LocationDescriptor} from 'history'; + +export interface TabListItemProps extends ItemProps { + key: string | number; + disabled?: boolean; + hidden?: boolean; + to?: LocationDescriptor; +} + +export const Item = _Item as (props: TabListItemProps) => JSX.Element; diff --git a/static/app/components/draggableTabs/utils.tsx b/static/app/components/draggableTabs/utils.tsx new file mode 100644 index 00000000000000..f8c644fe4b5ea6 --- /dev/null +++ b/static/app/components/draggableTabs/utils.tsx @@ -0,0 +1,4 @@ +import isPropValid from '@emotion/is-prop-valid'; + +export const tabsShouldForwardProp = (prop: string) => + typeof prop === 'string' && isPropValid(prop) && prop !== 'orientation'; From 47cc7a665e0e0610d8965e5fc234ad9d183dcfef Mon Sep 17 00:00:00 2001 From: MichaelSun48 Date: Thu, 25 Jul 2024 14:46:06 -0700 Subject: [PATCH 2/7] Add framer motion to tabs --- .../components/draggableTabs/draggableTab.tsx | 251 ++++-------------- .../draggableTabs/draggableTabList.tsx | 239 +++++++---------- .../draggableTabs/draggableTabPanels.tsx | 99 ------- .../draggableTabs/index.stories.tsx | 64 +++++ static/app/components/draggableTabs/index.tsx | 115 +++----- static/app/components/draggableTabs/item.tsx | 4 +- static/app/components/tabs/tabList.tsx | 2 +- 7 files changed, 248 insertions(+), 526 deletions(-) delete mode 100644 static/app/components/draggableTabs/draggableTabPanels.tsx create mode 100644 static/app/components/draggableTabs/index.stories.tsx diff --git a/static/app/components/draggableTabs/draggableTab.tsx b/static/app/components/draggableTabs/draggableTab.tsx index 9f5cd4bfdf1c7b..e1d863efad6989 100644 --- a/static/app/components/draggableTabs/draggableTab.tsx +++ b/static/app/components/draggableTabs/draggableTab.tsx @@ -1,5 +1,5 @@ -import {forwardRef, useCallback} from 'react'; -import type {Theme} from '@emotion/react'; +import type React from 'react'; +import {forwardRef} from 'react'; import styled from '@emotion/styled'; import type {AriaTabProps} from '@react-aria/tabs'; import {useTab} from '@react-aria/tabs'; @@ -7,13 +7,11 @@ import {useObjectRef} from '@react-aria/utils'; import type {TabListState} from '@react-stately/tabs'; import type {Node, Orientation} from '@react-types/shared'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; -import Link from 'sentry/components/links/link'; -import {space} from 'sentry/styles/space'; +import {BaseTab} from 'sentry/components/tabs/tab'; -import {tabsShouldForwardProp} from './utils'; - -interface TabProps extends AriaTabProps { +interface DraggableTabProps extends AriaTabProps { + // dropState: DroppableCollectionState; + isChanged: boolean; item: Node; orientation: Orientation; /** @@ -25,212 +23,53 @@ interface TabProps extends AriaTabProps { state: TabListState; } -/** - * Stops event propagation if the command/ctrl/shift key is pressed, in effect - * preventing any state change. This is useful because when a user - * command/ctrl/shift-clicks on a tab link, the intention is to view the tab - * in a new browser tab/window, not to update the current view. - */ -function handleLinkClick(e: React.PointerEvent) { - if (e.metaKey || e.ctrlKey || e.shiftKey) { - e.stopPropagation(); - } -} - /** * Renders a single tab item. This should not be imported directly into any * page/view – it's only meant to be used by . See the correct * usage in tabs.stories.js */ -function BaseTab( - {item, state, orientation, overflowing}: TabProps, - forwardedRef: React.ForwardedRef -) { - const ref = useObjectRef(forwardedRef); - - const { - key, - rendered, - props: {to, hidden}, - } = item; - const {tabProps, isSelected} = useTab({key, isDisabled: hidden}, state, ref); - - const InnerWrap = useCallback( - ({children}) => - to ? ( - - {children} - - ) : ( - {children} - ), - [to, orientation] - ); - - return ( - - ); -} - -export const Tab = forwardRef(BaseTab); - -const TabWrap = styled('li', {shouldForwardProp: tabsShouldForwardProp})<{ - overflowing: boolean; - selected: boolean; -}>` - color: ${p => (p.selected ? p.theme.activeText : p.theme.textColor)}; - white-space: nowrap; - cursor: pointer; - - &:hover { - color: ${p => (p.selected ? p.theme.activeText : p.theme.headingColor)}; +export const DraggableTab = forwardRef( + ( + {item, state, orientation, overflowing}: DraggableTabProps, + forwardedRef: React.ForwardedRef + ) => { + const ref = useObjectRef(forwardedRef); + + const { + key, + rendered, + props: {to, hidden}, + } = item; + const {tabProps, isSelected} = useTab({key, isDisabled: hidden}, state, ref); + + return ( + + ); } - - &:focus { - outline: none; - } - - &[aria-disabled], - &[aria-disabled]:hover { - color: ${p => p.theme.subText}; - pointer-events: none; - cursor: default; - } - - ${p => - p.overflowing && - ` - opacity: 0; - pointer-events: none; - `} +); + +const StyledBaseTab = styled(BaseTab)` + padding: 2px 12px 2px 12px; + gap: 8px; + border-radius: 6px 6px 0px 0px; + border: 1px solid ${p => p.theme.gray200}; + opacity: 0px; `; -const innerWrapStyles = ({ - theme, - orientation, -}: { - orientation: Orientation; - theme: Theme; -}) => ` +const TabContentWrap = styled('span')` display: flex; align-items: center; - position: relative; - height: calc( - ${theme.form.sm.height}px + - ${orientation === 'horizontal' ? space(0.75) : '0px'} - ); - border-radius: ${theme.borderRadius}; - transform: translateY(1px); - - ${ - orientation === 'horizontal' - ? ` - /* Extra padding + negative margin trick, to expand click area */ - padding: ${space(0.75)} ${space(1)} ${space(1.5)}; - margin-left: -${space(1)}; - margin-right: -${space(1)}; - ` - : `padding: ${space(0.75)} ${space(2)};` - }; -`; - -const TabLink = styled(Link)<{orientation: Orientation}>` - ${innerWrapStyles} - - &, - &:hover { - color: inherit; - } -`; - -const TabInnerWrap = styled('span')<{orientation: Orientation}>` - ${innerWrapStyles} -`; - -const StyledInteractionStateLayer = styled(InteractionStateLayer)<{ - orientation: Orientation; -}>` - position: absolute; - width: auto; - height: auto; - transform: none; - left: 0; - right: 0; - top: 0; - bottom: ${p => (p.orientation === 'horizontal' ? space(0.75) : 0)}; -`; - -const FocusLayer = styled('div')<{orientation: Orientation}>` - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: ${p => (p.orientation === 'horizontal' ? space(0.75) : 0)}; - - pointer-events: none; - border-radius: inherit; - z-index: 0; - transition: box-shadow 0.1s ease-out; - - li:focus-visible & { - box-shadow: - ${p => p.theme.focusBorder} 0 0 0 1px, - inset ${p => p.theme.focusBorder} 0 0 0 1px; - } -`; - -const TabSelectionIndicator = styled('div')<{ - orientation: Orientation; - selected: boolean; -}>` - position: absolute; - border-radius: 2px; - pointer-events: none; - background: ${p => (p.selected ? p.theme.active : 'transparent')}; - transition: background 0.1s ease-out; - - li[aria-disabled='true'] & { - background: ${p => (p.selected ? p.theme.subText : 'transparent')}; - } - - ${p => - p.orientation === 'horizontal' - ? ` - width: calc(100% - ${space(2)}); - height: 3px; - - bottom: 0; - left: 50%; - transform: translateX(-50%); - ` - : ` - width: 3px; - height: 50%; - - left: 0; - top: 50%; - transform: translateY(-50%); - `}; + flex-direction: row; + gap: 6px; `; diff --git a/static/app/components/draggableTabs/draggableTabList.tsx b/static/app/components/draggableTabs/draggableTabList.tsx index 2ef4d52a21c287..aa446c83c95705 100644 --- a/static/app/components/draggableTabs/draggableTabList.tsx +++ b/static/app/components/draggableTabs/draggableTabList.tsx @@ -1,4 +1,4 @@ -import {useContext, useEffect, useMemo, useRef, useState} from 'react'; +import {useContext, useEffect, useMemo, useRef} from 'react'; import styled from '@emotion/styled'; import type {AriaTabListOptions} from '@react-aria/tabs'; import {useTabList} from '@react-aria/tabs'; @@ -7,102 +7,34 @@ import {ListCollection} from '@react-stately/list'; import type {TabListStateOptions} from '@react-stately/tabs'; import {useTabListState} from '@react-stately/tabs'; import type {Node, Orientation} from '@react-types/shared'; +import {Reorder} from 'framer-motion'; import type {SelectOption} from 'sentry/components/compactSelect'; -import {CompactSelect} from 'sentry/components/compactSelect'; -import DropdownButton from 'sentry/components/dropdownButton'; -import {IconEllipsis} from 'sentry/icons'; -import {t} from 'sentry/locale'; +import type {Tab} from 'sentry/components/draggableTabs'; +import {TabsContext} from 'sentry/components/tabs'; +import {OverflowMenu, useOverflowTabs} from 'sentry/components/tabs/tabList'; import {space} from 'sentry/styles/space'; import {browserHistory} from 'sentry/utils/browserHistory'; -import {Tab} from './draggableTab'; -import {TabsContext} from './index'; -import type {TabListItemProps} from './item'; +import {DraggableTab} from './draggableTab'; +import type {DraggableTabListItemProps} from './item'; import {Item} from './item'; import {tabsShouldForwardProp} from './utils'; -/** - * Uses IntersectionObserver API to detect overflowing tabs. Returns an array - * containing of keys of overflowing tabs. - */ -function useOverflowTabs({ - tabListRef, - tabItemsRef, - tabItems, -}: { - tabItems: TabListItemProps[]; - tabItemsRef: React.RefObject>; - tabListRef: React.RefObject; -}) { - const [overflowTabs, setOverflowTabs] = useState>([]); - - useEffect(() => { - const options = { - root: tabListRef.current, - // Nagative right margin to account for overflow menu's trigger button - rootMargin: `0px -42px 1px ${space(1)}`, - // Use 0.95 rather than 1 because of a bug in Edge (Windows) where the intersection - // ratio may unexpectedly drop to slightly below 1 (0.999…) on page scroll. - threshold: 0.95, - }; - - const callback: IntersectionObserverCallback = entries => { - entries.forEach(entry => { - const {target} = entry; - const {key} = (target as HTMLElement).dataset; - if (!key) { - return; - } - - if (!entry.isIntersecting) { - setOverflowTabs(prev => prev.concat([key])); - return; - } - - setOverflowTabs(prev => prev.filter(k => k !== key)); - }); - }; - - const observer = new IntersectionObserver(callback, options); - Object.values(tabItemsRef.current ?? {}).forEach( - element => element && observer.observe(element) - ); - - return () => observer.disconnect(); - }, [tabListRef, tabItemsRef]); - - const tabItemKeyToHiddenMap = tabItems.reduce( - (acc, next) => ({ - ...acc, - [next.key]: next.hidden, - }), - {} - ); - - // Tabs that are hidden will be rendered with display: none so won't intersect, - // but we don't want to show them in the overflow menu - return overflowTabs.filter(tabKey => !tabItemKeyToHiddenMap[tabKey]); -} - -export interface TabListProps - extends AriaTabListOptions, - TabListStateOptions { - className?: string; - hideBorder?: boolean; - outerWrapStyles?: React.CSSProperties; +interface BaseDraggableTabListProps extends DraggableTabListProps { + items: DraggableTabListItemProps[]; + setTabs: (tabs: Tab[]) => void; + tabs: Tab[]; } -interface BaseTabListProps extends TabListProps { - items: TabListItemProps[]; -} - -function BaseTabList({ +function BaseDraggableTabList({ hideBorder = false, className, outerWrapStyles, + tabs, + setTabs, ...props -}: BaseTabListProps) { +}: BaseDraggableTabListProps) { const tabListRef = useRef(null); const {rootProps, setTabListState} = useContext(TabsContext); const { @@ -136,6 +68,7 @@ function BaseTabList({ }; const state = useTabListState(ariaProps); + const {tabListProps} = useTabList({orientation, ...ariaProps}, state, tabListRef); useEffect(() => { setTabListState(state); @@ -175,47 +108,53 @@ function BaseTabList({ return ( - { + setTabs(newOrder); + }} + as="div" > - {[...state.collection].map(item => ( - (tabItemsRef.current[item.key] = element)} - /> - ))} - + + {[...state.collection].map(item => ( + tab.key === item.key)} + style={{display: 'flex', flexDirection: 'row'}} + > + (tabItemsRef.current[item.key] = element)} + isChanged + /> + {state.selectedKey !== item.key && + state.collection.getKeyAfter(item.key) !== state.selectedKey && ( + + )} + + ))} + + {orientation === 'horizontal' && overflowMenuItems.length > 0 && ( - - state.setSelectedKey(opt.value)} - disabled={disabled} - position="bottom-end" - size="sm" - offset={4} - trigger={triggerProps => ( - } - aria-label={t('More tabs')} - /> - )} - /> - + )} ); @@ -223,14 +162,26 @@ function BaseTabList({ const collectionFactory = (nodes: Iterable>) => new ListCollection(nodes); +export interface DraggableTabListProps + extends AriaTabListOptions, + TabListStateOptions { + setTabs: (tabs: Tab[]) => void; + tabs: Tab[]; + className?: string; + hideBorder?: boolean; + outerWrapStyles?: React.CSSProperties; +} + /** * To be used as a direct child of the component. See example usage * in tabs.stories.js */ -export function TabList({items, ...props}: TabListProps) { - /** - * Initial, unfiltered list of tab items. - */ +export function DraggableTabList({ + items, + tabs, + setTabs, + ...props +}: DraggableTabListProps) { const collection = useCollection({items, ...props}, collectionFactory); const parsedItems = useMemo( @@ -248,19 +199,35 @@ export function TabList({items, ...props}: TabListProps) { ); return ( - + {item => } - + ); } -TabList.Item = Item; +DraggableTabList.Item = Item; + +const TabDivider = styled('div')` + height: 50%; + width: 1px; + border-radius: 6px; + background-color: ${p => p.theme.gray200}; + margin: 9px auto; +`; const TabListOuterWrap = styled('div')` position: relative; `; -const TabListWrap = styled('ul', {shouldForwardProp: tabsShouldForwardProp})<{ +const TabListWrap = styled('ul', { + shouldForwardProp: tabsShouldForwardProp, +})<{ hideBorder: boolean; orientation: Orientation; }>` @@ -270,14 +237,16 @@ const TabListWrap = styled('ul', {shouldForwardProp: tabsShouldForwardProp})<{ margin: 0; list-style-type: none; flex-shrink: 0; + padding-left: 15px; ${p => p.orientation === 'horizontal' ? ` grid-auto-flow: column; justify-content: start; - gap: ${space(2)}; + gap: ${space(0.5)}; ${!p.hideBorder && `border-bottom: solid 1px ${p.theme.border};`} + stroke-dasharray: 4, 3; ` : ` height: 100%; @@ -288,13 +257,3 @@ const TabListWrap = styled('ul', {shouldForwardProp: tabsShouldForwardProp})<{ ${!p.hideBorder && `border-right: solid 1px ${p.theme.border};`} `}; `; - -const TabListOverflowWrap = styled('div')` - position: absolute; - right: 0; - bottom: ${space(0.75)}; -`; -const OverflowMenuTrigger = styled(DropdownButton)` - padding-left: ${space(1)}; - padding-right: ${space(1)}; -`; diff --git a/static/app/components/draggableTabs/draggableTabPanels.tsx b/static/app/components/draggableTabs/draggableTabPanels.tsx deleted file mode 100644 index 38741ed667a49b..00000000000000 --- a/static/app/components/draggableTabs/draggableTabPanels.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import {useContext, useRef} from 'react'; -import styled from '@emotion/styled'; -import type {AriaTabPanelProps} from '@react-aria/tabs'; -import {useTabPanel} from '@react-aria/tabs'; -import {useCollection} from '@react-stately/collections'; -import {ListCollection} from '@react-stately/list'; -import type {TabListState} from '@react-stately/tabs'; -import type {CollectionBase, Node, Orientation} from '@react-types/shared'; - -import {TabsContext} from './index'; -import {Item} from './item'; -import {tabsShouldForwardProp} from './utils'; - -const collectionFactory = (nodes: Iterable>) => new ListCollection(nodes); - -interface TabPanelsProps extends AriaTabPanelProps, CollectionBase { - className?: string; -} - -/** - * To be used as a direct child of the component. See example usage - * in tabs.stories.js - */ -export function TabPanels(props: TabPanelsProps) { - const { - rootProps: {orientation, items}, - tabListState, - } = useContext(TabsContext); - - // Parse child tab panels from props and identify the selected panel - const collection = useCollection({items, ...props}, collectionFactory, { - suppressTextValueWarning: true, - }); - const selectedPanel = tabListState - ? collection.getItem(tabListState.selectedKey) - : null; - - if (!tabListState) { - return null; - } - - return ( - - {selectedPanel?.props.children} - - ); -} - -TabPanels.Item = Item; - -interface TabPanelProps extends AriaTabPanelProps { - state: TabListState; - children?: React.ReactNode; - className?: string; - orientation?: Orientation; -} - -function TabPanel({ - state, - orientation = 'horizontal', - className, - children, - ...props -}: TabPanelProps) { - const ref = useRef(null); - const {tabPanelProps} = useTabPanel(props, state, ref); - - return ( - - {children} - - ); -} - -const TabPanelWrap = styled('div', {shouldForwardProp: tabsShouldForwardProp})<{ - orientation: Orientation; -}>` - border-radius: ${p => p.theme.borderRadius}; - - ${p => (p.orientation === 'horizontal' ? `height: 100%;` : `width: 100%;`)}; - - &:focus-visible { - outline: none; - box-shadow: - inset ${p => p.theme.focusBorder} 0 0 0 1px, - ${p => p.theme.focusBorder} 0 0 0 1px; - z-index: 1; - } -`; diff --git a/static/app/components/draggableTabs/index.stories.tsx b/static/app/components/draggableTabs/index.stories.tsx new file mode 100644 index 00000000000000..bc20c4f36e74b0 --- /dev/null +++ b/static/app/components/draggableTabs/index.stories.tsx @@ -0,0 +1,64 @@ +import {Fragment} from 'react'; +import styled from '@emotion/styled'; + +import {DraggableTabBar} from 'sentry/components/draggableTabs'; +import JSXNode from 'sentry/components/stories/jsxNode'; +import SizingWindow from 'sentry/components/stories/sizingWindow'; +import storyBook from 'sentry/stories/storyBook'; + +const TabPanelContainer = styled('div')` + width: 90%; + height: 250px; + background-color: white; +`; + +export default storyBook(DraggableTabBar, story => { + const TABS = [ + { + key: 'one', + label: 'Inbox', + content: This is the Inbox view, + }, + { + key: 'two', + label: 'For Review', + content: This is the For Review view, + }, + { + key: 'three', + label: 'Regressed', + content: This is the Regressed view, + }, + ]; + + story('Default', () => ( + +

+ You should be using all of , ,{' '} + , and + components. +

+

+ This will give you all kinds of accessibility and state tracking out of the box. + But you will have to render all tab content, including hooks, upfront. +

+ + + This is a temporary tab + } + /> + + +
+ )); +}); + +const TabBarContainer = styled('div')` + display: flex; + justify-content: start; + width: 90%; + height: 300px; +`; diff --git a/static/app/components/draggableTabs/index.tsx b/static/app/components/draggableTabs/index.tsx index 7ded52b54ed766..d2b405c8ae0598 100644 --- a/static/app/components/draggableTabs/index.tsx +++ b/static/app/components/draggableTabs/index.tsx @@ -1,91 +1,50 @@ import 'intersection-observer'; // polyfill -import {createContext, useState} from 'react'; -import styled from '@emotion/styled'; -import type {AriaTabListOptions} from '@react-aria/tabs'; -import type {TabListState, TabListStateOptions} from '@react-stately/tabs'; -import type {Orientation} from '@react-types/shared'; +import {useEffect, useState} from 'react'; +import type {Key} from '@react-types/shared'; -import {tabsShouldForwardProp} from './utils'; +import {DraggableTabList} from 'sentry/components/draggableTabs/draggableTabList'; +import {TabPanels, Tabs} from 'sentry/components/tabs'; -export interface TabsProps - extends Omit< - AriaTabListOptions, - 'selectedKey' | 'defaultSelectedKey' | 'onSelectionChange' | 'isDisabled' - >, - Omit< - TabListStateOptions, - | 'children' - | 'selectedKey' - | 'defaultSelectedKey' - | 'onSelectionChange' - | 'isDisabled' - > { - children?: React.ReactNode; - className?: string; - /** - * [Uncontrolled] Default selected tab. Must match the `key` prop on the - * selected tab item. - */ - defaultValue?: T; - disabled?: boolean; - /** - * Callback when the selected tab changes. - */ - onChange?: (key: T) => void; - /** - * [Controlled] Selected tab . Must match the `key` prop on the selected tab - * item. - */ - value?: T; +export interface Tab { + content: React.ReactNode; + key: Key; + label: string; + queryCount?: number; } -interface TabContext { - rootProps: Omit, 'children' | 'className'>; - setTabListState: (state: TabListState) => void; - tabListState?: TabListState; +export interface DragAndDropTabBarProps { + tabs: Tab[]; + tempTabContent: React.ReactNode; } -export const TabsContext = createContext({ - rootProps: {orientation: 'horizontal'}, - setTabListState: () => {}, -}); +export function DraggableTabBar(props: DragAndDropTabBarProps) { + const [tabs, setTabs] = useState([ + ...props.tabs, + {key: 'temporary-tab', label: 'Unsaved', content: props.tempTabContent}, + ]); -/** - * Root tabs component. Provides the necessary data (via React context) for - * child components (TabList and TabPanels) to work together. See example - * usage in tabs.stories.js - */ -export function Tabs({ - orientation = 'horizontal', - className, - children, - ...props -}: TabsProps) { - const [tabListState, setTabListState] = useState>(); + useEffect(() => { + setTabs([ + ...props.tabs, + {key: 'temporary-tab', label: 'Unsaved', content: props.tempTabContent}, + ]); + }, [props.tabs, props.tempTabContent]); return ( - - - {children} - - + + + {tabs.map(tab => ( + + {tab.label} + + ))} + + + {tabs.map(tab => ( + {tab.content} + ))} + + ); } - -const TabsWrap = styled('div', {shouldForwardProp: tabsShouldForwardProp})<{ - orientation: Orientation; -}>` - display: flex; - flex-direction: ${p => (p.orientation === 'horizontal' ? 'column' : 'row')}; - flex-grow: 1; - - ${p => - p.orientation === 'vertical' && - ` - height: 100%; - align-items: stretch; - `}; -`; diff --git a/static/app/components/draggableTabs/item.tsx b/static/app/components/draggableTabs/item.tsx index a8aac0aa157cd9..00cd6e94e91c38 100644 --- a/static/app/components/draggableTabs/item.tsx +++ b/static/app/components/draggableTabs/item.tsx @@ -2,11 +2,11 @@ import {Item as _Item} from '@react-stately/collections'; import type {ItemProps} from '@react-types/shared'; import type {LocationDescriptor} from 'history'; -export interface TabListItemProps extends ItemProps { +export interface DraggableTabListItemProps extends ItemProps { key: string | number; disabled?: boolean; hidden?: boolean; to?: LocationDescriptor; } -export const Item = _Item as (props: TabListItemProps) => JSX.Element; +export const Item = _Item as (props: DraggableTabListItemProps) => JSX.Element; diff --git a/static/app/components/tabs/tabList.tsx b/static/app/components/tabs/tabList.tsx index a22455c7a99fe5..ea5432ccc42306 100644 --- a/static/app/components/tabs/tabList.tsx +++ b/static/app/components/tabs/tabList.tsx @@ -26,7 +26,7 @@ import {tabsShouldForwardProp} from './utils'; * Uses IntersectionObserver API to detect overflowing tabs. Returns an array * containing of keys of overflowing tabs. */ -function useOverflowTabs({ +export function useOverflowTabs({ tabListRef, tabItemsRef, tabItems, From dbd3932551b132cfbd72448a34cc06129ff2f9f4 Mon Sep 17 00:00:00 2001 From: MichaelSun48 Date: Thu, 25 Jul 2024 14:59:33 -0700 Subject: [PATCH 3/7] Clean up some artifacts of previous changes --- static/app/components/draggableTabs/draggableTab.tsx | 7 ------- static/app/components/draggableTabs/draggableTabList.tsx | 3 +-- static/app/components/draggableTabs/index.tsx | 4 +--- static/app/components/draggableTabs/utils.tsx | 4 ---- 4 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 static/app/components/draggableTabs/utils.tsx diff --git a/static/app/components/draggableTabs/draggableTab.tsx b/static/app/components/draggableTabs/draggableTab.tsx index e1d863efad6989..338b28faa843aa 100644 --- a/static/app/components/draggableTabs/draggableTab.tsx +++ b/static/app/components/draggableTabs/draggableTab.tsx @@ -10,8 +10,6 @@ import type {Node, Orientation} from '@react-types/shared'; import {BaseTab} from 'sentry/components/tabs/tab'; interface DraggableTabProps extends AriaTabProps { - // dropState: DroppableCollectionState; - isChanged: boolean; item: Node; orientation: Orientation; /** @@ -23,11 +21,6 @@ interface DraggableTabProps extends AriaTabProps { state: TabListState; } -/** - * Renders a single tab item. This should not be imported directly into any - * page/view – it's only meant to be used by . See the correct - * usage in tabs.stories.js - */ export const DraggableTab = forwardRef( ( {item, state, orientation, overflowing}: DraggableTabProps, diff --git a/static/app/components/draggableTabs/draggableTabList.tsx b/static/app/components/draggableTabs/draggableTabList.tsx index aa446c83c95705..c403591e6736e6 100644 --- a/static/app/components/draggableTabs/draggableTabList.tsx +++ b/static/app/components/draggableTabs/draggableTabList.tsx @@ -13,13 +13,13 @@ import type {SelectOption} from 'sentry/components/compactSelect'; import type {Tab} from 'sentry/components/draggableTabs'; import {TabsContext} from 'sentry/components/tabs'; import {OverflowMenu, useOverflowTabs} from 'sentry/components/tabs/tabList'; +import {tabsShouldForwardProp} from 'sentry/components/tabs/utils'; import {space} from 'sentry/styles/space'; import {browserHistory} from 'sentry/utils/browserHistory'; import {DraggableTab} from './draggableTab'; import type {DraggableTabListItemProps} from './item'; import {Item} from './item'; -import {tabsShouldForwardProp} from './utils'; interface BaseDraggableTabListProps extends DraggableTabListProps { items: DraggableTabListItemProps[]; @@ -138,7 +138,6 @@ function BaseDraggableTabList({ orientation === 'horizontal' && overflowTabs.includes(item.key) } ref={element => (tabItemsRef.current[item.key] = element)} - isChanged /> {state.selectedKey !== item.key && state.collection.getKeyAfter(item.key) !== state.selectedKey && ( diff --git a/static/app/components/draggableTabs/index.tsx b/static/app/components/draggableTabs/index.tsx index d2b405c8ae0598..1344bb798f09e8 100644 --- a/static/app/components/draggableTabs/index.tsx +++ b/static/app/components/draggableTabs/index.tsx @@ -35,9 +35,7 @@ export function DraggableTabBar(props: DragAndDropTabBarProps) { {tabs.map(tab => ( - - {tab.label} - + {tab.label} ))} diff --git a/static/app/components/draggableTabs/utils.tsx b/static/app/components/draggableTabs/utils.tsx deleted file mode 100644 index f8c644fe4b5ea6..00000000000000 --- a/static/app/components/draggableTabs/utils.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import isPropValid from '@emotion/is-prop-valid'; - -export const tabsShouldForwardProp = (prop: string) => - typeof prop === 'string' && isPropValid(prop) && prop !== 'orientation'; From 723f34e90b1514727841f23ec774125c0ad8499f Mon Sep 17 00:00:00 2001 From: MichaelSun48 Date: Thu, 25 Jul 2024 15:36:23 -0700 Subject: [PATCH 4/7] remove reference to temporary tabs --- static/app/components/draggableTabs/index.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/static/app/components/draggableTabs/index.tsx b/static/app/components/draggableTabs/index.tsx index 1344bb798f09e8..7bb80a40b23ec2 100644 --- a/static/app/components/draggableTabs/index.tsx +++ b/static/app/components/draggableTabs/index.tsx @@ -1,6 +1,6 @@ import 'intersection-observer'; // polyfill -import {useEffect, useState} from 'react'; +import {useState} from 'react'; import type {Key} from '@react-types/shared'; import {DraggableTabList} from 'sentry/components/draggableTabs/draggableTabList'; @@ -19,17 +19,7 @@ export interface DragAndDropTabBarProps { } export function DraggableTabBar(props: DragAndDropTabBarProps) { - const [tabs, setTabs] = useState([ - ...props.tabs, - {key: 'temporary-tab', label: 'Unsaved', content: props.tempTabContent}, - ]); - - useEffect(() => { - setTabs([ - ...props.tabs, - {key: 'temporary-tab', label: 'Unsaved', content: props.tempTabContent}, - ]); - }, [props.tabs, props.tempTabContent]); + const [tabs, setTabs] = useState([...props.tabs]); return ( From 811229c688c324cf8ca68ade7bc3af602bf7af80 Mon Sep 17 00:00:00 2001 From: MichaelSun48 Date: Thu, 25 Jul 2024 16:12:17 -0700 Subject: [PATCH 5/7] Change tab padding to better match designs --- static/app/components/tabs/tab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/tabs/tab.tsx b/static/app/components/tabs/tab.tsx index c489c26488b721..8ab42271d090b4 100644 --- a/static/app/components/tabs/tab.tsx +++ b/static/app/components/tabs/tab.tsx @@ -185,7 +185,7 @@ const FilledTabWrap = styled('li', {shouldForwardProp: tabsShouldForwardProp})<{ border-top: 1px solid transparent; } - padding: ${space(0.5)} ${space(1)}; + padding: ${space(0.75)} ${space(1.5)}; transform: translateY(1px); From 39805e6dc13106cc4c79def118bc71387aff401a Mon Sep 17 00:00:00 2001 From: MichaelSun48 Date: Thu, 25 Jul 2024 17:58:19 -0700 Subject: [PATCH 6/7] minor code clean up --- static/app/components/draggableTabs/draggableTabList.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/static/app/components/draggableTabs/draggableTabList.tsx b/static/app/components/draggableTabs/draggableTabList.tsx index c403591e6736e6..cf1853864bfdf5 100644 --- a/static/app/components/draggableTabs/draggableTabList.tsx +++ b/static/app/components/draggableTabs/draggableTabList.tsx @@ -108,14 +108,7 @@ function BaseDraggableTabList({ return ( - { - setTabs(newOrder); - }} - as="div" - > + Date: Fri, 26 Jul 2024 14:53:55 -0700 Subject: [PATCH 7/7] Move draggableTabBar to issueList --- static/app/components/draggableTabs/draggableTab.tsx | 2 +- static/app/components/draggableTabs/draggableTabList.tsx | 2 +- static/app/components/draggableTabs/index.stories.tsx | 2 +- .../index.tsx => views/issueList/draggableTabBar.tsx} | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename static/app/{components/draggableTabs/index.tsx => views/issueList/draggableTabBar.tsx} (89%) diff --git a/static/app/components/draggableTabs/draggableTab.tsx b/static/app/components/draggableTabs/draggableTab.tsx index 338b28faa843aa..dfce9e947cf5b5 100644 --- a/static/app/components/draggableTabs/draggableTab.tsx +++ b/static/app/components/draggableTabs/draggableTab.tsx @@ -44,7 +44,7 @@ export const DraggableTab = forwardRef( orientation={orientation} overflowing={overflowing} ref={ref} - variant={'filled'} + variant="filled" > {rendered} diff --git a/static/app/components/draggableTabs/draggableTabList.tsx b/static/app/components/draggableTabs/draggableTabList.tsx index cf1853864bfdf5..a48ceeeb7f5fc2 100644 --- a/static/app/components/draggableTabs/draggableTabList.tsx +++ b/static/app/components/draggableTabs/draggableTabList.tsx @@ -10,12 +10,12 @@ import type {Node, Orientation} from '@react-types/shared'; import {Reorder} from 'framer-motion'; import type {SelectOption} from 'sentry/components/compactSelect'; -import type {Tab} from 'sentry/components/draggableTabs'; import {TabsContext} from 'sentry/components/tabs'; import {OverflowMenu, useOverflowTabs} from 'sentry/components/tabs/tabList'; import {tabsShouldForwardProp} from 'sentry/components/tabs/utils'; import {space} from 'sentry/styles/space'; import {browserHistory} from 'sentry/utils/browserHistory'; +import type {Tab} from 'sentry/views/issueList/draggableTabBar'; import {DraggableTab} from './draggableTab'; import type {DraggableTabListItemProps} from './item'; diff --git a/static/app/components/draggableTabs/index.stories.tsx b/static/app/components/draggableTabs/index.stories.tsx index bc20c4f36e74b0..f8bbad7f752bfc 100644 --- a/static/app/components/draggableTabs/index.stories.tsx +++ b/static/app/components/draggableTabs/index.stories.tsx @@ -1,10 +1,10 @@ import {Fragment} from 'react'; import styled from '@emotion/styled'; -import {DraggableTabBar} from 'sentry/components/draggableTabs'; import JSXNode from 'sentry/components/stories/jsxNode'; import SizingWindow from 'sentry/components/stories/sizingWindow'; import storyBook from 'sentry/stories/storyBook'; +import {DraggableTabBar} from 'sentry/views/issueList/draggableTabBar'; const TabPanelContainer = styled('div')` width: 90%; diff --git a/static/app/components/draggableTabs/index.tsx b/static/app/views/issueList/draggableTabBar.tsx similarity index 89% rename from static/app/components/draggableTabs/index.tsx rename to static/app/views/issueList/draggableTabBar.tsx index 7bb80a40b23ec2..67b772a1e6ea56 100644 --- a/static/app/components/draggableTabs/index.tsx +++ b/static/app/views/issueList/draggableTabBar.tsx @@ -13,12 +13,12 @@ export interface Tab { queryCount?: number; } -export interface DragAndDropTabBarProps { +export interface DraggableTabBarProps { tabs: Tab[]; tempTabContent: React.ReactNode; } -export function DraggableTabBar(props: DragAndDropTabBarProps) { +export function DraggableTabBar(props: DraggableTabBarProps) { const [tabs, setTabs] = useState([...props.tabs]); return (