@timmo001/groundwork-ui 0.9.2 → 0.9.4
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/custom-elements.json +69 -4
- package/package.json +1 -1
- package/src/gw-button.ts +20 -1
- package/src/gw-menu-button.ts +131 -9
package/custom-elements.json
CHANGED
|
@@ -3613,7 +3613,7 @@
|
|
|
3613
3613
|
"type": {
|
|
3614
3614
|
"text": "\"button\"|\"submit\"|\"reset\""
|
|
3615
3615
|
},
|
|
3616
|
-
"description": "
|
|
3616
|
+
"description": "What a click does: nothing more, or submit or reset the form the button is in. Defaults to `button`.",
|
|
3617
3617
|
"name": "type"
|
|
3618
3618
|
},
|
|
3619
3619
|
{
|
|
@@ -10318,7 +10318,7 @@
|
|
|
10318
10318
|
"declarations": [
|
|
10319
10319
|
{
|
|
10320
10320
|
"kind": "class",
|
|
10321
|
-
"description": "A button that opens a menu on the Popover API, so it shows in the top\nlayer and closes on Esc or a click outside. Set `items`; arrow keys, Home\nand End move between them, and choosing one fires `select` with its\n`value` and closes the menu.\n\nContextual actions, such as \"Add a schedule\", go in `actions`: they stay\nbelow the items as the list scrolls, so they're found without scrolling to\nthe end, and with no items the menu says `empty` above them.\n\nItems render inside the component, so a page's own button styles can't\nreach them.",
|
|
10321
|
+
"description": "A button that opens a menu on the Popover API, so it shows in the top\nlayer and closes on Esc or a click outside. Set `items`; arrow keys, Home\nand End move between them, and choosing one fires `select` with its\n`value` and closes the menu.\n\nContextual actions, such as \"Add a schedule\", go in `actions`: they stay\nbelow the items as the list scrolls, so they're found without scrolling to\nthe end, and with no items the menu says `empty` above them.\n\nFor a menu of choices, such as sort and order, give each item `checked`:\nthe picked one shows a tick, and `separator` starts the next group.\n\nItems render inside the component, so a page's own button styles can't\nreach them.",
|
|
10322
10322
|
"name": "GwMenuButton",
|
|
10323
10323
|
"cssProperties": [
|
|
10324
10324
|
{
|
|
@@ -10344,7 +10344,7 @@
|
|
|
10344
10344
|
"text": "Properties"
|
|
10345
10345
|
},
|
|
10346
10346
|
"static": true,
|
|
10347
|
-
"default": "{ label: {}, icon: {}, empty: {}, items: { type: \"json\" }, actions: { type: \"json\" }, size: { reflect: true }, }",
|
|
10347
|
+
"default": "{ label: {}, heading: {}, icon: {}, empty: {}, items: { type: \"json\" }, actions: { type: \"json\" }, size: { reflect: true }, active: { type: \"boolean\", reflect: true }, labels: { reflect: true }, }",
|
|
10348
10348
|
"inheritedFrom": {
|
|
10349
10349
|
"name": "GwElement",
|
|
10350
10350
|
"module": "src/element.ts"
|
|
@@ -10357,7 +10357,7 @@
|
|
|
10357
10357
|
"text": "readonly CSSStyleSheet[]"
|
|
10358
10358
|
},
|
|
10359
10359
|
"static": true,
|
|
10360
|
-
"default": "[ base, controlSizes(\"menu-button\"), css` :host { position: relative; display: inline-block; anchor-scope: --gw-menu; } .trigger { --gw-size: var(--gw-menu-button-scale, 1); --gw-scale: calc(var(--gw-size) * var(--gw-control-scale, 1)); --gw-icon-size: calc(1.25rem * var(--gw-scale)); display: inline-flex; gap: calc(var(--gw-space-2) * var(--gw-size)); align-items: center; box-sizing: border-box; height: calc(2.5rem * var(--gw-scale)); padding: 0 calc(var(--gw-space-4) * var(--gw-size)); font: inherit; font-size: calc(var(--gw-font-size-s) * var(--gw-size)); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); cursor: pointer; background: var(--gw-color-fill-neutral-resting); border: var(--gw-border-width) solid var(--gw-border-color); border-radius: var(--gw-radius-control); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); anchor-name: --gw-menu; } .trigger:hover { background: var(--gw-color-fill-neutral-hovered); } .chevron { width: 0.4rem; height: 0.4rem; margin-inline-start: var(--gw-space-1); border-inline-end: 1.5px solid currentColor; border-block-end: 1.5px solid currentColor; rotate: 45deg; translate: 0 -0.125rem; transition: rotate var(--gw-motion-normal) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing); } .trigger[aria-expanded=\"true\"] .chevron { rotate: 225deg; translate: 0 0.125rem; } [popover] { position-anchor: --gw-menu; position-try-fallbacks: flip-block, flip-inline; inset: auto; top: anchor(bottom); left: anchor(left); box-sizing: border-box; min-width: 15rem; max-height: min(24rem, 60vh); padding: 0; margin: var(--gw-space-1) 0 0; overflow: hidden; color: var(--gw-color-text-primary); background: var(--gw-color-surface-overlay); border: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-l); box-shadow: var(--gw-shadow-m); opacity: 0; translate: 0 -0.25rem; transition: opacity var(--gw-motion-fast) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing), display var(--gw-motion-normal) allow-discrete, overlay var(--gw-motion-normal) allow-discrete; } [popover]:popover-open { display: flex; flex-direction: column; opacity: 1; translate: 0 0; } @starting-style { [popover]:popover-open { opacity: 0; translate: 0 -0.25rem; } } /* The items scroll; the actions stay put below them, so \"Add\" is always in reach rather than at the end of a long list */ .items, .actions { display: flex; flex-direction: column; gap: var(--gw-space-0-5); padding: var(--gw-space-1); } .items { flex: 1; min-height: 0; overflow-y: auto; } .actions { border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .empty { padding: var(--gw-space-3); margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } .item { display: flex; gap: var(--gw-space-3); align-items: center; box-sizing: border-box; min-height: 2.5rem; padding: var(--gw-space-2) var(--gw-space-3); font: inherit; font-size: var(--gw-font-size-s); color: var(--gw-color-text-primary); text-align: start; cursor: pointer; background: transparent; border: 0; border-radius: var(--gw-radius-m); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); --gw-icon-size: 1.125rem; } .item:hover, .item:focus-visible { background: var(--gw-color-fill-neutral-hovered); outline: none; } .item:focus-visible { box-shadow: inset 0 0 0 var(--gw-focus-width) var(--gw-color-focus); } .item:disabled { color: var(--gw-color-text-disabled); cursor: not-allowed; background: transparent; } .action { font-weight: var(--gw-font-weight-strong); color: var(--gw-color-accent); } .text { display: flex; flex-direction: column; gap: var(--gw-space-0-5); } .description { font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } `, ]",
|
|
10360
|
+
"default": "[ base, controlSizes(\"menu-button\"), css` :host { position: relative; display: inline-block; anchor-scope: --gw-menu; } .trigger { --gw-size: var(--gw-menu-button-scale, 1); --gw-scale: calc(var(--gw-size) * var(--gw-control-scale, 1)); --gw-icon-size: calc(1.25rem * var(--gw-scale)); display: inline-flex; gap: calc(var(--gw-space-2) * var(--gw-size)); align-items: center; box-sizing: border-box; height: calc(2.5rem * var(--gw-scale)); padding: 0 calc(var(--gw-space-4) * var(--gw-size)); font: inherit; font-size: calc(var(--gw-font-size-s) * var(--gw-size)); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); cursor: pointer; background: var(--gw-color-fill-neutral-resting); border: var(--gw-border-width) solid var(--gw-border-color); border-radius: var(--gw-radius-control); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); anchor-name: --gw-menu; } .trigger:hover { background: var(--gw-color-fill-neutral-hovered); } :host([active]) .trigger { color: var(--gw-color-accent); border-color: var(--gw-color-accent); } /* Icon only, square, with the text kept for assistive technology */ :host([labels=\"hide\"]) .trigger.has-icon { padding: 0 calc(var(--gw-space-2) * var(--gw-size)); } :host([labels=\"hide\"]) .has-icon .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } @media (width < 40rem) { :host([labels=\"auto\"]) .trigger.has-icon { padding: 0 calc(var(--gw-space-2) * var(--gw-size)); } :host([labels=\"auto\"]) .has-icon .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } } .chevron { width: 0.4rem; height: 0.4rem; margin-inline-start: var(--gw-space-1); border-inline-end: 1.5px solid currentColor; border-block-end: 1.5px solid currentColor; rotate: 45deg; translate: 0 -0.125rem; transition: rotate var(--gw-motion-normal) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing); } .trigger[aria-expanded=\"true\"] .chevron { rotate: 225deg; translate: 0 0.125rem; } [popover] { position-anchor: --gw-menu; position-try-fallbacks: flip-block, flip-inline; inset: auto; top: anchor(bottom); left: anchor(left); box-sizing: border-box; min-width: 15rem; max-height: min(24rem, 60vh); padding: 0; margin: var(--gw-space-1) 0 0; overflow: hidden; color: var(--gw-color-text-primary); background: var(--gw-color-surface-overlay); border: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-l); box-shadow: var(--gw-shadow-m); opacity: 0; translate: 0 -0.25rem; transition: opacity var(--gw-motion-fast) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing), display var(--gw-motion-normal) allow-discrete, overlay var(--gw-motion-normal) allow-discrete; } [popover]:popover-open { display: flex; flex-direction: column; opacity: 1; translate: 0 0; } @starting-style { [popover]:popover-open { opacity: 0; translate: 0 -0.25rem; } } /* The items scroll; the actions stay put below them, so \"Add\" is always in reach rather than at the end of a long list */ .items, .actions { display: flex; flex-direction: column; gap: var(--gw-space-0-5); padding: var(--gw-space-1); } .items { flex: 1; min-height: 0; overflow-y: auto; } .actions { border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .empty { padding: var(--gw-space-3); margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } .item { display: flex; gap: var(--gw-space-3); align-items: center; box-sizing: border-box; min-height: 2.5rem; padding: var(--gw-space-2) var(--gw-space-3); font: inherit; font-size: var(--gw-font-size-s); color: var(--gw-color-text-primary); text-align: start; cursor: pointer; background: transparent; border: 0; border-radius: var(--gw-radius-m); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); --gw-icon-size: 1.125rem; } .item:hover, .item:focus-visible { background: var(--gw-color-fill-neutral-hovered); outline: none; } .item:focus-visible { box-shadow: inset 0 0 0 var(--gw-focus-width) var(--gw-color-focus); } .item:disabled { color: var(--gw-color-text-disabled); cursor: not-allowed; background: transparent; } .action { font-weight: var(--gw-font-weight-strong); color: var(--gw-color-accent); } /* Holds the tick's place on every choice, so the labels line up */ .tick { flex: none; width: var(--gw-icon-size); height: var(--gw-icon-size); color: var(--gw-color-accent); } .item[aria-checked=\"false\"] .tick { visibility: hidden; } .separator { flex: none; height: 0; margin: var(--gw-space-0-5) calc(-1 * var(--gw-space-1)); border: 0; border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .text { display: flex; flex-direction: column; gap: var(--gw-space-0-5); } .description { font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } `, ]",
|
|
10361
10361
|
"inheritedFrom": {
|
|
10362
10362
|
"name": "GwElement",
|
|
10363
10363
|
"module": "src/element.ts"
|
|
@@ -10371,6 +10371,15 @@
|
|
|
10371
10371
|
},
|
|
10372
10372
|
"default": "\"\""
|
|
10373
10373
|
},
|
|
10374
|
+
{
|
|
10375
|
+
"kind": "field",
|
|
10376
|
+
"name": "heading",
|
|
10377
|
+
"type": {
|
|
10378
|
+
"text": "string"
|
|
10379
|
+
},
|
|
10380
|
+
"description": "What the menu is for, such as \"Sort by\", when the trigger shows the current choice.",
|
|
10381
|
+
"default": "\"\""
|
|
10382
|
+
},
|
|
10374
10383
|
{
|
|
10375
10384
|
"kind": "field",
|
|
10376
10385
|
"name": "icon",
|
|
@@ -10413,6 +10422,24 @@
|
|
|
10413
10422
|
},
|
|
10414
10423
|
"default": "\"medium\""
|
|
10415
10424
|
},
|
|
10425
|
+
{
|
|
10426
|
+
"kind": "field",
|
|
10427
|
+
"name": "active",
|
|
10428
|
+
"type": {
|
|
10429
|
+
"text": "boolean"
|
|
10430
|
+
},
|
|
10431
|
+
"description": "Marks the trigger as set, such as a grouping that's on.",
|
|
10432
|
+
"default": "false"
|
|
10433
|
+
},
|
|
10434
|
+
{
|
|
10435
|
+
"kind": "field",
|
|
10436
|
+
"name": "labels",
|
|
10437
|
+
"type": {
|
|
10438
|
+
"text": "\"show\" | \"hide\" | \"auto\""
|
|
10439
|
+
},
|
|
10440
|
+
"description": "Whether the trigger's text shows beside its icon: always, never, or not on narrow screens.",
|
|
10441
|
+
"default": "\"show\""
|
|
10442
|
+
},
|
|
10416
10443
|
{
|
|
10417
10444
|
"kind": "field",
|
|
10418
10445
|
"name": "#trigger",
|
|
@@ -10423,6 +10450,18 @@
|
|
|
10423
10450
|
"kind": "field",
|
|
10424
10451
|
"name": "#triggerText",
|
|
10425
10452
|
"privacy": "private",
|
|
10453
|
+
"type": {
|
|
10454
|
+
"text": "HTMLSpanElement"
|
|
10455
|
+
},
|
|
10456
|
+
"readonly": true
|
|
10457
|
+
},
|
|
10458
|
+
{
|
|
10459
|
+
"kind": "field",
|
|
10460
|
+
"name": "#heading",
|
|
10461
|
+
"privacy": "private",
|
|
10462
|
+
"type": {
|
|
10463
|
+
"text": "HTMLSpanElement"
|
|
10464
|
+
},
|
|
10426
10465
|
"readonly": true
|
|
10427
10466
|
},
|
|
10428
10467
|
{
|
|
@@ -10500,6 +10539,11 @@
|
|
|
10500
10539
|
}
|
|
10501
10540
|
]
|
|
10502
10541
|
},
|
|
10542
|
+
{
|
|
10543
|
+
"kind": "method",
|
|
10544
|
+
"name": "#tick",
|
|
10545
|
+
"privacy": "private"
|
|
10546
|
+
},
|
|
10503
10547
|
{
|
|
10504
10548
|
"kind": "method",
|
|
10505
10549
|
"name": "create",
|
|
@@ -10829,6 +10873,13 @@
|
|
|
10829
10873
|
"description": "The trigger's text.",
|
|
10830
10874
|
"name": "label"
|
|
10831
10875
|
},
|
|
10876
|
+
{
|
|
10877
|
+
"type": {
|
|
10878
|
+
"text": "string"
|
|
10879
|
+
},
|
|
10880
|
+
"description": "What the menu is for, such as \"Sort by\", when the trigger shows the current choice: it names the menu, starts the trigger's accessible name and is its tooltip.",
|
|
10881
|
+
"name": "heading"
|
|
10882
|
+
},
|
|
10832
10883
|
{
|
|
10833
10884
|
"type": {
|
|
10834
10885
|
"text": "string"
|
|
@@ -10863,6 +10914,20 @@
|
|
|
10863
10914
|
},
|
|
10864
10915
|
"description": "The trigger's size, 24 to 56px tall. Defaults to `medium`.",
|
|
10865
10916
|
"name": "size"
|
|
10917
|
+
},
|
|
10918
|
+
{
|
|
10919
|
+
"type": {
|
|
10920
|
+
"text": "boolean"
|
|
10921
|
+
},
|
|
10922
|
+
"description": "Marks the trigger as set, such as a grouping that's on, with an accent border and text.",
|
|
10923
|
+
"name": "active"
|
|
10924
|
+
},
|
|
10925
|
+
{
|
|
10926
|
+
"type": {
|
|
10927
|
+
"text": "\"show\"|\"hide\"|\"auto\""
|
|
10928
|
+
},
|
|
10929
|
+
"description": "Whether the trigger's text shows beside its icon: always, never, or not on narrow screens. Defaults to `show`.",
|
|
10930
|
+
"name": "labels"
|
|
10866
10931
|
}
|
|
10867
10932
|
],
|
|
10868
10933
|
"superclass": {
|
package/package.json
CHANGED
package/src/gw-button.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { base, controlSizes, createIcon, setIconPath } from "./styles.js";
|
|
|
32
32
|
* @attr {"brand"|"neutral"|"info"|"success"|"warning"|"danger"} variant - The color. Defaults to `brand`.
|
|
33
33
|
* @attr {"accent"|"filled"|"outlined"|"plain"} appearance - How the color is applied. Defaults to `accent`.
|
|
34
34
|
* @attr {"xs"|"small"|"medium"|"large"|"xl"} size - The size, 24 to 56px tall. Defaults to `medium`.
|
|
35
|
-
* @attr {"button"|"submit"|"reset"} type -
|
|
35
|
+
* @attr {"button"|"submit"|"reset"} type - What a click does: nothing more, or submit or reset the form the button is in. Defaults to `button`.
|
|
36
36
|
* @attr {string} href - Renders a link to this address instead of a button.
|
|
37
37
|
* @attr {string} label - The accessible name, when the text alone isn't enough.
|
|
38
38
|
* @attr {boolean} disabled - Disables the button.
|
|
@@ -423,6 +423,25 @@ export class GwButton extends GwElement {
|
|
|
423
423
|
if (link !== null && !event.composedPath().includes(link)) {
|
|
424
424
|
link.click();
|
|
425
425
|
}
|
|
426
|
+
|
|
427
|
+
// The native button is in the shadow root, so it has no form of its
|
|
428
|
+
// own; submit and reset act on the form around the element instead
|
|
429
|
+
const form = this.closest("form");
|
|
430
|
+
|
|
431
|
+
if (
|
|
432
|
+
form === null ||
|
|
433
|
+
this.disabled ||
|
|
434
|
+
event.defaultPrevented ||
|
|
435
|
+
!(this.#control instanceof HTMLButtonElement)
|
|
436
|
+
) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (this.type === "submit") {
|
|
441
|
+
form.requestSubmit();
|
|
442
|
+
} else if (this.type === "reset") {
|
|
443
|
+
form.reset();
|
|
444
|
+
}
|
|
426
445
|
});
|
|
427
446
|
|
|
428
447
|
const focus = () => {
|
package/src/gw-menu-button.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
|
-
import { GwElement, css, h } from "./element.js";
|
|
7
|
+
import { GwElement, css, h, setAttribute } from "./element.js";
|
|
8
8
|
import { base, controlSizes, createIcon, setIconPath } from "./styles.js";
|
|
9
9
|
|
|
10
10
|
export interface MenuItem {
|
|
@@ -15,8 +15,18 @@ export interface MenuItem {
|
|
|
15
15
|
/** SVG path data for an icon before the label. */
|
|
16
16
|
readonly icon?: string;
|
|
17
17
|
readonly disabled?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the item is the one picked, for a menu of choices such as a sort
|
|
20
|
+
* order. Give it on every item in the group: the picked one shows a tick.
|
|
21
|
+
*/
|
|
22
|
+
readonly checked?: boolean;
|
|
23
|
+
/** Draws a line above the item, starting a new group. */
|
|
24
|
+
readonly separator?: boolean;
|
|
18
25
|
}
|
|
19
26
|
|
|
27
|
+
// mdi's check, for the picked item in a menu of choices
|
|
28
|
+
const tick = "M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z";
|
|
29
|
+
|
|
20
30
|
/**
|
|
21
31
|
* A button that opens a menu on the Popover API, so it shows in the top
|
|
22
32
|
* layer and closes on Esc or a click outside. Set `items`; arrow keys, Home
|
|
@@ -27,6 +37,9 @@ export interface MenuItem {
|
|
|
27
37
|
* below the items as the list scrolls, so they're found without scrolling to
|
|
28
38
|
* the end, and with no items the menu says `empty` above them.
|
|
29
39
|
*
|
|
40
|
+
* For a menu of choices, such as sort and order, give each item `checked`:
|
|
41
|
+
* the picked one shows a tick, and `separator` starts the next group.
|
|
42
|
+
*
|
|
30
43
|
* Items render inside the component, so a page's own button styles can't
|
|
31
44
|
* reach them.
|
|
32
45
|
*
|
|
@@ -34,11 +47,14 @@ export interface MenuItem {
|
|
|
34
47
|
* @summary A button that opens a menu of items and actions.
|
|
35
48
|
*
|
|
36
49
|
* @attr {string} label - The trigger's text.
|
|
50
|
+
* @attr {string} heading - What the menu is for, such as "Sort by", when the trigger shows the current choice: it names the menu, starts the trigger's accessible name and is its tooltip.
|
|
37
51
|
* @attr {string} icon - SVG path data for an icon before the trigger's text.
|
|
38
52
|
* @attr {string} empty - What the menu says with no items. Defaults to `Nothing here yet`.
|
|
39
53
|
* @attr {MenuItem[]} items - The items, as JSON.
|
|
40
54
|
* @attr {MenuItem[]} actions - Actions pinned under the items, as JSON.
|
|
41
55
|
* @attr {"xs"|"small"|"medium"|"large"|"xl"} size - The trigger's size, 24 to 56px tall. Defaults to `medium`.
|
|
56
|
+
* @attr {boolean} active - Marks the trigger as set, such as a grouping that's on, with an accent border and text.
|
|
57
|
+
* @attr {"show"|"hide"|"auto"} labels - Whether the trigger's text shows beside its icon: always, never, or not on narrow screens. Defaults to `show`.
|
|
42
58
|
*
|
|
43
59
|
* @csspart trigger - The button that opens the menu.
|
|
44
60
|
*
|
|
@@ -50,11 +66,14 @@ export interface MenuItem {
|
|
|
50
66
|
export class GwMenuButton extends GwElement {
|
|
51
67
|
static override properties = {
|
|
52
68
|
label: {},
|
|
69
|
+
heading: {},
|
|
53
70
|
icon: {},
|
|
54
71
|
empty: {},
|
|
55
72
|
items: { type: "json" },
|
|
56
73
|
actions: { type: "json" },
|
|
57
74
|
size: { reflect: true },
|
|
75
|
+
active: { type: "boolean", reflect: true },
|
|
76
|
+
labels: { reflect: true },
|
|
58
77
|
} as const;
|
|
59
78
|
|
|
60
79
|
static override styles = [
|
|
@@ -95,6 +114,40 @@ export class GwMenuButton extends GwElement {
|
|
|
95
114
|
background: var(--gw-color-fill-neutral-hovered);
|
|
96
115
|
}
|
|
97
116
|
|
|
117
|
+
:host([active]) .trigger {
|
|
118
|
+
color: var(--gw-color-accent);
|
|
119
|
+
border-color: var(--gw-color-accent);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Icon only, square, with the text kept for assistive technology */
|
|
123
|
+
:host([labels="hide"]) .trigger.has-icon {
|
|
124
|
+
padding: 0 calc(var(--gw-space-2) * var(--gw-size));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
:host([labels="hide"]) .has-icon .label {
|
|
128
|
+
position: absolute;
|
|
129
|
+
width: 1px;
|
|
130
|
+
height: 1px;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
clip-path: inset(50%);
|
|
133
|
+
white-space: nowrap;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@media (width < 40rem) {
|
|
137
|
+
:host([labels="auto"]) .trigger.has-icon {
|
|
138
|
+
padding: 0 calc(var(--gw-space-2) * var(--gw-size));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
:host([labels="auto"]) .has-icon .label {
|
|
142
|
+
position: absolute;
|
|
143
|
+
width: 1px;
|
|
144
|
+
height: 1px;
|
|
145
|
+
overflow: hidden;
|
|
146
|
+
clip-path: inset(50%);
|
|
147
|
+
white-space: nowrap;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
98
151
|
.chevron {
|
|
99
152
|
width: 0.4rem;
|
|
100
153
|
height: 0.4rem;
|
|
@@ -223,6 +276,27 @@ export class GwMenuButton extends GwElement {
|
|
|
223
276
|
color: var(--gw-color-accent);
|
|
224
277
|
}
|
|
225
278
|
|
|
279
|
+
/* Holds the tick's place on every choice, so the labels line up */
|
|
280
|
+
.tick {
|
|
281
|
+
flex: none;
|
|
282
|
+
width: var(--gw-icon-size);
|
|
283
|
+
height: var(--gw-icon-size);
|
|
284
|
+
color: var(--gw-color-accent);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.item[aria-checked="false"] .tick {
|
|
288
|
+
visibility: hidden;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.separator {
|
|
292
|
+
flex: none;
|
|
293
|
+
height: 0;
|
|
294
|
+
margin: var(--gw-space-0-5) calc(-1 * var(--gw-space-1));
|
|
295
|
+
border: 0;
|
|
296
|
+
border-block-start: max(var(--gw-border-width), 1px) solid
|
|
297
|
+
var(--gw-color-border-normal);
|
|
298
|
+
}
|
|
299
|
+
|
|
226
300
|
.text {
|
|
227
301
|
display: flex;
|
|
228
302
|
flex-direction: column;
|
|
@@ -237,6 +311,8 @@ export class GwMenuButton extends GwElement {
|
|
|
237
311
|
];
|
|
238
312
|
|
|
239
313
|
declare label: string;
|
|
314
|
+
/** What the menu is for, such as "Sort by", when the trigger shows the current choice. */
|
|
315
|
+
declare heading: string;
|
|
240
316
|
/** SVG path data for an icon before the trigger's label. */
|
|
241
317
|
declare icon: string;
|
|
242
318
|
/** What the menu says when it has no items, such as "No backups yet". */
|
|
@@ -244,6 +320,10 @@ export class GwMenuButton extends GwElement {
|
|
|
244
320
|
declare items: readonly MenuItem[];
|
|
245
321
|
declare actions: readonly MenuItem[];
|
|
246
322
|
declare size: "xs" | "small" | "medium" | "large" | "xl";
|
|
323
|
+
/** Marks the trigger as set, such as a grouping that's on. */
|
|
324
|
+
declare active: boolean;
|
|
325
|
+
/** Whether the trigger's text shows beside its icon: always, never, or not on narrow screens. */
|
|
326
|
+
declare labels: "show" | "hide" | "auto";
|
|
247
327
|
|
|
248
328
|
readonly #trigger = h("button", {
|
|
249
329
|
class: "trigger",
|
|
@@ -253,7 +333,16 @@ export class GwMenuButton extends GwElement {
|
|
|
253
333
|
"aria-expanded": "false",
|
|
254
334
|
});
|
|
255
335
|
|
|
256
|
-
readonly #triggerText =
|
|
336
|
+
readonly #triggerText: HTMLSpanElement = h("span", {
|
|
337
|
+
class: "label",
|
|
338
|
+
id: "label",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Read before the label, for a trigger that shows the current choice
|
|
342
|
+
readonly #heading: HTMLSpanElement = h("span", {
|
|
343
|
+
id: "heading",
|
|
344
|
+
hidden: true,
|
|
345
|
+
});
|
|
257
346
|
|
|
258
347
|
readonly #triggerIcon = createIcon("");
|
|
259
348
|
|
|
@@ -271,11 +360,14 @@ export class GwMenuButton extends GwElement {
|
|
|
271
360
|
constructor() {
|
|
272
361
|
super();
|
|
273
362
|
this.label = "";
|
|
363
|
+
this.heading = "";
|
|
274
364
|
this.icon = "";
|
|
275
365
|
this.empty = "Nothing here yet";
|
|
276
366
|
this.items = [];
|
|
277
367
|
this.actions = [];
|
|
278
368
|
this.size = "medium";
|
|
369
|
+
this.active = false;
|
|
370
|
+
this.labels = "show";
|
|
279
371
|
}
|
|
280
372
|
|
|
281
373
|
/** The enabled items then actions, in the order arrow keys move through them. */
|
|
@@ -327,14 +419,18 @@ export class GwMenuButton extends GwElement {
|
|
|
327
419
|
}
|
|
328
420
|
|
|
329
421
|
#renderItem(item: MenuItem, kind: "item" | "action") {
|
|
422
|
+
const choice = item.checked !== undefined;
|
|
423
|
+
|
|
330
424
|
const button = h(
|
|
331
425
|
"button",
|
|
332
426
|
{
|
|
333
427
|
class: kind === "action" ? "item action" : "item",
|
|
334
|
-
role: "menuitem",
|
|
428
|
+
role: choice ? "menuitemradio" : "menuitem",
|
|
429
|
+
"aria-checked": choice ? String(item.checked) : null,
|
|
335
430
|
value: item.value,
|
|
336
431
|
disabled: item.disabled === true,
|
|
337
432
|
},
|
|
433
|
+
choice ? this.#tick() : null,
|
|
338
434
|
item.icon ? createIcon(item.icon) : null,
|
|
339
435
|
h(
|
|
340
436
|
"span",
|
|
@@ -348,7 +444,17 @@ export class GwMenuButton extends GwElement {
|
|
|
348
444
|
|
|
349
445
|
button.addEventListener("click", () => this.#choose(item.value));
|
|
350
446
|
|
|
351
|
-
return
|
|
447
|
+
return item.separator === true
|
|
448
|
+
? [h("hr", { class: "separator", role: "separator" }), button]
|
|
449
|
+
: [button];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
#tick() {
|
|
453
|
+
const icon = createIcon(tick);
|
|
454
|
+
|
|
455
|
+
icon.classList.add("tick");
|
|
456
|
+
|
|
457
|
+
return icon;
|
|
352
458
|
}
|
|
353
459
|
|
|
354
460
|
protected override create(root: ShadowRoot) {
|
|
@@ -359,14 +465,30 @@ export class GwMenuButton extends GwElement {
|
|
|
359
465
|
);
|
|
360
466
|
this.#popover.addEventListener("toggle", this.#onToggle);
|
|
361
467
|
this.#popover.addEventListener("keydown", this.#onKeydown);
|
|
362
|
-
root.append(this.#trigger, this.#popover);
|
|
468
|
+
root.append(this.#heading, this.#trigger, this.#popover);
|
|
363
469
|
}
|
|
364
470
|
|
|
365
471
|
protected override update(changed: ReadonlySet<string>) {
|
|
366
|
-
this.#triggerText.
|
|
472
|
+
this.#triggerText.textContent = this.label;
|
|
367
473
|
setIconPath(this.#triggerIcon, this.icon);
|
|
368
474
|
this.#triggerIcon.toggleAttribute("hidden", !this.icon);
|
|
369
|
-
this.#
|
|
475
|
+
this.#trigger.classList.toggle("has-icon", this.icon !== "");
|
|
476
|
+
this.#heading.textContent = this.heading;
|
|
477
|
+
setAttribute(
|
|
478
|
+
this.#trigger,
|
|
479
|
+
"aria-labelledby",
|
|
480
|
+
this.heading ? "heading label" : null,
|
|
481
|
+
);
|
|
482
|
+
// A tooltip saying what the menu is for, or the text when it's hidden
|
|
483
|
+
setAttribute(
|
|
484
|
+
this.#trigger,
|
|
485
|
+
"title",
|
|
486
|
+
this.heading ||
|
|
487
|
+
(this.icon !== "" && this.labels !== "show"
|
|
488
|
+
? this.label || null
|
|
489
|
+
: null),
|
|
490
|
+
);
|
|
491
|
+
this.#popover.setAttribute("aria-label", this.heading || this.label);
|
|
370
492
|
|
|
371
493
|
if (changed.has("items") || changed.has("empty")) {
|
|
372
494
|
const items = Array.isArray(this.items) ? this.items : [];
|
|
@@ -374,7 +496,7 @@ export class GwMenuButton extends GwElement {
|
|
|
374
496
|
this.#items.replaceChildren(
|
|
375
497
|
...(items.length === 0
|
|
376
498
|
? [h("p", { class: "empty", role: "none" }, this.empty)]
|
|
377
|
-
: items.
|
|
499
|
+
: items.flatMap((item) => this.#renderItem(item, "item"))),
|
|
378
500
|
);
|
|
379
501
|
}
|
|
380
502
|
|
|
@@ -382,7 +504,7 @@ export class GwMenuButton extends GwElement {
|
|
|
382
504
|
const actions = Array.isArray(this.actions) ? this.actions : [];
|
|
383
505
|
|
|
384
506
|
this.#actions.replaceChildren(
|
|
385
|
-
...actions.
|
|
507
|
+
...actions.flatMap((item) => this.#renderItem(item, "action")),
|
|
386
508
|
);
|
|
387
509
|
this.#actions.hidden = actions.length === 0;
|
|
388
510
|
}
|