import { classMap } from 'lit/directives/class-map.js' import { customElement, property, state } from 'lit/decorators.js' import { html, nothing, PropertyValues } from 'lit' import { PktElementWithSlot } from '@/base-elements/element-with-slot' import { slotContent } from '@/directives/slot-content' import { ref } from 'lit/directives/ref.js' import { Ref, createRef } from 'lit/directives/ref.js' import { TPktSize } from '@/types/size' import { createScrollLockToken, setScrollLock, type ScrollLockToken, } from 'shared-utils/scroll-lock' import specs from 'componentSpecs/modal.json' import '@/components/icon' type ModalSize = TPktSize | 'fit-content' export interface IPktModal { open?: boolean headingText?: string removePadding?: boolean hideCloseButton?: boolean closeOnBackdropClick?: boolean closeButtonSkin?: 'blue' | 'yellow-filled' size?: ModalSize variant?: 'dialog' | 'drawer' drawerPosition?: 'left' | 'right' transparentBackdrop?: boolean } export class PktModal extends PktElementWithSlot implements IPktModal { // Public properties @property({ type: Boolean, reflect: true }) open?: boolean = false @property({ type: String }) headingText?: string = '' @property({ type: Boolean }) removePadding?: boolean = false @property({ type: Boolean }) hideCloseButton?: boolean = specs.props.hideCloseButton.default @property({ type: Boolean }) closeOnBackdropClick?: boolean = specs.props.closeOnBackdropClick.default @property({ type: String }) closeButtonSkin?: 'blue' | 'yellow-filled' = 'blue' @property({ type: String }) size?: ModalSize = specs.props.size.default as ModalSize @property({ type: String }) variant?: 'dialog' | 'drawer' = 'dialog' @property({ type: String }) drawerPosition?: 'left' | 'right' = 'right' @property({ type: Boolean }) transparentBackdrop?: boolean = false dialogRef: Ref = createRef() private _scrollLockToken: ScrollLockToken = createScrollLockToken() @state() _isOpen: boolean = false constructor() { super() this._isOpen = false } async connectedCallback(): Promise { super.connectedCallback() document.addEventListener('keydown', this.handleKeyDown.bind(this)) document.addEventListener('click', this.handleBackdropClick.bind(this)) } disconnectedCallback(): void { super.disconnectedCallback() document.removeEventListener('keydown', this.handleKeyDown) document.removeEventListener('click', this.handleBackdropClick) // Rydd opp scroll-lock hvis elementet fjernes fra DOM mens det er åpent setScrollLock(this._scrollLockToken, false) } protected updated(changedProperties: PropertyValues): void { super.updated(changedProperties) if (changedProperties.has('open')) { if (this.open && !this._isOpen) { this.showModal() } else if (!this.open && this._isOpen) { this.close(new Event('close'), false) } } } protected async firstUpdated(_changedProperties: PropertyValues): Promise { super.firstUpdated(_changedProperties) if (this.dialogRef.value && !window.HTMLDialogElement && !this.dialogRef.value.showModal) { if ('document' in window && 'createElement' in document) { const dialogPolyfill = await import('dialog-polyfill').then((module) => module.default) dialogPolyfill.registerDialog(this.dialogRef.value) } this.dialogRef.value.addEventListener('close', () => { this.close(new Event('close'), true) }) } } // P R I V A T E M E T H O D S private handleKeyDown(event: KeyboardEvent) { if (event.key === 'Escape') { this.close(event) } } private handleBackdropClick(event: MouseEvent) { if (this.closeOnBackdropClick && event.target === this.dialogRef?.value) { this.close(event) } } private isElementInViewport(element: HTMLElement): boolean { const rect = element.getBoundingClientRect() return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ) } // P U B L I C M E T H O D S public close = (event: Event, bypassNativeClose: boolean = false) => { if (!this._isOpen) return this._isOpen = false this.open = false setScrollLock(this._scrollLockToken, false) // Scroll to the opener element if it's not in the viewport const openerElement = document.activeElement as HTMLElement if (openerElement && !this.isElementInViewport(openerElement)) { openerElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) } // Dispatch close event this.dispatchEvent( new CustomEvent('close', { detail: { origin: event }, bubbles: true, composed: true }), ) if (!bypassNativeClose) this.dialogRef.value?.close() this.requestUpdate() } public showModal = (event: Event | null = null) => { this._isOpen = true this.open = true this.dialogRef.value?.showModal() setScrollLock(this._scrollLockToken, true) this.dispatchEvent( new CustomEvent('showModal', { detail: { origin: event }, bubbles: true, composed: true }), ) this.requestUpdate() } render() { const classes = { 'pkt-modal': true, 'pkt-modal--removePadding': this.removePadding ?? false, 'pkt-modal--noHeadingText': this.headingText === '' || this.headingText === undefined, 'pkt-modal--noShadow': this.closeButtonSkin === 'yellow-filled', 'pkt-modal--transparentBackdrop': this.transparentBackdrop ?? false, [`pkt-modal--${this.size}`]: this.size !== undefined, [`pkt-modal__${this.variant}`]: this.variant !== undefined, [`pkt-modal__drawer--${this.drawerPosition}`]: this.variant === 'drawer', } const headingClasses = { 'pkt-modal__headingText': true, 'pkt-txt-24': true, } const contentClasses = { 'pkt-modal__content': true, 'pkt-txt-18-light': true, } const isCloseButtonSkinDefault = this.closeButtonSkin === 'blue' const closeButtonClasses = { 'pkt-modal__closeButton': true, [`pkt-modal__closeButton--${this.closeButtonSkin}`]: true, } return html` this.close(event, true)} >
${this.headingText || !this.hideCloseButton ? html`
${this.headingText ? html`

${this.headingText}

` : html`
`} ${!this.hideCloseButton ? html`
this.close(event)} aria-label="close" iconname="close" variant="icon-only" size="medium" skin=${isCloseButtonSkinDefault ? 'tertiary' : 'primary'} > Lukk
` : html`
`}
` : nothing}
${slotContent(this)}
` } } try { customElement('pkt-modal')(PktModal) } catch (e) { console.warn('Forsøker å definere , men den er allerede definert') }