Skip to content

Instantly share code, notes, and snippets.

@khusamov
Last active January 18, 2020 14:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save khusamov/26b87429c7718cb0399df07ea36d4bb8 to your computer and use it in GitHub Desktop.
Save khusamov/26b87429c7718cb0399df07ea36d4bb8 to your computer and use it in GitHub Desktop.
Плавающий элемент (React)
import {ReactNode, ReactPortal} from 'react';
import {createPortal} from 'react-dom';
/**
* Создать портал в `document.body`.
* @param children
* @param key
*/
export default function createBodyPortal(children: ReactNode, key?: null | string): ReactPortal {
return createPortal(children, document.body, key);
}
import React, {forwardRef} from 'react';
import {styled} from '@material-ui/styles';
import {TopProperty, LeftProperty} from 'csstype';
type TLength = string | 0;
type TDivProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
interface IFloatingDivProps {
x: number;
y: number;
}
const createFloatingDiv = styled(
forwardRef<HTMLDivElement, IFloatingDivProps & TDivProps>(({x, y, ...other}, ref) => (
<div
ref={ref}
// tabindex="-1" позволяет фокусироваться на элементе только программно.
// Клавиша Tab проигнорирует такой элемент, но метод elem.focus() будет действовать.
// https://learn.javascript.ru/focus-blur#vklyuchaem-fokusirovku-na-lyubom-elemente-tabindex
tabIndex={-1}
{...other}
/>
))
);
export const FloatingDiv = (
createFloatingDiv<{}, IFloatingDivProps>(({x, y}) => ({
position: 'absolute',
left: x as TopProperty<TLength>,
top: y as LeftProperty<TLength>,
outline: 'none'
}), {name: 'FloatingDiv'})
);
import React, {createRef, useEffect, useState, forwardRef, useImperativeHandle} from 'react';
import {PropsWithChildren, MouseEventHandler, MouseEvent, FocusEvent} from 'react';
import createBodyPortal from '../../functions/createBodyPortal';
import {FloatingDiv} from './Floating.style';
interface IFloatingProps {
}
export interface IFloatingImperativeHandle {
/**
* Изменить координаты плавающего элемента на основе события
* мышки (берутся свойства event.pageX и event.pageY).
*/
setPositionByMouseEvent: MouseEventHandler;
/**
* Изменить координаты плавающего элемента.
* После изменения координат элемент становится видимым.
*/
setPosition: (x: number, y: number) => void;
}
/**
* Плавающий элемент.
*/
const Floating = (
forwardRef<IFloatingImperativeHandle, PropsWithChildren<IFloatingProps>>(
(props, ref) => {
const {children} = props;
const [visibled, setVisibled] = useState(false);
const [x, setX] = useState(0);
const [y, setY] = useState(0);
const floatingDivRef = createRef<HTMLDivElement>();
const onFloatingDivBlur = (event: FocusEvent<HTMLDivElement>) => {
const floatingDiv = floatingDivRef.current;
if (!(floatingDiv && event.relatedTarget instanceof Element && floatingDiv.contains(event.relatedTarget))) {
setVisibled(false);
}
};
useEffect(() => {
if (floatingDivRef.current) {
floatingDivRef.current.focus();
}
});
useImperativeHandle(ref, () => ({
setPositionByMouseEvent(event: MouseEvent) {
event.preventDefault();
this.setPosition(event.pageX, event.pageY);
},
setPosition(x: number, y: number) {
setX(x);
setY(y);
setVisibled(true);
}
}));
return (
createBodyPortal(
visibled && <FloatingDiv
ref={floatingDivRef}
onBlur={onFloatingDivBlur}
{...{x, y, children}}
/>
)
);
}
)
);
export default Floating;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment