Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/action-group/test/action-group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ describe('ActionGroup', () => {
expect(el.children[3]).to.equal(document.activeElement);
});

it.skip('action-group with action-menu manages tabIndex correctly while using mouse', async () => {
it('action-group with action-menu manages tabIndex correctly while using mouse', async () => {
const el = await fixture<ActionGroup>(
HasActionMenuAsChild({ label: 'Action Group' })
);
Expand Down
16 changes: 8 additions & 8 deletions packages/contextual-help/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,23 @@
"type": "module",
"exports": {
".": {
"default": "./src/index.js",
"development": "./src/index.dev.js"
"development": "./src/index.dev.js",
"default": "./src/index.js"
},
"./package.json": "./package.json",
"./sp-contextual-help.js": {
"default": "./sp-contextual-help.js",
"development": "./sp-contextual-help.dev.js"
"development": "./sp-contextual-help.dev.js",
"default": "./sp-contextual-help.js"
},
"./src/ContextualHelp.js": {
"default": "./src/ContextualHelp.js",
"development": "./src/ContextualHelp.dev.js"
"development": "./src/ContextualHelp.dev.js",
"default": "./src/ContextualHelp.js"
},
"./src/contextual-help-overrides.css.js": "./src/contextual-help-overrides.css.js",
"./src/contextual-help.css.js": "./src/contextual-help.css.js",
"./src/index.js": {
"default": "./src/index.js",
"development": "./src/index.dev.js"
"development": "./src/index.dev.js",
"default": "./src/index.js"
}
},
"main": "./src/index.js",
Expand Down
30 changes: 4 additions & 26 deletions packages/dialog/src/DialogBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import '@spectrum-web-components/underlay/sp-underlay.js';
import '@spectrum-web-components/button/sp-button.js';

// Leveraged in build systems that use aliasing to prevent multiple registrations: https://github.com/adobe/spectrum-web-components/pull/3225
// eslint-disable-next-line import/no-extraneous-dependencies
import '@spectrum-web-components/dialog/sp-dialog.js';
import modalWrapperStyles from '@spectrum-web-components/modal/src/modal-wrapper.css.js';
import modalStyles from '@spectrum-web-components/modal/src/modal.css.js';
Expand Down Expand Up @@ -155,41 +156,18 @@ export class DialogBase extends FocusVisiblePolyfillMixin(SpectrumElement) {
this.handleTransitionEvent(event);
}

private get hasTransitionDuration(): boolean {
const modal = this.shadowRoot.querySelector('.modal') as HTMLElement;

const modalTransitionDurations =
window.getComputedStyle(modal).transitionDuration;
for (const duration of modalTransitionDurations.split(','))
if (parseFloat(duration) > 0) return true;

const underlay = this.shadowRoot.querySelector(
'sp-underlay'
) as HTMLElement;

if (underlay) {
const underlayTransitionDurations =
window.getComputedStyle(underlay).transitionDuration;
for (const duration of underlayTransitionDurations.split(','))
if (parseFloat(duration) > 0) return true;
}

return false;
}

protected override update(changes: PropertyValues<this>): void {
if (changes.has('open') && changes.get('open') !== undefined) {
const hasTransitionDuration = this.hasTransitionDuration;
this.animating = true;
this.transitionPromise = new Promise((res) => {
this.resolveTransitionPromise = () => {
this.animating = false;
if (!this.open && hasTransitionDuration)
this.dispatchClosed();
res();
};
});
if (!this.open && !hasTransitionDuration) this.dispatchClosed();
if (!this.open) {
this.dispatchClosed();
}
}
super.update(changes);
}
Expand Down
1 change: 1 addition & 0 deletions packages/number-field/src/NumberField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,7 @@ export class NumberField extends TextfieldBase {
}

protected override updated(changes: PropertyValues<this>): void {
super.updated(changes);
if (!this.inputElement || !this.isConnected) {
// Prevent race conditions if inputElement is removed from DOM while a queued update is still running.
return;
Expand Down
131 changes: 121 additions & 10 deletions packages/overlay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,6 @@ This means that in both cases, if the transition is meant to be a part of the op
.triggerElement=${HTMLElement}
.triggerInteraction=${'click' | 'longpress' | 'hover'}
type=${'auto' | 'hint' | 'manual' | 'modal' | 'page'}
?allow-outside-click=${boolean}
></sp-overlay>
```

Expand Down Expand Up @@ -574,24 +573,136 @@ Common in `modal`/`page` overlays for full-screen content</sp-table-cell>
</sp-table-body>
</sp-table>

##### Deprecated Properties
The `type` of an Overlay outlines a number of things about the interaction model within which is works.

> **⚠️ Deprecation Notice**: The `allow-outside-click` property is deprecated and will be removed in a future version.
### Modal

The `allow-outside-click` property allows clicks outside the overlay to close it. **We do not recommend using this property for accessibility reasons** as it can cause unexpected behavior and accessibility issues. When set to `true`, it configures the focus trap to allow outside clicks, which may interfere with proper focus management and user expectations.
`'modal'` Overlays are opened with the `showModal()` method on a `<dialog>` element, which causes the Overlay to accept focus and trap the tab stop within the content of said Overlay.

They should be used when you need to ensure that the user has interacted with the content of the Overlay before continuing with their work. This is commonly used for dialogs that require a user to confirm or cancel an action before continuing.

```html
<!-- @deprecated Not recommended for accessibility reasons -->
<sp-overlay trigger="trigger@click" allow-outside-click="true">
<sp-popover>
<p>This overlay can be closed by clicking outside</p>
<sp-button id="trigger">open modal</sp-button>
<sp-overlay trigger="trigger@click" type="modal">
<sp-dialog-wrapper headline="Signin form" dismissable underlay>
<p>I am a modal type overlay.</p>
<sp-field-label>Enter your email</sp-field-label>
<sp-textfield placeholder="test@gmail.com"></sp-textfield>
<sp-action-button
onClick="
this.dispatchEvent(
new Event('close', {
bubbles: true,
composed: true,
})
);
"
>
Sign in
</sp-action-button>
</sp-dialog-wrapper>
</sp-overlay>
```

### Page

`'page'` Overlays will act in a similar manner to a `'modal'` Overlay, however they will not be allowed to close via the "light dismiss" algorithm (e.g. the Escape key).

A page overlay could be used for a full-screen menu on a mobile website. When the user clicks on the menu button, the entire screen is covered with the menu options.

```html
<sp-button id="trigger">open page</sp-button>
<sp-overlay trigger="trigger@click" type="page">
<sp-dialog-wrapper
headline="Full screen menu"
mode="fullscreenTakeover"
cancel-label="Close"
>
<p>I am a page type overlay.</p>
</sp-dialog-wrapper>
</sp-overlay>
```

### Hint

`'hint'` Overlays are much like tooltips so they are not just ephemeral, but they are delivered primarily as a visual helper and exist outside of the tab order. In this way, be sure _not_ to place interactive content within this type of Overlay.

This overlay type does not accept focus and does not interfere with the user's interaction with the rest of the page.

```html
<sp-button id="trigger">open hint</sp-button>
<sp-overlay trigger="trigger@hover" type="hint">
<sp-tooltip>
I am a hint type overlay. I am not interactive and will close when the
user interacts with the page.
</sp-tooltip>
</sp-overlay>
```

### Auto

`'auto'` Overlays provide a place for content that is ephemeral _and_ interactive. These Overlays can accept focus and remain open while interacting with their content. They will close when focus moves outside the overlay or when clicking elsewhere on the page.

```html
<sp-button id="trigger">Open Overlay</sp-button>
<sp-overlay trigger="trigger@click" type="auto" placement="bottom">
<sp-popover dialog>
<p>
My slider in overlay element:
<sp-slider label="Slider Label - Editable" editable></sp-slider>
</p>
</sp-popover>
</sp-overlay>
```

### Manual

`'manual'` Overlays act much like `'auto'` Overlays, but do not close when losing focus or interacting with other parts of the page.

Note: When a `'manual'` Overlay is at the top of the "overlay stack", it will still respond to the Escape key and close.

```html
<style>
.chat-container {
position: fixed;
bottom: 1em;
left: 1em;
}
</style>
<sp-button id="trigger">open manual</sp-button>
<sp-overlay trigger="trigger@click" type="manual">
<sp-popover class="chat-container">
<sp-dialog dismissable>
<span slot="heading">Chat Window</span>
<sp-textfield placeholder="Enter your message"></sp-textfield>
<sp-action-button>Send</sp-action-button>
</sp-dialog>
</sp-popover>
</sp-overlay>
```

**Alternative approaches**: Instead of using `allow-outside-click`, consider implementing explicit close buttons or using the `type="modal"` or `type="page"` overlay types which provide better accessibility and user experience.
### Events

When fully open the `<sp-overlay>` element will dispatch the `sp-opened` event, and when fully closed the `sp-closed` event will be dispatched. Both of these events are of type:

```ts
type OverlayStateEvent = Event & {
overlay: Overlay;
};
```

The `overlay` value in this case will hold a reference to the actual `<sp-overlay>` that is opening or closing to trigger this event. Remember that some `<sp-overlay>` element (like those creates via the imperative API) can be transiently available in the DOM, so if you choose to build a cache of Overlay elements to some end, be sure to leverage a weak reference so that the `<sp-overlay>` can be garbage collected as desired by the browser.

#### When it is "fully" open or closed?

"Fully" in this context means that all CSS transitions that have dispatched `transitionrun` events on the direct children of the `<sp-overlay>` element have successfully dispatched their `transitionend` or `transitioncancel` event. Keep in mind the following:

- `transition*` events bubble; this means that while transition events on light DOM content of those direct children will be heard, those events will not be taken into account
- `transition*` events are not composed; this means that transition events on shadow DOM content of the direct children will not propagate to a level in the DOM where they can be heard

This means that in both cases, if the transition is meant to be a part of the opening or closing of the overlay in question you will need to redispatch the `transitionrun`, `transitionend`, and `transitioncancel` events from that transition from the closest direct child of the `<sp-overlay>`.

#### Styling
## Styling

`<sp-overlay>` element will use the `<dialog>` element or `popover` attribute to project your content onto the top-layer of the browser, without being moved in the DOM tree. That means that you can style your overlay content with whatever techniques you are already leveraging to style the content that doesn't get overlaid. This means standard CSS selectors, CSS Custom Properties, and CSS Parts applied in your parent context will always apply to your overlaid content.

Expand Down
3 changes: 1 addition & 2 deletions packages/overlay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,7 @@
"@spectrum-web-components/base": "1.9.0",
"@spectrum-web-components/reactive-controllers": "1.9.0",
"@spectrum-web-components/shared": "1.9.0",
"@spectrum-web-components/theme": "1.9.0",
"focus-trap": "7.6.5"
"@spectrum-web-components/theme": "1.9.0"
},
"types": "./src/index.d.ts",
"customElements": "custom-elements.json",
Expand Down
4 changes: 4 additions & 0 deletions packages/overlay/src/AbstractOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ export class AbstractOverlay extends SpectrumElement {
return;
}
/* c8 ignore next 3 */
protected async manageDialogOpen(): Promise<void> {
return;
}
/* c8 ignore next 3 */
protected async managePopoverOpen(): Promise<void> {
return;
}
Expand Down
13 changes: 13 additions & 0 deletions packages/overlay/src/HoverController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
InteractionController,
InteractionTypes,
lastInteractionType,
SAFARI_FOCUS_RING_CLASS,
} from './InteractionController.js';

const HOVER_DELAY = 300;
Expand All @@ -36,6 +37,7 @@ export class HoverController extends InteractionController {
handleKeyup(event: KeyboardEvent): void {
if (event.code === 'Tab' || event.code === 'Escape') {
this.open = true;
this.removeSafariFocusRingClass();
}
}

Expand All @@ -48,14 +50,17 @@ export class HoverController extends InteractionController {
isWebKit() &&
this.target[lastInteractionType] === InteractionTypes.click
) {
this.target.classList.add(SAFARI_FOCUS_RING_CLASS);
return;
}

this.open = true;
this.focusedin = true;
this.removeSafariFocusRingClass();
}

handleTargetFocusout(): void {
this.removeSafariFocusRingClass();
this.focusedin = false;
if (this.pointerentered) return;
this.open = false;
Expand Down Expand Up @@ -199,4 +204,12 @@ export class HoverController extends InteractionController {
{ signal }
);
}

private removeSafariFocusRingClass(): void {
if (
isWebKit() &&
this.target.classList.contains(SAFARI_FOCUS_RING_CLASS)
)
this.target.classList.remove(SAFARI_FOCUS_RING_CLASS);
}
}
2 changes: 2 additions & 0 deletions packages/overlay/src/InteractionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum InteractionTypes {
}

export const lastInteractionType = Symbol('lastInteractionType');
export const SAFARI_FOCUS_RING_CLASS = 'remove-focus-ring-safari-hack';

export type ControllerOptions = {
overlay?: AbstractOverlay;
Expand Down Expand Up @@ -74,6 +75,7 @@ export class InteractionController implements ReactiveController {
this.overlay.open = true;
this.target[lastInteractionType] = this.type;
});
// eslint-disable-next-line import/no-extraneous-dependencies
import('@spectrum-web-components/overlay/sp-overlay.js');
}

Expand Down
Loading