Commit 60dbf728 by Ivana Huckova Committed by GitHub

@grafana/ui: Create slider component (#22275)

* grafana/ui: Create slider

* grafana/ui: Create slider, tests, add to storybook

* Update Slider, minor changes

* Implement single value slider

* Update style

* Update packages/grafana-ui/package.json

Co-Authored-By: Dominik Prokop <dominik.prokop@grafana.com>

* Update packages/grafana-ui/src/components/Slider/Slider.tsx

Co-Authored-By: Dominik Prokop <dominik.prokop@grafana.com>

* Update slider, include PR review feedback

* Update packages/grafana-ui/src/components/Slider/Slider.tsx

Co-Authored-By: Dominik Prokop <dominik.prokop@grafana.com>

* Update packages/grafana-ui/src/components/Slider/Slider.tsx

Co-Authored-By: Dominik Prokop <dominik.prokop@grafana.com>

* Export orientatin types, gix selectability of tooltip text

* Remove Orientation export from grafana/ui

* Testing Global component to inject global styles

* Add comments

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
parent 646b74ef
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@emotion/core": "^10.0.27",
"@grafana/data": "6.7.0-pre", "@grafana/data": "6.7.0-pre",
"@grafana/slate-react": "0.22.9-grafana", "@grafana/slate-react": "0.22.9-grafana",
"@grafana/tsconfig": "^1.0.0-rc1", "@grafana/tsconfig": "^1.0.0-rc1",
...@@ -47,6 +48,7 @@ ...@@ -47,6 +48,7 @@
"papaparse": "4.6.3", "papaparse": "4.6.3",
"rc-cascader": "0.17.5", "rc-cascader": "0.17.5",
"rc-drawer": "3.0.2", "rc-drawer": "3.0.2",
"rc-slider": "8.7.1",
"rc-time-picker": "^3.7.2", "rc-time-picker": "^3.7.2",
"react": "16.12.0", "react": "16.12.0",
"react-calendar": "2.19.2", "react-calendar": "2.19.2",
...@@ -78,6 +80,7 @@ ...@@ -78,6 +80,7 @@
"@types/node": "10.14.1", "@types/node": "10.14.1",
"@types/papaparse": "4.5.9", "@types/papaparse": "4.5.9",
"@types/pretty-format": "20.0.1", "@types/pretty-format": "20.0.1",
"@types/rc-slider": "8.6.5",
"@types/react": "16.8.16", "@types/react": "16.8.16",
"@types/react-custom-scrollbars": "4.0.5", "@types/react-custom-scrollbars": "4.0.5",
"@types/react-test-renderer": "16.9.0", "@types/react-test-renderer": "16.9.0",
......
import React from 'react';
import { Slider } from './Slider';
import { select, number, boolean } from '@storybook/addon-knobs';
export default {
title: 'General/Slider',
component: Slider,
};
const getKnobs = () => {
return {
min: number('min', 0),
max: number('max', 100),
orientation: select('orientation', ['horizontal', 'vertical'], 'horizontal'),
reverse: boolean('reverse', true),
singleValue: boolean('single value', false),
};
};
const SliderWrapper = () => {
const { min, max, orientation, reverse, singleValue } = getKnobs();
return (
<div style={{ width: '200px', height: '200px' }}>
<Slider min={min} max={max} orientation={orientation} value={singleValue ? [10] : undefined} reverse={reverse} />
</div>
);
};
export const basic = () => <SliderWrapper />;
import React from 'react';
import { Slider, Props } from './Slider';
import { mount } from 'enzyme';
const sliderProps: Props = {
min: 10,
max: 20,
};
describe('Slider', () => {
it('renders without error', () => {
mount(<Slider {...sliderProps} />);
});
it('renders correct contents', () => {
const wrapper = mount(<Slider {...sliderProps} />);
expect(wrapper.html()).toContain('aria-valuemin="10"');
expect(wrapper.html()).toContain('aria-valuemax="20"');
expect(wrapper.html()).toContain('aria-valuenow="10"');
expect(wrapper.html()).toContain('aria-valuenow="20"');
});
it('renders correct contents with a value', () => {
const wrapper = mount(<Slider {...sliderProps} value={[15]} />);
expect(wrapper.html()).toContain('aria-valuenow="15"');
expect(wrapper.html()).not.toContain('aria-valuenow="20"');
expect(wrapper.html()).not.toContain('aria-valuenow="10"');
});
});
import React, { FunctionComponent } from 'react';
import { Range, createSliderWithTooltip } from 'rc-slider';
import { cx, css } from 'emotion';
import { Global, css as cssCore } from '@emotion/core';
import { stylesFactory } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
import { useTheme } from '../../themes/ThemeContext';
import { Orientation } from '../../types/orientation';
export interface Props {
min: number;
max: number;
orientation?: Orientation;
/** Set current positions of handle(s). If only 1 value supplied, only 1 handle displayed. */
value?: number[];
reverse?: boolean;
formatTooltipResult?: (value: number) => number | string;
onChange?: (values: number[]) => void;
}
const getStyles = stylesFactory((theme: GrafanaTheme, isHorizontal: boolean) => {
const trackColor = theme.isLight ? theme.colors.gray5 : theme.colors.dark6;
const container = isHorizontal
? css`
width: 100%;
margin: ${theme.spacing.lg} ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm};
`
: css`
height: 100%;
margin: ${theme.spacing.sm} ${theme.spacing.lg} ${theme.spacing.sm} ${theme.spacing.sm};
`;
return {
container,
slider: css`
.rc-slider-vertical .rc-slider-handle {
margin-top: -10px;
}
.rc-slider-handle {
border: solid 2px ${theme.colors.blue77};
background-color: ${theme.colors.blue77};
}
.rc-slider-handle:hover {
border-color: ${theme.colors.blue77};
}
.rc-slider-handle:focus {
border-color: ${theme.colors.blue77};
box-shadow: none;
}
.rc-slider-handle:active {
border-color: ${theme.colors.blue77};
box-shadow: none;
}
.rc-slider-handle-click-focused:focus {
border-color: ${theme.colors.blue77};
}
.rc-slider-dot-active {
border-color: ${theme.colors.blue77};
}
.rc-slider-track {
background-color: ${theme.colors.blue77};
}
.rc-slider-rail {
background-color: ${trackColor};
border: 1px solid ${trackColor};
}
`,
/** Global component from @emotion/core doesn't accept computed classname string returned from css from emotion.
* It accepts object containing the computed name and flattened styles returned from css from @emotion/core
* */
tooltip: cssCore`
body {
.rc-slider-tooltip {
cursor: grab;
user-select: none;
}
.rc-slider-tooltip-inner {
color: ${theme.colors.text};
background-color: transparent !important;
border-radius: 0;
box-shadow: none;
}
.rc-slider-tooltip-placement-top .rc-slider-tooltip-arrow {
display: none;
}
.rc-slider-tooltip-placement-top {
padding: 0;
}
}
`,
};
});
export const Slider: FunctionComponent<Props> = ({
min,
max,
onChange,
orientation = 'horizontal',
reverse,
formatTooltipResult,
value,
}) => {
const isHorizontal = orientation === 'horizontal';
const theme = useTheme();
const styles = getStyles(theme, isHorizontal);
const RangeWithTooltip = createSliderWithTooltip(Range);
return (
<div className={cx(styles.container, styles.slider)}>
{/** Slider tooltip's parent component is body and therefore we need Global component to do css overrides for it. */}
<Global styles={styles.tooltip} />
<RangeWithTooltip
tipProps={{ visible: true, placement: isHorizontal ? 'top' : 'right' }}
min={min}
max={max}
defaultValue={value || [min, max]}
tipFormatter={(value: number) => (formatTooltipResult ? formatTooltipResult(value) : value)}
onChange={onChange}
vertical={!isHorizontal}
reverse={reverse}
/>
</div>
);
};
Slider.displayName = 'Slider';
@import '../../node_modules/rc-slider/assets/index.css';
...@@ -15,3 +15,4 @@ ...@@ -15,3 +15,4 @@
@import 'Tooltip/Tooltip'; @import 'Tooltip/Tooltip';
@import 'ValueMappingsEditor/ValueMappingsEditor'; @import 'ValueMappingsEditor/ValueMappingsEditor';
@import 'Alert/Alert'; @import 'Alert/Alert';
@import 'Slider/Slider';
...@@ -115,6 +115,7 @@ export { Segment, SegmentAsync, SegmentInput, SegmentSelect } from './Segment/'; ...@@ -115,6 +115,7 @@ export { Segment, SegmentAsync, SegmentInput, SegmentSelect } from './Segment/';
export { default as Chart } from './Chart'; export { default as Chart } from './Chart';
export { Icon } from './Icon/Icon'; export { Icon } from './Icon/Icon';
export { Drawer } from './Drawer/Drawer'; export { Drawer } from './Drawer/Drawer';
export { Slider } from './Slider/Slider';
// TODO: namespace!! // TODO: namespace!!
export { export {
......
export type Orientation = 'horizontal' | 'vertical';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment