FloatingIndicator
FloatingIndicator (플로팅 인디케이터)
FloatingIndicator 컴포넌트는 요소 그룹 위에 떠 있는(floating) 인디케이터를 표시하는 컴포넌트예요. 커스텀 세그먼트 컨트롤, 탭 등을 만드는 데 사용돼요.
출처: 문서
본문
FloatingIndicator는 그룹 안의 활성 요소를 하이라이트하도록 설계되었어요. 커스텀 세그먼트 컨트롤, 탭과 유사한 컴포넌트를 만드는 데 사용할 수 있어요.
FloatingIndicator는 target 요소 위에 요소를 렌더링해요. 위치를 계산하려면 parent 요소를 전달해야 해요. parent 요소는 position: relative CSS 속성을 가져야 해요. 이는 정확한 위치 지정에 필수예요. target이나 parent가 제공되지 않으면 컴포넌트는 null을 반환하고 아무것도 렌더링하지 않아요.
기본적으로 FloatingIndicator는 보이는 스타일이 없어요. className prop이나 Styles API로 스타일을 적용할 수 있어요. 인디케이터의 transform, width, height 스타일은 부드러운 위치 트랜지션을 위해 JavaScript로 직접 설정되며, Styles API로 덮어쓸 수 없어요.
import { useState } from 'react';
import { FloatingIndicator, UnstyledButton } from '@mantine/core';
import classes from './Demo.module.css';
const data = ['React', 'Vue', 'Angular', 'Svelte'];
function Demo() {
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
const [controlsRefs, setControlsRefs] = useState<Record<string, HTMLButtonElement | null>>({});
const [active, setActive] = useState(0);
const setControlRef = (index: number) => (node: HTMLButtonElement) => {
controlsRefs[index] = node;
setControlsRefs(controlsRefs);
};
const controls = data.map((item, index) => (
<UnstyledButton
key={item}
className={classes.control}
ref={setControlRef(index)}
onClick={() => setActive(index)}
mod={{ active: active === index }}
>
<span className={classes.controlLabel}>{item}</span>
</UnstyledButton>
));
return (
<div ref={setRootRef} className={classes.root}>
{controls}
<FloatingIndicator
target={controlsRefs[active]}
parent={rootRef}
className={classes.indicator}
/>
</div>
);
}
여러 행 (Multiple rows)
FloatingIndicator는 여러 행을 가진 그룹에서 활성 요소를 하이라이트하는 데도 사용할 수 있어요.
import { useState } from 'react';
import { ArrowDownIcon, ArrowDownLeftIcon, ArrowDownRightIcon, ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, ArrowUpLeftIcon, ArrowUpRightIcon, CircleIcon } from '@phosphor-icons/react';
import { FloatingIndicator, UnstyledButton } from '@mantine/core';
import classes from './Demo.module.css';
function Demo() {
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
const [controlsRefs, setControlsRefs] = useState<Record<string, HTMLButtonElement | null>>({});
const [active, setActive] = useState('center');
const setControlRef = (name: string) => (node: HTMLButtonElement) => {
controlsRefs[name] = node;
setControlsRefs(controlsRefs);
};
return (
<div ref={setRootRef} className={classes.root}>
<FloatingIndicator
target={controlsRefs[active]}
parent={rootRef}
className={classes.indicator}
/>
<div>
<UnstyledButton
className={classes.control}
onClick={() => setActive('up-left')}
ref={setControlRef('up-left')}
mod={{ active: active === 'up-left' }}
>
<ArrowUpLeftIcon size={26} />
</UnstyledButton>
<UnstyledButton
className={classes.control}
onClick={() => setActive('up')}
ref={setControlRef('up')}
mod={{ active: active === 'up' }}
>
<ArrowUpIcon size={26} />
</UnstyledButton>
<UnstyledButton
className={classes.control}
onClick={() => setActive('up-right')}
ref={setControlRef('up-right')}
mod={{ active: active === 'up-right' }}
>
<ArrowUpRightIcon size={26} />
</UnstyledButton>
</div>
<div>
<UnstyledButton
className={classes.control}
onClick={() => setActive('left')}
ref={setControlRef('left')}
mod={{ active: active === 'left' }}
>
<ArrowLeftIcon size={26} />
</UnstyledButton>
<UnstyledButton
className={classes.control}
onClick={() => setActive('center')}
ref={setControlRef('center')}
mod={{ active: active === 'center' }}
>
<CircleIcon size={26} />
</UnstyledButton>
<UnstyledButton
className={classes.control}
onClick={() => setActive('right')}
ref={setControlRef('right')}
mod={{ active: active === 'right' }}
>
<ArrowRightIcon size={26} />
</UnstyledButton>
</div>
<div>
<UnstyledButton
className={classes.control}
onClick={() => setActive('down-left')}
ref={setControlRef('down-left')}
mod={{ active: active === 'down-left' }}
>
<ArrowDownLeftIcon size={26} />
</UnstyledButton>
<UnstyledButton
className={classes.control}
onClick={() => setActive('down')}
ref={setControlRef('down')}
mod={{ active: active === 'down' }}
>
<ArrowDownIcon size={26} />
</UnstyledButton>
<UnstyledButton
className={classes.control}
onClick={() => setActive('down-right')}
ref={setControlRef('down-right')}
mod={{ active: active === 'down-right' }}
>
<ArrowDownRightIcon size={26} />
</UnstyledButton>
</div>
</div>
);
}
예시: Tabs
FloatingIndicator로 커스텀 탭을 만들 수 있어요.
import { useState } from 'react';
import { FloatingIndicator, Tabs } from '@mantine/core';
import classes from './Demo.module.css';
function Demo() {
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
const [value, setValue] = useState<string | null>('1');
const [controlsRefs, setControlsRefs] = useState<Record<string, HTMLButtonElement | null>>({});
const setControlRef = (val: string) => (node: HTMLButtonElement) => {
controlsRefs[val] = node;
setControlsRefs(controlsRefs);
};
return (
<Tabs variant="none" value={value} onChange={setValue}>
<Tabs.List ref={setRootRef} className={classes.list}>
<Tabs.Tab value="1" ref={setControlRef('1')} className={classes.tab}>
First tab
</Tabs.Tab>
<Tabs.Tab value="2" ref={setControlRef('2')} className={classes.tab}>
Second tab
</Tabs.Tab>
<Tabs.Tab value="3" ref={setControlRef('3')} className={classes.tab}>
Third tab
</Tabs.Tab>
<FloatingIndicator
target={value ? controlsRefs[value] : null}
parent={rootRef}
className={classes.indicator}
/>
</Tabs.List>
<Tabs.Panel value="1">First tab content</Tabs.Panel>
<Tabs.Panel value="2">Second tab content</Tabs.Panel>
<Tabs.Panel value="3">Third tab content</Tabs.Panel>
</Tabs>
);
}
더 알아보기 (Learn more)
- Tabs — 탭 컴포넌트
- SegmentedControl — 세그먼트 컨트롤 컴포넌트
- UnstyledButton — 스타일 없는 버튼 컴포넌트