@descope-ui/descope-password-strength 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/e2e/descope-password-strength.spec.ts +49 -0
- package/package.json +35 -0
- package/project.json +17 -0
- package/src/component/PasswordStrengthClass.js +162 -0
- package/src/component/calcScore.js +16 -0
- package/src/component/index.js +5 -0
- package/src/theme.js +52 -0
- package/stories/descope-password-strength.stories.js +29 -0
- package/testDriver/passwordStrengthTestDriver.ts +11 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
|
|
4
|
+
|
|
5
|
+
## 0.0.1 (2025-04-06)
|
|
6
|
+
|
|
7
|
+
### Dependency Updates
|
|
8
|
+
|
|
9
|
+
* `e2e-utils` updated to version `0.0.1`
|
|
10
|
+
* `test-drivers` updated to version `0.0.1`
|
|
11
|
+
* `@descope-ui/common` updated to version `0.0.9`
|
|
12
|
+
* `@descope-ui/theme-globals` updated to version `0.0.9`
|
|
13
|
+
* `@descope-ui/theme-input-wrapper` updated to version `0.0.9`
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { getStoryUrl, loopConfig, loopPresets } from 'e2e-utils';
|
|
3
|
+
import createPasswordStrengthTestDriver from '../testDriver/passwordStrengthTestDriver';
|
|
4
|
+
|
|
5
|
+
const componentAttributes = {
|
|
6
|
+
fullWidth: ['true', 'false'],
|
|
7
|
+
password: ['', '1', '1Qaz', '1QaZ2Wse', '1QaZ2Wse3E', '1QaZ2Wse3EdC']
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const presets = {
|
|
11
|
+
'direction rtl': {
|
|
12
|
+
direction: 'rtl',
|
|
13
|
+
password: '1',
|
|
14
|
+
},
|
|
15
|
+
'full width': {
|
|
16
|
+
fullWidth: 'true',
|
|
17
|
+
password: '1',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const storyName = 'descope-password-strength';
|
|
22
|
+
const componentName = 'descope-password-strength';
|
|
23
|
+
|
|
24
|
+
test.describe('theme', () => {
|
|
25
|
+
loopConfig(componentAttributes, (attr, value) => {
|
|
26
|
+
test.describe(`${attr}: ${value}`, () => {
|
|
27
|
+
test.beforeEach(async ({ page }) => {
|
|
28
|
+
await page.goto(getStoryUrl(storyName, { [attr]: value }), { waitUntil: 'load' });
|
|
29
|
+
});
|
|
30
|
+
test('style', async ({ page }) => {
|
|
31
|
+
await page.waitForSelector(componentName);
|
|
32
|
+
const component = createPasswordStrengthTestDriver(page.locator(componentName));
|
|
33
|
+
|
|
34
|
+
expect(await component.screenshot({animations: 'disabled', delay: 5000, timeout: 5000})).toMatchSnapshot();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
loopPresets(presets, (preset, name) => {
|
|
40
|
+
test(name, async ({ page }) => {
|
|
41
|
+
await page.goto(getStoryUrl(storyName, preset));
|
|
42
|
+
await page.waitForSelector(componentName);
|
|
43
|
+
const component = createPasswordStrengthTestDriver(page.locator(componentName));
|
|
44
|
+
expect(
|
|
45
|
+
await component.screenshot({ animations: 'disabled', delay: 5000, timeout: 5000 })
|
|
46
|
+
).toMatchSnapshot();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@descope-ui/descope-password-strength",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": {
|
|
6
|
+
"import": "./src/component/index.js"
|
|
7
|
+
},
|
|
8
|
+
"./theme": {
|
|
9
|
+
"import": "./src/theme.js"
|
|
10
|
+
},
|
|
11
|
+
"./class": {
|
|
12
|
+
"import": "./src/component/PasswordStrengthClass.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@playwright/test": "1.38.1",
|
|
17
|
+
"e2e-utils": "0.0.1",
|
|
18
|
+
"test-drivers": "0.0.1"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@zxcvbn-ts/core": "^3.0.4",
|
|
22
|
+
"@zxcvbn-ts/language-common": "^3.0.4",
|
|
23
|
+
"@zxcvbn-ts/language-en": "^3.0.2",
|
|
24
|
+
"@descope-ui/common": "0.0.9",
|
|
25
|
+
"@descope-ui/theme-globals": "0.0.9",
|
|
26
|
+
"@descope-ui/theme-input-wrapper": "0.0.9"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"link-workspace-packages": false
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "echo 'No tests defined' && exit 0",
|
|
33
|
+
"test:e2e": "echo 'No e2e tests defined' && exit 0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@descope-ui/descope-password-strength",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "packages/web-components/components/descope-password-strength/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"targets": {
|
|
7
|
+
"version": {
|
|
8
|
+
"executor": "@jscutlery/semver:version",
|
|
9
|
+
"options": {
|
|
10
|
+
"trackDeps": true,
|
|
11
|
+
"push": false,
|
|
12
|
+
"preset": "conventional"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"tags": []
|
|
17
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createBaseClass } from '@descope-ui/common/base-classes';
|
|
2
|
+
import {
|
|
3
|
+
getComponentName,
|
|
4
|
+
injectStyle
|
|
5
|
+
} from '@descope-ui/common/components-helpers';
|
|
6
|
+
import {
|
|
7
|
+
componentNameValidationMixin,
|
|
8
|
+
createStyleMixin,
|
|
9
|
+
draggableMixin,
|
|
10
|
+
} from '@descope-ui/common/components-mixins';
|
|
11
|
+
import { compose } from '@descope-ui/common/utils';
|
|
12
|
+
|
|
13
|
+
export const componentName = getComponentName('password-strength');
|
|
14
|
+
class RawPasswordStrength extends createBaseClass({
|
|
15
|
+
componentName,
|
|
16
|
+
baseSelector: ':host > .wrapper',
|
|
17
|
+
}) {
|
|
18
|
+
static get observedAttributes() {
|
|
19
|
+
return ['options', 'value'];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
super();
|
|
24
|
+
|
|
25
|
+
this.attachShadow({ mode: 'open' }).innerHTML = `
|
|
26
|
+
<div class="wrapper">
|
|
27
|
+
<div class="bars-container"></div>
|
|
28
|
+
<div class="text-container">
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
injectStyle(`
|
|
34
|
+
:host {
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.wrapper {
|
|
39
|
+
display: inline-flex;
|
|
40
|
+
position: relative;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
width: 100%;
|
|
43
|
+
height: 100%;
|
|
44
|
+
align-items: flex-start;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.bar {
|
|
48
|
+
flex-grow: 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.bars-container {
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-grow: 1;
|
|
54
|
+
width: 100%;
|
|
55
|
+
}
|
|
56
|
+
`, this);
|
|
57
|
+
|
|
58
|
+
this.barsContainer = this.shadowRoot.querySelector('.bars-container');
|
|
59
|
+
this.textContainer = this.shadowRoot.querySelector('.text-container');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get options() {
|
|
63
|
+
return this.getAttribute('options')?.split(',') || [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// we need to have the option to take the zxcvbn from outside the component
|
|
67
|
+
getCalcScoreFn = () => import('./calcScore').then(module => module.default);
|
|
68
|
+
|
|
69
|
+
#score = -1;
|
|
70
|
+
|
|
71
|
+
async calcScore() {
|
|
72
|
+
const value = this.getAttribute('value') || ''
|
|
73
|
+
if (!value) return -1;
|
|
74
|
+
|
|
75
|
+
const calcScore = await this.getCalcScoreFn();
|
|
76
|
+
|
|
77
|
+
return calcScore(value).score;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
renderBars() {
|
|
81
|
+
this.barsContainer.innerHTML = '';
|
|
82
|
+
this.bars = this.options.map((option, index) => {
|
|
83
|
+
const bar = document.createElement('div');
|
|
84
|
+
bar.classList.add('bar');
|
|
85
|
+
bar.classList.toggle('filled', index <= this.#score);
|
|
86
|
+
this.barsContainer.appendChild(bar);
|
|
87
|
+
|
|
88
|
+
return bar;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
init() {
|
|
93
|
+
super.init?.();
|
|
94
|
+
// we are prefetching the calcScore function
|
|
95
|
+
this.getCalcScoreFn();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async onValueChange() {
|
|
100
|
+
const newScore = await this.calcScore();
|
|
101
|
+
if (newScore === this.#score) return;
|
|
102
|
+
|
|
103
|
+
this.#score = newScore;
|
|
104
|
+
this.dispatchEvent(new CustomEvent('score-changed', { bubbles: true, composed: true, detail: this.#score }));
|
|
105
|
+
|
|
106
|
+
this.bars?.forEach((bar, index) => {
|
|
107
|
+
bar.classList.toggle('filled', index <= this.#score);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this.textContainer.innerHTML = this.options[this.#score] || '';
|
|
111
|
+
|
|
112
|
+
if (this.#score === -1) this.removeAttribute('score');
|
|
113
|
+
else this.setAttribute('score', this.#score);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
117
|
+
if (oldValue === newValue) return;
|
|
118
|
+
|
|
119
|
+
if (name === 'options') {
|
|
120
|
+
return this.renderBars();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (name === 'value') {
|
|
124
|
+
return this.onValueChange();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { host, bar, filledBar, barsContainer, text } = {
|
|
130
|
+
host: { selector: () => ':host' },
|
|
131
|
+
bar: { selector: ' .bar' },
|
|
132
|
+
barsContainer: { selector: ' .bars-container' },
|
|
133
|
+
filledBar: { selector: ' .bar.filled' },
|
|
134
|
+
text: { selector: ' .text-container' },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const PasswordStrengthClass = compose(
|
|
138
|
+
createStyleMixin({ componentNameOverride: getComponentName('input-wrapper') }),
|
|
139
|
+
createStyleMixin({
|
|
140
|
+
mappings: {
|
|
141
|
+
hostWidth: [
|
|
142
|
+
{ ...host, property: 'width' },
|
|
143
|
+
{ ...host, property: 'min-width' },
|
|
144
|
+
],
|
|
145
|
+
hostDirection: { ...host, property: 'direction' },
|
|
146
|
+
hostMinWidth: { ...host, property: 'min-width' },
|
|
147
|
+
barHeight: { ...bar, property: 'height' },
|
|
148
|
+
barBorderRadius: { ...bar, property: 'border-radius' },
|
|
149
|
+
emptyBarColor: { ...bar, property: 'background-color' },
|
|
150
|
+
filledBarColor: { ...filledBar, property: 'background-color' },
|
|
151
|
+
textColor: { ...text, property: 'color' },
|
|
152
|
+
spacing: { property: 'gap' },
|
|
153
|
+
barSpacing: { ...barsContainer, property: 'gap' },
|
|
154
|
+
fontSize: {},
|
|
155
|
+
fontWeight: {},
|
|
156
|
+
fontFamily: {},
|
|
157
|
+
barTransition: { ...bar, property: 'transition' },
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
draggableMixin,
|
|
161
|
+
componentNameValidationMixin,
|
|
162
|
+
)(RawPasswordStrength);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { zxcvbn, zxcvbnOptions } from "@zxcvbn-ts/core";
|
|
2
|
+
import * as zxcvbnCommonPackage from "@zxcvbn-ts/language-common";
|
|
3
|
+
import * as zxcvbnEnPackage from "@zxcvbn-ts/language-en";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export const options = {
|
|
7
|
+
translations: zxcvbnEnPackage.translations,
|
|
8
|
+
graphs: zxcvbnCommonPackage.adjacencyGraphs,
|
|
9
|
+
dictionary: {
|
|
10
|
+
...zxcvbnCommonPackage.dictionary,
|
|
11
|
+
...zxcvbnEnPackage.dictionary,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
zxcvbnOptions.setOptions(options);
|
|
15
|
+
|
|
16
|
+
export default zxcvbn
|
package/src/theme.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import globals from '@descope-ui/theme-globals';
|
|
2
|
+
import { PasswordStrengthClass } from './component/PasswordStrengthClass';
|
|
3
|
+
import { getThemeRefs } from '@descope-ui/common/theme-helpers';
|
|
4
|
+
import { refs } from '@descope-ui/theme-input-wrapper';
|
|
5
|
+
|
|
6
|
+
const globalRefs = getThemeRefs(globals);
|
|
7
|
+
const compVars = PasswordStrengthClass.cssVarList;
|
|
8
|
+
|
|
9
|
+
const passwordStrength = {
|
|
10
|
+
[compVars.hostDirection]: globalRefs.direction,
|
|
11
|
+
[compVars.barHeight]: '4px',
|
|
12
|
+
[compVars.barTransition]: 'background-color 0.3s',
|
|
13
|
+
[compVars.barBorderRadius]: globalRefs.radius.sm,
|
|
14
|
+
[compVars.hostMinWidth]: refs.minWidth,
|
|
15
|
+
[compVars.barSpacing]: globalRefs.spacing.sm,
|
|
16
|
+
[compVars.emptyBarColor]: globalRefs.colors.surface.light,
|
|
17
|
+
[compVars.fontFamily]: refs.fontFamily,
|
|
18
|
+
[compVars.fontSize]: refs.labelFontSize,
|
|
19
|
+
[compVars.fontWeight]: refs.labelFontWeight,
|
|
20
|
+
[compVars.spacing]: globalRefs.spacing.sm,
|
|
21
|
+
_fullWidth: {
|
|
22
|
+
[compVars.hostWidth]: '100%',
|
|
23
|
+
},
|
|
24
|
+
score : {
|
|
25
|
+
0: {
|
|
26
|
+
[compVars.filledBarColor]: globalRefs.colors.error.light,
|
|
27
|
+
[compVars.textColor]: globalRefs.colors.error.light,
|
|
28
|
+
},
|
|
29
|
+
1: {
|
|
30
|
+
[compVars.filledBarColor]: globalRefs.colors.error.main,
|
|
31
|
+
[compVars.textColor]: globalRefs.colors.error.main,
|
|
32
|
+
},
|
|
33
|
+
2: {
|
|
34
|
+
[compVars.filledBarColor]: globalRefs.colors.error.dark,
|
|
35
|
+
[compVars.textColor]: globalRefs.colors.error.dark,
|
|
36
|
+
},
|
|
37
|
+
3: {
|
|
38
|
+
[compVars.filledBarColor]: globalRefs.colors.success.dark,
|
|
39
|
+
[compVars.textColor]: globalRefs.colors.success.dark,
|
|
40
|
+
},
|
|
41
|
+
4: {
|
|
42
|
+
[compVars.filledBarColor]: globalRefs.colors.success.main,
|
|
43
|
+
[compVars.textColor]: globalRefs.colors.success.main,
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default passwordStrength;
|
|
49
|
+
|
|
50
|
+
export const vars = {
|
|
51
|
+
...compVars,
|
|
52
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { componentName } from '../src/component';
|
|
2
|
+
import { directionControl } from '@descope-ui/common/sb-controls';
|
|
3
|
+
|
|
4
|
+
const Template = ({ options, password, direction, fullWidth }) =>
|
|
5
|
+
`
|
|
6
|
+
<descope-password-strength
|
|
7
|
+
value="${password ?? -1}"
|
|
8
|
+
options="${options.join(',') || ''}"
|
|
9
|
+
st-host-direction="${direction ?? ''}"
|
|
10
|
+
full-width="${fullWidth ?? false}"
|
|
11
|
+
></descope-password-strength>
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
component: componentName,
|
|
16
|
+
title: 'descope-password-strength',
|
|
17
|
+
argTypes: {
|
|
18
|
+
...directionControl,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Default = Template.bind({});
|
|
23
|
+
|
|
24
|
+
Default.args = {
|
|
25
|
+
direction: '',
|
|
26
|
+
password: '',
|
|
27
|
+
options: ['Very weak','Weak','Average','Strong','Very strong'],
|
|
28
|
+
fullWidth: false,
|
|
29
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Locator } from '@playwright/test';
|
|
2
|
+
import { createComponentTestDriver } from 'test-drivers';
|
|
3
|
+
|
|
4
|
+
const createPasswordStrengthTestDriver = (locator: Locator) => {
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
...createComponentTestDriver(locator),
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default createPasswordStrengthTestDriver;
|