chore: migrates from react-sortable-hoc to @dnd-kit

This commit is contained in:
Jacob Fletcher
2022-12-02 13:57:47 -05:00
parent dd217750d7
commit 83eef0bc77
15 changed files with 2583 additions and 2392 deletions

View File

@@ -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);
}
}
}

View File

@@ -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,
},
}}
/>
);
};

View File

@@ -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;
}
}
}

View File

@@ -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>
);
};

View File

@@ -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);
}
}
}
}

View File

@@ -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>
);
};

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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>
);
};

View File

@@ -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);

View File

@@ -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'

View File

@@ -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;
}