From c7d7ede59aa29998c021baad49f8c8594cee27fa Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 11 Jan 2023 10:26:03 +0000 Subject: [PATCH] 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 --- .../style-panel/style-source-input/index.ts | 1 + .../style-source-input.stories.tsx | 83 +++++++++++++++-- .../style-source-input/style-source-input.tsx | 29 ++++-- .../style-source-input/style-source.tsx | 91 +++++++++++++------ 4 files changed, 160 insertions(+), 44 deletions(-) diff --git a/apps/designer/app/designer/features/style-panel/style-source-input/index.ts b/apps/designer/app/designer/features/style-panel/style-source-input/index.ts index a4729cd9f9..6a267f7d93 100644 --- a/apps/designer/app/designer/features/style-panel/style-source-input/index.ts +++ b/apps/designer/app/designer/features/style-panel/style-source-input/index.ts @@ -1 +1,2 @@ export * from "./style-source-input"; +export { type ItemState } from "./style-source"; diff --git a/apps/designer/app/designer/features/style-panel/style-source-input/style-source-input.stories.tsx b/apps/designer/app/designer/features/style-panel/style-source-input/style-source-input.stories.tsx index 35f149d122..8c8391d3a8 100644 --- a/apps/designer/app/designer/features/style-panel/style-source-input/style-source-input.stories.tsx +++ b/apps/designer/app/designer/features/style-panel/style-source-input/style-source-input.stories.tsx @@ -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 = [ - { 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 = () => { @@ -35,7 +49,13 @@ export const Initial: 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) => { @@ -59,7 +79,13 @@ export const WithItems: 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) => { @@ -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 = () => { const [value, setValue] = useState>([ - { id: "0", label: "Apple", type: "token", hasMenu: true }, + { + id: "0", + label: "Apple", + type: "token", + hasMenu: true, + state: "initial", + }, ]); const [editingIndex, setEditingIndex] = useState(-1); return ( @@ -121,7 +160,13 @@ export const WithMenu: ComponentStory = () => { 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 = () => { }) ); }} + 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, diff --git a/apps/designer/app/designer/features/style-panel/style-source-input/style-source-input.tsx b/apps/designer/app/designer/features/style-panel/style-source-input/style-source-input.tsx index 8b552b620f..7ce868bf81 100644 --- a/apps/designer/app/designer/features/style-panel/style-source-input/style-source-input.tsx +++ b/apps/designer/app/designer/features/style-panel/style-source-input/style-source-input.tsx @@ -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 = Omit, "value"> & @@ -40,6 +41,8 @@ type TextFieldBaseWrapperProps = Omit, "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) => ( { - 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 = { 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 = ( 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 = ( onRemoveItem={props.onRemoveItem} onChangeItem={props.onChangeItem} onDuplicateItem={props.onDuplicateItem} + onDisableItem={props.onDisableItem} + onEnableItem={props.onEnableItem} label={label} value={value} css={props.css} diff --git a/apps/designer/app/designer/features/style-panel/style-source-input/style-source.tsx b/apps/designer/app/designer/features/style-panel/style-source-input/style-source.tsx index 98ec427db3..eb6cd40275 100644 --- a/apps/designer/app/designer/features/style-panel/style-source-input/style-source.tsx +++ b/apps/designer/app/designer/features/style-panel/style-source-input/style-source.tsx @@ -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) => { Edit Name - + {props.state === "disabled" && ( + + Enable + + )} + {props.state !== "disabled" && ( + + Disable + + )} + Duplicate - + Remove @@ -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(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 | FocusEvent ) => { 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} @@ -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; - isEditing: boolean; + state: ItemState; }; -const EditableItem = ({ children, isEditing }: EditableItemProps) => { +const EditableItem = ({ children, state }: EditableItemProps) => { const ref = useForceRecalcStyle( "max-width", - isEditing === false + state === "editing" ); return ( - + {children} ); @@ -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 ( - + - {hasMenu === true && isEditing === false && ( + {hasMenu === true && state !== "editing" && ( { + onStateChange("initial"); + }} + onDisable={() => { + onStateChange("disabled"); + }} onEdit={() => { - onEditingChange(true); + onStateChange("editing"); }} /> )}