diff --git a/package.json b/package.json index 2a450b2b8eb..3afc1e4b30d 100644 --- a/package.json +++ b/package.json @@ -351,7 +351,7 @@ "pseudoizer": "^0.1.0", "rc-cascader": "3.7.0", "rc-drawer": "4.4.3", - "rc-slider": "9.7.5", + "rc-slider": "10.0.1", "rc-time-picker": "3.7.3", "rc-tree": "5.7.0", "re-resizable": "6.9.9", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 58f7d802de2..ccb17faf9b6 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -80,8 +80,9 @@ "prismjs": "1.29.0", "rc-cascader": "3.7.0", "rc-drawer": "4.4.3", - "rc-slider": "9.7.5", + "rc-slider": "10.0.1", "rc-time-picker": "^3.7.3", + "rc-tooltip": "5.2.2", "react-beautiful-dnd": "13.1.0", "react-calendar": "3.9.0", "react-colorful": "5.6.1", diff --git a/packages/grafana-ui/src/components/Slider/HandleTooltip.tsx b/packages/grafana-ui/src/components/Slider/HandleTooltip.tsx new file mode 100644 index 00000000000..387555d06dc --- /dev/null +++ b/packages/grafana-ui/src/components/Slider/HandleTooltip.tsx @@ -0,0 +1,78 @@ +import { css } from '@emotion/css'; +import Tooltip from 'rc-tooltip'; +import React, { useEffect, useRef } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../themes/ThemeContext'; + +interface RCTooltipRef { + // rc-tooltip's ref is essentially untyped, so we be cautious by saying the function is + // potentially undefined which, given rc's track record, seems likely :) + forcePopupAlign?: () => {}; +} + +const HandleTooltip = (props: { + value: number; + children: React.ReactElement; + visible: boolean; + placement: 'top' | 'right'; + tipFormatter?: (value: number) => React.ReactNode; +}) => { + const { value, children, visible, placement, tipFormatter, ...restProps } = props; + + const tooltipRef = useRef(); + const rafRef = useRef(null); + const styles = useStyles2(tooltipStyles); + + function cancelKeepAlign() { + if (rafRef.current !== null) { + cancelAnimationFrame(rafRef.current); + } + } + + function keepAlign() { + rafRef.current = requestAnimationFrame(() => { + tooltipRef.current?.forcePopupAlign?.(); + }); + } + + useEffect(() => { + if (visible) { + keepAlign(); + } else { + cancelKeepAlign(); + } + + return cancelKeepAlign; + }, [value, visible]); + + return ( + + {children} + + ); +}; + +const tooltipStyles = (theme: GrafanaTheme2) => { + return { + tooltip: css({ + position: 'absolute', + display: 'block', + visibility: 'visible', + fontSize: theme.typography.bodySmall.fontSize, + opacity: 0.9, + padding: 3, + }), + }; +}; + +export default HandleTooltip; diff --git a/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx b/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx index a1192f777bf..03b8152299b 100644 --- a/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx +++ b/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx @@ -27,7 +27,15 @@ const meta: ComponentMeta = { export const Basic: ComponentStory = (args) => { return (
- + +
+ ); +}; + +export const Vertical: ComponentStory = (args) => { + return ( +
+
); }; diff --git a/packages/grafana-ui/src/components/Slider/RangeSlider.tsx b/packages/grafana-ui/src/components/Slider/RangeSlider.tsx index 4b32222692f..b8b1f4d436c 100644 --- a/packages/grafana-ui/src/components/Slider/RangeSlider.tsx +++ b/packages/grafana-ui/src/components/Slider/RangeSlider.tsx @@ -1,10 +1,11 @@ import { cx } from '@emotion/css'; import { Global } from '@emotion/react'; -import { Range as RangeComponent, createSliderWithTooltip } from 'rc-slider'; -import React, { FunctionComponent } from 'react'; +import Slider, { SliderProps } from 'rc-slider'; +import React, { FunctionComponent, useCallback } from 'react'; import { useTheme2 } from '../../themes/ThemeContext'; +import HandleTooltip from './HandleTooltip'; import { getStyles } from './styles'; import { RangeSliderProps } from './types'; @@ -25,33 +26,59 @@ export const RangeSlider: FunctionComponent = ({ value, tooltipAlwaysVisible = true, }) => { + const handleChange = useCallback( + (v: number | number[]) => { + const value = typeof v === 'number' ? [v, v] : v; + onChange?.(value); + }, + [onChange] + ); + + const handleAfterChange = useCallback( + (v: number | number[]) => { + const value = typeof v === 'number' ? [v, v] : v; + onAfterChange?.(value); + }, + [onAfterChange] + ); + const isHorizontal = orientation === 'horizontal'; const theme = useTheme2(); const styles = getStyles(theme, isHorizontal); - const RangeWithTooltip = createSliderWithTooltip(RangeComponent); + + const tipHandleRender: SliderProps['handleRender'] = (node, handleProps) => { + return ( + + {node} + + ); + }; + 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} - onAfterChange={onAfterChange} + range={true} + onChange={handleChange} + onAfterChange={handleAfterChange} vertical={!isHorizontal} reverse={reverse} // TODO: The following is a temporary work around for making content after the slider accessible and it will be removed when fixing the slider in public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx. tabIndex={[0, 1]} + handleRender={tipHandleRender} />
); }; -RangeSlider.displayName = 'Range'; +RangeSlider.displayName = 'RangeSlider'; diff --git a/packages/grafana-ui/src/components/Slider/Slider.tsx b/packages/grafana-ui/src/components/Slider/Slider.tsx index 7f161850ec2..e566b1455e6 100644 --- a/packages/grafana-ui/src/components/Slider/Slider.tsx +++ b/packages/grafana-ui/src/components/Slider/Slider.tsx @@ -32,12 +32,11 @@ export const Slider: FunctionComponent = ({ const [sliderValue, setSliderValue] = useState(value ?? min); const onSliderChange = useCallback( - (v: number) => { - setSliderValue(v); + (v: number | number[]) => { + const value = typeof v === 'number' ? v : v[0]; - if (onChange) { - onChange(v); - } + setSliderValue(value); + onChange?.(value); }, [setSliderValue, onChange] ); @@ -78,6 +77,14 @@ export const Slider: FunctionComponent = ({ [max, min] ); + const handleAfterChange = useCallback( + (v: number | number[]) => { + const value = typeof v === 'number' ? v : v[0]; + onAfterChange?.(value); + }, + [onAfterChange] + ); + const sliderInputClassNames = !isHorizontal ? [styles.sliderInputVertical] : []; const sliderInputFieldClassNames = !isHorizontal ? [styles.sliderInputFieldVertical] : []; @@ -85,7 +92,7 @@ export const Slider: FunctionComponent = ({
{/** Slider tooltip's parent component is body and therefore we need Global component to do css overrides for it. */} -
); }; diff --git a/packages/grafana-ui/src/components/Slider/styles.ts b/packages/grafana-ui/src/components/Slider/styles.ts index deee13ea378..c10e6953b8f 100644 --- a/packages/grafana-ui/src/components/Slider/styles.ts +++ b/packages/grafana-ui/src/components/Slider/styles.ts @@ -44,14 +44,21 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2, isHorizontal: bool background-color: ${handleColor}; box-shadow: ${theme.shadows.z1}; cursor: pointer; + opacity: 1; } + .rc-slider-handle:hover, .rc-slider-handle:active, - .rc-slider-handle:focus, .rc-slider-handle-click-focused:focus { ${hoverSyle}; } + // The triple class names is needed because that's the specificity used in the source css :( + .rc-slider-handle-dragging.rc-slider-handle-dragging.rc-slider-handle-dragging, + .rc-slider-handle:focus-visible { + box-shadow: 0 0 0 5px ${theme.colors.text.primary}; + } + .rc-slider-dot, .rc-slider-dot-active { background-color: ${theme.colors.text.primary}; diff --git a/public/app/core/components/OptionsUI/slider.tsx b/public/app/core/components/OptionsUI/slider.tsx index 016b3b65218..be4d46de1fd 100644 --- a/public/app/core/components/OptionsUI/slider.tsx +++ b/public/app/core/components/OptionsUI/slider.tsx @@ -1,6 +1,6 @@ import { css, cx } from '@emotion/css'; import { Global } from '@emotion/react'; -import SliderComponent from 'rc-slider'; +import Slider from 'rc-slider'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { FieldConfigEditorProps, GrafanaTheme2, SliderFieldConfigSettings } from '@grafana/data'; @@ -30,7 +30,6 @@ export const SliderValueEditor: React.FC(value ?? min); const [inputWidth, setInputWidth] = useState(inputWidthDefault); @@ -60,11 +59,12 @@ export const SliderValueEditor: React.FC { - setSliderValue(v); + (v: number | number[]) => { + const value = typeof v === 'number' ? v : v[0]; + setSliderValue(value); if (onChange) { - onChange(v); + onChange(value); } }, [setSliderValue, onChange] @@ -97,7 +97,7 @@ export const SliderValueEditor: React.FC
- =16.9.0" react-dom: ">=16.9.0" - checksum: e5b7b8e6036ea51d88e40c11c46e9c1639f5133fd1a6b5353c87547b1bc92393a291f9d0772a0465d1d139c9dbd14a349838861df86d33b1abb6a94593e7b17e + checksum: 803f0cc39d43897c0b24549e87232a668d26ff5b0e14b528fd454aa455cdf96ebc60654832c51bb1a6c7b7594ca39017d6c96b3237662471efb863f1723e3d9c languageName: node linkType: hard @@ -31884,16 +31884,17 @@ __metadata: languageName: node linkType: hard -"rc-tooltip@npm:^5.0.1": - version: 5.1.1 - resolution: "rc-tooltip@npm:5.1.1" +"rc-tooltip@npm:5.2.2": + version: 5.2.2 + resolution: "rc-tooltip@npm:5.2.2" dependencies: "@babel/runtime": ^7.11.2 + classnames: ^2.3.1 rc-trigger: ^5.0.0 peerDependencies: react: ">=16.9.0" react-dom: ">=16.9.0" - checksum: e65b175c28356cee101db56217d02a1eb1d96e727620bb1f6551f98e3ab49d3ec4381f1d408d19054b46349cba0e89056a4b4f09eeebbe70a1013733ad0e1fc9 + checksum: df6a59096876becf930df0347cfe6379cde9647f338a333dd0aae99039bf45e72db866f03ab6b5fd5ce616b074ec888f50e61ebe0f8d2a135c2617595dbf0583 languageName: node linkType: hard @@ -31944,7 +31945,23 @@ __metadata: languageName: node linkType: hard -"rc-trigger@npm:^5.0.0, rc-trigger@npm:^5.0.4": +"rc-trigger@npm:^5.0.0": + version: 5.3.1 + resolution: "rc-trigger@npm:5.3.1" + dependencies: + "@babel/runtime": ^7.18.3 + classnames: ^2.2.6 + rc-align: ^4.0.0 + rc-motion: ^2.0.0 + rc-util: ^5.19.2 + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 29302e9a0a66eb26cef5ce0b291ada9bb3e284f19980bf02da6863a5306d5de863aa87fbbf30c176a4453c13db160b69789ac50aae09af8e5c53942ac0368a3b + languageName: node + linkType: hard + +"rc-trigger@npm:^5.0.4": version: 5.2.10 resolution: "rc-trigger@npm:5.2.10" dependencies: @@ -31987,6 +32004,20 @@ __metadata: languageName: node linkType: hard +"rc-util@npm:^5.18.1, rc-util@npm:^5.19.2": + version: 5.24.4 + resolution: "rc-util@npm:5.24.4" + dependencies: + "@babel/runtime": ^7.18.3 + react-is: ^16.12.0 + shallowequal: ^1.1.0 + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: efda306dcf9eeb0df6d3a8d315d18ec73c653d463174355bd91d34f4d42acc092b2d6ffee47993312841ea4d13b4fe036c764a3a46fa935f307d51a8807e2c10 + languageName: node + linkType: hard + "rc-util@npm:^5.2.1, rc-util@npm:^5.3.0, rc-util@npm:^5.5.0, rc-util@npm:^5.7.0": version: 5.14.0 resolution: "rc-util@npm:5.14.0"