@openvoxproject/voxblocks 0.14.0 → 0.15.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.
@@ -1,5 +1,6 @@
1
- import { html, css } from 'lit';
1
+ import { html, css, nothing } from 'lit';
2
2
  import { customElement, property, query } from 'lit/decorators.js';
3
+ import { ifDefined } from 'lit/directives/if-defined.js';
3
4
  import { VoxFieldElement, fieldStyles } from '../../internal/field.js';
4
5
 
5
6
  /**
@@ -12,11 +13,23 @@ import { VoxFieldElement, fieldStyles } from '../../internal/field.js';
12
13
  * <option value="rpm">RHEL</option>
13
14
  * </vox-select>
14
15
  * ```
16
+ *
17
+ * Add `multiple` for a list that accepts more than one selection; read the
18
+ * result from `values` rather than `value`.
15
19
  */
16
20
  @customElement('vox-select')
17
21
  export class VoxSelect extends VoxFieldElement {
18
22
  @property() value = '';
19
23
 
24
+ /** Accept more than one selection, rendering as a scrolling list box. */
25
+ @property({ type: Boolean, reflect: true }) multiple = false;
26
+
27
+ /** Rows shown when `multiple` is set. */
28
+ @property({ type: Number }) size?: number;
29
+
30
+ /** Selected values. Only meaningful when `multiple` is set. */
31
+ @property({ type: Array }) values: string[] = [];
32
+
20
33
  @query('select') private selectEl!: HTMLSelectElement;
21
34
 
22
35
  static styles = [
@@ -30,19 +43,48 @@ export class VoxSelect extends VoxFieldElement {
30
43
  background-position: right var(--vox-space-3) center;
31
44
  cursor: pointer;
32
45
  }
46
+
47
+ /* A list box has no collapsed affordance, so drop the chevron. */
48
+ :host([multiple]) select.control {
49
+ appearance: none;
50
+ padding-right: var(--vox-space-3);
51
+ background-image: none;
52
+ cursor: default;
53
+ }
54
+
55
+ :host([multiple]) select.control option {
56
+ padding: var(--vox-space-1) var(--vox-space-2);
57
+ }
33
58
  `,
34
59
  ];
35
60
 
36
61
  formResetCallback() {
37
62
  this.value = '';
63
+ this.values = [];
38
64
  this.syncOptions();
39
65
  }
40
66
 
41
67
  updated() {
42
- this.internals.setFormValue(this.value);
68
+ this.internals.setFormValue(this.multiple ? this.formData() : this.value);
43
69
  if (this.selectEl) this.syncValidity(this.selectEl);
44
70
  }
45
71
 
72
+ focus(options?: FocusOptions) {
73
+ this.selectEl?.focus(options);
74
+ }
75
+
76
+ /**
77
+ * A multi-select submits one entry per selection, which only FormData can
78
+ * express. Without a `name` there is nothing to key them on, so submit
79
+ * nothing — matching a native select with no name.
80
+ */
81
+ private formData(): FormData | null {
82
+ if (!this.name) return null;
83
+ const data = new FormData();
84
+ for (const value of this.values) data.append(this.name, value);
85
+ return data;
86
+ }
87
+
46
88
  private syncOptions() {
47
89
  if (!this.selectEl) return;
48
90
  const slot = this.renderRoot.querySelector('slot');
@@ -53,14 +95,34 @@ export class VoxSelect extends VoxFieldElement {
53
95
  .filter((el) => el instanceof HTMLOptionElement || el instanceof HTMLOptGroupElement)
54
96
  .map((el) => el.cloneNode(true)),
55
97
  );
98
+
99
+ if (this.multiple) {
100
+ const wanted = new Set(this.values);
101
+ for (const option of this.selectEl.options) {
102
+ option.selected = wanted.has(option.value);
103
+ }
104
+ this.values = this.selected();
105
+ return;
106
+ }
107
+
56
108
  if (this.value) {
57
109
  this.selectEl.value = this.value;
58
110
  }
59
111
  this.value = this.selectEl.value;
60
112
  }
61
113
 
114
+ private selected(): string[] {
115
+ return [...this.selectEl.selectedOptions].map((option) => option.value);
116
+ }
117
+
62
118
  private handleChange() {
63
- this.value = this.selectEl.value;
119
+ if (this.multiple) {
120
+ this.values = this.selected();
121
+ // Keep `value` meaningful as the first selection, as the native API does.
122
+ this.value = this.values[0] ?? '';
123
+ } else {
124
+ this.value = this.selectEl.value;
125
+ }
64
126
  this.dispatchEvent(new Event('change', { bubbles: true }));
65
127
  }
66
128
 
@@ -71,8 +133,11 @@ export class VoxSelect extends VoxFieldElement {
71
133
  <select
72
134
  id="select"
73
135
  class="control"
136
+ ?multiple=${this.multiple}
137
+ size=${ifDefined(this.multiple ? (this.size ?? 4) : undefined)}
74
138
  ?required=${this.required}
75
139
  ?disabled=${this.disabled}
140
+ aria-label=${this.label ? nothing : 'options'}
76
141
  @change=${this.handleChange}
77
142
  ></select>
78
143
  ${this.renderNote()}
package/src/index.ts CHANGED
@@ -12,11 +12,13 @@ export { VoxIcon } from './components/icon/vox-icon.js';
12
12
 
13
13
  // Forms
14
14
  export { VoxCheckbox } from './components/checkbox/vox-checkbox.js';
15
+ export { VoxCombobox } from './components/combobox/vox-combobox.js';
15
16
  export { VoxFileInput } from './components/file-input/vox-file-input.js';
16
17
  export { VoxInput } from './components/input/vox-input.js';
17
18
  export { VoxInputGroup } from './components/input-group/vox-input-group.js';
18
19
  export { VoxRadio } from './components/radio/vox-radio.js';
19
20
  export { VoxRadioGroup } from './components/radio/vox-radio-group.js';
21
+ export { VoxRange } from './components/range/vox-range.js';
20
22
  export { VoxSelect } from './components/select/vox-select.js';
21
23
  export { VoxSwitch } from './components/switch/vox-switch.js';
22
24
  export { VoxTextarea } from './components/textarea/vox-textarea.js';
@@ -38,7 +40,6 @@ export { VoxToc, VoxTocItem } from './components/toc/vox-toc.js';
38
40
  export { VoxDialog } from './components/dialog/vox-dialog.js';
39
41
  export { VoxDisclosure } from './components/disclosure/vox-disclosure.js';
40
42
  export { VoxDropdown } from './components/dropdown/vox-dropdown.js';
41
- export { VoxMenu } from './components/menu/vox-menu.js';
42
43
 
43
44
  // Page content
44
45
  export { VoxAccordion, VoxAccordionItem } from './components/accordion/vox-accordion.js';
@@ -47,8 +48,8 @@ export { VoxBadge } from './components/badge/vox-badge.js';
47
48
  export { VoxCalendarTile } from './components/calendar-tile/vox-calendar-tile.js';
48
49
  export { VoxCallout } from './components/callout/vox-callout.js';
49
50
  export { VoxCard } from './components/card/vox-card.js';
51
+ export { VoxCodeBlock } from './components/code-block/vox-code-block.js';
50
52
  export { VoxCtaBand } from './components/cta-band/vox-cta-band.js';
51
- export { VoxDatum } from './components/datum/vox-datum.js';
52
53
  export { VoxEmptyState } from './components/empty-state/vox-empty-state.js';
53
54
  export { VoxFooter, VoxFooterColumn } from './components/footer/vox-footer.js';
54
55
  export { VoxGrid } from './components/grid/vox-grid.js';
@@ -57,7 +58,6 @@ export { VoxLinkHub, VoxLinkHubItem } from './components/link-hub/vox-link-hub.j
57
58
  export { VoxLoader } from './components/loader/vox-loader.js';
58
59
  export { VoxPagination } from './components/pagination/vox-pagination.js';
59
60
  export { VoxQuote } from './components/quote/vox-quote.js';
60
- export { VoxRecordList, VoxRecordListItem } from './components/record-list/vox-record-list.js';
61
61
  export { VoxSponsor, VoxSponsorTier } from './components/sponsor/vox-sponsor.js';
62
62
  export { VoxStat } from './components/stat/vox-stat.js';
63
63
  export { VoxStepIndicator, VoxStep } from './components/step-indicator/vox-step-indicator.js';
@@ -67,10 +67,9 @@ export { VoxTimeline, VoxTimelineItem } from './components/timeline/vox-timeline
67
67
  export type { IconName, IconSize } from './components/icon/vox-icon.js';
68
68
  export type { ButtonVariant, ButtonSize } from './components/button/vox-button.js';
69
69
  export type { CalloutVariant } from './components/callout/vox-callout.js';
70
+ export type { CodeBlockLanguage } from './components/code-block/vox-code-block.js';
70
71
  export type { BadgeVariant } from './components/badge/vox-badge.js';
71
72
  export type { AlertVariant } from './components/alert/vox-alert.js';
72
73
  export type { AvatarSize } from './components/avatar/vox-avatar.js';
73
74
  export type { LoaderSize } from './components/loader/vox-loader.js';
74
75
  export type { StepState } from './components/step-indicator/vox-step-indicator.js';
75
- export type { MenuPlacement } from './components/menu/vox-menu.js';
76
- export type { RecordListItemSize } from './components/record-list/vox-record-list.js';
@@ -33,6 +33,15 @@
33
33
  --vox-color-danger-3: #e0575b;
34
34
  --vox-color-danger-soft: rgba(244, 63, 94, 0.14);
35
35
 
36
+ /* Code syntax highlighting — one palette hue per token role */
37
+ --vox-code-comment: var(--vox-palette-gray-700);
38
+ --vox-code-keyword: var(--vox-palette-purple-700);
39
+ --vox-code-string: var(--vox-palette-green-800);
40
+ --vox-code-number: var(--vox-palette-gold-800);
41
+ --vox-code-function: var(--vox-palette-blue-700);
42
+ --vox-code-property: var(--vox-palette-teal-800);
43
+ --vox-code-tag: var(--vox-palette-red-700);
44
+
36
45
  /* Surfaces */
37
46
  --vox-color-bg: #f5faf9;
38
47
  --vox-color-bg-alt: #e8f1ef;
@@ -96,6 +105,14 @@
96
105
  --vox-color-danger-3: #b62a3c;
97
106
  --vox-color-danger-soft: rgba(244, 63, 94, 0.16);
98
107
 
108
+ --vox-code-comment: var(--vox-palette-gray-400);
109
+ --vox-code-keyword: var(--vox-palette-purple-300);
110
+ --vox-code-string: var(--vox-palette-green-400);
111
+ --vox-code-number: var(--vox-palette-gold-400);
112
+ --vox-code-function: var(--vox-palette-blue-300);
113
+ --vox-code-property: var(--vox-palette-teal-400);
114
+ --vox-code-tag: var(--vox-palette-red-300);
115
+
99
116
  --vox-color-bg: #0d1417;
100
117
  --vox-color-bg-alt: #131b1f;
101
118
  --vox-color-bg-soft: #131b1f;
@@ -1,25 +0,0 @@
1
- import { LitElement } from 'lit';
2
- /**
3
- * A small labeled value with an optional icon — an owner, a date, a
4
- * target host, anything that's "icon + value" — for use inside record
5
- * rows, cards, or wherever a compact data point is needed.
6
- *
7
- * `name` isn't shown visually (the icon, if any, carries that meaning for
8
- * sighted users); it's exposed to assistive tech as a spoken label, e.g.
9
- * "User: J. Smith".
10
- *
11
- * @slot icon - Optional icon before the value, e.g. `<vox-icon size="sm">`.
12
- * @slot - The value.
13
- */
14
- export declare class VoxDatum extends LitElement {
15
- name: string;
16
- static styles: import("lit").CSSResult;
17
- private hasIcon;
18
- private handleIconSlotChange;
19
- render(): import("lit-html").TemplateResult<1>;
20
- }
21
- declare global {
22
- interface HTMLElementTagNameMap {
23
- 'vox-datum': VoxDatum;
24
- }
25
- }
@@ -1,44 +0,0 @@
1
- import { LitElement } from 'lit';
2
- export type MenuPlacement = 'bottom-start' | 'bottom-end';
3
- /**
4
- * An overlay menu anchored to an arbitrary trigger — an avatar, an icon
5
- * button, anything — unlike `<vox-dropdown>`, which owns a fixed
6
- * label+chevron trigger of its own.
7
- *
8
- * ```html
9
- * <vox-menu label="Account menu">
10
- * <vox-avatar slot="trigger" initials="UN"></vox-avatar>
11
- * <a href="/profile">Profile</a>
12
- * <a href="/settings">Settings</a>
13
- * <hr />
14
- * <button type="button">Log out</button>
15
- * </vox-menu>
16
- * ```
17
- *
18
- * @slot trigger - Content shown inside the trigger button, e.g. a `<vox-avatar>`.
19
- * @slot - Menu entries (`<a>`, `<button>`, `<hr>` for separators).
20
- * @fires vox-close - When the menu closes (Escape, outside click, or an entry click).
21
- */
22
- export declare class VoxMenu extends LitElement {
23
- /**
24
- * Accessible name for the trigger button. Set this when the `trigger`
25
- * slot has no visible text of its own (e.g. an avatar-only trigger);
26
- * leave unset when it does, so that text stays the accessible name.
27
- */
28
- label?: string;
29
- placement: MenuPlacement;
30
- open: boolean;
31
- static styles: import("lit").CSSResult;
32
- connectedCallback(): void;
33
- disconnectedCallback(): void;
34
- private handleOutsideClick;
35
- private handleKeydown;
36
- private toggle;
37
- private close;
38
- render(): import("lit-html").TemplateResult<1>;
39
- }
40
- declare global {
41
- interface HTMLElementTagNameMap {
42
- 'vox-menu': VoxMenu;
43
- }
44
- }
@@ -1,53 +0,0 @@
1
- import { LitElement } from 'lit';
2
- export type RecordListItemSize = 'md' | 'sm';
3
- /**
4
- * A list of linked records — search results, an admin index, anything
5
- * that's a stack of "go to this thing" rows. A grid (heading, meta, end,
6
- * plus a trailing filler column) is always defined here; regular
7
- * `size="md"` items ignore the first three tracks and just render their own
8
- * full-width flex row, while `size="sm"` items opt into them via `subgrid`
9
- * so their columns line up into a real table across every row, sized to
10
- * each column's widest cell like a native `<table>`. The filler column
11
- * absorbs whatever width the three content columns don't use, so every
12
- * item — table row or not — still spans the list's full width instead of
13
- * shrinking to fit its content.
14
- *
15
- * @slot - `<vox-record-list-item>` elements.
16
- */
17
- export declare class VoxRecordList extends LitElement {
18
- static styles: import("lit").CSSResult;
19
- render(): import("lit-html").TemplateResult<1>;
20
- }
21
- /**
22
- * One row in a `<vox-record-list>`: a heading link plus optional metadata
23
- * — pass `<vox-datum>` elements (or anything else) into the default slot.
24
- * Becomes fully navigable (heading and trailing arrow both link to `href`)
25
- * when `href` is set; otherwise renders as a static row.
26
- *
27
- * At `size="sm"`, the arrow drops out and heading/meta/`end` become real
28
- * table columns (via CSS `subgrid`, aligned against every other
29
- * `size="sm"` row in the same `<vox-record-list>`) — suited to a dense
30
- * activity/run log rather than an indexed record. `heading` and the meta
31
- * column each link to `href` individually, since a single grid row can't
32
- * itself be one native link the way a block row can.
33
- *
34
- * @slot - Metadata, e.g. `<vox-datum>` elements.
35
- * @slot end - Trailing content after the meta, e.g. a `<vox-badge>` status.
36
- */
37
- export declare class VoxRecordListItem extends LitElement {
38
- heading: string;
39
- href?: string;
40
- size: RecordListItemSize;
41
- static styles: import("lit").CSSResult;
42
- private hasMeta;
43
- private hasEnd;
44
- private handleMetaSlotChange;
45
- private handleEndSlotChange;
46
- render(): import("lit-html").TemplateResult<1>;
47
- }
48
- declare global {
49
- interface HTMLElementTagNameMap {
50
- 'vox-record-list': VoxRecordList;
51
- 'vox-record-list-item': VoxRecordListItem;
52
- }
53
- }
@@ -1,76 +0,0 @@
1
- import { LitElement, html, css, nothing } from 'lit';
2
- import { customElement, property } from 'lit/decorators.js';
3
-
4
- /**
5
- * A small labeled value with an optional icon — an owner, a date, a
6
- * target host, anything that's "icon + value" — for use inside record
7
- * rows, cards, or wherever a compact data point is needed.
8
- *
9
- * `name` isn't shown visually (the icon, if any, carries that meaning for
10
- * sighted users); it's exposed to assistive tech as a spoken label, e.g.
11
- * "User: J. Smith".
12
- *
13
- * @slot icon - Optional icon before the value, e.g. `<vox-icon size="sm">`.
14
- * @slot - The value.
15
- */
16
- @customElement('vox-datum')
17
- export class VoxDatum extends LitElement {
18
- @property() name = '';
19
-
20
- static styles = css`
21
- :host {
22
- display: inline-flex;
23
- align-items: center;
24
- gap: var(--vox-space-1);
25
- font-family: var(--vox-font-family-base);
26
- font-size: 13px;
27
- color: var(--vox-color-text-2);
28
- }
29
-
30
- .icon {
31
- display: flex;
32
- flex: none;
33
- color: var(--vox-color-text-3);
34
- }
35
-
36
- .icon:not(.has-icon) {
37
- display: none;
38
- }
39
-
40
- .sr-only {
41
- position: absolute;
42
- width: 1px;
43
- height: 1px;
44
- padding: 0;
45
- margin: -1px;
46
- overflow: hidden;
47
- clip: rect(0, 0, 0, 0);
48
- white-space: nowrap;
49
- border: 0;
50
- }
51
- `;
52
-
53
- private hasIcon = false;
54
-
55
- private handleIconSlotChange(event: Event) {
56
- const slot = event.target as HTMLSlotElement;
57
- this.hasIcon = slot.assignedNodes({ flatten: true }).length > 0;
58
- this.requestUpdate();
59
- }
60
-
61
- render() {
62
- return html`
63
- <span class="icon ${this.hasIcon ? 'has-icon' : ''}">
64
- <slot name="icon" @slotchange=${this.handleIconSlotChange}></slot>
65
- </span>
66
- ${this.name ? html`<span class="sr-only">${this.name}: </span>` : nothing}
67
- <slot></slot>
68
- `;
69
- }
70
- }
71
-
72
- declare global {
73
- interface HTMLElementTagNameMap {
74
- 'vox-datum': VoxDatum;
75
- }
76
- }
@@ -1,203 +0,0 @@
1
- import { LitElement, html, css, nothing } from 'lit';
2
- import { customElement, property } from 'lit/decorators.js';
3
-
4
- export type MenuPlacement = 'bottom-start' | 'bottom-end';
5
-
6
- /**
7
- * An overlay menu anchored to an arbitrary trigger — an avatar, an icon
8
- * button, anything — unlike `<vox-dropdown>`, which owns a fixed
9
- * label+chevron trigger of its own.
10
- *
11
- * ```html
12
- * <vox-menu label="Account menu">
13
- * <vox-avatar slot="trigger" initials="UN"></vox-avatar>
14
- * <a href="/profile">Profile</a>
15
- * <a href="/settings">Settings</a>
16
- * <hr />
17
- * <button type="button">Log out</button>
18
- * </vox-menu>
19
- * ```
20
- *
21
- * @slot trigger - Content shown inside the trigger button, e.g. a `<vox-avatar>`.
22
- * @slot - Menu entries (`<a>`, `<button>`, `<hr>` for separators).
23
- * @fires vox-close - When the menu closes (Escape, outside click, or an entry click).
24
- */
25
- @customElement('vox-menu')
26
- export class VoxMenu extends LitElement {
27
- /**
28
- * Accessible name for the trigger button. Set this when the `trigger`
29
- * slot has no visible text of its own (e.g. an avatar-only trigger);
30
- * leave unset when it does, so that text stays the accessible name.
31
- */
32
- @property() label?: string;
33
- @property({ reflect: true }) placement: MenuPlacement = 'bottom-end';
34
- @property({ type: Boolean, reflect: true }) open = false;
35
-
36
- static styles = css`
37
- :host {
38
- position: relative;
39
- display: inline-block;
40
- /* Without this, a flex/grid container's default stretch alignment
41
- grows the host to fill the cross axis (e.g. a tall sibling, or a
42
- container given a min-height for layout purposes) while the
43
- trigger button inside stays content-sized — and since the menu's
44
- "top: 100%" is measured against the host's own box, it then opens
45
- far below the trigger instead of right under it. */
46
- align-self: flex-start;
47
- font-family: var(--vox-font-family-base);
48
- }
49
-
50
- .trigger {
51
- display: inline-flex;
52
- align-items: center;
53
- background: none;
54
- border: none;
55
- padding: 0;
56
- border-radius: var(--vox-radius-md);
57
- color: inherit;
58
- font: inherit;
59
- cursor: pointer;
60
- }
61
-
62
- .trigger:focus-visible {
63
- outline: 2px solid var(--vox-color-brand-1);
64
- outline-offset: 2px;
65
- }
66
-
67
- .menu {
68
- position: absolute;
69
- top: calc(100% + 4px);
70
- z-index: 10;
71
- min-width: 180px;
72
- display: none;
73
- flex-direction: column;
74
- padding: var(--vox-space-2);
75
- background-color: var(--vox-color-bg-elv);
76
- border: 1px solid var(--vox-color-divider);
77
- border-radius: var(--vox-radius-md);
78
- box-shadow: var(--vox-shadow-2);
79
- }
80
-
81
- :host([placement='bottom-start']) .menu {
82
- left: 0;
83
- }
84
-
85
- :host([placement='bottom-end']) .menu {
86
- right: 0;
87
- }
88
-
89
- :host([open]) .menu {
90
- display: flex;
91
- }
92
-
93
- ::slotted(a),
94
- ::slotted(button) {
95
- display: block;
96
- width: 100%;
97
- box-sizing: border-box;
98
- padding: var(--vox-space-2) var(--vox-space-3);
99
- background: none;
100
- border: none;
101
- border-radius: var(--vox-radius-sm);
102
- color: var(--vox-color-text-1);
103
- font-family: inherit;
104
- font-size: 14px;
105
- text-align: left;
106
- text-decoration: none;
107
- cursor: pointer;
108
- white-space: nowrap;
109
- }
110
-
111
- ::slotted(a:hover),
112
- ::slotted(button:hover),
113
- ::slotted(a:focus-visible),
114
- ::slotted(button:focus-visible) {
115
- background-color: var(--vox-color-brand-soft);
116
- color: var(--vox-color-brand-1);
117
- outline: none;
118
- }
119
-
120
- ::slotted(hr) {
121
- width: 100%;
122
- margin: var(--vox-space-1) 0;
123
- border: none;
124
- border-top: 1px solid var(--vox-color-divider);
125
- }
126
- `;
127
-
128
- connectedCallback() {
129
- super.connectedCallback();
130
- document.addEventListener('click', this.handleOutsideClick);
131
- this.addEventListener('keydown', this.handleKeydown);
132
- }
133
-
134
- disconnectedCallback() {
135
- super.disconnectedCallback();
136
- document.removeEventListener('click', this.handleOutsideClick);
137
- this.removeEventListener('keydown', this.handleKeydown);
138
- }
139
-
140
- private handleOutsideClick = (event: MouseEvent) => {
141
- if (this.open && !event.composedPath().includes(this)) {
142
- this.close();
143
- }
144
- };
145
-
146
- private handleKeydown = (event: KeyboardEvent) => {
147
- if (event.key === 'Escape' && this.open) {
148
- this.close();
149
- this.renderRoot.querySelector<HTMLElement>('.trigger')?.focus();
150
- return;
151
- }
152
-
153
- if ((event.key === 'ArrowDown' || event.key === 'ArrowUp') && this.open) {
154
- event.preventDefault();
155
- const items = [...this.querySelectorAll<HTMLElement>('a, button')].filter(
156
- (el) => el.slot !== 'trigger',
157
- );
158
- if (items.length === 0) return;
159
- const active = document.activeElement as HTMLElement;
160
- const index = items.indexOf(active);
161
- const delta = event.key === 'ArrowDown' ? 1 : -1;
162
- const next = items[(Math.max(index, 0) + delta + items.length) % items.length];
163
- next.focus();
164
- }
165
- };
166
-
167
- private toggle() {
168
- if (this.open) {
169
- this.close();
170
- } else {
171
- this.open = true;
172
- }
173
- }
174
-
175
- private close() {
176
- if (!this.open) return;
177
- this.open = false;
178
- this.dispatchEvent(new CustomEvent('vox-close', { bubbles: true, composed: true }));
179
- }
180
-
181
- render() {
182
- return html`
183
- <button
184
- class="trigger"
185
- aria-expanded=${this.open ? 'true' : 'false'}
186
- aria-haspopup="true"
187
- aria-label=${this.label ?? nothing}
188
- @click=${this.toggle}
189
- >
190
- <slot name="trigger"></slot>
191
- </button>
192
- <div class="menu" role="menu">
193
- <slot @click=${() => this.close()}></slot>
194
- </div>
195
- `;
196
- }
197
- }
198
-
199
- declare global {
200
- interface HTMLElementTagNameMap {
201
- 'vox-menu': VoxMenu;
202
- }
203
- }