import React, { useMemo } from 'react'; import type { Element as HadronElementType } from 'hadron-document'; import type { TypeCastMap } from 'hadron-type-checker'; import TypeChecker from 'hadron-type-checker'; import { css, cx } from '@leafygreen-ui/emotion'; import { palette } from '@leafygreen-ui/palette'; import { spacing } from '@leafygreen-ui/tokens'; import BSONValue, { BSONValueContainer } from '../bson-value'; import { Tooltip } from '../tooltip'; import { mergeProps } from '../../utils/merge-props'; import { documentTypography } from './typography'; import { Icon } from '../leafygreen'; import { useDarkMode } from '../../hooks/use-theme'; const maxWidth = css({ maxWidth: '100%', overflowX: 'hidden', textOverflow: 'ellipsis', }); const editorReset = css({ padding: 0, margin: 0, border: 'none', boxShadow: 'none', outline: 'none', backgroundColor: 'transparent', maxWidth: '100%', }); const editorOutline = css({ '&:focus, &:active': { borderRadius: `2px`, boxShadow: `0 0 0 2px ${palette.blue.light1}`, }, }); const editorInvalid = css({ '&:focus, &:active': { boxShadow: `0 0 0 2px ${palette.red.dark2}`, }, }); const editorInvalidLightMode = css({ backgroundColor: palette.red.light2, color: palette.red.dark2, }); const editorInvalidDarkMode = css({ backgroundColor: palette.red.dark2, color: palette.red.light2, }); export const KeyEditor: React.FunctionComponent<{ editing?: boolean; onEditStart(): void; value: string; valid: boolean; validationMessage: string | null; onChange(newVal: string): void; autoFocus?: boolean; }> = ({ editing, value, valid, validationMessage, onChange, autoFocus, onEditStart, }) => { const darkMode = useDarkMode(); const width = `${Math.max(value.length, 1)}ch`; return ( <> {editing ? ( ) => { return (
{ onChange(evt.currentTarget.value); }} // See ./element.tsx // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus={autoFocus} className={cx( maxWidth, editorReset, editorOutline, !valid && editorInvalid, !valid && (darkMode ? editorInvalidDarkMode : editorInvalidLightMode) )} style={{ width }} spellCheck="false" {...triggerProps} > {children}
); }} > {validationMessage}
) : ( // Double-click is not accessible so no reason for this to be a button
{value}
)} ); }; const textareaContainer = css({ width: '100%', maxWidth: '100%', '&::before, &::after': { content: "'\"'", userSelect: 'none', }, }); const editorTextarea = css({ display: 'inline-block', whiteSpace: 'nowrap', minWidth: '5ch', // 2ch for `"` around the textarea maxWidth: 'calc(100% - 2ch)', verticalAlign: 'top', color: 'inherit', }); export const ValueEditor: React.FunctionComponent<{ editing?: boolean; onEditStart(): void; type: string; value: string; valid: boolean; validationMessage: string | null; originalValue: TypeCastMap[keyof TypeCastMap]; autoFocus?: boolean; onChange(newVal: string): void; onFocus(): void; onBlur(): void; }> = ({ editing, onEditStart, type, value, valid, validationMessage, originalValue, autoFocus, onChange, onFocus, onBlur, }) => { const val = String(value); const inputStyle = useMemo(() => { if (type === 'String') { const lines = val.split('\n'); const longestLineCharLength = Math.max( ...lines.map((line) => line.length) ); const width = `${Math.min( // Adding one to account for a textarea resize icon button thingie longestLineCharLength + 1, 70 )}ch`; const minLines = Math.max( lines.length, longestLineCharLength > 70 ? 2 : 1 ); const maxLines = Math.min(minLines, 10); const minHeight = documentTypography.lineHeight * Math.min(minLines, maxLines); const height = documentTypography.lineHeight * maxLines; return { width, minHeight, height }; } return { width: `${Math.max(val.length, 1)}ch` }; }, [val, type]); return ( <> {editing ? ( ) => { // NB: Order is important, if triggerProps has onFocus / onBlur we // want to merge them with ours, if they are not passed, we want our // listeners to overwrite undefined keys const mergedProps = mergeProps(triggerProps, { onBlur, onFocus }); return (
{type === 'String' ? ( ) : ( { onChange(evt.currentTarget.value); }} // See ./element.tsx // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus={autoFocus} className={cx( editorReset, editorOutline, !valid && editorInvalid )} style={inputStyle} spellCheck="false" {...(mergedProps as React.HTMLProps)} > )} {children}
); }} > {validationMessage}
) : ( // Double-click is not accessible so no reason for this to be a button, // users won't be able to interact with it anyway
)} ); }; const TYPES = TypeChecker.castableTypes(true); const longestTypeNameCharLength = Math.max(...TYPES.map((type) => type.length)); const typeEditor = css({ color: palette.gray.base, appearance: 'none', // Accounting for the margin that `appearance: auto` will add to the shadow // dom inside select node paddingLeft: spacing[1], width: `calc(${longestTypeNameCharLength}ch + ${spacing[4]}px)`, '&:hover': { color: 'inherit', cursor: 'pointer', }, }); const typeEditorActive = css({ appearance: 'auto', paddingLeft: 0, }); const typeEditorChevron = css({ position: 'absolute', right: 4, top: 2, pointerEvents: 'none', display: 'none', }); const typeEditorOptionLight = css({ backgroundColor: palette.white, }); const typeEditorOptionDark = css({ backgroundColor: palette.black, }); const typeEditorContainer = css({ [`&:hover .${typeEditorChevron}`]: { display: 'block' }, position: 'relative', cursor: 'pointer', }); export const TypeEditor: React.FunctionComponent<{ editing?: boolean; autoFocus?: boolean; type: HadronElementType['type']; onChange(newVal: HadronElementType['type']): void; visuallyActive?: boolean; }> = ({ editing, autoFocus, type, onChange, visuallyActive }) => { const darkMode = useDarkMode(); return ( <> {editing && (
{/* This rule is deprecated https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-onchange.md#deprecated-no-onchange */} {/* eslint-disable-next-line jsx-a11y/no-onchange */}
)} ); };