@@ -10,9 +10,13 @@ OF ANY KIND, either express or implied. See the License for the specific languag
10
10
governing permissions and limitations under the License.
11
11
*/
12
12
13
- import type { ReactiveController } from '@spectrum-web-components/base' ;
13
+ import {
14
+ ReactiveController ,
15
+ TemplateResult ,
16
+ } from '@spectrum-web-components/base' ;
14
17
import { 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' ;
16
20
17
21
export enum InteractionTypes {
18
22
'desktop' ,
@@ -22,47 +26,143 @@ export enum InteractionTypes {
22
26
export class InteractionController implements ReactiveController {
23
27
abortController ! : AbortController ;
24
28
29
+ public preventNextToggle : 'no' | 'maybe' | 'yes' = 'no' ;
30
+ public pointerdownState = false ;
31
+ public enterKeydownOn : EventTarget | null = null ;
32
+
33
+ public container ! : TemplateResult ;
34
+
25
35
get activelyOpening ( ) : boolean {
26
36
return false ;
27
37
}
28
38
29
- private handleOverlayReady ?: ( overlay : AbstractOverlay ) => void ;
39
+ private _open = false ;
30
40
31
41
public get open ( ) : boolean {
32
- return this . host . open ;
42
+ return this . _open ;
33
43
}
34
44
35
45
/**
36
46
* Set `open`
37
47
*/
38
48
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 ;
40
81
}
41
82
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 ( ) ;
44
88
}
45
89
46
90
type ! : InteractionTypes ;
47
91
48
92
constructor (
49
93
public target : HTMLElement ,
50
- public overlay : AbstractOverlay | undefined ,
51
94
public host : PickerBase
52
95
) {
53
96
this . target = target ;
54
- this . overlay = overlay ;
55
97
this . host = host ;
56
98
this . init ( ) ;
57
99
}
58
100
59
101
releaseDescription ( ) : void { }
60
102
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
+ }
64
125
}
65
126
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
+
66
166
abort ( ) : void {
67
167
this . releaseDescription ( ) ;
68
168
this . abortController ?. abort ( ) ;
@@ -73,8 +173,6 @@ export class InteractionController implements ReactiveController {
73
173
}
74
174
75
175
hostDisconnected ( ) : void {
76
- if ( ! this . isPersistent ) {
77
- this . abort ( ) ;
78
- }
176
+ this . abortController ?. abort ( ) ;
79
177
}
80
178
}
0 commit comments