@vollowx/seele 0.13.6 → 0.13.8

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 (65) hide show
  1. package/README.md +32 -13
  2. package/package.json +1 -1
  3. package/src/all.d.ts +2 -0
  4. package/src/all.js +2 -0
  5. package/src/base/autocomplete.d.ts +1 -0
  6. package/src/base/autocomplete.js +7 -13
  7. package/src/base/button.d.ts +4 -2
  8. package/src/base/button.js +15 -10
  9. package/src/base/checkbox.js +3 -8
  10. package/src/base/controllers/popover-controller.js +1 -1
  11. package/src/base/experimental/popup-styles.css.js +2 -0
  12. package/src/base/experimental/popup.d.ts +38 -0
  13. package/src/base/experimental/popup.js +276 -0
  14. package/src/base/menu.d.ts +2 -0
  15. package/src/base/menu.js +2 -0
  16. package/src/base/mixins/button-toggle-mixin.d.ts +7 -0
  17. package/src/base/mixins/button-toggle-mixin.js +102 -0
  18. package/src/base/mixins/internals-attached.d.ts +2 -0
  19. package/src/base/mixins/internals-attached.js +5 -0
  20. package/src/base/toggle-button.d.ts +6 -45
  21. package/src/base/toggle-button.js +6 -135
  22. package/src/core/ensure-ready.d.ts +1 -0
  23. package/src/core/ensure-ready.js +15 -0
  24. package/src/m3/autocomplete/autocomplete.d.ts +1 -0
  25. package/src/m3/autocomplete/autocomplete.js +1 -0
  26. package/src/m3/badge/badge.d.ts +1 -0
  27. package/src/m3/badge/badge.js +4 -0
  28. package/src/m3/button/common-button-toggle.d.ts +4 -10
  29. package/src/m3/button/common-button-toggle.js +6 -40
  30. package/src/m3/button/icon-button-toggle.d.ts +4 -6
  31. package/src/m3/button/icon-button-toggle.js +6 -33
  32. package/src/m3/button/shared-button-styles.css.js +1 -1
  33. package/src/m3/button/shared-button-toggle-styles.css.js +1 -1
  34. package/src/m3/checkbox/checkbox-styles.css.js +1 -1
  35. package/src/m3/checkbox/checkbox.d.ts +1 -1
  36. package/src/m3/dialog/dialog-styles.css.js +1 -1
  37. package/src/m3/experimental/popup-styles.css.js +2 -0
  38. package/src/m3/experimental/popup.d.ts +9 -0
  39. package/src/m3/experimental/popup.js +11 -0
  40. package/src/m3/fab/fab-styles.css.js +1 -1
  41. package/src/m3/field/field-styles.css.js +1 -1
  42. package/src/m3/field/filled-field-styles.css.js +1 -1
  43. package/src/m3/focus-ring/focus-ring-styles.css.js +1 -1
  44. package/src/m3/list/list-item-styles.css.js +1 -1
  45. package/src/m3/list/list-item.d.ts +4 -2
  46. package/src/m3/list/list-item.js +2 -0
  47. package/src/m3/menu/menu-styles.css.js +1 -1
  48. package/src/m3/radio/radio-styles.css.js +1 -1
  49. package/src/m3/ripple/ripple-styles.css.js +1 -1
  50. package/src/m3/ripple/ripple.d.ts +1 -12
  51. package/src/m3/ripple/ripple.js +8 -9
  52. package/src/m3/select/select.d.ts +1 -0
  53. package/src/m3/select/select.js +1 -0
  54. package/src/m3/switch/switch-styles.css.js +1 -1
  55. package/src/m3/switch/switch.d.ts +3 -2
  56. package/src/m3/switch/switch.js +11 -10
  57. package/src/m3/tab/tabs-styles.css.js +1 -1
  58. package/src/m3/toolbar/toolbar-styles.css.js +1 -1
  59. package/src/win98/button/button-styles.css.js +1 -1
  60. package/src/m3/dialog/dialog-spring.d.ts +0 -45
  61. package/src/m3/dialog/dialog-spring.js +0 -176
  62. package/src/win98/button/radio.d.ts +0 -3
  63. package/src/win98/button/radio.js +0 -9
  64. package/src/win98/radio/radio.d.ts +0 -3
  65. package/src/win98/radio/radio.js +0 -9
package/README.md CHANGED
@@ -37,8 +37,9 @@ Check out [Overview](./docs/00-Overview.md).
37
37
 
38
38
  ## Documentations
39
39
 
40
+ - [Documentations](./docs/00-Overview.md) (Markdown)
41
+ - [lucaz.srht.site](https://lucaz.srht.site/) (components rendered)
40
42
  - Previous [documentations and demos](https://tideover.cc/seele/docs/), being replaced
41
- - [Documentations](./docs/00-Overview.md)
42
43
 
43
44
  ## Download
44
45
 
@@ -50,25 +51,34 @@ Check out [Overview](./docs/00-Overview.md).
50
51
 
51
52
  seele relies on the following modern web standards:
52
53
 
53
- - [`ElementInternals`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals), Baseline 2023
54
- - [`:dir()`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:dir), Baseline 2023, C 120, F 49
55
- - [Constructable Stylesheets](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/CSSStyleSheet), Baseline 2024, C[^1] 90, F 126
56
- - [`:state()`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:state), Baseline 2024, C 125, F 126
57
- - [`ariaActiveDescendantElement`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/ariaActiveDescendantElement) and other similar ones, Baseline 2025, C 135, F 136
54
+ | feature | baseline | Chromium | Firefox | Safari |
55
+ | --- | :---: | :---: | :---: | :---: |
56
+ | [`ElementInternals`][element-internals] | 2023 | - | - | - |
57
+ | [`:dir()`][:dir] | 2023 | 120 | - | - |
58
+ | [Constructable Stylesheets][constructable-stylesheets] | 2024 | - | 126 | - |
59
+ | [`:state()`][:state] | 2024 | 125 | 126 | - |
60
+ | [`aria*Element`][aria-active] and similar | 2025 | 135 | 136 | 16.4 |
61
+ | [`CSSNumericValue`][css-numeric-value] | not yet | - | Nightly | - |
58
62
 
59
- And all that result in:
63
+ _* `CSSNumericValue` is worked around currently so it does not raise the minimum
64
+ version of Firefox_
65
+
66
+ All of the above result in the following minimum requirements:
60
67
 
61
68
  - Chromium: >= 135
62
69
  - Firefox: >= 136
70
+ - Safari: >= 16.4
63
71
 
64
- It is 2026 now, you don't really need to worry about this. But in the future,
65
- these following web features might be used and require higher browser versions:
72
+ It is 2026 now, so you don't really need to worry about this. However, in the
73
+ future, the following web features might be used and require higher browser
74
+ versions:
66
75
 
67
- - [anchor()](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/anchor), Baseline 2026, C 125, F 147, will remove the dependency `floating-dom`
68
- - [`::view-*`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::view-transition), Baseline 2025, C 111, F 144, will optimize some animations for menu, dialog, etc
69
- - [`CSSNumericValue`](https://developer.mozilla.org/en-US/docs/Web/API/CSSNumericValue), not Baseline, C 66, F Nightly, worked around currently
76
+ - [Popover API][popover-api] — Baseline 2025, Safari 17, _will improve menu, popup, and combobox_
77
+ - [`@starting-style`][starting-style] — Baseline 2025, Safari 17.5
78
+ - [`anchor()`][anchor] — Baseline 2026, Firefox 147, Safari 26, _will remove the dependency `floating-dom`_
70
79
 
71
- [^1]: C = Chromium, F = Firefox.
80
+ _* omitting a specific browser version means using that standard does not raise
81
+ the minimum required version of seele_
72
82
 
73
83
  ## Other Information
74
84
 
@@ -79,3 +89,12 @@ these following web features might be used and require higher browser versions:
79
89
  [web-comps]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components
80
90
  [apg-patterns]: https://www.w3.org/WAI/ARIA/apg/patterns/
81
91
  [lit]: https://lit.dev/
92
+ [element-internals]: https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals
93
+ [:dir]: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:dir
94
+ [constructable-stylesheets]: https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/CSSStyleSheet
95
+ [:state]: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:state
96
+ [aria-active]: https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/ariaActiveDescendantElement
97
+ [css-numeric-value]: https://developer.mozilla.org/en-US/docs/Web/API/CSSNumericValue
98
+ [popover-api]: https://developer.mozilla.org/en-US/docs/Web/API/Popover_API
99
+ [starting-style]: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@starting-style
100
+ [anchor]: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/anchor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vollowx/seele",
3
- "version": "0.13.6",
3
+ "version": "0.13.8",
4
4
  "description": "A extensible Web Components library, with Material Design 3 components pre-provided.",
5
5
  "author": "vollowx",
6
6
  "license": "Apache-2.0",
package/src/all.d.ts CHANGED
@@ -32,3 +32,5 @@ import './m3/text-field/outlined-text-field.js';
32
32
  import './m3/toolbar/toolbar.js';
33
33
  import './m3/tooltip/tooltip.js';
34
34
  import './win98/button/button.js';
35
+ import './base/experimental/popup.js';
36
+ import './m3/experimental/popup.js';
package/src/all.js CHANGED
@@ -32,3 +32,5 @@ import './m3/text-field/outlined-text-field.js';
32
32
  import './m3/toolbar/toolbar.js';
33
33
  import './m3/tooltip/tooltip.js';
34
34
  import './win98/button/button.js';
35
+ import './base/experimental/popup.js';
36
+ import './m3/experimental/popup.js';
@@ -5,6 +5,7 @@ declare const Base: Constructor<import("./mixins/focus-delegated.js").FocusDeleg
5
5
  type AutocompleteMode = 'none' | 'list' | 'both';
6
6
  /**
7
7
  * TODO: Check if manually dispatching input/change events on input is necessary
8
+ * TODO: handle slotchange
8
9
  */
9
10
  export declare class Autocomplete extends Base {
10
11
  open: boolean;
@@ -1,11 +1,13 @@
1
1
  import { __decorate } from "tslib";
2
2
  import { LitElement, html } from 'lit';
3
3
  import { property, query, queryAssignedElements } from 'lit/decorators.js';
4
+ import { ensureReady } from '../core/ensure-ready.js';
4
5
  import { InternalsAttached } from './mixins/internals-attached.js';
5
6
  import { FocusDelegated } from './mixins/focus-delegated.js';
6
7
  const Base = FocusDelegated(InternalsAttached(LitElement));
7
8
  /**
8
9
  * TODO: Check if manually dispatching input/change events on input is necessary
10
+ * TODO: handle slotchange
9
11
  */
10
12
  export class Autocomplete extends Base {
11
13
  constructor() {
@@ -52,12 +54,11 @@ export class Autocomplete extends Base {
52
54
  renderMenu() {
53
55
  return html ``;
54
56
  }
57
+ // TODO: handle multiple calls on this function, currently double-call
58
+ // disallowed
55
59
  handleInputSlotChange() {
56
- const input = this.$input;
57
- if (!input)
58
- return;
59
- const $realInput = this.$input.$inputOrTextarea;
60
- if ($realInput) {
60
+ ensureReady(this.$input).then((input) => {
61
+ const $realInput = input.$inputOrTextarea;
61
62
  $realInput.role = 'combobox';
62
63
  $realInput.ariaExpanded = String(this.open);
63
64
  $realInput.ariaHasPopup = 'listbox';
@@ -67,7 +68,7 @@ export class Autocomplete extends Base {
67
68
  input.addEventListener('keydown', this.handleInputKeydown.bind(this));
68
69
  input.addEventListener('click', () => (this.open = !this.open));
69
70
  this.$menu.attach($realInput);
70
- }
71
+ });
71
72
  }
72
73
  handleItemsSlotChange() {
73
74
  // Initial filter based on current input value (if any)
@@ -121,12 +122,6 @@ export class Autocomplete extends Base {
121
122
  }
122
123
  }
123
124
  handleMenuSelect(event) {
124
- // FIXME: pressing <Enter> in a 'both' mode autocomplete triggers this
125
- // function, which overrides the current value in the text field
126
- // since the selected value in the menu is not the same as the
127
- // one in autocomplete
128
- //
129
- // if (this.mode === 'list') {
130
125
  const selectedItem = event.detail.item;
131
126
  const newValue = selectedItem.getAttribute('value') ||
132
127
  selectedItem.textContent?.trim() ||
@@ -134,7 +129,6 @@ export class Autocomplete extends Base {
134
129
  if (this.$input) {
135
130
  this.$input.value = newValue;
136
131
  }
137
- // }
138
132
  if (!this.keepOpenSelect) {
139
133
  this.open = false;
140
134
  }
@@ -1,4 +1,5 @@
1
1
  import { LitElement } from 'lit';
2
+ export declare const updateInternals: unique symbol;
2
3
  declare const Base: Constructor<import("./mixins/form-associated.js").FormAssociatedInterface> & Constructor<import("./mixins/internals-attached.js").InternalsAttachedInterface> & typeof LitElement;
3
4
  export declare class Button extends Base {
4
5
  #private;
@@ -7,7 +8,8 @@ export declare class Button extends Base {
7
8
  constructor();
8
9
  connectedCallback(): void;
9
10
  disconnectedCallback(): void;
10
- protected updated(changed: Map<string, any>): void;
11
- protected updateState(): void;
11
+ updated(changed: Map<string, any>): void;
12
+ [updateInternals](): void;
13
+ _handleClick(e: Event): void;
12
14
  }
13
15
  export {};
@@ -4,6 +4,7 @@ import { property } from 'lit/decorators.js';
4
4
  import { InternalsAttached, internals } from './mixins/internals-attached.js';
5
5
  import { FormAssociated } from './mixins/form-associated.js';
6
6
  import { hiddenStyles } from './hidden-styles.css.js';
7
+ export const updateInternals = Symbol('updateInternals');
7
8
  const Base = FormAssociated(InternalsAttached(LitElement));
8
9
  export class Button extends Base {
9
10
  static { this.styles = [hiddenStyles]; }
@@ -17,20 +18,21 @@ export class Button extends Base {
17
18
  e.stopPropagation();
18
19
  if (e.key === 'Enter')
19
20
  this.click();
21
+ else
22
+ this[internals].states.add('active');
23
+ // TODO: consider handle `pointerdown` and ditch native `:active` selector
24
+ // TODO: supporting `Enter` also requires listening on `focusout`
20
25
  };
21
26
  this.#handleKeyUp = (e) => {
22
27
  if (e.key === ' ') {
28
+ this[internals].states.delete('active');
23
29
  e.preventDefault();
24
30
  e.stopPropagation();
25
31
  this.click();
26
32
  }
27
33
  };
28
- this.#handleClick = () => {
29
- if (this.type !== 'button')
30
- this[internals].form?.[this.type]();
31
- };
32
34
  this[internals].role = 'button';
33
- this.updateState();
35
+ this[updateInternals]();
34
36
  // This is set to prevent a first-paint-time transition for styles like
35
37
  // `border-radius` on `:host`. Also in `ToggleButton`.
36
38
  // Double `requestAnimationFrame` is necessary since the callback inside is
@@ -41,26 +43,29 @@ export class Button extends Base {
41
43
  super.connectedCallback();
42
44
  this.addEventListener('keydown', this.#handleKeyDown);
43
45
  this.addEventListener('keyup', this.#handleKeyUp);
44
- this.addEventListener('click', this.#handleClick);
46
+ this.addEventListener('click', this._handleClick.bind(this));
45
47
  requestAnimationFrame(() => requestAnimationFrame(() => this.removeAttribute('notransition')));
46
48
  }
47
49
  disconnectedCallback() {
48
50
  super.disconnectedCallback();
49
51
  this.removeEventListener('keydown', this.#handleKeyDown);
50
52
  this.removeEventListener('keyup', this.#handleKeyUp);
51
- this.removeEventListener('click', this.#handleClick);
53
+ this.removeEventListener('click', this._handleClick.bind(this));
52
54
  }
53
55
  updated(changed) {
54
56
  if (changed.has('disabled'))
55
- this.updateState();
57
+ this[updateInternals]();
56
58
  }
57
- updateState() {
59
+ [updateInternals]() {
58
60
  this.tabIndex = this.disabled ? -1 : 0;
59
61
  this[internals].ariaDisabled = String(this.disabled);
60
62
  }
61
63
  #handleKeyDown;
62
64
  #handleKeyUp;
63
- #handleClick;
65
+ _handleClick(e) {
66
+ if (this.type !== 'button')
67
+ this[internals].form?.[this.type]();
68
+ }
64
69
  }
65
70
  __decorate([
66
71
  property({ reflect: true })
@@ -2,7 +2,7 @@ import { __decorate } from "tslib";
2
2
  import { LitElement } from 'lit';
3
3
  import { property } from 'lit/decorators.js';
4
4
  import { FormAssociated } from './mixins/form-associated.js';
5
- import { InternalsAttached, internals } from './mixins/internals-attached.js';
5
+ import { InternalsAttached, internals, replaceStates, } from './mixins/internals-attached.js';
6
6
  import { hiddenStyles } from './hidden-styles.css.js';
7
7
  const PROPERTY_FROM_ARIA_CHECKED = {
8
8
  true: 'checked',
@@ -63,9 +63,7 @@ export class Checkbox extends Base {
63
63
  updateState() {
64
64
  const prevAriaChecked = this[internals]
65
65
  .ariaChecked;
66
- this[internals].states.delete('was-unchecked');
67
- this[internals].states.delete('was-checked');
68
- this[internals].states.delete('was-indeterminate');
66
+ this[replaceStates](['was-unchecked', 'was-checked', 'was-indeterminate'], []);
69
67
  if (prevAriaChecked && PROPERTY_FROM_ARIA_CHECKED[prevAriaChecked]) {
70
68
  this[internals].states.add(`was-${PROPERTY_FROM_ARIA_CHECKED[prevAriaChecked]}`);
71
69
  }
@@ -76,10 +74,7 @@ export class Checkbox extends Base {
76
74
  : 'false';
77
75
  const currentAriaChecked = this[internals]
78
76
  .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]}`);
77
+ this[replaceStates](['unchecked', 'checked', 'indeterminate'], [PROPERTY_FROM_ARIA_CHECKED[currentAriaChecked]]);
83
78
  this.tabIndex = this.disabled ? -1 : 0;
84
79
  this[internals].ariaDisabled = String(this.disabled);
85
80
  this[internals].setFormValue(this.checked ? 'on' : null);
@@ -134,7 +134,7 @@ export class PopoverController {
134
134
  this.cleanupAutoUpdate?.();
135
135
  this.cleanupAutoUpdate = undefined;
136
136
  }
137
- reposition() {
137
+ async reposition() {
138
138
  const trigger = this.config.trigger();
139
139
  const popover = this.config.popover();
140
140
  if (!trigger || !popover)
@@ -0,0 +1,2 @@
1
+ import { css } from 'lit';
2
+ export const popupStyles = css`:host([notransition]){transition:none!important}:host{box-sizing:border-box;border:none;margin:0;padding:0;position:absolute;inset:auto;overflow:auto}`;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * NOTE: demos at dev/pending.html
3
+ * NOTE: should apply writing style on event listeners to other files
4
+ * TODO: consider removing popoverController, but in case of tooltip, reserve
5
+ * for now
6
+ * TODO: hijack all `hidePopover()`s to allow more complex JS animations
7
+ * TODO: simpler Menu that just inherits List - do we really need a separated
8
+ * Menu? Why not just `<md-list role="presentation">`?
9
+ *
10
+ * Design: TO BE ADDED
11
+ */
12
+ import { LitElement, PropertyValues } from 'lit';
13
+ import { Placement, Strategy } from '@floating-ui/dom';
14
+ export declare function lookForTabbable(root: Node): HTMLElement | null;
15
+ declare const Popup_base: Constructor<import("../mixins/attachable.js").AttachableInterface> & Constructor<import("../mixins/internals-attached.js").InternalsAttachedInterface> & typeof LitElement;
16
+ export declare class Popup extends Popup_base {
17
+ #private;
18
+ open: boolean;
19
+ noFocusControl: boolean;
20
+ placement: Placement;
21
+ strategy: Strategy;
22
+ offset: number;
23
+ windowPadding: number;
24
+ static styles: any[];
25
+ render(): import("lit-html").TemplateResult<1>;
26
+ constructor();
27
+ connectedCallback(): void;
28
+ disconnectedCallback(): void;
29
+ protected updated(changedProperties: PropertyValues<this>): void;
30
+ handleControlChange(prev: HTMLElement | null, next: HTMLElement | null): void;
31
+ toggle(): Promise<void>;
32
+ show(): Promise<void>;
33
+ hide(): Promise<void>;
34
+ _show(): Promise<void>;
35
+ _hide(): Promise<void>;
36
+ _cleanup(): void;
37
+ }
38
+ export {};
@@ -0,0 +1,276 @@
1
+ /**
2
+ * NOTE: demos at dev/pending.html
3
+ * NOTE: should apply writing style on event listeners to other files
4
+ * TODO: consider removing popoverController, but in case of tooltip, reserve
5
+ * for now
6
+ * TODO: hijack all `hidePopover()`s to allow more complex JS animations
7
+ * TODO: simpler Menu that just inherits List - do we really need a separated
8
+ * Menu? Why not just `<md-list role="presentation">`?
9
+ *
10
+ * Design: TO BE ADDED
11
+ */
12
+ import { __decorate } from "tslib";
13
+ import { LitElement, html, isServer } from 'lit';
14
+ import { customElement, property } from 'lit/decorators.js';
15
+ import { autoUpdate, computePosition, arrow, flip, offset, shift, } from '@floating-ui/dom';
16
+ import { InternalsAttached } from '../mixins/internals-attached.js';
17
+ import { Attachable } from '../mixins/attachable.js';
18
+ import { transformOriginFromArrow } from '../controllers/popover-controller.js';
19
+ import { popupStyles } from './popup-styles.css.js';
20
+ // Returns the first tabbable element within an eleent
21
+ //
22
+ // TODO: reverse search?
23
+ export function lookForTabbable(root) {
24
+ const FOCUSABLE_SELECTOR = [
25
+ 'a[href]',
26
+ 'button',
27
+ 'input',
28
+ 'textarea',
29
+ 'select',
30
+ 'summary',
31
+ '[tabindex]',
32
+ '[contenteditable=true]',
33
+ ].join(',');
34
+ const isTabbable = (el) => el.matches(FOCUSABLE_SELECTOR) &&
35
+ !el.hasAttribute('disabled') &&
36
+ el.getAttribute('tabindex') !== '-1';
37
+ if ('shadowRoot' in root && root.shadowRoot) {
38
+ const shadowResult = lookForTabbable(root.shadowRoot);
39
+ if (shadowResult)
40
+ return shadowResult;
41
+ }
42
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
43
+ acceptNode: (node) => {
44
+ const el = node;
45
+ return isTabbable(el) || el.shadowRoot
46
+ ? NodeFilter.FILTER_ACCEPT
47
+ : NodeFilter.FILTER_SKIP;
48
+ },
49
+ });
50
+ let currentNode = walker.nextNode();
51
+ while (currentNode) {
52
+ if (isTabbable(currentNode)) {
53
+ return currentNode;
54
+ }
55
+ if (currentNode.shadowRoot) {
56
+ const shadowFocusable = lookForTabbable(currentNode.shadowRoot);
57
+ if (shadowFocusable) {
58
+ return shadowFocusable;
59
+ }
60
+ }
61
+ currentNode = walker.nextNode();
62
+ }
63
+ return null;
64
+ }
65
+ let Popup = class Popup extends Attachable(InternalsAttached(LitElement)) {
66
+ static { this.styles = [popupStyles]; }
67
+ render() {
68
+ return html `<slot></slot>`;
69
+ }
70
+ constructor() {
71
+ super();
72
+ this.open = false;
73
+ this.noFocusControl = false;
74
+ this.placement = 'bottom-start';
75
+ this.strategy = 'absolute';
76
+ this.offset = 0;
77
+ this.windowPadding = 8;
78
+ this.#onRequestHide = () => {
79
+ this.hide();
80
+ };
81
+ // #allowHidePopover = false;
82
+ // #onBeforeToggle = (e: ToggleEvent) => {
83
+ // // Intercept browser light-dismiss (ESC, click outside) or external hidePopover() calls
84
+ // if (e.newState === 'closed' && !this.#allowHidePopover) {
85
+ // e.preventDefault();
86
+ // if (this.open) {
87
+ // this.hide();
88
+ // }
89
+ // }
90
+ // };
91
+ this.#onToggle = (e) => {
92
+ e.preventDefault();
93
+ if (e.newState === 'closed' && this.open)
94
+ this.hide();
95
+ };
96
+ this.#onFocusOut = (e) => {
97
+ // for `this` and `this.$control`
98
+ if (this.noFocusControl || !this.open)
99
+ return;
100
+ const target = e.relatedTarget;
101
+ if (target && (this.contains(target) || this.$control?.contains(target))) {
102
+ return;
103
+ }
104
+ if (target) {
105
+ this.hide();
106
+ return;
107
+ }
108
+ requestAnimationFrame(() => {
109
+ if (!document.hasFocus())
110
+ this.hide();
111
+ });
112
+ };
113
+ this.#onTriggerClick = (e) => {
114
+ e.preventDefault();
115
+ this.toggle();
116
+ };
117
+ this.#dummyArrow = isServer ? null : document.createElement('div');
118
+ if (!this.hasAttribute('popover'))
119
+ this.setAttribute('popover', 'auto');
120
+ if (!isServer) {
121
+ this.addEventListener('request-popup-hide', this.#onRequestHide);
122
+ // this.addEventListener('beforetoggle', this.#onBeforeToggle);
123
+ this.addEventListener('toggle', this.#onToggle);
124
+ this.addEventListener('focusout', this.#onFocusOut);
125
+ }
126
+ this.setAttribute('notransition', '');
127
+ }
128
+ connectedCallback() {
129
+ super.connectedCallback();
130
+ requestAnimationFrame(() => requestAnimationFrame(() => this.removeAttribute('notransition')));
131
+ }
132
+ disconnectedCallback() {
133
+ this._cleanup();
134
+ super.disconnectedCallback();
135
+ }
136
+ updated(changedProperties) {
137
+ if (changedProperties.has('open')) {
138
+ if (this.open)
139
+ this._show();
140
+ else
141
+ this._hide();
142
+ this.#syncTriggerAria();
143
+ }
144
+ }
145
+ handleControlChange(prev, next) {
146
+ if (prev) {
147
+ prev.removeEventListener('click', this.#onTriggerClick);
148
+ prev.removeEventListener('focusout', this.#onFocusOut);
149
+ }
150
+ if (next) {
151
+ next.addEventListener('click', this.#onTriggerClick);
152
+ next.addEventListener('focusout', this.#onFocusOut);
153
+ next.ariaHasPopup = 'true';
154
+ next.ariaExpanded = String(this.open);
155
+ next.ariaControlsElements = [this];
156
+ }
157
+ }
158
+ #onRequestHide;
159
+ // #allowHidePopover = false;
160
+ // #onBeforeToggle = (e: ToggleEvent) => {
161
+ // // Intercept browser light-dismiss (ESC, click outside) or external hidePopover() calls
162
+ // if (e.newState === 'closed' && !this.#allowHidePopover) {
163
+ // e.preventDefault();
164
+ // if (this.open) {
165
+ // this.hide();
166
+ // }
167
+ // }
168
+ // };
169
+ #onToggle;
170
+ #onFocusOut;
171
+ #onTriggerClick;
172
+ #syncTriggerAria() {
173
+ this.$control?.setAttribute('aria-expanded', String(this.open));
174
+ }
175
+ #focusFirstInteractiveElement() {
176
+ if (this.noFocusControl)
177
+ return;
178
+ const autoFocusEl = this.querySelector('[autofocus]');
179
+ if (autoFocusEl) {
180
+ autoFocusEl.focus();
181
+ return;
182
+ }
183
+ const firstFocusable = lookForTabbable(this);
184
+ if (firstFocusable) {
185
+ firstFocusable.focus();
186
+ }
187
+ }
188
+ async toggle() {
189
+ this.open = !this.open;
190
+ }
191
+ async show() {
192
+ this.open = true;
193
+ }
194
+ async hide() {
195
+ this.open = false;
196
+ }
197
+ async _show() {
198
+ if (this.isConnected && !this.matches(':popover-open'))
199
+ this.showPopover();
200
+ const trigger = this.$control;
201
+ if (trigger) {
202
+ this._cleanup();
203
+ this.#cleanupAutoUpdate = autoUpdate(trigger, this, () => this.#reposition());
204
+ await this.#reposition();
205
+ }
206
+ // await this._showing();
207
+ requestAnimationFrame(() => {
208
+ if (this.open)
209
+ this.#focusFirstInteractiveElement();
210
+ });
211
+ }
212
+ async _hide() {
213
+ // await this._hiding();
214
+ this._cleanup();
215
+ if (this.matches(':popover-open'))
216
+ this.hidePopover();
217
+ // if (this.matches(':popover-open')) {
218
+ // this.#allowHidePopover = true;
219
+ // try {
220
+ // this.hidePopover();
221
+ // } finally {
222
+ // this.#allowHidePopover = false;
223
+ // }
224
+ // }
225
+ }
226
+ // async _showing(): Promise<void> {}
227
+ // async _hiding(): Promise<void> {}
228
+ #cleanupAutoUpdate;
229
+ #dummyArrow;
230
+ async #reposition() {
231
+ const trigger = this.$control;
232
+ if (!trigger)
233
+ return;
234
+ const { x, y, placement, middlewareData } = await computePosition(trigger, this, {
235
+ placement: this.placement,
236
+ strategy: this.strategy,
237
+ middleware: [
238
+ offset(this.offset),
239
+ flip({ padding: this.windowPadding }),
240
+ shift({ padding: this.windowPadding, crossAxis: true }),
241
+ arrow({ element: this.#dummyArrow }),
242
+ ],
243
+ });
244
+ Object.assign(this.style, {
245
+ left: `${x}px`,
246
+ top: `${y}px`,
247
+ transformOrigin: transformOriginFromArrow(placement, middlewareData.arrow),
248
+ });
249
+ }
250
+ _cleanup() {
251
+ this.#cleanupAutoUpdate?.();
252
+ this.#cleanupAutoUpdate = undefined;
253
+ }
254
+ };
255
+ __decorate([
256
+ property({ type: Boolean, reflect: true })
257
+ ], Popup.prototype, "open", void 0);
258
+ __decorate([
259
+ property({ type: Boolean, attribute: 'no-focus-control' })
260
+ ], Popup.prototype, "noFocusControl", void 0);
261
+ __decorate([
262
+ property({ type: String })
263
+ ], Popup.prototype, "placement", void 0);
264
+ __decorate([
265
+ property({ type: String })
266
+ ], Popup.prototype, "strategy", void 0);
267
+ __decorate([
268
+ property({ type: Number })
269
+ ], Popup.prototype, "offset", void 0);
270
+ __decorate([
271
+ property({ type: Number })
272
+ ], Popup.prototype, "windowPadding", void 0);
273
+ Popup = __decorate([
274
+ customElement('complementary-popup')
275
+ ], Popup);
276
+ export { Popup };
@@ -19,6 +19,8 @@ export type MenuItemFocusEvent = CustomEvent<ItemFocusDetail>;
19
19
  * @fires {Event} close - Fires when the menu is closed.
20
20
  * @fires {MenuSelectEvent} select - Fires when an item is selected.
21
21
  * @fires {MenuItemFocusEvent} item-focus - Fires when an item is focused
22
+ *
23
+ * TODO: handle slotchange
22
24
  */
23
25
  export declare class Menu extends Base {
24
26
  #private;
package/src/base/menu.js CHANGED
@@ -16,6 +16,8 @@ const Base = FocusDelegated(InternalsAttached(Attachable(LitElement)));
16
16
  * @fires {Event} close - Fires when the menu is closed.
17
17
  * @fires {MenuSelectEvent} select - Fires when an item is selected.
18
18
  * @fires {MenuItemFocusEvent} item-focus - Fires when an item is focused
19
+ *
20
+ * TODO: handle slotchange
19
21
  */
20
22
  export class Menu extends Base {
21
23
  constructor() {
@@ -0,0 +1,7 @@
1
+ import { Button } from '../button.js';
2
+ export declare class ToggleableInterface {
3
+ checked: boolean;
4
+ _ignoreClick: boolean;
5
+ _toggle(): void;
6
+ }
7
+ export declare const ButtonToggleMixin: <T extends Constructor<Button>>(superClass: T) => Constructor<ToggleableInterface> & T;