Modal
+Modal...
+ +Props
+| prop | +type | +default | +
|---|---|---|
| open | +
+ boolean
+ Opens the modal
+ |
+ false | +
diff --git a/index.js b/index.js
index 69705da8..dfd2fbd3 100644
--- a/index.js
+++ b/index.js
@@ -6,4 +6,5 @@ export * from './packages/toast/toast';
export * from './packages/toast/toast-container';
export * from './packages/toast/api';
export * from './packages/broadcast';
+export * from './packages/modal';
export * from './packages/utils/expand-transition';
diff --git a/package.json b/package.json
index 7457ba6a..313b9058 100644
--- a/package.json
+++ b/package.json
@@ -79,9 +79,12 @@
"vite-plugin-html": "3.2.0"
},
"dependencies": {
+ "@a11y/focus-trap": "^1.0.5",
"@fabric-ds/core": "0.0.15",
"@open-wc/testing": "3.1.6",
- "html-format": "1.0.2"
+ "dialog-polyfill": "^0.5.6",
+ "html-format": "1.0.2",
+ "scroll-doctor": "^1.0.1"
},
"publishConfig": {
"access": "public"
diff --git a/packages/modal/index.js b/packages/modal/index.js
new file mode 100644
index 00000000..764ef235
--- /dev/null
+++ b/packages/modal/index.js
@@ -0,0 +1,182 @@
+import { html, css } from 'lit';
+import { fclasses, FabricElement } from '../utils';
+import { modal as c } from '@fabric-ds/css/component-classes';
+import { leftButtonSvg, rightButtonSvg } from './svgs';
+import { setup, teardown } from 'scroll-doctor';
+import dialogPolyfill from 'dialog-polyfill';
+
+class FabricModal extends FabricElement {
+ static properties = {
+ open: { type: Boolean },
+ left: { type: Boolean },
+ right: { type: Boolean },
+ };
+
+ static styles = css`
+ :host {
+ --f-modal-width: 640px;
+ --f-modal-max-height: 80%;
+ }
+ .modal {
+ width: var(--f-modal-width);
+ }
+ dialog {
+ padding: 0;
+ border: none !important;
+ /* !important used here to override polyfill CSS, if loaded */
+ }
+ ::backdrop {
+ background-color: #00000059;
+ }
+ `;
+
+ connectedCallback() {
+ super.connectedCallback();
+ const dialog = this.renderRoot.querySelector('dialog');
+ dialogPolyfill.registerDialog(dialog);
+ }
+
+ get _leftButtonClasses() {
+ return fclasses({
+ [c.transitionTitle]: true,
+ [c.titleButton]: true,
+ [c.titleButtonLeft]: true,
+ 'justify-self-start': true,
+ });
+ }
+
+ get _rightButtonClasses() {
+ return fclasses({
+ [c.transitionTitle]: true,
+ [c.titleButton]: true,
+ [c.titleButtonRight]: true,
+ 'justify-self-end': true,
+ });
+ }
+
+ get _titleClasses() {
+ return fclasses({
+ [c.transitionTitle]: true,
+ 'justify-self-center': !!this.left,
+ 'col-span-2': !this.left,
+ });
+ }
+
+ willUpdate() {
+ if (!this.open && this._scrollDoctorEnabled) {
+ // this.renderRoot.querySelector('dialog').close();
+ // close dialog case
+ this.renderRoot.querySelector('dialog').close();
+ teardown();
+ this._removeSafariDialogHack();
+ }
+ }
+
+ updated() {
+ if (this.open && !this._scrollDoctorEnabled) {
+ // open dialog case
+ setup(this);
+ this._scrollDoctorEnabled = true;
+ // take note of where the focus is for later
+ this._activeEl = document.activeElement;
+ this.renderRoot.querySelector('dialog').showModal();
+ this._applySafariDialogHack();
+ } else if (!this.open && this._scrollDoctorEnabled) {
+ this._scrollDoctorEnabled = false;
+ this._activeEl?.focus();
+ }
+ }
+
+ _containerKeyDown(event) {
+ if (event.key === 'Escape') this._dismiss();
+ }
+
+ _containerClick(event) {
+ event.stopPropagation();
+ }
+
+ _dismiss(event) {
+ this.dispatchEvent(new CustomEvent('close'));
+ }
+
+ get _leftButton() {
+ return html``;
+ }
+
+ get _rightButton() {
+ return html``;
+ }
+
+ _applySafariDialogHack() {
+ // Nasty workaround for Safari + VoiceOver to make sure surrounding content is not available for VoiceOver.
+ // Super important that these aria-hidden attributes are removed when the dialog is closed.
+ this._hiddenSurroundings = [];
+ ['previousElementSibling', 'nextElementSibling'].forEach((direction) => {
+ let el = this;
+ while (el !== document.body) {
+ if (el[direction]) {
+ el = el[direction];
+ if (el.getAttribute('aria-hidden') !== 'true') {
+ this._hiddenSurroundings.push(el);
+ el.setAttribute('aria-hidden', 'true');
+ }
+ } else el = el.parentNode;
+ }
+ });
+ }
+
+ _removeSafariDialogHack() {
+ this._hiddenSurroundings.forEach((el) => el.removeAttribute('aria-hidden'));
+ }
+
+ render() {
+ return html`
+ ${this._fabricStylesheet}
+
+ `;
+ }
+}
+
+if (!customElements.get('f-modal')) {
+ customElements.define('f-modal', FabricModal);
+}
+
+export { FabricModal };
diff --git a/packages/modal/svgs.js b/packages/modal/svgs.js
new file mode 100644
index 00000000..eb6223cc
--- /dev/null
+++ b/packages/modal/svgs.js
@@ -0,0 +1,30 @@
+import { fclasses } from '../utils';
+import { html } from 'lit';
+import { modal as c } from '@fabric-ds/css/component-classes';
+
+export const leftButtonSvg = html``;
+
+export const rightButtonSvg = html` `;
diff --git a/packages/modal/test.js b/packages/modal/test.js
new file mode 100644
index 00000000..fb08c634
--- /dev/null
+++ b/packages/modal/test.js
@@ -0,0 +1,43 @@
+/* eslint-disable no-undef */
+import tap, { test, beforeEach, teardown } from 'tap';
+import { chromium } from 'playwright';
+import { addContentToPage } from '../../tests/utils/index.js';
+
+tap.before(async () => {
+ const browser = await chromium.launch({ headless: true });
+ tap.context.browser = browser;
+});
+
+beforeEach(async (t) => {
+ const { browser } = t.context;
+ const context = await browser.newContext();
+ t.context.page = await context.newPage();
+});
+
+teardown(async () => {
+ const { browser } = tap.context;
+ browser.close();
+});
+
+test('Box component with no attributes is rendered on the page', async (t) => {
+ // GIVEN: A box component
+ const component = `
+ This is a box
This is a box
', 'HTML should be rendered'); + t.equal(await locator.getAttribute('bleed'), null, 'Bleed attribute should be null'); + t.equal(await locator.getAttribute('bordered'), null, 'Bordered attribute should be null'); + t.equal(await locator.getAttribute('info'), null, 'Info attribute should be null'); + t.equal(await locator.getAttribute('neutral'), null, 'Neutral attribute should be null'); +}); diff --git a/pages/components/modal.html b/pages/components/modal.html new file mode 100644 index 00000000..fd4cda40 --- /dev/null +++ b/pages/components/modal.html @@ -0,0 +1,142 @@ + + <%- include('head.html'); -%> + +Modal...
+ +| prop | +type | +default | +
|---|---|---|
| open | +
+ boolean
+ Opens the modal
+ |
+ false | +