fix: Column tile reposition on focus state of Input element inside property pane configuration for Table and Tabs widget (#10046)

This commit is contained in:
Vicky Bansal 2022-02-04 16:29:54 +05:30 committed by GitHub
parent 324b077ffa
commit b7ebc7501e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 500 additions and 434 deletions

View File

@ -1,11 +1,11 @@
{ {
"tabWidget": ".t--draggable-tabswidget", "tabWidget": ".t--draggable-tabswidget",
"tabInput": ".t--draggable-tabswidget span.t--widget-name", "tabInput": ".t--draggable-tabswidget span.t--widget-name",
"tabName": ".t--property-control-tabs input", "tabName": ".t--property-control-tabs input",
"tabDefault": ".t--property-control-defaulttab .CodeMirror-code", "tabDefault": ".t--property-control-defaulttab .CodeMirror-code",
"tabButton": ".t--property-control-tabs button", "tabButton": ".t--property-control-tabs button",
"tabDelete": ".t--property-control-tabs .t--delete-tab-btn", "tabDelete": ".t--property-control-tabs .t--delete-column-btn",
"tabContainer": "div[type='TABS_WIDGET']", "tabContainer": "div[type='TABS_WIDGET']",
"tabEdit": ".t--property-control-tabs .t--edit-column-btn", "tabEdit": ".t--property-control-tabs .t--edit-column-btn",
"tabVisibility": ".t--property-control-visible .bp3-control-indicator" "tabVisibility": ".t--property-control-visible .bp3-control-indicator"
} }

View File

@ -60,10 +60,27 @@ const DraggableListWrapper = styled.div`
`; `;
function DraggableList(props: any) { function DraggableList(props: any) {
const { itemHeight, ItemRenderer, items, onUpdate } = props; const {
fixedHeight,
focusedIndex,
itemHeight,
ItemRenderer,
items,
onUpdate,
updateDragging,
} = props;
const listContainerHeight =
fixedHeight && fixedHeight < items.length * itemHeight
? fixedHeight
: items.length * itemHeight;
const shouldReRender = get(props, "shouldReRender", true); const shouldReRender = get(props, "shouldReRender", true);
// order of items in the list // order of items in the list
const order = useRef<any>(items.map((_: any, index: any) => index)); const order = useRef<any>(items.map((_: any, index: any) => index));
const displacement = useRef<number>(0);
const dragging = useRef<boolean>(false);
const listRef = useRef<HTMLDivElement | null>(null);
const onDrop = (originalIndex: number, newIndex: number) => { const onDrop = (originalIndex: number, newIndex: number) => {
onUpdate(order.current, originalIndex, newIndex); onUpdate(order.current, originalIndex, newIndex);
@ -82,6 +99,29 @@ function DraggableList(props: any) {
} }
}, [items]); }, [items]);
useEffect(() => {
if (focusedIndex && listRef && listRef.current) {
const container = listRef.current;
if (focusedIndex * itemHeight < container.scrollTop) {
listRef.current.scrollTo({
top: (focusedIndex - 1) * itemHeight,
left: 0,
behavior: "smooth",
});
} else if (
(focusedIndex + 1) * itemHeight >
listRef.current.scrollTop + listRef.current.clientHeight
) {
listRef.current.scrollTo({
top: (focusedIndex + 1) * itemHeight - listRef.current.clientHeight,
left: 0,
behavior: "smooth",
});
}
}
}, [focusedIndex]);
const [springs, setSprings] = useSprings<any>( const [springs, setSprings] = useSprings<any>(
items.length, items.length,
updateSpringStyles(order.current, itemHeight), updateSpringStyles(order.current, itemHeight),
@ -90,60 +130,122 @@ function DraggableList(props: any) {
const bind: any = useDrag<any>((props: any) => { const bind: any = useDrag<any>((props: any) => {
const originalIndex = props.args[0]; const originalIndex = props.args[0];
const curIndex = order.current.indexOf(originalIndex); const curIndex = order.current.indexOf(originalIndex);
const curRow = clamp( const pointerFromTop = props.xy[1];
Math.round((curIndex * itemHeight + props.movement[1]) / itemHeight), if (listRef && listRef.current) {
0, const containerCoordinates = listRef?.current.getBoundingClientRect();
items.length - 1, const container = listRef.current;
); if (containerCoordinates) {
const newOrder = swap(order.current, curIndex, curRow); const containerDistanceFromTop = containerCoordinates.top;
setSprings( if (props.dragging) {
dragIdleSpringStyles(newOrder, { if (pointerFromTop < containerDistanceFromTop + itemHeight / 2) {
down: props.down, // Scroll inside container till first element in list is completely visible
originalIndex, if (container.scrollTop > 0) {
curIndex, container.scrollTop -= itemHeight / 10;
y: props.movement[1], }
itemHeight, } else if (
}), pointerFromTop >=
); containerDistanceFromTop + container.clientHeight - itemHeight / 2
if (curRow !== curIndex) { ) {
// Feed springs new style data, they'll animate the view without causing a single render // Scroll inside container till container cannnot be scrolled more towards bottom
if (!props.down) { if (
order.current = newOrder; container.scrollTop <=
setSprings(updateSpringStyles(order.current, itemHeight)); springs.length * itemHeight -
debounce(onDrop, 400)(curIndex, curRow); container.clientHeight -
itemHeight / 2
) {
container.scrollTop += itemHeight / 10;
}
}
// finding distance of current pointer from the top of the container to find the final position
// currIndex * itemHeight for the initial position
// subtraction formar with latter for displacement
displacement.current =
pointerFromTop -
containerDistanceFromTop +
container.scrollTop -
curIndex * itemHeight -
itemHeight / 2;
if (!dragging.current && Math.abs(displacement.current) > 10) {
dragging.current = props.dragging;
updateDragging(dragging.current);
}
} else {
if (dragging.current) {
dragging.current = props.dragging;
updateDragging(dragging.current);
}
}
const curRow = clamp(
Math.round(
(curIndex * itemHeight + displacement.current) / itemHeight,
),
0,
items.length - 1,
);
const newOrder = swap(order.current, curIndex, curRow);
setSprings(
dragIdleSpringStyles(newOrder, {
down: props.down,
originalIndex,
curIndex,
y: Math.abs(displacement.current) > 10 ? displacement.current : 0,
itemHeight,
}),
);
if (curRow !== curIndex) {
// Feed springs new style data, they'll animate the view without causing a single render
if (!props.down) {
order.current = newOrder;
setSprings(updateSpringStyles(order.current, itemHeight));
debounce(onDrop, 400)(curIndex, curRow);
}
}
} }
} }
}); });
return ( return (
<DraggableListWrapper <div
className="content" ref={listRef}
onMouseDown={() => { style={{
// set events to null to stop other parent draggable elements execution(ex: Property pane) height: listContainerHeight,
document.onmouseup = null; overflowY: "auto",
document.onmousemove = null; zIndex: 1,
}} }}
style={{ height: items.length * itemHeight }}
> >
{springs.map(({ scale, y, zIndex }, i) => ( <DraggableListWrapper
<animated.div className="content"
{...bind(i)} onMouseDown={() => {
data-rbd-draggable-id={items[i].id} // set events to null to stop other parent draggable elements execution(ex: Property pane)
key={i} document.onmouseup = null;
style={{ document.onmousemove = null;
zIndex, }}
width: "100%", style={{
transform: to( height: "100%",
[y, scale], }}
(y, s) => `translate3d(0,${y}px,0) scale(${s})`, >
), {springs.map(({ scale, y, zIndex }, i) => (
}} <animated.div
> {...bind(i)}
<div> data-rbd-draggable-id={items[i].id}
<ItemRenderer index={i} item={items[i]} /> key={i}
</div> style={{
</animated.div> zIndex,
))} width: "100%",
</DraggableListWrapper> transform: to(
[y, scale],
(y, s) => `translate3d(0,${y}px,0) scale(${s})`,
),
}}
>
<div>
<ItemRenderer index={i} item={items[i]} />
</div>
</animated.div>
))}
</DraggableListWrapper>
</div>
); );
} }
DraggableList.displayName = "DraggableList"; DraggableList.displayName = "DraggableList";

View File

@ -0,0 +1,162 @@
import React, { useCallback, useState, useRef, useEffect } from "react";
import styled from "styled-components";
import _ from "lodash";
import {
StyledDragIcon,
StyledOptionControlInputGroup,
StyledEditIcon,
StyledDeleteIcon,
StyledVisibleIcon,
StyledHiddenIcon,
} from "components/propertyControls/StyledControls";
import { Colors } from "constants/Colors";
const ItemWrapper = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
&.has-duplicate-label > div:nth-child(2) {
border: 1px solid ${Colors.DANGER_SOLID};
}
`;
type RenderComponentProps = {
focusedIndex: number | null | undefined;
index: number;
item: {
label: string;
isDerived?: boolean;
isVisible?: boolean;
isDuplicateLabel?: boolean;
};
isDelete?: boolean;
isDragging: boolean;
placeholder: string;
updateFocus?: (index: number, isFocused: boolean) => void;
updateOption: (index: number, value: string) => void;
onEdit?: (index: number) => void;
deleteOption: (index: number) => void;
toggleVisibility?: (index: number) => void;
};
export function DraggableListCard(props: RenderComponentProps) {
const [value, setValue] = useState(props.item.label);
const [isEditing, setEditing] = useState(false);
const {
deleteOption,
focusedIndex,
index,
isDelete,
isDragging,
item,
onEdit,
placeholder,
toggleVisibility,
updateFocus,
updateOption,
} = props;
const [visibility, setVisibility] = useState(item.isVisible);
const ref = useRef<HTMLInputElement | null>(null);
const debouncedUpdate = _.debounce(updateOption, 1000);
useEffect(() => {
if (!isEditing && item && item.label) setValue(item.label);
}, [item?.label, isEditing]);
useEffect(() => {
if (focusedIndex !== null && focusedIndex === index && !isDragging) {
if (ref && ref.current) {
ref?.current.focus();
}
} else if (isDragging && focusedIndex === index) {
if (ref && ref.current) {
ref?.current.blur();
}
}
}, [focusedIndex, isDragging]);
const onChange = useCallback(
(index: number, value: string) => {
setValue(value);
debouncedUpdate(index, value);
},
[updateOption],
);
const onFocus = () => {
setEditing(false);
if (updateFocus) {
updateFocus(index, true);
}
};
const onBlur = () => {
if (!isDragging) {
setEditing(false);
if (updateFocus) {
updateFocus(index, false);
}
}
};
return (
<ItemWrapper
className={props.item.isDuplicateLabel ? "has-duplicate-label" : ""}
>
<StyledDragIcon height={20} width={20} />
<StyledOptionControlInputGroup
autoFocus={index === focusedIndex}
dataType="text"
onBlur={onBlur}
onChange={(value: string) => {
onChange(index, value);
}}
onFocus={onFocus}
placeholder={placeholder}
ref={ref}
value={value}
width="100%"
/>
<StyledEditIcon
className="t--edit-column-btn"
height={20}
onClick={() => {
onEdit && onEdit(index);
}}
width={20}
/>
{!!item.isDerived || isDelete ? (
<StyledDeleteIcon
className="t--delete-column-btn"
height={20}
onClick={() => {
deleteOption && deleteOption(index);
}}
width={20}
/>
) : visibility ? (
<StyledVisibleIcon
className="t--show-column-btn"
height={20}
onClick={() => {
setVisibility(!visibility);
toggleVisibility && toggleVisibility(index);
}}
width={20}
/>
) : (
<StyledHiddenIcon
className="t--show-column-btn"
height={20}
onClick={() => {
setVisibility(!visibility);
toggleVisibility && toggleVisibility(index);
}}
width={20}
/>
)}
</ItemWrapper>
);
}

View File

@ -3,11 +3,13 @@ import React from "react";
import DraggableList from "./DraggableList"; import DraggableList from "./DraggableList";
type RenderComponentProps = { type RenderComponentProps = {
focusedIndex: number | null | undefined;
index: number; index: number;
item: { item: {
label: string; label: string;
isDerived?: boolean; isDerived?: boolean;
}; };
isDragging: boolean;
deleteOption: (index: number) => void; deleteOption: (index: number) => void;
updateOption: (index: number, value: string) => void; updateOption: (index: number, value: string) => void;
toggleVisibility?: (index: number) => void; toggleVisibility?: (index: number) => void;
@ -16,6 +18,8 @@ type RenderComponentProps = {
}; };
interface DroppableComponentProps { interface DroppableComponentProps {
fixedHeight?: number | boolean;
focusedIndex?: number | null | undefined;
items: Array<Record<string, unknown>>; items: Array<Record<string, unknown>>;
itemHeight: number; itemHeight: number;
renderComponent: (props: RenderComponentProps) => JSX.Element; renderComponent: (props: RenderComponentProps) => JSX.Element;
@ -34,11 +38,18 @@ export class DroppableComponent extends React.Component<
super(props); super(props);
} }
shouldComponentUpdate(prevProps: DroppableComponentProps) { public readonly state = {
isDragging: false,
};
shouldComponentUpdate(prevProps: DroppableComponentProps, prevState: any) {
const presentOrder = this.props.items.map(this.getVisibleObject); const presentOrder = this.props.items.map(this.getVisibleObject);
const previousOrder = prevProps.items.map(this.getVisibleObject); const previousOrder = prevProps.items.map(this.getVisibleObject);
return (
return !isEqual(presentOrder, previousOrder); !isEqual(presentOrder, previousOrder) ||
this.props.focusedIndex !== prevProps.focusedIndex ||
prevState.isDragging !== this.state.isDragging
);
} }
getVisibleObject(item: Record<string, unknown>) { getVisibleObject(item: Record<string, unknown>) {
@ -52,14 +63,26 @@ export class DroppableComponent extends React.Component<
}; };
} }
onUpdate = (itemsOrder: number[]) => { onUpdate = (
itemsOrder: number[],
originalIndex: number,
newIndex: number,
) => {
const newOrderedItems = itemsOrder.map((each) => this.props.items[each]); const newOrderedItems = itemsOrder.map((each) => this.props.items[each]);
this.props.updateItems(newOrderedItems); this.props.updateItems(newOrderedItems);
if (this.props.updateFocus && originalIndex !== newIndex) {
this.props.updateFocus(newIndex, true);
}
};
updateDragging = (isDragging: boolean) => {
this.setState({ isDragging });
}; };
renderItem = ({ index, item }: any) => { renderItem = ({ index, item }: any) => {
const { const {
deleteOption, deleteOption,
focusedIndex,
onEdit, onEdit,
renderComponent, renderComponent,
toggleVisibility, toggleVisibility,
@ -73,8 +96,10 @@ export class DroppableComponent extends React.Component<
updateOption, updateOption,
toggleVisibility, toggleVisibility,
onEdit, onEdit,
focusedIndex,
item, item,
index, index,
isDragging: this.state.isDragging,
}); });
}; };
@ -82,10 +107,13 @@ export class DroppableComponent extends React.Component<
return ( return (
<DraggableList <DraggableList
ItemRenderer={this.renderItem} ItemRenderer={this.renderItem}
fixedHeight={this.props.fixedHeight}
focusedIndex={this.props.focusedIndex}
itemHeight={45} itemHeight={45}
items={this.props.items} items={this.props.items}
onUpdate={this.onUpdate} onUpdate={this.onUpdate}
shouldReRender={false} shouldReRender={false}
updateDragging={this.updateDragging}
/> />
); );
} }

View File

@ -17,6 +17,7 @@ type RenderComponentProps = {
isDerived?: boolean; isDerived?: boolean;
}; };
deleteOption: (index: number) => void; deleteOption: (index: number) => void;
updateCurrentFocusedInput: (index: number | null) => void;
updateOption: (index: number, value: string) => void; updateOption: (index: number, value: string) => void;
toggleVisibility?: (index: number) => void; toggleVisibility?: (index: number) => void;
onEdit?: (index: number) => void; onEdit?: (index: number) => void;
@ -26,6 +27,7 @@ interface DroppableComponentProps {
items: Array<Record<string, unknown>>; items: Array<Record<string, unknown>>;
renderComponent: (props: RenderComponentProps) => JSX.Element; renderComponent: (props: RenderComponentProps) => JSX.Element;
deleteOption: (index: number) => void; deleteOption: (index: number) => void;
updateCurrentFocusedInput: (index: number | null) => void;
updateOption: (index: number, value: string) => void; updateOption: (index: number, value: string) => void;
toggleVisibility?: (index: number) => void; toggleVisibility?: (index: number) => void;
updateItems: (items: Array<Record<string, unknown>>) => void; updateItems: (items: Array<Record<string, unknown>>) => void;
@ -85,6 +87,7 @@ export class DroppableComponent extends React.Component<
onEdit, onEdit,
renderComponent, renderComponent,
toggleVisibility, toggleVisibility,
updateCurrentFocusedInput,
updateOption, updateOption,
} = this.props; } = this.props;
return ( return (
@ -116,6 +119,7 @@ export class DroppableComponent extends React.Component<
> >
{renderComponent({ {renderComponent({
deleteOption, deleteOption,
updateCurrentFocusedInput,
updateOption, updateOption,
toggleVisibility, toggleVisibility,
onEdit, onEdit,

View File

@ -1,20 +1,15 @@
import React, { useCallback, useEffect, useState } from "react"; import React from "react";
import BaseControl, { ControlProps } from "./BaseControl"; import BaseControl, { ControlProps } from "./BaseControl";
import { import { StyledPropertyPaneButton } from "./StyledControls";
StyledPropertyPaneButton,
StyledDragIcon,
StyledDeleteIcon,
StyledEditIcon,
StyledOptionControlInputGroup,
} from "./StyledControls";
import styled from "constants/DefaultTheme"; import styled from "constants/DefaultTheme";
import { generateReactKey } from "utils/generators"; import { generateReactKey } from "utils/generators";
import { DroppableComponent } from "components/ads/DraggableListComponent"; import { DroppableComponent } from "components/ads/DraggableListComponent";
import { getNextEntityName } from "utils/AppsmithUtils"; import { getNextEntityName } from "utils/AppsmithUtils";
import _, { debounce } from "lodash"; import _ from "lodash";
import { Category, Size } from "components/ads/Button"; import { Category, Size } from "components/ads/Button";
import { Colors } from "constants/Colors"; import { Colors } from "constants/Colors";
import { ButtonPlacementTypes } from "components/constants"; import { ButtonPlacementTypes } from "components/constants";
import { DraggableListCard } from "components/ads/DraggableListCard";
const StyledPropertyPaneButtonWrapper = styled.div` const StyledPropertyPaneButtonWrapper = styled.div`
display: flex; display: flex;
@ -23,12 +18,6 @@ const StyledPropertyPaneButtonWrapper = styled.div`
margin-top: 10px; margin-top: 10px;
`; `;
const ButtonWrapper = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
`;
const ButtonListWrapper = styled.div` const ButtonListWrapper = styled.div`
width: 100%; width: 100%;
display: flex; display: flex;
@ -40,77 +29,28 @@ const AddNewButton = styled(StyledPropertyPaneButton)`
flex-grow: 1; flex-grow: 1;
`; `;
type RenderComponentProps = { type State = {
index: number; focusedIndex: number | null;
item: {
label: string;
isVisible?: boolean;
};
deleteOption: (index: number) => void;
updateOption: (index: number, value: string) => void;
toggleVisibility?: (index: number) => void;
onEdit?: (props: any) => void;
}; };
function GroupButtonComponent(props: RenderComponentProps) { class ButtonListControl extends BaseControl<ControlProps, State> {
const { deleteOption, index, item, updateOption } = props; constructor(props: ControlProps) {
super(props);
const [value, setValue] = useState(item.label); this.state = {
const [isEditing, setEditing] = useState(false); focusedIndex: null,
};
}
useEffect(() => { componentDidUpdate(prevProps: ControlProps): void {
if (!isEditing && item && item.label) setValue(item.label); //on adding a new column last column should get focused
}, [item?.label, isEditing]); if (
Object.keys(prevProps.propertyValue).length + 1 ===
const debouncedUpdate = debounce(updateOption, 1000); Object.keys(this.props.propertyValue).length
const onChange = useCallback( ) {
(index: number, value: string) => { this.updateFocus(Object.keys(this.props.propertyValue).length - 1, true);
setValue(value); }
debouncedUpdate(index, value); }
},
[updateOption],
);
const handleChange = useCallback(() => props.onEdit && props.onEdit(index), [
index,
]);
const onFocus = () => setEditing(true);
const onBlur = () => setEditing(false);
return (
<ButtonWrapper>
<StyledDragIcon height={20} width={20} />
<StyledOptionControlInputGroup
dataType="text"
onBlur={onBlur}
onChange={(value: string) => {
onChange(index, value);
}}
onFocus={onFocus}
placeholder="Button label"
trimValue={false}
value={value}
/>
<StyledDeleteIcon
className="t--delete-tab-btn"
height={20}
marginRight={12}
onClick={() => {
deleteOption(index);
}}
width={20}
/>
<StyledEditIcon
className="t--edit-column-btn"
height={20}
onClick={handleChange}
width={20}
/>
</ButtonWrapper>
);
}
class ButtonListControl extends BaseControl<ControlProps> {
updateItems = (items: Array<Record<string, any>>) => { updateItems = (items: Array<Record<string, any>>) => {
const menuItems = items.reduce((obj: any, each: any, index: number) => { const menuItems = items.reduce((obj: any, each: any, index: number) => {
obj[each.id] = { obj[each.id] = {
@ -146,11 +86,20 @@ class ButtonListControl extends BaseControl<ControlProps> {
<ButtonListWrapper> <ButtonListWrapper>
<DroppableComponent <DroppableComponent
deleteOption={this.deleteOption} deleteOption={this.deleteOption}
fixedHeight={370}
focusedIndex={this.state.focusedIndex}
itemHeight={45} itemHeight={45}
items={menuItems} items={menuItems}
onEdit={this.onEdit} onEdit={this.onEdit}
renderComponent={GroupButtonComponent} renderComponent={(props) =>
DraggableListCard({
...props,
isDelete: true,
placeholder: "Button label",
})
}
toggleVisibility={this.toggleVisibility} toggleVisibility={this.toggleVisibility}
updateFocus={this.updateFocus}
updateItems={this.updateItems} updateItems={this.updateItems}
updateOption={this.updateOption} updateOption={this.updateOption}
/> />
@ -246,6 +195,10 @@ class ButtonListControl extends BaseControl<ControlProps> {
this.updateProperty(this.props.propertyName, groupButtons); this.updateProperty(this.props.propertyName, groupButtons);
}; };
updateFocus = (index: number, isFocused: boolean) => {
this.setState({ focusedIndex: isFocused ? index : null });
};
static getControlType() { static getControlType() {
return "GROUP_BUTTONS"; return "GROUP_BUTTONS";
} }

View File

@ -1,18 +1,13 @@
import React, { useCallback, useEffect, useState } from "react"; import React from "react";
import BaseControl, { ControlProps } from "./BaseControl"; import BaseControl, { ControlProps } from "./BaseControl";
import { import { StyledPropertyPaneButton } from "./StyledControls";
StyledPropertyPaneButton,
StyledDragIcon,
StyledDeleteIcon,
StyledEditIcon,
StyledOptionControlInputGroup,
} from "./StyledControls";
import styled from "constants/DefaultTheme"; import styled from "constants/DefaultTheme";
import { generateReactKey } from "utils/generators"; import { generateReactKey } from "utils/generators";
import { DroppableComponent } from "components/ads/DraggableListComponent"; import { DroppableComponent } from "components/ads/DraggableListComponent";
import { getNextEntityName } from "utils/AppsmithUtils"; import { getNextEntityName } from "utils/AppsmithUtils";
import _, { debounce, orderBy } from "lodash"; import _, { orderBy } from "lodash";
import { Category, Size } from "components/ads/Button"; import { Category, Size } from "components/ads/Button";
import { DraggableListCard } from "components/ads/DraggableListCard";
const StyledPropertyPaneButtonWrapper = styled.div` const StyledPropertyPaneButtonWrapper = styled.div`
display: flex; display: flex;
@ -21,12 +16,6 @@ const StyledPropertyPaneButtonWrapper = styled.div`
margin-top: 10px; margin-top: 10px;
`; `;
const ItemWrapper = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
`;
const MenuItemsWrapper = styled.div` const MenuItemsWrapper = styled.div`
width: 100%; width: 100%;
display: flex; display: flex;
@ -38,77 +27,30 @@ const AddMenuItemButton = styled(StyledPropertyPaneButton)`
flex-grow: 1; flex-grow: 1;
`; `;
type RenderComponentProps = { type State = {
index: number; focusedIndex: number | null;
item: {
label: string;
isVisible?: boolean;
};
deleteOption: (index: number) => void;
updateOption: (index: number, value: string) => void;
toggleVisibility?: (index: number) => void;
onEdit?: (props: any) => void;
}; };
function MenuItemComponent(props: RenderComponentProps) { class MenuItemsControl extends BaseControl<ControlProps, State> {
const { deleteOption, index, item, updateOption } = props; constructor(props: ControlProps) {
super(props);
const [value, setValue] = useState(item.label); this.state = {
const [isEditing, setEditing] = useState(false); focusedIndex: null,
};
}
useEffect(() => { componentDidUpdate(prevProps: ControlProps): void {
if (!isEditing && item && item.label) setValue(item.label); //on adding a new column last column should get focused
}, [item?.label, isEditing]); if (
prevProps.propertyValue &&
const debouncedUpdate = debounce(updateOption, 1000); this.props.propertyValue &&
const onChange = useCallback( Object.keys(prevProps.propertyValue).length + 1 ===
(index: number, value: string) => { Object.keys(this.props.propertyValue).length
setValue(value); ) {
debouncedUpdate(index, value); this.updateFocus(Object.keys(this.props.propertyValue).length - 1, true);
}, }
[updateOption], }
);
const handleChange = useCallback(() => props.onEdit && props.onEdit(index), [
index,
]);
const onFocus = () => setEditing(true);
const onBlur = () => setEditing(false);
return (
<ItemWrapper>
<StyledDragIcon height={20} width={20} />
<StyledOptionControlInputGroup
dataType="text"
onBlur={onBlur}
onChange={(value: string) => {
onChange(index, value);
}}
onFocus={onFocus}
placeholder="Menu item label"
trimValue={false}
value={value}
/>
<StyledDeleteIcon
className="t--delete-tab-btn"
height={20}
marginRight={12}
onClick={() => {
deleteOption(index);
}}
width={20}
/>
<StyledEditIcon
className="t--edit-column-btn"
height={20}
onClick={handleChange}
width={20}
/>
</ItemWrapper>
);
}
class MenuItemsControl extends BaseControl<ControlProps> {
updateItems = (items: Array<Record<string, any>>) => { updateItems = (items: Array<Record<string, any>>) => {
const menuItems = items.reduce((obj: any, each: any, index: number) => { const menuItems = items.reduce((obj: any, each: any, index: number) => {
obj[each.id] = { obj[each.id] = {
@ -146,11 +88,20 @@ class MenuItemsControl extends BaseControl<ControlProps> {
<MenuItemsWrapper> <MenuItemsWrapper>
<DroppableComponent <DroppableComponent
deleteOption={this.deleteOption} deleteOption={this.deleteOption}
fixedHeight={370}
focusedIndex={this.state.focusedIndex}
itemHeight={45} itemHeight={45}
items={orderBy(menuItems, ["index"], ["asc"])} items={orderBy(menuItems, ["index"], ["asc"])}
onEdit={this.onEdit} onEdit={this.onEdit}
renderComponent={MenuItemComponent} renderComponent={(props) =>
DraggableListCard({
...props,
isDelete: true,
placeholder: "Menu item label",
})
}
toggleVisibility={this.toggleVisibility} toggleVisibility={this.toggleVisibility}
updateFocus={this.updateFocus}
updateItems={this.updateItems} updateItems={this.updateItems}
updateOption={this.updateOption} updateOption={this.updateOption}
/> />
@ -243,6 +194,10 @@ class MenuItemsControl extends BaseControl<ControlProps> {
this.updateProperty(this.props.propertyName, menuItems); this.updateProperty(this.props.propertyName, menuItems);
}; };
updateFocus = (index: number, isFocused: boolean) => {
this.setState({ focusedIndex: isFocused ? index : null });
};
static getControlType() { static getControlType() {
return "MENU_ITEMS"; return "MENU_ITEMS";
} }

View File

@ -1,19 +1,11 @@
import React, { useCallback, useEffect, useState, Component } from "react"; import React, { Component } from "react";
import { AppState } from "reducers"; import { AppState } from "reducers";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Placement } from "popper.js"; import { Placement } from "popper.js";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import _ from "lodash"; import _ from "lodash";
import BaseControl, { ControlProps } from "./BaseControl"; import BaseControl, { ControlProps } from "./BaseControl";
import { import { StyledPropertyPaneButton } from "./StyledControls";
StyledDragIcon,
StyledEditIcon,
StyledDeleteIcon,
StyledVisibleIcon,
StyledHiddenIcon,
StyledPropertyPaneButton,
StyledOptionControlInputGroup,
} from "./StyledControls";
import styled from "constants/DefaultTheme"; import styled from "constants/DefaultTheme";
import { Indices } from "constants/Layers"; import { Indices } from "constants/Layers";
import { DroppableComponent } from "components/ads/DraggableListComponent"; import { DroppableComponent } from "components/ads/DraggableListComponent";
@ -37,17 +29,7 @@ import {
PropertyEvaluationErrorType, PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils"; } from "utils/DynamicBindingUtils";
import { getNextEntityName } from "utils/AppsmithUtils"; import { getNextEntityName } from "utils/AppsmithUtils";
import { Colors } from "constants/Colors"; import { DraggableListCard } from "components/ads/DraggableListCard";
import { noop } from "utils/AppsmithUtils";
const ItemWrapper = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
&.has-duplicate-label > div:nth-child(2) {
border: 1px solid ${Colors.DANGER_SOLID};
}
`;
const TabsWrapper = styled.div` const TabsWrapper = styled.div`
width: 100%; width: 100%;
@ -83,21 +65,6 @@ type EvaluatedValuePopupWrapperProps = ReduxStateProps & {
children: JSX.Element; children: JSX.Element;
}; };
type RenderComponentProps = {
index: number;
item: {
label: string;
isDerived?: boolean;
isVisible?: boolean;
isDuplicateLabel?: boolean;
};
updateFocus?: (index: number, isFocused: boolean) => void;
updateOption: (index: number, value: string) => void;
onEdit?: (index: number) => void;
deleteOption: (index: number) => void;
toggleVisibility?: (index: number) => void;
};
const getOriginalColumn = ( const getOriginalColumn = (
columns: Record<string, ColumnProperties>, columns: Record<string, ColumnProperties>,
index: number, index: number,
@ -110,107 +77,6 @@ const getOriginalColumn = (
return column; return column;
}; };
function ColumnControlComponent(props: RenderComponentProps) {
const [value, setValue] = useState(props.item.label);
const [isEditing, setEditing] = useState(false);
useEffect(() => {
if (!isEditing && props.item && props.item.label)
setValue(props.item.label);
}, [props.item?.label, isEditing]);
const {
deleteOption,
index,
item,
onEdit,
toggleVisibility,
updateFocus,
updateOption,
} = props;
const [visibility, setVisibility] = useState(item.isVisible);
useEffect(() => {
setVisibility(item.isVisible);
}, [item.isVisible]);
const debouncedUpdate = _.debounce(updateOption, 1000);
const debouncedFocus = updateFocus ? _.debounce(updateFocus, 400) : noop;
const onChange = useCallback(
(index: number, value: string) => {
setValue(value);
debouncedUpdate(index, value);
},
[updateOption],
);
const onFocus = () => {
setEditing(false);
debouncedFocus(index, true);
};
const onBlur = () => {
setEditing(false);
debouncedFocus(index, false);
};
return (
<ItemWrapper
className={props.item.isDuplicateLabel ? "has-duplicate-label" : ""}
>
<StyledDragIcon height={20} width={20} />
<StyledOptionControlInputGroup
dataType="text"
onBlur={onBlur}
onChange={(value: string) => {
onChange(index, value);
}}
onFocus={onFocus}
placeholder="Column Title"
trimValue={false}
value={value}
width="100%"
/>
<StyledEditIcon
className="t--edit-column-btn"
height={20}
onClick={() => {
onEdit && onEdit(index);
}}
width={20}
/>
{!!item.isDerived ? (
<StyledDeleteIcon
className="t--delete-column-btn"
height={20}
onClick={() => {
deleteOption && deleteOption(index);
}}
width={20}
/>
) : visibility ? (
<StyledVisibleIcon
className="t--show-column-btn"
height={20}
onClick={() => {
setVisibility(!visibility);
toggleVisibility && toggleVisibility(index);
}}
width={20}
/>
) : (
<StyledHiddenIcon
className="t--show-column-btn"
height={20}
onClick={() => {
setVisibility(!visibility);
toggleVisibility && toggleVisibility(index);
}}
width={20}
/>
)}
</ItemWrapper>
);
}
type State = { type State = {
focusedIndex: number | null; focusedIndex: number | null;
duplicateColumnIds: string[]; duplicateColumnIds: string[];
@ -241,6 +107,16 @@ class PrimaryColumnsControl extends BaseControl<ControlProps, State> {
}; };
} }
componentDidUpdate(prevProps: ControlProps): void {
//on adding a new column last column should get focused
if (
Object.keys(prevProps.propertyValue).length + 1 ===
Object.keys(this.props.propertyValue).length
) {
this.updateFocus(Object.keys(this.props.propertyValue).length - 1, true);
}
}
render() { render() {
// Get columns from widget properties // Get columns from widget properties
const columns: Record<string, ColumnProperties> = const columns: Record<string, ColumnProperties> =
@ -291,10 +167,18 @@ class PrimaryColumnsControl extends BaseControl<ControlProps, State> {
<EvaluatedValuePopupWrapper {...this.props} isFocused={isFocused}> <EvaluatedValuePopupWrapper {...this.props} isFocused={isFocused}>
<DroppableComponent <DroppableComponent
deleteOption={this.deleteOption} deleteOption={this.deleteOption}
fixedHeight={370}
focusedIndex={this.state.focusedIndex}
itemHeight={45} itemHeight={45}
items={draggableComponentColumns} items={draggableComponentColumns}
onEdit={this.onEdit} onEdit={this.onEdit}
renderComponent={ColumnControlComponent} renderComponent={(props) =>
DraggableListCard({
...props,
isDelete: false,
placeholder: "Column Title",
})
}
toggleVisibility={this.toggleVisibility} toggleVisibility={this.toggleVisibility}
updateFocus={this.updateFocus} updateFocus={this.updateFocus}
updateItems={this.updateItems} updateItems={this.updateItems}
@ -445,6 +329,8 @@ class PrimaryColumnsControl extends BaseControl<ControlProps, State> {
this.setState({ focusedIndex: isFocused ? index : null }); this.setState({ focusedIndex: isFocused ? index : null });
}; };
// updateCurrentFocusedInput = (index: number | null) => {};
static getControlType() { static getControlType() {
return "PRIMARY_COLUMNS"; return "PRIMARY_COLUMNS";
} }

View File

@ -1,21 +1,16 @@
import React, { useCallback, useEffect, useState } from "react"; import React from "react";
import BaseControl, { ControlProps } from "./BaseControl"; import BaseControl, { ControlProps } from "./BaseControl";
import { import { StyledPropertyPaneButton } from "./StyledControls";
StyledPropertyPaneButton,
StyledDragIcon,
StyledDeleteIcon,
StyledEditIcon,
StyledOptionControlInputGroup,
} from "./StyledControls";
import styled from "constants/DefaultTheme"; import styled from "constants/DefaultTheme";
import { generateReactKey } from "utils/generators"; import { generateReactKey } from "utils/generators";
import { DroppableComponent } from "components/ads/DraggableListComponent"; import { DroppableComponent } from "components/ads/DraggableListComponent";
import { getNextEntityName, noop } from "utils/AppsmithUtils"; import { getNextEntityName, noop } from "utils/AppsmithUtils";
import _, { debounce, orderBy } from "lodash"; import _, { orderBy } from "lodash";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { Category, Size } from "components/ads/Button"; import { Category, Size } from "components/ads/Button";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { ReduxActionTypes } from "constants/ReduxActionConstants"; import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { DraggableListCard } from "components/ads/DraggableListCard";
const StyledPropertyPaneButtonWrapper = styled.div` const StyledPropertyPaneButtonWrapper = styled.div`
display: flex; display: flex;
@ -24,12 +19,6 @@ const StyledPropertyPaneButtonWrapper = styled.div`
margin-top: 10px; margin-top: 10px;
`; `;
const ItemWrapper = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
`;
const TabsWrapper = styled.div` const TabsWrapper = styled.div`
width: 100%; width: 100%;
display: flex; display: flex;
@ -37,12 +26,15 @@ const TabsWrapper = styled.div`
`; `;
type RenderComponentProps = { type RenderComponentProps = {
focusedIndex: number | null | undefined;
index: number; index: number;
isDragging: boolean;
item: { item: {
label: string; label: string;
isVisible?: boolean; isVisible?: boolean;
}; };
deleteOption: (index: number) => void; deleteOption: (index: number) => void;
updateFocus?: (index: number, isFocused: boolean) => void;
updateOption: (index: number, value: string) => void; updateOption: (index: number, value: string) => void;
toggleVisibility?: (index: number) => void; toggleVisibility?: (index: number) => void;
onEdit?: (props: any) => void; onEdit?: (props: any) => void;
@ -74,7 +66,7 @@ function AddTabButtonComponent({ widgetId }: any) {
} }
function TabControlComponent(props: RenderComponentProps) { function TabControlComponent(props: RenderComponentProps) {
const { index, item, updateOption } = props; const { index, item } = props;
const dispatch = useDispatch(); const dispatch = useDispatch();
const deleteOption = () => { const deleteOption = () => {
dispatch({ dispatch({
@ -83,65 +75,42 @@ function TabControlComponent(props: RenderComponentProps) {
}); });
}; };
const [value, setValue] = useState(item.label);
const [isEditing, setEditing] = useState(false);
useEffect(() => {
if (!isEditing && item && item.label) setValue(item.label);
}, [item?.label, isEditing]);
const debouncedUpdate = debounce(updateOption, 1000);
const handleChange = useCallback(() => props.onEdit && props.onEdit(index), [
index,
]);
const onChange = useCallback(
(index: number, value: string) => {
setValue(value);
debouncedUpdate(index, value);
},
[updateOption],
);
const onFocus = () => setEditing(true);
const onBlur = () => setEditing(false);
return ( return (
<ItemWrapper> <DraggableListCard
<StyledDragIcon height={20} width={20} /> {...props}
<StyledOptionControlInputGroup deleteOption={deleteOption}
dataType="text" isDelete
onBlur={onBlur} placeholder="Tab Title"
onChange={(value: string) => { />
onChange(index, value);
}}
onFocus={onFocus}
placeholder="Tab Title"
trimValue={false}
value={value}
/>
<StyledDeleteIcon
className="t--delete-tab-btn"
height={20}
marginRight={12}
onClick={deleteOption}
width={20}
/>
<StyledEditIcon
className="t--edit-column-btn"
height={20}
onClick={handleChange}
width={20}
/>
</ItemWrapper>
); );
} }
class TabControl extends BaseControl<ControlProps> { type State = {
focusedIndex: number | null;
};
class TabControl extends BaseControl<ControlProps, State> {
constructor(props: ControlProps) {
super(props);
this.state = {
focusedIndex: null,
};
}
componentDidMount() { componentDidMount() {
this.migrateTabData(this.props.propertyValue); this.migrateTabData(this.props.propertyValue);
} }
componentDidUpdate(prevProps: ControlProps): void {
//on adding a new column last column should get focused
if (
Object.keys(prevProps.propertyValue).length + 1 ===
Object.keys(this.props.propertyValue).length
) {
this.updateFocus(Object.keys(this.props.propertyValue).length - 1, true);
}
}
migrateTabData( migrateTabData(
tabData: Array<{ tabData: Array<{
id: string; id: string;
@ -204,11 +173,14 @@ class TabControl extends BaseControl<ControlProps> {
<TabsWrapper> <TabsWrapper>
<DroppableComponent <DroppableComponent
deleteOption={noop} deleteOption={noop}
fixedHeight={370}
focusedIndex={this.state.focusedIndex}
itemHeight={45} itemHeight={45}
items={orderBy(tabs, ["index"], ["asc"])} items={orderBy(tabs, ["index"], ["asc"])}
onEdit={this.onEdit} onEdit={this.onEdit}
renderComponent={TabControlComponent} renderComponent={TabControlComponent}
toggleVisibility={this.toggleVisibility} toggleVisibility={this.toggleVisibility}
updateFocus={this.updateFocus}
updateItems={this.updateItems} updateItems={this.updateItems}
updateOption={this.updateOption} updateOption={this.updateOption}
/> />
@ -269,6 +241,10 @@ class TabControl extends BaseControl<ControlProps> {
this.updateProperty(this.props.propertyName, tabs); this.updateProperty(this.props.propertyName, tabs);
}; };
updateFocus = (index: number, isFocused: boolean) => {
this.setState({ focusedIndex: isFocused ? index : null });
};
static getControlType() { static getControlType() {
return "TABS_INPUT"; return "TABS_INPUT";
} }