popper and styling

This commit is contained in:
Oleg Isonen committed 2022-07-21 01:41:58 +02:00
1 parent 163aa43326
commit 459ecd4e8a
6 files changed
+240 -114

No files matched your search

@@ -54,23 +54,29 @@ const Property = ({
<Combobox
name="prop"
autoFocus={prop === ""}
options={allProps}
items={allProps}
value={{
value: prop,
label: prop,
}}
onOptionSelect={({ value }) => {
onItemSelect={({ value }) => {
console.log(value);
onChange(id, "prop", value);
}}
disclosure={(props) => (
disclosure={({ inputProps }) => (
<TextField
{...props}
{...inputProps}
readOnly={required}
variant="ghost"
placeholder="Property"
/>
)}
popperProps={{
align: "start",
sideOffset: 5,
}}
// @todo should we use the side panel (width - x) or something?
listCss={{ width: 230 }}
/>
{isInvalidProp ? (
@@ -180,12 +180,7 @@ const ComboboxControl = ({
id={styleConfig.property}
items={styleConfig.items}
variant="ghost"
css={{
// @todo drag&drop cursor to adjust numeric value
// const cursorUrl = data:image/svg+xml;base64,${btoa(svgCursor)}
//cursor: `url(${cursorUrl}), text`,
textAlign: "right",
}}
popperProps={{ align: "right", sideOffset: 5 }}
state={value.type === "invalid" ? "invalid" : undefined}
value={String(value.value)}
onValueSelect={setValue}
@@ -2,61 +2,65 @@ import { ComponentStory } from "@storybook/react";
import React from "react";
import { Combobox } from "~/shared/design-system";
import { TextField } from "./text-field";
import { Flex } from "./flex";
import { ChevronDownIcon } from "~/shared/icons";
import { IconButton } from "./icon-button";
export default {
component: Combobox,
};
export const Simple: ComponentStory<typeof Combobox> = () => {
const options = [
{ label: "Apple" },
{ label: "Banana" },
{ label: "Orange" },
];
const [value, setValue] = React.useState(options[0]);
const items = [{ label: "Apple" }, { label: "Banana" }, { label: "Orange" }];
const [value, setValue] = React.useState(items[0]);
return (
<Combobox
name="fruit"
options={options}
items={items}
value={value}
onOptionSelect={setValue}
onItemSelect={setValue}
/>
);
};
export const CustomInput: ComponentStory<typeof Combobox> = () => {
const options = [
const items = [
{ label: "Apple", value: "apple", disabled: true },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
];
const [value, setValue] = React.useState(options[0]);
const [value, setValue] = React.useState(items[0]);
return (
<Combobox
name="fruit"
options={options}
items={items}
value={value}
onOptionSelect={setValue}
onOptionHighlight={(option) => {
console.log(option);
onItemSelect={setValue}
onItemHighlight={(item) => {
console.log(item);
}}
disclosure={(props) => (
<TextField
{...props}
onKeyDown={(event) => {
props.onKeyDown(event);
switch (event.key) {
case "Enter": {
console.log(props.value);
break;
disclosure={({ inputProps, toggleProps }) => (
<Flex>
<TextField
{...inputProps}
onKeyDown={(event) => {
inputProps?.onKeyDown(event);
switch (event.key) {
case "Enter": {
console.log(inputProps.value);
break;
}
}
}
}}
onBlur={(event) => {
props.onBlur(event);
console.log(props.value);
}}
/>
}}
onBlur={(event) => {
inputProps?.onBlur(event);
console.log(inputProps.value);
}}
/>
<IconButton variant="ghost" size="1" {...toggleProps}>
<ChevronDownIcon />
</IconButton>
</Flex>
)}
/>
);
@@ -1,52 +1,59 @@
import { type ComponentProps, useState } from "react";
import { CheckIcon } from "@radix-ui/react-icons";
import { Popper, PopperContent, PopperAnchor } from "@radix-ui/react-popper";
import { useCombobox } from "downshift";
import { matchSorter } from "match-sorter";
import { type ComponentProps, useState } from "react";
import {
Box,
Popover,
PopoverContent,
PopoverTrigger,
styled,
} from "~/shared/design-system";
import { itemCss } from "~/shared/design-system/components/menu";
import { panelStyles } from "~/shared/design-system/components/panel";
import { ChevronDownIcon } from "~/shared/icons";
import { styled, type CSS } from "../stitches.config";
import { itemCss } from "./menu";
import { panelStyles } from "./panel";
import { IconButton } from "./icon-button";
import { TextField } from "./text-field";
import { Box } from "./box";
type BaseOption = { label: string };
type BaseItem = { label: string; disabled?: boolean };
const getTextValue = <Option extends BaseOption>(option?: Option) => {
return option ? option.label : "";
const getTextValue = <Item extends BaseItem>(item?: Item) => {
return item ? item.label : "";
};
type DisclosureProps = ComponentProps<typeof TextField>;
type ComboboxProps<Option> = {
type ComboboxProps<Item> = {
name: string;
options: ReadonlyArray<Option>;
value?: Option;
onOptionSelect?: (value: Option) => void;
onOptionHighlight?: (value?: Option) => void;
disclosure?: (props: DisclosureProps) => JSX.Element;
items: Array<Item>;
value?: Item;
onItemSelect?: (value: Item) => void;
onItemHighlight?: (value?: Item) => void;
disclosure?: (items: {
inputProps: ComponentProps<typeof TextField>;
toggleProps: ComponentProps<typeof IconButton>;
}) => JSX.Element;
// @todo should we spread those props flat?
popperProps?: ComponentProps<typeof PopperContent>;
listCss: CSS;
};
const Listbox = styled("ul", panelStyles, { padding: 0, margin: 0 });
const ListboxItem = styled("li", itemCss, { padding: 0, margin: 0 });
const Listbox = styled("ul", panelStyles, {
padding: 0,
margin: 0,
overflow: "auto",
// @todo need some non-hardcoded value
maxHeight: 400,
});
const ListboxItem = styled("li", itemCss);
export const Combobox = <Option extends BaseOption>({
options,
export const Combobox = <Item extends BaseItem>({
items,
value,
name,
onOptionSelect,
onOptionHighlight,
disclosure = (props) => <TextField {...props} />,
}: ComboboxProps<Option>) => {
const [items, setItems] = useState(options);
popperProps,
listCss,
onItemSelect,
onItemHighlight,
disclosure = ({ inputProps }) => <TextField {...inputProps} />,
}: ComboboxProps<Item>) => {
const [filteredItems, setFilteredItems] = useState(items);
const {
isOpen,
// getToggleButtonProps,
getToggleButtonProps,
// getLabelProps,
getMenuProps,
getInputProps,
@@ -56,62 +63,66 @@ export const Combobox = <Option extends BaseOption>({
selectedItem,
} = useCombobox({
onInputValueChange({ inputValue }) {
const filteredItems = matchSorter(options, inputValue, {
keys: ["label", "value"],
});
setItems(filteredItems);
if (inputValue) {
const filteredItems = matchSorter(items, inputValue, {
keys: ["label", "value"],
});
setFilteredItems(filteredItems);
}
},
items,
items: filteredItems,
selectedItem: value,
itemToString(item) {
return item ? item.label : "";
},
onSelectedItemChange({ selectedItem }) {
onOptionSelect?.(selectedItem);
if (selectedItem) {
onItemSelect?.(selectedItem);
}
},
onHighlightedIndexChange({ highlightedIndex }) {
onOptionHighlight?.(items[highlightedIndex]);
if (highlightedIndex !== undefined) {
onItemHighlight?.(items[highlightedIndex]);
}
},
});
const inputProps = getInputProps({ name });
const toggleProps = getToggleButtonProps();
const comboboxProps = getComboboxProps();
const menuProps = getMenuProps();
return (
<Box
{...getComboboxProps()}
css={{
position: "relative",
}}
>
{disclosure(getInputProps({ name }))}
{/*<IconButton variant="ghost" size="1">*/}
{/* <ChevronDownIcon />*/}
{/*</IconButton>*/}
{isOpen && (
<Listbox
{...getMenuProps()}
css={{
position: "absolute",
width: "100%",
}}
>
{items.map((item, index) => {
return (
<ListboxItem
key={index}
{...getItemProps({
<Popper>
<Box {...comboboxProps}>
<PopperAnchor asChild>
{disclosure({ inputProps, toggleProps })}
</PopperAnchor>
{isOpen && (
<PopperContent {...popperProps}>
<Listbox {...menuProps} css={listCss}>
{filteredItems.map((item, index) => {
const itemProps = getItemProps({
item,
index,
disabled: item.disabled,
...(item.disabled ? { "data-disabled": true } : {}),
key: index,
...(item.disabled
? { "data-disabled": true, disabled: true }
: {}),
...(highlightedIndex === index ? { "data-found": true } : {}),
})}
>
{getTextValue<Option>(item)}
{selectedItem === item && <CheckIcon />}
</ListboxItem>
);
})}
</Listbox>
)}
</Box>
});
return (
<ListboxItem {...itemProps}>
{selectedItem === item && <CheckIcon />}
{getTextValue<Item>(item)}
</ListboxItem>
);
})}
</Listbox>
</PopperContent>
)}
</Box>
</Popper>
);
};
+1
View File
@@ -36,6 +36,7 @@
"@radix-ui/react-label": "^0.1.5",
"@radix-ui/react-menu": "^0.1.6",
"@radix-ui/react-popover": "^0.1.6",
"@radix-ui/react-popper": "^1.0.0",
"@radix-ui/react-portal": "^0.1.4",
"@radix-ui/react-progress": "^0.1.4",
"@radix-ui/react-radio-group": "^0.1.5",
+109
View File
@@ -2143,6 +2143,26 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@floating-ui/core@^0.7.3":
version "0.7.3"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.7.3.tgz#d274116678ffae87f6b60e90f88cc4083eefab86"
integrity sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==
"@floating-ui/dom@^0.5.3":
version "0.5.4"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-0.5.4.tgz#4eae73f78bcd4bd553ae2ade30e6f1f9c73fe3f1"
integrity sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==
dependencies:
"@floating-ui/core" "^0.7.3"
"@floating-ui/react-dom@0.7.2":
version "0.7.2"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-0.7.2.tgz#0bf4ceccb777a140fc535c87eb5d6241c8e89864"
integrity sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==
dependencies:
"@floating-ui/dom" "^0.5.3"
use-isomorphic-layout-effect "^1.1.1"
"@gar/promisify@^1.0.1":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
@@ -2776,6 +2796,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "0.1.4"
"@radix-ui/react-arrow@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.0.tgz#c461f4c2cab3317e3d42a1ae62910a4cbb0192a1"
integrity sha512-1MUuv24HCdepi41+qfv125EwMuxgQ+U+h0A9K3BjCO/J8nVRREKHHpkD9clwfnjEDk9hgGzCnff4aUKCPiRepw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-avatar@^0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-0.1.4.tgz#36210e629e0eecde44027195c82d2a4101cbd7e9"
@@ -2836,6 +2864,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae"
integrity sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-context-menu@^0.1.6":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-0.1.6.tgz#0c75f2faffec6c8697247a4b685a432b3c4d07f0"
@@ -2855,6 +2890,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-context@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0"
integrity sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-dialog@0.1.7", "@radix-ui/react-dialog@^0.1.7":
version "0.1.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-0.1.7.tgz#285414cf66f5bbf42bc9935314e0381abe01e7d0"
@@ -3004,6 +3046,22 @@
"@radix-ui/react-use-size" "0.1.1"
"@radix-ui/rect" "0.1.1"
"@radix-ui/react-popper@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.0.0.tgz#fb4f937864bf39c48f27f55beee61fa9f2bef93c"
integrity sha512-k2dDd+1Wl0XWAMs9ZvAxxYsB9sOsEhrFQV4CINd7IUZf0wfdye4OHen9siwxvZImbzhgVeKTJi68OQmPRvVdMg==
dependencies:
"@babel/runtime" "^7.13.10"
"@floating-ui/react-dom" "0.7.2"
"@radix-ui/react-arrow" "1.0.0"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-use-layout-effect" "1.0.0"
"@radix-ui/react-use-rect" "1.0.0"
"@radix-ui/react-use-size" "1.0.0"
"@radix-ui/rect" "1.0.0"
"@radix-ui/react-portal@0.1.4", "@radix-ui/react-portal@^0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-0.1.4.tgz#17bdce3d7f1a9a0b35cb5e935ab8bc562441a7d2"
@@ -3030,6 +3088,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-slot" "0.1.2"
"@radix-ui/react-primitive@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz#376cd72b0fcd5e0e04d252ed33eb1b1f025af2b0"
integrity sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-slot" "1.0.0"
"@radix-ui/react-progress@^0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-0.1.4.tgz#8d0bac115ea31c3a8513c37c4dd77b86258ab08f"
@@ -3130,6 +3196,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "0.1.0"
"@radix-ui/react-slot@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.0.tgz#7fa805b99891dea1e862d8f8fbe07f4d6d0fd698"
integrity sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-switch@^0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-0.1.5.tgz#071ffa19a17a47fdc5c5e6f371bd5901c9fef2f4"
@@ -3247,6 +3321,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz#2fc19e97223a81de64cd3ba1dc42ceffd82374dc"
integrity sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-previous@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-0.1.1.tgz#0226017f72267200f6e832a7103760e96a6db5d0"
@@ -3262,6 +3343,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/rect" "0.1.1"
"@radix-ui/react-use-rect@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz#b040cc88a4906b78696cd3a32b075ed5b1423b3e"
integrity sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/rect" "1.0.0"
"@radix-ui/react-use-size@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-0.1.1.tgz#f6b75272a5d41c3089ca78c8a2e48e5f204ef90f"
@@ -3269,6 +3358,14 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-size@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.0.tgz#a0b455ac826749419f6354dc733e2ca465054771"
integrity sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.0"
"@radix-ui/react-visually-hidden@0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-0.1.4.tgz#6c75eae34fb5d084b503506fbfc05587ced05f03"
@@ -3284,6 +3381,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/rect@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.0.tgz#0dc8e6a829ea2828d53cbc94b81793ba6383bf3c"
integrity sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==
dependencies:
"@babel/runtime" "^7.13.10"
"@react-dnd/asap@4.0.1", "@react-dnd/asap@^4.0.0":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab"
@@ -16990,6 +17094,11 @@ use-callback-ref@^1.3.0:
dependencies:
tslib "^2.0.0"
use-isomorphic-layout-effect@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
use-sidecar@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"