@descope-ui/descope-timer 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 +14 -0
- package/e2e/descope-timer.spec.ts +68 -0
- package/package.json +32 -0
- package/project.json +17 -0
- package/src/component/TimerClass.js +180 -0
- package/src/component/clock.svg +4 -0
- package/src/component/helpers.js +16 -0
- package/src/component/index.js +6 -0
- package/src/theme.js +38 -0
- package/stories/descope-timer.stories.js +72 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
|
|
4
|
+
|
|
5
|
+
## 0.0.1 (2025-03-11)
|
|
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/descope-icon` updated to version `0.0.4`
|
|
14
|
+
# Changelog
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { getStoryUrl, loopConfig } from 'e2e-utils';
|
|
3
|
+
import { createTimerTestDriver } from 'test-drivers';
|
|
4
|
+
|
|
5
|
+
const componentAttributes = {
|
|
6
|
+
size: ['xs', 'sm', 'md', 'lg'],
|
|
7
|
+
'hide-icon': ['true', 'false'],
|
|
8
|
+
'full-width': ['true', 'false'],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const storyName = 'descope-timer';
|
|
12
|
+
const componentName = 'descope-timer';
|
|
13
|
+
|
|
14
|
+
test.describe('theme', () => {
|
|
15
|
+
loopConfig(componentAttributes, (attr, value) => {
|
|
16
|
+
test(`${attr}: ${value}`, async ({ page }) => {
|
|
17
|
+
await page.goto(getStoryUrl(storyName, { [attr]: value }), {
|
|
18
|
+
waitUntil: 'load',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const component = createTimerTestDriver(page.locator(componentName));
|
|
22
|
+
await component.pause();
|
|
23
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
['left', 'center', 'right'].forEach((alignment) => {
|
|
28
|
+
test(`text-align: ${alignment}`, async ({ page }) => {
|
|
29
|
+
await page.goto(
|
|
30
|
+
getStoryUrl(storyName, {
|
|
31
|
+
'text-align': alignment,
|
|
32
|
+
'full-width': 'true',
|
|
33
|
+
}),
|
|
34
|
+
{ waitUntil: 'load' },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const component = createTimerTestDriver(page.locator(componentName));
|
|
38
|
+
await component.pause();
|
|
39
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test.describe('logic', () => {
|
|
45
|
+
test(`reset time when finished`, async ({ page }) => {
|
|
46
|
+
await page.goto(getStoryUrl(storyName, { seconds: 10 }), {
|
|
47
|
+
waitUntil: 'load',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const component = createTimerTestDriver(page.locator(componentName));
|
|
51
|
+
await component.stop();
|
|
52
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
['', '-1', '0', '1', '10', '100', '10000', '100000', '100000000'].forEach(
|
|
56
|
+
(seconds) => {
|
|
57
|
+
test(`time format: ${seconds || 'empty'} seconds`, async ({ page }) => {
|
|
58
|
+
await page.goto(getStoryUrl(storyName, { seconds }), {
|
|
59
|
+
waitUntil: 'load',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const component = createTimerTestDriver(page.locator(componentName));
|
|
63
|
+
await component.pause();
|
|
64
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@descope-ui/descope-timer",
|
|
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/TimerClass.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
|
+
"@descope-ui/common": "0.0.9",
|
|
22
|
+
"@descope-ui/theme-globals": "0.0.9",
|
|
23
|
+
"@descope-ui/descope-icon": "0.0.4"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"link-workspace-packages": false
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "echo 'No tests defined' && exit 0",
|
|
30
|
+
"test:e2e": "echo 'No e2e tests defined' && exit 0"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@descope-ui/descope-timer",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "packages/web-components/components/descope-timer/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,180 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createStyleMixin,
|
|
3
|
+
draggableMixin,
|
|
4
|
+
componentNameValidationMixin,
|
|
5
|
+
} from '@descope-ui/common/components-mixins';
|
|
6
|
+
import { createBaseClass } from '@descope-ui/common/base-classes';
|
|
7
|
+
import { compose } from '@descope-ui/common/utils';
|
|
8
|
+
import {
|
|
9
|
+
getComponentName,
|
|
10
|
+
injectStyle,
|
|
11
|
+
} from '@descope-ui/common/components-helpers';
|
|
12
|
+
import clockIcon from './clock.svg';
|
|
13
|
+
import { IconClass } from '@descope-ui/descope-icon/class';
|
|
14
|
+
import { formatTime } from './helpers';
|
|
15
|
+
|
|
16
|
+
export const componentName = getComponentName('timer');
|
|
17
|
+
|
|
18
|
+
const observedAttributes = ['seconds', 'hide-icon'];
|
|
19
|
+
|
|
20
|
+
const BaseClass = createBaseClass({
|
|
21
|
+
componentName,
|
|
22
|
+
baseSelector: ':host > .wrapper',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const DEFAULT_INTERVAL = 1000;
|
|
26
|
+
|
|
27
|
+
class RawTimer extends BaseClass {
|
|
28
|
+
#timeRemains = 0;
|
|
29
|
+
|
|
30
|
+
#intervalId;
|
|
31
|
+
|
|
32
|
+
static get observedAttributes() {
|
|
33
|
+
return observedAttributes.concat(BaseClass.observedAttributes || []);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
constructor() {
|
|
37
|
+
super();
|
|
38
|
+
|
|
39
|
+
this.attachShadow({ mode: 'open' }).innerHTML = `
|
|
40
|
+
<div class="wrapper">
|
|
41
|
+
<descope-icon src=${clockIcon} class="icon"></descope-icon>
|
|
42
|
+
<div class="timer"></div>
|
|
43
|
+
</div>
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
injectStyle(
|
|
47
|
+
`
|
|
48
|
+
:host {
|
|
49
|
+
display: inline-flex;
|
|
50
|
+
user-select: none;
|
|
51
|
+
-webkit-user-select: none;
|
|
52
|
+
}
|
|
53
|
+
.wrapper {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
flex-direction: row;
|
|
57
|
+
width: 100%;
|
|
58
|
+
}
|
|
59
|
+
.hidden {
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
`,
|
|
63
|
+
this,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
this.icon = this.shadowRoot.querySelector('.icon');
|
|
67
|
+
this.timer = this.shadowRoot.querySelector('.timer');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get seconds() {
|
|
71
|
+
const val = this.getAttribute('seconds');
|
|
72
|
+
const secs = parseInt(val || 0, 10) * 1000;
|
|
73
|
+
return Math.max(0, secs);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
init() {
|
|
77
|
+
super.init?.();
|
|
78
|
+
|
|
79
|
+
this.#timeRemains = this.seconds;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
start() {
|
|
83
|
+
this.dispatchEvent(new CustomEvent('timer-started', { bubbles: true }));
|
|
84
|
+
|
|
85
|
+
this.startInterval();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
startInterval() {
|
|
89
|
+
const intervalCycle = () => {
|
|
90
|
+
this.#updateDisplay(this.#timeRemains);
|
|
91
|
+
|
|
92
|
+
if (!this.#timeRemains) {
|
|
93
|
+
this.stop();
|
|
94
|
+
} else {
|
|
95
|
+
this.#decreaseInterval();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
intervalCycle();
|
|
100
|
+
|
|
101
|
+
if (this.#timeRemains) {
|
|
102
|
+
this.#intervalId = setInterval(intervalCycle, DEFAULT_INTERVAL);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#decreaseInterval() {
|
|
107
|
+
this.#timeRemains = Math.max(this.#timeRemains - DEFAULT_INTERVAL, 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
reset() {
|
|
111
|
+
this.#timeRemains = this.seconds;
|
|
112
|
+
this.start();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
stop() {
|
|
116
|
+
this.pause();
|
|
117
|
+
this.#timeRemains = 0;
|
|
118
|
+
this.#updateDisplay(this.#timeRemains);
|
|
119
|
+
this.dispatchEvent(new CustomEvent('timer-ended', { bubbles: true }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pause() {
|
|
123
|
+
clearInterval(this.#intervalId);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
resume() {
|
|
127
|
+
this.startInterval();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#updateDisplay(val) {
|
|
131
|
+
this.timer.textContent = formatTime(val);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#toggleIcon(val) {
|
|
135
|
+
this.icon.classList.toggle('hidden', val === 'true');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
attributeChangedCallback(attrName, oldValue, newValue) {
|
|
139
|
+
super.attributeChangedCallback?.(attrName, oldValue, newValue);
|
|
140
|
+
|
|
141
|
+
if (newValue !== oldValue) {
|
|
142
|
+
if (attrName === 'seconds') {
|
|
143
|
+
this.reset();
|
|
144
|
+
}
|
|
145
|
+
if (attrName === 'hide-icon') {
|
|
146
|
+
this.#toggleIcon(newValue);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const { host, icon, timer } = {
|
|
153
|
+
host: { selector: () => ':host' },
|
|
154
|
+
icon: { selector: '.icon' },
|
|
155
|
+
timer: { selector: '.timer' },
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const TimerClass = compose(
|
|
159
|
+
createStyleMixin({
|
|
160
|
+
mappings: {
|
|
161
|
+
backgroundColor: {},
|
|
162
|
+
fontSize: [{}, { ...icon }],
|
|
163
|
+
iconSize: [
|
|
164
|
+
{ ...icon, property: 'width' },
|
|
165
|
+
{ ...icon, property: 'height' },
|
|
166
|
+
],
|
|
167
|
+
fontFamily: {},
|
|
168
|
+
fontWeight: { ...timer },
|
|
169
|
+
lineHeight: { property: 'min-height' },
|
|
170
|
+
textColor: { ...timer, property: 'color' },
|
|
171
|
+
gap: {},
|
|
172
|
+
textAlign: { property: 'justify-content' },
|
|
173
|
+
hostWidth: { ...host, property: 'width' },
|
|
174
|
+
hostDirection: { ...host, property: 'direction' },
|
|
175
|
+
iconColor: { ...icon, property: IconClass.cssVarList.fill },
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
draggableMixin,
|
|
179
|
+
componentNameValidationMixin,
|
|
180
|
+
)(RawTimer);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M6 0C2.7 0 0 2.7 0 6C0 9.3 2.7 12 6 12C9.3 12 12 9.3 12 6C12 2.7 9.3 0 6 0ZM6 10.5C3.525 10.5 1.5 8.475 1.5 6C1.5 3.525 3.525 1.5 6 1.5C8.475 1.5 10.5 3.525 10.5 6C10.5 8.475 8.475 10.5 6 10.5Z" fill="#181A1C"/>
|
|
3
|
+
<path d="M6 2.25H5.25V6.75H9V6H6V2.25Z" fill="#181A1C"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const fmt = (val, pad = 2) => String(val).padStart(pad, '0');
|
|
2
|
+
|
|
3
|
+
export const formatTime = (ms = 0) => {
|
|
4
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
5
|
+
|
|
6
|
+
const secondsInMinute = 60;
|
|
7
|
+
const secondsInHour = secondsInMinute * 60;
|
|
8
|
+
|
|
9
|
+
const hours = Math.floor(totalSeconds / secondsInHour);
|
|
10
|
+
const minutes = Math.floor((totalSeconds % secondsInHour) / secondsInMinute);
|
|
11
|
+
const seconds = totalSeconds % secondsInMinute;
|
|
12
|
+
|
|
13
|
+
const timeParts = [...(hours ? [fmt(hours)] : []), fmt(minutes), fmt(seconds)];
|
|
14
|
+
|
|
15
|
+
return timeParts.join(':');
|
|
16
|
+
};
|
package/src/theme.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import globals from '@descope-ui/theme-globals';
|
|
2
|
+
import { TimerClass } from './component/TimerClass';
|
|
3
|
+
import { getThemeRefs } from '@descope-ui/common/theme-helpers';
|
|
4
|
+
|
|
5
|
+
const globalRefs = getThemeRefs(globals);
|
|
6
|
+
const vars = TimerClass.cssVarList;
|
|
7
|
+
|
|
8
|
+
const timer = {
|
|
9
|
+
[vars.hostDirection]: globalRefs.direction,
|
|
10
|
+
[vars.gap]: '0.25em',
|
|
11
|
+
[vars.fontFamily]: globalRefs.fonts.font1.family,
|
|
12
|
+
[vars.lineHeight]: '2em',
|
|
13
|
+
[vars.fontWeight]: globalRefs.fonts.font1.fontWeight,
|
|
14
|
+
[vars.textColor]: globalRefs.colors.surface.contrast,
|
|
15
|
+
[vars.iconColor]: globalRefs.colors.surface.contrast,
|
|
16
|
+
[vars.iconSize]: '1em',
|
|
17
|
+
|
|
18
|
+
size: {
|
|
19
|
+
xs: { [vars.fontSize]: '12px' },
|
|
20
|
+
sm: { [vars.fontSize]: '14px' },
|
|
21
|
+
md: { [vars.fontSize]: '16px' },
|
|
22
|
+
lg: { [vars.fontSize]: '18px' },
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
textAlign: {
|
|
26
|
+
right: { [vars.textAlign]: 'right' },
|
|
27
|
+
left: { [vars.textAlign]: 'left' },
|
|
28
|
+
center: { [vars.textAlign]: 'center' },
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
_fullWidth: {
|
|
32
|
+
[vars.hostWidth]: '100%',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default timer;
|
|
37
|
+
|
|
38
|
+
export { vars };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { componentName } from '../src/component';
|
|
2
|
+
import {
|
|
3
|
+
directionControl,
|
|
4
|
+
fullWidthControl,
|
|
5
|
+
sizeControl,
|
|
6
|
+
textAlignControl,
|
|
7
|
+
} from '@descope-ui/common/sb-controls';
|
|
8
|
+
|
|
9
|
+
const Template = ({
|
|
10
|
+
seconds,
|
|
11
|
+
size,
|
|
12
|
+
direction,
|
|
13
|
+
'hide-icon': hideIcon,
|
|
14
|
+
'full-width': fullWidth,
|
|
15
|
+
'text-align': textAlign,
|
|
16
|
+
}) => `
|
|
17
|
+
<descope-timer
|
|
18
|
+
seconds="${seconds}"
|
|
19
|
+
size="${size}"
|
|
20
|
+
hide-icon="${hideIcon || false}"
|
|
21
|
+
full-width="${fullWidth}"
|
|
22
|
+
text-align="${textAlign}"
|
|
23
|
+
st-host-direction="${direction ?? ''}"
|
|
24
|
+
></descope-timer>
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
component: componentName,
|
|
29
|
+
title: 'descope-timer',
|
|
30
|
+
decorators: [
|
|
31
|
+
(story) => {
|
|
32
|
+
setTimeout(() => {
|
|
33
|
+
const component = document.querySelector('descope-timer');
|
|
34
|
+
component.addEventListener('timer-started', (e) =>
|
|
35
|
+
console.log('timer started', e.detail),
|
|
36
|
+
);
|
|
37
|
+
component.addEventListener('timer-ended', (e) =>
|
|
38
|
+
console.log('timer ended', e.detail),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
return story();
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
argTypes: {
|
|
45
|
+
...sizeControl,
|
|
46
|
+
...directionControl,
|
|
47
|
+
...fullWidthControl,
|
|
48
|
+
...textAlignControl,
|
|
49
|
+
...directionControl,
|
|
50
|
+
seconds: {
|
|
51
|
+
name: 'Seconds',
|
|
52
|
+
control: {
|
|
53
|
+
type: 'number',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
'hide-icon': {
|
|
57
|
+
name: 'Hide Icon',
|
|
58
|
+
control: {
|
|
59
|
+
type: 'boolean',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const Default = Template.bind({});
|
|
66
|
+
|
|
67
|
+
Default.args = {
|
|
68
|
+
size: 'sm',
|
|
69
|
+
direction: '',
|
|
70
|
+
seconds: 5,
|
|
71
|
+
'text-align': 'center',
|
|
72
|
+
};
|