Skip to content

Commit 99743a0

Browse files
committed
input editor at bottom
1 parent 4ddd3f0 commit 99743a0

File tree

4 files changed

+104
-138
lines changed

4 files changed

+104
-138
lines changed

packages/browser-repl/src/components/shell-output.spec.tsx renamed to packages/browser-repl/src/components/shell-content.spec.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,22 @@ import { expect } from '../../testing/chai';
33
import { screen, render, waitFor, cleanup } from '@testing-library/react';
44

55
import type { ShellOutputEntry } from './shell-output-line';
6-
import { ShellOutput } from './shell-output';
6+
import { ShellContent } from './shell-content';
77

8-
function WrappedShellOutput(props: { output: ShellOutputEntry[] }) {
8+
function WrappedShellOutput(props: Partial<React.ComponentProps<typeof ShellContent>>) {
99
return (
1010
<div style={{ height: '200px' }}>
11-
<ShellOutput
12-
output={props.output}
11+
<ShellContent
12+
output={[]}
13+
InputPrompt={<div />}
1314
__TEST_LIST_HEIGHT={200}
14-
setInnerContainerRef={() => {
15-
/** */
16-
}}
15+
{...props}
1716
/>
1817
</div>
1918
);
2019
}
2120

22-
describe('<ShellOutput />', function () {
21+
describe('<ShellContent />', function () {
2322
beforeEach(cleanup);
2423

2524
it('renders no output lines if none are passed', function () {
@@ -59,4 +58,9 @@ describe('<ShellOutput />', function () {
5958
expect(screen.getByText(/new line/i)).to.exist;
6059
});
6160
});
61+
62+
it('renders input prompt', function () {
63+
render(<WrappedShellOutput InputPrompt={<p>Enter here</p>} />);
64+
expect(screen.getByText(/enter here/i)).to.exist;
65+
});
6266
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import { ShellOutputLine, type ShellOutputEntry } from './shell-output-line';
3+
import {
4+
rafraf,
5+
VirtualList,
6+
type VirtualListRef,
7+
} from '@mongodb-js/compass-components';
8+
9+
export const ShellContent = ({
10+
output,
11+
InputPrompt,
12+
__TEST_LIST_HEIGHT,
13+
}: {
14+
output: ShellOutputEntry[];
15+
InputPrompt: JSX.Element;
16+
__TEST_LIST_HEIGHT?: number;
17+
}) => {
18+
const [inputEditorHeight, setInputEditorHeight] = useState(24);
19+
const shellInputContainerRef = useRef<HTMLDivElement>(null);
20+
21+
const listRef: VirtualListRef = useRef();
22+
23+
useEffect(() => {
24+
const lastIndex = output.length - 1;
25+
listRef.current?.resetAfterIndex(lastIndex);
26+
const abortFn = rafraf(() => {
27+
listRef.current?.scrollToItem(lastIndex, 'end');
28+
});
29+
return abortFn;
30+
}, [output.length]);
31+
32+
useEffect(() => {
33+
if (!shellInputContainerRef.current) {
34+
return;
35+
}
36+
const observer = new ResizeObserver(([input]) => {
37+
setInputEditorHeight(input.contentRect.height);
38+
});
39+
observer.observe(shellInputContainerRef.current);
40+
return () => {
41+
observer.disconnect();
42+
};
43+
}, []);
44+
45+
return (
46+
<>
47+
<div style={{ height: `calc(100% - ${inputEditorHeight}px)` }}>
48+
<VirtualList
49+
dataTestId="shell-output-virtual-list"
50+
items={output}
51+
overScanCount={10}
52+
listRef={listRef}
53+
renderItem={(item, ref) => (
54+
<div ref={ref} data-testid="shell-output-line">
55+
<ShellOutputLine entry={item} />
56+
</div>
57+
)}
58+
estimateItemInitialHeight={() => 0}
59+
__TEST_LIST_HEIGHT={__TEST_LIST_HEIGHT}
60+
/>
61+
</div>
62+
<div ref={shellInputContainerRef}>{InputPrompt}</div>
63+
</>
64+
);
65+
};

packages/browser-repl/src/components/shell-output.tsx

Lines changed: 0 additions & 48 deletions
This file was deleted.

packages/browser-repl/src/components/shell.tsx

Lines changed: 27 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import { changeHistory } from '@mongosh/history';
2424
import type { WorkerRuntime } from '@mongosh/node-runtime-worker-thread';
2525
import { PasswordPrompt } from './password-prompt';
2626
import { ShellInput } from './shell-input';
27-
import type { ShellOutputEntry } from './shell-output';
28-
import { ShellOutput } from './shell-output';
27+
import { ShellContent } from './shell-content';
28+
import type { ShellOutputEntry } from './shell-output-line';
2929

3030
const shellContainer = css({
3131
fontSize: '13px',
@@ -206,7 +206,6 @@ const _Shell: ForwardRefRenderFunction<EditorRef | null, ShellProps> = (
206206
const darkMode = useDarkMode();
207207

208208
const editorRef = useRef<EditorRef | null>(null);
209-
const shellInputContainerRef = useRef<HTMLDivElement>(null);
210209
const initialEvaluateRef = useRef(initialEvaluate);
211210
const outputRef = useRef(output);
212211
const historyRef = useRef(history);
@@ -464,42 +463,6 @@ const _Shell: ForwardRefRenderFunction<EditorRef | null, ShellProps> = (
464463
}
465464
}, [onInput, updateShellPrompt]);
466465

467-
const listInnerContainerRef = useRef<HTMLDivElement | null>(null);
468-
const setInnerContainerRef = useCallback((ref: HTMLDivElement) => {
469-
listInnerContainerRef.current = ref;
470-
}, []);
471-
472-
const [virtualListInnerHeight, setVirtualListInnerHeight] = useState(0);
473-
const [inputEditorHeight, setInputEditorHeight] = useState(0);
474-
475-
useEffect(() => {
476-
if (!listInnerContainerRef.current) {
477-
return;
478-
}
479-
const observer = new ResizeObserver(([list]) => {
480-
requestAnimationFrame(() => {
481-
setVirtualListInnerHeight(list.contentRect.height);
482-
});
483-
});
484-
observer.observe(listInnerContainerRef.current);
485-
return () => {
486-
observer.disconnect();
487-
};
488-
}, [listInnerContainerRef.current]);
489-
490-
useEffect(() => {
491-
if (!shellInputContainerRef.current) {
492-
return;
493-
}
494-
const observer = new ResizeObserver(([input]) => {
495-
setInputEditorHeight(input.contentRect.height);
496-
});
497-
observer.observe(shellInputContainerRef.current);
498-
return () => {
499-
observer.disconnect();
500-
};
501-
}, []);
502-
503466
/* eslint-disable jsx-a11y/no-static-element-interactions */
504467
/* eslint-disable jsx-a11y/click-events-have-key-events */
505468
return (
@@ -512,49 +475,31 @@ const _Shell: ForwardRefRenderFunction<EditorRef | null, ShellProps> = (
512475
)}
513476
onClick={onShellClicked}
514477
>
515-
<div
516-
style={{
517-
// By default, we set the initial height to 1px so that the
518-
// virtual list can render the items. Once we have content, we
519-
// we will get the height of the list:
520-
// - If the height of the list is smaller than the height of the
521-
// container, we will set it to the height of the list.
522-
// - If the height of the list is bigger than the height of the
523-
// container, we will set it to the height of the container minus
524-
// the height of the input editor.
525-
height: `min(calc(100% - ${inputEditorHeight}px), ${Math.max(
526-
(output ?? []).length > 0 ? virtualListInnerHeight : 1,
527-
1
528-
)}px)`,
529-
}}
530-
>
531-
<ShellOutput
532-
output={output ?? []}
533-
setInnerContainerRef={setInnerContainerRef}
534-
/>
535-
</div>
536-
<div ref={shellInputContainerRef}>
537-
{passwordPrompt ? (
538-
<PasswordPrompt
539-
onFinish={onFinishPasswordPrompt}
540-
onCancel={onCancelPasswordPrompt}
541-
prompt={passwordPrompt}
542-
/>
543-
) : (
544-
<ShellInput
545-
initialText={initialText}
546-
onTextChange={onInputChanged}
547-
prompt={shellPrompt}
548-
autocompleter={runtime}
549-
history={history}
550-
onClearCommand={listener.onClearCommand}
551-
onInput={onInput}
552-
operationInProgress={isOperationInProgress}
553-
editorRef={setEditorRef}
554-
onSigInt={onSigInt}
555-
/>
556-
)}
557-
</div>
478+
<ShellContent
479+
output={output ?? []}
480+
InputPrompt={
481+
passwordPrompt ? (
482+
<PasswordPrompt
483+
onFinish={onFinishPasswordPrompt}
484+
onCancel={onCancelPasswordPrompt}
485+
prompt={passwordPrompt}
486+
/>
487+
) : (
488+
<ShellInput
489+
initialText={initialText}
490+
onTextChange={onInputChanged}
491+
prompt={shellPrompt}
492+
autocompleter={runtime}
493+
history={history}
494+
onClearCommand={listener.onClearCommand}
495+
onInput={onInput}
496+
operationInProgress={isOperationInProgress}
497+
editorRef={setEditorRef}
498+
onSigInt={onSigInt}
499+
/>
500+
)
501+
}
502+
/>
558503
</div>
559504
);
560505
/* eslint-enable jsx-a11y/no-static-element-interactions */

0 commit comments

Comments
 (0)