Compare commits

...

157 Commits

Author SHA1 Message Date
Lea Verou
4a7fd7964f Update callout.css 2024-12-11 17:35:01 -05:00
Lea Verou
5a683300b2 Convert viewport-demo and callout styles 2024-12-11 16:56:49 -05:00
Lea Verou
20321f84d9 Start integrating esbuild-plugin-lit-css 2024-12-11 16:56:01 -05:00
Lea Verou
8fd897e348 [viewport-demo] Revert resize button 2024-12-11 16:22:42 -05:00
Lea Verou
adc76b4eb1 Merge branch 'next' of https://github.com/shoelace-style/webawesome into next 2024-12-11 16:09:17 -05:00
Lea Verou
cbb4aa8be1 Fix <wa-viewport-demo> zooming in Safari & Firefox
Also some refactoring to use `updated()` rather than `handleXXX()` functions
2024-12-11 16:09:15 -05:00
lindsaym-fa
968930c543 improve responsiveness of media example page 2024-12-11 10:52:46 -05:00
lindsaym-fa
4be8a46cdd touch up media example page 2024-12-11 10:32:09 -05:00
Lea Verou
c02496ff02 Add warning 2024-12-11 10:06:19 -05:00
Lea Verou
4b8afb4bc3 Hide zoom/viewport readings when they are lying 2024-12-11 10:02:25 -05:00
Lea Verou
01aa2afc15 Move display: flex to shared rule 2024-12-11 02:11:56 -05:00
Lea Verou
4f3539cb43 Formatting 2024-12-11 01:59:26 -05:00
Lea Verou
260bd47030 Simplify selector 2024-12-11 01:54:27 -05:00
Lea Verou
2f6fe33e2c Merge branch 'next' of https://github.com/shoelace-style/webawesome into next 2024-12-10 18:18:22 -05:00
Lea Verou
c307cfde1c Re-enable zoom in Safari & FF
The way they do zooming is crap, but hey, at least you can now tweak it again
2024-12-10 18:18:20 -05:00
Cory LaViska
883ef50186 Merge branch 'next' of https://github.com/shoelace-style/webawesome into next 2024-12-10 17:42:07 -05:00
Cory LaViska
c359556605 cleanup changelog 2024-12-10 17:41:51 -05:00
Lea Verou
3e18309367 Reorg page docs, light content editing 2024-12-10 15:34:14 -05:00
Cory LaViska
2fb0cc7d6d Merge branch 'next' into alpha 2024-12-10 15:27:19 -05:00
Lea Verou
26449e12e5 Merge branch 'next' of https://github.com/shoelace-style/webawesome into next 2024-12-10 15:23:22 -05:00
Lea Verou
7058de0568 Convert preview scripts to modules when using a manually slotted preview 2024-12-10 15:23:16 -05:00
Cory LaViska
0c3438e1a5 Merge branch 'next' of https://github.com/shoelace-style/webawesome into next 2024-12-10 15:21:04 -05:00
Cory LaViska
bae9b57c2a temp fix for page demo not showing when rendered with turbo 2024-12-10 15:21:01 -05:00
Lea Verou
739c45d34c Better proxy feature for detecting Chrome 2024-12-10 15:20:39 -05:00
Cory LaViska
32df4598fc fix default checked state 2024-12-10 14:59:09 -05:00
Cory LaViska
f52daeecdc Merge branch 'next' into alpha 2024-12-10 14:28:33 -05:00
Cory LaViska
930216e3f7 3.0.0-alpha.5 2024-12-10 14:27:49 -05:00
Lea Verou
8e40a082f5 Quick hack to disable zooming in Firefox and Safari 2024-12-10 14:14:27 -05:00
Konnor Rogers
e3fdf61e4a add layout toggling (#294)
* Add nav toggling

* fix comments

* fix comments

* prettier

* add comments / docs

* prettier

* fix up page stuff

* prettier

* Add comment

* minor fixes

* prettier
2024-12-10 13:53:08 -05:00
lindsaym-fa
414b29b046 tweak page demo styles 2024-12-10 12:58:28 -05:00
Lea Verou
ba4cdc9b28 Merge branch 'next' of https://github.com/shoelace-style/webawesome into next 2024-12-10 11:54:04 -05:00
Lea Verou
99704faeb4 Page demo draft 2024-12-10 11:53:39 -05:00
lindsaym-fa
78c446f525 update layout anatomy image 2024-12-10 11:41:41 -05:00
lindsaym-fa
b50b4ca632 revise responsive section on widths 2024-12-10 11:24:34 -05:00
Lea Verou
21d84332e4 Merge branch 'next' of https://github.com/shoelace-style/webawesome into next 2024-12-10 11:18:04 -05:00
Cory LaViska
c88a2c788d Merge pull request #291 from shoelace-style/konnorrogers/set-a-default-theme
Fix FOUC for default theme
2024-12-10 11:15:54 -05:00
konnorrogers
b1c459c226 fix fouc for default theme 2024-12-10 11:06:35 -05:00
Lea Verou
a8ddefe31e Merge branch 'next' of https://github.com/shoelace-style/webawesome into next 2024-12-10 10:30:27 -05:00
Lea Verou
80d23a4efe Fix code-demo expand animations on page load
Rn this means we don't get expand animations at all but that’s fine. I think it's fine to have an animation on close and not on open (different user goals — you open to get to the info, you close to get it out of the way, the opening is more urgent)
2024-12-10 10:29:55 -05:00
lindsaym-fa
5393a86e2b add descriptions for page examples 2024-12-10 10:19:12 -05:00
Lea Verou
1d822db30d Reapply "Fix missing content issue, closes #198"
This reverts commit 60c7c91e8a.

Also used a longer timeout
2024-12-10 10:14:20 -05:00
konnorrogers
654cd0b0bd set a default theme 2024-12-10 10:03:20 -05:00
Cory LaViska
6da942083c Merge branch 'next' of https://github.com/shoelace-style/webawesome into next 2024-12-10 09:54:39 -05:00
Cory LaViska
f15814c784 use callout markdown syntax 2024-12-10 09:54:37 -05:00
Lea Verou
60c7c91e8a Revert "Fix missing content issue, closes #198"
This reverts commit 22ca715ddb.
2024-12-10 09:52:29 -05:00
Cory LaViska
62cf254365 remove tooltips for now 2024-12-10 09:45:23 -05:00
Cory LaViska
747f185dc5 add labels to zoom buttons 2024-12-10 09:45:17 -05:00
Cory LaViska
1a61bd9583 fix outline spacing 2024-12-10 09:35:08 -05:00
Cory LaViska
7cff8fadde update tests 2024-12-10 09:26:58 -05:00
Cory LaViska
67abde6d97 Merge pull request #289 from shoelace-style/page-docs
Page documentation revisions
2024-12-10 09:23:09 -05:00
Cory LaViska
6e0b3d820c Merge pull request #288 from shoelace-style/callout
Simplify `<wa-callout>`
2024-12-10 08:57:31 -05:00
Cory LaViska
998180b05d Update docs/docs/components/callout.md 2024-12-10 08:57:24 -05:00
lindsaym-fa
ee9cc49956 Merge branch 'next' into page-docs 2024-12-10 05:01:06 -05:00
lindsaym-fa
3f79b35878 add media layout example 2024-12-10 05:00:44 -05:00
Lea Verou
fd3ec885f9 [viewport-demp] Parts for zoom buttons, zoom cursors 2024-12-10 04:50:36 -05:00
Lea Verou
22ca715ddb Fix missing content issue, closes #198 2024-12-10 04:45:13 -05:00
Lea Verou
5e6b8628d4 We don’t use Liquid! 2024-12-10 01:29:24 -05:00
lindsaym-fa
0a980addc3 remove duplicate heading, link Theming heading 2024-12-09 22:50:24 -05:00
lindsaym-fa
9bbfa1aeb2 add page card to components overview 2024-12-09 22:49:14 -05:00
lindsaym-fa
48fc9d9779 touch up header and sidebar spacing 2024-12-09 22:31:44 -05:00
lindsaym-fa
8ec8e2a9b4 Merge branch 'next' into page-docs 2024-12-09 21:56:09 -05:00
lindsaym-fa
5ec5979136 move styles to bottom of example code 2024-12-09 21:55:47 -05:00
Lea Verou
4926ad6ce5 Remove wa-callout::part(base) 2024-12-09 21:55:29 -05:00
lindsaym-fa
f013756b7b revise and edit docs 2024-12-09 21:46:52 -05:00
Lea Verou
feb092a7fa [code-examples] Smarter default for open 2024-12-09 21:41:54 -05:00
Lea Verou
406e899b34 Merge branch 'next' into page-docs 2024-12-09 21:03:42 -05:00
Lea Verou
4d89c344d1 [code-examples] Better syntax, do not include docs.css 2024-12-09 21:01:49 -05:00
Lea Verou
b9f484ceb6 data-viewport -> viewport 2024-12-09 21:01:27 -05:00
Lea Verou
1920ae8c1e Oops, commited debug code 2024-12-09 20:43:58 -05:00
lindsaym-fa
08422c29e7 Merge branch 'next' into page-docs 2024-12-09 20:29:49 -05:00
lindsaym-fa
59a70dd5b7 add documentation page demo 2024-12-09 20:29:16 -05:00
Lea Verou
86a80d3e78 [code-demo] Bugfixes
Slotted previews still don't work well with isolated demos :(
2024-12-09 20:27:23 -05:00
lindsaym-fa
1cebae7868 fix typo 2024-12-09 20:23:39 -05:00
Lea Verou
7b0ab1f7b7 Merge branch 'next' into page-docs 2024-12-09 19:42:03 -05:00
Lea Verou
1aebdf4a06 Fix isolated demos 2024-12-09 19:40:28 -05:00
Lea Verou
e94a424596 Simplify <wa-callout>
- Remove `base` (rel #207)
- Eliminate properties mirroring standard CSS properties (rel #259)
- Simplify markup
2024-12-09 19:32:55 -05:00
lindsaym-fa
14db763613 initial progress 2024-12-09 19:09:53 -05:00
Konnor Rogers
65ced601e9 fix various page issues (#286)
* fix dialog issues

* fix various page issues

* prettier
2024-12-09 19:00:16 -05:00
Lea Verou
2f0b259d96 Merge branch 'next' of https://github.com/shoelace-style/webawesome into next 2024-12-09 18:43:17 -05:00
Lea Verou
8267dd91d9 Do not restrict what attributes are allowed in curlies
We're the only ones writing Markdown…
2024-12-09 18:43:14 -05:00
Cory LaViska
2149a1efde Merge branch 'next' of https://github.com/shoelace-style/webawesome into next 2024-12-09 17:56:12 -05:00
Cory LaViska
7990717a95 move selectors to sidebar on mobile 2024-12-09 17:56:10 -05:00
Konnor Rogers
4526effbfa fix footer / header dialog (#285) 2024-12-09 17:30:56 -05:00
Lea Verou
a8774b5c88 [code-demo] Fix Safari collapsing bug 2024-12-09 17:15:07 -05:00
Lea Verou
fbc5946e71 [viewport-demo] Beautiful, cursed hack to make the resizers visible in Safari 2024-12-09 16:37:07 -05:00
Lea Verou
0fbdf110b1 Merge branch 'next' of https://github.com/shoelace-style/webawesome into next 2024-12-09 15:23:27 -05:00
Lea Verou
d37d873b4c More/better parts for *-demo components 2024-12-09 15:23:21 -05:00
Lea Verou
d5419ab0d0 [code-demo] Fix Safari bug with viewport emulation 2024-12-09 15:18:15 -05:00
lindsaym-fa
4805cdb64c update sample page metadata 2024-12-09 14:43:18 -05:00
Cory LaViska
5d74d8163b hide pro themes for alpha 2024-12-09 14:31:31 -05:00
Cory LaViska
76c74fc8a9 remove code-demo + viewport-demo from the alpha 2024-12-09 14:04:22 -05:00
Lea Verou
321f53f953 Add <wa-code-demo> and <wa-viewport-demo> and use them by default
Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2024-12-09 13:44:14 -05:00
Cory LaViska
849def9ef2 Merge pull request #284 from shoelace-style/konnorrogers/run-ssr-only-on-push-to-next
run ssr only on next push
2024-12-09 13:38:21 -05:00
konnorrogers
911668617b only on push 2024-12-09 13:35:31 -05:00
konnorrogers
d55e750ecd run ssr only on next push 2024-12-09 13:04:59 -05:00
Cory LaViska
08d6af502c Merge pull request #272 from shoelace-style/konnorrogers/set-outline-false-on-page
set outline false on page, add pattern.njk
2024-12-09 12:57:45 -05:00
Cory LaViska
9a68409ccd Merge branch 'next' into konnorrogers/set-outline-false-on-page 2024-12-09 12:56:40 -05:00
Cory LaViska
b8f4a38c80 Merge branch 'next' of https://github.com/shoelace-style/webawesome into next 2024-12-09 12:15:41 -05:00
Cory LaViska
a86acc9717 remove pro/dev stuff from alpha release 2024-12-09 12:13:32 -05:00
lindsaym-fa
a1fc75689e fix outdated logo 2024-12-09 11:55:24 -05:00
Lea Verou
0a5e4ab99c Make sidebar headings links whenever an overview is available (#241) 2024-12-09 11:33:04 -05:00
Cory LaViska
fd8876b3c4 fix filename typop 2024-12-09 11:23:43 -05:00
Lindsay M
994cba2fcf Page spacing (#279)
* initial progress

* spruce up docs sample page

* clean up and organization

* revert extraneous addition to applied.css

* accidentally reverted the wrong change 😅

* take 2 - revert extraneous addition in applied.css

* more cleanup
2024-12-09 11:10:15 -05:00
lindsaym-fa
fb565d202e remove outline from 'page' layout 2024-12-09 10:54:52 -05:00
Lea Verou
adb655a53b Properties ➡️ Attributes & Properties 2024-12-09 10:44:03 -05:00
Konnor Rogers
2d19c5d915 fix dialog scroll locking (#282)
* fix dialog scroll locking

* add changelog entry
2024-12-06 17:26:03 -05:00
Cory LaViska
ff0e00e4e4 Merge pull request #277 from shoelace-style/konnorrogers/fix-dialog-show
Fix calling `.show()`
2024-12-06 12:57:17 -05:00
Cory LaViska
1ea76cc08a fix padding and newlines 2024-12-06 10:45:13 -05:00
Cory LaViska
786b3c3da2 add layout and utilities to codepen 2024-12-06 10:36:30 -05:00
konnorrogers
c74049a140 changelog 2024-12-05 18:58:12 -05:00
konnorrogers
e7a0444c67 fixed a bug in wa-dialog causing it to close prematurely 2024-12-05 18:57:12 -05:00
Cory LaViska
32ed169ca8 don't redirect on 404 in build 2024-12-05 16:34:07 -05:00
konnorrogers
9e2f1d15d8 set outline false on page, add pattern.njk 2024-12-05 12:22:45 -05:00
Lea Verou
8d93a44566 Revert edit, turns out code previews are already documented in the linked page 🤦🏽‍♀️ 2024-12-04 17:17:26 -05:00
Lea Verou
471cb0287d Document API around code examples
cc @claviska
2024-12-04 16:25:35 -05:00
Cory LaViska
1428ec0708 Merge pull request #271 from shoelace-style/backports
Backports from Shoelace
2024-12-04 14:34:41 -05:00
Cory LaViska
4ed0a93ec6 backport SL-2281, SL-2295 2024-12-04 14:05:04 -05:00
Cory LaViska
ba125dc06a backport SL-2218 2024-12-04 12:47:57 -05:00
Cory LaViska
fe8b0624fe backport SL-2192 2024-12-04 12:43:43 -05:00
Cory LaViska
56dc07e30d backport SL-2189 2024-12-04 12:39:03 -05:00
Cory LaViska
6dd31748e9 backport SL-2210 2024-12-04 12:29:23 -05:00
Cory LaViska
2f4379716a backport SL-2182 2024-12-04 12:26:30 -05:00
Cory LaViska
58ac1c920d fix prefixes 2024-12-04 12:19:49 -05:00
Cory LaViska
abedd14cac backport SL-2299 2024-12-04 12:18:17 -05:00
Cory LaViska
73ca47b608 who 2024-12-04 12:12:14 -05:00
Cory LaViska
5ddc832e9d backport SL-2292 2024-12-04 12:09:17 -05:00
Cory LaViska
c097b21443 backport SL-2274 2024-12-04 12:03:49 -05:00
Cory LaViska
17fb0ef94e backport SL-2293 2024-12-04 11:52:09 -05:00
Cory LaViska
2089065499 add missing translations from shoelace 2024-12-04 11:45:38 -05:00
Cory LaViska
b168214952 backport SL-2270 2024-12-04 11:45:27 -05:00
Cory LaViska
678e3bd108 backport SL-2272 2024-12-04 11:39:12 -05:00
Cory LaViska
6cc030fa06 backport SL-2220 2024-12-04 11:38:18 -05:00
Cory LaViska
b84bd8e3cf backport SL-2221 2024-12-04 11:33:22 -05:00
Cory LaViska
8c1921da15 lint 2024-12-03 14:21:11 -05:00
Cory LaViska
9a86311bc4 Merge branch 'next' of https://github.com/shoelace-style/webawesome into next 2024-12-03 14:17:56 -05:00
Cory LaViska
5950100615 skip test that crashes 2024-12-03 14:17:43 -05:00
Lea Verou
4e3e0aad14 [cheatsheet] Make match URL param work
Co-Authored-By: Konnor Rogers <konnor5456@gmail.com>
2024-12-03 12:38:24 -05:00
Lea Verou
6cc3b05fb7 Move cheatsheet JS to separate file 2024-12-03 12:36:51 -05:00
Cory LaViska
0e75ec6758 fix failing test 2024-12-03 12:17:52 -05:00
Cory LaViska
f157d4dfa4 remove tests to reflect new button href behavior 2024-12-03 12:10:08 -05:00
Cory LaViska
3c81f9c5e9 Merge pull request #264 from shoelace-style/honor-existing-ids
Leave existing ids intact when generating anchor headings
2024-12-03 12:06:44 -05:00
Cory LaViska
d97bf9a9e9 Update docs/_utils/anchor-headings.js
Co-authored-by: Lea Verou <lea@verou.me>
2024-12-03 12:06:31 -05:00
Cory LaViska
bda66ab4b9 don't bork ids when already provided 2024-12-03 11:58:30 -05:00
Cory LaViska
2719880636 temporarily disable prettier in prod to speed things up 2024-12-03 11:52:23 -05:00
lindsaym-fa
ca20f4de22 add custom property for checkbox icon color 2024-12-03 10:52:59 -05:00
Lea Verou
9080810c20 [cheatsheet] Permalinks should include match params too 2024-12-03 09:40:08 -05:00
Lea Verou
0a6b36d3c8 [cheatsheet] Rename references -> cheatsheet 2024-12-02 20:38:34 -05:00
Lea Verou
0cf6765d56 [reference] Implement case insensitive and regexp search 2024-12-02 20:24:18 -05:00
Lea Verou
7b3e572d25 [reference] Update outline links when searching 2024-12-02 20:11:41 -05:00
Lea Verou
be387992e6 Enable Prettier for workspace 2024-12-02 19:57:21 -05:00
Lea Verou
1f8a282971 [reference] Support filtering permalinks 2024-12-02 19:57:10 -05:00
Lea Verou
8c804957fa [reference] Add filter input, add counts 2024-12-02 19:37:29 -05:00
Lea Verou
8b1fc9a18f [components/reference] Show count of components if > 1 2024-12-02 16:08:17 -05:00
Lea Verou
bd35c23784 Indices of components by attribute, slot, part, etc. (#240)
* Simplify how list of components is exposed to 11ty

No need for functions or computing multiple times, that's what data is for!

* Add attributes to component data

* Cleanup

* Add slug (tagName without wa-)

* Add component reference / cheatsheet
2024-11-24 21:19:50 -05:00
Cory LaViska
aef3ed8bcb Merge pull request #247 from shoelace-style/autoslot
automatically slot tab into tab group; fixes #239
2024-11-22 12:29:06 -05:00
Cory LaViska
01b23a3e88 Update src/components/tab/tab.ts
Co-authored-by: Lea Verou <lea@verou.me>
2024-11-22 12:28:58 -05:00
Cory LaViska
4185f3816b automatically slot tab into tab group; fixes #239 2024-11-22 12:15:30 -05:00
120 changed files with 5931 additions and 800 deletions

View File

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

39
.github/workflows/client_tests.js.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
# # 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,42 +1,13 @@
# # 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: Node.js CI
name: SSR 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
ssr_test:
runs-on: ubuntu-latest

View File

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

View File

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

View File

@@ -3,31 +3,37 @@ 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 { getComponents } from './_utils/manifest.js';
import componentList from './_data/componentList.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 || ''));
@@ -39,6 +45,7 @@ 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 => {
@@ -46,16 +53,9 @@ export default function (eleventyConfig) {
});
// Helpers
eleventyConfig.addNunjucksGlobal('getComponent', tagName => {
const component = getComponents().find(c => c.tagName === tagName);
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;
});
// Remove elements that have [data-alpha="remove"]
eleventyConfig.addPlugin(removeDataAlphaElements({ isAlpha }));
// 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,8 +115,7 @@ export default function (eleventyConfig) {
// mutation-observer (why SSR this?)
// resize-observer (why SSR this?)
// tooltip (why SSR this?)
const componentModules = getComponents()
const componentModules = componentList
// .filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
.map(component => {
const name = component.tagName.split(/wa-/)[1];
@@ -138,12 +137,16 @@ export default function (eleventyConfig) {
);
// Production-only plugins
if (!isDeveloping) {
// Run Prettier on each file (prod only because it can be slow)
eleventyConfig.addPlugin(formatCodePlugin());
}
//
// 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());
// }
return {
markdownTemplateEngine: 'njk',
dir: {
includes: '_includes',
layouts: '_layouts'

View File

@@ -0,0 +1,75 @@
/**
* @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;

3
docs/_data/components.js Normal file
View File

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

View File

@@ -0,0 +1,43 @@
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.4/+esm"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+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" />
<link rel="stylesheet" id="theme-stylesheet" href="/dist/themes/default.css" />
<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,10 +58,24 @@
return colorScheme === 'dark';
}
const stylesheet = document.getElementById("theme-stylesheet")
let preset = getPresetTheme()
const oldStylesheet = document.querySelector("#theme-stylesheet")
stylesheet.href = `/dist/themes/${preset}.css`
const newStylesheet = document.createElement("link")
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)
document.documentElement.classList.toggle(
`wa-theme-${preset}-dark`,
@@ -71,14 +85,15 @@
</head>
<body class="layout-{{ layout | stripExtension }}">
<!-- use view="desktop" as default to reduce layout jank on desktop site. -->
<wa-page view="desktop">
<wa-page view="desktop" disable-navigation-toggle="">
<header slot="header">
{# Logo #}
<div id="docs-branding">
{# Nav toggle #}
<wa-button appearance="text" data-toggle-nav>
<wa-button appearance="text" size="small" 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>
@@ -88,40 +103,11 @@
</div>
<div id="docs-toolbar">
{# 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>
{# Desktop selectors #}
<div class="only-desktop">
{% include "preset-theme-selector.njk" %}
{% include "color-scheme-selector.njk" %}
</div>
{# Search #}
<wa-button id="search-trigger" appearance="outlined" size="small" data-search>
@@ -134,12 +120,16 @@
{# Sidebar #}
{% if hasSidebar %}
<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>
{# 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>
{% endif %}
{# Outline #}

View File

@@ -0,0 +1,15 @@
{# 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

@@ -0,0 +1,22 @@
<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

@@ -0,0 +1,18 @@
{# 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,11 +20,12 @@
</ul>
{# Components #}
<h2>Components</h2>
<h2>
<a href="/docs/components/" title="Overview">Components
<wa-icon name="grid-2"></wa-icon>
</a>
</h2>
<ul>
<li>
<a href="/docs/components/">Components Overview</a>
</li>
<li>
<a href="/docs/components/animated-image">Animated Image</a>
</li>
@@ -68,6 +69,11 @@
<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>
@@ -127,6 +133,11 @@
<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>
@@ -210,15 +221,24 @@
</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 #}
<h2>Layout</h2>
{% if not isAlpha %}
<h2>
<a href="/docs/layout" title="Overview">Layout
<wa-icon name="grid-2"></wa-icon>
</a>
</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>
@@ -227,13 +247,12 @@
<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>
@@ -243,20 +262,22 @@
<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-summmary">Order Summaries</a></li>
<li><a href="/docs/patterns/ecommerce-order-summary">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>Theming</h2>
<h2>
<a href="/docs/theming" title="Overview">Theming
<wa-icon name="grid-2"></wa-icon>
</a>
</h2>
<ul>
<li>
<a href="/docs/theming/">Theming Overview</a>
</li>
<li>
<a href="/docs/theming/color">Color</a>
</li>
@@ -284,10 +305,12 @@
</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>
</ul>
{% endif %}

View File

@@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 1.1 KiB

69
docs/_layouts/blank.njk Normal file
View File

@@ -0,0 +1,69 @@
<!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 = getComponent('wa-' + page.fileSlug) %}
{% set component = components[page.fileSlug] %}
{% set description = component.summary %}
{% extends '../_includes/base.njk' %}
@@ -17,6 +17,10 @@
>
{{ 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 }}
@@ -62,7 +66,7 @@
{# Properties #}
{% if component.properties.length %}
<h2>Properties</h2>
<h2>Attributes & Properties</h2>
<div class="table-scroll">
<table class="component-table">
@@ -77,9 +81,9 @@
{% for prop in component.properties %}
<tr>
<td class="table-name">
<code>{{ prop.name }}</code><br>
<code title="JS property">{{ prop.name }}</code><br>
{% if prop.attribute %}
<div><small><code>{{ prop.attribute }}</code></small></div>
<div><small><code title="HTML attribute">{{ prop.attribute }}</code></small></div>
{% endif %}
</td>
<td class="table-description">
@@ -266,9 +270,9 @@
</p>
<wa-tab-group label="How would you like to import this component?">
<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="cdn">CDN</wa-tab>
<wa-tab panel="npm">npm</wa-tab>
<wa-tab 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 = true %}
{% set hasOutline = false %}
{% extends "../_includes/base.njk" %}

View File

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

View File

@@ -39,22 +39,26 @@ 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());
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}`;
if (hasAnchor) {
return;
}
if (hasAnchor || !id) {
return;
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}`;
}
}
// Create the anchor
@@ -67,7 +71,9 @@ export function anchorHeadingsPlugin(options = {}) {
anchor.querySelector('.wa-visually-hidden').textContent = options.anchorLabel;
// Update the heading
heading.setAttribute('id', id);
if (!existingId) {
heading.setAttribute('id', id);
}
heading.classList.add('anchor-heading');
heading.appendChild(anchor);
});

View File

@@ -1,82 +1,190 @@
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(options = {}) {
options = {
export function codeExamplesPlugin(eleventyConfig, options = {}) {
const defaultOptions = {
container: 'body',
...options
defaultOpen: (code, { outputPathIndex }) => {
return (
outputPathIndex === 1 && // is first
code.textContent.length < 500
); // is short
}
};
options = { ...defaultOptions, ...options };
const stats = {
inputPaths: {},
outputPaths: {}
};
return function (eleventyConfig) {
eleventyConfig.addTransform('code-examples', content => {
const doc = parse(content, { blockTextElements: { code: true } });
const container = doc.querySelector(options.container);
eleventyConfig.addTransform('code-examples', function (content) {
const { inputPath, outputPath } = this.page;
if (!container) {
return content;
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;
}
}
// 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;
for (const attribute of ['viewport', 'include']) {
if (code.hasAttribute(attribute)) {
localOptions.attributes[attribute] = code.getAttribute(attribute);
code.removeAttribute(attribute);
}
}
// 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 (Object.keys(localOptions.attributes).length > 0) {
// attributes only work on the new syntax
localOptions.new = true;
}
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>
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]
});
}
}
${
noEdit
? ''
: `
<button class="code-example-pen" type="button">
<wa-icon name="pen-to-square"></wa-icon>
Edit
</button>
`
}
const template = localOptions.new ? 'new' : 'old';
const codeExample = parse(templates[template](pre, code, localOptions));
`
: ''
}
</div>
</div>
`);
pre.replaceWith(codeExample);
});
return doc.toString();
pre.replaceWith(codeExample);
});
};
return doc.toString();
});
}

View File

@@ -1,71 +0,0 @@
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: ['id', 'class', 'data']
allowedAttributes: []
});

View File

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,23 @@
#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

@@ -0,0 +1,50 @@
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

@@ -0,0 +1,66 @@
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: 35 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,3 +1,3 @@
<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>
<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>

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 742 B

View File

@@ -0,0 +1,76 @@
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,10 +18,12 @@ 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\n` +
`<link rel="stylesheet" href="${cdnUrl}themes/applied.css">\n\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` +
`${code.textContent}`;
const css = 'body {\n font: 16px sans-serif;\n padding: 2rem;\n}';
const css = 'html > body {\n font: 16px sans-serif;\n padding: 2rem;\n}';
const js = '';
const form = document.createElement('form');

View File

@@ -25,12 +25,19 @@ 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='/'] {
@@ -40,6 +47,7 @@ wa-page > header {
wa-button[data-toggle-nav] {
--label-color: currentColor;
font-size: 1rem;
margin-inline-start: -0.875rem;
margin-inline-end: 0;
@@ -87,10 +95,10 @@ wa-page > header {
#outline {
h2 {
font-size: var(--wa-font-size-m);
margin-block-end: var(--wa-space-m);
margin: 0;
}
h2:not(:first-of-type) {
margin-block-start: var(--wa-space-xl);
h2:not(:first-child) {
margin-block-start: var(--wa-space-xs);
}
ul {
border-inline-start: var(--wa-border-width-s) solid var(--wa-color-surface-border);
@@ -104,7 +112,9 @@ wa-page > header {
}
li {
list-style: none;
margin-block-end: var(--wa-space-m);
}
li + li {
margin-block-start: var(--wa-space-m);
}
li a {
color: var(--wa-color-text-normal);
@@ -113,6 +123,59 @@ 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 {
@@ -143,6 +206,14 @@ 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,3 +64,9 @@ 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

@@ -0,0 +1,82 @@
---
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

@@ -0,0 +1,211 @@
---
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,13 +315,21 @@ layout: page-outline
</wa-card>
</a>
<a href="/docs/components/drawer">
<wa-card id="drawer-card">
<wa-card with-header 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

@@ -0,0 +1,238 @@
---
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

@@ -0,0 +1,401 @@
---
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,50 +1,33 @@
---
title: Page
description: Layouts offer an easy way to scaffold pages using minimal markup.
description: Pages offer an easy way to scaffold entire page layouts using minimal markup.
layout: component
isPro: true
---
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.
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.
## Layout Anatomy
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.
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.
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.
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.
![Screenshot of Layout Anatomy showing various slots](/assets/images/layout-anatomy.svg)
{% include "page-demo.njk" %}
<!-- ![Screenshot of Layout Anatomy showing various slots](/assets/images/layout-anatomy.svg) -->
## Using `wa-page`
:::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.
:::
## Sticky Sections
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.
The following sections of the layout are "sticky" by default, meaning they remain in position as the user scrolls.
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.
- `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>`.
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>`:
```css
html,
@@ -56,7 +39,687 @@ body {
}
```
## Skip To Content
## 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
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.
@@ -70,15 +733,44 @@ This example localizes the "skip to content" link for German users.
</wa-page>
```
## Responsiveness
### Responsiveness
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.
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.
```html
<wa-page mobile-breakpoint="600"> ... </wa-page>
```
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.)
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.
```html
<wa-page mobile-breakpoint="600">
@@ -94,25 +786,44 @@ Alternatively, you can apply `nav-state="open"` and `nav-state="closed"` to the
<wa-page nav-state="open"> ... </wa-page>
```
## Providing Navigation Items
`<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;
}
```
- TODO - example with navigation items
- TODO - example with`<h2>` and `<a>` as navigation items
#### Custom Widths
## Examples
You specify widths for some slots on your page with [CSS custom properties](#css-custom-properties) for `--menu-width`, `--main-width`, and `--aside-width`.
### Hero Layout
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;
}
```
- TODO - Sticky header + main + footer
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;
}
```
### Blog Layout
### Spacing
- TODO - Sticky header + main + aside + footer (blog)
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.
### App Layout
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);
}
```
- 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 tab must be slotted into the `nav` slot and its `panel` must refer to a tab panel of the same name.
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.
```html {.example}
<wa-tab-group>
<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="general">General</wa-tab>
<wa-tab panel="custom">Custom</wa-tab>
<wa-tab panel="advanced">Advanced</wa-tab>
<wa-tab panel="disabled" disabled>Disabled</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
<wa-tab-panel name="custom">This is the custom tab panel.</wa-tab-panel>
@@ -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 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="general">General</wa-tab>
<wa-tab panel="custom">Custom</wa-tab>
<wa-tab 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 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="general">General</wa-tab>
<wa-tab panel="custom">Custom</wa-tab>
<wa-tab panel="advanced">Advanced</wa-tab>
<wa-tab panel="disabled" disabled>Disabled</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
<wa-tab-panel name="custom">This is the custom tab panel.</wa-tab-panel>
@@ -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 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="general">General</wa-tab>
<wa-tab panel="custom">Custom</wa-tab>
<wa-tab panel="advanced">Advanced</wa-tab>
<wa-tab panel="disabled" disabled>Disabled</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
<wa-tab-panel name="custom">This is the custom tab panel.</wa-tab-panel>
@@ -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 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="general">General</wa-tab>
<wa-tab panel="custom">Custom</wa-tab>
<wa-tab panel="advanced">Advanced</wa-tab>
<wa-tab panel="disabled" disabled>Disabled</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
<wa-tab-panel name="custom">This is the custom tab panel.</wa-tab-panel>
@@ -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 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="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-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[slot="nav"]');
const closeButton = tabGroup.querySelector('wa-icon-button');
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 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="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-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 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="general">General</wa-tab>
<wa-tab panel="custom">Custom</wa-tab>
<wa-tab panel="advanced">Advanced</wa-tab>
<wa-tab panel="disabled" disabled>Disabled</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
<wa-tab-panel name="custom">This is the custom tab panel.</wa-tab-panel>

View File

@@ -6,10 +6,10 @@ layout: component
```html {.example}
<wa-tab-group>
<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="general">General</wa-tab>
<wa-tab panel="custom">Custom</wa-tab>
<wa-tab panel="advanced">Advanced</wa-tab>
<wa-tab panel="disabled" disabled>Disabled</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
<wa-tab-panel name="custom">This is the custom tab panel.</wa-tab-panel>

View File

@@ -0,0 +1,69 @@
---
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

@@ -0,0 +1,100 @@
---
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 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 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="classic">Classic</wa-option>
</wa-select>
</div>

View File

@@ -29,6 +29,7 @@ Available translations include:
<div style="columns: 3; gap: 1rem; margin-block-end: 1.5rem;">
- ar
- cs
- da
- de-ch
- de
@@ -36,19 +37,24 @@ 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: page.njk
layout: pattern.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: page.njk
layout: pattern.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: page.njk
layout: pattern.njk
---
TODO Page Description

View File

@@ -1,12 +1,12 @@
---
title: E-commerce - Category Filter
description: TODO
layout: page.njk
layout: pattern.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: page.njk
layout: pattern.njk
---
TODO Page Description

View File

@@ -1,7 +1,7 @@
---
title: E-commerce - Order History
description: TODO
layout: page.njk
layout: pattern.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: page.njk
layout: pattern.njk
---
TODO Page Description

View File

@@ -1,7 +1,7 @@
---
title: E-commerce - Product Detail
description: TODO
layout: page.njk
layout: pattern.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: page.njk
layout: pattern.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,11 +156,10 @@ 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: page.njk
layout: pattern.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 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="general">General</wa-tab>
<wa-tab panel="custom">Custom</wa-tab>
<wa-tab panel="advanced">Advanced</wa-tab>
<wa-tab panel="disabled" disabled>Disabled</wa-tab>
<wa-tab-panel name="general">
<div></div>
@@ -461,4 +461,4 @@ TODO Page Description
</div>
```
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,36 +12,31 @@ 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!
:::
## Next
## 3.0.0-alpha.5
- 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 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 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
## 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!
- Updated all checks for directionality to use `this.localize.dir()` instead of `el.matches(:dir(rtl))` so older browsers don't error out
---
@@ -68,7 +63,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 append `:preview` to the language.
To render a code preview, use the standard code field syntax and add a class of `example`:
````md
```html {.example}
@@ -105,7 +105,11 @@ To render a code preview, use the standard code field syntax and append `:previe
```
````
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.
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).
````md
```html {.example .open .no-edit}
@@ -113,12 +117,23 @@ You can also append `.open` to expand the code by default, and `.no-edit` to dis
```
````
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.
@@ -386,4 +401,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-{name}-{#}`.
Web Awesome defines seven literal colors each with 11 lightness values using the format `--wa-color-{hue}-{tint}`.
<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,6 +4,8 @@ description: Build better with Web Awesome, the open source library of web compo
layout: page
---
<style>
.title,
.anchor-heading a,
@@ -207,7 +209,7 @@ layout: page
& > * + * {
flex-grow: 1;
}
& wa-callout::part(base),
& wa-callout,
& wa-button::part(base) {
height: 100%;
width: 100%;
@@ -386,4 +388,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.4",
"version": "3.0.0-alpha.5",
"homepage": "https://webawesome.com/",
"author": "Web Awesome",
"license": "MIT",
@@ -18,10 +18,13 @@
"./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": [
@@ -98,6 +101,7 @@
"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,6 +10,7 @@ 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';
@@ -106,10 +107,11 @@ 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/color_standard.css'), join(cdnDir, 'themes/color_standard.css'), {
overwrite: true
});
await copy(join(rootDir, 'src/themes/classic.css'), join(cdnDir, 'themes/classic.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 });
}
@@ -170,7 +172,7 @@ async function generateBundle() {
bundle: true,
splitting: true,
minify: false,
plugins: [replace({ __WEBAWESOME_VERSION__: version })]
plugins: [replace({ __WEBAWESOME_VERSION__: version }), litCssPlugin()]
};
const unbundledConfig = {
@@ -288,12 +290,18 @@ if (isDeveloping) {
callbacks: {
ready: (_err, instance) => {
// 404 errors
instance.addMiddleware('*', (req, res) => {
instance.addMiddleware('*', async (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 {
res.writeHead(302, { location: '/404.html' });
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.end();

View File

@@ -183,22 +183,6 @@ 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

@@ -0,0 +1,64 @@
: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

@@ -1,80 +0,0 @@
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,9 +15,7 @@ describe('<wa-callout>', () => {
await customElements.whenDefined('wa-callout');
await callout.updateComplete;
const base = callout.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
expect(base).to.have.class(`callout--${variant}`);
expect(callout).to.have.attribute('variant', 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,8 +1,7 @@
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.style.js';
import styles from './callout.css';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
@@ -15,19 +14,12 @@ 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.
* @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.
*/
@customElement('wa-callout')
export default class WaCallout extends WebAwesomeElement {
@@ -38,24 +30,12 @@ export default class WaCallout extends WebAwesomeElement {
render() {
return html`
<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="icon">
<slot name="icon"></slot>
</div>
<div part="message" class="callout__message">
<slot></slot>
</div>
<div part="message">
<slot></slot>
</div>
`;
}

View File

@@ -67,7 +67,8 @@ describe('<wa-carousel>', () => {
});
});
it('should scroll forwards every `autoplay-interval` milliseconds', async () => {
// 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 () => {
// Arrange
const el = await fixture<WaCarousel>(html`
<wa-carousel autoplay autoplay-interval="10">

View File

@@ -102,6 +102,7 @@ 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();
@@ -175,7 +176,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.matches(':dir(rtl)');
const isRtl = this.localize.dir() === 'rtl';
const isFocusInPagination = target.closest('[part~="pagination-item"]') !== null;
const isNext =
event.key === 'ArrowDown' || (!isRtl && event.key === 'ArrowRight') || (isRtl && event.key === 'ArrowLeft');
@@ -285,6 +286,9 @@ 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. */
@@ -302,18 +306,29 @@ export default class WaCarousel extends WebAwesomeElement {
const firstIntersecting = entries.find(entry => entry.isIntersecting);
if (firstIntersecting) {
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 (!firstIntersecting) {
return;
}
// 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;
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.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');
}
}
}
},
@@ -334,6 +349,8 @@ export default class WaCarousel extends WebAwesomeElement {
this.synchronizeSlides();
this.scrolling = false;
this.pendingSlideChange = false;
this.synchronizeSlides();
}
private isCarouselItem(node: Node): node is WaCarouselItem {
@@ -403,7 +420,7 @@ export default class WaCarousel extends WebAwesomeElement {
}
@watch('activeSlide')
handelSlideChange() {
handleSlideChange() {
const slides = this.getSlides();
slides.forEach((slide, i) => {
slide.classList.toggle('--is-active', i === this.activeSlide);
@@ -484,7 +501,7 @@ export default class WaCarousel extends WebAwesomeElement {
: clamp(index, 0, slides.length - slidesPerPage);
this.activeSlide = newActiveSlide;
const isRtl = this.matches(':dir(rtl)');
const isRtl = this.localize.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.
@@ -501,17 +518,35 @@ export default class WaCarousel extends WebAwesomeElement {
}
private scrollToSlide(slide: HTMLElement, behavior: ScrollBehavior = 'smooth') {
const scrollContainer = this.scrollContainer;
const scrollContainerRect = scrollContainer.getBoundingClientRect();
const nextSlideRect = slide.getBoundingClientRect();
// 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 nextLeft = nextSlideRect.left - scrollContainerRect.left;
const nextTop = nextSlideRect.top - scrollContainerRect.top;
const scrollContainer = this.scrollContainer;
const scrollContainerRect = scrollContainer.getBoundingClientRect();
const nextSlideRect = slide.getBoundingClientRect();
scrollContainer.scrollTo({
left: nextLeft + scrollContainer.scrollLeft,
top: nextTop + scrollContainer.scrollTop,
behavior
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;
}
});
}
@@ -532,7 +567,7 @@ export default class WaCarousel extends WebAwesomeElement {
}
// We can't rely on `this.matches()` on the server.
const isRTL = isServer ? this.dir === 'rtl' : this.matches(':dir(rtl)');
const isRTL = isServer ? this.dir === 'rtl' : this.localize.dir() === 'rtl';
return html`
<div part="base" class="carousel">

View File

@@ -13,6 +13,7 @@ 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;
@@ -90,7 +91,7 @@ export default css`
/* Checked/indeterminate */
.checkbox--checked .checkbox__control,
.checkbox--indeterminate .checkbox__control {
color: var(--wa-color-brand-on-loud);
color: var(--checked-icon-color);
border-color: var(--border-color-checked);
background-color: var(--background-color-checked);
}

View File

@@ -51,6 +51,7 @@ 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

@@ -0,0 +1,197 @@
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

@@ -0,0 +1,376 @@
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,6 +3,7 @@ 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';
@@ -52,13 +53,14 @@ 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.
@@ -208,7 +210,7 @@ export default class WaDetails extends WebAwesomeElement {
}
render() {
const isRtl = !this.hasUpdated ? this.dir === 'rtl' : this.matches(':dir(rtl)');
const isRtl = !this.hasUpdated ? this.dir === 'rtl' : this.localize.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.dialog.open) {
} else if (!this.open && this.dialog.open) {
this.open = true;
this.requestClose(this.dialog);
}

View File

@@ -4,6 +4,7 @@ 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';
@@ -41,6 +42,8 @@ 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;
@@ -49,7 +52,7 @@ export default class WaImageComparer extends WebAwesomeElement {
private handleDrag(event: PointerEvent) {
const { width } = this.base.getBoundingClientRect();
const isRtl = this.matches(':dir(rtl)');
const isRtl = this.localize.dir() === 'rtl';
event.preventDefault();
@@ -64,7 +67,7 @@ export default class WaImageComparer extends WebAwesomeElement {
private handleKeyDown(event: KeyboardEvent) {
const isLtr = this.matches(':dir(ltr)');
const isRtl = this.matches(':dir(rtl)');
const isRtl = this.localize.dir() === 'rtl';
if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
const incr = event.shiftKey ? 10 : 1;
@@ -96,7 +99,7 @@ export default class WaImageComparer extends WebAwesomeElement {
}
render() {
const isRtl = this.hasUpdated ? this.matches(':dir(rtl)') : this.dir === 'rtl';
const isRtl = this.hasUpdated ? this.localize.dir() === 'rtl' : this.dir === 'rtl';
return html`
<div

View File

@@ -5,6 +5,7 @@ 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';
@@ -44,6 +45,7 @@ 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;
@@ -163,7 +165,7 @@ export default class WaMenuItem extends WebAwesomeElement {
}
render() {
const isRtl = this.hasUpdated ? this.matches(':dir(rtl)') : this.dir === 'rtl';
const isRtl = this.hasUpdated ? this.localize.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 = this.host.hasUpdated ? this.host.matches(':dir(rtl)') : this.host.dir === 'rtl';
const isRtl = getComputedStyle(this.host).direction === 'rtl';
if (!menu) {
return;
@@ -265,7 +265,7 @@ export class SubmenuController implements ReactiveController {
return html` <slot name="submenu" hidden></slot> `;
}
const isRtl = this.host.matches(':dir(rtl)');
const isRtl = getComputedStyle(this.host).direction === 'rtl';
return html`
<wa-popup

View File

@@ -3,6 +3,7 @@ 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;
@@ -14,12 +15,71 @@ 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']) {
}
@@ -28,6 +88,7 @@ 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'],
@@ -36,11 +97,13 @@ 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;
@@ -54,6 +117,7 @@ export default css`
'body'
'footer';
}
/* Grid areas */
[part~='banner'] {
grid-area: banner;
@@ -92,6 +156,12 @@ 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));
@@ -123,6 +193,7 @@ export default css`
[part~='main-footer'] {
grid-area: main-footer;
}
/* Visually hidden */
.skip-to-content:not(:focus-within) {
position: absolute !important;
@@ -135,6 +206,7 @@ export default css`
white-space: nowrap !important;
padding: 0 !important;
}
.skip-to-content {
position: absolute;
top: var(--wa-space-m);
@@ -149,6 +221,7 @@ export default css`
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
[part~='menu'],
[part~='aside'] {
position: sticky;
@@ -165,12 +238,39 @@ 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; }
[part~='navigation'] {
display: none;
}
:host(:not([disable-navigation-toggle])) slot[name~='navigation-toggle'] {
display: contents;
}
}
`;

View File

@@ -23,7 +23,7 @@ if (typeof ResizeObserver === 'undefined') {
}
/**
* @summary Pages offer an easy way to scaffold pages using minimal markup.
* @summary Pages offer an easy way to scaffold entire page layouts using minimal markup.
* @documentation https://backers.webawesome.com/docs/components/page
* @status experimental
* @since 3.0
@@ -32,10 +32,12 @@ 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.
* @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-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.
@@ -48,8 +50,11 @@ 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.
@@ -89,10 +94,24 @@ 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;
}
if (e.composedPath().find((el: Element) => el.hasAttribute?.('data-toggle-nav'))) {
const path = e.composedPath();
const navigationToggleSlot = this.navigationToggleSlot;
if (
path.find((el: Element) => {
return (
el.hasAttribute?.('data-toggle-nav') ||
el.assignedSlot === navigationToggleSlot ||
el === navigationToggleSlot
);
})
) {
e.preventDefault();
this.toggleNavigation();
}
@@ -103,6 +122,7 @@ 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
@@ -127,6 +147,12 @@ 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) {
@@ -168,11 +194,21 @@ 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));
});
}
@@ -235,6 +271,13 @@ 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">
@@ -285,19 +328,20 @@ export default class WaPage extends WebAwesomeElement {
@wa-after-show=${() => (this.navOpen = this.navigationDrawer.open)}
@wa-after-hide=${() => (this.navOpen = this.navigationDrawer.open)}
exportparts="
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
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,
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,8 +1,11 @@
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 () => {
@@ -10,6 +13,26 @@ 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,6 +2,7 @@ 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';
@@ -60,6 +61,7 @@ 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;
@@ -276,7 +278,7 @@ export default class WaPopup extends WebAwesomeElement {
private start() {
// We can't start the positioner without an anchor
if (!this.anchorEl) {
if (!this.anchorEl || !this.active) {
return;
}
@@ -418,7 +420,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.matches(':dir(rtl)');
const isRtl = this.localize.dir() === 'rtl';
const staticSide = { top: 'bottom', right: 'left', bottom: 'top', left: 'right' }[placement.split('-')[0]]!;
this.setAttribute('data-current-placement', placement);

View File

@@ -297,6 +297,102 @@ 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,14 +170,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
}
private handleLabelClick() {
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();
}
this.focus();
}
private async syncRadioElements() {
@@ -305,6 +298,19 @@ 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.matches(':dir(rtl)');
const isRtl = this.localize.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,6 +3,7 @@ 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';
@@ -37,6 +38,8 @@ 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;
@@ -80,7 +83,7 @@ export default class WaRating extends WebAwesomeElement {
}
private getValueFromXCoordinate(coordinate: number) {
const isRtl = this.matches(':dir(rtl)');
const isRtl = this.localize.dir() === 'rtl';
const { left, right, width } = this.rating.getBoundingClientRect();
const value = isRtl
? this.roundToPrecision(((right - coordinate) / width) * this.max, this.precision)
@@ -109,7 +112,7 @@ export default class WaRating extends WebAwesomeElement {
private handleKeyDown(event: KeyboardEvent) {
const isLtr = this.matches(':dir(ltr)');
const isRtl = this.matches(':dir(rtl)');
const isRtl = this.localize.dir() === 'rtl';
const oldValue = this.value;
if (this.disabled || this.readonly) {
@@ -214,7 +217,7 @@ export default class WaRating extends WebAwesomeElement {
}
render() {
const isRtl = this.hasUpdated ? this.matches(':dir(rtl)') : this.dir;
const isRtl = this.hasUpdated ? this.localize.dir() === 'rtl' : this.dir;
const counter = Array.from(Array(this.max).keys());
let displayValue = 0;
@@ -240,7 +243,7 @@ export default class WaRating extends WebAwesomeElement {
aria-valuenow=${this.value}
aria-valuemin=${0}
aria-valuemax=${this.max}
tabindex=${this.disabled ? '-1' : '0'}
tabindex=${this.disabled || this.readonly ? '-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 .select__prefix::slotted(*) {
.select--small.select--multiple:not(.select--placeholder-visible) .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 .select__prefix::slotted(*) {
.select--medium.select--multiple:not(.select--placeholder-visible) .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 .select__prefix::slotted(*) {
.select--large.select--multiple:not(.select--placeholder-visible) .select__prefix::slotted(*) {
margin-inline-start: var(--wa-space-l);
}

View File

@@ -1,4 +1,4 @@
import { aTimeout, expect, oneEvent, waitUntil } from '@open-wc/testing';
import { aTimeout, expect, waitUntil } from '@open-wc/testing';
import { clickOnElement } from '../../internal/test.js';
import { fixtures } from '../../internal/test/fixture.js';
import { html } from 'lit';
@@ -217,6 +217,29 @@ 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>
@@ -502,7 +525,7 @@ describe('<wa-select>', () => {
expect(displayInput.value).to.equal('Option 1');
option.textContent = 'updated';
await oneEvent(option, 'slotchange');
await aTimeout(250);
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.matches(':dir(rtl)') : this.dir === 'rtl';
const isRtl = this.hasUpdated ? this.localize.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.matches(':dir(rtl)') : this.dir === 'rtl';
const isRtl = this.hasUpdated ? this.localize.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 slot="nav" panel="general">General</wa-tab>
<wa-tab panel="general">General</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
</wa-tab-group>
`);
@@ -86,10 +86,17 @@ 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 slot="nav" panel="general">General</wa-tab>
<wa-tab panel="general">General</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
</wa-tab-group>
`);
@@ -100,8 +107,8 @@ describe('<wa-tab-group>', () => {
it('displays all tabs', async () => {
const tabGroup = await fixture<WaTabGroup>(html`
<wa-tab-group>
<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="general" data-testid="general-tab-header">General</wa-tab>
<wa-tab 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>
@@ -114,8 +121,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 slot="nav" panel="general">General</wa-tab>
<wa-tab slot="nav" panel="custom">Custom</wa-tab>
<wa-tab panel="general">General</wa-tab>
<wa-tab 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>
@@ -128,7 +135,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 slot="nav" panel="general">General</wa-tab>
<wa-tab panel="general">General</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
</wa-tab-group>
`);
@@ -142,7 +149,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 slot="nav" panel="general">General</wa-tab>
<wa-tab panel="general">General</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
</wa-tab-group>
`);
@@ -157,7 +164,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 slot="nav" panel="general">General</wa-tab>
<wa-tab panel="general">General</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
</wa-tab-group>
`);
@@ -172,7 +179,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 slot="nav" panel="general">General</wa-tab>
<wa-tab panel="general">General</wa-tab>
<wa-tab-panel name="general">This is the general tab panel.</wa-tab-panel>
</wa-tab-group>
`);
@@ -190,7 +197,7 @@ describe('<wa-tab-group>', () => {
const result: HTMLTemplateResult[] = [];
for (let i = 0; i < n; i++) {
result.push(
html`<wa-tab slot="nav" panel="tab-${i}">Tab ${i}</wa-tab>
html`<wa-tab panel="tab-${i}">Tab ${i}</wa-tab>
<wa-tab-panel name="tab-${i}">Content of tab ${i}0</wa-tab-panel> `
);
}
@@ -350,8 +357,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 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="general" data-testid="general-header">General</wa-tab>
<wa-tab 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>
@@ -361,11 +368,36 @@ describe('<wa-tab-group>', () => {
return expectCustomTabToBeActiveAfter(tabGroup, () => clickOnElement(customHeader!));
});
it('does not change if the active tab is reselected', async () => {
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">Custom</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-panel name="general" data-testid="general-tab-content"
>This is the general tab panel.</wa-tab-panel
>
@@ -380,8 +412,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 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="general" data-testid="general-header">General</wa-tab>
<wa-tab 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
>
@@ -396,8 +428,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 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="general" data-testid="general-header">General</wa-tab>
<wa-tab 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>
@@ -409,8 +441,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 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="general" data-testid="general-header">General</wa-tab>
<wa-tab 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>
@@ -438,8 +470,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 slot="nav" panel="general" data-testid="general-header">General</wa-tab>
<wa-tab slot="nav" panel="disabled" disabled>Disabled</wa-tab>
<wa-tab panel="general" data-testid="general-header">General</wa-tab>
<wa-tab 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
>
@@ -453,8 +485,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 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="general" data-testid="general-header">General</wa-tab>
<wa-tab 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