Skip to content

Commit 4e664c9

Browse files
committed
Add support for event handlers
1 parent 2a01c46 commit 4e664c9

File tree

5 files changed

+112
-7
lines changed

5 files changed

+112
-7
lines changed

.eslintrc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
"plugins": ["@typescript-eslint"],
1010
"ignorePatterns": ["*.css"],
1111
"rules": {
12-
"@typescript-eslint/ban-ts-comment": 0,
12+
"@typescript-eslint/ban-types": ["error"],
13+
"@typescript-eslint/ban-ts-comment": ["off"],
1314
"arrow-body-style": ["error", "as-needed"],
14-
"react/prop-types": 0
15+
"react/prop-types": ["off"]
1516
},
1617
"settings": {
1718
"react": {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-activity-calendar",
3-
"version": "1.3.6",
3+
"version": "1.4.0",
44
"description": " A React component to display activity data in calendar",
55
"author": "Jonathan Gruber <gruberjonathan@gmail.com>",
66
"license": "MIT",

src/component/ActivityCalendar.stories.tsx

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { CSSProperties } from 'react';
22
import { Story, Meta } from '@storybook/react';
33
import ReactTooltip from 'react-tooltip';
44
import eachDayOfInterval from 'date-fns/eachDayOfInterval';
@@ -9,6 +9,17 @@ import ActivityCalendar, { Props } from './ActivityCalendar';
99
import { Day, Level, Theme } from '../types';
1010
import { DEFAULT_MONTH_LABELS, DEFAULT_WEEKDAY_LABELS } from '../util';
1111

12+
const styles: {
13+
[elem: string]: CSSProperties;
14+
} = {
15+
code: {
16+
fontSize: '0.9rem',
17+
},
18+
p: {
19+
maxWidth: '68ch',
20+
},
21+
};
22+
1223
export default {
1324
title: 'Activity Calendar',
1425
component: ActivityCalendar,
@@ -63,8 +74,9 @@ const Template: Story<Props> = args => <ActivityCalendar {...args} />;
6374
const TemplateLocalized: Story<Props> = args => (
6475
<>
6576
<h1>Localization</h1>
77+
<p>(Example in German)</p>
6678
<ActivityCalendar {...args} style={{ margin: '2rem 0' }} />
67-
<pre>
79+
<pre style={styles.code}>
6880
{`
6981
// Shape of \`labels\` property (default values).
7082
// All properties are optional.
@@ -104,6 +116,40 @@ const labels = {
104116
</>
105117
);
106118

119+
const TemplateEventHandlers: Story<Props> = args => (
120+
<>
121+
<h1>Event Handlers</h1>
122+
<p style={styles.p}>
123+
You can register event handlers for the SVG <code style={styles.code}>&lt;rect/&gt;</code>{' '}
124+
elements that are used to render the calendar days. This way you can control the behaviour on
125+
click, hover, etc.
126+
</p>
127+
<p style={styles.p}>
128+
All event listeners have the following signature, so you are able to use the shown data inside
129+
the handler:
130+
</p>
131+
<p style={styles.p}>
132+
<code style={styles.code}>(event: React.SyntheticEvent) =&gt; (data: Day) =&gt; void</code>
133+
</p>
134+
<p style={styles.p}>Click on any block below to see it in action:</p>
135+
<ActivityCalendar {...args} style={{ margin: '2rem 0' }} />
136+
<pre style={styles.code}>
137+
{`
138+
<ActivityCalendar
139+
data={data}
140+
eventHandlers: {
141+
onClick: event => data => {
142+
console.log({ event, data });
143+
alert(JSON.stringify(data, null, 4));
144+
},
145+
onMouseEnter: event => data => console.log('mouseEnter'),
146+
}
147+
/>
148+
`}
149+
</pre>
150+
</>
151+
);
152+
107153
const theme: Theme = {
108154
level0: '#F0F0F0',
109155
level1: '#C4EDDE',
@@ -211,6 +257,18 @@ WithLocalizedLabels.args = {
211257
},
212258
};
213259

260+
const eventHandlerData = generateData();
261+
export const EventHandlers = TemplateEventHandlers.bind({});
262+
EventHandlers.args = {
263+
data: eventHandlerData,
264+
eventHandlers: {
265+
onClick: event => data => {
266+
console.log({ event, data });
267+
alert(JSON.stringify(data, null, 4));
268+
},
269+
},
270+
};
271+
214272
function generateData(monthStart = 0, monthEnd = 11): Array<Day> {
215273
const MAX = 10;
216274
const LEVELS = 5;

src/component/ActivityCalendar.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { Day as WeekDay } from 'date-fns';
88

99
import styles from './styles.css';
1010

11-
import { Day, Labels, Theme } from '../types';
11+
import { Day, EventHandlerMap, Labels, ReactEvent, SVGRectEventHandler, Theme } from '../types';
1212
import {
1313
generateEmptyData,
1414
getClassName,
@@ -65,6 +65,10 @@ export interface Props {
6565
* A date-fns/format compatible date string used in tooltips.
6666
*/
6767
dateFormat?: string;
68+
/**
69+
* Event handlers to register for the SVG `<rect>` elements that are used to render the calendar days. Handler signature: `event => data => void`
70+
*/
71+
eventHandlers?: EventHandlerMap;
6872
/**
6973
* Font size for text in pixels.
7074
*/
@@ -115,6 +119,7 @@ const ActivityCalendar: FunctionComponent<Props> = ({
115119
children,
116120
color = undefined,
117121
dateFormat = 'MMM do, yyyy',
122+
eventHandlers = {},
118123
fontSize = 14,
119124
hideColorLegend = false,
120125
hideMonthLabels = false,
@@ -154,6 +159,18 @@ const ActivityCalendar: FunctionComponent<Props> = ({
154159
return `<strong>${contribution.count} contributions</strong> on ${date}`;
155160
}
156161

162+
function getEventHandlers(data: Day): SVGRectEventHandler {
163+
return (
164+
Object.keys(eventHandlers) as Array<keyof SVGRectEventHandler>
165+
).reduce<SVGRectEventHandler>(
166+
(handlers, key) => ({
167+
...handlers,
168+
[key]: (event: ReactEvent<SVGRectElement>) => eventHandlers[key]?.(event)(data),
169+
}),
170+
{},
171+
);
172+
}
173+
157174
function renderLabels() {
158175
const style = {
159176
fontSize,
@@ -224,6 +241,7 @@ const ActivityCalendar: FunctionComponent<Props> = ({
224241

225242
return (
226243
<rect
244+
{...getEventHandlers(day)}
227245
x={0}
228246
y={textHeight + (blockSize + blockMargin) * dayIndex}
229247
width={blockSize}
@@ -297,7 +315,7 @@ const ActivityCalendar: FunctionComponent<Props> = ({
297315
const additionalStyles = {
298316
width,
299317
maxWidth: '100%',
300-
// Required to have correct colors in CSS loading animation
318+
// Required for correct colors in CSS loading animation
301319
[`--${NAMESPACE}-loading`]: theme.level0,
302320
[`--${NAMESPACE}-loading-active`]: tinycolor(theme.level0).darken(8).toString(),
303321
};

src/types.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import React, { DOMAttributes } from 'react';
2+
13
export type Level = 0 | 1 | 2 | 3 | 4;
24

35
export interface Day {
@@ -26,3 +28,29 @@ export interface Theme {
2628
readonly level1: string;
2729
readonly level0: string;
2830
}
31+
32+
export type SVGRectEventHandler = Omit<
33+
DOMAttributes<SVGRectElement>,
34+
'css' | 'children' | 'dangerouslySetInnerHTML'
35+
>;
36+
37+
export type EventHandlerMap = {
38+
[key in keyof SVGRectEventHandler]: (
39+
...event: Parameters<NonNullable<SVGRectEventHandler[keyof SVGRectEventHandler]>>
40+
) => (data: Day) => void;
41+
};
42+
43+
export type ReactEvent<E extends Element> = React.AnimationEvent<E> &
44+
React.ClipboardEvent<E> &
45+
React.CompositionEvent<E> &
46+
React.DragEvent<E> &
47+
React.FocusEvent<E> &
48+
React.FormEvent<E> &
49+
React.KeyboardEvent<E> &
50+
React.MouseEvent<E> &
51+
React.PointerEvent<E> &
52+
React.SyntheticEvent<E> &
53+
React.TouchEvent<E> &
54+
React.TransitionEvent<E> &
55+
React.UIEvent<E> &
56+
React.WheelEvent<E>;

0 commit comments

Comments
 (0)