@dile/ui 3.5.5 → 3.6.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/input/index.js +1 -0
- package/components/input/input-icon.js +5 -0
- package/components/input/input-icon.spec.js +166 -0
- package/components/input/src/DileInputIcon.js +181 -0
- package/components/spinner/src/DileSpinnerIcon.js +1 -1
- package/components/tooltip/src/DileTooltip.js +1 -1
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { DileInput } from './src/DileInput.js';
|
|
2
|
+
export { DileInputIcon } from './src/DileInputIcon.js';
|
|
2
3
|
export { DileInputInteger } from './src/DileInputInteger.js';
|
|
3
4
|
export { DileInputMessage } from './src/DileInputMessage.js';
|
|
4
5
|
export { DileInputMoney } from './src/DileInputMoney.js';
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
2
|
+
import './input-icon.js';
|
|
3
|
+
import '@dile/iconlib/lucide-icons/dot.js';
|
|
4
|
+
|
|
5
|
+
describe('dile-input-icon', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
document.body.innerHTML = '';
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
async function renderInputIcon(html) {
|
|
11
|
+
document.body.innerHTML = html;
|
|
12
|
+
const el = document.body.querySelector('dile-input-icon');
|
|
13
|
+
await el.updateComplete;
|
|
14
|
+
return el;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
it('renders a text input with the given value', async () => {
|
|
18
|
+
const el = await renderInputIcon('<dile-input-icon name="test" value="hello"></dile-input-icon>');
|
|
19
|
+
const input = el.shadowRoot.querySelector('input');
|
|
20
|
+
expect(input).toBeTruthy();
|
|
21
|
+
expect(input.value).toBe('hello');
|
|
22
|
+
expect(input.type).toBe('text');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('renders the label when set', async () => {
|
|
26
|
+
const el = await renderInputIcon('<dile-input-icon label="Username"></dile-input-icon>');
|
|
27
|
+
const label = el.shadowRoot.querySelector('label');
|
|
28
|
+
expect(label).toBeTruthy();
|
|
29
|
+
expect(label.textContent.trim()).toBe('Username');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('does not render icon button when icon is not set', async () => {
|
|
33
|
+
const el = await renderInputIcon('<dile-input-icon></dile-input-icon>');
|
|
34
|
+
const button = el.shadowRoot.querySelector('.input-icon-btn');
|
|
35
|
+
expect(button).toBeFalsy();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders icon button when icon is set', async () => {
|
|
39
|
+
const el = await renderInputIcon('<dile-input-icon icon="lucide.dot"></dile-input-icon>');
|
|
40
|
+
await new Promise(resolve => setTimeout(resolve, 50)); // Small delay for icon registration
|
|
41
|
+
const button = el.shadowRoot.querySelector('.input-icon-btn');
|
|
42
|
+
expect(button).toBeTruthy();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('disables the icon button when input is disabled', async () => {
|
|
46
|
+
const el = await renderInputIcon('<dile-input-icon icon="lucide.dot" disabled></dile-input-icon>');
|
|
47
|
+
const button = el.shadowRoot.querySelector('.input-icon-btn');
|
|
48
|
+
expect(button).toBeTruthy();
|
|
49
|
+
expect(button.disabled).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('dispatches dile-input-icon-clicked event when icon button is clicked', async () => {
|
|
53
|
+
const el = await renderInputIcon('<dile-input-icon icon="lucide.dot" value="test-value"></dile-input-icon>');
|
|
54
|
+
const button = el.shadowRoot.querySelector('.input-icon-btn');
|
|
55
|
+
|
|
56
|
+
let eventData = null;
|
|
57
|
+
el.addEventListener('dile-input-icon-clicked', (e) => {
|
|
58
|
+
eventData = e.detail;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
button.click();
|
|
62
|
+
await el.updateComplete;
|
|
63
|
+
|
|
64
|
+
expect(eventData).toBeTruthy();
|
|
65
|
+
expect(eventData.icon).toBe('lucide.dot');
|
|
66
|
+
expect(eventData.value).toBe('test-value');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('calls onIconClick callback when icon button is clicked', async () => {
|
|
70
|
+
const callback = vi.fn();
|
|
71
|
+
const el = await renderInputIcon('<dile-input-icon icon="lucide.dot" value="test"></dile-input-icon>');
|
|
72
|
+
|
|
73
|
+
el.onIconClick = callback;
|
|
74
|
+
const button = el.shadowRoot.querySelector('.input-icon-btn');
|
|
75
|
+
button.click();
|
|
76
|
+
await el.updateComplete;
|
|
77
|
+
|
|
78
|
+
expect(callback).toHaveBeenCalledWith(el);
|
|
79
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('callback receives component instance with access to value property', async () => {
|
|
83
|
+
const el = await renderInputIcon('<dile-input-icon icon="lucide.dot" value="secret"></dile-input-icon>');
|
|
84
|
+
|
|
85
|
+
let receivedComponent = null;
|
|
86
|
+
el.onIconClick = (component) => {
|
|
87
|
+
receivedComponent = component;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const button = el.shadowRoot.querySelector('.input-icon-btn');
|
|
91
|
+
button.click();
|
|
92
|
+
await el.updateComplete;
|
|
93
|
+
|
|
94
|
+
expect(receivedComponent).toBe(el);
|
|
95
|
+
expect(receivedComponent.value).toBe('secret');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('renders a message when set', async () => {
|
|
99
|
+
const el = await renderInputIcon('<dile-input-icon></dile-input-icon>');
|
|
100
|
+
el.message = 'Required field';
|
|
101
|
+
await el.updateComplete;
|
|
102
|
+
|
|
103
|
+
const message = el.shadowRoot.querySelector('.message span');
|
|
104
|
+
expect(message).toBeTruthy();
|
|
105
|
+
expect(message.textContent.trim()).toBe('Required field');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('updates the value property on user input', async () => {
|
|
109
|
+
const el = await renderInputIcon('<dile-input-icon name="test"></dile-input-icon>');
|
|
110
|
+
const input = el.shadowRoot.querySelector('input');
|
|
111
|
+
input.value = 'typed';
|
|
112
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
113
|
+
await el.updateComplete;
|
|
114
|
+
|
|
115
|
+
expect(el.value).toBe('typed');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('does not render tooltip wrapper when tooltip is not set', async () => {
|
|
119
|
+
const el = await renderInputIcon('<dile-input-icon icon="lucide.dot"></dile-input-icon>');
|
|
120
|
+
const tooltip = el.shadowRoot.querySelector('dile-tooltip');
|
|
121
|
+
expect(tooltip).toBeFalsy();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('renders dile-tooltip when tooltip property is set', async () => {
|
|
125
|
+
const el = await renderInputIcon('<dile-input-icon icon="lucide.dot" tooltip="Help text"></dile-input-icon>');
|
|
126
|
+
await el.updateComplete;
|
|
127
|
+
const tooltip = el.shadowRoot.querySelector('dile-tooltip');
|
|
128
|
+
expect(tooltip).toBeTruthy();
|
|
129
|
+
expect(tooltip.getAttribute('tooltip')).toBe('Help text');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('maps tooltipPosition to tooltip position attribute', async () => {
|
|
133
|
+
const el = await renderInputIcon('<dile-input-icon icon="lucide.dot" tooltip="Help" tooltipPosition="bottom"></dile-input-icon>');
|
|
134
|
+
await el.updateComplete;
|
|
135
|
+
const tooltip = el.shadowRoot.querySelector('dile-tooltip');
|
|
136
|
+
expect(tooltip.getAttribute('position')).toBe('bottom');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('maps tooltipFadeIn to tooltip fadeIn attribute', async () => {
|
|
140
|
+
const el = await renderInputIcon('<dile-input-icon icon="lucide.dot" tooltip="Help" tooltipFadeIn></dile-input-icon>');
|
|
141
|
+
await el.updateComplete;
|
|
142
|
+
const tooltip = el.shadowRoot.querySelector('dile-tooltip');
|
|
143
|
+
expect(tooltip.getAttribute('fadeIn')).toBe('');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('maps tooltipArrow to tooltip arrow attribute', async () => {
|
|
147
|
+
const el = await renderInputIcon('<dile-input-icon icon="lucide.dot" tooltip="Help" tooltipArrow></dile-input-icon>');
|
|
148
|
+
await el.updateComplete;
|
|
149
|
+
const tooltip = el.shadowRoot.querySelector('dile-tooltip');
|
|
150
|
+
expect(tooltip.getAttribute('arrow')).toBe('');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('button is still clickable when wrapped in tooltip', async () => {
|
|
154
|
+
const callback = vi.fn();
|
|
155
|
+
const el = await renderInputIcon('<dile-input-icon icon="lucide.dot" tooltip="Help"></dile-input-icon>');
|
|
156
|
+
el.onIconClick = callback;
|
|
157
|
+
await el.updateComplete;
|
|
158
|
+
|
|
159
|
+
const button = el.shadowRoot.querySelector('.input-icon-btn');
|
|
160
|
+
expect(button).toBeTruthy();
|
|
161
|
+
button.click();
|
|
162
|
+
await el.updateComplete;
|
|
163
|
+
|
|
164
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { DileInput } from '../../input/index.js';
|
|
3
|
+
import '@dile/iconlib/dile-iconlib.js';
|
|
4
|
+
import '@dile/ui/components/tooltip/tooltip.js';
|
|
5
|
+
|
|
6
|
+
export class DileInputIcon extends DileInput {
|
|
7
|
+
|
|
8
|
+
static get properties() {
|
|
9
|
+
return {
|
|
10
|
+
...super.properties,
|
|
11
|
+
/** Icon to display in the button, format: "family.name" (e.g., "lucide.eye") */
|
|
12
|
+
icon: { type: String },
|
|
13
|
+
/** Callback function that receives the component instance when icon button is clicked */
|
|
14
|
+
onIconClick: { type: Function },
|
|
15
|
+
/** Tooltip text to show on icon hover */
|
|
16
|
+
tooltip: { type: String },
|
|
17
|
+
/** Tooltip position: "top", "bottom", "left", "right" */
|
|
18
|
+
tooltipPosition: { type: String },
|
|
19
|
+
/** Enable fade-in animation for tooltip */
|
|
20
|
+
tooltipFadeIn: { type: Boolean },
|
|
21
|
+
/** Show arrow pointing to the icon on tooltip */
|
|
22
|
+
tooltipArrow: { type: Boolean },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static get styles() {
|
|
27
|
+
return [
|
|
28
|
+
...super.styles,
|
|
29
|
+
css`
|
|
30
|
+
.input-icon-btn {
|
|
31
|
+
background: none;
|
|
32
|
+
border: none;
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
padding: var(--dile-input-icon-padding, var(--dile-input-padding, 5px));
|
|
35
|
+
margin: var(--dile-input-icon-margin, 0 5px);
|
|
36
|
+
color: var(--dile-input-icon-color, var(--dile-on-background-color, #303030));
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: center;
|
|
40
|
+
width: var(--dile-input-icon-width, 32px);
|
|
41
|
+
height: var(--dile-input-icon-height, 32px);
|
|
42
|
+
border-radius: var(--dile-input-icon-border-radius, var(--dile-input-border-radius, 5px));
|
|
43
|
+
transition: var(--dile-input-icon-transition, background-color 0.2s);
|
|
44
|
+
}
|
|
45
|
+
.input-icon-btn:hover {
|
|
46
|
+
background-color: var(--dile-input-icon-hover-bg, rgba(0, 0, 0, 0.05));
|
|
47
|
+
}
|
|
48
|
+
.input-icon-btn:active {
|
|
49
|
+
background-color: var(--dile-input-icon-active-bg, rgba(0, 0, 0, 0.1));
|
|
50
|
+
}
|
|
51
|
+
.input-icon-btn:disabled {
|
|
52
|
+
cursor: not-allowed;
|
|
53
|
+
opacity: 0.5;
|
|
54
|
+
}
|
|
55
|
+
.input-icon-btn dile-iconlib {
|
|
56
|
+
--dile-icon-color: var(--dile-input-icon-color, var(--dile-on-background-color, #303030));
|
|
57
|
+
--dile-icon-size: var(--dile-input-icon-size, 20px);
|
|
58
|
+
}
|
|
59
|
+
`
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
constructor() {
|
|
64
|
+
super();
|
|
65
|
+
this.icon = '';
|
|
66
|
+
this.onIconClick = null;
|
|
67
|
+
this.tooltip = '';
|
|
68
|
+
this.tooltipPosition = 'top';
|
|
69
|
+
this.tooltipFadeIn = false;
|
|
70
|
+
this.tooltipArrow = false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
render() {
|
|
74
|
+
const inputType = this.type || 'text';
|
|
75
|
+
|
|
76
|
+
return html`
|
|
77
|
+
<div>
|
|
78
|
+
${this.label
|
|
79
|
+
? html`<label for="textField">${this.label}</label>`
|
|
80
|
+
: ''
|
|
81
|
+
}
|
|
82
|
+
<section class="for-input">
|
|
83
|
+
<input
|
|
84
|
+
type="${inputType}"
|
|
85
|
+
id="textField"
|
|
86
|
+
name="${this.name}"
|
|
87
|
+
placeholder="${this.placeholder}"
|
|
88
|
+
?disabled="${this.disabled}"
|
|
89
|
+
?readonly="${this.readonly}"
|
|
90
|
+
@keypress="${this._lookForEnter}"
|
|
91
|
+
@input="${this._input}"
|
|
92
|
+
@blur="${this.doBlur}"
|
|
93
|
+
@focus="${this.doFocus}"
|
|
94
|
+
.value="${this.value}"
|
|
95
|
+
class="${this.errored ? 'errored' : ''}"
|
|
96
|
+
autocomplete="${this.disableAutocomplete ? 'off' : 'on'}"
|
|
97
|
+
?selectonfocus="${this.selectOnFocus}"
|
|
98
|
+
>
|
|
99
|
+
${this.icon
|
|
100
|
+
? this.renderIconButton()
|
|
101
|
+
: ''
|
|
102
|
+
}
|
|
103
|
+
</section>
|
|
104
|
+
${this.message
|
|
105
|
+
? html`<div class="message ${this.errored ? 'errored-msg' : ''}"><span>${this.message}</span></div>`
|
|
106
|
+
: ''
|
|
107
|
+
}
|
|
108
|
+
</div>
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Renders an icon using dile-iconlib
|
|
114
|
+
* Supports format "family.name" (e.g., "lucide.eye", "material.home")
|
|
115
|
+
* dile-iconlib handles warnings if icon is not registered
|
|
116
|
+
* @param {string} iconName - Icon in "family.name" format
|
|
117
|
+
* @returns {TemplateResult}
|
|
118
|
+
*/
|
|
119
|
+
renderIconlibIcon(iconName) {
|
|
120
|
+
if (!iconName || typeof iconName !== 'string' || !iconName.includes('.')) {
|
|
121
|
+
console.warn(`Invalid icon format: "${iconName}". Use "family.name" (e.g., "lucide.eye")`);
|
|
122
|
+
return html``;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const [family, name] = iconName.split('.');
|
|
126
|
+
if (!family || !name) {
|
|
127
|
+
console.warn(`Invalid icon format: "${iconName}". Use "family.name" (e.g., "lucide.eye")`);
|
|
128
|
+
return html``;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Always render dile-iconlib, it will handle non-registered icons
|
|
132
|
+
return html`<dile-iconlib icon="${iconName}"></dile-iconlib>`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Renders the icon button, optionally wrapped with a tooltip
|
|
137
|
+
* @returns {TemplateResult}
|
|
138
|
+
*/
|
|
139
|
+
renderIconButton() {
|
|
140
|
+
const button = html`<button
|
|
141
|
+
class="input-icon-btn"
|
|
142
|
+
type="button"
|
|
143
|
+
@click="${this._handleIconClick}"
|
|
144
|
+
?disabled="${this.disabled}"
|
|
145
|
+
aria-label="Icon action button"
|
|
146
|
+
>
|
|
147
|
+
${this.renderIconlibIcon(this.icon)}
|
|
148
|
+
</button>`;
|
|
149
|
+
|
|
150
|
+
// If tooltip is defined, wrap button with dile-tooltip
|
|
151
|
+
if (this.tooltip) {
|
|
152
|
+
return html`<dile-tooltip
|
|
153
|
+
tooltip="${this.tooltip}"
|
|
154
|
+
position="${this.tooltipPosition}"
|
|
155
|
+
?fadeIn="${this.tooltipFadeIn}"
|
|
156
|
+
?arrow="${this.tooltipArrow}"
|
|
157
|
+
>
|
|
158
|
+
${button}
|
|
159
|
+
</dile-tooltip>`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return button;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_handleIconClick() {
|
|
166
|
+
// Dispatch custom event
|
|
167
|
+
this.dispatchEvent(new CustomEvent('dile-input-icon-clicked', {
|
|
168
|
+
bubbles: true,
|
|
169
|
+
composed: true,
|
|
170
|
+
detail: {
|
|
171
|
+
icon: this.icon,
|
|
172
|
+
value: this.value,
|
|
173
|
+
}
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
// Execute callback if provided
|
|
177
|
+
if (typeof this.onIconClick === 'function') {
|
|
178
|
+
this.onIconClick(this);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -9,7 +9,7 @@ export class DileSpinnerIcon extends DileSpinnerMixin(LitElement) {
|
|
|
9
9
|
display: none;
|
|
10
10
|
}
|
|
11
11
|
:host([active]) {
|
|
12
|
-
display: inline-
|
|
12
|
+
display: inline-flex;
|
|
13
13
|
}
|
|
14
14
|
dile-lucide-icon-loader-circle {
|
|
15
15
|
animation: spin var(--dile-spinner-icon-animation-time, 1s) linear infinite;
|
|
@@ -76,7 +76,7 @@ export class DileTooltip extends LitElement {
|
|
|
76
76
|
transform: translateX(-50%);
|
|
77
77
|
border-width: 5px;
|
|
78
78
|
border-style: solid;
|
|
79
|
-
border-color: transparent transparent var(--dile-tooltip-background-color, #e74c3c) transparent;
|
|
79
|
+
border-color: transparent transparent var(--dile-tooltip-background-color, var(--dile-primary-color, #e74c3c)) transparent;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
.animation{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.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": "38fa60c43d27a6b9d083a0d5be2e3be4b2a67f00"
|
|
32
32
|
}
|