+
+
`;
}
+
+ /**
+ * Bind the open/close transition into the update complete lifecycle so
+ * that the overlay system can wait for it to be "visibly ready" before
+ * attempting to throw focus into the content contained herein. Not
+ * waiting for this can cause small amounts of page scroll to happen
+ * while opening the Tray when focusable content is included: e.g. Menu
+ * elements whose selected Menu Item is not the first Menu Item.
+ */
+ protected async getUpdateComplete(): Promise
{
+ const complete = (await super.getUpdateComplete()) as boolean;
+ await this.transitionPromise;
+ return complete;
+ }
}
diff --git a/packages/tray/src/spectrum-config.js b/packages/tray/src/spectrum-config.js
index e50ba93786..c37f2a7760 100644
--- a/packages/tray/src/spectrum-config.js
+++ b/packages/tray/src/spectrum-config.js
@@ -13,6 +13,19 @@ governing permissions and limitations under the License.
const config = {
spectrum: 'tray',
components: [
+ {
+ name: 'tray-wrapper',
+ host: {
+ selector: '.spectrum-Tray-wrapper',
+ },
+ attributes: [
+ {
+ type: 'boolean',
+ selector: '.is-open',
+ name: 'open',
+ },
+ ],
+ },
{
name: 'tray',
host: {
diff --git a/packages/tray/src/spectrum-tray-wrapper.css b/packages/tray/src/spectrum-tray-wrapper.css
new file mode 100644
index 0000000000..594f477ad3
--- /dev/null
+++ b/packages/tray/src/spectrum-tray-wrapper.css
@@ -0,0 +1,31 @@
+/* stylelint-disable */ /*
+Copyright 2020 Adobe. All rights reserved.
+This file is licensed to you under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License. You may obtain a copy
+of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under
+the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+OF ANY KIND, either express or implied. See the License for the specific language
+governing permissions and limitations under the License.
+
+THIS FILE IS MACHINE GENERATED. DO NOT EDIT */
+:host([dir='ltr']) {
+ left: 0; /* [dir=ltr] .spectrum-Tray-wrapper */
+}
+:host([dir='rtl']) {
+ right: 0; /* [dir=rtl] .spectrum-Tray-wrapper */
+}
+:host {
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ position: fixed; /* .spectrum-Tray-wrapper */
+ width: 100%;
+ z-index: 2;
+}
+@media (max-width: 375px) {
+ .spectrum-Tray {
+ border-radius: var(--spectrum-tray-border-radius, 0);
+ }
+}
diff --git a/packages/tray/src/tray.css b/packages/tray/src/tray.css
index 0b984fc0f3..63a355505b 100644
--- a/packages/tray/src/tray.css
+++ b/packages/tray/src/tray.css
@@ -10,10 +10,17 @@ OF ANY KIND, either express or implied. See the License for the specific languag
governing permissions and limitations under the License.
*/
+@import './spectrum-tray-wrapper.css';
@import './spectrum-tray.css';
:host {
align-items: flex-end;
+ position: fixed !important;
+ max-height: var(--swc-visual-viewport-height);
+}
+
+sp-underlay {
+ touch-action: none;
}
/**
@@ -24,4 +31,19 @@ governing permissions and limitations under the License.
**/
.tray {
padding: var(--spectrum-tray-padding-y, 0) var(--spectrum-tray-padding-x, 0);
+ display: inline-flex;
+ overscroll-behavior: contain;
+}
+
+::slotted(.visually-hidden) {
+ border: 0;
+ clip: rect(0, 0, 0, 0);
+ clip-path: inset(50%);
+ height: 1px;
+ margin: 0 -1px -1px 0;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ white-space: nowrap;
}
diff --git a/packages/tray/test/tray.test.ts b/packages/tray/test/tray.test.ts
index 72f0897ad1..44b6f0b3a7 100644
--- a/packages/tray/test/tray.test.ts
+++ b/packages/tray/test/tray.test.ts
@@ -15,11 +15,14 @@ import {
expect,
fixture,
html,
+ nextFrame,
oneEvent,
} from '@open-wc/testing';
import '../sp-tray.js';
import { Tray } from '..';
+import '@spectrum-web-components/theme/sp-theme.js';
+import '@spectrum-web-components/theme/src/themes.js';
describe('Tray', () => {
it('loads default tray accessibly', async () => {
@@ -52,13 +55,24 @@ describe('Tray', () => {
expect(document.activeElement).to.equal(anchor);
});
it('closes', async () => {
- const el = await fixture(
+ const test = await fixture(
html`
-
+
+
+
`
);
+ const el = test.querySelector('sp-tray') as Tray;
+ // Ensure closed styles are set before opening so that
+ // the `transitionend` event will be met below.
+ await nextFrame();
+ await nextFrame();
+ expect(el.open).to.be.false;
+
+ el.open = true;
await elementUpdated(el);
+
expect(el.open).to.be.true;
const closed = oneEvent(el, 'close');
const overlay = el.shadowRoot.querySelector(
diff --git a/projects/story-decorator/src/StoryDecorator.ts b/projects/story-decorator/src/StoryDecorator.ts
index 171132714d..ab33371458 100644
--- a/projects/story-decorator/src/StoryDecorator.ts
+++ b/projects/story-decorator/src/StoryDecorator.ts
@@ -65,6 +65,7 @@ const reduceMotionProperties = css`
--spectrum-global-animation-duration-2000: 0ms;
--spectrum-global-animation-duration-4000: 0ms;
--spectrum-coachmark-animation-indicator-ring-duration: 0ms;
+ --swc-test-duration: 1ms;
`;
ActiveOverlay.prototype.renderTheme = function (
diff --git a/tools/reactive-controllers/README.md b/tools/reactive-controllers/README.md
index c104a94694..1029ac7707 100644
--- a/tools/reactive-controllers/README.md
+++ b/tools/reactive-controllers/README.md
@@ -4,4 +4,5 @@
### Reactive controllers
+- [MatchMediaController](../match-media)
- [RovingTabindexController](../roving-tab-index)
diff --git a/tools/reactive-controllers/match-media.md b/tools/reactive-controllers/match-media.md
new file mode 100644
index 0000000000..83abdcb44f
--- /dev/null
+++ b/tools/reactive-controllers/match-media.md
@@ -0,0 +1,47 @@
+## Description
+
+The [match media](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) API allows for a developer to query the state of a supplied CSS media query from the JS scope while surfacing an event based API to listen for changes to whether that query is currently matched or not. `MatchMediaController` binds the supplied CSS media query to the supplied Reactive Element and calls for an update in the host element when the query goes between matching and not. This allow for the `matches` property on the reactive controller to be leveraged in your render lifecycle.
+
+A `MatchMediaController` can be bound to any of the growing number of [CSS media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries) and any number of `MatchMediaControllers` can be bound to a host element. With this in mind the `MatchMediaController` can support a wide array of complex layouts.
+
+### Usage
+
+[![See it on NPM!](https://img.shields.io/npm/v/@spectrum-web-components/reactive-controllers?style=for-the-badge)](https://www.npmjs.com/package/@spectrum-web-components/reactive-controllers)
+[![How big is this package in your project?](https://img.shields.io/bundlephobia/minzip/@spectrum-web-components/reactive-controllers?style=for-the-badge)](https://bundlephobia.com/result?p=@spectrum-web-components/reactive-controllers)
+
+```
+yarn add @spectrum-web-components/reactive-controllers
+```
+
+Import the `MatchMediaController` via:
+
+```
+import { MatchMediaController } from '@spectrum-web-components/reactive-controllers/MatchMediaController.js';
+```
+
+## Example
+
+A `Host` element that renders a different message depending on the "orientation" of the window in which is it delivered:
+
+```js
+import { html, LitElement } from 'lit';
+import { MatchMediaController } from '@spectrum-web-components/reactive-controllers/MatchMediaController.js';
+
+class Host extends LitElement {
+ orientationLandscape = new MatchMediaController(
+ this,
+ '(orientation: landscape)'
+ );
+
+ render() {
+ if (this.orientationLandscape.matches) {
+ return html`
+ The orientation is landscape.
+ `;
+ }
+ return html`
+ The orientation is portrait.
+ `;
+ }
+}
+```
diff --git a/tools/reactive-controllers/src/MatchMedia.ts b/tools/reactive-controllers/src/MatchMedia.ts
new file mode 100644
index 0000000000..8f0dcdbd5b
--- /dev/null
+++ b/tools/reactive-controllers/src/MatchMedia.ts
@@ -0,0 +1,48 @@
+/*
+Copyright 2020 Adobe. All rights reserved.
+This file is licensed to you under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License. You may obtain a copy
+of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under
+the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+OF ANY KIND, either express or implied. See the License for the specific language
+governing permissions and limitations under the License.
+*/
+import type { ReactiveController, ReactiveElement } from 'lit';
+
+export const DARK_MODE = '(prefers-color-scheme: dark)';
+export const IS_MOBILE =
+ '(max-width: 700px) and (hover: none) and (pointer: coarse), (max-height: 700px) and (hover: none) and (pointer: coarse)';
+
+export class MatchMediaController implements ReactiveController {
+ key = Symbol('match-media-key');
+
+ matches = false;
+
+ protected host: ReactiveElement;
+
+ protected media: MediaQueryList;
+
+ constructor(host: ReactiveElement, query: string) {
+ this.host = host;
+ this.media = window.matchMedia(query);
+ this.matches = this.media.matches;
+ this.onChange = this.onChange.bind(this);
+ host.addController(this);
+ }
+
+ public hostConnected(): void {
+ this.media.addEventListener('change', this.onChange);
+ }
+
+ public hostDisconnected(): void {
+ this.media.removeEventListener('change', this.onChange);
+ }
+
+ protected onChange(event: MediaQueryListEvent): void {
+ if (this.matches === event.matches) return;
+ this.matches = event.matches;
+ this.host.requestUpdate(this.key, !this.matches);
+ }
+}
diff --git a/tools/reactive-controllers/src/index.ts b/tools/reactive-controllers/src/index.ts
index bb7e3b774b..b5f2cbf571 100644
--- a/tools/reactive-controllers/src/index.ts
+++ b/tools/reactive-controllers/src/index.ts
@@ -10,4 +10,5 @@ OF ANY KIND, either express or implied. See the License for the specific languag
governing permissions and limitations under the License.
*/
+export * from './MatchMedia.js';
export * from './RovingTabindex.js';
diff --git a/tools/reactive-controllers/test/match-media.test.ts b/tools/reactive-controllers/test/match-media.test.ts
new file mode 100644
index 0000000000..3fc9ecb0b4
--- /dev/null
+++ b/tools/reactive-controllers/test/match-media.test.ts
@@ -0,0 +1,37 @@
+/*
+Copyright 2020 Adobe. All rights reserved.
+This file is licensed to you under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License. You may obtain a copy
+of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under
+the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+OF ANY KIND, either express or implied. See the License for the specific language
+governing permissions and limitations under the License.
+*/
+
+import { html, LitElement } from 'lit';
+import { expect, fixture, nextFrame } from '@open-wc/testing';
+import { setViewport } from '@web/test-runner-commands';
+import { MatchMediaController } from '../';
+
+describe('Match Media', () => {
+ it('responds to media changes', async () => {
+ class TestEl extends LitElement {}
+ customElements.define('test-match-media-el', TestEl);
+ const el = await fixture(
+ html`
+
+ `
+ );
+ const controller = new MatchMediaController(
+ el as LitElement & { shadowRoot: ShadowRoot },
+ '(min-width: 500px)'
+ );
+ expect(controller.matches).to.be.true;
+ await setViewport({ width: 360, height: 640 });
+ // Allow viewport update to propagate.
+ await nextFrame();
+ expect(controller.matches).to.be.false;
+ });
+});
diff --git a/tools/reactive-controllers/test/reactive-controllers-integration.test.ts b/tools/reactive-controllers/test/roving-tabindex-integration.test.ts
similarity index 100%
rename from tools/reactive-controllers/test/reactive-controllers-integration.test.ts
rename to tools/reactive-controllers/test/roving-tabindex-integration.test.ts
diff --git a/tools/reactive-controllers/test/reactive-controllers.test.ts b/tools/reactive-controllers/test/roving-tabindex.test.ts
similarity index 95%
rename from tools/reactive-controllers/test/reactive-controllers.test.ts
rename to tools/reactive-controllers/test/roving-tabindex.test.ts
index cb8d291ec9..05a32d2e8e 100644
--- a/tools/reactive-controllers/test/reactive-controllers.test.ts
+++ b/tools/reactive-controllers/test/roving-tabindex.test.ts
@@ -12,7 +12,7 @@ governing permissions and limitations under the License.
import { LitElement } from 'lit';
import { expect } from '@open-wc/testing';
-import { RovingTabindexController } from '../';
+import { RovingTabindexController } from '../src';
describe('RovingTabindex', () => {
it('constructs with defaults', async () => {
diff --git a/yarn.lock b/yarn.lock
index cbe62d533a..467a4c2673 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -17773,13 +17773,42 @@ playwright-core@=1.16.3:
yauzl "^2.10.0"
yazl "^2.5.1"
-playwright@^1.14.0, playwright@^1.16.3:
+playwright-core@=1.18.1:
+ version "1.18.1"
+ resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.18.1.tgz#a5cf3f212d10692382e2acd1f7bc8c9ff9bbb849"
+ integrity sha512-NALGl8R1GHzGLlhUApmpmfh6M1rrrPcDTygWvhTbprxwGB9qd/j9DRwyn4HTQcUB6o0/VOpo46fH9ez3+D/Rog==
+ dependencies:
+ commander "^8.2.0"
+ debug "^4.1.1"
+ extract-zip "^2.0.1"
+ https-proxy-agent "^5.0.0"
+ jpeg-js "^0.4.2"
+ mime "^2.4.6"
+ pngjs "^5.0.0"
+ progress "^2.0.3"
+ proper-lockfile "^4.1.1"
+ proxy-from-env "^1.1.0"
+ rimraf "^3.0.2"
+ socks-proxy-agent "^6.1.0"
+ stack-utils "^2.0.3"
+ ws "^7.4.6"
+ yauzl "^2.10.0"
+ yazl "^2.5.1"
+
+playwright@^1.14.0:
version "1.16.3"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.16.3.tgz#27a292d9fa54fbac923998d3af58cd2b691f5ebe"
integrity sha512-nfJx/OpIb/8OexL3rYGxNN687hGyaM3XNpfuMzoPlrekURItyuiHHsNhC9oQCx3JDmCn5O3EyyyFCnrZjH6MpA==
dependencies:
playwright-core "=1.16.3"
+playwright@^1.18.1:
+ version "1.18.1"
+ resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.18.1.tgz#45c2ca6ee25c44e336985de9b51955727b5f17cf"
+ integrity sha512-8EaX9EtbtAoMq5tnzIsoA3b/V86V/6Mq2skuOU4qEw+5OVxs1lwesDwmjy/RVU1Qfx5UuwSQzhp45wyH22oa+A==
+ dependencies:
+ playwright-core "=1.18.1"
+
please-upgrade-node@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"