Skip to content
Merged
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
@@ -1,16 +1,11 @@
import { Box, Flex, Text } from "@radix-ui/themes";
import { compactHomePath } from "@utils/path";
import { useCallback, useEffect, useRef } from "react";
import { isOtherOption } from "./constants";
import { OptionRow } from "./OptionRow";
import { StepTabs } from "./StepTabs";
import type { ActionSelectorProps, SelectorOption } from "./types";
import type { ActionSelectorProps } from "./types";
import { useActionSelectorState } from "./useActionSelectorState";

function needsCustomInput(option: SelectorOption): boolean {
return option.customInput === true || isOtherOption(option.id);
}

export function ActionSelector({
title,
pendingAction,
Expand Down Expand Up @@ -199,7 +194,15 @@ export function ActionSelector({
ref={containerRef}
tabIndex={0}
p="3"
onClick={() => containerRef.current?.focus()}
onClick={(e) => {
if (
e.target instanceof HTMLElement &&
e.target.closest("[contenteditable]")
) {
return;
}
containerRef.current?.focus();
}}
style={{
outline: "none",
border: "1px solid var(--blue-11)",
Expand Down Expand Up @@ -233,10 +236,7 @@ export function ActionSelector({
<Flex direction="column" gap="1">
{allOptions.map((option, index) => {
const isSelected = selectedIndex === index;
const hasCustomContent =
needsCustomInput(option) && customInput.trim() !== "";
const isChecked =
checkedOptions.has(option.id) || hasCustomContent;
const isChecked = checkedOptions.has(option.id);

return (
<OptionRow
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Text } from "@radix-ui/themes";
import { Box, Text } from "@radix-ui/themes";
import { useCallback, useEffect } from "react";

interface InlineEditableTextProps {
Expand All @@ -24,7 +24,7 @@ export function InlineEditableText({
}: InlineEditableTextProps) {
useEffect(() => {
if (inputRef.current) {
inputRef.current.textContent = value || placeholder;
inputRef.current.textContent = value || "";
inputRef.current.focus();
if (value) {
const range = document.createRange();
Expand All @@ -35,23 +35,14 @@ export function InlineEditableText({
sel?.addRange(range);
}
}
}, [inputRef, placeholder, value]);
}, [inputRef, value]);

const handleInput = useCallback(
(e: React.FormEvent<HTMLSpanElement>) => {
const text = e.currentTarget.textContent ?? "";
onChange(text);
if (!text && inputRef.current) {
inputRef.current.textContent = placeholder;
const range = document.createRange();
range.setStart(inputRef.current, 0);
range.collapse(true);
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
}
},
[onChange, placeholder, inputRef],
[onChange],
);

const handleKeyDown = useCallback(
Expand All @@ -68,45 +59,60 @@ export function InlineEditableText({
} else if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
onSubmit();
} else if (!value && e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
e.currentTarget.textContent = e.key;
onChange(e.key);
const range = document.createRange();
range.selectNodeContents(e.currentTarget);
range.collapse(false);
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
}
},
[value, onChange, onNavigateUp, onNavigateDown, onEscape, onSubmit],
[onNavigateUp, onNavigateDown, onEscape, onSubmit],
);

return (
<Text
asChild
size="1"
weight="medium"
className={value ? "text-gray-12" : "text-gray-10"}
<Box
style={{
display: "inline-grid",
minWidth: "200px",
}}
>
{/* biome-ignore lint/a11y/useSemanticElements: contentEditable span needed for inline editing UX */}
<span
ref={inputRef}
role="textbox"
tabIndex={0}
contentEditable
suppressContentEditableWarning
onInput={handleInput}
onKeyDown={handleKeyDown}
style={{
outline: "none",
minWidth: "200px",
display: "inline-block",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
/>
</Text>
{!value && (
<Text
size="1"
weight="medium"
className="text-gray-10"
style={{
gridRow: 1,
gridColumn: 1,
pointerEvents: "none",
userSelect: "none",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
>
{placeholder}
</Text>
)}
<Text
asChild
size="1"
weight="medium"
className={value ? "text-gray-12" : ""}
>
{/* biome-ignore lint/a11y/useSemanticElements: contentEditable span needed for inline editing UX */}
<span
ref={inputRef}
role="textbox"
tabIndex={0}
contentEditable
suppressContentEditableWarning
onClick={(e) => e.stopPropagation()}
onInput={handleInput}
onKeyDown={handleKeyDown}
style={{
gridRow: 1,
gridColumn: 1,
outline: "none",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
/>
</Text>
</Box>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -310,31 +310,85 @@ export function useActionSelectorState({
containerRef.current?.focus();
}, []);

const handleCustomInputChange = useCallback(
(value: string) => {
setCustomInput(value);
if (
value.trim() &&
showSubmitButton &&
selectedOption &&
needsCustomInput(selectedOption)
) {
setCheckedOptions((prev) => {
if (prev.has(selectedOption.id)) return prev;
const next = new Set(prev);
next.add(selectedOption.id);
return next;
});
}
},
[showSubmitButton, selectedOption],
);

const ensureChecked = useCallback((optionId: string) => {
setCheckedOptions((prev) => {
if (prev.has(optionId)) return prev;
const next = new Set(prev);
next.add(optionId);
return next;
});
}, []);

const handleInlineSubmit = useCallback(() => {
if (!selectedOption) return;
if (showSubmitButton) {
toggleCheck(selectedOption.id);
ensureChecked(selectedOption.id);
containerRef.current?.focus();
moveDown();
} else if (customInput.trim()) {
onSelect(selectedOption.id, customInput.trim());
}
}, [showSubmitButton, toggleCheck, selectedOption, customInput, onSelect]);
}, [
showSubmitButton,
ensureChecked,
selectedOption,
customInput,
onSelect,
moveDown,
]);

const handleNavigateUp = useCallback(() => {
if (
selectedOption &&
needsCustomInput(selectedOption) &&
customInput.trim() &&
showSubmitButton
) {
ensureChecked(selectedOption.id);
}
containerRef.current?.focus();
moveUp();
}, [moveUp]);
}, [moveUp, selectedOption, customInput, showSubmitButton, ensureChecked]);

const handleNavigateDown = useCallback(() => {
if (
selectedOption &&
needsCustomInput(selectedOption) &&
customInput.trim() &&
showSubmitButton
) {
ensureChecked(selectedOption.id);
}
containerRef.current?.focus();
moveDown();
}, [moveDown]);
}, [moveDown, selectedOption, customInput, showSubmitButton, ensureChecked]);

return {
selectedIndex,
setSelectedIndex,
checkedOptions,
customInput,
setCustomInput,
setCustomInput: handleCustomInputChange,
isEditing,
activeStep,
stepAnswers,
Expand Down
Loading