{"version":3,"file":"EditInPlace.cjs","names":[],"sources":["../../../../../src/components/Form/Controls/EditInPlace/EditInPlace.tsx"],"sourcesContent":["/*\nCopyright 2024 New Vector Ltd.\n\nSPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial\nPlease see LICENSE files in the repository root for full details.\n*/\n\nimport React, {\n  forwardRef,\n  useCallback,\n  useRef,\n  useState,\n  useEffect,\n  useReducer,\n} from \"react\";\nimport { Submit, ValidityState } from \"@radix-ui/react-form\";\nimport CheckIcon from \"@vector-im/compound-design-tokens/assets/web/icons/check\";\nimport CancelIcon from \"@vector-im/compound-design-tokens/assets/web/icons/close\";\n\nimport styles from \"./EditInPlace.module.css\";\n\nimport {\n  Field,\n  HelpMessage,\n  Label,\n  LoadingMessage,\n  Root,\n  SuccessMessage,\n  TextControl,\n} from \"../..\";\nimport { Button, Tooltip } from \"../../../..\";\n\ntype Props = {\n  /**\n   * The label for the control\n   */\n  label: string;\n\n  /**\n   * The CSS class name.\n   */\n  className?: string;\n\n  /**\n   * Callback for when the user confirms the change\n   */\n  onSave?: (e: React.FormEvent<HTMLFormElement>) => Promise<void> | void;\n\n  /**\n   * Callback for when the user wishes to cancel the change\n   */\n  onCancel?: (e: React.FormEvent<HTMLFormElement>) => void;\n\n  /**\n   * onInput event handler on the text control\n   */\n  onInput?: (e: React.ChangeEvent<HTMLInputElement>) => void;\n\n  /**\n   * Callback for when the server validation errors should be cleared.\n   */\n  onClearServerErrors?: () => void;\n\n  /**\n   * Whether the field is in an error state according to the server validation.\n   *\n   * For validation messages, use native validations properties directly, or add custom error messages as children.\n   */\n  serverInvalid?: boolean;\n\n  /**\n   * Label to be displayed by the green check at the bottom. Will only be displayed\n   * for 2 seconds after the onSave callback promise resolves successfully.\n   */\n  savedLabel?: string;\n\n  /**\n   * The label for the save button\n   */\n  saveButtonLabel: string;\n\n  /**\n   * The label for the 'in progress' saving caption\n   */\n  savingLabel: string;\n\n  /**\n   * The label for the cancel button\n   */\n  cancelButtonLabel: string;\n\n  /**\n   * Label to be displayed under the input as a help text\n   */\n  helpLabel?: string;\n\n  /**\n   * If true, disabled the entire component to disallow editing.\n   */\n  disabled?: boolean;\n} & React.ComponentProps<typeof TextControl>;\n\nenum State {\n  /** No changes on the input has been made */\n  Initial,\n\n  /** The input has been changed */\n  Dirty,\n\n  /** The input is being saved */\n  Saving,\n\n  /** The input has been saved */\n  Saved,\n}\n\nenum Event {\n  Touch, // The user 'touched' the control\n  Save, // The user has clicked the save button\n  Saved, // The onSave callback finished successfully\n  SaveError, // The onSave callback finished with an error\n  Cancel, // The user has clicked the cancel button\n  SavedTimeout, // The user has clicked the save button and the saved label has been shown for 2 seconds\n}\n\nfunction reducer(state: State, action: Event): State {\n  switch (action) {\n    case Event.Touch:\n      if (state === State.Initial || state === State.Saved) return State.Dirty;\n      else return state;\n\n    case Event.Save:\n      return State.Saving;\n\n    case Event.Cancel:\n      return State.Initial;\n\n    case Event.Saved:\n      if (state === State.Saving) return State.Saved;\n      else return state;\n\n    case Event.SaveError:\n      if (state === State.Saving) return State.Initial;\n      else return state;\n\n    case Event.SavedTimeout:\n      if (state === State.Saved) return State.Initial;\n      else return state;\n  }\n\n  assertNever(action);\n}\n\nfunction assertNever(value: never): never {\n  throw new Error(`Unreachable value: ${value}`);\n}\n\n/**\n * A text box with save/cancel buttons that appear when the field is active.\n * Since thios control has its own 'save' button, it should *not* appear as part\n * of a larger form: it exists as its own form that submits separately.\n */\nexport const EditInPlace = forwardRef<HTMLInputElement, Props>(\n  function EditInPlace(\n    {\n      className,\n      label,\n      onSave,\n      onCancel,\n      onInput,\n      onClearServerErrors,\n      serverInvalid,\n      saveButtonLabel,\n      cancelButtonLabel,\n      savedLabel,\n      savingLabel,\n      helpLabel,\n      disabled,\n      children,\n      ...props\n    },\n    ref,\n  ) {\n    const [state, dispatch] = useReducer(reducer, State.Initial);\n\n    // Tracks the focus state of the form\n    // This uses a `ref` to make sure the onFocus/onBlur callback don't trigger unnecessary re-renders\n    // and a state to track the focus state and hide the buttons when the form is not focused\n    const isFocusWithinRef = useRef(false);\n    const [isFocusWithin, setFocusWithin] = useState(false);\n\n    const shouldShowSaveButton =\n      state === State.Dirty || state === State.Saving || isFocusWithin;\n\n    const hideTimer = useRef<ReturnType<typeof setTimeout> | undefined>(\n      undefined,\n    );\n\n    useEffect(() => {\n      // Start a timer when we switch to the saved state\n      if (state === State.Saved) {\n        hideTimer.current = setTimeout(() => {\n          dispatch(Event.SavedTimeout);\n          hideTimer.current = undefined;\n        }, 2000);\n      }\n\n      return () => {\n        // Clear any timers that may have been set\n        if (hideTimer.current) clearTimeout(hideTimer.current);\n        hideTimer.current = undefined;\n      };\n    }, [state]);\n\n    const formRef = useRef<HTMLFormElement>(null);\n    const saveButtonRef = useRef<HTMLButtonElement>(null);\n    const cancelButtonRef = useRef<HTMLButtonElement>(null);\n\n    const onFocus = useCallback(() => {\n      if (isFocusWithinRef.current) return;\n      isFocusWithinRef.current = true;\n      setFocusWithin(true);\n    }, [isFocusWithin, setFocusWithin]);\n\n    const onBlur = useCallback(\n      (e: React.FocusEvent) => {\n        if (!isFocusWithinRef.current) return;\n        // If the user switched to another element within the form\n        // consider that we're still focused within the form\n        if (e.currentTarget.contains(e.relatedTarget)) return;\n\n        isFocusWithinRef.current = false;\n        setFocusWithin(false);\n      },\n      [isFocusWithin, setFocusWithin],\n    );\n\n    const onInputHandler = useCallback(\n      (e: React.InputEvent<HTMLInputElement>) => {\n        dispatch(Event.Touch);\n        onInput?.(e);\n      },\n      [dispatch, onInput],\n    );\n\n    const onFormSubmit = useCallback(\n      async (e: React.FormEvent<HTMLFormElement>) => {\n        e.preventDefault();\n\n        // Prevent submitting the form if the user has not yet entered any text\n        if (state === State.Initial) {\n          return;\n        }\n\n        try {\n          dispatch(Event.Save);\n          saveButtonRef.current?.blur();\n          await onSave?.(e);\n          dispatch(Event.Saved);\n        } catch {\n          // We don't really need to do anything here, we just don't want to display the\n          // 'saved' label, obviously. The user of the component can update the error to\n          // show what failed.\n          dispatch(Event.SaveError);\n        }\n      },\n      [onSave, state, hideTimer],\n    );\n\n    const onFormReset = useCallback(\n      (e: React.FormEvent<HTMLFormElement>) => {\n        cancelButtonRef.current?.blur();\n        onCancel?.(e);\n        dispatch(Event.Cancel);\n      },\n      [cancelButtonRef, onCancel],\n    );\n\n    return (\n      <Root\n        className={className}\n        onSubmit={onFormSubmit}\n        onReset={onFormReset}\n        onFocus={onFocus}\n        onBlur={onBlur}\n        onClearServerErrors={onClearServerErrors}\n        ref={formRef}\n      >\n        <Field name=\"input\" serverInvalid={serverInvalid}>\n          <Label>{label}</Label>\n          <div className={styles.controls}>\n            <TextControl\n              ref={ref}\n              {...props}\n              onInput={onInputHandler}\n              disabled={disabled || state === State.Saving}\n            />\n\n            {shouldShowSaveButton && (\n              <div className={styles[\"button-group\"]}>\n                <Tooltip label={saveButtonLabel}>\n                  <Submit asChild>\n                    <Button\n                      type=\"submit\"\n                      kind=\"primary\"\n                      size=\"sm\"\n                      ref={saveButtonRef}\n                      disabled={state !== State.Dirty}\n                      iconOnly\n                      Icon={CheckIcon}\n                    />\n                  </Submit>\n                </Tooltip>\n\n                <Tooltip label={cancelButtonLabel}>\n                  <Button\n                    type=\"reset\"\n                    kind=\"secondary\"\n                    size=\"sm\"\n                    ref={cancelButtonRef}\n                    className={styles.button}\n                    disabled={state === State.Saving}\n                    iconOnly\n                    Icon={CancelIcon}\n                  />\n                </Tooltip>\n              </div>\n            )}\n          </div>\n\n          {/*\n            During the loading saving state, we only show the saving message.\n            Else, we show whatever children were passed on, as they will have other validation messages\n          */}\n          {state === State.Saving ? (\n            <LoadingMessage>{savingLabel}</LoadingMessage>\n          ) : (\n            children\n          )}\n\n          {savedLabel && state === State.Saved && (\n            <SuccessMessage>{savedLabel}</SuccessMessage>\n          )}\n\n          {/*\n            We show the help message only if:\n              - the helpLabel is set\n              - the form hasn't been validated yet\n              - the 'serverInvalid' prop is not set\n              - we're in the initial or dirty state\n            */}\n          {helpLabel && (state === State.Initial || state === State.Dirty) && (\n            <ValidityState>\n              {(validity) =>\n                (validity === undefined || validity.valid) &&\n                !serverInvalid && <HelpMessage>{helpLabel}</HelpMessage>\n              }\n            </ValidityState>\n          )}\n        </Field>\n      </Root>\n    );\n  },\n);\n"],"mappings":"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"}