Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {PreviewRenderer} from "./preview-renderer";
* only be used to support editor previews in Storybook!
*/
const ExercisePreviewPage = () => {
const {content, reportHeight} = usePreviewPresenter();
const {content, isMobile, hasLintGutter, reportHeight} =
usePreviewPresenter();
const containerRef = React.useRef<HTMLDivElement>(null);
const lastHeightRef = React.useRef<number | null>(null);

Expand Down Expand Up @@ -70,7 +71,11 @@ const ExercisePreviewPage = () => {

return (
<div ref={containerRef}>
<PreviewRenderer content={content} />
<PreviewRenderer
content={content}
isMobile={isMobile}
hasLintGutter={hasLintGutter}
/>
</div>
);
};
Expand Down
120 changes: 82 additions & 38 deletions packages/perseus-editor/src/testing/preview/preview-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {MobileKeypad} from "@khanacademy/math-input";
import {
ArticleRenderer,
Dependencies,
HintRenderer,
Renderer,
UserInputManager,
usePerseusI18n,
} from "@khanacademy/perseus";
import {pushContextStack} from "@khanacademy/perseus-linter";
Expand All @@ -23,23 +25,24 @@ import type {PreviewContent} from "../../preview/message-types";

type Props = {
content: PreviewContent;
isMobile: boolean;
hasLintGutter: boolean;
};

function PreviewWithKeypad({
isMobile,
hasLintGutter,
children,
}: {
isMobile: boolean;
hasLintGutter: boolean;
children: ({
setKeypadActive,
keypadElement,
setKeypadElement,
isMobile,
}) => React.ReactNode;
}) {
// eslint-disable-next-line no-restricted-syntax
const iframe = window.frameElement as HTMLIFrameElement | null;
const isMobile = iframe?.dataset.mobile === "true";
const hasLintGutter = iframe?.dataset.lintGutter === "true";

const className = isMobile ? "perseus-mobile" : "";
const keypadCtx = React.useContext(KeypadContext);

Expand All @@ -59,11 +62,18 @@ function PreviewWithKeypad({
>
{children({...keypadCtx, isMobile})}

<MobileKeypad
onAnalyticsEvent={() => Promise.resolve()}
onDismiss={() => keypadCtx.setKeypadActive(false)}
onElementMounted={keypadCtx.setKeypadElement}
/>
{/* Only mobile previews use the custom keypad (the
parent sends customKeypad: true for phone/tablet).
Desktop widgets pop their own keypad, and an inactive
MobileKeypad still renders a fixed, bordered container
across the bottom of the preview. */}
{isMobile && (
<MobileKeypad
onAnalyticsEvent={() => Promise.resolve()}
onDismiss={() => keypadCtx.setKeypadActive(false)}
onElementMounted={keypadCtx.setKeypadElement}
/>
)}
</View>
</StatefulKeypadContextProvider>
</Dependencies.DependenciesContext.Provider>
Expand All @@ -73,49 +83,77 @@ function PreviewWithKeypad({
/**
* Renders the appropriate content based on preview data type
*/
export function PreviewRenderer({content}: Props) {
export function PreviewRenderer({content, isMobile, hasLintGutter}: Props) {
const i18n = usePerseusI18n();

if (content.type === "question") {
const {question, apiOptions, linterContext, reviewMode, problemNum} =
content.data;
const {
question,
apiOptions,
linterContext,
reviewMode,
legacyPerseusLint,
problemNum,
} = content.data;

return (
<PreviewWithKeypad>
<PreviewWithKeypad
isMobile={isMobile}
hasLintGutter={hasLintGutter}
>
{({keypadElement, isMobile}) => (
<Renderer
strings={i18n.strings}
content={question.content}
<UserInputManager
widgets={question.widgets}
images={question.images}
apiOptions={{...apiOptions, isMobile}}
keypadElement={keypadElement}
reviewMode={reviewMode}
problemNum={problemNum}
linterContext={pushContextStack(
linterContext,
"question",
problemNum={problemNum ?? 0}
>
{({
userInput,
handleUserInput,
initializeUserInput,
}) => (
<Renderer
strings={i18n.strings}
content={question.content}
widgets={question.widgets}
images={question.images}
apiOptions={{...apiOptions, isMobile}}
userInput={userInput}
handleUserInput={handleUserInput}
initializeUserInput={initializeUserInput}
keypadElement={keypadElement}
reviewMode={reviewMode}
legacyPerseusLint={legacyPerseusLint}
problemNum={problemNum}
linterContext={pushContextStack(
linterContext,
"question",
)}
/>
)}
/>
</UserInputManager>
)}
</PreviewWithKeypad>
);
}

if (content.type === "hint") {
const {hint, apiOptions, linterContext} = content.data;
const {hint, pos, apiOptions, linterContext} = content.data;

// HintRenderer manages user input itself, forces the custom keypad
// off, and pushes "hint" onto the lint stack — so it gets an empty
// initial stack rather than a pre-pushed one.
return (
<PreviewWithKeypad>
{({keypadElement, isMobile}) => (
<Renderer
strings={i18n.strings}
content={hint.content}
widgets={hint.widgets}
images={hint.images}
<PreviewWithKeypad
isMobile={isMobile}
hasLintGutter={hasLintGutter}
>
{({isMobile}) => (
<HintRenderer
hint={hint}
pos={pos}
apiOptions={{...apiOptions, isMobile}}
keypadElement={keypadElement}
linterContext={pushContextStack(linterContext, "hint")}
linterContext={{...linterContext, stack: []}}
dependencies={storybookDependenciesV2}
/>
)}
</PreviewWithKeypad>
Expand All @@ -127,7 +165,10 @@ export function PreviewRenderer({content}: Props) {
content.data;

return (
<PreviewWithKeypad>
<PreviewWithKeypad
isMobile={isMobile}
hasLintGutter={hasLintGutter}
>
{({keypadElement, isMobile}) => (
<ArticleRenderer
json={article}
Expand All @@ -149,7 +190,10 @@ export function PreviewRenderer({content}: Props) {
const {article, apiOptions} = content.data;

return (
<PreviewWithKeypad>
<PreviewWithKeypad
isMobile={isMobile}
hasLintGutter={hasLintGutter}
>
{({keypadElement, isMobile}) => (
<ArticleRenderer
json={[...article]}
Expand Down
Loading