Skip to content
Open
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
99 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
e4e15be
initial focus a11y updates
aresnik11 Mar 25, 2026
422e220
PR feedback
aresnik11 Mar 25, 2026
8072692
clean up utils
aresnik11 Mar 26, 2026
ca9a66b
add full date aria label to date cells
aresnik11 Mar 26, 2026
13d1a3d
move around files and clean up exports
aresnik11 Mar 26, 2026
6d9082a
update locale
aresnik11 Mar 26, 2026
15ceaf4
week starts on from locale + polyfill
aresnik11 Mar 27, 2026
2effd5f
remove button from DateCell
aresnik11 Mar 27, 2026
e6d8f15
refactor CalendarHeader
aresnik11 Mar 27, 2026
af18ad7
Merge branch 'main' into ajr-datepicker-localization
aresnik11 Mar 30, 2026
d189e1c
fix errors
aresnik11 Mar 30, 2026
4df63bb
fix ci errors
aresnik11 Mar 30, 2026
5005ee6
Merge branch 'main' into ajr-datepicker-localization
aresnik11 Mar 31, 2026
23458a7
input segments
aresnik11 Apr 6, 2026
bab532a
fix segments typing
aresnik11 Apr 6, 2026
8f4c3c2
Merge branch 'main' into ajr-datepicker-localization
aresnik11 Apr 6, 2026
dc273f3
dedupe and format
aresnik11 Apr 6, 2026
43007af
version plan
aresnik11 Apr 6, 2026
aa6748d
fix sb import
aresnik11 Apr 6, 2026
cd040bb
tests round 1
aresnik11 Apr 6, 2026
e086026
CalendarBody tests
aresnik11 Apr 6, 2026
3b676f2
style updates
aresnik11 Apr 7, 2026
878339f
utils tests
aresnik11 Apr 7, 2026
c87e08d
segmentUtils tests
aresnik11 Apr 8, 2026
e8fe147
segment tests
aresnik11 Apr 8, 2026
0394f4b
fix segment refs
aresnik11 Apr 9, 2026
1d7671a
close on date select
aresnik11 Apr 9, 2026
ed8e59c
fix focus & active range part
aresnik11 Apr 9, 2026
6431e61
move DatePicker to organism
aresnik11 Apr 10, 2026
464b82a
WIP mdx file
aresnik11 Apr 10, 2026
533625b
quick actions
aresnik11 Apr 13, 2026
32ce0ff
evan pr feedback
aresnik11 Apr 13, 2026
e4b9092
dont render empty border in footer
aresnik11 Apr 13, 2026
1cc29cd
first passthrough of PR feedback
aresnik11 Apr 14, 2026
decc8ec
reorg files
aresnik11 Apr 15, 2026
5283400
update disabled date logic
aresnik11 Apr 15, 2026
cedb664
fix icon button tip sticking around
aresnik11 Apr 15, 2026
e8d2435
discriminated union context
aresnik11 Apr 15, 2026
589f1e5
change to object syntax
aresnik11 Apr 15, 2026
0e746bc
PR feedback
aresnik11 Apr 16, 2026
de8c1f6
fix date keyboard nav
aresnik11 Apr 16, 2026
dda6e47
more tests & tweaks
aresnik11 Apr 16, 2026
5fbe2b9
more tests
aresnik11 Apr 16, 2026
94c7a2d
sync segment input and calendar month shown
aresnik11 Apr 16, 2026
885bb2d
pr feedback
aresnik11 Apr 21, 2026
fbcdf13
fix bugs
aresnik11 Apr 22, 2026
2475b6b
id clean up
aresnik11 Apr 22, 2026
80c03b8
always close calendar on escape
aresnik11 Apr 23, 2026
9104021
form tests
aresnik11 Apr 24, 2026
45e2a4e
remove robo comments
aresnik11 Apr 24, 2026
76dcd6c
prop comments and clean up
aresnik11 Apr 24, 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 }> = ({
Comment thread
aresnik11 marked this conversation as resolved.
Outdated
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>
);
218 changes: 218 additions & 0 deletions packages/gamut/src/DatePicker/Calendar/CalendarBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
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 {
getMonthGrid,
isDateDisabled,
isDateInRange,
isSameDay,
} from './utils/dateGrid';
import { getWeekdayFullNames, getWeekdayLabels } from './utils/format';
import { getDatesWithRow, keyHandler } from './utils/keyHandler';

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

const DateButton = styled(TextButton)(
states({
isToday: {
Comment thread
aresnik11 marked this conversation as resolved.
Outdated
position: 'relative',
'&::after': {
content: '""',
position: 'absolute',
bottom: 4,
left: '50%',
width: 4,
height: 4,
borderRadius: 'full',
bg: 'hyper',
},
},
isSelected: {
Comment thread
aresnik11 marked this conversation as resolved.
Outdated
bg: 'text',
color: 'background',
'&:hover, &:focus': {
bg: 'secondary-hover',
color: 'background',
},
'&::after': {
bg: 'background',
},
},
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',
},
}),
css({
fontWeight: 'base',
width: '32px',
})
);

export const CalendarBody: React.FC<CalendarBodyProps> = ({
visibleDate,
Comment thread
aresnik11 marked this conversation as resolved.
Outdated
selectedDate,
endDate = null,
disabledDates = [],
onDateSelect,
locale,
weekStartsOn = 0,
labelledById,
focusedDate,
onFocusedDateChange,
onVisibleDateChange,
onEscapeKeyPress,
}) => {
const year = visibleDate.getFullYear();
const month = visibleDate.getMonth();
const weeks = getMonthGrid(year, month, weekStartsOn);
const weekdayLabels = getWeekdayLabels(locale, weekStartsOn);
const weekdayFullNames = getWeekdayFullNames(locale, weekStartsOn);
const buttonRefs = useRef<Map<number, HTMLElement>>(new Map());

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

const isToday = useCallback(
(d: Date | null) => d !== null && isSameDay(d, 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,
onVisibleDateChange
),
[
onFocusedDateChange,
datesWithRow,
month,
year,
disabledDates,
onDateSelect,
onEscapeKeyPress,
onVisibleDateChange,
]
);

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
<td key={`empty-${rowIndex}-${colIndex}`} role="gridcell" />
);
}
const selected =
isSameDay(date, selectedDate) || isSameDay(date, endDate);
const inRange =
!!selectedDate &&
!!endDate &&
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 (
<td
aria-selected={selected}
key={date.getTime()}
role="gridcell"
>
<DateButton
disabled={disabled}
isInRange={inRange}
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>
</td>
);
})}
</tr>
))}
</tbody>
</table>
);
};
58 changes: 58 additions & 0 deletions packages/gamut/src/DatePicker/Calendar/CalendarFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as React from 'react';

import { FlexBox } from '../../Box';
import { TextButton } from '../../Button';
import { CalendarFooterProps } from './types';
import { getRelativeTodayLabel } from './utils/format';

Comment thread
aresnik11 marked this conversation as resolved.
Outdated
// function formatQuickActionLabel(action: QuickAction): string {
// const { num, timePeriod } = action;
// const period =
// timePeriod === 'day'
// ? num === 1
// ? 'day'
// : 'days'
// : timePeriod === 'week'
// ? num === 1
// ? 'week'
// : 'weeks'
// : timePeriod === 'month'
// ? num === 1
// ? 'month'
// : 'months'
// : num === 1
// ? 'year'
// : 'years';
// return `${num} ${period}`;
// }
Comment thread
aresnik11 marked this conversation as resolved.
Outdated

export const CalendarFooter: React.FC<CalendarFooterProps> = ({
onClearDate,
onTodayClick,
locale,
clearText,
disabled,
showClearButton,
}) => {
// const actions = quickActions.slice(0, 3);
Comment thread
aresnik11 marked this conversation as resolved.
Outdated

return (
<FlexBox
alignItems="center"
borderTop={1}
justifyContent="space-between"
p={12}
>
{showClearButton && (
<TextButton disabled={disabled} onClick={() => onClearDate?.()}>
{clearText}
</TextButton>
)}
<FlexBox gap={32}>
<TextButton onClick={() => onTodayClick?.()}>
{getRelativeTodayLabel(locale)}
</TextButton>
</FlexBox>
</FlexBox>
);
};
Loading
Loading