diff --git a/components/src/main/java/org/patternfly/component/ComponentType.java b/components/src/main/java/org/patternfly/component/ComponentType.java
index 3e739d004..e7a921044 100644
--- a/components/src/main/java/org/patternfly/component/ComponentType.java
+++ b/components/src/main/java/org/patternfly/component/ComponentType.java
@@ -145,6 +145,8 @@ public enum ComponentType {
TextInputGroup("tig", "PF6/TextInputGroup"),
+ Timestamp("ts", "PF6/Timestamp"),
+
Title("tlt", "PF6/Title"),
ToggleGroup("tg", null),
diff --git a/components/src/main/java/org/patternfly/component/timestamp/CustomFormat.java b/components/src/main/java/org/patternfly/component/timestamp/CustomFormat.java
new file mode 100644
index 000000000..bea0943a6
--- /dev/null
+++ b/components/src/main/java/org/patternfly/component/timestamp/CustomFormat.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2023 Red Hat
+ *
+ * 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
+ *
+ * https://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.
+ */
+package org.patternfly.component.timestamp;
+
+import java.util.Objects;
+
+import jsinterop.annotations.JsOverlay;
+import jsinterop.annotations.JsPackage;
+import jsinterop.annotations.JsProperty;
+import jsinterop.annotations.JsType;
+
+@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
+public class CustomFormat implements FormatOptions {
+
+ @JsOverlay
+ public static final CustomFormat create() {
+ return new CustomFormat();
+ }
+
+ @JsProperty
+ private String year;
+ @JsProperty
+ private String month;
+ @JsProperty
+ private String day;
+ @JsProperty
+ private String weekday;
+ @JsProperty
+ private String hour;
+ @JsProperty
+ private String minute;
+ @JsProperty
+ private String second;
+ @JsProperty
+ private String timeZone;
+ @JsProperty
+ private String timeZoneName;
+ @JsProperty
+ private Boolean hour12;
+ @JsProperty
+ private String era;
+ @JsProperty
+ private String dayPeriod;
+ @JsProperty
+ private String fractionalSecondDigits;
+
+ private CustomFormat() {
+ }
+
+ @JsOverlay
+ public final CustomFormat year(DateTimeFormatOptions.Year year) {
+ if (!Objects.equals(this.year, year.value)) {
+ this.year = year.value;
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final CustomFormat month(DateTimeFormatOptions.Month month) {
+ if (!Objects.equals(this.month, month.value)) {
+ this.month = month.value;
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final CustomFormat day(DateTimeFormatOptions.Day day) {
+ if (!Objects.equals(this.day, day.value)) {
+ this.day = day.value;
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final CustomFormat weekday(DateTimeFormatOptions.Weekday weekday) {
+ if (!Objects.equals(this.weekday, weekday.value)) {
+ this.weekday = weekday.value;
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final CustomFormat hour(DateTimeFormatOptions.Hour hour) {
+ if (!Objects.equals(this.hour, hour.value)) {
+ this.hour = hour.value;
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final CustomFormat minute(DateTimeFormatOptions.Minute minute) {
+ if (!Objects.equals(this.minute, minute.value)) {
+ this.minute = minute.value;
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final CustomFormat second(DateTimeFormatOptions.Second second) {
+ if (!Objects.equals(this.second, second.value)) {
+ this.second = second.value;
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final CustomFormat timeZone(String timeZone) {
+ if (!Objects.equals(this.timeZone, timeZone)) {
+ this.timeZone = timeZone; // IANA time zone name
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final CustomFormat timeZoneName(DateTimeFormatOptions.TimeZoneName timeZoneName) {
+ if (!Objects.equals(this.timeZoneName, timeZoneName.value)) {
+ this.timeZoneName = timeZoneName.value;
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final CustomFormat hour12(boolean hour12) {
+ if (this.hour12 != hour12) {
+ this.hour12 = hour12;
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final CustomFormat era(DateTimeFormatOptions.Era era) {
+ if (!Objects.equals(this.era, era.value)) {
+ this.era = era.value;
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final CustomFormat dayPeriod(DateTimeFormatOptions.DayPeriod dayPeriod) {
+ if (!Objects.equals(this.dayPeriod, dayPeriod.value)) {
+ this.dayPeriod = dayPeriod.value;
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final CustomFormat fractionalSecondDigits(DateTimeFormatOptions.FractionalSecondDigits digits) {
+ if (!Objects.equals(this.fractionalSecondDigits, digits.value)) {
+ this.fractionalSecondDigits = digits.value;
+ }
+ return this;
+ }
+}
diff --git a/components/src/main/java/org/patternfly/component/timestamp/DateTimeFormatOptions.java b/components/src/main/java/org/patternfly/component/timestamp/DateTimeFormatOptions.java
new file mode 100644
index 000000000..5e03eb42b
--- /dev/null
+++ b/components/src/main/java/org/patternfly/component/timestamp/DateTimeFormatOptions.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright 2023 Red Hat
+ *
+ * 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
+ *
+ * https://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.
+ */
+package org.patternfly.component.timestamp;
+
+/**
+ * Container for all date/time formatting options based on MDN
+ * Intl.DateTimeFormat
+ * Consolidates all format enums into static inner classes for better
+ * organization.
+ *
+ */
+public class DateTimeFormatOptions {
+
+ /**
+ * Options for weekday formatting.
+ *
+ * - {@code narrow} - "T"
+ * - {@code _short} - "Thu"
+ * - {@code _long} - "Thursday"
+ *
+ */
+ public enum Weekday {
+ narrow("narrow"),
+ _short("short"),
+ _long("long");
+
+ public final String value;
+
+ Weekday(String value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Options for era formatting.
+ *
+ * - {@code narrow} - "A"
+ * - {@code _short} - "AD"
+ * - {@code _long} - "Anno Domini"
+ *
+ */
+ public enum Era {
+ narrow("narrow"),
+ _short("short"),
+ _long("long");
+
+ public final String value;
+
+ Era(String value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Options for year formatting.
+ *
+ * - {@code numeric} - "2023"
+ * - {@code _2digit} - "23"
+ *
+ */
+ public enum Year {
+ numeric("numeric"),
+ _2digit("2-digit");
+
+ public final String value;
+
+ Year(String value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Options for month formatting.
+ *
+ * - {@code numeric} - "3"
+ * - {@code _2digit} - "03"
+ * - {@code narrow} - "M"
+ * - {@code _short} - "Mar"
+ * - {@code _long} - "March"
+ *
+ */
+ public enum Month {
+ numeric("numeric"),
+ _2digit("2-digit"),
+ narrow("narrow"),
+ _short("short"),
+ _long("long");
+
+ public final String value;
+
+ Month(String value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Options for day formatting.
+ *
+ * - {@code numeric} - "9"
+ * - {@code _2digit} - "09"
+ *
+ */
+ public enum Day {
+ numeric("numeric"),
+ _2digit("2-digit");
+
+ public final String value;
+
+ Day(String value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Options for hour formatting.
+ *
+ * - {@code numeric} - "2"
+ * - {@code _2digit} - "02"
+ *
+ */
+ public enum Hour {
+ numeric("numeric"),
+ _2digit("2-digit");
+
+ public final String value;
+
+ Hour(String value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Options for minute formatting.
+ *
+ * - {@code numeric} - "5"
+ * - {@code _2digit} - "05"
+ *
+ */
+ public enum Minute {
+ numeric("numeric"),
+ _2digit("2-digit");
+
+ public final String value;
+
+ Minute(String value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Options for second formatting.
+ *
+ * - {@code numeric} - "7"
+ * - {@code _2digit} - "07"
+ *
+ */
+ public enum Second {
+ numeric("numeric"),
+ _2digit("2-digit");
+
+ public final String value;
+
+ Second(String value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Options for fractional second digits.
+ * The number of digits used to represent fractions of a second (any additional
+ * digits are truncated).
+ *
+ * - {@code _1} - 1 digit
+ * - {@code _2} - 2 digits
+ * - {@code _3} - 3 digits
+ *
+ */
+ public enum FractionalSecondDigits {
+ _1("1"),
+ _2("2"),
+ _3("3");
+
+ public final String value;
+
+ FractionalSecondDigits(String value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Options for day period formatting.
+ * The formatting style used for day periods like "in the morning", "am",
+ * "noon", etc.
+ *
+ * Note: This option only has an effect if a 12-hour clock
+ * ({@code hourCycle: "h12"} or {@code hourCycle: "h11"}) is used.
+ * Many locales use the same string irrespective of the width specified.
+ *
+ *
+ * - {@code narrow} - "mat." (French), "a" (English)
+ * - {@code _short} - "matin" (French), "AM" (English)
+ * - {@code _long} - "du matin" (French), "AM" (English)
+ *
+ */
+ public enum DayPeriod {
+ narrow("narrow"),
+ _short("short"),
+ _long("long");
+
+ public final String value;
+
+ DayPeriod(String value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Options for time zone name formatting.
+ * The localized representation of the time zone name.
+ *
+ * Note: Timezone display may fall back to another format if a
+ * required string is unavailable.
+ * For example, the non-location formats should display the timezone without a
+ * specific country/city location
+ * like "Pacific Time", but may fall back to a timezone like "Los Angeles Time".
+ *
+ *
+ * - {@code _short} - "PST", "GMT-8"
+ * - {@code _long} - "Pacific Standard Time", "Nordamerikanische
+ * Westküsten-Normalzeit"
+ * - {@code shortOffset} - "GMT-8"
+ * - {@code longOffset} - "GMT-08:00"
+ * - {@code shortGeneric} - "PT", "Los Angeles Zeit"
+ * - {@code longGeneric} - "Pacific Time", "Nordamerikanische
+ * Westküstenzeit"
+ *
+ */
+ public enum TimeZoneName {
+ _short("short"),
+ _long("long"),
+ shortOffset("shortOffset"),
+ longOffset("longOffset"),
+ shortGeneric("shortGeneric"),
+ longGeneric("longGeneric");
+
+ public final String value;
+
+ TimeZoneName(String value) {
+ this.value = value;
+ }
+ }
+}
diff --git a/components/src/main/java/org/patternfly/component/timestamp/FormatOptions.java b/components/src/main/java/org/patternfly/component/timestamp/FormatOptions.java
new file mode 100644
index 000000000..73ffe6fac
--- /dev/null
+++ b/components/src/main/java/org/patternfly/component/timestamp/FormatOptions.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 Red Hat
+ *
+ * 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
+ *
+ * https://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.
+ */
+package org.patternfly.component.timestamp;
+
+import jsinterop.annotations.JsPackage;
+import jsinterop.annotations.JsType;
+
+/**
+ * A marker interface for different formatting options used in the Timestamp component.
+ * This allows for a unified handling of various format settings.
+ */
+@JsType(isNative = true, namespace = JsPackage.GLOBAL)
+public interface FormatOptions {
+ // This is a marker interface and does not define any methods.
+}
diff --git a/components/src/main/java/org/patternfly/component/timestamp/LocaleOptions.java b/components/src/main/java/org/patternfly/component/timestamp/LocaleOptions.java
new file mode 100644
index 000000000..d4c6e47dd
--- /dev/null
+++ b/components/src/main/java/org/patternfly/component/timestamp/LocaleOptions.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2023 Red Hat
+ *
+ * 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
+ *
+ * https://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.
+ */
+package org.patternfly.component.timestamp;
+
+import java.util.Objects;
+
+import jsinterop.annotations.JsOverlay;
+import jsinterop.annotations.JsPackage;
+import jsinterop.annotations.JsProperty;
+import jsinterop.annotations.JsType;
+
+@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
+public class LocaleOptions implements FormatOptions {
+
+ @JsOverlay
+ public static final LocaleOptions create() {
+ return new LocaleOptions();
+ }
+
+ @JsProperty
+ private String dateStyle;
+ @JsProperty
+ private String timeStyle;
+ @JsProperty
+ private boolean hour12;
+ @JsProperty
+ private String timeZone;
+
+ private LocaleOptions() {
+ }
+
+ @JsOverlay
+ public final LocaleOptions dateStyle(TimestampFormat dateStyle) {
+ if (!Objects.equals(this.dateStyle, dateStyle.value)) {
+ this.dateStyle = dateStyle.value;
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final LocaleOptions timeStyle(TimestampFormat timeStyle) {
+ if (!Objects.equals(this.timeStyle, timeStyle.value)) {
+ this.timeStyle = timeStyle.value;
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final LocaleOptions hour12(boolean hour12) {
+ if (this.hour12 != hour12) {
+ this.hour12 = hour12;
+ }
+ return this;
+ }
+
+ @JsOverlay
+ public final LocaleOptions timeZone(String timeZone) {
+ if (!Objects.equals(this.timeZone, timeZone)) {
+ this.timeZone = timeZone;
+ }
+ return this;
+ }
+}
diff --git a/components/src/main/java/org/patternfly/component/timestamp/Timestamp.java b/components/src/main/java/org/patternfly/component/timestamp/Timestamp.java
new file mode 100644
index 000000000..9b9658738
--- /dev/null
+++ b/components/src/main/java/org/patternfly/component/timestamp/Timestamp.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2023 Red Hat
+ *
+ * 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
+ *
+ * https://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.
+ */
+package org.patternfly.component.timestamp;
+
+import java.util.Date;
+
+import org.jboss.elemento.ElementTextDelegate;
+import org.jboss.elemento.HTMLContainerBuilder;
+import org.jboss.elemento.logger.Logger;
+import org.patternfly.component.BaseComponent;
+import org.patternfly.component.ComponentType;
+
+import elemental2.core.JsDate;
+import elemental2.dom.Element;
+import elemental2.dom.HTMLElement;
+
+import static org.jboss.elemento.Elements.span;
+import static org.jboss.elemento.Elements.time;
+import static org.patternfly.component.timestamp.TimestampFormat.full;
+import static org.patternfly.style.Classes.component;
+import static org.patternfly.style.Classes.text;
+import static org.patternfly.style.Classes.timestamp;
+
+/**
+ * A timestamp component for displaying date and time values.
+ * This component supports various formatting options including
+ * custom formats, standard date/time styles, and UTC display.
+ * It can also handle custom HTML content or plain text.
+ *
+ * @see https://www.patternfly.org/components/timestamp
+ *
+ * @author mskacelik
+ */
+public class Timestamp extends BaseComponent
+ implements ElementTextDelegate {
+
+ private static final Logger logger = Logger.getLogger(Timestamp.class.getName());
+ private static final String DATETIME_ATTR = "datetime";
+ private static final String COORDINATED_UNIVERSAL_TIME = "Coordinated Universal Time";
+ private static final String UTC = "UTC";
+
+ // ------------------------------------------------------ factory
+
+ public static Timestamp timestamp() {
+ return new Timestamp();
+ }
+
+ public static Timestamp timestamp(String text) {
+ return new Timestamp().text(text);
+ }
+
+ public static Timestamp timestamp(Date dateTime) {
+ return new Timestamp().dateTime(dateTime);
+ }
+
+ public static Timestamp timestamp(String text, Date dateTime) {
+ return new Timestamp().text(text).dateTime(dateTime);
+ }
+
+ // ------------------------------------------------------ instance
+
+ private HTMLContainerBuilder timeElement;
+ private Date dateTime;
+ private TimestampFormat dateFormat;
+ private TimestampFormat timeFormat;
+ private CustomFormat customFormat;
+ private String displaySuffix = "";
+ private boolean is12Hour = true;
+ private String locale;
+ private boolean shouldDisplayUTC = false;
+ private boolean showDateTimeAsTextFlag = true;
+
+ Timestamp() {
+ super(ComponentType.Timestamp, span().css(component(timestamp)).element());
+ this.timeElement = time().css(component(timestamp, text));
+ updateDisplayAndDatetime();
+ }
+
+ // ------------------------------------------------------ builder
+
+ @Override
+ public Timestamp that() {
+ return this;
+ }
+
+ @Override
+ public Element textDelegate() {
+ return timeElement.element();
+ }
+
+ @Override
+ public Timestamp text(String text) {
+ this.showDateTimeAsTextFlag = text == null || text.trim().length() == 0; // isBlank()
+ textDelegate().textContent = text;
+ return this;
+ }
+
+ public Timestamp dateTime(Date dateTime) {
+ this.dateTime = dateTime;
+ updateDisplayAndDatetime();
+ return this;
+ }
+
+ public Timestamp dateFormat(TimestampFormat dateFormat) {
+ if (this.customFormat != null) {
+ logger.warn("Setting dateFormat while customFormat is already set. " +
+ "CustomFormat will be cleared to avoid conflicting format options. " +
+ "Use either dateFormat/timeFormat OR customFormat, not both.");
+ }
+ this.dateFormat = dateFormat;
+ this.customFormat = null;
+ updateDisplayAndDatetime();
+ return this;
+ }
+
+ public Timestamp timeFormat(TimestampFormat timeFormat) {
+ if (this.customFormat != null) {
+ logger.warn("Setting timeFormat while customFormat is already set. " +
+ "CustomFormat will be cleared to avoid conflicting format options. " +
+ "Use either dateFormat/timeFormat OR customFormat, not both.");
+ }
+ this.timeFormat = timeFormat;
+ this.customFormat = null;
+ updateDisplayAndDatetime();
+ return this;
+ }
+
+ public Timestamp customFormat(CustomFormat customFormat) {
+ if (this.dateFormat != null || this.timeFormat != null) {
+ logger.warn("Setting customFormat while dateFormat and/or timeFormat are already set. " +
+ "Standard formats will be cleared to avoid conflicting format options. " +
+ "Use either dateFormat/timeFormat OR customFormat, not both.");
+ }
+ this.customFormat = customFormat;
+ this.dateFormat = null;
+ this.timeFormat = null;
+ updateDisplayAndDatetime();
+ return this;
+ }
+
+ public Timestamp displaySuffix(String displaySuffix) {
+ this.displaySuffix = displaySuffix;
+ updateDisplayAndDatetime();
+ return this;
+ }
+
+ public Timestamp is12Hour(boolean is12Hour) {
+ this.is12Hour = is12Hour;
+ updateDisplayAndDatetime();
+ return this;
+ }
+
+ public Timestamp locale(String locale) {
+ this.locale = locale;
+ updateDisplayAndDatetime();
+ return this;
+ }
+
+ public Timestamp utc(boolean shouldDisplayUTC) {
+ this.shouldDisplayUTC = shouldDisplayUTC;
+ updateDisplayAndDatetime();
+ return this;
+ }
+
+ // ------------------------------------------------------ accessors
+
+ public Date dateTime() {
+ return dateTime;
+ }
+ // ------------------------------------------------------ internal
+
+
+ /**
+ * Updates both the display text and datetime attribute of the timestamp
+ * component.
+ * This method is called whenever any formatting property changes.
+ */
+ private void updateDisplayAndDatetime() {
+ timeElement.attr(DATETIME_ATTR, formatDateTimeAttribute());
+ if (showDateTimeAsTextFlag) {
+ timeElement.text(formatDisplayText()); // important not to set via this#text()
+ }
+ element().replaceChildren(timeElement.element());
+ }
+
+ /**
+ * Formats the display text using current date and formatting options.
+ * Separated from updateDisplayAndDatetime for clarity and reusability.
+ */
+ private String formatDisplayText() {
+ Date dateToFormat = (dateTime != null) ? dateTime : new Date();
+ JsDate jsDate = new JsDate((double) dateToFormat.getTime());
+
+ String formattedText;
+ if (customFormat != null) {
+ formattedText = formatWithCustomFormat(jsDate);
+ } else {
+ formattedText = formatWithStandardOptions(jsDate);
+ }
+ if (!shouldDisplayUTC) {
+ return appendSuffix(formattedText);
+ }
+ return formattedText;
+ }
+
+ /**
+ * Formats the date using custom format options.
+ * CustomFormat settings take precedence over builder-level settings.
+ */
+ private String formatWithCustomFormat(JsDate jsDate) {
+ if (shouldDisplayUTC) {
+ return formatAsUTC(jsDate, customFormat);
+ }
+ return jsDate.toLocaleString(locale, customFormat);
+ }
+
+ /**
+ * Formats the date using standard TimestampFormat options with builder-level
+ * preferences.
+ */
+ private String formatWithStandardOptions(JsDate jsDate) {
+ LocaleOptions formatOptions = LocaleOptions.create();
+
+ if (dateFormat != null) {
+ formatOptions.dateStyle(dateFormat);
+ }
+ formatOptions.hour12(is12Hour);
+ if (timeFormat != null) {
+ formatOptions.timeStyle(timeFormat);
+ }
+ if (shouldDisplayUTC) {
+ return formatAsUTC(jsDate, formatOptions);
+ }
+ return jsDate.toLocaleString(locale, formatOptions);
+ }
+
+ private String formatAsUTC(JsDate jsDate, FormatOptions formatOptions) {
+ JsDate utcDate = convertToUtcDate(jsDate);
+ String utcDateString = utcDate.toLocaleString(locale, formatOptions);
+ return appendSuffix(utcDateString);
+ }
+
+ private JsDate convertToUtcDate(JsDate jsDate) {
+ String utcString = jsDate.toUTCString();
+ String convertToUTCString = utcString.substring(0, utcString.length() - 3);
+ return new JsDate(convertToUTCString);
+ }
+
+ /**
+ * Determines the UTC suffix to use for display.
+ * Returns the displaySuffix if set, otherwise uses the default based on
+ * timeFormat.
+ */
+ private String determineUtcSuffix() {
+ if (displaySuffix != null && !displaySuffix.isEmpty()) {
+ return displaySuffix;
+ }
+ return full.equals(timeFormat) ? COORDINATED_UNIVERSAL_TIME : UTC;
+ }
+
+ private String appendSuffix(String dateAsString) {
+ String suffixToUse = "";
+ if (shouldDisplayUTC) {
+ suffixToUse = determineUtcSuffix();
+ } else if (displaySuffix != null && !displaySuffix.isEmpty()) {
+ suffixToUse = displaySuffix;
+ }
+
+ if (!suffixToUse.isEmpty()) {
+ return dateAsString + " " + suffixToUse;
+ }
+ return dateAsString;
+ }
+
+ /**
+ * Formats the datetime attribute for semantic HTML compliance.
+ * Always uses ISO format regardless of display formatting.
+ */
+ private String formatDateTimeAttribute() {
+ Date dateToFormat = (dateTime != null) ? dateTime : new Date();
+ JsDate jsDate = new JsDate((double) dateToFormat.getTime());
+ return jsDate.toISOString();
+ }
+}
diff --git a/components/src/main/java/org/patternfly/component/timestamp/TimestampFormat.java b/components/src/main/java/org/patternfly/component/timestamp/TimestampFormat.java
new file mode 100644
index 000000000..ebf0ba1d2
--- /dev/null
+++ b/components/src/main/java/org/patternfly/component/timestamp/TimestampFormat.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 Red Hat
+ *
+ * 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
+ *
+ * https://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.
+ */
+package org.patternfly.component.timestamp;
+
+/**
+ * Enum representing various timestamp formats–for {@code dateStyle} and
+ * {@code timeStyle}–based on the Intl.DateTimeFormat
+ * API.
+ *
+ *
+ * The formats correspond to the standard JavaScript Intl.DateTimeFormat
+ * options:
+ *
+ *
+ * - {@code full} - Tuesday, August 9, 2022 | 11:25:00 AM Eastern
+ * Daylight Time
+ * - {@code long} - August 9, 2022 | 11:25:00 AM EDT
+ * - {@code medium} - Aug 9, 2022 | 11:25:00 AM
+ * - {@code short} - 8/9/22 | 11:25 AM
+ *
+ */
+public enum TimestampFormat {
+ full("full"),
+ _long("long"),
+ medium("medium"),
+ _short("short");
+
+ public final String value;
+
+ TimestampFormat(String value) {
+ this.value = value;
+ }
+}
diff --git a/core/src/main/java/org/patternfly/style/Classes.java b/core/src/main/java/org/patternfly/style/Classes.java
index 90f94ecd8..c9499c6e7 100644
--- a/core/src/main/java/org/patternfly/style/Classes.java
+++ b/core/src/main/java/org/patternfly/style/Classes.java
@@ -272,6 +272,7 @@ public interface Classes {
String thead = "thead";
String thumb = "thumb";
String tick = "tick";
+ String timestamp = "timestamp";
String title = "title";
String titleCell = "title-cell";
String toast = "toast";
diff --git a/showcase/common/src/bundle/components.json b/showcase/common/src/bundle/components.json
index 0e0bef5c6..30fc22137 100644
--- a/showcase/common/src/bundle/components.json
+++ b/showcase/common/src/bundle/components.json
@@ -617,7 +617,8 @@
"timestamp": {
"name": "timestamp",
"title": "Timestamp",
- "route": "/components/date-and-time/timestamp",
+ "route": "/components/timestamp",
+ "clazz": "org.patternfly.component.timestamp.Timestamp",
"illustration": "timestamp.png",
"summary": "A timestamp is a consistently formatted visual that displays date and time values."
},
diff --git a/showcase/common/src/main/java/org/patternfly/showcase/component/TimestampComponent.java b/showcase/common/src/main/java/org/patternfly/showcase/component/TimestampComponent.java
new file mode 100644
index 000000000..6f86a3eb1
--- /dev/null
+++ b/showcase/common/src/main/java/org/patternfly/showcase/component/TimestampComponent.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2023 Red Hat
+ *
+ * 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
+ *
+ * https://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.
+ */
+package org.patternfly.showcase.component;
+
+import java.util.Date;
+
+import org.jboss.elemento.router.Route;
+import org.patternfly.component.timestamp.CustomFormat;
+import org.patternfly.component.timestamp.TimestampFormat;
+import org.patternfly.component.timestamp.DateTimeFormatOptions.Day;
+import org.patternfly.component.timestamp.DateTimeFormatOptions.Hour;
+import org.patternfly.component.timestamp.DateTimeFormatOptions.Month;
+import org.patternfly.component.timestamp.DateTimeFormatOptions.Weekday;
+import org.patternfly.component.timestamp.DateTimeFormatOptions.Year;
+import org.patternfly.component.timestamp.Timestamp;
+import org.patternfly.showcase.Snippet;
+import org.patternfly.showcase.SnippetPage;
+
+import static org.jboss.elemento.Elements.br;
+import static org.jboss.elemento.Elements.div;
+import static org.patternfly.component.timestamp.Timestamp.timestamp;
+import static org.patternfly.showcase.ApiDoc.Type.component;
+import static org.patternfly.showcase.Code.code;
+import static org.patternfly.showcase.Data.components;
+import static org.patternfly.style.Classes.timestamp;
+
+@Route(value = "/components/timestamp", title = "Timestamp")
+public class TimestampComponent extends SnippetPage {
+
+ public TimestampComponent() {
+ super(components.get(timestamp));
+
+ startExamples();
+ addSnippet(new Snippet("timestamp-default", "Default",
+ code("timestamp-default"), () -> {
+ // @code-start:timestamp-default
+ return div()
+ .add(timestamp())
+ .add(br())
+ .add(timestamp().utc(true))
+ .add(br())
+ .add(timestamp().timeFormat(TimestampFormat._short))
+ .element();
+ // @code-end:timestamp-default
+ }));
+
+ addSnippet(new Snippet("timestamp-basic-formats", "Basic formats",
+ code("timestamp-basic-formats"), () -> {
+ // @code-start:timestamp-basic-formats
+ Date currentDateTime = new Date();
+ return div()
+ .add(timestamp().dateTime(currentDateTime)
+ .dateFormat(TimestampFormat.full)
+ .timeFormat(TimestampFormat.full))
+ .add(br()).add(br())
+ .add(timestamp().dateTime(currentDateTime)
+ .dateFormat(TimestampFormat.full))
+ .add(br()).add(br())
+ .add(timestamp().dateTime(currentDateTime)
+ .timeFormat(TimestampFormat.full))
+ .add(br()).add(br())
+ .add(timestamp().dateTime(currentDateTime)
+ .dateFormat(TimestampFormat.medium)
+ .timeFormat(TimestampFormat._short)
+ .displaySuffix("US Eastern"))
+ .element();
+ // @code-end:timestamp-basic-formats
+ }));
+
+ addSnippet(new Snippet("timestamp-custom-format", "Custom format",
+ code("timestamp-custom-format"), () -> {
+ // @code-start:timestamp-custom-format
+ Date currentDate = new Date();
+
+ return div()
+ .add(timestamp().dateTime(currentDate)
+ .customFormat(CustomFormat.create()
+ .weekday(Weekday._short)
+ .day(Day.numeric)
+ .month(Month._short)
+ .year(Year._2digit)
+ .hour(Hour._2digit)))
+ .element();
+ // @code-end:timestamp-custom-format
+ }));
+
+ addSnippet(new Snippet("timestamp-custom-content", "Custom content",
+ code("timestamp-custom-content"), () -> {
+ // @code-start:timestamp-custom-content
+ Date pastDateTime = new Date(122, 7, 9, 14, 57);
+ return div()
+ .add(timestamp("1 hour ago").dateTime(pastDateTime))
+ .add(br()).add(br())
+ .add(timestamp("Last updated August 9th, 2022 at 2:57 PM EDT")
+ .dateTime(pastDateTime))
+ .element();
+ // @code-end:timestamp-custom-content
+ }));
+
+ startApiDocs(Timestamp.class);
+ addApiDoc(Timestamp.class, component);
+ }
+}