React side panel (#205)

* React Side Panel

* README

* README - headers update
This commit is contained in:
Alex Terentiev 2017-05-12 08:08:19 -07:00 committed by Vesa Juvonen
parent f0202c5c7b
commit 1d63c62778
30 changed files with 690 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

View File

@ -0,0 +1 @@
* text=auto

32
samples/react-side-panel/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,14 @@
# Folders
.vscode
coverage
node_modules
sharepoint
src
temp
# Files
*.csproj
.git*
.yo-rc.json
gulpfile.js
tsconfig.json

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"libraryName": "react-side-panel",
"framework": "react",
"version": "1.0.2",
"libraryId": "e9f0eff5-4e0f-492d-a5c8-d16568d2bfc3"
}
}

View File

@ -0,0 +1,59 @@
# React Side Panel Client-Side Web Part
## Summary
The web part illustrates creation and usage of Side Panel (Sidebar) control.
![React Side Panel Client-Side Web Part](./assets/side-panel.gif)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-ga-green.svg)
## Applies to
* [SharePoint Framework](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
## Solution
Solution|Author(s)
--------|---------
react-side-panel | Alex Terentiev ([Sharepointalist Inc.](http://www.sharepointalist.com), [AJIXuMuK](https://github.com/AJIXuMuK))
## Version history
Version|Date|Comments
-------|----|--------
1.0|May 11, 2017|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
## Features
Sample features:
- ability to add controls outside web part markup (thanks to Layout component)
- usage of Office UI Fabric React (Layout, ImageButton)
- CSS transition animations
Control features:
- left or right positioning
- usage of this.props.children for content
### Resources
- [React Quick Start](https://facebook.github.io/react/docs/tutorial.html)
- [TypeScript React Tutorials](https://www.typescriptlang.org/docs/handbook/react-&-webpack.html)
- [Office UI Fabric](https://dev.office.com/fabric)
## Building the code
```bash
git clone the repo
npm i
npm i -g gulp
gulp
```
This package produces the following:
* lib/* - intermediate-stage commonjs build artifacts
* dist/* - the bundled script, along with other resources
* deploy/* - all resources which should be uploaded to a CDN.

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

View File

@ -0,0 +1,13 @@
{
"entries": [
{
"entry": "./lib/webparts/sidePanel/SidePanelWebPart.js",
"manifest": "./src/webparts/sidePanel/SidePanelWebPart.manifest.json",
"outputPath": "./dist/side-panel.bundle.js"
}
],
"externals": {},
"localizedResources": {
"sidePanelStrings": "webparts/sidePanel/loc/{locale}.js"
}
}

View File

@ -0,0 +1,3 @@
{
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,6 @@
{
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-side-panel",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,10 @@
{
"solution": {
"name": "react-side-panel-client-side-solution",
"id": "e9f0eff5-4e0f-492d-a5c8-d16568d2bfc3",
"version": "1.0.0.0"
},
"paths": {
"zippedPackage": "solution/react-side-panel.sppkg"
}
}

View File

@ -0,0 +1,9 @@
{
"port": 4321,
"initialPage": "https://localhost:5432/workbench",
"https": true,
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,45 @@
{
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-unused-imports": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,3 @@
{
"cdnBasePath": "<!-- PATH TO CDN -->"
}

6
samples/react-side-panel/gulpfile.js vendored Normal file
View File

@ -0,0 +1,6 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.initialize(gulp);

View File

@ -0,0 +1,36 @@
{
"name": "react-side-panel",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"@microsoft/sp-client-base": "~1.0.0",
"@microsoft/sp-core-library": "~1.0.0",
"@microsoft/sp-webpart-base": "~1.0.0",
"@types/react": "0.14.46",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
"@types/react-dom": "0.14.18",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"classnames": "^2.2.5",
"office-ui-fabric-react": "^2.27.3",
"react": "15.4.2",
"react-dom": "15.4.2"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.0.1",
"@microsoft/sp-module-interfaces": "~1.0.0",
"@microsoft/sp-webpart-workbench": "~1.0.0",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
}
}

View File

@ -0,0 +1,5 @@
import { PanelPosition } from './components/Panel/Panel';
export interface ISidePanelWebPartProps {
panelPosition: PanelPosition;
}

View File

@ -0,0 +1,20 @@
{
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
"id": "df0b8550-f4db-4d80-af6b-53c0fdcdae73",
"alias": "SidePanelWebPart",
"componentType": "WebPart",
"version": "0.0.1",
"manifestVersion": 2,
"preconfiguredEntries": [{
"groupId": "df0b8550-f4db-4d80-af6b-53c0fdcdae73",
"group": { "default": "Under Development" },
"title": { "default": "Side Panel" },
"description": { "default": "Side Panel description" },
"officeFabricIconFontName": "Page",
"properties": {
"panelPosition": 1
}
}]
}

View File

@ -0,0 +1,62 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneDropdown,
IPropertyPaneDropdownOption
} from '@microsoft/sp-webpart-base';
import * as strings from 'sidePanelStrings';
import SidePanel from './components/WebPart/SidePanel';
import { ISidePanelProps } from './components/WebPart/ISidePanelProps';
import { ISidePanelWebPartProps } from './ISidePanelWebPartProps';
export default class SidePanelWebPart extends BaseClientSideWebPart<ISidePanelWebPartProps> {
public render(): void {
const element: React.ReactElement<ISidePanelProps > = React.createElement(
SidePanel,
{
panelPosition: this.properties.panelPosition
}
);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneDropdown('panelPosition', {
label: strings.PanelPositionFieldLabel,
options: [{
key: 0,
text: 'Left'
}, {
key: 1,
text: 'Right'
}],
selectedKey: this.properties.panelPosition
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,53 @@
$duration: 500ms;
$width: 340px;
$contentPadding: 0 28px 20px;
:export { duration: $duration }
.panel {
position: absolute;
top: 0px;
bottom: 0px;
width: $width;
background: white;
white-space: nowrap;
overflow: hidden;
box-shadow: 0 0 5px 0 rgba(0,0,0,.4);
&.left {
left: -$width;
transition: left $duration;
border-right: 1px solid #eaeaea;
&.visible {
left: 0px;
}
}
&.right {
right: -$width;
transition: right $duration;
border-left: 1px solid #eaeaea;
&.visible {
right: 0px;
}
}
.header {
position: relative;
.closeButton {
float: right;
padding: 12px 12px 0 0;
}
.clear {
clear: both;
}
}
.content {
padding: $contentPadding;
}
}

View File

@ -0,0 +1,120 @@
import * as React from 'react';
import { Layer, IconButton, IButtonProps } from 'office-ui-fabric-react';
import * as classnames from 'classnames';
import styles from './Panel.module.scss';
export enum PanelPosition {
Left,
Right
}
export interface IPanelProps {
isOpen?: boolean;
position?: PanelPosition;
onDismiss?: () => void;
}
export interface IPanelState {
isOpen?: boolean;
isVisible?: boolean;
}
export default class Panel extends React.Component<IPanelProps, IPanelState> {
private _onCloseTimer: number;
private _onOpenTimer: number;
public constructor(props: IPanelProps, state: IPanelState) {
super(props, state);
this.state = {
isOpen: this.props.isOpen
};
}
public componentWillReceiveProps(newProps: IPanelProps) {
if (newProps.isOpen === this.props.isOpen)
return;
//
// From https://github.com/OfficeDev/office-ui-fabric-react/blob/master/packages/office-ui-fabric-react/src/components/Modal/Modal.tsx
//
clearTimeout(this._onCloseTimer);
if (newProps.isOpen) {
if (!this.state.isOpen) {
this.setState({
isOpen: true
});
}
else {
this.setState({
isVisible: true
});
}
}
if (!newProps.isOpen && this.state.isOpen) {
this._close();
}
}
public componentDidUpdate(prevProps: IPanelProps, prevState: IPanelState) {
if (!prevProps.isOpen && !prevState.isVisible && this.state.isOpen) {
setTimeout(this._onOpen.bind(this), 45); // just to set open class a little bit later to have animation
}
}
public render(): JSX.Element {
if (!this.state.isOpen)
return null;
const optionalClasses: any = {};
optionalClasses[styles.visible] = this.state.isVisible;
optionalClasses[styles.left] = this.props.position === PanelPosition.Left;
optionalClasses[styles.right] = this.props.position === PanelPosition.Right;
const className = classnames(styles.panel, optionalClasses);
return (
<Layer>
<div className={className}>
<div className={styles.header}>
<div className={styles.closeButton}>
<IconButton
iconProps={{ iconName: 'Cancel' }}
onClick={this.onDismiss.bind(this)} />
</div>
<div className={styles.clear}></div>
</div>
<div className={styles.content}>
{this.props.children}
</div>
</div>
</Layer>);
}
private onDismiss() {
this._close();
}
private _close() {
this._onCloseTimer = setTimeout(this._onClose.bind(this), parseFloat(styles.duration));
this.setState({
isVisible: false
});
}
private _onOpen() {
this.setState({
isVisible: true
});
}
private _onClose() {
this.setState({
isOpen: false
});
if (this.props.onDismiss)
this.props.onDismiss();
}
}

View File

@ -0,0 +1,5 @@
import {PanelPosition} from '../Panel/Panel';
export interface ISidePanelProps {
panelPosition?: PanelPosition;
}

View File

@ -0,0 +1,52 @@
.helloWorld {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
padding: 20px;
}
.listItem {
max-width: 715px;
margin: 5px auto 5px auto;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: #0078d7;
border-color: #0078d7;
color: #ffffff;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
font-weight: 400;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: 600;
font-size: 14px;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,53 @@
import * as React from 'react';
import styles from './SidePanel.module.scss';
import { ISidePanelProps } from './ISidePanelProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { Button, ButtonType } from 'office-ui-fabric-react';
import Panel from '../Panel/Panel';
import { PanelPosition } from '../Panel/Panel';
export interface ISidePanelState {
isOpen?: boolean;
}
export default class SidePanel extends React.Component<ISidePanelProps, ISidePanelState> {
public constructor(props: ISidePanelProps, state: ISidePanelState) {
super(props, state);
this.state = {};
}
public render(): React.ReactElement<ISidePanelProps> {
const panelPosition = !this.props.panelPosition && this.props.panelPosition !== 0
? PanelPosition.Right : this.props.panelPosition;
return (
<div className={styles.helloWorld}>
<div className={styles.container}>
<div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
<div className="ms-Grid-col ms-u-lg10 ms-u-xl8 ms-u-xlPush2 ms-u-lgPush1">
<p className="ms-font-l ms-fontColor-white">This Web Part shows how to open a side panel.</p>
<p className="ms-font-l ms-fontColor-white">Please, click the button below.</p>
<Button onClick={this.onButtonClick.bind(this)} buttonType={ButtonType.default}>{this.state.isOpen ? 'Hide Panel' : 'Show Panel'}</Button>
</div>
</div>
</div>
<Panel isOpen={this.state.isOpen} position={panelPosition} onDismiss={this.onPanelClosed.bind(this)}>
<span>Child content of the panel</span>
</Panel>
</div>
);
}
private onPanelClosed() {
this.setState({
isOpen: false
});
}
private onButtonClick() {
this.setState({
isOpen: !this.state.isOpen
});
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"PanelPositionFieldLabel": "Panel Position"
}
});

View File

@ -0,0 +1,10 @@
declare interface ISidePanelStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
PanelPositionFieldLabel: string;
}
declare module 'sidePanelStrings' {
const strings: ISidePanelStrings;
export = strings;
}

View File

@ -0,0 +1,9 @@
/// <reference types="mocha" />
import { assert } from 'chai';
describe('SidePanelWebPart', () => {
it('should do something', () => {
assert.ok(true);
});
});

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"types": [
"es6-promise",
"es6-collections",
"webpack-env"
]
}
}

View File

@ -0,0 +1,8 @@
// Type definitions for Microsoft ODSP projects
// Project: ODSP
/* Global definition for UNIT_TEST builds
Code that is wrapped inside an if(UNIT_TEST) {...}
block will not be included in the final bundle when the
--ship flag is specified */
declare const UNIT_TEST: boolean;

View File

@ -0,0 +1 @@
/// <reference path="@ms/odsp.d.ts" />