-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added reorderable list and updated fieldProps (#57)
- Loading branch information
1 parent
d3b87b2
commit 5a074b5
Showing
2 changed files
with
175 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import { Fragment, JSXElementConstructor, ReactElement, useEffect, useState } from 'react'; | ||
|
||
import { Field, FieldProps, Focusable, GamepadButton } from '../deck-components'; | ||
|
||
/** | ||
* A ReorderableList entry of type <T>. | ||
* @param label The name of this entry in the list. | ||
* @param data Optional data to connect to this entry. | ||
* @param position The position of this entry in the list. | ||
*/ | ||
export type ReorderableEntry<T> = { | ||
label: string; | ||
data?: T; | ||
position: number; | ||
}; | ||
|
||
/** | ||
* Properties for a ReorderableList component of type <T>. | ||
* | ||
* @param animate If the list should animate. @default true | ||
*/ | ||
export type ReorderableListProps<T> = { | ||
entries: ReorderableEntry<T>[]; | ||
onSave: (entries: ReorderableEntry<T>[]) => void; | ||
interactables?: JSXElementConstructor<{ entry: ReorderableEntry<T> }>; | ||
fieldProps?: FieldProps; | ||
animate?: boolean; | ||
}; | ||
|
||
/** | ||
* A component for creating reorderable lists. | ||
* | ||
* See an example implementation {@linkplain https://github.com/Tormak9970/Component-Testing-Plugin/blob/main/src/testing-window/ReorderableListTest.tsx here}. | ||
*/ | ||
export function ReorderableList<T>(props: ReorderableListProps<T>) { | ||
if (props.animate === undefined) props.animate = true; | ||
const [entryList, setEntryList] = useState<ReorderableEntry<T>[]>( | ||
props.entries.sort((a: ReorderableEntry<T>, b: ReorderableEntry<T>) => a.position - b.position), | ||
); | ||
const [reorderEnabled, setReorderEnabled] = useState<boolean>(false); | ||
|
||
useEffect(() => { | ||
setEntryList(props.entries.sort((a: ReorderableEntry<T>, b: ReorderableEntry<T>) => a.position - b.position)); | ||
}, [props.entries]); | ||
|
||
function toggleReorderEnabled(): void { | ||
let newReorderValue = !reorderEnabled; | ||
setReorderEnabled(newReorderValue); | ||
|
||
if (!newReorderValue) { | ||
props.onSave(entryList); | ||
} | ||
} | ||
|
||
return ( | ||
<Fragment> | ||
<div | ||
style={{ | ||
width: 'inherit', | ||
height: 'inherit', | ||
flex: '1 1 1px', | ||
scrollPadding: '48px 0px', | ||
display: 'flex', | ||
flexDirection: 'column', | ||
justifyContent: 'flex-start', | ||
alignContent: 'stretch', | ||
}} | ||
> | ||
<Focusable | ||
onSecondaryButton={toggleReorderEnabled} | ||
onSecondaryActionDescription={reorderEnabled ? 'Save Order' : 'Reorder'} | ||
onClick={toggleReorderEnabled} | ||
> | ||
{entryList.map((entry: ReorderableEntry<T>) => ( | ||
<ReorderableItem | ||
animate={props.animate!} | ||
listData={entryList} | ||
entryData={entry} | ||
reorderEntryFunc={setEntryList} | ||
reorderEnabled={reorderEnabled} | ||
fieldProps={props.fieldProps} | ||
> | ||
{props.interactables ? <props.interactables entry={entry} /> : null} | ||
</ReorderableItem> | ||
))} | ||
</Focusable> | ||
</div> | ||
</Fragment> | ||
); | ||
} | ||
|
||
/** | ||
* Properties for a ReorderableItem component of type <T> | ||
*/ | ||
export type ReorderableListEntryProps<T> = { | ||
fieldProps?: FieldProps; | ||
listData: ReorderableEntry<T>[]; | ||
entryData: ReorderableEntry<T>; | ||
reorderEntryFunc: CallableFunction; | ||
reorderEnabled: boolean; | ||
animate: boolean; | ||
children: ReactElement | null; | ||
}; | ||
|
||
function ReorderableItem<T>(props: ReorderableListEntryProps<T>) { | ||
const [isSelected, _setIsSelected] = useState<boolean>(false); | ||
const [isSelectedLastFrame, setIsSelectedLastFrame] = useState<boolean>(false); | ||
const listEntries = props.listData; | ||
|
||
function onReorder(e: Event): void { | ||
if (!props.reorderEnabled) return; | ||
|
||
const event = e as CustomEvent; | ||
const currentIdx = listEntries.findIndex((entryData: ReorderableEntry<T>) => entryData === props.entryData); | ||
const currentIdxValue = listEntries[currentIdx]; | ||
if (currentIdx < 0) return; | ||
|
||
let targetPosition: number = -1; | ||
if (event.detail.button == GamepadButton.DIR_DOWN) { | ||
targetPosition = currentIdxValue.position + 1; | ||
} else if (event.detail.button == GamepadButton.DIR_UP) { | ||
targetPosition = currentIdxValue.position - 1; | ||
} | ||
|
||
if (targetPosition >= listEntries.length || targetPosition < 0) return; | ||
|
||
let otherToUpdate = listEntries.find((entryData: ReorderableEntry<T>) => entryData.position === targetPosition); | ||
if (!otherToUpdate) return; | ||
|
||
let currentPosition = currentIdxValue.position; | ||
|
||
currentIdxValue.position = otherToUpdate.position; | ||
otherToUpdate.position = currentPosition; | ||
|
||
props.reorderEntryFunc( | ||
[...listEntries].sort((a: ReorderableEntry<T>, b: ReorderableEntry<T>) => a.position - b.position), | ||
); | ||
} | ||
|
||
async function setIsSelected(val: boolean) { | ||
_setIsSelected(val); | ||
// Wait 3 frames, then set. I have no idea why, but if you dont wait long enough it doesn't work. | ||
for (let i = 0; i < 3; i++) await new Promise((res) => requestAnimationFrame(res)); | ||
setIsSelectedLastFrame(val); | ||
} | ||
|
||
return ( | ||
<div | ||
style={ | ||
props.animate | ||
? { | ||
transition: | ||
isSelected || isSelectedLastFrame | ||
? '' | ||
: 'transform 0.3s cubic-bezier(0.25, 1, 0.5, 1), opacity 0.3s cubic-bezier(0.25, 1, 0.5, 1)', // easeOutQuart https://easings.net/#easeOutQuart | ||
transform: !props.reorderEnabled || isSelected ? 'scale(1)' : 'scale(0.9)', | ||
opacity: !props.reorderEnabled || isSelected ? 1 : 0.7, | ||
} | ||
: {} | ||
} | ||
> | ||
<Field | ||
label={props.entryData.label} | ||
{...props.fieldProps} | ||
focusable={!props.children} | ||
onButtonDown={onReorder} | ||
onGamepadBlur={() => setIsSelected(false)} | ||
onGamepadFocus={() => setIsSelected(true)} | ||
> | ||
<Focusable style={{ display: 'flex', width: '100%', position: 'relative' }}>{props.children}</Focusable> | ||
</Field> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './SuspensefulImage'; | ||
export * from './ColorPickerModal'; | ||
export * from './ReorderableList'; |