@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
178 lines • 9 kB
JavaScript
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