1 import type { ReactNode } from 'react';
2 import { type KeyboardEvent, useRef } from 'react';
4 import { FieldArray, type FormikContextType, type FormikErrors } from 'formik';
5 import { c } from 'ttag';
7 import { Button } from '@proton/atoms';
8 import { Icon, InputFieldTwo } from '@proton/components/';
9 import { maybeErrorMessage } from '@proton/pass/hooks/useFieldControl';
10 import type { UrlGroupValues, UrlItem } from '@proton/pass/types';
11 import { isEmptyString } from '@proton/pass/utils/string/is-empty-string';
12 import { uniqueId } from '@proton/pass/utils/string/unique-id';
13 import { sanitizeURL } from '@proton/pass/utils/url/sanitize';
15 import { FieldBox } from './Layout/FieldBox';
17 export type UrlGroupProps<V extends UrlGroupValues = UrlGroupValues> = {
18 form: FormikContextType<V>;
19 renderExtraActions?: (helpers: {
20 handleRemove: (idx: number) => () => void;
21 handleAdd: (url: string) => void;
22 handleReplace: (idx: number) => (url: string) => void;
26 export const createNewUrl = (url: string) => ({ id: uniqueId(), url: sanitizeURL(url).valid ? url : '' });
28 export const UrlGroupField = <T extends UrlGroupValues>({ form, renderExtraActions }: UrlGroupProps<T>) => {
29 const inputRef = useRef<HTMLInputElement>(null);
30 const { values, errors, handleChange } = form;
32 const onKeyEnter = (event: KeyboardEvent<HTMLInputElement>) => {
33 if (event.key === 'Enter') {
34 event.preventDefault(); /* avoid submitting the form */
35 event.currentTarget.blur();
39 const hasURL = Boolean(values.url) || values.urls.some(({ url }) => !isEmptyString(url));
42 <FieldBox icon="earth">
44 htmlFor="next-url-field"
45 className="field-two-label text-sm"
46 style={{ color: hasURL ? 'var(--text-weak)' : 'inherit' }}
48 {c('Label').t`Websites`}
53 render={(helpers) => {
54 const handleRemove = helpers.handleRemove;
56 const handleReplace = (index: number) => (url: string) =>
57 helpers.replace(index, { id: values.urls[index].id, url });
59 const handleAdd = (url: string) => {
60 helpers.push(createNewUrl(sanitizeURL(url).url));
61 return form.setFieldValue('url', '');
66 <ul className="unstyled m-0 mb-1">
67 {values.urls.map(({ url, id }, index) => (
68 <li key={id} className="flex items-center flex-nowrap">
70 error={(errors.urls?.[index] as FormikErrors<UrlItem>)?.url}
71 onValue={handleReplace(index)}
72 onBlur={() => handleReplace(index)(sanitizeURL(url).url)}
75 assistContainerClassName="empty:hidden"
76 inputClassName="color-norm p-0 rounded-none"
77 placeholder="https://"
78 onKeyDown={onKeyEnter}
83 className="shrink-0 ml-2"
85 onClick={handleRemove(index)}
88 title={c('Action').t`Delete`}
90 <Icon name="cross" size={5} className="color-weak" />
99 assistContainerClassName="empty:hidden"
100 inputClassName="color-norm p-0 rounded-none"
101 placeholder="https://"
104 error={maybeErrorMessage(errors.url)}
105 onChange={handleChange}
106 onBlur={() => values.url && !errors.url && handleAdd(values.url)}
107 onKeyDown={onKeyEnter}
111 <hr className="mt-3 mb-1" />
113 {renderExtraActions?.({ handleAdd, handleRemove, handleReplace })}
120 title={c('Action').t`Add`}
121 className="flex items-center gap-1"
122 onClick={() => handleAdd(values.url).then(() => inputRef.current?.focus())}
124 <Icon name="plus" /> {c('Action').t`Add`}