mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
eslint: enable react-hooks checks
Also fix all errors, which can lead to subtile bugs. Mostly fixings deps and refactoring to make typescript happy.
This commit is contained in:
committed by
David Lechner
parent
ee466ac904
commit
9d71bb4734
@@ -3,6 +3,7 @@ module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
@@ -25,6 +26,7 @@ module.exports = {
|
||||
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'sort-imports': ['error', { ignoreDeclarationSort: true }],
|
||||
'import/order': ['error', { alphabetize: { order: 'asc' } }],
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
},
|
||||
settings: {
|
||||
react: { version: 'detect' },
|
||||
|
||||
@@ -39,7 +39,7 @@ export function useActivitiesSelectedActivity(): ReturnType<
|
||||
|
||||
useEffect(() => {
|
||||
setLastSelectedActivity(selectedActivity);
|
||||
}, [selectedActivity]);
|
||||
}, [selectedActivity, setLastSelectedActivity]);
|
||||
|
||||
return [selectedActivity, setSelectedActivity];
|
||||
}
|
||||
|
||||
+17
-13
@@ -27,25 +27,29 @@ export function useAppLastDocsPageSetting() {
|
||||
// mirror session storage value to local storage
|
||||
useEffect(() => {
|
||||
setLastPageGlobalSetting(lastPageSessionSetting);
|
||||
}, [lastPageSessionSetting]);
|
||||
}, [lastPageSessionSetting, setLastPageGlobalSetting]);
|
||||
|
||||
// the way the docs control works, we only provide the initial page, then
|
||||
// it manages navigation after that, so we only want the initial of this
|
||||
// value when the app first starts
|
||||
const initialDocsPage = useMemo(() => {
|
||||
try {
|
||||
const url = new URL(lastPageSessionSetting);
|
||||
const initialDocsPage = useMemo(
|
||||
() => {
|
||||
try {
|
||||
const url = new URL(lastPageSessionSetting);
|
||||
|
||||
// in case someone is hacking the storage value directly
|
||||
if (!url.pathname.startsWith(`/${docsPathPrefix}`)) {
|
||||
throw new Error('invalid or outdated path');
|
||||
// in case someone is hacking the storage value directly
|
||||
if (!url.pathname.startsWith(`/${docsPathPrefix}`)) {
|
||||
throw new Error('invalid or outdated path');
|
||||
}
|
||||
|
||||
return lastPageSessionSetting;
|
||||
} catch {
|
||||
return defaultPage;
|
||||
}
|
||||
|
||||
return lastPageSessionSetting;
|
||||
} catch {
|
||||
return defaultPage;
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[], // no deps so that we only get the initial value
|
||||
);
|
||||
|
||||
return { initialDocsPage, setLastDocsPage: setLastPageSessionSetting };
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ type ToolbarItemFocusAria = {
|
||||
*/
|
||||
export function useToolbarItemFocus(props: { id: string }): ToolbarItemFocusAria {
|
||||
const { id } = props;
|
||||
const state = useContext(ToolbarStateContext);
|
||||
const { lastFocusedItem, setLastFocusedItem } = useContext(ToolbarStateContext);
|
||||
const focusManager = useFocusManager();
|
||||
|
||||
const onKeyDown = useCallback<KeyboardEventHandler<HTMLElement>>(
|
||||
@@ -66,11 +66,12 @@ export function useToolbarItemFocus(props: { id: string }): ToolbarItemFocusAria
|
||||
|
||||
const onFocus = useCallback<FocusEventHandler<HTMLElement>>(
|
||||
(e) => {
|
||||
state.setLastFocusedItem(e.target.id);
|
||||
setLastFocusedItem(e.target.id);
|
||||
},
|
||||
[state.setLastFocusedItem],
|
||||
[setLastFocusedItem],
|
||||
);
|
||||
const excludeFromTabOrder = id !== state.lastFocusedItem;
|
||||
|
||||
const excludeFromTabOrder = id !== lastFocusedItem;
|
||||
|
||||
return { toolbarItemFocusProps: { onKeyDown, onFocus }, excludeFromTabOrder };
|
||||
}
|
||||
|
||||
@@ -343,7 +343,8 @@ function useEditor(
|
||||
}
|
||||
|
||||
return callback(maybeEditor);
|
||||
}, [maybeEditor, ...deps]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [maybeEditor, callback, ...deps]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -81,13 +81,15 @@ const Welcome: React.VoidFunctionComponent<WelcomeProps> = ({ isVisible }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = elementRef.current;
|
||||
|
||||
// istanbul ignore if: should not happen
|
||||
if (!elementRef.current) {
|
||||
console.error('elementRef was null!');
|
||||
if (!element) {
|
||||
console.error('elementRef.current was null!');
|
||||
return;
|
||||
}
|
||||
|
||||
const two = new Two({ fitted: true }).appendTo(elementRef.current);
|
||||
const two = new Two({ fitted: true }).appendTo(element);
|
||||
|
||||
const logo = two.load(logoSvg, (g) => {
|
||||
g.center();
|
||||
@@ -113,7 +115,7 @@ const Welcome: React.VoidFunctionComponent<WelcomeProps> = ({ isVisible }) => {
|
||||
two.fit();
|
||||
});
|
||||
|
||||
observer.observe(elementRef.current);
|
||||
observer.observe(element);
|
||||
|
||||
const handleClick = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
@@ -149,7 +151,7 @@ const Welcome: React.VoidFunctionComponent<WelcomeProps> = ({ isVisible }) => {
|
||||
two.renderer.domElement.removeEventListener('pointerdown', handleClick);
|
||||
observer.disconnect();
|
||||
two.removeEventListener('update');
|
||||
elementRef.current?.removeChild(two.renderer.domElement);
|
||||
element.removeChild(two.renderer.domElement);
|
||||
two.clear();
|
||||
};
|
||||
}, [isVisible]);
|
||||
|
||||
+53
-38
@@ -36,7 +36,12 @@ import { useToolbarItemFocus } from '../components/toolbar/aria';
|
||||
import { UUID } from '../fileStorage';
|
||||
import { useFileStorageMetadata } from '../fileStorage/hooks';
|
||||
import { isMacOS } from '../utils/os';
|
||||
import { TreeItemContext, TreeItemData, renderers } from '../utils/tree-renderer';
|
||||
import {
|
||||
RenderProps,
|
||||
TreeItemContext,
|
||||
TreeItemData,
|
||||
renderers,
|
||||
} from '../utils/tree-renderer';
|
||||
import {
|
||||
explorerArchiveAllFiles,
|
||||
explorerCreateNewFile,
|
||||
@@ -257,7 +262,9 @@ function useLiveDescriptors(): LiveDescriptors {
|
||||
* REVISIT: maybe there will be a better way to do this some day:
|
||||
* https://github.com/lukasbach/react-complex-tree/issues/47
|
||||
*/
|
||||
const renderTreeContainer: typeof renderers.renderTreeContainer = (props) => {
|
||||
const TreeContainer: React.VoidFunctionComponent<RenderProps<'renderTreeContainer'>> = (
|
||||
props,
|
||||
) => {
|
||||
const dispatch = useDispatch();
|
||||
const { treeId } = useTree();
|
||||
const environment = useTreeEnvironment();
|
||||
@@ -272,28 +279,28 @@ const renderTreeContainer: typeof renderers.renderTreeContainer = (props) => {
|
||||
const fileName = environment.getItemTitle(environment.items[focusedItem]);
|
||||
dispatch(explorerRenameFile(fileName));
|
||||
}
|
||||
}, [environment]);
|
||||
}, [focusedItem, environment, dispatch]);
|
||||
|
||||
const handleDuplicateKeyDown = useCallback(() => {
|
||||
if (focusedItem !== undefined) {
|
||||
const fileName = environment.getItemTitle(environment.items[focusedItem]);
|
||||
dispatch(explorerDuplicateFile(fileName));
|
||||
}
|
||||
}, [environment]);
|
||||
}, [focusedItem, environment, dispatch]);
|
||||
|
||||
const handleDeleteKeyDown = useCallback(() => {
|
||||
if (focusedItem !== undefined) {
|
||||
const fileName = environment.getItemTitle(environment.items[focusedItem]);
|
||||
dispatch(explorerDeleteFile(fileName, focusedItem as UUID));
|
||||
}
|
||||
}, [environment]);
|
||||
}, [focusedItem, environment, dispatch]);
|
||||
|
||||
const handleExportKeyDown = useCallback(() => {
|
||||
if (focusedItem !== undefined) {
|
||||
const fileName = environment.getItemTitle(environment.items[focusedItem]);
|
||||
dispatch(explorerExportFile(fileName));
|
||||
}
|
||||
}, [environment]);
|
||||
}, [focusedItem, environment, dispatch]);
|
||||
|
||||
const hotkeys = useMemo<readonly HotkeyConfig[]>(
|
||||
() => [
|
||||
@@ -330,7 +337,13 @@ const renderTreeContainer: typeof renderers.renderTreeContainer = (props) => {
|
||||
onKeyDown: handleExportKeyDown,
|
||||
},
|
||||
],
|
||||
[hotKeyActive, handleDeleteKeyDown],
|
||||
[
|
||||
hotKeyActive,
|
||||
handleDeleteKeyDown,
|
||||
handleExportKeyDown,
|
||||
handleDuplicateKeyDown,
|
||||
handleRenameKeyDown,
|
||||
],
|
||||
);
|
||||
|
||||
const { handleKeyDown } = useHotkeys(hotkeys);
|
||||
@@ -340,43 +353,45 @@ const renderTreeContainer: typeof renderers.renderTreeContainer = (props) => {
|
||||
|
||||
const FileTree: React.VoidFunctionComponent = () => {
|
||||
const [focusedItem, setFocusedItem] = useState<TreeItemIndex>();
|
||||
const files = useFileStorageMetadata() ?? [];
|
||||
const files = useFileStorageMetadata();
|
||||
const liveDescriptors = useLiveDescriptors();
|
||||
|
||||
const rootItemIndex = 'root';
|
||||
|
||||
const treeItems = useMemo(
|
||||
() =>
|
||||
files.reduce(
|
||||
(obj, file) => {
|
||||
const index = file.uuid;
|
||||
const treeItems = useMemo(() => {
|
||||
if (files === undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
obj[index] = {
|
||||
index,
|
||||
data: {
|
||||
fileName: file.path,
|
||||
icon: 'document',
|
||||
secondaryLabel: (
|
||||
<TreeItemContext.Consumer>
|
||||
{(item) => <FileActionButtonGroup item={item} />}
|
||||
</TreeItemContext.Consumer>
|
||||
),
|
||||
},
|
||||
};
|
||||
return files.reduce<Record<TreeItemIndex, FileTreeItem>>(
|
||||
(obj, file) => {
|
||||
const index = file.uuid;
|
||||
|
||||
return obj;
|
||||
},
|
||||
{
|
||||
[rootItemIndex]: {
|
||||
index: rootItemIndex,
|
||||
data: { fileName: '/' },
|
||||
isFolder: true,
|
||||
children: [...files].map((f) => f.uuid),
|
||||
obj[index] = {
|
||||
index,
|
||||
data: {
|
||||
fileName: file.path,
|
||||
icon: 'document',
|
||||
secondaryLabel: (
|
||||
<TreeItemContext.Consumer>
|
||||
{(item) => <FileActionButtonGroup item={item} />}
|
||||
</TreeItemContext.Consumer>
|
||||
),
|
||||
},
|
||||
} as Record<TreeItemIndex, FileTreeItem>,
|
||||
),
|
||||
[files],
|
||||
);
|
||||
};
|
||||
|
||||
return obj;
|
||||
},
|
||||
{
|
||||
[rootItemIndex]: {
|
||||
index: rootItemIndex,
|
||||
data: { fileName: '/' },
|
||||
isFolder: true,
|
||||
children: [...files].map((f) => f.uuid),
|
||||
},
|
||||
},
|
||||
);
|
||||
}, [files]);
|
||||
|
||||
const getItemTitle = useCallback((item: FileTreeItem) => item.data.fileName, []);
|
||||
|
||||
@@ -406,7 +421,7 @@ const FileTree: React.VoidFunctionComponent = () => {
|
||||
return (
|
||||
<ControlledTreeEnvironment<FileTreeItemData>
|
||||
{...renderers}
|
||||
renderTreeContainer={renderTreeContainer}
|
||||
renderTreeContainer={TreeContainer}
|
||||
items={treeItems}
|
||||
getItemTitle={getItemTitle}
|
||||
viewState={viewState}
|
||||
|
||||
@@ -58,7 +58,7 @@ const NewFileWizard: React.VoidFunctionComponent = () => {
|
||||
),
|
||||
);
|
||||
},
|
||||
[dispatch, fileName, pythonFileExtension, hubType, useTemplate],
|
||||
[dispatch, fileName, hubType, useTemplate],
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
|
||||
@@ -187,12 +187,15 @@ const SelectHubPanel: React.VoidFunctionComponent<SelectHubPanelProps> = ({
|
||||
const i18n = useI18n();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
// should only be one file since multiple={false}
|
||||
acceptedFiles.forEach((f) => {
|
||||
onCustomFirmwareZip(f);
|
||||
});
|
||||
}, []);
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
// should only be one file since multiple={false}
|
||||
acceptedFiles.forEach((f) => {
|
||||
onCustomFirmwareZip(f);
|
||||
});
|
||||
},
|
||||
[onCustomFirmwareZip],
|
||||
);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
try {
|
||||
@@ -218,7 +221,7 @@ const SelectHubPanel: React.VoidFunctionComponent<SelectHubPanelProps> = ({
|
||||
);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}, [dispatch, onCustomFirmwareZip]);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
|
||||
@@ -202,7 +202,7 @@ export function useCustomFirmware(zipFile: File | undefined) {
|
||||
};
|
||||
|
||||
readFile();
|
||||
}, [zipFile, isMounted]);
|
||||
}, [zipFile, isMounted, reduxDispatch]);
|
||||
|
||||
const isCustomFirmwareRequested = useMemo(
|
||||
() => state.firmwareData !== undefined,
|
||||
|
||||
@@ -23,7 +23,7 @@ export function useSettingIsShowDocsEnabled(): {
|
||||
|
||||
useEffect(() => {
|
||||
setIsLastSettingShowDocsEnabled(isSettingShowDocsEnabled);
|
||||
}, [isSettingShowDocsEnabled]);
|
||||
}, [isSettingShowDocsEnabled, setIsLastSettingShowDocsEnabled]);
|
||||
|
||||
const toggleIsSettingShowDocsEnabled = useCallback(
|
||||
() => setIsSettingShowDocsEnabled((x) => !x),
|
||||
|
||||
@@ -52,7 +52,7 @@ function createXTerm(): { xterm: XTerm; fitAddon: FitAddon } {
|
||||
function createContextMenu(
|
||||
xterm: XTerm,
|
||||
): (props: ContextMenu2ContentProps) => JSX.Element {
|
||||
const contextMenu = (_props: ContextMenu2ContentProps): JSX.Element => {
|
||||
const ContextMenu = (_props: ContextMenu2ContentProps): JSX.Element => {
|
||||
const i18n = useI18n();
|
||||
|
||||
return (
|
||||
@@ -92,7 +92,7 @@ function createContextMenu(
|
||||
);
|
||||
};
|
||||
|
||||
return contextMenu;
|
||||
return ContextMenu;
|
||||
}
|
||||
|
||||
const Terminal: React.FC = (_props) => {
|
||||
@@ -120,7 +120,7 @@ const Terminal: React.FC = (_props) => {
|
||||
xterm.textarea?.setAttribute('tabindex', '-1');
|
||||
|
||||
return () => xterm.dispose();
|
||||
}, [xterm]);
|
||||
}, [xterm, fitAddon]);
|
||||
|
||||
// wire up isDarkMode to terminal
|
||||
useEffect(() => {
|
||||
@@ -133,7 +133,7 @@ const Terminal: React.FC = (_props) => {
|
||||
? 'rgb(81,81,81,0.5)'
|
||||
: 'rgba(181,213,255,0.5)', // this should match editor theme
|
||||
};
|
||||
}, [isDarkMode]);
|
||||
}, [isDarkMode, xterm]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent): void => {
|
||||
@@ -166,18 +166,15 @@ const Terminal: React.FC = (_props) => {
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, [terminalStream]);
|
||||
}, [terminalStream, xterm]);
|
||||
|
||||
// wire terminal input to actions
|
||||
useEffect(() => {
|
||||
const onDataHandle = xterm.onData((d) => dispatch(receiveData(d)));
|
||||
return () => onDataHandle.dispose();
|
||||
}, [dispatch]);
|
||||
}, [dispatch, xterm]);
|
||||
|
||||
const contextMenu = useMemo(
|
||||
() => createContextMenu(xterm),
|
||||
[createContextMenu, xterm],
|
||||
);
|
||||
const contextMenu = useMemo(() => createContextMenu(xterm), [xterm]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = () => {
|
||||
|
||||
@@ -83,7 +83,7 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [keyboardShortcut, tooltip, enabled, label, onAction]);
|
||||
}, [keyboardShortcut, enabled, label, onAction]);
|
||||
|
||||
useHotkeys(hotkeys);
|
||||
|
||||
|
||||
+2
-9
@@ -172,7 +172,7 @@ const Tour: React.VoidFunctionComponent = () => {
|
||||
disableBeacon: true,
|
||||
},
|
||||
],
|
||||
[selectedActivity, i18n],
|
||||
[selectedActivity],
|
||||
);
|
||||
|
||||
const styles = useMemo<Styles>(
|
||||
@@ -231,14 +231,7 @@ const Tour: React.VoidFunctionComponent = () => {
|
||||
setStepIndex(nextIndex);
|
||||
}
|
||||
},
|
||||
[
|
||||
dispatch,
|
||||
selectedActivity,
|
||||
setSelectedActivity,
|
||||
stepIndex,
|
||||
setStepIndex,
|
||||
steps,
|
||||
],
|
||||
[dispatch, setSelectedActivity, stepIndex, setStepIndex, steps],
|
||||
);
|
||||
|
||||
// automatically show the tour on the first run only
|
||||
|
||||
+211
-177
@@ -39,186 +39,220 @@ export const TreeItemContext = createContext<TreeItem<TreeItemData>>({
|
||||
data: {},
|
||||
});
|
||||
|
||||
type RendererFuncs = Omit<typeof renderers, 'renderDepthOffset'>;
|
||||
|
||||
/**
|
||||
* The type of the `props` parameter of a react-complex-tree render function.
|
||||
* @typeParam T The name of the render function.
|
||||
*/
|
||||
export type RenderProps<T extends keyof RendererFuncs> = Parameters<
|
||||
RendererFuncs[T]
|
||||
>[0];
|
||||
|
||||
const TreeContainer: React.VoidFunctionComponent<RenderProps<'renderTreeContainer'>> = (
|
||||
props,
|
||||
) => {
|
||||
// work around https://github.com/lukasbach/react-complex-tree/issues/195
|
||||
const { treeLabel } = useTree();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(Classes.TREE, Classes.FOCUS_STYLE_MANAGER_IGNORE)}
|
||||
{...props.containerProps}
|
||||
aria-label={treeLabel}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ItemsContainer: React.VoidFunctionComponent<
|
||||
RenderProps<'renderTreeContainer'>
|
||||
> = (props) => (
|
||||
<ul
|
||||
className={cx(Classes.TREE_NODE_LIST, Classes.TREE_ROOT)}
|
||||
{...props.containerProps}
|
||||
// containerProps sets role="group", which is incorrect, so this
|
||||
// has to be after
|
||||
role={undefined}
|
||||
>
|
||||
{props.children}
|
||||
</ul>
|
||||
);
|
||||
|
||||
const Item: React.VoidFunctionComponent<RenderProps<'renderItem'>> = (props) => {
|
||||
const {
|
||||
value: isHover,
|
||||
setTrue: setIsHoverTrue,
|
||||
setFalse: setIsHoverFalse,
|
||||
} = useBoolean(false);
|
||||
|
||||
return (
|
||||
<TreeItemContext.Provider value={props.item}>
|
||||
<li
|
||||
className={cx(
|
||||
Classes.TREE_NODE,
|
||||
// TODO: include Classes.DISABLED if disabled
|
||||
props.context.isExpanded && Classes.TREE_NODE_EXPANDED,
|
||||
(props.context.isSelected || props.context.isDraggingOver) &&
|
||||
Classes.TREE_NODE_SELECTED,
|
||||
)}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onMouseEnter={setIsHoverTrue}
|
||||
onMouseLeave={setIsHoverFalse}
|
||||
{...props.context.itemContainerWithChildrenProps}
|
||||
{...props.context.interactiveElementProps}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
Classes.TREE_NODE_CONTENT,
|
||||
`${Classes.TREE_NODE_CONTENT}-${props.depth}`,
|
||||
)}
|
||||
{...props.context.itemContainerWithoutChildrenProps}
|
||||
>
|
||||
{props.item.isFolder ? (
|
||||
props.arrow
|
||||
) : (
|
||||
<span className={Classes.TREE_NODE_CARET_NONE} />
|
||||
)}
|
||||
<Icon
|
||||
className={Classes.TREE_NODE_ICON}
|
||||
icon={props.item.data.icon}
|
||||
aria-hidden={true}
|
||||
/>
|
||||
{props.title}
|
||||
{props.item.data.secondaryLabel && isHover && (
|
||||
<span className={Classes.TREE_NODE_SECONDARY_LABEL}>
|
||||
{props.item.data.secondaryLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{props.context.isExpanded && props.children}
|
||||
</li>
|
||||
</TreeItemContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const ItemArrow: React.VoidFunctionComponent<RenderProps<'renderItemArrow'>> = (
|
||||
props,
|
||||
) => (
|
||||
<Icon
|
||||
icon="chevron-right"
|
||||
className={cx(
|
||||
Classes.TREE_NODE_CARET,
|
||||
props.context.isExpanded
|
||||
? Classes.TREE_NODE_CARET_OPEN
|
||||
: Classes.TREE_NODE_CARET_CLOSED,
|
||||
)}
|
||||
{...(props.context.arrowProps as Omit<
|
||||
React.HTMLAttributes<HTMLElement>,
|
||||
'title' | 'children'
|
||||
>)}
|
||||
/>
|
||||
);
|
||||
|
||||
const ItemTitle: React.VoidFunctionComponent<RenderProps<'renderItemTitle'>> = ({
|
||||
title,
|
||||
context,
|
||||
info,
|
||||
}) => {
|
||||
if (!info.isSearching || !context.isSearchMatching || !info.search) {
|
||||
return <span className={Classes.TREE_NODE_LABEL}>{title}</span>;
|
||||
} else {
|
||||
const startIndex = title.toLowerCase().indexOf(info.search.toLowerCase());
|
||||
return (
|
||||
<React.Fragment>
|
||||
{startIndex > 0 && <span>{title.slice(0, startIndex)}</span>}
|
||||
<span className="rct-tree-item-search-highlight">
|
||||
{title.slice(startIndex, startIndex + info.search.length)}
|
||||
</span>
|
||||
{startIndex + info.search.length < title.length && (
|
||||
<span>
|
||||
{title.slice(startIndex + info.search.length, title.length)}
|
||||
</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const DragBetweenLine: React.VoidFunctionComponent<
|
||||
RenderProps<'renderDragBetweenLine'>
|
||||
> = ({ draggingPosition, lineProps }) => (
|
||||
<div
|
||||
{...lineProps}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '0',
|
||||
top:
|
||||
draggingPosition.targetType === 'between-items' &&
|
||||
draggingPosition.linePosition === 'top'
|
||||
? '0px'
|
||||
: draggingPosition.targetType === 'between-items' &&
|
||||
draggingPosition.linePosition === 'bottom'
|
||||
? '-4px'
|
||||
: '-2px',
|
||||
left: `${draggingPosition.depth * 23}px`,
|
||||
height: '4px',
|
||||
backgroundColor: Colors.BLUE3,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const RenameInput: React.VoidFunctionComponent<RenderProps<'renderRenameInput'>> = (
|
||||
props,
|
||||
) => (
|
||||
<form {...props.formProps} style={{ display: 'contents' }}>
|
||||
<span className={Classes.TREE_NODE_LABEL}>
|
||||
<input
|
||||
{...props.inputProps}
|
||||
ref={props.inputRef}
|
||||
className="rct-tree-item-renaming-input"
|
||||
/>
|
||||
</span>
|
||||
<span className={Classes.TREE_NODE_SECONDARY_LABEL}>
|
||||
<Button
|
||||
icon="tick"
|
||||
{...props.submitButtonProps}
|
||||
type="submit"
|
||||
minimal={true}
|
||||
small={true}
|
||||
/>
|
||||
</span>
|
||||
</form>
|
||||
);
|
||||
|
||||
const SearchInput: React.VoidFunctionComponent<RenderProps<'renderSearchInput'>> = (
|
||||
props,
|
||||
) => {
|
||||
const { ref, defaultValue, value, ...inputProps } = props.inputProps;
|
||||
|
||||
assert(typeof ref !== 'string', 'cannot be a legacy ref');
|
||||
|
||||
// unused - have to be excluded from inputProps for InputGroup typing compatibility
|
||||
defaultValue;
|
||||
value;
|
||||
|
||||
// TODO: translate placeholder and inputProps.aria-label
|
||||
|
||||
return (
|
||||
<div className={cx('rct-tree-search-input-container')}>
|
||||
<InputGroup inputRef={ref} {...inputProps} placeholder="Search..." />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderers: Omit<
|
||||
Required<TreeRenderProps<TreeItemData>>,
|
||||
'renderDraggingItem' | 'renderDraggingItemTitle' | 'renderLiveDescriptorContainer'
|
||||
> = {
|
||||
renderTreeContainer: (props) => {
|
||||
// work around https://github.com/lukasbach/react-complex-tree/issues/195
|
||||
const { treeLabel } = useTree();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(Classes.TREE, Classes.FOCUS_STYLE_MANAGER_IGNORE)}
|
||||
{...props.containerProps}
|
||||
aria-label={treeLabel}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderItemsContainer: (props) => (
|
||||
<ul
|
||||
className={cx(Classes.TREE_NODE_LIST, Classes.TREE_ROOT)}
|
||||
{...props.containerProps}
|
||||
// containerProps sets role="group", which is incorrect, so this
|
||||
// has to be after
|
||||
role={undefined}
|
||||
>
|
||||
{props.children}
|
||||
</ul>
|
||||
),
|
||||
|
||||
renderItem: (props) => {
|
||||
const {
|
||||
value: isHover,
|
||||
setTrue: setIsHoverTrue,
|
||||
setFalse: setIsHoverFalse,
|
||||
} = useBoolean(false);
|
||||
|
||||
return (
|
||||
<TreeItemContext.Provider value={props.item}>
|
||||
<li
|
||||
className={cx(
|
||||
Classes.TREE_NODE,
|
||||
// TODO: include Classes.DISABLED if disabled
|
||||
props.context.isExpanded && Classes.TREE_NODE_EXPANDED,
|
||||
(props.context.isSelected || props.context.isDraggingOver) &&
|
||||
Classes.TREE_NODE_SELECTED,
|
||||
)}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onMouseEnter={setIsHoverTrue}
|
||||
onMouseLeave={setIsHoverFalse}
|
||||
{...props.context.itemContainerWithChildrenProps}
|
||||
{...props.context.interactiveElementProps}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
Classes.TREE_NODE_CONTENT,
|
||||
`${Classes.TREE_NODE_CONTENT}-${props.depth}`,
|
||||
)}
|
||||
{...props.context.itemContainerWithoutChildrenProps}
|
||||
>
|
||||
{props.item.isFolder ? (
|
||||
props.arrow
|
||||
) : (
|
||||
<span className={Classes.TREE_NODE_CARET_NONE} />
|
||||
)}
|
||||
<Icon
|
||||
className={Classes.TREE_NODE_ICON}
|
||||
icon={props.item.data.icon}
|
||||
aria-hidden={true}
|
||||
/>
|
||||
{props.title}
|
||||
{props.item.data.secondaryLabel && isHover && (
|
||||
<span className={Classes.TREE_NODE_SECONDARY_LABEL}>
|
||||
{props.item.data.secondaryLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{props.context.isExpanded && props.children}
|
||||
</li>
|
||||
</TreeItemContext.Provider>
|
||||
);
|
||||
},
|
||||
|
||||
renderItemArrow: (props) => (
|
||||
<Icon
|
||||
icon="chevron-right"
|
||||
className={cx(
|
||||
Classes.TREE_NODE_CARET,
|
||||
props.context.isExpanded
|
||||
? Classes.TREE_NODE_CARET_OPEN
|
||||
: Classes.TREE_NODE_CARET_CLOSED,
|
||||
)}
|
||||
{...(props.context.arrowProps as Omit<
|
||||
React.HTMLAttributes<HTMLElement>,
|
||||
'title' | 'children'
|
||||
>)}
|
||||
/>
|
||||
),
|
||||
|
||||
renderItemTitle: ({ title, context, info }) => {
|
||||
if (!info.isSearching || !context.isSearchMatching || !info.search) {
|
||||
return <span className={Classes.TREE_NODE_LABEL}>{title}</span>;
|
||||
} else {
|
||||
const startIndex = title.toLowerCase().indexOf(info.search.toLowerCase());
|
||||
return (
|
||||
<React.Fragment>
|
||||
{startIndex > 0 && <span>{title.slice(0, startIndex)}</span>}
|
||||
<span className="rct-tree-item-search-highlight">
|
||||
{title.slice(startIndex, startIndex + info.search.length)}
|
||||
</span>
|
||||
{startIndex + info.search.length < title.length && (
|
||||
<span>
|
||||
{title.slice(startIndex + info.search.length, title.length)}
|
||||
</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
renderDragBetweenLine: ({ draggingPosition, lineProps }) => (
|
||||
<div
|
||||
{...lineProps}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '0',
|
||||
top:
|
||||
draggingPosition.targetType === 'between-items' &&
|
||||
draggingPosition.linePosition === 'top'
|
||||
? '0px'
|
||||
: draggingPosition.targetType === 'between-items' &&
|
||||
draggingPosition.linePosition === 'bottom'
|
||||
? '-4px'
|
||||
: '-2px',
|
||||
left: `${draggingPosition.depth * 23}px`,
|
||||
height: '4px',
|
||||
backgroundColor: Colors.BLUE3,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
||||
renderRenameInput: (props) => (
|
||||
<form {...props.formProps} style={{ display: 'contents' }}>
|
||||
<span className={Classes.TREE_NODE_LABEL}>
|
||||
<input
|
||||
{...props.inputProps}
|
||||
ref={props.inputRef}
|
||||
className="rct-tree-item-renaming-input"
|
||||
/>
|
||||
</span>
|
||||
<span className={Classes.TREE_NODE_SECONDARY_LABEL}>
|
||||
<Button
|
||||
icon="tick"
|
||||
{...props.submitButtonProps}
|
||||
type="submit"
|
||||
minimal={true}
|
||||
small={true}
|
||||
/>
|
||||
</span>
|
||||
</form>
|
||||
),
|
||||
|
||||
renderSearchInput: (props) => {
|
||||
const { ref, defaultValue, value, ...inputProps } = props.inputProps;
|
||||
|
||||
assert(typeof ref !== 'string', 'cannot be a legacy ref');
|
||||
|
||||
// unused - have to be excluded from inputProps for InputGroup typing compatibility
|
||||
defaultValue;
|
||||
value;
|
||||
|
||||
// TODO: translate placeholder and inputProps.aria-label
|
||||
|
||||
return (
|
||||
<div className={cx('rct-tree-search-input-container')}>
|
||||
<InputGroup inputRef={ref} {...inputProps} placeholder="Search..." />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderTreeContainer: TreeContainer,
|
||||
renderItemsContainer: ItemsContainer,
|
||||
renderItem: Item,
|
||||
renderItemArrow: ItemArrow,
|
||||
renderItemTitle: ItemTitle,
|
||||
renderDragBetweenLine: DragBetweenLine,
|
||||
renderRenameInput: RenameInput,
|
||||
renderSearchInput: SearchInput,
|
||||
renderDepthOffset: 1,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user