mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 10:03:33 -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"
|
||||
},
|
||||
"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",
|
||||
|
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 'ValueMappingsEditor/ValueMappingsEditor';
|
||||
@import 'Alert/Alert';
|
||||
@import 'Slider/Slider';
|
||||
|
@ -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 {
|
||||
|
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