chore: migrates from react-sortable-hoc to @dnd-kit
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.multi-value {
|
||||
&.rs__multi-value {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: $style-stroke-width-s solid var(--theme-elevation-800);
|
||||
line-height: calc(#{$baseline} - #{$style-stroke-width-s * 2});
|
||||
margin: base(.25) base(.5) base(.25) 0;
|
||||
|
||||
&.draggable {
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
|
||||
.rs__multi-value__remove {
|
||||
padding: 0 base(.125);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-elevation-800);
|
||||
background: var(--theme-error-150);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { MouseEventHandler } from 'react';
|
||||
import {
|
||||
MultiValueProps,
|
||||
components as SelectComponents,
|
||||
} from 'react-select';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { Option as OptionType } from '../types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'multi-value';
|
||||
|
||||
export const MultiValue: React.FC<MultiValueProps<OptionType>> = (props) => {
|
||||
const {
|
||||
className,
|
||||
isDisabled,
|
||||
innerProps,
|
||||
data: {
|
||||
value,
|
||||
},
|
||||
} = props;
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform } = useSortable({
|
||||
id: value as string,
|
||||
});
|
||||
|
||||
const onMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
// prevent the dropdown from opening when clicking on the drag handle
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
className,
|
||||
!isDisabled && 'draggable',
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<SelectComponents.MultiValue
|
||||
{...props}
|
||||
className={classes}
|
||||
innerProps={{
|
||||
...innerProps,
|
||||
ref: setNodeRef,
|
||||
onMouseDown,
|
||||
style: {
|
||||
...transform ? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
} : {},
|
||||
},
|
||||
}}
|
||||
selectProps={{
|
||||
// NOTE: pass the draggable props to the label to act as the draggable handle
|
||||
draggableProps: {
|
||||
...attributes,
|
||||
...listeners,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../../../../scss/styles.scss';
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.multi-value-label {
|
||||
display: flex;
|
||||
@@ -13,8 +13,24 @@
|
||||
justify-content: center;
|
||||
margin-left: base(0.25);
|
||||
|
||||
.icon {
|
||||
width: base(0.75);
|
||||
height: base(0.75);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-error-150);
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
padding: 0 base(.125) 0 base(.25);
|
||||
max-width: 150px;
|
||||
color: currentColor;
|
||||
|
||||
> * {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { components, MultiValueProps } from 'react-select';
|
||||
import { useDocumentDrawer } from '../../DocumentDrawer';
|
||||
import Edit from '../../../icons/Edit';
|
||||
import { Option } from '../../../forms/field-types/Relationship/types';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'multi-value-label';
|
||||
|
||||
export const MultiValueLabel: React.FC<MultiValueProps<Option>> = (props) => {
|
||||
const {
|
||||
data: {
|
||||
value,
|
||||
relationTo,
|
||||
label,
|
||||
},
|
||||
selectProps,
|
||||
} = props;
|
||||
|
||||
const { DocumentDrawer, DocumentDrawerToggler } = useDocumentDrawer();
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__label`}>
|
||||
<components.MultiValueLabel
|
||||
{...props}
|
||||
innerProps={{
|
||||
...selectProps?.draggableProps || {},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{relationTo && (
|
||||
<Fragment>
|
||||
<DocumentDrawerToggler
|
||||
collection={relationTo}
|
||||
id={value.toString()}
|
||||
className={`${baseClass}__drawer-toggler`}
|
||||
aria-label={`Edit ${label}`}
|
||||
>
|
||||
<Edit />
|
||||
</DocumentDrawerToggler>
|
||||
<DocumentDrawer
|
||||
collection={relationTo}
|
||||
id={value.toString()}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.value-container {
|
||||
flex-grow: 1;
|
||||
|
||||
.rs__value-container {
|
||||
padding: base(.25) 0;
|
||||
min-height: base(1.5);
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&--is-multi {
|
||||
margin-left: - base(0.25);
|
||||
padding-top: base(0.25);
|
||||
padding-bottom: base(0.25);
|
||||
|
||||
> * {
|
||||
margin: base(.125);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { components as SelectComponents, ValueContainerProps } from 'react-select';
|
||||
import { Option } from '../types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'value-container';
|
||||
|
||||
export const ValueContainer: React.FC<ValueContainerProps<Option, any>> = (props) => {
|
||||
const {
|
||||
selectProps,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={selectProps.selectProps.droppableRef}
|
||||
className={baseClass}
|
||||
>
|
||||
<SelectComponents.ValueContainer {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,24 +6,7 @@ div.react-select {
|
||||
height: auto;
|
||||
padding-top: base(.25);
|
||||
padding-bottom: base(.25);
|
||||
}
|
||||
|
||||
.rs__value-container {
|
||||
padding: base(.25) 0;
|
||||
min-height: base(1.5);
|
||||
|
||||
>* {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&--is-multi {
|
||||
margin-left: - base(.25);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.rs__indicators {
|
||||
@@ -83,34 +66,6 @@ div.react-select {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.rs__multi-value {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: $style-stroke-width-s solid var(--theme-elevation-800);
|
||||
line-height: calc(#{$baseline} - #{$style-stroke-width-s * 2});
|
||||
margin: base(.25) base(.5) base(.25) 0;
|
||||
|
||||
&.draggable {
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
|
||||
.rs__multi-value__label {
|
||||
padding: 0 base(.125) 0 base(.25);
|
||||
max-width: 150px;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.rs__multi-value__remove {
|
||||
padding: 0 base(.125);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-elevation-800);
|
||||
background: var(--theme-error-150);
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
div.rs__control {
|
||||
background-color: var(--theme-error-200);
|
||||
@@ -120,4 +75,4 @@ div.react-select {
|
||||
&.rs--is-disabled .rs__control {
|
||||
background: var(--theme-elevation-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,35 @@
|
||||
import React, { MouseEventHandler, useCallback } from 'react';
|
||||
import Select, {
|
||||
components as SelectComponents,
|
||||
MultiValueProps,
|
||||
Props as SelectProps,
|
||||
} from 'react-select';
|
||||
import React, { useCallback, useId } from 'react';
|
||||
import {
|
||||
SortableContainer,
|
||||
SortableContainerProps,
|
||||
SortableElement,
|
||||
SortStartHandler,
|
||||
SortEndHandler,
|
||||
SortableHandle,
|
||||
} from 'react-sortable-hoc';
|
||||
DragEndEvent,
|
||||
useDroppable,
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import Select from 'react-select';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { arrayMove } from '../../../../utilities/arrayMove';
|
||||
import { Props, Option as OptionType } from './types';
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { Props } from './types';
|
||||
import Chevron from '../../icons/Chevron';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import { MultiValueLabel } from './MultiValueLabel';
|
||||
import { MultiValue } from './MultiValue';
|
||||
import { ValueContainer } from './ValueContainer';
|
||||
import './index.scss';
|
||||
|
||||
const SortableMultiValue = SortableElement(
|
||||
(props: MultiValueProps<OptionType>) => {
|
||||
// this prevents the menu from being opened/closed when the user clicks
|
||||
// on a value to begin dragging it. ideally, detecting a click (instead of
|
||||
// a drag) would still focus the control and toggle the menu, but that
|
||||
// requires some magic with refs that are out of scope for this example
|
||||
const onMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const classes = [
|
||||
props.className,
|
||||
!props.isDisabled && 'draggable',
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<SelectComponents.MultiValue
|
||||
{...props}
|
||||
className={classes}
|
||||
innerProps={{ ...props.innerProps, onMouseDown }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
const SortableMultiValueLabel = SortableHandle((props) => <SelectComponents.MultiValueLabel {...props} />);
|
||||
|
||||
const SortableSelect = SortableContainer(Select) as React.ComponentClass<SelectProps<OptionType, true> & SortableContainerProps>;
|
||||
|
||||
const ReactSelect: React.FC<Props> = (props) => {
|
||||
const SelectAdapter: React.FC<Props> = (props) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const {
|
||||
className,
|
||||
showError = false,
|
||||
showError,
|
||||
options,
|
||||
onChange,
|
||||
value,
|
||||
@@ -63,12 +37,11 @@ const ReactSelect: React.FC<Props> = (props) => {
|
||||
placeholder = t('general:selectValue'),
|
||||
isSearchable = true,
|
||||
isClearable = true,
|
||||
isMulti,
|
||||
isSortable,
|
||||
filterOption = undefined,
|
||||
isLoading,
|
||||
onMenuOpen,
|
||||
components,
|
||||
droppableRef,
|
||||
} = props;
|
||||
|
||||
const classes = [
|
||||
@@ -77,54 +50,6 @@ const ReactSelect: React.FC<Props> = (props) => {
|
||||
showError && 'react-select--error',
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const onSortStart: SortStartHandler = useCallback(({ helper }) => {
|
||||
const portalNode = helper;
|
||||
if (portalNode && portalNode.style) {
|
||||
portalNode.style.cssText += 'pointer-events: auto; cursor: grabbing;';
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onSortEnd: SortEndHandler = useCallback(({ oldIndex, newIndex }) => {
|
||||
onChange(arrayMove(value as OptionType[], oldIndex, newIndex));
|
||||
}, [onChange, value]);
|
||||
|
||||
if (isMulti && isSortable) {
|
||||
return (
|
||||
<SortableSelect
|
||||
useDragHandle
|
||||
// react-sortable-hoc props:
|
||||
axis="xy"
|
||||
onSortStart={onSortStart}
|
||||
onSortEnd={onSortEnd}
|
||||
// small fix for https://github.com/clauderic/react-sortable-hoc/pull/352:
|
||||
getHelperDimensions={({ node }) => node.getBoundingClientRect()}
|
||||
// react-select props:
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
{...props}
|
||||
value={value as OptionType[]}
|
||||
onChange={onChange}
|
||||
disabled={disabled ? 'disabled' : undefined}
|
||||
className={classes}
|
||||
classNamePrefix="rs"
|
||||
captureMenuScroll
|
||||
options={options}
|
||||
isSearchable={isSearchable}
|
||||
isClearable={isClearable}
|
||||
isLoading={isLoading}
|
||||
onMenuOpen={onMenuOpen}
|
||||
filterOption={filterOption}
|
||||
components={{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore We're failing to provide a required index prop to SortableElement
|
||||
MultiValue: SortableMultiValue,
|
||||
MultiValueLabel: SortableMultiValueLabel,
|
||||
DropdownIndicator: Chevron,
|
||||
...components,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
isLoading={isLoading}
|
||||
@@ -141,7 +66,13 @@ const ReactSelect: React.FC<Props> = (props) => {
|
||||
isClearable={isClearable}
|
||||
filterOption={filterOption}
|
||||
onMenuOpen={onMenuOpen}
|
||||
selectProps={{
|
||||
droppableRef,
|
||||
}}
|
||||
components={{
|
||||
ValueContainer,
|
||||
MultiValue,
|
||||
MultiValueLabel,
|
||||
DropdownIndicator: Chevron,
|
||||
...components,
|
||||
}}
|
||||
@@ -149,4 +80,75 @@ const ReactSelect: React.FC<Props> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SortableSelect: React.FC<Props> = (props) => {
|
||||
const {
|
||||
onChange,
|
||||
value,
|
||||
} = props;
|
||||
|
||||
const uuid = useId();
|
||||
|
||||
const { setNodeRef } = useDroppable({
|
||||
id: uuid,
|
||||
});
|
||||
|
||||
const onDragEnd = useCallback((event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!active || !over) return;
|
||||
|
||||
let sorted = value;
|
||||
|
||||
if (value && Array.isArray(value)) {
|
||||
const oldIndex = value.findIndex((item) => item.value === active.id);
|
||||
const newIndex = value.findIndex((item) => item.value === over.id);
|
||||
sorted = arrayMove(value, oldIndex, newIndex);
|
||||
}
|
||||
|
||||
onChange(sorted);
|
||||
}, [onChange, value]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
|
||||
let ids: string[] = [];
|
||||
if (value) ids = Array.isArray(value) ? value.map((item) => item?.value as string) : [value?.value as string]; // TODO: fix these types
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
onDragEnd={onDragEnd}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
>
|
||||
<SortableContext items={ids}>
|
||||
<SelectAdapter
|
||||
{...props}
|
||||
droppableRef={setNodeRef}
|
||||
/>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
const ReactSelect: React.FC<Props> = (props) => {
|
||||
const {
|
||||
isMulti,
|
||||
isSortable,
|
||||
} = props;
|
||||
|
||||
if (isMulti && isSortable) {
|
||||
return (
|
||||
<SortableSelect {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectAdapter {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ReactSelect;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Ref } from 'react';
|
||||
|
||||
export type Option = {
|
||||
[key: string]: unknown
|
||||
value: unknown
|
||||
@@ -9,6 +11,7 @@ export type OptionGroup = {
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
droppableRef?: Ref<HTMLElement>
|
||||
className?: string
|
||||
value?: Option | Option[],
|
||||
onChange?: (value: any) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { components, MultiValueProps } from 'react-select';
|
||||
import { useDocumentDrawer } from '../../../../elements/DocumentDrawer';
|
||||
import Edit from '../../../../icons/Edit';
|
||||
import { Value, ValueAsObject } from '../types';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'multi-value-label';
|
||||
|
||||
export const CustomEditButton: React.FC<{
|
||||
data: ValueAsObject;
|
||||
}> = ({ data }) => {
|
||||
const { DocumentDrawer, DocumentDrawerToggler } = useDocumentDrawer();
|
||||
return (
|
||||
<Fragment>
|
||||
<DocumentDrawerToggler
|
||||
collection={data.relationTo}
|
||||
id={data.value.toString()}
|
||||
className={`${baseClass}__drawer-toggler`}
|
||||
>
|
||||
<Edit />
|
||||
</DocumentDrawerToggler>
|
||||
<DocumentDrawer
|
||||
collection={data.relationTo}
|
||||
id={data.value.toString()}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export const MultiValueLabel: React.FC<MultiValueProps<{
|
||||
data: Value
|
||||
}>> = (props) => {
|
||||
const {
|
||||
data,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<components.MultiValueLabel {...props} />
|
||||
{typeof data === 'object' && (
|
||||
<CustomEditButton data={data as unknown as ValueAsObject} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -23,7 +23,6 @@ import wordBoundariesRegex from '../../../../../utilities/wordBoundariesRegex';
|
||||
import { AddNewRelation } from './AddNew';
|
||||
import { findOptionsByValue } from './findOptionsByValue';
|
||||
import { GetFilterOptions } from './GetFilterOptions';
|
||||
import { MultiValueLabel } from './MultiValueLabel';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -369,9 +368,6 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
isMulti={hasMany}
|
||||
isSortable={isSortable}
|
||||
isLoading={isLoading}
|
||||
components={{
|
||||
MultiValueLabel,
|
||||
}}
|
||||
onMenuOpen={() => {
|
||||
if (!hasLoadedFirstPage) {
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -19,12 +19,10 @@ export type OptionGroup = {
|
||||
options: Option[]
|
||||
}
|
||||
|
||||
export type ValueAsObject = {
|
||||
export type Value = {
|
||||
relationTo: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
export type Value = ValueAsObject | string | number
|
||||
} | string | number
|
||||
|
||||
type CLEAR = {
|
||||
type: 'CLEAR'
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export function arrayMove<T>(array: readonly T[], from: number, to: number) {
|
||||
const slicedArray = array.slice();
|
||||
slicedArray.splice(
|
||||
to < 0 ? array.length + to : to,
|
||||
0,
|
||||
slicedArray.splice(from, 1)[0],
|
||||
);
|
||||
return slicedArray;
|
||||
}
|
||||
Reference in New Issue
Block a user