Skip to content
Merged
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
74 changes: 73 additions & 1 deletion packages/opub-ui/src/components/Calendar/Calendar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import {
today,
} from '@internationalized/date';
import { Meta, StoryObj } from '@storybook/react-vite';
import { useDateFormatter, useLocale } from 'react-aria';
import { I18nProvider, useDateFormatter, useLocale } from 'react-aria';

import { Calendar } from './Calendar';
import { MultiSelectYearCalendar } from './MultiSelectYearCalendar';
import { RangeCalendar } from './RangeCalendar';
import { YearCalendar } from './YearCalendar';

Expand Down Expand Up @@ -89,3 +90,74 @@ export const YearDisabled: any = {
},
args: {},
};

export const MultiSelectYear: any = {
render: ({ ...args }) => {
return (
<MultiSelectYearCalendar
{...args}
onChange={(dates: any) => {
console.log(dates);
}}
/>
);
},
args: {},
};

export const MultiSelectYearDisabled: any = {
render: ({ ...args }) => {
return (
<MultiSelectYearCalendar
minValue={parseDate('2021-01-01')}
maxValue={today(getLocalTimeZone())}
{...args}
onChange={(dates: any) => {
console.log(dates);
}}
/>
);
},
args: {},
};

/**
* Consumers translate the calendar's aria-label strings by passing `labels`,
* the same way the Table footer accepts localized labels. Inspect a selected
* month's aria-label to see the localized "selected" suffix.
*/
export const YearLocalizedLabels: any = {
render: ({ ...args }) => {
return (
<YearCalendar
labels={{ selected: 'sélectionné' }}
{...args}
onChange={(date: any) => {
console.log(date);
}}
/>
);
},
args: {},
};

/**
* Month names are formatted from the active locale (via `useDateFormatter`),
* so wrapping the calendar in an `<I18nProvider>` localizes them with no
* consumer-supplied strings. Here the months render in French.
*/
export const YearLocalizedMonths: any = {
render: ({ ...args }) => {
return (
<I18nProvider locale="fr-FR">
<YearCalendar
{...args}
onChange={(date: any) => {
console.log(date);
}}
/>
</I18nProvider>
);
},
args: {},
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,39 +15,30 @@ import {
useCalendarState,
} from 'react-stately';

import { YearCalendarLabels } from '../../types/datetime';
import { cn } from '../../utils';
import { Icon } from '../Icon';
import { Text } from '../Text';
import styles from './Calendar.module.scss';

const monthsObj: {
[key: number]: { month: string; value: number; label: string }[];
} = {
0: [
{ month: 'Jan', value: 1, label: 'January' },
{ month: 'Feb', value: 2, label: 'February' },
{ month: 'Mar', value: 3, label: 'March' },
{ month: 'Apr', value: 4, label: 'April' },
],
1: [
{ month: 'May', value: 5, label: 'May' },
{ month: 'Jun', value: 6, label: 'June' },
{ month: 'Jul', value: 7, label: 'July' },
{ month: 'Aug', value: 8, label: 'August' },
],
2: [
{ month: 'Sep', value: 9, label: 'September' },
{ month: 'Oct', value: 10, label: 'October' },
{ month: 'Nov', value: 11, label: 'November' },
{ month: 'Dec', value: 12, label: 'December' },
],
// Month numbers laid out as the 3x4 grid. Names are formatted per locale at
// render time (see Cell), so no month strings are hardcoded here.
const monthsObj: { [key: number]: { value: number }[] } = {
0: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }],
1: [{ value: 5 }, { value: 6 }, { value: 7 }, { value: 8 }],
2: [{ value: 9 }, { value: 10 }, { value: 11 }, { value: 12 }],
};

interface MultiSelectYearCalendarProps {
maxSelections?: number;
disabledMonths?: DateValue[];
yearRange?: { start: number; end: number };

/** Earliest selectable month; earlier months render disabled */
minValue?: DateValue | null;
/** Latest selectable month; later months render disabled */
maxValue?: DateValue | null;

// onSelectionChange: (
// selections: {
// year: number;
Expand All @@ -61,6 +52,9 @@ interface MultiSelectYearCalendarProps {
defaultValues?: DateValue[];

onChange?: (dates: DateValue[]) => void;

/** Localizable strings. Falls back to English defaults. */
labels?: YearCalendarLabels;
}

export const MultiSelectYearCalendar = (
Expand Down Expand Up @@ -147,6 +141,7 @@ export const MultiSelectYearCalendar = (
state={state}
selectedDates={selectedDates}
defaultValue={props.defaultValues}
labels={props.labels}
/>
</div>
);
Expand All @@ -170,10 +165,12 @@ function MonthSelector({
state,
selectedDates,
defaultValue,
labels,
}: {
state: CalendarState;
selectedDates: DateValue[];
defaultValue?: DateValue[];
labels?: YearCalendarLabels;
}) {
React.useEffect(() => {
const nextFocusElm = document.querySelector(
Expand Down Expand Up @@ -224,6 +221,7 @@ function MonthSelector({
state={state}
selectedDates={selectedDates}
defaultValue={defaultValue}
labels={labels}
/>
);
})}
Expand All @@ -243,14 +241,25 @@ const Cell = ({
state,
selectedDates,
defaultValue,
labels,
}: {
mon: { month: string; value: number; label: string };
mon: { value: number };
state: CalendarState;
selectedDates: DateValue[];
defaultValue?: DateValue[];
labels?: YearCalendarLabels;
}) => {
let date = state.focusedDate.set({ month: mon.value });

// Localized month names from the active locale (via the nearest
// <I18nProvider>, falling back to the runtime locale). The day is fixed to
// the 1st in local time to avoid the UTC-midnight off-by-one.
const monthShortFormatter = useDateFormatter({ month: 'short' });
const monthLongFormatter = useDateFormatter({ month: 'long' });
const nameDate = new Date(state.focusedDate.year, mon.value - 1, 1);
const monthShort = monthShortFormatter.format(nameDate);
const monthLong = monthLongFormatter.format(nameDate);

const { minValue, maxValue } = state;
const isDisabled =
(minValue && date.compare(minValue) < 0) ||
Expand Down Expand Up @@ -278,16 +287,21 @@ const Cell = ({
state.setFocusedDate(date);
};

const ariaLabel = `${monthLong}, ${state.focusedDate.year}${
isSelected ? `, ${labels?.selected ?? 'selected'}` : ''
}`;

return (
<td aria-selected={isSelected} role="gridcell">
<button
onClick={handleClick}
className={classname}
value={mon.value}
aria-label={`${mon.label}, ${state.focusedDate.year}`}
aria-disabled={isDisabled ? true : undefined}
aria-label={ariaLabel}
data-label={`${mon.value}, ${state.focusedDate.year}`}
>
<Text color="subdued">{mon.month}</Text>
<Text color="subdued">{monthShort}</Text>
</button>
</td>
);
Expand Down
70 changes: 41 additions & 29 deletions packages/opub-ui/src/components/Calendar/YearCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,25 @@ import {
useCalendarState,
} from 'react-stately';

import { YearCalendarLabels } from '../../types/datetime';
import { cn } from '../../utils';
import { Icon } from '../Icon';
import { Text } from '../Text';
import styles from './Calendar.module.scss';

const monthsObj: {
[key: number]: { month: string; value: number; label: string }[];
} = {
0: [
{ month: 'Jan', value: 1, label: 'January' },
{ month: 'Feb', value: 2, label: 'February' },
{ month: 'Mar', value: 3, label: 'March' },
{ month: 'Apr', value: 4, label: 'April' },
],
1: [
{ month: 'May', value: 5, label: 'May' },
{ month: 'Jun', value: 6, label: 'June' },
{ month: 'Jul', value: 7, label: 'July' },
{ month: 'Aug', value: 8, label: 'August' },
],
2: [
{ month: 'Sep', value: 9, label: 'September' },
{ month: 'Oct', value: 10, label: 'October' },
{ month: 'Nov', value: 11, label: 'November' },
{ month: 'Dec', value: 12, label: 'December' },
],
// Month numbers laid out as the 3x4 grid. Names are formatted per locale at
// render time (see Cell), so no month strings are hardcoded here.
const monthsObj: { [key: number]: { value: number }[] } = {
0: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }],
1: [{ value: 5 }, { value: 6 }, { value: 7 }, { value: 8 }],
2: [{ value: 9 }, { value: 10 }, { value: 11 }, { value: 12 }],
};

export const YearCalendar = (
props: CalendarStateOptions<DateValue> | AriaCalendarProps<DateValue>
props: (CalendarStateOptions<DateValue> | AriaCalendarProps<DateValue>) & {
/** Localizable strings. Falls back to English defaults. */
labels?: YearCalendarLabels;
}
) => {
let { locale } = useLocale();
let state = useCalendarState({
Expand Down Expand Up @@ -98,7 +87,7 @@ export const YearCalendar = (
<NextButton />
</div>

<MonthSelector state={state} />
<MonthSelector state={state} labels={props.labels} />
</div>
);
};
Expand All @@ -117,7 +106,13 @@ function YearDropdown({ state }: { state: CalendarState }) {
);
}

function MonthSelector({ state }: { state: CalendarState }) {
function MonthSelector({
state,
labels,
}: {
state: CalendarState;
labels?: YearCalendarLabels;
}) {
React.useEffect(() => {
const nextFocusElm = document.querySelector(
`[data-label="${state.focusedDate.month}, ${state.focusedDate.year}"]`
Expand Down Expand Up @@ -155,7 +150,9 @@ function MonthSelector({ state }: { state: CalendarState }) {
return (
<tr key={i}>
{monthsObj[i].map((mon) => {
return <Cell key={mon.value} mon={mon} state={state} />;
return (
<Cell key={mon.value} mon={mon} state={state} labels={labels} />
);
})}
</tr>
);
Expand All @@ -171,12 +168,23 @@ function MonthSelector({ state }: { state: CalendarState }) {
const Cell = ({
mon,
state,
labels,
}: {
mon: { month: string; value: number; label: string };
mon: { value: number };
state: CalendarState;
labels?: YearCalendarLabels;
}) => {
let date = state.focusedDate.set({ month: mon.value });

// Localized month names from the active locale (via the nearest
// <I18nProvider>, falling back to the runtime locale). The day is fixed to
// the 1st in local time to avoid the UTC-midnight off-by-one.
const monthShortFormatter = useDateFormatter({ month: 'short' });
const monthLongFormatter = useDateFormatter({ month: 'long' });
const nameDate = new Date(state.focusedDate.year, mon.value - 1, 1);
const monthShort = monthShortFormatter.format(nameDate);
const monthLong = monthLongFormatter.format(nameDate);

const { minValue, maxValue } = state;
const isDisabled =
(minValue && date.compare(minValue) < 0) ||
Expand All @@ -198,22 +206,26 @@ const Cell = ({

const value = Number((e.target as HTMLElement).getAttribute('value'));
let date = state.focusedDate.set({ month: value });
console.log(date.toDate('UTC'));

state.setFocusedDate(date);
state.setValue(date);
};

const ariaLabel = `${monthLong}, ${state.focusedDate.year}${
isSelected ? `, ${labels?.selected ?? 'selected'}` : ''
}`;

return (
<td aria-selected={isSelected} role="gridcell">
<button
onClick={handleClick}
className={classname}
value={mon.value}
aria-label={`${mon.label}, ${state.focusedDate.year}`}
aria-disabled={isDisabled ? true : undefined}
aria-label={ariaLabel}
data-label={`${mon.value}, ${state.focusedDate.year}`}
>
<Text color="subdued">{mon.month}</Text>
<Text color="subdued">{monthShort}</Text>
</button>
</td>
);
Expand Down
20 changes: 20 additions & 0 deletions packages/opub-ui/src/components/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,26 @@ export function Disabled({ ...props }) {
// );
// }

export function WithDescribedBy({ ...props }) {
const options = [
{ label: 'Block A', value: 'a' },
{ label: 'Block B', value: 'b' },
];

return (
<>
<p id="block-dependency-hint">Select a district first to enable this field.</p>
<Select
label="Block"
name="select-1"
describedBy="block-dependency-hint"
options={options}
{...props}
/>
</>
);
}

export function Error({ ...props }) {
const [selected, setSelected] = useState('Bangaluru');

Expand Down
Loading