Skip to content

Commit 757b90e

Browse files
committed
fix: separated interaction strategy for picker
1 parent 6543a94 commit 757b90e

File tree

4 files changed

+190
-131
lines changed

4 files changed

+190
-131
lines changed

packages/picker/src/DesktopController.ts

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,14 @@ import {
1616
} from './InteractionController.js';
1717

1818
export class DesktopController extends InteractionController {
19-
override type = InteractionTypes.click;
19+
override type = InteractionTypes.desktop;
2020

21-
/**
22-
* An overlay with a `click` interaction should not close on click `triggerElement`.
23-
* When a click is initiated (`pointerdown`), apply `preventNextToggle` when the
24-
* overlay is `open` to prevent from toggling the overlay when the click event
25-
* propagates later in the interaction.
26-
*/
27-
// private preventNextToggle: 'no' | 'maybe' | 'yes' = 'no';
28-
// private pointerdownState = false;
29-
30-
protected handlePointerdown(event: PointerEvent): void {
21+
public override handlePointerdown(event: PointerEvent): void {
3122
if (event.button !== 0) {
3223
return;
3324
}
34-
this.host.pointerdownState = this.open;
35-
// this.pointerdownState = this.open;
36-
// this.preventNextToggle = 'maybe';
37-
this.host.preventNextToggle = 'maybe';
25+
this.pointerdownState = this.open;
26+
this.preventNextToggle = 'maybe';
3827
let cleanupAction = 0;
3928
const cleanup = (): void => {
4029
cancelAnimationFrame(cleanupAction);
@@ -45,7 +34,7 @@ export class DesktopController extends InteractionController {
4534
requestAnimationFrame(() => {
4635
// Complete cleanup on the second animation frame so that `click` can go first.
4736
// this.preventNextToggle = 'no';
48-
this.host.preventNextToggle = 'no';
37+
this.preventNextToggle = 'no';
4938
});
5039
});
5140
};
@@ -56,34 +45,19 @@ export class DesktopController extends InteractionController {
5645
this.handleActivate();
5746
}
5847

59-
protected handleButtonFocus(event: FocusEvent): void {
60-
// When focus comes from a pointer event, and the related target is the Menu,
61-
// we don't want to reopen the Menu.
62-
if (
63-
this.host.preventNextToggle === 'maybe' &&
64-
event.relatedTarget === this.host.optionsMenu
65-
) {
66-
// this.preventNextToggle = 'yes';
67-
this.host.preventNextToggle = 'yes';
68-
}
69-
}
70-
71-
protected handleActivate(event?: Event): void {
72-
if (this.enterKeydownOn && this.enterKeydownOn !== this.button) {
48+
public override handleActivate(event?: Event): void {
49+
if (this.enterKeydownOn && this.enterKeydownOn !== this.target) {
7350
return;
7451
}
75-
if (this.host.preventNextToggle === 'yes') {
52+
if (this.preventNextToggle === 'yes') {
7653
return;
7754
}
78-
if (
79-
event?.type === 'click' &&
80-
this.open !== this.host.pointerdownState
81-
) {
55+
if (event?.type === 'click' && this.open !== this.pointerdownState) {
8256
// When activation comes from a `click` event ensure that the `pointerup`
8357
// event didn't already toggle the Picker state before doing so.
8458
return;
8559
}
86-
this.host.toggle();
60+
this.open = !this.open;
8761
}
8862

8963
override init(): void {
@@ -93,7 +67,7 @@ export class DesktopController extends InteractionController {
9367
const { signal } = this.abortController;
9468
this.target.addEventListener(
9569
'click',
96-
(event: PointerEvent) => this.handleActivate(event),
70+
(event: Event) => this.handleActivate(event),
9771
{
9872
signal,
9973
}

packages/picker/src/InteractionController.ts

Lines changed: 113 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ OF ANY KIND, either express or implied. See the License for the specific languag
1010
governing 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';
1417
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';
1620

1721
export enum InteractionTypes {
1822
'desktop',
@@ -22,47 +26,143 @@ export enum InteractionTypes {
2226
export 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
}

packages/picker/src/MobileController.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,17 @@ import {
1515
} from './InteractionController.js';
1616

1717
export class MobileController extends InteractionController {
18-
override type = InteractionTypes.click;
19-
20-
/**
21-
* An overlay with a `click` interaction should not close on click `triggerElement`.
22-
* When a click is initiated (`pointerdown`), apply `preventNextToggle` when the
23-
* overlay is `open` to prevent from toggling the overlay when the click event
24-
* propagates later in the interaction.
25-
*/
26-
private preventNextToggle = false;
18+
override type = InteractionTypes.mobile;
2719

2820
handleClick(): void {
29-
if (!this.preventNextToggle) {
30-
this.host.open = !this.host.open;
21+
if (this.preventNextToggle == 'no') {
22+
this.open = !this.open;
3123
}
32-
this.preventNextToggle = false;
24+
this.preventNextToggle = 'no';
3325
}
3426

35-
handlePointerdown(): void {
36-
this.preventNextToggle = this.host.open;
27+
public override handlePointerdown(): void {
28+
this.preventNextToggle = this.open ? 'yes' : 'no';
3729
}
3830

3931
override init(): void {
@@ -49,5 +41,12 @@ export class MobileController extends InteractionController {
4941
() => this.handlePointerdown(),
5042
{ signal }
5143
);
44+
this.target.addEventListener(
45+
'focus',
46+
(event: FocusEvent) => this.handleButtonFocus(event),
47+
{
48+
signal,
49+
}
50+
);
5251
}
5352
}

0 commit comments

Comments
 (0)