[UI Framework] Add KuiContextMenu. (#14183) (#14427)

* Add KuiContextMenu.
- Update KuiPopover to use K7 code.
- Add KuiPanelSimple for use within KuiPopover; it's just the K7 KuiPanel renamed.
- Refactor/rewrite KuiExpression and KuiExpressionButton to depend upon KuiPopover.
- Add K7 shadow mixins and size and z-index vars to global_styling.

* Update Dashboard panel to use KuiContextMenu.
- Fix reloading issue when editing a visualization from within a dashboard.

* Completely refactor KuiContextMenu to enable a single panel.
- Move keyboard navigation logic into KuiContextPanel.
- Set focus on the item which shows the panel we're leaving within KuiContextMenu.
- Remove unnecessary logic from KuiPopoverTitle.
- Replace confusing idToPanelMap and idToPreviousPanelIdMap props with a panels prop.
- Replace panelRef prop with onHeightChange prop.
- Migrate transition state and logic from KuiContextMenu into KuiContextMenuPanel.
- Rename 'current panel' to 'incoming panel' for cohesion with 'outgoing panel.'
- Map panel items to panels up-front.
- Convert maps from state variables into instance variables.
This commit is contained in:
CJ Cenizal 2017-10-11 09:22:58 -07:00 committed by GitHub
parent b1c759b0a2
commit 20742822bc
76 changed files with 3224 additions and 838 deletions

View file

@ -122,6 +122,7 @@
"expose-loader": "0.7.0",
"extract-text-webpack-plugin": "0.8.2",
"file-loader": "0.8.4",
"focus-trap-react": "3.0.3",
"font-awesome": "4.4.0",
"glob": "5.0.13",
"glob-all": "3.0.1",
@ -194,6 +195,7 @@
"script-loader": "0.6.1",
"semver": "5.1.0",
"style-loader": "0.12.3",
"tabbable": "1.1.0",
"tar": "2.2.0",
"tinygradient": "0.3.0",
"trunc-html": "1.0.2",

View file

@ -79,10 +79,17 @@ export class DashboardGrid extends React.Component {
};
onPanelFocused = panelIndex => {
this.gridItems[panelIndex].style.zIndex = '1';
const gridItem = this.gridItems[panelIndex];
if (gridItem) {
gridItem.style.zIndex = '1';
}
};
onPanelBlurred = panelIndex => {
this.gridItems[panelIndex].style.zIndex = 'auto';
const gridItem = this.gridItems[panelIndex];
if (gridItem) {
gridItem.style.zIndex = 'auto';
}
};
renderDOM() {

View file

@ -29,56 +29,6 @@ exports[`DashboardPanel matches snapshot 1`] = `
role="button"
tabindex="0"
/>
<div
class="kuiPopover__body"
>
<ul
class="kuiMenu"
>
<li
class="kuiMenuItem dashboardPanelMenuItem"
data-test-subj="dashboardPanelEditLink"
>
<span
aria-hidden="true"
class="kuiButton__icon kuiIcon fa-edit"
/>
<p
class="kuiText"
>
Edit Visualization
</p>
</li>
<li
class="kuiMenuItem dashboardPanelMenuItem"
data-test-subj="dashboardPanelExpandIcon"
>
<span
aria-hidden="true"
class="kuiButton__icon kuiIcon fa-expand"
/>
<p
class="kuiText"
>
Full screen
</p>
</li>
<li
class="kuiMenuItem dashboardPanelMenuItem"
data-test-subj="dashboardPanelRemoveIcon"
>
<span
aria-hidden="true"
class="kuiButton__icon kuiIcon fa-trash"
/>
<p
class="kuiText"
>
Delete from dashboard
</p>
</li>
</ul>
</div>
</div>
</div>
</div>

View file

@ -60,9 +60,11 @@ export class DashboardPanel extends React.Component {
}
toggleExpandedPanel = () => this.props.onToggleExpanded(this.props.panel.panelIndex);
deletePanel = () => {
this.props.onDeletePanel(this.props.panel.panelIndex);
};
onEditPanel = () => window.location = this.state.editUrl;
onFocus = () => {
@ -71,6 +73,7 @@ export class DashboardPanel extends React.Component {
onPanelFocused(this.props.panel.panelIndex);
}
};
onBlur = () => {
const { onPanelBlurred } = this.props;
if (onPanelBlurred) {

View file

@ -1,27 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { KuiMenuItem } from 'ui_framework/components';
export function PanelMenuItem({ iconClass, onClick, label, ...props }) {
const iconClasses = classNames('kuiButton__icon kuiIcon', iconClass);
return (
<KuiMenuItem
className="dashboardPanelMenuItem"
onClick={onClick}
{...props}
>
<span
aria-hidden="true"
className={iconClasses}
/>
<p className="kuiText">{label}</p>
</KuiMenuItem>
);
}
PanelMenuItem.propTypes = {
iconClass: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
label: PropTypes.string.isRequired
};

View file

@ -1,78 +1,100 @@
import React from 'react';
import PropTypes from 'prop-types';
import { PanelMenuItem } from './panel_menu_item';
import {
KuiPopover,
KuiMenu,
KuiKeyboardAccessible
KuiContextMenuPanel,
KuiContextMenuItem,
KuiKeyboardAccessible,
} from 'ui_framework/components';
export class PanelOptionsMenu extends React.Component {
state = {
showMenu: false
isPopoverOpen: false
};
toggleMenu = () => {
this.setState({ showMenu: !this.state.showMenu });
this.setState({ isPopoverOpen: !this.state.isPopoverOpen });
};
closeMenu = () => this.setState({ showMenu: false });
renderEditVisualizationMenuItem() {
return (
<PanelMenuItem
onClick={this.props.onEditPanel}
closePopover = () => this.setState({ isPopoverOpen: false });
renderItems() {
const items = [(
<KuiContextMenuItem
key="0"
data-test-subj="dashboardPanelEditLink"
label="Edit Visualization"
iconClass="fa-edit"
/>
);
}
renderDeleteMenuItem() {
return (
<PanelMenuItem
onClick={this.props.onDeletePanel}
data-test-subj="dashboardPanelRemoveIcon"
label="Delete from dashboard"
iconClass="fa-trash"
/>
);
}
renderToggleExpandMenuItem() {
return (
<PanelMenuItem
onClick={this.props.onToggleExpandPanel}
onClick={this.props.onEditPanel}
icon={(
<span
aria-hidden="true"
className="kuiButton__icon kuiIcon fa-edit"
/>
)}
>
Edit Visualization
</KuiContextMenuItem>
), (
<KuiContextMenuItem
key="1"
data-test-subj="dashboardPanelExpandIcon"
label={this.props.isExpanded ? 'Minimize' : 'Full screen'}
iconClass={this.props.isExpanded ? 'fa-compress' : 'fa-expand'}
/>
);
onClick={this.props.onToggleExpandPanel}
icon={(
<span
aria-hidden="true"
className={`kuiButton__icon kuiIcon ${this.props.isExpanded ? 'fa-compress' : 'fa-expand'}`}
/>
)}
>
{this.props.isExpanded ? 'Minimize' : 'Full screen'}
</KuiContextMenuItem>
)];
if (!this.props.isExpanded) {
items.push(
<KuiContextMenuItem
key="2"
data-test-subj="dashboardPanelRemoveIcon"
onClick={this.props.onDeletePanel}
icon={(
<span
aria-hidden="true"
className="kuiButton__icon kuiIcon fa-trash"
/>
)}
>
Delete from dashboard
</KuiContextMenuItem>
);
}
return items;
}
render() {
const button = (
<KuiKeyboardAccessible>
<span
aria-label="Click for more panel options"
className="kuiButton__icon kuiIcon panel-dropdown fa fa-caret-down"
data-test-subj="dashboardPanelToggleMenuIcon"
onClick={this.toggleMenu}
/>
</KuiKeyboardAccessible>
);
return (
<KuiPopover
className="dashboardPanelPopOver"
button={(
<KuiKeyboardAccessible>
<span
aria-label="Click for more panel options"
className="kuiButton__icon kuiIcon panel-dropdown fa fa-caret-down"
data-test-subj="dashboardPanelToggleMenuIcon"
onClick={this.toggleMenu}
/>
</KuiKeyboardAccessible>
)}
isOpen={this.state.showMenu}
button={button}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover}
panelPaddingSize="none"
anchorPosition="right"
closePopover={this.closeMenu}
>
<KuiMenu>
{this.renderEditVisualizationMenuItem()}
{this.renderToggleExpandMenuItem()}
{this.props.isExpanded ? null : this.renderDeleteMenuItem()}
</KuiMenu>
<KuiContextMenuPanel
onClose={this.closePopover}
items={this.renderItems()}
/>
</KuiPopover>
);
}

View file

@ -215,21 +215,6 @@ dashboard-panel {
z-index: 25;
}
.dashboardPanelMenuItem {
padding: 10px;
color: @text-color;
p {
display: inline;
padding: 0 0 0 5px;
}
&:hover {
color: @link-hover-color;
}
}
.panel-title {
font-size: inherit;

View file

@ -856,6 +856,213 @@ main {
/* 2 */
width: 100%; }
.kuiContextMenu {
width: 256px;
position: relative;
overflow: hidden;
transition: height 150ms cubic-bezier(0.694, 0.0482, 0.335, 1);
border-radius: 4px; }
.kuiContextMenu .kuiContextMenu__content {
padding: 8px; }
.kuiContextMenu__panel {
position: absolute; }
.kuiContextMenu__icon {
margin-right: 8px; }
.kuiContextMenu__itemLayout {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center; }
.kuiContextMenuPanel {
width: 100%;
visibility: visible;
background-color: #ffffff; }
.kuiContextMenuPanel.kuiContextMenuPanel-txInLeft {
pointer-events: none;
-webkit-animation: kuiContextMenuPanelTxInLeft 250ms cubic-bezier(0.694, 0.0482, 0.335, 1);
animation: kuiContextMenuPanelTxInLeft 250ms cubic-bezier(0.694, 0.0482, 0.335, 1); }
.kuiContextMenuPanel.kuiContextMenuPanel-txOutLeft {
pointer-events: none;
-webkit-animation: kuiContextMenuPanelTxOutLeft 250ms cubic-bezier(0.694, 0.0482, 0.335, 1);
animation: kuiContextMenuPanelTxOutLeft 250ms cubic-bezier(0.694, 0.0482, 0.335, 1); }
.kuiContextMenuPanel.kuiContextMenuPanel-txInRight {
pointer-events: none;
-webkit-animation: kuiContextMenuPanelTxInRight 250ms cubic-bezier(0.694, 0.0482, 0.335, 1);
animation: kuiContextMenuPanelTxInRight 250ms cubic-bezier(0.694, 0.0482, 0.335, 1); }
.kuiContextMenuPanel.kuiContextMenuPanel-txOutRight {
pointer-events: none;
-webkit-animation: kuiContextMenuPanelTxOutRight 250ms cubic-bezier(0.694, 0.0482, 0.335, 1);
animation: kuiContextMenuPanelTxOutRight 250ms cubic-bezier(0.694, 0.0482, 0.335, 1); }
.theme-dark .kuiContextMenuPanel {
background-color: #777777; }
.kuiContextMenuPanel--next {
-webkit-transform: translateX(256px);
transform: translateX(256px);
visibility: hidden; }
.kuiContextMenuPanel--previous {
-webkit-transform: translateX(-256px);
transform: translateX(-256px);
visibility: hidden; }
/**
* 1. Button reset.
*/
.kuiContextMenuPanelTitle {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
/* 1 */
border: none;
/* 1 */
cursor: pointer;
/* 1 */
background-color: #e6e6e6;
border-bottom: 1px solid #D9D9D9;
padding: 12px;
font-size: 14px;
width: 100%;
text-align: left;
/**
* 1. Overwrite default style.
*/ }
.theme-dark .kuiContextMenuPanelTitle {
background-color: #777777;
border-color: #444444;
color: #ffffff; }
.kuiContextMenuPanelTitle:hover .kuiContextMenu__text, .kuiContextMenuPanelTitle:focus .kuiContextMenu__text {
text-decoration: underline; }
.kuiContextMenuPanelTitle:focus {
box-shadow: none;
/* 1 */ }
@-webkit-keyframes kuiContextMenuPanelTxInLeft {
0% {
-webkit-transform: translateX(100%);
transform: translateX(100%); }
100% {
-webkit-transform: translateX(0);
transform: translateX(0); } }
@keyframes kuiContextMenuPanelTxInLeft {
0% {
-webkit-transform: translateX(100%);
transform: translateX(100%); }
100% {
-webkit-transform: translateX(0);
transform: translateX(0); } }
@-webkit-keyframes kuiContextMenuPanelTxOutLeft {
0% {
-webkit-transform: translateX(0);
transform: translateX(0); }
100% {
-webkit-transform: translateX(-100%);
transform: translateX(-100%); } }
@keyframes kuiContextMenuPanelTxOutLeft {
0% {
-webkit-transform: translateX(0);
transform: translateX(0); }
100% {
-webkit-transform: translateX(-100%);
transform: translateX(-100%); } }
@-webkit-keyframes kuiContextMenuPanelTxInRight {
0% {
-webkit-transform: translateX(-100%);
transform: translateX(-100%); }
100% {
-webkit-transform: translateX(0);
transform: translateX(0); } }
@keyframes kuiContextMenuPanelTxInRight {
0% {
-webkit-transform: translateX(-100%);
transform: translateX(-100%); }
100% {
-webkit-transform: translateX(0);
transform: translateX(0); } }
@-webkit-keyframes kuiContextMenuPanelTxOutRight {
0% {
-webkit-transform: translateX(0);
transform: translateX(0); }
100% {
-webkit-transform: translateX(100%);
transform: translateX(100%); } }
@keyframes kuiContextMenuPanelTxOutRight {
0% {
-webkit-transform: translateX(0);
transform: translateX(0); }
100% {
-webkit-transform: translateX(100%);
transform: translateX(100%); } }
/**
* 1. Button reset.
* 2. Ensure buttons stack.
*/
.kuiContextMenuItem {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
/* 1 */
background-color: transparent;
/* 1 */
font-size: 14px;
/* 1 */
border: none;
/* 1 */
cursor: pointer;
/* 1 */
display: block;
/* 2 */
padding: 12px;
width: 100%;
text-align: left;
color: #2d2d2d;
/**
* 1. Overwrite default style.
*/ }
.kuiContextMenuItem:hover .kuiContextMenuItem__text, .kuiContextMenuItem:focus .kuiContextMenuItem__text {
text-decoration: underline; }
.kuiContextMenuItem:focus {
background-color: rgba(63, 168, 199, 0.2);
box-shadow: none;
/* 1 */ }
.theme-dark .kuiContextMenuItem:focus {
background-color: transparent; }
.theme-dark .kuiContextMenuItem {
color: #ffffff; }
.kuiContextMenuItem__inner {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex; }
.kuiContextMenuItem__text {
-webkit-box-flex: 1;
-webkit-flex-grow: 1;
-ms-flex-positive: 1;
flex-grow: 1; }
.kuiContextMenuItem__arrow {
-webkit-align-self: flex-end;
-ms-flex-item-align: end;
align-self: flex-end; }
.kuiEvent {
display: -webkit-box;
display: -webkit-flex;
@ -887,13 +1094,11 @@ main {
line-height: 1.5;
color: #666; }
.kuiExpressionItem {
display: inline-block;
position: relative; }
.kuiExpressionItem + .kuiExpressionItem {
margin-left: 10px; }
.kuiExpression {
padding: 20px;
white-space: nowrap; }
.kuiExpressionItem__button {
.kuiExpressionButton {
background-color: transparent;
padding: 5px 0px;
border: none;
@ -901,95 +1106,17 @@ main {
font-size: 14px;
cursor: pointer; }
.kuiExpressionItem__buttonDescription {
.kuiExpressionButton__description {
color: #00A69B;
text-transform: uppercase; }
.kuiExpressionItem__buttonValue {
.kuiExpressionButton__value {
color: #2d2d2d;
text-transform: lowercase; }
.kuiExpressionItem__button--isActive {
.kuiExpressionButton-isActive {
border-bottom: solid 2px #00A69B; }
.kuiExpressionItem__popover {
position: absolute;
top: calc(100% + 15px);
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-flex: 1;
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
background-color: white;
border: 1px solid #D9D9D9;
border-radius: 6px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
visibility: visible;
opacity: 1;
-webkit-transform: translateY(-5px) translateZ(0);
transform: translateY(-5px) translateZ(0);
transition: opacity 250ms cubic-bezier(0.34, 1.61, 0.7, 1), -webkit-transform 250ms cubic-bezier(0.34, 1.61, 0.7, 1);
transition: transform 250ms cubic-bezier(0.34, 1.61, 0.7, 1), opacity 250ms cubic-bezier(0.34, 1.61, 0.7, 1);
transition: transform 250ms cubic-bezier(0.34, 1.61, 0.7, 1), opacity 250ms cubic-bezier(0.34, 1.61, 0.7, 1), -webkit-transform 250ms cubic-bezier(0.34, 1.61, 0.7, 1); }
.kuiExpressionItem__popover.ng-hide {
display: block !important;
visibility: hidden;
opacity: 0;
-webkit-transform: translateY(0px) translateZ(0);
transform: translateY(0px) translateZ(0); }
.kuiExpressionItem__popover:before {
position: absolute;
content: "";
top: -8px;
left: 20px;
height: 0;
width: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid #D9D9D9; }
.kuiExpressionItem__popover:after {
position: absolute;
content: "";
top: -7px;
left: 20px;
height: 0;
width: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid #e6e6e6; }
.kuiExpressionItem__popover.kuiExpressionItem__popover--alignRight {
right: 0; }
.kuiExpressionItem__popover.kuiExpressionItem__popover--alignRight:before, .kuiExpressionItem__popover.kuiExpressionItem__popover--alignRight:after {
left: auto;
right: 20px; }
.kuiExpressionItem__popoverTitle {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 1;
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
background-color: #e6e6e6;
border-radius: 4px 4px 0 0;
color: #2d2d2d;
padding: 5px 10px;
line-height: 1.5; }
.kuiExpressionItem__popoverContent {
padding: 20px;
white-space: nowrap; }
.kuiFlexGroup {
display: -webkit-box;
display: -webkit-flex;
@ -3112,38 +3239,59 @@ main {
.kuiPanelBody {
padding: 10px; }
.kuiPanelSimple {
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1);
background-color: #FFF;
border: 1px solid #D9D9D9;
border-radius: 4px;
-webkit-box-flex: 1;
-webkit-flex-grow: 1;
-ms-flex-positive: 1;
flex-grow: 1; }
.kuiPanelSimple.kuiPanelSimple--paddingSmall {
padding: 8px; }
.kuiPanelSimple.kuiPanelSimple--paddingMedium {
padding: 16px; }
.kuiPanelSimple.kuiPanelSimple--paddingLarge {
padding: 24px; }
.kuiPanelSimple.kuiPanelSimple--shadow {
box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); }
.kuiPanelSimple.kuiPanelSimple--flexGrowZero {
-webkit-box-flex: 0;
-webkit-flex-grow: 0;
-ms-flex-positive: 0;
flex-grow: 0; }
.theme-dark .kuiPanelSimple {
background-color: #777777;
border-color: #444444; }
.kuiPopover {
display: inline-block;
position: relative; }
.kuiPopover.kuiPopover-isOpen .kuiPopover__body {
.kuiPopover.kuiPopover-isOpen .kuiPopover__panel {
opacity: 1;
visibility: visible;
display: inline-block;
z-index: 1;
margin-top: 10px;
box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); }
z-index: 2000;
margin-top: 8px;
pointer-events: auto; }
.kuiPopover__body {
line-height: 1.5;
font-size: 14px;
.kuiPopover__panel {
position: absolute;
min-width: 256px;
top: 100%;
left: 50%;
background: #FFF;
border: 1px solid #D9D9D9;
border-radius: 4px 0 4px 4px;
padding: 16px;
-webkit-transform: translateX(-50%) translateY(8px) translateZ(0);
transform: translateX(-50%) translateY(8px) translateZ(0);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
transition: opacity cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, visibility cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, margin-top cubic-bezier(0.34, 1.61, 0.7, 1) 350ms;
-webkit-transform-origin: center top;
transform-origin: center top;
opacity: 0;
display: none;
margin-top: 32px; }
.kuiPopover__body:before {
visibility: hidden;
pointer-events: none;
margin-top: 24px; }
.kuiPopover__panel:before {
position: absolute;
content: "";
top: -16px;
@ -3154,7 +3302,9 @@ main {
border-left: 16px solid transparent;
border-right: 16px solid transparent;
border-bottom: 16px solid #D9D9D9; }
.kuiPopover__body:after {
.theme-dark .kuiPopover__panel:before {
border-bottom-color: #444444; }
.kuiPopover__panel:after {
position: absolute;
content: "";
top: -15px;
@ -3165,25 +3315,42 @@ main {
width: 0;
border-left: 16px solid transparent;
border-right: 16px solid transparent;
border-bottom: 16px solid #FFF; }
border-bottom: 16px solid #ffffff; }
.theme-dark .kuiPopover__panel:after {
border-bottom-color: #777777; }
.kuiPopover--anchorLeft .kuiPopover__body {
.kuiPopover--withTitle .kuiPopover__panel:after {
border-bottom-color: #e6e6e6; }
.theme-dark .kuiPopover--withTitle .kuiPopover__panel:after {
border-bottom-color: #777777; }
.kuiPopover--anchorLeft .kuiPopover__panel {
left: 0;
-webkit-transform: translateX(0%) translateY(8px) translateZ(0);
transform: translateX(0%) translateY(8px) translateZ(0); }
.kuiPopover--anchorLeft .kuiPopover__body:before, .kuiPopover--anchorLeft .kuiPopover__body:after {
.kuiPopover--anchorLeft .kuiPopover__panel:before, .kuiPopover--anchorLeft .kuiPopover__panel:after {
right: auto;
left: 8px;
left: 16px;
margin: 0; }
.kuiPopover--anchorRight .kuiPopover__body {
.kuiPopover--anchorRight .kuiPopover__panel {
left: 100%;
-webkit-transform: translateX(-100%) translateY(8px) translateZ(0);
transform: translateX(-100%) translateY(8px) translateZ(0); }
.kuiPopover--anchorRight .kuiPopover__body:before, .kuiPopover--anchorRight .kuiPopover__body:after {
right: 8px;
.kuiPopover--anchorRight .kuiPopover__panel:before, .kuiPopover--anchorRight .kuiPopover__panel:after {
right: 16px;
left: auto; }
.kuiPopoverTitle {
background-color: #e6e6e6;
border-bottom: 1px solid #D9D9D9;
padding: 12px;
font-size: 14px; }
.theme-dark .kuiPopoverTitle {
background-color: #777777;
border-color: #444444;
color: #ffffff; }
.kuiEmptyTablePrompt {
display: -webkit-box;
display: -webkit-flex;

View file

@ -34,15 +34,30 @@ export class GuideDemo extends Component {
}
render() {
const classes = classNames('guideDemo', this.props.className, {
'guideDemo--fullScreen': this.props.isFullScreen,
'guideDemo--darkTheme': this.props.isDarkTheme,
'theme-dark': this.props.isDarkTheme,
const {
isFullScreen,
isDarkTheme,
children,
className,
js, // eslint-disable-line no-unused-vars
html, // eslint-disable-line no-unused-vars
css, // eslint-disable-line no-unused-vars
...rest,
} = this.props;
const classes = classNames('guideDemo', className, {
'guideDemo--fullScreen': isFullScreen,
'guideDemo--darkTheme': isDarkTheme,
'theme-dark': isDarkTheme,
});
return (
<div className={classes} ref={c => (this.content = c)}>
{this.props.children}
<div
className={classes}
ref={c => (this.content = c)}
{...rest}
>
{children}
</div>
);
}

View file

@ -30,6 +30,9 @@ import ColorPickerExample
import ColumnExample
from '../../views/column/column_example';
import ContextMenuExample
from '../../views/context_menu/context_menu_example';
import EventExample
from '../../views/event/event_example';
@ -93,6 +96,9 @@ import PagerExample
import PanelExample
from '../../views/panel/panel_example';
import PanelSimpleExample
from '../../views/panel_simple/panel_simple_example';
import PopoverExample
from '../../views/popover/popover_example';
@ -162,6 +168,14 @@ const components = [{
}, {
name: 'Column',
component: ColumnExample,
}, {
name: 'CollapseButton',
component: CollapseButtonExample,
hasReact: true,
}, {
name: 'ContextMenu',
component: ContextMenuExample,
hasReact: true,
}, {
name: 'EmptyTablePrompt',
component: EmptyTablePromptExample,
@ -230,6 +244,10 @@ const components = [{
}, {
name: 'Panel',
component: PanelExample,
}, {
name: 'PanelSimple',
component: PanelSimpleExample,
hasReact: true,
}, {
name: 'Popover',
component: PopoverExample,

View file

@ -0,0 +1,151 @@
import React, {
Component,
} from 'react';
import {
KuiButton,
KuiContextMenu,
KuiFieldGroup,
KuiFieldGroupSection,
KuiPopover,
} from '../../../../components';
function flattenPanelTree(tree, array = []) {
array.push(tree);
if (tree.items) {
tree.items.forEach(item => {
if (item.panel) {
flattenPanelTree(item.panel, array);
item.panel = item.panel.id;
}
});
}
return array;
}
export default class extends Component {
constructor(props) {
super(props);
this.state = {
isPopoverOpen: false,
};
const panelTree = {
id: 0,
title: 'View options',
items: [{
name: 'Show fullscreen',
icon: (
<span className="kuiIcon fa-search" />
),
onClick: () => window.alert('Show fullscreen'),
}, {
name: 'Share this dasbhoard',
icon: <span className="kuiIcon fa-user" />,
panel: {
id: 1,
title: 'Share this dashboard',
items: [{
name: 'PDF reports',
icon: <span className="kuiIcon fa-user" />,
onClick: () => window.alert('PDF reports'),
}, {
name: 'CSV reports',
icon: <span className="kuiIcon fa-user" />,
onClick: () => window.alert('CSV reports'),
}, {
name: 'Embed code',
icon: <span className="kuiIcon fa-user" />,
panel: {
id: 2,
title: 'Embed code',
content: (
<div style={{ padding: 16 }}>
<div className="kuiVerticalRhythmSmall">
<KuiFieldGroup>
<KuiFieldGroupSection isWide>
<div className="kuiSearchInput">
<div className="kuiSearchInput__icon kuiIcon fa-search" />
<input
className="kuiSearchInput__input"
type="text"
/>
</div>
</KuiFieldGroupSection>
<KuiFieldGroupSection>
<select className="kuiSelect">
<option>Animal</option>
<option>Mineral</option>
<option>Vegetable</option>
</select>
</KuiFieldGroupSection>
</KuiFieldGroup>
</div>
<div className="kuiVerticalRhythmSmall">
<KuiButton buttonType="primary">Save</KuiButton>
</div>
</div>
),
},
}, {
name: 'Permalinks',
icon: <span className="kuiIcon fa-user" />,
onClick: () => window.alert('Permalinks'),
}],
},
}, {
name: 'Edit / add panels',
icon: <span className="kuiIcon fa-user" />,
onClick: () => window.alert('Edit / add panels'),
}, {
name: 'Display options',
icon: <span className="kuiIcon fa-user" />,
onClick: () => window.alert('Display options'),
}],
};
this.panels = flattenPanelTree(panelTree);
}
onButtonClick() {
this.setState({
isPopoverOpen: !this.state.isPopoverOpen,
});
}
closePopover() {
this.setState({
isPopoverOpen: false,
});
}
render() {
const button = (
<KuiButton buttonType="basic" onClick={this.onButtonClick.bind(this)}>
Click me to load a context menu
</KuiButton>
);
return (
<KuiPopover
button={button}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover.bind(this)}
panelPaddingSize="none"
withTitle
anchorPosition="left"
>
<KuiContextMenu
initialPanelId={0}
isVisible={this.state.isPopoverOpen}
panels={this.panels}
/>
</KuiPopover>
);
}
}

View file

@ -0,0 +1,69 @@
import React from 'react';
import { renderToHtml } from '../../services';
import {
GuideCode,
GuideDemo,
GuidePage,
GuideSection,
GuideSectionTypes,
GuideText,
} from '../../components';
import ContextMenu from './context_menu';
const contextMenuSource = require('!!raw!./context_menu');
const contextMenuHtml = renderToHtml(ContextMenu);
import SinglePanel from './single_panel';
const singlePanelSource = require('!!raw!./single_panel');
const singlePanelHtml = renderToHtml(SinglePanel);
export default props => (
<GuidePage title={props.route.name}>
<GuideSection
title="Context Menu"
source={[{
type: GuideSectionTypes.JS,
code: contextMenuSource,
}, {
type: GuideSectionTypes.HTML,
code: contextMenuHtml,
}]}
>
<GuideText>
<GuideCode>KuiContextMenu</GuideCode> is a nested menu system useful
for navigating complicated trees. It lives within a <GuideCode>KuiPopover</GuideCode>
which itself can be wrapped around any component (like a button in this example).
</GuideText>
<GuideDemo style={{ height: 280 }}>
<ContextMenu />
</GuideDemo>
<GuideDemo isDarkTheme={true} style={{ height: 280 }}>
<ContextMenu />
</GuideDemo>
</GuideSection>
<GuideSection
title="Single panel"
source={[{
type: GuideSectionTypes.JS,
code: singlePanelSource,
}, {
type: GuideSectionTypes.HTML,
code: singlePanelHtml,
}]}
>
<GuideText>
You can put a single panel inside of the menu using the
<GuideCode>KuiContextMenuPanel</GuideCode> component directly.
</GuideText>
<GuideDemo style={{ height: 280 }}>
<SinglePanel />
</GuideDemo>
</GuideSection>
</GuidePage>
);

View file

@ -0,0 +1,82 @@
import React, {
Component,
} from 'react';
import {
KuiButton,
KuiContextMenuPanel,
KuiContextMenuItem,
KuiPopover,
} from '../../../../components';
export default class extends Component {
constructor(props) {
super(props);
this.state = {
isPopoverOpen: false,
};
}
onButtonClick() {
this.setState({
isPopoverOpen: !this.state.isPopoverOpen,
});
}
closePopover() {
this.setState({
isPopoverOpen: false,
});
}
render() {
const button = (
<KuiButton buttonType="basic" onClick={this.onButtonClick.bind(this)}>
Click me to load a context menu
</KuiButton>
);
const items = [(
<KuiContextMenuItem
key="A"
icon={<span className="kuiIcon fa-user" />}
onClick={() => { window.alert('A'); }}
>
Option A
</KuiContextMenuItem>
), (
<KuiContextMenuItem
key="B"
icon={<span className="kuiIcon fa-user" />}
onClick={() => { window.alert('B'); }}
>
Option B
</KuiContextMenuItem>
), (
<KuiContextMenuItem
key="C"
icon={<span className="kuiIcon fa-user" />}
onClick={() => { window.alert('C'); }}
>
Option C
</KuiContextMenuItem>
)];
return (
<KuiPopover
button={button}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover.bind(this)}
panelPaddingSize="none"
withTitle
anchorPosition="left"
>
<KuiContextMenuPanel
title="Options"
items={items}
/>
</KuiPopover>
);
}
}

View file

@ -1,8 +1,11 @@
import React, { PropTypes } from 'react';
import {
KuiExpressionItem,
KuiExpressionItemButton,
KuiExpressionItemPopover,
KuiExpression,
KuiExpressionButton,
KuiFieldGroup,
KuiFieldGroupSection,
KuiPopover,
KuiPopoverTitle,
} from '../../../../components';
@ -12,6 +15,7 @@ class KuiExpressionItemExample extends React.Component {
this.state = {
example1: {
isOpen: false,
value: 'count()'
},
example2: {
@ -19,10 +23,53 @@ class KuiExpressionItemExample extends React.Component {
value: '100',
description: 'Is above'
},
activeButton: props.defaultActiveButton
};
}
openExample1 = () => {
this.setState({
example1: {
...this.state.example1,
isOpen: true,
},
example2: {
...this.state.example2,
isOpen: false,
},
});
};
closeExample1 = () => {
this.setState({
example1: {
...this.state.example1,
isOpen: false,
},
});
};
openExample2 = () => {
this.setState({
example1: {
...this.state.example1,
isOpen: false,
},
example2: {
...this.state.example2,
isOpen: true,
},
});
};
closeExample2 = () => {
this.setState({
example2: {
...this.state.example2,
isOpen: false,
},
});
};
changeExample1 = (event) => {
this.setState({ example1: { ...this.state.example1, value: event.target.value } });
}
@ -39,73 +86,81 @@ class KuiExpressionItemExample extends React.Component {
this.setState({ example2: { ...this.state.example2, description: event.target.value } });
}
onOutsideClick = () => {
this.setState({ activeButton:null });
}
render() {
//Rise the popovers above GuidePageSideNav
const popoverStyle = { zIndex:'200' };
const popover1 = (this.state.activeButton === 'example1') ? this.getPopover1(popoverStyle) : null;
const popover2 = (this.state.activeButton === 'example2') ? this.getPopover2(popoverStyle) : null;
// Rise the popovers above GuidePageSideNav
const popoverStyle = { zIndex: '200' };
return (
<div>
<KuiExpressionItem key="example1">
<KuiExpressionItemButton
description="when"
buttonValue={this.state.example1.value}
isActive={this.state.activeButton === 'example1'}
onClick={()=>this.setState({ activeButton:'example1' })}
/>
{popover1}
</KuiExpressionItem>
<KuiExpressionItem key="example2">
<KuiExpressionItemButton
description={this.state.example2.description}
buttonValue={this.state.example2.value}
isActive={this.state.activeButton === 'example2'}
onClick={()=>this.setState({ activeButton:'example2' })}
/>
{popover2}
</KuiExpressionItem>
</div>
<KuiFieldGroup>
<KuiFieldGroupSection>
<KuiPopover
button={(
<KuiExpressionButton
description="when"
buttonValue={this.state.example1.value}
isActive={this.state.example1.isOpen}
onClick={this.openExample1}
/>
)}
isOpen={this.state.example1.isOpen}
closePopover={this.closeExample1}
panelPaddingSize="none"
withTitle
>
{this.getPopover1(popoverStyle)}
</KuiPopover>
</KuiFieldGroupSection>
<KuiFieldGroupSection>
<KuiPopover
button={(
<KuiExpressionButton
description={this.state.example2.description}
buttonValue={this.state.example2.value}
isActive={this.state.example2.isOpen}
onClick={this.openExample2}
/>
)}
isOpen={this.state.example2.isOpen}
closePopover={this.closeExample2}
panelPaddingSize="none"
withTitle
anchorPosition="left"
>
{this.getPopover2(popoverStyle)}
</KuiPopover>
</KuiFieldGroupSection>
</KuiFieldGroup>
);
}
getPopover1(popoverStyle) {
return (
<KuiExpressionItemPopover
title="When"
onOutsideClick={this.onOutsideClick}
style={popoverStyle}
>
<select
className="kuiSelect"
value={this.state.example1.value}
onChange={this.changeExample1}
>
<option label="count()">count()</option>
<option label="average()">average()</option>
<option label="sum()">sum()</option>
<option label="median()">median()</option>
<option label="min()">min()</option>
<option label="max()">max()</option>
</select>
</KuiExpressionItemPopover>
<div style={popoverStyle}>
<KuiPopoverTitle>When</KuiPopoverTitle>
<KuiExpression>
<select
className="kuiSelect"
value={this.state.example1.value}
onChange={this.changeExample1}
>
<option label="count()">count()</option>
<option label="average()">average()</option>
<option label="sum()">sum()</option>
<option label="median()">median()</option>
<option label="min()">min()</option>
<option label="max()">max()</option>
</select>
</KuiExpression>
</div>
);
}
getPopover2(popoverStyle) {
return (
<KuiExpressionItemPopover
title={this.state.example2.description}
onOutsideClick={this.onOutsideClick}
align="right"
style={popoverStyle}
>
<div>
<div style={popoverStyle}>
<KuiPopoverTitle>{this.state.example2.description}</KuiPopoverTitle>
<KuiExpression>
<select
className="kuiSelect"
value={this.state.example2.object}
@ -132,8 +187,8 @@ class KuiExpressionItemExample extends React.Component {
<option label="Is below">Is below</option>
<option label="Is exactly">Is exactly</option>
</select>
</div>
</KuiExpressionItemPopover>
</KuiExpression>
</div>
);
}
}

View file

@ -16,7 +16,7 @@ const expressionHtml = renderToHtml(Expression, { defaultActiveButton: 'example2
export default props => (
<GuidePage title={props.route.name}>
<GuideSection
title="ExpressionItem"
title="ExpressionButton"
source={[{
type: GuideSectionTypes.JS,
code: expressionSource,
@ -26,8 +26,7 @@ export default props => (
}]}
>
<GuideText>
ExpressionItems allow you to compress a complicated form into a small space.
Left aligned to the button by default. Can be optionally right aligned (as in the last example).
ExpressionButtons allow you to compress a complicated form into a small space.
</GuideText>
<GuideDemo>

View file

@ -0,0 +1,37 @@
import React from 'react';
import {
KuiPanelSimple,
} from '../../../../components';
export default () => (
<div>
<KuiPanelSimple paddingSize="none">
sizePadding=&quot;none&quot;
</KuiPanelSimple>
<br />
<KuiPanelSimple paddingSize="s">
sizePadding=&quot;s&quot;
</KuiPanelSimple>
<br />
<KuiPanelSimple paddingSize="m">
sizePadding=&quot;m&quot;
</KuiPanelSimple>
<br />
<KuiPanelSimple paddingSize="l">
sizePadding=&quot;l&quot;
</KuiPanelSimple>
<br />
<KuiPanelSimple paddingSize="l" hasShadow>
sizePadding=&quot;l&quot;, hasShadow
</KuiPanelSimple>
</div>
);

View file

@ -0,0 +1,43 @@
import React from 'react';
import { Link } from 'react-router';
import { renderToHtml } from '../../services';
import {
GuideCode,
GuideDemo,
GuidePage,
GuideSection,
GuideSectionTypes,
GuideText,
} from '../../components';
import PanelSimple from './panel_simple';
const panelSimpleSource = require('!!raw!./panel_simple');
const panelSimpleHtml = renderToHtml(PanelSimple);
export default props => (
<GuidePage title={props.route.name}>
<GuideSection
title="PanelSimple"
source={[{
type: GuideSectionTypes.JS,
code: panelSimpleSource,
}, {
type: GuideSectionTypes.HTML,
code: panelSimpleHtml,
}]}
>
<GuideText>
<GuideCode>PanelSimple</GuideCode> is a simple wrapper component to add
depth to a contained layout. It it commonly used as a base for
other larger components like <Link className="guideLink" to="/popover">Popover</Link>.
</GuideText>
<GuideDemo>
<PanelSimple />
</GuideDemo>
</GuideSection>
</GuidePage>
);

View file

@ -31,7 +31,7 @@ export default class extends Component {
render() {
const button = (
<KuiButton buttonType="basic" onClick={this.onButtonClick.bind(this)}>
Click me
Show popover
</KuiButton>
);

View file

@ -62,7 +62,7 @@ export default class extends Component {
<KuiPopover
button={(
<KuiButton buttonType="basic" onClick={this.onButtonClick2.bind(this)}>
Popover anchored to the right.
Popover anchored to the left.
</KuiButton>
)}
isOpen={this.state.isPopoverOpen2}

View file

@ -18,9 +18,13 @@ import PopoverAnchorPosition from './popover_anchor_position';
const popoverAnchorPositionSource = require('!!raw!./popover_anchor_position');
const popoverAnchorPositionHtml = renderToHtml(PopoverAnchorPosition);
import PopoverBodyClassName from './popover_body_class_name';
const popoverBodyClassNameSource = require('!!raw!./popover_body_class_name');
const popoverBodyClassNameHtml = renderToHtml(PopoverBodyClassName);
import PopoverPanelClassName from './popover_panel_class_name';
const popoverPanelClassNameSource = require('!!raw!./popover_panel_class_name');
const popoverPanelClassNameHtml = renderToHtml(PopoverPanelClassName);
import PopoverWithTitle from './popover_with_title';
const popoverWithTitleSource = require('!!raw!./popover_with_title');
const popoverWithTitleHtml = renderToHtml(PopoverWithTitle);
export default props => (
<GuidePage title={props.route.name}>
@ -43,6 +47,28 @@ export default props => (
</GuideDemo>
</GuideSection>
<GuideSection
title="Popover with title"
source={[{
type: GuideSectionTypes.JS,
code: popoverWithTitleSource,
}, {
type: GuideSectionTypes.HTML,
code: popoverWithTitleHtml,
}]}
>
<GuideText>
Popovers often have need for titling. This can be applied through
a prop or used separately as its own component
KuiPopoverTitle nested somwhere in the child
prop.
</GuideText>
<GuideDemo>
<PopoverWithTitle />
</GuideDemo>
</GuideSection>
<GuideSection
title="Anchor position"
source={[{
@ -53,23 +79,35 @@ export default props => (
code: popoverAnchorPositionHtml,
}]}
>
<GuideText>
The alignment and arrow on your popover can be set with
the anchorPostion prop.
</GuideText>
<GuideDemo>
<PopoverAnchorPosition />
</GuideDemo>
</GuideSection>
<GuideSection
title="Body class name"
title="Panel class name and padding size"
source={[{
type: GuideSectionTypes.JS,
code: popoverBodyClassNameSource,
code: popoverPanelClassNameSource,
}, {
type: GuideSectionTypes.HTML,
code: popoverBodyClassNameHtml,
code: popoverPanelClassNameHtml,
}]}
>
<GuideText>
Use the panelPaddingSize prop to adjust the padding
on the panel within the panel. Use the panelClassName
prop to pass a custom class to the panel.
inside a popover.
</GuideText>
<GuideDemo>
<PopoverBodyClassName />
<PopoverPanelClassName />
</GuideDemo>
</GuideSection>
</GuidePage>

View file

@ -0,0 +1,48 @@
import React, {
Component,
} from 'react';
import {
KuiPopover,
KuiButton,
} from '../../../../components';
export default class extends Component {
constructor(props) {
super(props);
this.state = {
isPopoverOpen: false,
};
}
onButtonClick() {
this.setState({
isPopoverOpen: !this.state.isPopoverOpen,
});
}
closePopover() {
this.setState({
isPopoverOpen: false,
});
}
render() {
return (
<KuiPopover
button={(
<KuiButton buttonType="basic" onClick={this.onButtonClick.bind(this)}>
Turn padding off and apply a custom class
</KuiButton>
)}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover.bind(this)}
panelClassName="yourClassNameHere"
panelPaddingSize="none"
>
This should have no padding, and if you inspect, also a custom class.
</KuiPopover>
);
}
}

View file

@ -0,0 +1,59 @@
import React, {
Component,
} from 'react';
import {
KuiPopover,
KuiPopoverTitle,
KuiButton,
} from '../../../../components';
export default class extends Component {
constructor(props) {
super(props);
this.state = {
isPopoverOpen: false,
};
}
onButtonClick() {
this.setState({
isPopoverOpen: !this.state.isPopoverOpen,
});
}
closePopover() {
this.setState({
isPopoverOpen: false,
});
}
render() {
const button = (
<KuiButton
buttonType="basic"
onClick={this.onButtonClick.bind(this)}
>
Show popover with Title
</KuiButton>
);
return (
<KuiPopover
button={button}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover.bind(this)}
panelPaddingSize="none"
withTitle
>
<div style={{ width: '300px' }}>
<KuiPopoverTitle>Hello, I&rsquo;m a popover title</KuiPopoverTitle>
<p className="kuiText" style={{ padding: 20 }}>
Popover content that&rsquo;s wider than the default width
</p>
</div>
</KuiPopover>
);
}
}

View file

@ -0,0 +1,205 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`KuiContextMenu is rendered 1`] = `
<div
aria-label="aria-label"
class="kuiContextMenu testClass1 testClass2"
data-test-subj="test subject string"
/>
`;
exports[`KuiContextMenu props idToPanelMap and initialPanelId renders the referenced panel 1`] = `
<div
class="kuiContextMenu"
>
<div
class="kuiContextMenuPanel kuiContextMenu__panel"
>
<button
class="kuiContextMenuPanelTitle"
data-test-subj="contextMenuPanelTitleButton"
>
<span
class="kuiContextMenu__itemLayout"
>
<span
class="kuiContextMenu__icon kuiIcon fa-angle-left"
/>
<span
class="kuiContextMenu__text"
>
2
</span>
</span>
</button>
<div>
2
</div>
</div>
</div>
`;
exports[`KuiContextMenu props idToPreviousPanelIdMap allows you to click the title button to go back to the previous panel 1`] = `
<div
class="kuiContextMenu"
style="height: 0px;"
>
<div
class="kuiContextMenuPanel kuiContextMenu__panel"
>
<button
class="kuiContextMenuPanelTitle"
data-test-subj="contextMenuPanelTitleButton"
>
<span
class="kuiContextMenu__itemLayout"
>
<span
class="kuiContextMenu__icon kuiIcon fa-angle-left"
/>
<span
class="kuiContextMenu__text"
>
2
</span>
</span>
</button>
<div>
2
</div>
</div>
</div>
`;
exports[`KuiContextMenu props idToPreviousPanelIdMap allows you to click the title button to go back to the previous panel 2`] = `
<div
class="kuiContextMenu"
style="height: 0px;"
>
<div
class="kuiContextMenuPanel kuiContextMenu__panel kuiContextMenuPanel-txOutRight"
>
<button
class="kuiContextMenuPanelTitle"
data-test-subj="contextMenuPanelTitleButton"
>
<span
class="kuiContextMenu__itemLayout"
>
<span
class="kuiContextMenu__icon kuiIcon fa-angle-left"
/>
<span
class="kuiContextMenu__text"
>
2
</span>
</span>
</button>
<div>
2
</div>
</div>
<div
class="kuiContextMenuPanel kuiContextMenu__panel kuiContextMenuPanel-txInRight"
>
<button
class="kuiContextMenuPanelTitle"
data-test-subj="contextMenuPanelTitleButton"
>
<span
class="kuiContextMenu__itemLayout"
>
<span
class="kuiContextMenu__icon kuiIcon fa-angle-left"
/>
<span
class="kuiContextMenu__text"
>
1
</span>
</span>
</button>
<button
class="kuiContextMenuItem"
>
<span
class="kuiContextMenu__itemLayout"
>
<span
class="kuiContextMenuItem__text"
>
2a
</span>
<span
class="kuiContextMenu__arrow kuiIcon fa-angle-right"
/>
</span>
</button>
<button
class="kuiContextMenuItem"
>
<span
class="kuiContextMenu__itemLayout"
>
<span
class="kuiContextMenuItem__text"
>
2b
</span>
<span
class="kuiContextMenu__arrow kuiIcon fa-angle-right"
/>
</span>
</button>
<button
class="kuiContextMenuItem"
>
<span
class="kuiContextMenu__itemLayout"
>
<span
class="kuiContextMenuItem__text"
>
2c
</span>
<span
class="kuiContextMenu__arrow kuiIcon fa-angle-right"
/>
</span>
</button>
</div>
</div>
`;
exports[`KuiContextMenu props isVisible causes the first panel to be shown when it becomes true 1`] = `
<div
class="kuiContextMenu"
style="height: 0px;"
>
<div
class="kuiContextMenuPanel kuiContextMenu__panel"
>
<button
class="kuiContextMenuPanelTitle"
data-test-subj="contextMenuPanelTitleButton"
>
<span
class="kuiContextMenu__itemLayout"
>
<span
class="kuiContextMenu__icon kuiIcon fa-angle-left"
/>
<span
class="kuiContextMenu__text"
>
2
</span>
</span>
</button>
<div>
2
</div>
</div>
</div>
`;

View file

@ -0,0 +1,53 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`KuiContextMenuItem is rendered 1`] = `
<button
aria-label="aria-label"
class="kuiContextMenuItem testClass1 testClass2"
data-test-subj="test subject string"
>
<span
class="kuiContextMenu__itemLayout"
>
<span
class="kuiContextMenuItem__text"
>
Hello
</span>
</span>
</button>
`;
exports[`KuiContextMenuItem props hasPanel is rendered 1`] = `
<button
class="kuiContextMenuItem"
>
<span
class="kuiContextMenu__itemLayout"
>
<span
class="kuiContextMenuItem__text"
/>
<span
class="kuiContextMenu__arrow kuiIcon fa-angle-right"
/>
</span>
</button>
`;
exports[`KuiContextMenuItem props icon is rendered 1`] = `
<button
class="kuiContextMenuItem"
>
<span
class="kuiContextMenu__itemLayout"
>
<span
class="kuiIcon fa-user kuiContextMenu__icon"
/>
<span
class="kuiContextMenuItem__text"
/>
</span>
</button>
`;

View file

@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`KuiContextMenuPanel is rendered 1`] = `
<div
aria-label="aria-label"
class="kuiContextMenuPanel testClass1 testClass2"
data-test-subj="test subject string"
>
Hello
</div>
`;
exports[`KuiContextMenuPanel props onClose renders a button as a title 1`] = `
<div
class="kuiContextMenuPanel"
>
<button
class="kuiContextMenuPanelTitle"
data-test-subj="contextMenuPanelTitleButton"
>
<span
class="kuiContextMenu__itemLayout"
>
<span
class="kuiContextMenu__icon kuiIcon fa-angle-left"
/>
<span
class="kuiContextMenu__text"
>
Title
</span>
</span>
</button>
</div>
`;
exports[`KuiContextMenuPanel props title is rendered 1`] = `
<div
class="kuiContextMenuPanel"
>
<div
class="kuiPopoverTitle"
>
<span
class="kuiContextMenu__itemLayout"
>
Title
</span>
</div>
</div>
`;
exports[`KuiContextMenuPanel props transitionDirection next with transitionType in is rendered 1`] = `
<div
class="kuiContextMenuPanel kuiContextMenuPanel-txInLeft"
/>
`;
exports[`KuiContextMenuPanel props transitionDirection next with transitionType out is rendered 1`] = `
<div
class="kuiContextMenuPanel kuiContextMenuPanel-txOutLeft"
/>
`;
exports[`KuiContextMenuPanel props transitionDirection previous with transitionType in is rendered 1`] = `
<div
class="kuiContextMenuPanel kuiContextMenuPanel-txInRight"
/>
`;
exports[`KuiContextMenuPanel props transitionDirection previous with transitionType out is rendered 1`] = `
<div
class="kuiContextMenuPanel kuiContextMenuPanel-txOutRight"
/>
`;

View file

@ -0,0 +1,26 @@
$kuiContextMenuWidth: $kuiSize * 16;
.kuiContextMenu {
width: $kuiContextMenuWidth;
position: relative;
overflow: hidden;
transition: height $kuiAnimSpeedFast $kuiAnimSlightResistance;
border-radius: $kuiBorderRadius;
.kuiContextMenu__content {
padding: $kuiSizeS;
}
}
.kuiContextMenu__panel {
position: absolute;
}
.kuiContextMenu__icon {
margin-right: $kuiSizeS;
}
.kuiContextMenu__itemLayout {
display: flex;
align-items: center;
}

View file

@ -0,0 +1,50 @@
/**
* 1. Button reset.
* 2. Ensure buttons stack.
*/
.kuiContextMenuItem {
appearance: none; /* 1 */
background-color: transparent; /* 1 */
font-size: $kuiFontSize; /* 1 */
border: none; /* 1 */
cursor: pointer; /* 1 */
display: block; /* 2 */
padding: 12px;
width: 100%;
text-align: left;
color: $kuiTextColor;
&:hover, &:focus {
.kuiContextMenuItem__text {
text-decoration: underline;
}
}
/**
* 1. Overwrite default style.
*/
&:focus {
background-color: $kuiFocusAltBackgroundColor;
box-shadow: none; /* 1 */
@include darkTheme {
background-color: transparent;
}
}
@include darkTheme {
color: #ffffff;
}
}
.kuiContextMenuItem__inner {
display: flex;
}
.kuiContextMenuItem__text {
flex-grow: 1;
}
.kuiContextMenuItem__arrow {
align-self: flex-end;
}

View file

@ -0,0 +1,104 @@
@import '../popover/mixins';
.kuiContextMenuPanel {
width: 100%;
visibility: visible;
background-color: #ffffff;
&.kuiContextMenuPanel-txInLeft {
pointer-events: none;
animation: kuiContextMenuPanelTxInLeft $kuiAnimSpeedNormal $kuiAnimSlightResistance;
}
&.kuiContextMenuPanel-txOutLeft {
pointer-events: none;
animation: kuiContextMenuPanelTxOutLeft $kuiAnimSpeedNormal $kuiAnimSlightResistance;
}
&.kuiContextMenuPanel-txInRight {
pointer-events: none;
animation: kuiContextMenuPanelTxInRight $kuiAnimSpeedNormal $kuiAnimSlightResistance;
}
&.kuiContextMenuPanel-txOutRight {
pointer-events: none;
animation: kuiContextMenuPanelTxOutRight $kuiAnimSpeedNormal $kuiAnimSlightResistance;
}
@include darkTheme {
background-color: $kuiBackgroundColor--darkTheme;
}
}
.kuiContextMenuPanel--next {
transform: translateX($kuiContextMenuWidth);
visibility: hidden;
}
.kuiContextMenuPanel--previous {
transform: translateX(-$kuiContextMenuWidth);
visibility: hidden;
}
/**
* 1. Button reset.
*/
.kuiContextMenuPanelTitle {
appearance: none; /* 1 */
border: none; /* 1 */
cursor: pointer; /* 1 */
@include kuiPopoverTitle;
width: 100%;
text-align: left;
&:hover, &:focus {
.kuiContextMenu__text {
text-decoration: underline;
}
}
/**
* 1. Overwrite default style.
*/
&:focus {
box-shadow: none; /* 1 */
}
}
@keyframes kuiContextMenuPanelTxInLeft {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(0);
}
}
@keyframes kuiContextMenuPanelTxOutLeft {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-100%);
}
}
@keyframes kuiContextMenuPanelTxInRight {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(0);
}
}
@keyframes kuiContextMenuPanelTxOutRight {
0% {
transform: translateX(0);
}
100% {
transform: translateX(100%);
}
}

View file

@ -0,0 +1,3 @@
@import 'context_menu';
@import 'context_menu_panel';
@import 'context_menu_item';

View file

@ -0,0 +1,269 @@
import React, {
Component,
} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { KuiContextMenuPanel } from './context_menu_panel';
import { KuiContextMenuItem } from './context_menu_item';
function mapIdsToPanels(panels) {
const map = {};
panels.forEach(panel => {
map[panel.id] = panel;
});
return map;
}
function mapIdsToPreviousPanels(panels) {
const idToPreviousPanelIdMap = {};
panels.forEach(panel => {
if (Array.isArray(panel.items)) {
panel.items.forEach(item => {
const isCloseable = item.panel !== undefined;
if (isCloseable) {
idToPreviousPanelIdMap[item.panel] = panel.id;
}
});
}
});
return idToPreviousPanelIdMap;
}
function mapPanelItemsToPanels(panels) {
const idAndItemIndexToPanelIdMap = {};
panels.forEach(panel => {
idAndItemIndexToPanelIdMap[panel.id] = {};
if (panel.items) {
panel.items.forEach((item, index) => {
if (item.panel) {
idAndItemIndexToPanelIdMap[panel.id][index] = item.panel;
}
});
}
});
return idAndItemIndexToPanelIdMap;
}
export class KuiContextMenu extends Component {
static propTypes = {
className: PropTypes.string,
panels: PropTypes.array,
initialPanelId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
isVisible: PropTypes.bool.isRequired,
}
static defaultProps = {
panels: [],
isVisible: true,
}
constructor(props) {
super(props);
this.idToPanelMap = {};
this.idToPreviousPanelIdMap = {};
this.idAndItemIndexToPanelIdMap = {};
this.state = {
height: undefined,
outgoingPanelId: undefined,
incomingPanelId: props.initialPanelId,
transitionDirection: undefined,
isOutgoingPanelVisible: false,
focusedItemIndex: undefined,
};
}
hasPreviousPanel = panelId => {
const previousPanelId = this.idToPreviousPanelIdMap[panelId];
return typeof previousPanelId !== 'undefined';
};
showPanel(panelId, direction) {
this.setState({
outgoingPanelId: this.state.incomingPanelId,
incomingPanelId: panelId,
transitionDirection: direction,
isOutgoingPanelVisible: true,
});
}
showNextPanel = itemIndex => {
const nextPanelId = this.idAndItemIndexToPanelIdMap[this.state.incomingPanelId][itemIndex];
if (nextPanelId) {
this.showPanel(nextPanelId, 'next');
}
};
showPreviousPanel = () => {
// If there's a previous panel, then we can close the current panel to go back to it.
if (this.hasPreviousPanel(this.state.incomingPanelId)) {
const previousPanelId = this.idToPreviousPanelIdMap[this.state.incomingPanelId];
// Set focus on the item which shows the panel we're leaving.
const previousPanel = this.idToPanelMap[previousPanelId];
const focusedItemIndex = previousPanel.items.findIndex(
item => item.panel === this.state.incomingPanelId
);
if (focusedItemIndex !== -1) {
this.setState({
focusedItemIndex,
});
}
this.showPanel(previousPanelId, 'previous');
}
};
onIncomingPanelHeightChange = height => {
this.setState({
height,
});
}
onOutGoingPanelTransitionComplete = () => {
this.setState({
isOutgoingPanelVisible: false,
});
}
updatePanelMaps(panels) {
this.idToPanelMap = mapIdsToPanels(panels);
this.idToPreviousPanelIdMap = mapIdsToPreviousPanels(panels);
this.idAndItemIndexToPanelIdMap = mapPanelItemsToPanels(panels);
}
componentWillMount() {
this.updatePanelMaps(this.props.panels);
}
componentWillReceiveProps(nextProps) {
// If the user is opening the context menu, reset the state.
if (nextProps.isVisible && !this.props.isVisible) {
this.setState({
outgoingPanelId: undefined,
incomingPanelId: nextProps.initialPanelId,
transitionDirection: undefined,
focusedItemIndex: undefined,
});
}
if (nextProps.panels !== this.props.panels) {
this.updatePanelMaps(nextProps.panels);
}
}
renderItems(items = []) {
return items.map((item, index) => {
const {
panel,
name,
icon,
onClick,
...rest,
} = item;
const onClickHandler = panel
? () => {
// This component is commonly wrapped in a KuiOutsideClickDetector, which means we'll
// need to wait for that logic to complete before re-rendering the DOM via showPanel.
window.requestAnimationFrame(() => {
if (onClick) onClick();
this.showNextPanel(index);
});
} : onClick;
return (
<KuiContextMenuItem
key={name}
icon={icon}
onClick={onClickHandler}
hasPanel={Boolean(panel)}
{...rest}
>
{name}
</KuiContextMenuItem>
);
});
}
renderPanel(panelId, transitionType) {
const panel = this.idToPanelMap[panelId];
if (!panel) {
return;
}
// As above, we need to wait for KuiOutsideClickDetector to complete its logic before
// re-rendering via showPanel.
let onClose;
if (this.hasPreviousPanel(panelId)) {
onClose = () => window.requestAnimationFrame(this.showPreviousPanel);
}
return (
<KuiContextMenuPanel
key={panelId}
className="kuiContextMenu__panel"
onHeightChange={(transitionType === 'in') ? this.onIncomingPanelHeightChange : undefined}
onTransitionComplete={(transitionType === 'out') ? this.onOutGoingPanelTransitionComplete : undefined}
title={panel.title}
onClose={onClose}
transitionType={this.state.isOutgoingPanelVisible ? transitionType : undefined}
transitionDirection={this.state.isOutgoingPanelVisible ? this.state.transitionDirection : undefined}
hasFocus={transitionType === 'in'}
items={this.renderItems(panel.items)}
focusedItemIndex={
// Set focus on the item which shows the panel we're leaving.
transitionType === 'in' && this.state.transitionDirection === 'previous'
? this.state.focusedItemIndex
: undefined
}
showNextPanel={this.showNextPanel}
showPreviousPanel={this.showPreviousPanel}
>
{panel.content}
</KuiContextMenuPanel>
);
}
render() {
const {
panels, // eslint-disable-line no-unused-vars
className,
initialPanelId, // eslint-disable-line no-unused-vars
isVisible, // eslint-disable-line no-unused-vars
...rest,
} = this.props;
const incomingPanel = this.renderPanel(this.state.incomingPanelId, 'in');
let outgoingPanel;
if (this.state.isOutgoingPanelVisible) {
outgoingPanel = this.renderPanel(this.state.outgoingPanelId, 'out');
}
const classes = classNames('kuiContextMenu', className);
return (
<div
ref={node => { this.menu = node; }}
className={classes}
style={{ height: this.state.height }}
{...rest}
>
{outgoingPanel}
{incomingPanel}
</div>
);
}
}

View file

@ -0,0 +1,115 @@
import React from 'react';
import { render, mount } from 'enzyme';
import {
requiredProps,
takeMountedSnapshot,
} from '../../test';
import { KuiContextMenu } from './context_menu';
const panel2 = {
id: 2,
title: '2',
content: <div>2</div>,
};
const panel1 = {
id: 1,
title: '1',
items: [{
name: '2a',
panel: 2,
}, {
name: '2b',
panel: 2,
}, {
name: '2c',
panel: 2,
}],
};
const panel0 = {
id: 0,
title: '0',
items: [{
name: '1',
panel: 1,
}],
};
const panels = [
panel0,
panel1,
panel2,
];
describe('KuiContextMenu', () => {
test('is rendered', () => {
const component = render(
<KuiContextMenu {...requiredProps} />
);
expect(component)
.toMatchSnapshot();
});
describe('props', () => {
describe('idToPanelMap and initialPanelId', () => {
it('renders the referenced panel', () => {
const component = render(
<KuiContextMenu
panels={panels}
initialPanelId={2}
isVisible
/>
);
expect(component)
.toMatchSnapshot();
});
});
describe('idToPreviousPanelIdMap', () => {
it('allows you to click the title button to go back to the previous panel', () => {
const component = mount(
<KuiContextMenu
panels={panels}
initialPanelId={2}
isVisible
/>
);
expect(takeMountedSnapshot(component))
.toMatchSnapshot();
// Navigate to a different panel.
component.find('[data-test-subj="contextMenuPanelTitleButton"]').simulate('click');
expect(takeMountedSnapshot(component))
.toMatchSnapshot();
});
});
describe('isVisible', () => {
it('causes the first panel to be shown when it becomes true', () => {
const component = mount(
<KuiContextMenu
panels={panels}
initialPanelId={2}
isVisible
/>
);
// Navigate to a different panel.
component.find('[data-test-subj="contextMenuPanelTitleButton"]').simulate('click');
// Hide and then show the menu to reset the panel to the initial one.
component.setProps({ isVisible: false });
component.setProps({ isVisible: true });
expect(takeMountedSnapshot(component))
.toMatchSnapshot();
});
});
});
});

View file

@ -0,0 +1,60 @@
import React, {
cloneElement,
Component,
} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export class KuiContextMenuItem extends Component {
static propTypes = {
children: PropTypes.node,
className: PropTypes.string,
icon: PropTypes.element,
onClick: PropTypes.func,
hasPanel: PropTypes.bool,
buttonRef: PropTypes.func,
}
render() {
const {
children,
className,
hasPanel,
icon,
buttonRef,
...rest,
} = this.props;
let iconInstance;
if (icon) {
iconInstance = cloneElement(icon, {
className: classNames(icon.props.className, 'kuiContextMenu__icon'),
});
}
let arrow;
if (hasPanel) {
arrow = <span className="kuiContextMenu__arrow kuiIcon fa-angle-right" />;
}
const classes = classNames('kuiContextMenuItem', className);
return (
<button
className={classes}
ref={buttonRef}
{...rest}
>
<span className="kuiContextMenu__itemLayout">
{iconInstance}
<span className="kuiContextMenuItem__text">
{children}
</span>
{arrow}
</span>
</button>
);
}
}

View file

@ -0,0 +1,67 @@
import React from 'react';
import { render, shallow } from 'enzyme';
import sinon from 'sinon';
import { requiredProps } from '../../test/required_props';
import { KuiContextMenuItem } from './context_menu_item';
describe('KuiContextMenuItem', () => {
test('is rendered', () => {
const component = render(
<KuiContextMenuItem {...requiredProps}>
Hello
</KuiContextMenuItem>
);
expect(component)
.toMatchSnapshot();
});
describe('props', () => {
describe('icon', () => {
test('is rendered', () => {
const component = render(
<KuiContextMenuItem icon={<span className="kuiIcon fa-user" />} />
);
expect(component)
.toMatchSnapshot();
});
});
describe('onClick', () => {
test(`isn't called upon instantiation`, () => {
const onClickHandler = sinon.stub();
shallow(
<KuiContextMenuItem onClick={onClickHandler} />
);
sinon.assert.notCalled(onClickHandler);
});
test('is called when the item is clicked', () => {
const onClickHandler = sinon.stub();
const component = shallow(
<KuiContextMenuItem onClick={onClickHandler} />
);
component.simulate('click');
sinon.assert.calledOnce(onClickHandler);
});
});
describe('hasPanel', () => {
test('is rendered', () => {
const component = render(
<KuiContextMenuItem hasPanel />
);
expect(component)
.toMatchSnapshot();
});
});
});
});

View file

@ -0,0 +1,296 @@
import React, {
cloneElement,
Component,
} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import tabbable from 'tabbable';
import { KuiPopoverTitle } from '../../components';
import { cascadingMenuKeyCodes } from '../../services';
const transitionDirectionAndTypeToClassNameMap = {
next: {
in: 'kuiContextMenuPanel-txInLeft',
out: 'kuiContextMenuPanel-txOutLeft',
},
previous: {
in: 'kuiContextMenuPanel-txInRight',
out: 'kuiContextMenuPanel-txOutRight',
},
};
export class KuiContextMenuPanel extends Component {
static propTypes = {
children: PropTypes.node,
className: PropTypes.string,
title: PropTypes.string,
onClose: PropTypes.func,
onHeightChange: PropTypes.func,
transitionType: PropTypes.oneOf(['in', 'out']),
transitionDirection: PropTypes.oneOf(['next', 'previous']),
onTransitionComplete: PropTypes.func,
hasFocus: PropTypes.bool,
items: PropTypes.array,
showNextPanel: PropTypes.func,
showPreviousPanel: PropTypes.func,
focusedItemIndex: PropTypes.number,
}
static defaultProps = {
hasFocus: true,
items: [],
}
constructor(props) {
super(props);
this.menuItems = [];
this.state = {
pressedArrowDirection: undefined,
isTransitioning: Boolean(props.transitionType),
};
}
onKeyDown = e => {
// If this panel contains items you can use the left arrow key to go back at any time.
// But if it doesn't contain items, then you have to focus on the back button specifically,
// since there could be content inside the panel which requires use of the left arrow key,
// e.g. text inputs.
if (this.props.items.length || document.activeElement === this.backButton) {
if (e.keyCode === cascadingMenuKeyCodes.LEFT) {
if (this.props.showPreviousPanel) {
this.props.showPreviousPanel();
}
}
}
if (this.props.items.length) {
switch (e.keyCode) {
case cascadingMenuKeyCodes.TAB:
// Normal tabbing doesn't work within panels with items.
e.preventDefault();
break;
case cascadingMenuKeyCodes.UP:
e.preventDefault();
this.setState({ pressedArrowDirection: 'up' });
break;
case cascadingMenuKeyCodes.DOWN:
e.preventDefault();
this.setState({ pressedArrowDirection: 'down' });
break;
case cascadingMenuKeyCodes.RIGHT:
if (this.props.showNextPanel) {
this.props.showNextPanel(this.getFocusedMenuItemIndex());
}
break;
default:
break;
}
}
};
isMenuItemFocused() {
const indexOfActiveElement = this.menuItems.indexOf(document.activeElement);
return indexOfActiveElement !== -1;
}
getFocusedMenuItemIndex() {
return this.menuItems.indexOf(document.activeElement);
}
updateFocusedMenuItem() {
// If this panel isn't active, don't focus any items.
if (!this.props.hasFocus) {
if (this.isMenuItemFocused()) {
document.activeElement.blur();
}
return;
}
// Setting focus while transitioning causes the animation to glitch, so we have to wait
// until it's finished before we focus anything.
if (this.state.isTransitioning) {
return;
}
// If we're active, but nothing is focused then we should focus the first item.
if (!this.isMenuItemFocused()) {
if (this.props.focusedItemIndex !== undefined) {
this.menuItems[this.props.focusedItemIndex].focus();
return;
}
if (this.menuItems.length !== 0) {
this.menuItems[0].focus();
return;
}
// Focus first tabbable item.
const tabbableItems = tabbable(this.panel);
if (tabbableItems.length) {
tabbableItems[0].focus();
}
return;
}
// Update focused state based on arrow key navigation.
if (this.state.pressedArrowDirection) {
const indexOfActiveElement = this.getFocusedMenuItemIndex();
let nextFocusedMenuItemIndex;
switch (this.state.pressedArrowDirection) {
case 'up':
nextFocusedMenuItemIndex =
(indexOfActiveElement - 1) !== -1
? indexOfActiveElement - 1
: this.menuItems.length - 1;
break;
case 'down':
nextFocusedMenuItemIndex =
(indexOfActiveElement + 1) !== this.menuItems.length
? indexOfActiveElement + 1
: 0;
break;
default:
break;
}
this.menuItems[nextFocusedMenuItemIndex].focus();
this.setState({ pressedArrowDirection: undefined });
}
}
onTransitionComplete = () => {
this.setState({
isTransitioning: false,
});
if (this.props.onTransitionComplete) {
this.props.onTransitionComplete();
}
}
componentWillReceiveProps(nextProps) {
// Clear refs to menuItems if we're getting new ones.
if (nextProps.items !== this.props.items) {
this.menuItems = [];
}
if (nextProps.transitionType) {
this.setState({
isTransitioning: true,
});
}
}
componentDidMount() {
this.updateFocusedMenuItem();
}
componentDidUpdate() {
this.updateFocusedMenuItem();
}
componentWillUnmount() {
this.panel.removeEventListener('animationend', this.onTransitionComplete);
}
menuItemRef = (index, node) => {
// There's a weird bug where if you navigate to a panel without items, then this callback
// is still invoked, so we have to do a truthiness check.
if (node) {
// Store all menu items.
this.menuItems[index] = node;
}
};
panelRef = node => {
if (node) {
this.panel = node;
this.panel.addEventListener('animationend', this.onTransitionComplete);
if (this.props.onHeightChange) {
this.props.onHeightChange(node.clientHeight);
}
}
}
render() {
const {
children,
className,
onClose,
title,
onHeightChange, // eslint-disable-line no-unused-vars
transitionType,
transitionDirection,
onTransitionComplete, // eslint-disable-line no-unused-vars
hasFocus, // eslint-disable-line no-unused-vars
items,
focusedItemIndex, // eslint-disable-line no-unused-vars
showNextPanel, // eslint-disable-line no-unused-vars
showPreviousPanel, // eslint-disable-line no-unused-vars
...rest,
} = this.props;
let panelTitle;
if (title) {
if (Boolean(onClose)) {
panelTitle = (
<button
className="kuiContextMenuPanelTitle"
onClick={onClose}
ref={node => { this.backButton = node; }}
data-test-subj="contextMenuPanelTitleButton"
>
<span className="kuiContextMenu__itemLayout">
<span className="kuiContextMenu__icon kuiIcon fa-angle-left" />
<span className="kuiContextMenu__text">
{title}
</span>
</span>
</button>
);
} else {
panelTitle = (
<KuiPopoverTitle>
<span className="kuiContextMenu__itemLayout">
{title}
</span>
</KuiPopoverTitle>
);
}
}
const classes = classNames('kuiContextMenuPanel', className, (
this.state.isTransitioning && transitionDirectionAndTypeToClassNameMap[transitionDirection]
? transitionDirectionAndTypeToClassNameMap[transitionDirection][transitionType]
: undefined
));
const content = items.length
? items.map((MenuItem, index) => cloneElement(MenuItem, {
buttonRef: this.menuItemRef.bind(this, index),
}))
: children;
return (
<div
ref={this.panelRef}
className={classes}
onKeyDown={this.onKeyDown}
{...rest}
>
{panelTitle}
{content}
</div>
);
}
}

View file

@ -0,0 +1,244 @@
import React from 'react';
import { render, shallow, mount } from 'enzyme';
import sinon from 'sinon';
import { requiredProps } from '../../test/required_props';
import {
KuiContextMenuPanel,
} from './context_menu_panel';
import {
KuiContextMenuItem,
} from './context_menu_item';
import { keyCodes } from '../../services';
const items = [(
<KuiContextMenuItem
key="A"
data-test-subj="itemA"
>
Option A
</KuiContextMenuItem>
), (
<KuiContextMenuItem
key="B"
data-test-subj="itemB"
>
Option B
</KuiContextMenuItem>
), (
<KuiContextMenuItem
key="C"
data-test-subj="itemC"
>
Option C
</KuiContextMenuItem>
)];
describe('KuiContextMenuPanel', () => {
test('is rendered', () => {
const component = render(
<KuiContextMenuPanel {...requiredProps}>
Hello
</KuiContextMenuPanel>
);
expect(component)
.toMatchSnapshot();
});
describe('props', () => {
describe('title', () => {
test('is rendered', () => {
const component = render(
<KuiContextMenuPanel title="Title" />
);
expect(component)
.toMatchSnapshot();
});
});
describe('onClose', () => {
test('renders a button as a title', () => {
const component = render(
<KuiContextMenuPanel title="Title" onClose={() =>{}} />
);
expect(component)
.toMatchSnapshot();
});
test(`isn't called upon instantiation`, () => {
const onCloseHandler = sinon.stub();
shallow(
<KuiContextMenuPanel title="Title" onClose={onCloseHandler} />
);
sinon.assert.notCalled(onCloseHandler);
});
test('is called when the title is clicked', () => {
const onCloseHandler = sinon.stub();
const component = shallow(
<KuiContextMenuPanel title="Title" onClose={onCloseHandler} />
);
component.find('button').simulate('click');
sinon.assert.calledOnce(onCloseHandler);
});
});
describe('onHeightChange', () => {
it('is called with a height value', () => {
const onHeightChange = sinon.stub();
mount(
<KuiContextMenuPanel onHeightChange={onHeightChange} />
);
sinon.assert.calledWith(onHeightChange, 0);
});
});
describe('transitionDirection', () => {
describe('next', () => {
describe('with transitionType', () => {
describe('in', () => {
test('is rendered', () => {
const component = render(
<KuiContextMenuPanel transitionDirection="next" transitionType="in" />
);
expect(component)
.toMatchSnapshot();
});
});
describe('out', () => {
test('is rendered', () => {
const component = render(
<KuiContextMenuPanel transitionDirection="next" transitionType="out" />
);
expect(component)
.toMatchSnapshot();
});
});
});
});
describe('previous', () => {
describe('with transitionType', () => {
describe('in', () => {
test('is rendered', () => {
const component = render(
<KuiContextMenuPanel transitionDirection="previous" transitionType="in" />
);
expect(component)
.toMatchSnapshot();
});
});
describe('out', () => {
test('is rendered', () => {
const component = render(
<KuiContextMenuPanel transitionDirection="previous" transitionType="out" />
);
expect(component)
.toMatchSnapshot();
});
});
});
});
});
describe('focusedItemIndex', () => {
it('sets focus on the item occupying that index', () => {
const component = mount(
<KuiContextMenuPanel
items={items}
focusedItemIndex={1}
/>
);
expect(
component.find('[data-test-subj="itemB"]').matchesElement(document.activeElement)
).toBe(true);
});
});
});
describe('behavior', () => {
describe('focus', () => {
it('is set on the first focusable element by default, if there are no items', () => {
const component = mount(
<KuiContextMenuPanel>
<button data-test-subj="button" />
</KuiContextMenuPanel>
);
expect(
component.find('[data-test-subj="button"]').matchesElement(document.activeElement)
).toBe(true);
});
});
describe('keyboard navigation of items', () => {
let component;
let showNextPanelHandler;
let showPreviousPanelHandler;
beforeEach(() => {
showNextPanelHandler = sinon.stub();
showPreviousPanelHandler = sinon.stub();
component = mount(
<KuiContextMenuPanel
items={items}
showNextPanel={showNextPanelHandler}
showPreviousPanel={showPreviousPanelHandler}
/>
);
});
it('focuses the first menu item by default, if there are items', () => {
expect(
component.find('[data-test-subj="itemA"]').matchesElement(document.activeElement)
).toBe(true);
});
it('down arrow key focuses the next menu item', () => {
component.simulate('keydown', { keyCode: keyCodes.DOWN });
expect(
component.find('[data-test-subj="itemB"]').matchesElement(document.activeElement)
).toBe(true);
});
it('up arrow key focuses the previous menu item', () => {
component.simulate('keydown', { keyCode: keyCodes.UP });
expect(
component.find('[data-test-subj="itemC"]').matchesElement(document.activeElement)
).toBe(true);
});
it('right arrow key shows next panel', () => {
component.simulate('keydown', { keyCode: keyCodes.RIGHT });
sinon.assert.calledWith(showNextPanelHandler, 0);
});
it('left arrow key shows previous panel', () => {
component.simulate('keydown', { keyCode: keyCodes.LEFT });
sinon.assert.calledOnce(showPreviousPanelHandler);
});
});
});
});

View file

@ -0,0 +1,11 @@
export {
KuiContextMenu,
} from './context_menu';
export {
KuiContextMenuPanel,
} from './context_menu_panel';
export {
KuiContextMenuItem,
} from './context_menu_item';

View file

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`KuiExpression Props children is rendered 1`] = `
<div
class="kuiExpression"
>
some expression
</div>
`;
exports[`KuiExpression renders 1`] = `
<div
aria-label="aria-label"
class="kuiExpression testClass1 testClass2"
data-test-subj="test subject string"
/>
`;

View file

@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`KuiExpressionButton Props isActive false renders inactive 1`] = `
<button
class="kuiExpressionButton"
>
<span
class="kuiExpressionButton__description"
>
the answer is
</span>
<span
class="kuiExpressionButton__value"
>
42
</span>
</button>
`;
exports[`KuiExpressionButton Props isActive true renders active 1`] = `
<button
class="kuiExpressionButton kuiExpressionButton-isActive"
>
<span
class="kuiExpressionButton__description"
>
the answer is
</span>
<span
class="kuiExpressionButton__value"
>
42
</span>
</button>
`;
exports[`KuiExpressionButton renders 1`] = `
<button
aria-label="aria-label"
class="kuiExpressionButton testClass1 testClass2"
data-test-subj="test subject string"
>
<span
class="kuiExpressionButton__description"
>
the answer is
</span>
<span
class="kuiExpressionButton__value"
>
42
</span>
</button>
`;

View file

@ -1,17 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`KuiExpressionItem Props children is rendered 1`] = `
<div
class="kuiExpressionItem"
>
some expression
</div>
`;
exports[`KuiExpressionItem renders 1`] = `
<div
aria-label="aria-label"
class="kuiExpressionItem testClass1 testClass2"
data-test-subj="test subject string"
/>
`;

View file

@ -1,57 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`KuiExpressionItemButton Props isActive false renders inactive 1`] = `
<button
class="kuiExpressionItem__button"
>
<span
class="kuiExpressionItem__buttonDescription"
>
the answer is
</span>
<span
class="kuiExpressionItem__buttonValue"
>
42
</span>
</button>
`;
exports[`KuiExpressionItemButton Props isActive true renders active 1`] = `
<button
class="kuiExpressionItem__button kuiExpressionItem__button--isActive"
>
<span
class="kuiExpressionItem__buttonDescription"
>
the answer is
</span>
<span
class="kuiExpressionItem__buttonValue"
>
42
</span>
</button>
`;
exports[`KuiExpressionItemButton renders 1`] = `
<button
aria-label="aria-label"
class="kuiExpressionItem__button testClass1 testClass2"
data-test-subj="test subject string"
>
<span
class="kuiExpressionItem__buttonDescription"
>
the answer is
</span>
<span
class="kuiExpressionItem__buttonValue"
>
42
</span>
</button>
`;

View file

@ -1,80 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`KuiExpressionItemPopover Props align renders default 1`] = `
<div
class="kuiExpressionItem__popover"
>
<div
class="kuiExpressionItem__popoverTitle"
>
title
</div>
<div
class="kuiExpressionItem__popoverContent"
/>
</div>
`;
exports[`KuiExpressionItemPopover Props align renders the left class 1`] = `
<div
class="kuiExpressionItem__popover"
>
<div
class="kuiExpressionItem__popoverTitle"
>
title
</div>
<div
class="kuiExpressionItem__popoverContent"
/>
</div>
`;
exports[`KuiExpressionItemPopover Props align renders the right class 1`] = `
<div
class="kuiExpressionItem__popover kuiExpressionItem__popover--alignRight"
>
<div
class="kuiExpressionItem__popoverTitle"
>
title
</div>
<div
class="kuiExpressionItem__popoverContent"
/>
</div>
`;
exports[`KuiExpressionItemPopover Props children is rendered 1`] = `
<div
class="kuiExpressionItem__popover"
>
<div
class="kuiExpressionItem__popoverTitle"
>
title
</div>
<div
class="kuiExpressionItem__popoverContent"
>
popover content
</div>
</div>
`;
exports[`KuiExpressionItemPopover renders 1`] = `
<div
aria-label="aria-label"
class="kuiExpressionItem__popover testClass1 testClass2"
data-test-subj="test subject string"
>
<div
class="kuiExpressionItem__popoverTitle"
>
title
</div>
<div
class="kuiExpressionItem__popoverContent"
/>
</div>
`;

View file

@ -1,103 +1,27 @@
.kuiExpressionItem {
display: inline-block;
position: relative;
& + & {
margin-left: 10px;
}
.kuiExpression {
padding: 20px;
white-space: nowrap;
}
.kuiExpressionItem__button {
background-color: transparent;
padding: 5px 0px;
border: none;
border-bottom: dotted 2px $kuiBorderColor;
font-size: $kuiFontSize;
cursor: pointer;
}
.kuiExpressionButton {
background-color: transparent;
padding: 5px 0px;
border: none;
border-bottom: dotted 2px $kuiBorderColor;
font-size: $kuiFontSize;
cursor: pointer;
}
.kuiExpressionItem__buttonDescription {
color: $expressionColorHighlight;
text-transform: uppercase;
}
.kuiExpressionButton__description {
color: $expressionColorHighlight;
text-transform: uppercase;
}
.kuiExpressionItem__buttonValue {
color: $kuiTextColor;
text-transform: lowercase;
}
.kuiExpressionButton__value {
color: $kuiTextColor;
text-transform: lowercase;
}
.kuiExpressionItem__button--isActive {
border-bottom: solid 2px $expressionColorHighlight;
}
.kuiExpressionItem__popover {
position: absolute;
top: calc(100% + 15px);
display: flex;
flex-direction: column;
flex: 1 1 auto;
background-color: white;
border: 1px solid $kuiBorderColor;
border-radius: 6px;
box-shadow: $kuiBoxShadow;
visibility: visible;
opacity: 1;
transform: translateY(-5px) translateZ(0);
transition: transform $kuiAnimSpeedNormal $kuiAnimSlightBounce, opacity $kuiAnimSpeedNormal $kuiAnimSlightBounce;
// 1. Angulars ng-hide uses display: none. To use animations we need to use visibility instead.
&.ng-hide {
display: block !important; // 1
visibility: hidden;
opacity: 0;
transform: translateY(0px) translateZ(0);
}
&:before {
position: absolute;
content: "";
top: -($kuiBorderRadius * 2);
left: 20px;
height: 0;
width: 0;
border-left: $kuiBorderRadius * 2 solid transparent;
border-right: $kuiBorderRadius * 2 solid transparent;
border-bottom: $kuiBorderRadius * 2 solid $kuiBorderColor;
}
&:after {
position: absolute;
content: "";
top: -($kuiBorderRadius * 2) + 1;
left: 20px;
height: 0;
width: 0;
border-left: $kuiBorderRadius * 2 solid transparent;
border-right: $kuiBorderRadius * 2 solid transparent;
border-bottom: $kuiBorderRadius * 2 solid lighten($kuiBorderColor, 5%);
}
&.kuiExpressionItem__popover--alignRight {
right: 0;
&:before, &:after {
left: auto;
right: 20px;
}
}
}
.kuiExpressionItem__popoverTitle {
display: flex;
flex: 1 1 auto;
background-color: lighten($kuiBorderColor, 5%);
border-radius: $kuiBorderRadius $kuiBorderRadius 0 0;
color: $kuiTextColor;
padding: 5px 10px;
line-height: $kuiLineHeight;
}
.kuiExpressionItem__popoverContent {
padding: 20px;
white-space: nowrap;
}
.kuiExpressionButton-isActive {
border-bottom: solid 2px $expressionColorHighlight;
}

View file

@ -2,12 +2,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export const KuiExpressionItem = ({
export const KuiExpression = ({
children,
className,
...rest
}) => {
const classes = classNames('kuiExpressionItem', className);
const classes = classNames('kuiExpression', className);
return (
<div
@ -19,7 +19,7 @@ export const KuiExpressionItem = ({
);
};
KuiExpressionItem.propTypes = {
KuiExpression.propTypes = {
children: PropTypes.node,
className: PropTypes.string
};

View file

@ -3,13 +3,13 @@ import { render } from 'enzyme';
import { requiredProps } from '../../test/required_props';
import {
KuiExpressionItem,
} from './expression_item';
KuiExpression,
} from './expression';
describe('KuiExpressionItem', () => {
describe('KuiExpression', () => {
test('renders', () => {
const component = (
<KuiExpressionItem {...requiredProps} />
<KuiExpression {...requiredProps} />
);
expect(render(component)).toMatchSnapshot();
@ -19,9 +19,9 @@ describe('KuiExpressionItem', () => {
describe('children', () => {
test('is rendered', () => {
const component = render(
<KuiExpressionItem>
<KuiExpression>
some expression
</KuiExpressionItem>
</KuiExpression>
);
expect(component)

View file

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export const KuiExpressionItemButton = ({
export const KuiExpressionButton = ({
className,
description,
buttonValue,
@ -10,8 +10,8 @@ export const KuiExpressionItemButton = ({
onClick,
...rest
}) => {
const classes = classNames('kuiExpressionItem__button', className, {
'kuiExpressionItem__button--isActive': isActive
const classes = classNames('kuiExpressionButton', className, {
'kuiExpressionButton-isActive': isActive
});
return (
@ -20,16 +20,20 @@ export const KuiExpressionItemButton = ({
onClick={onClick}
{...rest}
>
<span className="kuiExpressionItem__buttonDescription">{description}</span>{' '}
<span className="kuiExpressionItem__buttonValue">{buttonValue}</span>
<span className="kuiExpressionButton__description">{description}</span>{' '}
<span className="kuiExpressionButton__value">{buttonValue}</span>
</button>
);
};
KuiExpressionItemButton.propTypes = {
KuiExpressionButton.propTypes = {
className: PropTypes.string,
description: PropTypes.string.isRequired,
buttonValue: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
};
KuiExpressionButton.defaultProps = {
isActive: false,
};

View file

@ -4,13 +4,13 @@ import { requiredProps } from '../../test/required_props';
import sinon from 'sinon';
import {
KuiExpressionItemButton,
} from './expression_item_button';
KuiExpressionButton,
} from './expression_button';
describe('KuiExpressionItemButton', () => {
describe('KuiExpressionButton', () => {
test('renders', () => {
const component = (
<KuiExpressionItemButton
<KuiExpressionButton
description="the answer is"
buttonValue="42"
isActive={false}
@ -26,7 +26,7 @@ describe('KuiExpressionItemButton', () => {
describe('isActive', () => {
test('true renders active', () => {
const component = (
<KuiExpressionItemButton
<KuiExpressionButton
description="the answer is"
buttonValue="42"
isActive={true}
@ -39,7 +39,7 @@ describe('KuiExpressionItemButton', () => {
test('false renders inactive', () => {
const component = (
<KuiExpressionItemButton
<KuiExpressionButton
description="the answer is"
buttonValue="42"
isActive={false}
@ -56,7 +56,7 @@ describe('KuiExpressionItemButton', () => {
const onClickHandler = sinon.spy();
const button = shallow(
<KuiExpressionItemButton
<KuiExpressionButton
description="the answer is"
buttonValue="42"
isActive={false}

View file

@ -1,54 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { KuiOutsideClickDetector } from '../outside_click_detector';
const POPOVER_ALIGN = [
'left',
'right',
];
const KuiExpressionItemPopover = ({
className,
title,
children,
align,
onOutsideClick,
...rest
}) => {
const classes = classNames('kuiExpressionItem__popover', className, {
'kuiExpressionItem__popover--alignRight': align === 'right'
});
return (
<KuiOutsideClickDetector onOutsideClick={onOutsideClick}>
<div
className={classes}
{...rest}
>
<div className="kuiExpressionItem__popoverTitle">
{title}
</div>
<div className="kuiExpressionItem__popoverContent">
{children}
</div>
</div>
</KuiOutsideClickDetector>
);
};
KuiExpressionItemPopover.defaultProps = {
align: 'left',
};
KuiExpressionItemPopover.propTypes = {
className: PropTypes.string,
title: PropTypes.string.isRequired,
children: PropTypes.node,
align: PropTypes.oneOf(POPOVER_ALIGN),
onOutsideClick: PropTypes.func.isRequired,
};
export {
POPOVER_ALIGN,
KuiExpressionItemPopover
};

View file

@ -1,68 +0,0 @@
import React from 'react';
import { render } from 'enzyme';
import { requiredProps } from '../../test/required_props';
import {
KuiExpressionItemPopover,
POPOVER_ALIGN
} from './expression_item_popover';
describe('KuiExpressionItemPopover', () => {
test('renders', () => {
const component = (
<KuiExpressionItemPopover
title="title"
align="left"
onOutsideClick={()=>{}}
{...requiredProps}
/>
);
expect(render(component)).toMatchSnapshot();
});
describe('Props', () => {
describe('children', () => {
test('is rendered', () => {
const component = render(
<KuiExpressionItemPopover
title="title"
align="left"
onOutsideClick={()=>{}}
>
popover content
</KuiExpressionItemPopover>
);
expect(component).toMatchSnapshot();
});
});
describe('align', () => {
test('renders default', () => {
const component = render(
<KuiExpressionItemPopover
title="title"
onOutsideClick={()=>{}}
/>
);
expect(component).toMatchSnapshot();
});
POPOVER_ALIGN.forEach(align => {
test(`renders the ${align} class`, () => {
const component = render(
<KuiExpressionItemPopover
title="title"
align={align}
onOutsideClick={()=>{}}
/>
);
expect(component).toMatchSnapshot();
});
});
});
});
});

View file

@ -1,3 +1,2 @@
export { KuiExpressionItem } from './expression_item';
export { KuiExpressionItemButton } from './expression_item_button';
export { KuiExpressionItemPopover } from './expression_item_popover';
export { KuiExpression } from './expression';
export { KuiExpressionButton } from './expression_button';

View file

@ -39,6 +39,12 @@ export {
KuiCollapseButton,
} from './collapse_button';
export {
KuiContextMenu,
KuiContextMenuPanel,
KuiContextMenuItem,
} from './context_menu';
export {
KuiEmptyTablePrompt,
KuiEmptyTablePromptMessage,
@ -54,9 +60,8 @@ export {
} from './event';
export {
KuiExpressionItem,
KuiExpressionItemButton,
KuiExpressionItemPopover,
KuiExpression,
KuiExpressionButton,
} from './expression';
export {
@ -121,8 +126,13 @@ export {
KuiPagerButtonGroup,
} from './pager';
export {
KuiPanelSimple,
} from './panel_simple';
export {
KuiPopover,
KuiPopoverTitle,
} from './popover';
export {

View file

@ -22,6 +22,7 @@
@import "collapse_button/index";
@import "color_picker/index";
@import "column/index";
@import 'context_menu/index';
@import "event/index";
@import "expression/index";
@import "flex/index";
@ -41,6 +42,7 @@
@import "notice/index";
@import "pager/index";
@import "panel/index";
@import "panel_simple/index";
@import "popover/index";
@import "empty_table_prompt/index";
@import "status_text/index";

View file

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`KuiPanelSimple is rendered 1`] = `
<div
aria-label="aria-label"
class="kuiPanelSimple kuiPanelSimple--paddingMedium testClass1 testClass2"
data-test-subj="test subject string"
/>
`;

View file

@ -0,0 +1 @@
@import 'panel_simple';

View file

@ -0,0 +1,33 @@
.kuiPanelSimple {
@include kuiBottomShadowSmall;
background-color: $kuiColorEmptyShade;
border: $kuiBorderThin;
border-radius: $kuiBorderRadius;
flex-grow: 1;
&.kuiPanelSimple--paddingSmall {
padding: $kuiSizeS;
}
&.kuiPanelSimple--paddingMedium {
padding: $kuiSize;
}
&.kuiPanelSimple--paddingLarge {
padding: $kuiSizeL;
}
&.kuiPanelSimple--shadow {
@include kuiBottomShadow;
}
&.kuiPanelSimple--flexGrowZero {
flex-grow: 0;
}
@include darkTheme {
background-color: $kuiBackgroundColor--darkTheme;
border-color: $kuiInputBorderColor--darkTheme;
}
}

View file

@ -0,0 +1,4 @@
export {
KuiPanelSimple,
SIZES,
} from './panel_simple';

View file

@ -0,0 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const paddingSizeToClassNameMap = {
'none': null,
's': 'kuiPanelSimple--paddingSmall',
'm': 'kuiPanelSimple--paddingMedium',
'l': 'kuiPanelSimple--paddingLarge',
};
export const SIZES = Object.keys(paddingSizeToClassNameMap);
export const KuiPanelSimple = ({
children,
className,
paddingSize,
hasShadow,
grow,
panelRef,
...rest,
}) => {
const classes = classNames(
'kuiPanelSimple',
paddingSizeToClassNameMap[paddingSize],
{
'kuiPanelSimple--shadow': hasShadow,
'kuiPanelSimple--flexGrowZero': !grow,
},
className
);
return (
<div
ref={panelRef}
className={classes}
{...rest}
>
{children}
</div>
);
};
KuiPanelSimple.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
hasShadow: PropTypes.bool,
paddingSize: PropTypes.oneOf(SIZES),
grow: PropTypes.bool,
panelRef: PropTypes.func,
};
KuiPanelSimple.defaultProps = {
paddingSize: 'm',
hasShadow: false,
grow: true,
};

View file

@ -0,0 +1,16 @@
import React from 'react';
import { render } from 'enzyme';
import { requiredProps } from '../../test/required_props';
import { KuiPanelSimple } from './panel_simple';
describe('KuiPanelSimple', () => {
test('is rendered', () => {
const component = render(
<KuiPanelSimple {...requiredProps} />
);
expect(component)
.toMatchSnapshot();
});
});

View file

@ -5,9 +5,6 @@ exports[`KuiPopover anchorPosition defaults to center 1`] = `
class="kuiPopover"
>
<button />
<div
class="kuiPopover__body"
/>
</div>
`;
@ -16,9 +13,6 @@ exports[`KuiPopover anchorPosition left is rendered 1`] = `
class="kuiPopover kuiPopover--anchorLeft"
>
<button />
<div
class="kuiPopover__body"
/>
</div>
`;
@ -27,9 +21,6 @@ exports[`KuiPopover anchorPosition right is rendered 1`] = `
class="kuiPopover kuiPopover--anchorRight"
>
<button />
<div
class="kuiPopover__body"
/>
</div>
`;
@ -38,11 +29,6 @@ exports[`KuiPopover children is rendered 1`] = `
class="kuiPopover"
>
<button />
<div
class="kuiPopover__body"
>
Children
</div>
</div>
`;
@ -53,9 +39,6 @@ exports[`KuiPopover is rendered 1`] = `
data-test-subj="test subject string"
>
<button />
<div
class="kuiPopover__body"
/>
</div>
`;
@ -64,19 +47,18 @@ exports[`KuiPopover isOpen defaults to false 1`] = `
class="kuiPopover"
>
<button />
<div
class="kuiPopover__body"
/>
</div>
`;
exports[`KuiPopover isOpen renders true 1`] = `
<div
class="kuiPopover kuiPopover-isOpen"
class="kuiPopover"
>
<button />
<div
class="kuiPopover__body"
/>
<div>
<div
class="kuiPanelSimple kuiPanelSimple--paddingMedium kuiPanelSimple--shadow kuiPopover__panel"
/>
</div>
</div>
`;

View file

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`KuiPopoverTitle is rendered 1`] = `
<div
aria-label="aria-label"
class="kuiPopoverTitle testClass1 testClass2"
data-test-subj="test subject string"
/>
`;

View file

@ -1,3 +1,3 @@
$popOverBackgroundColor: #FFF;
@import 'mixins';
@import 'popover';
@import 'popover_title';

View file

@ -0,0 +1,12 @@
@mixin kuiPopoverTitle {
background-color: $kuiColorLightestShade;
border-bottom: $kuiBorderThin;
padding: $kuiSizeM;
font-size: $kuiFontSize;
@include darkTheme {
background-color: $kuiBackgroundColor--darkTheme;
border-color: $kuiInputBorderColor--darkTheme;
color: #ffffff;
}
}

View file

@ -5,78 +5,91 @@
display: inline-block;
position: relative;
// Open state happens on the wrapper and applies to the body.
// Open state happens on the wrapper and applies to the panel.
&.kuiPopover-isOpen {
.kuiPopover__body {
.kuiPopover__panel {
opacity: 1;
visibility: visible;
display: inline-block;
z-index: 1;
margin-top: 10px;
box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1);
z-index: $kuiZContentMenu;
margin-top: $kuiSizeS;
pointer-events: auto;
}
}
}
// Animation happens on the body.
.kuiPopover__body {
line-height: $kuiLineHeight;
font-size: $kuiFontSize;
// Animation happens on the panel.
.kuiPopover__panel {
position: absolute;
min-width: 256px; // Can expand further, but this size is good for our menus.
min-width: $kuiSize * 16; // Can expand further, but this size is good for our menus.
top: 100%;
left: 50%;
background: $popOverBackgroundColor;
border: 1px solid $kuiBorderColor;
border-radius: $kuiBorderRadius 0 $kuiBorderRadius $kuiBorderRadius;
padding: 16px;
transform: translateX(-50%) translateY(8px) translateZ(0);
transform: translateX(-50%) translateY($kuiSizeS) translateZ(0);
backface-visibility: hidden;
transition:
opacity $kuiAnimSlightBounce $kuiAnimSpeedSlow,
visibility $kuiAnimSlightBounce $kuiAnimSpeedSlow,
margin-top $kuiAnimSlightBounce $kuiAnimSpeedSlow;
transform-origin: center top;
opacity: 0;
display: none;
margin-top: 32px;
visibility: hidden;
pointer-events: none;
margin-top: $kuiSizeL;
// This fakes a border on the arrow.
&:before {
position: absolute;
content: "";
top: -16px;
top: -$kuiSize;
height: 0;
width: 0;
left: 50%;
margin-left: -16px;
border-left: 16px solid transparent;
border-right: 16px solid transparent;
border-bottom: 16px solid $kuiBorderColor;
margin-left: -$kuiSize;
border-left: $kuiSize solid transparent;
border-right: $kuiSize solid transparent;
border-bottom: $kuiSize solid $kuiBorderColor;
@include darkTheme {
border-bottom-color: $kuiInputBorderColor--darkTheme;
}
}
// This part of the arrow matches the body.
// This part of the arrow matches the panel.
&:after {
position: absolute;
content: "";
top: -16px + 1;
top: -$kuiSize + 1;
right: 0;
height: 0;
left: 50%;
margin-left: -16px;
margin-left: -$kuiSize;
width: 0;
border-left: 16px solid transparent;
border-right: 16px solid transparent;
border-bottom: 16px solid $popOverBackgroundColor;
border-left: $kuiSize solid transparent;
border-right: $kuiSize solid transparent;
border-bottom: $kuiSize solid #ffffff;
@include darkTheme {
border-bottom-color: $kuiBackgroundColor--darkTheme;
}
}
}
.kuiPopover--withTitle .kuiPopover__panel:after {
border-bottom-color: $kuiColorLightestShade;
@include darkTheme {
border-bottom-color: $kuiBackgroundColor--darkTheme;
}
}
// Positions the menu and arrow to the left of the parent.
.kuiPopover--anchorLeft {
.kuiPopover__body {
.kuiPopover__panel {
left: 0;
transform: translateX(0%) translateY(8px) translateZ(0);
transform: translateX(0%) translateY($kuiSizeS) translateZ(0);
&:before, &:after {
right: auto;
left: 8px;
left: $kuiSize;
margin: 0;
}
}
@ -84,12 +97,12 @@
// Positions the menu and arrow to the right of the parent.
.kuiPopover--anchorRight {
.kuiPopover__body {
.kuiPopover__panel {
left: 100%;
transform: translateX(-100%) translateY(8px) translateZ(0);
transform: translateX(-100%) translateY($kuiSizeS) translateZ(0);
&:before, &:after {
right: 8px;
right: $kuiSize;
left: auto;
}
}

View file

@ -0,0 +1,3 @@
.kuiPopoverTitle {
@include kuiPopoverTitle;
}

View file

@ -1,3 +1,2 @@
export {
KuiPopover,
} from './popover';
export { KuiPopover, } from './popover';
export { KuiPopoverTitle } from './popover_title';

View file

@ -1,9 +1,16 @@
import React from 'react';
import React, {
Component,
} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import FocusTrap from 'focus-trap-react';
import { cascadingMenuKeyCodes } from '../../services';
import { KuiOutsideClickDetector } from '../outside_click_detector';
import { KuiPanelSimple, SIZES } from '../../components/panel_simple';
const anchorPositionToClassNameMap = {
'center': '',
'left': 'kuiPopover--anchorLeft',
@ -12,56 +19,134 @@ const anchorPositionToClassNameMap = {
export const ANCHOR_POSITIONS = Object.keys(anchorPositionToClassNameMap);
export const KuiPopover = ({
anchorPosition,
bodyClassName,
button,
isOpen,
children,
className,
closePopover,
...rest,
}) => {
const classes = classNames(
'kuiPopover',
anchorPositionToClassNameMap[anchorPosition],
className,
{
'kuiPopover-isOpen': isOpen,
},
);
export class KuiPopover extends Component {
constructor(props) {
super(props);
const bodyClasses = classNames('kuiPopover__body', bodyClassName);
this.closingTransitionTimeout = undefined;
const body = (
<div className={bodyClasses}>
{ children }
</div>
);
this.state = {
isClosing: false,
isOpening: false,
};
}
return (
<KuiOutsideClickDetector onOutsideClick={closePopover}>
<div
className={classes}
{...rest}
>
{ button }
{ body }
</div>
</KuiOutsideClickDetector>
);
};
onKeyDown = e => {
if (e.keyCode === cascadingMenuKeyCodes.ESCAPE) {
this.props.closePopover();
}
};
componentWillReceiveProps(nextProps) {
// The popover is being opened.
if (!this.props.isOpen && nextProps.isOpen) {
clearTimeout(this.closingTransitionTimeout);
// We need to set this state a beat after the render takes place, so that the CSS
// transition can take effect.
window.requestAnimationFrame(() => {
this.setState({
isOpening: true,
});
});
}
// The popover is being closed.
if (this.props.isOpen && !nextProps.isOpen) {
// If the user has just closed the popover, queue up the removal of the content after the
// transition is complete.
this.setState({
isClosing: true,
isOpening: false,
});
this.closingTransitionTimeout = setTimeout(() => {
this.setState({
isClosing: false,
});
}, 250);
}
}
componentWillUnmount() {
clearTimeout(this.closingTransitionTimeout);
}
render() {
const {
anchorPosition,
button,
isOpen,
withTitle,
children,
className,
closePopover,
panelClassName,
panelPaddingSize,
...rest,
} = this.props;
const classes = classNames(
'kuiPopover',
anchorPositionToClassNameMap[anchorPosition],
className,
{
'kuiPopover-isOpen': this.state.isOpening,
'kuiPopover--withTitle': withTitle,
},
);
const panelClasses = classNames('kuiPopover__panel', panelClassName);
let panel;
if (isOpen || this.state.isClosing) {
panel = (
<FocusTrap
focusTrapOptions={{
clickOutsideDeactivates: true,
fallbackFocus: () => this.panel,
}}
>
<KuiPanelSimple
panelRef={node => { this.panel = node; }}
className={panelClasses}
paddingSize={panelPaddingSize}
hasShadow
>
{children}
</KuiPanelSimple>
</FocusTrap>
);
}
return (
<KuiOutsideClickDetector onOutsideClick={closePopover}>
<div
className={classes}
onKeyDown={this.onKeyDown}
{...rest}
>
{button}
{panel}
</div>
</KuiOutsideClickDetector>
);
}
}
KuiPopover.propTypes = {
isOpen: PropTypes.bool,
withTitle: PropTypes.bool,
closePopover: PropTypes.func.isRequired,
button: PropTypes.node.isRequired,
children: PropTypes.node,
anchorPosition: PropTypes.oneOf(ANCHOR_POSITIONS),
bodyClassName: PropTypes.string,
panelClassName: PropTypes.string,
panelPaddingSize: PropTypes.oneOf(SIZES),
};
KuiPopover.defaultProps = {
isOpen: false,
anchorPosition: 'center',
panelPaddingSize: 'm',
};

View file

@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export const KuiPopoverTitle = ({ children, className, ...rest }) => {
const classes = classNames('kuiPopoverTitle', className);
return (
<div
className={classes}
{...rest}
>
{children}
</div>
);
};
KuiPopoverTitle.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
};

View file

@ -0,0 +1,16 @@
import React from 'react';
import { render } from 'enzyme';
import { requiredProps } from '../../test/required_props';
import { KuiPopoverTitle } from './popover_title';
describe('KuiPopoverTitle', () => {
test('is rendered', () => {
const component = render(
<KuiPopoverTitle {...requiredProps} />
);
expect(component)
.toMatchSnapshot();
});
});

View file

@ -1,2 +1,3 @@
@import 'responsive';
@import 'shadow';
@import 'global_mixins';

View file

@ -0,0 +1,26 @@
@mixin kuiBottomShadow {
box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1);
}
@mixin kuiBottomShadowSmall {
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1);
}
@mixin kuiBottomShadowMedium {
box-shadow: 0 4px 4px -2px rgba(0, 0, 0, 0.2);
}
@mixin kuiSlightShadow {
box-shadow:
0 2px 2px -1px rgba(0, 0, 0, 0.15),
}
@mixin kuiSlightShadowHover {
box-shadow:
0 4px 4px -2px rgba(0, 0, 0, 0.1),
}
@mixin kuiSlightShadowActive {
box-shadow:
0 1px 1px 0px rgba(0, 0, 0, 0.2),
}

View file

@ -10,6 +10,8 @@ $kuiColorDarkGray: #666;
$kuiColorDarkestGray: #3F3F3F;
$kuiColorBlack: #000;
$kuiColorWhite: #FFF;
$kuiColorEmptyShade: $kuiColorWhite;
$kuiColorLightestShade: lighten($kuiColorLightGray, 5%);
// Normal colors
@ -38,6 +40,7 @@ $kuiSuccessColor: #417505;
$kuiWarningColor: #ec9800;
$kuiDangerColor: $kuiColorRed;
$kuiFocusColor: $kuiColorBlue;
$kuiFocusAltBackgroundColor: rgba($kuiInfoColor, 0.2);
$kuiFocusDangerColor: #ff523c;
$kuiFocusWarningColor: #ffa500;
$kuiFocusBackgroundColor: #ffffff;

View file

@ -0,0 +1,19 @@
// Z-Index
$kuiZLevel0: 0;
$kuiZLevel1: 1000;
$kuiZLevel2: 2000;
$kuiZLevel3: 3000;
$kuiZLevel4: 4000;
$kuiZLevel5: 5000;
$kuiZLevel6: 6000;
$kuiZLevel7: 7000;
$kuiZLevel8: 8000;
$kuiZLevel9: 9000;
$kuiZContent: $kuiZLevel0;
$kuiZContentMenu: $kuiZLevel2;
$kuiZNavigation: $kuiZLevel4;
$kuiZToastList: $kuiZLevel5;
$kuiZMask: $kuiZLevel6;
$kuiZModal: $kuiZLevel8;

View file

@ -0,0 +1,27 @@
/**
* These keys are used for navigating cascading menu UI components.
*
* UP: Select the previous item in the list.
* DOWN: Select the next item in the list.
* LEFT: Show the previous menu.
* RIGHT: Show the next menu for the selected item.
* ESC: Deselect the current selection and hide the list.
*/
import {
DOWN,
ESCAPE,
LEFT,
RIGHT,
TAB,
UP,
} from '../key_codes';
export const cascadingMenuKeyCodes = {
DOWN,
ESCAPE,
LEFT,
RIGHT,
TAB,
UP,
};

View file

@ -1,3 +1,4 @@
export { accessibleClickKeys } from './accessible_click_keys';
export { cascadingMenuKeyCodes } from './cascading_menu_key_codes';
export { comboBoxKeyCodes } from './combo_box_key_codes';
export { htmlIdGenerator } from './html_id_generator';

View file

@ -4,6 +4,7 @@ export { keyCodes };
export {
accessibleClickKeys,
cascadingMenuKeyCodes,
comboBoxKeyCodes,
htmlIdGenerator
} from './accessibility';

View file

@ -6,3 +6,5 @@ export const TAB = 9;
// Arrow keys
export const DOWN = 40;
export const UP = 38;
export const LEFT = 37;
export const RIGHT = 39;

View file

@ -0,0 +1,6 @@
export const snapshotComponent = component => {
const html = component.html();
const template = document.createElement('template');
template.innerHTML = html;
return template.content.firstChild;
};