ui phase 5
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { ThemeProvider } from '../../../theme';
|
||||
|
||||
jest.mock('next-intl', () => ({
|
||||
useLocale: () => 'en',
|
||||
}));
|
||||
|
||||
import JalaliDateIntentPicker from './JalaliDateIntentPicker';
|
||||
import { todayIso } from '../JalaliDatePicker/calendarEngine';
|
||||
|
||||
const LABELS = { todayLabel: 'Today', tomorrowLabel: 'Tomorrow', pickOtherLabel: 'Pick another date' };
|
||||
|
||||
function wrap(ui: React.ReactNode) {
|
||||
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
||||
}
|
||||
|
||||
describe('<JalaliDateIntentPicker/> component', () => {
|
||||
it('renders the near-date chip strip and emits the ISO date on click', () => {
|
||||
const onChange = jest.fn();
|
||||
const { container } = wrap(<JalaliDateIntentPicker value="" onChange={onChange} min={todayIso()} {...LABELS} />);
|
||||
const chips = container.querySelectorAll('[data-day]');
|
||||
expect(chips.length).toBeGreaterThan(0);
|
||||
fireEvent.click(chips[0]);
|
||||
expect(onChange).toHaveBeenCalledWith(todayIso());
|
||||
});
|
||||
|
||||
it('opens a full-grid popover from the calendar-icon button', () => {
|
||||
wrap(<JalaliDateIntentPicker value="" onChange={jest.fn()} min={todayIso()} {...LABELS} />);
|
||||
fireEvent.click(screen.getByLabelText('Pick another date'));
|
||||
expect(screen.getByText(/\d{4}/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes the popover and emits the picked date when a grid day is clicked', () => {
|
||||
const onChange = jest.fn();
|
||||
wrap(<JalaliDateIntentPicker value={todayIso()} onChange={onChange} {...LABELS} />);
|
||||
fireEvent.click(screen.getByLabelText('Pick another date'));
|
||||
const gridDay = screen.getAllByText('15')[0];
|
||||
fireEvent.click(gridDay);
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
import { FunctionComponent, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import AppIconButton from '../AppIconButton';
|
||||
import JalaliDatePicker from '../JalaliDatePicker';
|
||||
|
||||
export interface JalaliDateIntentPickerProps {
|
||||
/** The selected date as a wire ISO (Gregorian) `YYYY-MM-DD` string, or `''` for no selection. */
|
||||
value: string;
|
||||
/** Fired with the wire ISO (Gregorian) date of the day the user picked. */
|
||||
onChange: (iso: string) => void;
|
||||
/** Inclusive ISO lower bound (typically `todayIso()`). */
|
||||
min?: string;
|
||||
/** Number of day-chips in the near strip (default 7). */
|
||||
chipDayCount?: number;
|
||||
/** Already-translated relative labels for the first two chips when they land on today/tomorrow. */
|
||||
todayLabel: string;
|
||||
tomorrowLabel: string;
|
||||
/** Already-translated accessible label for the "pick another date" calendar-icon entry. */
|
||||
pickOtherLabel: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A horizontal near-date chip strip (`JalaliDatePicker` `chips` variant) plus a calendar-icon entry into
|
||||
* the full Jalali grid (a `Popover`) for dates beyond the strip — the shape both C1 search (date-intent,
|
||||
* never hard-filters) and C4 booking-request (a real required field) need. Presentational, caller-owned
|
||||
* copy, per the `components/common` convention.
|
||||
* @component JalaliDateIntentPicker
|
||||
*/
|
||||
const JalaliDateIntentPicker: FunctionComponent<JalaliDateIntentPickerProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
min,
|
||||
chipDayCount = 7,
|
||||
todayLabel,
|
||||
tomorrowLabel,
|
||||
pickOtherLabel,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
|
||||
return (
|
||||
<Stack direction="row" sx={{ gap: 1, alignItems: 'center' }}>
|
||||
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<JalaliDatePicker
|
||||
variant="chips"
|
||||
chipDayCount={chipDayCount}
|
||||
value={value || null}
|
||||
onChange={onChange}
|
||||
min={min}
|
||||
todayLabel={todayLabel}
|
||||
tomorrowLabel={tomorrowLabel}
|
||||
/>
|
||||
</Box>
|
||||
<AppIconButton
|
||||
icon="calendar"
|
||||
title={pickOtherLabel}
|
||||
onClick={(event) => setAnchorEl(event.currentTarget)}
|
||||
/>
|
||||
<Popover
|
||||
open={Boolean(anchorEl)}
|
||||
anchorEl={anchorEl}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
>
|
||||
<Box sx={{ p: 2, width: 320 }}>
|
||||
<JalaliDatePicker
|
||||
value={value || null}
|
||||
min={min}
|
||||
onChange={(iso) => {
|
||||
onChange(iso);
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Popover>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default JalaliDateIntentPicker;
|
||||
@@ -0,0 +1,4 @@
|
||||
import JalaliDateIntentPicker from './JalaliDateIntentPicker';
|
||||
|
||||
export default JalaliDateIntentPicker;
|
||||
export type { JalaliDateIntentPickerProps } from './JalaliDateIntentPicker';
|
||||
@@ -26,6 +26,10 @@ export interface JalaliDatePickerProps {
|
||||
/** Already-translated accessible label for the previous/next-month buttons. */
|
||||
prevMonthLabel?: string;
|
||||
nextMonthLabel?: string;
|
||||
/** `chips` variant only: overrides the weekday label of the first chip with "today"/"tomorrow" copy
|
||||
* when that chip's date genuinely is today/tomorrow (never forced when a `min` pushes the strip later). */
|
||||
todayLabel?: string;
|
||||
tomorrowLabel?: string;
|
||||
}
|
||||
|
||||
const GRID_COLUMNS = 7;
|
||||
@@ -47,6 +51,8 @@ const JalaliDatePicker: FunctionComponent<JalaliDatePickerProps> = ({
|
||||
chipDayCount = 14,
|
||||
prevMonthLabel,
|
||||
nextMonthLabel,
|
||||
todayLabel,
|
||||
tomorrowLabel,
|
||||
}) => {
|
||||
const locale = useLocale();
|
||||
const engine = useMemo(() => engineForLocale(locale), [locale]);
|
||||
@@ -77,6 +83,9 @@ const JalaliDatePicker: FunctionComponent<JalaliDatePickerProps> = ({
|
||||
const weekday = new Intl.DateTimeFormat(localeTag(locale), { weekday: 'short' }).format(
|
||||
new Date(`${dayIso}T00:00:00`),
|
||||
);
|
||||
const relativeLabel =
|
||||
dayIso === todayIso() ? todayLabel : dayIso === addDaysIso(todayIso(), 1) ? tomorrowLabel : undefined;
|
||||
const topLabel = relativeLabel ?? weekday;
|
||||
return (
|
||||
<Box
|
||||
key={dayIso}
|
||||
@@ -104,8 +113,8 @@ const JalaliDatePicker: FunctionComponent<JalaliDatePickerProps> = ({
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
{weekday}
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: relativeLabel ? 700 : 400 }}>
|
||||
{topLabel}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: selected ? 700 : 500 }}>
|
||||
{formatNumber(cell.day, locale)}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ThemeProvider } from '../../../theme';
|
||||
import StickyActionBar from './StickyActionBar';
|
||||
|
||||
describe('<StickyActionBar/> component', () => {
|
||||
it('renders its children inside a sticky-bottom container', () => {
|
||||
const { container } = render(
|
||||
<ThemeProvider>
|
||||
<StickyActionBar>
|
||||
<button type="button">مشاهده ۱۲ پرستار</button>
|
||||
</StickyActionBar>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
expect(screen.getByText('مشاهده ۱۲ پرستار')).toBeInTheDocument();
|
||||
const bar = container.querySelector('[data-sticky-action-bar]');
|
||||
expect(bar).toBeInTheDocument();
|
||||
expect(bar).toHaveStyle({ position: 'sticky', bottom: '0px' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { FunctionComponent, ReactNode } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
export interface StickyActionBarProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bottom-pinned action bar for a scrolling screen — the C1 live-count CTA and the C3 booking CTA.
|
||||
* Rendered as the last child of a page's scrolling content, `position: sticky` pins it to the bottom
|
||||
* of the nearest scrolling ancestor (the shell's `main`), so on mobile it naturally sits directly above
|
||||
* `BottomBar` (a separate flex sibling below `main`, which already owns the `env(safe-area-inset-bottom)`
|
||||
* padding — this component does not re-implement it) and on desktop it sits at the viewport bottom.
|
||||
* @component StickyActionBar
|
||||
*/
|
||||
const StickyActionBar: FunctionComponent<StickyActionBarProps> = ({ children }) => (
|
||||
<Box
|
||||
data-sticky-action-bar
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
zIndex: 1,
|
||||
mt: 2,
|
||||
pt: 1.5,
|
||||
pb: 1.5,
|
||||
bgcolor: 'background.default',
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default StickyActionBar;
|
||||
@@ -0,0 +1,4 @@
|
||||
import StickyActionBar from './StickyActionBar';
|
||||
|
||||
export default StickyActionBar;
|
||||
export type { StickyActionBarProps } from './StickyActionBar';
|
||||
@@ -16,7 +16,9 @@ import Money from './Money';
|
||||
import StatusTimeline from './StatusTimeline';
|
||||
import JalaliDatePicker from './JalaliDatePicker';
|
||||
import JalaliDateField from './JalaliDateField';
|
||||
import JalaliDateIntentPicker from './JalaliDateIntentPicker';
|
||||
import LocaleSwitcher from './LocaleSwitcher';
|
||||
import StickyActionBar from './StickyActionBar';
|
||||
|
||||
export {
|
||||
ErrorBoundary,
|
||||
@@ -37,7 +39,9 @@ export {
|
||||
StatusTimeline,
|
||||
JalaliDatePicker,
|
||||
JalaliDateField,
|
||||
JalaliDateIntentPicker,
|
||||
LocaleSwitcher,
|
||||
StickyActionBar,
|
||||
};
|
||||
export type { EmptyStateProps } from './EmptyState';
|
||||
export type { ErrorStateProps } from './ErrorState';
|
||||
@@ -50,3 +54,5 @@ export type { MoneyProps } from './Money';
|
||||
export type { StatusTimelineProps, TimelineNode, TimelineNodeState } from './StatusTimeline';
|
||||
export type { JalaliDatePickerProps } from './JalaliDatePicker';
|
||||
export type { JalaliDateFieldProps } from './JalaliDateField';
|
||||
export type { JalaliDateIntentPickerProps } from './JalaliDateIntentPicker';
|
||||
export type { StickyActionBarProps } from './StickyActionBar';
|
||||
|
||||
Reference in New Issue
Block a user