Proposal Date picker overflow (#4996)

Unfortunately, it is not possible in CSS to have an overflow:visible
over x-axis while having an overflow:hidden over y-axis, leading to the
following issue:

<img width="1512" alt="image"
src="https://github.com/twentyhq/twenty/assets/12035771/9b84cbbb-c6c4-4fd6-a630-a24f01eccf73">

I'm refactoring the RecordInlineCell and RecordTableCell to use
useFloating + createPortal to open the cell.
This commit is contained in:
Charles Bochet
2024-04-17 11:35:45 +02:00
committed by GitHub
parent 340af9a244
commit 67db7d85c0
14 changed files with 154 additions and 161 deletions

View File

@ -1,7 +1,6 @@
import { useAddressField } from '@/object-record/record-field/meta-types/hooks/useAddressField';
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { AddressInput } from '@/ui/field/input/components/AddressInput';
import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay';
import { usePersistField } from '../../../hooks/usePersistField';
@ -69,17 +68,15 @@ export const AddressFieldInput = ({
};
return (
<FieldInputOverlay>
<AddressInput
value={convertToAddress(draftValue)}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
hotkeyScope={hotkeyScope}
onChange={handleChange}
onTab={handleTab}
onShiftTab={handleShiftTab}
/>
</FieldInputOverlay>
<AddressInput
value={convertToAddress(draftValue)}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
hotkeyScope={hotkeyScope}
onChange={handleChange}
onTab={handleTab}
onShiftTab={handleShiftTab}
/>
);
};

View File

@ -72,7 +72,6 @@ const StyledInlineCellBaseContainer = styled.div`
gap: ${({ theme }) => theme.spacing(1)};
position: relative;
user-select: none;
`;

View File

@ -1,4 +1,6 @@
import { createPortal } from 'react-dom';
import styled from '@emotion/styled';
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
const StyledInlineCellEditModeContainer = styled.div<RecordInlineCellEditModeProps>`
align-items: center;
@ -7,7 +9,6 @@ const StyledInlineCellEditModeContainer = styled.div<RecordInlineCellEditModePro
height: 24px;
margin-left: -${({ theme }) => theme.spacing(1)};
position: relative;
z-index: 10;
`;
@ -28,8 +29,24 @@ type RecordInlineCellEditModeProps = {
export const RecordInlineCellEditMode = ({
children,
}: RecordInlineCellEditModeProps) => (
<StyledInlineCellEditModeContainer data-testid="inline-cell-edit-mode-container">
<StyledInlineCellInput>{children}</StyledInlineCellInput>
</StyledInlineCellEditModeContainer>
);
}: RecordInlineCellEditModeProps) => {
const { refs, floatingStyles } = useFloating({
placement: 'right',
middleware: [flip(), offset(-1)],
whileElementsMounted: autoUpdate,
});
return (
<StyledInlineCellEditModeContainer
ref={refs.setReference}
data-testid="inline-cell-edit-mode-container"
>
{createPortal(
<StyledInlineCellInput ref={refs.setFloating} style={floatingStyles}>
{children}
</StyledInlineCellInput>,
document.body,
)}
</StyledInlineCellEditModeContainer>
);
};

View File

@ -7,7 +7,6 @@ interface PropertyBoxProps {
export const StyledPropertyBoxContainer = styled.div`
align-self: stretch;
background: ${({ theme }) => theme.background.secondary};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
flex-direction: column;

View File

@ -1,12 +1,11 @@
import { Key } from 'ts-key-enum';
import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import {
useListenClickOutside,
useListenClickOutsideByClassName,
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
type RecordTableInternalEffectProps = {
recordTableId: string;
@ -22,6 +21,10 @@ export const RecordTableInternalEffect = ({
useMapKeyboardToSoftFocus();
const { useListenClickOutside } = useClickOutsideListener(
SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID,
);
useListenClickOutside({
refs: [tableBodyRef],
callback: () => {

View File

@ -29,10 +29,6 @@ import { useRecordTable } from '../hooks/useRecordTable';
import { RecordTableInternalEffect } from './RecordTableInternalEffect';
const StyledTableWithHeader = styled.div`
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
height: 100%;
`;

View File

@ -0,0 +1,2 @@
export const SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID =
'soft-focus-click-outside-listener-id';

View File

@ -48,8 +48,6 @@ const StyledCellBaseContainer = styled.div`
export type RecordTableCellContainerProps = {
editModeContent: ReactElement;
nonEditModeContent: ReactElement;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
editHotkeyScope?: HotkeyScope;
transparent?: boolean;
maxContentWidth?: number;
@ -62,8 +60,6 @@ const DEFAULT_CELL_SCOPE: HotkeyScope = {
};
export const RecordTableCellContainer = ({
editModeHorizontalAlign = 'left',
editModeVerticalPosition = 'over',
editModeContent,
nonEditModeContent,
editHotkeyScope,
@ -163,12 +159,7 @@ export const RecordTableCellContainer = ({
onMouseLeave={handleContainerMouseLeave}
>
{isCurrentTableCellInEditMode ? (
<RecordTableCellEditMode
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
>
{editModeContent}
</RecordTableCellEditMode>
<RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode>
) : hasSoftFocus ? (
<>
{showButton && (

View File

@ -1,5 +1,7 @@
import { ReactElement } from 'react';
import { createPortal } from 'react-dom';
import styled from '@emotion/styled';
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
const StyledEditableCellEditModeContainer = styled.div<RecordTableCellEditModeProps>`
align-items: center;
@ -7,27 +9,46 @@ const StyledEditableCellEditModeContainer = styled.div<RecordTableCellEditModePr
min-width: 200px;
width: calc(100% + 2px);
z-index: 1;
height: 100%;
`;
const StyledTableCellInput = styled.div`
align-items: center;
display: flex;
min-height: 32px;
min-width: 200px;
z-index: 10;
`;
export type RecordTableCellEditModeProps = {
children: ReactElement;
transparent?: boolean;
maxContentWidth?: number;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
initialValue?: string;
};
export const RecordTableCellEditMode = ({
editModeHorizontalAlign,
editModeVerticalPosition,
children,
}: RecordTableCellEditModeProps) => (
<StyledEditableCellEditModeContainer
data-testid="editable-cell-edit-mode-container"
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
>
{children}
</StyledEditableCellEditModeContainer>
);
}: RecordTableCellEditModeProps) => {
const { refs, floatingStyles } = useFloating({
placement: 'top-start',
middleware: [flip(), offset(-32)],
whileElementsMounted: autoUpdate,
});
return (
<StyledEditableCellEditModeContainer
ref={refs.setReference}
data-testid="editable-cell-edit-mode-container"
>
{createPortal(
<StyledTableCellInput ref={refs.setFloating} style={floatingStyles}>
{children}
</StyledTableCellInput>,
document.body,
)}
</StyledEditableCellEditModeContainer>
);
};

View File

@ -1,8 +1,10 @@
import { useResetRecoilState } from 'recoil';
import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useCloseCurrentTableCellInEditMode } from '../../hooks/internal/useCloseCurrentTableCellInEditMode';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
@ -12,11 +14,16 @@ export const useCloseRecordTableCell = () => {
const { setDragSelectionStartEnabled } = useDragSelect();
const { pendingRecordIdState } = useRecordTableStates();
const { toggleClickOutsideListener } = useClickOutsideListener(
SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID,
);
const closeCurrentTableCellInEditMode = useCloseCurrentTableCellInEditMode();
const resetRecordTablePendingRecordId =
useResetRecoilState(pendingRecordIdState);
const closeTableCell = async () => {
toggleClickOutsideListener(true);
setDragSelectionStartEnabled(true);
closeCurrentTableCellInEditMode();
setHotkeyScope(TableHotkeyScope.TableSoftFocus);

View File

@ -5,12 +5,14 @@ import { useRecoilCallback } from 'recoil';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { isDefined } from '~/utils/isDefined';
import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext';
@ -33,6 +35,9 @@ export const useOpenRecordTableCell = () => {
const navigate = useNavigate();
const leaveTableFocus = useLeaveTableFocus();
const { toggleClickOutsideListener } = useClickOutsideListener(
SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID,
);
const { columnIndex } = useContext(RecordTableCellContext);
const isFirstColumnCell = columnIndex === 0;
@ -60,6 +65,7 @@ export const useOpenRecordTableCell = () => {
setCurrentTableCellInEditMode();
initFieldInputDraftValue(options?.initialValue);
toggleClickOutsideListener(false);
if (isDefined(customCellHotkeyScope)) {
setHotkeyScope(
@ -80,6 +86,7 @@ export const useOpenRecordTableCell = () => {
setDragSelectionStartEnabled,
setCurrentTableCellInEditMode,
initFieldInputDraftValue,
toggleClickOutsideListener,
customCellHotkeyScope,
leaveTableFocus,
navigate,

View File

@ -1,7 +1,5 @@
import { RefObject, useEffect, useRef, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { flip, offset, useFloating } from '@floating-ui/react';
import { Key } from 'ts-key-enum';
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
@ -57,8 +55,6 @@ export const AddressInput = ({
onClickOutside,
onChange,
}: AddressInputProps) => {
const theme = useTheme();
const [internalValue, setInternalValue] = useState(value);
const addressStreet1InputRef = useRef<HTMLInputElement>(null);
const addressStreet2InputRef = useRef<HTMLInputElement>(null);
@ -81,16 +77,6 @@ export const AddressInput = ({
const wrapperRef = useRef<HTMLDivElement>(null);
const { refs, floatingStyles } = useFloating({
placement: 'top-start',
middleware: [
flip(),
offset({
mainAxis: theme.spacingMultiplicator * 2,
}),
],
});
const getChangeHandler =
(field: keyof FieldAddressDraftValue) => (updatedAddressPart: string) => {
const updatedAddress = { ...value, [field]: updatedAddressPart };
@ -192,63 +178,61 @@ export const AddressInput = ({
}, [value]);
return (
<div ref={refs.setFloating} style={floatingStyles}>
<StyledAddressContainer ref={wrapperRef}>
<StyledAddressContainer ref={wrapperRef}>
<TextInput
autoFocus
value={internalValue.addressStreet1 ?? ''}
ref={inputRefs['addressStreet1']}
label="ADDRESS 1"
fullWidth
onChange={getChangeHandler('addressStreet1')}
onFocus={getFocusHandler('addressStreet1')}
disableHotkeys
/>
<TextInput
value={internalValue.addressStreet2 ?? ''}
ref={inputRefs['addressStreet2']}
label="ADDRESS 2"
fullWidth
onChange={getChangeHandler('addressStreet2')}
onFocus={getFocusHandler('addressStreet2')}
disableHotkeys
/>
<StyledHalfRowContainer>
<TextInput
autoFocus
value={internalValue.addressStreet1 ?? ''}
ref={inputRefs['addressStreet1']}
label="ADDRESS 1"
value={internalValue.addressCity ?? ''}
ref={inputRefs['addressCity']}
label="CITY"
fullWidth
onChange={getChangeHandler('addressStreet1')}
onFocus={getFocusHandler('addressStreet1')}
onChange={getChangeHandler('addressCity')}
onFocus={getFocusHandler('addressCity')}
disableHotkeys
/>
<TextInput
value={internalValue.addressStreet2 ?? ''}
ref={inputRefs['addressStreet2']}
label="ADDRESS 2"
value={internalValue.addressState ?? ''}
ref={inputRefs['addressState']}
label="STATE"
fullWidth
onChange={getChangeHandler('addressStreet2')}
onFocus={getFocusHandler('addressStreet2')}
onChange={getChangeHandler('addressState')}
onFocus={getFocusHandler('addressState')}
disableHotkeys
/>
<StyledHalfRowContainer>
<TextInput
value={internalValue.addressCity ?? ''}
ref={inputRefs['addressCity']}
label="CITY"
fullWidth
onChange={getChangeHandler('addressCity')}
onFocus={getFocusHandler('addressCity')}
disableHotkeys
/>
<TextInput
value={internalValue.addressState ?? ''}
ref={inputRefs['addressState']}
label="STATE"
fullWidth
onChange={getChangeHandler('addressState')}
onFocus={getFocusHandler('addressState')}
disableHotkeys
/>
</StyledHalfRowContainer>
<StyledHalfRowContainer>
<TextInput
value={internalValue.addressPostcode ?? ''}
ref={inputRefs['addressPostcode']}
label="POST CODE"
fullWidth
onChange={getChangeHandler('addressPostcode')}
onFocus={getFocusHandler('addressPostcode')}
disableHotkeys
/>
<CountrySelect
onChange={getChangeHandler('addressCountry')}
selectedCountryName={internalValue.addressCountry ?? ''}
/>
</StyledHalfRowContainer>
</StyledAddressContainer>
</div>
</StyledHalfRowContainer>
<StyledHalfRowContainer>
<TextInput
value={internalValue.addressPostcode ?? ''}
ref={inputRefs['addressPostcode']}
label="POST CODE"
fullWidth
onChange={getChangeHandler('addressPostcode')}
onFocus={getFocusHandler('addressPostcode')}
disableHotkeys
/>
<CountrySelect
onChange={getChangeHandler('addressCountry')}
selectedCountryName={internalValue.addressCountry ?? ''}
/>
</StyledHalfRowContainer>
</StyledAddressContainer>
);
};

View File

@ -1,10 +1,7 @@
import { useRef, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { flip, offset, useFloating } from '@floating-ui/react';
import { Nullable } from 'twenty-ui';
import { DateDisplay } from '@/ui/field/display/components/DateDisplay';
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
import {
MONTH_AND_YEAR_DROPDOWN_ID,
@ -19,16 +16,9 @@ const StyledCalendarContainer = styled.div`
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
top: 0;
position: absolute;
z-index: 1;
`;
const StyledInputContainer = styled.div`
padding: ${({ theme }) => theme.spacing(0)} ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
export type DateInputProps = {
@ -55,22 +45,10 @@ export const DateInput = ({
isDateTimeInput,
onClear,
}: DateInputProps) => {
const theme = useTheme();
const [internalValue, setInternalValue] = useState(value);
const wrapperRef = useRef<HTMLDivElement>(null);
const { refs, floatingStyles } = useFloating({
placement: 'bottom-start',
middleware: [
flip(),
offset({
mainAxis: theme.spacingMultiplicator * -6,
}),
],
});
const handleChange = (newDate: Date | null) => {
setInternalValue(newDate);
onChange?.(newDate);
@ -104,27 +82,20 @@ export const DateInput = ({
return (
<div ref={wrapperRef}>
<div ref={refs.setReference}>
<StyledInputContainer>
<DateDisplay value={internalValue ?? new Date()} />
</StyledInputContainer>
</div>
<div ref={refs.setFloating} style={floatingStyles}>
<StyledCalendarContainer>
<InternalDatePicker
date={internalValue ?? new Date()}
onChange={handleChange}
onMouseSelect={(newDate: Date | null) => {
onEnter(newDate);
}}
clearable={clearable ? clearable : false}
isDateTimeInput={isDateTimeInput}
onEnter={onEnter}
onEscape={onEscape}
onClear={handleClear}
/>
</StyledCalendarContainer>
</div>
<StyledCalendarContainer>
<InternalDatePicker
date={internalValue ?? new Date()}
onChange={handleChange}
onMouseSelect={(newDate: Date | null) => {
onEnter(newDate);
}}
clearable={clearable ? clearable : false}
isDateTimeInput={isDateTimeInput}
onEnter={onEnter}
onEscape={onEscape}
onClear={handleClear}
/>
</StyledCalendarContainer>
</div>
);
};

View File

@ -20,7 +20,6 @@ const StyledInnerContainer = styled.div`
display: flex;
flex-direction: column;
width: ${() => (useIsMobile() ? `100%` : '348px')};
overflow-x: hidden;
`;
const StyledIntermediateContainer = styled.div`