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 :  - [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 : 
270 lines
7.9 KiB
TypeScript
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>
|
|
);
|
|
};
|