mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-19 15:34:15 +00:00
Compare commits
13 Commits
docs-fix
...
konnorroge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3058c6d3b9 | ||
|
|
07327be95e | ||
|
|
b99d7771dc | ||
|
|
10cc9bdc68 | ||
|
|
bc598dad92 | ||
|
|
ff61ac002f | ||
|
|
63ec9d5bc1 | ||
|
|
555327b2fc | ||
|
|
15c6eaec90 | ||
|
|
6f856fbd89 | ||
|
|
df46e1db3b | ||
|
|
d21d829c29 | ||
|
|
2331e88dcf |
@@ -20,7 +20,7 @@
|
||||
</head>
|
||||
<body class="layout-{{ layout | stripExtension }}{{ ' page-wide' if wide }}">
|
||||
<!-- use view="desktop" as default to reduce layout jank on desktop site. -->
|
||||
<wa-page view="desktop" disable-navigation-toggle="" mobile-breakpoint="1140">
|
||||
<wa-page view="desktop" disable-navigation-toggle="" mobile-breakpoint="1180">
|
||||
<header slot="header" class="wa-split">
|
||||
{# Logo #}
|
||||
<div id="docs-branding">
|
||||
@@ -46,9 +46,9 @@
|
||||
|
||||
{# Search #}
|
||||
<wa-button id="search-trigger" appearance="outlined" size="small" data-search>
|
||||
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
|
||||
<wa-icon slot="start" name="magnifying-glass"></wa-icon>
|
||||
Search
|
||||
<kbd slot="suffix" class="wa-desktop-only">/</kbd>
|
||||
<kbd slot="end" class="wa-desktop-only">/</kbd>
|
||||
</wa-button>
|
||||
|
||||
{# Login #}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{# Color scheme selector #}
|
||||
<wa-select class="color-scheme-selector" appearance="filled" size="small" value="auto" pill title="Press \ to toggle">
|
||||
<wa-icon class="only-light" slot="prefix" name="sun" variant="regular"></wa-icon>
|
||||
<wa-icon class="only-dark" slot="prefix" name="moon" variant="regular"></wa-icon>
|
||||
<wa-icon class="only-light" slot="start" name="sun" variant="regular"></wa-icon>
|
||||
<wa-icon class="only-dark" slot="start" name="moon" variant="regular"></wa-icon>
|
||||
<wa-option value="light">
|
||||
<wa-icon slot="prefix" name="sun" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="sun" variant="regular"></wa-icon>
|
||||
Light
|
||||
</wa-option>
|
||||
<wa-option value="dark">
|
||||
<wa-icon slot="prefix" name="moon" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="moon" variant="regular"></wa-icon>
|
||||
Dark
|
||||
</wa-option>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-option value="auto">
|
||||
<wa-icon class="only-light" slot="prefix" name="sun" variant="regular"></wa-icon>
|
||||
<wa-icon class="only-dark" slot="prefix" name="moon" variant="regular"></wa-icon>
|
||||
<wa-icon class="only-light" slot="start" name="sun" variant="regular"></wa-icon>
|
||||
<wa-icon class="only-dark" slot="start" name="moon" variant="regular"></wa-icon>
|
||||
System
|
||||
</wa-option>
|
||||
</wa-select>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{# Preset theme selector #}
|
||||
<wa-select appearance="filled" size="small" value="default" pill class="preset-theme-selector">
|
||||
<wa-icon slot="prefix" name="paintbrush" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="paintbrush" variant="regular"></wa-icon>
|
||||
{% for theme in collections.theme | sort %}
|
||||
<wa-option value="{{ theme.page.fileSlug }}">
|
||||
{{ theme.data.title }}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
aria-haspopup="listbox"
|
||||
aria-activedescendant
|
||||
>
|
||||
<wa-icon slot="prefix" name="search"></wa-icon>
|
||||
<wa-icon slot="start" name="search"></wa-icon>
|
||||
</wa-input>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -121,20 +121,18 @@
|
||||
<li><a href="/docs/components/dialog/">Dialog</a></li>
|
||||
<li><a href="/docs/components/divider/">Divider</a></li>
|
||||
<li><a href="/docs/components/drawer/">Drawer</a></li>
|
||||
<li><a href="/docs/components/dropdown/">Dropdown</a></li>
|
||||
<li>
|
||||
<a href="/docs/components/dropdown">Dropdown</a>
|
||||
<ul>
|
||||
<li><a href="/docs/components/dropdown-item">Dropdown Item</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/docs/components/format-bytes/">Format Bytes</a></li>
|
||||
<li><a href="/docs/components/format-date/">Format Date</a></li>
|
||||
<li><a href="/docs/components/format-number/">Format Number</a></li>
|
||||
<li><a href="/docs/components/icon/">Icon</a></li>
|
||||
<li><a href="/docs/components/include/">Include</a></li>
|
||||
<li><a href="/docs/components/input/">Input</a></li>
|
||||
<li>
|
||||
<a href="/docs/components/menu/">Menu</a>
|
||||
<ul>
|
||||
<li><a href="/docs/components/menu-item/">Menu Item</a></li>
|
||||
<li><a href="/docs/components/menu-label/">Menu Label</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/docs/components/mutation-observer/">Mutation Observer</a></li>
|
||||
<li><a href="/docs/components/popover/">Popover</a></li>
|
||||
<li><a href="/docs/components/popup/">Popup</a></li>
|
||||
@@ -351,6 +349,21 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/docs/patterns/layouts/">Layouts</a>
|
||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/docs/patterns/layouts/ecommerce/">Ecommerce</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/docs/patterns/layouts/app/">App</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/docs/patterns/layouts/blog/">Blog</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</wa-details>
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
<span class="wa-caption-m">Shipping and taxes calculated at checkout.</span>
|
||||
<wa-button tabindex="-1" variant="brand">
|
||||
<wa-icon slot="prefix" name="shopping-bag"></wa-icon>
|
||||
<wa-icon slot="start" name="shopping-bag"></wa-icon>
|
||||
Checkout
|
||||
</wa-button>
|
||||
</div>
|
||||
@@ -62,10 +62,10 @@
|
||||
<div class="wa-stack">
|
||||
<h3 class="wa-heading-m">Sign In</h3>
|
||||
<wa-input tabindex="-1" label="Email" placeholder="ddjarin@mandalore.gov">
|
||||
<wa-icon slot="prefix" name="envelope" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="envelope" variant="regular"></wa-icon>
|
||||
</wa-input>
|
||||
<wa-input tabindex="-1" label="Password" type="password">
|
||||
<wa-icon slot="prefix" name="lock" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="lock" variant="regular"></wa-icon>
|
||||
</wa-input>
|
||||
<wa-button tabindex="-1" variant="brand">Sign In</wa-button>
|
||||
<a href="#" tabindex="-1" class="wa-body-s">I forgot my password</a>
|
||||
@@ -244,11 +244,11 @@
|
||||
</div>
|
||||
<div slot="footer" class="wa-grid wa-gap-xs" style="--min-column-size: 10ch;">
|
||||
<wa-button appearance="outlined" tabindex="-1">
|
||||
<wa-icon slot="prefix" name="at"></wa-icon>
|
||||
<wa-icon slot="start" name="at"></wa-icon>
|
||||
Email
|
||||
</wa-button>
|
||||
<wa-button appearance="outlined" tabindex="-1">
|
||||
<wa-icon slot="prefix" name="phone"></wa-icon>
|
||||
<wa-icon slot="start" name="phone"></wa-icon>
|
||||
Phone
|
||||
</wa-button>
|
||||
</div>
|
||||
@@ -268,11 +268,9 @@
|
||||
<wa-button id="more-actions-2" slot="trigger" appearance="plain" size="small" tabindex="-1">
|
||||
<wa-icon name="ellipsis-vertical" label="View menu"></wa-icon>
|
||||
</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Copy link</wa-menu-item>
|
||||
<wa-menu-item>Rename</wa-menu-item>
|
||||
<wa-menu-item>Move to trash</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item>Copy link</wa-dropdown-item>
|
||||
<wa-dropdown-item>Rename</wa-dropdown-item>
|
||||
<wa-dropdown-item>Move to trash</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
<wa-tooltip for="more-actions-2">View menu</wa-tooltip>
|
||||
</div>
|
||||
|
||||
@@ -259,19 +259,15 @@
|
||||
<td>
|
||||
<wa-dropdown size="small">
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Menu Item 1</wa-menu-item>
|
||||
<wa-menu-item>Menu Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item>Menu Item 1</wa-dropdown-item>
|
||||
<wa-dropdown-item>Menu Item 2</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
<td>
|
||||
<wa-dropdown class="wa-size-s">
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Menu Item 1</wa-menu-item>
|
||||
<wa-menu-item>Menu Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item>Menu Item 1</wa-dropdown-item>
|
||||
<wa-dropdown-item>Menu Item 2</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -280,19 +276,15 @@
|
||||
<td>
|
||||
<wa-dropdown size="medium">
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Menu Item 1</wa-menu-item>
|
||||
<wa-menu-item>Menu Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item>Menu Item 1</wa-dropdown-item>
|
||||
<wa-dropdown-item>Menu Item 2</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
<td>
|
||||
<wa-dropdown class="wa-size-m">
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Menu Item 1</wa-menu-item>
|
||||
<wa-menu-item>Menu Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item>Menu Item 1</wa-dropdown-item>
|
||||
<wa-dropdown-item>Menu Item 2</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -301,19 +293,15 @@
|
||||
<td>
|
||||
<wa-dropdown size="large">
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Menu Item 1</wa-menu-item>
|
||||
<wa-menu-item>Menu Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item>Menu Item 1</wa-dropdown-item>
|
||||
<wa-dropdown-item>Menu Item 2</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
<td>
|
||||
<wa-dropdown class="wa-size-l">
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Menu Item 1</wa-menu-item>
|
||||
<wa-menu-item>Menu Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item>Menu Item 1</wa-dropdown-item>
|
||||
<wa-dropdown-item>Menu Item 2</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -322,66 +310,6 @@
|
||||
</div>
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<h3>Menu</h3>
|
||||
|
||||
<div class="table-scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<th></th>
|
||||
<th><code>size=""</code></th>
|
||||
<th><code>.wa-size-[s|m|l]</code></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><code>small</code>/<code>s</code></th>
|
||||
<td>
|
||||
<wa-menu size="small">
|
||||
<wa-menu-item>Menu Item 1</wa-menu-item>
|
||||
<wa-menu-item>Menu Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
</td>
|
||||
<td>
|
||||
<wa-menu class="wa-size-s">
|
||||
<wa-menu-item>Menu Item 1</wa-menu-item>
|
||||
<wa-menu-item>Menu Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><code>medium</code>/<code>m</code></th>
|
||||
<td>
|
||||
<wa-menu size="medium">
|
||||
<wa-menu-item>Menu Item 1</wa-menu-item>
|
||||
<wa-menu-item>Menu Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
</td>
|
||||
<td>
|
||||
<wa-menu class="wa-size-m">
|
||||
<wa-menu-item>Menu Item 1</wa-menu-item>
|
||||
<wa-menu-item>Menu Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><code>large</code>/<code>l</code></th>
|
||||
<td>
|
||||
<wa-menu size="large">
|
||||
<wa-menu-item>Menu Item 1</wa-menu-item>
|
||||
<wa-menu-item>Menu Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
</td>
|
||||
<td>
|
||||
<wa-menu class="wa-size-l">
|
||||
<wa-menu-item>Menu Item 1</wa-menu-item>
|
||||
<wa-menu-item>Menu Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<h3>Input</h3>
|
||||
|
||||
<div class="table-scroll">
|
||||
@@ -799,4 +727,4 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ layout: page-outline
|
||||
|
||||
<div id="block-filter">
|
||||
<wa-input type="search" placeholder="Search {{ title }}" with-clear autofocus>
|
||||
<wa-icon slot="prefix" name="search"></wa-icon>
|
||||
<wa-icon slot="start" name="search"></wa-icon>
|
||||
</wa-input>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
</div>
|
||||
|
||||
<wa-button @click="reset()" appearance="outlined" variant="danger" size="small">
|
||||
<wa-icon slot="prefix" name="circle-xmark" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="circle-xmark" variant="regular"></wa-icon>
|
||||
Reset
|
||||
</wa-button>
|
||||
</wa-callout>
|
||||
|
||||
@@ -24,12 +24,12 @@ if (location.pathname.endsWith('/custom/') && !location.search) {
|
||||
<h4 slot="summary" data-no-anchor data-no-outline id="remix">
|
||||
<wa-icon name="arrows-rotate"></wa-icon>
|
||||
Remix this theme
|
||||
<wa-icon id="what-is-remixing" href="#remixing" name="circle-question" slot="suffix" variant="regular"></wa-icon>
|
||||
<wa-icon id="what-is-remixing" href="#remixing" name="circle-question" slot="end" variant="regular"></wa-icon>
|
||||
<wa-tooltip for="what-is-remixing">Customize this theme by changing its colors and/or remixing it with design elements from other themes!</wa-tooltip>
|
||||
</h4>
|
||||
|
||||
<wa-select name="palette" label="Color palette" with-clear v-model="theme.palette">
|
||||
<wa-icon name="swatchbook" slot="prefix" variant="regular"></wa-icon>
|
||||
<wa-icon name="swatchbook" slot="start" variant="regular"></wa-icon>
|
||||
<wa-option v-for="(palette, paletteId) in palettes" :label="palette.title" :value="paletteId === baseTheme.palette ? '' : paletteId">
|
||||
<palette-card :palette="paletteId" size="small">
|
||||
<template #extra>
|
||||
@@ -43,7 +43,7 @@ if (location.pathname.endsWith('/custom/') && !location.search) {
|
||||
:values="hues"></color-select>
|
||||
|
||||
<wa-select name="colors" class="theme-colors-select" label="Color contrast from…" value="" with-clear v-model="theme.colors">
|
||||
<wa-icon name="palette" slot="prefix" variant="regular"></wa-icon>
|
||||
<wa-icon name="palette" slot="start" variant="regular"></wa-icon>
|
||||
<template v-for="(themeMeta, themeId) in themes">
|
||||
<wa-option v-if="themeId !== 'custom'" :label="themeMeta.title" :value="themeId === computed.colors ? '' : themeId">
|
||||
<theme-card :theme="themeId" type="colors" :rest="{base: computed.base, palette: computed.palette, brand: computed.brand}" size="small">
|
||||
@@ -56,7 +56,7 @@ if (location.pathname.endsWith('/custom/') && !location.search) {
|
||||
</wa-select>
|
||||
|
||||
<wa-select name="typography" label="Typography from…" with-clear v-model="theme.typography">
|
||||
<wa-icon name="font-case" slot="prefix"></wa-icon>
|
||||
<wa-icon name="font-case" slot="start"></wa-icon>
|
||||
|
||||
<wa-option v-for="(themeMeta, themeId) in themes" :label="themeMeta.title" :value="themeId === theme.base ? '' : themeId">
|
||||
<fonts-card :theme="themeId" size="small">
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</wa-breadcrumb>
|
||||
</div>
|
||||
<wa-input id="search" class="wa-desktop-only" placeholder="Search" size="small" style="max-inline-size: 12rem">
|
||||
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
|
||||
<wa-icon slot="start" name="magnifying-glass"></wa-icon>
|
||||
</wa-input>
|
||||
</nav>
|
||||
<nav slot="navigation-header">
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<span class="wa-heading-m">radiogaga</span>
|
||||
</div>
|
||||
<wa-input id="search-header" placeholder="Search" class="wa-desktop-only" style="max-inline-size: 100%">
|
||||
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
|
||||
<wa-icon slot="start" name="magnifying-glass"></wa-icon>
|
||||
</wa-input>
|
||||
<div class="wa-cluster">
|
||||
<wa-button appearance="outlined">Log In</wa-button>
|
||||
@@ -28,7 +28,7 @@
|
||||
</header>
|
||||
<div slot="navigation-header" class="wa-split">
|
||||
<wa-input id="search-nav-drawer" placeholder="Search" style="max-inline-size: 100%" class="wa-mobile-only">
|
||||
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
|
||||
<wa-icon slot="start" name="magnifying-glass"></wa-icon>
|
||||
</wa-input>
|
||||
<div class="wa-split">
|
||||
<h2 class="wa-heading-s">For You</h2>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pre {
|
||||
/* Only code blocks generated by our docs get these styles */
|
||||
pre[id*='code-block-'] {
|
||||
background-color: var(--wa-color-gray-20);
|
||||
color: white;
|
||||
|
||||
@@ -8,6 +9,7 @@ pre {
|
||||
background-color: var(--wa-color-surface-lowered);
|
||||
}
|
||||
}
|
||||
|
||||
.code-comment,
|
||||
.code-prolog,
|
||||
.code-doctype,
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/* Prep our code blocks to host the copy button */
|
||||
pre[id*='code-block-']:has(code) {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
white-space: normal;
|
||||
|
||||
& code {
|
||||
display: block;
|
||||
font-size: 1em;
|
||||
background-color: transparent;
|
||||
padding: var(--wa-space-m);
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
wa-copy-button.copy-button {
|
||||
--background-color: var(--wa-color-gray-20);
|
||||
--background-color-hover: color-mix(in oklab, var(--background-color), white 5%);
|
||||
|
||||
@@ -251,12 +251,6 @@ wa-page[view='mobile'] :is([slot='navigation-header'], [slot='navigation']) {
|
||||
}
|
||||
}
|
||||
|
||||
[slot='navigation-header'] wa-menu {
|
||||
font-family: var(--wa-font-family-body);
|
||||
font-size: var(--wa-font-size-m);
|
||||
font-weight: var(--wa-font-weight-normal);
|
||||
}
|
||||
|
||||
/* Main content */
|
||||
wa-page > main {
|
||||
max-width: 80ch;
|
||||
|
||||
@@ -185,11 +185,11 @@ html.wa-theme-brutalist .preview-container {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.product-card wa-button::part(prefix) {
|
||||
.product-card wa-button::part(start) {
|
||||
padding-inline-start: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
.product-card wa-button::part(suffix) {
|
||||
.product-card wa-button::part(end) {
|
||||
padding-inline-end: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
|
||||
@@ -141,14 +141,3 @@ One of the most common use cases for badges is attaching them to buttons. To mak
|
||||
<wa-badge variant="danger" pill>6</wa-badge>
|
||||
</wa-button>
|
||||
```
|
||||
|
||||
### With Menu Items
|
||||
|
||||
When including badges in menu items, use the `suffix` slot to make sure they're aligned correctly.
|
||||
|
||||
```html {.example}
|
||||
<wa-menu style="max-width: 240px;">
|
||||
<wa-menu-label>Messages</wa-menu-label>
|
||||
<wa-menu-item>Comments <wa-badge slot="suffix" variant="neutral" pill>4</wa-badge></wa-menu-item>
|
||||
<wa-menu-item>Replies <wa-badge slot="suffix" variant="neutral" pill>12</wa-badge></wa-menu-item>
|
||||
</wa-menu>
|
||||
|
||||
@@ -8,7 +8,7 @@ parent: breadcrumb
|
||||
```html {.example}
|
||||
<wa-breadcrumb>
|
||||
<wa-breadcrumb-item>
|
||||
<wa-icon slot="prefix" name="house" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="house" variant="solid"></wa-icon>
|
||||
Home
|
||||
</wa-breadcrumb-item>
|
||||
<wa-breadcrumb-item>Clothing</wa-breadcrumb-item>
|
||||
|
||||
@@ -36,32 +36,20 @@ For websites, you'll probably want to use links instead. You can make any breadc
|
||||
</wa-breadcrumb>
|
||||
```
|
||||
|
||||
### Prefixes
|
||||
### Start & End Decorations
|
||||
|
||||
Use the `prefix` slot to add content before any breadcrumb item.
|
||||
Use the `start` and `end` slots to add presentational elements like `<wa-icon>` next to any breadcrumb item.
|
||||
|
||||
```html {.example}
|
||||
<wa-breadcrumb>
|
||||
<wa-breadcrumb-item>
|
||||
<wa-icon slot="prefix" name="house" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="house"></wa-icon>
|
||||
Home
|
||||
</wa-breadcrumb-item>
|
||||
<wa-breadcrumb-item>Articles</wa-breadcrumb-item>
|
||||
<wa-breadcrumb-item>Traveling</wa-breadcrumb-item>
|
||||
</wa-breadcrumb>
|
||||
```
|
||||
|
||||
### Suffixes
|
||||
|
||||
Use the `suffix` slot to add content after any breadcrumb item.
|
||||
|
||||
```html {.example}
|
||||
<wa-breadcrumb>
|
||||
<wa-breadcrumb-item>Documents</wa-breadcrumb-item>
|
||||
<wa-breadcrumb-item>Policies</wa-breadcrumb-item>
|
||||
<wa-breadcrumb-item>
|
||||
Security
|
||||
<wa-icon slot="suffix" name="shield" variant="solid"></wa-icon>
|
||||
<wa-icon slot="end" name="tree-palm"></wa-icon>
|
||||
Traveling
|
||||
</wa-breadcrumb-item>
|
||||
</wa-breadcrumb>
|
||||
```
|
||||
@@ -99,7 +87,7 @@ Use the `separator` slot to change the separator that goes between breadcrumb it
|
||||
|
||||
### Custom Colors
|
||||
|
||||
Breadcrumb labels match the color set on `<wa-breadcrumb-item>`. Prefixes, suffixes, and separators can be styled using CSS parts.
|
||||
Breadcrumb labels match the color set on `<wa-breadcrumb-item>`. Content in the `start`, `end`, and `separator` slots can be styled using CSS parts.
|
||||
|
||||
```html {.example}
|
||||
<style>
|
||||
@@ -112,14 +100,14 @@ Breadcrumb labels match the color set on `<wa-breadcrumb-item>`. Prefixes, suffi
|
||||
.redcrumbs wa-breadcrumb-item::part(separator) {
|
||||
color: pink;
|
||||
}
|
||||
.redcrumbs wa-breadcrumb-item::part(prefix),
|
||||
.redcrumbs wa-breadcrumb-item::part(suffix) {
|
||||
.redcrumbs wa-breadcrumb-item::part(start),
|
||||
.redcrumbs wa-breadcrumb-item::part(end) {
|
||||
color: currentColor;
|
||||
}
|
||||
</style>
|
||||
<wa-breadcrumb class="redcrumbs">
|
||||
<wa-breadcrumb-item>
|
||||
<wa-icon slot="prefix" name="house" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="house" variant="solid"></wa-icon>
|
||||
Home
|
||||
</wa-breadcrumb-item>
|
||||
<wa-breadcrumb-item>Articles</wa-breadcrumb-item>
|
||||
@@ -139,11 +127,9 @@ Dropdown menus can be placed in the default slot to provide additional options.
|
||||
<wa-button slot="trigger" size="small" appearance="filled" pill>
|
||||
<wa-icon label="More options" name="ellipsis" variant="solid"></wa-icon>
|
||||
</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item type="checkbox" checked>Web Design</wa-menu-item>
|
||||
<wa-menu-item type="checkbox">Web Development</wa-menu-item>
|
||||
<wa-menu-item type="checkbox">Marketing</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item type="checkbox" checked>Web Design</wa-dropdown-item>
|
||||
<wa-dropdown-item type="checkbox">Web Development</wa-dropdown-item>
|
||||
<wa-dropdown-item type="checkbox">Marketing</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</wa-breadcrumb-item>
|
||||
<wa-breadcrumb-item>Our Services</wa-breadcrumb-item>
|
||||
@@ -151,7 +137,7 @@ Dropdown menus can be placed in the default slot to provide additional options.
|
||||
</wa-breadcrumb>
|
||||
```
|
||||
|
||||
Alternatively, you can place dropdown menus in a prefix or suffix slot.
|
||||
Alternatively, you can place dropdown menus in a `start` or `end` slot.
|
||||
|
||||
```html {.example}
|
||||
<wa-breadcrumb>
|
||||
@@ -160,15 +146,14 @@ Alternatively, you can place dropdown menus in a prefix or suffix slot.
|
||||
<wa-breadcrumb-item>Digital Media</wa-breadcrumb-item>
|
||||
<wa-breadcrumb-item>
|
||||
Web Design
|
||||
<wa-dropdown slot="suffix">
|
||||
<wa-dropdown slot="end">
|
||||
<wa-button slot="trigger" size="small" appearance="filled" pill>
|
||||
<wa-icon label="More options" name="ellipsis" variant="solid"></wa-icon>
|
||||
</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item type="checkbox" checked>Web Design</wa-menu-item>
|
||||
<wa-menu-item type="checkbox">Web Development</wa-menu-item>
|
||||
<wa-menu-item type="checkbox">Marketing</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item type="checkbox" checked>Web Design</wa-dropdown-item>
|
||||
<wa-dropdown-item type="checkbox">Web Development</wa-dropdown-item>
|
||||
<wa-dropdown-item type="checkbox">Marketing</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</wa-breadcrumb-item>
|
||||
</wa-breadcrumb>
|
||||
```
|
||||
|
||||
@@ -50,26 +50,26 @@ and it will override the inherited size,
|
||||
it is rarely a good idea to mix sizes within the same button group.
|
||||
:::
|
||||
|
||||
### Vertical button groups
|
||||
### Vertical Button Groups
|
||||
|
||||
Set the `orientation` attribute to `vertical` to make a vertical button group.
|
||||
|
||||
```html {.example}
|
||||
<wa-button-group orientation="vertical" label="Options" style="max-width: 120px;">
|
||||
<wa-button>
|
||||
<wa-icon slot="prefix" name="plus"></wa-icon>
|
||||
<wa-icon slot="start" name="plus"></wa-icon>
|
||||
New
|
||||
</wa-button>
|
||||
<wa-button>
|
||||
<wa-icon slot="prefix" name="folder-open"></wa-icon>
|
||||
<wa-icon slot="start" name="folder-open"></wa-icon>
|
||||
Open
|
||||
</wa-button>
|
||||
<wa-button>
|
||||
<wa-icon slot="prefix" name="save"></wa-icon>
|
||||
<wa-icon slot="start" name="save"></wa-icon>
|
||||
Save
|
||||
</wa-button>
|
||||
<wa-button>
|
||||
<wa-icon slot="prefix" name="print"></wa-icon>
|
||||
<wa-icon slot="start" name="print"></wa-icon>
|
||||
Print
|
||||
</wa-button>
|
||||
</wa-button-group>
|
||||
@@ -166,11 +166,9 @@ Dropdowns can be placed into button groups.
|
||||
<wa-button>Button</wa-button>
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
<wa-menu-item>Item 2</wa-menu-item>
|
||||
<wa-menu-item>Item 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item>Item 1</wa-dropdown-item>
|
||||
<wa-dropdown-item>Item 2</wa-dropdown-item>
|
||||
<wa-dropdown-item>Item 3</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
<wa-button>Button</wa-button>
|
||||
</wa-button-group>
|
||||
@@ -187,11 +185,9 @@ Create a split button using a button and a dropdown. Use a [visually hidden](/do
|
||||
<wa-button slot="trigger" variant="brand">
|
||||
<wa-icon name="chevron-down" label="More options"></wa-icon>
|
||||
</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Save</wa-menu-item>
|
||||
<wa-menu-item>Save as…</wa-menu-item>
|
||||
<wa-menu-item>Save all</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item>Save</wa-dropdown-item>
|
||||
<wa-dropdown-item>Save as…</wa-dropdown-item>
|
||||
<wa-dropdown-item>Save all</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</wa-button-group>
|
||||
```
|
||||
@@ -230,9 +226,15 @@ Create interactive toolbars with button groups.
|
||||
</wa-button-group>
|
||||
|
||||
<wa-button-group label="Alignment">
|
||||
<wa-button id="button-align-left"><wa-icon name="align-left" variant="solid" label="Align Left"></wa-icon></wa-button>
|
||||
<wa-button id="button-align-center"><wa-icon name="align-center" variant="solid" label="Align Center"></wa-icon></wa-button>
|
||||
<wa-button id="button-align-right"><wa-icon name="align-right" variant="solid" label="Align Right"></wa-icon></wa-button>
|
||||
<wa-button id="button-align-left">
|
||||
<wa-icon name="align-left" variant="solid" label="Align Left"></wa-icon>
|
||||
</wa-button>
|
||||
<wa-button id="button-align-center">
|
||||
<wa-icon name="align-center" variant="solid" label="Align Center"></wa-icon>
|
||||
</wa-button>
|
||||
<wa-button id="button-align-right">
|
||||
<wa-icon name="align-right" variant="solid" label="Align Right"></wa-icon>
|
||||
</wa-button>
|
||||
</wa-button-group>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -126,60 +126,60 @@ As expected, buttons can be given a custom width by setting the `width` CSS prop
|
||||
<wa-button size="large" style="width: 100%;">Large</wa-button>
|
||||
```
|
||||
|
||||
### Prefix and Suffix Icons
|
||||
### Start & End Decorations
|
||||
|
||||
Use the `prefix` and `suffix` slots to add icons.
|
||||
Use the `start` and `end` slots to add presentational elements like `<wa-icon>` next to the button label.
|
||||
|
||||
```html {.example}
|
||||
<wa-button size="small">
|
||||
<wa-icon slot="prefix" name="gear" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="gear"></wa-icon>
|
||||
Settings
|
||||
</wa-button>
|
||||
|
||||
<wa-button size="small">
|
||||
<wa-icon slot="suffix" name="undo" variant="solid"></wa-icon>
|
||||
<wa-icon slot="end" name="undo"></wa-icon>
|
||||
Refresh
|
||||
</wa-button>
|
||||
|
||||
<wa-button size="small">
|
||||
<wa-icon slot="prefix" name="link" variant="solid"></wa-icon>
|
||||
<wa-icon slot="suffix" name="arrow-up-right-from-square" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="link"></wa-icon>
|
||||
<wa-icon slot="end" name="arrow-up-right-from-square"></wa-icon>
|
||||
Open
|
||||
</wa-button>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<wa-button>
|
||||
<wa-icon slot="prefix" name="gear" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="gear"></wa-icon>
|
||||
Settings
|
||||
</wa-button>
|
||||
|
||||
<wa-button>
|
||||
<wa-icon slot="suffix" name="undo" variant="solid"></wa-icon>
|
||||
<wa-icon slot="end" name="undo"></wa-icon>
|
||||
Refresh
|
||||
</wa-button>
|
||||
|
||||
<wa-button>
|
||||
<wa-icon slot="prefix" name="link" variant="solid"></wa-icon>
|
||||
<wa-icon slot="suffix" name="arrow-up-right-from-square" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="link"></wa-icon>
|
||||
<wa-icon slot="end" name="arrow-up-right-from-square"></wa-icon>
|
||||
Open
|
||||
</wa-button>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<wa-button size="large">
|
||||
<wa-icon slot="prefix" name="gear" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="gear"></wa-icon>
|
||||
Settings
|
||||
</wa-button>
|
||||
|
||||
<wa-button size="large">
|
||||
<wa-icon slot="suffix" name="undo" variant="solid"></wa-icon>
|
||||
<wa-icon slot="end" name="undo"></wa-icon>
|
||||
Refresh
|
||||
</wa-button>
|
||||
|
||||
<wa-button size="large">
|
||||
<wa-icon slot="prefix" name="link" variant="solid"></wa-icon>
|
||||
<wa-icon slot="suffix" name="arrow-up-right-from-square" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="link"></wa-icon>
|
||||
<wa-icon slot="end" name="arrow-up-right-from-square"></wa-icon>
|
||||
Open
|
||||
</wa-button>
|
||||
```
|
||||
|
||||
@@ -58,13 +58,14 @@ The default orientation for dividers is `horizontal`. Set `orientation` attribut
|
||||
Use dividers in [menus](/docs/components/menu) to visually group menu items.
|
||||
|
||||
```html {.example}
|
||||
<wa-menu style="max-width: 200px;">
|
||||
<wa-menu-item value="1">Option 1</wa-menu-item>
|
||||
<wa-menu-item value="2">Option 2</wa-menu-item>
|
||||
<wa-menu-item value="3">Option 3</wa-menu-item>
|
||||
<wa-dropdown style="max-width: 200px;">
|
||||
<wa-button slot="trigger" caret>Menu</wa-button>
|
||||
<wa-dropdown-item value="1">Option 1</wa-dropdown-item>
|
||||
<wa-dropdown-item value="2">Option 2</wa-dropdown-item>
|
||||
<wa-dropdown-item value="3">Option 3</wa-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item value="4">Option 4</wa-menu-item>
|
||||
<wa-menu-item value="5">Option 5</wa-menu-item>
|
||||
<wa-menu-item value="6">Option 6</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item value="4">Option 4</wa-dropdown-item>
|
||||
<wa-dropdown-item value="5">Option 5</wa-dropdown-item>
|
||||
<wa-dropdown-item value="6">Option 6</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
```
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Dropdown Item
|
||||
description: Description of component.
|
||||
layout: component
|
||||
---
|
||||
|
||||
This component must be used as a child of `<wa-dropdown>`. Please see the [Dropdown docs](/docs/components/dropdown) to see examples of this component in action.
|
||||
@@ -7,28 +7,38 @@ icon: dropdown
|
||||
|
||||
Dropdowns consist of a trigger and a panel. By default, activating the trigger will expose the panel and interacting outside of the panel will close it.
|
||||
|
||||
Dropdowns are designed to work well with [menus](/docs/components/menu) to provide a list of options the user can select from. However, dropdowns can also be used in lower-level applications (e.g. [color picker](/docs/components/color-picker)). The API gives you complete control over showing, hiding, and positioning the panel.
|
||||
Dropdowns are designed to work well with [dropdown items](/docs/components/dropdown-item) to provide a list of options the user can select from. However, dropdowns can also be used in lower-level applications. The API gives you complete control over showing, hiding, and positioning the panel.
|
||||
|
||||
```html {.example}
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Dropdown Item 1</wa-menu-item>
|
||||
<wa-menu-item>Dropdown Item 2</wa-menu-item>
|
||||
<wa-menu-item>Dropdown Item 3</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item type="checkbox" checked>Checkbox</wa-menu-item>
|
||||
<wa-menu-item disabled>Disabled</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item>
|
||||
Prefix
|
||||
<wa-icon slot="prefix" name="gift" variant="solid"></wa-icon>
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
Suffix Icon
|
||||
<wa-icon slot="suffix" name="heart" variant="solid"></wa-icon>
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="icon" name="scissors"></wa-icon>
|
||||
Cut
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="icon" name="copy"></wa-icon>
|
||||
Copy
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="icon" name="paste"></wa-icon>
|
||||
Paste
|
||||
</wa-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-dropdown-item>
|
||||
Show images
|
||||
<wa-dropdown-item slot="submenu" value="show-all-images">Show All Images</wa-dropdown-item>
|
||||
<wa-dropdown-item slot="submenu" value="show-thumbnails">Show Thumbnails</wa-dropdown-item>
|
||||
</wa-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-dropdown-item type="checkbox" checked>Emoji Shortcuts<wa-dropdown-item>
|
||||
<wa-dropdown-item type="checkbox" checked>Word Wrap</wa-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-dropdown-item variant="danger">
|
||||
<wa-icon slot="icon" name="trash"></wa-icon>
|
||||
Delete
|
||||
</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
```
|
||||
|
||||
@@ -36,17 +46,16 @@ Dropdowns are designed to work well with [menus](/docs/components/menu) to provi
|
||||
|
||||
### Getting the Selected Item
|
||||
|
||||
When dropdowns are used with [menus](/docs/components/menu), you can listen for the [`wa-select`](/docs/components/menu#events) event to determine which menu item was selected. The menu item element will be exposed in `event.detail.item`. You can set `value` props to make it easier to identify commands.
|
||||
When an item is selected, the `wa-select` event will be emitted by the dropdown. You can inspect `event.detail.item` to get a reference to the selected item. If you've provided a value for each [dropdown item](/docs/components/dropdown-item), it will be available in `event.detail.item.value`.
|
||||
|
||||
```html {.example}
|
||||
<div class="dropdown-selection">
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Edit</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item value="cut">Cut</wa-menu-item>
|
||||
<wa-menu-item value="copy">Copy</wa-menu-item>
|
||||
<wa-menu-item value="paste">Paste</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-button slot="trigger" caret>View</wa-button>
|
||||
<wa-dropdown-item value="full-screen">Enter full screen</wa-dropdown-item>
|
||||
<wa-dropdown-item value="actual">Actual size</wa-dropdown-item>
|
||||
<wa-dropdown-item value="zoom-in">Zoom in</wa-dropdown-item>
|
||||
<wa-dropdown-item value="zoom-out">Zoom out</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</div>
|
||||
|
||||
@@ -55,53 +64,191 @@ When dropdowns are used with [menus](/docs/components/menu), you can listen for
|
||||
const dropdown = container.querySelector('wa-dropdown');
|
||||
|
||||
dropdown.addEventListener('wa-select', event => {
|
||||
const selectedItem = event.detail.item;
|
||||
console.log(selectedItem.value);
|
||||
console.log(event.detail.item.value);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
Alternatively, you can listen for the `click` event on individual menu items. Note that, using this approach, disabled menu items will still emit a `click` event.
|
||||
:::info
|
||||
To keep the dropdown open after selection, call `event.preventDefault()` in the `wa-select` event's callback.
|
||||
:::
|
||||
|
||||
### Showing Icons
|
||||
|
||||
Use the `icon` slot to add icons to [dropdown items](/docs/components/dropdown-item). This works best with [icon](/docs/components/icon) elements.
|
||||
|
||||
```html {.example}
|
||||
<div class="dropdown-selection-alt">
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Edit</wa-button>
|
||||
|
||||
<wa-dropdown-item value="cut">
|
||||
<wa-icon slot="icon" name="scissors"></wa-icon>
|
||||
Cut
|
||||
</wa-dropdown-item>
|
||||
|
||||
<wa-dropdown-item value="copy">
|
||||
<wa-icon slot="icon" name="copy"></wa-icon>
|
||||
Copy
|
||||
</wa-dropdown-item>
|
||||
|
||||
<wa-dropdown-item value="paste">
|
||||
<wa-icon slot="icon" name="paste"></wa-icon>
|
||||
Paste
|
||||
</wa-dropdown-item>
|
||||
|
||||
<wa-dropdown-item value="delete">
|
||||
<wa-icon slot="icon" name="trash"></wa-icon>
|
||||
Delete
|
||||
</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
```
|
||||
|
||||
### Showing Labels & Dividers
|
||||
|
||||
Use any heading, e.g. `<h1>`–`<h6>` to add labels and the [`<wa-divider>`](/docs/components/divider) element for separators.
|
||||
|
||||
```html {.example}
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Device</wa-button>
|
||||
|
||||
<h3>Type</h3>
|
||||
<wa-dropdown-item value="phone">Phone</wa-dropdown-item>
|
||||
<wa-dropdown-item value="tablet">Tablet</wa-dropdown-item>
|
||||
<wa-dropdown-item value="desktop">Desktop</wa-dropdown-item>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<wa-dropdown-item value="more">More options…</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
```
|
||||
|
||||
### Showing Details
|
||||
|
||||
Use the `details` slot to display details, such as keyboard shortcuts, inside [dropdown items](/docs/components/dropdown-item).
|
||||
|
||||
```html {.example}
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Message</wa-button>
|
||||
|
||||
<wa-dropdown-item value="reply">
|
||||
Reply
|
||||
<span slot="details">⌘R</span>
|
||||
</wa-dropdown-item>
|
||||
|
||||
<wa-dropdown-item value="forward">
|
||||
Forward
|
||||
<span slot="details">⌘F</span>
|
||||
</wa-dropdown-item>
|
||||
|
||||
<wa-dropdown-item value="move">
|
||||
Move
|
||||
<span slot="details">⌘M</span>
|
||||
</wa-dropdown-item>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<wa-dropdown-item value="archive">
|
||||
Archive
|
||||
<span slot="details">⌘A</span>
|
||||
</wa-dropdown-item>
|
||||
|
||||
<wa-dropdown-item value="delete" variant="danger">
|
||||
Delete
|
||||
<span slot="details">Del</span>
|
||||
</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
```
|
||||
|
||||
### Checkable Items
|
||||
|
||||
You can turn a [dropdown item](/docs/components/dropdown-item) into a checkable option by setting `type="checkbox"`. Add the `checked` attribute to make it checked initially. When clicked, the item's checked state will toggle and the dropdown will close. You can cancel the `wa-select` event if you want to keep it open instead.
|
||||
|
||||
```html {.example}
|
||||
<div class="dropdown-checkboxes">
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Edit</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item value="cut">Cut</wa-menu-item>
|
||||
<wa-menu-item value="copy">Copy</wa-menu-item>
|
||||
<wa-menu-item value="paste">Paste</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-button slot="trigger" caret>View</wa-button>
|
||||
|
||||
<wa-dropdown-item type="checkbox" value="canvas" checked>Show canvas</wa-dropdown-item>
|
||||
<wa-dropdown-item type="checkbox" value="grid" checked>Show grid</wa-dropdown-item>
|
||||
<wa-dropdown-item type="checkbox" value="source">Show source</wa-dropdown-item>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<wa-dropdown-item value="preferences">Preferences…</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.dropdown-selection-alt');
|
||||
const cut = container.querySelector('wa-menu-item[value="cut"]');
|
||||
const copy = container.querySelector('wa-menu-item[value="copy"]');
|
||||
const paste = container.querySelector('wa-menu-item[value="paste"]');
|
||||
const container = document.querySelector('.dropdown-checkboxes');
|
||||
const dropdown = container.querySelector('wa-dropdown');
|
||||
|
||||
cut.addEventListener('click', () => console.log('cut'));
|
||||
copy.addEventListener('click', () => console.log('copy'));
|
||||
paste.addEventListener('click', () => console.log('paste'));
|
||||
dropdown.addEventListener('wa-select', event => {
|
||||
if (event.detail.item.type === 'checkbox') {
|
||||
// Checkbox
|
||||
console.log(event.detail.item.value, event.detail.item.checked ? 'checked' : 'unchecked');
|
||||
} else {
|
||||
// Not a checkbox
|
||||
console.log(event.detail.item.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
:::info
|
||||
When a checkable option exists anywhere in the dropdown, all items will receive additional padding so they align properly.
|
||||
:::
|
||||
|
||||
### Destructive Items
|
||||
|
||||
Add `variant="danger"` to any [dropdown item](/docs/components/dropdown-item) to highlight that it's a dangerous action.
|
||||
|
||||
```html {.example}
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Project</wa-button>
|
||||
|
||||
<wa-dropdown-item value="share">
|
||||
<wa-icon slot="icon" name="share"></wa-icon>
|
||||
Share
|
||||
</wa-dropdown-item>
|
||||
|
||||
<wa-dropdown-item value="preferences">
|
||||
<wa-icon slot="icon" name="gear"></wa-icon>
|
||||
Preferences
|
||||
</wa-dropdown-item>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<h3>Danger zone</h3>
|
||||
|
||||
<wa-dropdown-item value="archive">
|
||||
<wa-icon slot="icon" name="archive"></wa-icon>
|
||||
Archive
|
||||
</wa-dropdown-item>
|
||||
|
||||
<wa-dropdown-item value="delete" variant="danger">
|
||||
<wa-icon slot="icon" name="trash"></wa-icon>
|
||||
Delete
|
||||
</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
```
|
||||
|
||||
### Placement
|
||||
|
||||
The preferred placement of the dropdown can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport.
|
||||
|
||||
```html {.example}
|
||||
<wa-dropdown placement="top-start">
|
||||
<wa-button slot="trigger" caret>Edit</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Cut</wa-menu-item>
|
||||
<wa-menu-item>Copy</wa-menu-item>
|
||||
<wa-menu-item>Paste</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item>Find</wa-menu-item>
|
||||
<wa-menu-item>Replace</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown placement="right-start">
|
||||
<wa-button slot="trigger">
|
||||
File formats
|
||||
<wa-icon slot="end" name="chevron-right"></wa-icon>
|
||||
</wa-button>
|
||||
|
||||
<wa-dropdown-item value="pdf">PDF Document</wa-dropdown-item>
|
||||
<wa-dropdown-item value="docx">Word Document</wa-dropdown-item>
|
||||
<wa-dropdown-item value="xlsx">Excel Spreadsheet</wa-dropdown-item>
|
||||
<wa-dropdown-item value="pptx">PowerPoint Presentation</wa-dropdown-item>
|
||||
<wa-dropdown-item value="txt">Plain Text</wa-dropdown-item>
|
||||
<wa-dropdown-item value="json">JSON File</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
```
|
||||
|
||||
@@ -112,71 +259,111 @@ The distance from the panel to the trigger can be customized using the `distance
|
||||
```html {.example}
|
||||
<wa-dropdown distance="30">
|
||||
<wa-button slot="trigger" caret>Edit</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Cut</wa-menu-item>
|
||||
<wa-menu-item>Copy</wa-menu-item>
|
||||
<wa-menu-item>Paste</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item>Find</wa-menu-item>
|
||||
<wa-menu-item>Replace</wa-menu-item>
|
||||
</wa-menu>
|
||||
|
||||
<wa-dropdown-item>Cut</wa-dropdown-item>
|
||||
<wa-dropdown-item>Copy</wa-dropdown-item>
|
||||
<wa-dropdown-item>Paste</wa-dropdown-item>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<wa-dropdown-item>Find</wa-dropdown-item>
|
||||
<wa-dropdown-item>Replace</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
```
|
||||
|
||||
### Skidding
|
||||
### Offset
|
||||
|
||||
The offset of the panel along the trigger can be customized using the `skidding` attribute. This value is specified in pixels.
|
||||
The offset of the panel along the trigger can be customized using the `offset` attribute. This value is specified in pixels.
|
||||
|
||||
```html {.example}
|
||||
<wa-dropdown skidding="30">
|
||||
<wa-dropdown offset="30">
|
||||
<wa-button slot="trigger" caret>Edit</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Cut</wa-menu-item>
|
||||
<wa-menu-item>Copy</wa-menu-item>
|
||||
<wa-menu-item>Paste</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item>Find</wa-menu-item>
|
||||
<wa-menu-item>Replace</wa-menu-item>
|
||||
</wa-menu>
|
||||
|
||||
<wa-dropdown-item>Cut</wa-dropdown-item>
|
||||
<wa-dropdown-item>Copy</wa-dropdown-item>
|
||||
<wa-dropdown-item>Paste</wa-dropdown-item>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<wa-dropdown-item>Find</wa-dropdown-item>
|
||||
<wa-dropdown-item>Replace</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
```
|
||||
|
||||
### Submenus
|
||||
|
||||
To create a submenu, nest an `<wa-menu slot="submenu">` element in a [menu item](/docs/components/menu-item).
|
||||
To create submenus, nest [dropdown items](/docs/components/dropdown-item) inside of a dropdown item and assign `slot="submenu"` to each one. You can also add [dividers](/docs/components/divider) as needed.
|
||||
|
||||
```html {.example}
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Edit</wa-button>
|
||||
<div class="dropdown-submenus">
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Export</wa-button>
|
||||
|
||||
<wa-dropdown-item>
|
||||
Documents
|
||||
<wa-dropdown-item slot="submenu" value="pdf">PDF</wa-dropdown-item>
|
||||
<wa-dropdown-item slot="submenu" value="docx">Word Document</wa-dropdown-item>
|
||||
</wa-dropdown-item>
|
||||
|
||||
<wa-dropdown-item>
|
||||
Spreadsheets
|
||||
<wa-dropdown-item slot="submenu">
|
||||
Excel Formats
|
||||
<wa-dropdown-item slot="submenu" value="xlsx">Excel (.xlsx)</wa-dropdown-item>
|
||||
<wa-dropdown-item slot="submenu" value="xls">Excel 97-2003 (.xls)</wa-dropdown-item>
|
||||
<wa-dropdown-item slot="submenu" value="csv">CSV (.csv)</wa-dropdown-item>
|
||||
</wa-dropdown-item>
|
||||
|
||||
<wa-dropdown-item slot="submenu">
|
||||
Other Formats
|
||||
<wa-dropdown-item slot="submenu" value="ods">OpenDocument (.ods)</wa-dropdown-item>
|
||||
<wa-dropdown-item slot="submenu" value="tsv">Tab-separated (.tsv)</wa-dropdown-item>
|
||||
<wa-dropdown-item slot="submenu" value="json">JSON (.json)</wa-dropdown-item>
|
||||
</wa-dropdown-item>
|
||||
|
||||
<wa-dropdown-item slot="submenu" value="numbers">Apple Numbers</wa-dropdown-item>
|
||||
</wa-dropdown-item>
|
||||
|
||||
<wa-menu style="max-width: 200px;">
|
||||
<wa-menu-item value="undo">Undo</wa-menu-item>
|
||||
<wa-menu-item value="redo">Redo</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item value="cut">Cut</wa-menu-item>
|
||||
<wa-menu-item value="copy">Copy</wa-menu-item>
|
||||
<wa-menu-item value="paste">Paste</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item>
|
||||
Find
|
||||
<wa-menu slot="submenu">
|
||||
<wa-menu-item value="find">Find…</wa-menu-item>
|
||||
<wa-menu-item value="find-previous">Find Next</wa-menu-item>
|
||||
<wa-menu-item value="find-next">Find Previous</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
Transformations
|
||||
<wa-menu slot="submenu">
|
||||
<wa-menu-item value="uppercase">Make uppercase</wa-menu-item>
|
||||
<wa-menu-item value="lowercase">Make lowercase</wa-menu-item>
|
||||
<wa-menu-item value="capitalize">Capitalize</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
|
||||
<wa-dropdown-item>
|
||||
Options
|
||||
<wa-dropdown-item slot="submenu" type="checkbox" value="compress">Compress files</wa-dropdown-item>
|
||||
<wa-dropdown-item slot="submenu" type="checkbox" checked value="metadata">Include metadata</wa-dropdown-item>
|
||||
<wa-dropdown-item slot="submenu" type="checkbox" value="password">Password protect</wa-dropdown-item>
|
||||
</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.dropdown-submenus');
|
||||
const dropdown = container.querySelector('wa-dropdown');
|
||||
|
||||
dropdown.addEventListener('wa-select', event => {
|
||||
console.log(event.detail.item.value);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
:::info
|
||||
Dropdown items that have a submenu will not dispatch the `wa-select` event. However, items inside the submenu will, unless they also have a submenu.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
As a UX best practice, avoid using more than one level of submenu when possible.
|
||||
:::
|
||||
|
||||
### Disabling Items
|
||||
|
||||
Add the `disabled` attribute to any [dropdown item](/docs/components/dropdown-item) to disable it.
|
||||
|
||||
```html {.example}
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Payment method</wa-button>
|
||||
|
||||
<wa-dropdown-item value="cash">Cash</wa-dropdown-item>
|
||||
<wa-dropdown-item value="check" disabled>Personal check</wa-dropdown-item>
|
||||
<wa-dropdown-item value="credit">Credit card</wa-dropdown-item>
|
||||
<wa-dropdown-item value="gift-card">Gift card</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
```
|
||||
|
||||
@@ -109,24 +109,24 @@ The `type` attribute controls the type of input the browser renders.
|
||||
<wa-input type="date" placeholder="Date"></wa-input>
|
||||
```
|
||||
|
||||
### Prefix & Suffix Icons
|
||||
### Start & End Decorations
|
||||
|
||||
Use the `prefix` and `suffix` slots to add icons.
|
||||
Use the `start` and `end` slots to add presentational elements like `<wa-icon>` within the input.
|
||||
|
||||
```html {.example}
|
||||
<wa-input placeholder="Small" size="small">
|
||||
<wa-icon name="house" variant="solid" slot="prefix"></wa-icon>
|
||||
<wa-icon name="comment" variant="solid" slot="suffix"></wa-icon>
|
||||
<wa-icon name="house" slot="start"></wa-icon>
|
||||
<wa-icon name="comment" slot="end"></wa-icon>
|
||||
</wa-input>
|
||||
<br />
|
||||
<wa-input placeholder="Medium" size="medium">
|
||||
<wa-icon name="house" variant="solid" slot="prefix"></wa-icon>
|
||||
<wa-icon name="comment" variant="solid" slot="suffix"></wa-icon>
|
||||
<wa-icon name="house" slot="start"></wa-icon>
|
||||
<wa-icon name="comment" slot="end"></wa-icon>
|
||||
</wa-input>
|
||||
<br />
|
||||
<wa-input placeholder="Large" size="large">
|
||||
<wa-icon name="house" variant="solid" slot="prefix"></wa-icon>
|
||||
<wa-icon name="comment" variant="solid" slot="suffix"></wa-icon>
|
||||
<wa-icon name="house" slot="start"></wa-icon>
|
||||
<wa-icon name="comment" slot="end"></wa-icon>
|
||||
</wa-input>
|
||||
```
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
---
|
||||
title: Menu Item
|
||||
description: Menu items provide options for the user to pick from in a menu.
|
||||
tags: component
|
||||
parent: menu
|
||||
icon: menu
|
||||
---
|
||||
|
||||
```html {.example}
|
||||
<wa-menu style="max-width: 200px;">
|
||||
<wa-menu-item>Option 1</wa-menu-item>
|
||||
<wa-menu-item>Option 2</wa-menu-item>
|
||||
<wa-menu-item>Option 3</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item type="checkbox" checked>Checkbox</wa-menu-item>
|
||||
<wa-menu-item disabled>Disabled</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item>
|
||||
Prefix Icon
|
||||
<wa-icon slot="prefix" name="gift" variant="solid"></wa-icon>
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
Suffix Icon
|
||||
<wa-icon slot="suffix" name="heart" variant="solid"></wa-icon>
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Prefix & Suffix
|
||||
|
||||
Add content to the start and end of menu items using the `prefix` and `suffix` slots.
|
||||
|
||||
```html {.example}
|
||||
<wa-menu style="max-width: 200px;">
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="house" variant="solid"></wa-icon>
|
||||
Home
|
||||
</wa-menu-item>
|
||||
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="envelope" variant="solid"></wa-icon>
|
||||
Messages
|
||||
<wa-badge slot="suffix" variant="brand" pill>12</wa-badge>
|
||||
</wa-menu-item>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="gear" variant="solid"></wa-icon>
|
||||
Settings
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Add the `disabled` attribute to disable the menu item so it cannot be selected.
|
||||
|
||||
```html {.example}
|
||||
<wa-menu style="max-width: 200px;">
|
||||
<wa-menu-item>Option 1</wa-menu-item>
|
||||
<wa-menu-item disabled>Option 2</wa-menu-item>
|
||||
<wa-menu-item>Option 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
```
|
||||
|
||||
### Loading
|
||||
|
||||
Use the `loading` attribute to indicate that a menu item is busy. Like a disabled menu item, clicks will be suppressed until the loading state is removed.
|
||||
|
||||
```html {.example}
|
||||
<wa-menu style="max-width: 200px;">
|
||||
<wa-menu-item>Option 1</wa-menu-item>
|
||||
<wa-menu-item loading>Option 2</wa-menu-item>
|
||||
<wa-menu-item>Option 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
```
|
||||
|
||||
### Checkbox Menu Items
|
||||
|
||||
Set the `type` attribute to `checkbox` to create a menu item that will toggle on and off when selected. You can use the `checked` attribute to set the initial state.
|
||||
|
||||
Checkbox menu items are visually indistinguishable from regular menu items. Their ability to be toggled is primarily inferred from context, much like you'd find in the menu of a native app.
|
||||
|
||||
```html {.example}
|
||||
<wa-menu style="max-width: 200px;">
|
||||
<wa-menu-item type="checkbox">Autosave</wa-menu-item>
|
||||
<wa-menu-item type="checkbox" checked>Check Spelling</wa-menu-item>
|
||||
<wa-menu-item type="checkbox">Word Wrap</wa-menu-item>
|
||||
</wa-menu>
|
||||
```
|
||||
|
||||
### Value & Selection
|
||||
|
||||
The `value` attribute can be used to assign a hidden value, such as a unique identifier, to a menu item. When an item is selected, the `wa-select` event will be emitted and a reference to the item will be available at `event.detail.item`. You can use this reference to access the selected item's value, its checked state, and more.
|
||||
|
||||
```html {.example}
|
||||
<wa-menu class="menu-value" style="max-width: 200px;">
|
||||
<wa-menu-item value="opt-1">Option 1</wa-menu-item>
|
||||
<wa-menu-item value="opt-2">Option 2</wa-menu-item>
|
||||
<wa-menu-item value="opt-3">Option 3</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item type="checkbox" value="opt-4">Checkbox 4</wa-menu-item>
|
||||
<wa-menu-item type="checkbox" value="opt-5">Checkbox 5</wa-menu-item>
|
||||
<wa-menu-item type="checkbox" value="opt-6">Checkbox 6</wa-menu-item>
|
||||
</wa-menu>
|
||||
|
||||
<script>
|
||||
const menu = document.querySelector('.menu-value');
|
||||
|
||||
menu.addEventListener('wa-select', event => {
|
||||
const item = event.detail.item;
|
||||
|
||||
// Log value
|
||||
if (item.type === 'checkbox') {
|
||||
console.log(`Selected value: ${item.value} (${item.checked ? 'checked' : 'unchecked'})`);
|
||||
} else {
|
||||
console.log(`Selected value: ${item.value}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
title: Menu Label
|
||||
description: Menu labels are used to describe a group of menu items.
|
||||
tags: component
|
||||
parent: menu
|
||||
icon: menu
|
||||
---
|
||||
|
||||
```html {.example}
|
||||
<wa-menu style="max-width: 200px;">
|
||||
<wa-menu-label>Fruits</wa-menu-label>
|
||||
<wa-menu-item value="apple">Apple</wa-menu-item>
|
||||
<wa-menu-item value="banana">Banana</wa-menu-item>
|
||||
<wa-menu-item value="orange">Orange</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-label>Vegetables</wa-menu-label>
|
||||
<wa-menu-item value="broccoli">Broccoli</wa-menu-item>
|
||||
<wa-menu-item value="carrot">Carrot</wa-menu-item>
|
||||
<wa-menu-item value="zucchini">Zucchini</wa-menu-item>
|
||||
</wa-menu>
|
||||
```
|
||||
@@ -1,77 +0,0 @@
|
||||
---
|
||||
title: Menu
|
||||
description: Menus provide a list of options for the user to choose from.
|
||||
tags: [actions, apps]
|
||||
icon: menu
|
||||
---
|
||||
|
||||
You can use [menu items](/docs/components/menu-item), [menu labels](/docs/components/menu-label), and [dividers](/docs/components/divider) to compose a menu. Menus support keyboard interactions, including type-to-select an option.
|
||||
|
||||
```html {.example}
|
||||
<wa-menu style="max-width: 200px;">
|
||||
<wa-menu-item value="undo">Undo</wa-menu-item>
|
||||
<wa-menu-item value="redo">Redo</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item value="cut">Cut</wa-menu-item>
|
||||
<wa-menu-item value="copy">Copy</wa-menu-item>
|
||||
<wa-menu-item value="paste">Paste</wa-menu-item>
|
||||
<wa-menu-item value="delete">Delete</wa-menu-item>
|
||||
</wa-menu>
|
||||
```
|
||||
|
||||
:::info
|
||||
Menus are intended for system menus (dropdown menus, select menus, context menus, etc.). They should not be mistaken for navigation menus which serve a different purpose and have a different semantic meaning. If you're building navigation, use `<nav>` and `<a>` elements instead.
|
||||
:::
|
||||
|
||||
## Examples
|
||||
|
||||
### In Dropdowns
|
||||
|
||||
Menus work really well when used inside [dropdowns](/docs/components/dropdown).
|
||||
|
||||
```html {.example}
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Edit</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item value="cut">Cut</wa-menu-item>
|
||||
<wa-menu-item value="copy">Copy</wa-menu-item>
|
||||
<wa-menu-item value="paste">Paste</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
```
|
||||
|
||||
### Submenus
|
||||
|
||||
To create a submenu, nest an `<wa-menu slot="submenu">` in any [menu item](/docs/components/menu-item).
|
||||
|
||||
```html {.example}
|
||||
<wa-menu style="max-width: 200px;">
|
||||
<wa-menu-item value="undo">Undo</wa-menu-item>
|
||||
<wa-menu-item value="redo">Redo</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item value="cut">Cut</wa-menu-item>
|
||||
<wa-menu-item value="copy">Copy</wa-menu-item>
|
||||
<wa-menu-item value="paste">Paste</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item>
|
||||
Find
|
||||
<wa-menu slot="submenu">
|
||||
<wa-menu-item value="find">Find…</wa-menu-item>
|
||||
<wa-menu-item value="find-previous">Find Next</wa-menu-item>
|
||||
<wa-menu-item value="find-next">Find Previous</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
Transformations
|
||||
<wa-menu slot="submenu">
|
||||
<wa-menu-item value="uppercase">Make uppercase</wa-menu-item>
|
||||
<wa-menu-item value="lowercase">Make lowercase</wa-menu-item>
|
||||
<wa-menu-item value="capitalize">Capitalize</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
```
|
||||
|
||||
:::warning
|
||||
As a UX best practice, avoid using more than one level of submenus when possible.
|
||||
:::
|
||||
@@ -28,28 +28,28 @@ Use the `disabled` attribute to disable an option and prevent it from being sele
|
||||
</wa-select>
|
||||
```
|
||||
|
||||
### Prefix & Suffix
|
||||
### Start & End Decorations
|
||||
|
||||
Add icons to the start and end of menu items using the `prefix` and `suffix` slots.
|
||||
Use the `start` and `end` slots to add presentational elements like `<wa-icon>` next to the option label.
|
||||
|
||||
```html {.example}
|
||||
<wa-select label="Select one">
|
||||
<wa-option value="option-1">
|
||||
<wa-icon slot="prefix" name="envelope" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="envelope"></wa-icon>
|
||||
Email
|
||||
<wa-icon slot="suffix" name="circle-check" variant="solid"></wa-icon>
|
||||
<wa-icon slot="end" name="circle-check"></wa-icon>
|
||||
</wa-option>
|
||||
|
||||
<wa-option value="option-2">
|
||||
<wa-icon slot="prefix" name="phone" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="phone"></wa-icon>
|
||||
Phone
|
||||
<wa-icon slot="suffix" name="circle-check" variant="solid"></wa-icon>
|
||||
<wa-icon slot="end" name="circle-check"></wa-icon>
|
||||
</wa-option>
|
||||
|
||||
<wa-option value="option-3">
|
||||
<wa-icon slot="prefix" name="comment" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="comment"></wa-icon>
|
||||
Chat
|
||||
<wa-icon slot="suffix" name="circle-check" variant="solid"></wa-icon>
|
||||
<wa-icon slot="end" name="circle-check"></wa-icon>
|
||||
</wa-option>
|
||||
</wa-select>
|
||||
```
|
||||
|
||||
@@ -137,7 +137,7 @@ Use the [`autofocus`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_a
|
||||
</wa-popover>
|
||||
|
||||
<wa-button id="popover__autofocus">
|
||||
<wa-icon name="comment" slot="prefix"></wa-icon>
|
||||
<wa-icon name="comment" slot="start"></wa-icon>
|
||||
Feedback
|
||||
</wa-button>
|
||||
```
|
||||
@@ -7,9 +7,7 @@ icon: progress-bar
|
||||
---
|
||||
|
||||
```html {.example}
|
||||
<wa-progress-bar value="40">
|
||||
<wa-icon slot="prefix" name="tasks"></wa-icon>
|
||||
</wa-progress-bar>
|
||||
<wa-progress-bar value="40"></wa-progress-bar>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -6,9 +6,7 @@ icon: progress-ring
|
||||
---
|
||||
|
||||
```html {.example}
|
||||
<wa-progress-ring value="25">
|
||||
<wa-icon slot="prefix" name="circle-notch"></wa-icon>
|
||||
</wa-progress-ring>
|
||||
<wa-progress-ring value="25"></wa-progress-ring>
|
||||
```
|
||||
|
||||
## Examples
|
||||
@@ -18,9 +16,7 @@ icon: progress-ring
|
||||
Use the `--size` custom property to set the diameter of the progress ring.
|
||||
|
||||
```html {.example}
|
||||
<wa-progress-ring value="50" style="--size: 200px;">
|
||||
<wa-icon slot="prefix" name="expand"></wa-icon>
|
||||
</wa-progress-ring>
|
||||
<wa-progress-ring value="50" style="--size: 200px;"></wa-progress-ring>
|
||||
```
|
||||
|
||||
### Track and Indicator Width
|
||||
@@ -28,9 +24,7 @@ Use the `--size` custom property to set the diameter of the progress ring.
|
||||
Use the `--track-width` and `--indicator-width` custom properties to set the width of the progress ring's track and indicator.
|
||||
|
||||
```html {.example}
|
||||
<wa-progress-ring value="50" style="--track-width: 6px; --indicator-width: 12px;">
|
||||
<wa-icon slot="prefix" name="arrows-alt"></wa-icon>
|
||||
</wa-progress-ring>
|
||||
<wa-progress-ring value="50" style="--track-width: 6px; --indicator-width: 12px;"></wa-progress-ring>
|
||||
```
|
||||
|
||||
### Colors
|
||||
@@ -45,7 +39,6 @@ To change the color, use the `--track-color` and `--indicator-color` custom prop
|
||||
--indicator-color: deeppink;
|
||||
"
|
||||
>
|
||||
<wa-icon slot="prefix" name="palette"></wa-icon>
|
||||
</wa-progress-ring>
|
||||
```
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ QR codes are useful for providing small pieces of information to users who can q
|
||||
<br />
|
||||
|
||||
<wa-input maxlength="255" with-clear label="Value">
|
||||
<wa-icon slot="prefix" name="link"></wa-icon>
|
||||
<wa-icon slot="start" name="link"></wa-icon>
|
||||
</wa-input>
|
||||
</div>
|
||||
|
||||
@@ -46,9 +46,7 @@ QR codes are useful for providing small pieces of information to users who can q
|
||||
Use the `fill` and `background` attributes to modify the QR code's colors. You should always ensure good contrast for optimal compatibility with QR code scanners.
|
||||
|
||||
```html {.example}
|
||||
<wa-qr-code value="https://shoelace.style/" fill="deeppink" background="white">
|
||||
<wa-icon slot="prefix" name="palette"></wa-icon>
|
||||
</wa-qr-code>
|
||||
<wa-qr-code value="https://shoelace.style/" fill="deeppink" background="white"></wa-qr-code>
|
||||
```
|
||||
|
||||
### Size
|
||||
@@ -56,9 +54,7 @@ Use the `fill` and `background` attributes to modify the QR code's colors. You s
|
||||
Use the `size` attribute to change the size of the QR code.
|
||||
|
||||
```html {.example}
|
||||
<wa-qr-code value="https://shoelace.style/" size="64">
|
||||
<wa-icon slot="prefix" name="expand"></wa-icon>
|
||||
</wa-qr-code>
|
||||
<wa-qr-code value="https://shoelace.style/" size="64"></wa-qr-code>
|
||||
```
|
||||
|
||||
### Radius
|
||||
@@ -66,9 +62,7 @@ Use the `size` attribute to change the size of the QR code.
|
||||
Create a rounded effect with the `radius` attribute.
|
||||
|
||||
```html {.example}
|
||||
<wa-qr-code value="https://shoelace.style/" radius="0.5">
|
||||
<wa-icon slot="prefix" name="circle"></wa-icon>
|
||||
</wa-qr-code>
|
||||
<wa-qr-code value="https://shoelace.style/" radius="0.5"></wa-qr-code>
|
||||
```
|
||||
|
||||
### Error Correction
|
||||
@@ -77,18 +71,10 @@ QR codes can be rendered with various levels of [error correction](https://www.q
|
||||
|
||||
```html {.example}
|
||||
<div class="qr-error-correction">
|
||||
<wa-qr-code value="https://shoelace.style/" error-correction="L">
|
||||
<wa-icon slot="prefix" name="shield"></wa-icon>
|
||||
</wa-qr-code>
|
||||
<wa-qr-code value="https://shoelace.style/" error-correction="M">
|
||||
<wa-icon slot="prefix" name="shield"></wa-icon>
|
||||
</wa-qr-code>
|
||||
<wa-qr-code value="https://shoelace.style/" error-correction="Q">
|
||||
<wa-icon slot="prefix" name="shield"></wa-icon>
|
||||
</wa-qr-code>
|
||||
<wa-qr-code value="https://shoelace.style/" error-correction="H">
|
||||
<wa-icon slot="prefix" name="shield"></wa-icon>
|
||||
</wa-qr-code>
|
||||
<wa-qr-code value="https://shoelace.style/" error-correction="L"></wa-qr-code>
|
||||
<wa-qr-code value="https://shoelace.style/" error-correction="M"></wa-qr-code>
|
||||
<wa-qr-code value="https://shoelace.style/" error-correction="Q"></wa-qr-code>
|
||||
<wa-qr-code value="https://shoelace.style/" error-correction="H"></wa-qr-code>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -208,54 +208,30 @@ The preferred placement of the select's listbox can be set with the `placement`
|
||||
</wa-select>
|
||||
```
|
||||
|
||||
### Prefix Icons
|
||||
### Start & End Decorations
|
||||
|
||||
Use the `prefix` slot to prepend an icon to the control.
|
||||
Use the `start` and `end` slots to add presentational elements like `<wa-icon>` within the combobox.
|
||||
|
||||
```html {.example}
|
||||
<wa-select placeholder="Small" size="small" with-clear>
|
||||
<wa-icon slot="prefix" name="house" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="house" variant="solid"></wa-icon>
|
||||
<wa-icon slot="end" name="flag-checkered"></wa-icon>
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
<wa-option value="option-2">Option 2</wa-option>
|
||||
<wa-option value="option-3">Option 3</wa-option>
|
||||
</wa-select>
|
||||
<br />
|
||||
<wa-select placeholder="Medium" size="medium" with-clear>
|
||||
<wa-icon slot="prefix" name="house" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="house" variant="solid"></wa-icon>
|
||||
<wa-icon slot="end" name="flag-checkered"></wa-icon>
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
<wa-option value="option-2">Option 2</wa-option>
|
||||
<wa-option value="option-3">Option 3</wa-option>
|
||||
</wa-select>
|
||||
<br />
|
||||
<wa-select placeholder="Large" size="large" with-clear>
|
||||
<wa-icon slot="prefix" name="house" variant="solid"></wa-icon>
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
<wa-option value="option-2">Option 2</wa-option>
|
||||
<wa-option value="option-3">Option 3</wa-option>
|
||||
</wa-select>
|
||||
```
|
||||
|
||||
### Suffix Icons
|
||||
|
||||
Use the `suffix` slot to append an icon to the control.
|
||||
|
||||
```html {.example}
|
||||
<wa-select placeholder="Small" size="small" with-clear>
|
||||
<wa-icon name="house" slot="suffix"></wa-icon>
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
<wa-option value="option-2">Option 2</wa-option>
|
||||
<wa-option value="option-3">Option 3</wa-option>
|
||||
</wa-select>
|
||||
<br />
|
||||
<wa-select placeholder="Medium" size="medium" with-clear>
|
||||
<wa-icon name="house" slot="suffix"></wa-icon>
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
<wa-option value="option-2">Option 2</wa-option>
|
||||
<wa-option value="option-3">Option 3</wa-option>
|
||||
</wa-select>
|
||||
<br />
|
||||
<wa-select placeholder="Large" size="large" with-clear>
|
||||
<wa-icon name="house" slot="suffix"></wa-icon>
|
||||
<wa-icon slot="start" name="house" variant="solid"></wa-icon>
|
||||
<wa-icon slot="end" name="flag-checkered"></wa-icon>
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
<wa-option value="option-2">Option 2</wa-option>
|
||||
<wa-option value="option-3">Option 3</wa-option>
|
||||
@@ -277,15 +253,15 @@ Remember that custom tags are rendered in a shadow root. To style them, you can
|
||||
class="custom-tag"
|
||||
>
|
||||
<wa-option value="email">
|
||||
<wa-icon slot="prefix" name="envelope" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="envelope" variant="solid"></wa-icon>
|
||||
Email
|
||||
</wa-option>
|
||||
<wa-option value="phone">
|
||||
<wa-icon slot="prefix" name="phone" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="phone" variant="solid"></wa-icon>
|
||||
Phone
|
||||
</wa-option>
|
||||
<wa-option value="chat">
|
||||
<wa-icon slot="prefix" name="comment" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="comment" variant="solid"></wa-icon>
|
||||
Chat
|
||||
</wa-option>
|
||||
</wa-select>
|
||||
@@ -297,7 +273,7 @@ Remember that custom tags are rendered in a shadow root. To style them, you can
|
||||
|
||||
select.getTag = (option, index) => {
|
||||
// Use the same icon used in wa-option
|
||||
const name = option.querySelector('wa-icon[slot="prefix"]').name;
|
||||
const name = option.querySelector('wa-icon[slot="start"]').name;
|
||||
|
||||
// You can return a string, a Lit Template, or an HTMLElement here
|
||||
return `
|
||||
|
||||
@@ -7,7 +7,20 @@ icon: slider
|
||||
---
|
||||
|
||||
```html {.example}
|
||||
<wa-slider></wa-slider>
|
||||
<wa-slider
|
||||
label="Number of cats"
|
||||
hint="Limit six per household"
|
||||
name="value"
|
||||
value="3"
|
||||
min="0"
|
||||
max="6"
|
||||
with-markers
|
||||
with-tooltip
|
||||
with-references
|
||||
>
|
||||
<span slot="reference">Less</span>
|
||||
<span slot="reference">More</span>
|
||||
</wa-slider>
|
||||
```
|
||||
|
||||
:::info
|
||||
@@ -18,7 +31,7 @@ This component works with standard `<form>` elements. Please refer to the sectio
|
||||
|
||||
### Labels
|
||||
|
||||
Use the `label` attribute to give the range an accessible label. For labels that contain HTML, use the `label` slot instead.
|
||||
Use the `label` attribute to give the slider an accessible label. For labels that contain HTML, use the `label` slot instead.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider label="Volume" min="0" max="100"></wa-slider>
|
||||
@@ -26,18 +39,233 @@ Use the `label` attribute to give the range an accessible label. For labels that
|
||||
|
||||
### Hint
|
||||
|
||||
Add descriptive hint to a range with the `hint` attribute. For hints that contain HTML, use the `hint` slot instead.
|
||||
Add descriptive hint to a slider with the `hint` attribute. For hints that contain HTML, use the `hint` slot instead.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider label="Volume" hint="Controls the volume of the current song." min="0" max="100"></wa-slider>
|
||||
```
|
||||
|
||||
### Min, Max, and Step
|
||||
### Showing tooltips
|
||||
|
||||
Use the `min` and `max` attributes to set the range's minimum and maximum values, respectively. The `step` attribute determines the value's interval when increasing and decreasing.
|
||||
Use the `with-tooltip` attribute to display a tooltip with the current value when the slider is focused or being dragged.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider min="0" max="10" step="1"></wa-slider>
|
||||
<wa-slider label="Quality" name="quality" min="0" max="100" value="50" with-tooltip></wa-slider>
|
||||
```
|
||||
|
||||
### Setting min, max, and step
|
||||
|
||||
Use the `min` and `max` attributes to define the slider's range, and the `step` attribute to control the increment between values.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider label="Between zero and one" min="0" max="1" step="0.1" value="0.5" with-tooltip></wa-slider>
|
||||
```
|
||||
|
||||
### Showing markers
|
||||
|
||||
Use the `with-markers` attribute to display visual indicators at each step increment. This works best with sliders that have a smaller range of values.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider label="Size" name="size" min="0" max="8" value="4" with-markers></wa-slider>
|
||||
```
|
||||
|
||||
### Adding references
|
||||
|
||||
Use the `with-references` attribute along with the `reference` slot to add contextual labels below the slider. References are automatically spaced using `space-between`, making them easy to align with the start, center, and end positions.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider label="Speed" name="speed" min="1" max="5" value="3" with-markers with-references>
|
||||
<span slot="reference">Slow</span>
|
||||
<span slot="reference">Medium</span>
|
||||
<span slot="reference">Fast</span>
|
||||
</wa-slider>
|
||||
```
|
||||
|
||||
:::info
|
||||
If you want to show a reference next to a specific marker, you can add `position: absolute` to it and set the `left`, `right`, `top`, or `bottom` property to a percentage that corresponds to the marker's position.
|
||||
:::
|
||||
|
||||
### Formatting the value
|
||||
|
||||
Customize how values are displayed in tooltips and announced to screen readers using the `valueFormatter` property. Set it to a function that accepts a number and returns a formatted string. The [`Intl.NumberFormat API`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) is particularly useful for this.
|
||||
|
||||
```html {.example}
|
||||
<!-- Percent -->
|
||||
<wa-slider
|
||||
id="slider__percent"
|
||||
label="Percentage"
|
||||
name="percentage"
|
||||
value="0.5"
|
||||
min="0"
|
||||
max="1"
|
||||
step=".01"
|
||||
with-tooltip
|
||||
></wa-slider
|
||||
><br />
|
||||
|
||||
<script>
|
||||
const slider = document.getElementById('slider__percent');
|
||||
const formatter = new Intl.NumberFormat('en-US', { style: 'percent' });
|
||||
|
||||
customElements.whenDefined('wa-slider').then(() => {
|
||||
slider.valueFormatter = value => formatter.format(value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Duration -->
|
||||
<wa-slider id="slider__duration" label="Duration" name="duration" value="12" min="0" max="24" with-tooltip></wa-slider
|
||||
><br />
|
||||
|
||||
<script>
|
||||
const slider = document.getElementById('slider__duration');
|
||||
const formatter = new Intl.NumberFormat('en-US', { style: 'unit', unit: 'hour', unitDisplay: 'long' });
|
||||
|
||||
customElements.whenDefined('wa-slider').then(() => {
|
||||
slider.valueFormatter = value => formatter.format(value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Currency -->
|
||||
<wa-slider id="slider__currency" label="Currency" name="currency" min="0" max="100" value="50" with-tooltip></wa-slider>
|
||||
|
||||
<script>
|
||||
const slider = document.getElementById('slider__currency');
|
||||
const formatter = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
currencyDisplay: 'symbol',
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
|
||||
customElements.whenDefined('wa-slider').then(() => {
|
||||
slider.valueFormatter = value => formatter.format(value);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Range selection
|
||||
|
||||
Use the `range` attribute to enable dual-thumb selection for choosing a range of values. Set the initial thumb positions with the `min-value` and `max-value` attributes.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider
|
||||
label="Price Range"
|
||||
hint="Select minimum and maximum price"
|
||||
name="price"
|
||||
range
|
||||
min="0"
|
||||
max="100"
|
||||
min-value="20"
|
||||
max-value="80"
|
||||
with-tooltip
|
||||
with-references
|
||||
id="slider__range"
|
||||
>
|
||||
<span slot="reference">$0</span>
|
||||
<span slot="reference">$50</span>
|
||||
<span slot="reference">$100</span>
|
||||
</wa-slider>
|
||||
|
||||
<script>
|
||||
const slider = document.getElementById('slider__range');
|
||||
const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
|
||||
|
||||
customElements.whenDefined('wa-slider').then(() => {
|
||||
slider.valueFormatter = value => formatter.format(value);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
For range sliders, the `minValue` and `maxValue` properties represent the current positions of the thumbs. When the form is submitted, both values will be included as separate entries with the same name.
|
||||
|
||||
```ts
|
||||
const slider = document.querySelector('wa-slider[range]');
|
||||
|
||||
// Get the current values
|
||||
console.log(`Min value: ${slider.minValue}, Max value: ${slider.maxValue}`);
|
||||
|
||||
// Set the values programmatically
|
||||
slider.minValue = 30;
|
||||
slider.maxValue = 70;
|
||||
```
|
||||
|
||||
### Vertical Sliders
|
||||
|
||||
Set the `orientation` attribute to `vertical` to create a vertical slider. Vertical sliders automatically center themselves and fill the available vertical space.
|
||||
|
||||
```html {.example}
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<wa-slider orientation="vertical" label="Volume" name="volume" value="65" style="width: 80px"></wa-slider>
|
||||
|
||||
<wa-slider orientation="vertical" label="Bass" name="bass" value="50" style="width: 80px"></wa-slider>
|
||||
|
||||
<wa-slider orientation="vertical" label="Treble" name="treble" value="40" style="width: 80px"></wa-slider>
|
||||
</div>
|
||||
```
|
||||
|
||||
Range sliders can also be vertical.
|
||||
|
||||
```html {.example}
|
||||
<div style="height: 300px; display: flex; align-items: center; gap: 2rem;">
|
||||
<wa-slider
|
||||
label="Temperature Range"
|
||||
orientation="vertical"
|
||||
range
|
||||
min="0"
|
||||
max="100"
|
||||
min-value="30"
|
||||
max-value="70"
|
||||
with-tooltip
|
||||
tooltip-placement="right"
|
||||
id="slider__vertical-range"
|
||||
>
|
||||
</wa-slider>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const slider = document.getElementById('slider__vertical-range');
|
||||
slider.valueFormatter = value => {
|
||||
return new Intl.NumberFormat('en', {
|
||||
style: 'unit',
|
||||
unit: 'fahrenheit',
|
||||
unitDisplay: 'short',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 1,
|
||||
}).format(value);
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### Size
|
||||
|
||||
Control the slider's size using the `size` attribute. Valid options include `small`, `medium`, and `large`.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider size="small" value="50" label="Small"></wa-slider><br />
|
||||
<wa-slider size="medium" value="50" label="Medium"></wa-slider><br />
|
||||
<wa-slider size="large" value="50" label="Large"></wa-slider>
|
||||
```
|
||||
|
||||
### Indicator Offset
|
||||
|
||||
By default, the filled indicator extends from the minimum value to the current position. Use the `indicator-offset` attribute to change the starting point of this visual indicator.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider
|
||||
label="Cat playfulness"
|
||||
hint="Energy level during playtime"
|
||||
name="value"
|
||||
value="0"
|
||||
min="-5"
|
||||
max="5"
|
||||
indicator-offset="0"
|
||||
with-markers
|
||||
with-tooltip
|
||||
with-references
|
||||
>
|
||||
<span slot="reference">Lazy</span>
|
||||
<span slot="reference">Zoomies</span>
|
||||
</wa-slider>
|
||||
```
|
||||
|
||||
### Disabled
|
||||
@@ -45,74 +273,17 @@ Use the `min` and `max` attributes to set the range's minimum and maximum values
|
||||
Use the `disabled` attribute to disable a slider.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider disabled></wa-slider>
|
||||
<wa-slider label="Disabled" value="50" disabled></wa-slider>
|
||||
```
|
||||
|
||||
### Tooltip Placement
|
||||
### Required
|
||||
|
||||
By default, the tooltip is shown on top. Set `tooltip` to `bottom` to show it below the slider.
|
||||
Mark a slider as required using the `required` attribute. Users must interact with required sliders before the form can be submitted.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider tooltip="bottom"></wa-slider>
|
||||
```
|
||||
|
||||
### Disable the Tooltip
|
||||
|
||||
To disable the tooltip, set `tooltip` to `none`.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider tooltip="none"></wa-slider>
|
||||
```
|
||||
|
||||
### Custom Track Colors
|
||||
|
||||
You can customize the active and inactive portions of the track using the `--track-color-active` and `--track-color-inactive` custom properties.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider
|
||||
style="
|
||||
--track-color-active: var(--wa-color-brand-fill-loud);
|
||||
--track-color-inactive: var(--wa-color-brand-fill-normal);
|
||||
"
|
||||
></wa-slider>
|
||||
```
|
||||
|
||||
### Custom Track Offset
|
||||
|
||||
You can customize the initial offset of the active track using the `--track-active-offset` custom property.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider
|
||||
min="-100"
|
||||
max="100"
|
||||
style="
|
||||
--track-color-active: var(--wa-color-brand-fill-loud);
|
||||
--track-color-inactive: var(--wa-color-brand-fill-normal);
|
||||
--track-active-offset: 50%;
|
||||
"
|
||||
></wa-slider>
|
||||
```
|
||||
|
||||
### Custom Tooltip Formatter
|
||||
|
||||
You can change the tooltip's content by setting the `tooltipFormatter` property to a function that accepts the range's value as an argument.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider min="0" max="100" step="1" class="range-with-custom-formatter"></wa-slider>
|
||||
|
||||
<script>
|
||||
const range = document.querySelector('.range-with-custom-formatter');
|
||||
range.tooltipFormatter = value => `Total - ${value}%`;
|
||||
</script>
|
||||
```
|
||||
|
||||
### Right-to-Left languages
|
||||
|
||||
The component adapts to right-to-left (RTL) languages as you would expect.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider dir="rtl"
|
||||
label="مقدار"
|
||||
hint="التحكم في مستوى صوت الأغنية الحالية."
|
||||
style="--track-color-active: var(--wa-color-brand-fill-loud)" value="10"></wa-slider>
|
||||
```
|
||||
<form action="about:blank" target="_blank" method="get">
|
||||
<wa-slider name="slide" label="Required slider" min="0" max="10" required></wa-slider>
|
||||
<br />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
```
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
title: Clamped Color Tokens
|
||||
layout: block
|
||||
---
|
||||
|
||||
{% set tints = ['max-50', 'max-60', 'max-70', 'min-50', 'min-60', 'min-70'] %}
|
||||
{% set hues = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'indigo', 'purple', 'pink', 'gray'] %}
|
||||
|
||||
<table class="colors">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="core-column">Core tint</th>
|
||||
{% for tint in tints -%}
|
||||
<th>{{ tint }}</th>
|
||||
{%- endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% for hue in hues -%}
|
||||
<tr class="wa-color-{{ hue }}">
|
||||
<th>{{ hue | capitalize }}</th>
|
||||
<td class="core-column">
|
||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-on); --key: var(--wa-color-{{ hue }}-key);">
|
||||
{{ palettes[paletteId][hue].maxChromaTint }}
|
||||
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
|
||||
</div>
|
||||
</td>
|
||||
{% for tint in tints -%}
|
||||
<td>
|
||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})">
|
||||
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
|
||||
</div>
|
||||
</td>
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</table>
|
||||
|
||||
<style>
|
||||
.core-column .color.swatch::before {
|
||||
counter-reset: key var(--key);
|
||||
content: counter(key);
|
||||
}
|
||||
</style>
|
||||
@@ -5,71 +5,74 @@ title: Size tests
|
||||
Button size should default to `medium`:
|
||||
|
||||
```html {.example}
|
||||
<wa-button size=small>Small</wa-button>
|
||||
<wa-button size="small">Small</wa-button>
|
||||
<wa-button>Medium</wa-button>
|
||||
<wa-button size=medium>Medium</wa-button>
|
||||
<wa-button size=large>Large</wa-button>
|
||||
<wa-button size="medium">Medium</wa-button>
|
||||
<wa-button size="large">Large</wa-button>
|
||||
```
|
||||
|
||||
If no button size is specified, it should default to that of its ancestor:
|
||||
|
||||
```html {.example}
|
||||
<wa-button-group size="small">
|
||||
<wa-button>Small 1</wa-button>
|
||||
<wa-button>Small 2</wa-button>
|
||||
<wa-button>Small 3</wa-button>
|
||||
<wa-button>Small 1</wa-button>
|
||||
<wa-button>Small 2</wa-button>
|
||||
<wa-button>Small 3</wa-button>
|
||||
</wa-button-group>
|
||||
<br><br>
|
||||
<br /><br />
|
||||
<wa-button-group>
|
||||
<wa-button>Medium 1</wa-button>
|
||||
<wa-button>Medium 2</wa-button>
|
||||
<wa-button>Medium 3</wa-button>
|
||||
<wa-button>Medium 1</wa-button>
|
||||
<wa-button>Medium 2</wa-button>
|
||||
<wa-button>Medium 3</wa-button>
|
||||
</wa-button-group>
|
||||
<br><br>
|
||||
<br /><br />
|
||||
<wa-button-group size="large">
|
||||
<wa-button>Large 1</wa-button>
|
||||
<wa-button>Large 2</wa-button>
|
||||
<wa-button>Large 3</wa-button>
|
||||
<wa-button>Large 1</wa-button>
|
||||
<wa-button>Large 2</wa-button>
|
||||
<wa-button>Large 3</wa-button>
|
||||
</wa-button-group>
|
||||
```
|
||||
|
||||
Dropdown:
|
||||
|
||||
```html {.example}
|
||||
<p>Small dropdown:
|
||||
<wa-dropdown size="small">
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Dropdown Item 1</wa-menu-item>
|
||||
<wa-menu-item>Dropdown Item 2</wa-menu-item>
|
||||
<wa-menu-item>Dropdown Item 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
<p>Small menu:
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-menu size="small">
|
||||
<wa-menu-item>Dropdown Item 1</wa-menu-item>
|
||||
<wa-menu-item>Dropdown Item 2</wa-menu-item>
|
||||
<wa-menu-item>Dropdown Item 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
<p>Small menu item:
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item size="small">Dropdown Item 1</wa-menu-item>
|
||||
<wa-menu-item size="small">Dropdown Item 2</wa-menu-item>
|
||||
<wa-menu-item size="small">Dropdown Item 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
<p>No size:
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Dropdown Item 1</wa-menu-item>
|
||||
<wa-menu-item>Dropdown Item 2</wa-menu-item>
|
||||
<wa-menu-item>Dropdown Item 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
<p>
|
||||
Small dropdown:
|
||||
<wa-dropdown size="small">
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-dropdown-item>Dropdown Item 1</wa-dropdown-item>
|
||||
<wa-dropdown-item>Dropdown Item 2</wa-dropdown-item>
|
||||
<wa-dropdown-item>Dropdown Item 3</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Small menu:
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-dropdown-item size="small">Dropdown Item 1</wa-dropdown-item>
|
||||
<wa-dropdown-item size="small">Dropdown Item 2</wa-dropdown-item>
|
||||
<wa-dropdown-item size="small">Dropdown Item 3</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Small menu item:
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-dropdown-item size="small">Dropdown Item 1</wa-dropdown-item>
|
||||
<wa-dropdown-item size="small">Dropdown Item 2</wa-dropdown-item>
|
||||
<wa-dropdown-item size="small">Dropdown Item 3</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
No size:
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-dropdown-item>Dropdown Item 1</wa-dropdown-item>
|
||||
<wa-dropdown-item>Dropdown Item 2</wa-dropdown-item>
|
||||
<wa-dropdown-item>Dropdown Item 3</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</p>
|
||||
```
|
||||
|
||||
@@ -671,7 +671,7 @@ hasOutline: false
|
||||
<div style="display: grid; grid-template-rows: minmax(0, auto) minmax(0, 1fr); height: 100%; gap: 1rem;">
|
||||
<div style="display: flex; gap: 1.25rem;">
|
||||
<wa-input name="icon-search" autofocus placeholder="Search Icons" with-clear style="flex: 1 1 auto;">
|
||||
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
|
||||
<wa-icon slot="start" name="magnifying-glass"></wa-icon>
|
||||
</wa-input>
|
||||
<wa-select name="icon-variant" value="solid" style="flex: 0 1 auto;">
|
||||
<wa-option value="solid">Solid</wa-option>
|
||||
@@ -2017,7 +2017,7 @@ hasOutline: false
|
||||
<div class="title">
|
||||
<h1 class="hero-title">What you know you can't explain, but you feel it.</h1>
|
||||
<wa-button variant="brand" class="hero-cta">
|
||||
<wa-icon slot="prefix" name="arrow-down"></wa-icon>
|
||||
<wa-icon slot="start" name="arrow-down"></wa-icon>
|
||||
Free Your Mind
|
||||
</wa-button>
|
||||
</div>
|
||||
@@ -2037,11 +2037,11 @@ hasOutline: false
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<wa-button size="small">
|
||||
<wa-icon slot="prefix" name="plus" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="plus" variant="regular"></wa-icon>
|
||||
Add to Cart
|
||||
</wa-button>
|
||||
<wa-button size="small" appearance="outline">
|
||||
<wa-icon slot="prefix" name="bookmark" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="bookmark" variant="regular"></wa-icon>
|
||||
Save
|
||||
</wa-button>
|
||||
</div>
|
||||
@@ -2060,11 +2060,11 @@ hasOutline: false
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<wa-button size="small">
|
||||
<wa-icon slot="prefix" name="plus" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="plus" variant="regular"></wa-icon>
|
||||
Add to Cart
|
||||
</wa-button>
|
||||
<wa-button size="small" appearance="outline">
|
||||
<wa-icon slot="prefix" name="bookmark" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="bookmark" variant="regular"></wa-icon>
|
||||
Save
|
||||
</wa-button>
|
||||
</div>
|
||||
@@ -2082,11 +2082,11 @@ hasOutline: false
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<wa-button size="small">
|
||||
<wa-icon slot="prefix" name="plus" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="plus" variant="regular"></wa-icon>
|
||||
Add to Cart
|
||||
</wa-button>
|
||||
<wa-button size="small" appearance="outline">
|
||||
<wa-icon slot="prefix" name="bookmark" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="bookmark" variant="regular"></wa-icon>
|
||||
Save
|
||||
</wa-button>
|
||||
</div>
|
||||
@@ -2236,7 +2236,7 @@ hasOutline: false
|
||||
</div>
|
||||
<div class="send">
|
||||
<wa-button variant="brand" size="small">
|
||||
<wa-icon slot="prefix" name="paper-plane-top" variant="solid" label="Add File"></wa-icon>
|
||||
<wa-icon slot="start" name="paper-plane-top" variant="solid" label="Add File"></wa-icon>
|
||||
Send
|
||||
</wa-button>
|
||||
</div>
|
||||
@@ -2286,11 +2286,11 @@ hasOutline: false
|
||||
<div style="display: flex; align-items: end; gap: 1rem;">
|
||||
<wa-input type="number" label="How many?"></wa-input>
|
||||
<wa-button variant="brand">
|
||||
<wa-icon slot="prefix" name="bag-shopping" variant="solid" label="Add to Basket"></wa-icon>
|
||||
<wa-icon slot="start" name="bag-shopping" variant="solid" label="Add to Basket"></wa-icon>
|
||||
Add to Basket
|
||||
</wa-button>
|
||||
<wa-button variant="neutral">
|
||||
<wa-icon slot="prefix" name="bookmark" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="bookmark" variant="regular"></wa-icon>
|
||||
Save
|
||||
</wa-button>
|
||||
</div>
|
||||
@@ -2320,24 +2320,22 @@ hasOutline: false
|
||||
<td>
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret size="small">Action</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="check" variant="regular"></wa-icon>
|
||||
Resolved
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="clock" variant="regular"></wa-icon>
|
||||
Pending
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="arrow-rotate-left" variant="regular"></wa-icon>
|
||||
Re-open
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="xmark" variant="regular"></wa-icon>
|
||||
Delete
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="check" variant="regular"></wa-icon>
|
||||
Resolved
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="clock" variant="regular"></wa-icon>
|
||||
Pending
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="arrow-rotate-left" variant="regular"></wa-icon>
|
||||
Re-open
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="xmark" variant="regular"></wa-icon>
|
||||
Delete
|
||||
</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -2348,26 +2346,24 @@ hasOutline: false
|
||||
<td><wa-avatar image="/assets/images/themer/avatar-char.jpg" label="Char" style="margin-right: var(--wa-space-xs)"></wa-avatar> Char McCoy</td>
|
||||
<td style="text-align: center;"><wa-tag variant="success" size="small">Resolved</wa-tag></td>
|
||||
<td>
|
||||
<wa-dropdown>
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret size="small">Action</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="check" variant="regular"></wa-icon>
|
||||
Resolved
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="clock" variant="regular"></wa-icon>
|
||||
Pending
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="arrow-rotate-left" variant="regular"></wa-icon>
|
||||
Re-open
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="xmark" variant="regular"></wa-icon>
|
||||
Delete
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="check" variant="regular"></wa-icon>
|
||||
Resolved
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="clock" variant="regular"></wa-icon>
|
||||
Pending
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="arrow-rotate-left" variant="regular"></wa-icon>
|
||||
Re-open
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="xmark" variant="regular"></wa-icon>
|
||||
Delete
|
||||
</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -2378,26 +2374,24 @@ hasOutline: false
|
||||
<td><wa-avatar initials="DE" label="Avatar with initials: DE" style="margin-right: var(--wa-space-xs)"></wa-avatar> Debbie Evans</td>
|
||||
<td style="text-align: center;"><wa-tag variant="warning" size="small">Pending</wa-tag></td>
|
||||
<td>
|
||||
<wa-dropdown>
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret size="small">Action</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="check" variant="regular"></wa-icon>
|
||||
Resolved
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="clock" variant="regular"></wa-icon>
|
||||
Pending
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="arrow-rotate-left" variant="regular"></wa-icon>
|
||||
Re-open
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="xmark" variant="regular"></wa-icon>
|
||||
Delete
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="check" variant="regular"></wa-icon>
|
||||
Resolved
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="clock" variant="regular"></wa-icon>
|
||||
Pending
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="arrow-rotate-left" variant="regular"></wa-icon>
|
||||
Re-open
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="xmark" variant="regular"></wa-icon>
|
||||
Delete
|
||||
</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -2408,26 +2402,24 @@ hasOutline: false
|
||||
<td></td>
|
||||
<td style="text-align: center;"><wa-tag variant="danger" size="small">Bounced</wa-tag></td>
|
||||
<td>
|
||||
<wa-dropdown>
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret size="small">Action</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="check" variant="regular"></wa-icon>
|
||||
Resolved
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="clock" variant="regular"></wa-icon>
|
||||
Pending
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="arrow-rotate-left" variant="regular"></wa-icon>
|
||||
Re-open
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="xmark" variant="regular"></wa-icon>
|
||||
Delete
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="check" variant="regular"></wa-icon>
|
||||
Resolved
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="clock" variant="regular"></wa-icon>
|
||||
Pending
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="arrow-rotate-left" variant="regular"></wa-icon>
|
||||
Re-open
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="xmark" variant="regular"></wa-icon>
|
||||
Delete
|
||||
</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -2438,26 +2430,24 @@ hasOutline: false
|
||||
<td><wa-avatar image="/assets/images/themer/avatar-dara.jpg" label="Dara" style="margin-right: var(--wa-space-xs)"></wa-avatar> Dara Prescott</td>
|
||||
<td style="text-align: center;"><wa-tag variant="neutral" size="small">Expired</wa-tag></td>
|
||||
<td>
|
||||
<wa-dropdown>
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret size="small">Action</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="check" variant="regular"></wa-icon>
|
||||
Resolved
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="clock" variant="regular"></wa-icon>
|
||||
Pending
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="arrow-rotate-left" variant="regular"></wa-icon>
|
||||
Re-open
|
||||
</wa-menu-item>
|
||||
<wa-menu-item>
|
||||
<wa-icon slot="prefix" name="xmark" variant="regular"></wa-icon>
|
||||
Delete
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="check" variant="regular"></wa-icon>
|
||||
Resolved
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="clock" variant="regular"></wa-icon>
|
||||
Pending
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="arrow-rotate-left" variant="regular"></wa-icon>
|
||||
Re-open
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item>
|
||||
<wa-icon slot="start" name="xmark" variant="regular"></wa-icon>
|
||||
Delete
|
||||
</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -2470,30 +2460,30 @@ hasOutline: false
|
||||
<h2 style="margin-bottom: var(--wa-space-3xl);">Payment</h2>
|
||||
<form>
|
||||
<wa-input type="email" placeholder="ex. tanderson@metacortex.com" label="Email">
|
||||
<wa-icon name="envelope" variant="regular" slot="prefix"></wa-icon>
|
||||
<wa-icon name="envelope" variant="regular" slot="start"></wa-icon>
|
||||
</wa-input>
|
||||
<wa-input placeholder="1234 1234 1234 1234" label="Card Number">
|
||||
<wa-icon name="credit-card" variant="regular" slot="prefix"></wa-icon>
|
||||
<wa-icon name="credit-card" variant="regular" slot="start"></wa-icon>
|
||||
</wa-input>
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<wa-input placeholder="MM / YY" label="Expiration">
|
||||
<wa-icon name="calendar" variant="regular" slot="prefix"></wa-icon>
|
||||
<wa-icon name="calendar" variant="regular" slot="start"></wa-icon>
|
||||
</wa-input>
|
||||
<wa-input placeholder="CVC" label="CVC">
|
||||
<wa-icon name="lock" variant="regular" slot="prefix"></wa-icon>
|
||||
<wa-icon name="lock" variant="regular" slot="start"></wa-icon>
|
||||
</wa-input>
|
||||
</div>
|
||||
<wa-input placeholder="Thomas Anderson" label="Cardholder Name">
|
||||
<wa-icon name="user" variant="regular" slot="prefix"></wa-icon>
|
||||
<wa-icon name="user" variant="regular" slot="start"></wa-icon>
|
||||
</wa-input>
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<wa-select label="Country" value="USA">
|
||||
<wa-icon slot="prefix" name="globe" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="globe" variant="regular"></wa-icon>
|
||||
<wa-option value="USA">United States</wa-option>
|
||||
<wa-option value="CAN">Canada</wa-option>
|
||||
</wa-select>
|
||||
<wa-input placeholder="12345" label="Zip">
|
||||
<wa-icon name="location-dot" variant="regular" slot="prefix"></wa-icon>
|
||||
<wa-icon name="location-dot" variant="regular" slot="start"></wa-icon>
|
||||
</wa-input>
|
||||
</div>
|
||||
<wa-switch checked style="margin: var(--wa-space-2xl) 0 var(--wa-space-3xl) 0;">Sign me up for more offers from this store</wa-switch>
|
||||
|
||||
@@ -14,6 +14,7 @@ During the alpha period, things might break! We take breaking changes very serio
|
||||
|
||||
## Next
|
||||
|
||||
- 🚨 BREAKING: `input` and `change` events on form controls like `<wa-input>` now are always set to `bubble` and `compose`.
|
||||
- 🚨 BREAKING: Greatly simplified how native styles work and removed redundant utilities
|
||||
- Removed `.wa-button`, `.wa-callout` classes
|
||||
- Removed `themes/native/*.css` files; use `native.css` to opt into native styles
|
||||
@@ -31,14 +32,33 @@ During the alpha period, things might break! We take breaking changes very serio
|
||||
- `<wa-tab-group no-scroll-controls>` => `<wa-tab-group without-scroll-controls>`
|
||||
- `<wa-tag removable>` => `<wa-tag with-remove>`
|
||||
- 🚨 BREAKING: removed the `size` attribute from `<wa-card>`; please set the size of child elements on the children directly
|
||||
- 🚨 BREAKING: Greatly simplified the sizing strategy across components and utilities
|
||||
- 🚨 BREAKING: greatly simplified the sizing strategy across components and utilities
|
||||
- Removed `--wa-size`, `--wa-size-smaller`, `--wa-size-larger`, `--wa-space`, `--wa-space-smaller`, and `--wa-space-larger`
|
||||
- Added tokens for `--wa-form-control-padding-inline`, `--wa-form-control-padding-block`, and `--wa-form-control-toggle-size`
|
||||
- Refactored default `--wa-font-size-*` values to use an apparent 1.125 ratio and round rendered values to the nearest whole pixel
|
||||
- Added convenience tokens for `--wa-font-size-smaller` and `--wa-font-size-larger`
|
||||
- Updated components to use relative `em` values for internal padding and margin wherever appropriate
|
||||
- 🚨 BREAKING: removed the `hint` property and slot from `<wa-radio>`; please apply hints directly to `<wa-radio-group>` instead
|
||||
- 🚨 BREAKING: redesigned `<wa-slider>` with extensive new functionality
|
||||
- Added support for range sliders with dual thumbs using the `range` attribute
|
||||
- Added vertical orientation support with `orientation="vertical"`
|
||||
- Added visual markers at each step with `with-markers`
|
||||
- Added contextual reference labels with `with-references` and the `reference` slot
|
||||
- Added tooltips showing current values with `with-tooltip`
|
||||
- Added customizable indicator offset with `indicator-offset` attribute
|
||||
- Added value formatting support with the `valueFormatter` property
|
||||
- Improved the styling API to be consistent and more powerful (no more browser-specific selectors and pseudo elements to style)
|
||||
- Updated to use consistent `with-*` attribute naming pattern
|
||||
- 🚨 BREAKING: removed `<wa-icon-button>`; use `<wa-button><wa-icon name="..." label="..."></wa-icon></wa-button>` instead
|
||||
- 🚨 BREAKING: completely reworked `<wa-dropdown>` to be easier to use
|
||||
- Added `<wa-dropdown-item>`, greatly simplifying the dropdown's markup structure
|
||||
- Removed `<wa-menu>`, `<wa-menu-item>`, and `<wa-menu-label>`; use `<wa-dropdown-item>` and native headings instead
|
||||
- 🚨 BREAKING: renamed all `prefix` and `suffix` slots to `start` and `end`, affecting the following components:
|
||||
- `<wa-breadcrumb-item>`
|
||||
- `<wa-button>`
|
||||
- `<wa-input>`
|
||||
- `<wa-select>`
|
||||
- `<wa-option>`
|
||||
- Added a new free component: `<wa-popover>` (#2 of 14 per stretch goals)
|
||||
- Added a new free component: `<wa-zoomable-frame>` (#3 of 14 per stretch goals)
|
||||
- Added a `min-block-size` to `<wa-divider orientation="vertical">` to ensure the divider is visible regardless of container height [issue:675]
|
||||
@@ -358,4 +378,4 @@ Here's a list of some of the things that have changed since Shoelace v2. For que
|
||||
|
||||
Did we miss something? [Let us know!](https://github.com/shoelace-style/webawesome-alpha/discussions)
|
||||
|
||||
Are you coming from Shoelace? [The 2.x changelog can be found here.](https://shoelace.style/resources/changelog/)
|
||||
Are you coming from Shoelace? [The 2.x changelog can be found here.](https://shoelace.style/resources/changelog/)
|
||||
@@ -18,7 +18,7 @@ The [discussion forum](https://github.com/shoelace-style/shoelace/discussions) i
|
||||
- Learn more about the project, its values, and its roadmap
|
||||
|
||||
<wa-button variant="brand" href="https://github.com/shoelace-style/shoelace/discussions" target="_blank" style="margin-block-end: var(--wa-flow-spacing);">
|
||||
<wa-icon name="github" family="brands" slot="prefix"></wa-icon>
|
||||
<wa-icon name="github" family="brands" slot="start"></wa-icon>
|
||||
Join the Discussion
|
||||
</wa-button>
|
||||
|
||||
@@ -32,7 +32,7 @@ The [community chat](https://discord.gg/mg8f26C) is open to the public and power
|
||||
- Chat live with other designers, developers, and Web Awesome fans
|
||||
|
||||
<wa-button variant="brand" href="https://discord.gg/mg8f26C" target="_blank" style="margin-block-end: var(--wa-flow-spacing);">
|
||||
<wa-icon name="discord" family="brands" slot="prefix"></wa-icon>
|
||||
<wa-icon name="discord" family="brands" slot="start"></wa-icon>
|
||||
Join the Chat
|
||||
</wa-button>
|
||||
|
||||
@@ -43,6 +43,6 @@ Follow [@webawesomer](https://twitter.com/webawesomer) on Twitter for general up
|
||||
**Please avoid using Twitter for support questions.** The [discussion forum](https://github.com/shoelace-style/shoelace/discussions) is a much better place to share code snippets, screenshots, and other troubleshooting info. You'll have much better luck there, as more users will have a chance to help you.
|
||||
|
||||
<wa-button variant="brand" href="https://twitter.com/webawesomer" target="_blank" style="margin-block-end: var(--wa-flow-spacing);">
|
||||
<wa-icon name="twitter" family="brands" slot="prefix"></wa-icon>
|
||||
<wa-icon name="twitter" family="brands" slot="start"></wa-icon>
|
||||
Follow on Twitter
|
||||
</wa-button>
|
||||
|
||||
@@ -26,7 +26,7 @@ unlisted: true
|
||||
{% if theme.fileSlug === 'custom' %}
|
||||
<p>
|
||||
<wa-button href="../edit/" class="edit-link" target="_parent" appearance="outlined">
|
||||
<wa-icon slot="prefix" name="pencil"></wa-icon>
|
||||
<wa-icon slot="start" name="pencil"></wa-icon>
|
||||
Edit theme
|
||||
</wa-button>
|
||||
</p>
|
||||
|
||||
@@ -42,7 +42,7 @@ unlisted: true
|
||||
<wa-tab-panel name="css">
|
||||
<p>
|
||||
<wa-button variant="brand" :href="code.css.blob" :download="cssFilename">
|
||||
<wa-icon name="arrow-down-to-line" variant="solid" slot="prefix"></wa-icon>
|
||||
<wa-icon name="arrow-down-to-line" variant="solid" slot="start"></wa-icon>
|
||||
Download <code v-text="cssFilename"></code>
|
||||
</wa-button>
|
||||
</p>
|
||||
@@ -153,7 +153,7 @@ unlisted: true
|
||||
</icons-card>
|
||||
|
||||
<wa-input label="Font Awesome Pro Kit Code" v-model="theme.icon.kit" placeholder="e.g. f0nta7e50e">
|
||||
<info-tip slot="suffix"><template #content>You need a Font Awesome Pro license to use certain families and styles.</template></info-tip>
|
||||
<info-tip slot="end"><template #content>You need a Font Awesome Pro license to use certain families and styles.</template></info-tip>
|
||||
<a href="https://fontawesome.com/kits" target="_blank" slot="hint" class="wa-caption-m wa-cluster wa-gap-2xs">
|
||||
<span>Find your kit code here</span>
|
||||
<wa-icon name="arrow-up-right-from-square" variant="regular" style="font-size: 0.75em"></wa-icon>
|
||||
|
||||
@@ -36,7 +36,7 @@ noTheme: true
|
||||
<div class="title">
|
||||
<h1 class="hero-title">What you know you can't explain, but you feel it.</h1>
|
||||
<wa-button variant="brand" class="hero-cta">
|
||||
<wa-icon slot="prefix" name="arrow-down"></wa-icon>
|
||||
<wa-icon slot="start" name="arrow-down"></wa-icon>
|
||||
Free Your Mind
|
||||
</wa-button>
|
||||
</div>
|
||||
@@ -56,11 +56,11 @@ noTheme: true
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<wa-button size="small">
|
||||
<wa-icon slot="prefix" name="plus" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="plus" variant="regular"></wa-icon>
|
||||
Add to Cart
|
||||
</wa-button>
|
||||
<wa-button size="small" appearance="outline">
|
||||
<wa-icon slot="prefix" name="bookmark" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="bookmark" variant="regular"></wa-icon>
|
||||
Save
|
||||
</wa-button>
|
||||
</div>
|
||||
@@ -79,11 +79,11 @@ noTheme: true
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<wa-button size="small">
|
||||
<wa-icon slot="prefix" name="plus" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="plus" variant="regular"></wa-icon>
|
||||
Add to Cart
|
||||
</wa-button>
|
||||
<wa-button size="small" appearance="outline">
|
||||
<wa-icon slot="prefix" name="bookmark" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="bookmark" variant="regular"></wa-icon>
|
||||
Save
|
||||
</wa-button>
|
||||
</div>
|
||||
@@ -101,11 +101,11 @@ noTheme: true
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<wa-button size="small">
|
||||
<wa-icon slot="prefix" name="plus" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="plus" variant="regular"></wa-icon>
|
||||
Add to Cart
|
||||
</wa-button>
|
||||
<wa-button size="small" appearance="outline">
|
||||
<wa-icon slot="prefix" name="bookmark" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="bookmark" variant="regular"></wa-icon>
|
||||
Save
|
||||
</wa-button>
|
||||
</div>
|
||||
|
||||
@@ -108,11 +108,11 @@ For example, a button's default slot is used to populate its label.
|
||||
<wa-button>Click me</wa-button>
|
||||
```
|
||||
|
||||
Some components also have _named_ slots. A named slot can be populated by adding a child element with the appropriate `slot` attribute. Notice how the icon below has the `slot="prefix"` attribute? This tells the component to place the icon into its `prefix` slot.
|
||||
Some components also have _named_ slots. A named slot can be populated by adding a child element with the appropriate `slot` attribute. Notice how the icon below has the `slot="start"` attribute? This tells the component to place the icon into its `start` slot.
|
||||
|
||||
```html
|
||||
<wa-button>
|
||||
<wa-icon slot="prefix" name="gear" variant="solid"></wa-icon>
|
||||
<wa-icon slot="start" name="gear" variant="solid"></wa-icon>
|
||||
Settings
|
||||
</wa-button>
|
||||
```
|
||||
|
||||
@@ -37,7 +37,7 @@ Flanks work especially well for asides, inputs with adjacent buttons, and rich d
|
||||
```html {.example}
|
||||
<div class="wa-flank:end wa-gap-xs">
|
||||
<wa-input>
|
||||
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
|
||||
<wa-icon slot="start" name="magnifying-glass"></wa-icon>
|
||||
</wa-input>
|
||||
<wa-button>Search</wa-button>
|
||||
</div>
|
||||
|
||||
@@ -77,16 +77,16 @@ Organized content in bulleted or numbered format with proper nesting support.
|
||||
<ol>
|
||||
<li>List item 1</li>
|
||||
<li>List item 2
|
||||
<ul>
|
||||
<ol>
|
||||
<li>Subitem a</li>
|
||||
<li>Subitem b</li>
|
||||
</ul>
|
||||
</ol>
|
||||
</li>
|
||||
<li>List item 3</li>
|
||||
</ol>
|
||||
```
|
||||
|
||||
### Definition Lists
|
||||
### Description Lists
|
||||
|
||||
Term and definition pairs for glossaries and descriptions.
|
||||
|
||||
@@ -141,19 +141,19 @@ Various text formatting elements for emphasis and semantic meaning.
|
||||
```html {.example}
|
||||
<div class="two-columns">
|
||||
<p><strong>Bold</strong></p>
|
||||
<p><em>Italics</em></p>
|
||||
<p><em>Italic</em></p>
|
||||
<p><u>Underline</u></p>
|
||||
<p><s>Strike-through</s></p>
|
||||
<p><del>Deleted</del></p>
|
||||
<p><ins>Inserted</ins></p>
|
||||
<p><s>Strike-through</s></p>
|
||||
<p><small>Small</small></p>
|
||||
<p><span>Text <sub>Sub</sub></span></p>
|
||||
<p><span>Text <sup>Sup</sup></span></p>
|
||||
<p><span>Subscript <sub>Sub</sub></span></p>
|
||||
<p><span>Superscript <sup>Sup</sup></span></p>
|
||||
<p><abbr title="Abbreviation">Abbr.</abbr></p>
|
||||
<p><kbd>Keyboard</kbd></p>
|
||||
<p><mark>Highlighted</mark></p>
|
||||
<p><a href="#">Link text</a></p>
|
||||
<p><code>Inline code</code></p>
|
||||
<p><kbd>Keyboard</kbd></p>
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -161,11 +161,13 @@ Various text formatting elements for emphasis and semantic meaning.
|
||||
|
||||
Formatted code snippets with proper syntax styling.
|
||||
|
||||
```
|
||||
```html {.example}
|
||||
<pre>
|
||||
// do a thing
|
||||
export function thing() {
|
||||
return true;
|
||||
}
|
||||
</pre>
|
||||
```
|
||||
|
||||
### Images
|
||||
@@ -454,3 +456,12 @@ Multi-line text input fields for longer content.
|
||||
```html {.example}
|
||||
<label>Textarea <textarea placeholder="Type something"></textarea></label>
|
||||
```
|
||||
|
||||
### Fieldsets
|
||||
|
||||
```html {.example}
|
||||
<fieldset>
|
||||
<legend>Legend</legend>
|
||||
Nunc mi ipsum faucibus vitae aliquet nec ullamcorper. Tincidunt id aliquet risus feugiat in ante. Ac turpis egestas integer eget aliquet nibh praesent tristique magna.
|
||||
</fieldset>
|
||||
```
|
||||
@@ -36,10 +36,10 @@ Stacks are well suited for forms, text, and ensuring consistent spacing between
|
||||
```html {.example}
|
||||
<div class="wa-stack">
|
||||
<wa-input label="Email">
|
||||
<wa-icon slot="prefix" name="envelope" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="envelope" variant="regular"></wa-icon>
|
||||
</wa-input>
|
||||
<wa-input label="Password" type="password">
|
||||
<wa-icon slot="prefix" name="lock" variant="regular"></wa-icon>
|
||||
<wa-icon slot="start" name="lock" variant="regular"></wa-icon>
|
||||
</wa-input>
|
||||
<wa-checkbox>Remember me on this device</wa-checkbox>
|
||||
<wa-button>Log In</wa-button>
|
||||
|
||||
@@ -222,7 +222,7 @@ layout: page
|
||||
text-align: left;
|
||||
white-space: wrap;
|
||||
}
|
||||
wa-button.tile::part(suffix) {
|
||||
wa-button.tile::part(end) {
|
||||
display: none;
|
||||
}
|
||||
wa-button.tile {
|
||||
@@ -266,7 +266,7 @@ layout: page
|
||||
<div class="hero-cta">
|
||||
<span><em>Psst!</em> You can pre-order Web Awesome Pro at a low, guaranteed-for-life price — but not for long. Get in while the gettin’s good.</span>
|
||||
<wa-button class="wa-dark" size="small" href="https://www.kickstarter.com/projects/fontawesome/web-awesome">
|
||||
<wa-icon slot="prefix" name="person-running"></wa-icon>
|
||||
<wa-icon slot="start" name="person-running"></wa-icon>
|
||||
Pre-order WA Pro
|
||||
</wa-button>
|
||||
</div>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
version=$1
|
||||
|
||||
# Check for a version number
|
||||
if [ -z "$version" ]; then
|
||||
echo "A version number must be provided as the first and only argument."
|
||||
echo
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "This command will build Web Awesome $version and publish it to the the CDN."
|
||||
echo
|
||||
read -p "🔥 Are you sure you want to do this? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]
|
||||
then
|
||||
# do dangerous stuff
|
||||
echo
|
||||
echo "🚀 OK, blasting off..."
|
||||
|
||||
# build it
|
||||
npm run build || { echo >&2 "❌ Something went wrong."; exit 1; }
|
||||
|
||||
# copy dist-cdn to the CDN
|
||||
aws --profile early-webawesome-com --endpoint-url https://c0c64e1b38a89d8ae060d40170ceef46.r2.cloudflarestorage.com s3 cp ./dist-cdn s3://early-webawesome-com/webawesome@$version/dist --recursive || { echo >&2 "❌ Something went wrong."; exit 1; }
|
||||
|
||||
echo
|
||||
echo "✅ All done. Now go make something awesome!"
|
||||
echo
|
||||
exit 0
|
||||
fi
|
||||
@@ -149,13 +149,14 @@ export async function build(options = {}) {
|
||||
if (process.env.ROOT_DIR) {
|
||||
process.chdir(process.env.ROOT_DIR);
|
||||
}
|
||||
execSync(`tsc --project ./tsconfig.prod.json --outdir "${getCdnDir()}"`);
|
||||
execSync(`tsc --project ./tsconfig.prod.json --outdir "${getCdnDir()}"`, { stdio: 'inherit' });
|
||||
process.chdir(cwd);
|
||||
} catch (error) {
|
||||
process.chdir(cwd);
|
||||
if (!isDeveloping) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return Promise.reject(error.stdout);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,25 +45,25 @@
|
||||
outline-offset: var(--wa-focus-ring-offset);
|
||||
}
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
.start,
|
||||
.end {
|
||||
display: none;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
.start,
|
||||
.end {
|
||||
display: inline-flex;
|
||||
color: var(--wa-color-text-quiet);
|
||||
}
|
||||
|
||||
::slotted([slot='prefix']) {
|
||||
::slotted([slot='start']) {
|
||||
margin-inline-end: var(--wa-space-s);
|
||||
}
|
||||
|
||||
::slotted([slot='suffix']) {
|
||||
::slotted([slot='end']) {
|
||||
margin-inline-start: var(--wa-space-s);
|
||||
}
|
||||
|
||||
|
||||
@@ -122,11 +122,11 @@ describe('<wa-breadcrumb-item>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an element in the slot "prefix" to support prefix icons', () => {
|
||||
describe('when provided an element in the slot "start" to support start icons', () => {
|
||||
it('should pass accessibility tests', async () => {
|
||||
const el = await fixture<WaBreadcrumbItem>(html`
|
||||
<wa-breadcrumb-item>
|
||||
<span class="prefix-example" slot="prefix">/</span>
|
||||
<span class="start-example" slot="start">/</span>
|
||||
Home
|
||||
</wa-breadcrumb-item>
|
||||
`);
|
||||
@@ -136,22 +136,22 @@ describe('<wa-breadcrumb-item>', () => {
|
||||
it('should accept as an assigned child in the shadow root', async () => {
|
||||
const el = await fixture<WaBreadcrumbItem>(html`
|
||||
<wa-breadcrumb-item>
|
||||
<span class="prefix-example" slot="prefix">/</span>
|
||||
<span class="start-example" slot="start">/</span>
|
||||
Home
|
||||
</wa-breadcrumb-item>
|
||||
`);
|
||||
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=prefix]')!;
|
||||
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=start]')!;
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an element in the slot "suffix" to support suffix icons', () => {
|
||||
describe('when provided an element in the slot "end" to support end icons', () => {
|
||||
it('should pass accessibility tests', async () => {
|
||||
const el = await fixture<WaBreadcrumbItem>(html`
|
||||
<wa-breadcrumb-item>
|
||||
<span class="prefix-example" slot="suffix">/</span>
|
||||
<span class="end-example" slot="end">/</span>
|
||||
Security
|
||||
</wa-breadcrumb-item>
|
||||
`);
|
||||
@@ -162,40 +162,16 @@ describe('<wa-breadcrumb-item>', () => {
|
||||
it('should accept as an assigned child in the shadow root', async () => {
|
||||
const el = await fixture<WaBreadcrumbItem>(html`
|
||||
<wa-breadcrumb-item>
|
||||
<span class="prefix-example" slot="suffix">/</span>
|
||||
<span class="end-example" slot="end">/</span>
|
||||
Security
|
||||
</wa-breadcrumb-item>
|
||||
`);
|
||||
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=suffix]')!;
|
||||
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=end]')!;
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when rendering a wa-dropdown in the default slot', () => {
|
||||
it('should not render a link or button tag, but a div wrapper', async () => {
|
||||
const el = await fixture<WaBreadcrumbItem>(html`
|
||||
<wa-breadcrumb-item>
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" size="small" circle>
|
||||
<wa-icon label="More options" name="ellipsis"></wa-icon>
|
||||
</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item type="checkbox" checked>Web Design</wa-menu-item>
|
||||
<wa-menu-item type="checkbox">Web Development</wa-menu-item>
|
||||
<wa-menu-item type="checkbox">Marketing</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
</wa-breadcrumb-item>
|
||||
`);
|
||||
|
||||
await expect(el).to.be.accessible();
|
||||
expect(el.shadowRoot!.querySelector('a')).to.be.null;
|
||||
expect(el.shadowRoot!.querySelector('button')).to.be.null;
|
||||
expect(el.shadowRoot!.querySelector('.label-dropdown')).not.to.be.null;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,14 +12,14 @@ import styles from './breadcrumb-item.css';
|
||||
* @since 2.0
|
||||
*
|
||||
* @slot - The breadcrumb item's label.
|
||||
* @slot prefix - An optional prefix, usually an icon.
|
||||
* @slot suffix - An optional suffix, usually an icon.
|
||||
* @slot start - An element, such as `<wa-icon>`, placed before the label.
|
||||
* @slot end - An element, such as `<wa-icon>`, placed after the label.
|
||||
* @slot separator - The separator to use for the breadcrumb item. This will only change the separator for this item. If
|
||||
* you want to change it for all items in the group, set the separator on `<wa-breadcrumb>` instead.
|
||||
*
|
||||
* @csspart label - The breadcrumb item's label.
|
||||
* @csspart prefix - The container that wraps the prefix.
|
||||
* @csspart suffix - The container that wraps the suffix.
|
||||
* @csspart start - The container that wraps the `start` slot.
|
||||
* @csspart end - The container that wraps the `end` slot.
|
||||
* @csspart separator - The container that wraps the separator.
|
||||
*/
|
||||
@customElement('wa-breadcrumb-item')
|
||||
@@ -71,8 +71,8 @@ export default class WaBreadcrumbItem extends WebAwesomeElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<span part="prefix" class="prefix">
|
||||
<slot name="prefix"></slot>
|
||||
<span part="start" class="start">
|
||||
<slot name="start"></slot>
|
||||
</span>
|
||||
|
||||
${this.renderType === 'link'
|
||||
@@ -103,8 +103,8 @@ export default class WaBreadcrumbItem extends WebAwesomeElement {
|
||||
`
|
||||
: ''}
|
||||
|
||||
<span part="suffix" class="suffix">
|
||||
<slot name="suffix"></slot>
|
||||
<span part="end" class="end">
|
||||
<slot name="end"></slot>
|
||||
</span>
|
||||
|
||||
<span part="separator" class="separator" aria-hidden="true">
|
||||
|
||||
@@ -95,12 +95,12 @@ describe('<wa-breadcrumb>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "prefix" to support prefix icons', () => {
|
||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "start" to support start icons', () => {
|
||||
it('should pass accessibility tests', async () => {
|
||||
const el = await fixture<WaBreadcrumb>(html`
|
||||
<wa-breadcrumb>
|
||||
<wa-breadcrumb-item>
|
||||
<span class="prefix-example" slot="prefix">/</span>
|
||||
<span class="start-example" slot="start">/</span>
|
||||
Home
|
||||
</wa-breadcrumb-item>
|
||||
<wa-breadcrumb-item>First</wa-breadcrumb-item>
|
||||
@@ -112,7 +112,7 @@ describe('<wa-breadcrumb>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "suffix" to support suffix icons', () => {
|
||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "end" to support end icons', () => {
|
||||
it('should pass accessibility tests', async () => {
|
||||
const el = await fixture<WaBreadcrumb>(html`
|
||||
<wa-breadcrumb>
|
||||
@@ -120,7 +120,7 @@ describe('<wa-breadcrumb>', () => {
|
||||
<wa-breadcrumb-item>Second</wa-breadcrumb-item>
|
||||
<wa-breadcrumb-item>Third</wa-breadcrumb-item>
|
||||
<wa-breadcrumb-item>
|
||||
<span class="prefix-example" slot="suffix">/</span>
|
||||
<span class="end-example" slot="end">/</span>
|
||||
Security
|
||||
</wa-breadcrumb-item>
|
||||
</wa-breadcrumb>
|
||||
|
||||
@@ -107,8 +107,8 @@
|
||||
* Label
|
||||
*/
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
.start,
|
||||
.end {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -137,7 +137,7 @@ wa-icon[part~='caret'] {
|
||||
height: 0.875em;
|
||||
}
|
||||
|
||||
.button:has(&) .suffix {
|
||||
.button:has(&) .end {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -150,9 +150,9 @@ wa-icon[part~='caret'] {
|
||||
position: relative;
|
||||
cursor: wait;
|
||||
|
||||
.prefix,
|
||||
.start,
|
||||
.label,
|
||||
.suffix,
|
||||
.end,
|
||||
.caret {
|
||||
visibility: hidden;
|
||||
}
|
||||
@@ -191,11 +191,11 @@ button ::slotted(wa-badge) {
|
||||
* Button spacing
|
||||
*/
|
||||
|
||||
slot[name='prefix']::slotted(*) {
|
||||
slot[name='start']::slotted(*) {
|
||||
margin-inline-end: var(--wa-form-control-padding-inline);
|
||||
}
|
||||
|
||||
slot[name='suffix']::slotted(*),
|
||||
slot[name='end']::slotted(*),
|
||||
.button:not(.visually-hidden-label) [part~='caret'] {
|
||||
margin-inline-start: var(--wa-form-control-padding-inline);
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ import styles from './button.css';
|
||||
* @event wa-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
|
||||
*
|
||||
* @slot - The button's label.
|
||||
* @slot prefix - A presentational prefix icon or similar element.
|
||||
* @slot suffix - A presentational suffix icon or similar element.
|
||||
* @slot start - An element, such as `<wa-icon>`, placed before the label.
|
||||
* @slot end - An element, such as `<wa-icon>`, placed after the label.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart prefix - The container that wraps the prefix.
|
||||
* @csspart start - The container that wraps the `start` slot.
|
||||
* @csspart label - The button's label.
|
||||
* @csspart suffix - The container that wraps the suffix.
|
||||
* @csspart end - The container that wraps the `end` slot.
|
||||
* @csspart caret - The button's caret icon, a `<wa-icon>` element.
|
||||
* @csspart spinner - The spinner that shows when the button is in the loading state.
|
||||
*
|
||||
@@ -60,7 +60,7 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
|
||||
}
|
||||
|
||||
assumeInteractionOn = ['click'];
|
||||
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
||||
private readonly hasSlotController = new HasSlotController(this, '[default]', 'start', 'end');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('.button') button: HTMLButtonElement | HTMLLinkElement;
|
||||
@@ -263,8 +263,8 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
|
||||
loading: this.loading,
|
||||
rtl: this.localize.dir() === 'rtl',
|
||||
'has-label': this.hasSlotController.test('[default]'),
|
||||
'has-prefix': this.hasSlotController.test('prefix'),
|
||||
'has-suffix': this.hasSlotController.test('suffix'),
|
||||
'has-start': this.hasSlotController.test('start'),
|
||||
'has-end': this.hasSlotController.test('end'),
|
||||
'is-icon-button': this.isIconButton,
|
||||
})}
|
||||
?disabled=${ifDefined(isLink ? undefined : this.disabled)}
|
||||
@@ -282,9 +282,9 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
|
||||
@invalid=${this.isButton() ? this.handleInvalid : null}
|
||||
@click=${this.handleClick}
|
||||
>
|
||||
<slot name="prefix" part="prefix" class="prefix"></slot>
|
||||
<slot name="start" part="start" class="start"></slot>
|
||||
<slot part="label" class="label" @slotchange=${this.handleLabelSlotChange}></slot>
|
||||
<slot name="suffix" part="suffix" class="suffix"></slot>
|
||||
<slot name="end" part="end" class="end"></slot>
|
||||
${
|
||||
this.caret
|
||||
? html`
|
||||
|
||||
@@ -132,7 +132,9 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
|
||||
this.hasInteracted = true;
|
||||
this.checked = !this.checked;
|
||||
this.indeterminate = false;
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
|
||||
@watch('defaultChecked')
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
.color-picker {
|
||||
background-color: var(--background-color);
|
||||
border-radius: var(--border-radius);
|
||||
border-style: var(--border-style);
|
||||
border-width: var(--border-width);
|
||||
border-color: var(--border-color);
|
||||
box-shadow: var(--wa-shadow-l);
|
||||
color: var(--color);
|
||||
font: inherit;
|
||||
user-select: none;
|
||||
|
||||
@@ -300,6 +300,7 @@ describe('<wa-color-picker>', () => {
|
||||
await sendKeys({ type: 'fc0' }); // type in a color
|
||||
input.blur(); // commit changes by blurring the field
|
||||
await el.updateComplete;
|
||||
await aTimeout(1);
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
@@ -329,13 +330,6 @@ describe('<wa-color-picker>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should render in a dropdown', async () => {
|
||||
const el = await fixture<WaColorPicker>(html` <wa-color-picker></wa-color-picker> `);
|
||||
const dropdown = el.shadowRoot!.querySelector('wa-dropdown');
|
||||
|
||||
expect(dropdown).to.exist;
|
||||
});
|
||||
|
||||
it('should show opacity slider when opacity is enabled', async () => {
|
||||
const el = await fixture<WaColorPicker>(html` <wa-color-picker opacity></wa-color-picker> `);
|
||||
const opacitySlider = el.shadowRoot!.querySelector('[part*="opacity-slider"]')!;
|
||||
@@ -368,7 +362,7 @@ describe('<wa-color-picker>', () => {
|
||||
<button type="button">Click me</button>
|
||||
</div>
|
||||
`);
|
||||
const colorPicker = el.querySelector('wa-color-picker')!;
|
||||
const colorPicker = el.querySelector<WaColorPicker>('wa-color-picker')!;
|
||||
const trigger = colorPicker.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const button = el.querySelector('button')!;
|
||||
const focusHandler = sinon.spy();
|
||||
@@ -456,7 +450,7 @@ describe('<wa-color-picker>', () => {
|
||||
</form>
|
||||
`);
|
||||
const button = form.querySelector('wa-button')!;
|
||||
const colorPicker = form.querySelector('wa-color-picker')!;
|
||||
const colorPicker = form.querySelector<WaColorPicker>('wa-color-picker')!;
|
||||
colorPicker.value = '#000000';
|
||||
|
||||
await colorPicker.updateComplete;
|
||||
|
||||
@@ -6,7 +6,9 @@ import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { WaInvalidEvent } from '../../events/invalid.js';
|
||||
import { animateWithClass } from '../../internal/animate.js';
|
||||
import { drag } from '../../internal/drag.js';
|
||||
import { waitForEvent } from '../../internal/event.js';
|
||||
import { clamp } from '../../internal/math.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
import { RequiredValidator } from '../../internal/validators/required-validator.js';
|
||||
@@ -18,11 +20,11 @@ import visuallyHidden from '../../styles/utilities/visually-hidden.css';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import '../button-group/button-group.js';
|
||||
import '../button/button.js';
|
||||
import '../dropdown/dropdown.js';
|
||||
import type WaDropdown from '../dropdown/dropdown.js';
|
||||
import '../icon/icon.js';
|
||||
import '../input/input.js';
|
||||
import type WaInput from '../input/input.js';
|
||||
import '../popup/popup.js';
|
||||
import type WaPopup from '../popup/popup.js';
|
||||
import styles from './color-picker.css';
|
||||
|
||||
interface EyeDropperConstructor {
|
||||
@@ -43,8 +45,8 @@ declare const EyeDropper: EyeDropperConstructor;
|
||||
*
|
||||
* @dependency wa-button
|
||||
* @dependency wa-button-group
|
||||
* @dependency wa-dropdown
|
||||
* @dependency wa-input
|
||||
* @dependency wa-popup
|
||||
* @dependency wa-visually-hidden
|
||||
*
|
||||
* @slot label - The color picker's form label. Alternatively, you can use the `label` attribute.
|
||||
@@ -72,15 +74,15 @@ declare const EyeDropper: EyeDropperConstructor;
|
||||
* @csspart input - The text input.
|
||||
* @csspart eye-dropper-button - The eye dropper button.
|
||||
* @csspart eye-dropper-button__base - The eye dropper button's exported `button` part.
|
||||
* @csspart eye-dropper-button__prefix - The eye dropper button's exported `prefix` part.
|
||||
* @csspart eye-dropper-button__start - The eye dropper button's exported `start` part.
|
||||
* @csspart eye-dropper-button__label - The eye dropper button's exported `label` part.
|
||||
* @csspart eye-dropper-button__suffix - The eye dropper button's exported `suffix` part.
|
||||
* @csspart eye-dropper-button__end - The eye dropper button's exported `end` part.
|
||||
* @csspart eye-dropper-button__caret - The eye dropper button's exported `caret` part.
|
||||
* @csspart format-button - The format button.
|
||||
* @csspart format-button__base - The format button's exported `button` part.
|
||||
* @csspart format-button__prefix - The format button's exported `prefix` part.
|
||||
* @csspart format-button__start - The format button's exported `start` part.
|
||||
* @csspart format-button__label - The format button's exported `label` part.
|
||||
* @csspart format-button__suffix - The format button's exported `suffix` part.
|
||||
* @csspart format-button__end - The format button's exported `end` part.
|
||||
* @csspart format-button__caret - The format button's exported `caret` part.
|
||||
*
|
||||
* @cssproperty --background-color - The color picker's background color.
|
||||
@@ -125,7 +127,7 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
// or is the new behavior okay?
|
||||
get validationTarget() {
|
||||
// This puts the popup on the element only if the color picker is expanded.
|
||||
if (this.dropdown?.open) {
|
||||
if (this.popup?.active) {
|
||||
return this.input;
|
||||
}
|
||||
|
||||
@@ -134,7 +136,7 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
return this.trigger;
|
||||
}
|
||||
|
||||
@query('.color-dropdown') dropdown: WaDropdown;
|
||||
@query('.color-popup') popup: WaPopup;
|
||||
@query('[part~="preview"]') previewButton: HTMLButtonElement;
|
||||
@query('[part~="trigger"]') trigger: HTMLButtonElement;
|
||||
|
||||
@@ -210,6 +212,12 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
/** Disables the color picker. */
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the popup is open. You can toggle this attribute to show and hide the popup, or you
|
||||
* can use the `show()` and `hide()` methods and this attribute will reflect the popup's open state.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
/** Shows the opacity slider. Enabling this will cause the formatted value to be HEXA, RGBA, or HSLA. */
|
||||
@property({ type: Boolean }) opacity = false;
|
||||
|
||||
@@ -267,8 +275,11 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
const nextIndex = (formats.indexOf(this.format) + 1) % formats.length;
|
||||
this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl' | 'hsv';
|
||||
this.setColor(this.value || '');
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
|
||||
private handleAlphaDrag(event: PointerEvent) {
|
||||
@@ -288,13 +299,18 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
if (this.value !== currentValue) {
|
||||
currentValue = this.value;
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
},
|
||||
onStop: () => {
|
||||
if (this.value !== initialValue) {
|
||||
initialValue = this.value;
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
},
|
||||
initialEvent: event,
|
||||
@@ -318,13 +334,17 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
if (this.value !== currentValue) {
|
||||
currentValue = this.value;
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
});
|
||||
}
|
||||
},
|
||||
onStop: () => {
|
||||
if (this.value !== initialValue) {
|
||||
initialValue = this.value;
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
},
|
||||
initialEvent: event,
|
||||
@@ -351,14 +371,18 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
if (this.value !== currentValue) {
|
||||
currentValue = this.value;
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
},
|
||||
onStop: () => {
|
||||
this.isDraggingGridHandle = false;
|
||||
if (this.value !== initialValue) {
|
||||
initialValue = this.value;
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
},
|
||||
initialEvent: event,
|
||||
@@ -394,8 +418,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,8 +454,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,8 +490,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,8 +512,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,8 +535,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
this.input.value = this.value;
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => this.input.select());
|
||||
@@ -688,8 +722,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
this.setColor(colorSelectionResult.sRGBHex);
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -704,8 +740,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
this.setColor(color);
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -790,8 +828,8 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
elementToBlur.blur();
|
||||
}
|
||||
|
||||
if (this.dropdown?.open) {
|
||||
this.dropdown.hide();
|
||||
if (this.popup?.active) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -839,10 +877,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
// This won't get called when a form is submitted. This is only for manual calls.
|
||||
if (!this.validity.valid && !this.dropdown.open) {
|
||||
// Show the dropdown so the browser can focus on it
|
||||
if (!this.validity.valid && !this.open) {
|
||||
// Show the popup so the browser can focus on it
|
||||
this.addEventListener('wa-after-show', this.reportValidityAfterShow, { once: true });
|
||||
this.dropdown.show();
|
||||
this.show();
|
||||
|
||||
if (!this.disabled) {
|
||||
// By standards we have to emit a `wa-invalid` event here synchronously.
|
||||
@@ -867,6 +905,158 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
this.hasEyeDropper = 'EyeDropper' in window;
|
||||
}
|
||||
|
||||
private handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Close when escape is pressed inside an open popup. We need to listen on the panel itself and stop propagation
|
||||
// in case any ancestors are also listening for this key.
|
||||
if (this.open && event.key === 'Escape') {
|
||||
event.stopPropagation();
|
||||
this.hide();
|
||||
this.focus();
|
||||
}
|
||||
};
|
||||
|
||||
private handleDocumentKeyDown = (event: KeyboardEvent) => {
|
||||
// Close when escape or tab is pressed
|
||||
if (event.key === 'Escape' && this.open) {
|
||||
event.stopPropagation();
|
||||
this.focus();
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle tabbing
|
||||
if (event.key === 'Tab') {
|
||||
// Tabbing outside of the containing element closes the panel
|
||||
//
|
||||
// If the popup is used within a shadow DOM, we need to obtain the activeElement within that shadowRoot,
|
||||
// otherwise `document.activeElement` will only return the name of the parent shadow DOM element.
|
||||
setTimeout(() => {
|
||||
const activeElement =
|
||||
this.getRootNode() instanceof ShadowRoot
|
||||
? document.activeElement?.shadowRoot?.activeElement
|
||||
: document.activeElement;
|
||||
|
||||
if (!this || activeElement?.closest(this.tagName.toLowerCase()) !== this) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private handleDocumentMouseDown = (event: MouseEvent) => {
|
||||
// Close when clicking outside of the popup panel and trigger
|
||||
const path = event.composedPath();
|
||||
|
||||
// Check if click is inside the popup panel or the trigger element specifically
|
||||
const isInsideRelevantArea = path.some(
|
||||
element => element instanceof Element && (element.closest('.color-picker') || element === this.trigger),
|
||||
);
|
||||
|
||||
if (this && !isInsideRelevantArea) {
|
||||
this.hide();
|
||||
}
|
||||
};
|
||||
|
||||
handleTriggerClick() {
|
||||
if (this.open) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
async handleTriggerKeyDown(event: KeyboardEvent) {
|
||||
// When spacebar/enter is pressed, show the panel but don't focus on the menu. This let's the user press the same
|
||||
// key again to hide the menu in case they don't want to make a selection.
|
||||
if ([' ', 'Enter'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
this.handleTriggerClick();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handleTriggerKeyUp(event: KeyboardEvent) {
|
||||
// Prevent space from triggering a click event in Firefox
|
||||
if (event.key === ' ') {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
updateAccessibleTrigger() {
|
||||
const accessibleTrigger = this.trigger;
|
||||
|
||||
if (accessibleTrigger) {
|
||||
accessibleTrigger.setAttribute('aria-haspopup', 'true');
|
||||
accessibleTrigger.setAttribute('aria-expanded', this.open ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the color picker panel. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
return waitForEvent(this, 'wa-after-show');
|
||||
}
|
||||
|
||||
/** Hides the color picker panel */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
return waitForEvent(this, 'wa-after-hide');
|
||||
}
|
||||
|
||||
addOpenListeners() {
|
||||
this.base.addEventListener('keydown', this.handleKeyDown);
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
}
|
||||
|
||||
removeOpenListeners() {
|
||||
if (this.base) {
|
||||
this.base.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
async handleOpenChange() {
|
||||
if (this.disabled) {
|
||||
this.open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateAccessibleTrigger();
|
||||
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.dispatchEvent(new CustomEvent('wa-show'));
|
||||
|
||||
this.addOpenListeners();
|
||||
await this.updateComplete;
|
||||
this.base.hidden = false;
|
||||
this.popup.active = true;
|
||||
await animateWithClass(this.popup.popup, 'show-with-scale');
|
||||
this.dispatchEvent(new CustomEvent('wa-after-show'));
|
||||
} else {
|
||||
// Hide
|
||||
this.dispatchEvent(new CustomEvent('wa-hide'));
|
||||
|
||||
this.removeOpenListeners();
|
||||
await animateWithClass(this.popup.popup, 'hide-with-scale');
|
||||
this.base.hidden = true;
|
||||
this.popup.active = false;
|
||||
this.dispatchEvent(new CustomEvent('wa-after-hide'));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const hasLabelSlot = !this.hasUpdated ? this.withLabel : this.withLabel || this.hasSlotController.test('label');
|
||||
const hasHintSlot = !this.hasUpdated ? this.withHint : this.withHint || this.hasSlotController.test('hint');
|
||||
@@ -1021,9 +1211,9 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
aria-label=${this.localize.term('toggleColorFormat')}
|
||||
exportparts="
|
||||
base:format-button__base,
|
||||
prefix:format-button__prefix,
|
||||
start:format-button__start,
|
||||
label:format-button__label,
|
||||
suffix:format-button__suffix,
|
||||
end:format-button__end,
|
||||
caret:format-button__caret
|
||||
"
|
||||
@click=${this.handleFormatToggle}
|
||||
@@ -1042,9 +1232,9 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
appearance="outlined"
|
||||
exportparts="
|
||||
base:eye-dropper-button__base,
|
||||
prefix:eye-dropper-button__prefix,
|
||||
start:eye-dropper-button__start,
|
||||
label:eye-dropper-button__label,
|
||||
suffix:eye-dropper-button__suffix,
|
||||
end:eye-dropper-button__end,
|
||||
caret:eye-dropper-button__caret
|
||||
"
|
||||
@click=${this.handleEyeDropper}
|
||||
@@ -1095,82 +1285,64 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Render as a dropdown
|
||||
// Render with popup
|
||||
return html`
|
||||
<wa-dropdown
|
||||
class="color-dropdown"
|
||||
<div
|
||||
class=${classMap({
|
||||
container: true,
|
||||
'form-control': true,
|
||||
'form-control-has-label': hasLabel,
|
||||
})}
|
||||
part="trigger-container form-control"
|
||||
>
|
||||
<div part="form-control-label" class="label" id="form-control-label">
|
||||
<slot name="label">${this.label}</slot>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="trigger"
|
||||
part="trigger form-control-input"
|
||||
class=${classMap({
|
||||
trigger: true,
|
||||
'trigger-empty': this.isEmpty,
|
||||
'transparent-bg': true,
|
||||
'form-control-input': true,
|
||||
})}
|
||||
style=${styleMap({
|
||||
color: this.getHexString(this.hue, this.saturation, this.brightness, this.alpha),
|
||||
})}
|
||||
type="button"
|
||||
aria-labelledby="form-control-label"
|
||||
aria-describedby="hint"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this.handleTriggerClick}
|
||||
@keydown=${this.handleTriggerKeyDown}
|
||||
@keyup=${this.handleTriggerKeyUp}
|
||||
></button>
|
||||
|
||||
<slot
|
||||
name="hint"
|
||||
part="hint"
|
||||
class=${classMap({
|
||||
'has-slotted': hasHint,
|
||||
})}
|
||||
>${this.hint}</slot
|
||||
>
|
||||
</div>
|
||||
|
||||
<wa-popup
|
||||
class="color-popup"
|
||||
anchor="trigger"
|
||||
placement="bottom-start"
|
||||
distance="0"
|
||||
skidding="0"
|
||||
sync="width"
|
||||
aria-disabled=${this.disabled ? 'true' : 'false'}
|
||||
.containingElement=${this}
|
||||
?disabled=${this.disabled}
|
||||
@wa-after-show=${this.handleAfterShow}
|
||||
@wa-after-hide=${this.handleAfterHide}
|
||||
>
|
||||
<div
|
||||
class=${classMap({
|
||||
container: true,
|
||||
'form-control': true,
|
||||
'form-control-has-label': hasLabel,
|
||||
})}
|
||||
part="trigger-container form-control"
|
||||
slot="trigger"
|
||||
@click=${(e: Event) => {
|
||||
const composedPath = e.composedPath();
|
||||
const triggerButton = this.triggerButton;
|
||||
const triggerLabel = this.triggerLabel;
|
||||
const buttonOrLabelClicked = composedPath.find(el => el === triggerButton || el === triggerLabel);
|
||||
|
||||
if (buttonOrLabelClicked) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop clicks from bubbling on anything except the button and the label. This is a hacky work around i may come to regret, but this "fixes" the issue of `<wa-dropdown>` expecting all children in the "trigger slot" to open the trigger. [Konnor]
|
||||
e.stopImmediatePropagation();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this.dropdown.open) {
|
||||
this.dropdown.hide();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div part="form-control-label" class="label" id="form-control-label">
|
||||
<slot name="label">${this.label}</slot>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="trigger"
|
||||
part="trigger form-control-input"
|
||||
class=${classMap({
|
||||
trigger: true,
|
||||
'trigger-empty': this.isEmpty,
|
||||
'transparent-bg': true,
|
||||
'form-control-input': true,
|
||||
})}
|
||||
style=${styleMap({
|
||||
color: this.getHexString(this.hue, this.saturation, this.brightness, this.alpha),
|
||||
})}
|
||||
type="button"
|
||||
aria-labelledby="form-control-label"
|
||||
aria-describedby="hint"
|
||||
.disabled=${this.disabled}
|
||||
></button>
|
||||
|
||||
<slot
|
||||
name="hint"
|
||||
part="hint"
|
||||
class=${classMap({
|
||||
'has-slotted': hasHint,
|
||||
})}
|
||||
>${this.hint}</slot
|
||||
>
|
||||
</div>
|
||||
${colorPicker}
|
||||
</wa-dropdown>
|
||||
</wa-popup>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'wa-color-picker': WaColorPicker;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
--border-radius: var(--wa-panel-border-radius);
|
||||
--box-shadow: var(--wa-shadow-l);
|
||||
--width: 31rem;
|
||||
--spacing: var(--wa-space-xl);
|
||||
--spacing: var(--wa-space-l);
|
||||
--show-duration: 200ms;
|
||||
--hide-duration: 200ms;
|
||||
|
||||
|
||||
@@ -63,10 +63,7 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
|
||||
@query('.dialog') dialog: HTMLDialogElement;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the dialog is open. You can toggle this attribute to show and hide the dialog, or you can
|
||||
* use the `show()` and `hide()` methods and this attribute will reflect the dialog's open state.
|
||||
*/
|
||||
/** Indicates whether or not the dialog is open. Toggle this attribute to show and hide the dialog. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
--background-color: var(--wa-color-surface-raised);
|
||||
--box-shadow: var(--wa-shadow-l);
|
||||
--size: 25rem;
|
||||
--spacing: var(--wa-space-xl);
|
||||
--spacing: var(--wa-space-l);
|
||||
--show-duration: 200ms;
|
||||
--hide-duration: 200ms;
|
||||
|
||||
|
||||
@@ -68,10 +68,7 @@ export default class WaDrawer extends WebAwesomeElement {
|
||||
|
||||
@query('.drawer') drawer: HTMLDialogElement;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the drawer is open. You can toggle this attribute to show and hide the drawer, or you can
|
||||
* use the `show()` and `hide()` methods and this attribute will reflect the drawer's open state.
|
||||
*/
|
||||
/** Indicates whether or not the drawer is open. Toggle this attribute to show and hide the drawer. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
:host {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
padding: 0.5em 1em;
|
||||
border-radius: var(--wa-border-radius-s);
|
||||
isolation: isolate;
|
||||
color: var(--wa-color-text-normal);
|
||||
line-height: var(--wa-line-height-condensed);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
100ms background-color ease,
|
||||
100ms color ease;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
:host(:hover:not(:state(disabled))) {
|
||||
background-color: var(--wa-color-neutral-fill-normal);
|
||||
}
|
||||
}
|
||||
|
||||
:host(:focus-visible) {
|
||||
z-index: 1;
|
||||
outline: var(--wa-focus-ring);
|
||||
background-color: var(--wa-color-neutral-fill-normal);
|
||||
}
|
||||
|
||||
:host(:state(disabled)) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Danger variant */
|
||||
:host([variant='danger']),
|
||||
:host([variant='danger']) #details {
|
||||
color: var(--wa-color-danger-on-quiet);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
:host([variant='danger']:hover) {
|
||||
background-color: var(--wa-color-danger-fill-normal);
|
||||
color: var(--wa-color-danger-on-normal);
|
||||
}
|
||||
}
|
||||
|
||||
:host([variant='danger']:focus-visible) {
|
||||
background-color: var(--wa-color-danger-fill-normal);
|
||||
color: var(--wa-color-danger-on-normal);
|
||||
}
|
||||
|
||||
:host([checkbox-adjacent]) {
|
||||
padding-inline-start: 2em;
|
||||
}
|
||||
|
||||
/* Only add padding when item actually has a submenu */
|
||||
:host([submenu-adjacent]:not(:state(has-submenu))) #details {
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
|
||||
:host(:state(has-submenu)[submenu-adjacent]) #details {
|
||||
padding-inline-end: 1.75em;
|
||||
}
|
||||
|
||||
#check {
|
||||
visibility: hidden;
|
||||
margin-inline-start: -1.5em;
|
||||
margin-inline-end: 0.5em;
|
||||
font-size: var(--wa-font-size-smaller);
|
||||
}
|
||||
|
||||
:host(:state(checked)) #check {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#icon ::slotted(*) {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
margin-inline-end: 0.75em !important;
|
||||
font-size: var(--wa-font-size-smaller);
|
||||
}
|
||||
|
||||
#label {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#details {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
color: var(--wa-color-text-quiet);
|
||||
font-size: var(--wa-font-size-smaller) !important;
|
||||
}
|
||||
|
||||
#details ::slotted(*) {
|
||||
margin-inline-start: 2em !important;
|
||||
}
|
||||
|
||||
/* Submenu indicator icon */
|
||||
#submenu-indicator {
|
||||
position: absolute;
|
||||
inset-inline-end: 1em;
|
||||
color: var(--wa-color-neutral-on-quiet);
|
||||
font-size: var(--wa-font-size-smaller);
|
||||
}
|
||||
|
||||
/* Flip chevron icon when RTL */
|
||||
:host(:dir(rtl)) #submenu-indicator {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
/* Submenu styles */
|
||||
#submenu {
|
||||
display: flex;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
flex-direction: column;
|
||||
width: max-content;
|
||||
margin: 0;
|
||||
padding: 0.25em;
|
||||
border: var(--wa-border-style) var(--wa-border-width-s) var(--wa-color-surface-border);
|
||||
border-radius: var(--wa-border-radius-m);
|
||||
background-color: var(--wa-color-surface-raised);
|
||||
box-shadow: var(--wa-shadow-m);
|
||||
color: var(--wa-color-text-normal);
|
||||
text-align: start;
|
||||
user-select: none;
|
||||
|
||||
/* Override default popover styles */
|
||||
&[popover] {
|
||||
margin: 0;
|
||||
inset: auto;
|
||||
padding: 0.25em;
|
||||
overflow: visible;
|
||||
border-radius: var(--wa-border-radius-m);
|
||||
}
|
||||
|
||||
&.show {
|
||||
animation: submenu-show var(--show-duration, 50ms) ease;
|
||||
}
|
||||
|
||||
&.hide {
|
||||
animation: submenu-show var(--show-duration, 50ms) ease reverse;
|
||||
}
|
||||
|
||||
/* Submenu placement transform origins */
|
||||
&[data-placement^='top'] {
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
&[data-placement^='bottom'] {
|
||||
transform-origin: top;
|
||||
}
|
||||
|
||||
&[data-placement^='left'] {
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
&[data-placement^='right'] {
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
&[data-placement='left-start'] {
|
||||
transform-origin: right top;
|
||||
}
|
||||
|
||||
&[data-placement='left-end'] {
|
||||
transform-origin: right bottom;
|
||||
}
|
||||
|
||||
&[data-placement='right-start'] {
|
||||
transform-origin: left top;
|
||||
}
|
||||
|
||||
&[data-placement='right-end'] {
|
||||
transform-origin: left bottom;
|
||||
}
|
||||
|
||||
/* Safe triangle styling */
|
||||
&::before {
|
||||
display: none;
|
||||
z-index: 9;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: transparent;
|
||||
content: '';
|
||||
clip-path: polygon(
|
||||
var(--safe-triangle-cursor-x, 0) var(--safe-triangle-cursor-y, 0),
|
||||
var(--safe-triangle-submenu-start-x, 0) var(--safe-triangle-submenu-start-y, 0),
|
||||
var(--safe-triangle-submenu-end-x, 0) var(--safe-triangle-submenu-end-y, 0)
|
||||
);
|
||||
pointer-events: auto; /* Enable mouse events on the triangle */
|
||||
}
|
||||
|
||||
&[data-visible]::before {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
::slotted(wa-dropdown-item) {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
::slotted(wa-divider) {
|
||||
--spacing: 0.25em;
|
||||
}
|
||||
|
||||
@keyframes submenu-show {
|
||||
from {
|
||||
scale: 0.9;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
scale: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
describe('<wa-dropdown-item>', () => {
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <wa-dropdown-item></wa-dropdown-item> `);
|
||||
|
||||
expect(el).to.exist;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,302 @@
|
||||
import type { PropertyValues } from 'lit';
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { animateWithClass } from '../../internal/animate.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import styles from './dropdown-item.css';
|
||||
|
||||
/**
|
||||
* @summary Represents an individual item within a dropdown menu, supporting standard items, checkboxes, and submenus.
|
||||
* @documentation https://backers.webawesome.com/docs/components/dropdown-item
|
||||
* @status experimental
|
||||
* @since 3.0
|
||||
*
|
||||
* @dependency wa-icon
|
||||
*
|
||||
* @event blur - Emitted when the dropdown item loses focus.
|
||||
* @event focus - Emitted when the dropdown item gains focus.
|
||||
*
|
||||
* @slot - The dropdown item's label.
|
||||
* @slot icon - An optional icon to display before the label.
|
||||
* @slot details - Additional content or details to display after the label.
|
||||
* @slot submenu - Submenu items, typically `<wa-dropdown-item>` elements, to create a nested menu.
|
||||
*
|
||||
* @csspart checkmark - The checkmark icon (a `<wa-icon>` element) when the item is a checkbox.
|
||||
* @csspart icon - The container for the icon slot.
|
||||
* @csspart label - The container for the label slot.
|
||||
* @csspart details - The container for the details slot.
|
||||
* @csspart submenu-icon - The submenu indicator icon (a `<wa-icon>` element).
|
||||
* @csspart submenu - The submenu container.
|
||||
*/
|
||||
@customElement('wa-dropdown-item')
|
||||
export default class WaDropdownItem extends WebAwesomeElement {
|
||||
static css = styles;
|
||||
|
||||
private readonly hasSlotController = new HasSlotController(this, '[default]', 'start', 'end');
|
||||
|
||||
@query('#submenu') submenuElement: HTMLDivElement;
|
||||
|
||||
/** @internal The controller will set this property to true when the item is active. */
|
||||
@property({ type: Boolean }) active = false;
|
||||
|
||||
/** The type of menu item to render. */
|
||||
@property({ reflect: true }) variant: 'danger' | 'default' = 'default';
|
||||
|
||||
/**
|
||||
* @internal The dropdown item's size.
|
||||
*/
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/**
|
||||
* @internal The controller will set this property to true when at least one checkbox exists in the dropdown. This
|
||||
* allows non-checkbox items to draw additional space to align properly with checkbox items.
|
||||
*/
|
||||
@property({ attribute: 'checkbox-adjacent', type: Boolean, reflect: true }) checkboxAdjacent = false;
|
||||
|
||||
/**
|
||||
* @internal The controller will set this property to true when at least one item with a submenu exists in the
|
||||
* dropdown. This allows non-submenu items to draw additional space to align properly with items that have submenus.
|
||||
*/
|
||||
@property({ attribute: 'submenu-adjacent', type: Boolean, reflect: true }) submenuAdjacent = false;
|
||||
|
||||
/**
|
||||
* An optional value for the menu item. This is useful for determining which item was selected when listening to the
|
||||
* dropdown's `wa-select` event.
|
||||
*/
|
||||
@property() value: string;
|
||||
|
||||
/** Set to `checkbox` to make the item a checkbox. */
|
||||
@property({ reflect: true }) type: 'normal' | 'checkbox' = 'normal';
|
||||
|
||||
/** Set to true to check the dropdown item. Only valid when `type` is `checkbox`. */
|
||||
@property({ type: Boolean }) checked = false;
|
||||
|
||||
/** Disables the dropdown item. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** Whether the submenu is currently open. */
|
||||
@property({ type: Boolean, reflect: true }) submenuOpen = false;
|
||||
|
||||
/** @internal Store whether this item has a submenu */
|
||||
@state() hasSubmenu = false;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener('mouseenter', this.handleMouseEnter.bind(this));
|
||||
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.closeSubmenu();
|
||||
this.removeEventListener('mouseenter', this.handleMouseEnter);
|
||||
this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.setAttribute('tabindex', '-1');
|
||||
this.hasSubmenu = this.hasSlotController.test('submenu');
|
||||
this.updateHasSubmenuState();
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has('active')) {
|
||||
this.setAttribute('tabindex', this.active ? '0' : '-1');
|
||||
this.customStates.set('active', this.active);
|
||||
}
|
||||
|
||||
if (changedProperties.has('checked')) {
|
||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||
this.customStates.set('checked', this.checked);
|
||||
}
|
||||
|
||||
if (changedProperties.has('disabled')) {
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
this.customStates.set('disabled', this.disabled);
|
||||
}
|
||||
|
||||
if (changedProperties.has('type')) {
|
||||
if (this.type === 'checkbox') {
|
||||
this.setAttribute('role', 'menuitemcheckbox');
|
||||
} else {
|
||||
this.setAttribute('role', 'menuitem');
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has('submenuOpen')) {
|
||||
this.customStates.set('submenu-open', this.submenuOpen);
|
||||
if (this.submenuOpen) {
|
||||
this.openSubmenu();
|
||||
} else {
|
||||
this.closeSubmenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleSlotChange = () => {
|
||||
this.hasSubmenu = this.hasSlotController.test('submenu');
|
||||
this.updateHasSubmenuState();
|
||||
|
||||
if (this.hasSubmenu) {
|
||||
this.setAttribute('aria-haspopup', 'menu');
|
||||
this.setAttribute('aria-expanded', this.submenuOpen ? 'true' : 'false');
|
||||
} else {
|
||||
this.removeAttribute('aria-haspopup');
|
||||
this.removeAttribute('aria-expanded');
|
||||
}
|
||||
};
|
||||
|
||||
/** Update the has-submenu custom state */
|
||||
private updateHasSubmenuState() {
|
||||
this.customStates.set('has-submenu', this.hasSubmenu);
|
||||
}
|
||||
|
||||
/** Opens the submenu. */
|
||||
async openSubmenu() {
|
||||
if (!this.hasSubmenu || !this.submenuElement) return;
|
||||
|
||||
// Notify parent dropdown to handle positioning
|
||||
this.notifyParentOfOpening();
|
||||
|
||||
// Use Popover API to show the submenu
|
||||
this.submenuElement.showPopover();
|
||||
this.submenuElement.hidden = false;
|
||||
this.submenuElement.setAttribute('data-visible', '');
|
||||
this.submenuOpen = true;
|
||||
this.setAttribute('aria-expanded', 'true');
|
||||
|
||||
// Animate the submenu
|
||||
await animateWithClass(this.submenuElement, 'show');
|
||||
|
||||
// Set focus to the first submenu item
|
||||
setTimeout(() => {
|
||||
const items = this.getSubmenuItems();
|
||||
if (items.length > 0) {
|
||||
items.forEach((item, index) => (item.active = index === 0));
|
||||
items[0].focus();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/** Notifies the parent dropdown that this item is opening its submenu */
|
||||
private notifyParentOfOpening() {
|
||||
// First notify the parent that we're about to open
|
||||
const event = new CustomEvent('submenu-opening', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { item: this },
|
||||
});
|
||||
this.dispatchEvent(event);
|
||||
|
||||
// Find sibling items that have open submenus and close them
|
||||
const parent = this.parentElement;
|
||||
if (parent) {
|
||||
const siblings = [...parent.children].filter(
|
||||
el =>
|
||||
el !== this &&
|
||||
el.localName === 'wa-dropdown-item' &&
|
||||
el.getAttribute('slot') === this.getAttribute('slot') &&
|
||||
(el as WaDropdownItem).submenuOpen,
|
||||
) as WaDropdownItem[];
|
||||
|
||||
// Close each sibling submenu with animation
|
||||
siblings.forEach(sibling => {
|
||||
sibling.submenuOpen = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Closes the submenu. */
|
||||
async closeSubmenu() {
|
||||
if (!this.hasSubmenu || !this.submenuElement) return;
|
||||
|
||||
this.submenuOpen = false;
|
||||
this.setAttribute('aria-expanded', 'false');
|
||||
|
||||
if (!this.submenuElement.hidden) {
|
||||
await animateWithClass(this.submenuElement, 'hide');
|
||||
this.submenuElement.hidden = true;
|
||||
this.submenuElement.removeAttribute('data-visible');
|
||||
this.submenuElement.hidePopover();
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets all dropdown items in the submenu. */
|
||||
private getSubmenuItems(): WaDropdownItem[] {
|
||||
// Only get direct children with slot="submenu", not nested ones
|
||||
return [...this.children].filter(
|
||||
el =>
|
||||
el.localName === 'wa-dropdown-item' && el.getAttribute('slot') === 'submenu' && !el.hasAttribute('disabled'),
|
||||
) as WaDropdownItem[];
|
||||
}
|
||||
|
||||
/** Handles mouse enter to open the submenu */
|
||||
private handleMouseEnter() {
|
||||
if (this.hasSubmenu && !this.disabled) {
|
||||
this.notifyParentOfOpening();
|
||||
this.submenuOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this.type === 'checkbox'
|
||||
? html`
|
||||
<wa-icon
|
||||
id="check"
|
||||
part="checkmark"
|
||||
exportparts="svg:checkmark__svg"
|
||||
library="system"
|
||||
name="check"
|
||||
></wa-icon>
|
||||
`
|
||||
: ''}
|
||||
|
||||
<span id="icon" part="icon">
|
||||
<slot name="icon"></slot>
|
||||
</span>
|
||||
|
||||
<span id="label" part="label">
|
||||
<slot></slot>
|
||||
</span>
|
||||
|
||||
<span id="details" part="details">
|
||||
<slot name="details"></slot>
|
||||
</span>
|
||||
|
||||
${this.hasSubmenu
|
||||
? html`
|
||||
<wa-icon
|
||||
id="submenu-indicator"
|
||||
part="submenu-icon"
|
||||
exportparts="svg:submenu-icon__svg"
|
||||
library="system"
|
||||
name="chevron-right"
|
||||
></wa-icon>
|
||||
`
|
||||
: ''}
|
||||
${this.hasSubmenu
|
||||
? html`
|
||||
<div
|
||||
id="submenu"
|
||||
part="submenu"
|
||||
popover="manual"
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
aria-orientation="vertical"
|
||||
hidden
|
||||
>
|
||||
<slot name="submenu"></slot>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'wa-dropdown-item': WaDropdownItem;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +1,93 @@
|
||||
:host {
|
||||
--box-shadow: var(--wa-shadow-m);
|
||||
|
||||
display: inline-block;
|
||||
--show-duration: 50ms;
|
||||
--hide-duration: 50ms;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.dropdown::part(popup) {
|
||||
z-index: 900;
|
||||
#menu {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
flex-direction: column;
|
||||
width: max-content;
|
||||
margin: 0;
|
||||
padding: 0.25em;
|
||||
border: var(--wa-border-style) var(--wa-border-width-s) var(--wa-color-surface-border);
|
||||
border-radius: var(--wa-border-radius-m);
|
||||
background-color: var(--wa-color-surface-raised);
|
||||
box-shadow: var(--wa-shadow-m);
|
||||
color: var(--wa-color-text-normal);
|
||||
text-align: start;
|
||||
user-select: none;
|
||||
|
||||
&.show {
|
||||
animation: show var(--show-duration) ease;
|
||||
}
|
||||
|
||||
&.hide {
|
||||
animation: show var(--hide-duration) ease reverse;
|
||||
}
|
||||
|
||||
::slotted(h1),
|
||||
::slotted(h2),
|
||||
::slotted(h3),
|
||||
::slotted(h4),
|
||||
::slotted(h5),
|
||||
::slotted(h6) {
|
||||
display: block !important;
|
||||
margin: 0.25em 0 !important;
|
||||
padding: 0.25em 0.75em !important;
|
||||
color: var(--wa-color-text-quiet) !important;
|
||||
font-family: var(--wa-font-family-body) !important;
|
||||
font-weight: var(--wa-font-weight-semibold) !important;
|
||||
font-size: var(--wa-font-size-smaller) !important;
|
||||
}
|
||||
|
||||
::slotted(wa-divider) {
|
||||
--spacing: 0.25em; /* Component-specific, left as-is */
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown[data-current-placement^='top']::part(popup) {
|
||||
wa-popup[data-current-placement^='top'] #menu {
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
.dropdown[data-current-placement^='bottom']::part(popup) {
|
||||
wa-popup[data-current-placement^='bottom'] #menu {
|
||||
transform-origin: top;
|
||||
}
|
||||
|
||||
.dropdown[data-current-placement^='left']::part(popup) {
|
||||
wa-popup[data-current-placement^='left'] #menu {
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
.dropdown[data-current-placement^='right']::part(popup) {
|
||||
wa-popup[data-current-placement^='right'] #menu {
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
#trigger {
|
||||
display: block; /* for boundingClientRect */
|
||||
wa-popup[data-current-placement='left-start'] #menu {
|
||||
transform-origin: right top;
|
||||
}
|
||||
|
||||
.panel {
|
||||
font: inherit;
|
||||
box-shadow: var(--box-shadow);
|
||||
border-radius: var(--wa-border-radius-m);
|
||||
pointer-events: none;
|
||||
wa-popup[data-current-placement='left-end'] #menu {
|
||||
transform-origin: right bottom;
|
||||
}
|
||||
|
||||
.dropdown-open .panel {
|
||||
display: block;
|
||||
pointer-events: all;
|
||||
wa-popup[data-current-placement='right-start'] #menu {
|
||||
transform-origin: left top;
|
||||
}
|
||||
|
||||
/* Sizes */
|
||||
:host([size='small']) ::slotted(wa-menu) {
|
||||
font-size: var(--wa-font-size-s);
|
||||
wa-popup[data-current-placement='right-end'] #menu {
|
||||
transform-origin: left bottom;
|
||||
}
|
||||
|
||||
:host([size='medium']) ::slotted(wa-menu) {
|
||||
font-size: var(--wa-font-size-m);
|
||||
}
|
||||
|
||||
:host([size='large']) ::slotted(wa-menu) {
|
||||
font-size: var(--wa-font-size-l);
|
||||
}
|
||||
|
||||
/* When users slot a menu, make sure it conforms to the popup's auto-size */
|
||||
::slotted(wa-menu) {
|
||||
max-width: var(--auto-size-available-width) !important;
|
||||
max-height: var(--auto-size-available-height) !important;
|
||||
@keyframes show {
|
||||
from {
|
||||
scale: 0.9;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
scale: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,405 +1,9 @@
|
||||
import { expect, waitUntil } from '@open-wc/testing';
|
||||
import { sendKeys, sendMouse } from '@web/test-runner-commands';
|
||||
import { html } from 'lit';
|
||||
import sinon from 'sinon';
|
||||
import { clickOnElement } from '../../internal/test.js';
|
||||
import { fixtures } from '../../internal/test/fixture.js';
|
||||
import type WaDropdown from './dropdown.js';
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
describe('<wa-dropdown>', () => {
|
||||
for (const fixture of fixtures) {
|
||||
describe(`with "${fixture.type}" rendering`, () => {
|
||||
it('should be visible with the open attribute', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown open>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
<wa-menu-item>Item 2</wa-menu-item>
|
||||
<wa-menu-item>Item 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <wa-dropdown></wa-dropdown> `);
|
||||
|
||||
expect(panel.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should not be visible without the open attribute', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
<wa-menu-item>Item 2</wa-menu-item>
|
||||
<wa-menu-item>Item 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
||||
|
||||
expect(panel.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should emit wa-show and wa-after-show when calling show()', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
<wa-menu-item>Item 2</wa-menu-item>
|
||||
<wa-menu-item>Item 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('wa-show', showHandler);
|
||||
el.addEventListener('wa-after-show', afterShowHandler);
|
||||
el.show();
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
|
||||
expect(showHandler).to.have.been.calledOnce;
|
||||
expect(afterShowHandler).to.have.been.calledOnce;
|
||||
expect(panel.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should emit wa-hide and wa-after-hide when calling hide()', async () => {
|
||||
// @TODO: Fix this [Konnor]
|
||||
if (fixture.type === 'ssr-client-hydrated') {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown open>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
<wa-menu-item>Item 2</wa-menu-item>
|
||||
<wa-menu-item>Item 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('wa-hide', hideHandler);
|
||||
el.addEventListener('wa-after-hide', afterHideHandler);
|
||||
el.hide();
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
|
||||
expect(hideHandler).to.have.been.calledOnce;
|
||||
expect(afterHideHandler).to.have.been.calledOnce;
|
||||
expect(panel.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should emit wa-show and wa-after-show when setting open = true', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
<wa-menu-item>Item 2</wa-menu-item>
|
||||
<wa-menu-item>Item 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('wa-show', showHandler);
|
||||
el.addEventListener('wa-after-show', afterShowHandler);
|
||||
el.open = true;
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
|
||||
expect(showHandler).to.have.been.calledOnce;
|
||||
expect(afterShowHandler).to.have.been.calledOnce;
|
||||
expect(panel.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should emit wa-hide and wa-after-hide when setting open = false', async () => {
|
||||
// @TODO: Fix this [Konnor]
|
||||
if (fixture.type === 'ssr-client-hydrated') {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown open>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
<wa-menu-item>Item 2</wa-menu-item>
|
||||
<wa-menu-item>Item 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('wa-hide', hideHandler);
|
||||
el.addEventListener('wa-after-hide', afterHideHandler);
|
||||
el.open = false;
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
|
||||
expect(hideHandler).to.have.been.calledOnce;
|
||||
expect(afterHideHandler).to.have.been.calledOnce;
|
||||
expect(panel.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should still open on arrow navigation when no menu items', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu> </wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const trigger = el.querySelector('wa-button')!;
|
||||
|
||||
trigger.focus();
|
||||
await sendKeys({ press: 'ArrowDown' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.open).to.be.true;
|
||||
});
|
||||
|
||||
it('should open on arrow down navigation', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
<wa-menu-item>Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const trigger = el.querySelector('wa-button')!;
|
||||
const firstMenuItem = el.querySelectorAll('wa-menu-item')[0];
|
||||
|
||||
trigger.focus();
|
||||
await sendKeys({ press: 'ArrowDown' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.open).to.be.true;
|
||||
expect(document.activeElement).to.equal(firstMenuItem);
|
||||
});
|
||||
|
||||
it('should open on arrow up navigation', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
<wa-menu-item>Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const trigger = el.querySelector('wa-button')!;
|
||||
const secondMenuItem = el.querySelectorAll('wa-menu-item')[1];
|
||||
|
||||
trigger.focus();
|
||||
await sendKeys({ press: 'ArrowUp' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.open).to.be.true;
|
||||
expect(document.activeElement).to.equal(secondMenuItem);
|
||||
});
|
||||
|
||||
it('should navigate to first focusable item on arrow navigation', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-label>Top Label</wa-menu-label>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const trigger = el.querySelector('wa-button')!;
|
||||
const item = el.querySelector('wa-menu-item')!;
|
||||
|
||||
await clickOnElement(trigger);
|
||||
await trigger.updateComplete;
|
||||
await sendKeys({ press: 'ArrowDown' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(document.activeElement).to.equal(item);
|
||||
});
|
||||
|
||||
it('should close on escape key', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown open>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
<wa-menu-item>Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const trigger = el.querySelector('wa-button')!;
|
||||
|
||||
trigger.focus();
|
||||
await sendKeys({ press: 'Escape' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.open).to.be.false;
|
||||
});
|
||||
|
||||
it('should not open on arrow navigation when no menu exists', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<div>Some custom content</div>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const trigger = el.querySelector('wa-button')!;
|
||||
|
||||
trigger.focus();
|
||||
await sendKeys({ press: 'ArrowDown' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.open).to.be.false;
|
||||
});
|
||||
|
||||
it('should open on enter key', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const trigger = el.querySelector('wa-button')!;
|
||||
|
||||
trigger.focus();
|
||||
await el.updateComplete;
|
||||
await sendKeys({ press: 'Enter' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.open).to.be.true;
|
||||
});
|
||||
|
||||
it('should focus on menu items when clicking the trigger and arrowing through options', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
<wa-menu-item>Item 2</wa-menu-item>
|
||||
<wa-menu-item>Item 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const trigger = el.querySelector('wa-button')!;
|
||||
const secondMenuItem = el.querySelectorAll('wa-menu-item')[1];
|
||||
|
||||
await clickOnElement(trigger);
|
||||
await trigger.updateComplete;
|
||||
await sendKeys({ press: 'ArrowDown' });
|
||||
await el.updateComplete;
|
||||
await sendKeys({ press: 'ArrowDown' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(document.activeElement).to.equal(secondMenuItem);
|
||||
});
|
||||
|
||||
it('should open on enter key when no menu exists', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<div>Some custom content</div>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const trigger = el.querySelector('wa-button')!;
|
||||
|
||||
trigger.focus();
|
||||
await el.updateComplete;
|
||||
await sendKeys({ press: 'Enter' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.open).to.be.true;
|
||||
});
|
||||
|
||||
it('should hide when clicked outside container and initially open', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown open>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
|
||||
await sendMouse({ type: 'click', position: [0, 0] });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.open).to.be.false;
|
||||
});
|
||||
|
||||
it('should hide when clicked outside container', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const trigger = el.querySelector('wa-button')!;
|
||||
|
||||
trigger.click();
|
||||
await el.updateComplete;
|
||||
await sendMouse({ type: 'click', position: [0, 0] });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.open).to.be.false;
|
||||
});
|
||||
|
||||
it('should close and stop propagating the keydown event when Escape is pressed and the dropdown is open ', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown open>
|
||||
<wa-button slot="trigger" caret>Toggle</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Dropdown Item 1</wa-menu-item>
|
||||
<wa-menu-item>Dropdown Item 2</wa-menu-item>
|
||||
<wa-menu-item>Dropdown Item 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
const firstMenuItem = el.querySelector('wa-menu-item')!;
|
||||
const hideHandler = sinon.spy();
|
||||
|
||||
document.body.addEventListener('keydown', hideHandler);
|
||||
firstMenuItem.focus();
|
||||
await sendKeys({ press: 'Escape' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.open).to.be.false;
|
||||
|
||||
if ('CloseWatcher' in window) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @TODO: Fix this [Konnor]
|
||||
if (fixture.type === 'ssr-client-hydrated') {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(hideHandler).to.not.have.been.called;
|
||||
});
|
||||
});
|
||||
}
|
||||
expect(el).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -129,8 +129,8 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
.start,
|
||||
.end {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
@@ -141,11 +141,11 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
.prefix::slotted(*) {
|
||||
.start::slotted(*) {
|
||||
margin-inline-end: var(--wa-form-control-padding-inline);
|
||||
}
|
||||
|
||||
.suffix::slotted(*) {
|
||||
.end::slotted(*) {
|
||||
margin-inline-start: var(--wa-form-control-padding-inline);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { WaClearEvent } from '../../events/clear.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
import { submitOnEnter } from '../../internal/submit-on-enter.js';
|
||||
import { MirrorValidator } from '../../internal/validators/mirror-validator.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
|
||||
@@ -12,7 +13,6 @@ import formControlStyles from '../../styles/component/form-control.css';
|
||||
import appearanceStyles from '../../styles/utilities/appearance.css';
|
||||
import sizeStyles from '../../styles/utilities/size.css';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import type WaButton from '../button/button.js';
|
||||
import '../icon/icon.js';
|
||||
import styles from './input.css';
|
||||
|
||||
@@ -25,8 +25,8 @@ import styles from './input.css';
|
||||
* @dependency wa-icon
|
||||
*
|
||||
* @slot label - The input's label. Alternatively, you can use the `label` attribute.
|
||||
* @slot prefix - Used to prepend a presentational icon or similar element to the input.
|
||||
* @slot suffix - Used to append a presentational icon or similar element to the input.
|
||||
* @slot start - An element, such as `<wa-icon>`, placed at the start of the input control.
|
||||
* @slot end - An element, such as `<wa-icon>`, placed at the end of the input control.
|
||||
* @slot clear-icon - An icon to use in lieu of the default clear icon.
|
||||
* @slot show-password-icon - An icon to use in lieu of the default show password icon.
|
||||
* @slot hide-password-icon - An icon to use in lieu of the default hide password icon.
|
||||
@@ -43,10 +43,10 @@ import styles from './input.css';
|
||||
* @csspart hint - The hint's wrapper.
|
||||
* @csspart input - The wrapper being rendered as an input
|
||||
* @csspart base - The internal `<input>` control.
|
||||
* @csspart prefix - The container that wraps the prefix.
|
||||
* @csspart start - The container that wraps the `start` slot.
|
||||
* @csspart clear-button - The clear button.
|
||||
* @csspart password-toggle-button - The password toggle button.
|
||||
* @csspart suffix - The container that wraps the suffix.
|
||||
* @csspart end - The container that wraps the `end` slot.
|
||||
*
|
||||
* @cssproperty --background-color - The input's background color.
|
||||
* @cssproperty --border-color - The color of the input's borders.
|
||||
@@ -223,8 +223,9 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
|
||||
@property({ attribute: 'with-hint', type: Boolean }) withHint = false;
|
||||
|
||||
private handleChange(event: Event) {
|
||||
this.relayNativeEvent(event, { bubbles: true, composed: true });
|
||||
this.value = this.input.value;
|
||||
|
||||
this.relayNativeEvent(event, { bubbles: true, composed: true });
|
||||
}
|
||||
|
||||
private handleClearClick(event: MouseEvent) {
|
||||
@@ -232,9 +233,12 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
if (this.value !== '') {
|
||||
this.value = '';
|
||||
this.dispatchEvent(new WaClearEvent());
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new WaClearEvent());
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
|
||||
this.input.focus();
|
||||
@@ -245,51 +249,7 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
|
||||
}
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
const hasModifier = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
||||
|
||||
// Pressing enter when focused on an input should submit the form like a native input, but we wait a tick before
|
||||
// submitting to allow users to cancel the keydown event if they need to
|
||||
if (event.key === 'Enter' && !hasModifier) {
|
||||
setTimeout(() => {
|
||||
//
|
||||
// When using an Input Method Editor (IME), pressing enter will cause the form to submit unexpectedly. One way
|
||||
// to check for this is to look at event.isComposing, which will be true when the IME is open.
|
||||
//
|
||||
// See https://github.com/shoelace-style/shoelace/pull/988
|
||||
//
|
||||
if (!event.defaultPrevented && !event.isComposing) {
|
||||
const form = this.getForm();
|
||||
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formElements = [...form.elements];
|
||||
|
||||
// If we're the only formElement, we submit like a native input.
|
||||
if (formElements.length === 1) {
|
||||
form.requestSubmit(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const button = formElements.find(
|
||||
(el: HTMLButtonElement) => el.type === 'submit' && !el.matches(':disabled'),
|
||||
) as undefined | HTMLButtonElement | WaButton;
|
||||
|
||||
// No button found, don't submit.
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (button.tagName.toLowerCase() === 'button') {
|
||||
form.requestSubmit(button);
|
||||
} else {
|
||||
// requestSubmit() wont work with `<wa-button>`
|
||||
button.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
submitOnEnter(event, this);
|
||||
}
|
||||
|
||||
private handlePasswordToggle() {
|
||||
@@ -400,7 +360,7 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
|
||||
</label>
|
||||
|
||||
<div part="input" class="text-field">
|
||||
<slot name="prefix" part="prefix" class="prefix"></slot>
|
||||
<slot name="start" part="start" class="start"></slot>
|
||||
|
||||
<input
|
||||
part="base"
|
||||
@@ -474,7 +434,7 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
|
||||
`
|
||||
: ''}
|
||||
|
||||
<slot name="suffix" part="suffix" class="suffix"></slot>
|
||||
<slot name="end" part="end" class="end"></slot>
|
||||
</div>
|
||||
|
||||
<slot
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
:host {
|
||||
--background-color-hover: var(--wa-color-neutral-fill-normal);
|
||||
--text-color-hover: var(--wa-color-neutral-on-normal);
|
||||
--submenu-offset: -0.125rem;
|
||||
|
||||
display: block;
|
||||
color: var(--wa-color-text-normal);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
font: inherit;
|
||||
padding: 0.5em 0.25em;
|
||||
line-height: var(--wa-line-height-condensed);
|
||||
transition: fill var(--wa-transition-normal) var(--wa-transition-easing);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:host([inert]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([disabled]) {
|
||||
outline: none;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:host([loading]) {
|
||||
outline: none;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
:host([loading]) *:not(wa-spinner) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
:host([loading]) wa-spinner {
|
||||
--indicator-color: currentColor;
|
||||
--track-width: round(0.0625em, 1px);
|
||||
position: absolute;
|
||||
font-size: var(--wa-font-size-smaller);
|
||||
top: calc(50% - 0.5em);
|
||||
left: 0.6em;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1 1 auto;
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.prefix {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.prefix::slotted(*) {
|
||||
margin-inline-end: 0.5em;
|
||||
}
|
||||
|
||||
.suffix {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.suffix::slotted(*) {
|
||||
margin-inline-start: 0.5em;
|
||||
}
|
||||
|
||||
/* Safe triangle */
|
||||
:host(:state(submenu-expanded))::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
z-index: 899;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
clip-path: polygon(
|
||||
var(--safe-triangle-cursor-x, 0) var(--safe-triangle-cursor-y, 0),
|
||||
var(--safe-triangle-submenu-start-x, 0) var(--safe-triangle-submenu-start-y, 0),
|
||||
var(--safe-triangle-submenu-end-x, 0) var(--safe-triangle-submenu-end-y, 0)
|
||||
);
|
||||
}
|
||||
|
||||
:host(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:host(:hover:not([aria-disabled='true'], :focus-visible)),
|
||||
:host(:state(submenu-expanded)) {
|
||||
background-color: var(--background-color-hover);
|
||||
color: var(--text-color-hover);
|
||||
}
|
||||
|
||||
:host(:focus-visible) {
|
||||
outline: var(--wa-focus-ring);
|
||||
outline-offset: calc(-1 * var(--wa-focus-ring-width));
|
||||
background: var(--background-color-hover);
|
||||
color: var(--text-color-hover);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.check,
|
||||
.chevron {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--wa-font-size-smaller);
|
||||
width: 2em;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
:host([checked]) .check,
|
||||
:host(:state(has-submenu)) .chevron {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Add elevation and z-index to submenus */
|
||||
wa-popup::part(popup) {
|
||||
box-shadow: var(--wa-shadow-m);
|
||||
z-index: 900;
|
||||
margin-left: var(--submenu-offset);
|
||||
}
|
||||
|
||||
wa-popup:dir(rtl)::part(popup) {
|
||||
margin-left: calc(-1 * var(--submenu-offset));
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
:host(:hover:not([aria-disabled='true'])),
|
||||
:host(:focus-visible) {
|
||||
outline: dashed 1px SelectedItem;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
::slotted(wa-menu) {
|
||||
max-width: var(--auto-size-available-width) !important;
|
||||
max-height: var(--auto-size-available-height) !important;
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import { aTimeout, expect, waitUntil } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import { html } from 'lit';
|
||||
import sinon from 'sinon';
|
||||
import type { WaSelectEvent } from '../../events/select.js';
|
||||
import { clickOnElement } from '../../internal/test.js';
|
||||
import { fixtures } from '../../internal/test/fixture.js';
|
||||
import type WaMenuItem from './menu-item.js';
|
||||
|
||||
describe('<wa-menu-item>', () => {
|
||||
for (const fixture of fixtures) {
|
||||
describe(`with "${fixture.type}" rendering`, () => {
|
||||
it('should pass accessibility tests', async () => {
|
||||
const el = await fixture<WaMenuItem>(html`
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
<wa-menu-item>Item 2</wa-menu-item>
|
||||
<wa-menu-item>Item 3</wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-item type="checkbox" checked>Checked</wa-menu-item>
|
||||
<wa-menu-item type="checkbox">Unchecked</wa-menu-item>
|
||||
</wa-menu>
|
||||
`);
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should pass accessibility tests when using a submenu', async () => {
|
||||
const el = await fixture<WaMenuItem>(html`
|
||||
<wa-menu>
|
||||
<wa-menu-item>
|
||||
Submenu
|
||||
<wa-menu slot="submenu">
|
||||
<wa-menu-item>Submenu Item 1</wa-menu-item>
|
||||
<wa-menu-item>Submenu Item 2</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
`);
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should have the correct default properties', async () => {
|
||||
const el = await fixture<WaMenuItem>(html` <wa-menu-item>Test</wa-menu-item> `);
|
||||
|
||||
expect(el.value).to.equal('');
|
||||
expect(el.disabled).to.be.false;
|
||||
expect(el.loading).to.equal(false);
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('false');
|
||||
});
|
||||
|
||||
it('should render the correct aria attributes when disabled', async () => {
|
||||
const el = await fixture<WaMenuItem>(html` <wa-menu-item disabled>Test</wa-menu-item> `);
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('true');
|
||||
});
|
||||
|
||||
describe('when loading', () => {
|
||||
it('should have a spinner present', async () => {
|
||||
const el = await fixture<WaMenuItem>(html` <wa-menu-item loading>Menu Item Label</wa-menu-item> `);
|
||||
expect(el.shadowRoot!.querySelector('wa-spinner')).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
it('defaultLabel should return a text label', async () => {
|
||||
const el = await fixture<WaMenuItem>(html` <wa-menu-item>Test</wa-menu-item> `);
|
||||
expect(el.defaultLabel).to.equal('Test');
|
||||
expect(el.label).to.equal('Test');
|
||||
});
|
||||
|
||||
it('label attribute should override default label', async () => {
|
||||
const el = await fixture<WaMenuItem>(html` <wa-menu-item label="Manual label">Text content</wa-menu-item> `);
|
||||
expect(el.defaultLabel).to.equal('Text content');
|
||||
expect(el.label).to.equal('Manual label');
|
||||
});
|
||||
|
||||
it('should emit the slotchange event when the label changes', async () => {
|
||||
const el = await fixture<WaMenuItem>(html` <wa-menu-item>Text</wa-menu-item> `);
|
||||
const slotChangeHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('slotchange', slotChangeHandler);
|
||||
el.textContent = 'New Text';
|
||||
await waitUntil(() => slotChangeHandler.calledOnce);
|
||||
|
||||
expect(slotChangeHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should render a hidden menu item when the inert attribute is used', async () => {
|
||||
const menu = await fixture<WaMenuItem>(html`
|
||||
<wa-menu>
|
||||
<wa-menu-item inert>Item 1</wa-menu-item>
|
||||
<wa-menu-item>Item 2</wa-menu-item>
|
||||
<wa-menu-item>Item 3</wa-menu-item>
|
||||
</wa-menu>
|
||||
`);
|
||||
const item1 = menu.querySelector('wa-menu-item')!;
|
||||
|
||||
expect(getComputedStyle(item1).display).to.equal('none');
|
||||
});
|
||||
|
||||
it('should not render a wa-popup if the slot="submenu" attribute is missing, but the slot should exist in the component and be hidden.', async () => {
|
||||
const menu = await fixture<WaMenuItem>(html`
|
||||
<wa-menu>
|
||||
<wa-menu-item>
|
||||
Item 1
|
||||
<wa-menu>
|
||||
<wa-menu-item> Nested Item 1 </wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
`);
|
||||
|
||||
const menuItem: HTMLElement = menu.querySelector('wa-menu-item')!;
|
||||
expect(menuItem.shadowRoot!.querySelector('wa-popup')).to.be.null;
|
||||
const submenuSlot: HTMLElement = menuItem.shadowRoot!.querySelector('slot[name="submenu"]')!;
|
||||
expect(submenuSlot.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should render a wa-popup if the slot="submenu" attribute is present', async () => {
|
||||
const menu = await fixture<WaMenuItem>(html`
|
||||
<wa-menu>
|
||||
<wa-menu-item id="test">
|
||||
Item 1
|
||||
<wa-menu slot="submenu">
|
||||
<wa-menu-item> Nested Item 1 </wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
`);
|
||||
|
||||
const menuItem = menu.querySelector('wa-menu-item')!;
|
||||
expect(menuItem.shadowRoot!.querySelector('wa-popup')).to.be.not.null;
|
||||
const submenuSlot: HTMLElement = menuItem.shadowRoot!.querySelector('slot[name="submenu"]')!;
|
||||
expect(submenuSlot.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should focus on first menuitem of submenu if ArrowRight is pressed on parent menuitem', async () => {
|
||||
const menu = await fixture<WaMenuItem>(html`
|
||||
<wa-menu>
|
||||
<wa-menu-item id="item-1">
|
||||
Submenu
|
||||
<wa-menu slot="submenu">
|
||||
<wa-menu-item value="submenu-item-1"> Nested Item 1 </wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
`);
|
||||
|
||||
const selectHandler = sinon.spy((event: WaSelectEvent) => {
|
||||
const item = event.detail.item;
|
||||
expect(item.value).to.equal('submenu-item-1');
|
||||
});
|
||||
menu.addEventListener('wa-select', selectHandler);
|
||||
|
||||
const submenu = menu.querySelector<WaMenuItem>('wa-menu-item')!;
|
||||
// Sometimes Chrome fails if we dont click before triggering focus.
|
||||
await clickOnElement(submenu);
|
||||
submenu.focus();
|
||||
await menu.updateComplete;
|
||||
await sendKeys({ press: 'ArrowRight' });
|
||||
await menu.updateComplete;
|
||||
await sendKeys({ press: 'Enter' });
|
||||
await menu.updateComplete;
|
||||
// Once for each menu element.
|
||||
expect(selectHandler).to.have.been.calledTwice;
|
||||
});
|
||||
|
||||
it('should focus on outer menu if ArrowRight is pressed on nested menuitem', async () => {
|
||||
const menu = await fixture<WaMenuItem>(html`
|
||||
<wa-menu>
|
||||
<wa-menu-item id="outer" value="outer-item-1">
|
||||
Submenu
|
||||
<wa-menu slot="submenu">
|
||||
<wa-menu-item value="inner-item-1"> Nested Item 1 </wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
`);
|
||||
|
||||
const focusHandler = sinon.spy((event: FocusEvent) => {
|
||||
const target = event.target as WaMenuItem;
|
||||
const relatedTarget = event.relatedTarget as WaMenuItem;
|
||||
expect(target.value).to.equal('outer-item-1');
|
||||
expect(relatedTarget.value).to.equal('inner-item-1');
|
||||
});
|
||||
|
||||
const outerItem = menu.querySelector<WaMenuItem>('#outer')!;
|
||||
// Silly fix for CI + Chrome to focus properly.
|
||||
await clickOnElement(outerItem);
|
||||
|
||||
outerItem.focus();
|
||||
await menu.updateComplete;
|
||||
await sendKeys({ press: 'ArrowRight' });
|
||||
|
||||
outerItem.addEventListener('focus', focusHandler);
|
||||
await menu.updateComplete;
|
||||
await sendKeys({ press: 'ArrowLeft' });
|
||||
await menu.updateComplete;
|
||||
expect(focusHandler).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,238 +0,0 @@
|
||||
import type { PropertyValues } from 'lit';
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import getText from '../../internal/get-text.js';
|
||||
import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import '../icon/icon.js';
|
||||
import '../popup/popup.js';
|
||||
import '../spinner/spinner.js';
|
||||
import styles from './menu-item.css';
|
||||
import { SubmenuController } from './submenu-controller.js';
|
||||
|
||||
/**
|
||||
* @summary Menu items provide options for the user to pick from in a menu.
|
||||
* @documentation https://backers.webawesome.com/docs/components/menu-item
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency wa-icon
|
||||
* @dependency wa-popup
|
||||
*
|
||||
* @slot - The menu item's label.
|
||||
* @slot prefix - Used to prepend an icon or similar element to the menu item.
|
||||
* @slot suffix - Used to append an icon or similar element to the menu item.
|
||||
* @slot submenu - Used to denote a nested menu.
|
||||
* @slot checked-icon - The icon used to indicate that this menu item is checked. Usually a `<wa-icon>`.
|
||||
* @slot submenu-icon - The icon used to indicate that this menu item has a submenu. Usually a `<wa-icon>`.
|
||||
*
|
||||
* @csspart checked-icon - The checked icon, which is only visible when the menu item is checked.
|
||||
* @csspart prefix - The prefix container.
|
||||
* @csspart label - The menu item label.
|
||||
* @csspart suffix - The suffix container.
|
||||
* @csspart spinner - The spinner that shows when the menu item is in the loading state.
|
||||
* @csspart spinner__base - The spinner's base part.
|
||||
* @csspart submenu-icon - The submenu icon, visible only when the menu item has a submenu (not yet implemented).
|
||||
*
|
||||
* @cssproperty --background-color-hover - The menu item's background color on hover.
|
||||
* @cssproperty --text-color-hover - The label color on hover.
|
||||
* @cssproperty [--submenu-offset=-2px] - The distance submenus shift to overlap the parent menu.
|
||||
*
|
||||
* @cssstate has-submenu - Applied when the menu item has a submenu.
|
||||
* @cssstate submenu-expanded - Applied when the menu item has a submenu and it is expanded.
|
||||
*/
|
||||
@customElement('wa-menu-item')
|
||||
export default class WaMenuItem extends WebAwesomeElement {
|
||||
static css = styles;
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
|
||||
@query('.menu-item') menuItem: HTMLElement;
|
||||
|
||||
/** The type of menu item to render. To use `checked`, this value must be set to `checkbox`. */
|
||||
@property() type: 'normal' | 'checkbox' = 'normal';
|
||||
|
||||
/** Draws the item in a checked state. */
|
||||
@property({ type: Boolean, reflect: true }) checked = false;
|
||||
|
||||
/** A unique value to store in the menu item. This can be used as a way to identify menu items when selected. */
|
||||
@property() value = '';
|
||||
|
||||
/** Draws the menu item in a loading state. */
|
||||
@property({ type: Boolean, reflect: true }) loading = false;
|
||||
|
||||
/** Draws the menu item in a disabled state, preventing selection. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
_label: string = '';
|
||||
/**
|
||||
* The option’s plain text label.
|
||||
* Usually automatically generated, but can be useful to provide manually for cases involving complex content.
|
||||
*/
|
||||
@property()
|
||||
set label(value) {
|
||||
const oldValue = this._label;
|
||||
this._label = value || '';
|
||||
|
||||
if (this._label !== oldValue) {
|
||||
this.requestUpdate('label', oldValue);
|
||||
}
|
||||
}
|
||||
|
||||
get label(): string {
|
||||
if (this._label) {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
if (!this.defaultLabel) {
|
||||
this.updateDefaultLabel();
|
||||
}
|
||||
|
||||
return this.defaultLabel;
|
||||
}
|
||||
|
||||
/** The default label, generated from the element contents. Will be equal to `label` in most cases. */
|
||||
@state() defaultLabel = '';
|
||||
|
||||
/**
|
||||
* Used for SSR purposes. If true, will render a ">" caret icon for showing that it has a submenu, but will be non-interactive.
|
||||
*/
|
||||
@property({ attribute: 'with-submenu', type: Boolean }) withSubmenu = false;
|
||||
|
||||
private submenuController: SubmenuController = new SubmenuController(this);
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener('click', this.handleHostClick);
|
||||
this.addEventListener('mouseover', this.handleMouseOver);
|
||||
this.updateDefaultLabel();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener('click', this.handleHostClick);
|
||||
this.removeEventListener('mouseover', this.handleMouseOver);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
// Kick it so that it renders the "submenu" properly.
|
||||
if (this.isSubmenu()) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
super.firstUpdated(changedProperties);
|
||||
}
|
||||
|
||||
private handleDefaultSlotChange() {
|
||||
let labelChanged = this.updateDefaultLabel();
|
||||
|
||||
// When the label changes, emit a slotchange event so parent controls see it
|
||||
if (labelChanged) {
|
||||
/** @internal - prevent the CEM from recording this event */
|
||||
this.dispatchEvent(new Event('slotchange', { bubbles: true, composed: false, cancelable: false }));
|
||||
}
|
||||
|
||||
this.customStates.set('has-submenu', this.isSubmenu());
|
||||
}
|
||||
|
||||
private handleHostClick = (event: MouseEvent) => {
|
||||
// Prevent the click event from being emitted when the button is disabled or loading
|
||||
if (this.disabled) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
|
||||
private handleMouseOver = (event: MouseEvent) => {
|
||||
this.focus();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has('checked')) {
|
||||
// For proper accessibility, users have to use type="checkbox" to use the checked attribute
|
||||
if (this.checked && this.type !== 'checkbox') {
|
||||
this.checked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only checkbox types can receive the aria-checked attribute
|
||||
if (this.type === 'checkbox') {
|
||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||
} else {
|
||||
this.removeAttribute('aria-checked');
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has('disabled')) {
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
}
|
||||
|
||||
if (changedProperties.has('type')) {
|
||||
if (this.type === 'checkbox') {
|
||||
this.setAttribute('role', 'menuitemcheckbox');
|
||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||
} else {
|
||||
this.setAttribute('role', 'menuitem');
|
||||
this.removeAttribute('aria-checked');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateDefaultLabel() {
|
||||
let oldValue = this.defaultLabel;
|
||||
this.defaultLabel = getText(this).trim();
|
||||
let changed = this.defaultLabel !== oldValue;
|
||||
|
||||
if (!this._label && changed) {
|
||||
// Uses default label, and it has changed
|
||||
this.requestUpdate('label', oldValue);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/** Does this element have a submenu? */
|
||||
private isSubmenu() {
|
||||
return this.hasUpdated ? this.querySelector(`:scope > [slot="submenu"]`) !== null : this.withSubmenu;
|
||||
}
|
||||
|
||||
render() {
|
||||
const isRtl = this.hasUpdated ? this.localize.dir() === 'rtl' : this.dir === 'rtl';
|
||||
const isSubmenuExpanded = this.submenuController.isExpanded();
|
||||
this.customStates.set('submenu-expanded', isSubmenuExpanded);
|
||||
|
||||
this.internals.ariaHasPopup = this.isSubmenu() + '';
|
||||
this.internals.ariaExpanded = isSubmenuExpanded + '';
|
||||
|
||||
return html`
|
||||
<slot name="checked-icon" part="checked-icon" class="check">
|
||||
<wa-icon name="check" library="system" variant="solid" aria-hidden="true"></wa-icon>
|
||||
</slot>
|
||||
|
||||
<slot name="prefix" part="prefix" class="prefix"></slot>
|
||||
|
||||
<slot part="label" class="label" @slotchange=${this.handleDefaultSlotChange}></slot>
|
||||
|
||||
<slot name="suffix" part="suffix" class="suffix"></slot>
|
||||
|
||||
<slot name="submenu-icon" part="submenu-icon" class="chevron">
|
||||
<wa-icon
|
||||
name=${isRtl ? 'chevron-left' : 'chevron-right'}
|
||||
library="system"
|
||||
variant="solid"
|
||||
aria-hidden="true"
|
||||
></wa-icon>
|
||||
</slot>
|
||||
|
||||
${this.submenuController.renderSubmenu()} ${this.loading ? html`<wa-spinner part="spinner"></wa-spinner>` : ''}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'wa-menu-item': WaMenuItem;
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
||||
import { html } from 'lit';
|
||||
import { createRef, ref, type Ref } from 'lit/directives/ref.js';
|
||||
import type WaPopup from '../popup/popup.js';
|
||||
import type WaMenuItem from './menu-item.js';
|
||||
|
||||
/** A reactive controller to manage the registration of event listeners for submenus. */
|
||||
export class SubmenuController implements ReactiveController {
|
||||
private host: ReactiveControllerHost & WaMenuItem;
|
||||
private popupRef: Ref<WaPopup> = createRef();
|
||||
private enableSubmenuTimer = -1;
|
||||
private isConnected = false;
|
||||
private isPopupConnected = false;
|
||||
private skidding = 0;
|
||||
private readonly submenuOpenDelay = 100;
|
||||
|
||||
constructor(host: ReactiveControllerHost & WaMenuItem) {
|
||||
(this.host = host).addController(this);
|
||||
}
|
||||
|
||||
private hasSubmenu() {
|
||||
return this.host.querySelector(`:scope > [slot="submenu"]`) !== null;
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
if (this.hasSubmenu() && !this.host.disabled) {
|
||||
this.addListeners();
|
||||
}
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
this.removeListeners();
|
||||
}
|
||||
|
||||
hostUpdated() {
|
||||
if (this.hasSubmenu() && !this.host.disabled) {
|
||||
this.addListeners();
|
||||
this.updateSkidding();
|
||||
} else {
|
||||
this.removeListeners();
|
||||
}
|
||||
}
|
||||
|
||||
private addListeners() {
|
||||
if (!this.isConnected) {
|
||||
this.host.addEventListener('mousemove', this.handleMouseMove);
|
||||
this.host.addEventListener('mouseover', this.handleMouseOver);
|
||||
this.host.addEventListener('keydown', this.handleKeyDown);
|
||||
this.host.addEventListener('click', this.handleClick);
|
||||
this.host.addEventListener('focusout', this.handleFocusOut);
|
||||
this.isConnected = true;
|
||||
}
|
||||
|
||||
// The popup does not seem to get wired when the host is
|
||||
// connected, so manage its listeners separately.
|
||||
if (!this.isPopupConnected) {
|
||||
if (this.popupRef.value) {
|
||||
this.popupRef.value.addEventListener('mouseover', this.handlePopupMouseover);
|
||||
this.popupRef.value.addEventListener('wa-reposition', this.handlePopupReposition);
|
||||
this.isPopupConnected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private removeListeners() {
|
||||
if (this.isConnected) {
|
||||
this.host.removeEventListener('mousemove', this.handleMouseMove);
|
||||
this.host.removeEventListener('mouseover', this.handleMouseOver);
|
||||
this.host.removeEventListener('keydown', this.handleKeyDown);
|
||||
this.host.removeEventListener('click', this.handleClick);
|
||||
this.host.removeEventListener('focusout', this.handleFocusOut);
|
||||
this.isConnected = false;
|
||||
}
|
||||
if (this.isPopupConnected) {
|
||||
if (this.popupRef.value) {
|
||||
this.popupRef.value.removeEventListener('mouseover', this.handlePopupMouseover);
|
||||
this.popupRef.value.removeEventListener('wa-reposition', this.handlePopupReposition);
|
||||
this.isPopupConnected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the safe triangle cursor position
|
||||
private handleMouseMove = (event: MouseEvent) => {
|
||||
this.host.style.setProperty('--safe-triangle-cursor-x', `${event.clientX}px`);
|
||||
this.host.style.setProperty('--safe-triangle-cursor-y', `${event.clientY}px`);
|
||||
};
|
||||
|
||||
private handleMouseOver = () => {
|
||||
if (this.hasSubmenu()) {
|
||||
this.enableSubmenu();
|
||||
}
|
||||
};
|
||||
|
||||
private handleSubmenuEntry(event: KeyboardEvent) {
|
||||
// Pass focus to the first menu-item in the submenu.
|
||||
const submenuSlot: HTMLSlotElement | null = this.host.renderRoot.querySelector("slot[name='submenu']");
|
||||
|
||||
// Missing slot
|
||||
if (!submenuSlot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Menus
|
||||
let menuItems: NodeListOf<Element> | null = null;
|
||||
for (const elt of submenuSlot.assignedElements()) {
|
||||
menuItems = elt.querySelectorAll("wa-menu-item, [role^='menuitem']");
|
||||
if (menuItems.length !== 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!menuItems || menuItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
menuItems[0].setAttribute('tabindex', '0');
|
||||
for (let i = 1; i !== menuItems.length; ++i) {
|
||||
menuItems[i].setAttribute('tabindex', '-1');
|
||||
}
|
||||
|
||||
// Open the submenu (if not open), and set focus to first menuitem.
|
||||
if (this.popupRef.value) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (this.popupRef.value.active) {
|
||||
if (menuItems[0] instanceof HTMLElement) {
|
||||
menuItems[0].focus();
|
||||
}
|
||||
} else {
|
||||
this.enableSubmenu(false);
|
||||
this.host.updateComplete.then(() => {
|
||||
if (menuItems[0] instanceof HTMLElement) {
|
||||
menuItems[0].focus();
|
||||
}
|
||||
});
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Focus on the first menu-item of a submenu.
|
||||
private handleKeyDown = (event: KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
case 'Tab':
|
||||
this.disableSubmenu();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
// Either focus is currently on the host element or a child
|
||||
if (event.target !== this.host) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.host.focus();
|
||||
this.disableSubmenu();
|
||||
}
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
this.handleSubmenuEntry(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private handleClick = (event: MouseEvent) => {
|
||||
// Clicking on the item which heads the menu does nothing, otherwise hide submenu and propagate
|
||||
if (event.target === this.host) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
} else if (
|
||||
event.target instanceof Element &&
|
||||
(event.target.tagName === 'wa-menu-item' || event.target.role?.startsWith('menuitem'))
|
||||
) {
|
||||
this.disableSubmenu();
|
||||
}
|
||||
};
|
||||
|
||||
// Close this submenu on focus outside of the parent or any descendants.
|
||||
private handleFocusOut = (event: FocusEvent) => {
|
||||
if (event.relatedTarget && event.relatedTarget instanceof Element && this.host.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
this.disableSubmenu();
|
||||
};
|
||||
|
||||
// Prevent the parent menu-item from getting focus on mouse movement on the submenu
|
||||
private handlePopupMouseover = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
// Set the safe triangle values for the submenu when the position changes
|
||||
private handlePopupReposition = () => {
|
||||
const submenuSlot: HTMLSlotElement | null = this.host.renderRoot.querySelector("slot[name='submenu']");
|
||||
const menu = submenuSlot?.assignedElements({ flatten: true }).filter(el => el.localName === 'wa-menu')[0];
|
||||
const isRtl = getComputedStyle(this.host).direction === 'rtl';
|
||||
|
||||
if (!menu) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { left, top, width, height } = menu.getBoundingClientRect();
|
||||
|
||||
this.host.style.setProperty('--safe-triangle-submenu-start-x', `${isRtl ? left + width : left}px`);
|
||||
this.host.style.setProperty('--safe-triangle-submenu-start-y', `${top}px`);
|
||||
this.host.style.setProperty('--safe-triangle-submenu-end-x', `${isRtl ? left + width : left}px`);
|
||||
this.host.style.setProperty('--safe-triangle-submenu-end-y', `${top + height}px`);
|
||||
};
|
||||
|
||||
private setSubmenuState(state: boolean) {
|
||||
if (this.popupRef.value) {
|
||||
if (this.popupRef.value.active !== state) {
|
||||
this.popupRef.value.active = state;
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shows the submenu. Supports disabling the opening delay, e.g. for keyboard events that want to set the focus to the
|
||||
// newly opened menu.
|
||||
private enableSubmenu(delay = true) {
|
||||
if (delay) {
|
||||
window.clearTimeout(this.enableSubmenuTimer);
|
||||
this.enableSubmenuTimer = window.setTimeout(() => {
|
||||
this.setSubmenuState(true);
|
||||
}, this.submenuOpenDelay);
|
||||
} else {
|
||||
this.setSubmenuState(true);
|
||||
}
|
||||
}
|
||||
|
||||
private disableSubmenu() {
|
||||
window.clearTimeout(this.enableSubmenuTimer);
|
||||
this.setSubmenuState(false);
|
||||
}
|
||||
|
||||
// Calculate the space the top of a menu takes-up, for aligning the popup menu-item with the activating element.
|
||||
private updateSkidding(): void {
|
||||
// .computedStyleMap() not always available.
|
||||
if (!this.host.parentElement?.computedStyleMap) {
|
||||
return;
|
||||
}
|
||||
const styleMap: StylePropertyMapReadOnly = this.host.parentElement.computedStyleMap();
|
||||
const attrs: string[] = ['padding-top', 'border-top-width', 'margin-top'];
|
||||
|
||||
const skidding = attrs.reduce((accumulator, attr) => {
|
||||
const styleValue: CSSStyleValue = styleMap.get(attr) ?? new CSSUnitValue(0, 'px');
|
||||
const unitValue = styleValue instanceof CSSUnitValue ? styleValue : new CSSUnitValue(0, 'px');
|
||||
const pxValue = unitValue.to('px');
|
||||
return accumulator - pxValue.value;
|
||||
}, 0);
|
||||
|
||||
this.skidding = skidding;
|
||||
}
|
||||
|
||||
isExpanded(): boolean {
|
||||
return this.popupRef.value ? this.popupRef.value.active : false;
|
||||
}
|
||||
|
||||
renderSubmenu() {
|
||||
// Always render the slot, but conditionally render the outer <wa-popup>
|
||||
if (!this.host.hasUpdated) {
|
||||
return html` <slot name="submenu" hidden></slot> `;
|
||||
}
|
||||
|
||||
const isRtl = getComputedStyle(this.host).direction === 'rtl';
|
||||
|
||||
return html`
|
||||
<wa-popup
|
||||
${ref(this.popupRef)}
|
||||
placement=${isRtl ? 'left-start' : 'right-start'}
|
||||
.anchor="${this.host}"
|
||||
flip
|
||||
flip-fallback-strategy="best-fit"
|
||||
skidding="${this.skidding}"
|
||||
auto-size="vertical"
|
||||
auto-size-padding="10"
|
||||
>
|
||||
<slot name="submenu"></slot>
|
||||
</wa-popup>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
color: var(--wa-color-text-quiet);
|
||||
font-size: var(--wa-font-size-smaller);
|
||||
font-weight: var(--wa-font-weight-semibold);
|
||||
padding: 0.5em 2.25em;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { html } from 'lit';
|
||||
import { fixtures } from '../../internal/test/fixture.js';
|
||||
import type WaMenuLabel from './menu-label.js';
|
||||
|
||||
describe('<wa-menu-label>', () => {
|
||||
for (const fixture of fixtures) {
|
||||
describe(`with "${fixture.type}" rendering`, () => {
|
||||
it('passes accessibility test', async () => {
|
||||
const el = await fixture<WaMenuLabel>(html` <wa-menu-label>Test</wa-menu-label> `);
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import styles from './menu-label.css';
|
||||
|
||||
/**
|
||||
* @summary Menu labels are used to describe a group of menu items.
|
||||
* @documentation https://backers.webawesome.com/docs/components/menu-label
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @slot - The menu label's content.
|
||||
*/
|
||||
@customElement('wa-menu-label')
|
||||
export default class WaMenuLabel extends WebAwesomeElement {
|
||||
static css = styles;
|
||||
|
||||
render() {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'wa-menu-label': WaMenuLabel;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
text-align: start;
|
||||
background-color: var(--wa-color-surface-raised);
|
||||
border: var(--wa-border-style) var(--wa-border-width-s) var(--wa-color-surface-border);
|
||||
border-radius: var(--wa-border-radius-m);
|
||||
padding: 0.5em 0;
|
||||
overflow: auto;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
::slotted(wa-divider) {
|
||||
--spacing: 0.5em;
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import { html } from 'lit';
|
||||
import sinon from 'sinon';
|
||||
import type { WaSelectEvent } from '../../events/select.js';
|
||||
import { clickOnElement } from '../../internal/test.js';
|
||||
import { fixtures } from '../../internal/test/fixture.js';
|
||||
import type WaMenu from './menu.js';
|
||||
|
||||
describe('<wa-menu>', () => {
|
||||
for (const fixture of fixtures) {
|
||||
describe(`with "${fixture.type}" rendering`, () => {
|
||||
it('emits wa-select with the correct event detail when clicking an item', async () => {
|
||||
const menu = await fixture<WaMenu>(html`
|
||||
<wa-menu>
|
||||
<wa-menu-item value="item-1">Item 1</wa-menu-item>
|
||||
<wa-menu-item value="item-2">Item 2</wa-menu-item>
|
||||
<wa-menu-item value="item-3">Item 3</wa-menu-item>
|
||||
<wa-menu-item value="item-4">Item 4</wa-menu-item>
|
||||
</wa-menu>
|
||||
`);
|
||||
const item2 = menu.querySelectorAll('wa-menu-item')[1];
|
||||
const selectHandler = sinon.spy((event: WaSelectEvent) => {
|
||||
const item = event.detail.item;
|
||||
if (item !== item2) {
|
||||
expect.fail('Incorrect event detail emitted with wa-select');
|
||||
}
|
||||
});
|
||||
|
||||
menu.addEventListener('wa-select', selectHandler);
|
||||
await clickOnElement(item2);
|
||||
|
||||
expect(selectHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('can be selected via keyboard', async () => {
|
||||
const menu = await fixture<WaMenu>(html`
|
||||
<wa-menu>
|
||||
<wa-menu-item value="item-1">Item 1</wa-menu-item>
|
||||
<wa-menu-item value="item-2">Item 2</wa-menu-item>
|
||||
<wa-menu-item value="item-3">Item 3</wa-menu-item>
|
||||
<wa-menu-item value="item-4">Item 4</wa-menu-item>
|
||||
</wa-menu>
|
||||
`);
|
||||
const [item1, item2] = menu.querySelectorAll('wa-menu-item');
|
||||
const selectHandler = sinon.spy((event: WaSelectEvent) => {
|
||||
const item = event.detail.item;
|
||||
if (item !== item2) {
|
||||
expect.fail('Incorrect item selected');
|
||||
}
|
||||
});
|
||||
|
||||
menu.addEventListener('wa-select', selectHandler);
|
||||
|
||||
item1.focus();
|
||||
await item1.updateComplete;
|
||||
await sendKeys({ press: 'ArrowDown' });
|
||||
await sendKeys({ press: 'Enter' });
|
||||
|
||||
expect(selectHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('does not select disabled items when clicking', async () => {
|
||||
const menu = await fixture<WaMenu>(html`
|
||||
<wa-menu>
|
||||
<wa-menu-item value="item-1">Item 1</wa-menu-item>
|
||||
<wa-menu-item value="item-2" disabled>Item 2</wa-menu-item>
|
||||
<wa-menu-item value="item-3">Item 3</wa-menu-item>
|
||||
<wa-menu-item value="item-4">Item 4</wa-menu-item>
|
||||
</wa-menu>
|
||||
`);
|
||||
const item2 = menu.querySelectorAll('wa-menu-item')[1];
|
||||
const selectHandler = sinon.spy();
|
||||
|
||||
menu.addEventListener('wa-select', selectHandler);
|
||||
|
||||
await clickOnElement(item2);
|
||||
|
||||
expect(selectHandler).to.not.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('does not select disabled items when pressing enter', async () => {
|
||||
const menu = await fixture<WaMenu>(html`
|
||||
<wa-menu>
|
||||
<wa-menu-item value="item-1">Item 1</wa-menu-item>
|
||||
<wa-menu-item value="item-2" disabled>Item 2</wa-menu-item>
|
||||
<wa-menu-item value="item-3">Item 3</wa-menu-item>
|
||||
<wa-menu-item value="item-4">Item 4</wa-menu-item>
|
||||
</wa-menu>
|
||||
`);
|
||||
const [item1, item2] = menu.querySelectorAll('wa-menu-item');
|
||||
const selectHandler = sinon.spy();
|
||||
|
||||
menu.addEventListener('wa-select', selectHandler);
|
||||
|
||||
item1.focus();
|
||||
await item1.updateComplete;
|
||||
await sendKeys({ press: 'ArrowDown' });
|
||||
expect(document.activeElement).to.equal(item2);
|
||||
await sendKeys({ press: 'Enter' });
|
||||
await item2.updateComplete;
|
||||
|
||||
expect(selectHandler).to.not.have.been.called;
|
||||
});
|
||||
|
||||
// @see https://github.com/shoelace-style/shoelace/issues/1596
|
||||
it('Should fire "wa-select" when clicking an element within a menu-item', async () => {
|
||||
// eslint-disable-next-line
|
||||
const selectHandler = sinon.spy(() => {});
|
||||
|
||||
const menu: WaMenu = await fixture(html`
|
||||
<wa-menu>
|
||||
<wa-menu-item>
|
||||
<span>Menu item</span>
|
||||
</wa-menu-item>
|
||||
</wa-menu>
|
||||
`);
|
||||
|
||||
menu.addEventListener('wa-select', selectHandler);
|
||||
const span = menu.querySelector('span')!;
|
||||
await clickOnElement(span);
|
||||
|
||||
expect(selectHandler).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,172 +0,0 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { WaSelectEvent } from '../../events/select.js';
|
||||
import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import sizeStyles from '../../styles/utilities/size.css';
|
||||
import '../menu-item/menu-item.js';
|
||||
import type WaMenuItem from '../menu-item/menu-item.js';
|
||||
import styles from './menu.css';
|
||||
|
||||
export interface MenuSelectEventDetail {
|
||||
item: WaMenuItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Menus provide a list of options for the user to choose from.
|
||||
* @documentation https://backers.webawesome.com/docs/components/menu
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency wa-menu-item
|
||||
*
|
||||
* @slot - The menu's content, including menu items, menu labels, and dividers.
|
||||
*
|
||||
* @event {{ item: WaMenuItem }} wa-select - Emitted when a menu item is selected.
|
||||
*/
|
||||
@customElement('wa-menu')
|
||||
export default class WaMenu extends WebAwesomeElement {
|
||||
static css = [sizeStyles, styles];
|
||||
|
||||
/** The component's size. */
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
@query('slot') defaultSlot: HTMLSlotElement;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.setAttribute('role', 'menu');
|
||||
}
|
||||
|
||||
private handleClick(event: MouseEvent) {
|
||||
const menuItemTypes = ['menuitem', 'menuitemcheckbox'];
|
||||
|
||||
const target = event.composedPath().find((el: Element) => menuItemTypes.includes(el?.getAttribute?.('role') || ''));
|
||||
|
||||
if (!target) return;
|
||||
|
||||
// This isn't true. But we use it for TypeScript checks below.
|
||||
const item = target as WaMenuItem;
|
||||
|
||||
if (item.type === 'checkbox') {
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new WaSelectEvent({ item }));
|
||||
}
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
// Make a selection when pressing enter or space
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
const item = this.getCurrentItem();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Simulate a click to support @click handlers on menu items that also work with the keyboard
|
||||
item?.click();
|
||||
}
|
||||
|
||||
// Move the selection when pressing down or up
|
||||
else if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
||||
const items = this.getAllItems();
|
||||
const activeItem = this.getCurrentItem();
|
||||
let index = activeItem ? items.indexOf(activeItem) : 0;
|
||||
|
||||
if (items.length > 0) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
index++;
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
index--;
|
||||
} else if (event.key === 'Home') {
|
||||
index = 0;
|
||||
} else if (event.key === 'End') {
|
||||
index = items.length - 1;
|
||||
}
|
||||
|
||||
if (index < 0) {
|
||||
index = items.length - 1;
|
||||
}
|
||||
if (index > items.length - 1) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
this.setCurrentItem(items[index]);
|
||||
items[index].focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleMouseDown(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
if (this.isMenuItem(target)) {
|
||||
this.setCurrentItem(target as WaMenuItem);
|
||||
}
|
||||
}
|
||||
|
||||
private handleSlotChange() {
|
||||
const items = this.getAllItems();
|
||||
|
||||
// Reset the roving tab index when the slotted items change
|
||||
if (items.length > 0) {
|
||||
this.setCurrentItem(items[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private isMenuItem(item: HTMLElement) {
|
||||
return (
|
||||
item.tagName.toLowerCase() === 'wa-menu-item' ||
|
||||
['menuitem', 'menuitemcheckbox', 'menuitemradio'].includes(item.getAttribute('role') ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
/** @internal Gets all slotted menu items, ignoring dividers, headers, and other elements. */
|
||||
getAllItems() {
|
||||
return [...this.defaultSlot.assignedElements({ flatten: true })].filter((el: HTMLElement) => {
|
||||
if (el.inert || !this.isMenuItem(el)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}) as WaMenuItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Gets the current menu item, which is the menu item that has `tabindex="0"` within the roving tab index.
|
||||
* The menu item may or may not have focus, but for keyboard interaction purposes it's considered the "active" item.
|
||||
*/
|
||||
getCurrentItem() {
|
||||
return this.getAllItems().find(i => i.getAttribute('tabindex') === '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Sets the current menu item to the specified element. This sets `tabindex="0"` on the target element and
|
||||
* `tabindex="-1"` to all other items. This method must be called prior to setting focus on a menu item.
|
||||
*/
|
||||
setCurrentItem(item: WaMenuItem) {
|
||||
const items = this.getAllItems();
|
||||
|
||||
// Update tab indexes
|
||||
items.forEach(i => {
|
||||
i.setAttribute('tabindex', i === item ? '0' : '-1');
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<slot
|
||||
@slotchange=${this.handleSlotChange}
|
||||
@click=${this.handleClick}
|
||||
@keydown=${this.handleKeyDown}
|
||||
@mousedown=${this.handleMouseDown}
|
||||
></slot>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'wa-menu': WaMenu;
|
||||
}
|
||||
}
|
||||
@@ -62,18 +62,18 @@
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
.start,
|
||||
.end {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.prefix::slotted(*) {
|
||||
.start::slotted(*) {
|
||||
margin-inline-end: 0.5em;
|
||||
}
|
||||
|
||||
.suffix::slotted(*) {
|
||||
.end::slotted(*) {
|
||||
margin-inline-start: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ import styles from './option.css';
|
||||
* @dependency wa-icon
|
||||
*
|
||||
* @slot - The option's label.
|
||||
* @slot prefix - Used to prepend an icon or similar element to the menu item.
|
||||
* @slot suffix - Used to append an icon or similar element to the menu item.
|
||||
* @slot start - An element, such as `<wa-icon>`, placed before the label.
|
||||
* @slot end - An element, such as `<wa-icon>`, placed after the label.
|
||||
*
|
||||
* @cssproperty --background-color-current - The current option's background color.
|
||||
* @cssproperty --background-color-hover - The options's background color on hover.
|
||||
@@ -26,8 +26,8 @@ import styles from './option.css';
|
||||
*
|
||||
* @csspart checked-icon - The checked icon, a `<wa-icon>` element.
|
||||
* @csspart label - The option's label.
|
||||
* @csspart prefix - The container that wraps the prefix.
|
||||
* @csspart suffix - The container that wraps the suffix.
|
||||
* @csspart start - The container that wraps the `start` slot.
|
||||
* @csspart end - The container that wraps the `end` slot.
|
||||
*
|
||||
* @cssstate current - The user has keyed into the option, but hasn't selected it yet (shows a highlight)
|
||||
* @cssstate selected - The option is selected and has aria-selected="true"
|
||||
@@ -192,9 +192,9 @@ export default class WaOption extends WebAwesomeElement {
|
||||
variant="solid"
|
||||
aria-hidden="true"
|
||||
></wa-icon>
|
||||
<slot part="prefix" name="prefix" class="prefix"></slot>
|
||||
<slot part="start" name="start" class="start"></slot>
|
||||
<slot part="label" class="label" @slotchange=${this.handleDefaultSlotChange}></slot>
|
||||
<slot part="suffix" name="suffix" class="suffix"></slot>
|
||||
<slot part="end" name="end" class="end"></slot>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,8 +171,10 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -274,8 +276,10 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
@@ -101,7 +101,9 @@ export default class WaRating extends WebAwesomeElement {
|
||||
}
|
||||
|
||||
this.setValue(this.getValueFromMousePosition(event));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
|
||||
private setValue(newValue: number) {
|
||||
@@ -145,7 +147,9 @@ export default class WaRating extends WebAwesomeElement {
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +182,9 @@ export default class WaRating extends WebAwesomeElement {
|
||||
private handleTouchEnd(event: TouchEvent) {
|
||||
this.isHovering = false;
|
||||
this.setValue(this.hoverValue);
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
|
||||
// Prevent click on mobile devices
|
||||
event.preventDefault();
|
||||
|
||||
@@ -158,25 +158,25 @@ label:has(select),
|
||||
}
|
||||
}
|
||||
|
||||
/* Prefix and Suffix */
|
||||
/* Start and End */
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
.start,
|
||||
.end {
|
||||
flex: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--wa-color-neutral-on-quiet);
|
||||
}
|
||||
|
||||
.suffix::slotted(*) {
|
||||
.end::slotted(*) {
|
||||
margin-inline-start: var(--wa-form-control-padding-inline);
|
||||
}
|
||||
|
||||
.prefix::slotted(*) {
|
||||
.start::slotted(*) {
|
||||
margin-inline-end: var(--wa-form-control-padding-inline);
|
||||
}
|
||||
|
||||
:host([multiple]) .prefix::slotted(*) {
|
||||
:host([multiple]) .start::slotted(*) {
|
||||
margin-inline: var(--wa-form-control-padding-inline);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ import styles from './select.css';
|
||||
*
|
||||
* @slot - The listbox options. Must be `<wa-option>` elements. You can use `<wa-divider>` to group items visually.
|
||||
* @slot label - The input's label. Alternatively, you can use the `label` attribute.
|
||||
* @slot prefix - Used to prepend a presentational icon or similar element to the combobox.
|
||||
* @slot suffix - Used to append a presentational icon or similar element to the combobox.
|
||||
* @slot start - An element, such as `<wa-icon>`, placed at the start of the combobox.
|
||||
* @slot end - An element, such as `<wa-icon>`, placed at the end of the combobox.
|
||||
* @slot clear-icon - An icon to use in lieu of the default clear icon.
|
||||
* @slot expand-icon - The icon to show when the control is expanded and collapsed. Rotates on open and close.
|
||||
* @slot hint - Text that describes how to use the input. Alternatively, you can use the `hint` attribute.
|
||||
@@ -62,9 +62,9 @@ import styles from './select.css';
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
* @csspart form-control-input - The select's wrapper.
|
||||
* @csspart hint - The hint's wrapper.
|
||||
* @csspart combobox - The container the wraps the prefix, suffix, combobox, clear icon, and expand button.
|
||||
* @csspart prefix - The container that wraps the prefix slot.
|
||||
* @csspart suffix - The container that wraps the suffix slot.
|
||||
* @csspart combobox - The container the wraps the start, end, value, clear icon, and expand button.
|
||||
* @csspart start - The container that wraps the `start` slot.
|
||||
* @csspart end - The container that wraps the `end` slot.
|
||||
* @csspart display-input - The element that displays the selected option's label, an `<input>` element.
|
||||
* @csspart listbox - The listbox container where options are slotted.
|
||||
* @csspart tags - The container that houses option tags when `multiselect` is used.
|
||||
@@ -381,7 +381,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
// Emit after updating
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
|
||||
@@ -511,7 +511,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
// Emit after update
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new WaClearEvent());
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
@@ -542,7 +542,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
if (this.value !== oldValue) {
|
||||
// Emit after updating
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
@@ -600,7 +600,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
// Emit after updating
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
@@ -900,7 +900,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
@keydown=${this.handleComboboxKeyDown}
|
||||
@mousedown=${this.handleComboboxMouseDown}
|
||||
>
|
||||
<slot part="prefix" name="prefix" class="prefix"></slot>
|
||||
<slot part="start" name="start" class="start"></slot>
|
||||
|
||||
<input
|
||||
part="display-input"
|
||||
@@ -962,7 +962,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
`
|
||||
: ''}
|
||||
|
||||
<slot name="suffix" part="suffix" class="suffix"></slot>
|
||||
<slot name="end" part="end" class="end"></slot>
|
||||
|
||||
<slot name="expand-icon" part="expand-icon" class="expand-icon">
|
||||
<wa-icon library="system" name="chevron-down" variant="solid"></wa-icon>
|
||||
|
||||
@@ -1,214 +1,227 @@
|
||||
:host {
|
||||
--thumb-color: var(--wa-form-control-activated-color);
|
||||
--thumb-gap: calc(var(--thumb-size) * 0.125);
|
||||
--thumb-shadow: initial;
|
||||
--thumb-size: calc(1em * var(--wa-form-control-value-line-height));
|
||||
|
||||
--track-color-active: var(--wa-color-neutral-fill-normal);
|
||||
--track-color-inactive: var(--wa-color-neutral-fill-normal);
|
||||
--track-active-offset: 0%;
|
||||
--track-height: calc(var(--thumb-size) * 0.25);
|
||||
--tooltip-offset: calc(var(--wa-tooltip-arrow-size) * 1.375);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
min-height: max(var(--thumb-size), var(--track-height));
|
||||
--track-size: 0.5em;
|
||||
--thumb-width: 1.4em;
|
||||
--thumb-height: 1.4em;
|
||||
--marker-width: 0.1875em;
|
||||
--marker-height: 0.1875em;
|
||||
}
|
||||
|
||||
input[type='range'] {
|
||||
--percent: 0%;
|
||||
-webkit-appearance: none;
|
||||
border-radius: calc(var(--track-height) / 2);
|
||||
width: 100%;
|
||||
height: var(--track-height);
|
||||
font-size: inherit;
|
||||
line-height: var(--wa-form-control-height);
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
--dir: right;
|
||||
:host([orientation='vertical']) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
background-image: linear-gradient(
|
||||
to var(--dir),
|
||||
var(--track-color-inactive) min(var(--percent), var(--track-active-offset)),
|
||||
var(--track-color-active) min(var(--percent), var(--track-active-offset)),
|
||||
var(--track-color-active) max(var(--percent), var(--track-active-offset)),
|
||||
var(--track-color-inactive) max(var(--percent), var(--track-active-offset))
|
||||
);
|
||||
#label:has(~ .vertical) {
|
||||
display: block;
|
||||
order: 2;
|
||||
max-width: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#description:has(~ .vertical) {
|
||||
order: 3;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Add extra space between slider and label, when present */
|
||||
#label:has(*:not(:empty)) ~ #slider {
|
||||
&.horizontal {
|
||||
margin-block-start: 0.5em;
|
||||
}
|
||||
&.vertical {
|
||||
margin-block-end: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
#slider {
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:focus-visible:not(.disabled) #thumb,
|
||||
&:focus-visible:not(.disabled) #thumb-min,
|
||||
&:focus-visible:not(.disabled) #thumb-max {
|
||||
outline: var(--wa-focus-ring);
|
||||
/* intentionally no offset due to border */
|
||||
}
|
||||
}
|
||||
|
||||
#track {
|
||||
position: relative;
|
||||
border-radius: 9999px;
|
||||
background: var(--wa-color-neutral-fill-normal);
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
/* Orientation */
|
||||
.horizontal #track {
|
||||
height: var(--track-size);
|
||||
}
|
||||
|
||||
.vertical #track {
|
||||
order: 1;
|
||||
width: var(--track-size);
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
/* Disabled */
|
||||
.disabled #track {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Indicator */
|
||||
#indicator {
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
background-color: var(--wa-form-control-activated-color);
|
||||
|
||||
&:dir(ltr) {
|
||||
right: calc(100% - max(var(--start), var(--end)));
|
||||
left: min(var(--start), var(--end));
|
||||
}
|
||||
|
||||
&:dir(rtl) {
|
||||
--dir: left;
|
||||
}
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: var(--track-height);
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
width: var(--thumb-size);
|
||||
height: var(--thumb-size);
|
||||
border-radius: 50%;
|
||||
background-color: var(--thumb-color);
|
||||
box-shadow:
|
||||
var(--thumb-shadow, 0 0 transparent),
|
||||
0 0 0 var(--thumb-gap) var(--wa-color-surface-default);
|
||||
-webkit-appearance: none;
|
||||
margin-top: calc(var(--thumb-size) / -2 + var(--track-height) / 2);
|
||||
transition: var(--wa-transition-fast);
|
||||
transition-property: width, height;
|
||||
}
|
||||
|
||||
&:enabled {
|
||||
&:focus-visible::-webkit-slider-thumb {
|
||||
outline: var(--wa-focus-ring);
|
||||
outline-offset: var(--wa-focus-ring-offset);
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
&::-moz-focus-outer {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&::-moz-range-progress {
|
||||
background-color: var(--track-color-active);
|
||||
border-radius: 3px;
|
||||
height: var(--track-height);
|
||||
}
|
||||
|
||||
&::-moz-range-track {
|
||||
width: 100%;
|
||||
height: var(--track-height);
|
||||
background-color: var(--track-color-inactive);
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
height: var(--thumb-size);
|
||||
width: var(--thumb-size);
|
||||
border-radius: 50%;
|
||||
background-color: var(--thumb-color);
|
||||
box-shadow:
|
||||
var(--thumb-shadow, 0 0 transparent),
|
||||
0 0 0 var(--thumb-gap) var(--wa-color-surface-default);
|
||||
transition-property: background-color, border-color, box-shadow, color;
|
||||
transition-duration: var(--wa-transition-normal);
|
||||
transition-timing-function: var(--wa-transition-easing);
|
||||
}
|
||||
|
||||
&:enabled {
|
||||
&:focus-visible::-moz-range-thumb {
|
||||
outline: var(--wa-focus-ring);
|
||||
outline-offset: var(--wa-focus-ring-offset);
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
right: min(var(--start), var(--end));
|
||||
left: calc(100% - max(var(--start), var(--end)));
|
||||
}
|
||||
}
|
||||
|
||||
/* States */
|
||||
/* nesting these styles yields broken results in Safari */
|
||||
input[type='range']:focus {
|
||||
outline: none;
|
||||
.horizontal #indicator {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:host :has(:disabled) input[type='range'] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
||||
&::-moz-range-thumb,
|
||||
&::-webkit-slider-thumb {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.vertical #indicator {
|
||||
top: calc(100% - var(--end));
|
||||
bottom: var(--start);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Tooltip output */
|
||||
.tooltip {
|
||||
/* Thumbs */
|
||||
#thumb,
|
||||
#thumb-min,
|
||||
#thumb-max {
|
||||
z-index: 3;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
inset-inline-start: 0;
|
||||
width: var(--thumb-width);
|
||||
height: var(--thumb-height);
|
||||
border: solid 0.125em var(--wa-color-surface-default);
|
||||
border-radius: 50%;
|
||||
background-color: var(--wa-form-control-activated-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
inset-block-end: calc(50% + (var(--thumb-size) / 2) + var(--tooltip-offset));
|
||||
border-radius: var(--wa-tooltip-border-radius);
|
||||
background-color: var(--wa-tooltip-background-color);
|
||||
font-family: inherit;
|
||||
font-size: var(--wa-tooltip-font-size);
|
||||
line-height: var(--wa-tooltip-line-height);
|
||||
color: var(--wa-tooltip-content-color);
|
||||
opacity: 0;
|
||||
padding: 0.25em 0.5em;
|
||||
transition: var(--wa-transition-normal) opacity;
|
||||
.disabled #thumb,
|
||||
.disabled #thumb-min,
|
||||
.disabled #thumb-max {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.horizontal #thumb,
|
||||
.horizontal #thumb-min,
|
||||
.horizontal #thumb-max {
|
||||
top: calc(50% - var(--thumb-height) / 2);
|
||||
|
||||
&:dir(ltr) {
|
||||
right: auto;
|
||||
left: calc(var(--position) - var(--thumb-width) / 2);
|
||||
}
|
||||
|
||||
&:dir(rtl) {
|
||||
right: calc(var(--position) - var(--thumb-width) / 2);
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical #thumb,
|
||||
.vertical #thumb-min,
|
||||
.vertical #thumb-max {
|
||||
bottom: calc(var(--position) - var(--thumb-height) / 2);
|
||||
left: calc(50% - var(--thumb-width) / 2);
|
||||
}
|
||||
|
||||
/* Range-specific thumb styles */
|
||||
:host([range]) {
|
||||
#thumb-min:focus-visible,
|
||||
#thumb-max:focus-visible {
|
||||
z-index: 4; /* Ensure focused thumb appears on top */
|
||||
outline: var(--wa-focus-ring);
|
||||
/* intentionally no offset due to border */
|
||||
}
|
||||
}
|
||||
|
||||
/* Markers */
|
||||
#markers {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
inset-inline-start: 50%;
|
||||
inset-block-start: 100%;
|
||||
translate: calc(-1 * var(--wa-tooltip-arrow-size));
|
||||
border-inline: var(--wa-tooltip-arrow-size) solid transparent;
|
||||
border-block-start: var(--border-block);
|
||||
.marker {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
width: var(--marker-width);
|
||||
height: var(--marker-height);
|
||||
border-radius: 50%;
|
||||
background-color: var(--wa-color-surface-default);
|
||||
}
|
||||
|
||||
.marker:first-of-type,
|
||||
.marker:last-of-type {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.horizontal .marker {
|
||||
top: calc(50% - var(--marker-height) / 2);
|
||||
left: calc(var(--position) - var(--marker-width) / 2);
|
||||
}
|
||||
|
||||
.vertical .marker {
|
||||
top: calc(var(--position) - var(--marker-height) / 2);
|
||||
left: calc(50% - var(--marker-width) / 2);
|
||||
}
|
||||
|
||||
/* Marker labels */
|
||||
#references {
|
||||
position: relative;
|
||||
|
||||
slot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&:dir(rtl)::after {
|
||||
translate: var(--wa-tooltip-arrow-size);
|
||||
::slotted(*) {
|
||||
color: var(--wa-color-text-quiet);
|
||||
font-size: 0.875em;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal {
|
||||
#references {
|
||||
margin-block-start: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical {
|
||||
display: flex;
|
||||
margin-inline: auto;
|
||||
|
||||
#track {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
#references {
|
||||
order: 2;
|
||||
width: min-content;
|
||||
margin-inline-start: 0.75em;
|
||||
|
||||
--inset-block: calc(50% + (var(--thumb-size) / 2) + var(--tooltip-offset));
|
||||
--border-block: var(--wa-tooltip-arrow-size) solid var(--wa-tooltip-background-color);
|
||||
|
||||
@media (forced-colors: active) {
|
||||
border: solid 1px transparent;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
slot {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* RTL tooltip positioning */
|
||||
:host(:dir(rtl)) .tooltip {
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
/* Tooltip on bottom */
|
||||
:host([tooltip='bottom']) .tooltip {
|
||||
inset-block-end: auto;
|
||||
inset-block-start: calc(50% + (var(--thumb-size) / 2) + var(--tooltip-offset));
|
||||
|
||||
&::after {
|
||||
border-block-end: var(--border-block);
|
||||
inset-block-start: auto;
|
||||
inset-block-end: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bottom tooltip RTL fix */
|
||||
:host([tooltip='bottom']:dir(rtl)) .tooltip {
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: 0;
|
||||
.vertical #references slot {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -21,9 +21,8 @@ describe('<wa-slider>', () => {
|
||||
it('default properties', async () => {
|
||||
const el = await fixture<WaSlider>(html` <wa-slider></wa-slider> `);
|
||||
|
||||
expect(el.name).to.equal('');
|
||||
expect(el.name).to.equal(null);
|
||||
expect(el.value).to.equal(0);
|
||||
expect(el.title).to.equal('');
|
||||
expect(el.label).to.equal('');
|
||||
expect(el.hint).to.equal('');
|
||||
expect(el.disabled).to.be.false;
|
||||
@@ -31,22 +30,16 @@ describe('<wa-slider>', () => {
|
||||
expect(el.min).to.equal(0);
|
||||
expect(el.max).to.equal(100);
|
||||
expect(el.step).to.equal(1);
|
||||
expect(el.tooltip).to.equal('top');
|
||||
expect(el.tooltipPlacement).to.equal('top');
|
||||
expect(el.defaultValue).to.equal(0);
|
||||
});
|
||||
|
||||
it('should have title if title attribute is set', async () => {
|
||||
const el = await fixture<WaSlider>(html` <wa-slider title="Test"></wa-slider> `);
|
||||
const input = el.shadowRoot!.querySelector('input')!;
|
||||
|
||||
expect(input.title).to.equal('Test');
|
||||
});
|
||||
|
||||
it('should be disabled with the disabled attribute', async () => {
|
||||
const el = await fixture<WaSlider>(html` <wa-slider disabled></wa-slider> `);
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>('.control')!;
|
||||
const input = el.shadowRoot!.querySelector<HTMLElement>("[role='slider']")!;
|
||||
|
||||
expect(input.disabled).to.be.true;
|
||||
expect(el.matches(':disabled')).to.be.true;
|
||||
expect(input.getAttribute('aria-disabled')).to.equal('true');
|
||||
});
|
||||
|
||||
describe('when the value changes', () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -117,22 +117,29 @@ export default class WaSwitch extends WebAwesomeFormAssociatedElement {
|
||||
private handleClick() {
|
||||
this.hasInteracted = true;
|
||||
this.checked = !this.checked;
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
this.checked = false;
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
this.checked = true;
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new InputEvent('input'));
|
||||
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -206,13 +206,14 @@ export default class WaTextarea extends WebAwesomeFormAssociatedElement {
|
||||
this.valueHasChanged = true;
|
||||
this.value = this.input.value;
|
||||
this.setTextareaDimensions();
|
||||
this.relayNativeEvent(event, { bubbles: true, composed: true });
|
||||
this.checkValidity();
|
||||
this.relayNativeEvent(event, { bubbles: true, composed: true });
|
||||
}
|
||||
|
||||
private handleInput() {
|
||||
private handleInput(event: InputEvent) {
|
||||
this.valueHasChanged = true;
|
||||
this.value = this.input.value;
|
||||
this.relayNativeEvent(event, { bubbles: true, composed: true });
|
||||
}
|
||||
|
||||
private setTextareaDimensions() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type WaMenuItem from '../components/menu-item/menu-item.js';
|
||||
|
||||
export class WaSelectEvent extends Event {
|
||||
readonly detail;
|
||||
|
||||
@@ -10,7 +8,7 @@ export class WaSelectEvent extends Event {
|
||||
}
|
||||
|
||||
interface WaSelectEventDetail {
|
||||
item: WaMenuItem;
|
||||
item: Element;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -14,6 +14,9 @@ export function animateWithClass(el: Element, className: string) {
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
if (el.classList.contains(className)) {
|
||||
return;
|
||||
}
|
||||
el.classList.remove(className);
|
||||
el.classList.add(className);
|
||||
|
||||
|
||||
@@ -43,3 +43,132 @@ export function drag(container: HTMLElement, options?: Partial<DragOptions>) {
|
||||
move(options.initialEvent);
|
||||
}
|
||||
}
|
||||
|
||||
const supportsTouch = typeof window !== 'undefined' && 'ontouchstart' in window;
|
||||
|
||||
/**
|
||||
* Attaches the necessary events to make an element draggable.
|
||||
*
|
||||
* This by itself will not make the element draggable, but it provides the events and callbacks necessary to facilitate
|
||||
* dragging. Use the `clientX` and `clientY` arguments of each callback to update the UI as desired when dragging.
|
||||
*
|
||||
* Drag functionality will be enabled as soon as the constructor is called. A `start()` and `stop()` method can be used
|
||||
* to start and stop it, if needed.
|
||||
*
|
||||
* @usage
|
||||
*
|
||||
* const draggable = new DraggableElement(element, {
|
||||
* start: (clientX, clientY) => { ... },
|
||||
* move: (clientX, clientY) => { ... },
|
||||
* stop: (clientX, clientY) => { ... }
|
||||
* });
|
||||
*/
|
||||
export class DraggableElement {
|
||||
private element: Element;
|
||||
private isActive = false;
|
||||
private isDragging = false;
|
||||
private options: DraggableElementOptions;
|
||||
|
||||
constructor(el: Element, options: Partial<DraggableElementOptions>) {
|
||||
this.element = el;
|
||||
this.options = {
|
||||
start: () => undefined,
|
||||
stop: () => undefined,
|
||||
move: () => undefined,
|
||||
...options,
|
||||
};
|
||||
|
||||
this.start();
|
||||
}
|
||||
|
||||
private handleDragStart = (event: PointerEvent | TouchEvent) => {
|
||||
const clientX = supportsTouch && 'touches' in event ? event.touches[0].clientX : (event as PointerEvent).clientX;
|
||||
const clientY = supportsTouch && 'touches' in event ? event.touches[0].clientY : (event as PointerEvent).clientY;
|
||||
|
||||
// Prevent scrolling while dragging
|
||||
event.preventDefault();
|
||||
|
||||
if (
|
||||
this.isDragging ||
|
||||
// Prevent right-clicks from triggering drags
|
||||
(!supportsTouch && (event as PointerEvent).buttons > 1)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isDragging = true;
|
||||
|
||||
document.addEventListener('pointerup', this.handleDragStop);
|
||||
document.addEventListener('pointermove', this.handleDragMove);
|
||||
document.addEventListener('touchend', this.handleDragStop);
|
||||
document.addEventListener('touchmove', this.handleDragMove);
|
||||
this.options.start(clientX, clientY);
|
||||
};
|
||||
|
||||
private handleDragStop = (event: PointerEvent | TouchEvent) => {
|
||||
const clientX = supportsTouch && 'touches' in event ? event.touches[0].clientX : (event as PointerEvent).clientX;
|
||||
const clientY = supportsTouch && 'touches' in event ? event.touches[0].clientY : (event as PointerEvent).clientY;
|
||||
|
||||
this.isDragging = false;
|
||||
document.removeEventListener('pointerup', this.handleDragStop);
|
||||
document.removeEventListener('pointermove', this.handleDragMove);
|
||||
document.removeEventListener('touchend', this.handleDragStop);
|
||||
document.removeEventListener('touchmove', this.handleDragMove);
|
||||
this.options.stop(clientX, clientY);
|
||||
};
|
||||
|
||||
private handleDragMove = (event: PointerEvent | TouchEvent) => {
|
||||
const clientX = supportsTouch && 'touches' in event ? event.touches[0].clientX : (event as PointerEvent).clientX;
|
||||
const clientY = supportsTouch && 'touches' in event ? event.touches[0].clientY : (event as PointerEvent).clientY;
|
||||
|
||||
// Prevent text selection while dragging
|
||||
window.getSelection()?.removeAllRanges();
|
||||
|
||||
this.options.move(clientX, clientY);
|
||||
};
|
||||
|
||||
/** Start listening to drags. */
|
||||
public start() {
|
||||
if (!this.isActive) {
|
||||
this.element.addEventListener('pointerdown', this.handleDragStart);
|
||||
if (supportsTouch) {
|
||||
this.element.addEventListener('touchstart', this.handleDragStart);
|
||||
}
|
||||
this.isActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop listening to drags. */
|
||||
public stop() {
|
||||
document.removeEventListener('pointerup', this.handleDragStop);
|
||||
document.removeEventListener('pointermove', this.handleDragMove);
|
||||
document.removeEventListener('touchend', this.handleDragStop);
|
||||
document.removeEventListener('touchmove', this.handleDragMove);
|
||||
this.element.removeEventListener('pointerdown', this.handleDragStart);
|
||||
if (supportsTouch) {
|
||||
this.element.removeEventListener('touchstart', this.handleDragStart);
|
||||
}
|
||||
this.isActive = false;
|
||||
this.isDragging = false;
|
||||
}
|
||||
|
||||
/** Starts or stops the drag listeners. */
|
||||
public toggle(isActive?: boolean) {
|
||||
const isGoingToBeActive = isActive !== undefined ? isActive : !this.isActive;
|
||||
|
||||
if (isGoingToBeActive) {
|
||||
this.start();
|
||||
} else {
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface DraggableElementOptions {
|
||||
/** Runs when dragging starts. */
|
||||
start: (clientX: number, clientY: number) => void;
|
||||
/** Runs as the user is dragging. This may execute often, so avoid expensive operations. */
|
||||
move: (clientX: number, clientY: number) => void;
|
||||
/** Runs when dragging ends. */
|
||||
stop: (clientX: number, clientY: number) => void;
|
||||
}
|
||||
|
||||
64
packages/webawesome/src/internal/submit-on-enter.ts
Normal file
64
packages/webawesome/src/internal/submit-on-enter.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type WaButton from '../components/button/button.js';
|
||||
import type { WebAwesomeFormAssociatedElement } from './webawesome-form-associated-element.js';
|
||||
|
||||
export function submitOnEnter<T extends HTMLElement>(event: KeyboardEvent, el: T) {
|
||||
const hasModifier = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
||||
|
||||
// Pressing enter when focused on an input should submit the form like a native input, but we wait a tick before
|
||||
// submitting to allow users to cancel the keydown event if they need to
|
||||
if (event.key === 'Enter' && !hasModifier) {
|
||||
// setTimeout in case the event is caught higher up in the tree and defaultPrevented
|
||||
setTimeout(() => {
|
||||
//
|
||||
// When using an Input Method Editor (IME), pressing enter will cause the form to submit unexpectedly. One way
|
||||
// to check for this is to look at event.isComposing, which will be true when the IME is open.
|
||||
//
|
||||
// See https://github.com/shoelace-style/shoelace/pull/988
|
||||
//
|
||||
if (!event.defaultPrevented && !event.isComposing) {
|
||||
submitForm(el);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function submitForm(el: HTMLElement | WebAwesomeFormAssociatedElement) {
|
||||
let form: HTMLFormElement | null = null;
|
||||
|
||||
if ('form' in el) {
|
||||
form = el.form as HTMLFormElement | null;
|
||||
}
|
||||
|
||||
if (!form && 'getForm' in el) {
|
||||
form = el.getForm();
|
||||
}
|
||||
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formElements = [...form.elements];
|
||||
|
||||
// If we're the only formElement, we submit like a native input.
|
||||
if (formElements.length === 1) {
|
||||
form.requestSubmit(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const button = formElements.find((el: HTMLButtonElement) => el.type === 'submit' && !el.matches(':disabled')) as
|
||||
| undefined
|
||||
| HTMLButtonElement
|
||||
| WaButton;
|
||||
|
||||
// No button found, don't submit.
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (['input', 'button'].includes(button.localName)) {
|
||||
form.requestSubmit(button);
|
||||
} else {
|
||||
// requestSubmit() wont work with `<wa-button>`, so trigger a manual click.
|
||||
button.click();
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,7 @@ function runAllValidityTests(
|
||||
const form = await fixture(html`<form id="${formId}"></form>`);
|
||||
const control = await createControl();
|
||||
expect(control.getForm()).to.equal(null);
|
||||
// control.setAttribute("form", 'test-form');
|
||||
control.form = 'test-form';
|
||||
await control.updateComplete;
|
||||
expect(control.getForm()).to.equal(form);
|
||||
|
||||
123
packages/webawesome/src/internal/validators/slider-validator.ts
Normal file
123
packages/webawesome/src/internal/validators/slider-validator.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type WaSlider from '../../components/slider/slider.js';
|
||||
import type { Validator } from '../webawesome-form-associated-element.js';
|
||||
|
||||
/**
|
||||
* Comprehensive validator for sliders that handles required, range, and step validation
|
||||
*/
|
||||
export const SliderValidator = (): Validator<WaSlider> => {
|
||||
// Create a native range input to get localized validation messages
|
||||
const nativeRequiredRange = Object.assign(document.createElement('input'), {
|
||||
type: 'range',
|
||||
required: true,
|
||||
});
|
||||
|
||||
return {
|
||||
observedAttributes: ['required', 'min', 'max', 'step'],
|
||||
checkValidity(element) {
|
||||
const validity: ReturnType<Validator['checkValidity']> = {
|
||||
message: '',
|
||||
isValid: true,
|
||||
invalidKeys: [],
|
||||
};
|
||||
|
||||
// Create native range input to get localized validation messages
|
||||
const createNativeRange = (value: number, min: number, max: number, step: number) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'range';
|
||||
input.min = String(min);
|
||||
input.max = String(max);
|
||||
input.step = String(step);
|
||||
input.value = String(value);
|
||||
|
||||
// Trigger validation
|
||||
input.checkValidity();
|
||||
return input.validationMessage;
|
||||
};
|
||||
|
||||
// Check required validation first
|
||||
if (element.required && !element.hasInteracted) {
|
||||
validity.isValid = false;
|
||||
validity.invalidKeys.push('valueMissing');
|
||||
validity.message = nativeRequiredRange.validationMessage || 'Please fill out this field.';
|
||||
return validity;
|
||||
}
|
||||
|
||||
// For range sliders, validate both values
|
||||
if (element.isRange) {
|
||||
const minValue = element.minValue;
|
||||
const maxValue = element.maxValue;
|
||||
|
||||
// Check range underflow for min value
|
||||
if (minValue < element.min) {
|
||||
validity.isValid = false;
|
||||
validity.invalidKeys.push('rangeUnderflow');
|
||||
validity.message =
|
||||
createNativeRange(minValue, element.min, element.max, element.step) ||
|
||||
`Value must be greater than or equal to ${element.min}.`;
|
||||
return validity;
|
||||
}
|
||||
|
||||
// Check range overflow for max value
|
||||
if (maxValue > element.max) {
|
||||
validity.isValid = false;
|
||||
validity.invalidKeys.push('rangeOverflow');
|
||||
validity.message =
|
||||
createNativeRange(maxValue, element.min, element.max, element.step) ||
|
||||
`Value must be less than or equal to ${element.max}.`;
|
||||
return validity;
|
||||
}
|
||||
|
||||
// Check step mismatch
|
||||
if (element.step && element.step !== 1) {
|
||||
const minStepMismatch = (minValue - element.min) % element.step !== 0;
|
||||
const maxStepMismatch = (maxValue - element.min) % element.step !== 0;
|
||||
|
||||
if (minStepMismatch || maxStepMismatch) {
|
||||
validity.isValid = false;
|
||||
validity.invalidKeys.push('stepMismatch');
|
||||
const testValue = minStepMismatch ? minValue : maxValue;
|
||||
validity.message =
|
||||
createNativeRange(testValue, element.min, element.max, element.step) ||
|
||||
`Value must be a multiple of ${element.step}.`;
|
||||
return validity;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single value validation
|
||||
const value = element.value;
|
||||
|
||||
// Check range underflow
|
||||
if (value < element.min) {
|
||||
validity.isValid = false;
|
||||
validity.invalidKeys.push('rangeUnderflow');
|
||||
validity.message =
|
||||
createNativeRange(value, element.min, element.max, element.step) ||
|
||||
`Value must be greater than or equal to ${element.min}.`;
|
||||
return validity;
|
||||
}
|
||||
|
||||
// Check range overflow
|
||||
if (value > element.max) {
|
||||
validity.isValid = false;
|
||||
validity.invalidKeys.push('rangeOverflow');
|
||||
validity.message =
|
||||
createNativeRange(value, element.min, element.max, element.step) ||
|
||||
`Value must be less than or equal to ${element.max}.`;
|
||||
return validity;
|
||||
}
|
||||
|
||||
// Check step mismatch
|
||||
if (element.step && element.step !== 1 && (value - element.min) % element.step !== 0) {
|
||||
validity.isValid = false;
|
||||
validity.invalidKeys.push('stepMismatch');
|
||||
validity.message =
|
||||
createNativeRange(value, element.min, element.max, element.step) ||
|
||||
`Value must be a multiple of ${element.step}.`;
|
||||
return validity;
|
||||
}
|
||||
}
|
||||
|
||||
return validity;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -16,23 +16,6 @@
|
||||
--wa-color-brand-10: var(--wa-color-blue-10);
|
||||
--wa-color-brand-05: var(--wa-color-blue-05);
|
||||
--wa-color-brand: var(--wa-color-blue);
|
||||
|
||||
--wa-color-brand-lt-50: var(--wa-color-blue-lt-50);
|
||||
--wa-color-brand-gte-50: var(--wa-color-blue-gte-50);
|
||||
|
||||
--wa-color-brand-lt-60: var(--wa-color-blue-lt-60);
|
||||
--wa-color-brand-gte-60: var(--wa-color-blue-gte-60);
|
||||
|
||||
--wa-color-brand-lt-70: var(--wa-color-blue-lt-70);
|
||||
--wa-color-brand-gte-70: var(--wa-color-blue-gte-70);
|
||||
|
||||
--wa-color-brand-max-50: var(--wa-color-blue-max-50);
|
||||
--wa-color-brand-min-50: var(--wa-color-blue-min-50);
|
||||
--wa-color-brand-max-60: var(--wa-color-blue-max-60);
|
||||
--wa-color-brand-min-60: var(--wa-color-blue-min-60);
|
||||
--wa-color-brand-max-70: var(--wa-color-blue-max-70);
|
||||
--wa-color-brand-min-70: var(--wa-color-blue-min-70);
|
||||
|
||||
--wa-color-brand-on: var(--wa-color-blue-on);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user