@dile/ui 3.17.1 → 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.
@@ -0,0 +1,2 @@
1
+ export { DileSteps } from './src/DileSteps.js';
2
+ export { DileStep } from './src/DileStep.js';
@@ -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,5 @@
1
+ import { DileStep } from './src/DileStep.js';
2
+
3
+ if (!customElements.get('dile-step')) {
4
+ customElements.define('dile-step', DileStep);
5
+ }
@@ -0,0 +1,5 @@
1
+ import { DileSteps } from './src/DileSteps.js';
2
+
3
+ if (!customElements.get('dile-steps')) {
4
+ customElements.define('dile-steps', DileSteps);
5
+ }
@@ -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.17.1",
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": "eb8d274006be88ab2b44ed574f5c2829f27a2ec3"
31
+ "gitHead": "1b065736666cc26f8afc1106ec7ffc98feaec60e"
32
32
  }