@operato/popup 8.0.0-beta.0 → 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.
@@ -1,247 +0,0 @@
1
- import { css, html, PropertyValues } from 'lit'
2
- import { render } from 'lit-html'
3
- import { customElement, property } from 'lit/decorators.js'
4
-
5
- import { OxPopup } from './ox-popup'
6
- import { convertToFixedPosition } from './position-converter.js'
7
-
8
- function focusClosest(element: HTMLElement) {
9
- /* Find the closest focusable element. */
10
- const closest = element.closest(
11
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
12
- ) as HTMLElement
13
-
14
- closest?.focus()
15
-
16
- return closest
17
- }
18
-
19
- /**
20
- * Custom element representing a popup menu. It extends OxPopup.
21
- */
22
- @customElement('ox-popup-menu')
23
- export class OxPopupMenu extends OxPopup {
24
- static styles = [
25
- ...OxPopup.styles,
26
- css`
27
- :host {
28
- display: none;
29
- flex-direction: column;
30
- align-items: stretch;
31
- background-color: var(--ox-popup-menu-background-color, var(--md-sys-color-surface));
32
- color: var(--ox-popup-list-color, var(--md-sys-color-on-surface));
33
- z-index: 100;
34
- box-shadow: 2px 3px 10px 5px rgba(0, 0, 0, 0.15);
35
- padding: var(--spacing-small) 0;
36
- border-radius: var(--spacing-small);
37
-
38
- font-size: var(--md-sys-typescale-label-large-size, 0.875rem);
39
- }
40
-
41
- :host([active]) {
42
- display: flex;
43
- }
44
-
45
- :host(*:focus) {
46
- outline: none;
47
- }
48
-
49
- ::slotted(*) {
50
- padding: var(--spacing-medium);
51
- border-bottom: 1px solid var(--md-sys-color-surface-variant);
52
- cursor: pointer;
53
- color: var(--ox-popup-list-color, var(--md-sys-color-outline-variant));
54
- }
55
-
56
- ::slotted(*:focus) {
57
- cursor: pointer;
58
- outline: none;
59
- }
60
-
61
- ::slotted([menu]),
62
- ::slotted(ox-popup-menuitem) {
63
- border-left: 3px solid transparent;
64
- background-color: var(--ox-popup-menu-background-color, var(--md-sys-color-surface));
65
- color: var(--ox-popup-menu-color, var(--md-sys-color-on-surface));
66
- }
67
-
68
- ::slotted([menu][active]),
69
- ::slotted([menu]:hover),
70
- ::slotted(ox-popup-menuitem[active]),
71
- ::slotted(ox-popup-menuitem:hover) {
72
- background-color: var(--ox-popup-list-background-color-variant, var(--md-sys-color-surface-variant));
73
- color: var(--ox-popup-list-color-variant, var(--md-sys-color-on-surface-variant));
74
- }
75
-
76
- ::slotted(ox-popup-menuitem[active]) {
77
- border-left: 3px solid var(--md-sys-color-primary);
78
- font-weight: var(--md-sys-typescale-label-large-weight, var(--md-ref-typeface-weight-medium, 500));
79
- }
80
-
81
- ::slotted([separator]) {
82
- height: 1px;
83
- width: 100%;
84
- padding: 0;
85
- background-color: var(--ox-popup-menu-separator-color, var(--md-sys-color-surface-variant));
86
- }
87
- `
88
- ]
89
-
90
- /**
91
- * Property to track the index of the active menu item.
92
- */
93
- @property({ type: Number }) activeIndex: number = 0
94
-
95
- render() {
96
- return html` <slot> </slot> `
97
- }
98
-
99
- protected _onkeydown: (e: KeyboardEvent) => void = function (this: OxPopupMenu, e: KeyboardEvent) {
100
- e.stopPropagation()
101
-
102
- switch (e.key) {
103
- case 'Esc': // for IE/Edge
104
- case 'Escape':
105
- case 'Left': // for IE/Edge
106
- case 'ArrowLeft':
107
- this.close()
108
- break
109
-
110
- case 'Up': // for IE/Edge
111
- case 'ArrowUp':
112
- this.activeIndex--
113
- break
114
-
115
- case 'Right': // for IE/Edge
116
- case 'ArrowRight':
117
- case 'Down': // for IE/Edge
118
- case 'ArrowDown':
119
- this.activeIndex++
120
- break
121
-
122
- case 'Enter':
123
- e.stopPropagation()
124
- var menu = (e.target as HTMLElement)?.closest('[menu], ox-popup-menuitem')
125
- if (menu) {
126
- this.select(menu)
127
- }
128
- break
129
- }
130
- }.bind(this)
131
-
132
- protected _onfocusout: (e: FocusEvent) => void = function (this: OxPopupMenu, e: FocusEvent) {
133
- const target = e.target as HTMLElement
134
- const to = e.relatedTarget as HTMLElement
135
- const from = target.closest('ox-popup-menu')
136
-
137
- if (!to && from !== this) {
138
- e.stopPropagation()
139
-
140
- /* "하위의 POPUP-MENU 엘리먼트가 포커스를 잃었지만, 그 포커스를 받은 엘리먼트가 없다."는 의미는 그 서브메뉴가 클로즈된 것을 의미한다. */
141
- this.setActive(this.activeIndex)
142
- } else {
143
- if (!this.contains(to)) {
144
- /* 분명히 내 범위가 아닌 엘리먼트로 포커스가 옮겨졌다면, popup-menu는 닫혀야 한다. */
145
- // @ts-ignore for debug
146
- !window.POPUP_DEBUG && this.close()
147
- }
148
- }
149
- }.bind(this)
150
-
151
- protected _onclick: (e: MouseEvent) => void = function (this: OxPopupMenu, e: MouseEvent) {
152
- e.stopPropagation()
153
-
154
- const menu = (e.target as HTMLElement)?.closest('[menu], ox-popup-menuitem')
155
- if (menu) {
156
- this.setActive(menu)
157
- this.select(menu)
158
- }
159
- }.bind(this)
160
-
161
- updated(changes: PropertyValues<this>) {
162
- if (changes.has('activeIndex')) {
163
- this.setActive(this.activeIndex)
164
- }
165
- }
166
-
167
- /**
168
- * Selects a menu item by dispatching a 'select' event.
169
- * Closes the menu if the item doesn't have 'alive-on-select' attribute.
170
- */
171
- select(menu: Element) {
172
- menu.dispatchEvent(new CustomEvent('select'))
173
- if (!menu.hasAttribute('alive-on-select')) {
174
- this.dispatchEvent(new CustomEvent('ox-close', { bubbles: true, composed: true, detail: this }))
175
- }
176
- }
177
-
178
- /**
179
- * Sets the active menu item based on the index or the menu element itself.
180
- */
181
- setActive(active: number | Element | null) {
182
- const menus = Array.from(this.querySelectorAll(':scope > ox-popup-menuitem, :scope > [menu]'))
183
-
184
- menus.map(async (menu, index) => {
185
- if (typeof active === 'number' && index === (active + menus.length) % menus.length) {
186
- menu.setAttribute('active', '')
187
- focusClosest(menu as HTMLElement)
188
-
189
- this.activeIndex = index
190
- } else if (active === menu) {
191
- if (this.activeIndex === index) {
192
- /* 메뉴의 update를 유도하기 위해서 강제로 토글시킴 */
193
- menu.removeAttribute('active')
194
- await this.updateComplete
195
- menu.setAttribute('active', '')
196
- }
197
-
198
- this.activeIndex = index
199
- } else {
200
- menu.removeAttribute('active')
201
- }
202
- })
203
- }
204
-
205
- /**
206
- * Static method to open a popup menu with the provided template and position options.
207
- * Creates and returns an instance of OxPopupMenu.
208
- *
209
- * @param {PopupOpenOptions}
210
- */
211
- static open({
212
- template,
213
- top,
214
- left,
215
- right,
216
- bottom,
217
- parent
218
- }: {
219
- template: unknown
220
- top?: number
221
- left?: number
222
- right?: number
223
- bottom?: number
224
- parent?: Element | null
225
- }): OxPopupMenu {
226
- const target = document.createElement('ox-popup-menu') as OxPopupMenu
227
- render(template, target)
228
-
229
- target.setAttribute('active', '')
230
-
231
- if (parent) {
232
- var { left, top, right, bottom } = convertToFixedPosition({
233
- left,
234
- top,
235
- right,
236
- bottom,
237
- relativeElement: parent as HTMLElement
238
- })
239
- }
240
-
241
- document.body.appendChild(target)
242
- target.removeAfterUse = true
243
- target.open({ top, left, right, bottom })
244
-
245
- return target
246
- }
247
- }
@@ -1,187 +0,0 @@
1
- import { css, html, LitElement, PropertyValues } from 'lit'
2
- import { customElement, property, state } from 'lit/decorators.js'
3
-
4
- import { OxPopupMenu } from './ox-popup-menu'
5
-
6
- /**
7
- * Custom element representing a menu item within an OxPopup menu.
8
- * It can contain a label and an optional submenu.
9
- */
10
- @customElement('ox-popup-menuitem')
11
- export class OxPopupMenuItem extends LitElement {
12
- static styles = [
13
- css`
14
- :host {
15
- display: flex;
16
- flex-direction: row;
17
- position: relative;
18
- align-items: center;
19
- }
20
-
21
- [icon] {
22
- width: 20px;
23
- display: flex;
24
- flex-direction: row;
25
- padding: 0;
26
- margin: 0 var(--spacing-small) 0 0;
27
- align-items: center;
28
- justify-content: center;
29
- }
30
-
31
- [icon] > * {
32
- flex: 1;
33
- }
34
-
35
- [label] {
36
- flex: 1;
37
- text-transform: capitalize;
38
- }
39
-
40
- ::slotted(*[slot='icon']) {
41
- color: var(--ox-popup-menu-color-variant, var(--md-sys-color-on-surface-variant));
42
- font-size: var(--icon-size-small);
43
- }
44
-
45
- md-icon {
46
- display: block;
47
- width: 24px;
48
- text-align: right;
49
- font-size: var(--icon-size-small);
50
- color: var(--ox-popup-menu-color-variant, var(--md-sys-color-primary));
51
- opacity: 0.7;
52
- }
53
- `
54
- ]
55
-
56
- /**
57
- * Property indicating whether the menu item is active or not.
58
- * When active, it may show a submenu.
59
- */
60
- @property({ type: Boolean }) active: boolean = false
61
-
62
- /**
63
- * The label to display for the menu item.
64
- */
65
- @property({ type: String }) label!: string
66
-
67
- @state() _submenu?: OxPopupMenu
68
-
69
- render() {
70
- return html`
71
- <div icon>
72
- <slot name="icon"> </slot>
73
- </div>
74
- <div label>${this.label}</div>
75
-
76
- ${this._submenu ? html`<md-icon>chevron_right</md-icon>` : html``}
77
-
78
- <slot @slotchange=${this._onslotchange}> </slot>
79
- `
80
- }
81
-
82
- firstUpdated() {
83
- this.setAttribute('tabindex', '0')
84
- this.addEventListener('keydown', this._onkeydown)
85
- this.addEventListener('click', this._onclick)
86
- }
87
-
88
- protected _onclick: (e: MouseEvent) => void = function (this: OxPopupMenuItem, e: MouseEvent) {
89
- if (!this._submenu) {
90
- return
91
- }
92
-
93
- e.stopPropagation()
94
-
95
- const parent = this.closest('ox-popup-menu') as OxPopupMenu
96
- if (parent) {
97
- parent.setActive(this)
98
- }
99
-
100
- this.dispatchEvent(new CustomEvent('select'))
101
-
102
- requestAnimationFrame(() => {
103
- this.expand(false)
104
- })
105
- }.bind(this)
106
-
107
- protected _onkeydown: (e: KeyboardEvent) => void = function (this: OxPopupMenuItem, e: KeyboardEvent) {
108
- switch (e.key) {
109
- case 'Right':
110
- case 'ArrowRight':
111
- e.stopPropagation()
112
- this.expand(false)
113
- break
114
-
115
- case 'Left':
116
- case 'ArrowLeft':
117
- e.stopPropagation()
118
- this.collapseSelf()
119
- break
120
-
121
- case 'Enter':
122
- if (this._submenu) {
123
- e.stopPropagation()
124
- this.expand(false)
125
- }
126
- break
127
- }
128
- }.bind(this)
129
-
130
- protected _onslotchange: (e: Event) => void = function (this: OxPopupMenuItem, e: Event) {
131
- this._submenu = this.querySelector('ox-popup-menu') as OxPopupMenu
132
- }.bind(this)
133
-
134
- updated(changes: PropertyValues<this>) {
135
- if (changes.has('active')) {
136
- this.updateActive()
137
- }
138
- }
139
-
140
- updateActive() {
141
- if (this.active) {
142
- this.expand(true)
143
- } else {
144
- this.collapse()
145
- }
146
- }
147
-
148
- /**
149
- * Expands the submenu, if it exists.
150
- * The submenu is displayed below and to the right of the menu item.
151
- *
152
- * @param {boolean} silent - If true, the submenu is opened silently without user interaction.
153
- */
154
- expand(silent: boolean) {
155
- if (!this._submenu) {
156
- return
157
- }
158
-
159
- const top = 0
160
- const left = this.clientWidth
161
-
162
- this._submenu.open({ top, left, silent })
163
- }
164
-
165
- /**
166
- * Collapses the submenu, if it exists.
167
- */
168
- collapse() {
169
- this._submenu?.close()
170
- }
171
-
172
- /**
173
- * Dispatches a custom 'ox-collapse' event indicating that the menu item should collapse itself.
174
- * This event can be used to trigger further interactions or logic in the application.
175
- */
176
- collapseSelf() {
177
- this.dispatchEvent(new CustomEvent('ox-collapse', { bubbles: true, composed: true, detail: this }))
178
- }
179
-
180
- /**
181
- * Dispatches a custom 'ox-close' event indicating that the menu item should close itself.
182
- * This event can be used to trigger further interactions or logic in the application.
183
- */
184
- close() {
185
- this.dispatchEvent(new CustomEvent('ox-close', { bubbles: true, composed: true, detail: this }))
186
- }
187
- }