1 import type { ComponentType, ElementType, ReactElement } from 'react';
2 import { type ReactNode, useMemo, useState } from 'react';
4 import { c } from 'ttag';
6 import { Button } from '@proton/atoms';
7 import { Icon, type IconName } from '@proton/components';
8 import clsx from '@proton/utils/clsx';
10 import { FieldBox, type FieldBoxProps } from '../Layout/FieldBox';
11 import type { ClickToCopyProps } from './ClickToCopy';
12 import { ClickToCopy } from './ClickToCopy';
14 import './ValueControl.scss';
16 const isIntrinsicElement = <E extends ElementType>(c: E) => typeof c === 'string' || typeof c === 'symbol';
18 export type ValueControlProps<E extends ElementType> = Omit<FieldBoxProps, 'icon'> & {
20 children?: E extends ComponentType<infer U> ? (U extends { children?: infer C } ? C : never) : ReactNode;
22 clickToCopy?: boolean;
23 clipboardValue?: string;
30 icon?: IconName | ReactElement;
34 valueClassName?: string;
37 const HideButton = ({ hidden, onClick }: { hidden: boolean; onClick: () => void }) => (
45 title={hidden ? c('Action').t`Show` : c('Action').t`Hide`}
47 <Icon size={5} name={hidden ? 'eye' : 'eye-slash'} />
51 /* When passed both children and a value prop:
52 * children will be rendered, value will be passed
54 export const ValueControl = <E extends ElementType = 'div'>({
56 actionsContainerClassName,
74 }: ValueControlProps<E>) => {
75 /* we're leveraging type-safety at the consumer level - we're recasting
76 * the `as` prop as a generic `ElementType` to avoid switching over all
77 * possible sub-types. Trade-off is being extra careful with the children
78 * the `ValueContainer` can accept */
79 const ValueContainer = (as ?? 'div') as ElementType;
80 const intrinsicEl = isIntrinsicElement(ValueContainer);
82 const [hide, setHide] = useState(hidden);
83 const defaultHiddenValue = '••••••••••••';
85 const displayValue = useMemo(() => {
86 /* intrinsinc elements support nesting custom DOM structure */
87 if (intrinsicEl && loading) return <div className="pass-skeleton pass-skeleton--value" />;
88 if (intrinsicEl && !value && !children) return <div className="color-weak">{c('Info').t`None`}</div>;
90 if (hide) return hiddenValue ?? defaultHiddenValue;
92 /* if children are passed: display them - when working with
93 * a `ValueContainer` component, we leverage the inherited prop
95 if (children) return children;
97 /* if no children provided: fallback to value which is always
98 * a valid "string" ReactNode */
100 }, [value, children, loading, hide, intrinsicEl]);
102 const canCopy = clickToCopy && value;
103 const interactive = (canCopy || onClick) && !disabled;
104 const MaybeClickToCopy: ElementType<ClickToCopyProps> = canCopy ? ClickToCopy : 'div';
109 'pass-value-control',
110 interactive && 'pass-value-control--interactive cursor-pointer',
111 !loading && error && 'border-danger',
112 disabled && 'opacity-50',
115 {...(canCopy ? { value: clipboardValue ?? value } : { onClick: disabled ? undefined : onClick })}
120 ? [<HideButton hidden={hide} onClick={() => setHide((prev) => !prev)} />, actions ?? []].flat()
123 actionsContainerClassName={actionsContainerClassName}
126 <div className="color-weak text-sm">{label}</div>
129 key={`${hide ? 'hidden' : 'visible'}-container`}
131 'pass-value-control--value m-0 p-0',
132 !disabled && 'cursor-pointer',
133 ellipsis && 'text-ellipsis',
134 hide && 'text-nowrap overflow-hidden',
137 children={displayValue}