✨(DatePicker) add classic variant and hideLabel props
- Add variant prop to DatePicker and DateRangePicker - Add hideLabel prop for accessible hidden labels - Label(s) rendered outside wrapper in classic mode - DateRangePicker shows both labels above fields in classic mode - Compact height in classic mode - Add unit tests and Storybook stories Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
import { createCalendar, DateValue } from "@internationalized/date";
|
||||
import classNames from "classnames";
|
||||
import { LabelledBox, Props } from ":/components/Forms/LabelledBox";
|
||||
import type { FieldVariant } from ":/components/Forms/types";
|
||||
|
||||
interface DateSegmentProps {
|
||||
currentSegment: DateSegment;
|
||||
@@ -68,10 +69,15 @@ const DateField = (props: AriaDatePickerProps<DateValue>) => {
|
||||
|
||||
interface DateFieldBoxProps
|
||||
extends Props,
|
||||
Omit<AriaDatePickerProps<DateValue>, "label"> {}
|
||||
Omit<AriaDatePickerProps<DateValue>, "label"> {
|
||||
variant?: FieldVariant;
|
||||
}
|
||||
|
||||
const DateFieldBox = ({ ...props }: DateFieldBoxProps) => (
|
||||
<LabelledBox {...props}>
|
||||
const DateFieldBox = ({
|
||||
variant = "floating",
|
||||
...props
|
||||
}: DateFieldBoxProps) => (
|
||||
<LabelledBox {...props} variant={variant}>
|
||||
<div
|
||||
className={classNames("c__date-picker__inner", {
|
||||
"c__date-picker__inner--collapsed": props.labelAsPlaceholder,
|
||||
|
||||
@@ -1459,4 +1459,82 @@ describe("<DatePicker/>", () => {
|
||||
document.querySelector(".c__field.my-custom-class"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("classic variant", () => {
|
||||
it("renders with classic variant", async () => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker label="Pick a date" variant="classic" />
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
// In classic mode, label is rendered outside the wrapper with its own class
|
||||
expect(
|
||||
document.querySelector(".c__date-picker__label"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
document.querySelector(".c__date-picker--classic"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("label is always static in classic variant", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker label="Pick a date" variant="classic" />
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
|
||||
const label = screen.getByText("Pick a date");
|
||||
|
||||
// In classic variant, label is outside the wrapper with c__date-picker__label class
|
||||
expect(label.classList.contains("c__date-picker__label")).toBe(true);
|
||||
|
||||
// Open calendar
|
||||
const toggleButton = (await screen.findAllByRole("button"))![1];
|
||||
await user.click(toggleButton);
|
||||
|
||||
// Label should still have the same class
|
||||
expect(label.classList.contains("c__date-picker__label")).toBe(true);
|
||||
});
|
||||
|
||||
it("defaults to floating variant", () => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker label="Pick a date" />
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
expect(
|
||||
document.querySelector(".c__date-picker--classic"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hideLabel", () => {
|
||||
it("hides label visually but keeps it accessible in floating variant", () => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker label="Pick a date" hideLabel />
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
// Label should be visually hidden via LabelledBox
|
||||
const label = screen.getByText("Pick a date");
|
||||
expect(label.closest("label")).toHaveClass("c__offscreen");
|
||||
});
|
||||
|
||||
it("hides label visually but keeps it accessible in classic variant", () => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker label="Pick a date" variant="classic" hideLabel />
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
// Label should be visually hidden with c__offscreen class
|
||||
const label = screen.getByText("Pick a date");
|
||||
expect(label).toHaveClass("c__offscreen");
|
||||
// The visible label class should not be present
|
||||
expect(
|
||||
document.querySelector(".c__date-picker__label:not(.c__offscreen)"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,6 +51,7 @@ export const DatePicker = (props: DatePickerProps) => {
|
||||
ref,
|
||||
);
|
||||
|
||||
const isClassic = props.variant === "classic";
|
||||
const labelAsPlaceholder = useMemo(
|
||||
() => !isFocused && !pickerState.isOpen && !pickerState.value,
|
||||
[pickerState.value, pickerState.isOpen, isFocused],
|
||||
@@ -74,8 +75,12 @@ export const DatePicker = (props: DatePickerProps) => {
|
||||
<DateFieldBox
|
||||
{...{
|
||||
...fieldProps,
|
||||
label: props.label,
|
||||
labelAsPlaceholder,
|
||||
// In classic mode, label is rendered outside by DatePickerAux
|
||||
label: isClassic ? undefined : props.label,
|
||||
variant: props.variant,
|
||||
hideLabel: isClassic ? undefined : props.hideLabel,
|
||||
// In classic mode, always show date segments (never collapse them)
|
||||
labelAsPlaceholder: isClassic ? false : labelAsPlaceholder,
|
||||
onFocusChange: setIsFocused,
|
||||
disabled: props.disabled,
|
||||
}}
|
||||
|
||||
@@ -14,17 +14,23 @@ import { I18nProvider } from "@react-aria/i18n";
|
||||
import { Button } from ":/components/Button";
|
||||
import { Popover } from ":/components/Popover";
|
||||
import { Field, FieldProps } from ":/components/Forms/Field";
|
||||
import { ClassicLabel } from ":/components/Forms/ClassicLabel";
|
||||
import { useCunningham } from ":/components/Provider";
|
||||
import {
|
||||
Calendar,
|
||||
CalendarRange,
|
||||
} from ":/components/Forms/DatePicker/Calendar";
|
||||
import { convertDateValueToString } from ":/components/Forms/DatePicker/utils";
|
||||
import type { FieldVariant } from ":/components/Forms/types";
|
||||
|
||||
export type DatePickerAuxSubProps = FieldProps & {
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
label?: string;
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
variant?: FieldVariant;
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
hideLabel?: boolean;
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
minValue?: string;
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
maxValue?: string;
|
||||
@@ -48,6 +54,13 @@ export type DatePickerAuxProps = PropsWithChildren &
|
||||
optionalClassName?: string;
|
||||
isRange?: boolean;
|
||||
onClear: () => void;
|
||||
// For classic range mode: render labels above the wrapper
|
||||
rangeLabels?: {
|
||||
startLabel: string;
|
||||
endLabel: string;
|
||||
disabled?: boolean;
|
||||
hideLabel?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -68,17 +81,21 @@ const DatePickerAux = ({
|
||||
disabled = false,
|
||||
optionalClassName,
|
||||
isRange,
|
||||
rangeLabels,
|
||||
ref,
|
||||
...props
|
||||
}: DatePickerAuxProps) => {
|
||||
const { t, currentLocale } = useCunningham();
|
||||
const pickerRef = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isDateInvalid = useMemo(
|
||||
() => pickerState.validationState === "invalid" || props.state === "error",
|
||||
[pickerState.validationState, props.state],
|
||||
);
|
||||
|
||||
const isClassic = props.variant === "classic";
|
||||
|
||||
return (
|
||||
<I18nProvider locale={locale || currentLocale}>
|
||||
<Field
|
||||
@@ -95,13 +112,91 @@ const DatePickerAux = ({
|
||||
"c__date-picker--success": props.state === "success",
|
||||
"c__date-picker--focused":
|
||||
!isDateInvalid && !disabled && (pickerState.isOpen || isFocused),
|
||||
"c__date-picker--classic": isClassic,
|
||||
})}
|
||||
>
|
||||
{isClassic && !isRange && (
|
||||
<ClassicLabel
|
||||
label={props.label}
|
||||
hideLabel={props.hideLabel}
|
||||
disabled={disabled}
|
||||
className="c__date-picker__label"
|
||||
disabledClassName="c__date-picker__label--disabled"
|
||||
onClick={() => {
|
||||
wrapperRef.current?.focus();
|
||||
if (!pickerState.isOpen) {
|
||||
pickerState.open();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* Classic variant: range labels above the wrapper */}
|
||||
{isClassic && rangeLabels && !rangeLabels.hideLabel && (
|
||||
<div className="c__date-picker__range__labels">
|
||||
<ClassicLabel
|
||||
label={rangeLabels.startLabel}
|
||||
disabled={rangeLabels.disabled}
|
||||
className="c__date-picker__label"
|
||||
disabledClassName="c__date-picker__label--disabled"
|
||||
onClick={() => {
|
||||
wrapperRef.current?.focus();
|
||||
if (!pickerState.isOpen) {
|
||||
pickerState.open();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="c__date-picker__range__labels__spacer" />
|
||||
<ClassicLabel
|
||||
label={rangeLabels.endLabel}
|
||||
disabled={rangeLabels.disabled}
|
||||
className="c__date-picker__label"
|
||||
disabledClassName="c__date-picker__label--disabled"
|
||||
onClick={() => {
|
||||
wrapperRef.current?.focus();
|
||||
if (!pickerState.isOpen) {
|
||||
pickerState.open();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Hidden range labels for accessibility when hideLabel is true */}
|
||||
{isClassic && rangeLabels && rangeLabels.hideLabel && (
|
||||
<>
|
||||
<ClassicLabel
|
||||
label={rangeLabels.startLabel}
|
||||
hideLabel
|
||||
onClick={() => {
|
||||
wrapperRef.current?.focus();
|
||||
if (!pickerState.isOpen) {
|
||||
pickerState.open();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ClassicLabel
|
||||
label={rangeLabels.endLabel}
|
||||
hideLabel
|
||||
onClick={() => {
|
||||
wrapperRef.current?.focus();
|
||||
if (!pickerState.isOpen) {
|
||||
pickerState.open();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className={classNames("c__date-picker__wrapper", {
|
||||
"c__date-picker__wrapper--clickable": labelAsPlaceholder,
|
||||
})}
|
||||
ref={ref}
|
||||
ref={(node) => {
|
||||
wrapperRef.current = node;
|
||||
if (typeof ref === "function") {
|
||||
ref(node);
|
||||
} else if (ref) {
|
||||
ref.current = node;
|
||||
}
|
||||
}}
|
||||
{...pickerProps.groupProps}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
|
||||
@@ -1212,4 +1212,116 @@ describe("<DateRangePicker/>", () => {
|
||||
document.querySelector(".c__field.my-custom-class"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("classic variant", () => {
|
||||
it("renders with classic variant", async () => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DateRangePicker
|
||||
label="Pick a date"
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
variant="classic"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
// In classic mode, both labels are rendered in a row above the wrapper
|
||||
expect(
|
||||
document.querySelector(".c__date-picker--classic"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
document.querySelector(".c__date-picker__range__labels"),
|
||||
).toBeInTheDocument();
|
||||
// Should have two labels in the labels row
|
||||
const labels = document.querySelectorAll(".c__date-picker__label");
|
||||
expect(labels.length).toBe(2);
|
||||
});
|
||||
|
||||
it("labels are always static in classic variant", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DateRangePicker
|
||||
label="Pick a date"
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
variant="classic"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
|
||||
const startLabel = screen.getByText("Start date");
|
||||
const endLabel = screen.getByText("End date");
|
||||
|
||||
// In classic variant, labels are outside the wrapper with c__date-picker__label class
|
||||
expect(startLabel.classList.contains("c__date-picker__label")).toBe(true);
|
||||
expect(endLabel.classList.contains("c__date-picker__label")).toBe(true);
|
||||
|
||||
// Open calendar
|
||||
const toggleButton = (await screen.findAllByRole("button"))![1];
|
||||
await user.click(toggleButton);
|
||||
|
||||
// Labels should still have the same class
|
||||
expect(startLabel.classList.contains("c__date-picker__label")).toBe(true);
|
||||
expect(endLabel.classList.contains("c__date-picker__label")).toBe(true);
|
||||
});
|
||||
|
||||
it("defaults to floating variant", () => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DateRangePicker
|
||||
label="Pick a date"
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
expect(
|
||||
document.querySelector(".c__date-picker--classic"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hideLabel", () => {
|
||||
it("hides labels visually but keeps them accessible in floating variant", () => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DateRangePicker
|
||||
label="Pick a date"
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
hideLabel
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
// Labels should be visually hidden via LabelledBox
|
||||
const startLabel = screen.getByText("Start date");
|
||||
const endLabel = screen.getByText("End date");
|
||||
expect(startLabel.closest("label")).toHaveClass("c__offscreen");
|
||||
expect(endLabel.closest("label")).toHaveClass("c__offscreen");
|
||||
});
|
||||
|
||||
it("hides labels visually but keeps them accessible in classic variant", () => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DateRangePicker
|
||||
label="Pick a date"
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
variant="classic"
|
||||
hideLabel
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
// Labels should be visually hidden with c__offscreen class
|
||||
const startLabel = screen.getByText("Start date");
|
||||
const endLabel = screen.getByText("End date");
|
||||
expect(startLabel).toHaveClass("c__offscreen");
|
||||
expect(endLabel).toHaveClass("c__offscreen");
|
||||
// The visible labels row should not be present
|
||||
expect(
|
||||
document.querySelector(".c__date-picker__range__labels"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,6 +60,7 @@ export const DateRangePicker = ({
|
||||
const { startFieldProps, endFieldProps, calendarProps, ...pickerProps } =
|
||||
useDateRangePicker(options, pickerState, ref);
|
||||
|
||||
const isClassic = props.variant === "classic";
|
||||
const labelAsPlaceholder = useMemo(
|
||||
() =>
|
||||
!isFocused &&
|
||||
@@ -88,14 +89,27 @@ export const DateRangePicker = ({
|
||||
});
|
||||
},
|
||||
calendar,
|
||||
// Pass labels for classic range mode
|
||||
rangeLabels: isClassic
|
||||
? {
|
||||
startLabel,
|
||||
endLabel,
|
||||
disabled: props.disabled,
|
||||
hideLabel: props.hideLabel,
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<DateFieldBox
|
||||
{...{
|
||||
...startFieldProps,
|
||||
label: startLabel,
|
||||
labelAsPlaceholder,
|
||||
// In classic mode, label is rendered outside by DatePickerAux
|
||||
label: isClassic ? undefined : startLabel,
|
||||
variant: props.variant,
|
||||
hideLabel: isClassic ? undefined : props.hideLabel,
|
||||
// In classic mode, always show date segments (never collapse them)
|
||||
labelAsPlaceholder: isClassic ? false : labelAsPlaceholder,
|
||||
onFocusChange: setIsFocused,
|
||||
disabled: props.disabled,
|
||||
}}
|
||||
@@ -104,8 +118,12 @@ export const DateRangePicker = ({
|
||||
<DateFieldBox
|
||||
{...{
|
||||
...endFieldProps,
|
||||
label: endLabel,
|
||||
labelAsPlaceholder,
|
||||
// In classic mode, label is rendered outside by DatePickerAux
|
||||
label: isClassic ? undefined : endLabel,
|
||||
variant: props.variant,
|
||||
hideLabel: isClassic ? undefined : props.hideLabel,
|
||||
// In classic mode, always show date segments (never collapse them)
|
||||
labelAsPlaceholder: isClassic ? false : labelAsPlaceholder,
|
||||
onFocusChange: setIsFocused,
|
||||
disabled: props.disabled,
|
||||
}}
|
||||
|
||||
@@ -4,6 +4,33 @@
|
||||
.c__date-picker {
|
||||
position: relative;
|
||||
|
||||
&__label {
|
||||
display: block;
|
||||
font-size: var(--c--components--forms-labelledbox--classic-label-font-size);
|
||||
color: var(--c--components--forms-labelledbox--label-color--small);
|
||||
margin-bottom: var(--c--components--forms-labelledbox--classic-label-margin-bottom);
|
||||
|
||||
&--disabled {
|
||||
color: var(--c--components--forms-labelledbox--label-color--small--disabled);
|
||||
}
|
||||
}
|
||||
|
||||
&--classic {
|
||||
.c__date-picker__wrapper {
|
||||
align-items: center;
|
||||
height: 2.75rem;
|
||||
}
|
||||
|
||||
.c__date-picker__inner__action {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.c__date-picker__range__separator {
|
||||
align-self: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
border-radius: var(--c--components--forms-datepicker--border-radius);
|
||||
border-width: var(--c--components--forms-datepicker--border-width);
|
||||
@@ -202,6 +229,22 @@
|
||||
&__range {
|
||||
$component-min-width: px-to-rem(350px);
|
||||
|
||||
&__labels {
|
||||
display: flex;
|
||||
margin-bottom: var(--c--components--forms-labelledbox--classic-label-margin-bottom);
|
||||
|
||||
.c__date-picker__label {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__spacer {
|
||||
// Space for the separator
|
||||
width: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// MUST READ:
|
||||
// We can only use @container property for full-width fields, as the container-type: inline-size property
|
||||
// should not be based on the children's width. We cannot at the same time use container-type and a default
|
||||
|
||||
@@ -13,6 +13,19 @@ import { RhfDatePicker } from ":/components/Forms/DatePicker/stories-utils";
|
||||
export default {
|
||||
title: "Components/Forms/DatePicker",
|
||||
component: DatePicker,
|
||||
argTypes: {
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
},
|
||||
state: {
|
||||
control: "select",
|
||||
options: ["default", "success", "error"],
|
||||
},
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["floating", "classic"],
|
||||
},
|
||||
},
|
||||
} as Meta<typeof DatePicker>;
|
||||
|
||||
const Template: StoryFn<typeof DatePicker> = (args) => (
|
||||
@@ -250,3 +263,80 @@ export const RangeControlledFull = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ClassicVariant = () => (
|
||||
<div style={{ minHeight: "400px" }}>
|
||||
<DatePicker label="Pick a date" variant="classic" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ClassicVariantWithValue = () => (
|
||||
<div style={{ minHeight: "400px" }}>
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
variant="classic"
|
||||
defaultValue="2023-05-24T00:00:00.000+00:00"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const RangeClassicVariant = () => (
|
||||
<div style={{ minHeight: "400px" }}>
|
||||
<DateRangePicker
|
||||
label="Pick a date range"
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
variant="classic"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const RangeClassicVariantWithValue = () => (
|
||||
<div style={{ minHeight: "400px" }}>
|
||||
<DateRangePicker
|
||||
label="Pick a date range"
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
variant="classic"
|
||||
defaultValue={[
|
||||
"2023-05-23T00:00:00.000+00:00",
|
||||
"2023-06-23T00:00:00.000+00:00",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const HiddenLabel = () => (
|
||||
<div style={{ minHeight: "400px" }}>
|
||||
<DatePicker label="Pick a date" hideLabel />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const HiddenLabelClassic = () => (
|
||||
<div style={{ minHeight: "400px" }}>
|
||||
<DatePicker label="Pick a date" variant="classic" hideLabel />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const RangeHiddenLabel = () => (
|
||||
<div style={{ minHeight: "400px" }}>
|
||||
<DateRangePicker
|
||||
label="Pick a date range"
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
hideLabel
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const RangeHiddenLabelClassic = () => (
|
||||
<div style={{ minHeight: "400px" }}>
|
||||
<DateRangePicker
|
||||
label="Pick a date range"
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
variant="classic"
|
||||
hideLabel
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user