Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Select items and keyboard nav #2983

Merged
merged 43 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7b1af02
First pass
OAGr Jan 18, 2024
43aa195
Minor enhancements
OAGr Jan 18, 2024
cfe8be0
Simple up-down working for RightView
OAGr Jan 18, 2024
c6cdb43
ScrollToPath for up-down
OAGr Jan 19, 2024
dd38c46
Right scroll determines left scroll
OAGr Jan 19, 2024
be90365
Tree refactor
OAGr Jan 19, 2024
b60259a
First round of refactors
OAGr Jan 19, 2024
f5bc700
Merged with sqPath-refactor
OAGr Jan 20, 2024
1472c1c
More cleanup
OAGr Jan 20, 2024
7676381
Merged with sqPath-refactor
OAGr Jan 20, 2024
061a924
Added UID for Paths
OAGr Jan 20, 2024
ac70297
Moving arrowActions out
OAGr Jan 20, 2024
d0eba5e
Quick fix
OAGr Jan 20, 2024
4557692
Keyboard nav works using focus state
OAGr Jan 20, 2024
7ba78b8
Focus-cleanup
OAGr Jan 20, 2024
534d9d6
Merge branch 'sqPath-refactor' into select-items
OAGr Jan 21, 2024
df6847c
Merge branch 'sqPath-refactor' into select-items
OAGr Jan 21, 2024
d775530
Minor fix
OAGr Jan 22, 2024
5675798
Minor improvements for TableChartWidget
OAGr Jan 22, 2024
c635466
Refactored pathTree into SqViewNode.tsx
OAGr Jan 23, 2024
cf604f6
Cleaning up new SqViewNode file
OAGr Jan 23, 2024
00e668d
Refactored keyboard nav code into new keyboardNav directory
OAGr Jan 23, 2024
295c045
Removed find-in-editor in dropdown
OAGr Jan 23, 2024
62d47d5
Minor renaming
OAGr Jan 23, 2024
8f91960
Minor fixes
OAGr Jan 23, 2024
e5f5bfb
More small changes
OAGr Jan 23, 2024
e8751bf
Tiny fix
OAGr Jan 23, 2024
2b474a1
Merge branch 'main' into select-items
OAGr Jan 24, 2024
5727765
Minor focused fix
OAGr Jan 24, 2024
897c476
Made/use/store ValueWithContextViewerHandle refactor
OAGr Jan 25, 2024
81a32ba
Update packages/components/src/components/SquiggleViewer/SqViewNode.tsx
OAGr Jan 25, 2024
232d5d0
Addressed most code parts of CR
OAGr Jan 25, 2024
898d930
Merge branch 'select-items' of github.com:quantified-uncertainty/squi…
OAGr Jan 25, 2024
61df9e5
Responded to other comments
OAGr Jan 25, 2024
d9f0085
Fixed styles
OAGr Jan 25, 2024
22ee7f8
Fixed weird focus for website, without important
OAGr Jan 25, 2024
93b17ee
Formatted CSS
OAGr Jan 25, 2024
aae4053
Focus->Zoom In
OAGr Jan 25, 2024
25e76d4
Adds changeset
OAGr Jan 25, 2024
3ce6991
Merge pull request #2999 from quantified-uncertainty/focus-refactor
berekuk Jan 25, 2024
7d04a7b
refactor auto-focus zoomed in values
berekuk Jan 25, 2024
a425662
no need for custom `key`
berekuk Jan 25, 2024
da41d3e
move focusOnPath to ItemStore
berekuk Jan 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/strong-balloons-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@quri/squiggle-lang": patch
"@quri/squiggle-components": patch
---

Adds simple keyboard navigation for Viewer
6 changes: 3 additions & 3 deletions packages/components/src/components/CodeEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,20 @@ export type CodeEditorProps = {

export type CodeEditorHandle = {
format(): void;
scrollTo(position: number): void;
scrollTo(position: number, focus: boolean): void;
};

export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
function CodeEditor(props, ref) {
const { view, ref: editorRef } = useSquiggleEditorView(props);

const scrollTo = (position: number) => {
const scrollTo = (position: number, focus) => {
if (!view) return;
view.dispatch({
selection: { anchor: position },
scrollIntoView: true,
});
view.focus();
focus && view.focus();
};

useImperativeHandle(ref, () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const ValueTooltip: FC<{ value: SqValue; view: EditorView }> = ({
<InnerViewerProvider
partialPlaygroundSettings={globalSettings}
viewerType="tooltip"
rootValue={value}
>
<SquiggleValueChart value={value} settings={globalSettings} />
</InnerViewerProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const LocationLine: FC<{
const { editor } = useViewerContext();

const findInEditor = () => {
editor?.scrollTo(location.start.offset);
editor?.scrollTo(location.start.offset, true);
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,28 @@ type Props = {
isRunning: boolean;
};

export function modeToValue(
mode: ViewerMode,
output: SqOutputResult
): SqValue | undefined {
if (!output.ok) {
return;
}
const sqOutput = output.value;
switch (mode) {
case "Result":
return sqOutput.result;
case "Variables":
return sqOutput.bindings.asValue();
case "Imports":
return sqOutput.imports.asValue();
case "Exports":
return sqOutput.exports.asValue();
case "AST":
return;
}
}

export const ViewerBody: FC<Props> = ({ output, mode, isRunning }) => {
if (!output.ok) {
return <SquiggleErrorAlert error={output.value} />;
Expand All @@ -28,20 +50,8 @@ export const ViewerBody: FC<Props> = ({ output, mode, isRunning }) => {
</pre>
);
}
let usedValue: SqValue | undefined;
switch (mode) {
case "Result":
usedValue = output.value.result;
break;
case "Variables":
usedValue = sqOutput.bindings.asValue();
break;
case "Imports":
usedValue = sqOutput.imports.asValue();
break;
case "Exports":
usedValue = sqOutput.exports.asValue();
}

const usedValue = modeToValue(mode, output);

if (!usedValue) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "../SquiggleViewer/ViewerProvider.js";
import { Layout } from "./Layout.js";
import { RenderingIndicator } from "./RenderingIndicator.js";
import { ViewerBody } from "./ViewerBody.js";
import { modeToValue, ViewerBody } from "./ViewerBody.js";
import { ViewerMenu } from "./ViewerMenu.js";

type Props = {
Expand Down Expand Up @@ -51,6 +51,7 @@ export const SquiggleOutputViewer = forwardRef<SquiggleViewerHandle, Props>(
partialPlaygroundSettings={settings}
editor={editor}
ref={viewerRef}
rootValue={modeToValue(mode, output) || undefined}
>
<Layout
menu={<ViewerMenu mode={mode} setMode={setMode} output={output} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const ItemSettingsModal: FC<Props> = ({

const { itemStore } = useContext(ViewerContext);
const resetScroll = () => {
itemStore.scrollToPath(path);
itemStore.scrollViewerToPath(path);
};

return (
Expand Down
168 changes: 168 additions & 0 deletions packages/components/src/components/SquiggleViewer/SqViewNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { SqValue, SqValuePath } from "@quri/squiggle-lang";

import { getChildrenValues, TraverseCalculatorEdge } from "./utils.js";

//We might want to bring this into the SquiggleLang library. The ``traverseCalculatorEdge`` part is awkward though.
class SqValueNode {
constructor(
public root: SqValue,
public path: SqValuePath,
public traverseCalculatorEdge: TraverseCalculatorEdge
) {
this.isEqual = this.isEqual.bind(this);
}

uid() {
return this.path.uid();
}

isEqual(other: SqValueNode): boolean {
return this.uid() === other.uid();
}

sqValue(): SqValue | undefined {
return this.root.getSubvalueByPath(this.path, this.traverseCalculatorEdge);
}

parent() {
const parentPath = this.path.parent();
return parentPath
? new SqValueNode(this.root, parentPath, this.traverseCalculatorEdge)
: undefined;
}

children() {
const value = this.sqValue();
if (!value) {
return [];
}
return getChildrenValues(value)
.map((childValue) => {
const path = childValue.context?.path;
return path
? new SqValueNode(this.root, path, this.traverseCalculatorEdge)
: undefined;
})
.filter((a): a is NonNullable<typeof a> => a !== undefined);
}

lastChild(): SqValueNode | undefined {
return this.children().at(-1);
}

siblings() {
return this.parent()?.children() ?? [];
}

prevSibling() {
const index = this.getParentIndex();
const isRootOrError = index === -1;
const isFirstSibling = index === 0;
if (isRootOrError || isFirstSibling) {
return undefined;
}
return this.siblings()[index - 1];
}

nextSibling() {
const index = this.getParentIndex();
const isRootOrError = index === -1;
const isLastSibling = index === this.siblings().length - 1;
if (isRootOrError || isLastSibling) {
return undefined;
}
return this.siblings()[index + 1];
OAGr marked this conversation as resolved.
Show resolved Hide resolved
}

getParentIndex() {
return this.siblings().findIndex(this.isEqual);
}
}

type GetIsCollapsed = (path: SqValuePath) => boolean;
type Params = { getIsCollapsed: GetIsCollapsed };
OAGr marked this conversation as resolved.
Show resolved Hide resolved

//This is split from SqValueNode because it handles more specialized logic for viewing open/closed nodes in the Viewer. It works for lists of nodes - we'll need new logic for tabular data.
export class SqListViewNode {
constructor(
public node: SqValueNode,
public params: Params
) {
this.make = this.make.bind(this);
}

static make(
root: SqValue,
path: SqValuePath,
traverseCalculatorEdge: TraverseCalculatorEdge,
getIsCollapsed: GetIsCollapsed
) {
const node = new SqValueNode(root, path, traverseCalculatorEdge);
return new SqListViewNode(node, { getIsCollapsed });
}

make(node: SqValueNode) {
return new SqListViewNode(node, this.params);
}

// A helper function to make a node or undefined
makeU(node: SqValueNode | undefined) {
return node ? new SqListViewNode(node, this.params) : undefined;
}

value(): SqValue | undefined {
return this.node.sqValue();
}
isRoot() {
return this.node.path.isRoot();
}
parent() {
return this.makeU(this.node.parent());
}
children() {
return this.node.children().map(this.make);
}
lastChild() {
return this.makeU(this.node.lastChild());
}
siblings() {
return this.node.siblings().map(this.make);
}
prevSibling() {
return this.makeU(this.node.prevSibling());
}
nextSibling() {
return this.makeU(this.node.nextSibling());
}
private isCollapsed() {
return this.params.getIsCollapsed(this.node.path);
}

private hasVisibleChildren() {
return !this.isCollapsed() && this.children().length > 0;
}

private lastVisibleSubChild(): SqListViewNode | undefined {
if (this.hasVisibleChildren()) {
const lastChild = this.lastChild();
return lastChild?.lastVisibleSubChild() || lastChild;
} else {
return this;
}
}

private nextAvailableSibling(): SqListViewNode | undefined {
return this.nextSibling() || this.parent()?.nextAvailableSibling();
}

next(): SqListViewNode | undefined {
return this.hasVisibleChildren()
? this.children()[0]
: this.nextAvailableSibling();
}

prev(): SqListViewNode | undefined {
const prevSibling = this.prevSibling();
return prevSibling ? prevSibling.lastVisibleSubChild() : this.parent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { clsx } from "clsx";
import { FC } from "react";

import {
CodeBracketIcon,
Cog8ToothIcon,
CommandLineIcon,
Dropdown,
Expand All @@ -19,60 +18,37 @@ import { valueToHeadingString } from "../../widgets/utils.js";
import { CollapsedIcon, ExpandedIcon } from "./icons.js";
import { getChildrenValues } from "./utils.js";
import {
useFocus,
useHasLocalSettings,
useIsFocused,
useIsZoomedIn,
useSetCollapsed,
useUnfocus,
useViewerContext,
useZoomIn,
useZoomOut,
} from "./ViewerProvider.js";

const FindInEditorItem: FC<{ value: SqValueWithContext }> = ({ value }) => {
const { editor } = useViewerContext();
const closeDropdown = useCloseDropdown();

if (!editor || value.context.path.isRoot()) {
return null;
}

const findInEditor = () => {
const location = value.context.findLocation();
editor?.scrollTo(location.start.offset);
closeDropdown();
};

return (
<DropdownMenuActionItem
title="Show in Editor"
icon={CodeBracketIcon}
onClick={findInEditor}
/>
);
};

const FocusItem: FC<{ value: SqValueWithContext }> = ({ value }) => {
const { path } = value.context;
const isFocused = useIsFocused(path);
const focus = useFocus();
const unfocus = useUnfocus();
const isFocused = useIsZoomedIn(path);
const zoomIn = useZoomIn();
const zoomOut = useZoomOut();
if (path.isRoot()) {
return null;
}

if (isFocused) {
return (
<DropdownMenuActionItem
title="Unfocus"
title="Zoom Out"
icon={FocusIcon}
onClick={unfocus}
onClick={zoomOut}
/>
);
} else {
return (
<DropdownMenuActionItem
title="Focus"
title="Zoom In"
icon={FocusIcon}
onClick={() => focus(path)}
onClick={() => zoomIn(path)}
/>
);
}
Expand Down Expand Up @@ -153,7 +129,6 @@ export const SquiggleValueMenu: FC<{
{widgetHeading && (
<DropdownMenuHeader>{widgetHeading}</DropdownMenuHeader>
)}
<FindInEditorItem value={value} />
<FocusItem value={value} />
<SetChildrenCollapsedStateItem
value={value}
Expand Down