Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Add

- Localization, #70.

## [0.0.8] - 2022-05-12

No package update.
Expand Down
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,14 @@
"prepare": "husky install"
},
"type": "module",
"dependencies": {},
"dependencies": {
"@date-io/date-fns": "^2.13.2",
"@date-io/date-fns-jalali": "^2.13.2",
"@date-io/dayjs": "^2.13.2",
"@date-io/jalaali": "^2.13.2",
"@date-io/luxon": "^2.13.2",
"@date-io/moment": "^2.13.2"
},
"devDependencies": {
"@chakra-ui/icons": "^1.1.7",
"@chakra-ui/react": "^1.8.6",
Expand All @@ -53,6 +60,7 @@
"@typescript-eslint/eslint-plugin": "^5.19.0",
"@typescript-eslint/parser": "^5.19.0",
"@vitejs/plugin-react": "^1.0.7",
"date-fns": "^2.28.0",
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-react": "^7.29.4",
Expand Down
62 changes: 62 additions & 0 deletions src/LocalizationProvider/LocalizationProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Ref: https://github.com/mui/mui-x/blob/5dc5bd4b04eecf21c45cf6ea1169727ba5ab3be3/packages/x-date-pickers/src/LocalizationProvider/LocalizationProvider.tsx

import * as React from "react";
import { DateIOFormats } from "@date-io/core/IUtils";
import { IUtils } from "@date-io/core/IUtils";

type DateAdapter<TDate> = IUtils<TDate>;

export interface DateAdapterContextValue<TDate> {
utils: DateAdapter<TDate>;
}

export const DateAdapterContext =
React.createContext<DateAdapterContextValue<unknown> | null>(null);
if (process.env.NODE_ENV !== "production") {
DateAdapterContext.displayName = "DateAdapterContext";
}

export interface LocalizationProviderProps {
children?: React.ReactNode;
/** DateIO adapter class function */
dateAdapter: new (...args: any) => DateAdapter<unknown>;
/** Formats that are used for the child dropdown */
dateFormats?: Partial<DateIOFormats>;
/**
* Date library instance you are using, if it has some global overrides
* ```jsx
* dateLibInstance={momentTimeZone}
* ```
*/
dateLibInstance?: any;
/** Locale for the date library you are using */
locale?: string | object;
}

/**
* @ignore - do not document.
*/
export function LocalizationProvider(props: LocalizationProviderProps) {
const {
children,
dateAdapter: Utils,
dateFormats,
dateLibInstance,
locale,
} = props;
const utils = React.useMemo(
() =>
new Utils({ locale, formats: dateFormats, instance: dateLibInstance }),
[Utils, locale, dateFormats, dateLibInstance]
);

const contextValue: DateAdapterContextValue<unknown> = React.useMemo(() => {
return { utils };
}, [utils]);

return (
<DateAdapterContext.Provider value={contextValue}>
{children}
</DateAdapterContext.Provider>
);
}
1 change: 1 addition & 0 deletions src/LocalizationProvider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./LocalizationProvider";
12 changes: 12 additions & 0 deletions src/LocalizationProvider/useUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as React from "react";
import { DateAdapterContext } from "./LocalizationProvider";

export const useUtils = () => {
const localization = React.useContext(DateAdapterContext);

if (localization == null) {
return null;
}

return localization.utils;
};
1 change: 1 addition & 0 deletions src/adapters/date-fns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "@date-io/date-fns";
1 change: 1 addition & 0 deletions src/adapters/dayjs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "@date-io/dayjs";
1 change: 1 addition & 0 deletions src/adapters/luxon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "@date-io/luxon";
1 change: 1 addition & 0 deletions src/adapters/moment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "@date-io/moment";
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export {
export * from "./use-date-select";
export { getDateString } from "./date-string";
export * from "./types";
export * from "./LocalizationProvider";
73 changes: 53 additions & 20 deletions src/use-date-select.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { range } from "./range";
import { compileDateString, parseDateString } from "./date-string";
import { Option, Options } from "./types";
import { Options } from "./types";
import { useUtils } from "./LocalizationProvider/useUtils";

const DEFAULT_MIN_YEAR = 1960;
const DEFAULT_MAX_YEAR = new Date().getFullYear();
Expand All @@ -13,17 +14,6 @@ function convertToSelectValue(value: number): string {
return value.toString();
}

function compileOption(value: string): Option {
return { value, label: value }; // TODO: Be customizable for localization
}

const monthOptions: Options = range(1, 12).map((i) =>
compileOption(convertToSelectValue(i))
);
const dayOptions: Options = range(1, 31).map((i) =>
compileOption(convertToSelectValue(i))
);

interface DefaultDateOptions {
defaultYear?: number | "now";
defaultMonth?: number | "now";
Expand Down Expand Up @@ -169,18 +159,61 @@ export const useDateSelect = (
}
}, [setDate, value]);

const yearOptions = useMemo(() => {
// Generate year, month, and day arrays using the locale.
const utils = useUtils();

const rawYearOptions = useMemo(() => {
const minYear = opts.minYear != null ? opts.minYear : DEFAULT_MIN_YEAR;
const maxYear = opts.maxYear != null ? opts.maxYear : DEFAULT_MAX_YEAR;
const raw = range(minYear, maxYear).map((i) => {
const s = convertToSelectValue(i);
return { value: s, label: s };
const _yearOptions = range(minYear, maxYear).map((i) => {
const label = utils
? utils.format(new Date(i, 0, 1), "year")
: i.toString();
return { value: convertToSelectValue(i), label };
});
if (!raw.some((o) => o.value === state.yearValue)) {
return raw.concat(compileOption(state.yearValue));
return _yearOptions;
}, [opts.minYear, opts.maxYear, utils]);

// If the value of `state.yearValue` is not included in the year select options, add it.
const yearOptions = useMemo(() => {
if (
state.yearValue !== "" &&
!rawYearOptions.some((o) => o.value === state.yearValue)
) {
let label: string;
try {
label = utils
? utils.format(
new Date(parseSelectValue(state.yearValue), 0, 1),
"year"
)
: state.yearValue;
} catch {
label = state.yearValue;
}
return rawYearOptions.concat({ label, value: state.yearValue });
}
return raw;
}, [opts.minYear, opts.maxYear, state.yearValue]);

return rawYearOptions;
}, [rawYearOptions, state.yearValue]);

const [monthOptions, dayOptions] = useMemo(() => {
const _monthOptions = range(1, 12).map((i) => {
const label = utils
? utils.format(new Date(1960, i - 1, 1), "monthShort")
: i.toString();
return { value: convertToSelectValue(i), label };
});

const _dayOptions: Options = range(1, 31).map((i) => {
const label = utils
? utils.format(new Date(1960, 1, i), "dayOfMonth")
: i.toString();
return { value: convertToSelectValue(i), label };
});

return [_monthOptions, _dayOptions];
}, [utils]);

return {
yearValue: state.yearValue,
Expand Down
15 changes: 15 additions & 0 deletions website/Main.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,21 @@ import PartialDefaultValueNowSampleCode from "./samples/options/partial-default-

You can set `"now"` to `defaultYear`, `defaultMonth`, and `defaultDay`.

### Localization

import LocalizationSampleCode from "./samples/options/localization?raw";

<CodePreview initialCode={LocalizationSampleCode} language="tsx" />

You can set the locale through `<LocalizationProvider />` wrapping the component.

It accesses the date library you are using to use locale functionality through the "adapter" object which is, in the example above, `react-ymd-date-select/adapters/date-fns` for [`date-fns`](https://date-fns.org/).
Please select an appropriate adapter for your project.

With this design, this library can reuse the already installed date functions and can save the final bundle size.

Note that the date library such as `date-fns` is NOT installed along with `react-ymd-date-select`, so you have to install it separately.

### Hide day

import HideDaySampleCode from "./samples/options/hide-day?raw";
Expand Down
12 changes: 12 additions & 0 deletions website/components/CodePreview/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import * as reactYmdDateSelect from "react-ymd-date-select";
import * as vanillaPreset from "react-ymd-date-select/presets/vanilla";
import * as chakraPreset from "react-ymd-date-select/presets/chakra-ui";
import * as muiPreset from "react-ymd-date-select/presets/mui";
import { default as DateFnsAdapter } from "react-ymd-date-select/adapters/date-fns";
import dateFnsFrLocale from "date-fns/locale/fr";
import dateFnsRuLocale from "date-fns/locale/ru";
import dateFnsDeLocale from "date-fns/locale/de";
import dateFnsEnLocale from "date-fns/locale/en-US";
import dateFnsJaLocale from "date-fns/locale/ja";

export const scope = {
import: {
Expand All @@ -15,5 +21,11 @@ export const scope = {
"react-ymd-date-select/presets/vanilla": vanillaPreset,
"react-ymd-date-select/presets/chakra-ui": chakraPreset,
"react-ymd-date-select/presets/material": muiPreset,
"react-ymd-date-select/adapters/date-fns": DateFnsAdapter,
"date-fns/locale/fr": dateFnsFrLocale,
"date-fns/locale/ru": dateFnsRuLocale,
"date-fns/locale/de": dateFnsDeLocale,
"date-fns/locale/en-US": dateFnsEnLocale,
"date-fns/locale/ja": dateFnsJaLocale,
},
};
17 changes: 15 additions & 2 deletions website/components/EyeCatchDateSelect/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { useState } from "react";
import { FormControl, FormErrorMessage, Select } from "@chakra-ui/react";
import styled from "@emotion/styled";
import { useDateSelect, getDateString } from "react-ymd-date-select";
import DateFnsAdapter from "@date-io/date-fns";
import {
useDateSelect,
getDateString,
LocalizationProvider,
} from "react-ymd-date-select";

const dropdownIconColor = "#be5f6f";
const errorColor = "#fcdfff";
Expand Down Expand Up @@ -75,4 +80,12 @@ function EyeCatchDateSelect() {
);
}

export default EyeCatchDateSelect;
function LocalizedEyeCatchDateSelect() {
return (
<LocalizationProvider dateAdapter={DateFnsAdapter}>
<EyeCatchDateSelect />
</LocalizationProvider>
);
}

export default LocalizedEyeCatchDateSelect;
58 changes: 58 additions & 0 deletions website/samples/options/localization.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useState } from "react";
import frLocale from "date-fns/locale/fr";
import ruLocale from "date-fns/locale/ru";
import deLocale from "date-fns/locale/de";
import enLocale from "date-fns/locale/en-US";
import jaLocale from "date-fns/locale/ja";
import { LocalizationProvider } from "react-ymd-date-select";
import { DateSelect } from "react-ymd-date-select/presets/vanilla";
/* Select the appropriate adapter for the library you are using as below. */
import DateAdapter from "react-ymd-date-select/adapters/date-fns";
// import DateAdapter from "react-ymd-date-select/adapters/dayjs";
// import DateAdapter from "react-ymd-date-select/adapters/luxon";
// import DateAdapter from "react-ymd-date-select/adapters/moment";

const localeMap = {
en: enLocale,
fr: frLocale,
ru: ruLocale,
de: deLocale,
ja: jaLocale,
};

type Locale = keyof typeof localeMap;

function Sample() {
const [locale, setLocale] = useState<Locale>("en");
const [date, setDate] = useState<string>("");

return (
<div>
<div>
<label>
Locale:
<select
value={locale}
onChange={(e) => setLocale(e.target.value as Locale)}
>
{Object.keys(localeMap).map((localeItem) => (
<option key={localeItem} value={localeItem}>
{localeItem}
</option>
))}
</select>
</label>
</div>

<LocalizationProvider
dateAdapter={DateAdapter}
locale={localeMap[locale]}
>
<DateSelect value={date} onChange={setDate} defaultMonth="now" />
<p>Selected date is: {date}</p>
</LocalizationProvider>
</div>
);
}

export default Sample;
Loading