1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2023 The Android Open Source Project 2*6dbdd20aSAndroid Build Coastguard Worker// 3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License"); 4*6dbdd20aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License. 5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at 6*6dbdd20aSAndroid Build Coastguard Worker// 7*6dbdd20aSAndroid Build Coastguard Worker// http://www.apache.org/licenses/LICENSE-2.0 8*6dbdd20aSAndroid Build Coastguard Worker// 9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software 10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS, 11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and 13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License. 14*6dbdd20aSAndroid Build Coastguard Worker 15*6dbdd20aSAndroid Build Coastguard Workerimport {createPopper, Instance, OptionsGeneric} from '@popperjs/core'; 16*6dbdd20aSAndroid Build Coastguard Workerimport type {Modifier, StrictModifiers} from '@popperjs/core'; 17*6dbdd20aSAndroid Build Coastguard Workerimport m from 'mithril'; 18*6dbdd20aSAndroid Build Coastguard Workerimport {MountOptions, Portal, PortalAttrs} from './portal'; 19*6dbdd20aSAndroid Build Coastguard Workerimport {classNames} from '../base/classnames'; 20*6dbdd20aSAndroid Build Coastguard Workerimport {findRef, isOrContains, toHTMLElement} from '../base/dom_utils'; 21*6dbdd20aSAndroid Build Coastguard Workerimport {assertExists} from '../base/logging'; 22*6dbdd20aSAndroid Build Coastguard Workerimport {scheduleFullRedraw} from './raf'; 23*6dbdd20aSAndroid Build Coastguard Worker 24*6dbdd20aSAndroid Build Coastguard Workertype CustomModifier = Modifier<'sameWidth', {}>; 25*6dbdd20aSAndroid Build Coastguard Workertype ExtendedModifiers = StrictModifiers | CustomModifier; 26*6dbdd20aSAndroid Build Coastguard Worker 27*6dbdd20aSAndroid Build Coastguard Worker// Note: We could just use the Placement type from popper.js instead, which is a 28*6dbdd20aSAndroid Build Coastguard Worker// union of string literals corresponding to the values in this enum, but having 29*6dbdd20aSAndroid Build Coastguard Worker// the emun makes it possible to enumerate the possible options, which is a 30*6dbdd20aSAndroid Build Coastguard Worker// feature used in the widgets page. 31*6dbdd20aSAndroid Build Coastguard Workerexport enum PopupPosition { 32*6dbdd20aSAndroid Build Coastguard Worker Auto = 'auto', 33*6dbdd20aSAndroid Build Coastguard Worker AutoStart = 'auto-start', 34*6dbdd20aSAndroid Build Coastguard Worker AutoEnd = 'auto-end', 35*6dbdd20aSAndroid Build Coastguard Worker Top = 'top', 36*6dbdd20aSAndroid Build Coastguard Worker TopStart = 'top-start', 37*6dbdd20aSAndroid Build Coastguard Worker TopEnd = 'top-end', 38*6dbdd20aSAndroid Build Coastguard Worker Bottom = 'bottom', 39*6dbdd20aSAndroid Build Coastguard Worker BottomStart = 'bottom-start', 40*6dbdd20aSAndroid Build Coastguard Worker BottomEnd = 'bottom-end', 41*6dbdd20aSAndroid Build Coastguard Worker Right = 'right', 42*6dbdd20aSAndroid Build Coastguard Worker RightStart = 'right-start', 43*6dbdd20aSAndroid Build Coastguard Worker RightEnd = 'right-end', 44*6dbdd20aSAndroid Build Coastguard Worker Left = 'left', 45*6dbdd20aSAndroid Build Coastguard Worker LeftStart = 'left-start', 46*6dbdd20aSAndroid Build Coastguard Worker LeftEnd = 'left-end', 47*6dbdd20aSAndroid Build Coastguard Worker} 48*6dbdd20aSAndroid Build Coastguard Worker 49*6dbdd20aSAndroid Build Coastguard Workertype OnChangeCallback = (shouldOpen: boolean) => void; 50*6dbdd20aSAndroid Build Coastguard Worker 51*6dbdd20aSAndroid Build Coastguard Workerexport interface PopupAttrs { 52*6dbdd20aSAndroid Build Coastguard Worker // Which side of the trigger to place to popup. 53*6dbdd20aSAndroid Build Coastguard Worker // Defaults to "Auto" 54*6dbdd20aSAndroid Build Coastguard Worker position?: PopupPosition; 55*6dbdd20aSAndroid Build Coastguard Worker // The element used to open and close the popup, and the target which the near 56*6dbdd20aSAndroid Build Coastguard Worker // which the popup should hover. 57*6dbdd20aSAndroid Build Coastguard Worker // Beware this element will have its `onclick`, `ref`, and `active` attributes 58*6dbdd20aSAndroid Build Coastguard Worker // overwritten. 59*6dbdd20aSAndroid Build Coastguard Worker // eslint-disable-next-line @typescript-eslint/no-explicit-any 60*6dbdd20aSAndroid Build Coastguard Worker trigger: m.Vnode<any, any>; 61*6dbdd20aSAndroid Build Coastguard Worker // Close when the escape key is pressed 62*6dbdd20aSAndroid Build Coastguard Worker // Defaults to true. 63*6dbdd20aSAndroid Build Coastguard Worker closeOnEscape?: boolean; 64*6dbdd20aSAndroid Build Coastguard Worker // Close on mouse down somewhere other than the popup or trigger. 65*6dbdd20aSAndroid Build Coastguard Worker // Defaults to true. 66*6dbdd20aSAndroid Build Coastguard Worker closeOnOutsideClick?: boolean; 67*6dbdd20aSAndroid Build Coastguard Worker // Controls whether the popup is open or not. 68*6dbdd20aSAndroid Build Coastguard Worker // If omitted, the popup operates in uncontrolled mode. 69*6dbdd20aSAndroid Build Coastguard Worker isOpen?: boolean; 70*6dbdd20aSAndroid Build Coastguard Worker // Called when the popup isOpen state should be changed in controlled mode. 71*6dbdd20aSAndroid Build Coastguard Worker onChange?: OnChangeCallback; 72*6dbdd20aSAndroid Build Coastguard Worker // Space delimited class names applied to the popup div. 73*6dbdd20aSAndroid Build Coastguard Worker className?: string; 74*6dbdd20aSAndroid Build Coastguard Worker // Whether to show a little arrow pointing to our trigger element. 75*6dbdd20aSAndroid Build Coastguard Worker // Defaults to true. 76*6dbdd20aSAndroid Build Coastguard Worker showArrow?: boolean; 77*6dbdd20aSAndroid Build Coastguard Worker // Whether this popup should form a new popup group. 78*6dbdd20aSAndroid Build Coastguard Worker // When nesting popups, grouping controls how popups are closed. 79*6dbdd20aSAndroid Build Coastguard Worker // When closing popups via the Escape key, each group is closed one by one, 80*6dbdd20aSAndroid Build Coastguard Worker // starting at the topmost group in the stack. 81*6dbdd20aSAndroid Build Coastguard Worker // When using a magic button to close groups (see DISMISS_POPUP_GROUP_CLASS), 82*6dbdd20aSAndroid Build Coastguard Worker // only the group in which the button lives and it's children will be closed. 83*6dbdd20aSAndroid Build Coastguard Worker // Defaults to true. 84*6dbdd20aSAndroid Build Coastguard Worker createNewGroup?: boolean; 85*6dbdd20aSAndroid Build Coastguard Worker // Called when the popup mounts, passing the popup's dom element. 86*6dbdd20aSAndroid Build Coastguard Worker onPopupMount?: (dom: HTMLElement) => void; 87*6dbdd20aSAndroid Build Coastguard Worker // Called when the popup unmounts, padding the popup's dom element. 88*6dbdd20aSAndroid Build Coastguard Worker onPopupUnMount?: (dom: HTMLElement) => void; 89*6dbdd20aSAndroid Build Coastguard Worker // Popup matches the width of the trigger element. Default = false. 90*6dbdd20aSAndroid Build Coastguard Worker matchWidth?: boolean; 91*6dbdd20aSAndroid Build Coastguard Worker // Distance in px between the popup and its trigger. Default = 0. 92*6dbdd20aSAndroid Build Coastguard Worker offset?: number; 93*6dbdd20aSAndroid Build Coastguard Worker // Cross-axial popup offset in px. Defaults to 0. 94*6dbdd20aSAndroid Build Coastguard Worker // When position is *-end or *-start, this setting specifies where start and 95*6dbdd20aSAndroid Build Coastguard Worker // end is as an offset from the edge of the popup. 96*6dbdd20aSAndroid Build Coastguard Worker // Positive values move the positioning away from the edge towards the center 97*6dbdd20aSAndroid Build Coastguard Worker // of the popup. 98*6dbdd20aSAndroid Build Coastguard Worker // If position is not *-end or *-start, this setting has no effect. 99*6dbdd20aSAndroid Build Coastguard Worker edgeOffset?: number; 100*6dbdd20aSAndroid Build Coastguard Worker} 101*6dbdd20aSAndroid Build Coastguard Worker 102*6dbdd20aSAndroid Build Coastguard Worker// A popup is a portal whose position is dynamically updated so that it floats 103*6dbdd20aSAndroid Build Coastguard Worker// next to a trigger element. It is also styled with a nice backdrop, and 104*6dbdd20aSAndroid Build Coastguard Worker// a little arrow pointing at the trigger element. 105*6dbdd20aSAndroid Build Coastguard Worker// Useful for displaying things like popup menus. 106*6dbdd20aSAndroid Build Coastguard Workerexport class Popup implements m.ClassComponent<PopupAttrs> { 107*6dbdd20aSAndroid Build Coastguard Worker private isOpen: boolean = false; 108*6dbdd20aSAndroid Build Coastguard Worker private triggerElement?: Element; 109*6dbdd20aSAndroid Build Coastguard Worker private popupElement?: HTMLElement; 110*6dbdd20aSAndroid Build Coastguard Worker private popper?: Instance; 111*6dbdd20aSAndroid Build Coastguard Worker private onChange: OnChangeCallback = () => {}; 112*6dbdd20aSAndroid Build Coastguard Worker private closeOnEscape?: boolean; 113*6dbdd20aSAndroid Build Coastguard Worker private closeOnOutsideClick?: boolean; 114*6dbdd20aSAndroid Build Coastguard Worker 115*6dbdd20aSAndroid Build Coastguard Worker private static readonly TRIGGER_REF = 'trigger'; 116*6dbdd20aSAndroid Build Coastguard Worker private static readonly POPUP_REF = 'popup'; 117*6dbdd20aSAndroid Build Coastguard Worker static readonly POPUP_GROUP_CLASS = 'pf-popup-group'; 118*6dbdd20aSAndroid Build Coastguard Worker 119*6dbdd20aSAndroid Build Coastguard Worker // Any element with this class will close its containing popup group on click 120*6dbdd20aSAndroid Build Coastguard Worker static readonly DISMISS_POPUP_GROUP_CLASS = 'pf-dismiss-popup-group'; 121*6dbdd20aSAndroid Build Coastguard Worker 122*6dbdd20aSAndroid Build Coastguard Worker view({attrs, children}: m.CVnode<PopupAttrs>): m.Children { 123*6dbdd20aSAndroid Build Coastguard Worker const { 124*6dbdd20aSAndroid Build Coastguard Worker trigger, 125*6dbdd20aSAndroid Build Coastguard Worker isOpen = this.isOpen, 126*6dbdd20aSAndroid Build Coastguard Worker onChange = () => {}, 127*6dbdd20aSAndroid Build Coastguard Worker closeOnEscape = true, 128*6dbdd20aSAndroid Build Coastguard Worker closeOnOutsideClick = true, 129*6dbdd20aSAndroid Build Coastguard Worker } = attrs; 130*6dbdd20aSAndroid Build Coastguard Worker 131*6dbdd20aSAndroid Build Coastguard Worker this.isOpen = isOpen; 132*6dbdd20aSAndroid Build Coastguard Worker this.onChange = onChange; 133*6dbdd20aSAndroid Build Coastguard Worker this.closeOnEscape = closeOnEscape; 134*6dbdd20aSAndroid Build Coastguard Worker this.closeOnOutsideClick = closeOnOutsideClick; 135*6dbdd20aSAndroid Build Coastguard Worker 136*6dbdd20aSAndroid Build Coastguard Worker return [ 137*6dbdd20aSAndroid Build Coastguard Worker this.renderTrigger(trigger), 138*6dbdd20aSAndroid Build Coastguard Worker isOpen && this.renderPopup(attrs, children), 139*6dbdd20aSAndroid Build Coastguard Worker ]; 140*6dbdd20aSAndroid Build Coastguard Worker } 141*6dbdd20aSAndroid Build Coastguard Worker 142*6dbdd20aSAndroid Build Coastguard Worker // eslint-disable-next-line @typescript-eslint/no-explicit-any 143*6dbdd20aSAndroid Build Coastguard Worker private renderTrigger(trigger: m.Vnode<any, any>): m.Children { 144*6dbdd20aSAndroid Build Coastguard Worker trigger.attrs = { 145*6dbdd20aSAndroid Build Coastguard Worker ...trigger.attrs, 146*6dbdd20aSAndroid Build Coastguard Worker ref: Popup.TRIGGER_REF, 147*6dbdd20aSAndroid Build Coastguard Worker onclick: (e: MouseEvent) => { 148*6dbdd20aSAndroid Build Coastguard Worker this.togglePopup(); 149*6dbdd20aSAndroid Build Coastguard Worker e.preventDefault(); 150*6dbdd20aSAndroid Build Coastguard Worker }, 151*6dbdd20aSAndroid Build Coastguard Worker active: this.isOpen, 152*6dbdd20aSAndroid Build Coastguard Worker }; 153*6dbdd20aSAndroid Build Coastguard Worker return trigger; 154*6dbdd20aSAndroid Build Coastguard Worker } 155*6dbdd20aSAndroid Build Coastguard Worker 156*6dbdd20aSAndroid Build Coastguard Worker // eslint-disable-next-line @typescript-eslint/no-explicit-any 157*6dbdd20aSAndroid Build Coastguard Worker private renderPopup(attrs: PopupAttrs, children: any): m.Children { 158*6dbdd20aSAndroid Build Coastguard Worker const { 159*6dbdd20aSAndroid Build Coastguard Worker className, 160*6dbdd20aSAndroid Build Coastguard Worker showArrow = true, 161*6dbdd20aSAndroid Build Coastguard Worker createNewGroup = true, 162*6dbdd20aSAndroid Build Coastguard Worker onPopupMount = () => {}, 163*6dbdd20aSAndroid Build Coastguard Worker onPopupUnMount = () => {}, 164*6dbdd20aSAndroid Build Coastguard Worker } = attrs; 165*6dbdd20aSAndroid Build Coastguard Worker 166*6dbdd20aSAndroid Build Coastguard Worker const portalAttrs: PortalAttrs = { 167*6dbdd20aSAndroid Build Coastguard Worker className: 'pf-popup-portal', 168*6dbdd20aSAndroid Build Coastguard Worker onBeforeContentMount: (dom: Element): MountOptions => { 169*6dbdd20aSAndroid Build Coastguard Worker // Check to see if dom is a descendant of a popup 170*6dbdd20aSAndroid Build Coastguard Worker // If so, get the popup's "container" and put it in there instead 171*6dbdd20aSAndroid Build Coastguard Worker // This handles the case where popups are placed inside the other popups 172*6dbdd20aSAndroid Build Coastguard Worker // we nest outselves in their containers instead of document body which 173*6dbdd20aSAndroid Build Coastguard Worker // means we become part of their hitbox for mouse events. 174*6dbdd20aSAndroid Build Coastguard Worker const closestPopup = dom.closest(`[ref=${Popup.POPUP_REF}]`); 175*6dbdd20aSAndroid Build Coastguard Worker return {container: closestPopup ?? undefined}; 176*6dbdd20aSAndroid Build Coastguard Worker }, 177*6dbdd20aSAndroid Build Coastguard Worker onContentMount: (dom: HTMLElement) => { 178*6dbdd20aSAndroid Build Coastguard Worker const popupElement = toHTMLElement( 179*6dbdd20aSAndroid Build Coastguard Worker assertExists(findRef(dom, Popup.POPUP_REF)), 180*6dbdd20aSAndroid Build Coastguard Worker ); 181*6dbdd20aSAndroid Build Coastguard Worker this.popupElement = popupElement; 182*6dbdd20aSAndroid Build Coastguard Worker this.createOrUpdatePopper(attrs); 183*6dbdd20aSAndroid Build Coastguard Worker document.addEventListener('mousedown', this.handleDocMouseDown); 184*6dbdd20aSAndroid Build Coastguard Worker document.addEventListener('keydown', this.handleDocKeyPress); 185*6dbdd20aSAndroid Build Coastguard Worker dom.addEventListener('click', this.handleContentClick); 186*6dbdd20aSAndroid Build Coastguard Worker onPopupMount(popupElement); 187*6dbdd20aSAndroid Build Coastguard Worker }, 188*6dbdd20aSAndroid Build Coastguard Worker onContentUpdate: () => { 189*6dbdd20aSAndroid Build Coastguard Worker // The content inside the portal has updated, so we call popper to 190*6dbdd20aSAndroid Build Coastguard Worker // recompute the popup's position, in case it has changed size. 191*6dbdd20aSAndroid Build Coastguard Worker this.popper && this.popper.update(); 192*6dbdd20aSAndroid Build Coastguard Worker }, 193*6dbdd20aSAndroid Build Coastguard Worker onContentUnmount: (dom: HTMLElement) => { 194*6dbdd20aSAndroid Build Coastguard Worker if (this.popupElement) { 195*6dbdd20aSAndroid Build Coastguard Worker onPopupUnMount(this.popupElement); 196*6dbdd20aSAndroid Build Coastguard Worker } 197*6dbdd20aSAndroid Build Coastguard Worker dom.removeEventListener('click', this.handleContentClick); 198*6dbdd20aSAndroid Build Coastguard Worker document.removeEventListener('keydown', this.handleDocKeyPress); 199*6dbdd20aSAndroid Build Coastguard Worker document.removeEventListener('mousedown', this.handleDocMouseDown); 200*6dbdd20aSAndroid Build Coastguard Worker this.popper && this.popper.destroy(); 201*6dbdd20aSAndroid Build Coastguard Worker this.popper = undefined; 202*6dbdd20aSAndroid Build Coastguard Worker this.popupElement = undefined; 203*6dbdd20aSAndroid Build Coastguard Worker }, 204*6dbdd20aSAndroid Build Coastguard Worker }; 205*6dbdd20aSAndroid Build Coastguard Worker 206*6dbdd20aSAndroid Build Coastguard Worker return m( 207*6dbdd20aSAndroid Build Coastguard Worker Portal, 208*6dbdd20aSAndroid Build Coastguard Worker portalAttrs, 209*6dbdd20aSAndroid Build Coastguard Worker m( 210*6dbdd20aSAndroid Build Coastguard Worker '.pf-popup', 211*6dbdd20aSAndroid Build Coastguard Worker { 212*6dbdd20aSAndroid Build Coastguard Worker class: classNames( 213*6dbdd20aSAndroid Build Coastguard Worker className, 214*6dbdd20aSAndroid Build Coastguard Worker createNewGroup && Popup.POPUP_GROUP_CLASS, 215*6dbdd20aSAndroid Build Coastguard Worker ), 216*6dbdd20aSAndroid Build Coastguard Worker ref: Popup.POPUP_REF, 217*6dbdd20aSAndroid Build Coastguard Worker }, 218*6dbdd20aSAndroid Build Coastguard Worker showArrow && m('.pf-popup-arrow[data-popper-arrow]'), 219*6dbdd20aSAndroid Build Coastguard Worker m('.pf-popup-content', children), 220*6dbdd20aSAndroid Build Coastguard Worker ), 221*6dbdd20aSAndroid Build Coastguard Worker ); 222*6dbdd20aSAndroid Build Coastguard Worker } 223*6dbdd20aSAndroid Build Coastguard Worker 224*6dbdd20aSAndroid Build Coastguard Worker oncreate({dom}: m.VnodeDOM<PopupAttrs, this>) { 225*6dbdd20aSAndroid Build Coastguard Worker this.triggerElement = assertExists(findRef(dom, Popup.TRIGGER_REF)); 226*6dbdd20aSAndroid Build Coastguard Worker } 227*6dbdd20aSAndroid Build Coastguard Worker 228*6dbdd20aSAndroid Build Coastguard Worker onupdate({attrs}: m.VnodeDOM<PopupAttrs, this>) { 229*6dbdd20aSAndroid Build Coastguard Worker // We might have some new popper options, or the trigger might have changed 230*6dbdd20aSAndroid Build Coastguard Worker // size, so we call popper to recompute the popup's position. 231*6dbdd20aSAndroid Build Coastguard Worker this.createOrUpdatePopper(attrs); 232*6dbdd20aSAndroid Build Coastguard Worker } 233*6dbdd20aSAndroid Build Coastguard Worker 234*6dbdd20aSAndroid Build Coastguard Worker onremove(_: m.VnodeDOM<PopupAttrs, this>) { 235*6dbdd20aSAndroid Build Coastguard Worker this.triggerElement = undefined; 236*6dbdd20aSAndroid Build Coastguard Worker } 237*6dbdd20aSAndroid Build Coastguard Worker 238*6dbdd20aSAndroid Build Coastguard Worker private createOrUpdatePopper(attrs: PopupAttrs) { 239*6dbdd20aSAndroid Build Coastguard Worker const { 240*6dbdd20aSAndroid Build Coastguard Worker position = PopupPosition.Auto, 241*6dbdd20aSAndroid Build Coastguard Worker showArrow = true, 242*6dbdd20aSAndroid Build Coastguard Worker matchWidth = false, 243*6dbdd20aSAndroid Build Coastguard Worker offset = 0, 244*6dbdd20aSAndroid Build Coastguard Worker edgeOffset = 0, 245*6dbdd20aSAndroid Build Coastguard Worker } = attrs; 246*6dbdd20aSAndroid Build Coastguard Worker 247*6dbdd20aSAndroid Build Coastguard Worker let matchWidthModifier: Modifier<'sameWidth', {}>[]; 248*6dbdd20aSAndroid Build Coastguard Worker if (matchWidth) { 249*6dbdd20aSAndroid Build Coastguard Worker matchWidthModifier = [ 250*6dbdd20aSAndroid Build Coastguard Worker { 251*6dbdd20aSAndroid Build Coastguard Worker name: 'sameWidth', 252*6dbdd20aSAndroid Build Coastguard Worker enabled: true, 253*6dbdd20aSAndroid Build Coastguard Worker phase: 'beforeWrite', 254*6dbdd20aSAndroid Build Coastguard Worker requires: ['computeStyles'], 255*6dbdd20aSAndroid Build Coastguard Worker fn: ({state}) => { 256*6dbdd20aSAndroid Build Coastguard Worker state.styles.popper.width = `${state.rects.reference.width}px`; 257*6dbdd20aSAndroid Build Coastguard Worker }, 258*6dbdd20aSAndroid Build Coastguard Worker effect: ({state}) => { 259*6dbdd20aSAndroid Build Coastguard Worker const trigger = state.elements.reference as HTMLElement; 260*6dbdd20aSAndroid Build Coastguard Worker state.elements.popper.style.width = `${trigger.offsetWidth}px`; 261*6dbdd20aSAndroid Build Coastguard Worker }, 262*6dbdd20aSAndroid Build Coastguard Worker }, 263*6dbdd20aSAndroid Build Coastguard Worker ]; 264*6dbdd20aSAndroid Build Coastguard Worker } else { 265*6dbdd20aSAndroid Build Coastguard Worker matchWidthModifier = []; 266*6dbdd20aSAndroid Build Coastguard Worker } 267*6dbdd20aSAndroid Build Coastguard Worker 268*6dbdd20aSAndroid Build Coastguard Worker const options: Partial<OptionsGeneric<ExtendedModifiers>> = { 269*6dbdd20aSAndroid Build Coastguard Worker placement: position, 270*6dbdd20aSAndroid Build Coastguard Worker modifiers: [ 271*6dbdd20aSAndroid Build Coastguard Worker // Move the popup away from the target allowing room for the arrow 272*6dbdd20aSAndroid Build Coastguard Worker { 273*6dbdd20aSAndroid Build Coastguard Worker name: 'offset', 274*6dbdd20aSAndroid Build Coastguard Worker options: { 275*6dbdd20aSAndroid Build Coastguard Worker offset: ({placement}) => { 276*6dbdd20aSAndroid Build Coastguard Worker let skid = 0; 277*6dbdd20aSAndroid Build Coastguard Worker if (placement.includes('-end')) { 278*6dbdd20aSAndroid Build Coastguard Worker skid = edgeOffset; 279*6dbdd20aSAndroid Build Coastguard Worker } else if (placement.includes('-start')) { 280*6dbdd20aSAndroid Build Coastguard Worker skid = -edgeOffset; 281*6dbdd20aSAndroid Build Coastguard Worker } 282*6dbdd20aSAndroid Build Coastguard Worker return [skid, showArrow ? offset + 8 : offset]; 283*6dbdd20aSAndroid Build Coastguard Worker }, 284*6dbdd20aSAndroid Build Coastguard Worker }, 285*6dbdd20aSAndroid Build Coastguard Worker }, 286*6dbdd20aSAndroid Build Coastguard Worker // Don't let the popup touch the edge of the viewport 287*6dbdd20aSAndroid Build Coastguard Worker {name: 'preventOverflow', options: {padding: 8}}, 288*6dbdd20aSAndroid Build Coastguard Worker // Don't let the arrow reach the end of the popup, which looks odd when 289*6dbdd20aSAndroid Build Coastguard Worker // the popup has rounded corners 290*6dbdd20aSAndroid Build Coastguard Worker {name: 'arrow', options: {padding: 2}}, 291*6dbdd20aSAndroid Build Coastguard Worker ...matchWidthModifier, 292*6dbdd20aSAndroid Build Coastguard Worker ], 293*6dbdd20aSAndroid Build Coastguard Worker }; 294*6dbdd20aSAndroid Build Coastguard Worker 295*6dbdd20aSAndroid Build Coastguard Worker if (this.popper) { 296*6dbdd20aSAndroid Build Coastguard Worker this.popper.setOptions(options); 297*6dbdd20aSAndroid Build Coastguard Worker } else { 298*6dbdd20aSAndroid Build Coastguard Worker if (this.popupElement && this.triggerElement) { 299*6dbdd20aSAndroid Build Coastguard Worker this.popper = createPopper<ExtendedModifiers>( 300*6dbdd20aSAndroid Build Coastguard Worker this.triggerElement, 301*6dbdd20aSAndroid Build Coastguard Worker this.popupElement, 302*6dbdd20aSAndroid Build Coastguard Worker options, 303*6dbdd20aSAndroid Build Coastguard Worker ); 304*6dbdd20aSAndroid Build Coastguard Worker } 305*6dbdd20aSAndroid Build Coastguard Worker } 306*6dbdd20aSAndroid Build Coastguard Worker } 307*6dbdd20aSAndroid Build Coastguard Worker 308*6dbdd20aSAndroid Build Coastguard Worker private eventInPopupOrTrigger(e: Event): boolean { 309*6dbdd20aSAndroid Build Coastguard Worker const target = e.target as HTMLElement; 310*6dbdd20aSAndroid Build Coastguard Worker const onTrigger = isOrContains(assertExists(this.triggerElement), target); 311*6dbdd20aSAndroid Build Coastguard Worker const onPopup = isOrContains(assertExists(this.popupElement), target); 312*6dbdd20aSAndroid Build Coastguard Worker return onTrigger || onPopup; 313*6dbdd20aSAndroid Build Coastguard Worker } 314*6dbdd20aSAndroid Build Coastguard Worker 315*6dbdd20aSAndroid Build Coastguard Worker private handleDocMouseDown = (e: Event) => { 316*6dbdd20aSAndroid Build Coastguard Worker if (this.closeOnOutsideClick && !this.eventInPopupOrTrigger(e)) { 317*6dbdd20aSAndroid Build Coastguard Worker this.closePopup(); 318*6dbdd20aSAndroid Build Coastguard Worker } 319*6dbdd20aSAndroid Build Coastguard Worker }; 320*6dbdd20aSAndroid Build Coastguard Worker 321*6dbdd20aSAndroid Build Coastguard Worker private handleDocKeyPress = (e: KeyboardEvent) => { 322*6dbdd20aSAndroid Build Coastguard Worker // Close on escape keypress if we are in the toplevel group 323*6dbdd20aSAndroid Build Coastguard Worker const nextGroupElement = this.popupElement?.querySelector( 324*6dbdd20aSAndroid Build Coastguard Worker `.${Popup.POPUP_GROUP_CLASS}`, 325*6dbdd20aSAndroid Build Coastguard Worker ); 326*6dbdd20aSAndroid Build Coastguard Worker if (!nextGroupElement) { 327*6dbdd20aSAndroid Build Coastguard Worker if (this.closeOnEscape && e.key === 'Escape') { 328*6dbdd20aSAndroid Build Coastguard Worker this.closePopup(); 329*6dbdd20aSAndroid Build Coastguard Worker } 330*6dbdd20aSAndroid Build Coastguard Worker } 331*6dbdd20aSAndroid Build Coastguard Worker }; 332*6dbdd20aSAndroid Build Coastguard Worker 333*6dbdd20aSAndroid Build Coastguard Worker private handleContentClick = (e: Event) => { 334*6dbdd20aSAndroid Build Coastguard Worker // Close the popup if the clicked element: 335*6dbdd20aSAndroid Build Coastguard Worker // - Is in the same group as this class 336*6dbdd20aSAndroid Build Coastguard Worker // - Has the magic class 337*6dbdd20aSAndroid Build Coastguard Worker const target = e.target as HTMLElement; 338*6dbdd20aSAndroid Build Coastguard Worker const childPopup = this.popupElement?.querySelector( 339*6dbdd20aSAndroid Build Coastguard Worker `.${Popup.POPUP_GROUP_CLASS}`, 340*6dbdd20aSAndroid Build Coastguard Worker ); 341*6dbdd20aSAndroid Build Coastguard Worker if (childPopup) { 342*6dbdd20aSAndroid Build Coastguard Worker if (childPopup.contains(target)) { 343*6dbdd20aSAndroid Build Coastguard Worker return; 344*6dbdd20aSAndroid Build Coastguard Worker } 345*6dbdd20aSAndroid Build Coastguard Worker } 346*6dbdd20aSAndroid Build Coastguard Worker if (target.closest(`.${Popup.DISMISS_POPUP_GROUP_CLASS}`)) { 347*6dbdd20aSAndroid Build Coastguard Worker this.closePopup(); 348*6dbdd20aSAndroid Build Coastguard Worker } 349*6dbdd20aSAndroid Build Coastguard Worker }; 350*6dbdd20aSAndroid Build Coastguard Worker 351*6dbdd20aSAndroid Build Coastguard Worker private closePopup() { 352*6dbdd20aSAndroid Build Coastguard Worker if (this.isOpen) { 353*6dbdd20aSAndroid Build Coastguard Worker this.isOpen = false; 354*6dbdd20aSAndroid Build Coastguard Worker this.onChange(this.isOpen); 355*6dbdd20aSAndroid Build Coastguard Worker scheduleFullRedraw('force'); 356*6dbdd20aSAndroid Build Coastguard Worker } 357*6dbdd20aSAndroid Build Coastguard Worker } 358*6dbdd20aSAndroid Build Coastguard Worker 359*6dbdd20aSAndroid Build Coastguard Worker private togglePopup() { 360*6dbdd20aSAndroid Build Coastguard Worker this.isOpen = !this.isOpen; 361*6dbdd20aSAndroid Build Coastguard Worker this.onChange(this.isOpen); 362*6dbdd20aSAndroid Build Coastguard Worker scheduleFullRedraw('force'); 363*6dbdd20aSAndroid Build Coastguard Worker } 364*6dbdd20aSAndroid Build Coastguard Worker} 365