Files
twenty/packages/twenty-front/src/modules/ui/field/input/components/AddressInput.tsx
Lucas Bordeau a9cb20f317 Refactor and fixes dropdown bugs (#8807)
Fixes https://github.com/twentyhq/twenty/issues/8788
Fixes https://github.com/twentyhq/twenty/issues/8793
Fixes https://github.com/twentyhq/twenty/issues/8791
Fixes https://github.com/twentyhq/twenty/issues/8890
Fixes https://github.com/twentyhq/twenty/issues/8893

- [x] Also : 

Icon buttons under dropdown are visible without blur : 

![Capture d’écran du 2024-11-29
15-09-53](https://github.com/user-attachments/assets/f563333d-4e43-4ded-acc7-62e116004ed9)

- [x] Also : 

<img width="237" alt="image"
src="https://github.com/user-attachments/assets/e4c70936-beff-4481-89cb-0a32a36e0ee2">

- [x] Also : 

<img width="335" alt="image"
src="https://github.com/user-attachments/assets/5be60395-6baf-49eb-8d40-197add049e20">

- [x] Also : 

<img width="287" alt="image"
src="https://github.com/user-attachments/assets/a317561f-7986-4d70-a1c0-deee4f4e268a">

- Button create new without padding
- Container is expanding

- [x] Also : 

<img width="303" alt="image"
src="https://github.com/user-attachments/assets/09f8a27f-91db-4191-acdc-aaaeedaf6da5">

- [x] Also : 

<img width="133" alt="image"
src="https://github.com/user-attachments/assets/fe17b32e-f7a4-46c4-8040-239eaf8198e8">

Font is cut at bottom ?

- [x] Also : 

<img width="385" alt="image"
src="https://github.com/user-attachments/assets/7bab2092-2936-4112-a2ee-d32d6737e304">

The component should flip and not resize in this situation

- [x] Also : 

<img width="244" alt="image"
src="https://github.com/user-attachments/assets/5384f49a-71f9-4638-a60c-158cc8c83f81">

- [x] Also : 


![image](https://github.com/user-attachments/assets/9cd1f43a-df59-401e-9a41-bdb8e93ebe58)
2024-12-06 14:27:48 +00:00

270 lines
7.9 KiB
TypeScript

import styled from '@emotion/styled';
import { RefObject, useEffect, useRef, useState } from 'react';
import { Key } from 'ts-key-enum';
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { FieldAddressValue } from '@/object-record/record-field/types/FieldMetadata';
import { CountrySelect } from '@/ui/input/components/internal/country/components/CountrySelect';
import { SELECT_COUNTRY_DROPDOWN_ID } from '@/ui/input/components/internal/country/constants/SelectCountryDropdownId';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRecoilValue } from 'recoil';
import { isDefined, MOBILE_VIEWPORT } from 'twenty-ui';
const StyledAddressContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
padding: 4px 8px;
width: 344px;
> div {
margin-bottom: 6px;
}
input {
background-color: ${({ theme }) => theme.background.transparent.secondary};
backdrop-filter: ${({ theme }) => theme.blur.medium};
}
@media (max-width: ${MOBILE_VIEWPORT}px) {
width: auto;
min-width: 100px;
max-width: 200px;
overflow: hidden;
> div {
margin-bottom: 8px;
}
}
`;
const StyledHalfRowContainer = styled.div`
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
@media (max-width: ${MOBILE_VIEWPORT}px) {
display: block;
> div {
margin-bottom: 7px;
}
}
`;
export type AddressInputProps = {
value: FieldAddressValue;
onTab: (newAddress: FieldAddressDraftValue) => void;
onShiftTab: (newAddress: FieldAddressDraftValue) => void;
onEnter: (newAddress: FieldAddressDraftValue) => void;
onEscape: (newAddress: FieldAddressDraftValue) => void;
onClickOutside: (
event: MouseEvent | TouchEvent,
newAddress: FieldAddressDraftValue,
) => void;
hotkeyScope: string;
clearable?: boolean;
onChange?: (updatedValue: FieldAddressDraftValue) => void;
};
export const AddressInput = ({
value,
hotkeyScope,
onTab,
onShiftTab,
onEnter,
onEscape,
onClickOutside,
onChange,
}: AddressInputProps) => {
const [internalValue, setInternalValue] = useState(value);
const addressStreet1InputRef = useRef<HTMLInputElement>(null);
const addressStreet2InputRef = useRef<HTMLInputElement>(null);
const addressCityInputRef = useRef<HTMLInputElement>(null);
const addressStateInputRef = useRef<HTMLInputElement>(null);
const addressPostCodeInputRef = useRef<HTMLInputElement>(null);
const inputRefs: {
[key in keyof FieldAddressDraftValue]?: RefObject<HTMLInputElement>;
} = {
addressStreet1: addressStreet1InputRef,
addressStreet2: addressStreet2InputRef,
addressCity: addressCityInputRef,
addressState: addressStateInputRef,
addressPostcode: addressPostCodeInputRef,
};
const [focusPosition, setFocusPosition] =
useState<keyof FieldAddressDraftValue>('addressStreet1');
const { closeDropdown: closeCountryDropdown } = useDropdown(
SELECT_COUNTRY_DROPDOWN_ID,
);
const wrapperRef = useRef<HTMLDivElement>(null);
const getChangeHandler =
(field: keyof FieldAddressDraftValue) => (updatedAddressPart: string) => {
const updatedAddress = { ...value, [field]: updatedAddressPart };
setInternalValue(updatedAddress);
onChange?.(updatedAddress);
};
const getFocusHandler = (fieldName: keyof FieldAddressDraftValue) => () => {
setFocusPosition(fieldName);
inputRefs[fieldName]?.current?.focus();
};
useScopedHotkeys(
'tab',
() => {
const currentFocusPosition = Object.keys(inputRefs).findIndex(
(key) => key === focusPosition,
);
const maxFocusPosition = Object.keys(inputRefs).length - 1;
const nextFocusPosition = currentFocusPosition + 1;
const isFocusPositionAfterLast = nextFocusPosition > maxFocusPosition;
if (isFocusPositionAfterLast) {
onTab?.(internalValue);
} else {
const nextFocusFieldName = Object.keys(inputRefs)[
nextFocusPosition
] as keyof FieldAddressDraftValue;
setFocusPosition(nextFocusFieldName);
inputRefs[nextFocusFieldName]?.current?.focus();
}
},
hotkeyScope,
[onTab, internalValue, focusPosition],
);
useScopedHotkeys(
'shift+tab',
() => {
const currentFocusPosition = Object.keys(inputRefs).findIndex(
(key) => key === focusPosition,
);
const nextFocusPosition = currentFocusPosition - 1;
const isFocusPositionBeforeFirst = nextFocusPosition < 0;
if (isFocusPositionBeforeFirst) {
onShiftTab?.(internalValue);
} else {
const nextFocusFieldName = Object.keys(inputRefs)[
nextFocusPosition
] as keyof FieldAddressDraftValue;
setFocusPosition(nextFocusFieldName);
inputRefs[nextFocusFieldName]?.current?.focus();
}
},
hotkeyScope,
[onTab, internalValue, focusPosition],
);
useScopedHotkeys(
Key.Enter,
() => {
onEnter(internalValue);
},
hotkeyScope,
[onEnter, internalValue],
);
useScopedHotkeys(
[Key.Escape],
() => {
onEscape(internalValue);
},
hotkeyScope,
[onEscape, internalValue],
);
const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState);
useListenClickOutside({
refs: [wrapperRef],
callback: (event) => {
if (activeDropdownFocusId === SELECT_COUNTRY_DROPDOWN_ID) {
return;
}
event.stopImmediatePropagation();
closeCountryDropdown();
onClickOutside?.(event, internalValue);
},
enabled: isDefined(onClickOutside),
listenerId: 'address-input',
});
useEffect(() => {
setInternalValue(value);
}, [value]);
return (
<StyledAddressContainer ref={wrapperRef}>
<TextInputV2
autoFocus
value={internalValue.addressStreet1 ?? ''}
ref={inputRefs['addressStreet1']}
label="ADDRESS 1"
fullWidth
onChange={getChangeHandler('addressStreet1')}
onFocus={getFocusHandler('addressStreet1')}
/>
<TextInputV2
value={internalValue.addressStreet2 ?? ''}
ref={inputRefs['addressStreet2']}
label="ADDRESS 2"
fullWidth
onChange={getChangeHandler('addressStreet2')}
onFocus={getFocusHandler('addressStreet2')}
/>
<StyledHalfRowContainer>
<TextInputV2
value={internalValue.addressCity ?? ''}
ref={inputRefs['addressCity']}
label="CITY"
fullWidth
onChange={getChangeHandler('addressCity')}
onFocus={getFocusHandler('addressCity')}
/>
<TextInputV2
value={internalValue.addressState ?? ''}
ref={inputRefs['addressState']}
label="STATE"
fullWidth
onChange={getChangeHandler('addressState')}
onFocus={getFocusHandler('addressState')}
/>
</StyledHalfRowContainer>
<StyledHalfRowContainer>
<TextInputV2
value={internalValue.addressPostcode ?? ''}
ref={inputRefs['addressPostcode']}
label="POST CODE"
fullWidth
onChange={getChangeHandler('addressPostcode')}
onFocus={getFocusHandler('addressPostcode')}
/>
<CountrySelect
onChange={getChangeHandler('addressCountry')}
selectedCountryName={internalValue.addressCountry ?? ''}
/>
</StyledHalfRowContainer>
</StyledAddressContainer>
);
};