@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.
@@ -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-primary-color, #039be5));
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-primary-color, #039be5));
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-primary-color, #039be5));
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-primary-color, #039be5)));
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,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.0",
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": "ea92314b81f3274d6f843b7a926fc528f1a13879"
31
+ "gitHead": "1b065736666cc26f8afc1106ec7ffc98feaec60e"
32
32
  }