@@ -17,7 +17,10 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { useTheme } from '@emotion/react'; | |||
import styled from '@emotion/styled'; | |||
import { throttle } from 'lodash'; | |||
import React from 'react'; | |||
import tw from 'twin.macro'; | |||
import { | |||
LAYOUT_GLOBAL_NAV_HEIGHT, | |||
@@ -25,11 +28,12 @@ import { | |||
LAYOUT_LOGO_MAX_HEIGHT, | |||
LAYOUT_LOGO_MAX_WIDTH, | |||
LAYOUT_VIEWPORT_MIN_WIDTH, | |||
THROTTLE_SCROLL_DELAY, | |||
} from '../helpers/constants'; | |||
import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; | |||
import { themeBorder, themeColor, themeContrast, themeShadow } from '../helpers/theme'; | |||
import { BaseLink } from './Link'; | |||
const MainAppBarDiv = styled.div` | |||
const MainAppBarHeader = styled.header` | |||
${tw`sw-flex`}; | |||
${tw`sw-items-center`}; | |||
${tw`sw-px-6`}; | |||
@@ -68,14 +72,28 @@ export function MainAppBar({ | |||
children, | |||
Logo, | |||
}: React.PropsWithChildren<{ Logo: React.ElementType }>) { | |||
const theme = useTheme(); | |||
const [boxShadow, setBoxShadow] = React.useState('none'); | |||
React.useEffect(() => { | |||
const handleScroll = throttle(() => { | |||
setBoxShadow(document.documentElement?.scrollTop > 0 ? themeShadow('md')({ theme }) : 'none'); | |||
}, THROTTLE_SCROLL_DELAY); | |||
document.addEventListener('scroll', handleScroll); | |||
return () => { | |||
document.removeEventListener('scroll', handleScroll); | |||
}; | |||
}, [theme]); | |||
return ( | |||
<MainAppBarDiv> | |||
<MainAppBarHeader style={{ boxShadow }}> | |||
<MainAppBarNavLogoDiv> | |||
<MainAppBarNavLogoLink to="/"> | |||
<Logo /> | |||
</MainAppBarNavLogoLink> | |||
</MainAppBarNavLogoDiv> | |||
<MainAppBarNavRightDiv>{children}</MainAppBarNavRightDiv> | |||
</MainAppBarDiv> | |||
</MainAppBarHeader> | |||
); | |||
} |
@@ -19,7 +19,7 @@ | |||
*/ | |||
/* eslint-disable import/no-extraneous-dependencies */ | |||
import { screen } from '@testing-library/react'; | |||
import { fireEvent, screen } from '@testing-library/react'; | |||
import { MemoryRouter, Route, Routes } from 'react-router-dom'; | |||
import { LAYOUT_LOGO_MAX_HEIGHT, LAYOUT_LOGO_MAX_WIDTH } from '../../helpers/constants'; | |||
import { render } from '../../helpers/testUtils'; | |||
@@ -45,6 +45,21 @@ it('should render the logo', () => { | |||
expect(element.container.querySelector('svg')).toHaveStyle({ height: '40px', width: '132px' }); | |||
}); | |||
it('should add shadow when scrolled', () => { | |||
setupWithProps(); | |||
expect(screen.getByRole('banner')).toHaveStyle({ | |||
'box-shadow': 'none', | |||
}); | |||
document.documentElement.scrollTop = 100; | |||
fireEvent.scroll(document, { target: { scrollTop: 100 } }); | |||
expect(screen.getByRole('banner')).toHaveStyle({ | |||
'box-shadow': '0px 4px 8px -2px rgba(29,33,47,0.1),0px 2px 15px -2px rgba(29,33,47,0.06)', | |||
}); | |||
}); | |||
function setupWithProps( | |||
props: FCProps<typeof MainAppBar> = { | |||
Logo: () => <img alt="logo" src="http://example.com/logo.png" />, |
@@ -264,7 +264,7 @@ export class AllProjects extends React.PureComponent<Props, State> { | |||
); | |||
renderHeader = () => ( | |||
<div className="sw-w-full" style={{ height: '120px' }}> | |||
<PageHeaderWrapper className="sw-w-full"> | |||
<PageHeader | |||
currentUser={this.props.currentUser} | |||
onPerspectiveChange={this.handlePerspectiveChange} | |||
@@ -275,7 +275,7 @@ export class AllProjects extends React.PureComponent<Props, State> { | |||
total={this.state.total} | |||
view={this.getView()} | |||
/> | |||
</div> | |||
</PageHeaderWrapper> | |||
); | |||
renderMain = () => { | |||
@@ -382,3 +382,8 @@ const SideBarStyle = styled.div` | |||
border-right: ${themeBorder('default', 'filterbarBorder')}; | |||
background-color: ${themeColor('backgroundSecondary')}; | |||
`; | |||
const PageHeaderWrapper = styled.div` | |||
height: 7.5rem; | |||
border-bottom: ${themeBorder('default', 'filterbarBorder')}; | |||
`; |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
import { Spinner } from 'design-system'; | |||
import * as React from 'react'; | |||
import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer'; | |||
@@ -80,7 +81,12 @@ export default class ProjectsList extends React.PureComponent<Props> { | |||
const project = projects[index]; | |||
return ( | |||
<div key={key} role="row" style={{ ...style, height: PROJECT_CARD_HEIGHT }}> | |||
<div | |||
className={classNames({ 'sw-mt-4': index === 0 })} | |||
key={key} | |||
role="row" | |||
style={{ ...style, height: PROJECT_CARD_HEIGHT }} | |||
> | |||
<div className="sw-h-full" role="gridcell"> | |||
<ProjectCard | |||
currentUser={this.props.currentUser} | |||
@@ -105,11 +111,18 @@ export default class ProjectsList extends React.PureComponent<Props> { | |||
height={height} | |||
overscanRowCount={2} | |||
rowCount={this.props.projects.length + 1} | |||
rowHeight={({ index }) => | |||
index === this.props.projects.length | |||
? PROJECT_LIST_FOOTER_HEIGHT | |||
: PROJECT_CARD_HEIGHT + PROJECT_CARD_MARGIN | |||
} | |||
rowHeight={({ index }) => { | |||
if (index === 0) { | |||
// first card, double top and bottom margin | |||
return PROJECT_CARD_HEIGHT + PROJECT_CARD_MARGIN * 2; | |||
} | |||
if (index === this.props.projects.length) { | |||
// Footer card, no margin | |||
return PROJECT_LIST_FOOTER_HEIGHT; | |||
} | |||
// all other cards, only bottom margin | |||
return PROJECT_CARD_HEIGHT + PROJECT_CARD_MARGIN; | |||
}} | |||
rowRenderer={this.renderRow} | |||
style={{ outline: 'none' }} | |||
tabIndex={-1} |