mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-25 11:04:00 +00:00
Extract useOnClickAway
This commit is contained in:
parent
e815d62f6e
commit
75d1420ae4
2 changed files with 73 additions and 63 deletions
|
|
@ -1,80 +1,53 @@
|
|||
import React from "react";
|
||||
import React, { useState, ReactNode, useCallback } from "react";
|
||||
import classnames from "classnames";
|
||||
import { useOnClickAway } from "../../hooks";
|
||||
|
||||
import PlaylistMenuEnry from "./PlaylistMenuEntry";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
children: ReactNode | Array<ReactNode>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export default class PlaylistMenu extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { selected: false };
|
||||
}
|
||||
export default function PlaylistMenu(props: Props) {
|
||||
const [selected, setSelected] = useState(false);
|
||||
|
||||
_handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const { target } = e;
|
||||
if (!(target instanceof Element)) {
|
||||
// TypeScript doesn't realize this will always be true
|
||||
return;
|
||||
}
|
||||
const { selected } = this.state;
|
||||
if (selected) {
|
||||
this.setState({ selected: false });
|
||||
return;
|
||||
}
|
||||
const [ref, setRef] = useState<Element | null>(null);
|
||||
|
||||
const handleClickOut = (ee: MouseEvent) => {
|
||||
const clickOutTarget = ee.target;
|
||||
if (!(clickOutTarget instanceof Element)) {
|
||||
// TypeScript doesn't realize this will always be true
|
||||
return;
|
||||
}
|
||||
// If the click is _not_ inside the menu.
|
||||
if (!target.contains(clickOutTarget)) {
|
||||
// If we've clicked on a Context Menu spawed inside this menu, it will
|
||||
// register as an external click. However, hiding the menu will remove
|
||||
// the Context Menu from the DOM. Therefore, we wait until the next
|
||||
// event loop to actually hide ourselves.
|
||||
setTimeout(() => {
|
||||
// Close the menu
|
||||
this.setState({ selected: false });
|
||||
}, 0);
|
||||
window.document.removeEventListener("click", handleClickOut, {
|
||||
capture: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
window.document.addEventListener("click", handleClickOut, {
|
||||
capture: true,
|
||||
});
|
||||
const callback = useCallback(() => {
|
||||
// If we've clicked on a Context Menu spawed inside this menu, it will
|
||||
// register as an external click. However, hiding the menu will remove
|
||||
// the Context Menu from the DOM. Therefore, we wait until the next
|
||||
// event loop to actually hide ourselves.
|
||||
setTimeout(() => {
|
||||
// Close the menu
|
||||
setSelected(false);
|
||||
}, 0);
|
||||
}, []);
|
||||
|
||||
this.setState({ selected: true });
|
||||
};
|
||||
useOnClickAway(ref, selected ? callback : null);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
id={this.props.id}
|
||||
className={classnames("playlist-menu", {
|
||||
selected: this.state.selected,
|
||||
})}
|
||||
onClick={this._handleClick}
|
||||
>
|
||||
<div className="bar" />
|
||||
{this.state.selected && (
|
||||
<ul>
|
||||
{React.Children.map(this.props.children, (child, i) => (
|
||||
<PlaylistMenuEnry key={i}>{child}</PlaylistMenuEnry>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
id={props.id}
|
||||
className={classnames("playlist-menu", {
|
||||
selected,
|
||||
})}
|
||||
ref={setRef}
|
||||
onClick={() => setSelected(selected_ => !selected_)}
|
||||
>
|
||||
<div className="bar" />
|
||||
{selected && (
|
||||
<ul>
|
||||
{React.Children.map(props.children, (child, i) => (
|
||||
<PlaylistMenuEnry key={i}>{child}</PlaylistMenuEnry>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
37
js/hooks.ts
37
js/hooks.ts
|
|
@ -91,6 +91,43 @@ export function useIsHovered() {
|
|||
return { ref: setNode, hover };
|
||||
}
|
||||
|
||||
export function useOnClickAway(
|
||||
ref: Element | null,
|
||||
callback: null | (() => void)
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (ref == null || callback == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleClickOut = (ee: MouseEvent) => {
|
||||
const clickOutTarget = ee.target;
|
||||
if (!(clickOutTarget instanceof Element)) {
|
||||
// TypeScript doesn't realize this will always be true
|
||||
return;
|
||||
}
|
||||
if (ref.contains(clickOutTarget)) {
|
||||
return;
|
||||
}
|
||||
// If the click is _not_ inside the menu.
|
||||
callback();
|
||||
window.document.removeEventListener("click", handleClickOut, {
|
||||
capture: true,
|
||||
});
|
||||
};
|
||||
|
||||
window.document.addEventListener("click", handleClickOut, {
|
||||
capture: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.document.removeEventListener("click", handleClickOut, {
|
||||
capture: true,
|
||||
});
|
||||
};
|
||||
}, [ref, callback]);
|
||||
}
|
||||
|
||||
// TODO: Return useSelector directly and apply the type without wrapping
|
||||
export function useTypedSelector<T>(selector: (state: AppState) => T): T {
|
||||
return useSelector(selector);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue