From 1d9787f3766c8b7464e96d731b00b2d00eeddaf1 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 8 Aug 2017 18:40:09 -0700 Subject: [PATCH 1/6] Fix bugs with KuiCallOut styles and update snapshots for KuiButton and KuiCallOut. --- .../src/components/button/__snapshots__/button.test.js.snap | 2 +- .../components/call_out/__snapshots__/call_out.test.js.snap | 2 +- ui_framework/src/components/call_out/_call_out.scss | 6 +----- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/ui_framework/src/components/button/__snapshots__/button.test.js.snap b/ui_framework/src/components/button/__snapshots__/button.test.js.snap index fea8a987adef4..e4f3179b82fca 100644 --- a/ui_framework/src/components/button/__snapshots__/button.test.js.snap +++ b/ui_framework/src/components/button/__snapshots__/button.test.js.snap @@ -3,7 +3,7 @@ exports[`KuiButton is rendered 1`] = ` + ); + } + + return ( +
+
+ {headerIcon} + + + {title} + + +
+ + {closeButton} + {children} +
+ ); +}; + +KuiToast.propTypes = { + title: PropTypes.node, + iconType: PropTypes.oneOf(ICON_TYPES), + type: PropTypes.oneOf(TYPES), + onClose: PropTypes.func, +}; diff --git a/ui_framework/src/components/toast/toast.test.js b/ui_framework/src/components/toast/toast.test.js new file mode 100644 index 0000000000000..46b22059b1fd8 --- /dev/null +++ b/ui_framework/src/components/toast/toast.test.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { render } from 'enzyme'; +import { requiredProps } from '../../test/required_props'; + +import { KuiToast } from './toast'; + +describe('KuiToast', () => { + test('is rendered', () => { + const component = render( + + ); + + expect(component) + .toMatchSnapshot(); + }); +}); From 4efadbb3b34774c5fff767d0146e472d98e1eb3e Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 9 Aug 2017 14:53:34 -0700 Subject: [PATCH 3/6] Add KuiGlobalToastList. --- ui_framework/dist/ui_framework.css | 75 +++++++++++- .../doc_site/src/views/kibana/kibana.js | 109 +++++++++++++++++- .../src/components/header/_header.scss | 3 +- ui_framework/src/components/index.js | 1 + .../global_toast_list.test.js.snap | 9 ++ .../components/toast/_global_toast_list.scss | 30 +++++ ui_framework/src/components/toast/_index.scss | 1 + .../src/components/toast/global_toast_list.js | 25 ++++ .../toast/global_toast_list.test.js | 16 +++ ui_framework/src/components/toast/index.js | 8 +- .../src/global_styling/mixins/_helpers.scss | 19 +++ .../global_styling/variables/_z_index.scss | 1 + 12 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 ui_framework/src/components/toast/__snapshots__/global_toast_list.test.js.snap create mode 100644 ui_framework/src/components/toast/_global_toast_list.scss create mode 100644 ui_framework/src/components/toast/global_toast_list.js create mode 100644 ui_framework/src/components/toast/global_toast_list.test.js diff --git a/ui_framework/dist/ui_framework.css b/ui_framework/dist/ui_framework.css index 8a1f99252dc0b..50cc61c3517d0 100644 --- a/ui_framework/dist/ui_framework.css +++ b/ui_framework/dist/ui_framework.css @@ -10,6 +10,9 @@ 100% { opacity: 1; } } +/** + * Set scroll bar appearance on Chrome. + */ /** * Adapted from Eric Meyer's reset (http://meyerweb.com/eric/tools/css/reset/, v2.0 | 20110126). * @@ -558,13 +561,13 @@ table { /* 1 */ } .kuiHeader { + box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1); display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; background: #FFF; - border-bottom: 1px solid #D9D9D9; - box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1); } + border-bottom: 1px solid #D9D9D9; } .kuiHeader__notification { display: inline-block; @@ -1647,12 +1650,80 @@ table { -webkit-transform: scaleX(1); transform: scaleX(1); } } +/** + * 1. Allow list to expand as items are added, but cap it at the screen height. + */ +.kuiGlobalToastList { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: stretch; + -webkit-align-items: stretch; + -ms-flex-align: stretch; + align-items: stretch; + position: fixed; + z-index: 5000; + bottom: 0; + right: 10px; + width: 320px; + padding-right: 4px; + overflow: auto; + max-height: 100vh; + /* 1 */ } + .kuiGlobalToastList::-webkit-scrollbar { + width: 16px; + height: 16px; } + .kuiGlobalToastList::-webkit-scrollbar-thumb { + background-color: rgba(102, 102, 102, 0.5); + border: 6px solid transparent; + background-clip: content-box; } + .kuiGlobalToastList::-webkit-scrollbar-track { + background-color: transparent; } + +.kuiGlobalToastList__item { + margin-bottom: 20px; + /** + * 1. justify-content: flex-end interferes with overflowing content, so we'll use this to push + * items to the bottom instead. + */ } + .kuiGlobalToastList__item:first-child { + margin-top: auto; + /* 1 */ } + .kuiToast { box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); + position: relative; padding: 12px; background-color: #FFF; border: 1px solid #D9D9D9; } +/** + * 1. Fit button to icon. + */ +.kuiToast__closeButton { + position: absolute; + top: 10px; + right: 10px; + line-height: 0; + /* 1 */ + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; } + .kuiToast__closeButton svg { + fill: gray; } + .kuiToast__closeButton:hover svg { + fill: #000; } + .kuiToast__closeButton:focus { + background-color: #e6f2f6; } + .kuiToast__closeButton:focus svg { + fill: #0079a5; } + .kuiToast--info { border-top: 2px solid #0079a5; } diff --git a/ui_framework/doc_site/src/views/kibana/kibana.js b/ui_framework/doc_site/src/views/kibana/kibana.js index f7d2015d29f5e..463d88ea96e7e 100644 --- a/ui_framework/doc_site/src/views/kibana/kibana.js +++ b/ui_framework/doc_site/src/views/kibana/kibana.js @@ -1,4 +1,5 @@ import React, { + cloneElement, Component, } from 'react'; @@ -12,9 +13,11 @@ import { KuiHeaderSection, KuiHeaderSectionItem, KuiHeaderSectionItemButton, + KuiGlobalToastList, KuiIcon, KuiKeyPadMenu, KuiKeyPadMenuItem, + KuiLink, KuiPage, KuiPageBody, KuiPageContent, @@ -28,9 +31,13 @@ import { KuiSideNav, KuiSideNavItem, KuiSideNavTitle, + KuiText, + KuiToast, KuiTitle, } from '../../../../components'; +let toastId = 0; + export default class extends Component { constructor(props) { super(props); @@ -39,6 +46,7 @@ export default class extends Component { isUserMenuOpen: false, isAppMenuOpen: false, isSideNavOpenOnMobile: false, + toasts: [], }; } @@ -54,6 +62,12 @@ export default class extends Component { }); } + onAddToastClick() { + this.setState({ + toasts: this.state.toasts.concat(this.renderRandomToast()), + }); + } + closeUserMenu() { this.setState({ isUserMenuOpen: false, @@ -72,6 +86,12 @@ export default class extends Component { }); } + onDismissToast(toastId) { + this.setState({ + toasts: this.state.toasts.filter(toast => toast.key !== toastId), + }); + } + renderLogo() { return ( @@ -413,7 +433,12 @@ export default class extends Component { - asdf + + Add toast + @@ -421,11 +446,93 @@ export default class extends Component { ); } + renderRandomToast() { + const id = (toastId++).toString(); + + const toasts = [( + + +

+ Here’s some stuff that you need to know. We can make this text really long so that, + when viewed within a browser that’s fairly narrow, it will wrap, too. +

+
+ + +

+ And some other stuff on another line, just for kicks. And here’s a link. +

+
+
+ ), ( + + +

+ Thanks for your patience! +

+
+
+ ), ( + + +

+ This is a security measure. +

+
+ + +

+ Please move your mouse to show that you’re still using Kibana. +

+
+
+ ), ( + + +

+ Sorry. We’ll try not to let it happen it again. +

+
+
+ )]; + + return cloneElement(toasts[Math.floor(Math.random() * toasts.length)], { + key: id + }); + } + + renderToasts() { + return ( + + {this.state.toasts} + + ); + } + render() { return (
{this.renderHeader()} {this.renderPage()} + {this.renderToasts()}
); } diff --git a/ui_framework/src/components/header/_header.scss b/ui_framework/src/components/header/_header.scss index f845ce16e6d9f..73de4f13011a8 100644 --- a/ui_framework/src/components/header/_header.scss +++ b/ui_framework/src/components/header/_header.scss @@ -1,10 +1,11 @@ // Header. Includes breadcrumbs of nav buttons. .kuiHeader { + @include kuiBottomShadowSmall; + display: flex; background: $kuiHeaderBackgroundColor; border-bottom: $kuiBorderThin; - @include kuiBottomShadowSmall; } .kuiHeader__notification { diff --git a/ui_framework/src/components/index.js b/ui_framework/src/components/index.js index c89d437b34c5b..3879b537772fd 100644 --- a/ui_framework/src/components/index.js +++ b/ui_framework/src/components/index.js @@ -92,6 +92,7 @@ export { } from './side_nav'; export { + KuiGlobalToastList, KuiToast, } from './toast'; diff --git a/ui_framework/src/components/toast/__snapshots__/global_toast_list.test.js.snap b/ui_framework/src/components/toast/__snapshots__/global_toast_list.test.js.snap new file mode 100644 index 0000000000000..397822261c613 --- /dev/null +++ b/ui_framework/src/components/toast/__snapshots__/global_toast_list.test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KuiGlobalToastList is rendered 1`] = ` +
+`; diff --git a/ui_framework/src/components/toast/_global_toast_list.scss b/ui_framework/src/components/toast/_global_toast_list.scss new file mode 100644 index 0000000000000..4215a15a5c38e --- /dev/null +++ b/ui_framework/src/components/toast/_global_toast_list.scss @@ -0,0 +1,30 @@ +/** + * 1. Allow list to expand as items are added, but cap it at the screen height. + */ +.kuiGlobalToastList { + @include kuiScrollBar; + + display: flex; + flex-direction: column; + align-items: stretch; + position: fixed; + z-index: $kuiZToastList; + bottom: 0; + right: 10px; + width: 320px; + padding-right: 4px; + overflow: auto; + max-height: 100vh; /* 1 */ +} + + .kuiGlobalToastList__item { + margin-bottom: 20px; + + /** + * 1. justify-content: flex-end interferes with overflowing content, so we'll use this to push + * items to the bottom instead. + */ + &:first-child { + margin-top: auto; /* 1 */ + } + } diff --git a/ui_framework/src/components/toast/_index.scss b/ui_framework/src/components/toast/_index.scss index 491bd4392973d..7d2197e77410d 100644 --- a/ui_framework/src/components/toast/_index.scss +++ b/ui_framework/src/components/toast/_index.scss @@ -1 +1,2 @@ +@import 'global_toast_list'; @import 'toast'; diff --git a/ui_framework/src/components/toast/global_toast_list.js b/ui_framework/src/components/toast/global_toast_list.js new file mode 100644 index 0000000000000..741ee298a1195 --- /dev/null +++ b/ui_framework/src/components/toast/global_toast_list.js @@ -0,0 +1,25 @@ +import React, { + cloneElement, + Children, +} from 'react'; +import classNames from 'classnames'; + +export const KuiGlobalToastList = ({ children, className, ...rest }) => { + const classes = classNames('kuiGlobalToastList', className); + + return ( +
+ {Children.map(children, child => ( + cloneElement(child, Object.assign({}, child.props, { + className: classNames(child.props.className, 'kuiGlobalToastList__item'), + })) + ))} +
+ ); +}; + +KuiGlobalToastList.propTypes = { +}; diff --git a/ui_framework/src/components/toast/global_toast_list.test.js b/ui_framework/src/components/toast/global_toast_list.test.js new file mode 100644 index 0000000000000..d0b3823ee7b25 --- /dev/null +++ b/ui_framework/src/components/toast/global_toast_list.test.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { render } from 'enzyme'; +import { requiredProps } from '../../test/required_props'; + +import { KuiGlobalToastList } from './global_toast_list'; + +describe('KuiGlobalToastList', () => { + test('is rendered', () => { + const component = render( + + ); + + expect(component) + .toMatchSnapshot(); + }); +}); diff --git a/ui_framework/src/components/toast/index.js b/ui_framework/src/components/toast/index.js index faa8a2339c74a..f1edb420ec5dd 100644 --- a/ui_framework/src/components/toast/index.js +++ b/ui_framework/src/components/toast/index.js @@ -1 +1,7 @@ -export { KuiToast } from './toast'; +export { + KuiToast, +} from './toast'; + +export { + KuiGlobalToastList, +} from './global_toast_list'; diff --git a/ui_framework/src/global_styling/mixins/_helpers.scss b/ui_framework/src/global_styling/mixins/_helpers.scss index 8a64b8b3931f7..cf30b219b8be8 100644 --- a/ui_framework/src/global_styling/mixins/_helpers.scss +++ b/ui_framework/src/global_styling/mixins/_helpers.scss @@ -24,3 +24,22 @@ } } +/** + * Set scroll bar appearance on Chrome. + */ +@mixin kuiScrollBar { + &::-webkit-scrollbar { + width: 16px; + height: 16px; + } + + &::-webkit-scrollbar-thumb { + background-color: rgba($kuiColorDarkShade, 0.5); + border: 6px solid transparent; + background-clip: content-box; + } + + &::-webkit-scrollbar-track { + background-color: transparent; + } +} diff --git a/ui_framework/src/global_styling/variables/_z_index.scss b/ui_framework/src/global_styling/variables/_z_index.scss index ecbc0867996ec..c1c0cfb88fae7 100644 --- a/ui_framework/src/global_styling/variables/_z_index.scss +++ b/ui_framework/src/global_styling/variables/_z_index.scss @@ -14,5 +14,6 @@ $kuiZLevel9: 9000; $kuiZContent: $kuiZLevel0; $kuiZContentMenu: $kuiZLevel2; $kuiZNavigation: $kuiZLevel4; +$kuiZToastList: $kuiZLevel5; $kuiZMask: $kuiZLevel6; $kuiZModal: $kuiZLevel8; From f71769ac4d7e77fdb32c89a36fe60478128c74ac Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 9 Aug 2017 16:13:28 -0700 Subject: [PATCH 4/6] Add stick-to-bottom behavior and transition-in animation. --- ui_framework/dist/ui_framework.css | 30 ++++- .../doc_site/src/views/kibana/kibana.js | 17 +++ .../components/toast/_global_toast_list.scss | 6 +- ui_framework/src/components/toast/_toast.scss | 13 ++ .../src/components/toast/global_toast_list.js | 120 +++++++++++++++--- 5 files changed, 163 insertions(+), 23 deletions(-) diff --git a/ui_framework/dist/ui_framework.css b/ui_framework/dist/ui_framework.css index 50cc61c3517d0..48aeca636a1b5 100644 --- a/ui_framework/dist/ui_framework.css +++ b/ui_framework/dist/ui_framework.css @@ -1670,10 +1670,10 @@ table { position: fixed; z-index: 5000; bottom: 0; - right: 10px; + right: 0; width: 320px; - padding-right: 4px; - overflow: auto; + overflow-x: hidden; + overflow-y: scroll; max-height: 100vh; /* 1 */ } .kuiGlobalToastList::-webkit-scrollbar { @@ -1701,7 +1701,9 @@ table { position: relative; padding: 12px; background-color: #FFF; - border: 1px solid #D9D9D9; } + border: 1px solid #D9D9D9; + -webkit-animation: 0.5s kuiShowToast; + animation: 0.5s kuiShowToast; } /** * 1. Fit button to icon. @@ -1775,6 +1777,26 @@ table { color: #000; font-weight: 300; } +@-webkit-keyframes kuiShowToast { + from { + -webkit-transform: translateX(30px); + transform: translateX(30px); + opacity: 0; } + to { + -webkit-transform: translateX(0); + transform: translateX(0); + opacity: 1; } } + +@keyframes kuiShowToast { + from { + -webkit-transform: translateX(30px); + transform: translateX(30px); + opacity: 0; } + to { + -webkit-transform: translateX(0); + transform: translateX(0); + opacity: 1; } } + .kuiTitle { font-size: 24px; font-size: 1.5rem; diff --git a/ui_framework/doc_site/src/views/kibana/kibana.js b/ui_framework/doc_site/src/views/kibana/kibana.js index 463d88ea96e7e..7532d455ab7f0 100644 --- a/ui_framework/doc_site/src/views/kibana/kibana.js +++ b/ui_framework/doc_site/src/views/kibana/kibana.js @@ -92,6 +92,12 @@ export default class extends Component { }); } + onDeleteAllToasts() { + this.setState({ + toasts: [], + }); + } + renderLogo() { return ( @@ -439,6 +445,17 @@ export default class extends Component { > Add toast + +
+
+ + + Clear toasts + diff --git a/ui_framework/src/components/toast/_global_toast_list.scss b/ui_framework/src/components/toast/_global_toast_list.scss index 4215a15a5c38e..1a761214a93b0 100644 --- a/ui_framework/src/components/toast/_global_toast_list.scss +++ b/ui_framework/src/components/toast/_global_toast_list.scss @@ -10,10 +10,10 @@ position: fixed; z-index: $kuiZToastList; bottom: 0; - right: 10px; + right: 0; width: 320px; - padding-right: 4px; - overflow: auto; + overflow-x: hidden; + overflow-y: scroll; max-height: 100vh; /* 1 */ } diff --git a/ui_framework/src/components/toast/_toast.scss b/ui_framework/src/components/toast/_toast.scss index 323f86483755b..65d5b57aef9e1 100644 --- a/ui_framework/src/components/toast/_toast.scss +++ b/ui_framework/src/components/toast/_toast.scss @@ -5,6 +5,7 @@ padding: $kuiSizeM; background-color: $kuiColorEmptyShade; border: $kuiBorderThin; + animation: 0.5s kuiShowToast; } /** @@ -80,3 +81,15 @@ $toastTypes: ( color: $kuiTitleColor; font-weight: $kuiFontWeightLight; } + +@keyframes kuiShowToast { + from { + transform: translateX(30px); + opacity: 0; + } + + to { + transform: translateX(0); + opacity: 1; + } +} diff --git a/ui_framework/src/components/toast/global_toast_list.js b/ui_framework/src/components/toast/global_toast_list.js index 741ee298a1195..5eb507d5a04fb 100644 --- a/ui_framework/src/components/toast/global_toast_list.js +++ b/ui_framework/src/components/toast/global_toast_list.js @@ -1,25 +1,113 @@ import React, { cloneElement, Children, + Component, } from 'react'; +import PropTypes from 'prop-types'; import classNames from 'classnames'; -export const KuiGlobalToastList = ({ children, className, ...rest }) => { - const classes = classNames('kuiGlobalToastList', className); - - return ( -
- {Children.map(children, child => ( - cloneElement(child, Object.assign({}, child.props, { - className: classNames(child.props.className, 'kuiGlobalToastList__item'), - })) - ))} -
- ); -}; +export class KuiGlobalToastList extends Component { + constructor(props) { + super(props); + + this.isScrollingToBottom = false; + this.isScrolledToBottom = true; + this.onScroll = this.onScroll.bind(this); + this.onMouseEnter = this.onMouseEnter.bind(this); + this.onMouseLeave = this.onMouseLeave.bind(this); + } + + startScrollingToBottom() { + this.isScrollingToBottom = true; + + const scrollToBottom = () => { + const position = this.listElement.scrollTop; + const destination = this.listElement.scrollHeight - this.listElement.clientHeight; + const distanceToDestination = destination - position; + + if (distanceToDestination < 5) { + this.listElement.scrollTop = destination; + this.isScrollingToBottom = false; + this.isScrolledToBottom = true; + return; + } + + this.listElement.scrollTop = position + distanceToDestination * 0.25; + + if (this.isScrollingToBottom) { + window.requestAnimationFrame(scrollToBottom); + } + }; + + window.requestAnimationFrame(scrollToBottom); + } + + onMouseEnter() { + // Stop scrolling to bottom if we're in mid-scroll, because the user wants to interact with + // the list. + this.isScrollingToBottom = false; + this.isUserInteracting = true; + } + + onMouseLeave() { + this.isUserInteracting = false; + } + + onScroll() { + this.isScrolledToBottom = + this.listElement.scrollHeight - this.listElement.scrollTop === this.listElement.clientHeight; + } + + componentDidMount() { + this.listElement.addEventListener('scroll', this.onScroll); + this.listElement.addEventListener('mouseenter', this.onMouseEnter); + this.listElement.addEventListener('mouseleave', this.onMouseLeave); + } + + componentDidUpdate(prevProps) { + if (!this.isUserInteracting) { + // If the user has scrolled up the toast list then we don't want to annoy them by scrolling + // all the way back to the bottom. + if (this.isScrolledToBottom) { + if (prevProps.children.length < this.props.children.length) { + this.startScrollingToBottom(); + } + } + } + } + + componentWillUnmount() { + this.listElement.removeEventListener('scroll', this.onScroll); + this.listElement.removeEventListener('mouseenter', this.onMouseEnter); + this.listElement.removeEventListener('mouseleave', this.onMouseLeave); + } + + render() { + const { + children, + className, + ...rest, + } = this.props; + + const classes = classNames('kuiGlobalToastList', className); + + return ( +
{ this.listElement = element; }} + className={classes} + {...rest} + > + {Children.map(children, child => ( + cloneElement(child, Object.assign({}, child.props, { + className: classNames(child.props.className, 'kuiGlobalToastList__item'), + })) + ))} +
+ ); + } +} KuiGlobalToastList.propTypes = { + children: PropTypes.node, + className: PropTypes.string, }; From 096f6e84979e731db9b0819b3159b6758a555e34 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Thu, 10 Aug 2017 07:14:41 -0700 Subject: [PATCH 5/6] Move transition animation from KuiToast to KuiGlobalToastList. --- ui_framework/dist/ui_framework.css | 60 ++++++++++--------- .../doc_site/src/views/kibana/kibana.js | 60 +++++++++++++++---- ui_framework/src/components/index.js | 1 + .../global_toast_list_item.test.js.snap | 9 +++ .../components/toast/_global_toast_list.scss | 37 +++++++++--- ui_framework/src/components/toast/_toast.scss | 13 ---- .../src/components/toast/global_toast_list.js | 8 +-- .../toast/global_toast_list_item.js | 20 +++++++ .../toast/global_toast_list_item.test.js | 17 ++++++ ui_framework/src/components/toast/index.js | 4 ++ .../global_styling/variables/_animations.scss | 1 - 11 files changed, 161 insertions(+), 69 deletions(-) create mode 100644 ui_framework/src/components/toast/__snapshots__/global_toast_list_item.test.js.snap create mode 100644 ui_framework/src/components/toast/global_toast_list_item.js create mode 100644 ui_framework/src/components/toast/global_toast_list_item.test.js diff --git a/ui_framework/dist/ui_framework.css b/ui_framework/dist/ui_framework.css index 48aeca636a1b5..961efb3b41e64 100644 --- a/ui_framework/dist/ui_framework.css +++ b/ui_framework/dist/ui_framework.css @@ -1686,24 +1686,48 @@ table { .kuiGlobalToastList::-webkit-scrollbar-track { background-color: transparent; } -.kuiGlobalToastList__item { +.kuiGlobalToastListItem { margin-bottom: 20px; + -webkit-animation: 0.5s kuiShowToast; + animation: 0.5s kuiShowToast; + opacity: 1; /** - * 1. justify-content: flex-end interferes with overflowing content, so we'll use this to push - * items to the bottom instead. - */ } - .kuiGlobalToastList__item:first-child { + * 1. justify-content: flex-end interferes with overflowing content, so we'll use this to push + * items to the bottom instead. + */ } + .kuiGlobalToastListItem:first-child { margin-top: auto; /* 1 */ } + .kuiGlobalToastListItem.kuiGlobalToastListItem-isDismissed { + transition: opacity 250ms; + opacity: 0; } + +@-webkit-keyframes kuiShowToast { + from { + -webkit-transform: translateX(30px); + transform: translateX(30px); + opacity: 0; } + to { + -webkit-transform: translateX(0); + transform: translateX(0); + opacity: 1; } } + +@keyframes kuiShowToast { + from { + -webkit-transform: translateX(30px); + transform: translateX(30px); + opacity: 0; } + to { + -webkit-transform: translateX(0); + transform: translateX(0); + opacity: 1; } } .kuiToast { box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); position: relative; padding: 12px; background-color: #FFF; - border: 1px solid #D9D9D9; - -webkit-animation: 0.5s kuiShowToast; - animation: 0.5s kuiShowToast; } + border: 1px solid #D9D9D9; } /** * 1. Fit button to icon. @@ -1777,26 +1801,6 @@ table { color: #000; font-weight: 300; } -@-webkit-keyframes kuiShowToast { - from { - -webkit-transform: translateX(30px); - transform: translateX(30px); - opacity: 0; } - to { - -webkit-transform: translateX(0); - transform: translateX(0); - opacity: 1; } } - -@keyframes kuiShowToast { - from { - -webkit-transform: translateX(30px); - transform: translateX(30px); - opacity: 0; } - to { - -webkit-transform: translateX(0); - transform: translateX(0); - opacity: 1; } } - .kuiTitle { font-size: 24px; font-size: 1.5rem; diff --git a/ui_framework/doc_site/src/views/kibana/kibana.js b/ui_framework/doc_site/src/views/kibana/kibana.js index 7532d455ab7f0..0f8497c6cbfed 100644 --- a/ui_framework/doc_site/src/views/kibana/kibana.js +++ b/ui_framework/doc_site/src/views/kibana/kibana.js @@ -14,6 +14,7 @@ import { KuiHeaderSectionItem, KuiHeaderSectionItemButton, KuiGlobalToastList, + KuiGlobalToastListItem, KuiIcon, KuiKeyPadMenu, KuiKeyPadMenuItem, @@ -36,7 +37,9 @@ import { KuiTitle, } from '../../../../components'; -let toastId = 0; +const TOAST_LIFE_TIME_MS = 4000; +const TOAST_FADE_OUT_MS = 250; +let toastIdCounter = 0; export default class extends Component { constructor(props) { @@ -63,9 +66,16 @@ export default class extends Component { } onAddToastClick() { + const { + toast, + toastId, + } = this.renderRandomToast(); + this.setState({ - toasts: this.state.toasts.concat(this.renderRandomToast()), + toasts: this.state.toasts.concat(toast), }); + + this.scheduleToastForDismissal(toastId); } closeUserMenu() { @@ -86,7 +96,31 @@ export default class extends Component { }); } - onDismissToast(toastId) { + scheduleToastForDismissal(toastId) { + setTimeout(() => { + this.dismissToast(toastId); + }, TOAST_LIFE_TIME_MS); + + setTimeout(() => { + this.startDismissingToast(toastId); + }, TOAST_LIFE_TIME_MS - TOAST_FADE_OUT_MS); + } + + startDismissingToast(toastId) { + this.setState({ + toasts: this.state.toasts.map(toast => { + if (toast.key === toastId) { + return cloneElement(toast, { + isDismissed: true, + }); + } + + return toast; + }), + }); + } + + dismissToast(toastId) { this.setState({ toasts: this.state.toasts.filter(toast => toast.key !== toastId), }); @@ -464,13 +498,13 @@ export default class extends Component { } renderRandomToast() { - const id = (toastId++).toString(); + const toastId = (toastIdCounter++).toString(); const toasts = [(

@@ -489,7 +523,7 @@ export default class extends Component {

@@ -502,7 +536,7 @@ export default class extends Component { title="Logging you out soon, due to inactivity" type="warning" iconType="user" - onClose={this.onDismissToast.bind(this, id)} + onClose={this.dismissToast.bind(this, toastId)} >

@@ -521,7 +555,7 @@ export default class extends Component { title="Oops, there was an error" type="danger" iconType="help" - onClose={this.onDismissToast.bind(this, id)} + onClose={this.dismissToast.bind(this, toastId)} >

@@ -531,9 +565,13 @@ export default class extends Component { )]; - return cloneElement(toasts[Math.floor(Math.random() * toasts.length)], { - key: id - }); + const toast = ( + + {toasts[Math.floor(Math.random() * toasts.length)]} + + ); + + return { toast, toastId }; } renderToasts() { diff --git a/ui_framework/src/components/index.js b/ui_framework/src/components/index.js index 3879b537772fd..c3f9ad5b40954 100644 --- a/ui_framework/src/components/index.js +++ b/ui_framework/src/components/index.js @@ -93,6 +93,7 @@ export { export { KuiGlobalToastList, + KuiGlobalToastListItem, KuiToast, } from './toast'; diff --git a/ui_framework/src/components/toast/__snapshots__/global_toast_list_item.test.js.snap b/ui_framework/src/components/toast/__snapshots__/global_toast_list_item.test.js.snap new file mode 100644 index 0000000000000..86b585850e280 --- /dev/null +++ b/ui_framework/src/components/toast/__snapshots__/global_toast_list_item.test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KuiGlobalToastListItem is rendered 1`] = ` +

+ Hi +
+`; diff --git a/ui_framework/src/components/toast/_global_toast_list.scss b/ui_framework/src/components/toast/_global_toast_list.scss index 1a761214a93b0..a3570aa3fd62b 100644 --- a/ui_framework/src/components/toast/_global_toast_list.scss +++ b/ui_framework/src/components/toast/_global_toast_list.scss @@ -17,14 +17,33 @@ max-height: 100vh; /* 1 */ } - .kuiGlobalToastList__item { - margin-bottom: 20px; +.kuiGlobalToastListItem { + margin-bottom: 20px; + animation: 0.5s kuiShowToast; + opacity: 1; - /** - * 1. justify-content: flex-end interferes with overflowing content, so we'll use this to push - * items to the bottom instead. - */ - &:first-child { - margin-top: auto; /* 1 */ - } + /** + * 1. justify-content: flex-end interferes with overflowing content, so we'll use this to push + * items to the bottom instead. + */ + &:first-child { + margin-top: auto; /* 1 */ } + + &.kuiGlobalToastListItem-isDismissed { + transition: opacity $kuiAnimSpeedNormal; + opacity: 0; + } +} + +@keyframes kuiShowToast { + from { + transform: translateX(30px); + opacity: 0; + } + + to { + transform: translateX(0); + opacity: 1; + } +} diff --git a/ui_framework/src/components/toast/_toast.scss b/ui_framework/src/components/toast/_toast.scss index 65d5b57aef9e1..323f86483755b 100644 --- a/ui_framework/src/components/toast/_toast.scss +++ b/ui_framework/src/components/toast/_toast.scss @@ -5,7 +5,6 @@ padding: $kuiSizeM; background-color: $kuiColorEmptyShade; border: $kuiBorderThin; - animation: 0.5s kuiShowToast; } /** @@ -81,15 +80,3 @@ $toastTypes: ( color: $kuiTitleColor; font-weight: $kuiFontWeightLight; } - -@keyframes kuiShowToast { - from { - transform: translateX(30px); - opacity: 0; - } - - to { - transform: translateX(0); - opacity: 1; - } -} diff --git a/ui_framework/src/components/toast/global_toast_list.js b/ui_framework/src/components/toast/global_toast_list.js index 5eb507d5a04fb..f44c0dbccb9d0 100644 --- a/ui_framework/src/components/toast/global_toast_list.js +++ b/ui_framework/src/components/toast/global_toast_list.js @@ -1,6 +1,4 @@ import React, { - cloneElement, - Children, Component, } from 'react'; import PropTypes from 'prop-types'; @@ -97,11 +95,7 @@ export class KuiGlobalToastList extends Component { className={classes} {...rest} > - {Children.map(children, child => ( - cloneElement(child, Object.assign({}, child.props, { - className: classNames(child.props.className, 'kuiGlobalToastList__item'), - })) - ))} + {children}
); } diff --git a/ui_framework/src/components/toast/global_toast_list_item.js b/ui_framework/src/components/toast/global_toast_list_item.js new file mode 100644 index 0000000000000..d7fed8d583340 --- /dev/null +++ b/ui_framework/src/components/toast/global_toast_list_item.js @@ -0,0 +1,20 @@ +import { + cloneElement, +} from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +export const KuiGlobalToastListItem = ({ isDismissed, children }) => { + const classes = classNames('kuiGlobalToastListItem', children.props.className, { + 'kuiGlobalToastListItem-isDismissed': isDismissed, + }); + + return cloneElement(children, Object.assign({}, children.props, { + className: classes, + })); +}; + +KuiGlobalToastListItem.propTypes = { + isDismissed: PropTypes.bool, + children: PropTypes.node, +}; diff --git a/ui_framework/src/components/toast/global_toast_list_item.test.js b/ui_framework/src/components/toast/global_toast_list_item.test.js new file mode 100644 index 0000000000000..511593af75d2b --- /dev/null +++ b/ui_framework/src/components/toast/global_toast_list_item.test.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { render } from 'enzyme'; + +import { KuiGlobalToastListItem } from './global_toast_list_item'; + +describe('KuiGlobalToastListItem', () => { + test('is rendered', () => { + const component = render( + +
Hi
+
+ ); + + expect(component) + .toMatchSnapshot(); + }); +}); diff --git a/ui_framework/src/components/toast/index.js b/ui_framework/src/components/toast/index.js index f1edb420ec5dd..f4c005184a1c6 100644 --- a/ui_framework/src/components/toast/index.js +++ b/ui_framework/src/components/toast/index.js @@ -5,3 +5,7 @@ export { export { KuiGlobalToastList, } from './global_toast_list'; + +export { + KuiGlobalToastListItem, +} from './global_toast_list_item'; diff --git a/ui_framework/src/global_styling/variables/_animations.scss b/ui_framework/src/global_styling/variables/_animations.scss index be2292876146d..a0c6e1803dce8 100644 --- a/ui_framework/src/global_styling/variables/_animations.scss +++ b/ui_framework/src/global_styling/variables/_animations.scss @@ -9,7 +9,6 @@ $kuiAnimSpeedNormal: 250ms; $kuiAnimSpeedSlow: 350ms; $kuiAnimSpeedExtra: 500ms; - @keyframes kuiAnimFadeIn { 0% { opacity: 0; From 562b369a79a47f2d429ff7f2f583d038c90b389c Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Thu, 10 Aug 2017 15:19:08 -0700 Subject: [PATCH 6/6] Support body-less KuiToasts. Fade them out when they're dismissed manually. --- ui_framework/dist/ui_framework.css | 10 +++-- .../doc_site/src/views/kibana/kibana.js | 26 +++++++---- .../doc_site/src/views/toast/default.js | 44 ++++++++++++------- ui_framework/doc_site/src/views/toast/info.js | 2 +- .../components/toast/_global_toast_list.scss | 2 +- ui_framework/src/components/toast/_toast.scss | 9 ++-- ui_framework/src/components/toast/toast.js | 5 ++- 7 files changed, 62 insertions(+), 36 deletions(-) diff --git a/ui_framework/dist/ui_framework.css b/ui_framework/dist/ui_framework.css index 961efb3b41e64..54a7cba93f543 100644 --- a/ui_framework/dist/ui_framework.css +++ b/ui_framework/dist/ui_framework.css @@ -1687,7 +1687,7 @@ table { background-color: transparent; } .kuiGlobalToastListItem { - margin-bottom: 20px; + margin-bottom: 12px; -webkit-animation: 0.5s kuiShowToast; animation: 0.5s kuiShowToast; opacity: 1; @@ -1734,8 +1734,8 @@ table { */ .kuiToast__closeButton { position: absolute; - top: 10px; - right: 10px; + top: 4px; + right: 4px; line-height: 0; /* 1 */ -webkit-appearance: none; @@ -1770,7 +1770,6 @@ table { font-size: 16px; font-size: 1rem; line-height: 24px; - margin-bottom: 9px; display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -1801,6 +1800,9 @@ table { color: #000; font-weight: 300; } +.kuiToastHeader--withBody { + margin-bottom: 9px; } + .kuiTitle { font-size: 24px; font-size: 1.5rem; diff --git a/ui_framework/doc_site/src/views/kibana/kibana.js b/ui_framework/doc_site/src/views/kibana/kibana.js index 0f8497c6cbfed..7b7c30b1a79ba 100644 --- a/ui_framework/doc_site/src/views/kibana/kibana.js +++ b/ui_framework/doc_site/src/views/kibana/kibana.js @@ -40,6 +40,7 @@ import { const TOAST_LIFE_TIME_MS = 4000; const TOAST_FADE_OUT_MS = 250; let toastIdCounter = 0; +const timeoutIds = []; export default class extends Component { constructor(props) { @@ -96,14 +97,16 @@ export default class extends Component { }); } - scheduleToastForDismissal(toastId) { - setTimeout(() => { + scheduleToastForDismissal(toastId, isImmediate = false) { + const lifeTime = isImmediate ? TOAST_FADE_OUT_MS : TOAST_LIFE_TIME_MS; + + timeoutIds.push(setTimeout(() => { this.dismissToast(toastId); - }, TOAST_LIFE_TIME_MS); + }, lifeTime)); - setTimeout(() => { + timeoutIds.push(setTimeout(() => { this.startDismissingToast(toastId); - }, TOAST_LIFE_TIME_MS - TOAST_FADE_OUT_MS); + }, lifeTime - TOAST_FADE_OUT_MS)); } startDismissingToast(toastId) { @@ -132,6 +135,10 @@ export default class extends Component { }); } + componentWillUnmount() { + timeoutIds.forEach(timeoutId => clearTimeout(timeoutId)); + } + renderLogo() { return ( @@ -499,12 +506,13 @@ export default class extends Component { renderRandomToast() { const toastId = (toastIdCounter++).toString(); + const dismissToast = this.scheduleToastForDismissal.bind(this, toastId, true); const toasts = [(

@@ -523,7 +531,7 @@ export default class extends Component {

@@ -536,7 +544,7 @@ export default class extends Component { title="Logging you out soon, due to inactivity" type="warning" iconType="user" - onClose={this.dismissToast.bind(this, toastId)} + onClose={dismissToast} >

@@ -555,7 +563,7 @@ export default class extends Component { title="Oops, there was an error" type="danger" iconType="help" - onClose={this.dismissToast.bind(this, toastId)} + onClose={dismissToast} >

diff --git a/ui_framework/doc_site/src/views/toast/default.js b/ui_framework/doc_site/src/views/toast/default.js index adac48395a531..a5a8d4ca00464 100644 --- a/ui_framework/doc_site/src/views/toast/default.js +++ b/ui_framework/doc_site/src/views/toast/default.js @@ -7,22 +7,32 @@ import { } from '../../../../components'; export default () => ( - window.alert('Dismiss toast')} - > - -

- Here’s some stuff that you need to know. We can make this text really long so that, - when viewed within a browser that’s fairly narrow, it will wrap, too. -

-
+
+ window.alert('Dismiss toast')} + > + +

+ Here’s some stuff that you need to know. We can make this text really long so that, + when viewed within a browser that’s fairly narrow, it will wrap, too. +

+
- -

- And some other stuff on another line, just for kicks. And here’s a link. -

-
-
+ +

+ And some other stuff on another line, just for kicks. And here’s a link. +

+
+ + +
+ + window.alert('Dismiss toast')} + /> +
); diff --git a/ui_framework/doc_site/src/views/toast/info.js b/ui_framework/doc_site/src/views/toast/info.js index 0ac391c1ae756..74f23b49c7c59 100644 --- a/ui_framework/doc_site/src/views/toast/info.js +++ b/ui_framework/doc_site/src/views/toast/info.js @@ -10,7 +10,7 @@ export default () => ( window.alert('Dismiss toast')} > diff --git a/ui_framework/src/components/toast/_global_toast_list.scss b/ui_framework/src/components/toast/_global_toast_list.scss index a3570aa3fd62b..214dd9d777e9a 100644 --- a/ui_framework/src/components/toast/_global_toast_list.scss +++ b/ui_framework/src/components/toast/_global_toast_list.scss @@ -18,7 +18,7 @@ } .kuiGlobalToastListItem { - margin-bottom: 20px; + margin-bottom: $kuiSizeM; animation: 0.5s kuiShowToast; opacity: 1; diff --git a/ui_framework/src/components/toast/_toast.scss b/ui_framework/src/components/toast/_toast.scss index 323f86483755b..f46211d2516df 100644 --- a/ui_framework/src/components/toast/_toast.scss +++ b/ui_framework/src/components/toast/_toast.scss @@ -12,8 +12,8 @@ */ .kuiToast__closeButton { position: absolute; - top: 10px; - right: 10px; + top: $kuiSizeXS; + right: $kuiSizeXS; line-height: 0; /* 1 */ appearance: none; @@ -58,7 +58,6 @@ $toastTypes: ( .kuiToastHeader { @include kuiFontSizeM; - margin-bottom: $kuiVerticalRhythmS; display: flex; align-items: baseline; /* 1 */ @@ -80,3 +79,7 @@ $toastTypes: ( color: $kuiTitleColor; font-weight: $kuiFontWeightLight; } + +.kuiToastHeader--withBody { + margin-bottom: $kuiVerticalRhythmS; +} diff --git a/ui_framework/src/components/toast/toast.js b/ui_framework/src/components/toast/toast.js index 22303bbf57934..0d07d5e9f4623 100644 --- a/ui_framework/src/components/toast/toast.js +++ b/ui_framework/src/components/toast/toast.js @@ -19,6 +19,9 @@ export const TYPES = Object.keys(typeToClassNameMap); export const KuiToast = ({ title, type, iconType, onClose, children, className, ...rest }) => { const classes = classNames('kuiToast', typeToClassNameMap[type], className); + const headerClasses = classNames('kuiToastHeader', { + 'kuiToastHeader--withBody': children, + }); let headerIcon; @@ -56,7 +59,7 @@ export const KuiToast = ({ title, type, iconType, onClose, children, className, className={classes} {...rest} > -
+
{headerIcon}