Skip to content

Commit

Permalink
Merged with main
Browse files Browse the repository at this point in the history
  • Loading branch information
OAGr committed Jan 25, 2024
2 parents e10580f + e6dde34 commit d8f4fd6
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 112 deletions.
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +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 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
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,5 @@ export const ValueViewer: React.FC<Props> = ({ value, ...rest }) => {
return <MessageAlert heading="Can't display pathless value" />;
}

// The key ID is needed to make sure that when open a nested value as Focused, it will get focused.
return (
<ValueWithContextViewer
value={value}
key={value.context.path.uid()}
{...rest}
/>
);
return <ValueWithContextViewer value={value} {...rest} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import "../../widgets/index.js";

import { clsx } from "clsx";
import { FC, PropsWithChildren, useEffect, useMemo, useRef } from "react";
import { FC, PropsWithChildren, useCallback, useMemo, useRef } from "react";

import { SqValue } from "@quri/squiggle-lang";
import { CommentIcon, TextTooltip } from "@quri/ui";
Expand All @@ -12,8 +12,8 @@ import { MarkdownViewer } from "../../lib/MarkdownViewer.js";
import { SqValueWithContext } from "../../lib/utility.js";
import { ErrorBoundary } from "../ErrorBoundary.js";
import { CollapsedIcon, ExpandedIcon } from "./icons.js";
import { useFocusedSqValueKeyEvent } from "./keyboardNav/focusedSqValue.js";
import { useUnfocusedSqValueKeyEvent } from "./keyboardNav/unfocusedSqValue.js";
import { useZoomedInSqValueKeyEvent } from "./keyboardNav/zoomedInSqValue.js";
import { useZoomedOutSqValueKeyEvent } from "./keyboardNav/zoomedOutSqValue.js";
import { SquiggleValueChart } from "./SquiggleValueChart.js";
import { SquiggleValueMenu } from "./SquiggleValueMenu.js";
import { SquiggleValuePreview } from "./SquiggleValuePreview.js";
Expand All @@ -23,13 +23,13 @@ import {
pathToShortName,
} from "./utils.js";
import {
useFocus,
useMergedSettings,
useRegisterAsItemViewer,
useScrollToEditorPath,
useToggleCollapsed,
useViewerContext,
useViewerType,
useZoomIn,
} from "./ViewerProvider.js";

const CommentIconForValue: FC<{ value: SqValueWithContext }> = ({ value }) => {
Expand Down Expand Up @@ -129,35 +129,38 @@ export const ValueWithContextViewer: FC<Props> = ({
const { path } = value.context;

const containerRef = useRef<HTMLDivElement>(null);
const headerRef = useRef<HTMLDivElement>(null);
const headerRef = useRef<HTMLElement | null>(null);

const toggleCollapsed_ = useToggleCollapsed();

// Identity must be stable for the sake of `setHeaderRef` callback
const focusOnHeader = useCallback(() => {
headerRef.current?.focus();
}, []);

const handle: ValueWithContextViewerHandle = {
scrollIntoView: () => {
containerRef?.current?.scrollIntoView({
behavior: "smooth",
});
},
forceUpdate: useForceUpdate(),
focusOnHeader: () => {
headerRef.current?.focus();
},
focusOnHeader,
toggleCollapsed: () => toggleCollapsed_(path),
};

useRegisterAsItemViewer(path, { type: "listItem", value: handle });

const _focus = useFocus();
const focus = () => enableFocus && _focus(path);
const focusedKeyEvent = useFocusedSqValueKeyEvent(path);
const unfocusedKeyEvent = useUnfocusedSqValueKeyEvent(path);
const zoomIn = useZoomIn();
const focus = () => enableFocus && zoomIn(path);
const focusedKeyEvent = useZoomedInSqValueKeyEvent(path);
const unfocusedKeyEvent = useZoomedOutSqValueKeyEvent(path);

const viewerType = useViewerType();
const scrollEditorToPath = useScrollToEditorPath(path);

const { itemStore, focused: _focused } = useViewerContext();
const isFocused = _focused?.isEqual(path);
const { itemStore, zoomedInPath } = useViewerContext();
const isZoomedIn = zoomedInPath?.isEqual(path);
const itemState = itemStore.getStateOrInitialize(value);

const isRoot = path.isRoot();
Expand All @@ -175,12 +178,6 @@ export const ValueWithContextViewer: FC<Props> = ({
// In that case, the output would look broken (empty).
const isOpen = !collapsible || !itemState.collapsed;

useEffect(() => {
if (isFocused && !isRoot) {
handle.focusOnHeader();
}
}, []);

const triangleToggle = () => {
const Icon = itemState.collapsed ? CollapsedIcon : ExpandedIcon;
const _hasExtraContentToShow = hasExtraContentToShow(value);
Expand Down Expand Up @@ -265,31 +262,37 @@ export const ValueWithContextViewer: FC<Props> = ({
}
};

//Focus on the header on mount if focused
useEffect(() => {
if (isFocused && !isRoot && headerRef && headerVisibility !== "hide") {
handle.focusOnHeader();
}
}, []);
// Store the header reference for the future `focusOnHeader()` handle, and auto-focus zoomed in values on mount.
const setHeaderRef = useCallback(
(el: HTMLElement | null) => {
headerRef.current = el;

// If `isZoomedIn` toggles from `false` to `true`, this callback identity will change and it will update the focus.
if (isZoomedIn) {
focusOnHeader();
}
},
[isZoomedIn, focusOnHeader]
);

return (
<ErrorBoundary>
<div ref={containerRef}>
{headerVisibility !== "hide" && (
<header
ref={headerRef}
ref={setHeaderRef}
tabIndex={viewerType === "tooltip" ? undefined : 0}
className={clsx(
"flex justify-between group pr-0.5 hover:bg-stone-100 rounded-sm focus-visible:outline-none",
isFocused
isZoomedIn
? "focus:bg-indigo-50 mb-2 px-0.5 py-1"
: "focus:bg-indigo-100"
)}
onFocus={(_) => {
scrollEditorToPath();
}}
onKeyDown={(event) => {
isFocused ? focusedKeyEvent(event) : unfocusedKeyEvent(event);
isZoomedIn ? focusedKeyEvent(event) : unfocusedKeyEvent(event);
}}
>
<div className="inline-flex items-center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,15 +194,19 @@ export class ItemStore {
handle.value.scrollIntoView();
}
}

focusOnPath(path: SqValuePath) {
this.handles[path.uid()]?.focusOnHeader();
}
}

type ViewerContextShape = {
// Note that we don't store `localItemState` itself in the context (that would cause rerenders of the entire tree on each settings update).
// Instead, we keep `localItemState` in local state and notify the global context via `setLocalItemState` to pass them down the component tree again if it got rebuilt from scratch.
// See ./SquiggleViewer.tsx and ./ValueWithContextViewer.tsx for other implementation details on this.
globalSettings: PlaygroundSettings;
focused: SqValuePath | undefined;
setFocused: (value: SqValuePath | undefined) => void;
zoomedInPath: SqValuePath | undefined;
setZoomedInPath: (value: SqValuePath | undefined) => void;
editor?: CodeEditorHandle;
itemStore: ItemStore;
viewerType: ViewerType;
Expand All @@ -215,8 +219,8 @@ type ViewerContextShape = {

export const ViewerContext = createContext<ViewerContextShape>({
globalSettings: defaultPlaygroundSettings,
focused: undefined,
setFocused: () => undefined,
zoomedInPath: undefined,
setZoomedInPath: () => undefined,
editor: undefined,
itemStore: new ItemStore(),
viewerType: "normal",
Expand Down Expand Up @@ -310,23 +314,24 @@ export function useHasLocalSettings(path: SqValuePath) {
);
}

export function useFocus() {
const { focused, setFocused } = useViewerContext();
export function useZoomIn() {
const { zoomedInPath: zoomedInPath, setZoomedInPath: setZoomedInPath } =
useViewerContext();
return (path: SqValuePath) => {
if (focused?.isEqual(path)) {
if (zoomedInPath?.isEqual(path)) {
return; // nothing to do
}
if (path.isRoot()) {
setFocused(undefined); // focusing on root nodes is not allowed
setZoomedInPath(undefined); // full screening on root nodes is not allowed
} else {
setFocused(path);
setZoomedInPath(path);
}
};
}

export function useUnfocus() {
const { setFocused } = useViewerContext();
return () => setFocused(undefined);
export function useZoomOut() {
const { setZoomedInPath: setZoomedInPath } = useViewerContext();
return () => setZoomedInPath(undefined);
}

export function useScrollToEditorPath(path: SqValuePath) {
Expand All @@ -343,9 +348,9 @@ export function useScrollToEditorPath(path: SqValuePath) {
};
}

export function useIsFocused(path: SqValuePath) {
const { focused } = useViewerContext();
return focused?.isEqual(path);
export function useIsZoomedIn(path: SqValuePath) {
const { zoomedInPath: zoomedInPath } = useViewerContext();
return zoomedInPath?.isEqual(path);
}

export function useMergedSettings(path: SqValuePath) {
Expand Down Expand Up @@ -394,7 +399,9 @@ export const InnerViewerProvider = forwardRef<SquiggleViewerHandle, Props>(
unstablePlaygroundSettings
);

const [focused, setFocused] = useState<SqValuePath | undefined>();
const [zoomedInPath, setZoomedInPathPath] = useState<
SqValuePath | undefined
>();

const globalSettings = useMemo(() => {
return merge({}, defaultPlaygroundSettings, playgroundSettings);
Expand All @@ -417,8 +424,8 @@ export const InnerViewerProvider = forwardRef<SquiggleViewerHandle, Props>(
rootValue: _rootValue,
globalSettings,
editor,
focused,
setFocused,
zoomedInPath,
setZoomedInPath: setZoomedInPathPath,
itemStore,
viewerType,
handle,
Expand Down
Loading

0 comments on commit d8f4fd6

Please sign in to comment.