@crowdstrike/glide-core 0.30.1 → 0.31.1

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/dist/dropdown.js CHANGED
@@ -1556,8 +1556,8 @@ let Dropdown = class Dropdown extends LitElement {
1556
1556
  // Prevent page scroll. When filterable, also prevent the insertion point from
1557
1557
  // moving to the beginning of the field.
1558
1558
  event.preventDefault();
1559
- const firstOption = [...this.#optionElementsNotHiddenIncludingSelectAll]
1560
- .reverse()
1559
+ const firstOption = this.#optionElementsNotHiddenIncludingSelectAll
1560
+ .toReversed()
1561
1561
  .findLast((option) => !option.disabled);
1562
1562
  if (firstOption) {
1563
1563
  this.#previouslyActiveOption = this.activeOption;
package/dist/menu.js CHANGED
@@ -13,6 +13,7 @@ import { customElement, property } from 'lit/decorators.js';
13
13
  import packageJson from '../package.json' with { type: 'json' };
14
14
  import { LocalizeController } from './library/localize.js';
15
15
  import Options from './options.js';
16
+ import OptionsGroup from './options.group.js';
16
17
  import Option from './option.js';
17
18
  import Input from './input.js';
18
19
  import assertSlot from './library/assert-slot.js';
@@ -54,7 +55,7 @@ let Menu = class Menu extends LitElement {
54
55
  // `#onComponentFocusOut()` to decide if Menu should close. Also used in
55
56
  // `#onTargetAndDefaultSlotKeyDown()` to decide if we need to move focus.
56
57
  this.#hasVoiceOverMovedFocusToOptionsOrAnOption = false;
57
- // Set in `#onTargetSlotMouseUp()`. Used in `#onDocumentClick()` to guard against
58
+ // Set in `#onDefaultSlotMouseUp()`. Used in `#onDocumentClick()` to guard against
58
59
  // Menu closing when any number of things that are not an Option are clicked. Those
59
60
  // "click" events will be retargeted to Menu's host the moment they bubble out of
60
61
  // Menu. So checking in `#onDocumentClick()` if the click's `event.target` came
@@ -67,8 +68,8 @@ let Menu = class Menu extends LitElement {
67
68
  // used in `connectedCallback()` to guard against listening for document clicks for
68
69
  // sub-Menus.
69
70
  this.#isSubMenuOpen = false;
70
- // Set in `#onTargetSlotMouseUp()` and `#onDocumentClick()`. Used in
71
- // `#onDocumentClick()`:
71
+ // Similar situation as with `isDefaultSlotClick`. Set in `#onTargetSlotClick()`
72
+ // and `#onDocumentClick()`. Used in `#onDocumentClick()`:
72
73
  //
73
74
  // 1. Menu is open.
74
75
  // 2. User clicks Menu's target.
@@ -76,19 +77,9 @@ let Menu = class Menu extends LitElement {
76
77
  // 4. `#onTargetSlotClick()` sets `open` to true`.
77
78
  // 5. Menu never closes.
78
79
  //
79
- // Setting `#isTargetSlotMouseUp` to `true` in `#onTargetSlotMouseUp()` gives
80
+ // Setting `#isTargetSlotClick` to `true` in `#onTargetSlotClick()` gives
80
81
  // `#onDocumentClick()` the information it needs to not set `open` to `false`.
81
- //
82
- // The normal approach would be to set an `#isTargetSlotClick` property in
83
- // `#onTargetSlotClick()`. But `#onDocumentClick()` listens for "click" events in
84
- // their capture phase. So `#onDocumentClick()` would be called before
85
- // `#onTargetSlotClick()`.
86
- //
87
- // Note too that `#onDocumentClick()` sets `#isTargetSlotMouseUp` to `false`
88
- // instead of `#onTargetSlotClick()` doing it. That's so `#isTargetSlotMouseUp`
89
- // is set to `false` even if the user mouses down on Menu's target then moves the
90
- // mouse outside of Menu before mousing up.
91
- this.#isTargetSlotMouseUp = false;
82
+ this.#isTargetSlotClick = false;
92
83
  this.#localize = new LocalizeController(this);
93
84
  this.#targetSlotElementRef = createRef();
94
85
  // An arrow function field instead of a method so `this` is closed over and set to
@@ -98,8 +89,8 @@ let Menu = class Menu extends LitElement {
98
89
  this.#isDefaultSlotClick = false;
99
90
  return;
100
91
  }
101
- if (this.#isTargetSlotMouseUp) {
102
- this.#isTargetSlotMouseUp = false;
92
+ if (this.#isTargetSlotClick) {
93
+ this.#isTargetSlotClick = false;
103
94
  return;
104
95
  }
105
96
  if (this.#optionsElement) {
@@ -175,20 +166,7 @@ let Menu = class Menu extends LitElement {
175
166
  // easier to understand because developers don't have to think about sub-Menus when
176
167
  // looking at `#onDocumentClick()`.
177
168
  if (!this.#isSubMenu) {
178
- // 1. The consumer has a "click" handler on an element that isn't Menu's target.
179
- // 2. The user clicks that element.
180
- // 3. The handler is called. It sets `open` to `true`.
181
- // 4. The "click" bubbles up and is handled by `#onDocumentClick()`.
182
- // 5. `#onDocumentClick()` sets `open` to `false` because the click came from
183
- // outside Menu.
184
- // 6. Menu is opened then immediately closed and so never opens.
185
- //
186
- // `capture` ensures `#onDocumentClick()` is called before #3, so that `open` set
187
- // `true` in the consumer's handler isn't overwritten by `#onDocumentClick()`
188
- // handler setting it to `false`.
189
- document.addEventListener('click', this.#onDocumentClick, {
190
- capture: true,
191
- });
169
+ document.addEventListener('click', this.#onDocumentClick);
192
170
  }
193
171
  }
194
172
  createRenderRoot() {
@@ -197,9 +175,7 @@ let Menu = class Menu extends LitElement {
197
175
  }
198
176
  disconnectedCallback() {
199
177
  super.disconnectedCallback();
200
- document.removeEventListener('click', this.#onDocumentClick, {
201
- capture: true,
202
- });
178
+ document.removeEventListener('click', this.#onDocumentClick);
203
179
  }
204
180
  firstUpdated() {
205
181
  if (this.#optionsElement && this.#targetElement) {
@@ -293,7 +269,6 @@ let Menu = class Menu extends LitElement {
293
269
  name="target"
294
270
  @click=${this.#onTargetSlotClick}
295
271
  @keydown=${this.#onTargetAndDefaultSlotKeyDown}
296
- @mouseup=${this.#onTargetSlotMouseUp}
297
272
  @input=${this.#onTargetSlotInput}
298
273
  @slotchange=${this.#onTargetSlotChange}
299
274
  ${assertSlot([Element])}
@@ -341,7 +316,7 @@ let Menu = class Menu extends LitElement {
341
316
  // `#onComponentFocusOut()` to decide if Menu should close. Also used in
342
317
  // `#onTargetAndDefaultSlotKeyDown()` to decide if we need to move focus.
343
318
  #hasVoiceOverMovedFocusToOptionsOrAnOption;
344
- // Set in `#onTargetSlotMouseUp()`. Used in `#onDocumentClick()` to guard against
319
+ // Set in `#onDefaultSlotMouseUp()`. Used in `#onDocumentClick()` to guard against
345
320
  // Menu closing when any number of things that are not an Option are clicked. Those
346
321
  // "click" events will be retargeted to Menu's host the moment they bubble out of
347
322
  // Menu. So checking in `#onDocumentClick()` if the click's `event.target` came
@@ -354,8 +329,8 @@ let Menu = class Menu extends LitElement {
354
329
  // used in `connectedCallback()` to guard against listening for document clicks for
355
330
  // sub-Menus.
356
331
  #isSubMenuOpen;
357
- // Set in `#onTargetSlotMouseUp()` and `#onDocumentClick()`. Used in
358
- // `#onDocumentClick()`:
332
+ // Similar situation as with `isDefaultSlotClick`. Set in `#onTargetSlotClick()`
333
+ // and `#onDocumentClick()`. Used in `#onDocumentClick()`:
359
334
  //
360
335
  // 1. Menu is open.
361
336
  // 2. User clicks Menu's target.
@@ -363,19 +338,9 @@ let Menu = class Menu extends LitElement {
363
338
  // 4. `#onTargetSlotClick()` sets `open` to true`.
364
339
  // 5. Menu never closes.
365
340
  //
366
- // Setting `#isTargetSlotMouseUp` to `true` in `#onTargetSlotMouseUp()` gives
341
+ // Setting `#isTargetSlotClick` to `true` in `#onTargetSlotClick()` gives
367
342
  // `#onDocumentClick()` the information it needs to not set `open` to `false`.
368
- //
369
- // The normal approach would be to set an `#isTargetSlotClick` property in
370
- // `#onTargetSlotClick()`. But `#onDocumentClick()` listens for "click" events in
371
- // their capture phase. So `#onDocumentClick()` would be called before
372
- // `#onTargetSlotClick()`.
373
- //
374
- // Note too that `#onDocumentClick()` sets `#isTargetSlotMouseUp` to `false`
375
- // instead of `#onTargetSlotClick()` doing it. That's so `#isTargetSlotMouseUp`
376
- // is set to `false` even if the user mouses down on Menu's target then moves the
377
- // mouse outside of Menu before mousing up.
378
- #isTargetSlotMouseUp;
343
+ #isTargetSlotClick;
379
344
  #localize;
380
345
  #offset;
381
346
  // Used in various situations to reactivate the previously active Option.
@@ -435,7 +400,9 @@ let Menu = class Menu extends LitElement {
435
400
  .flatMap((element) => {
436
401
  return element instanceof HTMLSlotElement
437
402
  ? element.assignedElements({ flatten: true })
438
- : element;
403
+ : element instanceof OptionsGroup
404
+ ? [...element.children]
405
+ : element;
439
406
  })
440
407
  ?.filter((element) => {
441
408
  return element instanceof Option;
@@ -451,8 +418,14 @@ let Menu = class Menu extends LitElement {
451
418
  // The "content" slot case.
452
419
  ':scope > glide-core-options > glide-core-option > [slot="content"] > glide-core-menu'),
453
420
  ...this.querySelectorAll(
421
+ // The "content" slot and Options Group case.
422
+ ':scope > glide-core-options > glide-core-options-group > glide-core-option > [slot="content"] > glide-core-menu'),
423
+ ...this.querySelectorAll(
454
424
  // The "content" slot fallback case.
455
425
  ':scope > glide-core-options > glide-core-option > [slot="submenu"]'),
426
+ ...this.querySelectorAll(
427
+ // The "content" slot fallback and Options Group case.
428
+ ':scope > glide-core-options > glide-core-options-group > glide-core-option > [slot="submenu"]'),
456
429
  ];
457
430
  }
458
431
  get #targetElement() {
@@ -532,6 +505,15 @@ let Menu = class Menu extends LitElement {
532
505
  if (this.#isSubMenu && event.target === this.#defaultSlotElementRef.value) {
533
506
  event.stopPropagation();
534
507
  }
508
+ // When Menu is in the shadow DOM of another component, `event.target` will be
509
+ // retargeted to the host of that component the moment the event bubbles out of
510
+ // it.
511
+ //
512
+ // So, when the timeout callback below is called, `event.target` will have been
513
+ // retargeted and the `instanceof` check will wrongly fail when an Option is
514
+ // clicked, and Menu will never close. So we store a reference to the original
515
+ // `event.target` and use it in the `instanceof` condition.
516
+ const originalEventTarget = event.target;
535
517
  // The timeout gives consumers a chance to cancel the event to prevent Menu from
536
518
  // closing.
537
519
  setTimeout(() => {
@@ -546,7 +528,7 @@ let Menu = class Menu extends LitElement {
546
528
  //
547
529
  // When the default slot's padding is clicked, Menu should remain open because the
548
530
  // user most likely meant to click an Option but missed.
549
- if (!event.defaultPrevented && event.target instanceof Option) {
531
+ if (!event.defaultPrevented && originalEventTarget instanceof Option) {
550
532
  this.open = false;
551
533
  }
552
534
  });
@@ -1117,6 +1099,7 @@ let Menu = class Menu extends LitElement {
1117
1099
  }
1118
1100
  }
1119
1101
  #onTargetSlotClick(event) {
1102
+ this.#isTargetSlotClick = true;
1120
1103
  const closestOption = event.target instanceof Element &&
1121
1104
  event.target.closest('glide-core-option');
1122
1105
  const isSubMenuTarget = event.target instanceof Element && Boolean(closestOption);
@@ -1179,9 +1162,6 @@ let Menu = class Menu extends LitElement {
1179
1162
  #onTargetSlotInput() {
1180
1163
  this.open = true;
1181
1164
  }
1182
- #onTargetSlotMouseUp() {
1183
- this.#isTargetSlotMouseUp = true;
1184
- }
1185
1165
  #show() {
1186
1166
  this.#cleanUpFloatingUi?.();
1187
1167
  if (this.#previouslyActiveOption &&
@@ -15,9 +15,11 @@ export default [
15
15
  font-weight: var(--glide-core-typography-weight-regular);
16
16
  inline-size: 100%;
17
17
  max-inline-size: 21.875rem;
18
+ min-block-size: 1.75rem;
18
19
  padding-block: var(--glide-core-spacing-base-xxs);
19
20
  padding-inline: var(--glide-core-spacing-base-sm);
20
- transition: background-color var(--glide-core-duration-fast-02) ease-in-out;
21
+ transition: background-color var(--glide-core-duration-fast-02)
22
+ ease-in-out;
21
23
  user-select: none;
22
24
 
23
25
  &.active {
@@ -0,0 +1,30 @@
1
+ import { LitElement } from 'lit';
2
+ declare global {
3
+ interface HTMLElementTagNameMap {
4
+ 'glide-core-options-group': OptionsGroup;
5
+ }
6
+ }
7
+ /**
8
+ * @attr {string} label
9
+ * @attr {boolean} [hide-label=false]
10
+ * @attr {'group'|'optgroup'} [role='group']
11
+ *
12
+ * @readonly
13
+ * @attr {string} [version]
14
+ *
15
+ * @slot {Option}
16
+ */
17
+ export default class OptionsGroup extends LitElement {
18
+ #private;
19
+ static shadowRootOptions: ShadowRootInit;
20
+ static styles: import("lit").CSSResult[];
21
+ /**
22
+ * @default undefined
23
+ */
24
+ get label(): string | undefined;
25
+ set label(label: string);
26
+ hideLabel: boolean;
27
+ role: 'group' | 'optgroup';
28
+ readonly version: string;
29
+ render(): import("lit").TemplateResult<1>;
30
+ }
@@ -0,0 +1,84 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { html, LitElement } from 'lit';
8
+ import { customElement, property } from 'lit/decorators.js';
9
+ import { classMap } from 'lit/directives/class-map.js';
10
+ import packageJson from '../package.json' with { type: 'json' };
11
+ import styles from './options.group.styles.js';
12
+ import shadowRootMode from './library/shadow-root-mode.js';
13
+ import final from './library/final.js';
14
+ import assertSlot from './library/assert-slot.js';
15
+ import Option from './option.js';
16
+ import required from './library/required.js';
17
+ /**
18
+ * @attr {string} label
19
+ * @attr {boolean} [hide-label=false]
20
+ * @attr {'group'|'optgroup'} [role='group']
21
+ *
22
+ * @readonly
23
+ * @attr {string} [version]
24
+ *
25
+ * @slot {Option}
26
+ */
27
+ let OptionsGroup = class OptionsGroup extends LitElement {
28
+ constructor() {
29
+ super(...arguments);
30
+ this.hideLabel = false;
31
+ this.role = 'group';
32
+ this.version = packageJson.version;
33
+ }
34
+ static { this.shadowRootOptions = {
35
+ ...LitElement.shadowRootOptions,
36
+ mode: shadowRootMode,
37
+ }; }
38
+ static { this.styles = styles; }
39
+ /**
40
+ * @default undefined
41
+ */
42
+ get label() {
43
+ return this.#label;
44
+ }
45
+ set label(label) {
46
+ this.#label = label;
47
+ this.ariaLabel = label;
48
+ }
49
+ render() {
50
+ return html `
51
+ <div class="component">
52
+ <div
53
+ aria-hidden="true"
54
+ class=${classMap({ label: true, 'visually-hidden': this.hideLabel })}
55
+ >
56
+ ${this.label}
57
+ </div>
58
+
59
+ <slot ${assertSlot([Option])}>
60
+ <!-- @type {Option} -->
61
+ </slot>
62
+ </div>
63
+ `;
64
+ }
65
+ #label;
66
+ };
67
+ __decorate([
68
+ property({ reflect: true }),
69
+ required
70
+ ], OptionsGroup.prototype, "label", null);
71
+ __decorate([
72
+ property({ attribute: 'hide-label', reflect: true, type: Boolean })
73
+ ], OptionsGroup.prototype, "hideLabel", void 0);
74
+ __decorate([
75
+ property({ reflect: true })
76
+ ], OptionsGroup.prototype, "role", void 0);
77
+ __decorate([
78
+ property({ reflect: true })
79
+ ], OptionsGroup.prototype, "version", void 0);
80
+ OptionsGroup = __decorate([
81
+ customElement('glide-core-options-group'),
82
+ final
83
+ ], OptionsGroup);
84
+ export default OptionsGroup;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("lit").CSSResult[];
2
+ export default _default;
@@ -0,0 +1,31 @@
1
+ import { css } from 'lit';
2
+ import visuallyHidden from './styles/visually-hidden.js';
3
+ export default [
4
+ css `
5
+ ${visuallyHidden('.label.visually-hidden')}
6
+ `,
7
+ css `
8
+ :host(:not(:last-of-type)) .component::after {
9
+ background-color: var(--glide-core-color-static-stroke-secondary);
10
+ block-size: 1px;
11
+ border-radius: var(--glide-core-rounding-base-radius-sm);
12
+ content: '';
13
+ display: block;
14
+ margin-block: var(--glide-core-spacing-base-xxxs);
15
+ }
16
+
17
+ .label {
18
+ block-size: 1.75rem;
19
+ box-sizing: border-box;
20
+ font-family: var(--glide-core-typography-family-primary);
21
+ font-size: var(--glide-core-typography-size-body-small);
22
+ font-weight: var(--glide-core-typography-weight-bold);
23
+ max-inline-size: 21.875rem;
24
+ overflow: hidden;
25
+ padding-block: var(--glide-core-spacing-base-xxs);
26
+ padding-inline: var(--glide-core-spacing-base-sm);
27
+ text-overflow: ellipsis;
28
+ white-space: nowrap;
29
+ }
30
+ `,
31
+ ];
package/dist/options.js CHANGED
@@ -17,6 +17,7 @@ import final from './library/final.js';
17
17
  import uniqueId from './library/unique-id.js';
18
18
  import assertSlot from './library/assert-slot.js';
19
19
  import Option from './option.js';
20
+ import OptionsGroup from './options.group.js';
20
21
  // This component exists because Menu's target and its menu (`"role="menu"` or
21
22
  // `role="listbox"`) both need to be in the light DOM so the target and menu can
22
23
  // reference each other's IDs in ARIA attributes.
@@ -82,7 +83,7 @@ let Options = class Options extends LitElement {
82
83
  loading: this.privateLoading,
83
84
  })}
84
85
  @slotchange=${this.#onDefaultSlotChange}
85
- ${assertSlot([Option, Text], true)}
86
+ ${assertSlot([OptionsGroup, Option, Text], true)}
86
87
  >
87
88
  <!-- @type {Option | Text} -->
88
89
  </slot>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdstrike/glide-core",
3
- "version": "0.30.1",
3
+ "version": "0.31.1",
4
4
  "description": "A Web Component design system",
5
5
  "author": "CrowdStrike UX Team",
6
6
  "license": "Apache-2.0",
@@ -94,13 +94,13 @@
94
94
  "comment-parser": "^1.4.1",
95
95
  "custom-elements-manifest": "^2.1.0",
96
96
  "esbuild": "^0.25.0",
97
- "eslint": "^9.28.0",
97
+ "eslint": "^9.31.0",
98
98
  "eslint-config-prettier": "^10.1.5",
99
99
  "eslint-plugin-import": "^2.31.0",
100
100
  "eslint-plugin-lit": "^1.15.0",
101
101
  "eslint-plugin-lit-a11y": "^4.1.4",
102
102
  "eslint-plugin-sort-class-members": "^1.21.0",
103
- "eslint-plugin-unicorn": "^58.0.0",
103
+ "eslint-plugin-unicorn": "^60.0.0",
104
104
  "globals": "^15.13.0",
105
105
  "globby": "^14.0.2",
106
106
  "husky": "^8.0.3",