@operato/popup 8.0.0-alpha.51 → 8.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ox-popup.ts DELETED
@@ -1,400 +0,0 @@
1
- import { css, html, LitElement, nothing } from 'lit'
2
- import { render } from 'lit-html'
3
- import { customElement, property, state } from 'lit/decorators.js'
4
-
5
- import { ScrollbarStyles } from '@operato/styles'
6
-
7
- import { convertToFixedPosition } from './position-converter.js'
8
-
9
- declare global {
10
- interface Window {
11
- POPUP_DEBUG?: boolean
12
- }
13
- }
14
-
15
- /**
16
- * Custom element class representing the 'ox-popup' component.
17
- *
18
- * This component provides the functionality to display a popup with various configuration options.
19
- * It can be used to show additional information, notifications, or user interactions within a web application.
20
- *
21
- * @fires ox-close - Dispatched when the popup is closed.
22
- * @fires ox-collapse - Dispatched when the popup is collapsed.
23
- */
24
- @customElement('ox-popup')
25
- export class OxPopup extends LitElement {
26
- static styles = [
27
- ScrollbarStyles,
28
- css`
29
- :host {
30
- position: absolute;
31
- display: none;
32
- z-index: 1000; /* Increased z-index to ensure it's on top */
33
- padding: 0;
34
- box-shadow: 2px 3px 10px 5px rgba(0, 0, 0, 0.15);
35
- box-sizing: border-box;
36
- min-width: fit-content;
37
- line-height: initial;
38
- text-align: initial;
39
- background-color: var(--ox-popup-list-background-color, var(--md-sys-color-surface));
40
- color: var(--ox-popup-list-color, var(--md-sys-color-on-surface));
41
- margin: 0;
42
- }
43
-
44
- :host([active]) {
45
- display: flex;
46
- flex-direction: column;
47
- }
48
-
49
- :host(*:focus) {
50
- outline: none;
51
- }
52
-
53
- #backdrop {
54
- position: fixed;
55
- top: -100vh;
56
- left: -100vw;
57
- width: 300vw;
58
- height: 300vh;
59
- background-color: black;
60
- opacity: 0.5;
61
- }
62
- `
63
- ]
64
-
65
- @property({ type: Boolean, attribute: 'prevent-close-on-blur' }) preventCloseOnBlur: boolean = false
66
- @property({ type: Boolean, attribute: 'backdrop' }) backdrop: boolean = false
67
-
68
- private lastActive: HTMLElement = document.activeElement as HTMLElement
69
- protected removeAfterUse: boolean = false
70
-
71
- render() {
72
- return html`
73
- ${this.backdrop ? html`<div id="backdrop" @click=${() => this.close()}></div>` : nothing}
74
- <slot></slot>
75
- `
76
- }
77
-
78
- protected _onfocusout: (e: FocusEvent) => void = function (this: OxPopup, e: FocusEvent) {
79
- const to = e.relatedTarget as HTMLElement
80
-
81
- if (!this.contains(to)) {
82
- !this.preventCloseOnBlur && !window.POPUP_DEBUG && this.close()
83
- }
84
- }.bind(this)
85
-
86
- protected _onkeydown: (e: KeyboardEvent) => void = function (this: OxPopup, e: KeyboardEvent) {
87
- e.stopPropagation()
88
-
89
- switch (e.key) {
90
- case 'Esc': // for IE/Edge
91
- case 'Escape':
92
- this.close()
93
- break
94
- }
95
- }.bind(this)
96
-
97
- protected _onkeyup: (e: KeyboardEvent) => void = function (this: OxPopup, e: KeyboardEvent) {
98
- e.stopPropagation()
99
- }.bind(this)
100
-
101
- protected _onmouseup: (e: MouseEvent) => void = function (this: OxPopup, e: MouseEvent) {
102
- e.stopPropagation()
103
- }.bind(this)
104
-
105
- protected _onmousedown: (e: MouseEvent) => void = function (this: OxPopup, e: MouseEvent) {
106
- e.stopPropagation()
107
- }.bind(this)
108
-
109
- protected _oncontextmenu: (e: Event) => void = function (this: OxPopup, e: Event) {
110
- e.stopPropagation()
111
- // this.close()
112
- }.bind(this)
113
-
114
- protected _onclick: (e: MouseEvent) => void = function (this: OxPopup, e: MouseEvent) {
115
- e.stopPropagation()
116
- }.bind(this)
117
-
118
- protected _onclose: (e: Event) => void = function (this: OxPopup, e: Event) {
119
- this.close()
120
- }.bind(this)
121
-
122
- protected _oncollapse: (e: Event) => void = function (this: OxPopup, e: Event) {
123
- e.stopPropagation()
124
- this.close()
125
- }.bind(this)
126
-
127
- protected _onwindowblur: (e: Event) => void = function (this: OxPopup, e: Event) {
128
- !this.preventCloseOnBlur && !window.POPUP_DEBUG && this.close()
129
- }.bind(this)
130
-
131
- connectedCallback() {
132
- super.connectedCallback()
133
-
134
- this.addEventListener('focusout', this._onfocusout)
135
- this.addEventListener('keydown', this._onkeydown)
136
- this.addEventListener('keyup', this._onkeyup)
137
- this.addEventListener('click', this._onclick)
138
- this.addEventListener('mouseup', this._onmouseup)
139
- this.addEventListener('mousedown', this._onmousedown)
140
- this.addEventListener('contextmenu', this._oncontextmenu)
141
- this.addEventListener('ox-close', this._onclose)
142
- this.addEventListener('ox-collapse', this._oncollapse)
143
-
144
- this.setAttribute('tabindex', '0') // make this element focusable
145
- this.guaranteeFocus()
146
- }
147
-
148
- /**
149
- * Configuration for opening ox-popup
150
- *
151
- * @typedef {Object} PopupOpenOptions
152
- * @property {HTMLTemplate} template HTMLTemplate to be displayed inside the popup
153
- * @property {Number} top The position-top where the pop-up will be displayed
154
- * @property {Number} left The position-left where the pop-up will be displayed
155
- * @property {Number} right The position-right where the pop-up will be displayed
156
- * @property {Number} bottom The position-bottom where the pop-up will be displayed
157
- * @property {string} width The maximum width of the popup (CSS string)
158
- * @property {string} height The maximum height of the popup (CSS string)
159
- * @property {HTMLElement} parent Popup's parent element
160
- * @property {string} style Additional CSS styles for the popup
161
- * @property {boolean} preventCloseOnBlur Flag to prevent closing the popup on blur
162
- * @property {boolean} backdrop If true, a semi-transparent background will be applied behind the popup, dimming the rest of the page.
163
- */
164
- static open({
165
- template,
166
- top,
167
- left,
168
- right,
169
- bottom,
170
- width,
171
- height,
172
- parent,
173
- style,
174
- preventCloseOnBlur,
175
- backdrop
176
- }: {
177
- template: unknown
178
- top?: number
179
- left?: number
180
- right?: number
181
- bottom?: number
182
- width?: string
183
- height?: string
184
- parent?: Element | null
185
- style?: string
186
- preventCloseOnBlur?: boolean
187
- backdrop?: boolean
188
- }): OxPopup {
189
- const target = document.createElement('ox-popup') as OxPopup
190
- if (preventCloseOnBlur) {
191
- target.preventCloseOnBlur = preventCloseOnBlur
192
- }
193
-
194
- if (backdrop) {
195
- target.backdrop = backdrop
196
- }
197
-
198
- if (style) {
199
- target.style.cssText = style
200
- }
201
-
202
- render(template, target)
203
-
204
- if (parent) {
205
- var { left, top, right, bottom } = convertToFixedPosition({
206
- left,
207
- top,
208
- right,
209
- bottom,
210
- relativeElement: parent as HTMLElement
211
- })
212
- }
213
-
214
- document.body.appendChild(target)
215
- target.removeAfterUse = true
216
- target.open({ top, left, right, bottom, width, height })
217
-
218
- return target
219
- }
220
-
221
- /**
222
- * Opens the 'ox-popup' with the specified position and dimensions.
223
- *
224
- * @param {Object} options - An object specifying the position and dimensions of the popup.
225
- * @param {number} options.left - The left position (in pixels) where the popup should be displayed. If not provided, it will be centered horizontally.
226
- * @param {number} options.top - The top position (in pixels) where the popup should be displayed. If not provided, it will be centered vertically.
227
- * @param {number} options.right - The right position (in pixels) where the popup should be displayed. If provided, it will override the 'left' value.
228
- * @param {number} options.bottom - The bottom position (in pixels) where the popup should be displayed. If provided, it will override the 'top' value.
229
- * @param {string} options.width - The maximum width of the popup (CSS string). For example, '300px'.
230
- * @param {string} options.height - The maximum height of the popup (CSS string). For example, '200px'.
231
- * @param {boolean} [options.silent=false] - A flag indicating whether the popup should auto-focus or not. If set to true, the popup won't attempt to focus on any element.
232
- * @param {boolean} [options.fixed=false] - A flag indicating whether the popup should be positioned fixed relative to the viewport. If set to true, the popup will be fixed.
233
- */
234
- open({
235
- left,
236
- top,
237
- right,
238
- bottom,
239
- width,
240
- height,
241
- silent = false,
242
- fixed = false
243
- }: {
244
- left?: number
245
- top?: number
246
- right?: number
247
- bottom?: number
248
- width?: string
249
- height?: string
250
- silent?: boolean
251
- fixed?: boolean
252
- }) {
253
- if (width) {
254
- this.style.maxWidth = width
255
- this.style.overflowX = 'auto'
256
- }
257
-
258
- if (height) {
259
- this.style.maxHeight = height
260
- this.style.overflowY = 'auto'
261
- }
262
-
263
- if (left === undefined && top === undefined && right === undefined && bottom === undefined) {
264
- this.style.transform = 'translateX(-50%) translateY(-50%)'
265
- this.style.left = '50%'
266
- this.style.top = '50%'
267
- } else if (fixed) {
268
- const bounding = this.parentElement?.getBoundingClientRect() || {
269
- left: 0,
270
- right: 0,
271
- top: 0,
272
- bottom: 0
273
- }
274
-
275
- this.style.position = 'fixed'
276
-
277
- if (left !== undefined) {
278
- left += bounding.left
279
- this.style.left = `${left}px`
280
- }
281
- if (top !== undefined) {
282
- top += bounding.top
283
- this.style.top = `${top}px`
284
- }
285
- if (right !== undefined) {
286
- right = window.innerWidth - (bounding.right - right)
287
- this.style.right = `${right}px`
288
- }
289
- if (bottom !== undefined) {
290
- bottom = window.innerHeight - (bounding.bottom - bottom)
291
- this.style.bottom = `${bottom}px`
292
- }
293
- } else {
294
- if (left !== undefined) {
295
- this.style.left = `${left}px`
296
- }
297
- if (top !== undefined) {
298
- this.style.top = `${top}px`
299
- }
300
- if (right !== undefined) {
301
- this.style.right = `${right}px`
302
- }
303
- if (bottom !== undefined) {
304
- this.style.bottom = `${bottom}px`
305
- }
306
- }
307
-
308
- this.setAttribute('active', '')
309
-
310
- requestAnimationFrame(() => {
311
- const vh = window.innerHeight
312
- const vw = window.innerWidth
313
-
314
- var bounding = this.getBoundingClientRect()
315
-
316
- var h = bounding.height
317
- var w = bounding.width
318
- var t = bounding.top
319
- var l = bounding.left
320
-
321
- // If the popup is too large, it will cause overflow scrolling.
322
- if (vh < h) {
323
- this.style.height = `${Math.min(Math.max(Math.floor((vh * 2) / 3), vh - (t + 20)), vh)}px`
324
- this.style.overflow = 'auto'
325
- h = vh
326
- }
327
-
328
- if (vw < w) {
329
- this.style.width = `${Math.min(Math.max(Math.floor((vw * 2) / 3), vw - (l + 20)), vw)}px`
330
- this.style.overflow = 'auto'
331
- w = vw
332
- }
333
-
334
- // To prevent pop-ups from crossing screen boundaries, use the
335
- const computedStyle = getComputedStyle(this)
336
-
337
- if (t < 0) {
338
- this.style.top = '10px'
339
- this.style.bottom = ''
340
- } else if (vh <= t + h) {
341
- this.style.top = ''
342
- this.style.bottom = '10px'
343
- }
344
-
345
- if (l < 0) {
346
- this.style.left = `calc(${computedStyle.left} - ${l - 10}px)`
347
- this.style.right = ''
348
- } else if (vw < l + w) {
349
- this.style.left = `calc(${computedStyle.left} - ${l + w - vw + 10}px)`
350
- this.style.right = ''
351
- }
352
- })
353
-
354
- // auto focusing
355
- !silent && this.guaranteeFocus()
356
-
357
- /* When the window is out of focus, all pop-ups should disappear. */
358
- window.addEventListener('blur', this._onwindowblur)
359
- }
360
-
361
- guaranteeFocus(target?: HTMLElement) {
362
- const focusible = (target || this).querySelector(
363
- ':scope > button, :scope > [href], :scope > input, :scope > select, :scope > textarea, :scope > [tabindex]:not([tabindex="-1"])'
364
- )
365
-
366
- if (focusible) {
367
- ;(focusible as HTMLElement).focus()
368
- } else {
369
- this.focus()
370
- }
371
- }
372
-
373
- /**
374
- * Closes the 'ox-popup'.
375
- */
376
- close() {
377
- this.removeAttribute('active')
378
-
379
- window.removeEventListener('blur', this._onwindowblur)
380
-
381
- if (this.removeAfterUse && this.parentElement) {
382
- /* this case is when the popup is opened by OxPopup.open(...) */
383
- this.removeEventListener('focusout', this._onfocusout)
384
- this.removeEventListener('keydown', this._onkeydown)
385
- this.removeEventListener('keyup', this._onkeyup)
386
- this.removeEventListener('click', this._onclick)
387
- this.removeEventListener('ox-close', this._onclose)
388
- this.removeEventListener('ox-collapse', this._oncollapse)
389
- this.removeEventListener('mouseup', this._onmouseup)
390
- this.removeEventListener('mousedown', this._onmousedown)
391
- this.removeEventListener('contextmenu', this._oncontextmenu)
392
-
393
- this.parentElement.removeChild(this)
394
-
395
- if (this.lastActive) {
396
- this.lastActive.focus && this.lastActive.focus()
397
- }
398
- }
399
- }
400
- }