@crowdstrike/glide-core 0.31.0 → 0.31.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/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';
@@ -399,7 +400,9 @@ let Menu = class Menu extends LitElement {
399
400
  .flatMap((element) => {
400
401
  return element instanceof HTMLSlotElement
401
402
  ? element.assignedElements({ flatten: true })
402
- : element;
403
+ : element instanceof OptionsGroup
404
+ ? [...element.children]
405
+ : element;
403
406
  })
404
407
  ?.filter((element) => {
405
408
  return element instanceof Option;
@@ -415,8 +418,14 @@ let Menu = class Menu extends LitElement {
415
418
  // The "content" slot case.
416
419
  ':scope > glide-core-options > glide-core-option > [slot="content"] > glide-core-menu'),
417
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(
418
424
  // The "content" slot fallback case.
419
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"]'),
420
429
  ];
421
430
  }
422
431
  get #targetElement() {
@@ -496,6 +505,15 @@ let Menu = class Menu extends LitElement {
496
505
  if (this.#isSubMenu && event.target === this.#defaultSlotElementRef.value) {
497
506
  event.stopPropagation();
498
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;
499
517
  // The timeout gives consumers a chance to cancel the event to prevent Menu from
500
518
  // closing.
501
519
  setTimeout(() => {
@@ -510,7 +528,7 @@ let Menu = class Menu extends LitElement {
510
528
  //
511
529
  // When the default slot's padding is clicked, Menu should remain open because the
512
530
  // user most likely meant to click an Option but missed.
513
- if (!event.defaultPrevented && event.target instanceof Option) {
531
+ if (!event.defaultPrevented && originalEventTarget instanceof Option) {
514
532
  this.open = false;
515
533
  }
516
534
  });
@@ -553,59 +571,41 @@ let Menu = class Menu extends LitElement {
553
571
  event.preventDefault();
554
572
  }
555
573
  #onDefaultSlotMouseOver(event) {
556
- // Chrome has this funky Popover API bug where the popover is, for about 10
557
- // milliseconds, rendered above where it's supposed to be whenever `showPopover()`
558
- // is called.
559
- //
560
- // It's not clear if the popover is invisible during that time, or if it's visible
561
- // and 10 milliseconds isn't enough to be noticable. Either way, it's effectively
562
- // invisible but still picks up "mouseover" events.
563
- //
564
- // If the user's mouse happens to be over one of the Option(s) that's inside our
565
- // invisible, mispositioned popover, then the Option is activated. Thus this
566
- // variable and the guard below it.
567
- //
568
- // There's no mention of the above issue in it. But this bug is probably a good one
569
- // to keep an eye on: https://issues.chromium.org/issues/364669918.
570
- const isOutOfBounds = this.#componentElementRef.value &&
571
- event.y < this.#componentElementRef.value.getBoundingClientRect().y;
572
- if (!isOutOfBounds) {
573
- const option = event.target instanceof Element &&
574
- event.target.closest('glide-core-option');
575
- const isSubMenuTarget = event.target instanceof Element &&
576
- event.target.closest('[slot="target"]');
577
- const isOwnOption = option && this.#optionElements?.includes(option);
578
- // This handler is also called when a sub-Menu Option is hovered because sub-Menu
579
- // Option(s) are children of their super-Menu's default slot. And hovering a
580
- // sub-Menu Option shouldn't deactivate the super-Menu's active Option. Thus
581
- // `isOwnOption`.
582
- if (isOwnOption && !isSubMenuTarget && !option.disabled) {
583
- this.#previouslyActiveOption = this.#activeOption;
584
- if (this.#activeOption) {
585
- this.#activeOption.privateActive = false;
586
- }
587
- option.privateActive = true;
588
- if (this.#optionsElement) {
589
- this.#optionsElement.ariaActivedescendant = event.target.id;
590
- }
591
- }
592
- if (this.#isSubMenu) {
593
- // Allowing the event to propagate from a sub-Menu's parent Option means it would
594
- // get picked up by the super-Menu Option's Tooltip "mouseover" handler. Then it
595
- // would open the super-Menu's tooltip.
596
- event.stopPropagation();
574
+ const option = event.target instanceof Element &&
575
+ event.target.closest('glide-core-option');
576
+ const isSubMenuTarget = event.target instanceof Element &&
577
+ event.target.closest('[slot="target"]');
578
+ const isOwnOption = option && this.#optionElements?.includes(option);
579
+ // This handler is also called when a sub-Menu Option is hovered because sub-Menu
580
+ // Option(s) are children of their super-Menu's default slot. And hovering a
581
+ // sub-Menu Option shouldn't deactivate the super-Menu's active Option. Thus
582
+ // `isOwnOption`.
583
+ if (isOwnOption && !isSubMenuTarget && !option.disabled) {
584
+ this.#previouslyActiveOption = this.#activeOption;
585
+ if (this.#activeOption) {
586
+ this.#activeOption.privateActive = false;
597
587
  }
598
- if (isSubMenuTarget && this.#activeOption) {
599
- // When the cursor is already inside an Option and the user mouses to the Option's
600
- // sub-Menu target, the browser will dispatch "mouseout" followed by "mouseover".
601
- //
602
- // The Option's tooltip will pick up both events and will remain open because the
603
- // tooltip will be closed then immediately reopened. But we want the tooltip to
604
- // close when a sub-Menu target is hovered. Canceling the event stops the tooltip
605
- // from reopening.
606
- event.preventDefault();
588
+ option.privateActive = true;
589
+ if (this.#optionsElement) {
590
+ this.#optionsElement.ariaActivedescendant = event.target.id;
607
591
  }
608
592
  }
593
+ if (this.#isSubMenu) {
594
+ // Allowing the event to propagate from a sub-Menu's parent Option means it would
595
+ // get picked up by the super-Menu Option's Tooltip "mouseover" handler. Then it
596
+ // would open the super-Menu's tooltip.
597
+ event.stopPropagation();
598
+ }
599
+ if (isSubMenuTarget && this.#activeOption) {
600
+ // When the cursor is already inside an Option and the user mouses to the Option's
601
+ // sub-Menu target, the browser will dispatch "mouseout" followed by "mouseover".
602
+ //
603
+ // The Option's tooltip will pick up both events and will remain open because the
604
+ // tooltip will be closed then immediately reopened. But we want the tooltip to
605
+ // close when a sub-Menu target is hovered. Canceling the event stops the tooltip
606
+ // from reopening.
607
+ event.preventDefault();
608
+ }
609
609
  }
610
610
  #onDefaultSlotMouseUp() {
611
611
  this.#isDefaultSlotClick = true;
@@ -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.31.0",
3
+ "version": "0.31.2",
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",