/** * Copyright (c) 2020-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { useEffect, useState } from 'react'; import { MdVerticalAlignBottom, MdAdd, MdEdit } from 'react-icons/md'; import clsx from 'clsx'; import { useResizeDetector } from 'react-resize-detector'; import { BlankPanelContent } from './BlankPanelContent'; const DEFAULT_CONTENT_PADDING = 20; const MIN_DIMENSION = 50; const TEXT_GRAPHIC_SPACING = 20; type ClickActionType = 'add' | 'modify'; const renderClickActionIcon = ( type: ClickActionType | undefined, ): React.ReactElement | null => !type ? null : type === 'add' ? : ; /** * This component is used as placeholder for empty panel, which represents an unset/empty value of something. * If this value is of multiple values (i.e. a an empty list), the most common operation to interact * with a list is `add`, otherwise use `modify`. * * When the the value is editable, supposed this is a list value, the placeholder can be interacted with in 2 ways: * 1. clicking on this placeholder to add a new item, * 2. (optional) or, dropping some material to create a new item and add it to the list * * When the value is read-only, show a placeholder to let user know the value is empty/blank/unset */ export const BlankPanelPlaceholder: React.FC<{ /** * Placeholder text is similar to what we would put in text input, e.g. `Choose a file` * This should be in `Sentence case` and should have ellipses when it can potentially require more user actions */ placeholderText: string; /** * Possible values are `add` for list-type value and `modify` for single value */ clickActionType?: ClickActionType; onClick?: () => void; /** * The tooltip text for the add/drop zone */ tooltipText: string; dndProps?: { canDrop: boolean; isDragOver: boolean; }; readOnlyProps?: { placeholderText: string; }; }> = (props) => { const { placeholderText, clickActionType, onClick, tooltipText, dndProps, readOnlyProps, } = props; // if no action is provided, it means the panel support DnD const clickActionIcon = !onClick ? ( ) : ( renderClickActionIcon(clickActionType) ); const { ref: textRef, width: textWidth, height: textHeight, } = useResizeDetector({ refreshMode: 'throttle', refreshRate: 50, refreshOptions: { trailing: true }, }); const { ref: graphicRef, width: graphicWidth, height: graphicHeight, } = useResizeDetector({ refreshMode: 'throttle', refreshRate: 50, refreshOptions: { trailing: true }, }); /** * NOTE: usually, we would want to set initial states for these to `false` and as the dimensions are detected, * we will set them to `true` if allowed. The problem is if they start out to be `false`, which for this component * means `display: none`, dimension detector will always show their width and height as 0. As such, we want to initialize * these to `true` so that the detector can detect their size. This, in turn, causes a flash if these flag are set to 0 * after container size detector call `handleResize` for the first time. So to make this smooth, we introduce a mask * which initially makes everything invisible then gradually fade in `stylishly` and reveal all after the first `handleResize` * is called */ const [showText, setShowText] = useState(true); const [showGraphic, setShowGraphic] = useState(true); const [fullGraphic, setFullGraphic] = useState(true); const [showPlaceholder, setShowPlaceholder] = useState(false); const { ref: containerRef, width: containerWidth, height: containerHeight, } = useResizeDetector({ refreshMode: 'throttle', refreshRate: 50, refreshOptions: { trailing: true }, }); useEffect(() => { const _containerWidth = containerWidth ?? 0; const _containerHeight = containerHeight ?? 0; const _textWidth = textWidth ?? 0; const _textHeight = textHeight ?? 0; const _graphicWidth = graphicWidth ?? 0; const _graphicHeight = graphicHeight ?? 0; setShowText( _containerWidth > Math.max(_textWidth, _graphicWidth) + DEFAULT_CONTENT_PADDING * 2 && _containerHeight > _textHeight + TEXT_GRAPHIC_SPACING + _graphicHeight + DEFAULT_CONTENT_PADDING * 2, ); setFullGraphic( _containerWidth > _graphicWidth + DEFAULT_CONTENT_PADDING * 2 && _containerHeight > _graphicHeight + DEFAULT_CONTENT_PADDING * 2, ); setShowGraphic( _containerWidth > MIN_DIMENSION && _containerHeight > MIN_DIMENSION, ); setShowPlaceholder(true); // reveal everything by resetting opacity }, [ containerWidth, containerHeight, textWidth, textHeight, graphicWidth, graphicHeight, ]); if (readOnlyProps) { return ( {readOnlyProps.placeholderText} ); } return (
{dndProps && (
)}
{placeholderText}
{showText &&
}
{dndProps && ( <>
{clickActionIcon}
)} {!dndProps && clickActionIcon}
); };