@dile/ui 3.17.0 → 3.18.0
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/components/bottom-nav/src/DileBottomNavItem.js +4 -4
- package/components/steps/index.js +2 -0
- package/components/steps/src/DileStep.js +90 -0
- package/components/steps/src/DileSteps.js +171 -0
- package/components/steps/step.js +5 -0
- package/components/steps/steps.js +5 -0
- package/components/steps/steps.spec.js +84 -0
- package/package.json +2 -2
|
@@ -114,7 +114,7 @@ export class DileBottomNavItem extends DileOverlay(DileCloseOnEscPressed(DileSel
|
|
|
114
114
|
--dile-icon-color: var(--dile-bottom-nav-item-color, var(--dile-on-background-color, #666));
|
|
115
115
|
}
|
|
116
116
|
:host([selected]) {
|
|
117
|
-
--dile-icon-color: var(--dile-bottom-nav-item-selected-color, var(--dile-
|
|
117
|
+
--dile-icon-color: var(--dile-bottom-nav-item-selected-color, var(--dile-secondary-color, #039be5));
|
|
118
118
|
}
|
|
119
119
|
.trigger {
|
|
120
120
|
all: unset;
|
|
@@ -134,10 +134,10 @@ export class DileBottomNavItem extends DileOverlay(DileCloseOnEscPressed(DileSel
|
|
|
134
134
|
transition: color 0.2s ease;
|
|
135
135
|
}
|
|
136
136
|
:host([selected]) .trigger {
|
|
137
|
-
color: var(--dile-bottom-nav-item-selected-color, var(--dile-
|
|
137
|
+
color: var(--dile-bottom-nav-item-selected-color, var(--dile-secondary-color, #039be5));
|
|
138
138
|
}
|
|
139
139
|
.trigger:focus-visible {
|
|
140
|
-
outline: 2px solid var(--dile-bottom-nav-item-selected-color, var(--dile-
|
|
140
|
+
outline: 2px solid var(--dile-bottom-nav-item-selected-color, var(--dile-secondary-color, #039be5));
|
|
141
141
|
outline-offset: -2px;
|
|
142
142
|
border-radius: 4px;
|
|
143
143
|
}
|
|
@@ -163,7 +163,7 @@ export class DileBottomNavItem extends DileOverlay(DileCloseOnEscPressed(DileSel
|
|
|
163
163
|
width: var(--dile-bottom-nav-close-icon-size, 44px);
|
|
164
164
|
height: var(--dile-bottom-nav-close-icon-size, 44px);
|
|
165
165
|
border-radius: 50%;
|
|
166
|
-
background-color: var(--dile-bottom-nav-close-icon-background-color, var(--dile-bottom-nav-item-selected-color, var(--dile-
|
|
166
|
+
background-color: var(--dile-bottom-nav-close-icon-background-color, var(--dile-bottom-nav-item-selected-color, var(--dile-secondary-color, #039be5)));
|
|
167
167
|
--dile-icon-color: var(--dile-bottom-nav-close-icon-color, var(--dile-bottom-nav-background-color, var(--dile-background-color, #fff)));
|
|
168
168
|
animation: dile-bottom-nav-close-in 0.3s ease;
|
|
169
169
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
|
|
3
|
+
export class DileStep extends LitElement {
|
|
4
|
+
static styles = [
|
|
5
|
+
css`
|
|
6
|
+
:host {
|
|
7
|
+
display: block;
|
|
8
|
+
width: var(--dile-step-size, 1.5rem);
|
|
9
|
+
height: var(--dile-step-size, 1.5rem);
|
|
10
|
+
}
|
|
11
|
+
.step {
|
|
12
|
+
display: flex;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
align-items: center;
|
|
15
|
+
width: 100%;
|
|
16
|
+
height: 100%;
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
background-color: var(--dile-step-background-color, var(--dile-background-color, #fff));
|
|
19
|
+
border-radius: 3rem;
|
|
20
|
+
font-size: var(--dile-step-font-size, 0.8rem);
|
|
21
|
+
font-weight: normal;
|
|
22
|
+
font-family: inherit;
|
|
23
|
+
padding: 0;
|
|
24
|
+
border: var(--dile-step-border-width, 2px) solid var(--dile-step-border-color, #ddd);
|
|
25
|
+
color: var(--dile-step-text-color, var(--dile-on-background-color, #303030));
|
|
26
|
+
cursor: default;
|
|
27
|
+
}
|
|
28
|
+
button.step:focus-visible {
|
|
29
|
+
outline: 2px solid var(--dile-step-active-border-color, var(--dile-primary-dark-color, #0039cb));
|
|
30
|
+
outline-offset: 2px;
|
|
31
|
+
}
|
|
32
|
+
:host([reached]) .step {
|
|
33
|
+
background-color: var(--dile-step-reached-background-color, var(--dile-primary-color, #2962ff));
|
|
34
|
+
border-color: var(--dile-step-reached-background-color, var(--dile-primary-color, #2962ff));
|
|
35
|
+
color: var(--dile-step-reached-text-color, var(--dile-on-primary-color, #fff));
|
|
36
|
+
}
|
|
37
|
+
:host([reached]:not([readonly])) .step {
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
}
|
|
40
|
+
:host([active]) .step {
|
|
41
|
+
border-color: var(--dile-step-active-border-color, var(--dile-primary-dark-color, #0039cb));
|
|
42
|
+
font-weight: bold;
|
|
43
|
+
}
|
|
44
|
+
`
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
static get properties() {
|
|
48
|
+
return {
|
|
49
|
+
/** Step label, used as accessible name of the button */
|
|
50
|
+
label: { type: String },
|
|
51
|
+
/** Zero-based position of the step */
|
|
52
|
+
index: { type: Number },
|
|
53
|
+
/** The step is the current one */
|
|
54
|
+
active: { type: Boolean, reflect: true },
|
|
55
|
+
/** The step can be navigated to */
|
|
56
|
+
reached: { type: Boolean, reflect: true },
|
|
57
|
+
/** Display only: renders the bubble without a button */
|
|
58
|
+
readonly: { type: Boolean, reflect: true },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
constructor() {
|
|
63
|
+
super();
|
|
64
|
+
this.index = 0;
|
|
65
|
+
this.active = false;
|
|
66
|
+
this.reached = false;
|
|
67
|
+
this.readonly = false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
render() {
|
|
71
|
+
if (this.readonly) {
|
|
72
|
+
return html`<span class="step" aria-hidden="true">${this.index + 1}</span>`;
|
|
73
|
+
}
|
|
74
|
+
return html`<button
|
|
75
|
+
type="button"
|
|
76
|
+
class="step"
|
|
77
|
+
?disabled=${!this.reached}
|
|
78
|
+
aria-label=${this.label ?? ''}
|
|
79
|
+
@click=${this.doClick}
|
|
80
|
+
>${this.index + 1}</button>`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
doClick() {
|
|
84
|
+
this.dispatchEvent(new CustomEvent('dile-step-click', {
|
|
85
|
+
bubbles: true,
|
|
86
|
+
composed: true,
|
|
87
|
+
detail: { index: this.index, label: this.label }
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import '../step.js';
|
|
3
|
+
|
|
4
|
+
export class DileSteps extends LitElement {
|
|
5
|
+
static styles = [
|
|
6
|
+
css`
|
|
7
|
+
:host {
|
|
8
|
+
display: block;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* ── Horizontal ── */
|
|
12
|
+
.horizontal {
|
|
13
|
+
display: flex;
|
|
14
|
+
position: relative;
|
|
15
|
+
padding-bottom: 0.5rem;
|
|
16
|
+
}
|
|
17
|
+
.line-bg {
|
|
18
|
+
position: absolute;
|
|
19
|
+
top: calc(var(--dile-step-size, 1.5rem) / 2 - var(--dile-steps-line-width, 0.5rem) / 2);
|
|
20
|
+
left: 0.75rem;
|
|
21
|
+
right: 0.75rem;
|
|
22
|
+
height: var(--dile-steps-line-width, 0.5rem);
|
|
23
|
+
background-color: var(--dile-steps-line-color, var(--dile-primary-color, #2962ff));
|
|
24
|
+
border-radius: 2rem;
|
|
25
|
+
}
|
|
26
|
+
.h-step {
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
align-items: center;
|
|
30
|
+
flex: 1;
|
|
31
|
+
position: relative;
|
|
32
|
+
z-index: 1;
|
|
33
|
+
}
|
|
34
|
+
.h-label {
|
|
35
|
+
font-size: var(--dile-step-label-font-size, 0.7rem);
|
|
36
|
+
text-align: center;
|
|
37
|
+
margin-top: 0.3rem;
|
|
38
|
+
color: var(--dile-step-label-color, #606060);
|
|
39
|
+
line-height: 1.2;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ── Vertical ── */
|
|
43
|
+
.vertical {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
}
|
|
47
|
+
.v-step {
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: row;
|
|
50
|
+
align-items: flex-start;
|
|
51
|
+
gap: 0.75rem;
|
|
52
|
+
}
|
|
53
|
+
.v-track {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
align-items: center;
|
|
57
|
+
width: var(--dile-step-size, 1.5rem);
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
}
|
|
60
|
+
.connector {
|
|
61
|
+
width: var(--dile-steps-connector-width, 0.35rem);
|
|
62
|
+
height: var(--dile-steps-connector-height, 1rem);
|
|
63
|
+
background-color: var(--dile-steps-line-color, var(--dile-primary-color, #2962ff));
|
|
64
|
+
border-radius: 2rem;
|
|
65
|
+
}
|
|
66
|
+
.v-label {
|
|
67
|
+
font-size: var(--dile-step-label-font-size, 0.85rem);
|
|
68
|
+
color: var(--dile-step-label-color, #606060);
|
|
69
|
+
padding-top: 0.2rem;
|
|
70
|
+
line-height: 1.2;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.active .h-label,
|
|
74
|
+
.active .v-label {
|
|
75
|
+
color: var(--dile-step-active-label-color, var(--dile-step-active-border-color, var(--dile-primary-dark-color, #0039cb)));
|
|
76
|
+
font-weight: 600;
|
|
77
|
+
}
|
|
78
|
+
`
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
static get properties() {
|
|
82
|
+
return {
|
|
83
|
+
/** Array with the label of each step */
|
|
84
|
+
steps: { type: Array },
|
|
85
|
+
/** Zero-based index of the current step */
|
|
86
|
+
current: { type: Number },
|
|
87
|
+
/** Highest step index reached so far */
|
|
88
|
+
reached: { type: Number },
|
|
89
|
+
/** All steps can be selected, regardless of reached */
|
|
90
|
+
freeNavigation: { type: Boolean },
|
|
91
|
+
/** Vertical layout */
|
|
92
|
+
vertical: { type: Boolean },
|
|
93
|
+
/** Display only: steps can not be clicked */
|
|
94
|
+
readonly: { type: Boolean },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
constructor() {
|
|
99
|
+
super();
|
|
100
|
+
this.steps = [];
|
|
101
|
+
this.current = 0;
|
|
102
|
+
this.reached = 0;
|
|
103
|
+
this.freeNavigation = false;
|
|
104
|
+
this.vertical = false;
|
|
105
|
+
this.readonly = false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
isReached(index) {
|
|
109
|
+
return this.freeNavigation || index <= this.reached;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
render() {
|
|
113
|
+
return this.vertical ? this.renderVertical() : this.renderHorizontal();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
renderHorizontal() {
|
|
117
|
+
return html`
|
|
118
|
+
<div class="horizontal" role="list" @dile-step-click=${this.onStepClick}>
|
|
119
|
+
<div class="line-bg"></div>
|
|
120
|
+
${this.steps.map((step, index) => html`
|
|
121
|
+
<div class="h-step ${index === this.current ? 'active' : ''}" role="listitem" aria-current=${index === this.current ? 'step' : 'false'}>
|
|
122
|
+
${this.stepTemplate(step, index)}
|
|
123
|
+
<span class="h-label">${step}</span>
|
|
124
|
+
</div>
|
|
125
|
+
`)}
|
|
126
|
+
</div>
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
renderVertical() {
|
|
131
|
+
return html`
|
|
132
|
+
<div class="vertical" role="list" @dile-step-click=${this.onStepClick}>
|
|
133
|
+
${this.steps.map((step, index) => html`
|
|
134
|
+
<div class="v-step ${index === this.current ? 'active' : ''}" role="listitem" aria-current=${index === this.current ? 'step' : 'false'}>
|
|
135
|
+
<div class="v-track">
|
|
136
|
+
${this.stepTemplate(step, index)}
|
|
137
|
+
${index < this.steps.length - 1
|
|
138
|
+
? html`<div class="connector"></div>`
|
|
139
|
+
: ''}
|
|
140
|
+
</div>
|
|
141
|
+
<span class="v-label">${step}</span>
|
|
142
|
+
</div>
|
|
143
|
+
`)}
|
|
144
|
+
</div>
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
stepTemplate(step, index) {
|
|
149
|
+
return html`
|
|
150
|
+
<dile-step
|
|
151
|
+
?active=${index === this.current}
|
|
152
|
+
?reached=${this.isReached(index)}
|
|
153
|
+
?readonly=${this.readonly}
|
|
154
|
+
label="${step}"
|
|
155
|
+
index="${index}"
|
|
156
|
+
></dile-step>
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
onStepClick(e) {
|
|
161
|
+
e.stopPropagation();
|
|
162
|
+
if (this.readonly) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.dispatchEvent(new CustomEvent('dile-steps-select', {
|
|
166
|
+
bubbles: true,
|
|
167
|
+
composed: true,
|
|
168
|
+
detail: { index: e.detail.index, label: e.detail.label }
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import './steps.js';
|
|
3
|
+
|
|
4
|
+
describe('dile-steps', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
document.body.innerHTML = '';
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
async function renderSteps(attributes = '') {
|
|
10
|
+
document.body.innerHTML = `<dile-steps ${attributes}></dile-steps>`;
|
|
11
|
+
const el = document.body.querySelector('dile-steps');
|
|
12
|
+
el.steps = ['Customer', 'Products', 'Review'];
|
|
13
|
+
await el.updateComplete;
|
|
14
|
+
return el;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function bubbles(el) {
|
|
18
|
+
return [...el.shadowRoot.querySelectorAll('dile-step')];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
it('renders one step bubble and label per step', async () => {
|
|
22
|
+
const el = await renderSteps();
|
|
23
|
+
|
|
24
|
+
expect(bubbles(el)).toHaveLength(3);
|
|
25
|
+
const labels = [...el.shadowRoot.querySelectorAll('.h-label')].map(label => label.textContent);
|
|
26
|
+
expect(labels).toEqual(['Customer', 'Products', 'Review']);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('marks the current step as active and with aria-current', async () => {
|
|
30
|
+
const el = await renderSteps('current="1" reached="1"');
|
|
31
|
+
|
|
32
|
+
expect(bubbles(el).map(step => step.active)).toEqual([false, true, false]);
|
|
33
|
+
const items = [...el.shadowRoot.querySelectorAll('[role="listitem"]')];
|
|
34
|
+
expect(items[1].getAttribute('aria-current')).toBe('step');
|
|
35
|
+
expect(items[0].getAttribute('aria-current')).toBe('false');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('only enables the steps already reached', async () => {
|
|
39
|
+
const el = await renderSteps('current="0" reached="1"');
|
|
40
|
+
await Promise.all(bubbles(el).map(step => step.updateComplete));
|
|
41
|
+
|
|
42
|
+
const buttons = bubbles(el).map(step => step.shadowRoot.querySelector('button'));
|
|
43
|
+
expect(buttons.map(button => button.disabled)).toEqual([false, false, true]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('enables every step with freeNavigation', async () => {
|
|
47
|
+
const el = await renderSteps('freeNavigation');
|
|
48
|
+
await Promise.all(bubbles(el).map(step => step.updateComplete));
|
|
49
|
+
|
|
50
|
+
const buttons = bubbles(el).map(step => step.shadowRoot.querySelector('button'));
|
|
51
|
+
expect(buttons.every(button => !button.disabled)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('dispatches dile-steps-select when a reached step is clicked', async () => {
|
|
55
|
+
const el = await renderSteps('reached="2"');
|
|
56
|
+
await Promise.all(bubbles(el).map(step => step.updateComplete));
|
|
57
|
+
let detail;
|
|
58
|
+
let innerClickEscaped = false;
|
|
59
|
+
el.addEventListener('dile-steps-select', e => { detail = e.detail; });
|
|
60
|
+
document.body.addEventListener('dile-step-click', () => { innerClickEscaped = true; });
|
|
61
|
+
|
|
62
|
+
bubbles(el)[2].shadowRoot.querySelector('button').click();
|
|
63
|
+
|
|
64
|
+
expect(detail).toEqual({ index: 2, label: 'Review' });
|
|
65
|
+
expect(innerClickEscaped).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('renders the bubbles without buttons in readonly mode', async () => {
|
|
69
|
+
const el = await renderSteps('readonly reached="2"');
|
|
70
|
+
await Promise.all(bubbles(el).map(step => step.updateComplete));
|
|
71
|
+
|
|
72
|
+
bubbles(el).forEach(step => {
|
|
73
|
+
expect(step.shadowRoot.querySelector('button')).toBeNull();
|
|
74
|
+
expect(step.shadowRoot.querySelector('.step').textContent).toBe(String(step.index + 1));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('renders the vertical layout', async () => {
|
|
79
|
+
const el = await renderSteps('vertical');
|
|
80
|
+
|
|
81
|
+
expect(el.shadowRoot.querySelector('.vertical')).toBeTruthy();
|
|
82
|
+
expect(el.shadowRoot.querySelectorAll('.connector')).toHaveLength(2);
|
|
83
|
+
});
|
|
84
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.18.0",
|
|
4
4
|
"description": "UI Core components from dile-components.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "1b065736666cc26f8afc1106ec7ffc98feaec60e"
|
|
32
32
|
}
|