diff --git a/resources/js/components/ui/DatePicker/DatePicker.vue b/resources/js/components/ui/DatePicker/DatePicker.vue
index bdc9930e96d..055c5f2861b 100644
--- a/resources/js/components/ui/DatePicker/DatePicker.vue
+++ b/resources/js/components/ui/DatePicker/DatePicker.vue
@@ -211,6 +211,7 @@ const getInputLabel = (part) => {
v-if="timeZoneLabel"
:date="modelValue.toDate()"
:additional-timezones="additionalTimezones"
+ :show-seconds="granularity === 'second'"
side="top"
>
diff --git a/resources/js/components/ui/DateRangePicker/DateRangePicker.vue b/resources/js/components/ui/DateRangePicker/DateRangePicker.vue
index 71bcca14d94..e02975927d8 100644
--- a/resources/js/components/ui/DateRangePicker/DateRangePicker.vue
+++ b/resources/js/components/ui/DateRangePicker/DateRangePicker.vue
@@ -194,6 +194,7 @@ const hoverCardDate = computed(() => {
v-if="timeZoneLabel && hoverCardDate"
:date="hoverCardDate"
:additional-timezones="additionalTimezones"
+ :show-seconds="granularity === 'second'"
side="top"
>
diff --git a/resources/js/components/ui/TimezoneHoverCard.vue b/resources/js/components/ui/TimezoneHoverCard.vue
index 0a8a2cd85ce..e732ed21d65 100644
--- a/resources/js/components/ui/TimezoneHoverCard.vue
+++ b/resources/js/components/ui/TimezoneHoverCard.vue
@@ -7,6 +7,8 @@ const props = defineProps({
date: { type: [String, Date, Number, Object], required: true },
/** Extra `{ timezone, label }` rows to render above the defaults. Forwarded to ``. */
additionalTimezones: { type: Array, default: () => [] },
+ /** When `false`, times are displayed without seconds. Forwarded to ``. */
+ showSeconds: { type: Boolean, default: true },
});
@@ -15,6 +17,6 @@ const props = defineProps({
-
+
diff --git a/resources/js/components/ui/Timezones.vue b/resources/js/components/ui/Timezones.vue
index 08fc4a75883..a33ea09242a 100644
--- a/resources/js/components/ui/Timezones.vue
+++ b/resources/js/components/ui/Timezones.vue
@@ -10,6 +10,8 @@ const props = defineProps({
date: { type: [String, Date, Number, Object], required: true },
/** Extra `{ timezone, label }` rows to render above the defaults. */
additionalTimezones: { type: Array, default: () => [] },
+ /** When `false`, times are displayed without seconds. */
+ showSeconds: { type: Boolean, default: true },
});
const isRange = computed(() => {
@@ -40,7 +42,7 @@ const formatTimeZone = (timeZone) => {
const formatDateTime = (timeZone) => {
const formatter = new Intl.DateTimeFormat(dateFormatter.locale, {
dateStyle: isRange.value ? 'short' : 'medium',
- timeStyle: 'medium',
+ timeStyle: props.showSeconds ? 'medium' : 'short',
timeZone,
});
return isRange.value ? formatter.formatRange(start.value, end.value) : formatter.format(start.value);
diff --git a/resources/js/stories/Timezones.stories.ts b/resources/js/stories/Timezones.stories.ts
index da2854a79c5..40d6a5df151 100644
--- a/resources/js/stories/Timezones.stories.ts
+++ b/resources/js/stories/Timezones.stories.ts
@@ -91,6 +91,21 @@ export const _Range: Story = {
}),
};
+export const _WithoutSeconds: Story = {
+ tags: ['!dev'],
+ args: {
+ date: exampleDate,
+ },
+ render: (args) => ({
+ components: { Timezones },
+ setup() {
+ const date = args.date;
+ return { date };
+ },
+ template: ``,
+ }),
+};
+
const customDateCode = `
diff --git a/resources/js/stories/docs/Timezones.mdx b/resources/js/stories/docs/Timezones.mdx
index 5c1f60e253b..2bd98a2a3cf 100644
--- a/resources/js/stories/docs/Timezones.mdx
+++ b/resources/js/stories/docs/Timezones.mdx
@@ -13,6 +13,10 @@ Displays a given instant rendered across the user's local timezone, the configur
Pass a `{ start, end }` object to render a date range. The value column uses locale-aware formatting that collapses repeated parts (e.g. shared month or year).
+## Seconds
+Seconds are displayed by default. Set `show-seconds` to `false` when they aren't relevant, like for a date picked with `minute` granularity.
+
+
## Hover Card
This is a convenience wrapper component that displays the timezones in a hover card. You can provide any of [HoverCard](?path=/docs/overlays-hovercard--docs)'s props.
diff --git a/resources/js/tests/components/ui/DatePicker.test.js b/resources/js/tests/components/ui/DatePicker.test.js
index 08f27bf91c4..55a905ed09c 100644
--- a/resources/js/tests/components/ui/DatePicker.test.js
+++ b/resources/js/tests/components/ui/DatePicker.test.js
@@ -2,15 +2,16 @@ import { mount } from '@vue/test-utils';
import { expect, test } from 'vitest';
import { CalendarDateTime, toZoned } from '@internationalized/date';
import DatePicker from '@/components/ui/DatePicker/DatePicker.vue';
+import TimezoneHoverCard from '@/components/ui/TimezoneHoverCard.vue';
import { config } from '@api';
window.__ = (key) => key;
config.initialize({ translationLocale: 'en' });
-const makeDatePicker = (modelValue) => {
+const makeDatePicker = (modelValue, granularity = 'minute') => {
return mount(DatePicker, {
- props: { modelValue, granularity: 'minute' },
+ props: { modelValue, granularity },
global: {
mocks: {
$date: { locale: 'en-US' },
@@ -38,3 +39,12 @@ test('typing an hour keeps the day period of the zoned date', async () => {
expect(datePicker.emitted('update:modelValue')[0][0].toString()).toBe(modelValue.set({ hour: 17 }).toString());
});
+
+test.each([
+ ['minute', false],
+ ['second', true],
+])('the timezone hover card with %s granularity shows seconds: %s', (granularity, showSeconds) => {
+ const datePicker = makeDatePicker(toZoned(new CalendarDateTime(2024, 1, 20, 15, 30, 27), 'Europe/London'), granularity);
+
+ expect(datePicker.findComponent(TimezoneHoverCard).props('showSeconds')).toBe(showSeconds);
+});
diff --git a/resources/js/tests/components/ui/DateRangePicker.test.js b/resources/js/tests/components/ui/DateRangePicker.test.js
index dcb02368e44..f51fb88a669 100644
--- a/resources/js/tests/components/ui/DateRangePicker.test.js
+++ b/resources/js/tests/components/ui/DateRangePicker.test.js
@@ -2,15 +2,16 @@ import { mount } from '@vue/test-utils';
import { expect, test } from 'vitest';
import { CalendarDateTime, toZoned } from '@internationalized/date';
import DateRangePicker from '@/components/ui/DateRangePicker/DateRangePicker.vue';
+import TimezoneHoverCard from '@/components/ui/TimezoneHoverCard.vue';
import { config } from '@api';
window.__ = (key) => key;
config.initialize({ translationLocale: 'en' });
-const makeDateRangePicker = (modelValue) => {
+const makeDateRangePicker = (modelValue, granularity = 'minute') => {
return mount(DateRangePicker, {
- props: { modelValue, granularity: 'minute' },
+ props: { modelValue, granularity },
global: {
mocks: {
$date: { locale: 'en-US' },
@@ -60,3 +61,18 @@ test('day period reflects the zoned date rather than the browser timezone when o
expect(dateRangePicker.get(segment('end', 'hour')).text()).toBe('4');
expect(dateRangePicker.get(segment('end', 'dayPeriod')).text()).toBe('PM');
});
+
+test.each([
+ ['minute', false],
+ ['second', true],
+])('the timezone hover card with %s granularity shows seconds: %s', (granularity, showSeconds) => {
+ const dateRangePicker = makeDateRangePicker(
+ {
+ start: toZoned(new CalendarDateTime(2024, 1, 20, 15, 30, 27), 'Europe/London'),
+ end: toZoned(new CalendarDateTime(2024, 1, 21, 16, 30, 27), 'Europe/London'),
+ },
+ granularity,
+ );
+
+ expect(dateRangePicker.findComponent(TimezoneHoverCard).props('showSeconds')).toBe(showSeconds);
+});
diff --git a/resources/js/tests/components/ui/Timezones.test.js b/resources/js/tests/components/ui/Timezones.test.js
new file mode 100644
index 00000000000..597d7b2fab7
--- /dev/null
+++ b/resources/js/tests/components/ui/Timezones.test.js
@@ -0,0 +1,40 @@
+import { mount } from '@vue/test-utils';
+import { expect, test } from 'vitest';
+import Timezones from '@/components/ui/Timezones.vue';
+import DateFormatter from '@/components/DateFormatter.js';
+import { config } from '@api';
+
+config.initialize({ translationLocale: 'en', displayTimezone: 'UTC' });
+DateFormatter.defaultLocale = 'en-US';
+
+const makeTimezones = (props) => mount(Timezones, { props });
+
+const date = '2026-05-05T12:34:27.000Z';
+const range = { start: '2026-05-05T12:34:27.000Z', end: '2026-05-08T17:30:42.000Z' };
+
+test('seconds are shown by default', () => {
+ expect(makeTimezones({ date }).text()).toContain('12:34:27');
+});
+
+test('seconds can be hidden', () => {
+ const text = makeTimezones({ date, showSeconds: false }).text();
+
+ expect(text).toContain('12:34');
+ expect(text).not.toContain(':27');
+});
+
+test('seconds are shown for ranges by default', () => {
+ const text = makeTimezones({ date: range }).text();
+
+ expect(text).toContain('12:34:27');
+ expect(text).toContain('5:30:42');
+});
+
+test('seconds can be hidden for ranges', () => {
+ const text = makeTimezones({ date: range, showSeconds: false }).text();
+
+ expect(text).toContain('12:34');
+ expect(text).toContain('5:30');
+ expect(text).not.toContain(':27');
+ expect(text).not.toContain(':42');
+});