From 3e586d3f4689ab36625836b467466a5c6c796368 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Tue, 2 Jun 2026 18:05:46 -0300 Subject: [PATCH 01/11] feat: initial implementation --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 +- .github/ISSUE_TEMPLATE/feature-request.yml | 2 +- README.md | 47 ++- SPECIFICATIONS.md | 212 +++++++++++ pom.xml | 16 +- .../relativetime/DateTimePartStyle.java | 51 +++ .../vaadin/addons/relativetime/Format.java | 68 ++++ .../addons/relativetime/FormatStyle.java | 37 ++ .../Precision.java} | 32 +- .../addons/relativetime/RelativeTime.java | 353 ++++++++++++++++++ .../vaadin/addons/relativetime/Tense.java} | 33 +- .../addons/relativetime/TimeZoneName.java | 50 +++ .../META-INF/VAADIN/package.properties | 19 + .../addons/AppShellConfiguratorImpl.java | 35 ++ .../flowingcode/vaadin/addons/DemoLayout.java | 8 +- .../AbstractRelativeTimeDemo.java | 62 +++ .../{template => relativetime}/DemoView.java | 12 +- .../addons/relativetime/RelativeTimeDemo.java | 315 ++++++++++++++++ .../RelativeTimeDemoView.java} | 23 +- .../addons/relativetime/UseCasesDemo.java | 318 ++++++++++++++++ .../it/AbstractViewTest.java | 10 +- .../{template => relativetime}/it/ViewIT.java | 22 +- .../relativetime/test/RelativeTimeTest.java | 285 ++++++++++++++ .../relativetime/test/SerializationTest.java | 90 +++++ .../template/test/SerializationTest.java | 52 --- .../relative-time-add-on-demo-styles.css | 134 +++++++ .../frontend/styles/shared-styles.css | 1 - 27 files changed, 2151 insertions(+), 138 deletions(-) create mode 100644 SPECIFICATIONS.md create mode 100644 src/main/java/com/flowingcode/vaadin/addons/relativetime/DateTimePartStyle.java create mode 100644 src/main/java/com/flowingcode/vaadin/addons/relativetime/Format.java create mode 100644 src/main/java/com/flowingcode/vaadin/addons/relativetime/FormatStyle.java rename src/main/java/com/flowingcode/vaadin/addons/{template/TemplateAddon.java => relativetime/Precision.java} (54%) create mode 100644 src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java rename src/{test/java/com/flowingcode/vaadin/addons/template/TemplateDemo.java => main/java/com/flowingcode/vaadin/addons/relativetime/Tense.java} (53%) create mode 100644 src/main/java/com/flowingcode/vaadin/addons/relativetime/TimeZoneName.java create mode 100644 src/test/java/com/flowingcode/vaadin/addons/AppShellConfiguratorImpl.java create mode 100644 src/test/java/com/flowingcode/vaadin/addons/relativetime/AbstractRelativeTimeDemo.java rename src/test/java/com/flowingcode/vaadin/addons/{template => relativetime}/DemoView.java (85%) create mode 100644 src/test/java/com/flowingcode/vaadin/addons/relativetime/RelativeTimeDemo.java rename src/test/java/com/flowingcode/vaadin/addons/{template/TemplateDemoView.java => relativetime/RelativeTimeDemoView.java} (65%) create mode 100644 src/test/java/com/flowingcode/vaadin/addons/relativetime/UseCasesDemo.java rename src/test/java/com/flowingcode/vaadin/addons/{template => relativetime}/it/AbstractViewTest.java (96%) rename src/test/java/com/flowingcode/vaadin/addons/{template => relativetime}/it/ViewIT.java (69%) create mode 100644 src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java create mode 100644 src/test/java/com/flowingcode/vaadin/addons/relativetime/test/SerializationTest.java delete mode 100644 src/test/java/com/flowingcode/vaadin/addons/template/test/SerializationTest.java create mode 100644 src/test/resources/META-INF/frontend/styles/relative-time-add-on-demo-styles.css delete mode 100644 src/test/resources/META-INF/frontend/styles/shared-styles.css diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index b1540d4..3300a17 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 Relative Time 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..d74cca6 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 Relative Time Add-On here. body: - type: textarea id: feature-proposal diff --git a/README.md b/README.md index d4224a3..b9b6d9c 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,26 @@ -[![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/relative-time-add-on) +[![Stars on vaadin.com/directory](https://img.shields.io/vaadin-directory/star/relative-time-addon.svg)](https://vaadin.com/directory/component/relative-time-add-on) +[![Build Status](https://jenkins.flowingcode.com/job/RelativeTime-addon/badge/icon)](https://jenkins.flowingcode.com/job/RelativeTime-addon) +[![Maven Central](https://img.shields.io/maven-central/v/com.flowingcode.vaadin.addons/relative-time-addon)](https://mvnrepository.com/artifact/com.flowingcode.vaadin.addons/relative-time-addon) +[![Javadoc](https://img.shields.io/badge/javadoc-00b4f0)](https://javadoc.flowingcode.com/artifact/com.flowingcode.vaadin.addons/relative-time-addon) -# Template Add-On +# Relative Time Add-On -This is a template project for building new Vaadin 24 add-ons +Component for rendering dates as auto-updating, locale-aware relative time strings. ## Features -* List the features of your add-on in here +* Renders a date/time as a human-readable relative string ("4 hours from now", "3 days ago", "in 2 weeks") that updates in the browser as time passes. +* Accepts the standard `java.time` types (`Instant`, `OffsetDateTime`, `ZonedDateTime`, `LocalDateTime`, `LocalDate`). +* Typed Java API for the upstream [`@github/relative-time-element`](https://github.com/github/relative-time-element) attributes: tense, format, precision, format style, threshold, prefix, locale, time-zone, and per-part absolute-date formatting (year/month/day/weekday/hour/minute/second). ## Online demo -[Online demo here](http://addonsv24.flowingcode.com/template) +[Online demo here](http://addonsv25.flowingcode.com/relative-time) ## Download release -[Available in Vaadin Directory](https://vaadin.com/directory/component/template-add-on) +[Available in Vaadin Directory](https://vaadin.com/directory/component/relative-time-add-on) ### Maven install @@ -27,7 +29,7 @@ Add the following dependencies in your pom.xml file: ```xml com.flowingcode.vaadin.addons - template-addon + relative-time-addon X.Y.Z ``` @@ -44,7 +46,7 @@ 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/RelativeTime/releases) ## Issue tracking @@ -84,13 +86,30 @@ 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. +Relative Time Add-On is written by Flowing Code S.A. # Developer Guide ## Getting started -Add your code samples in this section +Add a `RelativeTime` to your layout and configure it through fluent setters: + +```java +RelativeTime deadline = new RelativeTime(task.getDueDate()) + .setTense(Tense.AUTO) + .setFormat(Format.RELATIVE) + .setFormatStyle(FormatStyle.LONG) + .setThreshold(Duration.ofDays(30)) // switch to absolute date past a month + .setLocale(UI.getCurrent().getLocale()); + +add(new Span("Due "), deadline); +``` + +The setters accept any of `Instant`, `OffsetDateTime`, `ZonedDateTime`, `LocalDateTime`, or `LocalDate`. `LocalDateTime` and `LocalDate` are interpreted in the server's default zone. + +The relative string is computed and updated in the browser by the underlying web component. The server sends only the target datetime, so the text reflects the **viewer's** clock and locale, not the server's, and there is no server-side API to read the rendered string. + +For continuously-ticking elapsed displays use `Format.DURATION` or `Format.MICRO`. `Format.RELATIVE` (the default) collapses past times under a minute to "now". See [SPECIFICATIONS.md](SPECIFICATIONS.md) §2.8 for the full live-update behaviour matrix. ## Special configuration when using Spring diff --git a/SPECIFICATIONS.md b/SPECIFICATIONS.md new file mode 100644 index 0000000..3a7653d --- /dev/null +++ b/SPECIFICATIONS.md @@ -0,0 +1,212 @@ +# Relative Time Add-on - Specification + +## 1. Overview + +The Relative Time Add-on is a component that renders a date/time as a human-readable relative string ("4 hours from now", "3 days ago", "in 2 weeks") that updates in the browser as time passes and is shown in the viewer's local time. + +`RelativeTime` is a thin wrapper around [`@github/relative-time-element`](https://github.com/github/relative-time-element), a small, well-maintained, accessibility-tested custom element. The wrapper exposes the element's attributes as a typed Java API and accepts the standard `java.time` types for the target date. + +The component is added to the layout directly. Display options (tense, format, precision, threshold, locale, time-zone, individual date parts) are configured through setters on the `RelativeTime` instance. + +## 2. Core Concepts + +### 2.1 Client-Side Rendering + +The relative string is computed and updated in the browser by the underlying web component. The server sends only the target datetime as an ISO-8601 string in the `datetime` attribute; no server round-trip is needed to keep the displayed text current. + +This has two consequences worth being explicit about: + +- The text reflects the **viewer's** clock and locale, not the server's (unless `time-zone` is set explicitly). +- The server-side component has no API to read the rendered string, there is nothing to read; the string lives only in the DOM. + +### 2.2 Java-Type to Attribute Mapping + +The setters accept the standard `java.time` types. Each is converted to an ISO-8601 instant before being written to the `datetime` attribute: + +| Java Type | Conversion | Notes | +|-----------|------------|-------| +| `Instant` | direct | Canonical form. | +| `OffsetDateTime` | `toInstant()` | Offset preserved in the wire string. | +| `ZonedDateTime` | `toInstant()` | Zone preserved in the wire string. | +| `LocalDateTime` | `atZone(systemDefault()).toInstant()` | Server zone assumed; document this in the Javadoc. | +| `LocalDate` | `atStartOfDay(systemDefault()).toInstant()` | Midnight of the server zone. | + +`null` clears the `datetime` attribute and the component renders as empty. + +The wrapper drives the element through HTML attributes (`setAttribute`), not DOM properties: `@github/relative-time-element` is attribute-driven, and its kebab-case attribute names match the upstream docs one-to-one (§2.3). + +### 2.3 Display Modes + +The web component supports several formats. The Java API exposes them as enums with values that match the attribute vocabulary one-to-one, so a developer reading either side of the wrapper sees the same names: + +| Enum | Values | Maps to attribute | Notes | +|------|--------|-------------------|-------| +| `Tense` | `AUTO`, `PAST`, `FUTURE` | `tense` | Default `auto`. Ignored when `format=datetime`. (Subsumes the upstream `` / `` / `` element variants, which are degenerate cases of ``; no separate Java classes are provided.) | +| `Format` | `RELATIVE`, `DURATION`, `DATETIME`, `AUTO`, `ELAPSED`, `MICRO` | `format` | Default `auto`, which is an alias for `relative`. | +| `Precision` | `YEAR`, `MONTH`, `DAY`, `HOUR`, `MINUTE`, `SECOND` | `precision` | Default `second`. Ignored when `format=datetime`. | +| `FormatStyle` | `LONG`, `SHORT`, `NARROW` | `format-style` | Default depends on `format`: `narrow` for `elapsed`/`micro`, `short` for `datetime`, `long` for `relative`/`auto`/`duration`. | + +### 2.4 Threshold + +The `threshold` attribute (default `P30D`) switches the component from relative ("3 weeks ago") to absolute ("Jan 4, 2024") once the gap to the target exceeds a configurable duration. The Java setter accepts a `java.time.Duration` and serialises it to ISO-8601 (`P30D`, `PT24H`, etc.) before writing the attribute. + +```java +relativeTime.setThreshold(Duration.ofDays(30)); // → threshold="P30D" +``` + +**Threshold is only consulted when `format=auto`/`relative` AND `tense=auto`.** Setting `tense=PAST` or `tense=FUTURE` commits the element to relative phrasing regardless of how far away the target is, so the threshold has no effect. Likewise, explicit non-relative formats (`DURATION`, `MICRO`, `ELAPSED`, `DATETIME`) ignore it. This is a common footgun: pairing `setTense(FUTURE)` with `setThreshold(...)` silently makes the threshold inert. + +### 2.5 Prefix + +The `prefix` attribute (default `"on"`) is the word prepended to absolute dates once the threshold is crossed. `setPrefix("")` drops the prefix entirely; `setPrefix(null)` restores the default. + +(Future-tense phrasing like "in 3 days" comes from `Intl.RelativeTimeFormat`, not from `prefix`.) + +### 2.6 Locale + +`setLocale(Locale)` writes the `lang` attribute on the element. The browser's `Intl.RelativeTimeFormat` and `Intl.DateTimeFormat` produce the localized phrasing ("hace 4 horas", "in 4 ore"). When no locale is set, the browser uses the document language. + +**DOM inheritance.** When the element has no `lang` of its own, the upstream walks the DOM ancestors via `closest('[lang]')` and falls back to the `` element's `lang`. This means an app-wide locale set on `` (or via Vaadin's `UI.setLocale(...)`, which forwards to the document) is automatically picked up by every `RelativeTime` without per-instance configuration. + +### 2.7 Time-Zone and Absolute-Date Formatting + +These attributes only take effect when `format=datetime` (i.e. when the absolute date is rendered, either explicitly or after the threshold is crossed). They are not relevant for purely relative output. + +| Setter | Attribute | Java type / values | +|--------|-----------|--------------------| +| `setTimeZone(ZoneId)` | `time-zone` | IANA name (e.g. `America/New_York`). `null` removes it; the browser default is used. When unset on the element, the upstream walks DOM ancestors via `closest('[time-zone]')` and falls back to the `` element's `time-zone` attribute, mirroring the `lang` inheritance pattern. | +| `setTimeZoneName(TimeZoneName)` | `time-zone-name` | `LONG`, `SHORT`, `SHORT_OFFSET`, `LONG_OFFSET`, `SHORT_GENERIC`, `LONG_GENERIC` | +| `setYear(DateTimePartStyle)` | `year` | `NUMERIC`, `TWO_DIGIT` | +| `setMonth(DateTimePartStyle)` | `month` | `NUMERIC`, `TWO_DIGIT`, `NARROW`, `SHORT`, `LONG` | +| `setDay(DateTimePartStyle)` | `day` | `NUMERIC`, `TWO_DIGIT` | +| `setWeekday(DateTimePartStyle)` | `weekday` | `NARROW`, `SHORT`, `LONG` | +| `setHour(DateTimePartStyle)` | `hour` | `NUMERIC`, `TWO_DIGIT` | +| `setMinute(DateTimePartStyle)` | `minute` | `NUMERIC`, `TWO_DIGIT` | +| `setSecond(DateTimePartStyle)` | `second` | `NUMERIC`, `TWO_DIGIT` | + +`DateTimePartStyle` is a single enum spanning every value any of the seven date-part attributes can take; per-setter Javadoc documents the subset that's meaningful for that part. Invalid combinations are not validated on the Java side; the upstream element silently falls back. + +`TimeZoneName.SHORT_OFFSET` / `LONG_OFFSET` / `SHORT_GENERIC` / `LONG_GENERIC` are written to the wire in camelCase (`shortOffset`, etc.) to match the `Intl.DateTimeFormat` vocabulary. + +### 2.8 Live Update Behaviour + +The upstream element auto-updates itself: an internal `setTimeout` loop re-renders the displayed phrase on each unit boundary as wall-clock time advances. No Java-side timer is involved. When the user opens a page showing `RelativeTime(Instant.now().minus(Duration.ofHours(2)))`, they'll see the phrase advance from "2 hours ago" to "3 hours ago" without any server round-trip. + +The cadence depends on the displayed unit: + +- Seconds-level display → one re-render per second +- Minutes-level display → one re-render per minute +- Hours-level display → one re-render per hour +- And so on, getting progressively cheaper as the unit grows. + +**Format-specific quirks worth knowing:** + +| Format | Live behaviour | +|---|---| +| `RELATIVE` / `AUTO` | **Collapses past times under ~55 seconds to "now"** because of a signed-int threshold in the element source (`int < 10` with negative ints, so every past second-unit elapsed counts as "now"). Then jumps to "1 minute ago" when the duration rounds up. Use this format when you want human-friendly relative phrasing and don't care about sub-minute precision. | +| `DURATION` / `ELAPSED` | Tick every second from zero (`0s`, `1s`, `2s`, …). No "now" plateau. Right choice for live elapsed-time displays (timers, "uptime" counters). | +| `MICRO` | Compact single-unit form (`5s`, `2m`, `3h`, `5d`). Re-renders only when the displayed unit changes, so it ticks less frequently than `DURATION`. | +| `DATETIME` | Does not tick; absolute dates don't change. | + +**Browser-imposed limits:** when a tab is backgrounded, browsers throttle `setTimeout` callbacks to roughly once per minute. A `RelativeTime` in a hidden tab will look frozen and "catch up" only when the tab regains focus. This is browser behaviour; nothing the wrapper or the element can override. + +## 3. API Design + +### 3.1 Construction + +```java +// Empty: datetime can be set later +RelativeTime rt = new RelativeTime(); +add(rt); + +// From any supported java.time type +add(new RelativeTime(Instant.now().plus(4, ChronoUnit.HOURS))); +add(new RelativeTime(LocalDate.of(2025, 1, 1))); +``` + +### 3.2 Setting the Target Date + +```java +public class RelativeTime extends Component implements HasSize { + + public RelativeTime(); // empty; datetime can be set later + public RelativeTime(Instant datetime); + public RelativeTime(OffsetDateTime datetime); + public RelativeTime(ZonedDateTime datetime); + public RelativeTime(LocalDateTime datetime); // uses ZoneId.systemDefault() + public RelativeTime(LocalDate date); // uses ZoneId.systemDefault() + + public RelativeTime setDateTime(Instant datetime); + public RelativeTime setDateTime(OffsetDateTime datetime); + public RelativeTime setDateTime(ZonedDateTime datetime); + public RelativeTime setDateTime(LocalDateTime datetime); // uses ZoneId.systemDefault() + public RelativeTime setDateTime(LocalDate date); // uses ZoneId.systemDefault() + public Instant getDateTime(); // last value pushed, in UTC +} +``` + +### 3.3 Display Configuration + +```java +// Relative-format controls +public RelativeTime setTense(Tense tense); +public RelativeTime setFormat(Format format); +public RelativeTime setPrecision(Precision precision); +public RelativeTime setFormatStyle(FormatStyle style); +public RelativeTime setThreshold(Duration threshold); +public RelativeTime setPrefix(String prefix); // "" drops prefix; null restores default +public RelativeTime setNoTitle(boolean noTitle); // suppress the absolute-date tooltip +public RelativeTime setLocale(Locale locale); + +// Absolute-format controls (apply when format=DATETIME) +public RelativeTime setTimeZone(ZoneId timeZone); +public RelativeTime setTimeZoneName(TimeZoneName value); +public RelativeTime setYear(DateTimePartStyle style); +public RelativeTime setMonth(DateTimePartStyle style); +public RelativeTime setDay(DateTimePartStyle style); +public RelativeTime setWeekday(DateTimePartStyle style); +public RelativeTime setHour(DateTimePartStyle style); +public RelativeTime setMinute(DateTimePartStyle style); +public RelativeTime setSecond(DateTimePartStyle style); +``` + +All setters return `this` to support fluent chaining. All reference-typed setters accept `null` to clear the corresponding attribute (falling back to the element's default). `setNoTitle(false)` is the no-op / clear equivalent for the boolean setter. + +### 3.4 Usage Example + +```java +RelativeTime deadline = new RelativeTime(task.getDueDate()) + .setTense(Tense.AUTO) + .setFormat(Format.RELATIVE) + .setFormatStyle(FormatStyle.LONG) + .setThreshold(Duration.ofDays(30)) // switch to absolute date past a month + .setLocale(UI.getCurrent().getLocale()); + +add(new Span("Due "), deadline); +``` + +## 4. Default Behavior + +When no configuration is applied: + +- The element renders the relative string in the browser's current language. +- `format` is `auto` (equivalent to `relative`), `tense` is `auto`, `precision` is `second`, `threshold` is `P30D`, `prefix` is `"on"`. +- `time-zone` is unset, so absolute-date output uses the viewer's browser default zone. +- The `title` attribute is set automatically to the absolute formatted date and is surfaced as a native tooltip. +- The element auto-updates on its own timer; no polling code is needed on the Java side. +- `RelativeTime` with no `datetime` set renders as an empty inline element. + +## 5. Theming + +The component is rendered as inline text and inherits font, color, and size from its parent. It is compatible with both **Lumo** and **Aura** with no theme-specific CSS. Standard `HasStyle` (inherited from `Component`) and `HasSize` are exposed on the Java side for layout adjustments. + +## 6. Serialization + +`RelativeTime` must be fully serializable for Vaadin session persistence. The only Java-side state is the last-applied `Instant` (backing `getDateTime()`); all other configuration lives on the `Element`'s attributes and is serialised as part of the Vaadin state node. All inputs accepted by the setters (`Instant`, `Duration`, `Locale`, `ZoneId`, enums) are themselves `Serializable`. + +## 7. Dependencies + +- Vaadin Flow (24.x and 25.x). +- npm: `@github/relative-time-element`, currently pinned to `5.0.0` via `@NpmPackage` on the component class. +- Lombok (per Flowing Code convention for new add-ons; not used directly in `RelativeTime` because every setter performs a side-effecting attribute write and cannot be lombok-generated). + diff --git a/pom.xml b/pom.xml index b2bb59f..76624cc 100644 --- a/pom.xml +++ b/pom.xml @@ -5,10 +5,10 @@ 4.0.0 com.flowingcode.vaadin.addons - template-addon + relative-time-addon 1.0.0-SNAPSHOT - Template Add-on - Template Add-on for Vaadin Flow + Relative Time Add-On + Component for rendering dates as auto-updating, locale-aware relative time strings https://www.flowingcode.com/en/open-source/ @@ -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/RelativeTime + scm:git:git://github.com/FlowingCode/RelativeTime.git + scm:git:ssh://git@github.com:/FlowingCode/RelativeTime.git master @@ -330,7 +330,7 @@ jetty-maven-plugin ${jetty.version} - 0 + 0 jar @@ -580,7 +580,7 @@ 21 21 - 25.0.3 + 25.1.6 diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/DateTimePartStyle.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/DateTimePartStyle.java new file mode 100644 index 0000000..4796fe5 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/DateTimePartStyle.java @@ -0,0 +1,51 @@ +/*- + * #%L + * Relative Time 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.relativetime; + +/** + * Values for the {@code Intl.DateTimeFormat} date-part attributes ({@code year}, {@code month}, + * {@code day}, {@code hour}, {@code minute}, {@code second}, {@code weekday}). Not every part + * accepts every value; each setter on {@link RelativeTime} documents its valid subset. + * + *

Only relevant when {@link Format#DATETIME} is in effect. + */ +public enum DateTimePartStyle { + /** Numeric ("1", "2", "12"). Accepted by year, month, day, hour, minute, second. */ + NUMERIC("numeric"), + /** Two-digit, zero-padded ("01", "12"). Accepted by year, month, day, hour, minute, second. */ + TWO_DIGIT("2-digit"), + /** Narrow ("M", "T"). Accepted by month and weekday. */ + NARROW("narrow"), + /** Short ("Mon", "Tue"). Accepted by month and weekday. */ + SHORT("short"), + /** Long ("Monday", "Tuesday"). Accepted by month and weekday. */ + LONG("long"); + + private final String attributeValue; + + DateTimePartStyle(String attributeValue) { + this.attributeValue = attributeValue; + } + + String attributeValue() { + return attributeValue; + } +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/Format.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/Format.java new file mode 100644 index 0000000..33e1f3a --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/Format.java @@ -0,0 +1,68 @@ +/*- + * #%L + * Relative Time 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.relativetime; + +import java.util.Locale; + +/** Values for the {@code format} attribute on {@code }. */ +public enum Format { + /** + * Relative phrase ("3 days ago"). Uses {@code Intl.RelativeTimeFormat} under the hood and + * collapses past times under ~55 seconds to "now" regardless of the {@link Precision} + * setting. This is a signed-int threshold in the upstream element, not something the wrapper + * can override. If you need continuous second-by-second ticking from zero, use {@link #DURATION} + * or {@link #MICRO}. + */ + RELATIVE, + /** + * Duration breakdown ("3d 4h", "1m 5s"). Ticks every second from zero with no "now" threshold, + * making it the right choice for live elapsed-time displays. Re-renders on every second + * boundary while the displayed value contains seconds. + */ + DURATION, + /** + * Absolute formatted date-time. Does not tick: once rendered, the displayed string + * doesn't change unless the inputs do. Use {@link RelativeTime#setTimeZone setTimeZone}, + * {@link RelativeTime#setTimeZoneName setTimeZoneName}, and the per-part setters + * ({@link RelativeTime#setYear setYear}, etc.) to shape the output. + */ + DATETIME, + /** + * Alias for {@link #RELATIVE} (the upstream default). Same behaviour, same "now" plateau for + * past times under ~55 seconds. + */ + AUTO, + /** + * Elapsed-time style. Routes through the same duration-format path as {@link #DURATION}; ticks + * second-by-second with no "now" threshold. + */ + ELAPSED, + /** + * Compact single-unit form ("3d", "5h", "2m"). Re-renders only when the displayed unit changes, + * so it ticks less frequently than {@link #DURATION}. Good when space is tight (badges, table + * cells). + */ + MICRO; + + String attributeValue() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/FormatStyle.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/FormatStyle.java new file mode 100644 index 0000000..fd7b15d --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/FormatStyle.java @@ -0,0 +1,37 @@ +/*- + * #%L + * Relative Time 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.relativetime; + +import java.util.Locale; + +/** Values for the {@code format-style} attribute on {@code }. */ +public enum FormatStyle { + /** Long phrasing ("3 days ago"). */ + LONG, + /** Short phrasing ("3 days ago" with abbreviations where the locale supports it). */ + SHORT, + /** Narrow phrasing ("3d ago"). */ + NARROW; + + String attributeValue() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/template/TemplateAddon.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/Precision.java similarity index 54% rename from src/main/java/com/flowingcode/vaadin/addons/template/TemplateAddon.java rename to src/main/java/com/flowingcode/vaadin/addons/relativetime/Precision.java index a318158..5141c4b 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/template/TemplateAddon.java +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/Precision.java @@ -1,8 +1,8 @@ /*- * #%L - * Template Add-on + * Relative Time 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. @@ -18,15 +18,23 @@ * #L% */ -package com.flowingcode.vaadin.addons.template; +package com.flowingcode.vaadin.addons.relativetime; -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; +import java.util.Locale; -@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 {} +/** + * Values for the {@code precision} attribute on {@code }. The element rounds the + * displayed phrase to the chosen unit. + */ +public enum Precision { + YEAR, + MONTH, + DAY, + HOUR, + MINUTE, + SECOND; + + String attributeValue() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java new file mode 100644 index 0000000..4ecd1e1 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java @@ -0,0 +1,353 @@ +/*- + * #%L + * Relative Time 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.relativetime; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasSize; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.component.dependency.NpmPackage; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Locale; + +/** + * Renders a date/time as a human-readable relative string ("4 hours ago", "in 2 weeks") that + * updates in the browser as time passes and is shown in the viewer's local time. + * + *

Thin wrapper around the {@code @github/relative-time-element} + * custom element. The Java setters write the corresponding attributes on the element and the + * browser does all the formatting and updating work. + * + *

Client-side rendering. The rendered text reflects the viewer's clock and locale, not + * the server's. There is no server-side API to read the displayed string; the string lives only + * in the DOM. + * + *

Attributes, not properties. Setters write HTML attributes + * ({@code setAttribute}), not DOM properties, because the upstream element is + * attribute-driven and its kebab-case attribute names match the upstream docs + * one-to-one. {@code setProperty} would force diverging camelCase names. + * + *

{@code LocalDateTime} and {@code LocalDate} inputs are interpreted in the server's + * {@link ZoneId#systemDefault() default zone} before being serialised as an instant. + */ +@Tag("relative-time") +@NpmPackage(value = "@github/relative-time-element", version = "5.0.0") +@JsModule("@github/relative-time-element") +@SuppressWarnings("serial") +public class RelativeTime extends Component implements HasSize { + + private static final String ATTR_DATETIME = "datetime"; + private static final String ATTR_TENSE = "tense"; + private static final String ATTR_FORMAT = "format"; + private static final String ATTR_PRECISION = "precision"; + private static final String ATTR_FORMAT_STYLE = "format-style"; + private static final String ATTR_THRESHOLD = "threshold"; + private static final String ATTR_PREFIX = "prefix"; + private static final String ATTR_NO_TITLE = "no-title"; + private static final String ATTR_LANG = "lang"; + private static final String ATTR_TIME_ZONE = "time-zone"; + private static final String ATTR_TIME_ZONE_NAME = "time-zone-name"; + private static final String ATTR_YEAR = "year"; + private static final String ATTR_MONTH = "month"; + private static final String ATTR_DAY = "day"; + private static final String ATTR_WEEKDAY = "weekday"; + private static final String ATTR_HOUR = "hour"; + private static final String ATTR_MINUTE = "minute"; + private static final String ATTR_SECOND = "second"; + + private Instant lastDateTime; + + /** Creates an empty component. {@link #setDateTime} can be called later. */ + public RelativeTime() {} + + /** Creates a component bound to the given instant. */ + public RelativeTime(Instant datetime) { + setDateTime(datetime); + } + + /** Creates a component bound to the given offset date-time. */ + public RelativeTime(OffsetDateTime datetime) { + setDateTime(datetime); + } + + /** Creates a component bound to the given zoned date-time. */ + public RelativeTime(ZonedDateTime datetime) { + setDateTime(datetime); + } + + /** + * Creates a component bound to the given local date-time, interpreted in the server's + * {@link ZoneId#systemDefault() default zone}. + */ + public RelativeTime(LocalDateTime datetime) { + setDateTime(datetime); + } + + /** + * Creates a component bound to midnight of the given date, in the server's + * {@link ZoneId#systemDefault() default zone}. + */ + public RelativeTime(LocalDate date) { + setDateTime(date); + } + + /** + * Sets the target instant. {@code null} clears the {@code datetime} attribute and the component + * renders as empty. + */ + public RelativeTime setDateTime(Instant datetime) { + lastDateTime = datetime; + setOrRemove(ATTR_DATETIME, datetime == null ? null : datetime.toString()); + return this; + } + + /** Sets the target as an {@link OffsetDateTime}; the offset is preserved on the wire string. */ + public RelativeTime setDateTime(OffsetDateTime datetime) { + return setDateTime(datetime == null ? null : datetime.toInstant()); + } + + /** Sets the target as a {@link ZonedDateTime}; the zone is preserved on the wire string. */ + public RelativeTime setDateTime(ZonedDateTime datetime) { + return setDateTime(datetime == null ? null : datetime.toInstant()); + } + + /** + * Sets the target as a {@link LocalDateTime}, interpreted in the server's + * {@link ZoneId#systemDefault() default zone}. + */ + public RelativeTime setDateTime(LocalDateTime datetime) { + return setDateTime( + datetime == null ? null : datetime.atZone(ZoneId.systemDefault()).toInstant()); + } + + /** + * Sets the target to midnight of the given date, in the server's + * {@link ZoneId#systemDefault() default zone}. + */ + public RelativeTime setDateTime(LocalDate date) { + return setDateTime( + date == null ? null : date.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } + + /** + * Returns the last instant applied via any of the {@code setDateTime} overloads, normalised to + * UTC. Returns {@code null} if no datetime has been set or it was cleared. + */ + public Instant getDateTime() { + return lastDateTime; + } + + /** + * Sets the {@code tense} attribute (default {@code auto}). Ignored when {@link #setFormat + * format} is {@link Format#DATETIME}. {@code null} removes the attribute. + */ + public RelativeTime setTense(Tense tense) { + setOrRemove(ATTR_TENSE, tense == null ? null : tense.attributeValue()); + return this; + } + + /** + * Sets the {@code format} attribute (default {@code auto}, which is an alias for + * {@link Format#RELATIVE}). {@code null} removes the attribute. + */ + public RelativeTime setFormat(Format format) { + setOrRemove(ATTR_FORMAT, format == null ? null : format.attributeValue()); + return this; + } + + /** + * Sets the {@code precision} attribute (default {@code second}). Ignored when {@link #setFormat + * format} is {@link Format#DATETIME}. {@code null} removes the attribute. + */ + public RelativeTime setPrecision(Precision precision) { + setOrRemove(ATTR_PRECISION, precision == null ? null : precision.attributeValue()); + return this; + } + + /** + * Sets the {@code format-style} attribute. The default depends on the {@link #setFormat format} + * in effect: {@code narrow} for {@link Format#ELAPSED}/{@link Format#MICRO}, {@code short} for + * {@link Format#DATETIME}, {@code long} for {@link Format#RELATIVE}/{@link Format#AUTO}/{@link + * Format#DURATION}. {@code null} removes the attribute. + */ + public RelativeTime setFormatStyle(FormatStyle style) { + setOrRemove(ATTR_FORMAT_STYLE, style == null ? null : style.attributeValue()); + return this; + } + + /** + * Sets the {@code threshold} attribute as an ISO-8601 duration ({@code P30D}, {@code PT24H}). + * Default is {@code P30D}. Once the gap to the target exceeds this duration the element switches + * from a relative phrase to an absolute date. {@code null} removes the attribute. + * + *

Only consulted when {@link #setFormat format} is {@link Format#AUTO}/{@link + * Format#RELATIVE} AND {@link #setTense tense} is {@link Tense#AUTO}. Pairing this with + * {@code setTense(PAST)} or {@code setTense(FUTURE)} silently makes the threshold inert, + * because the element then commits to relative phrasing regardless of distance. + */ + public RelativeTime setThreshold(Duration threshold) { + setOrRemove(ATTR_THRESHOLD, threshold == null ? null : threshold.toString()); + return this; + } + + /** + * Sets the {@code prefix} attribute (default {@code "on"}), the word prepended to absolute dates + * when the relative-format threshold is crossed. Pass an empty string to drop the prefix + * entirely; {@code null} removes the attribute and restores the default. + */ + public RelativeTime setPrefix(String prefix) { + setOrRemove(ATTR_PREFIX, prefix); + return this; + } + + /** + * When {@code true}, adds the {@code no-title} attribute, suppressing the automatic absolute-date + * tooltip. {@code false} removes it (the element's default behaviour shows the tooltip). + */ + public RelativeTime setNoTitle(boolean noTitle) { + if (noTitle) { + getElement().setAttribute(ATTR_NO_TITLE, true); + } else { + getElement().removeAttribute(ATTR_NO_TITLE); + } + return this; + } + + /** + * Sets the {@code lang} attribute from a {@link Locale} (rendered as a BCP 47 language tag). + * The browser uses {@code Intl.RelativeTimeFormat} / {@code Intl.DateTimeFormat} to localise the + * phrasing. {@code null} removes the attribute; when unset, the upstream walks DOM ancestors + * via {@code closest('[lang]')} and falls back to the document's {@code lang}, so an app-wide + * locale on {@code } is picked up automatically. + */ + public RelativeTime setLocale(Locale locale) { + setOrRemove(ATTR_LANG, locale == null ? null : locale.toLanguageTag()); + return this; + } + + /** + * Sets the {@code time-zone} attribute from a {@link ZoneId} (e.g. {@code America/New_York}). + * When set, the element formats the date in this zone instead of the viewer's. {@code null} + * removes the attribute; when unset, the upstream walks DOM ancestors via {@code + * closest('[time-zone]')} and falls back to the document's {@code time-zone} attribute (and, + * absent that, the browser default). + */ + public RelativeTime setTimeZone(ZoneId timeZone) { + setOrRemove(ATTR_TIME_ZONE, timeZone == null ? null : timeZone.getId()); + return this; + } + + /** + * Sets the {@code time-zone-name} attribute, controlling whether and how the time-zone name is + * appended in absolute-date output. Only relevant when {@link #setFormat format} is + * {@link Format#DATETIME}. {@code null} removes the attribute. + */ + public RelativeTime setTimeZoneName(TimeZoneName value) { + setOrRemove(ATTR_TIME_ZONE_NAME, value == null ? null : value.attributeValue()); + return this; + } + + /** + * Sets the {@code year} attribute. Accepts {@link DateTimePartStyle#NUMERIC} or + * {@link DateTimePartStyle#TWO_DIGIT}. Only relevant when {@link #setFormat format} is + * {@link Format#DATETIME}. {@code null} removes the attribute. + */ + public RelativeTime setYear(DateTimePartStyle style) { + setOrRemove(ATTR_YEAR, style == null ? null : style.attributeValue()); + return this; + } + + /** + * Sets the {@code month} attribute. Accepts {@link DateTimePartStyle#NUMERIC}, + * {@link DateTimePartStyle#TWO_DIGIT}, {@link DateTimePartStyle#NARROW}, + * {@link DateTimePartStyle#SHORT}, or {@link DateTimePartStyle#LONG}. Only relevant when + * {@link #setFormat format} is {@link Format#DATETIME}. {@code null} removes the attribute. + */ + public RelativeTime setMonth(DateTimePartStyle style) { + setOrRemove(ATTR_MONTH, style == null ? null : style.attributeValue()); + return this; + } + + /** + * Sets the {@code day} attribute. Accepts {@link DateTimePartStyle#NUMERIC} or + * {@link DateTimePartStyle#TWO_DIGIT}. Only relevant when {@link #setFormat format} is + * {@link Format#DATETIME}. {@code null} removes the attribute. + */ + public RelativeTime setDay(DateTimePartStyle style) { + setOrRemove(ATTR_DAY, style == null ? null : style.attributeValue()); + return this; + } + + /** + * Sets the {@code weekday} attribute. Accepts {@link DateTimePartStyle#NARROW}, + * {@link DateTimePartStyle#SHORT}, or {@link DateTimePartStyle#LONG}. Only relevant when + * {@link #setFormat format} is {@link Format#DATETIME}. {@code null} removes the attribute. + */ + public RelativeTime setWeekday(DateTimePartStyle style) { + setOrRemove(ATTR_WEEKDAY, style == null ? null : style.attributeValue()); + return this; + } + + /** + * Sets the {@code hour} attribute. Accepts {@link DateTimePartStyle#NUMERIC} or + * {@link DateTimePartStyle#TWO_DIGIT}. Only relevant when {@link #setFormat format} is + * {@link Format#DATETIME}. {@code null} removes the attribute. + */ + public RelativeTime setHour(DateTimePartStyle style) { + setOrRemove(ATTR_HOUR, style == null ? null : style.attributeValue()); + return this; + } + + /** + * Sets the {@code minute} attribute. Accepts {@link DateTimePartStyle#NUMERIC} or + * {@link DateTimePartStyle#TWO_DIGIT}. Only relevant when {@link #setFormat format} is + * {@link Format#DATETIME}. {@code null} removes the attribute. + */ + public RelativeTime setMinute(DateTimePartStyle style) { + setOrRemove(ATTR_MINUTE, style == null ? null : style.attributeValue()); + return this; + } + + /** + * Sets the {@code second} attribute. Accepts {@link DateTimePartStyle#NUMERIC} or + * {@link DateTimePartStyle#TWO_DIGIT}. Only relevant when {@link #setFormat format} is + * {@link Format#DATETIME}. {@code null} removes the attribute. + */ + public RelativeTime setSecond(DateTimePartStyle style) { + setOrRemove(ATTR_SECOND, style == null ? null : style.attributeValue()); + return this; + } + + private void setOrRemove(String attribute, String value) { + if (value == null) { + getElement().removeAttribute(attribute); + } else { + getElement().setAttribute(attribute, value); + } + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemo.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/Tense.java similarity index 53% rename from src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemo.java rename to src/main/java/com/flowingcode/vaadin/addons/relativetime/Tense.java index 294ae97..178e556 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemo.java +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/Tense.java @@ -1,15 +1,15 @@ /*- * #%L - * Template Add-on + * Relative Time 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. @@ -17,20 +17,21 @@ * 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; +package com.flowingcode.vaadin.addons.relativetime; -@DemoSource -@PageTitle("Template Add-on Demo") -@SuppressWarnings("serial") -@Route(value = "demo", layout = TemplateDemoView.class) -public class TemplateDemo extends Div { +import java.util.Locale; - public TemplateDemo() { - add(new TemplateAddon()); +/** Values for the {@code tense} attribute on {@code }. */ +public enum Tense { + /** Past or future is decided from the target instant. */ + AUTO, + /** Force past phrasing ("3 days ago"). */ + PAST, + /** Force future phrasing ("in 3 days"). */ + FUTURE; + + String attributeValue() { + return name().toLowerCase(Locale.ROOT); } } diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/TimeZoneName.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/TimeZoneName.java new file mode 100644 index 0000000..8735619 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/TimeZoneName.java @@ -0,0 +1,50 @@ +/*- + * #%L + * Relative Time 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.relativetime; + +/** + * Values for the {@code time-zone-name} attribute on {@code }. Forwarded to + * {@code Intl.DateTimeFormat} when {@link Format#DATETIME} is in effect. + */ +public enum TimeZoneName { + /** Localised name ("Pacific Daylight Time"). */ + LONG("long"), + /** Localised abbreviation ("PDT"). */ + SHORT("short"), + /** Localised offset ("GMT-8"). */ + SHORT_OFFSET("shortOffset"), + /** Localised offset ("GMT-08:00"). */ + LONG_OFFSET("longOffset"), + /** Generic non-location ("PT"). */ + SHORT_GENERIC("shortGeneric"), + /** Generic non-location ("Pacific Time"). */ + LONG_GENERIC("longGeneric"); + + private final String attributeValue; + + TimeZoneName(String attributeValue) { + this.attributeValue = attributeValue; + } + + String attributeValue() { + return attributeValue; + } +} diff --git a/src/main/resources/META-INF/VAADIN/package.properties b/src/main/resources/META-INF/VAADIN/package.properties index c66616f..4ccb7cd 100644 --- a/src/main/resources/META-INF/VAADIN/package.properties +++ b/src/main/resources/META-INF/VAADIN/package.properties @@ -1 +1,20 @@ +### +# #%L +# Relative Time 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/test/java/com/flowingcode/vaadin/addons/AppShellConfiguratorImpl.java b/src/test/java/com/flowingcode/vaadin/addons/AppShellConfiguratorImpl.java new file mode 100644 index 0000000..2c1ecd6 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/AppShellConfiguratorImpl.java @@ -0,0 +1,35 @@ +/*- + * #%L + * Relative Time 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; + +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 configurePage(AppShellSettings settings) { + if (DynamicTheme.isFeatureSupported()) { + DynamicTheme.LUMO.initialize(settings); + } + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java b/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java index 8996b87..fbec06c 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 + * Relative Time 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/relativetime/AbstractRelativeTimeDemo.java b/src/test/java/com/flowingcode/vaadin/addons/relativetime/AbstractRelativeTimeDemo.java new file mode 100644 index 0000000..edcaf1d --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/relativetime/AbstractRelativeTimeDemo.java @@ -0,0 +1,62 @@ +/*- + * #%L + * Relative Time 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.relativetime; + +import com.flowingcode.vaadin.addons.demo.SourceCodeViewer; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; + +/** + * Shared scaffolding for the RelativeTime demo views. Both {@link RelativeTimeDemo} and + * {@link UseCasesDemo} extend this so they don't duplicate the separator helper and the + * "add a section + wire its fragment highlight" boilerplate. + * + *

Uses a styled {@link Div} (not {@code


}) for the separator because the upstream Vaadin + * {@code
} ships with Lumo-specific CSS that doesn't render under Aura or the bare base + * theme. A {@code
} sidesteps every {@code
}-targeted rule. + */ +@SuppressWarnings("serial") +abstract class AbstractRelativeTimeDemo extends Div { + + /** + * Returns a horizontal-rule-equivalent {@link Div} (class {@code relative-time-demo-separator}) + * that renders the same in Lumo, Aura, and the bare base theme. + */ + protected static Component separator() { + Div div = new Div(); + div.addClassName("relative-time-demo-separator"); + return div; + } + + /** + * Adds {@code sectionContent} to {@code parent}, preceded by a {@link #separator()} when this + * isn't the first section, and wires hover-to-highlight on the source fragment named + * {@code fragmentId}. + */ + protected static void addHighlightedSection(VerticalLayout parent, String fragmentId, + Component sectionContent) { + if (parent.getComponentCount() > 1) { + parent.add(separator()); + } + SourceCodeViewer.highlightOnHover(sectionContent, fragmentId); + parent.add(sectionContent); + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/DemoView.java b/src/test/java/com/flowingcode/vaadin/addons/relativetime/DemoView.java similarity index 85% rename from src/test/java/com/flowingcode/vaadin/addons/template/DemoView.java rename to src/test/java/com/flowingcode/vaadin/addons/relativetime/DemoView.java index 0561979..b8266d9 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/template/DemoView.java +++ b/src/test/java/com/flowingcode/vaadin/addons/relativetime/DemoView.java @@ -1,15 +1,15 @@ /*- * #%L - * Template Add-on + * Relative Time 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. @@ -18,7 +18,7 @@ * #L% */ -package com.flowingcode.vaadin.addons.template; +package com.flowingcode.vaadin.addons.relativetime; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.BeforeEnterEvent; @@ -31,6 +31,6 @@ public class DemoView extends VerticalLayout implements BeforeEnterObserver { @Override public void beforeEnter(BeforeEnterEvent event) { - event.forwardTo(TemplateDemoView.class); + event.forwardTo(RelativeTimeDemoView.class); } } diff --git a/src/test/java/com/flowingcode/vaadin/addons/relativetime/RelativeTimeDemo.java b/src/test/java/com/flowingcode/vaadin/addons/relativetime/RelativeTimeDemo.java new file mode 100644 index 0000000..e3b7edf --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/relativetime/RelativeTimeDemo.java @@ -0,0 +1,315 @@ +/*- + * #%L + * Relative Time 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.relativetime; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.H4; +import com.vaadin.flow.component.html.H5; +import com.vaadin.flow.component.html.Span; +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; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Locale; + +/** + * Feature-by-feature showcase of {@link RelativeTime}. Opens with the most commonly-needed + * features (threshold-based switching, format-style variations, live-update format comparison, + * localisation, prefix), then expands into a Past/Future date matrix that mirrors the upstream + * {@code + * relative-time-element} examples page: two fixed reference dates ({@code 1970-01-01} and the + * {@code 2038-01-19} Y2K38 moment) are rendered through each of the three formats + * ({@link Format#DATETIME}, {@link Format#RELATIVE}, {@link Format#DURATION}) with the same + * attribute variations the upstream demo uses. Hover any section to highlight its source fragment. + * + *

For realistic patterns lifted into Vaadin views (Grid, chat bubble, event card, date-picker + * preview, stopwatch, session-expiry banner), see {@link UseCasesDemo}. + */ +@DemoSource +@PageTitle("Basic Demo") +@SuppressWarnings("serial") +@Route(value = "basic", layout = RelativeTimeDemoView.class) +public class RelativeTimeDemo extends AbstractRelativeTimeDemo { + + private static final Instant PAST = Instant.parse("1970-01-01T00:00:00Z"); + private static final Instant FUTURE = Instant.parse("2038-01-19T03:14:08Z"); + + public RelativeTimeDemo() { + VerticalLayout layout = new VerticalLayout(); + layout.setPadding(false); + layout.setSpacing(true); + + layout.add( + new Span( + "Common features on top; Past/Future format-attribute matrices below." + + " Hover any section to highlight its source.")); + + addSection(layout, "threshold", "Threshold", buildThresholdSection()); + addSection(layout, "format-style", "Format style", buildFormatStyleSection()); + addSection(layout, "live-update", "Live update", buildLiveUpdateSection()); + addSection(layout, "localised", "Localised", buildLocalisedSection()); + addSection(layout, "prefix", "Prefix", buildPrefixSection()); + addSection(layout, "past-date", "Past date: 1970-01-01", buildPastDateSection()); + addSection(layout, "future-date", "Future date: 2038-01-19 (Y2K38)", + buildFutureDateSection()); + + add(layout); + } + + private static void addSection(VerticalLayout parent, String fragmentId, String title, + Component content) { + VerticalLayout section = new VerticalLayout(new H4(title), content); + section.setPadding(false); + section.setSpacing(false); + addHighlightedSection(parent, fragmentId, section); + } + + // --- Threshold ------------------------------------------------------- + + // begin-block threshold + private static Component buildThresholdSection() { + Instant now = Instant.now(); + VerticalLayout section = new VerticalLayout(); + section.setPadding(false); + section.setSpacing(false); + section.add( + new Span( + "Beyond the threshold, the element flips from a relative phrase to an absolute" + + " date.")); + section.add(row("10 days out, P30D threshold (relative)", + new RelativeTime(now.plus(10, ChronoUnit.DAYS)).setThreshold(Duration.ofDays(30)))); + section.add(row("60 days out, P30D threshold (absolute)", + new RelativeTime(now.plus(60, ChronoUnit.DAYS)).setThreshold(Duration.ofDays(30)))); + return section; + } + // end-block + + // --- Format style --------------------------------------------------- + + // begin-block format-style + private static Component buildFormatStyleSection() { + Instant aWeekAgo = Instant.now().minus(7, ChronoUnit.DAYS); + VerticalLayout section = new VerticalLayout(); + section.setPadding(false); + section.setSpacing(false); + section.add( + new Span( + "FormatStyle controls how compact the relative phrase is. Same instant rendered" + + " three ways.")); + section.add(row("LONG / SHORT / NARROW (a week ago)", + new RelativeTime(aWeekAgo).setFormatStyle(FormatStyle.LONG), + new RelativeTime(aWeekAgo).setFormatStyle(FormatStyle.SHORT), + new RelativeTime(aWeekAgo).setFormatStyle(FormatStyle.NARROW))); + return section; + } + // end-block + + // --- Live update ---------------------------------------------------- + + // begin-block live-update + private static Component buildLiveUpdateSection() { + Instant now = Instant.now(); + + RelativeTime auto = new RelativeTime(now); + RelativeTime duration = new RelativeTime(now).setFormat(Format.DURATION); + RelativeTime micro = new RelativeTime(now).setFormat(Format.MICRO); + + Button restart = new Button("Restart", e -> { + Instant fresh = Instant.now(); + auto.setDateTime(fresh); + duration.setDateTime(fresh); + micro.setDateTime(fresh); + }); + + VerticalLayout section = new VerticalLayout(); + section.setPadding(false); + section.setSpacing(false); + section.add( + new Span( + "All three rows below are pinned at the same instant and update by" + + " themselves. Format.AUTO (the default) sits at \"now\" for ~55" + + " seconds before jumping to \"1 minute ago\" because of an upstream" + + " signed-int threshold; DURATION and MICRO tick from zero with no" + + " plateau. Use the Restart button to re-pin.")); + section.add(row("Format.AUTO (default)", auto)); + section.add(row("Format.DURATION", duration)); + section.add(row("Format.MICRO", micro)); + section.add(restart); + return section; + } + // end-block + + // --- Localised ------------------------------------------------------- + + // begin-block localised + private static Component buildLocalisedSection() { + Instant recent = Instant.now().minus(4, ChronoUnit.HOURS); + VerticalLayout section = new VerticalLayout(); + section.setPadding(false); + section.setSpacing(false); + section.add( + new Span( + "The lang attribute drives Intl.RelativeTimeFormat / Intl.DateTimeFormat. setLocale" + + " on the Java side writes it via locale.toLanguageTag().")); + section.add(row("English (default browser locale)", + new RelativeTime(recent))); + section.add(row("Spanish", + new RelativeTime(recent).setLocale(Locale.forLanguageTag("es")))); + section.add(row("Finnish", + new RelativeTime(recent).setLocale(Locale.forLanguageTag("fi")))); + section.add(row("Finnish with Format.DATETIME", + new RelativeTime(recent) + .setFormat(Format.DATETIME) + .setLocale(Locale.forLanguageTag("fi")))); + section.add(row("French with Format.DURATION", + new RelativeTime(recent) + .setFormat(Format.DURATION) + .setLocale(Locale.forLanguageTag("fr")))); + return section; + } + // end-block + + // --- Prefix --------------------------------------------------------- + + // begin-block prefix + private static Component buildPrefixSection() { + Instant farFuture = Instant.now().plus(Duration.ofDays(60)); + VerticalLayout section = new VerticalLayout(); + section.setPadding(false); + section.setSpacing(false); + section.add( + new Span( + "When the threshold is crossed, the element renders an absolute date prefixed" + + " by the word from the prefix attribute (default \"on\"). These rows" + + " pin a date 60 days out, past the default P30D threshold, so the" + + " prefix is visible.")); + section.add(row("Default prefix (\"on\")", + new RelativeTime(farFuture))); + section.add(row("Custom prefix (setPrefix(\"Due\"))", + new RelativeTime(farFuture).setPrefix("Due"))); + section.add(row("No prefix (setPrefix(\"\"))", + new RelativeTime(farFuture).setPrefix(""))); + return section; + } + // end-block + + // --- Past Date ------------------------------------------------------- + + // begin-block past-date + private static Component buildPastDateSection() { + VerticalLayout section = new VerticalLayout(); + section.setPadding(false); + section.setSpacing(false); + + section.add(new H5("Format.DATETIME")); + section.add(row("Default", + new RelativeTime(PAST).setFormat(Format.DATETIME))); + section.add(row("With time", + new RelativeTime(PAST).setFormat(Format.DATETIME) + .setHour(DateTimePartStyle.NUMERIC) + .setMinute(DateTimePartStyle.TWO_DIGIT) + .setSecond(DateTimePartStyle.TWO_DIGIT))); + section.add(row("Customised (all parts)", + new RelativeTime(PAST).setFormat(Format.DATETIME) + .setWeekday(DateTimePartStyle.NARROW) + .setYear(DateTimePartStyle.TWO_DIGIT) + .setMonth(DateTimePartStyle.NARROW) + .setDay(DateTimePartStyle.NUMERIC) + .setHour(DateTimePartStyle.NUMERIC) + .setMinute(DateTimePartStyle.TWO_DIGIT) + .setSecond(DateTimePartStyle.TWO_DIGIT))); + + section.add(new H5("Format.RELATIVE (default)")); + section.add(row("Default", + new RelativeTime(PAST))); + section.add(row("Fixed to future tense", + new RelativeTime(PAST).setTense(Tense.FUTURE))); + + section.add(new H5("Format.DURATION")); + section.add(row("Default", + new RelativeTime(PAST).setFormat(Format.DURATION))); + section.add(row("Month precision", + new RelativeTime(PAST).setFormat(Format.DURATION).setPrecision(Precision.MONTH))); + + return section; + } + // end-block + + // --- Future Date ----------------------------------------------------- + + // begin-block future-date + private static Component buildFutureDateSection() { + VerticalLayout section = new VerticalLayout(); + section.setPadding(false); + section.setSpacing(false); + + section.add(new H5("Format.DATETIME")); + section.add(row("Default", + new RelativeTime(FUTURE).setFormat(Format.DATETIME))); + section.add(row("With time", + new RelativeTime(FUTURE).setFormat(Format.DATETIME) + .setHour(DateTimePartStyle.NUMERIC) + .setMinute(DateTimePartStyle.TWO_DIGIT) + .setSecond(DateTimePartStyle.TWO_DIGIT))); + section.add(row("Customised (all parts)", + new RelativeTime(FUTURE).setFormat(Format.DATETIME) + .setWeekday(DateTimePartStyle.NARROW) + .setYear(DateTimePartStyle.TWO_DIGIT) + .setMonth(DateTimePartStyle.NARROW) + .setDay(DateTimePartStyle.NUMERIC) + .setHour(DateTimePartStyle.NUMERIC) + .setMinute(DateTimePartStyle.TWO_DIGIT) + .setSecond(DateTimePartStyle.TWO_DIGIT))); + + section.add(new H5("Format.RELATIVE (default)")); + section.add(row("Default", + new RelativeTime(FUTURE))); + section.add(row("Fixed to past tense", + new RelativeTime(FUTURE).setTense(Tense.PAST))); + + section.add(new H5("Format.DURATION")); + section.add(row("Default", + new RelativeTime(FUTURE).setFormat(Format.DURATION))); + section.add(row("Month precision", + new RelativeTime(FUTURE).setFormat(Format.DURATION).setPrecision(Precision.MONTH))); + + return section; + } + // end-block + + // --- shared helper --------------------------------------------------- + + private static HorizontalLayout row(String label, Component... values) { + HorizontalLayout row = new HorizontalLayout(); + row.setSpacing(true); + row.setPadding(false); + row.addClassName("relative-time-demo-row"); + Span labelSpan = new Span(label + ":"); + labelSpan.addClassName("relative-time-demo-row-label"); + row.add(labelSpan); + row.add(values); + return row; + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemoView.java b/src/test/java/com/flowingcode/vaadin/addons/relativetime/RelativeTimeDemoView.java similarity index 65% rename from src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemoView.java rename to src/test/java/com/flowingcode/vaadin/addons/relativetime/RelativeTimeDemoView.java index a17133f..24f4a8f 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemoView.java +++ b/src/test/java/com/flowingcode/vaadin/addons/relativetime/RelativeTimeDemoView.java @@ -1,15 +1,15 @@ /*- * #%L - * Template Add-on + * Relative Time 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. @@ -17,22 +17,25 @@ * limitations under the License. * #L% */ -package com.flowingcode.vaadin.addons.template; +package com.flowingcode.vaadin.addons.relativetime; import com.flowingcode.vaadin.addons.DemoLayout; import com.flowingcode.vaadin.addons.GithubLink; import com.flowingcode.vaadin.addons.demo.TabbedDemo; +import com.vaadin.flow.component.dependency.CssImport; import com.vaadin.flow.router.ParentLayout; import com.vaadin.flow.router.Route; @SuppressWarnings("serial") @ParentLayout(DemoLayout.class) -@Route("template") -@GithubLink("https://github.com/FlowingCode/AddonStarter24") -public class TemplateDemoView extends TabbedDemo { +@Route("relative-time") +@CssImport("./styles/relative-time-add-on-demo-styles.css") +@GithubLink("https://github.com/FlowingCode/RelativeTime") +public class RelativeTimeDemoView extends TabbedDemo { - public TemplateDemoView() { - addDemo(TemplateDemo.class); + public RelativeTimeDemoView() { + addDemo(RelativeTimeDemo.class); + addDemo(UseCasesDemo.class); setSizeFull(); } } diff --git a/src/test/java/com/flowingcode/vaadin/addons/relativetime/UseCasesDemo.java b/src/test/java/com/flowingcode/vaadin/addons/relativetime/UseCasesDemo.java new file mode 100644 index 0000000..6f93137 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/relativetime/UseCasesDemo.java @@ -0,0 +1,318 @@ +/*- + * #%L + * Relative Time 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.relativetime; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.datetimepicker.DateTimePicker; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment; +import com.vaadin.flow.component.orderedlayout.FlexComponent.JustifyContentMode; +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; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Locale; + +/** + * Realistic UI patterns showing how {@link RelativeTime} fits into common Vaadin views: + * audit-log grids, chat timestamps, event cards, a date-picker preview, a live stopwatch, and + * a session-expiry banner. Each section is structured so it can be lifted into a real app with + * minimal changes. Hover over any example to highlight the corresponding source fragment. + */ +@DemoSource +@PageTitle("Use Cases") +@SuppressWarnings("serial") +@Route(value = "use-cases", layout = RelativeTimeDemoView.class) +public class UseCasesDemo extends AbstractRelativeTimeDemo { + + public UseCasesDemo() { + VerticalLayout layout = new VerticalLayout(); + layout.setPadding(false); + layout.setSpacing(true); + + layout.add( + new Span( + "Realistic UI patterns where RelativeTime fits naturally. Each timestamp re-renders" + + " in the browser as time passes. Each example uses the format best suited to its" + + " pattern: AUTO for human-friendly feeds, DURATION for stopwatches," + + " future-tense with a threshold for countdowns, and MICRO for tight spaces." + + " Hover over any example to highlight the corresponding source fragment on" + + " the right.")); + + addUseCase(layout, "audit-log-grid", + "Audit log Grid", buildAuditLogGrid(), + "Vaadin Grid with a RelativeTime in a component column. Drops in alongside regular" + + " columns and scrolls smoothly. Each row's timestamp is a tiny custom element" + + " that handles its own updates, so the Grid stays cheap to render even with" + + " thousands of rows. Pattern: audit logs, change history, activity tables in" + + " admin UIs."); + + addUseCase(layout, "chat-messages", + "Chat messages", buildChatMessages(), + "Bubble-style messages with the timestamp tucked inside each bubble, formatted as" + + " Format.MICRO (e.g. \"3m\", \"45s\"). Pattern: any chat, DM, or comment thread" + + " where you want a glance-able timestamp embedded in the message without" + + " consuming a whole column."); + + addUseCase(layout, "event-card", + "Upcoming event card", buildEventCard(), + "Future-tense relative time inside a card. The element's auto-tense gives \"in 2 hours\"" + + " for a future instant. Pattern: calendar widgets, meeting cards, reservation" + + " summaries."); + + addUseCase(layout, "date-time-picker", + "Date picker preview", buildDateTimePickerSection(), + "DateTimePicker driving a RelativeTime preview through a value-change listener; a" + + " locale combo drives setLocale on the same preview. A 30-day threshold flips the" + + " preview to an absolute date for far-away picks. Pattern: any form field with a" + + " date input where you want an \"in X days\" preview next to the input."); + + addUseCase(layout, "stopwatch", + "Live stopwatch", buildStopwatch(), + "A running counter using Format.DURATION (ticks every second from 0s with no \"now\"" + + " plateau). The Start button pins the datetime to now; Stop clears it. Pattern:" + + " timers for in-progress work, build/deploy status, \"uptime since\" indicators."); + + addUseCase(layout, "session-expiry", + "Session expiry warning", buildSessionWarning(), + "Banner with a future-tense countdown. The \"Stay logged in\" button re-pins the" + + " datetime, restarting the countdown. Pattern: any time-limited UI such as" + + " session expiry, OTP or code expiry, auction or sale countdowns."); + + add(layout); + } + + private static void addUseCase(VerticalLayout parent, String fragmentId, String title, + Component content, String explanation) { + Component block = useCase(title, content, explanation); + addHighlightedSection(parent, fragmentId, block); + } + + // --- Audit log Grid ---------------------------------------------------- + + private record AuditLog(String user, String action, Instant when) {} + + // begin-block audit-log-grid + private static Component buildAuditLogGrid() { + Instant now = Instant.now(); + List rows = + List.of( + new AuditLog("alice", "Created project \"Atlas\"", now.minus(Duration.ofMinutes(2))), + new AuditLog("bob", "Updated permissions on /repos", now.minus(Duration.ofMinutes(8))), + new AuditLog("carol", "Deleted user 'temp123'", now.minus(Duration.ofMinutes(35))), + new AuditLog("dave", "Rotated API key", now.minus(Duration.ofHours(2))), + new AuditLog("alice", "Approved PR #142", now.minus(Duration.ofHours(5))), + new AuditLog("eve", "Logged in from new IP", now.minus(Duration.ofDays(1))), + new AuditLog("bob", "Changed billing email", now.minus(Duration.ofDays(3))), + new AuditLog("carol", "Exported user database", now.minus(Duration.ofDays(7)))); + + Grid grid = new Grid<>(AuditLog.class, false); + grid.setItems(rows); + grid.addColumn(AuditLog::user).setHeader("User").setAutoWidth(true); + grid.addColumn(AuditLog::action).setHeader("Action").setFlexGrow(1); + grid.addComponentColumn(log -> new RelativeTime(log.when())) + .setHeader("When") + .setAutoWidth(true); + grid.setAllRowsVisible(true); + return grid; + } + // end-block + + // --- Chat messages ----------------------------------------------------- + + // begin-block chat-messages + private static Component buildChatMessages() { + Instant now = Instant.now(); + VerticalLayout chat = + new VerticalLayout( + chatBubble("Hi! Got a minute?", now.minus(Duration.ofMinutes(3)), true), + chatBubble("Sure, what's up?", now.minus(Duration.ofMinutes(2)), false), + chatBubble("Want to grab lunch?", now.minus(Duration.ofSeconds(45)), true)); + chat.setPadding(false); + chat.setSpacing(true); + chat.addClassName("relative-time-demo-chat"); + return chat; + } + + private static HorizontalLayout chatBubble(String text, Instant when, boolean sent) { + Span body = new Span(text); + + RelativeTime stamp = new RelativeTime(when).setFormat(Format.MICRO); + stamp.addClassName("relative-time-demo-chat-bubble-stamp"); + + VerticalLayout bubble = new VerticalLayout(body, stamp); + bubble.setPadding(false); + bubble.setSpacing(false); + bubble.addClassName("relative-time-demo-chat-bubble"); + bubble.addClassName(sent ? "relative-time-demo-chat-bubble-sent" : "relative-time-demo-chat-bubble-received"); + + HorizontalLayout row = new HorizontalLayout(bubble); + row.setWidthFull(); + row.setJustifyContentMode(sent ? JustifyContentMode.END : JustifyContentMode.START); + return row; + } + // end-block + + // --- Date picker preview ---------------------------------------------- + + // begin-block date-time-picker + private static Component buildDateTimePickerSection() { + DateTimePicker picker = new DateTimePicker("Pick a deadline"); + + ComboBox localeCombo = new ComboBox<>("Locale"); + localeCombo.setItems( + List.of( + Locale.ENGLISH, + Locale.forLanguageTag("es"), + Locale.forLanguageTag("it"), + Locale.forLanguageTag("fr"))); + localeCombo.setItemLabelGenerator(loc -> loc.getDisplayName(Locale.ENGLISH)); + + RelativeTime preview = + new RelativeTime() + .setTense(Tense.AUTO) + .setFormatStyle(FormatStyle.LONG) + .setThreshold(Duration.ofDays(30)); + + picker.addValueChangeListener(e -> preview.setDateTime(e.getValue())); + localeCombo.addValueChangeListener(e -> preview.setLocale(e.getValue())); + + // Initial state: 5 days from now, English. Within the 30-day threshold, + // so the preview starts as a relative phrase. + picker.setValue(LocalDateTime.now().plusDays(5)); + localeCombo.setValue(Locale.ENGLISH); + + HorizontalLayout controls = new HorizontalLayout(picker, localeCombo); + controls.setAlignItems(Alignment.END); + + Div previewRow = new Div(new Span("Deadline: "), preview); + + VerticalLayout section = new VerticalLayout(controls, previewRow); + section.setPadding(false); + section.setSpacing(true); + return section; + } + // end-block + + // --- Upcoming event card ---------------------------------------------- + + // begin-block event-card + private static Component buildEventCard() { + Instant inTwoHours = Instant.now().plus(Duration.ofHours(2)); + + Span title = new Span("Daily Standup"); + title.addClassName("relative-time-demo-event-card-title"); + + Div timeLine = new Div(new Span("starts "), new RelativeTime(inTwoHours)); + + Span location = new Span("Conference room A"); + location.addClassName("relative-time-demo-event-card-location"); + + VerticalLayout card = new VerticalLayout(title, timeLine, location); + card.setPadding(true); + card.setSpacing(false); + card.addClassName("relative-time-demo-event-card"); + return card; + } + // end-block + + // --- Live stopwatch ---------------------------------------------------- + + // begin-block stopwatch + private static Component buildStopwatch() { + RelativeTime elapsed = new RelativeTime().setFormat(Format.DURATION); + Span placeholder = new Span("..."); + placeholder.addClassName("relative-time-demo-stopwatch-placeholder"); + + Div display = new Div(new Span("Elapsed: "), placeholder, elapsed); + + Button startStop = new Button("Start"); + startStop.addClickListener( + e -> { + if (elapsed.getDateTime() != null) { + elapsed.setDateTime((Instant) null); + placeholder.setVisible(true); + startStop.setText("Start"); + } else { + elapsed.setDateTime(Instant.now()); + placeholder.setVisible(false); + startStop.setText("Stop"); + } + }); + + VerticalLayout layout = new VerticalLayout(display, startStop); + layout.setPadding(false); + layout.setSpacing(true); + return layout; + } + // end-block + + // --- Session expiry warning ------------------------------------------- + + // begin-block session-expiry + private static Component buildSessionWarning() { + RelativeTime expiry = new RelativeTime(Instant.now().plus(Duration.ofMinutes(14))); + + Span icon = new Span("⚠️"); + icon.addClassName("relative-time-demo-session-banner-icon"); + + Div textRow = new Div(icon, new Span("Your session expires "), expiry, new Span(".")); + + Button stayLoggedIn = new Button("Stay logged in"); + stayLoggedIn.addClickListener( + e -> expiry.setDateTime(Instant.now().plus(Duration.ofMinutes(14)))); + + HorizontalLayout banner = new HorizontalLayout(textRow, stayLoggedIn); + banner.setWidthFull(); + banner.setJustifyContentMode(JustifyContentMode.BETWEEN); + banner.setAlignItems(Alignment.CENTER); + banner.addClassName("relative-time-demo-session-banner"); + return banner; + } + // end-block + + // --- shared helper ----------------------------------------------------- + + private static Component useCase(String title, Component content, String explanation) { + VerticalLayout block = new VerticalLayout(); + block.setPadding(false); + block.setSpacing(false); + + Span titleSpan = new Span(title); + titleSpan.addClassName("relative-time-demo-use-case-title"); + + content.getElement().getClassList().add("relative-time-demo-use-case-content"); + + Span explanationSpan = new Span(explanation); + explanationSpan.addClassName("relative-time-demo-use-case-explanation"); + + block.add(titleSpan, content, explanationSpan); + return block; + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/it/AbstractViewTest.java b/src/test/java/com/flowingcode/vaadin/addons/relativetime/it/AbstractViewTest.java similarity index 96% rename from src/test/java/com/flowingcode/vaadin/addons/template/it/AbstractViewTest.java rename to src/test/java/com/flowingcode/vaadin/addons/relativetime/it/AbstractViewTest.java index a2579ad..43b5916 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/template/it/AbstractViewTest.java +++ b/src/test/java/com/flowingcode/vaadin/addons/relativetime/it/AbstractViewTest.java @@ -1,15 +1,15 @@ /*- * #%L - * Template Add-on + * Relative Time 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. @@ -18,7 +18,7 @@ * #L% */ -package com.flowingcode.vaadin.addons.template.it; +package com.flowingcode.vaadin.addons.relativetime.it; import com.vaadin.testbench.ScreenshotOnFailureRule; import com.vaadin.testbench.TestBench; diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/it/ViewIT.java b/src/test/java/com/flowingcode/vaadin/addons/relativetime/it/ViewIT.java similarity index 69% rename from src/test/java/com/flowingcode/vaadin/addons/template/it/ViewIT.java rename to src/test/java/com/flowingcode/vaadin/addons/relativetime/it/ViewIT.java index 628dc14..91dd982 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/template/it/ViewIT.java +++ b/src/test/java/com/flowingcode/vaadin/addons/relativetime/it/ViewIT.java @@ -1,15 +1,15 @@ /*- * #%L - * Template Add-on + * Relative Time 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. @@ -18,11 +18,11 @@ * #L% */ -package com.flowingcode.vaadin.addons.template.it; +package com.flowingcode.vaadin.addons.relativetime.it; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; -import static org.junit.Assert.assertThat; +import static org.hamcrest.MatcherAssert.assertThat; import com.vaadin.testbench.TestBenchElement; import org.hamcrest.Description; @@ -42,7 +42,13 @@ public void describeTo(Description description) { @Override protected boolean matchesSafely(TestBenchElement item, Description mismatchDescription) { - String script = "let s=arguments[0].shadowRoot; return !!(s&&s.childElementCount)"; + // An element is considered upgraded if it has a shadow root AND the shadow root has + // either an element child OR non-empty text content. relative-time writes its rendered + // phrase via `shadowRoot.textContent =`, which creates a text node (not an element + // child), so checking only childElementCount fails for this kind of component. + String script = + "let s=arguments[0].shadowRoot;" + + " return !!(s && (s.childElementCount || s.textContent.trim()))"; if (!item.getTagName().contains("-")) { return true; } @@ -58,7 +64,7 @@ protected boolean matchesSafely(TestBenchElement item, Description mismatchDescr @Test public void componentWorks() { - TestBenchElement element = $("paper-input").first(); + TestBenchElement element = $("relative-time").first(); assertThat(element, hasBeenUpgradedToCustomElement); } } diff --git a/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java b/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java new file mode 100644 index 0000000..c66e42f --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java @@ -0,0 +1,285 @@ +/*- + * #%L + * Relative Time 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.relativetime.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.flowingcode.vaadin.addons.relativetime.DateTimePartStyle; +import com.flowingcode.vaadin.addons.relativetime.Format; +import com.flowingcode.vaadin.addons.relativetime.FormatStyle; +import com.flowingcode.vaadin.addons.relativetime.Precision; +import com.flowingcode.vaadin.addons.relativetime.RelativeTime; +import com.flowingcode.vaadin.addons.relativetime.Tense; +import com.flowingcode.vaadin.addons.relativetime.TimeZoneName; +import com.vaadin.flow.dom.Element; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Locale; +import org.junit.Test; + +public class RelativeTimeTest { + + private static final Instant FIXED = Instant.parse("2026-01-15T12:34:56Z"); + + // --- datetime overloads ------------------------------------------------- + + @Test + public void setDateTime_instant_writesAttributeAndUpdatesGetter() { + RelativeTime rt = new RelativeTime(); + rt.setDateTime(FIXED); + assertEquals(FIXED.toString(), attr(rt, "datetime")); + assertEquals(FIXED, rt.getDateTime()); + } + + @Test + public void setDateTime_null_clearsAttributeAndGetter() { + RelativeTime rt = new RelativeTime(FIXED); + rt.setDateTime((Instant) null); + assertNull(attr(rt, "datetime")); + assertNull(rt.getDateTime()); + } + + @Test + public void setDateTime_offsetDateTime_normalisesToInstant() { + OffsetDateTime odt = OffsetDateTime.of(2026, 1, 15, 14, 34, 56, 0, ZoneOffset.ofHours(2)); + RelativeTime rt = new RelativeTime(); + rt.setDateTime(odt); + assertEquals(odt.toInstant(), rt.getDateTime()); + assertEquals(odt.toInstant().toString(), attr(rt, "datetime")); + } + + @Test + public void setDateTime_zonedDateTime_normalisesToInstant() { + ZonedDateTime zdt = ZonedDateTime.of(2026, 1, 15, 13, 34, 56, 0, ZoneId.of("Europe/Madrid")); + RelativeTime rt = new RelativeTime(); + rt.setDateTime(zdt); + assertEquals(zdt.toInstant(), rt.getDateTime()); + } + + @Test + public void setDateTime_localDateTime_usesSystemDefaultZone() { + LocalDateTime ldt = LocalDateTime.of(2026, 1, 15, 12, 34, 56); + Instant expected = ldt.atZone(ZoneId.systemDefault()).toInstant(); + RelativeTime rt = new RelativeTime(); + rt.setDateTime(ldt); + assertEquals(expected, rt.getDateTime()); + } + + @Test + public void setDateTime_localDate_usesStartOfDayInSystemDefaultZone() { + LocalDate date = LocalDate.of(2026, 1, 15); + Instant expected = date.atStartOfDay(ZoneId.systemDefault()).toInstant(); + RelativeTime rt = new RelativeTime(); + rt.setDateTime(date); + assertEquals(expected, rt.getDateTime()); + } + + // --- enum setters ------------------------------------------------------- + + @Test + public void setTense_writesLowercaseName() { + RelativeTime rt = new RelativeTime(); + rt.setTense(Tense.FUTURE); + assertEquals("future", attr(rt, "tense")); + rt.setTense(null); + assertNull(attr(rt, "tense")); + } + + @Test + public void setFormat_writesLowercaseName() { + RelativeTime rt = new RelativeTime(); + rt.setFormat(Format.DURATION); + assertEquals("duration", attr(rt, "format")); + rt.setFormat(null); + assertNull(attr(rt, "format")); + } + + @Test + public void setPrecision_writesLowercaseName() { + RelativeTime rt = new RelativeTime(); + rt.setPrecision(Precision.DAY); + assertEquals("day", attr(rt, "precision")); + rt.setPrecision(null); + assertNull(attr(rt, "precision")); + } + + @Test + public void setFormatStyle_writesKebabAttribute() { + RelativeTime rt = new RelativeTime(); + rt.setFormatStyle(FormatStyle.NARROW); + assertEquals("narrow", attr(rt, "format-style")); + rt.setFormatStyle(null); + assertNull(attr(rt, "format-style")); + } + + // --- duration, prefix, no-title, locale -------------------------------- + + @Test + public void setThreshold_writesIso8601Duration() { + RelativeTime rt = new RelativeTime(); + rt.setThreshold(Duration.ofDays(30)); + assertEquals("PT720H", attr(rt, "threshold")); + rt.setThreshold(null); + assertNull(attr(rt, "threshold")); + } + + @Test + public void setPrefix_emptyStringSetsEmpty_nullClears() { + RelativeTime rt = new RelativeTime(); + rt.setPrefix(""); + assertEquals("", attr(rt, "prefix")); + rt.setPrefix(null); + assertNull(attr(rt, "prefix")); + } + + @Test + public void setNoTitle_trueSets_falseRemoves() { + RelativeTime rt = new RelativeTime(); + rt.setNoTitle(true); + assertTrue(rt.getElement().hasAttribute("no-title")); + rt.setNoTitle(false); + assertFalse(rt.getElement().hasAttribute("no-title")); + } + + @Test + public void setLocale_writesLanguageTagAsLang() { + RelativeTime rt = new RelativeTime(); + rt.setLocale(Locale.forLanguageTag("es-ES")); + assertEquals("es-ES", attr(rt, "lang")); + rt.setLocale(null); + assertNull(attr(rt, "lang")); + } + + // --- time-zone, time-zone-name, date parts ----------------------------- + + @Test + public void setTimeZone_writesZoneIdAsTimeZone() { + RelativeTime rt = new RelativeTime(); + rt.setTimeZone(ZoneId.of("America/New_York")); + assertEquals("America/New_York", attr(rt, "time-zone")); + rt.setTimeZone(null); + assertNull(attr(rt, "time-zone")); + } + + @Test + public void setTimeZoneName_writesCamelCaseValue() { + RelativeTime rt = new RelativeTime(); + rt.setTimeZoneName(TimeZoneName.SHORT_OFFSET); + assertEquals("shortOffset", attr(rt, "time-zone-name")); + rt.setTimeZoneName(TimeZoneName.LONG); + assertEquals("long", attr(rt, "time-zone-name")); + rt.setTimeZoneName(null); + assertNull(attr(rt, "time-zone-name")); + } + + @Test + public void datePartSetters_writeAttribute_andTwoDigitSerialisesWithHyphen() { + RelativeTime rt = new RelativeTime(); + + rt.setYear(DateTimePartStyle.NUMERIC); + assertEquals("numeric", attr(rt, "year")); + + rt.setMonth(DateTimePartStyle.LONG); + assertEquals("long", attr(rt, "month")); + + rt.setDay(DateTimePartStyle.TWO_DIGIT); + assertEquals("2-digit", attr(rt, "day")); + + rt.setWeekday(DateTimePartStyle.NARROW); + assertEquals("narrow", attr(rt, "weekday")); + + rt.setHour(DateTimePartStyle.TWO_DIGIT); + assertEquals("2-digit", attr(rt, "hour")); + + rt.setMinute(DateTimePartStyle.NUMERIC); + assertEquals("numeric", attr(rt, "minute")); + + rt.setSecond(DateTimePartStyle.TWO_DIGIT); + assertEquals("2-digit", attr(rt, "second")); + } + + @Test + public void datePartSetters_nullRemovesAttribute() { + RelativeTime rt = + new RelativeTime() + .setYear(DateTimePartStyle.NUMERIC) + .setMonth(DateTimePartStyle.LONG) + .setDay(DateTimePartStyle.TWO_DIGIT) + .setWeekday(DateTimePartStyle.SHORT) + .setHour(DateTimePartStyle.NUMERIC) + .setMinute(DateTimePartStyle.NUMERIC) + .setSecond(DateTimePartStyle.NUMERIC); + + rt.setYear(null); + rt.setMonth(null); + rt.setDay(null); + rt.setWeekday(null); + rt.setHour(null); + rt.setMinute(null); + rt.setSecond(null); + + assertNull(attr(rt, "year")); + assertNull(attr(rt, "month")); + assertNull(attr(rt, "day")); + assertNull(attr(rt, "weekday")); + assertNull(attr(rt, "hour")); + assertNull(attr(rt, "minute")); + assertNull(attr(rt, "second")); + } + + // --- fluent chain returns this ----------------------------------------- + + @Test + public void settersReturnSameInstance() { + RelativeTime rt = new RelativeTime(); + assertEquals(rt, rt.setDateTime(FIXED)); + assertEquals(rt, rt.setTense(Tense.AUTO)); + assertEquals(rt, rt.setFormat(Format.RELATIVE)); + assertEquals(rt, rt.setPrecision(Precision.HOUR)); + assertEquals(rt, rt.setFormatStyle(FormatStyle.LONG)); + assertEquals(rt, rt.setThreshold(Duration.ofDays(1))); + assertEquals(rt, rt.setPrefix("on")); + assertEquals(rt, rt.setNoTitle(true)); + assertEquals(rt, rt.setLocale(Locale.ENGLISH)); + assertEquals(rt, rt.setTimeZone(ZoneId.of("UTC"))); + assertEquals(rt, rt.setTimeZoneName(TimeZoneName.SHORT)); + assertEquals(rt, rt.setYear(DateTimePartStyle.NUMERIC)); + assertEquals(rt, rt.setMonth(DateTimePartStyle.LONG)); + assertEquals(rt, rt.setDay(DateTimePartStyle.TWO_DIGIT)); + assertEquals(rt, rt.setWeekday(DateTimePartStyle.SHORT)); + assertEquals(rt, rt.setHour(DateTimePartStyle.NUMERIC)); + assertEquals(rt, rt.setMinute(DateTimePartStyle.NUMERIC)); + assertEquals(rt, rt.setSecond(DateTimePartStyle.NUMERIC)); + } + + private static String attr(RelativeTime rt, String name) { + Element el = rt.getElement(); + return el.hasAttribute(name) ? el.getAttribute(name) : null; + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/SerializationTest.java b/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/SerializationTest.java new file mode 100644 index 0000000..cdce46f --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/SerializationTest.java @@ -0,0 +1,90 @@ +/*- + * #%L + * Relative Time 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.relativetime.test; + +import com.flowingcode.vaadin.addons.relativetime.DateTimePartStyle; +import com.flowingcode.vaadin.addons.relativetime.Format; +import com.flowingcode.vaadin.addons.relativetime.FormatStyle; +import com.flowingcode.vaadin.addons.relativetime.Precision; +import com.flowingcode.vaadin.addons.relativetime.RelativeTime; +import com.flowingcode.vaadin.addons.relativetime.Tense; +import com.flowingcode.vaadin.addons.relativetime.TimeZoneName; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Locale; +import org.junit.Assert; +import org.junit.Test; + +public class SerializationTest { + + private void testSerializationOf(Object obj) throws IOException, ClassNotFoundException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(obj); + } + try (ObjectInputStream in = + new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { + obj.getClass().cast(in.readObject()); + } + } + + @Test + public void testSerialization() throws ClassNotFoundException, IOException { + try { + testSerializationOf(new RelativeTime()); + } catch (Exception e) { + Assert.fail("Problem while testing serialization: " + e.getMessage()); + } + } + + @Test + public void testSerializationOfConfiguredInstance() throws ClassNotFoundException, IOException { + try { + RelativeTime configured = + new RelativeTime(Instant.parse("2026-01-01T00:00:00Z")) + .setTense(Tense.FUTURE) + .setFormat(Format.RELATIVE) + .setPrecision(Precision.DAY) + .setFormatStyle(FormatStyle.LONG) + .setThreshold(Duration.ofDays(30)) + .setPrefix("") + .setNoTitle(true) + .setLocale(Locale.forLanguageTag("es-ES")) + .setTimeZone(ZoneId.of("America/New_York")) + .setTimeZoneName(TimeZoneName.SHORT_OFFSET) + .setYear(DateTimePartStyle.NUMERIC) + .setMonth(DateTimePartStyle.LONG) + .setDay(DateTimePartStyle.TWO_DIGIT) + .setWeekday(DateTimePartStyle.SHORT) + .setHour(DateTimePartStyle.TWO_DIGIT) + .setMinute(DateTimePartStyle.TWO_DIGIT) + .setSecond(DateTimePartStyle.TWO_DIGIT); + testSerializationOf(configured); + } catch (Exception e) { + Assert.fail("Problem while testing serialization: " + e.getMessage()); + } + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/test/SerializationTest.java b/src/test/java/com/flowingcode/vaadin/addons/template/test/SerializationTest.java deleted file mode 100644 index 1f8ceed..0000000 --- a/src/test/java/com/flowingcode/vaadin/addons/template/test/SerializationTest.java +++ /dev/null @@ -1,52 +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.test; - -import com.flowingcode.vaadin.addons.template.TemplateAddon; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import org.junit.Assert; -import org.junit.Test; - -public class SerializationTest { - - private void testSerializationOf(Object obj) throws IOException, ClassNotFoundException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { - oos.writeObject(obj); - } - try (ObjectInputStream in = - new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { - obj.getClass().cast(in.readObject()); - } - } - - @Test - public void testSerialization() throws ClassNotFoundException, IOException { - try { - testSerializationOf(new TemplateAddon()); - } catch (Exception e) { - Assert.fail("Problem while testing serialization: " + e.getMessage()); - } - } -} diff --git a/src/test/resources/META-INF/frontend/styles/relative-time-add-on-demo-styles.css b/src/test/resources/META-INF/frontend/styles/relative-time-add-on-demo-styles.css new file mode 100644 index 0000000..e22db2b --- /dev/null +++ b/src/test/resources/META-INF/frontend/styles/relative-time-add-on-demo-styles.css @@ -0,0 +1,134 @@ +/*- + * #%L + * Relative Time 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. + Every property uses a Lumo -> Vaadin base -> literal fallback chain so the + demos look correct under Lumo, Aura, and the bare Vaadin base theme. */ + +/* Row helper (used in RelativeTimeDemo basic-demo rows) */ +.relative-time-demo-row { + padding: var(--lumo-space-xs, var(--vaadin-padding-xs, 0.25rem)) 0; +} + +.relative-time-demo-row-label { + min-width: 22em; + color: var(--lumo-secondary-text-color, + var(--vaadin-text-color-secondary, #6b7280)); +} + +/* Shared separator (works in Lumo, Aura, base) */ +/* Rendered as a

(not
) so theme-specific hr styling can't hide it. + align-self/flex-shrink are needed because a VerticalLayout is a flex column; + without them a 1px row gets squashed to zero or rendered at content-only + width. */ +.relative-time-demo-separator { + display: block; + align-self: stretch; + flex: 0 0 1px; + height: 1px; + background-color: var(--lumo-contrast-20pct, + var(--vaadin-border-color-secondary, rgba(0, 0, 0, 0.2))); + margin: var(--lumo-space-s, var(--vaadin-padding-s, 0.5rem)) 0; +} + +/* Use-case block helper */ +.relative-time-demo-use-case-title { + font-weight: bold; + font-size: 1.05em; +} + +.relative-time-demo-use-case-content { + margin-top: var(--lumo-space-s, var(--vaadin-padding-s, 0.5rem)); +} + +.relative-time-demo-use-case-explanation { + font-size: 0.9em; + color: var(--lumo-secondary-text-color, + var(--vaadin-text-color-secondary, #6b7280)); + margin-top: var(--lumo-space-s, var(--vaadin-padding-s, 0.5rem)); +} + +/* Chat messages */ +.relative-time-demo-chat { + max-width: 32em; +} + +.relative-time-demo-chat-bubble { + padding: var(--lumo-space-s, var(--vaadin-padding-s, 0.5rem)) + var(--lumo-space-m, var(--vaadin-padding-m, 1rem)); + border-radius: var(--lumo-border-radius-l, var(--vaadin-radius-l, 0.75rem)); + max-width: 75%; +} + +.relative-time-demo-chat-bubble-sent { + background: var(--lumo-primary-color, #1676f3); + color: var(--lumo-primary-contrast-color, #fff); +} + +.relative-time-demo-chat-bubble-received { + background: var(--lumo-contrast-10pct, + var(--vaadin-background-container, rgba(0, 0, 0, 0.06))); + color: var(--lumo-body-text-color, var(--vaadin-text-color, inherit)); +} + +.relative-time-demo-chat-bubble-stamp { + font-size: 0.75em; + display: block; + text-align: right; + margin-top: var(--lumo-space-xs, var(--vaadin-padding-xs, 0.25rem)); + opacity: 0.75; +} + +/* Event card */ +.relative-time-demo-event-card { + border: 1px solid var(--lumo-contrast-20pct, + var(--vaadin-border-color-secondary, rgba(0, 0, 0, 0.12))); + border-radius: var(--lumo-border-radius-m, var(--vaadin-radius-m, 0.25rem)); + max-width: 20em; +} + +.relative-time-demo-event-card-title { + font-weight: bold; + font-size: 1.1em; +} + +.relative-time-demo-event-card-location { + color: var(--lumo-secondary-text-color, + var(--vaadin-text-color-secondary, #6b7280)); +} + +/* Stopwatch */ +.relative-time-demo-stopwatch-placeholder { + color: var(--lumo-secondary-text-color, + var(--vaadin-text-color-secondary, #6b7280)); +} + +/* Session expiry banner */ +.relative-time-demo-session-banner { + background: var(--lumo-error-color-10pct, rgba(220, 38, 38, 0.08)); + border: 1px solid var(--lumo-error-color-50pct, rgba(220, 38, 38, 0.4)); + border-radius: var(--lumo-border-radius-m, var(--vaadin-radius-m, 0.25rem)); + padding: var(--lumo-space-s, var(--vaadin-padding-s, 0.5rem)); + max-width: 32em; +} + +.relative-time-demo-session-banner-icon { + margin-right: var(--lumo-space-s, var(--vaadin-padding-s, 0.5rem)); +} diff --git a/src/test/resources/META-INF/frontend/styles/shared-styles.css b/src/test/resources/META-INF/frontend/styles/shared-styles.css deleted file mode 100644 index 6680e2d..0000000 --- a/src/test/resources/META-INF/frontend/styles/shared-styles.css +++ /dev/null @@ -1 +0,0 @@ -/*Demo styles*/ \ No newline at end of file From 9f10a764d6f4e3906133105739962e6899a43166 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Wed, 3 Jun 2026 15:27:08 -0300 Subject: [PATCH 02/11] WIP: fix: fix finding 1 --- .../addons/relativetime/RelativeTime.java | 45 +++++++++++++------ .../relativetime/test/RelativeTimeTest.java | 24 ++++++++++ 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java index 4ecd1e1..06f2904 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java @@ -32,7 +32,9 @@ import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.util.EnumSet; import java.util.Locale; +import java.util.Set; /** * Renders a date/time as a human-readable relative string ("4 hours ago", "in 2 weeks") that @@ -80,6 +82,16 @@ public class RelativeTime extends Component implements HasSize { private static final String ATTR_MINUTE = "minute"; private static final String ATTR_SECOND = "second"; + /** Styles accepted by the numeric date-part attributes (year, day, hour, minute, second). */ + private static final Set NUMERIC_STYLES = + Set.of(DateTimePartStyle.NUMERIC, DateTimePartStyle.TWO_DIGIT); + /** Styles accepted by the textual date-part attribute (weekday). */ + private static final Set TEXT_STYLES = + Set.of(DateTimePartStyle.NARROW, DateTimePartStyle.SHORT, DateTimePartStyle.LONG); + /** Styles accepted by the month attribute (numeric and textual). */ + private static final Set ALL_STYLES = + EnumSet.allOf(DateTimePartStyle.class); + private Instant lastDateTime; /** Creates an empty component. {@link #setDateTime} can be called later. */ @@ -278,8 +290,7 @@ public RelativeTime setTimeZoneName(TimeZoneName value) { * {@link Format#DATETIME}. {@code null} removes the attribute. */ public RelativeTime setYear(DateTimePartStyle style) { - setOrRemove(ATTR_YEAR, style == null ? null : style.attributeValue()); - return this; + return setDatePart(ATTR_YEAR, style, NUMERIC_STYLES); } /** @@ -289,8 +300,7 @@ public RelativeTime setYear(DateTimePartStyle style) { * {@link #setFormat format} is {@link Format#DATETIME}. {@code null} removes the attribute. */ public RelativeTime setMonth(DateTimePartStyle style) { - setOrRemove(ATTR_MONTH, style == null ? null : style.attributeValue()); - return this; + return setDatePart(ATTR_MONTH, style, ALL_STYLES); } /** @@ -299,8 +309,7 @@ public RelativeTime setMonth(DateTimePartStyle style) { * {@link Format#DATETIME}. {@code null} removes the attribute. */ public RelativeTime setDay(DateTimePartStyle style) { - setOrRemove(ATTR_DAY, style == null ? null : style.attributeValue()); - return this; + return setDatePart(ATTR_DAY, style, NUMERIC_STYLES); } /** @@ -309,8 +318,7 @@ public RelativeTime setDay(DateTimePartStyle style) { * {@link #setFormat format} is {@link Format#DATETIME}. {@code null} removes the attribute. */ public RelativeTime setWeekday(DateTimePartStyle style) { - setOrRemove(ATTR_WEEKDAY, style == null ? null : style.attributeValue()); - return this; + return setDatePart(ATTR_WEEKDAY, style, TEXT_STYLES); } /** @@ -319,8 +327,7 @@ public RelativeTime setWeekday(DateTimePartStyle style) { * {@link Format#DATETIME}. {@code null} removes the attribute. */ public RelativeTime setHour(DateTimePartStyle style) { - setOrRemove(ATTR_HOUR, style == null ? null : style.attributeValue()); - return this; + return setDatePart(ATTR_HOUR, style, NUMERIC_STYLES); } /** @@ -329,8 +336,7 @@ public RelativeTime setHour(DateTimePartStyle style) { * {@link Format#DATETIME}. {@code null} removes the attribute. */ public RelativeTime setMinute(DateTimePartStyle style) { - setOrRemove(ATTR_MINUTE, style == null ? null : style.attributeValue()); - return this; + return setDatePart(ATTR_MINUTE, style, NUMERIC_STYLES); } /** @@ -339,7 +345,20 @@ public RelativeTime setMinute(DateTimePartStyle style) { * {@link Format#DATETIME}. {@code null} removes the attribute. */ public RelativeTime setSecond(DateTimePartStyle style) { - setOrRemove(ATTR_SECOND, style == null ? null : style.attributeValue()); + return setDatePart(ATTR_SECOND, style, NUMERIC_STYLES); + } + + /** + * Writes a date-part attribute after checking the style is in the part's allowed subset. The + * upstream element silently ignores or coerces out-of-subset values, so we fail fast instead. + */ + private RelativeTime setDatePart(String attribute, DateTimePartStyle style, + Set allowed) { + if (style != null && !allowed.contains(style)) { + throw new IllegalArgumentException( + attribute + " does not accept " + style + "; allowed values: " + allowed); + } + setOrRemove(attribute, style == null ? null : style.attributeValue()); return this; } diff --git a/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java b/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java index c66e42f..782c2aa 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java +++ b/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java @@ -253,6 +253,30 @@ public void datePartSetters_nullRemovesAttribute() { assertNull(attr(rt, "second")); } + @Test(expected = IllegalArgumentException.class) + public void setWeekday_rejectsNumericStyle() { + new RelativeTime().setWeekday(DateTimePartStyle.NUMERIC); + } + + @Test(expected = IllegalArgumentException.class) + public void setYear_rejectsTextStyle() { + new RelativeTime().setYear(DateTimePartStyle.LONG); + } + + @Test(expected = IllegalArgumentException.class) + public void setDay_rejectsTextStyle() { + new RelativeTime().setDay(DateTimePartStyle.NARROW); + } + + @Test + public void setMonth_acceptsEveryStyle() { + RelativeTime rt = new RelativeTime(); + for (DateTimePartStyle style : DateTimePartStyle.values()) { + rt.setMonth(style); + assertTrue("month should accept " + style, rt.getElement().hasAttribute("month")); + } + } + // --- fluent chain returns this ----------------------------------------- @Test From b85c3b2c74348a38a794ca9f7e979b6e75d37799 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Wed, 3 Jun 2026 15:33:08 -0300 Subject: [PATCH 03/11] WIP: fix: fix finding 3 --- SPECIFICATIONS.md | 4 ++-- .../flowingcode/vaadin/addons/relativetime/RelativeTime.java | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/SPECIFICATIONS.md b/SPECIFICATIONS.md index 3a7653d..d6fa7fc 100644 --- a/SPECIFICATIONS.md +++ b/SPECIFICATIONS.md @@ -127,7 +127,7 @@ add(new RelativeTime(LocalDate.of(2025, 1, 1))); ### 3.2 Setting the Target Date ```java -public class RelativeTime extends Component implements HasSize { +public class RelativeTime extends Component { // HasStyle inherited from Component public RelativeTime(); // empty; datetime can be set later public RelativeTime(Instant datetime); @@ -198,7 +198,7 @@ When no configuration is applied: ## 5. Theming -The component is rendered as inline text and inherits font, color, and size from its parent. It is compatible with both **Lumo** and **Aura** with no theme-specific CSS. Standard `HasStyle` (inherited from `Component`) and `HasSize` are exposed on the Java side for layout adjustments. +The component is rendered as inline text and inherits font, color, and size from its parent. It is compatible with both **Lumo** and **Aura** with no theme-specific CSS. `HasStyle` (inherited from `Component`) is available for class-name and inline-style adjustments. `HasSize` is intentionally **not** implemented: the host renders inline, so `width`/`height` would be no-ops; size the surrounding container or set `display` via `getStyle()` if explicit sizing is needed. ## 6. Serialization diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java index 06f2904..3d5c3d0 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java @@ -21,7 +21,6 @@ package com.flowingcode.vaadin.addons.relativetime; import com.vaadin.flow.component.Component; -import com.vaadin.flow.component.HasSize; import com.vaadin.flow.component.Tag; import com.vaadin.flow.component.dependency.JsModule; import com.vaadin.flow.component.dependency.NpmPackage; @@ -61,7 +60,7 @@ @NpmPackage(value = "@github/relative-time-element", version = "5.0.0") @JsModule("@github/relative-time-element") @SuppressWarnings("serial") -public class RelativeTime extends Component implements HasSize { +public class RelativeTime extends Component { private static final String ATTR_DATETIME = "datetime"; private static final String ATTR_TENSE = "tense"; From f616cec951128b895ba3de0400cec234bce21a09 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Wed, 3 Jun 2026 15:44:11 -0300 Subject: [PATCH 04/11] WIP: fix: fix finding 4 --- SPECIFICATIONS.md | 4 ++-- .../vaadin/addons/relativetime/RelativeTime.java | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/SPECIFICATIONS.md b/SPECIFICATIONS.md index d6fa7fc..edc35da 100644 --- a/SPECIFICATIONS.md +++ b/SPECIFICATIONS.md @@ -26,8 +26,8 @@ The setters accept the standard `java.time` types. Each is converted to an ISO-8 | Java Type | Conversion | Notes | |-----------|------------|-------| | `Instant` | direct | Canonical form. | -| `OffsetDateTime` | `toInstant()` | Offset preserved in the wire string. | -| `ZonedDateTime` | `toInstant()` | Zone preserved in the wire string. | +| `OffsetDateTime` | `toInstant()` | Offset applied, then discarded; wire string is the resulting UTC instant. | +| `ZonedDateTime` | `toInstant()` | Zone applied, then discarded; wire string is the resulting UTC instant. | | `LocalDateTime` | `atZone(systemDefault()).toInstant()` | Server zone assumed; document this in the Javadoc. | | `LocalDate` | `atStartOfDay(systemDefault()).toInstant()` | Midnight of the server zone. | diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java index 3d5c3d0..f04a4c7 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java @@ -137,12 +137,18 @@ public RelativeTime setDateTime(Instant datetime) { return this; } - /** Sets the target as an {@link OffsetDateTime}; the offset is preserved on the wire string. */ + /** + * Sets the target as an {@link OffsetDateTime}. The offset is applied via {@code toInstant()}; + * the wire string is the resulting UTC instant (the offset itself is not retained). + */ public RelativeTime setDateTime(OffsetDateTime datetime) { return setDateTime(datetime == null ? null : datetime.toInstant()); } - /** Sets the target as a {@link ZonedDateTime}; the zone is preserved on the wire string. */ + /** + * Sets the target as a {@link ZonedDateTime}. The zone is applied via {@code toInstant()}; the + * wire string is the resulting UTC instant (the zone itself is not retained). + */ public RelativeTime setDateTime(ZonedDateTime datetime) { return setDateTime(datetime == null ? null : datetime.toInstant()); } From fcd7e5d5906ae94d4326bd3549f8673686aa64b6 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Wed, 3 Jun 2026 15:57:02 -0300 Subject: [PATCH 05/11] WIP: fix: fix finding 5 --- SPECIFICATIONS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SPECIFICATIONS.md b/SPECIFICATIONS.md index edc35da..9823a2f 100644 --- a/SPECIFICATIONS.md +++ b/SPECIFICATIONS.md @@ -51,9 +51,11 @@ The web component supports several formats. The Java API exposes them as enums w The `threshold` attribute (default `P30D`) switches the component from relative ("3 weeks ago") to absolute ("Jan 4, 2024") once the gap to the target exceeds a configurable duration. The Java setter accepts a `java.time.Duration` and serialises it to ISO-8601 (`P30D`, `PT24H`, etc.) before writing the attribute. ```java -relativeTime.setThreshold(Duration.ofDays(30)); // → threshold="P30D" +relativeTime.setThreshold(Duration.ofDays(30)); // → threshold="PT720H" ``` +(Java's `Duration.toString()` has no day component, so `Duration.ofDays(30)` serialises to `PT720H`. The upstream element treats `PT720H` and `P30D` as equivalent, so the behaviour is identical to the documented `P30D` default.) + **Threshold is only consulted when `format=auto`/`relative` AND `tense=auto`.** Setting `tense=PAST` or `tense=FUTURE` commits the element to relative phrasing regardless of how far away the target is, so the threshold has no effect. Likewise, explicit non-relative formats (`DURATION`, `MICRO`, `ELAPSED`, `DATETIME`) ignore it. This is a common footgun: pairing `setTense(FUTURE)` with `setThreshold(...)` silently makes the threshold inert. ### 2.5 Prefix From 0ecc2e4ad36ed0d29507544d63fef7a8f57cc392 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Wed, 3 Jun 2026 15:59:59 -0300 Subject: [PATCH 06/11] WIP: fix: fix finding 6 --- SPECIFICATIONS.md | 1 + .../vaadin/addons/relativetime/RelativeTime.java | 9 +++++++++ .../addons/relativetime/test/RelativeTimeTest.java | 8 ++++++++ 3 files changed, 18 insertions(+) diff --git a/SPECIFICATIONS.md b/SPECIFICATIONS.md index 9823a2f..dba774a 100644 --- a/SPECIFICATIONS.md +++ b/SPECIFICATIONS.md @@ -143,6 +143,7 @@ public class RelativeTime extends Component { // HasStyle inheri public RelativeTime setDateTime(ZonedDateTime datetime); public RelativeTime setDateTime(LocalDateTime datetime); // uses ZoneId.systemDefault() public RelativeTime setDateTime(LocalDate date); // uses ZoneId.systemDefault() + public RelativeTime clear(); // clears datetime (no cast needed) public Instant getDateTime(); // last value pushed, in UTC } ``` diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java index f04a4c7..514fa65 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java @@ -171,6 +171,15 @@ public RelativeTime setDateTime(LocalDate date) { date == null ? null : date.atStartOfDay(ZoneId.systemDefault()).toInstant()); } + /** + * Clears the target datetime: removes the {@code datetime} attribute and the component renders + * as empty. Equivalent to {@code setDateTime((Instant) null)} but without the cast that the + * overloaded {@code setDateTime(null)} would otherwise require. + */ + public RelativeTime clear() { + return setDateTime((Instant) null); + } + /** * Returns the last instant applied via any of the {@code setDateTime} overloads, normalised to * UTC. Returns {@code null} if no datetime has been set or it was cleared. diff --git a/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java b/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java index 782c2aa..4edf14d 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java +++ b/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java @@ -65,6 +65,14 @@ public void setDateTime_null_clearsAttributeAndGetter() { assertNull(rt.getDateTime()); } + @Test + public void clear_removesAttributeAndGetter() { + RelativeTime rt = new RelativeTime(FIXED); + assertEquals(rt, rt.clear()); + assertNull(attr(rt, "datetime")); + assertNull(rt.getDateTime()); + } + @Test public void setDateTime_offsetDateTime_normalisesToInstant() { OffsetDateTime odt = OffsetDateTime.of(2026, 1, 15, 14, 34, 56, 0, ZoneOffset.ofHours(2)); From 66980ee5265227f88cbffd2a64c5420c9c787d33 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Wed, 3 Jun 2026 16:01:17 -0300 Subject: [PATCH 07/11] WIP: fix: fix finding 7 --- .../flowingcode/vaadin/addons/relativetime/RelativeTime.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java index 514fa65..385ff7f 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java @@ -270,6 +270,11 @@ public RelativeTime setNoTitle(boolean noTitle) { * phrasing. {@code null} removes the attribute; when unset, the upstream walks DOM ancestors * via {@code closest('[lang]')} and falls back to the document's {@code lang}, so an app-wide * locale on {@code } is picked up automatically. + * + *

Note: {@link Locale#ROOT} (and {@code new Locale("")}) serialises to {@code lang="und"}, + * which the browser treats as "undetermined" and silently resolves to the document/default + * locale, so passing the root locale looks like it was ignored. Pass {@code null} to clear + * instead. */ public RelativeTime setLocale(Locale locale) { setOrRemove(ATTR_LANG, locale == null ? null : locale.toLanguageTag()); From 7159a8600b7f9b424d3f0aa5ff97104be863b0d0 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Wed, 3 Jun 2026 16:04:49 -0300 Subject: [PATCH 08/11] WIP: fix: fix finding 8 --- SPECIFICATIONS.md | 2 ++ .../vaadin/addons/relativetime/RelativeTime.java | 7 +++++++ .../addons/relativetime/test/RelativeTimeTest.java | 12 ++++++++++++ 3 files changed, 21 insertions(+) diff --git a/SPECIFICATIONS.md b/SPECIFICATIONS.md index dba774a..d441b4f 100644 --- a/SPECIFICATIONS.md +++ b/SPECIFICATIONS.md @@ -58,6 +58,8 @@ relativeTime.setThreshold(Duration.ofDays(30)); // → threshold="PT720H" **Threshold is only consulted when `format=auto`/`relative` AND `tense=auto`.** Setting `tense=PAST` or `tense=FUTURE` commits the element to relative phrasing regardless of how far away the target is, so the threshold has no effect. Likewise, explicit non-relative formats (`DURATION`, `MICRO`, `ELAPSED`, `DATETIME`) ignore it. This is a common footgun: pairing `setTense(FUTURE)` with `setThreshold(...)` silently makes the threshold inert. +A negative `Duration` is rejected with `IllegalArgumentException`: Java serialises it as `PT-nS`, which the upstream duration parser rejects, silently reverting to the default `P30D`. Zero (`PT0S`) is allowed and means "always absolute". + ### 2.5 Prefix The `prefix` attribute (default `"on"`) is the word prepended to absolute dates once the threshold is crossed. `setPrefix("")` drops the prefix entirely; `setPrefix(null)` restores the default. diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java index 385ff7f..6b99c23 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java @@ -235,8 +235,15 @@ public RelativeTime setFormatStyle(FormatStyle style) { * Format#RELATIVE} AND {@link #setTense tense} is {@link Tense#AUTO}. Pairing this with * {@code setTense(PAST)} or {@code setTense(FUTURE)} silently makes the threshold inert, * because the element then commits to relative phrasing regardless of distance. + * + * @throws IllegalArgumentException if {@code threshold} is negative. Java serialises a negative + * duration as {@code PT-nS}, which the upstream element's duration parser rejects, silently + * reverting to the default {@code P30D}. Zero is allowed (it means "always absolute"). */ public RelativeTime setThreshold(Duration threshold) { + if (threshold != null && threshold.isNegative()) { + throw new IllegalArgumentException("threshold must not be negative: " + threshold); + } setOrRemove(ATTR_THRESHOLD, threshold == null ? null : threshold.toString()); return this; } diff --git a/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java b/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java index 4edf14d..5107e7f 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java +++ b/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java @@ -157,6 +157,18 @@ public void setThreshold_writesIso8601Duration() { assertNull(attr(rt, "threshold")); } + @Test + public void setThreshold_acceptsZero() { + RelativeTime rt = new RelativeTime(); + rt.setThreshold(Duration.ZERO); + assertEquals("PT0S", attr(rt, "threshold")); + } + + @Test(expected = IllegalArgumentException.class) + public void setThreshold_rejectsNegative() { + new RelativeTime().setThreshold(Duration.ofSeconds(-30)); + } + @Test public void setPrefix_emptyStringSetsEmpty_nullClears() { RelativeTime rt = new RelativeTime(); From eeb3137ca372ea190a648678c32197f707838f2d Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Wed, 3 Jun 2026 16:10:34 -0300 Subject: [PATCH 09/11] WIP: fix: fix finding 9 --- .../addons/relativetime/AttributeValues.java | 37 +++++++++++++++++++ .../vaadin/addons/relativetime/Format.java | 4 +- .../addons/relativetime/FormatStyle.java | 4 +- .../vaadin/addons/relativetime/Precision.java | 4 +- .../vaadin/addons/relativetime/Tense.java | 4 +- 5 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/flowingcode/vaadin/addons/relativetime/AttributeValues.java diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/AttributeValues.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/AttributeValues.java new file mode 100644 index 0000000..2f06e6b --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/AttributeValues.java @@ -0,0 +1,37 @@ +/*- + * #%L + * Relative Time 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.relativetime; + +import java.util.Locale; + +/** Internal helpers for mapping enum constants to their {@code } wire values. */ +final class AttributeValues { + + private AttributeValues() {} + + /** + * Returns the enum constant's name in lower case (e.g. {@code FUTURE} → {@code "future"}), + * the default wire form for enums whose attribute value matches the constant name. + */ + static String ofName(Enum value) { + return value.name().toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/Format.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/Format.java index 33e1f3a..7971ea4 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/relativetime/Format.java +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/Format.java @@ -20,8 +20,6 @@ package com.flowingcode.vaadin.addons.relativetime; -import java.util.Locale; - /** Values for the {@code format} attribute on {@code }. */ public enum Format { /** @@ -63,6 +61,6 @@ public enum Format { MICRO; String attributeValue() { - return name().toLowerCase(Locale.ROOT); + return AttributeValues.ofName(this); } } diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/FormatStyle.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/FormatStyle.java index fd7b15d..771cedc 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/relativetime/FormatStyle.java +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/FormatStyle.java @@ -20,8 +20,6 @@ package com.flowingcode.vaadin.addons.relativetime; -import java.util.Locale; - /** Values for the {@code format-style} attribute on {@code }. */ public enum FormatStyle { /** Long phrasing ("3 days ago"). */ @@ -32,6 +30,6 @@ public enum FormatStyle { NARROW; String attributeValue() { - return name().toLowerCase(Locale.ROOT); + return AttributeValues.ofName(this); } } diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/Precision.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/Precision.java index 5141c4b..97d3c6c 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/relativetime/Precision.java +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/Precision.java @@ -20,8 +20,6 @@ package com.flowingcode.vaadin.addons.relativetime; -import java.util.Locale; - /** * Values for the {@code precision} attribute on {@code }. The element rounds the * displayed phrase to the chosen unit. @@ -35,6 +33,6 @@ public enum Precision { SECOND; String attributeValue() { - return name().toLowerCase(Locale.ROOT); + return AttributeValues.ofName(this); } } diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/Tense.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/Tense.java index 178e556..48908a7 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/relativetime/Tense.java +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/Tense.java @@ -20,8 +20,6 @@ package com.flowingcode.vaadin.addons.relativetime; -import java.util.Locale; - /** Values for the {@code tense} attribute on {@code }. */ public enum Tense { /** Past or future is decided from the target instant. */ @@ -32,6 +30,6 @@ public enum Tense { FUTURE; String attributeValue() { - return name().toLowerCase(Locale.ROOT); + return AttributeValues.ofName(this); } } From 5dad4b2151bbf32ac8327a8c2ec3b91ee25e7521 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Wed, 3 Jun 2026 16:23:36 -0300 Subject: [PATCH 10/11] WIP: fix: fix finding 2 --- SPECIFICATIONS.md | 2 +- .../addons/relativetime/RelativeTime.java | 11 +++++++++++ .../relativetime/test/RelativeTimeTest.java | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/SPECIFICATIONS.md b/SPECIFICATIONS.md index d441b4f..014a5ca 100644 --- a/SPECIFICATIONS.md +++ b/SPECIFICATIONS.md @@ -78,7 +78,7 @@ These attributes only take effect when `format=datetime` (i.e. when the absolute | Setter | Attribute | Java type / values | |--------|-----------|--------------------| -| `setTimeZone(ZoneId)` | `time-zone` | IANA name (e.g. `America/New_York`). `null` removes it; the browser default is used. When unset on the element, the upstream walks DOM ancestors via `closest('[time-zone]')` and falls back to the `` element's `time-zone` attribute, mirroring the `lang` inheritance pattern. | +| `setTimeZone(ZoneId)` | `time-zone` | IANA name (e.g. `America/New_York`); validated against `ZoneId.getAvailableZoneIds()` (includes `UTC`). Offset-based zones (`ZoneOffset` like `+02:00`/`Z`, or `GMT+02:00`) throw `IllegalArgumentException`, because the browser's `Intl.DateTimeFormat` rejects them with a `RangeError` that breaks rendering. `null` removes it; when unset, the upstream walks DOM ancestors via `closest('[time-zone]')` and falls back to the `` element's `time-zone` attribute, mirroring the `lang` inheritance pattern. | | `setTimeZoneName(TimeZoneName)` | `time-zone-name` | `LONG`, `SHORT`, `SHORT_OFFSET`, `LONG_OFFSET`, `SHORT_GENERIC`, `LONG_GENERIC` | | `setYear(DateTimePartStyle)` | `year` | `NUMERIC`, `TWO_DIGIT` | | `setMonth(DateTimePartStyle)` | `month` | `NUMERIC`, `TWO_DIGIT`, `NARROW`, `SHORT`, `LONG` | diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java index 6b99c23..fea347d 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java @@ -294,8 +294,19 @@ public RelativeTime setLocale(Locale locale) { * removes the attribute; when unset, the upstream walks DOM ancestors via {@code * closest('[time-zone]')} and falls back to the document's {@code time-zone} attribute (and, * absent that, the browser default). + * + * @throws IllegalArgumentException if {@code timeZone} is not an IANA region zone. Offset-based + * zones (a {@link java.time.ZoneOffset} such as {@code +02:00} or {@code Z}, or a + * {@code GMT+02:00} form) are written verbatim into the attribute and rejected by the + * browser's {@code Intl.DateTimeFormat} with a {@code RangeError}, which breaks rendering. + * The accepted set is {@link ZoneId#getAvailableZoneIds()} (which includes {@code UTC}). */ public RelativeTime setTimeZone(ZoneId timeZone) { + if (timeZone != null && !ZoneId.getAvailableZoneIds().contains(timeZone.getId())) { + throw new IllegalArgumentException( + "time-zone must be an IANA zone ID (e.g. America/New_York); offset-based zones like " + + timeZone.getId() + " are not accepted by the browser's Intl.DateTimeFormat"); + } setOrRemove(ATTR_TIME_ZONE, timeZone == null ? null : timeZone.getId()); return this; } diff --git a/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java b/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java index 5107e7f..90c6fe9 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java +++ b/src/test/java/com/flowingcode/vaadin/addons/relativetime/test/RelativeTimeTest.java @@ -207,6 +207,23 @@ public void setTimeZone_writesZoneIdAsTimeZone() { assertNull(attr(rt, "time-zone")); } + @Test + public void setTimeZone_acceptsUtc() { + RelativeTime rt = new RelativeTime(); + rt.setTimeZone(ZoneId.of("UTC")); + assertEquals("UTC", attr(rt, "time-zone")); + } + + @Test(expected = IllegalArgumentException.class) + public void setTimeZone_rejectsOffsetZone() { + new RelativeTime().setTimeZone(ZoneOffset.ofHours(2)); + } + + @Test(expected = IllegalArgumentException.class) + public void setTimeZone_rejectsZuluOffset() { + new RelativeTime().setTimeZone(ZoneOffset.UTC); + } + @Test public void setTimeZoneName_writesCamelCaseValue() { RelativeTime rt = new RelativeTime(); From 638adcc4ada54f827ef6e7ad2a2840c8fdba6d83 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Fri, 12 Jun 2026 10:54:47 -0300 Subject: [PATCH 11/11] WIP: use unmodifiable EnumSet for style sets and simplify setNoTitle --- .../addons/relativetime/RelativeTime.java | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java index fea347d..c990a3f 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java +++ b/src/main/java/com/flowingcode/vaadin/addons/relativetime/RelativeTime.java @@ -31,6 +31,7 @@ import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.util.Collections; import java.util.EnumSet; import java.util.Locale; import java.util.Set; @@ -82,14 +83,14 @@ public class RelativeTime extends Component { private static final String ATTR_SECOND = "second"; /** Styles accepted by the numeric date-part attributes (year, day, hour, minute, second). */ - private static final Set NUMERIC_STYLES = - Set.of(DateTimePartStyle.NUMERIC, DateTimePartStyle.TWO_DIGIT); + private static final Set NUMERIC_STYLES = Collections.unmodifiableSet( + EnumSet.of(DateTimePartStyle.NUMERIC, DateTimePartStyle.TWO_DIGIT)); /** Styles accepted by the textual date-part attribute (weekday). */ - private static final Set TEXT_STYLES = - Set.of(DateTimePartStyle.NARROW, DateTimePartStyle.SHORT, DateTimePartStyle.LONG); + private static final Set TEXT_STYLES = Collections.unmodifiableSet( + EnumSet.of(DateTimePartStyle.NARROW, DateTimePartStyle.SHORT, DateTimePartStyle.LONG)); /** Styles accepted by the month attribute (numeric and textual). */ - private static final Set ALL_STYLES = - EnumSet.allOf(DateTimePartStyle.class); + private static final Set ALL_STYLES = Collections.unmodifiableSet( + EnumSet.allOf(DateTimePartStyle.class)); private Instant lastDateTime; @@ -263,11 +264,7 @@ public RelativeTime setPrefix(String prefix) { * tooltip. {@code false} removes it (the element's default behaviour shows the tooltip). */ public RelativeTime setNoTitle(boolean noTitle) { - if (noTitle) { - getElement().setAttribute(ATTR_NO_TITLE, true); - } else { - getElement().removeAttribute(ATTR_NO_TITLE); - } + getElement().setAttribute(ATTR_NO_TITLE, noTitle); return this; }