From fc9173cf217187ca95cf6da336f74d287217f5ba Mon Sep 17 00:00:00 2001 From: Brian Garland Date: Tue, 20 Feb 2018 12:16:39 -0800 Subject: [PATCH 1/2] implement base file structure and styled() function --- .../src/components/Nav/Nav.base.tsx | 381 +++++++++++++++++ .../src/components/Nav/Nav.styles.ts | 28 ++ .../src/components/Nav/Nav.tsx | 386 +----------------- .../src/components/Nav/Nav.types.ts | 40 +- 4 files changed, 454 insertions(+), 381 deletions(-) create mode 100644 packages/office-ui-fabric-react/src/components/Nav/Nav.base.tsx create mode 100644 packages/office-ui-fabric-react/src/components/Nav/Nav.styles.ts diff --git a/packages/office-ui-fabric-react/src/components/Nav/Nav.base.tsx b/packages/office-ui-fabric-react/src/components/Nav/Nav.base.tsx new file mode 100644 index 00000000000000..da9be9817ab851 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Nav/Nav.base.tsx @@ -0,0 +1,381 @@ +import * as React from 'react'; +import { + autobind, + BaseComponent, + css, + divProperties, + getNativeProps, + getRTL +} from '../../Utilities'; +import { FocusZone, FocusZoneDirection } from '../../FocusZone'; +import { ActionButton, IButtonStyles } from '../../Button'; +import { Icon } from '../../Icon'; +import * as stylesImport from './Nav.scss'; +const styles: any = stylesImport; +import { AnimationClassNames, mergeStyles } from '../../Styling'; +import { + INav, + INavProps, + INavLinkGroup, + INavLink +} from './Nav.types'; + +// The number pixels per indentation level for Nav links. +const _indentationSize: number = 14; + +// The number of pixels of left margin +const _baseIndent: number = 3; + +// The number of pixels of padding to add to the far side of the button (allows ellipsis to happen) +const _farSidePadding: number = 20; + +// global var used in _isLinkSelectedKey +let _urlResolver: HTMLAnchorElement | undefined; + +export function isRelativeUrl(url: string): boolean { + // A URL is relative if it has no protocol. + return !!url && !/^[a-z0-9+-.]:\/\//i.test(url); +} + +export interface INavState { + isGroupCollapsed?: { [key: string]: boolean }; + isLinkExpandStateChanged?: boolean; + selectedKey?: string; +} + +export class NavBase extends BaseComponent implements INav { + + public static defaultProps: INavProps = { + groups: null + }; + + private _hasExpandButton: boolean; + + constructor(props: INavProps) { + super(props); + + this.state = { + isGroupCollapsed: {}, + isLinkExpandStateChanged: false, + selectedKey: props.initialSelectedKey || props.selectedKey, + }; + + if (props.groups) { + for (const group of props.groups) { + if (group.collapseByDefault && group.name) { + this.state.isGroupCollapsed![group.name] = true; + } + } + } + this._hasExpandButton = false; + } + + public componentWillReceiveProps(newProps: INavProps) { + const newGroups = newProps.groups || []; + const isGroupCollapsed = this.state.isGroupCollapsed!; + + // If the component's props were updated, new groups may have been added, which may have + // collapseByDefault set. Ensure that setting is respected for any new groups. + // (If isGroupCollapsed is already set for a group, don't overwrite that.) + let hasUpdated = false; + for (const newGroup of newGroups) { + if (newGroup.name && newGroup.collapseByDefault && !isGroupCollapsed.hasOwnProperty(newGroup.name)) { + isGroupCollapsed[newGroup.name] = true; + hasUpdated = true; + } + } + + if (hasUpdated) { + this.setState({ + isGroupCollapsed: isGroupCollapsed + }); + } + } + + public render(): JSX.Element | null { + const { groups, className, isOnTop } = this.props; + + if (!groups) { + return null; + } + + // When groups[x].name is specified or any of the links have children, the expand/collapse + // chevron button is shown and different padding is needed. _hasExpandButton marks this condition. + this._hasExpandButton = groups.some((group: INavLinkGroup) => { + return group ? !!group.name || (group.links && group.links.some((link: INavLink) => { + return !!(link && link.links && link.links.length); + })) : false; + }); + + const groupElements: React.ReactElement<{}>[] = groups.map(this._renderGroup); + + return ( + + + + ); + } + + public get selectedKey(): string | undefined { + return this.state.selectedKey; + } + + private _onRenderLink(link: INavLink) { + return (
{ link.name }
); + } + + private _renderNavLink(link: INavLink, linkIndex: number, nestingLevel: number) { + const isRtl: boolean = getRTL(); + const paddingBefore = _indentationSize * nestingLevel + _baseIndent; + const buttonStyles: IButtonStyles = { + root: { + [isRtl ? 'paddingRight' : 'paddingLeft']: paddingBefore, + [isRtl ? 'paddingLeft' : 'paddingRight']: _farSidePadding, + }, + textContainer: { + overflow: 'hidden', + }, + label: { + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', + lineHeight: '36px' + } + }; + const { + onRenderLink = this._onRenderLink + } = this.props; + + // Prevent hijacking of the parent window if link.target is defined + const rel = link.url && link.target && !isRelativeUrl(link.url) ? 'noopener noreferrer' : undefined; + + return ( + + { onRenderLink(link, this._onRenderLink) } + ); + } + + private _renderCompositeLink(link: INavLink, linkIndex: number, nestingLevel: number): React.ReactElement<{}> { + const isLinkSelected: boolean = this._isLinkSelected(link); + const isRtl: boolean = getRTL(); + const absolutePositionString = `${_indentationSize * nestingLevel + 1}px`; + + return ( +
+ { (link.links && link.links.length > 0 ? + : null + ) } + { this._renderNavLink(link, linkIndex, nestingLevel) } +
+ ); + } + + private _renderLink(link: INavLink, linkIndex: number, nestingLevel: number): React.ReactElement<{}> { + return ( +
  • + { this._renderCompositeLink(link, linkIndex, nestingLevel) } + { (link.isExpanded ? this._renderLinks(link.links, ++nestingLevel) : null) } +
  • + ); + } + + private _renderLinks(links: INavLink[] | undefined, nestingLevel: number): React.ReactElement<{}> | null { + if (!links || !links.length) { + return null; + } + const linkElements: React.ReactElement<{}>[] = links.map( + (link: INavLink, linkIndex: number) => this._renderLink(link, linkIndex, nestingLevel)); + + return ( + + ); + } + + @autobind + private _renderGroup(group: INavLinkGroup, groupIndex: number): React.ReactElement<{}> { + const isGroupExpanded: boolean = !this.state.isGroupCollapsed![group.name!]; + + return ( +
    + { (group.name ? + : null) + } +
    + { this._renderLinks(group.links, 0 /* nestingLevel */) } +
    +
    + ); + } + + private _onGroupHeaderClicked(group: INavLinkGroup, ev: React.MouseEvent): void { + const { isGroupCollapsed } = this.state; + const groupKey = group.name!; + const isCollapsed = !isGroupCollapsed![groupKey]; + + if (group.onHeaderClick) { + group.onHeaderClick(ev, isCollapsed); + } + + isGroupCollapsed![groupKey] = isCollapsed; + this.setState({ isGroupCollapsed: isGroupCollapsed }); + + ev.preventDefault(); + ev.stopPropagation(); + } + + private _onLinkExpandClicked(link: INavLink, ev: React.MouseEvent): void { + const { onLinkExpandClick } = this.props; + + if (onLinkExpandClick) { + onLinkExpandClick(ev, link); + } + + if (!ev.defaultPrevented) { + link.isExpanded = !link.isExpanded; + this.setState({ isLinkExpandStateChanged: true }); + } + + ev.preventDefault(); + ev.stopPropagation(); + } + + private _onNavAnchorLinkClicked(link: INavLink, ev: React.MouseEvent): void { + if (this.props.onLinkClick) { + this.props.onLinkClick(ev, link); + } + + this.setState({ selectedKey: link.key }); + } + + private _onNavButtonLinkClicked(link: INavLink, ev: React.MouseEvent): void { + if (link.onClick) { + link.onClick(ev, link); + } + + this.setState({ selectedKey: link.key }); + } + + private _isLinkSelected(link: INavLink): boolean { + // if caller passes in selectedKey, use it as first choice or + // if current state.selectedKey (from addressbar) is match to the link + if (this.props.selectedKey !== undefined) { + return link.key === this.props.selectedKey; + } else if (this.state.selectedKey !== undefined && link.key === this.state.selectedKey) { + return true; + } + + // resolve is not supported for ssr + if (typeof (window) === 'undefined') { + return false; + } + + if (!link.url) { + return false; + } + + _urlResolver = _urlResolver || document.createElement('a'); + + _urlResolver.href = link.url || ''; + const target: string = _urlResolver.href; + + if (location.href === target) { + return true; + } + + if (location.protocol + '//' + location.host + location.pathname === target) { + return true; + } + + if (location.hash) { + // Match the hash to the url. + if (location.hash === link.url) { + return true; + } + + // Match a rebased url. (e.g. #foo becomes http://hostname/foo) + _urlResolver.href = location.hash.substring(1); + + return _urlResolver.href === target; + } + return false; + } +} diff --git a/packages/office-ui-fabric-react/src/components/Nav/Nav.styles.ts b/packages/office-ui-fabric-react/src/components/Nav/Nav.styles.ts new file mode 100644 index 00000000000000..55c5277ba69204 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Nav/Nav.styles.ts @@ -0,0 +1,28 @@ +import { INavStyleProps, INavStyles } from './Nav.types'; +import { + IStyle, + ITheme, +} from '../../Styling'; + +export const getStyles = ( + props: INavStyleProps +): INavStyles => { + const { + className, + theme, + } = props; + + const { palette, semanticColors } = theme; + + return ({ + root: [ + 'ms-Nav', + { + // Insert css properties + + } + ], + + // Insert className styles + }); +}; diff --git a/packages/office-ui-fabric-react/src/components/Nav/Nav.tsx b/packages/office-ui-fabric-react/src/components/Nav/Nav.tsx index e4ce5042d54491..857d638b9998f3 100644 --- a/packages/office-ui-fabric-react/src/components/Nav/Nav.tsx +++ b/packages/office-ui-fabric-react/src/components/Nav/Nav.tsx @@ -1,381 +1,13 @@ -import * as React from 'react'; +import { styled } from '../../Utilities'; import { - autobind, - BaseComponent, - css, - divProperties, - getNativeProps, - getRTL -} from '../../Utilities'; -import { FocusZone, FocusZoneDirection } from '../../FocusZone'; -import { ActionButton, IButtonStyles } from '../../Button'; -import { Icon } from '../../Icon'; -import * as stylesImport from './Nav.scss'; -const styles: any = stylesImport; -import { AnimationClassNames, mergeStyles } from '../../Styling'; -import { - INav, INavProps, - INavLinkGroup, - INavLink + INavStyleProps, + INavStyles } from './Nav.types'; +import { NavBase } from './Nav.base'; +import { getStyles } from './Nav.styles'; -// The number pixels per indentation level for Nav links. -const _indentationSize: number = 14; - -// The number of pixels of left margin -const _baseIndent: number = 3; - -// The number of pixels of padding to add to the far side of the button (allows ellipsis to happen) -const _farSidePadding: number = 20; - -// global var used in _isLinkSelectedKey -let _urlResolver: HTMLAnchorElement | undefined; - -export function isRelativeUrl(url: string): boolean { - // A URL is relative if it has no protocol. - return !!url && !/^[a-z0-9+-.]:\/\//i.test(url); -} - -export interface INavState { - isGroupCollapsed?: { [key: string]: boolean }; - isLinkExpandStateChanged?: boolean; - selectedKey?: string; -} - -export class Nav extends BaseComponent implements INav { - - public static defaultProps: INavProps = { - groups: null - }; - - private _hasExpandButton: boolean; - - constructor(props: INavProps) { - super(props); - - this.state = { - isGroupCollapsed: {}, - isLinkExpandStateChanged: false, - selectedKey: props.initialSelectedKey || props.selectedKey, - }; - - if (props.groups) { - for (const group of props.groups) { - if (group.collapseByDefault && group.name) { - this.state.isGroupCollapsed![group.name] = true; - } - } - } - this._hasExpandButton = false; - } - - public componentWillReceiveProps(newProps: INavProps) { - const newGroups = newProps.groups || []; - const isGroupCollapsed = this.state.isGroupCollapsed!; - - // If the component's props were updated, new groups may have been added, which may have - // collapseByDefault set. Ensure that setting is respected for any new groups. - // (If isGroupCollapsed is already set for a group, don't overwrite that.) - let hasUpdated = false; - for (const newGroup of newGroups) { - if (newGroup.name && newGroup.collapseByDefault && !isGroupCollapsed.hasOwnProperty(newGroup.name)) { - isGroupCollapsed[newGroup.name] = true; - hasUpdated = true; - } - } - - if (hasUpdated) { - this.setState({ - isGroupCollapsed: isGroupCollapsed - }); - } - } - - public render(): JSX.Element | null { - const { groups, className, isOnTop } = this.props; - - if (!groups) { - return null; - } - - // When groups[x].name is specified or any of the links have children, the expand/collapse - // chevron button is shown and different padding is needed. _hasExpandButton marks this condition. - this._hasExpandButton = groups.some((group: INavLinkGroup) => { - return group ? !!group.name || (group.links && group.links.some((link: INavLink) => { - return !!(link && link.links && link.links.length); - })) : false; - }); - - const groupElements: React.ReactElement<{}>[] = groups.map(this._renderGroup); - - return ( - - - - ); - } - - public get selectedKey(): string | undefined { - return this.state.selectedKey; - } - - private _onRenderLink(link: INavLink) { - return (
    { link.name }
    ); - } - - private _renderNavLink(link: INavLink, linkIndex: number, nestingLevel: number) { - const isRtl: boolean = getRTL(); - const paddingBefore = _indentationSize * nestingLevel + _baseIndent; - const buttonStyles: IButtonStyles = { - root: { - [isRtl ? 'paddingRight' : 'paddingLeft']: paddingBefore, - [isRtl ? 'paddingLeft' : 'paddingRight']: _farSidePadding, - }, - textContainer: { - overflow: 'hidden', - }, - label: { - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - lineHeight: '36px' - } - }; - const { - onRenderLink = this._onRenderLink - } = this.props; - - // Prevent hijacking of the parent window if link.target is defined - const rel = link.url && link.target && !isRelativeUrl(link.url) ? 'noopener noreferrer' : undefined; - - return ( - - { onRenderLink(link, this._onRenderLink) } - ); - } - - private _renderCompositeLink(link: INavLink, linkIndex: number, nestingLevel: number): React.ReactElement<{}> { - const isLinkSelected: boolean = this._isLinkSelected(link); - const isRtl: boolean = getRTL(); - const absolutePositionString = `${_indentationSize * nestingLevel + 1}px`; - - return ( -
    - { (link.links && link.links.length > 0 ? - : null - ) } - { this._renderNavLink(link, linkIndex, nestingLevel) } -
    - ); - } - - private _renderLink(link: INavLink, linkIndex: number, nestingLevel: number): React.ReactElement<{}> { - return ( -
  • - { this._renderCompositeLink(link, linkIndex, nestingLevel) } - { (link.isExpanded ? this._renderLinks(link.links, ++nestingLevel) : null) } -
  • - ); - } - - private _renderLinks(links: INavLink[] | undefined, nestingLevel: number): React.ReactElement<{}> | null { - if (!links || !links.length) { - return null; - } - const linkElements: React.ReactElement<{}>[] = links.map( - (link: INavLink, linkIndex: number) => this._renderLink(link, linkIndex, nestingLevel)); - - return ( -
      - { linkElements } -
    - ); - } - - @autobind - private _renderGroup(group: INavLinkGroup, groupIndex: number): React.ReactElement<{}> { - const isGroupExpanded: boolean = !this.state.isGroupCollapsed![group.name!]; - - return ( -
    - { (group.name ? - : null) - } -
    - { this._renderLinks(group.links, 0 /* nestingLevel */) } -
    -
    - ); - } - - private _onGroupHeaderClicked(group: INavLinkGroup, ev: React.MouseEvent): void { - const { isGroupCollapsed } = this.state; - const groupKey = group.name!; - const isCollapsed = !isGroupCollapsed![groupKey]; - - if (group.onHeaderClick) { - group.onHeaderClick(ev, isCollapsed); - } - - isGroupCollapsed![groupKey] = isCollapsed; - this.setState({ isGroupCollapsed: isGroupCollapsed }); - - ev.preventDefault(); - ev.stopPropagation(); - } - - private _onLinkExpandClicked(link: INavLink, ev: React.MouseEvent): void { - const { onLinkExpandClick } = this.props; - - if (onLinkExpandClick) { - onLinkExpandClick(ev, link); - } - - if (!ev.defaultPrevented) { - link.isExpanded = !link.isExpanded; - this.setState({ isLinkExpandStateChanged: true }); - } - - ev.preventDefault(); - ev.stopPropagation(); - } - - private _onNavAnchorLinkClicked(link: INavLink, ev: React.MouseEvent): void { - if (this.props.onLinkClick) { - this.props.onLinkClick(ev, link); - } - - this.setState({ selectedKey: link.key }); - } - - private _onNavButtonLinkClicked(link: INavLink, ev: React.MouseEvent): void { - if (link.onClick) { - link.onClick(ev, link); - } - - this.setState({ selectedKey: link.key }); - } - - private _isLinkSelected(link: INavLink): boolean { - // if caller passes in selectedKey, use it as first choice or - // if current state.selectedKey (from addressbar) is match to the link - if (this.props.selectedKey !== undefined) { - return link.key === this.props.selectedKey; - } else if (this.state.selectedKey !== undefined && link.key === this.state.selectedKey) { - return true; - } - - // resolve is not supported for ssr - if (typeof (window) === 'undefined') { - return false; - } - - if (!link.url) { - return false; - } - - _urlResolver = _urlResolver || document.createElement('a'); - - _urlResolver.href = link.url || ''; - const target: string = _urlResolver.href; - - if (location.href === target) { - return true; - } - - if (location.protocol + '//' + location.host + location.pathname === target) { - return true; - } - - if (location.hash) { - // Match the hash to the url. - if (location.hash === link.url) { - return true; - } - - // Match a rebased url. (e.g. #foo becomes http://hostname/foo) - _urlResolver.href = location.hash.substring(1); - - return _urlResolver.href === target; - } - return false; - } -} +export const Nav = styled( + NavBase, + getStyles +); diff --git a/packages/office-ui-fabric-react/src/components/Nav/Nav.types.ts b/packages/office-ui-fabric-react/src/components/Nav/Nav.types.ts index 200b55a318f21f..37de48d7bb05f6 100644 --- a/packages/office-ui-fabric-react/src/components/Nav/Nav.types.ts +++ b/packages/office-ui-fabric-react/src/components/Nav/Nav.types.ts @@ -1,5 +1,7 @@ import * as React from 'react'; -import { IRenderFunction } from '../../Utilities'; +import { NavBase } from './Nav.base'; +import { IStyle, ITheme } from '../../Styling'; +import { IRenderFunction, IStyleFunction } from '../../Utilities'; export interface INav { /** @@ -19,15 +21,26 @@ export interface INavProps { componentRef?: (component: INav) => void; /** - * A collection of link groups to display in the navigation bar + * Call to provide customized styling that will layer on top of the variant rules */ - groups: INavLinkGroup[] | null; + getStyles?: IStyleFunction; + + /** + * Theme provided by HOC. + */ + theme?: ITheme; /** - * Optional class name to allow styling. + * Additional css class to apply to the Nav + * @defaultvalue undefined */ className?: string; + /** + * A collection of link groups to display in the navigation bar + */ + groups: INavLinkGroup[] | null; + /** * Used to customize how content inside the link tag is rendered * @defaultvalue Default link rendering @@ -201,3 +214,22 @@ export interface INavLink { */ [propertyName: string]: any; } + +export interface INavStyleProps { + /** + * Accept theme prop. + */ + theme: ITheme; + + /** + * Accept custom classNames + */ + className?: string; +} + +export interface INavStyles { + /** + * Style for the root element. + */ + root?: IStyle; +} \ No newline at end of file From 4a6b76325aab909aca3ff406fe8644d3df0fca5e Mon Sep 17 00:00:00 2001 From: Brian Garland Date: Tue, 20 Feb 2018 12:17:30 -0800 Subject: [PATCH 2/2] rush change --- .../nav-scss2ms-pt1_2018-02-20-20-17.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/office-ui-fabric-react/nav-scss2ms-pt1_2018-02-20-20-17.json diff --git a/common/changes/office-ui-fabric-react/nav-scss2ms-pt1_2018-02-20-20-17.json b/common/changes/office-ui-fabric-react/nav-scss2ms-pt1_2018-02-20-20-17.json new file mode 100644 index 00000000000000..0a9eb4d10f6f0e --- /dev/null +++ b/common/changes/office-ui-fabric-react/nav-scss2ms-pt1_2018-02-20-20-17.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "Converting Nav SCSS to MergeStyles step 1 - file structure", + "type": "minor" + } + ], + "packageName": "office-ui-fabric-react", + "email": "v-brgarl@microsoft.com" +} \ No newline at end of file