Skip to content

Commit 2948b0e

Browse files
committed
IBX-10850: AltRadio List Field
1 parent cc83b8b commit 2948b0e

File tree

7 files changed

+197
-23
lines changed

7 files changed

+197
-23
lines changed

src/bundle/Resources/public/ts/components/alt_radio/alt_radio_input.ts

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { Base } from '../../partials';
22

3+
export interface AltRadioInputOptions {
4+
onTileClick?: (event: MouseEvent, inputId: string) => void;
5+
}
6+
37
export class AltRadioInput extends Base {
4-
private _inputElement: HTMLInputElement;
5-
private _tileElement: HTMLDivElement;
8+
private inputElement: HTMLInputElement;
9+
private tileElement: HTMLDivElement;
10+
private onTileClick?: (event: MouseEvent, inputId: string) => void;
611

7-
private _isFocused = false;
12+
private isChecked = false;
13+
private isFocused = false;
814

9-
constructor(container: HTMLDivElement) {
15+
constructor(container: HTMLDivElement, { onTileClick }: AltRadioInputOptions = {}) {
1016
super(container);
1117

1218
const inputElement = this._container.querySelector<HTMLInputElement>('.ids-alt-radio__source .ids-input');
@@ -16,55 +22,67 @@ export class AltRadioInput extends Base {
1622
throw new Error('AltRadio: Required elements are missing in the container.');
1723
}
1824

19-
this._inputElement = inputElement;
20-
this._tileElement = tileElement;
25+
this.inputElement = inputElement;
26+
this.tileElement = tileElement;
27+
28+
this.onTileClick = onTileClick;
2129
}
2230

2331
setFocus(nextIsFocused: boolean) {
24-
if (this._isFocused === nextIsFocused) {
32+
if (this.isFocused === nextIsFocused) {
2533
return;
2634
}
2735

28-
this._isFocused = nextIsFocused;
36+
this.isFocused = nextIsFocused;
2937

30-
this._tileElement.classList.toggle('ids-alt-radio__tile--focused', nextIsFocused);
38+
this.tileElement.classList.toggle('ids-alt-radio__tile--focused', nextIsFocused);
3139

3240
if (nextIsFocused) {
33-
this._inputElement.focus();
41+
this.inputElement.focus();
3442
} else {
35-
this._inputElement.blur();
43+
this.inputElement.blur();
3644
}
3745
}
3846

3947
setError(value: boolean) {
40-
this._tileElement.classList.toggle('ids-alt-radio__tile--error', value);
48+
this.tileElement.classList.toggle('ids-alt-radio_tile--error', value);
4149
}
4250

4351
getInputElement(): HTMLInputElement {
44-
return this._inputElement;
52+
return this.inputElement;
53+
}
54+
55+
toggleChecked(value?: boolean) {
56+
const isChecked = value ?? !this.isChecked;
57+
58+
this.isChecked = isChecked;
59+
this.inputElement.checked = isChecked;
60+
this.tileElement.classList.toggle('ids-alt-radio__tile--checked', isChecked);
4561
}
4662

4763
initInputListeners() {
48-
this._inputElement.addEventListener('focus', () => {
64+
this.inputElement.addEventListener('focus', () => {
4965
this.setFocus(true);
5066
});
5167

52-
this._inputElement.addEventListener('blur', () => {
68+
this.inputElement.addEventListener('blur', () => {
5369
this.setFocus(false);
5470
});
5571

56-
this._inputElement.addEventListener('input', () => {
57-
this._tileElement.classList.toggle('ids-alt-radio__tile--checked', this._inputElement.checked);
72+
this.inputElement.addEventListener('input', () => {
73+
this.toggleChecked();
5874
});
5975
}
6076

6177
initTileBtn() {
62-
this._tileElement.addEventListener('click', (event) => {
78+
this.tileElement.addEventListener('click', (event) => {
6379
event.preventDefault();
6480
event.stopPropagation();
6581

66-
this._inputElement.focus();
67-
this._inputElement.click();
82+
this.inputElement.focus();
83+
this.inputElement.click();
84+
85+
this.onTileClick?.(event, this.inputElement.id);
6886
});
6987
}
7088

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { AltRadioInput } from './alt_radio_input';
2+
import { BaseInputsList } from '../../partials';
3+
4+
export enum AltRadiosListFieldAction {
5+
Check = 'check',
6+
Uncheck = 'uncheck',
7+
}
8+
9+
export class AltRadiosListField extends BaseInputsList<string> {
10+
private itemsContainer: HTMLDivElement;
11+
private itemsMap = new Map<string, AltRadioInput>();
12+
protected value?: string;
13+
14+
static EVENTS = {
15+
...BaseInputsList.EVENTS,
16+
CHANGE: 'ids:alt-radio-list-field:change',
17+
};
18+
19+
constructor(container: HTMLDivElement) {
20+
super(container);
21+
22+
const itemsContainer = container.querySelector<HTMLDivElement>('.ids-choice-inputs-list__items');
23+
24+
if (!itemsContainer) {
25+
throw new Error('AltRadiosListField: Required elements are missing in the container.');
26+
}
27+
28+
this.itemsContainer = itemsContainer;
29+
30+
this.onItemClick = this.onItemClick.bind(this);
31+
32+
this.saveItemsInstancesToMap();
33+
}
34+
35+
protected saveItemsInstancesToMap() {
36+
const itemsButtons = this.getItemsButtons();
37+
38+
this.itemsMap.clear();
39+
40+
itemsButtons.forEach((button) => {
41+
const buttonInstance = new AltRadioInput(button, { onTileClick: this.onItemClick });
42+
const buttonId = buttonInstance.getInputElement().id;
43+
44+
this.itemsMap.set(buttonId, buttonInstance);
45+
});
46+
}
47+
48+
getItemsButtons() {
49+
const itemsButtons = [...this.itemsContainer.querySelectorAll<HTMLDivElement>('.ids-alt-radio')];
50+
51+
return itemsButtons;
52+
}
53+
54+
protected onItemClick(event: MouseEvent, itemValue: string) {
55+
if (!(event.target instanceof HTMLDivElement)) {
56+
return;
57+
}
58+
59+
this.onChange(itemValue);
60+
}
61+
62+
protected onChange(itemValue: string) {
63+
const changeEvent = new CustomEvent(AltRadiosListField.EVENTS.CHANGE, {
64+
bubbles: true,
65+
detail: itemValue,
66+
});
67+
68+
const currentValueInstance = this.itemsMap.get(this.value ?? '');
69+
70+
if (currentValueInstance) {
71+
currentValueInstance.toggleChecked(false);
72+
}
73+
74+
const nextValueInstance = this.itemsMap.get(itemValue);
75+
76+
if (nextValueInstance) {
77+
nextValueInstance.toggleChecked(true);
78+
}
79+
80+
this.value = itemValue;
81+
this._container.dispatchEvent(changeEvent);
82+
}
83+
84+
protected initButtons() {
85+
this.itemsMap.forEach((itemInstance) => {
86+
itemInstance.init();
87+
});
88+
}
89+
90+
public init() {
91+
super.init();
92+
93+
this.initButtons();
94+
}
95+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './alt_radio_input';
2+
export * from './alt_radios_list_field';

src/bundle/Resources/public/ts/init_components.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { AltRadioInput, AltRadiosListField } from './components/alt_radio';
12
import { CheckboxInput, CheckboxesListField } from './components/checkbox';
23
import { InputTextField, InputTextInput } from './components/input_text';
34
import { Accordion } from './components/accordion';
4-
import { AltRadioInput } from './components/alt_radio/alt_radio_input';
55
import { DropdownSingleInput } from './components/dropdown/dropdown_single_input';
66
import { OverflowList } from './components/overflow_list';
77

@@ -21,6 +21,14 @@ altRadioContainers.forEach((altRadioContainer: HTMLDivElement) => {
2121
altRadioInstance.init();
2222
});
2323

24+
const altRadiosListContainers = document.querySelectorAll<HTMLDivElement>('.ids-alt-radio-list-field:not([data-ids-custom-init])');
25+
26+
altRadiosListContainers.forEach((altRadiosListContainer: HTMLDivElement) => {
27+
const altRadiosListInstance = new AltRadiosListField(altRadiosListContainer);
28+
29+
altRadiosListInstance.init();
30+
});
31+
2432
const checkboxContainers = document.querySelectorAll<HTMLDivElement>('.ids-checkbox:not([data-ids-custom-init])');
2533

2634
checkboxContainers.forEach((checkboxContainer: HTMLDivElement) => {
@@ -29,7 +37,7 @@ checkboxContainers.forEach((checkboxContainer: HTMLDivElement) => {
2937
checkboxInstance.init();
3038
});
3139

32-
const checkboxesFieldContainers = document.querySelectorAll<HTMLDivElement>('.ids-field.ids-field--list:not([data-ids-custom-init])');
40+
const checkboxesFieldContainers = document.querySelectorAll<HTMLDivElement>('.ids-checkboxes-list-field:not([data-ids-custom-init])');
3341

3442
checkboxesFieldContainers.forEach((checkboxesFieldContainer: HTMLDivElement) => {
3543
const checkboxesFieldInstance = new CheckboxesListField(checkboxesFieldContainer);

src/bundle/Resources/views/themes/standard/design_system/components/alt_radio/input.html.twig

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
tile_class,
1111
)
1212
%}
13+
{% set custom_init = attributes.render('data-ids-custom-init') is not null %}
1314

14-
<div class="{{ component_classes }}">
15+
<div class="{{ component_classes }}"{{ custom_init ? ' data-ids-custom-init' : '' }}>
1516
<div class="ids-alt-radio__source">
1617
<input
1718
class="ids-input ids-input--radio"
1819
type="radio"
20+
name="{{ name }}"
1921
{{ attributes.defaults({ checked, disabled, required }) }}
2022
/>
2123
</div>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{% extends '@IbexaDesignSystemTwig/themes/standard/design_system/partials/base_inputs_list.html.twig' %}
2+
3+
{% set class = html_classes('ids-alt-radio-list-field', attributes.render('class') ?? '') %}
4+
5+
{% block item %}
6+
<twig:ibexa:alt_radio:input {{ ...item }} data-ids-custom-init="true">
7+
{{ item.label }}
8+
</twig:ibexa:alt_radio:input>
9+
{% endblock item %}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\DesignSystemTwig\Twig\Components\AltRadio;
10+
11+
use Ibexa\DesignSystemTwig\Twig\Components\AbstractField;
12+
use Ibexa\DesignSystemTwig\Twig\Components\ListFieldTrait;
13+
use Symfony\Component\OptionsResolver\OptionsResolver;
14+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
15+
16+
/**
17+
* @phpstan-type AltRadioItem array{
18+
* value: string|int,
19+
* label: string,
20+
* disabled?: bool
21+
* }
22+
* @phpstan-type AltRadioItems list<AltRadioItem>
23+
*/
24+
#[AsTwigComponent('ibexa:alt_radio:list_field')]
25+
final class ListField extends AbstractField
26+
{
27+
use ListFieldTrait;
28+
29+
public string $value = '';
30+
31+
protected function configurePropsResolver(OptionsResolver $resolver): void
32+
{
33+
$this->validateListFieldProps($resolver);
34+
35+
// TODO: check if items have value and label component
36+
37+
$resolver->setDefaults(['direction' => 'horizontal']);
38+
$resolver->setDefaults(['value' => '']);
39+
$resolver->setAllowedTypes('value', 'string');
40+
}
41+
}

0 commit comments

Comments
 (0)