Merge pull request #14766 from grafana/fix-toggle-button-group-corners

Fix toggle button group corners
This commit is contained in:
Torkel Ödegaard
2019-01-09 13:41:40 +01:00
committed by GitHub
29 changed files with 232 additions and 249 deletions

View File

@@ -11,6 +11,8 @@
"license": "ISC",
"dependencies": {
"@torkelo/react-select": "2.1.1",
"@types/react-test-renderer": "^16.0.3",
"@types/react-transition-group": "^2.0.15",
"classnames": "^2.2.5",
"jquery": "^3.2.1",
"lodash": "^4.17.10",

View File

@@ -6,11 +6,11 @@ interface Props {
root?: HTMLElement;
}
export default class BodyPortal extends PureComponent<Props> {
export class Portal extends PureComponent<Props> {
node: HTMLElement = document.createElement('div');
portalRoot: HTMLElement;
constructor(props) {
constructor(props: Props) {
super(props);
const {
className,

View File

@@ -1,6 +1,7 @@
import React, { PureComponent } from 'react';
import Portal from 'app/core/components/Portal/Portal';
import { Manager, Popper as ReactPopper, Reference } from 'react-popper';
import * as PopperJS from 'popper.js';
import { Manager, Popper as ReactPopper } from 'react-popper';
import { Portal } from '@grafana/ui';
import Transition from 'react-transition-group/Transition';
export enum Themes {
@@ -13,45 +14,40 @@ const defaultTransitionStyles = {
opacity: 0,
};
const transitionStyles = {
const transitionStyles: {[key: string]: object} = {
exited: { opacity: 0 },
entering: { opacity: 0 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
};
interface Props {
interface Props extends React.DOMAttributes<HTMLDivElement> {
renderContent: (content: any) => any;
show: boolean;
placement?: any;
placement?: PopperJS.Placement;
content: string | ((props: any) => JSX.Element);
refClassName?: string;
referenceElement: PopperJS.ReferenceObject;
theme?: Themes;
}
class Popper extends PureComponent<Props> {
render() {
const { children, renderContent, show, placement, refClassName, theme } = this.props;
const { renderContent, show, placement, onMouseEnter, onMouseLeave, theme } = this.props;
const { content } = this.props;
const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : '');
return (
<Manager>
<Reference>
{({ ref }) => (
<div className={`popper_ref ${refClassName || ''}`} ref={ref}>
{children}
</div>
)}
</Reference>
<Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
{transitionState => (
<Portal>
<ReactPopper placement={placement}>
<ReactPopper placement={placement} referenceElement={this.props.referenceElement}>
{({ ref, style, placement, arrowProps }) => {
return (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
ref={ref}
style={{
...style,

View File

@@ -0,0 +1,99 @@
import React from 'react';
import * as PopperJS from 'popper.js';
import { Themes } from './Popper';
type PopperContent = string | (() => JSX.Element);
export interface UsingPopperProps {
show?: boolean;
placement?: PopperJS.Placement;
content: PopperContent;
children: JSX.Element;
renderContent?: (content: PopperContent) => JSX.Element;
theme?: Themes;
}
type PopperControllerRenderProp = (
showPopper: () => void,
hidePopper: () => void,
popperProps: {
show: boolean;
placement: PopperJS.Placement;
content: string | ((props: any) => JSX.Element);
renderContent: (content: any) => any;
theme?: Themes;
}
) => JSX.Element;
interface Props {
placement?: PopperJS.Placement;
content: PopperContent;
className?: string;
children: PopperControllerRenderProp;
theme?: Themes;
}
interface State {
placement: PopperJS.Placement;
show: boolean;
}
class PopperController extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
placement: this.props.placement || 'auto',
show: false,
};
}
componentWillReceiveProps(nextProps: Props) {
if (nextProps.placement && nextProps.placement !== this.state.placement) {
this.setState((prevState: State) => {
return {
...prevState,
placement: nextProps.placement || 'auto',
};
});
}
}
showPopper = () => {
this.setState(prevState => ({
...prevState,
show: true,
}));
};
hidePopper = () => {
this.setState(prevState => ({
...prevState,
show: false,
}));
};
renderContent(content: PopperContent) {
if (typeof content === 'function') {
// If it's a function we assume it's a React component
const ReactComponent = content;
return <ReactComponent />;
}
return content;
}
render() {
const { children, content, theme } = this.props;
const { show, placement } = this.state;
return children(this.showPopper, this.hidePopper, {
show,
placement,
content,
renderContent: this.renderContent,
theme,
});
}
}
export default PopperController;

View File

@@ -1,13 +1,15 @@
import React from 'react';
import renderer from 'react-test-renderer';
import Tooltip from './Tooltip';
import { Tooltip } from './Tooltip';
describe('Tooltip', () => {
it('renders correctly', () => {
const tree = renderer
.create(
<Tooltip className="test-class" placement="auto" content="Tooltip text">
<a href="http://www.grafana.com">Link with tooltip</a>
<Tooltip placement="auto" content="Tooltip text">
<a className="test-class" href="http://www.grafana.com">
Link with tooltip
</a>
</Tooltip>
)
.toJSON();

View File

@@ -0,0 +1,32 @@
import React, { createRef } from 'react';
import * as PopperJS from 'popper.js';
import Popper from './Popper';
import PopperController, { UsingPopperProps } from './PopperController';
export const Tooltip = ({ children, renderContent, ...controllerProps }: UsingPopperProps) => {
const tooltipTriggerRef = createRef<PopperJS.ReferenceObject>();
return (
<PopperController {...controllerProps}>
{(showPopper, hidePopper, popperProps) => {
return (
<>
{tooltipTriggerRef.current && (
<Popper
{...popperProps}
onMouseEnter={showPopper}
onMouseLeave={hidePopper}
referenceElement={tooltipTriggerRef.current}
/>
)}
{React.cloneElement(children, {
ref: tooltipTriggerRef,
onMouseEnter: showPopper,
onMouseLeave: hidePopper,
})}
</>
);
}}
</PopperController>
);
};

View File

@@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Tooltip renders correctly 1`] = `
<a
className="test-class"
href="http://www.grafana.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Link with tooltip
</a>
`;

View File

@@ -1,2 +1,3 @@
@import 'CustomScrollbar/CustomScrollbar';
@import 'DeleteButton/DeleteButton';
@import 'Tooltip/Tooltip';

View File

@@ -1,2 +1,4 @@
export { DeleteButton } from './DeleteButton/DeleteButton';
export { Tooltip } from './Tooltip/Tooltip';
export { Portal } from './Portal/Portal';
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';

View File

@@ -1,5 +1,5 @@
import React, { SFC, ReactNode } from 'react';
import Tooltip from '../Tooltip/Tooltip';
import { Tooltip } from '@grafana/ui';
interface Props {
tooltip?: string;
@@ -14,8 +14,10 @@ export const Label: SFC<Props> = props => {
<span className={`gf-form-label width-${props.width ? props.width : '10'}`}>
<span>{props.children}</span>
{props.tooltip && (
<Tooltip className="gf-form-help-icon--right-normal" placement="auto" content={props.tooltip}>
<i className="gicon gicon-question gicon--has-hover" />
<Tooltip placement="auto" content={props.tooltip}>
<div className="gf-form-help-icon--right-normal">
<i className="gicon gicon-question gicon--has-hover" />
</div>
</Tooltip>
)}
</span>

View File

@@ -1,5 +1,5 @@
import React, { SFC, ReactNode, PureComponent } from 'react';
import Tooltip from 'app/core/components/Tooltip/Tooltip';
import { Tooltip } from '@grafana/ui';
interface ToggleButtonGroupProps {
label?: string;

View File

@@ -1,16 +0,0 @@
import React from 'react';
import renderer from 'react-test-renderer';
import Popover from './Popover';
describe('Popover', () => {
it('renders correctly', () => {
const tree = renderer
.create(
<Popover className="test-class" placement="auto" content="Popover text">
<button>Button with Popover</button>
</Popover>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -1,19 +0,0 @@
import React, { PureComponent } from 'react';
import Popper from './Popper';
import withPopper, { UsingPopperProps } from './withPopper';
class Popover extends PureComponent<UsingPopperProps> {
render() {
const { children, hidePopper, showPopper, className, ...restProps } = this.props;
const togglePopper = restProps.show ? hidePopper : showPopper;
return (
<div className={`popper__manager ${className}`} onClick={togglePopper}>
<Popper {...restProps}>{children}</Popper>
</div>
);
}
}
export default withPopper(Popover);

View File

@@ -1,17 +0,0 @@
import React, { PureComponent } from 'react';
import Popper from './Popper';
import withPopper, { UsingPopperProps } from './withPopper';
class Tooltip extends PureComponent<UsingPopperProps> {
render() {
const { children, hidePopper, showPopper, className, ...restProps } = this.props;
return (
<div className={`popper__manager ${className}`} onMouseEnter={showPopper} onMouseLeave={hidePopper}>
<Popper {...restProps}>{children}</Popper>
</div>
);
}
}
export default withPopper(Tooltip);

View File

@@ -1,16 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Popover renders correctly 1`] = `
<div
className="popper__manager test-class"
onClick={[Function]}
>
<div
className="popper_ref "
>
<button>
Button with Popover
</button>
</div>
</div>
`;

View File

@@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Tooltip renders correctly 1`] = `
<div
className="popper__manager test-class"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<div
className="popper_ref "
>
<a
href="http://www.grafana.com"
>
Link with tooltip
</a>
</div>
</div>
`;

View File

@@ -1,89 +0,0 @@
import React from 'react';
import { Themes } from './Popper';
export interface UsingPopperProps {
showPopper: (prevState: object) => void;
hidePopper: (prevState: object) => void;
renderContent: (content: any) => any;
show: boolean;
placement?: string;
content: string | ((props: any) => JSX.Element);
className?: string;
refClassName?: string;
theme?: Themes;
}
interface Props {
placement?: string;
className?: string;
refClassName?: string;
content: string | ((props: any) => JSX.Element);
theme?: Themes;
}
interface State {
placement: string;
show: boolean;
}
export default function withPopper(WrappedComponent) {
return class extends React.Component<Props, State> {
constructor(props) {
super(props);
this.setState = this.setState.bind(this);
this.state = {
placement: this.props.placement || 'auto',
show: false,
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.placement && nextProps.placement !== this.state.placement) {
this.setState(prevState => {
return {
...prevState,
placement: nextProps.placement,
};
});
}
}
showPopper = () => {
this.setState(prevState => ({
...prevState,
show: true,
}));
};
hidePopper = () => {
this.setState(prevState => ({
...prevState,
show: false,
}));
};
renderContent(content) {
if (typeof content === 'function') {
// If it's a function we assume it's a React component
const ReactComponent = content;
return <ReactComponent />;
}
return content;
}
render() {
const { show, placement } = this.state;
const className = this.props.className || '';
return (
<WrappedComponent
{...this.props}
showPopper={this.showPopper}
hidePopper={this.hidePopper}
renderContent={this.renderContent}
show={show}
placement={placement}
className={className}
/>
);
}
};
}

View File

@@ -1,6 +1,8 @@
// Library
import React, { Component } from 'react';
import Tooltip from 'app/core/components/Tooltip/Tooltip';
import { Tooltip } from '@grafana/ui';
import { Themes } from '@grafana/ui/src/components/Tooltip/Popper';
import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary';
// Services
@@ -12,7 +14,6 @@ import kbn from 'app/core/utils/kbn';
// Types
import { DataQueryOptions, DataQueryResponse } from 'app/types';
import { TimeRange, TimeSeries, LoadingState } from '@grafana/ui';
import { Themes } from 'app/core/components/Tooltip/Popper';
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
@@ -144,10 +145,10 @@ export class DataPanel extends Component<Props, State> {
this.setState({
loading: LoadingState.Error,
isFirstLoad: false,
errorMessage: errorMessage
errorMessage: errorMessage,
});
}
}
};
render() {
const { queries } = this.props;
@@ -171,7 +172,7 @@ export class DataPanel extends Component<Props, State> {
<>
{this.renderLoadingStates()}
<ErrorBoundary>
{({error, errorInfo}) => {
{({ error, errorInfo }) => {
if (errorInfo) {
this.onError(error.message || DEFAULT_PLUGIN_ERROR);
return null;
@@ -200,15 +201,11 @@ export class DataPanel extends Component<Props, State> {
);
} else if (loading === LoadingState.Error) {
return (
<Tooltip
content={errorMessage}
className="popper__manager--block"
refClassName={`panel-info-corner panel-info-corner--error`}
placement="bottom-start"
theme={Themes.Error}
>
<i className="fa" />
<span className="panel-info-corner-inner" />
<Tooltip content={errorMessage} placement="bottom-start" theme={Themes.Error}>
<div className="panel-info-corner panel-info-corner--error">
<i className="fa" />
<span className="panel-info-corner-inner" />
</div>
</Tooltip>
);
}

View File

@@ -1,5 +1,5 @@
import React, { SFC } from 'react';
import Tooltip from 'app/core/components/Tooltip/Tooltip';
import { Tooltip } from '@grafana/ui';
interface Props {
label: string;

View File

@@ -15,7 +15,7 @@ import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { PanelPlugin } from 'app/types/plugins';
import Tooltip from 'app/core/components/Tooltip/Tooltip';
import { Tooltip } from '@grafana/ui';
interface PanelEditorProps {
panel: PanelModel;
@@ -138,7 +138,7 @@ function TabItem({ tab, activeTab, onClick }: TabItemParams) {
return (
<div className="panel-editor-tabs__item" onClick={() => onClick(tab)}>
<a className={tabClasses}>
<Tooltip content={`${tab.text}`} className="popper__manager--block" placement="auto">
<Tooltip content={`${tab.text}`} placement="auto">
<i className={`gicon gicon-${tab.id}${activeTab === tab.id ? '-active' : ''}`} />
</Tooltip>
</a>

View File

@@ -1,10 +1,10 @@
import React, { Component } from 'react';
import Remarkable from 'remarkable';
import { Tooltip } from '@grafana/ui';
import { PanelModel } from 'app/features/dashboard/panel_model';
import Tooltip from 'app/core/components/Tooltip/Tooltip';
import templateSrv from 'app/features/templating/template_srv';
import { LinkSrv } from 'app/features/dashboard/panellinks/link_srv';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/time_srv';
import Remarkable from 'remarkable';
enum InfoModes {
Error = 'Error',
@@ -78,12 +78,14 @@ export class PanelHeaderCorner extends Component<Props> {
{infoMode === InfoModes.Info || infoMode === InfoModes.Links ? (
<Tooltip
content={this.getInfoContent}
className="popper__manager--block"
refClassName={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}
placement="bottom-start"
>
<i className="fa" />
<span className="panel-info-corner-inner" />
<div
className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}
>
<i className="fa" />
<span className="panel-info-corner-inner" />
</div>
</Tooltip>
) : null}
</>

View File

@@ -1,5 +1,5 @@
import React, { PureComponent } from 'react';
import Tooltip from 'app/core/components/Tooltip/Tooltip';
import { Tooltip } from '@grafana/ui';
import SlideDown from 'app/core/components/Animations/SlideDown';
import { StoreState, FolderInfo } from 'app/types';
import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
@@ -70,8 +70,10 @@ export class DashboardPermissions extends PureComponent<Props, State> {
<div className="dashboard-settings__header">
<div className="page-action-bar">
<h3 className="d-inline-block">Permissions</h3>
<Tooltip className="page-sub-heading-icon" placement="auto" content={PermissionsInfo}>
<i className="gicon gicon-question gicon--has-hover" />
<Tooltip placement="auto" content={PermissionsInfo}>
<div className="page-sub-heading-icon">
<i className="gicon gicon-question gicon--has-hover" />
</div>
</Tooltip>
<div className="page-action-bar__spacer" />
<button className="btn btn-success pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>

View File

@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import PageHeader from 'app/core/components/PageHeader/PageHeader';
import Tooltip from 'app/core/components/Tooltip/Tooltip';
import { Tooltip } from '@grafana/ui';
import SlideDown from 'app/core/components/Animations/SlideDown';
import { getNavModel } from 'app/core/selectors/navModel';
import { NavModel, StoreState, FolderState } from 'app/types';
@@ -84,8 +84,10 @@ export class FolderPermissions extends PureComponent<Props, State> {
<div className="page-container page-body">
<div className="page-action-bar">
<h3 className="page-sub-heading">Folder Permissions</h3>
<Tooltip className="page-sub-heading-icon" placement="auto" content={PermissionsInfo}>
<i className="gicon gicon-question gicon--has-hover" />
<Tooltip placement="auto" content={PermissionsInfo}>
<div className="page-sub-heading-icon">
<i className="gicon gicon-question gicon--has-hover" />
</div>
</Tooltip>
<div className="page-action-bar__spacer" />
<button className="btn btn-success pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>

View File

@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import SlideDown from 'app/core/components/Animations/SlideDown';
import Tooltip from 'app/core/components/Tooltip/Tooltip';
import { Tooltip } from '@grafana/ui';
import { TeamGroup } from '../../types';
import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions';
import { getTeamGroups } from './state/selectors';
@@ -77,8 +77,10 @@ export class TeamGroupSync extends PureComponent<Props, State> {
<div>
<div className="page-action-bar">
<h3 className="page-sub-heading">External group sync</h3>
<Tooltip className="page-sub-heading-icon" placement="auto" content={headerTooltip}>
<i className="gicon gicon-question gicon--has-hover" />
<Tooltip placement="auto" content={headerTooltip}>
<div className="page-sub-heading-icon">
<i className="gicon gicon-question gicon--has-hover" />
</div>
</Tooltip>
<div className="page-action-bar__spacer" />
{groups.length > 0 && (

View File

@@ -60,6 +60,7 @@ export class TeamSettings extends React.Component<Props, State> {
onChange={this.onChangeName}
/>
</div>
<div className="gf-form max-width-30">
<Label tooltip="This is optional and is primarily used to set the team profile avatar (via gravatar service)">
Email

View File

@@ -10,15 +10,18 @@ exports[`Render should render component 1`] = `
>
External group sync
</h3>
<class_1
className="page-sub-heading-icon"
<Component
content="Sync LDAP or OAuth groups with your Grafana teams."
placement="auto"
>
<i
className="gicon gicon-question gicon--has-hover"
/>
</class_1>
<div
className="page-sub-heading-icon"
>
<i
className="gicon gicon-question gicon--has-hover"
/>
</div>
</Component>
<div
className="page-action-bar__spacer"
/>
@@ -116,15 +119,18 @@ exports[`Render should render groups table 1`] = `
>
External group sync
</h3>
<class_1
className="page-sub-heading-icon"
<Component
content="Sync LDAP or OAuth groups with your Grafana teams."
placement="auto"
>
<i
className="gicon gicon-question gicon--has-hover"
/>
</class_1>
<div
className="page-sub-heading-icon"
>
<i
className="gicon gicon-question gicon--has-hover"
/>
</div>
</Component>
<div
className="page-action-bar__spacer"
/>

View File

@@ -97,7 +97,6 @@
@import 'components/page_header';
@import 'components/dashboard_settings';
@import 'components/empty_list_cta';
@import 'components/popper';
@import 'components/form_select_box';
@import 'components/panel_editor';
@import 'components/toolbar';

View File

@@ -1098,10 +1098,10 @@
dependencies:
"@types/react" "*"
"@types/react-transition-group@*":
version "2.0.14"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.0.14.tgz#afd0cd785a97f070b55765e9f9d76ff568269001"
integrity sha512-pa7qB0/mkhwWMBFoXhX8BcntK8G4eQl4sIfSrJCxnivTYRQWjOWf2ClR9bWdm0EUFBDHzMbKYS+QYfDtBzkY4w==
"@types/react-transition-group@^2.0.15":
version "2.0.15"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.0.15.tgz#e5ee3fe558832e141cc6041bdd54caea7b787af8"
integrity sha512-S0QnNzbHoWXDbKBl/xk5dxA4FT+BNlBcI3hku991cl8Cz3ytOkUMcCRtzdX11eb86E131bSsQqy5WrPCdJYblw==
dependencies:
"@types/react" "*"