Skip to content

Commit bf3c1cb

Browse files
committed
added comparasion component
1 parent 98f289a commit bf3c1cb

24 files changed

+1841
-1
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"docs/@capsule/components/capsule-select/vscode.data.json",
1818
"docs/@capsule/components/capsule-kbd/vscode.data.json",
1919
"docs/@capsule/components/capsule-aspect-ratio/vscode.data.json",
20-
"docs/@capsule/components/capsule-progress/vscode.data.json"
20+
"docs/@capsule/components/capsule-progress/vscode.data.json",
21+
"docs/@capsule/components/capsule-comparison/vscode.data.json"
2122
]
2223
}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
</p>
44

55
<h2 align="center">CapsuleUI</h2>
6+
67
<p align="center">
78
Native Web Components • Unstyled-by-design • Bring your own design system
89
</p>

docs/.vitepress/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export default defineConfig({
9595
{ text: 'Range', link: '/components/range' },
9696
{ text: 'Rating', link: '/components/rating' },
9797
{ text: 'Progress', link: '/components/progress' },
98+
{ text: 'Comparison', link: '/components/comparison' },
9899
{ text: 'Tooltip', link: '/components/tooltip' },
99100
],
100101
},
@@ -160,6 +161,7 @@ export default defineConfig({
160161
{ text: 'Range', link: '/ru/components/range' },
161162
{ text: 'Rating', link: '/ru/components/rating' },
162163
{ text: 'Progress', link: '/ru/components/progress' },
164+
{ text: 'Comparison', link: '/ru/components/comparison' },
163165
{ text: 'Tooltip', link: '/ru/components/tooltip' },
164166
],
165167
},
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { LitElement, html } from '../../lit';
2+
3+
class CapsuleComparisonAfter extends LitElement {
4+
createRenderRoot() {
5+
return this;
6+
}
7+
8+
render() {
9+
return html`<slot></slot>`;
10+
}
11+
}
12+
13+
customElements.define('capsule-comparison-after', CapsuleComparisonAfter);
14+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { LitElement, html } from '../../lit';
2+
3+
class CapsuleComparisonBefore extends LitElement {
4+
createRenderRoot() {
5+
return this;
6+
}
7+
8+
render() {
9+
return html`<slot></slot>`;
10+
}
11+
}
12+
13+
customElements.define(
14+
'capsule-comparison-before',
15+
CapsuleComparisonBefore
16+
);
17+
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { LitElement, html } from '../../lit';
2+
3+
class CapsuleComparisonLine extends LitElement {
4+
constructor() {
5+
super();
6+
this._isDragging = false;
7+
this._handlePointerDown = this._handlePointerDown.bind(this);
8+
this._handlePointerMove = this._handlePointerMove.bind(this);
9+
this._handlePointerUp = this._handlePointerUp.bind(this);
10+
this._handleKeyDown = this._handleKeyDown.bind(this);
11+
}
12+
13+
connectedCallback() {
14+
super.connectedCallback();
15+
this.addEventListener('pointerdown', this._handlePointerDown);
16+
this.addEventListener('keydown', this._handleKeyDown);
17+
this.setAttribute('role', 'slider');
18+
this.setAttribute('tabindex', '0');
19+
this.setAttribute('aria-label', 'Adjust comparison position');
20+
this.setAttribute('aria-valuemin', '0');
21+
this.setAttribute('aria-valuemax', '100');
22+
}
23+
24+
disconnectedCallback() {
25+
super.disconnectedCallback();
26+
this.removeEventListener('pointerdown', this._handlePointerDown);
27+
this.removeEventListener('keydown', this._handleKeyDown);
28+
this._removeGlobalListeners();
29+
}
30+
31+
createRenderRoot() {
32+
return this;
33+
}
34+
35+
render() {
36+
return html`
37+
<div
38+
class="line"
39+
part="line"
40+
>
41+
<slot></slot>
42+
</div>
43+
`;
44+
}
45+
46+
_handlePointerDown(event) {
47+
if (event.button !== 0) return;
48+
event.preventDefault();
49+
this._isDragging = true;
50+
this.setPointerCapture(event.pointerId);
51+
this.setAttribute('aria-pressed', 'true');
52+
document.addEventListener('pointermove', this._handlePointerMove);
53+
document.addEventListener('pointerup', this._handlePointerUp);
54+
this._updatePosition(event);
55+
}
56+
57+
_handlePointerMove(event) {
58+
if (!this._isDragging) return;
59+
event.preventDefault();
60+
this._updatePosition(event);
61+
}
62+
63+
_handlePointerUp(event) {
64+
if (!this._isDragging) return;
65+
this._isDragging = false;
66+
this.removeAttribute('aria-pressed');
67+
this.releasePointerCapture(event.pointerId);
68+
this._removeGlobalListeners();
69+
}
70+
71+
_removeGlobalListeners() {
72+
document.removeEventListener('pointermove', this._handlePointerMove);
73+
document.removeEventListener('pointerup', this._handlePointerUp);
74+
}
75+
76+
_updatePosition(event) {
77+
const container = this.closest('capsule-comparison');
78+
if (!container) return;
79+
80+
const containerRect = container.getBoundingClientRect();
81+
const x = event.clientX - containerRect.left;
82+
const position = (x / containerRect.width) * 100;
83+
const clampedPosition = Math.max(0, Math.min(100, position));
84+
85+
container.setPosition(clampedPosition);
86+
this.setAttribute('aria-valuenow', Number(clampedPosition).toFixed(0));
87+
88+
this.dispatchEvent(
89+
new CustomEvent('comparison-line-move', {
90+
bubbles: true,
91+
detail: { position: clampedPosition },
92+
})
93+
);
94+
}
95+
96+
_handleKeyDown(event) {
97+
const container = this.closest('capsule-comparison');
98+
if (!container) return;
99+
100+
let newPosition = Number(container.position) || 50;
101+
const step = event.shiftKey ? 10 : 1;
102+
103+
switch (event.key) {
104+
case 'ArrowLeft':
105+
event.preventDefault();
106+
newPosition = Math.max(0, newPosition - step);
107+
break;
108+
case 'ArrowRight':
109+
event.preventDefault();
110+
newPosition = Math.min(100, newPosition + step);
111+
break;
112+
case 'Home':
113+
event.preventDefault();
114+
newPosition = 0;
115+
break;
116+
case 'End':
117+
event.preventDefault();
118+
newPosition = 100;
119+
break;
120+
default:
121+
return;
122+
}
123+
124+
container.setPosition(newPosition);
125+
this.setAttribute('aria-valuenow', Number(newPosition).toFixed(0));
126+
}
127+
}
128+
129+
customElements.define('capsule-comparison-line', CapsuleComparisonLine);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { LitElement, html } from '../../lit';
2+
3+
class CapsuleComparison extends LitElement {
4+
static properties = {
5+
position: { type: Number, reflect: true },
6+
};
7+
8+
constructor() {
9+
super();
10+
this.position = 50;
11+
this._handleLineMove = this._handleLineMove.bind(this);
12+
}
13+
14+
connectedCallback() {
15+
super.connectedCallback();
16+
this.addEventListener('comparison-line-move', this._handleLineMove);
17+
this._updatePosition();
18+
}
19+
20+
disconnectedCallback() {
21+
super.disconnectedCallback();
22+
this.removeEventListener('comparison-line-move', this._handleLineMove);
23+
}
24+
25+
updated(changedProperties) {
26+
if (changedProperties.has('position')) {
27+
this._updatePosition();
28+
}
29+
}
30+
31+
firstUpdated() {
32+
this._updatePosition();
33+
this._updateLineAria();
34+
}
35+
36+
render() {
37+
return html`<slot></slot>`;
38+
}
39+
40+
_handleLineMove(event) {
41+
this.position = event.detail.position;
42+
this._updatePosition();
43+
this._updateLineAria();
44+
}
45+
46+
_updatePosition() {
47+
const position = Math.max(0, Math.min(100, Number(this.position) || 50));
48+
this.style.setProperty('--comparison-position', `${position}%`);
49+
}
50+
51+
_updateLineAria() {
52+
const line = this.querySelector('capsule-comparison-line');
53+
if (line) {
54+
const position = Number(this.position) || 50;
55+
line.setAttribute('aria-valuenow', position.toFixed(0));
56+
}
57+
}
58+
59+
setPosition(position) {
60+
this.position = Math.max(0, Math.min(100, Number(position) || 50));
61+
this._updatePosition();
62+
this._updateLineAria();
63+
}
64+
}
65+
66+
customElements.define('capsule-comparison', CapsuleComparison);
67+
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
capsule-comparison {
2+
display: block;
3+
position: relative;
4+
width: 100%;
5+
overflow: hidden;
6+
user-select: none;
7+
}
8+
9+
capsule-comparison-before,
10+
capsule-comparison-after {
11+
display: block;
12+
position: absolute;
13+
top: 0;
14+
left: 0;
15+
width: 100%;
16+
height: 100%;
17+
overflow: hidden;
18+
}
19+
20+
capsule-comparison-before {
21+
clip-path: inset(0 calc(100% - var(--comparison-position, 50%)) 0 0);
22+
}
23+
24+
capsule-comparison-after {
25+
clip-path: inset(0 0 0 var(--comparison-position, 50%));
26+
}
27+
28+
capsule-comparison-before > img,
29+
capsule-comparison-after > img,
30+
capsule-comparison-before > video,
31+
capsule-comparison-after > video {
32+
width: 100%;
33+
height: 100%;
34+
object-fit: cover;
35+
display: block;
36+
}
37+
38+
capsule-comparison-line {
39+
position: absolute;
40+
top: 0;
41+
left: var(--comparison-position, 50%);
42+
transform: translateX(-50%);
43+
width: 4px;
44+
height: 100%;
45+
background: var(--capsule-color-surface);
46+
cursor: ew-resize;
47+
z-index: 10;
48+
display: flex;
49+
align-items: center;
50+
justify-content: center;
51+
touch-action: none;
52+
}
53+
54+
capsule-comparison-line:focus-visible {
55+
outline: 2px solid var(--capsule-color-primary);
56+
outline-offset: 2px;
57+
}
58+
59+
capsule-comparison-line .line {
60+
width: 100%;
61+
height: 100%;
62+
display: flex;
63+
align-items: center;
64+
justify-content: center;
65+
position: relative;
66+
}
67+
68+
capsule-comparison-line .icon {
69+
position: absolute;
70+
width: 40px;
71+
height: 40px;
72+
display: flex;
73+
align-items: center;
74+
justify-content: center;
75+
background: var(--capsule-color-surface);
76+
border: 2px solid var(--capsule-color-outline);
77+
border-radius: var(--capsule-radius-full);
78+
box-shadow: var(--capsule-shadow-md);
79+
color: var(--capsule-color-on-surface);
80+
}
81+
82+
capsule-comparison-line .icon svg {
83+
width: 24px;
84+
height: 24px;
85+
}
86+
87+
capsule-comparison-line[aria-pressed='true'] .icon {
88+
background: var(--capsule-color-primary);
89+
color: var(--capsule-color-on-primary);
90+
border-color: var(--capsule-color-primary);
91+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import './capsule-comparison-before.js';
2+
import './capsule-comparison-after.js';
3+
import './capsule-comparison-line.js';
4+
import './capsule-comparison.js';
5+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"version": 1.1,
3+
"tags": [
4+
{
5+
"name": "capsule-comparison",
6+
"attributes": [
7+
{
8+
"name": "position",
9+
"description": "Initial position of the divider line as a percentage (0-100)",
10+
"valueSet": "number"
11+
}
12+
]
13+
},
14+
{
15+
"name": "capsule-comparison-before",
16+
"attributes": []
17+
},
18+
{
19+
"name": "capsule-comparison-after",
20+
"attributes": []
21+
},
22+
{
23+
"name": "capsule-comparison-line",
24+
"attributes": []
25+
}
26+
]
27+
}
28+

0 commit comments

Comments
 (0)