Skip to content

Commit

Permalink
Merge pull request #94 from perimetre/date-input
Browse files Browse the repository at this point in the history
date input
  • Loading branch information
AssisrMatheus authored Dec 7, 2022
2 parents 1cf354f + f4ccf29 commit 1426e3e
Show file tree
Hide file tree
Showing 16 changed files with 3,801 additions and 24 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

## [9.2.0] 2022-12-07

### Added

- Added `DatePickerInput` and `DateRangePickerInput`

## [9.1.0] 2022-11-10

### Added
Expand Down
2,800 changes: 2,789 additions & 11 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 11 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@perimetre/ui",
"description": "A component library made by @perimetre",
"version": "9.1.0",
"version": "9.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/perimetre/ui.git"
Expand Down Expand Up @@ -46,10 +46,10 @@
"peerDependencies": {
"@types/react": "18.x",
"@types/react-dom": "18.x",
"formik": "^2.2.9",
"isomorphic-dompurify": "0.x",
"react": "18.x",
"react-dom": "18.x",
"formik": "^2.2.9"
"react-dom": "18.x"
},
"dependencies": {
"@babel/core": "^7.12.13",
Expand All @@ -73,8 +73,8 @@
"@types/lodash": "^4.14.168",
"@types/node": "^14.14.22",
"@types/preval.macro": "^3.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.1",
"@types/react": "18.x",
"@types/react-dom": "18.x",
"@types/styled-components": "^5.1.7",
"@types/svgo": "^2.6.0",
"@types/yup": "^0.29.11",
Expand All @@ -100,6 +100,8 @@
"postcss-import": "^14.0.2",
"postcss-preset-env": "^7.4.2",
"preval.macro": "^5.0.0",
"react-aria": "^3.21.0",
"react-stately": "^3.19.0",
"rollup": "^3.2.3",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
Expand All @@ -115,8 +117,6 @@
"devDependencies": {
"@commitlint/cli": "^11.0.0",
"@commitlint/config-conventional": "^11.0.0",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@storybook/addon-a11y": "^6.5.12",
"@storybook/addon-actions": "^6.5.12",
"@storybook/addon-essentials": "^6.5.12",
Expand All @@ -128,6 +128,8 @@
"@storybook/react": "^6.5.12",
"@storybook/storybook-deployer": "^2.8.12",
"@storybook/theming": "^6.5.12",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^4.14.1",
"eslint": "^7.18.0",
Expand All @@ -139,6 +141,7 @@
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-storybook": "^0.6.6",
"formik": "^2.2.9",
"html-webpack-plugin": "^5.5.0",
"http-server": "^0.12.3",
"husky": "^4.3.8",
Expand All @@ -152,8 +155,7 @@
"stylelint": "^13.9.0",
"stylelint-config-recommended": "^3.0.0",
"stylelint-config-styled-components": "^0.1.1",
"webpack": "^5.74.0",
"formik": "^2.2.9"
"webpack": "^5.74.0"
},
"resolutions": {
"@storybook/react/webpack": "^5"
Expand Down
3 changes: 2 additions & 1 deletion src/components/Button/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
}

/* If clicking */
&:active {
&:active,
.active {
/* Remove transition effect if there's any transition transform(only on active) */
@apply transition-none;
/* Moves the button down a little bit to give feedback */
Expand Down
129 changes: 129 additions & 0 deletions src/components/DatePickerInput/DateCalendar/CalendarCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { CalendarDate, getDayOfWeek, isSameDay } from '@internationalized/date';
import { CalendarState, RangeCalendarState } from '@react-stately/calendar';
import classnames from 'classnames';
import React, { useRef } from 'react';
import { mergeProps, useCalendarCell, useFocusRing } from 'react-aria';
import { Instance as TippyInstance } from 'tippy.js';

type CalendarCellProps = {
/**
* The calendar date value
*/
date: CalendarDate;
/**
* The calendar state from `useCalendarState`
*/
state: CalendarState | RangeCalendarState;
/**
* The current app locale
*/
locale: string;
/**
* Instance for the tooltip component
*/
tippyInstance?: TippyInstance;
};

/**
* Renders a single calendar cell
*
* @param props Props for the calendar cell
* @param props.state The calendar state from `useCalendarState`
* @param props.date The calendar date value
* @param props.locale The current app locale
* @param props.tippyInstance Instance for the tooltip component
*/
export const CalendarCell: React.FC<CalendarCellProps> = ({ state, date, locale, tippyInstance }) => {
const ref = useRef<HTMLDivElement>(null);

const { cellProps, buttonProps, isSelected, isOutsideVisibleRange, isDisabled, formattedDate, isInvalid } =
useCalendarCell({ date }, state, ref);

// IF this is a range selection calendar
// The start and end date of the selected range will have
// an emphasized appearance.
const isSelectionStart = (state as RangeCalendarState).highlightedRange
? isSameDay(date, (state as RangeCalendarState).highlightedRange.start)
: isSelected;
const isSelectionEnd = (state as RangeCalendarState).highlightedRange
? isSameDay(date, (state as RangeCalendarState).highlightedRange.end)
: isSelected;

// We add rounded corners on the left for the first day of the month,
// the first day of each week, and the start date of the selection.
// We add rounded corners on the right for the last day of the month,
// the last day of each week, and the end date of the selection.
const dayOfWeek = getDayOfWeek(date, locale);
const isRoundedLeft = isSelected && (isSelectionStart || dayOfWeek === 0 || date.day === 1);
const isRoundedRight =
isSelected && (isSelectionEnd || dayOfWeek === 6 || date.day === date.calendar.getDaysInMonth(date));

const { focusProps, isFocusVisible } = useFocusRing();
const finalButtonProps = mergeProps(buttonProps, focusProps);

return (
<td {...cellProps} className={`relative py-0.5 ${isFocusVisible ? 'z-10' : 'z-0'}`}>
<div
{...finalButtonProps}
onPointerUp={(e) => {
finalButtonProps.onPointerUp?.(e);

if ('highlightedRange' in state ? !state.isDragging : true) {
// Makes sure to run hide on the next tick
setTimeout(() => tippyInstance?.hide(), 10);
}
}}
onKeyUp={(e) => {
finalButtonProps.onKeyUp?.(e);

// If it's finished selecting a range, or if it's a single date picker
if (('highlightedRange' in state ? !state.isDragging : true) && e.key === 'Enter') {
// Makes sure to run hide on the next tick
setTimeout(() => tippyInstance?.hide(), 10);
}
}}
ref={ref}
hidden={isOutsideVisibleRange}
className={classnames(
'group h-10 w-10 outline-none',
{
'rounded-l-full': isRoundedLeft,
'rounded-r-full': isRoundedRight,
disabled: isDisabled,
// Hover state for non-selected cells.
'hover:bg-gray-100': !isSelected && !isDisabled
},
isSelected ? (isInvalid ? 'bg-red-300' : 'bg-gray-300') : undefined
)}
>
<div
className={classnames(
'flex h-full w-full items-center justify-center rounded-full',
{
'text-gray-400': isDisabled && !isInvalid,
// Focus ring, visible while the cell has keyboard focus.
'group-focus:z-2 ring-2 ring-pui-placeholder-color ring-offset-2': isFocusVisible
},
// Darker selection background for the start and end.
isSelectionStart || isSelectionEnd
? isInvalid
? 'bg-red-600 text-white hover:bg-red-700'
: 'bg-pui-placeholder-color text-white hover:bg-pui-placeholder-color'
: undefined,
// Hover state for cells in the middle of the range.
isSelected && !isDisabled && !(isSelectionStart || isSelectionEnd)
? isInvalid
? 'hover:bg-red-400'
: 'hover:bg-pui-placeholder-color'
: undefined,
'cursor-default'
)}
>
{formattedDate}
</div>
</div>
</td>
);
};
65 changes: 65 additions & 0 deletions src/components/DatePickerInput/DateCalendar/CalendarGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { getWeeksInMonth } from '@internationalized/date';
import { CalendarState, RangeCalendarState } from '@react-stately/calendar';
import React from 'react';
import { AriaCalendarGridProps, useCalendarGrid } from 'react-aria';
import { CalendarCell } from './CalendarCell';
import { Instance as TippyInstance } from 'tippy.js';

type CalendarGridProps = AriaCalendarGridProps & {
/**
* The calendar state from `useCalendarState`
*/
state: CalendarState | RangeCalendarState;
/**
* The current app locale
*/
locale: string;
/**
* Instance for the tooltip component
*/
tippyInstance?: TippyInstance;
};

/**
* Renders the table with days in a grid for the date input
*
* @param props Props for the calendar grid
* @param props.state The calendar state from `useCalendarState`
* @param props.locale The current app locale
* @param props.tippyInstance Instance for the tooltip component
*/
export const CalendarGrid: React.FC<CalendarGridProps> = ({ state, locale, tippyInstance, ...props }) => {
const { gridProps, headerProps, weekDays } = useCalendarGrid(props, state);

// Get the number of weeks in the month so we can render the proper number of rows.
const weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale);

return (
<table {...gridProps} cellPadding="0" className="flex-1">
<thead {...headerProps} className="text-gray-600">
<tr>
{weekDays.map((day, index) => (
<th key={index} className="text-center">
{day}
</th>
))}
</tr>
</thead>
<tbody>
{[...new Array(weeksInMonth).keys()].map((weekIndex) => (
<tr key={weekIndex}>
{state
.getDatesInWeek(weekIndex)
.map((date, i) =>
date ? (
<CalendarCell tippyInstance={tippyInstance} locale={locale} key={i} date={date} state={state} />
) : (
<td key={i} />
)
)}
</tr>
))}
</tbody>
</table>
);
};
93 changes: 93 additions & 0 deletions src/components/DatePickerInput/DateCalendar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { createCalendar } from '@internationalized/date';
import classnames from 'classnames';
import React, { PropsWithChildren, useRef } from 'react';
import { AriaButtonProps, AriaCalendarProps, mergeProps, useButton, useCalendar, useFocusRing } from 'react-aria';
import { useCalendarState } from 'react-stately';
import { DateValue } from '..';
import { Button } from '../../Button';
import { CalendarGrid } from './CalendarGrid';
import { Instance as TippyInstance } from 'tippy.js';

type CalendarButtonProps = AriaButtonProps<'button'> & {
/**
* Whether the button is currently pressed
*/
isPressed?: boolean;
};

/**
* Renders a single button for the calendar
*
* @param props Button props
* @param props.children Content for the button
*/
export const CalendarButton: React.FC<PropsWithChildren<CalendarButtonProps>> = ({ children, ...props }) => {
const ref = useRef<HTMLButtonElement>(null);

const { buttonProps } = useButton(props, ref);
const { focusProps, isFocusVisible } = useFocusRing();

return (
<Button
variant="icon"
{...mergeProps(buttonProps, focusProps)}
ref={ref}
className={classnames(
'rounded-full p-2 outline-none',
props.isDisabled ? 'text-gray-400' : 'hover:bg-gray-100 active:bg-gray-200',
{
'ring-2 ring-pui-placeholder-color ring-offset-2': isFocusVisible
}
)}
>
{children}
</Button>
);
};

type DateCalendarProps<T extends DateValue = DateValue> = AriaCalendarProps<T> & {
/**
* The current app locale
*/
locale: string;
/**
* Instance for the tooltip component
*/
tippyInstance?: TippyInstance;
};

/**
* Calendar that renders the days in a grid for the date input
*
* @param props Calendar props for `useCalendar`
* @param props.locale The current app locale
* @param props.tippyInstance Instance for the tooltip component
*/
export const DateCalendar: React.FC<DateCalendarProps> = ({ locale, tippyInstance, ...props }) => {
const ref = useRef<HTMLDivElement>(null);

const state = useCalendarState({
...props,
locale,
createCalendar
});

const { calendarProps, prevButtonProps, nextButtonProps, title } = useCalendar(props, state);

return (
<div {...calendarProps} ref={ref} className="inline-block text-gray-800">
<div className="flex items-center pb-4">
<h2 className="ml-2 flex-1 text-xl font-bold">{title}</h2>
<CalendarButton {...prevButtonProps}>
<FontAwesomeIcon className="h-6 w-6" icon={faChevronLeft} />
</CalendarButton>
<CalendarButton {...nextButtonProps}>
<FontAwesomeIcon className="h-6 w-6" icon={faChevronRight} />
</CalendarButton>
</div>
<CalendarGrid locale={locale} state={state} tippyInstance={tippyInstance} />
</div>
);
};
Loading

0 comments on commit 1426e3e

Please sign in to comment.