Merge branch 'main' into new_canvas_route

This commit is contained in:
Roman Pominov authored and GitHub committed 2022-10-10 09:24:02 +03:00
commit 1c91d5a8d0
27 files changed
+1435 -635

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

+50 -38
View File
@@ -1,16 +1,15 @@
import { LinksFunction, MetaFunction } from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import { useState } from "react";
import {
Card,
Flex,
Heading,
Button,
__DEPRECATED__Text,
TextField,
} from "@webstudio-is/design-system";
import interStyles from "~/shared/font-faces/inter.css";
import { GithubIcon, CommitIcon, GoogleIcon } from "@webstudio-is/icons";
import { LoginButton } from "./components/login-button";
import loginStyles from "./login.css";
@@ -44,59 +43,72 @@ export const Login = ({ errorMessage }: { errorMessage: string }) => {
justify="center"
>
<Card size={2}>
<Flex direction="column" gap="4" align="center">
<Flex direction="column" align="center" gap="3">
<Heading size="2">Login</Heading>
{errorMessage.length ? (
<__DEPRECATED__Text css={{ textAlign: "center" }} variant="red">
{errorMessage}
</__DEPRECATED__Text>
) : null}
<Flex gap="2" direction="column" align="center">
<Form action="/auth/github" method="post">
<LoginButton enabled={data.isGithubEnabled}>
<Flex gap="1">
<GithubIcon width="20" />
Login with GitHub
</Flex>
</LoginButton>
</Form>
<Form action="/auth/google" method="post">
<LoginButton enabled={data.isGoogleEnabled}>
<Flex gap="1">
<GoogleIcon width="20" />
Login with Google
</Flex>
</LoginButton>
</Form>
{data.isDevLogin && (
<>
{isDevLoginOpen ? (
<Form action="/auth/dev" method="post">
<Flex direction="column" gap="4" css={{ width: 300 }}>
{errorMessage.length ? (
<__DEPRECATED__Text css={{ textAlign: "center" }} variant="red">
{errorMessage}
</__DEPRECATED__Text>
) : null}
<Flex gap="2" direction="column">
<Form action="/auth/github" method="post">
<LoginButton enabled={data.isGithubEnabled}>
<Flex gap="2" align="center">
<Flex css={{ width: 20, height: 20 }}>
<GithubIcon />
</Flex>
Login with GitHub
</Flex>
</LoginButton>
</Form>
<Form action="/auth/google" method="post">
<LoginButton enabled={data.isGoogleEnabled}>
<Flex gap="2" align="center">
<Flex css={{ width: 20, height: 20 }}>
<GoogleIcon />
</Flex>
Login with Google
</Flex>
</LoginButton>
</Form>
{data.isDevLogin &&
(isDevLoginOpen ? (
<Flex
as="form"
action="/auth/dev"
method="post"
css={{
flexDirection: "row",
gap: "$2",
}}
>
<TextField
size={2}
css={{ width: "100%", flexGrow: 1 }}
name="secret"
type="text"
minLength={2}
required
autoFocus
placeholder="Place your auth secret here"
placeholder="Auth secret"
css={{ flexGrow: 1 }}
/>
</Form>
<Button size={2}>Login</Button>
</Flex>
) : (
<LoginButton
enabled={data.isDevLogin}
isDevLogin
onClick={() => openDevLogin(true)}
>
<Flex gap="1" align="center">
<CommitIcon width="20" />
<Flex gap="2" align="center">
<Flex css={{ width: 20, height: 20 }}>
<CommitIcon />
</Flex>
Dev Login
</Flex>
</LoginButton>
)}
</>
)}
))}
</Flex>
</Flex>
</Flex>
</Card>
@@ -55,7 +55,6 @@ export const SelectProjectCard = ({
<Form method="post">
<Flex gap="1">
<TextField
size={2}
state={errors ? "invalid" : undefined}
name="project"
defaultValue={newProject}
@@ -31,7 +31,6 @@ const TextControl = ({
}: TextControlProps) => (
<TextField
type={type}
variant="ghost"
placeholder="Value"
name="value"
value={String(value || defaultValue || "")}
@@ -10,19 +10,100 @@ import type { SelectedInstanceData } from "~/shared/canvas-components";
import {
Box,
Button,
Combobox,
ComboboxTextField,
ComboboxPopperContent,
Grid,
TextField,
Tooltip,
useCombobox,
ComboboxPopper,
ComboboxPopperContent,
ComboboxPopperAnchor,
ComboboxListbox,
ComboboxListboxItem,
IconButton,
} from "@webstudio-is/design-system";
import {
PlusIcon,
TrashIcon,
ExclamationTriangleIcon,
ChevronDownIcon,
} from "@webstudio-is/icons";
import { handleChangePropType, usePropsLogic } from "./use-props-logic";
type ComboboxProps = {
isReadonly: boolean;
isInvalid: boolean;
items: Array<string>;
value: string;
onItemSelect: (value: string | null) => void;
onSubmit: (value: string) => void;
};
const Combobox = ({
isReadonly,
isInvalid,
items: itemsProp,
value,
onItemSelect,
onSubmit,
}: ComboboxProps) => {
const {
items,
getInputProps,
getComboboxProps,
getToggleButtonProps,
getMenuProps,
getItemProps,
isOpen,
} = useCombobox({
items: itemsProp,
value,
itemToString: (item) => item ?? "",
onItemSelect,
});
return (
<ComboboxPopper>
<Box {...getComboboxProps()}>
<ComboboxPopperAnchor>
<TextField
{...getInputProps({
onKeyPress: (event) => {
if (event.key === "Enter") {
onSubmit(event.currentTarget.value);
}
},
})}
name="prop"
placeholder="Property"
readOnly={isReadonly}
state={isInvalid ? "invalid" : undefined}
suffix={
<IconButton {...getToggleButtonProps()}>
<ChevronDownIcon />
</IconButton>
}
/>
</ComboboxPopperAnchor>
<ComboboxPopperContent align="start" sideOffset={5}>
<ComboboxListbox {...getMenuProps()}>
{isOpen &&
items.map((item, index) => {
return (
<ComboboxListboxItem
{...getItemProps({ item, index })}
key={index}
>
{item}
</ComboboxListboxItem>
);
})}
</ComboboxListbox>
</ComboboxPopperContent>
</Box>
</ComboboxPopper>
);
};
type PropertyProps = UserProp & {
component: Instance["component"];
onChange: handleChangePropType;
@@ -33,14 +114,15 @@ const Property = ({
id,
prop,
value,
required,
required = false,
component,
onChange,
onDelete,
}: PropertyProps) => {
const meta = componentsMeta[component];
const argType = meta.argTypes?.[prop as keyof typeof meta.argTypes];
const isInvalidProp =
const isInvalid =
prop != null &&
prop.length > 0 &&
typeof argType === "undefined" &&
!prop.match(/^data-(.)+/);
@@ -51,32 +133,24 @@ const Property = ({
return (
<Grid
gap="1"
gap={1}
css={{ gridTemplateColumns: "1fr 1fr auto", alignItems: "center" }}
>
<Combobox
name="prop"
items={allProps}
value={prop}
itemToString={(item) => item ?? ""}
onItemSelect={(value) => {
onItemSelect={(value: PropertyProps["value"] | null) => {
if (value !== null) {
onChange(id, "prop", value);
}
}}
onSubmit={(value) => {
onChange(id, "prop", value);
}}
renderTextField={({ inputProps, toggleProps }) => (
<ComboboxTextField
toggleProps={toggleProps}
inputProps={{
...inputProps,
readOnly: required,
placeholder: "Property",
}}
/>
)}
renderPopperContent={(props) => (
<ComboboxPopperContent {...props} align="start" sideOffset={5} />
)}
isInvalid={isInvalid}
isReadonly={required}
/>
{isInvalidProp ? (
{isInvalid ? (
<Tooltip content={`Invalid property name: ${prop}`}>
<ExclamationTriangleIcon width={12} height={12} />
</Tooltip>
@@ -9,7 +9,6 @@ import {
PopoverContent,
PopoverPortal,
TextField,
Tooltip,
css,
} from "@webstudio-is/design-system";
import { PropertyName } from "../../shared/property-name";
@@ -54,73 +53,56 @@ const ColorPicker = ({
onChange,
onChangeComplete,
id,
styleConfig,
}: ColorPickerProps) => {
const [displayColorPicker, setDisplayColorPicker] = useState(false);
// Color picker will use 0 as alpha value, which will force user to set alpha every time they have to change from transparent
if (value === "transparent") value = "";
return (
<Popover
modal
open={displayColorPicker}
onOpenChange={setDisplayColorPicker}
>
<PopoverTrigger asChild aria-label="Open color picker">
<Grid
css={{
gridTemplateColumns: "$sizes$6 auto",
gridTemplateRows: "repeat(1, 1fr)",
}}
<TextField
onChange={(e) => onChange(e.target.value)}
value={value}
id={id}
prefix={
<Popover
modal
open={displayColorPicker}
onOpenChange={setDisplayColorPicker}
>
<Tooltip
content={styleConfig.label}
delayDuration={700 / 4}
disableHoverableContent={true}
<PopoverTrigger
asChild
aria-label="Open color picker"
onClick={() => setDisplayColorPicker((shown) => !shown)}
>
<Box
css={{
gridArea: "1 / 1 / 2 / 2",
width: "calc($sizes$6 - 6px)",
height: "calc($sizes$6 - 6px)",
margin: 3,
width: "$sizes$3",
height: "$sizes$3",
borderRadius: 2,
background: value,
zIndex: 0,
}}
/>
</Tooltip>
<TextField
css={{
height: "$6",
gridArea: "1 / 1 / -1 / -1",
paddingLeft: "calc($sizes$6 + 6px)",
}}
onChange={(e) => onChange(e.target.value)}
onClick={() => setDisplayColorPicker((shown) => !shown)}
value={value}
id={id}
/>
</Grid>
</PopoverTrigger>
<PopoverPortal>
<PopoverContent>
<SketchPicker
color={value}
onChange={(color: ColorResult) =>
onChange(stringifyRGBA(color.rgb))
}
onChangeComplete={(color: ColorResult) => {
onChangeComplete(stringifyRGBA(color.rgb));
}}
// @todo to remove both when we have preset colors
presetColors={[]}
className={pickerStyle()}
styles={defaultPickerStyles}
/>
</PopoverContent>
</PopoverPortal>
</Popover>
</PopoverTrigger>
<PopoverPortal>
<PopoverContent>
<SketchPicker
color={value}
onChange={(color: ColorResult) =>
onChange(stringifyRGBA(color.rgb))
}
onChangeComplete={(color: ColorResult) => {
onChangeComplete(stringifyRGBA(color.rgb));
}}
// @todo to remove both when we have preset colors
presetColors={[]}
className={pickerStyle()}
styles={defaultPickerStyles}
/>
</PopoverContent>
</PopoverPortal>
</Popover>
}
/>
);
};
@@ -1,6 +1,5 @@
import { ChangeEvent, FocusEvent, KeyboardEvent, useRef } from "react";
import {
Box,
Text,
Grid,
IconButton,
@@ -25,35 +24,6 @@ const commonUnitsSubset = units
: 1
);
const gridTemplateStyle = {
gridTemplateColumns: "$sizes$6 auto $sizes$6",
gridTemplateRows: "repeat(1, 1fr)",
};
const gridLeftStyle = {
"& ~ input": {
paddingLeft: "calc($sizes$6 + $nudge$3)",
},
gridArea: "1 / 1 / 2 / 2",
};
const gridRightStyle = {
gridArea: "-1 / -1 / -2 / -2",
"& ~ input": {
paddingRight: "calc($sizes$5 + $nudge$3)",
},
};
const iconButtonStyle = {
borderRadius: 2,
height: "calc($sizes$6 - $nudge$3)",
width: "calc($sizes$6 - $nudge$3)",
transform: "translate(3px, 3px)",
"&:focus": {
boxShadow: "none",
},
};
const iconButtonActiveStyle = {
bc: "$colors$blue4",
"&:not(:hover)": {
@@ -61,24 +31,6 @@ const iconButtonActiveStyle = {
},
};
const iconButtonGridLeftStyle = {
...gridLeftStyle,
...iconButtonStyle,
};
const iconButtonGridRightStyle = {
...gridRightStyle,
"& button": { ...iconButtonStyle },
};
const textFieldStyle = {
height: "$6",
fontWeight: "500",
paddingLeft: "calc($sizes$1 + $nudge$3)",
gridArea: "1 / 1 / -1 / -1",
cursor: "default",
};
export const TextControl = ({
currentStyle,
inheritedStyle,
@@ -102,107 +54,106 @@ export const TextControl = ({
const Icon = iconConfigs[styleConfig.property]?.normal;
return (
<Grid columns={2}>
<Grid gap={2} css={{ gridTemplateColumns: "$sizes$8 1fr" }}>
<PropertyName property={styleConfig.property} label={styleConfig.label} />
<Grid css={gridTemplateStyle}>
{Icon && (
<Tooltip
content={styleConfig.label}
delayDuration={200}
disableHoverableContent={true}
>
<IconButton
onPointerEnter={(event) => {
if (value.type !== "unit" || !textFieldRef.current) return;
const textFieldNode = textFieldRef.current;
numericScrubRef.current =
numericScrubRef.current ||
numericScrubControl(event.target as HTMLInputElement, {
minValue: 0,
initialValue: value.value as number,
onValueChange: ({ value }) => {
textFieldNode.value = String(value);
setValue(String(value), { isEphemeral: true });
},
});
}}
onPointerLeave={() => {
if (value.type !== "unit") return;
numericScrubRef.current?.disconnectedCallback();
numericScrubRef.current = undefined;
}}
onPointerUp={(event) => {
setValue((event.target as HTMLInputElement).value);
}}
css={{
...iconButtonGridLeftStyle,
...(isCurrentBreakpoint && iconButtonActiveStyle),
}}
<TextField
ref={textFieldRef}
spellCheck={false}
defaultValue={value.value}
prefix={
Icon && (
<Tooltip
content={styleConfig.label}
delayDuration={200}
disableHoverableContent={true}
>
{Icon && <Icon />}
</IconButton>
</Tooltip>
)}
{value.type === "unit" ? (
<Units
value={value.unit}
items={commonUnitsSubset.map((unit) => ({
name: unit,
label: unit,
}))}
onChange={(unit) => setValue(value.value + unit)}
onHover={(unit) =>
setValue(value.value + unit, { isEphemeral: true })
}
/>
) : (
<Items
value={value.value}
items={styleConfig.items}
onChange={(item) => setValue(item)}
onHover={(item) => setValue(item, { isEphemeral: true })}
/>
)}
<TextField
ref={textFieldRef}
css={textFieldStyle}
spellCheck={false}
defaultValue={value.value}
onFocus={(event: FocusEvent<HTMLInputElement>) =>
event.target.select()
<IconButton
onPointerEnter={(event) => {
if (value.type !== "unit" || !textFieldRef.current) return;
const textFieldNode = textFieldRef.current;
numericScrubRef.current =
numericScrubRef.current ||
numericScrubControl(event.target as HTMLInputElement, {
minValue: 0,
initialValue: value.value as number,
onValueChange: ({ value }) => {
textFieldNode.value = String(value);
setValue(String(value), { isEphemeral: true });
},
});
}}
onPointerLeave={() => {
if (value.type !== "unit") return;
numericScrubRef.current?.disconnectedCallback();
numericScrubRef.current = undefined;
}}
onPointerUp={(event) => {
setValue((event.target as HTMLInputElement).value);
}}
css={{
...(isCurrentBreakpoint && iconButtonActiveStyle),
}}
>
{Icon && <Icon />}
</IconButton>
</Tooltip>
)
}
suffix={
value.type === "unit" ? (
<Units
value={value.unit}
items={commonUnitsSubset.map((unit) => ({
name: unit,
label: unit,
}))}
onChange={(unit) => setValue(value.value + unit)}
onHover={(unit) =>
setValue(value.value + unit, { isEphemeral: true })
}
/>
) : (
<Items
value={value.value}
items={styleConfig.items}
onChange={(item) => setValue(item)}
onHover={(item) => setValue(item, { isEphemeral: true })}
/>
)
}
onFocus={(event: FocusEvent<HTMLInputElement>) => event.target.select()}
onBlur={(event: ChangeEvent<HTMLInputElement>) =>
setValue(event.target.value)
}
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
const target = event.target as HTMLInputElement;
if (event.code === "Enter") {
setValue(target.value);
const number = parseFloat(target.value);
if (!isNaN(number)) target.value = String(number);
}
onBlur={(event: ChangeEvent<HTMLInputElement>) =>
setValue(event.target.value)
}
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
const target = event.target as HTMLInputElement;
if (event.code === "Enter") {
setValue(target.value);
const number = parseFloat(target.value);
if (!isNaN(number)) target.value = String(number);
}
if (value.type !== "unit") return;
if (!["ArrowUp", "ArrowDown"].includes(event.code)) return;
event.preventDefault();
let currentValue = parseFloat(target.value);
let currentDelta = 1;
if (event.shiftKey) currentDelta = 10;
if (event.altKey) currentDelta = 0.1;
if (event.code === "ArrowUp")
currentValue = currentValue + currentDelta;
if (event.code === "ArrowDown")
currentValue = currentValue - currentDelta;
const currentValueAsString =
currentValue % 1
? currentValue.toPrecision(
Math.abs(currentValue).toString().indexOf(".") + 2
)
: String(currentValue);
target.value = currentValueAsString;
setValue(currentValueAsString, { isEphemeral: true });
}}
/>
</Grid>
if (value.type !== "unit") return;
if (!["ArrowUp", "ArrowDown"].includes(event.code)) return;
event.preventDefault();
let currentValue = parseFloat(target.value);
let currentDelta = 1;
if (event.shiftKey) currentDelta = 10;
if (event.altKey) currentDelta = 0.1;
if (event.code === "ArrowUp")
currentValue = currentValue + currentDelta;
if (event.code === "ArrowDown")
currentValue = currentValue - currentDelta;
const currentValueAsString =
currentValue % 1
? currentValue.toPrecision(
Math.abs(currentValue).toString().indexOf(".") + 2
)
: String(currentValue);
target.value = currentValueAsString;
setValue(currentValueAsString, { isEphemeral: true });
}}
/>
</Grid>
);
};
@@ -219,19 +170,17 @@ const Units = ({
onHover?: (value: string) => void;
}) => {
return (
<Box css={iconButtonGridRightStyle}>
<IconButtonWithMenu
icon={
<Text css={{ cursor: "default" }}>
{value === "number" ? "—" : value}
</Text>
}
items={items}
value={String(value)}
onChange={onChange}
onHover={onHover}
/>
</Box>
<IconButtonWithMenu
icon={
<Text css={{ cursor: "default" }}>
{value === "number" ? "—" : value}
</Text>
}
items={items}
value={String(value)}
onChange={onChange}
onHover={onHover}
/>
);
};
@@ -248,14 +197,12 @@ const Items = ({
}) => {
if (!items?.length) return null;
return (
<Box css={iconButtonGridRightStyle}>
<IconButtonWithMenu
icon={<ChevronDownIcon />}
items={items}
value={String(value)}
onChange={onChange}
onHover={onHover}
/>
</Box>
<IconButtonWithMenu
icon={<ChevronDownIcon />}
items={items}
value={String(value)}
onChange={onChange}
onHover={onHover}
/>
);
};
@@ -1,9 +1,8 @@
import { KeyboardEvent, useState } from "react";
import { useRef, useState } from "react";
import { IconButton, TextField } from "@webstudio-is/design-system";
import { Cross2Icon, MagnifyingGlassIcon } from "@webstudio-is/icons";
import { components } from "@webstudio-is/react-sdk";
import type { SelectedInstanceData } from "~/shared/canvas-components";
import { Box } from "@webstudio-is/design-system";
type OnSearch = (search: string) => void;
@@ -25,55 +24,35 @@ type SearchProps = {
export const Search = ({ onSearch, selectedInstanceData }: SearchProps) => {
const [search, setSearch] = useSearch(onSearch);
const inputRef = useRef<HTMLInputElement>(null);
return (
<Box
as="form"
css={{ position: "relative" }}
onReset={() => {
setSearch("");
<TextField
type="search"
value={search}
inputRef={inputRef}
prefix={
<IconButton aria-label="Search">
<MagnifyingGlassIcon />
</IconButton>
}
suffix={
search.length > 0 && (
<IconButton
aria-label="Reset search"
onClick={() => {
setSearch("");
inputRef.current?.focus();
}}
>
<Cross2Icon />
</IconButton>
)
}
placeholder={components[selectedInstanceData.component].label}
onChange={(event) => {
const { value } = event.target;
setSearch(value.toLowerCase());
}}
onKeyDown={(event: KeyboardEvent<HTMLFormElement>) => {
if (event.key === "Escape") {
event.currentTarget.reset();
}
}}
>
<TextField
value={search}
css={{
fontSize: "$2",
height: "$sizes$6",
padding: "$3 calc($2 + $1)",
border: "2px solid $colors$slate7",
bc: "$colors$slate3",
borderRadius: "$2",
boxShadow: "none",
"&:focus": {
bc: "$colors$slate1",
borderWidth: 2,
borderColor: "$colors$blue10",
boxShadow: "none",
},
}}
placeholder={components[selectedInstanceData.component].label}
onChange={(event) => {
const { value } = event.target;
setSearch(value.toLowerCase());
}}
/>
<IconButton
type="reset"
aria-label="Reset search"
css={{
color: "$slate9",
position: "absolute",
top: "50%",
right: 0,
transform: "translate(-25%,-50%)",
}}
>
{search.length === 0 ? <MagnifyingGlassIcon /> : <Cross2Icon />}
</IconButton>
</Box>
/>
);
};
@@ -55,13 +55,13 @@ export const StylePanel = ({
return (
<>
<Box css={{ px: "$3", py: "$1" }}>
<Search
onSearch={setSearch}
selectedInstanceData={selectedInstanceData}
/>
</Box>
<Box css={{ overflow: "auto" }}>
<Box css={{ px: "$3", paddingBottom: "$1" }}>
<Search
onSearch={setSearch}
selectedInstanceData={selectedInstanceData}
/>
</Box>
<StyleSettings
search={search}
selectedInstanceData={selectedInstanceData}
+5 -2
View File
@@ -17,12 +17,13 @@
"devDependencies": {
"@storybook/addon-actions": "^6.5.6",
"@storybook/addon-essentials": "^6.5.6",
"@storybook/addon-interactions": "^6.5.6",
"@storybook/addon-interactions": "^6.5.12",
"@storybook/addon-links": "^6.5.6",
"@storybook/builder-webpack4": "^6.5.6",
"@storybook/jest": "^0.0.10",
"@storybook/manager-webpack4": "^6.5.6",
"@storybook/react": "^6.5.6",
"@storybook/testing-library": "^0.0.11",
"@storybook/testing-library": "^0.0.13",
"@webstudio-is/jest-config": "*",
"lost-pixel": "^2.20.0",
"tsup": "^6.1.3",
@@ -61,6 +62,8 @@
"@radix-ui/react-toggle-group": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0",
"@radix-ui/react-use-layout-effect": "^1.0.0",
"@react-aria/interactions": "^3.11.0",
"@react-aria/utils": "^3.13.3",
"@stitches/react": "^1.2.8",
"@webstudio-is/icons": "*",
"react-hot-toast": "^2.3.0"
@@ -25,7 +25,7 @@ export const Button = styled("button", {
// Custom
height: "$5",
px: "$2",
fontFamily: "$untitled",
fontFamily: "$sans",
fontSize: "$2",
fontWeight: 500,
fontVariantNumeric: "tabular-nums",
@@ -1,7 +1,10 @@
import { action } from "@storybook/addon-actions";
import { ComponentStory } from "@storybook/react";
import React from "react";
import { Combobox, ComboboxTextField } from "./combobox";
import { MagnifyingGlassIcon } from "@webstudio-is/icons";
import { useCombobox as useDownshiftCombobox } from "downshift";
import React, { useCallback } from "react";
import { TextField } from "./text-field";
import { Combobox, ComboboxListboxItem, useCombobox } from "./combobox";
import { Flex } from "./flex";
export default {
component: Combobox,
@@ -9,55 +12,90 @@ export default {
export const Simple: ComponentStory<typeof Combobox> = () => {
const items = [{ label: "Apple" }, { label: "Banana" }, { label: "Orange" }];
const [value, setValue] = React.useState(items[0]);
const [value, setValue] = React.useState<typeof items[number] | null>(null);
return (
<Combobox
name="fruit"
placeholder="Select a fruit"
items={items}
value={value}
onItemSelect={setValue}
/>
);
};
export const CustomInput: ComponentStory<typeof Combobox> = () => {
const items = [
{ label: "Apple", value: "apple", disabled: true },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
];
const [value, setValue] = React.useState(items[0]);
return (
<Combobox
name="fruit"
items={items}
value={value}
onItemSelect={setValue}
onItemHighlight={(item) => {
action("Highlight")(item);
onItemSelect={(value) => {
// If the value is cleared, we revert to previous state
if (value !== null) {
setValue(value);
}
}}
renderTextField={({ inputProps, toggleProps }) => (
<ComboboxTextField
toggleProps={toggleProps}
inputProps={{
...inputProps,
onKeyDown: (event) => {
inputProps.onKeyDown?.(event);
switch (event.key) {
case "Enter": {
action("Enter")(inputProps.value);
break;
}
}
},
onBlur: (event) => {
inputProps.onBlur?.(event);
action("Blur")(inputProps.value);
},
}}
/>
)}
itemToString={(item) => item?.label ?? ""}
/>
);
};
export const Complex: ComponentStory<typeof Combobox> = () => {
const [value, setValue] = React.useState<string | null>(null);
const stateReducer = useCallback((state, actionAndChanges) => {
const { type, changes } = actionAndChanges;
switch (type) {
// on item selection.
case useDownshiftCombobox.stateChangeTypes.ItemClick:
case useDownshiftCombobox.stateChangeTypes.InputKeyDownEnter:
case useDownshiftCombobox.stateChangeTypes.InputBlur:
case useDownshiftCombobox.stateChangeTypes
.ControlledPropUpdatedSelectedItem:
return {
...changes,
// if we have a selected item.
...(changes.selectedItem && {
// we will set the input value to "" (empty string).
inputValue: "",
}),
};
// Remove "reset" action
case useDownshiftCombobox.stateChangeTypes.InputKeyDownEscape: {
return {
...state,
};
}
default:
return changes; // otherwise business as usual.
}
}, []);
const { items, getComboboxProps, getMenuProps, getItemProps, getInputProps } =
useCombobox({
items: ["Apple", "Banana", "Orange"],
value,
itemToString: (item) => item ?? "",
stateReducer,
onItemSelect: (value) => {
setValue(value);
},
});
return (
<Flex {...getComboboxProps()} css={{ flexDirection: "column", gap: "$3" }}>
<TextField
type="search"
prefix={<MagnifyingGlassIcon />}
{...getInputProps({})}
/>
<fieldset>
<legend>Choose an item</legend>
<Flex {...getMenuProps()} css={{ flexDirection: "column" }}>
{items.map((item, index) => {
return (
<ComboboxListboxItem
key={index}
{...getItemProps({ item, index })}
>
{item}
</ComboboxListboxItem>
);
})}
</Flex>
</fieldset>
</Flex>
);
};
+180 -152
View File
@@ -1,13 +1,19 @@
import {
useState,
forwardRef,
useEffect,
useCallback,
type ComponentProps,
type ForwardRefRenderFunction,
useEffect,
useRef,
} from "react";
import { CheckIcon, ChevronDownIcon } from "@webstudio-is/icons";
import { Popper, PopperContent, PopperAnchor } from "@radix-ui/react-popper";
import { useCombobox, type UseComboboxGetItemPropsOptions } from "downshift";
import {
DownshiftState,
UseComboboxStateChangeOptions,
useCombobox as useDownshiftCombobox,
} from "downshift";
import { matchSorter } from "match-sorter";
import { styled } from "../stitches.config";
import { IconButton } from "./icon-button";
@@ -17,41 +23,6 @@ import { TextField } from "./text-field";
import { Box } from "./box";
import { Grid } from "./grid";
type Label = string;
type BaseItem = { label: Label; disabled?: boolean } | Label;
type ComboboxTextFieldProps<Item> = {
inputProps: ComponentProps<typeof TextField>;
toggleProps: ComponentProps<typeof IconButton>;
highlightedItem?: Item;
};
const ComboboxTextFieldBase: ForwardRefRenderFunction<
HTMLDivElement,
ComboboxTextFieldProps<BaseItem>
> = ({ inputProps, toggleProps }, ref) => {
return (
<Box ref={ref} css={{ position: "relative" }}>
<TextField css={{ paddingRight: "$4" }} {...inputProps} />
<IconButton
{...toggleProps}
css={{
position: "absolute",
transform: "translateX(-100%)",
width: "$4",
}}
>
<ChevronDownIcon />
</IconButton>
</Box>
);
};
export const ComboboxTextField = forwardRef(ComboboxTextFieldBase);
ComboboxTextField.displayName = "ComboboxTextField";
const Listbox = styled("ul", panelStyles, {
padding: 0,
margin: 0,
@@ -66,115 +37,110 @@ const ListboxItem = styled("li", itemCss, {
margin: 0,
});
type ListProps<Item> = {
containerProps: ComponentProps<typeof Listbox>;
items: Array<Item>;
getItemProps: (
options: UseComboboxGetItemPropsOptions<Item>
) => ComponentProps<typeof ListboxItem>;
highlightedIndex: number;
selectedItem: Item | null;
itemToString: (item: Item | null) => string;
};
export const List = <Item extends BaseItem>({
containerProps,
items,
getItemProps,
highlightedIndex,
selectedItem,
itemToString,
}: ListProps<Item>) => {
export const ListboxItemBase: ForwardRefRenderFunction<
HTMLLIElement,
ComponentProps<typeof ListboxItem> & {
disabled?: boolean;
selected?: boolean;
highlighted?: boolean;
}
> = (props, ref) => {
const { disabled, selected, highlighted, children, ...rest } = props;
return (
<Listbox {...containerProps}>
{items.map((item, index) => {
const itemProps: Record<string, unknown> = getItemProps({
item,
index,
key: index,
...(typeof item === "object" && item.disabled
? { "data-disabled": true, disabled: true }
: {}),
...(highlightedIndex === index ? { "data-found": true } : {}),
});
return (
// eslint-disable-next-line react/jsx-key
<ListboxItem {...itemProps}>
<Grid align="center" css={{ gridTemplateColumns: "$4 1fr" }}>
{selectedItem === item && <CheckIcon />}
<Box css={{ gridColumn: 2 }}>{itemToString(item)}</Box>
</Grid>
</ListboxItem>
);
})}
</Listbox>
<ListboxItem
ref={ref}
{...(disabled ? { "aria-disabled": true, disabled: true } : {})}
{...(selected ? { "aria-current": true } : {})}
{...rest}
>
<Grid align="center" css={{ gridTemplateColumns: "$4 1fr" }}>
{selected && <CheckIcon />}
<Box css={{ gridColumn: 2 }}>{children}</Box>
</Grid>
</ListboxItem>
);
};
export const ComboboxListbox = Listbox;
export const ComboboxListboxItem = forwardRef(ListboxItemBase);
export const ComboboxPopper = Popper;
export const ComboboxPopperContent = PopperContent;
type ComboboxProps<Item> = {
name: string;
label?: string;
export const ComboboxPopperAnchor = PopperAnchor;
const useFilter = <Item,>({
items,
itemToString,
}: {
items: Array<Item>;
value?: Item;
onItemSelect?: (value: Item) => void;
onItemHighlight?: (value?: Item) => void;
itemToString?: (item: Item | null) => string;
renderTextField?: (
props: ComponentProps<typeof ComboboxTextField>
) => JSX.Element;
renderList?: (props: ListProps<Item>) => JSX.Element;
renderPopperContent?: (
props: ComponentProps<typeof ComboboxPopperContent>
) => JSX.Element;
itemToString: (item: Item | null) => string;
}) => {
const [filteredItems, setFilteredItems] = useState<Array<Item>>(items);
const cachedItems = useRef(items);
useEffect(() => {
cachedItems.current = items;
}, [items]);
const filter = useCallback(
(search?: string) => {
const foundItems: Array<Item> = matchSorter(items, search ?? "", {
keys: [(item) => itemToString(item)],
});
setFilteredItems(foundItems);
},
[itemToString, items]
);
const resetFilter = useCallback(() => {
setFilteredItems(cachedItems.current);
}, []);
return {
filteredItems,
filter,
resetFilter,
};
};
export const Combobox = <Item extends BaseItem>({
type useComboboxProps<Item> = {
items: Array<Item>;
itemToString: (item: Item | null) => string;
value: Item | null; // This is to prevent: "downshift: A component has changed the uncontrolled prop "selectedItem" to be controlled."
onItemSelect?: (value: Item | null) => void;
onItemHighlight?: (value: Item | null) => void;
stateReducer?: (
state: DownshiftState<Item>,
changes: UseComboboxStateChangeOptions<Item>
) => Partial<UseComboboxStateChangeOptions<Item>>;
};
export const useCombobox = <Item,>({
items,
value,
name,
itemToString = (item) =>
typeof item === "object" && item !== null && "label" in item
? item.label
: item ?? "",
itemToString,
onItemSelect,
onItemHighlight,
renderTextField = (props) => <ComboboxTextField {...props} />,
// IMPORTANT! Without Item passed to list <List<Item> typescript is 10x slower!
renderList = (props) => <List<Item> {...props} />,
renderPopperContent = (props) => <ComboboxPopperContent {...props} />,
}: ComboboxProps<Item>) => {
const [foundItems, setFoundItems] = useState(items);
const {
isOpen,
getToggleButtonProps,
// getLabelProps,
getMenuProps,
getInputProps,
getComboboxProps,
highlightedIndex,
getItemProps,
selectedItem,
} = useCombobox({
onInputValueChange({ inputValue }) {
if (inputValue) {
const options =
typeof items[0] === "object" && "label" in items[0]
? { keys: ["label"] }
: undefined;
const foundItems = matchSorter(items, inputValue, options);
setFoundItems(foundItems);
}
},
items: foundItems,
selectedItem: value,
stateReducer = (state, { changes }) => changes,
}: useComboboxProps<Item>) => {
const { filteredItems, filter, resetFilter } = useFilter<Item>({
items,
itemToString,
});
const downshiftProps = useDownshiftCombobox({
items: filteredItems,
selectedItem: value, // Prevent downshift warning about switching controlled mode
stateReducer,
itemToString,
onInputValueChange({ inputValue }) {
filter(inputValue);
},
onSelectedItemChange({ selectedItem }) {
if (selectedItem) {
onItemSelect?.(selectedItem);
}
onItemSelect?.(selectedItem ?? null);
},
onHighlightedIndexChange({ highlightedIndex }) {
if (highlightedIndex !== undefined) {
@@ -183,35 +149,97 @@ export const Combobox = <Item extends BaseItem>({
},
});
const { isOpen, getItemProps, highlightedIndex, selectedItem } =
downshiftProps;
useEffect(() => {
if (isOpen === false) {
setFoundItems(items);
resetFilter();
}
}, [isOpen, items]);
}, [isOpen, resetFilter]);
const inputProps: Record<string, unknown> = getInputProps({ name });
const toggleProps: Record<string, unknown> = getToggleButtonProps();
const comboboxProps: Record<string, unknown> = getComboboxProps();
const menuProps: Record<string, unknown> = getMenuProps();
const highlightedItem = foundItems[highlightedIndex];
const enhancedGetItemProps = useCallback(
(options) => {
return getItemProps({
highlighted: highlightedIndex === options.index,
// We need to either deep compare objects here or use itemToString to get primitive types
selected: itemToString(selectedItem) === itemToString(options.item),
key: options.id,
...options,
});
},
[getItemProps, highlightedIndex, itemToString, selectedItem]
);
return {
...downshiftProps,
items: filteredItems,
getItemProps: enhancedGetItemProps,
};
};
type ComboboxProps<Item> = useComboboxProps<Item> & {
name: string;
label?: string;
placeholder?: string;
};
export const Combobox = <Item,>({
items,
value = null,
name,
placeholder,
itemToString,
onItemSelect,
onItemHighlight,
}: ComboboxProps<Item>) => {
const {
items: foundItems,
getInputProps,
getComboboxProps,
getToggleButtonProps,
getMenuProps,
getItemProps,
isOpen,
} = useCombobox({
items,
value,
itemToString,
onItemSelect,
onItemHighlight,
});
return (
<Popper>
<Box {...comboboxProps}>
<PopperAnchor asChild>
{renderTextField({ inputProps, toggleProps, highlightedItem })}
<Box {...getComboboxProps()}>
<PopperAnchor>
<TextField
{...getInputProps({
name,
placeholder,
})}
suffix={
<IconButton {...getToggleButtonProps()}>
<ChevronDownIcon />
</IconButton>
}
/>
</PopperAnchor>
{renderPopperContent({
style: { zIndex: 1 },
children: renderList({
containerProps: menuProps,
items: isOpen ? foundItems : [],
getItemProps,
highlightedIndex,
selectedItem,
itemToString,
}),
})}
<PopperContent>
<Listbox {...getMenuProps()}>
{isOpen &&
foundItems.map((item, index) => {
return (
// eslint-disable-next-line react/jsx-key
<ComboboxListboxItem
key={index}
{...getItemProps({ item, index })}
>
{itemToString(item)}
</ComboboxListboxItem>
);
})}
</Listbox>
</PopperContent>
</Box>
</Popper>
);
@@ -10,7 +10,7 @@ export const baseItemCss = css({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
fontFamily: "$untitled",
fontFamily: "$sans",
fontSize: "$1",
fontVariantNumeric: "tabular-nums",
lineHeight: "1",
@@ -24,12 +24,12 @@ export const baseItemCss = css({
export const itemCss = css(baseItemCss, {
position: "relative",
color: "$hiContrast",
"&:focus, &[data-found]": {
"&:focus, &[data-found], &[aria-selected=true]": {
outline: "none",
backgroundColor: "$blue9",
color: "white",
},
"&[data-disabled]": {
"&[data-disabled], &[aria-disabled]": {
color: "$slate9",
},
});
@@ -0,0 +1,226 @@
import { expect } from "@storybook/jest";
import { userEvent, waitFor, within } from "@storybook/testing-library";
import React from "react";
import { ComponentStory } from "@storybook/react";
import { RowGapIcon, ChevronDownIcon } from "@webstudio-is/icons";
import { Button } from "./button";
import { Flex } from "./flex";
import { IconButton } from "./icon-button";
import { TextField } from "./text-field";
import { Box } from "./box";
import { Grid } from "./grid";
import { Text } from "./text";
export default {
component: TextField,
argTypes: {
onFocus: { action: true },
onBlur: { action: true },
},
};
export const Default: ComponentStory<typeof TextField> = () => {
return <TextField />;
};
export const NativeProps: ComponentStory<typeof TextField> = () => {
return (
<Flex direction="column" gap={3}>
<TextField placeholder="This is a placeholder" />
<TextField disabled placeholder="This is a disabled placeholder" />
<TextField type="number" defaultValue={25} />
<TextField type="search" placeholder="This is a search input" />
<TextField readOnly value="Read-only" />
<TextField disabled value="Disabled" />
</Flex>
);
};
export const Variants: ComponentStory<typeof TextField> = () => {
return (
<Flex direction="column" gap={3}>
<TextField />
<TextField variant="ghost" />
</Flex>
);
};
export const State: ComponentStory<typeof TextField> = () => {
return (
<Flex direction="column" gap={3}>
<TextField />
<TextField state="invalid" />
<TextField state="valid" />
</Flex>
);
};
export const PrefixSuffix: ComponentStory<typeof TextField> = () => {
return (
<Flex direction="column" gap={3}>
<TextField
prefix={<RowGapIcon />}
suffix={
<IconButton>
<ChevronDownIcon />
</IconButton>
}
/>
<TextField
state="invalid"
prefix={<RowGapIcon />}
suffix={
<IconButton>
<ChevronDownIcon />
</IconButton>
}
/>
<TextField
disabled
prefix={<RowGapIcon />}
suffix={
<IconButton>
<ChevronDownIcon />
</IconButton>
}
/>
</Flex>
);
};
export const Layout: ComponentStory<typeof TextField> = () => {
return (
<>
<Flex direction="row" gap={2} css={{ justifyContent: "space-between" }}>
<TextField
value="Long content comes here and it doesn't wrap"
prefix={<RowGapIcon />}
suffix={
<IconButton>
<ChevronDownIcon />
</IconButton>
}
css={{
flexGrow: 1,
maxWidth: "25%",
}}
/>
<Box css={{ background: "$muted" }}>Content</Box>
<TextField
state="invalid"
value="Long content comes here and it doesn't wrap"
/>
</Flex>
<Grid
gap={2}
css={{
gridTemplateColumns: "100px 1fr",
width: 250,
border: "1px solid",
}}
>
<Box css={{ background: "$muted" }}>
<Text as="label" htmlFor="field">
This is a label
</Text>
</Box>
<Box css={{ background: "$muted" }}>
<TextField
id="field"
value="Long content comes here and it doesn't wrap"
prefix={<RowGapIcon />}
/>
</Box>
<Box css={{ background: "$muted" }}>
<Text as="label" htmlFor="field2">
This is a label
</Text>
</Box>
<TextField
id="field2"
value="Long content comes here and it doesn't wrap"
/>
</Grid>
</>
);
};
export const Interactive: ComponentStory<typeof TextField> = () => {
const [value, setValue] = React.useState("");
const wrapperRef = React.useRef<HTMLDivElement>(null);
const inputRef = React.useRef<HTMLInputElement>(null);
return (
<Flex direction="column" gap={3}>
<TextField ref={wrapperRef} inputRef={inputRef} value={value} readOnly />
<Button
onClick={() => {
// eslint-disable-next-line no-console
setValue(JSON.stringify(wrapperRef.current?.getBoundingClientRect()));
}}
>
Measure TextField
</Button>
<Button
onClick={() => {
inputRef.current?.focus();
}}
>
Focus TextField
</Button>
</Flex>
);
};
export const FocusEvents: ComponentStory<typeof TextField> = (args) => {
return (
<label>
Focus and blur:
<TextField name="focus" suffix={<Button>test</Button>} {...args} />
</label>
);
};
FocusEvents.play = async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
const el = canvas.getByLabelText("Focus and blur:");
await userEvent.tab();
await waitFor(() => expect(el).toHaveFocus());
await userEvent.tab();
await waitFor(() => expect(args.onBlur).not.toHaveBeenCalled());
await userEvent.tab();
await waitFor(() => expect(args.onBlur).toHaveBeenCalled());
await waitFor(() => expect(args.onFocus).toHaveBeenCalledTimes(1));
await waitFor(() => expect(args.onBlur).toHaveBeenCalledTimes(1));
};
export const ClickCapture: ComponentStory<typeof TextField> = (args) => {
return (
<TextField
name="click"
placeholder="Click on the icon to focus input"
prefix={
<Flex title="icon">
<RowGapIcon />
</Flex>
}
suffix={
<IconButton>
<ChevronDownIcon />
</IconButton>
}
{...args}
/>
);
};
ClickCapture.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByPlaceholderText("Click on the icon to focus input");
await userEvent.click(canvas.getByTitle("icon"));
await waitFor(() => expect(input).toHaveFocus());
await userEvent.click(canvas.getByRole("button"));
await waitFor(() => expect(input).toHaveFocus());
};
@@ -1,64 +1,39 @@
import { mergeRefs } from "@react-aria/utils";
import React from "react";
import { useFocusWithin } from "@react-aria/interactions";
import { styled } from "../stitches.config";
import { Flex } from "./flex";
export const TextField = styled("input", {
const InputBase = styled("input", {
// Reset
appearance: "none",
borderWidth: "0",
backgroundColor: "transparent",
boxSizing: "border-box",
fontFamily: "inherit",
fontSize: "inherit",
color: "inherit",
margin: "0",
outline: "none",
padding: "0",
flexGrow: 1,
flexShrink: 1,
minWidth: 0,
width: "100%",
textOverflow: "ellipsis",
outline: "none",
WebkitTapHighlightColor: "rgba(0,0,0,0)",
"&::before": {
boxSizing: "border-box",
},
"&::after": {
boxSizing: "border-box",
// Focus should start on the input element then move to prefix and suffix elements.
// DOM order reflects focus path and visually we use order to put them into the correct visual order.
order: 1,
'&[type="search"]': {
"&::-webkit-search-decoration, &::-webkit-search-cancel-button, &::-webkit-search-results-button, &::-webkit-search-results-decoration":
{
display: "none",
},
},
// Custom
backgroundColor: "$loContrast",
boxShadow: "inset 0 0 0 1px $colors$slate7",
color: "$hiContrast",
fontVariantNumeric: "tabular-nums",
"&:-webkit-autofill": {
boxShadow: "inset 0 0 0 1px $colors$blue6, inset 0 0 0 100px $colors$blue3",
},
"&:-webkit-autofill::first-line": {
fontFamily: "$untitled",
color: "$hiContrast",
},
"&:focus": {
boxShadow:
"inset 0px 0px 0px 1px $colors$blue8, 0px 0px 0px 1px $colors$blue8",
"&:-webkit-autofill": {
boxShadow:
"inset 0px 0px 0px 1px $colors$blue8, 0px 0px 0px 1px $colors$blue8, inset 0 0 0 100px $colors$blue3",
},
},
"&::placeholder": {
color: "$slate9",
},
"&:disabled": {
pointerEvents: "none",
backgroundColor: "$slate2",
color: "$slate8",
cursor: "not-allowed",
"&::placeholder": {
color: "$slate7",
},
},
"&:read-only": {
backgroundColor: "$slate2",
"&:focus": {
boxShadow: "inset 0px 0px 0px 1px $colors$slate7",
},
},
'&[type="number"]': {
"&::-webkit-outer-spin-button, &::-webkit-inner-spin-button": {
WebkitAppearance: "none",
@@ -68,29 +43,64 @@ export const TextField = styled("input", {
},
},
variants: {
size: {
"1": {
borderRadius: "$1",
height: "$5",
fontSize: "$1",
px: "$1",
lineHeight: "$sizes$5",
"&:-webkit-autofill::first-line": {
fontSize: "$1",
},
},
"2": {
borderRadius: "$2",
height: "$6",
fontSize: "$3",
px: "$2",
lineHeight: "$sizes$6",
"&:-webkit-autofill::first-line": {
fontSize: "$3",
},
},
"&:-webkit-autofill::first-line": {
fontFamily: "$sans",
color: "$hiContrast",
},
"&::placeholder": {
color: "$slate9",
},
"&:disabled": {
color: "$slate8",
cursor: "not-allowed",
"&::placeholder": {
color: "$slate7",
},
},
});
const TextFieldBase = styled("div", {
// Custom
display: "flex",
gap: "$1",
backgroundColor: "$loContrast",
boxShadow: "inset 0 0 0 1px $colors$slate7",
color: "$hiContrast",
fontVariantNumeric: "tabular-nums",
px: "$2",
borderRadius: "$1",
fontFamily: "$sans",
fontSize: "$1",
height: 28, // @todo waiting for the sizing scale
lineHeight: 1,
"&[data-has-prefix]": {
paddingLeft: 2,
},
"&[data-has-suffix]": {
paddingRight: 2,
},
"&:focus-within": {
boxShadow:
"inset 0px 0px 0px 1px $colors$blue10, 0px 0px 0px 1px $colors$blue10",
},
"&[aria-disabled=true]": {
pointerEvents: "none",
backgroundColor: "$slate2",
},
"&:has(input:read-only)": {
backgroundColor: "$slate2",
"&:focus": {
boxShadow: "inset 0px 0px 0px 1px $colors$slate7",
},
},
variants: {
variant: {
ghost: {
boxShadow: "none",
@@ -103,7 +113,7 @@ export const TextField = styled("input", {
"&:focus": {
backgroundColor: "$loContrast",
boxShadow:
"inset 0px 0px 0px 1px $colors$blue8, 0px 0px 0px 1px $colors$blue8",
"inset 0px 0px 0px 1px $colors$blue8, 0px 0px 0px 1px $colors$blue10",
},
"&:disabled": {
backgroundColor: "transparent",
@@ -129,19 +139,97 @@ export const TextField = styled("input", {
},
},
},
cursor: {
default: {
cursor: "default",
"&:focus": {
cursor: "text",
},
},
text: {
cursor: "text",
},
},
},
defaultVariants: {
size: "1",
},
});
type TextFieldProps = Pick<
React.ComponentProps<typeof TextFieldBase>,
"variant" | "state" | "css"
> &
Omit<React.ComponentProps<"input">, "prefix" | "children"> & {
inputRef?: React.Ref<HTMLInputElement>;
prefix?: React.ReactNode;
suffix?: React.ReactNode;
};
export const TextField = React.forwardRef<HTMLDivElement, TextFieldProps>(
(props, forwardedRef) => {
const {
prefix,
suffix,
css,
disabled,
inputRef,
state,
variant,
onFocus,
onBlur,
...textFieldProps
} = props;
const internalInputRef = React.useRef<HTMLInputElement>(null);
const focusInnerInput = React.useCallback(() => {
internalInputRef.current?.focus();
}, [internalInputRef]);
const { focusWithinProps } = useFocusWithin({
isDisabled: disabled,
onFocusWithin: (event) => {
// @ts-expect-error Type mismatch from react-aria
onFocus?.(event);
},
onBlurWithin: (event) => {
// @ts-expect-error Type mismatch from react-aria
onBlur?.(event);
},
});
return (
<TextFieldBase
aria-disabled={disabled}
ref={forwardedRef}
state={state}
variant={variant}
css={css}
{...focusWithinProps}
{...(prefix && { "data-has-prefix": true })}
{...(suffix && { "data-has-suffix": true })}
onClickCapture={focusInnerInput}
>
{/* We want input to be the first element in DOM so it receives the focus first */}
<InputBase
disabled={disabled}
{...textFieldProps}
ref={mergeRefs(internalInputRef, inputRef ?? null)}
/>
{prefix && (
<Flex
css={{
alignItems: "center",
flexShrink: 0,
order: 0,
}}
>
{prefix}
</Flex>
)}
{suffix && (
<Flex
css={{
alignItems: "center",
flexShrink: 0,
order: 2,
}}
>
{suffix}
</Flex>
)}
</TextFieldBase>
);
}
);
TextField.displayName = "TextField";
+430 -5
View File
@@ -2,6 +2,11 @@
# yarn lockfile v1
"@adobe/css-tools@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.0.1.tgz#b38b444ad3aa5fedbb15f2f746dcd934226a12dd"
integrity sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==
"@ampproject/remapping@^2.1.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
@@ -1979,7 +1984,7 @@
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.8", "@babel/runtime@^7.17.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.8", "@babel/runtime@^7.17.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
@@ -2336,6 +2341,13 @@
"@types/node" "*"
jest-mock "^27.5.1"
"@jest/expect-utils@^29.1.2":
version "29.1.2"
resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.1.2.tgz#66dbb514d38f7d21456bc774419c9ae5cca3f88d"
integrity sha512-4a48bhKfGj/KAH39u0ppzNTABXQ8QPccWAFUFobWBaEMSMp+sB31Z2fK/l47c4a/Mu1po2ffmfAIPxXbVTXdtg==
dependencies:
jest-get-type "^29.0.0"
"@jest/fake-timers@^27.5.1":
version "27.5.1"
resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74"
@@ -2388,6 +2400,13 @@
terminal-link "^2.0.0"
v8-to-istanbul "^8.1.0"
"@jest/schemas@^29.0.0":
version "29.0.0"
resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.0.0.tgz#5f47f5994dd4ef067fb7b4188ceac45f77fe952a"
integrity sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==
dependencies:
"@sinclair/typebox" "^0.24.1"
"@jest/source-map@^27.5.1":
version "27.5.1"
resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf"
@@ -2481,6 +2500,18 @@
"@types/yargs" "^16.0.0"
chalk "^4.0.0"
"@jest/types@^29.1.2":
version "29.1.2"
resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.1.2.tgz#7442d32b16bcd7592d9614173078b8c334ec730a"
integrity sha512-DcXGtoTykQB5jiwCmVr8H4vdg2OJhQex3qPkG+ISyDO7xQXbt/4R6dowcRyPemRnkH7JoHvZuxPBdlq+9JxFCg==
dependencies:
"@jest/schemas" "^29.0.0"
"@types/istanbul-lib-coverage" "^2.0.0"
"@types/istanbul-reports" "^3.0.0"
"@types/node" "*"
"@types/yargs" "^17.0.8"
chalk "^4.0.0"
"@jridgewell/gen-mapping@^0.1.0":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996"
@@ -3401,6 +3432,45 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@react-aria/interactions@^3.11.0":
version "3.11.0"
resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.11.0.tgz#aa6118af58ff443670152393edab97e403d6d359"
integrity sha512-ZjK4m8u6FlV7Q9/1h9P2Ii6j/NwKR3BmTeGeBQssS2i4dV2pJeOPePnGzVQQGG8FzGQ+TcNRvZPXKaU4AlnBjw==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/utils" "^3.13.3"
"@react-types/shared" "^3.14.1"
"@react-aria/ssr@^3.3.0":
version "3.3.0"
resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.3.0.tgz#25e81daf0c7a270a4a891159d8d984578e4512d8"
integrity sha512-yNqUDuOVZIUGP81R87BJVi/ZUZp/nYOBXbPsRe7oltJOfErQZD+UezMpw4vM2KRz18cURffvmC8tJ6JTeyDtaQ==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/utils@^3.13.3":
version "3.13.3"
resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.13.3.tgz#1b27912e4630f0db6a7b39eb1013f6c4f710075c"
integrity sha512-wqjGNFX4TrXriUU1gvCaoqRhuckdoYogUWN0iyQRkTmzvb7H/NNzQzHou5ggWAdts/NzJUInwKarBWM9hCZZbg==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/ssr" "^3.3.0"
"@react-stately/utils" "^3.5.1"
"@react-types/shared" "^3.14.1"
clsx "^1.1.1"
"@react-stately/utils@^3.5.1":
version "3.5.1"
resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.5.1.tgz#502de762e5d33e892347c5f58053674e06d3bc92"
integrity sha512-INeQ5Er2Jm+db8Py4upKBtgfzp3UYgwXYmbU/XJn49Xw27ktuimH9e37qP3bgHaReb5L3g8IrGs38tJUpnGPHA==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-types/shared@^3.14.1":
version "3.14.1"
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.14.1.tgz#8fe25f729426e8043054e442eb5392364200e028"
integrity sha512-yPPgVRWWanXqbdxFTgJmVwx0JlcnEK3dqkKDIbVk6mxAHvEESI9+oDnHvO8IMHqF+GbrTCzVtAs0zwhYI/uHJA==
"@remix-run/dev@^1.6.4":
version "1.6.4"
resolved "https://registry.yarnpkg.com/@remix-run/dev/-/dev-1.6.4.tgz#7a2a7fc24e66434c5cf64d6e79a62d35d09b73de"
@@ -3701,6 +3771,11 @@
dependencies:
"@sentry/cli" "^1.74.4"
"@sinclair/typebox@^0.24.1":
version "0.24.44"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.44.tgz#0a0aa3bf4a155a678418527342a3ee84bd8caa5c"
integrity sha512-ka0W0KN5i6LfrSocduwliMMpqVgohtPFidKdMEOUjoOFCHcOOYkKsPRxfs5f15oPNHTm6ERAm0GV/+/LTKeiWg==
"@sindresorhus/is@^4.0.0":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f"
@@ -3847,6 +3922,27 @@
regenerator-runtime "^0.13.7"
ts-dedent "^2.0.0"
"@storybook/addon-interactions@^6.5.12":
version "6.5.12"
resolved "https://registry.yarnpkg.com/@storybook/addon-interactions/-/addon-interactions-6.5.12.tgz#b9b7ae50de4c41ea2c9023dcaacf3f318a072ea4"
integrity sha512-wAkKqUX57qZMzgykMIltjDrFfA0II+pKz2SchK4hXHK3/2rtOe+Ah0Sj5nTQo++Ee5oC1PVZWIq/Y1kufCTBJQ==
dependencies:
"@devtools-ds/object-inspector" "^1.1.2"
"@storybook/addons" "6.5.12"
"@storybook/api" "6.5.12"
"@storybook/client-logger" "6.5.12"
"@storybook/components" "6.5.12"
"@storybook/core-common" "6.5.12"
"@storybook/core-events" "6.5.12"
"@storybook/csf" "0.0.2--canary.4566f4d.1"
"@storybook/instrumenter" "6.5.12"
"@storybook/theming" "6.5.12"
core-js "^3.8.2"
global "^4.4.0"
jest-mock "^27.0.6"
polished "^4.2.2"
ts-dedent "^2.2.0"
"@storybook/addon-interactions@^6.5.6", "@storybook/addon-interactions@^6.5.9":
version "6.5.9"
resolved "https://registry.yarnpkg.com/@storybook/addon-interactions/-/addon-interactions-6.5.9.tgz#4356e96beae8f44000955d8870c3acc6c8d1fb3a"
@@ -3946,6 +4042,23 @@
prop-types "^15.7.2"
regenerator-runtime "^0.13.7"
"@storybook/addons@6.5.12":
version "6.5.12"
resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.5.12.tgz#891767b5f88ea99b956cf19e9e2893594068adc7"
integrity sha512-y3cgxZq41YGnuIlBJEuJjSFdMsm8wnvlNOGUP9Q+Er2dgfx8rJz4Q22o4hPjpvpaj4XdBtxCJXI2NeFpN59+Cw==
dependencies:
"@storybook/api" "6.5.12"
"@storybook/channels" "6.5.12"
"@storybook/client-logger" "6.5.12"
"@storybook/core-events" "6.5.12"
"@storybook/csf" "0.0.2--canary.4566f4d.1"
"@storybook/router" "6.5.12"
"@storybook/theming" "6.5.12"
"@types/webpack-env" "^1.16.0"
core-js "^3.8.2"
global "^4.4.0"
regenerator-runtime "^0.13.7"
"@storybook/addons@6.5.9":
version "6.5.9"
resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.5.9.tgz#5a9d7395c579a9cbc44dfc122362fb3c95dfb9d5"
@@ -3963,6 +4076,29 @@
global "^4.4.0"
regenerator-runtime "^0.13.7"
"@storybook/api@6.5.12":
version "6.5.12"
resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.5.12.tgz#7cc82087fc9298be03f15bf4ab9c4aab294b3bac"
integrity sha512-DuUZmMlQxkFNU9Vgkp9aNfCkAongU76VVmygvCuSpMVDI9HQ2lG0ydL+ppL4XKoSMCCoXTY6+rg4hJANnH+1AQ==
dependencies:
"@storybook/channels" "6.5.12"
"@storybook/client-logger" "6.5.12"
"@storybook/core-events" "6.5.12"
"@storybook/csf" "0.0.2--canary.4566f4d.1"
"@storybook/router" "6.5.12"
"@storybook/semver" "^7.3.2"
"@storybook/theming" "6.5.12"
core-js "^3.8.2"
fast-deep-equal "^3.1.3"
global "^4.4.0"
lodash "^4.17.21"
memoizerific "^1.11.3"
regenerator-runtime "^0.13.7"
store2 "^2.12.0"
telejson "^6.0.8"
ts-dedent "^2.0.0"
util-deprecate "^1.0.2"
"@storybook/api@6.5.9":
version "6.5.9"
resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.5.9.tgz#303733214c9de0422d162f7c54ae05d088b89bf9"
@@ -4107,6 +4243,15 @@
global "^4.4.0"
telejson "^6.0.8"
"@storybook/channels@6.5.12":
version "6.5.12"
resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.5.12.tgz#98baf01691d263e2ac341853361ec69c1a6621bc"
integrity sha512-X5XaKbe4b7LXJ4sUakBo00x6pXnW78JkOonHoaKoWsccHLlEzwfBZpVVekhVZnqtCoLT23dB8wjKgA71RYWoiw==
dependencies:
core-js "^3.8.2"
ts-dedent "^2.0.0"
util-deprecate "^1.0.2"
"@storybook/channels@6.5.9":
version "6.5.9"
resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.5.9.tgz#abfab89a6587a2688e9926d4aafeb11c9d8b2e79"
@@ -4142,6 +4287,14 @@
ts-dedent "^2.0.0"
util-deprecate "^1.0.2"
"@storybook/client-logger@6.5.12":
version "6.5.12"
resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.5.12.tgz#d9809e13dc7939eb61452a5e94b1ccb61c4a022c"
integrity sha512-IrkMr5KZcudX935/C2balFbxLHhkvQnJ78rbVThHDVckQ7l3oIXTh66IMzldeOabVFDZEMiW8AWuGEYof+JtLw==
dependencies:
core-js "^3.8.2"
global "^4.4.0"
"@storybook/client-logger@6.5.9", "@storybook/client-logger@^6.4.0", "@storybook/client-logger@^6.4.0 || >=6.5.0-0":
version "6.5.9"
resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.5.9.tgz#dc1669abe8c45af1cc38f74c6f4b15ff33e63014"
@@ -4150,6 +4303,20 @@
core-js "^3.8.2"
global "^4.4.0"
"@storybook/components@6.5.12":
version "6.5.12"
resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.5.12.tgz#e137f0683ea92e22de116bfa62cfd65ce4efe01d"
integrity sha512-NAAGl5PDXaHdVLd6hA+ttmLwH3zAVGXeUmEubzKZ9bJzb+duhFKxDa9blM4YEkI+palumvgAMm0UgS7ou680Ig==
dependencies:
"@storybook/client-logger" "6.5.12"
"@storybook/csf" "0.0.2--canary.4566f4d.1"
"@storybook/theming" "6.5.12"
core-js "^3.8.2"
memoizerific "^1.11.3"
qs "^6.10.0"
regenerator-runtime "^0.13.7"
util-deprecate "^1.0.2"
"@storybook/components@6.5.9":
version "6.5.9"
resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.5.9.tgz#97e07ffe11ab76c01ccee380888991bd161f75b2"
@@ -4192,6 +4359,62 @@
unfetch "^4.2.0"
util-deprecate "^1.0.2"
"@storybook/core-common@6.5.12":
version "6.5.12"
resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-6.5.12.tgz#9f8d5cb3812382c49c84dcfb4279a39e228a1b83"
integrity sha512-gG20+eYdIhwQNu6Xs805FLrOCWtkoc8Rt8gJiRt8yXzZh9EZkU4xgCRoCxrrJ03ys/gTiCFbBOfRi749uM3z4w==
dependencies:
"@babel/core" "^7.12.10"
"@babel/plugin-proposal-class-properties" "^7.12.1"
"@babel/plugin-proposal-decorators" "^7.12.12"
"@babel/plugin-proposal-export-default-from" "^7.12.1"
"@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.1"
"@babel/plugin-proposal-object-rest-spread" "^7.12.1"
"@babel/plugin-proposal-optional-chaining" "^7.12.7"
"@babel/plugin-proposal-private-methods" "^7.12.1"
"@babel/plugin-proposal-private-property-in-object" "^7.12.1"
"@babel/plugin-syntax-dynamic-import" "^7.8.3"
"@babel/plugin-transform-arrow-functions" "^7.12.1"
"@babel/plugin-transform-block-scoping" "^7.12.12"
"@babel/plugin-transform-classes" "^7.12.1"
"@babel/plugin-transform-destructuring" "^7.12.1"
"@babel/plugin-transform-for-of" "^7.12.1"
"@babel/plugin-transform-parameters" "^7.12.1"
"@babel/plugin-transform-shorthand-properties" "^7.12.1"
"@babel/plugin-transform-spread" "^7.12.1"
"@babel/preset-env" "^7.12.11"
"@babel/preset-react" "^7.12.10"
"@babel/preset-typescript" "^7.12.7"
"@babel/register" "^7.12.1"
"@storybook/node-logger" "6.5.12"
"@storybook/semver" "^7.3.2"
"@types/node" "^14.0.10 || ^16.0.0"
"@types/pretty-hrtime" "^1.0.0"
babel-loader "^8.0.0"
babel-plugin-macros "^3.0.1"
babel-plugin-polyfill-corejs3 "^0.1.0"
chalk "^4.1.0"
core-js "^3.8.2"
express "^4.17.1"
file-system-cache "^1.0.5"
find-up "^5.0.0"
fork-ts-checker-webpack-plugin "^6.0.4"
fs-extra "^9.0.1"
glob "^7.1.6"
handlebars "^4.7.7"
interpret "^2.2.0"
json5 "^2.1.3"
lazy-universal-dotenv "^3.0.1"
picomatch "^2.3.0"
pkg-dir "^5.0.0"
pretty-hrtime "^1.0.3"
resolve-from "^5.0.0"
slash "^3.0.0"
telejson "^6.0.8"
ts-dedent "^2.0.0"
util-deprecate "^1.0.2"
webpack "4"
"@storybook/core-common@6.5.9":
version "6.5.9"
resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-6.5.9.tgz#7ca8258ea2634b1d64695c1e4262f71cc7457989"
@@ -4248,6 +4471,13 @@
util-deprecate "^1.0.2"
webpack "4"
"@storybook/core-events@6.5.12":
version "6.5.12"
resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.5.12.tgz#28bd727cc4216012409bfac412fcb708346c56bc"
integrity sha512-0AMyMM19R/lHsYRfWqM8zZTXthasTAK2ExkSRzYi2GkIaVMxRKtM33YRwxKIpJ6KmIKIs8Ru3QCXu1mfCmGzNg==
dependencies:
core-js "^3.8.2"
"@storybook/core-events@6.5.9":
version "6.5.9"
resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.5.9.tgz#5b0783c7d22a586c0f5e927a61fe1b1223e19637"
@@ -4361,6 +4591,24 @@
lodash "^4.17.21"
regenerator-runtime "^0.13.7"
"@storybook/expect@storybook-jest":
version "27.5.2-0"
resolved "https://registry.yarnpkg.com/@storybook/expect/-/expect-27.5.2-0.tgz#131fc4336106805ccd100809a3c2852d31f1da68"
integrity sha512-cP99mhWN/JeCp7VSIiymvj5tmuMY050iFohvp8Zq+kewKsBSZ6/qpTJAGCCZk6pneTcp4S0Fm5BSqyxzbyJ3gw==
dependencies:
"@types/jest" ">=26.0.0"
"@storybook/instrumenter@6.5.12":
version "6.5.12"
resolved "https://registry.yarnpkg.com/@storybook/instrumenter/-/instrumenter-6.5.12.tgz#acd721c50b65183a53fb5913fe7cdb35b06e2433"
integrity sha512-VMl+Yg6ab79A1VqlhC69sBFI6ySw4Lganqdf9JJf6Y7uNRcZwsjfGpGfYiL4WGH1q72tGiq/25XKGIJV0jgi1Q==
dependencies:
"@storybook/addons" "6.5.12"
"@storybook/client-logger" "6.5.12"
"@storybook/core-events" "6.5.12"
core-js "^3.8.2"
global "^4.4.0"
"@storybook/instrumenter@6.5.9", "@storybook/instrumenter@^6.4.0", "@storybook/instrumenter@^6.4.0 || >=6.5.0-0":
version "6.5.9"
resolved "https://registry.yarnpkg.com/@storybook/instrumenter/-/instrumenter-6.5.9.tgz#885d9dec31b7b7fa6ea29b446105480450e527b8"
@@ -4372,6 +4620,16 @@
core-js "^3.8.2"
global "^4.4.0"
"@storybook/jest@^0.0.10":
version "0.0.10"
resolved "https://registry.yarnpkg.com/@storybook/jest/-/jest-0.0.10.tgz#4fbc43ebc3ae6cce8232d6bd9b56ef710f481e52"
integrity sha512-qeYLIplpcOUQXboJde5pRCjTvkGmF80jEszRUoNYCNcEPfC2sMK68Wq6Ct8EQj3CoEdJqsK54O2YYh+7D9S+ag==
dependencies:
"@storybook/expect" storybook-jest
"@storybook/instrumenter" "^6.4.0"
"@testing-library/jest-dom" "^5.16.2"
jest-mock "^27.3.0"
"@storybook/manager-webpack4@6.5.9", "@storybook/manager-webpack4@^6.5.6", "@storybook/manager-webpack4@^6.5.9":
version "6.5.9"
resolved "https://registry.yarnpkg.com/@storybook/manager-webpack4/-/manager-webpack4-6.5.9.tgz#c75d2cced4550c8a786f00b0e57b203d613e706c"
@@ -4468,6 +4726,17 @@
prettier ">=2.2.1 <=2.3.0"
ts-dedent "^2.0.0"
"@storybook/node-logger@6.5.12":
version "6.5.12"
resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.5.12.tgz#0f9efcd1a37c7aae493b22fe33cacca87c135b9b"
integrity sha512-jdLtT3mX5GQKa+0LuX0q0sprKxtCGf6HdXlKZGD5FEuz4MgJUGaaiN0Hgi+U7Z4tVNOtSoIbYBYXHqfUgJrVZw==
dependencies:
"@types/npmlog" "^4.1.2"
chalk "^4.1.0"
core-js "^3.8.2"
npmlog "^5.0.1"
pretty-hrtime "^1.0.3"
"@storybook/node-logger@6.5.9":
version "6.5.9"
resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.5.9.tgz#129cfe0d0f79cab4f6a2ba194d39516680b1626f"
@@ -4562,6 +4831,17 @@
util-deprecate "^1.0.2"
webpack ">=4.43.0 <6.0.0"
"@storybook/router@6.5.12":
version "6.5.12"
resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.5.12.tgz#58efbc1f2f301c8584802af1c710b2f6f03f948c"
integrity sha512-xHubde9YnBbpkDY5+zGO4Pr6VPxP8H9J2v4OTF3H82uaxCIKR0PKG0utS9pFKIsEiP3aM62Hb9qB8nU+v1nj3w==
dependencies:
"@storybook/client-logger" "6.5.12"
core-js "^3.8.2"
memoizerific "^1.11.3"
qs "^6.10.0"
regenerator-runtime "^0.13.7"
"@storybook/router@6.5.9":
version "6.5.9"
resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.5.9.tgz#4740248f8517425b2056273fb366ace8a17c65e8"
@@ -4658,6 +4938,16 @@
"@testing-library/user-event" "^13.2.1"
ts-dedent "^2.2.0"
"@storybook/theming@6.5.12":
version "6.5.12"
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.5.12.tgz#7df1b52913d49c5e84fc1f2e837c02d9fa8cc639"
integrity sha512-uWOo84qMQ2R6c1C0faZ4Q0nY01uNaX7nXoJKieoiJ6ZqY9PSYxJl1kZLi3uPYnrxLZjzjVyXX8MgdxzbppYItA==
dependencies:
"@storybook/client-logger" "6.5.12"
core-js "^3.8.2"
memoizerific "^1.11.3"
regenerator-runtime "^0.13.7"
"@storybook/theming@6.5.9":
version "6.5.9"
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.5.9.tgz#13f60a3a3cd73ceb5caf9f188e1627e79f1891aa"
@@ -4716,6 +5006,21 @@
lz-string "^1.4.4"
pretty-format "^27.0.2"
"@testing-library/jest-dom@^5.16.2":
version "5.16.5"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz#3912846af19a29b2dbf32a6ae9c31ef52580074e"
integrity sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==
dependencies:
"@adobe/css-tools" "^4.0.1"
"@babel/runtime" "^7.9.2"
"@types/testing-library__jest-dom" "^5.9.1"
aria-query "^5.0.0"
chalk "^3.0.0"
css.escape "^1.5.1"
dom-accessibility-api "^0.5.6"
lodash "^4.17.15"
redent "^3.0.0"
"@testing-library/react-hooks@^8.0.1":
version "8.0.1"
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12"
@@ -4983,6 +5288,14 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/jest@*", "@types/jest@>=26.0.0":
version "29.1.1"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.1.1.tgz#cf21a0835a1ba9a30ea1966019f1261c6a114c92"
integrity sha512-U9Ey07dGWl6fUFaIaUQUKWG5NoKi/zizeVQCGV8s4nSU0jPgqphVZvS64+8BtWYvrc3ZGw6wo943NSYPxkrp/g==
dependencies:
expect "^29.0.0"
pretty-format "^29.0.0"
"@types/jest@^27.4.0", "@types/jest@^27.4.1":
version "27.5.2"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.2.tgz#ec49d29d926500ffb9fd22b84262e862049c026c"
@@ -5236,6 +5549,13 @@
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310"
integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==
"@types/testing-library__jest-dom@^5.9.1":
version "5.14.5"
resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz#d113709c90b3c75fdb127ec338dad7d5f86c974f"
integrity sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==
dependencies:
"@types/jest" "*"
"@types/uglify-js@*":
version "3.16.0"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.16.0.tgz#2cf74a0e6ebb6cd54c0d48e509d5bd91160a9602"
@@ -5303,6 +5623,13 @@
dependencies:
"@types/yargs-parser" "*"
"@types/yargs@^17.0.8":
version "17.0.13"
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.13.tgz#34cced675ca1b1d51fcf4d34c3c6f0fa142a5c76"
integrity sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^5.36.2":
version "5.36.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.36.2.tgz#6df092a20e0f9ec748b27f293a12cb39d0c1fe4d"
@@ -7051,6 +7378,14 @@ chalk@^2.0.0, chalk@^2.4.1:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@@ -7330,7 +7665,7 @@ clsx@1.1.0:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702"
integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA==
clsx@^1.0.4:
clsx@^1.0.4, clsx@^1.1.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
@@ -7839,6 +8174,11 @@ css-what@^6.0.1:
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
css.escape@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
@@ -8121,6 +8461,11 @@ diff-sequences@^27.5.1:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327"
integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==
diff-sequences@^29.0.0:
version "29.0.0"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.0.0.tgz#bae49972ef3933556bcb0800b72e8579d19d9e4f"
integrity sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
@@ -8168,7 +8513,7 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dom-accessibility-api@^0.5.9:
dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9:
version "0.5.14"
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz#56082f71b1dc7aac69d83c4285eef39c15d93f56"
integrity sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==
@@ -9397,6 +9742,17 @@ expect@^27.5.1:
jest-matcher-utils "^27.5.1"
jest-message-util "^27.5.1"
expect@^29.0.0:
version "29.1.2"
resolved "https://registry.yarnpkg.com/expect/-/expect-29.1.2.tgz#82f8f28d7d408c7c68da3a386a490ee683e1eced"
integrity sha512-AuAGn1uxva5YBbBlXb+2JPxJRuemZsmlGcapPXWNSBNsQtAULfjioREGBWuI0EOvYUKjDnrCy8PW5Zlr1md5mw==
dependencies:
"@jest/expect-utils" "^29.1.2"
jest-get-type "^29.0.0"
jest-matcher-utils "^29.1.2"
jest-message-util "^29.1.2"
jest-util "^29.1.2"
express@4.17.3:
version "4.17.3"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1"
@@ -11615,6 +11971,16 @@ jest-diff@^27.5.1:
jest-get-type "^27.5.1"
pretty-format "^27.5.1"
jest-diff@^29.1.2:
version "29.1.2"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.1.2.tgz#bb7aaf5353227d6f4f96c5e7e8713ce576a607dc"
integrity sha512-4GQts0aUopVvecIT4IwD/7xsBaMhKTYoM4/njE/aVw9wpw+pIUVp8Vab/KnSzSilr84GnLBkaP3JLDnQYCKqVQ==
dependencies:
chalk "^4.0.0"
diff-sequences "^29.0.0"
jest-get-type "^29.0.0"
pretty-format "^29.1.2"
jest-docblock@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0"
@@ -11663,6 +12029,11 @@ jest-get-type@^27.5.1:
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==
jest-get-type@^29.0.0:
version "29.0.0"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.0.0.tgz#843f6c50a1b778f7325df1129a0fd7aa713aef80"
integrity sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw==
jest-haste-map@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa"
@@ -11745,6 +12116,16 @@ jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1:
jest-get-type "^27.5.1"
pretty-format "^27.5.1"
jest-matcher-utils@^29.1.2:
version "29.1.2"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.1.2.tgz#e68c4bcc0266e70aa1a5c13fb7b8cd4695e318a1"
integrity sha512-MV5XrD3qYSW2zZSHRRceFzqJ39B2z11Qv0KPyZYxnzDHFeYZGJlgGi0SW+IXSJfOewgJp/Km/7lpcFT+cgZypw==
dependencies:
chalk "^4.0.0"
jest-diff "^29.1.2"
jest-get-type "^29.0.0"
pretty-format "^29.1.2"
jest-message-util@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf"
@@ -11760,7 +12141,22 @@ jest-message-util@^27.5.1:
slash "^3.0.0"
stack-utils "^2.0.3"
jest-mock@^27.0.6, jest-mock@^27.5.1:
jest-message-util@^29.1.2:
version "29.1.2"
resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.1.2.tgz#c21a33c25f9dc1ebfcd0f921d89438847a09a501"
integrity sha512-9oJ2Os+Qh6IlxLpmvshVbGUiSkZVc2FK+uGOm6tghafnB2RyjKAxMZhtxThRMxfX1J1SOMhTn9oK3/MutRWQJQ==
dependencies:
"@babel/code-frame" "^7.12.13"
"@jest/types" "^29.1.2"
"@types/stack-utils" "^2.0.0"
chalk "^4.0.0"
graceful-fs "^4.2.9"
micromatch "^4.0.4"
pretty-format "^29.1.2"
slash "^3.0.0"
stack-utils "^2.0.3"
jest-mock@^27.0.6, jest-mock@^27.3.0, jest-mock@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6"
integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==
@@ -11931,6 +12327,18 @@ jest-util@^27.5.1:
graceful-fs "^4.2.9"
picomatch "^2.2.3"
jest-util@^29.1.2:
version "29.1.2"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.1.2.tgz#ac5798e93cb6a6703084e194cfa0898d66126df1"
integrity sha512-vPCk9F353i0Ymx3WQq3+a4lZ07NXu9Ca8wya6o4Fe4/aO1e1awMMprZ3woPFpKwghEOW+UXgd15vVotuNN9ONQ==
dependencies:
"@jest/types" "^29.1.2"
"@types/node" "*"
chalk "^4.0.0"
ci-info "^3.2.0"
graceful-fs "^4.2.9"
picomatch "^2.2.3"
jest-validate@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067"
@@ -14642,6 +15050,15 @@ pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1:
ansi-styles "^5.0.0"
react-is "^17.0.1"
pretty-format@^29.0.0, pretty-format@^29.1.2:
version "29.1.2"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.1.2.tgz#b1f6b75be7d699be1a051f5da36e8ae9e76a8e6a"
integrity sha512-CGJ6VVGXVRP2o2Dorl4mAwwvDWT25luIsYhkyVQW32E4nL+TgW939J7LlKT/npq5Cpq6j3s+sy+13yk7xYpBmg==
dependencies:
"@jest/schemas" "^29.0.0"
ansi-styles "^5.0.0"
react-is "^18.0.0"
pretty-hrtime@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
@@ -15033,7 +15450,7 @@ react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
"react-is@^16.12.0 || ^17.0.0 || ^18.0.0":
"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
@@ -15271,6 +15688,14 @@ redent@^1.0.0:
indent-string "^2.1.0"
strip-indent "^1.0.1"
redent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
dependencies:
indent-string "^4.0.0"
strip-indent "^3.0.0"
refractor@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a"