mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 04:55:06 +09:00
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:
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";
|
||||
+74
-9
@@ -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,
|
||||
|
||||
+22
-7
@@ -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}
|
||||
|
||||
+63
-28
@@ -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");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user