@blockquote-web-components/blockquote-dialog 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 blockquote-dialog
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,194 @@
1
+ ![Lit](https://img.shields.io/badge/lit-3.0.0-blue.svg)
2
+
3
+ [ARIA patterns](https://www.w3.org/WAI/ARIA/apg/patterns/)
4
+
5
+ A dialog is a window overlaid on either the primary window or another dialog window. Windows under a modal dialog are inert. That is, users cannot interact with content outside an active dialog window.
6
+ Inert content outside an active dialog is typically visually obscured or dimmed so it is difficult to discern, and in some implementations, attempts to interact with the inert content cause the dialog to close.
7
+
8
+ ### Demo
9
+
10
+ [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/oscarmarina/blockquote-web-components/tree/main/packages/components/blockquote-dialog)
11
+
12
+ ### Usage
13
+
14
+ ```html
15
+ <blockquote-dialog>
16
+ <button form="form1" aria-label="close" formnovalidate>X</button>
17
+ <p>Fill in your email address to receive our newsletter!</p>
18
+ <form id="form1 method="dialog">
19
+ <label for="email">Email (required)</label>
20
+ <input type="email" name="EMAIL" id="email" placeholder="john.doe@gmail.com" required />
21
+ <button type="submit" name="button">Sign up</button>
22
+ </form>
23
+ </blockquote-dialog>
24
+ ```
25
+
26
+
27
+ ### `src/BlockquoteDialog.js`:
28
+
29
+ #### class: `BlockquoteDialog`, `blockquote-dialog`
30
+
31
+ ##### Fields
32
+
33
+ | Name | Privacy | Type | Default | Description | Inherited From |
34
+ | ----------------------------- | ------- | --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
35
+ | `treewalker` | | | | | |
36
+ | `dialogRef` | | | | | |
37
+ | `open` | public | `boolean` | | Opens the dialog when set to \`true\` and closes it when set to \`false\`. | |
38
+ | `_slotTpl` | | | | | |
39
+ | `_labeledbyTpl` | | | | | |
40
+ | `_contentTpl` | | | | | |
41
+ | `_scrollerTpl` | | | | | |
42
+ | `_firstNodeFocusTrapTpl` | | | | | |
43
+ | `_lastNodeFocusTrapTpl` | | | | | |
44
+ | `_isConnectedCallbackResolve` | | | | | |
45
+ | `_isConnectedCallback` | | | | | |
46
+ | `_firstFocusableChild` | | | `undefined` | | |
47
+ | `_lastFocusableChild` | | | `undefined` | | |
48
+ | `_nextClickIsFromContent` | | `boolean` | `false` | | |
49
+ | `_overflowRoot` | | | | | |
50
+ | `type` | public | `string` | `'alert'` | The type of dialog for accessibility. Set this to \`alert\` to announce a&#xA;dialog as an alert dialog. | |
51
+ | `label` | public | `string` | `''` | | |
52
+ | `labelledby` | public | `string` | `''` | | |
53
+ | `returnValue` | public | | | Gets or sets the dialog's return value, usually to indicate which button&#xA;a user pressed to close it.&#xA;&#xA;https\://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/returnValue returnValue | |
54
+
55
+ ##### Methods
56
+
57
+ | Name | Privacy | Description | Parameters | Return | Inherited From |
58
+ | ------------------------------- | ------- | ----------- | ------------------- | ------ | -------------- |
59
+ | `getIsConnectedCallbackResolve` | | | | | |
60
+ | `show` | | | | | |
61
+ | `close` | | | | | |
62
+ | `_handleSubmit` | | | `ev: SubmitEvent` | | |
63
+ | `_handleOpen` | | | | | |
64
+ | `_handleClose` | | | `ev: Event` | | |
65
+ | `_handleCancel` | | | `ev: Event` | | |
66
+ | `_handleDialogClick` | | | | | |
67
+ | `_handleContentClick` | | | | | |
68
+ | `_firstFocusTrap` | | | `{ relatedTarget }` | | |
69
+ | `_lastFocusTrap` | | | `{ relatedTarget }` | | |
70
+
71
+ ##### Events
72
+
73
+ | Name | Type | Description | Inherited From |
74
+ | -------- | ---- | ----------- | -------------- |
75
+ | `open` | | | |
76
+ | `close` | | | |
77
+ | `cancel` | | | |
78
+
79
+ ##### Attributes
80
+
81
+ | Name | Field | Inherited From |
82
+ | ------------ | ---------- | -------------- |
83
+ | `open` | open | |
84
+ | `label` | label | |
85
+ | `labelledby` | labelledby | |
86
+ | `type` | type | |
87
+
88
+ ##### Slots
89
+
90
+ | Name | Description |
91
+ | ---- | ----------------------- |
92
+ | | This element has a slot |
93
+
94
+ <details><summary>Private API</summary>
95
+
96
+ ##### Fields
97
+
98
+ | Name | Privacy | Type | Default | Description | Inherited From |
99
+ | ------- | ------- | --------- | ------- | ----------- | -------------- |
100
+ | `#open` | private | `boolean` | `false` | | |
101
+
102
+ </details>
103
+
104
+ <hr/>
105
+
106
+ #### Exports
107
+
108
+ | Kind | Name | Declaration | Module | Package |
109
+ | ---- | ------------------ | ---------------- | ----------------------- | ------- |
110
+ | `js` | `BlockquoteDialog` | BlockquoteDialog | src/BlockquoteDialog.js | |
111
+
112
+ ### `src/dom-utils.js`:
113
+
114
+ #### Functions
115
+
116
+ | Name | Description | Parameters | Return |
117
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | ----------------------------------------------------- |
118
+ | `redispatchEvent` | Re-dispatches an event from the provided element.&#xA;&#xA;This function is useful for forwarding non-composed events, such as \`change\`&#xA;events. | `element: Element, event: Event\|string, options: Object` | `boolean` |
119
+ | `isElementInvisible` | Checks if an element should be ignored. | `element: Element, exceptions: Array` | `boolean` |
120
+ | `isFocusable` | Checks if an element is focusable. An element is considered focusable if it matches&#xA;standard focusable elements criteria (such as buttons, inputs, etc., that are not disabled&#xA;and do not have a negative tabindex) or is a custom element with a shadow root that delegates focus. | `element: Element` | `boolean` |
121
+ | `getFirstAndLastFocusableChildren` | Retrieves the first and last focusable children of a node using a TreeWalker. | `walker: IterableIterator<HTMLElement>` | `[first: HTMLElement\|null, last: HTMLElement\|null]` |
122
+ | `walkComposedTree` | Traverse the composed tree from the root, selecting elements that meet the provided filter criteria.&#xA;You can pass \[NodeFilter]\(https\://developer.mozilla.org/en-US/docs/Web/API/NodeFilter) or 0 to retrieve all nodes. | `node: Node, whatToShow: number, filter: function, skipNode: function` | `IterableIterator<Node>` |
123
+ | `getDeepActiveElement` | Returns the deepest active element, considering Shadow DOM subtrees | `root: Document \| ShadowRoot` | `Element` |
124
+ | `deepContains` | Returns true if the first node contains the second, even if the second node&#xA;is in a shadow tree.&#xA;&#xA;The standard Node.contains() function does not account for Shadow DOM, and&#xA;returns false if the supplied target node is sitting inside a shadow tree&#xA;within the container. | `container: Node, target: Node` | `boolean` |
125
+ | `indexOfItemContainingTarget` | Search a list element for the item that contains the specified target.&#xA;&#xA;When dealing with UI events (e.g., mouse clicks) that may occur in&#xA;subelements inside a list item, you can use this routine to obtain the&#xA;containing list item. | `items: NodeList\|Node[], target: Node` | `number` |
126
+ | `composedAncestors` | Return the ancestors of the given node in the composed tree.&#xA;&#xA;In the composed tree, the ancestor of a node assigned to a slot is that slot,&#xA;not the node's DOM ancestor. The ancestor of a shadow root is its host. | `node: Node` | `Iterable<Node>` |
127
+ | `isClickInsideRect` | Checks if a click event occurred inside a given bounding rectangle. | `rect: DOMRect, ev: PointerEvent` | `boolean` |
128
+ | `randomID` | | `length` | |
129
+
130
+ <hr/>
131
+
132
+ #### Exports
133
+
134
+ | Kind | Name | Declaration | Module | Package |
135
+ | ---- | ---------------------------------- | -------------------------------- | ---------------- | ------- |
136
+ | `js` | `redispatchEvent` | redispatchEvent | src/dom-utils.js | |
137
+ | `js` | `isElementInvisible` | isElementInvisible | src/dom-utils.js | |
138
+ | `js` | `isFocusable` | isFocusable | src/dom-utils.js | |
139
+ | `js` | `getFirstAndLastFocusableChildren` | getFirstAndLastFocusableChildren | src/dom-utils.js | |
140
+ | `js` | `walkComposedTree` | walkComposedTree | src/dom-utils.js | |
141
+ | `js` | `getDeepActiveElement` | getDeepActiveElement | src/dom-utils.js | |
142
+ | `js` | `deepContains` | deepContains | src/dom-utils.js | |
143
+ | `js` | `indexOfItemContainingTarget` | indexOfItemContainingTarget | src/dom-utils.js | |
144
+ | `js` | `composedAncestors` | composedAncestors | src/dom-utils.js | |
145
+ | `js` | `isClickInsideRect` | isClickInsideRect | src/dom-utils.js | |
146
+ | `js` | `randomID` | randomID | src/dom-utils.js | |
147
+
148
+ ### `src/index.js`:
149
+
150
+ #### Exports
151
+
152
+ | Kind | Name | Declaration | Module | Package |
153
+ | ---- | ------------------ | ---------------- | --------------------- | ------- |
154
+ | `js` | `BlockquoteDialog` | BlockquoteDialog | ./BlockquoteDialog.js | |
155
+
156
+ ### `src/styles/blockqoute-dialog-animations-styles.css.js`:
157
+
158
+ #### Variables
159
+
160
+ | Name | Description | Type |
161
+ | -------- | ----------- | ---- |
162
+ | `styles` | | |
163
+
164
+ <hr/>
165
+
166
+ #### Exports
167
+
168
+ | Kind | Name | Declaration | Module | Package |
169
+ | ---- | -------- | ----------- | ----------------------------------------------------- | ------- |
170
+ | `js` | `styles` | styles | src/styles/blockqoute-dialog-animations-styles.css.js | |
171
+
172
+ ### `src/styles/blockquote-dialog-styles.css.js`:
173
+
174
+ #### Variables
175
+
176
+ | Name | Description | Type |
177
+ | -------- | ----------- | ---- |
178
+ | `styles` | | |
179
+
180
+ <hr/>
181
+
182
+ #### Exports
183
+
184
+ | Kind | Name | Declaration | Module | Package |
185
+ | ---- | -------- | ----------- | ------------------------------------------ | ------- |
186
+ | `js` | `styles` | styles | src/styles/blockquote-dialog-styles.css.js | |
187
+
188
+ ### `define/blockquote-dialog.js`:
189
+
190
+ #### Exports
191
+
192
+ | Kind | Name | Declaration | Module | Package |
193
+ | --------------------------- | ------------------- | ---------------- | ------------------------ | ------- |
194
+ | `custom-element-definition` | `blockquote-dialog` | BlockquoteDialog | /src/BlockquoteDialog.js | |
@@ -0,0 +1,3 @@
1
+ import { BlockquoteDialog } from '../src/BlockquoteDialog.js';
2
+
3
+ window.customElements.define('blockquote-dialog', BlockquoteDialog);
package/package.json ADDED
@@ -0,0 +1,168 @@
1
+ {
2
+ "name": "@blockquote-web-components/blockquote-dialog",
3
+ "version": "1.0.0",
4
+ "description": "Webcomponent blockquote-dialog following open-wc recommendations",
5
+ "keywords": [
6
+ "lit",
7
+ "web-component",
8
+ "lit-element"
9
+ ],
10
+ "license": "MIT",
11
+ "author": "blockquote-dialog",
12
+ "type": "module",
13
+ "exports": {
14
+ ".": {
15
+ "default": "./src/index.js"
16
+ },
17
+ "./package.json": {
18
+ "default": "./package.json"
19
+ },
20
+ "./src/BlockquoteDialog.js": {
21
+ "default": "./src/BlockquoteDialog.js"
22
+ },
23
+ "./define/blockquote-dialog.js": {
24
+ "default": "./define/blockquote-dialog.js"
25
+ },
26
+ "./index.js": {
27
+ "default": "./src/index.js"
28
+ },
29
+ "./src/styles/blockquote-dialog-styles.css.js": {
30
+ "default": "./src/styles/blockquote-dialog-styles.css.js"
31
+ }
32
+ },
33
+ "main": "src/index.js",
34
+ "module": "src/index.js",
35
+ "files": [
36
+ "/define/",
37
+ "/src/",
38
+ "index.js",
39
+ "!/**/*.scss"
40
+ ],
41
+ "scripts": {
42
+ "analyze": "cem analyze --litelement --globs \"{src,define}/**/*.{js,ts}\" \"index.js\"",
43
+ "build": "echo \"This is not a TypeScript project, so no need to build.\"",
44
+ "dev:vite": "vite build",
45
+ "format": "npm run format:eslint && npm run format:prettier && npm run format:stylelint",
46
+ "format:eslint": "eslint \"**/*.{js,ts,html}\" --fix --ignore-path .eslintignore",
47
+ "format:prettier": "prettier \"**/*.{js,ts,json,html}\" --write --ignore-path .eslintignore",
48
+ "format:stylelint": "stylelint \"**/*.{scss,css}\" --fix --allow-empty-input --ignore-path .eslintignore",
49
+ "postinstall": "npm run sort:package",
50
+ "lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:stylelint",
51
+ "lint:eslint": "eslint \"**/*.{js,ts,html}\" --ignore-path .eslintignore",
52
+ "lint:prettier": "prettier \"**/*.{js,ts,json,html}\" --check --ignore-path .eslintignore",
53
+ "lint:stylelint": "stylelint \"**/*.{scss,css}\" --allow-empty-input --ignore-path .eslintignore",
54
+ "prepare": "husky",
55
+ "preview:vite": "vite preview",
56
+ "sass:watch": "sass-style-template",
57
+ "sort:package": "npx sort-package-json",
58
+ "start": "concurrently -k -r \"npm:sass:watch\" \"npm:vite\"",
59
+ "test": "wtr",
60
+ "test:watch": "wtr --watch",
61
+ "vite": "vite"
62
+ },
63
+ "lint-staged": {
64
+ "**/*.{js,ts,html}": [
65
+ "npm run format:eslint"
66
+ ],
67
+ "**/*.{js,ts,json,html}": [
68
+ "npm run format:prettier"
69
+ ],
70
+ "**/*.{scss,css}": [
71
+ "npm run format:stylelint"
72
+ ]
73
+ },
74
+ "prettier": {
75
+ "arrowParens": "avoid",
76
+ "printWidth": 100,
77
+ "singleQuote": true,
78
+ "trailingComma": "all",
79
+ "overrides": [
80
+ {
81
+ "files": "*.{scss,css}",
82
+ "options": {
83
+ "printWidth": 280,
84
+ "singleQuote": false
85
+ }
86
+ }
87
+ ]
88
+ },
89
+ "eslintConfig": {
90
+ "parserOptions": {
91
+ "ecmaVersion": "latest"
92
+ },
93
+ "extends": [
94
+ "@open-wc",
95
+ "prettier"
96
+ ],
97
+ "rules": {
98
+ "class-methods-use-this": "off",
99
+ "indent": [
100
+ "error",
101
+ 2,
102
+ {
103
+ "SwitchCase": 1,
104
+ "ignoredNodes": [
105
+ "PropertyDefinition",
106
+ "TemplateLiteral > CallExpression"
107
+ ]
108
+ }
109
+ ],
110
+ "no-unused-expressions": [
111
+ "error",
112
+ {
113
+ "allowShortCircuit": true,
114
+ "allowTernary": true
115
+ }
116
+ ],
117
+ "object-curly-newline": "off",
118
+ "import/extensions": [
119
+ "error",
120
+ "always",
121
+ {
122
+ "ignorePackages": true
123
+ }
124
+ ],
125
+ "import/no-extraneous-dependencies": [
126
+ "error",
127
+ {
128
+ "devDependencies": [
129
+ "**/test/**/*.{js,ts}",
130
+ "**/*.config.{js,ts,mjs,cjs}",
131
+ "**/*.conf.{js,ts,mjs,cjs}"
132
+ ]
133
+ }
134
+ ],
135
+ "import/no-unresolved": "off",
136
+ "import/prefer-default-export": "off",
137
+ "lit-a11y/click-events-have-key-events": "off",
138
+ "lit/no-classfield-shadowing": "off",
139
+ "lit/no-native-attributes": "off"
140
+ }
141
+ },
142
+ "stylelint": {
143
+ "extends": "stylelint-config-standard-scss",
144
+ "rules": {
145
+ "custom-property-pattern": null,
146
+ "no-duplicate-selectors": null,
147
+ "color-function-notation": null,
148
+ "alpha-value-notation": null
149
+ }
150
+ },
151
+ "dependencies": {
152
+ "@blockquote-web-components/blockquote-directive-ariaidref-slot": "^1.0.0",
153
+ "@blockquote-web-components/blockquote-mixin-slot-content": "^1.5.0",
154
+ "@material/web": "^1.4.1",
155
+ "lit": "^3.1.3"
156
+ },
157
+ "devDependencies": {
158
+ "@blockquote-web-components/blockquote-base-common-dev-dependencies": "^1.8.0",
159
+ "@blockquote-web-components/blockquote-base-embedded-webview": "^1.10.0",
160
+ "@blockquote-web-components/blockquote-foundations-sass": "^1.1.2",
161
+ "sinon": "^18.0.0"
162
+ },
163
+ "publishConfig": {
164
+ "access": "public"
165
+ },
166
+ "customElements": "custom-elements.json",
167
+ "gitHead": "17dc7bb75989d134c948238d64c634faf035941a"
168
+ }
@@ -0,0 +1,350 @@
1
+ import { html, LitElement, nothing, isServer } from 'lit';
2
+ import { ref, createRef } from 'lit/directives/ref.js';
3
+ import { blockquoteDirectiveAriaidrefSlot } from '@blockquote-web-components/blockquote-directive-ariaidref-slot';
4
+ import {
5
+ redispatchEvent,
6
+ isElementInvisible,
7
+ isFocusable,
8
+ getFirstAndLastFocusableChildren,
9
+ walkComposedTree,
10
+ } from './dom-utils.js';
11
+ import { styles } from './styles/blockquote-dialog-styles.css.js';
12
+ import { styles as animations } from './styles/blockqoute-dialog-animations-styles.css.js';
13
+
14
+ // https://web.dev/learn/html/dialog
15
+ // https://github.com/oscarmarina/material-web/blob/main/dialog/dialog.ts
16
+ // https://a11y-dialog.netlify.app/
17
+
18
+ /**
19
+ * ![Lit](https://img.shields.io/badge/lit-3.0.0-blue.svg)
20
+ *
21
+ * [ARIA patterns](https://www.w3.org/WAI/ARIA/apg/patterns/)
22
+ *
23
+ * A dialog is a window overlaid on either the primary window or another dialog window. Windows under a modal dialog are inert. That is, users cannot interact with content outside an active dialog window.
24
+ * Inert content outside an active dialog is typically visually obscured or dimmed so it is difficult to discern, and in some implementations, attempts to interact with the inert content cause the dialog to close.
25
+ *
26
+ * ### Demo
27
+ *
28
+ * [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/oscarmarina/blockquote-web-components/tree/main/packages/components/blockquote-dialog)
29
+ *
30
+ * ### Usage
31
+ *
32
+ * ```html
33
+ * <blockquote-dialog>
34
+ * <button form="form1" aria-label="close" formnovalidate>X</button>
35
+ * <p>Fill in your email address to receive our newsletter!</p>
36
+ * <form id="form1 method="dialog">
37
+ * <label for="email">Email (required)</label>
38
+ * <input type="email" name="EMAIL" id="email" placeholder="john.doe@gmail.com" required />
39
+ * <button type="submit" name="button">Sign up</button>
40
+ * </form>
41
+ * </blockquote-dialog>
42
+ * ```
43
+ *
44
+ * @attribute open
45
+ * @attribute label
46
+ * @attribute labelledby
47
+ * @attribute type
48
+ * @fires open
49
+ * @fires close
50
+ * @fires cancel
51
+ * @slot - This element has a slot
52
+ */
53
+ export class BlockquoteDialog extends LitElement {
54
+ treewalker = walkComposedTree(this, NodeFilter.SHOW_ELEMENT, isFocusable, isElementInvisible);
55
+
56
+ #open = false;
57
+
58
+ dialogRef = createRef();
59
+
60
+ /**
61
+ * @override
62
+ */
63
+ static styles = [styles, animations];
64
+
65
+ /**
66
+ * @override
67
+ */
68
+ static properties = {
69
+ /**
70
+ * Opens the dialog when set to `true` and closes it when set to `false`.
71
+ */
72
+ open: {
73
+ type: Boolean,
74
+ reflect: true,
75
+ },
76
+
77
+ /**
78
+ * Gets or sets the dialog's return value, usually to indicate which button
79
+ * a user pressed to close it.
80
+ *
81
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/returnValue returnValue}
82
+ */
83
+ returnValue: {
84
+ attribute: false,
85
+ },
86
+
87
+ label: {
88
+ type: String,
89
+ },
90
+
91
+ labelledby: {
92
+ type: String,
93
+ },
94
+
95
+ /**
96
+ * The type of dialog for accessibility. Set this to `alert` to announce a
97
+ * dialog as an alert dialog.
98
+ */
99
+ type: {
100
+ type: String,
101
+ reflect: true,
102
+ },
103
+ };
104
+
105
+ set open(value) {
106
+ const old = this.#open;
107
+ if (value === old) {
108
+ return;
109
+ }
110
+
111
+ this.#open = value;
112
+ if (value) {
113
+ this.show();
114
+ } else {
115
+ this.close();
116
+ }
117
+ }
118
+
119
+ get open() {
120
+ return this.#open;
121
+ }
122
+
123
+ constructor() {
124
+ super();
125
+ this._isConnectedCallbackResolve = () => undefined;
126
+ this._isConnectedCallback = this.getIsConnectedCallbackResolve();
127
+ this._firstFocusableChild = undefined;
128
+ this._lastFocusableChild = undefined;
129
+ this._nextClickIsFromContent = false;
130
+ this._overflowRoot = document.body;
131
+ this.type = 'alert';
132
+ this.label = '';
133
+ this.labelledby = '';
134
+
135
+ if (!isServer) {
136
+ this.addEventListener('submit', this._handleSubmit);
137
+ }
138
+ }
139
+
140
+ getIsConnectedCallbackResolve() {
141
+ return new Promise(resolve => {
142
+ /** @type {unknown} */ (this._isConnectedCallbackResolve) = resolve;
143
+ });
144
+ }
145
+
146
+ async connectedCallback() {
147
+ super.connectedCallback?.();
148
+ await this.updateComplete;
149
+ this.scroller = this.shadowRoot?.querySelector('.scroller');
150
+ const [first, last] = getFirstAndLastFocusableChildren(
151
+ /** @type {IterableIterator<HTMLElement>} */ (this.treewalker),
152
+ );
153
+
154
+ this._firstFocusableChild = first;
155
+ this._lastFocusableChild = last;
156
+ this.role = 'presentation';
157
+
158
+ this._isConnectedCallbackResolve();
159
+ }
160
+
161
+ disconnectedCallback() {
162
+ super.disconnectedCallback();
163
+ this.isConnectedPromise = this.getIsConnectedCallbackResolve();
164
+ }
165
+
166
+ async show() {
167
+ await this._isConnectedCallback;
168
+ const { value } = this.dialogRef;
169
+
170
+ if (/** @type {HTMLDialogElement} */ (value)?.open) {
171
+ return;
172
+ }
173
+
174
+ const preventDefault = !this._handleOpen();
175
+ if (preventDefault) {
176
+ this.open = false;
177
+ return;
178
+ }
179
+
180
+ /** @type {HTMLDialogElement} */ (value)?.showModal();
181
+ this.requestUpdate();
182
+
183
+ const autofocusNode = this.querySelector('[autofocus]');
184
+ if (autofocusNode) {
185
+ /** @type {HTMLElement} */ (autofocusNode)?.focus();
186
+ } else if (this._firstFocusableChild) {
187
+ this._firstFocusableChild?.focus();
188
+ }
189
+
190
+ this._overflowRoot && this._overflowRoot.style.setProperty('overflow', 'hidden');
191
+
192
+ if (this.scroller) {
193
+ this.scroller.scrollTop = 0;
194
+ }
195
+ }
196
+
197
+ close() {
198
+ const { value } = this.dialogRef;
199
+ if (!(/** @type {HTMLDialogElement} */ (value)?.open)) {
200
+ return;
201
+ }
202
+
203
+ /** @type {HTMLDialogElement} */ (value)?.close();
204
+ this.requestUpdate();
205
+ this._overflowRoot && this._overflowRoot.style.setProperty('overflow', '');
206
+ }
207
+
208
+ get _slotTpl() {
209
+ return html` <slot></slot> `;
210
+ }
211
+
212
+ get _labeledbyTpl() {
213
+ return html` ${this.labelledby ? blockquoteDirectiveAriaidrefSlot(this.labelledby) : ''} `;
214
+ }
215
+
216
+ get _contentTpl() {
217
+ return html` <div class="content" @click=${this._handleContentClick}>${this._slotTpl}</div>`;
218
+ }
219
+
220
+ get _scrollerTpl() {
221
+ return html` <div class="scroller">${this._contentTpl} ${this._labeledbyTpl}</div> `;
222
+ }
223
+
224
+ get _firstNodeFocusTrapTpl() {
225
+ return html`
226
+ <span
227
+ ?hidden="${!(/** @type {HTMLDialogElement} */ (this.dialogRef.value)?.open)}"
228
+ tabindex="0"
229
+ @focus="${this._lastFocusTrap}"
230
+ ></span>
231
+ `;
232
+ }
233
+
234
+ get _lastNodeFocusTrapTpl() {
235
+ return html`
236
+ <span
237
+ ?hidden="${!(/** @type {HTMLDialogElement} */ (this.dialogRef.value)?.open)}"
238
+ tabindex="0"
239
+ @focus="${this._firstFocusTrap}"
240
+ ></span>
241
+ `;
242
+ }
243
+
244
+ /**
245
+ * @override
246
+ */
247
+ render() {
248
+ return html`
249
+ <dialog
250
+ ${ref(this.dialogRef)}
251
+ aria-label=${this.label || nothing}
252
+ aria-labelledby="${this.labelledby || nothing}"
253
+ role=${this.type === 'alert' ? 'alertdialog' : nothing}
254
+ @click=${this._handleDialogClick}
255
+ @cancel=${this._handleCancel}
256
+ @close=${this._handleClose}
257
+ .returnValue=${this.returnValue || nothing}
258
+ >
259
+ ${this._firstNodeFocusTrapTpl} ${this._scrollerTpl} ${this._lastNodeFocusTrapTpl}
260
+ </dialog>
261
+ `;
262
+ }
263
+
264
+ /**
265
+ * @param {SubmitEvent} ev
266
+ */
267
+ _handleSubmit(ev) {
268
+ const { target, submitter } = ev;
269
+ const isFormMethodDialog = /** @type {HTMLFormElement} */ (target)?.method === 'dialog';
270
+ const isSubmitterFormMethodDialog =
271
+ /** @type {HTMLButtonElement} */ (submitter)?.formMethod === 'dialog';
272
+
273
+ if (!isFormMethodDialog && !isSubmitterFormMethodDialog) {
274
+ return;
275
+ }
276
+
277
+ this._submitter = submitter;
278
+ this.open = false;
279
+ }
280
+
281
+ _handleOpen() {
282
+ /**
283
+ * Dispatched when the dialog is open.
284
+ * @event open
285
+ */
286
+ const preventDefault = redispatchEvent(this, 'open', { cancelable: true });
287
+ return preventDefault;
288
+ }
289
+
290
+ /**
291
+ * @param {Event} ev
292
+ */
293
+ _handleClose(ev) {
294
+ this.returnValue = this._submitter ?? this.returnValue;
295
+
296
+ /**
297
+ * Dispatched when the dialog is close.
298
+ * @event close
299
+ */
300
+ const preventDefault = !redispatchEvent(this, ev, { cancelable: true });
301
+ if (preventDefault) {
302
+ return;
303
+ }
304
+
305
+ this.open = false;
306
+ }
307
+
308
+ /**
309
+ * @param {Event} ev
310
+ */
311
+ _handleCancel(ev) {
312
+ const { target } = ev;
313
+ if (target !== this.dialogRef.value) {
314
+ return;
315
+ }
316
+
317
+ /**
318
+ * Dispatched when the dialog is cancel.
319
+ * @event cancel
320
+ */
321
+ const preventDefault = !redispatchEvent(this, ev, { cancelable: true });
322
+ if (preventDefault) {
323
+ return;
324
+ }
325
+
326
+ this.open = false;
327
+ }
328
+
329
+ _handleDialogClick() {
330
+ if (this._nextClickIsFromContent) {
331
+ this._nextClickIsFromContent = false;
332
+ return;
333
+ }
334
+
335
+ const { value } = this.dialogRef;
336
+ value?.dispatchEvent(new Event('cancel'));
337
+ }
338
+
339
+ _handleContentClick() {
340
+ this._nextClickIsFromContent = true;
341
+ }
342
+
343
+ _firstFocusTrap({ relatedTarget }) {
344
+ (relatedTarget != null ? this._firstFocusableChild : this._lastFocusableChild)?.focus();
345
+ }
346
+
347
+ _lastFocusTrap({ relatedTarget }) {
348
+ (relatedTarget != null ? this._lastFocusableChild : this._firstFocusableChild)?.focus();
349
+ }
350
+ }
@@ -0,0 +1,270 @@
1
+ /* c8 ignore start */
2
+ /**
3
+ * Re-dispatches an event from the provided element.
4
+ * @author @material/web
5
+ * @see https://github.com/material-components/material-web/blob/main/internal/events/redispatch-event.ts
6
+ * @param {Element} element - The element to dispatch the event from.
7
+ * @param {Event} event - The event to re-dispatch.
8
+ * @param {Object} [options={}] - An object with properties to override in the new event.
9
+ * @returns {boolean} - Whether or not the event was dispatched (if cancelable).
10
+ */
11
+
12
+ const redispatchEventFromEvent = (element, event, options = {}) => {
13
+ // For bubbling events in SSR light DOM (or composed), stop their propagation
14
+ // and dispatch the copy.
15
+ if (event.bubbles && (!element.shadowRoot || event.composed)) {
16
+ event.stopPropagation();
17
+ }
18
+
19
+ const copy = Reflect.construct(event.constructor, [event.type, { ...event, ...options }]);
20
+ const dispatched = element.dispatchEvent(copy);
21
+ if (!dispatched) {
22
+ event.preventDefault();
23
+ }
24
+
25
+ return dispatched;
26
+ };
27
+
28
+ /**
29
+ * Re-dispatches an event from the provided element.
30
+ *
31
+ * This function is useful for forwarding non-composed events, such as `change`
32
+ * events.
33
+ *
34
+ * @example
35
+ * class MyDialog extends LitElement {
36
+ * render() {
37
+ * return html`<dialog @close=${this.redispatchEvent}>...</dialog>`;
38
+ * }
39
+ *
40
+ * protected redispatchEvent(ev: Event) {
41
+ * redispatchEvent(this, ev, { cancelable: true });
42
+ * }
43
+ * }
44
+ *
45
+ * @param {Element} element - The element to dispatch the event from.
46
+ * @param {Event|string} event - The event to re-dispatch. If it's a string, a new Event is created.
47
+ * @param {Object} [options={}] - An object with properties to override in the new event.
48
+ * @returns {boolean} - Whether or not the event was dispatched (if cancelable).
49
+ */
50
+ export const redispatchEvent = (element, event, options = {}) => {
51
+ if (typeof event === 'string') {
52
+ const eventType = event;
53
+ const newEvent = new CustomEvent(eventType);
54
+ return redispatchEventFromEvent(element, newEvent, options);
55
+ }
56
+ return redispatchEventFromEvent(element, event, options);
57
+ };
58
+
59
+ /**
60
+ * Checks if an element should be ignored.
61
+ * @param {Element} element - The DOM element to check.
62
+ * @param {Array} [exceptions=['dialog', '[popover]']] - Array of Elements to ignore when checking the element.
63
+ * @returns {boolean} True if the element should be ignored by a screen reader, false otherwise.
64
+ */
65
+ export const isElementInvisible = (element, exceptions = ['dialog', '[popover]']) => {
66
+ if (!element || !(element instanceof HTMLElement)) {
67
+ return false;
68
+ }
69
+
70
+ if (element.matches(exceptions.join(','))) {
71
+ return false;
72
+ }
73
+
74
+ const computedStyle = window.getComputedStyle(element);
75
+ const isStyleHidden = computedStyle.display === 'none' || computedStyle.visibility === 'hidden';
76
+ const isAttributeHidden = element.matches('[disabled], [hidden], [inert], [aria-hidden="true"]');
77
+
78
+ return isStyleHidden || isAttributeHidden;
79
+ };
80
+
81
+ /**
82
+ * Checks if an element is focusable. An element is considered focusable if it matches
83
+ * standard focusable elements criteria (such as buttons, inputs, etc., that are not disabled
84
+ * and do not have a negative tabindex) or is a custom element with a shadow root that delegates focus.
85
+ *
86
+ * @param {Element} element - The DOM element to check for focusability.
87
+ * @returns {boolean} True if the element is focusable, false otherwise.
88
+ */
89
+ export const isFocusable = element => {
90
+ if (!(element instanceof HTMLElement)) {
91
+ return false;
92
+ }
93
+
94
+ // https://stackoverflow.com/a/30753870/76472
95
+ const knownFocusableElements = `a[href],area[href],button:not([disabled]),details,iframe,object,input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[contentEditable="true"],[tabindex]:not([tabindex^="-"]),audio[controls],video[controls]`;
96
+
97
+ if (element.matches(knownFocusableElements)) {
98
+ return true;
99
+ }
100
+
101
+ const isDisabledCustomElement =
102
+ element.localName.includes('-') && element.matches('[disabled], [aria-disabled="true"]');
103
+ if (isDisabledCustomElement) {
104
+ return false;
105
+ }
106
+
107
+ return element.shadowRoot?.delegatesFocus ?? false;
108
+ };
109
+
110
+ /**
111
+ * Retrieves the first and last focusable children of a node using a TreeWalker.
112
+ *
113
+ * @param {IterableIterator<HTMLElement>} walker - The TreeWalker object used to traverse the node's children.
114
+ * @returns {[first: HTMLElement|null, last: HTMLElement|null]} An object containing the first and last focusable children. If no focusable children are found, `null` is returned for both.
115
+ */
116
+ export const getFirstAndLastFocusableChildren = walker => {
117
+ let firstFocusableChild = null;
118
+ let lastFocusableChild = null;
119
+
120
+ for (const currentNode of walker) {
121
+ if (!firstFocusableChild) {
122
+ firstFocusableChild = currentNode;
123
+ }
124
+ lastFocusableChild = currentNode;
125
+ }
126
+
127
+ return [firstFocusableChild, lastFocusableChild];
128
+ };
129
+
130
+ /**
131
+ * Traverse the composed tree from the root, selecting elements that meet the provided filter criteria.
132
+ * You can pass [NodeFilter](https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter) or 0 to retrieve all nodes.
133
+ * @author Jan Miksovsky
134
+ * @see https://github.com/JanMiksovsky/elix/blob/main/src/core/dom.js
135
+ * @param {Node} node - The root node for traversal.
136
+ * @param {number} [whatToShow=0] - NodeFilter code for node types to include.
137
+ * @param {function} [filter=(n: Node) => true] - Filters nodes. Child nodes are considered even if parent does not satisfy the filter.
138
+ * @param {function} [skipNode=(n: Node) => false] - Determines whether to skip a node and its children.
139
+ * @returns {IterableIterator<Node>} An iterator yielding nodes meeting the filter criteria.
140
+ */
141
+ export function* walkComposedTree(
142
+ node,
143
+ whatToShow = 0,
144
+ filter = () => true,
145
+ skipNode = () => false,
146
+ ) {
147
+ if ((whatToShow && node.nodeType !== whatToShow) || skipNode(node)) {
148
+ return;
149
+ }
150
+
151
+ if (filter(node)) {
152
+ yield node;
153
+ }
154
+
155
+ const children =
156
+ // eslint-disable-next-line no-nested-ternary
157
+ node instanceof HTMLElement && node.shadowRoot
158
+ ? node.shadowRoot.children
159
+ : node instanceof HTMLSlotElement
160
+ ? node.assignedNodes({ flatten: true })
161
+ : node.childNodes;
162
+
163
+ for (const child of children) {
164
+ yield* walkComposedTree(child, whatToShow, filter, skipNode);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Returns the deepest active element, considering Shadow DOM subtrees
170
+ * @param {Document | ShadowRoot} root - The root element to start the search from.
171
+ * @returns {Element} The deepest active element or body element if no active element is found.
172
+ */
173
+ export const getDeepActiveElement = (root = document) => {
174
+ const activeEl = root.activeElement;
175
+ if (activeEl) {
176
+ if (activeEl.shadowRoot) {
177
+ return getDeepActiveElement(activeEl.shadowRoot) ?? activeEl;
178
+ }
179
+ return activeEl;
180
+ }
181
+ return document.body;
182
+ };
183
+
184
+ /**
185
+ * Returns true if the first node contains the second, even if the second node
186
+ * is in a shadow tree.
187
+ *
188
+ * The standard Node.contains() function does not account for Shadow DOM, and
189
+ * returns false if the supplied target node is sitting inside a shadow tree
190
+ * within the container.
191
+ *
192
+ * @param {Node} container - The container to search within.
193
+ * @param {Node} target - The node that may be inside the container.
194
+ * @returns {boolean} - True if the container contains the target node.
195
+ */
196
+ export const deepContains = (container, target) => {
197
+ /** @type {any} */
198
+ let current = target;
199
+ while (current) {
200
+ const parent = current.assignedSlot || current.parentNode || current.host;
201
+ if (parent === container) {
202
+ return true;
203
+ }
204
+ current = parent;
205
+ }
206
+ return false;
207
+ };
208
+
209
+ /**
210
+ * Search a list element for the item that contains the specified target.
211
+ *
212
+ * When dealing with UI events (e.g., mouse clicks) that may occur in
213
+ * subelements inside a list item, you can use this routine to obtain the
214
+ * containing list item.
215
+ *
216
+ * @author Jan Miksovsky
217
+ * @see https://github.com/JanMiksovsky/elix/blob/main/src/core/dom.js
218
+ * @param {NodeList|Node[]} items - A list element containing a set of items
219
+ * @param {Node} target - A target element that may or may not be an item in the
220
+ * list.
221
+ * @returns {number} - The index of the list child that is or contains the
222
+ * indicated target node. Returns -1 if not found.
223
+ */
224
+ export const indexOfItemContainingTarget = (items, target) =>
225
+ Array.prototype.findIndex.call(
226
+ items,
227
+ (/** @type Node */ item) => item === target || deepContains(item, target),
228
+ );
229
+
230
+ /**
231
+ * Return the ancestors of the given node in the composed tree.
232
+ *
233
+ * In the composed tree, the ancestor of a node assigned to a slot is that slot,
234
+ * not the node's DOM ancestor. The ancestor of a shadow root is its host.
235
+ *
236
+ * @author Jan Miksovsky
237
+ * @see https://github.com/JanMiksovsky/elix/blob/main/src/core/dom.js
238
+ * @param {Node} node
239
+ * @returns {Iterable<Node>}
240
+ */
241
+ export function* composedAncestors(node) {
242
+ for (let /** @type {Node|null} */ current = node; current; ) {
243
+ const next =
244
+ // eslint-disable-next-line no-nested-ternary
245
+ current instanceof HTMLElement && current.assignedSlot
246
+ ? current.assignedSlot
247
+ : current instanceof ShadowRoot
248
+ ? current.host
249
+ : current.parentNode;
250
+ if (next) {
251
+ yield next;
252
+ }
253
+ current = next;
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Checks if a click event occurred inside a given bounding rectangle.
259
+ *
260
+ * @param {DOMRect} rect - The bounding rectangle, typically obtained from `element.getBoundingClientRect()`.
261
+ * @param {PointerEvent} ev - The click event.
262
+ * @returns {boolean} True if the click occurred inside the rectangle, false otherwise.
263
+ */
264
+ export const isClickInsideRect = (rect, ev) => {
265
+ const { top, left, height, width } = rect;
266
+ const { clientX, clientY } = ev;
267
+ return clientY >= top && clientY <= top + height && clientX >= left && clientX <= left + width;
268
+ };
269
+
270
+ export const randomID = (length = 10) => Math.random().toString(36).substring(2, length);
package/src/index.js ADDED
@@ -0,0 +1 @@
1
+ export { BlockquoteDialog } from './BlockquoteDialog.js';
@@ -0,0 +1,45 @@
1
+ import { css } from 'lit';
2
+
3
+ export const styles = css`/* Closed state of the dialog */
4
+ dialog {
5
+ opacity: 0;
6
+ transform: translateY(16%);
7
+ transition: opacity 200ms ease-out, transform 200ms ease-out, overlay 200ms ease-out allow-discrete, display 200ms ease-out allow-discrete;
8
+ /* Equivalent to
9
+ transition: all 200ms allow-discrete; */
10
+ }
11
+
12
+ /* Open state of the dialog */
13
+ dialog[open] {
14
+ opacity: 1;
15
+ transform: translateY(0);
16
+ }
17
+
18
+ /* Before-open state */
19
+ /* Needs to be after the previous dialog[open] rule to take effect,
20
+ as the specificity is the same */
21
+ @starting-style {
22
+ dialog[open] {
23
+ opacity: 0;
24
+ transform: translateY(16%);
25
+ }
26
+ }
27
+ /* Transition the :backdrop when the dialog modal is promoted to the top layer */
28
+ dialog::backdrop {
29
+ background-color: rgba(120, 120, 120, 0);
30
+ transition: display 190ms ease-in allow-discrete, overlay 190ms ease-in allow-discrete, background-color 190ms;
31
+ /* Equivalent to
32
+ transition: all 190ms allow-discrete; */
33
+ }
34
+
35
+ dialog[open]::backdrop {
36
+ background-color: rgba(120, 120, 120, 0.25);
37
+ }
38
+
39
+ /* This starting-style rule cannot be nested inside the above selector
40
+ because the nesting selector cannot represent pseudo-elements. */
41
+ @starting-style {
42
+ dialog[open]::backdrop {
43
+ background-color: rgba(120, 120, 120, 0);
44
+ }
45
+ }`;
@@ -0,0 +1,66 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { css } from 'lit';
3
+
4
+ export const styles = css`:host {
5
+ --_background-color: var(--blockquote-dialog-background-color, rgb(255, 255, 255));
6
+ --_max-height: var(--blockquote-dialog-max-height, min(35rem, calc(100% - 3rem)));
7
+ --_max-width: var(--blockquote-dialog-max-width, min(35rem, calc(100% - 3rem)));
8
+ --_min-height: var(--blockquote-dialog-min-height, 8.75rem);
9
+ --_min-width: var(--blockquote-dialog-min-width, 17.5rem);
10
+ --_padding: var(--blockquote-padding, 1rem);
11
+ box-sizing: border-box;
12
+ display: contents;
13
+ background-color: var(--_background-color);
14
+ margin: auto;
15
+ max-height: var(--_max-height);
16
+ max-width: var(--_max-width);
17
+ min-height: var(--_min-height);
18
+ min-width: var(--_min-width);
19
+ position: fixed;
20
+ height: -moz-fit-content;
21
+ height: fit-content;
22
+ width: -moz-fit-content;
23
+ width: fit-content;
24
+ }
25
+
26
+ :host([hidden]),
27
+ [hidden] {
28
+ display: none !important;
29
+ }
30
+
31
+ *,
32
+ *::before,
33
+ *::after {
34
+ box-sizing: inherit;
35
+ }
36
+
37
+ dialog {
38
+ background: inherit;
39
+ border: none;
40
+ border-radius: inherit;
41
+ flex-direction: column;
42
+ margin: inherit;
43
+ height: inherit;
44
+ width: inherit;
45
+ max-height: inherit;
46
+ max-width: inherit;
47
+ min-height: inherit;
48
+ min-width: inherit;
49
+ outline: none;
50
+ overflow: visible;
51
+ padding: 0;
52
+ }
53
+
54
+ :host([open]) dialog {
55
+ display: flex;
56
+ }
57
+
58
+ .scroller {
59
+ overflow-y: auto;
60
+ min-height: var(--_min-height);
61
+ }
62
+
63
+ .content {
64
+ padding: var(--_padding);
65
+ min-height: inherit;
66
+ }`;