From 0a3591a1575f6d527c701d39adecb8beef70063e Mon Sep 17 00:00:00 2001 From: Patrick Riley Date: Fri, 26 Oct 2018 17:30:33 -0400 Subject: [PATCH] feat(page layout): adds the condensed header feature to the page header (#843) --- packages/patternfly-4/react-core/package.json | 2 +- .../demos/PageLayout/PageLayoutDemo.docs.js | 4 +- .../examples/PageLayoutCondensedHeader.js | 245 ++++++++++++++++++ .../react-core/src/internal/util.js | 8 + .../react-core/src/internal/util.test.js | 19 +- .../react-core/src/layouts/Page/Page.d.ts | 2 + .../react-core/src/layouts/Page/Page.js | 71 ++++- .../react-core/src/layouts/Page/Page.test.js | 14 + .../src/layouts/Page/PageHeader.d.ts | 1 + .../react-core/src/layouts/Page/PageHeader.js | 76 +++--- .../src/layouts/Page/PageHeader.test.js | 11 + .../Page/__snapshots__/Page.test.js.snap | 223 +++++++++++++++- .../__snapshots__/PageHeader.test.js.snap | 55 ++++ .../patternfly-4/react-tokens/package.json | 2 +- packages/react-icons/package.json | 2 +- yarn.lock | 6 +- 16 files changed, 684 insertions(+), 57 deletions(-) create mode 100644 packages/patternfly-4/react-core/src/demos/PageLayout/examples/PageLayoutCondensedHeader.js create mode 100644 packages/patternfly-4/react-core/src/layouts/Page/PageHeader.test.js create mode 100644 packages/patternfly-4/react-core/src/layouts/Page/__snapshots__/PageHeader.test.js.snap diff --git a/packages/patternfly-4/react-core/package.json b/packages/patternfly-4/react-core/package.json index 4228900947a..bc833cd03cf 100644 --- a/packages/patternfly-4/react-core/package.json +++ b/packages/patternfly-4/react-core/package.json @@ -47,7 +47,7 @@ "@patternfly/react-tokens": "^1.0.0" }, "devDependencies": { - "@patternfly/patternfly-next": "1.0.63", + "@patternfly/patternfly-next": "1.0.64", "css": "^2.2.3", "fs-extra": "^6.0.1", "glob": "^7.1.2", diff --git a/packages/patternfly-4/react-core/src/demos/PageLayout/PageLayoutDemo.docs.js b/packages/patternfly-4/react-core/src/demos/PageLayout/PageLayoutDemo.docs.js index 44dad737559..f9f3baf54ef 100644 --- a/packages/patternfly-4/react-core/src/demos/PageLayout/PageLayoutDemo.docs.js +++ b/packages/patternfly-4/react-core/src/demos/PageLayout/PageLayoutDemo.docs.js @@ -3,6 +3,7 @@ import PageLayoutExpandableNav from './examples/PageLayoutExpandableNav'; import PageLayoutGroupsNav from './examples/PageLayoutGroupsNav'; import PageLayoutHorizontalNav from './examples/PageLayoutHorizontalNav'; import PageLayoutSimpleNav from './examples/PageLayoutSimpleNav'; +import PageLayoutCondensedHeader from './examples/PageLayoutCondensedHeader'; export default { title: 'Page Layout Demos', @@ -11,7 +12,8 @@ export default { PageLayoutExpandableNav, PageLayoutGroupsNav, PageLayoutHorizontalNav, - PageLayoutSimpleNav + PageLayoutSimpleNav, + PageLayoutCondensedHeader ], fullPageOnly: true }; diff --git a/packages/patternfly-4/react-core/src/demos/PageLayout/examples/PageLayoutCondensedHeader.js b/packages/patternfly-4/react-core/src/demos/PageLayout/examples/PageLayoutCondensedHeader.js new file mode 100644 index 00000000000..a575c580236 --- /dev/null +++ b/packages/patternfly-4/react-core/src/demos/PageLayout/examples/PageLayoutCondensedHeader.js @@ -0,0 +1,245 @@ +import React from 'react'; +import { + Avatar, + BackgroundImage, + BackgroundImageSrc, + Brand, + Button, + ButtonVariant, + Card, + CardBody, + Dropdown, + DropdownToggle, + DropdownItem, + DropdownSeparator, + Gallery, + GalleryItem, + KebabToggle, + Nav, + NavExpandable, + NavItem, + NavList, + Page, + PageHeader, + PageSection, + PageSectionVariants, + PageSidebar, + TextContent, + Text, + Toolbar, + ToolbarGroup, + ToolbarItem +} from '@patternfly/react-core'; +import { global_breakpoint_md as breakpointMd } from '@patternfly/react-tokens'; +import accessibleStyles from '@patternfly/patternfly-next/utilities/Accessibility/accessibility.css'; +import spacingStyles from '@patternfly/patternfly-next/utilities/Spacing/spacing.css'; +import { css } from '@patternfly/react-styles'; +import { BellIcon, CogIcon } from '@patternfly/react-icons'; +import brandImg from './l_pf-reverse-164x11.png'; +import avatarImg from './img_avatar.png'; + +class PageLayoutCondensedHeader extends React.Component { + static title = 'Using condensed header'; + + constructor(props) { + super(props); + // Set initial isNavOpen state based on window width + const isNavOpen = typeof window !== 'undefined' && window.innerWidth >= parseInt(breakpointMd.value, 10); + this.state = { + isDropdownOpen: false, + isKebabDropdownOpen: false, + isNavOpen, + activeGroup: 'grp-1', + activeItem: 'grp-1_itm-1' + }; + } + + onDropdownToggle = isDropdownOpen => { + this.setState({ + isDropdownOpen + }); + }; + + onDropdownSelect = event => { + this.setState({ + isDropdownOpen: !this.state.isDropdownOpen + }); + }; + + onKebabDropdownToggle = isKebabDropdownOpen => { + this.setState({ + isKebabDropdownOpen + }); + }; + + onKebabDropdownSelect = event => { + this.setState({ + isKebabDropdownOpen: !this.state.isKebabDropdownOpen + }); + }; + + onNavSelect = result => { + this.setState({ + activeItem: result.itemId, + activeGroup: result.groupId + }); + }; + + onNavToggle = () => { + this.setState({ + isNavOpen: !this.state.isNavOpen + }); + }; + + render() { + const { isDropdownOpen, isKebabDropdownOpen, activeItem, isNavOpen, activeGroup } = this.state; + + const PageNav = ( + + ); + const PageToolbar = ( + + + + + + + + + + + + } + isOpen={isKebabDropdownOpen} + > + + Notifications + + + Settings + + + + + Kyle Baker} + > + Link + Action + Disabled Link + + Disabled Action + + + Separated Link + Separated Action + + + + + ); + const bgImages = { + [BackgroundImageSrc.lg]: '/assets/images/pfbg_1200.jpg', + [BackgroundImageSrc.md]: '/assets/images/pfbg_992.jpg', + [BackgroundImageSrc.md2x]: '/assets/images/pfbg_992@2x.jpg', + [BackgroundImageSrc.sm]: '/assets/images/pfbg_768.jpg', + [BackgroundImageSrc.sm2x]: '/assets/images/pfbg_768@2x.jpg', + [BackgroundImageSrc.xl]: '/assets/images/pfbg_2000.jpg', + [BackgroundImageSrc.xs]: '/assets/images/pfbg_576.jpg', + [BackgroundImageSrc.xs2x]: '/assets/images/pfbg_576@2x.jpg', + [BackgroundImageSrc.filter]: '/assets/images/background-filter.svg' + }; + + const Header = ( + } + toolbar={PageToolbar} + avatar={} + showNavToggle + onNavToggle={this.onNavToggle} + /> + ); + const Sidebar = ; + + return ( + + + + + + Main Title + + Body text should be Overpass Regular at 16px. It should have leading of 24px because
+ of it’s relative line height of 1.5. +
+
+
+ + + {Array.apply(0, Array(50)).map((x, i) => ( + + + This is a card + + + ))} + + +
+
+ ); + } +} + +export default PageLayoutCondensedHeader; diff --git a/packages/patternfly-4/react-core/src/internal/util.js b/packages/patternfly-4/react-core/src/internal/util.js index 0fc4db319ba..3c8f8a01c90 100644 --- a/packages/patternfly-4/react-core/src/internal/util.js +++ b/packages/patternfly-4/react-core/src/internal/util.js @@ -10,3 +10,11 @@ export function getUniqueId(prefix = 'pf') { .slice(2); return `${prefix}-${uid}`; } + +export function debounce(func, wait) { + let timeout; + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; +} diff --git a/packages/patternfly-4/react-core/src/internal/util.test.js b/packages/patternfly-4/react-core/src/internal/util.test.js index a4206372443..1c27cb12b25 100644 --- a/packages/patternfly-4/react-core/src/internal/util.test.js +++ b/packages/patternfly-4/react-core/src/internal/util.test.js @@ -1,4 +1,4 @@ -import { capitalize, getUniqueId } from './util'; +import { capitalize, getUniqueId, debounce } from './util'; test('capitalize', () => { expect(capitalize('foo')).toBe('Foo'); @@ -12,3 +12,20 @@ test('getUniqueId prefixed', () => { expect(getUniqueId().substring(0, 3)).toBe('pf-'); expect(getUniqueId('pf-switch').substring(0, 10)).toBe('pf-switch-'); }); + +test('debounce', () => { + jest.useFakeTimers(); + const callback = jest.fn(); + const debouncedFunction = debounce(callback, 50); + + debouncedFunction(); + // At this point in time, the callback should not have been called yet + expect(callback).toHaveBeenCalledTimes(0); + + for (let i = 0; i < 10; i++) { + jest.advanceTimersByTime(50); + debouncedFunction(); + } + + expect(callback).toBeCalledTimes(10); +}); diff --git a/packages/patternfly-4/react-core/src/layouts/Page/Page.d.ts b/packages/patternfly-4/react-core/src/layouts/Page/Page.d.ts index 1fdd372bfe4..0cf54de7de0 100644 --- a/packages/patternfly-4/react-core/src/layouts/Page/Page.d.ts +++ b/packages/patternfly-4/react-core/src/layouts/Page/Page.d.ts @@ -6,6 +6,8 @@ export interface PageProps extends HTMLProps { className?: string; header?: ReactNode; sidebar?: ReactNode; + useCondensed?: boolean; + scrollingDistance?: number; } declare const Page: SFC; diff --git a/packages/patternfly-4/react-core/src/layouts/Page/Page.js b/packages/patternfly-4/react-core/src/layouts/Page/Page.js index df71a6175f6..c36ca69cd3a 100644 --- a/packages/patternfly-4/react-core/src/layouts/Page/Page.js +++ b/packages/patternfly-4/react-core/src/layouts/Page/Page.js @@ -2,6 +2,7 @@ import React from 'react'; import styles from '@patternfly/patternfly-next/layouts/Page/page.css'; import { css } from '@patternfly/react-styles'; import PropTypes from 'prop-types'; +import { debounce } from '../../internal/util'; export const PageLayouts = { vertical: 'vertical', @@ -16,25 +17,73 @@ const propTypes = { /** Header component (e.g. ) */ header: PropTypes.node, /** Sidebar component for a side nav (e.g. ) */ - sidebar: PropTypes.node + sidebar: PropTypes.node, + /** condensed header on scroll */ + useCondensed: PropTypes.bool, + /** condensed height override */ + scrollingDistance: PropTypes.number }; const defaultProps = { children: null, className: '', header: null, - sidebar: null + sidebar: null, + useCondensed: false, + scrollingDistance: 20 }; -const Page = ({ className, children, header, sidebar, ...props }) => ( -
- {header} - {sidebar} -
- {children} -
-
-); +class Page extends React.Component { + state = { isCondensed: false }; + + constructor(props) { + super(props); + this.mainRef = React.createRef(); + } + componentDidMount() { + const { useCondensed } = this.props; + if (useCondensed) { + // I picked this because it's approx 1 frame (ie: 16.7ms) + this.mainRef.current.addEventListener('scroll', debounce(this.handleScroll, 16)); + } + } + componentWillUnmount() { + const { useCondensed } = this.props; + if (useCondensed) { + this.mainRef.current.removeEventListener('scroll', this.handleScroll); + } + } + + handleScroll = e => { + window.requestAnimationFrame(() => { + const { isCondensed } = this.state; + const { scrollingDistance } = this.props; + const main = e.target; + const mainPosition = main.scrollTop; + if (mainPosition > scrollingDistance && !isCondensed) { + this.setState({ isCondensed: true }); + } else if (mainPosition < scrollingDistance && isCondensed) { + this.setState({ isCondensed: false }); + } + }); + }; + + render() { + const { className, children, header, sidebar, useCondensed, scrollingDistance, ...rest } = this.props; + const { isCondensed } = this.state; + + const clonedHeader = React.cloneElement(header, { isCondensed }); + return ( +
+ {useCondensed ? clonedHeader : header} + {sidebar} +
+ {children} +
+
+ ); + } +} Page.propTypes = propTypes; Page.defaultProps = defaultProps; diff --git a/packages/patternfly-4/react-core/src/layouts/Page/Page.test.js b/packages/patternfly-4/react-core/src/layouts/Page/Page.test.js index 6dde3ca7459..4cb4fe96db3 100644 --- a/packages/patternfly-4/react-core/src/layouts/Page/Page.test.js +++ b/packages/patternfly-4/react-core/src/layouts/Page/Page.test.js @@ -25,6 +25,20 @@ test('Check page vertical layout example against snapshot', () => { expect(view).toMatchSnapshot(); }); +test('Check page condensed header example against snapshot', () => { + const Header = undefined} />; + const Sidebar = ; + const view = mount( + + Section with default background + Section with light background + Section with dark background + Section with darker background + + ); + expect(view).toMatchSnapshot(); +}); + test('Check page horizontal layout example against snapshot', () => { const Header = ; const Sidebar = ; diff --git a/packages/patternfly-4/react-core/src/layouts/Page/PageHeader.d.ts b/packages/patternfly-4/react-core/src/layouts/Page/PageHeader.d.ts index 1c940c0b495..e1be4e1ec5f 100644 --- a/packages/patternfly-4/react-core/src/layouts/Page/PageHeader.d.ts +++ b/packages/patternfly-4/react-core/src/layouts/Page/PageHeader.d.ts @@ -9,6 +9,7 @@ export interface PageHeaderProps extends HTMLProps { topNav?: ReactNode; showNavToggle?: boolean; onNavToggle?: Function; + isCondensed?: boolean; } declare const PageHeader: SFC; diff --git a/packages/patternfly-4/react-core/src/layouts/Page/PageHeader.js b/packages/patternfly-4/react-core/src/layouts/Page/PageHeader.js index fa8aa6ac0ad..14c13e26aec 100644 --- a/packages/patternfly-4/react-core/src/layouts/Page/PageHeader.js +++ b/packages/patternfly-4/react-core/src/layouts/Page/PageHeader.js @@ -19,7 +19,9 @@ const propTypes = { /** True to show the nav toggle button (toggles side nav) */ showNavToggle: PropTypes.bool, /** Callback function to handle the side nav toggle button */ - onNavToggle: PropTypes.func + onNavToggle: PropTypes.func, + /** header should be condensed */ + isCondensed: PropTypes.bool }; const defaultProps = { @@ -29,38 +31,52 @@ const defaultProps = { avatar: null, topNav: null, showNavToggle: false, - onNavToggle: () => undefined + onNavToggle: () => undefined, + isCondensed: false }; /* Added temporary style as a workaround to make dropdowns work until fix is made (patternfly-next #780) */ -const PageHeader = ({ className, logo, toolbar, avatar, topNav, showNavToggle, onNavToggle, ...props }) => ( -
-
- {showNavToggle && ( -
- -
- )} - {logo} -
- {/* Hide for now until we have the context selector component */} - {/*
- pf-c-context-selector -
*/} - {topNav &&
{topNav}
} -
- {toolbar} - {avatar} -
-
-); +const PageHeader = ({ + className, + logo, + toolbar, + avatar, + topNav, + showNavToggle, + onNavToggle, + isCondensed, + ...props +}) => { + const customClassName = css(styles.pageHeader, isCondensed && styles.modifiers.condensed, className); + return ( +
+
+ {showNavToggle && ( +
+ +
+ )} + {logo} +
+ {/* Hide for now until we have the context selector component */} + {/*
+ pf-c-context-selector +
*/} + {topNav &&
{topNav}
} +
+ {toolbar} + {avatar} +
+
+ ); +}; PageHeader.propTypes = propTypes; PageHeader.defaultProps = defaultProps; diff --git a/packages/patternfly-4/react-core/src/layouts/Page/PageHeader.test.js b/packages/patternfly-4/react-core/src/layouts/Page/PageHeader.test.js new file mode 100644 index 00000000000..111461293ff --- /dev/null +++ b/packages/patternfly-4/react-core/src/layouts/Page/PageHeader.test.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import PageHeader from './PageHeader'; + +test('Check page vertical layout example against snapshot', () => { + const Header = ( + undefined} isCondensed /> + ); + const view = shallow(Header); + expect(view).toMatchSnapshot(); +}); diff --git a/packages/patternfly-4/react-core/src/layouts/Page/__snapshots__/Page.test.js.snap b/packages/patternfly-4/react-core/src/layouts/Page/__snapshots__/Page.test.js.snap index c2ace191bac..3c6ba89638f 100644 --- a/packages/patternfly-4/react-core/src/layouts/Page/__snapshots__/Page.test.js.snap +++ b/packages/patternfly-4/react-core/src/layouts/Page/__snapshots__/Page.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Check page horizontal layout example against snapshot 1`] = ` +exports[`Check page condensed header example against snapshot 1`] = ` .pf-l-page__header-brand-link { display: flex; align-items: center; @@ -25,7 +25,207 @@ exports[`Check page horizontal layout example against snapshot 1`] = ` align-items: flex-end; max-width: 100%; min-height: 6.875rem; +} +.pf-l-page__sidebar.pf-m-expanded { + display: block; + grid-area: 2 / 1; + z-index: 500; + width: 80%; + padding-top: 1rem; + padding-bottom: 1rem; + overflow-x: hidden; + overflow-y: auto; + background-color: #ffffff; + transform: translate3d(0,0,0); + box-shadow: 0.75rem 0 0.625rem -0.25rem rgba(3, 3, 3, 0.07); +} +.pf-l-page__main-section { + display: block; + padding: 2rem 2rem 2rem 2rem; + background-color: #ededed; +} +.pf-l-page__main-section.pf-m-light { + display: block; + padding: 2rem 2rem 2rem 2rem; + background-color: #ffffff; +} +.pf-l-page__main-section.pf-m-dark-200 { + display: block; + padding: 2rem 2rem 2rem 2rem; + background-color: rgba(3, 3, 3, 0.32); +} +.pf-l-page__main-section.pf-m-dark-100 { + display: block; + padding: 2rem 2rem 2rem 2rem; + background-color: rgba(3, 3, 3, 0.62); +} +.pf-l-page__main { + display: flex; + grid-area: main; + flex-direction: column; + padding-right: 0px; overflow-x: hidden; + overflow-y: auto; +} +.pf-l-page.my-page-class { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: max-content 1fr; + grid-template-areas: "header" "main"; +} + + + } + id="PageId" + scrollingDistance={20} + sidebar={ + + } + useCondensed={true} +> +
+ +
+ +
+ Toolbar + | Avatar +
+
+
+ + + +
+ +
+ Section with default background +
+
+ +
+ Section with light background +
+
+ +
+ Section with dark background +
+
+ +
+ Section with darker background +
+
+
+
+
+`; + +exports[`Check page horizontal layout example against snapshot 1`] = ` +.pf-l-page__header-brand-link { + display: flex; + align-items: center; + min-height: 2.5rem; +} +.pf-l-page__header-brand { + display: block; + padding-top: 1rem; + padding-bottom: 1rem; + padding-left: 2rem; +} +.pf-l-page__header-tools { + display: block; + align-items: center; + margin-right: 2rem; + margin-left: auto; +} +.pf-l-page__header { + display: flex; + grid-area: header; + flex-wrap: wrap; + align-items: flex-end; + max-width: 100%; + min-height: 6.875rem; } .pf-l-page__sidebar.pf-m-expanded { display: block; @@ -34,7 +234,8 @@ exports[`Check page horizontal layout example against snapshot 1`] = ` width: 80%; padding-top: 1rem; padding-bottom: 1rem; - overflow: hidden; + overflow-x: hidden; + overflow-y: auto; background-color: #ffffff; transform: translate3d(0,0,0); box-shadow: 0.75rem 0 0.625rem -0.25rem rgba(3, 3, 3, 0.07); @@ -65,11 +266,10 @@ exports[`Check page horizontal layout example against snapshot 1`] = ` flex-direction: column; padding-right: 0px; overflow-x: hidden; - overflow-y: visible; + overflow-y: auto; } .pf-l-page.my-page-class { display: grid; - min-height: 100vh; grid-template-columns: 1fr; grid-template-rows: max-content 1fr; grid-template-areas: "header" "main"; @@ -82,6 +282,7 @@ exports[`Check page horizontal layout example against snapshot 1`] = ` } + useCondensed={false} >
} id="PageId" + scrollingDistance={20} sidebar={ } + useCondensed={false} >
+ +
+ Toolbar + | Avatar +
+ +`; diff --git a/packages/patternfly-4/react-tokens/package.json b/packages/patternfly-4/react-tokens/package.json index e02d66ade54..b771037ff39 100644 --- a/packages/patternfly-4/react-tokens/package.json +++ b/packages/patternfly-4/react-tokens/package.json @@ -27,7 +27,7 @@ "build": "node build/generateTokens.js" }, "devDependencies": { - "@patternfly/patternfly-next": "1.0.63", + "@patternfly/patternfly-next": "1.0.64", "css": "^2.2.3", "fs-extra": "^6.0.1", "glob": "^7.1.2" diff --git a/packages/react-icons/package.json b/packages/react-icons/package.json index 0b40a419215..5084b31db90 100644 --- a/packages/react-icons/package.json +++ b/packages/react-icons/package.json @@ -33,7 +33,7 @@ }, "devDependencies": { "@fortawesome/free-solid-svg-icons": "^5.3.1", - "@patternfly/patternfly-next": "1.0.63", + "@patternfly/patternfly-next": "1.0.64", "fs-extra": "^6.0.1", "glob": "^7.1.2", "node-plop": "^0.15.0", diff --git a/yarn.lock b/yarn.lock index 8460711e2ec..734200f0ea5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -326,9 +326,9 @@ version "1.0.0" resolved "https://registry.yarnpkg.com/@novnc/novnc/-/novnc-1.0.0.tgz#76b0e89e6f8738ca8154195baf5b8e6a80bc9105" -"@patternfly/patternfly-next@1.0.63": - version "1.0.63" - resolved "https://registry.yarnpkg.com/@patternfly/patternfly-next/-/patternfly-next-1.0.63.tgz#8ccf2c281483d71b5338b907ef5a11d235e50dd8" +"@patternfly/patternfly-next@1.0.64": + version "1.0.64" + resolved "https://registry.yarnpkg.com/@patternfly/patternfly-next/-/patternfly-next-1.0.64.tgz#d9a59c4361dea66b52573e2af0c9807896eeccfb" "@sindresorhus/is@^0.7.0": version "0.7.0"