Skip to content

Commit

Permalink
feat: responsive prompt on mobile
Browse files Browse the repository at this point in the history
  • Loading branch information
nico-i committed Jan 1, 2024
1 parent e84c3a9 commit d67288c
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 39 deletions.
4 changes: 2 additions & 2 deletions src/components/AsciiLine/AsciiLine.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCharWidth } from '@/hooks';
import { useCharDimensions } from '@/hooks';
import clsx from 'clsx';

export interface AsciiLineProps {
Expand All @@ -12,7 +12,7 @@ export const AsciiLine = ({
withEndCap,
capChar = `+`,
}: Readonly<AsciiLineProps>) => {
const { charWidth } = useCharWidth();
const { width: charWidth } = useCharDimensions();

return (
<span
Expand Down
6 changes: 4 additions & 2 deletions src/components/AsciiProgressBar/AsciiProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getCharWidth } from '@/util/helper';
import { getCharDimensions } from '@/util/helper';
import { useEffect, useRef, useState } from 'react';

export interface AsciiProgressBarProps {
Expand Down Expand Up @@ -29,6 +29,7 @@ export const AsciiProgressBar = ({
const [currentInterval, setCurrentInterval] = useState<NodeJS.Timeout | null>(
null,
);

useEffect(() => {
const currentRef = wrapperRef.current;
// find target width in chars
Expand All @@ -37,7 +38,8 @@ export const AsciiProgressBar = ({
setProgressCharCount(0);
const { width } = entries[0].contentRect;
setWidthInChars(0);
const charWidth = getCharWidth();
// purposely not using the hook here since we don't want to trigger a re-render
const { width: charWidth } = getCharDimensions();
const widthInChars =
Math.floor(width / charWidth) -
3 - // account for left and right end caps (+1 for percentage padding on the right)
Expand Down
76 changes: 60 additions & 16 deletions src/components/Shell/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import {
PromptHistoryEntry,
PromptResult,
} from '@/context/PromptHistoryContext/types';
import { useCharDimensions } from '@/hooks';
import { CustomEvents, RunEvent, SearchParams } from '@/util/types';
import { useLocation } from '@reach/router';
import clsx from 'clsx';
import { useTranslation } from 'gatsby-plugin-react-i18next';
import {
ChangeEventHandler,
Expand All @@ -44,14 +46,17 @@ export const Shell = ({
initialPrompt,
}: Readonly<ShellProps>) => {
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const textAreaCopyRef = useRef<HTMLTextAreaElement>(null);

const { promptHistory, dispatch } = useContext(PromptHistoryContext);
const [currentPrompt, setCurrentPrompt] = useState<string>(``);
const [tmpPrompt, setTmpPrompt] = useState<string>(``); // used for arrow up/down
const [standalone, setStandalone] = useState<ReactNode | null>(null);
const [currentDescHistoryIndex, setCurrentDescHistoryIndex] = useState(-1); // -1 means current prompt is not in history
const [tabSuggestions, setTabSuggestions] = useState<string[]>([]);
const [isMainFlexCol, setIsMainFlexCol] = useState<boolean>(false);

const { height: charHeight } = useCharDimensions();
const location = useLocation();
const { t } = useTranslation();

Expand Down Expand Up @@ -136,6 +141,7 @@ export const Shell = ({
});

if (!(`ontouchstart` in window)) {
console.log(`focusing textarea`);
textAreaRef.current?.focus();
}
},
Expand All @@ -160,10 +166,6 @@ export const Shell = ({
});
}, [dispatch, promptHistory, standalone]);

useEffect(() => {
textAreaRef.current?.scrollIntoView();
}, [currentPrompt]);

useEffect(() => {
window.addEventListener(CustomEvents.run, handleRunEvent);
window.addEventListener(
Expand Down Expand Up @@ -195,16 +197,20 @@ export const Shell = ({
e,
) => {
setCurrentPrompt(e.target.value);
if (!textAreaRef.current) return;
if (!textAreaRef.current || !textAreaCopyRef.current) return;
textAreaRef.current.style.height = `auto`; // reset height
textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`; // auto grow the textarea to fit the text
setIsMainFlexCol(
textAreaCopyRef.current.scrollHeight > charHeight &&
e.target.value !== ``,
);
};

const handleUserTextAreaKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (
event,
) => {
setTabSuggestions([]);

textAreaRef.current?.scrollIntoView();
if (event.key.length === 1) {
setCurrentDescHistoryIndex(-1); // only reset history index if user is typing
setTmpPrompt(currentPrompt + event.key);
Expand Down Expand Up @@ -292,6 +298,7 @@ export const Shell = ({
flex-col
hover:cursor-text
relative
break-words
w-full`}
onClickCapture={(e) => {
if (
Expand All @@ -311,10 +318,10 @@ export const Shell = ({
if (hideEntry) return null;
return (
<Fragment key={i}>
<p className="w-full">
<span>
<PromptPrefix username={username} domain={domain} />
{fullPrompt}
</p>
</span>
{responses.map((response, j) => (
<Fragment key={j}>
{response.result !== `` ? (
Expand All @@ -327,29 +334,39 @@ export const Shell = ({
</Fragment>
);
})}
<pre id="active-prompt" className="w-full flex relative">
<PromptPrefix username={username} domain={domain} />
<div
id="active-prompt"
className={clsx(
`
w-full
flex
relative`,
isMainFlexCol && `flex-col`,
)}
>
<label className="sr-only" htmlFor="prompt">
CLI prompt
</label>
<PromptPrefix username={username} domain={domain} />
<textarea
id="prompt"
rows={1}
tabIndex={0}
ref={textAreaRef}
onKeyDown={handleUserTextAreaKeyDown}
onChange={handleUserTextValueChange}
value={currentPrompt}
className={
`
w-full
focus:outline-none
overflow-hidden
resize-none
`
w-full
focus:outline-none
overflow-hidden
resize-none
`
// overflow-hidden and resize-none are necessary for the auto grow textarea
}
/>
</pre>
</div>
{tabSuggestions.length > 0 && (
<div className="flex flex-col">
{tabSuggestions
Expand All @@ -361,6 +378,33 @@ export const Shell = ({
)}
</>
)}
{/* Hidden prompt copy for width measurement */}
<div
className="
invisible
-z-10
-mt-6
flex
w-full"
>
<PromptPrefix
username={username}
domain={domain}
className="pointer-events-none"
/>
<textarea
rows={1}
tabIndex={-1}
readOnly
disabled
ref={textAreaCopyRef}
value={currentPrompt}
className={`
w-full
pointer-events-none
`}
/>
</div>
</main>
);
};
17 changes: 6 additions & 11 deletions src/components/Table/TableTextCell/TableTextCell.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AsciiLine } from '@/components/AsciiLine';
import { getCharWidth } from '@/util/helper';
import { useCharDimensions } from '@/hooks';
import clsx from 'clsx';
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';

Expand All @@ -14,10 +14,10 @@ export const TableTextCell = ({
}: TableTextCellProps) => {
const [textLines, setTextLines] = useState<string[]>([]);
const cellRef = useRef<HTMLTableCellElement>(null);
const { width: charWidth } = useCharDimensions();

const handleResize = useCallback(() => {
if (!cellRef?.current) return;
const charWidth = getCharWidth();
const cellWidth = cellRef?.current?.offsetWidth || 0;
const words = children.split(` `);
let line = ``;
Expand All @@ -37,21 +37,16 @@ export const TableTextCell = ({
line += `${words[i]} `;
}
setTextLines(lines);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [children]);

useEffect(() => {
handleResize();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [charWidth, children]);

useEffect(() => {
if (!cellRef?.current) return;

window.addEventListener(`resize`, handleResize);
handleResize();

return () => window.removeEventListener(`resize`, handleResize);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [handleResize]);

return (
<td ref={cellRef} className="flex flex-col w-full relative pt-6">
Expand Down
14 changes: 8 additions & 6 deletions src/hooks/useCharWidth/useCharWidth.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { getCharWidth } from '@/util/helper';
import { getCharDimensions } from '@/util/helper';
import { useEffect, useState } from 'react';

export const useCharWidth = () => {
export const useCharDimensions = () => {
const [charWidth, setCharWidth] = useState<number>(0);
const [charHeight, setCharHeight] = useState<number>(0);
useEffect(() => {
if (charWidth > 0) return;
if (typeof window === `undefined`) return;
setCharWidth(getCharWidth());
}, [charWidth]);
const { width, height } = getCharDimensions();
setCharWidth(width);
setCharHeight(height);
}, []);

return { charWidth };
return { width: charWidth, height: charHeight };
};
5 changes: 3 additions & 2 deletions src/util/helper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { StrapiCollection } from '@/util/types';

export function getCharWidth(): number {
export function getCharDimensions(): { width: number; height: number } {
const span = document.createElement(`span`);

// Set the font and content
Expand All @@ -11,11 +11,12 @@ export function getCharWidth(): number {
// Append it to the body and measure
document.body.appendChild(span);
const width = span.offsetWidth; // Get the width of the character
const height = span.offsetHeight; // Get the height of the character

// Clean up
document.body.removeChild(span);

return width;
return { width, height };
}

export function escapeMarkdown(toEscape: string, replacer: string = `\\$1`) {
Expand Down

0 comments on commit d67288c

Please sign in to comment.