Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions resources/js/components/ui/DatePicker/DatePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ const getInputLabel = (part) => {
v-if="timeZoneLabel"
:date="modelValue.toDate()"
:additional-timezones="additionalTimezones"
:show-seconds="granularity === 'second'"
side="top"
>
<Text class="text-gray-600! dark:text-gray-400! ms-2" size="xs" :text="timeZoneLabel" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ const hoverCardDate = computed(() => {
v-if="timeZoneLabel && hoverCardDate"
:date="hoverCardDate"
:additional-timezones="additionalTimezones"
:show-seconds="granularity === 'second'"
side="top"
>
<Text class="text-gray-600! dark:text-gray-400! ms-2" size="xs" :text="timeZoneLabel" />
Expand Down
4 changes: 3 additions & 1 deletion resources/js/components/ui/TimezoneHoverCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Timezones>`. */
additionalTimezones: { type: Array, default: () => [] },
/** When `false`, times are displayed without seconds. Forwarded to `<Timezones>`. */
showSeconds: { type: Boolean, default: true },
});
</script>

Expand All @@ -15,6 +17,6 @@ const props = defineProps({
<template #trigger>
<slot />
</template>
<Timezones :date :additional-timezones="additionalTimezones" />
<Timezones :date :additional-timezones="additionalTimezones" :show-seconds="showSeconds" />
</HoverCard>
</template>
4 changes: 3 additions & 1 deletion resources/js/components/ui/Timezones.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 15 additions & 0 deletions resources/js/stories/Timezones.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<Timezones :date="date" :show-seconds="false" />`,
}),
};

const customDateCode = `
<Timezones :date="isoString" />
<Timezones :date="dateObject" />
Expand Down
4 changes: 4 additions & 0 deletions resources/js/stories/docs/Timezones.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
<Canvas of={TimezonesStories._Range} sourceState={'shown'} />

## Seconds
Seconds are displayed by default. Set `show-seconds` to `false` when they aren't relevant, like for a date picked with `minute` granularity.
<Canvas of={TimezonesStories._WithoutSeconds} sourceState={'shown'} />

## 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.
<Canvas of={TimezonesStories._HoverCard} sourceState={'shown'} />
Expand Down
14 changes: 12 additions & 2 deletions resources/js/tests/components/ui/DatePicker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -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);
});
20 changes: 18 additions & 2 deletions resources/js/tests/components/ui/DateRangePicker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -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);
});
40 changes: 40 additions & 0 deletions resources/js/tests/components/ui/Timezones.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
Loading