Skip to content

Commit 2732aad

Browse files
fix(picker): restore overlay declarative rendering to avoid VO crash on Safari (#5983)
1 parent f8bdeec commit 2732aad

File tree

3 files changed

+77
-41
lines changed

3 files changed

+77
-41
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@spectrum-web-components/picker': patch
3+
'@spectrum-web-components/action-menu': patch
4+
---
5+
6+
**Fixed**: Safari + VoiceOver crash when opening Picker and ActionMenu. The issue was caused by an imperative `render()` call that mutated the DOM during the render cycle, causing Safari to crash while VoiceOver scanned the accessibility tree.

1st-gen/packages/picker/src/InteractionController.ts

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -50,24 +50,11 @@ export class InteractionController implements ReactiveController {
5050
if (this._open === open) return;
5151
this._open = open;
5252

53-
if (this.overlay) {
54-
this.host.open = open;
55-
return;
56-
}
53+
this.host.open = open;
5754

58-
// When there is no Overlay and `open` is moving to `true`, lazily import/create
59-
// an Overlay and apply that state to it.
60-
customElements
61-
.whenDefined('sp-overlay')
62-
.then(async (): Promise<void> => {
63-
const { Overlay } = await import(
64-
'@spectrum-web-components/overlay/src/Overlay.js'
65-
);
66-
this.overlay = new Overlay();
67-
this.host.open = true;
68-
this.host.requestUpdate();
69-
});
70-
import('@spectrum-web-components/overlay/sp-overlay.js');
55+
if (!this.overlay && this.host.overlayElement) {
56+
this.overlay = this.host.overlayElement;
57+
}
7158
}
7259

7360
private _overlay!: AbstractOverlay;
@@ -110,8 +97,6 @@ export class InteractionController implements ReactiveController {
11097
if (this.preventNextToggle === 'no') {
11198
this.open = false;
11299
} else if (!this.pointerdownState) {
113-
// Prevent browser driven closure while opening the Picker
114-
// and the expected event series has not completed.
115100
this.overlay?.manuallyKeepOpen();
116101
}
117102
}
@@ -195,13 +180,12 @@ export class InteractionController implements ReactiveController {
195180
}
196181

197182
public hostUpdated(): void {
198-
if (
199-
this.overlay &&
200-
this.host.dependencyManager.loaded &&
201-
this.host.open !== this.overlay.open
202-
) {
183+
if (!this.overlay && this.host.overlayElement) {
184+
this.overlay = this.host.overlayElement;
185+
}
186+
187+
if (this.overlay && this.host.dependencyManager.loaded) {
203188
this.overlay.willPreventClose = this.preventNextToggle !== 'no';
204-
this.overlay.open = this.host.open;
205189
}
206190
}
207191
}

1st-gen/packages/picker/src/Picker.ts

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
html,
1717
nothing,
1818
PropertyValues,
19-
render,
2019
SizedMixin,
2120
SpectrumElement,
2221
TemplateResult,
@@ -438,6 +437,30 @@ export class PickerBase extends SizedMixin(SpectrumElement, {
438437

439438
public handleSlottableRequest = (_event: SlottableRequestEvent): void => {};
440439

440+
protected handleBeforetoggle = (
441+
event: Event & {
442+
target: Overlay;
443+
newState: 'open' | 'closed';
444+
}
445+
): void => {
446+
if (event.composedPath()[0] !== event.target) {
447+
return;
448+
}
449+
if (event.newState === 'closed') {
450+
if (this.strategy?.preventNextToggle === 'no') {
451+
this.open = false;
452+
} else if (!this.strategy?.pointerdownState) {
453+
// Prevent browser driven closure while opening the Picker
454+
// and the expected event series has not completed.
455+
this.overlayElement?.manuallyKeepOpen();
456+
}
457+
}
458+
if (!this.open) {
459+
this.optionsMenu.updateSelectedItemIndex();
460+
this.optionsMenu.closeDescendentOverlays();
461+
}
462+
};
463+
441464
protected renderLabelContent(content: Node[]): TemplateResult | Node[] {
442465
if (this.value && this.selectedItem) {
443466
return content;
@@ -577,14 +600,30 @@ export class PickerBase extends SizedMixin(SpectrumElement, {
577600
}
578601

579602
protected renderOverlay(menu: TemplateResult): TemplateResult {
580-
if (this.strategy?.overlay === undefined) {
581-
return menu;
582-
}
583603
const container = this.renderContainer(menu);
584-
render(container, this.strategy?.overlay as unknown as HTMLElement, {
585-
host: this,
586-
});
587-
return this.strategy?.overlay as unknown as TemplateResult;
604+
this.dependencyManager.add('sp-overlay');
605+
import('@spectrum-web-components/overlay/sp-overlay.js');
606+
return html`
607+
<sp-overlay
608+
@slottable-request=${this.handleSlottableRequest}
609+
@beforetoggle=${this.handleBeforetoggle}
610+
.triggerElement=${this as HTMLElement}
611+
.offset=${0}
612+
?open=${this.open && this.dependencyManager.loaded}
613+
.placement=${this.isMobile.matches && !this.forcePopover
614+
? undefined
615+
: this.placement}
616+
.type=${this.isMobile.matches && !this.forcePopover
617+
? 'modal'
618+
: 'auto'}
619+
.receivesFocus=${'false'}
620+
.willPreventClose=${this.strategy?.preventNextToggle !== 'no' &&
621+
this.open &&
622+
this.dependencyManager.loaded}
623+
>
624+
${container}
625+
</sp-overlay>
626+
`;
588627
}
589628

590629
protected get renderDescriptionSlot(): TemplateResult {
@@ -662,7 +701,7 @@ export class PickerBase extends SizedMixin(SpectrumElement, {
662701
// await the same here.
663702
this.shouldScheduleManageSelection();
664703
}
665-
// Maybe it's finally time to remove this support?s
704+
// Maybe it's finally time to remove this support?
666705
if (!this.hasUpdated) {
667706
this.deprecatedMenu = this.querySelector(':scope > sp-menu');
668707
this.deprecatedMenu?.toggleAttribute('ignore', true);
@@ -697,15 +736,26 @@ export class PickerBase extends SizedMixin(SpectrumElement, {
697736

698737
protected override updated(changes: PropertyValues<this>): void {
699738
super.updated(changes);
700-
if (changes.has('open')) {
701-
this.strategy.open = this.open;
739+
if (
740+
changes.has('open') &&
741+
this.overlayElement &&
742+
!this.strategy.overlay
743+
) {
744+
this.strategy.overlay = this.overlayElement;
702745
}
703746
}
704747

705-
protected override firstUpdated(changes: PropertyValues<this>): void {
748+
protected override async firstUpdated(
749+
changes: PropertyValues<this>
750+
): Promise<void> {
706751
super.firstUpdated(changes);
707752
this.bindButtonKeydownListener();
708753
this.bindEvents();
754+
755+
await this.updateComplete;
756+
if (this.overlayElement && !this.strategy.overlay) {
757+
this.strategy.overlay = this.overlayElement;
758+
}
709759
}
710760

711761
protected get dismissHelper(): TemplateResult {
@@ -815,7 +865,6 @@ export class PickerBase extends SizedMixin(SpectrumElement, {
815865
((event.target as HTMLElement).getRootNode() as ShadowRoot)
816866
.host === this)
817867
) {
818-
//s set a flag to manage selection on the next frame
819868
this.willManageSelection = true;
820869
requestAnimationFrame(() => {
821870
requestAnimationFrame(() => {
@@ -881,9 +930,6 @@ export class PickerBase extends SizedMixin(SpectrumElement, {
881930
protected override async getUpdateComplete(): Promise<boolean> {
882931
const complete = (await super.getUpdateComplete()) as boolean;
883932
await this.selectionPromise;
884-
// if (this.overlayElement) {
885-
// await this.overlayElement.updateComplete;
886-
// }
887933
return complete;
888934
}
889935

0 commit comments

Comments
 (0)