@operato/popup
Advanced tools
Comparing version 0.2.35 to 0.2.36
@@ -6,2 +6,21 @@ # Change Log | ||
### [0.2.36](https://github.com/hatiolab/operato/compare/v0.2.35...v0.2.36) (2021-12-06) | ||
### :bug: Bug Fix | ||
* apply class name prefix Ox for operato/popup ([d528848](https://github.com/hatiolab/operato/commit/d5288486ea0e1bec84fff3c5b6bac696eb701e5d)) | ||
* operato/input to support formfield ([21b4d46](https://github.com/hatiolab/operato/commit/21b4d46d750ba833c571b5caeed27eebd8aa03ca)) | ||
* operato/input to support formfield ([544bf92](https://github.com/hatiolab/operato/commit/544bf928946d878aa967f33b20efdf90eb8e82f5)) | ||
* operato/ox-popup-list rewritten ([f78b3e9](https://github.com/hatiolab/operato/commit/f78b3e9623faac5ea653442ced653173d630d9e6)) | ||
* operato/popup, input sample ([116b037](https://github.com/hatiolab/operato/commit/116b0371446cb24d8863c2bdea876b97f6e11dfd)) | ||
* operato/popup, input sample ([31fb99f](https://github.com/hatiolab/operato/commit/31fb99fc4e37a26c38826dd3554f92ded0632e82)) | ||
### :rocket: New Features | ||
* adding filter options for data-grist ([3620e8b](https://github.com/hatiolab/operato/commit/3620e8b5c39e7e66f8e7ab39b2ae7200f7f7afb5)) | ||
### [0.2.35](https://github.com/hatiolab/operato/compare/v0.2.34...v0.2.35) (2021-12-03) | ||
@@ -8,0 +27,0 @@ |
import { PropertyValues } from 'lit'; | ||
import { Popup } from './ox-popup'; | ||
export declare class PopupList extends Popup { | ||
import { OxPopup } from './ox-popup'; | ||
export declare class OxPopupList extends OxPopup { | ||
static styles: import("lit").CSSResult[]; | ||
activeIndex: number; | ||
multiple: boolean; | ||
attrSelected?: string; | ||
activeIndex?: number; | ||
render(): import("lit-html").TemplateResult<1>; | ||
@@ -11,15 +13,25 @@ protected _onkeydown: (e: KeyboardEvent) => void; | ||
updated(changes: PropertyValues<this>): void; | ||
select(option: Element): void; | ||
setActive(active: number | Element | null): void; | ||
select(): Promise<void>; | ||
setActive(active: number | Element | null, withSelect?: boolean): void; | ||
open(params: { | ||
left?: number; | ||
top?: number; | ||
right?: number; | ||
bottom?: number; | ||
silent?: boolean; | ||
}): void; | ||
close(): void; | ||
/** | ||
* Open Popup | ||
* Open OxPopup | ||
* | ||
* @param {PopupOpenOptions} | ||
*/ | ||
static open({ template, top, left, parent }: { | ||
static open({ template, top, left, right, bottom, parent }: { | ||
template: unknown; | ||
top: number; | ||
left: number; | ||
top?: number; | ||
left?: number; | ||
right?: number; | ||
bottom?: number; | ||
parent?: Element | null; | ||
}): void; | ||
} |
import { __decorate } from "tslib"; | ||
import { css, html } from 'lit'; | ||
import { customElement, property } from 'lit/decorators.js'; | ||
import { Popup } from './ox-popup'; | ||
import { customElement, property, state } from 'lit/decorators.js'; | ||
import { OxPopup } from './ox-popup'; | ||
import { render } from 'lit-html'; | ||
function focusClosest(element) { | ||
/* Find the closest focusable element. */ | ||
function guaranteeFocus(element) { | ||
// 1. 옵션 엘리먼트의 하위 첫번째 focusible 엘리먼트에 focus 기회를 준다. | ||
const focusible = element.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); | ||
if (focusible) { | ||
; | ||
focusible.focus(); | ||
return; | ||
} | ||
// 2. 자신을 포함해서 가장 가까운 부모에게 focus 기회를 준다. | ||
const closest = element.closest('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); | ||
closest === null || closest === void 0 ? void 0 : closest.focus(); | ||
return closest; | ||
} | ||
let PopupList = class PopupList extends Popup { | ||
let OxPopupList = class OxPopupList extends OxPopup { | ||
constructor() { | ||
super(...arguments); | ||
this.activeIndex = 0; | ||
this.multiple = false; | ||
this._onkeydown = function (e) { | ||
var _a; | ||
e.stopPropagation(); | ||
@@ -37,7 +42,6 @@ switch (e.key) { | ||
case 'Enter': | ||
e.stopPropagation(); | ||
var option = (_a = e.target) === null || _a === void 0 ? void 0 : _a.closest('[option]'); | ||
if (option) { | ||
this.select(option); | ||
} | ||
case ' ': | ||
case 'Spacebar': // for old firefox | ||
this.setActive(this.activeIndex, true); | ||
this.select(); | ||
break; | ||
@@ -47,17 +51,8 @@ } | ||
this._onfocusout = function (e) { | ||
const target = e.target; | ||
const to = e.relatedTarget; | ||
const from = target.closest('ox-popup-list'); | ||
if (!to && from !== this) { | ||
e.stopPropagation(); | ||
/* "하위의 ox-popup-list 엘리먼트가 포커스를 잃었지만, 그 포커스를 받은 엘리먼트가 없다."는 의미는 그 서브메뉴가 클로즈된 것을 의미한다. */ | ||
this.setActive(this.activeIndex); | ||
if (!this.contains(to)) { | ||
/* 분명히 내 범위가 아닌 엘리먼트로 포커스가 옮겨졌다면, ox-popup-list는 닫혀야 한다. */ | ||
// @ts-ignore for debug | ||
!window.POPUP_DEBUG && this.close(); | ||
} | ||
else { | ||
if (!this.contains(to)) { | ||
/* 분명히 내 범위가 아닌 엘리먼트로 포커스가 옮겨졌다면, ox-popup-list는 닫혀야 한다. */ | ||
// @ts-ignore for debug | ||
!window.POPUP_DEBUG && this.close(); | ||
} | ||
} | ||
}.bind(this); | ||
@@ -69,4 +64,4 @@ this._onclick = function (e) { | ||
if (option) { | ||
this.setActive(option); | ||
this.select(option); | ||
this.setActive(option, true); | ||
this.select(); | ||
} | ||
@@ -76,43 +71,89 @@ }.bind(this); | ||
render() { | ||
return html ` <slot> </slot> `; | ||
return html ` | ||
<slot | ||
@change=${(e) => { | ||
e.stopPropagation(); | ||
}} | ||
> | ||
</slot> | ||
`; | ||
} | ||
updated(changes) { | ||
if (changes.has('activeIndex')) { | ||
this.setActive(this.activeIndex); | ||
this.activeIndex !== undefined && this.setActive(this.activeIndex); | ||
} | ||
} | ||
select(option) { | ||
option.dispatchEvent(new CustomEvent('selected', { bubbles: true, composed: true, detail: this })); | ||
if (!option.hasAttribute('alive-on-select') && !this.hasAttribute('alive-on-select')) { | ||
async select() { | ||
await this.updateComplete; | ||
const options = Array.from(this.querySelectorAll(':scope > [option]')); | ||
const selected = options | ||
.filter(option => option.hasAttribute('value') && option.hasAttribute(this.attrSelected || 'selected')) | ||
.map(option => option.getAttribute('value')); | ||
this.dispatchEvent(new CustomEvent('select', { | ||
bubbles: true, | ||
composed: true, | ||
detail: this.multiple ? selected : selected[0] | ||
})); | ||
const option = options[this.activeIndex]; | ||
if (!option.hasAttribute('alive-on-select') && !this.hasAttribute('multiple')) { | ||
this.close(); | ||
} | ||
} | ||
setActive(active) { | ||
const options = Array.from(this.querySelectorAll(':scope > [option]')); | ||
options.map(async (option, index) => { | ||
setActive(active, withSelect) { | ||
const options = Array.from(this.querySelectorAll('[option]')); | ||
if (active instanceof Element) { | ||
const index = options.findIndex(option => option === active); | ||
this.setActive(index === -1 ? 0 : index, withSelect); | ||
return; | ||
} | ||
options.forEach(async (option, index) => { | ||
if (typeof active === 'number' && index === (active + options.length) % options.length) { | ||
option.setAttribute('active', ''); | ||
focusClosest(option); | ||
if (withSelect && !this.attrSelected) { | ||
this.multiple ? option.toggleAttribute('selected') : option.setAttribute('selected', ''); | ||
} | ||
guaranteeFocus(option); | ||
this.activeIndex = index; | ||
} | ||
else if (active === option) { | ||
this.activeIndex = index; | ||
} | ||
else { | ||
option.removeAttribute('active'); | ||
!this.attrSelected && !this.multiple && withSelect && option.removeAttribute('selected'); | ||
} | ||
}); | ||
} | ||
open(params) { | ||
super.open(params); | ||
if (this.activeIndex === undefined) { | ||
this.activeIndex = 0; | ||
} | ||
else { | ||
this.setActive(this.activeIndex); | ||
} | ||
} | ||
close() { | ||
if (this.hasAttribute('active')) { | ||
this.dispatchEvent(new CustomEvent('close', { | ||
bubbles: true, | ||
composed: true | ||
})); | ||
} | ||
super.close(); | ||
} | ||
/** | ||
* Open Popup | ||
* Open OxPopup | ||
* | ||
* @param {PopupOpenOptions} | ||
*/ | ||
static open({ template, top, left, parent }) { | ||
static open({ template, top, left, right, bottom, parent }) { | ||
const owner = parent || document.body; | ||
const target = document.createElement('ox-popup-list'); | ||
render(template, target); | ||
target.style.left = `${left}px`; | ||
target.style.top = `${top}px`; | ||
target.setAttribute('active', ''); | ||
if (left !== undefined) | ||
target.style.left = `${left}px`; | ||
if (top !== undefined) | ||
target.style.top = `${top}px`; | ||
if (right !== undefined) | ||
target.style.right = `${right}px`; | ||
if (bottom !== undefined) | ||
target.style.bottom = `${bottom}px`; | ||
target._parent = owner; | ||
@@ -122,4 +163,4 @@ owner.appendChild(target); | ||
}; | ||
PopupList.styles = [ | ||
...Popup.styles, | ||
OxPopupList.styles = [ | ||
...OxPopup.styles, | ||
css ` | ||
@@ -147,2 +188,6 @@ :host { | ||
::slotted([option]) { | ||
white-space: nowrap; | ||
} | ||
::slotted(*) { | ||
@@ -166,2 +211,7 @@ margin: 1px 0; | ||
::slotted([option][selected]) { | ||
background-color: red; | ||
cursor: pointer; | ||
} | ||
::slotted([separator]) { | ||
@@ -179,8 +229,14 @@ height: 1px; | ||
__decorate([ | ||
property({ type: Number }) | ||
], PopupList.prototype, "activeIndex", void 0); | ||
PopupList = __decorate([ | ||
property({ type: Boolean }) | ||
], OxPopupList.prototype, "multiple", void 0); | ||
__decorate([ | ||
property({ type: String, attribute: 'attr-selected' }) | ||
], OxPopupList.prototype, "attrSelected", void 0); | ||
__decorate([ | ||
state() | ||
], OxPopupList.prototype, "activeIndex", void 0); | ||
OxPopupList = __decorate([ | ||
customElement('ox-popup-list') | ||
], PopupList); | ||
export { PopupList }; | ||
], OxPopupList); | ||
export { OxPopupList }; | ||
//# sourceMappingURL=ox-popup-list.js.map |
import { PropertyValues } from 'lit'; | ||
import { Popup } from './ox-popup'; | ||
export declare class PopupMenu extends Popup { | ||
import { OxPopup } from './ox-popup'; | ||
export declare class OxPopupMenu extends OxPopup { | ||
static styles: import("lit").CSSResult[]; | ||
@@ -5,0 +5,0 @@ activeIndex: number; |
import { __decorate } from "tslib"; | ||
import { css, html } from 'lit'; | ||
import { customElement, property } from 'lit/decorators.js'; | ||
import { Popup } from './ox-popup'; | ||
import { OxPopup } from './ox-popup'; | ||
import { render } from 'lit-html'; | ||
@@ -12,3 +12,3 @@ function focusClosest(element) { | ||
} | ||
let PopupMenu = class PopupMenu extends Popup { | ||
let OxPopupMenu = class OxPopupMenu extends OxPopup { | ||
constructor() { | ||
@@ -82,3 +82,3 @@ super(...arguments); | ||
select(menu) { | ||
menu.dispatchEvent(new CustomEvent('selected')); | ||
menu.dispatchEvent(new CustomEvent('select')); | ||
if (!menu.hasAttribute('alive-on-select')) { | ||
@@ -126,4 +126,4 @@ this.dispatchEvent(new CustomEvent('ox-close', { bubbles: true, composed: true, detail: this })); | ||
}; | ||
PopupMenu.styles = [ | ||
...Popup.styles, | ||
OxPopupMenu.styles = [ | ||
...OxPopup.styles, | ||
css ` | ||
@@ -184,7 +184,7 @@ :host { | ||
property({ type: Number }) | ||
], PopupMenu.prototype, "activeIndex", void 0); | ||
PopupMenu = __decorate([ | ||
], OxPopupMenu.prototype, "activeIndex", void 0); | ||
OxPopupMenu = __decorate([ | ||
customElement('ox-popup-menu') | ||
], PopupMenu); | ||
export { PopupMenu }; | ||
], OxPopupMenu); | ||
export { OxPopupMenu }; | ||
//# sourceMappingURL=ox-popup-menu.js.map |
import { LitElement, PropertyValues } from 'lit'; | ||
import { PopupMenu } from './ox-popup-menu'; | ||
export declare class PopupMenuItem extends LitElement { | ||
import { OxPopupMenu } from './ox-popup-menu'; | ||
export declare class OxPopupMenuItem extends LitElement { | ||
static styles: import("lit").CSSResult[]; | ||
active: boolean; | ||
label: string; | ||
_submenu?: PopupMenu; | ||
_submenu?: OxPopupMenu; | ||
render(): import("lit-html").TemplateResult<1>; | ||
@@ -9,0 +9,0 @@ firstUpdated(): void; |
import { __decorate } from "tslib"; | ||
import { LitElement, css, html } from 'lit'; | ||
import { customElement, property, state } from 'lit/decorators.js'; | ||
let PopupMenuItem = class PopupMenuItem extends LitElement { | ||
let OxPopupMenuItem = class OxPopupMenuItem extends LitElement { | ||
constructor() { | ||
@@ -17,3 +17,3 @@ super(...arguments); | ||
} | ||
this.dispatchEvent(new CustomEvent('selected')); | ||
this.dispatchEvent(new CustomEvent('select')); | ||
requestAnimationFrame(() => { | ||
@@ -96,3 +96,3 @@ this.expand(false); | ||
}; | ||
PopupMenuItem.styles = [ | ||
OxPopupMenuItem.styles = [ | ||
css ` | ||
@@ -142,13 +142,13 @@ :host { | ||
property({ type: Boolean }) | ||
], PopupMenuItem.prototype, "active", void 0); | ||
], OxPopupMenuItem.prototype, "active", void 0); | ||
__decorate([ | ||
property({ type: String }) | ||
], PopupMenuItem.prototype, "label", void 0); | ||
], OxPopupMenuItem.prototype, "label", void 0); | ||
__decorate([ | ||
state() | ||
], PopupMenuItem.prototype, "_submenu", void 0); | ||
PopupMenuItem = __decorate([ | ||
], OxPopupMenuItem.prototype, "_submenu", void 0); | ||
OxPopupMenuItem = __decorate([ | ||
customElement('ox-popup-menuitem') | ||
], PopupMenuItem); | ||
export { PopupMenuItem }; | ||
], OxPopupMenuItem); | ||
export { OxPopupMenuItem }; | ||
//# sourceMappingURL=ox-popup-menuitem.js.map |
import { LitElement } from 'lit'; | ||
export declare class Popup extends LitElement { | ||
export declare class OxPopup extends LitElement { | ||
static styles: import("lit").CSSResult[]; | ||
@@ -28,11 +28,15 @@ _parent?: Element; | ||
*/ | ||
static open({ template, top, left, parent }: { | ||
static open({ template, top, left, right, bottom, parent }: { | ||
template: unknown; | ||
top: number; | ||
left: number; | ||
top?: number; | ||
left?: number; | ||
right?: number; | ||
bottom?: number; | ||
parent?: Element | null; | ||
}): void; | ||
open({ left, top, silent }: { | ||
open({ left, top, right, bottom, silent }: { | ||
left?: number; | ||
top?: number; | ||
right?: number; | ||
bottom?: number; | ||
silent?: boolean; | ||
@@ -39,0 +43,0 @@ }): void; |
import { __decorate } from "tslib"; | ||
import { LitElement, css, html } from 'lit'; | ||
import { customElement, state } from 'lit/decorators.js'; | ||
import { ScrollbarStyles } from '@operato/styles'; | ||
import { render } from 'lit-html'; | ||
let Popup = class Popup extends LitElement { | ||
let OxPopup = class OxPopup extends LitElement { | ||
constructor() { | ||
@@ -54,4 +55,2 @@ super(...arguments); | ||
this.addEventListener('ox-collapse', this._oncollapse); | ||
/* When the window is out of focus, all pop-ups should disappear. */ | ||
window.addEventListener('blur', this._onwindowblur); | ||
this.setAttribute('tabindex', '0'); // make this element focusable | ||
@@ -74,17 +73,23 @@ this.guaranteeFocus(); | ||
*/ | ||
static open({ template, top, left, parent }) { | ||
static open({ template, top, left, right, bottom, parent }) { | ||
const owner = parent || document.body; | ||
const target = document.createElement('ox-popup'); | ||
render(template, target); | ||
target.style.left = `${left}px`; | ||
target.style.top = `${top}px`; | ||
target.setAttribute('active', ''); | ||
target._parent = owner; | ||
owner.appendChild(target); | ||
target.open({ top, left, right, bottom }); | ||
} | ||
open({ left = 0, top = 0, silent = false }) { | ||
this.style.left = `${left}px`; | ||
this.style.top = `${top}px`; | ||
open({ left, top, right, bottom, silent = false }) { | ||
if (left !== undefined) | ||
this.style.left = `${left}px`; | ||
if (top !== undefined) | ||
this.style.top = `${top}px`; | ||
if (right !== undefined) | ||
this.style.right = `${right}px`; | ||
if (bottom !== undefined) | ||
this.style.bottom = `${bottom}px`; | ||
this.setAttribute('active', ''); | ||
!silent && this.guaranteeFocus(); | ||
/* When the window is out of focus, all pop-ups should disappear. */ | ||
window.addEventListener('blur', this._onwindowblur); | ||
} | ||
@@ -103,4 +108,5 @@ guaranteeFocus() { | ||
this.removeAttribute('active'); | ||
window.removeEventListener('blur', this._onwindowblur); | ||
if (this._parent) { | ||
/* this case is when the popup is opened by Popup.open(...) */ | ||
/* this case is when the popup is opened by OxPopup.open(...) */ | ||
this.removeEventListener('focusout', this._onfocusout); | ||
@@ -112,3 +118,2 @@ this.removeEventListener('keydown', this._onkeydown); | ||
this.removeEventListener('ox-collapse', this._oncollapse); | ||
window.removeEventListener('blur', this._onwindowblur); | ||
this._parent.removeChild(this); | ||
@@ -119,3 +124,4 @@ delete this._parent; | ||
}; | ||
Popup.styles = [ | ||
OxPopup.styles = [ | ||
ScrollbarStyles, | ||
css ` | ||
@@ -127,2 +133,4 @@ :host { | ||
z-index: 100; | ||
box-sizing: border-box; | ||
min-width: fit-content; | ||
} | ||
@@ -141,7 +149,7 @@ | ||
state() | ||
], Popup.prototype, "_parent", void 0); | ||
Popup = __decorate([ | ||
], OxPopup.prototype, "_parent", void 0); | ||
OxPopup = __decorate([ | ||
customElement('ox-popup') | ||
], Popup); | ||
export { Popup }; | ||
], OxPopup); | ||
export { OxPopup }; | ||
//# sourceMappingURL=ox-popup.js.map |
import '../src/ox-popup-menu'; | ||
import { expect, fixture } from '@open-wc/testing'; | ||
import { html } from 'lit'; | ||
import { expect, fixture } from '@open-wc/testing'; | ||
describe('PopupMenu', () => { | ||
describe('OxPopupMenu', () => { | ||
it('has a default title "Hey there" and counter 5', async () => { | ||
@@ -6,0 +6,0 @@ const el = await fixture(html `<ox-popup-menu></ox-popup-menu>`); |
import '../src/ox-popup'; | ||
import { expect, fixture } from '@open-wc/testing'; | ||
import { html } from 'lit'; | ||
import { expect, fixture } from '@open-wc/testing'; | ||
describe('Popup', () => { | ||
@@ -5,0 +5,0 @@ it('has a default title "Hey there" and counter 5', async () => { |
@@ -6,3 +6,3 @@ { | ||
"author": "heartyoh", | ||
"version": "0.2.35", | ||
"version": "0.2.36", | ||
"main": "dist/src/index.js", | ||
@@ -36,2 +36,3 @@ "module": "dist/src/index.js", | ||
"@material/mwc-icon": "^0.25.3", | ||
"@operato/styles": "^0.2.36", | ||
"lit": "^2.0.2" | ||
@@ -71,3 +72,3 @@ }, | ||
}, | ||
"gitHead": "6099777251e8f46c5adc98dc9fc2cfbc8194cf12" | ||
"gitHead": "fa4b02d776ebca4674d2c5e02fafa7042a5d5448" | ||
} |
import { PropertyValues, css, html } from 'lit' | ||
import { customElement, property } from 'lit/decorators.js' | ||
import { customElement, property, state } from 'lit/decorators.js' | ||
import { Popup } from './ox-popup' | ||
import { OxPopup } from './ox-popup' | ||
import { render } from 'lit-html' | ||
function focusClosest(element: HTMLElement) { | ||
/* Find the closest focusable element. */ | ||
function guaranteeFocus(element: HTMLElement) { | ||
// 1. 옵션 엘리먼트의 하위 첫번째 focusible 엘리먼트에 focus 기회를 준다. | ||
const focusible = element.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])') | ||
if (focusible) { | ||
;(focusible as HTMLElement).focus() | ||
return | ||
} | ||
// 2. 자신을 포함해서 가장 가까운 부모에게 focus 기회를 준다. | ||
const closest = element.closest( | ||
@@ -14,10 +22,8 @@ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' | ||
closest?.focus() | ||
return closest | ||
} | ||
@customElement('ox-popup-list') | ||
export class PopupList extends Popup { | ||
export class OxPopupList extends OxPopup { | ||
static styles = [ | ||
...Popup.styles, | ||
...OxPopup.styles, | ||
css` | ||
@@ -45,2 +51,6 @@ :host { | ||
::slotted([option]) { | ||
white-space: nowrap; | ||
} | ||
::slotted(*) { | ||
@@ -64,2 +74,7 @@ margin: 1px 0; | ||
::slotted([option][selected]) { | ||
background-color: red; | ||
cursor: pointer; | ||
} | ||
::slotted([separator]) { | ||
@@ -77,9 +92,19 @@ height: 1px; | ||
@property({ type: Number }) activeIndex: number = 0 | ||
@property({ type: Boolean }) multiple: boolean = false | ||
@property({ type: String, attribute: 'attr-selected' }) attrSelected?: string | ||
@state() activeIndex?: number | ||
render() { | ||
return html` <slot> </slot> ` | ||
return html` | ||
<slot | ||
@change=${(e: Event) => { | ||
e.stopPropagation() | ||
}} | ||
> | ||
</slot> | ||
` | ||
} | ||
protected _onkeydown: (e: KeyboardEvent) => void = function (this: PopupList, e: KeyboardEvent) { | ||
protected _onkeydown: (e: KeyboardEvent) => void = function (this: OxPopupList, e: KeyboardEvent) { | ||
e.stopPropagation() | ||
@@ -97,3 +122,3 @@ | ||
case 'ArrowUp': | ||
this.activeIndex-- | ||
this.activeIndex!-- | ||
break | ||
@@ -105,11 +130,10 @@ | ||
case 'ArrowDown': | ||
this.activeIndex++ | ||
this.activeIndex!++ | ||
break | ||
case 'Enter': | ||
e.stopPropagation() | ||
var option = (e.target as HTMLElement)?.closest('[option]') | ||
if (option) { | ||
this.select(option) | ||
} | ||
case ' ': | ||
case 'Spacebar': // for old firefox | ||
this.setActive(this.activeIndex!, true) | ||
this.select() | ||
break | ||
@@ -119,22 +143,13 @@ } | ||
protected _onfocusout: (e: FocusEvent) => void = function (this: PopupList, e: FocusEvent) { | ||
const target = e.target as HTMLElement | ||
protected _onfocusout: (e: FocusEvent) => void = function (this: OxPopupList, e: FocusEvent) { | ||
const to = e.relatedTarget as HTMLElement | ||
const from = target.closest('ox-popup-list') | ||
if (!to && from !== this) { | ||
e.stopPropagation() | ||
/* "하위의 ox-popup-list 엘리먼트가 포커스를 잃었지만, 그 포커스를 받은 엘리먼트가 없다."는 의미는 그 서브메뉴가 클로즈된 것을 의미한다. */ | ||
this.setActive(this.activeIndex) | ||
} else { | ||
if (!this.contains(to)) { | ||
/* 분명히 내 범위가 아닌 엘리먼트로 포커스가 옮겨졌다면, ox-popup-list는 닫혀야 한다. */ | ||
// @ts-ignore for debug | ||
!window.POPUP_DEBUG && this.close() | ||
} | ||
if (!this.contains(to)) { | ||
/* 분명히 내 범위가 아닌 엘리먼트로 포커스가 옮겨졌다면, ox-popup-list는 닫혀야 한다. */ | ||
// @ts-ignore for debug | ||
!window.POPUP_DEBUG && this.close() | ||
} | ||
}.bind(this) | ||
protected _onclick: (e: MouseEvent) => void = function (this: PopupList, e: MouseEvent) { | ||
protected _onclick: (e: MouseEvent) => void = function (this: OxPopupList, e: MouseEvent) { | ||
e.stopPropagation() | ||
@@ -144,4 +159,4 @@ | ||
if (option) { | ||
this.setActive(option) | ||
this.select(option) | ||
this.setActive(option, true) | ||
this.select() | ||
} | ||
@@ -152,10 +167,25 @@ }.bind(this) | ||
if (changes.has('activeIndex')) { | ||
this.setActive(this.activeIndex) | ||
this.activeIndex !== undefined && this.setActive(this.activeIndex) | ||
} | ||
} | ||
select(option: Element) { | ||
option.dispatchEvent(new CustomEvent('selected', { bubbles: true, composed: true, detail: this })) | ||
async select() { | ||
await this.updateComplete | ||
if (!option.hasAttribute('alive-on-select') && !this.hasAttribute('alive-on-select')) { | ||
const options = Array.from(this.querySelectorAll(':scope > [option]')) | ||
const selected = options | ||
.filter(option => option.hasAttribute('value') && option.hasAttribute(this.attrSelected || 'selected')) | ||
.map(option => option.getAttribute('value')) | ||
this.dispatchEvent( | ||
new CustomEvent('select', { | ||
bubbles: true, | ||
composed: true, | ||
detail: this.multiple ? selected : selected[0] | ||
}) | ||
) | ||
const option = options[this.activeIndex!] | ||
if (!option.hasAttribute('alive-on-select') && !this.hasAttribute('multiple')) { | ||
this.close() | ||
@@ -165,15 +195,24 @@ } | ||
setActive(active: number | Element | null) { | ||
const options = Array.from(this.querySelectorAll(':scope > [option]')) | ||
setActive(active: number | Element | null, withSelect?: boolean) { | ||
const options = Array.from(this.querySelectorAll('[option]')) | ||
options.map(async (option, index) => { | ||
if (active instanceof Element) { | ||
const index = options.findIndex(option => option === active) | ||
this.setActive(index === -1 ? 0 : index, withSelect) | ||
return | ||
} | ||
options.forEach(async (option, index) => { | ||
if (typeof active === 'number' && index === (active + options.length) % options.length) { | ||
option.setAttribute('active', '') | ||
focusClosest(option as HTMLElement) | ||
if (withSelect && !this.attrSelected) { | ||
this.multiple ? option.toggleAttribute('selected') : option.setAttribute('selected', '') | ||
} | ||
guaranteeFocus(option as HTMLElement) | ||
this.activeIndex = index | ||
} else if (active === option) { | ||
this.activeIndex = index | ||
} else { | ||
option.removeAttribute('active') | ||
!this.attrSelected && !this.multiple && withSelect && option.removeAttribute('selected') | ||
} | ||
@@ -183,4 +222,27 @@ }) | ||
override open(params: { left?: number; top?: number; right?: number; bottom?: number; silent?: boolean }) { | ||
super.open(params) | ||
if (this.activeIndex === undefined) { | ||
this.activeIndex = 0 | ||
} else { | ||
this.setActive(this.activeIndex) | ||
} | ||
} | ||
override close() { | ||
if (this.hasAttribute('active')) { | ||
this.dispatchEvent( | ||
new CustomEvent('close', { | ||
bubbles: true, | ||
composed: true | ||
}) | ||
) | ||
} | ||
super.close() | ||
} | ||
/** | ||
* Open Popup | ||
* Open OxPopup | ||
* | ||
@@ -193,18 +255,22 @@ * @param {PopupOpenOptions} | ||
left, | ||
right, | ||
bottom, | ||
parent | ||
}: { | ||
template: unknown | ||
top: number | ||
left: number | ||
top?: number | ||
left?: number | ||
right?: number | ||
bottom?: number | ||
parent?: Element | null | ||
}) { | ||
const owner = parent || document.body | ||
const target = document.createElement('ox-popup-list') as PopupList | ||
const target = document.createElement('ox-popup-list') as OxPopupList | ||
render(template, target) | ||
target.style.left = `${left}px` | ||
target.style.top = `${top}px` | ||
if (left !== undefined) target.style.left = `${left}px` | ||
if (top !== undefined) target.style.top = `${top}px` | ||
if (right !== undefined) target.style.right = `${right}px` | ||
if (bottom !== undefined) target.style.bottom = `${bottom}px` | ||
target.setAttribute('active', '') | ||
target._parent = owner | ||
@@ -211,0 +277,0 @@ |
import { PropertyValues, css, html } from 'lit' | ||
import { customElement, property } from 'lit/decorators.js' | ||
import { Popup } from './ox-popup' | ||
import { OxPopup } from './ox-popup' | ||
import { render } from 'lit-html' | ||
@@ -19,5 +19,5 @@ | ||
@customElement('ox-popup-menu') | ||
export class PopupMenu extends Popup { | ||
export class OxPopupMenu extends OxPopup { | ||
static styles = [ | ||
...Popup.styles, | ||
...OxPopup.styles, | ||
css` | ||
@@ -83,3 +83,3 @@ :host { | ||
protected _onkeydown: (e: KeyboardEvent) => void = function (this: PopupMenu, e: KeyboardEvent) { | ||
protected _onkeydown: (e: KeyboardEvent) => void = function (this: OxPopupMenu, e: KeyboardEvent) { | ||
e.stopPropagation() | ||
@@ -117,3 +117,3 @@ | ||
protected _onfocusout: (e: FocusEvent) => void = function (this: PopupMenu, e: FocusEvent) { | ||
protected _onfocusout: (e: FocusEvent) => void = function (this: OxPopupMenu, e: FocusEvent) { | ||
const target = e.target as HTMLElement | ||
@@ -137,3 +137,3 @@ const to = e.relatedTarget as HTMLElement | ||
protected _onclick: (e: MouseEvent) => void = function (this: PopupMenu, e: MouseEvent) { | ||
protected _onclick: (e: MouseEvent) => void = function (this: OxPopupMenu, e: MouseEvent) { | ||
e.stopPropagation() | ||
@@ -155,3 +155,3 @@ | ||
select(menu: Element) { | ||
menu.dispatchEvent(new CustomEvent('selected')) | ||
menu.dispatchEvent(new CustomEvent('select')) | ||
if (!menu.hasAttribute('alive-on-select')) { | ||
@@ -203,3 +203,3 @@ this.dispatchEvent(new CustomEvent('ox-close', { bubbles: true, composed: true, detail: this })) | ||
const owner = parent || document.body | ||
const target = document.createElement('ox-popup-menu') as PopupMenu | ||
const target = document.createElement('ox-popup-menu') as OxPopupMenu | ||
render(template, target) | ||
@@ -206,0 +206,0 @@ |
import { LitElement, PropertyValues, css, html } from 'lit' | ||
import { customElement, property, state } from 'lit/decorators.js' | ||
import { PopupMenu } from './ox-popup-menu' | ||
import { OxPopupMenu } from './ox-popup-menu' | ||
@customElement('ox-popup-menuitem') | ||
export class PopupMenuItem extends LitElement { | ||
export class OxPopupMenuItem extends LitElement { | ||
static styles = [ | ||
@@ -55,3 +55,3 @@ css` | ||
@state() _submenu?: PopupMenu | ||
@state() _submenu?: OxPopupMenu | ||
@@ -77,3 +77,3 @@ render() { | ||
protected _onclick: (e: MouseEvent) => void = function (this: PopupMenuItem, e: MouseEvent) { | ||
protected _onclick: (e: MouseEvent) => void = function (this: OxPopupMenuItem, e: MouseEvent) { | ||
if (!this._submenu) { | ||
@@ -85,3 +85,3 @@ return | ||
const parent = this.closest('ox-popup-menu') as PopupMenu | ||
const parent = this.closest('ox-popup-menu') as OxPopupMenu | ||
if (parent) { | ||
@@ -91,3 +91,3 @@ parent.setActive(this) | ||
this.dispatchEvent(new CustomEvent('selected')) | ||
this.dispatchEvent(new CustomEvent('select')) | ||
@@ -99,3 +99,3 @@ requestAnimationFrame(() => { | ||
protected _onkeydown: (e: KeyboardEvent) => void = function (this: PopupMenuItem, e: KeyboardEvent) { | ||
protected _onkeydown: (e: KeyboardEvent) => void = function (this: OxPopupMenuItem, e: KeyboardEvent) { | ||
switch (e.key) { | ||
@@ -123,4 +123,4 @@ case 'Right': | ||
protected _onslotchange: (e: Event) => void = function (this: PopupMenuItem, e: Event) { | ||
this._submenu = this.querySelector('ox-popup-menu') as PopupMenu | ||
protected _onslotchange: (e: Event) => void = function (this: OxPopupMenuItem, e: Event) { | ||
this._submenu = this.querySelector('ox-popup-menu') as OxPopupMenu | ||
}.bind(this) | ||
@@ -127,0 +127,0 @@ |
import { LitElement, css, html } from 'lit' | ||
import { customElement, state } from 'lit/decorators.js' | ||
import { ScrollbarStyles } from '@operato/styles' | ||
import { render } from 'lit-html' | ||
@customElement('ox-popup') | ||
export class Popup extends LitElement { | ||
export class OxPopup extends LitElement { | ||
static styles = [ | ||
ScrollbarStyles, | ||
css` | ||
@@ -15,2 +17,4 @@ :host { | ||
z-index: 100; | ||
box-sizing: border-box; | ||
min-width: fit-content; | ||
} | ||
@@ -34,3 +38,3 @@ | ||
protected _onfocusout: (e: FocusEvent) => void = function (this: Popup, e: FocusEvent) { | ||
protected _onfocusout: (e: FocusEvent) => void = function (this: OxPopup, e: FocusEvent) { | ||
const to = e.relatedTarget as HTMLElement | ||
@@ -45,3 +49,3 @@ | ||
protected _onkeydown: (e: KeyboardEvent) => void = function (this: Popup, e: KeyboardEvent) { | ||
protected _onkeydown: (e: KeyboardEvent) => void = function (this: OxPopup, e: KeyboardEvent) { | ||
e.stopPropagation() | ||
@@ -57,15 +61,15 @@ | ||
protected _onkeyup: (e: KeyboardEvent) => void = function (this: Popup, e: KeyboardEvent) { | ||
protected _onkeyup: (e: KeyboardEvent) => void = function (this: OxPopup, e: KeyboardEvent) { | ||
e.stopPropagation() | ||
}.bind(this) | ||
protected _onclick: (e: MouseEvent) => void = function (this: Popup, e: MouseEvent) { | ||
protected _onclick: (e: MouseEvent) => void = function (this: OxPopup, e: MouseEvent) { | ||
e.stopPropagation() | ||
}.bind(this) | ||
protected _onclose: (e: Event) => void = function (this: Popup, e: Event) { | ||
protected _onclose: (e: Event) => void = function (this: OxPopup, e: Event) { | ||
this.close() | ||
}.bind(this) | ||
protected _oncollapse: (e: Event) => void = function (this: Popup, e: Event) { | ||
protected _oncollapse: (e: Event) => void = function (this: OxPopup, e: Event) { | ||
e.stopPropagation() | ||
@@ -75,3 +79,3 @@ this.close() | ||
protected _onwindowblur: (e: Event) => void = function (this: Popup, e: Event) { | ||
protected _onwindowblur: (e: Event) => void = function (this: OxPopup, e: Event) { | ||
// @ts-ignore for debug | ||
@@ -91,5 +95,2 @@ !window.POPUP_DEBUG && this.close() | ||
/* When the window is out of focus, all pop-ups should disappear. */ | ||
window.addEventListener('blur', this._onwindowblur) | ||
this.setAttribute('tabindex', '0') // make this element focusable | ||
@@ -118,25 +119,41 @@ this.guaranteeFocus() | ||
left, | ||
right, | ||
bottom, | ||
parent | ||
}: { | ||
template: unknown | ||
top: number | ||
left: number | ||
top?: number | ||
left?: number | ||
right?: number | ||
bottom?: number | ||
parent?: Element | null | ||
}) { | ||
const owner = parent || document.body | ||
const target = document.createElement('ox-popup') as Popup | ||
const target = document.createElement('ox-popup') as OxPopup | ||
render(template, target) | ||
target.style.left = `${left}px` | ||
target.style.top = `${top}px` | ||
target.setAttribute('active', '') | ||
target._parent = owner | ||
owner.appendChild(target) | ||
target.open({ top, left, right, bottom }) | ||
} | ||
open({ left = 0, top = 0, silent = false }: { left?: number; top?: number; silent?: boolean }) { | ||
this.style.left = `${left}px` | ||
this.style.top = `${top}px` | ||
open({ | ||
left, | ||
top, | ||
right, | ||
bottom, | ||
silent = false | ||
}: { | ||
left?: number | ||
top?: number | ||
right?: number | ||
bottom?: number | ||
silent?: boolean | ||
}) { | ||
if (left !== undefined) this.style.left = `${left}px` | ||
if (top !== undefined) this.style.top = `${top}px` | ||
if (right !== undefined) this.style.right = `${right}px` | ||
if (bottom !== undefined) this.style.bottom = `${bottom}px` | ||
@@ -146,2 +163,5 @@ this.setAttribute('active', '') | ||
!silent && this.guaranteeFocus() | ||
/* When the window is out of focus, all pop-ups should disappear. */ | ||
window.addEventListener('blur', this._onwindowblur) | ||
} | ||
@@ -164,4 +184,6 @@ | ||
window.removeEventListener('blur', this._onwindowblur) | ||
if (this._parent) { | ||
/* this case is when the popup is opened by Popup.open(...) */ | ||
/* this case is when the popup is opened by OxPopup.open(...) */ | ||
this.removeEventListener('focusout', this._onfocusout) | ||
@@ -174,4 +196,2 @@ this.removeEventListener('keydown', this._onkeydown) | ||
window.removeEventListener('blur', this._onwindowblur) | ||
this._parent.removeChild(this) | ||
@@ -178,0 +198,0 @@ delete this._parent |
import '../src/ox-popup-menu' | ||
import { html } from 'lit' | ||
import { expect, fixture } from '@open-wc/testing' | ||
import { PopupMenu } from '../src/ox-popup-menu' | ||
import { OxPopupMenu } from '../src/ox-popup-menu' | ||
import { html } from 'lit' | ||
describe('PopupMenu', () => { | ||
describe('OxPopupMenu', () => { | ||
it('has a default title "Hey there" and counter 5', async () => { | ||
const el = await fixture<PopupMenu>(html`<ox-popup-menu></ox-popup-menu>`) | ||
const el = await fixture<OxPopupMenu>(html`<ox-popup-menu></ox-popup-menu>`) | ||
@@ -18,3 +17,3 @@ expect(el.title).to.equal('Hey there') | ||
it('increases the counter on button click', async () => { | ||
const el = await fixture<PopupMenu>(html`<ox-popup-menu></ox-popup-menu>`) | ||
const el = await fixture<OxPopupMenu>(html`<ox-popup-menu></ox-popup-menu>`) | ||
el.shadowRoot!.querySelector('button')!.click() | ||
@@ -26,3 +25,3 @@ | ||
it('can override the title via attribute', async () => { | ||
const el = await fixture<PopupMenu>(html`<ox-popup-menu title="attribute title"></ox-popup-menu>`) | ||
const el = await fixture<OxPopupMenu>(html`<ox-popup-menu title="attribute title"></ox-popup-menu>`) | ||
@@ -33,3 +32,3 @@ expect(el.title).to.equal('attribute title') | ||
it('passes the a11y audit', async () => { | ||
const el = await fixture<PopupMenu>(html`<ox-popup-menu></ox-popup-menu>`) | ||
const el = await fixture<OxPopupMenu>(html`<ox-popup-menu></ox-popup-menu>`) | ||
@@ -36,0 +35,0 @@ await expect(el).shadowDom.to.be.accessible() |
import '../src/ox-popup' | ||
import { html } from 'lit' | ||
import { expect, fixture } from '@open-wc/testing' | ||
import { Popup } from '../src/ox-popup' | ||
import { OxPopup } from '../src/ox-popup' | ||
import { html } from 'lit' | ||
describe('Popup', () => { | ||
it('has a default title "Hey there" and counter 5', async () => { | ||
const el = await fixture<Popup>(html`<ox-popup></ox-popup>`) | ||
const el = await fixture<OxPopup>(html`<ox-popup></ox-popup>`) | ||
@@ -18,3 +17,3 @@ expect(el.title).to.equal('Hey there') | ||
it('increases the counter on button click', async () => { | ||
const el = await fixture<Popup>(html`<ox-popup></ox-popup>`) | ||
const el = await fixture<OxPopup>(html`<ox-popup></ox-popup>`) | ||
el.shadowRoot!.querySelector('button')!.click() | ||
@@ -26,3 +25,3 @@ | ||
it('can override the title via attribute', async () => { | ||
const el = await fixture<Popup>(html`<ox-popup title="attribute title"></ox-popup>`) | ||
const el = await fixture<OxPopup>(html`<ox-popup title="attribute title"></ox-popup>`) | ||
@@ -33,3 +32,3 @@ expect(el.title).to.equal('attribute title') | ||
it('passes the a11y audit', async () => { | ||
const el = await fixture<Popup>(html`<ox-popup></ox-popup>`) | ||
const el = await fixture<OxPopup>(html`<ox-popup></ox-popup>`) | ||
@@ -36,0 +35,0 @@ await expect(el).shadowDom.to.be.accessible() |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
188591
1794
3
+ Added@operato/styles@^0.2.36
+ Added@operato/styles@0.2.52(transitive)