Implement disable function for style source input (#761)

## Description

1. Added a way to disable/enable style source item

## Steps for reproduction

1. source item menu > disable
2. source item menu > enable

## Code Review

- [ ] hi @TrySound , I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it in the story

## Before requesting a review

- [x] made a self-review
- [x] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [x] tested locally and on preview environment (preview dev login:
5de6)
- [x] updated [test
cases](https://github.com/webstudio-is/webstudio-designer/blob/main/apps/designer/docs/test-cases.md)
document
- [ ] added tests
- [x] if any new env variables are added, added them to `.env.example`
and the `designer/env-check.js` if mandatory
This commit is contained in:
Oleg Isonen authored and GitHub committed 2023-01-11 11:26:03 +01:00
1 parent 6387029b3c
commit c7d7ede59a
4 files changed
+160 -44

No files matched your search

@@ -1 +1,2 @@
export * from "./style-source-input";
export { type ItemState } from "./style-source";
@@ -1,7 +1,7 @@
import { v4 as uuid } from "uuid";
import type { ComponentStory } from "@storybook/react";
import { useState } from "react";
import { StyleSourceInput } from "./style-source-input";
import { StyleSourceInput, type ItemState } from "./";
export default {
component: StyleSourceInput,
@@ -12,6 +12,7 @@ type Item = {
label: string;
type: "token" | "local";
hasMenu: boolean;
state: ItemState;
};
const localItem: Item = {
@@ -19,12 +20,25 @@ const localItem: Item = {
label: "Local",
type: "local",
hasMenu: false,
state: "initial",
};
const items: Array<Item> = [
{ id: "1", label: "Apple", type: "token", hasMenu: false },
{ id: "2", label: "Banana", type: "token", hasMenu: false },
{ id: "3", label: "Orange", type: "token", hasMenu: false },
{ id: "1", label: "Apple", type: "token", hasMenu: false, state: "initial" },
{
id: "2",
label: "Banana",
type: "token",
hasMenu: false,
state: "initial",
},
{
id: "3",
label: "Orange",
type: "token",
hasMenu: false,
state: "initial",
},
];
export const Initial: ComponentStory<typeof StyleSourceInput> = () => {
@@ -35,7 +49,13 @@ export const Initial: ComponentStory<typeof StyleSourceInput> = () => {
items={items}
value={value}
onCreateItem={({ label }) => {
const item: Item = { id: uuid(), label, type: "token", hasMenu: false };
const item: Item = {
id: uuid(),
label,
type: "token",
hasMenu: false,
state: "initial",
};
setValue([...value, item]);
}}
onSelectItem={(item) => {
@@ -59,7 +79,13 @@ export const WithItems: ComponentStory<typeof StyleSourceInput> = () => {
items={items}
value={value}
onCreateItem={({ label }) => {
const item: Item = { id: uuid(), label, type: "token", hasMenu: false };
const item: Item = {
id: uuid(),
label,
type: "token",
hasMenu: false,
state: "initial",
};
setValue([...value, item]);
}}
onSelectItem={(item) => {
@@ -85,6 +111,7 @@ export const WithTruncatedItem: ComponentStory<
"Local Something Something Something Something Something Something Something Something Something Something Something",
type: "local",
hasMenu: false,
state: "initial",
},
]);
return (
@@ -93,7 +120,13 @@ export const WithTruncatedItem: ComponentStory<
items={items}
value={value}
onCreateItem={({ label }) => {
const item: Item = { id: uuid(), label, type: "token", hasMenu: false };
const item: Item = {
id: uuid(),
label,
type: "token",
hasMenu: false,
state: "initial",
};
setValue([...value, item]);
}}
onSelectItem={(item) => {
@@ -111,7 +144,13 @@ export const WithTruncatedItem: ComponentStory<
export const WithMenu: ComponentStory<typeof StyleSourceInput> = () => {
const [value, setValue] = useState<Array<Item>>([
{ id: "0", label: "Apple", type: "token", hasMenu: true },
{
id: "0",
label: "Apple",
type: "token",
hasMenu: true,
state: "initial",
},
]);
const [editingIndex, setEditingIndex] = useState<number>(-1);
return (
@@ -121,7 +160,13 @@ export const WithMenu: ComponentStory<typeof StyleSourceInput> = () => {
value={value}
editingIndex={editingIndex}
onCreateItem={({ label }) => {
const item: Item = { id: uuid(), label, type: "token", hasMenu: true };
const item: Item = {
id: uuid(),
label,
type: "token",
hasMenu: true,
state: "initial",
};
setValue([...value, item]);
}}
onSelectItem={(item) => {
@@ -143,6 +188,26 @@ export const WithMenu: ComponentStory<typeof StyleSourceInput> = () => {
})
);
}}
onDisableItem={(itemToDisable) => {
setValue(
value.map((item) => {
if (item.id === itemToDisable.id) {
return { ...item, state: "disabled" };
}
return item;
})
);
}}
onEnableItem={(itemToEnable) => {
setValue(
value.map((item) => {
if (item.id === itemToEnable.id) {
return { ...item, state: "initial" };
}
return item;
})
);
}}
onDuplicateItem={(itemToDuplicate) => {
const duplicatedItem = {
...itemToDuplicate,
@@ -20,11 +20,12 @@ import {
type RefObject,
} from "react";
import { mergeRefs } from "@react-aria/utils";
import { StyleSource } from "./style-source";
import { StyleSource, type ItemState } from "./style-source";
type IntermediateItem = {
label: string;
hasMenu: boolean;
state: ItemState;
};
type TextFieldBaseWrapperProps<Item> = Omit<ComponentProps<"input">, "value"> &
@@ -40,6 +41,8 @@ type TextFieldBaseWrapperProps<Item> = Omit<ComponentProps<"input">, "value"> &
onRemoveItem?: (item: Item) => void;
onDuplicateItem?: (item: Item) => void;
onChangeItem?: (item: Item) => void;
onDisableItem?: (item: Item) => void;
onEnableItem?: (item: Item) => void;
editingIndex: number;
};
@@ -64,6 +67,8 @@ const TextFieldBase: ForwardRefRenderFunction<
onRemoveItem,
onDuplicateItem,
onChangeItem,
onDisableItem,
onEnableItem,
editingIndex: editingIndexProp,
...textFieldProps
} = props;
@@ -88,18 +93,24 @@ const TextFieldBase: ForwardRefRenderFunction<
>
{value.map((item, index) => (
<StyleSource
isEditing={index === editingIndex}
onEditingChange={(isEditing) => {
setEditingIndex(isEditing === true ? index : -1);
state={index === editingIndex ? "editing" : item.state}
onStateChange={(state) => {
setEditingIndex(state === "editing" ? index : -1);
if (state === "disabled") {
onDisableItem?.(item);
}
if (state === "initial") {
onEnableItem?.(item);
}
}}
onChange={(label) => {
setEditingIndex(-1);
onChangeItem?.({ ...item, label });
}}
onDuplicateItem={() => {
onDuplicate={() => {
onDuplicateItem?.(item);
}}
onRemoveItem={() => {
onRemove={() => {
onRemoveItem?.(item);
}}
label={item.label}
@@ -135,6 +146,8 @@ type StyleSourceInputProps<Item> = {
onCreateItem?: (item: Item) => void;
onChangeItem?: (item: Item) => void;
onDuplicateItem?: (item: Item) => void;
onDisableItem?: (item: Item) => void;
onEnableItem?: (item: Item) => void;
css?: CSS;
};
@@ -153,7 +166,7 @@ export const StyleSourceInput = <Item extends IntermediateItem>(
isOpen,
} = useCombobox({
items: props.items ?? [],
value: { label, hasMenu: true },
value: { label, hasMenu: true, state: "initial" },
selectedItem: undefined,
itemToString: (item) => (item ? item.label : ""),
onItemSelect(item) {
@@ -187,6 +200,8 @@ export const StyleSourceInput = <Item extends IntermediateItem>(
onRemoveItem={props.onRemoveItem}
onChangeItem={props.onChangeItem}
onDuplicateItem={props.onDuplicateItem}
onDisableItem={props.onDisableItem}
onEnableItem={props.onEnableItem}
label={label}
value={value}
css={props.css}
@@ -41,9 +41,12 @@ const menuCssVars = ({ show }: { show: boolean }) => ({
});
type MenuProps = {
state: ItemState;
onEdit: () => void;
onDuplicateItem: () => void;
onRemoveItem: () => void;
onDisable: () => void;
onEnable: () => void;
onDuplicate: () => void;
onRemove: () => void;
};
const Menu = (props: MenuProps) => {
@@ -68,10 +71,20 @@ const Menu = (props: MenuProps) => {
<DropdownMenuItem onSelect={scheduler.set(props.onEdit)}>
Edit Name
</DropdownMenuItem>
<DropdownMenuItem onSelect={scheduler.set(props.onDuplicateItem)}>
{props.state === "disabled" && (
<DropdownMenuItem onSelect={scheduler.set(props.onEnable)}>
Enable
</DropdownMenuItem>
)}
{props.state !== "disabled" && (
<DropdownMenuItem onSelect={scheduler.set(props.onDisable)}>
Disable
</DropdownMenuItem>
)}
<DropdownMenuItem onSelect={scheduler.set(props.onDuplicate)}>
Duplicate
</DropdownMenuItem>
<DropdownMenuItem onSelect={scheduler.set(props.onRemoveItem)}>
<DropdownMenuItem onSelect={scheduler.set(props.onRemove)}>
Remove
</DropdownMenuItem>
</DropdownMenuContent>
@@ -80,18 +93,20 @@ const Menu = (props: MenuProps) => {
);
};
export type ItemState = "initial" | "editing" | "disabled";
type EditableTextProps = {
label: string;
onChange: (value: string) => void;
isEditing: boolean;
onEditingChange: (isEditing: boolean) => void;
state: ItemState;
onStateChange: (state: ItemState) => void;
};
const EditableText = ({
onChange,
label,
isEditing,
onEditingChange,
state,
onStateChange,
}: EditableTextProps) => {
const ref = useRef<HTMLDivElement>(null);
@@ -100,7 +115,7 @@ const EditableText = ({
return;
}
if (isEditing === true) {
if (state === "editing") {
ref.current.setAttribute("contenteditable", "plaintext-only");
ref.current.focus();
getSelection()?.selectAllChildren(ref.current);
@@ -108,13 +123,13 @@ const EditableText = ({
}
ref.current?.removeAttribute("contenteditable");
}, [isEditing]);
}, [state]);
const handleFinishEditing = (
event: KeyboardEvent<Element> | FocusEvent<Element>
) => {
event.preventDefault();
onEditingChange(false);
onStateChange("initial");
onChange(ref.current?.textContent ?? "");
};
@@ -125,7 +140,7 @@ const EditableText = ({
};
const handleClick = () => {
onEditingChange(true);
onStateChange("editing");
};
return (
@@ -135,7 +150,10 @@ const EditableText = ({
onKeyDown={handleKeyDown}
onBlur={handleFinishEditing}
onClick={handleClick}
css={{ outline: "none", textOverflow: isEditing ? "clip" : "ellipsis" }}
css={{
outline: "none",
textOverflow: state === "editing" ? "clip" : "ellipsis",
}}
>
{label}
</Text>
@@ -172,20 +190,30 @@ const Item = styled(Button, {
position: "relative",
...menuCssVars({ show: false }),
"&:hover": menuCssVars({ show: true }),
variants: {
state: {
disabled: {
// @todo styling
opacity: 0.5,
},
editing: {},
initial: {},
},
},
});
type EditableItemProps = {
children: Array<JSX.Element | false>;
isEditing: boolean;
state: ItemState;
};
const EditableItem = ({ children, isEditing }: EditableItemProps) => {
const EditableItem = ({ children, state }: EditableItemProps) => {
const ref = useForceRecalcStyle<HTMLDivElement>(
"max-width",
isEditing === false
state === "editing"
);
return (
<Item variant="gray" ref={ref} as="div">
<Item variant="gray" state={state} ref={ref} as="div">
{children}
</Item>
);
@@ -194,34 +222,41 @@ const EditableItem = ({ children, isEditing }: EditableItemProps) => {
type StyleSourceProps = {
label: string;
hasMenu: boolean;
isEditing: boolean;
onEditingChange: (isEditing: boolean) => void;
onDuplicateItem: () => void;
onRemoveItem: () => void;
state: ItemState;
onStateChange: (state: ItemState) => void;
onDuplicate: () => void;
onRemove: () => void;
onChange: (value: string) => void;
};
export const StyleSource = ({
label,
hasMenu,
state,
onChange,
isEditing,
onEditingChange,
onStateChange,
...menuProps
}: StyleSourceProps) => {
return (
<EditableItem isEditing={isEditing}>
<EditableItem state={state}>
<EditableText
isEditing={isEditing}
onEditingChange={onEditingChange}
state={state}
onStateChange={onStateChange}
onChange={onChange}
label={label}
/>
{hasMenu === true && isEditing === false && (
{hasMenu === true && state !== "editing" && (
<Menu
{...menuProps}
state={state}
onEnable={() => {
onStateChange("initial");
}}
onDisable={() => {
onStateChange("disabled");
}}
onEdit={() => {
onEditingChange(true);
onStateChange("editing");
}}
/>
)}