@redvars/peacock 3.6.2 → 3.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 (200) hide show
  1. package/dist/ButtonConstants-D06bY4uy.js +114 -0
  2. package/dist/ButtonConstants-D06bY4uy.js.map +1 -0
  3. package/dist/{BaseHyperlinkMixin-BNuwbiEf.js → NativeHyperlinkMixin-DrYXyfMQ.js} +8 -10
  4. package/dist/NativeHyperlinkMixin-DrYXyfMQ.js.map +1 -0
  5. package/dist/assets/components.css +1 -1
  6. package/dist/assets/components.css.map +1 -1
  7. package/dist/assets/styles.css +1 -1
  8. package/dist/assets/styles.css.map +1 -1
  9. package/dist/button-colors-Dwnez1tR.js +586 -0
  10. package/dist/button-colors-Dwnez1tR.js.map +1 -0
  11. package/dist/button-group.js +8 -8
  12. package/dist/button-group.js.map +1 -1
  13. package/dist/button.js +236 -133
  14. package/dist/button.js.map +1 -1
  15. package/dist/calendar-column-view.js +0 -1
  16. package/dist/calendar-column-view.js.map +1 -1
  17. package/dist/calendar-month-view.js +0 -1
  18. package/dist/calendar-month-view.js.map +1 -1
  19. package/dist/canvas.js +126 -107
  20. package/dist/canvas.js.map +1 -1
  21. package/dist/card-content.js +0 -1
  22. package/dist/card-content.js.map +1 -1
  23. package/dist/card.js +96 -90
  24. package/dist/card.js.map +1 -1
  25. package/dist/cb-compound-expression.js +4 -1
  26. package/dist/cb-compound-expression.js.map +1 -1
  27. package/dist/cb-divider.js +0 -1
  28. package/dist/cb-divider.js.map +1 -1
  29. package/dist/cb-expression.js +0 -2
  30. package/dist/cb-expression.js.map +1 -1
  31. package/dist/cb-predicate.js +0 -1
  32. package/dist/cb-predicate.js.map +1 -1
  33. package/dist/code-highlighter.js +23 -6
  34. package/dist/code-highlighter.js.map +1 -1
  35. package/dist/custom-elements-jsdocs.json +5079 -17882
  36. package/dist/custom-elements.json +19272 -19314
  37. package/dist/fab.js +181 -117
  38. package/dist/fab.js.map +1 -1
  39. package/dist/flow-designer.js +4 -4
  40. package/dist/icon-button-DJ0kZXYr.js +318 -0
  41. package/dist/icon-button-DJ0kZXYr.js.map +1 -0
  42. package/dist/index.js +7 -7
  43. package/dist/{navigation-rail-CD7IrqbN.js → navigation-rail-CM_svs5_.js} +1311 -730
  44. package/dist/navigation-rail-CM_svs5_.js.map +1 -0
  45. package/dist/observe-slot-change-D8Xg-kSS.js +60 -0
  46. package/dist/observe-slot-change-D8Xg-kSS.js.map +1 -0
  47. package/dist/peacock-loader.js +10 -7
  48. package/dist/peacock-loader.js.map +1 -1
  49. package/dist/popover-content.js +0 -1
  50. package/dist/popover-content.js.map +1 -1
  51. package/dist/search.js +15 -15
  52. package/dist/search.js.map +1 -1
  53. package/dist/src/__controllers/attachable-controller.d.ts +109 -0
  54. package/dist/src/__mixins/{BaseButtonMixin.d.ts → NativeButtonMixin.d.ts} +3 -3
  55. package/dist/src/__mixins/{BaseHyperlinkMixin.d.ts → NativeHyperlinkMixin.d.ts} +3 -4
  56. package/dist/src/__utils/is-link.d.ts +1 -0
  57. package/dist/src/__utils/observe-slot-change.d.ts +1 -1
  58. package/dist/src/accordion/accordion-item.d.ts +0 -1
  59. package/dist/src/breadcrumb/breadcrumb-item/breadcrumb-item.d.ts +0 -1
  60. package/dist/src/button/ButtonConstants.d.ts +1 -0
  61. package/dist/src/button/GroupButtonInterface.d.ts +4 -0
  62. package/dist/src/button/button/button.d.ts +32 -7
  63. package/dist/src/button/button-group/button-group.d.ts +2 -1
  64. package/dist/src/button/icon-button/icon-button.d.ts +26 -5
  65. package/dist/src/button/index.d.ts +1 -1
  66. package/dist/src/calendar/calendar-column-view.d.ts +0 -1
  67. package/dist/src/calendar/calendar-month-view.d.ts +0 -1
  68. package/dist/src/canvas/canvas.d.ts +3 -3
  69. package/dist/src/card/card-content.d.ts +0 -1
  70. package/dist/src/card/card.d.ts +9 -6
  71. package/dist/src/chip/chip/chip.d.ts +22 -3
  72. package/dist/src/condition-builder/cb-compound-expression.d.ts +0 -1
  73. package/dist/src/condition-builder/cb-divider.d.ts +0 -1
  74. package/dist/src/condition-builder/cb-expression.d.ts +0 -1
  75. package/dist/src/condition-builder/cb-predicate.d.ts +0 -1
  76. package/dist/src/fab/fab.d.ts +20 -6
  77. package/dist/src/field/field.d.ts +1 -0
  78. package/dist/src/focus-ring/focus-ring.d.ts +26 -20
  79. package/dist/src/image/image.d.ts +2 -2
  80. package/dist/src/index.d.ts +1 -0
  81. package/dist/src/input/input.d.ts +1 -3
  82. package/dist/src/item/index.d.ts +1 -0
  83. package/dist/src/item/item.d.ts +49 -0
  84. package/dist/src/link/link.d.ts +1 -1
  85. package/dist/src/list/list-item.d.ts +1 -2
  86. package/dist/src/menu/menu-item/menu-item.d.ts +9 -11
  87. package/dist/src/menu/sub-menu/sub-menu.d.ts +1 -1
  88. package/dist/src/navigation-rail/navigation-rail-item.d.ts +0 -2
  89. package/dist/src/navigation-rail/navigation-rail.d.ts +2 -6
  90. package/dist/src/popover/popover-content.d.ts +0 -1
  91. package/dist/src/ripple/ripple.d.ts +9 -1
  92. package/dist/src/search/search.d.ts +2 -6
  93. package/dist/src/segmented-button/segmented-button.d.ts +0 -1
  94. package/dist/src/select/option.d.ts +0 -1
  95. package/dist/src/sidebar-menu/sidebar-menu-item.d.ts +0 -1
  96. package/dist/src/sidebar-menu/sidebar-sub-menu.d.ts +0 -1
  97. package/dist/src/tabs/tab-panel.d.ts +0 -1
  98. package/dist/src/tabs/tab.d.ts +4 -6
  99. package/dist/test/item.test.d.ts +1 -0
  100. package/dist/tsconfig.tsbuildinfo +1 -1
  101. package/package.json +4 -2
  102. package/readme.md +2 -2
  103. package/scss/components.scss +0 -1
  104. package/scss/mixin.scss +33 -13
  105. package/scss/styles.scss +1 -3
  106. package/src/__controllers/attachable-controller.ts +198 -0
  107. package/src/__mixins/NativeButtonMixin.ts +87 -0
  108. package/src/__mixins/{BaseHyperlinkMixin.ts → NativeHyperlinkMixin.ts} +15 -15
  109. package/src/__utils/is-link.ts +3 -0
  110. package/src/__utils/observe-slot-change.ts +46 -14
  111. package/src/accordion/accordion-item.scss +1 -1
  112. package/src/accordion/accordion-item.ts +0 -1
  113. package/src/breadcrumb/breadcrumb-item/breadcrumb-item.ts +0 -1
  114. package/src/button/ButtonConstants.ts +1 -0
  115. package/src/button/GroupButtonInterface.ts +4 -0
  116. package/src/button/button/button-colors.scss +2 -2
  117. package/src/button/button/button-layers.scss +124 -0
  118. package/src/button/button/button-sizes.scss +31 -53
  119. package/src/button/button/button.scss +139 -262
  120. package/src/button/button/button.ts +260 -106
  121. package/src/button/button/only-button.scss +13 -0
  122. package/src/button/button-group/button-group.ts +59 -17
  123. package/src/button/icon-button/icon-button-sizes.scss +12 -27
  124. package/src/button/icon-button/icon-button.ts +191 -83
  125. package/src/button/index.ts +1 -1
  126. package/src/calendar/calendar-column-view.ts +0 -1
  127. package/src/calendar/calendar-month-view.ts +0 -1
  128. package/src/canvas/canvas.scss +18 -6
  129. package/src/canvas/canvas.ts +125 -103
  130. package/src/card/card-content.ts +2 -3
  131. package/src/card/card.scss +87 -95
  132. package/src/card/card.ts +62 -60
  133. package/src/chip/chip/chip.scss +66 -71
  134. package/src/chip/chip/chip.ts +155 -56
  135. package/src/code-highlighter/code-highlighter.scss +1 -1
  136. package/src/code-highlighter/code-highlighter.ts +20 -5
  137. package/src/condition-builder/cb-compound-expression.scss +4 -0
  138. package/src/condition-builder/cb-compound-expression.ts +0 -1
  139. package/src/condition-builder/cb-divider.ts +0 -1
  140. package/src/condition-builder/cb-expression.scss +0 -1
  141. package/src/condition-builder/cb-expression.ts +0 -1
  142. package/src/condition-builder/cb-predicate.ts +0 -1
  143. package/src/elevation/elevation.scss +5 -1
  144. package/src/empty-state/empty-state.scss +1 -0
  145. package/src/fab/fab-colors.scss +2 -2
  146. package/src/fab/fab-sizes.scss +24 -34
  147. package/src/fab/fab.scss +77 -71
  148. package/src/fab/fab.ts +141 -65
  149. package/src/field/field.ts +6 -0
  150. package/src/focus-ring/focus-ring.ts +81 -72
  151. package/src/image/image.scss +21 -16
  152. package/src/image/image.ts +13 -14
  153. package/src/index.ts +1 -0
  154. package/src/input/input.ts +16 -25
  155. package/src/item/index.ts +1 -0
  156. package/src/item/item.scss +195 -0
  157. package/src/item/item.ts +362 -0
  158. package/src/link/link.scss +1 -10
  159. package/src/link/link.ts +4 -2
  160. package/src/list/list-item.ts +8 -8
  161. package/src/menu/menu/menu.ts +5 -9
  162. package/src/menu/menu-item/menu-item.scss +30 -108
  163. package/src/menu/menu-item/menu-item.ts +102 -133
  164. package/src/menu/sub-menu/sub-menu.ts +6 -3
  165. package/src/navigation-rail/navigation-rail-item.scss +5 -0
  166. package/src/navigation-rail/navigation-rail-item.ts +10 -15
  167. package/src/navigation-rail/navigation-rail.ts +2 -6
  168. package/src/peacock-loader.ts +5 -1
  169. package/src/popover/popover-content.ts +0 -1
  170. package/src/ripple/ripple.ts +52 -20
  171. package/src/search/search.scss +3 -0
  172. package/src/search/search.ts +11 -16
  173. package/src/segmented-button/segmented-button.ts +0 -1
  174. package/src/select/option.ts +1 -2
  175. package/src/select/select.scss +1 -10
  176. package/src/select/select.ts +2 -0
  177. package/src/sidebar-menu/sidebar-menu-item.ts +0 -1
  178. package/src/sidebar-menu/sidebar-sub-menu.ts +0 -1
  179. package/src/skeleton/skeleton.scss +5 -1
  180. package/src/tabs/tab-panel.ts +0 -1
  181. package/src/tabs/tab.ts +60 -70
  182. package/src/text/text.css-component.scss +3 -21
  183. package/src/tooltip/tooltip.scss +5 -8
  184. package/src/tooltip/tooltip.ts +1 -2
  185. package/dist/BaseButton-BNFAYn-S.js +0 -219
  186. package/dist/BaseButton-BNFAYn-S.js.map +0 -1
  187. package/dist/BaseHyperlinkMixin-BNuwbiEf.js.map +0 -1
  188. package/dist/button-colors-AvGh22Zn.js +0 -561
  189. package/dist/button-colors-AvGh22Zn.js.map +0 -1
  190. package/dist/icon-button-ohxHhy4t.js +0 -247
  191. package/dist/icon-button-ohxHhy4t.js.map +0 -1
  192. package/dist/navigation-rail-CD7IrqbN.js.map +0 -1
  193. package/dist/observe-slot-change-BGJfgg2E.js +0 -31
  194. package/dist/observe-slot-change-BGJfgg2E.js.map +0 -1
  195. package/dist/src/button/BaseButton.d.ts +0 -28
  196. package/dist/src/focus-ring/FocusAttachableController.d.ts +0 -8
  197. package/src/__mixins/BaseButtonMixin.ts +0 -83
  198. package/src/button/BaseButton.ts +0 -113
  199. package/src/focus-ring/FocusAttachableController.ts +0 -28
  200. package/src/popover/tooltip.css-component.scss +0 -19
package/src/fab/fab.ts CHANGED
@@ -1,17 +1,23 @@
1
- import { html, nothing } from 'lit';
2
- import { property, state } from 'lit/decorators.js';
1
+ import { html, LitElement, nothing } from 'lit';
2
+ import { property, query } from 'lit/decorators.js';
3
3
  import { classMap } from 'lit/directives/class-map.js';
4
4
  import { ifDefined } from 'lit/directives/if-defined.js';
5
5
 
6
6
  import IndividualComponent from '@/IndividualComponent.js';
7
- import { dispatchActivationClick, isActivationClick } from '@/__utils/dispatch-event-utils.js';
7
+ import {
8
+ dispatchActivationClick,
9
+ isActivationClick,
10
+ } from '@/__utils/dispatch-event-utils.js';
11
+ import { isLink } from '@/__utils/is-link.js';
8
12
  import { throttle } from '@/__utils/throttle.js';
9
13
  import { spread } from '@/__directive/spread.js';
10
14
 
11
15
  import styles from './fab.scss';
12
16
  import colorStyles from './fab-colors.scss';
13
17
  import sizeStyles from './fab-sizes.scss';
14
- import { BaseButton } from '@/button/BaseButton.js';
18
+ import NativeButtonMixin from '@/__mixins/NativeButtonMixin.js';
19
+ import NativeHyperlinkMixin from '@/__mixins/NativeHyperlinkMixin.js';
20
+ import { DISABLED_REASON_ID } from '@/button/ButtonConstants.js';
15
21
 
16
22
  /**
17
23
  * @label FAB
@@ -36,12 +42,10 @@ import { BaseButton } from '@/button/BaseButton.js';
36
42
  * @tags controls
37
43
  */
38
44
  @IndividualComponent
39
- export class Fab extends BaseButton {
45
+ export class Fab extends NativeButtonMixin(NativeHyperlinkMixin(LitElement)) {
40
46
  static override styles = [styles, colorStyles, sizeStyles];
41
47
 
42
48
  #id = crypto.randomUUID();
43
-
44
-
45
49
 
46
50
  /**
47
51
  * Optional label text for the extended FAB variant.
@@ -56,7 +60,11 @@ export class Fab extends BaseButton {
56
60
  * `"secondary"` uses the secondary color role.
57
61
  * `"tertiary"` uses the tertiary color role.
58
62
  */
59
- @property({ reflect: true }) color: 'surface' | 'primary' | 'secondary' | 'tertiary' = 'surface';
63
+ @property({ reflect: true }) color:
64
+ | 'surface'
65
+ | 'primary'
66
+ | 'secondary'
67
+ | 'tertiary' = 'surface';
60
68
 
61
69
  /**
62
70
  * The style variant of the FAB.
@@ -94,26 +102,57 @@ export class Fab extends BaseButton {
94
102
  */
95
103
  @property() tooltip?: string;
96
104
 
97
- @state()
98
- isPressed = false;
105
+ @property({ type: Boolean, reflect: true }) skeleton: boolean = false;
99
106
 
100
- override focus() {
101
- this.buttonElement?.focus();
102
- }
107
+ @property({ type: Boolean, reflect: true }) toggle: boolean = false;
103
108
 
104
- override blur() {
105
- this.buttonElement?.blur();
109
+ @property({ type: Boolean, reflect: true }) selected: boolean = false;
110
+
111
+ /**
112
+ * States
113
+ */
114
+ @property({ type: Boolean, reflect: true })
115
+ pressed = false;
116
+
117
+ @query('.button') readonly buttonElement!: HTMLElement | null;
118
+
119
+ override connectedCallback() {
120
+ super.connectedCallback();
121
+ this.addEventListener('click', this.__dispatchClickWithThrottle);
122
+ window.addEventListener('mouseup', this.__handlePress);
106
123
  }
107
124
 
108
- override firstUpdated() {
109
- this.__dispatchClickWithThrottle = throttle(
110
- this.__dispatchClick,
111
- this.throttleDelay,
112
- );
125
+ override disconnectedCallback() {
126
+ window.removeEventListener('mouseup', this.__handlePress);
127
+ this.removeEventListener('click', this.__dispatchClickWithThrottle);
128
+ super.disconnectedCallback();
113
129
  }
114
130
 
131
+ __handlePress = (event: KeyboardEvent | MouseEvent) => {
132
+ if (this.disabled || this.skeleton || this.softDisabled) return;
133
+ if (
134
+ event instanceof KeyboardEvent &&
135
+ event.type === 'keydown' &&
136
+ (event.key === 'Enter' || event.key === ' ')
137
+ ) {
138
+ this.pressed = true;
139
+ } else if (event.type === 'mousedown') {
140
+ this.pressed = true;
141
+ } else {
142
+ this.pressed = false;
143
+ }
144
+ };
145
+
146
+ __dispatchClickWithThrottle: (event: MouseEvent | KeyboardEvent) => void =
147
+ event => {
148
+ this.__dispatchClick(event);
149
+ };
150
+
115
151
  __dispatchClick = (event: MouseEvent | KeyboardEvent) => {
116
- if (this.disabled && this.href) {
152
+ // If the button is soft-disabled or a disabled link, we need to explicitly
153
+ // prevent the click from propagating to other event listeners as well as
154
+ // prevent the default action.
155
+ if (this.softDisabled || (this.disabled && this.href) || this.skeleton) {
117
156
  event.stopImmediatePropagation();
118
157
  event.preventDefault();
119
158
  return;
@@ -123,18 +162,66 @@ export class Fab extends BaseButton {
123
162
  return;
124
163
  }
125
164
 
165
+ if (this.toggle) {
166
+ this.selected = !this.selected;
167
+ }
168
+
126
169
  this.focus();
127
170
  dispatchActivationClick(this.buttonElement);
128
171
  };
129
172
 
130
- __getDisabledReasonID() {
131
- return this.disabled ? `disabled-reason-${this.#id}` : nothing;
173
+ __renderDisabledReason(softDisabled: boolean) {
174
+ if (softDisabled)
175
+ return html`<div
176
+ id=${DISABLED_REASON_ID}
177
+ role="tooltip"
178
+ aria-label=${this.disabledReason}
179
+ class="screen-reader-only"
180
+ >
181
+ ${this.disabledReason}
182
+ </div>`;
183
+ return nothing;
184
+ }
185
+
186
+ __renderTooltip() {
187
+ if (this.tooltip) {
188
+ return html`<wc-tooltip class="tooltip" for="button"
189
+ >${this.tooltip}</wc-tooltip
190
+ >`;
191
+ }
192
+ return nothing;
193
+ }
194
+
195
+ override focus() {
196
+ this.buttonElement?.focus();
197
+ }
198
+
199
+ override blur() {
200
+ this.buttonElement?.blur();
201
+ }
202
+
203
+ override firstUpdated() {
204
+ this.__dispatchClickWithThrottle = throttle(
205
+ this.__dispatchClick,
206
+ this.throttleDelay,
207
+ );
132
208
  }
133
209
 
134
210
  override render() {
135
- const isLink = this.__isLink();
136
211
  const isExtended = !!this.label;
137
212
 
213
+ return html`
214
+ <wc-focus-ring class="focus-ring" for="button"></wc-focus-ring>
215
+ <wc-elevation class="elevation"></wc-elevation>
216
+ <div class="background"></div>
217
+ <wc-ripple class="ripple" for="button"></wc-ripple>
218
+ <wc-skeleton class="skeleton"></wc-skeleton>
219
+
220
+ ${this.__renderFabElement(isExtended)} ${this.__renderTooltip()}
221
+ `;
222
+ }
223
+
224
+ __renderFabElement(isExtended: boolean) {
138
225
  const cssClasses = {
139
226
  button: true,
140
227
  fab: true,
@@ -145,70 +232,59 @@ export class Fab extends BaseButton {
145
232
  extended: isExtended,
146
233
  lowered: this.lowered,
147
234
  disabled: this.disabled,
148
- pressed: this.isPressed,
235
+ pressed: this.pressed,
149
236
  };
150
237
 
151
- if (!isLink) {
238
+ if (!isLink(this)) {
152
239
  return html`<button
153
- class=${classMap(cssClasses)}
154
- id="button"
155
- type="button"
156
- @click=${this.__dispatchClickWithThrottle}
157
- @mousedown=${this.__handlePress}
158
- @keydown=${this.__handlePress}
159
- @keyup=${this.__handlePress}
160
-
161
- aria-describedby=${ifDefined(this.softDisabled ? BaseButton.DISABLED_REASON_ID : undefined)}
162
- ?aria-disabled=${this.softDisabled}
163
-
164
- ?disabled=${this.disabled}
165
- ${spread(this.configAria)}
166
- >
167
- ${this.__renderFabContent(isExtended)}
168
- </button>
169
- ${this.__renderTooltip()}`;
170
- }
171
-
172
- return html`<a
173
240
  class=${classMap(cssClasses)}
174
241
  id="button"
175
- tabindex=${this.disabled ? '-1' : '0'}
176
- href=${ifDefined(this.href)}
177
- target=${this.target}
178
- @click=${this.__dispatchClick}
242
+ type="button"
243
+ @click=${this.__dispatchClickWithThrottle}
179
244
  @mousedown=${this.__handlePress}
180
245
  @keydown=${this.__handlePress}
181
246
  @keyup=${this.__handlePress}
182
- role="button"
183
-
184
- aria-describedby=${ifDefined(this.softDisabled ? BaseButton.DISABLED_REASON_ID : undefined)}
247
+ aria-describedby=${ifDefined(
248
+ this.softDisabled ? DISABLED_REASON_ID : undefined,
249
+ )}
185
250
  ?aria-disabled=${this.softDisabled}
186
-
251
+ ?disabled=${this.disabled}
187
252
  ${spread(this.configAria)}
188
253
  >
189
254
  ${this.__renderFabContent(isExtended)}
190
- </a>
191
- ${this.__renderTooltip()}`;
255
+ </button>`;
256
+ }
257
+
258
+ return html`<a
259
+ class=${classMap(cssClasses)}
260
+ id="button"
261
+ tabindex=${this.disabled ? '-1' : '0'}
262
+ href=${ifDefined(this.href)}
263
+ target=${this.target}
264
+ @click=${this.__dispatchClick}
265
+ @mousedown=${this.__handlePress}
266
+ @keydown=${this.__handlePress}
267
+ @keyup=${this.__handlePress}
268
+ role="button"
269
+ aria-describedby=${ifDefined(
270
+ this.softDisabled ? DISABLED_REASON_ID : undefined,
271
+ )}
272
+ ?aria-disabled=${this.softDisabled}
273
+ ${spread(this.configAria)}
274
+ >
275
+ ${this.__renderFabContent(isExtended)}
276
+ </a>`;
192
277
  }
193
278
 
194
279
  __renderFabContent(isExtended: boolean) {
195
280
  return html`
196
- <wc-focus-ring class="focus-ring" for='button'></wc-focus-ring>
197
- <wc-elevation class="elevation"></wc-elevation>
198
- <div class="background"></div>
199
- <wc-ripple class="ripple"></wc-ripple>
200
- <wc-skeleton class="skeleton"></wc-skeleton>
201
-
202
281
  <div class="fab-content">
203
-
204
282
  <slot></slot>
205
283
  ${isExtended
206
284
  ? html`<span class="fab-label">${this.label}</span>`
207
285
  : nothing}
208
286
  </div>
209
-
210
287
  ${this.__renderDisabledReason(this.softDisabled)}
211
288
  `;
212
289
  }
213
-
214
290
  }
@@ -24,6 +24,12 @@ import styles from './field.scss';
24
24
  * @tags form
25
25
  */
26
26
  export class Field extends LitElement {
27
+
28
+ static shadowRootOptions: ShadowRootInit = {
29
+ ...LitElement.shadowRootOptions,
30
+ delegatesFocus: true,
31
+ };
32
+
27
33
  static styles = [styles];
28
34
 
29
35
  @property({ type: String })
@@ -1,6 +1,20 @@
1
- import { LitElement, nothing } from 'lit';
1
+ import { isServer, LitElement, PropertyValues } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
3
  import styles from './focus-ring.scss';
4
+ import {
5
+ Attachable,
6
+ AttachableController,
7
+ } from '@/__controllers/attachable-controller.js';
8
+
9
+ /**
10
+ * Events that the focus ring listens to.
11
+ */
12
+ const EVENTS = ['focusin', 'focusout', 'pointerdown'];
13
+ const HANDLED_BY_FOCUS_RING = Symbol('handledByFocusRing');
14
+
15
+ interface FocusRingEvent extends Event {
16
+ [HANDLED_BY_FOCUS_RING]: true;
17
+ }
4
18
 
5
19
  /**
6
20
  * @label Focus Ring
@@ -12,103 +26,98 @@ import styles from './focus-ring.scss';
12
26
  *
13
27
  *
14
28
  * @tags display
29
+ *
30
+ * @fires visibility-changed {Event} Fired whenever `visible` changes.
15
31
  */
16
- export class FocusRing extends LitElement {
32
+ export class FocusRing extends LitElement implements Attachable {
17
33
  static styles = [styles];
18
34
 
19
- @property({ type: Boolean, reflect: true }) visible: boolean = false;
20
-
21
- @property({ type: String }) for = '';
22
-
23
- private __boundFocusin = this.__focusin.bind(this);
24
-
25
- private __boundFocusout = this.__focusout.bind(this);
35
+ /**
36
+ * Makes the focus ring visible.
37
+ */
38
+ @property({ type: Boolean, reflect: true })
39
+ visible = false;
26
40
 
27
- private __boundPointerdown = this.__pointerdown.bind(this);
41
+ /**
42
+ * Makes the focus ring animate inwards instead of outwards.
43
+ */
44
+ @property({ type: Boolean, reflect: true }) inward = false;
28
45
 
29
- render() {
30
- return nothing;
46
+ get htmlFor() {
47
+ return this.attachableController.htmlFor;
31
48
  }
32
49
 
33
- connectedCallback() {
34
- super.connectedCallback();
35
- this.attach();
50
+ set htmlFor(htmlFor: string | null) {
51
+ this.attachableController.htmlFor = htmlFor;
36
52
  }
37
53
 
38
- disconnectedCallback() {
39
- this.detach();
40
- super.disconnectedCallback();
54
+ get control() {
55
+ return this.attachableController.control;
41
56
  }
42
57
 
43
- updated(changed: Map<string, unknown>) {
44
- if (changed.has('for')) {
45
- const prevId = changed.get('for') as string;
46
- if (prevId) {
47
- const root = this.parentElement?.getRootNode() as ShadowRoot | Document;
48
- const prevEl = root?.getElementById(prevId) ?? document.getElementById(prevId);
49
- if (prevEl) {
50
- prevEl.removeEventListener('focusin', this.__boundFocusin);
51
- prevEl.removeEventListener('focusout', this.__boundFocusout);
52
- prevEl.removeEventListener('pointerdown', this.__boundPointerdown);
53
- }
54
- }
55
- this.attach();
56
- }
58
+ set control(control: HTMLElement | null) {
59
+ this.attachableController.control = control;
57
60
  }
58
61
 
59
- __focusin() {
60
- const focusTarget = this.__getFocusTarget();
61
- this.visible = focusTarget?.matches(':focus-visible') ?? false;
62
+ private readonly attachableController = new AttachableController(
63
+ this,
64
+ this.onControlChange.bind(this),
65
+ );
66
+
67
+ attach(control: HTMLElement) {
68
+ this.attachableController.attach(control);
62
69
  }
63
70
 
64
- __focusout() {
65
- this.visible = false;
71
+ detach() {
72
+ this.attachableController.detach();
66
73
  }
67
74
 
68
- __pointerdown() {
69
- this.visible = false;
75
+ override connectedCallback() {
76
+ super.connectedCallback();
77
+ // Needed for VoiceOver, which will create a "group" if the element is a
78
+ // sibling to other content.
79
+ this.setAttribute('aria-hidden', 'true');
70
80
  }
71
81
 
72
- /**
73
- * Resolves the element that should receive focus-ring event listeners by id.
74
- * Prefers lookup from the current control's root node, then falls back to a
75
- * document-level lookup.
76
- *
77
- * @returns The resolved focus target, if one can be found.
78
- */
79
- __getFocusTarget(): HTMLElement | undefined {
80
- if (this.for) {
81
- const root = this.parentElement?.getRootNode() as ShadowRoot | Document;
82
- if (root) {
83
- const focusTarget = root.getElementById(this.for);
84
- if (focusTarget) {
85
- return focusTarget;
86
- }
87
- }
88
- const focusTarget = document.getElementById(this.for);
89
- if (focusTarget) {
90
- return focusTarget;
91
- }
82
+ /** @private */
83
+ handleEvent(event: FocusRingEvent) {
84
+ if (event[HANDLED_BY_FOCUS_RING]) {
85
+ // This ensures the focus ring does not activate when multiple focus rings
86
+ // are used within a single component.
87
+ return;
88
+ }
89
+
90
+ switch (event.type) {
91
+ case 'focusin':
92
+ this.visible = this.control?.matches(':focus-visible') ?? false;
93
+ break;
94
+ case 'focusout':
95
+ case 'pointerdown':
96
+ this.visible = false;
97
+ break;
98
+ default:
99
+ return;
92
100
  }
93
101
 
94
- return undefined;
102
+ // eslint-disable-next-line no-param-reassign
103
+ event[HANDLED_BY_FOCUS_RING] = true;
95
104
  }
96
105
 
97
- attach() {
98
- const focusTarget = this.__getFocusTarget();
99
- if (focusTarget) {
100
- focusTarget.addEventListener('focusin', this.__boundFocusin);
101
- focusTarget.addEventListener('focusout', this.__boundFocusout);
102
- focusTarget.addEventListener('pointerdown', this.__boundPointerdown);
106
+ private onControlChange(prev: HTMLElement | null, next: HTMLElement | null) {
107
+ if (isServer) return;
108
+
109
+ for (const event of EVENTS) {
110
+ prev?.removeEventListener(event, this);
111
+ next?.addEventListener(event, this);
103
112
  }
104
113
  }
105
114
 
106
- detach() {
107
- const focusTarget = this.__getFocusTarget();
108
- if (focusTarget) {
109
- focusTarget.removeEventListener('focusin', this.__boundFocusin);
110
- focusTarget.removeEventListener('focusout', this.__boundFocusout);
111
- focusTarget.removeEventListener('pointerdown', this.__boundPointerdown);
115
+ override update(changed: PropertyValues<FocusRing>) {
116
+ if (changed.has('visible')) {
117
+ // This logic can be removed once the `:has` selector has been introduced
118
+ // to Firefox. This is necessary to allow correct submenu styles.
119
+ this.dispatchEvent(new Event('visibility-changed'));
112
120
  }
121
+ super.update(changed);
113
122
  }
114
123
  }
@@ -30,27 +30,32 @@ img.clickable {
30
30
  display: block;
31
31
  }
32
32
 
33
- /* Preview overlayrendered in light DOM via portal, but we keep
34
- a host-level overlay as a fallback when shadow DOM is used. */
35
- .preview-overlay {
36
- display: none;
37
- position: fixed;
38
- inset: 0;
39
- z-index: 9999;
40
- background: rgba(0, 0, 0, 0.85);
41
- align-items: center;
42
- justify-content: center;
33
+ /* Lightbox previewnative <dialog> renders in the top layer,
34
+ bypassing any stacking context on the host page. */
35
+ .preview-dialog {
36
+ background: transparent;
37
+ border: none;
43
38
  cursor: zoom-out;
39
+ max-height: 90dvh;
40
+ max-width: 90dvw;
41
+ padding: 0;
42
+
43
+ &[open] {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ }
48
+
49
+ &::backdrop {
50
+ background: color-mix(in srgb, var(--color-scrim), transparent 15%);
51
+ }
44
52
  }
45
53
 
46
- .preview-overlay.open {
47
- display: flex;
48
- }
49
-
50
- .preview-overlay img {
54
+ .preview-dialog img {
51
55
  max-width: 90vw;
52
56
  max-height: 90vh;
53
57
  object-fit: contain;
54
- box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6);
58
+ box-shadow: 0 8px 40px color-mix(in srgb, var(--color-shadow), transparent 40%);
55
59
  border-radius: 4px;
60
+ cursor: auto;
56
61
  }
@@ -1,5 +1,5 @@
1
1
  import { LitElement, html } from 'lit';
2
- import { property, state } from 'lit/decorators.js';
2
+ import { property, query, state } from 'lit/decorators.js';
3
3
 
4
4
  import { isDarkMode } from '@/__utils/is-dark-mode.js';
5
5
  import { observeThemeChange } from '@/__utils/observe-theme-change.js';
@@ -37,7 +37,7 @@ export class Image extends LitElement {
37
37
 
38
38
  @state() private _loaded = false;
39
39
 
40
- @state() private _previewOpen = false;
40
+ @query('.preview-dialog') private _dialog?: HTMLDialogElement;
41
41
 
42
42
  private _intersectionObserver: IntersectionObserver | null = null;
43
43
 
@@ -90,13 +90,15 @@ export class Image extends LitElement {
90
90
 
91
91
  private _handleClick() {
92
92
  if (this.preview) {
93
- this._previewOpen = true;
93
+ this._dialog?.showModal();
94
94
  }
95
95
  }
96
96
 
97
- private _closePreview(e: Event) {
98
- e.stopPropagation();
99
- this._previewOpen = false;
97
+ private _handleDialogClick(e: MouseEvent) {
98
+ // Close when clicking the backdrop (target is the dialog itself, not the image)
99
+ if (e.target === e.currentTarget) {
100
+ (e.currentTarget as HTMLDialogElement).close();
101
+ }
100
102
  }
101
103
 
102
104
  render() {
@@ -112,17 +114,14 @@ export class Image extends LitElement {
112
114
  : html`<span class="placeholder" aria-hidden="true"></span>`}
113
115
  </div>
114
116
 
115
- <!-- Lightbox preview overlay (inside shadow root) -->
116
- <div
117
- class="preview-overlay ${this._previewOpen ? 'open' : ''}"
118
- role="dialog"
119
- aria-modal="true"
117
+ <!-- Lightbox preview dialog uses native top-layer to avoid stacking context issues -->
118
+ <dialog
119
+ class="preview-dialog"
120
120
  aria-label="Image preview"
121
- @click=${this._closePreview}
122
- @keydown=${(e: KeyboardEvent) => e.key === 'Escape' && this._closePreview(e)}
121
+ @click=${this._handleDialogClick}
123
122
  >
124
123
  <img src=${this._activeSrc} alt=${this.imageTitle} @click=${(e: Event) => e.stopPropagation()} />
125
- </div>
124
+ </dialog>
126
125
  `;
127
126
  }
128
127
  }
package/src/index.ts CHANGED
@@ -39,6 +39,7 @@ export { Popover, PopoverContent } from './popover/index.js';
39
39
  export { Breadcrumb, BreadcrumbItem } from './breadcrumb/index.js';
40
40
  export { Menu, MenuItem, SubMenu } from './menu/index.js';
41
41
  export { List, ListItem } from './list/index.js';
42
+ export { Item } from './item/index.js';
42
43
 
43
44
  export { CodeHighlighter } from './code-highlighter/index.js';
44
45
  export { CodeEditor } from './code-editor/index.js';