diff --git a/ui_framework/dist/ui_framework.css b/ui_framework/dist/ui_framework.css index 8f36df456519e..54a7cba93f543 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). * @@ -549,22 +552,22 @@ table { * 1. Vertically center icon with first line of title. */ .kuiCallOutHeader__icon { - fill: #0079a5; + -webkit-box-flex: 0; + -webkit-flex: 0 0 auto; + -ms-flex: 0 0 auto; + flex: 0 0 auto; -webkit-transform: translateY(2px); transform: translateY(2px); /* 1 */ } -.kuiCallOutHeader__title { - color: #0079a5; } - .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,6 +1650,159 @@ 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: 0; + width: 320px; + overflow-x: hidden; + overflow-y: scroll; + 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; } + +.kuiGlobalToastListItem { + margin-bottom: 12px; + -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. + */ } + .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; } + +/** + * 1. Fit button to icon. + */ +.kuiToast__closeButton { + position: absolute; + top: 4px; + right: 4px; + 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; } + +.kuiToast--success { + border-top: 2px solid #00A69B; } + +.kuiToast--warning { + border-top: 2px solid #E5830E; } + +.kuiToast--danger { + border-top: 2px solid #A30000; } + +/** + * 1. Align icon with first line of title text if it wraps. + * 2. Apply margin to all but last item in the flex. + */ +.kuiToastHeader { + font-size: 16px; + font-size: 1rem; + line-height: 24px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: baseline; + -webkit-align-items: baseline; + -ms-flex-align: baseline; + align-items: baseline; + /* 1 */ } + .kuiToastHeader > * + * { + margin-left: 8px; + /* 2 */ } + +/** + * 1. Vertically center icon with first line of title. + */ +.kuiToastHeader__icon { + -webkit-box-flex: 0; + -webkit-flex: 0 0 auto; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + fill: #000; + -webkit-transform: translateY(2px); + transform: translateY(2px); + /* 1 */ } + +.kuiToastHeader__title { + 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/services/routes/routes.js b/ui_framework/doc_site/src/services/routes/routes.js index c2f5ead97aaf3..2e701b73103c8 100644 --- a/ui_framework/doc_site/src/services/routes/routes.js +++ b/ui_framework/doc_site/src/services/routes/routes.js @@ -45,6 +45,9 @@ import TableExample import TabsExample from '../../views/tabs/tabs_example'; +import ToastExample + from '../../views/toast/toast_example'; + import TypographyExample from '../../views/typography/typography_example'; @@ -105,6 +108,10 @@ const components = [{ name: 'Tabs', component: TabsExample, hasReact: true, +}, { + name: 'Toast', + component: ToastExample, + hasReact: true, }, { name: 'Typography', component: TypographyExample, diff --git a/ui_framework/doc_site/src/views/kibana/kibana.js b/ui_framework/doc_site/src/views/kibana/kibana.js index f7d2015d29f5e..7b7c30b1a79ba 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,12 @@ import { KuiHeaderSection, KuiHeaderSectionItem, KuiHeaderSectionItemButton, + KuiGlobalToastList, + KuiGlobalToastListItem, KuiIcon, KuiKeyPadMenu, KuiKeyPadMenuItem, + KuiLink, KuiPage, KuiPageBody, KuiPageContent, @@ -28,9 +32,16 @@ import { KuiSideNav, KuiSideNavItem, KuiSideNavTitle, + KuiText, + KuiToast, KuiTitle, } from '../../../../components'; +const TOAST_LIFE_TIME_MS = 4000; +const TOAST_FADE_OUT_MS = 250; +let toastIdCounter = 0; +const timeoutIds = []; + export default class extends Component { constructor(props) { super(props); @@ -39,6 +50,7 @@ export default class extends Component { isUserMenuOpen: false, isAppMenuOpen: false, isSideNavOpenOnMobile: false, + toasts: [], }; } @@ -54,6 +66,19 @@ export default class extends Component { }); } + onAddToastClick() { + const { + toast, + toastId, + } = this.renderRandomToast(); + + this.setState({ + toasts: this.state.toasts.concat(toast), + }); + + this.scheduleToastForDismissal(toastId); + } + closeUserMenu() { this.setState({ isUserMenuOpen: false, @@ -72,6 +97,48 @@ export default class extends Component { }); } + scheduleToastForDismissal(toastId, isImmediate = false) { + const lifeTime = isImmediate ? TOAST_FADE_OUT_MS : TOAST_LIFE_TIME_MS; + + timeoutIds.push(setTimeout(() => { + this.dismissToast(toastId); + }, lifeTime)); + + timeoutIds.push(setTimeout(() => { + this.startDismissingToast(toastId); + }, lifeTime - 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), + }); + } + + onDeleteAllToasts() { + this.setState({ + toasts: [], + }); + } + + componentWillUnmount() { + timeoutIds.forEach(timeoutId => clearTimeout(timeoutId)); + } + renderLogo() { return ( @@ -413,7 +480,23 @@ export default class extends Component { - asdf + + Add toast + + +
+
+ + + Clear toasts +
@@ -421,11 +504,98 @@ export default class extends Component { ); } + renderRandomToast() { + const toastId = (toastIdCounter++).toString(); + const dismissToast = this.scheduleToastForDismissal.bind(this, toastId, true); + + 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. +

+
+
+ )]; + + const toast = ( + + {toasts[Math.floor(Math.random() * toasts.length)]} + + ); + + return { toast, toastId }; + } + + renderToasts() { + return ( + + {this.state.toasts} + + ); + } + render() { return (
{this.renderHeader()} {this.renderPage()} + {this.renderToasts()}
); } diff --git a/ui_framework/doc_site/src/views/toast/danger.js b/ui_framework/doc_site/src/views/toast/danger.js new file mode 100644 index 0000000000000..7f45694851be5 --- /dev/null +++ b/ui_framework/doc_site/src/views/toast/danger.js @@ -0,0 +1,28 @@ +import React from 'react'; + +import { + KuiLink, + KuiText, + KuiToast, +} from '../../../../components'; + +export default () => ( + + +

+ 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. +

+
+
+); diff --git a/ui_framework/doc_site/src/views/toast/default.js b/ui_framework/doc_site/src/views/toast/default.js new file mode 100644 index 0000000000000..a5a8d4ca00464 --- /dev/null +++ b/ui_framework/doc_site/src/views/toast/default.js @@ -0,0 +1,38 @@ +import React from 'react'; + +import { + KuiLink, + KuiText, + KuiToast, +} 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. +

+
+ + +

+ 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 new file mode 100644 index 0000000000000..74f23b49c7c59 --- /dev/null +++ b/ui_framework/doc_site/src/views/toast/info.js @@ -0,0 +1,29 @@ +import React from 'react'; + +import { + KuiLink, + KuiText, + KuiToast, +} 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. +

+
+ + +

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

+
+
+); diff --git a/ui_framework/doc_site/src/views/toast/success.js b/ui_framework/doc_site/src/views/toast/success.js new file mode 100644 index 0000000000000..6ed77c84173fa --- /dev/null +++ b/ui_framework/doc_site/src/views/toast/success.js @@ -0,0 +1,28 @@ +import React from 'react'; + +import { + KuiLink, + KuiText, + KuiToast, +} from '../../../../components'; + +export default () => ( + + +

+ 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. +

+
+
+); diff --git a/ui_framework/doc_site/src/views/toast/toast_example.js b/ui_framework/doc_site/src/views/toast/toast_example.js new file mode 100644 index 0000000000000..4d6a21ff36d3c --- /dev/null +++ b/ui_framework/doc_site/src/views/toast/toast_example.js @@ -0,0 +1,109 @@ +import React from 'react'; + +import { renderToHtml } from '../../services'; + +import { + GuideDemo, + GuidePage, + GuideSection, + GuideSectionTypes, +} from '../../components'; + +import Default from './default'; +const defaultSource = require('!!raw!./default'); +const defaultHtml = renderToHtml(Default); + +import Info from './info'; +const infoSource = require('!!raw!./info'); +const infoHtml = renderToHtml(Info); + +import Success from './success'; +const successSource = require('!!raw!./success'); +const successHtml = renderToHtml(Success); + +import Warning from './warning'; +const warningSource = require('!!raw!./warning'); +const warningHtml = renderToHtml(Warning); + +import Danger from './danger'; +const dangerSource = require('!!raw!./danger'); +const dangerHtml = renderToHtml(Danger); + +export default props => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/ui_framework/doc_site/src/views/toast/warning.js b/ui_framework/doc_site/src/views/toast/warning.js new file mode 100644 index 0000000000000..f0636dd519135 --- /dev/null +++ b/ui_framework/doc_site/src/views/toast/warning.js @@ -0,0 +1,28 @@ +import React from 'react'; + +import { + KuiLink, + KuiText, + KuiToast, +} from '../../../../components'; + +export default () => ( + + +

+ 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. +

+
+
+); 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(); + }); +}); 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/_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; 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;