mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-16 05:59:15 +00:00
Compare commits
18 Commits
docs-fix
...
with-caret
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2b05600a5 | ||
|
|
dc556e5379 | ||
|
|
c11d6129a3 | ||
|
|
dd5c32680a | ||
|
|
81996cbc63 | ||
|
|
d20945d78b | ||
|
|
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>
|
||||
|
||||
@@ -258,20 +258,16 @@
|
||||
<th><code>small</code>/<code>s</code></th>
|
||||
<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-button slot="trigger" with-caret>Dropdown</wa-button>
|
||||
<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-button slot="trigger" with-caret>Dropdown</wa-button>
|
||||
<wa-dropdown-item>Menu Item 1</wa-dropdown-item>
|
||||
<wa-dropdown-item>Menu Item 2</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -279,20 +275,16 @@
|
||||
<th><code>medium</code>/<code>m</code></th>
|
||||
<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-button slot="trigger" with-caret>Dropdown</wa-button>
|
||||
<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-button slot="trigger" with-caret>Dropdown</wa-button>
|
||||
<wa-dropdown-item>Menu Item 1</wa-dropdown-item>
|
||||
<wa-dropdown-item>Menu Item 2</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -300,20 +292,16 @@
|
||||
<th><code>large</code>/<code>l</code></th>
|
||||
<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-button slot="trigger" with-caret>Dropdown</wa-button>
|
||||
<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-button slot="trigger" with-caret>Dropdown</wa-button>
|
||||
<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>
|
||||
|
||||
@@ -5,17 +5,4 @@ tags: component
|
||||
parent: breadcrumb
|
||||
---
|
||||
|
||||
```html {.example}
|
||||
<wa-breadcrumb>
|
||||
<wa-breadcrumb-item>
|
||||
<wa-icon slot="prefix" name="house" variant="solid"></wa-icon>
|
||||
Home
|
||||
</wa-breadcrumb-item>
|
||||
<wa-breadcrumb-item>Clothing</wa-breadcrumb-item>
|
||||
<wa-breadcrumb-item>Shirts</wa-breadcrumb-item>
|
||||
</wa-breadcrumb>
|
||||
```
|
||||
|
||||
:::info
|
||||
Additional demonstrations can be found in the [breadcrumb examples](/docs/components/breadcrumb).
|
||||
:::
|
||||
This component must be used as a child of `<wa-breadcrumb>`. Please see the [Breadcrumb docs](/docs/components/breadcrumb) to see examples of this component in action.
|
||||
|
||||
@@ -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>
|
||||
@@ -165,12 +165,10 @@ Dropdowns can be placed into button groups.
|
||||
<wa-button-group label="Example Button Group">
|
||||
<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-button slot="trigger" with-caret>Dropdown</wa-button>
|
||||
<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,72 +126,72 @@ 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>
|
||||
```
|
||||
|
||||
### Caret
|
||||
|
||||
Use the `caret` attribute to add a dropdown indicator when a button will trigger a dropdown, menu, or popover.
|
||||
Use the `with-caret` attribute to add a dropdown indicator when a button will trigger a dropdown, menu, or popover.
|
||||
|
||||
```html {.example}
|
||||
<wa-button size="small" caret>Small</wa-button>
|
||||
<wa-button size="medium" caret>Medium</wa-button>
|
||||
<wa-button size="large" caret>Large</wa-button>
|
||||
<wa-button size="small" with-caret>Small</wa-button>
|
||||
<wa-button size="medium" with-caret>Medium</wa-button>
|
||||
<wa-button size="large" with-caret>Large</wa-button>
|
||||
```
|
||||
|
||||
### Loading
|
||||
|
||||
@@ -6,41 +6,4 @@ parent: carousel
|
||||
icon: carousel-item
|
||||
---
|
||||
|
||||
```html {.example}
|
||||
<wa-carousel pagination>
|
||||
<wa-carousel-item>
|
||||
<img
|
||||
alt="The sun shines on the mountains and trees - Photo by Adam Kool on Unsplash"
|
||||
src="/assets/examples/carousel/mountains.jpg"
|
||||
/>
|
||||
</wa-carousel-item>
|
||||
<wa-carousel-item>
|
||||
<img
|
||||
alt="A waterfall in the middle of a forest - Photo by Thomas Kelly on Unsplash"
|
||||
src="/assets/examples/carousel/waterfall.jpg"
|
||||
/>
|
||||
</wa-carousel-item>
|
||||
<wa-carousel-item>
|
||||
<img
|
||||
alt="The sun is setting over a lavender field - Photo by Leonard Cotte on Unsplash"
|
||||
src="/assets/examples/carousel/sunset.jpg"
|
||||
/>
|
||||
</wa-carousel-item>
|
||||
<wa-carousel-item>
|
||||
<img
|
||||
alt="A field of grass with the sun setting in the background - Photo by Sapan Patel on Unsplash"
|
||||
src="/assets/examples/carousel/field.jpg"
|
||||
/>
|
||||
</wa-carousel-item>
|
||||
<wa-carousel-item>
|
||||
<img
|
||||
alt="A scenic view of a mountain with clouds rolling in - Photo by V2osk on Unsplash"
|
||||
src="/assets/examples/carousel/valley.jpg"
|
||||
/>
|
||||
</wa-carousel-item>
|
||||
</wa-carousel>
|
||||
```
|
||||
|
||||
:::info
|
||||
Additional demonstrations can be found in the [carousel examples](/docs/components/carousel).
|
||||
:::
|
||||
This component must be used as a child of `<wa-carousel>`. Please see the [Carousel docs](/docs/components/carousel) to see examples of this component in action.
|
||||
|
||||
@@ -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" with-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-button slot="trigger" with-caret>Dropdown</wa-button>
|
||||
|
||||
<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" with-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" with-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" with-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" with-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" with-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" with-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>
|
||||
```
|
||||
|
||||
@@ -111,72 +258,112 @@ 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-button slot="trigger" with-caret>Edit</wa-button>
|
||||
|
||||
<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-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 offset="30">
|
||||
<wa-button slot="trigger" with-caret>Edit</wa-button>
|
||||
|
||||
<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" with-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" with-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.
|
||||
:::
|
||||
@@ -6,50 +6,4 @@ parent: select
|
||||
icon: option
|
||||
---
|
||||
|
||||
```html {.example}
|
||||
<wa-select label="Select one">
|
||||
<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>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable an option and prevent it from being selected.
|
||||
|
||||
```html {.example}
|
||||
<wa-select label="Select one">
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
<wa-option value="option-2" disabled>Option 2</wa-option>
|
||||
<wa-option value="option-3">Option 3</wa-option>
|
||||
</wa-select>
|
||||
```
|
||||
|
||||
### Prefix & Suffix
|
||||
|
||||
Add icons to the start and end of menu items using the `prefix` and `suffix` slots.
|
||||
|
||||
```html {.example}
|
||||
<wa-select label="Select one">
|
||||
<wa-option value="option-1">
|
||||
<wa-icon slot="prefix" name="envelope" variant="solid"></wa-icon>
|
||||
Email
|
||||
<wa-icon slot="suffix" name="circle-check" variant="solid"></wa-icon>
|
||||
</wa-option>
|
||||
|
||||
<wa-option value="option-2">
|
||||
<wa-icon slot="prefix" name="phone" variant="solid"></wa-icon>
|
||||
Phone
|
||||
<wa-icon slot="suffix" name="circle-check" variant="solid"></wa-icon>
|
||||
</wa-option>
|
||||
|
||||
<wa-option value="option-3">
|
||||
<wa-icon slot="prefix" name="comment" variant="solid"></wa-icon>
|
||||
Chat
|
||||
<wa-icon slot="suffix" name="circle-check" variant="solid"></wa-icon>
|
||||
</wa-option>
|
||||
</wa-select>
|
||||
```
|
||||
This component must be used as a child of `<wa-select>`. Please see the [Select docs](/docs/components/select) to see examples of this component in action.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -44,7 +44,7 @@ Set the `appearance` attribute to `button` on all radios to render a radio butto
|
||||
<wa-radio appearance="button" value="3">Option 3</wa-radio>
|
||||
</wa-radio-group>
|
||||
|
||||
<br>
|
||||
<br />
|
||||
|
||||
<wa-radio-group
|
||||
label="Vertical options"
|
||||
@@ -60,12 +60,22 @@ Set the `appearance` attribute to `button` on all radios to render a radio butto
|
||||
</wa-radio-group>
|
||||
```
|
||||
|
||||
### Disabling Options
|
||||
### Disabling
|
||||
|
||||
Radios and radio buttons can be disabled by adding the `disabled` attribute to the respective options inside the radio group.
|
||||
To disable the entire radio group, add the `disabled` attribute to the radio group.
|
||||
|
||||
```html {.example}
|
||||
<wa-radio-group label="Select an option" name="a" value="1">
|
||||
<wa-radio-group label="Select an option" disabled>
|
||||
<wa-radio value="1">Option 1</wa-radio>
|
||||
<wa-radio value="2" disabled>Option 2</wa-radio>
|
||||
<wa-radio value="3">Option 3</wa-radio>
|
||||
</wa-radio-group>
|
||||
```
|
||||
|
||||
To disable individual options, add the `disabled` attribute to the respective options.
|
||||
|
||||
```html {.example}
|
||||
<wa-radio-group label="Select an option">
|
||||
<wa-radio value="1">Option 1</wa-radio>
|
||||
<wa-radio value="2" disabled>Option 2</wa-radio>
|
||||
<wa-radio value="3">Option 3</wa-radio>
|
||||
|
||||
@@ -7,71 +7,4 @@ native: radio
|
||||
icon: radio-group
|
||||
---
|
||||
|
||||
Radios are designed to be used with [radio groups](/docs/components/radio-group).
|
||||
|
||||
```html {.example}
|
||||
<wa-radio-group label="Select an option" name="a" value="1">
|
||||
<wa-radio value="1">Option 1</wa-radio>
|
||||
<wa-radio value="2">Option 2</wa-radio>
|
||||
<wa-radio value="3">Option 3</wa-radio>
|
||||
</wa-radio-group>
|
||||
```
|
||||
|
||||
:::info
|
||||
This component works with standard `<form>` elements. Please refer to the section on [form controls](/docs/form-controls) to learn more about form submission and client-side validation.
|
||||
:::
|
||||
|
||||
## Examples
|
||||
|
||||
### Initial Value
|
||||
|
||||
To set the initial value and checked state, use the `value` attribute on the containing radio group.
|
||||
|
||||
```html {.example}
|
||||
<wa-radio-group label="Select an option" name="a" value="3">
|
||||
<wa-radio value="1">Option 1</wa-radio>
|
||||
<wa-radio value="2">Option 2</wa-radio>
|
||||
<wa-radio value="3">Option 3</wa-radio>
|
||||
</wa-radio-group>
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable a radio.
|
||||
|
||||
```html {.example}
|
||||
<wa-radio-group label="Select an option" name="a" value="1">
|
||||
<wa-radio value="1">Option 1</wa-radio>
|
||||
<wa-radio value="2" disabled>Option 2</wa-radio>
|
||||
<wa-radio value="3">Option 3</wa-radio>
|
||||
</wa-radio-group>
|
||||
```
|
||||
|
||||
### Sizes
|
||||
|
||||
Add the `size` attribute to the [Radio Group](/docs/components/radio-group) to change the radios' size.
|
||||
|
||||
```html {.example}
|
||||
<wa-radio-group size="small" value="1">
|
||||
<wa-radio value="1">Small 1</wa-radio>
|
||||
<wa-radio value="2">Small 2</wa-radio>
|
||||
<wa-radio value="3">Small 3</wa-radio>
|
||||
</wa-radio-group>
|
||||
|
||||
<br />
|
||||
|
||||
<wa-radio-group size="medium" value="1">
|
||||
<wa-radio value="1">Medium 1</wa-radio>
|
||||
<wa-radio value="2">Medium 2</wa-radio>
|
||||
<wa-radio value="3">Medium 3</wa-radio>
|
||||
</wa-radio-group>
|
||||
|
||||
<br />
|
||||
|
||||
<wa-radio-group size="large" value="1">
|
||||
<wa-radio value="1">Large 1</wa-radio>
|
||||
<wa-radio value="2">Large 2</wa-radio>
|
||||
<wa-radio value="3">Large 3</wa-radio>
|
||||
</wa-radio-group>
|
||||
```
|
||||
|
||||
This component must be used as a child of `<wa-radio-group>`. Please see the [Radio Group docs](/docs/components/radio-group) to see examples of this component in action.
|
||||
|
||||
@@ -109,13 +109,13 @@ Use the `disabled` attribute to disable a select.
|
||||
|
||||
### Multiple
|
||||
|
||||
To allow multiple options to be selected, use the `multiple` attribute. It's a good practice to use `with-clear` when this option is enabled. To set multiple values at once, set `value` to a space-delimited list of values.
|
||||
To allow multiple options to be selected, use the `multiple` attribute. It's a good practice to use `with-clear` when this option is enabled. You can select multiple options by adding the `selected` attribute to individual options.
|
||||
|
||||
```html {.example}
|
||||
<wa-select label="Select a Few" value="option-1 option-2 option-3" multiple with-clear>
|
||||
<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 label="Select a Few" multiple with-clear>
|
||||
<wa-option value="option-1" selected>Option 1</wa-option>
|
||||
<wa-option value="option-2" selected>Option 2</wa-option>
|
||||
<wa-option value="option-3" selected>Option 3</wa-option>
|
||||
<wa-option value="option-4">Option 4</wa-option>
|
||||
<wa-option value="option-5">Option 5</wa-option>
|
||||
<wa-option value="option-6">Option 6</wa-option>
|
||||
@@ -123,33 +123,37 @@ To allow multiple options to be selected, use the `multiple` attribute. It's a g
|
||||
```
|
||||
|
||||
:::info
|
||||
Note that multi-select options may wrap, causing the control to expand vertically. You can use the `max-options-visible` attribute to control the maximum number of selected options to show at once.
|
||||
Selecting multiple options may result in wrapping, causing the control to expand vertically. You can use the `max-options-visible` attribute to control the maximum number of selected options to show at once.
|
||||
:::
|
||||
|
||||
### Setting Initial Values
|
||||
|
||||
Use the `value` attribute to set the initial selection.
|
||||
Use the `selected` attribute on individual options to set the initial selection, similar to native HTML.
|
||||
|
||||
```html {.example}
|
||||
<wa-select value="option-1">
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
<wa-select>
|
||||
<wa-option value="option-1" selected>Option 1</wa-option>
|
||||
<wa-option value="option-2">Option 2</wa-option>
|
||||
<wa-option value="option-3">Option 3</wa-option>
|
||||
<wa-option value="option-4">Option 4</wa-option>
|
||||
</wa-select>
|
||||
```
|
||||
|
||||
When using `multiple`, the `value` _attribute_ uses space-delimited values to select more than one option. Because of this, `<wa-option>` values cannot contain spaces. If you're accessing the `value` _property_ through Javascript, it will be an array.
|
||||
For multiple selections, apply it to all selected options.
|
||||
|
||||
```html {.example}
|
||||
<wa-select value="option-1 option-2" multiple with-clear>
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
<wa-option value="option-2">Option 2</wa-option>
|
||||
<wa-select multiple with-clear>
|
||||
<wa-option value="option-1" selected>Option 1</wa-option>
|
||||
<wa-option value="option-2" selected>Option 2</wa-option>
|
||||
<wa-option value="option-3">Option 3</wa-option>
|
||||
<wa-option value="option-4">Option 4</wa-option>
|
||||
</wa-select>
|
||||
```
|
||||
|
||||
:::info
|
||||
Framework users can bind directly to the `value` property for reactive data binding and form state management.
|
||||
:::
|
||||
|
||||
### Grouping Options
|
||||
|
||||
Use `<wa-divider>` to group listbox items visually. You can also use `<small>` to provide labels, but they won't be announced by most assistive devices.
|
||||
@@ -208,54 +212,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>
|
||||
@@ -264,40 +244,34 @@ Use the `suffix` slot to append an icon to the control.
|
||||
|
||||
### Custom Tags
|
||||
|
||||
When multiple options can be selected, you can provide custom tags by passing a function to the `getTag` property. Your function can return a string of HTML, a <a href="https://lit.dev/docs/templates/overview/">Lit Template</a>, or an [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). The `getTag()` function will be called for each option. The first argument is an `<wa-option>` element and the second argument is the tag's index (its position in the tag list).
|
||||
When multiple options can be selected, you can provide custom tags by passing a function to the `getTag` property. Your function can return a string of HTML, a [Lit Template](https://lit.dev/docs/templates/overview/), or an [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). The `getTag()` function will be called for each option. The first argument is an `<wa-option>` element and the second argument is the tag's index (its position in the tag list).
|
||||
|
||||
Remember that custom tags are rendered in a shadow root. To style them, you can use the `style` attribute in your template or you can add your own [parts](/docs/customizing/#css-parts) and target them with the [`::part()`](https://developer.mozilla.org/en-US/docs/Web/CSS/::part) selector.
|
||||
|
||||
```html {.example}
|
||||
<wa-select
|
||||
placeholder="Select one"
|
||||
value="email phone"
|
||||
multiple
|
||||
with-clear
|
||||
class="custom-tag"
|
||||
>
|
||||
<wa-option value="email">
|
||||
<wa-icon slot="prefix" name="envelope" variant="solid"></wa-icon>
|
||||
<wa-select placeholder="Select one" multiple with-clear class="custom-tag">
|
||||
<wa-option value="email" selected>
|
||||
<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-option value="phone" selected>
|
||||
<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>
|
||||
|
||||
<script type="module">
|
||||
await customElements.whenDefined("wa-select")
|
||||
await customElements.whenDefined('wa-select');
|
||||
const select = document.querySelector('.custom-tag');
|
||||
await select.updateComplete
|
||||
await select.updateComplete;
|
||||
|
||||
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 `
|
||||
@@ -316,17 +290,15 @@ Be sure you trust the content you are outputting! Passing unsanitized user input
|
||||
|
||||
### Lazy loading options
|
||||
|
||||
Lazy loading options is very hard to get right. `<wa-select>` largely follows how a native `<select>` works.
|
||||
Lazy loading options works similarly to native `<select>` elements. The select component handles various scenarios intelligently:
|
||||
|
||||
Here are the following conditions:
|
||||
#### Basic lazy loading scenarios:
|
||||
|
||||
- If a `<wa-select>` is created without any options, but is given a `value` attribute, its `value` will be `""`, and then when options are added, if any of the options have a value equal to the `<wa-select>` value, the value of the `<wa-select>` will equal that of the option.
|
||||
- **Empty select with value**: If a `<wa-select>` is created without any options but given a `value` attribute, its value will be `""` initially. When options are added later, if any option has a value matching the select's value attribute, the select's value will update to match.
|
||||
|
||||
EX: `<wa-select value="foo">` will have a value of `""` until `<wa-option value="foo">Foo</wa-option>` connects, at which point its value will become `"foo"` when submitting.
|
||||
- **Multiple select with partial options**: If a `<wa-select multiple>` has an initial value with multiple options, but only some options are present in the DOM, it will respect only the available options. When additional selected options are loaded later (and the user hasn't changed the selection), those options will be automatically added to the selection.
|
||||
|
||||
- If a `<wa-select multiple>` with an initial value has multiple values, but only some of the options are present, it will only respect the options that are present, and if a selected option is loaded in later, *AND* the value of the select has not changed via user interaction or direct property assignment, it will add the selected option to the form value and to the `.value` of the select.
|
||||
|
||||
This can be hard to conceptualize, so heres a fairly large example showing how lazy loaded options work with `<wa-select>` and `<wa-select multiple>` when given initial value attributes. Feel free to play around with it in a codepen.
|
||||
Here's a comprehensive example showing different lazy loading scenarios:
|
||||
|
||||
```html {.example}
|
||||
<form id="lazy-options-example">
|
||||
@@ -335,93 +307,100 @@ This can be hard to conceptualize, so heres a fairly large example showing how l
|
||||
<wa-option value="bar">Bar</wa-option>
|
||||
<wa-option value="baz">Baz</wa-option>
|
||||
</wa-select>
|
||||
<br>
|
||||
<br />
|
||||
<wa-button type="button">Add "foo" option</wa-button>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<br />
|
||||
|
||||
<div>
|
||||
<wa-select name="select-2" value="foo" label="Single select (with no existing options)">
|
||||
</wa-select>
|
||||
<br>
|
||||
<wa-select name="select-2" value="foo" label="Single select (with no existing options)"> </wa-select>
|
||||
<br />
|
||||
<wa-button type="button">Add "foo" option</wa-button>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<br />
|
||||
|
||||
<div>
|
||||
<wa-select name="select-3" value="foo bar baz" multiple label="Multiple Select (with existing options)">
|
||||
<wa-option value="bar">Bar</wa-option>
|
||||
<wa-option value="baz">Baz</wa-option>
|
||||
<wa-select name="select-3" multiple label="Multiple Select (with existing selected options)">
|
||||
<wa-option value="bar" selected>Bar</wa-option>
|
||||
<wa-option value="baz" selected>Baz</wa-option>
|
||||
</wa-select>
|
||||
<br>
|
||||
<wa-button type="button">Add "foo" option</wa-button>
|
||||
<br />
|
||||
<wa-button type="button">Add "foo" option (selected)</wa-button>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<br />
|
||||
|
||||
<div>
|
||||
<wa-select name="select-4" value="foo" multiple label="Multiple Select (with no existing options)">
|
||||
</wa-select>
|
||||
<br>
|
||||
<wa-select name="select-4" value="foo" multiple label="Multiple Select (with no existing options)"> </wa-select>
|
||||
<br />
|
||||
<wa-button type="button">Add "foo" option</wa-button>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
<br /><br />
|
||||
|
||||
<div style="display: flex; gap: 16px;">
|
||||
<wa-button type="reset">Reset</wa-button>
|
||||
<wa-button type="submit" variant="brand">Show FormData</wa-button>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<br />
|
||||
|
||||
<pre hidden><code id="lazy-options-example-form-data"></code></pre>
|
||||
|
||||
<br>
|
||||
<br />
|
||||
</form>
|
||||
|
||||
<script type="module">
|
||||
function addFooOption(e) {
|
||||
const addFooButton = e.target.closest("wa-button[type='button']")
|
||||
const addFooButton = e.target.closest("wa-button[type='button']");
|
||||
if (!addFooButton) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const select = addFooButton.parentElement.querySelector("wa-select")
|
||||
const select = addFooButton.parentElement.querySelector('wa-select');
|
||||
|
||||
if (select.querySelector("wa-option[value='foo']")) {
|
||||
// Foo already exists. no-op.
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const option = document.createElement("wa-option")
|
||||
option.setAttribute("value", "foo")
|
||||
option.innerText = "Foo"
|
||||
select.append(option)
|
||||
const option = document.createElement('wa-option');
|
||||
option.setAttribute('value', 'foo');
|
||||
option.innerText = 'Foo';
|
||||
|
||||
// For the multiple select with existing selected options, make the new option selected
|
||||
if (select.getAttribute('name') === 'select-3') {
|
||||
option.selected = true;
|
||||
}
|
||||
|
||||
select.append(option);
|
||||
}
|
||||
|
||||
function handleLazySubmit (event) {
|
||||
event.preventDefault()
|
||||
function handleLazySubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target)
|
||||
const codeElement = document.querySelector("#lazy-options-example-form-data")
|
||||
const formData = new FormData(event.target);
|
||||
const codeElement = document.querySelector('#lazy-options-example-form-data');
|
||||
|
||||
const obj = {}
|
||||
const obj = {};
|
||||
for (const key of formData.keys()) {
|
||||
const val = formData.getAll(key).length > 1 ? formData.getAll(key) : formData.get(key)
|
||||
obj[key] = val
|
||||
const val = formData.getAll(key).length > 1 ? formData.getAll(key) : formData.get(key);
|
||||
obj[key] = val;
|
||||
}
|
||||
|
||||
codeElement.textContent = JSON.stringify(obj, null, 2)
|
||||
codeElement.textContent = JSON.stringify(obj, null, 2);
|
||||
|
||||
const preElement = codeElement.parentElement
|
||||
preElement.removeAttribute("hidden")
|
||||
const preElement = codeElement.parentElement;
|
||||
preElement.removeAttribute('hidden');
|
||||
}
|
||||
|
||||
const container = document.querySelector("#lazy-options-example")
|
||||
container.addEventListener("click", addFooOption)
|
||||
container.addEventListener("submit", handleLazySubmit)
|
||||
const container = document.querySelector('#lazy-options-example');
|
||||
container.addEventListener('click', addFooOption);
|
||||
container.addEventListener('submit', handleLazySubmit);
|
||||
</script>
|
||||
```
|
||||
|
||||
:::info
|
||||
The key principle is that the select component prioritizes user interactions and explicit selections over programmatic changes, ensuring a predictable user experience even with dynamically loaded content.
|
||||
:::
|
||||
|
||||
@@ -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>
|
||||
```
|
||||
@@ -6,20 +6,4 @@ parent: tab-group
|
||||
icon: tab-panel
|
||||
---
|
||||
|
||||
```html {.example}
|
||||
<wa-tab-group>
|
||||
<wa-tab panel="general">General</wa-tab>
|
||||
<wa-tab panel="custom">Custom</wa-tab>
|
||||
<wa-tab panel="advanced">Advanced</wa-tab>
|
||||
<wa-tab panel="disabled" disabled>Disabled</wa-tab>
|
||||
|
||||
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
|
||||
<wa-tab-panel name="custom">This is the custom tab panel.</wa-tab-panel>
|
||||
<wa-tab-panel name="advanced">This is the advanced tab panel.</wa-tab-panel>
|
||||
<wa-tab-panel name="disabled">This is a disabled tab panel.</wa-tab-panel>
|
||||
</wa-tab-group>
|
||||
```
|
||||
|
||||
:::info
|
||||
Additional demonstrations can be found in the [tab group examples](/docs/components/tab-group).
|
||||
:::
|
||||
This component must be used as a child of `<wa-tab-group>`. Please see the [Tab Group docs](/docs/components/tab-group) to see examples of this component in action.
|
||||
|
||||
@@ -6,6 +6,4 @@ parent: tab-group
|
||||
icon: tab
|
||||
---
|
||||
|
||||
:::info
|
||||
Additional demonstrations can be found in the [tab group examples](/docs/components/tab-group).
|
||||
:::
|
||||
This component must be used as a child of `<wa-tab-group>`. Please see the [Tab Group docs](/docs/components/tab-group) to see examples of this component in action.
|
||||
|
||||
@@ -5,78 +5,4 @@ tags: [navigation, disclosure, apps]
|
||||
icon: tree
|
||||
---
|
||||
|
||||
```html {.example}
|
||||
<wa-tree>
|
||||
<wa-tree-item>
|
||||
Item 1
|
||||
<wa-tree-item>Item A</wa-tree-item>
|
||||
<wa-tree-item>Item B</wa-tree-item>
|
||||
<wa-tree-item>Item C</wa-tree-item>
|
||||
</wa-tree-item>
|
||||
<wa-tree-item>Item 2</wa-tree-item>
|
||||
<wa-tree-item>Item 3</wa-tree-item>
|
||||
</wa-tree>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Nested tree items
|
||||
|
||||
A tree item can contain other tree items. This allows the node to be expanded or collapsed by the user.
|
||||
|
||||
```html {.example}
|
||||
<wa-tree>
|
||||
<wa-tree-item>
|
||||
Item 1
|
||||
<wa-tree-item>
|
||||
Item A
|
||||
<wa-tree-item>Item Z</wa-tree-item>
|
||||
<wa-tree-item>Item Y</wa-tree-item>
|
||||
<wa-tree-item>Item X</wa-tree-item>
|
||||
</wa-tree-item>
|
||||
<wa-tree-item>Item B</wa-tree-item>
|
||||
<wa-tree-item>Item C</wa-tree-item>
|
||||
</wa-tree-item>
|
||||
<wa-tree-item>Item 2</wa-tree-item>
|
||||
<wa-tree-item>Item 3</wa-tree-item>
|
||||
</wa-tree>
|
||||
```
|
||||
|
||||
### Selected
|
||||
|
||||
Use the `selected` attribute to select a tree item initially.
|
||||
|
||||
```html {.example}
|
||||
<wa-tree>
|
||||
<wa-tree-item selected>
|
||||
Item 1
|
||||
<wa-tree-item>Item A</wa-tree-item>
|
||||
<wa-tree-item>Item B</wa-tree-item>
|
||||
<wa-tree-item>Item C</wa-tree-item>
|
||||
</wa-tree-item>
|
||||
<wa-tree-item>Item 2</wa-tree-item>
|
||||
<wa-tree-item>Item 3</wa-tree-item>
|
||||
</wa-tree>
|
||||
```
|
||||
|
||||
### Expanded
|
||||
|
||||
Use the `expanded` attribute to expand a tree item initially.
|
||||
|
||||
```html {.example}
|
||||
<wa-tree>
|
||||
<wa-tree-item expanded>
|
||||
Item 1
|
||||
<wa-tree-item expanded>
|
||||
Item A
|
||||
<wa-tree-item>Item Z</wa-tree-item>
|
||||
<wa-tree-item>Item Y</wa-tree-item>
|
||||
<wa-tree-item>Item X</wa-tree-item>
|
||||
</wa-tree-item>
|
||||
<wa-tree-item>Item B</wa-tree-item>
|
||||
<wa-tree-item>Item C</wa-tree-item>
|
||||
</wa-tree-item>
|
||||
<wa-tree-item>Item 2</wa-tree-item>
|
||||
<wa-tree-item>Item 3</wa-tree-item>
|
||||
</wa-tree>
|
||||
```
|
||||
This component must be used as a child of `<wa-tree>`. Please see the [Tree docs](/docs/components/tree) to see examples of this component in action.
|
||||
|
||||
@@ -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" with-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" with-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" with-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" with-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>
|
||||
@@ -2319,25 +2319,23 @@ hasOutline: false
|
||||
<td style="text-align: center;"><wa-tag variant="warning" size="small">Pending</wa-tag></td>
|
||||
<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-button slot="trigger" with-caret size="small">Action</wa-button>
|
||||
<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-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>
|
||||
<wa-button slot="trigger" with-caret size="small">Action</wa-button>
|
||||
<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-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>
|
||||
<wa-button slot="trigger" with-caret size="small">Action</wa-button>
|
||||
<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-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>
|
||||
<wa-button slot="trigger" with-caret size="small">Action</wa-button>
|
||||
<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-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>
|
||||
<wa-button slot="trigger" with-caret size="small">Action</wa-button>
|
||||
<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
|
||||
@@ -22,7 +23,8 @@ During the alpha period, things might break! We take breaking changes very serio
|
||||
- 🚨 BREAKING: Renamed the `classic` theme to `shoelace`
|
||||
- 🚨 BREAKING: Renamed `pulse` attribute in `<wa-badge>` to `attention="pulse"` and added `attention="bounce"` [issue:#940]
|
||||
- 🚨 BREAKING: Renamed the `vertical` attribute to `orientation="vertical"` in `<wa-split-panel>` and `<wa-divider>` to align with other components and the platform [issue:674]
|
||||
- 🚨 BREAKING: Renamed certain boolean attributes to be more consistent using the `with-*` and `without-*` pattern:
|
||||
- 🚨 BREAKING: Renamed certain boolean attributes to be consistent using the `with-*` and `without-*` pattern:
|
||||
- `<wa-button caret>` => `<wa-button with-caret>`
|
||||
- `<wa-color-picker no-format-toggle>` => `<wa-color-picker without-format-toggle>`
|
||||
- `<wa-format-number no-grouping>` => `<wa-format-number without-grouping>`
|
||||
- `<wa-input no-spin-buttons>` => `<wa-input without-spin-buttons>`
|
||||
@@ -31,14 +33,34 @@ 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>`
|
||||
- 🚨 BREAKING: reworked `<wa-select>` to use `<wa-option selected>` to set initially selected options, removing the "no spaces allowed" restrictions for option values
|
||||
- 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]
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,20 +6,20 @@ import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import styles from './breadcrumb-item.css';
|
||||
|
||||
/**
|
||||
* @summary Breadcrumb Items are used inside [breadcrumbs](/docs/components/breadcrumb) to represent different links.
|
||||
* @summary Breadcrumb Items are used inside breadcrumbs to represent different links.
|
||||
* @documentation https://backers.webawesome.com/docs/components/breadcrumb-item
|
||||
* @status stable
|
||||
* @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);
|
||||
}
|
||||
|
||||
@@ -75,9 +75,9 @@ describe('<wa-button>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when caret', () => {
|
||||
describe('when with-caret', () => {
|
||||
it('should have a caret present', async () => {
|
||||
const el = await fixture<WaButton>(html` <wa-button caret>Button Label</wa-button> `);
|
||||
const el = await fixture<WaButton>(html` <wa-button with-caret>Button Label</wa-button> `);
|
||||
expect(el.shadowRoot!.querySelector('[part~="caret"]')).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -81,7 +81,7 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/** Draws the button with a caret. Used to indicate that the button triggers a dropdown menu or similar behavior. */
|
||||
@property({ type: Boolean, reflect: true }) caret = false;
|
||||
@property({ attribute: 'with-caret', type: Boolean, reflect: true }) withCaret = false;
|
||||
|
||||
/** Disables the button. Does not apply to link buttons. */
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
@@ -258,13 +258,13 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
|
||||
part="base"
|
||||
class=${classMap({
|
||||
button: true,
|
||||
caret: this.caret,
|
||||
caret: this.withCaret,
|
||||
disabled: this.disabled,
|
||||
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,11 +282,11 @@ 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
|
||||
this.withCaret
|
||||
? html`
|
||||
<wa-icon part="caret" class="caret" library="system" name="chevron-down" variant="solid"></wa-icon>
|
||||
`
|
||||
|
||||
@@ -4,7 +4,7 @@ import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import styles from './carousel-item.css';
|
||||
|
||||
/**
|
||||
* @summary A carousel item represent a slide within a [carousel](/docs/components/carousel).
|
||||
* @summary A carousel item represent a slide within a carousel.
|
||||
*
|
||||
* @since 2.0
|
||||
* @status experimental
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import '../icon/icon.js';
|
||||
import styles from './option.css';
|
||||
|
||||
/**
|
||||
* @summary Options define the selectable items within various form controls such as [select](/docs/components/select).
|
||||
* @summary Options define the selectable items within a select component.
|
||||
* @documentation https://backers.webawesome.com/docs/components/option
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
@@ -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"
|
||||
@@ -46,8 +46,6 @@ export default class WaOption extends WebAwesomeElement {
|
||||
// Set via the parent select
|
||||
@state() current = false;
|
||||
|
||||
@state() selected = false;
|
||||
|
||||
/**
|
||||
* The option's value. When selected, the containing form control will receive this value. The value must be unique
|
||||
* from other options in the same group. Values may not contain spaces, as spaces are used as delimiters when listing
|
||||
@@ -56,7 +54,13 @@ export default class WaOption extends WebAwesomeElement {
|
||||
@property({ reflect: true }) value = '';
|
||||
|
||||
/** Draws the option in a disabled state, preventing selection. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
|
||||
/** @internal */
|
||||
@property({ type: Boolean, attribute: false }) selected = false;
|
||||
|
||||
/** Selects an option initially. */
|
||||
@property({ type: Boolean, attribute: 'selected' }) defaultSelected = false;
|
||||
|
||||
_label: string = '';
|
||||
/**
|
||||
@@ -107,10 +111,6 @@ export default class WaOption extends WebAwesomeElement {
|
||||
|
||||
private handleDefaultSlotChange() {
|
||||
// Tell the controller to update the label
|
||||
if (customElements.get('wa-select')) {
|
||||
this.closest('wa-select')?.selectionChanged();
|
||||
}
|
||||
|
||||
this.updateDefaultLabel();
|
||||
|
||||
if (this.isInitialized) {
|
||||
@@ -119,6 +119,7 @@ export default class WaOption extends WebAwesomeElement {
|
||||
const controller = this.closest('wa-select');
|
||||
if (controller) {
|
||||
controller.handleDefaultSlotChange();
|
||||
controller.selectionChanged?.();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -136,6 +137,17 @@ export default class WaOption extends WebAwesomeElement {
|
||||
}
|
||||
};
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has('defaultSelected')) {
|
||||
if (!this.closest('wa-select')?.hasInteracted) {
|
||||
const oldVal = this.selected;
|
||||
this.selected = this.defaultSelected;
|
||||
this.requestUpdate('selected', oldVal);
|
||||
}
|
||||
}
|
||||
super.willUpdate(changedProperties);
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
@@ -146,6 +158,7 @@ export default class WaOption extends WebAwesomeElement {
|
||||
if (changedProperties.has('selected')) {
|
||||
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
|
||||
this.customStates.set('selected', this.selected);
|
||||
this.handleDefaultSlotChange();
|
||||
}
|
||||
|
||||
if (changedProperties.has('value')) {
|
||||
@@ -155,12 +168,6 @@ export default class WaOption extends WebAwesomeElement {
|
||||
this.value = String(this.value);
|
||||
}
|
||||
|
||||
if (this.value.includes(' ')) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Option values cannot include a space. All spaces have been replaced with underscores.`, this);
|
||||
this.value = this.value.replace(/ /g, '_');
|
||||
}
|
||||
|
||||
this.handleDefaultSlotChange();
|
||||
}
|
||||
|
||||
@@ -192,9 +199,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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { PropertyValues } from 'lit';
|
||||
import { html, isServer } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { uniqueId } from '../../internal/math.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
import { RequiredValidator } from '../../internal/validators/required-validator.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
|
||||
import formControlStyles from '../../styles/component/form-control.css';
|
||||
import sizeStyles from '../../styles/utilities/size.css';
|
||||
@@ -73,6 +73,9 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
/** The name of the radio group, submitted as a name/value pair with form data. */
|
||||
@property({ reflect: true }) name: string | null = null;
|
||||
|
||||
/** Disables the radio group and all child radios. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** The orientation in which to show radio items. */
|
||||
@property({ reflect: true }) orientation: 'horizontal' | 'vertical' = 'vertical';
|
||||
|
||||
@@ -141,6 +144,12 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
return radio;
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has('disabled') || changedProperties.has('value')) {
|
||||
this.syncRadioElements();
|
||||
}
|
||||
}
|
||||
|
||||
formResetCallback(...args: Parameters<WebAwesomeFormAssociatedElement['formResetCallback']>) {
|
||||
this.value = this.defaultValue;
|
||||
|
||||
@@ -152,7 +161,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
private handleRadioClick = (e: Event) => {
|
||||
const clickedRadio = (e.target as HTMLElement).closest<WaRadio>('wa-radio');
|
||||
|
||||
if (!clickedRadio || clickedRadio.disabled) {
|
||||
if (!clickedRadio || clickedRadio.disabled || (clickedRadio as any).forceDisabled || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -171,8 +180,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 }));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -197,6 +208,9 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
radio.toggleAttribute('data-wa-radio-first', index === 0);
|
||||
radio.toggleAttribute('data-wa-radio-inner', index !== 0 && index !== radios.length - 1);
|
||||
radio.toggleAttribute('data-wa-radio-last', index === radios.length - 1);
|
||||
|
||||
// Set forceDisabled state based on radio group's disabled state
|
||||
(radio as WaRadio).forceDisabled = this.disabled;
|
||||
});
|
||||
|
||||
// If at least one radio button exists, we assume it's a radio button group
|
||||
@@ -214,18 +228,42 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
}),
|
||||
);
|
||||
|
||||
if (radios.length > 0 && !radios.some(radio => radio.checked)) {
|
||||
radios[0].setAttribute('tabindex', '0');
|
||||
// Manage tabIndex based on disabled state and checked status
|
||||
if (this.disabled) {
|
||||
// If radio group is disabled, all radios should not be tabbable
|
||||
radios.forEach(radio => {
|
||||
radio.tabIndex = -1;
|
||||
});
|
||||
} else {
|
||||
// Normal tabbing behavior
|
||||
const enabledRadios = radios.filter(radio => !radio.disabled);
|
||||
const checkedRadio = enabledRadios.find(radio => radio.checked);
|
||||
|
||||
if (enabledRadios.length > 0) {
|
||||
if (checkedRadio) {
|
||||
// If there's a checked radio, it should be tabbable
|
||||
enabledRadios.forEach(radio => {
|
||||
radio.tabIndex = radio.checked ? 0 : -1;
|
||||
});
|
||||
} else {
|
||||
// If no radio is checked, first enabled radio should be tabbable
|
||||
enabledRadios.forEach((radio, index) => {
|
||||
radio.tabIndex = index === 0 ? 0 : -1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled radios should never be tabbable
|
||||
radios
|
||||
.filter(radio => radio.disabled)
|
||||
.forEach(radio => {
|
||||
radio.tabIndex = -1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@watch('value')
|
||||
handleValueChange() {
|
||||
this.syncRadioElements();
|
||||
}
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(event.key)) {
|
||||
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(event.key) || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -274,8 +312,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();
|
||||
@@ -283,6 +323,8 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
/** Sets focus on the radio group. */
|
||||
public focus(options?: FocusOptions) {
|
||||
if (this.disabled) return;
|
||||
|
||||
const radios = this.getAllRadios();
|
||||
const checked = radios.find(radio => radio.checked);
|
||||
const firstEnabledRadio = radios.find(radio => !radio.disabled);
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
}
|
||||
|
||||
/* Disabled */
|
||||
:host([disabled]) {
|
||||
:host(:state(disabled)) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -142,7 +142,7 @@
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
:host([appearance='button']:hover:not([disabled], :state(checked))) {
|
||||
:host([appearance='button']:hover:not(:state(disabled), :state(checked))) {
|
||||
background-color: color-mix(in srgb, var(--wa-color-surface-default) 95%, var(--wa-color-mix-hover));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
@state() checked = false;
|
||||
|
||||
/** @internal Used by radio group to force disable radios while preserving their original disabled state. */
|
||||
@state() forceDisabled = false;
|
||||
|
||||
/**
|
||||
* The string pointing to a form's id.
|
||||
*/
|
||||
@@ -79,7 +82,7 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
|
||||
private setInitialAttributes() {
|
||||
this.setAttribute('role', 'radio');
|
||||
this.tabIndex = 0;
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
this.setAttribute('aria-disabled', this.disabled || this.forceDisabled ? 'true' : 'false');
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
@@ -88,12 +91,24 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
|
||||
if (changedProperties.has('checked')) {
|
||||
this.customStates.set('checked', this.checked);
|
||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||
this.tabIndex = this.checked ? 0 : -1;
|
||||
// Only set tabIndex if not disabled
|
||||
if (!this.disabled && !this.forceDisabled) {
|
||||
this.tabIndex = this.checked ? 0 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has('disabled')) {
|
||||
this.customStates.set('disabled', this.disabled);
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
if (changedProperties.has('disabled') || changedProperties.has('forceDisabled')) {
|
||||
const effectivelyDisabled = this.disabled || this.forceDisabled;
|
||||
this.customStates.set('disabled', effectivelyDisabled);
|
||||
this.setAttribute('aria-disabled', effectivelyDisabled ? 'true' : 'false');
|
||||
|
||||
// Set tabIndex based on disabled state
|
||||
if (effectivelyDisabled) {
|
||||
this.tabIndex = -1;
|
||||
} else {
|
||||
// Restore proper tabIndex - this will be managed by the radio group
|
||||
this.tabIndex = this.checked ? 0 : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,8 +119,9 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
|
||||
// We override `setValue` because we don't want to set form values from here. We want to do that in "RadioGroup" itself.
|
||||
}
|
||||
|
||||
// Update the handleClick method (around line 75)
|
||||
private handleClick = () => {
|
||||
if (!this.disabled) {
|
||||
if (!this.disabled && !this.forceDisabled) {
|
||||
this.checked = true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ describe('<wa-select>', () => {
|
||||
it('should not throw on incomplete events', async () => {
|
||||
const el = await fixture<WaSelect>(html`
|
||||
<wa-select required>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
</wa-select>
|
||||
`);
|
||||
|
||||
@@ -416,10 +416,10 @@ describe('<wa-select>', () => {
|
||||
it('should serialize its name and value in FormData when multiple options are selected', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<wa-select name="a" value="option-2 option-3" multiple>
|
||||
<wa-select name="a" multiple>
|
||||
<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-option value="option-2" selected>Option 2</wa-option>
|
||||
<wa-option value="option-3" selected>Option 3</wa-option>
|
||||
</wa-select>
|
||||
</form>
|
||||
`);
|
||||
@@ -445,10 +445,10 @@ describe('<wa-select>', () => {
|
||||
it('should serialize its name and value in JSON when multiple options are selected', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<wa-select name="a" value="option-2 option-3" multiple>
|
||||
<wa-select name="a" multiple>
|
||||
<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-option value="option-2" selected>Option 2</wa-option>
|
||||
<wa-option value="option-3" selected>Option 3</wa-option>
|
||||
</wa-select>
|
||||
</form>
|
||||
`);
|
||||
@@ -576,10 +576,10 @@ describe('<wa-select>', () => {
|
||||
|
||||
it('should emit change and input when a tag is removed', async () => {
|
||||
const el = await fixture<WaSelect>(html`
|
||||
<wa-select value="option-1 option-2 option-3" multiple>
|
||||
<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 multiple>
|
||||
<wa-option value="option-1" selected>Option 1</wa-option>
|
||||
<wa-option value="option-2" selected>Option 2</wa-option>
|
||||
<wa-option value="option-3" selected>Option 3</wa-option>
|
||||
</wa-select>
|
||||
`);
|
||||
const changeHandler = sinon.spy();
|
||||
@@ -628,9 +628,9 @@ describe('<wa-select>', () => {
|
||||
|
||||
it('should have rounded tags when using the pill attribute', async () => {
|
||||
const el = await fixture<WaSelect>(html`
|
||||
<wa-select value="option-1 option-2" multiple pill>
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
<wa-option value="option-2">Option 2</wa-option>
|
||||
<wa-select multiple pill>
|
||||
<wa-option value="option-1" selected>Option 1</wa-option>
|
||||
<wa-option value="option-2" selected>Option 2</wa-option>
|
||||
<wa-option value="option-3">Option 3</wa-option>
|
||||
</wa-select>
|
||||
`);
|
||||
@@ -714,24 +714,17 @@ describe('<wa-select>', () => {
|
||||
it('Should not select the option if options already exists for multiple select', async () => {
|
||||
const form = await fixture<HTMLFormElement>(
|
||||
html` <form>
|
||||
<wa-select name="select" value="foo" multiple>
|
||||
<wa-select name="select" multiple>
|
||||
<wa-option value="bar">Bar</wa-option>
|
||||
<wa-option value="baz">Baz</wa-option>
|
||||
<wa-option value="foo" selected>Foo</wa-option>
|
||||
</wa-select>
|
||||
</form>`,
|
||||
);
|
||||
|
||||
const el = form.querySelector<WaSelect>('wa-select')!;
|
||||
expect(el.value).to.be.an('array');
|
||||
expect(el.value!.length).to.equal(0);
|
||||
|
||||
const option = document.createElement('wa-option');
|
||||
option.value = 'foo';
|
||||
option.innerText = 'Foo';
|
||||
el.append(option);
|
||||
|
||||
await aTimeout(10);
|
||||
await el.updateComplete;
|
||||
expect(el.value!.length).to.equal(1);
|
||||
expect(el.value).to.have.members(['foo']);
|
||||
expect(new FormData(form).getAll('select')).to.have.members(['foo']);
|
||||
});
|
||||
@@ -739,9 +732,9 @@ describe('<wa-select>', () => {
|
||||
it('Should only select the existing options if options already exists for multiple select', async () => {
|
||||
const form = await fixture<HTMLFormElement>(
|
||||
html` <form>
|
||||
<wa-select name="select" value="foo bar baz" multiple>
|
||||
<wa-option value="bar">Bar</wa-option>
|
||||
<wa-option value="baz">Baz</wa-option>
|
||||
<wa-select name="select" multiple>
|
||||
<wa-option value="bar" selected>Bar</wa-option>
|
||||
<wa-option value="baz" selected>Baz</wa-option>
|
||||
</wa-select>
|
||||
</form>`,
|
||||
);
|
||||
@@ -756,12 +749,13 @@ describe('<wa-select>', () => {
|
||||
const option = document.createElement('wa-option');
|
||||
option.value = 'foo';
|
||||
option.innerText = 'Foo';
|
||||
option.selected = true;
|
||||
el.append(option);
|
||||
|
||||
await aTimeout(10);
|
||||
await el.updateComplete;
|
||||
expect(el.value).to.have.members(['foo', 'bar', 'baz']);
|
||||
expect(new FormData(form).getAll('select')).to.have.members(['foo', 'bar', 'baz']);
|
||||
expect(el.value).to.have.members(['bar', 'baz', 'foo']);
|
||||
expect(new FormData(form).getAll('select')).to.have.members(['bar', 'baz', 'foo']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -796,6 +790,74 @@ describe('<wa-select>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('with selected attribute', () => {
|
||||
it('should select options using the selected attribute for single select', async () => {
|
||||
const el = await fixture<WaSelect>(html`
|
||||
<wa-select>
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
<wa-option value="option-2" selected>Option 2</wa-option>
|
||||
<wa-option value="option-3">Option 3</wa-option>
|
||||
</wa-select>
|
||||
`);
|
||||
|
||||
expect(el.value).to.equal('option-2');
|
||||
expect(el.displayInput.value).to.equal('Option 2');
|
||||
});
|
||||
|
||||
it('should select multiple options using the selected attribute', async () => {
|
||||
const el = await fixture<WaSelect>(html`
|
||||
<wa-select multiple>
|
||||
<wa-option value="option-1" selected>Option 1</wa-option>
|
||||
<wa-option value="option-2">Option 2</wa-option>
|
||||
<wa-option value="option-3" selected>Option 3</wa-option>
|
||||
</wa-select>
|
||||
`);
|
||||
|
||||
expect(el.value).to.have.members(['option-1', 'option-3']);
|
||||
expect(el.value).to.have.length(2);
|
||||
});
|
||||
|
||||
it('should handle options with spaces in values', async () => {
|
||||
const el = await fixture<WaSelect>(html`
|
||||
<wa-select>
|
||||
<wa-option value="option with spaces">Option with spaces</wa-option>
|
||||
<wa-option value="another option" selected>Another option</wa-option>
|
||||
</wa-select>
|
||||
`);
|
||||
|
||||
expect(el.value).to.equal('another option');
|
||||
expect(el.displayInput.value).to.equal('Another option');
|
||||
});
|
||||
|
||||
it('should handle multiple options with spaces in values', async () => {
|
||||
const el = await fixture<WaSelect>(html`
|
||||
<wa-select multiple>
|
||||
<wa-option value="option with spaces" selected>Option with spaces</wa-option>
|
||||
<wa-option value="another option">Another option</wa-option>
|
||||
<wa-option value="third option" selected>Third option</wa-option>
|
||||
</wa-select>
|
||||
`);
|
||||
|
||||
expect(el.value).to.have.members(['option with spaces', 'third option']);
|
||||
expect(el.value).to.have.length(2);
|
||||
});
|
||||
|
||||
it('should serialize options with spaces correctly in FormData', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<wa-select name="test" multiple>
|
||||
<wa-option value="option with spaces" selected>Option with spaces</wa-option>
|
||||
<wa-option value="another option" selected>Another option</wa-option>
|
||||
</wa-select>
|
||||
</form>
|
||||
`);
|
||||
|
||||
const formData = new FormData(form);
|
||||
const values = formData.getAll('test');
|
||||
expect(values).to.have.members(['option with spaces', 'another option']);
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/shoelace-style/webawesome-alpha/issues/263
|
||||
it('should allow interaction after being disabled and re-enabled', async () => {
|
||||
const el = await fixture<WaSelect>(html`
|
||||
|
||||
@@ -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.
|
||||
@@ -127,21 +127,13 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
private _defaultValue: string | string[] = '';
|
||||
|
||||
@property({
|
||||
attribute: 'value',
|
||||
reflect: true,
|
||||
converter: {
|
||||
fromAttribute: (value: string) => value.split(' '),
|
||||
toAttribute: (value: string | string[]) => (Array.isArray(value) ? value.join(' ') : value),
|
||||
},
|
||||
attribute: false,
|
||||
})
|
||||
set defaultValue(val: string | string[]) {
|
||||
this._defaultValue = this.convertDefaultValue(val);
|
||||
}
|
||||
|
||||
get defaultValue() {
|
||||
if (!this.hasUpdated) {
|
||||
this._defaultValue = this.convertDefaultValue(this._defaultValue);
|
||||
}
|
||||
return this._defaultValue;
|
||||
}
|
||||
|
||||
@@ -154,28 +146,32 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
const isMultiple = this.multiple || this.hasAttribute('multiple');
|
||||
|
||||
if (!isMultiple && Array.isArray(val)) {
|
||||
val = val.join(' ');
|
||||
val = val[0];
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
private _value: string[] | undefined;
|
||||
@property({ attribute: false })
|
||||
|
||||
/** The select's value. This will be a string for single select or an array for multi-select. */
|
||||
@property({ attribute: 'value', reflect: false })
|
||||
set value(val: string | string[]) {
|
||||
let oldValue = this.value;
|
||||
|
||||
if (!Array.isArray(val)) {
|
||||
val = val.split(' ');
|
||||
if ((val as any) instanceof FormData) {
|
||||
val = (val as unknown as FormData).getAll(this.name) as string[];
|
||||
}
|
||||
|
||||
if (!this._value || this._value.join(' ') !== val.join(' ')) {
|
||||
this._value = val;
|
||||
let newValue = this.value;
|
||||
if (!Array.isArray(val)) {
|
||||
val = [val];
|
||||
}
|
||||
|
||||
if (newValue !== oldValue) {
|
||||
this.requestUpdate('value', oldValue);
|
||||
}
|
||||
this._value = val;
|
||||
let newValue = this.value;
|
||||
|
||||
if (newValue !== oldValue) {
|
||||
this.requestUpdate('value', oldValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,6 +296,17 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
// Because this is a form control, it shouldn't be opened initially
|
||||
this.open = false;
|
||||
|
||||
if (!this._defaultValue) {
|
||||
const allOptions = this.getAllOptions();
|
||||
const selectedOptions = allOptions.filter(el => el.selected || el.defaultSelected);
|
||||
if (selectedOptions.length > 0) {
|
||||
const selectedValues = selectedOptions.map(el => el.value);
|
||||
this._defaultValue = this.multiple ? selectedValues : selectedValues[0];
|
||||
} else if (this.hasAttribute('value')) {
|
||||
this._defaultValue = this.getAttribute('value') || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addOpenListeners() {
|
||||
@@ -381,7 +388,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 +518,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 +549,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 }));
|
||||
});
|
||||
}
|
||||
@@ -563,10 +570,19 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
const allOptions = this.getAllOptions();
|
||||
this.optionValues = undefined; // dirty the value so it gets recalculated
|
||||
|
||||
// Update defaultValue if it hasn't been explicitly set and we have selected options
|
||||
if (!this._defaultValue && !this.hasUpdated) {
|
||||
const selectedOptions = allOptions.filter(el => el.selected || el.defaultSelected);
|
||||
if (selectedOptions.length > 0) {
|
||||
const selectedValues = selectedOptions.map(el => el.value);
|
||||
this._defaultValue = this.multiple ? selectedValues : selectedValues[0];
|
||||
}
|
||||
}
|
||||
|
||||
const value = this.value;
|
||||
|
||||
// Select only the options that match the new value
|
||||
this.setSelectedOptions(allOptions.filter(el => value.includes(el.value)));
|
||||
this.setSelectedOptions(allOptions.filter(el => value.includes(el.value) || el.selected));
|
||||
}
|
||||
|
||||
private handleTagRemove(event: WaRemoveEvent, directOption?: WaOption) {
|
||||
@@ -600,7 +616,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 }));
|
||||
});
|
||||
}
|
||||
@@ -645,7 +661,12 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
const newSelectedOptions = Array.isArray(option) ? option : [option];
|
||||
|
||||
// Clear existing selection
|
||||
allOptions.forEach(el => (el.selected = false));
|
||||
allOptions.forEach(el => {
|
||||
if (newSelectedOptions.includes(el)) {
|
||||
return;
|
||||
}
|
||||
el.selected = false;
|
||||
});
|
||||
|
||||
// Set the new selection
|
||||
if (newSelectedOptions.length) {
|
||||
@@ -673,7 +694,9 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
const options = this.getAllOptions();
|
||||
|
||||
// Update selected options cache
|
||||
this.selectedOptions = options.filter(el => el.selected);
|
||||
this.selectedOptions = options.filter(el => {
|
||||
return el.selected;
|
||||
});
|
||||
let selectedValues = new Set(this.selectedOptions.map(el => el.value));
|
||||
|
||||
// Toggle values present in the DOM from this.value, while preserving options NOT present in the DOM (for lazy loading)
|
||||
@@ -847,6 +870,11 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
this.value = this.defaultValue;
|
||||
super.formResetCallback();
|
||||
this.handleValueChange();
|
||||
|
||||
this.updateComplete.then(() => {
|
||||
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -900,7 +928,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 +990,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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user