Add harness workflow and Material showcase design system
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
@@ -9,7 +11,9 @@ import {
|
||||
AccordionTrigger
|
||||
} from "./accordion";
|
||||
|
||||
function ExampleAccordion(props: any = {}) {
|
||||
type ExampleAccordionProps = ComponentProps<typeof Accordion>;
|
||||
|
||||
function ExampleAccordion(props: ExampleAccordionProps = {}) {
|
||||
return (
|
||||
<Accordion {...props}>
|
||||
<AccordionItem value="editorial">
|
||||
@@ -85,7 +89,9 @@ describe("Accordion", () => {
|
||||
|
||||
await user.click(trigger);
|
||||
|
||||
const content = screen.getByText("Copy is locked for launch review.").closest('[data-slot="content"]');
|
||||
const content = screen
|
||||
.getByText("Copy is locked for launch review.")
|
||||
.closest('[data-slot="content"]');
|
||||
|
||||
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
||||
expect(content).toHaveAttribute("data-state", "open");
|
||||
|
||||
@@ -19,7 +19,8 @@ export const cardVariants = cva(
|
||||
},
|
||||
interactive: {
|
||||
false: "",
|
||||
true: "hover:translate-y-[var(--ui-card-hover-translate)] hover:shadow-[var(--ui-card-hover-shadow)]"
|
||||
true:
|
||||
"hover:translate-y-[var(--ui-card-hover-translate)] hover:scale-[var(--ui-card-hover-scale,1)] hover:shadow-[var(--ui-card-hover-shadow)]"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("Combobox", () => {
|
||||
it("renders a selected value, filters options, and updates uncontrolled state", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const loadingView = render(
|
||||
render(
|
||||
<Combobox
|
||||
aria-label="Review lane"
|
||||
defaultValue="design"
|
||||
|
||||
@@ -157,7 +157,7 @@ export const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(function Co
|
||||
|
||||
return haystack.includes(query);
|
||||
});
|
||||
}, [items, resolvedSearchValue]);
|
||||
}, [filter, items, resolvedSearchValue]);
|
||||
const listboxId = filteredItems.length > 0 ? `${controlId}-listbox` : undefined;
|
||||
|
||||
const groupedItems = useMemo(() => {
|
||||
|
||||
@@ -32,11 +32,9 @@ import {
|
||||
datePickerFooterVariants,
|
||||
datePickerGridVariants,
|
||||
datePickerHeaderVariants,
|
||||
datePickerMonthLabelVariants,
|
||||
datePickerNavigationVariants,
|
||||
datePickerRootVariants,
|
||||
datePickerSelectorsVariants,
|
||||
datePickerTriggerVariants,
|
||||
datePickerWeekdayVariants
|
||||
} from "./date-picker.variants";
|
||||
import { cn } from "../lib/cn";
|
||||
@@ -56,10 +54,6 @@ function normalizeDate(value?: Date) {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function getDateKey(value?: Date) {
|
||||
return value ? `${value.getFullYear()}-${value.getMonth()}-${value.getDate()}` : "";
|
||||
}
|
||||
|
||||
function sameDay(left?: Date, right?: Date) {
|
||||
if (!left || !right) {
|
||||
return false;
|
||||
@@ -232,111 +226,109 @@ export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(function
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const reactId = useId();
|
||||
const today = useMemo(() => normalizeDate(new Date()), []);
|
||||
const normalizedControlledValue = useMemo(
|
||||
() => normalizeDate(value),
|
||||
[value ? getDateKey(value) : ""]
|
||||
);
|
||||
const normalizedDefaultValue = useMemo(
|
||||
() => normalizeDate(defaultValue),
|
||||
[defaultValue ? getDateKey(defaultValue) : ""]
|
||||
);
|
||||
const normalizedDefaultMonth = useMemo(
|
||||
() => normalizeDate(defaultMonth),
|
||||
[defaultMonth ? getDateKey(defaultMonth) : ""]
|
||||
);
|
||||
const [selectedDate, setSelectedDate] = useControllableState<DatePickerValue>({
|
||||
controlledValue: normalizedControlledValue,
|
||||
defaultValue: normalizedDefaultValue,
|
||||
onChange: onValueChange
|
||||
const reactId = useId();
|
||||
const today = useMemo(() => normalizeDate(new Date()), []);
|
||||
const normalizedControlledValue = normalizeDate(value);
|
||||
const normalizedDefaultValue = normalizeDate(defaultValue);
|
||||
const normalizedDefaultMonth = normalizeDate(defaultMonth);
|
||||
const [selectedDate, setSelectedDate] = useControllableState<DatePickerValue>({
|
||||
controlledValue: normalizedControlledValue,
|
||||
defaultValue: normalizedDefaultValue,
|
||||
onChange: onValueChange
|
||||
});
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
||||
const resolvedOpen = open ?? uncontrolledOpen;
|
||||
const [visibleMonth, setVisibleMonth] = useState(
|
||||
startOfMonth(
|
||||
normalizedDefaultMonth ?? normalizedControlledValue ?? normalizedDefaultValue ?? today ?? new Date()
|
||||
)
|
||||
);
|
||||
const dayRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
||||
const controlId = id ?? `date-picker-${reactId.replace(/:/g, "")}`;
|
||||
const popupId = `${controlId}-dialog`;
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frame = requestAnimationFrame(() => {
|
||||
setVisibleMonth(startOfMonth(selectedDate));
|
||||
});
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
||||
const resolvedOpen = open ?? uncontrolledOpen;
|
||||
const [visibleMonth, setVisibleMonth] = useState(
|
||||
startOfMonth(normalizedDefaultMonth ?? normalizedDefaultValue ?? today ?? new Date())
|
||||
);
|
||||
const dayRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
||||
const controlId = id ?? `date-picker-${reactId.replace(/:/g, "")}`;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDate) {
|
||||
setVisibleMonth(startOfMonth(selectedDate));
|
||||
}
|
||||
}, [selectedDate]);
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [selectedDate]);
|
||||
|
||||
const monthLabel = formatMonthLabel(visibleMonth, locale);
|
||||
const weekdays = useMemo(() => {
|
||||
const formatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
|
||||
const base = new Date(2025, 0, 5 + getWeekStartIndex(weekStartsOn));
|
||||
const monthLabel = formatMonthLabel(visibleMonth, locale);
|
||||
const weekdays = useMemo(() => {
|
||||
const formatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
|
||||
const base = new Date(2025, 0, 5 + getWeekStartIndex(weekStartsOn));
|
||||
|
||||
return Array.from({ length: 7 }, (_, index) => {
|
||||
const day = new Date(base);
|
||||
day.setDate(base.getDate() + index);
|
||||
return formatter.format(day);
|
||||
});
|
||||
}, [locale, weekStartsOn]);
|
||||
const days = useMemo(
|
||||
() => buildMonthGrid(visibleMonth, weekStartsOn),
|
||||
[visibleMonth, weekStartsOn]
|
||||
);
|
||||
const yearOptions = useMemo(
|
||||
() => getYearOptions(visibleMonth, selectedDate),
|
||||
[selectedDate, visibleMonth]
|
||||
);
|
||||
const selectedIndex = days.findIndex((day) => sameDay(day, selectedDate));
|
||||
return Array.from({ length: 7 }, (_, index) => {
|
||||
const day = new Date(base);
|
||||
day.setDate(base.getDate() + index);
|
||||
return formatter.format(day);
|
||||
});
|
||||
}, [locale, weekStartsOn]);
|
||||
const days = useMemo(
|
||||
() => buildMonthGrid(visibleMonth, weekStartsOn),
|
||||
[visibleMonth, weekStartsOn]
|
||||
);
|
||||
const yearOptions = useMemo(
|
||||
() => getYearOptions(visibleMonth, selectedDate),
|
||||
[selectedDate, visibleMonth]
|
||||
);
|
||||
const selectedIndex = days.findIndex((day) => sameDay(day, selectedDate));
|
||||
|
||||
useEffect(() => {
|
||||
dayRefs.current = [];
|
||||
}, [visibleMonth]);
|
||||
useEffect(() => {
|
||||
dayRefs.current = [];
|
||||
}, [visibleMonth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!resolvedOpen) {
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!resolvedOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusIndex =
|
||||
selectedIndex >= 0
|
||||
? selectedIndex
|
||||
: days.findIndex(
|
||||
(day) =>
|
||||
day.getMonth() === visibleMonth.getMonth() &&
|
||||
sameDay(day, today)
|
||||
);
|
||||
const focusIndex =
|
||||
selectedIndex >= 0
|
||||
? selectedIndex
|
||||
: days.findIndex(
|
||||
(day) => day.getMonth() === visibleMonth.getMonth() && sameDay(day, today)
|
||||
);
|
||||
|
||||
const frame = requestAnimationFrame(() => {
|
||||
dayRefs.current[focusIndex >= 0 ? focusIndex : 0]?.focus();
|
||||
});
|
||||
const frame = requestAnimationFrame(() => {
|
||||
dayRefs.current[focusIndex >= 0 ? focusIndex : 0]?.focus();
|
||||
});
|
||||
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [days, resolvedOpen, selectedIndex, today, visibleMonth]);
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [days, resolvedOpen, selectedIndex, today, visibleMonth]);
|
||||
|
||||
const setOpenState = (nextOpen: boolean) => {
|
||||
if (open === undefined) {
|
||||
setUncontrolledOpen(nextOpen);
|
||||
}
|
||||
const setOpenState = (nextOpen: boolean) => {
|
||||
if (open === undefined) {
|
||||
setUncontrolledOpen(nextOpen);
|
||||
}
|
||||
|
||||
onOpenChange?.(nextOpen);
|
||||
};
|
||||
onOpenChange?.(nextOpen);
|
||||
};
|
||||
|
||||
const goToMonth = (offset: number) => {
|
||||
const next = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() + offset, 1);
|
||||
setVisibleMonth(next);
|
||||
onMonthChange?.(next);
|
||||
};
|
||||
const goToMonth = (offset: number) => {
|
||||
const next = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() + offset, 1);
|
||||
setVisibleMonth(next);
|
||||
onMonthChange?.(next);
|
||||
};
|
||||
|
||||
const handleTriggerKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
setOpenState(true);
|
||||
}
|
||||
const handleTriggerKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
setOpenState(true);
|
||||
}
|
||||
|
||||
if (event.key === "Escape") {
|
||||
setOpenState(false);
|
||||
}
|
||||
};
|
||||
if (event.key === "Escape") {
|
||||
setOpenState(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDayKeyDown = (event: KeyboardEvent<HTMLButtonElement>, index: number) => {
|
||||
const handleDayKeyDown = (event: KeyboardEvent<HTMLButtonElement>, index: number) => {
|
||||
const movementMap: Record<string, number> = {
|
||||
ArrowDown: 7,
|
||||
ArrowLeft: -1,
|
||||
@@ -408,6 +400,7 @@ export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(function
|
||||
<div {...createSlot("field")} className={datePickerFieldVariants()}>
|
||||
<Input
|
||||
{...props}
|
||||
aria-controls={popupId}
|
||||
aria-expanded={resolvedOpen}
|
||||
aria-haspopup="dialog"
|
||||
className={cn("cursor-pointer pr-20", className)}
|
||||
@@ -430,7 +423,7 @@ export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(function
|
||||
</div>
|
||||
</PopoverAnchor>
|
||||
|
||||
<PopoverContent className={datePickerContentVariants()} padding="sm" size="xl">
|
||||
<PopoverContent className={datePickerContentVariants()} id={popupId} padding="sm" size="xl">
|
||||
<div className="grid gap-3">
|
||||
<div {...createSlot("header")} className={datePickerHeaderVariants()}>
|
||||
<div className={datePickerNavigationVariants()}>
|
||||
|
||||
@@ -16,5 +16,5 @@ export const switchVariants = cva(
|
||||
export const switchThumbVariants = cva([
|
||||
"pointer-events-none block size-5 rounded-[var(--ui-switch-thumb-radius)] bg-[var(--ui-switch-thumb-bg)] shadow-[var(--ui-switch-thumb-shadow)]",
|
||||
"translate-x-0.5 will-change-transform transition-[transform,box-shadow,background-color] duration-[var(--ui-switch-transition-duration,var(--dur-base))] ease-[var(--ease-emphasized)]",
|
||||
"data-[state=checked]:translate-x-[1.55rem] data-[state=checked]:shadow-[var(--ui-switch-thumb-checked-shadow,var(--ui-switch-thumb-shadow))]"
|
||||
"data-[state=checked]:translate-x-[1.55rem] data-[state=checked]:bg-[var(--ui-switch-thumb-checked-bg,var(--ui-switch-thumb-bg))] data-[state=checked]:shadow-[var(--ui-switch-thumb-checked-shadow,var(--ui-switch-thumb-shadow))]"
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user