Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
6 changes: 6 additions & 0 deletions .changeset/great-roses-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@sit-onyx/headless": minor
"sit-onyx": minor
---

feat(OnyxCalendar): implemented individual disabled dates
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const setupCalendar = (options: {
min?: Date;
max?: Date;
selection?: SelectionMode;
disabled?: boolean | ((date: Date) => boolean);
}): SetupResult => {
const modelValue = ref(options.modelValue ?? null);
const viewMonth = ref(options.viewMonth ?? new Date());
Expand All @@ -32,7 +33,7 @@ const setupCalendar = (options: {
locale: ref("en"),
calendarSize: ref("small"),
weekStartDay: ref("Monday"),
disabled: ref(false),
disabled: ref(options.disabled ?? false),
showCalendarWeeks: ref(false),
min: ref(options.min ?? null),
max: ref(options.max ?? null),
Expand Down Expand Up @@ -88,6 +89,28 @@ describe("createCalendar (Headless)", () => {
expect(internals.isDisabled.value(createDate(2025, 8, 21))).toBe(true);
});

it("should disable all dates when disabled is true", () => {
const { internals } = setupCalendar({
disabled: true,
});
const randomDate = createDate(2050, 0, 1);

expect(internals.isDisabled.value(randomDate)).toBe(true);
expect(internals.isDisabled.value(new Date())).toBe(true);
});

it("should only disable days specified by the disabled function", () => {
const disableMondays = (date: Date) => date.getDay() === 1;
const mondayDate = createDate(2025, 8, 15);
const tuesdayDate = createDate(2025, 8, 16);
const { internals } = setupCalendar({
disabled: disableMondays,
viewMonth: mondayDate,
});
expect(internals.isDisabled.value(mondayDate)).toBe(true);
expect(internals.isDisabled.value(tuesdayDate)).toBe(false);
});

it("should navigate months correctly", () => {
const { state, internals, viewMonth } = setupCalendar({ viewMonth: initialDate });

Expand Down
12 changes: 10 additions & 2 deletions packages/headless/src/composables/calendar/createCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type CreateCalendarOptions = {
weekStartDay: MaybeRefOrGetter<Weekday>;
viewMonth: Ref<DateValue>;
modelValue?: Ref<Nullable<DateValue | DateValue[] | DateRange>>;
disabled?: MaybeRefOrGetter<boolean>;
disabled?: MaybeRefOrGetter<boolean | ((date: Date) => boolean)>;
min?: MaybeRefOrGetter<Nullable<DateValue>>;
max?: MaybeRefOrGetter<Nullable<DateValue>>;
showCalendarWeeks?: MaybeRefOrGetter<boolean>;
Expand Down Expand Up @@ -130,7 +130,14 @@ export const _unstableCreateCalendar = createBuilder((options: CreateCalendarOpt

const isDisabled = computed(() => {
return (date: Date): boolean => {
if (toValue(options.disabled)) return true;
const disabledPropValue = toValue(options.disabled);

if (typeof disabledPropValue === "boolean") {
if (disabledPropValue) return true;
}
if (typeof disabledPropValue === "function") {
if (disabledPropValue(date)) return true;
}

const min = toValue(options.min);
const minDate = min ? new Date(min) : undefined;
Expand All @@ -142,6 +149,7 @@ export const _unstableCreateCalendar = createBuilder((options: CreateCalendarOpt
if (minDate && maxDate) return !isInDateRange(date, minDate, maxDate);
if (minDate) return date.getTime() < minDate.getTime();
if (maxDate) return date.getTime() > maxDate.getTime();

return false;
};
});
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test } from "../../playwright/a11y.js";
import { executeMatrixScreenshotTest } from "../../playwright/screenshots.js";
import OnyxIconButton from "../OnyxIconButton/OnyxIconButton.vue";
import OnyxCalendar from "./OnyxCalendar.vue";
import TestCaseDayContent from "./TestCaseDayContent.ct.vue";
import TestCase from "./TestCase.ct.vue";

test.describe("Screenshot tests", () => {
const testDate = new Date(2024, 9, 23);
Expand Down Expand Up @@ -96,14 +96,16 @@ test.describe("Screenshot tests", () => {
});

executeMatrixScreenshotTest({
name: "OnyxCalendar (custom content)",
name: "OnyxCalendar (custom content, custom disabled days)",
columns: ["small", "big"],
rows: ["default"],
component: (column) => {
rows: ["custom-content", "custom-disabled-days"],
component: (column, row) => {
return (
<TestCaseDayContent
<TestCase
size={column}
style={{ width: column === "small" ? "20rem" : "40rem" }}
showContent={row === "custom-content"}
disabledDays={row === "custom-disabled-days"}
/>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ export const Disabled = {
},
} satisfies Story;

export const DisabledDays = {
...createAdvancedStoryExample("OnyxCalendar", "DisabledDaysExample"),
} satisfies Story;

export const Small = {
args: {
size: "small",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,15 @@ const getDayRangeType = computed(() => {
v-if="calendarSize !== 'small'"
:label="t('calendar.todayButton.label')"
class="control-container__today-btn"
:disabled="disabled"
:disabled="disabled === true"
:clickable="t('calendar.todayButton.tooltip')"
@click="goToToday"
/>
<OnyxIconButton
:label="t('calendar.previousMonthButton')"
color="neutral"
:icon="iconChevronLeftSmall"
:disabled="disabled"
:disabled="disabled === true"
@click="goToMonthByOffset(-1)"
/>
<OnyxHeadline is="h2" class="control-container__date-display">
Expand All @@ -179,7 +179,7 @@ const getDayRangeType = computed(() => {
:label="t('calendar.nextMonthButton')"
color="neutral"
:icon="iconChevronRightSmall"
:disabled="disabled"
:disabled="disabled === true"
@click="goToMonthByOffset(1)"
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ import type { OnyxCalendarSize } from "./types.js";
type EventType = { date: Date; color: OnyxColor; description: string };

const props = defineProps<{
/**
* Whether to show custom content
*/
showContent?: boolean;
/**
* Calender Size
*/
size: OnyxCalendarSize;
/**
* disabled
*/
disabledDays?: boolean;
}>();

const testDate = new Date("2024-10-10T12:00:00Z");
Expand All @@ -29,12 +37,20 @@ const events: EventType[] = [
const getEvent = (date: Date) => {
return events.find((event) => event.date.toDateString() === date.toDateString());
};
const isDisabledDays = (date: Date) => {
return date.getDay() === 2;
};
</script>

<template>
<OnyxCalendar class="calendar" v-bind="props" :view-month="testDate">
<OnyxCalendar
class="calendar"
v-bind="props"
:view-month="testDate"
:disabled="props.disabledDays ? isDisabledDays : false"
>
<template #day="{ date, size: daySize }">
<div class="event">
<div v-if="showContent" class="event">
<OnyxBadge v-if="getEvent(date)" :color="getEvent(date)?.color" dot />
<span
v-if="daySize === 'big'"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { OnyxUnstableCalendar, type DateRange } from "../../../index.js";
import OnyxSwitch from "../../OnyxSwitch/OnyxSwitch.vue";

const selected = ref<DateRange>();
const dynamicMin = ref<Date | undefined>();
const dynamicMax = ref<Date | undefined>();
const includeDisabledDays = ref(false);

/**
* Checks if a given date is a weekend (Sunday or Saturday).
* Used to disable weekend days in the calendar.
*/
const isDisabled = (date: Date) => date.getDay() === 0 || date.getDay() === 6;

/**
* Dynamically sets the calendar's min and max constraints
* to prevent the user from selecting a range that includes a disabled day (weekend).
*/
watch(selected, (newRange) => {
if (newRange?.start && !newRange.end && !includeDisabledDays.value) {
const start = new Date(newRange.start);
const nextDisabledDay = new Date(start);
nextDisabledDay.setDate(nextDisabledDay.getDate() + 1);
while (!isDisabled(nextDisabledDay)) {
nextDisabledDay.setDate(nextDisabledDay.getDate() + 1);
}
dynamicMax.value = nextDisabledDay;
const prevDisabledDay = new Date(start);
prevDisabledDay.setDate(prevDisabledDay.getDate() - 1);
while (!isDisabled(prevDisabledDay)) {
prevDisabledDay.setDate(prevDisabledDay.getDate() - 1);
}
dynamicMin.value = prevDisabledDay;
} else {
dynamicMin.value = undefined;
dynamicMax.value = undefined;
}
});
</script>

<template>
<OnyxSwitch v-model="includeDisabledDays" label="Range is allowed to have disabled days " />
<OnyxUnstableCalendar
v-model="selected"
:disabled="isDisabled"
class="calendar"
selection-mode="range"
:min="dynamicMin"
:max="dynamicMax"
/>
</template>

<style lang="scss" scoped>
.calendar {
max-width: 45rem;
}
</style>
4 changes: 2 additions & 2 deletions packages/sit-onyx/src/components/OnyxCalendar/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type OnyxCalendarProps<TSelection extends OnyxCalendarSelectionMode> = De
/**
* Whether the calendar is disabled. Disables all interactions and prevents date selection.
*/
disabled?: boolean;
disabled?: boolean | ((date: Date) => boolean);

/**
* The earliest selectable date. Dates before this will be disabled.
Expand Down Expand Up @@ -73,7 +73,7 @@ export type OnyxWeekDays =
| "Saturday"
| "Sunday";

export type DateRange = { start: Date; end: Date };
export type DateRange = { start: Date; end?: Date };

export type OnyxCalendarValueBySelection<TSelection extends OnyxCalendarSelectionMode> =
TSelection extends "single"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ const slots = defineSlots<{
}>();

const { densityClass } = useDensity(props);
const handleHover = () => {
if (!props.disabled) {
emit("hovered");
}
};

const contentAttributes = computed(() => {
return props.is === "button"
Expand All @@ -45,8 +50,8 @@ const contentAttributes = computed(() => {
[`onyx-calendar-cell--range-${props.rangeType}`]: props.rangeType,
},
]"
@mouseenter="emit('hovered')"
@focusin="emit('hovered')"
@mouseenter="handleHover"
@focusin="handleHover"
>
<component
:is="props.is"
Expand Down
Loading