diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index f804cc1..ce77a38 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,5 +1,5 @@ name: Bug Report -description: Please report issues related to TEMPLATE_ADDON here. +description: Please report issues related to Toggle Button Add-On here. body: - type: textarea id: problem-description diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 4d37c3b..3ba43fa 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -1,5 +1,5 @@ name: Feature Request -description: Please add feature suggestions related to TEMPLATE_ADDON here. +description: Please add feature suggestions related to Toggle Button Add-On here. body: - type: textarea id: feature-proposal diff --git a/.gitignore b/.gitignore index 9a38502..749053c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ vite.config.ts /src/main/bundles /src/main/frontend/generated /src/main/frontend/index.html +/.claude diff --git a/LICENSE b/LICENSE.txt similarity index 99% rename from LICENSE rename to LICENSE.txt index 980a15a..261eeb9 100644 --- a/LICENSE +++ b/LICENSE.txt @@ -1,4 +1,4 @@ - Apache License + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -178,7 +178,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" + boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright {yyyy} {name of copyright owner} + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index d4224a3..2a38027 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,32 @@ -[![Published on Vaadin Directory](https://img.shields.io/badge/Vaadin%20Directory-published-00b4f0.svg)](https://vaadin.com/directory/component/template-add-on) -[![Stars on vaadin.com/directory](https://img.shields.io/vaadin-directory/star/template-add-on.svg)](https://vaadin.com/directory/component/template-add-on) -[![Build Status](https://jenkins.flowingcode.com/job/template-addon/badge/icon)](https://jenkins.flowingcode.com/job/template-addon) -[![Maven Central](https://img.shields.io/maven-central/v/com.flowingcode.vaadin.addons/template-addon)](https://mvnrepository.com/artifact/com.flowingcode.vaadin.addons/template-addon) -[![Javadoc](https://img.shields.io/badge/javadoc-00b4f0)](https://javadoc.flowingcode.com/artifact/com.flowingcode.vaadin.addons/template-addon) +[![Published on Vaadin Directory](https://img.shields.io/badge/Vaadin%20Directory-published-00b4f0.svg)](https://vaadin.com/directory/component/toggle-button-addon) +[![Stars on vaadin.com/directory](https://img.shields.io/vaadin-directory/star/toggle-button-addon.svg)](https://vaadin.com/directory/component/toggle-button-addon) +[![Build Status](https://jenkins.flowingcode.com/job/toggle-button-addon/badge/icon)](https://jenkins.flowingcode.com/job/toggle-button-addon) +[![Maven Central](https://img.shields.io/maven-central/v/com.flowingcode.vaadin.addons/toggle-button-addon)](https://mvnrepository.com/artifact/com.flowingcode.vaadin.addons/toggle-button-addon) +[![Javadoc](https://img.shields.io/badge/javadoc-00b4f0)](https://javadoc.flowingcode.com/artifact/com.flowingcode.vaadin.addons/toggle-button-addon) -# Template Add-On +# Toggle Button Add-On -This is a template project for building new Vaadin 24 add-ons +A toggle button component for Vaadin Flow that supports customizable labels and icons on both sides of the toggle. ## Features -* List the features of your add-on in here +* Toggle between two states with a single click +* Customizable left and right labels +* Support for icons on both sides via slots +* Optional label highlighting: active-side label uses the theme variant color (primary, success, warning, error, or contrast), inactive side is dimmed +* Optional icons-inside layout: icons adjacent to the switch, labels on the outer edges +* Theme variants: `SMALL`, `MEDIUM`, `LARGE`, `LONGSWIPE`, `PRIMARY`, `SUCCESS`, `WARNING`, `ERROR`, `CONTRAST` +* `LONGSWIPE` variant produces a wider switch track, optimized for touch interaction; can be combined with size variants +* Fluent API for easy configuration +* Full integration with Vaadin's `HasValue`, `HasSize`, `HasLabel`, `HasAriaLabel`, and `HasTooltip` ## Online demo -[Online demo here](http://addonsv24.flowingcode.com/template) +[Online demo here](http://addonsv25.flowingcode.com/togglebutton) ## Download release -[Available in Vaadin Directory](https://vaadin.com/directory/component/template-add-on) +[Available in Vaadin Directory](https://vaadin.com/directory/component/toggle-button-addon) ### Maven install @@ -27,8 +35,8 @@ Add the following dependencies in your pom.xml file: ```xml com.flowingcode.vaadin.addons - template-addon - X.Y.Z + toggle-button-addon + 1.0.0 ``` @@ -44,11 +52,11 @@ To see the demo, navigate to http://localhost:8080/ ## Release notes -See [here](https://github.com/FlowingCode/TemplateAddon/releases) +See [here](https://github.com/FlowingCode/ToggleButton/releases) ## Issue tracking -The issues for this add-on are tracked on its github.com page. All bug reports and feature requests are appreciated. +The issues for this add-on are tracked on its github.com page. All bug reports and feature requests are appreciated. ## Contributions @@ -62,9 +70,9 @@ Creating an issue is a highly valuable contribution. If you've found a bug or ha * If not, create a new issue, choosing the right option: "Bug Report" or "Feature Request". Try to keep the scope minimal but as detailed as possible. > **A Note on Bug Reports** -> +> > Please complete all the requested fields to the best of your ability. Each piece of information, like the environment versions and a clear description, helps us understand the context of the issue. -> +> > While all details are important, the **[minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example)** is the most critical part of your report. It's essential because it removes ambiguity and allows our team to observe the problem firsthand, exactly as you are experiencing it. #### 2. Contributing Code via Pull Requests @@ -72,7 +80,7 @@ Creating an issue is a highly valuable contribution. If you've found a bug or ha As a first step, please refer to our [Development Conventions](https://github.com/FlowingCode/DevelopmentConventions) page to find information about Conventional Commits & Code Style requirements. Then, follow these steps for creating a contribution: - + - Fork this project. - Create an issue to this project about the contribution (bug or feature) if there is no such issue about it already. Try to keep the scope minimal. - Develop and test the fix or functionality carefully. Only include minimum amount of code needed to fix the issue. @@ -84,22 +92,69 @@ Then, follow these steps for creating a contribution: This add-on is distributed under Apache License 2.0. For license terms, see LICENSE.txt. -Template Add-On is written by Flowing Code S.A. +Toggle Button Add-On is written by Flowing Code S.A. # Developer Guide ## Getting started -Add your code samples in this section +```java +// Basic toggle button +ToggleButton toggle = new ToggleButton(); + +// With a field label (shown above the toggle) +ToggleButton toggle = new ToggleButton("Notifications"); + +// With a field label and initial value +ToggleButton toggle = new ToggleButton("Dark mode", true); + +// With labels +ToggleButton toggle = new ToggleButton() + .setLeftLabel("Off") + .setRightLabel("On"); + +// With icons +ToggleButton toggle = new ToggleButton() + .setLeftLabel("Dark") + .setRightLabel("Light") + .setLeftIcon(new Icon(VaadinIcon.MOON)) + .setRightIcon(new Icon(VaadinIcon.SUN_O)); + +// Listen to value changes +toggle.addValueChangeListener(e -> + Notification.show("Toggle is now: " + (e.getValue() ? "on" : "off"))); + +// Enable label highlighting (active side uses the theme variant color, inactive side is dimmed) +ToggleButton toggle = new ToggleButton() + .setLeftLabel("Off") + .setRightLabel("On") + .withHighlightLabel(); + +// Icons inside: [label] [icon] [switch] [icon] [label] (default is [icon] [label] [switch] [label] [icon]) +ToggleButton toggle = new ToggleButton() + .setLeftIcon(new Icon(VaadinIcon.MOON)) + .setLeftLabel("Dark") + .setRightLabel("Light") + .setRightIcon(new Icon(VaadinIcon.SUN_O)) + .withIconsInside(); + +// Apply theme variants +toggle.addThemeVariants(ToggleButtonVariant.PRIMARY); +toggle.addThemeVariants(ToggleButtonVariant.CONTRAST); + +// Long swipe: wider track, optimized for touch (can be combined with size variants) +toggle.addThemeVariants(ToggleButtonVariant.LONGSWIPE); +toggle.addThemeVariants(ToggleButtonVariant.LONGSWIPE, ToggleButtonVariant.LARGE); +``` ## Special configuration when using Spring -By default, Vaadin Flow only includes `com/vaadin/flow/component` to be always scanned for UI components and views. For this reason, the add-on might need to be allowed in order to display correctly. +By default, Vaadin Flow only includes `com/vaadin/flow/component` to be always scanned for UI components and views. For this reason, the add-on might need to be allowed in order to display correctly. To do so, just add `com.flowingcode` to the `vaadin.allowed-packages` property in `src/main/resources/application.properties`, like: ``` vaadin.allowed-packages = com.vaadin,org.vaadin,dev.hilla,com.flowingcode ``` - + More information on Spring scanning configuration [here](https://vaadin.com/docs/latest/integrations/spring/configuration/#configure-the-scanning-of-packages). diff --git a/pom.xml b/pom.xml index ad70f45..69158cd 100644 --- a/pom.xml +++ b/pom.xml @@ -5,11 +5,11 @@ 4.0.0 com.flowingcode.vaadin.addons - template-addon + toggle-button-addon 1.0.0-SNAPSHOT - Template Add-on - Template Add-on for Vaadin Flow - https://www.flowingcode.com/en/open-source/ + Toggle Button Add-On + Toggle button built on Vaadin components with support for customizable labels and icons + https://github.com/FlowingCode/ToggleButton 24.9.1 @@ -20,7 +20,7 @@ UTF-8 ${project.basedir}/drivers 11.0.20 - 5.0.0 + 5.2.0 @@ -38,9 +38,9 @@ - https://github.com/FlowingCode/AddonStarter24 - scm:git:git://github.com/FlowingCode/AddonStarter24.git - scm:git:ssh://git@github.com:/FlowingCode/AddonStarter24.git + https://github.com/FlowingCode/ToggleButton + scm:git:git://github.com/FlowingCode/ToggleButton.git + scm:git:ssh://git@github.com:/FlowingCode/ToggleButton.git master @@ -580,6 +580,12 @@ vaadin-dev true + + jakarta.servlet + jakarta.servlet-api + 6.1.0 + test + diff --git a/src/main/java/com/flowingcode/vaadin/addons/template/TemplateAddon.java b/src/main/java/com/flowingcode/vaadin/addons/template/TemplateAddon.java deleted file mode 100644 index a318158..0000000 --- a/src/main/java/com/flowingcode/vaadin/addons/template/TemplateAddon.java +++ /dev/null @@ -1,32 +0,0 @@ -/*- - * #%L - * Template Add-on - * %% - * Copyright (C) 2025 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.template; - -import com.vaadin.flow.component.Tag; -import com.vaadin.flow.component.dependency.JsModule; -import com.vaadin.flow.component.dependency.NpmPackage; -import com.vaadin.flow.component.html.Div; - -@SuppressWarnings("serial") -@NpmPackage(value = "@polymer/paper-input", version = "3.2.1") -@JsModule("@polymer/paper-input/paper-input.js") -@Tag("paper-input") -public class TemplateAddon extends Div {} diff --git a/src/main/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButton.java b/src/main/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButton.java new file mode 100644 index 0000000..3c504ef --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButton.java @@ -0,0 +1,255 @@ +/*- + * #%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.AbstractSinglePropertyField; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasAriaLabel; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.HasLabel; +import com.vaadin.flow.component.HasSize; +import com.vaadin.flow.component.ItemLabelGenerator; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.component.shared.HasThemeVariant; +import com.vaadin.flow.component.shared.HasTooltip; +import com.vaadin.flow.component.shared.Tooltip; + +/** + * A toggle button component built on Vaadin with support for customizable labels and icons. + * + *

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 + implements HasSize, + HasComponents, + HasLabel, + HasAriaLabel, + HasTooltip, + HasThemeVariant { + + private ItemLabelGenerator itemLabelGenerator = b -> ""; + private Tooltip tooltip; + + /** Creates a new toggle button with an initial value of {@code false}. */ + public ToggleButton() { + this(false); + } + + /** + * Creates a new toggle button with the given initial value. + * + * @param initialValue the initial checked state + * @since 1.0.0 + */ + public ToggleButton(boolean initialValue) { + super("checked", initialValue, false); + if (initialValue) { + getElement().setAttribute("checked", ""); + } + setSynchronizedEvent("checked-changed"); + } + + /** + * Creates a new toggle button with the given label and an initial value of {@code false}. + * + * @param label the label text shown above the toggle + * @since 1.0.0 + */ + public ToggleButton(String label) { + this(false); + setLabel(label); + } + + /** + * Creates a new toggle button with the given label and initial value. + * + * @param label the label text shown above the toggle + * @param initialValue the initial checked state + * @since 1.0.0 + */ + public ToggleButton(String label, boolean initialValue) { + this(initialValue); + setLabel(label); + } + + @Override + public void setReadOnly(boolean readOnly) { + getElement().setProperty("readonly", readOnly); + } + + @Override + public boolean isReadOnly() { + return getElement().getProperty("readonly", false); + } + + @Override + public Tooltip setTooltipText(String text) { + if (tooltip == null) { + tooltip = Tooltip.forComponent(this); + } + tooltip.setText(text); + return tooltip; + } + + @Override + public Tooltip getTooltip() { + if (tooltip == null) { + tooltip = Tooltip.forComponent(this); + } + return tooltip; + } + + /** + * Sets a generator that provides labels for the checked ({@code true}) and unchecked + * ({@code false}) states. The generator is called with the state value and its result is used + * as the right label for {@code true} and the left label for {@code false}. + * + * @param itemLabelGenerator the label generator; must not be {@code null} + * @return this instance for method chaining + * @since 1.0.0 + */ + public ToggleButton setItemLabelGenerator(ItemLabelGenerator itemLabelGenerator) { + this.itemLabelGenerator = itemLabelGenerator; + updateLabels(); + return this; + } + + private void updateLabels() { + getElement().setProperty("leftLabel", itemLabelGenerator.apply(false)); + getElement().setProperty("rightLabel", itemLabelGenerator.apply(true)); + } + + /** + * Enables label highlighting: the label on the active side is shown using the color of the + * active theme variant ({@link ToggleButtonVariant#PRIMARY PRIMARY}, + * {@link ToggleButtonVariant#SUCCESS SUCCESS}, {@link ToggleButtonVariant#WARNING WARNING}, + * {@link ToggleButtonVariant#ERROR ERROR}, or {@link ToggleButtonVariant#CONTRAST + * CONTRAST}), falling back to the primary color when no color variant is set. The inactive-side + * label is dimmed. + * + * @return this instance for method chaining + * @since 1.0.0 + */ + public ToggleButton withHighlightLabel() { + getElement().setProperty("highlightLabel", true); + return this; + } + + /** + * Places icons adjacent to the switch and labels on the outer edges, producing the layout + * {@code [left-label] [left-icon] [switch] [right-icon] [right-label]}. + * + *

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``; + const rightSlot = html``; + return html` + ${this.label ? html`${this.label}` : ''} +

+ ${this.iconsInside ? leftLabelEl : leftSlot} + ${this.iconsInside ? leftSlot : leftLabelEl} +
+
+
+ ${this.iconsInside ? rightSlot : rightLabelEl} + ${this.iconsInside ? rightLabelEl : rightSlot} +
+ `; + } +} + +customElements.define('fc-toggle-button', ToggleButton); +export { ToggleButton }; diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/DemoView.java b/src/test/java/com/flowingcode/vaadin/addons/AppShellConfiguratorImpl.java similarity index 53% rename from src/test/java/com/flowingcode/vaadin/addons/template/DemoView.java rename to src/test/java/com/flowingcode/vaadin/addons/AppShellConfiguratorImpl.java index 0561979..4fea7d8 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/template/DemoView.java +++ b/src/test/java/com/flowingcode/vaadin/addons/AppShellConfiguratorImpl.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,20 +17,19 @@ * limitations under the License. * #L% */ +package com.flowingcode.vaadin.addons; -package com.flowingcode.vaadin.addons.template; - -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.router.BeforeEnterEvent; -import com.vaadin.flow.router.BeforeEnterObserver; -import com.vaadin.flow.router.Route; - -@SuppressWarnings("serial") -@Route("") -public class DemoView extends VerticalLayout implements BeforeEnterObserver { +import com.flowingcode.vaadin.addons.demo.DynamicTheme; +import com.vaadin.flow.component.page.AppShellConfigurator; +import com.vaadin.flow.server.AppShellSettings; +public class AppShellConfiguratorImpl implements AppShellConfigurator { + @Override - public void beforeEnter(BeforeEnterEvent event) { - event.forwardTo(TemplateDemoView.class); + public void configurePage(AppShellSettings settings) { + if (DynamicTheme.isFeatureSupported()) { + DynamicTheme.LUMO.initialize(settings); + } } -} + +} \ No newline at end of file diff --git a/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java b/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java index 8996b87..5ade712 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java +++ b/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java @@ -1,15 +1,15 @@ /*- * #%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. * 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. diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemo.java b/src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemo.java deleted file mode 100644 index 294ae97..0000000 --- a/src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemo.java +++ /dev/null @@ -1,36 +0,0 @@ -/*- - * #%L - * Template Add-on - * %% - * Copyright (C) 2025 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.template; - -import com.flowingcode.vaadin.addons.demo.DemoSource; -import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; - -@DemoSource -@PageTitle("Template Add-on Demo") -@SuppressWarnings("serial") -@Route(value = "demo", layout = TemplateDemoView.class) -public class TemplateDemo extends Div { - - public TemplateDemo() { - add(new TemplateAddon()); - } -} diff --git a/src/test/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonDemo.java b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonDemo.java new file mode 100644 index 0000000..125dfd5 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonDemo.java @@ -0,0 +1,126 @@ +/*- + * #%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.flowingcode.vaadin.addons.demo.DemoSource; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H3; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; + +@DemoSource +@PageTitle("Labels and Icons") +@SuppressWarnings("serial") +@Route(value = "togglebutton/labels", layout = ToggleButtonDemoView.class) +public class ToggleButtonDemo extends Div { + + public ToggleButtonDemo() { + + ToggleButton basic = new ToggleButton(); + basic.setId("basic"); + + ToggleButton withLabel = new ToggleButton("Notifications"); + + ToggleButton withLabelAndInitialValue = new ToggleButton("Dark mode", true) + .setLeftLabel("Off") + .setRightLabel("On"); + + ToggleButton withLeftLabel = new ToggleButton() + .setLeftLabel("Off"); + withLeftLabel.setId("with-left-label"); + + ToggleButton withRightLabel = new ToggleButton() + .setRightLabel("On"); + withRightLabel.setId("with-right-label"); + + ToggleButton withBothLabels = new ToggleButton() + .setLeftLabel("Off") + .setRightLabel("On"); + withBothLabels.setId("with-both-labels"); + + ToggleButton withLeftIcon = new ToggleButton() + .setLeftIcon(new Icon(VaadinIcon.MOON)); + + ToggleButton withRightIcon = new ToggleButton() + .setRightIcon(new Icon(VaadinIcon.SUN_O)); + + ToggleButton withBothIcons = new ToggleButton() + .setLeftIcon(new Icon(VaadinIcon.MOON)) + .setRightIcon(new Icon(VaadinIcon.SUN_O)); + + ToggleButton withLabelsAndIcons = new ToggleButton() + .setLeftIcon(new Icon(VaadinIcon.MOON)) + .setLeftLabel("Dark") + .setRightLabel("Light") + .setRightIcon(new Icon(VaadinIcon.SUN_O)); + + ToggleButton withIconsInside = new ToggleButton() + .setLeftIcon(new Icon(VaadinIcon.MOON)) + .setLeftLabel("Dark") + .setRightLabel("Light") + .setRightIcon(new Icon(VaadinIcon.SUN_O)) + .withIconsInside(); + + ToggleButton highlightPrimary = new ToggleButton(true).setLeftLabel("Off").setRightLabel("On").withHighlightLabel(); + highlightPrimary.setId("highlight-primary"); + highlightPrimary.addThemeVariants(ToggleButtonVariant.PRIMARY); + + ToggleButton highlightSuccess = new ToggleButton(true).setLeftLabel("Off").setRightLabel("On").withHighlightLabel(); + highlightSuccess.addThemeVariants(ToggleButtonVariant.SUCCESS); + + ToggleButton highlightWarning = new ToggleButton(true).setLeftLabel("Off").setRightLabel("On").withHighlightLabel(); + highlightWarning.addThemeVariants(ToggleButtonVariant.WARNING); + + ToggleButton highlightError = new ToggleButton(true).setLeftLabel("Off").setRightLabel("On").withHighlightLabel(); + highlightError.addThemeVariants(ToggleButtonVariant.ERROR); + + ToggleButton highlightContrast = new ToggleButton(true).setLeftLabel("Off").setRightLabel("On").withHighlightLabel(); + highlightContrast.addThemeVariants(ToggleButtonVariant.CONTRAST); + + HorizontalLayout fieldLabelRow = new HorizontalLayout(withLabel, withLabelAndInitialValue); + HorizontalLayout labelsRow = new HorizontalLayout(withLeftLabel, withRightLabel, withBothLabels); + HorizontalLayout iconsRow = new HorizontalLayout(withLeftIcon, withRightIcon, withBothIcons); + HorizontalLayout labelsAndIconsRow = new HorizontalLayout(withLabelsAndIcons, withIconsInside); + HorizontalLayout highlightRow = new HorizontalLayout(highlightPrimary, highlightSuccess, highlightWarning, highlightError, highlightContrast); + fieldLabelRow.getStyle().set("gap", "var(--lumo-space-l)"); + labelsRow.getStyle().set("gap", "var(--lumo-space-l)"); + iconsRow.getStyle().set("gap", "var(--lumo-space-l)"); + labelsAndIconsRow.getStyle().set("gap", "var(--lumo-space-l)"); + highlightRow.getStyle().set("gap", "var(--lumo-space-l)"); + + add(new VerticalLayout( + new H3("Basic"), + basic, + new H3("With field label"), + fieldLabelRow, + new H3("With labels"), + labelsRow, + new H3("With icons"), + iconsRow, + new H3("With labels and icons"), + labelsAndIconsRow, + new H3("With label highlighting"), + highlightRow)); + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemoView.java b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonDemoView.java similarity index 69% rename from src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemoView.java rename to src/test/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonDemoView.java index a17133f..915634d 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemoView.java +++ b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonDemoView.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,7 +17,7 @@ * limitations under the License. * #L% */ -package com.flowingcode.vaadin.addons.template; +package com.flowingcode.vaadin.addons.togglebutton; import com.flowingcode.vaadin.addons.DemoLayout; import com.flowingcode.vaadin.addons.GithubLink; @@ -27,12 +27,14 @@ @SuppressWarnings("serial") @ParentLayout(DemoLayout.class) -@Route("template") -@GithubLink("https://github.com/FlowingCode/AddonStarter24") -public class TemplateDemoView extends TabbedDemo { +@Route("togglebutton") +@GithubLink("https://github.com/FlowingCode/ToggleButton") +public class ToggleButtonDemoView extends TabbedDemo { - public TemplateDemoView() { - addDemo(TemplateDemo.class); + public ToggleButtonDemoView() { + addDemo(ToggleButtonDemo.class); + addDemo(ToggleButtonVariantsDemo.class); + addDemo(ToggleButtonEventsDemo.class); setSizeFull(); } } diff --git a/src/test/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonEventsDemo.java b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonEventsDemo.java new file mode 100644 index 0000000..ef492c7 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonEventsDemo.java @@ -0,0 +1,62 @@ +/*- + * #%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.flowingcode.vaadin.addons.demo.DemoSource; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H3; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; + +@DemoSource +@PageTitle("Events and Accessibility") +@SuppressWarnings("serial") +@Route(value = "togglebutton/events", layout = ToggleButtonDemoView.class) +public class ToggleButtonEventsDemo extends Div { + + public ToggleButtonEventsDemo() { + + ToggleButton withValueChange = new ToggleButton() + .setLeftLabel("Off") + .setRightLabel("On"); + withValueChange.addValueChangeListener(e -> + Notification.show("Value changed to: " + e.getValue())); + + ToggleButton withAriaLabel = new ToggleButton() + .setLeftLabel("Off") + .setRightLabel("On"); + withAriaLabel.setAriaLabel("Enable notifications"); + + ToggleButton withTooltip = new ToggleButton() + .setLeftLabel("Off") + .setRightLabel("On"); + withTooltip.setTooltipText("Toggle to enable or disable this feature"); + + add(new VerticalLayout( + new H3("Value change event"), + withValueChange, + new H3("Aria label"), + withAriaLabel, + new H3("Tooltip"), + withTooltip)); + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonVariantsDemo.java b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonVariantsDemo.java new file mode 100644 index 0000000..740f95b --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/ToggleButtonVariantsDemo.java @@ -0,0 +1,110 @@ +/*- + * #%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.flowingcode.vaadin.addons.demo.DemoSource; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H3; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; + +@DemoSource +@PageTitle("Theme Variants") +@SuppressWarnings("serial") +@Route(value = "togglebutton/variants", layout = ToggleButtonDemoView.class) +public class ToggleButtonVariantsDemo extends Div { + + public ToggleButtonVariantsDemo() { + + // Color variants + ToggleButton primary = new ToggleButton(true).setLeftLabel("Primary"); + primary.setId("primary"); + primary.addThemeVariants(ToggleButtonVariant.PRIMARY); + + ToggleButton success = new ToggleButton(true).setLeftLabel("Success"); + success.setId("success"); + success.addThemeVariants(ToggleButtonVariant.SUCCESS); + + ToggleButton error = new ToggleButton(true).setLeftLabel("Error"); + error.setId("error"); + error.addThemeVariants(ToggleButtonVariant.ERROR); + + ToggleButton warning = new ToggleButton(true).setLeftLabel("Warning"); + warning.setId("warning"); + warning.addThemeVariants(ToggleButtonVariant.WARNING); + + ToggleButton contrast = new ToggleButton(true).setLeftLabel("Contrast"); + contrast.setId("contrast"); + contrast.addThemeVariants(ToggleButtonVariant.CONTRAST); + + // Size variants + ToggleButton small = new ToggleButton().setRightLabel("Small"); + small.setId("small"); + small.addThemeVariants(ToggleButtonVariant.SMALL); + + ToggleButton medium = new ToggleButton().setRightLabel("Medium"); + medium.setId("medium"); + medium.addThemeVariants(ToggleButtonVariant.MEDIUM); + + ToggleButton large = new ToggleButton().setRightLabel("Large"); + large.setId("large"); + large.addThemeVariants(ToggleButtonVariant.LARGE); + + // Long swipe variants + ToggleButton longswipeSmall = new ToggleButton().setRightLabel("Small"); + longswipeSmall.addThemeVariants(ToggleButtonVariant.LONGSWIPE, ToggleButtonVariant.SMALL); + + ToggleButton longswipeMedium = new ToggleButton().setRightLabel("Medium"); + longswipeMedium.addThemeVariants(ToggleButtonVariant.LONGSWIPE, ToggleButtonVariant.MEDIUM); + + ToggleButton longswipeLarge = new ToggleButton().setRightLabel("Large"); + longswipeLarge.addThemeVariants(ToggleButtonVariant.LONGSWIPE, ToggleButtonVariant.LARGE); + + // States + ToggleButton disabled = new ToggleButton().setRightLabel("Disabled"); + disabled.setId("disabled"); + disabled.setEnabled(false); + + ToggleButton readOnly = new ToggleButton(true).setRightLabel("Read-only"); + readOnly.setId("read-only"); + readOnly.setReadOnly(true); + + HorizontalLayout colorRow = new HorizontalLayout(primary, success, error, warning, contrast); + HorizontalLayout sizeRow = new HorizontalLayout(small, medium, large); + HorizontalLayout longswipeRow = new HorizontalLayout(longswipeSmall, longswipeMedium, longswipeLarge); + HorizontalLayout statesRow = new HorizontalLayout(disabled, readOnly); + colorRow.getStyle().set("gap", "var(--lumo-space-l)"); + sizeRow.getStyle().set("gap", "var(--lumo-space-l)"); + longswipeRow.getStyle().set("gap", "var(--lumo-space-l)"); + statesRow.getStyle().set("gap", "var(--lumo-space-l)"); + + add(new VerticalLayout( + new H3("Color variants"), + colorRow, + new H3("Size variants"), + sizeRow, + new H3("Long swipe"), + longswipeRow, + new H3("States"), + statesRow)); + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/it/AbstractViewTest.java b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/it/AbstractViewTest.java similarity index 93% rename from src/test/java/com/flowingcode/vaadin/addons/template/it/AbstractViewTest.java rename to src/test/java/com/flowingcode/vaadin/addons/togglebutton/it/AbstractViewTest.java index a2579ad..d4d7da8 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/template/it/AbstractViewTest.java +++ b/src/test/java/com/flowingcode/vaadin/addons/togglebutton/it/AbstractViewTest.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 com.vaadin.testbench.ScreenshotOnFailureRule; import com.vaadin.testbench.TestBench; @@ -33,7 +32,7 @@ * Base class for ITs * *

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*/