Skip to content

Commit 1044dda

Browse files
authored
feat: initialFocus prop for calendar components (#207)
1 parent 970c5ff commit 1044dda

File tree

9 files changed

+119
-5
lines changed

9 files changed

+119
-5
lines changed

.changeset/thick-guests-brush.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"bits-ui": patch
3+
---
4+
5+
Calendar & Range Calendar: add `initialFocus` prop to autofocus dates on mount

src/content/api-reference/calendar.ts

+6
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ export const root: APISchema<Calendar.Props> = {
117117
description: "Whether or not the calendar is readonly.",
118118
default: C.FALSE
119119
},
120+
initialFocus: {
121+
type: C.BOOLEAN,
122+
description:
123+
"If `true`, the calendar will focus the selected day, today, or the first day of the month in that order depending on what is visible when the calendar is mounted.",
124+
default: C.FALSE
125+
},
120126
asChild
121127
},
122128
slotProps: {

src/content/api-reference/range-calendar.ts

+6
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ export const root: APISchema<RangeCalendar.Props> = {
116116
description: "Whether or not the calendar is readonly.",
117117
default: C.FALSE
118118
},
119+
initialFocus: {
120+
type: C.BOOLEAN,
121+
description:
122+
"If `true`, the calendar will focus the selected day, today, or the first day of the month in that order depending on what is visible when the calendar is mounted.",
123+
default: C.FALSE
124+
},
119125
asChild
120126
},
121127
slotProps: {

src/lib/bits/calendar/_types.ts

+9
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ type Props<Multiple extends boolean = false> = Expand<
4949
* A callback function called when the placeholder changes.
5050
*/
5151
onPlaceholderChange?: OnChangeFn<DateValue>;
52+
53+
/**
54+
* If `true`, the calendar will focus the selected day,
55+
* today, or the first day of the month in that order depending
56+
* on what is visible when the calendar is mounted.
57+
*
58+
* @default false
59+
*/
60+
initialFocus?: boolean;
5261
} & AsChild
5362
>;
5463

src/lib/bits/calendar/components/calendar.svelte

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<script lang="ts">
2+
import { handleCalendarInitialFocus } from "$lib/internal/focus.js";
23
import { createDispatcher } from "$lib/internal/events.js";
3-
44
import { melt } from "@melt-ui/svelte";
5+
import { onMount } from "svelte";
56
import { setCtx, getAttrs } from "../ctx.js";
67
import type { Props } from "../types.js";
78
@@ -29,6 +30,14 @@
2930
export let asChild: $$Props["asChild"] = false;
3031
export let id: $$Props["id"] = undefined;
3132
export let numberOfMonths: $$Props["numberOfMonths"] = undefined;
33+
export let initialFocus: $$Props["initialFocus"] = false;
34+
35+
let el: HTMLElement | undefined = undefined;
36+
37+
onMount(() => {
38+
if (!initialFocus || !el) return;
39+
handleCalendarInitialFocus(el);
40+
});
3241
3342
const {
3443
elements: { calendar },
@@ -112,7 +121,12 @@
112121
{#if asChild}
113122
<slot {...slotProps} />
114123
{:else}
115-
<div use:melt={builder} {...$$restProps} on:m-keydown={dispatch}>
124+
<div
125+
use:melt={builder}
126+
{...$$restProps}
127+
on:m-keydown={dispatch}
128+
bind:this={el}
129+
>
116130
<slot {...slotProps} />
117131
</div>
118132
{/if}

src/lib/bits/range-calendar/_types.ts

+34-2
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,42 @@ type Props = Expand<
2020
| "onValueChange"
2121
| "ids"
2222
> & {
23-
placeholder?: DateValue;
23+
/**
24+
* The selected date range. This updates as the user selects
25+
* date ranges in the calendar.
26+
*
27+
* You can bind this to a value to programmatically control the
28+
* value state.
29+
*/
2430
value?: DateRange;
25-
onPlaceholderChange?: OnChangeFn<DateValue>;
31+
32+
/**
33+
* A callback function called when the value changes.
34+
*/
2635
onValueChange?: OnChangeFn<DateRange>;
36+
37+
/**
38+
* The placeholder date, used to display the calendar when no
39+
* date is selected. This updates as the user navigates
40+
* the calendar.
41+
*
42+
* You can bind this to a value to programmatically control the
43+
* placeholder state.
44+
*/
45+
placeholder?: DateValue;
46+
47+
/**
48+
* A callback function called when the placeholder changes.
49+
*/
50+
onPlaceholderChange?: OnChangeFn<DateValue>;
51+
/**
52+
* If `true`, the calendar will focus the selected day,
53+
* today, or the first day of the month in that order depending
54+
* on what is visible when the calendar is mounted.
55+
*
56+
* @default false
57+
*/
58+
initialFocus?: boolean;
2759
} & AsChild
2860
>;
2961

src/lib/bits/range-calendar/components/range-calendar.svelte

+16-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import { setCtx, getAttrs } from "../ctx.js";
44
import type { Events, Props } from "../types.js";
55
import { createDispatcher } from "$lib/internal/events.js";
6+
import { onMount } from "svelte";
7+
import { handleCalendarInitialFocus } from "$lib/internal/focus.js";
68
79
type $$Props = Props;
810
type $$Events = Events;
@@ -26,6 +28,14 @@
2628
export let asChild: $$Props["asChild"] = false;
2729
export let id: $$Props["id"] = undefined;
2830
export let weekdayFormat: $$Props["weekdayFormat"] = undefined;
31+
export let initialFocus: $$Props["initialFocus"] = false;
32+
33+
let el: HTMLElement | undefined = undefined;
34+
35+
onMount(() => {
36+
if (!initialFocus || !el) return;
37+
handleCalendarInitialFocus(el);
38+
});
2939
3040
const {
3141
elements: { calendar },
@@ -106,7 +116,12 @@
106116
{#if asChild}
107117
<slot {...slotProps} />
108118
{:else}
109-
<div use:melt={builder} {...$$restProps} on:m-keydown={dispatch}>
119+
<div
120+
use:melt={builder}
121+
{...$$restProps}
122+
on:m-keydown={dispatch}
123+
bind:this={el}
124+
>
110125
<slot {...slotProps} />
111126
</div>
112127
{/if}

src/lib/internal/focus.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { isBrowser } from ".";
2+
3+
/**
4+
* Handles `initialFocus` prop behavior for the
5+
* Calendar & RangeCalendar components.
6+
*/
7+
export function handleCalendarInitialFocus(calendar: HTMLElement) {
8+
if (!isBrowser) return;
9+
const selectedDay = calendar.querySelector<HTMLElement>("[data-selected]");
10+
if (selectedDay) return focusWithoutScroll(selectedDay);
11+
12+
const today = calendar.querySelector<HTMLElement>("[data-today]");
13+
if (today) return focusWithoutScroll(today);
14+
15+
const firstDay = calendar.querySelector<HTMLElement>("[data-calendar-date]");
16+
if (firstDay) return focusWithoutScroll(firstDay);
17+
}
18+
19+
export function focusWithoutScroll(element: HTMLElement) {
20+
const scrollPosition = {
21+
x: window.pageXOffset || document.documentElement.scrollLeft,
22+
y: window.pageYOffset || document.documentElement.scrollTop
23+
};
24+
element.focus();
25+
window.scrollTo(scrollPosition.x, scrollPosition.y);
26+
}

src/lib/internal/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from "./sleep.js";
88
export * from "./style.js";
99
export * from "./types.js";
1010
export * from "./updater.js";
11+
export * from "./focus.js";

0 commit comments

Comments
 (0)