diff --git a/inst/editor/src/components/Inputs/ListInput/KeyInput.tsx b/inst/editor/src/components/Inputs/ListInput/KeyInput.tsx new file mode 100644 index 000000000..aa3815cd5 --- /dev/null +++ b/inst/editor/src/components/Inputs/ListInput/KeyInput.tsx @@ -0,0 +1,115 @@ +import React from "react"; + +import { mergeClasses } from "../../../utils/mergeClasses"; + +import type { useListState } from "./useListState"; + +/** + * Props for the KeyInput component. + */ +type KeyInputProps = { + /** The index of the element in the list. */ + index: number; + /** The current value of the key. */ + keyValue: string; + /** Function to call when the key value is updated. */ + onUpdate: (newKey: string) => void; + /** The list of all elements in the list. Used to check for duplicate keys */ + allElements: ReturnType["flatList"]; +}; + +/** + * Custom input for the key field of a list item. This input will show a warning + * if the key is empty or if it is the same as another key in the list + * @param props KeyInputProps object + * @returns + */ +export function KeyInput(props: KeyInputProps) { + const { displayedValue, isInvalid, onNewValue, isSameAsOtherKey } = + useKeyInput(props); + + return ( + + { + e.stopPropagation(); + onNewValue(e.target.value); + }} + /> + + ); +} + +/** + * Custom input for the key field of a list item. This input will show a warning + * if the key is empty or if it is the same as another key in the list + * @param params KeyInputProps object + * @returns An object with the current value of the input, whether or not it is + * invalid, and a function to update the value + * @see useListState + */ +function useKeyInput({ + keyValue, + onUpdate, + allElements, + index, +}: KeyInputProps) { + // Displayed value can sometimes be differnt from the actual value if the user + // enters an invalid value. We want to show that value to the user, but not + // update the actual value until they enter a valid value + const [displayedValue, setDisplayedValue] = React.useState(keyValue); + + // Make sure whenever the passed in "true" value updates, then we update the + // displayed value to match it + // Done this way thanks to these docs https://react.dev/learn/you-might-not-need-an-effect + const [prevValue, setPrevValue] = React.useState(keyValue); + + if (prevValue !== keyValue) { + setPrevValue(keyValue); + setDisplayedValue(keyValue); + } + + const otherKeys = allElements.filter((_, i) => i !== index).map((e) => e.key); + + const isEmpty = displayedValue === ""; + + const isSameAsOtherKey = otherKeys.includes(displayedValue); + + const isInvalid = isEmpty || isSameAsOtherKey; + + function onNewValue(newValue: string) { + setDisplayedValue(newValue); + + const validNewValue = !( + newValue === "" || + otherKeys.some((key) => key.toLowerCase() === newValue.toLowerCase()) + ); + + if (validNewValue) { + onUpdate(newValue); + } + } + + return { + displayedValue, + isInvalid, + isSameAsOtherKey, + onNewValue, + }; +} diff --git a/inst/editor/src/components/Inputs/ListInput/NamedListInput.tsx b/inst/editor/src/components/Inputs/ListInput/NamedListInput.tsx index c7d2dc367..d66ecb611 100644 --- a/inst/editor/src/components/Inputs/ListInput/NamedListInput.tsx +++ b/inst/editor/src/components/Inputs/ListInput/NamedListInput.tsx @@ -12,6 +12,7 @@ import { Trash } from "../../Icons"; import { ControlledPopup } from "../../PopoverEl/ControlledPopup"; import Button from "../Button/Button"; +import { KeyInput } from "./KeyInput"; import { useListState } from "./useListState"; export function NamedListInput({ @@ -260,71 +261,3 @@ function ListItem({ /> ); } - -function KeyInput({ - keyValue, - onUpdate, - allElements, - index, -}: { - index: number; - keyValue: string; - onUpdate: (newKey: string) => void; - allElements: ReturnType["flatList"]; -}) { - const [displayedValue, setDisplayedValue] = React.useState(keyValue); - - // Make sure whenever the passed in "true" value updates, then we update the - // displayed value to match it - React.useEffect(() => { - setDisplayedValue(keyValue); - }, [keyValue]); - - const otherKeys = allElements.filter((_, i) => i !== index).map((e) => e.key); - - const isEmpty = displayedValue === ""; - - const isSameAsOtherKey = otherKeys.includes(displayedValue); - - const isInvalid = isEmpty || isSameAsOtherKey; - - return ( - - { - e.stopPropagation(); - const newValue = e.target.value; - - setDisplayedValue(newValue); - - const validNewValue = !( - newValue === "" || - otherKeys.some( - (key) => key.toLowerCase() === newValue.toLowerCase() - ) - ); - - if (validNewValue) { - onUpdate(e.target.value); - } - }} - /> - - ); -}