@super-calendar/native 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/DefaultMonthEvent-86XCNCge.d.ts +326 -0
- package/dist/DefaultMonthEvent-CpRoOloh.d.mts +326 -0
- package/dist/MonthList-BCfN-UGz.js +1089 -0
- package/dist/MonthList-CyrEJ_U6.mjs +1030 -0
- package/dist/index.d.mts +386 -0
- package/dist/index.d.ts +386 -0
- package/dist/index.js +1700 -0
- package/dist/index.mjs +1543 -0
- package/dist/picker.d.mts +3 -0
- package/dist/picker.d.ts +3 -0
- package/dist/picker.js +96 -0
- package/dist/picker.mjs +3 -0
- package/package.json +92 -0
- package/src/components/Agenda.tsx +133 -0
- package/src/components/AllDayLane.tsx +98 -0
- package/src/components/Calendar.tsx +456 -0
- package/src/components/DefaultEvent.tsx +223 -0
- package/src/components/DefaultMonthEvent.tsx +105 -0
- package/src/components/MonthList.tsx +570 -0
- package/src/components/MonthPager.tsx +377 -0
- package/src/components/MonthView.tsx +518 -0
- package/src/components/TimeGrid.tsx +1943 -0
- package/src/index.tsx +70 -0
- package/src/picker.tsx +55 -0
- package/src/theme.ts +86 -0
- package/src/types.ts +62 -0
- package/src/utils/useWebGridZoom.ts +59 -0
- package/src/utils/useWebPagerKeys.ts +56 -0
package/src/index.tsx
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export { Calendar, type CalendarProps } from "./components/Calendar";
|
|
2
|
+
export { Agenda, type AgendaProps } from "./components/Agenda";
|
|
3
|
+
export { MonthView, type MonthViewProps } from "./components/MonthView";
|
|
4
|
+
export { MonthPager, type MonthPagerProps } from "./components/MonthPager";
|
|
5
|
+
export { MonthList, type MonthListProps } from "./components/MonthList";
|
|
6
|
+
export {
|
|
7
|
+
TimeGrid,
|
|
8
|
+
type TimeGridProps,
|
|
9
|
+
type EventDragHandler,
|
|
10
|
+
type EventDragStartHandler,
|
|
11
|
+
type HourRenderer,
|
|
12
|
+
DEFAULT_HOUR_HEIGHT,
|
|
13
|
+
} from "./components/TimeGrid";
|
|
14
|
+
export { DefaultEvent } from "./components/DefaultEvent";
|
|
15
|
+
export { DefaultMonthEvent } from "./components/DefaultMonthEvent";
|
|
16
|
+
export {
|
|
17
|
+
type CalendarTheme,
|
|
18
|
+
type PartialCalendarTheme,
|
|
19
|
+
defaultTheme,
|
|
20
|
+
darkTheme,
|
|
21
|
+
mergeTheme,
|
|
22
|
+
CalendarThemeProvider,
|
|
23
|
+
useCalendarTheme,
|
|
24
|
+
} from "./theme";
|
|
25
|
+
export type {
|
|
26
|
+
BusinessHours,
|
|
27
|
+
CalendarEvent,
|
|
28
|
+
CalendarMode,
|
|
29
|
+
EventKeyExtractor,
|
|
30
|
+
ICalendarEvent,
|
|
31
|
+
RecurrenceFrequency,
|
|
32
|
+
RecurrenceRule,
|
|
33
|
+
RenderEvent,
|
|
34
|
+
RenderEventArgs,
|
|
35
|
+
TimeGridMode,
|
|
36
|
+
WeekStartsOn,
|
|
37
|
+
} from "./types";
|
|
38
|
+
export {
|
|
39
|
+
type DateRange,
|
|
40
|
+
type DateSelectionConstraints,
|
|
41
|
+
type DaySelectionState,
|
|
42
|
+
type UseDateRangeOptions,
|
|
43
|
+
daySelectionState,
|
|
44
|
+
isDateSelectable,
|
|
45
|
+
isRangeEndpoint,
|
|
46
|
+
isWithinDateRange,
|
|
47
|
+
nextDateRange,
|
|
48
|
+
useDateRange,
|
|
49
|
+
} from "@super-calendar/core";
|
|
50
|
+
export {
|
|
51
|
+
buildMonthGrid,
|
|
52
|
+
type MonthGrid,
|
|
53
|
+
type MonthGridDay,
|
|
54
|
+
type MonthGridWeek,
|
|
55
|
+
type MonthGridWeekday,
|
|
56
|
+
type UseMonthGridOptions,
|
|
57
|
+
useMonthGrid,
|
|
58
|
+
} from "@super-calendar/core";
|
|
59
|
+
export { expandRecurringEvents } from "@super-calendar/core";
|
|
60
|
+
export { eventsInTimeZone, toZonedTime } from "@super-calendar/core";
|
|
61
|
+
export {
|
|
62
|
+
buildMonthWeeks,
|
|
63
|
+
getViewDays,
|
|
64
|
+
getWeekDays,
|
|
65
|
+
getIsToday,
|
|
66
|
+
isWeekend,
|
|
67
|
+
isSameCalendarDay,
|
|
68
|
+
minutesIntoDay,
|
|
69
|
+
} from "@super-calendar/core";
|
|
70
|
+
export { layoutDayEvents, isAllDayEvent, type PositionedEvent } from "@super-calendar/core";
|
package/src/picker.tsx
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Date-picker entry point: the month grid, selection, and the headless grid,
|
|
2
|
+
// with no dependency on the timetable views or Reanimated. Import from
|
|
3
|
+
// "react-native-super-calendar/picker" for a picker-only bundle that doesn't
|
|
4
|
+
// require react-native-reanimated. The main entry re-exports everything.
|
|
5
|
+
export { MonthView, type MonthViewProps } from "./components/MonthView";
|
|
6
|
+
export { MonthPager, type MonthPagerProps } from "./components/MonthPager";
|
|
7
|
+
export { MonthList, type MonthListProps } from "./components/MonthList";
|
|
8
|
+
export { DefaultMonthEvent } from "./components/DefaultMonthEvent";
|
|
9
|
+
export {
|
|
10
|
+
type CalendarTheme,
|
|
11
|
+
type PartialCalendarTheme,
|
|
12
|
+
defaultTheme,
|
|
13
|
+
darkTheme,
|
|
14
|
+
mergeTheme,
|
|
15
|
+
CalendarThemeProvider,
|
|
16
|
+
useCalendarTheme,
|
|
17
|
+
} from "./theme";
|
|
18
|
+
export type {
|
|
19
|
+
CalendarEvent,
|
|
20
|
+
CalendarMode,
|
|
21
|
+
EventKeyExtractor,
|
|
22
|
+
ICalendarEvent,
|
|
23
|
+
RenderEvent,
|
|
24
|
+
RenderEventArgs,
|
|
25
|
+
WeekStartsOn,
|
|
26
|
+
} from "./types";
|
|
27
|
+
export {
|
|
28
|
+
type DateRange,
|
|
29
|
+
type DateSelectionConstraints,
|
|
30
|
+
type DaySelectionState,
|
|
31
|
+
type UseDateRangeOptions,
|
|
32
|
+
daySelectionState,
|
|
33
|
+
isDateSelectable,
|
|
34
|
+
isRangeEndpoint,
|
|
35
|
+
isWithinDateRange,
|
|
36
|
+
nextDateRange,
|
|
37
|
+
useDateRange,
|
|
38
|
+
} from "@super-calendar/core";
|
|
39
|
+
export {
|
|
40
|
+
buildMonthGrid,
|
|
41
|
+
type MonthGrid,
|
|
42
|
+
type MonthGridDay,
|
|
43
|
+
type MonthGridWeek,
|
|
44
|
+
type MonthGridWeekday,
|
|
45
|
+
type UseMonthGridOptions,
|
|
46
|
+
useMonthGrid,
|
|
47
|
+
} from "@super-calendar/core";
|
|
48
|
+
export {
|
|
49
|
+
buildMonthWeeks,
|
|
50
|
+
getWeekDays,
|
|
51
|
+
getIsToday,
|
|
52
|
+
isWeekend,
|
|
53
|
+
isSameCalendarDay,
|
|
54
|
+
minutesIntoDay,
|
|
55
|
+
} from "@super-calendar/core";
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type CalendarColors, darkColors, lightColors } from "@super-calendar/core";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
import type { TextStyle } from "react-native";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The full set of colours, text styles and metrics the calendar paints with.
|
|
7
|
+
* Supply a `Partial<CalendarTheme>` to `<Calendar theme={...} />`; missing keys
|
|
8
|
+
* fall back to {@link defaultTheme}, so you only override what you care about.
|
|
9
|
+
*/
|
|
10
|
+
export interface CalendarTheme {
|
|
11
|
+
/** The shared colour palette (sourced from `@super-calendar/core`). */
|
|
12
|
+
colors: CalendarColors;
|
|
13
|
+
text: {
|
|
14
|
+
/** Large day number in the week/day header. */
|
|
15
|
+
dayNumber: TextStyle;
|
|
16
|
+
/** Short weekday label ("Mon") in headers. */
|
|
17
|
+
weekday: TextStyle;
|
|
18
|
+
/** Date number inside a month cell. */
|
|
19
|
+
dateCell: TextStyle;
|
|
20
|
+
/** Hour labels down the left of the time grid. */
|
|
21
|
+
hourLabel: TextStyle;
|
|
22
|
+
/** The "+N more" overflow label in month cells. */
|
|
23
|
+
more: TextStyle;
|
|
24
|
+
/** Title inside the built-in default event box. */
|
|
25
|
+
eventTitle: TextStyle;
|
|
26
|
+
};
|
|
27
|
+
/** Corner radius of the today badge. Use a large value for a circle. */
|
|
28
|
+
todayBadgeRadius: number;
|
|
29
|
+
/**
|
|
30
|
+
* Height of the range selection band (the centered rounded "pill"); its corner
|
|
31
|
+
* radius is half this. Ignored when `fillCellOnSelection` fills the whole cell.
|
|
32
|
+
*/
|
|
33
|
+
rangeBandHeight: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const defaultTheme: CalendarTheme = {
|
|
37
|
+
colors: lightColors,
|
|
38
|
+
text: {
|
|
39
|
+
dayNumber: { fontSize: 22, fontWeight: "700" },
|
|
40
|
+
weekday: { fontSize: 13, fontWeight: "700" },
|
|
41
|
+
dateCell: { fontSize: 13, fontWeight: "700" },
|
|
42
|
+
hourLabel: { fontSize: 10 },
|
|
43
|
+
more: { fontSize: 11, fontWeight: "700" },
|
|
44
|
+
// Explicit lineHeight so the timed-grid renderer can clamp the title to a
|
|
45
|
+
// whole number of lines (clipping on a line boundary, never mid-line).
|
|
46
|
+
eventTitle: { fontSize: 12, fontWeight: "700", lineHeight: 16 },
|
|
47
|
+
},
|
|
48
|
+
todayBadgeRadius: 999,
|
|
49
|
+
rangeBandHeight: 22,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A ready-made dark palette. Pass it straight to `<Calendar theme={darkTheme} />`,
|
|
54
|
+
* or switch on the system scheme with React Native's `useColorScheme()`. Shares
|
|
55
|
+
* {@link defaultTheme}'s typography and metrics; only the colours change.
|
|
56
|
+
*/
|
|
57
|
+
export const darkTheme: CalendarTheme = {
|
|
58
|
+
colors: darkColors,
|
|
59
|
+
text: defaultTheme.text,
|
|
60
|
+
todayBadgeRadius: defaultTheme.todayBadgeRadius,
|
|
61
|
+
rangeBandHeight: defaultTheme.rangeBandHeight,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Deep-merge a partial theme over {@link defaultTheme}. */
|
|
65
|
+
export function mergeTheme(theme?: PartialCalendarTheme): CalendarTheme {
|
|
66
|
+
if (!theme) return defaultTheme;
|
|
67
|
+
return {
|
|
68
|
+
colors: { ...defaultTheme.colors, ...theme.colors },
|
|
69
|
+
text: { ...defaultTheme.text, ...theme.text },
|
|
70
|
+
todayBadgeRadius: theme.todayBadgeRadius ?? defaultTheme.todayBadgeRadius,
|
|
71
|
+
rangeBandHeight: theme.rangeBandHeight ?? defaultTheme.rangeBandHeight,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type PartialCalendarTheme = {
|
|
76
|
+
colors?: Partial<CalendarTheme["colors"]>;
|
|
77
|
+
text?: Partial<CalendarTheme["text"]>;
|
|
78
|
+
todayBadgeRadius?: number;
|
|
79
|
+
rangeBandHeight?: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const CalendarThemeContext = createContext<CalendarTheme>(defaultTheme);
|
|
83
|
+
|
|
84
|
+
export const CalendarThemeProvider = CalendarThemeContext.Provider;
|
|
85
|
+
|
|
86
|
+
export const useCalendarTheme = () => useContext(CalendarThemeContext);
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
import type { StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
4
|
+
import type { CalendarEvent, CalendarMode } from "@super-calendar/core";
|
|
5
|
+
|
|
6
|
+
// Re-export the renderer-agnostic types from core so native modules can keep
|
|
7
|
+
// importing them from "../types".
|
|
8
|
+
export type {
|
|
9
|
+
BusinessHours,
|
|
10
|
+
CalendarEvent,
|
|
11
|
+
CalendarMode,
|
|
12
|
+
EventKeyExtractor,
|
|
13
|
+
ICalendarEvent,
|
|
14
|
+
RecurrenceFrequency,
|
|
15
|
+
RecurrenceRule,
|
|
16
|
+
TimeGridMode,
|
|
17
|
+
WeekStartsOn,
|
|
18
|
+
} from "@super-calendar/core";
|
|
19
|
+
|
|
20
|
+
// The React Native render contract lives here, not in core: it references
|
|
21
|
+
// Reanimated (`SharedValue`) and React Native (`ViewStyle`) types, which must
|
|
22
|
+
// not leak into the platform-free core.
|
|
23
|
+
export type RenderEventArgs<T = unknown> = {
|
|
24
|
+
event: CalendarEvent<T>;
|
|
25
|
+
mode: CalendarMode;
|
|
26
|
+
/**
|
|
27
|
+
* Live pixel height of the event box on the week/day grid, driven on the UI
|
|
28
|
+
* thread by pinch-to-zoom. Use it to reveal detail progressively as the box
|
|
29
|
+
* grows. `undefined` in month mode, where events render at a fixed size.
|
|
30
|
+
*/
|
|
31
|
+
boxHeight?: SharedValue<number>;
|
|
32
|
+
/**
|
|
33
|
+
* On the week/day grid, true when this is a clipped segment of a multi-day
|
|
34
|
+
* event that started on an earlier day / continues onto a later day. Lets a
|
|
35
|
+
* renderer draw "continues" affordances. `undefined` in month mode.
|
|
36
|
+
*/
|
|
37
|
+
continuesBefore?: boolean;
|
|
38
|
+
continuesAfter?: boolean;
|
|
39
|
+
/** True when this event is rendered in the all-day lane (week/day) or is an all-day event in month view. */
|
|
40
|
+
isAllDay?: boolean;
|
|
41
|
+
/** Format the built-in renderer's time range in 12-hour AM/PM. Default false (24h). */
|
|
42
|
+
ampm?: boolean;
|
|
43
|
+
/** Show the time range in the built-in renderer (day/week/schedule). Default true. */
|
|
44
|
+
showTime?: boolean;
|
|
45
|
+
/** Add a trailing ellipsis (…) when a clipped title overflows in the built-in renderer; otherwise the text is hard-clipped. Default false. */
|
|
46
|
+
ellipsizeTitle?: boolean;
|
|
47
|
+
/** Label shown for an all-day event in the schedule (and its screen-reader text). Default "All day". */
|
|
48
|
+
allDayLabel?: string;
|
|
49
|
+
/** Per-event style resolved from `eventCellStyle`; the built-in renderer merges it onto the box. */
|
|
50
|
+
cellStyle?: StyleProp<ViewStyle>;
|
|
51
|
+
onPress: () => void;
|
|
52
|
+
/** Wired when the consumer passes `onLongPressEvent`; otherwise undefined. */
|
|
53
|
+
onLongPress?: () => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A component that renders a single event. It is rendered as a real component
|
|
58
|
+
* (not called as a function), so it may safely use hooks — including Reanimated
|
|
59
|
+
* hooks driven by `boxHeight`. Render an element that fills its container
|
|
60
|
+
* (`flex: 1`); the calendar positions and sizes the wrapping box for you.
|
|
61
|
+
*/
|
|
62
|
+
export type RenderEvent<T = unknown> = ComponentType<RenderEventArgs<T>>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
4
|
+
|
|
5
|
+
// Minimal DOM shapes so this compiles without the TS "DOM" lib (the library
|
|
6
|
+
// targets React Native). Only the bits we touch are typed.
|
|
7
|
+
type WheelEventLike = {
|
|
8
|
+
ctrlKey: boolean;
|
|
9
|
+
metaKey: boolean;
|
|
10
|
+
deltaY: number;
|
|
11
|
+
preventDefault: () => void;
|
|
12
|
+
};
|
|
13
|
+
type WheelTarget = {
|
|
14
|
+
addEventListener: (
|
|
15
|
+
type: "wheel",
|
|
16
|
+
listener: (event: WheelEventLike) => void,
|
|
17
|
+
options?: { passive?: boolean },
|
|
18
|
+
) => void;
|
|
19
|
+
removeEventListener: (type: "wheel", listener: (event: WheelEventLike) => void) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// A scroll notch scales the row height by this ratio; matching pinch's feel.
|
|
23
|
+
const ZOOM_SENSITIVITY = 0.002;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Web-only: zoom the time grid with Ctrl/Cmd + scroll, standing in for the pinch
|
|
27
|
+
* gesture that web has no equivalent of. `target` is the grid's host node — on
|
|
28
|
+
* web a React Native View ref resolves to its DOM element. No-op off web, when
|
|
29
|
+
* `enabled` is false, or before the node mounts. Plain scroll is left untouched.
|
|
30
|
+
*/
|
|
31
|
+
export function useWebGridZoom(
|
|
32
|
+
enabled: boolean,
|
|
33
|
+
target: { current: unknown },
|
|
34
|
+
cellHeight: SharedValue<number>,
|
|
35
|
+
committedCellHeight: SharedValue<number>,
|
|
36
|
+
minHeight: number,
|
|
37
|
+
maxHeight: number,
|
|
38
|
+
): void {
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (Platform.OS !== "web" || !enabled) return;
|
|
41
|
+
const node = target.current as WheelTarget | null;
|
|
42
|
+
if (!node) return;
|
|
43
|
+
const handler = (event: WheelEventLike) => {
|
|
44
|
+
if (!event.ctrlKey && !event.metaKey) return;
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
// Exponential so each notch scales by a constant ratio, like pinch.
|
|
47
|
+
const next = Math.min(
|
|
48
|
+
maxHeight,
|
|
49
|
+
Math.max(minHeight, cellHeight.value * Math.exp(-event.deltaY * ZOOM_SENSITIVITY)),
|
|
50
|
+
);
|
|
51
|
+
// eslint-disable-next-line react-hooks/immutability -- Reanimated shared value: assigning .value is the intended mutation API
|
|
52
|
+
cellHeight.value = next;
|
|
53
|
+
// eslint-disable-next-line react-hooks/immutability -- Reanimated shared value: assigning .value is the intended mutation API
|
|
54
|
+
committedCellHeight.value = next;
|
|
55
|
+
};
|
|
56
|
+
node.addEventListener("wheel", handler, { passive: false });
|
|
57
|
+
return () => node.removeEventListener("wheel", handler);
|
|
58
|
+
}, [enabled, target, cellHeight, committedCellHeight, minHeight, maxHeight]);
|
|
59
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
// Minimal DOM shapes so this compiles without the TS "DOM" lib (the library
|
|
5
|
+
// targets React Native). Only the bits we touch are typed.
|
|
6
|
+
type FocusedElement = { tagName?: string; isContentEditable?: boolean } | null;
|
|
7
|
+
type KeyEvent = {
|
|
8
|
+
key: string;
|
|
9
|
+
defaultPrevented: boolean;
|
|
10
|
+
metaKey: boolean;
|
|
11
|
+
ctrlKey: boolean;
|
|
12
|
+
altKey: boolean;
|
|
13
|
+
preventDefault: () => void;
|
|
14
|
+
};
|
|
15
|
+
type WebDocument = {
|
|
16
|
+
activeElement: FocusedElement;
|
|
17
|
+
addEventListener: (type: "keydown", listener: (event: KeyEvent) => void) => void;
|
|
18
|
+
removeEventListener: (type: "keydown", listener: (event: KeyEvent) => void) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const getDocument = (): WebDocument | undefined =>
|
|
22
|
+
(globalThis as { document?: WebDocument }).document;
|
|
23
|
+
|
|
24
|
+
// Don't hijack arrow keys while the user is typing or moving a caret.
|
|
25
|
+
const isTextEntry = (element: FocusedElement): boolean => {
|
|
26
|
+
const tag = element?.tagName;
|
|
27
|
+
return (
|
|
28
|
+
tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || element?.isContentEditable === true
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Web-only: page the calendar with the ← / → arrow keys, standing in for the
|
|
34
|
+
* horizontal swipe that the grid disables on web. No-op off web or when
|
|
35
|
+
* `enabled` is false. `onPage(delta)` receives -1 (previous) or +1 (next).
|
|
36
|
+
*/
|
|
37
|
+
export function useWebPagerKeys(enabled: boolean, onPage: (delta: number) => void): void {
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (Platform.OS !== "web" || !enabled) return;
|
|
40
|
+
const doc = getDocument();
|
|
41
|
+
if (!doc) return;
|
|
42
|
+
const handler = (event: KeyEvent) => {
|
|
43
|
+
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
44
|
+
if (isTextEntry(doc.activeElement)) return;
|
|
45
|
+
if (event.key === "ArrowRight") {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
onPage(1);
|
|
48
|
+
} else if (event.key === "ArrowLeft") {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
onPage(-1);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
doc.addEventListener("keydown", handler);
|
|
54
|
+
return () => doc.removeEventListener("keydown", handler);
|
|
55
|
+
}, [enabled, onPage]);
|
|
56
|
+
}
|