Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / pass / components / Form / Field / ListFieldItem.tsx
blob641373d6a10889c1f86ef61d59908b7e6340736a
1 /* eslint-disable jsx-a11y/no-static-element-interactions */
2 import type { KeyboardEventHandler } from 'react';
3 import { useRef, useState } from 'react';
5 import { CircleLoader } from '@proton/atoms';
6 import clsx from '@proton/utils/clsx';
8 import type { ListFieldValue } from './ListField';
10 export type ListFieldItemProps<T> = ListFieldValue<T> & {
11     error?: boolean;
12     loading: boolean;
13     onChange: (value: string) => void;
14     onMoveLeft: () => void;
15     onMoveRight: () => void;
16     renderValue: (value: T) => string;
19 const getCaretPosition = (el: HTMLElement): number => {
20     const selection = getSelection();
22     if (selection) {
23         const range = selection.getRangeAt(0);
24         const clonedRange = range.cloneRange();
25         clonedRange.selectNodeContents(el);
26         clonedRange.setEnd(range.endContainer, range.endOffset);
28         return clonedRange.toString().length;
29     }
31     return 0;
34 export const ListFieldItem = <T,>({
35     id,
36     error,
37     loading,
38     value,
39     onChange,
40     onMoveLeft,
41     onMoveRight,
42     renderValue,
43 }: ListFieldItemProps<T>) => {
44     const ref = useRef<HTMLSpanElement>(null);
45     const [editing, setEditing] = useState(false);
47     const handleBlur = () => {
48         setEditing(false);
49         const update = ref.current?.innerText?.trim() ?? '';
50         if (update !== value) onChange(update);
51     };
53     const handleKeyDown: KeyboardEventHandler<HTMLSpanElement> = (evt) => {
54         if (!ref.current) return;
56         const el = ref.current;
57         const { innerText = '' } = el;
59         switch (evt.key) {
60             case 'Enter':
61                 evt.preventDefault();
62                 onMoveRight();
63                 break;
65             case 'ArrowLeft':
66             case 'Backspace':
67                 /** trigger move left if we've reached the start
68                  * of the contenteditable's inner text */
69                 if (getCaretPosition(el) === 0) {
70                     evt.preventDefault();
71                     onMoveLeft();
72                 }
73                 break;
75             case 'ArrowRight':
76                 /** trigger move right if we've reached the end
77                  * of the contenteditable's inner text */
78                 if (getCaretPosition(el) >= (innerText.length ?? 0)) {
79                     evt.preventDefault();
80                     onMoveRight();
81                 }
82                 break;
83         }
84     };
86     return (
87         <li
88             className={clsx(
89                 'pass-field-text-group--item flex flex-nowrap flex-row max-w-full overflow-hidden stop-propagation border rounded',
90                 error && 'pass-field-text-group--item:error',
91                 loading && 'pass-field-text-group--item:loading'
92             )}
93         >
94             <button
95                 className="pill-remove inline-flex flex-nowrap items-center px-2 py-1 max-w-full gap-2"
96                 type="button"
97                 tabIndex={-1}
98             >
99                 <span
100                     id={id}
101                     onBlur={handleBlur}
102                     onFocus={() => setEditing(true)}
103                     onClick={(e) => e.stopPropagation()}
104                     contentEditable
105                     onKeyDown={handleKeyDown}
106                     ref={ref}
107                     spellCheck={false}
108                     className={clsx(!editing && 'text-ellipsis')}
109                     suppressContentEditableWarning
110                 >
111                     {renderValue(value)}
112                 </span>
113                 {loading && <CircleLoader size="small" className="shrink-0" />}
114             </button>
115         </li>
116     );