11import { css , html , LitElement } from 'lit'
2+ import { render as renderPortal } from 'lit/html.js'
23import { downArrowIcon } from '../shared/downArrow'
34import { renderListbox } from '../shared/listboxTemplate'
45import { SelectOption } from '../shared/optionTypes'
@@ -16,6 +17,9 @@ import {
1617/* Prompt: can you wire up the keyboard interactions and aria attributes for Select */
1718export class Select extends LitElement {
1819 private static _nextId = 0
20+ private _popupPortalHost : HTMLDivElement | null = null
21+ private _popupPortalRoot : ShadowRoot | null = null
22+ private _popupPortalContainer : Element | null = null
1923 private readonly _handleDocumentPointerDown = ( event : Event ) => {
2024 const eventTarget = event . target
2125
@@ -34,11 +38,23 @@ export class Select extends LitElement {
3438 return
3539 }
3640
41+ if (
42+ ( this . _popupPortalHost && eventPath . includes ( this . _popupPortalHost ) ) ||
43+ ( this . _popupPortalRoot && eventPath . includes ( this . _popupPortalRoot ) )
44+ ) {
45+ return
46+ }
47+
3748 if ( ! this . contains ( eventTarget ) ) {
3849 this . _closePopup ( )
3950 }
4051 }
4152
53+ private readonly _handleViewportChange = ( ) => {
54+ if ( ! this . _popupOpen ) return
55+ this . _updatePopupPosition ( )
56+ }
57+
4258 static properties = {
4359 label : { type : String , reflect : true } ,
4460 theme : { type : String , reflect : true } ,
@@ -268,20 +284,105 @@ export class Select extends LitElement {
268284 connectedCallback ( ) {
269285 super . connectedCallback ( )
270286 document . addEventListener ( 'pointerdown' , this . _handleDocumentPointerDown )
287+ window . addEventListener ( 'resize' , this . _handleViewportChange )
288+ window . addEventListener ( 'scroll' , this . _handleViewportChange , true )
271289 }
272290
273291 disconnectedCallback ( ) {
292+ this . _detachPopupPortal ( )
274293 document . removeEventListener ( 'pointerdown' , this . _handleDocumentPointerDown )
294+ window . removeEventListener ( 'resize' , this . _handleViewportChange )
295+ window . removeEventListener ( 'scroll' , this . _handleViewportChange , true )
275296 super . disconnectedCallback ( )
276297 }
277298
278299 protected updated ( ) {
279300 this . toggleAttribute ( 'popup-open' , this . _popupOpen )
301+
302+ if ( this . _popupOpen ) {
303+ this . _updatePopupPosition ( )
304+ if ( this . _popupPortalRoot ) {
305+ renderPortal ( this . _renderPopup ( ) , this . _popupPortalRoot )
306+ }
307+ } else if ( this . _popupPortalRoot ) {
308+ renderPortal ( null , this . _popupPortalRoot )
309+ }
310+ }
311+
312+ private _getPopupPortalContainer ( ) {
313+ return this . closest ( 'dialog[open]' ) || document . body
314+ }
315+
316+ private _ensurePopupPortal ( ) {
317+ const nextContainer = this . _getPopupPortalContainer ( )
318+
319+ if (
320+ this . _popupPortalHost &&
321+ this . _popupPortalRoot &&
322+ this . _popupPortalContainer === nextContainer
323+ ) {
324+ return
325+ }
326+
327+ this . _detachPopupPortal ( )
328+
329+ this . _popupPortalHost = document . createElement ( 'div' )
330+ this . _popupPortalHost . setAttribute ( 'data-solid-ui-select-portal' , '' )
331+ this . _popupPortalHost . style . position = 'fixed'
332+ this . _popupPortalHost . style . inset = '0 auto auto 0'
333+ this . _popupPortalHost . style . zIndex = '2147483647'
334+ this . _popupPortalHost . style . pointerEvents = 'none'
335+ this . _popupPortalHost . style . boxSizing = 'border-box'
336+
337+ this . _popupPortalRoot = this . _popupPortalHost . attachShadow ( { mode : 'open' } )
338+ const styleSheets = ( Array . isArray ( Select . styles ) ? Select . styles : [ Select . styles ] )
339+ . map ( ( style ) => style ?. styleSheet )
340+ . filter ( ( styleSheet ) : styleSheet is CSSStyleSheet => Boolean ( styleSheet ) )
341+
342+ if ( styleSheets . length > 0 ) {
343+ this . _popupPortalRoot . adoptedStyleSheets = styleSheets
344+ }
345+
346+ nextContainer . appendChild ( this . _popupPortalHost )
347+ this . _popupPortalContainer = nextContainer
348+ }
349+
350+ private _detachPopupPortal ( ) {
351+ if ( this . _popupPortalRoot ) {
352+ renderPortal ( null , this . _popupPortalRoot )
353+ }
354+
355+ if ( this . _popupPortalHost ?. parentNode ) {
356+ this . _popupPortalHost . parentNode . removeChild ( this . _popupPortalHost )
357+ }
358+
359+ this . _popupPortalHost = null
360+ this . _popupPortalRoot = null
361+ this . _popupPortalContainer = null
362+ }
363+
364+ private _updatePopupPosition ( ) {
365+ this . _ensurePopupPortal ( )
366+
367+ const rect = this . getBoundingClientRect ( )
368+ const maxHeight = Math . min ( 288 , Math . max ( 120 , window . innerHeight - rect . bottom - 12 ) )
369+
370+ if ( this . _popupPortalHost ) {
371+ this . _popupPortalHost . style . top = `${ Math . round ( rect . bottom + 2 ) } px`
372+ this . _popupPortalHost . style . left = `${ Math . round ( rect . left ) } px`
373+ this . _popupPortalHost . style . width = `${ Math . round ( rect . width ) } px`
374+ this . _popupPortalHost . style . maxHeight = `${ Math . round ( maxHeight ) } px`
375+ this . _popupPortalHost . style . height = '0px'
376+ }
280377 }
281378
282379 private _closePopup ( ) {
283380 this . _popupOpen = false
284381 this . _activeIndex = - 1
382+
383+ if ( this . _popupPortalRoot ) {
384+ renderPortal ( null , this . _popupPortalRoot )
385+ }
285386 }
286387
287388 private _getSelectedIndex ( ) {
@@ -345,6 +446,7 @@ export class Select extends LitElement {
345446 const popupOptions = this . _getDisplayedOptions ( )
346447
347448 this . _popupOpen = true
449+ this . _updatePopupPosition ( )
348450 this . _activeIndex = findOptionIndexByValue ( popupOptions , this . value )
349451
350452 if ( this . _activeIndex < 0 ) {
@@ -422,7 +524,7 @@ export class Select extends LitElement {
422524 const activeOption = this . _getActiveOption ( )
423525
424526 return html `
425- < div class ="popup-box " part ="popup-box ">
527+ < div class ="popup-box " part ="popup-box " style =" pointer-events: auto; max-height: inherit; overflow: auto; " >
426528 < div class ="select-options-section ">
427529 ${ renderListbox ( {
428530 selectedOption,
@@ -475,9 +577,7 @@ export class Select extends LitElement {
475577 @click ="${ ( e : MouseEvent ) => {
476578 if ( e . target === e . currentTarget ) this . _closePopup ( )
477579 } } "
478- >
479- ${ this . _popupOpen ? this . _renderPopup ( ) : '' }
480- </ div >
580+ > </ div >
481581 `
482582 }
483583}
0 commit comments