@@ -10,9 +10,13 @@ OF ANY KIND, either express or implied. See the License for the specific languag
1010governing permissions and limitations under the License.
1111*/
1212
13- import type { ReactiveController } from '@spectrum-web-components/base' ;
13+ import {
14+ ReactiveController ,
15+ TemplateResult ,
16+ } from '@spectrum-web-components/base' ;
1417import { AbstractOverlay } from '@spectrum-web-components/overlay/src/AbstractOverlay' ;
15- import { PickerBase } from './PickerBase.js' ;
18+ import { Overlay } from '@spectrum-web-components/overlay/src/Overlay.js' ;
19+ import { PickerBase } from './Picker.js' ;
1620
1721export enum InteractionTypes {
1822 'desktop' ,
@@ -22,47 +26,143 @@ export enum InteractionTypes {
2226export class InteractionController implements ReactiveController {
2327 abortController ! : AbortController ;
2428
29+ public preventNextToggle : 'no' | 'maybe' | 'yes' = 'no' ;
30+ public pointerdownState = false ;
31+ public enterKeydownOn : EventTarget | null = null ;
32+
33+ public container ! : TemplateResult ;
34+
2535 get activelyOpening ( ) : boolean {
2636 return false ;
2737 }
2838
29- private handleOverlayReady ?: ( overlay : AbstractOverlay ) => void ;
39+ private _open = false ;
3040
3141 public get open ( ) : boolean {
32- return this . host . open ;
42+ return this . _open ;
3343 }
3444
3545 /**
3646 * Set `open`
3747 */
3848 public set open ( open : boolean ) {
39- this . host . open = open ;
49+ this . _open = open ;
50+ if ( this . overlay ) {
51+ // If there already is an Overlay, apply the value of `open` directly.
52+ this . overlay . open = open ;
53+ this . host . open = open ;
54+ return ;
55+ }
56+ if ( ! open ) {
57+ this . host . open = open ;
58+ // When `open` moves to `false` and there is not yet an Overlay,
59+ // assume that no Overlay and a closed Overlay are the same and return early.
60+ return ;
61+ }
62+ // When there is no Overlay and `open` is moving to `true`, lazily import/create
63+ // an Overlay and apply that state to it.
64+ customElements
65+ . whenDefined ( 'sp-overlay' )
66+ . then ( async ( ) : Promise < void > => {
67+ const { Overlay } = await import (
68+ '@spectrum-web-components/overlay/src/Overlay.js'
69+ ) ;
70+ this . overlay = new Overlay ( ) ;
71+ this . overlay . open = true ;
72+ this . host . open = true ;
73+ } ) ;
74+ import ( '@spectrum-web-components/overlay/sp-overlay.js' ) ;
75+ }
76+
77+ private _overlay ! : AbstractOverlay ;
78+
79+ public get overlay ( ) : AbstractOverlay {
80+ return this . _overlay ;
4081 }
4182
42- toggle ( target ?: boolean ) : void {
43- this . host . toggle ( target ) ;
83+ public set overlay ( overlay : AbstractOverlay | undefined ) {
84+ if ( ! overlay ) return ;
85+ if ( this . overlay === overlay ) return ;
86+ this . _overlay = overlay ;
87+ this . initOverlay ( ) ;
4488 }
4589
4690 type ! : InteractionTypes ;
4791
4892 constructor (
4993 public target : HTMLElement ,
50- public overlay : AbstractOverlay | undefined ,
5194 public host : PickerBase
5295 ) {
5396 this . target = target ;
54- this . overlay = overlay ;
5597 this . host = host ;
5698 this . init ( ) ;
5799 }
58100
59101 releaseDescription ( ) : void { }
60102
61- /* c8 ignore next 3 */
62- init ( ) : void {
63- // Abstract init() method.
103+ protected handleBeforetoggle (
104+ event : Event & {
105+ target : Overlay ;
106+ newState : 'open' | 'closed' ;
107+ }
108+ ) : void {
109+ if ( event . composedPath ( ) [ 0 ] !== event . target ) {
110+ return ;
111+ }
112+ if ( event . newState === 'closed' ) {
113+ if ( this . preventNextToggle === 'no' ) {
114+ this . open = false ;
115+ } else if ( ! this . pointerdownState ) {
116+ // Prevent browser driven closure while opening the Picker
117+ // and the expected event series has not completed.
118+ this . overlay ?. manuallyKeepOpen ( ) ;
119+ }
120+ }
121+ if ( ! this . open ) {
122+ this . host . optionsMenu . updateSelectedItemIndex ( ) ;
123+ this . host . optionsMenu . closeDescendentOverlays ( ) ;
124+ }
64125 }
65126
127+ initOverlay ( ) : void {
128+ if ( this . overlay ) {
129+ this . overlay . addEventListener ( 'beforetoggle' , ( event : Event ) => {
130+ this . handleBeforetoggle (
131+ event as Event & {
132+ target : Overlay ;
133+ newState : 'open' | 'closed' ;
134+ }
135+ ) ;
136+ } ) ;
137+
138+ this . overlay . triggerElement = this . host as HTMLElement ;
139+ this . overlay . placement = this . host . isMobile . matches
140+ ? undefined
141+ : this . host . placement ;
142+ this . overlay . receivesFocus = 'true' ;
143+ this . overlay . willPreventClose =
144+ this . preventNextToggle !== 'no' && this . open ;
145+ }
146+ }
147+
148+ public handlePointerdown ( _event : PointerEvent ) : void { }
149+
150+ public handleButtonFocus ( event : FocusEvent ) : void {
151+ // When focus comes from a pointer event, and the related target is the Menu,
152+ // we don't want to reopen the Menu.
153+ if (
154+ this . preventNextToggle === 'maybe' &&
155+ event . relatedTarget === this . host . optionsMenu
156+ ) {
157+ this . preventNextToggle = 'yes' ;
158+ }
159+ }
160+
161+ public handleActivate ( _event : Event ) : void { }
162+
163+ /* c8 ignore next 3 */
164+ init ( ) : void { }
165+
66166 abort ( ) : void {
67167 this . releaseDescription ( ) ;
68168 this . abortController ?. abort ( ) ;
@@ -73,8 +173,6 @@ export class InteractionController implements ReactiveController {
73173 }
74174
75175 hostDisconnected ( ) : void {
76- if ( ! this . isPersistent ) {
77- this . abort ( ) ;
78- }
176+ this . abortController ?. abort ( ) ;
79177 }
80178}
0 commit comments