Merge branch 'master' into fix/checked-index-and-value
This commit is contained in:
commit
0ee55369bc
54 changed files with 1080 additions and 15319 deletions
5
.changeset/hungry-mirrors-stare.md
Normal file
5
.changeset/hungry-mirrors-stare.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/form-core': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
remove name property check on lion field for forward compatibility with Form participation api
|
||||||
5
.changeset/little-melons-lay.md
Normal file
5
.changeset/little-melons-lay.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/input-tel': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Make use of awsome-phonenumber, remove local copy
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
'@lion/input-tel-dropdown': patch
|
|
||||||
---
|
|
||||||
|
|
||||||
sync disable state to dropdown for a11y
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
'@lion/switch': patch
|
|
||||||
---
|
|
||||||
|
|
||||||
fix(switch) unregister on disconnectedCallback
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
'@lion/form-core': patch
|
|
||||||
---
|
|
||||||
|
|
||||||
fix: reset the form validators after a form `reset` click or field emptied
|
|
||||||
3
.github/workflows/verify.yml
vendored
3
.github/workflows/verify.yml
vendored
|
|
@ -9,6 +9,9 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Sanity check
|
||||||
|
run: node ./scripts/yarn-lock-scan.js
|
||||||
|
|
||||||
- name: Setup Node 14.x
|
- name: Setup Node 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -219,3 +219,37 @@ export const indeterminateChildren = () => html`
|
||||||
</lion-checkbox-group>
|
</lion-checkbox-group>
|
||||||
`;
|
`;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can also use `mixed-state` attribute so your indeterminate checkbox toggles through three states (indeterminate, checked, unchecked), where for indeterminate state the old children states are restored when you toggle back into this.
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const mixedState = () => html`
|
||||||
|
<lion-checkbox-group name="scientists[]" label="Favorite scientists">
|
||||||
|
<lion-checkbox-indeterminate mixed-state label="Scientists">
|
||||||
|
<lion-checkbox
|
||||||
|
slot="checkbox"
|
||||||
|
label="Isaac Newton"
|
||||||
|
.choiceValue=${'Isaac Newton'}
|
||||||
|
></lion-checkbox>
|
||||||
|
<lion-checkbox
|
||||||
|
slot="checkbox"
|
||||||
|
label="Galileo Galilei"
|
||||||
|
.choiceValue=${'Galileo Galilei'}
|
||||||
|
></lion-checkbox>
|
||||||
|
<lion-checkbox-indeterminate mixed-state slot="checkbox" label="Old Greek scientists">
|
||||||
|
<lion-checkbox
|
||||||
|
slot="checkbox"
|
||||||
|
label="Archimedes"
|
||||||
|
.choiceValue=${'Archimedes'}
|
||||||
|
></lion-checkbox>
|
||||||
|
<lion-checkbox slot="checkbox" label="Plato" .choiceValue=${'Plato'}></lion-checkbox>
|
||||||
|
<lion-checkbox
|
||||||
|
slot="checkbox"
|
||||||
|
label="Pythagoras"
|
||||||
|
.choiceValue=${'Pythagoras'}
|
||||||
|
></lion-checkbox>
|
||||||
|
</lion-checkbox-indeterminate>
|
||||||
|
</lion-checkbox-indeterminate>
|
||||||
|
</lion-checkbox-group>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ export const disabledRotateNavigation = () => html`
|
||||||
<lion-option .choiceValue=${'Beets'}>Beets</lion-option>
|
<lion-option .choiceValue=${'Beets'}>Beets</lion-option>
|
||||||
<lion-option .choiceValue=${'Bell pepper'}>Bell pepper</lion-option>
|
<lion-option .choiceValue=${'Bell pepper'}>Bell pepper</lion-option>
|
||||||
<lion-option .choiceValue=${'Broccoli'}>Broccoli</lion-option>
|
<lion-option .choiceValue=${'Broccoli'}>Broccoli</lion-option>
|
||||||
<lion-option .choiceValue=${'Brussel sprout'} disabled>Brussels sprout</lion-option>
|
<lion-option .choiceValue=${'Brussels sprout'} disabled>Brussels sprout</lion-option>
|
||||||
<lion-option .choiceValue=${'Cabbage'}>Cabbage</lion-option>
|
<lion-option .choiceValue=${'Cabbage'}>Cabbage</lion-option>
|
||||||
<lion-option .choiceValue=${'Carrot'}>Carrot</lion-option>
|
<lion-option .choiceValue=${'Carrot'}>Carrot</lion-option>
|
||||||
</lion-listbox>
|
</lion-listbox>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 0.15.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 558edcb6: Adds a `maxResponseSize` cache option to specify a max size for responses to be cached. The option prevents caching and cache retrieval for responses that are larger than the given maximum as reported in the `Content-Length` header. If this header is missing nothing happens, that is to say caching is not prevented.
|
||||||
|
- efcdf653: Adds a `contentTypes` cache option to specify a whitelist of content types to be cached. The option prevents caching and cache retrieval for responses that do not have one of these values in the `Content-Type` header.
|
||||||
|
- 56af96f1: Add an option "addCaching" to the Ajax config, in order to add cache interceptors when useCache is turned off. In this situation, all requests are cached proactively.
|
||||||
|
|
||||||
## 0.14.0
|
## 0.14.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@lion/ajax",
|
"name": "@lion/ajax",
|
||||||
"version": "0.14.0",
|
"version": "0.15.0",
|
||||||
"description": "Thin wrapper around fetch with support for interceptors.",
|
"description": "Thin wrapper around fetch with support for interceptors.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "ing-bank",
|
"author": "ing-bank",
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,12 @@ export class Ajax {
|
||||||
*/
|
*/
|
||||||
constructor(config = {}) {
|
constructor(config = {}) {
|
||||||
/**
|
/**
|
||||||
* @type {Partial<AjaxConfig>}
|
* @type {AjaxConfig}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.__config = {
|
this.__config = {
|
||||||
addAcceptLanguage: true,
|
addAcceptLanguage: true,
|
||||||
|
addCaching: false,
|
||||||
xsrfCookieName: 'XSRF-TOKEN',
|
xsrfCookieName: 'XSRF-TOKEN',
|
||||||
xsrfHeaderName: 'X-XSRF-TOKEN',
|
xsrfHeaderName: 'X-XSRF-TOKEN',
|
||||||
jsonPrefix: '',
|
jsonPrefix: '',
|
||||||
|
|
@ -53,7 +54,7 @@ export class Ajax {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { cacheOptions } = this.__config;
|
const { cacheOptions } = this.__config;
|
||||||
if (cacheOptions?.useCache) {
|
if (cacheOptions.useCache || this.__config.addCaching) {
|
||||||
const { cacheRequestInterceptor, cacheResponseInterceptor } = createCacheInterceptors(
|
const { cacheRequestInterceptor, cacheResponseInterceptor } = createCacheInterceptors(
|
||||||
cacheOptions.getCacheIdentifier,
|
cacheOptions.getCacheIdentifier,
|
||||||
cacheOptions,
|
cacheOptions,
|
||||||
|
|
@ -65,7 +66,7 @@ export class Ajax {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the Ajax instance
|
* Configures the Ajax instance
|
||||||
* @param {Partial<AjaxConfig>} config configuration for the Ajax instance
|
* @param {AjaxConfig} config configuration for the Ajax instance
|
||||||
*/
|
*/
|
||||||
set options(config) {
|
set options(config) {
|
||||||
this.__config = config;
|
this.__config = config;
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@ export const extendCacheOptions = ({
|
||||||
requestIdFunction = DEFAULT_GET_REQUEST_ID,
|
requestIdFunction = DEFAULT_GET_REQUEST_ID,
|
||||||
invalidateUrls,
|
invalidateUrls,
|
||||||
invalidateUrlsRegex,
|
invalidateUrlsRegex,
|
||||||
|
contentTypes,
|
||||||
|
maxResponseSize,
|
||||||
}) => ({
|
}) => ({
|
||||||
useCache,
|
useCache,
|
||||||
methods,
|
methods,
|
||||||
|
|
@ -89,6 +91,8 @@ export const extendCacheOptions = ({
|
||||||
requestIdFunction,
|
requestIdFunction,
|
||||||
invalidateUrls,
|
invalidateUrls,
|
||||||
invalidateUrlsRegex,
|
invalidateUrlsRegex,
|
||||||
|
contentTypes,
|
||||||
|
maxResponseSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -101,6 +105,8 @@ export const validateCacheOptions = ({
|
||||||
requestIdFunction,
|
requestIdFunction,
|
||||||
invalidateUrls,
|
invalidateUrls,
|
||||||
invalidateUrlsRegex,
|
invalidateUrlsRegex,
|
||||||
|
contentTypes,
|
||||||
|
maxResponseSize,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
if (useCache !== undefined && typeof useCache !== 'boolean') {
|
if (useCache !== undefined && typeof useCache !== 'boolean') {
|
||||||
throw new Error('Property `useCache` must be a `boolean`');
|
throw new Error('Property `useCache` must be a `boolean`');
|
||||||
|
|
@ -112,14 +118,20 @@ export const validateCacheOptions = ({
|
||||||
throw new Error('Property `maxAge` must be a finite `number`');
|
throw new Error('Property `maxAge` must be a finite `number`');
|
||||||
}
|
}
|
||||||
if (invalidateUrls !== undefined && !Array.isArray(invalidateUrls)) {
|
if (invalidateUrls !== undefined && !Array.isArray(invalidateUrls)) {
|
||||||
throw new Error('Property `invalidateUrls` must be an `Array` or `falsy`');
|
throw new Error('Property `invalidateUrls` must be an `Array` or `undefined`');
|
||||||
}
|
}
|
||||||
if (invalidateUrlsRegex !== undefined && !(invalidateUrlsRegex instanceof RegExp)) {
|
if (invalidateUrlsRegex !== undefined && !(invalidateUrlsRegex instanceof RegExp)) {
|
||||||
throw new Error('Property `invalidateUrlsRegex` must be a `RegExp` or `falsy`');
|
throw new Error('Property `invalidateUrlsRegex` must be a `RegExp` or `undefined`');
|
||||||
}
|
}
|
||||||
if (requestIdFunction !== undefined && typeof requestIdFunction !== 'function') {
|
if (requestIdFunction !== undefined && typeof requestIdFunction !== 'function') {
|
||||||
throw new Error('Property `requestIdFunction` must be a `function`');
|
throw new Error('Property `requestIdFunction` must be a `function`');
|
||||||
}
|
}
|
||||||
|
if (contentTypes !== undefined && !Array.isArray(contentTypes)) {
|
||||||
|
throw new Error('Property `contentTypes` must be an `Array` or `undefined`');
|
||||||
|
}
|
||||||
|
if (maxResponseSize !== undefined && !Number.isFinite(maxResponseSize)) {
|
||||||
|
throw new Error('Property `maxResponseSize` must be a finite `number`');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,42 @@ import {
|
||||||
isCurrentSessionId,
|
isCurrentSessionId,
|
||||||
} from '../cacheManager.js';
|
} from '../cacheManager.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether the request method is supported according to the `cacheOptions`
|
||||||
|
* @param {ValidatedCacheOptions} cacheOptions
|
||||||
|
* @param {string} method
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const isMethodSupported = (cacheOptions, method) =>
|
||||||
|
cacheOptions.methods.includes(method.toLowerCase());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether the response content type is supported by the `contentTypes` whitelist
|
||||||
|
* @param {Response} response
|
||||||
|
* @param {CacheOptions} cacheOptions
|
||||||
|
* @returns {boolean} `true` if the contentTypes property is not an array, or if the value of the Content-Type header is in the array
|
||||||
|
*/
|
||||||
|
const isResponseContentTypeSupported = (response, { contentTypes } = {}) => {
|
||||||
|
if (!Array.isArray(contentTypes)) return true;
|
||||||
|
|
||||||
|
return contentTypes.includes(String(response.headers.get('Content-Type')));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether the response size is not too large to be cached according to the `maxResponseSize` property
|
||||||
|
* @param {Response} response
|
||||||
|
* @param {CacheOptions} cacheOptions
|
||||||
|
* @returns {boolean} `true` if the `maxResponseSize` property is not larger than zero, or if the Content-Length header is not present, or if the value of the header is not larger than the `maxResponseSize` property
|
||||||
|
*/
|
||||||
|
const isResponseSizeSupported = (response, { maxResponseSize } = {}) => {
|
||||||
|
const responseSize = +(response.headers.get('Content-Length') || 0);
|
||||||
|
|
||||||
|
if (!maxResponseSize) return true;
|
||||||
|
if (!responseSize) return true;
|
||||||
|
|
||||||
|
return responseSize <= maxResponseSize;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request interceptor to return relevant cached requests
|
* Request interceptor to return relevant cached requests
|
||||||
* @param {function(): string} getCacheId used to invalidate cache if identifier is changed
|
* @param {function(): string} getCacheId used to invalidate cache if identifier is changed
|
||||||
|
|
@ -36,9 +72,8 @@ const createCacheRequestInterceptor =
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestId = cacheOptions.requestIdFunction(request);
|
const requestId = cacheOptions.requestIdFunction(request);
|
||||||
const isMethodSupported = cacheOptions.methods.includes(request.method.toLowerCase());
|
|
||||||
|
|
||||||
if (!isMethodSupported) {
|
if (!isMethodSupported(cacheOptions, request.method)) {
|
||||||
invalidateMatchingCache(requestId, cacheOptions);
|
invalidateMatchingCache(requestId, cacheOptions);
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +85,11 @@ const createCacheRequestInterceptor =
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedResponse = ajaxCache.get(requestId, cacheOptions.maxAge);
|
const cachedResponse = ajaxCache.get(requestId, cacheOptions.maxAge);
|
||||||
if (cachedResponse) {
|
if (
|
||||||
|
cachedResponse &&
|
||||||
|
isResponseContentTypeSupported(cachedResponse, cacheOptions) &&
|
||||||
|
isResponseSizeSupported(cachedResponse, cacheOptions)
|
||||||
|
) {
|
||||||
// Return the response from cache
|
// Return the response from cache
|
||||||
request.cacheOptions = request.cacheOptions ?? { useCache: false };
|
request.cacheOptions = request.cacheOptions ?? { useCache: false };
|
||||||
/** @type {CacheResponse} */
|
/** @type {CacheResponse} */
|
||||||
|
|
@ -81,13 +120,14 @@ const createCacheResponseInterceptor =
|
||||||
...response.request.cacheOptions,
|
...response.request.cacheOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const requestId = cacheOptions.requestIdFunction(response.request);
|
if (!response.fromCache && isMethodSupported(cacheOptions, response.request.method)) {
|
||||||
const isAlreadyFromCache = !!response.fromCache;
|
const requestId = cacheOptions.requestIdFunction(response.request);
|
||||||
const isCacheActive = cacheOptions.useCache;
|
|
||||||
const isMethodSupported = cacheOptions.methods.includes(response.request?.method.toLowerCase());
|
|
||||||
|
|
||||||
if (!isAlreadyFromCache && isCacheActive && isMethodSupported) {
|
if (
|
||||||
if (isCurrentSessionId(response.request.cacheSessionId)) {
|
isCurrentSessionId(response.request.cacheSessionId) &&
|
||||||
|
isResponseContentTypeSupported(response, cacheOptions) &&
|
||||||
|
isResponseSizeSupported(response, cacheOptions)
|
||||||
|
) {
|
||||||
// Cache the response
|
// Cache the response
|
||||||
ajaxCache.set(requestId, response.clone());
|
ajaxCache.set(requestId, response.clone());
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +135,7 @@ const createCacheResponseInterceptor =
|
||||||
// Mark the pending request as resolved
|
// Mark the pending request as resolved
|
||||||
pendingRequestStore.resolve(requestId);
|
pendingRequestStore.resolve(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ describe('Ajax', () => {
|
||||||
};
|
};
|
||||||
const expected = {
|
const expected = {
|
||||||
addAcceptLanguage: true,
|
addAcceptLanguage: true,
|
||||||
|
addCaching: false,
|
||||||
xsrfCookieName: 'XSRF-TOKEN',
|
xsrfCookieName: 'XSRF-TOKEN',
|
||||||
xsrfHeaderName: 'X-XSRF-TOKEN',
|
xsrfHeaderName: 'X-XSRF-TOKEN',
|
||||||
jsonPrefix: ")]}',",
|
jsonPrefix: ")]}',",
|
||||||
|
|
@ -303,6 +304,44 @@ describe('Ajax', () => {
|
||||||
getCacheIdentifier = () => String(cacheId);
|
getCacheIdentifier = () => String(cacheId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not add cache interceptors when useCache is turned off', () => {
|
||||||
|
const customAjax = new Ajax({
|
||||||
|
cacheOptions: {
|
||||||
|
maxAge: 100,
|
||||||
|
getCacheIdentifier,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(customAjax._requestInterceptors.length).to.equal(2);
|
||||||
|
expect(customAjax._responseInterceptors.length).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds cache interceptors when useCache is turned on', () => {
|
||||||
|
const customAjax = new Ajax({
|
||||||
|
cacheOptions: {
|
||||||
|
useCache: true,
|
||||||
|
maxAge: 100,
|
||||||
|
getCacheIdentifier,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(customAjax._requestInterceptors.length).to.equal(3);
|
||||||
|
expect(customAjax._responseInterceptors.length).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds cache interceptors when addCaching is turned on', () => {
|
||||||
|
const customAjax = new Ajax({
|
||||||
|
addCaching: true,
|
||||||
|
cacheOptions: {
|
||||||
|
maxAge: 100,
|
||||||
|
getCacheIdentifier,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(customAjax._requestInterceptors.length).to.equal(3);
|
||||||
|
expect(customAjax._responseInterceptors.length).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
describe('caching interceptors', async () => {
|
describe('caching interceptors', async () => {
|
||||||
/**
|
/**
|
||||||
* @type {Ajax}
|
* @type {Ajax}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,8 @@ describe('cacheManager', () => {
|
||||||
requestIdFunction,
|
requestIdFunction,
|
||||||
invalidateUrls: invalidateUrlsResult,
|
invalidateUrls: invalidateUrlsResult,
|
||||||
invalidateUrlsRegex: invalidateUrlsRegexResult,
|
invalidateUrlsRegex: invalidateUrlsRegexResult,
|
||||||
|
contentTypes,
|
||||||
|
maxResponseSize,
|
||||||
} = extendCacheOptions({ invalidateUrls, invalidateUrlsRegex });
|
} = extendCacheOptions({ invalidateUrls, invalidateUrlsRegex });
|
||||||
// Assert
|
// Assert
|
||||||
expect(useCache).to.be.false;
|
expect(useCache).to.be.false;
|
||||||
|
|
@ -82,6 +84,8 @@ describe('cacheManager', () => {
|
||||||
expect(typeof requestIdFunction).to.eql('function');
|
expect(typeof requestIdFunction).to.eql('function');
|
||||||
expect(invalidateUrlsResult).to.equal(invalidateUrls);
|
expect(invalidateUrlsResult).to.equal(invalidateUrls);
|
||||||
expect(invalidateUrlsRegexResult).to.equal(invalidateUrlsRegex);
|
expect(invalidateUrlsRegexResult).to.equal(invalidateUrlsRegex);
|
||||||
|
expect(contentTypes).to.be.undefined;
|
||||||
|
expect(maxResponseSize).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the DEFAULT_GET_REQUEST_ID function throws when called with no arguments', () => {
|
it('the DEFAULT_GET_REQUEST_ID function throws when called with no arguments', () => {
|
||||||
|
|
@ -129,18 +133,22 @@ describe('cacheManager', () => {
|
||||||
it('does not accept null as argument', () => {
|
it('does not accept null as argument', () => {
|
||||||
expect(() => validateCacheOptions(null)).to.throw(TypeError);
|
expect(() => validateCacheOptions(null)).to.throw(TypeError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts an empty object', () => {
|
it('accepts an empty object', () => {
|
||||||
expect(() => validateCacheOptions({})).not.to.throw(
|
expect(() => validateCacheOptions({})).not.to.throw(
|
||||||
'Property `useCache` must be a `boolean`',
|
'Property `useCache` must be a `boolean`',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('the useCache property', () => {
|
describe('the useCache property', () => {
|
||||||
it('accepts a boolean', () => {
|
it('accepts a boolean', () => {
|
||||||
expect(() => validateCacheOptions({ useCache: false })).not.to.throw;
|
expect(() => validateCacheOptions({ useCache: false })).not.to.throw;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts undefined', () => {
|
it('accepts undefined', () => {
|
||||||
expect(() => validateCacheOptions({ useCache: undefined })).not.to.throw;
|
expect(() => validateCacheOptions({ useCache: undefined })).not.to.throw;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not accept anything else', () => {
|
it('does not accept anything else', () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(() => validateCacheOptions({ useCache: '' })).to.throw(
|
expect(() => validateCacheOptions({ useCache: '' })).to.throw(
|
||||||
|
|
@ -148,13 +156,16 @@ describe('cacheManager', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('the methods property', () => {
|
describe('the methods property', () => {
|
||||||
it('accepts an array with the value `get`', () => {
|
it('accepts an array with the value `get`', () => {
|
||||||
expect(() => validateCacheOptions({ methods: ['get'] })).not.to.throw;
|
expect(() => validateCacheOptions({ methods: ['get'] })).not.to.throw;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts undefined', () => {
|
it('accepts undefined', () => {
|
||||||
expect(() => validateCacheOptions({ methods: undefined })).not.to.throw;
|
expect(() => validateCacheOptions({ methods: undefined })).not.to.throw;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not accept anything else', () => {
|
it('does not accept anything else', () => {
|
||||||
expect(() => validateCacheOptions({ methods: [] })).to.throw(
|
expect(() => validateCacheOptions({ methods: [] })).to.throw(
|
||||||
'Cache can only be utilized with `GET` method',
|
'Cache can only be utilized with `GET` method',
|
||||||
|
|
@ -167,13 +178,16 @@ describe('cacheManager', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('the maxAge property', () => {
|
describe('the maxAge property', () => {
|
||||||
it('accepts a finite number', () => {
|
it('accepts a finite number', () => {
|
||||||
expect(() => validateCacheOptions({ maxAge: 42 })).not.to.throw;
|
expect(() => validateCacheOptions({ maxAge: 42 })).not.to.throw;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts undefined', () => {
|
it('accepts undefined', () => {
|
||||||
expect(() => validateCacheOptions({ maxAge: undefined })).not.to.throw;
|
expect(() => validateCacheOptions({ maxAge: undefined })).not.to.throw;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not accept anything else', () => {
|
it('does not accept anything else', () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(() => validateCacheOptions({ maxAge: 'string' })).to.throw(
|
expect(() => validateCacheOptions({ maxAge: 'string' })).to.throw(
|
||||||
|
|
@ -184,6 +198,7 @@ describe('cacheManager', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('the invalidateUrls property', () => {
|
describe('the invalidateUrls property', () => {
|
||||||
it('accepts an array', () => {
|
it('accepts an array', () => {
|
||||||
// @ts-ignore Typescript requires this to be an array of string, but this is not checked by validateCacheOptions
|
// @ts-ignore Typescript requires this to be an array of string, but this is not checked by validateCacheOptions
|
||||||
|
|
@ -191,31 +206,37 @@ describe('cacheManager', () => {
|
||||||
validateCacheOptions({ invalidateUrls: [6, 'elements', 'in', 1, true, Array] }),
|
validateCacheOptions({ invalidateUrls: [6, 'elements', 'in', 1, true, Array] }),
|
||||||
).not.to.throw;
|
).not.to.throw;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts undefined', () => {
|
it('accepts undefined', () => {
|
||||||
expect(() => validateCacheOptions({ invalidateUrls: undefined })).not.to.throw;
|
expect(() => validateCacheOptions({ invalidateUrls: undefined })).not.to.throw;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not accept anything else', () => {
|
it('does not accept anything else', () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(() => validateCacheOptions({ invalidateUrls: 'not-an-array' })).to.throw(
|
expect(() => validateCacheOptions({ invalidateUrls: 'not-an-array' })).to.throw(
|
||||||
'Property `invalidateUrls` must be an `Array` or `falsy`',
|
'Property `invalidateUrls` must be an `Array` or `undefined`',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('the invalidateUrlsRegex property', () => {
|
describe('the invalidateUrlsRegex property', () => {
|
||||||
it('accepts a regular expression', () => {
|
it('accepts a regular expression', () => {
|
||||||
expect(() => validateCacheOptions({ invalidateUrlsRegex: /this is a very picky regex/ }))
|
expect(() => validateCacheOptions({ invalidateUrlsRegex: /this is a very picky regex/ }))
|
||||||
.not.to.throw;
|
.not.to.throw;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts undefined', () => {
|
it('accepts undefined', () => {
|
||||||
expect(() => validateCacheOptions({ invalidateUrlsRegex: undefined })).not.to.throw;
|
expect(() => validateCacheOptions({ invalidateUrlsRegex: undefined })).not.to.throw;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not accept anything else', () => {
|
it('does not accept anything else', () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(() =>
|
expect(() =>
|
||||||
validateCacheOptions({ invalidateUrlsRegex: 'a string is not a regex' }),
|
validateCacheOptions({ invalidateUrlsRegex: 'a string is not a regex' }),
|
||||||
).to.throw('Property `invalidateUrlsRegex` must be a `RegExp` or `falsy`');
|
).to.throw('Property `invalidateUrlsRegex` must be a `RegExp` or `undefined`');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('the requestIdFunction property', () => {
|
describe('the requestIdFunction property', () => {
|
||||||
it('accepts a function', () => {
|
it('accepts a function', () => {
|
||||||
// @ts-ignore Typescript requires the requestIdFunction to return a string, but this is not checked by validateCacheOptions
|
// @ts-ignore Typescript requires the requestIdFunction to return a string, but this is not checked by validateCacheOptions
|
||||||
|
|
@ -223,9 +244,11 @@ describe('cacheManager', () => {
|
||||||
validateCacheOptions({ requestIdFunction: () => ['this-is-ok-outside-typescript'] }),
|
validateCacheOptions({ requestIdFunction: () => ['this-is-ok-outside-typescript'] }),
|
||||||
).not.to.throw;
|
).not.to.throw;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts undefined', () => {
|
it('accepts undefined', () => {
|
||||||
expect(() => validateCacheOptions({ requestIdFunction: undefined })).not.to.throw;
|
expect(() => validateCacheOptions({ requestIdFunction: undefined })).not.to.throw;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not accept anything else', () => {
|
it('does not accept anything else', () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(() => validateCacheOptions({ requestIdFunction: 'not a function' })).to.throw(
|
expect(() => validateCacheOptions({ requestIdFunction: 'not a function' })).to.throw(
|
||||||
|
|
@ -233,6 +256,45 @@ describe('cacheManager', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('the contentTypes property', () => {
|
||||||
|
it('accepts an array', () => {
|
||||||
|
// @ts-ignore Typescript requires this to be an array of string, but this is not checked by validateCacheOptions
|
||||||
|
expect(() => validateCacheOptions({ contentTypes: [6, 'elements', 'in', 1, true, Array] }))
|
||||||
|
.not.to.throw;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts undefined', () => {
|
||||||
|
expect(() => validateCacheOptions({ contentTypes: undefined })).not.to.throw;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not accept anything else', () => {
|
||||||
|
// @ts-ignore
|
||||||
|
expect(() => validateCacheOptions({ contentTypes: 'not-an-array' })).to.throw(
|
||||||
|
'Property `contentTypes` must be an `Array` or `undefined`',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the maxResponseSize property', () => {
|
||||||
|
it('accepts a finite number', () => {
|
||||||
|
expect(() => validateCacheOptions({ maxResponseSize: 42 })).not.to.throw;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts undefined', () => {
|
||||||
|
expect(() => validateCacheOptions({ maxResponseSize: undefined })).not.to.throw;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not accept anything else', () => {
|
||||||
|
// @ts-ignore
|
||||||
|
expect(() => validateCacheOptions({ maxResponseSize: 'string' })).to.throw(
|
||||||
|
'Property `maxResponseSize` must be a finite `number`',
|
||||||
|
);
|
||||||
|
expect(() => validateCacheOptions({ maxResponseSize: Infinity })).to.throw(
|
||||||
|
'Property `maxResponseSize` must be a finite `number`',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('invalidateMatchingCache', () => {
|
describe('invalidateMatchingCache', () => {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ describe('cache interceptors', () => {
|
||||||
let cacheId;
|
let cacheId;
|
||||||
/** @type {sinon.SinonStub} */
|
/** @type {sinon.SinonStub} */
|
||||||
let fetchStub;
|
let fetchStub;
|
||||||
|
/** @type {Response} */
|
||||||
|
let mockResponse;
|
||||||
const getCacheIdentifier = () => String(cacheId);
|
const getCacheIdentifier = () => String(cacheId);
|
||||||
/** @type {sinon.SinonSpy} */
|
/** @type {sinon.SinonSpy} */
|
||||||
let ajaxRequestSpy;
|
let ajaxRequestSpy;
|
||||||
|
|
@ -51,8 +53,9 @@ describe('cache interceptors', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ajax = new Ajax();
|
ajax = new Ajax();
|
||||||
|
mockResponse = new Response('mock response');
|
||||||
fetchStub = sinon.stub(window, 'fetch');
|
fetchStub = sinon.stub(window, 'fetch');
|
||||||
fetchStub.returns(Promise.resolve(new Response('mock response')));
|
fetchStub.resolves(mockResponse);
|
||||||
ajaxRequestSpy = sinon.spy(ajax, 'fetch');
|
ajaxRequestSpy = sinon.spy(ajax, 'fetch');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -150,14 +153,12 @@ describe('cache interceptors', () => {
|
||||||
expect(fetchStub.callCount).to.equal(1);
|
expect(fetchStub.callCount).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Check if this is the behaviour we want
|
it('all calls are cached proactively', async () => {
|
||||||
it('all calls with non-default `maxAge` are cached proactively', async () => {
|
|
||||||
// Given
|
// Given
|
||||||
newCacheId();
|
newCacheId();
|
||||||
|
|
||||||
addCacheInterceptors(ajax, {
|
addCacheInterceptors(ajax, {
|
||||||
useCache: false,
|
useCache: false,
|
||||||
maxAge: 100,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// When
|
// When
|
||||||
|
|
@ -169,11 +170,7 @@ describe('cache interceptors', () => {
|
||||||
expect(fetchStub.callCount).to.equal(1);
|
expect(fetchStub.callCount).to.equal(1);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
await ajax.fetch('/test', {
|
await ajax.fetch('/test');
|
||||||
cacheOptions: {
|
|
||||||
useCache: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(fetchStub.callCount).to.equal(2);
|
expect(fetchStub.callCount).to.equal(2);
|
||||||
|
|
@ -297,7 +294,7 @@ describe('cache interceptors', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-ignore not an actual valid CacheResponse object
|
// @ts-ignore not an actual valid CacheResponse object
|
||||||
await cacheResponseInterceptor({ request: { method: 'get' } })
|
await cacheResponseInterceptor({ request: { method: 'get' }, headers: new Headers() })
|
||||||
.then(() => expect('everything').to.be.ok)
|
.then(() => expect('everything').to.be.ok)
|
||||||
.catch(err =>
|
.catch(err =>
|
||||||
expect.fail(
|
expect.fail(
|
||||||
|
|
@ -305,6 +302,259 @@ describe('cache interceptors', () => {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('caches concurrent requests', async () => {
|
||||||
|
newCacheId();
|
||||||
|
|
||||||
|
const clock = sinon.useFakeTimers();
|
||||||
|
|
||||||
|
fetchStub.onFirstCall().returns(returnResponseOnTick(900, 1));
|
||||||
|
fetchStub.onSecondCall().returns(returnResponseOnTick(1900, 2));
|
||||||
|
|
||||||
|
addCacheInterceptors(ajax, {
|
||||||
|
useCache: true,
|
||||||
|
maxAge: 750,
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstRequest = ajax.fetch('/test').then(r => r.text());
|
||||||
|
const concurrentFirstRequest1 = ajax.fetch('/test').then(r => r.text());
|
||||||
|
const concurrentFirstRequest2 = ajax.fetch('/test').then(r => r.text());
|
||||||
|
|
||||||
|
clock.tick(1000);
|
||||||
|
|
||||||
|
// firstRequest is cached at tick 1000 in the next line!
|
||||||
|
const firstResponses = await Promise.all([
|
||||||
|
firstRequest,
|
||||||
|
concurrentFirstRequest1,
|
||||||
|
concurrentFirstRequest2,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(fetchStub.callCount).to.equal(1);
|
||||||
|
|
||||||
|
const cachedFirstRequest = ajax.fetch('/test').then(r => r.text());
|
||||||
|
|
||||||
|
clock.tick(500);
|
||||||
|
|
||||||
|
const cachedFirstResponse = await cachedFirstRequest;
|
||||||
|
|
||||||
|
expect(fetchStub.callCount).to.equal(1);
|
||||||
|
|
||||||
|
const secondRequest = ajax.fetch('/test').then(r => r.text());
|
||||||
|
const secondConcurrentRequest = ajax.fetch('/test').then(r => r.text());
|
||||||
|
|
||||||
|
clock.tick(1000);
|
||||||
|
|
||||||
|
const secondResponses = await Promise.all([secondRequest, secondConcurrentRequest]);
|
||||||
|
|
||||||
|
expect(fetchStub.callCount).to.equal(2);
|
||||||
|
|
||||||
|
expect(firstResponses).to.eql(['mock response 1', 'mock response 1', 'mock response 1']);
|
||||||
|
|
||||||
|
expect(cachedFirstResponse).to.equal('mock response 1');
|
||||||
|
|
||||||
|
expect(secondResponses).to.eql(['mock response 2', 'mock response 2']);
|
||||||
|
|
||||||
|
clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves status and headers when returning cached response', async () => {
|
||||||
|
newCacheId();
|
||||||
|
fetchStub.returns(
|
||||||
|
Promise.resolve(
|
||||||
|
new Response('mock response', { status: 206, headers: { 'x-foo': 'x-bar' } }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
addCacheInterceptors(ajax, {
|
||||||
|
useCache: true,
|
||||||
|
maxAge: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response1 = await ajax.fetch('/test');
|
||||||
|
const response2 = await ajax.fetch('/test');
|
||||||
|
expect(fetchStub.callCount).to.equal(1);
|
||||||
|
expect(response1.status).to.equal(206);
|
||||||
|
expect(response1.headers.get('x-foo')).to.equal('x-bar');
|
||||||
|
expect(response2.status).to.equal(206);
|
||||||
|
expect(response2.headers.get('x-foo')).to.equal('x-bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does save to the cache when `contentTypes` is specified and a supported content type is returned', async () => {
|
||||||
|
// Given
|
||||||
|
newCacheId();
|
||||||
|
mockResponse.headers.set('content-type', 'application/xml');
|
||||||
|
addCacheInterceptors(ajax, {
|
||||||
|
useCache: true,
|
||||||
|
contentTypes: ['application/json', 'application/xml'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(fetchStub.callCount).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does save to the cache when `maxResponseSize` is specified and the response size is within the threshold', async () => {
|
||||||
|
// Given
|
||||||
|
newCacheId();
|
||||||
|
mockResponse.headers.set('content-length', '20000');
|
||||||
|
addCacheInterceptors(ajax, {
|
||||||
|
useCache: true,
|
||||||
|
maxResponseSize: 50000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(fetchStub.callCount).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does save to the cache when `maxResponseSize` is specified and the response size is unknown', async () => {
|
||||||
|
// Given
|
||||||
|
newCacheId();
|
||||||
|
|
||||||
|
addCacheInterceptors(ajax, {
|
||||||
|
useCache: true,
|
||||||
|
maxResponseSize: 50000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(fetchStub.callCount).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Bypassing the cache', () => {
|
||||||
|
it('caches response but does not return it when expiration time is 0', async () => {
|
||||||
|
newCacheId();
|
||||||
|
|
||||||
|
addCacheInterceptors(ajax, {
|
||||||
|
useCache: true,
|
||||||
|
maxAge: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clock = sinon.useFakeTimers();
|
||||||
|
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
|
||||||
|
expect(ajaxRequestSpy.calledOnce).to.be.true;
|
||||||
|
|
||||||
|
expect(ajaxRequestSpy.calledWith('/test')).to.be.true;
|
||||||
|
|
||||||
|
clock.tick(1);
|
||||||
|
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
|
||||||
|
clock.restore();
|
||||||
|
|
||||||
|
expect(fetchStub.callCount).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not use cache when cacheOption `useCache: false` is passed to fetch method', async () => {
|
||||||
|
// Given
|
||||||
|
newCacheId();
|
||||||
|
addCacheInterceptors(ajax, { useCache: true });
|
||||||
|
|
||||||
|
// When
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(fetchStub.callCount).to.equal(1);
|
||||||
|
|
||||||
|
// When
|
||||||
|
await ajax.fetch('/test', { cacheOptions: { useCache: false } });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(fetchStub.callCount).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not save to the cache when `contentTypes` is specified and an unsupported content type is returned', async () => {
|
||||||
|
// Given
|
||||||
|
newCacheId();
|
||||||
|
mockResponse.headers.set('content-type', 'text/html');
|
||||||
|
addCacheInterceptors(ajax, {
|
||||||
|
useCache: true,
|
||||||
|
contentTypes: ['application/json', 'application/xml'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
await ajax.fetch('/test', { cacheOptions: { contentTypes: ['text/html'] } });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(fetchStub.callCount).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not read from the cache when `contentTypes` is specified and an unsupported content type is returned', async () => {
|
||||||
|
// Given
|
||||||
|
newCacheId();
|
||||||
|
mockResponse.headers.set('content-type', 'application/json');
|
||||||
|
addCacheInterceptors(ajax, {
|
||||||
|
useCache: true,
|
||||||
|
contentTypes: ['application/json', 'application/xml'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(fetchStub.callCount).to.equal(1);
|
||||||
|
|
||||||
|
// When
|
||||||
|
await ajax.fetch('/test', { cacheOptions: { contentTypes: [] } });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(fetchStub.callCount).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not save to the cache when `maxResponseSize` is specified and a larger content-length is specified in the response', async () => {
|
||||||
|
// Given
|
||||||
|
newCacheId();
|
||||||
|
mockResponse.headers.set('content-length', '80000');
|
||||||
|
addCacheInterceptors(ajax, {
|
||||||
|
useCache: true,
|
||||||
|
maxResponseSize: 50000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
await ajax.fetch('/test', { cacheOptions: { maxResponseSize: 100000 } });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(fetchStub.callCount).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not read from the cache when `maxResponseSize` is specified and a larger content-length is specified in the response', async () => {
|
||||||
|
// Given
|
||||||
|
newCacheId();
|
||||||
|
mockResponse.headers.set('content-length', '80000');
|
||||||
|
addCacheInterceptors(ajax, {
|
||||||
|
useCache: true,
|
||||||
|
maxResponseSize: 100000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
await ajax.fetch('/test');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(fetchStub.callCount).to.equal(1);
|
||||||
|
|
||||||
|
// When
|
||||||
|
await ajax.fetch('/test', { cacheOptions: { maxResponseSize: 50000 } });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(fetchStub.callCount).to.equal(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Cache invalidation', () => {
|
describe('Cache invalidation', () => {
|
||||||
|
|
@ -449,101 +699,6 @@ describe('cache interceptors', () => {
|
||||||
expect(fetchStub.callCount).to.equal(3);
|
expect(fetchStub.callCount).to.equal(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('caches response but does not return it when expiration time is 0', async () => {
|
|
||||||
newCacheId();
|
|
||||||
|
|
||||||
addCacheInterceptors(ajax, {
|
|
||||||
useCache: true,
|
|
||||||
maxAge: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const clock = sinon.useFakeTimers();
|
|
||||||
|
|
||||||
await ajax.fetch('/test');
|
|
||||||
|
|
||||||
expect(ajaxRequestSpy.calledOnce).to.be.true;
|
|
||||||
|
|
||||||
expect(ajaxRequestSpy.calledWith('/test')).to.be.true;
|
|
||||||
|
|
||||||
clock.tick(1);
|
|
||||||
|
|
||||||
await ajax.fetch('/test');
|
|
||||||
|
|
||||||
clock.restore();
|
|
||||||
|
|
||||||
expect(fetchStub.callCount).to.equal(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not use cache when cacheOption `useCache: false` is passed to fetch method', async () => {
|
|
||||||
// Given
|
|
||||||
addCacheInterceptors(ajax, { useCache: true });
|
|
||||||
|
|
||||||
// When
|
|
||||||
await ajax.fetch('/test');
|
|
||||||
await ajax.fetch('/test');
|
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(fetchStub.callCount).to.equal(1);
|
|
||||||
|
|
||||||
// When
|
|
||||||
await ajax.fetch('/test', { cacheOptions: { useCache: false } });
|
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(fetchStub.callCount).to.equal(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('caches concurrent requests', async () => {
|
|
||||||
newCacheId();
|
|
||||||
|
|
||||||
const clock = sinon.useFakeTimers();
|
|
||||||
|
|
||||||
fetchStub.onFirstCall().returns(returnResponseOnTick(900, 1));
|
|
||||||
fetchStub.onSecondCall().returns(returnResponseOnTick(1900, 2));
|
|
||||||
|
|
||||||
addCacheInterceptors(ajax, {
|
|
||||||
useCache: true,
|
|
||||||
maxAge: 750,
|
|
||||||
});
|
|
||||||
|
|
||||||
const firstRequest = ajax.fetch('/test').then(r => r.text());
|
|
||||||
const concurrentFirstRequest1 = ajax.fetch('/test').then(r => r.text());
|
|
||||||
const concurrentFirstRequest2 = ajax.fetch('/test').then(r => r.text());
|
|
||||||
|
|
||||||
clock.tick(1000);
|
|
||||||
|
|
||||||
// firstRequest is cached at tick 1000 in the next line!
|
|
||||||
const firstResponses = await Promise.all([
|
|
||||||
firstRequest,
|
|
||||||
concurrentFirstRequest1,
|
|
||||||
concurrentFirstRequest2,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(fetchStub.callCount).to.equal(1);
|
|
||||||
|
|
||||||
const cachedFirstRequest = ajax.fetch('/test').then(r => r.text());
|
|
||||||
|
|
||||||
clock.tick(500);
|
|
||||||
|
|
||||||
const cachedFirstResponse = await cachedFirstRequest;
|
|
||||||
|
|
||||||
expect(fetchStub.callCount).to.equal(1);
|
|
||||||
|
|
||||||
const secondRequest = ajax.fetch('/test').then(r => r.text());
|
|
||||||
const secondConcurrentRequest = ajax.fetch('/test').then(r => r.text());
|
|
||||||
|
|
||||||
clock.tick(1000);
|
|
||||||
|
|
||||||
const secondResponses = await Promise.all([secondRequest, secondConcurrentRequest]);
|
|
||||||
|
|
||||||
expect(fetchStub.callCount).to.equal(2);
|
|
||||||
|
|
||||||
expect(firstResponses).to.eql(['mock response 1', 'mock response 1', 'mock response 1']);
|
|
||||||
|
|
||||||
expect(cachedFirstResponse).to.equal('mock response 1');
|
|
||||||
|
|
||||||
expect(secondResponses).to.eql(['mock response 2', 'mock response 2']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('discards responses that are requested in a different cache session', async () => {
|
it('discards responses that are requested in a different cache session', async () => {
|
||||||
newCacheId();
|
newCacheId();
|
||||||
|
|
||||||
|
|
@ -569,27 +724,5 @@ describe('cache interceptors', () => {
|
||||||
expect(ajaxCache._cachedRequests).to.deep.equal({});
|
expect(ajaxCache._cachedRequests).to.deep.equal({});
|
||||||
expect(fetchStub.callCount).to.equal(1);
|
expect(fetchStub.callCount).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('preserves status and headers when returning cached response', async () => {
|
|
||||||
newCacheId();
|
|
||||||
fetchStub.returns(
|
|
||||||
Promise.resolve(
|
|
||||||
new Response('mock response', { status: 206, headers: { 'x-foo': 'x-bar' } }),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
addCacheInterceptors(ajax, {
|
|
||||||
useCache: true,
|
|
||||||
maxAge: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response1 = await ajax.fetch('/test');
|
|
||||||
const response2 = await ajax.fetch('/test');
|
|
||||||
expect(fetchStub.callCount).to.equal(1);
|
|
||||||
expect(response1.status).to.equal(206);
|
|
||||||
expect(response1.headers.get('x-foo')).to.equal('x-bar');
|
|
||||||
expect(response2.status).to.equal(206);
|
|
||||||
expect(response2.headers.get('x-foo')).to.equal('x-bar');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
3
packages/ajax/types/types.d.ts
vendored
3
packages/ajax/types/types.d.ts
vendored
|
|
@ -12,6 +12,7 @@ export interface LionRequestInit extends Omit<RequestInit, 'body'> {
|
||||||
|
|
||||||
export interface AjaxConfig {
|
export interface AjaxConfig {
|
||||||
addAcceptLanguage: boolean;
|
addAcceptLanguage: boolean;
|
||||||
|
addCaching: boolean;
|
||||||
xsrfCookieName: string | null;
|
xsrfCookieName: string | null;
|
||||||
xsrfHeaderName: string | null;
|
xsrfHeaderName: string | null;
|
||||||
cacheOptions: CacheOptionsWithIdentifier;
|
cacheOptions: CacheOptionsWithIdentifier;
|
||||||
|
|
@ -39,6 +40,8 @@ export interface CacheOptions {
|
||||||
invalidateUrls?: string[];
|
invalidateUrls?: string[];
|
||||||
invalidateUrlsRegex?: RegExp;
|
invalidateUrlsRegex?: RegExp;
|
||||||
requestIdFunction?: RequestIdFunction;
|
requestIdFunction?: RequestIdFunction;
|
||||||
|
contentTypes?: string[];
|
||||||
|
maxResponseSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CacheOptionsWithIdentifier extends CacheOptions {
|
export interface CacheOptionsWithIdentifier extends CacheOptions {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 0.20.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 41297869: Add mixed-state feature to checkbox indeterminate. See https://www.w3.org/TR/wai-aria-practices-1.1/examples/checkbox/checkbox-2/checkbox-2.html for the WAI ARIA pattern.
|
||||||
|
- Updated dependencies [43dd1320]
|
||||||
|
- @lion/form-core@0.17.1
|
||||||
|
|
||||||
## 0.20.0
|
## 0.20.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@lion/checkbox-group",
|
"name": "@lion/checkbox-group",
|
||||||
"version": "0.20.0",
|
"version": "0.20.1",
|
||||||
"description": "A container for multiple checkboxes",
|
"description": "A container for multiple checkboxes",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "ing-bank",
|
"author": "ing-bank",
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lion/core": "^0.22.0",
|
"@lion/core": "^0.22.0",
|
||||||
"@lion/form-core": "^0.17.0",
|
"@lion/form-core": "^0.17.1",
|
||||||
"@lion/input": "^0.17.0"
|
"@lion/input": "^0.17.0"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,11 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
reflect: true,
|
reflect: true,
|
||||||
},
|
},
|
||||||
|
mixedState: {
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
attribute: 'mixed-state',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,6 +63,18 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
|
||||||
return /** @type LionCheckbox[] */ (checkboxes);
|
return /** @type LionCheckbox[] */ (checkboxes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_storeIndeterminateState() {
|
||||||
|
this._indeterminateSubStates = this._subCheckboxes.map(checkbox => checkbox.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setOldState() {
|
||||||
|
if (this.indeterminate) {
|
||||||
|
this._oldState = 'indeterminate';
|
||||||
|
} else {
|
||||||
|
this._oldState = this.checked ? 'checked' : 'unchecked';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
|
|
@ -89,6 +106,27 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setBasedOnMixedState() {
|
||||||
|
switch (this._oldState) {
|
||||||
|
case 'checked':
|
||||||
|
// --> unchecked
|
||||||
|
this.checked = false;
|
||||||
|
this.indeterminate = false;
|
||||||
|
break;
|
||||||
|
case 'unchecked':
|
||||||
|
// --> indeterminate
|
||||||
|
this.checked = false;
|
||||||
|
this.indeterminate = true;
|
||||||
|
break;
|
||||||
|
case 'indeterminate':
|
||||||
|
// --> checked
|
||||||
|
this.checked = true;
|
||||||
|
this.indeterminate = false;
|
||||||
|
break;
|
||||||
|
// no default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Event} ev
|
* @param {Event} ev
|
||||||
* @private
|
* @private
|
||||||
|
|
@ -97,15 +135,44 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
|
||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _ev = /** @type {CustomEvent} */ (ev);
|
const _ev = /** @type {CustomEvent} */ (ev);
|
||||||
|
|
||||||
|
// If the model value change event is coming from out own _inputNode
|
||||||
|
// and we're not already setting our own (mixed) state programmatically
|
||||||
if (_ev.detail.formPath[0] === this && !this.__settingOwnChecked) {
|
if (_ev.detail.formPath[0] === this && !this.__settingOwnChecked) {
|
||||||
this._subCheckboxes.forEach(checkbox => {
|
const allEqual = (/** @type {any[]} */ arr) => arr.every(val => val === arr[0]);
|
||||||
// eslint-disable-next-line no-param-reassign
|
// If our child checkboxes states are all the same, we shouldn't be able to reach indeterminate (mixed) state
|
||||||
checkbox.checked = this._inputNode.checked;
|
if (this.mixedState && !allEqual(this._indeterminateSubStates)) {
|
||||||
|
this._setBasedOnMixedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__settingOwnSubs = true;
|
||||||
|
if (this.indeterminate && this.mixedState) {
|
||||||
|
this._subCheckboxes.forEach((checkbox, i) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
checkbox.checked = this._indeterminateSubStates[i];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._subCheckboxes.forEach(checkbox => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
checkbox.checked = this._inputNode.checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
this.__settingOwnSubs = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._setOwnCheckedState();
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
if (!this.__settingOwnSubs && !this.__settingOwnChecked && this.mixedState) {
|
||||||
|
this._storeIndeterminateState();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this._setOwnCheckedState();
|
|
||||||
|
if (this.mixedState) {
|
||||||
|
this._setOldState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -132,6 +199,9 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
|
||||||
this.indeterminate = false;
|
this.indeterminate = false;
|
||||||
this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
|
this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
|
||||||
this.__onModelValueChanged = this.__onModelValueChanged.bind(this);
|
this.__onModelValueChanged = this.__onModelValueChanged.bind(this);
|
||||||
|
/** @type {boolean[]} */
|
||||||
|
this._indeterminateSubStates = [];
|
||||||
|
this.mixedState = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
|
@ -146,6 +216,15 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
|
||||||
this.removeEventListener('form-element-register', this._onRequestToAddFormElement);
|
this.removeEventListener('form-element-register', this._onRequestToAddFormElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {import('lit-element').PropertyValues } changedProperties */
|
||||||
|
firstUpdated(changedProperties) {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
this._setOldState();
|
||||||
|
if (this.indeterminate) {
|
||||||
|
this._storeIndeterminateState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @param {import('lit-element').PropertyValues } changedProperties */
|
/** @param {import('lit-element').PropertyValues } changedProperties */
|
||||||
updated(changedProperties) {
|
updated(changedProperties) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
import '@lion/checkbox-group/define';
|
import '@lion/checkbox-group/define';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @typedef {import('../src/LionCheckbox').LionCheckbox} LionCheckbox
|
||||||
* @typedef {import('../src/LionCheckboxIndeterminate').LionCheckboxIndeterminate} LionCheckboxIndeterminate
|
* @typedef {import('../src/LionCheckboxIndeterminate').LionCheckboxIndeterminate} LionCheckboxIndeterminate
|
||||||
* @typedef {import('../src/LionCheckboxGroup').LionCheckboxGroup} LionCheckboxGroup
|
* @typedef {import('../src/LionCheckboxGroup').LionCheckboxGroup} LionCheckboxGroup
|
||||||
*/
|
*/
|
||||||
|
|
@ -428,4 +429,153 @@ describe('<lion-checkbox-indeterminate>', () => {
|
||||||
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||||
expect(elIndeterminate?.checked).to.be.true;
|
expect(elIndeterminate?.checked).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/wai-aria-practices-1.1/examples/checkbox/checkbox-2/checkbox-2.html
|
||||||
|
describe('mixed-state', () => {
|
||||||
|
it('can have a mixed-state (using mixed-state attribute), none -> indeterminate -> all, cycling through', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-checkbox-group name="scientists[]">
|
||||||
|
<lion-checkbox-indeterminate mixed-state label="Favorite scientists">
|
||||||
|
<lion-checkbox slot="checkbox" label="Archimedes" checked></lion-checkbox>
|
||||||
|
<lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox>
|
||||||
|
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
|
||||||
|
</lion-checkbox-indeterminate>
|
||||||
|
</lion-checkbox-group>
|
||||||
|
`);
|
||||||
|
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
|
||||||
|
el.querySelector('lion-checkbox-indeterminate')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(elIndeterminate.mixedState).to.be.true;
|
||||||
|
expect(elIndeterminate.checked).to.be.false;
|
||||||
|
expect(elIndeterminate.indeterminate).to.be.true;
|
||||||
|
|
||||||
|
// @ts-ignore for testing purposes, we access this protected getter
|
||||||
|
elIndeterminate._inputNode.click();
|
||||||
|
await elIndeterminate.updateComplete;
|
||||||
|
expect(elIndeterminate.checked).to.be.true;
|
||||||
|
expect(elIndeterminate.indeterminate).to.be.false;
|
||||||
|
|
||||||
|
// @ts-ignore for testing purposes, we access this protected getter
|
||||||
|
elIndeterminate._inputNode.click();
|
||||||
|
await elIndeterminate.updateComplete;
|
||||||
|
expect(elIndeterminate.checked).to.be.false;
|
||||||
|
expect(elIndeterminate.indeterminate).to.be.false;
|
||||||
|
|
||||||
|
// @ts-ignore for testing purposes, we access this protected getter
|
||||||
|
elIndeterminate._inputNode.click();
|
||||||
|
await elIndeterminate.updateComplete;
|
||||||
|
expect(elIndeterminate.checked).to.be.false;
|
||||||
|
expect(elIndeterminate.indeterminate).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset to old child checkbox states when reaching indeterminate state', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-checkbox-group name="scientists[]">
|
||||||
|
<lion-checkbox-indeterminate mixed-state label="Favorite scientists">
|
||||||
|
<lion-checkbox slot="checkbox" label="Archimedes" checked></lion-checkbox>
|
||||||
|
<lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox>
|
||||||
|
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
|
||||||
|
</lion-checkbox-indeterminate>
|
||||||
|
</lion-checkbox-group>
|
||||||
|
`);
|
||||||
|
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
|
||||||
|
el.querySelector('lion-checkbox-indeterminate')
|
||||||
|
);
|
||||||
|
const checkboxEls = /** @type {LionCheckbox[]} */ (
|
||||||
|
Array.from(el.querySelectorAll('lion-checkbox'))
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkboxEls.map(checkboxEl => checkboxEl.checked)).to.eql([true, false, false]);
|
||||||
|
|
||||||
|
// @ts-ignore for testing purposes, we access this protected getter
|
||||||
|
elIndeterminate._inputNode.click();
|
||||||
|
await elIndeterminate.updateComplete;
|
||||||
|
expect(checkboxEls.map(checkboxEl => checkboxEl.checked)).to.eql([true, true, true]);
|
||||||
|
|
||||||
|
// @ts-ignore for testing purposes, we access this protected getter
|
||||||
|
elIndeterminate._inputNode.click();
|
||||||
|
await elIndeterminate.updateComplete;
|
||||||
|
expect(checkboxEls.map(checkboxEl => checkboxEl.checked)).to.eql([false, false, false]);
|
||||||
|
|
||||||
|
// @ts-ignore for testing purposes, we access this protected getter
|
||||||
|
elIndeterminate._inputNode.click();
|
||||||
|
await elIndeterminate.updateComplete;
|
||||||
|
expect(checkboxEls.map(checkboxEl => checkboxEl.checked)).to.eql([true, false, false]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should no longer reach indeterminate state if the child boxes are all checked or all unchecked during indeterminate state', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-checkbox-group name="scientists[]">
|
||||||
|
<lion-checkbox-indeterminate mixed-state label="Favorite scientists">
|
||||||
|
<lion-checkbox slot="checkbox" label="Archimedes" checked></lion-checkbox>
|
||||||
|
<lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox>
|
||||||
|
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
|
||||||
|
</lion-checkbox-indeterminate>
|
||||||
|
</lion-checkbox-group>
|
||||||
|
`);
|
||||||
|
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
|
||||||
|
el.querySelector('lion-checkbox-indeterminate')
|
||||||
|
);
|
||||||
|
const checkboxEls = /** @type {LionCheckbox[]} */ (
|
||||||
|
Array.from(el.querySelectorAll('lion-checkbox'))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check when all child boxes in indeterminate state are unchecked
|
||||||
|
// we don't have a tri-state, but a duo-state.
|
||||||
|
|
||||||
|
// @ts-ignore for testing purposes, we access this protected getter
|
||||||
|
checkboxEls[0]._inputNode.click();
|
||||||
|
await elIndeterminate.updateComplete;
|
||||||
|
|
||||||
|
// @ts-ignore for testing purposes, we access this protected getter
|
||||||
|
elIndeterminate._inputNode.click();
|
||||||
|
await elIndeterminate.updateComplete;
|
||||||
|
expect(elIndeterminate.checked).to.be.true;
|
||||||
|
expect(elIndeterminate.indeterminate).to.be.false;
|
||||||
|
|
||||||
|
// @ts-ignore for testing purposes, we access this protected getter
|
||||||
|
elIndeterminate._inputNode.click();
|
||||||
|
await elIndeterminate.updateComplete;
|
||||||
|
expect(elIndeterminate.checked).to.be.false;
|
||||||
|
expect(elIndeterminate.indeterminate).to.be.false;
|
||||||
|
|
||||||
|
// @ts-ignore for testing purposes, we access this protected getter
|
||||||
|
elIndeterminate._inputNode.click();
|
||||||
|
await elIndeterminate.updateComplete;
|
||||||
|
expect(elIndeterminate.checked).to.be.true;
|
||||||
|
expect(elIndeterminate.indeterminate).to.be.false;
|
||||||
|
|
||||||
|
// Check when all child boxes in indeterminate state are getting checked
|
||||||
|
// we also don't have a tri-state, but a duo-state.
|
||||||
|
|
||||||
|
// @ts-ignore for testing purposes, we access this protected getter
|
||||||
|
elIndeterminate._inputNode.click(); // unchecked
|
||||||
|
await elIndeterminate.updateComplete;
|
||||||
|
|
||||||
|
for (const checkEl of checkboxEls) {
|
||||||
|
// @ts-ignore for testing purposes, we access this protected getter
|
||||||
|
checkEl._inputNode.click();
|
||||||
|
// Give each checking of the sub checkbox a chance to finish updating
|
||||||
|
// This means indeterminate state will be true for a bit and the state gets stored
|
||||||
|
await checkEl.updateComplete;
|
||||||
|
await elIndeterminate.updateComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(elIndeterminate.checked).to.be.true;
|
||||||
|
expect(elIndeterminate.indeterminate).to.be.false;
|
||||||
|
|
||||||
|
// @ts-ignore for testing purposes, we access this protected getter
|
||||||
|
elIndeterminate._inputNode.click();
|
||||||
|
await elIndeterminate.updateComplete;
|
||||||
|
expect(elIndeterminate.checked).to.be.false;
|
||||||
|
expect(elIndeterminate.indeterminate).to.be.false;
|
||||||
|
|
||||||
|
// @ts-ignore for testing purposes, we access this protected getter
|
||||||
|
elIndeterminate._inputNode.click();
|
||||||
|
await elIndeterminate.updateComplete;
|
||||||
|
expect(elIndeterminate.checked).to.be.true;
|
||||||
|
expect(elIndeterminate.indeterminate).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,11 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
this._listboxReceivesNoFocus = true;
|
this._listboxReceivesNoFocus = true;
|
||||||
|
/**
|
||||||
|
* @configure ListboxMixin
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
this._noTypeAhead = true;
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 0.17.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 43dd1320: fix: reset the form validators after a form `reset` click or field emptied
|
||||||
|
|
||||||
## 0.17.0
|
## 0.17.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@lion/form-core",
|
"name": "@lion/form-core",
|
||||||
"version": "0.17.0",
|
"version": "0.17.1",
|
||||||
"description": "Form-core contains all essential building blocks for creating form fields and fieldsets",
|
"description": "Form-core contains all essential building blocks for creating form fields and fieldsets",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "ing-bank",
|
"author": "ing-bank",
|
||||||
|
|
|
||||||
|
|
@ -156,10 +156,6 @@ const FormRegistrarMixinImplementation = superclass =>
|
||||||
// 2. Add children as object key
|
// 2. Add children as object key
|
||||||
if (this._isFormOrFieldset) {
|
if (this._isFormOrFieldset) {
|
||||||
const { name } = child;
|
const { name } = child;
|
||||||
if (!name) {
|
|
||||||
console.info('Error Node:', child); // eslint-disable-line no-console
|
|
||||||
throw new TypeError('You need to define a name');
|
|
||||||
}
|
|
||||||
if (name === this.name) {
|
if (name === this.name) {
|
||||||
console.info('Error Node:', child); // eslint-disable-line no-console
|
console.info('Error Node:', child); // eslint-disable-line no-console
|
||||||
throw new TypeError(`You can not have the same name "${name}" as your parent`);
|
throw new TypeError(`You can not have the same name "${name}" as your parent`);
|
||||||
|
|
|
||||||
|
|
@ -133,27 +133,6 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if an element without a name tries to register', async () => {
|
|
||||||
const orig = console.info;
|
|
||||||
console.info = () => {};
|
|
||||||
|
|
||||||
let error;
|
|
||||||
const el = /** @type {FormGroup} */ (await fixture(html`<${tag}></${tag}>`));
|
|
||||||
try {
|
|
||||||
// we test the api directly as errors thrown from a web component are in a
|
|
||||||
// different context and we can not catch them here => register fake elements
|
|
||||||
el.addFormElement(
|
|
||||||
/** @type {HTMLElement & import('../../types/FormControlMixinTypes').FormControlHost} */ ({}),
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
error = err;
|
|
||||||
}
|
|
||||||
expect(error).to.be.instanceOf(TypeError);
|
|
||||||
expect(/** @type {TypeError} */ (error).message).to.equal('You need to define a name');
|
|
||||||
|
|
||||||
console.info = orig; // restore original console
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws if name is the same as its parent', async () => {
|
it('throws if name is the same as its parent', async () => {
|
||||||
const orig = console.info;
|
const orig = console.info;
|
||||||
console.info = () => {};
|
console.info = () => {};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
# @lion/input-tel-dropdown
|
# @lion/input-tel-dropdown
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 8ecfc31c: sync disable state to dropdown for a11y
|
||||||
|
- @lion/input-tel@0.1.2
|
||||||
|
|
||||||
## 0.1.1
|
## 0.1.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@lion/input-tel-dropdown",
|
"name": "@lion/input-tel-dropdown",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"description": "Input field for entering phone numbers with the help of a dropdown region list",
|
"description": "Input field for entering phone numbers with the help of a dropdown region list",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "ing-bank",
|
"author": "ing-bank",
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lion/core": "0.22.0",
|
"@lion/core": "0.22.0",
|
||||||
"@lion/input-tel": "0.1.1",
|
"@lion/input-tel": "0.1.2",
|
||||||
"@lion/localize": "0.24.0"
|
"@lion/localize": "0.24.0"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,12 @@ import { localize } from '@lion/localize';
|
||||||
* @typedef {import('lit/directives/ref.js').Ref} Ref
|
* @typedef {import('lit/directives/ref.js').Ref} Ref
|
||||||
* @typedef {import('@lion/core').RenderOptions} RenderOptions
|
* @typedef {import('@lion/core').RenderOptions} RenderOptions
|
||||||
* @typedef {import('@lion/form-core/types/FormatMixinTypes').FormatHost} FormatHost
|
* @typedef {import('@lion/form-core/types/FormatMixinTypes').FormatHost} FormatHost
|
||||||
* @typedef {import('@lion/input-tel/types').FormatStrategy} FormatStrategy
|
|
||||||
* @typedef {import('@lion/input-tel/types').RegionCode} RegionCode
|
* @typedef {import('@lion/input-tel/types').RegionCode} RegionCode
|
||||||
* @typedef {import('../types').TemplateDataForDropdownInputTel} TemplateDataForDropdownInputTel
|
* @typedef {import('../types').TemplateDataForDropdownInputTel} TemplateDataForDropdownInputTel
|
||||||
* @typedef {import('../types').OnDropdownChangeEvent} OnDropdownChangeEvent
|
* @typedef {import('../types').OnDropdownChangeEvent} OnDropdownChangeEvent
|
||||||
* @typedef {import('../types').DropdownRef} DropdownRef
|
* @typedef {import('../types').DropdownRef} DropdownRef
|
||||||
* @typedef {import('../types').RegionMeta} RegionMeta
|
* @typedef {import('../types').RegionMeta} RegionMeta
|
||||||
* @typedef {* & import('@lion/input-tel/src/lib/awesome-phonenumber-esm').default} AwesomePhoneNumber
|
* @typedef {* & import('awesome-phonenumber').default} AwesomePhoneNumber
|
||||||
* @typedef {import('@lion/select-rich').LionSelectRich} LionSelectRich
|
* @typedef {import('@lion/select-rich').LionSelectRich} LionSelectRich
|
||||||
* @typedef {import('@lion/overlays').OverlayController} OverlayController
|
* @typedef {import('@lion/overlays').OverlayController} OverlayController
|
||||||
* @typedef {TemplateDataForDropdownInputTel & {data: {regionMetaList:RegionMeta[]}}} TemplateDataForIntlInputTel
|
* @typedef {TemplateDataForDropdownInputTel & {data: {regionMetaList:RegionMeta[]}}} TemplateDataForIntlInputTel
|
||||||
|
|
|
||||||
2
packages/input-tel-dropdown/types/index.d.ts
vendored
2
packages/input-tel-dropdown/types/index.d.ts
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
import { RegionCode } from '@lion/input-tel/types/types';
|
import { RegionCode } from '@lion/input-tel/types';
|
||||||
import { LionSelectRich } from '@lion/select-rich';
|
import { LionSelectRich } from '@lion/select-rich';
|
||||||
import { LionCombobox } from '@lion/combobox';
|
import { LionCombobox } from '@lion/combobox';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
# @lion/input-tel
|
# @lion/input-tel
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [43dd1320]
|
||||||
|
- @lion/form-core@0.17.1
|
||||||
|
|
||||||
## 0.1.1
|
## 0.1.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@lion/input-tel",
|
"name": "@lion/input-tel",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"description": "Input field for entering phone numbers, including validation, formatting and mobile keyboard support.",
|
"description": "Input field for entering phone numbers, including validation, formatting and mobile keyboard support.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "ing-bank",
|
"author": "ing-bank",
|
||||||
|
|
@ -38,9 +38,10 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lion/core": "0.22.0",
|
"@lion/core": "0.22.0",
|
||||||
"@lion/form-core": "0.17.0",
|
"@lion/form-core": "0.17.1",
|
||||||
"@lion/input": "0.17.0",
|
"@lion/input": "0.17.0",
|
||||||
"@lion/localize": "0.24.0"
|
"@lion/localize": "0.24.0",
|
||||||
|
"awesome-phonenumber": "^3.0.1"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"input",
|
"input",
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@ import { PhoneNumber } from './validators.js';
|
||||||
import { localizeNamespaceLoader } from './localizeNamespaceLoader.js';
|
import { localizeNamespaceLoader } from './localizeNamespaceLoader.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types').FormatStrategy} FormatStrategy
|
|
||||||
* @typedef {import('../types').RegionCode} RegionCode
|
* @typedef {import('../types').RegionCode} RegionCode
|
||||||
* @typedef {import('../types').PhoneNumberType} PhoneNumberType
|
* @typedef {import('awesome-phonenumber').PhoneNumberFormat} PhoneNumberFormat
|
||||||
|
* @typedef {import('awesome-phonenumber').PhoneNumberTypes} PhoneNumberTypes
|
||||||
* @typedef {import('@lion/form-core/types/FormatMixinTypes').FormatOptions} FormatOptions
|
* @typedef {import('@lion/form-core/types/FormatMixinTypes').FormatOptions} FormatOptions
|
||||||
* @typedef {* & import('@lion/input-tel/src/lib/awesome-phonenumber-esm').default} AwesomePhoneNumber
|
* @typedef {* & import('awesome-phonenumber').default} AwesomePhoneNumber
|
||||||
* @typedef {FormatOptions & {regionCode: RegionCode; formatStrategy: FormatStrategy}} FormatOptionsTel
|
* @typedef {FormatOptions & {regionCode: RegionCode; formatStrategy: PhoneNumberFormat}} FormatOptionsTel
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class LionInputTel extends LocalizeMixin(LionInput) {
|
export class LionInputTel extends LocalizeMixin(LionInput) {
|
||||||
|
|
@ -65,7 +65,7 @@ export class LionInputTel extends LocalizeMixin(LionInput) {
|
||||||
* -'unknown'
|
* -'unknown'
|
||||||
* See https://www.npmjs.com/package/awesome-phonenumber
|
* See https://www.npmjs.com/package/awesome-phonenumber
|
||||||
* @readonly
|
* @readonly
|
||||||
* @property {PhoneNumberType|undefined} activePhoneNumberType
|
* @property {PhoneNumberTypes|undefined} activePhoneNumberTypes
|
||||||
*/
|
*/
|
||||||
get activePhoneNumberType() {
|
get activePhoneNumberType() {
|
||||||
let pn;
|
let pn;
|
||||||
|
|
@ -135,7 +135,7 @@ export class LionInputTel extends LocalizeMixin(LionInput) {
|
||||||
* Determines what the formatter output should look like.
|
* Determines what the formatter output should look like.
|
||||||
* Formatting strategies as provided by google-libphonenumber
|
* Formatting strategies as provided by google-libphonenumber
|
||||||
* See: https://www.npmjs.com/package/google-libphonenumber
|
* See: https://www.npmjs.com/package/google-libphonenumber
|
||||||
* @type {FormatStrategy}
|
* @type {PhoneNumberFormat}
|
||||||
*/
|
*/
|
||||||
this.formatStrategy = 'international';
|
this.formatStrategy = 'international';
|
||||||
|
|
||||||
|
|
@ -286,7 +286,7 @@ export class LionInputTel extends LocalizeMixin(LionInput) {
|
||||||
*/
|
*/
|
||||||
_onPhoneNumberUtilReady() {
|
_onPhoneNumberUtilReady() {
|
||||||
// This should trigger a rerender in shadow dom
|
// This should trigger a rerender in shadow dom
|
||||||
this._phoneUtil = /** @type {PhoneNumber} */ (PhoneUtilManager.PhoneUtil);
|
this._phoneUtil = /** @type {AwesomePhoneNumber} */ (PhoneUtilManager.PhoneUtil);
|
||||||
// This should trigger a rerender in light dom
|
// This should trigger a rerender in light dom
|
||||||
this._scheduleLightDomRender();
|
this._scheduleLightDomRender();
|
||||||
// Format when libPhoneNumber is loaded
|
// Format when libPhoneNumber is loaded
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ let resolveLoaded;
|
||||||
*/
|
*/
|
||||||
export class PhoneUtilManager {
|
export class PhoneUtilManager {
|
||||||
static async loadLibPhoneNumber() {
|
static async loadLibPhoneNumber() {
|
||||||
const PhoneUtil = (await import('./lib/awesome-phonenumber-esm.js')).default;
|
const PhoneUtil = (await import('awesome-phonenumber')).default;
|
||||||
this.PhoneUtil = PhoneUtil;
|
this.PhoneUtil = PhoneUtil;
|
||||||
resolveLoaded(undefined);
|
resolveLoaded(undefined);
|
||||||
return PhoneUtil;
|
return PhoneUtil;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { PhoneUtilManager } from './PhoneUtilManager.js';
|
import { PhoneUtilManager } from './PhoneUtilManager.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types').FormatStrategy} FormatStrategy
|
* @typedef {import('awesome-phonenumber').PhoneNumberFormat} PhoneNumberFormat
|
||||||
* @typedef {import('../types').RegionCode} RegionCode
|
* @typedef {import('../types').RegionCode} RegionCode
|
||||||
* @typedef {* & import('@lion/input-tel/src/lib/awesome-phonenumber-esm').default} AwesomePhoneNumber
|
* @typedef {* & import('awesome-phonenumber').default} AwesomePhoneNumber
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} modelValue
|
* @param {string} modelValue
|
||||||
* @param {object} options
|
* @param {object} options
|
||||||
* @param {RegionCode} options.regionCode
|
* @param {RegionCode} options.regionCode
|
||||||
* @param {FormatStrategy} [options.formatStrategy='international']
|
* @param {PhoneNumberFormat} [options.formatStrategy='international']
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function formatPhoneNumber(modelValue, { regionCode, formatStrategy = 'international' }) {
|
export function formatPhoneNumber(modelValue, { regionCode, formatStrategy = 'international' }) {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -2,12 +2,12 @@ import { PhoneUtilManager } from './PhoneUtilManager.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types').RegionCode} RegionCode
|
* @typedef {import('../types').RegionCode} RegionCode
|
||||||
* @typedef {* & import('@lion/input-tel/src/lib/awesome-phonenumber-esm').default} AwesomePhoneNumber
|
* @typedef {* & import('awesome-phonenumber').default} AwesomePhoneNumber
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} viewValue
|
* @param {string} viewValue
|
||||||
* @param {{regionCode:RegionCode;}} options
|
* @param {{regionCode:RegionCode}} options
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function parsePhoneNumber(viewValue, { regionCode }) {
|
export function parsePhoneNumber(viewValue, { regionCode }) {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ import { PhoneUtilManager } from './PhoneUtilManager.js';
|
||||||
import { formatPhoneNumber } from './formatters.js';
|
import { formatPhoneNumber } from './formatters.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types').FormatStrategy} FormatStrategy
|
|
||||||
* @typedef {import('../types').RegionCode} RegionCode
|
* @typedef {import('../types').RegionCode} RegionCode
|
||||||
* @typedef {* & import('@lion/input-tel/src/lib/awesome-phonenumber-esm').default} AwesomePhoneNumber
|
* @typedef {import('awesome-phonenumber').PhoneNumberFormat} PhoneNumberFormat
|
||||||
|
* @typedef {* & import('awesome-phonenumber').default} AwesomePhoneNumber
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -13,7 +13,7 @@ import { formatPhoneNumber } from './formatters.js';
|
||||||
* @param {RegionCode} options.regionCode
|
* @param {RegionCode} options.regionCode
|
||||||
* @param {string} options.prevViewValue
|
* @param {string} options.prevViewValue
|
||||||
* @param {number} options.currentCaretIndex
|
* @param {number} options.currentCaretIndex
|
||||||
* @param {FormatStrategy} options.formatStrategy
|
* @param {PhoneNumberFormat} options.formatStrategy
|
||||||
* @returns {{viewValue:string; caretIndex:number;}|undefined}
|
* @returns {{viewValue:string; caretIndex:number;}|undefined}
|
||||||
*/
|
*/
|
||||||
export function liveFormatPhoneNumber(
|
export function liveFormatPhoneNumber(
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { PhoneUtilManager } from './PhoneUtilManager.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types').RegionCode} RegionCode
|
* @typedef {import('../types').RegionCode} RegionCode
|
||||||
* @typedef {* & import('@lion/input-tel/src/lib/awesome-phonenumber-esm').default} AwesomePhoneNumber
|
* @typedef {* & import('awesome-phonenumber').default} AwesomePhoneNumber
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
} from '../test-helpers/mockPhoneUtilManager.js';
|
} from '../test-helpers/mockPhoneUtilManager.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {* & import('@lion/input-tel/src/lib/awesome-phonenumber-esm').default} AwesomePhoneNumber
|
* @typedef {* & import('awesome-phonenumber').default} AwesomePhoneNumber
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// For enum output, see: https://www.npmjs.com/package/awesome-phonenumber
|
// For enum output, see: https://www.npmjs.com/package/awesome-phonenumber
|
||||||
|
|
|
||||||
42
packages/input-tel/types/index.d.ts
vendored
42
packages/input-tel/types/index.d.ts
vendored
|
|
@ -1,46 +1,6 @@
|
||||||
/*
|
|
||||||
* Phone number types as provided by google-libphonenumber
|
|
||||||
* See:
|
|
||||||
* - https://www.npmjs.com/package/google-libphonenumber
|
|
||||||
* - https://www.npmjs.com/package/awesome-phonenumber
|
|
||||||
*/
|
|
||||||
export type PhoneNumberType =
|
|
||||||
| 'fixed-line'
|
|
||||||
| 'fixed-line-or-mobile'
|
|
||||||
| 'mobile'
|
|
||||||
| 'pager'
|
|
||||||
| 'personal-number'
|
|
||||||
| 'premium-rate'
|
|
||||||
| 'shared-cost'
|
|
||||||
| 'toll-free'
|
|
||||||
| 'uan'
|
|
||||||
| 'voip'
|
|
||||||
| 'unknown';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Phone number possibilities as provided by google-libphonenumber
|
|
||||||
* See:
|
|
||||||
* - https://www.npmjs.com/package/google-libphonenumber
|
|
||||||
* - https://www.npmjs.com/package/awesome-phonenumber
|
|
||||||
*/
|
|
||||||
export type PhoneNumberPossibility =
|
|
||||||
| 'is-possible'
|
|
||||||
| 'invalid-country-code'
|
|
||||||
| 'too-long'
|
|
||||||
| 'too-short'
|
|
||||||
| 'unknown';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Phone number formats / formatting strategies as provided by google-libphonenumber
|
|
||||||
* See:
|
|
||||||
* - https://www.npmjs.com/package/google-libphonenumber
|
|
||||||
* - https://www.npmjs.com/package/awesome-phonenumber
|
|
||||||
*/
|
|
||||||
export type FormatStrategy = 'e164' | 'international' | 'national' | 'rfc3966' | 'significant';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supported countries/regions as provided via
|
* Supported countries/regions as provided via
|
||||||
* `libphonenumber.PhoneNumberUtil.getInstance().getSupportedRegions()`
|
* `libphonenumber.getSupportedRegionCodes()`
|
||||||
*/
|
*/
|
||||||
export type RegionCode =
|
export type RegionCode =
|
||||||
| 'AC'
|
| 'AC'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
# @lion/listbox
|
# @lion/listbox
|
||||||
|
|
||||||
|
## 0.13.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- a28686ee: Add TypeAhead, so with typing characters you will set an option with matching value active/checked
|
||||||
|
- Updated dependencies [43dd1320]
|
||||||
|
- @lion/form-core@0.17.1
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@lion/listbox",
|
"name": "@lion/listbox",
|
||||||
"version": "0.13.0",
|
"version": "0.13.1",
|
||||||
"description": "A listbox widget presents a list of options and allows a user to select one or more of them",
|
"description": "A listbox widget presents a list of options and allows a user to select one or more of them",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "ing-bank",
|
"author": "ing-bank",
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lion/core": "^0.22.0",
|
"@lion/core": "^0.22.0",
|
||||||
"@lion/form-core": "^0.17.0"
|
"@lion/form-core": "^0.17.1"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"form",
|
"form",
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,9 @@ const ListboxMixinImplementation = superclass =>
|
||||||
reflect: true,
|
reflect: true,
|
||||||
attribute: 'has-no-default-selected',
|
attribute: 'has-no-default-selected',
|
||||||
},
|
},
|
||||||
|
_noTypeAhead: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,7 +273,16 @@ const ListboxMixinImplementation = superclass =>
|
||||||
* See: https://www.w3.org/TR/wai-aria-practices/#kbd_selection_follows_focus
|
* See: https://www.w3.org/TR/wai-aria-practices/#kbd_selection_follows_focus
|
||||||
*/
|
*/
|
||||||
this.selectionFollowsFocus = false;
|
this.selectionFollowsFocus = false;
|
||||||
|
/**
|
||||||
|
* When false, a user can type on which the focus will jump to the matching option
|
||||||
|
*/
|
||||||
|
this._noTypeAhead = false;
|
||||||
|
/**
|
||||||
|
* The pending char sequence that will set active list item
|
||||||
|
* @type {number}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
this._typeAheadTimeout = 1000;
|
||||||
/**
|
/**
|
||||||
* @type {number | null}
|
* @type {number | null}
|
||||||
* @protected
|
* @protected
|
||||||
|
|
@ -327,6 +339,11 @@ const ListboxMixinImplementation = superclass =>
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.__preventScrollingWithArrowKeys = this.__preventScrollingWithArrowKeys.bind(this);
|
this.__preventScrollingWithArrowKeys = this.__preventScrollingWithArrowKeys.bind(this);
|
||||||
|
/**
|
||||||
|
* @type {string[]}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.__typedChars = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
|
@ -466,6 +483,39 @@ const ListboxMixinImplementation = superclass =>
|
||||||
this.resetInteractionState();
|
this.resetInteractionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {KeyboardEvent} ev
|
||||||
|
* @param {{setAsChecked:boolean}} options
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_handleTypeAhead(ev, { setAsChecked }) {
|
||||||
|
const { key, code } = ev;
|
||||||
|
|
||||||
|
if (code.startsWith('Key') || code.startsWith('Digit') || code.startsWith('Numpad')) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.__typedChars.push(key);
|
||||||
|
const chars = this.__typedChars.join('');
|
||||||
|
const matchedItemIndex =
|
||||||
|
// TODO: consider making this condition overridable for Subclassers by extracting it into protected method
|
||||||
|
this.formElements.findIndex(el => el.modelValue.value.toLowerCase().startsWith(chars));
|
||||||
|
if (matchedItemIndex >= 0) {
|
||||||
|
if (setAsChecked) {
|
||||||
|
this.setCheckedIndex(matchedItemIndex);
|
||||||
|
}
|
||||||
|
this.activeIndex = matchedItemIndex;
|
||||||
|
}
|
||||||
|
if (this.__pendingTypeAheadTimeout) {
|
||||||
|
// Prevent that pending timeouts 'intersect' with new 'typeahead sessions'
|
||||||
|
// @ts-ignore
|
||||||
|
window.clearTimeout(this.__pendingTypeAheadTimeout);
|
||||||
|
}
|
||||||
|
this.__pendingTypeAheadTimeout = setTimeout(() => {
|
||||||
|
// schedule a timeout to reset __typedChars
|
||||||
|
this.__typedChars = [];
|
||||||
|
}, this._typeAheadTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override ChoiceGroupMixin: in the select disabled options are still going to a possible
|
* @override ChoiceGroupMixin: in the select disabled options are still going to a possible
|
||||||
* value, for example when prefilling or programmatically setting it.
|
* value, for example when prefilling or programmatically setting it.
|
||||||
|
|
@ -621,7 +671,12 @@ const ListboxMixinImplementation = superclass =>
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.activeIndex = this._getPreviousEnabledOption(this.formElements.length - 1, 0);
|
this.activeIndex = this._getPreviousEnabledOption(this.formElements.length - 1, 0);
|
||||||
break;
|
break;
|
||||||
/* no default */
|
default:
|
||||||
|
if (!this._noTypeAhead) {
|
||||||
|
this._handleTypeAhead(ev, {
|
||||||
|
setAsChecked: this.selectionFollowsFocus && !this.multipleChoice,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'];
|
const keys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'];
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,11 @@ const fixture = /** @type {(arg: TemplateResult) => Promise<LionListbox>} */ (_f
|
||||||
/**
|
/**
|
||||||
* @param {HTMLElement} el
|
* @param {HTMLElement} el
|
||||||
* @param {string} key
|
* @param {string} key
|
||||||
|
* @param {string} code
|
||||||
*/
|
*/
|
||||||
function mimicKeyPress(el, key) {
|
function mimicKeyPress(el, key, code = '') {
|
||||||
el.dispatchEvent(new KeyboardEvent('keydown', { key }));
|
el.dispatchEvent(new KeyboardEvent('keydown', { key, code }));
|
||||||
el.dispatchEvent(new KeyboardEvent('keyup', { key }));
|
el.dispatchEvent(new KeyboardEvent('keyup', { key, code }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -381,8 +382,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
// TODO: enable when native button is not a child anymore
|
it('[axe]: is accessible when opened', async () => {
|
||||||
it.skip('[axe]: is accessible when opened', async () => {
|
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag} label="age" opened>
|
<${tag} label="age" opened>
|
||||||
<${optionTag} .choiceValue=${10}>Item 1</${optionTag}>
|
<${optionTag} .choiceValue=${10}>Item 1</${optionTag}>
|
||||||
|
|
@ -396,14 +396,13 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// NB: regular listbox is always 'opened', but needed for combobox and select-rich
|
// NB: regular listbox is always 'opened', but needed for combobox and select-rich
|
||||||
// TODO: enable when native button is not a child anymore
|
it('[axe]: is accessible when closed', async () => {
|
||||||
it.skip('[axe]: is accessible when closed', async () => {
|
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag} label="age">
|
<${tag} label="age">
|
||||||
<${optionTag} .choiceValue=${10}>Item 1</${optionTag}>
|
<${optionTag} .choiceValue=${10}>Item 1</${optionTag}>
|
||||||
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -442,7 +441,6 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(_activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal('first');
|
expect(_activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal('first');
|
||||||
mimicKeyPress(_activeDescendantOwnerNode, 'ArrowDown');
|
mimicKeyPress(_activeDescendantOwnerNode, 'ArrowDown');
|
||||||
// _activeDescendantOwnerNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(_activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal('second');
|
expect(_activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal('second');
|
||||||
});
|
});
|
||||||
|
|
@ -601,7 +599,6 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
// Normalize
|
// Normalize
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
// mimicKeyPress(listbox, 'ArrowUp');
|
|
||||||
|
|
||||||
mimicKeyPress(_listboxNode, 'ArrowUp');
|
mimicKeyPress(_listboxNode, 'ArrowUp');
|
||||||
|
|
||||||
|
|
@ -609,7 +606,6 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
expect(options[1].active).to.be.false;
|
expect(options[1].active).to.be.false;
|
||||||
expect(options[2].active).to.be.false;
|
expect(options[2].active).to.be.false;
|
||||||
el.activeIndex = 2;
|
el.activeIndex = 2;
|
||||||
// mimicKeyPress(listbox, 'ArrowDown');
|
|
||||||
mimicKeyPress(_listboxNode, 'ArrowDown');
|
mimicKeyPress(_listboxNode, 'ArrowDown');
|
||||||
|
|
||||||
expect(options[0].active).to.be.false;
|
expect(options[0].active).to.be.false;
|
||||||
|
|
@ -709,32 +705,138 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// TODO: add key combinations like shift+home/ctrl+A etc etc.
|
// TODO: add key combinations like shift+home/ctrl+A etc etc.
|
||||||
// TODO: nice to have. Get from menu impl.
|
|
||||||
it.skip('selects a value with single [character] key', async () => {
|
describe('Typeahead', () => {
|
||||||
const el = await fixture(html`
|
it('activates a value with single [character] key', async () => {
|
||||||
<${tag} opened>
|
const el = await fixture(html`
|
||||||
<${optionTag} .choiceValue=${'a'}>A</${optionTag}>
|
<${tag} opened id="color" name="color" label="Favorite color">
|
||||||
<${optionTag} .choiceValue=${'b'}>B</${optionTag}>
|
<${optionTag} .choiceValue=${'red'}>Red</${optionTag}>
|
||||||
<${optionTag} .choiceValue=${'c'}>C</${optionTag}>
|
<${optionTag} .choiceValue=${'teal'}>Teal</${optionTag}>
|
||||||
</${tag}>
|
<${optionTag} .choiceValue=${'turquoise'}>Turquoise</${optionTag}>
|
||||||
`);
|
</${tag}>
|
||||||
expect(el.modelValue).to.equal('a');
|
`);
|
||||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'C' }));
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
expect(el.modelValue).to.equal('c');
|
if (el._noTypeAhead) {
|
||||||
});
|
return;
|
||||||
it.skip('selects a value with multiple [character] keys', async () => {
|
}
|
||||||
const el = await fixture(html`
|
|
||||||
<${tag} opened>
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
<${optionTag} .choiceValue=${'bar'}>Bar</${optionTag}>
|
|
||||||
<${optionTag} .choiceValue=${'far'}>Far</${optionTag}>
|
// Normalize start values between listbox, select and combobox and test interaction below
|
||||||
<${optionTag} .choiceValue=${'foo'}>Foo</${optionTag}>
|
el.activeIndex = 0;
|
||||||
</${tag}>
|
|
||||||
`);
|
mimicKeyPress(_listboxNode, 't', 'KeyT');
|
||||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'F' }));
|
// await aTimeout(0);
|
||||||
expect(el.modelValue).to.equal('far');
|
expect(el.activeIndex).to.equal(1);
|
||||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'O' }));
|
});
|
||||||
expect(el.modelValue).to.equal('foo');
|
|
||||||
|
it('activates a value with multiple [character] keys', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag} opened id="color" name="color" label="Favorite color">
|
||||||
|
<${optionTag} .choiceValue=${'red'}>Red</${optionTag}>
|
||||||
|
<${optionTag} .choiceValue=${'teal'}>Teal</${optionTag}>
|
||||||
|
<${optionTag} .choiceValue=${'turquoise'}>Turquoise</${optionTag}>
|
||||||
|
</${tag}>
|
||||||
|
`);
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
if (el._noTypeAhead) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
|
// Normalize start values between listbox, select and combobox and test interaction below
|
||||||
|
el.activeIndex = 0;
|
||||||
|
|
||||||
|
mimicKeyPress(_listboxNode, 't', 'KeyT');
|
||||||
|
expect(el.activeIndex).to.equal(1);
|
||||||
|
|
||||||
|
mimicKeyPress(_listboxNode, 'u', 'KeyU');
|
||||||
|
expect(el.activeIndex).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects a value with [character] keys and selectionFollowsFocus', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag} opened id="color" name="color" label="Favorite color" selection-follows-focus>
|
||||||
|
<${optionTag} .choiceValue=${'red'}>Red</${optionTag}>
|
||||||
|
<${optionTag} .choiceValue=${'teal'}>Teal</${optionTag}>
|
||||||
|
<${optionTag} .choiceValue=${'turquoise'}>Turquoise</${optionTag}>
|
||||||
|
</${tag}>
|
||||||
|
`);
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
if (el._noTypeAhead) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
|
// Normalize start values between listbox, select and combobox and test interaction below
|
||||||
|
el.checkedIndex = 0;
|
||||||
|
|
||||||
|
mimicKeyPress(_listboxNode, 't', 'KeyT');
|
||||||
|
expect(el.checkedIndex).to.equal(1);
|
||||||
|
|
||||||
|
mimicKeyPress(_listboxNode, 'u', 'KeyU');
|
||||||
|
expect(el.checkedIndex).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears typedChars after _typeAheadTimeout', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag} opened id="color" name="color" label="Favorite color">
|
||||||
|
<${optionTag} .choiceValue=${'red'}>Red</${optionTag}>
|
||||||
|
<${optionTag} .choiceValue=${'teal'}>Teal</${optionTag}>
|
||||||
|
<${optionTag} .choiceValue=${'turquoise'}>turquoise</${optionTag}>
|
||||||
|
</${tag}>
|
||||||
|
`);
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
if (el._noTypeAhead) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clock = sinon.useFakeTimers();
|
||||||
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
|
mimicKeyPress(_listboxNode, 't', 'KeyT');
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
|
expect(el.__typedChars).to.deep.equal(['t']);
|
||||||
|
|
||||||
|
mimicKeyPress(_listboxNode, 'u', 'KeyU');
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
|
expect(el.__typedChars).to.deep.equal(['t', 'u']);
|
||||||
|
|
||||||
|
clock.tick(1000);
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
|
expect(el.__typedChars).to.deep.equal([]);
|
||||||
|
|
||||||
|
clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears scheduled timeouts', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag} opened id="color" name="color" label="Favorite color">
|
||||||
|
<${optionTag} .choiceValue=${'red'}>Red</${optionTag}>
|
||||||
|
<${optionTag} .choiceValue=${'teal'}>Teal</${optionTag}>
|
||||||
|
<${optionTag} .choiceValue=${'turquoise'}>Turquoise</${optionTag}>
|
||||||
|
</${tag}>
|
||||||
|
`);
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
if (el._noTypeAhead) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
|
// Normalize start values between listbox, select and combobox and test interaction below
|
||||||
|
el.activeIndex = 0;
|
||||||
|
mimicKeyPress(_listboxNode, 't', 'KeyT');
|
||||||
|
// @ts-expect-error [allow-private-in-tests]
|
||||||
|
const pendingClear = el.__pendingTypeAheadTimeout;
|
||||||
|
const clearTimeoutSpy = sinon.spy(window, 'clearTimeout');
|
||||||
|
mimicKeyPress(_listboxNode, 'u', 'KeyU');
|
||||||
|
expect(clearTimeoutSpy.args[0][0]).to.equal(pendingClear);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('navigates to first and last option with [Home] and [End] keys', async () => {
|
it('navigates to first and last option with [Home] and [End] keys', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag} opened>
|
<${tag} opened>
|
||||||
|
|
@ -1021,7 +1123,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
|
|
||||||
const { _listboxNode } = getListboxMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
// Normalize start values between listbox, slect and combobox and test interaction below
|
// Normalize start values between listbox, select and combobox and test interaction below
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
el.checkedIndex = 0;
|
el.checkedIndex = 0;
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,14 @@ export declare class ListboxHost {
|
||||||
|
|
||||||
protected _listboxReceivesNoFocus: boolean;
|
protected _listboxReceivesNoFocus: boolean;
|
||||||
|
|
||||||
|
protected _noTypeAhead: boolean;
|
||||||
|
|
||||||
protected _uncheckChildren(): void;
|
protected _uncheckChildren(): void;
|
||||||
|
|
||||||
private __setupListboxNode(): void;
|
private __setupListboxNode(): void;
|
||||||
|
|
||||||
|
protected _handleTypeAhead(ev: KeyboardEvent, { setAsChecked: boolean }): void;
|
||||||
|
|
||||||
protected _getPreviousEnabledOption(currentIndex: number, offset?: number): number;
|
protected _getPreviousEnabledOption(currentIndex: number, offset?: number): number;
|
||||||
|
|
||||||
protected _getNextEnabledOption(currentIndex: number, offset?: number): number;
|
protected _getNextEnabledOption(currentIndex: number, offset?: number): number;
|
||||||
|
|
@ -78,6 +82,8 @@ export declare class ListboxHost {
|
||||||
protected get _activeDescendantOwnerNode(): HTMLElement;
|
protected get _activeDescendantOwnerNode(): HTMLElement;
|
||||||
|
|
||||||
protected _onListboxContentChanged(): void;
|
protected _onListboxContentChanged(): void;
|
||||||
|
|
||||||
|
private __pendingTypeAheadTimeout: number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function ListboxImplementation<T extends Constructor<LitElement>>(
|
export declare function ListboxImplementation<T extends Constructor<LitElement>>(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 0.30.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- a28686ee: Add TypeAhead, so with typing characters you will set an option with matching value active/checked
|
||||||
|
- Updated dependencies [a28686ee]
|
||||||
|
- Updated dependencies [43dd1320]
|
||||||
|
- @lion/listbox@0.13.1
|
||||||
|
- @lion/form-core@0.17.1
|
||||||
|
|
||||||
## 0.30.0
|
## 0.30.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@lion/select-rich",
|
"name": "@lion/select-rich",
|
||||||
"version": "0.30.0",
|
"version": "0.30.1",
|
||||||
"description": "Provides a select with options that can contain html",
|
"description": "Provides a select with options that can contain html",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "ing-bank",
|
"author": "ing-bank",
|
||||||
|
|
@ -42,8 +42,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lion/button": "^0.17.0",
|
"@lion/button": "^0.17.0",
|
||||||
"@lion/core": "^0.22.0",
|
"@lion/core": "^0.22.0",
|
||||||
"@lion/form-core": "^0.17.0",
|
"@lion/form-core": "^0.17.1",
|
||||||
"@lion/listbox": "^0.13.0",
|
"@lion/listbox": "^0.13.1",
|
||||||
"@lion/overlays": "^0.32.0"
|
"@lion/overlays": "^0.32.0"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
||||||
|
|
@ -489,7 +489,10 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
this.opened = true;
|
this.opened = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
/* no default */
|
default:
|
||||||
|
if (!this._noTypeAhead) {
|
||||||
|
this._handleTypeAhead(ev, { setAsChecked: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -514,7 +517,6 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
// Tab can only be caught in keydown
|
// Tab can only be caught in keydown
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
break;
|
break;
|
||||||
/* no default */
|
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
this.__blockListShowDuringTransition();
|
this.__blockListShowDuringTransition();
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,16 @@ import '@lion/select-rich/define';
|
||||||
* @typedef {import('@lion/listbox').LionOption} LionOption
|
* @typedef {import('@lion/listbox').LionOption} LionOption
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} el
|
||||||
|
* @param {string} key
|
||||||
|
* @param {string} code
|
||||||
|
*/
|
||||||
|
function mimicKeyPress(el, key, code = '') {
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keydown', { key, code }));
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keyup', { key, code }));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {LionSelectRich} lionSelectEl
|
* @param {LionSelectRich} lionSelectEl
|
||||||
*/
|
*/
|
||||||
|
|
@ -142,6 +152,27 @@ describe('lion-select-rich interactions', () => {
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
expectOnlyGivenOneOptionToBeChecked(options, 0);
|
expectOnlyGivenOneOptionToBeChecked(options, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('checkes a value with [character] keys while listbox unopened', async () => {
|
||||||
|
const el = /** @type {LionSelectRich} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<lion-select-rich interaction-mode="windows/linux">
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${'red'}>Red</lion-option>
|
||||||
|
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
||||||
|
<lion-option .choiceValue=${'turquoise'}>Turquoise</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
|
mimicKeyPress(el, 't', 'KeyT');
|
||||||
|
expect(el.checkedIndex).to.equal(1);
|
||||||
|
|
||||||
|
mimicKeyPress(el, 'u', 'KeyU');
|
||||||
|
expect(el.checkedIndex).to.equal(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Disabled', () => {
|
describe('Disabled', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 0.20.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 355aabc0: fix(switch) unregister on disconnectedCallback
|
||||||
|
- Updated dependencies [43dd1320]
|
||||||
|
- @lion/form-core@0.17.1
|
||||||
|
|
||||||
## 0.20.0
|
## 0.20.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@lion/switch",
|
"name": "@lion/switch",
|
||||||
"version": "0.20.0",
|
"version": "0.20.1",
|
||||||
"description": "A Switch is used for switching a property or feature on and off",
|
"description": "A Switch is used for switching a property or feature on and off",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "ing-bank",
|
"author": "ing-bank",
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lion/core": "^0.22.0",
|
"@lion/core": "^0.22.0",
|
||||||
"@lion/form-core": "^0.17.0",
|
"@lion/form-core": "^0.17.1",
|
||||||
"@lion/helpers": "^0.11.0"
|
"@lion/helpers": "^0.11.0"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const yarnLockPath = './yarn.lock';
|
const lockFileContent = fs.readFileSync(path.resolve('./yarn.lock'), 'utf8');
|
||||||
const data = fs.readFileSync(path.resolve(yarnLockPath), 'utf8');
|
|
||||||
if (data.match(/artifactory/g)) {
|
const allowedRegistries = ['registry.yarnpkg.com', 'registry.npmjs.org'];
|
||||||
throw new Error(
|
const resolvedUrls = lockFileContent.match(/"https:.*"/g);
|
||||||
'Artifactory references in your yarn.lock! Please make sure you are using a public npm registry when downloading your dependencies!',
|
resolvedUrls.forEach(url => {
|
||||||
);
|
const [, registry] = url.match(/^"https:\/\/(.*?)\/.*"$/) || [];
|
||||||
}
|
if (!allowedRegistries.includes(registry)) {
|
||||||
|
throw new Error(
|
||||||
|
`Disallowed registries ("${registry}") in your yarn.lock!
|
||||||
|
Please make sure you are using a public npm registry when downloading your dependencies!`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -3430,6 +3430,11 @@ autosize@4.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.2.tgz#073cfd07c8bf45da4b9fd153437f5bafbba1e4c9"
|
resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.2.tgz#073cfd07c8bf45da4b9fd153437f5bafbba1e4c9"
|
||||||
integrity sha512-jnSyH2d+qdfPGpWlcuhGiHmqBJ6g3X+8T+iRwFrHPLVcdoGJE/x6Qicm6aDHfTsbgZKxyV8UU/YB2p4cjKDRRA==
|
integrity sha512-jnSyH2d+qdfPGpWlcuhGiHmqBJ6g3X+8T+iRwFrHPLVcdoGJE/x6Qicm6aDHfTsbgZKxyV8UU/YB2p4cjKDRRA==
|
||||||
|
|
||||||
|
awesome-phonenumber@3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/awesome-phonenumber/-/awesome-phonenumber-3.0.1.tgz#8d73aaa1c2b0a660b117567b0d9797623457e1d0"
|
||||||
|
integrity sha512-H5rqjTJ1+HxmyuSKDoPgvHUgP+RBRhtWQ25ccy4BmSLQL5UVg3K+yo2QCX4IlkxiVNst3suGMArV9TH7B1KEPw==
|
||||||
|
|
||||||
axe-core@^4.3.3:
|
axe-core@^4.3.3:
|
||||||
version "4.3.3"
|
version "4.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.3.tgz#b55cd8e8ddf659fe89b064680e1c6a4dceab0325"
|
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.3.tgz#b55cd8e8ddf659fe89b064680e1c6a4dceab0325"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue