@redvars/peacock 3.6.3 → 3.8.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 (179) 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-Cg6oxiz-.js → button-colors-DSuBHd-i.js} +200 -186
  10. package/dist/button-colors-DSuBHd-i.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 +224 -124
  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/card-content.js +0 -1
  20. package/dist/card-content.js.map +1 -1
  21. package/dist/card.js +96 -90
  22. package/dist/card.js.map +1 -1
  23. package/dist/cb-compound-expression.js +4 -1
  24. package/dist/cb-compound-expression.js.map +1 -1
  25. package/dist/cb-divider.js +0 -1
  26. package/dist/cb-divider.js.map +1 -1
  27. package/dist/cb-expression.js +0 -2
  28. package/dist/cb-expression.js.map +1 -1
  29. package/dist/cb-predicate.js +0 -1
  30. package/dist/cb-predicate.js.map +1 -1
  31. package/dist/code-highlighter.js +23 -6
  32. package/dist/code-highlighter.js.map +1 -1
  33. package/dist/custom-elements-jsdocs.json +5102 -18408
  34. package/dist/custom-elements.json +19630 -20205
  35. package/dist/fab.js +181 -117
  36. package/dist/fab.js.map +1 -1
  37. package/dist/{flow-designer-node-9Bqyn6qx.js → flow-designer-node-BWrPuxAR.js} +1 -2
  38. package/dist/flow-designer-node-BWrPuxAR.js.map +1 -0
  39. package/dist/flow-designer-node.js +1 -1
  40. package/dist/flow-designer.js +5 -5
  41. package/dist/icon-button-CYqrnMnF.js +318 -0
  42. package/dist/icon-button-CYqrnMnF.js.map +1 -0
  43. package/dist/index.js +8 -8
  44. package/dist/{navigation-rail-DAUuJ_Yp.js → navigation-rail-CM_svs5_.js} +511 -295
  45. package/dist/navigation-rail-CM_svs5_.js.map +1 -0
  46. package/dist/observe-slot-change-D8Xg-kSS.js +60 -0
  47. package/dist/observe-slot-change-D8Xg-kSS.js.map +1 -0
  48. package/dist/peacock-loader.js +7 -7
  49. package/dist/peacock-loader.js.map +1 -1
  50. package/dist/popover-content.js +0 -1
  51. package/dist/popover-content.js.map +1 -1
  52. package/dist/search.js +4 -1
  53. package/dist/search.js.map +1 -1
  54. package/dist/src/__controllers/attachable-controller.d.ts +109 -0
  55. package/dist/src/__mixins/{BaseButtonMixin.d.ts → NativeButtonMixin.d.ts} +3 -3
  56. package/dist/src/__mixins/{BaseHyperlinkMixin.d.ts → NativeHyperlinkMixin.d.ts} +3 -4
  57. package/dist/src/__utils/is-link.d.ts +1 -0
  58. package/dist/src/__utils/observe-slot-change.d.ts +1 -1
  59. package/dist/src/accordion/accordion-item.d.ts +0 -1
  60. package/dist/src/breadcrumb/breadcrumb-item/breadcrumb-item.d.ts +0 -1
  61. package/dist/src/button/ButtonConstants.d.ts +1 -0
  62. package/dist/src/button/GroupButtonInterface.d.ts +4 -0
  63. package/dist/src/button/button/button.d.ts +32 -7
  64. package/dist/src/button/button-group/button-group.d.ts +2 -1
  65. package/dist/src/button/icon-button/icon-button.d.ts +26 -5
  66. package/dist/src/button/index.d.ts +1 -1
  67. package/dist/src/calendar/calendar-column-view.d.ts +0 -1
  68. package/dist/src/calendar/calendar-month-view.d.ts +0 -1
  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/flow-designer/flow-designer-node.d.ts +0 -1
  78. package/dist/src/focus-ring/focus-ring.d.ts +26 -20
  79. package/dist/src/item/item.d.ts +2 -1
  80. package/dist/src/link/link.d.ts +1 -1
  81. package/dist/src/list/list-item.d.ts +1 -2
  82. package/dist/src/menu/menu-item/menu-item.d.ts +1 -2
  83. package/dist/src/menu/sub-menu/sub-menu.d.ts +0 -1
  84. package/dist/src/navigation-rail/navigation-rail-item.d.ts +0 -2
  85. package/dist/src/popover/popover-content.d.ts +0 -1
  86. package/dist/src/ripple/ripple.d.ts +9 -1
  87. package/dist/src/segmented-button/segmented-button.d.ts +0 -1
  88. package/dist/src/select/option.d.ts +0 -1
  89. package/dist/src/sidebar-menu/sidebar-menu-item.d.ts +0 -1
  90. package/dist/src/sidebar-menu/sidebar-sub-menu.d.ts +0 -1
  91. package/dist/src/tabs/tab-panel.d.ts +0 -1
  92. package/dist/src/tabs/tab.d.ts +4 -6
  93. package/dist/tsconfig.tsbuildinfo +1 -1
  94. package/package.json +4 -2
  95. package/readme.md +2 -2
  96. package/scss/components.scss +0 -1
  97. package/scss/mixin.scss +10 -13
  98. package/scss/styles.scss +1 -3
  99. package/src/__controllers/attachable-controller.ts +198 -0
  100. package/src/__mixins/NativeButtonMixin.ts +87 -0
  101. package/src/__mixins/{BaseHyperlinkMixin.ts → NativeHyperlinkMixin.ts} +15 -15
  102. package/src/__utils/is-link.ts +3 -0
  103. package/src/__utils/observe-slot-change.ts +46 -14
  104. package/src/accordion/accordion-item.scss +1 -1
  105. package/src/accordion/accordion-item.ts +0 -1
  106. package/src/breadcrumb/breadcrumb-item/breadcrumb-item.ts +0 -1
  107. package/src/button/ButtonConstants.ts +1 -0
  108. package/src/button/GroupButtonInterface.ts +4 -0
  109. package/src/button/button/button-colors.scss +2 -2
  110. package/src/button/button/button-layers.scss +124 -0
  111. package/src/button/button/button-sizes.scss +20 -42
  112. package/src/button/button/button.scss +71 -169
  113. package/src/button/button/button.ts +229 -78
  114. package/src/button/button/only-button.scss +13 -0
  115. package/src/button/button-group/button-group.ts +59 -17
  116. package/src/button/icon-button/icon-button-sizes.scss +6 -21
  117. package/src/button/icon-button/icon-button.ts +198 -93
  118. package/src/button/index.ts +1 -1
  119. package/src/calendar/calendar-column-view.ts +0 -1
  120. package/src/calendar/calendar-month-view.ts +0 -1
  121. package/src/card/card-content.ts +2 -3
  122. package/src/card/card.scss +87 -95
  123. package/src/card/card.ts +62 -60
  124. package/src/chip/chip/chip.scss +65 -70
  125. package/src/chip/chip/chip.ts +155 -56
  126. package/src/code-highlighter/code-highlighter.scss +1 -1
  127. package/src/code-highlighter/code-highlighter.ts +20 -5
  128. package/src/condition-builder/cb-compound-expression.scss +4 -0
  129. package/src/condition-builder/cb-compound-expression.ts +0 -1
  130. package/src/condition-builder/cb-divider.ts +0 -1
  131. package/src/condition-builder/cb-expression.scss +0 -1
  132. package/src/condition-builder/cb-expression.ts +0 -1
  133. package/src/condition-builder/cb-predicate.ts +0 -1
  134. package/src/elevation/elevation.scss +5 -1
  135. package/src/fab/fab-colors.scss +2 -2
  136. package/src/fab/fab-sizes.scss +24 -34
  137. package/src/fab/fab.scss +77 -71
  138. package/src/fab/fab.ts +141 -65
  139. package/src/flow-designer/flow-designer-node.ts +0 -1
  140. package/src/focus-ring/focus-ring.ts +81 -72
  141. package/src/item/item.scss +77 -66
  142. package/src/item/item.ts +61 -39
  143. package/src/link/link.scss +1 -10
  144. package/src/link/link.ts +4 -2
  145. package/src/list/list-item.ts +8 -8
  146. package/src/menu/menu-item/menu-item.ts +17 -8
  147. package/src/menu/sub-menu/sub-menu.ts +0 -1
  148. package/src/navigation-rail/navigation-rail-item.scss +5 -0
  149. package/src/navigation-rail/navigation-rail-item.ts +10 -15
  150. package/src/peacock-loader.ts +1 -1
  151. package/src/popover/popover-content.ts +0 -1
  152. package/src/ripple/ripple.ts +52 -20
  153. package/src/search/search.scss +3 -0
  154. package/src/segmented-button/segmented-button.ts +0 -1
  155. package/src/select/option.ts +0 -1
  156. package/src/sidebar-menu/sidebar-menu-item.ts +0 -1
  157. package/src/sidebar-menu/sidebar-sub-menu.ts +0 -1
  158. package/src/skeleton/skeleton.scss +5 -1
  159. package/src/tabs/tab-panel.ts +0 -1
  160. package/src/tabs/tab.ts +60 -70
  161. package/src/text/text.css-component.scss +3 -21
  162. package/src/tooltip/tooltip.scss +5 -8
  163. package/src/tooltip/tooltip.ts +1 -2
  164. package/dist/BaseButton-BNFAYn-S.js +0 -219
  165. package/dist/BaseButton-BNFAYn-S.js.map +0 -1
  166. package/dist/BaseHyperlinkMixin-BNuwbiEf.js.map +0 -1
  167. package/dist/button-colors-Cg6oxiz-.js.map +0 -1
  168. package/dist/flow-designer-node-9Bqyn6qx.js.map +0 -1
  169. package/dist/icon-button-AdJBEoNy.js +0 -251
  170. package/dist/icon-button-AdJBEoNy.js.map +0 -1
  171. package/dist/navigation-rail-DAUuJ_Yp.js.map +0 -1
  172. package/dist/observe-slot-change-BGJfgg2E.js +0 -31
  173. package/dist/observe-slot-change-BGJfgg2E.js.map +0 -1
  174. package/dist/src/button/BaseButton.d.ts +0 -28
  175. package/dist/src/focus-ring/FocusAttachableController.d.ts +0 -8
  176. package/src/__mixins/BaseButtonMixin.ts +0 -83
  177. package/src/button/BaseButton.ts +0 -113
  178. package/src/focus-ring/FocusAttachableController.ts +0 -28
  179. 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
  }
@@ -10,7 +10,6 @@ import type { WorkflowNode } from './types.js';
10
10
  *
11
11
  * @tag wc-flow-designer-node
12
12
  * @rawTag flow-designer-node
13
- * @parentRawTag flow-designer
14
13
  * @wip true
15
14
  */
16
15
  @IndividualComponent
@@ -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
  }
@@ -3,9 +3,31 @@
3
3
  @include mixin.base-styles;
4
4
 
5
5
  :host {
6
+ position: relative;
6
7
  display: block;
7
8
  padding-inline: var(--spacing-050);
8
9
  --item-height: 3.5rem;
10
+
11
+ --item-container-color: transparent;
12
+ --item-label-text-color: var(--color-on-surface);
13
+ --item-leading-trailing-color: var(--color-on-surface-variant);
14
+ --item-supporting-text-color: var(--color-on-surface-variant);
15
+
16
+ --item-container-selected-color: var(--color-tertiary-container);
17
+ --item-label-text-selected-color: var(--color-on-tertiary-container);
18
+
19
+ --private-item-container-color: var(--item-container-color);
20
+ --private-item-label-text-color: var(--item-label-text-color);
21
+ --private-item-leading-trailing-color: var(--item-leading-trailing-color);
22
+ --private-item-supporting-text-color: var(--item-supporting-text-color);
23
+
24
+ --private-item-container-state-color: var(--private-item-label-text-color);
25
+
26
+ --private-item-container-shape-start-start: var(--item-container-shape-start-start, var(--shape-corner-extra-small));
27
+ --private-item-container-shape-start-end: var(--item-container-shape-start-end, var(--shape-corner-extra-small));
28
+ --private-item-container-shape-end-start: var(--item-container-shape-end-start, var(--shape-corner-extra-small));
29
+ --private-item-container-shape-end-end: var(--item-container-shape-end-end, var(--shape-corner-extra-small));
30
+ --private-item-container-shape-variant: var(--item-container-shape-variant, none);
9
31
  }
10
32
 
11
33
  /**
@@ -19,18 +41,12 @@
19
41
  }
20
42
 
21
43
  .item {
22
- position: relative;
23
44
  min-height: var(--item-height);
24
45
  width: 100%;
25
46
  background: transparent;
26
47
  text-align: initial;
27
48
  cursor: pointer;
28
-
29
- --private-item-container-shape-start-start: var(--item-container-shape-start-start, var(--shape-corner-extra-small));
30
- --private-item-container-shape-start-end: var(--item-container-shape-start-end, var(--shape-corner-extra-small));
31
- --private-item-container-shape-end-start: var(--item-container-shape-end-start, var(--shape-corner-extra-small));
32
- --private-item-container-shape-end-end: var(--item-container-shape-end-end, var(--shape-corner-extra-small));
33
- --private-item-container-shape-variant: var(--item-container-shape-variant, none);
49
+ z-index: 0;
34
50
 
35
51
  font-family: var(--item-label-font-family, var(--typography-body-large-font-family)) !important;
36
52
  font-size: var(--item-label-font-size, var(--typography-body-large-font-size)) !important;
@@ -112,73 +128,68 @@
112
128
  white-space: nowrap;
113
129
  color: var(--private-item-supporting-text-color);
114
130
  }
131
+ }
115
132
 
116
- .background {
117
- position: absolute;
118
- inset: 0;
119
- background-color: var(--private-item-container-color);
120
- opacity: var(--private-item-container-opacity, 1);
121
- pointer-events: none;
122
-
123
- @include mixin.apply-container-shape(private-item);
124
- }
125
-
126
- .focus-ring {
127
- z-index: 2;
128
- @include mixin.copy-container-shape(private-item, focus-ring);
129
-
130
- }
133
+ /*
134
+ Background layers
135
+ */
136
+ .background {
137
+ position: absolute;
138
+ inset: 0;
139
+ background-color: var(--private-item-container-color);
140
+ opacity: var(--private-item-container-opacity, 1);
141
+ pointer-events: none;
142
+ border-start-start-radius: var(--private-item-container-shape-start-start, var(--private-item-container-shape));
143
+ border-start-end-radius: var(--private-item-container-shape-start-end, var(--private-item-container-shape));
144
+ border-end-start-radius: var(--private-item-container-shape-end-start, var(--private-item-container-shape));
145
+ border-end-end-radius: var(--private-item-container-shape-end-end, var(--private-item-container-shape));
146
+ corner-shape: var(--private-item-container-shape-variant);
147
+ }
131
148
 
132
- .ripple {
133
- @include mixin.apply-container-shape(private-item);
134
- --ripple-state-opacity: var(--private-item-container-state-opacity, 0);
135
- --ripple-pressed-color: var(--private-item-container-state-color);
136
- }
149
+ .focus-ring {
150
+ z-index: 2;
151
+ --focus-ring-container-shape-start-start: var(--private-item-container-shape-start-start, var(--private-item-container-shape));
152
+ --focus-ring-container-shape-start-end: var(--private-item-container-shape-start-end, var(--private-item-container-shape));
153
+ --focus-ring-container-shape-end-start: var(--private-item-container-shape-end-start, var(--private-item-container-shape));
154
+ --focus-ring-container-shape-end-end: var(--private-item-container-shape-end-end, var(--private-item-container-shape));
155
+ --focus-ring-container-shape-variant: var(--private-item-container-shape-variant);
137
156
  }
138
157
 
139
- .item {
140
- --private-item-container-color: var(--item-container-color);
141
- --private-item-label-text-color: var(--item-label-text-color);
142
- --private-item-leading-trailing-color: var(--item-leading-trailing-color);
143
- --private-item-supporting-text-color: var(--item-supporting-text-color);
158
+ .ripple {
159
+ border-start-start-radius: var(--private-item-container-shape-start-start, var(--private-item-container-shape));
160
+ border-start-end-radius: var(--private-item-container-shape-start-end, var(--private-item-container-shape));
161
+ border-end-start-radius: var(--private-item-container-shape-end-start, var(--private-item-container-shape));
162
+ border-end-end-radius: var(--private-item-container-shape-end-end, var(--private-item-container-shape));
163
+ corner-shape: var(--private-item-container-shape-variant);
164
+ --ripple-state-opacity: var(--private-item-container-state-opacity, 0);
165
+ --ripple-pressed-color: var(--private-item-container-state-color);
166
+ }
144
167
 
145
- --private-item-container-state-color: var(--private-item-label-text-color);
168
+ /* State management */
169
+ :host:hover:not([disabled], [selected]) {
170
+ --private-item-container-state-opacity: 0.08;
171
+ }
146
172
 
147
- &:hover:not(:where(.disabled, .selected)) {
148
- --private-item-container-state-opacity: 0.08;
149
- }
173
+ :host:has(.item.pressed):not([disabled]) {
174
+ --private-item-container-state-opacity: 0.12;
175
+ }
150
176
 
151
- &.pressed:not(:where(.disabled)) {
152
- --private-item-container-state-opacity: 0.12;
153
- }
177
+ :host([selected]) {
178
+ --private-item-container-color: var(--item-container-selected-color);
179
+ --private-item-label-text-color: var(--item-label-text-selected-color);
180
+ --private-item-leading-trailing-color: var(--item-label-text-selected-color);
181
+ --private-item-supporting-text-color: var(--item-label-text-selected-color);
182
+ }
154
183
 
155
- &.selected {
156
- --private-item-container-color: var(--item-container-selected-color);
157
- --private-item-label-text-color: var(--item-label-text-selected-color);
158
- --private-item-leading-trailing-color: var(--item-label-text-selected-color);
159
- --private-item-supporting-text-color: var(--item-label-text-selected-color);
160
- }
184
+ :host([disabled]) {
185
+ cursor: not-allowed;
186
+ --private-item-label-text-color: var(--color-on-surface);
187
+ --private-item-label-text-opacity: 0.38;
188
+ --private-item-leading-trailing-color: var(--color-on-surface);
189
+ --private-item-supporting-text-color: var(--color-on-surface);
190
+ --private-item-container-opacity: 0.12;
161
191
 
162
- &.disabled {
163
- cursor: not-allowed;
164
- --private-item-label-text-color: var(--color-on-surface);
165
- --private-item-label-text-opacity: 0.38;
166
- --private-item-leading-trailing-color: var(--color-on-surface);
167
- --private-item-supporting-text-color: var(--color-on-surface);
168
- --private-item-container-opacity: 0.12;
169
-
170
- .ripple {
171
- display: none;
172
- }
192
+ .ripple {
193
+ display: none;
173
194
  }
174
195
  }
175
-
176
- :host {
177
- --item-container-color: transparent;
178
- --item-label-text-color: var(--color-on-surface);
179
- --item-leading-trailing-color: var(--color-on-surface-variant);
180
- --item-supporting-text-color: var(--color-on-surface-variant);
181
-
182
- --item-container-selected-color: var(--color-tertiary-container);
183
- --item-label-text-selected-color: var(--color-on-tertiary-container);
184
- }