From dfc84b1bcca8253e94f9eaa3bc46102ba3d4e538 Mon Sep 17 00:00:00 2001 From: Jammy Louie Date: Wed, 15 May 2019 09:53:23 -0400 Subject: [PATCH] refactor(core): add React Components for PageSection and PageNavigator (#6926) --- .../modules/core/src/presentation/index.ts | 1 + .../presentation/navigation/PageNavigator.tsx | 182 ++++++++++++++++++ .../presentation/navigation/PageSection.tsx | 52 +++++ .../core/src/presentation/navigation/index.ts | 2 + .../navigation/pageNavigation.less | 15 +- 5 files changed, 246 insertions(+), 6 deletions(-) create mode 100644 app/scripts/modules/core/src/presentation/navigation/PageNavigator.tsx create mode 100644 app/scripts/modules/core/src/presentation/navigation/PageSection.tsx create mode 100644 app/scripts/modules/core/src/presentation/navigation/index.ts diff --git a/app/scripts/modules/core/src/presentation/index.ts b/app/scripts/modules/core/src/presentation/index.ts index 4f5e94ce8e9..eea10c632ae 100644 --- a/app/scripts/modules/core/src/presentation/index.ts +++ b/app/scripts/modules/core/src/presentation/index.ts @@ -13,3 +13,4 @@ export * from './collapsibleSection/CollapsibleSection'; export * from './forms'; export * from './robotToHumanFilter/robotToHuman.filter'; export * from './sortToggle'; +export * from './navigation'; diff --git a/app/scripts/modules/core/src/presentation/navigation/PageNavigator.tsx b/app/scripts/modules/core/src/presentation/navigation/PageNavigator.tsx new file mode 100644 index 00000000000..e58f23004af --- /dev/null +++ b/app/scripts/modules/core/src/presentation/navigation/PageNavigator.tsx @@ -0,0 +1,182 @@ +import * as React from 'react'; +import { isFunction, throttle } from 'lodash'; + +import { ReactInjector } from 'core/reactShims'; +import { ScrollToService } from 'core/utils/scrollTo/scrollTo.service'; +import { UUIDGenerator } from 'core/utils/uuid.service'; + +export interface INavigationPage { + key: string; + label: string; + visible?: boolean; + badge?: string; +} + +export interface IPageNavigatorProps { + scrollableContainer: string; + deepLinkParam?: string; + hideNavigation?: boolean; +} + +export interface IPageNavigatorState { + id: string; + currentPageKey: string; + pages: INavigationPage[]; +} + +export class PageNavigator extends React.Component { + private element: JQuery; + private container: any; + private navigator: any; + + constructor(props: IPageNavigatorProps) { + super(props); + this.state = { + id: UUIDGenerator.generateUuid(), + currentPageKey: null, + pages: [], + }; + } + + public componentDidMount(): void { + const { children, deepLinkParam, hideNavigation, scrollableContainer } = this.props; + this.container = this.element.closest(scrollableContainer); + if (isFunction(this.container.bind) && !hideNavigation) { + this.container.bind(this.getEventKey(), throttle(() => this.handleScroll(), 20)); + } + this.navigator = this.element.find('.page-navigation'); + if (deepLinkParam && ReactInjector.$stateParams[deepLinkParam]) { + this.setCurrentSection(ReactInjector.$stateParams[deepLinkParam]); + } + + const pages = React.Children.map(children, (child: any) => { + if (child.type && child.type.name === 'PageSection') { + return { + key: child.props.pageKey, + label: child.props.label, + visible: child.props.visible !== false, + badge: child.props.badge, + }; + } + return null; + }); + this.setState({ + pages, + currentPageKey: pages.length > 0 ? pages[0].key : null, + }); + } + + public componentWillUnmount(): void { + if (isFunction(this.container.unbind) && !this.props.hideNavigation) { + this.container.unbind(this.getEventKey()); + } + } + + private setCurrentSection(key: string): void { + this.setState({ currentPageKey: key }); + this.syncLocation(key); + ScrollToService.scrollTo(`[data-page-id=${key}]`, this.props.scrollableContainer, this.container.offset().top); + this.container.find('.highlighted').removeClass('highlighted'); + this.container.find(`[data-page-id=${key}]`).addClass('highlighted'); + } + + private getEventKey(): string { + return `scroll.pageNavigation.${this.state.id}`; + } + + private handleScroll(): void { + const navigatorRect = this.element.get(0).getBoundingClientRect(), + scrollableContainerTop = this.container.get(0).getBoundingClientRect().top; + + const currentPage = this.state.pages.find(p => { + const content = this.container.find(`[data-page-content=${p.key}]`); + if (content.length) { + return content.get(0).getBoundingClientRect().bottom > scrollableContainerTop; + } + return false; + }); + if (currentPage) { + this.setState({ currentPageKey: currentPage.key }); + this.syncLocation(currentPage.key); + this.navigator.find('li').removeClass('current'); + this.navigator.find(`[data-page-navigation-link=${currentPage.key}]`).addClass('current'); + } + + if (navigatorRect.top < scrollableContainerTop) { + this.navigator.css({ + position: 'fixed', + width: this.navigator.get(0).getBoundingClientRect().width, + top: scrollableContainerTop, + }); + } else { + this.navigator.css({ + position: 'relative', + top: 0, + width: '100%', + }); + } + } + + private syncLocation(key: string): void { + const { deepLinkParam } = this.props; + if (deepLinkParam) { + ReactInjector.$state.go('.', { [deepLinkParam]: key }, { notify: false, location: 'replace' }); + } + } + + private refCallback = (element: HTMLDivElement): void => { + if (element) { + this.element = $(element); + } + }; + + private updatePagesConfig(page: INavigationPage): void { + const pages = [...this.state.pages]; + const pageConfig = pages.find(p => p.key === page.key); + if (pageConfig) { + pageConfig.badge = page.badge; + pageConfig.label = page.label; + pageConfig.visible = page.visible; + this.setState({ pages }); + } + } + + public render(): JSX.Element { + const { children, hideNavigation } = this.props; + const { currentPageKey, pages } = this.state; + const updatedChildren = React.Children.map(children, (child: any) => + React.cloneElement(child, { updatePagesConfig: (p: INavigationPage) => this.updatePagesConfig(p) }), + ); + return ( +
+
+ {!hideNavigation && ( +
+ +
+ )} +
+
{updatedChildren}
+
+
+
+ ); + } +} diff --git a/app/scripts/modules/core/src/presentation/navigation/PageSection.tsx b/app/scripts/modules/core/src/presentation/navigation/PageSection.tsx new file mode 100644 index 00000000000..bda06b4d7ce --- /dev/null +++ b/app/scripts/modules/core/src/presentation/navigation/PageSection.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; + +import { INavigationPage } from './PageNavigator'; + +interface IPageSectionProps { + pageKey: string; + label: string; + badge?: string; + visible?: boolean; + noWrapper?: boolean; + updatePagesConfig?: (page: INavigationPage) => void; +} + +export class PageSection extends React.Component { + constructor(props: IPageSectionProps) { + super(props); + } + + public componentDidUpdate(prevProps: IPageSectionProps): void { + const { badge, pageKey, label, updatePagesConfig, visible } = this.props; + if ( + prevProps.visible !== this.props.visible || + prevProps.badge !== this.props.badge || + prevProps.label !== this.props.label + ) { + updatePagesConfig && + updatePagesConfig({ + key: pageKey, + label, + visible: visible !== false, + badge, + }); + } + } + + public render(): JSX.Element { + const { children, pageKey, label, noWrapper, visible } = this.props; + + return visible !== false ? ( +
+
+

{label}

+
+ {children} +
+
+
+ ) : ( + <> + ); + } +} diff --git a/app/scripts/modules/core/src/presentation/navigation/index.ts b/app/scripts/modules/core/src/presentation/navigation/index.ts new file mode 100644 index 00000000000..4740ca14ec1 --- /dev/null +++ b/app/scripts/modules/core/src/presentation/navigation/index.ts @@ -0,0 +1,2 @@ +export * from './PageNavigator'; +export * from './PageSection'; diff --git a/app/scripts/modules/core/src/presentation/navigation/pageNavigation.less b/app/scripts/modules/core/src/presentation/navigation/pageNavigation.less index 7dd77b311b0..bad2de201a8 100644 --- a/app/scripts/modules/core/src/presentation/navigation/pageNavigation.less +++ b/app/scripts/modules/core/src/presentation/navigation/pageNavigation.less @@ -1,6 +1,7 @@ -@import (reference) "~core/presentation/less/imports/commonImports.less"; +@import (reference) '~core/presentation/less/imports/commonImports.less'; -page-navigator { +page-navigator, +.page-navigator { display: block; .page-navigation { list-style-type: none; @@ -12,7 +13,8 @@ page-navigator { } li { display: block; - &:hover, &.current { + &:hover, + &.current { background-color: var(--color-accent-g2); a { color: var(--color-primary); @@ -31,9 +33,11 @@ page-navigator { } } } - page-section { + page-section, + .page-section { display: block; - .no-wrapper, .section-body { + .no-wrapper, + .section-body { margin: 10px 10px 20px 10px; } .no-wrapper { @@ -56,7 +60,6 @@ page-navigator { animation: 0.5s ease-in-out 0 highlighted; animation-iteration-count: 1; } - } }