mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
@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>
This commit is contained in:
parent
646b74efbb
commit
60dbf72820
@ -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 @@
|
|||||||
"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 @@
|
|||||||
"@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",
|
||||||
|
29
packages/grafana-ui/src/components/Slider/Slider.story.tsx
Normal file
29
packages/grafana-ui/src/components/Slider/Slider.story.tsx
Normal file
@ -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 (
|
||||||
|
<div style={{ width: '200px', height: '200px' }}>
|
||||||
|
<Slider min={min} max={max} orientation={orientation} value={singleValue ? [10] : undefined} reverse={reverse} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const basic = () => <SliderWrapper />;
|
27
packages/grafana-ui/src/components/Slider/Slider.test.tsx
Normal file
27
packages/grafana-ui/src/components/Slider/Slider.test.tsx
Normal file
@ -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(<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"');
|
||||||
|
});
|
||||||
|
});
|
128
packages/grafana-ui/src/components/Slider/Slider.tsx
Normal file
128
packages/grafana-ui/src/components/Slider/Slider.tsx
Normal file
@ -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<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';
|
1
packages/grafana-ui/src/components/Slider/_Slider.scss
Normal file
1
packages/grafana-ui/src/components/Slider/_Slider.scss
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import '../../node_modules/rc-slider/assets/index.css';
|
@ -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/';
|
|||||||
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 {
|
||||||
|
1
packages/grafana-ui/src/types/orientation.ts
Normal file
1
packages/grafana-ui/src/types/orientation.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type Orientation = 'horizontal' | 'vertical';
|
Loading…
Reference in New Issue
Block a user