diff --git a/.npmignore b/.npmignore index 72a6b833758ccc..7f38ee5d80b7ac 100644 --- a/.npmignore +++ b/.npmignore @@ -1,12 +1,22 @@ -src -node_modules -.gitignore -.gitattributes +*.config.js +*.nuspec +*.yml .editorconfig +.gitattributes +.gitignore +.vscode +coverage +dist/*.stats.html +dist/demo*.* +examples gulpfile.js -config.js -tsconfig.json +index.html jsconfig.json -webpack.config.js +karma.config.js +node_modules +src +tsconfig.json +tsd.json +tslint.json typings - +webpack.config.js diff --git a/.pullapprove.yml b/.pullapprove.yml new file mode 100644 index 00000000000000..df88e9e2688ab5 --- /dev/null +++ b/.pullapprove.yml @@ -0,0 +1,19 @@ +approve_by_comment: true +approve_regex: ^Approved +reject_regex: ^Rejected +reset_on_push: false +reject_value: -1 +reviewers: + members: + - ericthompson + - Jahnp + - battletoilet + - wdo3650 + - mikewheaton + - dzearing + - cliffkoh + - aditima + - yiminwu + - antonlabunets + name: pullapprove + required: 1 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000000000..b8208214c949e3 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: node_js +node_js: +- '4.2' +before_script: +- npm install -g gulp +script: gulp --production +deploy: + provider: npm + email: dzearing@microsoft.com + api_key: + secure: kLwlKSvDBoGsYP0p+64JnW5kmSpZfSmt2YxKKwWFcThlsiftbZtUILWFVpPd7A5yymsYuOONZTXppaW00OWaH1Bqs6yVHOn3kVQzE7VK/FtVwGZnqFfF0ea0pYYfto3OIFnyZ86vZ0M8u2A+3FGqjGYT1y3RDbG5GqhURBNd+KBhHrMS+pSaGDgEjskslLeQ+QtQ1/t17j0ZQZeYpxl+qFKislR1djeF0pkOjaoJ8wAUB77E0RhVyKP4cOyvtiF9E8nmnaurNYuabZqFMzOg0PgzMhh6xWv10aGF7jNzgojzCix/grCJR1gdfaE6epPGkrufK1nFjCVb60Z+jHZmuhXrW80Qa9AsMbNkXLlm9g6OLm63Ub2xqNQ5338NteGygWj8FRYe7ZngQ/vAY7SK44oA6+QRJAcpV6ieUFeIN8oEoZ42TJjUuKa6xaRVxuRQs2yobB3GzE4QioWLtqlDo+jbkzc0uFUpQHfErH5pHa7+qHDawiY9sO41lKvds6KfD3PXqHJxdokQh5nRB28k0eN+rFiSyvimDmerqBeG0U2oVJuYGVwGZSMKkOXGoZjZaCOFkPaZcS+HQZ+iDYpqEoB1jF1iYgkknZQLDlprWywjXEy/u2C2Bkd1K31aFCwYxrxkgGquii1ugUFVrjCpNLbeeoKALx5JapDfyAUVF+U= + on: + tags: true + repo: OfficeDev/office-ui-fabric-react diff --git a/.vscode/settings.json b/.vscode/settings.json index e71cf1cd095f29..d6a1e0b062b296 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,14 +8,14 @@ // When enabled, will trim trailing whitespace when you save a file. "files.trimTrailingWhitespace": true, - // Controls if the editor should automatically close brackets after opening them - "editor.autoClosingBrackets": false, + // Controls if the editor should automatically close brackets after opening them + "editor.autoClosingBrackets": false, // Controls whether the editor should render whitespace characters "editor.renderWhitespace": true, - // Controls if the editor will insert spaces for tabs. Accepted values: "auto", true, false. If set to "auto", the value will be guessed when a file is opened. - "editor.insertSpaces": true, + // Controls if the editor will insert spaces for tabs. Accepted values: "auto", true, false. If set to "auto", the value will be guessed when a file is opened. + "editor.insertSpaces": true, "files.exclude": { "**/.git": true, diff --git a/README.md b/README.md index ce742def97894f..7b8880790be502 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Fabric React is a responsive, mobile-first collection of robust components desig ## View the docs -Before you get started, make sure you have [node.js](https://nodejs.org/), [gulp](http://gulpjs.com/), and [git](https://git-scm.com/) installed. To view the doucmentation including examples, contracts, current statuses, and to add functionality or fix issues locally, you can: +Before you get started, make sure you have [node.js](https://nodejs.org/), [gulp](http://gulpjs.com/), and [git](https://git-scm.com/) installed. To view the documentation including examples, contracts, component status, and to add functionality or fix issues locally, you can: 1. `git clone https://github.com/OfficeDev/office-ui-fabric-react.git` 2. `npm install` @@ -30,6 +30,10 @@ This will open a web browser with the example page. You can make changes to the ## Get started +### Tutorial +[Here is a step by step tutorial](./TUTORIAL.md) on how to build a simple React app with an Office UI Fabric React component. + +### Integrate into an existing project Integrating components into your project depends heavily on your setup. The recommended setup is to use a bundler such as Webpack which can resolve NPM package imports in your code and can bundle the specific things you import. Within an npm project, you should install the package and save it as a dependency: @@ -52,7 +56,6 @@ const MyPage = () => (
); ReactDOM.render(, document.body.firstChild); ``` - ## Advanced usage For advanced usage including info about module vs. path-based imports and using an AMD bundler like Require, see our [advanced documentation](https://github.com/OfficeDev/office-ui-fabric-react/blob/master/ghdocs/ADVANCED.md). @@ -70,7 +73,7 @@ We're excited to share our development of this project with folks outside of the ## Licenses -All files on the Office UI Fabric React GitHub repository are subject to the MIT license. Please read the License file at the root of the project. +All files on the Office UI Fabric React GitHub repository are subject to the MIT license. Please read the License file at the root of the project. Usage of the fonts referenced in Office UI Fabric files is subject to the [license](http://appsforoffice.microsoft.com/fabric/Segoe_UI_and_Fabric_CDN_License.txt). @@ -79,6 +82,6 @@ Usage of the fonts referenced in Office UI Fabric files is subject to the [licen We use [GitHub Releases](https://github.com/blog/1547-release-your-software) to manage our releases, including the changelog between every release. View a complete list of additions, fixes, and changes on the [releases](https://github.com/OfficeDev/office-ui-fabric-react/releases) page. -- - - +- - - This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/examples/todo-app/gulpfile.js b/examples/todo-app/gulpfile.js new file mode 100644 index 00000000000000..5e39ea431c63be --- /dev/null +++ b/examples/todo-app/gulpfile.js @@ -0,0 +1,8 @@ +'use strict'; + +let build = require('web-library-build'); + +build.tslint.isEnabled = () => false; +build.sass.isEnabled =() => true; + +build.initialize(require('gulp')); diff --git a/examples/todo-app/index.html b/examples/todo-app/index.html new file mode 100644 index 00000000000000..d9315a40400cf5 --- /dev/null +++ b/examples/todo-app/index.html @@ -0,0 +1,17 @@ + + + + + + + + Test Page + + + + + + + + + diff --git a/examples/todo-app/package.json b/examples/todo-app/package.json new file mode 100644 index 00000000000000..38ff083fb860fe --- /dev/null +++ b/examples/todo-app/package.json @@ -0,0 +1,23 @@ +{ + "name": "fabric-react-example", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "react-addons-update": "^0.14.0", + "web-library-build": "^0.1.3" + }, + "dependencies": { + "es6-promise": "^3.2.1", + "load-themed-styles": "^1.2.0", + "office-ui-fabric": "^2.6.1", + "office-ui-fabric-react": "^0.33.0", + "react": "^0.14.0", + "react-dom": "^0.14.0" + } +} diff --git a/examples/todo-app/src/DataProvider.ts b/examples/todo-app/src/DataProvider.ts new file mode 100644 index 00000000000000..9ee39e125b9007 --- /dev/null +++ b/examples/todo-app/src/DataProvider.ts @@ -0,0 +1,166 @@ +import update = require('react-addons-update'); +import { findIndex } from 'office-ui-fabric-react/lib/utilities/array'; +import { ITodoItem, IDataProvider } from './types/index'; + +const ADD_ITEMS_DELAY = 500; + +/** + * DataProvider the class used to maintain the client side data. + * + * It interact with data provider to sync data items with sharepoint list. + * It also maintain the loading state to tell React Component once the action is + * started of finished. + */ +export default class DataProvider implements IDataProvider { + /** + * The counter used to identify the latest server call. + * If the current call is not the latest one, we don't want to update the list + * to mess up with the out of date data with data in this store. + */ + private _items: ITodoItem[]; + private _isLoading: boolean; + private _listeners: Array<() => void>; + + /** + * The items store in the local. It only contains the data recently fetched from server. + */ + public get items(): Array { return this._items; } + public set items(value: Array) { + this._items = value; + this._emitChange(); + } + + /** + * Whether there is unfinished server request currently. + */ + public get isLoading(): boolean { return this._isLoading; } + public set isLoading(value: boolean) { + this._isLoading = value; + this._emitChange(); + } + + constructor() { + this._items = [ + { + id: '61b59681-2a82-4a51-b221-8c35e333ae89', + title: 'Finish Sample Todo web part before dev kitchen', + isComplete: false + }, + { + id: '94a844ae-0c6a-4820-8042-dbc386bdf930', + title: 'Finish All the work in Todo web part before dev kitchen', + isComplete: false + }, + { + id: '5fa55618-90f9-4b5f-b12d-60c9fb1fc7f0', + title: 'SharePoint API investigation for Todo web part', + isComplete: true + }, + { + id: '2ae54c74-1395-4a49-8dd2-4857efdd0e5e', + title: 'Bug fixing of Pivot Control', + isComplete: true + } + ]; + this._isLoading = false; + + this._listeners = []; + this.createItem = this.createItem.bind(this); + this.deleteItem = this.deleteItem.bind(this); + this.toggleComplete = this.toggleComplete.bind(this); + } + + /** + * Create a new item and add it to the list through data provider. + */ + public createItem(title: string): Promise { + this.isLoading = true; + + return new Promise((resolve) => { + const newItem: ITodoItem = { + id: this._generateGuid(), + title: title, + isComplete: false + }; + + setTimeout(() => { + this.items = this.items.concat(newItem); + this.isLoading = false; + resolve(this.items); + }, ADD_ITEMS_DELAY); + }); + } + + /** + * Delete a item from the list through data provider. + */ + public deleteItem(delItem: ITodoItem): Promise { + return new Promise((resolve) => { + this.items = + this.items.filter((item: ITodoItem) => item.id !== delItem.id); + resolve(this.items); + }); + } + + /** + * Toggle the complete state of an item by. + * + * Will call updateItem function to update complete state of this item. + */ + public toggleComplete(item: ITodoItem): Promise { + // Create a new Item in which the PercentComplete value has been changed. + const newItem: ITodoItem = update(item, { + isComplete: { $set: item.isComplete === true ? false : true } + }); + + return new Promise((resolve, reject) => { + const index: number = + findIndex( + this.items, + (item: ITodoItem) => item.id === newItem.id + ); + if (index !== -1) { + this.items[index] = newItem; + this.items = this.items.slice(0); + resolve(this.items); + } else { + reject(new Error(`Item to update doesn't exist.`)); + } + }); + } + + /** + * Add listener to the provider. + * + * Once the store has a change of state and emit that change, the listeners will be called. + */ + public addListener(listener: () => void): void { + this._listeners.push(listener); + } + + /** + * Remove the registered listener. + * + * You must remove the listener registered by addListener method when you are not going to use it anymore. + */ + public removeListener(listener: () => void): void { + const listenerIdx: number = this._listeners.indexOf(listener); + if (listenerIdx > -1) { + this._listeners.splice(listenerIdx, 1); + } + } + + /** + * Emit the changes in the store to all listeners. + */ + private _emitChange(): void { + this._listeners.forEach((listener: () => void) => listener()); + } + + private _generateGuid(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + } +} diff --git a/examples/todo-app/src/components/Todo.module.scss b/examples/todo-app/src/components/Todo.module.scss new file mode 100644 index 00000000000000..35fd4fc8dd9c91 --- /dev/null +++ b/examples/todo-app/src/components/Todo.module.scss @@ -0,0 +1,93 @@ +@import "../../node_modules/office-ui-fabric/dist/sass/Fabric.Common"; + +.todo { + padding: 28px 40px 60px 40px; + max-width: 640px; + margin: 0 auto; + border: 2px $ms-color-neutralLighter solid; + @include ms-font-m; + + .topRow { + position: relative; + } + + .todoHeading { + display: inline-block; + } + + .todoPivot { + padding-top: 24px; + } + + .todoForm { + display: table-row; + + .textField { + display: table-cell; + width: 100%; + } + .addButton { + display: table-cell; + @include margin-left(16px); + white-space: nowrap; + } + } + + .todoList { + margin-top: 20px; + border-top: 1px $ms-color-neutralLight solid; + + .todoItem { + border: 1px $ms-color-neutralLight solid; + border-top: none; + + .itemTaskRow { + padding: 16px 20px; + + .deleteButton { + position: absolute; + top: 50%; + transform: translateY(-50%); + right: 0px; + @include margin-right(16px); + + &:hover, &:focus { + color: $ms-color-themePrimary; + } + } + } + + &.isHidden { + display: none; + } + + &.isCompleted { + border-bottom: 1px $ms-color-white solid; + border-left: 1px $ms-color-neutralLighter solid; + border-right: 1px $ms-color-neutralLighter solid; + @include ms-bgColor-neutralLighter; + } + } + } + + .workingOnItSpinner { + display: inline-block; + position: absolute; + top: 0; + bottom: 0; + margin: auto; + height: 33.33333%; + line-height: 1.5em; + padding: 0px 24px; + } + + .fetchingTasksSpinner { + display: flex; + justify-content: center; + padding: 24px 0px; + } + + @media only screen and (max-width:640px) { + padding: 20px 20px; + } +} \ No newline at end of file diff --git a/examples/todo-app/src/components/Todo.tsx b/examples/todo-app/src/components/Todo.tsx new file mode 100644 index 00000000000000..bc4911c16caf85 --- /dev/null +++ b/examples/todo-app/src/components/Todo.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; +import { Spinner, SpinnerType } from 'office-ui-fabric-react/lib/Spinner'; +import DataProvider from '../DataProvider'; +import { ITodoProps, ITodoState, ITodoItem, ITodoItemProps } from '../types/index'; + +import TodoForm from './TodoForm'; +import TodoTabs from './TodoTabs'; + +import styles from './Todo.module.scss'; +import strings from './../strings'; + +/** + * Todo component is the top level react component of this web part. + * It uses fabric-react component + * + * Link of Spinner: https://fabricreact.azurewebsites.net/fabric-react/master/#/examples/spinner + */ +export default class Todo extends React.Component { + constructor(props: ITodoProps) { + super(props); + + this._onProviderChange = this._onProviderChange.bind(this); + this.state = { + items: this.props.dataProvider.items + }; + } + + public componentDidMount(): void { + this.props.dataProvider.addListener(this._onProviderChange); + } + + public componentWillUnmount(): void { + this.props.dataProvider.removeListener(this._onProviderChange); + } + + public render(): React.ReactElement { + return ( +
+
+

{ strings.todoListTitle }

+ { this._renderWorkingOnItSpinner() } +
+ + + { this._renderFetchingTasksSpinner() } +
+ ); + } + + private _renderWorkingOnItSpinner(): React.ReactElement> { + return this.props.dataProvider.isLoading && this.state.items.length > 0 + ?
+ +
+ : undefined; + } + + private _renderFetchingTasksSpinner(): React.ReactElement> { + return this.props.dataProvider.isLoading && this.state.items.length === 0 + ?
+ +
+ : undefined; + } + + private _onProviderChange(): void { + this.setState({ + items: this.props.dataProvider.items + }); + } +} diff --git a/examples/todo-app/src/components/TodoForm.tsx b/examples/todo-app/src/components/TodoForm.tsx new file mode 100644 index 00000000000000..5d8843420bfe84 --- /dev/null +++ b/examples/todo-app/src/components/TodoForm.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { Button, ButtonType } from 'office-ui-fabric-react/lib/Button'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import { ITodoFormProps, ITodoFormState } from '../types/index'; +import styles from './Todo.module.scss'; +import strings from './../strings'; + +/** + * The form component used for adding new item to the list. + * + * It uses fabric-react component + + ); + } + + private _onSubmit(event: React.FormEvent): void { + event.preventDefault(); + + if (!this._getTitleErrorMessage(this._textField.value)) { + this.setState({ + inputValue: '' + } as ITodoFormState); + + this.props.onSubmit(this._textField.value); + } else { + this.setState({ + errorMessage: this._getTitleErrorMessage(this.state.inputValue) + } as ITodoFormState); + + this._textField.focus(); + } + } + + private _onBeforeTextFieldChange(newValue: string): void { + this.setState({ + inputValue: newValue, + errorMessage: '' + }); + } + + private _getTitleErrorMessage(title: string): string { + if (title.trim() === '' ) { + return strings.titleEmptyErrorMessage; + } else { + return ''; + } + } +} diff --git a/examples/todo-app/src/components/TodoItem.tsx b/examples/todo-app/src/components/TodoItem.tsx new file mode 100644 index 00000000000000..ca6e51cfaf2c6a --- /dev/null +++ b/examples/todo-app/src/components/TodoItem.tsx @@ -0,0 +1,102 @@ +import * as React from 'react'; +import { Button, ButtonType } from 'office-ui-fabric-react/lib/Button'; +import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; +import { FocusZone, FocusZoneDirection } from 'office-ui-fabric-react/lib/FocusZone'; +import { DocumentCardActivity } from 'office-ui-fabric-react/lib/DocumentCard'; +import { css } from 'office-ui-fabric-react/lib/utilities/css'; +import { ITodoItem, ITodoItemProps } from '../types/index'; + +import styles from './Todo.module.scss'; +import strings from './../strings'; + +/** + * TodoItem component using fabric-react component - )} + ) : ( GroupSpacer({ count: 1 }) ) + } { GroupSpacer({ count: groupLevel }) } diff --git a/src/components/DetailsList/GroupSpacer.scss b/src/components/GroupedList/GroupSpacer.scss similarity index 100% rename from src/components/DetailsList/GroupSpacer.scss rename to src/components/GroupedList/GroupSpacer.scss diff --git a/src/components/DetailsList/GroupSpacer.tsx b/src/components/GroupedList/GroupSpacer.tsx similarity index 100% rename from src/components/DetailsList/GroupSpacer.tsx rename to src/components/GroupedList/GroupSpacer.tsx diff --git a/src/components/GroupedList/GroupedList.Props.ts b/src/components/GroupedList/GroupedList.Props.ts new file mode 100644 index 00000000000000..86f2e011d5ea3a --- /dev/null +++ b/src/components/GroupedList/GroupedList.Props.ts @@ -0,0 +1,187 @@ +import * as React from 'react'; +import { + GroupedList +} from './GroupedList'; +import { + IListProps +} from '../List'; + +import { + IDragDropContext, + IDragDropEvents, + IDragDropHelper +} from '../../utilities/dragdrop/index'; +import { + ISelection, + SelectionMode +} from '../../utilities/selection/index'; +import { IViewport } from '../../utilities/decorators/withViewport'; + +export interface IGroupedList { + /** + * Ensures that the list content is updated. Call this in cases where the list prop updates don't change, but the list + * still needs to be re-evaluated. For example, if a sizer bar is adjusted and causes the list width to change, you can + * call this to force a re-evaluation. Be aware that this can be an expensive operation and should be done sparingly. + */ + forceUpdate: () => void; + + /** + * Toggles the collapsed state of all the groups in the list. + */ + toggleCollapseAll: (allCollapsed: boolean) => void; +} + +export interface IGroupedListProps extends React.Props { + /** Optional class name to add to the root element. */ + className?: string; + + /** Map of callback functions related to drag and drop functionality. */ + dragDropEvents?: IDragDropEvents; + + /** helper to manage drag/drop across item and groups */ + dragDropHelper?: IDragDropHelper; + + /** Event names and corresponding callbacks that will be registered to groups and rendered elements */ + eventsToRegister?: [{ eventName: string, callback: (context: IDragDropContext, event?: any) => void }]; + + /** Optional override properties to render groups. */ + groupProps?: IGroupRenderProps; + + /** Optional grouping instructions. */ + groups?: IGroup[]; + + /** List of items to render. */ + items: any[]; + + /** Optional properties to pass through to the list components being rendered. */ + listProps?: IListProps; + + /** Rendering callback to render the group items. */ + onRenderCell: ( + nestingDepth?: number, + item?: any, + index?: number + ) => React.ReactNode; + + /** Optional selection model to track selection state. */ + selection?: ISelection; + + /** Controls how/if the list manages selection. */ + selectionMode?: SelectionMode; + + /** Optional Viewport, provided by the parent component. */ + viewport?: IViewport; +} + +export interface IGroup { + /** + * Unique identifier for the group. + */ + key: string; + + /** + * Display name for the group, rendered on the header. + */ + name: string; + + /** + * Start index for the group within the given items. + */ + startIndex: number; + + /** + * How many items should be rendered within the group. + */ + count: number; + + /** + * Nested groups, if any. + */ + children?: IGroup[]; + + /** + * Number indicating the level of nested groups. + */ + level?: number; + + /** + * If all the items in the group are selected. + */ + isSelected?: boolean; + + /** + * If all the items in the group are collapsed. + */ + isCollapsed?: boolean; + + /** + * If the items within the group are summarized or showing all. + */ + isShowingAll?: boolean; + + /** + * If drag/drop is enabled for the group header. + */ + isDropEnabled?: boolean; + + /** + * Arbitrary data required to be preserved by the caller. + */ + data?: any; + + /** + * Override which allows the caller to provide a custom header. + */ + onRenderHeader?: (group: IGroup) => React.ReactNode; + + /** + * Override which allows the caller to provider a customer footer. + */ + onRenderFooter?: (group: IGroup) => React.ReactNode; +} + +export interface IGroupRenderProps { + /** Boolean indicating if all groups are in collapsed state. */ + isAllGroupsCollapsed?: boolean; + + /** Grouping item limit. */ + getGroupItemLimit?: (group: IGroup) => number; + + /** Callback for when all groups are expanded or collapsed. */ + onToggleCollapseAll?: (isAllCollapsed: boolean) => void; + + /** Information to pass in to the group header. */ + headerProps?: IGroupHeaderProps; + + /** Information to pass in to the group footer. */ + footerProps?: IGroupFooterProps; +} + +export interface IGroupHeaderProps { + /** Callback to determine if a group has missing items and needs to load them from the server. */ + isGroupLoading?: (group: IGroup) => boolean; + + /** Text shown on group headers to indicate the group is being loaded. */ + loadingText?: string; + + /** Callback for when the group header is clicked. */ + onGroupHeaderClick?: (group: IGroup) => void; + + /** Callback for when the group is expanded or collapsed. */ + onToggleCollapse?: (group: IGroup) => void; + + /** Callback for when the group is selected. */ + onToggleSelectGroup?: (group: IGroup) => void; + + /** Determines if the group selection check box is shown for collapsed groups. */ + isCollapsedGroupSelectVisible?: boolean; +} + +export interface IGroupFooterProps { + /** Callback for when the "Show All" link in group footer is clicked */ + onToggleSummarize?: (group: IGroup) => void; + + /** Text to display for the group footer show all link. */ + showAllLinkText?: string; +} + diff --git a/src/components/GroupedList/GroupedList.scss b/src/components/GroupedList/GroupedList.scss new file mode 100644 index 00000000000000..5585b5edd40fdd --- /dev/null +++ b/src/components/GroupedList/GroupedList.scss @@ -0,0 +1,42 @@ +@import '../../common/common'; + +.ms-GroupedList { + position: relative; + font-size: $ms-font-size-s; + + BUTTON { + font-family: inherit; + background-color: transparent; + } + + > .ms-FocusZone { + display: inline-block; + vertical-align: top; + min-width: 1px; + min-height: 1px; + } + +} + +.ms-GroupedList.is-horizontalConstrained { + overflow-x: auto; + overflow-y: visible; + + -webkit-overflow-scrolling: touch; + transform: translateZ(0); +} + +.ms-GroupedList-cell { + word-break: break-word; +} + +.ms-GroupedList-group { + &.is-dropping { + background-color: $ms-color-neutralLight; + } +} + +/* Set the min height for a row to 38px so even rendering empty cells takes that space. */ +.ms-GroupedList .ms-List-cell { + min-height: 38px; +} \ No newline at end of file diff --git a/src/components/GroupedList/GroupedList.tsx b/src/components/GroupedList/GroupedList.tsx new file mode 100644 index 00000000000000..0e1b17fc304841 --- /dev/null +++ b/src/components/GroupedList/GroupedList.tsx @@ -0,0 +1,283 @@ +import * as React from 'react'; +import { + IGroupedList, + IGroupedListProps, + IGroup, + IGroupFooterProps, + IGroupHeaderProps +} from './GroupedList.Props'; +import { + Group +} from './Group'; + +import { css } from '../../utilities/css'; +import { + List +} from '../../List'; +import { + SelectionMode +} from '../../utilities/selection/index'; + +import { assign } from '../../utilities/object'; +import './GroupedList.scss'; + +export interface IGroupedListState { + lastWidth?: number; + lastSelectionMode?: SelectionMode; + groups?: IGroup[]; +} + +export class GroupedList extends React.Component implements IGroupedList { + public static defaultProps = { + selectionMode: SelectionMode.multiple, + isHeaderVisible: true + }; + + public refs: { + [key: string]: React.ReactInstance, + root: HTMLElement, + list: List + }; + + constructor(props: IGroupedListProps) { + super(props); + + this._onToggleCollapse = this._onToggleCollapse.bind(this); + this._onToggleSelectGroup = this._onToggleSelectGroup.bind(this); + this._onToggleSummarize = this._onToggleSummarize.bind(this); + this._getGroupKey = this._getGroupKey.bind(this); + this._renderGroup = this._renderGroup.bind(this); + + this.state = { + lastWidth: 0, + groups: props.groups + }; + } + + public componentWillReceiveProps(newProps) { + let { + groups, + selectionMode + } = this.props; + let shouldForceUpdates = false; + + if (newProps.groups !== groups) { + this.setState({ groups: newProps.groups }); + shouldForceUpdates = true; + } + + if (newProps.selectionMode !== selectionMode) { + shouldForceUpdates = true; + } + + if (shouldForceUpdates) { + this._forceListUpdates(); + } + } + + public render() { + let { + className + } = this.props; + let { + groups + } = this.state; + + return ( +
+ { !groups ? + this._renderGroup(null, 0) : ( + 1 } + /> + ) + } +
+ ); + } + + public forceUpdate() { + super.forceUpdate(); + this._forceListUpdates(); + } + + public toggleCollapseAll(allCollapsed: boolean) { + let { groups } = this.state; + let { groupProps } = this.props; + let onToggleCollapseAll = groupProps && groupProps.onToggleCollapseAll; + + if (groups) { + if (onToggleCollapseAll) { + onToggleCollapseAll(allCollapsed); + } + + for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) { + groups[groupIndex].isCollapsed = allCollapsed; + } + + this.forceUpdate(); + } + } + + private _renderGroup(group, groupIndex) { + let { + dragDropEvents, + dragDropHelper, + eventsToRegister, + groupProps, + items, + listProps, + onRenderCell, + selectionMode, + selection, + viewport + } = this.props; + + // override group header/footer props as needed + let headerProps = assign({}, groupProps && groupProps.headerProps ? groupProps.headerProps : {}, { + onToggleCollapse: this._onToggleCollapse, + onToggleSelectGroup: this._onToggleSelectGroup + }) as IGroupHeaderProps; + let footerProps = assign({}, groupProps && groupProps.footerProps ? groupProps.footerProps : {}, { + onToggleSummarize: this._onToggleSummarize + }) as IGroupFooterProps; + let groupNestingDepth = this._getGroupNestingDepth(); + + return (!group || group.count > 0) ? ( + + ) : null; + } + + private _getGroupKey(group: IGroup): string { + return 'group-' + (group ? + group.key + '-' + group.count : + ''); + } + + private _getGroupNestingDepth(): number { + let { groups } = this.state; + let level = 0; + let groupsInLevel = groups; + + while (groupsInLevel && groupsInLevel.length > 0) { + level++; + groupsInLevel = groupsInLevel[0].children; + } + + return level; + } + + private _onToggleCollapse(group: IGroup) { + let { groupProps } = this.props; + let onToggleCollapse = groupProps && groupProps.headerProps && groupProps.headerProps.onToggleCollapse; + + if (group) { + if (onToggleCollapse) { + onToggleCollapse(group); + } + + group.isCollapsed = !group.isCollapsed; + this.setState({ }, this.forceUpdate); + } + } + + private _onToggleSelectGroup(group: IGroup) { + let { groups } = this.state; + + if (group) { + let isSelected = !group.isSelected; + this._selectGroup(group, isSelected); + + this.setState({ + groups: groups + }); + } + } + + private _selectGroup(group: IGroup, isSelected: boolean) { + let { groupProps } = this.props; + + group.isSelected = isSelected; + if (group.children && group.children.length > 0) { + group.children.forEach((childGroup: IGroup) => { + this._selectGroup(childGroup, isSelected); + }); + } else { + let getGroupItemLimit = groupProps && groupProps.getGroupItemLimit; + let groupItemLimit = getGroupItemLimit ? getGroupItemLimit(group) : Infinity; + let start = group.startIndex; + let end = group.startIndex + Math.min(group.count, groupItemLimit); + for (let idx = start; idx < end; idx++) { + this.props.selection.setIndexSelected(idx, isSelected, false /* shouldAnchor */); + } + this.setState({ }, this.forceUpdate); + } + } + + private _forceListUpdates(groups?: IGroup[]) { + groups = groups || this.state.groups; + + let groupCount = groups ? groups.length : 1; + + if (this.refs.list) { + this.refs.list.forceUpdate(); + + for (let i = 0; i < groupCount; i++) { + let group = this.refs.list.refs['group_' + String(i)] as Group; + if (group) { + group.forceListUpdate(); + } + } + } else { + let group = this.refs['group_' + String(0)] as Group; + if (group) { + group.forceListUpdate(); + } + } + } + + private _onToggleSummarize(group: IGroup) { + let { groups } = this.state; + let { groupProps } = this.props; + let onToggleSummarize = groupProps && groupProps.footerProps && groupProps.footerProps.onToggleSummarize; + + if (onToggleSummarize) { + onToggleSummarize(group); + } else { + if (group) { + group.isShowingAll = !group.isShowingAll; + + this.setState({ + groups: groups + }); + } + } + } +} diff --git a/src/components/GroupedList/index.ts b/src/components/GroupedList/index.ts new file mode 100644 index 00000000000000..0f160bac47c2d3 --- /dev/null +++ b/src/components/GroupedList/index.ts @@ -0,0 +1,3 @@ +export * from './GroupedList'; +export * from './GroupedList.Props'; +export * from './Group'; \ No newline at end of file diff --git a/src/components/Label/Label.scss b/src/components/Label/Label.scss index fa7a06fd75ead2..3f56d1115ebee1 100644 --- a/src/components/Label/Label.scss +++ b/src/components/Label/Label.scss @@ -1,2 +1,50 @@ @import '../../common/common'; -@import "~office-ui-fabric/src/components/Label/Label"; + +// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. + +// +// Office UI Fabric +// -------------------------------------------------- +// Form field label styles + +@mixin ms-Label-is-disabled { + color: $ms-color-neutralTertiary; + + @media screen and (-ms-high-contrast: active) { + color: $ms-color-contrastBlackDisabled; + } + + @media screen and (-ms-high-contrast: black-on-white) { + color: $ms-color-contrastWhiteDisabled; + } +} + +@mixin ms-Label-is-required { + &:after { + content: ' *'; + color: $ms-color-error; + } +} + + +.ms-Label { + @include ms-font-s; + @include ms-u-normalize; + box-sizing: border-box; + display: block; + padding: 5px 0; + + &.is-required { + @include ms-Label-is-required; + } + + &.is-disabled { + @include ms-Label-is-disabled; + } +} + +.is-disabled { + .ms-Label { + @include ms-Label-is-disabled; + } +} diff --git a/src/components/Link/Link.scss b/src/components/Link/Link.scss index c0c7b94d13a8f7..def5dffe1fef25 100644 --- a/src/components/Link/Link.scss +++ b/src/components/Link/Link.scss @@ -1,5 +1,41 @@ @import '../../common/common'; -@import "~office-ui-fabric/src/components/Link/Link"; + +// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. + +// +// Office UI Fabric +// -------------------------------------------------- +// Link (anchor) styles + + +@mixin ms-Link { + color: $ms-color-themePrimary; + text-decoration: none; + cursor: pointer; + + &:hover, + &:focus { + color: $ms-color-themeDarker; + } + + &:active { + color: $ms-color-themePrimary; + } + + @media screen and (-ms-high-contrast: active) { + color: $ms-color-contrastBlackLink; + } + + @media screen and (-ms-high-contrast: black-on-white) { + color: $ms-color-contrastWhiteLink; + } +} + +.ms-Link { + @include ms-Link; +} + +// TODO: Remove overrides below. .ms-Link, BUTTON.ms-Link { diff --git a/src/components/List/List.Props.ts b/src/components/List/List.Props.ts index e202983b553d48..79b22fcd402e7c 100644 --- a/src/components/List/List.Props.ts +++ b/src/components/List/List.Props.ts @@ -1,5 +1,4 @@ import * as React from 'react'; -import { ISelection } from '../../utilities/selection/interfaces'; import { List } from './List'; export interface IListProps extends React.Props { @@ -52,9 +51,6 @@ export interface IListProps extends React.Props { /** Number of items to render. Defaults to items.length. */ renderCount?: number; - - /** Optional selection model to track selection state. */ - selection?: ISelection; } export interface IPage { diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx index 37a25b47e24686..56bcf09e689c1a 100644 --- a/src/components/List/List.tsx +++ b/src/components/List/List.tsx @@ -80,6 +80,7 @@ export class List extends BaseComponent { private _scrollElement: HTMLElement; private _scrollingToIndex: number; private _hasCompletedFirstRender: boolean; + private isFirstRenderRectUpdate: boolean; // surface rect relative to window private _surfaceRect: ClientRect; @@ -110,6 +111,7 @@ export class List extends BaseComponent { this._totalEstimates = 0; this._requiredWindowsAhead = 0; this._requiredWindowsBehind = 0; + this.isFirstRenderRectUpdate = true; // Track the measure version for everything. this._measureVersion = 0; @@ -567,7 +569,9 @@ export class List extends BaseComponent { } private _getItemCountForPage(itemIndex: number, visibileRect: ClientRect): number { - return this.props.getItemCountForPage ? this.props.getItemCountForPage(itemIndex, visibileRect) : DEFAULT_ITEMS_PER_PAGE; + let itemsPerPage = this.props.getItemCountForPage ? this.props.getItemCountForPage(itemIndex, visibileRect) : DEFAULT_ITEMS_PER_PAGE; + + return itemsPerPage ? itemsPerPage : DEFAULT_ITEMS_PER_PAGE; } private _createPage(pageKey: string, items: any[], startIndex?: number, count?: number, style?: any): IPage { @@ -613,13 +617,16 @@ export class List extends BaseComponent { surfaceRect = this._surfaceRect = _measureSurfaceRect(this.refs.surface); } - // If the surface is above the container top or below the container bottom, return empty rect. - if ( - surfaceRect.bottom < 0 || - surfaceRect.top > window.innerHeight) { + // If the surface is above the container top or below the container bottom, or if this is not the first + // render return empty rect. + // The first time the list gets rendered we need to calculate the rectangle. The width of the list is + // used to calculate the width of the list items. + if ( (surfaceRect.bottom < 0 || + surfaceRect.top > window.innerHeight) && !this.isFirstRenderRectUpdate) { this._requiredRect = EMPTY_RECT; this._allowedRect = EMPTY_RECT; } else { + this.isFirstRenderRectUpdate = false; const visibleTop = Math.max(0, -surfaceRect.top); const visibleRect = { top: visibleTop, diff --git a/src/components/MessageBar/MessageBar.scss b/src/components/MessageBar/MessageBar.scss index 156aee7e3e7e1f..48d98d62c145bf 100644 --- a/src/components/MessageBar/MessageBar.scss +++ b/src/components/MessageBar/MessageBar.scss @@ -1,5 +1,112 @@ @import '../../common/common'; -@import "~office-ui-fabric/src/components/MessageBar/MessageBar"; + +// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. + +// +// Office UI Fabric +// -------------------------------------------------- +// MessageBar Styles + +/* + Base template file +*/ + +$MessageBar-padding: 8px; + +.ms-MessageBar { + padding: $MessageBar-padding; + display: table; + @include ms-bgColor-info; +} + +.ms-MessageBar-icon, +.ms-MessageBar-text { + display: table-cell; + vertical-align: top; +} + +.ms-MessageBar-icon { + padding-right: $MessageBar-padding; + font-size: 16px; + @include ms-fontColor-neutralSecondaryAlt; +} + +.ms-MessageBar-text { + @include ms-font-s; +} + + +//== Modifier: Warning message +// +.ms-MessageBar.ms-MessageBar--warning { + @include ms-bgColor-warning; +} + + +//== Modifier: Severe Warning message +// +.ms-MessageBar.ms-MessageBar--severeWarning { + @include ms-bgColor-severeWarning; + + .ms-MessageBar-icon { + @include ms-fontColor-severeWarning; + } +} + + +//== Modifier: Error message +// +.ms-MessageBar.ms-MessageBar--error { + @include ms-bgColor-error; + + .ms-MessageBar-icon { + @include ms-fontColor-error; + } +} + + +//== Modifier: Remove message +// +.ms-MessageBar.ms-MessageBar--remove { + @include ms-bgColor-error; + + .ms-MessageBar-icon { + @include ms-fontColor-error; + } + + .ms-Icon { + font-size: 8px; + margin-left: 3px; + } +} + + +//== Modifier: Success message +// +.ms-MessageBar.ms-MessageBar--success { + @include ms-bgColor-success; + + .ms-MessageBar-icon { + @include ms-fontColor-green; + } + + .ms-Icon { + font-size: $ms-font-size-s; + top: 3px; + + &:before { + margin-left: 1px; + } + + &:after { + font-size: 8px; + margin-left: 3px; + top: 1px; + } + } +} + +// TODO: Remove overrides below. // Shared .ms-MessageBar-icon { diff --git a/src/components/Nav/Nav.Props.ts b/src/components/Nav/Nav.Props.ts index 33febb0c728c10..c925f974b2f6de 100644 --- a/src/components/Nav/Nav.Props.ts +++ b/src/components/Nav/Nav.Props.ts @@ -81,4 +81,14 @@ export interface INavLink { * Any additional properties to apply to the rendered links. */ [propertyName: string]: any; + + /** + * (Optional) Aria label for nav link + */ + ariaLabel?: string; + + /** + * (Optional) Title for nav link + */ + title?: string; } \ No newline at end of file diff --git a/src/components/Nav/Nav.tsx b/src/components/Nav/Nav.tsx index 2941e036d999bd..f314ef23cd5885 100644 --- a/src/components/Nav/Nav.tsx +++ b/src/components/Nav/Nav.tsx @@ -44,13 +44,15 @@ export class Nav extends React.Component { } private _renderLink(link: INavLink, linkIndex: number): React.ReactElement<{}> { + let { onLinkClick } = this.props; return (
  • { (link.iconClassName ? diff --git a/src/components/Overlay/Overlay.scss b/src/components/Overlay/Overlay.scss index f6f3469b6caf09..445f6baee4fbfe 100644 --- a/src/components/Overlay/Overlay.scss +++ b/src/components/Overlay/Overlay.scss @@ -1,2 +1,30 @@ @import '../../common/common'; -@import '~office-ui-fabric/src/components/Overlay/Overlay'; + +// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. + +// +// Office UI Fabric +// -------------------------------------------------- +// Modal overlay styles + +.ms-Overlay { + background-color: $ms-color-whiteTranslucent40; + position: absolute; + bottom: 0; + left: 0; + right: 0; + top: 0; + z-index: $ms-zIndex-Overlay; + + //== Modifier: Hidden overlay + // + &.ms-Overlay--none { + visibility: hidden; + } + + //== Modifier: Dark overlay + // + &.ms-Overlay--dark { + background-color: $ms-color-blackTranslucent40; + } +} diff --git a/src/components/PeoplePicker/PeoplePicker.scss b/src/components/PeoplePicker/PeoplePicker.scss index 46eaf9033655c4..1cbf4d85879635 100644 --- a/src/components/PeoplePicker/PeoplePicker.scss +++ b/src/components/PeoplePicker/PeoplePicker.scss @@ -15,7 +15,643 @@ } } -@import "~office-ui-fabric/src/components/PeoplePicker/PeoplePicker"; +// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. + +// +// Office UI Fabric +// -------------------------------------------------- +// People Picker styles + +$personaItemHeight: 42px; + + +.ms-PeoplePicker { + @include ms-font-m; + @include ms-u-normalize; + background-color: $ms-color-white; + margin-bottom: 10px; +} + +// Box that contains the search field and selected people. +.ms-PeoplePicker-searchBox { + @include ms-u-clearfix; + border: 1px solid $ms-color-neutralTertiaryAlt; + box-sizing: border-box; + min-height: 40px; + width: 100%; + + &:hover { + border-color: $ms-color-neutralSecondaryAlt; + } +} + +// Highlight the search box when the people picker is active +.ms-PeoplePicker.is-active .ms-PeoplePicker-searchBox { + border-color: $ms-color-themePrimary; +} + +// The search field. +.ms-PeoplePicker-searchField { + border: 0; + box-sizing: border-box; + display: inline-block; + float: left; + height: 38px; + outline: none; + padding-left: 8px; + width: 100%; +} + +// A selected persona, which appears within the search field. +.ms-PeoplePicker-persona { + display: inline-block; + float: left; + margin: 4px; + outline: 1px solid transparent; + + //TODO: Avoid styling child components like this. + .ms-Persona { + background-color: $ms-color-neutralLighter; + float: left; + min-height: 30px; + } +} + +// The selected persona may be in an error state. +.ms-PeoplePicker-persona.has-error { + .ms-Persona-primaryText { + color: $ms-color-error; + } +} + +// Button to remove a selected person. +.ms-PeoplePicker-personaRemove { + @include button-reset(); + background-color: $ms-color-neutralLighter; + color: $ms-color-neutralSecondary; + display: inline-block; + float: left; + text-align: center; + height: 32px; + width: 32px; + + &:hover { + background-color: $ms-color-neutralLight; + color: $ms-color-neutralPrimary; + cursor: pointer; + } + + &:focus { + background-color: $ms-color-neutralLight; + color: $ms-color-neutralPrimary; + border: 1px solid $ms-color-themePrimary; + outline: none; + } +} + +// Results area, hidden by default +.ms-PeoplePicker-results { + @include drop-shadow(); + background-color: $ms-color-white; + border: 1px solid $ms-color-neutralTertiaryAlt; + display: none; + margin-bottom: -1px; + max-width: 340px; + padding-top: 9px; + position: absolute; + z-index: ($ms-zIndex-PeoplePicker + $ms-zIndex-middle); +} + +// Show the results area when the people picker is active +.ms-PeoplePicker.is-active .ms-PeoplePicker-results { + display: block; + opacity: 1; +} + + +// One or more groups of results (ms-PeoplePicker-resultGroup) are contained in this scrollable area. +// This is limited to five results for both regular and compact sizes. +.ms-PeoplePicker-resultGroups { + max-height: 309px; + overflow-y: scroll; +} + +// A group of results +.ms-PeoplePicker-resultGroup { + border-top: 1px solid $ms-color-neutralLight; + + // The first result group needs to be bumped up 1px to account for border on ms-PeoplePicker-results + &:first-child { + border-top: 0; + } +} + +// Title for a group of results (optional) +.ms-PeoplePicker-resultGroupTitle { + color: $ms-color-themePrimary; + font-family: $ms-font-family-semilight; + font-size: $ms-font-size-s; + padding: 17px 0 0 12px; + text-transform: uppercase; + height: 40px; +} + +// List of results +.ms-PeoplePicker-resultList { + @include ms-u-normalize; + margin-bottom: -1px; + list-style-type: none; // Browser default override. +} + +// A single result in the result list +.ms-PeoplePicker-result { + position: relative; + + .ms-Persona { + &:hover { + @extend .ms-Persona.ms-Persona--darkText; + cursor: pointer; + } + + // TODO: Active style is being blocked by the inner content on IE + // http://stackoverflow.com/questions/5594102/active-css-selector-not-working-for-ie8-and-ie9?rq=1 + &:active { + background-color: $ms-color-themeLight; + } + } + + // Ensure the width is 100%. + .ms-Persona-details { + width: 100%; + } +} + +// Result buttons +.ms-PeoplePicker-resultBtn, +.ms-PeoplePicker-peopleListBtn { + @include button-reset(); + position: relative; + box-sizing: border-box; + height: 34px; + width: 100%; + background: none; + border: 0; + text-align: left; + margin: 0 0 10px 0; + padding: 0 0 0 9px; + + @media (min-width: $ms-screen-md-min) { + height: 48px; + } + + &:hover { + background-color: $ms-color-neutralLight; + outline: 1px solid transparent; + } + + &:focus { + outline: 1; + } + + &.ms-PeoplePicker-resultBtn--compact { + height: 32px; + } +} + +.ms-PeoplePicker-peopleListBtn { + margin-bottom: 0; + padding: 0; + + &:hover { + background-color: transparent; + } +} + +// Actionable icon on a result +.ms-PeoplePicker-resultAction { + @include button-reset(); + display: block; + height: 34px; + transition: background-color 0.367s $ms-ease1; + position: absolute; + right: 0; + top: 0; + width: 30px; + text-align: center; + + @media (min-width: $ms-screen-md-min) { + height: 48px; + } + + .ms-Icon { + color: $ms-color-neutralSecondary; + font-size: $ms-font-size-m-plus; + } + + &:hover { + background-color: $ms-color-neutralTertiaryAlt; + outline: 1px solid transparent; + } + + &:active { + background-color: $ms-color-themeTertiary; + } +} + +// A result can contain additional content (usually a ms-PeoplePicker-resultList of Persona components) that is hidden initially +.ms-PeoplePicker-resultAdditionalContent { + display: none; +} + +// Use the .is-expanded state to reveal the additional content +.ms-PeoplePicker-result.is-expanded { + background-color: $ms-color-neutralLighter; + margin-bottom: 11px; + + // Switch the toggle icon + .ms-PeoplePicker-resultAction .ms-Icon { + transform: rotate(180deg); + } + + // Show the content + .ms-PeoplePicker-resultAdditionalContent { + display: block; + } +} + +// After the result groups we have an area to trigger additional searches +.ms-PeoplePicker-searchMore { + border-top: 1px solid $ms-color-neutralLight; + height: 69px; + position: relative; + overflow: hidden; + + .ms-Spinner { + position: absolute; + width: 32px; + height: 32px; + top: 20px; + left: 20px; + display: none; + + .ms-Spinner-circle { + background-color: $ms-color-themePrimary; + } + } +} + +// Searching state +.ms-PeoplePicker-searchMore.is-searching { + + .ms-Spinner { + display: block; + } + + .ms-PeoplePicker-searchMoreIcon { + .ms-Icon { + display: none; + } + } + + .ms-PeoplePicker-searchMorePrimary { + color: $ms-color-themePrimary; + } + + &:hover { + background-color: transparent; + cursor: default; + } +} + +.ms-PeoplePicker-searchMoreBtn { + @include button-reset(); + position: relative; + height: 69px; + width: 100%; + padding: 0; + margin: 0; + padding-left: 70px; + text-align: left; + + &:hover { + background-color: $ms-color-neutralLight; + cursor: pointer; + } + + // TODO: Works in Chrome, but not working in IE + &:focus, + &:active { + background-color: $ms-color-themeLight; + } +} + +.ms-PeoplePicker-searchMoreBtn.ms-PeoplePicker-searchMoreBtn--compact { + height: 49px; + padding-left: 50px; +} + +// Default search icon +.ms-PeoplePicker-searchMoreIcon { + height: 70px; + position: absolute; + top: 0; + left: 0; + width: 70px; + + .ms-Icon { + color: $ms-color-neutralPrimary; + font-size: $ms-font-size-m + 2; + position: absolute; + text-align: center; + top: 27px; + width: 100%; + } +} + +// Primary text +.ms-PeoplePicker-searchMorePrimary { + padding-top: 2px; + font-family: $ms-font-family-regular; +} + +// Secondary text +.ms-PeoplePicker-searchMoreSecondary { + font-family: $ms-font-family-semilight; + font-size: $ms-font-size-xs; + color: $ms-color-neutralSecondary; +} + +// The search more area may be in a disconnected state. +.ms-PeoplePicker-searchMore.ms-PeoplePicker-searchMore--disconnected { + + // Do nothing on hover + &:hover { + background-color: inherit; + cursor: default; + } + + // Alert icon + .ms-PeoplePicker-searchMoreIcon .ms-Icon { + color: $ms-color-neutralSecondary; + } + + // Primary text + .ms-PeoplePicker-searchMorePrimary { + color: $ms-color-neutralSecondary; + font-family: $ms-font-family-semilight; + font-size: $ms-font-size-xs; + line-height: 20px; + position: relative; + top: 12px; + } +} + +// Compact size +.ms-PeoplePicker.ms-PeoplePicker--compact { + + // Limit to 5 results before scrolling. + .ms-PeoplePicker-resultGroups { + max-height: 209px; + } + + .ms-PeoplePicker-resultAction { + height: 32px; + + .ms-Icon { + margin-top: -8px; + } + } + + .ms-PeoplePicker-searchMore { + height: 49px; + + .ms-Spinner { + width: 28px; + height: 28px; + top: 12px; + left: 12px; + } + + } + + .ms-PeoplePicker-searchMore.is-searching .ms-PeoplePicker-searchMoreIcon { + background-size: 16px; + } + + .ms-PeoplePicker-searchMoreIcon { + height: 50px; + width: 50px; + + .ms-Icon { + font-size: $ms-font-size-l; + top: 0; + margin-top: 0; + line-height: 50px; + } + } + + .ms-PeoplePicker-searchMorePrimary { + font-size: $ms-font-size-s; + line-height: 45px; + } + + .ms-PeoplePicker-searchMoreSecondary { + display: none; + } +} + + +//== Modifier: Facepile and Members list +// +.ms-PeoplePicker.ms-PeoplePicker--Facepile, +.ms-PeoplePicker.ms-PeoplePicker--membersList { + .ms-PeoplePicker-searchBox { + height: 30px; + min-height: 30px; + } + + .ms-PeoplePicker-searchField { + height: 28px; + } + + .ms-Persona { + cursor: pointer; + } +} + +.ms-PeoplePicker-selected { + margin-bottom: 20px; + display: none; + + &.is-active { + display: block; + } +} + +.ms-PeoplePicker.ms-PeoplePicker--Facepile { + //= State: Searching in peoplepicker search field + &.is-searching { + .ms-PeoplePicker-results { + border-bottom: 0; + padding: 20px 0 0; + } + + .ms-PeoplePicker-peopleListHeader { + display: none; + } + } + + .ms-PeoplePicker-results { + position: relative; + border: 0; + box-shadow: none; + margin: 0; + max-width: 100%; + padding: 0; + padding-bottom: 10px; + border-bottom: 1px solid $ms-color-neutralLight; + } + + // Personas are size xs on mobile, sm on md screens and above + .ms-PeoplePicker-results, + .ms-PeoplePicker-selectedPeople { + @media (max-width: $ms-screen-sm-max) { + .ms-Persona-imageArea, + .ms-Persona-image { + width: 32px; + height: 32px; + } + + .ms-Persona-placeholder { + font-size: 28px; + top: 6px; + } + + .ms-Persona-initials { + font-size: $ms-font-size-s; + line-height: 32px; + } + + .ms-Persona-presence { + left: 19px; + } + + .ms-Persona-details { + padding-left: 8px; + } + + .ms-Persona-primaryText { + font-size: $ms-font-size-m; + padding-top: 3px; + } + + .ms-Persona-secondaryText { + display: none; + } + } + + @media (min-width: $ms-screen-md-min) { + .ms-Persona .ms-Persona-secondaryText { + display: block; + } + } + } + + .ms-PeoplePicker-resultBtn, + .ms-PeoplePicker-peopleListBtn { + @media (min-width: $ms-screen-md-min) { + height: $personaItemHeight; + } + } + + .ms-PeoplePicker-resultAction { + @media (min-width: $ms-screen-md-min) { + height: $personaItemHeight; + } + } + + .ms-Persona.ms-Persona--selectable { + padding: 0; + } + + .ms-PeoplePicker-searchMore { + display: none; + + &.is-active { + display: block; + } + } + + .ms-PeoplePicker-searchMore, + .ms-PeoplePicker-searchMoreBtn, + .ms-PeoplePicker-searchMoreIcon { + height: 48px; + } + + .ms-PeoplePicker-searchMoreBtn { + padding-left: 48px; + } + + .ms-PeoplePicker-searchMoreIcon { + width: 48px; + } + + .ms-PeoplePicker-searchMorePrimary { + font-size: $ms-font-size-s; + line-height: 48px; + } + + .ms-PeoplePicker-searchMoreIcon .ms-Icon { + top: 0; + line-height: 48px; + } + + .ms-Spinner { + top: 16px; + left: 14px; + height: 20px; + width: 20px; + } +} + +.ms-PeoplePicker.ms-PeoplePicker--Facepile { + .ms-PersonaCard { + display: none; + position: absolute; + height: 200px; + + &.is-active { + display: block; + } + } +} + +.ms-PeoplePicker-selectedHeader, +.ms-PeoplePicker-peopleListHeader { + color: $ms-color-themePrimary; + font-size: $ms-font-size-s; + font-family: $ms-font-family-regular; + height: 50px; + line-height: 50px; +} + +.ms-PeoplePicker-selectedPeople, +.ms-PeoplePicker-peopleList { + @include ms-u-normalize; + list-style: none; +} + +.ms-PeoplePicker-selectedPerson { + margin-bottom: 8px; + position: relative; +} + +.ms-PeoplePicker-peopleListItem { + margin-bottom: 6px; + position: relative; +} + +// TODO: remove overides below. .ms-PeoplePicker-searchBox { min-height: 42px; // this is accounting for the border. Actual inner height is 40px; diff --git a/src/components/Persona/Persona.scss b/src/components/Persona/Persona.scss index 3494001641955b..2c1c9530766efc 100644 --- a/src/components/Persona/Persona.scss +++ b/src/components/Persona/Persona.scss @@ -1,5 +1,602 @@ @import '../../common/common'; -@import "~office-ui-fabric/src/components/Persona/Persona"; + +// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. + +// +// Office UI Fabric +// -------------------------------------------------- +// Persona styles + + +//= Colors used in the initials color block +$ms-color-initials-lightBlue: #6ba5e7; +$ms-color-initials-blue: #2d89ef; +$ms-color-initials-darkBlue: #2b5797; +$ms-color-initials-teal: #00aba9; +$ms-color-initials-lightGreen: #99b433; +$ms-color-initials-green: #00a300; +$ms-color-initials-darkGreen: #1e7145; +$ms-color-initials-lightPink: #e773bd; +$ms-color-initials-pink: #ff0097; +$ms-color-initials-magenta: #7e3878; +$ms-color-initials-purple: #603cba; +$ms-color-initials-black: #1d1d1d; +$ms-color-initials-orange: #da532c; +$ms-color-initials-red: #ee1111; +$ms-color-initials-darkRed: #b91d47; + + +.ms-Persona { + @include ms-font-m; + @include ms-u-normalize; + display: table; + line-height: 1; + position: relative; +} + +.ms-Persona-imageArea { + position: relative; + display: block; + overflow: hidden; + text-align: center; + width: 48px; + height: 48px; + border-radius: 50%; + z-index: $ms-zIndex-back; + + @media screen and (-ms-high-contrast: active) { + border: 1px solid $ms-color-white; + } + + @media screen and (-ms-high-contrast: black-on-white) { + border: 1px solid $ms-color-black; + } +} + +//= Note: The doughboy placeholder is being deprecated. +// The initials color block (.ms-Persona-initials) will be used going forward +// as a fallback when the persona does not have an image. +.ms-Persona-placeholder { + color: $ms-color-white; + position: absolute; + right: 0; + left: 0; + font-size: 47px; + top: 9px; +} + +.ms-Persona-initials { + color: $ms-color-white; + font-size: $ms-font-size-l; + font-family: $ms-font-family-light; + line-height: 48px; + + &.ms-Persona-initials--lightBlue { + background-color: $ms-color-initials-lightBlue; + } + &.ms-Persona-initials--blue { + background-color: $ms-color-initials-blue; + } + &.ms-Persona-initials--darkBlue { + background-color: $ms-color-initials-darkBlue; + } + &.ms-Persona-initials--teal { + background-color: $ms-color-initials-teal; + } + &.ms-Persona-initials--lightGreen { + background-color: $ms-color-initials-lightGreen; + } + &.ms-Persona-initials--green { + background-color: $ms-color-initials-green; + } + &.ms-Persona-initials--darkGreen { + background-color: $ms-color-initials-darkGreen; + } + &.ms-Persona-initials--lightPink { + background-color: $ms-color-initials-lightPink; + } + &.ms-Persona-initials--pink { + background-color: $ms-color-initials-pink; + } + &.ms-Persona-initials--magenta { + background-color: $ms-color-initials-magenta; + } + &.ms-Persona-initials--purple { + background-color: $ms-color-initials-purple; + } + &.ms-Persona-initials--black { + background-color: $ms-color-initials-black; + } + &.ms-Persona-initials--orange { + background-color: $ms-color-initials-orange; + } + &.ms-Persona-initials--red { + background-color: $ms-color-initials-red; + } + &.ms-Persona-initials--darkRed { + background-color: $ms-color-initials-darkRed; + } +} + +.ms-Persona-image { + display: table-cell; + margin-right: 10px; + position: absolute; + top: 0; + left: 0; + width: 48px; + height: 48px; +} + +.ms-Persona-image[src=""] { + display: none; +} + +.ms-Persona-presence { + background-color: $ms-color-presence-available; + position: absolute; + height: 12px; + width: 12px; + border-radius: 50%; + top: auto; + left: 34px; + bottom: -1px; + border: 2px solid $ms-color-white; +} + +.ms-Persona-details { + display: table-cell; + padding: 0 12px; + vertical-align: middle; + overflow: hidden; +} + +.ms-Persona-primaryText, +.ms-Persona-secondaryText, +.ms-Persona-tertiaryText, +.ms-Persona-optionalText { + @include noWrap(); + width: 190px; + overflow: hidden; + text-overflow: ellipsis; +} + +.ms-Persona-primaryText { + color: $ms-color-neutralPrimary; + font-family: $ms-font-family-regular; + font-size: $ms-font-size-l; + margin-top: -3px; + line-height: 1.4; +} + +.ms-Persona-secondaryText, +.ms-Persona-tertiaryText, +.ms-Persona-optionalText { + color: $ms-color-neutralSecondary; + font-family: $ms-font-family-regular; + font-size: $ms-font-size-s; + white-space: nowrap; + line-height: 1.3; +} + +.ms-Persona-secondaryText { + padding-top: 3px; +} + +.ms-Persona-tertiaryText, +.ms-Persona-optionalText { + padding-top: 5px; + display: none; // Hidden on default persona +} + + +//== Modifier: Persona with square images +// +.ms-Persona.ms-Persona--square { + .ms-Persona-imageArea { + background-color: $ms-color-neutralTertiary; + border-radius: 0; + } + + .ms-Persona-presence { + top: 0; + left: 0; + bottom: auto; + right: auto; + height: 48px; + width: 5px; + border-radius: 0; + border: 0; + + @media screen and (-ms-high-contrast: active) { + border: 1px solid $ms-color-white; + } + + @media screen and (-ms-high-contrast: black-on-white) { + border: 1px solid $ms-color-black; + } + } +} + + +//== Modifier: Tiny Persona +// +.ms-Persona.ms-Persona--tiny { + height: 30px; + display: inline-block; + + .ms-Persona-imageArea { + overflow: visible; + background: transparent; + height: 0; + width: 0; + } + + .ms-Persona-presence { + right: auto; + top: 10px; + left: 0; + border: 0; + + @media screen and (-ms-high-contrast: active) { + top: 9px; + border: 1px solid $ms-color-white; + } + + @media screen and (-ms-high-contrast: black-on-white) { + border: 1px solid $ms-color-black; + } + } + + .ms-Persona-details { + padding-left: 20px; + } + + .ms-Persona-primaryText { + font-size: $ms-font-size-m; + padding-top: 9px; + } + + .ms-Persona-secondaryText { + display: none; + } +} + + +//== Modifier: Tiny Persona with read only state +// +// This variant includes a semicolon, and is +// most often presented within a People Picker. +.ms-Persona.ms-Persona--tiny.ms-Persona--readonly { + padding: 0; + background-color: transparent; + + .ms-Persona-primaryText:after { + content: ';'; + } +} + + +//== Modifier: Tiny Square Persona +// +.ms-Persona.ms-Persona--square.ms-Persona--tiny { + .ms-Persona-presence { + height: 12px; + width: 12px; + top: 10px; + } +} + + +//== Modifier: Extra Small Persona +// +.ms-Persona.ms-Persona--xs { + .ms-Persona-imageArea, + .ms-Persona-image { + width: 32px; + height: 32px; + } + + .ms-Persona-placeholder { + font-size: 28px; + top: 6px; + } + + .ms-Persona-initials { + font-size: $ms-font-size-s; + line-height: 32px; + } + + .ms-Persona-presence { + left: 19px; + } + + .ms-Persona-details { + padding-left: 8px; + } + + .ms-Persona-primaryText { + font-size: $ms-font-size-m; + padding-top: 3px; + } + + .ms-Persona-secondaryText { + display: none; + } +} + + +//== Modifier: Extra Small Square Persona +// +.ms-Persona.ms-Persona--square.ms-Persona--xs { + .ms-Persona-presence { + height: 32px; + width: 4px; + left: 0; + } +} + + +//== Modifier: Small Persona +// +.ms-Persona.ms-Persona--sm { + .ms-Persona-imageArea, + .ms-Persona-image { + width: 40px; + height: 40px; + } + + .ms-Persona-placeholder { + font-size: 38px; + top: 5px; + } + + .ms-Persona-initials { + font-size: $ms-font-size-m; + line-height: 40px; + } + + .ms-Persona-presence { + left: 27px; + } + + .ms-Persona-details { + padding-left: 8px; + } + + .ms-Persona-primaryText { + font-size: $ms-font-size-m; + } + + .ms-Persona-primaryText, + .ms-Persona-secondaryText { + padding-top: 1px; + } +} + + +//== Modifier: Small Square Persona +// +.ms-Persona.ms-Persona--square.ms-Persona--sm { + .ms-Persona-presence { + height: 40px; + width: 4px; + left: 0; + } +} + + +//== Modifier: Large Persona +// +.ms-Persona.ms-Persona--lg { + .ms-Persona-imageArea, + .ms-Persona-image { + width: 72px; + height: 72px; + } + + .ms-Persona-placeholder { + font-size: 67px; + top: 10px; + } + + .ms-Persona-initials { + font-size: $ms-font-size-xxl; + line-height: 72px; + } + + .ms-Persona-presence { + left: 49px; + } + + .ms-Persona-secondaryText { + padding-top: 3px; + } + + .ms-Persona-tertiaryText { + padding-top: 5px; + } + + .ms-Persona-tertiaryText { + display: block; // Show tertiary text + } +} + + +//== Modifier: Large Square Persona +// +.ms-Persona.ms-Persona--square.ms-Persona--lg { + .ms-Persona-presence { + height: 72px; + width: 7px; + left: 0; + } +} + + +//== Modifier: Extra Large Persona +// +.ms-Persona.ms-Persona--xl { + .ms-Persona-imageArea, + .ms-Persona-image { + width: 100px; + height: 100px; + } + + .ms-Persona-placeholder { + font-size: 95px; + top: 12px; + } + + .ms-Persona-initials { + font-size: $ms-font-size-su; + line-height: 100px; + } + + .ms-Persona-presence { + height: 20px; + width: 20px; + left: 71px; + } + + .ms-Persona-details { + padding-left: 20px; + } + + .ms-Persona-primaryText { + font-size: $ms-font-size-xl; + font-family: $ms-font-family-semilight; + margin-top: 0; + } + + .ms-Persona-secondaryText { + padding-top: 2px; + } + + .ms-Persona-tertiaryText, + .ms-Persona-optionalText { + padding-top: 5px; + display: block; // Show tertiary and optional text + } +} + + +//== Modifier: Extra Large Square Persona +// +.ms-Persona.ms-Persona--square.ms-Persona--xl { + .ms-Persona-presence { + height: 100px; + width: 9px; + left: 0; + } +} + + +//== Modifier: Persona with darker text +// +// Note: Typically applied when the component has a colored background. +.ms-Persona.ms-Persona--darkText { + .ms-Persona-primaryText { + color: $ms-color-neutralDark; + } + + .ms-Persona-secondaryText, + .ms-Persona-tertiaryText, + .ms-Persona-optionalText { + color: $ms-color-neutralPrimary; + } +} + + +//== Modifier: Selectable Persona +// +.ms-Persona.ms-Persona--selectable { + cursor: pointer; + padding: 0 10px; + + &:not(.ms-Persona--xl) { + &:hover, + &:focus { + background-color: $ms-color-themeLighter; + outline: 1px solid transparent; + } + } +} + + +//== Presence indicator variants. + +//== Modifier: Persona with available presence +// +.ms-Persona.ms-Persona--available { + .ms-Persona-presence { + background-color: $ms-color-presence-available; + } +} + + +//== Modifier: Persona with away presence +// +.ms-Persona.ms-Persona--away { + .ms-Persona-presence { + background-color: $ms-color-presence-away; + } +} + + +//== Modifier: Persona with blocked presence +// +.ms-Persona.ms-Persona--blocked { + .ms-Persona-presence { + background-color: $ms-color-presence-blocked-background; + // Use a gradient to include the stripe on modern browsers. + background-image: linear-gradient( to bottom, $ms-color-presence-blocked-background 0%, $ms-color-presence-blocked-background 48%, $ms-color-presence-blocked-line 40%, $ms-color-presence-blocked-line 58%, $ms-color-presence-blocked-background 52%, $ms-color-presence-blocked-background 100% ); + } +} + + +//== Modifier: Persona with busy presence +// +.ms-Persona.ms-Persona--busy { + .ms-Persona-presence { + background-color: $ms-color-presence-busy-average; + // Replace solid background with stripes on modern browsers. + background: repeating-linear-gradient( -45deg, $ms-color-presence-busy-stripe-light, $ms-color-presence-busy-stripe-light 1px, $ms-color-presence-busy-stripe-dark 0px, $ms-color-presence-busy-stripe-dark 2px ); + } +} + + +//== Modifier: Square Persona with busy presence +// +.ms-Persona.ms-Persona--busy.ms-Persona--square { + .ms-Persona-presence { + background-color: $ms-color-presence-busy-average; + // Replace solid background with stripes on modern browsers. + background: repeating-linear-gradient( -45deg, $ms-color-presence-busy-stripe-light, $ms-color-presence-busy-stripe-light 3px, $ms-color-presence-busy-stripe-dark 3px, $ms-color-presence-busy-stripe-dark 6px ); + } +} + + +//== Modifier: Persona with do not disturb presence +// +.ms-Persona.ms-Persona--dnd { + .ms-Persona-presence { + background-color: $ms-color-presence-dnd-background; + // Use a gradient to include the stripe on modern browsers. + background-image: linear-gradient( to bottom, $ms-color-presence-dnd-background 0%, $ms-color-presence-dnd-background 48%, $ms-color-presence-dnd-line 48%, $ms-color-presence-dnd-line 52%, $ms-color-presence-dnd-background 52%, $ms-color-presence-dnd-background 100% ); + } +} + + +//== Modifier: Persona with offline presence +// +.ms-Persona.ms-Persona--offline { + .ms-Persona-presence { + background-color: $ms-color-presence-offline; + } +} + +// TODO: Remove overrides below // div.ms-Persona-image to overwrite Fabric's ms-Persona-image div.ms-Persona-image { diff --git a/src/components/Pivot/Pivot.scss b/src/components/Pivot/Pivot.scss index 3358bd61f62fc8..5b373f4aceaf3c 100644 --- a/src/components/Pivot/Pivot.scss +++ b/src/components/Pivot/Pivot.scss @@ -1,5 +1,249 @@ @import '../../common/common'; -@import "~office-ui-fabric/src/components/Pivot/Pivot"; + +// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. + +// +// Office UI Fabric +// -------------------------------------------------- +// Pivot and tab styles + + +.ms-Pivot { + @include ms-font-m; + @include ms-u-normalize; + height: 40px; + list-style-type: none; + overflow-x: hidden; + white-space: nowrap; +} + +.ms-Pivot-link { + color: $ms-color-neutralPrimary; + display: inline-block; + position: relative; + font-family: $ms-font-family-regular; + font-size: $ms-font-size-m-plus; + line-height: 40px; + margin-right: 8px; + + &:after { + content: ''; + width: 100%; + position: absolute; + display: none; + bottom: 0; + left: 0; + height: 2px; + background-color: $ms-color-themePrimary; + + @media screen and (-ms-high-contrast: active) { + background-color: $ms-color-contrastBlackSelected; + } + + @media screen and (-ms-high-contrast: black-on-white) { + background-color: $ms-color-contrastWhiteSelected; + } + } + + &:hover, + &:focus, + &:active { + color: $ms-color-black; + cursor: pointer; + + + .ms-Pivot-dropdownIcon { + color: $ms-color-neutralDark; + } + } + + &:active { + font-family: $ms-font-family-semibold; + + @media screen and (-ms-high-contrast: active) { + color: $ms-color-contrastBlackSelected; + } + + @media screen and (-ms-high-contrast: black-on-white) { + color: $ms-color-contrastWhiteSelected; + } + + &:after { + display: block; + } + } + + //== State: Selected + &.is-selected { + color: $ms-color-black; + font-family: $ms-font-family-semibold; + + @media screen and (-ms-high-contrast: active) { + color: $ms-color-contrastBlackSelected; + } + + @media screen and (-ms-high-contrast: black-on-white) { + color: $ms-color-contrastWhiteSelected; + } + + &:after { + display: block; + } + + + .ms-Pivot-dropdownIcon { + color: $ms-color-neutralDark; + } + } +} + +.ms-Pivot-dropdownIcon { + font-size: $ms-font-size-m-plus + 1; + position: relative; + top: 2px; +} + +// Overflow (ellipsis) +.ms-Pivot-link.ms-Pivot-link--overflow { + color: $ms-color-neutralSecondary; + + &.is-selected { + color: $ms-color-themePrimary; + } + + &:hover:not(.is-selected), + &:focus:not(.is-selected) { + color: $ms-color-neutralDark; + } + + &:active { + &:after { + display: none; + } + } +} + +// Ellipsis icon +.ms-Pivot-ellipsis { + font-size: $ms-font-size-m-plus; + position: relative; + top: 0; +} + + +//== Modifier: Large Pivots +// +.ms-Pivot.ms-Pivot--large { + .ms-Pivot-link { + font-size: $ms-font-size-l; + + &:active { + font-family: $ms-font-family-regular; + } + + &.is-selected { + font-family: $ms-font-family-regular; + } + } + + .ms-Pivot-link.ms-Pivot-link--overflow { + &:after { + font-size: $ms-font-size-l; + } + } +} + + +//== Modifier: Tabs +// +.ms-Pivot.ms-Pivot--tabs { + height: 40px; + + .ms-Pivot-link { + height: 40px; + background-color: $ms-color-neutralLighter; + line-height: 40px; + margin-right: -2px; // Remove space next to inline-block element + padding: 0 10px; + font-family: $ms-font-family-semilight !important; + + &:hover:not(.is-selected):not(.ms-Pivot-link--overflow), + &:focus:not(.is-selected):not(.ms-Pivot-link--overflow) { + color: $ms-color-black; + } + + &:active { + color: $ms-color-white !important; + background-color: $ms-color-themePrimary; + font-family: $ms-font-family-semilight; + + @media screen and (-ms-high-contrast: active) { + background-color: $ms-color-contrastBlackSelected; + color: $ms-color-black; + } + + @media screen and (-ms-high-contrast: black-on-white) { + background-color: $ms-color-contrastWhiteSelected; + color: $ms-color-white; + } + } + + //== State: Selected + &.is-selected { + background-color: $ms-color-themePrimary; + color: $ms-color-white; + font-family: $ms-font-family-semilight; + + @media screen and (-ms-high-contrast: active) { + background-color: $ms-color-contrastBlackSelected; + color: $ms-color-black; + } + + @media screen and (-ms-high-contrast: black-on-white) { + background-color: $ms-color-contrastWhiteSelected; + color: $ms-color-white; + } + } + } + + .ms-Pivot-link.ms-Pivot-link--overflow { + &:hover:not(.is-selected), + &:focus:not(.is-selected) { + background-color: $ms-color-white; + } + + &:active { + background-color: $ms-color-themePrimary !important; + } + } +} + + + +@media (min-width: $ms-screen-lg-min) { + .ms-Pivot-link { + font-size: $ms-font-size-m; + } + + .ms-Pivot-link.ms-Pivot-link--overflow { + &:after { + font-size: $ms-font-size-m; + } + } +} + + + +// All high contrast styling rules +@media screen and (-ms-high-contrast: active) { + .ms-Pivot.ms-Pivot--tabs { + .ms-Pivot-link { + &.is-selected { + font-family: $ms-font-family-semibold; + } + } + } +} + +// TODO: Remove override below. .ms-Pivot { position: relative; diff --git a/src/components/ProgressIndicator/ProgressIndicator.scss b/src/components/ProgressIndicator/ProgressIndicator.scss index 4445ff91e60fd2..c9ce0334236ad0 100644 --- a/src/components/ProgressIndicator/ProgressIndicator.scss +++ b/src/components/ProgressIndicator/ProgressIndicator.scss @@ -1,5 +1,63 @@ @import "~office-ui-fabric/src/sass/Fabric.Common"; -@import "~office-ui-fabric/src/components/ProgressIndicator/ProgressIndicator"; + +// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. + +// +// Office UI Fabric +// -------------------------------------------------- +// ProgressIndicator Styles + + +$ProgressIndicatorMarginBetweenText: 8px; +$ProgressIndicatorButtonsWidth: 218px; +$ProgressIndicatorTextHeight: 18px; + +.ms-ProgressIndicator-itemName { + @include ms-font-m(); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + padding-top: $ProgressIndicatorMarginBetweenText / 2; + line-height: $ProgressIndicatorTextHeight + 2; +} + +.ms-ProgressIndicator-itemDescription { + @include ms-font-m(); + color: $ms-color-neutralSecondaryAlt; + font-size: 11px; + line-height: $ProgressIndicatorTextHeight; +} + +.ms-ProgressIndicator-itemProgress { + position: relative; + width: 180px; + height: 2px; + padding: $ProgressIndicatorMarginBetweenText 0; +} + +.ms-ProgressIndicator-progressTrack { + position: absolute; + width: 100%; + height: 2px; + background-color: $ms-color-neutralLight; + outline: 1px solid transparent; +} + +.ms-ProgressIndicator-progressBar { + position: absolute; + height: 2px; + background-color: $ms-color-themePrimary; + + @media screen and (-ms-high-contrast: active) { + background-color: $ms-color-white; + } + + @media screen and (-ms-high-contrast: black-on-white) { + background-color: $ms-color-black; + } +} + +// TODO: Remove override below. .ms-ProgressIndicator-progressBar.smoothTransition { transition-property: width; diff --git a/src/components/SearchBox/SearchBox.scss b/src/components/SearchBox/SearchBox.scss index ea2aafb647cf11..9d96eeb44ad672 100644 --- a/src/components/SearchBox/SearchBox.scss +++ b/src/components/SearchBox/SearchBox.scss @@ -1,6 +1,114 @@ @import '../../common/common'; -@import "~office-ui-fabric/src/components/Label/Label"; -@import "~office-ui-fabric/src/components/SearchBox/SearchBox"; + +// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. + +// +// Office UI Fabric +// -------------------------------------------------- +// Search box styles + + +.ms-SearchBox { + @include ms-font-m; + @include ms-u-normalize; + position: relative; + margin-bottom: 10px; + display: inline-block; + + // State: Disabled searchbox + &.is-disabled { + + .ms-SearchBox-icon { + color: $ms-color-neutralTertiaryAlt; + } + .ms-SearchBox-field { + background-color: $ms-color-neutralLighter; + border-color: $ms-color-neutralLighter; + pointer-events: none; + cursor: default; + } + } + + // State: Active searchbox + &.is-active { + .ms-SearchBox-closeButton { + display: block; + outline: transparent 1px solid; + } + } +} + +.ms-SearchBox-field { + position: relative; + @include ms-u-normalize; + border: 1px solid $ms-color-themeTertiary; + outline: transparent 1px solid; + border-radius: 0; + font-family: $ms-font-family-semilight; + font-size: $ms-font-size-m; + color: $ms-color-black; + height: 32px; + padding: 6px 3px 7px 10px; + width: 180px; + background-color: transparent; + z-index: $ms-zIndex-middle; + + &.hovering { + border-color: $ms-color-themePrimary; + background-color: $ms-color-themeLighter; + + & + .ms-SearchBox-label { + color: $ms-color-black; + + .ms-Icon { + color: $ms-color-neutralPrimary; + } + } + } + + &:focus { + padding: 6px 32px 7px 10px; + border-color: $ms-color-themePrimary; + background-color: $ms-color-themeLighter; + } + + &::-ms-clear { + display: none; + } +} + +.ms-SearchBox-closeButton { + border: none; + cursor: pointer; + position: absolute; + right: 0; + top: 0; + height: 32px; + width: 32px; + background-color: $ms-color-themePrimary; + text-align: center; + display: none; + font-size: $ms-font-size-l; + color: $ms-color-white; + z-index: $ms-zIndex-front; +} + +.ms-SearchBox-label { + position: absolute; + top: 0; + left: 0; + padding-left: 8px; + line-height: 32px; + color: $ms-color-neutralSecondary; +} + +.ms-SearchBox-icon { + margin-right: 7px; + font-size: $ms-font-size-l; + color: $ms-color-neutralSecondaryAlt; +} + +// TODO: Remove overrides below. // Override Fabric so that the SearchBox takes up 100% of the parent // container's width diff --git a/src/components/Slider/Slider.tsx b/src/components/Slider/Slider.tsx index 15dc626bc23006..ca6a3e0d5ed9ac 100644 --- a/src/components/Slider/Slider.tsx +++ b/src/components/Slider/Slider.tsx @@ -97,6 +97,11 @@ export class Slider extends BaseComponent implements 'ms-Slider-showTransitions': ( renderedValue === value ) })} { ...disabled ? { } : { 'tabIndex': 0 } } + id={ this._id } + role='slider' + aria-valuenow={ value } + aria-valuemin={ min } + aria-valuemax={ max } { ...onMouseDownProp } { ...onTouchStartProp } { ...onKeyDownProp } @@ -107,13 +112,8 @@ export class Slider extends BaseComponent implements > { ); return ( - + -
    +
    {
    { (responsiveMode > ResponsiveMode.large) ? ( -
    +
    { navPanel }
    ) : (null) } -
    +
    { this.props.children }
    { (responsiveMode <= ResponsiveMode.large) ? (

    diff --git a/src/demo/pages/ContextualMenuPage/examples/ContextualMenu.Basic.Example.tsx b/src/demo/pages/ContextualMenuPage/examples/ContextualMenu.Basic.Example.tsx index bdf436efe4c2fd..2f24a49662a4f8 100644 --- a/src/demo/pages/ContextualMenuPage/examples/ContextualMenu.Basic.Example.tsx +++ b/src/demo/pages/ContextualMenuPage/examples/ContextualMenu.Basic.Example.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { ContextualMenu, DirectionalHint, Button } from '../../../../index'; +import { ContextualMenu, DirectionalHint, Button, getRTL } from '../../../../index'; import './ContextualMenuExample.scss'; export class ContextualMenuBasicExample extends React.Component { @@ -21,7 +21,7 @@ export class ContextualMenuBasicExample extends React.Component { targetPoint={this.state.target} useTargetPoint={true} onDismiss={this._onDismiss} - directionalHint={DirectionalHint.bottomLeftEdge} + directionalHint={ getRTL() ? DirectionalHint.bottomRightEdge : DirectionalHint.bottomLeftEdge} items={ [ { diff --git a/src/demo/pages/ContextualMenuPage/examples/ContextualMenuExample.scss b/src/demo/pages/ContextualMenuPage/examples/ContextualMenuExample.scss index e251638b8487cc..83762b9af212f2 100644 --- a/src/demo/pages/ContextualMenuPage/examples/ContextualMenuExample.scss +++ b/src/demo/pages/ContextualMenuPage/examples/ContextualMenuExample.scss @@ -27,8 +27,12 @@ .ms-ContextualMenu-customizationExample-item { display: inline-block; - padding: 3px; - margin: 2px; + width: 39px; + height: 39px; + line-height: 39px; + text-align: center; + vertical-align: middle; + margin-bottom: 8px; &:hover { background-color: #eaeaea; @@ -36,16 +40,16 @@ } .ms-ContextualMenu-customizationExample-categoriesList { - max-height: 200px; - overflow-y: auto; margin: 0px; padding: 0; list-style-type: none; } .ms-ContextualMenu-customizationExample-categorySwatch { - @include margin-left(-10px); - @include margin-right(10px); + margin: 8px; + width: 24px; + height: 24px; + vertical-align: top; } .ms-ContextualMenu-example-clickableArea { diff --git a/src/demo/pages/DetailsListPage/DetailsListPage.tsx b/src/demo/pages/DetailsListPage/DetailsListPage.tsx index f1c9582db481f5..890cebf2e90ba4 100644 --- a/src/demo/pages/DetailsListPage/DetailsListPage.tsx +++ b/src/demo/pages/DetailsListPage/DetailsListPage.tsx @@ -26,7 +26,7 @@ export class DetailsListPage extends React.Component { DetailsList is a derivative of List - and provides a sortable, filterable, groupable, justified table for rendering large sets of items. This component replaces the Table Component. + and provides a sortable, filterable, justified table for rendering large sets of items. This component replaces the Table Component.

    Examples

    diff --git a/src/demo/pages/DetailsListPage/examples/DetailsList.Basic.Example.tsx b/src/demo/pages/DetailsListPage/examples/DetailsList.Basic.Example.tsx index 844c6ad2b69fd4..79eb9c98006976 100644 --- a/src/demo/pages/DetailsListPage/examples/DetailsList.Basic.Example.tsx +++ b/src/demo/pages/DetailsListPage/examples/DetailsList.Basic.Example.tsx @@ -2,16 +2,35 @@ import * as React from 'react'; /* tslint:enable:no-unused-variable */ import { - DetailsList + DetailsList, + TextField } from '../../../../index'; import { createListItems } from '../../../utilities/data'; let _items: any[]; -export const DetailsListBasicExample = () => { - _items = _items || createListItems(500); +export class DetailsListBasicExample extends React.Component { + constructor() { + super(); + + _items = _items || createListItems(500); + this.state = { filterText: '' }; + } + + public render() { + let { filterText } = this.state; + let items = this.state.filterText ? _items.filter(i => i.name.toLowerCase().indexOf(filterText) > -1) : _items; + + return ( +
    + this.setState({ filterText: text }) } + /> + +
    + ); + } + +} - return ( - - ); -}; diff --git a/src/demo/pages/GroupedListPage/GroupedListPage.tsx b/src/demo/pages/GroupedListPage/GroupedListPage.tsx new file mode 100644 index 00000000000000..06bfd558a151e2 --- /dev/null +++ b/src/demo/pages/GroupedListPage/GroupedListPage.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { + ExampleCard, + PropertiesTableSet +} from '../../components/index'; + +import { GroupedListBasicExample } from './examples/GroupedList.Basic.Example'; +import { GroupedListCustomExample } from './examples/GroupedList.Custom.Example'; + +const GroupedListBasicExampleCode = require('./examples/GroupedList.Basic.Example.tsx'); +const GroupedListCustomExampleCode = require('./examples/GroupedList.Custom.Example.tsx'); + +export class GroupedListPage extends React.Component { + + public render() { + return ( +
    +

    GroupedList

    + +

    Allows you to render a set of items as multiple lists with various grouping properties.

    + +

    Examples

    + + + + + + + +
    + ); + } + +} diff --git a/src/demo/pages/GroupedListPage/examples/GroupedList.Basic.Example.tsx b/src/demo/pages/GroupedListPage/examples/GroupedList.Basic.Example.tsx new file mode 100644 index 00000000000000..8988d66399c23f --- /dev/null +++ b/src/demo/pages/GroupedListPage/examples/GroupedList.Basic.Example.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import { + GroupedList, + IGroup +} from '../../../../components/GroupedList/index'; +import { IColumn } from '../../../../DetailsList'; +import { DetailsRow } from '../../../../components/DetailsList/DetailsRow'; +import { + FocusZone +} from '../../../../FocusZone'; +import { + Selection, + SelectionMode, + SelectionZone +} from '../../../../utilities/selection/index'; + +import { + createListItems, + createGroups +} from '../../../utilities/data'; + +const groupCount = 15; +const groupDepth = 3; +const items = createListItems(Math.pow(groupCount, groupDepth + 1)); + +export class GroupedListBasicExample extends React.Component { + private _selection: Selection; + private _groups: IGroup[]; + + constructor() { + super(); + this._onRenderCell = this._onRenderCell.bind(this); + this._selection = new Selection; + this._selection.setItems(items); + + this._groups = createGroups(groupCount, groupDepth, 0, groupCount); + } + + public render() { + return ( + + + + + + ); + } + + private _onRenderCell(nestingDepth: number, item: any, itemIndex: number) { + let { + _selection: selection + } = this; + return ( + { + return { + key: value, + name: value, + fieldName: value, + minWidth: 300 + }; + }) + } + groupNestingDepth={ nestingDepth } + item={ item } + itemIndex={ itemIndex } + selection={ selection } + selectionMode={ SelectionMode.multiple } + canSelectItem={ () => true } + /> + ); + } +} diff --git a/src/demo/pages/GroupedListPage/examples/GroupedList.Custom.Example.scss b/src/demo/pages/GroupedListPage/examples/GroupedList.Custom.Example.scss new file mode 100644 index 00000000000000..76006b885832d1 --- /dev/null +++ b/src/demo/pages/GroupedListPage/examples/GroupedList.Custom.Example.scss @@ -0,0 +1,20 @@ +.ms-GroupedListExample-header, +.ms-GroupedListExample-footer { + min-width: 300px; + min-height: 40px; + line-height: 40px; + padding-left: 16px; +} + +.ms-GroupedListExample-name { + display: inline-block; + overflow: hidden; + height: 24px; + cursor: default; + padding: 8px; + box-sizing: border-box; + vertical-align: top; + background: none; + border: none; + padding-left: 32px; +} diff --git a/src/demo/pages/GroupedListPage/examples/GroupedList.Custom.Example.tsx b/src/demo/pages/GroupedListPage/examples/GroupedList.Custom.Example.tsx new file mode 100644 index 00000000000000..750363ccfbc2ac --- /dev/null +++ b/src/demo/pages/GroupedListPage/examples/GroupedList.Custom.Example.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import { + GroupedList, + IGroup +} from '../../../../components/GroupedList/index'; + +import { createListItems } from '../../../utilities/data'; +import './GroupedList.Custom.Example.scss'; + +export class GroupedListCustomExample extends React.Component { + private _items: any[]; + private _groups: IGroup[]; + + constructor() { + super(); + + this._items = createListItems(20); + this._groups = Array.apply(null, Array(4)).map((value, index): IGroup => { + return { + count: 5, + key: 'group' + index, + name: 'group ' + index, + startIndex: 5 * index, + level: 0, + onRenderFooter: this._onRenderFooter, + onRenderHeader: this._onRenderHeader + }; + }); + } + + public render() { + return ( + + ); + } + + private _onRenderCell(nestingDepth: number, item: any, itemIndex: number) { + return ( +
    + + { item.name } + +
    + ); + } + + private _onRenderHeader(group: IGroup): React.ReactNode { + return ( +
    + This is a custom header for { group.name } +
    + ); + } + + private _onRenderFooter(group: IGroup): React.ReactNode { + return ( +
    + This is a custom footer for { group.name } +
    + ); + } +} diff --git a/src/demo/pages/ListPage/examples/List.Mail.Example.scss b/src/demo/pages/ListPage/examples/List.Mail.Example.scss index 0eefed8343fe89..59dbfd77ba7a44 100644 --- a/src/demo/pages/ListPage/examples/List.Mail.Example.scss +++ b/src/demo/pages/ListPage/examples/List.Mail.Example.scss @@ -1,5 +1,235 @@ @import '../../../../common/common'; -@import '~office-ui-fabric/src/components/ListItem/ListItem.scss'; + +// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. + +// +// Office UI Fabric +// -------------------------------------------------- +// List item styles + + +.ms-ListItem { + @include ms-font-m; + @include ms-u-normalize; + @include ms-u-clearfix; + padding: 9px 28px 3px; + position: relative; + display: block; +} + +.ms-ListItem-primaryText, +.ms-ListItem-secondaryText, +.ms-ListItem-tertiaryText { + @include noWrap; + display: block; +} + +.ms-ListItem-primaryText { + color: $ms-color-neutralDark; + font-family: $ms-font-family-semilight; + font-size: $ms-font-size-xl; + padding-right: 80px; // Prevent overlap with up to three actions. + position: relative; + top: -4px; +} + +.ms-ListItem-secondaryText { + color: $ms-color-neutralPrimary; + font-family: $ms-font-family-regular; + font-size: $ms-font-size-m; + line-height: 25px; + position: relative; + top: -7px; + padding-right: 30px; +} + +.ms-ListItem-tertiaryText { + color: $ms-color-neutralSecondaryAlt; + font-family: $ms-font-family-semilight; + font-size: $ms-font-size-m; + position: relative; + top: -9px; + margin-bottom: -4px; + padding-right: 30px; +} + +.ms-ListItem-metaText { + color: $ms-color-neutralPrimary; + font-family: $ms-font-family-semilight; + font-size: $ms-font-size-xs; + position: absolute; + right: 30px; + top: 39px; +} + +.ms-ListItem-image { + float: left; + height: 70px; + margin-left: -8px; // Images sit closer to the edge than text. + margin-right: 10px; + width: 70px; +} + +.ms-ListItem-selectionTarget { + display: none; +} + +.ms-ListItem-actions { + max-width: 80px; // Up to three actions. + position: absolute; + right: 30px; + text-align: right; + top: 10px; +} + +.ms-ListItem-action { + color: $ms-color-neutralTertiary; + display: inline-block; + font-size: 15px; + position: relative; + text-align: center; + top: 3px; + cursor: pointer; + height: 16px; + width: 16px; + + .ms-Icon { + vertical-align: top; + } + + &:hover { + color: $ms-color-neutralSecondary; + outline: 1px solid transparent; + } +} + + +//== State: Unread list item +// +.ms-ListItem.is-unread { + border-left: 3px solid $ms-color-themePrimary; + padding-left: 27px; // Reduce padding to allow room for border. + + .ms-ListItem-secondaryText, .ms-ListItem-metaText { + color: $ms-color-themePrimary; + font-family: $ms-font-family-semibold; + } +} + + +//== State: Unseen list item +// +.ms-ListItem.is-unseen { + &:after { + border-right: 10px solid transparent; + border-top: 10px solid $ms-color-themePrimary; + left: 0; + position: absolute; + top: 0; + } +} + + +//== State: Selectable list item +// +.ms-ListItem.is-selectable { + .ms-ListItem-selectionTarget { + display: block; + height: 20px; + left: 6px; + position: absolute; + top: 13px; + width: 20px; + } + + .ms-ListItem-image { + margin-left: 0; + } + + &:hover { + background-color: $ms-color-neutralLight; + cursor: pointer; + outline: 1px solid transparent; + + // Insert the empty box. + &:before { + @include ms-Icon; + position: absolute; + top: 12px; + left: 6px; + height: 15px; + width: 15px; + border: 1px solid $ms-color-neutralSecondaryAlt; + } + } +} + + +//== State: Selected list item +// +.ms-ListItem.is-selected { + // Insert the checkmark. + &:before { + border: 1px solid transparent; + } + + &:before, + &:hover:before { + @include ms-Icon; + content: '\e041'; + font-size: $ms-font-size-m-plus; + color: $ms-color-neutralSecondaryAlt; + position: absolute; + top: 12px; + left: 6px; + } + + &:hover { + background-color: $ms-color-themeLight; + outline: 1px solid transparent; + } +} + + +//== Modifier: Document list item +// +.ms-ListItem.ms-ListItem--document { + padding: 0; + + // The icon for a file or folder in the items list. This may + // be an .ms-Icon or a specific modifier that loads an image. + .ms-ListItem-itemIcon { + width: 70px; + height: 70px; + float: left; + text-align: center; + } + + // If the item icon is an .ms-Icon, position and color it appropriately. + .ms-ListItem-itemIcon .ms-Icon { + font-size: 38px; + line-height: 70px; + color: $ms-color-neutralSecondary; + } + + // Primary text, typically the name. + .ms-ListItem-primaryText { + @include noWrap; + font-size: $ms-font-size-m; + padding-top: 15px; + padding-right: 0; + position: static; + } + + // Secondary text, typically the modified date or some other metadata. + .ms-ListItem-secondaryText { + @include noWrap; + color: $ms-color-neutralSecondary; + font-family: $ms-font-family-regular; + font-size: $ms-font-size-xs; + padding-top: 6px; + } +} .MailList { overflow-y: auto; diff --git a/src/demo/pages/NavPage/examples/Nav.Basic.Example.scss b/src/demo/pages/NavPage/examples/Nav.Basic.Example.scss new file mode 100644 index 00000000000000..6552f9d869556e --- /dev/null +++ b/src/demo/pages/NavPage/examples/Nav.Basic.Example.scss @@ -0,0 +1,9 @@ +@import '../../../../common/common'; + +.ms-NavExample-LeftPane { + width: 207px; + height: 400px; + box-sizing: border-box; + @include border-right(1px, solid, #EEE); + overflow-y: auto; +} \ No newline at end of file diff --git a/src/demo/pages/SearchBoxPage/examples/SearchBox.Fullsize.Example.tsx b/src/demo/pages/SearchBoxPage/examples/SearchBox.FullSize.Example.tsx similarity index 100% rename from src/demo/pages/SearchBoxPage/examples/SearchBox.Fullsize.Example.tsx rename to src/demo/pages/SearchBoxPage/examples/SearchBox.FullSize.Example.tsx diff --git a/src/demo/pages/SelectionPage/examples/Selection.Basic.Example.tsx b/src/demo/pages/SelectionPage/examples/Selection.Basic.Example.tsx index 795bf58fb46156..1f995cf48cce76 100644 --- a/src/demo/pages/SelectionPage/examples/Selection.Basic.Example.tsx +++ b/src/demo/pages/SelectionPage/examples/Selection.Basic.Example.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { CommandBar, IContextualMenuItem } from '../../../../index'; -import { Check } from '../../../../components/DetailsList/Check'; +import { Check } from '../../../../components/Check/Check'; import { IObjectWithKey, ISelection, diff --git a/src/demo/utilities/data.ts b/src/demo/utilities/data.ts index 0966eb30aff807..493b79627f7a5d 100644 --- a/src/demo/utilities/data.ts +++ b/src/demo/utilities/data.ts @@ -30,6 +30,26 @@ export function createListItems(count: number, startIndex = 0): any { }); } +export function createGroups( + groupCount: number, groupDepth: number, startIndex: number, + itemsPerGroup: number, level?: number, key?: string) { + key = key ? key + '-' : ''; + level = level ? level : 0; + let count = Math.pow(itemsPerGroup, groupDepth); + return Array.apply(null, Array(groupCount)).map((value, index) => { + return { + count: count, + key: 'group' + key + index, + name: 'group ' + key + index, + startIndex: index * count + startIndex, + level: level, + children: groupDepth > 1 ? + createGroups(groupCount, groupDepth - 1, index * count + startIndex, itemsPerGroup, level + 1, key + index) : + [] + }; + }); +} + export function lorem(wordCount: number): string { return Array.apply(null, Array(wordCount)) .map(item => _randWord(LOREM_IPSUM)) diff --git a/src/index.ts b/src/index.ts index 850a4afdf6c93b..00b5ed7ca280ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ export * from './Fabric'; export * from './Facepile'; export * from './FocusTrapZone'; export * from './FocusZone'; +export * from './GroupedList'; export * from './Image'; export * from './Label'; export * from './Layer'; diff --git a/src/utilities/decorators/withViewport.tsx b/src/utilities/decorators/withViewport.tsx index fb87156158106e..894478b42eba40 100644 --- a/src/utilities/decorators/withViewport.tsx +++ b/src/utilities/decorators/withViewport.tsx @@ -50,37 +50,50 @@ export function withViewport(ComposedComponent: any): any { public render() { let { viewport } = this.state; + let isViewportVisible = viewport.width > 0 && viewport.height > 0; return (
    - { this.state.viewport.height > 0 && ( - - )} + { isViewportVisible && ( + + ) }
    ); } public forceUpdate() { - this.refs.component.forceUpdate(); + this._updateViewport(true); } private _onAsyncResize() { this._updateViewport(); } - private _updateViewport() { + private _updateViewport(withForceUpdate?: boolean) { + let { viewport } = this.state; let viewportElement = (this.refs as any).root; let scrollElement = this._findScrollableElement(viewportElement); - let clientRect = viewportElement.getBoundingClientRect(); let scrollRect = scrollElement.getBoundingClientRect(); - - this.setState({ - viewport: { - width: clientRect.width, - height: scrollRect.height - } - }); + let updateComponent = () => { + if (withForceUpdate && this.refs.component) { + this.refs.component.forceUpdate(); + } + }; + let isSizeChanged = ( + clientRect.width !== viewport.width || + scrollRect.height !== viewport.height); + + if (isSizeChanged) { + this.setState({ + viewport: { + width: clientRect.width, + height: scrollRect.height + } + }, updateComponent); + } else { + updateComponent(); + } } private _findScrollableElement(rootElement: HTMLElement) { @@ -91,6 +104,9 @@ export function withViewport(ComposedComponent: any): any { (computedOverflow !== 'auto') && (computedOverflow !== 'scroll') ) { + if (rootElement.parentElement === null) { + break; + } rootElement = rootElement.parentElement; computedOverflow = getComputedStyle(rootElement)['overflow-y']; }