Skip to content

Commit

Permalink
[feat] reorder tooltips (#2378)
Browse files Browse the repository at this point in the history
Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>
Co-authored-by: Rui Wang <64480766+albatross97@users.noreply.github.com>
  • Loading branch information
igorDykhta and albatross97 committed Oct 20, 2023
1 parent fdecb05 commit 2500a27
Show file tree
Hide file tree
Showing 5 changed files with 316 additions and 145 deletions.
3 changes: 3 additions & 0 deletions src/components/src/common/field-selector.tsx
Expand Up @@ -113,6 +113,7 @@ interface FieldSelectorFactoryProps {
suggested?: ReadonlyArray<string | number | boolean | object> | null;
CustomChickletComponent?: ComponentType<any>;
size?: string;
reorderItems?: (newOrder: any) => void;
}

function FieldSelectorFactory(
Expand All @@ -124,6 +125,7 @@ function FieldSelectorFactory(
error: false,
fields: [],
onSelect: () => {},
reorderItems: () => {},
placement: 'bottom',
value: null,
multiSelect: false,
Expand Down Expand Up @@ -194,6 +196,7 @@ function FieldSelectorFactory(
placeholder={this.props.placeholder}
placement={this.props.placement}
onChange={this.props.onSelect}
reorderItems={this.props.reorderItems}
DropDownLineItemRenderComponent={this.fieldListItemSelector(this.props)}
DropdownHeaderComponent={this.props.suggested ? SuggestedFieldHeader : null}
CustomChickletComponent={this.props.CustomChickletComponent}
Expand Down
145 changes: 113 additions & 32 deletions src/components/src/common/item-selector/chickleted-input.tsx
Expand Up @@ -18,9 +18,13 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import React, {ElementType, MouseEventHandler, ReactNode} from 'react';
import React, {ElementType, MouseEventHandler, ReactNode, useMemo, useCallback} from 'react';

import styled from 'styled-components';
import {DndContext, DragOverlay, pointerWithin} from '@dnd-kit/core';
import {SortableContext, useSortable, arrayMove} from '@dnd-kit/sortable';
import {restrictToParentElement} from '@dnd-kit/modifiers';

import Delete from '../icons/delete';
import {FormattedMessage} from '@kepler.gl/localization';

Expand All @@ -39,6 +43,7 @@ interface ChickletedInputProps {
inputTheme?: string;
CustomChickletComponent?: ElementType;
className?: string;
reorderItems?: (newOrder: any) => void;
}

interface ChickletButtonProps {
Expand All @@ -65,6 +70,7 @@ export const ChickletButton = styled.div<ChickletButtonProps>`
}
`;

const DND_MODIFIERS = [restrictToParentElement];
export const ChickletTag = styled.span`
margin-right: 10px;
text-overflow: ellipsis;
Expand Down Expand Up @@ -108,46 +114,121 @@ const ChickletedInputContainer = styled.div<ChickletedInputContainerProps>`
overflow: hidden;
`;

const ChickletedItem = ({
item,
removeItem,
displayOption,
CustomChickletComponent,
inputTheme,
disabled,
itemId
}) => {
const {attributes, listeners, setNodeRef, transform, transition, isDragging} = useSortable({
id: itemId
});
const chickletProps = useMemo(
() => ({
inputTheme,
disabled,
name: displayOption(item),
displayOption,
item,
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
remove: e => removeItem(item, e)
}),
[
item,
removeItem,
displayOption,
CustomChickletComponent,
inputTheme,
disabled,
itemId,
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging
]
);
return CustomChickletComponent ? (
<CustomChickletComponent {...chickletProps} />
) : (
<Chicklet {...chickletProps} />
);
};

const ChickletedInput: React.FC<ChickletedInputProps> = ({
disabled,
onClick,
className,
selectedItems = [],
placeholder = '',
removeItem,
reorderItems = d => d,
displayOption = d => d,
inputTheme,
CustomChickletComponent
}) => (
<ChickletedInputContainer
className={`${className} chickleted-input`}
onClick={onClick}
inputTheme={inputTheme}
hasPlaceholder={!selectedItems || !selectedItems.length}
>
{selectedItems.length > 0 ? (
selectedItems.map((item, i) => {
const chickletProps = {
inputTheme,
disabled,
key: `${displayOption(item)}_${i}`,
name: displayOption(item),
displayOption,
item,
remove: e => removeItem(item, e)
};
return CustomChickletComponent ? (
<CustomChickletComponent {...chickletProps} />
) : (
<Chicklet {...chickletProps} />
);
})
) : (
<span className={`${className} chickleted-input__placeholder`}>
<FormattedMessage id={placeholder || 'placeholder.enterValue'} />
</span>
)}
</ChickletedInputContainer>
);
}) => {
const selectedItemIds = useMemo(() => selectedItems.map(item => displayOption(item)), [
displayOption,
selectedItems
]);
const handleDragEnd = useCallback(
({active, over}) => {
if (!over) return;
if (active.id !== over.id) {
const oldIndex = selectedItemIds.findIndex(itemId => itemId === active.id);
const newIndex = selectedItemIds.findIndex(itemId => itemId === over.id);
reorderItems(arrayMove(selectedItems, oldIndex, newIndex));
}
},
[selectedItems, displayOption, reorderItems]
);

return (
<ChickletedInputContainer
className={`${className} chickleted-input`}
onClick={onClick}
inputTheme={inputTheme}
hasPlaceholder={!selectedItems || !selectedItems.length}
>
<DndContext
onDragEnd={handleDragEnd}
modifiers={DND_MODIFIERS}
collisionDetection={pointerWithin}
autoScroll={false}
>
<SortableContext items={selectedItemIds}>
{selectedItems.length > 0 ? (
selectedItems.map((item, index) => (
<ChickletedItem
item={item}
itemId={displayOption(item)}
removeItem={removeItem}
displayOption={displayOption}
CustomChickletComponent={CustomChickletComponent}
disabled={disabled}
inputTheme={inputTheme}
key={`${displayOption(item)}_${index}`}
/>
))
) : (
<span className={`${className} chickleted-input__placeholder`}>
<FormattedMessage id={placeholder || 'placeholder.enterValue'} />
</span>
)}
</SortableContext>
<DragOverlay dropAnimation={null} />
</DndContext>
</ChickletedInputContainer>
);
};

export default ChickletedInput;
5 changes: 4 additions & 1 deletion src/components/src/common/item-selector/item-selector.tsx
Expand Up @@ -154,6 +154,7 @@ export type ItemSelectorProps = {
CustomChickletComponent?: ComponentType<any>;
intl: IntlShape;
className?: string;
reorderItems?: (newOrder: any) => void;
showDropdownOnMount?: boolean;
};

Expand All @@ -164,7 +165,8 @@ class ItemSelectorUnmemoized extends Component<ItemSelectorProps> {
closeOnSelect: true,
searchable: true,
DropDownRenderComponent: DropdownList,
DropDownLineItemRenderComponent: ListItem
DropDownLineItemRenderComponent: ListItem,
reorderItems: undefined
};

state = {
Expand Down Expand Up @@ -342,6 +344,7 @@ class ItemSelectorUnmemoized extends Component<ItemSelectorProps> {
selectedItems={toArray(this.props.selectedItems)}
placeholder={this.props.placeholder}
removeItem={this._removeItem}
reorderItems={this.props.reorderItems}
CustomChickletComponent={this.props.CustomChickletComponent}
inputTheme={inputTheme}
/>
Expand Down
107 changes: 63 additions & 44 deletions src/components/src/side-panel/interaction-panel/tooltip-config.tsx
Expand Up @@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import React from 'react';
import React, {useCallback} from 'react';
import styled from 'styled-components';
import {injectIntl, IntlShape} from 'react-intl';
import {FormattedMessage} from '@kepler.gl/localization';
Expand Down Expand Up @@ -119,27 +119,62 @@ function TooltipConfigFactory(
onDisplayFormatChange
}: DatasetTooltipConfigProps) => {
const dataId = dataset.id;

const handleClick = useCallback(
() =>
onChange({
...config,
fieldsToShow: {
...config.fieldsToShow,
[dataId]: []
}
}),
[config, dataId, onChange]
);

const findSelectedHelper = useCallback((selected, tooltipFields) => {
return selected.map(
f =>
tooltipFields.find(tooltipField => tooltipField.name === f.name) || {
name: f.name,
// default initial tooltip is null
format: null
}
);
}, []);

const handleSelect = useCallback(
selected => {
const newConfig: DatasetTooltipConfigProps['config'] = {
...config,
fieldsToShow: {
...config.fieldsToShow,
[dataId]: findSelectedHelper(selected, config.fieldsToShow[dataId])
}
};
onChange(newConfig);
},
[config, dataId, onChange]
);

const handleReorderItems = useCallback(
newOrder =>
onChange({
...config,
fieldsToShow: {
...config.fieldsToShow,
[dataId]: newOrder
}
}),
[config, dataId, onChange]
);
return (
<SidePanelSection key={dataId}>
<SBFlexboxNoMargin>
<DatasetTag dataset={dataset} />
{Boolean(config.fieldsToShow[dataId].length) && (
<ButtonWrapper>
<Button
className="clear-all"
onClick={() => {
const newConfig = {
...config,
fieldsToShow: {
...config.fieldsToShow,
[dataId]: []
}
};
onChange(newConfig);
}}
width="54px"
secondary
>
<Button className="clear-all" onClick={handleClick} width="54px" secondary>
<FormattedMessage id="fieldSelector.clearAll" />
</Button>
</ButtonWrapper>
Expand All @@ -148,26 +183,8 @@ function TooltipConfigFactory(
<FieldSelector
fields={dataset.fields}
value={config.fieldsToShow[dataId]}
onSelect={selected => {
const newConfig: DatasetTooltipConfigProps['config'] = {
...config,
fieldsToShow: {
...config.fieldsToShow,
// @ts-expect-error
[dataId]: selected.map(
f =>
config.fieldsToShow[dataId].find(
tooltipField => tooltipField.name === f.name
) || {
name: f.name,
// default initial tooltip is null
format: null
}
)
}
};
onChange(newConfig);
}}
onSelect={handleSelect}
reorderItems={handleReorderItems}
closeOnSelect={false}
multiSelect
inputTheme="secondary"
Expand All @@ -190,6 +207,15 @@ function TooltipConfigFactory(
onDisplayFormatChange,
intl
}: TooltipConfigProps) => {
const handleChange = useCallback(
(option: string | number | boolean | object | null) =>
onChange({
...config,
compareType: option as string | null
}),
[config, onChange]
);

return (
<TooltipConfigWrapper>
{Object.keys(config.fieldsToShow).map(dataId =>
Expand Down Expand Up @@ -235,14 +261,7 @@ function TooltipConfigFactory(
searchable={false}
inputTheme={'secondary'}
getOptionValue={d => d}
onChange={option => {
const newConfig: TooltipConfigProps['config'] = {
...config,
// @ts-expect-error
compareType: option
};
onChange(newConfig);
}}
onChange={handleChange}
/>
</SidePanelSection>
</TooltipConfigWrapper>
Expand Down

0 comments on commit 2500a27

Please sign in to comment.