Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
fd2fbce
calendar phase 1
aresnik11 Mar 2, 2026
0ffb424
add focus management
aresnik11 Mar 4, 2026
b8a856d
add input
aresnik11 Mar 4, 2026
c096fe9
datepicker
aresnik11 Mar 4, 2026
9346ac9
clean up
aresnik11 Mar 5, 2026
00a8584
refactor datepicker
aresnik11 Mar 6, 2026
1946d73
clean up hooks
aresnik11 Mar 6, 2026
39d0de0
update exports
aresnik11 Mar 9, 2026
23f593d
add missing validation file
aresnik11 Mar 9, 2026
c1c9245
clean up inputref
aresnik11 Mar 9, 2026
9a8f70d
wip fix for typing date
aresnik11 Mar 10, 2026
82854c8
cleaning up
aresnik11 Mar 11, 2026
4f39ff9
update range logic
aresnik11 Mar 12, 2026
d194237
range logic when specific input is focused
aresnik11 Mar 12, 2026
3bbeb01
deselect logic
aresnik11 Mar 12, 2026
a6cbe80
fix lint
aresnik11 Mar 12, 2026
a56426f
fix story
aresnik11 Mar 12, 2026
f79ce4a
fix lint again
aresnik11 Mar 12, 2026
e0ee942
update snapshot
aresnik11 Mar 12, 2026
a417554
fix lint
aresnik11 Mar 12, 2026
cf4a438
show 2 months on larger screens
aresnik11 Mar 13, 2026
d3f6fac
style updates
aresnik11 Mar 13, 2026
e615f9f
PR feedback
aresnik11 Mar 17, 2026
a19fc7b
fix today in range color
aresnik11 Mar 17, 2026
9c30d34
clean up context
aresnik11 Mar 17, 2026
b657556
update placeholder text to be locale based
aresnik11 Mar 17, 2026
1924433
update next/last month text to be locale based
aresnik11 Mar 17, 2026
a83a99a
capitalize
aresnik11 Mar 17, 2026
40a0c8b
Merge branch 'main' into ajr-datepicker-styles
aresnik11 Mar 17, 2026
bfd8206
translations
aresnik11 Mar 17, 2026
f6463b4
add formgroup
aresnik11 Mar 17, 2026
7de8b27
Merge branch 'ajr-datepicker-styles' into ajr-datepicker-localization
aresnik11 Mar 17, 2026
c9a7576
fix calendar alignment and add shadow
aresnik11 Mar 17, 2026
d3f6fba
clear button logic
aresnik11 Mar 17, 2026
2834206
more translations
aresnik11 Mar 18, 2026
b436daa
fix disabled hover
aresnik11 Mar 18, 2026
5b7a8be
make disabledDates optional and add to story
aresnik11 Mar 18, 2026
496e740
disabled date in range logic
aresnik11 Mar 18, 2026
5df9d1a
fix keyboard nav now that showing 2 months
aresnik11 Mar 18, 2026
38f81a9
PR feedback
aresnik11 Mar 18, 2026
b9aa903
toLocaleUpperCase
aresnik11 Mar 18, 2026
b400a75
small fixes
aresnik11 Mar 19, 2026
0e10748
fix range styling
aresnik11 Mar 19, 2026
dfdabe8
fix alignments
aresnik11 Mar 23, 2026
1ee01fc
rename props
aresnik11 Mar 23, 2026
a6c8388
move around helpers
aresnik11 Mar 23, 2026
3bfab02
fix calendar story
aresnik11 Mar 23, 2026
afaea71
support small input size
aresnik11 Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/gamut/__tests__/__snapshots__/gamut.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ exports[`Gamut Exported Keys 1`] = `
"BodyPortal",
"Box",
"Breadcrumbs",
"Calendar",
"CalendarBody",
"CalendarFooter",
"CalendarHeader",
"Card",
"Checkbox",
"clampToMonth",
"Coachmark",
"Column",
"ConnectedCheckbox",
Expand All @@ -32,6 +37,11 @@ exports[`Gamut Exported Keys 1`] = `
"CTAButton",
"DataList",
"DataTable",
"DatePicker",
"DatePickerCalendar",
"DatePickerContext",
"DatePickerInput",
"DatePickerProvider",
"DelayedRenderWrapper",
"Dialog",
"Disclosure",
Expand All @@ -46,14 +56,21 @@ exports[`Gamut Exported Keys 1`] = `
"FocusTrap",
"focusVisibleStyle",
"Form",
"formatDateForInput",
"formatDateRangeForInput",
"formatMonthYear",
"FormError",
"FormGroup",
"FormGroupDescription",
"FormGroupLabel",
"FormPropsContext",
"FormRequiredText",
"generateResponsiveClassnames",
"getDayOfWeek",
"getFocusableElements",
"getMonthGrid",
"getWeekdayFullNames",
"getWeekdayLabels",
"GridBox",
"GridForm",
"GridFormContent",
Expand All @@ -63,6 +80,11 @@ exports[`Gamut Exported Keys 1`] = `
"InfoTip",
"Input",
"isClickableCrumb",
"isDateDisabled",
"isDateInRange",
"isPastDate",
"isSameDay",
"isValidDate",
"LayoutGrid",
"List",
"ListCol",
Expand All @@ -75,6 +97,8 @@ exports[`Gamut Exported Keys 1`] = `
"omitProps",
"Overlay",
"Pagination",
"parseDateFromInput",
"parseDateRangeFromInput",
"Popover",
"PopoverContainer",
"PreviewTip",
Expand Down Expand Up @@ -112,6 +136,7 @@ exports[`Gamut Exported Keys 1`] = `
"ToolTip",
"USE_DEBOUNCED_FIELD_DIRTY_KEY",
"useConnectedForm",
"useDatePicker",
"useDebouncedField",
"useField",
"useFormState",
Expand Down
26 changes: 26 additions & 0 deletions packages/gamut/src/DatePicker/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { CheckerDense } from '@codecademy/gamut-patterns';
import * as React from 'react';

import { Box } from '../../Box';

/**
* Outer wrapper for the calendar (header + body + footer).
* Used by DatePickerCalendar to group the calendar content.
* Renders a CheckerDense pattern shadow at offset left 8, top 8.
*/
export const Calendar: React.FC<{ children: React.ReactNode }> = ({
children,
}) => (
<Box position="relative" width="max-content">
<CheckerDense left={8} position="absolute" top={8} />
<Box
bg="background"
border={1}
borderRadius="sm"
position="relative"
zIndex={1}
>
{children}
</Box>
</Box>
);
243 changes: 243 additions & 0 deletions packages/gamut/src/DatePicker/Calendar/CalendarBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { css, states } from '@codecademy/gamut-styles';
import styled from '@emotion/styled';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import * as React from 'react';

import { TextButton } from '../../Button';
import { CalendarBodyProps } from './types';
import {
getDatesWithRow,
getMonthGrid,
isDateDisabled,
isDateInRange,
isSameDay,
} from './utils/dateGrid';
import { getWeekdayNames } from './utils/format';
import { keyHandler } from './utils/keyHandler';

const TableHeader = styled.th(
css({
fontSize: 14,
fontWeight: 'base',
color: 'text-disabled',
textAlign: 'center',
})
);

const DateCell = styled.td(
css({
padding: 0,
})
);

const DateButton = styled(TextButton)(
states({
isToday: {
position: 'relative',
'&::after': {
content: '""',
position: 'absolute',
bottom: 4,
width: 4,
height: 4,
borderRadius: 'full',
bg: 'hyper',
},
},
isSelected: {
bg: 'text',
color: 'background',
'&:hover, &:focus': {
bg: 'secondary-hover',
color: 'background',
},
'&::after': {
bg: 'background',
},
},
isRangeStart: {
borderRadiusRight: 'none',
},
isRangeEnd: {
borderRadiusLeft: 'none',
},
isInRange: {
bg: 'text-disabled',
color: 'background',
borderRadius: 'none',
'&:hover, &:focus': {
bg: 'secondary-hover',
color: 'background',
},
'&::after': {
bg: 'background',
},
},
disabled: {
color: 'text-disabled',
textDecoration: 'line-through',
'&:hover': {
textDecoration: 'line-through',
},
},
}),
css({
fontWeight: 'base',
width: '32px',
})
);

export const CalendarBody: React.FC<CalendarBodyProps> = ({
displayDate,
selectedDate,
endDate = null,
disabledDates = [],
onDateSelect,
locale,
weekStartsOn = 0,
labelledById,
focusedDate,
onFocusedDateChange,
onDisplayDateChange,
onEscapeKeyPress,
hasAdjacentMonthRight,
hasAdjacentMonthLeft,
}) => {
const year = displayDate.getFullYear();
const month = displayDate.getMonth();
const weeks = getMonthGrid(year, month, weekStartsOn);
const weekdayLabels = getWeekdayNames('short', locale, weekStartsOn);
const weekdayFullNames = getWeekdayNames('long', locale, weekStartsOn);
const buttonRefs = useRef<Map<number, HTMLElement>>(new Map());

const datesWithRow = useMemo(() => getDatesWithRow(weeks), [weeks]);
const focusTarget = focusedDate ?? selectedDate;

const isToday = useCallback(
(date: Date | null) => date !== null && isSameDay(date, new Date()),
[]
);

const focusButton = useCallback((date: Date | null) => {
if (date === null) return;
const key = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate()
).getTime();
buttonRefs.current.get(key)?.focus();
}, []);

useEffect(() => {
if (focusTarget !== null) focusButton(focusTarget);
}, [focusTarget, focusButton]);

const handleKeyDown = useCallback(
(e: React.KeyboardEvent, date: Date) =>
keyHandler(
e,
date,
onFocusedDateChange,
datesWithRow,
month,
year,
disabledDates,
onDateSelect,
onEscapeKeyPress,
onDisplayDateChange,
hasAdjacentMonthRight,
hasAdjacentMonthLeft
),
[
onFocusedDateChange,
datesWithRow,
month,
year,
disabledDates,
onDateSelect,
onEscapeKeyPress,
onDisplayDateChange,
hasAdjacentMonthLeft,
hasAdjacentMonthRight,
]
);

const setButtonRef = useCallback((date: Date, el: HTMLElement | null) => {
const k = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate()
).getTime();
if (el) buttonRefs.current.set(k, el);
else buttonRefs.current.delete(k);
}, []);

return (
<table aria-labelledby={labelledById} role="grid" width="100%">
<thead>
<tr>
{weekdayLabels.map((label, i) => (
<TableHeader abbr={weekdayFullNames[i]} key={label} scope="col">
{label}
</TableHeader>
))}
</tr>
</thead>
<tbody>
{weeks.map((week, rowIndex) => (
<tr key={week.join('-')}>
{week.map((date, colIndex) => {
if (date === null) {
return (
// fix this error
// eslint-disable-next-line react/no-array-index-key, jsx-a11y/control-has-associated-label
<DateCell
key={`empty-${rowIndex}-${colIndex}`}

Check failure on line 195 in packages/gamut/src/DatePicker/Calendar/CalendarBody.tsx

View workflow job for this annotation

GitHub Actions / lint (lint)

Do not use Array index in keys

Check failure on line 195 in packages/gamut/src/DatePicker/Calendar/CalendarBody.tsx

View workflow job for this annotation

GitHub Actions / lint (lint)

Do not use Array index in keys
role="gridcell"
/>
);
}
const selected =
isSameDay(date, selectedDate) || isSameDay(date, endDate);
const range = !!selectedDate && !!endDate;
const inRange =
range && isDateInRange(date, selectedDate, endDate);
const disabled = isDateDisabled(date, disabledDates);
const today = isToday(date);
// this is making the selected date a differnet color bc it is focused, look into further
const isFocused =
focusTarget !== null && isSameDay(date, focusTarget);

return (
<DateCell
aria-selected={selected}
key={date.getTime()}
role="gridcell"
>
<DateButton
disabled={disabled}
isInRange={inRange}
isRangeEnd={range && isSameDay(date, endDate)}
isRangeStart={range && isSameDay(date, selectedDate)}
isSelected={selected}
isToday={today}
ref={(el) => setButtonRef(date, el as HTMLElement | null)}
tabIndex={isFocused ? 0 : -1}
variant="secondary"
onClick={() => onDateSelect(date)}
onFocus={() => onFocusedDateChange?.(date)}
onKeyDown={(e: React.KeyboardEvent) =>
handleKeyDown(e, date)
}
>
{date.getDate()}
</DateButton>
</DateCell>
);
})}
</tr>
))}
</tbody>
</table>
);
};
Loading
Loading