Merge branch 'next' of https://github.com/shoelace-style/webawesome into konnorrogers/layouts

This commit is contained in:
konnorrogers
2023-10-18 11:24:46 -04:00
61 changed files with 6206 additions and 3773 deletions

View File

@@ -1,4 +1,7 @@
contact_links:
- name: Feature Requests
url: https://github.com/shoelace-style/shoelace/discussions/categories/ideas
about: All requests for new features should go here.
- name: Help & Support
url: https://github.com/shoelace-style/shoelace/discussions/categories/help
about: Please don't create issues for personal help requests. Instead, ask your question on the discussion forum.

View File

@@ -1,15 +0,0 @@
---
name: Feature Request
about: Suggest an idea for this project.
title: ''
labels: feature
---
### What issue are you having?
Provide a clear and concise description of the problem you're facing.
### Describe the solution you'd like
How would you like to see the library solve it?
### Describe alternatives you've considered
In what ways have you tried to solve this with the current version?

View File

@@ -254,7 +254,9 @@ pre .token.italic {
right: 0;
white-space: normal;
color: var(--wa-color-neutral-text-on-muted-alt);
transition: 150ms opacity, 150ms scale;
transition:
150ms opacity,
150ms scale;
}
.copy-code-button::part(button) {

View File

@@ -803,7 +803,7 @@ const App = () => (
### Aspect Ratio
Use the `--aspect-ratio` custom property to customize the size of the carousel's viewport.
Use the `--aspect-ratio` custom property to customize the size of the carousel's viewport from the default value of 16/9.
```html:preview
<wa-carousel class="aspect-ratio" navigation pagination style="--aspect-ratio: 3/2;">

View File

@@ -212,9 +212,30 @@ If `settings.json` already exists, simply add the above line to the root of the
### JetBrains IDEs
If you are using a [JetBrains IDE](https://www.jetbrains.com/) and you are installing Web Awesome from NPM, the editor will automatically detect the `web-types.json` file from the package and you should immediately see component information in your editor.
If you are using a [JetBrains IDE](https://www.jetbrains.com/) and you are installing Shoelace from NPM, the editor will automatically detect the `web-types.json` file from the package and you should immediately see component information in your editor.
If you are installing from the CDN, you can [download a local copy](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace/dist/web-types.json) and add it to the root of your project.
If you are installing from the CDN, you can [download a local copy](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace/dist/web-types.json) and add it to the root of your project. Be sure to add a reference to the `web-types.json` file in your `package.json` in order for your editor to properly detect it.
```json
{
...
"web-types": "./web-types.json"
...
}
```
If you are using types from multiple projects, you can add an array of references.
```json
{
...
"web-types": [
...,
"./web-types.json"
]
...
}
```
### Other Editors

View File

@@ -85,7 +85,7 @@ With Web Awesome, you can:
- Incrementally adopt components as needed (no need to ditch your framework)
- Upgrade or switch frameworks without rebuilding foundational components
If your organization is looking to build a design system, [Web Awesome will save you thousands of dollars](https://medium.com/eightshapes-llc/and-you-thought-buttons-were-easy-26eb5b5c1871).\* All the foundational components you need are right here, ready to be customized for your brand. And since it's built on web standards, browsers will continue to support it for many years to come.
If your organization is looking to build a design system, [Web Awesome will save you thousands of dollars](https://medium.com/eightshapes-llc/and-you-thought-buttons-were-easy-26eb5b5c1871). All the foundational components you need are right here, ready to be customized for your brand. And since it's built on web standards, browsers will continue to support it for many years to come.
Whether you use Web Awesome as a starting point for your organization's design system or for a fun personal project, there's no limit to what you can do with it.

View File

@@ -20,12 +20,21 @@ New versions of Web Awesome are released as-needed and generally occur when a cr
- Removed `default` from `<wa-button>` and made `neutral` the new default
- Removed the `circle` modifier from `<wa-button>` because button's no longer have a set height
## Next
## 2.10.0
- Added the Simplified Chinese translation [#1604]
- Fixed a bug [in the localize dependency](https://github.com/shoelace-style/localize/issues/20) that caused underscores in language codes to throw a `RangeError`
- Fixed a bug in the focus trapping utility used by modals that caused unexpected focus behavior. [#1583]
- Fixed a bug in `<sl-copy-button>` that prevented exported tooltip parts from being styled [#1586]
- Fixed a bug in `<sl-menu>` that caused it not to fire the `sl-select` event if you clicked an element inside of a `<sl-menu-item>` [#1599]
- Fixed a bug that caused focus trap logic to hang the browser in certain circumstances [#1612]
- Improved submenu selection by implementing the [safe triangle](https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles/) method [#1550]
- Updated `@shoelace-style/localize` to 3.1.0
- Updated `@lib-labs/react` to stable `@lit/react`
- Updated Bootstrap Icons to 1.11.1
- Updated Lit to 3.0.0
- Updated TypeScript to 5.2.2
- Updated all other dependencies to latest versions
## 2.9.0

View File

@@ -36,6 +36,7 @@ I realize that one cannot reasonably enforce this any more than one can enforce
The [issue tracker](https://github.com/shoelace-style/shoelace/issues) is for bug reports, feature requests, and pull requests.
- Please **do not** use the issue tracker for personal support requests. Use [the discussion forum](https://github.com/shoelace-style/shoelace/discussions/categories/help) instead.
- Please **do not** use the issue tracker for feature requests. Use [the discussion forum](https://github.com/shoelace-style/shoelace/discussions/categories/ideas) instead.
- Please **do not** derail, hijack, or troll issues. Keep the discussion on topic and be respectful of others.
- Please **do not** post comments with "+1" or "👍". Use [reactions](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) instead.
- Please **do** use the issue tracker for feature requests, bug reports, and pull requests.
@@ -44,10 +45,10 @@ Issues that do not follow these guidelines are subject to closure. There simply
### Feature Requests
Feature requests can be added using the issue tracker.
Feature requests can be added using [the discussion forum](https://github.com/shoelace-style/shoelace/discussions/categories/ideas).
- Please **do** search for an existing request before suggesting a new feature.
- Please **do** use the "👍" reaction to vote for a feature.
- Please **do** use the voting buttons to vote for a feature.
- Please **do** share substantial use cases and perspective that support new features if they haven't already been mentioned.
- Please **do not** bump, spam, or ping contributors to prioritize your own feature.

View File

@@ -88,7 +88,7 @@ module.exports = environment;
The final step is to add the corresponding `pack_tags` to the page. You should have the following `tags` in the `<head>` section of `app/views/layouts/application.html.erb`.
```html
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<!-- ... -->

9005
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,8 +25,15 @@
"./dist/react/*": "./dist/react/*",
"./dist/translations/*": "./dist/translations/*"
},
"files": ["dist", "cdn"],
"keywords": ["web components", "custom elements", "components"],
"files": [
"dist",
"cdn"
],
"keywords": [
"web components",
"custom elements",
"components"
],
"repository": {
"type": "git",
"url": "git+https://github.com/shoelace-style/shoelace.git"
@@ -56,79 +63,82 @@
"node": ">=14.17.0"
},
"dependencies": {
"@ctrl/tinycolor": "^4.0.1",
"@floating-ui/dom": "^1.2.1",
"@lit-labs/react": "^2.0.3",
"@ctrl/tinycolor": "^4.0.2",
"@floating-ui/dom": "^1.5.3",
"@shoelace-style/animations": "^1.1.0",
"@shoelace-style/localize": "^3.1.2",
"composed-offset-position": "^0.0.4",
"lit": "^2.7.5",
"lit": "^3.0.0",
"qr-creator": "^1.0.0"
},
"devDependencies": {
"@11ty/eleventy": "^2.0.1",
"@custom-elements-manifest/analyzer": "^0.8.3",
"@open-wc/testing": "^3.1.7",
"@types/mocha": "^10.0.1",
"@types/react": "^18.0.26",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@web/dev-server-esbuild": "^0.3.3",
"@web/test-runner": "^0.15.0",
"@web/test-runner-commands": "^0.6.5",
"@custom-elements-manifest/analyzer": "^0.8.4",
"@lit/react": "^1.0.0",
"@open-wc/testing": "^3.2.0",
"@types/mocha": "^10.0.2",
"@types/react": "^18.2.28",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"@web/dev-server-esbuild": "^0.3.6",
"@web/test-runner": "^0.15.3",
"@web/test-runner-commands": "^0.6.6",
"@web/test-runner-playwright": "^0.9.0",
"bootstrap-icons": "^1.11.0",
"bootstrap-icons": "^1.11.1",
"browser-sync": "^2.29.3",
"chalk": "^5.2.0",
"chalk": "^5.3.0",
"change-case": "^4.1.2",
"command-line-args": "^5.2.1",
"comment-parser": "^1.3.1",
"comment-parser": "^1.4.0",
"cspell": "^6.18.1",
"custom-element-jet-brains-integration": "^1.1.0",
"custom-element-vs-code-integration": "^1.1.0",
"del": "^7.0.0",
"custom-element-jet-brains-integration": "^1.2.1",
"custom-element-vs-code-integration": "^1.2.1",
"del": "^7.1.0",
"download": "^8.0.0",
"esbuild": "^0.18.2",
"esbuild": "^0.19.4",
"esbuild-plugin-replace": "^1.4.0",
"eslint": "^8.44.0",
"eslint": "^8.51.0",
"eslint-plugin-chai-expect": "^3.0.0",
"eslint-plugin-chai-friendly": "^0.7.2",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-lit": "^1.8.3",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-lit": "^1.9.1",
"eslint-plugin-lit-a11y": "^4.1.0",
"eslint-plugin-markdown": "^3.0.0",
"eslint-plugin-markdown": "^3.0.1",
"eslint-plugin-sort-imports-es6-autofix": "^0.6.0",
"eslint-plugin-wc": "^1.5.0",
"eslint-plugin-wc": "^2.0.4",
"front-matter": "^4.0.2",
"get-port": "^7.0.0",
"globby": "^13.1.3",
"globby": "^13.2.2",
"husky": "^8.0.3",
"jsdom": "^22.1.0",
"jsonata": "^2.0.1",
"lint-staged": "^13.1.0",
"jsonata": "^2.0.3",
"lint-staged": "^14.0.1",
"lunr": "^2.3.9",
"markdown-it-container": "^3.0.0",
"markdown-it-ins": "^3.0.1",
"markdown-it-kbd": "^2.2.2",
"markdown-it-mark": "^3.0.1",
"markdown-it-replace-it": "^1.0.0",
"npm-check-updates": "^16.6.2",
"ora": "^6.3.1",
"npm-check-updates": "^16.14.6",
"ora": "^7.0.1",
"pascal-case": "^3.1.2",
"plop": "^3.1.1",
"prettier": "^2.8.8",
"plop": "^4.0.0",
"prettier": "^3.0.3",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"recursive-copy": "^2.0.14",
"sinon": "^15.0.1",
"sinon": "^16.1.0",
"smartquotes": "^2.3.2",
"source-map": "^0.7.4",
"strip-css-comments": "^5.0.0",
"tslib": "^2.4.1",
"typescript": "^5.1.3",
"user-agent-data-types": "^0.3.0"
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"user-agent-data-types": "^0.3.1"
},
"lint-staged": {
"*.{ts,js}": ["eslint --max-warnings 0 --cache --fix", "prettier --write"]
"*.{ts,js}": [
"eslint --max-warnings 0 --cache --fix",
"prettier --write"
]
}
}

View File

@@ -1,5 +1,5 @@
/* eslint-env node */
module.exports = {
/** @type {import("prettier").Config} */
const config = {
arrowParens: 'avoid',
bracketSpacing: true,
htmlWhitespaceSensitivity: 'css',
@@ -16,3 +16,5 @@ module.exports = {
trailingComma: 'none',
useTabs: false
};
export default config;

View File

@@ -87,7 +87,7 @@ async function buildTheDocs(watch = false) {
// Builds the source with esbuild.
//
async function buildTheSource() {
const alwaysExternal = ['@lit-labs/react', 'react'];
const alwaysExternal = ['@lit/react', 'react'];
const cdnConfig = {
format: 'esm',
@@ -122,7 +122,7 @@ async function buildTheSource() {
// We don't bundle certain dependencies in the unbundled build. This ensures we ship bare module specifiers,
// allowing end users to better optimize when using a bundler. (Only packages that ship ESM can be external.)
//
// We never bundle React or @lit-labs/react though!
// We never bundle React or @lit/react though!
//
external: alwaysExternal,
splitting: true,

View File

@@ -1,10 +1,9 @@
import commandLineArgs from 'command-line-args';
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import { deleteSync } from 'del';
import prettier from 'prettier';
import prettierConfig from '../prettier.config.cjs';
import { default as prettierConfig } from '../prettier.config.js';
import { getAllComponents } from './shared.js';
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
@@ -20,7 +19,7 @@ const metadata = JSON.parse(fs.readFileSync(path.join(outdir, 'custom-elements.j
const components = getAllComponents(metadata);
const index = [];
components.map(component => {
components.forEach(async component => {
const tagWithoutPrefix = component.tagName.replace(/^wa-/, '');
const componentDir = path.join(reactDir, tagWithoutPrefix);
const componentFile = path.join(componentDir, 'index.ts');
@@ -31,8 +30,7 @@ components.map(component => {
const eventExports = (component.events || [])
.map(event => `export type { ${event.eventName} } from '../../../src/events/events';`)
.join('\n');
const eventNameImport =
(component.events || []).length > 0 ? `import { type EventName } from '@lit-labs/react';` : ``;
const eventNameImport = (component.events || []).length > 0 ? `import { type EventName } from '@lit/react';` : ``;
const events = (component.events || [])
.map(event => `${event.reactName}: '${event.name}' as EventName<${event.eventName}>`)
.join(',\n');
@@ -41,10 +39,10 @@ components.map(component => {
const jsDoc = component.jsDoc || '';
const source = prettier.format(
const source = await prettier.format(
`
import * as React from 'react';
import { createComponent } from '@lit-labs/react';
import { createComponent } from '@lit/react';
import Component from '../../${importPath}';
${eventNameImport}

View File

@@ -24,7 +24,7 @@ filesToEmbed.forEach(file => {
});
// Loop through each theme file, copying the .css and generating a .js version for Lit users
files.forEach(file => {
files.forEach(async file => {
let source = fs.readFileSync(file, 'utf8');
// If the source has "/* _filename.css */" in it, replace it with the embedded styles
@@ -32,11 +32,11 @@ files.forEach(file => {
source = source.replace(`/* ${key} */`, embeds[key]);
});
const css = prettier.format(stripComments(source), {
const css = await prettier.format(stripComments(source), {
parser: 'css'
});
let js = prettier.format(
let js = await prettier.format(
`
import { css } from 'lit';

View File

@@ -210,10 +210,12 @@ describe('<wa-alert>', () => {
};
it('deletes the toast stack after the last alert is done', async () => {
const container = await fixture<HTMLElement>(html`<div>
<wa-alert data-testid="alert1" closable>alert 1</wa-alert>
<wa-alert data-testid="alert2" closable>alert 2</wa-alert>
</div>`);
const container = await fixture<HTMLElement>(
html`<div>
<wa-alert data-testid="alert1" closable>alert 1</wa-alert>
<wa-alert data-testid="alert2" closable>alert 2</wa-alert>
</div>`
);
const alert1 = queryByTestId<WaAlert>(container, 'alert1');
const alert2 = queryByTestId<WaAlert>(container, 'alert2');

View File

@@ -22,6 +22,7 @@ export default css`
font-size: calc(var(--size) * 0.5);
color: var(--wa-color-neutral-text-on-vivid);
user-select: none;
-webkit-user-select: none;
vertical-align: middle;
}

View File

@@ -20,6 +20,7 @@ export default css`
white-space: nowrap;
padding: 0.35em 0.6em;
user-select: none;
-webkit-user-select: none;
cursor: inherit;
}

View File

@@ -80,5 +80,6 @@ export default css`
align-items: center;
margin: 0 var(--wa-space-xs);
user-select: none;
-webkit-user-select: none;
}
`;

View File

@@ -21,11 +21,15 @@ export default css`
font-weight: var(--wa-font-weight-action);
text-decoration: none;
user-select: none;
-webkit-user-select: none;
white-space: nowrap;
vertical-align: middle;
padding: 0;
transition: var(--wa-transition-faster) background-color, var(--wa-transition-faster) color,
var(--wa-transition-faster) border, var(--wa-transition-faster) box-shadow;
transition:
var(--wa-transition-faster) background-color,
var(--wa-transition-faster) color,
var(--wa-transition-faster) border,
var(--wa-transition-faster) box-shadow;
cursor: inherit;
}

View File

@@ -98,25 +98,25 @@ describe('<wa-button>', () => {
});
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 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 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> `
);
const el = await fixture<WaButton>(html`
<wa-button href="https://example.com/" target="_blank" rel="1">Link</wa-button>
`);
const link = el.shadowRoot!.querySelector('a')!;
expect(link?.getAttribute('rel')).to.equal('1');
});

View File

@@ -7,9 +7,9 @@ describe('<wa-card>', () => {
describe('when provided no parameters', () => {
before(async () => {
el = await fixture<WaCard>(
html` <wa-card>This is just a basic card. No image, no header, and no footer. Just your content.</wa-card> `
);
el = await fixture<WaCard>(html`
<wa-card>This is just a basic card. No image, no header, and no footer. Just your content.</wa-card>
`);
});
it('should pass accessibility tests', async () => {

View File

@@ -40,7 +40,7 @@ import type { CSSResultGroup } from 'lit';
* @csspart navigation-button--next - Applied to the next button.
*
* @cssproperty --slide-gap - The space between each slide.
* @cssproperty --aspect-ratio - The aspect ratio of each slide.
* @cssproperty [--aspect-ratio=16/9] - The aspect ratio of each slide.
* @cssproperty --scroll-hint - The amount of padding to apply to the scroll area, allowing adjacent slides to become
* partially visible as a scroll hint.
*/

View File

@@ -45,8 +45,11 @@ export default css`
border-radius: min(0.375rem, var(--wa-corners-half)); /* min so it doesn't look like a circle/checkbox */
background-color: var(--wa-form-controls-background);
color: var(--wa-form-controls-text-color);
transition: var(--wa-transition-fast) border-color, var(--wa-transition-fast) background-color,
var(--wa-transition-fast) color, var(--wa-transition-fast) box-shadow;
transition:
var(--wa-transition-fast) border-color,
var(--wa-transition-fast) background-color,
var(--wa-transition-fast) color,
var(--wa-transition-fast) box-shadow;
}
.checkbox__input {
@@ -97,6 +100,7 @@ export default css`
line-height: var(--toggle-size);
margin-inline-start: var(--wa-space-xs);
user-select: none;
-webkit-user-select: none;
}
:host([required]) .checkbox__label::after {

View File

@@ -22,6 +22,7 @@ export default css`
background-color: var(--wa-color-surface-raised);
border-radius: var(--wa-corners-1x);
user-select: none;
-webkit-user-select: none;
}
.color-picker--inline {
@@ -243,7 +244,11 @@ export default css`
linear-gradient(45deg, transparent 75%, var(--wa-color-neutral-fill-muted-alt) 75%),
linear-gradient(45deg, var(--wa-color-neutral-fill-muted-alt) 25%, transparent 25%);
background-size: 10px 10px;
background-position: 0 0, 0 0, -5px -5px, 5px 5px;
background-position:
0 0,
0 0,
-5px -5px,
5px 5px;
}
.color-picker--disabled {
@@ -309,7 +314,9 @@ export default css`
height: 100%;
border-radius: inherit;
background-color: currentColor;
box-shadow: inset 0 0 0 2px var(--wa-form-controls-border-color-resting), inset 0 0 0 4px var(--wa-color-white);
box-shadow:
inset 0 0 0 2px var(--wa-form-controls-border-color-resting),
inset 0 0 0 4px var(--wa-color-white);
}
.color-dropdown__trigger--empty:before {

View File

@@ -97,9 +97,9 @@ describe('<wa-color-picker>', () => {
});
it('should render the correct swatches when passing a string of color values', async () => {
const el = await fixture<WaColorPicker>(
html` <wa-color-picker swatches="red; #008000; rgb(0,0,255);"></wa-color-picker> `
);
const el = await fixture<WaColorPicker>(html`
<wa-color-picker swatches="red; #008000; rgb(0,0,255);"></wa-color-picker>
`);
const swatches = [...el.shadowRoot!.querySelectorAll('[part~="swatch"] > div')];
expect(swatches.length).to.equal(3);

View File

@@ -24,6 +24,7 @@ export default css`
align-items: center;
padding: var(--wa-space-m);
user-select: none;
-webkit-user-select: none;
cursor: pointer;
}

View File

@@ -17,9 +17,9 @@ describe('<wa-dialog>', () => {
});
it('should not be visible without the open attribute', async () => {
const el = await fixture<WaDialog>(
html` <wa-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</wa-dialog> `
);
const el = await fixture<WaDialog>(html`
<wa-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</wa-dialog>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
expect(base.hidden).to.be.true;

View File

@@ -16,9 +16,9 @@ describe('<wa-drawer>', () => {
});
it('should not be visible without the open attribute', async () => {
const el = await fixture<WaDrawer>(
html` <wa-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</wa-drawer> `
);
const el = await fixture<WaDrawer>(html`
<wa-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</wa-drawer>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
expect(base.hidden).to.be.true;

View File

@@ -52,11 +52,9 @@ describe('<wa-format-date>', () => {
];
results.forEach(setup => {
it(`date has correct language format: ${setup.lang}`, async () => {
const el = await fixture<WaFormatDate>(
html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" lang="${setup.lang}"></wa-format-date>
`
);
const el = await fixture<WaFormatDate>(html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" lang="${setup.lang}"></wa-format-date>
`);
expect(el.shadowRoot?.textContent?.trim()).to.equal(setup.result);
});
});
@@ -66,14 +64,12 @@ describe('<wa-format-date>', () => {
const weekdays = ['narrow', 'short', 'long'];
weekdays.forEach((weekdayFormat: 'narrow' | 'short' | 'long') => {
it(`date has correct weekday format: ${weekdayFormat}`, async () => {
const el = await fixture<WaFormatDate>(
html`
<wa-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
weekday="${weekdayFormat}"
></wa-format-date>
`
);
const el = await fixture<WaFormatDate>(html`
<wa-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
weekday="${weekdayFormat}"
></wa-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { weekday: weekdayFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@@ -87,11 +83,9 @@ describe('<wa-format-date>', () => {
const eras = ['narrow', 'short', 'long'];
eras.forEach((eraFormat: 'narrow' | 'short' | 'long') => {
it(`date has correct era format: ${eraFormat}`, async () => {
const el = await fixture<WaFormatDate>(
html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" era="${eraFormat}"></wa-format-date>
`
);
const el = await fixture<WaFormatDate>(html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" era="${eraFormat}"></wa-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { era: eraFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@@ -105,11 +99,9 @@ describe('<wa-format-date>', () => {
const yearFormats = ['numeric', '2-digit'];
yearFormats.forEach((yearFormat: 'numeric' | '2-digit') => {
it(`date has correct year format: ${yearFormat}`, async () => {
const el = await fixture<WaFormatDate>(
html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" year="${yearFormat}"></wa-format-date>
`
);
const el = await fixture<WaFormatDate>(html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" year="${yearFormat}"></wa-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { year: yearFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@@ -123,11 +115,9 @@ describe('<wa-format-date>', () => {
const monthFormats = ['numeric', '2-digit', 'narrow', 'short', 'long'];
monthFormats.forEach((monthFormat: 'numeric' | '2-digit' | 'narrow' | 'short' | 'long') => {
it(`date has correct month format: ${monthFormat}`, async () => {
const el = await fixture<WaFormatDate>(
html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" month="${monthFormat}"></wa-format-date>
`
);
const el = await fixture<WaFormatDate>(html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" month="${monthFormat}"></wa-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { month: monthFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@@ -141,11 +131,9 @@ describe('<wa-format-date>', () => {
const dayFormats = ['numeric', '2-digit'];
dayFormats.forEach((dayFormat: 'numeric' | '2-digit') => {
it(`date has correct day format: ${dayFormat}`, async () => {
const el = await fixture<WaFormatDate>(
html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" day="${dayFormat}"></wa-format-date>
`
);
const el = await fixture<WaFormatDate>(html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" day="${dayFormat}"></wa-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { day: dayFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@@ -159,11 +147,9 @@ describe('<wa-format-date>', () => {
const hourFormats = ['numeric', '2-digit'];
hourFormats.forEach((hourFormat: 'numeric' | '2-digit') => {
it(`date has correct hour format: ${hourFormat}`, async () => {
const el = await fixture<WaFormatDate>(
html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" hour="${hourFormat}"></wa-format-date>
`
);
const el = await fixture<WaFormatDate>(html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" hour="${hourFormat}"></wa-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { hour: hourFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@@ -177,14 +163,9 @@ describe('<wa-format-date>', () => {
const minuteFormats = ['numeric', '2-digit'];
minuteFormats.forEach((minuteFormat: 'numeric' | '2-digit') => {
it(`date has correct minute format: ${minuteFormat}`, async () => {
const el = await fixture<WaFormatDate>(
html`
<wa-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
minute="${minuteFormat}"
></wa-format-date>
`
);
const el = await fixture<WaFormatDate>(html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" minute="${minuteFormat}"></wa-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { minute: minuteFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@@ -198,14 +179,9 @@ describe('<wa-format-date>', () => {
const secondFormats = ['numeric', '2-digit'];
secondFormats.forEach((secondFormat: 'numeric' | '2-digit') => {
it(`date has correct second format: ${secondFormat}`, async () => {
const el = await fixture<WaFormatDate>(
html`
<wa-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
second="${secondFormat}"
></wa-format-date>
`
);
const el = await fixture<WaFormatDate>(html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" second="${secondFormat}"></wa-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { second: secondFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@@ -219,14 +195,12 @@ describe('<wa-format-date>', () => {
const timeZoneNameFormats = ['short', 'long'];
timeZoneNameFormats.forEach((timeZoneNameFormat: 'short' | 'long') => {
it(`date has correct timeZoneName format: ${timeZoneNameFormat}`, async () => {
const el = await fixture<WaFormatDate>(
html`
<wa-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
time-zone-name="${timeZoneNameFormat}"
></wa-format-date>
`
);
const el = await fixture<WaFormatDate>(html`
<wa-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
time-zone-name="${timeZoneNameFormat}"
></wa-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { timeZoneName: timeZoneNameFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@@ -240,14 +214,9 @@ describe('<wa-format-date>', () => {
const timeZones = ['America/New_York', 'America/Los_Angeles', 'Europe/Zurich'];
timeZones.forEach(timeZone => {
it(`date has correct timeZoneName format: ${timeZone}`, async () => {
const el = await fixture<WaFormatDate>(
html`
<wa-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
time-zone="${timeZone}"
></wa-format-date>
`
);
const el = await fixture<WaFormatDate>(html`
<wa-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" time-zone="${timeZone}"></wa-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { timeZone: timeZone }).format(
new Date(new Date().getFullYear(), 0, 1)
@@ -261,14 +230,12 @@ describe('<wa-format-date>', () => {
const hourFormatValues = ['auto', '12', '24'];
hourFormatValues.forEach(hourFormatValue => {
it(`date has correct hourFormat format: ${hourFormatValue}`, async () => {
const el = await fixture<WaFormatDate>(
html`
<wa-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
hour-format="${hourFormatValue as 'auto' | '12' | '24'}"
></wa-format-date>
`
);
const el = await fixture<WaFormatDate>(html`
<wa-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
hour-format="${hourFormatValue as 'auto' | '12' | '24'}"
></wa-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', {
hour12: hourFormatValue === 'auto' ? undefined : hourFormatValue === '12'

View File

@@ -24,9 +24,9 @@ describe('<wa-format-number>', () => {
describe('lang property', () => {
['de', 'de-CH', 'fr', 'es', 'he', 'ja', 'nl', 'pl', 'pt', 'ru'].forEach(lang => {
it(`number has correct language format: ${lang}`, async () => {
const el = await fixture<WaFormatNumber>(
html` <wa-format-number value="1000" lang="${lang}"></wa-format-number> `
);
const el = await fixture<WaFormatNumber>(html`
<wa-format-number value="1000" lang="${lang}"></wa-format-number>
`);
const expected = new Intl.NumberFormat(lang, { style: 'decimal', useGrouping: true }).format(1000);
expect(el.shadowRoot?.textContent).to.equal(expected);
});
@@ -36,9 +36,9 @@ describe('<wa-format-number>', () => {
describe('type property', () => {
['currency', 'decimal', 'percent'].forEach(type => {
it(`number has correct type format: ${type}`, async () => {
const el = await fixture<WaFormatNumber>(
html` <wa-format-number value="1000" type="${type}"></wa-format-number> `
);
const el = await fixture<WaFormatNumber>(html`
<wa-format-number value="1000" type="${type}"></wa-format-number>
`);
const expected = new Intl.NumberFormat('en-US', { style: type, currency: 'USD' }).format(1000);
expect(el.shadowRoot?.textContent).to.equal(expected);
});
@@ -62,9 +62,9 @@ describe('<wa-format-number>', () => {
describe('currency property', () => {
['USD', 'CAD', 'AUD', 'UAH'].forEach(currency => {
it(`number has correct type format: ${currency}`, async () => {
const el = await fixture<WaFormatNumber>(
html` <wa-format-number value="1000" currency="${currency}"></wa-format-number> `
);
const el = await fixture<WaFormatNumber>(html`
<wa-format-number value="1000" currency="${currency}"></wa-format-number>
`);
const expected = new Intl.NumberFormat('en-US', { style: 'decimal', currency: currency }).format(1000);
expect(el.shadowRoot?.textContent).to.equal(expected);
});
@@ -74,9 +74,9 @@ describe('<wa-format-number>', () => {
describe('currencyDisplay property', () => {
['symbol', 'narrowSymbol', 'code', 'name'].forEach(currencyDisplay => {
it(`number has correct type format: ${currencyDisplay}`, async () => {
const el = await fixture<WaFormatNumber>(
html` <wa-format-number value="1000" currency-display="${currencyDisplay}"></wa-format-number> `
);
const el = await fixture<WaFormatNumber>(html`
<wa-format-number value="1000" currency-display="${currencyDisplay}"></wa-format-number>
`);
const expected = new Intl.NumberFormat('en-US', { style: 'decimal', currencyDisplay: currencyDisplay }).format(
1000
);
@@ -88,9 +88,9 @@ describe('<wa-format-number>', () => {
describe('minimumIntegerDigits property', () => {
[4, 5, 6].forEach(minDigits => {
it(`number has correct type format: ${minDigits}`, async () => {
const el = await fixture<WaFormatNumber>(
html` <wa-format-number value="1000" minimum-integer-digits="${minDigits}"></wa-format-number> `
);
const el = await fixture<WaFormatNumber>(html`
<wa-format-number value="1000" minimum-integer-digits="${minDigits}"></wa-format-number>
`);
const expected = new Intl.NumberFormat('en-US', {
style: 'decimal',
currencyDisplay: 'symbol',
@@ -104,9 +104,9 @@ describe('<wa-format-number>', () => {
describe('minimumFractionDigits property', () => {
[4, 5, 6].forEach(minFractionDigits => {
it(`number has correct type format: ${minFractionDigits}`, async () => {
const el = await fixture<WaFormatNumber>(
html` <wa-format-number value="1000" minimum-fraction-digits="${minFractionDigits}"></wa-format-number> `
);
const el = await fixture<WaFormatNumber>(html`
<wa-format-number value="1000" minimum-fraction-digits="${minFractionDigits}"></wa-format-number>
`);
const expected = new Intl.NumberFormat('en-US', {
style: 'decimal',
currencyDisplay: 'symbol',
@@ -120,9 +120,9 @@ describe('<wa-format-number>', () => {
describe('maximumFractionDigits property', () => {
[4, 5, 6].forEach(maxFractionDigits => {
it(`number has correct type format: ${maxFractionDigits}`, async () => {
const el = await fixture<WaFormatNumber>(
html` <wa-format-number value="1000" maximum-fraction-digits="${maxFractionDigits}"></wa-format-number> `
);
const el = await fixture<WaFormatNumber>(html`
<wa-format-number value="1000" maximum-fraction-digits="${maxFractionDigits}"></wa-format-number>
`);
const expected = new Intl.NumberFormat('en-US', {
style: 'decimal',
currencyDisplay: 'symbol',
@@ -136,11 +136,9 @@ describe('<wa-format-number>', () => {
describe('minimumSignificantDigits property', () => {
[4, 5, 6].forEach(minSignificantDigits => {
it(`number has correct type format: ${minSignificantDigits}`, async () => {
const el = await fixture<WaFormatNumber>(
html`
<wa-format-number value="1000" minimum-significant-digits="${minSignificantDigits}"></wa-format-number>
`
);
const el = await fixture<WaFormatNumber>(html`
<wa-format-number value="1000" minimum-significant-digits="${minSignificantDigits}"></wa-format-number>
`);
const expected = new Intl.NumberFormat('en-US', {
style: 'decimal',
currencyDisplay: 'symbol',
@@ -154,11 +152,9 @@ describe('<wa-format-number>', () => {
describe('maximumSignificantDigits property', () => {
[4, 5, 6].forEach(maxSignificantDigits => {
it(`number has correct type format: ${maxSignificantDigits}`, async () => {
const el = await fixture<WaFormatNumber>(
html`
<wa-format-number value="1000" maximum-significant-digits="${maxSignificantDigits}"></wa-format-number>
`
);
const el = await fixture<WaFormatNumber>(html`
<wa-format-number value="1000" maximum-significant-digits="${maxSignificantDigits}"></wa-format-number>
`);
const expected = new Intl.NumberFormat('en-US', {
style: 'decimal',
currencyDisplay: 'symbol',

View File

@@ -30,15 +30,13 @@ describe('<wa-icon-button>', () => {
describe('when styling the host element', () => {
it('renders the correct color and font size', async () => {
const el = await fixture<WaIconButton>(
html`
<wa-icon-button
library="system"
name="check"
style="color: rgb(0, 136, 221); font-size: 2rem;"
></wa-icon-button>
`
);
const el = await fixture<WaIconButton>(html`
<wa-icon-button
library="system"
name="check"
style="color: rgb(0, 136, 221); font-size: 2rem;"
></wa-icon-button>
`);
const icon = el.shadowRoot!.querySelector('wa-icon')!;
const styles = getComputedStyle(icon);
@@ -85,16 +83,16 @@ describe('<wa-icon-button>', () => {
describe('and target is present', () => {
['_blank', '_parent', '_self', '_top'].forEach((target: LinkTarget) => {
it(`the anchor target is the provided target: ${target}`, async () => {
const el = await fixture<WaIconButton>(
html` <wa-icon-button href="some/path" target="${target}"></wa-icon-button> `
);
const el = await fixture<WaIconButton>(html`
<wa-icon-button href="some/path" target="${target}"></wa-icon-button>
`);
expect(el.shadowRoot?.querySelector(`a[target="${target}"]`)).to.exist;
});
it(`the anchor rel is set to 'noreferrer noopener'`, async () => {
const el = await fixture<WaIconButton>(
html` <wa-icon-button href="some/path" target="${target}"></wa-icon-button> `
);
const el = await fixture<WaIconButton>(html`
<wa-icon-button href="some/path" target="${target}"></wa-icon-button>
`);
expect(el.shadowRoot?.querySelector(`a[rel="noreferrer noopener"]`)).to.exist;
});
});
@@ -103,9 +101,9 @@ describe('<wa-icon-button>', () => {
describe('and download is present', () => {
it(`the anchor download attribute is the provided download`, async () => {
const fakeDownload = 'some/path';
const el = await fixture<WaIconButton>(
html` <wa-icon-button href="some/path" download="${fakeDownload}"></wa-icon-button> `
);
const el = await fixture<WaIconButton>(html`
<wa-icon-button href="some/path" download="${fakeDownload}"></wa-icon-button>
`);
expect(el.shadowRoot?.querySelector(`a[download="${fakeDownload}"]`)).to.exist;
});
@@ -121,9 +119,9 @@ describe('<wa-icon-button>', () => {
it('the internal aria-label attribute is set to the provided label when rendering an anchor', async () => {
const fakeLabel = 'some label';
const el = await fixture<WaIconButton>(
html` <wa-icon-button href="some/path" label="${fakeLabel}"></wa-icon-button> `
);
const el = await fixture<WaIconButton>(html`
<wa-icon-button href="some/path" label="${fakeLabel}"></wa-icon-button>
`);
expect(el.shadowRoot?.querySelector(`a[aria-label="${fakeLabel}"]`)).to.exist;
});
});

View File

@@ -21,7 +21,9 @@ export default css`
vertical-align: middle;
overflow: hidden;
cursor: text;
transition: var(--wa-transition-fast) border, var(--wa-transition-fast) background-color;
transition:
var(--wa-transition-fast) border,
var(--wa-transition-fast) background-color;
}
/* Standard inputs */
@@ -101,6 +103,7 @@ export default css`
.input__control::placeholder {
color: var(--wa-form-controls-placeholder-color);
user-select: none;
-webkit-user-select: none;
}
.input__control:focus {

View File

@@ -31,6 +31,7 @@ export default css`
padding: var(--wa-space-2xs) var(--wa-space-2xs);
transition: var(--wa-transition-fast) fill;
user-select: none;
-webkit-user-select: none;
white-space: nowrap;
cursor: pointer;
}

View File

@@ -14,5 +14,6 @@ export default css`
color: var(--wa-color-neutral-text-on-surface);
padding: var(--wa-space-2xs) var(--wa-space-xl);
user-select: none;
-webkit-user-select: none;
}
`;

View File

@@ -1,9 +1,9 @@
import { html } from 'lit';
import { query } from 'lit/decorators.js';
import styles from './menu.styles.js';
import WaMenuItem from '../menu-item/menu-item.component.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type WaMenuItem from '../menu-item/menu-item.component.js';
export interface MenuSelectEventDetail {
item: WaMenuItem;
}
@@ -29,11 +29,14 @@ export default class WaMenu extends WebAwesomeElement {
}
private handleClick(event: MouseEvent) {
if (!(event.target instanceof WaMenuItem)) {
return;
}
const menuItemTypes = ['menuitem', 'menuitemcheckbox'];
const item: WaMenuItem = event.target;
const target = event.composedPath().find((el: Element) => menuItemTypes.includes(el?.getAttribute?.('role') || ''));
if (!target) return;
// This isn't true. But we use it for TypeScript checks below.
const item = target as WaMenuItem;
if (item.type === 'checkbox') {
item.checked = !item.checked;

View File

@@ -100,4 +100,24 @@ describe('<wa-menu>', () => {
expect(selectHandler).to.not.have.been.called;
});
// @see https://github.com/shoelace-style/shoelace/issues/1596
it('Should fire "wa-select" when clicking an element within a menu-item', async () => {
// eslint-disable-next-line
const selectHandler = sinon.spy(() => {});
const menu: WaMenu = await fixture(html`
<wa-menu>
<wa-menu-item>
<span>Menu item</span>
</wa-menu-item>
</wa-menu>
`);
menu.addEventListener('wa-select', selectHandler);
const span = menu.querySelector('span')!;
await clickOnElement(span);
expect(selectHandler).to.have.been.calledOnce;
});
});

View File

@@ -7,6 +7,7 @@ export default css`
:host {
display: block;
user-select: none;
-webkit-user-select: none;
}
:host(:focus) {

View File

@@ -31,8 +31,11 @@ export default css`
line-height: var(--height);
white-space: nowrap;
overflow: hidden;
transition: 400ms width, 400ms background-color;
transition:
400ms width,
400ms background-color;
user-select: none;
-webkit-user-select: none;
}
/* Indeterminate */

View File

@@ -73,12 +73,10 @@ describe('<wa-progress-bar>', () => {
describe('when provided a ariaLabelledBy, and value parameter', () => {
before(async () => {
el = await fixture<WaProgressBar>(
html`
<label id="labelledby">Progress Ring Label</label>
<wa-progress-bar ariaLabelledBy="labelledby" value="25"></wa-progress-bar>
`
);
el = await fixture<WaProgressBar>(html`
<label id="labelledby">Progress Ring Label</label>
<wa-progress-bar ariaLabelledBy="labelledby" value="25"></wa-progress-bar>
`);
});
it('should pass accessibility tests', async () => {

View File

@@ -66,5 +66,6 @@ export default css`
height: 100%;
text-align: center;
user-select: none;
-webkit-user-select: none;
}
`;

View File

@@ -52,12 +52,10 @@ describe('<wa-progress-ring>', () => {
describe('when provided a ariaLabelledBy, and value parameter', () => {
before(async () => {
el = await fixture<WaProgressRing>(
html`
<label id="labelledby">Progress Ring Label</label>
<wa-progress-ring ariaLabelledBy="labelledby" value="25"></wa-progress-ring>
`
);
el = await fixture<WaProgressRing>(html`
<label id="labelledby">Progress Ring Label</label>
<wa-progress-ring ariaLabelledBy="labelledby" value="25"></wa-progress-ring>
`);
});
it('should pass accessibility tests', async () => {

View File

@@ -54,8 +54,11 @@ export default css`
border-radius: 50%;
background-color: var(--wa-form-controls-background);
color: transparent;
transition: var(--wa-transition-fast) border-color, var(--wa-transition-fast) background-color,
var(--wa-transition-fast) color, var(--wa-transition-fast) box-shadow;
transition:
var(--wa-transition-fast) border-color,
var(--wa-transition-fast) background-color,
var(--wa-transition-fast) color,
var(--wa-transition-fast) box-shadow;
}
.radio__input {
@@ -96,5 +99,6 @@ export default css`
line-height: var(--toggle-size);
margin-inline-start: 0.5em;
user-select: none;
-webkit-user-select: none;
}
`;

View File

@@ -110,8 +110,11 @@ export default css`
border-radius: 50%;
background-color: var(--wa-color-brand-fill-vivid);
border-color: var(--wa-color-brand-fill-vivid);
transition: var(--wa-transition-fast) border-color, var(--wa-transition-fast) background-color,
var(--wa-transition-fast) color, var(--wa-transition-fast) box-shadow;
transition:
var(--wa-transition-fast) border-color,
var(--wa-transition-fast) background-color,
var(--wa-transition-fast) color,
var(--wa-transition-fast) box-shadow;
cursor: pointer;
}

View File

@@ -20,9 +20,9 @@ const expectFormattedRelativeTimeToBe = async (relativeTime: WaRelativeTime, exp
};
const createRelativeTimeWithDate = async (relativeDate: Date): Promise<WaRelativeTime> => {
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(
html` <wa-relative-time lang="en-US"></wa-relative-time> `
);
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(html`
<wa-relative-time lang="en-US"></wa-relative-time>
`);
relativeTime.date = relativeDate;
return relativeTime;
};
@@ -113,27 +113,27 @@ describe('wa-relative-time', () => {
it(`shows the correct relative time given a String object: ${testCase.expectedOutput}`, async () => {
const dateString = testCase.date.toISOString();
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(
html` <wa-relative-time lang="en-US" date="${dateString}"></wa-relative-time> `
);
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(html`
<wa-relative-time lang="en-US" date="${dateString}"></wa-relative-time>
`);
await expectFormattedRelativeTimeToBe(relativeTime, testCase.expectedOutput);
});
});
it('always shows numeric if requested via numeric property', async () => {
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(
html` <wa-relative-time lang="en-US" numeric="always"></wa-relative-time> `
);
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(html`
<wa-relative-time lang="en-US" numeric="always"></wa-relative-time>
`);
relativeTime.date = yesterday;
await expectFormattedRelativeTimeToBe(relativeTime, '1 day ago');
});
it('shows human readable form if appropriate and numeric property is auto', async () => {
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(
html` <wa-relative-time lang="en-US" numeric="auto"></wa-relative-time> `
);
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(html`
<wa-relative-time lang="en-US" numeric="auto"></wa-relative-time>
`);
relativeTime.date = yesterday;
await expectFormattedRelativeTimeToBe(relativeTime, 'yesterday');
@@ -150,9 +150,9 @@ describe('wa-relative-time', () => {
it('allows to use a short form of the unit', async () => {
const twoYearsAgo = new Date(currentTime.getTime() - 2 * nonLeapYearInSeconds);
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(
html` <wa-relative-time lang="en-US" numeric="always" format="short"></wa-relative-time> `
);
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(html`
<wa-relative-time lang="en-US" numeric="always" format="short"></wa-relative-time>
`);
relativeTime.date = twoYearsAgo;
await expectFormattedRelativeTimeToBe(relativeTime, '2 yr. ago');
@@ -160,18 +160,18 @@ describe('wa-relative-time', () => {
it('allows to use a long form of the unit', async () => {
const twoYearsAgo = new Date(currentTime.getTime() - 2 * nonLeapYearInSeconds);
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(
html` <wa-relative-time lang="en-US" numeric="always" format="long"></wa-relative-time> `
);
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(html`
<wa-relative-time lang="en-US" numeric="always" format="long"></wa-relative-time>
`);
relativeTime.date = twoYearsAgo;
await expectFormattedRelativeTimeToBe(relativeTime, '2 years ago');
});
it('is formatted according to the requested locale', async () => {
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(
html` <wa-relative-time lang="de-DE" numeric="auto"></wa-relative-time> `
);
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(html`
<wa-relative-time lang="de-DE" numeric="auto"></wa-relative-time>
`);
relativeTime.date = yesterday;
await expectFormattedRelativeTimeToBe(relativeTime, 'gestern');
@@ -192,9 +192,9 @@ describe('wa-relative-time', () => {
it('does not display a time element on invalid time string', async () => {
const invalidDateString = 'thisIsNotATimeString';
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(
html` <wa-relative-time lang="en-US" date="${invalidDateString}"></wa-relative-time> `
);
const relativeTime: WaRelativeTime = await fixture<WaRelativeTime>(html`
<wa-relative-time lang="en-US" date="${invalidDateString}"></wa-relative-time>
`);
await relativeTime.updateComplete;
expect(extractTimeElement(relativeTime)).to.be.null;

View File

@@ -44,7 +44,10 @@ export default css`
vertical-align: middle;
overflow: hidden;
cursor: pointer;
transition: var(--wa-transition-fast) color, var(--wa-transition-fast) border, var(--wa-transition-fast) box-shadow,
transition:
var(--wa-transition-fast) color,
var(--wa-transition-fast) border,
var(--wa-transition-fast) box-shadow,
var(--wa-transition-fast) background-color;
}

View File

@@ -43,19 +43,23 @@ describe('<wa-split-panel>', () => {
});
it('should be accessible', async () => {
const splitPanel = await fixture(html`<wa-split-panel>
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`);
const splitPanel = await fixture(
html`<wa-split-panel>
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`
);
await expect(splitPanel).to.be.accessible();
});
it('should show both panels', async () => {
const splitPanel = await fixture(html`<wa-split-panel>
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`);
const splitPanel = await fixture(
html`<wa-split-panel>
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`
);
expect(splitPanel).to.contain.text('Start');
expect(splitPanel).to.contain.text('End');
@@ -63,10 +67,12 @@ describe('<wa-split-panel>', () => {
describe('panel sizing horizontal', () => {
it('has two evenly sized panels by default', async () => {
const splitPanel = await fixture<WaSplitPanel>(html`<wa-split-panel>
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`);
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel>
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`
);
const startPanelWidth = getPanelWidth(splitPanel, 'start-panel');
const endPanelWidth = getPanelWidth(splitPanel, 'end-panel');
@@ -75,10 +81,12 @@ describe('<wa-split-panel>', () => {
});
it('changes the sizing of the panels based on the position attribute', async () => {
const splitPanel = await fixture<WaSplitPanel>(html`<wa-split-panel position="25">
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`);
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel position="25">
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`
);
const startPanelWidth = getPanelWidth(splitPanel, 'start-panel');
const endPanelWidth = getPanelWidth(splitPanel, 'end-panel');
@@ -87,10 +95,12 @@ describe('<wa-split-panel>', () => {
});
it('updates the position in pixels to the correct result', async () => {
const splitPanel = await fixture<WaSplitPanel>(html`<wa-split-panel position="25">
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`);
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel position="25">
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`
);
splitPanel.position = 10;
@@ -100,10 +110,12 @@ describe('<wa-split-panel>', () => {
});
it('emits the wa-reposition event on position change', async () => {
const splitPanel = await fixture<WaSplitPanel>(html`<wa-split-panel>
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`);
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel>
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`
);
const repositionPromise = oneEvent(splitPanel, 'wa-reposition');
splitPanel.position = 10;
@@ -111,10 +123,12 @@ describe('<wa-split-panel>', () => {
});
it('can be resized using the mouse', async () => {
const splitPanel = await fixture<WaSplitPanel>(html`<wa-split-panel>
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`);
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel>
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`
);
const positionInPixels = splitPanel.positionInPixels;
@@ -127,10 +141,12 @@ describe('<wa-split-panel>', () => {
});
it('cannot be resized if disabled', async () => {
const splitPanel = await fixture<WaSplitPanel>(html`<wa-split-panel disabled>
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`);
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel disabled>
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`
);
const positionInPixels = splitPanel.positionInPixels;
@@ -143,10 +159,12 @@ describe('<wa-split-panel>', () => {
});
it('snaps to predefined positions', async () => {
const splitPanel = await fixture<WaSplitPanel>(html`<wa-split-panel>
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`);
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel>
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`
);
const positionInPixels = splitPanel.positionInPixels;
splitPanel.snap = `${positionInPixels - 40}px`;
@@ -162,10 +180,12 @@ describe('<wa-split-panel>', () => {
describe('panel sizing vertical', () => {
it('has two evenly sized panels by default', async () => {
const splitPanel = await fixture<WaSplitPanel>(html`<wa-split-panel vertical style="height: 400px;">
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`);
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel vertical style="height: 400px;">
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`
);
const startPanelHeight = getPanelHeight(splitPanel, 'start-panel');
const endPanelHeight = getPanelHeight(splitPanel, 'end-panel');
@@ -174,10 +194,12 @@ describe('<wa-split-panel>', () => {
});
it('changes the sizing of the panels based on the position attribute', async () => {
const splitPanel = await fixture<WaSplitPanel>(html`<wa-split-panel position="25" vertical style="height: 400px;">
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`);
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel position="25" vertical style="height: 400px;">
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`
);
const startPanelHeight = getPanelHeight(splitPanel, 'start-panel');
const endPanelHeight = getPanelHeight(splitPanel, 'end-panel');
@@ -186,10 +208,12 @@ describe('<wa-split-panel>', () => {
});
it('updates the position in pixels to the correct result', async () => {
const splitPanel = await fixture<WaSplitPanel>(html`<wa-split-panel position="25" vertical style="height: 400px;">
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`);
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel position="25" vertical style="height: 400px;">
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`
);
splitPanel.position = 10;
@@ -199,10 +223,12 @@ describe('<wa-split-panel>', () => {
});
it('emits the wa-reposition event on position change ', async () => {
const splitPanel = await fixture<WaSplitPanel>(html`<wa-split-panel vertical style="height: 400px;">
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`);
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel vertical style="height: 400px;">
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`
);
const repositionPromise = oneEvent(splitPanel, 'wa-reposition');
splitPanel.position = 10;
@@ -210,10 +236,12 @@ describe('<wa-split-panel>', () => {
});
it('can be resized using the mouse ', async () => {
const splitPanel = await fixture<WaSplitPanel>(html`<wa-split-panel vertical style="height: 400px;">
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`);
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel vertical style="height: 400px;">
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`
);
const positionInPixels = splitPanel.positionInPixels;
@@ -226,10 +254,12 @@ describe('<wa-split-panel>', () => {
});
it('cannot be resized if disabled', async () => {
const splitPanel = await fixture<WaSplitPanel>(html`<wa-split-panel disabled vertical style="height: 400px;">
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`);
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel disabled vertical style="height: 400px;">
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`
);
const positionInPixels = splitPanel.positionInPixels;
@@ -242,10 +272,12 @@ describe('<wa-split-panel>', () => {
});
it('snaps to predefined positions', async () => {
const splitPanel = await fixture<WaSplitPanel>(html`<wa-split-panel vertical style="height: 400px;">
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`);
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel vertical style="height: 400px;">
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`
);
const positionInPixels = splitPanel.positionInPixels;
splitPanel.snap = `${positionInPixels - 40}px`;

View File

@@ -53,7 +53,9 @@ export default css`
background-color: var(--wa-color-neutral-fill-vivid);
border: solid var(--wa-border-width-thin) var(--wa-color-neutral-fill-muted);
border-radius: var(--height);
transition: var(--wa-transition-fast) border-color, var(--wa-transition-fast) background-color;
transition:
var(--wa-transition-fast) border-color,
var(--wa-transition-fast) background-color;
}
.switch__control .switch__thumb {
@@ -64,8 +66,11 @@ export default css`
border: var(--wa-form-controls-border-style) var(--wa-form-controls-border-width)
var(--wa-color-neutral-outline-vivid);
translate: calc((var(--width) - var(--height)) / -2);
transition: var(--wa-transition-fast) translate ease, var(--wa-transition-fast) background-color,
var(--wa-transition-fast) border-color, var(--wa-transition-fast) box-shadow;
transition:
var(--wa-transition-fast) translate ease,
var(--wa-transition-fast) background-color,
var(--wa-transition-fast) border-color,
var(--wa-transition-fast) box-shadow;
}
.switch__input {
@@ -104,6 +109,7 @@ export default css`
line-height: var(--height);
margin-inline-start: 0.5em;
user-select: none;
-webkit-user-select: none;
}
:host([required]) .switch__label::after {

View File

@@ -24,7 +24,9 @@ export default css`
.tab-group__indicator {
position: absolute;
transition: var(--wa-transition-fast) translate ease, var(--wa-transition-fast) width ease;
transition:
var(--wa-transition-fast) translate ease,
var(--wa-transition-fast) width ease;
}
.tab-group--has-scroll-controls .tab-group__nav-container {

View File

@@ -187,8 +187,10 @@ describe('<wa-tab-group>', () => {
const generateTabs = (n: number): HTMLTemplateResult[] => {
const result: HTMLTemplateResult[] = [];
for (let i = 0; i < n; i++) {
result.push(html`<wa-tab slot="nav" panel="tab-${i}">Tab ${i}</wa-tab>
<wa-tab-panel name="tab-${i}">Content of tab ${i}0</wa-tab-panel> `);
result.push(
html`<wa-tab slot="nav" panel="tab-${i}">Tab ${i}</wa-tab>
<wa-tab-panel name="tab-${i}">Content of tab ${i}0</wa-tab-panel> `
);
}
return result;
};

View File

@@ -18,8 +18,11 @@ export default css`
padding: var(--wa-space-m) var(--wa-space-l);
white-space: nowrap;
user-select: none;
-webkit-user-select: none;
cursor: pointer;
transition: var(--transition-speed) box-shadow, var(--transition-speed) color;
transition:
var(--transition-speed) box-shadow,
var(--transition-speed) color;
}
.tab:hover:not(.tab--disabled) {

View File

@@ -15,6 +15,7 @@ export default css`
line-height: 1;
white-space: nowrap;
user-select: none;
-webkit-user-select: none;
}
.tag__remove::part(base) {

View File

@@ -19,7 +19,9 @@ export default css`
color: var(--wa-form-controls-text-color);
line-height: var(--wa-form-controls-value-line-height);
vertical-align: middle;
transition: var(--wa-transition-fast) border, var(--wa-transition-fast) background-color;
transition:
var(--wa-transition-fast) border,
var(--wa-transition-fast) background-color;
cursor: text;
}
@@ -80,6 +82,7 @@ export default css`
.textarea__control::placeholder {
color: var(--wa-form-controls-placeholder-color);
user-select: none;
-webkit-user-select: none;
}
.textarea__control:focus {

View File

@@ -50,5 +50,6 @@ export default css`
padding: var(--wa-space-2xs) var(--wa-space-xs);
pointer-events: none;
user-select: none;
-webkit-user-select: none;
}
`;

View File

@@ -267,11 +267,10 @@ export default class WaTreeItem extends WebAwesomeElement {
${when(
this.selectable,
() =>
html`
<wa-checkbox
part="checkbox"
exportparts="
() => html`
<wa-checkbox
part="checkbox"
exportparts="
base:checkbox__base,
control:checkbox__control,
control--checked:checkbox__control--checked,
@@ -280,13 +279,13 @@ export default class WaTreeItem extends WebAwesomeElement {
indeterminate-icon:checkbox__indeterminate-icon,
label:checkbox__label
"
class="tree-item__checkbox"
?disabled="${this.disabled}"
?checked="${live(this.selected)}"
?indeterminate="${this.indeterminate}"
tabindex="-1"
></wa-checkbox>
`
class="tree-item__checkbox"
?disabled="${this.disabled}"
?checked="${live(this.selected)}"
?indeterminate="${this.indeterminate}"
tabindex="-1"
></wa-checkbox>
`
)}
<slot class="tree-item__label" part="label"></slot>

View File

@@ -26,6 +26,7 @@ export default css`
color: var(--wa-color-text-normal);
cursor: pointer;
user-select: none;
-webkit-user-select: none;
}
.tree-item__checkbox {

View File

@@ -20,3 +20,7 @@ export function* activeElements(activeElement: Element | null = document.activeE
yield* activeElements(activeElement.shadowRoot.activeElement);
}
}
export function getDeepestActiveElement() {
return [...activeElements()].pop();
}

View File

@@ -1,4 +1,4 @@
import { activeElements } from './active-elements.js';
import { getDeepestActiveElement } from './active-elements.js';
import { getTabbableElements } from './tabbable.js';
let activeModals: HTMLElement[] = [];
@@ -46,7 +46,7 @@ export default class Modal {
this.isExternalActivated = false;
}
checkFocus() {
private checkFocus() {
if (this.isActive() && !this.isExternalActivated) {
const tabbableElements = getTabbableElements(this.element);
if (!this.element.matches(':focus-within')) {
@@ -66,22 +66,6 @@ export default class Modal {
this.checkFocus();
};
get currentFocusIndex() {
return getTabbableElements(this.element).findIndex(el => el === this.currentFocus);
}
// Checks if the `startElement` is already focused. This is important if the modal already has an existing focus prior
// to the first tab key.
private startElementAlreadyFocused(startElement: HTMLElement) {
for (const activeElement of activeElements()) {
if (startElement === activeElement) {
return true;
}
}
return false;
}
private handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'Tab' || this.isExternalActivated) return;
@@ -94,29 +78,30 @@ export default class Modal {
event.preventDefault();
const tabbableElements = getTabbableElements(this.element);
const start = tabbableElements[0];
// Sometimes we programmatically focus the first element in a modal.
// Lets make sure the start element isn't already focused.
let focusIndex = this.startElementAlreadyFocused(start) ? 0 : this.currentFocusIndex;
// Because sometimes focus can actually be taken over from outside sources,
// we don't want to rely on `this.currentFocus`. Instead we check the actual `activeElement` and
// recurse through shadowRoots.
const currentActiveElement = getDeepestActiveElement();
let currentFocusIndex = tabbableElements.findIndex(el => el === currentActiveElement);
if (focusIndex === -1) {
this.currentFocus = start;
this.currentFocus.focus({ preventScroll: true });
if (currentFocusIndex === -1) {
this.currentFocus = tabbableElements[0];
this.currentFocus?.focus({ preventScroll: true });
return;
}
const addition = this.tabDirection === 'forward' ? 1 : -1;
if (focusIndex + addition >= tabbableElements.length) {
focusIndex = 0;
} else if (this.currentFocusIndex + addition < 0) {
focusIndex = tabbableElements.length - 1;
if (currentFocusIndex + addition >= tabbableElements.length) {
currentFocusIndex = 0;
} else if (currentFocusIndex + addition < 0) {
currentFocusIndex = tabbableElements.length - 1;
} else {
focusIndex += addition;
currentFocusIndex += addition;
}
this.currentFocus = tabbableElements[focusIndex];
this.currentFocus = tabbableElements[currentFocusIndex];
this.currentFocus?.focus({ preventScroll: true });
setTimeout(() => this.checkFocus());

View File

@@ -1,7 +1,7 @@
import { elementUpdated, expect, fixture } from '@open-wc/testing';
import '../../dist/webawesome.js';
import { activeElements } from './active-elements.js';
import { activeElements, getDeepestActiveElement } from './active-elements.js';
import { html } from 'lit';
import { sendKeys } from '@web/test-runner-commands';
@@ -19,10 +19,6 @@ function activeElementsArray() {
return [...activeElements()];
}
function getDeepestActiveElement() {
return activeElementsArray().pop();
}
window.customElements.define(
'tab-test-1',
class extends HTMLElement {
@@ -145,3 +141,36 @@ it('Should allow tabbing to slotted elements', async () => {
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
expect(activeElementsArray()).to.include(focusSix);
});
it('Should account for when focus is changed from outside sources (like clicking)', async () => {
const dialog = await fixture(html`
<wa-dialog open="" label="Dialog" class="dialog-overview">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-input placeholder="tab to me"></wa-input>
<wa-button slot="footer" variant="primary">Close</wa-button>
</wa-dialog>
`);
const inputEl = dialog.querySelector('wa-input')!;
const closeButton = dialog.shadowRoot!.querySelector('wa-icon-button')!;
const footerButton = dialog.querySelector('wa-button')!;
expect(activeElementsArray()).to.not.include(inputEl);
// Sets focus to the input element
inputEl.focus();
expect(activeElementsArray()).to.include(inputEl);
await sendKeys({ press: tabKey });
expect(activeElementsArray()).not.to.include(inputEl);
expect(activeElementsArray()).to.include(footerButton);
// Reset focus back to input el
inputEl.focus();
expect(activeElementsArray()).to.include(inputEl);
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
expect(activeElementsArray()).to.include(closeButton);
});

View File

@@ -1,4 +1,15 @@
import { offsetParent } from 'composed-offset-position';
//
// This doesn't technically check visibility, it checks if the element has been rendered and can maybe possibly be tabbed
// to. This is a workaround for shadow roots not having an `offsetParent`.
//
// See https://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom
//
// Previously, we used https://www.npmjs.com/package/composed-offset-position, but recursing up an entire node tree took
// up a lot of CPU cycles and made focus traps unusable in Chrome / Edge.
//
function isTakingUpSpace(elem: HTMLElement): boolean {
return Boolean(elem.offsetParent || elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
}
/** Determines if the specified element is tabbable using heuristics inspired by https://github.com/focus-trap/tabbable */
function isTabbable(el: HTMLElement) {
@@ -14,19 +25,13 @@ function isTabbable(el: HTMLElement) {
return false;
}
// Elements with aria-disabled are not tabbable
if (el.hasAttribute('aria-disabled') && el.getAttribute('aria-disabled') !== 'false') {
return false;
}
// Radios without a checked attribute are not tabbable
if (tag === 'input' && el.getAttribute('type') === 'radio' && !el.hasAttribute('checked')) {
return false;
}
// Elements that are hidden have no offsetParent and are not tabbable
// offsetParent() is added because otherwise it misses elements in Safari
if (el.offsetParent === null && offsetParent(el) === null) {
if (!isTakingUpSpace(el)) {
return false;
}
@@ -107,14 +112,12 @@ export function getTabbableElements(root: HTMLElement | ShadowRoot) {
// Collect all elements including the root
walk(root);
return tabbableElements;
// Is this worth having? Most sorts will always add increased overhead. And positive tabindexes shouldn't really be used.
// So is it worth being right? Or fast?
// return tabbableElements.filter(isTabbable).sort((a, b) => {
// // Make sure we sort by tabindex.
// const aTabindex = Number(a.getAttribute('tabindex')) || 0;
// const bTabindex = Number(b.getAttribute('tabindex')) || 0;
// return bTabindex - aTabindex;
// });
return tabbableElements.sort((a, b) => {
// Make sure we sort by tabindex.
const aTabindex = Number(a.getAttribute('tabindex')) || 0;
const bTabindex = Number(b.getAttribute('tabindex')) || 0;
return bTabindex - aTabindex;
});
}

View File

@@ -191,7 +191,9 @@ abbr[title] {
kbd {
background: var(--wa-color-neutral-fill-muted);
border: solid 1px var(--wa-color-neutral-outline-muted);
box-shadow: inset 0 1px 0 1px var(--wa-color-tint-white), 0 1px 0 0 var(--wa-color-tint-black);
box-shadow:
inset 0 1px 0 1px var(--wa-color-tint-white),
0 1px 0 0 var(--wa-color-tint-black);
font-family: var(--wa-font-family-code);
border-radius: var(--wa-corners-1x);
color: var(--wa-color-neutral-text-on-muted);
@@ -283,6 +285,7 @@ summary {
padding-block: var(--wa-space-m);
margin: 0;
user-select: none;
-webkit-user-select: none;
}
summary::-webkit-details-marker {