diff --git a/src/client/lazy-app/Compress/Flyout/index.tsx b/src/client/lazy-app/Compress/Flyout/index.tsx index 6cecfa18..f13e0e48 100644 --- a/src/client/lazy-app/Compress/Flyout/index.tsx +++ b/src/client/lazy-app/Compress/Flyout/index.tsx @@ -6,51 +6,119 @@ import { createRef, ComponentChildren, ComponentProps, + Fragment, + render, } from 'preact'; -import { ClickOutsideDetector } from '../ClickOutsideDetector'; import * as style from './style.css'; import 'add-css:./style.css'; type Anchor = 'left' | 'right' | 'top' | 'bottom'; type Direction = 'left' | 'right' | 'up' | 'down'; +const has = (haystack: string | string[] | undefined, needle: string) => + Array.isArray(haystack) ? haystack.includes(needle) : haystack === needle; + interface Props extends ComponentProps<'aside'> { showing?: boolean; direction?: Direction | Direction[]; - anchor?: Anchor | Anchor[]; + anchor?: Anchor; toggle?: VNode; children?: ComponentChildren; } interface State { showing: boolean; + hasShown: boolean; } export default class Flyout extends Component { state = { showing: this.props.showing === true, + hasShown: this.props.showing === true, }; + private wrap = createRef(); + private menu = createRef(); - private hide = () => { - this.setState({ showing: false }); + private resizeObserver?: ResizeObserver; + + private shown?: number; + + private dismiss = (event: Event) => { + if (this.menu.current && this.menu.current.contains(event.target as Node)) + return; + // prevent toggle buttons from immediately dismissing: + if (this.shown && Date.now() - this.shown < 10) return; + this.setShowing(false); + }; + + private setShowing = (showing?: boolean) => { + this.shown = Date.now(); + if (showing) this.setState({ showing: true, hasShown: true }); + else this.setState({ showing: false }); }; private toggle = () => { - this.setState({ showing: !this.state.showing }); + this.setShowing(!this.state.showing); + }; + + private reposition = () => { + const menu = this.menu.current; + const wrap = this.wrap.current; + if (!menu || !wrap || !this.state.showing) return; + const bbox = wrap.getBoundingClientRect(); + + const { direction = 'down', anchor = 'right' } = this.props; + const { innerWidth, innerHeight } = window; + + const anchorX = has(anchor, 'left') ? bbox.left : bbox.right; + + menu.style.left = menu.style.right = menu.style.top = menu.style.bottom = + ''; + + if (has(direction, 'left')) { + menu.style.right = innerWidth - anchorX + 'px'; + } else { + menu.style.left = anchorX + 'px'; + } + if (has(direction, 'up')) { + const anchorY = has(anchor, 'bottom') ? bbox.bottom : bbox.top; + menu.style.bottom = innerHeight - anchorY + 'px'; + } else { + const anchorY = has(anchor, 'top') ? bbox.top : bbox.bottom; + menu.style.top = anchorY + 'px'; + } }; componentWillReceiveProps({ showing }: Props) { if (showing !== this.props.showing) { - this.setState({ showing }); + this.setShowing(showing); } } + componentDidMount() { + addEventListener('click', this.dismiss, { passive: true }); + addEventListener('resize', this.reposition, { passive: true }); + if (typeof ResizeObserver === 'function' && this.wrap.current) { + this.resizeObserver = new ResizeObserver(this.reposition); + this.resizeObserver.observe(this.wrap.current); + } + if (this.props.showing) this.setShowing(true); + } + + componentWillUnmount() { + removeEventListener('click', this.dismiss); + removeEventListener('resize', this.reposition); + if (this.resizeObserver) this.resizeObserver.disconnect(); + } + componentDidUpdate(prevProps: Props, prevState: State) { if (this.state.showing && !prevState.showing) { const menu = this.menu.current; if (menu) { + this.reposition(); + let toFocus = menu.firstElementChild; for (let child of menu.children) { if (child.hasAttribute('autofocus')) { @@ -79,21 +147,56 @@ export default class Flyout extends Component { const anchorText = Array.isArray(anchor) ? anchor.join(' ') : anchor; return ( - - - {toggle && cloneElement(toggle, toggleProps)} + + {toggle && cloneElement(toggle, toggleProps)} - - + {showing && + createPortal( + , + document.body, + )} ); } } + +// not worth pulling in compat +function createPortal(children: ComponentChildren, parent: Element) { + return {children}; +} +// this is probably overly careful, since it works directly rendering into parent +function createPersistentFragment(parent: Element) { + const frag = { + nodeType: 11, + childNodes: [], + appendChild: parent.appendChild.bind(parent), + insertBefore: parent.insertBefore.bind(parent), + removeChild: parent.removeChild.bind(parent), + }; + return (frag as unknown) as Element; +} +class Portal extends Component<{ + children: ComponentChildren; + parent: Element; +}> { + root = createPersistentFragment(this.props.parent); + componentWillUnmount() { + render(null, this.root); + } + render() { + render({this.props.children}, this.root); + return null; + } +} diff --git a/src/client/lazy-app/Compress/Flyout/style.css b/src/client/lazy-app/Compress/Flyout/style.css index 73541deb..ebae67d7 100644 --- a/src/client/lazy-app/Compress/Flyout/style.css +++ b/src/client/lazy-app/Compress/Flyout/style.css @@ -5,11 +5,9 @@ justify-items: center; } -.wrap > aside:last-of-type { +.flyout { display: inline-block; - position: absolute; - left: 0; - top: 100%; + position: fixed; z-index: 100; display: flex; flex-wrap: nowrap; @@ -25,44 +23,14 @@ display: none; } - /* align to the right edge */ - &[data-anchor*='right'] { - left: 100%; - } - - /* open to the left */ &[data-direction*='left'] { - right: 0; - left: auto; align-items: flex-end; - &[anchor*='right'] { - right: 100%; - } } - /* align to the top edge */ - &[data-anchor*='top'] { - top: 0; - } - - /* open to the left */ &[data-direction*='up'] { - bottom: 100%; - top: auto; - flex-direction: column-reverse; --flyout-offset-y: 20px; - &[data-anchor*='bottom'] { - bottom: 0; - } - } - - /* - @media (min-width: 860px) { flex-direction: column-reverse; - top: auto; - bottom: 100%; } - */ } @keyframes menuOpen { diff --git a/src/client/lazy-app/Compress/Options/index.tsx b/src/client/lazy-app/Compress/Options/index.tsx index dff931d1..ee0efb1b 100644 --- a/src/client/lazy-app/Compress/Options/index.tsx +++ b/src/client/lazy-app/Compress/Options/index.tsx @@ -139,6 +139,7 @@ export default class Options extends Component {

Edit { altBackground: false, transform: false, menuOpen: false, + smallControls: + typeof matchMedia === 'function' && + matchMedia('(max-width: 859px)').matches, }; canvasLeft?: HTMLCanvasElement; canvasRight?: HTMLCanvasElement; @@ -84,6 +88,12 @@ export default class Output extends Component { if (this.canvasRight && rightDraw) { drawDataToCanvas(this.canvasRight, rightDraw); } + + if (typeof matchMedia === 'function') { + matchMedia('(max-width: 859px)').addEventListener('change', (e) => + this.setState({ smallControls: e.matches }), + ); + } } componentDidUpdate(prevProps: Props, prevState: State) { @@ -278,7 +288,7 @@ export default class Output extends Component { onShowPreprocessorTransforms, onToggleBackground, }: Props, - { scale, editingScale }: State, + { scale, editingScale, smallControls }: State, ) { const leftDraw = this.leftDrawable(); const rightDraw = this.rightDrawable(); @@ -370,7 +380,7 @@ export default class Output extends Component { class={style.menu} showing={hidden ? false : undefined} anchor="right" - direction={mobileView ? ['down', 'left'] : 'up'} + direction={smallControls ? ['down', 'left'] : 'up'} toggle={