Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / pass / components / Form / Field / Control / ValueControl.tsx
blob4a6e0ec383fb635e8c9a3a041cb9411e81d83061
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'> & {
19     as?: E;
20     children?: E extends ComponentType<infer U> ? (U extends { children?: infer C } ? C : never) : ReactNode;
21     className?: string;
22     clickToCopy?: boolean;
23     clipboardValue?: string;
24     disabled?: boolean;
25     error?: boolean;
26     ellipsis?: boolean;
27     extra?: ReactNode;
28     hidden?: boolean;
29     hiddenValue?: string;
30     icon?: IconName | ReactElement;
31     label: ReactNode;
32     loading?: boolean;
33     value?: string;
34     valueClassName?: string;
37 const HideButton = ({ hidden, onClick }: { hidden: boolean; onClick: () => void }) => (
38     <Button
39         icon
40         pill
41         color="norm"
42         onClick={onClick}
43         size="medium"
44         shape="ghost"
45         title={hidden ? c('Action').t`Show` : c('Action').t`Hide`}
46     >
47         <Icon size={5} name={hidden ? 'eye' : 'eye-slash'} />
48     </Button>
51 /* When passed both children and a value prop:
52  * children will be rendered, value will be passed
53  * to ClickToCopy */
54 export const ValueControl = <E extends ElementType = 'div'>({
55     actions,
56     actionsContainerClassName,
57     as,
58     children,
59     className,
60     clickToCopy = false,
61     clipboardValue,
62     disabled = false,
63     ellipsis = true,
64     error = false,
65     extra,
66     hidden = false,
67     hiddenValue,
68     icon,
69     label,
70     loading = false,
71     onClick,
72     value,
73     valueClassName,
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
94          * type-safety */
95         if (children) return children;
97         /* if no children provided: fallback to value which is always
98          * a valid "string" ReactNode */
99         return value ?? '';
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';
106     return (
107         <MaybeClickToCopy
108             className={clsx(
109                 'pass-value-control',
110                 interactive && 'pass-value-control--interactive cursor-pointer',
111                 !loading && error && 'border-danger',
112                 disabled && 'opacity-50',
113                 className
114             )}
115             {...(canCopy ? { value: clipboardValue ?? value } : { onClick: disabled ? undefined : onClick })}
116         >
117             <FieldBox
118                 actions={
119                     hidden && value
120                         ? [<HideButton hidden={hide} onClick={() => setHide((prev) => !prev)} />, actions ?? []].flat()
121                         : actions
122                 }
123                 actionsContainerClassName={actionsContainerClassName}
124                 icon={icon}
125             >
126                 <div className="color-weak text-sm">{label}</div>
128                 <ValueContainer
129                     key={`${hide ? 'hidden' : 'visible'}-container`}
130                     className={clsx(
131                         'pass-value-control--value m-0 p-0',
132                         !disabled && 'cursor-pointer',
133                         ellipsis && 'text-ellipsis',
134                         hide && 'text-nowrap overflow-hidden',
135                         valueClassName
136                     )}
137                     children={displayValue}
138                 />
140                 {extra}
141             </FieldBox>
142         </MaybeClickToCopy>
143     );