Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix passing user props through #72

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/rude-melons-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@solid-aria/button": patch
"@solid-aria/focus": patch
"@solid-aria/textfield": patch
---

Fix user props not being passed down to the returned props object. (#69)
23 changes: 5 additions & 18 deletions packages/button/src/createButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
/* eslint-disable solid/reactivity */

import { createFocusable } from "@solid-aria/focus";
import { createPress } from "@solid-aria/interactions";
import { ElementType } from "@solid-aria/types";
import { filterDOMProps } from "@solid-aria/utils";
import { combineProps } from "@solid-primitives/props";
import { Accessor, createMemo, JSX, mergeProps, splitProps } from "solid-js";
import { Accessor, JSX, mergeProps, splitProps } from "solid-js";

import { AriaButtonProps } from "./types";

Expand Down Expand Up @@ -82,7 +82,6 @@ export function createButton(
type: "button"
};

// eslint-disable-next-line solid/reactivity
props = mergeProps(defaultProps, props);

const additionalButtonElementProps: JSX.ButtonHTMLAttributes<any> = {
Expand Down Expand Up @@ -120,12 +119,8 @@ export function createButton(
}
};

const additionalProps = mergeProps(
createMemo(() => {
return props.elementType === "button"
? additionalButtonElementProps
: additionalOtherElementProps;
})
const additionalProps = mergeProps(() =>
props.elementType === "button" ? additionalButtonElementProps : additionalOtherElementProps
);

const [createPressProps] = splitProps(props, [
Expand All @@ -141,8 +136,6 @@ export function createButton(

const { focusableProps } = createFocusable(props, ref);

const domProps = filterDOMProps(props, { labelable: true });

const baseButtonProps: JSX.HTMLAttributes<any> = {
get "aria-haspopup"() {
return props["aria-haspopup"];
Expand All @@ -169,13 +162,7 @@ export function createButton(
}
};

const buttonProps = combineProps(
additionalProps,
focusableProps,
pressProps,
domProps,
baseButtonProps
);
const buttonProps = combineProps(additionalProps, focusableProps, pressProps, baseButtonProps);

return { buttonProps, isPressed };
}
26 changes: 26 additions & 0 deletions packages/button/test/createButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
* governing permissions and limitations under the License.
*/

import { JSX } from "solid-js";

import { createButton } from "../src";
import { AriaButtonProps } from "../src/types";

Expand Down Expand Up @@ -88,4 +90,28 @@ describe("createButton", () => {
// @ts-ignore
expect(buttonProps.rel).toBeUndefined();
});

it("user props are passed down to the returned button props", () => {
const ref = document.createElement("button");
const onMouseMove = jest.fn();
const props: AriaButtonProps<"button"> & JSX.IntrinsicElements["button"] = {
children: "Hello",
class: "test-class",
onMouseMove,
style: { color: "red" }
};

const { buttonProps } = createButton(props, () => ref);

expect(buttonProps.children).toBe("Hello");
expect(buttonProps.class).toBe("test-class");
expect(buttonProps.style).toEqual({ color: "red" });

expect(buttonProps).toHaveProperty("onMouseMove");
expect(typeof buttonProps.onMouseMove).toBe("function");

// @ts-expect-error
buttonProps.onMouseMove();
expect(onMouseMove).toHaveBeenCalled();
});
});
5 changes: 2 additions & 3 deletions packages/focus/src/createFocusable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,11 @@ export function createFocusable(
const { focusProps } = createFocus(props);
const { keyboardProps } = createKeyboard(props);

const focusableProps = {
...combineProps(focusProps, keyboardProps),
const focusableProps = combineProps(props, focusProps, keyboardProps, {
get tabIndex() {
return access(props.excludeFromTabOrder) && !access(props.isDisabled) ? -1 : undefined;
}
};
});

onMount(() => {
autofocus() && ref()?.focus();
Expand Down
30 changes: 30 additions & 0 deletions packages/focus/test/createFocusable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable solid/reactivity */
import { JSX } from "solid-js";

import { createFocusable, CreateFocusableProps } from "../src";

describe("createFocusable", () => {
test("user props are passed through", () => {
const ref = document.createElement("button");
const onClick = jest.fn();
const props: CreateFocusableProps & JSX.IntrinsicElements["button"] = {
children: "Hello",
class: "test-class",
onClick,
style: { color: "red" }
};

const { focusableProps } = createFocusable(props, () => ref);

expect(focusableProps.children).toBe("Hello");
expect(focusableProps.class).toBe("test-class");
expect(focusableProps.style).toEqual({ color: "red" });

expect(focusableProps).toHaveProperty("onClick");
expect(typeof focusableProps.onClick).toBe("function");

// @ts-expect-error
focusableProps.onClick();
expect(onClick).toHaveBeenCalled();
});
});
199 changes: 109 additions & 90 deletions packages/textfield/src/createTextField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,15 @@ import {
Validation,
ValueBase
} from "@solid-aria/types";
import { callHandler, filterDOMProps } from "@solid-aria/utils";
import { combineProps } from "@solid-primitives/props";
import { Accessor, createMemo, JSX, mergeProps } from "solid-js";
import { callHandler } from "@solid-aria/utils";
import { Accessor, JSX, mergeProps, splitProps } from "solid-js";

type DefaultElementType = "input";

/**
* The intrinsic HTML element names that `createTextField` supports; e.g. `input`, `textarea`.
*/
type TextFieldIntrinsicElements = keyof Pick<IntrinsicHTMLElements, "input" | "textarea">;
type TextFieldIntrinsicElements = "input" | "textarea";

/**
* The HTML element interfaces that `createTextField` supports based on what is
Expand Down Expand Up @@ -121,6 +120,23 @@ export interface TextFieldAria<T extends TextFieldIntrinsicElements = DefaultEle
errorMessageProps: JSX.HTMLAttributes<any>;
}

const inputPropKeys = [
"type",
"pattern",
"aria-errormessage",
"aria-activedescendant",
"aria-autocomplete",
"aria-haspopup",
"value",
"defaultValue",
"autocomplete",
"maxLength",
"minLength",
"name",
"placeholder",
"inputMode"
] as const;

/**
* Provides the behavior and accessibility implementation for a text field.
* @param props - Props for the text field.
Expand All @@ -141,93 +157,96 @@ export function createTextField<T extends TextFieldIntrinsicElements = DefaultEl
// eslint-disable-next-line solid/reactivity
props = mergeProps(defaultProps, props) as AriaTextFieldProps<T>;

const { focusableProps } = createFocusable(props, ref);
const { labelProps, fieldProps, descriptionProps, errorMessageProps } = createField(props);

const domProps = filterDOMProps(props, { labelable: true });

const baseInputProps: JSX.HTMLAttributes<any> = mergeProps(
{
get type() {
return props.inputElementType === "input" ? props.type : undefined;
},
get pattern() {
return props.inputElementType === "input" ? props.pattern : undefined;
},
get disabled() {
return props.isDisabled;
},
get readOnly() {
return props.isReadOnly;
},
get "aria-required"() {
return props.isRequired || undefined;
},
get "aria-invalid"() {
return props.validationState === "invalid" || undefined;
},
get "aria-errormessage"() {
return props["aria-errormessage"];
},
get "aria-activedescendant"() {
return props["aria-activedescendant"];
},
get "aria-autocomplete"() {
return props["aria-autocomplete"];
},
get "aria-haspopup"() {
return props["aria-haspopup"];
},
get value() {
return props.value;
},
get defaultValue() {
return props.value ? undefined : props.defaultValue;
},
get autocomplete() {
return props.autocomplete;
},
get maxLength() {
return props.maxLength;
},
get minLength() {
return props.minLength;
},
get name() {
return props.name;
},
get placeholder() {
return props.placeholder;
},
get inputMode() {
return props.inputMode;
},

// Change events
onChange: e => props.onChange?.((e.target as HTMLInputElement).value),

// Clipboard events
onCopy: e => callHandler(props.onCopy, e),
onCut: e => callHandler(props.onCut, e),
onPaste: e => callHandler(props.onPaste, e),

// Composition events
onCompositionEnd: e => callHandler(props.onCompositionEnd, e),
onCompositionStart: e => callHandler(props.onCompositionStart, e),
onCompositionUpdate: e => callHandler(props.onCompositionUpdate, e),

// Selection events
onSelect: e => callHandler(props.onSelect, e),

// Input events
onBeforeInput: e => callHandler(props.onBeforeInput, e),
onInput: e => callHandler(props.onInput, e)
} as JSX.HTMLAttributes<any>,
focusableProps,
fieldProps
);
// local props are separated so that they don't mess with mergeProps
// e.g. the `type` prop should return `undefined` if the element is not an input
// but `mergeProps` will search for the first defined value (ignoring undefined)
const localProps = splitProps(props, inputPropKeys)[1];

const { focusableProps } = createFocusable(localProps, ref);
const { labelProps, fieldProps, descriptionProps, errorMessageProps } = createField(localProps);

const baseInputProps: JSX.IntrinsicElements["input"] & { defaultValue: string | undefined } = {
get type() {
return props.inputElementType === "input" ? props.type : undefined;
},
get pattern() {
return props.inputElementType === "input" ? props.pattern : undefined;
},
get disabled() {
return props.isDisabled;
},
get readOnly() {
return props.isReadOnly;
},
get "aria-required"() {
return props.isRequired || undefined;
},
get "aria-invalid"() {
return props.validationState === "invalid" || undefined;
},
get "aria-errormessage"() {
return props["aria-errormessage"];
},
get "aria-activedescendant"() {
return props["aria-activedescendant"];
},
get "aria-autocomplete"() {
return props["aria-autocomplete"];
},
get "aria-haspopup"() {
return props["aria-haspopup"];
},
get value() {
return props.value;
},
get defaultValue() {
return props.value ? undefined : props.defaultValue;
},
get autocomplete() {
return props.autocomplete;
},
get maxLength() {
return props.maxLength;
},
get minLength() {
return props.minLength;
},
get name() {
return props.name;
},
get placeholder() {
return props.placeholder;
},
get inputMode() {
return props.inputMode;
},

// Change events
onChange: e => props.onChange?.((e.target as HTMLInputElement).value),

// Clipboard events
onCopy: e => callHandler(props.onCopy, e),
onCut: e => callHandler(props.onCut, e),
onPaste: e => callHandler(props.onPaste, e),

const inputProps = combineProps(domProps, baseInputProps);
// Composition events
onCompositionEnd: e => callHandler(props.onCompositionEnd, e),
onCompositionStart: e => callHandler(props.onCompositionStart, e),
onCompositionUpdate: e => callHandler(props.onCompositionUpdate, e),

// Selection events
onSelect: e => callHandler(props.onSelect, e),

// Input events
onBeforeInput: e => callHandler(props.onBeforeInput, e),
onInput: e => callHandler(props.onInput, e)
};

const inputProps = mergeProps(
focusableProps,
fieldProps,
baseInputProps
) as TextFieldInputProps<T>;

return {
labelProps,
Expand Down
Loading