Skip to content

Commit

Permalink
[fix] Use floating-ui to deal with closing on click outside (#2886)
Browse files Browse the repository at this point in the history
- use `floating-ui` in `AnimationSpeedSlider` and `ColorSelector` to deal with closing on click outside.
- For both of the above components, this fixes the component from disappearing immediately in a programable environment even when clicking internally, bypassing important onClick event logic.

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>
  • Loading branch information
igorDykhta authored Jan 3, 2025
1 parent 4bcf55b commit 92c9e6a
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

import React, {useCallback} from 'react';
import styled from 'styled-components';
import RangeSliderFactory from '../range-slider';
import {useDismiss, useFloating, useInteractions} from '@floating-ui/react';

import {SPEED_CONTROL_RANGE, SPEED_CONTROL_STEP} from '@kepler.gl/constants';
import useOnClickOutside from '../../hooks/use-on-click-outside';

import RangeSliderFactory from '../range-slider';

const SliderWrapper = styled.div`
position: relative;
Expand Down Expand Up @@ -41,13 +43,27 @@ export default function AnimationSpeedSliderFactory(
onHide,
speed,
updateAnimationSpeed
}) => {
const ref = useOnClickOutside<HTMLDivElement>(onHide);
}: AnimationSpeedSliderProps) => {
// floating-ui boilerplate to establish close on outside click
const {refs, context} = useFloating({
open: true,
onOpenChange: v => {
if (!v) {
onHide();
}
}
});
const dismiss = useDismiss(context);
const {getFloatingProps} = useInteractions([dismiss]);

const onChange = useCallback(v => updateAnimationSpeed(v[1]), [updateAnimationSpeed]);

return (
<SpeedSliderContainer className="animation-control__speed-slider" ref={ref}>
<SpeedSliderContainer
className="animation-control__speed-slider"
ref={refs.setFloating}
{...getFloatingProps()}
>
<SliderWrapper>
<RangeSlider
range={SPEED_CONTROL_RANGE}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ const ExportJsonMapUnmemoized = ({config = {}}: ExportJsonPropTypes) => {
<div className="viewer">
<JSONPretty id="json-pretty" json={config} />
<CopyToClipboard text={JSON.stringify(config)} onCopy={() => setCopy(true)}>
<Button width="80px" className="copy-button">{copied ? 'Copied!' : 'Copy'}</Button>
<Button width="80px" className="copy-button">
{copied ? 'Copied!' : 'Copy'}
</Button>
</CopyToClipboard>
</div>
<div className="disclaimer">
Expand Down
101 changes: 64 additions & 37 deletions src/components/src/side-panel/layer-panel/color-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
// SPDX-License-Identifier: MIT
// Copyright contributors to the kepler.gl project

import React, {useCallback, useState, MouseEvent} from 'react';
import {FormattedMessage} from 'react-intl';
import styled from 'styled-components';
import {useDismiss, useFloating, useInteractions} from '@floating-ui/react';

import {ColorRange} from '@kepler.gl/types';
import {ColorUI, NestedPartial, RGBAColor, RGBColor} from '@kepler.gl/types';
import {rgbToHex} from '@kepler.gl/utils';
import React, {MouseEvent, ComponentType, useState, useCallback} from 'react';
import {FormattedMessage} from 'react-intl';
import styled from 'styled-components';

import RangeSliderFactory from '../../common/range-slider';
import {PanelLabel, StyledPanelDropdown} from '../../common/styled-components';
import ColorPalette from './color-palette';
import ColorRangeSelectorFactory from './color-range-selector';
import SingleColorPalette from './single-color-palette';

import useOnClickOutside from '../../hooks/use-on-click-outside';

type ColorSelectorInputProps = {
active: boolean;
disabled?: boolean;
Expand Down Expand Up @@ -61,6 +62,12 @@ export const ColorBlock = styled.div<{backgroundcolor: RGBColor}>`
: 'transparent'};
`;

const StyledColorSelectorWrapper = styled.div`
.selector__dropdown {
max-height: 600px; /* increase from the default 500px defined by StyledPanelDropdown */
}
`;

export const ColorSelectorInput = styled.div<ColorSelectorInputProps>`
${props => (props.inputTheme === 'secondary' ? props.theme.secondaryInput : props.theme.input)};
height: ${props => props.theme.inputBoxHeight};
Expand All @@ -87,33 +94,36 @@ export const InputBoxContainer = styled.div`

ColorSelectorFactory.deps = [ColorRangeSelectorFactory, RangeSliderFactory];

function ColorSelectorFactory(ColorRangeSelector, RangeSlider): ComponentType<ColorSelectorProps> {
function ColorSelectorFactory(
ColorRangeSelector: ReturnType<typeof ColorRangeSelectorFactory>,
RangeSlider: ReturnType<typeof RangeSliderFactory>
): React.FC<ColorSelectorProps> {
const ColorSelector: React.FC<ColorSelectorProps> = ({
colorSets = [],
colorUI,
inputTheme,
disabled,
useOpacity,
setColorUI
}) => {
const [showDropdown, setShowDropdown] = useState(false);
}: ColorSelectorProps) => {
const [showDropdown, setShowDropdown] = useState(colorUI ? colorUI.showDropdown : false);
const showSketcher = colorUI ? colorUI.showSketcher : false;

const editing = colorUI ? colorUI.showDropdown : showDropdown;
const currentEditing =
typeof editing === 'number' && colorSets[editing] && typeof colorSets[editing] === 'object';
const editingLookup = colorUI ? colorUI.showDropdown : showDropdown;
const editingColorSet: ColorSet | false =
typeof editingLookup === 'number' && colorSets[editingLookup]
? colorSets[editingLookup]
: false;

const closePanelDropdown = useCallback(() => {
if (editing === false) {
if (editingLookup === false) {
return;
}

if (setColorUI) {
setColorUI({showDropdown: false, showSketcher: false});
} else {
setShowDropdown(false);
}
}, [editing, setColorUI, setShowDropdown]);
}, [editingLookup, setColorUI, setShowDropdown]);

const handleClickOutside = useCallback(() => {
if (Number.isInteger(showSketcher)) {
Expand All @@ -123,9 +133,19 @@ function ColorSelectorFactory(ColorRangeSelector, RangeSlider): ComponentType<Co
closePanelDropdown();
}, [showSketcher, closePanelDropdown]);

const ref = useOnClickOutside<HTMLDivElement>(handleClickOutside);
// floating-ui boilerplate to establish close on outside click
const {refs, context} = useFloating({
open: true,
onOpenChange: v => {
if (!v) {
handleClickOutside();
}
}
});
const dismiss = useDismiss(context);
const {getFloatingProps} = useInteractions([dismiss]);

const onSetColor = useCallback(
const setColor = useCallback(
(colorSet: ColorSet, color: RGBColor | RGBAColor | ColorRange, opacity: number) => {
const {setColor} = colorSet || {};
if (!setColor) {
Expand All @@ -143,47 +163,54 @@ function ColorSelectorFactory(ColorRangeSelector, RangeSlider): ComponentType<Co
const onSelectColor = useCallback(
(color: RGBColor | ColorRange, e: MouseEvent) => {
if (e) e.stopPropagation();
const colorSet = typeof editing === 'number' && colorSets[editing];
const colorSet = editingColorSet;
if (colorSet) {
onSetColor(colorSet, color, colorSet.selectedColor[3]);
setColor(colorSet, color, colorSet.selectedColor[3]);
}
},
[editing, colorSets, onSetColor]
[colorSets, editingColorSet, setColor]
);

const onSelectOpacity = useCallback(
(opacity: number[], e: MouseEvent) => {
(opacity: number[], e: Event | null | undefined) => {
if (e) e.stopPropagation();
const colorSet = typeof editing === 'number' && colorSets[editing];
const colorSet = editingColorSet;
if (colorSet) {
onSetColor(colorSet, colorSet.selectedColor, Math.round(opacity[1] * 255));
setColor(colorSet, colorSet.selectedColor, Math.round(opacity[1] * 255));
}
},
[onSetColor, colorSets, editing]
[colorSets, editingColorSet, setColor]
);

const onToggleDropdown = useCallback(
(e, i) => {
e.stopPropagation();
e.preventDefault();

const showDropdownValue =
editingLookup === false
? i // open it for the specific color set index
: false; // close it
if (setColorUI) {
setColorUI({showDropdown: i});
setColorUI({showDropdown: showDropdownValue});
} else {
setShowDropdown(i);
setShowDropdown(showDropdownValue);
}
},
[setColorUI, setShowDropdown]
[editingLookup, setColorUI, setShowDropdown]
);

return (
<div className="color-selector">
<StyledColorSelectorWrapper
className="color-selector"
ref={refs.setFloating}
{...getFloatingProps()}
>
<InputBoxContainer>
{colorSets.map((cSet, i) => (
<div className="color-select__input-group" key={i}>
<ColorSelectorInput
className="color-selector__selector"
active={editing === i}
active={editingLookup === i}
disabled={disabled}
inputTheme={inputTheme}
onClick={e => onToggleDropdown(e, i)}
Expand All @@ -203,18 +230,18 @@ function ColorSelectorFactory(ColorRangeSelector, RangeSlider): ComponentType<Co
</div>
))}
</InputBoxContainer>
{currentEditing ? (
<StyledPanelDropdown className="color-selector__dropdown" ref={ref}>
{colorSets[editing as number].isRange ? (
{editingColorSet ? (
<StyledPanelDropdown className="color-selector__dropdown">
{editingColorSet.isRange && colorUI && setColorUI ? (
<ColorRangeSelector
selectedColorRange={colorSets[editing as number].selectedColor as ColorRange}
selectedColorRange={editingColorSet.selectedColor as ColorRange}
onSelectColorRange={onSelectColor}
setColorPaletteUI={setColorUI}
colorPaletteUI={colorUI as ColorUI}
/>
) : (
<SingleColorPalette
selectedColor={rgbToHex(colorSets[editing as number].selectedColor as RGBColor)}
selectedColor={rgbToHex(editingColorSet.selectedColor as RGBColor)}
onSelectColor={onSelectColor}
/>
)}
Expand All @@ -225,14 +252,14 @@ function ColorSelectorFactory(ColorRangeSelector, RangeSlider): ComponentType<Co
</PanelLabel>
<RangeSlider
{...OPACITY_SLIDER_PROPS}
value1={colorSets[editing as number].selectedColor[3] / 255}
value1={editingColorSet.selectedColor[3] / 255}
onChange={onSelectOpacity}
/>
</OpacitySliderWrapper>
) : null}
</StyledPanelDropdown>
) : null}
</div>
</StyledColorSelectorWrapper>
);
};

Expand Down
4 changes: 2 additions & 2 deletions src/schemas/src/schema-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ export class KeplerGLSchema {
validVersions = VERSIONS,
version = CURRENT_VERSION,
composedReducerSchema
}: KeplerGLSchemaProps = {}) {
}: KeplerGLSchemaProps = {}) {
this._validVersions = validVersions;
this._version = version;
this._reducerSchemas = reducers;
this._datasetSchema = datasets;
this._composedReducerSchema = composedReducerSchema || null;
this._composedReducerSchema = composedReducerSchema || null;

this._datasetLastSaved = null;
this._savedDataset = null;
Expand Down
11 changes: 9 additions & 2 deletions src/utils/src/color-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import {
DEFAULT_CUSTOM_PALETTE,
colorPaletteToColorRange
} from '@kepler.gl/constants';
import {ColorMap, ColorRange, ColorRangeConfig, HexColor, RGBColor} from '@kepler.gl/types';
import {
ColorMap,
ColorRange,
ColorRangeConfig,
HexColor,
RGBAColor,
RGBColor
} from '@kepler.gl/types';
import {rgb as d3Rgb} from 'd3-color';
import {interpolate} from 'd3-interpolate';
import {arrayInsert, arrayMove} from './utils';
Expand Down Expand Up @@ -51,7 +58,7 @@ function PadNum(c) {
* @param rgb
* @returns hex string
*/
export function rgbToHex([r, g, b]: RGBColor): HexColor {
export function rgbToHex([r, g, b]: RGBColor | RGBAColor): HexColor {
return `#${[r, g, b].map(n => PadNum(n)).join('')}`.toUpperCase();
}

Expand Down

0 comments on commit 92c9e6a

Please sign in to comment.