@super-calendar/native 2.1.5 → 2.3.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.
@@ -63,8 +63,11 @@ import {
63
63
  snapDeltaMinutes,
64
64
  } from "@super-calendar/core";
65
65
  import { formatHour, layoutDayEvents, type PositionedEvent } from "@super-calendar/core";
66
+ import type { EventAccessibilityLabeler } from "@super-calendar/core";
67
+ import { type WeekdayFormat, weekdayFormatToken } from "@super-calendar/core";
66
68
  import { useWebGridZoom } from "../utils/useWebGridZoom";
67
69
  import { useWebPagerKeys } from "../utils/useWebPagerKeys";
70
+ import { withEventAccessibilityLabel } from "../utils/withEventAccessibilityLabel";
68
71
  import { AllDayLane } from "./AllDayLane";
69
72
 
70
73
  // Horizontal swipe paging doesn't translate to web; there we disable it and page
@@ -370,7 +373,9 @@ function AnimatedEventBox<T>({
370
373
  !draggable && onLongPress ? () => onLongPress(positioned.event) : undefined;
371
374
 
372
375
  const box = (
373
- <Animated.View style={[styles.eventBox, { left, width }, boxStyle]}>
376
+ <Animated.View
377
+ style={[styles.eventBox, { left, width }, boxStyle, theme.containers.timeGridEvent]}
378
+ >
374
379
  <RenderEventComponent
375
380
  event={positioned.event}
376
381
  mode={mode}
@@ -493,6 +498,7 @@ type NowIndicatorProps = {
493
498
  };
494
499
 
495
500
  const NowIndicator = ({ cellHeight, nowHours, minHour, left, width, color }: NowIndicatorProps) => {
501
+ const theme = useCalendarTheme();
496
502
  const animatedStyle = useAnimatedStyle(
497
503
  () => ({ top: (nowHours - minHour) * cellHeight.value }),
498
504
  [nowHours, minHour],
@@ -505,6 +511,7 @@ const NowIndicator = ({ cellHeight, nowHours, minHour, left, width, color }: Now
505
511
  styles.nonInteractive,
506
512
  { left, width, backgroundColor: color },
507
513
  animatedStyle,
514
+ theme.containers.nowIndicator,
508
515
  ]}
509
516
  />
510
517
  );
@@ -1179,7 +1186,15 @@ export type TimeGridProps<T> = {
1179
1186
  /** Initial per-hour row height in px; seeds scroll/zoom without reading the shared value during render. */
1180
1187
  hourHeight?: number;
1181
1188
  weekStartsOn: WeekStartsOn;
1189
+ /** Column-header weekday label width: `narrow` ("M"), `short` ("Mon", default), or `long` ("Monday"). */
1190
+ weekdayFormat?: WeekdayFormat;
1182
1191
  renderEvent: RenderEvent<T>;
1192
+ /**
1193
+ * Override the screen-reader label for each event. Receives the event and a
1194
+ * `{ mode, isAllDay, ampm }` context; return the full text to announce. Defaults
1195
+ * to the built-in title-and-time label.
1196
+ */
1197
+ eventAccessibilityLabel?: EventAccessibilityLabeler<T>;
1183
1198
  keyExtractor: EventKeyExtractor<T>;
1184
1199
  scrollOffsetMinutes?: number;
1185
1200
  hourColumnWidth?: number;
@@ -1258,7 +1273,9 @@ function TimeGridInner<T>({
1258
1273
  cellHeight,
1259
1274
  hourHeight = DEFAULT_HOUR_HEIGHT,
1260
1275
  weekStartsOn,
1276
+ weekdayFormat = "short",
1261
1277
  renderEvent,
1278
+ eventAccessibilityLabel,
1262
1279
  keyExtractor,
1263
1280
  scrollOffsetMinutes = 0,
1264
1281
  hourColumnWidth: hourColumnWidthProp = DEFAULT_HOUR_COLUMN_WIDTH,
@@ -1304,6 +1321,14 @@ function TimeGridInner<T>({
1304
1321
  // Collapse the hour gutter to zero when hours are hidden.
1305
1322
  const hourColumnWidth = hideHours ? 0 : hourColumnWidthProp;
1306
1323
 
1324
+ // Inject a consumer's `eventAccessibilityLabel` override into every event once,
1325
+ // so the timed columns and the all-day lane share it without threading a prop
1326
+ // through each. Passes the grid's `ampm` so the label can match the clock.
1327
+ const labeledRenderEvent = useMemo(
1328
+ () => withEventAccessibilityLabel(renderEvent, eventAccessibilityLabel, ampm),
1329
+ [renderEvent, eventAccessibilityLabel, ampm],
1330
+ );
1331
+
1307
1332
  const { width, height } = useWindowDimensions();
1308
1333
  const listRef = useRef<LegendListRef>(null);
1309
1334
  // The grid's outer view; on web its ref resolves to the DOM node we attach the
@@ -1590,7 +1615,7 @@ function TimeGridInner<T>({
1590
1615
  minHourHeight={minHourHeight}
1591
1616
  maxHourHeight={maxHourHeight}
1592
1617
  showNowIndicator={showNowIndicator}
1593
- renderEvent={renderEvent}
1618
+ renderEvent={labeledRenderEvent}
1594
1619
  keyExtractor={keyExtractor}
1595
1620
  snapMinutes={Math.max(1, dragStepMinutes)}
1596
1621
  showDragHandle={showDragHandle}
@@ -1634,7 +1659,7 @@ function TimeGridInner<T>({
1634
1659
  minHourHeight,
1635
1660
  maxHourHeight,
1636
1661
  showNowIndicator,
1637
- renderEvent,
1662
+ labeledRenderEvent,
1638
1663
  keyExtractor,
1639
1664
  dragStepMinutes,
1640
1665
  showDragHandle,
@@ -1667,6 +1692,7 @@ function TimeGridInner<T>({
1667
1692
  hourColumnWidth={hourColumnWidth}
1668
1693
  showWeekNumber={showWeekNumber}
1669
1694
  weekNumberPrefix={weekNumberPrefix}
1695
+ weekdayFormat={weekdayFormat}
1670
1696
  locale={locale}
1671
1697
  activeDate={activeDate}
1672
1698
  onPressDateHeader={onPressDateHeader}
@@ -1747,6 +1773,7 @@ type DefaultHeaderProps = {
1747
1773
  hourColumnWidth: number;
1748
1774
  showWeekNumber?: boolean;
1749
1775
  weekNumberPrefix?: string;
1776
+ weekdayFormat?: WeekdayFormat;
1750
1777
  locale?: Locale;
1751
1778
  activeDate?: Date;
1752
1779
  onPressDateHeader?: (date: Date) => void;
@@ -1759,6 +1786,7 @@ const DefaultHeader = ({
1759
1786
  hourColumnWidth,
1760
1787
  showWeekNumber,
1761
1788
  weekNumberPrefix = "W",
1789
+ weekdayFormat,
1762
1790
  locale,
1763
1791
  activeDate,
1764
1792
  onPressDateHeader,
@@ -1785,6 +1813,7 @@ const DefaultHeader = ({
1785
1813
  day={day}
1786
1814
  mode={mode}
1787
1815
  width={dayWidth}
1816
+ weekdayFormat={weekdayFormat}
1788
1817
  locale={locale}
1789
1818
  activeDate={activeDate}
1790
1819
  onPressDateHeader={onPressDateHeader}
@@ -1798,12 +1827,20 @@ type DayHeaderProps = {
1798
1827
  day: Date;
1799
1828
  mode: CalendarMode;
1800
1829
  width: number;
1830
+ weekdayFormat?: WeekdayFormat;
1801
1831
  locale?: Locale;
1802
1832
  activeDate?: Date;
1803
1833
  onPressDateHeader?: (date: Date) => void;
1804
1834
  };
1805
1835
 
1806
- const DayHeader = ({ day, width, locale, activeDate, onPressDateHeader }: DayHeaderProps) => {
1836
+ const DayHeader = ({
1837
+ day,
1838
+ width,
1839
+ weekdayFormat = "short",
1840
+ locale,
1841
+ activeDate,
1842
+ onPressDateHeader,
1843
+ }: DayHeaderProps) => {
1807
1844
  const theme = useCalendarTheme();
1808
1845
  const isToday = getIsToday(day);
1809
1846
  // Highlight the chosen `activeDate` when supplied, else the real today.
@@ -1811,28 +1848,36 @@ const DayHeader = ({ day, width, locale, activeDate, onPressDateHeader }: DayHea
1811
1848
 
1812
1849
  return (
1813
1850
  <Pressable
1814
- style={[styles.dayHeader, { width }]}
1851
+ style={[styles.dayHeader, { width }, theme.containers.columnHeader]}
1815
1852
  onPress={onPressDateHeader ? () => onPressDateHeader(day) : undefined}
1816
1853
  disabled={!onPressDateHeader}
1817
1854
  accessibilityRole={onPressDateHeader ? "button" : undefined}
1855
+ // A pressable header announces the full date it opens; a static one lets
1856
+ // the child labels speak for themselves.
1857
+ accessibilityLabel={onPressDateHeader ? format(day, "EEEE d MMMM", { locale }) : undefined}
1818
1858
  >
1819
1859
  {/* Mirrors the dom renderer's header: the muted weekday label sits above the
1820
- day number, and the number's circle fills for today / the active date. */}
1860
+ day number, and the number's circle fills for today / the active date.
1861
+ Theme text merges after the muted colour so a themed colour wins. */}
1821
1862
  <Text
1822
- style={[styles.dayHeaderWeekday, { color: theme.colors.textMuted }]}
1863
+ style={[{ color: theme.colors.textMuted }, theme.text.columnHeaderWeekday]}
1823
1864
  allowFontScaling={false}
1824
1865
  >
1825
- {format(day, "EEE", { locale })}
1866
+ {format(day, weekdayFormatToken(weekdayFormat), { locale })}
1826
1867
  </Text>
1827
1868
  <View
1869
+ testID="column-header-badge"
1828
1870
  style={[
1829
1871
  styles.dayHeaderBadge,
1872
+ theme.containers.columnHeaderBadge,
1830
1873
  isHighlighted && { backgroundColor: theme.colors.todayBackground },
1831
1874
  ]}
1832
1875
  >
1833
1876
  <Text
1834
1877
  style={[
1835
- styles.dayHeaderNumber,
1878
+ theme.text.dayNumber,
1879
+ // The state colour stays last so a themed dayNumber can't break the
1880
+ // today/active contrast.
1836
1881
  { color: isHighlighted ? theme.colors.todayText : theme.colors.text },
1837
1882
  ]}
1838
1883
  allowFontScaling={false}
@@ -1872,10 +1917,6 @@ const styles = StyleSheet.create({
1872
1917
  gap: 2,
1873
1918
  paddingVertical: 6,
1874
1919
  },
1875
- dayHeaderWeekday: {
1876
- fontSize: 11,
1877
- fontWeight: "600",
1878
- },
1879
1920
  dayHeaderBadge: {
1880
1921
  // A fixed circle so the today/active fill never shifts the header's height.
1881
1922
  width: 28,
@@ -1884,10 +1925,6 @@ const styles = StyleSheet.create({
1884
1925
  justifyContent: "center",
1885
1926
  alignItems: "center",
1886
1927
  },
1887
- dayHeaderNumber: {
1888
- fontSize: 15,
1889
- fontWeight: "600",
1890
- },
1891
1928
  content: {
1892
1929
  width: "100%",
1893
1930
  position: "relative",
package/src/index.tsx CHANGED
@@ -35,6 +35,12 @@ export {
35
35
  } from "./components/TimeGrid";
36
36
  export { DefaultEvent } from "./components/DefaultEvent";
37
37
  export { DefaultMonthEvent } from "./components/DefaultMonthEvent";
38
+ export {
39
+ ResourceTimeline,
40
+ type ResourceTimelineProps,
41
+ type Resource,
42
+ type ResourceEventArgs,
43
+ } from "./components/ResourceTimeline";
38
44
  export {
39
45
  type CalendarTheme,
40
46
  type PartialCalendarTheme,
@@ -48,6 +54,8 @@ export type {
48
54
  BusinessHours,
49
55
  CalendarEvent,
50
56
  CalendarMode,
57
+ EventAccessibilityLabelContext,
58
+ EventAccessibilityLabeler,
51
59
  EventKeyExtractor,
52
60
  ICalendarEvent,
53
61
  RecurrenceFrequency,
@@ -55,6 +63,7 @@ export type {
55
63
  RenderEvent,
56
64
  RenderEventArgs,
57
65
  TimeGridMode,
66
+ WeekdayFormat,
58
67
  WeekStartsOn,
59
68
  } from "./types";
60
69
  export {
@@ -77,6 +86,7 @@ export {
77
86
  type MonthGridWeekday,
78
87
  type UseMonthGridOptions,
79
88
  useMonthGrid,
89
+ weekdayFormatToken,
80
90
  } from "@super-calendar/core";
81
91
  export { expandRecurringEvents } from "@super-calendar/core";
82
92
  export { eventsInTimeZone, toZonedTime } from "@super-calendar/core";
package/src/theme.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type CalendarColors, darkColors, lightColors } from "@super-calendar/core";
2
2
  import { type Context, createContext, useContext } from "react";
3
- import type { TextStyle } from "react-native";
3
+ import type { TextStyle, ViewStyle } from "react-native";
4
4
 
5
5
  /**
6
6
  * The full set of colours, text styles and metrics the calendar paints with.
@@ -12,10 +12,14 @@ export interface CalendarTheme {
12
12
  colors: CalendarColors;
13
13
  /** Text styles for the calendar's labels and the built-in event box. */
14
14
  text: {
15
- /** Large day number in the week/day header. */
15
+ /** The day number in a time-grid (week/day) column header. */
16
16
  dayNumber: TextStyle;
17
- /** Short weekday label ("Mon") in headers. */
17
+ /** The weekday label ("Mon") in a time-grid column header. */
18
+ columnHeaderWeekday: TextStyle;
19
+ /** Short weekday label ("Mon") in the month grid's header row. */
18
20
  weekday: TextStyle;
21
+ /** The "MMMM yyyy" month title above the month grid. */
22
+ monthTitle: TextStyle;
19
23
  /** Date number inside a month cell. */
20
24
  dateCell: TextStyle;
21
25
  /** Hour labels down the left of the time grid. */
@@ -25,6 +29,46 @@ export interface CalendarTheme {
25
29
  /** Title inside the built-in default event box. */
26
30
  eventTitle: TextStyle;
27
31
  };
32
+ /**
33
+ * Per-part `ViewStyle` overrides for the renderer's container elements, the
34
+ * React Native counterpart of the web renderer's per-slot classes. Each is
35
+ * merged onto the built-in style, so you override only what you set. (Rolling
36
+ * out across the views; more slots as they land.)
37
+ */
38
+ containers: {
39
+ /** The month view's outer container (title + weekday header + grid). */
40
+ monthContainer: ViewStyle;
41
+ /** The weekday-label header row above a month grid. */
42
+ weekdayHeader: ViewStyle;
43
+ /** Each week (row of 7 day cells) in the month grid. */
44
+ weekRow: ViewStyle;
45
+ /** Each day cell in the month grid. */
46
+ dayCell: ViewStyle;
47
+ /** The date badge (the circle) inside a month day cell. */
48
+ dayBadge: ViewStyle;
49
+ /** An event chip inside a month day cell. */
50
+ monthEvent: ViewStyle;
51
+ /** Each day's column header in the time grid. */
52
+ columnHeader: ViewStyle;
53
+ /** The day-number badge (the circle) inside a time-grid column header. */
54
+ columnHeaderBadge: ViewStyle;
55
+ /** A timed event's positioned box in the time grid (and resource timeline). */
56
+ timeGridEvent: ViewStyle;
57
+ /** The current-time indicator line. */
58
+ nowIndicator: ViewStyle;
59
+ /** Each resource row in the resource timeline. */
60
+ resourceRow: ViewStyle;
61
+ /** The left-hand resource-label cell in the resource timeline. */
62
+ resourceLabel: ViewStyle;
63
+ /** The schedule/agenda list's outer container. */
64
+ agendaList: ViewStyle;
65
+ /** Each event row in the agenda list. */
66
+ agendaRow: ViewStyle;
67
+ /** The all-day lane above the time grid. */
68
+ allDayLane: ViewStyle;
69
+ /** Each day's column within the all-day lane. */
70
+ allDayColumn: ViewStyle;
71
+ };
28
72
  /** Corner radius of the today badge. Use a large value for a circle. */
29
73
  todayBadgeRadius: number;
30
74
  /**
@@ -38,8 +82,13 @@ export interface CalendarTheme {
38
82
  export const defaultTheme: CalendarTheme = {
39
83
  colors: lightColors,
40
84
  text: {
41
- dayNumber: { fontSize: 22, fontWeight: "700" },
85
+ // Match the header's shipped look: these two previously lived as hardcoded
86
+ // styles in the time-grid header, so honouring the theme changes nothing
87
+ // until a consumer overrides them.
88
+ dayNumber: { fontSize: 15, fontWeight: "600" },
89
+ columnHeaderWeekday: { fontSize: 11, fontWeight: "600" },
42
90
  weekday: { fontSize: 13, fontWeight: "700" },
91
+ monthTitle: { fontSize: 17, fontWeight: "700" },
43
92
  dateCell: { fontSize: 13, fontWeight: "700" },
44
93
  hourLabel: { fontSize: 10 },
45
94
  more: { fontSize: 11, fontWeight: "700" },
@@ -47,6 +96,24 @@ export const defaultTheme: CalendarTheme = {
47
96
  // whole number of lines (clipping on a line boundary, never mid-line).
48
97
  eventTitle: { fontSize: 12, fontWeight: "700", lineHeight: 16 },
49
98
  },
99
+ containers: {
100
+ monthContainer: {},
101
+ weekdayHeader: {},
102
+ weekRow: {},
103
+ dayCell: {},
104
+ dayBadge: {},
105
+ monthEvent: {},
106
+ columnHeader: {},
107
+ columnHeaderBadge: {},
108
+ timeGridEvent: {},
109
+ nowIndicator: {},
110
+ resourceRow: {},
111
+ resourceLabel: {},
112
+ agendaList: {},
113
+ agendaRow: {},
114
+ allDayLane: {},
115
+ allDayColumn: {},
116
+ },
50
117
  todayBadgeRadius: 999,
51
118
  rangeBandHeight: 22,
52
119
  };
@@ -59,6 +126,7 @@ export const defaultTheme: CalendarTheme = {
59
126
  export const darkTheme: CalendarTheme = {
60
127
  colors: darkColors,
61
128
  text: defaultTheme.text,
129
+ containers: defaultTheme.containers,
62
130
  todayBadgeRadius: defaultTheme.todayBadgeRadius,
63
131
  rangeBandHeight: defaultTheme.rangeBandHeight,
64
132
  };
@@ -69,6 +137,7 @@ export function mergeTheme(theme?: PartialCalendarTheme): CalendarTheme {
69
137
  return {
70
138
  colors: { ...defaultTheme.colors, ...theme.colors },
71
139
  text: { ...defaultTheme.text, ...theme.text },
140
+ containers: { ...defaultTheme.containers, ...theme.containers },
72
141
  todayBadgeRadius: theme.todayBadgeRadius ?? defaultTheme.todayBadgeRadius,
73
142
  rangeBandHeight: theme.rangeBandHeight ?? defaultTheme.rangeBandHeight,
74
143
  };
@@ -82,6 +151,7 @@ export function mergeTheme(theme?: PartialCalendarTheme): CalendarTheme {
82
151
  export type PartialCalendarTheme = {
83
152
  colors?: Partial<CalendarTheme["colors"]>;
84
153
  text?: Partial<CalendarTheme["text"]>;
154
+ containers?: Partial<CalendarTheme["containers"]>;
85
155
  todayBadgeRadius?: number;
86
156
  rangeBandHeight?: number;
87
157
  };
package/src/types.ts CHANGED
@@ -9,11 +9,14 @@ export type {
9
9
  BusinessHours,
10
10
  CalendarEvent,
11
11
  CalendarMode,
12
+ EventAccessibilityLabelContext,
13
+ EventAccessibilityLabeler,
12
14
  EventKeyExtractor,
13
15
  ICalendarEvent,
14
16
  RecurrenceFrequency,
15
17
  RecurrenceRule,
16
18
  TimeGridMode,
19
+ WeekdayFormat,
17
20
  WeekStartsOn,
18
21
  } from "@super-calendar/core";
19
22
 
@@ -53,6 +56,12 @@ export type RenderEventArgs<T = unknown> = {
53
56
  allDayLabel?: string;
54
57
  /** Per-event style resolved from `eventCellStyle`; the built-in renderer merges it onto the box. */
55
58
  cellStyle?: StyleProp<ViewStyle>;
59
+ /**
60
+ * Screen-reader label to announce for the event, injected by a consumer's
61
+ * `eventAccessibilityLabel` override. The built-in renderers use it in place of
62
+ * their default label; `undefined` falls back to that default.
63
+ */
64
+ accessibilityLabel?: string;
56
65
  onPress: () => void;
57
66
  /** Wired when the consumer passes `onLongPressEvent`; otherwise undefined. */
58
67
  onLongPress?: () => void;
@@ -0,0 +1,30 @@
1
+ import type { EventAccessibilityLabeler } from "@super-calendar/core";
2
+ import type { RenderEvent, RenderEventArgs } from "../types";
3
+
4
+ /**
5
+ * Wrap an event renderer so it injects a consumer's `eventAccessibilityLabel`
6
+ * override into `RenderEventArgs.accessibilityLabel`. The built-in renderers use
7
+ * that string in place of their default label, and custom renderers can read it
8
+ * too. Returns the renderer unchanged when no override is set, so the common path
9
+ * pays nothing.
10
+ */
11
+ export function withEventAccessibilityLabel<T>(
12
+ renderEvent: RenderEvent<T>,
13
+ labeler: EventAccessibilityLabeler<T> | undefined,
14
+ ampm: boolean,
15
+ ): RenderEvent<T> {
16
+ if (!labeler) return renderEvent;
17
+ const Base = renderEvent;
18
+ return function LabeledEvent(props: RenderEventArgs<T>) {
19
+ return (
20
+ <Base
21
+ {...props}
22
+ accessibilityLabel={labeler(props.event, {
23
+ mode: props.mode,
24
+ isAllDay: props.isAllDay ?? false,
25
+ ampm,
26
+ })}
27
+ />
28
+ );
29
+ };
30
+ }