@limetech/lime-elements 40.4.1 → 40.4.2

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/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## [40.4.2](https://github.com/Lundalogik/lime-elements/compare/v40.4.1...v40.4.2) (2026-09-18)
2
+
3
+ ### Bug Fixes
4
+
5
+
6
+ * **menu:** leave a trigger the consumer disabled alone ([561181d](https://github.com/Lundalogik/lime-elements/commit/561181d0913e05858ec76401570d4702d45aca47))
7
+ * **menu:** say the trigger is collapsed, not just that it is open ([99639b4](https://github.com/Lundalogik/lime-elements/commit/99639b4cc5b2ae79ddd9f5ad4e7b853b721f9a20))
8
+ * **menu:** stop arming the document listener on a detached menu ([c4ca168](https://github.com/Lundalogik/lime-elements/commit/c4ca168fadefaccb23204c57aa8ade1970f60693))
9
+ * **popover:** say the trigger is collapsed, not just that it is open ([cb74fdd](https://github.com/Lundalogik/lime-elements/commit/cb74fddc9fb9623e72644d2cbe703ad963ab4bc2))
10
+ * **popover:** take the document listeners down again ([72f6348](https://github.com/Lundalogik/lime-elements/commit/72f6348874c5b77f87d421dbd8cb207a39ce5842))
11
+
1
12
  ## [40.4.1](https://github.com/Lundalogik/lime-elements/compare/v40.4.0...v40.4.1) (2026-09-18)
2
13
 
3
14
  ### Bug Fixes
@@ -2698,6 +2698,7 @@ const Menu = class {
2698
2698
  this.keepOpenOnSelect = false;
2699
2699
  this.shouldRestoreFocusOnClose = false;
2700
2700
  this.normalizedHotkeyCache = new Map();
2701
+ this.triggersDisabledByMenu = new WeakSet();
2701
2702
  this.cachedSubMenuSource = null;
2702
2703
  this.cachedSubMenuItems = null;
2703
2704
  this.handleDocumentKeyDown = (event) => {
@@ -2977,20 +2978,32 @@ const Menu = class {
2977
2978
  this.handleSelect(parent);
2978
2979
  };
2979
2980
  this.setTriggerAttributes = (element) => {
2980
- const attributes = {
2981
- 'aria-haspopup': true,
2982
- 'aria-expanded': this.open,
2983
- 'aria-controls': this.portalId,
2984
- disabled: this.disabled,
2985
- role: 'button',
2986
- };
2987
- for (const [key, value] of Object.entries(attributes)) {
2988
- if (value) {
2989
- element.setAttribute(key, String(value));
2990
- }
2991
- else {
2992
- element.removeAttribute(key);
2981
+ element.setAttribute('aria-haspopup', 'true');
2982
+ element.setAttribute('aria-controls', this.portalId);
2983
+ element.setAttribute('role', 'button');
2984
+ // `aria-expanded` is needed even when the menu is closed. Without
2985
+ // it, the trigger reads as an ordinary button to a screen reader.
2986
+ element.setAttribute('aria-expanded', String(this.open));
2987
+ this.setTriggerDisabled(element);
2988
+ };
2989
+ /**
2990
+ * `disabled` has to be removed rather than set to `false`, because the
2991
+ * presence of the attribute is what disables the element. Only the one the
2992
+ * menu put there may go: a consumer can disable their own trigger, and
2993
+ * stripping that attribute would re-enable it on every render.
2994
+ * @param element - the slotted trigger element
2995
+ */
2996
+ this.setTriggerDisabled = (element) => {
2997
+ if (this.disabled) {
2998
+ if (!element.hasAttribute('disabled')) {
2999
+ element.setAttribute('disabled', 'true');
3000
+ this.triggersDisabledByMenu.add(element);
2993
3001
  }
3002
+ return;
3003
+ }
3004
+ if (this.triggersDisabledByMenu.has(element)) {
3005
+ element.removeAttribute('disabled');
3006
+ this.triggersDisabledByMenu.delete(element);
2994
3007
  }
2995
3008
  };
2996
3009
  this.onClose = () => {
@@ -3193,7 +3206,7 @@ const Menu = class {
3193
3206
  const cssProperties = this.getCssProperties();
3194
3207
  const dropdownZIndex = getComputedStyle(this.host).getPropertyValue('--dropdown-z-index');
3195
3208
  const menuSurfaceWidth = this.getMenuSurfaceWidth(cssProperties['--menu-surface-width']);
3196
- return (index.h("div", { key: '60e7d159ae0a65582f3184b6a6bfe6c252fe7850', class: "mdc-menu-surface--anchor", onClick: this.onTriggerClick }, index.h("slot", { key: 'f492115bc3b7115964814883bc103a00496d0178', ref: this.setTriggerRef, name: "trigger" }), this.renderNotificationBadge(), index.h("limel-portal", { key: 'a38e4fbab9a03fb34b86030d2782a2215ed72650', visible: this.open, containerId: this.portalId, openDirection: this.openDirection, position: "absolute", containerStyle: { 'z-index': dropdownZIndex } }, index.h("limel-menu-surface", { key: 'dd95ff8c4fc87135cdd00cf5174cef72af61c31d', open: this.open, onDismiss: this.onClose, style: Object.assign(Object.assign({}, cssProperties), { '--menu-surface-width': menuSurfaceWidth, '--limel-menu-surface-display': 'flex', '--limel-menu-surface-flex-direction': 'column' }), class: {
3209
+ return (index.h("div", { key: 'c66307a5cc7257e2b394f0b5d56d6fbc6fce18b1', class: "mdc-menu-surface--anchor", onClick: this.onTriggerClick }, index.h("slot", { key: '7c37448e29deaee202f74d3288d21697529c82b5', ref: this.setTriggerRef, name: "trigger" }), this.renderNotificationBadge(), index.h("limel-portal", { key: 'f7f1b14ae5c6e19fa8e9651c2a847db0bb246427', visible: this.open, containerId: this.portalId, openDirection: this.openDirection, position: "absolute", containerStyle: { 'z-index': dropdownZIndex } }, index.h("limel-menu-surface", { key: 'c2fd7257bfe8ccf820b8aabc6a8959f6d5d1cd2f', open: this.open, onDismiss: this.onClose, style: Object.assign(Object.assign({}, cssProperties), { '--menu-surface-width': menuSurfaceWidth, '--limel-menu-surface-display': 'flex', '--limel-menu-surface-flex-direction': 'column' }), class: {
3197
3210
  'has-grid-layout': this.gridLayout,
3198
3211
  } }, this.renderSearchField(), this.renderBreadcrumb(), this.renderLoader(), this.renderEmptyMessage(), this.renderMenuList()))));
3199
3212
  }
@@ -3203,24 +3216,33 @@ const Menu = class {
3203
3216
  this.setFocus();
3204
3217
  }
3205
3218
  connectedCallback() {
3206
- if (this.open) {
3207
- document.addEventListener('keydown', this.handleDocumentKeyDown, true);
3208
- }
3219
+ this.setupGlobalHandlers();
3209
3220
  }
3210
3221
  disconnectedCallback() {
3211
- document.removeEventListener('keydown', this.handleDocumentKeyDown, true);
3222
+ this.teardownGlobalHandlers();
3212
3223
  }
3213
3224
  openWatcher(newValue) {
3214
- const opened = newValue;
3215
- if (opened) {
3216
- document.addEventListener('keydown', this.handleDocumentKeyDown, true);
3225
+ this.setupGlobalHandlers();
3226
+ if (newValue) {
3217
3227
  this.setFocus();
3218
3228
  }
3219
3229
  else {
3220
- document.removeEventListener('keydown', this.handleDocumentKeyDown, true);
3221
3230
  this.clearSearch();
3222
3231
  }
3223
3232
  }
3233
+ setupGlobalHandlers() {
3234
+ // Stencil keeps firing `@Watch` on a detached instance, so `open` can
3235
+ // still flip after `disconnectedCallback` has run. Listening then
3236
+ // would leave a listener behind that no disconnect takes down.
3237
+ if (!this.open || !this.host.isConnected) {
3238
+ this.teardownGlobalHandlers();
3239
+ return;
3240
+ }
3241
+ document.addEventListener('keydown', this.handleDocumentKeyDown, true);
3242
+ }
3243
+ teardownGlobalHandlers() {
3244
+ document.removeEventListener('keydown', this.handleDocumentKeyDown, true);
3245
+ }
3224
3246
  isFromTextInput(event) {
3225
3247
  const path = typeof event.composedPath === 'function'
3226
3248
  ? event.composedPath()
@@ -74,20 +74,12 @@ const Popover = class {
74
74
  focusTriggerElement.focusTriggerElement(trigger);
75
75
  };
76
76
  this.setTriggerAttributes = (element) => {
77
- const attributes = {
78
- 'aria-haspopup': true,
79
- 'aria-expanded': this.open,
80
- 'aria-controls': this.portalId,
81
- role: 'button',
82
- };
83
- for (const [key, value] of Object.entries(attributes)) {
84
- if (value) {
85
- element.setAttribute(key, String(value));
86
- }
87
- else {
88
- element.removeAttribute(key);
89
- }
90
- }
77
+ element.setAttribute('aria-haspopup', 'true');
78
+ // `aria-expanded` is needed even when the popover is closed. Without
79
+ // it, the trigger reads as an ordinary button to a screen reader.
80
+ element.setAttribute('aria-expanded', String(this.open));
81
+ element.setAttribute('aria-controls', this.portalId);
82
+ element.setAttribute('role', 'button');
91
83
  };
92
84
  this.portalId = randomString.createRandomString();
93
85
  this.globalClickListener = this.globalClickListener.bind(this);
@@ -99,9 +91,12 @@ const Popover = class {
99
91
  setTimeout(this.focusTrigger, 0);
100
92
  }
101
93
  }
102
- componentWillLoad() {
94
+ connectedCallback() {
103
95
  this.setupGlobalHandlers();
104
96
  }
97
+ disconnectedCallback() {
98
+ this.teardownGlobalHandlers();
99
+ }
105
100
  componentDidRender() {
106
101
  if (!this.triggerSlot) {
107
102
  return;
@@ -110,21 +105,34 @@ const Popover = class {
110
105
  this.triggerSlot.assignedElements().forEach(this.setTriggerAttributes);
111
106
  }
112
107
  setupGlobalHandlers() {
113
- if (this.open) {
114
- document.addEventListener('click', this.globalClickListener, {
115
- capture: true,
116
- });
117
- document.addEventListener('keydown', this.handleGlobalKeyPress);
118
- }
119
- else {
120
- document.removeEventListener('click', this.globalClickListener);
121
- document.removeEventListener('keydown', this.handleGlobalKeyPress);
108
+ // Stencil keeps firing `@Watch` on a detached instance, so `open` can
109
+ // still flip after `disconnectedCallback` has run. Listening then
110
+ // would swallow every click on the page, with no disconnect left to
111
+ // undo it.
112
+ if (!this.open || !this.host.isConnected) {
113
+ this.teardownGlobalHandlers();
114
+ return;
122
115
  }
116
+ document.addEventListener('click', this.globalClickListener, {
117
+ capture: true,
118
+ });
119
+ document.addEventListener('keydown', this.handleGlobalKeyPress);
120
+ }
121
+ /**
122
+ * The capture flag has to be given again: `removeEventListener` only
123
+ * matches a listener registered with the same one, so leaving it out here
124
+ * left the click listener on `document` for good.
125
+ */
126
+ teardownGlobalHandlers() {
127
+ document.removeEventListener('click', this.globalClickListener, {
128
+ capture: true,
129
+ });
130
+ document.removeEventListener('keydown', this.handleGlobalKeyPress);
123
131
  }
124
132
  render() {
125
133
  const cssProperties = this.getCssProperties();
126
134
  const popoverZIndex = getComputedStyle(this.host).getPropertyValue('--popover-z-index');
127
- return (index.h("div", { key: '096e54a6fdc88f6571884bf0a642c67f144d3bbf', class: "trigger-anchor" }, index.h("slot", { key: 'ddc46b8755f6463dc2320198aefc50ef759f9de4', name: "trigger", ref: this.setTriggerRef }), index.h("limel-portal", { key: '89cce45b2218a2a5087f30f4d524d2c1b2041de2', visible: this.open, containerId: this.portalId, containerStyle: { 'z-index': popoverZIndex }, openDirection: this.openDirection }, index.h("limel-popover-surface", { key: 'a1d1edfb24fdbef9109adef03f8d77e6cced87c7', contentCollection: this.host.children, style: cssProperties }))));
135
+ return (index.h("div", { key: 'd34ccb8ce24c315432db83e6e1d998c4d230255f', class: "trigger-anchor" }, index.h("slot", { key: 'f2ab0bc064a439cecfee0fc75cb160cde227e018', name: "trigger", ref: this.setTriggerRef }), index.h("limel-portal", { key: '047d3643c80c445c8b97f1c30a6b7a8c236beaa9', visible: this.open, containerId: this.portalId, containerStyle: { 'z-index': popoverZIndex }, openDirection: this.openDirection }, index.h("limel-popover-surface", { key: '91c6f19807aeda2d37652bce452f6bd3cd6bcc9f', contentCollection: this.host.children, style: cssProperties }))));
128
136
  }
129
137
  globalClickListener(event) {
130
138
  const element = event.target;
@@ -84,6 +84,7 @@ export class Menu {
84
84
  this.keepOpenOnSelect = false;
85
85
  this.shouldRestoreFocusOnClose = false;
86
86
  this.normalizedHotkeyCache = new Map();
87
+ this.triggersDisabledByMenu = new WeakSet();
87
88
  this.cachedSubMenuSource = null;
88
89
  this.cachedSubMenuItems = null;
89
90
  this.handleDocumentKeyDown = (event) => {
@@ -363,20 +364,32 @@ export class Menu {
363
364
  this.handleSelect(parent);
364
365
  };
365
366
  this.setTriggerAttributes = (element) => {
366
- const attributes = {
367
- 'aria-haspopup': true,
368
- 'aria-expanded': this.open,
369
- 'aria-controls': this.portalId,
370
- disabled: this.disabled,
371
- role: 'button',
372
- };
373
- for (const [key, value] of Object.entries(attributes)) {
374
- if (value) {
375
- element.setAttribute(key, String(value));
376
- }
377
- else {
378
- element.removeAttribute(key);
367
+ element.setAttribute('aria-haspopup', 'true');
368
+ element.setAttribute('aria-controls', this.portalId);
369
+ element.setAttribute('role', 'button');
370
+ // `aria-expanded` is needed even when the menu is closed. Without
371
+ // it, the trigger reads as an ordinary button to a screen reader.
372
+ element.setAttribute('aria-expanded', String(this.open));
373
+ this.setTriggerDisabled(element);
374
+ };
375
+ /**
376
+ * `disabled` has to be removed rather than set to `false`, because the
377
+ * presence of the attribute is what disables the element. Only the one the
378
+ * menu put there may go: a consumer can disable their own trigger, and
379
+ * stripping that attribute would re-enable it on every render.
380
+ * @param element - the slotted trigger element
381
+ */
382
+ this.setTriggerDisabled = (element) => {
383
+ if (this.disabled) {
384
+ if (!element.hasAttribute('disabled')) {
385
+ element.setAttribute('disabled', 'true');
386
+ this.triggersDisabledByMenu.add(element);
379
387
  }
388
+ return;
389
+ }
390
+ if (this.triggersDisabledByMenu.has(element)) {
391
+ element.removeAttribute('disabled');
392
+ this.triggersDisabledByMenu.delete(element);
380
393
  }
381
394
  };
382
395
  this.onClose = () => {
@@ -579,7 +592,7 @@ export class Menu {
579
592
  const cssProperties = this.getCssProperties();
580
593
  const dropdownZIndex = getComputedStyle(this.host).getPropertyValue('--dropdown-z-index');
581
594
  const menuSurfaceWidth = this.getMenuSurfaceWidth(cssProperties['--menu-surface-width']);
582
- return (h("div", { key: '60e7d159ae0a65582f3184b6a6bfe6c252fe7850', class: "mdc-menu-surface--anchor", onClick: this.onTriggerClick }, h("slot", { key: 'f492115bc3b7115964814883bc103a00496d0178', ref: this.setTriggerRef, name: "trigger" }), this.renderNotificationBadge(), h("limel-portal", { key: 'a38e4fbab9a03fb34b86030d2782a2215ed72650', visible: this.open, containerId: this.portalId, openDirection: this.openDirection, position: "absolute", containerStyle: { 'z-index': dropdownZIndex } }, h("limel-menu-surface", { key: 'dd95ff8c4fc87135cdd00cf5174cef72af61c31d', open: this.open, onDismiss: this.onClose, style: Object.assign(Object.assign({}, cssProperties), { '--menu-surface-width': menuSurfaceWidth, '--limel-menu-surface-display': 'flex', '--limel-menu-surface-flex-direction': 'column' }), class: {
595
+ return (h("div", { key: 'c66307a5cc7257e2b394f0b5d56d6fbc6fce18b1', class: "mdc-menu-surface--anchor", onClick: this.onTriggerClick }, h("slot", { key: '7c37448e29deaee202f74d3288d21697529c82b5', ref: this.setTriggerRef, name: "trigger" }), this.renderNotificationBadge(), h("limel-portal", { key: 'f7f1b14ae5c6e19fa8e9651c2a847db0bb246427', visible: this.open, containerId: this.portalId, openDirection: this.openDirection, position: "absolute", containerStyle: { 'z-index': dropdownZIndex } }, h("limel-menu-surface", { key: 'c2fd7257bfe8ccf820b8aabc6a8959f6d5d1cd2f', open: this.open, onDismiss: this.onClose, style: Object.assign(Object.assign({}, cssProperties), { '--menu-surface-width': menuSurfaceWidth, '--limel-menu-surface-display': 'flex', '--limel-menu-surface-flex-direction': 'column' }), class: {
583
596
  'has-grid-layout': this.gridLayout,
584
597
  } }, this.renderSearchField(), this.renderBreadcrumb(), this.renderLoader(), this.renderEmptyMessage(), this.renderMenuList()))));
585
598
  }
@@ -589,24 +602,33 @@ export class Menu {
589
602
  this.setFocus();
590
603
  }
591
604
  connectedCallback() {
592
- if (this.open) {
593
- document.addEventListener('keydown', this.handleDocumentKeyDown, true);
594
- }
605
+ this.setupGlobalHandlers();
595
606
  }
596
607
  disconnectedCallback() {
597
- document.removeEventListener('keydown', this.handleDocumentKeyDown, true);
608
+ this.teardownGlobalHandlers();
598
609
  }
599
610
  openWatcher(newValue) {
600
- const opened = newValue;
601
- if (opened) {
602
- document.addEventListener('keydown', this.handleDocumentKeyDown, true);
611
+ this.setupGlobalHandlers();
612
+ if (newValue) {
603
613
  this.setFocus();
604
614
  }
605
615
  else {
606
- document.removeEventListener('keydown', this.handleDocumentKeyDown, true);
607
616
  this.clearSearch();
608
617
  }
609
618
  }
619
+ setupGlobalHandlers() {
620
+ // Stencil keeps firing `@Watch` on a detached instance, so `open` can
621
+ // still flip after `disconnectedCallback` has run. Listening then
622
+ // would leave a listener behind that no disconnect takes down.
623
+ if (!this.open || !this.host.isConnected) {
624
+ this.teardownGlobalHandlers();
625
+ return;
626
+ }
627
+ document.addEventListener('keydown', this.handleDocumentKeyDown, true);
628
+ }
629
+ teardownGlobalHandlers() {
630
+ document.removeEventListener('keydown', this.handleDocumentKeyDown, true);
631
+ }
610
632
  isFromTextInput(event) {
611
633
  const path = typeof event.composedPath === 'function'
612
634
  ? event.composedPath()
@@ -77,20 +77,12 @@ export class Popover {
77
77
  focusTriggerElement(trigger);
78
78
  };
79
79
  this.setTriggerAttributes = (element) => {
80
- const attributes = {
81
- 'aria-haspopup': true,
82
- 'aria-expanded': this.open,
83
- 'aria-controls': this.portalId,
84
- role: 'button',
85
- };
86
- for (const [key, value] of Object.entries(attributes)) {
87
- if (value) {
88
- element.setAttribute(key, String(value));
89
- }
90
- else {
91
- element.removeAttribute(key);
92
- }
93
- }
80
+ element.setAttribute('aria-haspopup', 'true');
81
+ // `aria-expanded` is needed even when the popover is closed. Without
82
+ // it, the trigger reads as an ordinary button to a screen reader.
83
+ element.setAttribute('aria-expanded', String(this.open));
84
+ element.setAttribute('aria-controls', this.portalId);
85
+ element.setAttribute('role', 'button');
94
86
  };
95
87
  this.portalId = createRandomString();
96
88
  this.globalClickListener = this.globalClickListener.bind(this);
@@ -102,9 +94,12 @@ export class Popover {
102
94
  setTimeout(this.focusTrigger, 0);
103
95
  }
104
96
  }
105
- componentWillLoad() {
97
+ connectedCallback() {
106
98
  this.setupGlobalHandlers();
107
99
  }
100
+ disconnectedCallback() {
101
+ this.teardownGlobalHandlers();
102
+ }
108
103
  componentDidRender() {
109
104
  if (!this.triggerSlot) {
110
105
  return;
@@ -113,21 +108,34 @@ export class Popover {
113
108
  this.triggerSlot.assignedElements().forEach(this.setTriggerAttributes);
114
109
  }
115
110
  setupGlobalHandlers() {
116
- if (this.open) {
117
- document.addEventListener('click', this.globalClickListener, {
118
- capture: true,
119
- });
120
- document.addEventListener('keydown', this.handleGlobalKeyPress);
121
- }
122
- else {
123
- document.removeEventListener('click', this.globalClickListener);
124
- document.removeEventListener('keydown', this.handleGlobalKeyPress);
111
+ // Stencil keeps firing `@Watch` on a detached instance, so `open` can
112
+ // still flip after `disconnectedCallback` has run. Listening then
113
+ // would swallow every click on the page, with no disconnect left to
114
+ // undo it.
115
+ if (!this.open || !this.host.isConnected) {
116
+ this.teardownGlobalHandlers();
117
+ return;
125
118
  }
119
+ document.addEventListener('click', this.globalClickListener, {
120
+ capture: true,
121
+ });
122
+ document.addEventListener('keydown', this.handleGlobalKeyPress);
123
+ }
124
+ /**
125
+ * The capture flag has to be given again: `removeEventListener` only
126
+ * matches a listener registered with the same one, so leaving it out here
127
+ * left the click listener on `document` for good.
128
+ */
129
+ teardownGlobalHandlers() {
130
+ document.removeEventListener('click', this.globalClickListener, {
131
+ capture: true,
132
+ });
133
+ document.removeEventListener('keydown', this.handleGlobalKeyPress);
126
134
  }
127
135
  render() {
128
136
  const cssProperties = this.getCssProperties();
129
137
  const popoverZIndex = getComputedStyle(this.host).getPropertyValue('--popover-z-index');
130
- return (h("div", { key: '096e54a6fdc88f6571884bf0a642c67f144d3bbf', class: "trigger-anchor" }, h("slot", { key: 'ddc46b8755f6463dc2320198aefc50ef759f9de4', name: "trigger", ref: this.setTriggerRef }), h("limel-portal", { key: '89cce45b2218a2a5087f30f4d524d2c1b2041de2', visible: this.open, containerId: this.portalId, containerStyle: { 'z-index': popoverZIndex }, openDirection: this.openDirection }, h("limel-popover-surface", { key: 'a1d1edfb24fdbef9109adef03f8d77e6cced87c7', contentCollection: this.host.children, style: cssProperties }))));
138
+ return (h("div", { key: 'd34ccb8ce24c315432db83e6e1d998c4d230255f', class: "trigger-anchor" }, h("slot", { key: 'f2ab0bc064a439cecfee0fc75cb160cde227e018', name: "trigger", ref: this.setTriggerRef }), h("limel-portal", { key: '047d3643c80c445c8b97f1c30a6b7a8c236beaa9', visible: this.open, containerId: this.portalId, containerStyle: { 'z-index': popoverZIndex }, openDirection: this.openDirection }, h("limel-popover-surface", { key: '91c6f19807aeda2d37652bce452f6bd3cd6bcc9f', contentCollection: this.host.children, style: cssProperties }))));
131
139
  }
132
140
  globalClickListener(event) {
133
141
  const element = event.target;
@@ -2696,6 +2696,7 @@ const Menu = class {
2696
2696
  this.keepOpenOnSelect = false;
2697
2697
  this.shouldRestoreFocusOnClose = false;
2698
2698
  this.normalizedHotkeyCache = new Map();
2699
+ this.triggersDisabledByMenu = new WeakSet();
2699
2700
  this.cachedSubMenuSource = null;
2700
2701
  this.cachedSubMenuItems = null;
2701
2702
  this.handleDocumentKeyDown = (event) => {
@@ -2975,20 +2976,32 @@ const Menu = class {
2975
2976
  this.handleSelect(parent);
2976
2977
  };
2977
2978
  this.setTriggerAttributes = (element) => {
2978
- const attributes = {
2979
- 'aria-haspopup': true,
2980
- 'aria-expanded': this.open,
2981
- 'aria-controls': this.portalId,
2982
- disabled: this.disabled,
2983
- role: 'button',
2984
- };
2985
- for (const [key, value] of Object.entries(attributes)) {
2986
- if (value) {
2987
- element.setAttribute(key, String(value));
2988
- }
2989
- else {
2990
- element.removeAttribute(key);
2979
+ element.setAttribute('aria-haspopup', 'true');
2980
+ element.setAttribute('aria-controls', this.portalId);
2981
+ element.setAttribute('role', 'button');
2982
+ // `aria-expanded` is needed even when the menu is closed. Without
2983
+ // it, the trigger reads as an ordinary button to a screen reader.
2984
+ element.setAttribute('aria-expanded', String(this.open));
2985
+ this.setTriggerDisabled(element);
2986
+ };
2987
+ /**
2988
+ * `disabled` has to be removed rather than set to `false`, because the
2989
+ * presence of the attribute is what disables the element. Only the one the
2990
+ * menu put there may go: a consumer can disable their own trigger, and
2991
+ * stripping that attribute would re-enable it on every render.
2992
+ * @param element - the slotted trigger element
2993
+ */
2994
+ this.setTriggerDisabled = (element) => {
2995
+ if (this.disabled) {
2996
+ if (!element.hasAttribute('disabled')) {
2997
+ element.setAttribute('disabled', 'true');
2998
+ this.triggersDisabledByMenu.add(element);
2991
2999
  }
3000
+ return;
3001
+ }
3002
+ if (this.triggersDisabledByMenu.has(element)) {
3003
+ element.removeAttribute('disabled');
3004
+ this.triggersDisabledByMenu.delete(element);
2992
3005
  }
2993
3006
  };
2994
3007
  this.onClose = () => {
@@ -3191,7 +3204,7 @@ const Menu = class {
3191
3204
  const cssProperties = this.getCssProperties();
3192
3205
  const dropdownZIndex = getComputedStyle(this.host).getPropertyValue('--dropdown-z-index');
3193
3206
  const menuSurfaceWidth = this.getMenuSurfaceWidth(cssProperties['--menu-surface-width']);
3194
- return (h("div", { key: '60e7d159ae0a65582f3184b6a6bfe6c252fe7850', class: "mdc-menu-surface--anchor", onClick: this.onTriggerClick }, h("slot", { key: 'f492115bc3b7115964814883bc103a00496d0178', ref: this.setTriggerRef, name: "trigger" }), this.renderNotificationBadge(), h("limel-portal", { key: 'a38e4fbab9a03fb34b86030d2782a2215ed72650', visible: this.open, containerId: this.portalId, openDirection: this.openDirection, position: "absolute", containerStyle: { 'z-index': dropdownZIndex } }, h("limel-menu-surface", { key: 'dd95ff8c4fc87135cdd00cf5174cef72af61c31d', open: this.open, onDismiss: this.onClose, style: Object.assign(Object.assign({}, cssProperties), { '--menu-surface-width': menuSurfaceWidth, '--limel-menu-surface-display': 'flex', '--limel-menu-surface-flex-direction': 'column' }), class: {
3207
+ return (h("div", { key: 'c66307a5cc7257e2b394f0b5d56d6fbc6fce18b1', class: "mdc-menu-surface--anchor", onClick: this.onTriggerClick }, h("slot", { key: '7c37448e29deaee202f74d3288d21697529c82b5', ref: this.setTriggerRef, name: "trigger" }), this.renderNotificationBadge(), h("limel-portal", { key: 'f7f1b14ae5c6e19fa8e9651c2a847db0bb246427', visible: this.open, containerId: this.portalId, openDirection: this.openDirection, position: "absolute", containerStyle: { 'z-index': dropdownZIndex } }, h("limel-menu-surface", { key: 'c2fd7257bfe8ccf820b8aabc6a8959f6d5d1cd2f', open: this.open, onDismiss: this.onClose, style: Object.assign(Object.assign({}, cssProperties), { '--menu-surface-width': menuSurfaceWidth, '--limel-menu-surface-display': 'flex', '--limel-menu-surface-flex-direction': 'column' }), class: {
3195
3208
  'has-grid-layout': this.gridLayout,
3196
3209
  } }, this.renderSearchField(), this.renderBreadcrumb(), this.renderLoader(), this.renderEmptyMessage(), this.renderMenuList()))));
3197
3210
  }
@@ -3201,24 +3214,33 @@ const Menu = class {
3201
3214
  this.setFocus();
3202
3215
  }
3203
3216
  connectedCallback() {
3204
- if (this.open) {
3205
- document.addEventListener('keydown', this.handleDocumentKeyDown, true);
3206
- }
3217
+ this.setupGlobalHandlers();
3207
3218
  }
3208
3219
  disconnectedCallback() {
3209
- document.removeEventListener('keydown', this.handleDocumentKeyDown, true);
3220
+ this.teardownGlobalHandlers();
3210
3221
  }
3211
3222
  openWatcher(newValue) {
3212
- const opened = newValue;
3213
- if (opened) {
3214
- document.addEventListener('keydown', this.handleDocumentKeyDown, true);
3223
+ this.setupGlobalHandlers();
3224
+ if (newValue) {
3215
3225
  this.setFocus();
3216
3226
  }
3217
3227
  else {
3218
- document.removeEventListener('keydown', this.handleDocumentKeyDown, true);
3219
3228
  this.clearSearch();
3220
3229
  }
3221
3230
  }
3231
+ setupGlobalHandlers() {
3232
+ // Stencil keeps firing `@Watch` on a detached instance, so `open` can
3233
+ // still flip after `disconnectedCallback` has run. Listening then
3234
+ // would leave a listener behind that no disconnect takes down.
3235
+ if (!this.open || !this.host.isConnected) {
3236
+ this.teardownGlobalHandlers();
3237
+ return;
3238
+ }
3239
+ document.addEventListener('keydown', this.handleDocumentKeyDown, true);
3240
+ }
3241
+ teardownGlobalHandlers() {
3242
+ document.removeEventListener('keydown', this.handleDocumentKeyDown, true);
3243
+ }
3222
3244
  isFromTextInput(event) {
3223
3245
  const path = typeof event.composedPath === 'function'
3224
3246
  ? event.composedPath()
@@ -72,20 +72,12 @@ const Popover = class {
72
72
  focusTriggerElement(trigger);
73
73
  };
74
74
  this.setTriggerAttributes = (element) => {
75
- const attributes = {
76
- 'aria-haspopup': true,
77
- 'aria-expanded': this.open,
78
- 'aria-controls': this.portalId,
79
- role: 'button',
80
- };
81
- for (const [key, value] of Object.entries(attributes)) {
82
- if (value) {
83
- element.setAttribute(key, String(value));
84
- }
85
- else {
86
- element.removeAttribute(key);
87
- }
88
- }
75
+ element.setAttribute('aria-haspopup', 'true');
76
+ // `aria-expanded` is needed even when the popover is closed. Without
77
+ // it, the trigger reads as an ordinary button to a screen reader.
78
+ element.setAttribute('aria-expanded', String(this.open));
79
+ element.setAttribute('aria-controls', this.portalId);
80
+ element.setAttribute('role', 'button');
89
81
  };
90
82
  this.portalId = createRandomString();
91
83
  this.globalClickListener = this.globalClickListener.bind(this);
@@ -97,9 +89,12 @@ const Popover = class {
97
89
  setTimeout(this.focusTrigger, 0);
98
90
  }
99
91
  }
100
- componentWillLoad() {
92
+ connectedCallback() {
101
93
  this.setupGlobalHandlers();
102
94
  }
95
+ disconnectedCallback() {
96
+ this.teardownGlobalHandlers();
97
+ }
103
98
  componentDidRender() {
104
99
  if (!this.triggerSlot) {
105
100
  return;
@@ -108,21 +103,34 @@ const Popover = class {
108
103
  this.triggerSlot.assignedElements().forEach(this.setTriggerAttributes);
109
104
  }
110
105
  setupGlobalHandlers() {
111
- if (this.open) {
112
- document.addEventListener('click', this.globalClickListener, {
113
- capture: true,
114
- });
115
- document.addEventListener('keydown', this.handleGlobalKeyPress);
116
- }
117
- else {
118
- document.removeEventListener('click', this.globalClickListener);
119
- document.removeEventListener('keydown', this.handleGlobalKeyPress);
106
+ // Stencil keeps firing `@Watch` on a detached instance, so `open` can
107
+ // still flip after `disconnectedCallback` has run. Listening then
108
+ // would swallow every click on the page, with no disconnect left to
109
+ // undo it.
110
+ if (!this.open || !this.host.isConnected) {
111
+ this.teardownGlobalHandlers();
112
+ return;
120
113
  }
114
+ document.addEventListener('click', this.globalClickListener, {
115
+ capture: true,
116
+ });
117
+ document.addEventListener('keydown', this.handleGlobalKeyPress);
118
+ }
119
+ /**
120
+ * The capture flag has to be given again: `removeEventListener` only
121
+ * matches a listener registered with the same one, so leaving it out here
122
+ * left the click listener on `document` for good.
123
+ */
124
+ teardownGlobalHandlers() {
125
+ document.removeEventListener('click', this.globalClickListener, {
126
+ capture: true,
127
+ });
128
+ document.removeEventListener('keydown', this.handleGlobalKeyPress);
121
129
  }
122
130
  render() {
123
131
  const cssProperties = this.getCssProperties();
124
132
  const popoverZIndex = getComputedStyle(this.host).getPropertyValue('--popover-z-index');
125
- return (h("div", { key: '096e54a6fdc88f6571884bf0a642c67f144d3bbf', class: "trigger-anchor" }, h("slot", { key: 'ddc46b8755f6463dc2320198aefc50ef759f9de4', name: "trigger", ref: this.setTriggerRef }), h("limel-portal", { key: '89cce45b2218a2a5087f30f4d524d2c1b2041de2', visible: this.open, containerId: this.portalId, containerStyle: { 'z-index': popoverZIndex }, openDirection: this.openDirection }, h("limel-popover-surface", { key: 'a1d1edfb24fdbef9109adef03f8d77e6cced87c7', contentCollection: this.host.children, style: cssProperties }))));
133
+ return (h("div", { key: 'd34ccb8ce24c315432db83e6e1d998c4d230255f', class: "trigger-anchor" }, h("slot", { key: 'f2ab0bc064a439cecfee0fc75cb160cde227e018', name: "trigger", ref: this.setTriggerRef }), h("limel-portal", { key: '047d3643c80c445c8b97f1c30a6b7a8c236beaa9', visible: this.open, containerId: this.portalId, containerStyle: { 'z-index': popoverZIndex }, openDirection: this.openDirection }, h("limel-popover-surface", { key: '91c6f19807aeda2d37652bce452f6bd3cd6bcc9f', contentCollection: this.host.children, style: cssProperties }))));
126
134
  }
127
135
  globalClickListener(event) {
128
136
  const element = event.target;