@koobiq/react-primitives 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ import type { GlobalDOMAttributes, FormProps as SharedFormProps } from '@koobiq/react-core';
2
+ import type { ContextValue, DOMProps } from '../../utils';
3
+ export interface FormProps extends SharedFormProps, DOMProps, GlobalDOMAttributes<HTMLFormElement> {
4
+ /**
5
+ * Whether to use native HTML form validation to prevent form submission
6
+ * when a field value is missing or invalid, or mark fields as required
7
+ * or invalid via ARIA.
8
+ * @default 'native'
9
+ */
10
+ validationBehavior?: 'aria' | 'native';
11
+ }
12
+ export declare const FormContext: import("react").Context<ContextValue<FormProps, HTMLFormElement>>;
13
+ /**
14
+ * A form is a group of inputs that allows users to submit data to a server,
15
+ * with support for providing field validation errors.
16
+ */
17
+ export declare const Form: import("react").ForwardRefExoticComponent<FormProps & import("react").RefAttributes<HTMLFormElement>>;
@@ -0,0 +1,29 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, forwardRef } from "react";
3
+ import { FormValidationContext } from "@react-stately/form";
4
+ import { useContextProps } from "../../utils/index.js";
5
+ const FormContext = createContext(null);
6
+ const Form = forwardRef(function Form2(props, ref) {
7
+ [props, ref] = useContextProps(props, ref, FormContext);
8
+ const {
9
+ validationErrors,
10
+ validationBehavior = "native",
11
+ children,
12
+ className,
13
+ ...domProps
14
+ } = props;
15
+ return /* @__PURE__ */ jsx(
16
+ "form",
17
+ {
18
+ noValidate: validationBehavior !== "native",
19
+ ...domProps,
20
+ ref,
21
+ className,
22
+ children: /* @__PURE__ */ jsx(FormContext.Provider, { value: { ...props, validationBehavior }, children: /* @__PURE__ */ jsx(FormValidationContext.Provider, { value: validationErrors ?? {}, children }) })
23
+ }
24
+ );
25
+ });
26
+ export {
27
+ Form,
28
+ FormContext
29
+ };
@@ -0,0 +1 @@
1
+ export * from './Form';
@@ -5,18 +5,21 @@ import { filterDOMProps } from "@koobiq/react-core";
5
5
  import { useLocale } from "@react-aria/i18n";
6
6
  import { useNumberField } from "@react-aria/numberfield";
7
7
  import { useNumberFieldState } from "@react-stately/numberfield";
8
- import { removeDataAttributes, useRenderProps, Provider } from "../../utils/index.js";
8
+ import { useSlottedContext, removeDataAttributes, useRenderProps, Provider } from "../../utils/index.js";
9
9
  import { GroupContext } from "../Group/GroupContext.js";
10
10
  import { ButtonContext } from "../Button/ButtonContext.js";
11
11
  import { LabelContext } from "../Label/LabelContext.js";
12
12
  import { InputContext } from "../Input/InputContext.js";
13
13
  import { TextContext } from "../Text/TextContext.js";
14
14
  import { FieldErrorContext } from "../FieldError/FieldError.js";
15
+ import { FormContext } from "../Form/Form.js";
15
16
  const NumberField = forwardRef(
16
17
  (props, ref) => {
17
18
  const { isDisabled, isReadOnly, isRequired } = props;
18
19
  const inputRef = useRef(null);
19
20
  const { locale } = useLocale();
21
+ const { validationBehavior: formValidationBehavior } = useSlottedContext(FormContext) || {};
22
+ const validationBehavior = props.validationBehavior ?? formValidationBehavior ?? "aria";
20
23
  const state = useNumberFieldState({ ...props, locale });
21
24
  const {
22
25
  labelProps,
@@ -27,7 +30,11 @@ const NumberField = forwardRef(
27
30
  incrementButtonProps,
28
31
  decrementButtonProps,
29
32
  ...validation
30
- } = useNumberField(removeDataAttributes(props), state, inputRef);
33
+ } = useNumberField(
34
+ { ...removeDataAttributes(props), validationBehavior },
35
+ state,
36
+ inputRef
37
+ );
31
38
  const DOMProps = filterDOMProps(props);
32
39
  delete DOMProps.id;
33
40
  const renderProps = useRenderProps({
@@ -2,23 +2,29 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { forwardRef } from "react";
4
4
  import { filterDOMProps } from "@koobiq/react-core";
5
- import { removeDataAttributes, useRenderProps, Provider } from "../../utils/index.js";
5
+ import { useSlottedContext, removeDataAttributes, useRenderProps, Provider } from "../../utils/index.js";
6
6
  import { useRadioGroupState } from "../../behaviors/useRadioGroupState.js";
7
7
  import { useRadioGroup } from "../../behaviors/useRadioGroup.js";
8
8
  import { FieldErrorContext } from "../FieldError/FieldError.js";
9
+ import { FormContext } from "../Form/Form.js";
9
10
  import { RadioContext } from "./RadioContext.js";
10
11
  import { LabelContext } from "../Label/LabelContext.js";
11
12
  import { TextContext } from "../Text/TextContext.js";
12
13
  const RadioGroup = forwardRef(
13
14
  (props, ref) => {
14
15
  const state = useRadioGroupState(props);
16
+ const { validationBehavior: formValidationBehavior } = useSlottedContext(FormContext) || {};
17
+ const validationBehavior = props.validationBehavior ?? formValidationBehavior ?? "aria";
15
18
  const {
16
19
  radioGroupProps,
17
20
  labelProps,
18
21
  descriptionProps,
19
22
  errorMessageProps,
20
23
  ...validation
21
- } = useRadioGroup(removeDataAttributes(props), state);
24
+ } = useRadioGroup(
25
+ { ...removeDataAttributes(props), validationBehavior },
26
+ state
27
+ );
22
28
  const renderProps = useRenderProps({
23
29
  ...props,
24
30
  values: {
@@ -3,14 +3,17 @@ import { jsx } from "react/jsx-runtime";
3
3
  import { forwardRef, useRef } from "react";
4
4
  import { filterDOMProps } from "@koobiq/react-core";
5
5
  import { useTextField } from "@react-aria/textfield";
6
- import { removeDataAttributes, useRenderProps, Provider } from "../../utils/index.js";
6
+ import { useSlottedContext, removeDataAttributes, useRenderProps, Provider } from "../../utils/index.js";
7
7
  import { InputContext } from "../Input/InputContext.js";
8
8
  import { TextareaContext } from "../Textarea/TextareaContext.js";
9
+ import { FormContext } from "../Form/Form.js";
9
10
  import { LabelContext } from "../Label/LabelContext.js";
10
11
  import { TextContext } from "../Text/TextContext.js";
11
12
  import { FieldErrorContext } from "../FieldError/FieldError.js";
12
13
  function TextFieldRender(props, ref) {
13
14
  const { isDisabled, isReadOnly, isRequired } = props;
15
+ const { validationBehavior: formValidationBehavior } = useSlottedContext(FormContext) || {};
16
+ const validationBehavior = props.validationBehavior ?? formValidationBehavior ?? "aria";
14
17
  const inputRef = useRef(null);
15
18
  const {
16
19
  labelProps,
@@ -20,7 +23,8 @@ function TextFieldRender(props, ref) {
20
23
  ...validation
21
24
  } = useTextField(
22
25
  {
23
- ...removeDataAttributes(props)
26
+ ...removeDataAttributes(props),
27
+ validationBehavior
24
28
  },
25
29
  inputRef
26
30
  );
@@ -12,3 +12,4 @@ export * from './ProgressBar';
12
12
  export * from './TextField';
13
13
  export * from './NumberField';
14
14
  export * from './FieldError';
15
+ export * from './Form';
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ import { useCalendar, useCalendarCell, useCalendarGrid } from "@react-aria/calen
30
30
  export * from "@react-stately/calendar";
31
31
  export * from "@react-stately/form";
32
32
  export * from "@react-aria/selection";
33
- import { Provider, removeDataAttributes, useRenderProps } from "./utils/index.js";
33
+ import { DEFAULT_SLOT, Provider, removeDataAttributes, useContextProps, useRenderProps, useSlottedContext } from "./utils/index.js";
34
34
  import { useButton } from "./behaviors/useButton.js";
35
35
  import { useCheckbox } from "./behaviors/useCheckbox.js";
36
36
  import { useLink } from "./behaviors/useLink.js";
@@ -64,12 +64,16 @@ import { ProgressBar } from "./components/ProgressBar/ProgressBar.js";
64
64
  import { TextField } from "./components/TextField/TextField.js";
65
65
  import { NumberField } from "./components/NumberField/NumberField.js";
66
66
  import { FieldError, FieldErrorContext } from "./components/FieldError/FieldError.js";
67
+ import { Form, FormContext } from "./components/Form/Form.js";
67
68
  export {
68
69
  Button,
69
70
  ButtonContext,
70
71
  Checkbox,
72
+ DEFAULT_SLOT,
71
73
  FieldError,
72
74
  FieldErrorContext,
75
+ Form,
76
+ FormContext,
73
77
  Group,
74
78
  GroupContext,
75
79
  Input,
@@ -95,6 +99,7 @@ export {
95
99
  useCalendarCell,
96
100
  useCalendarGrid,
97
101
  useCheckbox,
102
+ useContextProps,
98
103
  useGroupContext,
99
104
  useInputContext,
100
105
  useLink,
@@ -107,6 +112,7 @@ export {
107
112
  useRadioGroup,
108
113
  useRadioGroupState,
109
114
  useRenderProps,
115
+ useSlottedContext,
110
116
  useSwitch,
111
117
  useTextareaContext,
112
118
  useToggleButtonGroup,
@@ -1,5 +1,31 @@
1
- import type { JSX, CSSProperties, ReactNode, Context } from 'react';
2
- import type { AriaLabelingProps, DOMProps as SharedDOMProps } from '@react-types/shared';
1
+ import type { JSX, CSSProperties, ReactNode, Context, ForwardedRef } from 'react';
2
+ import type { AriaLabelingProps, DOMProps as SharedDOMProps, RefObject } from '@koobiq/react-core';
3
+ export declare const DEFAULT_SLOT: unique symbol;
4
+ interface SlottedValue<T> {
5
+ slots?: Record<string | symbol, T>;
6
+ }
7
+ export interface SlotProps {
8
+ /**
9
+ * A slot name for the component. Slots allow the component to receive props from a parent component.
10
+ * An explicit `null` value indicates that the local props completely override all props received from a parent.
11
+ */
12
+ slot?: string | null;
13
+ }
14
+ export type WithRef<T, E> = T & {
15
+ ref?: ForwardedRef<E>;
16
+ };
17
+ export type SlottedContextValue<T> = SlottedValue<T> | T | null | undefined;
18
+ export type ContextValue<T, E> = SlottedContextValue<WithRef<T, E>>;
19
+ export interface StyleProps {
20
+ /** The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. */
21
+ className?: string;
22
+ /** The inline [style](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style) for the element. */
23
+ style?: CSSProperties;
24
+ }
25
+ export interface DOMProps extends StyleProps, SharedDOMProps {
26
+ /** The children of the component. */
27
+ children?: ReactNode;
28
+ }
3
29
  export interface StyleRenderProps<T> {
4
30
  /** The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state. */
5
31
  className?: string | ((values: T & {
@@ -38,4 +64,6 @@ interface ProviderProps<T extends unknown[]> {
38
64
  }
39
65
  export declare function Provider<T extends unknown[]>({ values, children, }: ProviderProps<T>): JSX.Element;
40
66
  export declare function removeDataAttributes<T>(props: T): T;
67
+ export declare function useSlottedContext<T>(context: Context<SlottedContextValue<T>>, slot?: string | null): T | null | undefined;
68
+ export declare function useContextProps<T, U extends SlotProps, E extends Element>(props: T & SlotProps, ref: ForwardedRef<E> | undefined, context: Context<ContextValue<U, E>>): [T, RefObject<E | null>];
41
69
  export {};
@@ -1,5 +1,7 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { useMemo } from "react";
2
+ import { useMemo, useContext } from "react";
3
+ import { useObjectRef, mergeRefs, mergeProps } from "@koobiq/react-core";
4
+ const DEFAULT_SLOT = Symbol("default");
3
5
  function useRenderProps(props) {
4
6
  const {
5
7
  className,
@@ -65,8 +67,52 @@ function removeDataAttributes(props) {
65
67
  }
66
68
  return filteredProps;
67
69
  }
70
+ function useSlottedContext(context, slot) {
71
+ const ctx = useContext(context);
72
+ if (slot === null) {
73
+ return null;
74
+ }
75
+ if (ctx && typeof ctx === "object" && "slots" in ctx && ctx.slots) {
76
+ const slotKey = slot || DEFAULT_SLOT;
77
+ if (!ctx.slots[slotKey]) {
78
+ const availableSlots = new Intl.ListFormat().format(
79
+ Object.keys(ctx.slots).map((p) => `"${p}"`)
80
+ );
81
+ const errorMessage = slot ? `Invalid slot "${slot}".` : "A slot prop is required.";
82
+ throw new Error(
83
+ `${errorMessage} Valid slot names are ${availableSlots}.`
84
+ );
85
+ }
86
+ return ctx.slots[slotKey];
87
+ }
88
+ return ctx;
89
+ }
90
+ function useContextProps(props, ref, context) {
91
+ const ctx = useSlottedContext(context, props.slot) || {};
92
+ const { ref: contextRef, ...contextProps } = ctx;
93
+ const mergedRef = useObjectRef(
94
+ useMemo(() => mergeRefs(ref, contextRef), [ref, contextRef])
95
+ );
96
+ const mergedProps = mergeProps(contextProps, props);
97
+ if ("style" in contextProps && contextProps.style && "style" in props && props.style) {
98
+ if (typeof contextProps.style === "function" || typeof props.style === "function") {
99
+ mergedProps.style = (renderProps) => {
100
+ const contextStyle = typeof contextProps.style === "function" ? contextProps.style(renderProps) : contextProps.style;
101
+ const defaultStyle = { ...renderProps.defaultStyle, ...contextStyle };
102
+ const style = typeof props.style === "function" ? props.style({ ...renderProps, defaultStyle }) : props.style;
103
+ return { ...defaultStyle, ...style };
104
+ };
105
+ } else {
106
+ mergedProps.style = { ...contextProps.style, ...props.style };
107
+ }
108
+ }
109
+ return [mergedProps, mergedRef];
110
+ }
68
111
  export {
112
+ DEFAULT_SLOT,
69
113
  Provider,
70
114
  removeDataAttributes,
71
- useRenderProps
115
+ useContextProps,
116
+ useRenderProps,
117
+ useSlottedContext
72
118
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koobiq/react-primitives",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "license": "MIT",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -64,8 +64,8 @@
64
64
  "@react-stately/toggle": "^3.7.0",
65
65
  "@react-stately/tooltip": "^3.5.5",
66
66
  "@react-stately/tree": "^3.8.9",
67
- "@koobiq/logger": "0.10.0",
68
- "@koobiq/react-core": "0.10.0"
67
+ "@koobiq/logger": "0.11.0",
68
+ "@koobiq/react-core": "0.11.0"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": "18.x || 19.x",