FloatingWindow
FloatingWindow (플로팅 윈도우)
FloatingWindow 컴포넌트는 드래그할 수 있는 고정 위치의 플로팅 영역을 만드는 컴포넌트예요. 드래그, 리사이즈, 뷰포트 내 제약을 지원해요.
출처: 문서
본문
FloatingWindow는 고정 위치를 가진 드래그 가능한 요소를 만들어요.
import { Button, CloseButton, FloatingWindow, Group, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function Demo() {
const [visible, handlers] = useDisclosure();
return (
<>
<Button onClick={handlers.toggle} variant="default">
{visible ? 'Hide' : 'Show'} floating window
</Button>
{visible && (
<FloatingWindow
w={280}
p="md"
withBorder
excludeDragHandleSelector="button"
initialPosition={{ top: 300, left: 20 }}
style={{ cursor: 'move' }}
>
<Group justify="space-between" mb="md">
<Text>Usage demo</Text>
<CloseButton onClick={handlers.close} />
</Group>
<Text fz="sm">This is a floating window. You can drag it around.</Text>
</FloatingWindow>
)}
</>
);
}
뷰포트 내 제약 (Constrain to viewport)
constrainToViewport prop으로 요소의 이동을 뷰포트 경계 안으로 제한할 수 있어요. 이 prop을 설정하지 않으면 요소를 뷰포트 바깥으로 드래그할 수 있어요.
import { Button, CloseButton, FloatingWindow, Group, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function Demo() {
const [visible, handlers] = useDisclosure();
return (
<>
<Button onClick={handlers.toggle} variant="default">
{visible ? 'Hide' : 'Show'} floating window
</Button>
{visible && (
<FloatingWindow
w={280}
p="md"
withBorder
excludeDragHandleSelector="button"
initialPosition={{ top: 300, left: 20 }}
style={{ cursor: 'move' }}
constrainToViewport={false}
>
<Group justify="space-between" mb="md">
<Text>No constrain demo</Text>
<CloseButton onClick={handlers.close} />
</Group>
<Text fz="sm">
The floating window is not constrained by the viewport, it can move out of bounds.
</Text>
</FloatingWindow>
)}
</>
);
}
제약 오프셋 (Constrain offset)
constrainOffset prop으로 요소를 제약할 때 뷰포트 가장자리로부터의 오프셋을 설정할 수 있어요.
import { Button, CloseButton, FloatingWindow, Group, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function Demo() {
const [visible, handlers] = useDisclosure();
return (
<>
<Button onClick={handlers.toggle} variant="default">
{visible ? 'Hide' : 'Show'} floating window
</Button>
{visible && (
<FloatingWindow
w={280}
p="md"
withBorder
excludeDragHandleSelector="button"
initialPosition={{ top: 300, left: 30 }}
style={{ cursor: 'move' }}
constrainToViewport
constrainOffset={30}
>
<Group justify="space-between" mb="md">
<Text>Constrain offset demo</Text>
<CloseButton onClick={handlers.close} />
</Group>
<Text fz="sm">
This floating window has 30px offset, it cannot move closer that 30px to the edge of the
viewport.
</Text>
</FloatingWindow>
)}
</>
);
}
드래그 핸들 선택자 (Drag handle selector)
dragHandleSelector prop으로 플로팅 윈도우를 드래그하는 데 사용할 요소(또는 요소 그룹)의 선택자를 지정할 수 있어요. 설정하지 않으면 전체 루트 요소가 드래그 대상으로 사용돼요.
excludeDragHandleSelector prop은 dragHandleSelector 안의 요소를 드래그 이벤트에서 제외해요. 다음 예시에서는 닫기 버튼이 드래그 이벤트에서 제외돼요.
import { Button, CloseButton, FloatingWindow, Group, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function Demo() {
const [visible, handlers] = useDisclosure();
return (
<>
<Button onClick={handlers.toggle} variant="default">
{visible ? 'Hide' : 'Show'} floating window
</Button>
{visible && (
<FloatingWindow
w={280}
withBorder
dragHandleSelector=".drag-handle"
excludeDragHandleSelector="button"
initialPosition={{ top: 300, left: 20 }}
>
<Group
justify="space-between"
px="md"
py="sm"
className="drag-handle"
style={{ cursor: 'move' }}
>
<Text>Drag handle demo</Text>
<CloseButton onClick={handlers.close} />
</Group>
<Text fz="sm" px="md" pb="sm">
Drag floating window around with drag handle element.
</Text>
</FloatingWindow>
)}
</>
);
}
enabled prop
enabled 옵션으로 드래그를 활성화하거나 비활성화할 수 있어요.
import { Button, Chip, CloseButton, FloatingWindow, Group, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function Demo() {
const [visible, handlers] = useDisclosure();
const [enabled, setEnabled] = useState(true);
return (
<>
<Group>
<Button variant="default" onClick={handlers.toggle}>
{visible ? 'Hide' : 'Show'} floating window
</Button>
<Chip checked={enabled} onChange={() => setEnabled((e) => !e)}>
Drag {enabled ? 'enabled' : 'disabled'}
</Chip>
</Group>
{visible && (
<FloatingWindow
w={280}
p="md"
withBorder
excludeDragHandleSelector="button"
initialPosition={{ top: 300, left: 20 }}
style={{ cursor: 'move' }}
enabled={enabled}
>
<Group justify="space-between" mb="md">
<Text>Enabled demo</Text>
<CloseButton onClick={handlers.close} />
</Group>
<Text fz="sm">This is a floating window. You can drag it around.</Text>
</FloatingWindow>
)}
</>
);
}
위치 설정 (Set position)
setPosition 함수를 호출하면 요소의 위치를 프로그래밍 방식으로 설정할 수 있어요. 이 함수는 top, left, right, bottom 속성을 가진 객체를 받으며, 그중 두 개만 지정해야 해요(예: top과 left, bottom과 right).
import { useRef } from 'react';
import { Button, CloseButton, FloatingWindow, Group, Text } from '@mantine/core';
import { SetFloatingWindowPosition, useDisclosure } from '@mantine/hooks';
function Demo() {
const [visible, handlers] = useDisclosure();
const setPositionRef = useRef<SetFloatingWindowPosition | null>(null);
const setPosition = () => {
setPositionRef.current?.({ bottom: 40, right: 40 });
};
return (
<>
<Group>
<Button onClick={handlers.toggle} variant="default">
{visible ? 'Hide' : 'Show'} floating window
</Button>
<Button onClick={setPosition} variant="default">
Set position to bottom right corner
</Button>
</Group>
{visible && (
<FloatingWindow
w={280}
p="md"
withBorder
excludeDragHandleSelector="button"
initialPosition={{ top: 300, left: 20 }}
style={{ cursor: 'move' }}
setPositionRef={setPositionRef}
>
<Group justify="space-between" mb="md">
<Text>Set position demo</Text>
<CloseButton onClick={handlers.close} />
</Group>
<Text fz="sm">
You can control floating window position programmatically with setPositionRef.
</Text>
</FloatingWindow>
)}
</>
);
}
리사이즈 핸들 (Resize handle)
FloatingWindow.ResizeHandle 복합 컴포넌트로 플로팅 윈도우에 리사이즈 핸들을 추가할 수 있어요. FloatingWindow의 dimensions prop이 리사이즈 동작을 제어해요.
initialWidth/initialHeight– 윈도우의 초기 크기(px)minWidth/minHeight– 윈도우를 리사이즈할 수 있는 최소 크기maxWidth/maxHeight– 윈도우를 리사이즈할 수 있는 최대 크기
리사이즈 핸들은 constrainToViewport와 constrainOffset prop을 존중해요. 윈도우는 뷰포트 경계를 넘어 리사이즈될 수 없어요.
FloatingWindow.ResizeHandle은 role="separator"로 렌더링되며 키보드 상호작용을 지원해요. Arrow Left/Arrow Right 키로 너비를, Arrow Up/Arrow Down 키로 높이를 10px 단위로 리사이즈해요. Home/End 키는 최소/최대 크기로 이동해요.
import { NotchesIcon } from '@phosphor-icons/react';
import { Button, CloseButton, FloatingWindow, Group, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function Demo() {
const [visible, handlers] = useDisclosure();
return (
<>
<Button onClick={handlers.toggle} variant="default">
{visible ? 'Hide' : 'Show'} floating window
</Button>
{visible && (
<FloatingWindow
withBorder
constrainOffset={40}
dimensions={{
initialWidth: 260,
maxWidth: 500,
minWidth: 180,
initialHeight: 260,
maxHeight: 400,
minHeight: 220,
}}
dragHandleSelector=".drag-handle"
excludeDragHandleSelector="button"
initialPosition={{ top: 300, left: 60 }}
style={{ overflow: 'hidden' }}
>
<Group
justify="space-between"
px="md"
py="sm"
className="drag-handle"
style={{ cursor: 'move' }}
>
<Text fw={500} fz="sm">
Resize demo
</Text>
<CloseButton onClick={handlers.close} />
</Group>
<Text fz="sm" px="md" pb="sm">
Drag the grip icon in the bottom-right corner to resize.
Use Arrow keys when the handle is focused:
Left/Right for width, Up/Down for height.
</Text>
<FloatingWindow.ResizeHandle
aria-label="Resize floating window"
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: 20,
height: 20,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'nwse-resize',
}}
>
<NotchesIcon size={14} style={{ opacity: 0.5 }} />
</FloatingWindow.ResizeHandle>
</FloatingWindow>
)}
</>
);
}
리사이즈 콜백 (Resize callbacks)
FloatingWindow는 드래그에 사용되는 onPositionChange, onDragStart, onDragEnd를 반영한 다음 리사이즈 콜백을 지원해요.
onSizeChange– 윈도우가 리사이즈될 때마다{ width, height }와 함께 호출돼요onResizeStart– 리사이즈가 시작될 때 호출돼요onResizeEnd– 리사이즈가 끝날 때 호출돼요
onSizeChange에 전달되는 크기는 새 크기가 적용된 후에 측정되므로 이미 dimensions와 뷰포트 제약에 의해 제한되고 항상 윈도우의 실제 크기와 일치해요. width와 height 모두 항상 제공되며, 축 하나만 리사이즈 가능해도 마찬가지예요.
onSizeChange는 포인터와 키보드 리사이즈 모두에서 호출돼요. onResizeStart와 onResizeEnd는 포인터 리사이즈에서만 호출돼요. 키보드 리사이즈는 새 크기를 즉시 적용하며 시작/끝 단계가 없어요.
모든 리사이즈 콜백은 dimensions prop과 FloatingWindow.ResizeHandle이 필요해요. 윈도우가 리사이즈 가능하지 않으면 호출되지 않아요.
import { useState } from 'react';
import { NotchesIcon } from '@phosphor-icons/react';
import { Button, CloseButton, FloatingWindow, Group, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function Demo() {
const [visible, handlers] = useDisclosure();
const [size, setSize] = useState({ width: 260, height: 200 });
const [resizing, setResizing] = useState(false);
return (
<>
<Button onClick={handlers.toggle} variant="default">
{visible ? 'Hide' : 'Show'} floating window
</Button>
{visible && (
<FloatingWindow
withBorder
constrainOffset={40}
dimensions={{
initialWidth: 260,
maxWidth: 500,
minWidth: 180,
initialHeight: 200,
maxHeight: 400,
minHeight: 160,
}}
dragHandleSelector=".drag-handle"
excludeDragHandleSelector="button"
initialPosition={{ top: 300, left: 60 }}
onSizeChange={setSize}
onResizeStart={() => setResizing(true)}
onResizeEnd={() => setResizing(false)}
style={{ overflow: 'hidden' }}
>
<Group
justify="space-between"
px="md"
py="sm"
className="drag-handle"
style={{ cursor: 'move' }}
>
<Text fw={500} fz="sm">
Resize callbacks
</Text>
<CloseButton onClick={handlers.close} />
</Group>
<Text fz="sm" px="md">
{Math.round(size.width)} × {Math.round(size.height)}
</Text>
<Text fz="sm" px="md" c="dimmed">
{resizing ? 'Resizing…' : 'Drag the grip icon to resize'}
</Text>
<FloatingWindow.ResizeHandle
aria-label="Resize floating window"
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: 20,
height: 20,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'nwse-resize',
}}
>
<NotchesIcon size={14} style={{ opacity: 0.5 }} />
</FloatingWindow.ResizeHandle>
</FloatingWindow>
)}
</>
);
}
축 잠금 (Lock axis)
axis 옵션으로 이동을 특정 축으로 제한할 수 있어요.
import { Button, CloseButton, FloatingWindow, Group, SegmentedControl, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function Demo() {
const [visible, handlers] = useDisclosure();
const [axis, setAxis] = useState<'x' | 'y'>('y');
return (
<>
<Group>
<Button onClick={handlers.toggle} variant="default">
{visible ? 'Hide' : 'Show'} floating window
</Button>
<SegmentedControl data={['x', 'y']} onChange={(val) => setAxis(val as 'x')} value={axis} />
</Group>
{visible && (
<FloatingWindow
w={280}
p="md"
withBorder
excludeDragHandleSelector="button"
initialPosition={{ top: 200, left: 40 }}
style={{ cursor: 'move' }}
axis={axis}
>
<Group justify="space-between" mb="md">
<Text>Axis demo</Text>
<CloseButton onClick={handlers.close} />
</Group>
<Text fz="sm">
When you set axis prop, the floating window can be dragged only horizontally or
vertically.
</Text>
</FloatingWindow>
)}
</>
);
}
use-floating-window 훅
훅 API를 선호한다면 useFloatingWindow 훅을 사용할 수 있어요. 대부분의 FloatingWindow 기능을 지원해요.
import { Button, CloseButton, Group, Paper, Portal, Text } from '@mantine/core';
import { useDisclosure, useFloatingWindow } from '@mantine/hooks';
function Demo() {
const [visible, handlers] = useDisclosure();
const floatingWindow = useFloatingWindow({
constrainToViewport: true,
constrainOffset: 20,
excludeDragHandleSelector: 'button',
initialPosition: { top: 300, left: 20 },
});
return (
<>
<Button onClick={handlers.toggle} variant="default">
{visible ? 'Hide' : 'Show'} floating window
</Button>
{visible && (
<Portal>
<Paper
w={280}
p="md"
withBorder
pos="fixed"
style={{ cursor: 'move', transition: 'box-shadow 70ms ease', zIndex: 400 }}
shadow={floatingWindow.isDragging ? 'md' : undefined}
ref={floatingWindow.ref}
>
<Group justify="space-between" mb="md">
<Text>Usage demo</Text>
<CloseButton onClick={handlers.close} />
</Group>
<Text fz="sm">This is a floating window. You can drag it around.</Text>
</Paper>
</Portal>
)}
</>
);
}
더 알아보기 (Learn more)
- useFloatingWindow — 훅 버전
- Portal — 포털 컴포넌트
- Dialog — 다이얼로그 컴포넌트