+ );
+};
+
+Sidebar.propTypes = {
+ children: React.PropTypes.any,
+ className: React.PropTypes.string,
+ pinned: React.PropTypes.bool,
+ scrollY: React.PropTypes.bool,
+ width: React.PropTypes.oneOf([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 25, 33, 50, 66, 75, 100])
+};
+
+Sidebar.defaultProps = {
+ className: '',
+ pinned: false,
+ scrollY: false,
+ width: 5
+};
+
+export default Sidebar;
diff --git a/components/layout/_config.scss b/components/layout/_config.scss
new file mode 100644
index 00000000..63c0ef25
--- /dev/null
+++ b/components/layout/_config.scss
@@ -0,0 +1,17 @@
+$drawer-background-color: $palette-grey-50 !default;
+$drawer-border-color: $palette-grey-300 !default;
+$drawer-text-color: $palette-grey-800 !default;
+
+$drawer-overlay-color: $color-black !default;
+$drawer-overlay-opacity: .6 !default;
+
+
+// from: https://www.google.com/design/spec/layout/structure.html#structure-side-nav
+$navigation-drawer-desktop-width: 5 * $standard-increment-desktop !default;
+$navigation-drawer-max-desktop-width: 40 * $unit !default;
+// Mobile:
+// Width = Screen width − 56 dp
+// Maximum width: 320dp
+$navigation-drawer-mobile-width: 5 * $standard-increment-mobile !default;
+// sass doesn't like use of variable here: calc(100% - $standard-increment-mobile);
+$navigation-drawer-max-mobile-width: calc(100% - 5.6rem) !default;
diff --git a/components/layout/_mixins.scss b/components/layout/_mixins.scss
new file mode 100644
index 00000000..5ea572ec
--- /dev/null
+++ b/components/layout/_mixins.scss
@@ -0,0 +1,30 @@
+
+@mixin open() {
+ transition-delay: $animation-delay;
+ & > .scrim {
+ & > .drawerContent {
+ pointer-events: all;
+ transition-delay: $animation-delay;
+ transform: translateX(0);
+ }
+ }
+}
+
+@mixin permanent() {
+ @include open();
+
+ width: $navigation-drawer-desktop-width;
+ max-width: $navigation-drawer-desktop-width;
+
+ &.wide {
+ width: $navigation-drawer-max-desktop-width;
+ max-width: $navigation-drawer-max-desktop-width;
+ }
+
+ &.active {
+ & > .scrim {
+ width: 0;
+ background-color: rgba($drawer-overlay-color, 0);
+ }
+ }
+}
diff --git a/components/layout/index.js b/components/layout/index.js
new file mode 100644
index 00000000..9ece06a4
--- /dev/null
+++ b/components/layout/index.js
@@ -0,0 +1,4 @@
+export { default as Layout } from './Layout.jsx';
+export { default as Panel } from './Panel.jsx';
+export { default as NavDrawer } from './NavDrawer.jsx';
+export { default as Sidebar } from './Sidebar.jsx';
diff --git a/components/layout/readme.md b/components/layout/readme.md
new file mode 100644
index 00000000..2b74ed2e
--- /dev/null
+++ b/components/layout/readme.md
@@ -0,0 +1,193 @@
+# Layout
+
+A Layout is a container that can hold a main content area with an optional
+navigation drawer (on the left) and/or sidebar (on the right). According to
+the [material design spec](https://www.google.com/design/spec/layout/structure.html#structure-side-nav),
+the left drawer is typically used for navigation or identity-based content,
+while the right sidebar is secondary content related to the main content.
+
+
+```jsx
+import { AppBar, Checkbox, IconButton } from 'react-toolbox';
+import { Layout, NavDrawer, Panel, Sidebar } from 'react-toolbox';
+
+class LayoutTest extends React.Component {
+ state = {
+ drawerActive: false,
+ drawerPinned: false,
+ sidebarPinned: false
+ };
+
+ toggleDrawerActive = () => {
+ this.setState({ drawerActive: !this.state.drawerActive });
+ };
+
+ toggleDrawerPinned = () => {
+ this.setState({ drawerPinned: !this.state.drawerPinned });
+ }
+
+ toggleSidebar = () => {
+ this.setState({ sidebarPinned: !this.state.sidebarPinned });
+ };
+
+ render() {
+ return (
+
+
+
+ Navigation, account switcher, etc. go here.
+
+
+
+
+
+
Main Content
+
Main content goes here.
+
+
+
+
+
+
+
+
Supplemental content goes here.
+
+
+
+ );
+ }
+}
+```
+
+
+
+## Layout
+
+The primary layout component. This acts as the main container
+that all subcomponents are placed within. The layout is typically placed
+so as to fill the entire screen, although it does not have to be.
+
+### Breakpoints and Increments
+
+The Layout's subcomponents can alter their appearance and behavior based
+on the current screen size. The layout uses the screen breakpoints described
+in the [material design spec](https://www.google.com/design/spec/layout/responsive-ui.html#responsive-ui-breakpoints),
+namely:
+
+| Width | Abreviation | Typical Device |
+|:-----|:-----|:-----|
+| 480px | `xxs` | Phone (portrait) |
+| 600px | `xs` | Small tablet, phone (landscape) |
+| 720px | `sm-tablet` | Small tablet (portrait) |
+| 840px | `sm` | Large tablet (portrait) |
+| 960px | `md` | Small tablet (landscape) |
+| 1024px | `lg-tablet` | Large tablet (landscape) |
+| 1280px | `lg` | Large tablet (landscape), desktop |
+| 1440px | `xl` | desktop |
+| 1600px | `xxl` | desktop |
+| 1920px | `xxxl` | desktop |
+
+The components also make use of [standard increments](https://www.google.com/design/spec/layout/metrics-keylines.html#metrics-keylines-sizing-by-increments),
+which is a unit equal to the height of the action bar. At mobile sizes (< `xs`) the increment is
+56px. On larger screens, it is 64px.
+
+### Content Area Layout
+
+The content areas of all three of the subcomponents (`NavDrawer`, `Panel`, and `Sidebar`)
+use flexbox column layouts set to fill the entire height of the containing `Layout`.
+The column layout lends itself well to the fixed header/scrolling content that will frequently
+inhabit these components. By default, these components also do not scroll content vertically
+so that you can control where scrolling occurs. (For example, see the content of the `Panel`
+in the sample.)
+
+If the column layout does not suit your needs, simply fill the content area with an element
+with `flex` set to 1, and use whatever layout you like within it.
+
+### Properties
+| Name | Type | Default | Description |
+|:-----|:-----|:-----|:-----|
+| `children` | `Nodes` | | A `Panel`, optionally preceded by a `NavDrawer` and/or followed by a `Sidebar` |
+| `className` | `string` | | Additional class(es) for custom styling. |
+
+## NavDrawer
+
+The [navigation drawer](https://www.google.com/design/spec/patterns/navigation-drawer.html) slides
+in from the left and usually holds [the application's main navigation](https://www.google.com/design/spec/layout/structure.html#structure-side-nav).
+The drawer's width is based on the screen size:
+
+| Breakpoint | Drawer Width | Notes |
+|:-----|:-----|:-----|
+| < `xs` | 280px or (Screen width - 85px) | whichever is smaller |
+| > `xs` | 320px | |
+| > `xs` | 400px | If property `width` is set to `wide` |
+
+The drawer can be docked to the left side of the screen or can float temporarily
+as an overlay. You can control the drawer's display manually `active` and `pinned` properties,
+and can also specify a breakpoint at which the drawer automatically becomes permanently docked.
+
+### Properties
+| Name | Type | Default | Description |
+|:-----|:-----|:-----|:-----|
+| `width` | `enum`(`'normal'`,`'wide'`) | `normal` | 320px or 400px. Only applicable above the `sm` breakpoint. |
+| `active` | `bool` | `false` | If true, the drawer will be shown as an overlay. |
+| `pinned` | `bool` | `false` | If true, the drawer will be pinned open. `pinned` takes precedence over `active`. |
+| `permanentAt` | `enum`(`'sm'`,`'md'`,`'lg'`,`'xl'`,`'xxl'`,`'xxxl'`) | | The breakpoint at which the drawer is automatically pinned. |
+| `scrollY` | `bool` | `false` | If true, the drawer will vertically scroll all content. |
+| `onOverlayClick` | `Function` | | Callback function to be invoked when the overlay is clicked.|
+| `className` | `string` | | Additional class(es) for custom styling. |
+
+## Panel
+
+The `Panel` is the main content area within a `Layout`. It is a full-height
+flexbox column that takes up all remaining horizontal space after the `NavDrawer`
+and `Sidebar` are laid out.
+
+### Properties
+| Name | Type | Default | Description |
+|:-----|:-----|:-----|:-----|
+| `scrollY` | `bool` | `false` | If true, the panel will vertically scroll all content. |
+| `className` | `string` | | Additional class(es) for custom styling. |
+
+## Sidebar
+
+The `Sidebar` is an extra drawer that docks to the right side of the `Layout`.
+The sidebar's width can be set either to a multiple of the "standard increment"
+(1 - 12 increments) or as a percentage of the parent `Layout` width (25%, 33%, 50%, 66%, 75%, 100%).
+Regardless of the width set, at mobile screen sizes the sidebar acts like a full-screen dialog that
+covers the entire screen (see [examples](https://www.google.com/design/spec/layout/structure.html#structure-side-nav)).
+
+### Properties
+| Name | Type | Default | Description |
+|:-----|:-----|:-----|:-----|
+| `width` | `enum`(`1`,`2`,`3`,`4`,`5`,`6`,`7`,`8`,`9`,`10`,`11`,`12`,`25`,`33`,`50`,`66`,`75`,`100`) | `5` | Width in standard increments (1-12) or percentage (25, 33, 50, 66, 75, 100) |
+| `pinned` | `bool` | `false` | If true, the sidebar will be pinned open. |
+| `scrollY` | `bool` | `false` | If true, the sidebar will vertically scroll all content. |
+| `className` | `string` | | Additional class(es) for custom styling. |
+
+## Nesting Layouts
+
+The `Layout` is meant to be used near the top level of your application,
+so that it occupies the entire screen. However, it is possible to nest one
+layout inside another:
+
+```jsx
+
+ [navigation here]
+
+
+
+ [main content here]
+
+
+ [supplemental info here]
+
+
+
+
+```
+
+The main reason you would want to do something like this would be so that
+the navigation could be rendered at a high level, while the contents of the
+inner `Layout` would be controlled by react-router or something like that.
diff --git a/components/layout/style.scss b/components/layout/style.scss
new file mode 100644
index 00000000..06ac1c60
--- /dev/null
+++ b/components/layout/style.scss
@@ -0,0 +1,318 @@
+@import "../base";
+@import "./config";
+@import "./mixins";
+
+.root {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: stretch;
+ overflow-y: hidden;
+ position: relative;
+
+ width: 100%;
+ height: 100%;
+
+ .navDrawer {
+ height: 100%;
+ width: 0px;
+ min-width: 0px;
+ overflow-y: hidden;
+ overflow-x: hidden;
+ transition-timing-function: $animation-curve-default;
+ transition-duration: $animation-duration;
+ transition-property: width, min-width;
+
+
+ .scrim {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 0;
+ height: 100%;
+ background-color: rgba($drawer-overlay-color, 0);
+ transition: background-color $animation-duration $animation-curve-default,
+ width 10ms linear $animation-duration;
+ z-index: $z-index-highest;
+ }
+
+ .drawerContent {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: stretch;
+ overflow-x: hidden;
+ overflow-y: hidden;
+
+ @include shadow-2dp();
+ background-color: $drawer-background-color;
+ border-right: 1px solid $drawer-border-color;
+ color: $drawer-text-color;
+
+ width: $navigation-drawer-mobile-width;
+ max-width: $navigation-drawer-max-mobile-width;
+
+ pointer-events: none;
+
+ transform: translateX(-100%);
+ transition: transform $animation-duration $animation-curve-default;
+
+ &.scrollY {
+ overflow-y: auto;
+ }
+ }
+
+ &.pinned {
+ width: $navigation-drawer-mobile-width;
+ max-width: $navigation-drawer-max-mobile-width;
+
+ @include open();
+ }
+
+ &.active {
+ &:not(.pinned) {
+ .scrim {
+ width: 100%;
+ background-color: rgba($drawer-overlay-color, $drawer-overlay-opacity);
+ transition: background-color $animation-duration $animation-curve-default;
+ }
+
+ @include open();
+ }
+ }
+
+ // larger than mobile width drawer
+ @media screen and (min-width: $layout-breakpoint-xs) {
+
+ &.pinned {
+ width: $navigation-drawer-desktop-width;
+ max-width: $navigation-drawer-desktop-width;
+ }
+
+ .drawerContent {
+ width: $navigation-drawer-desktop-width;
+ max-width: $navigation-drawer-desktop-width;
+ }
+
+ &.wide {
+
+ &.pinned {
+ width: $navigation-drawer-max-desktop-width;
+ max-width: $navigation-drawer-max-desktop-width;
+ }
+
+ .drawerContent {
+ width: $navigation-drawer-max-desktop-width;
+ max-width: $navigation-drawer-max-desktop-width;
+ }
+ }
+ }
+
+ // permanent screen, ignore .active and make part of layout
+ @media screen and (min-width: $layout-breakpoint-sm) {
+ &.permanent-sm {
+ @include permanent();
+ }
+ }
+
+ @media screen and (min-width: $layout-breakpoint-md) {
+ &.permanent-md {
+ @include permanent();
+ }
+ }
+
+ @media screen and (min-width: $layout-breakpoint-lg) {
+ &.permanent-lg {
+ @include permanent();
+ }
+ }
+
+ @media screen and (min-width: $layout-breakpoint-xl) {
+ &.permanent-xl {
+ @include permanent();
+ }
+ }
+
+ @media screen and (min-width: $layout-breakpoint-xxl) {
+ &.permanent-xxl {
+ @include permanent();
+ }
+ }
+
+ @media screen and (min-width: $layout-breakpoint-xxxl) {
+ &.permanent-xxxl {
+ @include permanent();
+ }
+ }
+ }
+
+ & .root {
+ .scrim {
+ z-index: $z-index-highest - 1;
+ }
+ & .root {
+ .scrim {
+ z-index: $z-index-highest - 2;
+ }
+ }
+ }
+
+ .panel {
+ flex: 1;
+ height: 100%;
+ overflow-y: hidden;
+ position: relative;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: stretch;
+
+ &.scrollY {
+ overflow-y: auto;
+ }
+ }
+
+ .sidebar {
+ height: 100%;
+ overflow-y: hidden;
+ overflow-x: hidden;
+ width: 0;
+ transition-timing-function: $animation-curve-default;
+ transition-duration: $animation-duration;
+ transition-property: width;
+
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ z-index: $z-index-highest - 1;
+
+ background-color: $drawer-background-color;
+ color: $drawer-text-color;
+
+ .sidebarContent {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: stretch;
+ height: 100%;
+
+ overflow-y: hidden;
+ &.scrollY {
+ overflow-y: auto;
+ }
+ }
+
+ $increments: (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
+
+ @each $increment in $increments {
+ &.width-#{$increment} {
+ $mobile: $standard-increment-mobile * $increment;
+ $desktop: $standard-increment-desktop * $increment;
+
+ .sidebarContent {
+ min-width: 100%;
+ }
+
+ &.pinned {
+ width: 100%;
+ }
+
+ @if $increment < 10 {
+ @media screen and (min-width: $layout-breakpoint-xs) and (orientation: landscape) {
+ position: relative;
+ .sidebarContent {
+ min-width: $mobile;
+ }
+
+ &.pinned {
+ width: $mobile;
+ }
+ }
+
+ @media screen and (min-width: $layout-breakpoint-xs) and (orientation: portrait) {
+ position: relative;
+ .sidebarContent {
+ min-width: $desktop;
+ }
+
+ &.pinned {
+ width: $desktop;
+ }
+ }
+ }
+
+ @if $increment < 11 {
+ @media screen and (min-width: $layout-breakpoint-sm-tablet) {
+ position: relative;
+
+ .sidebarContent {
+ min-width: $desktop;
+ }
+
+ &.pinned {
+ width: $desktop;
+ }
+ }
+ }
+
+ @media screen and (min-width: $layout-breakpoint-sm) {
+ position: relative;
+
+ .sidebarContent {
+ min-width: $desktop;
+ }
+
+ &.pinned {
+ width: $desktop;
+ }
+ }
+ }
+ }
+
+
+ $percentages: (25, 33, 50, 66, 75);
+ &.width-100 {
+ position: absolute;
+
+ .sidebarContent {
+ min-width: 100%;
+ }
+
+ &.pinned {
+ width: 100%;
+ }
+ }
+ @each $pct in $percentages {
+ &.width-#{$pct} {
+ position: absolute;
+
+ .sidebarContent {
+ min-width: 100%;
+ }
+
+ &.pinned {
+ width: 100%;
+ }
+ }
+ }
+ @media screen and (min-width: $layout-breakpoint-sm-tablet) {
+ @each $pct in $percentages {
+ &.width-#{$pct} {
+ position: relative;
+
+ .sidebarContent {
+ min-width: $pct * 1%;
+ }
+
+ &.pinned {
+ width: $pct * 1%;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/docs/app/components/layout/main/modules/components.js b/docs/app/components/layout/main/modules/components.js
index 654f2de7..9b5a647b 100644
--- a/docs/app/components/layout/main/modules/components.js
+++ b/docs/app/components/layout/main/modules/components.js
@@ -11,6 +11,7 @@ import Drawer from 'react-toolbox/drawer/readme';
import Dropdown from 'react-toolbox/dropdown/readme';
import FontIcon from 'react-toolbox/font_icon/readme';
import Input from 'react-toolbox/input/readme';
+import Layout from 'react-toolbox/layout/readme';
import Link from 'react-toolbox/link/readme';
import List from 'react-toolbox/list/readme';
import Menu from 'react-toolbox/menu/readme';
@@ -38,6 +39,7 @@ import DrawerExample1 from './examples/drawer_example_1.txt';
import DrodpownExample1 from './examples/dropdown_example_1.txt';
import FontIconExample1 from './examples/font_icon_example_1.txt';
import InputExample1 from './examples/input_example_1.txt';
+import LayoutExample1 from './examples/layout_example_1.txt';
import LinkExample1 from './examples/link_example_1.txt';
import ListExample1 from './examples/list_example_1.txt';
import MenuExample1 from './examples/menu_example_1.txt';
@@ -125,6 +127,12 @@ export default {
path: '/components/input',
examples: [InputExample1]
},
+ layout: {
+ name: 'Layout',
+ docs: Layout,
+ path: '/components/layout',
+ examples: [LayoutExample1]
+ },
link: {
name: 'Link',
docs: Link,
diff --git a/docs/app/components/layout/main/modules/examples/layout_example_1.txt b/docs/app/components/layout/main/modules/examples/layout_example_1.txt
new file mode 100644
index 00000000..0844289c
--- /dev/null
+++ b/docs/app/components/layout/main/modules/examples/layout_example_1.txt
@@ -0,0 +1,50 @@
+class LayoutTest extends React.Component {
+ state = {
+ drawerActive: false,
+ drawerPinned: false,
+ sidebarPinned: false
+ };
+
+ toggleDrawerActive = () => {
+ this.setState({ drawerActive: !this.state.drawerActive });
+ };
+
+ toggleDrawerPinned = () => {
+ this.setState({ drawerPinned: !this.state.drawerPinned });
+ }
+
+ toggleSidebar = () => {
+ this.setState({ sidebarPinned: !this.state.sidebarPinned });
+ };
+
+ render() {
+ return (
+
+
+
+ Navigation, account switcher, etc. go here.
+
+
+
+
+
+
Main Content
+
Main content goes here.
+
+
+
+
+
+
+
+
Supplemental content goes here.
+
+
+
+ );
+ }
+}
+
+return ;
diff --git a/spec/components/layout.jsx b/spec/components/layout.jsx
new file mode 100644
index 00000000..f975c716
--- /dev/null
+++ b/spec/components/layout.jsx
@@ -0,0 +1,157 @@
+import React from 'react';
+import { AppBar, Checkbox, Dropdown, IconButton, RadioGroup, RadioButton } from '../../components';
+import { Layout, NavDrawer, Panel, Sidebar } from '../../components';
+
+const dummyText = 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.';
+
+const drawerItems = dummyText.split(/\s/).map(function (word, index) {
+ return (