Compare commits

..

1 Commits

Author SHA1 Message Date
Lea Verou
53997d5318 Display attribute and property in separate columns 2024-11-22 03:24:15 -05:00
120 changed files with 802 additions and 5930 deletions

View File

@@ -132,7 +132,7 @@ module.exports = {
'no-implicit-coercion': 'off',
'no-implicit-globals': 'error',
'no-implied-eval': 'error',
'no-invalid-this': 'off',
'no-invalid-this': 'error',
'no-labels': 'error',
'no-lone-blocks': 'error',
'no-new': 'error',

View File

@@ -1,39 +0,0 @@
# # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Client Tests
on:
push:
branches: [next]
pull_request:
branches: [next]
jobs:
client_test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run prettier && npm run lint
- name: Build
run: npm run build
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run CSR tests
# FAIL_FAST to fail on first failing test.
run: FAIL_FAST="true" CSR_ONLY="true" npm run test

View File

@@ -1,13 +1,42 @@
# # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: SSR Tests
name: Node.js CI
on:
push:
branches: [next]
pull_request:
branches: [next]
jobs:
client_test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run prettier && npm run lint
- name: Build
run: npm run build
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run CSR tests
# FAIL_FAST to fail on first failing test.
run: FAIL_FAST="true" CSR_ONLY="true" npm run test
ssr_test:
runs-on: ubuntu-latest

View File

@@ -1,7 +1,6 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"prettier.enable": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},

View File

@@ -45,7 +45,6 @@
"dogfood",
"dropdowns",
"easings",
"ecommerce",
"endraw",
"endregion",
"enterkeyhint",
@@ -84,8 +83,6 @@
"jsfiddle",
"keydown",
"keyframes",
"keymaker",
"Konnor",
"Kool",
"labelledby",
"Laravel",
@@ -111,7 +108,6 @@
"multiselectable",
"nextjs",
"nocheck",
"noindex",
"noopener",
"noreferrer",
"novalidate",
@@ -124,7 +120,6 @@
"peta",
"petabit",
"Preact",
"preconnect",
"prismjs",
"progressbar",
"radiogroup",
@@ -188,8 +183,7 @@
"webawesomer",
"WEBP",
"Webpacker",
"xmark",
"zoomable"
"xmark"
],
"ignorePaths": [
"package.json",

View File

@@ -3,37 +3,31 @@ import { markdown } from './_utils/markdown.js';
import { anchorHeadingsPlugin } from './_utils/anchor-headings.js';
import { codeExamplesPlugin } from './_utils/code-examples.js';
import { copyCodePlugin } from './_utils/copy-code.js';
import { removeDataAlphaElements } from './_utils/remove-data-alpha-elements.js';
import { currentLink } from './_utils/current-link.js';
import { highlightCodePlugin } from './_utils/highlight-code.js';
// import { formatCodePlugin } from './_utils/format-code.js';
import { formatCodePlugin } from './_utils/format-code.js';
import { replaceTextPlugin } from './_utils/replace-text.js';
import { searchPlugin } from './_utils/search.js';
import { readFile } from 'fs/promises';
import { outlinePlugin } from './_utils/outline.js';
import componentList from './_data/componentList.js';
import { getComponents } from './_utils/manifest.js';
import litPlugin from '@lit-labs/eleventy-plugin-lit';
import process from 'process';
const packageData = JSON.parse(await readFile('./package.json', 'utf-8'));
const isAlpha = process.argv.includes('--alpha');
// const isDeveloping = process.argv.includes('--develop');
const isDeveloping = process.argv.includes('--develop');
export default function (eleventyConfig) {
// NOTE - alpha setting removes certain pages
if (isAlpha) {
eleventyConfig.ignores.add('**/components/page.md');
eleventyConfig.ignores.add('**/experimental/**');
eleventyConfig.ignores.add('**/layout/**');
eleventyConfig.ignores.add('**/patterns/**');
eleventyConfig.ignores.add('**/style-utilities/**');
eleventyConfig.ignores.add('**/components/code-demo.md');
eleventyConfig.ignores.add('**/components/viewport-demo.md');
}
// Add template data
eleventyConfig.addGlobalData('package', packageData);
eleventyConfig.addGlobalData('isAlpha', isAlpha);
// Template filters - {{ content | filter }}
eleventyConfig.addFilter('inlineMarkdown', content => markdown.renderInline(content || ''));
@@ -45,7 +39,6 @@ export default function (eleventyConfig) {
// With Prettier 3, this means a leading pipe will exist be present when the line wraps.
return typeof content === 'string' ? content.replace(/^(\s|\|)/g, '').replace(/(\s|\|)$/g, '') : content;
});
eleventyConfig.addFilter('keys', obj => Object.keys(obj));
// Shortcodes - {% shortCode arg1, arg2 %}
eleventyConfig.addShortcode('cdnUrl', location => {
@@ -53,9 +46,16 @@ export default function (eleventyConfig) {
});
// Helpers
eleventyConfig.addNunjucksGlobal('getComponent', tagName => {
const component = getComponents().find(c => c.tagName === tagName);
// Remove elements that have [data-alpha="remove"]
eleventyConfig.addPlugin(removeDataAlphaElements({ isAlpha }));
if (!component) {
throw new Error(
`Unable to find "<${tagName}>". Make sure the file name is the same as the tag name (without prefix).`
);
}
return component;
});
// Use our own markdown instance
eleventyConfig.setLibrary('md', markdown);
@@ -79,7 +79,7 @@ export default function (eleventyConfig) {
eleventyConfig.addPlugin(currentLink());
// Add code examples for `<code class="example">` blocks
eleventyConfig.addPlugin(codeExamplesPlugin);
eleventyConfig.addPlugin(codeExamplesPlugin());
// Highlight code blocks with Prism
eleventyConfig.addPlugin(highlightCodePlugin());
@@ -115,7 +115,8 @@ export default function (eleventyConfig) {
// mutation-observer (why SSR this?)
// resize-observer (why SSR this?)
// tooltip (why SSR this?)
const componentModules = componentList
const componentModules = getComponents()
// .filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
.map(component => {
const name = component.tagName.split(/wa-/)[1];
@@ -137,16 +138,12 @@ export default function (eleventyConfig) {
);
// Production-only plugins
//
// TODO - disabled because it takes about a minute to run now
//
// if (!isDeveloping) {
// // Run Prettier on each file (prod only because it can be slow)
// eleventyConfig.addPlugin(formatCodePlugin());
// }
if (!isDeveloping) {
// Run Prettier on each file (prod only because it can be slow)
eleventyConfig.addPlugin(formatCodePlugin());
}
return {
markdownTemplateEngine: 'njk',
dir: {
includes: '_includes',
layouts: '_layouts'

View File

@@ -1,75 +0,0 @@
/**
* @module components Fetches components from custom-elements.json and exposes them in a saner format.
*/
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import { readFileSync } from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const manifest = JSON.parse(readFileSync(resolve(__dirname, '../../dist/custom-elements.json'), 'utf-8'));
const components = manifest.modules.flatMap(module => {
return module.declarations
.filter(c => c?.customElement)
.map(declaration => {
// Generate the dist path based on the src path and attach it to the component
declaration.path = module.path.replace(/^src\//, 'dist/').replace(/\.ts$/, '.js');
// Remove private members and those that lack a description
const members = declaration.members?.filter(member => member.description && member.privacy !== 'private');
const methods = members?.filter(prop => prop.kind === 'method' && prop.privacy !== 'private');
const attributes = declaration.attributes ?? [];
const properties = members?.filter(prop => {
// Look for a corresponding attribute
const attribute = attributes?.find(attr => attr.fieldName === prop.name);
if (attribute) {
prop.attribute = attribute.name || attribute.fieldName;
}
return prop.kind === 'field' && prop.privacy !== 'private';
});
return {
...declaration,
slug: declaration.tagName.replace(/^wa-/, ''),
methods,
attributes,
properties
};
});
});
// Build dependency graphs
components.forEach(component => {
const dependencies = [];
// Recursively fetch sub-dependencies
function getDependencies(tag) {
const cmp = components.find(c => c.tagName === tag);
if (!cmp || !Array.isArray(component.dependencies)) {
return;
}
cmp.dependencies?.forEach(dependentTag => {
if (!dependencies.includes(dependentTag)) {
dependencies.push(dependentTag);
}
getDependencies(dependentTag);
});
}
getDependencies(component.tagName);
component.dependencies = dependencies.sort();
});
// Sort by name
components.sort((a, b) => {
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
export default components;

View File

@@ -1,3 +0,0 @@
import componentList from './componentList.js';
export default Object.fromEntries(componentList.map(component => [component.slug, component]));

View File

@@ -1,43 +0,0 @@
import components from './components.js';
const by = {
attribute: {},
slot: {},
event: {},
method: {},
cssPart: {},
cssProperty: {}
};
function getAll(component, type) {
let prop = type + 's';
if (type === 'cssProperty') {
prop = 'cssProperties';
}
return component[prop] ?? [];
}
for (const componentName in components) {
const component = components[componentName];
for (const type of ['attribute', 'slot', 'event', 'method', 'cssPart', 'cssProperty']) {
for (const item of getAll(component, type)) {
by[type][item.name] ??= [];
by[type][item.name].push(component);
}
}
}
// Sort by descending number of components
const sortByLengthDesc = (a, b) => b[1].length - a[1].length;
for (const key in by) {
by[key] = sortObject(by[key], sortByLengthDesc);
}
export default by;
function sortObject(obj, sorter) {
return Object.fromEntries(Object.entries(obj).sort(sorter));
}

View File

@@ -17,7 +17,7 @@
<script src="/assets/scripts/hydration-errors.js"></script>
<link rel="stylesheet" href="/assets/styles/hydration-errors.css">
<link rel="preconnect" href="https://cdn.jsdelivr.net">
<script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.4/+esm"></script>
<script type="module" src="/assets/scripts/code-examples.js"></script>
<script type="module" src="/assets/scripts/color-scheme.js"></script>
@@ -31,7 +31,7 @@
{# Web Awesome #}
<script type="module" src="/dist/webawesome.ssr-loader.js"></script>
<link rel="stylesheet" id="theme-stylesheet" href="/dist/themes/default.css" />
<link rel="stylesheet" id="theme-stylesheet" />
<link rel="stylesheet" href="/dist/themes/applied.css" />
<link id="color-stylesheet" rel="stylesheet" href="/dist/themes/layout.css" />
<link id="color-stylesheet" rel="stylesheet" href="/dist/themes/utilities.css" />
@@ -58,24 +58,10 @@
return colorScheme === 'dark';
}
const oldStylesheet = document.querySelector("#theme-stylesheet")
const newStylesheet = document.createElement("link")
const stylesheet = document.getElementById("theme-stylesheet")
let preset = getPresetTheme()
newStylesheet.href = `/dist/themes/${preset}.css`
newStylesheet.rel = "preload"
newStylesheet.as = "style"
document.head.append(newStylesheet)
function updateStylesheet () {
newStylesheet.rel = "stylesheet"
newStylesheet.id = "theme-stylesheet"
requestAnimationFrame(() => oldStylesheet.remove())
}
newStylesheet.addEventListener("load", updateStylesheet)
stylesheet.href = `/dist/themes/${preset}.css`
document.documentElement.classList.toggle(
`wa-theme-${preset}-dark`,
@@ -85,15 +71,14 @@
</head>
<body class="layout-{{ layout | stripExtension }}">
<!-- use view="desktop" as default to reduce layout jank on desktop site. -->
<wa-page view="desktop" disable-navigation-toggle="">
<wa-page view="desktop">
<header slot="header">
{# Logo #}
<div id="docs-branding">
{# Nav toggle #}
<wa-button appearance="text" size="small" data-toggle-nav>
<wa-button appearance="text" data-toggle-nav>
<wa-icon name="bars" label="Toggle navigation"></wa-icon>
</wa-button>
<a href="/" aria-label="Web Awesome">
<span class="only-desktop">{% include "logo.njk" %}</span>
<span class="only-mobile">{% include "logo-simple.njk" %}</span>
@@ -103,11 +88,40 @@
</div>
<div id="docs-toolbar">
{# Desktop selectors #}
<div class="only-desktop">
{% include "preset-theme-selector.njk" %}
{% include "color-scheme-selector.njk" %}
</div>
{# Preset theme selector #}
<wa-dropdown id="preset-theme-selector">
<wa-button slot="trigger" appearance="tinted" size="small" pill caret>
<wa-icon slot="prefix" name="paintbrush" variant="regular"></wa-icon>
<span id="preset-theme-selector__text" class="only-desktop">Default</span>
</wa-button>
<wa-menu>
<wa-menu-item type="checkbox" value="default">Default</wa-menu-item>
<wa-menu-item type="checkbox" value="classic">Classic</wa-menu-item>
<wa-menu-item type="checkbox" value="fa">Font Awesome</wa-menu-item>
<wa-menu-item type="checkbox" value="active">Active</wa-menu-item>
<wa-menu-item type="checkbox" value="brutalist">Brutalism</wa-menu-item>
<wa-menu-item type="checkbox" value="glassy">Glassy</wa-menu-item>
<wa-menu-item type="checkbox" value="migration">Migration</wa-menu-item>
<wa-menu-item type="checkbox" value="playful">Playful</wa-menu-item>
<wa-menu-item type="checkbox" value="premium">Premium</wa-menu-item>
</wa-menu>
</wa-dropdown>
{# Color scheme selector #}
<wa-dropdown id="color-scheme-selector">
<wa-button slot="trigger" appearance="tinted" size="small" pill caret 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>
<span class="only-light only-desktop">Light</span>
<span class="only-dark only-desktop">Dark</span>
</wa-button>
<wa-menu>
<wa-menu-item type="checkbox" value="light">Light</wa-menu-item>
<wa-menu-item type="checkbox" value="dark">Dark</wa-menu-item>
<wa-divider></wa-divider>
<wa-menu-item type="checkbox" value="auto">System</wa-menu-item>
</wa-menu>
</wa-dropdown>
{# Search #}
<wa-button id="search-trigger" appearance="outlined" size="small" data-search>
@@ -120,16 +134,12 @@
{# Sidebar #}
{% if hasSidebar %}
{# Mobile selectors #}
<div class="only-mobile" slot="navigation-header">
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));">
{% include "preset-theme-selector.njk" %}
{% include "color-scheme-selector.njk" %}
</div>
</div>
<div slot="navigation" id="sidebar" class="docs-aside" data-remember-scroll>
{% include "sidebar.njk" %}
</div>
<aside slot="navigation" id="sidebar" class="docs-aside" data-remember-scroll>
<wa-icon-button id="sidebar-close-button" name="xmark" label="Close" data-drawer="close"></wa-icon-button>
<nav>
{% include "sidebar.njk" %}
</nav>
</aside>
{% endif %}
{# Outline #}

View File

@@ -1,15 +0,0 @@
{# Color scheme selector #}
<wa-dropdown id="color-scheme-selector">
<wa-button slot="trigger" appearance="tinted" size="small" pill caret 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>
<span class="only-light">Light</span>
<span class="only-dark">Dark</span>
</wa-button>
<wa-menu>
<wa-menu-item type="checkbox" value="light">Light</wa-menu-item>
<wa-menu-item type="checkbox" value="dark">Dark</wa-menu-item>
<wa-divider></wa-divider>
<wa-menu-item type="checkbox" value="auto">System</wa-menu-item>
</wa-menu>
</wa-dropdown>

View File

@@ -1,22 +0,0 @@
<div id="page_slots_demo">
<link rel="stylesheet" href="/assets/examples/page-demo/demo.css">
{% set slots = components.page.slots %}
<fieldset id="page_slots_fieldset">
<legend>Slots</legend>
<div class="options">
{% for slot in slots %}
{% if (slot.name != "skip-to-content") and (slot.name != "navigation-toggle-icon") %}
<wa-checkbox name="slot" value="{{ slot.name }}" {{ 'checked' if slot.name != "menu" and slot.name != 'navigation-toggle' | safe}} class="{{ 'default' if not slot.name }}"
data-description="{{ slot.description | inlineMarkdown }}" title="{{ slot.description | inlineMarkdown | striptags | safe }}">
{{ slot.name or "(default)" }}
</wa-checkbox>
{% endif %}
{% endfor %}
</div>
</fieldset>
<wa-viewport-demo viewport="1000">
<iframe srcdoc="" id="page_slots_iframe" data-turbo="false" data-turbo-temporary></iframe>
</wa-viewport-demo>
</div>
<script type=module src="/assets/examples/page-demo/demo.js"></script>

View File

@@ -1,18 +0,0 @@
{# Preset theme selector #}
<wa-dropdown id="preset-theme-selector">
<wa-button slot="trigger" appearance="tinted" size="small" pill caret>
<wa-icon slot="prefix" name="paintbrush" variant="regular"></wa-icon>
<span id="preset-theme-selector__text">Default</span>
</wa-button>
<wa-menu>
<wa-menu-item type="checkbox" value="default">Default</wa-menu-item>
<wa-menu-item type="checkbox" value="classic">Classic</wa-menu-item>
<wa-menu-item data-alpha="remove" type="checkbox" value="fa">Font Awesome</wa-menu-item>
<wa-menu-item data-alpha="remove" type="checkbox" value="active">Active</wa-menu-item>
<wa-menu-item data-alpha="remove" type="checkbox" value="brutalist">Brutalism</wa-menu-item>
<wa-menu-item data-alpha="remove" type="checkbox" value="glassy">Glassy</wa-menu-item>
<wa-menu-item data-alpha="remove" type="checkbox" value="migration">Migration</wa-menu-item>
<wa-menu-item data-alpha="remove" type="checkbox" value="playful">Playful</wa-menu-item>
<wa-menu-item data-alpha="remove" type="checkbox" value="premium">Premium</wa-menu-item>
</wa-menu>
</wa-dropdown>

View File

@@ -20,12 +20,11 @@
</ul>
{# Components #}
<h2>
<a href="/docs/components/" title="Overview">Components
<wa-icon name="grid-2"></wa-icon>
</a>
</h2>
<h2>Components</h2>
<ul>
<li>
<a href="/docs/components/">Components Overview</a>
</li>
<li>
<a href="/docs/components/animated-image">Animated Image</a>
</li>
@@ -69,11 +68,6 @@
<li>
<a href="/docs/components/checkbox">Checkbox</a>
</li>
{% if not isAlpha %}
<li>
<a href="/docs/components/code-demo">Code Demo</a>
</li>
{% endif %}
<li>
<a href="/docs/components/color-picker">Color Picker</a>
</li>
@@ -133,11 +127,6 @@
<li>
<a href="/docs/components/mutation-observer">Mutation Observer</a>
</li>
<li>
<a href="/docs/components/page">Page</a>
<wa-icon name="flask"></wa-icon>
<wa-badge class="pro">PRO</wa-badge>
</li>
<li>
<a href="/docs/components/popup">Popup</a>
</li>
@@ -221,24 +210,15 @@
</li>
</ul>
</li>
{% if not isAlpha %}
<li>
<a href="/docs/components/viewport-demo">Viewport Demo</a>
</li>
{% endif %}
<li>
<a href="/docs/components/visually-hidden">Visually Hidden</a>
</li>
</ul>
{# Layout #}
{% if not isAlpha %}
<h2>
<a href="/docs/layout" title="Overview">Layout
<wa-icon name="grid-2"></wa-icon>
</a>
</h2>
<h2>Layout</h2>
<ul>
<li><a href="/docs/layout">Layout Overview</a></li>
<li><a href="/docs/components/page">Page</a></li>
<li><a href="/docs/layout/cluster">Cluster</a></li>
<li><a href="/docs/layout/flank">Flank</a></li>
@@ -247,12 +227,13 @@
<li><a href="/docs/layout/split">Split</a></li>
<li><a href="/docs/layout/stack">Stack</a></li>
</ul>
{% endif %}
{# Patterns #}
{% if not isAlpha %}
<h2>Patterns</h2>
<ul>
<li>
<a href="/docs/patterns/">Pattern Overview</a>
</li>
<li><a href="/docs/patterns/app">Web App</a></li>
<li><a href="/docs/patterns/ecommerce">E-commerce</a>
<ul>
@@ -262,22 +243,20 @@
<li><a href="/docs/patterns/ecommerce-shopping-cart">Shopping Carts</a></li>
<li><a href="/docs/patterns/ecommerce-category-filter">Category Filters</a></li>
<li><a href="/docs/patterns/ecommerce-product-detail">Product Detail</a></li>
<li><a href="/docs/patterns/ecommerce-order-summary">Order Summaries</a></li>
<li><a href="/docs/patterns/ecommerce-order-summmary">Order Summaries</a></li>
<li><a href="/docs/patterns/ecommerce-order-history">Order History</a></li>
</ul>
</li>
<li><a href="/docs/patterns/blog">Blog</a></li>
<li><a href="/docs/patterns/news">News</a></li>
</ul>
{% endif %}
{# Theming #}
<h2>
<a href="/docs/theming" title="Overview">Theming
<wa-icon name="grid-2"></wa-icon>
</a>
</h2>
<h2>Theming</h2>
<ul>
<li>
<a href="/docs/theming/">Theming Overview</a>
</li>
<li>
<a href="/docs/theming/color">Color</a>
</li>
@@ -305,12 +284,10 @@
</ul>
{# Style Utilities #}
{% if not isAlpha %}
<h2>Style Utilities</h2>
<ul>
<li><a href="/docs/style-utilities/align-items">Align Items</a></li>
<li><a href="/docs/style-utilities/border-radius">Border Radius</a></li>
<li><a href="/docs/style-utilities/gap">Gap</a></li>
<li><a href="/docs/style-utilities/text">Text</a></li>
</ul>
{% endif %}
</ul>

View File

@@ -1,9 +0,0 @@
<svg width="96" height="80" viewBox="0 0 96 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="94" height="6" rx="3" fill="var(--wa-color-surface-default)" stroke="var(--wa-color-surface-border)" stroke-width="2"/>
<rect x="1" y="13" width="94" height="6" rx="3" fill="var(--wa-color-surface-default)" stroke="var(--wa-color-surface-border)" stroke-width="2"/>
<rect x="1" y="73" width="94" height="6" rx="3" fill="var(--wa-color-surface-default)" stroke="var(--wa-color-surface-border)" stroke-width="2"/>
<rect x="25" y="61" width="46" height="6" rx="3" fill="var(--wa-color-surface-default)" stroke="var(--wa-color-surface-border)" stroke-width="2"/>
<rect x="25" y="25" width="46" height="30" rx="5" fill="var(--wa-color-surface-default)" stroke="var(--wa-color-surface-border)" stroke-width="2"/>
<rect x="1" y="25" width="18" height="42" rx="5" fill="var(--wa-color-surface-default)" stroke="var(--wa-color-surface-border)" stroke-width="2"/>
<rect x="77" y="25" width="18" height="42" rx="5" fill="var(--wa-color-surface-default)" stroke="var(--wa-color-surface-border)" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,69 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ description }}">
{% if noindex %}<meta name="robots" content="noindex">{% endif %}
<title>{{ title }}</title>
<link rel="icon" href="/assets/images/webawesome-logo.svg" />
<link rel="apple-touch-icon" href="/assets/images/app-icon.png">
{# Scripts #}
{# Hydration stuff #}
<script src="/assets/scripts/hydration-errors.js"></script>
<link rel="stylesheet" href="/assets/styles/hydration-errors.css">
<link rel="preconnect" href="https://cdn.jsdelivr.net">
<script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm"></script>
{# Web Awesome #}
<script type="module" src="/dist/webawesome.ssr-loader.js"></script>
<link rel="stylesheet" id="theme-stylesheet" />
<link rel="stylesheet" href="/dist/themes/applied.css" />
<link id="color-stylesheet" rel="stylesheet" href="/dist/themes/layout.css" />
<link id="color-stylesheet" rel="stylesheet" href="/dist/themes/utilities.css" />
<link rel="stylesheet" href="/dist/themes/forms.css" />
{# Set the theme to prevent flashing #}
<script>
function getColorScheme() {
return localStorage.getItem('colorScheme') || 'auto';
}
function getPresetTheme () {
return localStorage.getItem('presetTheme') || 'default';
}
function isDark() {
const colorScheme = getColorScheme()
if (colorScheme === 'auto') {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return colorScheme === 'dark';
}
const stylesheet = document.getElementById("theme-stylesheet")
let preset = getPresetTheme()
stylesheet.href = `/dist/themes/${preset}.css`
document.documentElement.classList.toggle(
`wa-theme-${preset}-dark`,
isDark()
);
</script>
</head>
<body class="layout-{{ layout | stripExtension }}">
{% block beforeContent %}{% endblock %}
{% block content %}
{{ content | safe }}
{% endblock %}
{% block afterContent %}{% endblock %}
</body>
</html>

View File

@@ -1,6 +1,6 @@
{% set hasSidebar = true %}
{% set hasOutline = true %}
{% set component = components[page.fileSlug] %}
{% set component = getComponent('wa-' + page.fileSlug) %}
{% set description = component.summary %}
{% extends '../_includes/base.njk' %}
@@ -17,10 +17,6 @@
>
{{ component.status }}
</wa-badge>
{# TODO - add a pro flag for pro components #}
{% if component.tagName == 'wa-page' %}
<wa-badge class="pro">PRO</wa-badge>
{% endif %}
</div>
<p class="component-summary">
{{ component.summary | inlineMarkdown | safe }}
@@ -66,13 +62,14 @@
{# Properties #}
{% if component.properties.length %}
<h2>Attributes & Properties</h2>
<h2>Properties & Attributes</h2>
<div class="table-scroll">
<table class="component-table">
<thead>
<tr>
<th class="table-name">Name</th>
<th class="table-name">Property</th>
<th class="table-name">Attribute</th>
<th class="table-description">Description</th>
<th class="table-reflects">Reflects</th>
</tr>
@@ -81,9 +78,11 @@
{% for prop in component.properties %}
<tr>
<td class="table-name">
<code title="JS property">{{ prop.name }}</code><br>
<code>{{ prop.name }}</code><br>
</td>
<td class="table-name">
{% if prop.attribute %}
<div><small><code title="HTML attribute">{{ prop.attribute }}</code></small></div>
<code>{{ prop.attribute }}</code>
{% endif %}
</td>
<td class="table-description">
@@ -270,9 +269,9 @@
</p>
<wa-tab-group label="How would you like to import this component?">
<wa-tab panel="cdn">CDN</wa-tab>
<wa-tab panel="npm">npm</wa-tab>
<wa-tab panel="react">React</wa-tab>
<wa-tab slot="nav" panel="cdn">CDN</wa-tab>
<wa-tab slot="nav" panel="npm">npm</wa-tab>
<wa-tab slot="nav" panel="react">React</wa-tab>
<wa-tab-panel name="cdn">
<p>
To manually import this component from the CDN, use the following code.

View File

@@ -1,4 +1,4 @@
{% set hasSidebar = true %}
{% set hasOutline = false %}
{% set hasOutline = true %}
{% extends "../_includes/base.njk" %}

View File

@@ -1,4 +0,0 @@
{% set hasSidebar = true %}
{% set hasOutline = true %}
{% extends "../_includes/base.njk" %}

View File

@@ -39,26 +39,22 @@ export function anchorHeadingsPlugin(options = {}) {
// Look for headings
container.querySelectorAll(options.headingSelector).forEach(heading => {
const hasAnchor = heading.querySelector('a');
const existingId = heading.getAttribute('id');
const clone = parse(heading.outerHTML);
// Create a clone of the heading so we can remove [data-no-anchor] elements from the text content
clone.querySelectorAll('[data-no-anchor]').forEach(el => el.remove());
if (hasAnchor) {
return;
const slug = createId(clone.textContent ?? '') ?? uuid().slice(-12);
let id = slug;
let suffix = 0;
// Make sure the slug is unique in the document
while (doc.getElementById(id) !== null) {
id = `${slug}-${++suffix}`;
}
let id = existingId;
if (!id) {
const slug = createId(clone.textContent ?? '') ?? uuid().slice(-12);
id = slug;
let suffix = 1;
// Make sure the slug is unique in the document
while (doc.getElementById(id) !== null) {
id = `${slug}-${++suffix}`;
}
if (hasAnchor || !id) {
return;
}
// Create the anchor
@@ -71,9 +67,7 @@ export function anchorHeadingsPlugin(options = {}) {
anchor.querySelector('.wa-visually-hidden').textContent = options.anchorLabel;
// Update the heading
if (!existingId) {
heading.setAttribute('id', id);
}
heading.setAttribute('id', id);
heading.classList.add('anchor-heading');
heading.appendChild(anchor);
});

View File

@@ -1,190 +1,82 @@
import { parse } from 'node-html-parser';
import { v4 as uuid } from 'uuid';
const templates = {
old(pre, code, { open, buttons, edit }) {
const id = `code-example-${uuid().slice(-12)}`;
let preview = code.textContent;
// Run preview scripts as modules to prevent collisions
const root = parse(preview, { blockTextElements: { script: true } });
root.querySelectorAll('script').forEach(script => script.setAttribute('type', 'module'));
preview = root.toString();
return `
<div class="code-example ${open ? 'open' : ''}">
<div class="code-example-preview">
${preview}
</div>
<div class="code-example-source" id="${id}">
${pre.outerHTML}
</div>
${
buttons
? `
<div class="code-example-buttons">
<button
class="code-example-toggle"
type="button"
aria-expanded="${open ? 'true' : 'false'}"
aria-controls="${id}"
>
Code
<wa-icon name="chevron-down"></wa-icon>
</button>
${
edit
? `
<button class="code-example-pen" type="button">
<wa-icon name="pen-to-square"></wa-icon>
Edit
</button>
`
: ''
}
`
: ''
}
</div>
</div>
`;
},
new(pre, code, { open, first, attributes }) {
attributes = {
open,
include: `link[rel=stylesheet][href^='/dist/']`,
...attributes
};
const attributesString = Object.entries(attributes)
.map(([key, value]) => {
if (value === true) {
return key;
}
if (value === false || value === null) {
return '';
}
return `${key}="${value}"`;
})
.join(' ');
let includes = '';
if (first) {
includes = `
<template class="wa-code-demo-include-isolated">
<script src="/dist/webawesome.loader.js" type="module"></script>
</template>`;
}
let preview = '';
if (attributes.viewport === undefined) {
// Slot in pre-rendered preview
preview = `<div style="display:contents" slot="preview">${code.textContent}</div>`;
// Run preview scripts as modules to prevent collisions
const root = parse(preview, { blockTextElements: { script: true } });
root.querySelectorAll('script').forEach(script => script.setAttribute('type', 'module'));
preview = root.toString();
}
return `${includes}
<wa-code-demo ${attributesString}>
${preview}
${pre.outerHTML}
</wa-code-demo>
`;
}
};
/**
* Eleventy plugin to turn `<code class="example">` blocks into live examples.
*/
export function codeExamplesPlugin(eleventyConfig, options = {}) {
const defaultOptions = {
export function codeExamplesPlugin(options = {}) {
options = {
container: 'body',
defaultOpen: (code, { outputPathIndex }) => {
return (
outputPathIndex === 1 && // is first
code.textContent.length < 500
); // is short
}
};
options = { ...defaultOptions, ...options };
const stats = {
inputPaths: {},
outputPaths: {}
...options
};
eleventyConfig.addTransform('code-examples', function (content) {
const { inputPath, outputPath } = this.page;
return function (eleventyConfig) {
eleventyConfig.addTransform('code-examples', content => {
const doc = parse(content, { blockTextElements: { code: true } });
const container = doc.querySelector(options.container);
const doc = parse(content, { blockTextElements: { code: true } });
const container = doc.querySelector(options.container);
if (!container) {
return content;
}
// Look for external links
container.querySelectorAll('code.example').forEach(code => {
stats.inputPaths[inputPath] ??= 0;
stats.outputPaths[outputPath] ??= 0;
stats.inputPaths[inputPath]++;
stats.outputPaths[outputPath]++;
const pre = code.closest('pre');
const first = stats.inputPaths[inputPath] === 1;
const localOptions = {
...options,
first,
// Modifier defaults
edit: true,
buttons: true,
new: true, // comment this line to default back to the old demos
attributes: {}
};
for (const prop of ['new', 'open', 'buttons', 'edit']) {
if (code.classList.contains(prop)) {
localOptions[prop] = true;
} else if (code.classList.contains(`no-${prop}`)) {
localOptions[prop] = false;
}
if (!container) {
return content;
}
for (const attribute of ['viewport', 'include']) {
if (code.hasAttribute(attribute)) {
localOptions.attributes[attribute] = code.getAttribute(attribute);
code.removeAttribute(attribute);
}
}
// Look for external links
container.querySelectorAll('code.example').forEach(code => {
const pre = code.closest('pre');
const hasButtons = !code.classList.contains('no-buttons');
const isOpen = code.classList.contains('open') || !hasButtons;
const noEdit = code.classList.contains('no-edit');
const id = `code-example-${uuid().slice(-12)}`;
let preview = pre.textContent;
if (Object.keys(localOptions.attributes).length > 0) {
// attributes only work on the new syntax
localOptions.new = true;
}
// Run preview scripts as modules to prevent collisions
const root = parse(preview, { blockTextElements: { script: true } });
root.querySelectorAll('script').forEach(script => script.setAttribute('type', 'module'));
preview = root.toString();
if (localOptions.open === undefined) {
if (localOptions.defaultOpen === true) {
localOptions.open = localOptions.defaultOpen;
} else if (typeof localOptions.defaultOpen === 'function') {
localOptions.open = localOptions.defaultOpen(code, {
pre,
inputPathIndex: stats.inputPaths[inputPath],
outputPathIndex: stats.outputPaths[outputPath]
});
}
}
const codeExample = parse(`
<div class="code-example ${isOpen ? 'open' : ''}">
<div class="code-example-preview">
${preview}
</div>
<div class="code-example-source" id="${id}">
${pre.outerHTML}
</div>
${
hasButtons
? `
<div class="code-example-buttons">
<button
class="code-example-toggle"
type="button"
aria-expanded="${isOpen ? 'true' : 'false'}"
aria-controls="${id}"
>
Code
<wa-icon name="chevron-down"></wa-icon>
</button>
const template = localOptions.new ? 'new' : 'old';
const codeExample = parse(templates[template](pre, code, localOptions));
${
noEdit
? ''
: `
<button class="code-example-pen" type="button">
<wa-icon name="pen-to-square"></wa-icon>
Edit
</button>
`
}
pre.replaceWith(codeExample);
`
: ''
}
</div>
</div>
`);
pre.replaceWith(codeExample);
});
return doc.toString();
});
return doc.toString();
});
};
}

71
docs/_utils/manifest.js Normal file
View File

@@ -0,0 +1,71 @@
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import { readFileSync } from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const manifest = JSON.parse(readFileSync(resolve(__dirname, '../../dist/custom-elements.json'), 'utf-8'));
/**
* @returns Fetches components from custom-elements.json and returns them in more sane format.
*/
export function getComponents() {
const components = [];
manifest.modules?.forEach(module => {
module.declarations?.forEach(declaration => {
if (declaration.customElement) {
// Generate the dist path based on the src path and attach it to the component
declaration.path = module.path.replace(/^src\//, 'dist/').replace(/\.ts$/, '.js');
// Remove private members and those that lack a description
const members = declaration.members?.filter(member => member.description && member.privacy !== 'private');
const methods = members?.filter(prop => prop.kind === 'method' && prop.privacy !== 'private');
const properties = members?.filter(prop => {
// Look for a corresponding attribute
const attribute = declaration.attributes?.find(attr => attr.fieldName === prop.name);
if (attribute) {
prop.attribute = attribute.name || attribute.fieldName;
}
return prop.kind === 'field' && prop.privacy !== 'private';
});
components.push({
...declaration,
methods,
properties
});
}
});
});
// Build dependency graphs
components.forEach(component => {
const dependencies = [];
// Recursively fetch sub-dependencies
function getDependencies(tag) {
const cmp = components.find(c => c.tagName === tag);
if (!cmp || !Array.isArray(component.dependencies)) {
return;
}
cmp.dependencies?.forEach(dependentTag => {
if (!dependencies.includes(dependentTag)) {
dependencies.push(dependentTag);
}
getDependencies(dependentTag);
});
}
getDependencies(component.tagName);
component.dependencies = dependencies.sort();
});
// Sort by name
return components.sort((a, b) => {
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
}

View File

@@ -59,5 +59,5 @@ markdown.use(markdownItContainer, 'details', {
});
markdown.use(markdownItAttrs, {
allowedAttributes: []
allowedAttributes: ['id', 'class', 'data']
});

View File

@@ -1,23 +0,0 @@
import { parse } from 'node-html-parser';
/**
* Eleventy plugin to add remove elements with <div data-alpha="remove"> from the alpha build.
*/
export function removeDataAlphaElements(options = {}) {
options = {
isAlpha: false,
...options
};
return function (eleventyConfig) {
eleventyConfig.addTransform('remove-data-alpha-elements', content => {
const doc = parse(content, { blockTextElements: { code: true } });
if (options.isAlpha) {
doc.querySelectorAll('[data-alpha="remove"]').forEach(el => el.remove());
}
return doc.toString();
});
};
}

View File

@@ -1,23 +0,0 @@
#page_slots_demo {
display: flex;
flex-flow: column;
gap: 1em;
margin-bottom: var(--wa-space-xl);
fieldset .options {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(9em, 1fr));
gap: 0.2em 1em;
wa-checkbox {
white-space: nowrap;
}
p {
display: contents;
}
}
wa-viewport-demo {
}
}

View File

@@ -1,50 +0,0 @@
await customElements.whenDefined('wa-checkbox');
let container = document.getElementById('page_slots_demo');
let fieldset = container.querySelector('fieldset');
let iframe = container.querySelector('iframe');
let stylesheets = Array.from(document.querySelectorAll("link[rel=stylesheet][href^='/dist/']"))
.map(i => i.outerHTML)
.join('\n');
let includes = `${stylesheets}
<script src="/dist/webawesome.loader.js" type="module"></script>
<link rel="stylesheet" href="/assets/examples/page-demo/page.css">`;
function render() {
let slots = Array.from(fieldset.querySelectorAll('wa-checkbox[name=slot]:is([data-wa-checked])'));
let slotsHTML = slots
.map(slot => {
let name = slot.getAttribute('value');
let description = slot.getAttribute('data-description');
let tag = 'div';
if (name.endsWith('header')) {
tag = 'header';
}
if (name.endsWith('footer')) {
tag = 'footer';
}
return `<${tag} class="slot-content" slot="${name}">
<strong>${name || 'main <em>(default)</em>'}</strong>
<p>${description}</p>
</${tag}>`;
})
.join('\n');
let page = iframe.contentDocument?.querySelector('wa-page');
if (page) {
page.innerHTML = slotsHTML;
} else {
iframe.srcdoc = `${includes}<wa-page>${slotsHTML}</wa-page>`;
}
}
fieldset?.addEventListener('input', render);
render();
//
// TODO - fix Turbo caching. When this is removed, visiting the <wa-page> docs via Turbo will cause the <iframe srcdoc>
// to not render. Even with this, there are console errors when leaving the page.
//
// NOTE - the iframe already has `data-turbo="false"` and `data-turbo-temporary` on it.
//
document.body.setAttribute('data-turbo', 'false');

View File

@@ -1,66 +0,0 @@
body {
padding: 0;
margin: 0;
}
wa-page {
margin: var(--wa-space-xs);
margin-inline-start: 0;
&::part(base),
&::part(main),
&::part(navigation),
&::part(body) {
gap: var(--wa-space-xs);
}
}
:is([slot='banner'], [slot='header'], [slot='subheader'], [slot='footer'], [slot*='navigation']) {
margin-inline-start: var(--wa-space-xs);
}
.slot-content[slot='banner'],
.slot-content[slot='header'],
.slot-content[slot='subheader'] {
outline: 2px solid var(--wa-color-surface-default);
}
.slot-content {
padding: var(--wa-space-m);
border-radius: var(--wa-border-radius-s);
align-content: center;
justify-content: center;
text-align: center;
height: 100%;
box-sizing: border-box;
background: var(--wa-color-blue-80);
color: var(--wa-color-blue-20);
&[slot='banner'] {
background: var(--wa-color-blue-50);
color: white;
}
&[slot='header'] {
background: var(--wa-color-blue-60);
color: var(--wa-color-blue-10);
}
&[slot^='main'],
&[slot=''] {
background: var(--wa-color-gray-80);
color: var(--wa-color-gray-20);
}
&[slot^='navigation'] {
background: var(--wa-color-violet-80);
color: var(--wa-color-violet-20);
}
strong {
display: block;
}
&:not([slot='']) p {
display: none;
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -1,3 +1,3 @@
<svg width="20" height="15" viewBox="0 0 20 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.63 1.625C11.63 2.27911 11.2435 2.84296 10.6865 3.10064L14 6L17.2622 5.34755C17.0968 5.10642 17 4.81452 17 4.5C17 3.67157 17.6716 3 18.5 3C19.3284 3 20 3.67157 20 4.5C20 5.31157 19.3555 5.9726 18.5504 5.99917L15.0307 13.8207C14.7077 14.5384 13.9939 15 13.2068 15H6.79317C6.00615 15 5.29229 14.5384 4.96933 13.8207L1.44963 5.99917C0.64452 5.9726 0 5.31157 0 4.5C0 3.67157 0.671573 3 1.5 3C2.32843 3 3 3.67157 3 4.5C3 4.81452 2.9032 5.10642 2.73777 5.34755L6 6L9.31702 3.09761C8.76346 2.83855 8.38 2.27656 8.38 1.625C8.38 0.727537 9.10754 0 10.005 0C10.9025 0 11.63 0.727537 11.63 1.625Z" fill="var(--wa-brand-orange, #f36944)"/>
</svg>
<svg viewBox="0 0 20 16" xmlns="http://www.w3.org/2000/svg" fill="#f68a4c">
<path d="M11.63 1.625C11.63 2.27911 11.2435 2.84296 10.6865 3.10064L14 6L17.2622 5.34755C17.0968 5.10642 17 4.81452 17 4.5C17 3.67157 17.6716 3 18.5 3C19.3284 3 20 3.67157 20 4.5C20 5.31157 19.3555 5.9726 18.5504 5.99917L15.0307 13.8207C14.7077 14.5384 13.9939 15 13.2068 15H6.79317C6.00615 15 5.29229 14.5384 4.96933 13.8207L1.44963 5.99917C0.64452 5.9726 0 5.31157 0 4.5C0 3.67157 0.671573 3 1.5 3C2.32843 3 3 3.67157 3 4.5C3 4.81452 2.9032 5.10642 2.73777 5.34755L6 6L9.31702 3.09761C8.76346 2.83855 8.38 2.27656 8.38 1.625C8.38 0.727537 9.10754 0 10.005 0C10.9025 0 11.63 0.727537 11.63 1.625Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 742 B

After

Width:  |  Height:  |  Size: 684 B

View File

@@ -1,76 +0,0 @@
let url = new URL(location);
const pushedURL = false;
const matchers = {
default(textContent, query) {
return textContent.includes(query);
},
i(textContent, query) {
return textContent.toLowerCase().includes(query.toLowerCase());
},
regexp(textContent, query) {
query.lastIndex = 0;
return query.test(textContent);
}
};
matchers.iregexp = matchers.regexp; // i is baked into the query
function filterByName(value) {
const previousFilter = url.searchParams.get('name') || '';
url = new URL(location);
if (value) {
const isRegexp = name_search_regexp.checked;
const i = !name_search_i.checked;
const query = isRegexp ? new RegExp(value, 'gmsv' + (i ? 'i' : '')) : value;
const matcherId = (i ? 'i' : '') + (isRegexp ? 'regexp' : '');
const matcher = matchers[matcherId] ?? matchers.default;
for (const th of document.querySelectorAll('table tbody th:first-child')) {
const tr = th.parentNode;
const matches = matcher(th.textContent, query);
tr.toggleAttribute('hidden', !matches);
}
url.searchParams.set('name', value);
if (matcherId) {
url.searchParams.set('match', matcherId);
} else {
url.searchParams.delete('match');
}
} else {
for (const tr of document.querySelectorAll('table tbody tr[hidden]')) {
tr.removeAttribute('hidden');
}
url.searchParams.delete('name');
url.searchParams.delete('match');
}
if (value !== previousFilter) {
history[pushedURL ? 'replaceState' : 'pushState'](null, '', url);
}
// Update heading counts
for (const h2 of document.querySelectorAll('h2:has(+ table)')) {
const count = h2.querySelector('.count');
if (!count) continue;
const table = h2.nextElementSibling;
const visibleRows = table.querySelectorAll('tbody tr:not([hidden])').length;
count.textContent = visibleRows;
const outlineLink = document.querySelector(`#outline-standard a[href="#${h2.id}"]`);
if (outlineLink) {
// Why not just = h2.textContent? To skip the "Jump to heading" link
outlineLink.textContent = '';
outlineLink.append(...[...h2.childNodes].slice(0, 3).map(n => n.cloneNode(true)));
}
}
}
if (name_search.value) {
filterByName(name_search.value);
}
name_search_group.addEventListener('wa-input', e => filterByName(name_search.value));

View File

@@ -18,12 +18,10 @@ document.addEventListener('click', event => {
const cdnUrl = document.documentElement.dataset.cdnUrl;
const html =
`<script type="module" src="${cdnUrl}webawesome.loader.js"></script>\n` +
`<link rel="stylesheet" href="${cdnUrl}themes/default.css">\n` +
`<link rel="stylesheet" href="${cdnUrl}themes/applied.css">\n` +
`<link rel="stylesheet" href="${cdnUrl}themes/layout.css">\n` +
`<link rel="stylesheet" href="${cdnUrl}themes/utilities.css">\n\n` +
`<link rel="stylesheet" href="${cdnUrl}themes/default.css">\n\n` +
`<link rel="stylesheet" href="${cdnUrl}themes/applied.css">\n\n` +
`${code.textContent}`;
const css = 'html > body {\n font: 16px sans-serif;\n padding: 2rem;\n}';
const css = 'body {\n font: 16px sans-serif;\n padding: 2rem;\n}';
const js = '';
const form = document.createElement('form');

View File

@@ -25,19 +25,12 @@ wa-page::part(header) {
border-bottom: var(--wa-border-style) var(--wa-panel-border-width) var(--wa-color-neutral-border-quiet);
}
wa-page::part(drawer__body) {
padding-top: 0px;
}
wa-page::part(drawer__header-actions) {
align-self: center;
}
wa-page > header {
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: space-between;
height: 4rem;
padding-inline: var(--wa-space-xl);
a[href='/'] {
@@ -47,7 +40,6 @@ wa-page > header {
wa-button[data-toggle-nav] {
--label-color: currentColor;
font-size: 1rem;
margin-inline-start: -0.875rem;
margin-inline-end: 0;
@@ -95,10 +87,10 @@ wa-page > header {
#outline {
h2 {
font-size: var(--wa-font-size-m);
margin: 0;
margin-block-end: var(--wa-space-m);
}
h2:not(:first-child) {
margin-block-start: var(--wa-space-xs);
h2:not(:first-of-type) {
margin-block-start: var(--wa-space-xl);
}
ul {
border-inline-start: var(--wa-border-width-s) solid var(--wa-color-surface-border);
@@ -112,9 +104,7 @@ wa-page > header {
}
li {
list-style: none;
}
li + li {
margin-block-start: var(--wa-space-m);
margin-block-end: var(--wa-space-m);
}
li a {
color: var(--wa-color-text-normal);
@@ -123,59 +113,6 @@ wa-page > header {
li a:hover {
text-decoration: underline;
}
li wa-badge::part(base) {
font-size: var(--wa-font-size-2xs);
font-weight: var(--wa-font-weight-bold);
}
li wa-icon {
color: var(--wa-color-text-quiet);
vertical-align: middle;
&[name='flask'] {
margin-inline: 0.125em;
}
}
}
#outline-standard h2 {
margin-block-end: var(--wa-space-m);
}
/* Pro badges */
wa-badge.pro::part(base) {
background-color: var(--wa-brand-orange);
border-color: var(--wa-brand-orange);
}
#sidebar {
h2 {
color: var(--wa-color-text-quiet);
a {
display: flex;
align-items: center;
gap: 0.4em;
color: var(--wa-color-text-normal);
text-decoration: none;
wa-icon {
margin-block-end: -0.15em;
font-size: var(--wa-font-size-s);
font-weight: var(--wa-font-weight-action);
color: var(--wa-color-gray-70);
}
&:hover {
color: var(--wa-color-brand-on-normal);
wa-icon {
color: var(--wa-color-brand-on-quiet);
}
}
}
}
}
#sidebar-close-button {
@@ -206,14 +143,6 @@ wa-page > main {
margin-inline: auto;
}
h1.title wa-badge {
vertical-align: middle;
&::part(base) {
font-size: 1.5rem;
}
}
.component-info {
margin-block-end: var(--wa-flow-spacing);
}

View File

@@ -64,9 +64,3 @@ Icons are optional. Simply omit the `icon` slot if you don't want them.
```html {.example}
<wa-callout variant="brand"> Nothing fancy here, just a simple callout. </wa-callout>
```
### Styling
You can customize the callout's appearance mostly by setting regular CSS properties:
- `background`, `border`, `border-radius`, `color`, `padding`, `margin`, etc. work as expected
- `gap` sets the space between the icon and the content

View File

@@ -1,82 +0,0 @@
---
title: Component Cheatsheet
layout: docs
---
<style>
table code {
white-space: nowrap;
}
</style>
<p>
This page lists every bit of syntax used by every Web Awesome component and which components share it.
For these times when your memory is failing, or to simply explore the possibilities!
</p>
<fieldset id="name_search_group">
<legend>Filter by name</legend>
<wa-input type=search clearable id="name_search"></wa-input>
<wa-checkbox id="name_search_i" checked>Case sensitive</wa-checkbox>
<wa-checkbox id="name_search_regexp">Regular expression</wa-checkbox>
</fieldset>
<script>
{
let url = new URL(location);
if (url.searchParams.get("name")) {
name_search.value = url.searchParams.get("name");
}
if (url.searchParams.get("match")) {
let matcherId = url.searchParams.get("match");
let caseSensitive = !matcherId.startsWith("i");
let isRegexp = matcherId.endsWith("regexp");
customElements.whenDefined("wa-checkbox").then(async () => {
await Promise.all([
name_search_i.updateComplete,
name_search_regexp.updateComplete,
]);
name_search_i.checked = caseSensitive;
name_search_regexp.checked = isRegexp;
});
}
}
</script>
<script type="module" src="/assets/scripts/cheatsheet.js"></script>
{% for type, all in componentsBy -%}
{% set typeTitle = "CSS custom properties" if type == "cssProperty" else ("CSS parts" if type == "cssPart" else (type | title) + "s") %}
<h2 id="{{ typeTitle | slugify }}">
All <span class="count">{{ (all | keys).length }}</span>
{{ typeTitle }}
</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Components</th>
</tr>
</thead>
{% for name, thingComponents in all -%}
<tr>
<th><code>{{ name }}{{ "()" if type == "method" }}</code></th>
<td>
{% if thingComponents.length > 1 %}
<details open>
<summary><strong>{{ thingComponents.length }}</strong> components</summary>
{% endif %}
{% for component in thingComponents %}
<a href="../{{ component.slug }}"><code>&lt;{{ component.tagName }}&gt;</code></a>
{%- endfor -%}
{% if thingComponents.length > 1 %}
</details>
{% endif %}
</td>
</tr>
{%- endfor %}
</table>
{%- endfor %}

View File

@@ -1,211 +0,0 @@
---
title: Code Demo
description: Code demos can be used to render code examples as inline live demos.
layout: component
---
```html {.example}
<wa-code-demo>
<pre><code class="language-html">
&lt;button&gt;Click me!&lt;/button&gt;
&lt;wa-button&gt;Click me!&lt;/wa-button&gt;
</code></pre>
</wa-code-demo>
```
This component is used right here in the docs to render code examples.
:::warning
Do not render untrusted content in a `<wa-code-demo>` element. This component renders the content as HTML, which introduces XSS vulnerabilities if used with untrusted content.
:::
## Examples
### Open by default
```html {.example}
<wa-code-demo open>
<pre><code class="language-html">
&lt;button&gt;Click me!&lt;/button&gt;
&lt;wa-button&gt;Click me!&lt;/wa-button&gt;
</code></pre>
</wa-code-demo>
```
### Custom previews
In some cases you may want to preprocess the code displayed, for example to sanitize HTML, remove irrelevant elements or attributes, fix whitespace, or do server-side rendering (SSR).
For these cases, you can slot in a custom preview:
```html {.example}
<wa-code-demo>
<wa-button slot="preview">Click me!</wa-button>
<pre><code class="language-html">
&lt;button&gt;Click me!&lt;/button&gt;
</code></pre>
</wa-code-demo>
```
Note that this means the preview will be in the light DOM, and can conflict with other things on the page.
To only render the custom preview within the components shadow DOM, or to display raw text, you can wrap it in a `<template>` element:
```html {.example}
<wa-code-demo>
<template slot="preview">
<wa-button>Click me!</wa-button>
</template>
<pre><code class="language-html">
&lt;button&gt;Click me!&lt;/button&gt;
</code></pre>
</wa-code-demo>
```
### Including resources (CSS, scripts, etc.)
Demos are rendered in the shadow DOM of the component, so any resources (stylesheets, scripts, etc.) must be included anew.
The same applies to isolated demos (see below), opening demos in a new tab, or editing them on CodePen.
While you _could_ manually include all of these on every single demo, it would get tedious to write,
and it would add noise for the reader.
Instead, `<wa-code-demo>` provides several better ways to include resources.
The core idea is that rather than specifying these resources over and over on each demo,
you would **point to elements** which would then be cloned into the demo, at the beginning.
There are two ways to point to elements:
- Add a `wa-code-demo-include` class to them
- Specify a CSS selector for which resources to look for in the demos `include` attribute.
There are certain types of elements that are handled specially:
- `<template>`: contents are cloned instead of the element itself.
This is useful for including resources in your demo that you don't want rendered outside the demo.
The following example shows both methods.
It includes all stylesheets on this page whose URLs start with `/dist/themes/`,
plus any other elements with the class `.demo-import`, plus a CSS file with the class `wa-code-demo-include`:
```html {.example}
<template class="wa-code-demo-include-isolated">
<script type="module" src="/dist/webawesome.loader.js"></script>
<style>wa-callout { font-size: var(--wa-font-size-2xl) }</style>
<script>console.log('Hello!')</script>
</template>
<wa-code-demo include="link[rel=stylesheet]">
<pre><code class="language-html">
&lt;wa-callout&gt;Helloooo!&lt;/wa-callout&gt;
</code></pre>
</wa-code-demo>
```
### Isolated viewports
Often you may want to render your demo in a separate viewport, e.g. when its about a whole page.
Or, you may want to sandbox it.
For these cases, you can use the `viewport` attribute, which renders the demo in an iframe:
```html {.example}
<wa-code-demo viewport>
<pre><code class="language-html">
&lt;button&gt;Click me!&lt;/button&gt;
</code></pre>
</wa-code-demo>
```
### Viewport Emulation
When you use the `viewport` attribute, `<wa-code-demo>` uses [`<wa-viewport-demo>`](../viewport-demo/) internally, and passes the value of `viewport` to it.
This allows you to also also provide a width value to emulate and it will be scaled accordingly:
```html {.example}
<wa-code-demo viewport="300">
<pre><code class="language-html">
&lt;button&gt;Click me!&lt;/button&gt;
</code></pre>
</wa-code-demo>
```
Or both a width and a height value:
```html {.example}
<wa-code-demo viewport="1600 x 1000">
<pre><code class="language-html">
&lt;button&gt;Click me!&lt;/button&gt;
&lt;p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed maximus et tortor vel ullamcorper. Fusce tristique et justo quis auctor. In tristique dignissim dignissim. Fusce lacus urna, efficitur vel fringilla sed, hendrerit at ipsum. Donec suscipit ante ac ligula imperdiet varius. Aliquam ullamcorper augue sit amet lectus euismod finibus. Proin semper, diam at rhoncus posuere, diam dui semper turpis, ut faucibus mi ipsum nec ante. Morbi varius nibh ut facilisis varius. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce in blandit velit. Aliquam massa eros, commodo eu vestibulum a, faucibus non risus.
&lt;p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed maximus et tortor vel ullamcorper. Fusce tristique et justo quis auctor. In tristique dignissim dignissim. Fusce lacus urna, efficitur vel fringilla sed, hendrerit at ipsum. Donec suscipit ante ac ligula imperdiet varius. Aliquam ullamcorper augue sit amet lectus euismod finibus. Proin semper, diam at rhoncus posuere, diam dui semper turpis, ut faucibus mi ipsum nec ante. Morbi varius nibh ut facilisis varius. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce in blandit velit. Aliquam massa eros, commodo eu vestibulum a, faucibus non risus.
&lt;p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed maximus et tortor vel ullamcorper. Fusce tristique et justo quis auctor. In tristique dignissim dignissim. Fusce lacus urna, efficitur vel fringilla sed, hendrerit at ipsum. Donec suscipit ante ac ligula imperdiet varius. Aliquam ullamcorper augue sit amet lectus euismod finibus. Proin semper, diam at rhoncus posuere, diam dui semper turpis, ut faucibus mi ipsum nec ante. Morbi varius nibh ut facilisis varius. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce in blandit velit. Aliquam massa eros, commodo eu vestibulum a, faucibus non risus.
</code></pre>
</wa-code-demo>
```
If you only provide a width value, the viewport will be rendered to an initial 16:9 aspect ratio,
which can be changed via resizing.
You can customize this via the `--viewport-initial-aspect-ratio` property.
### Isolated demos with resources
Including resources in isolated demos works the same way.
Any relative URLs are still resolved relative to the host document.
In addition to the `wa-code-demo-include` class, which specifies resources to be included in *every* demo,
you can also use the `wa-code-demo-include-isolated` class which specifies resources to be included in every *isolated* demo,
i.e. the previews of demos using the `viewport` attribute, but also opening demos in a new tab or editing them on CodePen.
```html {.example}
<template class="wa-code-demo-include-isolated">
<script type="module" src="{% cdnUrl 'webawesome.loader.js' %}"></script>
<style>
body {
padding: var(--wa-space-l);
}
wa-callout { font-size: var(--wa-font-size-2xl) }
</style>
<script>console.log('Hello from iframe!')</script>
</template>
<wa-code-demo viewport include="link[rel=stylesheet]">
<pre><code class="language-html">
&lt;wa-callout&gt;Helloooo!&lt;/wa-callout&gt;
</code></pre>
</wa-code-demo>
```
## Styling
Just setting `border-radius` or `border` should work as expected:
```html{.example}
<wa-code-demo style="border: 2px dotted var(--wa-color-blue-50); border-radius: var(--wa-border-radius-s)">
<pre><code class="language-html">
&lt;button&gt;Click me!&lt;/button&gt;
&lt;wa-button&gt;Click me!&lt;/wa-button&gt;
</code></pre>
</wa-code-demo>
```
The divider width is controlled separately via `--divider-width`:
```html{.example}
<wa-code-demo open style="border-width: var(--wa-border-width-l); --divider-width: var(--wa-border-width-m);">
<pre><code class="language-html">
&lt;button&gt;Click me!&lt;/button&gt;
&lt;wa-button&gt;Click me!&lt;/wa-button&gt;
</code></pre>
</wa-code-demo>
```
## Roadmap
This component is a work in progress.
Some of the things that are not yet implemented are listed below.
It goes without saying that this list is a rough plan and subject to change.
### High priority
- Make the component dynamic so that when the code changes, the demo is updated
### Low priority
- Horizontal layout
- Tabbed layout
- Provide a way to display CSS and JS separately
- Provide a way to customize the playground used (currently it is hardcoded to CodePen)
- Provide a way to customize the buttons shown

View File

@@ -315,21 +315,13 @@ layout: page-outline
</wa-card>
</a>
<a href="/docs/components/drawer">
<wa-card with-header id="drawer-card">
<wa-card id="drawer-card">
<div slot="header">
{% include "svgs/drawer.njk" %}
</div>
<span class="page-name">Drawer</span>
</wa-card>
</a>
<a href="/docs/components/page">
<wa-card with-header>
<div slot="header">
{% include "svgs/page.njk" %}
</div>
<span class="page-name">Page</span>
</wa-card>
</a>
<a href="/docs/components/split-panel">
<wa-card with-header>
<div slot="header">

View File

@@ -1,238 +0,0 @@
---
title: Sample Documentation Page
description: A sample page for a documentation website using Web Awesome's page component.
layout: blank
---
<style>
wa-page {
--menu-width: 15rem;
--aside-width: 15rem;
}
wa-page[view='desktop'] [data-toggle-nav] {
display: none;
}
wa-page[view='mobile'] {
--menu-width: auto;
--aside-width: auto;
}
wa-page[view='mobile'] [slot='aside'] {
display: none;
}
wa-page[view='mobile'] #brand-name {
display: none;
}
wa-page[view='mobile'] #search {
display: none;
}
[slot='banner'] {
--wa-color-text-link: var(--wa-color-neutral-on-loud);
background-color: var(--wa-color-neutral-fill-loud);
}
[slot='header'] {
--wa-link-decoration-default: none;
border-block-end: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
[slot*='header'] a {
font-weight: var(--wa-font-weight-action);
}
[slot='subheader'] {
background-color: var(--wa-color-surface-lowered);
border-block-end: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
[slot='navigation-header'] {
border-block-end: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
wa-page[view='desktop'] [slot*='navigation'] {
border-inline-end: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
[slot*='navigation'] a {
--wa-color-text-link: var(--wa-color-text-normal);
}
[slot='navigation-footer'] {
border-block-start: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
[slot='main-header'],
main,
[slot='main-footer'] {
max-inline-size: 60rem;
margin-inline: auto;
}
[slot='main-footer'] {
border-block-start: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
[slot='footer'] {
--wa-color-text-link: var(--wa-color-text-quiet);
background-color: var(--wa-color-surface-lowered);
font-size: var(--wa-font-size-s);
}
</style>
<wa-page mobile-breakpoint="920">
<div slot="banner" class="wa-body-s">
<a href="#" class="wa-cluster wa-align-items-baseline wa-gap-xs" style="flex-wrap: nowrap;">
<wa-icon name="gift"></wa-icon>
<span>Give a Hoot for the Holidays: Donate now and double your impact.</span>
</a>
</div>
<header slot="header" class="wa-split">
<div class="wa-cluster">
<wa-icon name="feather-pointed" style="color: var(--wa-color-brand-fill-loud); font-size: 1.5em;"></wa-icon>
<span id="brand-name" class="wa-heading-s">Audubon Worldwide</span>
<a href="#">Our Work</a>
<a href="#">About Us</a>
<a href="#">Discover</a>
<a href="#">Get Involved</a>
</div>
<div class="wa-cluster wa-gap-xs">
<wa-button size="small" variant="brand" appearance="outlined">Find Your Local Audubon</wa-button>
<wa-button size="small" variant="brand">Donate</wa-button>
</div>
</header>
<nav slot="subheader">
<div class="wa-cluster" style="flex-wrap: nowrap;">
<wa-icon-button data-toggle-nav name="bars" label="Menu"></wa-icon-button>
<wa-breadcrumb style="font-size: var(--wa-font-size-s);">
<wa-breadcrumb-item>Field Guides</wa-breadcrumb-item>
<wa-breadcrumb-item>Owls</wa-breadcrumb-item>
<wa-breadcrumb-item>Great Horned Owl</wa-breadcrumb-item>
</wa-breadcrumb>
</div>
<wa-input id="search" placeholder="Search" size="small" style="max-inline-size: 12rem;">
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
</wa-input>
</nav>
<nav slot="navigation-header">
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1544648720-132573cb590d?q=20" label=""></wa-avatar>
<div class="wa-stack wa-gap-3xs">
<span class="wa-heading-s">Great Horned Owl</span>
<span class="wa-caption-s" lang="la"><em>Bubo virginianus</em></span>
</div>
</div>
</nav>
<nav slot="navigation">
<a href="#identification">Identification</a>
<a href="#range">Range and Habitat</a>
<a href="#behavior">Behavior</a>
<a href="#conservation">Conservation</a>
</nav>
<nav slot="navigation-footer">
<a href="#" class="wa-flank" style="--flank-size: 1.25em;">
<wa-icon name="camera"></wa-icon>
<span>Photo Gallery</span>
</a>
<a href="#" class="wa-flank" style="--flank-size: 1.25em;">
<wa-icon name="map-location-dot"></wa-icon>
<span>Interactive Range Map</span>
</a>
</nav>
<header slot="main-header">
<div class="wa-flank:end wa-border-radius-m wa-theme-default-dark" style="background-color: var(--wa-color-surface-lowered); --content-percentage: 35%; padding: var(--wa-space-m);">
<div class="wa-stack" style="margin: var(--wa-space-2xl);">
<h1>Great Horned Owl</h1>
<wa-divider></wa-divider>
<div class="wa-cluster wa-gap-xs">
<wa-tag size="small">Owls</wa-tag>
<wa-tag size="small">Birds of Prey</wa-tag>
<wa-tag size="small">Pleistocene Birds</wa-tag>
</div>
<div class="wa-flank">
<wa-icon name="ruler"></wa-icon>
<span class="wa-caption-m">L 21.5" | WS 48.5"</span>
</div>
<div class="wa-flank">
<wa-icon name="earth-americas"></wa-icon>
<span class="wa-caption-m">North America (Widespread), Central America (Limited), South America (Limited)</span>
</div>
<div class="wa-flank">
<wa-icon name="shield-heart"></wa-icon>
<span class="wa-caption-m">Least Concern</span>
</div>
</div>
<div class="wa-frame" style="border-radius: var(--wa-border-radius-m); max-inline-size: 40ch;">
<img src="https://images.unsplash.com/photo-1544648720-132573cb590d?q=20" />
</div>
</div>
</header>
<main class="wa-body-l">
<h2 id="identification">Identification</h2>
<p>Lorem ipsum odor amet, consectetuer adipiscing elit. Eget habitant scelerisque lectus ultrices nascetur aliquet sapien primis. Cursus sapien fusce semper nulla elit sociosqu lectus per sem. Sem ad porttitor dictum nisl pharetra tortor convallis. Sit molestie hendrerit porta dictum tortor posuere euismod magna. Mauris suspendisse pharetra finibus; eleifend etiam ridiculus.</p>
<h2 id="range">Range and Habitat</h2>
<p>Diam sed ipsum pretium porttitor class cubilia elementum. Blandit felis ligula habitant ultricies vulputate rutrum lacus commodo pulvinar. Nostra semper placerat lectus in dis eu. Sagittis ipsum placerat rhoncus lacus id eget. Erat pharetra aptent enim, augue accumsan ultricies inceptos habitasse. Senectus id maximus parturient tellus; fermentum posuere vulputate luctus. Ac tempus dapibus vehicula ligula ullamcorper sit duis.</p>
<h2 id="behavior">Behavior</h2>
<p>Erat vitae luctus arcu taciti malesuada pretium arcu justo primis. Cubilia vitae maecenas congue velit id netus arcu. Dictum vel pellentesque taciti fermentum risus consectetur amet. Faucibus commodo habitasse sem maximus praesent purus, dignissim tristique porta. Platea magna justo ipsum ut metus ac facilisi. Imperdiet laoreet pharetra maximus lacus tortor suscipit. Nam quisque iaculis orci porttitor pellentesque rhoncus. Molestie sagittis tincidunt quisque nisi non urna conubia.</p>
<h2 id="conservation">Conservation</h2>
<p>Nullam magna quam quisque eu varius integer. Inceptos donec facilisi risus himenaeos semper mollis habitasse. Vehicula lacus vivamus euismod pharetra mollis dictum. Ante ex tortor elementum eleifend habitasse orci aliquam. Fames erat senectus fames etiam dapibus cursus.</p>
</main>
<footer slot="main-footer">
<section>
<h2 class="wa-heading-m">Sources</h2>
<ul class="wa-body-s">
<li><cite><a href="https://www.audubon.org/field-guide/bird/great-horned-owl" target="_blank" rel="noopener">Great Horned Owl</a></cite>, National Audubon Society. Retrieved 5 December 2024.</li>
<li><cite><a href="https://www.allaboutbirds.org/guide/Great_Horned_Owl/" target="_blank" rel="noopener">Great Horned Owl</a></cite>, All About Birds by CornellLab. Retrieved 5 December 2024.</li>
<li>Armistead, G. L. (2015). <cite>Field guide to birds of Pennsylvania</cite>. Scott & Nix, Inc.</li>
</ul>
</section>
</footer>
<aside slot="aside">
<h2 class="wa-heading-m">Discover More Birds</h2>
<wa-card with-image>
<div slot="image" class="wa-frame">
<img src="https://images.unsplash.com/photo-1635254859323-65b78408dcca?q=20" alt="" />
</div>
<div class="wa-stack wa-gap-3xs">
<span class="wa-heading-s">Long-eared Owl</span>
<span class="wa-caption-s" lang="la"><em>Asio otus</em></span>
</div>
</wa-card>
<wa-card with-image>
<div slot="image" class="wa-frame">
<img src="https://images.unsplash.com/photo-1661350356618-f5915c7b6a3c?q=20" alt="" />
</div>
<div class="wa-stack wa-gap-3xs">
<span class="wa-heading-s">Northen Hawk Owl</span>
<span class="wa-caption-s" lang="la"><em>Surnia ulula</em></span>
</div>
</wa-card>
<wa-card with-image>
<div slot="image" class="wa-frame">
<img src="https://images.unsplash.com/photo-1660307777355-f08bced145d3?q=20" alt="" />
</div>
<div class="wa-stack wa-gap-3xs">
<span class="wa-heading-s">Golden Eagle</span>
<span class="wa-caption-s" lang="la"><em>Aquila chrysaetos</em></span>
</div>
</wa-card>
</aside>
<footer slot="footer" class="wa-grid wa-gap-xl">
<div class="wa-cluster" style="flex-wrap: nowrap;">
<wa-icon name="feather-pointed" style="font-size: 1.5em;"></wa-icon>
<span class="wa-heading-s">Audubon Worldwide</span>
</div>
<div class="wa-stack">
<h3 class="wa-heading-xs">Our Work</h3>
<a href="#">Habitat Restoration</a>
<a href="#">Migration Science</a>
<a href="#">Advocacy</a>
</div>
<div class="wa-stack">
<h3 class="wa-heading-xs">About Us</h3>
<a href="#">Our History</a>
<a href="#">Leadership</a>
<a href="#">Fiscal Reports</a>
</div>
<div class="wa-stack">
<h3 class="wa-heading-xs">Discover</h3>
<a href="#">Field Guides</a>
<a href="#">Photo Search</a>
<a href="#">Gear and Resources</a>
</div>
<div class="wa-stack">
<h3 class="wa-heading-xs">Get Involved</h3>
<a href="#">Adopt a Bird</a>
<a href="#">Your Local Audubon</a>
<a href="#">Youth Audubon Camps</a>
</div>
</footer>
</wa-page>

View File

@@ -1,401 +0,0 @@
---
title: Sample Media App Page
description: A sample page for a media app using Web Awesome's page component.
layout: blank
---
<wa-page class="wa-theme-default-dark">
<header slot="header">
<div class="wa-cluster">
<wa-icon-button name="bars" label="Menu" data-toggle-nav></wa-icon-button>
<wa-icon name="record-vinyl" family="duotone"></wa-icon>
<span class="wa-heading-m">radiogaga</span>
</div>
<wa-input placeholder="Search" style="max-inline-size: 100%;">
<wa-icon slot="prefix" name="magnifying-glass" ></wa-icon>
</wa-input>
<div class="wa-cluster">
<wa-button appearance="outlined">Log In</wa-button>
<wa-button>Sign Up</wa-button>
</div>
</header>
<div slot="navigation-header" class="wa-split">
<h2 class="wa-heading-s">For You</h2>
<wa-icon-button id="settings" name="gear" label="Settings"></wa-icon-button>
</div>
<nav slot="navigation">
<h3 class="wa-heading-xs">Discover</h3>
<ul class="wa-stack wa-gap-0">
<li>
<a href="#" class="wa-flank">
<wa-icon name="house"></wa-icon>
<span>Home</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="sparkles"></wa-icon>
<span>New</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="tower-broadcast"></wa-icon>
<span>Stations</span>
</a>
</li>
</ul>
<h3 class="wa-heading-xs">Library</h3>
<ul class="wa-stack wa-gap-0">
<li>
<a href="#" class="wa-flank">
<wa-icon name="heart"></wa-icon>
<span>Favorites</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="list-music"></wa-icon>
<span>Playlists</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="microphone-stand"></wa-icon>
<span>Artists</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="grid-2"></wa-icon>
<span>Albums</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="podcast"></wa-icon>
<span>Podcasts</span>
</a>
</li>
</ul>
<h3 class="wa-heading-xs">Recently Played</h3>
<ul id="recent" class="wa-stack wa-gap-0">
<li>
<a href="#" class="wa-flank">
<wa-icon name="cassette-tape" style="background: var(--wa-color-red-90); color: var(--wa-color-red-60);"></wa-icon>
<span>Lo-Fi Station</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="face-awesome" style="background: var(--wa-color-blue-30); color: var(--wa-color-yellow-90);"></wa-icon>
<span>Podcast Awesome</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="seedling" style="background: var(--wa-color-green-70); color: var(--wa-color-green-90);"></wa-icon>
<div class="wa-stack wa-gap-0">
<span>Seasons</span>
<span class="wa-caption-s">Blister Soul</span>
</div>
</a>
</li>
</ul>
</nav>
<div slot="main-header">
<wa-icon-button id="back" name="chevron-left" label="Back"></wa-icon-button>
<wa-tooltip for="back" placement="bottom" distance="2">Back</wa-tooltip>
<div class="wa-cluster">
<wa-icon-button id="favorite" name="heart" variant="regular" label="Favorite"></wa-icon-button>
<wa-tooltip for="favorite" placement="bottom" distance="2">Favorite</wa-tooltip>
<wa-icon-button id="options" name="ellipsis" label="Options"></wa-icon-button>
<wa-tooltip for="options" placement="bottom" distance="2">Options</wa-tooltip>
</div>
</div>
<main>
<div class="wa-stack wa-gap-3xl">
<div class="wa-flank wa-gap-3xl" style="--flank-size: 35%; --content-percentage: 55%;">
<div class="wa-frame wa-border-radius-l" style="max-inline-size: 40ch;">
<img src="https://images.unsplash.com/photo-1732430579016-8d5e5ebd3c99?q=20" alt="Home for the Holidays album artwork" />
</div>
<div class="wa-split:column wa-align-items-start">
<div class="wa-stack" style="margin-block: auto;">
<h1 class="wa-heading-3xl">Home for the Holidays</h1>
<a href="#" class="wa-heading-m">The Shire Choir</a>
<div class="wa-cluster wa-caption-m wa-gap-2xs">
<span>Holiday</span>
<span>&bull;</span>
<span>2024</span>
<span>&bull;</span>
<span>12 songs, 41 minutes 9 seconds</span>
</div>
</div>
<div id="play-controls" class="wa-split wa-gap-xl">
<div class="wa-cluster wa-gap-xl">
<wa-icon-button name="play" label="Play"></wa-icon-button>
<wa-icon-button name="shuffle" label="Shuffle"></wa-icon-button>
</div>
<wa-icon-button name="plus" label="Add to Library"></wa-icon-button>
</div>
</div>
</div>
<ol class="wa-stack wa-gap-0">
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="1"></wa-icon>
<span>Fa-La-La-Fellowship</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">3:27</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="2"></wa-icon>
<span>Sleigh Ride</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">2:36</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="3"></wa-icon>
<span>All I Want For Christmas Is Stew</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">2:51</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="4"></wa-icon>
<span>Rockin' Around the Christmas Ent</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">3:05</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="5"></wa-icon>
<span>Merry, Did You Know?</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">1:56</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="6"></wa-icon>
<span>Run Run Shadowfax</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">3:32</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="7"></wa-icon>
<span>You're a Mean One, Mr. Grima</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">2:46</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="8"></wa-icon>
<span>O Come, All Ye Faithful</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">3:27</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="9"></wa-icon>
<span>Do You Hear What I Hear</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">2:13</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<span class="wa-cluster wa-gap-3xs">
<wa-icon name="1"></wa-icon>
<wa-icon name="0"></wa-icon>
</span>
<span>Carol of the Horns</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">2:55</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<span class="wa-cluster wa-gap-3xs">
<wa-icon name="1"></wa-icon>
<wa-icon name="1"></wa-icon>
</span>
<span>Silent Night</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">3:10</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<span class="wa-cluster wa-gap-3xs">
<wa-icon name="1"></wa-icon>
<wa-icon name="2"></wa-icon>
</span>
<span>Wizard Wonderland</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">3:22</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
</ol>
</div>
</main>
<div slot="main-footer" class="wa-grid wa-gap-xl">
<h2 class="wa-heading-2xl">More You Might Like</h2>
<div class="wa-stack wa-gap-xs">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1675219119611-40323b738563?q=20" alt="" />
</div>
<span class="wa-heading-s">Festival of Lights</span>
<span class="wa-caption-s">Station</span>
</div>
<div class="wa-stack wa-gap-xs">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1481930916222-5ec4696fc0f2?q=20" alt="" />
</div>
<span class="wa-heading-s">Holiday Cheer</span>
<span class="wa-caption-s">Essential Playlist</span>
</div>
<div class="wa-stack wa-gap-xs">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1667514627762-521b1c815a89?q=20" alt="" />
</div>
<span class="wa-heading-s">Nursery Rhymes from the Shire</span>
<span class="wa-caption-s">The Shire Choir</span>
</div>
</div>
</wa-page>
<style>
wa-page {
--menu-width: 18rem;
--wa-tooltip-arrow-size: 0;
background-color: var(--wa-color-surface-lowered);
}
wa-page[view='desktop'] [data-toggle-nav] {
display: none;
}
wa-page[view='mobile'] {
--menu-width: auto;
}
wa-page,
[slot='header'],
wa-page[view='desktop'] [slot*='navigation'] {
background-color: var(--wa-color-surface-lowered);
}
wa-page[view='mobile'] [slot*='navigation'] {
padding: 0;
}
wa-page::part(base) {
background-color: var(--wa-color-surface-lowered);
}
[slot='header'] {
background: linear-gradient(to bottom, var(--wa-color-surface-raised), var(--wa-color-surface-lowered));
}
[slot='navigation-header'],
[slot='main-header'] {
padding-block-end: 0;
}
[slot='navigation'] a {
--wa-color-text-link: var(--wa-color-text-normal);
--wa-link-decoration-default: none;
--wa-link-decoration-hover: none;
--flank-size: 2rem;
font-weight: var(--wa-font-weight-action);
gap: 0.5rem;
}
[slot='navigation'] ul {
list-style: none;
margin: 0;
}
[slot='navigation'] ul a {
border-radius: var(--wa-border-radius-s);
padding: var(--wa-space-xs);
}
[slot='navigation'] ul a:hover,
main ol li:hover {
background-color: color-mix(in oklab, var(--wa-color-surface-default), var(--wa-color-brand-fill-quiet));
}
[slot='navigation'] wa-icon {
align-items: center;
aspect-ratio: 1;
color: var(--wa-color-brand-fill-loud);
display: flex;
height: var(--flank-size);
justify-content: center;
}
[slot='navigation'] #recent wa-icon {
border-radius: var(--wa-border-radius-xs);
}
[slot='main-header'] {
border-block-start: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
border-inline: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
border-radius: var(--wa-border-radius-l) var(--wa-border-radius-l) 0 0
}
main,
[slot*='main'] {
margin-inline: var(--wa-space-m);
}
main ol li {
padding: var(--wa-space-m);
}
main ol li .wa-flank {
--flank-size: 2rem;
}
main ol li:not(:first-child) {
border-block-start: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
main,
[slot='main-footer'] {
border-inline: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
main,
[slot='main-header'] {
background-color: var(--wa-color-surface-raised);
}
#play-controls wa-icon-button::part(base) {
border: var(--wa-border-width-l) var(--wa-border-style) currentColor;
border-radius: var(--wa-border-radius-circle);
font-size: 1.5rem;
}
#play-controls wa-icon-button[name="play"]::part(base) {
background-color: var(--wa-color-brand-fill-loud);
border: none;
color: var(--wa-color-brand-on-loud);
font-size: 3rem;
padding: 1.5rem;
}
</style>

View File

@@ -1,33 +1,50 @@
---
title: Page
description: Pages offer an easy way to scaffold entire page layouts using minimal markup.
description: Layouts offer an easy way to scaffold pages using minimal markup.
layout: component
isPro: true
---
The page component is designed to power full webpages. It is flexible enough to handle most modern designs and includes a simple mechanism for handling desktop and mobile navigation.
The layout component is designed to power full webpages. It is flexible enough to handle most modern designs and includes a simple mechanism for handling desktop and mobile navigation.
A number of sections are available as part of the layout, most of which are optional. Content is added by [slotting elements](/docs/usage/#slots) into various locations.
This component _does not_ implement any [content sectioning](https://developer.mozilla.org/en-US/docs/Web/HTML/Element#content_sectioning) or "semantic elements" internally (such as `<main>`, `<header>`, `<footer>`, etc.). Instead, it is recommended that you slot in content sectioning elements wherever you feel they're appropriate.
## Layout Anatomy
This image depicts a page's anatomy, including the default positions of each section. The labels represent the [named slots](#slots) you can use to populate them.
This image depicts the layout's anatomy, including the default positions of each section. The labels represent the [named slots](#slots) you can use to populate them.
Most slots are optional. Slots that have no content will not be shown, allowing you to opt-in to just the sections you actually need.
Most slots are optional. Slots that have no content will not be shown, allowing you to opt-in to just the sections of the layout you actually need.
{% include "page-demo.njk" %}
<!-- ![Screenshot of Layout Anatomy showing various slots](/assets/images/layout-anatomy.svg) -->
## Using `wa-page`
![Screenshot of Layout Anatomy showing various slots](/assets/images/layout-anatomy.svg)
:::info
If you're not familiar with how slots work in HTML, you might want to [learn more about slots](/docs/usage/#slots) before using this component.
:::
A number of sections are available as part of the page component, most of which are optional. Content is populated by [slotting elements](/docs/usage/#slots) into various locations.
## Sticky Sections
This component _does not_ implement any [content sectioning](https://developer.mozilla.org/en-US/docs/Web/HTML/Element#content_sectioning) or "semantic elements" internally (such as `<main>`, `<header>`, `<footer>`, etc.). Instead, we recommended that you slot in content sectioning elements wherever you feel they're appropriate.
The following sections of the layout are "sticky" by default, meaning they remain in position as the user scrolls.
When using `<wa-page>`, make sure to zero out all paddings and margins on `<html>` and `<body>`, otherwise you may see unexpected gaps. We highly recommend adding the following styles when using `<wa-page>`:
- `banner`
- `header`
- `sub-header`
- `aside`
- `menu`
This is often desirable, but you can change this behavior using the `disable-sticky` attribute. Use a space-delimited list of names to tell the layout which sections should not be sticky.
```html
<wa-page disable-sticky="header aside"> ... </wa-page>
```
## How to Apply Spacing to Your Layout
The layout component _does not_ apply spacing for you. You can apply the appropriate paddings or margins directly to the elements you slot in to fine tune your spacing needs.
TODO - add example here
When using `<wa-page>`, make sure to zero out all paddings and margins on `<html>` and `<body>`, otherwise you may see unexpected gaps. The following styles are highly recommended when using `<wa-page>`.
```css
html,
@@ -39,687 +56,7 @@ body {
}
```
## Examples
:::warning
Open demos in a new tab to examine their behavior in different window sizes.
The previews below use simulated zooming which, depending on your browser, may not be accurate.
:::
### Documentation
A sample documentation page using [all available slots](#slots).
The navigation menu collapses into a drawer at a custom `mobile-breakpoint` of 920px.
It can be opened using a button with `[data-toggle-nav]` that appears in the `subheader` slot. The `aside` slot is also hidden below 920px.
```html {.example viewport="1600"}
<wa-page mobile-breakpoint="920">
<div slot="banner" class="wa-body-s">
<a href="#" class="wa-cluster wa-align-items-baseline wa-gap-xs" style="flex-wrap: nowrap;">
<wa-icon name="gift"></wa-icon>
<span>Give a Hoot for the Holidays: Donate now and double your impact.</span>
</a>
</div>
<header slot="header" class="wa-split">
<div class="wa-cluster">
<wa-icon name="feather-pointed" style="color: var(--wa-color-brand-fill-loud); font-size: 1.5em;"></wa-icon>
<span id="brand-name" class="wa-heading-s">Audubon Worldwide</span>
<a href="#">Our Work</a>
<a href="#">About Us</a>
<a href="#">Discover</a>
<a href="#">Get Involved</a>
</div>
<div class="wa-cluster wa-gap-xs">
<wa-button size="small" variant="brand" appearance="outlined">Find Your Local Audubon</wa-button>
<wa-button size="small" variant="brand">Donate</wa-button>
</div>
</header>
<nav slot="subheader">
<div class="wa-cluster" style="flex-wrap: nowrap;">
<wa-icon-button data-toggle-nav name="bars" label="Menu"></wa-icon-button>
<wa-breadcrumb style="font-size: var(--wa-font-size-s);">
<wa-breadcrumb-item>Field Guides</wa-breadcrumb-item>
<wa-breadcrumb-item>Owls</wa-breadcrumb-item>
<wa-breadcrumb-item>Great Horned Owl</wa-breadcrumb-item>
</wa-breadcrumb>
</div>
<wa-input id="search" placeholder="Search" size="small" style="max-inline-size: 12rem;">
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
</wa-input>
</nav>
<nav slot="navigation-header">
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1544648720-132573cb590d?q=20" label=""></wa-avatar>
<div class="wa-stack wa-gap-3xs">
<span class="wa-heading-s">Great Horned Owl</span>
<span class="wa-caption-s" lang="la"><em>Bubo virginianus</em></span>
</div>
</div>
</nav>
<nav slot="navigation">
<a href="#identification">Identification</a>
<a href="#range">Range and Habitat</a>
<a href="#behavior">Behavior</a>
<a href="#conservation">Conservation</a>
</nav>
<nav slot="navigation-footer">
<a href="#" class="wa-flank" style="--flank-size: 1.25em;">
<wa-icon name="camera"></wa-icon>
<span>Photo Gallery</span>
</a>
<a href="#" class="wa-flank" style="--flank-size: 1.25em;">
<wa-icon name="map-location-dot"></wa-icon>
<span>Interactive Range Map</span>
</a>
</nav>
<header slot="main-header">
<div class="wa-flank:end wa-border-radius-m wa-theme-default-dark" style="background-color: var(--wa-color-surface-lowered); --content-percentage: 35%; padding: var(--wa-space-m);">
<div class="wa-stack" style="margin: var(--wa-space-2xl);">
<h1>Great Horned Owl</h1>
<wa-divider></wa-divider>
<div class="wa-cluster wa-gap-xs">
<wa-tag size="small">Owls</wa-tag>
<wa-tag size="small">Birds of Prey</wa-tag>
<wa-tag size="small">Pleistocene Birds</wa-tag>
</div>
<div class="wa-flank">
<wa-icon name="ruler"></wa-icon>
<span class="wa-caption-m">L 21.5" | WS 48.5"</span>
</div>
<div class="wa-flank">
<wa-icon name="earth-americas"></wa-icon>
<span class="wa-caption-m">North America (Widespread), Central America (Limited), South America (Limited)</span>
</div>
<div class="wa-flank">
<wa-icon name="shield-heart"></wa-icon>
<span class="wa-caption-m">Least Concern</span>
</div>
</div>
<div class="wa-frame" style="border-radius: var(--wa-border-radius-m); max-inline-size: 40ch;">
<img src="https://images.unsplash.com/photo-1544648720-132573cb590d?q=20" />
</div>
</div>
</header>
<main class="wa-body-l">
<h2 id="identification">Identification</h2>
<p>Lorem ipsum odor amet, consectetuer adipiscing elit. Eget habitant scelerisque lectus ultrices nascetur aliquet sapien primis. Cursus sapien fusce semper nulla elit sociosqu lectus per sem. Sem ad porttitor dictum nisl pharetra tortor convallis. Sit molestie hendrerit porta dictum tortor posuere euismod magna. Mauris suspendisse pharetra finibus; eleifend etiam ridiculus.</p>
<h2 id="range">Range and Habitat</h2>
<p>Diam sed ipsum pretium porttitor class cubilia elementum. Blandit felis ligula habitant ultricies vulputate rutrum lacus commodo pulvinar. Nostra semper placerat lectus in dis eu. Sagittis ipsum placerat rhoncus lacus id eget. Erat pharetra aptent enim, augue accumsan ultricies inceptos habitasse. Senectus id maximus parturient tellus; fermentum posuere vulputate luctus. Ac tempus dapibus vehicula ligula ullamcorper sit duis.</p>
<h2 id="behavior">Behavior</h2>
<p>Erat vitae luctus arcu taciti malesuada pretium arcu justo primis. Cubilia vitae maecenas congue velit id netus arcu. Dictum vel pellentesque taciti fermentum risus consectetur amet. Faucibus commodo habitasse sem maximus praesent purus, dignissim tristique porta. Platea magna justo ipsum ut metus ac facilisi. Imperdiet laoreet pharetra maximus lacus tortor suscipit. Nam quisque iaculis orci porttitor pellentesque rhoncus. Molestie sagittis tincidunt quisque nisi non urna conubia.</p>
<h2 id="conservation">Conservation</h2>
<p>Nullam magna quam quisque eu varius integer. Inceptos donec facilisi risus himenaeos semper mollis habitasse. Vehicula lacus vivamus euismod pharetra mollis dictum. Ante ex tortor elementum eleifend habitasse orci aliquam. Fames erat senectus fames etiam dapibus cursus.</p>
</main>
<footer slot="main-footer">
<section>
<h2 class="wa-heading-m">Sources</h2>
<ul class="wa-body-s">
<li><cite><a href="https://www.audubon.org/field-guide/bird/great-horned-owl" target="_blank" rel="noopener">Great Horned Owl</a></cite>, National Audubon Society. Retrieved 5 December 2024.</li>
<li><cite><a href="https://www.allaboutbirds.org/guide/Great_Horned_Owl/" target="_blank" rel="noopener">Great Horned Owl</a></cite>, All About Birds by CornellLab. Retrieved 5 December 2024.</li>
<li>Armistead, G. L. (2015). <cite>Field guide to birds of Pennsylvania</cite>. Scott & Nix, Inc.</li>
</ul>
</section>
</footer>
<aside slot="aside">
<h2 class="wa-heading-m">Discover More Birds</h2>
<wa-card with-image>
<div slot="image" class="wa-frame">
<img src="https://images.unsplash.com/photo-1635254859323-65b78408dcca?q=20" alt="" />
</div>
<div class="wa-stack wa-gap-3xs">
<span class="wa-heading-s">Long-eared Owl</span>
<span class="wa-caption-s" lang="la"><em>Asio otus</em></span>
</div>
</wa-card>
<wa-card with-image>
<div slot="image" class="wa-frame">
<img src="https://images.unsplash.com/photo-1661350356618-f5915c7b6a3c?q=20" alt="" />
</div>
<div class="wa-stack wa-gap-3xs">
<span class="wa-heading-s">Northen Hawk Owl</span>
<span class="wa-caption-s" lang="la"><em>Surnia ulula</em></span>
</div>
</wa-card>
<wa-card with-image>
<div slot="image" class="wa-frame">
<img src="https://images.unsplash.com/photo-1660307777355-f08bced145d3?q=20" alt="" />
</div>
<div class="wa-stack wa-gap-3xs">
<span class="wa-heading-s">Golden Eagle</span>
<span class="wa-caption-s" lang="la"><em>Aquila chrysaetos</em></span>
</div>
</wa-card>
</aside>
<footer slot="footer" class="wa-grid wa-gap-xl">
<div class="wa-cluster" style="flex-wrap: nowrap;">
<wa-icon name="feather-pointed" style="font-size: 1.5em;"></wa-icon>
<span class="wa-heading-s">Audubon Worldwide</span>
</div>
<div class="wa-stack">
<h3 class="wa-heading-xs">Our Work</h3>
<a href="#">Habitat Restoration</a>
<a href="#">Migration Science</a>
<a href="#">Advocacy</a>
</div>
<div class="wa-stack">
<h3 class="wa-heading-xs">About Us</h3>
<a href="#">Our History</a>
<a href="#">Leadership</a>
<a href="#">Fiscal Reports</a>
</div>
<div class="wa-stack">
<h3 class="wa-heading-xs">Discover</h3>
<a href="#">Field Guides</a>
<a href="#">Photo Search</a>
<a href="#">Gear and Resources</a>
</div>
<div class="wa-stack">
<h3 class="wa-heading-xs">Get Involved</h3>
<a href="#">Adopt a Bird</a>
<a href="#">Your Local Audubon</a>
<a href="#">Youth Audubon Camps</a>
</div>
</footer>
</wa-page>
<style>
wa-page {
--menu-width: 15rem;
--aside-width: 15rem;
}
wa-page[view='desktop'] [data-toggle-nav] {
display: none;
}
wa-page[view='mobile'] {
--menu-width: auto;
--aside-width: auto;
}
wa-page[view='mobile'] [slot='aside'],
wa-page[view='mobile'] #brand-name,
wa-page[view='mobile'] #search {
display: none;
}
[slot='banner'] {
--wa-color-text-link: var(--wa-color-neutral-on-loud);
background-color: var(--wa-color-neutral-fill-loud);
}
[slot='header'] {
--wa-link-decoration-default: none;
border-block-end: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
[slot*='header'] a {
font-weight: var(--wa-font-weight-action);
}
[slot='subheader'] {
background-color: var(--wa-color-surface-lowered);
border-block-end: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
[slot='navigation-header'] {
border-block-end: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
wa-page[view='desktop'] [slot*='navigation'] {
border-inline-end: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
[slot*='navigation'] a {
--wa-color-text-link: var(--wa-color-text-normal);
}
[slot='navigation-footer'] {
border-block-start: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
[slot='main-header'],
main,
[slot='main-footer'] {
max-inline-size: 60rem;
margin-inline: auto;
}
[slot='main-footer'] {
border-block-start: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
[slot='footer'] {
--wa-color-text-link: var(--wa-color-text-quiet);
background-color: var(--wa-color-surface-lowered);
font-size: var(--wa-font-size-s);
}
</style>
```
### Media
A sample media app page using `header`, `navigation-header`, `main-header`, and `main-footer` along with the default slot. The navigation menu collapses into a drawer at the default `mobile-breakpoint` and can be opened using a button with `[data-toggle-nav]` that appears in the `header` slot.
```html {.example viewport="1600"}
<wa-page class="wa-theme-default-dark">
<header slot="header">
<div class="wa-cluster">
<wa-icon-button name="bars" label="Menu" data-toggle-nav></wa-icon-button>
<wa-icon name="record-vinyl"></wa-icon>
<span class="wa-heading-m">radiogaga</span>
</div>
<wa-input id="search-header" placeholder="Search" style="max-inline-size: 100%;">
<wa-icon slot="prefix" name="magnifying-glass" ></wa-icon>
</wa-input>
<div class="wa-cluster">
<wa-button appearance="outlined">Log In</wa-button>
<wa-button>Sign Up</wa-button>
</div>
</header>
<div slot="navigation-header" class="wa-split">
<wa-input id="search-nav-drawer" placeholder="Search" style="max-inline-size: 100%;">
<wa-icon slot="prefix" name="magnifying-glass" ></wa-icon>
</wa-input>
<div class="wa-split">
<h2 class="wa-heading-s">For You</h2>
<wa-icon-button id="settings" name="gear" label="Settings"></wa-icon-button>
</div>
</div>
<nav slot="navigation">
<h3 class="wa-heading-xs">Discover</h3>
<ul class="wa-stack wa-gap-0">
<li>
<a href="#" class="wa-flank">
<wa-icon name="house"></wa-icon>
<span>Home</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="star"></wa-icon>
<span>New</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="tower-broadcast"></wa-icon>
<span>Stations</span>
</a>
</li>
</ul>
<h3 class="wa-heading-xs">Library</h3>
<ul class="wa-stack wa-gap-0">
<li>
<a href="#" class="wa-flank">
<wa-icon name="heart"></wa-icon>
<span>Favorites</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="bars-staggered"></wa-icon>
<span>Playlists</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="microphone-lines"></wa-icon>
<span>Artists</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="layer-group"></wa-icon>
<span>Albums</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="podcast"></wa-icon>
<span>Podcasts</span>
</a>
</li>
</ul>
<h3 class="wa-heading-xs">Recently Played</h3>
<ul id="recent" class="wa-stack wa-gap-0">
<li>
<a href="#" class="wa-flank">
<wa-icon name="radio" style="background: var(--wa-color-red-90); color: var(--wa-color-red-60);"></wa-icon>
<span>Lo-Fi Station</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="font-awesome" style="background: var(--wa-color-blue-30); color: var(--wa-color-yellow-90);"></wa-icon>
<span>Podcast Awesome</span>
</a>
</li>
<li>
<a href="#" class="wa-flank">
<wa-icon name="seedling" style="background: var(--wa-color-green-70); color: var(--wa-color-green-90);"></wa-icon>
<div class="wa-stack wa-gap-0">
<span>Seasons</span>
<span class="wa-caption-s">Blister Soul</span>
</div>
</a>
</li>
</ul>
</nav>
<div slot="main-header">
<wa-icon-button id="back" name="chevron-left" label="Back"></wa-icon-button>
<wa-tooltip for="back" placement="bottom" distance="2">Back</wa-tooltip>
<div class="wa-cluster">
<wa-icon-button id="favorite" name="heart" variant="regular" label="Favorite"></wa-icon-button>
<wa-tooltip for="favorite" placement="bottom" distance="2">Favorite</wa-tooltip>
<wa-icon-button id="options" name="ellipsis" label="Options"></wa-icon-button>
<wa-tooltip for="options" placement="bottom" distance="2">Options</wa-tooltip>
</div>
</div>
<main>
<div class="wa-stack wa-gap-3xl">
<div class="wa-flank wa-gap-3xl" style="--content-percentage: 40%;">
<div class="wa-frame wa-border-radius-l" style="max-inline-size: 40ch;">
<img src="https://images.unsplash.com/photo-1732430579016-8d5e5ebd3c99?q=20" alt="Home for the Holidays album artwork" />
</div>
<div class="wa-split:column wa-align-items-start">
<div class="wa-stack" style="margin-block: auto;">
<h1 class="wa-heading-3xl">Home for the Holidays</h1>
<a href="#" class="wa-heading-m">The Shire Choir</a>
<div class="wa-cluster wa-caption-m wa-gap-2xs">
<span>Holiday</span>
<span>&bull;</span>
<span>2024</span>
<span>&bull;</span>
<span>12 songs, 41 minutes 9 seconds</span>
</div>
</div>
<div id="play-controls" class="wa-split wa-gap-xl">
<div class="wa-cluster wa-gap-xl">
<wa-icon-button name="play" label="Play"></wa-icon-button>
<wa-icon-button name="shuffle" label="Shuffle"></wa-icon-button>
</div>
<wa-icon-button name="plus" label="Add to Library"></wa-icon-button>
</div>
</div>
</div>
<ol class="wa-stack wa-gap-0">
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="1"></wa-icon>
<span>Fa-La-La-Fellowship</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">3:27</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="2"></wa-icon>
<span>Sleigh Ride</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">2:36</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="3"></wa-icon>
<span>All I Want For Christmas Is Stew</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">2:51</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="4"></wa-icon>
<span>Rockin' Around the Christmas Ent</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">3:05</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="5"></wa-icon>
<span>Merry, Did You Know?</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">1:56</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="6"></wa-icon>
<span>Run Run Shadowfax</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">3:32</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="7"></wa-icon>
<span>You're a Mean One, Mr. Grima</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">2:46</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="8"></wa-icon>
<span>O Come, All Ye Faithful</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">3:27</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<wa-icon name="9"></wa-icon>
<span>Do You Hear What I Hear</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">2:13</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<span class="wa-cluster wa-gap-3xs">
<wa-icon name="1"></wa-icon>
<wa-icon name="0"></wa-icon>
</span>
<span>Carol of the Horns</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">2:55</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<span class="wa-cluster wa-gap-3xs">
<wa-icon name="1"></wa-icon>
<wa-icon name="1"></wa-icon>
</span>
<span>Silent Night</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">3:10</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
<li class="wa-split">
<span class="wa-flank">
<span class="wa-cluster wa-gap-3xs">
<wa-icon name="1"></wa-icon>
<wa-icon name="2"></wa-icon>
</span>
<span>Wizard Wonderland</span>
</span>
<span class="wa-cluster">
<span class="wa-caption-m">3:22</span>
<wa-icon-button name="ellipsis" label="Song Options"></wa-icon-button>
</span>
</li>
</ol>
</div>
</main>
<div slot="main-footer" class="wa-grid wa-gap-xl wa-align-items-center">
<h2 class="wa-heading-2xl">More You Might Like</h2>
<div class="wa-stack wa-gap-xs">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1675219119611-40323b738563?q=20" alt="" />
</div>
<span class="wa-heading-s">Festival of Lights</span>
<span class="wa-caption-s">Station</span>
</div>
<div class="wa-stack wa-gap-xs">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1481930916222-5ec4696fc0f2?q=20" alt="" />
</div>
<span class="wa-heading-s">Holiday Cheer</span>
<span class="wa-caption-s">Essential Playlist</span>
</div>
<div class="wa-stack wa-gap-xs">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1667514627762-521b1c815a89?q=20" alt="" />
</div>
<span class="wa-heading-s">Nursery Rhymes from the Shire</span>
<span class="wa-caption-s">The Shire Choir</span>
</div>
</div>
</wa-page>
<style>
wa-page {
--menu-width: 30ch;
--wa-tooltip-arrow-size: 0;
background-color: var(--wa-color-surface-lowered);
}
wa-page[view='desktop'] :is([data-toggle-nav], #search-nav-drawer) {
display: none;
}
wa-page[view='mobile'] {
--menu-width: auto;
}
wa-page[view='mobile'] #search-header {
display: none;
}
wa-page[view='mobile'] :is([slot*='main'], main) {
padding: var(--wa-space-xl);
}
wa-page,
[slot='header'],
wa-page[view='desktop'] [slot*='navigation'] {
background-color: var(--wa-color-surface-lowered);
}
wa-page[view='mobile'] [slot*='navigation'] {
padding: 0;
}
wa-page::part(base) {
background-color: var(--wa-color-surface-lowered);
}
[slot='header'] {
background: linear-gradient(to bottom, var(--wa-color-surface-raised), var(--wa-color-surface-lowered));
}
[slot='navigation-header'],
[slot='main-header'] {
padding-block-end: 0 !important;
padding-block-start: var(--wa-space-3xl);
}
[slot='navigation'] a {
--wa-color-text-link: var(--wa-color-text-normal);
--wa-link-decoration-default: none;
--wa-link-decoration-hover: none;
--flank-size: 2rem;
font-weight: var(--wa-font-weight-action);
gap: 0.5rem;
}
[slot='navigation'] ul {
list-style: none;
margin: 0;
}
[slot='navigation'] ul a {
border-radius: var(--wa-border-radius-s);
padding: var(--wa-space-xs);
}
[slot='navigation'] ul a:hover,
main ol li:hover {
background-color: color-mix(in oklab, var(--wa-color-surface-default), var(--wa-color-brand-fill-quiet));
}
[slot='navigation'] wa-icon {
align-items: center;
aspect-ratio: 1;
color: var(--wa-color-brand-fill-loud);
display: flex;
height: var(--flank-size);
justify-content: center;
}
[slot='navigation'] #recent wa-icon {
border-radius: var(--wa-border-radius-xs);
}
[slot='main-header'] {
border-block-start: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
border-inline: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
border-radius: var(--wa-border-radius-l) var(--wa-border-radius-l) 0 0
}
main,
[slot*='main'] {
margin-inline: var(--wa-space-m);
}
main ol li {
padding: var(--wa-space-m);
}
main ol li .wa-flank {
--flank-size: 2rem;
}
main ol li:not(:first-child) {
border-block-start: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
main,
[slot='main-footer'] {
border-inline: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
}
main,
[slot='main-header'] {
background-color: var(--wa-color-surface-raised);
}
#play-controls wa-icon-button::part(base) {
border: var(--wa-border-width-l) var(--wa-border-style) currentColor;
border-radius: var(--wa-border-radius-circle);
font-size: 1.5rem;
}
#play-controls wa-icon-button[name="play"]::part(base) {
background-color: var(--wa-color-brand-fill-loud);
border: none;
color: var(--wa-color-brand-on-loud);
font-size: 3rem;
padding: 0.5em 0.45em 0.5em 0.55em;
}
[slot='main-footer'].wa-grid > * {
max-inline-size: 30ch;
}
</style>
```
## Customization
### Sticky Sections
The following sections of a page are "sticky" by default, meaning they remain in position as the user scrolls.
- `banner`
- `header`
- `sub-header`
- `menu` (`navigation` itself is not sticky, but its parent `menu` is)
- `aside`
This is often desirable, but you can change this behavior using the `disable-sticky` attribute. Use a space-delimited list of names to tell the page which sections should not be sticky.
```html
<wa-page disable-sticky="header aside"> ... </wa-page>
```
### Skip To Content
## Skip To Content
The layout provides a "skip to content" link that's visually hidden until the user tabs into it. You don't have to do anything to configure this, unless you want to change the text displayed in the link. In that case, you can slot in your own text using the `skip-to-content` slot.
@@ -733,44 +70,15 @@ This example localizes the "skip to content" link for German users.
</wa-page>
```
### Responsiveness
## Responsiveness
A page isn't very opinionated when it comes to responsive behaviors, but there are tools in place to help make responsiveness easy.
#### Default Slot Styles
Each slot is a [flex container](https://developer.mozilla.org/en-US/docs/Glossary/Flex_Container) and specifies some flex properties so that your content is reasonably responsive by default.
The following slots specify `justify-content: space-between` and `flex-wrap: wrap` to evenly distribute child elements horizontally and allow them to wrap when space is limited:
- `header`
- `subheader`
- `main-header`
- `main-footer`
- `footer`
The following slots specify `flex-direction: column` to arrange child elements vertically:
- `navigation-header`
- `navigation` (or `menu`)
- `navigation-footer`
- `aside`
And the `banner` slot specifies `justify-content: center` to horizontally center its child elements.
You can override the default display and flex properties for each slot with your own CSS.
#### Responsive Navigation
When you use the `navigation` slot, your slotted content automatically collapses into a drawer on smaller screens.
The breakpoint at which this occurs is `768px` by default, but you can change it using the `mobile-breakpoint` attribute.
The layout component tries not to have too many opinions in terms of responsive behaviors — you get to decide with your own CSS and media queries how your content responds! However, the navigation menu _does_ respond by collapsing on smaller screens. The breakpoint at which this occurs is 768px by default, but you can change it using the `mobile-breakpoint` attribute.
```html
<wa-page mobile-breakpoint="600"> ... </wa-page>
```
By default, a "hamburger" button appears in the `header` slot to toggle the navigation menu on smaller screens.
You can customize what this looks like by slotting your own button in the `toggle-navigation` slot,
or place the `data-toggle-nav` attribute on any button on your page (This _does not_ have to be a Web Awesome element.).
The default button not be shown when using either of these methods — if you want to use multiple navigation toggles on your page, simply add the `data-toggle-nav` attribute to multiple elements.
You can provide a button to toggle the navigation menu anywhere inside the layout by adding the `data-toggle-nav` attribute. (This _does not_ have to be a Web Awesome button.)
```html
<wa-page mobile-breakpoint="600">
@@ -786,44 +94,25 @@ Alternatively, you can apply `nav-state="open"` and `nav-state="closed"` to the
<wa-page nav-state="open"> ... </wa-page>
```
`<wa-page>` is given the attribute `view="mobile"` or `view="desktop"` when the viewport narrower or wider than the `mobile-breakpoint` value, respectively. You can leverage these attributes to change styles depending on the size of the viewport.
This is especially useful to hide your `data-toggle-nav` button when the viewport is wider:
```css
wa-page[view='desktop'] [data-toggle-nav] {
display: none;
}
```
## Providing Navigation Items
#### Custom Widths
- TODO - example with navigation items
- TODO - example with`<h2>` and `<a>` as navigation items
You specify widths for some slots on your page with [CSS custom properties](#css-custom-properties) for `--menu-width`, `--main-width`, and `--aside-width`.
## Examples
If you specify `--menu-width` to apply a specific width to your `navigation` slot, space will still be reserved on the page even below the `mobile-breakpoint`. To collapse this space on smaller screens, add the following code to your styles:
```css
wa-page[view='mobile'] {
--menu-width: auto;
}
```
### Hero Layout
You can use a similar approach for `--aside-width` to hide the `aside` slot on smaller screens. Be sure to also specify `display: none` for the slot:
```css
wa-page[view='mobile'] {
--aside-width: auto;
}
wa-page[view='mobile'] [slot='aside'] {
display: none;
}
```
- TODO - Sticky header + main + footer
### Spacing
### Blog Layout
A page specifies default `padding` within each slot and a `gap` between the slot's direct children. You can drop elements into any slot, and reasonable spacing is already applied for you.
- TODO - Sticky header + main + aside + footer (blog)
You can override the default spacing for each slot with your own CSS. In this example, we're setting custom `gap` and `padding` for the `footer` slot:
```css
[slot="footer"] {
gap: var(--wa-space-xl);
padding: var(--wa-space-xl);
}
```
### App Layout
- TODO - Menu + main, plus maybe headers and footers in each (app)
### Docs Layout
- TODO - Menu + main + aside + footer (docs)

View File

@@ -4,14 +4,14 @@ description: Tab groups organize content into a container that shows one section
layout: component
---
Tab groups make use of [tabs](/docs/components/tab) and [tab panels](/docs/components/tab-panel). Each panel should have a name that's unique within the tab group, and tabs should have a `panel` attribute that points to the respective panel's name.
Tab groups make use of [tabs](/docs/components/tab) and [tab panels](/docs/components/tab-panel). Each tab must be slotted into the `nav` slot and its `panel` must refer to a tab panel of the same name.
```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 slot="nav" panel="general">General</wa-tab>
<wa-tab slot="nav" panel="custom">Custom</wa-tab>
<wa-tab slot="nav" panel="advanced">Advanced</wa-tab>
<wa-tab slot="nav" 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>
@@ -28,9 +28,9 @@ To make a tab active, set the `active` attribute to the name of the appropriate
```html {.example}
<wa-tab-group active="advanced">
<wa-tab panel="general">General</wa-tab>
<wa-tab panel="custom">Custom</wa-tab>
<wa-tab panel="advanced">Advanced</wa-tab>
<wa-tab slot="nav" panel="general">General</wa-tab>
<wa-tab slot="nav" panel="custom">Custom</wa-tab>
<wa-tab slot="nav" panel="advanced">Advanced</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>
@@ -44,10 +44,10 @@ Tabs can be shown on the bottom by setting `placement` to `bottom`.
```html {.example}
<wa-tab-group placement="bottom">
<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 slot="nav" panel="general">General</wa-tab>
<wa-tab slot="nav" panel="custom">Custom</wa-tab>
<wa-tab slot="nav" panel="advanced">Advanced</wa-tab>
<wa-tab slot="nav" 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>
@@ -62,10 +62,10 @@ Tabs can be shown on the starting side by setting `placement` to `start`.
```html {.example}
<wa-tab-group placement="start">
<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 slot="nav" panel="general">General</wa-tab>
<wa-tab slot="nav" panel="custom">Custom</wa-tab>
<wa-tab slot="nav" panel="advanced">Advanced</wa-tab>
<wa-tab slot="nav" 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>
@@ -80,10 +80,10 @@ Tabs can be shown on the ending side by setting `placement` to `end`.
```html {.example}
<wa-tab-group placement="end">
<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 slot="nav" panel="general">General</wa-tab>
<wa-tab slot="nav" panel="custom">Custom</wa-tab>
<wa-tab slot="nav" panel="advanced">Advanced</wa-tab>
<wa-tab slot="nav" 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>
@@ -98,10 +98,10 @@ You can make a tab closable by adding a close button next to the tab and inside
```html {.example}
<wa-tab-group class="tabs-closable">
<wa-tab panel="general">General</wa-tab>
<wa-tab panel="closable">Closable</wa-tab>
<wa-icon-button tabindex="-1" name="xmark" label="Close the closable tab"></wa-icon-button>
<wa-tab panel="closable-2">Advanced</wa-tab>
<wa-tab slot="nav" panel="general">General</wa-tab>
<wa-tab slot="nav" panel="closable">Closable</wa-tab>
<wa-icon-button slot="nav" tabindex="-1" name="xmark" label="Close the closable tab"></wa-icon-button>
<wa-tab slot="nav" panel="closable-2">Advanced</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
<wa-tab-panel name="closable">This is the closable tab panel.</wa-tab-panel>
@@ -123,7 +123,7 @@ You can make a tab closable by adding a close button next to the tab and inside
const tabGroup = document.querySelector('.tabs-closable');
const generalTab = tabGroup.querySelectorAll('wa-tab')[0];
const closableTab = tabGroup.querySelectorAll('wa-tab')[1];
const closeButton = tabGroup.querySelector('wa-icon-button');
const closeButton = tabGroup.querySelector('wa-icon-button[slot="nav"]');
const restoreButton = tabGroup.nextElementSibling.nextElementSibling;
// Remove the tab when the close button is clicked
@@ -148,26 +148,26 @@ When there are more tabs than horizontal space allows, the nav will be scrollabl
```html {.example}
<wa-tab-group>
<wa-tab panel="tab-1">Tab 1</wa-tab>
<wa-tab panel="tab-2">Tab 2</wa-tab>
<wa-tab panel="tab-3">Tab 3</wa-tab>
<wa-tab panel="tab-4">Tab 4</wa-tab>
<wa-tab panel="tab-5">Tab 5</wa-tab>
<wa-tab panel="tab-6">Tab 6</wa-tab>
<wa-tab panel="tab-7">Tab 7</wa-tab>
<wa-tab panel="tab-8">Tab 8</wa-tab>
<wa-tab panel="tab-9">Tab 9</wa-tab>
<wa-tab panel="tab-10">Tab 10</wa-tab>
<wa-tab panel="tab-11">Tab 11</wa-tab>
<wa-tab panel="tab-12">Tab 12</wa-tab>
<wa-tab panel="tab-13">Tab 13</wa-tab>
<wa-tab panel="tab-14">Tab 14</wa-tab>
<wa-tab panel="tab-15">Tab 15</wa-tab>
<wa-tab panel="tab-16">Tab 16</wa-tab>
<wa-tab panel="tab-17">Tab 17</wa-tab>
<wa-tab panel="tab-18">Tab 18</wa-tab>
<wa-tab panel="tab-19">Tab 19</wa-tab>
<wa-tab panel="tab-20">Tab 20</wa-tab>
<wa-tab slot="nav" panel="tab-1">Tab 1</wa-tab>
<wa-tab slot="nav" panel="tab-2">Tab 2</wa-tab>
<wa-tab slot="nav" panel="tab-3">Tab 3</wa-tab>
<wa-tab slot="nav" panel="tab-4">Tab 4</wa-tab>
<wa-tab slot="nav" panel="tab-5">Tab 5</wa-tab>
<wa-tab slot="nav" panel="tab-6">Tab 6</wa-tab>
<wa-tab slot="nav" panel="tab-7">Tab 7</wa-tab>
<wa-tab slot="nav" panel="tab-8">Tab 8</wa-tab>
<wa-tab slot="nav" panel="tab-9">Tab 9</wa-tab>
<wa-tab slot="nav" panel="tab-10">Tab 10</wa-tab>
<wa-tab slot="nav" panel="tab-11">Tab 11</wa-tab>
<wa-tab slot="nav" panel="tab-12">Tab 12</wa-tab>
<wa-tab slot="nav" panel="tab-13">Tab 13</wa-tab>
<wa-tab slot="nav" panel="tab-14">Tab 14</wa-tab>
<wa-tab slot="nav" panel="tab-15">Tab 15</wa-tab>
<wa-tab slot="nav" panel="tab-16">Tab 16</wa-tab>
<wa-tab slot="nav" panel="tab-17">Tab 17</wa-tab>
<wa-tab slot="nav" panel="tab-18">Tab 18</wa-tab>
<wa-tab slot="nav" panel="tab-19">Tab 19</wa-tab>
<wa-tab slot="nav" panel="tab-20">Tab 20</wa-tab>
<wa-tab-panel name="tab-1">Tab panel 1</wa-tab-panel>
<wa-tab-panel name="tab-2">Tab panel 2</wa-tab-panel>
@@ -198,10 +198,10 @@ When focused, keyboard users can press [[Left]] or [[Right]] to select the desir
```html {.example}
<wa-tab-group activation="manual">
<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 slot="nav" panel="general">General</wa-tab>
<wa-tab slot="nav" panel="custom">Custom</wa-tab>
<wa-tab slot="nav" panel="advanced">Advanced</wa-tab>
<wa-tab slot="nav" 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>

View File

@@ -6,10 +6,10 @@ layout: component
```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 slot="nav" panel="general">General</wa-tab>
<wa-tab slot="nav" panel="custom">Custom</wa-tab>
<wa-tab slot="nav" panel="advanced">Advanced</wa-tab>
<wa-tab slot="nav" 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>

View File

@@ -1,69 +0,0 @@
---
title: Viewport Demo
description: Viewport demos can be used to display an iframe as a resizable, zoomable preview.
layout: component
---
```html {.example}
<wa-viewport-demo viewport="1200">
<iframe src="."></iframe>
</wa-viewport-demo>
```
:::warning
A lot of the functionality of this component will not work on cross-origin iframes.
:::
## Examples
### Arbitrary HTML content
You can render arbitrary HTML content in the iframe by using the `srcdoc` attribute:
```html {.example}
<wa-viewport-demo>
<iframe srcdoc="
&lt;button&gt;Click me!&lt;/button&gt;
"></iframe>
</wa-viewport-demo>
```
### Viewport Emulation
You can also provide a width value to emulate and it will be scaled accordingly:
```html {.example}
<wa-viewport-demo viewport="300">
<iframe srcdoc="
&lt;button&gt;Click me!&lt;/button&gt;
&lt;wa-button&gt;Click me!&lt;/wa-button&gt;
"></iframe>
</wa-viewport-demo>
```
By default, the viewport will be rendered to an initial 16:9 aspect ratio,
which can be changed via resizing.
You can customize this via the `--viewport-initial-aspect-ratio` property.
Or, you could add a height value:
```html {.example}
<wa-viewport-demo viewport="1600 x 1000">
<iframe srcdoc="
&lt;button&gt;Click me!&lt;/button&gt;
&lt;p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed maximus et tortor vel ullamcorper. Fusce tristique et justo quis auctor. In tristique dignissim dignissim. Fusce lacus urna, efficitur vel fringilla sed, hendrerit at ipsum. Donec suscipit ante ac ligula imperdiet varius. Aliquam ullamcorper augue sit amet lectus euismod finibus. Proin semper, diam at rhoncus posuere, diam dui semper turpis, ut faucibus mi ipsum nec ante. Morbi varius nibh ut facilisis varius. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce in blandit velit. Aliquam massa eros, commodo eu vestibulum a, faucibus non risus.
&lt;p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed maximus et tortor vel ullamcorper. Fusce tristique et justo quis auctor. In tristique dignissim dignissim. Fusce lacus urna, efficitur vel fringilla sed, hendrerit at ipsum. Donec suscipit ante ac ligula imperdiet varius. Aliquam ullamcorper augue sit amet lectus euismod finibus. Proin semper, diam at rhoncus posuere, diam dui semper turpis, ut faucibus mi ipsum nec ante. Morbi varius nibh ut facilisis varius. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce in blandit velit. Aliquam massa eros, commodo eu vestibulum a, faucibus non risus.
&lt;p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed maximus et tortor vel ullamcorper. Fusce tristique et justo quis auctor. In tristique dignissim dignissim. Fusce lacus urna, efficitur vel fringilla sed, hendrerit at ipsum. Donec suscipit ante ac ligula imperdiet varius. Aliquam ullamcorper augue sit amet lectus euismod finibus. Proin semper, diam at rhoncus posuere, diam dui semper turpis, ut faucibus mi ipsum nec ante. Morbi varius nibh ut facilisis varius. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce in blandit velit. Aliquam massa eros, commodo eu vestibulum a, faucibus non risus.
"></iframe>
</wa-viewport-demo>
```
## Roadmap
This component is a work in progress.
Some of the things that are not yet implemented are listed below.
It goes without saying that this list is a rough plan and subject to change.
- Non-linear zoom scale
- Extend to general content, not just iframes
- Styles for mobile and tablet frames and an attribute to switch between them
- Automatic iframe height

View File

@@ -1,100 +0,0 @@
---
title: Default Layout and Spacing
description: TODO
layout: blank
---
<style>
[slot='banner'] {
background-color: pink;
}
[slot='header'] {
background-color: peachpuff;
}
[slot='subheader'] {
background-color: papayawhip;
}
[slot='navigation-header'] {
background-color: lemonchiffon;
}
[slot='navigation'] {
background-color: honeydew;
}
[slot='navigation-footer'] {
background-color: paleturquoise;
}
[slot='main-header'] {
background-color: lavenderblush;
}
main {
background-color: lavender;
height: 100%;
}
[slot='main-footer'] {
background-color: thistle;
}
[slot='aside'] {
background-color: lightcyan;
height: 100%;
}
[slot='footer'] {
background-color: lightsteelblue;
}
</style>
<wa-page>
<section slot="banner">
<strong>Banner</strong>
<span>Banner</span>
<span>Banner</span>
</section>
<header slot="header">
<strong>Header</strong>
<span>Header</span>
<span>Header</span>
</header>
<nav slot="subheader">
<strong>Subheader</strong>
<span>Subheader</span>
<span>Subheader</span>
</nav>
<nav slot="navigation-header">
<strong>Nav Header</strong>
<span>Nav Header</span>
<span>Nav Header</span>
</nav>
<nav slot="navigation">
<strong>Navigation</strong>
<span>Navigation</span>
<span>Navigation</span>
</nav>
<nav slot="navigation-footer">
<strong>Nav Footer</strong>
<span>Nav Footer</span>
<span>Nav Footer</span>
</nav>
<div slot="main-header">
<strong>Main Header</strong>
<span>Main Header</span>
<span>Main Header</span>
</div>
<main>
<h1>Main</h1>
<p>No flex properties here! The author can specify their own preferred content flow and layout in the default slot.</p>
</main>
<div slot="main-footer">
<strong>Main Footer</strong>
<span>Main Footer</span>
<span>Main Footer</span>
</div>
<aside slot="aside">
<strong>Aside</strong>
<span>Aside</span>
<span>Aside</span>
</aside>
<footer slot="footer">
<strong>Footer</strong>
<span>Footer</span>
<span>Footer</span>
</footer>
</wa-page>

View File

@@ -507,13 +507,13 @@ hasOutline: false
</div>
<wa-select name="theme" label="Pick a theme to start!" value="default">
<wa-option value="default">Default</wa-option>
<wa-option data-alpha="remove" value="fa">Font Awesome</wa-option>
<wa-option data-alpha="remove" value="premium">Premium</wa-option>
<wa-option data-alpha="remove" value="playful">Playful</wa-option>
<wa-option data-alpha="remove" value="brutalist">Brutalist</wa-option>
<wa-option data-alpha="remove" value="migration">Migration</wa-option>
<wa-option data-alpha="remove" value="glassy">Glassy</wa-option>
<wa-option data-alpha="remove" value="active">Active</wa-option>
<wa-option value="fa">Font Awesome</wa-option>
<wa-option value="premium">Premium</wa-option>
<wa-option value="playful">Playful</wa-option>
<wa-option value="brutalist">Brutalist</wa-option>
<wa-option value="migration">Migration</wa-option>
<wa-option value="glassy">Glassy</wa-option>
<wa-option value="active">Active</wa-option>
<wa-option value="classic">Classic</wa-option>
</wa-select>
</div>

View File

@@ -29,7 +29,6 @@ Available translations include:
<div style="columns: 3; gap: 1rem; margin-block-end: 1.5rem;">
- ar
- cs
- da
- de-ch
- de
@@ -37,24 +36,19 @@ Available translations include:
- en
- es
- fa
- fi
- fr
- he
- hr
- hu
- id
- it
- ja
- nb
- nl
- nn
- pl
- pt
- ru
- sl
- sv
- tr
- uk
- zh-cn
- zh-tw

View File

@@ -1,7 +1,7 @@
---
title: App
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description
@@ -285,7 +285,7 @@ TODO Page Description
Showing 1 to 10 of 50 Results
<span>
<wa-button><wa-icon slot="prefix" name="gear" variant="solid"></wa-icon> Prev</wa-button>
<wa-button>Next <wa-icon slot="suffix" name="gear" variant="solid"></wa-icon></wa-button>
<wa-button>Next <wa-icon slot="suffix" name="gear" variant="solid"></wa-icon></wa-button>
</span>
</div>
</wa-card>
@@ -349,7 +349,7 @@ TODO Page Description
wa-card {
width: 100%;
}
div.comment-footer {
display: flex;
@@ -401,7 +401,7 @@ TODO Page Description
grid-column: span 2/ span 2;
margin: 0;
}
}
</style>
```
@@ -446,7 +446,7 @@ TODO Page Description
align-items: center;
}
}
</style>
```
@@ -459,13 +459,13 @@ TODO Page Description
<wa-icon name="database" style="font-size: 64px; margin: var(--wa-flow-spacing) 0 calc(var(--wa-flow-spacing)/ 2);"></wa-icon>
<h4>No DBs</h4>
<p>Get started by creating a database.</p>
</div>
</a>
<style>
.empty-state {
text-decoration: none;
&.dashed .border {
margin: 0 auto;
@@ -605,10 +605,10 @@ TODO Page Description
</div>
</wa-card>
</dl>
</div>
</div>
<style>
.with-icon {
@@ -619,7 +619,7 @@ TODO Page Description
margin-bottom: 0;
wa-card::part(body) {
}
wa-card::part(footer) {
@@ -701,14 +701,14 @@ TODO Page Description
width: 100%;
dl {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(2, 1fr);
margin: 0;
dt {
color: #8991A6;
font-size: 14px;
}
div {
border-right-style: solid;
@@ -739,8 +739,8 @@ TODO Page Description
border: none;
}
}
}
wa-card.with-shared-borders::part(body) {
@@ -758,7 +758,7 @@ TODO Page Description
```html{.example}
<div class="leaderboard">
<h3 style="grid-column: 1/-1">Collective Activity for Yesterday</h3>
<wa-card class="activity-card" style="--wa-color-surface-default: tomato; --wa-color-text-normal: white; grid-column: 1/5;">
<span>
<wa-icon name="book"></wa-icon>
@@ -780,7 +780,7 @@ TODO Page Description
</span>
<div class="leaderboard-number">97,303</div>
</wa-card>
<wa-card class="card-header" with-header style="grid-column: 2/12">
<div slot="header">
<div class="leaderboard-badge">
@@ -802,7 +802,7 @@ TODO Page Description
</li>
<li>
<div>
<span>
<h5 style="--wa-space-xl: 0">Title</h5>
<span style="font-size: x-large;font-weight: 700;">4,500</span>
@@ -1045,4 +1045,4 @@ TODO Page Description
```
### With templates
### With recommendations grid
### With recommendations grid

View File

@@ -1,7 +1,7 @@
---
title: Blog
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description
@@ -14,7 +14,7 @@ TODO Page Description
<wa-carousel-item>
<a href="#" class="hero-link">
<div style="background: #fe53a0;">
</div>
<div style="background: gray;">
<img
@@ -28,7 +28,7 @@ TODO Page Description
<wa-carousel-item>
<a href="#" class="hero-link">
<div style="background: #5a90f3;">
</div>
<div style="background: gray;">
<img
@@ -42,7 +42,7 @@ TODO Page Description
<wa-carousel-item>
<a href="#" class="hero-link">
<div style="background: #8c431e;">
</div>
<div style="background: gray;">
<img
@@ -56,7 +56,7 @@ TODO Page Description
<wa-carousel-item>
<a href="#" class="hero-link">
<div style="background: #37b3e6;">
</div>
<div style="background: gray;">
<img
@@ -70,7 +70,7 @@ TODO Page Description
<wa-carousel-item>
<a href="#" class="hero-link">
<div style="background: #f993d6;">
</div>
<div style="background: gray;">
<img
@@ -81,7 +81,7 @@ TODO Page Description
<h2><span>Article Title</span></h2>
</a>
</wa-carousel-item>
</wa-carousel>
<style>
.hero-link {
@@ -202,10 +202,10 @@ TODO Page Description
img {
margin-right: 1rem;
object-fit: cover;
object-fit: cover;
min-width: 50px;
min-height: 50px;
width: 100px;
min-height: 50px;
width: 100px;
height: 100px;
border-radius: var(--wa-border-radius-circle);
}
@@ -333,7 +333,7 @@ TODO Page Description
<wa-input value="https://fontawesome.com"></wa-input>
<wa-button variant="brand"> <wa-icon slot="prefix" name="link" variant="solid"></wa-icon>Copy</wa-button>
</div>
</div>
</wa-card>
<style>
@@ -350,13 +350,13 @@ TODO Page Description
background: var(--background);
}
wa-icon-button::part(base) {
color: var(--color);
}
.share-input {
display: flex;
wa-input {
--border-radius: var(--wa-form-control-border-radius) 0 0 var(--wa-form-control-border-radius);
}
@@ -365,6 +365,6 @@ TODO Page Description
}
}
}
</style>
```

View File

@@ -1,7 +1,7 @@
---
title: Business
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description

View File

@@ -1,12 +1,12 @@
---
title: E-commerce - Category Filter
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description
## With inline actions and expandable sidebar filters
```html{.example}
```

View File

@@ -1,7 +1,7 @@
---
title: E-commerce - Category Preview
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description

View File

@@ -1,7 +1,7 @@
---
title: E-commerce - Order History
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description
@@ -9,5 +9,5 @@ TODO Page Description
## Invoice panels
```html{.example}
```

View File

@@ -1,7 +1,7 @@
---
title: E-commerce - Product List
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description

View File

@@ -1,7 +1,7 @@
---
title: E-commerce - Product Detail
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description
@@ -35,7 +35,7 @@ TODO Page Description
</wa-card>
</div>
<style>
.with-inline-price {
.with-inline-price {
wa-card {
width: 100%;
.card-header {

View File

@@ -1,7 +1,7 @@
---
title: E-commerce - Product Lists
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description
@@ -52,7 +52,7 @@ TODO Page Description
align-items: center;
}
.grid-item:nth-of-type(odd) {
border-right: var(--wa-panel-border-width) var(--wa-border-style) var(--wa-color-neutral-border-quiet);
}
.grid-item:not(:nth-last-child(-n + 2)) {
@@ -156,10 +156,11 @@ TODO Page Description
font-weight: var(--wa-font-weight-action);
}
}
</style>
```
## With color swatches (WIP)
```html{.example}
```

View File

@@ -1,7 +1,7 @@
---
title: E-commerce - Product Reviews
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description
@@ -132,7 +132,7 @@ TODO Page Description
<wa-icon family="solid" name="earth-americas"></wa-icon>
<h3>International delivery</h3>
<p>Get your order in 2 years</p>
</wa-card>
</div>
</div>
@@ -263,11 +263,11 @@ TODO Page Description
wa-radio-button #shadow-root div .button--medium {
padding: var(--wa-space-xs) var(--wa-space-xs);
}
.color-circle {
--background: #000;
background: var(--background);
width: 50px;
height: 100%;
}
@@ -372,12 +372,12 @@ TODO Page Description
<h2>Everyday Ruck Snack</h2>
<span>
<span>$220</span> |
<wa-rating label="Rating" precision="0.5" value="2.5"></wa-rating>
<wa-rating label="Rating" precision="0.5" value="2.5"></wa-rating>
<span>1624 reviews</span>
</span>
<p>Don't compromise on snack-carrying capacity with this lightweight and spacious bag. The drawstring top keeps all your favorite chips, crisps, fries, biscuits, crackers, and cookies secure.</p>
<span><wa-icon family="solid" name="check"></wa-icon> In stock and ready to ship</span>
</div>
<div class="div-2">
@@ -398,7 +398,7 @@ TODO Page Description
/* height: 1000px; */
/* gap: 1rem; */
.div-1 {
}
.div-2 {
/* background-color: black;
@@ -406,7 +406,7 @@ TODO Page Description
grid-row: span 2 / span 2; */
}
.div-3 {
}
}
</style>
@@ -440,10 +440,10 @@ TODO Page Description
<wa-icon family="brands" name="instagram"></wa-icon>
<wa-icon family="brands" name="x-twitter"></wa-icon>
<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 slot="nav" panel="general">General</wa-tab>
<wa-tab slot="nav" panel="custom">Custom</wa-tab>
<wa-tab slot="nav" panel="advanced">Advanced</wa-tab>
<wa-tab slot="nav" panel="disabled" disabled>Disabled</wa-tab>
<wa-tab-panel name="general">
<div></div>
@@ -461,4 +461,4 @@ TODO Page Description
</div>
```
```

View File

@@ -1,7 +1,7 @@
---
title: E-commerce - Shopping Cart
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description

View File

@@ -1,7 +1,7 @@
---
title: Business
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description

View File

@@ -1,7 +1,7 @@
---
title: Entertainment
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description

View File

@@ -1,7 +1,7 @@
---
title: Membership
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description

View File

@@ -1,7 +1,7 @@
---
title: News
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description

View File

@@ -1,7 +1,7 @@
---
title: Non-profit
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description

View File

@@ -0,0 +1,9 @@
---
title: Business
description: TODO
layout: page.njk
---
TODO Page Description
## Examples

View File

@@ -1,7 +1,7 @@
---
title: Portfolio
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description

View File

@@ -1,7 +1,7 @@
---
title: Product Landing
description: TODO
layout: pattern.njk
layout: page.njk
---
TODO Page Description

View File

@@ -12,31 +12,36 @@ Components with the <wa-badge variant="warning" pill>Experimental</wa-badge> bad
During the alpha period, things might break! We take breaking changes very seriously, but sometimes they're necessary to make the final product that much better. We appreciate your patience!
:::
## 3.0.0-alpha.5
## Next
- Added the Finnish translation
- Added the Italian translation
- Added the Ukrainian translation
- Added support for <kbd>Enter</kbd> to `<wa-split-panel>` to align with ARIA APG's [window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)
- Added support for <kbd>Enter</kbd> to `<sl-split-panel>` to align with ARIA APG's [window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)
- Added more resilient support for lazy loaded options in `<wa-select>`
- Added support for vertical button groups
- Added the `focus()` method to `<wa-radio-group>`
- Fixed a bug in `<wa-dialog>` with scroll locking shifting viewports.
- Fixed a bug in `<wa-dialog>` when using `.show()`
- Fixed a bug in `<wa-rating>` when using `precision`
- Fixed a bug in `<wa-rating>` that allowed tabbing into the rating when readonly
- Fixed a bug in `<wa-relative-time>` where the title attribute would show with redundant info
- Fixed a bug in `<wa-select>` that caused the placeholder to display incorrectly when using placeholder and multiple
- Fixed a bug in `<wa-tooltip>` that caused a memory leak in disconnected elements
- Fixed a bug in `<wa-select>` that prevented label changes in `<wa-option>` from updating the controller
- Fixed a bug in `<wa-carousel>` that caused interactive elements to be activated when dragging
- Fixed a bug in `<wa-tab-group>` that prevented changing tabs by setting `active` on `<wa-tab>` elements
- Fixed a bug in `<wa-tab-group>` that caused an error when removed from the DOM too quickly
- Fixed a bug in `<wa-textarea>` causing scroll jumping when using `resize="auto"`
- Fixed a bug with certain bundlers when using dynamic imports
- Improved alignment of the play icon in `<wa-animated-image>`
- Improved behavior of link buttons to not set `noreferrer noopener` by default
- Updated all checks for directionality to use `this.localize.dir()` instead of `el.matches(:dir(rtl))` so older browsers don't error out
## 3.0.0-alpha.3
- Added [SSR support](/docs/experimental/ssr/) to all components
- Added `scroll-margin-top` to children of `wa-page`
- Added `--scroll-margin-top` css variable `wa-page`
- Fixed form controls to behave like their native counterparts for value and defaultValue properties / attributes respectively.
- Fixed a bug in `<wa-input>` around value attributes and properties to behave like native `<input>`.
- Fixed a bug in `<wa-select>` that made the suffix slot collide with the clear button
- Fixed a bug in `<wa-checkbox>` where unchecking and then checking would "clear" its value.
- Fixed a bug where `<wa-relative-time>` would announce the full time instead of the relative time in screen readers [#22](https://github.com/shoelace-style/webawesome-alpha/issues/22)
- Fixed a bug in `<wa-tab-group>` in Firefox where the overflow container would keep focus. [#14](https://github.com/shoelace-style/webawesome-alpha/issues/14)
- Fixed a bug in `<wa-input>` where `minlength` and `maxlength` were not being properly validated. [#35](https://github.com/shoelace-style/webawesome-alpha/issues/35)
- Fixed a bug in `<wa-carousel>` that made pagination work incorrectly
## 3.0.0-alpha.2
- This is the initial release of Web Awesome alpha!
---
@@ -63,7 +68,7 @@ Here's a list of some of the things that have changed since Shoelace v2. For que
- Changed the `data-optional`, `data-required`, `data-invalid`, `data-valid`, `data-user-invalid`, and `data-user-valid` states to `data-wa-*` prefix to avoid conflicts with user provided attributes
- Changed `<wa-icon>` so icons are no longer fixed width by default to accommodate variable width icons
- Changed `<wa-radio>` from `display: block;` to `display: inline-block`
- Changed `<wa-tab-group>` to implement a "roving tabindex" and `<wa-tab>` is no longer tabbable by default. This aligns closer to the APG pattern for tabs [#2041]
- Changed `<wa-tab-group>` to implement a "roving tabindex" and `<wa-tab>` is no longer tabbable by default. This aligns closer to the APG pattern for tabs. [#2041]
- Changed `<wa-tooltip>` to no longer wrap content due to accessibility and styling issues. Tooltips are now associated using the `for` attribute + an `id` on the trigger [#123]
- Improved `<wa-spinner>` so it doesn't wobble when zooming in Safari
- Improved submenu selection by implementing the [safe triangle](https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles/) method [#1550]

View File

@@ -97,7 +97,7 @@ The Web Awesome documentation uses an extended version of [markdown-it](https://
#### Code Previews
To render a code preview, use the standard code field syntax and add a class of `example`:
To render a code preview, use the standard code field syntax and append `:preview` to the language.
````md
```html {.example}
@@ -105,11 +105,7 @@ To render a code preview, use the standard code field syntax and add a class of
```
````
You can add additional modifiers as classes or attributes.
For example, `.open` to expand the code by default.
The order of these modifiers doesn't matter, but no spaces should exist between them.
Class name modifiers are turned on by simply using their name as a class (e.g. `open` to expand the code by default),
and turned off by using `no-` followed by the class name (e.g. `no-edit` to hide the edit button).
You can also append `.open` to expand the code by default, and `.no-edit` to disable the CodePen button. The order of these modifiers doesn't matter, but no spaces should exist between the language and the modifiers.
````md
```html {.example .open .no-edit}
@@ -117,23 +113,12 @@ and turned off by using `no-` followed by the class name (e.g. `no-edit` to hide
```
````
the class modifiers currently supported are:
- `open` - expands the code (default: true for the first code example in the page, false for all others)
- `new` - Uses `<wa-code-demo>` (default: true). Disable to use the old, non-component demo code.
- `edit` - Enable the CodePen button (default: true) _(old only)_
The `viewport` and `include` attributes of [`<wa-code-demo>`](../components/code-demo/) can also be specified.
By default, `include` is set to `link[rel=stylesheet]` to include all stylesheets on the page for non-isolated demos,
and `link[rel=stylesheet][href^="/dist/"]` for isolated demos.
Attributes are specified as described in the [`markdown-it-attrs` documentation](https://www.npmjs.com/package/markdown-it-attrs).
This particular syntax was chosen for a few reasons:
1. It's easy to remember
2. It works out of the box with markdown-it
3. It appears to have the best support across editors and previewers (the language is usually highlighted correctly)
#### Callouts
Special callouts can be added using the following syntax.
@@ -401,4 +386,4 @@ or for hydrated rendering only:
```bash
SSR_ONLY="true" npm run test
```
```

View File

@@ -65,7 +65,7 @@ Lightness values on this scale have a strong correlation to [relative luminance]
- A difference of 50 ensures a minimum 4.5:1 contrast ratio, suitable for normal text (AA) and large text (AAA)
- A difference of 60 ensures a minimum 7:1 contrast ratio, suitable for all text (AAA)
Web Awesome defines seven literal colors each with 11 lightness values using the format `--wa-color-{hue}-{tint}`.
Web Awesome defines seven literal colors each with 11 lightness values using the format `--wa-color-{name}-{#}`.
<div class="color-name">Red</div>
<ul class="color-group">
@@ -549,4 +549,4 @@ Finally, each color is named according to how much attention it draws. Here, we
swatch.appendChild(copyButton)
})
</script>
</script>

View File

@@ -4,8 +4,6 @@ description: Build better with Web Awesome, the open source library of web compo
layout: page
---
<style>
.title,
.anchor-heading a,
@@ -209,7 +207,7 @@ layout: page
& > * + * {
flex-grow: 1;
}
& wa-callout,
& wa-callout::part(base),
& wa-button::part(base) {
height: 100%;
width: 100%;
@@ -388,4 +386,4 @@ layout: page
&copy; Fonticons, Inc.
</div>
</footer>
</div>
</div>

909
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@shoelace-style/webawesome",
"description": "A forward-thinking library of web components.",
"version": "3.0.0-alpha.5",
"version": "3.0.0-alpha.4",
"homepage": "https://webawesome.com/",
"author": "Web Awesome",
"license": "MIT",
@@ -18,13 +18,10 @@
"./dist/custom-elements.json": "./dist/custom-elements.json",
"./dist/webawesome.js": "./dist/webawesome.js",
"./dist/webawesome.loader.js": "./dist/webawesome.loader.js",
"./dist/themes": "./dist/themes",
"./dist/themes/*": "./dist/themes/*",
"./dist/components": "./dist/components",
"./dist/components/*": "./dist/components/*",
"./dist/react": "./dist/react/index.js",
"./dist/react/*": "./dist/react/*",
"./dist/translations": "./dist/translations",
"./dist/translations/*": "./dist/translations/*"
},
"files": [
@@ -101,7 +98,6 @@
"del": "^7.1.0",
"download": "^8.0.0",
"esbuild": "^0.19.4",
"esbuild-plugin-lit-css": "^3.0.1",
"esbuild-plugin-replace": "^1.4.0",
"eslint": "^8.51.0",
"eslint-plugin-chai-expect": "^3.0.0",

View File

@@ -10,7 +10,6 @@ import browserSync from 'browser-sync';
import chalk from 'chalk';
import copy from 'recursive-copy';
import esbuild from 'esbuild';
import { litCssPlugin } from 'esbuild-plugin-lit-css';
import getPort, { portNumbers } from 'get-port';
import ora from 'ora';
import process from 'process';
@@ -107,11 +106,10 @@ async function generateStyles() {
// NOTE - alpha setting omits all stylesheets except for these because we use them in the docs
if (isAlpha) {
await copy(join(rootDir, 'src/themes/applied.css'), join(cdnDir, 'themes/applied.css'), { overwrite: true });
await copy(join(rootDir, 'src/themes/classic.css'), join(cdnDir, 'themes/classic.css'), { overwrite: true });
await copy(join(rootDir, 'src/themes/color_standard.css'), join(cdnDir, 'themes/color_standard.css'), {
overwrite: true
});
await copy(join(rootDir, 'src/themes/default.css'), join(cdnDir, 'themes/default.css'), { overwrite: true });
await copy(join(rootDir, 'src/themes/forms.css'), join(cdnDir, 'themes/forms.css'), { overwrite: true });
await copy(join(rootDir, 'src/themes/layout.css'), join(cdnDir, 'themes/layout.css'), { overwrite: true });
await copy(join(rootDir, 'src/themes/utilities.css'), join(cdnDir, 'themes/utilities.css'), { overwrite: true });
} else {
await copy(join(rootDir, 'src/themes'), join(cdnDir, 'themes'), { overwrite: true });
}
@@ -172,7 +170,7 @@ async function generateBundle() {
bundle: true,
splitting: true,
minify: false,
plugins: [replace({ __WEBAWESOME_VERSION__: version }), litCssPlugin()]
plugins: [replace({ __WEBAWESOME_VERSION__: version })]
};
const unbundledConfig = {
@@ -290,18 +288,12 @@ if (isDeveloping) {
callbacks: {
ready: (_err, instance) => {
// 404 errors
instance.addMiddleware('*', async (req, res) => {
instance.addMiddleware('*', (req, res) => {
if (req.url.toLowerCase().endsWith('.svg')) {
// Make sure SVGs error out in dev instead of serve the 404 page
res.writeHead(404);
} else {
try {
const notFoundTemplate = await readFile(join(siteDir, '404.html'), 'utf-8');
res.writeHead(404);
res.write(notFoundTemplate || 'Page Not Found');
} catch {
// We're probably disconnected for some reason, so fail gracefully
}
res.writeHead(302, { location: '/404.html' });
}
res.end();

View File

@@ -183,6 +183,22 @@ describe('<wa-button>', () => {
expect(el.shadowRoot!.querySelector('button')).not.to.exist;
});
it('should render a link with rel="noreferrer noopener" when target is set and rel is not', async () => {
const el = await fixture<WaButton>(html`
<wa-button href="https://example.com/" target="_blank">Link</wa-button>
`);
const link = el.shadowRoot!.querySelector('a')!;
expect(link?.getAttribute('rel')).to.equal('noreferrer noopener');
});
it('should render a link with rel="" when a target is provided and rel is empty', async () => {
const el = await fixture<WaButton>(html`
<wa-button href="https://example.com/" target="_blank" rel="">Link</wa-button>
`);
const link = el.shadowRoot!.querySelector('a')!;
expect(link?.getAttribute('rel')).to.equal('');
});
it(`should render a link with a custom rel when a custom rel is provided`, async () => {
const el = await fixture<WaButton>(html`
<wa-button href="https://example.com/" target="_blank" rel="1">Link</wa-button>

View File

@@ -1,64 +0,0 @@
:host {
--icon-color: currentColor;
--icon-size: var(--wa-font-size-l);
--spacing: var(--wa-space-m);
position: relative;
display: flex;
align-items: stretch;
border-radius: var(--wa-panel-border-radius);
background-color: var(--background-color);
border-color: var(--border-color);
border-style: var(--wa-panel-border-style);
border-width: var(--wa-panel-border-width);
color: var(--content-color);
padding: var(--spacing);
}
:host([variant='brand']) {
--background-color: var(--wa-color-brand-fill-quiet);
--border-color: var(--wa-color-brand-border-quiet);
--content-color: var(--wa-color-brand-on-normal);
}
:host([variant='success']) {
--background-color: var(--wa-color-success-fill-quiet);
--border-color: var(--wa-color-success-border-quiet);
--content-color: var(--wa-color-success-on-normal);
}
:host([variant='neutral']) {
--background-color: var(--wa-color-neutral-fill-quiet);
--border-color: var(--wa-color-neutral-border-quiet);
--content-color: var(--wa-color-neutral-on-normal);
}
:host([variant='warning']) {
--background-color: var(--wa-color-warning-fill-quiet);
--border-color: var(--wa-color-warning-border-quiet);
--content-color: var(--wa-color-warning-on-normal);
}
:host([variant='danger']) {
--background-color: var(--wa-color-danger-fill-quiet);
--border-color: var(--wa-color-danger-border-quiet);
--content-color: var(--wa-color-danger-on-normal);
}
[part~='icon'] {
flex: 0 0 auto;
display: flex;
align-items: center;
color: var(--icon-color);
font-size: var(--icon-size);
::slotted(*) {
margin-inline-end: var(--spacing);
}
}
[part~='message'] {
flex: 1 1 auto;
display: block;
overflow: hidden;
}

View File

@@ -0,0 +1,80 @@
import { css } from 'lit';
export default css`
:host {
--border-radius: var(--wa-panel-border-radius);
--border-style: var(--wa-panel-border-style);
--border-width: var(--wa-panel-border-width);
--icon-color: currentColor;
--icon-size: var(--wa-font-size-l);
--spacing: var(--wa-space-m);
display: contents;
/* For better DX, we'll reset the margin here so the base part can inherit it */
margin: 0;
}
:host([variant='brand']) {
--background-color: var(--wa-color-brand-fill-quiet);
--border-color: var(--wa-color-brand-border-quiet);
--content-color: var(--wa-color-brand-on-normal);
}
:host([variant='success']) {
--background-color: var(--wa-color-success-fill-quiet);
--border-color: var(--wa-color-success-border-quiet);
--content-color: var(--wa-color-success-on-normal);
}
:host([variant='neutral']) {
--background-color: var(--wa-color-neutral-fill-quiet);
--border-color: var(--wa-color-neutral-border-quiet);
--content-color: var(--wa-color-neutral-on-normal);
}
:host([variant='warning']) {
--background-color: var(--wa-color-warning-fill-quiet);
--border-color: var(--wa-color-warning-border-quiet);
--content-color: var(--wa-color-warning-on-normal);
}
:host([variant='danger']) {
--background-color: var(--wa-color-danger-fill-quiet);
--border-color: var(--wa-color-danger-border-quiet);
--content-color: var(--wa-color-danger-on-normal);
}
.callout {
position: relative;
display: flex;
align-items: stretch;
background-color: var(--background-color);
border-color: var(--border-color);
border-radius: var(--border-radius);
border-style: var(--border-style);
border-width: var(--border-width);
color: var(--content-color);
font: inherit;
padding: var(--spacing);
margin: inherit;
}
.callout__icon {
flex: 0 0 auto;
display: flex;
align-items: center;
color: var(--icon-color);
font-size: var(--icon-size);
}
.callout__icon ::slotted(*) {
margin-inline-end: var(--spacing) !important;
}
.callout__message {
flex: 1 1 auto;
display: block;
overflow: hidden;
}
`;

View File

@@ -15,7 +15,9 @@ describe('<wa-callout>', () => {
await customElements.whenDefined('wa-callout');
await callout.updateComplete;
expect(callout).to.have.attribute('variant', variant);
const base = callout.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
expect(base).to.have.class(`callout--${variant}`);
// @TODO: For some reason this fails only in CI. I have no clue why. I tested this scenario on the real site, and it works as expected. [Konnor]
if (fixture.type === 'ssr-client-hydrated') {

View File

@@ -1,7 +1,8 @@
import { classMap } from 'lit/directives/class-map.js';
import { customElement, property } from 'lit/decorators.js';
import { html } from 'lit';
import componentStyles from '../../styles/component.styles.js';
import styles from './callout.css';
import styles from './callout.style.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
@@ -14,12 +15,19 @@ import type { CSSResultGroup } from 'lit';
* @slot - The callout's main content.
* @slot icon - An icon to show in the callout. Works best with `<wa-icon>`.
*
* @csspart base - The component's base wrapper.
* @csspart icon - The container that wraps the optional icon.
* @csspart message - The container that wraps the callout's main content.
*
* @cssproperty --background-color - The callout's background color.
* @cssproperty --border-color - The color of the callout's border.
* @cssproperty --border-radius - The radius of the callout's corners.
* @cssproperty --border-style - The style of the callout's borders.
* @cssproperty --border-width - The width of the callout's borders.
* @cssproperty --content-color - The color of the callout's content.
* @cssproperty --icon-color - The color of the callout's icon.
* @cssproperty --icon-size - The size of the callout's icon.
* @cssproperty --spacing - The amount of space around and between the callout's content. Expects a single value. If you want different spacing around and between the content, use `padding` on the callout itself.
* @cssproperty --spacing - The amount of space around and between the callout's content. Expects a single value.
*/
@customElement('wa-callout')
export default class WaCallout extends WebAwesomeElement {
@@ -30,12 +38,24 @@ export default class WaCallout extends WebAwesomeElement {
render() {
return html`
<div part="icon">
<slot name="icon"></slot>
</div>
<div
part="base"
class=${classMap({
callout: true,
'callout--brand': this.variant === 'brand',
'callout--success': this.variant === 'success',
'callout--neutral': this.variant === 'neutral',
'callout--warning': this.variant === 'warning',
'callout--danger': this.variant === 'danger'
})}
>
<div part="icon" class="callout__icon">
<slot name="icon"></slot>
</div>
<div part="message">
<slot></slot>
<div part="message" class="callout__message">
<slot></slot>
</div>
</div>
`;
}

View File

@@ -67,8 +67,7 @@ describe('<wa-carousel>', () => {
});
});
// TODO - this test is hanging the test runner, but autoplay was verified manually to work
it.skip('should scroll forwards every `autoplay-interval` milliseconds', async () => {
it('should scroll forwards every `autoplay-interval` milliseconds', async () => {
// Arrange
const el = await fixture<WaCarousel>(html`
<wa-carousel autoplay autoplay-interval="10">

View File

@@ -102,7 +102,6 @@ export default class WaCarousel extends WebAwesomeElement {
private dragStartPosition: [number, number] = [-1, -1];
private readonly localize = new LocalizeController(this);
private mutationObserver: MutationObserver;
private pendingSlideChange = false;
connectedCallback(): void {
super.connectedCallback();
@@ -176,7 +175,7 @@ export default class WaCarousel extends WebAwesomeElement {
private handleKeyDown(event: KeyboardEvent) {
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
const target = event.target as HTMLElement;
const isRtl = this.localize.dir() === 'rtl';
const isRtl = this.matches(':dir(rtl)');
const isFocusInPagination = target.closest('[part~="pagination-item"]') !== null;
const isNext =
event.key === 'ArrowDown' || (!isRtl && event.key === 'ArrowRight') || (isRtl && event.key === 'ArrowLeft');
@@ -286,9 +285,6 @@ export default class WaCarousel extends WebAwesomeElement {
@eventOptions({ passive: true })
private handleScroll() {
this.scrolling = true;
if (!this.pendingSlideChange) {
this.synchronizeSlides();
}
}
/** @internal Synchronizes the slides with the IntersectionObserver API. */
@@ -306,29 +302,18 @@ export default class WaCarousel extends WebAwesomeElement {
const firstIntersecting = entries.find(entry => entry.isIntersecting);
if (!firstIntersecting) {
return;
}
const slidesWithClones = this.getSlides({ excludeClones: false });
const slidesCount = this.getSlides().length;
// Update the current index based on the first visible slide
const slideIndex = slidesWithClones.indexOf(firstIntersecting.target as WaCarouselItem);
// Normalize the index to ignore clones
const normalizedIndex = this.loop ? slideIndex - this.slidesPerPage : slideIndex;
if (firstIntersecting) {
// Set the index to the closest "snappable" slide
this.activeSlide =
(Math.ceil(normalizedIndex / this.slidesPerMove) * this.slidesPerMove + slidesCount) % slidesCount;
if (this.loop && firstIntersecting.target.hasAttribute('data-clone')) {
const clonePosition = Number(firstIntersecting.target.getAttribute('data-clone'));
// Scrolls to the original slide without animating, so the user won't notice that the position has changed
this.goToSlide(clonePosition, 'instant');
} else {
const slides = this.getSlides();
if (!this.scrolling) {
if (this.loop && firstIntersecting.target.hasAttribute('data-clone')) {
const clonePosition = Number(firstIntersecting.target.getAttribute('data-clone'));
// Scrolls to the original slide without animating, so the user won't notice that the position has changed
this.goToSlide(clonePosition, 'instant');
}
// Update the current index based on the first visible slide
const slideIndex = slides.indexOf(firstIntersecting.target as WaCarouselItem);
// Set the index to the first "snappable" slide
this.activeSlide = Math.ceil(slideIndex / this.slidesPerMove) * this.slidesPerMove;
}
}
},
@@ -349,8 +334,6 @@ export default class WaCarousel extends WebAwesomeElement {
this.synchronizeSlides();
this.scrolling = false;
this.pendingSlideChange = false;
this.synchronizeSlides();
}
private isCarouselItem(node: Node): node is WaCarouselItem {
@@ -420,7 +403,7 @@ export default class WaCarousel extends WebAwesomeElement {
}
@watch('activeSlide')
handleSlideChange() {
handelSlideChange() {
const slides = this.getSlides();
slides.forEach((slide, i) => {
slide.classList.toggle('--is-active', i === this.activeSlide);
@@ -501,7 +484,7 @@ export default class WaCarousel extends WebAwesomeElement {
: clamp(index, 0, slides.length - slidesPerPage);
this.activeSlide = newActiveSlide;
const isRtl = this.localize.dir() === 'rtl';
const isRtl = this.matches(':dir(rtl)');
// Get the index of the next slide. For looping carousel it adds `slidesPerPage`
// to normalize the starting index in order to ignore the first nth clones.
@@ -518,35 +501,17 @@ export default class WaCarousel extends WebAwesomeElement {
}
private scrollToSlide(slide: HTMLElement, behavior: ScrollBehavior = 'smooth') {
// Since the geometry doesn't happen until rAF, we don't know if we'll be scrolling or not...
// It's best to assume that we will and cleanup in the else case below if we didn't need to
this.pendingSlideChange = true;
window.requestAnimationFrame(() => {
// This can happen if goToSlide is called before the scroll container is rendered
// We will have correctly set the activeSlide in goToSlide which will get picked up when initializeSlides is called.
if (!this.scrollContainer) {
return;
}
const scrollContainer = this.scrollContainer;
const scrollContainerRect = scrollContainer.getBoundingClientRect();
const nextSlideRect = slide.getBoundingClientRect();
const scrollContainer = this.scrollContainer;
const scrollContainerRect = scrollContainer.getBoundingClientRect();
const nextSlideRect = slide.getBoundingClientRect();
const nextLeft = nextSlideRect.left - scrollContainerRect.left;
const nextTop = nextSlideRect.top - scrollContainerRect.top;
const nextLeft = nextSlideRect.left - scrollContainerRect.left;
const nextTop = nextSlideRect.top - scrollContainerRect.top;
if (nextLeft || nextTop) {
// This is here just in case someone set it back to false
// between rAF being requested and the callback actually running
this.pendingSlideChange = true;
scrollContainer.scrollTo({
left: nextLeft + scrollContainer.scrollLeft,
top: nextTop + scrollContainer.scrollTop,
behavior
});
} else {
this.pendingSlideChange = false;
}
scrollContainer.scrollTo({
left: nextLeft + scrollContainer.scrollLeft,
top: nextTop + scrollContainer.scrollTop,
behavior
});
}
@@ -567,7 +532,7 @@ export default class WaCarousel extends WebAwesomeElement {
}
// We can't rely on `this.matches()` on the server.
const isRTL = isServer ? this.dir === 'rtl' : this.localize.dir() === 'rtl';
const isRTL = isServer ? this.dir === 'rtl' : this.matches(':dir(rtl)');
return html`
<div part="base" class="carousel">

View File

@@ -13,7 +13,6 @@ export default css`
--border-style: var(--wa-border-style);
--border-width: var(--wa-form-control-border-width);
--box-shadow: none;
--checked-icon-color: var(--wa-color-brand-on-loud);
--toggle-size: calc(1em * var(--wa-form-control-value-line-height));
display: inline-block;
@@ -91,7 +90,7 @@ export default css`
/* Checked/indeterminate */
.checkbox--checked .checkbox__control,
.checkbox--indeterminate .checkbox__control {
color: var(--checked-icon-color);
color: var(--wa-color-brand-on-loud);
border-color: var(--border-color-checked);
background-color: var(--background-color-checked);
}

View File

@@ -51,7 +51,6 @@ import type { CSSResultGroup, PropertyValues } from 'lit';
* @cssproperty --border-style - The style of the checkbox's borders.
* @cssproperty --border-width - The width of the checkbox's borders. Expects a single value.
* @cssproperty --box-shadow - The shadow effects around the edges of the checkbox.
* @cssproperty --checked-icon-color - The color of the checkbox's icon.
* @cssproperty --toggle-size - The size of the checkbox.
*/
@customElement('wa-checkbox')

View File

@@ -1,197 +0,0 @@
import { css } from 'lit';
export default css`
:host {
--preview-background: var(--wa-color-surface-default, canvas);
--preview-backdrop: var(--wa-color-surface-lowered, rgb(0 0 0 / 0.25));
--preview-resize: inline;
--preview-min-width: 10em;
--preview-max-width: 100%;
--preview-padding: var(--wa-space-2xl, 2rem);
--divider-width: var(--wa-border-width-s, 1px);
--viewport-initial-aspect-ratio: 16 / 9;
--viewport-bezel-width: 0.25em;
--code-expand-duration: var(--wa-transition-fast, 0.3s);
--code-collapse-duration: var(--wa-transition-normal, 0.3s);
display: flex;
flex-flow: column;
border: var(--wa-border-style) var(--wa-panel-border-width) var(--wa-color-neutral-border-quiet);
border-radius: var(--wa-code-demo-rounding, var(--wa-border-radius-m));
color: var(--wa-color-text-normal);
margin-block-end: var(--wa-flow-spacing);
background: var(--preview-backdrop);
interpolate-size: allow-keywords;
}
/* Different defaults for isolated demos */
:host([viewport]) {
--preview-resize: none; /* handled by wa-viewport-demo */
--preview-padding: var(--wa-space-l, 1rem);
}
#preview {
display: block;
padding: var(--preview-padding);
border-block-end: inherit;
border-block-end-width: var(--divider-width);
border-start-start-radius: inherit;
border-start-end-radius: inherit;
background: var(--preview-background);
resize: var(--preview-resize);
contain: inline-size; /* Safari chokes on scaled down viewports without this */
:host(:not([viewport])) & {
max-width: min(var(--preview-max-width), 100%);
min-width: var(--preview-min-width);
overflow: auto;
@container style(--preview-resize: none) {
overflow: visible;
}
}
> :first-child {
margin-block-start: 0;
}
> :last-child {
margin-block-end: 0;
}
}
wa-viewport-demo + slot[name='preview'].has-slotted {
display: none;
}
#source {
border-block-end: inherit;
overflow: hidden;
transition-property: height, display;
transition-behavior: allow-discrete;
display: block;
&::slotted(pre) {
position: relative;
border-radius: 0 !important;
margin: 0;
white-space: normal;
}
&:has(+ #buttons) {
border-end-start-radius: 0;
border-end-end-radius: 0;
}
&:not(:has(+ #buttons)) {
border-bottom: none;
}
/* Collapsed */
&:not(:host([open]) *) {
height: 0px;
display: none;
}
/* Expanded */
&:is(:host([open]) *) {
height: auto;
display: block;
}
}
[part~='toggle-button'] wa-icon {
transition-property: rotate;
&:is(:host([open]) *) {
rotate: 180deg;
}
}
#source,
[part~='toggle-button'] wa-icon {
&:not(:host([open]) *) {
transition-duration: var(--code-collapse-duration);
}
&:is(:host([open]) *) {
transition-duration: var(--code-expand-duration);
}
}
#buttons {
display: flex;
align-items: stretch;
background: var(--controls-background, var(--wa-color-surface-default, canvas));
border-end-start-radius: inherit;
border-end-end-radius: inherit;
border: inherit;
/* so that we don't get a visible border
border-style: none would be better but it affects how the others cascade :(
*/
border-width: 0;
button {
--padding-block: 0.5em;
--padding-inline: 1.5em;
all: unset;
padding-block: var(--padding-block);
padding-inline: var(--padding-inline);
cursor: pointer;
white-space: nowrap;
font-size: 0.875rem;
color: var(--wa-color-text-quiet);
text-align: center;
&:not(#preview:active ~ #buttons *) {
/* Interactive states should not apply while the preview is being resized */
&:hover {
background: oklab(from var(--wa-color-surface-lowered, rgb(0 0 0 / 0.05)) l a b / 50%);
}
&:active {
box-shadow: var(--wa-shadow-s) inset;
padding-block: calc(var(--padding-block) + 1px) calc(var(--padding-block) - 1px);
}
}
&:first-child {
/* bottom left in en */
border-end-start-radius: inherit;
}
&:last-child {
/* bottom right in en */
border-end-end-radius: inherit;
}
&:not(:first-child) {
/* bottom left in en */
border-end-start-radius: 0;
border-inline-start: inherit;
border-inline-start-width: var(--divider-width);
}
&:not(:last-child) {
/* bottom right in en */
border-end-end-radius: 0;
}
&:focus-visible {
outline: var(--wa-focus-ring);
}
&[part~='toggle-button'] {
flex: 1;
}
}
wa-icon {
width: 1em;
height: 1em;
vertical-align: -0.1em;
}
}
`;

View File

@@ -1,376 +0,0 @@
import '../icon/icon.js';
import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, query } from 'lit/decorators.js';
import { getInnerHTML, HasSlotController } from '../../internal/slot.js';
import { html } from 'lit';
import { viewportPropertyConverter } from '../viewport-demo/viewport-demo.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './code-demo.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup, TemplateResult } from 'lit';
import type { ViewportDimensions } from '../viewport-demo/viewport-demo.js';
interface DemoHTMLOptions {
/**
* If true, will only start watching after the initial update/render
*/
type?: string;
isolated?: boolean;
absolutize?: boolean | string | URL;
prettyWhitespace?: boolean;
}
const URL_ATTRIBUTES = ['src', 'href'];
/**
* @summary Code demos can be used to render code examples as inline live demos.
* @documentation https://backers.webawesome.com/docs/components/code-demo
* @status experimental
* @since 3.0
*
* @dependency wa-viewport-demo
* @dependency wa-icon
*
* @slot - The main code example (usually a `<pre>` element).
* @slot preview - One or more custom elements to display as the code example preview.
*
* @csspart preview - The container of the code example preview.
* @csspart controls - The container of the control buttons.
* @csspart button - The control buttons.
* @csspart open-button - The open in new tab button.
* @csspart toggle-button - The toggle button.
* @csspart edit-button - The edit button.
* @csspart iframe - The iframe that contains the preview (in isolated demos).
* @csspart viewport-demo - The viewport demo container (in isolated demos).
* @csspart viewport-controls - The viewport demo controls (in isolated demos).
*
* @cssproperty --preview-backdrop - The color behind the preview, shown when it is resized
* @cssproperty --preview-background - The background color of the preview.
* @cssproperty --preview-padding - The padding used for the preview. Defaults to `var(--wa-space-2xl)`.
* @cssproperty --preview-resize - The CSS `resize` property value used for the preview. Default: `inline`, for horizontal resizing.
* @cssproperty --viewport-initial-aspect-ratio - The initial aspect ratio of the viewport, when the `viewport` attribute is used. Defaults to `16 / 9`.
* @cssproperty --preview-max-width - The maximum width of the preview. Defaults to `100%`.
* @cssproperty --preview-min-width - The minimum width of the preview. Defaults to `4em`.
* @cssproperty --divider-width - The width of the divider. Defaults to `var(--wa-border-width-s)`.
* @cssproperty --code-collapse-duration - The duration of the code collapse animation (for supporting browsers). Defaults to `var(--wa-transition-normal)`.
*
*/
@customElement('wa-code-demo')
export default class WaCodeDemo extends WebAwesomeElement {
static styles: CSSResultGroup = [componentStyles, styles];
@query('slot[name=preview]')
private previewSlot: HTMLSlotElement;
/** Opens the code example */
@property({ attribute: 'open', type: Boolean, reflect: true }) open = false;
/** Renders in an iframe */
@property({
reflect: true,
converter: viewportPropertyConverter
})
viewport?: boolean | ViewportDimensions;
/** Includes resources and other elements in the preview */
@property({ reflect: true }) include?: string;
private readonly hasSlotController = new HasSlotController(this, 'preview');
render() {
// NOTE We don't want to render the contents of the code element anywhere if a custom preview is provided.
// That way, providing a custom preview can also be used to sanitize the code.
const code = this.getDemoHTML({ type: 'preview' });
let viewportHTML: string | TemplateResult = '';
if (this.viewport) {
// Viewport emulation
viewportHTML = html`
<wa-viewport-demo .viewport=${this.viewport} part="viewport-demo" exportparts="controls:viewport-controls">
<iframe title="Code preview" srcdoc="${code}" part="iframe"></iframe>
</wa-viewport-demo>
`;
}
const customPreview = this.hasUpdated ? this.hasSlotController.test('preview') : true;
return html`
<div id="preview" part="preview">
${viewportHTML}
<slot
name="preview"
class=${classMap({ 'has-slotted': customPreview })}
@slotchange=${this.handleSlotChange}
.innerHTML=${customPreview || this.viewport ? '' : code}
></slot>
</div>
<slot id="source"></slot>
<div id="buttons" part="controls">
<button
type="button"
part="toggle-button button"
aria-expanded="${this.open ? 'true' : 'false'}"
aria-controls="source"
@click=${this.toggle}
>
Code
<wa-icon name="chevron-down"></wa-icon>
</button>
${this.viewport
? html`<button type="button" part="open-button button" @click=${this.openInNewTab}>
<wa-icon name="arrow-up-right-from-square"></wa-icon>
Open
</button>`
: ''}
<button type="button" part="edit-button button" @click=${this.edit}>
<wa-icon name="pen-to-square"></wa-icon>
Edit
</button>
</div>
`;
}
// TODO memoize this and only update if:
// - this.include changes
//- elements have been added/removed that match the selector
public getIncludedHTML({ isolated = Boolean(this.viewport), absolutize, prettyWhitespace }: DemoHTMLOptions = {}):
| string
| null {
if (!this.ownerDocument) {
return null;
}
const selectors = ['.wa-code-demo-include'];
if (isolated) {
selectors.push('.wa-code-demo-include-isolated');
}
if (this.include) {
selectors.push(this.include);
}
const elements = recursiveQSA(selectors.join(', '), this);
return Array.from(elements, (el: Element) => {
const isTemplate = el.nodeName === 'TEMPLATE';
let source = el;
if (absolutize) {
// Absolutize URLs. Useful for opening in a new tab or code playgrounds.
const base = absolutize ? location.href : absolutize;
source = source.cloneNode(true) as Element;
absolutizeURLs(isTemplate ? (source as HTMLTemplateElement).content : source, base);
}
if (isTemplate) {
let ret = (source as HTMLTemplateElement).innerHTML;
if (prettyWhitespace) {
ret = dedent(ret);
}
return ret;
}
return source.outerHTML;
}).join('\n');
}
public getDemoHTML(options: DemoHTMLOptions = {}): string | null {
let code;
const customPreview = this.hasUpdated ? this.hasSlotController.test('preview') : true;
if (options.type === 'preview' && customPreview && this.previewSlot) {
code = getHTML(this.previewSlot.assignedNodes({ flatten: true }));
} else {
code = this.querySelector?.('code')?.textContent ?? this.textContent;
}
const includedHTML = this.getIncludedHTML(options);
if (includedHTML) {
return includedHTML + '\n\n' + code;
}
return code;
}
private handleSlotChange(e: Event) {
const slot = e.target as HTMLSlotElement;
if (slot.name === 'preview' && !this.viewport) {
const assignedNodes = slot.assignedNodes();
for (const node of assignedNodes) {
// Unwrap templates
// FIXME this will mess up the order of the nodes if there are mixed templates & regular nodes
if (node.nodeName === 'TEMPLATE') {
const content = (node as HTMLTemplateElement).content;
const clone = content.cloneNode(true);
slot.after(clone);
}
}
}
}
/**
* Toggles visibility of the code example
*/
public toggle() {
this.open = !this.open;
}
/** Opens the code example in a new tab */
public openInNewTab() {
const markup = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
${this.getDemoHTML({ isolated: true, absolutize: true, prettyWhitespace: true })}
</body>
</html>`;
const blob = new Blob([markup], { type: 'text/html' });
const a = Object.assign(document.createElement('a'), {
href: URL.createObjectURL(blob),
target: '_blank'
});
document.documentElement.append(a);
a.click();
a.remove();
}
/**
* Opens the code example in CodePen
*/
public edit() {
const markup = this.getDemoHTML({ isolated: true, absolutize: true, prettyWhitespace: true });
const css = 'body {\n font: 16px sans-serif;\n padding: 2rem;\n}';
const js = '';
const form = Object.assign(document.createElement('form'), {
action: 'https://codepen.io/pen/define',
method: 'POST',
target: '_blank'
});
const data = {
title: '',
description: '',
tags: ['webawesome'],
editors: '1000',
head: '<meta name="viewport" content="width=device-width">',
html_classes: '',
css_external: '',
js_external: '',
js_module: true,
js_pre_processor: 'none',
html: markup,
css,
js
};
const input = Object.assign(document.createElement('input'), {
type: 'hidden',
name: 'data',
value: JSON.stringify(data)
});
form.append(input);
document.documentElement.append(form);
form.submit();
form.remove();
}
}
declare global {
interface HTMLElementTagNameMap {
'wa-code-demo': WaCodeDemo;
}
}
// Private helpers
/**
* Convert URLs to absolute URLs on an element and any relevant elements within it
* @param root - The root element to start the search from
* @param base
*/
function absolutizeURLs(root: Element | DocumentFragment, base = location.href) {
const selector = URL_ATTRIBUTES.map(attr => `[${attr}]`).join(', ');
const elements = [];
if (root instanceof Element && root.matches(selector)) {
elements.push(root);
}
elements.push(...root.querySelectorAll(selector));
for (const element of elements) {
for (const attributeName of URL_ATTRIBUTES) {
if (element.hasAttribute(attributeName)) {
const url = element.getAttribute(attributeName) || '';
const absoluteURL = new URL(url, base).href;
element.setAttribute(attributeName, absoluteURL);
}
}
}
}
/**
* Get elements that match a selector within an elements shadow tree
* and any parent shadow trees, all the way up to the light DOM
* @param selector
* @param node - The node to start the search from
*/
function recursiveQSA(selector: string, node: Node) {
const ret: Element[] = [];
for (let root = node; root.nodeType !== Node.DOCUMENT_NODE; ) {
root = root.getRootNode();
const elements = (root as ShadowRoot | Document).querySelectorAll(selector);
ret.push(...elements);
}
return ret;
}
function dedent(code: string) {
// Remove blank lines at the start and end
code = code.replace(/^\s*\n|\n\s*$/g, '');
if (/^\S/gm.test(code)) {
// There are non-indented lines, so we can't dedent
return code;
}
// Find the smallest indentation
const lines = code.split(/\r?\n/);
const indents = lines.map(line => line.match(/^\s*/)?.[0]).filter(Boolean) as string[];
const minIndent = indents.reduce(
(minIndentSoFar, indent) => (minIndentSoFar.length < indent.length ? minIndentSoFar : indent),
indents[0]
);
if (!minIndent || lines.some(line => !line.startsWith(minIndent))) {
// Inconsistent indentation, can't dedent
return code;
}
return code.replace(new RegExp(`^${minIndent}`, 'gm'), '');
}
function getHTML(nodes: Iterable<Node>): string {
return getInnerHTML(nodes, node => {
if (node.nodeType === Node.ELEMENT_NODE && node.nodeName === 'TEMPLATE') {
const template = node as HTMLTemplateElement;
return template.innerHTML;
}
return undefined;
});
}

View File

@@ -3,7 +3,6 @@ import { animate, parseDuration } from '../../internal/animate.js';
import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, query } from 'lit/decorators.js';
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { WaAfterHideEvent } from '../../events/after-hide.js';
import { WaAfterShowEvent } from '../../events/after-show.js';
import { WaHideEvent } from '../../events/hide.js';
@@ -53,14 +52,13 @@ import type { CSSResultGroup } from 'lit';
export default class WaDetails extends WebAwesomeElement {
static styles: CSSResultGroup = [componentStyles, styles];
private detailsObserver: MutationObserver;
private readonly localize = new LocalizeController(this);
@query('.details') details: HTMLDetailsElement;
@query('.details__header') header: HTMLElement;
@query('.details__body') body: HTMLElement;
@query('.details__expand-icon-slot') expandIconSlot: HTMLSlotElement;
detailsObserver: MutationObserver;
/**
* Indicates whether or not the details is open. You can toggle this attribute to show and hide the details, or you
* can use the `show()` and `hide()` methods and this attribute will reflect the details' open state.
@@ -210,7 +208,7 @@ export default class WaDetails extends WebAwesomeElement {
}
render() {
const isRtl = !this.hasUpdated ? this.dir === 'rtl' : this.localize.dir() === 'rtl';
const isRtl = !this.hasUpdated ? this.dir === 'rtl' : this.matches(':dir(rtl)');
return html`
<details

View File

@@ -186,7 +186,7 @@ export default class WaDialog extends WebAwesomeElement {
// Open or close the dialog
if (this.open && !this.dialog.open) {
this.show();
} else if (!this.open && this.dialog.open) {
} else if (this.dialog.open) {
this.open = true;
this.requestClose(this.dialog);
}

View File

@@ -4,7 +4,6 @@ import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, query } from 'lit/decorators.js';
import { drag } from '../../internal/drag.js';
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { styleMap } from 'lit/directives/style-map.js';
import { WaChangeEvent } from '../../events/change.js';
import { watch } from '../../internal/watch.js';
@@ -42,8 +41,6 @@ import type { CSSResultGroup } from 'lit';
export default class WaImageComparer extends WebAwesomeElement {
static styles: CSSResultGroup = [componentStyles, styles];
private readonly localize = new LocalizeController(this);
@query('.image-comparer') base: HTMLElement;
@query('.image-comparer__handle') handle: HTMLElement;
@@ -52,7 +49,7 @@ export default class WaImageComparer extends WebAwesomeElement {
private handleDrag(event: PointerEvent) {
const { width } = this.base.getBoundingClientRect();
const isRtl = this.localize.dir() === 'rtl';
const isRtl = this.matches(':dir(rtl)');
event.preventDefault();
@@ -67,7 +64,7 @@ export default class WaImageComparer extends WebAwesomeElement {
private handleKeyDown(event: KeyboardEvent) {
const isLtr = this.matches(':dir(ltr)');
const isRtl = this.localize.dir() === 'rtl';
const isRtl = this.matches(':dir(rtl)');
if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
const incr = event.shiftKey ? 10 : 1;
@@ -99,7 +96,7 @@ export default class WaImageComparer extends WebAwesomeElement {
}
render() {
const isRtl = this.hasUpdated ? this.localize.dir() === 'rtl' : this.dir === 'rtl';
const isRtl = this.hasUpdated ? this.matches(':dir(rtl)') : this.dir === 'rtl';
return html`
<div

View File

@@ -5,7 +5,6 @@ import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, query } from 'lit/decorators.js';
import { getTextContent } from '../../internal/slot.js';
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { SubmenuController } from './submenu-controller.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
@@ -45,7 +44,6 @@ export default class WaMenuItem extends WebAwesomeElement {
static styles: CSSResultGroup = [componentStyles, styles];
private cachedTextLabel: string;
private readonly localize = new LocalizeController(this);
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
@query('.menu-item') menuItem: HTMLElement;
@@ -165,7 +163,7 @@ export default class WaMenuItem extends WebAwesomeElement {
}
render() {
const isRtl = this.hasUpdated ? this.localize.dir() === 'rtl' : this.dir === 'rtl';
const isRtl = this.hasUpdated ? this.matches(':dir(rtl)') : this.dir === 'rtl';
const isSubmenuExpanded = this.submenuController.isExpanded();
return html`

View File

@@ -195,7 +195,7 @@ export class SubmenuController implements ReactiveController {
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';
const isRtl = this.host.hasUpdated ? this.host.matches(':dir(rtl)') : this.host.dir === 'rtl';
if (!menu) {
return;
@@ -265,7 +265,7 @@ export class SubmenuController implements ReactiveController {
return html` <slot name="submenu" hidden></slot> `;
}
const isRtl = getComputedStyle(this.host).direction === 'rtl';
const isRtl = this.host.matches(':dir(rtl)');
return html`
<wa-popup

View File

@@ -3,7 +3,6 @@ import { css } from 'lit';
export default css`
:host {
display: block;
background-color: var(--wa-color-surface-default);
box-sizing: border-box;
height: 100%;
--menu-width: auto;
@@ -15,71 +14,12 @@ export default css`
--scroll-margin-top: calc(var(--header-height, 0px) + var(--subheader-height, 0px));
}
slot[name]:not([name='skip-to-content'], [name='navigation-toggle'])::slotted(*) {
display: flex;
background-color: var(--wa-color-surface-default);
}
::slotted([slot='banner']) {
align-items: center;
justify-content: center;
gap: var(--wa-space-m);
padding: var(--wa-space-xs) var(--wa-space-m);
}
::slotted([slot='header']) {
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--wa-space-m);
padding: var(--wa-space-m);
flex: auto;
}
::slotted([slot='subheader']) {
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--wa-space-m);
padding: var(--wa-space-xs) var(--wa-space-m);
}
::slotted([slot*='navigation']),
::slotted([slot='menu']),
::slotted([slot='aside']) {
flex-direction: column;
gap: var(--wa-space-m);
padding: var(--wa-space-m);
}
::slotted([slot='main-header']) {
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--wa-space-m);
padding: var(--wa-space-m) var(--wa-space-3xl);
}
::slotted(:not([slot])) {
padding: var(--wa-space-3xl);
}
::slotted([slot='main-footer']),
::slotted([slot='footer']) {
align-items: start;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--wa-space-m);
padding: var(--wa-space-3xl);
}
:host([disable-sticky~='banner']) :is([part~='header'], [part~='subheader']) {
--banner-height: 0px !important;
}
:host([disable-sticky~='header']) [part~='subheader'] {
--header-height: 0px !important;
}
/* Nothing else depends on subheader-height. */
:host([disable-sticky~='subheader']) {
}
@@ -88,7 +28,6 @@ export default css`
height: unset;
max-height: unset;
}
:host([disable-sticky~='banner']) [part~='banner'],
:host([disable-sticky~='header']) [part~='header'],
:host([disable-sticky~='subheader']) [part~='subheader'],
@@ -97,13 +36,11 @@ export default css`
position: static;
overflow: unset;
}
:host([disable-sticky~='aside']) [part~='aside'],
:host([disable-sticky~='menu']) [part~='menu'] {
height: auto;
max-height: auto;
}
[part~='base'] {
min-height: 100%;
display: grid;
@@ -117,7 +54,6 @@ export default css`
'body'
'footer';
}
/* Grid areas */
[part~='banner'] {
grid-area: banner;
@@ -156,12 +92,6 @@ export default css`
}
[part~='header'] {
top: var(--banner-height);
/** Make the header flex so that you don't unexpectedly have the default toggle button appearing above a slotted div because block elements are fun. */
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
}
[part~='subheader'] {
top: calc(var(--header-height) + var(--banner-height));
@@ -193,7 +123,6 @@ export default css`
[part~='main-footer'] {
grid-area: main-footer;
}
/* Visually hidden */
.skip-to-content:not(:focus-within) {
position: absolute !important;
@@ -206,7 +135,6 @@ export default css`
white-space: nowrap !important;
padding: 0 !important;
}
.skip-to-content {
position: absolute;
top: var(--wa-space-m);
@@ -221,7 +149,6 @@ export default css`
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
[part~='menu'],
[part~='aside'] {
position: sticky;
@@ -238,39 +165,12 @@ export default css`
grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(0, auto) minmax(0, 1fr) minmax(0, auto);
}
[part~='drawer']::part(dialog) {
background-color: var(--wa-color-surface-default);
}
/* Set these on the slot because we don't always control the navigation-toggle since that may be slotted. */
slot[name~='navigation-toggle'],
:host([disable-navigation-toggle]) slot[name~='navigation-toggle'] {
display: none;
}
/* Sometimes the media query in the viewport is stubborn in iframes. This is an extra check to make it behave properly. */
:host(:not([disable-navigation-toggle])[view='mobile']) slot[name~='navigation-toggle'] {
display: contents;
}
[part~='navigation-toggle'] {
/* Use only a margin-inline-start because the slotted header is expected to have default padding
so it looks really awkward if this sets a margin-inline-end and the slotted header has a padding-inline-start. */
margin-inline-start: var(--wa-space-m);
}
`;
export const mobileStyles = (breakpoint: number) => `
@media screen and (
max-width: ${(Number.isSafeInteger(breakpoint) ? breakpoint.toString() : '768') + 'px'}
) {
[part~='navigation'] {
display: none;
}
:host(:not([disable-navigation-toggle])) slot[name~='navigation-toggle'] {
display: contents;
}
[part~='navigation'] { display: none; }
}
`;

View File

@@ -23,7 +23,7 @@ if (typeof ResizeObserver === 'undefined') {
}
/**
* @summary Pages offer an easy way to scaffold entire page layouts using minimal markup.
* @summary Pages offer an easy way to scaffold pages using minimal markup.
* @documentation https://backers.webawesome.com/docs/components/page
* @status experimental
* @since 3.0
@@ -32,12 +32,10 @@ if (typeof ResizeObserver === 'undefined') {
* @slot banner - The banner that gets display above the header. The banner will not be shown if no content is provided.
* @slot header - The header to display at the top of the page. If a banner is present, the header will appear below the banner. The header will not be shown if there is no content.
* @slot subheader - A subheader to display below the `header`. This is a good place to put things like breadcrumbs.
* @slot menu - The left side of the page. If you slot an element in here, you will override the default `navigation` slot and will be handling navigation on your own. This also will not disable the fallback behavior of the navigation button. This section "sticks" to the top as the page scrolls.
* @slot menu - The left side of the page. If you slot an element in here, you will override the default "navigation" slot and will be handling navigation on your own. This also will not disable the fallback behavior of the navigation button. This section "sticks" to the top as the page scrolls.
* @slot navigation-header - The header for a navigation area. On mobile this will be the header for `<wa-drawer>`.
* @slot navigation - The main content to display in the navigation area. This is displayed on the left side of the page, if `menu` is not used. This section "sticks" to the top as the page scrolls.
* @slot navigation - The main content to display in the navigation area.
* @slot navigation-footer - The footer for a navigation area. On mobile this will be the footer for `<wa-drawer>`.
* @slot navigation-toggle - Use this slot to slot in your own button + icon for toggling the navigation drawer. By default it is a `<wa-button>` + a 3 bars `<wa-icon>`
* @slot navigation-toggle-icon - Use this to slot in your own icon for toggling the navigation drawer. By default it is 3 bars `<wa-icon>`.
* @slot main-header - Header to display inline above the main content.
* @slot main-footer - Footer to display inline below the main content.
* @slot aside - Content to be shown on the right side of the page. Typically contains a table of contents, ads, etc. This section "sticks" to the top as the page scrolls.
@@ -50,11 +48,8 @@ if (typeof ResizeObserver === 'undefined') {
* @csspart subheader - Shown below the header, usually intended for things like breadcrumbs and other page level navigation.
* @csspart body - The wrapper around menu, main, and aside.
* @csspart menu - The left hand side of the page. Generally intended for navigation.
* @csspart navigation - The `<nav>` that wraps the navigation slots on desktop viewports.
* @csspart navigation-header - The header for a navigation area. On mobile this will be the header for `<wa-drawer>`.
* @csspart navigation-footer - The footer for a navigation area. On mobile this will be the footer for `<wa-drawer>`.
* @csspart navigation-toggle - The default `<wa-button>` that will toggle the `<wa-drawer>` for mobile viewports.
* @csspart navigation-toggle-icon - The default `<wa-icon>` displayed inside of the navigation-toggle button.
* @csspart main-header - The header above main content.
* @csspart main-content - The main content.
* @csspart main-footer - The footer below main content.
@@ -94,24 +89,10 @@ export default class WaPage extends WebAwesomeElement {
private handleNavigationToggle = (e: Event) => {
// Don't toggle the nav when we're in desktop mode
if (this.view === 'desktop') {
// Just in case, try to hide the navigation.
this.hideNavigation();
return;
}
const path = e.composedPath();
const navigationToggleSlot = this.navigationToggleSlot;
if (
path.find((el: Element) => {
return (
el.hasAttribute?.('data-toggle-nav') ||
el.assignedSlot === navigationToggleSlot ||
el === navigationToggleSlot
);
})
) {
if (e.composedPath().find((el: Element) => el.hasAttribute?.('data-toggle-nav'))) {
e.preventDefault();
this.toggleNavigation();
}
@@ -122,7 +103,6 @@ export default class WaPage extends WebAwesomeElement {
@query("[part~='footer']") footer: HTMLElement;
@query("[part~='banner']") banner: HTMLElement;
@query("[part~='drawer']") navigationDrawer: WaDrawer;
@query("slot[name~='navigation-toggle']") navigationToggleSlot: HTMLSlotElement;
/**
* The view is a reflection of the "mobileBreakpoint", when the page is larger than the `mobile-breakpoint` (768px by
@@ -147,12 +127,6 @@ export default class WaPage extends WebAwesomeElement {
*/
@property({ attribute: 'navigation-placement', reflect: true }) navigationPlacement: 'start' | 'end' = 'start';
/**
* Determines whether or not to hide the default hamburger button. This will automatically flip to "true" if you add an element with `data-toggle-nav` anywhere in the element light DOM. Generally this will be set for you and you don't need to do anything, unless you're using SSR, in which case you should set this manually for initial page loads.
*/
@property({ attribute: 'disable-navigation-toggle', reflect: true, type: Boolean }) disableNavigationToggle: boolean =
false;
pageResizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
if (entry.contentBoxSize) {
@@ -194,21 +168,11 @@ export default class WaPage extends WebAwesomeElement {
this.pageResizeObserver.observe(this);
const navQuery = ":not([slot='toggle-navigation']) [data-toggle-nav]";
// check once on initial connect
// eslint-disable-next-line
this.disableNavigationToggle = Boolean(this.querySelector(navQuery));
setTimeout(() => {
this.headerResizeObserver.observe(this.header);
this.subheaderResizeObserver.observe(this.subheader);
this.bannerResizeObserver.observe(this.banner);
this.footerResizeObserver.observe(this.footer);
// Check again when the element updates
// eslint-disable-next-line
this.disableNavigationToggle = Boolean(this.querySelector(navQuery));
});
}
@@ -271,13 +235,6 @@ export default class WaPage extends WebAwesomeElement {
<slot name="banner"></slot>
</div>
<div class="header" part="header">
<slot name="navigation-toggle">
<wa-button part="navigation-toggle" size="small" appearance="text" variant="neutral">
<slot name="navigation-toggle-icon">
<wa-icon name="bars" part="navigation-toggle-icon" label="Toggle navigation drawer"></wa-icon>
</slot>
</wa-button>
</slot>
<slot name="header"></slot>
</div>
<div class="subheader" part="subheader">
@@ -328,20 +285,19 @@ export default class WaPage extends WebAwesomeElement {
@wa-after-show=${() => (this.navOpen = this.navigationDrawer.open)}
@wa-after-hide=${() => (this.navOpen = this.navigationDrawer.open)}
exportparts="
dialog:drawer__dialog,
overlay:drawer__overlay,
panel:drawer__panel,
header:drawer__header,
header-actions:drawer__header-actions,
title:drawer__title,
close-button:drawer__close-button,
close-button__base:drawer__close-button__base,
body:drawer__body,
panel:drawer__panel
base:drawer__base
overlay:drawer__overlay
panel:drawer__panel
header:drawer__header
header-actions:drawer__header-actions
title:drawer__title
close-button:drawer__close-button
close-button__base:drawer__close-button__base
body:drawer__body
footer:drawer__footer
"
class="navigation-drawer"
with-header
with-footer
>
<slot slot="label" part="navigation-header" name="mobile-navigation-header">
<slot name=${this.view === 'mobile' ? 'navigation-header' : '___'}></slot>

View File

@@ -1,11 +1,8 @@
import { expect } from '@open-wc/testing';
import { fixtures } from '../../internal/test/fixture.js';
import { html } from 'lit';
import type WaPopup from './popup.js';
describe('<wa-popup>', () => {
let element: WaPopup;
for (const fixture of fixtures) {
describe(`with "${fixture.type}" rendering`, () => {
it('should render a component', async () => {
@@ -13,26 +10,6 @@ describe('<wa-popup>', () => {
expect(el).to.exist;
});
it('should properly handle positioning when active changes', async () => {
element = await fixture(html`<wa-popup></wa-popup>`);
element.active = true;
await element.updateComplete;
// SImulate a scroll event
const event = new Event('scroll');
window.dispatchEvent(event);
element.active = false;
await element.updateComplete;
// The component should not throw an error when the window is scrolled
expect(() => {
element.active = true;
window.dispatchEvent(event);
}).not.to.throw();
});
});
}
});

View File

@@ -2,7 +2,6 @@ import { arrow, autoUpdate, computePosition, flip, offset, platform, shift, size
import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, query } from 'lit/decorators.js';
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { offsetParent } from 'composed-offset-position';
import { WaRepositionEvent } from '../../events/reposition.js';
import componentStyles from '../../styles/component.styles.js';
@@ -61,7 +60,6 @@ export default class WaPopup extends WebAwesomeElement {
private anchorEl: Element | VirtualElement | null;
private cleanup: ReturnType<typeof autoUpdate> | undefined;
private readonly localize = new LocalizeController(this);
/** A reference to the internal popup container. Useful for animating and styling the popup with JavaScript. */
@query('.popup') popup: HTMLElement;
@@ -278,7 +276,7 @@ export default class WaPopup extends WebAwesomeElement {
private start() {
// We can't start the positioner without an anchor
if (!this.anchorEl || !this.active) {
if (!this.anchorEl) {
return;
}
@@ -420,7 +418,7 @@ export default class WaPopup extends WebAwesomeElement {
//
// Source: https://github.com/floating-ui/floating-ui/blob/cb3b6ab07f95275730d3e6e46c702f8d4908b55c/packages/dom/src/utils/getDocumentRect.ts#L31
//
const isRtl = this.localize.dir() === 'rtl';
const isRtl = this.matches(':dir(rtl)');
const staticSide = { top: 'bottom', right: 'left', bottom: 'top', left: 'right' }[placement.split('-')[0]]!;
this.setAttribute('data-current-placement', placement);

View File

@@ -297,102 +297,6 @@ describe('<wa-radio-group>', () => {
});
});
describe('when handling focus', () => {
const doAction = async (instance: WaRadioGroup, type: string) => {
if (type === 'focus') {
instance.focus();
await instance.updateComplete;
return;
}
const label = instance.shadowRoot!.querySelector<HTMLLabelElement>('#label')!;
label.click();
await instance.updateComplete;
};
// Tests for focus and label actions with radio buttons
['focus', 'label'].forEach(actionType => {
describe(`when using ${actionType}`, () => {
it('should do nothing if all elements are disabled', async () => {
const el = await fixture<WaRadioGroup>(html`
<wa-radio-group>
<wa-radio id="radio-0" value="0" disabled></wa-radio>
<wa-radio id="radio-1" value="1" disabled></wa-radio>
<wa-radio id="radio-2" value="2" disabled></wa-radio>
<wa-radio id="radio-3" value="3" disabled></wa-radio>
</wa-radio-group>
`);
const validFocusHandler = sinon.spy();
Array.from(el.querySelectorAll<WaRadio>('wa-radio')).forEach(radio =>
radio.addEventListener('wa-focus', validFocusHandler)
);
expect(validFocusHandler).to.not.have.been.called;
await doAction(el, actionType);
expect(validFocusHandler).to.not.have.been.called;
});
it('should focus the first radio that is enabled when the group receives focus', async () => {
const el = await fixture<WaRadioGroup>(html`
<wa-radio-group>
<wa-radio id="radio-0" value="0" disabled></wa-radio>
<wa-radio id="radio-1" value="1"></wa-radio>
<wa-radio id="radio-2" value="2"></wa-radio>
<wa-radio id="radio-3" value="3"></wa-radio>
</wa-radio-group>
`);
const invalidFocusHandler = sinon.spy();
const validFocusHandler = sinon.spy();
const disabledRadio = el.querySelector('#radio-0')!;
const validRadio = el.querySelector('#radio-1')!;
disabledRadio.addEventListener('wa-focus', invalidFocusHandler);
validRadio.addEventListener('wa-focus', validFocusHandler);
expect(invalidFocusHandler).to.not.have.been.called;
expect(validFocusHandler).to.not.have.been.called;
await doAction(el, actionType);
expect(invalidFocusHandler).to.not.have.been.called;
expect(validFocusHandler).to.have.been.called;
});
it('should focus the currently enabled radio when the group receives focus', async () => {
const el = await fixture<WaRadioGroup>(html`
<wa-radio-group value="2">
<wa-radio id="radio-0" value="0" disabled></wa-radio>
<wa-radio id="radio-1" value="1"></wa-radio>
<wa-radio id="radio-2" value="2" checked></wa-radio>
<wa-radio id="radio-3" value="3"></wa-radio>
</wa-radio-group>
`);
const invalidFocusHandler = sinon.spy();
const validFocusHandler = sinon.spy();
const disabledRadio = el.querySelector('#radio-0')!;
const validRadio = el.querySelector('#radio-2')!;
disabledRadio.addEventListener('wa-focus', invalidFocusHandler);
validRadio.addEventListener('wa-focus', validFocusHandler);
expect(invalidFocusHandler).to.not.have.been.called;
expect(validFocusHandler).to.not.have.been.called;
await doAction(el, actionType);
expect(invalidFocusHandler).to.not.have.been.called;
expect(validFocusHandler).to.have.been.called;
});
});
});
});
describe('when the value changes', () => {
it('should emit wa-change when toggled with the arrow keys', async () => {
const radioGroup = await fixture<WaRadioGroup>(html`

View File

@@ -170,7 +170,14 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
}
private handleLabelClick() {
this.focus();
const radios = this.getAllRadios();
const checked = radios.find(radio => radio.checked);
const radioToFocus = checked || radios[0];
// Move focus to the checked radio (or the first one if none are checked) when clicking the label
if (radioToFocus) {
radioToFocus.focus();
}
}
private async syncRadioElements() {
@@ -298,19 +305,6 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
event.preventDefault();
}
/** Sets focus on the radio group. */
public focus(options?: FocusOptions) {
const radios = this.getAllRadios();
const checked = radios.find(radio => radio.checked);
const firstEnabledRadio = radios.find(radio => !radio.disabled);
const radioToFocus = checked || firstEnabledRadio;
// Call focus for the checked radio. If no radio is checked, focus the first one that isn't disabled.
if (radioToFocus) {
radioToFocus.focus(options);
}
}
render() {
const hasLabelSlot = this.hasUpdated ? this.hasSlotController.test('label') : this.withLabel;
const hasHelpTextSlot = this.hasUpdated ? this.hasSlotController.test('help-text') : this.withHelpText;

View File

@@ -202,7 +202,7 @@ export default class WaRange extends WebAwesomeFormAssociatedElement {
const inputWidth = this.input.offsetWidth;
const tooltipWidth = this.output.offsetWidth;
const thumbSize = getComputedStyle(this.input).getPropertyValue('--thumb-size');
const isRtl = this.localize.dir() === 'rtl';
const isRtl = this.matches(':dir(rtl)');
const percentAsWidth = inputWidth * percent;
// The calculations are used to "guess" where the thumb is located. Since we're using the native range control

View File

@@ -3,7 +3,6 @@ import { clamp } from '../../internal/math.js';
import { classMap } from 'lit/directives/class-map.js';
import { customElement, eventOptions, property, query, state } from 'lit/decorators.js';
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { styleMap } from 'lit/directives/style-map.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { WaChangeEvent } from '../../events/change.js';
@@ -38,8 +37,6 @@ import type { CSSResultGroup } from 'lit';
export default class WaRating extends WebAwesomeElement {
static styles: CSSResultGroup = [componentStyles, styles];
private readonly localize = new LocalizeController(this);
@query('.rating') rating: HTMLElement;
@state() private hoverValue = 0;
@@ -83,7 +80,7 @@ export default class WaRating extends WebAwesomeElement {
}
private getValueFromXCoordinate(coordinate: number) {
const isRtl = this.localize.dir() === 'rtl';
const isRtl = this.matches(':dir(rtl)');
const { left, right, width } = this.rating.getBoundingClientRect();
const value = isRtl
? this.roundToPrecision(((right - coordinate) / width) * this.max, this.precision)
@@ -112,7 +109,7 @@ export default class WaRating extends WebAwesomeElement {
private handleKeyDown(event: KeyboardEvent) {
const isLtr = this.matches(':dir(ltr)');
const isRtl = this.localize.dir() === 'rtl';
const isRtl = this.matches(':dir(rtl)');
const oldValue = this.value;
if (this.disabled || this.readonly) {
@@ -217,7 +214,7 @@ export default class WaRating extends WebAwesomeElement {
}
render() {
const isRtl = this.hasUpdated ? this.localize.dir() === 'rtl' : this.dir;
const isRtl = this.hasUpdated ? this.matches(':dir(rtl)') : this.dir;
const counter = Array.from(Array(this.max).keys());
let displayValue = 0;
@@ -243,7 +240,7 @@ export default class WaRating extends WebAwesomeElement {
aria-valuenow=${this.value}
aria-valuemin=${0}
aria-valuemax=${this.max}
tabindex=${this.disabled || this.readonly ? '-1' : '0'}
tabindex=${this.disabled ? '-1' : '0'}
@click=${this.handleClick}
@keydown=${this.handleKeyDown}
@mouseenter=${this.handleMouseEnter}

View File

@@ -164,7 +164,7 @@ export default css`
margin-inline-end: var(--wa-space-s);
}
.select--small.select--multiple:not(.select--placeholder-visible) .select__prefix::slotted(*) {
.select--small.select--multiple .select__prefix::slotted(*) {
margin-inline-start: var(--wa-space-s);
}
@@ -192,7 +192,7 @@ export default css`
margin-inline-end: var(--wa-space-m);
}
.select--medium.select--multiple:not(.select--placeholder-visible) .select__prefix::slotted(*) {
.select--medium.select--multiple .select__prefix::slotted(*) {
margin-inline-start: var(--wa-space-m);
}
@@ -220,7 +220,7 @@ export default css`
margin-inline-end: var(--wa-space-l);
}
.select--large.select--multiple:not(.select--placeholder-visible) .select__prefix::slotted(*) {
.select--large.select--multiple .select__prefix::slotted(*) {
margin-inline-start: var(--wa-space-l);
}

View File

@@ -1,4 +1,4 @@
import { aTimeout, expect, waitUntil } from '@open-wc/testing';
import { aTimeout, expect, oneEvent, waitUntil } from '@open-wc/testing';
import { clickOnElement } from '../../internal/test.js';
import { fixtures } from '../../internal/test/fixture.js';
import { html } from 'lit';
@@ -217,29 +217,6 @@ describe('<wa-select>', () => {
});
});
// This can happen in on Microsoft Edge auto-filling an associated input element in the same form
// https://github.com/shoelace-style/shoelace/issues/2117
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-select>
`);
const event = new KeyboardEvent('keydown');
Object.defineProperty(event, 'target', { writable: false, value: el });
Object.defineProperty(event, 'key', { writable: false, value: undefined });
/**
* If Edge does autofill, it creates a broken KeyboardEvent
* which is missing the key value.
* Using the normal dispatch mechanism does not allow to do this
* Thus passing the event directly to the private method for testing
*
* @ts-expect-error - private property */
el.handleDocumentKeyDown(event);
});
it('should open the listbox when any letter key is pressed with wa-select is on focus', async () => {
const el = await fixture<WaSelect>(html`
<wa-select>
@@ -525,7 +502,7 @@ describe('<wa-select>', () => {
expect(displayInput.value).to.equal('Option 1');
option.textContent = 'updated';
await aTimeout(250);
await oneEvent(option, 'slotchange');
await el.updateComplete;
expect(displayInput.value).to.equal('updated');

View File

@@ -417,7 +417,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
}
// All other "printable" keys trigger type to select
if (event.key?.length === 1 || event.key === 'Backspace') {
if (event.key.length === 1 || event.key === 'Backspace') {
const allOptions = this.getAllOptions();
// Don't block important key combos like CMD+R

View File

@@ -107,7 +107,7 @@ export default class WaSplitPanel extends WebAwesomeElement {
}
private handleDrag(event: PointerEvent) {
const isRtl = this.hasUpdated ? this.localize.dir() === 'rtl' : this.dir === 'rtl';
const isRtl = this.hasUpdated ? this.matches(':dir(rtl)') : this.dir === 'rtl';
if (this.disabled) {
return;
@@ -248,7 +248,7 @@ export default class WaSplitPanel extends WebAwesomeElement {
render() {
const gridTemplate = this.vertical ? 'gridTemplateRows' : 'gridTemplateColumns';
const gridTemplateAlt = this.vertical ? 'gridTemplateColumns' : 'gridTemplateRows';
const isRtl = this.hasUpdated ? this.localize.dir() === 'rtl' : this.dir === 'rtl';
const isRtl = this.hasUpdated ? this.matches(':dir(rtl)') : this.dir === 'rtl';
const primary = `
clamp(
0%,

View File

@@ -78,7 +78,7 @@ describe('<wa-tab-group>', () => {
it('renders', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general">General</wa-tab>
<wa-tab slot="nav" panel="general">General</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
</wa-tab-group>
`);
@@ -86,17 +86,10 @@ describe('<wa-tab-group>', () => {
expect(tabGroup).to.be.visible;
});
it('should not throw error when unmounted too fast', async () => {
const el = await fixture(html` <div></div> `);
el.innerHTML = '<sl-tab-group></sl-tab-group>';
el.innerHTML = '';
});
it('is accessible', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general">General</wa-tab>
<wa-tab slot="nav" panel="general">General</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
</wa-tab-group>
`);
@@ -107,8 +100,8 @@ describe('<wa-tab-group>', () => {
it('displays all tabs', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general" data-testid="general-tab-header">General</wa-tab>
<wa-tab panel="disabled" disabled data-testid="disabled-tab-header">Disabled</wa-tab>
<wa-tab slot="nav" panel="general" data-testid="general-tab-header">General</wa-tab>
<wa-tab slot="nav" panel="disabled" disabled data-testid="disabled-tab-header">Disabled</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
<wa-tab-panel name="disabled">This is a disabled tab panel.</wa-tab-panel>
</wa-tab-group>
@@ -121,8 +114,8 @@ describe('<wa-tab-group>', () => {
it('shows the first tab to be active by default', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general">General</wa-tab>
<wa-tab panel="custom">Custom</wa-tab>
<wa-tab slot="nav" panel="general">General</wa-tab>
<wa-tab slot="nav" panel="custom">Custom</wa-tab>
<wa-tab-panel name="general" data-testid="general-tab-content">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-group>
@@ -135,7 +128,7 @@ describe('<wa-tab-group>', () => {
it('shows the header above the tabs by default', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general">General</wa-tab>
<wa-tab slot="nav" panel="general">General</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
</wa-tab-group>
`);
@@ -149,7 +142,7 @@ describe('<wa-tab-group>', () => {
it('shows the header below the tabs by setting placement to bottom', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general">General</wa-tab>
<wa-tab slot="nav" panel="general">General</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
</wa-tab-group>
`);
@@ -164,7 +157,7 @@ describe('<wa-tab-group>', () => {
it('shows the header left of the tabs by setting placement to start', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general">General</wa-tab>
<wa-tab slot="nav" panel="general">General</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
</wa-tab-group>
`);
@@ -179,7 +172,7 @@ describe('<wa-tab-group>', () => {
it('shows the header right of the tabs by setting placement to end', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general">General</wa-tab>
<wa-tab slot="nav" panel="general">General</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
</wa-tab-group>
`);
@@ -197,7 +190,7 @@ describe('<wa-tab-group>', () => {
const result: HTMLTemplateResult[] = [];
for (let i = 0; i < n; i++) {
result.push(
html`<wa-tab panel="tab-${i}">Tab ${i}</wa-tab>
html`<wa-tab slot="nav" panel="tab-${i}">Tab ${i}</wa-tab>
<wa-tab-panel name="tab-${i}">Content of tab ${i}0</wa-tab-panel> `
);
}
@@ -357,8 +350,8 @@ describe('<wa-tab-group>', () => {
it('selects a tab by clicking on it', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general" data-testid="general-header">General</wa-tab>
<wa-tab panel="custom" data-testid="custom-header">Custom</wa-tab>
<wa-tab slot="nav" panel="general" data-testid="general-header">General</wa-tab>
<wa-tab slot="nav" panel="custom" data-testid="custom-header">Custom</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
<wa-tab-panel name="custom" data-testid="custom-tab-content">This is the custom tab panel.</wa-tab-panel>
</wa-tab-group>
@@ -368,36 +361,11 @@ describe('<wa-tab-group>', () => {
return expectCustomTabToBeActiveAfter(tabGroup, () => clickOnElement(customHeader!));
});
it('selects a tab by changing it via active property', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab slot="nav" panel="general" data-testid="general-header">General</wa-tab>
<wa-tab slot="nav" panel="custom" data-testid="custom-header">Custom</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
<wa-tab-panel name="custom" data-testid="custom-tab-content">This is the custom tab panel.</wa-tab-panel>
</wa-tab-group>
`);
const customHeader = queryByTestId<WaTab>(tabGroup, 'custom-header')!;
const generalHeader = await waitForHeaderToBeActive(tabGroup, 'general-header');
generalHeader.focus();
expect(customHeader).not.to.have.attribute('active');
const showEventPromise = oneEvent(tabGroup, 'wa-tab-show') as Promise<WaTabShowEvent>;
customHeader.active = true;
await tabGroup.updateComplete;
expect(customHeader).to.have.attribute('active');
await expectPromiseToHaveName(showEventPromise, 'custom');
return expectOnlyOneTabPanelToBeActive(tabGroup, 'custom-tab-content');
});
it('does not change if the active tab is reselected', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general" data-testid="general-header">General</wa-tab>
<wa-tab panel="custom">Custom</wa-tab>
<wa-tab slot="nav" panel="general" data-testid="general-header">General</wa-tab>
<wa-tab slot="nav" panel="custom">Custom</wa-tab>
<wa-tab-panel name="general" data-testid="general-tab-content"
>This is the general tab panel.</wa-tab-panel
>
@@ -412,8 +380,8 @@ describe('<wa-tab-group>', () => {
it('does not change if a disabled tab is clicked', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general" data-testid="general-header">General</wa-tab>
<wa-tab panel="disabled" data-testid="disabled-header" disabled>disabled</wa-tab>
<wa-tab slot="nav" panel="general" data-testid="general-header">General</wa-tab>
<wa-tab slot="nav" panel="disabled" data-testid="disabled-header" disabled>disabled</wa-tab>
<wa-tab-panel name="general" data-testid="general-tab-content"
>This is the general tab panel.</wa-tab-panel
>
@@ -428,8 +396,8 @@ describe('<wa-tab-group>', () => {
it('selects a tab by using the arrow keys', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general" data-testid="general-header">General</wa-tab>
<wa-tab panel="custom" data-testid="custom-header">Custom</wa-tab>
<wa-tab slot="nav" panel="general" data-testid="general-header">General</wa-tab>
<wa-tab slot="nav" panel="custom" data-testid="custom-header">Custom</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
<wa-tab-panel name="custom" data-testid="custom-tab-content">This is the custom tab panel.</wa-tab-panel>
</wa-tab-group>
@@ -441,8 +409,8 @@ describe('<wa-tab-group>', () => {
it('selects a tab by using the arrow keys and enter if activation is set to manual', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general" data-testid="general-header">General</wa-tab>
<wa-tab panel="custom" data-testid="custom-header">Custom</wa-tab>
<wa-tab slot="nav" panel="general" data-testid="general-header">General</wa-tab>
<wa-tab slot="nav" panel="custom" data-testid="custom-header">Custom</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
<wa-tab-panel name="custom" data-testid="custom-tab-content">This is the custom tab panel.</wa-tab-panel>
</wa-tab-group>
@@ -470,8 +438,8 @@ describe('<wa-tab-group>', () => {
it('does not allow selection of disabled tabs with arrow keys', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general" data-testid="general-header">General</wa-tab>
<wa-tab panel="disabled" disabled>Disabled</wa-tab>
<wa-tab slot="nav" panel="general" data-testid="general-header">General</wa-tab>
<wa-tab slot="nav" panel="disabled" disabled>Disabled</wa-tab>
<wa-tab-panel name="general" data-testid="general-tab-content"
>This is the general tab panel.</wa-tab-panel
>
@@ -485,8 +453,8 @@ describe('<wa-tab-group>', () => {
it('selects a tab by using the show function', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<wa-tab panel="general" data-testid="general-header">General</wa-tab>
<wa-tab panel="custom" data-testid="custom-header">Custom</wa-tab>
<wa-tab slot="nav" panel="general" data-testid="general-header">General</wa-tab>
<wa-tab slot="nav" panel="custom" data-testid="custom-header">Custom</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
<wa-tab-panel name="custom" data-testid="custom-tab-content">This is the custom tab panel.</wa-tab-panel>
</wa-tab-group>

Some files were not shown because too many files have changed in this diff Show More