UNPKG

@neo4j-ndl/react

Version:

React implementation of Neo4j Design System

178 lines 9 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * * Copyright (c) "Neo4j" * Neo4j Sweden AB [http://neo4j.com] * * This file is part of Neo4j. * * Neo4j is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { typescale } from '@neo4j-ndl/base'; import { forwardRef, useCallback, useEffect, useMemo, useState, } from 'react'; import { useFocusWithin } from 'react-aria'; import { classNames } from '../_common/defaultImports'; import { randomId } from '../_common/utils'; import { IconButton } from '../icon-button'; import { CheckIconOutline, ExclamationCircleIconSolid, PencilIconOutline, XMarkIconOutline, } from '../icons'; import { Tooltip } from '../tooltip'; import { Typography } from '../typography'; function getIconSize(variant) { switch (variant) { case 'h1': case 'h2': return '24'; case 'h3': case 'h4': case 'subheading-large': return '20'; default: return '16'; } } function getErrorIconSize(variant) { switch (variant) { case 'h1': case 'h2': return '24'; case 'h3': case 'h4': return '20'; default: return '16'; } } function isTypescalePropertyObj(obj) { return Boolean(obj && typeof obj === 'object' && ('lineHeight' in obj)); } function getMinHeight(variant) { let key; switch (variant) { case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': key = variant; break; default: key = `n-${variant}`; break; } const value = typescale[key][1]; if (isTypescalePropertyObj(value)) { return value.lineHeight; } // Subheading medium is the default, will never reach here, // just to satisfy the TS compiler return '1.5rem'; } export const InlineEdit = forwardRef(function InlineEdit(props, ref) { const { label, defaultValue, variant = 'subheading-medium', isEditing = undefined, onConfirm, onCancel, errorText, hasEditIcon, isDisabled = false, inputProps = {}, typographyProps = {}, isCancellingOnBlur = false, validate, htmlAttributes, className, style, placeholder, } = props; const Component = props.as || 'div'; const [hasEditMode, setEditMode] = useState(false); const [internalErrorText, setErrorText] = useState(errorText); const [value, setValue] = useState(defaultValue); useEffect(() => { // Reset the value if the default value changes setValue(defaultValue); }, [defaultValue]); const iconSize = getIconSize(variant); const hasError = Boolean(internalErrorText); const hasLabel = Boolean(label); const { focusWithinProps } = useFocusWithin({ onBlurWithin: () => { if (isCancellingOnBlur) { onCancelHandler(); } }, }); const resetToDefaults = useCallback(() => { setValue(defaultValue); setErrorText(undefined); setEditMode(false); }, [defaultValue]); const onConfirmHandler = useCallback(() => __awaiter(this, void 0, void 0, function* () { if (validate) { const validation = validate(value); let isValid; if (validation instanceof Promise) { isValid = yield validation; } else { isValid = validation; } if (typeof isValid === 'string') { setErrorText(isValid); return; } } onConfirm === null || onConfirm === void 0 ? void 0 : onConfirm(value); resetToDefaults(); }), [onConfirm, validate, value, resetToDefaults]); const onCancelHandler = useCallback(() => { onCancel === null || onCancel === void 0 ? void 0 : onCancel(); resetToDefaults(); }, [onCancel, resetToDefaults]); const inputID = useMemo(() => { return inputProps.id || randomId(12); }, [inputProps.id]); return (_jsxs(Component, Object.assign({}, focusWithinProps, { className: classNames(`n-${variant} ndl-inline-edit`, className, { 'ndl-disabled': isDisabled, }), style: style, ref: ref }, htmlAttributes, { children: [hasLabel && _jsx("label", { htmlFor: inputID, children: label }), (isEditing !== null && isEditing !== void 0 ? isEditing : hasEditMode) ? (_jsxs("div", { className: classNames('ndl-inline-edit-container', { 'ndl-inline-edit-error': hasError, }), children: [_jsx("input", Object.assign({ // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus: true, value: value, onChange: (e) => setValue(e.target.value), id: inputID }, inputProps, { // Enforce line-height to be the same as the typography's line-height className: classNames(`n-${variant}`, inputProps.className), // Custom padding for avoiding icon overlap style: Object.assign({ paddingRight: hasError ? `${parseInt(getErrorIconSize(variant)) + 4}px` : undefined }, inputProps.style), onKeyDown: (e) => { if (['Enter', 'Escape'].includes(e.key)) { e.key === 'Enter' && onConfirmHandler(); e.key === 'Escape' && onCancelHandler(); e.preventDefault(); e.stopPropagation(); } } })), hasError && (_jsxs(Tooltip, { type: "simple", children: [_jsx(Tooltip.Trigger, { children: _jsx(ExclamationCircleIconSolid, { "data-testid": "ndl-inline-edit-error-icon", "aria-label": "Error message", style: { width: `${getErrorIconSize(variant)}px`, height: `${getErrorIconSize(variant)}px`, }, className: "ndl-inline-edit-error-icon" }) }), _jsx(Tooltip.Content, { children: internalErrorText })] })), _jsxs("div", { className: "ndl-inline-edit-buttons", children: [_jsx(IconButton, { "data-testid": "ndl-confirm-button", ariaLabel: "Accept input's value change", onClick: onConfirmHandler, size: "small", isFloating: true, htmlAttributes: { 'data-testid': 'ndl-confirm-button', }, children: _jsx(CheckIconOutline, {}) }), _jsx(IconButton, { "data-testid": "ndl-cancel-button", ariaLabel: "Ignore input's value change", onClick: onCancelHandler, size: "small", isFloating: true, htmlAttributes: { 'data-testid': 'ndl-cancel-button', }, children: _jsx(XMarkIconOutline, {}) })] })] })) : (_jsxs("button", { className: classNames('ndl-inline-idle-container', { 'ndl-disabled': isDisabled, 'n-text-palette-neutral-text-weaker': !defaultValue && placeholder, }), style: { minHeight: getMinHeight(variant), }, onClick: () => !isDisabled && setEditMode(!hasEditMode), children: [_jsx(Typography, Object.assign({}, typographyProps, { variant: variant, children: defaultValue || placeholder })), hasEditIcon && (_jsx(PencilIconOutline, { style: { width: `${iconSize}px`, height: `${iconSize}px`, flexShrink: 0, } }))] }))] }), defaultValue)); }); //# sourceMappingURL=InlineEdit.js.map