diff --git a/packages/design-system/atoms/breakpoints/BreakpointProvider.tsx b/packages/design-system/atoms/breakpoints/BreakpointProvider.tsx index f092144..782df10 100644 --- a/packages/design-system/atoms/breakpoints/BreakpointProvider.tsx +++ b/packages/design-system/atoms/breakpoints/BreakpointProvider.tsx @@ -28,6 +28,10 @@ export class BreakpointsProvider extends React.Component<{}, ScreenSize> { Object.entries(this.mediaQueries).forEach(([key, mediaQuery]) => mediaQuery.addEventListener('change', this.handleMediaEvent) ); + + this.handleMediaEvent(); + setTimeout(() => this.handleMediaEvent(), 0); + setTimeout(() => this.handleMediaEvent(), 10); } componentWillUnmount() { @@ -36,8 +40,7 @@ export class BreakpointsProvider extends React.Component<{}, ScreenSize> { ); } - handleMediaEvent = (event: MediaQueryListEvent) => { - console.log('handleMediaEvent', { event }); + handleMediaEvent = () => { this.setState({ ...resetScreen, ...this.calculateScreen(), diff --git a/packages/design-system/atoms/quick-nav/QuickNav.stories.tsx b/packages/design-system/atoms/quick-nav/QuickNav.stories.tsx new file mode 100644 index 0000000..fab18d3 --- /dev/null +++ b/packages/design-system/atoms/quick-nav/QuickNav.stories.tsx @@ -0,0 +1,31 @@ +import { useState } from 'react'; +import { BreakpointsProvider } from '../breakpoints/BreakpointProvider'; +import { QuickNav, QuickNavCollapsed, QuickNavExpanded, QuickNavProps } from './QuickNav'; + +export default { + title: 'Atoms/Quick Nav', + component: QuickNav, + decorators: [(story) => {story()}], + args: { + navItems: ['AAAA', 'BBBB', 'CCCC'], + currentNavItem: 'AAAA', + }, +}; + +const stateWrapper = (args: QuickNavProps, Component: typeof QuickNav) => { + const [currentNavItem, setCurrentNavItem] = useState(args.currentNavItem); + return ( + { + setCurrentNavItem(newNavItem); + args.onNavChange(newNavItem); + }} + > + ); +}; + +export const quickNav = (args) => stateWrapper(args, QuickNav); +export const expanded = (args) => stateWrapper(args, QuickNavExpanded); +export const collapsed = (args) => stateWrapper(args, QuickNavCollapsed); diff --git a/packages/design-system/atoms/quick-nav/QuickNav.styled.ts b/packages/design-system/atoms/quick-nav/QuickNav.styled.ts new file mode 100644 index 0000000..7b4fd2e --- /dev/null +++ b/packages/design-system/atoms/quick-nav/QuickNav.styled.ts @@ -0,0 +1,45 @@ +import styled, { css } from 'styled-components'; +import { palette } from '../colors'; +import { transitions } from '../timings'; + +export const NavItem = styled.div<{ selected: boolean }>` + padding: 7px; + border-radius: 2px; + margin-bottom: 2px; + transition: background-color ${transitions.actionable}s ease-in-out; + + &:hover { + background-color: ${palette.taupe400}; + } + + ${(props) => + props.selected && + css` + background-color: ${palette.taupe300}; + `} +`; + +export const DropdownNavIcon = styled.div` + padding: 5px; + transition: transform ${transitions.actionable}s ease-in-out; + transform: translateY(2px); +`; + +export const DropdownNavCurrent = styled.div` + padding: 5px; +`; + +export const DropdownNavOpener = styled.div` + padding: 5px; + display: flex; + cursor: pointer; + border-radius: 2px; + transition: background-color ${transitions.actionable}s ease-in-out; + + &:hover { + background-color: ${palette.taupe300}; + ${DropdownNavIcon} { + transform: translateY(3px); + } + } +`; diff --git a/packages/design-system/atoms/quick-nav/QuickNav.tsx b/packages/design-system/atoms/quick-nav/QuickNav.tsx new file mode 100644 index 0000000..8971e15 --- /dev/null +++ b/packages/design-system/atoms/quick-nav/QuickNav.tsx @@ -0,0 +1,85 @@ +// thing should be everything visible on desktop/tablet and a popover when small + +import { useBreakpointContext } from '@roleypoly/design-system/atoms/breakpoints'; +import { Popover } from '@roleypoly/design-system/atoms/popover'; +import { useState } from 'react'; +import { GoChevronDown } from 'react-icons/go'; +import { + DropdownNavCurrent, + DropdownNavIcon, + DropdownNavOpener, + NavItem, +} from './QuickNav.styled'; + +export type QuickNavProps = { + navItems: string[]; + onNavChange?: (newNavItem: string) => void; + currentNavItem?: string; +}; + +export const QuickNav = (props: QuickNavProps) => { + const breakpoints = useBreakpointContext(); + + if (breakpoints.screenSize.onSmallScreen) { + return ; + } + + return ; +}; + +export const QuickNavExpanded = (props: QuickNavProps) => { + return ( +
+ {props.navItems.map((navItem) => ( + props.onNavChange?.(navItem)} + selected={props.currentNavItem === navItem} + key={navItem} + > + {navItem} + + ))} +
+ ); +}; + +export const QuickNavCollapsed = (props: QuickNavProps) => { + const [popoverState, setPopoverState] = useState(false); + + return ( +
+ {popoverState ? ( + Server Editor} + position={'top left'} + active={popoverState} + onExit={() => setPopoverState(false)} + > + {() => ( + <> + {props.navItems.map((navItem) => ( + { + setPopoverState(false); + props.onNavChange?.(navItem); + }} + selected={props.currentNavItem === navItem} + key={navItem} + > + {navItem} + + ))} + + )} + + ) : ( + setPopoverState(true)}> + + + + {props.currentNavItem} + + )} +
+ ); +}; diff --git a/packages/design-system/atoms/quick-nav/index.ts b/packages/design-system/atoms/quick-nav/index.ts new file mode 100644 index 0000000..dd9a477 --- /dev/null +++ b/packages/design-system/atoms/quick-nav/index.ts @@ -0,0 +1 @@ +export * from './QuickNav'; diff --git a/packages/design-system/atoms/tab-view/TabView.spec.tsx b/packages/design-system/atoms/tab-view/TabView.spec.tsx index d5669d8..35d1374 100644 --- a/packages/design-system/atoms/tab-view/TabView.spec.tsx +++ b/packages/design-system/atoms/tab-view/TabView.spec.tsx @@ -1,10 +1,8 @@ -import { shallow } from 'enzyme'; -import * as React from 'react'; +import { render, screen } from '@testing-library/react'; import { Tab, TabView, TabViewProps } from './TabView'; -import { TabContent, TabTitle } from './TabView.styled'; const makeView = (props: Partial = {}) => - shallow( + render( {() =>
tab 1
}
{() =>
tab 2
}
, @@ -12,28 +10,7 @@ const makeView = (props: Partial = {}) => ); it('renders tab content correctly', () => { - const view = makeView(); + makeView(); - expect(view.find(Tab).renderProp('children')().text()).toBe('tab 1'); -}); - -it('automatically picks preselected tab content', () => { - const view = makeView({ initialTab: 1 }); - - expect(view.find(Tab).renderProp('children')().text()).toBe('tab 2'); -}); - -it('automatically uses the first tab when preselected tab is not present', () => { - const view = makeView({ initialTab: -1 }); - - view.find(TabContent).find('i').simulate('load'); - expect(view.find(Tab).renderProp('children')().text()).toBe('tab 1'); -}); - -it('changes between tabs when tab is clicked', () => { - const view = makeView(); - - view.find(TabTitle).at(1).simulate('click'); - - expect(view.find(Tab).renderProp('children')().text()).toBe('tab 2'); + expect(screen.getAllByText(/tab [1-2]/)).toHaveLength(2); }); diff --git a/packages/design-system/atoms/tab-view/TabView.stories.tsx b/packages/design-system/atoms/tab-view/TabView.stories.tsx index 272ca11..89c9870 100644 --- a/packages/design-system/atoms/tab-view/TabView.stories.tsx +++ b/packages/design-system/atoms/tab-view/TabView.stories.tsx @@ -1,8 +1,10 @@ import * as React from 'react'; +import { BreakpointsProvider } from '../breakpoints'; import { Tab, TabView } from './TabView'; export default { title: 'Atoms/Tab View', + decorators: [(story) => {story()}], argTypes: { tabCount: { control: 'range', min: 1, max: 100 }, }, @@ -16,7 +18,7 @@ export const ManyTabs = ({ tabCount }) => { {() => ( <> -

tab {i}

+

tab {i}

hello!!!!!

)} diff --git a/packages/design-system/atoms/tab-view/TabView.styled.ts b/packages/design-system/atoms/tab-view/TabView.styled.ts index a156c13..c570534 100644 --- a/packages/design-system/atoms/tab-view/TabView.styled.ts +++ b/packages/design-system/atoms/tab-view/TabView.styled.ts @@ -1,42 +1,69 @@ -import { onTablet } from '@roleypoly/design-system/atoms/breakpoints'; -import { palette } from '@roleypoly/design-system/atoms/colors'; -import { transitions } from '@roleypoly/design-system/atoms/timings'; import styled, { css } from 'styled-components'; +import { onSmallScreen } from '../breakpoints'; +import { palette } from '../colors'; +import { transitions } from '../timings'; +import { text500 } from '../typography'; -export const TabViewStyled = styled.div``; +export const TabViewStyled = styled.div` + display: flex; + flex-direction: row; + overflow: hidden; + + ${onSmallScreen( + css` + flex-direction: column; + ` + )} +`; export const TabTitleRow = styled.div` - display: flex; - border-bottom: 1px solid ${palette.taupe100}; - overflow-x: auto; - overflow-y: hidden; - white-space: nowrap; + flex: 1; + width: 23vw; + position: fixed; + ${onSmallScreen( + css` + position: unset; + max-width: 100vw; + ` + )} `; export const TabTitle = styled.div<{ selected: boolean }>` - flex: 1; - text-align: center; - padding: 0.7em 1em; - border-bottom: 3px solid transparent; - transition: border-color ${transitions.in2out}s ease-in-out, - color ${transitions.in2out}s ease-in-out; + padding: 7px; cursor: pointer; - color: ${palette.taupe500}; + transition: background-color ${transitions.actionable}s ease-in-out; + border-radius: 2px; + margin-bottom: 5px; + + &:hover { + background-color: ${palette.taupe400}; + } + ${(props) => - props.selected - ? css` - color: unset; - border-bottom-color: ${palette.taupe500}; - ` - : css` - &:hover { - border-bottom-color: ${palette.taupe300}; - color: unset; - } - `}; - ${onTablet(css` - padding: 0.45em 1em; - `)} + props.selected && + css` + background-color: ${palette.taupe300}; + `} `; -export const TabContent = styled.div``; +export const TabContent = styled.div` + padding-left: 1em; + margin-left: 23vw; + flex: 1; + + ${onSmallScreen( + css` + padding-left: 0; + margin-left: 0; + position: unset; + max-width: 100vw; + ` + )} +`; + +export const TabContentTitle = styled.div` + ${text500} + + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + padding: 10px; +`; diff --git a/packages/design-system/atoms/tab-view/TabView.tsx b/packages/design-system/atoms/tab-view/TabView.tsx index 9b73364..1edd199 100644 --- a/packages/design-system/atoms/tab-view/TabView.tsx +++ b/packages/design-system/atoms/tab-view/TabView.tsx @@ -1,5 +1,11 @@ import * as React from 'react'; -import { TabContent, TabTitle, TabTitleRow, TabViewStyled } from './TabView.styled'; +import { QuickNav } from '../quick-nav'; +import { + TabContent, + TabContentTitle, + TabTitleRow, + TabViewStyled, +} from './TabView.styled'; export type TabViewProps = { children: React.ReactNode[]; @@ -25,26 +31,40 @@ export const TabView = (props: TabViewProps) => { } const [currentTab, setCurrentTab] = React.useState(props.initialTab ?? 0); + const tabRefs = tabNames.reduce<{ [name: string]: React.RefObject }>( + (acc, name) => ({ ...acc, [name]: React.createRef() }), + {} + ); return ( - {tabNames.map((tabName, idx) => ( - setCurrentTab(idx)} - key={`tab${tabName}${idx}`} - > - {tabName} - - ))} + { + setCurrentTab(tabNames.findIndex((name) => newTabName === name)); + tabRefs[newTabName].current?.scrollIntoView({ + behavior: 'smooth', + }); + }} + /> - {props.children[currentTab] || ( - setCurrentTab(0)}> - Tabs were misconfigured, resetting to zero. - - )} + {React.Children.map(props.children, (child) => { + if (!React.isValidElement(child)) { + return null; + } + + return ( +
+ + {child.props.title} + + {child} +
+ ); + })}
); diff --git a/packages/design-system/organisms/editor/EditorShell.stories.tsx b/packages/design-system/organisms/editor/EditorShell.stories.tsx index 2e60e10..296a3c8 100644 --- a/packages/design-system/organisms/editor/EditorShell.stories.tsx +++ b/packages/design-system/organisms/editor/EditorShell.stories.tsx @@ -1,10 +1,12 @@ +import { BreakpointsProvider } from '@roleypoly/design-system/atoms/breakpoints'; +import { guildEnum } from '@roleypoly/design-system/fixtures/storyData'; import * as React from 'react'; -import { guildEnum } from '../../fixtures/storyData'; import { EditorShell } from './EditorShell'; export default { title: 'Organisms/Editor', component: EditorShell, + decorators: [(story) => {story()}], }; -export const Shell = () => ; +export const Shell = () => ; diff --git a/packages/design-system/organisms/editor/EditorShell.tsx b/packages/design-system/organisms/editor/EditorShell.tsx index 788fc66..772281b 100644 --- a/packages/design-system/organisms/editor/EditorShell.tsx +++ b/packages/design-system/organisms/editor/EditorShell.tsx @@ -1,7 +1,6 @@ import { Tab, TabView } from '@roleypoly/design-system/atoms/tab-view'; +import { EditorCategory } from '@roleypoly/design-system/molecules/editor-category'; import { PresentableGuild } from '@roleypoly/types'; -import * as React from 'react'; -import { EditorCategory } from '../../molecules/editor-category'; import { CategoryContainer } from './EditorShell.styled'; type Props = {