**Tôi định nghĩa dropbox là 1 cái hộp được thả ra với phương hướng bất kì là cơ sở cho dropdown, autocomplete. phương hướng bất kì là nó tự động dựa vạo vị trí mà nó đang ở để xác định thả box ra ngoài trong phạm vị xem được hợp lý ví dụ như: box nó ở gần cuối trang thì nó phải xuất hiển quay ngược lên trên , nó ở bên phải , thì nó phải lệch box sang trái **
- Component
- dropbox content thì tạo bằng createPortal
- keyId thì dùng mặt định useId React 19
- impact là lớp thao tác vs UI dev tự code, content cũng vậy
- position là lớp css hiển thị box nó nằm trong logic
L.Master chỉ là cái tag div bình thường thui.
'use client';
import { $Master } from '@/libs';
import { L } from '@/libs/es';
import cx from 'classnames';
import { isEmpty } from 'lodash';
import React, { cloneElement, memo, useId } from 'react';
import { createPortal } from 'react-dom';
import useLogic from './logic';
import styles from './styles.module.scss'; type P = ReturnType<typeof useLogic>;
interface $Props { keyId?: string; // không truyền củng được || default dùng useId React 19 impact?(args: Pick<P, 'show' | 'onToggle'>): JSX.Element; contents?(args: Pick<P, 'show' | 'onToggle' | 'dropBoxRect'>): JSX.Element; // dropBoxRect trả về Rect clsDropbox?: string; // class css clsContent?: string; // class css isOutlet?: boolean; // nhấn ra ngoài để tắt setting?: $Master; // css
} function Dropdown(props: $Props) { const { clsDropbox, clsContent, impact, contents, keyId = useId(), isOutlet, setting, } = props; const args = useLogic({ keyId }); const { onToggle, position, show } = args; return cloneElement( <L.Master id={keyId} className={cx(styles.Dropbox, clsDropbox)} ></L.Master>, { ...setting }, <> {impact && cloneElement(impact?.(args), { id: `DropImpact_${keyId}`, })} {React.useMemo( () => show && !isEmpty(position) && createPortal( <L.Master id={`DropContent_${keyId}`} onMouseLeave={isOutlet ? onToggle : undefined} style={{ ...position, }} className={cx(styles.DropContent, clsContent)} > {contents?.(args)} </L.Master>, document.body ), [show, position, keyId, isOutlet] )} </> );
} export default memo(Dropdown);
- Logic Tạo cái use hook xử lý logic
- tính nó gần bottom hông , nó gần trái hông
- dùng transform
'use client';
import { $listenEvent } from '@/libs';
import { debounce } from 'lodash';
import React, { CSSProperties, useEffect, useState } from 'react'; type $Args = { keyId: string;
}; export default function useLogic({ keyId }: $Args) { const [show, setShow] = useState({ [keyId]: false, }); const [position, setPosition] = useState({ [keyId]: {} as CSSProperties, }); const [dropBoxRect, setDropBoxRect] = useState<DOMRect>(); const onPosition = React.useCallback( debounce(() => { const DropImpact = document.getElementById(`DropImpact_${keyId}`); const DropContent = document.getElementById(`DropContent_${keyId}`); const DropImpactRect = DropImpact?.getBoundingClientRect(); const DropContentRect = DropContent?.getBoundingClientRect(); const { innerHeight, innerWidth } = window; if (DropImpactRect) { const isNearBottom = innerHeight / 2 - DropImpactRect.y < 0; const isNearLeft = innerWidth / 2 - DropImpactRect.x < -200; setPosition((prev) => ({ ...prev, [keyId]: { transform: `translate(${ isNearLeft ? Number(DropImpactRect?.x) + Number(DropImpactRect?.width) - Number(DropContentRect?.width) : Number(DropImpactRect.left) }px, ${ isNearBottom ? Number(DropImpactRect.y) - Number(DropContentRect?.height) : Number(DropImpactRect.bottom) }px)`, top: -1, left: -1, }, })); } }, 300), [] ); useEffect(() => { if (!show[keyId]) { onPosition(); $listenEvent.add(onPosition); } return () => { onPosition(); setPosition({}); $listenEvent.remove(onPosition); }; }, []); const onGetRect = React.useCallback(() => { if (!dropBoxRect) { const Dropbox = document.getElementById(`${keyId}`); const DropboxRect = Dropbox?.getBoundingClientRect(); setDropBoxRect(DropboxRect); } }, [dropBoxRect]); useEffect(() => { if (!dropBoxRect) { onGetRect(); $listenEvent.add(onGetRect); } return () => { onGetRect(); $listenEvent.remove(onGetRect); }; }, [dropBoxRect]); const onToggle = () => { setShow((prev) => ({ ...prev, [keyId]: !prev[keyId] })); }; const onSetShow = (status: boolean) => { setShow((prev) => ({ ...prev, [keyId]: status })); }; return { onSetShow, // set status cho key thay cho onOpen, onClose onToggle, position: position[keyId], // vị trí box được mở show: show[keyId], // trạng thái mở box dropBoxRect, // rect của dropbox };
}
Mình đặt nhân tử chung ra ngoài làm thừa số chung:
export const $listenEvent = { add(fnc: () => void) { document.addEventListener('wheel', fnc); document.addEventListener('resize', fnc); document.addEventListener('orientationchange', fnc); document.addEventListener('load', fnc); document.addEventListener('reload', fnc); }, remove(fnc: () => void) { document.removeEventListener('wheel', fnc); document.removeEventListener('resize', fnc); document.removeEventListener('orientationchange', fnc); document.removeEventListener('load', fnc); document.removeEventListener('reload', fnc); },
};
- SCSS Đơn giản vầy thui, muốn thêm hiệu transform , transition, visiable , tự thêm vô
.Dropbox { display: flex;
} .DropContent { position: fixed; z-index: 999; min-height: 250px; display: flex; content: ''; padding: 0.5rem 0; flex: 1;
}
- Cách dùng :
<L.Dropbox {...{ setting: { width: '100%', }, clsDropbox: styles.clsDropbox, impact(args) { return ( <L.Magic hover width={'fit-content'} shadow padding={'1rem'} onClick={args.onToggle} className={styles.impact} > Toggle dropdown </L.Magic> ); }, contents({ dropBoxRect }) { return ( <L.Section background={'white'} shadow minWidth={dropBoxRect?.width} minHeight={'100%'} padding={'1rem'} > <L.Txt> contents contents contents contents contents contents </L.Txt> </L.Section> ); }, }} />