kopia lustrzana https://github.com/shoelace-style/shoelace
feat: add ESLint, improve types, improve a11y
rodzic
2ad00deb38
commit
9fb3b5cfed
|
@ -0,0 +1,9 @@
|
||||||
|
.cache
|
||||||
|
docs/dist
|
||||||
|
docs/search.json
|
||||||
|
docs/**/*.min.js
|
||||||
|
dist
|
||||||
|
examples
|
||||||
|
node_modules
|
||||||
|
src/react
|
||||||
|
scripts
|
|
@ -0,0 +1,252 @@
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: ['@typescript-eslint', 'wc', 'lit', 'lit-a11y', 'chai-expect', 'chai-friendly', 'import'],
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:wc/recommended',
|
||||||
|
'plugin:wc/best-practice',
|
||||||
|
'plugin:lit/recommended',
|
||||||
|
'plugin:lit-a11y/recommended'
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
es2021: true,
|
||||||
|
browser: true
|
||||||
|
},
|
||||||
|
reportUnusedDisableDirectives: true,
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module'
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/eslint-recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended-requiring-type-checking'
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
project: './tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname
|
||||||
|
},
|
||||||
|
files: ['*.ts'],
|
||||||
|
rules: {
|
||||||
|
'default-param-last': 'off',
|
||||||
|
'@typescript-eslint/default-param-last': 'error',
|
||||||
|
'no-empty-function': 'off',
|
||||||
|
'@typescript-eslint/no-empty-function': 'warn',
|
||||||
|
'no-implied-eval': 'off',
|
||||||
|
'@typescript-eslint/no-implied-eval': 'error',
|
||||||
|
'no-invalid-this': 'off',
|
||||||
|
'@typescript-eslint/no-invalid-this': 'error',
|
||||||
|
'no-shadow': 'off',
|
||||||
|
'@typescript-eslint/no-shadow': 'error',
|
||||||
|
'no-throw-literal': 'off',
|
||||||
|
'@typescript-eslint/no-throw-literal': 'error',
|
||||||
|
'no-unused-expressions': 'off',
|
||||||
|
'@typescript-eslint/prefer-regexp-exec': 'off',
|
||||||
|
'@typescript-eslint/no-unused-expressions': 'error',
|
||||||
|
'@typescript-eslint/unbound-method': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/consistent-type-assertions': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
assertionStyle: 'as',
|
||||||
|
objectLiteralTypeAssertions: 'never'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/consistent-type-imports': 'warn',
|
||||||
|
// "@typescript-eslint/explicit-function-return-type": [
|
||||||
|
// "error",
|
||||||
|
// {
|
||||||
|
// allowTypedFunctionExpressions: true,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// "@typescript-eslint/explicit-member-accessibility": "warn",
|
||||||
|
// "@typescript-eslint/explicit-module-boundary-types": "error",
|
||||||
|
'@typescript-eslint/no-base-to-string': 'error',
|
||||||
|
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
|
||||||
|
'@typescript-eslint/no-confusing-void-expression': 'error',
|
||||||
|
'@typescript-eslint/no-invalid-void-type': 'error',
|
||||||
|
'@typescript-eslint/no-require-imports': 'error',
|
||||||
|
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'warn',
|
||||||
|
'@typescript-eslint/no-unnecessary-condition': 'warn',
|
||||||
|
'@typescript-eslint/no-unnecessary-qualifier': 'warn',
|
||||||
|
'@typescript-eslint/non-nullable-type-assertion-style': 'warn',
|
||||||
|
'@typescript-eslint/prefer-for-of': 'warn',
|
||||||
|
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||||
|
'@typescript-eslint/prefer-readonly': 'warn',
|
||||||
|
'@typescript-eslint/prefer-ts-expect-error': 'warn',
|
||||||
|
'@typescript-eslint/prefer-return-this-type': 'error',
|
||||||
|
'@typescript-eslint/prefer-string-starts-ends-with': 'warn',
|
||||||
|
'@typescript-eslint/require-array-sort-compare': 'error',
|
||||||
|
'@typescript-eslint/unified-signatures': 'warn',
|
||||||
|
'@typescript-eslint/array-type': 'warn',
|
||||||
|
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
|
||||||
|
'@typescript-eslint/member-delimiter-style': 'warn',
|
||||||
|
'@typescript-eslint/method-signature-style': 'warn',
|
||||||
|
'@typescript-eslint/naming-convention': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
selector: 'default',
|
||||||
|
format: ['camelCase']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: ['function', 'enumMember', 'property'],
|
||||||
|
format: ['camelCase', 'PascalCase']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'variable',
|
||||||
|
modifiers: ['const'],
|
||||||
|
format: ['camelCase', 'PascalCase', 'UPPER_CASE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'typeLike',
|
||||||
|
format: ['PascalCase']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'typeProperty',
|
||||||
|
format: ['camelCase', 'PascalCase', 'UPPER_CASE']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-extraneous-class': 'error',
|
||||||
|
'@typescript-eslint/no-parameter-properties': 'error',
|
||||||
|
'@typescript-eslint/strict-boolean-expressions': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
allowString: false,
|
||||||
|
allowNumber: false,
|
||||||
|
allowNullableObject: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extends: ['plugin:chai-expect/recommended', 'plugin:chai-friendly/recommended'],
|
||||||
|
files: ['*.test.ts'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-unused-expressions': 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'no-template-curly-in-string': 'error',
|
||||||
|
'array-callback-return': 'error',
|
||||||
|
'consistent-return': 'error',
|
||||||
|
curly: 'warn',
|
||||||
|
'default-param-last': 'error',
|
||||||
|
eqeqeq: 'error',
|
||||||
|
'no-constructor-return': 'error',
|
||||||
|
'no-empty-function': 'warn',
|
||||||
|
'no-eval': 'error',
|
||||||
|
'no-extend-native': 'error',
|
||||||
|
'no-extra-bind': 'error',
|
||||||
|
'no-floating-decimal': 'error',
|
||||||
|
'no-implicit-coercion': 'error',
|
||||||
|
'no-implicit-globals': 'error',
|
||||||
|
'no-implied-eval': 'error',
|
||||||
|
'no-invalid-this': 'error',
|
||||||
|
'no-labels': 'error',
|
||||||
|
'no-lone-blocks': 'error',
|
||||||
|
'no-new': 'error',
|
||||||
|
'no-new-func': 'error',
|
||||||
|
'no-new-wrappers': 'error',
|
||||||
|
'no-octal-escape': 'error',
|
||||||
|
'no-proto': 'error',
|
||||||
|
'no-return-assign': 'warn',
|
||||||
|
'no-script-url': 'error',
|
||||||
|
'no-self-compare': 'warn',
|
||||||
|
'no-sequences': 'warn',
|
||||||
|
'no-throw-literal': 'error',
|
||||||
|
'no-unmodified-loop-condition': 'error',
|
||||||
|
'no-unused-expressions': 'warn',
|
||||||
|
'no-useless-call': 'error',
|
||||||
|
'no-useless-concat': 'error',
|
||||||
|
'no-useless-return': 'warn',
|
||||||
|
'prefer-promise-reject-errors': 'error',
|
||||||
|
radix: 'error',
|
||||||
|
'require-await': 'error',
|
||||||
|
'wrap-iife': ['warn', 'inside'],
|
||||||
|
'no-shadow': 'error',
|
||||||
|
'no-array-constructor': 'error',
|
||||||
|
'no-bitwise': 'error',
|
||||||
|
'no-multi-assign': 'warn',
|
||||||
|
'no-new-object': 'error',
|
||||||
|
'no-useless-computed-key': 'warn',
|
||||||
|
'no-useless-rename': 'warn',
|
||||||
|
'no-var': 'error',
|
||||||
|
'prefer-const': 'warn',
|
||||||
|
'prefer-numeric-literals': 'warn',
|
||||||
|
'prefer-object-spread': 'warn',
|
||||||
|
'prefer-rest-params': 'warn',
|
||||||
|
'prefer-spread': 'warn',
|
||||||
|
'prefer-template': 'warn',
|
||||||
|
'no-else-return': 'warn',
|
||||||
|
'func-names': ['warn', 'never'],
|
||||||
|
'func-style': ['warn', 'declaration'],
|
||||||
|
'one-var': ['warn', 'never'],
|
||||||
|
'operator-assignment': 'warn',
|
||||||
|
'prefer-arrow-callback': 'warn',
|
||||||
|
'no-restricted-syntax': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
selector: "CallExpression[callee.name='String']",
|
||||||
|
message: "Don't use the String function. Use .toString() instead."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: "CallExpression[callee.name='Number']",
|
||||||
|
message: "Don't use the Number function. Use parseInt or parseFloat instead."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: "CallExpression[callee.name='Boolean']",
|
||||||
|
message: "Don't use the Boolean function. Use a strict comparison instead."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
group: ['../*'],
|
||||||
|
message: 'Usage of relative parent imports is not allowed.'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
name: '.',
|
||||||
|
message: 'Usage of local index imports is not allowed.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: './index',
|
||||||
|
message: 'Import from the source file instead.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'import/no-duplicates': 'warn',
|
||||||
|
'import/order': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
groups: ['builtin', 'external', ['parent', 'sibling', 'internal', 'index']],
|
||||||
|
pathGroups: [
|
||||||
|
{
|
||||||
|
pattern: '~/**',
|
||||||
|
group: 'internal'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'dist/**',
|
||||||
|
group: 'external'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
alphabetize: {
|
||||||
|
order: 'asc',
|
||||||
|
caseInsensitive: true
|
||||||
|
},
|
||||||
|
'newlines-between': 'never',
|
||||||
|
warnOnUnassignedImports: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'wc/guard-super-call': 'off'
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"bierner.lit-html",
|
"bierner.lit-html",
|
||||||
"bashmish.es6-string-css",
|
"bashmish.es6-string-css",
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"csspart",
|
"csspart",
|
||||||
"cssproperty",
|
"cssproperty",
|
||||||
"datetime",
|
"datetime",
|
||||||
|
"describedby",
|
||||||
"Docsify",
|
"Docsify",
|
||||||
"dropdowns",
|
"dropdowns",
|
||||||
"easings",
|
"easings",
|
||||||
|
@ -79,6 +80,7 @@
|
||||||
"rgba",
|
"rgba",
|
||||||
"roadmap",
|
"roadmap",
|
||||||
"Roboto",
|
"Roboto",
|
||||||
|
"saturationl",
|
||||||
"Schilp",
|
"Schilp",
|
||||||
"Segoe",
|
"Segoe",
|
||||||
"semibold",
|
"semibold",
|
||||||
|
|
|
@ -4,7 +4,10 @@ import { pascalCase } from 'pascal-case';
|
||||||
|
|
||||||
const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
||||||
const { name, description, version, author, homepage, license } = packageData;
|
const { name, description, version, author, homepage, license } = packageData;
|
||||||
const noDash = string => string.replace(/^\s?-/, '').trim();
|
|
||||||
|
function noDash(string) {
|
||||||
|
return string.replace(/^\s?-/, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
globs: ['src/components/**/*.ts'],
|
globs: ['src/components/**/*.ts'],
|
||||||
|
@ -13,7 +16,7 @@ export default {
|
||||||
// Append package data
|
// Append package data
|
||||||
{
|
{
|
||||||
name: 'shoelace-package-data',
|
name: 'shoelace-package-data',
|
||||||
packageLinkPhase({ customElementsManifest, context }) {
|
packageLinkPhase({ customElementsManifest }) {
|
||||||
customElementsManifest.package = { name, description, version, author, homepage, license };
|
customElementsManifest.package = { name, description, version, author, homepage, license };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -21,9 +24,9 @@ export default {
|
||||||
// Parse custom jsDoc tags
|
// Parse custom jsDoc tags
|
||||||
{
|
{
|
||||||
name: 'shoelace-custom-tags',
|
name: 'shoelace-custom-tags',
|
||||||
analyzePhase({ ts, node, moduleDoc, context }) {
|
analyzePhase({ ts, node, moduleDoc }) {
|
||||||
switch (node.kind) {
|
switch (node.kind) {
|
||||||
case ts.SyntaxKind.ClassDeclaration:
|
case ts.SyntaxKind.ClassDeclaration: {
|
||||||
const className = node.name.getText();
|
const className = node.name.getText();
|
||||||
const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className);
|
const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className);
|
||||||
const customTags = ['animation', 'dependency', 'since', 'status'];
|
const customTags = ['animation', 'dependency', 'since', 'status'];
|
||||||
|
@ -39,8 +42,8 @@ export default {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = parse(customComments + '\n */');
|
const parsed = parse(`${customComments}\n */`);
|
||||||
parsed[0].tags?.map(t => {
|
parsed[0].tags?.forEach(t => {
|
||||||
switch (t.tag) {
|
switch (t.tag) {
|
||||||
// Animations
|
// Animations
|
||||||
case 'animation':
|
case 'animation':
|
||||||
|
@ -80,23 +83,25 @@ export default {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'shoelace-react-event-names',
|
name: 'shoelace-react-event-names',
|
||||||
analyzePhase({ ts, node, moduleDoc, context }) {
|
analyzePhase({ ts, node, moduleDoc }) {
|
||||||
switch (node.kind) {
|
switch (node.kind) {
|
||||||
case ts.SyntaxKind.ClassDeclaration:
|
case ts.SyntaxKind.ClassDeclaration: {
|
||||||
const className = node.name.getText();
|
const className = node.name.getText();
|
||||||
const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className);
|
const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className);
|
||||||
|
|
||||||
if (classDoc?.events) {
|
if (classDoc?.events) {
|
||||||
classDoc.events.map(event => {
|
classDoc.events.forEach(event => {
|
||||||
event.reactName = `on${pascalCase(event.name)}`;
|
event.reactName = `on${pascalCase(event.name)}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* global Prism */
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
const reactVersion = '17.0.2';
|
const reactVersion = '17.0.2';
|
||||||
let flavor = getFlavor();
|
let flavor = getFlavor();
|
||||||
|
@ -61,14 +63,9 @@
|
||||||
document.body.classList.toggle('flavor-react', flavor === 'react');
|
document.body.classList.toggle('flavor-react', flavor === 'react');
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrap(el, wrapper) {
|
window.$docsify.plugins.push(hook => {
|
||||||
el.parentNode.insertBefore(wrapper, el);
|
|
||||||
wrapper.appendChild(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.$docsify.plugins.push((hook, vm) => {
|
|
||||||
// Convert code blocks to previews
|
// Convert code blocks to previews
|
||||||
hook.afterEach(function (html, next) {
|
hook.afterEach((html, next) => {
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
const doc = domParser.parseFromString(html, 'text/html');
|
const doc = domParser.parseFromString(html, 'text/html');
|
||||||
|
|
||||||
|
@ -111,7 +108,7 @@
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
[...doc.querySelectorAll('code[class^="lang-"]')].map(code => {
|
[...doc.querySelectorAll('code[class^="lang-"]')].forEach(code => {
|
||||||
if (code.classList.contains('preview')) {
|
if (code.classList.contains('preview')) {
|
||||||
const isExpanded = code.classList.contains('expanded');
|
const isExpanded = code.classList.contains('expanded');
|
||||||
const pre = code.closest('pre');
|
const pre = code.closest('pre');
|
||||||
|
@ -119,12 +116,6 @@
|
||||||
const toggleId = `code-block-toggle-${count}`;
|
const toggleId = `code-block-toggle-${count}`;
|
||||||
const reactPre = getAdjacentExample('react', pre);
|
const reactPre = getAdjacentExample('react', pre);
|
||||||
const hasReact = reactPre !== null;
|
const hasReact = reactPre !== null;
|
||||||
const examples = [
|
|
||||||
{
|
|
||||||
name: 'HTML',
|
|
||||||
codeBlock: pre
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
pre.setAttribute('data-lang', pre.getAttribute('data-lang').replace(/ preview$/, ''));
|
pre.setAttribute('data-lang', pre.getAttribute('data-lang').replace(/ preview$/, ''));
|
||||||
pre.setAttribute('aria-labelledby', toggleId);
|
pre.setAttribute('aria-labelledby', toggleId);
|
||||||
|
@ -182,7 +173,7 @@
|
||||||
`;
|
`;
|
||||||
|
|
||||||
pre.replaceWith(domParser.parseFromString(codeBlock, 'text/html').body);
|
pre.replaceWith(domParser.parseFromString(codeBlock, 'text/html').body);
|
||||||
if (reactPre) reactPre.remove();
|
reactPre?.remove();
|
||||||
|
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
@ -201,12 +192,12 @@
|
||||||
|
|
||||||
// Horizontal resizing
|
// Horizontal resizing
|
||||||
hook.doneEach(() => {
|
hook.doneEach(() => {
|
||||||
[...document.querySelectorAll('.code-block__preview')].map(preview => {
|
[...document.querySelectorAll('.code-block__preview')].forEach(preview => {
|
||||||
const resizer = preview.querySelector('.code-block__resizer');
|
const resizer = preview.querySelector('.code-block__resizer');
|
||||||
let startX;
|
let startX;
|
||||||
let startWidth;
|
let startWidth;
|
||||||
|
|
||||||
const dragStart = event => {
|
function dragStart(event) {
|
||||||
startX = event.changedTouches ? event.changedTouches[0].pageX : event.clientX;
|
startX = event.changedTouches ? event.changedTouches[0].pageX : event.clientX;
|
||||||
startWidth = parseInt(document.defaultView.getComputedStyle(preview).width, 10);
|
startWidth = parseInt(document.defaultView.getComputedStyle(preview).width, 10);
|
||||||
preview.classList.add('code-block__preview--dragging');
|
preview.classList.add('code-block__preview--dragging');
|
||||||
|
@ -215,21 +206,23 @@
|
||||||
document.documentElement.addEventListener('touchmove', dragMove, false);
|
document.documentElement.addEventListener('touchmove', dragMove, false);
|
||||||
document.documentElement.addEventListener('mouseup', dragStop, false);
|
document.documentElement.addEventListener('mouseup', dragStop, false);
|
||||||
document.documentElement.addEventListener('touchend', dragStop, false);
|
document.documentElement.addEventListener('touchend', dragStop, false);
|
||||||
};
|
}
|
||||||
|
|
||||||
const dragMove = event => {
|
function dragMove(event) {
|
||||||
setWidth(startWidth + (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - startX);
|
setWidth(startWidth + (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - startX);
|
||||||
};
|
}
|
||||||
|
|
||||||
const dragStop = event => {
|
function dragStop() {
|
||||||
preview.classList.remove('code-block__preview--dragging');
|
preview.classList.remove('code-block__preview--dragging');
|
||||||
document.documentElement.removeEventListener('mousemove', dragMove, false);
|
document.documentElement.removeEventListener('mousemove', dragMove, false);
|
||||||
document.documentElement.removeEventListener('touchmove', dragMove, false);
|
document.documentElement.removeEventListener('touchmove', dragMove, false);
|
||||||
document.documentElement.removeEventListener('mouseup', dragStop, false);
|
document.documentElement.removeEventListener('mouseup', dragStop, false);
|
||||||
document.documentElement.removeEventListener('touchend', dragStop, false);
|
document.documentElement.removeEventListener('touchend', dragStop, false);
|
||||||
};
|
}
|
||||||
|
|
||||||
const setWidth = width => (preview.style.width = width + 'px');
|
function setWidth(width) {
|
||||||
|
preview.style.width = `${width}px`;
|
||||||
|
}
|
||||||
|
|
||||||
resizer.addEventListener('mousedown', dragStart);
|
resizer.addEventListener('mousedown', dragStart);
|
||||||
resizer.addEventListener('touchstart', dragStart);
|
resizer.addEventListener('touchstart', dragStart);
|
||||||
|
@ -240,7 +233,6 @@
|
||||||
// Toggle source mode
|
// Toggle source mode
|
||||||
document.addEventListener('click', event => {
|
document.addEventListener('click', event => {
|
||||||
const button = event.target.closest('button');
|
const button = event.target.closest('button');
|
||||||
const codeBlock = button?.closest('.code-block');
|
|
||||||
|
|
||||||
if (button?.classList.contains('code-block__button--html')) {
|
if (button?.classList.contains('code-block__button--html')) {
|
||||||
setFlavor('html');
|
setFlavor('html');
|
||||||
|
@ -251,7 +243,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update flavor buttons
|
// Update flavor buttons
|
||||||
[...document.querySelectorAll('.code-block')].map(codeBlock => {
|
[...document.querySelectorAll('.code-block')].forEach(codeBlock => {
|
||||||
codeBlock
|
codeBlock
|
||||||
.querySelector('.code-block__button--html')
|
.querySelector('.code-block__button--html')
|
||||||
?.classList.toggle('code-block__button--selected', flavor === 'html');
|
?.classList.toggle('code-block__button--selected', flavor === 'html');
|
||||||
|
@ -310,8 +302,7 @@
|
||||||
if (!isReact) {
|
if (!isReact) {
|
||||||
htmlTemplate =
|
htmlTemplate =
|
||||||
`<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/dist/shoelace.js"></script>\n` +
|
`<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/dist/shoelace.js"></script>\n` +
|
||||||
'\n' +
|
`\n${htmlExample}`;
|
||||||
htmlExample;
|
|
||||||
jsTemplate = '';
|
jsTemplate = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,13 +313,11 @@
|
||||||
`import React from 'https://cdn.skypack.dev/react@${reactVersion}';\n` +
|
`import React from 'https://cdn.skypack.dev/react@${reactVersion}';\n` +
|
||||||
`import ReactDOM from 'https://cdn.skypack.dev/react-dom@${reactVersion}';\n` +
|
`import ReactDOM from 'https://cdn.skypack.dev/react-dom@${reactVersion}';\n` +
|
||||||
`import { setBasePath } from 'https://cdn.skypack.dev/@shoelace-style/shoelace@${version}/dist/utilities/base-path';\n` +
|
`import { setBasePath } from 'https://cdn.skypack.dev/@shoelace-style/shoelace@${version}/dist/utilities/base-path';\n` +
|
||||||
'\n' +
|
`\n` +
|
||||||
`// Set the base path for Shoelace assets\n` +
|
`// Set the base path for Shoelace assets\n` +
|
||||||
`setBasePath('https://cdn.skypack.dev/@shoelace-style/shoelace@${version}/dist/')\n` +
|
`setBasePath('https://cdn.skypack.dev/@shoelace-style/shoelace@${version}/dist/')\n` +
|
||||||
'\n' +
|
`\n${convertModuleLinks(reactExample)}\n` +
|
||||||
convertModuleLinks(reactExample) +
|
`\n` +
|
||||||
'\n' +
|
|
||||||
'\n' +
|
|
||||||
`ReactDOM.render(<App />, document.getElementById('root'));`;
|
`ReactDOM.render(<App />, document.getElementById('root'));`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
${props
|
${props
|
||||||
.map(prop => {
|
.map(prop => {
|
||||||
const hasAttribute = !!prop.attribute;
|
const hasAttribute = typeof prop.attribute !== 'undefined';
|
||||||
const isAttributeDifferent = prop.attribute !== prop.name;
|
const isAttributeDifferent = prop.attribute !== prop.name;
|
||||||
let attributeInfo = '';
|
let attributeInfo = '';
|
||||||
|
|
||||||
|
@ -244,19 +244,19 @@
|
||||||
function getDependencies(tag) {
|
function getDependencies(tag) {
|
||||||
const component = allComponents.find(c => c.tagName === tag);
|
const component = allComponents.find(c => c.tagName === tag);
|
||||||
if (!component || !Array.isArray(component.dependencies)) {
|
if (!component || !Array.isArray(component.dependencies)) {
|
||||||
return [];
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
component.dependencies?.map(tag => {
|
component.dependencies?.forEach(dependentTag => {
|
||||||
if (!dependencies.includes(tag)) {
|
if (!dependencies.includes(dependentTag)) {
|
||||||
dependencies.push(tag);
|
dependencies.push(dependentTag);
|
||||||
}
|
}
|
||||||
getDependencies(tag);
|
getDependencies(dependentTag);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getDependencies(targetComponent);
|
getDependencies(targetComponent);
|
||||||
dependencies.sort().map(tag => {
|
dependencies.sort().forEach(tag => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.innerHTML = `<code><${tag}></code>`;
|
li.innerHTML = `<code><${tag}></code>`;
|
||||||
ul.appendChild(li);
|
ul.appendChild(li);
|
||||||
|
@ -266,7 +266,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(html) {
|
function escapeHtml(html) {
|
||||||
return (html + '')
|
if (typeof html === 'undefined') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return html
|
||||||
|
.toString()
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
|
@ -277,8 +281,8 @@
|
||||||
|
|
||||||
function getAllComponents(metadata) {
|
function getAllComponents(metadata) {
|
||||||
const allComponents = [];
|
const allComponents = [];
|
||||||
metadata.modules?.map(module => {
|
metadata.modules?.forEach(module => {
|
||||||
module.declarations?.map(declaration => {
|
module.declarations?.forEach(declaration => {
|
||||||
if (declaration.customElement) {
|
if (declaration.customElement) {
|
||||||
// Generate the dist path based on the src path and attach it to the component
|
// 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');
|
declaration.path = module.path.replace(/^src\//, 'dist/').replace(/\.ts$/, '.js');
|
||||||
|
@ -299,8 +303,8 @@
|
||||||
throw new Error('Docsify must be loaded before installing this plugin.');
|
throw new Error('Docsify must be loaded before installing this plugin.');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.$docsify.plugins.push((hook, vm) => {
|
window.$docsify.plugins.push(hook => {
|
||||||
hook.mounted(async function () {
|
hook.mounted(async () => {
|
||||||
const metadata = await customElements;
|
const metadata = await customElements;
|
||||||
const target = document.querySelector('.app-name');
|
const target = document.querySelector('.app-name');
|
||||||
|
|
||||||
|
@ -330,7 +334,7 @@
|
||||||
target.appendChild(buttons);
|
target.appendChild(buttons);
|
||||||
});
|
});
|
||||||
|
|
||||||
hook.beforeEach(async function (content, next) {
|
hook.beforeEach(async (content, next) => {
|
||||||
const metadata = await customElements;
|
const metadata = await customElements;
|
||||||
|
|
||||||
// Replace %VERSION% placeholders
|
// Replace %VERSION% placeholders
|
||||||
|
@ -342,15 +346,23 @@
|
||||||
let result = '';
|
let result = '';
|
||||||
|
|
||||||
if (!component) {
|
if (!component) {
|
||||||
console.error('Component not found in metadata: ' + tag);
|
console.error(`Component not found in metadata: ${tag}`);
|
||||||
return next(content);
|
return next(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
let badgeType = 'neutral';
|
let badgeType = 'neutral';
|
||||||
if (component.status === 'stable') badgeType = 'primary';
|
if (component.status === 'stable') {
|
||||||
if (component.status === 'experimental') badgeType = 'warning';
|
badgeType = 'primary';
|
||||||
if (component.status === 'planned') badgeType = 'neutral';
|
}
|
||||||
if (component.status === 'deprecated') badgeType = 'danger';
|
if (component.status === 'experimental') {
|
||||||
|
badgeType = 'warning';
|
||||||
|
}
|
||||||
|
if (component.status === 'planned') {
|
||||||
|
badgeType = 'neutral';
|
||||||
|
}
|
||||||
|
if (component.status === 'deprecated') {
|
||||||
|
badgeType = 'danger';
|
||||||
|
}
|
||||||
|
|
||||||
result += `
|
result += `
|
||||||
<div class="component-header">
|
<div class="component-header">
|
||||||
|
@ -379,7 +391,7 @@
|
||||||
let result = '';
|
let result = '';
|
||||||
|
|
||||||
if (!component) {
|
if (!component) {
|
||||||
console.error('Component not found in metadata: ' + tag);
|
console.error(`Component not found in metadata: ${tag}`);
|
||||||
return next(content);
|
return next(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -521,11 +533,11 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wrap tables so we can scroll them horizontally when needed
|
// Wrap tables so we can scroll them horizontally when needed
|
||||||
hook.doneEach(function () {
|
hook.doneEach(() => {
|
||||||
const content = document.querySelector('.content');
|
const content = document.querySelector('.content');
|
||||||
const tables = [...content.querySelectorAll('table')];
|
const tables = [...content.querySelectorAll('table')];
|
||||||
|
|
||||||
tables.map(table => {
|
tables.forEach(table => {
|
||||||
table.outerHTML = `
|
table.outerHTML = `
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
${table.outerHTML}
|
${table.outerHTML}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
// Docsify generates pages dynamically and asynchronously, so when a reload happens, the scroll position can't be
|
// Docsify generates pages dynamically and asynchronously, so when a reload happens, the scroll position can't be
|
||||||
// be restored immediately. This plugin waits until Docsify loads the page and then restores it.
|
// be restored immediately. This plugin waits until Docsify loads the page and then restores it.
|
||||||
//
|
//
|
||||||
window.$docsify.plugins.push((hook, vm) => {
|
window.$docsify.plugins.push(hook => {
|
||||||
hook.ready(() => {
|
hook.ready(() => {
|
||||||
// Restore
|
// Restore
|
||||||
const scrollTop = sessionStorage.getItem('bs-scroll');
|
const scrollTop = sessionStorage.getItem('bs-scroll');
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remember
|
// Remember
|
||||||
document.addEventListener('scroll', event => {
|
document.addEventListener('scroll', () => {
|
||||||
sessionStorage.setItem('bs-scroll', document.documentElement.scrollTop);
|
sessionStorage.setItem('bs-scroll', document.documentElement.scrollTop);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
|
/* global lunr */
|
||||||
(() => {
|
(() => {
|
||||||
if (!window.$docsify) {
|
if (!window.$docsify) {
|
||||||
throw new Error('Docsify must be loaded before installing this plugin.');
|
throw new Error('Docsify must be loaded before installing this plugin.');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.$docsify.plugins.push((hook, vm) => {
|
window.$docsify.plugins.push(hook => {
|
||||||
// Append the search box to the sidebar
|
// Append the search box to the sidebar
|
||||||
hook.mounted(function () {
|
hook.mounted(() => {
|
||||||
const appName = document.querySelector('.sidebar .app-name');
|
const appName = document.querySelector('.sidebar .app-name');
|
||||||
const searchBox = document.createElement('div');
|
const searchBox = document.createElement('div');
|
||||||
searchBox.classList.add('search-box');
|
searchBox.classList.add('search-box');
|
||||||
|
@ -76,7 +77,6 @@
|
||||||
`;
|
`;
|
||||||
document.body.append(siteSearch);
|
document.body.append(siteSearch);
|
||||||
|
|
||||||
const searchButtons = [...document.querySelectorAll('[data-site-search]')];
|
|
||||||
const overlay = siteSearch.querySelector('.site-search__overlay');
|
const overlay = siteSearch.querySelector('.site-search__overlay');
|
||||||
const panel = siteSearch.querySelector('.site-search__panel');
|
const panel = siteSearch.querySelector('.site-search__panel');
|
||||||
const input = siteSearch.querySelector('.site-search__input');
|
const input = siteSearch.querySelector('.site-search__input');
|
||||||
|
@ -89,7 +89,7 @@
|
||||||
let map;
|
let map;
|
||||||
|
|
||||||
// Load search data
|
// Load search data
|
||||||
const searchData = fetch('../../../search.json')
|
fetch('../../../search.json')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
searchIndex = lunr.Index.load(data.searchIndex);
|
searchIndex = lunr.Index.load(data.searchIndex);
|
||||||
|
@ -203,7 +203,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the selected item
|
// Update the selected item
|
||||||
items.map(item => {
|
items.forEach(item => {
|
||||||
if (item === nextEl) {
|
if (item === nextEl) {
|
||||||
item.setAttribute('aria-selected', 'true');
|
item.setAttribute('aria-selected', 'true');
|
||||||
nextEl.scrollIntoView({ block: 'nearest' });
|
nextEl.scrollIntoView({ block: 'nearest' });
|
||||||
|
@ -211,8 +211,6 @@
|
||||||
item.setAttribute('aria-selected', 'false');
|
item.setAttribute('aria-selected', 'false');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,27 +226,39 @@
|
||||||
matches = searchIndex.search(`${query}~2`);
|
matches = searchIndex.search(`${query}~2`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasResults = hasQuery && matches.length > 0;
|
const hasResults = hasQuery && matches.length > 0;
|
||||||
siteSearch.classList.toggle('site-search--has-results', hasQuery && hasResults);
|
siteSearch.classList.toggle('site-search--has-results', hasQuery && hasResults);
|
||||||
siteSearch.classList.toggle('site-search--no-results', hasQuery && !hasResults);
|
siteSearch.classList.toggle('site-search--no-results', hasQuery && !hasResults);
|
||||||
panel.setAttribute('aria-expanded', hasQuery && hasResults ? 'true' : 'false');
|
panel.setAttribute('aria-expanded', hasQuery && hasResults ? 'true' : 'false');
|
||||||
|
|
||||||
results.innerHTML = '';
|
results.innerHTML = '';
|
||||||
|
|
||||||
matches.map((match, index) => {
|
matches.forEach((match, index) => {
|
||||||
const page = map[match.ref];
|
const page = map[match.ref];
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
let icon = 'file-text';
|
let icon = 'file-text';
|
||||||
|
|
||||||
if (page.url.includes('getting-started/')) icon = 'lightbulb';
|
if (page.url.includes('getting-started/')) {
|
||||||
if (page.url.includes('resources/')) icon = 'book';
|
icon = 'lightbulb';
|
||||||
if (page.url.includes('components/')) icon = 'puzzle';
|
}
|
||||||
if (page.url.includes('tokens/')) icon = 'palette2';
|
if (page.url.includes('resources/')) {
|
||||||
if (page.url.includes('utilities/')) icon = 'wrench';
|
icon = 'book';
|
||||||
if (page.url.includes('tutorials/')) icon = 'joystick';
|
}
|
||||||
|
if (page.url.includes('components/')) {
|
||||||
|
icon = 'puzzle';
|
||||||
|
}
|
||||||
|
if (page.url.includes('tokens/')) {
|
||||||
|
icon = 'palette2';
|
||||||
|
}
|
||||||
|
if (page.url.includes('utilities/')) {
|
||||||
|
icon = 'wrench';
|
||||||
|
}
|
||||||
|
if (page.url.includes('tutorials/')) {
|
||||||
|
icon = 'joystick';
|
||||||
|
}
|
||||||
|
|
||||||
a.href = $docsify.routerMode === 'hash' ? `/#/${page.url}` : `/${page.url}`;
|
a.href = window.$docsify.routerMode === 'hash' ? `/#/${page.url}` : `/${page.url}`;
|
||||||
a.innerHTML = `
|
a.innerHTML = `
|
||||||
<div class="site-search__result-icon">
|
<div class="site-search__result-icon">
|
||||||
<sl-icon name="${icon}" aria-hidden="true"></sl-icon>
|
<sl-icon name="${icon}" aria-hidden="true"></sl-icon>
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
throw new Error('Docsify must be loaded before installing this plugin.');
|
throw new Error('Docsify must be loaded before installing this plugin.');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.$docsify.plugins.push((hook, vm) => {
|
window.$docsify.plugins.push(hook => {
|
||||||
hook.mounted(function () {
|
hook.mounted(() => {
|
||||||
function getTheme() {
|
function getTheme() {
|
||||||
return localStorage.getItem('theme') || 'auto';
|
return localStorage.getItem('theme') || 'auto';
|
||||||
}
|
}
|
||||||
|
@ -12,9 +12,8 @@
|
||||||
function isDark() {
|
function isDark() {
|
||||||
if (theme === 'auto') {
|
if (theme === 'auto') {
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
} else {
|
|
||||||
return theme === 'dark';
|
|
||||||
}
|
}
|
||||||
|
return theme === 'dark';
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTheme(newTheme) {
|
function setTheme(newTheme) {
|
||||||
|
@ -62,7 +61,7 @@
|
||||||
menu.addEventListener('sl-select', event => setTheme(event.detail.item.value));
|
menu.addEventListener('sl-select', event => setTheme(event.detail.item.value));
|
||||||
|
|
||||||
// Update the theme when the preference changes
|
// Update the theme when the preference changes
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addListener(event => setTheme(theme));
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => setTheme(theme));
|
||||||
|
|
||||||
// Toggle themes when pressing backslash
|
// Toggle themes when pressing backslash
|
||||||
document.addEventListener('keydown', event => {
|
document.addEventListener('keydown', event => {
|
||||||
|
|
|
@ -85,7 +85,7 @@ npm install
|
||||||
Once you've cloned the repo, run the following command to spin up the Shoelace dev server.
|
Once you've cloned the repo, run the following command to spin up the Shoelace dev server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start
|
npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
After the initial build, a browser will open automatically to a local version of the docs. The documentation is powered by Docsify, which uses raw markdown files to generate pages on the fly.
|
After the initial build, a browser will open automatically to a local version of the docs. The documentation is powered by Docsify, which uses raw markdown files to generate pages on the fly.
|
||||||
|
@ -113,7 +113,7 @@ For more information about running and building the project locally, refer to `R
|
||||||
Shoelace uses [Web Test Runner](https://modern-web.dev/guides/test-runner/getting-started/) for testing. To launch the test runner during development, open a terminal and launch the dev server.
|
Shoelace uses [Web Test Runner](https://modern-web.dev/guides/test-runner/getting-started/) for testing. To launch the test runner during development, open a terminal and launch the dev server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start
|
npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
In a second terminal window, launch the test runner.
|
In a second terminal window, launch the test runner.
|
||||||
|
@ -124,13 +124,20 @@ npm run test:watch
|
||||||
|
|
||||||
Follow the on-screen instructions to work with the test runner. Tests will automatically re-run as you make changes.
|
Follow the on-screen instructions to work with the test runner. Tests will automatically re-run as you make changes.
|
||||||
|
|
||||||
To run tests only once, make sure to build the project first.
|
To run all tests only once:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
|
||||||
npm run test
|
npm run test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To run just one test file:
|
||||||
|
|
||||||
|
If the test file is `src/components/breadcrumb-item.test.ts`, then `<nameOfTestFile>` would be `breadcrumb-item`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test -- --group <nameOfTestFile>
|
||||||
|
```
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
The following is a non-exhaustive list of conventions, patterns, and best practices we try to follow. As a contributor, we ask that you make a good faith effort to follow them as well. This ensures consistency and maintainability throughout the project.
|
The following is a non-exhaustive list of conventions, patterns, and best practices we try to follow. As a contributor, we ask that you make a good faith effort to follow them as well. This ensures consistency and maintainability throughout the project.
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export default {
|
export default {
|
||||||
'*.{js,ts,jsx,tsx,json,html,xml,css,scss,sass,md}': 'cspell --no-must-find-files',
|
'*.{js,ts,json,html,xml,css,scss,sass,md}': 'cspell --no-must-find-files',
|
||||||
|
'src/**/*.{js,ts}': 'eslint --max-warnings 0 --fix',
|
||||||
'*': 'prettier --write --ignore-unknown'
|
'*': 'prettier --write --ignore-unknown'
|
||||||
};
|
};
|
||||||
|
|
Plik diff jest za duży
Load Diff
28
package.json
28
package.json
|
@ -32,17 +32,19 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node scripts/build.js --bundle --serve",
|
"start": "node scripts/build.js --bundle --serve",
|
||||||
"build": "node scripts/build.js --bundle --types --copydir \"docs/dist\"",
|
"build": "node scripts/build.js --bundle --types --copydir \"docs/dist\"",
|
||||||
"verify": "npm run spellcheck && npm run prettier:check && npm run ts-check && npm run build && npm run test",
|
"verify": "npm run spellcheck && npm run prettier:check && npm run lint && npm run ts-check && npm run test && npm run build",
|
||||||
"prepublishOnly": "npm run verify",
|
"prepublishOnly": "npm run verify",
|
||||||
"prettier": "prettier --write --loglevel warn .",
|
"prettier": "prettier --write --loglevel warn .",
|
||||||
"prettier:check": "prettier --check --loglevel warn .",
|
"prettier:check": "prettier --check --loglevel warn .",
|
||||||
"ts-check": "tsc --noEmit",
|
"lint": "eslint src --max-warnings 0",
|
||||||
|
"lint:fix": "npm run lint -- --fix",
|
||||||
|
"ts-check": "tsc --noEmit --project ./tsconfig.json",
|
||||||
"create": "plop --plopfile scripts/plop/plopfile.js",
|
"create": "plop --plopfile scripts/plop/plopfile.js",
|
||||||
"test": "web-test-runner \"src/**/*.test.ts\" --node-resolve --playwright --browsers chromium firefox webkit",
|
"test": "node scripts/build.js --bundle && web-test-runner",
|
||||||
"test:watch": "web-test-runner \"src/**/*.test.ts\" --node-resolve --playwright --browsers chromium firefox webkit --watch",
|
"test:watch": "web-test-runner --watch",
|
||||||
"spellcheck": "cspell \"**/*.{js,ts,jsx,tsx,json,html,xml,css,scss,sass,md}\" --no-progress",
|
"spellcheck": "cspell \"**/*.{js,ts,json,html,css,md}\" --no-progress",
|
||||||
"list-outdated-dependencies": "npm-check-updates --format repo --peer",
|
"list-outdated-dependencies": "npm-check-updates --format repo --peer",
|
||||||
"update-dependencies": "npm-check-updates --peer -u && npm install && npm run prettier && npm run verify"
|
"update-dependencies": "npm-check-updates --peer -u && npm install && npm run lint:fix && npm run prettier && npm run verify"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.15.0"
|
"node": ">=14.15.0"
|
||||||
|
@ -54,13 +56,17 @@
|
||||||
"@shoelace-style/localize": "^2.1.3",
|
"@shoelace-style/localize": "^2.1.3",
|
||||||
"color": "4.1",
|
"color": "4.1",
|
||||||
"lit": "^2.1.0",
|
"lit": "^2.1.0",
|
||||||
|
"lit-html": "^2.1.1",
|
||||||
"qr-creator": "^1.0.0"
|
"qr-creator": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@custom-elements-manifest/analyzer": "^0.5.7",
|
"@custom-elements-manifest/analyzer": "^0.5.7",
|
||||||
"@open-wc/testing": "^3.0.3",
|
"@open-wc/testing": "^3.0.3",
|
||||||
"@types/color": "^3.0.2",
|
"@types/color": "^3.0.2",
|
||||||
|
"@types/mocha": "^9.0.0",
|
||||||
"@types/react": "^17.0.38",
|
"@types/react": "^17.0.38",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.9.0",
|
||||||
|
"@typescript-eslint/parser": "^5.9.0",
|
||||||
"@web/dev-server-esbuild": "^0.2.16",
|
"@web/dev-server-esbuild": "^0.2.16",
|
||||||
"@web/test-runner": "^0.13.23",
|
"@web/test-runner": "^0.13.23",
|
||||||
"@web/test-runner-playwright": "^0.8.8",
|
"@web/test-runner-playwright": "^0.8.8",
|
||||||
|
@ -73,6 +79,13 @@
|
||||||
"del": "^6.0.0",
|
"del": "^6.0.0",
|
||||||
"download": "^8.0.0",
|
"download": "^8.0.0",
|
||||||
"esbuild": "^0.14.10",
|
"esbuild": "^0.14.10",
|
||||||
|
"eslint": "^8.6.0",
|
||||||
|
"eslint-plugin-chai-expect": "^3.0.0",
|
||||||
|
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||||
|
"eslint-plugin-import": "^2.25.4",
|
||||||
|
"eslint-plugin-lit": "^1.6.1",
|
||||||
|
"eslint-plugin-lit-a11y": "^2.2.0",
|
||||||
|
"eslint-plugin-wc": "^1.3.2",
|
||||||
"front-matter": "^4.0.2",
|
"front-matter": "^4.0.2",
|
||||||
"get-port": "^6.0.0",
|
"get-port": "^6.0.0",
|
||||||
"globby": "^12.0.2",
|
"globby": "^12.0.2",
|
||||||
|
@ -88,6 +101,7 @@
|
||||||
"sinon": "^12.0.1",
|
"sinon": "^12.0.1",
|
||||||
"strip-css-comments": "^5.0.0",
|
"strip-css-comments": "^5.0.0",
|
||||||
"tslib": "^2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"typescript": "^4.5.4"
|
"typescript": "^4.5.4",
|
||||||
|
"utility-types": "^3.10.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ fs.mkdirSync(outdir, { recursive: true });
|
||||||
execSync(`node scripts/make-vscode-data.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
execSync(`node scripts/make-vscode-data.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||||
execSync(`node scripts/make-css.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
execSync(`node scripts/make-css.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||||
execSync(`node scripts/make-icons.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
execSync(`node scripts/make-icons.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||||
if (types) execSync(`tsc --project . --outdir "${outdir}"`, { stdio: 'inherit' });
|
if (types) execSync(`tsc --project ./tsconfig.prod.json --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(chalk.red(err));
|
console.error(chalk.red(err));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
//
|
//
|
||||||
// This script runs the Custom Elements Manifest analyzer to generate custom-elements.json
|
// This script runs the Custom Elements Manifest analyzer to generate custom-elements.json
|
||||||
//
|
//
|
||||||
import commandLineArgs from 'command-line-args';
|
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
|
import commandLineArgs from 'command-line-args';
|
||||||
|
|
||||||
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import chalk from 'chalk';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import del from 'del';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import del from 'del';
|
||||||
import { pascalCase } from 'pascal-case';
|
import { pascalCase } from 'pascal-case';
|
||||||
import prettier from 'prettier';
|
import prettier from 'prettier';
|
||||||
import prettierConfig from '../prettier.config.cjs';
|
import prettierConfig from '../prettier.config.cjs';
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import styles from './{{ tagWithoutPrefix tag }}.styles';
|
import styles from './{{ tagWithoutPrefix tag }}.styles';
|
||||||
|
import { emit } from '~/internal/event';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||||
// import sinon from 'sinon';
|
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type {{ properCase tag }} from './{{ tagWithoutPrefix tag }}';
|
|
||||||
|
|
||||||
describe('<{{ tag }}>', () => {
|
describe('<{{ tag }}>', () => {
|
||||||
it('should render a component', async () => {
|
it('should render a component', async () => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,33 +1,31 @@
|
||||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlAlert from './alert';
|
import type SlAlert from './alert';
|
||||||
|
|
||||||
describe('<sl-alert>', () => {
|
describe('<sl-alert>', () => {
|
||||||
it('should be visible with the open attribute', async () => {
|
it('should be visible with the open attribute', async () => {
|
||||||
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
|
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
|
|
||||||
expect(base.hidden).to.be.false;
|
expect(base.hidden).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not be visible without the open attribute', async () => {
|
it('should not be visible without the open attribute', async () => {
|
||||||
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
|
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
|
|
||||||
expect(base.hidden).to.be.true;
|
expect(base.hidden).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit sl-show and sl-after-show when calling show()', async () => {
|
it('should emit sl-show and sl-after-show when calling show()', async () => {
|
||||||
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
|
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
const showHandler = sinon.spy();
|
const showHandler = sinon.spy();
|
||||||
const afterShowHandler = sinon.spy();
|
const afterShowHandler = sinon.spy();
|
||||||
|
|
||||||
el.addEventListener('sl-show', showHandler);
|
el.addEventListener('sl-show', showHandler);
|
||||||
el.addEventListener('sl-after-show', afterShowHandler);
|
el.addEventListener('sl-after-show', afterShowHandler);
|
||||||
el.show();
|
void el.show();
|
||||||
|
|
||||||
await waitUntil(() => showHandler.calledOnce);
|
await waitUntil(() => showHandler.calledOnce);
|
||||||
await waitUntil(() => afterShowHandler.calledOnce);
|
await waitUntil(() => afterShowHandler.calledOnce);
|
||||||
|
@ -39,13 +37,13 @@ describe('<sl-alert>', () => {
|
||||||
|
|
||||||
it('should emit sl-hide and sl-after-hide when calling hide()', async () => {
|
it('should emit sl-hide and sl-after-hide when calling hide()', async () => {
|
||||||
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
|
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
const hideHandler = sinon.spy();
|
const hideHandler = sinon.spy();
|
||||||
const afterHideHandler = sinon.spy();
|
const afterHideHandler = sinon.spy();
|
||||||
|
|
||||||
el.addEventListener('sl-hide', hideHandler);
|
el.addEventListener('sl-hide', hideHandler);
|
||||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||||
el.hide();
|
void el.hide();
|
||||||
|
|
||||||
await waitUntil(() => hideHandler.calledOnce);
|
await waitUntil(() => hideHandler.calledOnce);
|
||||||
await waitUntil(() => afterHideHandler.calledOnce);
|
await waitUntil(() => afterHideHandler.calledOnce);
|
||||||
|
@ -57,7 +55,7 @@ describe('<sl-alert>', () => {
|
||||||
|
|
||||||
it('should emit sl-show and sl-after-show when setting open = true', async () => {
|
it('should emit sl-show and sl-after-show when setting open = true', async () => {
|
||||||
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
|
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
const showHandler = sinon.spy();
|
const showHandler = sinon.spy();
|
||||||
const afterShowHandler = sinon.spy();
|
const afterShowHandler = sinon.spy();
|
||||||
|
|
||||||
|
@ -75,7 +73,7 @@ describe('<sl-alert>', () => {
|
||||||
|
|
||||||
it('should emit sl-hide and sl-after-hide when setting open = false', async () => {
|
it('should emit sl-hide and sl-after-hide when setting open = false', async () => {
|
||||||
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
|
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
const hideHandler = sinon.spy();
|
const hideHandler = sinon.spy();
|
||||||
const afterHideHandler = sinon.spy();
|
const afterHideHandler = sinon.spy();
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, query } from 'lit/decorators.js';
|
import { customElement, property, query } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import { waitForEvent } from '../../internal/event';
|
|
||||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
|
||||||
import styles from './alert.styles';
|
import styles from './alert.styles';
|
||||||
|
import '~/components/icon-button/icon-button';
|
||||||
import '../icon-button/icon-button';
|
import { animateTo, stopAnimations } from '~/internal/animate';
|
||||||
|
import { emit, waitForEvent } from '~/internal/event';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
import { getAnimation, setDefaultAnimation } from '~/utilities/animation-registry';
|
||||||
|
|
||||||
const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' });
|
const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' });
|
||||||
|
|
||||||
|
@ -41,7 +39,7 @@ const toastStack = Object.assign(document.createElement('div'), { className: 'sl
|
||||||
export default class SlAlert extends LitElement {
|
export default class SlAlert extends LitElement {
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
|
|
||||||
private autoHideTimeout: any;
|
private autoHideTimeout: NodeJS.Timeout;
|
||||||
|
|
||||||
@query('[part="base"]') base: HTMLElement;
|
@query('[part="base"]') base: HTMLElement;
|
||||||
|
|
||||||
|
@ -58,7 +56,7 @@ export default class SlAlert extends LitElement {
|
||||||
* The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with
|
* The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with
|
||||||
* the alert before it closes (e.g. moves the mouse over it), the timer will restart. Defaults to `Infinity`.
|
* the alert before it closes (e.g. moves the mouse over it), the timer will restart. Defaults to `Infinity`.
|
||||||
*/
|
*/
|
||||||
@property({ type: Number }) duration: number = Infinity;
|
@property({ type: Number }) duration = Infinity;
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
this.base.hidden = !this.open;
|
this.base.hidden = !this.open;
|
||||||
|
@ -67,7 +65,7 @@ export default class SlAlert extends LitElement {
|
||||||
/** Shows the alert. */
|
/** Shows the alert. */
|
||||||
async show() {
|
async show() {
|
||||||
if (this.open) {
|
if (this.open) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.open = true;
|
this.open = true;
|
||||||
|
@ -77,7 +75,7 @@ export default class SlAlert extends LitElement {
|
||||||
/** Hides the alert */
|
/** Hides the alert */
|
||||||
async hide() {
|
async hide() {
|
||||||
if (!this.open) {
|
if (!this.open) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.open = false;
|
this.open = false;
|
||||||
|
@ -91,7 +89,7 @@ export default class SlAlert extends LitElement {
|
||||||
*/
|
*/
|
||||||
async toast() {
|
async toast() {
|
||||||
return new Promise<void>(resolve => {
|
return new Promise<void>(resolve => {
|
||||||
if (!toastStack.parentElement) {
|
if (toastStack.parentElement === null) {
|
||||||
document.body.append(toastStack);
|
document.body.append(toastStack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,8 +97,9 @@ export default class SlAlert extends LitElement {
|
||||||
|
|
||||||
// Wait for the toast stack to render
|
// Wait for the toast stack to render
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.clientWidth; // force a reflow for the initial transition
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- force a reflow for the initial transition
|
||||||
this.show();
|
this.clientWidth;
|
||||||
|
void this.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addEventListener(
|
this.addEventListener(
|
||||||
|
@ -110,7 +109,7 @@ export default class SlAlert extends LitElement {
|
||||||
resolve();
|
resolve();
|
||||||
|
|
||||||
// Remove the toast stack from the DOM when there are no more alerts
|
// Remove the toast stack from the DOM when there are no more alerts
|
||||||
if (!toastStack.querySelector('sl-alert')) {
|
if (toastStack.querySelector('sl-alert') === null) {
|
||||||
toastStack.remove();
|
toastStack.remove();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -122,12 +121,14 @@ export default class SlAlert extends LitElement {
|
||||||
restartAutoHide() {
|
restartAutoHide() {
|
||||||
clearTimeout(this.autoHideTimeout);
|
clearTimeout(this.autoHideTimeout);
|
||||||
if (this.open && this.duration < Infinity) {
|
if (this.open && this.duration < Infinity) {
|
||||||
this.autoHideTimeout = setTimeout(() => this.hide(), this.duration);
|
this.autoHideTimeout = setTimeout(() => {
|
||||||
|
void this.hide();
|
||||||
|
}, this.duration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCloseClick() {
|
handleCloseClick() {
|
||||||
this.hide();
|
void this.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseMove() {
|
handleMouseMove() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
// import sinon from 'sinon';
|
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlAnimatedImage from './animated-image';
|
|
||||||
|
|
||||||
describe('<sl-animated-image>', () => {
|
describe('<sl-animated-image>', () => {
|
||||||
it('should render a component', async () => {
|
it('should render a component', async () => {
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import styles from './animated-image.styles';
|
import styles from './animated-image.styles';
|
||||||
|
import '~/components/icon/icon';
|
||||||
import '../icon/icon';
|
import { emit } from '~/internal/event';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -63,7 +62,7 @@ export default class SlAnimatedImage extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
@watch('play')
|
@watch('play')
|
||||||
async handlePlayChange() {
|
handlePlayChange() {
|
||||||
// When the animation starts playing, reset the src so it plays from the beginning. Since the src is cached, this
|
// When the animation starts playing, reset the src so it plays from the beginning. Since the src is cached, this
|
||||||
// won't trigger another request.
|
// won't trigger another request.
|
||||||
if (this.play) {
|
if (this.play) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, queryAsync } from 'lit/decorators.js';
|
import { customElement, property, queryAsync } from 'lit/decorators.js';
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import { animations } from './animations';
|
|
||||||
import styles from './animation.styles';
|
import styles from './animation.styles';
|
||||||
|
import { animations } from './animations';
|
||||||
|
import { emit } from '~/internal/event';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -20,7 +20,7 @@ import styles from './animation.styles';
|
||||||
export default class SlAnimation extends LitElement {
|
export default class SlAnimation extends LitElement {
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
|
|
||||||
private animation: Animation;
|
private animation?: Animation;
|
||||||
private hasStarted = false;
|
private hasStarted = false;
|
||||||
|
|
||||||
@queryAsync('slot') defaultSlot: Promise<HTMLSlotElement>;
|
@queryAsync('slot') defaultSlot: Promise<HTMLSlotElement>;
|
||||||
|
@ -56,13 +56,13 @@ export default class SlAnimation extends LitElement {
|
||||||
@property() fill: FillMode = 'auto';
|
@property() fill: FillMode = 'auto';
|
||||||
|
|
||||||
/** The number of iterations to run before the animation completes. Defaults to `Infinity`, which loops. */
|
/** The number of iterations to run before the animation completes. Defaults to `Infinity`, which loops. */
|
||||||
@property({ type: Number }) iterations: number = Infinity;
|
@property({ type: Number }) iterations = Infinity;
|
||||||
|
|
||||||
/** The offset at which to start the animation, usually between 0 (start) and 1 (end). */
|
/** The offset at which to start the animation, usually between 0 (start) and 1 (end). */
|
||||||
@property({ attribute: 'iteration-start', type: Number }) iterationStart = 0;
|
@property({ attribute: 'iteration-start', type: Number }) iterationStart = 0;
|
||||||
|
|
||||||
/** The keyframes to use for the animation. If this is set, `name` will be ignored. */
|
/** The keyframes to use for the animation. If this is set, `name` will be ignored. */
|
||||||
@property({ attribute: false }) keyframes: Keyframe[];
|
@property({ attribute: false }) keyframes?: Keyframe[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the animation's playback rate. The default is `1`, which plays the animation at a normal speed. Setting this
|
* Sets the animation's playback rate. The default is `1`, which plays the animation at a normal speed. Setting this
|
||||||
|
@ -73,18 +73,18 @@ export default class SlAnimation extends LitElement {
|
||||||
|
|
||||||
/** Gets and sets the current animation time. */
|
/** Gets and sets the current animation time. */
|
||||||
get currentTime(): number {
|
get currentTime(): number {
|
||||||
return this.animation?.currentTime || 0;
|
return this.animation?.currentTime ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
set currentTime(time: number) {
|
set currentTime(time: number) {
|
||||||
if (this.animation) {
|
if (typeof this.animation !== 'undefined') {
|
||||||
this.animation.currentTime = time;
|
this.animation.currentTime = time;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.createAnimation();
|
void this.createAnimation();
|
||||||
this.handleAnimationCancel = this.handleAnimationCancel.bind(this);
|
this.handleAnimationCancel = this.handleAnimationCancel.bind(this);
|
||||||
this.handleAnimationFinish = this.handleAnimationFinish.bind(this);
|
this.handleAnimationFinish = this.handleAnimationFinish.bind(this);
|
||||||
}
|
}
|
||||||
|
@ -104,12 +104,12 @@ export default class SlAnimation extends LitElement {
|
||||||
@watch('iterations')
|
@watch('iterations')
|
||||||
@watch('iterationsStart')
|
@watch('iterationsStart')
|
||||||
@watch('keyframes')
|
@watch('keyframes')
|
||||||
async handleAnimationChange() {
|
handleAnimationChange() {
|
||||||
if (!this.hasUpdated) {
|
if (!this.hasUpdated) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.createAnimation();
|
void this.createAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAnimationFinish() {
|
handleAnimationFinish() {
|
||||||
|
@ -126,39 +126,43 @@ export default class SlAnimation extends LitElement {
|
||||||
|
|
||||||
@watch('play')
|
@watch('play')
|
||||||
handlePlayChange() {
|
handlePlayChange() {
|
||||||
if (this.animation) {
|
if (typeof this.animation !== 'undefined') {
|
||||||
if (this.play && !this.hasStarted) {
|
if (this.play && !this.hasStarted) {
|
||||||
this.hasStarted = true;
|
this.hasStarted = true;
|
||||||
emit(this, 'sl-start');
|
emit(this, 'sl-start');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.play ? this.animation.play() : this.animation.pause();
|
if (this.play) {
|
||||||
|
this.animation.play();
|
||||||
|
} else {
|
||||||
|
this.animation.pause();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@watch('playbackRate')
|
@watch('playbackRate')
|
||||||
handlePlaybackRateChange() {
|
handlePlaybackRateChange() {
|
||||||
if (this.animation) {
|
if (typeof this.animation !== 'undefined') {
|
||||||
this.animation.playbackRate = this.playbackRate;
|
this.animation.playbackRate = this.playbackRate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSlotChange() {
|
handleSlotChange() {
|
||||||
this.destroyAnimation();
|
this.destroyAnimation();
|
||||||
this.createAnimation();
|
void this.createAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
async createAnimation() {
|
async createAnimation() {
|
||||||
const easing = animations.easings[this.easing] || this.easing;
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- The specified easing may not exist
|
||||||
const keyframes: Keyframe[] = this.keyframes ? this.keyframes : (animations as any)[this.name];
|
const easing = animations.easings[this.easing] ?? this.easing;
|
||||||
|
const keyframes = this.keyframes ?? (animations as unknown as Partial<Record<string, Keyframe[]>>)[this.name];
|
||||||
const slot = await this.defaultSlot;
|
const slot = await this.defaultSlot;
|
||||||
const element = slot.assignedElements()[0] as HTMLElement;
|
const element = slot.assignedElements()[0] as HTMLElement | undefined;
|
||||||
|
|
||||||
if (!element) {
|
if (typeof element === 'undefined' || typeof keyframes === 'undefined') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +192,7 @@ export default class SlAnimation extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroyAnimation() {
|
destroyAnimation() {
|
||||||
if (this.animation) {
|
if (typeof this.animation !== 'undefined') {
|
||||||
this.animation.cancel();
|
this.animation.cancel();
|
||||||
this.animation.removeEventListener('cancel', this.handleAnimationCancel);
|
this.animation.removeEventListener('cancel', this.handleAnimationCancel);
|
||||||
this.animation.removeEventListener('finish', this.handleAnimationFinish);
|
this.animation.removeEventListener('finish', this.handleAnimationFinish);
|
||||||
|
@ -198,16 +202,12 @@ export default class SlAnimation extends LitElement {
|
||||||
|
|
||||||
/** Clears all KeyframeEffects caused by this animation and aborts its playback. */
|
/** Clears all KeyframeEffects caused by this animation and aborts its playback. */
|
||||||
cancel() {
|
cancel() {
|
||||||
try {
|
this.animation?.cancel();
|
||||||
this.animation.cancel();
|
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the playback time to the end of the animation corresponding to the current playback direction. */
|
/** Sets the playback time to the end of the animation corresponding to the current playback direction. */
|
||||||
finish() {
|
finish() {
|
||||||
try {
|
this.animation?.finish();
|
||||||
this.animation.finish();
|
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { expect, fixture, html } from '@open-wc/testing';
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlAvatar from './avatar';
|
import type SlAvatar from './avatar';
|
||||||
|
|
||||||
describe('<sl-avatar>', () => {
|
describe('<sl-avatar>', () => {
|
||||||
let el: SlAvatar;
|
let el: SlAvatar;
|
||||||
|
|
||||||
describe('when provided no parameters', async () => {
|
describe('when provided no parameters', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlAvatar>(html` <sl-avatar label="Avatar"></sl-avatar> `);
|
el = await fixture<SlAvatar>(html` <sl-avatar label="Avatar"></sl-avatar> `);
|
||||||
});
|
});
|
||||||
|
@ -15,14 +13,14 @@ describe('<sl-avatar>', () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should default to circle styling', async () => {
|
it('should default to circle styling', () => {
|
||||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||||
expect(el.getAttribute('shape')).to.eq('circle');
|
expect(el.getAttribute('shape')).to.eq('circle');
|
||||||
expect(part.classList.value.trim()).to.eq('avatar avatar--circle');
|
expect(part.classList.value.trim()).to.eq('avatar avatar--circle');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided an image and label parameter', async () => {
|
describe('when provided an image and label parameter', () => {
|
||||||
const image = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
const image = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||||
const label = 'Small transparent square';
|
const label = 'Small transparent square';
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
@ -40,20 +38,20 @@ describe('<sl-avatar>', () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders "image" part, with src and a role of presentation', async () => {
|
it('renders "image" part, with src and a role of presentation', () => {
|
||||||
const part = el.shadowRoot?.querySelector('[part="image"]') as HTMLImageElement;
|
const part = el.shadowRoot!.querySelector('[part="image"]')!;
|
||||||
|
|
||||||
expect(part.getAttribute('src')).to.eq(image);
|
expect(part.getAttribute('src')).to.eq(image);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the label attribute in the "base" part', async () => {
|
it('renders the label attribute in the "base" part', () => {
|
||||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||||
|
|
||||||
expect(part.getAttribute('aria-label')).to.eq(label);
|
expect(part.getAttribute('aria-label')).to.eq(label);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided initials parameter', async () => {
|
describe('when provided initials parameter', () => {
|
||||||
const initials = 'SL';
|
const initials = 'SL';
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlAvatar>(html`<sl-avatar initials="${initials}" label="Avatar"></sl-avatar>`);
|
el = await fixture<SlAvatar>(html`<sl-avatar initials="${initials}" label="Avatar"></sl-avatar>`);
|
||||||
|
@ -63,8 +61,8 @@ describe('<sl-avatar>', () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders "initials" part, with initials as the text node', async () => {
|
it('renders "initials" part, with initials as the text node', () => {
|
||||||
const part = el.shadowRoot?.querySelector('[part="initials"]') as HTMLImageElement;
|
const part = el.shadowRoot!.querySelector<HTMLElement>('[part="initials"]')!;
|
||||||
|
|
||||||
expect(part.innerText).to.eq(initials);
|
expect(part.innerText).to.eq(initials);
|
||||||
});
|
});
|
||||||
|
@ -73,15 +71,15 @@ describe('<sl-avatar>', () => {
|
||||||
['square', 'rounded', 'circle'].forEach(shape => {
|
['square', 'rounded', 'circle'].forEach(shape => {
|
||||||
describe(`when passed a shape attribute ${shape}`, () => {
|
describe(`when passed a shape attribute ${shape}`, () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlAvatar>(html`<sl-avatar shape="${shape as any}" label="Shaped avatar"></sl-avatar>`);
|
el = await fixture<SlAvatar>(html`<sl-avatar shape="${shape}" label="Shaped avatar"></sl-avatar>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes accessibility test', async () => {
|
it('passes accessibility test', async () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('appends the appropriate class on the "base" part', async () => {
|
it('appends the appropriate class on the "base" part', () => {
|
||||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||||
|
|
||||||
expect(el.getAttribute('shape')).to.eq(shape);
|
expect(el.getAttribute('shape')).to.eq(shape);
|
||||||
expect(part.classList.value.trim()).to.eq(`avatar avatar--${shape}`);
|
expect(part.classList.value.trim()).to.eq(`avatar avatar--${shape}`);
|
||||||
|
@ -89,7 +87,7 @@ describe('<sl-avatar>', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when passed a <span>, on slot "icon"', async () => {
|
describe('when passed a <span>, on slot "icon"', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlAvatar>(html`<sl-avatar label="Avatar"><span slot="icon">random content</span></sl-avatar>`);
|
el = await fixture<SlAvatar>(html`<sl-avatar label="Avatar"><span slot="icon">random content</span></sl-avatar>`);
|
||||||
});
|
});
|
||||||
|
@ -98,13 +96,13 @@ describe('<sl-avatar>', () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept as an assigned child in the shadow root', async () => {
|
it('should accept as an assigned child in the shadow root', () => {
|
||||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=icon]');
|
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=icon]')!;
|
||||||
const childNodes = slot.assignedNodes({ flatten: true });
|
const childNodes = slot.assignedNodes({ flatten: true }) as HTMLElement[];
|
||||||
|
|
||||||
expect(childNodes.length).to.eq(1);
|
expect(childNodes.length).to.eq(1);
|
||||||
|
|
||||||
const span = <HTMLElement>childNodes[0];
|
const span = childNodes[0];
|
||||||
expect(span.innerHTML).to.eq('random content');
|
expect(span.innerHTML).to.eq('random content');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,8 +2,7 @@ import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import styles from './avatar.styles';
|
import styles from './avatar.styles';
|
||||||
|
import '~/components/icon/icon';
|
||||||
import '../icon/icon';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -27,13 +26,13 @@ export default class SlAvatar extends LitElement {
|
||||||
@state() private hasError = false;
|
@state() private hasError = false;
|
||||||
|
|
||||||
/** The image source to use for the avatar. */
|
/** The image source to use for the avatar. */
|
||||||
@property() image: string;
|
@property() image?: string;
|
||||||
|
|
||||||
/** A label to use to describe the avatar to assistive devices. */
|
/** A label to use to describe the avatar to assistive devices. */
|
||||||
@property() label: string;
|
@property() label = '';
|
||||||
|
|
||||||
/** Initials to use as a fallback when no image is available (1-2 characters max recommended). */
|
/** Initials to use as a fallback when no image is available (1-2 characters max recommended). */
|
||||||
@property() initials: string;
|
@property() initials?: string;
|
||||||
|
|
||||||
/** The shape of the avatar. */
|
/** The shape of the avatar. */
|
||||||
@property({ reflect: true }) shape: 'circle' | 'square' | 'rounded' = 'circle';
|
@property({ reflect: true }) shape: 'circle' | 'square' | 'rounded' = 'circle';
|
||||||
|
@ -51,7 +50,7 @@ export default class SlAvatar extends LitElement {
|
||||||
role="img"
|
role="img"
|
||||||
aria-label=${this.label}
|
aria-label=${this.label}
|
||||||
>
|
>
|
||||||
${this.initials
|
${typeof this.initials !== 'undefined'
|
||||||
? html` <div part="initials" class="avatar__initials">${this.initials}</div> `
|
? html` <div part="initials" class="avatar__initials">${this.initials}</div> `
|
||||||
: html`
|
: html`
|
||||||
<div part="icon" class="avatar__icon" aria-hidden="true">
|
<div part="icon" class="avatar__icon" aria-hidden="true">
|
||||||
|
@ -60,7 +59,7 @@ export default class SlAvatar extends LitElement {
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
${this.image && !this.hasError
|
${typeof this.image !== 'undefined' && !this.hasError
|
||||||
? html`
|
? html`
|
||||||
<img
|
<img
|
||||||
part="image"
|
part="image"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { expect, fixture, html } from '@open-wc/testing';
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlBadge from './badge';
|
import type SlBadge from './badge';
|
||||||
|
|
||||||
describe('<sl-badge>', () => {
|
describe('<sl-badge>', () => {
|
||||||
let el: SlBadge;
|
let el: SlBadge;
|
||||||
|
|
||||||
describe('when provided no parameters', async () => {
|
describe('when provided no parameters', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlBadge>(html` <sl-badge>Badge</sl-badge> `);
|
el = await fixture<SlBadge>(html` <sl-badge>Badge</sl-badge> `);
|
||||||
});
|
});
|
||||||
|
@ -14,21 +12,21 @@ describe('<sl-badge>', () => {
|
||||||
it('should render a component that passes accessibility test, with a role of status on the base part.', async () => {
|
it('should render a component that passes accessibility test, with a role of status on the base part.', async () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
|
|
||||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||||
expect(part.getAttribute('role')).to.eq('status');
|
expect(part.getAttribute('role')).to.eq('status');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the child content provided', async () => {
|
it('should render the child content provided', () => {
|
||||||
expect(el.innerText).to.eq('Badge');
|
expect(el.innerText).to.eq('Badge');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should default to square styling, with the primary color', async () => {
|
it('should default to square styling, with the primary color', () => {
|
||||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||||
expect(part.classList.value.trim()).to.eq('badge badge--primary');
|
expect(part.classList.value.trim()).to.eq('badge badge--primary');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided a pill parameter', async () => {
|
describe('when provided a pill parameter', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlBadge>(html` <sl-badge pill>Badge</sl-badge> `);
|
el = await fixture<SlBadge>(html` <sl-badge pill>Badge</sl-badge> `);
|
||||||
});
|
});
|
||||||
|
@ -37,13 +35,13 @@ describe('<sl-badge>', () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should append the pill class to the classlist to render a pill', async () => {
|
it('should append the pill class to the classlist to render a pill', () => {
|
||||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||||
expect(part.classList.value.trim()).to.eq('badge badge--primary badge--pill');
|
expect(part.classList.value.trim()).to.eq('badge badge--primary badge--pill');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided a pulse parameter', async () => {
|
describe('when provided a pulse parameter', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlBadge>(html` <sl-badge pulse>Badge</sl-badge> `);
|
el = await fixture<SlBadge>(html` <sl-badge pulse>Badge</sl-badge> `);
|
||||||
});
|
});
|
||||||
|
@ -52,8 +50,8 @@ describe('<sl-badge>', () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should append the pulse class to the classlist to render a pulse', async () => {
|
it('should append the pulse class to the classlist to render a pulse', () => {
|
||||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||||
expect(part.classList.value.trim()).to.eq('badge badge--primary badge--pulse');
|
expect(part.classList.value.trim()).to.eq('badge badge--primary badge--pulse');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -61,15 +59,15 @@ describe('<sl-badge>', () => {
|
||||||
['primary', 'success', 'neutral', 'warning', 'danger'].forEach(variant => {
|
['primary', 'success', 'neutral', 'warning', 'danger'].forEach(variant => {
|
||||||
describe(`when passed a variant attribute ${variant}`, () => {
|
describe(`when passed a variant attribute ${variant}`, () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlBadge>(html`<sl-badge variant="${variant as any}">Badge</sl-badge>`);
|
el = await fixture<SlBadge>(html`<sl-badge variant="${variant}">Badge</sl-badge>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a component that passes accessibility test', async () => {
|
it('should render a component that passes accessibility test', async () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should default to square styling, with the primary color', async () => {
|
it('should default to square styling, with the primary color', () => {
|
||||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||||
expect(part.classList.value.trim()).to.eq(`badge badge--${variant}`);
|
expect(part.classList.value.trim()).to.eq(`badge badge--${variant}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { expect, fixture, html } from '@open-wc/testing';
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlBreadcrumbItem from './breadcrumb-item';
|
import type SlBreadcrumbItem from './breadcrumb-item';
|
||||||
|
|
||||||
describe('<sl-breadcrumb-item>', () => {
|
describe('<sl-breadcrumb-item>', () => {
|
||||||
let el: SlBreadcrumbItem;
|
let el: SlBreadcrumbItem;
|
||||||
|
|
||||||
describe('when not provided a href attribute', async () => {
|
describe('when not provided a href attribute', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlBreadcrumbItem>(html` <sl-breadcrumb-item>Home</sl-breadcrumb-item> `);
|
el = await fixture<SlBreadcrumbItem>(html` <sl-breadcrumb-item>Home</sl-breadcrumb-item> `);
|
||||||
});
|
});
|
||||||
|
@ -15,19 +13,19 @@ describe('<sl-breadcrumb-item>', () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hide the separator from screen readers', async () => {
|
it('should hide the separator from screen readers', () => {
|
||||||
const separator: HTMLSpanElement = el.shadowRoot.querySelector('[part="separator"]');
|
const separator = el.shadowRoot!.querySelector<HTMLSpanElement>('[part="separator"]');
|
||||||
expect(separator).attribute('aria-hidden', 'true');
|
expect(separator).attribute('aria-hidden', 'true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a HTMLButtonElement as the part "label", with a set type "button"', () => {
|
it('should render a HTMLButtonElement as the part "label", with a set type "button"', () => {
|
||||||
const button: HTMLButtonElement = el.shadowRoot.querySelector('[part="label"]');
|
const button = el.shadowRoot!.querySelector<HTMLButtonElement>('[part="label"]');
|
||||||
expect(button).to.exist;
|
expect(button).to.exist;
|
||||||
expect(button).attribute('type', 'button');
|
expect(button).attribute('type', 'button');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided a href attribute', async () => {
|
describe('when provided a href attribute', () => {
|
||||||
describe('and no target', () => {
|
describe('and no target', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlBreadcrumbItem>(html`
|
el = await fixture<SlBreadcrumbItem>(html`
|
||||||
|
@ -40,7 +38,7 @@ describe('<sl-breadcrumb-item>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a HTMLAnchorElement as the part "label", with the supplied href value', () => {
|
it('should render a HTMLAnchorElement as the part "label", with the supplied href value', () => {
|
||||||
const hyperlink: HTMLAnchorElement = el.shadowRoot.querySelector('[part="label"]');
|
const hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('[part="label"]');
|
||||||
expect(hyperlink).attribute('href', 'https://jsonplaceholder.typicode.com/');
|
expect(hyperlink).attribute('href', 'https://jsonplaceholder.typicode.com/');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -57,10 +55,10 @@ describe('<sl-breadcrumb-item>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should render a HTMLAnchorElement as the part "label"', () => {
|
describe('should render a HTMLAnchorElement as the part "label"', () => {
|
||||||
let hyperlink: HTMLAnchorElement;
|
let hyperlink: HTMLAnchorElement | null;
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
hyperlink = el.shadowRoot.querySelector('[part="label"]');
|
hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('[part="label"]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use the supplied href value, as the href attribute value', () => {
|
it('should use the supplied href value, as the href attribute value', () => {
|
||||||
|
@ -87,10 +85,10 @@ describe('<sl-breadcrumb-item>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should render a HTMLAnchorElement', () => {
|
describe('should render a HTMLAnchorElement', () => {
|
||||||
let hyperlink: HTMLAnchorElement;
|
let hyperlink: HTMLAnchorElement | null;
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
hyperlink = el.shadowRoot.querySelector('a');
|
hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('a');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use the supplied href value, as the href attribute value', () => {
|
it('should use the supplied href value, as the href attribute value', () => {
|
||||||
|
@ -104,7 +102,7 @@ describe('<sl-breadcrumb-item>', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided an element in the slot "prefix" to support prefix icons', async () => {
|
describe('when provided an element in the slot "prefix" to support prefix icons', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlBreadcrumbItem>(html`
|
el = await fixture<SlBreadcrumbItem>(html`
|
||||||
<sl-breadcrumb-item>
|
<sl-breadcrumb-item>
|
||||||
|
@ -119,19 +117,19 @@ describe('<sl-breadcrumb-item>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept as an assigned child in the shadow root', () => {
|
it('should accept as an assigned child in the shadow root', () => {
|
||||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=prefix]');
|
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=prefix]')!;
|
||||||
const childNodes = slot.assignedNodes({ flatten: true });
|
const childNodes = slot.assignedNodes({ flatten: true });
|
||||||
|
|
||||||
expect(childNodes.length).to.eq(1);
|
expect(childNodes.length).to.eq(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should append class "breadcrumb-item--has-prefix" to "base" part', () => {
|
it('should append class "breadcrumb-item--has-prefix" to "base" part', () => {
|
||||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||||
expect(part.classList.value.trim()).to.equal('breadcrumb-item breadcrumb-item--has-prefix');
|
expect(part.classList.value.trim()).to.equal('breadcrumb-item breadcrumb-item--has-prefix');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided an element in the slot "suffix" to support suffix icons', async () => {
|
describe('when provided an element in the slot "suffix" to support suffix icons', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlBreadcrumbItem>(html`
|
el = await fixture<SlBreadcrumbItem>(html`
|
||||||
<sl-breadcrumb-item>
|
<sl-breadcrumb-item>
|
||||||
|
@ -146,14 +144,14 @@ describe('<sl-breadcrumb-item>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept as an assigned child in the shadow root', () => {
|
it('should accept as an assigned child in the shadow root', () => {
|
||||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=suffix]');
|
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=suffix]')!;
|
||||||
const childNodes = slot.assignedNodes({ flatten: true });
|
const childNodes = slot.assignedNodes({ flatten: true });
|
||||||
|
|
||||||
expect(childNodes.length).to.eq(1);
|
expect(childNodes.length).to.eq(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should append class "breadcrumb-item--has-suffix" to "base" part', () => {
|
it('should append class "breadcrumb-item--has-suffix" to "base" part', () => {
|
||||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const part = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
expect(part.classList.value.trim()).to.equal('breadcrumb-item breadcrumb-item--has-suffix');
|
expect(part.classList.value.trim()).to.equal('breadcrumb-item breadcrumb-item--has-suffix');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { LitElement, html } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { HasSlotController } from '../../internal/slot';
|
|
||||||
import styles from './breadcrumb-item.styles';
|
import styles from './breadcrumb-item.styles';
|
||||||
|
import { HasSlotController } from '~/internal/slot';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -25,22 +25,22 @@ import styles from './breadcrumb-item.styles';
|
||||||
export default class SlBreadcrumbItem extends LitElement {
|
export default class SlBreadcrumbItem extends LitElement {
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
|
|
||||||
private hasSlotController = new HasSlotController(this, 'prefix', 'suffix');
|
private readonly hasSlotController = new HasSlotController(this, 'prefix', 'suffix');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional URL to direct the user to when the breadcrumb item is activated. When set, a link will be rendered
|
* Optional URL to direct the user to when the breadcrumb item is activated. When set, a link will be rendered
|
||||||
* internally. When unset, a button will be rendered instead.
|
* internally. When unset, a button will be rendered instead.
|
||||||
*/
|
*/
|
||||||
@property() href: string;
|
@property() href?: string;
|
||||||
|
|
||||||
/** Tells the browser where to open the link. Only used when `href` is set. */
|
/** Tells the browser where to open the link. Only used when `href` is set. */
|
||||||
@property() target: '_blank' | '_parent' | '_self' | '_top';
|
@property() target?: '_blank' | '_parent' | '_self' | '_top';
|
||||||
|
|
||||||
/** The `rel` attribute to use on the link. Only used when `href` is set. */
|
/** The `rel` attribute to use on the link. Only used when `href` is set. */
|
||||||
@property() rel: string = 'noreferrer noopener';
|
@property() rel = 'noreferrer noopener';
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const isLink = this.href ? true : false;
|
const isLink = typeof this.href !== 'undefined';
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
|
@ -62,7 +62,7 @@ export default class SlBreadcrumbItem extends LitElement {
|
||||||
class="breadcrumb-item__label breadcrumb-item__label--link"
|
class="breadcrumb-item__label breadcrumb-item__label--link"
|
||||||
href="${this.href}"
|
href="${this.href}"
|
||||||
target="${this.target}"
|
target="${this.target}"
|
||||||
rel=${ifDefined(this.target ? this.rel : undefined)}
|
rel=${ifDefined(typeof this.target !== 'undefined' ? this.rel : undefined)}
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { expect, fixture, html } from '@open-wc/testing';
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlBreadcrumb from './breadcrumb';
|
import type SlBreadcrumb from './breadcrumb';
|
||||||
|
|
||||||
describe('<sl-breadcrumb>', () => {
|
describe('<sl-breadcrumb>', () => {
|
||||||
let el: SlBreadcrumb;
|
let el: SlBreadcrumb;
|
||||||
|
|
||||||
describe('when provided a standard list of el-breadcrumb-item children and no parameters', async () => {
|
describe('when provided a standard list of el-breadcrumb-item children and no parameters', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlBreadcrumb>(html`
|
el = await fixture<SlBreadcrumb>(html`
|
||||||
<sl-breadcrumb>
|
<sl-breadcrumb>
|
||||||
|
@ -22,18 +20,18 @@ describe('<sl-breadcrumb>', () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render sl-icon as separator', async () => {
|
it('should render sl-icon as separator', () => {
|
||||||
expect(el.querySelectorAll('sl-icon').length).to.eq(4);
|
expect(el.querySelectorAll('sl-icon').length).to.eq(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should attach aria-current "page" on the last breadcrumb item.', async () => {
|
it('should attach aria-current "page" on the last breadcrumb item.', () => {
|
||||||
const breadcrumbItems = el.querySelectorAll('sl-breadcrumb-item');
|
const breadcrumbItems = el.querySelectorAll('sl-breadcrumb-item');
|
||||||
const lastNode = breadcrumbItems[3];
|
const lastNode = breadcrumbItems[3];
|
||||||
expect(lastNode).attribute('aria-current', 'page');
|
expect(lastNode).attribute('aria-current', 'page');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "separator" to support Custom Separators', async () => {
|
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "separator" to support Custom Separators', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlBreadcrumb>(html`
|
el = await fixture<SlBreadcrumb>(html`
|
||||||
<sl-breadcrumb>
|
<sl-breadcrumb>
|
||||||
|
@ -49,20 +47,20 @@ describe('<sl-breadcrumb>', () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept "separator" as an assigned child in the shadow root', async () => {
|
it('should accept "separator" as an assigned child in the shadow root', () => {
|
||||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=separator]');
|
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=separator]')!;
|
||||||
const childNodes = slot.assignedNodes({ flatten: true });
|
const childNodes = slot.assignedNodes({ flatten: true });
|
||||||
|
|
||||||
expect(childNodes.length).to.eq(1);
|
expect(childNodes.length).to.eq(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should replace the sl-icon separator with the provided separator', async () => {
|
it('should replace the sl-icon separator with the provided separator', () => {
|
||||||
expect(el.querySelectorAll('.replacement-separator').length).to.eq(4);
|
expect(el.querySelectorAll('.replacement-separator').length).to.eq(4);
|
||||||
expect(el.querySelectorAll('sl-icon').length).to.eq(0);
|
expect(el.querySelectorAll('sl-icon').length).to.eq(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "prefix" to support prefix icons', async () => {
|
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "prefix" to support prefix icons', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlBreadcrumb>(html`
|
el = await fixture<SlBreadcrumb>(html`
|
||||||
<sl-breadcrumb>
|
<sl-breadcrumb>
|
||||||
|
@ -82,7 +80,7 @@ describe('<sl-breadcrumb>', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "suffix" to support suffix icons', async () => {
|
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "suffix" to support suffix icons', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlBreadcrumb>(html`
|
el = await fixture<SlBreadcrumb>(html`
|
||||||
<sl-breadcrumb>
|
<sl-breadcrumb>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, query } from 'lit/decorators.js';
|
import { customElement, property, query } from 'lit/decorators.js';
|
||||||
import styles from './breadcrumb.styles';
|
import styles from './breadcrumb.styles';
|
||||||
import type SlBreadcrumbItem from '../breadcrumb-item/breadcrumb-item';
|
import type SlBreadcrumbItem from '~/components/breadcrumb-item/breadcrumb-item';
|
||||||
|
import '~/components/icon/icon';
|
||||||
import '../icon/icon';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -35,7 +34,9 @@ export default class SlBreadcrumb extends LitElement {
|
||||||
|
|
||||||
// Clone it, remove ids, and slot it
|
// Clone it, remove ids, and slot it
|
||||||
const clone = separator.cloneNode(true) as HTMLElement;
|
const clone = separator.cloneNode(true) as HTMLElement;
|
||||||
[clone, ...clone.querySelectorAll('[id]')].map(el => el.removeAttribute('id'));
|
[clone, ...clone.querySelectorAll('[id]')].forEach(el => {
|
||||||
|
el.removeAttribute('id');
|
||||||
|
});
|
||||||
clone.slot = 'separator';
|
clone.slot = 'separator';
|
||||||
|
|
||||||
return clone;
|
return clone;
|
||||||
|
@ -46,10 +47,10 @@ export default class SlBreadcrumb extends LitElement {
|
||||||
item => item.tagName.toLowerCase() === 'sl-breadcrumb-item'
|
item => item.tagName.toLowerCase() === 'sl-breadcrumb-item'
|
||||||
) as SlBreadcrumbItem[];
|
) as SlBreadcrumbItem[];
|
||||||
|
|
||||||
items.map((item, index) => {
|
items.forEach((item, index) => {
|
||||||
// Append separators to each item if they don't already have one
|
// Append separators to each item if they don't already have one
|
||||||
const separator = item.querySelector('[slot="separator"]') as HTMLElement;
|
const separator = item.querySelector('[slot="separator"]');
|
||||||
if (!separator) {
|
if (separator === null) {
|
||||||
item.append(this.getSeparator());
|
item.append(this.getSeparator());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -42,11 +42,11 @@ export default class SlButtonGroup extends LitElement {
|
||||||
handleSlotChange() {
|
handleSlotChange() {
|
||||||
const slottedElements = [...this.defaultSlot.assignedElements({ flatten: true })] as HTMLElement[];
|
const slottedElements = [...this.defaultSlot.assignedElements({ flatten: true })] as HTMLElement[];
|
||||||
|
|
||||||
slottedElements.map(el => {
|
slottedElements.forEach(el => {
|
||||||
const index = slottedElements.indexOf(el);
|
const index = slottedElements.indexOf(el);
|
||||||
const button = findButton(el);
|
const button = findButton(el);
|
||||||
|
|
||||||
if (button) {
|
if (button !== null) {
|
||||||
button.classList.add('sl-button-group__button');
|
button.classList.add('sl-button-group__button');
|
||||||
button.classList.toggle('sl-button-group__button--first', index === 0);
|
button.classList.toggle('sl-button-group__button--first', index === 0);
|
||||||
button.classList.toggle('sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1);
|
button.classList.toggle('sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1);
|
||||||
|
@ -56,6 +56,7 @@ export default class SlButtonGroup extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
// eslint-disable-next-line lit-a11y/mouse-events-have-key-events -- focusout & focusin support bubbling where as focus & blur do not which is necessary here
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
part="base"
|
part="base"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { LitElement } from 'lit';
|
import { LitElement } from 'lit';
|
||||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||||
import { html, literal } from 'lit/static-html.js';
|
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { emit } from '../../internal/event';
|
import { html, literal } from 'lit/static-html.js';
|
||||||
import { FormSubmitController } from '../../internal/form-control';
|
|
||||||
import { HasSlotController } from '../../internal/slot';
|
|
||||||
import styles from './button.styles';
|
import styles from './button.styles';
|
||||||
|
import '~/components/spinner/spinner';
|
||||||
import '../spinner/spinner';
|
import { emit } from '~/internal/event';
|
||||||
|
import { FormSubmitController } from '~/internal/form-control';
|
||||||
|
import { HasSlotController } from '~/internal/slot';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -35,8 +34,8 @@ export default class SlButton extends LitElement {
|
||||||
|
|
||||||
@query('.button') button: HTMLButtonElement | HTMLLinkElement;
|
@query('.button') button: HTMLButtonElement | HTMLLinkElement;
|
||||||
|
|
||||||
private formSubmitController = new FormSubmitController(this);
|
private readonly formSubmitController = new FormSubmitController(this);
|
||||||
private hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
||||||
|
|
||||||
@state() private hasFocus = false;
|
@state() private hasFocus = false;
|
||||||
|
|
||||||
|
@ -72,19 +71,19 @@ export default class SlButton extends LitElement {
|
||||||
@property() type: 'button' | 'submit' = 'button';
|
@property() type: 'button' | 'submit' = 'button';
|
||||||
|
|
||||||
/** An optional name for the button. Ignored when `href` is set. */
|
/** An optional name for the button. Ignored when `href` is set. */
|
||||||
@property() name: string;
|
@property() name?: string;
|
||||||
|
|
||||||
/** An optional value for the button. Ignored when `href` is set. */
|
/** An optional value for the button. Ignored when `href` is set. */
|
||||||
@property() value: string;
|
@property() value?: string;
|
||||||
|
|
||||||
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
|
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
|
||||||
@property() href: string;
|
@property() href?: string;
|
||||||
|
|
||||||
/** Tells the browser where to open the link. Only used when `href` is set. */
|
/** Tells the browser where to open the link. Only used when `href` is set. */
|
||||||
@property() target: '_blank' | '_parent' | '_self' | '_top';
|
@property() target?: '_blank' | '_parent' | '_self' | '_top';
|
||||||
|
|
||||||
/** Tells the browser to download the linked file as this filename. Only used when `href` is set. */
|
/** Tells the browser to download the linked file as this filename. Only used when `href` is set. */
|
||||||
@property() download: string;
|
@property() download?: string;
|
||||||
|
|
||||||
/** Simulates a click on the button. */
|
/** Simulates a click on the button. */
|
||||||
click() {
|
click() {
|
||||||
|
@ -124,9 +123,10 @@ export default class SlButton extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const isLink = this.href ? true : false;
|
const isLink = typeof this.href !== 'undefined';
|
||||||
const tag = isLink ? literal`a` : literal`button`;
|
const tag = isLink ? literal`a` : literal`button`;
|
||||||
|
|
||||||
|
/* eslint-disable lit/binding-positions, lit/no-invalid-html */
|
||||||
return html`
|
return html`
|
||||||
<${tag}
|
<${tag}
|
||||||
part="base"
|
part="base"
|
||||||
|
@ -161,7 +161,7 @@ export default class SlButton extends LitElement {
|
||||||
href=${ifDefined(this.href)}
|
href=${ifDefined(this.href)}
|
||||||
target=${ifDefined(this.target)}
|
target=${ifDefined(this.target)}
|
||||||
download=${ifDefined(this.download)}
|
download=${ifDefined(this.download)}
|
||||||
rel=${ifDefined(this.target ? 'noreferrer noopener' : undefined)}
|
rel=${ifDefined(typeof this.target !== 'undefined' ? 'noreferrer noopener' : undefined)}
|
||||||
role="button"
|
role="button"
|
||||||
aria-disabled=${this.disabled ? 'true' : 'false'}
|
aria-disabled=${this.disabled ? 'true' : 'false'}
|
||||||
tabindex=${this.disabled ? '-1' : '0'}
|
tabindex=${this.disabled ? '-1' : '0'}
|
||||||
|
@ -199,6 +199,7 @@ export default class SlButton extends LitElement {
|
||||||
${this.loading ? html`<sl-spinner></sl-spinner>` : ''}
|
${this.loading ? html`<sl-spinner></sl-spinner>` : ''}
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`;
|
`;
|
||||||
|
/* eslint-enable lit/binding-positions, lit/no-invalid-html */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { expect, fixture, html } from '@open-wc/testing';
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlCard from './card';
|
import type SlCard from './card';
|
||||||
|
|
||||||
describe('<sl-card>', () => {
|
describe('<sl-card>', () => {
|
||||||
let el: SlCard;
|
let el: SlCard;
|
||||||
|
|
||||||
describe('when provided no parameters', async () => {
|
describe('when provided no parameters', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlCard>(
|
el = await fixture<SlCard>(
|
||||||
html` <sl-card>This is just a basic card. No image, no header, and no footer. Just your content.</sl-card> `
|
html` <sl-card>This is just a basic card. No image, no header, and no footer. Just your content.</sl-card> `
|
||||||
|
@ -17,17 +15,17 @@ describe('<sl-card>', () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the child content provided.', async () => {
|
it('should render the child content provided.', () => {
|
||||||
expect(el.innerText).to.eq('This is just a basic card. No image, no header, and no footer. Just your content.');
|
expect(el.innerText).to.eq('This is just a basic card. No image, no header, and no footer. Just your content.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain the class card.', async () => {
|
it('should contain the class card.', () => {
|
||||||
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
|
const card = el.shadowRoot!.querySelector('.card')!;
|
||||||
expect(card.classList.value.trim()).to.eq('card');
|
expect(card.classList.value.trim()).to.eq('card');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided an element in the slot "header" to render a header', async () => {
|
describe('when provided an element in the slot "header" to render a header', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlCard>(
|
el = await fixture<SlCard>(
|
||||||
html`<sl-card>
|
html`<sl-card>
|
||||||
|
@ -41,29 +39,29 @@ describe('<sl-card>', () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the child content provided.', async () => {
|
it('should render the child content provided.', () => {
|
||||||
expect(el.innerText).to.contain('This card has a header. You can put all sorts of things in it!');
|
expect(el.innerText).to.contain('This card has a header. You can put all sorts of things in it!');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('render the header content provided.', async () => {
|
it('render the header content provided.', () => {
|
||||||
const header = <HTMLDivElement>el.querySelector('div[slot=header]');
|
const header = el.querySelector<HTMLElement>('div[slot=header]')!;
|
||||||
expect(header.innerText).eq('Header Title');
|
expect(header.innerText).eq('Header Title');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accept "header" as an assigned child in the shadow root.', async () => {
|
it('accept "header" as an assigned child in the shadow root.', () => {
|
||||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=header]');
|
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=header]')!;
|
||||||
const childNodes = slot.assignedNodes({ flatten: true });
|
const childNodes = slot.assignedNodes({ flatten: true });
|
||||||
|
|
||||||
expect(childNodes.length).to.eq(1);
|
expect(childNodes.length).to.eq(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain the class card--has-header.', async () => {
|
it('should contain the class card--has-header.', () => {
|
||||||
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
|
const card = el.shadowRoot!.querySelector('.card')!;
|
||||||
expect(card.classList.value.trim()).to.eq('card card--has-header');
|
expect(card.classList.value.trim()).to.eq('card card--has-header');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided an element in the slot "footer" to render a footer', async () => {
|
describe('when provided an element in the slot "footer" to render a footer', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlCard>(
|
el = await fixture<SlCard>(
|
||||||
html`<sl-card>
|
html`<sl-card>
|
||||||
|
@ -78,35 +76,35 @@ describe('<sl-card>', () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the child content provided.', async () => {
|
it('should render the child content provided.', () => {
|
||||||
expect(el.innerText).to.contain('This card has a footer. You can put all sorts of things in it!');
|
expect(el.innerText).to.contain('This card has a footer. You can put all sorts of things in it!');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('render the footer content provided.', async () => {
|
it('render the footer content provided.', () => {
|
||||||
const footer = <HTMLDivElement>el.querySelector('div[slot=footer]');
|
const footer = el.querySelector<HTMLElement>('div[slot=footer]')!;
|
||||||
expect(footer.innerText).eq('Footer Content');
|
expect(footer.innerText).eq('Footer Content');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accept "footer" as an assigned child in the shadow root.', async () => {
|
it('accept "footer" as an assigned child in the shadow root.', () => {
|
||||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=footer]');
|
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=footer]')!;
|
||||||
const childNodes = slot.assignedNodes({ flatten: true });
|
const childNodes = slot.assignedNodes({ flatten: true });
|
||||||
|
|
||||||
expect(childNodes.length).to.eq(1);
|
expect(childNodes.length).to.eq(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain the class card--has-footer.', async () => {
|
it('should contain the class card--has-footer.', () => {
|
||||||
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
|
const card = el.shadowRoot!.querySelector('.card')!;
|
||||||
expect(card.classList.value.trim()).to.eq('card card--has-footer');
|
expect(card.classList.value.trim()).to.eq('card card--has-footer');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided an element in the slot "image" to render a image', async () => {
|
describe('when provided an element in the slot "image" to render a image', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlCard>(
|
el = await fixture<SlCard>(
|
||||||
html`<sl-card>
|
html`<sl-card>
|
||||||
<img
|
<img
|
||||||
slot="image"
|
slot="image"
|
||||||
src="https://images.unsplash.com/photo-1547191783-94d5f8f6d8b1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=80"
|
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||||
alt="A kitten walks towards camera on top of pallet."
|
alt="A kitten walks towards camera on top of pallet."
|
||||||
/>
|
/>
|
||||||
This is a kitten, but not just any kitten. This kitten likes walking along pallets.
|
This is a kitten, but not just any kitten. This kitten likes walking along pallets.
|
||||||
|
@ -118,21 +116,21 @@ describe('<sl-card>', () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the child content provided.', async () => {
|
it('should render the child content provided.', () => {
|
||||||
expect(el.innerText).to.contain(
|
expect(el.innerText).to.contain(
|
||||||
'This is a kitten, but not just any kitten. This kitten likes walking along pallets.'
|
'This is a kitten, but not just any kitten. This kitten likes walking along pallets.'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accept "image" as an assigned child in the shadow root.', async () => {
|
it('accept "image" as an assigned child in the shadow root.', () => {
|
||||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=image]');
|
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=image]')!;
|
||||||
const childNodes = slot.assignedNodes({ flatten: true });
|
const childNodes = slot.assignedNodes({ flatten: true });
|
||||||
|
|
||||||
expect(childNodes.length).to.eq(1);
|
expect(childNodes.length).to.eq(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain the class card--has-image.', async () => {
|
it('should contain the class card--has-image.', () => {
|
||||||
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
|
const card = el.shadowRoot!.querySelector('.card')!;
|
||||||
expect(card.classList.value.trim()).to.eq('card card--has-image');
|
expect(card.classList.value.trim()).to.eq('card card--has-image');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement } from 'lit/decorators.js';
|
import { customElement } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { HasSlotController } from '../../internal/slot';
|
|
||||||
import styles from './card.styles';
|
import styles from './card.styles';
|
||||||
|
import { HasSlotController } from '~/internal/slot';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -28,7 +28,7 @@ import styles from './card.styles';
|
||||||
export default class SlCard extends LitElement {
|
export default class SlCard extends LitElement {
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
|
|
||||||
private hasSlotController = new HasSlotController(this, 'footer', 'header', 'image');
|
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'image');
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
|
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||||
import { sendKeys } from '@web/test-runner-commands';
|
import { sendKeys } from '@web/test-runner-commands';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlCheckbox from './checkbox';
|
import type SlCheckbox from './checkbox';
|
||||||
|
|
||||||
describe('<sl-checkbox>', () => {
|
describe('<sl-checkbox>', () => {
|
||||||
|
@ -9,7 +7,7 @@ describe('<sl-checkbox>', () => {
|
||||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox disabled></sl-checkbox> `);
|
const el = await fixture<SlCheckbox>(html` <sl-checkbox disabled></sl-checkbox> `);
|
||||||
const checkbox = el.shadowRoot?.querySelector('input');
|
const checkbox = el.shadowRoot?.querySelector('input');
|
||||||
|
|
||||||
expect(checkbox.disabled).to.be.true;
|
expect(checkbox!.disabled).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be valid by default', async () => {
|
it('should be valid by default', async () => {
|
||||||
|
@ -20,7 +18,9 @@ describe('<sl-checkbox>', () => {
|
||||||
|
|
||||||
it('should fire sl-change when clicked', async () => {
|
it('should fire sl-change when clicked', async () => {
|
||||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
|
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
|
||||||
setTimeout(() => el.shadowRoot?.querySelector('input').click());
|
setTimeout(() => {
|
||||||
|
el.shadowRoot!.querySelector('input')!.click();
|
||||||
|
});
|
||||||
const event = await oneEvent(el, 'sl-change');
|
const event = await oneEvent(el, 'sl-change');
|
||||||
expect(event.target).to.equal(el);
|
expect(event.target).to.equal(el);
|
||||||
expect(el.checked).to.be.true;
|
expect(el.checked).to.be.true;
|
||||||
|
@ -28,9 +28,11 @@ describe('<sl-checkbox>', () => {
|
||||||
|
|
||||||
it('should fire sl-change when toggled via keyboard', async () => {
|
it('should fire sl-change when toggled via keyboard', async () => {
|
||||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
|
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
|
||||||
const input = el.shadowRoot?.querySelector('input');
|
const input = el.shadowRoot!.querySelector('input')!;
|
||||||
input.focus();
|
input.focus();
|
||||||
setTimeout(() => sendKeys({ press: ' ' }));
|
setTimeout(() => {
|
||||||
|
void sendKeys({ press: ' ' });
|
||||||
|
});
|
||||||
const event = await oneEvent(el, 'sl-change');
|
const event = await oneEvent(el, 'sl-change');
|
||||||
expect(event.target).to.equal(el);
|
expect(event.target).to.equal(el);
|
||||||
expect(el.checked).to.be.true;
|
expect(el.checked).to.be.true;
|
||||||
|
|
|
@ -3,12 +3,11 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { live } from 'lit/directives/live.js';
|
import { live } from 'lit/directives/live.js';
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import { FormSubmitController } from '../../internal/form-control';
|
|
||||||
import styles from './checkbox.styles';
|
import styles from './checkbox.styles';
|
||||||
|
import { autoIncrement } from '~/internal/autoIncrement';
|
||||||
let id = 0;
|
import { emit } from '~/internal/event';
|
||||||
|
import { FormSubmitController } from '~/internal/form-control';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -32,12 +31,13 @@ export default class SlCheckbox extends LitElement {
|
||||||
|
|
||||||
@query('input[type="checkbox"]') input: HTMLInputElement;
|
@query('input[type="checkbox"]') input: HTMLInputElement;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-expect-error -- Controller is currently unused
|
||||||
private formSubmitController = new FormSubmitController(this, {
|
private readonly formSubmitController = new FormSubmitController(this, {
|
||||||
value: (control: SlCheckbox) => (control.checked ? control.value : undefined)
|
value: (control: SlCheckbox) => (control.checked ? control.value : undefined)
|
||||||
});
|
});
|
||||||
private inputId = `checkbox-${++id}`;
|
private readonly attrId = autoIncrement();
|
||||||
private labelId = `checkbox-label-${id}`;
|
private readonly inputId = `checkbox-${this.attrId}`;
|
||||||
|
private readonly labelId = `checkbox-label-${this.attrId}`;
|
||||||
|
|
||||||
@state() private hasFocus = false;
|
@state() private hasFocus = false;
|
||||||
|
|
||||||
|
@ -103,13 +103,11 @@ export default class SlCheckbox extends LitElement {
|
||||||
emit(this, 'sl-blur');
|
emit(this, 'sl-blur');
|
||||||
}
|
}
|
||||||
|
|
||||||
@watch('disabled')
|
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||||
handleDisabledChange() {
|
handleDisabledChange() {
|
||||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||||
if (this.input) {
|
this.input.disabled = this.disabled;
|
||||||
this.input.disabled = this.disabled;
|
this.invalid = !this.input.checkValidity();
|
||||||
this.invalid = !this.input.checkValidity();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFocus() {
|
handleFocus() {
|
||||||
|
@ -146,7 +144,6 @@ export default class SlCheckbox extends LitElement {
|
||||||
.checked=${live(this.checked)}
|
.checked=${live(this.checked)}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
role="checkbox"
|
|
||||||
aria-checked=${this.checked ? 'true' : 'false'}
|
aria-checked=${this.checked ? 'true' : 'false'}
|
||||||
aria-labelledby=${this.labelId}
|
aria-labelledby=${this.labelId}
|
||||||
@click=${this.handleClick}
|
@click=${this.handleClick}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlColorPicker from './color-picker';
|
import type SlColorPicker from './color-picker';
|
||||||
|
|
||||||
describe('<sl-color-picker>', () => {
|
describe('<sl-color-picker>', () => {
|
||||||
it('should emit change and show correct color when the value changes', async () => {
|
it('should emit change and show correct color when the value changes', async () => {
|
||||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||||
const trigger = el.shadowRoot.querySelector('[part="trigger"]') as HTMLElement;
|
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[part="trigger"]')!;
|
||||||
const changeHandler = sinon.spy();
|
const changeHandler = sinon.spy();
|
||||||
const color = 'rgb(255, 204, 0)';
|
const color = 'rgb(255, 204, 0)';
|
||||||
|
|
||||||
|
@ -22,21 +20,21 @@ describe('<sl-color-picker>', () => {
|
||||||
|
|
||||||
it('should render in a dropdown', async () => {
|
it('should render in a dropdown', async () => {
|
||||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||||
const dropdown = el.shadowRoot.querySelector('sl-dropdown');
|
const dropdown = el.shadowRoot!.querySelector('sl-dropdown');
|
||||||
|
|
||||||
expect(dropdown).to.exist;
|
expect(dropdown).to.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render in a dropdown when inline is enabled', async () => {
|
it('should not render in a dropdown when inline is enabled', async () => {
|
||||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker inline></sl-color-picker> `);
|
const el = await fixture<SlColorPicker>(html` <sl-color-picker inline></sl-color-picker> `);
|
||||||
const dropdown = el.shadowRoot.querySelector('sl-dropdown');
|
const dropdown = el.shadowRoot!.querySelector('sl-dropdown');
|
||||||
|
|
||||||
expect(dropdown).to.not.exist;
|
expect(dropdown).to.not.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show opacity slider when opacity is enabled', async () => {
|
it('should show opacity slider when opacity is enabled', async () => {
|
||||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity></sl-color-picker> `);
|
const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity></sl-color-picker> `);
|
||||||
const opacitySlider = el.shadowRoot.querySelector('[part*="opacity-slider"]') as HTMLElement;
|
const opacitySlider = el.shadowRoot!.querySelector('[part*="opacity-slider"]')!;
|
||||||
|
|
||||||
expect(opacitySlider).to.exist;
|
expect(opacitySlider).to.exist;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,27 +1,37 @@
|
||||||
|
import Color from 'color';
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { live } from 'lit/directives/live.js';
|
import { live } from 'lit/directives/live.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import { clamp } from '../../internal/math';
|
|
||||||
import { FormSubmitController } from '../../internal/form-control';
|
|
||||||
import { LocalizeController } from '../../utilities/localize';
|
|
||||||
import type SlDropdown from '../dropdown/dropdown';
|
|
||||||
import type SlInput from '../input/input';
|
|
||||||
import color from 'color';
|
|
||||||
import styles from './color-picker.styles';
|
import styles from './color-picker.styles';
|
||||||
|
import '~/components/button-group/button-group';
|
||||||
import '../button/button';
|
import '~/components/button/button';
|
||||||
import '../button-group/button-group';
|
import type SlDropdown from '~/components/dropdown/dropdown';
|
||||||
import '../dropdown/dropdown';
|
import '~/components/dropdown/dropdown';
|
||||||
import '../icon/icon';
|
import '~/components/icon/icon';
|
||||||
import '../input/input';
|
import type SlInput from '~/components/input/input';
|
||||||
|
import '~/components/input/input';
|
||||||
|
import { drag } from '~/internal/drag';
|
||||||
|
import { emit } from '~/internal/event';
|
||||||
|
import { FormSubmitController } from '~/internal/form-control';
|
||||||
|
import { clamp } from '~/internal/math';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
import { LocalizeController } from '~/utilities/localize';
|
||||||
|
|
||||||
const hasEyeDropper = 'EyeDropper' in window;
|
const hasEyeDropper = 'EyeDropper' in window;
|
||||||
|
|
||||||
|
interface EyeDropperConstructor {
|
||||||
|
new (): EyeDropperInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EyeDropperInterface {
|
||||||
|
open: () => Promise<{ sRGBHex: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const EyeDropper: EyeDropperConstructor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
* @status stable
|
* @status stable
|
||||||
|
@ -63,11 +73,11 @@ export default class SlColorPicker extends LitElement {
|
||||||
@query('[part="preview"]') previewButton: HTMLButtonElement;
|
@query('[part="preview"]') previewButton: HTMLButtonElement;
|
||||||
@query('.color-dropdown') dropdown: SlDropdown;
|
@query('.color-dropdown') dropdown: SlDropdown;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-expect-error -- Controller is currently unused
|
||||||
private formSubmitController = new FormSubmitController(this);
|
private readonly formSubmitController = new FormSubmitController(this);
|
||||||
private isSafeValue = false;
|
private isSafeValue = false;
|
||||||
private lastValueEmitted: string;
|
private lastValueEmitted: string;
|
||||||
private localize = new LocalizeController(this);
|
private readonly localize = new LocalizeController(this);
|
||||||
|
|
||||||
@state() private inputValue = '';
|
@state() private inputValue = '';
|
||||||
@state() private hue = 0;
|
@state() private hue = 0;
|
||||||
|
@ -151,7 +161,7 @@ export default class SlColorPicker extends LitElement {
|
||||||
|
|
||||||
this.inputValue = this.value;
|
this.inputValue = this.value;
|
||||||
this.lastValueEmitted = this.value;
|
this.lastValueEmitted = this.value;
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the current value as a string in the specified format. */
|
/** Returns the current value as a string in the specified format. */
|
||||||
|
@ -160,7 +170,7 @@ export default class SlColorPicker extends LitElement {
|
||||||
`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!currentColor) {
|
if (currentColor === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,11 +205,10 @@ export default class SlColorPicker extends LitElement {
|
||||||
},
|
},
|
||||||
{ once: true }
|
{ once: true }
|
||||||
);
|
);
|
||||||
this.dropdown.show();
|
void this.dropdown.show();
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
return this.input.reportValidity();
|
|
||||||
}
|
}
|
||||||
|
return this.input.reportValidity();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||||
|
@ -215,9 +224,9 @@ export default class SlColorPicker extends LitElement {
|
||||||
|
|
||||||
// Show copied animation
|
// Show copied animation
|
||||||
this.previewButton.classList.add('color-picker__preview-color--copied');
|
this.previewButton.classList.add('color-picker__preview-color--copied');
|
||||||
this.previewButton.addEventListener('animationend', () =>
|
this.previewButton.addEventListener('animationend', () => {
|
||||||
this.previewButton.classList.remove('color-picker__preview-color--copied')
|
this.previewButton.classList.remove('color-picker__preview-color--copied');
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFormatToggle() {
|
handleFormatToggle() {
|
||||||
|
@ -226,106 +235,74 @@ export default class SlColorPicker extends LitElement {
|
||||||
this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl';
|
this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl';
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAlphaDrag(event: any) {
|
handleAlphaDrag(event: Event) {
|
||||||
const container = this.shadowRoot!.querySelector('.color-picker__slider.color-picker__alpha') as HTMLElement;
|
const container = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__slider.color-picker__alpha')!;
|
||||||
const handle = container.querySelector('.color-picker__slider-handle') as HTMLElement;
|
const handle = container.querySelector<HTMLElement>('.color-picker__slider-handle')!;
|
||||||
const { width } = container.getBoundingClientRect();
|
const { width } = container.getBoundingClientRect();
|
||||||
|
|
||||||
handle.focus();
|
handle.focus();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
this.handleDrag(event, container, x => {
|
drag(container, x => {
|
||||||
this.alpha = clamp((x / width) * 100, 0, 100);
|
this.alpha = clamp((x / width) * 100, 0, 100);
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHueDrag(event: any) {
|
handleHueDrag(event: Event) {
|
||||||
const container = this.shadowRoot!.querySelector('.color-picker__slider.color-picker__hue') as HTMLElement;
|
const container = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__slider.color-picker__hue')!;
|
||||||
const handle = container.querySelector('.color-picker__slider-handle') as HTMLElement;
|
const handle = container.querySelector<HTMLElement>('.color-picker__slider-handle')!;
|
||||||
const { width } = container.getBoundingClientRect();
|
const { width } = container.getBoundingClientRect();
|
||||||
|
|
||||||
handle.focus();
|
handle.focus();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
this.handleDrag(event, container, x => {
|
drag(container, x => {
|
||||||
this.hue = clamp((x / width) * 360, 0, 360);
|
this.hue = clamp((x / width) * 360, 0, 360);
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleGridDrag(event: any) {
|
handleGridDrag(event: Event) {
|
||||||
const grid = this.shadowRoot!.querySelector('.color-picker__grid') as HTMLElement;
|
const grid = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__grid')!;
|
||||||
const handle = grid.querySelector('.color-picker__grid-handle') as HTMLElement;
|
const handle = grid.querySelector<HTMLElement>('.color-picker__grid-handle')!;
|
||||||
const { width, height } = grid.getBoundingClientRect();
|
const { width, height } = grid.getBoundingClientRect();
|
||||||
|
|
||||||
handle.focus();
|
handle.focus();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
this.handleDrag(event, grid, (x, y) => {
|
drag(grid, (x, y) => {
|
||||||
this.saturation = clamp((x / width) * 100, 0, 100);
|
this.saturation = clamp((x / width) * 100, 0, 100);
|
||||||
this.lightness = clamp(100 - (y / height) * 100, 0, 100);
|
this.lightness = clamp(100 - (y / height) * 100, 0, 100);
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDrag(event: any, container: HTMLElement, onMove: (x: number, y: number) => void) {
|
|
||||||
if (this.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const move = (event: any) => {
|
|
||||||
const dims = container.getBoundingClientRect();
|
|
||||||
const defaultView = container.ownerDocument.defaultView!;
|
|
||||||
const offsetX = dims.left + defaultView.pageXOffset;
|
|
||||||
const offsetY = dims.top + defaultView.pageYOffset;
|
|
||||||
const x = (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - offsetX;
|
|
||||||
const y = (event.changedTouches ? event.changedTouches[0].pageY : event.pageY) - offsetY;
|
|
||||||
|
|
||||||
onMove(x, y);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Move on init
|
|
||||||
move(event);
|
|
||||||
|
|
||||||
const stop = () => {
|
|
||||||
document.removeEventListener('mousemove', move);
|
|
||||||
document.removeEventListener('touchmove', move);
|
|
||||||
document.removeEventListener('mouseup', stop);
|
|
||||||
document.removeEventListener('touchend', stop);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', move);
|
|
||||||
document.addEventListener('touchmove', move);
|
|
||||||
document.addEventListener('mouseup', stop);
|
|
||||||
document.addEventListener('touchend', stop);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAlphaKeyDown(event: KeyboardEvent) {
|
handleAlphaKeyDown(event: KeyboardEvent) {
|
||||||
const increment = event.shiftKey ? 10 : 1;
|
const increment = event.shiftKey ? 10 : 1;
|
||||||
|
|
||||||
if (event.key === 'ArrowLeft') {
|
if (event.key === 'ArrowLeft') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.alpha = clamp(this.alpha - increment, 0, 100);
|
this.alpha = clamp(this.alpha - increment, 0, 100);
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'ArrowRight') {
|
if (event.key === 'ArrowRight') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.alpha = clamp(this.alpha + increment, 0, 100);
|
this.alpha = clamp(this.alpha + increment, 0, 100);
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'Home') {
|
if (event.key === 'Home') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.alpha = 0;
|
this.alpha = 0;
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'End') {
|
if (event.key === 'End') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.alpha = 100;
|
this.alpha = 100;
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,25 +312,25 @@ export default class SlColorPicker extends LitElement {
|
||||||
if (event.key === 'ArrowLeft') {
|
if (event.key === 'ArrowLeft') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.hue = clamp(this.hue - increment, 0, 360);
|
this.hue = clamp(this.hue - increment, 0, 360);
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'ArrowRight') {
|
if (event.key === 'ArrowRight') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.hue = clamp(this.hue + increment, 0, 360);
|
this.hue = clamp(this.hue + increment, 0, 360);
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'Home') {
|
if (event.key === 'Home') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.hue = 0;
|
this.hue = 0;
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'End') {
|
if (event.key === 'End') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.hue = 360;
|
this.hue = 360;
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,25 +340,25 @@ export default class SlColorPicker extends LitElement {
|
||||||
if (event.key === 'ArrowLeft') {
|
if (event.key === 'ArrowLeft') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.saturation = clamp(this.saturation - increment, 0, 100);
|
this.saturation = clamp(this.saturation - increment, 0, 100);
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'ArrowRight') {
|
if (event.key === 'ArrowRight') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.saturation = clamp(this.saturation + increment, 0, 100);
|
this.saturation = clamp(this.saturation + increment, 0, 100);
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'ArrowUp') {
|
if (event.key === 'ArrowUp') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.lightness = clamp(this.lightness + increment, 0, 100);
|
this.lightness = clamp(this.lightness + increment, 0, 100);
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'ArrowDown') {
|
if (event.key === 'ArrowDown') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.lightness = clamp(this.lightness - increment, 0, 100);
|
this.lightness = clamp(this.lightness - increment, 0, 100);
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -397,7 +374,9 @@ export default class SlColorPicker extends LitElement {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
this.setColor(this.input.value);
|
this.setColor(this.input.value);
|
||||||
this.input.value = this.value;
|
this.input.value = this.value;
|
||||||
setTimeout(() => this.input.select());
|
setTimeout(() => {
|
||||||
|
this.input.select();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -419,7 +398,7 @@ export default class SlColorPicker extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rgba[3].indexOf('%') > -1) {
|
if (rgba[3].indexOf('%') > -1) {
|
||||||
rgba[3] = (Number(rgba[3].replace(/%/g, '')) / 100).toString();
|
rgba[3] = (parseFloat(rgba[3].replace(/%/g, '')) / 100).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`;
|
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`;
|
||||||
|
@ -437,7 +416,7 @@ export default class SlColorPicker extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hsla[3].indexOf('%') > -1) {
|
if (hsla[3].indexOf('%') > -1) {
|
||||||
hsla[3] = (Number(hsla[3].replace(/%/g, '')) / 100).toString();
|
hsla[3] = (parseFloat(hsla[3].replace(/%/g, '')) / 100).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return `hsla(${hsla[0]}, ${hsla[1]}, ${hsla[2]}, ${hsla[3]})`;
|
return `hsla(${hsla[0]}, ${hsla[1]}, ${hsla[2]}, ${hsla[3]})`;
|
||||||
|
@ -451,41 +430,40 @@ export default class SlColorPicker extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
parseColor(colorString: string) {
|
parseColor(colorString: string) {
|
||||||
function toHex(value: number) {
|
let parsed: Color;
|
||||||
const hex = Math.round(value).toString(16);
|
|
||||||
return hex.length === 1 ? `0${hex}` : hex;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed: any;
|
|
||||||
|
|
||||||
// The color module has a weak parser, so we normalize certain things to make the user experience better
|
// The color module has a weak parser, so we normalize certain things to make the user experience better
|
||||||
colorString = this.normalizeColorString(colorString);
|
colorString = this.normalizeColorString(colorString);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parsed = color(colorString);
|
parsed = Color(colorString);
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hslColor = parsed.hsl();
|
||||||
|
|
||||||
const hsl = {
|
const hsl = {
|
||||||
h: parsed.hsl().color[0],
|
h: hslColor.hue(),
|
||||||
s: parsed.hsl().color[1],
|
s: hslColor.saturationl(),
|
||||||
l: parsed.hsl().color[2],
|
l: hslColor.lightness(),
|
||||||
a: parsed.hsl().valpha
|
a: hslColor.alpha()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rgbColor = parsed.rgb();
|
||||||
|
|
||||||
const rgb = {
|
const rgb = {
|
||||||
r: parsed.rgb().color[0],
|
r: rgbColor.red(),
|
||||||
g: parsed.rgb().color[1],
|
g: rgbColor.green(),
|
||||||
b: parsed.rgb().color[2],
|
b: rgbColor.blue(),
|
||||||
a: parsed.rgb().valpha
|
a: rgbColor.alpha()
|
||||||
};
|
};
|
||||||
|
|
||||||
const hex = {
|
const hex = {
|
||||||
r: toHex(parsed.rgb().color[0]),
|
r: toHex(rgb.r),
|
||||||
g: toHex(parsed.rgb().color[1]),
|
g: toHex(rgb.g),
|
||||||
b: toHex(parsed.rgb().color[2]),
|
b: toHex(rgb.b),
|
||||||
a: toHex(parsed.rgb().valpha * 255)
|
a: toHex(rgb.a * 255)
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -501,9 +479,7 @@ export default class SlColorPicker extends LitElement {
|
||||||
l: hsl.l,
|
l: hsl.l,
|
||||||
a: hsl.a,
|
a: hsl.a,
|
||||||
string: this.setLetterCase(
|
string: this.setLetterCase(
|
||||||
`hsla(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%, ${Number(
|
`hsla(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%, ${hsl.a.toFixed(2).toString()})`
|
||||||
hsl.a.toFixed(2).toString()
|
|
||||||
)})`
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
rgb: {
|
rgb: {
|
||||||
|
@ -518,9 +494,7 @@ export default class SlColorPicker extends LitElement {
|
||||||
b: rgb.b,
|
b: rgb.b,
|
||||||
a: rgb.a,
|
a: rgb.a,
|
||||||
string: this.setLetterCase(
|
string: this.setLetterCase(
|
||||||
`rgba(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)}, ${Number(
|
`rgba(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)}, ${rgb.a.toFixed(2).toString()})`
|
||||||
rgb.a.toFixed(2).toString()
|
|
||||||
)})`
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
hex: this.setLetterCase(`#${hex.r}${hex.g}${hex.b}`),
|
hex: this.setLetterCase(`#${hex.r}${hex.g}${hex.b}`),
|
||||||
|
@ -531,7 +505,7 @@ export default class SlColorPicker extends LitElement {
|
||||||
setColor(colorString: string) {
|
setColor(colorString: string) {
|
||||||
const newColor = this.parseColor(colorString);
|
const newColor = this.parseColor(colorString);
|
||||||
|
|
||||||
if (!newColor) {
|
if (newColor === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -540,13 +514,15 @@ export default class SlColorPicker extends LitElement {
|
||||||
this.lightness = newColor.hsla.l;
|
this.lightness = newColor.hsla.l;
|
||||||
this.alpha = this.opacity ? newColor.hsla.a * 100 : 100;
|
this.alpha = this.opacity ? newColor.hsla.a * 100 : 100;
|
||||||
|
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLetterCase(string: string) {
|
setLetterCase(string: string) {
|
||||||
if (typeof string !== 'string') return '';
|
if (typeof string !== 'string') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
return this.uppercase ? string.toUpperCase() : string.toLowerCase();
|
return this.uppercase ? string.toUpperCase() : string.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -555,7 +531,7 @@ export default class SlColorPicker extends LitElement {
|
||||||
`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!currentColor) {
|
if (currentColor === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -586,12 +562,11 @@ export default class SlColorPicker extends LitElement {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const eyeDropper = new EyeDropper();
|
const eyeDropper = new EyeDropper();
|
||||||
|
|
||||||
eyeDropper
|
eyeDropper
|
||||||
.open()
|
.open()
|
||||||
.then((colorSelectionResult: any) => this.setColor(colorSelectionResult.sRGBHex))
|
.then(colorSelectionResult => this.setColor(colorSelectionResult.sRGBHex))
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// The user canceled, do nothing
|
// The user canceled, do nothing
|
||||||
});
|
});
|
||||||
|
@ -599,7 +574,7 @@ export default class SlColorPicker extends LitElement {
|
||||||
|
|
||||||
@watch('format')
|
@watch('format')
|
||||||
handleFormatChange() {
|
handleFormatChange() {
|
||||||
this.syncValues();
|
void this.syncValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
@watch('opacity')
|
@watch('opacity')
|
||||||
|
@ -608,11 +583,11 @@ export default class SlColorPicker extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
@watch('value')
|
@watch('value')
|
||||||
handleValueChange(oldValue: string, newValue: string) {
|
handleValueChange(oldValue: string | undefined, newValue: string) {
|
||||||
if (!this.isSafeValue) {
|
if (!this.isSafeValue && oldValue !== undefined) {
|
||||||
const newColor = this.parseColor(newValue);
|
const newColor = this.parseColor(newValue);
|
||||||
|
|
||||||
if (newColor) {
|
if (newColor !== null) {
|
||||||
this.inputValue = this.value;
|
this.inputValue = this.value;
|
||||||
this.hue = newColor.hsla.h;
|
this.hue = newColor.hsla.h;
|
||||||
this.saturation = newColor.hsla.s;
|
this.saturation = newColor.hsla.s;
|
||||||
|
@ -658,11 +633,8 @@ export default class SlColorPicker extends LitElement {
|
||||||
left: `${x}%`,
|
left: `${x}%`,
|
||||||
backgroundColor: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%)`
|
backgroundColor: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%)`
|
||||||
})}
|
})}
|
||||||
role="slider"
|
role="application"
|
||||||
aria-label="HSL"
|
aria-label="HSL"
|
||||||
aria-valuetext=${`hsl(${Math.round(this.hue)}, ${Math.round(this.saturation)}%, ${Math.round(
|
|
||||||
this.lightness
|
|
||||||
)}%)`}
|
|
||||||
tabindex=${ifDefined(this.disabled ? undefined : '0')}
|
tabindex=${ifDefined(this.disabled ? undefined : '0')}
|
||||||
@keydown=${this.handleGridKeyDown}
|
@keydown=${this.handleGridKeyDown}
|
||||||
></span>
|
></span>
|
||||||
|
@ -743,7 +715,7 @@ export default class SlColorPicker extends LitElement {
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="color-picker__user-input">
|
<div class="color-picker__user-input" aria-live="polite">
|
||||||
<sl-input
|
<sl-input
|
||||||
part="input"
|
part="input"
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -762,7 +734,7 @@ export default class SlColorPicker extends LitElement {
|
||||||
${!this.noFormatToggle
|
${!this.noFormatToggle
|
||||||
? html`
|
? html`
|
||||||
<sl-button
|
<sl-button
|
||||||
aria-label=${this.localize.term('toggle_color_format')}
|
aria-label=${this.localize.term('toggleColorFormat')}
|
||||||
exportparts="base:format-button"
|
exportparts="base:format-button"
|
||||||
@click=${this.handleFormatToggle}
|
@click=${this.handleFormatToggle}
|
||||||
>
|
>
|
||||||
|
@ -776,7 +748,7 @@ export default class SlColorPicker extends LitElement {
|
||||||
<sl-icon
|
<sl-icon
|
||||||
library="system"
|
library="system"
|
||||||
name="eyedropper"
|
name="eyedropper"
|
||||||
label=${this.localize.term('select_a_color_from_the_screen')}
|
label=${this.localize.term('selectAColorFromTheScreen')}
|
||||||
></sl-icon>
|
></sl-icon>
|
||||||
</sl-button>
|
</sl-button>
|
||||||
`
|
`
|
||||||
|
@ -784,7 +756,7 @@ export default class SlColorPicker extends LitElement {
|
||||||
</sl-button-group>
|
</sl-button-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${this.swatches
|
${this.swatches.length > 0
|
||||||
? html`
|
? html`
|
||||||
<div part="swatches" class="color-picker__swatches">
|
<div part="swatches" class="color-picker__swatches">
|
||||||
${this.swatches.map(swatch => {
|
${this.swatches.map(swatch => {
|
||||||
|
@ -828,12 +800,14 @@ export default class SlColorPicker extends LitElement {
|
||||||
part="trigger"
|
part="trigger"
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
'color-dropdown__trigger': true,
|
'color-dropdown__trigger': true,
|
||||||
'color-dropdown__trigger--disabled': this.disabled,
|
'color-dropdown__trigger--disabled': this.disabled,
|
||||||
'color-dropdown__trigger--small': this.size === 'small',
|
'color-dropdown__trigger--small': this.size === 'small',
|
||||||
'color-dropdown__trigger--medium': this.size === 'medium',
|
'color-dropdown__trigger--medium': this.size === 'medium',
|
||||||
'color-dropdown__trigger--large': this.size === 'large',
|
'color-dropdown__trigger--large': this.size === 'large',
|
||||||
'color-picker__transparent-bg': true
|
'color-picker__transparent-bg': true
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
})}
|
})}
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
color: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
color: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
||||||
|
@ -846,6 +820,11 @@ export default class SlColorPicker extends LitElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toHex(value: number) {
|
||||||
|
const hex = Math.round(value).toString(16);
|
||||||
|
return hex.length === 1 ? `0${hex}` : hex;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'sl-color-picker': SlColorPicker;
|
'sl-color-picker': SlColorPicker;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
// cspell:dictionaries lorem-ipsum
|
// cspell:dictionaries lorem-ipsum
|
||||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlDetails from './details';
|
import type SlDetails from './details';
|
||||||
|
|
||||||
describe('<sl-details>', () => {
|
describe('<sl-details>', () => {
|
||||||
|
@ -14,7 +12,7 @@ describe('<sl-details>', () => {
|
||||||
consequat.
|
consequat.
|
||||||
</sl-details>
|
</sl-details>
|
||||||
`);
|
`);
|
||||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
|
||||||
|
|
||||||
expect(body.hidden).to.be.false;
|
expect(body.hidden).to.be.false;
|
||||||
});
|
});
|
||||||
|
@ -27,7 +25,7 @@ describe('<sl-details>', () => {
|
||||||
consequat.
|
consequat.
|
||||||
</sl-details>
|
</sl-details>
|
||||||
`);
|
`);
|
||||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
|
||||||
|
|
||||||
expect(body.hidden).to.be.true;
|
expect(body.hidden).to.be.true;
|
||||||
});
|
});
|
||||||
|
@ -40,13 +38,13 @@ describe('<sl-details>', () => {
|
||||||
consequat.
|
consequat.
|
||||||
</sl-details>
|
</sl-details>
|
||||||
`);
|
`);
|
||||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
|
||||||
const showHandler = sinon.spy();
|
const showHandler = sinon.spy();
|
||||||
const afterShowHandler = sinon.spy();
|
const afterShowHandler = sinon.spy();
|
||||||
|
|
||||||
el.addEventListener('sl-show', showHandler);
|
el.addEventListener('sl-show', showHandler);
|
||||||
el.addEventListener('sl-after-show', afterShowHandler);
|
el.addEventListener('sl-after-show', afterShowHandler);
|
||||||
el.show();
|
void el.show();
|
||||||
|
|
||||||
await waitUntil(() => showHandler.calledOnce);
|
await waitUntil(() => showHandler.calledOnce);
|
||||||
await waitUntil(() => afterShowHandler.calledOnce);
|
await waitUntil(() => afterShowHandler.calledOnce);
|
||||||
|
@ -64,13 +62,13 @@ describe('<sl-details>', () => {
|
||||||
consequat.
|
consequat.
|
||||||
</sl-details>
|
</sl-details>
|
||||||
`);
|
`);
|
||||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
|
||||||
const hideHandler = sinon.spy();
|
const hideHandler = sinon.spy();
|
||||||
const afterHideHandler = sinon.spy();
|
const afterHideHandler = sinon.spy();
|
||||||
|
|
||||||
el.addEventListener('sl-hide', hideHandler);
|
el.addEventListener('sl-hide', hideHandler);
|
||||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||||
el.hide();
|
void el.hide();
|
||||||
|
|
||||||
await waitUntil(() => hideHandler.calledOnce);
|
await waitUntil(() => hideHandler.calledOnce);
|
||||||
await waitUntil(() => afterHideHandler.calledOnce);
|
await waitUntil(() => afterHideHandler.calledOnce);
|
||||||
|
@ -88,7 +86,7 @@ describe('<sl-details>', () => {
|
||||||
consequat.
|
consequat.
|
||||||
</sl-details>
|
</sl-details>
|
||||||
`);
|
`);
|
||||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
|
||||||
const showHandler = sinon.spy();
|
const showHandler = sinon.spy();
|
||||||
const afterShowHandler = sinon.spy();
|
const afterShowHandler = sinon.spy();
|
||||||
|
|
||||||
|
@ -112,7 +110,7 @@ describe('<sl-details>', () => {
|
||||||
consequat.
|
consequat.
|
||||||
</sl-details>
|
</sl-details>
|
||||||
`);
|
`);
|
||||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
|
||||||
const hideHandler = sinon.spy();
|
const hideHandler = sinon.spy();
|
||||||
const afterHideHandler = sinon.spy();
|
const afterHideHandler = sinon.spy();
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, query } from 'lit/decorators.js';
|
import { customElement, property, query } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { animateTo, stopAnimations, shimKeyframesHeightAuto } from '../../internal/animate';
|
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import { waitForEvent } from '../../internal/event';
|
|
||||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
|
||||||
import styles from './details.styles';
|
import styles from './details.styles';
|
||||||
|
import '~/components/icon/icon';
|
||||||
import '../icon/icon';
|
import { animateTo, stopAnimations, shimKeyframesHeightAuto } from '~/internal/animate';
|
||||||
|
import { emit, waitForEvent } from '~/internal/event';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
import { getAnimation, setDefaultAnimation } from '~/utilities/animation-registry';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -58,7 +56,7 @@ export default class SlDetails extends LitElement {
|
||||||
/** Shows the details. */
|
/** Shows the details. */
|
||||||
async show() {
|
async show() {
|
||||||
if (this.open) {
|
if (this.open) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.open = true;
|
this.open = true;
|
||||||
|
@ -68,16 +66,24 @@ export default class SlDetails extends LitElement {
|
||||||
/** Hides the details */
|
/** Hides the details */
|
||||||
async hide() {
|
async hide() {
|
||||||
if (!this.open) {
|
if (!this.open) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.open = false;
|
this.open = false;
|
||||||
return waitForEvent(this, 'sl-after-hide');
|
return waitForEvent(this, 'sl-after-hide');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleOpen() {
|
||||||
|
if (this.open) {
|
||||||
|
void this.hide();
|
||||||
|
} else {
|
||||||
|
void this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleSummaryClick() {
|
handleSummaryClick() {
|
||||||
if (!this.disabled) {
|
if (!this.disabled) {
|
||||||
this.open ? this.hide() : this.show();
|
this.toggleOpen();
|
||||||
this.header.focus();
|
this.header.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,17 +91,17 @@ export default class SlDetails extends LitElement {
|
||||||
handleSummaryKeyDown(event: KeyboardEvent) {
|
handleSummaryKeyDown(event: KeyboardEvent) {
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.open ? this.hide() : this.show();
|
this.toggleOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
|
if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.hide();
|
void this.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
|
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.show();
|
void this.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
// cspell:dictionaries lorem-ipsum
|
// cspell:dictionaries lorem-ipsum
|
||||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlDialog from './dialog';
|
import type SlDialog from './dialog';
|
||||||
|
|
||||||
describe('<sl-dialog>', () => {
|
describe('<sl-dialog>', () => {
|
||||||
|
@ -10,7 +8,7 @@ describe('<sl-dialog>', () => {
|
||||||
const el = await fixture<SlDialog>(html`
|
const el = await fixture<SlDialog>(html`
|
||||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
|
|
||||||
expect(base.hidden).to.be.false;
|
expect(base.hidden).to.be.false;
|
||||||
});
|
});
|
||||||
|
@ -19,7 +17,7 @@ describe('<sl-dialog>', () => {
|
||||||
const el = await fixture<SlDialog>(
|
const el = await fixture<SlDialog>(
|
||||||
html` <sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> `
|
html` <sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> `
|
||||||
);
|
);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
|
|
||||||
expect(base.hidden).to.be.true;
|
expect(base.hidden).to.be.true;
|
||||||
});
|
});
|
||||||
|
@ -28,13 +26,13 @@ describe('<sl-dialog>', () => {
|
||||||
const el = await fixture<SlDialog>(html`
|
const el = await fixture<SlDialog>(html`
|
||||||
<sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
<sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
const showHandler = sinon.spy();
|
const showHandler = sinon.spy();
|
||||||
const afterShowHandler = sinon.spy();
|
const afterShowHandler = sinon.spy();
|
||||||
|
|
||||||
el.addEventListener('sl-show', showHandler);
|
el.addEventListener('sl-show', showHandler);
|
||||||
el.addEventListener('sl-after-show', afterShowHandler);
|
el.addEventListener('sl-after-show', afterShowHandler);
|
||||||
el.show();
|
void el.show();
|
||||||
|
|
||||||
await waitUntil(() => showHandler.calledOnce);
|
await waitUntil(() => showHandler.calledOnce);
|
||||||
await waitUntil(() => afterShowHandler.calledOnce);
|
await waitUntil(() => afterShowHandler.calledOnce);
|
||||||
|
@ -48,13 +46,13 @@ describe('<sl-dialog>', () => {
|
||||||
const el = await fixture<SlDialog>(html`
|
const el = await fixture<SlDialog>(html`
|
||||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
const hideHandler = sinon.spy();
|
const hideHandler = sinon.spy();
|
||||||
const afterHideHandler = sinon.spy();
|
const afterHideHandler = sinon.spy();
|
||||||
|
|
||||||
el.addEventListener('sl-hide', hideHandler);
|
el.addEventListener('sl-hide', hideHandler);
|
||||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||||
el.hide();
|
void el.hide();
|
||||||
|
|
||||||
await waitUntil(() => hideHandler.calledOnce);
|
await waitUntil(() => hideHandler.calledOnce);
|
||||||
await waitUntil(() => afterHideHandler.calledOnce);
|
await waitUntil(() => afterHideHandler.calledOnce);
|
||||||
|
@ -68,7 +66,7 @@ describe('<sl-dialog>', () => {
|
||||||
const el = await fixture<SlDialog>(html`
|
const el = await fixture<SlDialog>(html`
|
||||||
<sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
<sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
const showHandler = sinon.spy();
|
const showHandler = sinon.spy();
|
||||||
const afterShowHandler = sinon.spy();
|
const afterShowHandler = sinon.spy();
|
||||||
|
|
||||||
|
@ -88,7 +86,7 @@ describe('<sl-dialog>', () => {
|
||||||
const el = await fixture<SlDialog>(html`
|
const el = await fixture<SlDialog>(html`
|
||||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
const hideHandler = sinon.spy();
|
const hideHandler = sinon.spy();
|
||||||
const afterHideHandler = sinon.spy();
|
const afterHideHandler = sinon.spy();
|
||||||
|
|
||||||
|
@ -108,9 +106,11 @@ describe('<sl-dialog>', () => {
|
||||||
const el = await fixture<SlDialog>(html`
|
const el = await fixture<SlDialog>(html`
|
||||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||||
`);
|
`);
|
||||||
const overlay = el.shadowRoot?.querySelector('[part="overlay"]') as HTMLElement;
|
const overlay = el.shadowRoot!.querySelector<HTMLElement>('[part="overlay"]')!;
|
||||||
|
|
||||||
el.addEventListener('sl-request-close', event => event.preventDefault());
|
el.addEventListener('sl-request-close', event => {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
overlay.click();
|
overlay.click();
|
||||||
|
|
||||||
expect(el.open).to.be.true;
|
expect(el.open).to.be.true;
|
||||||
|
@ -118,14 +118,14 @@ describe('<sl-dialog>', () => {
|
||||||
|
|
||||||
it('should allow initial focus to be set', async () => {
|
it('should allow initial focus to be set', async () => {
|
||||||
const el = await fixture<SlDialog>(html` <sl-dialog><input /></sl-dialog> `);
|
const el = await fixture<SlDialog>(html` <sl-dialog><input /></sl-dialog> `);
|
||||||
const input = el.querySelector('input');
|
const input = el.querySelector('input')!;
|
||||||
const initialFocusHandler = sinon.spy(event => {
|
const initialFocusHandler = sinon.spy((event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
input.focus();
|
input.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('sl-initial-focus', initialFocusHandler);
|
el.addEventListener('sl-initial-focus', initialFocusHandler);
|
||||||
el.show();
|
void el.show();
|
||||||
|
|
||||||
await waitUntil(() => initialFocusHandler.calledOnce);
|
await waitUntil(() => initialFocusHandler.calledOnce);
|
||||||
|
|
||||||
|
|
|
@ -2,18 +2,16 @@ import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, query } from 'lit/decorators.js';
|
import { customElement, property, query } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import { waitForEvent } from '../../internal/event';
|
|
||||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
|
|
||||||
import { HasSlotController } from '../../internal/slot';
|
|
||||||
import { isPreventScrollSupported } from '../../internal/support';
|
|
||||||
import Modal from '../../internal/modal';
|
|
||||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
|
||||||
import styles from './dialog.styles';
|
import styles from './dialog.styles';
|
||||||
|
import '~/components/icon-button/icon-button';
|
||||||
import '../icon-button/icon-button';
|
import { animateTo, stopAnimations } from '~/internal/animate';
|
||||||
|
import { emit, waitForEvent } from '~/internal/event';
|
||||||
|
import Modal from '~/internal/modal';
|
||||||
|
import { lockBodyScrolling, unlockBodyScrolling } from '~/internal/scroll';
|
||||||
|
import { HasSlotController } from '~/internal/slot';
|
||||||
|
import { isPreventScrollSupported } from '~/internal/support';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
import { setDefaultAnimation, getAnimation } from '~/utilities/animation-registry';
|
||||||
|
|
||||||
const hasPreventScroll = isPreventScrollSupported();
|
const hasPreventScroll = isPreventScrollSupported();
|
||||||
|
|
||||||
|
@ -65,7 +63,7 @@ export default class SlDialog extends LitElement {
|
||||||
@query('.dialog__panel') panel: HTMLElement;
|
@query('.dialog__panel') panel: HTMLElement;
|
||||||
@query('.dialog__overlay') overlay: HTMLElement;
|
@query('.dialog__overlay') overlay: HTMLElement;
|
||||||
|
|
||||||
private hasSlotController = new HasSlotController(this, 'footer');
|
private readonly hasSlotController = new HasSlotController(this, 'footer');
|
||||||
private modal: Modal;
|
private modal: Modal;
|
||||||
private originalTrigger: HTMLElement | null;
|
private originalTrigger: HTMLElement | null;
|
||||||
|
|
||||||
|
@ -106,7 +104,7 @@ export default class SlDialog extends LitElement {
|
||||||
/** Shows the dialog. */
|
/** Shows the dialog. */
|
||||||
async show() {
|
async show() {
|
||||||
if (this.open) {
|
if (this.open) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.open = true;
|
this.open = true;
|
||||||
|
@ -116,7 +114,7 @@ export default class SlDialog extends LitElement {
|
||||||
/** Hides the dialog */
|
/** Hides the dialog */
|
||||||
async hide() {
|
async hide() {
|
||||||
if (!this.open) {
|
if (!this.open) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.open = false;
|
this.open = false;
|
||||||
|
@ -127,11 +125,11 @@ export default class SlDialog extends LitElement {
|
||||||
const slRequestClose = emit(this, 'sl-request-close', { cancelable: true });
|
const slRequestClose = emit(this, 'sl-request-close', { cancelable: true });
|
||||||
if (slRequestClose.defaultPrevented) {
|
if (slRequestClose.defaultPrevented) {
|
||||||
const animation = getAnimation(this, 'dialog.denyClose');
|
const animation = getAnimation(this, 'dialog.denyClose');
|
||||||
animateTo(this.panel, animation.keyframes, animation.options);
|
void animateTo(this.panel, animation.keyframes, animation.options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hide();
|
void this.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown(event: KeyboardEvent) {
|
handleKeyDown(event: KeyboardEvent) {
|
||||||
|
@ -197,8 +195,10 @@ export default class SlDialog extends LitElement {
|
||||||
|
|
||||||
// Restore focus to the original trigger
|
// Restore focus to the original trigger
|
||||||
const trigger = this.originalTrigger;
|
const trigger = this.originalTrigger;
|
||||||
if (trigger && typeof trigger.focus === 'function') {
|
if (typeof trigger?.focus === 'function') {
|
||||||
setTimeout(() => trigger.focus());
|
setTimeout(() => {
|
||||||
|
trigger.focus();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(this, 'sl-after-hide');
|
emit(this, 'sl-after-hide');
|
||||||
|
@ -216,7 +216,13 @@ export default class SlDialog extends LitElement {
|
||||||
})}
|
})}
|
||||||
@keydown=${this.handleKeyDown}
|
@keydown=${this.handleKeyDown}
|
||||||
>
|
>
|
||||||
<div part="overlay" class="dialog__overlay" @click=${this.requestClose} tabindex="-1"></div>
|
<div
|
||||||
|
part="overlay"
|
||||||
|
class="dialog__overlay"
|
||||||
|
@click=${this.requestClose}
|
||||||
|
@keydown=${this.handleKeyDown}
|
||||||
|
tabindex="-1"
|
||||||
|
></div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
part="panel"
|
part="panel"
|
||||||
|
@ -232,7 +238,7 @@ export default class SlDialog extends LitElement {
|
||||||
? html`
|
? html`
|
||||||
<header part="header" class="dialog__header">
|
<header part="header" class="dialog__header">
|
||||||
<span part="title" class="dialog__title" id="title">
|
<span part="title" class="dialog__title" id="title">
|
||||||
<slot name="label"> ${this.label || String.fromCharCode(65279)} </slot>
|
<slot name="label"> ${this.label.length > 0 ? this.label : String.fromCharCode(65279)} </slot>
|
||||||
</span>
|
</span>
|
||||||
<sl-icon-button
|
<sl-icon-button
|
||||||
exportparts="base:close-button"
|
exportparts="base:close-button"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { LitElement } from 'lit';
|
import { LitElement } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import styles from './divider.styles';
|
import styles from './divider.styles';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
// cspell:dictionaries lorem-ipsum
|
// cspell:dictionaries lorem-ipsum
|
||||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlDrawer from './drawer';
|
import type SlDrawer from './drawer';
|
||||||
|
|
||||||
describe('<sl-drawer>', () => {
|
describe('<sl-drawer>', () => {
|
||||||
|
@ -10,7 +8,7 @@ describe('<sl-drawer>', () => {
|
||||||
const el = await fixture<SlDrawer>(html`
|
const el = await fixture<SlDrawer>(html`
|
||||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
|
|
||||||
expect(base.hidden).to.be.false;
|
expect(base.hidden).to.be.false;
|
||||||
});
|
});
|
||||||
|
@ -19,7 +17,7 @@ describe('<sl-drawer>', () => {
|
||||||
const el = await fixture<SlDrawer>(
|
const el = await fixture<SlDrawer>(
|
||||||
html` <sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> `
|
html` <sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> `
|
||||||
);
|
);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
|
|
||||||
expect(base.hidden).to.be.true;
|
expect(base.hidden).to.be.true;
|
||||||
});
|
});
|
||||||
|
@ -28,13 +26,13 @@ describe('<sl-drawer>', () => {
|
||||||
const el = await fixture<SlDrawer>(html`
|
const el = await fixture<SlDrawer>(html`
|
||||||
<sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
<sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
const showHandler = sinon.spy();
|
const showHandler = sinon.spy();
|
||||||
const afterShowHandler = sinon.spy();
|
const afterShowHandler = sinon.spy();
|
||||||
|
|
||||||
el.addEventListener('sl-show', showHandler);
|
el.addEventListener('sl-show', showHandler);
|
||||||
el.addEventListener('sl-after-show', afterShowHandler);
|
el.addEventListener('sl-after-show', afterShowHandler);
|
||||||
el.show();
|
void el.show();
|
||||||
|
|
||||||
await waitUntil(() => showHandler.calledOnce);
|
await waitUntil(() => showHandler.calledOnce);
|
||||||
await waitUntil(() => afterShowHandler.calledOnce);
|
await waitUntil(() => afterShowHandler.calledOnce);
|
||||||
|
@ -48,13 +46,13 @@ describe('<sl-drawer>', () => {
|
||||||
const el = await fixture<SlDrawer>(html`
|
const el = await fixture<SlDrawer>(html`
|
||||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
const hideHandler = sinon.spy();
|
const hideHandler = sinon.spy();
|
||||||
const afterHideHandler = sinon.spy();
|
const afterHideHandler = sinon.spy();
|
||||||
|
|
||||||
el.addEventListener('sl-hide', hideHandler);
|
el.addEventListener('sl-hide', hideHandler);
|
||||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||||
el.hide();
|
void el.hide();
|
||||||
|
|
||||||
await waitUntil(() => hideHandler.calledOnce);
|
await waitUntil(() => hideHandler.calledOnce);
|
||||||
await waitUntil(() => afterHideHandler.calledOnce);
|
await waitUntil(() => afterHideHandler.calledOnce);
|
||||||
|
@ -68,7 +66,7 @@ describe('<sl-drawer>', () => {
|
||||||
const el = await fixture<SlDrawer>(html`
|
const el = await fixture<SlDrawer>(html`
|
||||||
<sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
<sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
const showHandler = sinon.spy();
|
const showHandler = sinon.spy();
|
||||||
const afterShowHandler = sinon.spy();
|
const afterShowHandler = sinon.spy();
|
||||||
|
|
||||||
|
@ -88,7 +86,7 @@ describe('<sl-drawer>', () => {
|
||||||
const el = await fixture<SlDrawer>(html`
|
const el = await fixture<SlDrawer>(html`
|
||||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||||
`);
|
`);
|
||||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||||
const hideHandler = sinon.spy();
|
const hideHandler = sinon.spy();
|
||||||
const afterHideHandler = sinon.spy();
|
const afterHideHandler = sinon.spy();
|
||||||
|
|
||||||
|
@ -108,9 +106,11 @@ describe('<sl-drawer>', () => {
|
||||||
const el = await fixture<SlDrawer>(html`
|
const el = await fixture<SlDrawer>(html`
|
||||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||||
`);
|
`);
|
||||||
const overlay = el.shadowRoot?.querySelector('[part="overlay"]') as HTMLElement;
|
const overlay = el.shadowRoot!.querySelector<HTMLElement>('[part="overlay"]')!;
|
||||||
|
|
||||||
el.addEventListener('sl-request-close', event => event.preventDefault());
|
el.addEventListener('sl-request-close', event => {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
overlay.click();
|
overlay.click();
|
||||||
|
|
||||||
expect(el.open).to.be.true;
|
expect(el.open).to.be.true;
|
||||||
|
@ -118,14 +118,14 @@ describe('<sl-drawer>', () => {
|
||||||
|
|
||||||
it('should allow initial focus to be set', async () => {
|
it('should allow initial focus to be set', async () => {
|
||||||
const el = await fixture<SlDrawer>(html` <sl-drawer><input /></sl-drawer> `);
|
const el = await fixture<SlDrawer>(html` <sl-drawer><input /></sl-drawer> `);
|
||||||
const input = el.querySelector('input');
|
const input = el.querySelector<HTMLInputElement>('input')!;
|
||||||
const initialFocusHandler = sinon.spy(event => {
|
const initialFocusHandler = sinon.spy((event: InputEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
input.focus();
|
input.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('sl-initial-focus', initialFocusHandler);
|
el.addEventListener('sl-initial-focus', initialFocusHandler);
|
||||||
el.show();
|
void el.show();
|
||||||
|
|
||||||
await waitUntil(() => initialFocusHandler.calledOnce);
|
await waitUntil(() => initialFocusHandler.calledOnce);
|
||||||
|
|
||||||
|
|
|
@ -2,19 +2,17 @@ import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, query } from 'lit/decorators.js';
|
import { customElement, property, query } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import { waitForEvent } from '../../internal/event';
|
|
||||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
|
|
||||||
import { HasSlotController } from '../../internal/slot';
|
|
||||||
import { uppercaseFirstLetter } from '../../internal/string';
|
|
||||||
import { isPreventScrollSupported } from '../../internal/support';
|
|
||||||
import Modal from '../../internal/modal';
|
|
||||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
|
||||||
import styles from './drawer.styles';
|
import styles from './drawer.styles';
|
||||||
|
import '~/components/icon-button/icon-button';
|
||||||
import '../icon-button/icon-button';
|
import { animateTo, stopAnimations } from '~/internal/animate';
|
||||||
|
import { emit, waitForEvent } from '~/internal/event';
|
||||||
|
import Modal from '~/internal/modal';
|
||||||
|
import { lockBodyScrolling, unlockBodyScrolling } from '~/internal/scroll';
|
||||||
|
import { HasSlotController } from '~/internal/slot';
|
||||||
|
import { uppercaseFirstLetter } from '~/internal/string';
|
||||||
|
import { isPreventScrollSupported } from '~/internal/support';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
import { setDefaultAnimation, getAnimation } from '~/utilities/animation-registry';
|
||||||
|
|
||||||
const hasPreventScroll = isPreventScrollSupported();
|
const hasPreventScroll = isPreventScrollSupported();
|
||||||
|
|
||||||
|
@ -73,7 +71,7 @@ export default class SlDrawer extends LitElement {
|
||||||
@query('.drawer__panel') panel: HTMLElement;
|
@query('.drawer__panel') panel: HTMLElement;
|
||||||
@query('.drawer__overlay') overlay: HTMLElement;
|
@query('.drawer__overlay') overlay: HTMLElement;
|
||||||
|
|
||||||
private hasSlotController = new HasSlotController(this, 'footer');
|
private readonly hasSlotController = new HasSlotController(this, 'footer');
|
||||||
private modal: Modal;
|
private modal: Modal;
|
||||||
private originalTrigger: HTMLElement | null;
|
private originalTrigger: HTMLElement | null;
|
||||||
|
|
||||||
|
@ -123,7 +121,7 @@ export default class SlDrawer extends LitElement {
|
||||||
/** Shows the drawer. */
|
/** Shows the drawer. */
|
||||||
async show() {
|
async show() {
|
||||||
if (this.open) {
|
if (this.open) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.open = true;
|
this.open = true;
|
||||||
|
@ -133,7 +131,7 @@ export default class SlDrawer extends LitElement {
|
||||||
/** Hides the drawer */
|
/** Hides the drawer */
|
||||||
async hide() {
|
async hide() {
|
||||||
if (!this.open) {
|
if (!this.open) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.open = false;
|
this.open = false;
|
||||||
|
@ -144,11 +142,11 @@ export default class SlDrawer extends LitElement {
|
||||||
const slRequestClose = emit(this, 'sl-request-close', { cancelable: true });
|
const slRequestClose = emit(this, 'sl-request-close', { cancelable: true });
|
||||||
if (slRequestClose.defaultPrevented) {
|
if (slRequestClose.defaultPrevented) {
|
||||||
const animation = getAnimation(this, 'drawer.denyClose');
|
const animation = getAnimation(this, 'drawer.denyClose');
|
||||||
animateTo(this.panel, animation.keyframes, animation.options);
|
void animateTo(this.panel, animation.keyframes, animation.options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hide();
|
void this.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown(event: KeyboardEvent) {
|
handleKeyDown(event: KeyboardEvent) {
|
||||||
|
@ -217,8 +215,10 @@ export default class SlDrawer extends LitElement {
|
||||||
|
|
||||||
// Restore focus to the original trigger
|
// Restore focus to the original trigger
|
||||||
const trigger = this.originalTrigger;
|
const trigger = this.originalTrigger;
|
||||||
if (trigger && typeof trigger.focus === 'function') {
|
if (typeof trigger?.focus === 'function') {
|
||||||
setTimeout(() => trigger.focus());
|
setTimeout(() => {
|
||||||
|
trigger.focus();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(this, 'sl-after-hide');
|
emit(this, 'sl-after-hide');
|
||||||
|
@ -242,7 +242,13 @@ export default class SlDrawer extends LitElement {
|
||||||
})}
|
})}
|
||||||
@keydown=${this.handleKeyDown}
|
@keydown=${this.handleKeyDown}
|
||||||
>
|
>
|
||||||
<div part="overlay" class="drawer__overlay" @click=${this.requestClose} tabindex="-1"></div>
|
<div
|
||||||
|
part="overlay"
|
||||||
|
class="drawer__overlay"
|
||||||
|
@click=${this.requestClose}
|
||||||
|
@keydown=${this.handleKeyDown}
|
||||||
|
tabindex="-1"
|
||||||
|
></div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
part="panel"
|
part="panel"
|
||||||
|
@ -259,7 +265,7 @@ export default class SlDrawer extends LitElement {
|
||||||
<header part="header" class="drawer__header">
|
<header part="header" class="drawer__header">
|
||||||
<span part="title" class="drawer__title" id="title">
|
<span part="title" class="drawer__title" id="title">
|
||||||
<!-- If there's no label, use an invisible character to prevent the header from collapsing -->
|
<!-- If there's no label, use an invisible character to prevent the header from collapsing -->
|
||||||
<slot name="label"> ${this.label || String.fromCharCode(65279)} </slot>
|
<slot name="label"> ${this.label.length > 0 ? this.label : String.fromCharCode(65279)} </slot>
|
||||||
</span>
|
</span>
|
||||||
<sl-icon-button
|
<sl-icon-button
|
||||||
exportparts="base:close-button"
|
exportparts="base:close-button"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlDropdown from './dropdown';
|
import type SlDropdown from './dropdown';
|
||||||
|
|
||||||
describe('<sl-dropdown>', () => {
|
describe('<sl-dropdown>', () => {
|
||||||
|
@ -16,7 +14,7 @@ describe('<sl-dropdown>', () => {
|
||||||
</sl-menu>
|
</sl-menu>
|
||||||
</sl-dropdown>
|
</sl-dropdown>
|
||||||
`);
|
`);
|
||||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||||
|
|
||||||
expect(panel.hidden).to.be.false;
|
expect(panel.hidden).to.be.false;
|
||||||
});
|
});
|
||||||
|
@ -32,7 +30,7 @@ describe('<sl-dropdown>', () => {
|
||||||
</sl-menu>
|
</sl-menu>
|
||||||
</sl-dropdown>
|
</sl-dropdown>
|
||||||
`);
|
`);
|
||||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||||
|
|
||||||
expect(panel.hidden).to.be.true;
|
expect(panel.hidden).to.be.true;
|
||||||
});
|
});
|
||||||
|
@ -48,13 +46,13 @@ describe('<sl-dropdown>', () => {
|
||||||
</sl-menu>
|
</sl-menu>
|
||||||
</sl-dropdown>
|
</sl-dropdown>
|
||||||
`);
|
`);
|
||||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||||
const showHandler = sinon.spy();
|
const showHandler = sinon.spy();
|
||||||
const afterShowHandler = sinon.spy();
|
const afterShowHandler = sinon.spy();
|
||||||
|
|
||||||
el.addEventListener('sl-show', showHandler);
|
el.addEventListener('sl-show', showHandler);
|
||||||
el.addEventListener('sl-after-show', afterShowHandler);
|
el.addEventListener('sl-after-show', afterShowHandler);
|
||||||
el.show();
|
void el.show();
|
||||||
|
|
||||||
await waitUntil(() => showHandler.calledOnce);
|
await waitUntil(() => showHandler.calledOnce);
|
||||||
await waitUntil(() => afterShowHandler.calledOnce);
|
await waitUntil(() => afterShowHandler.calledOnce);
|
||||||
|
@ -75,13 +73,13 @@ describe('<sl-dropdown>', () => {
|
||||||
</sl-menu>
|
</sl-menu>
|
||||||
</sl-dropdown>
|
</sl-dropdown>
|
||||||
`);
|
`);
|
||||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||||
const hideHandler = sinon.spy();
|
const hideHandler = sinon.spy();
|
||||||
const afterHideHandler = sinon.spy();
|
const afterHideHandler = sinon.spy();
|
||||||
|
|
||||||
el.addEventListener('sl-hide', hideHandler);
|
el.addEventListener('sl-hide', hideHandler);
|
||||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||||
el.hide();
|
void el.hide();
|
||||||
|
|
||||||
await waitUntil(() => hideHandler.calledOnce);
|
await waitUntil(() => hideHandler.calledOnce);
|
||||||
await waitUntil(() => afterHideHandler.calledOnce);
|
await waitUntil(() => afterHideHandler.calledOnce);
|
||||||
|
@ -102,7 +100,7 @@ describe('<sl-dropdown>', () => {
|
||||||
</sl-menu>
|
</sl-menu>
|
||||||
</sl-dropdown>
|
</sl-dropdown>
|
||||||
`);
|
`);
|
||||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||||
const showHandler = sinon.spy();
|
const showHandler = sinon.spy();
|
||||||
const afterShowHandler = sinon.spy();
|
const afterShowHandler = sinon.spy();
|
||||||
|
|
||||||
|
@ -129,7 +127,7 @@ describe('<sl-dropdown>', () => {
|
||||||
</sl-menu>
|
</sl-menu>
|
||||||
</sl-dropdown>
|
</sl-dropdown>
|
||||||
`);
|
`);
|
||||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||||
const hideHandler = sinon.spy();
|
const hideHandler = sinon.spy();
|
||||||
const afterHideHandler = sinon.spy();
|
const afterHideHandler = sinon.spy();
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
|
import type { Instance as PopperInstance } from '@popperjs/core/dist/esm';
|
||||||
|
import { createPopper } from '@popperjs/core/dist/esm';
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, query } from 'lit/decorators.js';
|
import { customElement, property, query } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { Instance as PopperInstance, createPopper } from '@popperjs/core/dist/esm';
|
|
||||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import { waitForEvent } from '../../internal/event';
|
|
||||||
import { scrollIntoView } from '../../internal/scroll';
|
|
||||||
import { getTabbableBoundary } from '../../internal/tabbable';
|
|
||||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
|
||||||
import type SlMenu from '../menu/menu';
|
|
||||||
import type SlMenuItem from '../menu-item/menu-item';
|
|
||||||
import styles from './dropdown.styles';
|
import styles from './dropdown.styles';
|
||||||
|
import type SlMenuItem from '~/components/menu-item/menu-item';
|
||||||
|
import type SlMenu from '~/components/menu/menu';
|
||||||
|
import { animateTo, stopAnimations } from '~/internal/animate';
|
||||||
|
import { emit, waitForEvent } from '~/internal/event';
|
||||||
|
import { scrollIntoView } from '~/internal/scroll';
|
||||||
|
import { getTabbableBoundary } from '~/internal/tabbable';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
import { setDefaultAnimation, getAnimation } from '~/utilities/animation-registry';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -40,7 +40,7 @@ export default class SlDropdown extends LitElement {
|
||||||
@query('.dropdown__panel') panel: HTMLElement;
|
@query('.dropdown__panel') panel: HTMLElement;
|
||||||
@query('.dropdown__positioner') positioner: HTMLElement;
|
@query('.dropdown__positioner') positioner: HTMLElement;
|
||||||
|
|
||||||
private popover: PopperInstance;
|
private popover?: PopperInstance;
|
||||||
|
|
||||||
/** Indicates whether or not the dropdown is open. You can use this in lieu of the show/hide methods. */
|
/** Indicates whether or not the dropdown is open. You can use this in lieu of the show/hide methods. */
|
||||||
@property({ type: Boolean, reflect: true }) open = false;
|
@property({ type: Boolean, reflect: true }) open = false;
|
||||||
|
@ -73,7 +73,7 @@ export default class SlDropdown extends LitElement {
|
||||||
@property({ attribute: 'stay-open-on-select', type: Boolean, reflect: true }) stayOpenOnSelect = false;
|
@property({ attribute: 'stay-open-on-select', type: Boolean, reflect: true }) stayOpenOnSelect = false;
|
||||||
|
|
||||||
/** The dropdown will close when the user interacts outside of this element (e.g. clicking). */
|
/** The dropdown will close when the user interacts outside of this element (e.g. clicking). */
|
||||||
@property({ attribute: false }) containingElement: HTMLElement;
|
@property({ attribute: false }) containingElement?: HTMLElement;
|
||||||
|
|
||||||
/** The distance in pixels from which to offset the panel away from its trigger. */
|
/** The distance in pixels from which to offset the panel away from its trigger. */
|
||||||
@property({ type: Number }) distance = 0;
|
@property({ type: Number }) distance = 0;
|
||||||
|
@ -94,12 +94,12 @@ export default class SlDropdown extends LitElement {
|
||||||
this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this);
|
this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this);
|
||||||
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
|
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
|
||||||
|
|
||||||
if (!this.containingElement) {
|
if (typeof this.containingElement === 'undefined') {
|
||||||
this.containingElement = this;
|
this.containingElement = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the popover after render
|
// Create the popover after render
|
||||||
this.updateComplete.then(() => {
|
void this.updateComplete.then(() => {
|
||||||
this.popover = createPopper(this.trigger, this.positioner, {
|
this.popover = createPopper(this.trigger, this.positioner, {
|
||||||
placement: this.placement,
|
placement: this.placement,
|
||||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||||
|
@ -127,30 +127,30 @@ export default class SlDropdown extends LitElement {
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.hide();
|
void void this.hide();
|
||||||
|
|
||||||
if (this.popover) {
|
this.popover?.destroy();
|
||||||
this.popover.destroy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
focusOnTrigger() {
|
focusOnTrigger() {
|
||||||
const slot = this.trigger.querySelector('slot')!;
|
const slot = this.trigger.querySelector('slot')!;
|
||||||
const trigger = slot.assignedElements({ flatten: true })[0] as any;
|
const trigger = slot.assignedElements({ flatten: true })[0] as HTMLElement | undefined;
|
||||||
if (trigger && typeof trigger.focus === 'function') {
|
if (typeof trigger?.focus === 'function') {
|
||||||
trigger.focus();
|
trigger.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getMenu() {
|
getMenu() {
|
||||||
const slot = this.panel.querySelector('slot')!;
|
const slot = this.panel.querySelector('slot')!;
|
||||||
return slot.assignedElements({ flatten: true }).filter(el => el.tagName.toLowerCase() === 'sl-menu')[0] as SlMenu;
|
return slot.assignedElements({ flatten: true }).find(el => el.tagName.toLowerCase() === 'sl-menu') as
|
||||||
|
| SlMenu
|
||||||
|
| undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDocumentKeyDown(event: KeyboardEvent) {
|
handleDocumentKeyDown(event: KeyboardEvent) {
|
||||||
// Close when escape is pressed
|
// Close when escape is pressed
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
this.hide();
|
void this.hide();
|
||||||
this.focusOnTrigger();
|
this.focusOnTrigger();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,7 @@ export default class SlDropdown extends LitElement {
|
||||||
// Tabbing within an open menu should close the dropdown and refocus the trigger
|
// Tabbing within an open menu should close the dropdown and refocus the trigger
|
||||||
if (this.open && document.activeElement?.tagName.toLowerCase() === 'sl-menu-item') {
|
if (this.open && document.activeElement?.tagName.toLowerCase() === 'sl-menu-item') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.hide();
|
void this.hide();
|
||||||
this.focusOnTrigger();
|
this.focusOnTrigger();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -171,13 +171,15 @@ export default class SlDropdown extends LitElement {
|
||||||
// otherwise `document.activeElement` will only return the name of the parent shadow DOM element.
|
// otherwise `document.activeElement` will only return the name of the parent shadow DOM element.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const activeElement =
|
const activeElement =
|
||||||
this.containingElement.getRootNode() instanceof ShadowRoot
|
this.containingElement?.getRootNode() instanceof ShadowRoot
|
||||||
? document.activeElement?.shadowRoot?.activeElement
|
? document.activeElement?.shadowRoot?.activeElement
|
||||||
: document.activeElement;
|
: document.activeElement;
|
||||||
|
|
||||||
if (activeElement?.closest(this.containingElement.tagName.toLowerCase()) !== this.containingElement) {
|
if (
|
||||||
this.hide();
|
typeof this.containingElement === 'undefined' ||
|
||||||
return;
|
activeElement?.closest(this.containingElement.tagName.toLowerCase()) !== this.containingElement
|
||||||
|
) {
|
||||||
|
void this.hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -185,10 +187,9 @@ export default class SlDropdown extends LitElement {
|
||||||
|
|
||||||
handleDocumentMouseDown(event: MouseEvent) {
|
handleDocumentMouseDown(event: MouseEvent) {
|
||||||
// Close when clicking outside of the containing element
|
// Close when clicking outside of the containing element
|
||||||
const path = event.composedPath() as Array<EventTarget>;
|
const path = event.composedPath();
|
||||||
if (!path.includes(this.containingElement)) {
|
if (typeof this.containingElement !== 'undefined' && !path.includes(this.containingElement)) {
|
||||||
this.hide();
|
void this.hide();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +203,7 @@ export default class SlDropdown extends LitElement {
|
||||||
|
|
||||||
// Hide the dropdown when a menu item is selected
|
// Hide the dropdown when a menu item is selected
|
||||||
if (!this.stayOpenOnSelect && target.tagName.toLowerCase() === 'sl-menu') {
|
if (!this.stayOpenOnSelect && target.tagName.toLowerCase() === 'sl-menu') {
|
||||||
this.hide();
|
void this.hide();
|
||||||
this.focusOnTrigger();
|
this.focusOnTrigger();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -212,42 +213,44 @@ export default class SlDropdown extends LitElement {
|
||||||
@watch('placement')
|
@watch('placement')
|
||||||
@watch('skidding')
|
@watch('skidding')
|
||||||
handlePopoverOptionsChange() {
|
handlePopoverOptionsChange() {
|
||||||
if (this.popover) {
|
void this.popover?.setOptions({
|
||||||
this.popover.setOptions({
|
placement: this.placement,
|
||||||
placement: this.placement,
|
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
modifiers: [
|
||||||
modifiers: [
|
{
|
||||||
{
|
name: 'flip',
|
||||||
name: 'flip',
|
options: {
|
||||||
options: {
|
boundary: 'viewport'
|
||||||
boundary: 'viewport'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'offset',
|
|
||||||
options: {
|
|
||||||
offset: [this.skidding, this.distance]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
});
|
{
|
||||||
}
|
name: 'offset',
|
||||||
|
options: {
|
||||||
|
offset: [this.skidding, this.distance]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTriggerClick() {
|
handleTriggerClick() {
|
||||||
this.open ? this.hide() : this.show();
|
if (this.open) {
|
||||||
|
void this.hide();
|
||||||
|
} else {
|
||||||
|
void this.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTriggerKeyDown(event: KeyboardEvent) {
|
handleTriggerKeyDown(event: KeyboardEvent) {
|
||||||
const menu = this.getMenu();
|
const menu = this.getMenu();
|
||||||
const menuItems = menu ? ([...menu.querySelectorAll('sl-menu-item')] as SlMenuItem[]) : [];
|
const menuItems = typeof menu !== 'undefined' ? ([...menu.querySelectorAll('sl-menu-item')] as SlMenuItem[]) : [];
|
||||||
const firstMenuItem = menuItems[0];
|
const firstMenuItem = menuItems[0];
|
||||||
const lastMenuItem = menuItems[menuItems.length - 1];
|
const lastMenuItem = menuItems[menuItems.length - 1];
|
||||||
|
|
||||||
// Close when escape or tab is pressed
|
// Close when escape or tab is pressed
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
this.focusOnTrigger();
|
this.focusOnTrigger();
|
||||||
this.hide();
|
void this.hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,7 +258,7 @@ export default class SlDropdown extends LitElement {
|
||||||
// key again to hide the menu in case they don't want to make a selection.
|
// key again to hide the menu in case they don't want to make a selection.
|
||||||
if ([' ', 'Enter'].includes(event.key)) {
|
if ([' ', 'Enter'].includes(event.key)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.open ? this.hide() : this.show();
|
this.handleTriggerClick();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,19 +270,18 @@ export default class SlDropdown extends LitElement {
|
||||||
|
|
||||||
// Show the menu if it's not already open
|
// Show the menu if it's not already open
|
||||||
if (!this.open) {
|
if (!this.open) {
|
||||||
this.show();
|
void this.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus on a menu item
|
// Focus on a menu item
|
||||||
if (event.key === 'ArrowDown' && firstMenuItem) {
|
if (event.key === 'ArrowDown' && typeof firstMenuItem !== 'undefined') {
|
||||||
const menu = this.getMenu();
|
menu!.setCurrentItem(firstMenuItem);
|
||||||
menu.setCurrentItem(firstMenuItem);
|
|
||||||
firstMenuItem.focus();
|
firstMenuItem.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'ArrowUp' && lastMenuItem) {
|
if (event.key === 'ArrowUp' && typeof lastMenuItem !== 'undefined') {
|
||||||
menu.setCurrentItem(lastMenuItem);
|
menu!.setCurrentItem(lastMenuItem);
|
||||||
lastMenuItem.focus();
|
lastMenuItem.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -287,9 +289,8 @@ export default class SlDropdown extends LitElement {
|
||||||
|
|
||||||
// Other keys bring focus to the menu and initiate type-to-select behavior
|
// Other keys bring focus to the menu and initiate type-to-select behavior
|
||||||
const ignoredKeys = ['Tab', 'Shift', 'Meta', 'Ctrl', 'Alt'];
|
const ignoredKeys = ['Tab', 'Shift', 'Meta', 'Ctrl', 'Alt'];
|
||||||
if (this.open && menu && !ignoredKeys.includes(event.key)) {
|
if (this.open && !ignoredKeys.includes(event.key)) {
|
||||||
menu.typeToSelect(event.key);
|
menu?.typeToSelect(event.key);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,22 +316,20 @@ export default class SlDropdown extends LitElement {
|
||||||
// To determine this, we assume the first tabbable element in the trigger slot is the "accessible trigger."
|
// To determine this, we assume the first tabbable element in the trigger slot is the "accessible trigger."
|
||||||
//
|
//
|
||||||
updateAccessibleTrigger() {
|
updateAccessibleTrigger() {
|
||||||
if (this.trigger) {
|
const slot = this.trigger.querySelector('slot')!;
|
||||||
const slot = this.trigger.querySelector('slot') as HTMLSlotElement;
|
const assignedElements = slot.assignedElements({ flatten: true }) as HTMLElement[];
|
||||||
const assignedElements = slot.assignedElements({ flatten: true }) as HTMLElement[];
|
const accessibleTrigger = assignedElements.find(el => getTabbableBoundary(el).start);
|
||||||
const accessibleTrigger = assignedElements.find(el => getTabbableBoundary(el).start);
|
|
||||||
|
|
||||||
if (accessibleTrigger) {
|
if (typeof accessibleTrigger !== 'undefined') {
|
||||||
accessibleTrigger.setAttribute('aria-haspopup', 'true');
|
accessibleTrigger.setAttribute('aria-haspopup', 'true');
|
||||||
accessibleTrigger.setAttribute('aria-expanded', this.open ? 'true' : 'false');
|
accessibleTrigger.setAttribute('aria-expanded', this.open ? 'true' : 'false');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shows the dropdown panel. */
|
/** Shows the dropdown panel. */
|
||||||
async show() {
|
async show() {
|
||||||
if (this.open) {
|
if (this.open) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.open = true;
|
this.open = true;
|
||||||
|
@ -340,7 +339,7 @@ export default class SlDropdown extends LitElement {
|
||||||
/** Hides the dropdown panel */
|
/** Hides the dropdown panel */
|
||||||
async hide() {
|
async hide() {
|
||||||
if (!this.open) {
|
if (!this.open) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.open = false;
|
this.open = false;
|
||||||
|
@ -356,7 +355,7 @@ export default class SlDropdown extends LitElement {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.popover.update();
|
void this.popover?.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@watch('open', { waitUntilFirstUpdate: true })
|
@watch('open', { waitUntilFirstUpdate: true })
|
||||||
|
@ -376,7 +375,7 @@ export default class SlDropdown extends LitElement {
|
||||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||||
|
|
||||||
await stopAnimations(this);
|
await stopAnimations(this);
|
||||||
this.popover.update();
|
void this.popover?.update();
|
||||||
this.panel.hidden = false;
|
this.panel.hidden = false;
|
||||||
const { keyframes, options } = getAnimation(this, 'dropdown.show');
|
const { keyframes, options } = getAnimation(this, 'dropdown.show');
|
||||||
await animateTo(this.panel, keyframes, options);
|
await animateTo(this.panel, keyframes, options);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { LitElement } from 'lit';
|
import { LitElement } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { formatBytes } from '../../internal/number';
|
import { formatBytes } from '~/internal/number';
|
||||||
import { LocalizeController } from '../../utilities/localize';
|
import { LocalizeController } from '~/utilities/localize';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -9,7 +9,7 @@ import { LocalizeController } from '../../utilities/localize';
|
||||||
*/
|
*/
|
||||||
@customElement('sl-format-bytes')
|
@customElement('sl-format-bytes')
|
||||||
export default class SlFormatBytes extends LitElement {
|
export default class SlFormatBytes extends LitElement {
|
||||||
private localize = new LocalizeController(this);
|
private readonly localize = new LocalizeController(this);
|
||||||
|
|
||||||
/** The number to format in bytes. */
|
/** The number to format in bytes. */
|
||||||
@property({ type: Number }) value = 0;
|
@property({ type: Number }) value = 0;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { LitElement } from 'lit';
|
import { LitElement } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { LocalizeController } from '../../utilities/localize';
|
import { LocalizeController } from '~/utilities/localize';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -8,7 +8,7 @@ import { LocalizeController } from '../../utilities/localize';
|
||||||
*/
|
*/
|
||||||
@customElement('sl-format-date')
|
@customElement('sl-format-date')
|
||||||
export default class SlFormatDate extends LitElement {
|
export default class SlFormatDate extends LitElement {
|
||||||
private localize = new LocalizeController(this);
|
private readonly localize = new LocalizeController(this);
|
||||||
|
|
||||||
/** The date/time to format. If not set, the current date and time will be used. */
|
/** The date/time to format. If not set, the current date and time will be used. */
|
||||||
@property() date: Date | string = new Date();
|
@property() date: Date | string = new Date();
|
||||||
|
@ -55,7 +55,7 @@ export default class SlFormatDate extends LitElement {
|
||||||
|
|
||||||
// Check for an invalid date
|
// Check for an invalid date
|
||||||
if (isNaN(date.getMilliseconds())) {
|
if (isNaN(date.getMilliseconds())) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.localize.date(date, {
|
return this.localize.date(date, {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { LitElement } from 'lit';
|
import { LitElement } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { LocalizeController } from '../../utilities/localize';
|
import { LocalizeController } from '~/utilities/localize';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -8,7 +8,7 @@ import { LocalizeController } from '../../utilities/localize';
|
||||||
*/
|
*/
|
||||||
@customElement('sl-format-number')
|
@customElement('sl-format-number')
|
||||||
export default class SlFormatNumber extends LitElement {
|
export default class SlFormatNumber extends LitElement {
|
||||||
private localize = new LocalizeController(this);
|
private readonly localize = new LocalizeController(this);
|
||||||
|
|
||||||
/** The number to format. */
|
/** The number to format. */
|
||||||
@property({ type: Number }) value = 0;
|
@property({ type: Number }) value = 0;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -3,8 +3,7 @@ import { customElement, property, query } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import styles from './icon-button.styles';
|
import styles from './icon-button.styles';
|
||||||
|
import '~/components/icon/icon';
|
||||||
import '../icon/icon';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -21,22 +20,22 @@ export default class SlIconButton extends LitElement {
|
||||||
@query('button') button: HTMLButtonElement | HTMLLinkElement;
|
@query('button') button: HTMLButtonElement | HTMLLinkElement;
|
||||||
|
|
||||||
/** The name of the icon to draw. */
|
/** The name of the icon to draw. */
|
||||||
@property() name: string;
|
@property() name?: string;
|
||||||
|
|
||||||
/** The name of a registered custom icon library. */
|
/** The name of a registered custom icon library. */
|
||||||
@property() library: string;
|
@property() library?: string;
|
||||||
|
|
||||||
/** An external URL of an SVG file. */
|
/** An external URL of an SVG file. */
|
||||||
@property() src: string;
|
@property() src?: string;
|
||||||
|
|
||||||
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
|
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
|
||||||
@property() href: string;
|
@property() href?: string;
|
||||||
|
|
||||||
/** Tells the browser where to open the link. Only used when `href` is set. */
|
/** Tells the browser where to open the link. Only used when `href` is set. */
|
||||||
@property() target: '_blank' | '_parent' | '_self' | '_top';
|
@property() target?: '_blank' | '_parent' | '_self' | '_top';
|
||||||
|
|
||||||
/** Tells the browser to download the linked file as this filename. Only used when `href` is set. */
|
/** Tells the browser to download the linked file as this filename. Only used when `href` is set. */
|
||||||
@property() download: string;
|
@property() download?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A description that gets read by screen readers and other assistive devices. For optimal accessibility, you should
|
* A description that gets read by screen readers and other assistive devices. For optimal accessibility, you should
|
||||||
|
@ -48,7 +47,7 @@ export default class SlIconButton extends LitElement {
|
||||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const isLink = this.href ? true : false;
|
const isLink = typeof this.href !== 'undefined';
|
||||||
|
|
||||||
const interior = html`
|
const interior = html`
|
||||||
<sl-icon
|
<sl-icon
|
||||||
|
@ -67,7 +66,7 @@ export default class SlIconButton extends LitElement {
|
||||||
href=${ifDefined(this.href)}
|
href=${ifDefined(this.href)}
|
||||||
target=${ifDefined(this.target)}
|
target=${ifDefined(this.target)}
|
||||||
download=${ifDefined(this.download)}
|
download=${ifDefined(this.download)}
|
||||||
rel=${ifDefined(this.target ? 'noreferrer noopener' : undefined)}
|
rel=${ifDefined(typeof this.target !== 'undefined' ? 'noreferrer noopener' : undefined)}
|
||||||
role="button"
|
role="button"
|
||||||
aria-disabled=${this.disabled ? 'true' : 'false'}
|
aria-disabled=${this.disabled ? 'true' : 'false'}
|
||||||
aria-label="${this.label}"
|
aria-label="${this.label}"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -2,11 +2,11 @@ import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
||||||
import { emit } from '../../internal/event';
|
import styles from './icon.styles';
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import { getIconLibrary, watchIcon, unwatchIcon } from './library';
|
import { getIconLibrary, watchIcon, unwatchIcon } from './library';
|
||||||
import { requestIcon } from './request';
|
import { requestIcon } from './request';
|
||||||
import styles from './icon.styles';
|
import { emit } from '~/internal/event';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
|
|
||||||
|
@ -26,13 +26,17 @@ export default class SlIcon extends LitElement {
|
||||||
@state() private svg = '';
|
@state() private svg = '';
|
||||||
|
|
||||||
/** The name of the icon to draw. */
|
/** The name of the icon to draw. */
|
||||||
@property() name: string;
|
@property() name?: string;
|
||||||
|
|
||||||
/** An external URL of an SVG file. */
|
/**
|
||||||
@property() src: string;
|
* An external URL of an SVG file.
|
||||||
|
*
|
||||||
|
* WARNING: Be sure you trust the content you are including as it will be executed as code and can result in XSS attacks.
|
||||||
|
*/
|
||||||
|
@property() src?: string;
|
||||||
|
|
||||||
/** An alternate description to use for accessibility. If omitted, the icon will be ignored by assistive devices. */
|
/** An alternate description to use for accessibility. If omitted, the icon will be ignored by assistive devices. */
|
||||||
@property() label: string;
|
@property() label = '';
|
||||||
|
|
||||||
/** The name of a registered custom icon library. */
|
/** The name of a registered custom icon library. */
|
||||||
@property() library = 'default';
|
@property() library = 'default';
|
||||||
|
@ -43,7 +47,7 @@ export default class SlIcon extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
this.setIcon();
|
void this.setIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
|
@ -51,18 +55,17 @@ export default class SlIcon extends LitElement {
|
||||||
unwatchIcon(this);
|
unwatchIcon(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getUrl(): string {
|
private getUrl() {
|
||||||
const library = getIconLibrary(this.library);
|
const library = getIconLibrary(this.library);
|
||||||
if (this.name && library) {
|
if (typeof this.name !== 'undefined' && typeof library !== 'undefined') {
|
||||||
return library.resolver(this.name);
|
return library.resolver(this.name);
|
||||||
} else {
|
|
||||||
return this.src;
|
|
||||||
}
|
}
|
||||||
|
return this.src;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal Fetches the icon and redraws it. Used to handle library registrations. */
|
/** @internal Fetches the icon and redraws it. Used to handle library registrations. */
|
||||||
redraw() {
|
redraw() {
|
||||||
this.setIcon();
|
void this.setIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
@watch('name')
|
@watch('name')
|
||||||
|
@ -71,7 +74,7 @@ export default class SlIcon extends LitElement {
|
||||||
async setIcon() {
|
async setIcon() {
|
||||||
const library = getIconLibrary(this.library);
|
const library = getIconLibrary(this.library);
|
||||||
const url = this.getUrl();
|
const url = this.getUrl();
|
||||||
if (url) {
|
if (typeof url !== 'undefined' && url.length > 0) {
|
||||||
try {
|
try {
|
||||||
const file = await requestIcon(url)!;
|
const file = await requestIcon(url)!;
|
||||||
if (url !== this.getUrl()) {
|
if (url !== this.getUrl()) {
|
||||||
|
@ -81,10 +84,8 @@ export default class SlIcon extends LitElement {
|
||||||
const doc = parser.parseFromString(file.svg, 'text/html');
|
const doc = parser.parseFromString(file.svg, 'text/html');
|
||||||
const svgEl = doc.body.querySelector('svg');
|
const svgEl = doc.body.querySelector('svg');
|
||||||
|
|
||||||
if (svgEl) {
|
if (svgEl !== null) {
|
||||||
if (library && library.mutator) {
|
library?.mutator?.(svgEl);
|
||||||
library.mutator(svgEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.svg = svgEl.outerHTML;
|
this.svg = svgEl.outerHTML;
|
||||||
emit(this, 'sl-load');
|
emit(this, 'sl-load');
|
||||||
|
@ -99,14 +100,14 @@ export default class SlIcon extends LitElement {
|
||||||
} catch {
|
} catch {
|
||||||
emit(this, 'sl-error', { detail: { status: -1 } });
|
emit(this, 'sl-error', { detail: { status: -1 } });
|
||||||
}
|
}
|
||||||
} else if (this.svg) {
|
} else if (this.svg.length > 0) {
|
||||||
// If we can't resolve a URL and an icon was previously set, remove it
|
// If we can't resolve a URL and an icon was previously set, remove it
|
||||||
this.svg = '';
|
this.svg = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange() {
|
handleChange() {
|
||||||
this.setIcon();
|
void this.setIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getBasePath } from '../../utilities/base-path';
|
|
||||||
import type { IconLibrary } from './library';
|
import type { IconLibrary } from './library';
|
||||||
|
import { getBasePath } from '~/utilities/base-path';
|
||||||
|
|
||||||
const library: IconLibrary = {
|
const library: IconLibrary = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
|
|
|
@ -86,11 +86,10 @@ const icons = {
|
||||||
const systemLibrary: IconLibrary = {
|
const systemLibrary: IconLibrary = {
|
||||||
name: 'system',
|
name: 'system',
|
||||||
resolver: (name: keyof typeof icons) => {
|
resolver: (name: keyof typeof icons) => {
|
||||||
if (icons[name]) {
|
if (name in icons) {
|
||||||
return `data:image/svg+xml,${encodeURIComponent(icons[name])}`;
|
return `data:image/svg+xml,${encodeURIComponent(icons[name])}`;
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import defaultLibrary from './library.default';
|
import defaultLibrary from './library.default';
|
||||||
import systemLibrary from './library.system';
|
import systemLibrary from './library.system';
|
||||||
import type SlIcon from '../icon/icon';
|
import type SlIcon from '~/components/icon/icon';
|
||||||
|
|
||||||
export type IconLibraryResolver = (name: string) => string;
|
export type IconLibraryResolver = (name: string) => string;
|
||||||
export type IconLibraryMutator = (svg: SVGElement) => void;
|
export type IconLibraryMutator = (svg: SVGElement) => void;
|
||||||
|
@ -22,7 +22,7 @@ export function unwatchIcon(icon: SlIcon) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIconLibrary(name?: string) {
|
export function getIconLibrary(name?: string) {
|
||||||
return registry.filter(lib => lib.name === name)[0];
|
return registry.find(lib => lib.name === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerIconLibrary(
|
export function registerIconLibrary(
|
||||||
|
@ -37,7 +37,7 @@ export function registerIconLibrary(
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redraw watched icons
|
// Redraw watched icons
|
||||||
watchedIcons.map(icon => {
|
watchedIcons.forEach(icon => {
|
||||||
if (icon.library === name) {
|
if (icon.library === name) {
|
||||||
icon.redraw();
|
icon.redraw();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,42 @@
|
||||||
interface IconFile {
|
import { requestInclude } from '~/components/include/request';
|
||||||
|
|
||||||
|
type IconFile =
|
||||||
|
| {
|
||||||
|
ok: true;
|
||||||
|
status: number;
|
||||||
|
svg: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
ok: false;
|
||||||
|
status: number;
|
||||||
|
svg: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IconFileUnknown {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
status: number;
|
status: number;
|
||||||
svg: string;
|
svg: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconFiles = new Map<string, Promise<IconFile>>();
|
const iconFiles = new Map<string, IconFile>();
|
||||||
|
|
||||||
export const requestIcon = (url: string) => {
|
export async function requestIcon(url: string): Promise<IconFile> {
|
||||||
if (iconFiles.has(url)) {
|
if (iconFiles.has(url)) {
|
||||||
return iconFiles.get(url);
|
return iconFiles.get(url)!;
|
||||||
} else {
|
|
||||||
const request = fetch(url).then(async response => {
|
|
||||||
if (response.ok) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.innerHTML = await response.text();
|
|
||||||
const svg = div.firstElementChild;
|
|
||||||
|
|
||||||
return {
|
|
||||||
ok: response.ok,
|
|
||||||
status: response.status,
|
|
||||||
svg: svg && svg.tagName.toLowerCase() === 'svg' ? svg.outerHTML : ''
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
ok: response.ok,
|
|
||||||
status: response.status,
|
|
||||||
svg: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}) as Promise<IconFile>;
|
|
||||||
|
|
||||||
iconFiles.set(url, request);
|
|
||||||
return request;
|
|
||||||
}
|
}
|
||||||
};
|
const fileData = await requestInclude(url);
|
||||||
|
const iconFileData: IconFileUnknown = {
|
||||||
|
ok: fileData.ok,
|
||||||
|
status: fileData.status,
|
||||||
|
svg: null
|
||||||
|
};
|
||||||
|
if (fileData.ok) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.innerHTML = fileData.html;
|
||||||
|
const svg = div.firstElementChild;
|
||||||
|
iconFileData.svg = svg?.tagName.toLowerCase() === 'svg' ? svg.outerHTML : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
iconFiles.set(url, iconFileData as IconFile);
|
||||||
|
return iconFileData as IconFile;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, query } from 'lit/decorators.js';
|
import { customElement, property, query } from 'lit/decorators.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
import { clamp } from '../../internal/math';
|
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import styles from './image-comparer.styles';
|
import styles from './image-comparer.styles';
|
||||||
|
import '~/components/icon/icon';
|
||||||
import '../icon/icon';
|
import { autoIncrement } from '~/internal/autoIncrement';
|
||||||
|
import { drag } from '~/internal/drag';
|
||||||
|
import { emit } from '~/internal/event';
|
||||||
|
import { clamp } from '~/internal/math';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -36,42 +37,19 @@ export default class SlImageComparer extends LitElement {
|
||||||
@query('.image-comparer') base: HTMLElement;
|
@query('.image-comparer') base: HTMLElement;
|
||||||
@query('.image-comparer__handle') handle: HTMLElement;
|
@query('.image-comparer__handle') handle: HTMLElement;
|
||||||
|
|
||||||
|
private readonly attrId = autoIncrement();
|
||||||
|
private readonly containerId = `comparer-container-${this.attrId}`;
|
||||||
|
|
||||||
/** The position of the divider as a percentage. */
|
/** The position of the divider as a percentage. */
|
||||||
@property({ type: Number, reflect: true }) position = 50;
|
@property({ type: Number, reflect: true }) position = 50;
|
||||||
|
|
||||||
handleDrag(event: any) {
|
handleDrag(event: Event) {
|
||||||
const { width } = this.base.getBoundingClientRect();
|
const { width } = this.base.getBoundingClientRect();
|
||||||
|
|
||||||
function drag(event: any, container: HTMLElement, onMove: (x: number) => void) {
|
|
||||||
const move = (event: any) => {
|
|
||||||
const { left } = container.getBoundingClientRect();
|
|
||||||
const { pageXOffset } = container.ownerDocument.defaultView!;
|
|
||||||
const offsetX = left + pageXOffset;
|
|
||||||
const x = (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - offsetX;
|
|
||||||
|
|
||||||
onMove(x);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Move on init
|
|
||||||
move(event);
|
|
||||||
|
|
||||||
const stop = () => {
|
|
||||||
document.removeEventListener('mousemove', move);
|
|
||||||
document.removeEventListener('touchmove', move);
|
|
||||||
document.removeEventListener('mouseup', stop);
|
|
||||||
document.removeEventListener('touchend', stop);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', move);
|
|
||||||
document.addEventListener('touchmove', move);
|
|
||||||
document.addEventListener('mouseup', stop);
|
|
||||||
document.addEventListener('touchend', stop);
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
drag(event, this.base, x => {
|
drag(this.base, x => {
|
||||||
this.position = Number(clamp((x / width) * 100, 0, 100).toFixed(2));
|
this.position = parseFloat(clamp((x / width) * 100, 0, 100).toFixed(2));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,10 +60,18 @@ export default class SlImageComparer extends LitElement {
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (event.key === 'ArrowLeft') newPosition = newPosition - incr;
|
if (event.key === 'ArrowLeft') {
|
||||||
if (event.key === 'ArrowRight') newPosition = newPosition + incr;
|
newPosition -= incr;
|
||||||
if (event.key === 'Home') newPosition = 0;
|
}
|
||||||
if (event.key === 'End') newPosition = 100;
|
if (event.key === 'ArrowRight') {
|
||||||
|
newPosition += incr;
|
||||||
|
}
|
||||||
|
if (event.key === 'Home') {
|
||||||
|
newPosition = 0;
|
||||||
|
}
|
||||||
|
if (event.key === 'End') {
|
||||||
|
newPosition = 100;
|
||||||
|
}
|
||||||
newPosition = clamp(newPosition, 0, 100);
|
newPosition = clamp(newPosition, 0, 100);
|
||||||
|
|
||||||
this.position = newPosition;
|
this.position = newPosition;
|
||||||
|
@ -99,7 +85,7 @@ export default class SlImageComparer extends LitElement {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div part="base" class="image-comparer" @keydown=${this.handleKeyDown}>
|
<div part="base" class="image-comparer" @keydown=${this.handleKeyDown} id=${this.containerId}>
|
||||||
<div class="image-comparer__image">
|
<div class="image-comparer__image">
|
||||||
<div part="before" class="image-comparer__before">
|
<div part="before" class="image-comparer__before">
|
||||||
<slot name="before"></slot>
|
<slot name="before"></slot>
|
||||||
|
@ -117,7 +103,7 @@ export default class SlImageComparer extends LitElement {
|
||||||
<div
|
<div
|
||||||
part="divider"
|
part="divider"
|
||||||
class="image-comparer__divider"
|
class="image-comparer__divider"
|
||||||
style=${styleMap({ left: this.position + '%' })}
|
style=${styleMap({ left: `${this.position}%` })}
|
||||||
@mousedown=${this.handleDrag}
|
@mousedown=${this.handleDrag}
|
||||||
@touchstart=${this.handleDrag}
|
@touchstart=${this.handleDrag}
|
||||||
>
|
>
|
||||||
|
@ -128,6 +114,7 @@ export default class SlImageComparer extends LitElement {
|
||||||
aria-valuenow=${this.position}
|
aria-valuenow=${this.position}
|
||||||
aria-valuemin="0"
|
aria-valuemin="0"
|
||||||
aria-valuemax="100"
|
aria-valuemax="100"
|
||||||
|
aria-controls=${this.containerId}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<slot name="handle-icon">
|
<slot name="handle-icon">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlInclude from './include';
|
import type SlInclude from './include';
|
||||||
|
|
||||||
const stubbedFetchResponse: Response = {
|
const stubbedFetchResponse: Response = {
|
||||||
|
@ -22,6 +20,14 @@ const stubbedFetchResponse: Response = {
|
||||||
clone: sinon.fake()
|
clone: sinon.fake()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function delayResolve(resolveValue: string) {
|
||||||
|
return new Promise<string>(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(resolveValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe('<sl-include>', () => {
|
describe('<sl-include>', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sinon.verifyAndRestore();
|
sinon.verifyAndRestore();
|
||||||
|
@ -32,7 +38,7 @@ describe('<sl-include>', () => {
|
||||||
...stubbedFetchResponse,
|
...stubbedFetchResponse,
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
text: () => Promise.resolve('"id": 1')
|
text: () => delayResolve('"id": 1')
|
||||||
});
|
});
|
||||||
const el = await fixture<SlInclude>(html` <sl-include src="/found"></sl-include> `);
|
const el = await fixture<SlInclude>(html` <sl-include src="/found"></sl-include> `);
|
||||||
const loadHandler = sinon.spy();
|
const loadHandler = sinon.spy();
|
||||||
|
@ -49,7 +55,7 @@ describe('<sl-include>', () => {
|
||||||
...stubbedFetchResponse,
|
...stubbedFetchResponse,
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 404,
|
status: 404,
|
||||||
text: () => Promise.resolve('{}')
|
text: () => delayResolve('{}')
|
||||||
});
|
});
|
||||||
const el = await fixture<SlInclude>(html` <sl-include src="/not-found"></sl-include> `);
|
const el = await fixture<SlInclude>(html` <sl-include src="/not-found"></sl-include> `);
|
||||||
const loadHandler = sinon.spy();
|
const loadHandler = sinon.spy();
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import { requestInclude } from './request';
|
|
||||||
import styles from './include.styles';
|
import styles from './include.styles';
|
||||||
|
import { requestInclude } from './request';
|
||||||
|
import { emit } from '~/internal/event';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -16,7 +16,11 @@ import styles from './include.styles';
|
||||||
export default class SlInclude extends LitElement {
|
export default class SlInclude extends LitElement {
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
|
|
||||||
/** The location of the HTML file to include. */
|
/**
|
||||||
|
* The location of the HTML file to include.
|
||||||
|
*
|
||||||
|
* WARNING: Be sure you trust the content you are including as it will be executed as code and can result in XSS attacks.
|
||||||
|
*/
|
||||||
@property() src: string;
|
@property() src: string;
|
||||||
|
|
||||||
/** The fetch mode to use. */
|
/** The fetch mode to use. */
|
||||||
|
@ -31,7 +35,9 @@ export default class SlInclude extends LitElement {
|
||||||
executeScript(script: HTMLScriptElement) {
|
executeScript(script: HTMLScriptElement) {
|
||||||
// Create a copy of the script and swap it out so the browser executes it
|
// Create a copy of the script and swap it out so the browser executes it
|
||||||
const newScript = document.createElement('script');
|
const newScript = document.createElement('script');
|
||||||
[...script.attributes].map(attr => newScript.setAttribute(attr.name, attr.value));
|
[...script.attributes].forEach(attr => {
|
||||||
|
newScript.setAttribute(attr.name, attr.value);
|
||||||
|
});
|
||||||
newScript.textContent = script.textContent;
|
newScript.textContent = script.textContent;
|
||||||
script.parentNode!.replaceChild(newScript, script);
|
script.parentNode!.replaceChild(newScript, script);
|
||||||
}
|
}
|
||||||
|
@ -47,7 +53,7 @@ export default class SlInclude extends LitElement {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file) {
|
if (typeof file === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +65,9 @@ export default class SlInclude extends LitElement {
|
||||||
this.innerHTML = file.html;
|
this.innerHTML = file.html;
|
||||||
|
|
||||||
if (this.allowScripts) {
|
if (this.allowScripts) {
|
||||||
[...this.querySelectorAll('script')].map(script => this.executeScript(script));
|
[...this.querySelectorAll('script')].forEach(script => {
|
||||||
|
this.executeScript(script);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(this, 'sl-load');
|
emit(this, 'sl-load');
|
||||||
|
|
|
@ -6,18 +6,17 @@ interface IncludeFile {
|
||||||
|
|
||||||
const includeFiles = new Map<string, Promise<IncludeFile>>();
|
const includeFiles = new Map<string, Promise<IncludeFile>>();
|
||||||
|
|
||||||
export const requestInclude = async (src: string, mode: 'cors' | 'no-cors' | 'same-origin' = 'cors') => {
|
export function requestInclude(src: string, mode: 'cors' | 'no-cors' | 'same-origin' = 'cors'): Promise<IncludeFile> {
|
||||||
if (includeFiles.has(src)) {
|
if (includeFiles.has(src)) {
|
||||||
return includeFiles.get(src);
|
return includeFiles.get(src)!;
|
||||||
} else {
|
|
||||||
const request = fetch(src, { mode: mode }).then(async response => {
|
|
||||||
return {
|
|
||||||
ok: response.ok,
|
|
||||||
status: response.status,
|
|
||||||
html: await response.text()
|
|
||||||
};
|
|
||||||
});
|
|
||||||
includeFiles.set(src, request);
|
|
||||||
return request;
|
|
||||||
}
|
}
|
||||||
};
|
const fileDataPromise = fetch(src, { mode: mode }).then(async response => {
|
||||||
|
return {
|
||||||
|
ok: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
html: await response.text()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
includeFiles.set(src, fileDataPromise);
|
||||||
|
return fileDataPromise;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
import formControlStyles from '../../styles/form-control.styles';
|
import formControlStyles from '~/styles/form-control.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlInput from './input';
|
import type SlInput from './input';
|
||||||
|
|
||||||
describe('<sl-input>', () => {
|
describe('<sl-input>', () => {
|
||||||
it('should be disabled with the disabled attribute', async () => {
|
it('should be disabled with the disabled attribute', async () => {
|
||||||
const el = await fixture<SlInput>(html` <sl-input disabled></sl-input> `);
|
const el = await fixture<SlInput>(html` <sl-input disabled></sl-input> `);
|
||||||
const input = el.shadowRoot?.querySelector('[part="input"]') as HTMLInputElement;
|
const input = el.shadowRoot!.querySelector<HTMLInputElement>('[part="input"]')!;
|
||||||
|
|
||||||
expect(input.disabled).to.be.true;
|
expect(input.disabled).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { live } from 'lit/directives/live.js';
|
import { live } from 'lit/directives/live.js';
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import { FormSubmitController, getLabelledBy, renderFormControl } from '../../internal/form-control';
|
|
||||||
import { HasSlotController } from '../../internal/slot';
|
|
||||||
import styles from './input.styles';
|
import styles from './input.styles';
|
||||||
|
import '~/components/icon/icon';
|
||||||
import '../icon/icon';
|
import { autoIncrement } from '~/internal/autoIncrement';
|
||||||
|
import { emit } from '~/internal/event';
|
||||||
let id = 0;
|
import { FormSubmitController, getLabelledBy, renderFormControl } from '~/internal/form-control';
|
||||||
|
import { HasSlotController } from '~/internal/slot';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -49,12 +47,13 @@ export default class SlInput extends LitElement {
|
||||||
|
|
||||||
@query('.input__control') input: HTMLInputElement;
|
@query('.input__control') input: HTMLInputElement;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-expect-error -- Controller is currently unused
|
||||||
private formSubmitController = new FormSubmitController(this);
|
private readonly formSubmitController = new FormSubmitController(this);
|
||||||
private hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||||
private inputId = `input-${++id}`;
|
private readonly attrId = autoIncrement();
|
||||||
private helpTextId = `input-help-text-${id}`;
|
private readonly inputId = `input-${this.attrId}`;
|
||||||
private labelId = `input-label-${id}`;
|
private readonly helpTextId = `input-help-text-${this.attrId}`;
|
||||||
|
private readonly labelId = `input-label-${this.attrId}`;
|
||||||
|
|
||||||
@state() private hasFocus = false;
|
@state() private hasFocus = false;
|
||||||
@state() private isPasswordVisible = false;
|
@state() private isPasswordVisible = false;
|
||||||
|
@ -79,7 +78,7 @@ export default class SlInput extends LitElement {
|
||||||
@property({ type: Boolean, reflect: true }) pill = false;
|
@property({ type: Boolean, reflect: true }) pill = false;
|
||||||
|
|
||||||
/** The input's label. Alternatively, you can use the label slot. */
|
/** The input's label. Alternatively, you can use the label slot. */
|
||||||
@property() label: string;
|
@property() label = '';
|
||||||
|
|
||||||
/** The input's help text. Alternatively, you can use the help-text slot. */
|
/** The input's help text. Alternatively, you can use the help-text slot. */
|
||||||
@property({ attribute: 'help-text' }) helpText = '';
|
@property({ attribute: 'help-text' }) helpText = '';
|
||||||
|
@ -146,7 +145,7 @@ export default class SlInput extends LitElement {
|
||||||
|
|
||||||
/** Gets or sets the current value as a `Date` object. Only valid when `type` is `date`. */
|
/** Gets or sets the current value as a `Date` object. Only valid when `type` is `date`. */
|
||||||
get valueAsDate() {
|
get valueAsDate() {
|
||||||
return this.input.valueAsDate as Date;
|
return this.input.valueAsDate!;
|
||||||
}
|
}
|
||||||
|
|
||||||
set valueAsDate(newValue: Date) {
|
set valueAsDate(newValue: Date) {
|
||||||
|
@ -156,7 +155,7 @@ export default class SlInput extends LitElement {
|
||||||
|
|
||||||
/** Gets or sets the current value as a number. */
|
/** Gets or sets the current value as a number. */
|
||||||
get valueAsNumber() {
|
get valueAsNumber() {
|
||||||
return this.input.valueAsNumber as number;
|
return this.input.valueAsNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
set valueAsNumber(newValue: number) {
|
set valueAsNumber(newValue: number) {
|
||||||
|
@ -180,7 +179,7 @@ export default class SlInput extends LitElement {
|
||||||
|
|
||||||
/** Selects all the text in the input. */
|
/** Selects all the text in the input. */
|
||||||
select() {
|
select() {
|
||||||
return this.input.select();
|
this.input.select();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the start and end positions of the text selection (0-based). */
|
/** Sets the start and end positions of the text selection (0-based). */
|
||||||
|
@ -189,7 +188,7 @@ export default class SlInput extends LitElement {
|
||||||
selectionEnd: number,
|
selectionEnd: number,
|
||||||
selectionDirection: 'forward' | 'backward' | 'none' = 'none'
|
selectionDirection: 'forward' | 'backward' | 'none' = 'none'
|
||||||
) {
|
) {
|
||||||
return this.input.setSelectionRange(selectionStart, selectionEnd, selectionDirection);
|
this.input.setSelectionRange(selectionStart, selectionEnd, selectionDirection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Replaces a range of text with a new string. */
|
/** Replaces a range of text with a new string. */
|
||||||
|
@ -239,13 +238,11 @@ export default class SlInput extends LitElement {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
@watch('disabled')
|
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||||
handleDisabledChange() {
|
handleDisabledChange() {
|
||||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||||
if (this.input) {
|
this.input.disabled = this.disabled;
|
||||||
this.input.disabled = this.disabled;
|
this.invalid = !this.input.checkValidity();
|
||||||
this.invalid = !this.input.checkValidity();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFocus() {
|
handleFocus() {
|
||||||
|
@ -266,11 +263,9 @@ export default class SlInput extends LitElement {
|
||||||
this.isPasswordVisible = !this.isPasswordVisible;
|
this.isPasswordVisible = !this.isPasswordVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@watch('value')
|
@watch('value', { waitUntilFirstUpdate: true })
|
||||||
handleValueChange() {
|
handleValueChange() {
|
||||||
if (this.input) {
|
this.invalid = !this.input.checkValidity();
|
||||||
this.invalid = !this.input.checkValidity();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -306,7 +301,7 @@ export default class SlInput extends LitElement {
|
||||||
'input--filled': this.filled,
|
'input--filled': this.filled,
|
||||||
'input--disabled': this.disabled,
|
'input--disabled': this.disabled,
|
||||||
'input--focused': this.hasFocus,
|
'input--focused': this.hasFocus,
|
||||||
'input--empty': this.value?.length === 0,
|
'input--empty': this.value.length === 0,
|
||||||
'input--invalid': this.invalid
|
'input--invalid': this.invalid
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -355,7 +350,7 @@ export default class SlInput extends LitElement {
|
||||||
@blur=${this.handleBlur}
|
@blur=${this.handleBlur}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
${this.clearable && this.value?.length > 0
|
${this.clearable && this.value.length > 0
|
||||||
? html`
|
? html`
|
||||||
<button
|
<button
|
||||||
part="clear-button"
|
part="clear-button"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, query } from 'lit/decorators.js';
|
import { customElement, property, query } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import styles from './menu-item.styles';
|
import styles from './menu-item.styles';
|
||||||
|
import '~/components/icon/icon';
|
||||||
import '../icon/icon';
|
import { watch } from '~/internal/watch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -43,12 +42,12 @@ export default class SlMenuItem extends LitElement {
|
||||||
|
|
||||||
@watch('checked')
|
@watch('checked')
|
||||||
handleCheckedChange() {
|
handleCheckedChange() {
|
||||||
this.setAttribute('aria-checked', String(this.checked));
|
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
@watch('disabled')
|
@watch('disabled')
|
||||||
handleDisabledChange() {
|
handleDisabledChange() {
|
||||||
this.setAttribute('aria-disabled', String(this.disabled));
|
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, query } from 'lit/decorators.js';
|
import { customElement, query } from 'lit/decorators.js';
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import { getTextContent } from '../../internal/slot';
|
|
||||||
import { hasFocusVisible } from '../../internal/focus-visible';
|
|
||||||
import type SlMenuItem from '../menu-item/menu-item';
|
|
||||||
import styles from './menu.styles';
|
import styles from './menu.styles';
|
||||||
|
import type SlMenuItem from '~/components/menu-item/menu-item';
|
||||||
|
import { emit } from '~/internal/event';
|
||||||
|
import { hasFocusVisible } from '~/internal/focus-visible';
|
||||||
|
import { getTextContent } from '~/internal/slot';
|
||||||
|
|
||||||
|
export interface MenuSelectEventDetail {
|
||||||
|
item: SlMenuItem;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -24,7 +28,7 @@ export default class SlMenu extends LitElement {
|
||||||
@query('slot') defaultSlot: HTMLSlotElement;
|
@query('slot') defaultSlot: HTMLSlotElement;
|
||||||
|
|
||||||
private typeToSelectString = '';
|
private typeToSelectString = '';
|
||||||
private typeToSelectTimeout: any;
|
private typeToSelectTimeout: NodeJS.Timeout;
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
this.setAttribute('role', 'menu');
|
this.setAttribute('role', 'menu');
|
||||||
|
@ -36,7 +40,7 @@ export default class SlMenu extends LitElement {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options?.includeDisabled && (el as SlMenuItem).disabled) {
|
if (!options.includeDisabled && (el as SlMenuItem).disabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,10 +62,12 @@ export default class SlMenu extends LitElement {
|
||||||
*/
|
*/
|
||||||
setCurrentItem(item: SlMenuItem) {
|
setCurrentItem(item: SlMenuItem) {
|
||||||
const items = this.getAllItems({ includeDisabled: false });
|
const items = this.getAllItems({ includeDisabled: false });
|
||||||
let activeItem = item.disabled ? items[0] : item;
|
const activeItem = item.disabled ? items[0] : item;
|
||||||
|
|
||||||
// Update tab indexes
|
// Update tab indexes
|
||||||
items.map(i => i.setAttribute('tabindex', i === activeItem ? '0' : '-1'));
|
items.forEach(i => {
|
||||||
|
i.setAttribute('tabindex', i === activeItem ? '0' : '-1');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -78,13 +84,15 @@ export default class SlMenu extends LitElement {
|
||||||
|
|
||||||
// Restore focus in browsers that don't support :focus-visible when using the keyboard
|
// Restore focus in browsers that don't support :focus-visible when using the keyboard
|
||||||
if (!hasFocusVisible) {
|
if (!hasFocusVisible) {
|
||||||
items.map(item => item.classList.remove('sl-focus-invisible'));
|
items.forEach(item => {
|
||||||
|
item.classList.remove('sl-focus-invisible');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const slot = item.shadowRoot!.querySelector('slot:not([name])') as HTMLSlotElement;
|
const slot = item.shadowRoot?.querySelector<HTMLSlotElement>('slot:not([name])');
|
||||||
const label = getTextContent(slot).toLowerCase().trim();
|
const label = getTextContent(slot).toLowerCase().trim();
|
||||||
if (label.substring(0, this.typeToSelectString.length) === this.typeToSelectString) {
|
if (label.startsWith(this.typeToSelectString)) {
|
||||||
this.setCurrentItem(item);
|
this.setCurrentItem(item);
|
||||||
|
|
||||||
// Set focus here to force the browser to show :focus-visible styles
|
// Set focus here to force the browser to show :focus-visible styles
|
||||||
|
@ -96,9 +104,9 @@ export default class SlMenu extends LitElement {
|
||||||
|
|
||||||
handleClick(event: MouseEvent) {
|
handleClick(event: MouseEvent) {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
const item = target.closest('sl-menu-item') as SlMenuItem;
|
const item = target.closest('sl-menu-item');
|
||||||
|
|
||||||
if (item && !item.disabled) {
|
if (item?.disabled === false) {
|
||||||
emit(this, 'sl-select', { detail: { item } });
|
emit(this, 'sl-select', { detail: { item } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,7 +115,9 @@ export default class SlMenu extends LitElement {
|
||||||
// Restore focus in browsers that don't support :focus-visible when using the keyboard
|
// Restore focus in browsers that don't support :focus-visible when using the keyboard
|
||||||
if (!hasFocusVisible) {
|
if (!hasFocusVisible) {
|
||||||
const items = this.getAllItems();
|
const items = this.getAllItems();
|
||||||
items.map(item => item.classList.remove('sl-focus-invisible'));
|
items.forEach(item => {
|
||||||
|
item.classList.remove('sl-focus-invisible');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,10 +127,8 @@ export default class SlMenu extends LitElement {
|
||||||
const item = this.getCurrentItem();
|
const item = this.getCurrentItem();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (item) {
|
// Simulate a click to support @click handlers on menu items that also work with the keyboard
|
||||||
// Simulate a click to support @click handlers on menu items that also work with the keyboard
|
item?.click();
|
||||||
item.click();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent scrolling when space is pressed
|
// Prevent scrolling when space is pressed
|
||||||
|
@ -132,9 +140,9 @@ export default class SlMenu extends LitElement {
|
||||||
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
||||||
const items = this.getAllItems({ includeDisabled: false });
|
const items = this.getAllItems({ includeDisabled: false });
|
||||||
const activeItem = this.getCurrentItem();
|
const activeItem = this.getCurrentItem();
|
||||||
let index = activeItem ? items.indexOf(activeItem) : 0;
|
let index = typeof activeItem !== 'undefined' ? items.indexOf(activeItem) : 0;
|
||||||
|
|
||||||
if (items.length) {
|
if (items.length > 0) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (event.key === 'ArrowDown') {
|
if (event.key === 'ArrowDown') {
|
||||||
|
@ -147,8 +155,12 @@ export default class SlMenu extends LitElement {
|
||||||
index = items.length - 1;
|
index = items.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < 0) index = 0;
|
if (index < 0) {
|
||||||
if (index > items.length - 1) index = items.length - 1;
|
index = 0;
|
||||||
|
}
|
||||||
|
if (index > items.length - 1) {
|
||||||
|
index = items.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
this.setCurrentItem(items[index]);
|
this.setCurrentItem(items[index]);
|
||||||
items[index].focus();
|
items[index].focus();
|
||||||
|
@ -177,7 +189,7 @@ export default class SlMenu extends LitElement {
|
||||||
const items = this.getAllItems({ includeDisabled: false });
|
const items = this.getAllItems({ includeDisabled: false });
|
||||||
|
|
||||||
// Reset the roving tab index when the slotted items change
|
// Reset the roving tab index when the slotted items change
|
||||||
if (items.length) {
|
if (items.length > 0) {
|
||||||
this.setCurrentItem(items[0]);
|
this.setCurrentItem(items[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
// import sinon from 'sinon';
|
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlMutationObserver from './mutation-observer';
|
|
||||||
|
|
||||||
describe('<sl-mutation-observer>', () => {
|
describe('<sl-mutation-observer>', () => {
|
||||||
it('should render a component', async () => {
|
it('should render a component', async () => {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { emit } from '../../internal/event';
|
|
||||||
import { watch } from '../../internal/watch';
|
|
||||||
import styles from './mutation-observer.styles';
|
import styles from './mutation-observer.styles';
|
||||||
|
import { emit } from '~/internal/event';
|
||||||
|
import { watch } from '~/internal/watch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { expect, fixture, html } from '@open-wc/testing';
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlProgressBar from './progress-bar';
|
import type SlProgressBar from './progress-bar';
|
||||||
|
|
||||||
describe('<sl-progress-bar>', () => {
|
describe('<sl-progress-bar>', () => {
|
||||||
let el: SlProgressBar;
|
let el: SlProgressBar;
|
||||||
|
|
||||||
describe('when provided just a value parameter', async () => {
|
describe('when provided just a value parameter', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlProgressBar>(html`<sl-progress-bar value="25"></sl-progress-bar>`);
|
el = await fixture<SlProgressBar>(html`<sl-progress-bar value="25"></sl-progress-bar>`);
|
||||||
});
|
});
|
||||||
|
@ -16,7 +14,7 @@ describe('<sl-progress-bar>', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided a title, and value parameter', async () => {
|
describe('when provided a title, and value parameter', () => {
|
||||||
let base: HTMLDivElement;
|
let base: HTMLDivElement;
|
||||||
let indicator: HTMLDivElement;
|
let indicator: HTMLDivElement;
|
||||||
|
|
||||||
|
@ -24,43 +22,43 @@ describe('<sl-progress-bar>', () => {
|
||||||
el = await fixture<SlProgressBar>(
|
el = await fixture<SlProgressBar>(
|
||||||
html`<sl-progress-bar title="Titled Progress Ring" value="25"></sl-progress-bar>`
|
html`<sl-progress-bar title="Titled Progress Ring" value="25"></sl-progress-bar>`
|
||||||
);
|
);
|
||||||
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
|
base = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||||
indicator = el.shadowRoot?.querySelector('[part="indicator"]') as HTMLDivElement;
|
indicator = el.shadowRoot!.querySelector('[part="indicator"]')!;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a component that passes accessibility test.', async () => {
|
it('should render a component that passes accessibility test.', async () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the value parameter on the base, as aria-valuenow', async () => {
|
it('uses the value parameter on the base, as aria-valuenow', () => {
|
||||||
expect(base).attribute('aria-valuenow', '25');
|
expect(base).attribute('aria-valuenow', '25');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('appends a % to the value, and uses it as the the value parameter to determine the width on the "indicator" part', async () => {
|
it('appends a % to the value, and uses it as the the value parameter to determine the width on the "indicator" part', () => {
|
||||||
expect(indicator).attribute('style', 'width:25%;');
|
expect(indicator).attribute('style', 'width:25%;');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided an indeterminate parameter', async () => {
|
describe('when provided an indeterminate parameter', () => {
|
||||||
let base: HTMLDivElement;
|
let base: HTMLDivElement;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlProgressBar>(
|
el = await fixture<SlProgressBar>(
|
||||||
html`<sl-progress-bar title="Titled Progress Ring" indeterminate></sl-progress-bar>`
|
html`<sl-progress-bar title="Titled Progress Ring" indeterminate></sl-progress-bar>`
|
||||||
);
|
);
|
||||||
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
|
base = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a component that passes accessibility test.', async () => {
|
it('should render a component that passes accessibility test.', async () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should append a progress-bar--indeterminate class to the "base" part.', async () => {
|
it('should append a progress-bar--indeterminate class to the "base" part.', () => {
|
||||||
expect(base.classList.value.trim()).to.eq('progress-bar progress-bar--indeterminate');
|
expect(base.classList.value.trim()).to.eq('progress-bar progress-bar--indeterminate');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided a ariaLabel, and value parameter', async () => {
|
describe('when provided a ariaLabel, and value parameter', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlProgressBar>(
|
el = await fixture<SlProgressBar>(
|
||||||
html`<sl-progress-bar ariaLabel="Labelled Progress Ring" value="25"></sl-progress-bar>`
|
html`<sl-progress-bar ariaLabel="Labelled Progress Ring" value="25"></sl-progress-bar>`
|
||||||
|
@ -72,7 +70,7 @@ describe('<sl-progress-bar>', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided a ariaLabelledBy, and value parameter', async () => {
|
describe('when provided a ariaLabelledBy, and value parameter', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlProgressBar>(
|
el = await fixture<SlProgressBar>(
|
||||||
html`
|
html`
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
import { LocalizeController } from '../../utilities/localize';
|
|
||||||
import styles from './progress-bar.styles';
|
import styles from './progress-bar.styles';
|
||||||
|
import { LocalizeController } from '~/utilities/localize';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -24,7 +24,7 @@ import styles from './progress-bar.styles';
|
||||||
@customElement('sl-progress-bar')
|
@customElement('sl-progress-bar')
|
||||||
export default class SlProgressBar extends LitElement {
|
export default class SlProgressBar extends LitElement {
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
private localize = new LocalizeController(this);
|
private readonly localize = new LocalizeController(this);
|
||||||
|
|
||||||
/** The current progress, 0 to 100. */
|
/** The current progress, 0 to 100. */
|
||||||
@property({ type: Number, reflect: true }) value = 0;
|
@property({ type: Number, reflect: true }) value = 0;
|
||||||
|
@ -33,7 +33,7 @@ export default class SlProgressBar extends LitElement {
|
||||||
@property({ type: Boolean, reflect: true }) indeterminate = false;
|
@property({ type: Boolean, reflect: true }) indeterminate = false;
|
||||||
|
|
||||||
/** A custom label for the progress bar's aria label. */
|
/** A custom label for the progress bar's aria label. */
|
||||||
@property() label: string;
|
@property() label = '';
|
||||||
|
|
||||||
/** The locale to render the component in. */
|
/** The locale to render the component in. */
|
||||||
@property() lang: string;
|
@property() lang: string;
|
||||||
|
@ -48,12 +48,12 @@ export default class SlProgressBar extends LitElement {
|
||||||
})}
|
})}
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
title=${ifDefined(this.title)}
|
title=${ifDefined(this.title)}
|
||||||
aria-label=${this.label || this.localize.term('progress')}
|
aria-label=${this.label.length > 0 ? this.label : this.localize.term('progress')}
|
||||||
aria-valuemin="0"
|
aria-valuemin="0"
|
||||||
aria-valuemax="100"
|
aria-valuemax="100"
|
||||||
aria-valuenow=${this.indeterminate ? 0 : this.value}
|
aria-valuenow=${this.indeterminate ? 0 : this.value}
|
||||||
>
|
>
|
||||||
<div part="indicator" class="progress-bar__indicator" style=${styleMap({ width: this.value + '%' })}>
|
<div part="indicator" class="progress-bar__indicator" style=${styleMap({ width: `${this.value}%` })}>
|
||||||
${!this.indeterminate
|
${!this.indeterminate
|
||||||
? html`
|
? html`
|
||||||
<span part="label" class="progress-bar__label">
|
<span part="label" class="progress-bar__label">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
import componentStyles from '../../styles/component.styles';
|
import componentStyles from '~/styles/component.styles';
|
||||||
|
|
||||||
export default css`
|
export default css`
|
||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { expect, fixture, html } from '@open-wc/testing';
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
|
||||||
import '../../../dist/shoelace.js';
|
|
||||||
import type SlProgressRing from './progress-ring';
|
import type SlProgressRing from './progress-ring';
|
||||||
|
|
||||||
describe('<sl-progress-ring>', () => {
|
describe('<sl-progress-ring>', () => {
|
||||||
let el: SlProgressRing;
|
let el: SlProgressRing;
|
||||||
|
|
||||||
describe('when provided just a value parameter', async () => {
|
describe('when provided just a value parameter', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlProgressRing>(html`<sl-progress-ring value="25"></sl-progress-ring>`);
|
el = await fixture<SlProgressRing>(html`<sl-progress-ring value="25"></sl-progress-ring>`);
|
||||||
});
|
});
|
||||||
|
@ -16,30 +14,30 @@ describe('<sl-progress-ring>', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided a title, and value parameter', async () => {
|
describe('when provided a title, and value parameter', () => {
|
||||||
let base: HTMLDivElement;
|
let base: HTMLDivElement;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlProgressRing>(
|
el = await fixture<SlProgressRing>(
|
||||||
html`<sl-progress-ring title="Titled Progress Ring" value="25"></sl-progress-ring>`
|
html`<sl-progress-ring title="Titled Progress Ring" value="25"></sl-progress-ring>`
|
||||||
);
|
);
|
||||||
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
|
base = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a component that passes accessibility test.', async () => {
|
it('should render a component that passes accessibility test.', async () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the value parameter on the base, as aria-valuenow', async () => {
|
it('uses the value parameter on the base, as aria-valuenow', () => {
|
||||||
expect(base).attribute('aria-valuenow', '25');
|
expect(base).attribute('aria-valuenow', '25');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('translates the value parameter to a percentage, and uses translation on the base, as percentage css variable', async () => {
|
it('translates the value parameter to a percentage, and uses translation on the base, as percentage css variable', () => {
|
||||||
expect(base).attribute('style', '--percentage: 0.25');
|
expect(base).attribute('style', '--percentage: 0.25');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided a ariaLabel, and value parameter', async () => {
|
describe('when provided a ariaLabel, and value parameter', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlProgressRing>(
|
el = await fixture<SlProgressRing>(
|
||||||
html`<sl-progress-ring ariaLabel="Labelled Progress Ring" value="25"></sl-progress-ring>`
|
html`<sl-progress-ring ariaLabel="Labelled Progress Ring" value="25"></sl-progress-ring>`
|
||||||
|
@ -51,7 +49,7 @@ describe('<sl-progress-ring>', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when provided a ariaLabelledBy, and value parameter', async () => {
|
describe('when provided a ariaLabelledBy, and value parameter', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
el = await fixture<SlProgressRing>(
|
el = await fixture<SlProgressRing>(
|
||||||
html`
|
html`
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||||
import { LocalizeController } from '../../utilities/localize';
|
|
||||||
import styles from './progress-ring.styles';
|
import styles from './progress-ring.styles';
|
||||||
|
import { LocalizeController } from '~/utilities/localize';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
|
@ -20,7 +20,7 @@ import styles from './progress-ring.styles';
|
||||||
@customElement('sl-progress-ring')
|
@customElement('sl-progress-ring')
|
||||||
export default class SlProgressRing extends LitElement {
|
export default class SlProgressRing extends LitElement {
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
private localize = new LocalizeController(this);
|
private readonly localize = new LocalizeController(this);
|
||||||
|
|
||||||
@query('.progress-ring__indicator') indicator: SVGCircleElement;
|
@query('.progress-ring__indicator') indicator: SVGCircleElement;
|
||||||
|
|
||||||
|
@ -30,12 +30,12 @@ export default class SlProgressRing extends LitElement {
|
||||||
@property({ type: Number, reflect: true }) value = 0;
|
@property({ type: Number, reflect: true }) value = 0;
|
||||||
|
|
||||||
/** A custom label for the progress ring's aria label. */
|
/** A custom label for the progress ring's aria label. */
|
||||||
@property() label: string;
|
@property() label = '';
|
||||||
|
|
||||||
/** The locale to render the component in. */
|
/** The locale to render the component in. */
|
||||||
@property() lang: string;
|
@property() lang: string;
|
||||||
|
|
||||||
updated(changedProps: Map<string, any>) {
|
updated(changedProps: Map<string, unknown>) {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -48,7 +48,7 @@ export default class SlProgressRing extends LitElement {
|
||||||
const circumference = 2 * Math.PI * radius;
|
const circumference = 2 * Math.PI * radius;
|
||||||
const offset = circumference - (this.value / 100) * circumference;
|
const offset = circumference - (this.value / 100) * circumference;
|
||||||
|
|
||||||
this.indicatorOffset = String(offset) + 'px';
|
this.indicatorOffset = `${offset}px`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ export default class SlProgressRing extends LitElement {
|
||||||
part="base"
|
part="base"
|
||||||
class="progress-ring"
|
class="progress-ring"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
aria-label=${this.label || this.localize.term('progress')}
|
aria-label=${this.label.length > 0 ? this.label : this.localize.term('progress')}
|
||||||
aria-valuemin="0"
|
aria-valuemin="0"
|
||||||
aria-valuemax="100"
|
aria-valuemax="100"
|
||||||
aria-valuenow="${this.value}"
|
aria-valuenow="${this.value}"
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue