mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(input): api improved
This commit is contained in:
parent
1ee0293bb3
commit
59003bd698
@ -102,6 +102,7 @@ CustomIconNode.args = {
|
||||
export const CustomIconFunction = Template.bind({});
|
||||
CustomIconFunction.args = {
|
||||
...defaultProps,
|
||||
// eslint-disable-next-line react/display-name
|
||||
icon: (props: CheckboxIconProps) => <CloseIcon {...props} />,
|
||||
};
|
||||
|
||||
@ -213,11 +214,17 @@ export const CustomWithStyles = (props: CustomCheckboxProps) => {
|
||||
};
|
||||
|
||||
export const CustomWithHooks = (props: CheckboxProps) => {
|
||||
const {children, isSelected, isFocusVisible, getBaseProps, getLabelProps, getInputProps} =
|
||||
useCheckbox({
|
||||
"aria-label": props["aria-label"] || "Toggle status",
|
||||
...props,
|
||||
});
|
||||
const {
|
||||
children,
|
||||
isSelected,
|
||||
isFocusVisible,
|
||||
getBaseProps,
|
||||
getLabelProps,
|
||||
getInputProps,
|
||||
} = useCheckbox({
|
||||
"aria-label": props["aria-label"] || "Toggle status",
|
||||
...props,
|
||||
});
|
||||
|
||||
return (
|
||||
<label {...getBaseProps()}>
|
||||
@ -226,12 +233,11 @@ export const CustomWithHooks = (props: CheckboxProps) => {
|
||||
</VisuallyHidden>
|
||||
<Chip
|
||||
color="primary"
|
||||
leftContent={isSelected ? <CheckIcon className="ml-1" color={colors.white} /> : null}
|
||||
startContent={isSelected ? <CheckIcon className="ml-1" color={colors.white} /> : null}
|
||||
styles={{
|
||||
base: clsx("border-neutral hover:bg-neutral-200", {
|
||||
"border-primary bg-primary hover:bg-primary-600 hover:border-primary-600": isSelected,
|
||||
"outline-none ring-2 !ring-primary ring-offset-2 ring-offset-background dark:ring-offset-background-dark":
|
||||
isFocusVisible,
|
||||
"outline-none ring-2 !ring-primary ring-offset-2 ring-offset-background dark:ring-offset-background-dark": isFocusVisible,
|
||||
}),
|
||||
content: clsx("text-primary", {
|
||||
"text-primary-contrastText pl-1": isSelected,
|
||||
|
||||
@ -31,13 +31,13 @@ describe("Chip", () => {
|
||||
expect(wrapper.getByTestId("avatar")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should support leftContent", () => {
|
||||
const wrapper = render(<Chip leftContent={<span data-testid="left-icon" />} />);
|
||||
it("should support startContent", () => {
|
||||
const wrapper = render(<Chip startContent={<span data-testid="left-icon" />} />);
|
||||
|
||||
expect(wrapper.getByTestId("left-icon")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should support rightContent", () => {
|
||||
it("should support endContent", () => {
|
||||
const wrapper = render(<Chip avatar={<span data-testid="close-icon" />} />);
|
||||
|
||||
expect(wrapper.getByTestId("close-icon")).not.toBeNull();
|
||||
|
||||
@ -15,8 +15,8 @@ const Chip = forwardRef<ChipProps, "div">((props, ref) => {
|
||||
styles,
|
||||
isDot,
|
||||
isCloseable,
|
||||
leftContent,
|
||||
rightContent,
|
||||
startContent,
|
||||
endContent,
|
||||
getCloseButtonProps,
|
||||
getChipProps,
|
||||
} = useChip({
|
||||
@ -24,29 +24,27 @@ const Chip = forwardRef<ChipProps, "div">((props, ref) => {
|
||||
...props,
|
||||
});
|
||||
|
||||
const left = useMemo(() => {
|
||||
if (isDot && !leftContent) {
|
||||
const start = useMemo(() => {
|
||||
if (isDot && !startContent) {
|
||||
return <span className={slots.dot({class: styles?.dot})} />;
|
||||
}
|
||||
|
||||
return leftContent;
|
||||
}, [slots, leftContent, isDot]);
|
||||
return startContent;
|
||||
}, [slots, startContent, isDot]);
|
||||
|
||||
const right = useMemo(() => {
|
||||
const end = useMemo(() => {
|
||||
if (isCloseable) {
|
||||
return (
|
||||
<span {...getCloseButtonProps()}>{!rightContent ? <CloseFilledIcon /> : rightContent}</span>
|
||||
);
|
||||
return <span {...getCloseButtonProps()}>{endContent || <CloseFilledIcon />}</span>;
|
||||
}
|
||||
|
||||
return rightContent;
|
||||
}, [rightContent, isCloseable, getCloseButtonProps]);
|
||||
return endContent;
|
||||
}, [endContent, isCloseable, getCloseButtonProps]);
|
||||
|
||||
return (
|
||||
<Component {...getChipProps()}>
|
||||
{left}
|
||||
{start}
|
||||
<span className={slots.content({class: styles?.content})}>{children}</span>
|
||||
{right}
|
||||
{end}
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
|
||||
@ -24,11 +24,14 @@ export interface UseChipProps extends HTMLNextUIProps<"div">, ChipVariantProps {
|
||||
* Element to be rendered in the left side of the chip.
|
||||
* this props overrides the `avatar` prop.
|
||||
*/
|
||||
leftContent?: React.ReactNode;
|
||||
startContent?: React.ReactNode;
|
||||
/**
|
||||
* Element to be rendered in the right side of the chip.
|
||||
* if you pass this prop and the `onClose` prop, the passed element
|
||||
* will have the close button props and it will be rendered instead of the
|
||||
* default close button.
|
||||
*/
|
||||
rightContent?: React.ReactNode;
|
||||
endContent?: React.ReactNode;
|
||||
/**
|
||||
* Classname or List of classes to change the styles of the element.
|
||||
* if `className` is passed, it will be added to the base slot.
|
||||
@ -47,7 +50,7 @@ export interface UseChipProps extends HTMLNextUIProps<"div">, ChipVariantProps {
|
||||
styles?: SlotsToClasses<ChipSlots>;
|
||||
/**
|
||||
* Callback fired when the chip is closed. if you pass this prop,
|
||||
* the chip will display a close button (rightContent).
|
||||
* the chip will display a close button (endContent).
|
||||
* @param e PressEvent
|
||||
*/
|
||||
onClose?: (e: PressEvent) => void;
|
||||
@ -61,8 +64,8 @@ export function useChip(originalProps: UseChipProps) {
|
||||
as,
|
||||
children,
|
||||
avatar,
|
||||
leftContent,
|
||||
rightContent,
|
||||
startContent,
|
||||
endContent,
|
||||
onClose,
|
||||
styles,
|
||||
className,
|
||||
@ -80,28 +83,27 @@ export function useChip(originalProps: UseChipProps) {
|
||||
|
||||
const {focusProps: closeFocusProps, isFocusVisible: isCloseButtonFocusVisible} = useFocusRing();
|
||||
|
||||
const isOneChar = useMemo(
|
||||
() => typeof children === "string" && children?.length === 1,
|
||||
[children],
|
||||
);
|
||||
const isOneChar = useMemo(() => typeof children === "string" && children?.length === 1, [
|
||||
children,
|
||||
]);
|
||||
|
||||
const hasLeftContent = useMemo(() => !!avatar || !!leftContent, [avatar, leftContent]);
|
||||
const hasRightContent = useMemo(() => !!rightContent || isCloseable, [rightContent, isCloseable]);
|
||||
const hasStartContent = useMemo(() => !!avatar || !!startContent, [avatar, startContent]);
|
||||
const hasEndContent = useMemo(() => !!endContent || isCloseable, [endContent, isCloseable]);
|
||||
|
||||
const slots = useMemo(
|
||||
() =>
|
||||
chip({
|
||||
...variantProps,
|
||||
hasLeftContent,
|
||||
hasRightContent,
|
||||
hasStartContent,
|
||||
hasEndContent,
|
||||
isOneChar,
|
||||
isCloseButtonFocusVisible,
|
||||
}),
|
||||
[
|
||||
...Object.values(variantProps),
|
||||
isCloseButtonFocusVisible,
|
||||
hasLeftContent,
|
||||
hasRightContent,
|
||||
hasStartContent,
|
||||
hasEndContent,
|
||||
isOneChar,
|
||||
],
|
||||
);
|
||||
@ -152,8 +154,8 @@ export function useChip(originalProps: UseChipProps) {
|
||||
styles,
|
||||
isDot: isDotVariant,
|
||||
isCloseable,
|
||||
leftContent: getAvatarClone(avatar) || getContentClone(leftContent),
|
||||
rightContent: getContentClone(rightContent),
|
||||
startContent: getAvatarClone(avatar) || getContentClone(startContent),
|
||||
endContent: getContentClone(endContent),
|
||||
getCloseButtonProps,
|
||||
getChipProps,
|
||||
};
|
||||
|
||||
@ -54,16 +54,24 @@ Default.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const LeftContent = Template.bind({});
|
||||
LeftContent.args = {
|
||||
export const StartContent = Template.bind({});
|
||||
StartContent.args = {
|
||||
...defaultProps,
|
||||
leftContent: <span className="ml-1">🎉</span>,
|
||||
startContent: (
|
||||
<span aria-label="celebration" className="ml-1" role="img">
|
||||
🎉
|
||||
</span>
|
||||
),
|
||||
};
|
||||
|
||||
export const RightContent = Template.bind({});
|
||||
RightContent.args = {
|
||||
export const EndContent = Template.bind({});
|
||||
EndContent.args = {
|
||||
...defaultProps,
|
||||
rightContent: <span className="mr-1">🚀</span>,
|
||||
endContent: (
|
||||
<span aria-label="rocket" className="mr-1" role="img">
|
||||
🚀
|
||||
</span>
|
||||
),
|
||||
};
|
||||
|
||||
export const Closeable = Template.bind({});
|
||||
@ -76,7 +84,7 @@ Closeable.args = {
|
||||
export const CustomCloseIcon = Template.bind({});
|
||||
CustomCloseIcon.args = {
|
||||
...defaultProps,
|
||||
rightContent: <CheckIcon />,
|
||||
endContent: <CheckIcon />,
|
||||
// eslint-disable-next-line
|
||||
onClose: () => console.log("Close"),
|
||||
};
|
||||
|
||||
@ -39,12 +39,14 @@
|
||||
"dependencies": {
|
||||
"@nextui-org/dom-utils": "workspace:*",
|
||||
"@nextui-org/shared-utils": "workspace:*",
|
||||
"@nextui-org/shared-icons": "workspace:*",
|
||||
"@nextui-org/system": "workspace:*",
|
||||
"@nextui-org/theme": "workspace:*",
|
||||
"@nextui-org/use-aria-field": "workspace:*",
|
||||
"@react-aria/focus": "^3.11.0",
|
||||
"@react-aria/utils": "^3.15.0",
|
||||
"@react-stately/utils": "^3.6.0"
|
||||
"@react-stately/utils": "^3.6.0",
|
||||
"@react-aria/interactions": "^3.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-types/shared": "^3.15.0",
|
||||
|
||||
@ -1,33 +1,62 @@
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
import {CloseFilledIcon} from "@nextui-org/shared-icons";
|
||||
import {useMemo} from "react";
|
||||
|
||||
import {UseInputProps, useInput} from "./use-input";
|
||||
|
||||
export interface InputProps extends Omit<UseInputProps, "ref"> {}
|
||||
export interface InputProps extends Omit<UseInputProps, "ref" | "isClearButtonFocusVisible"> {}
|
||||
|
||||
const Input = forwardRef<InputProps, "input">((props, ref) => {
|
||||
const {
|
||||
Component,
|
||||
label,
|
||||
description,
|
||||
isClearable,
|
||||
startContent,
|
||||
endContent,
|
||||
shouldLabelBeOutside,
|
||||
shouldLabelBeInside,
|
||||
errorMessage,
|
||||
getBaseProps,
|
||||
getLabelProps,
|
||||
getInputProps,
|
||||
getInnerWrapperProps,
|
||||
getInputWrapperProps,
|
||||
getDescriptionProps,
|
||||
getErrorMessageProps,
|
||||
getClearButtonProps,
|
||||
} = useInput({ref, ...props});
|
||||
|
||||
const labelContent = <label {...getLabelProps()}>{label}</label>;
|
||||
|
||||
const end = useMemo(() => {
|
||||
if (isClearable) {
|
||||
return <span {...getClearButtonProps()}>{endContent || <CloseFilledIcon />}</span>;
|
||||
}
|
||||
|
||||
return endContent;
|
||||
}, [isClearable, getClearButtonProps]);
|
||||
|
||||
const innerWrapper = useMemo(() => {
|
||||
if (startContent || end) {
|
||||
return (
|
||||
<div {...getInnerWrapperProps()}>
|
||||
{startContent}
|
||||
<input {...getInputProps()} />
|
||||
{end}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <input {...getInputProps()} />;
|
||||
}, [startContent, end, getInputProps, getInnerWrapperProps]);
|
||||
|
||||
return (
|
||||
<Component {...getBaseProps()}>
|
||||
{shouldLabelBeOutside ? labelContent : null}
|
||||
<div {...getInputWrapperProps()}>
|
||||
{shouldLabelBeInside ? labelContent : null}
|
||||
<input {...getInputProps()} />
|
||||
{innerWrapper}
|
||||
</div>
|
||||
{description && <div {...getDescriptionProps()}>{description}</div>}
|
||||
{errorMessage && <div {...getErrorMessageProps()}>{errorMessage}</div>}
|
||||
|
||||
@ -5,6 +5,7 @@ import {AriaTextFieldProps} from "@react-types/textfield";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {input} from "@nextui-org/theme";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {usePress} from "@react-aria/interactions";
|
||||
import {clsx, dataAttr} from "@nextui-org/shared-utils";
|
||||
import {useControlledState} from "@react-stately/utils";
|
||||
import {useMemo, Ref} from "react";
|
||||
@ -17,17 +18,34 @@ export interface Props extends HTMLNextUIProps<"input"> {
|
||||
* Ref to the DOM node.
|
||||
*/
|
||||
ref?: Ref<HTMLInputElement>;
|
||||
/**
|
||||
* Element to be rendered in the left side of the input.
|
||||
*/
|
||||
startContent?: React.ReactNode;
|
||||
/**
|
||||
* Element to be rendered in the right side of the input.
|
||||
* if you pass this prop and the `onClear` prop, the passed element
|
||||
* will have the clear button props and it will be rendered instead of the
|
||||
* default clear button.
|
||||
*/
|
||||
endContent?: React.ReactNode;
|
||||
/**
|
||||
* Callback fired when the value is cleared.
|
||||
* if you pass this prop, the clear button will be shown.
|
||||
*/
|
||||
onClear?: () => void;
|
||||
/**
|
||||
* Classname or List of classes to change the styles of the element.
|
||||
* if `className` is passed, it will be added to the base slot.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* <Chip styles={{
|
||||
* <Input styles={{
|
||||
* base:"base-classes",
|
||||
* label: "label-classes",
|
||||
* inputWrapper: "input-wrapper-classes",
|
||||
* input: "input-classes",
|
||||
* clearButton: "clear-button-classes",
|
||||
* description: "description-classes",
|
||||
* helperText: "helper-text-classes",
|
||||
* }} />
|
||||
@ -50,19 +68,33 @@ export function useInput(originalProps: UseInputProps) {
|
||||
className,
|
||||
styles,
|
||||
autoFocus,
|
||||
startContent,
|
||||
endContent,
|
||||
onClear,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const [inputValue, setInputValue] = useControlledState(props.value, props.defaultValue, () => {});
|
||||
|
||||
const Component = as || "div";
|
||||
const baseStyles = clsx(styles?.base, className);
|
||||
const baseStyles = clsx(styles?.base, className, !!inputValue ? "is-filled" : "");
|
||||
|
||||
const domRef = useDOMRef<HTMLInputElement>(ref);
|
||||
|
||||
const [inputValue, setInputValue] = useControlledState(props.value, props.defaultValue, () => {});
|
||||
const handleClear = () => {
|
||||
setInputValue(undefined);
|
||||
|
||||
if (domRef.current) {
|
||||
domRef.current.value = "";
|
||||
domRef.current.focus();
|
||||
}
|
||||
onClear?.();
|
||||
};
|
||||
|
||||
const {labelProps, inputProps, descriptionProps, errorMessageProps} = useAriaTextField(
|
||||
{
|
||||
...originalProps,
|
||||
value: inputValue,
|
||||
onChange: chain(props.onChange, setInputValue),
|
||||
},
|
||||
domRef,
|
||||
@ -73,22 +105,43 @@ export function useInput(originalProps: UseInputProps) {
|
||||
isTextInput: true,
|
||||
});
|
||||
|
||||
const {focusProps: clearFocusProps, isFocusVisible: isClearButtonFocusVisible} = useFocusRing();
|
||||
|
||||
const {pressProps: clearPressProps} = usePress({
|
||||
isDisabled: !!originalProps?.isDisabled,
|
||||
onPress: handleClear,
|
||||
});
|
||||
|
||||
const isInvalid = props.validationState === "invalid";
|
||||
const labelPosition = originalProps.labelPosition || "outside";
|
||||
const isLabelPlaceholder = !props.placeholder;
|
||||
const isLabelPlaceholder = !props.placeholder && labelPosition !== "outside-left";
|
||||
const isClearable = !!onClear || originalProps.isClearable;
|
||||
|
||||
const shouldLabelBeOutside = labelPosition === "outside" || labelPosition === "outside-left";
|
||||
const shouldLabelBeInside = labelPosition === "inside";
|
||||
|
||||
const hasStartContent = useMemo(() => !!startContent, [startContent]);
|
||||
const hasEndContent = useMemo(() => !!endContent || isClearable, [endContent, isClearable]);
|
||||
|
||||
const slots = useMemo(
|
||||
() =>
|
||||
input({
|
||||
...variantProps,
|
||||
isInvalid,
|
||||
isLabelPlaceholder,
|
||||
isClearable,
|
||||
isFocusVisible,
|
||||
isLabelPlaceholder: isLabelPlaceholder && !hasStartContent,
|
||||
isClearButtonFocusVisible,
|
||||
}),
|
||||
[...Object.values(variantProps), isInvalid, isLabelPlaceholder, isFocusVisible],
|
||||
[
|
||||
...Object.values(variantProps),
|
||||
isInvalid,
|
||||
isClearable,
|
||||
isClearButtonFocusVisible,
|
||||
isLabelPlaceholder,
|
||||
hasStartContent,
|
||||
isFocusVisible,
|
||||
],
|
||||
);
|
||||
|
||||
const getBaseProps: PropGetter = (props = {}) => {
|
||||
@ -112,7 +165,7 @@ export function useInput(originalProps: UseInputProps) {
|
||||
const getInputProps: PropGetter = (props = {}) => {
|
||||
return {
|
||||
ref: domRef,
|
||||
className: slots.input({class: styles?.input}),
|
||||
className: slots.input({class: clsx(styles?.input, !!inputValue ? "is-filled" : "")}),
|
||||
"data-focus-visible": dataAttr(isFocusVisible),
|
||||
"data-focused": dataAttr(isFocused),
|
||||
"data-invalid": dataAttr(isInvalid),
|
||||
@ -129,6 +182,15 @@ export function useInput(originalProps: UseInputProps) {
|
||||
};
|
||||
};
|
||||
|
||||
const getInnerWrapperProps: PropGetter = (props = {}) => {
|
||||
return {
|
||||
className: slots.innerWrapper({
|
||||
class: styles?.innerWrapper,
|
||||
}),
|
||||
...props,
|
||||
};
|
||||
};
|
||||
|
||||
const getDescriptionProps: PropGetter = (props = {}) => {
|
||||
return {
|
||||
className: slots.description({class: styles?.description}),
|
||||
@ -145,13 +207,25 @@ export function useInput(originalProps: UseInputProps) {
|
||||
};
|
||||
};
|
||||
|
||||
const getClearButtonProps: PropGetter = () => {
|
||||
return {
|
||||
role: "button",
|
||||
tabIndex: 0,
|
||||
className: slots.clearButton({class: styles?.clearButton}),
|
||||
...mergeProps(clearPressProps, clearFocusProps),
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
Component,
|
||||
styles,
|
||||
domRef,
|
||||
label,
|
||||
description,
|
||||
startContent,
|
||||
endContent,
|
||||
labelPosition,
|
||||
isClearable,
|
||||
shouldLabelBeOutside,
|
||||
shouldLabelBeInside,
|
||||
errorMessage,
|
||||
@ -159,8 +233,10 @@ export function useInput(originalProps: UseInputProps) {
|
||||
getLabelProps,
|
||||
getInputProps,
|
||||
getInputWrapperProps,
|
||||
getInnerWrapperProps,
|
||||
getDescriptionProps,
|
||||
getErrorMessageProps,
|
||||
getClearButtonProps,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import {ComponentStory, ComponentMeta} from "@storybook/react";
|
||||
import {input} from "@nextui-org/theme";
|
||||
import {MailFilledIcon} from "@nextui-org/shared-icons";
|
||||
|
||||
import {Input, InputProps} from "../src";
|
||||
|
||||
@ -44,6 +45,13 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center w-screen h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} as ComponentMeta<typeof Input>;
|
||||
|
||||
const defaultProps = {
|
||||
@ -52,9 +60,37 @@ const defaultProps = {
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof Input> = (args: InputProps) => (
|
||||
<div className="max-w-md flex flex-row gap-4">
|
||||
<div className="w-full max-w-[240px]">
|
||||
<Input {...args} />
|
||||
<Input {...args} placeholder="Enter your email" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const LabelPositionTemplate: ComponentStory<typeof Input> = (args: InputProps) => (
|
||||
<div className="w-full max-w-xl flex flex-row items-end gap-4">
|
||||
<Input {...args} />
|
||||
<Input {...args} labelPosition="outside" />
|
||||
<Input {...args} labelPosition="outside-left" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const StartContentTemplate: ComponentStory<typeof Input> = (args: InputProps) => (
|
||||
<div className="w-full max-w-xl flex flex-row items-end gap-4">
|
||||
<Input
|
||||
{...args}
|
||||
placeholder="you@example.com"
|
||||
startContent={<MailFilledIcon className="text-2xl pointer-events-none" />}
|
||||
/>
|
||||
<Input
|
||||
{...args}
|
||||
label="Price"
|
||||
placeholder="0.00"
|
||||
startContent={
|
||||
<div className="pointer-events-none flex items-center">
|
||||
<span className="text-gray-500 sm:text-sm">$</span>
|
||||
</div>
|
||||
}
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -69,6 +105,37 @@ WithPlaceholder.args = {
|
||||
placeholder: "Enter your email",
|
||||
};
|
||||
|
||||
export const Required = Template.bind({});
|
||||
Required.args = {
|
||||
...defaultProps,
|
||||
isRequired: true,
|
||||
};
|
||||
|
||||
export const LabelPosition = LabelPositionTemplate.bind({});
|
||||
LabelPosition.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const Clearable = Template.bind({});
|
||||
Clearable.args = {
|
||||
...defaultProps,
|
||||
variant: "bordered",
|
||||
placeholder: "Enter your email",
|
||||
defaultValue: "junior@nextui.org",
|
||||
onClear: () => console.log("input cleared"),
|
||||
};
|
||||
|
||||
export const StartContent = StartContentTemplate.bind({});
|
||||
StartContent.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const LabelPositionWithPlaceholder = LabelPositionTemplate.bind({});
|
||||
LabelPositionWithPlaceholder.args = {
|
||||
...defaultProps,
|
||||
placeholder: "Enter your email",
|
||||
};
|
||||
|
||||
export const WithErrorMessage = Template.bind({});
|
||||
WithErrorMessage.args = {
|
||||
...defaultProps,
|
||||
@ -79,6 +146,7 @@ export const InvalidValidationState = Template.bind({});
|
||||
InvalidValidationState.args = {
|
||||
...defaultProps,
|
||||
variant: "bordered",
|
||||
defaultValue: "invalid@email.com",
|
||||
validationState: "invalid",
|
||||
errorMessage: "Please enter a valid email address",
|
||||
};
|
||||
|
||||
@ -36,6 +36,7 @@ const chip = tv({
|
||||
"opacity-70",
|
||||
"hover:opacity-100",
|
||||
"cursor-pointer",
|
||||
"active:opacity-70",
|
||||
],
|
||||
},
|
||||
variants: {
|
||||
@ -119,10 +120,10 @@ const chip = tv({
|
||||
"3xl": {base: "rounded-3xl"},
|
||||
full: {base: "rounded-full"},
|
||||
},
|
||||
hasLeftContent: {
|
||||
hasStartContent: {
|
||||
true: {},
|
||||
},
|
||||
hasRightContent: {
|
||||
hasEndContent: {
|
||||
true: {},
|
||||
},
|
||||
isOneChar: {
|
||||
@ -442,31 +443,31 @@ const chip = tv({
|
||||
base: "w-8 h-8",
|
||||
},
|
||||
},
|
||||
// hasLeftContent / size
|
||||
// hasStartContent / size
|
||||
{
|
||||
hasLeftContent: true,
|
||||
hasStartContent: true,
|
||||
size: ["xs", "sm"],
|
||||
class: {
|
||||
content: "pl-0.5",
|
||||
},
|
||||
},
|
||||
{
|
||||
hasLeftContent: true,
|
||||
hasStartContent: true,
|
||||
size: ["md", "lg", "xl"],
|
||||
class: {
|
||||
content: "pl-1",
|
||||
},
|
||||
},
|
||||
// hasRightContent / size
|
||||
// hasEndContent / size
|
||||
{
|
||||
hasRightContent: true,
|
||||
hasEndContent: true,
|
||||
size: ["xs", "sm"],
|
||||
class: {
|
||||
content: "pr-0.5",
|
||||
},
|
||||
},
|
||||
{
|
||||
hasRightContent: true,
|
||||
hasEndContent: true,
|
||||
size: ["md", "lg", "xl"],
|
||||
class: {
|
||||
content: "pr-1",
|
||||
|
||||
@ -9,12 +9,13 @@ import {ringClasses} from "../utils";
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const {base, label, inputWrapper, input, description, helperText} = input({...})
|
||||
* const {base, label, inputWrapper, input, clearButton, description, helperText} = input({...})
|
||||
*
|
||||
* <div className={base())}>
|
||||
* <label className={label()}>Label</label>
|
||||
* <div className={inputWrapper()}>
|
||||
* <input className={input()}/>
|
||||
* <button className={clearButton()}>Clear</button>
|
||||
* </div>
|
||||
* <span className={description()}>Description</span>
|
||||
* <span className={helperText()}>Helper text</span>
|
||||
@ -25,8 +26,22 @@ const input = tv({
|
||||
slots: {
|
||||
base: "flex flex-col gap-2",
|
||||
label: "block text-sm font-medium text-neutral-600",
|
||||
inputWrapper: "w-full flex flex-row items-center shadow-sm px-3 gap-3",
|
||||
inputWrapper: "relative w-full inline-flex flex-row items-center shadow-sm px-3 gap-3",
|
||||
innerWrapper: "inline-flex items-center w-full gap-1.5",
|
||||
input: "w-full h-full bg-transparent outline-none placeholder:text-neutral-500",
|
||||
clearButton: [
|
||||
"z-10",
|
||||
"absolute",
|
||||
"right-3",
|
||||
"appearance-none",
|
||||
"outline-none",
|
||||
"select-none",
|
||||
"opacity-0",
|
||||
"hover:opacity-100",
|
||||
"cursor-pointer",
|
||||
"active:opacity-70",
|
||||
"rounded-full",
|
||||
],
|
||||
description: "text-xs text-neutral-500",
|
||||
errorMessage: "text-xs text-danger",
|
||||
},
|
||||
@ -97,23 +112,28 @@ const input = tv({
|
||||
label: "text-xs",
|
||||
inputWrapper: "h-6 px-1",
|
||||
input: "text-xs",
|
||||
clearButton: "right-2",
|
||||
},
|
||||
sm: {
|
||||
label: "text-xs",
|
||||
inputWrapper: "h-8 px-2",
|
||||
input: "text-xs",
|
||||
clearButton: "text-lg",
|
||||
},
|
||||
md: {
|
||||
inputWrapper: "h-10",
|
||||
input: "text-sm",
|
||||
clearButton: "text-xl",
|
||||
},
|
||||
lg: {
|
||||
inputWrapper: "h-12",
|
||||
input: "text-base",
|
||||
clearButton: "text-xl",
|
||||
},
|
||||
xl: {
|
||||
inputWrapper: "h-14",
|
||||
input: "text-md",
|
||||
clearButton: "text-2xl",
|
||||
},
|
||||
},
|
||||
radius: {
|
||||
@ -156,7 +176,13 @@ const input = tv({
|
||||
},
|
||||
isLabelPlaceholder: {
|
||||
true: {
|
||||
label: "absolute",
|
||||
label: "absolute z-10 pointer-events-none",
|
||||
},
|
||||
},
|
||||
isClearable: {
|
||||
true: {
|
||||
input: "peer",
|
||||
clearButton: "peer-[.is-filled]:opacity-70",
|
||||
},
|
||||
},
|
||||
isDisabled: {
|
||||
@ -167,25 +193,42 @@ const input = tv({
|
||||
isFocusVisible: {
|
||||
true: {},
|
||||
},
|
||||
isClearButtonFocusVisible: {
|
||||
true: {
|
||||
clearButton: [...ringClasses],
|
||||
},
|
||||
},
|
||||
isInvalid: {
|
||||
true: {
|
||||
label: "text-danger",
|
||||
input: "placeholder:text-danger",
|
||||
},
|
||||
},
|
||||
isRequired: {
|
||||
true: {
|
||||
label: "after:content-['*'] after:text-danger after:ml-0.5",
|
||||
},
|
||||
},
|
||||
disableAnimation: {
|
||||
true: {
|
||||
inputWrapper: "transition-none",
|
||||
label: "transition-none",
|
||||
},
|
||||
false: {
|
||||
inputWrapper: "transition-background motion-reduce:transition-none",
|
||||
inputWrapper: "transition-background motion-reduce:transition-none !duration-150",
|
||||
label: [
|
||||
"will-change-auto",
|
||||
"transition-all",
|
||||
"!duration-200",
|
||||
"!ease-[cubic-bezier(0,0,0.2,1)]",
|
||||
"motion-reduce:transition-none",
|
||||
],
|
||||
clearButton: [
|
||||
"transition-transform-opacity",
|
||||
"motion-reduce:transition-none",
|
||||
"translate-x-1/2",
|
||||
"peer-[.is-filled]:translate-x-0",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -195,7 +238,7 @@ const input = tv({
|
||||
size: "md",
|
||||
radius: "xl",
|
||||
fullWidth: true,
|
||||
labelPosition: "outside",
|
||||
labelPosition: "inside",
|
||||
isDisabled: false,
|
||||
disableAnimation: false,
|
||||
},
|
||||
@ -513,9 +556,8 @@ const input = tv({
|
||||
// !isLabelPlaceholder & labelPosition
|
||||
{
|
||||
isLabelPlaceholder: true,
|
||||
labelPosition: "inside",
|
||||
labelPosition: ["inside", "outside"],
|
||||
class: {
|
||||
inputWrapper: "group",
|
||||
label: [
|
||||
"font-normal",
|
||||
"text-neutral-500",
|
||||
@ -526,7 +568,22 @@ const input = tv({
|
||||
],
|
||||
},
|
||||
},
|
||||
// isLabelPlaceholder & labelPosition & size
|
||||
{
|
||||
isLabelPlaceholder: true,
|
||||
labelPosition: "inside",
|
||||
class: {
|
||||
inputWrapper: "group",
|
||||
},
|
||||
},
|
||||
{
|
||||
isLabelPlaceholder: true,
|
||||
labelPosition: "outside",
|
||||
class: {
|
||||
base: "group relative justify-end",
|
||||
label: ["group-focus-within:left-0", "group-[.is-filled]:left-0"],
|
||||
},
|
||||
},
|
||||
// isLabelPlaceholder & inside & size
|
||||
{
|
||||
isLabelPlaceholder: true,
|
||||
labelPosition: "inside",
|
||||
@ -599,6 +656,81 @@ const input = tv({
|
||||
input: "pt-8",
|
||||
},
|
||||
},
|
||||
// isLabelPlaceholder & outside & size
|
||||
{
|
||||
isLabelPlaceholder: true,
|
||||
labelPosition: "outside",
|
||||
size: "xs",
|
||||
class: {
|
||||
label: [
|
||||
"text-xs",
|
||||
"bottom-1",
|
||||
"left-1",
|
||||
"group-focus-within:bottom-8",
|
||||
"group-[.is-filled]:bottom-8",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
isLabelPlaceholder: true,
|
||||
labelPosition: "outside",
|
||||
size: "sm",
|
||||
class: {
|
||||
label: [
|
||||
"text-xs",
|
||||
"bottom-2",
|
||||
"left-2",
|
||||
"group-focus-within:bottom-10",
|
||||
"group-[.is-filled]:bottom-10",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
isLabelPlaceholder: true,
|
||||
labelPosition: "outside",
|
||||
size: "md",
|
||||
class: {
|
||||
label: [
|
||||
"text-sm",
|
||||
"bottom-2.5",
|
||||
"left-3",
|
||||
"group-focus-within:bottom-12",
|
||||
"group-[.is-filled]:bottom-12",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
isLabelPlaceholder: true,
|
||||
labelPosition: "outside",
|
||||
size: "lg",
|
||||
class: {
|
||||
label: [
|
||||
"text-base",
|
||||
"bottom-3",
|
||||
"left-3",
|
||||
"group-focus-within:text-sm",
|
||||
"group-[.is-filled]:bottom-sm",
|
||||
"group-focus-within:bottom-14",
|
||||
"group-[.is-filled]:bottom-14",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
isLabelPlaceholder: true,
|
||||
labelPosition: "outside",
|
||||
size: "xl",
|
||||
class: {
|
||||
label: [
|
||||
"text-base",
|
||||
"bottom-4",
|
||||
"left-3",
|
||||
"group-focus-within:text-sm",
|
||||
"group-[.is-filled]:bottom-sm",
|
||||
"group-focus-within:bottom-16",
|
||||
"group-[.is-filled]:bottom-16",
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ export * from "./forward";
|
||||
export * from "./sun";
|
||||
export * from "./sun-filled";
|
||||
export * from "./mail";
|
||||
export * from "./mail-filled";
|
||||
export * from "./moon";
|
||||
export * from "./moon-filled";
|
||||
export * from "./headphones";
|
||||
|
||||
19
packages/utilities/shared-icons/src/mail-filled.tsx
Normal file
19
packages/utilities/shared-icons/src/mail-filled.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import {IconSvgProps} from "./types";
|
||||
|
||||
export const MailFilledIcon = (props: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M17 3.5H7C4 3.5 2 5 2 8.5V15.5C2 19 4 20.5 7 20.5H17C20 20.5 22 19 22 15.5V8.5C22 5 20 3.5 17 3.5ZM17.47 9.59L14.34 12.09C13.68 12.62 12.84 12.88 12 12.88C11.16 12.88 10.31 12.62 9.66 12.09L6.53 9.59C6.21 9.33 6.16 8.85 6.41 8.53C6.67 8.21 7.14 8.15 7.46 8.41L10.59 10.91C11.35 11.52 12.64 11.52 13.4 10.91L16.53 8.41C16.85 8.15 17.33 8.2 17.58 8.53C17.84 8.85 17.79 9.33 17.47 9.59Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@ -822,6 +822,9 @@ importers:
|
||||
'@nextui-org/dom-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/dom-utils
|
||||
'@nextui-org/shared-icons':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/shared-icons
|
||||
'@nextui-org/shared-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/shared-utils
|
||||
@ -837,6 +840,12 @@ importers:
|
||||
'@react-aria/focus':
|
||||
specifier: ^3.11.0
|
||||
version: 3.11.0(react@18.2.0)
|
||||
'@react-aria/i18n':
|
||||
specifier: ^3.7.0
|
||||
version: 3.7.0(react@18.2.0)
|
||||
'@react-aria/interactions':
|
||||
specifier: ^3.14.0
|
||||
version: 3.14.0(react@18.2.0)
|
||||
'@react-aria/utils':
|
||||
specifier: ^3.15.0
|
||||
version: 3.15.0(react@18.2.0)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user