Interactive Elements: Aktivierung über Schalter statt Entfernen-Button

This commit is contained in:
2026-05-23 10:01:42 +02:00
parent 857785da1b
commit 4278019c02
3 changed files with 172 additions and 257 deletions
+162 -180
View File
@@ -120,17 +120,25 @@
<div class="sg-state-example">
<p class="sg-state-example__label sg-table-label">Variante aktivierbar</p>
<div class="sg-pulldown-demo" data-open="false" data-align="left" data-selection-mode="multiple" data-component="pulldown" data-component-state="inactive-selectable" data-activatable="true">
<div class="sg-activatable-row">
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung: aus" data-activation-target="component-pulldown-activatable">
<span class="sg-mode-toggle__switch" aria-hidden="true" data-component-part="toggle-track">
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--left">aus</span>
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--right">an</span>
<span class="sg-mode-toggle__handle" data-component-part="toggle-handle"></span>
</span>
</button>
<div class="sg-pulldown-demo" id="component-pulldown-activatable" data-open="false" data-align="left" data-selection-mode="multiple" data-component="pulldown" data-component-state="inactive-selectable" data-activatable="true">
<!--
The number in brackets shows the count of currently selected options inside this pulldown.
Initial state: no option is selected, therefore the trigger starts as inactive selectable.
Clicking an option activates the pulldown and shows the selected count in brackets.
Initial state: the activation toggle is off, therefore the trigger starts as inactive selectable.
Switching the activation toggle to "an" applies the active default selection and shows the selected count.
-->
<span class="sg-activatable-control">
<button class="sg-interaction-element sg-pulldown sg-pulldown--inactive-selectable sg-pulldown-demo__trigger" type="button" aria-expanded="false" aria-label="Pulldown ohne aktive Auswahl" data-label-base="Auswahl" data-component-part="pulldown-trigger">
Auswahl
</button>
<button class="sg-activatable-remove" type="button" aria-label="Pulldown-Filter entfernen" data-pulldown-activate-remove hidden>×</button>
</span>
<div class="sg-pulldown-panel" aria-label="Geöffnetes Pulldown" data-component-part="pulldown-panel">
@@ -201,6 +209,7 @@
</label>
</div>
</div>
</div>
</div>
</div>
@@ -390,14 +399,22 @@
<span>Standard Checkbox</span>
</label>
<label class="sg-checkbox-field-option sg-checkbox-field-option--inactive-selectable sg-body" data-component="checkbox-field" data-component-state="inactive-selectable" data-activatable="true">
<span class="sg-state-example__label sg-table-label">Variante aktivierbar</span>
<button class="sg-checkbox-field sg-checkbox-field--inactive-selectable" type="button" role="checkbox" aria-checked="false" aria-label="Option wählen">
<span class="sg-checkbox-field__mark" aria-hidden="true"></span>
<div class="sg-activatable-row">
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung: aus" data-activation-target="component-checkbox-activatable">
<span class="sg-mode-toggle__switch" aria-hidden="true" data-component-part="toggle-track">
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--left">aus</span>
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--right">an</span>
<span class="sg-mode-toggle__handle" data-component-part="toggle-handle"></span>
</span>
</button>
<span>Option wählbar</span>
<button class="sg-activatable-remove" type="button" aria-label="Checkbox entfernen" data-checkbox-activate-remove hidden>×</button>
</label>
<label class="sg-checkbox-field-option sg-checkbox-field-option--inactive-selectable sg-body" id="component-checkbox-activatable" data-component="checkbox-field" data-component-state="inactive-selectable" data-activatable="true">
<span class="sg-state-example__label sg-table-label">Variante aktivierbar</span>
<button class="sg-checkbox-field sg-checkbox-field--inactive-selectable" type="button" role="checkbox" aria-checked="false" aria-label="Option wählen">
<span class="sg-checkbox-field__mark" aria-hidden="true"></span>
</button>
<span>Option wählbar</span>
</label>
</div>
<label class="sg-checkbox-field-option sg-checkbox-field-option--disabled sg-body" data-component="checkbox-field" data-component-state="disabled">
<span class="sg-state-example__label sg-table-label">form-disabled</span>
@@ -430,24 +447,32 @@
<span>Standard Radio</span>
</label>
<label class="sg-checkbox-field-option sg-checkbox-field-option--inactive-selectable sg-body sg-radio-activatable-group" data-component="radio-field" data-component-state="inactive-selectable" data-activatable="true" data-activatable-radio-group="true">
<span class="sg-state-example__label sg-table-label">Variante aktivierbar</span>
<span class="sg-radio-activatable-group__choices">
<span class="sg-radio-activatable-group__choice">
<button class="sg-radio-field sg-radio-field--inactive-selectable" type="button" role="radio" aria-checked="false" aria-label="Option 1 wählen">
<span class="sg-radio-field__mark" aria-hidden="true"></span>
</button>
<span>Option 1</span>
<div class="sg-activatable-row">
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung: aus" data-activation-target="component-radio-activatable">
<span class="sg-mode-toggle__switch" aria-hidden="true" data-component-part="toggle-track">
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--left">aus</span>
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--right">an</span>
<span class="sg-mode-toggle__handle" data-component-part="toggle-handle"></span>
</span>
<span class="sg-radio-activatable-group__choice">
<button class="sg-radio-field sg-radio-field--inactive-selectable" type="button" role="radio" aria-checked="false" aria-label="Option 2 wählen">
<span class="sg-radio-field__mark" aria-hidden="true"></span>
</button>
<span>Option 2</span>
</button>
<label class="sg-checkbox-field-option sg-checkbox-field-option--inactive-selectable sg-body sg-radio-activatable-group" id="component-radio-activatable" data-component="radio-field" data-component-state="inactive-selectable" data-activatable="true" data-activatable-radio-group="true">
<span class="sg-state-example__label sg-table-label">Variante aktivierbar</span>
<span class="sg-radio-activatable-group__choices">
<span class="sg-radio-activatable-group__choice">
<button class="sg-radio-field sg-radio-field--inactive-selectable" type="button" role="radio" aria-checked="false" aria-label="Option 1 wählen">
<span class="sg-radio-field__mark" aria-hidden="true"></span>
</button>
<span>Option 1</span>
</span>
<span class="sg-radio-activatable-group__choice">
<button class="sg-radio-field sg-radio-field--inactive-selectable" type="button" role="radio" aria-checked="false" aria-label="Option 2 wählen">
<span class="sg-radio-field__mark" aria-hidden="true"></span>
</button>
<span>Option 2</span>
</span>
</span>
</span>
<button class="sg-activatable-remove" type="button" aria-label="Radio-Auswahl entfernen" data-radio-activate-remove hidden>×</button>
</label>
</label>
</div>
<label class="sg-checkbox-field-option sg-checkbox-field-option--disabled sg-body" data-component="radio-field" data-component-state="disabled">
<span class="sg-state-example__label sg-table-label">form-disabled</span>
@@ -528,20 +553,28 @@
<div class="sg-state-example">
<p class="sg-state-example__label sg-table-label">Variante aktivierbar</p>
<label class="sg-slider-row sg-slider-row--inactive-selectable" data-component="slider" data-component-state="inactive-selectable" data-activatable="true">
<span class="sg-label">Wert</span>
<input
class="sg-interaction-element sg-slider sg-form-inactive-selectable"
type="range"
min="0"
max="10"
step="0.1"
value="2.0"
aria-label="Slider inaktiv auswählbar"
>
<output class="sg-slider-value sg-body" for="slider">2.0</output>
<button class="sg-activatable-remove" type="button" aria-label="Slider-Filter entfernen" data-slider-activate-remove hidden>×</button>
</label>
<div class="sg-activatable-row">
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung: aus" data-activation-target="component-slider-activatable">
<span class="sg-mode-toggle__switch" aria-hidden="true" data-component-part="toggle-track">
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--left">aus</span>
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--right">an</span>
<span class="sg-mode-toggle__handle" data-component-part="toggle-handle"></span>
</span>
</button>
<label class="sg-slider-row sg-slider-row--inactive-selectable" id="component-slider-activatable" data-component="slider" data-component-state="inactive-selectable" data-activatable="true">
<span class="sg-label">Wert</span>
<input
class="sg-interaction-element sg-slider sg-form-inactive-selectable"
type="range"
min="0"
max="10"
step="0.1"
value="2.0"
aria-label="Slider inaktiv auswählbar"
>
<output class="sg-slider-value sg-body" for="slider">2.0</output>
</label>
</div>
</div>
</div>
</section>
@@ -625,11 +658,80 @@
});
});
const setActivatableComponentState = (target, isActive) => {
if (!target) {
return;
}
const componentType = target.dataset.component;
target.dataset.componentState = isActive ? 'active' : 'inactive-selectable';
if (componentType === 'pulldown') {
const trigger = target.querySelector('.sg-pulldown-demo__trigger');
const options = target.querySelectorAll('[data-pulldown-option]');
if (!trigger || options.length === 0) {
return;
}
options.forEach((option, index) => {
option.setAttribute('aria-checked', String(isActive && index === 0));
});
target.dataset.open = 'false';
trigger.setAttribute('aria-expanded', 'false');
updatePulldownSelectionState(target);
return;
}
if (componentType === 'checkbox-field') {
const checkbox = target.querySelector('.sg-checkbox-field');
if (!checkbox) {
return;
}
checkbox.setAttribute('aria-checked', String(isActive));
checkbox.classList.toggle('sg-form-active', isActive);
checkbox.classList.toggle('sg-checkbox-field--inactive-selectable', !isActive);
return;
}
if (componentType === 'radio-field') {
const radios = target.querySelectorAll('.sg-radio-field');
const defaultRadio = radios[0];
radios.forEach((radio) => {
const checked = isActive && radio === defaultRadio;
radio.setAttribute('aria-checked', String(checked));
radio.classList.toggle('sg-form-active', checked);
radio.classList.toggle('sg-radio-field--inactive-selectable', !isActive);
});
return;
}
if (componentType === 'slider') {
const slider = target.querySelector('.sg-slider');
if (!slider) {
return;
}
target.classList.toggle('sg-slider-row--inactive-selectable', !isActive);
slider.classList.toggle('sg-form-active', isActive);
slider.classList.toggle('sg-form-inactive-selectable', !isActive);
}
};
document.querySelectorAll('.sg-activation-mode-toggle').forEach((toggle) => {
toggle.addEventListener('click', () => {
const nextState = toggle.dataset.active === 'absolute' ? 'relative' : 'absolute';
const isActive = nextState === 'relative';
toggle.dataset.active = nextState;
toggle.setAttribute('aria-label', `Aktivierung: ${nextState === 'absolute' ? 'aus' : 'an'}`);
toggle.setAttribute('aria-label', `Aktivierung: ${isActive ? 'an' : 'aus'}`);
const targetId = toggle.dataset.activationTarget;
if (!targetId) {
return;
}
const target = document.getElementById(targetId);
setActivatableComponentState(target, isActive);
});
});
@@ -669,8 +771,6 @@
document.querySelectorAll('.sg-slider-row').forEach((row) => {
const slider = row.querySelector('.sg-slider');
const valueOutput = row.querySelector('.sg-slider-value');
const removeButton = row.querySelector('[data-slider-activate-remove]');
if (!slider || !valueOutput) {
return;
}
@@ -686,25 +786,12 @@
valueOutput.textContent = value.toFixed(1);
};
const activateSliderRow = () => {
if (row.dataset.activatable === 'true' && row.dataset.componentState === 'inactive-selectable') {
row.dataset.componentState = 'active';
row.classList.remove('sg-slider-row--inactive-selectable');
slider.classList.remove('sg-form-inactive-selectable');
slider.classList.add('sg-form-active');
if (removeButton) {
removeButton.hidden = false;
}
}
};
slider.addEventListener('input', () => {
activateSliderRow();
if (row.dataset.activatable === 'true' && row.dataset.componentState !== 'active') {
return;
}
updateSliderState();
});
slider.addEventListener('pointerdown', activateSliderRow);
slider.addEventListener('click', activateSliderRow);
updateSliderState();
});
@@ -716,15 +803,9 @@
const activatableOption = checkbox.closest('[data-component="checkbox-field"][data-activatable="true"]');
if (activatableOption) {
checkbox.setAttribute('aria-checked', 'true');
checkbox.classList.add('sg-form-active');
checkbox.classList.remove('sg-checkbox-field--inactive-selectable');
activatableOption.dataset.componentState = 'active';
const removeButton = activatableOption.querySelector('[data-checkbox-activate-remove]');
if (removeButton) {
removeButton.hidden = false;
if (activatableOption.dataset.componentState !== 'active') {
return;
}
return;
}
const nextState = checkbox.getAttribute('aria-checked') !== 'true';
@@ -744,6 +825,9 @@
if (!pulldownDemo) {
return;
}
if (pulldownDemo.dataset.activatable === 'true' && pulldownDemo.dataset.componentState !== 'active') {
return;
}
const selectionMode = pulldownDemo.dataset.selectionMode || 'single';
if (selectionMode === 'multiple') {
@@ -775,32 +859,16 @@
const activatableGroup = radio.closest('[data-activatable-radio-group="true"]');
if (activatableGroup) {
const radios = activatableGroup.querySelectorAll('.sg-radio-field');
const defaultRadio = radios[0];
const removeButton = activatableGroup.querySelector('[data-radio-activate-remove]');
if (activatableGroup.dataset.componentState === 'inactive-selectable' && defaultRadio) {
radios.forEach((otherRadio) => {
otherRadio.setAttribute('aria-checked', String(otherRadio === defaultRadio));
otherRadio.classList.remove('sg-radio-field--inactive-selectable');
otherRadio.classList.toggle('sg-form-active', otherRadio === defaultRadio);
});
activatableGroup.dataset.componentState = 'active';
if (removeButton) {
removeButton.hidden = false;
}
if (activatableGroup.dataset.componentState !== 'active') {
return;
}
const radios = activatableGroup.querySelectorAll('.sg-radio-field');
radios.forEach((otherRadio) => {
otherRadio.setAttribute('aria-checked', String(otherRadio === radio));
otherRadio.classList.remove('sg-radio-field--inactive-selectable');
otherRadio.classList.toggle('sg-form-active', otherRadio === radio);
});
activatableGroup.dataset.componentState = 'active';
if (removeButton) {
removeButton.hidden = false;
}
return;
}
@@ -818,7 +886,6 @@
const updatePulldownSelectionState = (demo) => {
const trigger = demo.querySelector('.sg-pulldown-demo__trigger');
const selectableOptions = demo.querySelectorAll('[data-pulldown-option]');
const activatableRemove = demo.querySelector('[data-pulldown-activate-remove]');
if (!trigger || selectableOptions.length === 0) {
return;
@@ -843,15 +910,13 @@
const labelBase = trigger.dataset.labelBase || 'Auswahl';
trigger.textContent = selectedCount > 0 ? `${labelBase} (${selectedCount})` : labelBase;
trigger.classList.toggle('sg-pulldown--selected', selectedCount > 0);
trigger.classList.toggle('sg-form-active', selectedCount > 0);
trigger.classList.toggle('sg-pulldown--inactive-selectable', selectedCount === 0);
if (activatableRemove) {
activatableRemove.hidden = selectedCount === 0;
}
const isActivatableActive = demo.dataset.activatable === 'true' && demo.dataset.componentState === 'active';
trigger.classList.toggle('sg-pulldown--selected', selectedCount > 0 || isActivatableActive);
trigger.classList.toggle('sg-form-active', selectedCount > 0 || isActivatableActive);
trigger.classList.toggle('sg-pulldown--inactive-selectable', selectedCount === 0 && !isActivatableActive);
trigger.setAttribute(
'aria-label',
selectedCount > 0 ? 'Pulldown mit aktiver Auswahl' : 'Pulldown ohne aktive Auswahl'
selectedCount > 0 || isActivatableActive ? 'Pulldown mit aktiver Auswahl' : 'Pulldown ohne aktive Auswahl'
);
};
@@ -869,17 +934,10 @@
trigger.addEventListener('click', (event) => {
event.stopPropagation();
const nextState = demo.dataset.open !== 'true';
if (demo.dataset.activatable === 'true') {
const activatableRemove = demo.querySelector('[data-pulldown-activate-remove]');
trigger.classList.add('sg-pulldown--selected', 'sg-form-active');
trigger.classList.remove('sg-pulldown--inactive-selectable');
trigger.setAttribute('aria-label', 'Pulldown mit aktiver Auswahl');
if (activatableRemove) {
activatableRemove.hidden = false;
}
if (demo.dataset.activatable === 'true' && demo.dataset.componentState !== 'active') {
return;
}
const nextState = demo.dataset.open !== 'true';
document.querySelectorAll('.sg-pulldown-demo').forEach((otherDemo) => {
const otherTrigger = otherDemo.querySelector('.sg-pulldown-demo__trigger');
@@ -922,82 +980,6 @@
});
});
document.querySelectorAll('[data-pulldown-activate-remove]').forEach((removeButton) => {
removeButton.addEventListener('click', (event) => {
event.stopPropagation();
const demo = removeButton.closest('.sg-pulldown-demo');
const trigger = demo ? demo.querySelector('.sg-pulldown-demo__trigger') : null;
if (!demo || !trigger) {
return;
}
demo.querySelectorAll('[data-pulldown-option]').forEach((option) => {
option.setAttribute('aria-checked', 'false');
});
demo.dataset.componentState = 'inactive-selectable';
demo.dataset.open = 'false';
trigger.setAttribute('aria-expanded', 'false');
updatePulldownSelectionState(demo);
});
});
document.querySelectorAll('[data-checkbox-activate-remove]').forEach((removeButton) => {
removeButton.addEventListener('click', (event) => {
event.stopPropagation();
const option = removeButton.closest('[data-component="checkbox-field"][data-activatable="true"]');
const checkbox = option ? option.querySelector('.sg-checkbox-field') : null;
if (!option || !checkbox) {
return;
}
option.dataset.componentState = 'inactive-selectable';
checkbox.setAttribute('aria-checked', 'false');
checkbox.classList.remove('sg-form-active');
checkbox.classList.add('sg-checkbox-field--inactive-selectable');
removeButton.hidden = true;
});
});
document.querySelectorAll('[data-radio-activate-remove]').forEach((removeButton) => {
removeButton.addEventListener('click', (event) => {
event.stopPropagation();
const option = removeButton.closest('[data-activatable-radio-group="true"]');
const radios = option ? option.querySelectorAll('.sg-radio-field') : null;
if (!option || !radios || radios.length === 0) {
return;
}
option.dataset.componentState = 'inactive-selectable';
radios.forEach((radio) => {
radio.setAttribute('aria-checked', 'false');
radio.classList.remove('sg-form-active');
radio.classList.add('sg-radio-field--inactive-selectable');
});
removeButton.hidden = true;
});
});
document.querySelectorAll('[data-slider-activate-remove]').forEach((removeButton) => {
removeButton.addEventListener('click', (event) => {
event.stopPropagation();
const row = removeButton.closest('.sg-slider-row[data-activatable="true"]');
const slider = row ? row.querySelector('.sg-slider') : null;
if (!row || !slider) {
return;
}
row.dataset.componentState = 'inactive-selectable';
row.classList.add('sg-slider-row--inactive-selectable');
slider.classList.remove('sg-form-active');
slider.classList.add('sg-form-inactive-selectable');
removeButton.hidden = true;
});
});
// Filter rows inside the opened pulldown start as inactive preselected rows.
// They are visually dimmed but remain operable; clicking or changing them activates the row.
document.querySelectorAll('[data-pulldown-filter-row]').forEach((row) => {