Implement all the ARIA keyboard interactions, and selection follows focus
This commit is contained in:
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "typescript",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"option": "watch",
|
||||
"problemMatcher": ["$tsc-watch"],
|
||||
"group": "build",
|
||||
"label": "tsc: watch - tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export function ColumnItem({ item }: { item: ColumnViewNode }) {
|
||||
return (
|
||||
<div
|
||||
className={`flex h-9 items-center justify-items-stretch mx-1 px-1 py-1 my-1 rounded-sm ${stateStyle}`}
|
||||
onClick={() => goToNodeId(id)}
|
||||
onClick={() => goToNodeId(id, "columnView")}
|
||||
ref={htmlElement}
|
||||
>
|
||||
<div className="w-4 flex-none flex-col justify-items-center">
|
||||
|
||||
@@ -63,7 +63,7 @@ function HomeInfoBoxSectionContent() {
|
||||
|
||||
useEffect(() => {
|
||||
const selectedPath = infoBoxData[index].highlight;
|
||||
api.goToNodeId(selectedPath);
|
||||
api.goToNodeId(selectedPath, "home");
|
||||
}, [index]);
|
||||
|
||||
const resetInterval = () => {
|
||||
|
||||
@@ -72,7 +72,7 @@ export function JsonEditor() {
|
||||
|
||||
const path = JSONHeroPath.fromPointer(pointer);
|
||||
|
||||
goToNodeId(path.toString());
|
||||
goToNodeId(path.toString(), "editor");
|
||||
},
|
||||
[goToNodeId]
|
||||
);
|
||||
|
||||
@@ -10,11 +10,12 @@ import { Body } from "./Primitives/Body";
|
||||
import { Mono } from "./Primitives/Mono";
|
||||
|
||||
export function JsonTreeView() {
|
||||
const { selectedNodeId } = useJsonColumnViewState();
|
||||
const { selectedNodeId, selectedNodeSource } = useJsonColumnViewState();
|
||||
const { goToNodeId } = useJsonColumnViewAPI();
|
||||
|
||||
const { tree, parentRef } = useJsonTreeViewContext();
|
||||
|
||||
// Scroll to the selected node when this component is first rendered.
|
||||
const scrolledToNodeRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -24,6 +25,43 @@ export function JsonTreeView() {
|
||||
}
|
||||
}, [selectedNodeId, scrolledToNodeRef]);
|
||||
|
||||
// This focuses and scrolls to the selected node when the selectedNodeId
|
||||
// is set from a source other than this tree (e.g. the search bar, path bar, related values).
|
||||
useEffect(() => {
|
||||
if (
|
||||
tree.focusedNodeId &&
|
||||
selectedNodeId &&
|
||||
tree.focusedNodeId !== selectedNodeId
|
||||
) {
|
||||
if (selectedNodeSource !== "tree") {
|
||||
if (selectedNodeId === "$") {
|
||||
tree.focusFirst();
|
||||
} else {
|
||||
tree.focusNode(selectedNodeId);
|
||||
tree.scrollToNode(selectedNodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [tree.focusedNodeId, goToNodeId, selectedNodeId, selectedNodeSource]);
|
||||
|
||||
// This is what syncs the tree view's focused node to the column view selected node
|
||||
const previousFocusedNodeId = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!previousFocusedNodeId.current) {
|
||||
previousFocusedNodeId.current = tree.focusedNodeId;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
tree.focusedNodeId &&
|
||||
previousFocusedNodeId.current !== tree.focusedNodeId
|
||||
) {
|
||||
previousFocusedNodeId.current = tree.focusedNodeId;
|
||||
goToNodeId(tree.focusedNodeId, "tree");
|
||||
}
|
||||
}, [previousFocusedNodeId, tree.focusedNodeId, tree.focusNode, goToNodeId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="text-white w-full"
|
||||
@@ -44,7 +82,6 @@ export function JsonTreeView() {
|
||||
virtualNode={virtualNode}
|
||||
key={virtualNode.node.id}
|
||||
onToggle={() => tree.toggleNode(virtualNode.node.id)}
|
||||
onClick={() => goToNodeId(virtualNode.node.id)}
|
||||
selectedNodeId={selectedNodeId}
|
||||
/>
|
||||
))}
|
||||
@@ -55,20 +92,29 @@ export function JsonTreeView() {
|
||||
|
||||
function TreeViewNode({
|
||||
virtualNode,
|
||||
onClick,
|
||||
onToggle,
|
||||
selectedNodeId,
|
||||
}: {
|
||||
virtualNode: VirtualNode<JsonTreeViewNode>;
|
||||
selectedNodeId?: string;
|
||||
onClick?: (node: JsonTreeViewNode) => void;
|
||||
onToggle?: (node: JsonTreeViewNode) => void;
|
||||
}) {
|
||||
const { tree } = useJsonTreeViewContext();
|
||||
|
||||
const { node, virtualItem, depth } = virtualNode;
|
||||
|
||||
const indentClassName = computeTreeNodePaddingClass(depth);
|
||||
|
||||
const isSelected = selectedNodeId === node.id;
|
||||
const isFocused = tree.focusedNodeId === node.id;
|
||||
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (elementRef.current && isFocused) {
|
||||
elementRef.current.focus();
|
||||
}
|
||||
}, [elementRef, isFocused]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -82,12 +128,7 @@ function TreeViewNode({
|
||||
}}
|
||||
key={virtualNode.node.id}
|
||||
{...virtualNode.getItemProps()}
|
||||
onClick={(e) => {
|
||||
if (onClick) {
|
||||
e.stopPropagation();
|
||||
onClick(virtualNode.node);
|
||||
}
|
||||
}}
|
||||
ref={elementRef}
|
||||
>
|
||||
<div
|
||||
className={`h-full flex select-none ${
|
||||
@@ -103,7 +144,7 @@ function TreeViewNode({
|
||||
<span
|
||||
onClick={(e) => {
|
||||
if (onToggle) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onToggle(node);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function PathBarLink({
|
||||
: "hover:bg-slate-300 px-2 py-[3px] rounded-sm transition dark:hover:bg-white dark:hover:bg-opacity-[5%]"
|
||||
}`}
|
||||
style={{ flexShrink: 1 }}
|
||||
onClick={() => goToNodeId(node.id)}
|
||||
onClick={() => goToNodeId(node.id, "pathBar")}
|
||||
>
|
||||
<div className="w-4 flex-shrink-[0.5] flex-grow-0 flex-col justify-items-center whitespace-nowrap overflow-x-hidden transition dark:text-slate-400">
|
||||
{node.icon && <node.icon className="h-3 w-3" />}
|
||||
|
||||
@@ -81,7 +81,8 @@ export function PathPreview({
|
||||
: "disabled"
|
||||
}`}
|
||||
onClick={() =>
|
||||
isEnabled && goToNodeId(components[components.length - 1].id)
|
||||
isEnabled &&
|
||||
goToNodeId(components[components.length - 1].id, "relatedValues")
|
||||
}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -134,7 +134,7 @@ export function JsonColumnViewProvider({ children }: { children: ReactNode }) {
|
||||
const restoredState = JSON.parse(storage) as ColumnViewInstanceState;
|
||||
if (!restoredState.selectedNodeId) return;
|
||||
|
||||
api.goToNodeId(restoredState.selectedNodeId);
|
||||
api.goToNodeId(restoredState.selectedNodeId, "localStorage");
|
||||
}, [doc.id, isStateRestored.current, state, api]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
+261
-8
@@ -1,4 +1,4 @@
|
||||
import { useReducer, Reducer, useCallback, Dispatch } from "react";
|
||||
import { useReducer, Reducer, useCallback, Dispatch, useEffect } from "react";
|
||||
import { useVirtual, VirtualItem } from "react-virtual";
|
||||
|
||||
type UseVirtualOptions<R> = Parameters<typeof useVirtual>[0];
|
||||
@@ -22,8 +22,11 @@ export type VirtualNode<T> = {
|
||||
|
||||
export type UseVirtualTreeInstance<T> = {
|
||||
nodes: VirtualNode<T>[];
|
||||
focusedNodeId: string | null;
|
||||
totalSize: number;
|
||||
toggleNode: (id: string) => void;
|
||||
focusNode: (id: string) => void;
|
||||
focusFirst: () => void;
|
||||
scrollToNode: (id: string) => void;
|
||||
getTreeProps: () => React.HTMLAttributes<HTMLElement>;
|
||||
};
|
||||
@@ -49,7 +52,42 @@ type ToggleNodeAction = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
type TreeAction = ToggleNodeAction;
|
||||
type FocusNodeAction = {
|
||||
type: "FOCUS_NODE";
|
||||
id: string;
|
||||
};
|
||||
|
||||
type MoveNodeAction = {
|
||||
type: "MOVE_DOWN" | "MOVE_UP" | "MOVE_TO_TOP" | "MOVE_TO_BOTTOM";
|
||||
source: KeyboardEvent | MouseEvent;
|
||||
};
|
||||
|
||||
type MoveRightAction = {
|
||||
type: "MOVE_RIGHT";
|
||||
source: KeyboardEvent | MouseEvent;
|
||||
id: string;
|
||||
isCollapsed: boolean;
|
||||
};
|
||||
|
||||
type MoveLeftAction = {
|
||||
type: "MOVE_LEFT";
|
||||
source: KeyboardEvent | MouseEvent;
|
||||
id: string;
|
||||
isCollapsed: boolean;
|
||||
hasChildren: boolean;
|
||||
};
|
||||
|
||||
type FocusFirstAction = {
|
||||
type: "FOCUS_FIRST";
|
||||
};
|
||||
|
||||
type TreeAction =
|
||||
| ToggleNodeAction
|
||||
| MoveNodeAction
|
||||
| FocusNodeAction
|
||||
| FocusFirstAction
|
||||
| MoveRightAction
|
||||
| MoveLeftAction;
|
||||
|
||||
function expandNode<T extends { id: string; children?: T[] }>(
|
||||
state: TreeState<T>,
|
||||
@@ -100,6 +138,126 @@ export function useVirtualTree<T extends { id: string; children?: T[] }, R>(
|
||||
return collapseNode<T>(state, action.id);
|
||||
}
|
||||
}
|
||||
case "FOCUS_NODE": {
|
||||
return {
|
||||
...state,
|
||||
focusedNodeId: action.id,
|
||||
};
|
||||
}
|
||||
case "FOCUS_FIRST":
|
||||
case "MOVE_TO_TOP": {
|
||||
const nextItem = state.items[0];
|
||||
|
||||
if (!nextItem) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
focusedNodeId: nextItem.id,
|
||||
};
|
||||
}
|
||||
case "MOVE_TO_BOTTOM": {
|
||||
const nextItem = state.items[state.items.length - 1];
|
||||
|
||||
if (!nextItem) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
focusedNodeId: nextItem.id,
|
||||
};
|
||||
}
|
||||
case "MOVE_DOWN": {
|
||||
const focusedNodeIdIndex = state.items.findIndex(
|
||||
(item) => item.id === state.focusedNodeId
|
||||
);
|
||||
|
||||
if (focusedNodeIdIndex === -1) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (state.items.length <= focusedNodeIdIndex + 1) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const nextItem = state.items[focusedNodeIdIndex + 1];
|
||||
|
||||
return {
|
||||
...state,
|
||||
focusedNodeId: nextItem.id,
|
||||
};
|
||||
}
|
||||
case "MOVE_UP": {
|
||||
const focusedNodeIdIndex = state.items.findIndex(
|
||||
(item) => item.id === state.focusedNodeId
|
||||
);
|
||||
|
||||
if (focusedNodeIdIndex === -1) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (focusedNodeIdIndex === 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const nextItem = state.items[focusedNodeIdIndex - 1];
|
||||
|
||||
return {
|
||||
...state,
|
||||
focusedNodeId: nextItem.id,
|
||||
};
|
||||
}
|
||||
case "MOVE_RIGHT": {
|
||||
if (action.isCollapsed) {
|
||||
return expandNode<T>(state, action.id);
|
||||
}
|
||||
|
||||
const nodeIndex = state.items.findIndex(
|
||||
(item) => item.id === action.id
|
||||
);
|
||||
|
||||
if (nodeIndex === -1) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (state.items.length <= nodeIndex + 1) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const nextItem = state.items[nodeIndex + 1];
|
||||
|
||||
return {
|
||||
...state,
|
||||
focusedNodeId: nextItem.id,
|
||||
};
|
||||
}
|
||||
case "MOVE_LEFT": {
|
||||
if (action.hasChildren && !action.isCollapsed) {
|
||||
return collapseNode<T>(state, action.id);
|
||||
}
|
||||
|
||||
if (!action.hasChildren || action.isCollapsed) {
|
||||
// Try to go to the parent node
|
||||
const parentNodeIndex = state.items.findIndex(
|
||||
(item) =>
|
||||
item.node.children &&
|
||||
item.node.children.map((child) => child.id).includes(action.id)
|
||||
);
|
||||
|
||||
if (parentNodeIndex === -1) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const nextItem = state.items[parentNodeIndex];
|
||||
|
||||
return {
|
||||
...state,
|
||||
focusedNodeId: nextItem.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -150,9 +308,24 @@ export function useVirtualTree<T extends { id: string; children?: T[] }, R>(
|
||||
};
|
||||
});
|
||||
|
||||
const toggleNode = useCallback((id: string) => {
|
||||
dispatch({ type: "TOGGLE_NODE", id });
|
||||
}, []);
|
||||
const toggleNode = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: "TOGGLE_NODE", id });
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const focusNode = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: "FOCUS_NODE", id });
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const focusFirst = useCallback(
|
||||
() => dispatch({ type: "FOCUS_FIRST" }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// TODO: have this work with collapsed nodes
|
||||
const scrollToNode = useCallback(
|
||||
@@ -160,16 +333,26 @@ export function useVirtualTree<T extends { id: string; children?: T[] }, R>(
|
||||
const itemIndex = state.items.findIndex((item) => item.id === id);
|
||||
|
||||
if (itemIndex !== -1) {
|
||||
rowVirtualizer.scrollToIndex(itemIndex, { align: "center" });
|
||||
rowVirtualizer.scrollToIndex(itemIndex, { align: "auto" });
|
||||
}
|
||||
},
|
||||
[state.items, rowVirtualizer]
|
||||
[state.items, rowVirtualizer.scrollToIndex]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.focusedNodeId) {
|
||||
scrollToNode(state.focusedNodeId);
|
||||
focusNode(state.focusedNodeId);
|
||||
}
|
||||
}, [state.focusedNodeId, scrollToNode, focusNode]);
|
||||
|
||||
return {
|
||||
nodes: allVirtualNodes,
|
||||
totalSize: rowVirtualizer.totalSize,
|
||||
toggleNode,
|
||||
focusNode,
|
||||
focusFirst,
|
||||
focusedNodeId: state.focusedNodeId,
|
||||
getTreeProps: useCallback(
|
||||
() => ({
|
||||
role: "tree",
|
||||
@@ -219,6 +402,76 @@ function createItemProps<T extends { id: string; children?: T[] }>(
|
||||
"aria-posinset": pos,
|
||||
"aria-setsize": size,
|
||||
role: "treeitem",
|
||||
tabIndex: node.id === state.focusedNodeId ? 0 : -1,
|
||||
tabIndex: node.id === state.focusedNodeId ? -1 : undefined,
|
||||
onClick: (e) => {
|
||||
if (e.defaultPrevented) {
|
||||
return; // Do nothing if the event was already processed
|
||||
}
|
||||
|
||||
if (node.id !== state.focusedNodeId) {
|
||||
dispatch({ type: "FOCUS_NODE", id: node.id });
|
||||
}
|
||||
},
|
||||
onKeyDown: (e) => {
|
||||
if (e.defaultPrevented) {
|
||||
return; // Do nothing if the event was already processed
|
||||
}
|
||||
|
||||
if (node.id === state.focusedNodeId) {
|
||||
switch (e.key) {
|
||||
case "Home": {
|
||||
dispatch({ type: "MOVE_TO_TOP", source: e.nativeEvent });
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
case "End": {
|
||||
dispatch({ type: "MOVE_TO_BOTTOM", source: e.nativeEvent });
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
case "Down":
|
||||
case "ArrowDown": {
|
||||
dispatch({ type: "MOVE_DOWN", source: e.nativeEvent });
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
case "Up":
|
||||
case "ArrowUp": {
|
||||
dispatch({ type: "MOVE_UP", source: e.nativeEvent });
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
case "Left":
|
||||
case "ArrowLeft": {
|
||||
dispatch({
|
||||
type: "MOVE_LEFT",
|
||||
id: node.id,
|
||||
isCollapsed,
|
||||
hasChildren:
|
||||
typeof node.children !== "undefined" &&
|
||||
node.children.length > 0,
|
||||
source: e.nativeEvent,
|
||||
});
|
||||
e.preventDefault();
|
||||
|
||||
break;
|
||||
}
|
||||
case "Right":
|
||||
case "ArrowRight": {
|
||||
if (node.children && node.children.length > 0) {
|
||||
dispatch({
|
||||
type: "MOVE_RIGHT",
|
||||
id: node.id,
|
||||
isCollapsed,
|
||||
source: e.nativeEvent,
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function JsonDocumentRoute() {
|
||||
<JsonProvider initialJson={loaderData.json}>
|
||||
<JsonSchemaProvider>
|
||||
<JsonColumnViewProvider>
|
||||
<JsonTreeViewProvider overscan={10}>
|
||||
<JsonTreeViewProvider overscan={25}>
|
||||
<div>
|
||||
<div className="h-screen flex flex-col">
|
||||
<Header />
|
||||
|
||||
@@ -39,6 +39,7 @@ export type ColumnViewInstanceState = {
|
||||
columns: Array<ColumnDefinition>;
|
||||
getColumnViewProps: () => ColumnViewProps;
|
||||
selectedNodeId?: string;
|
||||
selectedNodeSource?: string;
|
||||
selectedPath: string[];
|
||||
highlightedNodeId?: string;
|
||||
highlightedPath: string[];
|
||||
@@ -54,7 +55,7 @@ export type ColumnViewAPIOptions = {
|
||||
export type ColumnViewAPI = {
|
||||
goBack: () => void;
|
||||
goForward: () => void;
|
||||
goToNodeId: (nodeId: string) => void;
|
||||
goToNodeId: (nodeId: string, source: string) => void;
|
||||
goToParent: (options?: ColumnViewAPIOptions) => void;
|
||||
goToChildren: () => void;
|
||||
goToNextSibling: () => void;
|
||||
@@ -157,8 +158,8 @@ export function useColumnView({
|
||||
goForward: () => {
|
||||
dispatch(goForwardAction());
|
||||
},
|
||||
goToNodeId: (nodeId: string) => {
|
||||
dispatch(goToNodeIdAction(nodeId));
|
||||
goToNodeId: (nodeId: string, source: string) => {
|
||||
dispatch(goToNodeIdAction(nodeId, source));
|
||||
},
|
||||
goToParent: (options?: ColumnViewAPIOptions) => {
|
||||
dispatch(goToParentAction(options));
|
||||
@@ -176,10 +177,15 @@ export function useColumnView({
|
||||
dispatch(resetSelectionAction());
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
}, [dispatch]);
|
||||
|
||||
const { selectedNodeId, highlightedNodeId, history, historyCurrentIndex } =
|
||||
state;
|
||||
const {
|
||||
selectedNodeId,
|
||||
highlightedNodeId,
|
||||
selectedNodeSource,
|
||||
history,
|
||||
historyCurrentIndex,
|
||||
} = state;
|
||||
|
||||
const selectedPath = getPathToNode(nodeTable, selectedNodeId);
|
||||
const highlightedPath = getPathToNode(nodeTable, highlightedNodeId);
|
||||
@@ -202,6 +208,7 @@ export function useColumnView({
|
||||
return {
|
||||
state: {
|
||||
selectedNodeId,
|
||||
selectedNodeSource,
|
||||
selectedPath,
|
||||
selectedNodes,
|
||||
highlightedNodeId,
|
||||
@@ -218,6 +225,7 @@ export function useColumnView({
|
||||
export type ColumnViewState = {
|
||||
selectedNodeId?: string;
|
||||
highlightedNodeId?: string;
|
||||
selectedNodeSource?: string;
|
||||
history: Array<Omit<ColumnViewState, "history" | "historyCurrentIndex">>;
|
||||
historyCurrentIndex: number;
|
||||
nodeTable: NodeTable;
|
||||
@@ -227,6 +235,7 @@ export type ColumnViewState = {
|
||||
export type SetSelectedNodeIdAction = {
|
||||
type: "SET_SELECTED_NODE_ID";
|
||||
id: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export type MoveSelectedNodeAction = {
|
||||
@@ -269,10 +278,14 @@ function resetSelectionAction(): ResetSelectionNodeAction {
|
||||
};
|
||||
}
|
||||
|
||||
function goToNodeIdAction(nodeId: string): SetSelectedNodeIdAction {
|
||||
function goToNodeIdAction(
|
||||
nodeId: string,
|
||||
source: string
|
||||
): SetSelectedNodeIdAction {
|
||||
return {
|
||||
type: "SET_SELECTED_NODE_ID",
|
||||
id: nodeId,
|
||||
source,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -332,6 +345,7 @@ function columnViewReducer(
|
||||
...state,
|
||||
selectedNodeId: action.id,
|
||||
highlightedNodeId: action.id,
|
||||
selectedNodeSource: action.source,
|
||||
};
|
||||
case "MOVE_DOWN": {
|
||||
if (state.highlightedNodeId === state.rootNodeId) {
|
||||
|
||||
@@ -31,15 +31,19 @@ export function formatValue(type: JSONValueType): string | undefined {
|
||||
case "array": {
|
||||
if (type.value.length == 0) {
|
||||
return formatRawValue(type);
|
||||
} else if (type.value.length === 1) {
|
||||
return `1 item`;
|
||||
} else {
|
||||
return undefined;
|
||||
return `${type.value.length} items`;
|
||||
}
|
||||
}
|
||||
case "object": {
|
||||
if (Object.keys(type.value).length == 0) {
|
||||
return formatRawValue(type);
|
||||
} else if (Object.keys(type.value).length === 1) {
|
||||
return `1 field`;
|
||||
} else {
|
||||
return undefined;
|
||||
return `${Object.keys(type.value).length} fields`;
|
||||
}
|
||||
}
|
||||
case "bool": {
|
||||
|
||||
Generated
-18
@@ -42,8 +42,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^2.2.2",
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
|
||||
"@remix-run/dev": "^1.2.3",
|
||||
"@tailwindcss/forms": "^0.4.0",
|
||||
"@types/color": "^3.0.3",
|
||||
@@ -925,15 +923,6 @@
|
||||
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@esbuild-plugins/node-globals-polyfill": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.1.1.tgz",
|
||||
"integrity": "sha512-MR0oAA+mlnJWrt1RQVQ+4VYuRJW/P2YmRTv1AsplObyvuBMnPHiizUF95HHYiSsMGLhyGtWufaq2XQg6+iurBg==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"esbuild": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild-plugins/node-modules-polyfill": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.1.4.tgz",
|
||||
@@ -14193,13 +14182,6 @@
|
||||
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild-plugins/node-globals-polyfill": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.1.1.tgz",
|
||||
"integrity": "sha512-MR0oAA+mlnJWrt1RQVQ+4VYuRJW/P2YmRTv1AsplObyvuBMnPHiizUF95HHYiSsMGLhyGtWufaq2XQg6+iurBg==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@esbuild-plugins/node-modules-polyfill": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.1.4.tgz",
|
||||
|
||||
@@ -59,8 +59,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^2.2.2",
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
|
||||
"@remix-run/dev": "^1.2.3",
|
||||
"@tailwindcss/forms": "^0.4.0",
|
||||
"@types/color": "^3.0.3",
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
"skipDefaultLibCheck": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
// Remix takes care of building everything in `remix build`.
|
||||
"noEmit": true
|
||||
|
||||
Reference in New Issue
Block a user