@vollowx/seele 0.7.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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -0
  3. package/package.json +62 -0
  4. package/src/all.js +19 -0
  5. package/src/base/button.js +61 -0
  6. package/src/base/checkbox.js +118 -0
  7. package/src/base/controllers/list-controller.js +96 -0
  8. package/src/base/controllers/popover-controller.js +163 -0
  9. package/src/base/field.js +3 -0
  10. package/src/base/hidden-styles.css.js +2 -0
  11. package/src/base/input.js +182 -0
  12. package/src/base/item.js +7 -0
  13. package/src/base/list-item.js +54 -0
  14. package/src/base/menu-item.js +12 -0
  15. package/src/base/menu-utils.js +111 -0
  16. package/src/base/menu.js +244 -0
  17. package/src/base/mixins/attachable.js +71 -0
  18. package/src/base/mixins/form-associated.js +69 -0
  19. package/src/base/mixins/internals-attached.js +13 -0
  20. package/src/base/option.js +17 -0
  21. package/src/base/select.js +285 -0
  22. package/src/base/switch.js +86 -0
  23. package/src/base/tooltip.js +139 -0
  24. package/src/core/focus-visible.js +13 -0
  25. package/src/core/shared.d.ts +1 -0
  26. package/src/core/unique-id.js +11 -0
  27. package/src/m3/button/common-button-styles.css.js +2 -0
  28. package/src/m3/button/common-button-toggle-styles.css.js +2 -0
  29. package/src/m3/button/common-button-toggle.js +69 -0
  30. package/src/m3/button/common-button.js +65 -0
  31. package/src/m3/button/icon-button-styles.css.js +2 -0
  32. package/src/m3/button/icon-button-toggle-styles.css.js +2 -0
  33. package/src/m3/button/icon-button-toggle.js +57 -0
  34. package/src/m3/button/icon-button.js +51 -0
  35. package/src/m3/button/shared-button-styles.css.js +2 -0
  36. package/src/m3/checkbox-styles.css.js +2 -0
  37. package/src/m3/checkbox.js +46 -0
  38. package/src/m3/fab-styles.css.js +2 -0
  39. package/src/m3/fab.js +48 -0
  40. package/src/m3/field/field-styles.css.js +2 -0
  41. package/src/m3/field/field.js +93 -0
  42. package/src/m3/field/filled-field-styles.css.js +2 -0
  43. package/src/m3/field/filled-field.js +30 -0
  44. package/src/m3/field/outlined-field-styles.css.js +2 -0
  45. package/src/m3/field/outlined-field.js +34 -0
  46. package/src/m3/focus-ring-styles.css.js +2 -0
  47. package/src/m3/focus-ring.js +72 -0
  48. package/src/m3/item-styles.css.js +2 -0
  49. package/src/m3/item.js +46 -0
  50. package/src/m3/list-item-styles.css.js +2 -0
  51. package/src/m3/list-item.js +52 -0
  52. package/src/m3/list-styles.css.js +2 -0
  53. package/src/m3/list.js +16 -0
  54. package/src/m3/menu-item.js +15 -0
  55. package/src/m3/menu-part-styles.css.js +2 -0
  56. package/src/m3/menu-styles.css.js +2 -0
  57. package/src/m3/menu.js +30 -0
  58. package/src/m3/option.js +15 -0
  59. package/src/m3/ripple-styles.css.js +2 -0
  60. package/src/m3/ripple.js +199 -0
  61. package/src/m3/select/filled-select.js +41 -0
  62. package/src/m3/select/outlined-select.js +41 -0
  63. package/src/m3/select/select-styles.css.js +2 -0
  64. package/src/m3/select/select.js +34 -0
  65. package/src/m3/switch-styles.css.js +2 -0
  66. package/src/m3/switch.js +129 -0
  67. package/src/m3/target-styles.css.js +2 -0
  68. package/src/m3/text-field/filled-text-field.js +38 -0
  69. package/src/m3/text-field/outlined-text-field.js +40 -0
  70. package/src/m3/text-field/text-field-styles.css.js +2 -0
  71. package/src/m3/toolbar-styles.css.js +2 -0
  72. package/src/m3/toolbar.js +53 -0
  73. package/src/m3/tooltip-styles.css.js +2 -0
  74. package/src/m3/tooltip.js +18 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 - 2025 Vollow
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,79 @@
1
+ # Standard Extensible Elements
2
+
3
+ **SEELE** is a modern, lightweight [Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) library. It provides a set of highly customizable UI components that follow the [Material Design 3](https://m3.material.io/) guidelines out of the box, while being designed for easy extension and restyling.
4
+
5
+ Visit the [website of SEELE](https://seele.v9.nz/) for documentation and demos.
6
+
7
+ ## Features
8
+
9
+ - **Material Design 3**: Ready-to-use components following the latest Material guidelines.
10
+ - **Web Components**: Framework-agnostic. Works with vanilla HTML or any framework.
11
+ - **Extensible**: Built to be extended. Create your own design system on top of SEELE's logic.
12
+ - **Lightweight**: Built on [Lit](https://lit.dev/) and [floating-ui](https://floating-ui.com/) only, ensuring fast performance and small bundle sizes.
13
+ - **Accessible**: Designed with accessibility in mind (using `ElementInternals` and standard ARIA patterns).
14
+
15
+ ## Installation
16
+
17
+ Install SEELE using your preferred package manager:
18
+
19
+ ```bash
20
+ # npm
21
+ npm install @vollowx/seele
22
+
23
+ # pnpm
24
+ pnpm add @vollowx/seele
25
+
26
+ # yarn
27
+ yarn add @vollowx/seele
28
+
29
+ # bun
30
+ bun add @vollowx/seele
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ### Importing Components
36
+
37
+ You can import the entire library or individual components to keep your bundle size small.
38
+
39
+ ```javascript
40
+ // Import all components
41
+ import '@vollowx/seele';
42
+
43
+ // OR Import specific components (Recommended)
44
+ import '@vollowx/seele/m3/button/common-button.js';
45
+ import '@vollowx/seele/m3/checkbox.js';
46
+ ```
47
+
48
+ ### Using Components
49
+
50
+ Once imported, use the components just like standard HTML tags.
51
+
52
+ ```html
53
+ <md-button variant="filled">Filled Button</md-button>
54
+ <md-button variant="outlined">Outlined Button</md-button>
55
+
56
+ <label>
57
+ <md-checkbox checked></md-checkbox>
58
+ Labelled Checkbox
59
+ </label>
60
+ ```
61
+
62
+ ### Theming
63
+
64
+ SEELE components use CSS variables for styling. Currently, the global Material Design 3 token variables are not included in the JavaScript bundle.
65
+
66
+ To style the components correctly, you need to define the necessary CSS variables in your project. You can find reference implementations in [vollowx/seele-docs](https://github.com/vollowx/seele-docs/) or the `dev` folder of this repository.
67
+
68
+ ## Browser Supporty
69
+
70
+ SEELE relies on modern web standards like `ElementInternals`.
71
+
72
+ - **Chromium**: `>= 125.0`
73
+ - **Firefox**: `>= 126.0`
74
+
75
+ ## Resources
76
+
77
+ - [Roadmap](./ROADMAP.md)
78
+ - [Contributing Guide](./CONTRIBUTING.md)
79
+ - [License](./LICENSE)
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@vollowx/seele",
3
+ "version": "0.7.0",
4
+ "description": "Standard Extensible Elements. A web components library that can be styled and extended freely, pre-providing components in Material Design 3.",
5
+ "author": "vollowx",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./src/all.js",
9
+ "module": "./src/all.js",
10
+ "exports": {
11
+ ".": "./src/all.js",
12
+ "./m3/*": "./src/m3/*.js",
13
+ "./base/*": "./src/base/*.js"
14
+ },
15
+ "files": [
16
+ "src/**/*.js",
17
+ "src/**/*.d.ts",
18
+ "LICENSE",
19
+ "README.md"
20
+ ],
21
+ "keywords": [
22
+ "web-components",
23
+ "material-design",
24
+ "material-3",
25
+ "lit",
26
+ "ui-components"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/vollowx/seele.git"
31
+ },
32
+ "scripts": {
33
+ "dev": "wds",
34
+ "dev:styles": "node scripts/convert-styles.mjs watch",
35
+ "build": "bun run build:styles && bun run build:components",
36
+ "build:styles": "node scripts/convert-styles.mjs build",
37
+ "build:components": "tsc",
38
+ "cleanup": "node scripts/cleanup.mjs",
39
+ "prepublishOnly": "bun cleanup && bun run build",
40
+ "format": "prettier . --check",
41
+ "format:fix": "prettier . --check --write",
42
+ "format:sortcss": "bunx postcss \"src/**/*.css\" --replace --no-map"
43
+ },
44
+ "devDependencies": {
45
+ "@custom-elements-manifest/analyzer": "^0.11.0",
46
+ "@types/node": "^25.0.3",
47
+ "@web/dev-server": "^0.4.6",
48
+ "@web/dev-server-esbuild": "^1.0.4",
49
+ "chokidar": "^5.0.0",
50
+ "lightningcss": "^1.30.2",
51
+ "postcss": "^8.5.6",
52
+ "postcss-cli": "^11.0.1",
53
+ "postcss-sorting": "^9.1.0",
54
+ "prettier": "^3.7.4",
55
+ "typescript": "^5.9.3"
56
+ },
57
+ "dependencies": {
58
+ "@floating-ui/dom": "^1.7.4",
59
+ "lit": "^3.3.2",
60
+ "tslib": "^2.8.1"
61
+ }
62
+ }
package/src/all.js ADDED
@@ -0,0 +1,19 @@
1
+ export { M3Button } from './m3/button/common-button.js';
2
+ export { M3ButtonToggle } from './m3/button/common-button-toggle.js';
3
+ export { M3IconButton } from './m3/button/icon-button.js';
4
+ export { M3IconButtonToggle } from './m3/button/icon-button-toggle.js';
5
+ export { M3FilledTextField } from './m3/text-field/filled-text-field.js';
6
+ export { M3OutlinedTextField } from './m3/text-field/outlined-text-field.js';
7
+ export { MdFilledSelect } from './m3/select/filled-select.js';
8
+ export { MdOutlinedSelect } from './m3/select/outlined-select.js';
9
+ export { M3Checkbox } from './m3/checkbox.js';
10
+ export { M3FAB } from './m3/fab.js';
11
+ export { M3Item } from './m3/item.js';
12
+ export { M3List } from './m3/list.js';
13
+ export { M3Menu } from './m3/menu.js';
14
+ export { M3MenuItem } from './m3/menu-item.js';
15
+ export { M3Option } from './m3/option.js';
16
+ export { M3Ripple } from './m3/ripple.js';
17
+ export { M3Switch } from './m3/switch.js';
18
+ export { M3Tooltip } from './m3/tooltip.js';
19
+ export { M3Toolbar } from './m3/toolbar.js';
@@ -0,0 +1,61 @@
1
+ import { __decorate } from "tslib";
2
+ import { LitElement } from 'lit';
3
+ import { property } from 'lit/decorators.js';
4
+ import { InternalsAttached, internals } from './mixins/internals-attached.js';
5
+ import { FormAssociated } from './mixins/form-associated.js';
6
+ import { hiddenStyles } from './hidden-styles.css.js';
7
+ const Base = FormAssociated(InternalsAttached(LitElement));
8
+ export class Button extends Base {
9
+ static { this.styles = [hiddenStyles]; }
10
+ constructor() {
11
+ super();
12
+ this.type = 'button';
13
+ this.#handleKeyDown = (e) => {
14
+ if (e.key !== ' ' && e.key !== 'Enter')
15
+ return;
16
+ e.preventDefault();
17
+ e.stopPropagation();
18
+ if (e.key === 'Enter')
19
+ this.click();
20
+ };
21
+ this.#handleKeyUp = (e) => {
22
+ if (e.key === ' ') {
23
+ e.preventDefault();
24
+ e.stopPropagation();
25
+ this.click();
26
+ }
27
+ };
28
+ this.#handleClick = () => {
29
+ if (this.type !== 'button')
30
+ this[internals].form?.[this.type]();
31
+ };
32
+ this[internals].role = 'button';
33
+ this.updateState();
34
+ }
35
+ connectedCallback() {
36
+ super.connectedCallback();
37
+ this.addEventListener('keydown', this.#handleKeyDown);
38
+ this.addEventListener('keyup', this.#handleKeyUp);
39
+ this.addEventListener('click', this.#handleClick);
40
+ }
41
+ disconnectedCallback() {
42
+ super.disconnectedCallback();
43
+ this.removeEventListener('keydown', this.#handleKeyDown);
44
+ this.removeEventListener('keyup', this.#handleKeyUp);
45
+ this.removeEventListener('click', this.#handleClick);
46
+ }
47
+ updated(changed) {
48
+ if (changed.has('disabled'))
49
+ this.updateState();
50
+ }
51
+ updateState() {
52
+ this.tabIndex = this.disabled ? -1 : 0;
53
+ this[internals].ariaDisabled = String(this.disabled);
54
+ }
55
+ #handleKeyDown;
56
+ #handleKeyUp;
57
+ #handleClick;
58
+ }
59
+ __decorate([
60
+ property({ reflect: true })
61
+ ], Button.prototype, "type", void 0);
@@ -0,0 +1,118 @@
1
+ import { __decorate } from "tslib";
2
+ import { LitElement } from 'lit';
3
+ import { property } from 'lit/decorators.js';
4
+ import { FormAssociated } from './mixins/form-associated.js';
5
+ import { InternalsAttached, internals } from './mixins/internals-attached.js';
6
+ import { hiddenStyles } from './hidden-styles.css.js';
7
+ const PROPERTY_FROM_ARIA_CHECKED = {
8
+ true: 'checked',
9
+ false: 'unchecked',
10
+ mixed: 'indeterminate',
11
+ };
12
+ const Base = FormAssociated(InternalsAttached(LitElement));
13
+ export class Checkbox extends Base {
14
+ static { this.styles = [hiddenStyles]; }
15
+ constructor() {
16
+ super();
17
+ this.checked = false;
18
+ this.indeterminate = false;
19
+ this.required = false;
20
+ this.#handleClick = (e) => {
21
+ e.stopPropagation();
22
+ e.preventDefault();
23
+ this.#toggleChecked();
24
+ };
25
+ this.#handleKeyDown = (e) => {
26
+ if (e.key === ' ') {
27
+ e.preventDefault();
28
+ e.stopPropagation();
29
+ }
30
+ };
31
+ this.#handleKeyUp = (e) => {
32
+ if (e.key === ' ') {
33
+ e.preventDefault();
34
+ e.stopPropagation();
35
+ this.#toggleChecked();
36
+ }
37
+ };
38
+ this[internals].role = 'checkbox';
39
+ this.checked = this.hasAttribute('checked');
40
+ this.indeterminate = this.hasAttribute('indeterminate');
41
+ this.updateState();
42
+ }
43
+ connectedCallback() {
44
+ super.connectedCallback();
45
+ this.addEventListener('click', this.#handleClick);
46
+ this.addEventListener('keydown', this.#handleKeyDown);
47
+ this.addEventListener('keyup', this.#handleKeyUp);
48
+ }
49
+ disconnectedCallback() {
50
+ super.disconnectedCallback();
51
+ this.removeEventListener('click', this.#handleClick);
52
+ this.removeEventListener('keydown', this.#handleKeyDown);
53
+ this.removeEventListener('keyup', this.#handleKeyUp);
54
+ }
55
+ updated(changed) {
56
+ if (changed.has('checked') ||
57
+ changed.has('disabled') ||
58
+ changed.has('indeterminate') ||
59
+ changed.has('required')) {
60
+ this.updateState();
61
+ }
62
+ }
63
+ updateState() {
64
+ const prevAriaChecked = this[internals]
65
+ .ariaChecked;
66
+ this[internals].states.delete('was-unchecked');
67
+ this[internals].states.delete('was-checked');
68
+ this[internals].states.delete('was-indeterminate');
69
+ if (prevAriaChecked && PROPERTY_FROM_ARIA_CHECKED[prevAriaChecked]) {
70
+ this[internals].states.add(`was-${PROPERTY_FROM_ARIA_CHECKED[prevAriaChecked]}`);
71
+ }
72
+ this[internals].ariaChecked = this.indeterminate
73
+ ? 'mixed'
74
+ : this.checked
75
+ ? 'true'
76
+ : 'false';
77
+ const currentAriaChecked = this[internals]
78
+ .ariaChecked;
79
+ this[internals].states.delete('unchecked');
80
+ this[internals].states.delete('checked');
81
+ this[internals].states.delete('indeterminate');
82
+ this[internals].states.add(`${PROPERTY_FROM_ARIA_CHECKED[currentAriaChecked]}`);
83
+ this.tabIndex = this.disabled ? -1 : 0;
84
+ this[internals].ariaDisabled = String(this.disabled);
85
+ this[internals].setFormValue(this.checked ? 'on' : null);
86
+ if (this.required && !this.checked) {
87
+ this[internals].setValidity({ valueMissing: true },
88
+ // TODO: I18n
89
+ 'Please check this box if you want to proceed.');
90
+ }
91
+ else {
92
+ this[internals].setValidity({});
93
+ }
94
+ }
95
+ #handleClick;
96
+ #handleKeyDown;
97
+ #handleKeyUp;
98
+ #toggleChecked() {
99
+ if (this.disabled)
100
+ return;
101
+ this.checked = !this.checked;
102
+ this.indeterminate = false;
103
+ this.dispatchEvent(new CustomEvent('change', {
104
+ bubbles: true,
105
+ composed: true,
106
+ detail: this.checked,
107
+ }));
108
+ }
109
+ }
110
+ __decorate([
111
+ property({ type: Boolean })
112
+ ], Checkbox.prototype, "checked", void 0);
113
+ __decorate([
114
+ property({ type: Boolean })
115
+ ], Checkbox.prototype, "indeterminate", void 0);
116
+ __decorate([
117
+ property({ type: Boolean, reflect: true })
118
+ ], Checkbox.prototype, "required", void 0);
@@ -0,0 +1,96 @@
1
+ import { getIndexByLetter } from '../menu-utils.js';
2
+ export class ListController {
3
+ constructor(host, config) {
4
+ this._focusedIndex = -1;
5
+ this.searchString = '';
6
+ this.searchTimeout = null;
7
+ const { isItem, getPossibleItems, blurItem, focusItem, wrapNavigation } = config;
8
+ (this.host = host).addController(this);
9
+ this.isItem = isItem;
10
+ this.getPossibleItems = getPossibleItems;
11
+ this.blurItem = blurItem;
12
+ this.focusItem = focusItem;
13
+ this.wrapNavigation = wrapNavigation;
14
+ }
15
+ hostConnected() { }
16
+ hostDisconnected() { }
17
+ get items() {
18
+ return this.getPossibleItems().filter(this.isItem);
19
+ }
20
+ get currentIndex() {
21
+ const items = this.getPossibleItems().filter(this.isItem);
22
+ return items.findIndex((item) => item.focused) ?? -1;
23
+ }
24
+ handleType(char) {
25
+ const searchString = this.getSearchString(char);
26
+ const items = this.items;
27
+ const optionsText = items.map((item) => item.innerText);
28
+ const searchIndex = getIndexByLetter(optionsText, searchString, this.currentIndex + 1);
29
+ if (searchIndex >= 0) {
30
+ this._focusItem(items[searchIndex]);
31
+ return true;
32
+ }
33
+ else {
34
+ if (this.searchTimeout)
35
+ window.clearTimeout(this.searchTimeout);
36
+ this.searchString = '';
37
+ return false;
38
+ }
39
+ }
40
+ getSearchString(char) {
41
+ if (this.searchTimeout) {
42
+ window.clearTimeout(this.searchTimeout);
43
+ }
44
+ this.searchTimeout = window.setTimeout(() => {
45
+ this.searchString = '';
46
+ }, 500);
47
+ this.searchString += char;
48
+ return this.searchString;
49
+ }
50
+ clearSearch() {
51
+ this.searchString = '';
52
+ if (this.searchTimeout) {
53
+ window.clearTimeout(this.searchTimeout);
54
+ this.searchTimeout = null;
55
+ }
56
+ }
57
+ _focusItem(item) {
58
+ if (this._focusedIndex !== -1)
59
+ this._blurItem(this.items[this._focusedIndex]);
60
+ this.focusItem(item);
61
+ this._focusedIndex = this.items.indexOf(item);
62
+ }
63
+ _blurItem(item) {
64
+ this.blurItem(item);
65
+ this._focusedIndex = -1;
66
+ }
67
+ focusFirstItem() {
68
+ this._focusItem(this.items[0]);
69
+ }
70
+ focusLastItem() {
71
+ this._focusItem(this.items[this.items.length - 1]);
72
+ }
73
+ focusNextItem() {
74
+ const count = this.items.length;
75
+ if (count === 0)
76
+ return;
77
+ let nextIndex = this._focusedIndex + 1;
78
+ if (nextIndex >= count) {
79
+ nextIndex = this.wrapNavigation() ? count - 1 : 0;
80
+ }
81
+ this._focusItem(this.items[nextIndex]);
82
+ }
83
+ focusPreviousItem() {
84
+ const count = this.items.length;
85
+ if (count === 0)
86
+ return;
87
+ let prevIndex = this._focusedIndex - 1;
88
+ if (prevIndex < 0) {
89
+ prevIndex = this.wrapNavigation() ? 0 : count - 1;
90
+ }
91
+ this._focusItem(this.items[prevIndex]);
92
+ }
93
+ handleSlotChange() {
94
+ this._focusedIndex = this.currentIndex;
95
+ }
96
+ }
@@ -0,0 +1,163 @@
1
+ import { internals, } from '../mixins/internals-attached.js';
2
+ import { autoUpdate, computePosition, arrow, flip, offset, shift, } from '@floating-ui/dom';
3
+ export function transformOriginFromArrow(placement, arrowData) {
4
+ const { x: arrowX, y: arrowY } = arrowData || {};
5
+ const [side] = placement.split('-');
6
+ let originX = '';
7
+ let originY = '';
8
+ if (side === 'top') {
9
+ originX = arrowX != null ? `${arrowX}px` : 'center';
10
+ originY = 'bottom';
11
+ }
12
+ else if (side === 'bottom') {
13
+ originX = arrowX != null ? `${arrowX}px` : 'center';
14
+ originY = 'top';
15
+ }
16
+ else if (side === 'left') {
17
+ originX = 'right';
18
+ originY = arrowY != null ? `${arrowY}px` : 'center';
19
+ }
20
+ else if (side === 'right') {
21
+ originX = 'left';
22
+ originY = arrowY != null ? `${arrowY}px` : 'center';
23
+ }
24
+ return `${originX} ${originY}`;
25
+ }
26
+ export class PopoverController {
27
+ get open() {
28
+ return this._open;
29
+ }
30
+ constructor(host, config) {
31
+ this._open = false;
32
+ // Different from those in the Tooltip, these timers are used to manage the
33
+ // animation timing.
34
+ this.#openTimer = null;
35
+ this.#closeTimer = null;
36
+ // TODO: Provide the ability to specify an arrow element.
37
+ this._dummyArrow = document.createElement('div');
38
+ this.#handleClickOutside = (event) => {
39
+ const trigger = this.config.trigger();
40
+ const popover = this.config.popover();
41
+ const path = event.composedPath();
42
+ if (trigger && path.includes(trigger))
43
+ return;
44
+ if (popover && path.includes(popover))
45
+ return;
46
+ this.config.onClickOutside?.();
47
+ };
48
+ (this.host = host).addController(this);
49
+ this.config = config;
50
+ }
51
+ hostConnected() {
52
+ // Initial state
53
+ if (!this._open) {
54
+ this.host[internals].states.add('closed');
55
+ }
56
+ }
57
+ hostDisconnected() {
58
+ this.cleanupAutoUpdate?.();
59
+ window.removeEventListener('pointerup', this.#handleClickOutside);
60
+ }
61
+ // Different from those in the Tooltip, these timers are used to manage the
62
+ // animation timing.
63
+ #openTimer;
64
+ #closeTimer;
65
+ async animateOpen() {
66
+ if (this._open)
67
+ return;
68
+ this._open = true;
69
+ // Prevent the click that triggered the open from immediately closing it
70
+ // TODO: Use a global event listener to manage this more effectively
71
+ setTimeout(() => {
72
+ if (this._open) {
73
+ window.addEventListener('pointerup', this.#handleClickOutside);
74
+ }
75
+ }, 0);
76
+ clearTimeout(this.#openTimer);
77
+ clearTimeout(this.#closeTimer);
78
+ const openDuration = this.config.durations.open();
79
+ this.host[internals].states.delete('closed');
80
+ this.host[internals].states.delete('closing');
81
+ const trigger = this.config.trigger();
82
+ const popover = this.config.popover();
83
+ if (trigger && popover) {
84
+ this.cleanupAutoUpdate?.();
85
+ this.cleanupAutoUpdate = autoUpdate(trigger, popover, () => this.reposition());
86
+ // Initial position
87
+ await this.reposition();
88
+ }
89
+ if (openDuration === 0) {
90
+ this.host[internals].states.add('opened');
91
+ }
92
+ else {
93
+ // Wait for a frame to ensure the element is rendered with base styles
94
+ // e.g. (opacity 0) before adding the opening state e.g. (opacity 1) to
95
+ // trigger transition.
96
+ requestAnimationFrame(() => {
97
+ if (this._open) {
98
+ this.host[internals].states.add('opening');
99
+ }
100
+ });
101
+ this.#openTimer = setTimeout(() => {
102
+ this.host[internals].states.delete('opening');
103
+ this.host[internals].states.add('opened');
104
+ }, openDuration);
105
+ }
106
+ }
107
+ async animateClose() {
108
+ if (!this._open)
109
+ return;
110
+ this._open = false;
111
+ window.removeEventListener('pointerup', this.#handleClickOutside);
112
+ clearTimeout(this.#openTimer);
113
+ clearTimeout(this.#closeTimer);
114
+ const closeDuration = this.config.durations.close();
115
+ const wasOpened = this.host[internals].states.has('opened');
116
+ this.host[internals].states.delete('opened');
117
+ this.host[internals].states.delete('opening');
118
+ if (closeDuration === 0 || !wasOpened) {
119
+ this.cleanup();
120
+ this.host[internals].states.delete('closing');
121
+ this.host[internals].states.add('closed');
122
+ }
123
+ else {
124
+ this.host[internals].states.add('closing');
125
+ this.#closeTimer = setTimeout(() => {
126
+ this.cleanup();
127
+ this.host[internals].states.delete('closing');
128
+ this.host[internals].states.add('closed');
129
+ }, closeDuration);
130
+ }
131
+ }
132
+ cleanup() {
133
+ this.cleanupAutoUpdate?.();
134
+ this.cleanupAutoUpdate = undefined;
135
+ }
136
+ reposition() {
137
+ const trigger = this.config.trigger();
138
+ const popover = this.config.popover();
139
+ if (!trigger || !popover)
140
+ return Promise.resolve();
141
+ return computePosition(trigger, popover, {
142
+ placement: this.config.positioning.placement(),
143
+ strategy: this.config.positioning.strategy(),
144
+ middleware: [
145
+ offset(this.config.positioning.offset()),
146
+ flip({ padding: this.config.positioning.windowPadding() }),
147
+ shift({
148
+ padding: this.config.positioning.windowPadding(),
149
+ crossAxis: true,
150
+ }),
151
+ arrow({ element: this._dummyArrow }),
152
+ ],
153
+ }).then(({ x, y, strategy, placement, middlewareData }) => {
154
+ Object.assign(popover.style, {
155
+ left: `${x}px`,
156
+ top: `${y}px`,
157
+ position: strategy,
158
+ transformOrigin: transformOriginFromArrow(placement, middlewareData.arrow),
159
+ });
160
+ });
161
+ }
162
+ #handleClickOutside;
163
+ }
@@ -0,0 +1,3 @@
1
+ import { LitElement } from 'lit';
2
+ export class Field extends LitElement {
3
+ }
@@ -0,0 +1,2 @@
1
+ import { css } from 'lit';
2
+ export const hiddenStyles = css `:host([hidden]){visibility:hidden;display:none}`;