Skip to content

Commit 6f00216

Browse files
committed
added radio group
1 parent b5aa707 commit 6f00216

File tree

20 files changed

+1434
-1
lines changed

20 files changed

+1434
-1
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"docs/@capsule/components/capsule-container/vscode.data.json",
2727
"docs/@capsule/components/capsule-icon/vscode.data.json",
2828
"docs/@capsule/components/capsule-input/vscode.data.json",
29-
"docs/@capsule/components/capsule-hover-card/vscode.data.json"
29+
"docs/@capsule/components/capsule-hover-card/vscode.data.json",
30+
"docs/@capsule/components/capsule-radio-group/vscode.data.json"
3031
]
3132
}

docs/.vitepress/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export default defineConfig({
8080
{ text: 'Button', link: '/components/button' },
8181
{ text: 'Input', link: '/components/input' },
8282
{ text: 'Textarea', link: '/components/textarea' },
83+
{ text: 'RadioGroup', link: '/components/radio-group' },
8384
{ text: 'Button Group', link: '/components/button-group' },
8485
{ text: 'Badge', link: '/components/badge' },
8586
{ text: 'Kbd', link: '/components/kbd' },
@@ -150,6 +151,7 @@ export default defineConfig({
150151
{ text: 'Button', link: '/ru/components/button' },
151152
{ text: 'Input', link: '/ru/components/input' },
152153
{ text: 'Textarea', link: '/ru/components/textarea' },
154+
{ text: 'RadioGroup', link: '/ru/components/radio-group' },
153155
{ text: 'Button Group', link: '/ru/components/button-group' },
154156
{ text: 'Badge', link: '/ru/components/badge' },
155157
{ text: 'Kbd', link: '/ru/components/kbd' },
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { LitElement, html } from '../../lit';
2+
3+
class CapsuleRadioGroupIndicator extends LitElement {
4+
constructor() {
5+
super();
6+
}
7+
8+
createRenderRoot() {
9+
return super.createRenderRoot();
10+
}
11+
12+
connectedCallback() {
13+
super.connectedCallback();
14+
this.setAttribute('part', 'indicator-inner');
15+
}
16+
17+
render() {
18+
return html`<slot></slot>`;
19+
}
20+
}
21+
22+
customElements.define('capsule-radio-group-indicator', CapsuleRadioGroupIndicator);
23+
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { LitElement, html } from '../../lit';
2+
3+
class CapsuleRadioGroupItem extends LitElement {
4+
static formAssociated = true;
5+
6+
static properties = {
7+
value: { type: String, reflect: true },
8+
disabled: { type: Boolean, reflect: true },
9+
checked: { type: Boolean, reflect: true },
10+
};
11+
12+
constructor() {
13+
super();
14+
this._internals = this.attachInternals();
15+
this.value = '';
16+
this.disabled = false;
17+
this.checked = false;
18+
}
19+
20+
createRenderRoot() {
21+
return super.createRenderRoot();
22+
}
23+
24+
connectedCallback() {
25+
super.connectedCallback();
26+
this.setAttribute('role', 'radio');
27+
this._syncWithGroup();
28+
this.setAttribute('tabindex', this.checked ? '0' : '-1');
29+
this._updateAria();
30+
this._attachListeners();
31+
}
32+
33+
_getGroup() {
34+
const tagName = this.tagName.toLowerCase();
35+
if (!tagName.endsWith('-item')) {
36+
return null;
37+
}
38+
const groupTagName = tagName.slice(0, -5); // Remove '-item'
39+
return this.closest(groupTagName);
40+
}
41+
42+
_syncWithGroup() {
43+
const group = this._getGroup();
44+
if (group) {
45+
// Sync checked state
46+
if (group.value != null) {
47+
this.checked = this.value === group.value;
48+
}
49+
// Sync disabled state
50+
if (group.disabled != null) {
51+
this.disabled = group.disabled;
52+
}
53+
}
54+
}
55+
56+
disconnectedCallback() {
57+
super.disconnectedCallback();
58+
this._detachListeners();
59+
}
60+
61+
updated(changedProperties) {
62+
if (changedProperties.has('checked')) {
63+
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
64+
this.setAttribute('tabindex', this.checked ? '0' : '-1');
65+
this._updateFormValue();
66+
}
67+
if (changedProperties.has('disabled')) {
68+
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
69+
this.setAttribute(
70+
'tabindex',
71+
this.disabled ? '-1' : this.checked ? '0' : '-1'
72+
);
73+
}
74+
}
75+
76+
_attachListeners() {
77+
this.addEventListener('click', this._handleClick);
78+
this.addEventListener('keydown', this._handleKeyDown);
79+
}
80+
81+
_detachListeners() {
82+
this.removeEventListener('click', this._handleClick);
83+
this.removeEventListener('keydown', this._handleKeyDown);
84+
}
85+
86+
_handleClick(event) {
87+
if (this.disabled) {
88+
event.preventDefault();
89+
event.stopPropagation();
90+
return;
91+
}
92+
this._select();
93+
}
94+
95+
_handleKeyDown(event) {
96+
if (this.disabled) return;
97+
98+
if (event.key === ' ' || event.key === 'Enter') {
99+
event.preventDefault();
100+
this._select();
101+
} else if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
102+
event.preventDefault();
103+
this._selectNext();
104+
} else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
105+
event.preventDefault();
106+
this._selectPrevious();
107+
}
108+
}
109+
110+
_select() {
111+
if (this.checked || this.disabled) return;
112+
113+
const group = this._getGroup();
114+
if (group && typeof group.setValue === 'function') {
115+
group.setValue(this.value);
116+
} else {
117+
this.checked = true;
118+
this._dispatchChange();
119+
}
120+
}
121+
122+
_selectNext() {
123+
const items = this._getItems();
124+
if (items.length === 0) return;
125+
126+
const currentIndex = items.indexOf(this);
127+
const nextIndex = (currentIndex + 1) % items.length;
128+
const nextItem = items[nextIndex];
129+
if (nextItem) {
130+
nextItem._select();
131+
nextItem.focus();
132+
}
133+
}
134+
135+
_selectPrevious() {
136+
const items = this._getItems();
137+
if (items.length === 0) return;
138+
139+
const currentIndex = items.indexOf(this);
140+
const previousIndex = (currentIndex - 1 + items.length) % items.length;
141+
const previousItem = items[previousIndex];
142+
if (previousItem) {
143+
previousItem._select();
144+
previousItem.focus();
145+
}
146+
}
147+
148+
_getItems() {
149+
const group = this._getGroup();
150+
if (!group) return [this];
151+
152+
const itemTagName = this.tagName.toLowerCase();
153+
return Array.from(group.querySelectorAll(itemTagName)).filter(
154+
(item) => item && !item.disabled
155+
);
156+
}
157+
158+
_dispatchChange() {
159+
this.dispatchEvent(
160+
new CustomEvent('change', {
161+
bubbles: true,
162+
detail: {
163+
value: this.value,
164+
},
165+
})
166+
);
167+
}
168+
169+
_updateAria() {
170+
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
171+
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
172+
}
173+
174+
_updateFormValue() {
175+
if (this._internals) {
176+
this._internals.setFormValue(this.checked ? this.value : null);
177+
}
178+
}
179+
180+
setChecked(checked) {
181+
this.checked = checked;
182+
}
183+
184+
setDisabled(disabled) {
185+
this.disabled = disabled;
186+
}
187+
188+
render() {
189+
return html`<slot></slot>`;
190+
}
191+
}
192+
193+
customElements.define('capsule-radio-group-item', CapsuleRadioGroupItem);
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { LitElement, html } from '../../lit';
2+
3+
class CapsuleRadioGroup extends LitElement {
4+
static formAssociated = true;
5+
6+
static properties = {
7+
value: { type: String, reflect: true },
8+
disabled: { type: Boolean, reflect: true },
9+
};
10+
11+
constructor() {
12+
super();
13+
this._internals = this.attachInternals();
14+
this.value = '';
15+
this.disabled = false;
16+
}
17+
18+
createRenderRoot() {
19+
return this;
20+
}
21+
22+
connectedCallback() {
23+
super.connectedCallback();
24+
this.setAttribute('role', 'radiogroup');
25+
this._updateFormValue();
26+
this._attachListeners();
27+
}
28+
29+
disconnectedCallback() {
30+
super.disconnectedCallback();
31+
this._detachListeners();
32+
}
33+
34+
updated(changedProperties) {
35+
if (changedProperties.has('value')) {
36+
this._updateFormValue();
37+
this._updateItems();
38+
this._dispatchChange();
39+
}
40+
if (changedProperties.has('disabled')) {
41+
this._updateItems();
42+
}
43+
}
44+
45+
_attachListeners() {
46+
this.addEventListener('change', this._handleItemChange);
47+
}
48+
49+
_detachListeners() {
50+
this.removeEventListener('change', this._handleItemChange);
51+
}
52+
53+
_handleItemChange(event) {
54+
const item = event.target;
55+
if (
56+
item &&
57+
item.tagName &&
58+
item.tagName.toLowerCase().endsWith('-radio-group-item')
59+
) {
60+
const newValue = item.value;
61+
if (newValue !== this.value) {
62+
this.value = newValue;
63+
this._updateItems();
64+
this._dispatchChange();
65+
}
66+
}
67+
}
68+
69+
_updateItems() {
70+
const itemTagName = `${this.tagName.toLowerCase()}-item`;
71+
const items = this.querySelectorAll(itemTagName);
72+
items.forEach((item) => {
73+
if (item && typeof item.setChecked === 'function') {
74+
item.setChecked(item.value === this.value);
75+
}
76+
if (item && typeof item.setDisabled === 'function') {
77+
item.setDisabled(this.disabled);
78+
}
79+
});
80+
}
81+
82+
_updateFormValue() {
83+
if (this._internals) {
84+
this._internals.setFormValue(this.value === '' ? null : this.value);
85+
}
86+
}
87+
88+
_dispatchChange() {
89+
this.dispatchEvent(
90+
new CustomEvent('change', {
91+
bubbles: true,
92+
detail: {
93+
value: this.value,
94+
},
95+
})
96+
);
97+
}
98+
99+
setValue(value) {
100+
if (value !== this.value) {
101+
this.value = value;
102+
this._updateItems();
103+
this._dispatchChange();
104+
}
105+
}
106+
107+
render() {
108+
return html`<slot></slot>`;
109+
}
110+
}
111+
112+
customElements.define('capsule-radio-group', CapsuleRadioGroup);

0 commit comments

Comments
 (0)