Skip to content

Commit

Permalink
refactor date input
Browse files Browse the repository at this point in the history
  • Loading branch information
sirineJ committed Dec 27, 2024
1 parent 6620099 commit 39c0366
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 527 deletions.
25 changes: 20 additions & 5 deletions packages/circuit-ui/components/DateInput/DateInput.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
}

.content {
padding: var(--cui-spacings-mega);
color: var(--cui-fg-normal);
background-color: var(--cui-bg-elevated);
border: var(--cui-border-width-kilo) solid var(--cui-border-subtle);
Expand All @@ -118,17 +119,19 @@

@media (max-width: 479px) {
.content {
padding: 0;
border: none;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
box-shadow: none;
}
}

.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--cui-spacings-giga) var(--cui-spacings-mega)
var(--cui-spacings-byte) var(--cui-spacings-mega);
margin: var(--cui-spacings-byte) 0;
}

@media (min-width: 480px) {
Expand All @@ -151,16 +154,28 @@
}

.calendar {
padding: var(--cui-spacings-mega);
margin: var(--cui-spacings-mega) 0;
}

.divider {
width: calc(100% + 32px) !important;
margin-left: -16px;
}

.buttons {
display: flex;
flex-wrap: wrap;
gap: var(--cui-spacings-kilo);
justify-content: space-between;
padding: var(--cui-spacings-mega);
border-top: var(--cui-border-width-kilo) solid var(--cui-border-divider);
}

.popover {
max-width: min(410px, 100vw);
box-shadow: none !important;
}

.popover::after {
visibility: hidden;
}

.apply {
Expand Down
220 changes: 77 additions & 143 deletions packages/circuit-ui/components/DateInput/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,13 @@

import {
forwardRef,
useEffect,
useId,
useRef,
useState,
type InputHTMLAttributes,
} from 'react';
import type { Temporal } from 'temporal-polyfill';
import {
flip,
offset,
shift,
size,
useFloating,
type Placement,
} from '@floating-ui/react-dom';
import type { Placement } from '@floating-ui/react-dom';
import { Calendar as CalendarIcon } from '@sumup-oss/icons';

import type { ClickEvent } from '../../types/events.js';
Expand All @@ -45,7 +37,6 @@ import { clsx } from '../../styles/clsx.js';
import type { InputProps } from '../Input/Input.js';
import { Calendar, type CalendarProps } from '../Calendar/Calendar.js';
import { Button } from '../Button/Button.js';
import { CloseButton } from '../CloseButton/CloseButton.js';
import { IconButton } from '../Button/IconButton.js';
import { Headline } from '../Headline/Headline.js';
import {
Expand All @@ -58,8 +49,9 @@ import {
import { toPlainDate } from '../../util/date.js';
import { applyMultipleRefs } from '../../util/refs.js';
import { changeInputValue } from '../../util/input-value.js';
import { Popover } from '../Popover/index.js';
import { Hr } from '../Hr/index.js';

import { Dialog } from './components/Dialog.js';
import { DateSegment } from './components/DateSegment.js';
import { usePlainDateState } from './hooks/usePlainDateState.js';
import { useSegmentFocus } from './hooks/useSegmentFocus.js';
Expand Down Expand Up @@ -219,51 +211,6 @@ export const DateInput = forwardRef<HTMLInputElement, DateInputProps>(
const [open, setOpen] = useState(false);
const [selection, setSelection] = useState<Temporal.PlainDate>();

const padding = 16; // px

const { floatingStyles, update } = useFloating({
open,
placement,
strategy: 'fixed',
middleware: [
offset(4),
flip({ padding, fallbackAxisSideDirection: 'start' }),
shift({ padding }),
size({
padding,
apply({ availableHeight, elements }) {
elements.floating.style.maxHeight = `${availableHeight}px`;
},
}),
],
elements: {
reference: calendarButtonRef.current,
floating: dialogRef.current,
},
});

useEffect(() => {
/**
* When we support `ResizeObserver` (https://caniuse.com/resizeobserver),
* we can look into using Floating UI's `autoUpdate` (but we can't use
* `whileElementIsMounted` because our implementation hides the floating
* element using CSS instead of using conditional rendering.
* See https://floating-ui.com/docs/react-dom#updating
*/
if (open) {
update();
window.addEventListener('resize', update);
window.addEventListener('scroll', update);
} else {
window.removeEventListener('resize', update);
window.removeEventListener('scroll', update);
}
return () => {
window.removeEventListener('resize', update);
window.removeEventListener('scroll', update);
};
}, [open, update]);

// Focus the first date segment when clicking anywhere on the field...
const handleClick = (event: ClickEvent) => {
const element = event.target as HTMLElement;
Expand All @@ -274,9 +221,9 @@ export const DateInput = forwardRef<HTMLInputElement, DateInputProps>(
focus.next();
};

const openCalendar = () => {
const toggleCalendar = () => {
setSelection(state.date);
setOpen(true);
setOpen((prev) => !prev);
};

const closeCalendar = () => {
Expand Down Expand Up @@ -306,16 +253,6 @@ export const DateInput = forwardRef<HTMLInputElement, DateInputProps>(
closeCalendar();
};

const mobileStyles = {
position: 'fixed',
top: 'auto',
right: '0px',
bottom: '0px',
left: '0px',
} as const;

const dialogStyles = isMobile ? mobileStyles : floatingStyles;

const segments = getDateSegments(locale);
const calendarButtonLabel = getCalendarButtonLabel(
openCalendarButtonLabel,
Expand Down Expand Up @@ -435,20 +372,79 @@ export const DateInput = forwardRef<HTMLInputElement, DateInputProps>(
}
})}
</div>
<IconButton
ref={calendarButtonRef}
type="button"
icon={CalendarIcon}
variant="secondary"
onClick={openCalendar}
className={classes['calendar-button']}
disabled={disabled || readOnly}
aria-expanded={open}
aria-haspopup="true"
aria-controls={dialogId}
<Popover
ref={dialogRef}
id={dialogId}
isOpen={open}
onToggle={closeCalendar}
aria-labelledby={headlineId}
className={clsx(!isMobile && classes.popover)}
offset={4}
placement={placement}
closeButtonLabel={closeCalendarButtonLabel}
component={() => (
<IconButton
ref={calendarButtonRef}
type="button"
icon={CalendarIcon}
variant="secondary"
onClick={toggleCalendar}
className={classes['calendar-button']}
disabled={disabled || readOnly}
aria-expanded={open}
aria-haspopup="true"
aria-controls={dialogId}
>
{calendarButtonLabel}
</IconButton>
)}
>
{calendarButtonLabel}
</IconButton>
{() => (
<div className={clsx(!isMobile && classes.content)}>
<header className={classes.header}>
<Headline as="h2" size="m" id={headlineId}>
{label}
</Headline>
</header>

<Calendar
className={classes.calendar}
onSelect={handleSelect}
selection={selection}
minDate={minDate}
maxDate={maxDate}
locale={locale}
firstDayOfWeek={firstDayOfWeek}
modifiers={modifiers}
prevMonthButtonLabel={prevMonthButtonLabel}
nextMonthButtonLabel={nextMonthButtonLabel}
/>
<Hr className={classes.divider} />

{(!required || isMobile) && (
<div className={classes.buttons}>
{!required && (
<Button
type="button"
variant="tertiary"
onClick={handleClear}
>
{clearDateButtonLabel}
</Button>
)}
<Button
type="button"
variant="primary"
onClick={handleApply}
className={classes.apply}
>
{applyDateButtonLabel}
</Button>
</div>
)}
</div>
)}
</Popover>
</div>
<FieldValidationHint
id={validationHintId}
Expand All @@ -459,68 +455,6 @@ export const DateInput = forwardRef<HTMLInputElement, DateInputProps>(
validationHint={validationHint}
/>
</FieldSet>
<Dialog
ref={dialogRef}
id={dialogId}
open={open}
isModal={isMobile}
onClose={closeCalendar}
aria-labelledby={headlineId}
style={dialogStyles}
>
{() => (
<div className={classes.content}>
<header className={classes.header}>
<Headline as="h2" size="m" id={headlineId}>
{label}
</Headline>
<CloseButton
size="s"
variant="tertiary"
onClick={closeCalendar}
className={classes['close-button']}
>
{closeCalendarButtonLabel}
</CloseButton>
</header>

<Calendar
className={classes.calendar}
onSelect={handleSelect}
selection={selection}
minDate={minDate}
maxDate={maxDate}
locale={locale}
firstDayOfWeek={firstDayOfWeek}
modifiers={modifiers}
prevMonthButtonLabel={prevMonthButtonLabel}
nextMonthButtonLabel={nextMonthButtonLabel}
/>

{(!required || isMobile) && (
<div className={classes.buttons}>
{!required && (
<Button
type="button"
variant="tertiary"
onClick={handleClear}
>
{clearDateButtonLabel}
</Button>
)}
<Button
type="button"
variant="primary"
onClick={handleApply}
className={classes.apply}
>
{applyDateButtonLabel}
</Button>
</div>
)}
</div>
)}
</Dialog>
</FieldWrapper>
);
},
Expand Down
Loading

0 comments on commit 39c0366

Please sign in to comment.