Skip to content

Commit

Permalink
feat: datepicker (#665)
Browse files Browse the repository at this point in the history
* fix: rename datepicker to daterangepicker

* Move into date folder + wip: datepicker

* Feat: <DatePicker/> few fixes and improvements to the DateRangePicker

* Feat: new BackgroundAccent

When it is unclear if the element will be used on a background or
backgroundAlt the backgroundAccent can be used. This will probably most
often be used for borders where the background is background(alt).

* wip relative + few fixes

* Feat: add relative picker

* Fix: update DateRangePicker to use new component name

* Feat: loading state + popover placement prop

* chore: add popoverplacement prop to stories as selectable option

---------

Co-authored-by: Niek Candaele <[email protected]>
  • Loading branch information
emielvanseveren and niekcandaele authored Nov 18, 2023
1 parent af5c2c8 commit 29fb5cf
Show file tree
Hide file tree
Showing 40 changed files with 1,075 additions and 364 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { styled } from '../../../styled';

const Container = styled.div`
background-color: ${({ theme }) => theme.colors.background};
border: 0.1rem solid ${({ theme }) => theme.colors.secondary};
border: 0.1rem solid ${({ theme }) => theme.colors.backgroundAccent};
border-radius: ${({ theme }) => theme.borderRadius.medium};
padding: ${({ theme }) => theme.spacing['0_5']};
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { styled } from '../../../styled';
const Container = styled.div<{ divider: boolean; hasChildren: boolean }>`
padding-bottom: ${({ theme }) => theme.spacing['0_5']};
border-bottom: ${({ divider, theme, hasChildren }) =>
divider && hasChildren && `2px solid ${theme.colors.secondary}`};
divider && hasChildren && `2px solid ${theme.colors.backgroundAccent}`};
margin-bottom: ${({ divider, theme, hasChildren }) => divider && hasChildren && theme.spacing['0_5']};
padding-left: 0;
Expand Down
2 changes: 1 addition & 1 deletion packages/lib-components/src/components/data/Table/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const StyledTable = styled.table<{ density: Density }>`
}
td {
border-bottom: 1px solid ${({ theme }) => theme.colors.secondary};
border-bottom: 1px solid ${({ theme }) => theme.colors.backgroundAccent};
}
th {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import { Meta, StoryFn } from '@storybook/react';
import { Popover, PopoverProps } from '.';
import { IconButton } from '../../actions';
import { AiFillBug as BugIcon } from 'react-icons/ai';

export default {
title: 'Feedback/Popover',
Expand All @@ -12,7 +14,20 @@ export default {

export const UnControlled: StoryFn<PopoverProps> = () => (
<Popover>
<Popover.Trigger>hover this to open it the popover</Popover.Trigger>
<Popover.Trigger>Click this to open it in the popover</Popover.Trigger>
<Popover.Content>this is the content of the popover</Popover.Content>
</Popover>
);

export const CustomTrigger: StoryFn<PopoverProps> = () => (
<>
By default trigger is rendered as a button, but you can pass any component as a trigger by setting the asChild prop
on the Popover.Trigger component.
<Popover>
<Popover.Trigger asChild>
<IconButton icon={<BugIcon />} ariaLabel="click me" />
</Popover.Trigger>
<Popover.Content>this is the content of the popover</Popover.Content>
</Popover>
</>
);
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useTheme } from '../../../hooks';

const Container = styled.div`
background-color: ${({ theme }) => theme.colors.background};
border: 0.1rem solid ${({ theme }) => theme.colors.secondary};
border: 0.1rem solid ${({ theme }) => theme.colors.backgroundAccent};
border-radius: ${({ theme }) => theme.borderRadius.medium};
`;

Expand All @@ -32,7 +32,7 @@ export const PopoverContent = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElemen
ref={arrowRef}
context={floatingContext}
fill={theme.colors.background}
stroke={theme.colors.backgroundAlt}
stroke={theme.colors.backgroundAccent}
strokeWidth={1}
/>
</Container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
export interface PopoverOptions {
initialOpen?: boolean;
placement?: Placement;
/// If focus is modal, focus is trapped inside the floating element and outside content cannot be accessed.
/// When set, the floating element should have a dedicated close button.
modal?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { FC, useState } from 'react';
import { defaultInputProps, defaultInputPropsFactory, ControlledInputProps } from '../../InputProps';
import { useController } from 'react-hook-form';
import { GenericDatePicker, DatePickerProps } from './Generic';
import { Label, ErrorMessage, Wrapper, Description } from '../../layout';
import { Container } from './style';
import { Skeleton } from '../../../../components';

export type ControlledDatePickerProps = ControlledInputProps & DatePickerProps;
const defaultsApplier = defaultInputPropsFactory<ControlledDatePickerProps>(defaultInputProps);

export const ControlledDatePicker: FC<ControlledDatePickerProps> = (props) => {
const {
label,
name,
size,
hint,
control,
disabled,
readOnly,
required,
format,
mode,
relativePickerOptions,
timePickerOptions,
placeholder,
popOverPlacement,
loading,
description,
} = defaultsApplier(props);

const [showError, setShowError] = useState<boolean>(true);

const {
field,
fieldState: { error },
} = useController({
name,
control,
});

const handleOnFocus = () => {
setShowError(false);
};

const handleOnBlur = () => {
field.onBlur();
setShowError(true);
};

return (
<Wrapper>
<Container>
{label && (
<Label
required={required}
hint={hint}
htmlFor={name}
error={!!error}
size={size}
text={label}
disabled={disabled}
position="top"
/>
)}
{loading ? (
<Skeleton variant="text" width="100%" height="38px" />
) : (
<GenericDatePicker
onChange={field.onChange}
onFocus={handleOnFocus}
onBlur={handleOnBlur}
value={field.value}
disabled={disabled}
name={name}
id={name}
required={required}
size={size}
readOnly={readOnly}
hasError={!!error}
popOverPlacement={popOverPlacement}
hasDescription={!!description}
relativePickerOptions={relativePickerOptions}
timePickerOptions={timePickerOptions}
format={format}
placeholder={placeholder}
mode={mode}
/>
)}
{showError && error?.message && <ErrorMessage message={error.message} />}
</Container>
{description && <Description description={description} inputName={name} />}
</Wrapper>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import React from 'react';
import { useState } from 'react';
import { Meta, StoryFn } from '@storybook/react';
import { styled } from '../../../../styled';
import { Button, DatePicker, DatePickerProps } from '../../../../components';
import { useForm, SubmitHandler } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { DateTime } from 'luxon';

const Wrapper = styled.div`
display: flex;
height: 100vh;
width: 50%;
margin: 0 auto;
flex-direction: column;
align-items: center;
justify-content: center;
`;

export default {
title: 'Inputs/DatePicker',
component: DatePicker,
args: {
label: 'date',
name: 'date',
required: false,
description: 'This is a description',
popOverPlacement: 'bottom',
readOnly: false,
loading: false,
timePickerOptions: {
interval: 30,
},
relativePickerOptions: {
showFriendlyName: true,
timeDirection: 'future',
},
},
} as Meta<DatePickerProps>;

const validationSchema = z.object({
date: z.string().datetime({ offset: true }),
});
type FormFields = { date: string };

export const OnDateSubmit: StoryFn<DatePickerProps> = (args) => {
const [result, setResult] = useState<string>('none');

const { control, handleSubmit } = useForm<FormFields>({
mode: 'onSubmit',
resolver: zodResolver(validationSchema),
});

const onSubmit: SubmitHandler<FormFields> = ({ date }) => {
setResult(date);
};

return (
<Wrapper>
Note: for zod to accept the date, an extra option is needed: {'{ offset: true }'}
<form onSubmit={handleSubmit(onSubmit)}>
<DatePicker
mode="absolute"
control={control}
label={args.label}
name={args.name}
required={args.required}
loading={args.loading}
hint={args.hint}
description={args.description}
popOverPlacement={args.popOverPlacement}
/>
<Button type="submit" text="Submit form" />
</form>
<p>result: {result}</p>
</Wrapper>
);
};

export const OnDateAndTimeSubmit: StoryFn<DatePickerProps> = (args) => {
const [result, setResult] = useState<string>('none');

const { control, handleSubmit } = useForm<FormFields>({
mode: 'onSubmit',
resolver: zodResolver(validationSchema),
});

const onSubmit: SubmitHandler<FormFields> = ({ date }) => {
setResult(date);
};

return (
<Wrapper>
Note: for zod to accept the date, an extra option is needed: {'{ offset: true }'}
<form onSubmit={handleSubmit(onSubmit)}>
<DatePicker
mode="absolute"
control={control}
label={args.label}
name={args.name}
required={args.required}
loading={args.loading}
hint={args.hint}
format={DateTime.DATETIME_SHORT}
timePickerOptions={args.timePickerOptions}
description={args.description}
popOverPlacement={args.popOverPlacement}
/>
<Button type="submit" text="Submit form" />
</form>
<p>result: {result}</p>
</Wrapper>
);
};

export const onTimeSubmit: StoryFn<DatePickerProps> = (args) => {
const [result, setResult] = useState<string>('none');

const { control, handleSubmit } = useForm<FormFields>({
mode: 'onSubmit',
resolver: zodResolver(validationSchema),
});

const onSubmit: SubmitHandler<FormFields> = ({ date }) => {
setResult(date);
};

return (
<Wrapper>
Note: for zod to accept the date, an extra option is needed: {'{ offset: true }'}
<form onSubmit={handleSubmit(onSubmit)}>
<DatePicker
mode="absolute"
control={control}
label={args.label}
name={args.name}
required={args.required}
loading={args.loading}
hint={args.hint}
format={DateTime.TIME_SIMPLE}
timePickerOptions={args.timePickerOptions}
description={args.description}
popOverPlacement={args.popOverPlacement}
/>
<Button type="submit" text="Submit form" />
</form>
<p>result: {result}</p>
</Wrapper>
);
};

export const RelativeSubmit: StoryFn<DatePickerProps> = (args) => {
const [result, setResult] = useState<string>('none');

const { control, handleSubmit } = useForm<FormFields>({
mode: 'onSubmit',
resolver: zodResolver(validationSchema),
});

const onSubmit: SubmitHandler<FormFields> = ({ date }) => {
setResult(date);
};

return (
<Wrapper>
Note: for zod to accept the date, an extra option is needed: {'{ offset: true }'}
<form onSubmit={handleSubmit(onSubmit)}>
<DatePicker
mode="relative"
control={control}
label={args.label}
name={args.name}
required={args.required}
loading={args.loading}
hint={args.hint}
format={DateTime.DATETIME_MED_WITH_SECONDS}
timePickerOptions={args.timePickerOptions}
relativePickerOptions={args.relativePickerOptions}
description={args.description}
popOverPlacement={args.popOverPlacement}
/>
<Button type="submit" text="Submit form" />
</form>
<p>result: {result}</p>
</Wrapper>
);
};
Loading

0 comments on commit 29fb5cf

Please sign in to comment.