Transition Fixed Positioning Example
src / Demo.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
import React, { ReactElement, useCallback, useRef, useState } from "react";
import CSSTransition, {
CSSTransitionClassNames,
} from "react-transition-group/CSSTransition";
import { Button } from "@react-md/button";
import {
Checkbox,
Fieldset,
Form,
ListboxOption,
Select,
useChecked,
} from "@react-md/form";
import { ArrowDropDownSVGIcon } from "@react-md/material-icons";
import { Overlay } from "@react-md/overlay";
import { useFixedPositioning } from "@react-md/transition";
import { Text } from "@react-md/typography";
import {
HorizontalPosition,
PositionAnchor,
PositionWidth,
useToggle,
VerticalPosition,
} from "@react-md/utils";
import styles from "./FixedPositioningExample.module.scss";
const horizontals: HorizontalPosition[] = [
"left",
"right",
"center",
"inner-left",
"inner-right",
];
const verticals: VerticalPosition[] = [
"above",
"below",
"center",
"top",
"bottom",
];
const widths: PositionWidth[] = ["auto", "equal", "min"];
const anchors = horizontals.reduce<Record<string, PositionAnchor>>(
(value, x) => {
verticals.forEach((y) => {
value[`${x} ${y}`] = { x, y };
});
return value;
},
{}
);
const anchorOptions = Object.entries(anchors).map(([value, anchor]) => ({
...anchor,
label: value,
value,
}));
type Anchor = typeof anchorOptions[0];
const CENTERED_ANCHOR = anchorOptions.find(
(anchor) => anchor.label === "center center"
) as Anchor;
const CLASSNAMES: CSSTransitionClassNames = {
appear: styles.enter,
appearActive: styles.entering,
enter: styles.enter,
enterActive: styles.entering,
exit: styles.exit,
exitActive: styles.exiting,
};
export default function Demo(): ReactElement {
const [visible, show, hide] = useToggle(false);
const buttonRef = useRef<HTMLButtonElement | null>(null);
const divRef = useRef<HTMLDivElement>(null);
const [disableSwapping, handleSwapCange] = useChecked(false);
const [transformOrigin, handleOriginChange] = useChecked(false);
const [hideOnScroll, handleScrollChange] = useChecked(true);
const [hideOnResize, handleScrollResize] = useChecked(true);
const [anchor, setAnchor] = useState(anchorOptions[0]);
const handleAnchorChange = useCallback(
(_value: string, anchor: ListboxOption) => {
setAnchor(anchor as Anchor);
},
[]
);
const [width, setWidth] = useState<PositionWidth>("auto");
const handleWidthChange = useCallback((nextWidth: string) => {
setAnchor(CENTERED_ANCHOR);
setWidth(nextWidth as PositionWidth);
}, []);
const { style, onEnter, onEntering, onEntered, onExited } =
useFixedPositioning({
fixedTo: buttonRef.current,
anchor: { x: anchor.x, y: anchor.y },
width,
transformOrigin,
disableSwapping,
onScroll(_event, { fixedTo: button }) {
if (hideOnScroll) {
hide();
return;
}
if (!button) {
return;
}
const { top } = button.getBoundingClientRect();
if (top < 0 || top > window.innerHeight) {
hide();
}
},
onResize(_event) {
if (hideOnResize) {
hide();
}
},
});
return (
<>
<Form className={styles.form}>
<Fieldset legend="Fixed Positioning Options">
<Checkbox
id="fixed-swap"
name="options"
label="Disable Swapping"
checked={disableSwapping}
onChange={handleSwapCange}
/>
<Checkbox
id="fixed-origin"
name="options"
label="Transform Origin"
checked={transformOrigin}
onChange={handleOriginChange}
/>
<Checkbox
id="fixed-hide-on-scroll"
name="options"
label="Hide on scroll"
checked={hideOnScroll}
onChange={handleScrollChange}
/>
<Checkbox
id="fixed-hide-on-resize"
name="options"
label="Hide on resize"
checked={hideOnResize}
onChange={handleScrollResize}
/>
</Fieldset>
<Select
id="fixed-anchor-type"
label="Anchor"
className={styles.select}
listboxClassName={styles.listbox}
inline
options={anchorOptions}
value={anchor.value}
onChange={handleAnchorChange}
rightChildren={<ArrowDropDownSVGIcon />}
listboxWidth="min"
isOptionDisabled={(option) => {
const opt = option as Anchor;
return width !== "auto" && !opt.value.startsWith("center");
}}
/>
<Select
id="fixed-anchor-width"
label="Fixed element width"
className={styles.select}
inline
options={widths}
value={width}
onChange={handleWidthChange}
rightChildren={<ArrowDropDownSVGIcon />}
/>
<div className={styles.footer}>
<Button
id="fixed-positioning-button"
ref={buttonRef}
onClick={show}
theme="primary"
themeType="contained"
type="submit"
>
Toggle
</Button>
</div>
</Form>
<Overlay
id="fixed-positioning-overlay"
onRequestClose={hide}
hidden
visible={visible}
/>
<CSSTransition
in={visible}
nodeRef={divRef}
mountOnEnter
unmountOnExit
classNames={CLASSNAMES}
timeout={{ enter: 200, exit: 150 }}
onEnter={(isAppearing) => onEnter(divRef.current!, isAppearing)}
onEntering={(isAppearing) => onEntering(divRef.current!, isAppearing)}
onEntered={(isAppearing) => onEntered(divRef.current!, isAppearing)}
onExited={() => onExited(divRef.current!)}
>
<div
id="fixed-position-div"
ref={divRef}
style={style}
className={styles.div}
>
<Text>This is some amazing text in a fixed element!</Text>
</div>
</CSSTransition>
</>
);
}