diff --git a/packages/css-value-input/README.md b/packages/css-value-input/README.md index 3796fc85..67082f43 100644 --- a/packages/css-value-input/README.md +++ b/packages/css-value-input/README.md @@ -25,8 +25,8 @@ yarn add @acusti/css-value-input ### Props -This is the type signature for the props you can pass to `CSSValueInput`. The -unique features provided by the component are called out and explained +This is the type signature for the props you can pass to `CSSValueInput`. +The unique features provided by the component are called out and explained above the corresponding prop via JSDoc comments: ```ts diff --git a/packages/css-value-input/src/CSSValueInput.tsx b/packages/css-value-input/src/CSSValueInput.tsx index 6cd9a42b..a2f7cd52 100644 --- a/packages/css-value-input/src/CSSValueInput.tsx +++ b/packages/css-value-input/src/CSSValueInput.tsx @@ -127,10 +127,10 @@ export default React.forwardRef(function CSSValueInput( // ensures that submitting a new value with no unit doesn’t add a unit const defaultUnit = unit ? getUnitFromCSSValue({ - cssValueType, - defaultUnit: unit, - value: submittedValueRef.current, - }) + cssValueType, + defaultUnit: unit, + value: submittedValueRef.current, + }) : ''; if (!isCurrentValueFinite) { diff --git a/packages/docs/stories/InputText.stories.tsx b/packages/docs/stories/InputText.stories.tsx index d55c226f..2e7e1a5d 100644 --- a/packages/docs/stories/InputText.stories.tsx +++ b/packages/docs/stories/InputText.stories.tsx @@ -63,7 +63,6 @@ export const MultiLineInputWithInitialValueAndSelectTextOnFocus: Story = { }, }; -// TODO how do i wrap this in a
{console.log('form submitted')}}>? const SUBMIT_ON_ENTER_PROPS = { className: 'multi-line-input-text', maxHeight: 600, @@ -103,3 +102,12 @@ export const MultiLineInputWithSubmitOnEnterNoForm: Story = { name: SUBMIT_ON_ENTER_PROPS.name + '-no-form', }, }; + +export const InputWithDoubleClickToEdit: Story = { + args: { + className: 'input-text-double-click-to-edit', + doubleClickToEdit: true, + initialValue: 'Lorem ipsum dolor sit amet', + name: 'double-click-to-edit-input', + }, +}; diff --git a/packages/dropdown/src/Dropdown.tsx b/packages/dropdown/src/Dropdown.tsx index 048647e6..fd14c52b 100644 --- a/packages/dropdown/src/Dropdown.tsx +++ b/packages/dropdown/src/Dropdown.tsx @@ -98,7 +98,7 @@ type MousePosition = { clientX: number; clientY: number }; const { Children, Fragment, useCallback, useEffect, useMemo, useRef, useState } = React; -const noop = () => { }; // eslint-disable-line @typescript-eslint/no-empty-function +const noop = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function const CHILDREN_ERROR = '@acusti/dropdown requires either 1 child (the dropdown body) or 2 children: the dropdown trigger and the dropdown body.'; @@ -679,13 +679,13 @@ export default function Dropdown({ ...styleFromProps, ...(outOfBounds.maxHeight ? { - [BODY_MAX_HEIGHT_VAR]: `calc(${outOfBounds.maxHeight}px - var(--uktdd-body-buffer))`, - } + [BODY_MAX_HEIGHT_VAR]: `calc(${outOfBounds.maxHeight}px - var(--uktdd-body-buffer))`, + } : null), ...(outOfBounds.maxWidth ? { - [BODY_MAX_WIDTH_VAR]: `calc(${outOfBounds.maxWidth}px - var(--uktdd-body-buffer))`, - } + [BODY_MAX_WIDTH_VAR]: `calc(${outOfBounds.maxWidth}px - var(--uktdd-body-buffer))`, + } : null), }), [outOfBounds.maxHeight, outOfBounds.maxWidth, styleFromProps], diff --git a/packages/input-text/README.md b/packages/input-text/README.md index 54a53fbf..35e2f00c 100644 --- a/packages/input-text/README.md +++ b/packages/input-text/README.md @@ -37,6 +37,13 @@ type Props = { autoComplete?: HTMLInputElement['autocomplete']; className?: string; disabled?: boolean; + /** + * If true, input renders as readonly initially and only becomes interactive + * when double-clicked or when user focuses the readonly input and then + * presses the enter key. Likewise, the input becomes readonly again when + * it is blurred or when the user presses enter or escape. + */ + doubleClickToEdit?: boolean; enterKeyHint?: InputHTMLAttributes['enterKeyHint']; form?: string; /** diff --git a/packages/input-text/src/InputText.tsx b/packages/input-text/src/InputText.tsx index 13b05c45..27860e74 100644 --- a/packages/input-text/src/InputText.tsx +++ b/packages/input-text/src/InputText.tsx @@ -8,6 +8,13 @@ export type Props = { autoComplete?: HTMLInputElement['autocomplete']; className?: string; disabled?: boolean; + /** + * If true, input renders as readonly initially and only becomes interactive + * when double-clicked or when user focuses the readonly input and then + * presses the enter key. Likewise, the input becomes readonly again when + * it is blurred or when the user presses enter or escape. + */ + doubleClickToEdit?: boolean; enterKeyHint?: InputHTMLAttributes['enterKeyHint']; form?: string; /** @@ -62,6 +69,7 @@ export default React.forwardRef(function InputText( autoComplete, className, disabled, + doubleClickToEdit, enterKeyHint, form, initialValue, @@ -109,17 +117,28 @@ export default React.forwardRef(function InputText( inputRef.current.value = initialValue ?? ''; }, [initialValue]); + const [readOnlyState, setReadOnlyState] = useState( + readOnly ?? doubleClickToEdit, + ); const isInitialSelectionRef = useRef(true); + const startEditing = useCallback(() => { + if (!doubleClickToEdit) return; + setReadOnlyState(false); + }, [doubleClickToEdit]); + const handleBlur = useCallback( (event: React.FocusEvent) => { if (onBlur) onBlur(event); + if (doubleClickToEdit) { + setReadOnlyState(true); + } if (!selectTextOnFocus) return; setInputElement(event.currentTarget); // When input loses focus, reset isInitialSelection to true for next onSelect event isInitialSelectionRef.current = true; }, - [onBlur, selectTextOnFocus, setInputElement], + [doubleClickToEdit, onBlur, selectTextOnFocus, setInputElement], ); const setInputHeight = useCallback(() => { @@ -185,9 +204,17 @@ export default React.forwardRef(function InputText( // if no form to submit, trigger input blur event.currentTarget.blur(); } + } else if (doubleClickToEdit && inputRef.current) { + if (readOnlyState) { + if (event.key === 'Enter') { + setReadOnlyState(false); + } + } else if (event.key === 'Enter' || event.key === 'Escape') { + inputRef.current.blur(); + } } }, - [multiLine, onKeyDown, submitOnEnter], + [doubleClickToEdit, multiLine, onKeyDown, readOnlyState, submitOnEnter], ); // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment @@ -209,13 +236,14 @@ export default React.forwardRef(function InputText( name={name} onBlur={handleBlur} onChange={onChange} + onDoubleClick={startEditing} onFocus={onFocus} onKeyDown={handleKeyDown} onKeyUp={onKeyUp} onSelect={handleSelect} pattern={pattern} placeholder={placeholder} - readOnly={readOnly} + readOnly={readOnlyState} ref={setInputElement} required={required} size={size}