diff --git a/pom.xml b/pom.xml
index f890a23..0a74c66 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
com.flowingcode.vaadin.addons
day-of-week-selector-addon
- 1.1.1-SNAPSHOT
+ 1.2.0-SNAPSHOT
Day of Week Selector Add-on
Day of Week Selector Add-on for Vaadin Flow
https://www.flowingcode.com/en/open-source/
diff --git a/src/main/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelector.java b/src/main/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelector.java
index 522180f..faa5284 100644
--- a/src/main/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelector.java
+++ b/src/main/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelector.java
@@ -19,13 +19,19 @@
*/
package com.flowingcode.vaadin.addons.dayofweekselector;
+import com.vaadin.flow.component.ClientCallable;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.customfield.CustomField;
import com.vaadin.flow.component.datepicker.DatePicker.DatePickerI18n;
import com.vaadin.flow.component.dependency.CssImport;
-import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
+import com.vaadin.flow.component.dependency.JsModule;
+import com.vaadin.flow.dom.Element;
+
import java.time.DayOfWeek;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.EnumSet;
@@ -33,15 +39,23 @@
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
/**
* Shows the days of the week so they can be selected.
*/
@SuppressWarnings("serial")
@CssImport("./styles/fc-days-of-week-selector-styles.css")
+@JsModule("./src/fc-days-of-week-selector.ts")
+@Tag("fc-days-of-week-selector")
public class DayOfWeekSelector extends CustomField> {
+ public static final class CssProperties {
+ public static final String OVERFLOW_ICON_SIZE = "--fc-days-of-week-selector-overflow-icon-size";
+
+ private CssProperties() {
+ }
+ }
+
private static class DayOfWeekButton extends Button {
private static final String CLASS_NAME = "fc-days-of-week-selector-button";
@@ -59,6 +73,7 @@ public DayOfWeek getDayOfWeek() {
return dayOfWeek;
}
+ @ClientCallable
private void toggleState() {
setState(!state);
}
@@ -74,29 +89,13 @@ private void setState(boolean state) {
}
- private HorizontalLayout buttonsLayout;
+ private List dayButtons = new ArrayList<>();
/**
* Creates a new instance of {@code DayOfWeekSelector}.
*/
public DayOfWeekSelector() {
- getStyle().set("padding", "var(--lumo-space-m)");
- this.setWidthFull();
-
- buttonsLayout = new HorizontalLayout();
- buttonsLayout.add(new DayOfWeekButton(DayOfWeek.SUNDAY, "S"));
- buttonsLayout.add(new DayOfWeekButton(DayOfWeek.MONDAY, "M"));
- buttonsLayout.add(new DayOfWeekButton(DayOfWeek.TUESDAY, "T"));
- buttonsLayout.add(new DayOfWeekButton(DayOfWeek.WEDNESDAY, "W"));
- buttonsLayout.add(new DayOfWeekButton(DayOfWeek.THURSDAY, "T"));
- buttonsLayout.add(new DayOfWeekButton(DayOfWeek.FRIDAY, "F"));
- buttonsLayout.add(new DayOfWeekButton(DayOfWeek.SATURDAY, "S"));
-
- buttonsLayout.setClassName("fc-days-of-week-selector-buttons-layout");
-
- getButtons().forEach(button -> button.addClickListener(ev -> updateValue()));
- add(buttonsLayout);
- clear();
+ initDefaultDayButtons();
}
/**
@@ -142,32 +141,35 @@ protected boolean valueEquals(Set value1, Set value2) {
return value1 != value2 && Objects.equals(value1, value2);
}
- private Stream getButtons() {
- return buttonsLayout.getChildren()
- .filter(DayOfWeekButton.class::isInstance)
- .map(DayOfWeekButton.class::cast);
- }
-
@Override
protected Set generateModelValue() {
- return getButtons().filter(DayOfWeekButton::getState).map(DayOfWeekButton::getDayOfWeek)
- .collect(Collectors.toCollection(this::getEmptyValue));
+ return dayButtons.stream().filter(DayOfWeekButton::getState).map(DayOfWeekButton::getDayOfWeek)
+ .collect(Collectors.toCollection(this::getEmptyValue));
}
@Override
protected void setPresentationValue(Set newPresentationValue) {
- getButtons()
- .forEach(button -> button.setState(newPresentationValue.contains(button.getDayOfWeek())));
+ dayButtons
+ .forEach(button -> button.setState(newPresentationValue.contains(button.getDayOfWeek())));
}
@Override
public void setReadOnly(boolean readOnly) {
getElement().setProperty("readonly", readOnly);
getElement().setAttribute("readonly", readOnly);
- getButtons().forEach(button -> button.setEnabled(!readOnly));
+ dayButtons.forEach(button -> {
+ button.setEnabled(!readOnly);
+ if (readOnly) {
+ button.addClassName("readOnly");
+ } else {
+ button.removeClassName("readOnly");
+ }
+ });
}
- /** Sets the value of this object. */
+ /**
+ * Sets the value of this object.
+ */
public void setValue(DayOfWeek first, DayOfWeek... rest) {
setValue(EnumSet.of(first, rest));
}
@@ -202,14 +204,14 @@ public void setWeekDaysShort(List weekdaysShort) {
for (DayOfWeek day : DayOfWeek.values()) {
int index = day.getValue() % 7;
String text = weekdaysShort.get(index);
- getButtons().filter(button -> button.getDayOfWeek() == day)
- .forEach(button -> button.setText(text));
+ dayButtons.stream().filter(button -> button.getDayOfWeek() == day)
+ .forEach(button -> button.setText(text));
}
}
/**
* Sets the tooltips of the week days, starting from {@code sun} and ending on {@code sat}.
- *
+ *
* @param weekdaysTooltip the tooltips of the week days
*/
public void setWeekDaysTooltip(List weekdaysTooltip) {
@@ -218,8 +220,8 @@ public void setWeekDaysTooltip(List weekdaysTooltip) {
for (DayOfWeek day : DayOfWeek.values()) {
int index = day.getValue() % 7;
String text = weekdaysTooltip.get(index);
- getButtons().filter(button -> button.getDayOfWeek() == day)
- .forEach(button -> button.setTooltipText(text));
+ dayButtons.stream().filter(button -> button.getDayOfWeek() == day)
+ .forEach(button -> button.setTooltipText(text));
}
}
@@ -233,14 +235,68 @@ public void setWeekDaysTooltip(List weekdaysTooltip) {
* @throws IllegalArgumentException if firstDayOfWeek is invalid
*/
public void setFirstDayOfWeek(DayOfWeek first) {
- DayOfWeekButton[] buttons = getButtons().toArray(DayOfWeekButton[]::new);
+ DayOfWeekButton[] buttons = dayButtons.toArray(DayOfWeekButton[]::new);
if (buttons[0].dayOfWeek != first) {
+ var sortedButtons = new ArrayList();
Arrays.sort(buttons, Comparator.comparing(DayOfWeekButton::getDayOfWeek));
- buttonsLayout.removeAll();
- for (int i = 0; i < 7; i++) {
- buttonsLayout.add(buttons[(i + first.getValue() - 1) % 7]);
+ for (int i = 0; i < buttons.length; i++) {
+ sortedButtons.add(buttons[(i + first.getValue() - 1) % buttons.length]);
}
+ setDayButtons(sortedButtons);
}
}
+ /**
+ * Sets the icon for the overflow menu trigger.
+ *
+ * @param icon the icon component, not {@code null}
+ * @throws NullPointerException if {@code icon} is {@code null}
+ */
+ public void setOverflowIcon(Component icon) {
+ Objects.requireNonNull(icon, "icon cannot be null");
+
+ // Remove any existing slotted icon before appending the new one
+ getElement().getChildren()
+ .filter(el -> "overflowIcon".equals(el.getAttribute("slot")))
+ .findFirst()
+ .ifPresent(Element::removeFromParent);
+
+ icon.setClassName("overflow-icon");
+ // Assign icon to the slot
+ icon.getElement().setAttribute("slot", "overflowIcon");
+ getElement().appendChild(icon.getElement());
+ }
+
+ private void initDefaultDayButtons() {
+ setDayButtons(List.of(new DayOfWeekButton(DayOfWeek.SUNDAY, "S"),
+ new DayOfWeekButton(DayOfWeek.MONDAY, "M"),
+ new DayOfWeekButton(DayOfWeek.TUESDAY, "T"),
+ new DayOfWeekButton(DayOfWeek.WEDNESDAY, "W"),
+ new DayOfWeekButton(DayOfWeek.THURSDAY, "T"),
+ new DayOfWeekButton(DayOfWeek.FRIDAY, "F"),
+ new DayOfWeekButton(DayOfWeek.SATURDAY, "S")
+ ));
+ }
+
+ private void setDayButtons(List buttons) {
+ this.clearDayButtons();
+ this.dayButtons = buttons;
+ buttons.forEach(button -> button.addClickListener(ev -> updateValue()));
+ this.addDayButtons();
+ }
+
+ private void addDayButtons() {
+ this.dayButtons.forEach(button -> {
+ button.getElement().setAttribute("slot", "daysOfWeek");
+ this.getElement().appendChild(button.getElement());
+ });
+ }
+
+ private void clearDayButtons() {
+ this.dayButtons.forEach(button -> {
+ button.getElement().removeAttribute("slot");
+ button.removeFromParent();
+ });
+ }
+
}
diff --git a/src/main/resources/META-INF/frontend/styles/fc-days-of-week-selector-styles.css b/src/main/resources/META-INF/frontend/styles/fc-days-of-week-selector-styles.css
index 0979d3d..77f4e67 100644
--- a/src/main/resources/META-INF/frontend/styles/fc-days-of-week-selector-styles.css
+++ b/src/main/resources/META-INF/frontend/styles/fc-days-of-week-selector-styles.css
@@ -35,18 +35,26 @@ vaadin-button.fc-days-of-week-selector-button[theme~='primary'][disabled]::part(
opacity: 1;
}
-vaadin-custom-field[readonly] vaadin-button.fc-days-of-week-selector-button[disabled] {
+vaadin-button.fc-days-of-week-selector-button[disabled].readOnly{
color: var(--lumo-secondary-text-color);
background: transparent;
}
-vaadin-custom-field[readonly] vaadin-button.fc-days-of-week-selector-button[theme~='primary'][disabled] {
+vaadin-button.fc-days-of-week-selector-button[theme~='primary'][disabled].readOnly {
border: var(--vaadin-input-field-readonly-border, 1px dashed var(--lumo-contrast-30pct));
}
+vaadin-button.fc-days-of-week-selector-button[focus-ring]{
+ box-shadow: none;
+}
+
vaadin-horizontal-layout.fc-days-of-week-selector-buttons-layout {
width: 100%;
justify-content: center;
flex-wrap: wrap;
padding: 0 10px;
}
+
+vaadin-context-menu-overlay vaadin-context-menu-list-box::part(items) {
+ padding: 0 var(--lumo-space-xs);
+}
diff --git a/src/main/resources/META-INF/resources/frontend/src/fc-days-of-week-selector.ts b/src/main/resources/META-INF/resources/frontend/src/fc-days-of-week-selector.ts
new file mode 100644
index 0000000..239e247
--- /dev/null
+++ b/src/main/resources/META-INF/resources/frontend/src/fc-days-of-week-selector.ts
@@ -0,0 +1,235 @@
+/*-
+ * #%L
+ * Day of Week Selector 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%
+ */
+import {ResizeMixin} from '@vaadin/component-base/src/resize-mixin.js';
+import '@vaadin/context-menu';
+import type {ContextMenuItem} from '@vaadin/context-menu';
+import {ThemableMixin} from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+import {css, html, LitElement} from 'lit';
+import {customElement, property, query, queryAssignedNodes, state} from 'lit/decorators.js';
+import {Button} from "@vaadin/button";
+
+@customElement('fc-days-of-week-selector')
+export class DaysOfWeekSelector extends ResizeMixin(ThemableMixin(LitElement)) {
+
+ @query('[part~="overflow-badge"]')
+ _overflowBadge!: HTMLElement
+
+ @queryAssignedNodes({slot: 'daysOfWeek', flatten: true})
+ _daysOfWeek!: Array;
+
+ @query('[part~="container"]')
+ _container!: HTMLElement;
+
+ @property()
+ theme: string = '';
+
+ @property()
+ label: string = '';
+
+ @state()
+ private _overflowItems: ContextMenuItem[] = [];
+
+ static styles = [
+ css`
+ :host {
+ display: block;
+ width: 100%;
+ }
+
+ [hidden] {
+ display: none;
+ }
+
+ [part="container"] {
+ position: relative;
+ display: flex;
+ width: 100%;
+ flex-wrap: nowrap;
+ overflow: hidden;
+ align-items: center;
+ }
+
+ [part="overflow-badge"] {
+ line-height: 0.5em;
+ }
+
+ :host(:not([has-label])) [part='label'] {
+ display: none;
+ }
+
+ [part="label"] {
+ align-self: flex-start;
+ color: var(--badge-list-label-color);
+ font-weight: var(--badge-list-label-font-weight);
+ font-size: var(--badge-list-label-font-size);
+ margin-left: var(--badge-list-label-margin-left);
+ transition: color 0.2s;
+ line-height: inherit;
+ padding-right: 1em;
+ padding-bottom: 0.5em;
+ padding-top: 0.25em;
+ margin-top: -0.25em;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ position: relative;
+ max-width: 100%;
+ box-sizing: border-box;
+ }
+
+ ::slotted([slot="overflowIcon"]) {
+ --lumo-icon-size-m: var(--fc-days-of-week-selector-overflow-icon-size, 1em);
+ }
+
+ /* Style the fallback inside the slot */
+ vaadin-icon.overflow-icon {
+ --lumo-icon-size-m: var(--fc-days-of-week-selector-overflow-icon-size, 0.75em);
+ }
+
+ `
+ ];
+
+ static get is() {
+ return 'fc-days-of-week-selector';
+ }
+
+ _set_theme(theme: string) {
+ this.theme = theme;
+ }
+
+ /**
+ * Override getter from `ResizeMixin` to observe parent.
+ *
+ * @protected
+ * @override
+ */
+ get _observeParent() {
+ return true;
+ }
+
+ /**
+ * Implement callback from `ResizeMixin` to update badges
+ * and detect whether to show or hide the overflow badge.
+ *
+ * @protected
+ * @override
+ */
+ _onResize() {
+ this._updateOnOverflow();
+ }
+
+ /**
+ * Returns overflow button width.
+ *
+ * @private
+ */
+ private _getOverflowButtonWidth() {
+ let overflowWidth = 0;
+ const badge = this._overflowBadge;
+ if (badge) {
+ const wasHidden = badge.hasAttribute('hidden');
+ if (wasHidden) {
+ badge.removeAttribute('hidden');
+ }
+ overflowWidth = badge.getBoundingClientRect().width || 0;
+ if (wasHidden) {
+ badge.setAttribute('hidden', '');
+ }
+ }
+ return overflowWidth;
+ }
+
+ private _updateOnOverflow() {
+ const containerWidth = this.getBoundingClientRect().width;
+ let usedWidth = 0;
+ const overflowButtons: Node[] = [];
+ const overflowButtonWidth = this._getOverflowButtonWidth();
+
+ for (const button of this._daysOfWeek) {
+ button.style.display = '';
+ const width = button.getBoundingClientRect().width;
+
+ if (usedWidth + width > containerWidth - overflowButtonWidth) {
+ const clonedButton = this._cloneButton(button);
+ overflowButtons.push(clonedButton);
+
+ // hide button from slot
+ button.style.display = 'none';
+ } else {
+ usedWidth += width;
+ }
+ }
+
+ // Update context menu items from hidden buttons
+ this._overflowItems = overflowButtons.map(button => ({
+ component: button,
+ }));
+
+ // Update badges visibility
+ this._overflowBadge.toggleAttribute('hidden', this._overflowItems.length < 1);
+ }
+
+ private _cloneButton(originalButton: Button) {
+ const clonedButton = originalButton.cloneNode(true);
+ clonedButton._originalButton = originalButton;
+ clonedButton.removeAttribute("slot");
+ clonedButton.style.display = '';
+ clonedButton.addEventListener('click', ev => {
+ // Update server side button component state
+ clonedButton._originalButton.$server.toggleState();
+ if (clonedButton.getAttribute("theme")) {
+ clonedButton.removeAttribute("theme")
+ } else {
+ clonedButton.setAttribute("theme", "primary");
+ }
+ // Notify value change to DayOfWeekSelector component
+ this.dispatchEvent(new CustomEvent('change', {bubbles: true}));
+ });
+ return clonedButton;
+ }
+
+ render() {
+ return html`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ update(_changedProperties: Map) {
+ if (_changedProperties.has('label')) {
+ this.toggleAttribute('has-label', this.label != null);
+ }
+ super.update(_changedProperties);
+ }
+
+}
\ 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 2f0554d..eb2d644 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java
@@ -19,10 +19,12 @@
*/
package com.flowingcode.vaadin.addons;
+import com.vaadin.flow.component.dependency.CssImport;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.router.RouterLayout;
@SuppressWarnings("serial")
+@CssImport("/styles/shared-styles.css")
public class DemoLayout extends Div implements RouterLayout {
public DemoLayout() {
diff --git a/src/test/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelectorDemo.java b/src/test/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelectorDemo.java
index 5b727fe..69939c2 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelectorDemo.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelectorDemo.java
@@ -7,9 +7,9 @@
* 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.
@@ -20,29 +20,54 @@
package com.flowingcode.vaadin.addons.dayofweekselector;
import com.flowingcode.vaadin.addons.demo.DemoSource;
+import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
+import com.vaadin.flow.theme.lumo.LumoIcon;
+
import java.time.DayOfWeek;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
@DemoSource
-@PageTitle("Demo")
@SuppressWarnings("serial")
+@PageTitle("Demo")
@Route(value = "day-of-week-selector/demo", layout = DayOfWeekSelectorDemoView.class)
-public class DayOfWeekSelectorDemo extends Div {
+public class DayOfWeekSelectorDemo extends VerticalLayout {
public DayOfWeekSelectorDemo() {
- DayOfWeekSelector selector1 =
- new DayOfWeekSelector("Enabled", DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY);
- add(selector1);
+ addClassName("demo");
+
+ DayOfWeekSelector selector1 = new DayOfWeekSelector("Enabled", DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY);
+ selector1.setWidthFull();
+ Button updateValueButton = new Button("Set value to SUN-MON-SAT");
+ updateValueButton.addClickListener(e -> {
+ selector1.setValue(DayOfWeek.SUNDAY, DayOfWeek.MONDAY, DayOfWeek.FRIDAY, DayOfWeek.SATURDAY);
+ });
+ VerticalLayout content=new VerticalLayout(selector1, updateValueButton);
+ content.setPadding(false);
+ add(content);
+
+ DayOfWeekSelector selectorOverflow = new DayOfWeekSelector("Overflow", DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY);
+ selectorOverflow.setWidth("200px");
+ selectorOverflow.setOverflowIcon(LumoIcon.ANGLE_RIGHT.create());
+ selectorOverflow.getStyle().set(DayOfWeekSelector.CssProperties.OVERFLOW_ICON_SIZE, "1em");
+
+ Div valueContent = new Div();
+ Consumer> valueRenderer =
+ value -> valueContent.setText("Value: " + value.stream().map(Enum::name).collect(Collectors.joining(" - ")));
+ add(new Div(selectorOverflow, valueContent));
+ valueRenderer.accept(selectorOverflow.getValue());
+ selectorOverflow.addValueChangeListener(e -> valueRenderer.accept(e.getValue()));
- DayOfWeekSelector selector2 =
- new DayOfWeekSelector("Read-only", DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY);
+ DayOfWeekSelector selector2 = new DayOfWeekSelector("Read-only", DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY);
selector2.setReadOnly(true);
add(selector2);
- DayOfWeekSelector selector3 =
- new DayOfWeekSelector("Disabled", DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY);
+ DayOfWeekSelector selector3 = new DayOfWeekSelector("Disabled", DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY);
selector3.setEnabled(false);
add(selector3);
}
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 8b8413b..9db9d63 100644
--- a/src/test/resources/META-INF/frontend/styles/shared-styles.css
+++ b/src/test/resources/META-INF/frontend/styles/shared-styles.css
@@ -18,3 +18,13 @@
* #L%
*/
/*Demo styles*/
+html {
+}
+
+.demo {
+ gap: var(--lumo-space-m);
+}
+
+fc-days-of-week-selector::part(label) {
+ font-weight: bold;
+}
\ No newline at end of file