Skip to content

Commit

Permalink
next: date field onInvalid and validate props (#683)
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte authored Sep 28, 2024
1 parent 0408af8 commit ee240a1
Show file tree
Hide file tree
Showing 25 changed files with 223 additions and 55 deletions.
12 changes: 10 additions & 2 deletions packages/bits-ui/src/lib/bits/command/command.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,21 +549,29 @@ class CommandRootState {
}
}

type CommandEmptyStateProps = WithRefProps;
type CommandEmptyStateProps = WithRefProps &
ReadableBoxedValues<{
forceMount: boolean;
}>;

class CommandEmptyState {
#ref: CommandEmptyStateProps["ref"];
#id: CommandEmptyStateProps["id"];
#root: CommandRootState;
#forceMount: CommandEmptyStateProps["forceMount"];
#isInitialRender = true;

shouldRender = $derived.by(
() => this.#root._commandState.filtered.count === 0 && this.#isInitialRender === false
() =>
(this.#root._commandState.filtered.count === 0 && this.#isInitialRender === false) ||
this.#forceMount.current
);

constructor(props: CommandEmptyStateProps, root: CommandRootState) {
this.#ref = props.ref;
this.#id = props.id;
this.#root = root;
this.#forceMount = props.forceMount;

$effect(() => {
this.#isInitialRender = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ref = $bindable(null),
children,
child,
forceMount = false,
...restProps
}: EmptyProps = $props();
Expand All @@ -19,6 +20,7 @@
() => ref,
(v) => (ref = v)
),
forceMount: box.with(() => forceMount),
});
const mergedProps = $derived(mergeProps(emptyState.props, restProps));
Expand Down
8 changes: 7 additions & 1 deletion packages/bits-ui/src/lib/bits/command/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,13 @@ export type CommandRootPropsWithoutHTML = WithChild<{
export type CommandRootProps = CommandRootPropsWithoutHTML &
Without<PrimitiveDivAttributes, CommandRootPropsWithoutHTML>;

export type CommandEmptyPropsWithoutHTML = WithChild;
export type CommandEmptyPropsWithoutHTML = WithChild<{
/**
* Whether to force mount the group container regardless of
* filtering logic.
*/
forceMount?: boolean;
}>;

export type CommandEmptyProps = CommandEmptyPropsWithoutHTML &
Without<PrimitiveDivAttributes, CommandEmptyPropsWithoutHTML>;
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
minValue,
onPlaceholderChange = noop,
onValueChange = noop,
isDateInvalid,
validate = noop,
onInvalid = noop,
placeholder = $bindable(),
value = $bindable(),
readonly = false,
Expand Down Expand Up @@ -71,10 +72,11 @@
locale: box.with(() => locale),
maxValue: box.with(() => maxValue),
minValue: box.with(() => minValue),
isDateInvalid: box.with(() => isDateInvalid),
validate: box.with(() => validate),
readonly: box.with(() => readonly),
readonlySegments: box.with(() => readonlySegments),
required: box.with(() => required),
onInvalid: box.with(() => onInvalid),
});
</script>

Expand Down
60 changes: 51 additions & 9 deletions packages/bits-ui/src/lib/bits/date-field/date-field.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@ import type { SegmentPart } from "$lib/shared/index.js";
import { DATE_SEGMENT_PARTS, TIME_SEGMENT_PARTS } from "$lib/shared/date/field/parts.js";
import { createContext } from "$lib/internal/createContext.js";
import { useId } from "$lib/internal/useId.js";
import type { DateMatcher, Granularity, HourCycle } from "$lib/shared/date/types.js";
import type {
DateOnInvalid,
DateValidator,
Granularity,
HourCycle,
} from "$lib/shared/date/types.js";
import { onDestroyEffect } from "$lib/internal/onDestroyEffect.svelte.js";

export const DATE_FIELD_INPUT_ATTR = "data-date-field-input";
Expand All @@ -65,7 +70,8 @@ export type DateFieldRootStateProps = WritableBoxedValues<{
}> &
ReadableBoxedValues<{
readonlySegments: SegmentPart[];
isDateInvalid: DateMatcher | undefined;
validate: DateValidator | undefined;
onInvalid: DateOnInvalid | undefined;
minValue: DateValue | undefined;
maxValue: DateValue | undefined;
disabled: boolean;
Expand All @@ -80,7 +86,7 @@ export type DateFieldRootStateProps = WritableBoxedValues<{
export class DateFieldRootState {
value: DateFieldRootStateProps["value"];
placeholder: WritableBox<DateValue>;
isDateInvalid: DateFieldRootStateProps["isDateInvalid"];
validate: DateFieldRootStateProps["validate"];
minValue: DateFieldRootStateProps["minValue"];
maxValue: DateFieldRootStateProps["maxValue"];
disabled: DateFieldRootStateProps["disabled"];
Expand All @@ -91,6 +97,7 @@ export class DateFieldRootState {
locale: DateFieldRootStateProps["locale"];
hideTimeZone: DateFieldRootStateProps["hideTimeZone"];
required: DateFieldRootStateProps["required"];
onInvalid: DateFieldRootStateProps["onInvalid"];
descriptionId = useId();
formatter: Formatter;
initialSegments: SegmentValueObj;
Expand All @@ -116,7 +123,7 @@ export class DateFieldRootState {
*/
this.value = props.value;
this.placeholder = rangeRoot ? rangeRoot.placeholder : props.placeholder;
this.isDateInvalid = rangeRoot ? rangeRoot.isDateInvalid : props.isDateInvalid;
this.validate = rangeRoot ? rangeRoot.validate : props.validate;
this.minValue = rangeRoot ? rangeRoot.minValue : props.minValue;
this.maxValue = rangeRoot ? rangeRoot.maxValue : props.maxValue;
this.disabled = rangeRoot ? rangeRoot.disabled : props.disabled;
Expand All @@ -127,6 +134,7 @@ export class DateFieldRootState {
this.locale = rangeRoot ? rangeRoot.locale : props.locale;
this.hideTimeZone = rangeRoot ? rangeRoot.hideTimeZone : props.hideTimeZone;
this.required = rangeRoot ? rangeRoot.required : props.required;
this.onInvalid = rangeRoot ? rangeRoot.onInvalid : props.onInvalid;
this.formatter = createFormatter(this.locale.current);
this.initialSegments = initializeSegmentValues(this.inferredGranularity);
this.segmentValues = this.initialSegments;
Expand Down Expand Up @@ -181,6 +189,18 @@ export class DateFieldRootState {

this.clearUpdating();
});

$effect(() => {
this.validationStatus;
untrack(() => {
if (this.validationStatus !== false) {
this.onInvalid.current?.(
this.validationStatus.reason,
this.validationStatus.message
);
}
});
});
}

setName = (name: string) => {
Expand Down Expand Up @@ -337,17 +357,39 @@ export class DateFieldRootState {
this.segmentValues = Object.fromEntries(dateValues);
}

isInvalid = $derived.by(() => {
validationStatus = $derived.by(() => {
const value = this.value.current;
if (!value) return false;
if (this.isDateInvalid.current?.(value)) return true;
if (!value) return false as const;

const msg = this.validate.current?.(value);

if (msg) {
return {
reason: "custom",
message: msg,
} as const;
}

const minValue = this.minValue.current;
if (minValue && isBefore(value, minValue)) return true;
if (minValue && isBefore(value, minValue)) {
return {
reason: "min",
} as const;
}
const maxValue = this.maxValue.current;
if (maxValue && isBefore(maxValue, value)) return true;
if (maxValue && isBefore(maxValue, value)) {
return {
reason: "max",
} as const;
}
return false;
});

isInvalid = $derived.by(() => {
if (this.validationStatus === false) return false;
return true;
});

inferredGranularity = $derived.by(() => {
const granularity = this.granularity.current;
if (granularity) return granularity;
Expand Down
24 changes: 17 additions & 7 deletions packages/bits-ui/src/lib/bits/date-field/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import type { SegmentPart, WithChildren } from "$lib/shared/index.js";
import type { OnChangeFn, WithChild, Without } from "$lib/internal/types.js";
import type { PrimitiveDivAttributes, PrimitiveSpanAttributes } from "$lib/shared/attributes.js";
import type { EditableSegmentPart } from "$lib/shared/date/field/types.js";
import type { DateMatcher, Granularity } from "$lib/shared/date/types.js";
import type {
DateMatcher,
DateOnInvalid,
DateValidator,
Granularity,
} from "$lib/shared/date/types.js";

export type DateFieldRootPropsWithoutHTML = WithChildren<{
/**
Expand Down Expand Up @@ -34,23 +39,28 @@ export type DateFieldRootPropsWithoutHTML = WithChildren<{
onPlaceholderChange?: OnChangeFn<DateValue | undefined>;

/**
* A function that returns true if the given date is invalid. This will mark
* the field as invalid and you will be responsible for displaying an error message
* to the user to inform them of the invalid state.
* A function that returns a string or array of strings as validation errors if the date is
* invalid, or nothing if the date is valid
*/
isDateInvalid?: DateMatcher;
validate?: DateValidator;

/**
* A callback fired when the date field's value is invalid. Use this to display an error
* message to the user.
*/
onInvalid?: DateOnInvalid;

/**
* The minimum acceptable date. When provided, the date field
* will be marked as invalid if the user enters a date before this date.
*/
minValue?: DateValue | undefined;
minValue?: DateValue;

/**
* The maximum acceptable date. When provided, the date field
* will be marked as invalid if the user enters a date after this date.
*/
maxValue?: DateValue | undefined;
maxValue?: DateValue;

/**
* If true, the date field will be disabled and users will not be able
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
placeholder = $bindable(),
onPlaceholderChange = noop,
isDateUnavailable = () => false,
validate = noop,
onInvalid = noop,
minValue,
maxValue,
disabled = false,
Expand Down Expand Up @@ -131,16 +133,13 @@
open: pickerRootState.props.open,
});
function isUnavailableOrDisabled(date: DateValue) {
return isDateDisabled(date) || isDateUnavailable(date);
}
useDateFieldRoot({
value: pickerRootState.props.value,
disabled: pickerRootState.props.disabled,
readonly: pickerRootState.props.readonly,
readonlySegments: pickerRootState.props.readonlySegments,
isDateInvalid: box.with(() => isUnavailableOrDisabled),
validate: box.with(() => validate),
onInvalid: box.with(() => onInvalid),
minValue: pickerRootState.props.minValue,
maxValue: pickerRootState.props.maxValue,
granularity: pickerRootState.props.granularity,
Expand Down
20 changes: 19 additions & 1 deletion packages/bits-ui/src/lib/bits/date-picker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import type { DateValue } from "@internationalized/date";
import type { OnChangeFn, WithChild, WithChildren, Without } from "$lib/internal/types.js";
import type { PrimitiveDivAttributes } from "$lib/shared/attributes.js";
import type { EditableSegmentPart } from "$lib/shared/date/field/types.js";
import type { DateMatcher, Granularity, WeekStartsOn } from "$lib/shared/date/types.js";
import type {
DateMatcher,
DateOnInvalid,
DateValidator,
Granularity,
WeekStartsOn,
} from "$lib/shared/date/types.js";
import type { CalendarRootSnippetProps } from "$lib/types.js";

export type DatePickerRootPropsWithoutHTML = WithChildren<{
Expand Down Expand Up @@ -54,6 +60,18 @@ export type DatePickerRootPropsWithoutHTML = WithChildren<{
*/
isDateDisabled?: DateMatcher;

/**
* A function that returns a string or array of strings as validation errors if the date is
* invalid, or nothing if the date is valid
*/
validate?: DateValidator;

/**
* A callback fired when the date field's value is invalid. Use this to display an error
* message to the user.
*/
onInvalid?: DateOnInvalid;

/**
* The minimum acceptable date. When provided, the date field
* will be marked as invalid if the user enters a date before this date.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
granularity,
locale = "en-US",
hideTimeZone = false,
isDateInvalid,
validate = noop,
onInvalid = noop,
maxValue,
minValue,
readonlySegments = [],
Expand Down Expand Up @@ -75,7 +76,7 @@
granularity: box.with(() => granularity),
locale: box.with(() => locale),
hideTimeZone: box.with(() => hideTimeZone),
isDateInvalid: box.with(() => isDateInvalid),
validate: box.with(() => validate),
maxValue: box.with(() => maxValue),
minValue: box.with(() => minValue),
placeholder: box.with(
Expand Down Expand Up @@ -115,6 +116,7 @@
onEndValueChange(v);
}
),
onInvalid: box.with(() => onInvalid),
});
const mergedProps = $derived(mergeProps(restProps, rootState.props));
Expand Down
Loading

0 comments on commit ee240a1

Please sign in to comment.