Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#65 Separate components from IntlProvider dependency #66

Merged
merged 12 commits into from
Apr 26, 2023
64 changes: 46 additions & 18 deletions libs/core/src/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,30 @@ import * as React from "react";

import { range, slice } from "lodash";

import { Intl, useIntlContext } from "@tiller-ds/intl";
import { ComponentTokens, cx, useIcon, useTokens } from "@tiller-ds/theme";
import { useViewport } from "@tiller-ds/util";

import ButtonGroups from "./ButtonGroups";

import { useViewport } from "@tiller-ds/util";

export type PaginationProps = {
/**
* Complex values of types PageInfo and Components for formatting purposes.
*/
children?: (pageInfo: PageInfo, components: Components) => React.ReactNode;

/**
* Custom function activated when the page changes (takes the current page as a parameter)
* Custom additional class name for the main component.
*/
className?: string;

/**
* Custom icon for going to the next page.
*/
nextIcon?: React.ReactElement;

/**
* Custom function activated when the page changes (takes the current page as a parameter).
*/
onPageChange?: (page: number) => void;

Expand All @@ -42,28 +52,29 @@ export type PaginationProps = {
pageNumber: number;

/**
* Defines the size of each page (number of shown elements on each page).
* Defines a calculator for displaying page numbers.
*/
pageSize: number;
pagerCalculator?: (pageNumber: number, pageCount: number) => PagerPage[];

/**
* Defines the total number of elements on a source that the component is hooked up to.
* Enables or disables the page summary text next to the pagination.
*/
totalElements: number;
pageSummary?: boolean;

/**
* Defines a calculator for displaying page numbers.
* Defines the size of each page (number of shown elements on each page).
*/
pagerCalculator?: (pageNumber: number, pageCount: number) => PagerPage[];
pageSize: number;

/**
* Custom icon for going to the previous page.
*/
previousIcon?: React.ReactElement;

nextIcon?: React.ReactElement;

/**
* Custom additional class name for the main component.
* Defines the total number of elements on a source that the component is hooked up to.
*/
className?: string;
totalElements: number;
} & PaginationTokensProps;

type PaginationTokensProps = {
Expand Down Expand Up @@ -130,7 +141,7 @@ export function useLocalPagination<T>(data: T[], pageSize = 10): [PaginationStat
}

export default function Pagination(props: PaginationProps) {
const { pageNumber, pageSize, totalElements } = props;
const { pageNumber, pageSize, totalElements, pageSummary = true } = props;
const tokens = useTokens("Pagination", props.tokens);

const calculatedProps: CalculatedProps = {
Expand All @@ -140,15 +151,17 @@ export default function Pagination(props: PaginationProps) {

return (
<section className={tokens.master}>
<PageSummary {...props} />
{pageSummary && <PageSummary {...props} />}
<Pager {...props} {...calculatedProps} />
</section>
);
}

function PageSummary({ pageNumber, pageSize, totalElements, children, ...props }: PaginationProps) {
const calculatedFrom = pageNumber * pageSize;
const intl = useIntlContext();
const tokens = useTokens("Pagination", props.tokens);

const calculatedFrom = pageNumber * pageSize;
// user counting starts from 1
const from = totalElements === 0 ? calculatedFrom : calculatedFrom + 1;
const to = Math.min(totalElements, (pageNumber + 1) * pageSize);
Expand All @@ -162,13 +175,28 @@ function PageSummary({ pageNumber, pageSize, totalElements, children, ...props }
if (children) {
return children(
{ pageNumber, pageSize, totalElements, from, to },
{ from: fromElement, to: toElement, totalElements: totalElement }
{ from: fromElement, to: toElement, totalElements: totalElement },
) as React.ReactElement;
}

return (
<p className={pageSummaryClassName}>
Showing {fromElement} to {toElement} of {totalElement} results
{intl ? (
<Intl
name={intl.commonKeys["paginationSummary"] as string}
params={{ from: fromElement, to: toElement, total: totalElement }}
>
{{
from: () => fromElement,
to: () => toElement,
total: () => totalElement,
}}
</Intl>
) : (
<>
Showing {fromElement} to {toElement} of {totalElement} results
jtomic-croz marked this conversation as resolved.
Show resolved Hide resolved
</>
)}
</p>
);
}
Expand Down
39 changes: 37 additions & 2 deletions libs/date/src/Date.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,51 @@

import * as React from "react";

import * as dateFns from "date-fns";
import { FormattedDate } from "react-intl";

import { useIntlContext } from "@tiller-ds/intl";

import { formatDate } from "./utils";

export type DateProps = {
/**
* Data handed to the component in a valid date format.
*
* If using the component **with** IntlProvider you don't need to pass _format_ and _formatTo_ props, the
* format will be inferred from the locale.
*
* If using the component **without** IntlProvider, you must also pass _format_ and _formatTo_ props.
*
* Other props come from Intl.DateTimeFormatOptions (https://bit.ly/3urm8s5)
*/
children: Date;
children: string;
fbeceic marked this conversation as resolved.
Show resolved Hide resolved

/**
* Format of the passed date (e.g. 'dd. MM. yyyy.' or 'MM/dd/yyyy').
*
* Pass it if you wish to manually convert the date (without the help of Intl).
*/
formatFrom?: string;

/**
* Format of formatted date (e.g. 'dd. MM. yyyy.' or 'MM/dd/yyyy').
*/
formatTo?: string;
} & Intl.DateTimeFormatOptions;

export default function Date({ children, ...props }: DateProps) {
export default function Date({ children, formatFrom, formatTo, ...props }: DateProps) {
const intl = useIntlContext();

if (!intl && !formatFrom && !formatTo) {
throw new Error(
"You must pass format and formatTo props if you are using the Date component without IntlProvider.",
);
}

if (formatFrom && formatTo) {
return <>{dateFns.format(formatDate(children, formatFrom) as Date, formatTo)}</>;
}

return <FormattedDate value={children} {...props} />;
}
5 changes: 3 additions & 2 deletions libs/date/src/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export default function DateInput({
}: DateInputProps) {
const inputRef = React.useRef<HTMLInputElement>(null);
const datePickerRef = React.useRef<HTMLDivElement>(null);
const { lang } = useIntlContext();
const lang = useIntlContext(true)?.lang;

const finalDateFormat = dateFormat?.replace(/m/g, "M") || getDateFormatByLang(lang);
const formattedValue = value ? dateFns.format(value, finalDateFormat) : "";
Expand Down Expand Up @@ -314,7 +314,8 @@ function DateInputInput({
dateFormat,
...props
}: DateInputInputProps) {
const { lang } = useIntlContext();
const lang = useIntlContext(true)?.lang;

const tokens = useTokens("DateInput", props.tokens);

const dateIconClassName = cx({ [tokens.DatePicker.range.iconColor]: !(props.disabled || props.readOnly) });
Expand Down
16 changes: 10 additions & 6 deletions libs/date/src/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@

import * as React from "react";

import * as dateFns from "date-fns";
import { useDatepicker, useDay, useMonth, UseMonthProps } from "@datepicker-react/hooks";
import { useIntl } from "react-intl";

import { useIntlContext } from "@tiller-ds/intl";
import { cx, TokenProps, useIcon, useTokens } from "@tiller-ds/theme";

type DatePickerProps = {
Expand Down Expand Up @@ -354,13 +355,14 @@ function YearsPickerContainer({ ...props }) {

function MonthPickerLabels({ month, year, ...props }: MonthPickerLabelsProps) {
const tokens = useTokens("DateInput", props.tokens);
const intl = useIntl();
const intlContext = useIntlContext();
const { onDayPickerToggle, dayPicker, onActiveMonthToggle } = useDatePickerContext();

const { monthLabel } = useMonth({
year,
month,
monthLabelFormat: (date: Date) => intl.formatDate(date, { month: "long" }),
monthLabelFormat: (date: Date) =>
intlContext?.intl.formatDate(date, { month: "long" }) || dateFns.format(date, "MMMM"),
});

const onChevronDownClick = () => {
Expand Down Expand Up @@ -396,14 +398,16 @@ function MonthPickerLabels({ month, year, ...props }: MonthPickerLabelsProps) {
}

function MonthPicker({ year, month, firstDayOfWeek }: MonthPickerProps) {
const intl = useIntl();
const intlContext = useIntlContext();

const { days, weekdayLabels } = useMonth({
year,
month,
firstDayOfWeek,
monthLabelFormat: (date: Date) => intl.formatDate(date, { month: "long" }),
weekdayLabelFormat: (date: Date) => intl.formatDate(date, { weekday: "short" }),
monthLabelFormat: (date: Date) =>
intlContext?.intl.formatDate(date, { month: "long" }) || dateFns.format(date, "MMMM"),
weekdayLabelFormat: (date: Date) =>
intlContext?.intl.formatDate(date, { weekday: "short" }) || dateFns.format(date, "EEEEEE"),
});

return (
Expand Down
4 changes: 2 additions & 2 deletions libs/date/src/DateRangeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export default function DateRangeInput({
highlightToday,
...props
}: DateRangeInputProps) {
const { lang } = useIntlContext();
const lang = useIntlContext(true)?.lang;

const finalDateFormat = dateFormat?.replace(/m/g, "M") || getDateFormatByLang(lang);
const formattedStart = start ? dateFns.format(start, finalDateFormat) : "";
Expand Down Expand Up @@ -427,7 +427,7 @@ function DateRangeInputInput({
dateFormat,
...props
}: DateRangeInputInputProps) {
const { lang } = useIntlContext();
const lang = useIntlContext(true)?.lang;

const tokens = useTokens("DateInput", props.tokens);
const dateIconClassName = cx({ [tokens.DatePicker.range.iconColor]: !(props.disabled || props.readOnly) });
Expand Down
2 changes: 1 addition & 1 deletion libs/date/src/DateTimeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export default function DateTimeInput({
highlightToday,
...props
}: DateTimeInputProps & DateTimeInputTokens) {
const { lang } = useIntlContext();
const lang = useIntlContext(true)?.lang;

const dateTimePickerTokens = useTokens("DateTimePicker", props.dateTimePickerTokens);
const dateTimeInputTokens = useTokens("DateTimeInput", props.dateTimeInputTokens);
Expand Down
3 changes: 0 additions & 3 deletions libs/date/src/TimeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import Popover, { positionMatchWidth } from "@reach/popover";

import { IconButton } from "@tiller-ds/core";
import { defaultPlaceholderChar, InputProps, MaskedInput } from "@tiller-ds/form-elements";
import { useIntlContext } from "@tiller-ds/intl";
import { ComponentTokens, cx, useIcon, useTokens } from "@tiller-ds/theme";

import TimePicker, { ClockType, TimePickerProps } from "./TimePicker";
Expand Down Expand Up @@ -148,8 +147,6 @@ export default function TimeInput({
showMaskOnEmpty,
...props
}: TimeInputProps & TimeInputTokens) {
const { lang } = useIntlContext();

const isTwelveHours = type === "use12Hours";
const timeInputTokens = useTokens("TimeInput", props.timeInputTokens);
const inputTokens = useTokens("Input", props.inputTokens);
Expand Down
2 changes: 1 addition & 1 deletion libs/form-elements/src/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export function FieldLabel({ id, label, required, tooltip, ...props }: FieldLabe
const inputTokens = useTokens("Input", props.inputTokens);
const fieldTokens = useTokens("Field", props.fieldTokens);

const requiredLabelText = useLabel("required");
const requiredLabelText = useLabel("required", "Required field");

return (
<Label id={id}>
Expand Down
4 changes: 2 additions & 2 deletions libs/form-elements/src/FieldGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function FieldGroup({
const fieldGroupTokens = useTokens("FieldGroup", props.fieldGroupTokens);
const inputTokens = useTokens("Input", props.inputTokens);

const requiredLabelText = useLabel("required");
const requiredLabelText = useLabel("required", "Required field");
jtomic-croz marked this conversation as resolved.
Show resolved Hide resolved

let errorMessage = error;

Expand All @@ -102,7 +102,7 @@ function FieldGroup({
className,
fieldGroupTokens.Group.content.master,
{ [fieldGroupTokens.Group.content.horizontal]: !vertical },
{ [fieldGroupTokens.Group.content.vertical]: vertical }
{ [fieldGroupTokens.Group.content.vertical]: vertical },
);

return (
Expand Down
54 changes: 30 additions & 24 deletions libs/form-elements/src/NumberInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,28 +92,34 @@ export type NumberInputProps = {
} & Omit<NumberFormatProps, NumberFormatOnlyPropsUnion>;

export default function NumberInput({ name, onChange, onBlur, ...props }: NumberInputProps) {
fbeceic marked this conversation as resolved.
Show resolved Hide resolved
const { intl } = useIntlContext();
const decimalSeparator = getDecimalSeparator(intl);
const thousandSeparator = getThousandSeparator(intl);

const id = `numberformat-${name}`;

return (
<ReactNumberFormat
id={id}
data-testid={id}
name={name}
decimalSeparator={decimalSeparator}
thousandSeparator={thousandSeparator}
allowedDecimalSeparators={[decimalSeparator]}
onValueChange={(values) => {
if (onChange) {
onChange(values.floatValue);
}
}}
customInput={Input}
onBlur={onBlur}
{...props}
/>
);
const intlContext = useIntlContext();

if (intlContext) {
const { intl } = intlContext;
const decimalSeparator = getDecimalSeparator(intl);
const thousandSeparator = getThousandSeparator(intl);

const id = `numberformat-${name}`;

return (
<ReactNumberFormat
id={id}
data-testid={id}
name={name}
decimalSeparator={decimalSeparator}
thousandSeparator={thousandSeparator}
allowedDecimalSeparators={[decimalSeparator]}
onValueChange={(values) => {
if (onChange) {
onChange(values.floatValue);
}
}}
customInput={Input}
onBlur={onBlur}
{...props}
/>
);
} else {
throw new Error("NumberInput component requires IntlProvider wrapper for functioning.");
}
}
Loading