The component displays a switch with optional left and right labels or icons, and fires a
+ * value-change event when toggled.
+ *
+ * @since 1.0.0
+ */
+@Tag("fc-toggle-button")
+@JsModule("./fc-toggle-button.js")
+public class ToggleButton extends AbstractSinglePropertyField By default the order is {@code [left-icon] [left-label] [switch] [right-label]
+ * [right-icon]}.
+ *
+ * @return this instance for method chaining
+ * @since 1.0.0
+ */
+ public ToggleButton withIconsInside() {
+ getElement().setProperty("iconsInside", true);
+ return this;
+ }
+
+ /**
+ * Restores the default layout where icons are on the outer edges and labels are adjacent to the
+ * switch: {@code [left-icon] [left-label] [switch] [right-label] [right-icon]}.
+ *
+ * @return this instance for method chaining
+ * @since 1.0.0
+ */
+ public ToggleButton withIconsOutside() {
+ getElement().setProperty("iconsInside", false);
+ return this;
+ }
+
+ /**
+ * Disables label highlighting so both labels are rendered with the same color regardless of the
+ * toggle state.
+ *
+ * @return this instance for method chaining
+ * @since 1.0.0
+ */
+ public ToggleButton withoutHighlightLabel() {
+ getElement().setProperty("highlightLabel", false);
+ return this;
+ }
+
+ /**
+ * Sets the label displayed on the left side of the toggle switch.
+ *
+ * @param label the left label text
+ * @return this instance for method chaining
+ * @since 1.0.0
+ */
+ public ToggleButton setLeftLabel(String label) {
+ getElement().setProperty("leftLabel", label);
+ return this;
+ }
+
+ /**
+ * Sets the label displayed on the right side of the toggle switch.
+ *
+ * @param label the right label text
+ * @return this instance for method chaining
+ * @since 1.0.0
+ */
+ public ToggleButton setRightLabel(String label) {
+ getElement().setProperty("rightLabel", label);
+ return this;
+ }
+
+ /**
+ * Sets the icon displayed on the left side of the toggle switch.
+ *
+ * @param icon the component to use as the left icon
+ * @return this instance for method chaining
+ * @since 1.0.0
+ */
+ public ToggleButton setLeftIcon(Component icon) {
+ setSlottedIcon(icon, "left");
+ return this;
+ }
+
+ /**
+ * Sets the icon displayed on the right side of the toggle switch.
+ *
+ * @param icon the component to use as the right icon
+ * @return this instance for method chaining
+ * @since 1.0.0
+ */
+ public ToggleButton setRightIcon(Component icon) {
+ setSlottedIcon(icon, "right");
+ return this;
+ }
+
+ private void setSlottedIcon(Component icon, String slot) {
+ getElement().getChildren()
+ .filter(e -> slot.equals(e.getAttribute("slot")))
+ .forEach(e -> e.removeFromParent());
+ icon.getElement().setAttribute("slot", slot);
+ add(icon);
+ }
+}
diff --git a/src/main/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonVariant.java b/src/main/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonVariant.java
new file mode 100644
index 0000000..d7a7179
--- /dev/null
+++ b/src/main/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonVariant.java
@@ -0,0 +1,66 @@
+/*-
+ * #%L
+ * Toggle Button Add-On
+ * %%
+ * Copyright (C) 2026 Flowing Code
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package com.flowingcode.vaadin.addons.togglebutton;
+
+import com.vaadin.flow.component.shared.ThemeVariant;
+
+/**
+ * Theme variants for the {@link ToggleButton} component.
+ *
+ * Size variants ({@link #SMALL}, {@link #MEDIUM}, {@link #LARGE}) and {@link #LONGSWIPE} can be
+ * combined (e.g. {@code LONGSWIPE} + {@code LARGE}).
+ *
+ * @since 1.0.0
+ */
+public enum ToggleButtonVariant implements ThemeVariant {
+ /** Renders the toggle at a smaller size (32×18 px track). */
+ SMALL("small"),
+ /** Renders the toggle at the default medium size (44×24 px track). */
+ MEDIUM("medium"),
+ /** Renders the toggle at a larger size (56×32 px track). */
+ LARGE("large"),
+ /**
+ * Renders a wider switch track optimized for touch interaction. Can be combined with size
+ * variants: the track width is increased by 28 px while preserving the height of the chosen
+ * size.
+ */
+ LONGSWIPE("longswipe"),
+ /** Applies the primary color to the checked state. */
+ PRIMARY("primary"),
+ /** Applies the success color to the checked state. */
+ SUCCESS("success"),
+ /** Applies the warning color to the checked state. */
+ WARNING("warning"),
+ /** Applies the error color to the checked state. */
+ ERROR("error"),
+ /** Applies the contrast color to the checked state. */
+ CONTRAST("contrast");
+
+ private final String variant;
+
+ ToggleButtonVariant(String variant) {
+ this.variant = variant;
+ }
+
+ @Override
+ public String getVariantName() {
+ return variant;
+ }
+}
diff --git a/src/main/resources/META-INF/VAADIN/package.properties b/src/main/resources/META-INF/VAADIN/package.properties
index c66616f..ba53c73 100644
--- a/src/main/resources/META-INF/VAADIN/package.properties
+++ b/src/main/resources/META-INF/VAADIN/package.properties
@@ -1 +1,20 @@
+###
+# #%L
+# Toggle Button Add-On
+# %%
+# Copyright (C) 2026 Flowing Code
+# %%
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# #L%
+###
vaadin.allowed-packages=com.flowingcode
diff --git a/src/main/resources/META-INF/resources/frontend/fc-toggle-button.js b/src/main/resources/META-INF/resources/frontend/fc-toggle-button.js
new file mode 100644
index 0000000..3dd875a
--- /dev/null
+++ b/src/main/resources/META-INF/resources/frontend/fc-toggle-button.js
@@ -0,0 +1,501 @@
+/*-
+ * #%L
+ * Toggle Button Add-On
+ * %%
+ * Copyright (C) 2026 Flowing Code
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+import { LitElement, html, css } from 'lit';
+
+/**
+ * Custom Toggle Button component.
+ * Supports left and right labels, and is fully stylable.
+ */
+class ToggleButton extends LitElement {
+ static properties = {
+ checked: { type: Boolean, reflect: true },
+ label: { type: String },
+ leftLabel: { type: String },
+ rightLabel: { type: String },
+ disabled: { type: Boolean, reflect: true },
+ readonly: { type: Boolean, reflect: true },
+ highlightLabel: { type: Boolean, reflect: true },
+ iconsInside: { type: Boolean }
+ };
+
+ static styles = css`
+ :host {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: var(--lumo-space-xs, var(--vaadin-gap-xs));
+ font-family: var(--lumo-font-family, var(--aura-font-family));
+ color: var(--lumo-body-text-color, var(--vaadin-text-color));
+ cursor: pointer;
+ user-select: none;
+ transition: opacity 0.2s;
+ }
+
+ .field-label {
+ font-size: var(--lumo-font-size-s, var(--aura-font-size-s));
+ font-weight: 500;
+ color: var(--lumo-secondary-text-color, var(--vaadin-text-color-secondary));
+ line-height: 1;
+ }
+
+ :host([disabled]) {
+ opacity: 0.5;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+
+ :host([readonly]) {
+ cursor: default;
+ pointer-events: none;
+ }
+
+ .container {
+ display: flex;
+ align-items: center;
+ gap: var(--lumo-space-s, var(--vaadin-gap-s));
+ }
+
+ .label {
+ font-size: var(--lumo-font-size-s, var(--aura-font-size-s));
+ font-weight: 500;
+ transition: color 0.3s;
+ }
+
+ .label.active {
+ color: var(--lumo-primary-text-color, var(--aura-blue-text));
+ }
+
+ :host([theme~="success"]) .label.active {
+ color: var(--lumo-success-text-color, var(--aura-green-text));
+ }
+
+ :host([theme~="error"]) .label.active {
+ color: var(--lumo-error-text-color, var(--aura-red-text));
+ }
+
+ :host([theme~="warning"]) .label.active {
+ color: var(--lumo-warning-text-color, var(--aura-orange-text));
+ }
+
+ :host([theme~="contrast"]) .label.active {
+ color: var(--lumo-contrast, var(--aura-neutral));
+ }
+
+ .label.inactive {
+ color: var(--lumo-secondary-text-color, var(--vaadin-text-color-secondary));
+ opacity: 0.7;
+ }
+
+ .switch {
+ position: relative;
+ width: 44px;
+ height: 24px;
+ background-color: var(--lumo-contrast-20pct, #e0e0e0);
+ border-radius: 12px;
+ transition: background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ display: inline-block;
+ }
+
+ :host([checked]) .switch {
+ background-color: var(--lumo-primary-color, var(--aura-blue));
+ }
+
+ /* Theme Variants */
+ :host([theme~="success"][checked]) .switch {
+ background-color: var(--lumo-success-color, var(--aura-green));
+ }
+
+ :host([theme~="error"][checked]) .switch {
+ background-color: var(--lumo-error-color, var(--aura-red));
+ }
+
+ :host([theme~="warning"][checked]) .switch {
+ background-color: var(--lumo-warning-color, var(--aura-yellow));
+ color: rgba(0, 0, 0, 0.85);
+ }
+
+ :host([theme~="primary"][checked]) .switch {
+ background-color: var(--lumo-primary-color, var(--aura-blue));
+ }
+
+ :host([theme~="contrast"][checked]) .switch {
+ background-color: var(--lumo-contrast, var(--aura-neutral));
+ }
+
+ :host([theme~="contrast"]) .slider {
+ background-color: var(--lumo-base-color, var(--vaadin-background-color));
+ }
+
+ /* Slider colors for variants if needed */
+ :host([theme~="warning"]) .slider {
+ background-color: #fff;
+ }
+
+ .slider {
+ position: absolute;
+ top: 3px;
+ left: 3px;
+ width: 18px;
+ height: 18px;
+ background-color: var(--lumo-base-color, var(--vaadin-background-color));
+ border-radius: 50%;
+ box-shadow: var(--lumo-box-shadow-s, var(--aura-shadow-xs));
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ :host([checked]) .slider {
+ transform: translateX(20px);
+ }
+
+ /* Hover effects */
+ :host(:hover:not([disabled])) .switch {
+ filter: brightness(0.95);
+ }
+
+ :host([checked]:hover:not([disabled])) .switch {
+ filter: brightness(1.1);
+ }
+
+ .switch:active .slider {
+ width: 22px;
+ }
+
+ :host([checked]) .switch:active .slider {
+ transform: translateX(16px);
+ }
+
+ :host([theme~="medium"]) .switch:active .slider {
+ width: 22px;
+ }
+
+ :host([theme~="medium"][checked]) .switch:active .slider {
+ transform: translateX(16px);
+ }
+
+ :host([theme~="small"]) .switch:active .slider {
+ width: 17px;
+ }
+
+ :host([theme~="small"][checked]) .switch:active .slider {
+ transform: translateX(11px);
+ }
+
+ :host([theme~="large"]) .switch:active .slider {
+ width: 30px;
+ }
+
+ :host([theme~="large"][checked]) .switch:active .slider {
+ transform: translateX(18px);
+ }
+
+ /* Size Variants */
+ :host([theme~="medium"]) .switch {
+ width: 44px;
+ height: 24px;
+ }
+
+ :host([theme~="medium"]) .slider {
+ width: 18px;
+ height: 18px;
+ top: 3px;
+ left: 3px;
+ }
+
+ :host([theme~="medium"][checked]) .slider {
+ transform: translateX(20px);
+ }
+
+ :host([theme~="medium"]) .label {
+ font-size: var(--lumo-font-size-s, var(--aura-font-size-s));
+ }
+
+ :host([theme~="small"]) .switch {
+ width: 32px;
+ height: 18px;
+ }
+
+ :host([theme~="small"]) .slider {
+ width: 14px;
+ height: 14px;
+ top: 2px;
+ left: 2px;
+ }
+
+ :host([theme~="small"][checked]) .slider {
+ transform: translateX(14px);
+ }
+
+ :host([theme~="small"]) .label {
+ font-size: var(--lumo-font-size-xs, var(--aura-font-size-xs));
+ }
+
+ :host([theme~="large"]) .switch {
+ width: 56px;
+ height: 32px;
+ border-radius: 16px;
+ }
+
+ :host([theme~="large"]) .slider {
+ width: 24px;
+ height: 24px;
+ top: 4px;
+ left: 4px;
+ }
+
+ :host([theme~="large"][checked]) .slider {
+ transform: translateX(24px);
+ }
+
+ :host([theme~="large"]) .label {
+ font-size: var(--lumo-font-size-l, var(--aura-font-size-l));
+ }
+
+ :host([theme~="longswipe"]) .switch {
+ width: 72px;
+ height: 24px;
+ }
+
+ :host([theme~="longswipe"]) .slider {
+ width: 18px;
+ height: 18px;
+ top: 3px;
+ left: 3px;
+ }
+
+ :host([theme~="longswipe"][checked]) .slider {
+ transform: translateX(48px);
+ }
+
+ :host([theme~="longswipe"]) .switch:active .slider {
+ width: 22px;
+ }
+
+ :host([theme~="longswipe"][checked]) .switch:active .slider {
+ transform: translateX(44px);
+ }
+
+ :host([theme~="longswipe"]) .label {
+ font-size: var(--lumo-font-size-s, var(--aura-font-size-s));
+ }
+
+ :host([theme~="longswipe"][theme~="small"]) .switch {
+ width: 60px;
+ height: 18px;
+ }
+
+ :host([theme~="longswipe"][theme~="small"]) .slider {
+ width: 14px;
+ height: 14px;
+ top: 2px;
+ left: 2px;
+ }
+
+ :host([theme~="longswipe"][theme~="small"][checked]) .slider {
+ transform: translateX(42px);
+ }
+
+ :host([theme~="longswipe"][theme~="small"]) .switch:active .slider {
+ width: 17px;
+ }
+
+ :host([theme~="longswipe"][theme~="small"][checked]) .switch:active .slider {
+ transform: translateX(39px);
+ }
+
+ :host([theme~="longswipe"][theme~="large"]) .switch {
+ width: 84px;
+ height: 32px;
+ border-radius: 16px;
+ }
+
+ :host([theme~="longswipe"][theme~="large"]) .slider {
+ width: 24px;
+ height: 24px;
+ top: 4px;
+ left: 4px;
+ }
+
+ :host([theme~="longswipe"][theme~="large"][checked]) .slider {
+ transform: translateX(52px);
+ }
+
+ :host([theme~="longswipe"][theme~="large"]) .switch:active .slider {
+ width: 30px;
+ }
+
+ :host([theme~="longswipe"][theme~="large"][checked]) .switch:active .slider {
+ transform: translateX(46px);
+ }
+
+ /* Readonly Styles: Unify the look for both checked/unchecked and variants */
+ :host([readonly]) .switch {
+ background-color: transparent;
+ border: 2px dashed var(--lumo-contrast-30pct, var(--vaadin-border-color));
+ box-sizing: border-box;
+ }
+
+ :host([readonly]) .slider {
+ top: 1px;
+ left: 1px;
+ box-shadow: none;
+ background-color: var(--lumo-contrast-40pct, var(--vaadin-background-container-strong));
+ }
+ `;
+
+ constructor() {
+ super();
+ this.checked = false;
+ this.label = '';
+ this.leftLabel = '';
+ this.rightLabel = '';
+ this.disabled = false;
+ this.readonly = false;
+ this.highlightLabel = false;
+ this.iconsInside = false;
+ this._touchStartX = null;
+ this._touchStartY = null;
+ this._isSwiping = false;
+ this._swipeHandled = false;
+ this.addEventListener('click', this._onClick.bind(this));
+ this.addEventListener('touchstart', this._onTouchStart.bind(this), { passive: true });
+ this.addEventListener('touchmove', this._onTouchMove.bind(this), { passive: false });
+ this.addEventListener('touchend', this._onTouchEnd.bind(this));
+ }
+
+ _fireChange() {
+ this.dispatchEvent(new CustomEvent('checked-changed', {
+ detail: { value: this.checked },
+ bubbles: true,
+ composed: true
+ }));
+ }
+
+ _onClick(e) {
+ if (this._swipeHandled) {
+ this._swipeHandled = false;
+ return;
+ }
+ if (this.disabled || this.readonly) return;
+ this.checked = !this.checked;
+ this._fireChange();
+ }
+
+ _onTouchStart(e) {
+ if (this.disabled || this.readonly) return;
+ const touch = e.touches[0];
+ this._touchStartX = touch.clientX;
+ this._touchStartY = touch.clientY;
+ this._isSwiping = false;
+ this._swipeHandled = false;
+
+ const slider = this.shadowRoot.querySelector('.slider');
+ if (slider) {
+ slider.style.transition = 'none';
+ }
+ }
+
+ _onTouchMove(e) {
+ if (this._touchStartX === null || this.disabled || this.readonly) return;
+ const touch = e.touches[0];
+ const dx = touch.clientX - this._touchStartX;
+ const dy = touch.clientY - this._touchStartY;
+
+ // Only capture horizontal swipes
+ if (!this._isSwiping && Math.abs(dx) < Math.abs(dy)) return;
+ this._isSwiping = true;
+ e.preventDefault();
+
+ const switchEl = this.shadowRoot.querySelector('.switch');
+ const slider = this.shadowRoot.querySelector('.slider');
+ if (!switchEl || !slider) return;
+
+ const switchWidth = switchEl.offsetWidth;
+ const sliderWidth = slider.offsetWidth;
+ const gap = parseInt(getComputedStyle(slider).left);
+ const maxTranslate = switchWidth - sliderWidth - gap * 2;
+ const baseTranslate = this.checked ? maxTranslate : 0;
+ const clamped = Math.max(0, Math.min(maxTranslate, baseTranslate + dx));
+ slider.style.transform = `translateX(${clamped}px)`;
+ }
+
+ _onTouchEnd(e) {
+ if (this._touchStartX === null || this.disabled || this.readonly) return;
+ const slider = this.shadowRoot.querySelector('.slider');
+ const switchEl = this.shadowRoot.querySelector('.switch');
+
+ if (!this._isSwiping) {
+ if (slider) slider.style.transition = '';
+ this._touchStartX = null;
+ this._touchStartY = null;
+ return;
+ }
+
+ const touch = e.changedTouches[0];
+ const dx = touch.clientX - this._touchStartX;
+
+ // Threshold: 50% of the slider's actual travel range
+ const sliderWidth = slider ? slider.offsetWidth : 18;
+ const gap = slider ? parseInt(getComputedStyle(slider).left) : 3;
+ const maxTranslate = switchEl ? switchEl.offsetWidth - sliderWidth - gap * 2 : 20;
+ const threshold = maxTranslate * 0.5;
+
+ const newChecked = dx > 0;
+ if (Math.abs(dx) >= threshold && newChecked !== this.checked) {
+ this.checked = newChecked;
+ this._fireChange();
+ this._swipeHandled = true;
+ }
+
+ // Re-enable transition, then on the next frame clear the inline transform so
+ // the CSS-driven position animates smoothly from the current mid-swipe position.
+ if (slider) slider.style.transition = '';
+ requestAnimationFrame(() => {
+ if (slider) slider.style.transform = '';
+ });
+
+ this._touchStartX = null;
+ this._touchStartY = null;
+ this._isSwiping = false;
+ }
+
+ render() {
+ const leftLabelClass = this.highlightLabel && !this.checked ? 'active' : this.highlightLabel ? 'inactive' : '';
+ const rightLabelClass = this.highlightLabel && this.checked ? 'active' : this.highlightLabel ? 'inactive' : '';
+ const leftLabelEl = this.leftLabel ? html`${this.leftLabel}` : '';
+ const rightLabelEl = this.rightLabel ? html`${this.rightLabel}` : '';
+ const leftSlot = html` The tests use Chrome driver (see pom.xml for integration-tests profile) to run integration
- * tests on a headless Chrome. If a property {@code test.use .hub} is set to true, {@code
+ * tests on a headless Chrome. If a property {@code test.use.hub} is set to true, {@code
* AbstractViewTest} will assume that the TestBench test is running in a CI environment. In order to
* keep the this class light, it makes certain assumptions about the CI environment (such as
* available environment variables). It is not advisable to use this class as a base class for you
diff --git a/src/test/java/com/flowingcode/vaadin/addons/togglebutton/it/ToggleButtonIT.java b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/it/ToggleButtonIT.java
new file mode 100644
index 0000000..9e955ec
--- /dev/null
+++ b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/it/ToggleButtonIT.java
@@ -0,0 +1,107 @@
+/*-
+ * #%L
+ * Toggle Button Add-On
+ * %%
+ * Copyright (C) 2026 Flowing Code
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package com.flowingcode.vaadin.addons.togglebutton.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import com.vaadin.testbench.TestBenchElement;
+import org.junit.Test;
+
+/** Integration tests for {@code ToggleButton} label and value-change behavior. */
+public class ToggleButtonIT extends AbstractViewTest {
+
+ public ToggleButtonIT() {
+ super("labels");
+ }
+
+ private TestBenchElement getToggle(String id) {
+ return $("fc-toggle-button").id(id);
+ }
+
+ @Test
+ public void initialValueIsFalse() {
+ assertNull(getToggle("basic").getAttribute("checked"));
+ }
+
+ @Test
+ public void clickTogglesChecked() {
+ TestBenchElement toggle = getToggle("basic");
+ toggle.click();
+ assertNotNull(toggle.getAttribute("checked"));
+ }
+
+ @Test
+ public void clickAgainTogglesBack() {
+ TestBenchElement toggle = getToggle("basic");
+ toggle.click();
+ toggle.click();
+ assertNull(toggle.getAttribute("checked"));
+ }
+
+ @Test
+ public void leftLabelIsRendered() {
+ TestBenchElement toggle = getToggle("with-left-label");
+ String text =
+ (String)
+ toggle
+ .getCommandExecutor()
+ .executeScript(
+ "return arguments[0].shadowRoot.querySelector('.label').textContent", toggle);
+ assertEquals("Off", text);
+ }
+
+ @Test
+ public void rightLabelIsRendered() {
+ TestBenchElement toggle = getToggle("with-right-label");
+ String text =
+ (String)
+ toggle
+ .getCommandExecutor()
+ .executeScript(
+ "return arguments[0].shadowRoot.querySelector('.label').textContent", toggle);
+ assertEquals("On", text);
+ }
+
+ @Test
+ public void bothLabelsAreRendered() {
+ TestBenchElement toggle = getToggle("with-both-labels");
+ String texts =
+ (String)
+ toggle
+ .getCommandExecutor()
+ .executeScript(
+ "return Array.from(arguments[0].shadowRoot.querySelectorAll('.label'))"
+ + ".map(l => l.textContent).join(',')",
+ toggle);
+ assertEquals("Off,On", texts);
+ }
+
+ @Test
+ public void highlightLabelAttributeIsReflected() {
+ assertNotNull(getToggle("highlight-primary").getAttribute("highlightlabel"));
+ }
+
+ @Test
+ public void noHighlightLabelByDefault() {
+ assertNull(getToggle("basic").getAttribute("highlightlabel"));
+ }
+}
diff --git a/src/test/java/com/flowingcode/vaadin/addons/togglebutton/it/ToggleButtonVariantsIT.java b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/it/ToggleButtonVariantsIT.java
new file mode 100644
index 0000000..90392b0
--- /dev/null
+++ b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/it/ToggleButtonVariantsIT.java
@@ -0,0 +1,117 @@
+/*-
+ * #%L
+ * Toggle Button Add-On
+ * %%
+ * Copyright (C) 2026 Flowing Code
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package com.flowingcode.vaadin.addons.togglebutton.it;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.vaadin.testbench.TestBenchElement;
+import org.junit.Test;
+
+/** Integration tests for {@code ToggleButton} theme variants and component states. */
+public class ToggleButtonVariantsIT extends AbstractViewTest {
+
+ public ToggleButtonVariantsIT() {
+ super("variants");
+ }
+
+ private TestBenchElement getToggle(String id) {
+ return $("fc-toggle-button").id(id);
+ }
+
+ // --- Color variants ---
+
+ @Test
+ public void primaryVariantHasThemeAttribute() {
+ assertTrue(getToggle("primary").getAttribute("theme").contains("primary"));
+ }
+
+ @Test
+ public void successVariantHasThemeAttribute() {
+ assertTrue(getToggle("success").getAttribute("theme").contains("success"));
+ }
+
+ @Test
+ public void errorVariantHasThemeAttribute() {
+ assertTrue(getToggle("error").getAttribute("theme").contains("error"));
+ }
+
+ @Test
+ public void warningVariantHasThemeAttribute() {
+ assertTrue(getToggle("warning").getAttribute("theme").contains("warning"));
+ }
+
+ @Test
+ public void contrastVariantHasThemeAttribute() {
+ assertTrue(getToggle("contrast").getAttribute("theme").contains("contrast"));
+ }
+
+ // --- Size variants ---
+
+ @Test
+ public void smallVariantHasThemeAttribute() {
+ assertTrue(getToggle("small").getAttribute("theme").contains("small"));
+ }
+
+ @Test
+ public void mediumVariantHasThemeAttribute() {
+ assertTrue(getToggle("medium").getAttribute("theme").contains("medium"));
+ }
+
+ @Test
+ public void largeVariantHasThemeAttribute() {
+ assertTrue(getToggle("large").getAttribute("theme").contains("large"));
+ }
+
+ // --- Initial values ---
+
+ @Test
+ public void colorVariantsInitializedChecked() {
+ for (String id : new String[] {"primary", "success", "error", "warning", "contrast"}) {
+ assertNotNull("Expected " + id + " to be checked", getToggle(id).getAttribute("checked"));
+ }
+ }
+
+ @Test
+ public void sizeVariantsInitializedUnchecked() {
+ for (String id : new String[] {"small", "medium", "large"}) {
+ assertNull("Expected " + id + " to be unchecked", getToggle(id).getAttribute("checked"));
+ }
+ }
+
+ // --- States ---
+
+ @Test
+ public void disabledButtonCannotBeToggled() {
+ TestBenchElement disabled = getToggle("disabled");
+ assertNull(disabled.getAttribute("checked"));
+ disabled.click();
+ assertNull(disabled.getAttribute("checked"));
+ }
+
+ @Test
+ public void readonlyButtonCannotBeToggled() {
+ TestBenchElement readOnly = getToggle("read-only");
+ assertNotNull(readOnly.getAttribute("checked"));
+ readOnly.click();
+ assertNotNull(readOnly.getAttribute("checked"));
+ }
+}
diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/it/ViewIT.java b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/it/ViewIT.java
similarity index 91%
rename from src/test/java/com/flowingcode/vaadin/addons/template/it/ViewIT.java
rename to src/test/java/com/flowingcode/vaadin/addons/togglebutton/it/ViewIT.java
index 628dc14..f38ba2b 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/template/it/ViewIT.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/it/ViewIT.java
@@ -1,8 +1,8 @@
/*-
* #%L
- * Template Add-on
+ * Toggle Button Add-On
* %%
- * Copyright (C) 2025 Flowing Code
+ * Copyright (C) 2026 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,8 +17,7 @@
* limitations under the License.
* #L%
*/
-
-package com.flowingcode.vaadin.addons.template.it;
+package com.flowingcode.vaadin.addons.togglebutton.it;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
@@ -58,7 +57,7 @@ protected boolean matchesSafely(TestBenchElement item, Description mismatchDescr
@Test
public void componentWorks() {
- TestBenchElement element = $("paper-input").first();
+ TestBenchElement element = $("fc-toggle-button").first();
assertThat(element, hasBeenUpgradedToCustomElement);
}
}
diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/test/SerializationTest.java b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/test/SerializationTest.java
similarity index 86%
rename from src/test/java/com/flowingcode/vaadin/addons/template/test/SerializationTest.java
rename to src/test/java/com/flowingcode/vaadin/addons/togglebutton/test/SerializationTest.java
index 1f8ceed..baa282a 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/template/test/SerializationTest.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/test/SerializationTest.java
@@ -1,8 +1,8 @@
/*-
* #%L
- * Template Add-on
+ * Toggle Button Add-On
* %%
- * Copyright (C) 2025 Flowing Code
+ * Copyright (C) 2026 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,9 +17,9 @@
* limitations under the License.
* #L%
*/
-package com.flowingcode.vaadin.addons.template.test;
+package com.flowingcode.vaadin.addons.togglebutton.test;
-import com.flowingcode.vaadin.addons.template.TemplateAddon;
+import com.flowingcode.vaadin.addons.togglebutton.ToggleButton;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -44,7 +44,7 @@ private void testSerializationOf(Object obj) throws IOException, ClassNotFoundEx
@Test
public void testSerialization() throws ClassNotFoundException, IOException {
try {
- testSerializationOf(new TemplateAddon());
+ testSerializationOf(new ToggleButton());
} catch (Exception e) {
Assert.fail("Problem while testing serialization: " + e.getMessage());
}
diff --git a/src/test/resources/META-INF/frontend/styles/shared-styles.css b/src/test/resources/META-INF/frontend/styles/shared-styles.css
index 6680e2d..dded7b2 100644
--- a/src/test/resources/META-INF/frontend/styles/shared-styles.css
+++ b/src/test/resources/META-INF/frontend/styles/shared-styles.css
@@ -1 +1,20 @@
-/*Demo styles*/
\ No newline at end of file
+/*-
+ * #%L
+ * Toggle Button Add-On
+ * %%
+ * Copyright (C) 2026 Flowing Code
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+/*Demo styles*/