mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(switch): component created
This commit is contained in:
parent
71752ad04b
commit
a3ca1e8ed8
@ -33,27 +33,17 @@ export default {
|
|||||||
},
|
},
|
||||||
} as ComponentMeta<typeof AvatarGroup>;
|
} as ComponentMeta<typeof AvatarGroup>;
|
||||||
|
|
||||||
const pics = [
|
|
||||||
"https://i.pravatar.cc/300?u=a042581f4e29026705d",
|
|
||||||
"https://i.pravatar.cc/300?u=a042581f4e29026706d",
|
|
||||||
"https://i.pravatar.cc/300?u=a042581f4e29026707d",
|
|
||||||
"https://i.pravatar.cc/300?u=a042581f4e29026709d",
|
|
||||||
"https://i.pravatar.cc/300?u=a042581f4f29026709d",
|
|
||||||
"https://i.pravatar.cc/300?u=a042581f4e29026710d",
|
|
||||||
"https://i.pravatar.cc/300?u=a042581f4e29026711d",
|
|
||||||
];
|
|
||||||
|
|
||||||
const Template: ComponentStory<typeof AvatarGroup> = (args: AvatarGroupProps) => (
|
const Template: ComponentStory<typeof AvatarGroup> = (args: AvatarGroupProps) => (
|
||||||
<AvatarGroup {...args}>
|
<AvatarGroup {...args}>
|
||||||
<Avatar src={pics[0]} />
|
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026705d" />
|
||||||
<Avatar src={pics[1]} />
|
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026706d" />
|
||||||
<Avatar src={pics[2]} />
|
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026707d" />
|
||||||
<Avatar src={pics[3]} />
|
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026709d" />
|
||||||
<Avatar src={pics[4]} />
|
<Avatar src="https://i.pravatar.cc/300?u=a042581f4f29026709d" />
|
||||||
<Avatar src={pics[5]} />
|
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026710d" />
|
||||||
<Avatar src={pics[2]} />
|
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026711d" />
|
||||||
<Avatar src={pics[3]} />
|
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026712d" />
|
||||||
<Avatar src={pics[6]} />
|
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026713d" />
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type {CheckboxVariantProps, CheckboxSlots, SlotsToClasses} from "@nextui-
|
|||||||
import type {AriaCheckboxProps} from "@react-types/checkbox";
|
import type {AriaCheckboxProps} from "@react-types/checkbox";
|
||||||
import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
|
import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
|
||||||
|
|
||||||
import {ReactNode, Ref, useCallback} from "react";
|
import {ReactNode, Ref, useCallback, useId} from "react";
|
||||||
import {useMemo, useRef} from "react";
|
import {useMemo, useRef} from "react";
|
||||||
import {useToggleState} from "@react-stately/toggle";
|
import {useToggleState} from "@react-stately/toggle";
|
||||||
import {checkbox} from "@nextui-org/theme";
|
import {checkbox} from "@nextui-org/theme";
|
||||||
@ -79,6 +79,7 @@ export function useCheckbox(props: UseCheckboxProps) {
|
|||||||
name,
|
name,
|
||||||
isRequired = false,
|
isRequired = false,
|
||||||
isReadOnly = false,
|
isReadOnly = false,
|
||||||
|
autoFocus = false,
|
||||||
isSelected: isSelectedProp,
|
isSelected: isSelectedProp,
|
||||||
size = groupContext?.size ?? "md",
|
size = groupContext?.size ?? "md",
|
||||||
color = groupContext?.color ?? "primary",
|
color = groupContext?.color ?? "primary",
|
||||||
@ -115,11 +116,19 @@ export function useCheckbox(props: UseCheckboxProps) {
|
|||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
const domRef = useFocusableRef(ref as FocusableRef<HTMLLabelElement>, inputRef);
|
const domRef = useFocusableRef(ref as FocusableRef<HTMLLabelElement>, inputRef);
|
||||||
|
|
||||||
|
const labelId = useId();
|
||||||
|
const inputId = useId();
|
||||||
|
|
||||||
const ariaCheckboxProps = useMemo(() => {
|
const ariaCheckboxProps = useMemo(() => {
|
||||||
|
const ariaLabel =
|
||||||
|
otherProps["aria-label"] || typeof children === "string" ? (children as string) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
id: inputId,
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
children,
|
children,
|
||||||
|
autoFocus,
|
||||||
defaultSelected,
|
defaultSelected,
|
||||||
isSelected: isSelectedProp,
|
isSelected: isSelectedProp,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
@ -127,14 +136,17 @@ export function useCheckbox(props: UseCheckboxProps) {
|
|||||||
isRequired,
|
isRequired,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
validationState,
|
validationState,
|
||||||
"aria-label": otherProps["aria-label"],
|
"aria-label": ariaLabel,
|
||||||
"aria-labelledby": otherProps["aria-labelledby"],
|
"aria-labelledby": otherProps["aria-labelledby"] || labelId,
|
||||||
onChange,
|
onChange,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
value,
|
value,
|
||||||
name,
|
name,
|
||||||
|
inputId,
|
||||||
|
labelId,
|
||||||
children,
|
children,
|
||||||
|
autoFocus,
|
||||||
isIndeterminate,
|
isIndeterminate,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isSelectedProp,
|
isSelectedProp,
|
||||||
@ -223,6 +235,8 @@ export function useCheckbox(props: UseCheckboxProps) {
|
|||||||
|
|
||||||
const getLabelProps: PropGetter = useCallback(
|
const getLabelProps: PropGetter = useCallback(
|
||||||
() => ({
|
() => ({
|
||||||
|
id: labelId,
|
||||||
|
htmlFor: inputProps.id || inputId,
|
||||||
"data-disabled": dataAttr(isDisabled),
|
"data-disabled": dataAttr(isDisabled),
|
||||||
"data-checked": dataAttr(isSelected),
|
"data-checked": dataAttr(isSelected),
|
||||||
"data-invalid": dataAttr(isInvalid),
|
"data-invalid": dataAttr(isInvalid),
|
||||||
|
|||||||
@ -58,6 +58,24 @@ const defaultProps: CheckboxProps = {
|
|||||||
|
|
||||||
const Template: ComponentStory<typeof Checkbox> = (args: CheckboxProps) => <Checkbox {...args} />;
|
const Template: ComponentStory<typeof Checkbox> = (args: CheckboxProps) => <Checkbox {...args} />;
|
||||||
|
|
||||||
|
const ControlledTemplate: ComponentStory<typeof Checkbox> = (args: CheckboxProps) => {
|
||||||
|
const [selected, setSelected] = React.useState<boolean>(true);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("Checkbox ", selected);
|
||||||
|
}, [selected]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Checkbox isSelected={selected} onChange={setSelected} {...args}>
|
||||||
|
Subscribe (controlled)
|
||||||
|
</Checkbox>
|
||||||
|
<p className="text-neutral-500">Selected: {selected ? "true" : "false"}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const Default = Template.bind({});
|
export const Default = Template.bind({});
|
||||||
Default.args = {
|
Default.args = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
@ -111,21 +129,9 @@ DisableAnimation.args = {
|
|||||||
disableAnimation: true,
|
disableAnimation: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Controlled = () => {
|
export const Controlled = ControlledTemplate.bind({});
|
||||||
const [selected, setSelected] = React.useState<boolean>(true);
|
Controlled.args = {
|
||||||
|
...defaultProps,
|
||||||
React.useEffect(() => {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log("Checkbox ", selected);
|
|
||||||
}, [selected]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<Checkbox isSelected={selected} onChange={setSelected} {...checkbox.defaultVariants}>
|
|
||||||
Subscribe (controlled)
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CustomCheckboxProps extends CheckboxProps {
|
interface CustomCheckboxProps extends CheckboxProps {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type {AriaRadioProps} from "@react-types/radio";
|
import type {AriaRadioProps} from "@react-types/radio";
|
||||||
import type {RadioVariantProps, RadioSlots, SlotsToClasses} from "@nextui-org/theme";
|
import type {RadioVariantProps, RadioSlots, SlotsToClasses} from "@nextui-org/theme";
|
||||||
|
|
||||||
import {Ref, ReactNode, useCallback} from "react";
|
import {Ref, ReactNode, useCallback, useId} from "react";
|
||||||
import {useMemo, useRef} from "react";
|
import {useMemo, useRef} from "react";
|
||||||
import {useFocusRing} from "@react-aria/focus";
|
import {useFocusRing} from "@react-aria/focus";
|
||||||
import {useHover} from "@react-aria/interactions";
|
import {useHover} from "@react-aria/interactions";
|
||||||
@ -85,6 +85,11 @@ export function useRadio(props: UseRadioProps) {
|
|||||||
const domRef = useDOMRef(ref);
|
const domRef = useDOMRef(ref);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const labelId = useId();
|
||||||
|
const inputGenId = useId();
|
||||||
|
|
||||||
|
const inputId = id || inputGenId;
|
||||||
|
|
||||||
const isDisabled = useMemo(() => !!isDisabledProp, [isDisabledProp]);
|
const isDisabled = useMemo(() => !!isDisabledProp, [isDisabledProp]);
|
||||||
const isRequired = useMemo(() => groupContext.isRequired ?? false, [groupContext.isRequired]);
|
const isRequired = useMemo(() => groupContext.isRequired ?? false, [groupContext.isRequired]);
|
||||||
const isInvalid = useMemo(
|
const isInvalid = useMemo(
|
||||||
@ -101,17 +106,17 @@ export function useRadio(props: UseRadioProps) {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
id: inputId,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isRequired,
|
isRequired,
|
||||||
"aria-label": ariaLabel,
|
"aria-label": ariaLabel,
|
||||||
"aria-labelledby": otherProps["aria-labelledby"] || ariaLabel,
|
"aria-labelledby": otherProps["aria-labelledby"] || labelId,
|
||||||
"aria-describedby": otherProps["aria-describedby"] || ariaDescribedBy,
|
"aria-describedby": ariaDescribedBy,
|
||||||
};
|
};
|
||||||
}, [isDisabled, isRequired]);
|
}, [labelId, inputId, isDisabled, isRequired]);
|
||||||
|
|
||||||
const {inputProps} = useReactAriaRadio(
|
const {inputProps} = useReactAriaRadio(
|
||||||
{
|
{
|
||||||
id,
|
|
||||||
value,
|
value,
|
||||||
children,
|
children,
|
||||||
...groupContext,
|
...groupContext,
|
||||||
@ -190,6 +195,8 @@ export function useRadio(props: UseRadioProps) {
|
|||||||
const getLabelProps: PropGetter = useCallback(
|
const getLabelProps: PropGetter = useCallback(
|
||||||
(props = {}) => ({
|
(props = {}) => ({
|
||||||
...props,
|
...props,
|
||||||
|
id: labelId,
|
||||||
|
htmlFor: inputId,
|
||||||
"data-disabled": dataAttr(isDisabled),
|
"data-disabled": dataAttr(isDisabled),
|
||||||
"data-checked": dataAttr(isSelected),
|
"data-checked": dataAttr(isSelected),
|
||||||
"data-invalid": dataAttr(isInvalid),
|
"data-invalid": dataAttr(isInvalid),
|
||||||
|
|||||||
@ -107,6 +107,27 @@ const Template: ComponentStory<typeof RadioGroup> = (args: RadioGroupProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ControlledTemplate: ComponentStory<typeof RadioGroup> = (args: RadioGroupProps) => {
|
||||||
|
const [selectedItem, setSelectedItem] = React.useState<string>("london");
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("isSelected:", selectedItem);
|
||||||
|
}, [selectedItem]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<RadioGroup label="Select city" value={selectedItem} onChange={setSelectedItem} {...args}>
|
||||||
|
<Radio value="buenos-aires">Buenos Aires</Radio>
|
||||||
|
<Radio value="sydney">Sydney</Radio>
|
||||||
|
<Radio value="london">London</Radio>
|
||||||
|
<Radio value="tokyo">Tokyo</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
<p className="text-neutral-500">Selected: {selectedItem}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const Default = Template.bind({});
|
export const Default = Template.bind({});
|
||||||
Default.args = {
|
Default.args = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
@ -156,22 +177,9 @@ DisableAnimation.args = {
|
|||||||
disableAnimation: true,
|
disableAnimation: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Controlled = () => {
|
export const Controlled = ControlledTemplate.bind({});
|
||||||
const [isSelected, setIsSelected] = React.useState<string>("london");
|
Controlled.args = {
|
||||||
|
...defaultProps,
|
||||||
React.useEffect(() => {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log("isSelected:", isSelected);
|
|
||||||
}, [isSelected]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RadioGroup label="Select city" value={isSelected} onChange={setIsSelected}>
|
|
||||||
<Radio value="buenos-aires">Buenos Aires</Radio>
|
|
||||||
<Radio value="sydney">Sydney</Radio>
|
|
||||||
<Radio value="london">London</Radio>
|
|
||||||
<Radio value="tokyo">Tokyo</Radio>
|
|
||||||
</RadioGroup>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomRadio = (props: RadioProps) => {
|
const CustomRadio = (props: RadioProps) => {
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {render} from "@testing-library/react";
|
import {act, render} from "@testing-library/react";
|
||||||
|
|
||||||
import {Switch} from "../src";
|
import {Switch} from "../src";
|
||||||
|
|
||||||
describe("Switch", () => {
|
describe("Switch", () => {
|
||||||
it("should render correctly", () => {
|
it("should render correctly", () => {
|
||||||
const wrapper = render(<Switch />);
|
const wrapper = render(<Switch aria-label="switch" />);
|
||||||
|
|
||||||
expect(() => wrapper.unmount()).not.toThrow();
|
expect(() => wrapper.unmount()).not.toThrow();
|
||||||
});
|
});
|
||||||
@ -13,7 +13,188 @@ describe("Switch", () => {
|
|||||||
it("ref should be forwarded", () => {
|
it("ref should be forwarded", () => {
|
||||||
const ref = React.createRef<HTMLDivElement>();
|
const ref = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
render(<Switch ref={ref} />);
|
render(<Switch ref={ref} aria-label="switch" />);
|
||||||
expect(ref.current).not.toBeNull();
|
expect(ref.current).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should check and uncheck", () => {
|
||||||
|
const {getByRole} = render(<Switch aria-label="switch" />);
|
||||||
|
|
||||||
|
const checkbox = getByRole("switch");
|
||||||
|
|
||||||
|
expect(checkbox).not.toBeChecked();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
checkbox.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(checkbox).toBeChecked();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
checkbox.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(checkbox).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not check if disabled", () => {
|
||||||
|
const {getByRole} = render(<Switch isDisabled aria-label="switch" />);
|
||||||
|
|
||||||
|
const checkbox = getByRole("switch");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
checkbox.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(checkbox).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be checked if defaultSelected", () => {
|
||||||
|
const {getByRole} = render(<Switch defaultSelected aria-label="switch" />);
|
||||||
|
|
||||||
|
const checkbox = getByRole("switch");
|
||||||
|
|
||||||
|
expect(checkbox).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not check if readOnly", () => {
|
||||||
|
const {getByRole} = render(<Switch isReadOnly aria-label="switch" />);
|
||||||
|
|
||||||
|
const checkbox = getByRole("switch");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
checkbox.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(checkbox).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should check and uncheck with controlled state", () => {
|
||||||
|
const ControlledSwitch = ({onChange}: any) => {
|
||||||
|
const [isSelected, setIsSelected] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
aria-label="switch"
|
||||||
|
isSelected={isSelected}
|
||||||
|
onChange={(selected) => {
|
||||||
|
onChange?.(selected);
|
||||||
|
setIsSelected(selected);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChange = jest.fn();
|
||||||
|
|
||||||
|
const {getByRole} = render(<ControlledSwitch onChange={onChange} />);
|
||||||
|
|
||||||
|
const checkbox = getByRole("switch");
|
||||||
|
|
||||||
|
expect(checkbox).not.toBeChecked();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
checkbox.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(checkbox).toBeChecked();
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the thumbIcon", () => {
|
||||||
|
const wrapper = render(
|
||||||
|
<Switch aria-label="switch" thumbIcon={<svg data-testid="thumb-icon" />} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.getByTestId("thumb-icon")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with thumbIcon as "function"', () => {
|
||||||
|
const thumbIcon = jest.fn(() => <svg data-testid="thumb-icon" />);
|
||||||
|
|
||||||
|
const wrapper = render(<Switch aria-label="switch" thumbIcon={thumbIcon} />);
|
||||||
|
|
||||||
|
expect(thumbIcon).toHaveBeenCalled();
|
||||||
|
expect(wrapper.getByTestId("thumb-icon")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should change the thumbIcon when clicked", () => {
|
||||||
|
const thumbIcon = jest.fn((props) => {
|
||||||
|
const {isSelected} = props;
|
||||||
|
|
||||||
|
return isSelected ? (
|
||||||
|
<svg data-testid="checked-thumb-icon" />
|
||||||
|
) : (
|
||||||
|
<svg data-testid="unchecked-thumb-icon" />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const {getByRole, container} = render(<Switch aria-label="switch" thumbIcon={thumbIcon} />);
|
||||||
|
|
||||||
|
const checkbox = getByRole("switch");
|
||||||
|
|
||||||
|
expect(checkbox).not.toBeChecked();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
checkbox.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(checkbox).toBeChecked();
|
||||||
|
|
||||||
|
expect(thumbIcon).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
isSelected: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkedThumbIcon = container.querySelector("[data-testid=checked-thumb-icon]");
|
||||||
|
|
||||||
|
expect(checkedThumbIcon).toBeInTheDocument();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
checkbox.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(checkbox).not.toBeChecked();
|
||||||
|
|
||||||
|
expect(thumbIcon).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
isSelected: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const uncheckedThumbIcon = container.querySelector("[data-testid=unchecked-thumb-icon]");
|
||||||
|
|
||||||
|
expect(uncheckedThumbIcon).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with "leftIcon"', () => {
|
||||||
|
const wrapper = render(
|
||||||
|
<Switch aria-label="switch" leftIcon={<svg data-testid="left-icon" />} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.getByTestId("left-icon")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with "rightIcon"', () => {
|
||||||
|
const wrapper = render(
|
||||||
|
<Switch aria-label="switch" rightIcon={<svg data-testid="right-icon" />} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.getByTestId("right-icon")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with "leftIcon" and "rightIcon"', () => {
|
||||||
|
const wrapper = render(
|
||||||
|
<Switch
|
||||||
|
aria-label="switch"
|
||||||
|
leftIcon={<svg data-testid="left-icon" />}
|
||||||
|
rightIcon={<svg data-testid="right-icon" />}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.getByTestId("left-icon")).toBeInTheDocument();
|
||||||
|
expect(wrapper.getByTestId("right-icon")).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -37,14 +37,22 @@
|
|||||||
"react": ">=18"
|
"react": ">=18"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nextui-org/dom-utils": "workspace:*",
|
||||||
|
"@nextui-org/shared-utils": "workspace:*",
|
||||||
"@nextui-org/system": "workspace:*",
|
"@nextui-org/system": "workspace:*",
|
||||||
"@nextui-org/theme": "workspace:*",
|
"@nextui-org/theme": "workspace:*",
|
||||||
"@nextui-org/shared-utils": "workspace:*",
|
"@react-aria/focus": "^3.11.0",
|
||||||
"@nextui-org/dom-utils": "workspace:*"
|
"@react-aria/interactions": "^3.14.0",
|
||||||
|
"@react-aria/switch": "^3.4.0",
|
||||||
|
"@react-aria/utils": "^3.15.0",
|
||||||
|
"@react-aria/visually-hidden": "^3.7.0",
|
||||||
|
"@react-stately/toggle": "^3.5.0",
|
||||||
|
"@react-types/shared": "^3.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"clean-package": "2.2.0",
|
"clean-package": "2.2.0",
|
||||||
"react": "^18.0.0"
|
"react": "^18.0.0",
|
||||||
|
"@nextui-org/shared-icons": "workspace:*"
|
||||||
},
|
},
|
||||||
"tsup": {
|
"tsup": {
|
||||||
"clean": true,
|
"clean": true,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import Switch from "./switch";
|
|||||||
|
|
||||||
// export types
|
// export types
|
||||||
export type {SwitchProps} from "./switch";
|
export type {SwitchProps} from "./switch";
|
||||||
|
export type {SwitchThumbIconProps} from "./use-switch";
|
||||||
|
|
||||||
// export hooks
|
// export hooks
|
||||||
export {useSwitch} from "./use-switch";
|
export {useSwitch} from "./use-switch";
|
||||||
|
|||||||
@ -1,22 +1,51 @@
|
|||||||
import {forwardRef} from "@nextui-org/system";
|
import {forwardRef} from "@nextui-org/system";
|
||||||
import {__DEV__} from "@nextui-org/shared-utils";
|
import {VisuallyHidden} from "@react-aria/visually-hidden";
|
||||||
|
import {cloneElement, ReactElement} from "react";
|
||||||
|
|
||||||
import {UseSwitchProps, useSwitch} from "./use-switch";
|
import {UseSwitchProps, useSwitch} from "./use-switch";
|
||||||
|
|
||||||
export interface SwitchProps extends Omit<UseSwitchProps, "ref"> {}
|
export interface SwitchProps extends Omit<UseSwitchProps, "ref"> {}
|
||||||
|
|
||||||
const Switch = forwardRef<SwitchProps, "div">((props, ref) => {
|
const Switch = forwardRef<SwitchProps, "div">((props, ref) => {
|
||||||
const {Component, domRef, children, styles, ...otherProps} = useSwitch({ref, ...props});
|
const {
|
||||||
|
Component,
|
||||||
|
children,
|
||||||
|
leftIcon,
|
||||||
|
rightIcon,
|
||||||
|
thumbIcon,
|
||||||
|
getBaseProps,
|
||||||
|
getInputProps,
|
||||||
|
getWrapperProps,
|
||||||
|
getThumbProps,
|
||||||
|
getThumbIconProps,
|
||||||
|
getLabelProps,
|
||||||
|
getLeftIconProps,
|
||||||
|
getRightIconProps,
|
||||||
|
} = useSwitch({ref, ...props});
|
||||||
|
|
||||||
|
const clonedThumbIcon =
|
||||||
|
typeof thumbIcon === "function"
|
||||||
|
? thumbIcon(getThumbIconProps({includeStateProps: true}))
|
||||||
|
: thumbIcon && cloneElement(thumbIcon as ReactElement, getThumbIconProps());
|
||||||
|
|
||||||
|
const clonedLeftIcon = leftIcon && cloneElement(leftIcon as ReactElement, getLeftIconProps());
|
||||||
|
const clonedRightIcon = rightIcon && cloneElement(rightIcon as ReactElement, getRightIconProps());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component ref={domRef} className={styles} {...otherProps}>
|
<Component {...getBaseProps()}>
|
||||||
{children}
|
<VisuallyHidden>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
</VisuallyHidden>
|
||||||
|
<span {...getWrapperProps()}>
|
||||||
|
{leftIcon && clonedLeftIcon}
|
||||||
|
<span {...getThumbProps()}>{thumbIcon && clonedThumbIcon}</span>
|
||||||
|
{rightIcon && clonedRightIcon}
|
||||||
|
</span>
|
||||||
|
{children && <span {...getLabelProps()}>{children}</span>}
|
||||||
</Component>
|
</Component>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (__DEV__) {
|
Switch.displayName = "NextUI.Switch";
|
||||||
Switch.displayName = "NextUI.Switch";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Switch;
|
export default Switch;
|
||||||
|
|||||||
@ -1,37 +1,285 @@
|
|||||||
import type {ToggleVariantProps} from "@nextui-org/theme";
|
import type {ToggleVariantProps, ToggleSlots, SlotsToClasses} from "@nextui-org/theme";
|
||||||
|
import type {FocusableRef} from "@react-types/shared";
|
||||||
|
import type {AriaSwitchProps} from "@react-aria/switch";
|
||||||
|
import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
|
||||||
|
|
||||||
import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system";
|
import {ReactNode, Ref, useCallback, useId, useRef} from "react";
|
||||||
|
import {mapPropsVariants} from "@nextui-org/system";
|
||||||
|
import {useHover} from "@react-aria/interactions";
|
||||||
import {toggle} from "@nextui-org/theme";
|
import {toggle} from "@nextui-org/theme";
|
||||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
import {mergeProps} from "@react-aria/utils";
|
||||||
import {ReactRef} from "@nextui-org/shared-utils";
|
import {clsx, dataAttr} from "@nextui-org/shared-utils";
|
||||||
|
import {useFocusableRef} from "@nextui-org/dom-utils";
|
||||||
|
import {useSwitch as useReactAriaSwitch} from "@react-aria/switch";
|
||||||
import {useMemo} from "react";
|
import {useMemo} from "react";
|
||||||
|
import {useToggleState} from "@react-stately/toggle";
|
||||||
|
import {useFocusRing} from "@react-aria/focus";
|
||||||
|
|
||||||
export interface UseSwitchProps extends HTMLNextUIProps<"div">, ToggleVariantProps {
|
export type SwitchThumbIconProps = {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
"data-checked": string;
|
||||||
|
isSelected: boolean;
|
||||||
|
className: string;
|
||||||
|
};
|
||||||
|
interface Props extends HTMLNextUIProps<"label"> {
|
||||||
/**
|
/**
|
||||||
* Ref to the DOM node.
|
* Ref to the DOM node.
|
||||||
*/
|
*/
|
||||||
ref?: ReactRef<HTMLElement | null>;
|
ref?: Ref<HTMLElement>;
|
||||||
|
/**
|
||||||
|
* The label of the switch.
|
||||||
|
*/
|
||||||
|
children?: ReactNode;
|
||||||
|
/**
|
||||||
|
* Whether the switch is disabled.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
isDisabled?: boolean;
|
||||||
|
/**
|
||||||
|
* The icon to be displayed inside the thumb.
|
||||||
|
*/
|
||||||
|
thumbIcon?: ReactNode | ((props: SwitchThumbIconProps) => ReactNode);
|
||||||
|
/**
|
||||||
|
* Left icon to be displayed inside the switch.
|
||||||
|
*/
|
||||||
|
leftIcon?: ReactNode;
|
||||||
|
/**
|
||||||
|
* Right icon to be displayed inside the switch.
|
||||||
|
*/
|
||||||
|
rightIcon?: 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.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* <Switch styles={{
|
||||||
|
* base:"base-classes",
|
||||||
|
* wrapper: "wrapper-classes",
|
||||||
|
* thumb: "thumb-classes",
|
||||||
|
* thumbIcon: "thumbIcon-classes",
|
||||||
|
* label: "label-classes",
|
||||||
|
* }} />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
styles?: SlotsToClasses<ToggleSlots>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UseSwitchProps = Omit<Props, "defaultChecked" | "onChange"> &
|
||||||
|
Omit<AriaSwitchProps, keyof ToggleVariantProps> &
|
||||||
|
ToggleVariantProps;
|
||||||
|
|
||||||
export function useSwitch(originalProps: UseSwitchProps) {
|
export function useSwitch(originalProps: UseSwitchProps) {
|
||||||
const [props, variantProps] = mapPropsVariants(originalProps, toggle.variantKeys);
|
const [props, variantProps] = mapPropsVariants(originalProps, toggle.variantKeys);
|
||||||
|
|
||||||
const {ref, as, className, ...otherProps} = props;
|
const {
|
||||||
|
ref,
|
||||||
|
as,
|
||||||
|
name,
|
||||||
|
value = "",
|
||||||
|
isReadOnly = false,
|
||||||
|
autoFocus = false,
|
||||||
|
leftIcon,
|
||||||
|
rightIcon,
|
||||||
|
defaultSelected,
|
||||||
|
isSelected: isSelectedProp,
|
||||||
|
children,
|
||||||
|
thumbIcon,
|
||||||
|
className,
|
||||||
|
styles,
|
||||||
|
onChange,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
const Component = as || "div";
|
const Component = as || "label";
|
||||||
|
|
||||||
const domRef = useDOMRef(ref);
|
const inputRef = useRef(null);
|
||||||
|
const domRef = useFocusableRef(ref as FocusableRef<HTMLLabelElement>, inputRef);
|
||||||
|
|
||||||
const styles = useMemo(
|
const labelId = useId();
|
||||||
|
const inputId = useId();
|
||||||
|
|
||||||
|
const ariaSwitchProps = useMemo(() => {
|
||||||
|
const ariaLabel =
|
||||||
|
otherProps["aria-label"] || typeof children === "string" ? (children as string) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
children,
|
||||||
|
autoFocus,
|
||||||
|
defaultSelected,
|
||||||
|
isSelected: isSelectedProp,
|
||||||
|
isDisabled: !!originalProps.isDisabled,
|
||||||
|
isReadOnly,
|
||||||
|
"aria-label": ariaLabel,
|
||||||
|
"aria-labelledby": otherProps["aria-labelledby"] || labelId,
|
||||||
|
onChange,
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
value,
|
||||||
|
name,
|
||||||
|
labelId,
|
||||||
|
children,
|
||||||
|
autoFocus,
|
||||||
|
isSelectedProp,
|
||||||
|
defaultSelected,
|
||||||
|
originalProps.isDisabled,
|
||||||
|
otherProps["aria-label"],
|
||||||
|
otherProps["aria-labelledby"],
|
||||||
|
onChange,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const state = useToggleState(ariaSwitchProps);
|
||||||
|
const {inputProps} = useReactAriaSwitch(ariaSwitchProps, state, inputRef);
|
||||||
|
const {focusProps, isFocused, isFocusVisible} = useFocusRing({autoFocus: inputProps.autoFocus});
|
||||||
|
const {hoverProps, isHovered} = useHover({
|
||||||
|
isDisabled: inputProps.disabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSelected = inputProps.checked;
|
||||||
|
const isDisabled = inputProps.disabled;
|
||||||
|
|
||||||
|
const slots = useMemo(
|
||||||
() =>
|
() =>
|
||||||
toggle({
|
toggle({
|
||||||
...variantProps,
|
...variantProps,
|
||||||
className,
|
isFocusVisible,
|
||||||
}),
|
}),
|
||||||
[...Object.values(variantProps), className],
|
[...Object.values(variantProps), isFocusVisible],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {Component, styles, domRef, ...otherProps};
|
const baseStyles = clsx(styles?.base, className);
|
||||||
|
|
||||||
|
const getBaseProps: PropGetter = () => {
|
||||||
|
return {
|
||||||
|
ref: domRef,
|
||||||
|
className: slots.base({class: baseStyles}),
|
||||||
|
"data-disabled": dataAttr(isDisabled),
|
||||||
|
"data-checked": dataAttr(isSelected),
|
||||||
|
...mergeProps(hoverProps, otherProps),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWrapperProps: PropGetter = useCallback(() => {
|
||||||
|
return {
|
||||||
|
"data-hover": dataAttr(isHovered),
|
||||||
|
"data-checked": dataAttr(isSelected),
|
||||||
|
"data-focus": dataAttr(isFocused),
|
||||||
|
"data-focus-visible": dataAttr(isFocused && isFocusVisible),
|
||||||
|
"data-disabled": dataAttr(isDisabled),
|
||||||
|
"data-readonly": dataAttr(inputProps.readOnly),
|
||||||
|
"aria-hidden": true,
|
||||||
|
className: clsx(slots.wrapper({class: styles?.wrapper})),
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
slots,
|
||||||
|
styles?.wrapper,
|
||||||
|
isHovered,
|
||||||
|
isSelected,
|
||||||
|
isFocused,
|
||||||
|
isFocusVisible,
|
||||||
|
isDisabled,
|
||||||
|
inputProps.readOnly,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getInputProps: PropGetter = () => {
|
||||||
|
return {
|
||||||
|
ref: inputRef,
|
||||||
|
id: inputProps.id || inputId,
|
||||||
|
...mergeProps(inputProps, focusProps),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getThumbProps: PropGetter = useCallback(
|
||||||
|
() => ({
|
||||||
|
"data-checked": dataAttr(isSelected),
|
||||||
|
"data-focus": dataAttr(isFocused),
|
||||||
|
"data-focus-visible": dataAttr(isFocused && isFocusVisible),
|
||||||
|
"data-disabled": dataAttr(isDisabled),
|
||||||
|
"data-readonly": dataAttr(inputProps.readOnly),
|
||||||
|
className: slots.thumb({class: styles?.thumb}),
|
||||||
|
}),
|
||||||
|
[slots, styles?.thumb, isSelected, isFocused, isFocusVisible, isDisabled, inputProps.readOnly],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getLabelProps: PropGetter = useCallback(
|
||||||
|
() => ({
|
||||||
|
id: labelId,
|
||||||
|
htmlFor: inputProps.id || inputId,
|
||||||
|
"data-disabled": dataAttr(isDisabled),
|
||||||
|
"data-checked": dataAttr(isSelected),
|
||||||
|
className: slots.label({class: styles?.label}),
|
||||||
|
}),
|
||||||
|
[slots, styles?.label, isDisabled, isSelected],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getThumbIconProps = useCallback(
|
||||||
|
(
|
||||||
|
props = {
|
||||||
|
includeStateProps: false,
|
||||||
|
},
|
||||||
|
) =>
|
||||||
|
mergeProps(
|
||||||
|
{
|
||||||
|
width: "1em",
|
||||||
|
height: "1em",
|
||||||
|
"data-checked": dataAttr(isSelected),
|
||||||
|
className: slots.thumbIcon({class: styles?.thumbIcon}),
|
||||||
|
},
|
||||||
|
props.includeStateProps
|
||||||
|
? {
|
||||||
|
isSelected: isSelected,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
) as unknown as SwitchThumbIconProps,
|
||||||
|
[slots, styles?.thumbIcon, isSelected, originalProps.disableAnimation],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getLeftIconProps = useCallback(
|
||||||
|
() => ({
|
||||||
|
width: "1em",
|
||||||
|
height: "1em",
|
||||||
|
"data-checked": dataAttr(isSelected),
|
||||||
|
className: slots.leftIcon({class: styles?.leftIcon}),
|
||||||
|
}),
|
||||||
|
[slots, styles?.leftIcon, isSelected],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRightIconProps = useCallback(
|
||||||
|
() => ({
|
||||||
|
width: "1em",
|
||||||
|
height: "1em",
|
||||||
|
"data-checked": dataAttr(isSelected),
|
||||||
|
className: slots.rightIcon({class: styles?.rightIcon}),
|
||||||
|
}),
|
||||||
|
[slots, styles?.rightIcon, isSelected],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
Component,
|
||||||
|
slots,
|
||||||
|
styles,
|
||||||
|
domRef,
|
||||||
|
children,
|
||||||
|
thumbIcon,
|
||||||
|
leftIcon,
|
||||||
|
rightIcon,
|
||||||
|
isHovered,
|
||||||
|
isSelected,
|
||||||
|
isFocused,
|
||||||
|
isFocusVisible,
|
||||||
|
isDisabled,
|
||||||
|
getBaseProps,
|
||||||
|
getWrapperProps,
|
||||||
|
getInputProps,
|
||||||
|
getLabelProps,
|
||||||
|
getThumbProps,
|
||||||
|
getThumbIconProps,
|
||||||
|
getLeftIconProps,
|
||||||
|
getRightIconProps,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UseSwitchReturn = ReturnType<typeof useSwitch>;
|
export type UseSwitchReturn = ReturnType<typeof useSwitch>;
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {ComponentStory, ComponentMeta} from "@storybook/react";
|
import {ComponentStory, ComponentMeta} from "@storybook/react";
|
||||||
import {toggle} from "@nextui-org/theme";
|
import {toggle} from "@nextui-org/theme";
|
||||||
|
import {VisuallyHidden} from "@react-aria/visually-hidden";
|
||||||
|
import {SunFilledIcon, MoonFilledIcon} from "@nextui-org/shared-icons";
|
||||||
|
import {clsx} from "@nextui-org/shared-utils";
|
||||||
|
|
||||||
import {Switch, SwitchProps} from "../src";
|
import {Switch, SwitchProps, SwitchThumbIconProps, useSwitch} from "../src";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Switch",
|
title: "Components/Switch",
|
||||||
component: Switch,
|
component: Switch,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
color: {
|
color: {
|
||||||
@ -14,12 +17,6 @@ export default {
|
|||||||
options: ["neutral", "primary", "secondary", "success", "warning", "danger"],
|
options: ["neutral", "primary", "secondary", "success", "warning", "danger"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
radius: {
|
|
||||||
control: {
|
|
||||||
type: "select",
|
|
||||||
options: ["none", "base", "sm", "md", "lg", "xl", "full"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
size: {
|
size: {
|
||||||
control: {
|
control: {
|
||||||
type: "select",
|
type: "select",
|
||||||
@ -31,6 +28,11 @@ export default {
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
disableAnimation: {
|
||||||
|
control: {
|
||||||
|
type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as ComponentMeta<typeof Switch>;
|
} as ComponentMeta<typeof Switch>;
|
||||||
|
|
||||||
@ -40,7 +42,147 @@ const defaultProps = {
|
|||||||
|
|
||||||
const Template: ComponentStory<typeof Switch> = (args: SwitchProps) => <Switch {...args} />;
|
const Template: ComponentStory<typeof Switch> = (args: SwitchProps) => <Switch {...args} />;
|
||||||
|
|
||||||
|
const WithIconsTemplate: ComponentStory<typeof Switch> = (args: SwitchProps) => {
|
||||||
|
const [isSelected, setIsSelected] = React.useState<boolean>(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Switch
|
||||||
|
{...args}
|
||||||
|
isSelected={isSelected}
|
||||||
|
leftIcon={<SunFilledIcon />}
|
||||||
|
rightIcon={<MoonFilledIcon />}
|
||||||
|
styles={{
|
||||||
|
leftIcon: "text-white",
|
||||||
|
}}
|
||||||
|
onChange={setIsSelected}
|
||||||
|
/>
|
||||||
|
<p className="text-neutral-500">Selected: {isSelected ? "true" : "false"}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ControlledTemplate: ComponentStory<typeof Switch> = (args: SwitchProps) => {
|
||||||
|
const [isSelected, setIsSelected] = React.useState<boolean>(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Switch {...args} isSelected={isSelected} onChange={setIsSelected} />
|
||||||
|
<p className="text-neutral-500">Selected: {isSelected ? "true" : "false"}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomWithStylesTemplate: ComponentStory<typeof Switch> = (args: SwitchProps) => {
|
||||||
|
const [isSelected, setIsSelected] = React.useState<boolean>(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Switch
|
||||||
|
isSelected={isSelected}
|
||||||
|
size="lg"
|
||||||
|
styles={{
|
||||||
|
base: clsx(
|
||||||
|
"inline-flex flex-row-reverse w-full max-w-md bg-content1 hover:bg-content2 items-center justify-between cursor-pointer rounded-lg gap-2 p-4 border-1.5 border-transparent",
|
||||||
|
{
|
||||||
|
"border-primary": isSelected,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
onChange={setIsSelected}
|
||||||
|
{...args}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p className="text-base">Enable early access</p>
|
||||||
|
<p className="text-xs text-neutral-400">
|
||||||
|
Get access to new features before they are released.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Switch>
|
||||||
|
<p className="text-neutral-500">Selected: {isSelected ? "true" : "false"}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomWithHooksTemplate: ComponentStory<typeof Switch> = (args: SwitchProps) => {
|
||||||
|
const {Component, slots, isSelected, getBaseProps, getInputProps, getWrapperProps} =
|
||||||
|
useSwitch(args);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Component {...getBaseProps()}>
|
||||||
|
<VisuallyHidden>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
</VisuallyHidden>
|
||||||
|
<div
|
||||||
|
{...getWrapperProps()}
|
||||||
|
className={slots.wrapper({
|
||||||
|
class: [
|
||||||
|
"w-8 h-8",
|
||||||
|
"flex items-center justify-center",
|
||||||
|
"rounded-lg bg-neutral-100 hover:bg-neutral-200",
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{isSelected ? <SunFilledIcon /> : <MoonFilledIcon />}
|
||||||
|
</div>
|
||||||
|
</Component>
|
||||||
|
<p className="text-neutral-500 select-none">Lights: {isSelected ? "on" : "off"}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const Default = Template.bind({});
|
export const Default = Template.bind({});
|
||||||
Default.args = {
|
Default.args = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const IsReadOnly = Template.bind({});
|
||||||
|
IsReadOnly.args = {
|
||||||
|
...defaultProps,
|
||||||
|
isReadOnly: true,
|
||||||
|
defaultSelected: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithLabel = Template.bind({});
|
||||||
|
WithLabel.args = {
|
||||||
|
...defaultProps,
|
||||||
|
children: "Bluetooth",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DisableAnimation = Template.bind({});
|
||||||
|
DisableAnimation.args = {
|
||||||
|
...defaultProps,
|
||||||
|
disableAnimation: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithThumbIcon = Template.bind({});
|
||||||
|
WithThumbIcon.args = {
|
||||||
|
...defaultProps,
|
||||||
|
thumbIcon: (props: SwitchThumbIconProps) =>
|
||||||
|
props.isSelected ? (
|
||||||
|
<SunFilledIcon className={props.className} />
|
||||||
|
) : (
|
||||||
|
<MoonFilledIcon className={props.className} />
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithIcons = WithIconsTemplate.bind({});
|
||||||
|
WithIcons.args = {
|
||||||
|
...defaultProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Controlled = ControlledTemplate.bind({});
|
||||||
|
Controlled.args = {
|
||||||
|
...defaultProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomWithStyles = CustomWithStylesTemplate.bind({});
|
||||||
|
CustomWithStyles.args = {
|
||||||
|
...defaultProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomWithHooks = CustomWithHooksTemplate.bind({});
|
||||||
|
CustomWithHooks.args = {
|
||||||
|
...defaultProps,
|
||||||
|
};
|
||||||
|
|||||||
@ -20,19 +20,19 @@ const base: SemanticBaseColors = {
|
|||||||
},
|
},
|
||||||
content1: {
|
content1: {
|
||||||
DEFAULT: twColors.zinc[50],
|
DEFAULT: twColors.zinc[50],
|
||||||
contrastText: readableColor(twColors.zinc[50]),
|
contrastText: twColors.zinc[900],
|
||||||
},
|
},
|
||||||
content2: {
|
content2: {
|
||||||
DEFAULT: twColors.zinc[100],
|
DEFAULT: twColors.zinc[100],
|
||||||
contrastText: readableColor(twColors.zinc[100]),
|
contrastText: twColors.zinc[800],
|
||||||
},
|
},
|
||||||
content3: {
|
content3: {
|
||||||
DEFAULT: twColors.zinc[200],
|
DEFAULT: twColors.zinc[200],
|
||||||
contrastText: readableColor(twColors.zinc[200]),
|
contrastText: twColors.zinc[700],
|
||||||
},
|
},
|
||||||
content4: {
|
content4: {
|
||||||
DEFAULT: twColors.zinc[300],
|
DEFAULT: twColors.zinc[300],
|
||||||
contrastText: readableColor(twColors.zinc[300]),
|
contrastText: twColors.zinc[600],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
@ -47,19 +47,19 @@ const base: SemanticBaseColors = {
|
|||||||
},
|
},
|
||||||
content1: {
|
content1: {
|
||||||
DEFAULT: twColors.zinc[900],
|
DEFAULT: twColors.zinc[900],
|
||||||
contrastText: readableColor(twColors.zinc[900]),
|
contrastText: twColors.zinc[50],
|
||||||
},
|
},
|
||||||
content2: {
|
content2: {
|
||||||
DEFAULT: twColors.zinc[800],
|
DEFAULT: twColors.zinc[800],
|
||||||
contrastText: readableColor(twColors.zinc[800]),
|
contrastText: twColors.zinc[100],
|
||||||
},
|
},
|
||||||
content3: {
|
content3: {
|
||||||
DEFAULT: twColors.zinc[700],
|
DEFAULT: twColors.zinc[700],
|
||||||
contrastText: readableColor(twColors.zinc[700]),
|
contrastText: twColors.zinc[200],
|
||||||
},
|
},
|
||||||
content4: {
|
content4: {
|
||||||
DEFAULT: twColors.zinc[600],
|
DEFAULT: twColors.zinc[600],
|
||||||
contrastText: readableColor(twColors.zinc[600]),
|
contrastText: twColors.zinc[300],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -77,8 +77,8 @@ const button = tv({
|
|||||||
true: "[&:not(:first-child):not(:last-child)]:rounded-none",
|
true: "[&:not(:first-child):not(:last-child)]:rounded-none",
|
||||||
},
|
},
|
||||||
disableAnimation: {
|
disableAnimation: {
|
||||||
false: "transition-transform",
|
|
||||||
true: "!transition-none",
|
true: "!transition-none",
|
||||||
|
false: "transition-transform-background",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@ -220,32 +220,32 @@ const button = tv({
|
|||||||
{
|
{
|
||||||
variant: "light",
|
variant: "light",
|
||||||
color: "neutral",
|
color: "neutral",
|
||||||
class: colorVariants.light.neutral,
|
class: [colorVariants.light.neutral, "hover:!bg-neutral-100"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
variant: "light",
|
variant: "light",
|
||||||
color: "primary",
|
color: "primary",
|
||||||
class: colorVariants.light.primary,
|
class: [colorVariants.light.primary, "hover:!bg-primary-50"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
variant: "light",
|
variant: "light",
|
||||||
color: "secondary",
|
color: "secondary",
|
||||||
class: colorVariants.light.secondary,
|
class: [colorVariants.light.secondary, "hover:!bg-secondary-100"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
variant: "light",
|
variant: "light",
|
||||||
color: "success",
|
color: "success",
|
||||||
class: colorVariants.light.success,
|
class: [colorVariants.light.success, "hover:!bg-success-50"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
variant: "light",
|
variant: "light",
|
||||||
color: "warning",
|
color: "warning",
|
||||||
class: colorVariants.light.warning,
|
class: [colorVariants.light.warning, "hover:!bg-warning-50"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
variant: "light",
|
variant: "light",
|
||||||
color: "danger",
|
color: "danger",
|
||||||
class: colorVariants.light.danger,
|
class: [colorVariants.light.danger, "hover:!bg-danger-50"],
|
||||||
},
|
},
|
||||||
// ghost / color
|
// ghost / color
|
||||||
{
|
{
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {ringClasses} from "../utils";
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* <label className={base())}>
|
* <label className={base())}>
|
||||||
* // input
|
* // hidden input
|
||||||
* <span className={wrapper()} aria-hidden="true" data-checked={checked}>
|
* <span className={wrapper()} aria-hidden="true" data-checked={checked}>
|
||||||
* <svg className={icon()}>
|
* <svg className={icon()}>
|
||||||
* // check icon
|
* // check icon
|
||||||
@ -152,6 +152,8 @@ const checkbox = tv({
|
|||||||
disableAnimation: {
|
disableAnimation: {
|
||||||
true: {
|
true: {
|
||||||
wrapper: "transition-none",
|
wrapper: "transition-none",
|
||||||
|
icon: "transition-none",
|
||||||
|
label: "transition-none",
|
||||||
},
|
},
|
||||||
false: {
|
false: {
|
||||||
wrapper: ["before:transition-background", "after:transition-transform-opacity"],
|
wrapper: ["before:transition-background", "after:transition-transform-opacity"],
|
||||||
|
|||||||
@ -109,7 +109,10 @@ const pagination = tv({
|
|||||||
true: {},
|
true: {},
|
||||||
},
|
},
|
||||||
disableAnimation: {
|
disableAnimation: {
|
||||||
true: {},
|
true: {
|
||||||
|
item: "transition-none",
|
||||||
|
cursor: "transition-none",
|
||||||
|
},
|
||||||
false: {
|
false: {
|
||||||
item: "transition-background",
|
item: "transition-background",
|
||||||
cursor: ["transition-transform", "!duration-300"],
|
cursor: ["transition-transform", "!duration-300"],
|
||||||
|
|||||||
@ -1,22 +1,173 @@
|
|||||||
import {tv, type VariantProps} from "tailwind-variants";
|
import {tv, type VariantProps} from "tailwind-variants";
|
||||||
|
|
||||||
|
import {ringClasses} from "../utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle (Switch) wrapper **Tailwind Variants** component
|
* Toggle (Switch) wrapper **Tailwind Variants** component
|
||||||
*
|
*
|
||||||
* const {base, content, dot, avatar, closeButton} = toggle({...})
|
* const {base, wrapper, thumb, thumbIcon, label} = toggle({...})
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* <div className={base())}>
|
* <label className={base())}>
|
||||||
* // left content
|
* // hidden input
|
||||||
* <span className={avatar()}/>
|
* <span className={wrapper()} aria-hidden="true" data-checked={checked}>
|
||||||
* <svg className={dot()}/>
|
* <svg className={leftIcon()}>...</svg>
|
||||||
* <span className={content()}>Default</span>
|
* <span className={thumb()}>
|
||||||
* <svg className={closeButton()}>close button</svg>
|
* <svg className={thumbIcon()}>...</svg>
|
||||||
* // right content
|
* </span>
|
||||||
* </div>
|
* <svg className={rightIcon()}>...</svg>
|
||||||
|
* </span>
|
||||||
|
* <span className={label()}>Label</span>
|
||||||
|
* </label>
|
||||||
*/
|
*/
|
||||||
const toggle = tv({
|
const toggle = tv({
|
||||||
variants: {},
|
slots: {
|
||||||
|
base: "relative max-w-fit inline-flex items-center justify-start cursor-pointer",
|
||||||
|
wrapper: [
|
||||||
|
"group",
|
||||||
|
"px-1",
|
||||||
|
"relative",
|
||||||
|
"inline-flex",
|
||||||
|
"items-center",
|
||||||
|
"justify-start",
|
||||||
|
"flex-shrink-0",
|
||||||
|
"overflow-hidden",
|
||||||
|
"bg-neutral-200",
|
||||||
|
"rounded-full",
|
||||||
|
],
|
||||||
|
thumb: [
|
||||||
|
"z-10",
|
||||||
|
"flex",
|
||||||
|
"items-center",
|
||||||
|
"justify-center",
|
||||||
|
"bg-white",
|
||||||
|
"shadow-sm",
|
||||||
|
"rounded-full",
|
||||||
|
"data-[checked=true]:translate-x-full",
|
||||||
|
],
|
||||||
|
leftIcon: "z-0 absolute left-1.5 text-current",
|
||||||
|
rightIcon: "z-0 absolute right-1.5 text-neutral-600",
|
||||||
|
thumbIcon: "text-black",
|
||||||
|
label: "relative text-foreground select-none",
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
color: {
|
||||||
|
neutral: {
|
||||||
|
wrapper: [
|
||||||
|
"data-[checked=true]:bg-neutral-400",
|
||||||
|
"data-[checked=true]:text-neutral-contrastText",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
wrapper: [
|
||||||
|
"data-[checked=true]:bg-primary",
|
||||||
|
"data-[checked=true]:text-primary-contrastText",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
wrapper: [
|
||||||
|
"data-[checked=true]:bg-secondary",
|
||||||
|
"data-[checked=true]:text-secondary-contrastText",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
wrapper: [
|
||||||
|
"data-[checked=true]:bg-success",
|
||||||
|
"data-[checked=true]:text-success-contrastText",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
wrapper: [
|
||||||
|
"data-[checked=true]:bg-warning",
|
||||||
|
"data-[checked=true]:text-warning-contrastText",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
wrapper: ["data-[checked=true]:bg-danger", "data-[checked=true]:text-danger-contrastText"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
xs: {
|
||||||
|
wrapper: "px-0.5 w-7 h-4 mr-1",
|
||||||
|
thumb: "w-3 h-3 text-[0.5rem]",
|
||||||
|
leftIcon: "text-[0.5rem] left-1",
|
||||||
|
rightIcon: "text-[0.5rem] right-1",
|
||||||
|
right: "text-[0.5rem]",
|
||||||
|
label: "text-xs",
|
||||||
|
},
|
||||||
|
sm: {
|
||||||
|
wrapper: "w-8 h-5 mr-1",
|
||||||
|
thumb: "w-3 h-3 text-[0.6rem]",
|
||||||
|
leftIcon: "text-[0.6rem] left-1",
|
||||||
|
rightIcon: "text-[0.6rem] right-1",
|
||||||
|
label: "text-sm",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
wrapper: "w-10 h-6 mr-2",
|
||||||
|
thumb: "w-4 h-4 text-xs",
|
||||||
|
rightIcon: "text-xs",
|
||||||
|
leftIcon: "text-xs",
|
||||||
|
label: "text-base",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
wrapper: "w-12 h-7 mr-2",
|
||||||
|
thumb: "w-5 h-5 text-sm",
|
||||||
|
rightIcon: "text-sm",
|
||||||
|
leftIcon: "text-sm",
|
||||||
|
label: "text-lg",
|
||||||
|
},
|
||||||
|
xl: {
|
||||||
|
wrapper: "w-14 h-8 mr-2",
|
||||||
|
thumb: "w-6 h-6 text-base",
|
||||||
|
rightIcon: "text-base",
|
||||||
|
leftIcon: "text-base",
|
||||||
|
label: "text-xl",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isFocusVisible: {
|
||||||
|
true: {
|
||||||
|
wrapper: [...ringClasses],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isDisabled: {
|
||||||
|
true: {
|
||||||
|
base: "opacity-50 pointer-events-none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disableAnimation: {
|
||||||
|
true: {
|
||||||
|
wrapper: "transition-none",
|
||||||
|
thumb: "transition-none",
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
wrapper: "transition-background",
|
||||||
|
thumb: [
|
||||||
|
"transition",
|
||||||
|
"group-active:translate-x-1",
|
||||||
|
"data-[checked=true]:group-active:translate-x-[90%]",
|
||||||
|
],
|
||||||
|
leftIcon: [
|
||||||
|
"opacity-0",
|
||||||
|
"scale-50",
|
||||||
|
"transition-transform-opacity",
|
||||||
|
"data-[checked=true]:scale-100",
|
||||||
|
"data-[checked=true]:opacity-100",
|
||||||
|
],
|
||||||
|
rightIcon: [
|
||||||
|
"opacity-100",
|
||||||
|
"transition-transform-opacity",
|
||||||
|
"data-[checked=true]:translate-x-3",
|
||||||
|
"data-[checked=true]:opacity-0",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
color: "primary",
|
||||||
|
size: "md",
|
||||||
|
isDisabled: false,
|
||||||
|
disableAnimation: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ToggleVariantProps = VariantProps<typeof toggle>;
|
export type ToggleVariantProps = VariantProps<typeof toggle>;
|
||||||
|
|||||||
@ -66,4 +66,9 @@ export const utilities = {
|
|||||||
"transition-timing-function": "ease",
|
"transition-timing-function": "ease",
|
||||||
"transition-duration": DEFAULT_TRANSITION_DURATION,
|
"transition-duration": DEFAULT_TRANSITION_DURATION,
|
||||||
},
|
},
|
||||||
|
".transition-transform-background": {
|
||||||
|
"transition-property": "transform, background",
|
||||||
|
"transition-timing-function": "ease",
|
||||||
|
"transition-duration": DEFAULT_TRANSITION_DURATION,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -49,7 +49,7 @@ const faded = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const light = {
|
const light = {
|
||||||
neutral: "bg-transparent text-foreground",
|
neutral: "bg-transparent text-neutral-contrastText",
|
||||||
primary: "bg-transparent text-primary",
|
primary: "bg-transparent text-primary",
|
||||||
secondary: "bg-transparent text-secondary",
|
secondary: "bg-transparent text-secondary",
|
||||||
success: "bg-transparent text-success",
|
success: "bg-transparent text-success",
|
||||||
|
|||||||
@ -15,6 +15,7 @@ module.exports = {
|
|||||||
"../../components/checkbox/stories/*.stories.@(js|jsx|ts|tsx)",
|
"../../components/checkbox/stories/*.stories.@(js|jsx|ts|tsx)",
|
||||||
"../../components/radio/stories/*.stories.@(js|jsx|ts|tsx)",
|
"../../components/radio/stories/*.stories.@(js|jsx|ts|tsx)",
|
||||||
"../../components/pagination/stories/*.stories.@(js|jsx|ts|tsx)",
|
"../../components/pagination/stories/*.stories.@(js|jsx|ts|tsx)",
|
||||||
|
"../../components/switch/stories/*.stories.@(js|jsx|ts|tsx)",
|
||||||
],
|
],
|
||||||
staticDirs: ["../public"],
|
staticDirs: ["../public"],
|
||||||
addons: [
|
addons: [
|
||||||
|
|||||||
@ -134,7 +134,7 @@ import {link, button} from "@nextui-org/theme";
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<div class="block text-xs text-neutral-400">
|
<div class="block text-xs text-neutral-400">
|
||||||
Last updated on <time datetime="2023-03-07">March 7, 2023</time>
|
Last updated on <time datetime="2023-03-07">March 13, 2023</time>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
27
packages/utilities/shared-icons/src/headphones.tsx
Normal file
27
packages/utilities/shared-icons/src/headphones.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import {IconSvgProps} from "./types";
|
||||||
|
|
||||||
|
export const HeadphonesIcon = (props: IconSvgProps) => (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.74982 18.6508C2.33982 18.6508 1.99982 18.3108 1.99982 17.9008V12.2008C1.94982 9.49078 2.95982 6.93078 4.83982 5.01078C6.71982 3.10078 9.23982 2.05078 11.9498 2.05078C17.4898 2.05078 21.9998 6.56078 21.9998 12.1008V17.8008C21.9998 18.2108 21.6598 18.5508 21.2498 18.5508C20.8398 18.5508 20.4998 18.2108 20.4998 17.8008V12.1008C20.4998 7.39078 16.6698 3.55078 11.9498 3.55078C9.63982 3.55078 7.49982 4.44078 5.90982 6.06078C4.30982 7.69078 3.45982 9.86078 3.49982 12.1808V17.8908C3.49982 18.3108 3.16982 18.6508 2.74982 18.6508Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5.94 12.4492H5.81C3.71 12.4492 2 14.1592 2 16.2592V18.1392C2 20.2392 3.71 21.9492 5.81 21.9492H5.94C8.04 21.9492 9.75 20.2392 9.75 18.1392V16.2592C9.75 14.1592 8.04 12.4492 5.94 12.4492Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M18.19 12.4492H18.06C15.96 12.4492 14.25 14.1592 14.25 16.2592V18.1392C14.25 20.2392 15.96 21.9492 18.06 21.9492H18.19C20.29 21.9492 22 20.2392 22 18.1392V16.2592C22 14.1592 20.29 12.4492 18.19 12.4492Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@ -7,3 +7,9 @@ export * from "./close-filled";
|
|||||||
export * from "./chevron";
|
export * from "./chevron";
|
||||||
export * from "./ellipsis";
|
export * from "./ellipsis";
|
||||||
export * from "./forward";
|
export * from "./forward";
|
||||||
|
export * from "./sun";
|
||||||
|
export * from "./sun-filled";
|
||||||
|
export * from "./mail";
|
||||||
|
export * from "./moon";
|
||||||
|
export * from "./moon-filled";
|
||||||
|
export * from "./headphones";
|
||||||
|
|||||||
24
packages/utilities/shared-icons/src/mail.tsx
Normal file
24
packages/utilities/shared-icons/src/mail.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {IconSvgProps} from "./types";
|
||||||
|
|
||||||
|
export const MailIcon = (props: IconSvgProps) => (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path d="M12 20.5H7c-3 0-5-1.5-5-5v-7c0-3.5 2-5 5-5h10c3 0 5 1.5 5 5v3" />
|
||||||
|
<path d="M17 9l-3.13 2.5a3.166 3.166 0 01-3.75 0L7 9M19.21 14.77l-3.539 3.54a1.232 1.232 0 00-.3.59l-.19 1.35a.635.635 0 00.76.76l1.35-.19a1.189 1.189 0 00.59-.3l3.54-3.54a1.365 1.365 0 000-2.22 1.361 1.361 0 00-2.211.01zM18.7 15.28a3.185 3.185 0 002.22 2.22" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
18
packages/utilities/shared-icons/src/moon-filled.tsx
Normal file
18
packages/utilities/shared-icons/src/moon-filled.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {IconSvgProps} from "./types";
|
||||||
|
|
||||||
|
export const MoonFilledIcon = (props: IconSvgProps) => (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M21.53 15.93c-.16-.27-.61-.69-1.73-.49a8.46 8.46 0 01-1.88.13 8.409 8.409 0 01-5.91-2.82 8.068 8.068 0 01-1.44-8.66c.44-1.01.13-1.54-.09-1.76s-.77-.55-1.83-.11a10.318 10.318 0 00-6.32 10.21 10.475 10.475 0 007.04 8.99 10 10 0 002.89.55c.16.01.32.02.48.02a10.5 10.5 0 008.47-4.27c.67-.93.49-1.519.32-1.79z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
22
packages/utilities/shared-icons/src/moon.tsx
Normal file
22
packages/utilities/shared-icons/src/moon.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {IconSvgProps} from "./types";
|
||||||
|
|
||||||
|
export const MoonIcon = (props: IconSvgProps) => (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
width="1em"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M160 136c0-30.62 4.51-61.61 16-88C99.57 81.27 48 159.32 48 248c0 119.29 96.71 216 216 216 88.68 0 166.73-51.57 200-128-26.39 11.49-57.38 16-88 16-119.29 0-216-96.71-216-216z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={32}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
18
packages/utilities/shared-icons/src/sun-filled.tsx
Normal file
18
packages/utilities/shared-icons/src/sun-filled.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {IconSvgProps} from "./types";
|
||||||
|
|
||||||
|
export const SunFilledIcon = (props: IconSvgProps) => (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<g fill="currentColor">
|
||||||
|
<path d="M19 12a7 7 0 11-7-7 7 7 0 017 7z" />
|
||||||
|
<path d="M12 22.96a.969.969 0 01-1-.96v-.08a1 1 0 012 0 1.038 1.038 0 01-1 1.04zm7.14-2.82a1.024 1.024 0 01-.71-.29l-.13-.13a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.984.984 0 01-.7.29zm-14.28 0a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a1 1 0 01-.7.29zM22 13h-.08a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zM2.08 13H2a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zm16.93-7.01a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a.984.984 0 01-.7.29zm-14.02 0a1.024 1.024 0 01-.71-.29l-.13-.14a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.97.97 0 01-.7.3zM12 3.04a.969.969 0 01-1-.96V2a1 1 0 012 0 1.038 1.038 0 01-1 1.04z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
32
packages/utilities/shared-icons/src/sun.tsx
Normal file
32
packages/utilities/shared-icons/src/sun.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {IconSvgProps} from "./types";
|
||||||
|
|
||||||
|
export const SunIcon = (props: IconSvgProps) => (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
width="1em"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M256 48v48M256 416v48M403.08 108.92l-33.94 33.94M142.86 369.14l-33.94 33.94M464 256h-48M96 256H48M403.08 403.08l-33.94-33.94M142.86 142.86l-33.94-33.94"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeMiterlimit={10}
|
||||||
|
strokeWidth={32}
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx={256}
|
||||||
|
cy={256}
|
||||||
|
fill="none"
|
||||||
|
r={80}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeMiterlimit={10}
|
||||||
|
strokeWidth={32}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import {forwardRef} from "@nextui-org/system";
|
import {forwardRef} from "@nextui-org/system";
|
||||||
import {__DEV__} from "@nextui-org/shared-utils";
|
|
||||||
|
|
||||||
import { Use{{capitalize componentName}}Props, use{{capitalize componentName}} } from "./use-{{componentName}}";
|
import { Use{{capitalize componentName}}Props, use{{capitalize componentName}} } from "./use-{{componentName}}";
|
||||||
|
|
||||||
@ -15,8 +14,6 @@ const {{capitalize componentName}} = forwardRef<{{capitalize componentName}}Prop
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (__DEV__) {
|
{{capitalize componentName}}.displayName = "NextUI.{{capitalize componentName}}";
|
||||||
{{capitalize componentName}}.displayName = "NextUI.{{capitalize componentName}}";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {{capitalize componentName}};
|
export default {{capitalize componentName}};
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { {{componentName}} } from "@nextui-org/theme";
|
|||||||
import { {{capitalize componentName}}, {{capitalize componentName}}Props } from "../src";
|
import { {{capitalize componentName}}, {{capitalize componentName}}Props } from "../src";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "{{capitalize componentName}}",
|
title: "Components/{{capitalize componentName}}",
|
||||||
component: {{capitalize componentName}},
|
component: {{capitalize componentName}},
|
||||||
argTypes: {
|
argTypes: {
|
||||||
color: {
|
color: {
|
||||||
|
|||||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@ -686,9 +686,17 @@ importers:
|
|||||||
packages/components/switch:
|
packages/components/switch:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@nextui-org/dom-utils': workspace:*
|
'@nextui-org/dom-utils': workspace:*
|
||||||
|
'@nextui-org/shared-icons': workspace:*
|
||||||
'@nextui-org/shared-utils': workspace:*
|
'@nextui-org/shared-utils': workspace:*
|
||||||
'@nextui-org/system': workspace:*
|
'@nextui-org/system': workspace:*
|
||||||
'@nextui-org/theme': workspace:*
|
'@nextui-org/theme': workspace:*
|
||||||
|
'@react-aria/focus': ^3.11.0
|
||||||
|
'@react-aria/interactions': ^3.14.0
|
||||||
|
'@react-aria/switch': ^3.4.0
|
||||||
|
'@react-aria/utils': ^3.15.0
|
||||||
|
'@react-aria/visually-hidden': ^3.7.0
|
||||||
|
'@react-stately/toggle': ^3.5.0
|
||||||
|
'@react-types/shared': ^3.17.0
|
||||||
clean-package: 2.2.0
|
clean-package: 2.2.0
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -696,7 +704,15 @@ importers:
|
|||||||
'@nextui-org/shared-utils': link:../../utilities/shared-utils
|
'@nextui-org/shared-utils': link:../../utilities/shared-utils
|
||||||
'@nextui-org/system': link:../../core/system
|
'@nextui-org/system': link:../../core/system
|
||||||
'@nextui-org/theme': link:../../core/theme
|
'@nextui-org/theme': link:../../core/theme
|
||||||
|
'@react-aria/focus': 3.11.0_react@18.2.0
|
||||||
|
'@react-aria/interactions': 3.14.0_react@18.2.0
|
||||||
|
'@react-aria/switch': 3.4.0_react@18.2.0
|
||||||
|
'@react-aria/utils': 3.15.0_react@18.2.0
|
||||||
|
'@react-aria/visually-hidden': 3.7.0_react@18.2.0
|
||||||
|
'@react-stately/toggle': 3.5.0_react@18.2.0
|
||||||
|
'@react-types/shared': 3.17.0_react@18.2.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@nextui-org/shared-icons': link:../../utilities/shared-icons
|
||||||
clean-package: 2.2.0
|
clean-package: 2.2.0
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
|
|
||||||
@ -5288,6 +5304,18 @@ packages:
|
|||||||
'@swc/helpers': 0.4.14
|
'@swc/helpers': 0.4.14
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
|
|
||||||
|
/@react-aria/switch/3.4.0_react@18.2.0:
|
||||||
|
resolution: {integrity: sha512-FWbjqNcY+fAjRNXVQTvOx93udhaySgXLsMNLRAVUcFm45FqTaxAhHhNGtRnVhDvzHTJY/Y+kpcGzCcW2rXzPxg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
'@react-aria/toggle': 3.5.0_react@18.2.0
|
||||||
|
'@react-stately/toggle': 3.5.0_react@18.2.0
|
||||||
|
'@react-types/switch': 3.3.0_react@18.2.0
|
||||||
|
'@swc/helpers': 0.4.14
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@react-aria/toggle/3.5.0_react@18.2.0:
|
/@react-aria/toggle/3.5.0_react@18.2.0:
|
||||||
resolution: {integrity: sha512-K49OmHBmYW8pk0rXJU1TNRzR+PxLVvfL/ni6ifV5gcxoxV6DmFsNFj+5B/U3AMnCEQeyKQeiY6z9X7EBVX6j9Q==}
|
resolution: {integrity: sha512-K49OmHBmYW8pk0rXJU1TNRzR+PxLVvfL/ni6ifV5gcxoxV6DmFsNFj+5B/U3AMnCEQeyKQeiY6z9X7EBVX6j9Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user