diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 937475b9cc5..6a5d362b7fa 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -27,6 +27,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@emotion/core": "^10.0.27", "@grafana/data": "6.7.0-pre", "@grafana/slate-react": "0.22.9-grafana", "@grafana/tsconfig": "^1.0.0-rc1", @@ -47,6 +48,7 @@ "papaparse": "4.6.3", "rc-cascader": "0.17.5", "rc-drawer": "3.0.2", + "rc-slider": "8.7.1", "rc-time-picker": "^3.7.2", "react": "16.12.0", "react-calendar": "2.19.2", @@ -78,6 +80,7 @@ "@types/node": "10.14.1", "@types/papaparse": "4.5.9", "@types/pretty-format": "20.0.1", + "@types/rc-slider": "8.6.5", "@types/react": "16.8.16", "@types/react-custom-scrollbars": "4.0.5", "@types/react-test-renderer": "16.9.0", diff --git a/packages/grafana-ui/src/components/Slider/Slider.story.tsx b/packages/grafana-ui/src/components/Slider/Slider.story.tsx new file mode 100644 index 00000000000..2bd81439a5b --- /dev/null +++ b/packages/grafana-ui/src/components/Slider/Slider.story.tsx @@ -0,0 +1,29 @@ +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 ( +
+ +
+ ); +}; + +export const basic = () => ; diff --git a/packages/grafana-ui/src/components/Slider/Slider.test.tsx b/packages/grafana-ui/src/components/Slider/Slider.test.tsx new file mode 100644 index 00000000000..90b4d605f44 --- /dev/null +++ b/packages/grafana-ui/src/components/Slider/Slider.test.tsx @@ -0,0 +1,27 @@ +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(); + }); + it('renders correct contents', () => { + const wrapper = mount(); + 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(); + expect(wrapper.html()).toContain('aria-valuenow="15"'); + expect(wrapper.html()).not.toContain('aria-valuenow="20"'); + expect(wrapper.html()).not.toContain('aria-valuenow="10"'); + }); +}); diff --git a/packages/grafana-ui/src/components/Slider/Slider.tsx b/packages/grafana-ui/src/components/Slider/Slider.tsx new file mode 100644 index 00000000000..585e0e0e037 --- /dev/null +++ b/packages/grafana-ui/src/components/Slider/Slider.tsx @@ -0,0 +1,128 @@ +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 = ({ + 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 ( +
+ {/** Slider tooltip's parent component is body and therefore we need Global component to do css overrides for it. */} + + (formatTooltipResult ? formatTooltipResult(value) : value)} + onChange={onChange} + vertical={!isHorizontal} + reverse={reverse} + /> +
+ ); +}; + +Slider.displayName = 'Slider'; diff --git a/packages/grafana-ui/src/components/Slider/_Slider.scss b/packages/grafana-ui/src/components/Slider/_Slider.scss new file mode 100644 index 00000000000..271134d811b --- /dev/null +++ b/packages/grafana-ui/src/components/Slider/_Slider.scss @@ -0,0 +1 @@ +@import '../../node_modules/rc-slider/assets/index.css'; diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss index c22a9f5406c..d6c5dd824ad 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -15,3 +15,4 @@ @import 'Tooltip/Tooltip'; @import 'ValueMappingsEditor/ValueMappingsEditor'; @import 'Alert/Alert'; +@import 'Slider/Slider'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 2efde49203d..f7d678b254b 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -115,6 +115,7 @@ export { Segment, SegmentAsync, SegmentInput, SegmentSelect } from './Segment/'; export { default as Chart } from './Chart'; export { Icon } from './Icon/Icon'; export { Drawer } from './Drawer/Drawer'; +export { Slider } from './Slider/Slider'; // TODO: namespace!! export { diff --git a/packages/grafana-ui/src/types/orientation.ts b/packages/grafana-ui/src/types/orientation.ts new file mode 100644 index 00000000000..5c7be1d0550 --- /dev/null +++ b/packages/grafana-ui/src/types/orientation.ts @@ -0,0 +1 @@ +export type Orientation = 'horizontal' | 'vertical';