feat: editable dismissible tab component (#38788)
## Description Addition of a template component that combines editable entity name and dismissible tab. Fixes #37649 ## Automation /ok-to-test tags="@tag.Sanity" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!CAUTION] > 🔴 🔴 🔴 Some tests have failed. > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/12925482595> > Commit: 23c8fbe877a2390ec95877ae8f761d7c590b23c7 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12925482595&attempt=2&selectiontype=test&testsstatus=failed&specsstatus=fail" target="_blank">Cypress dashboard</a>. > Tags: @tag.Sanity > Spec: > The following are new failures, please fix them before merging the PR: <ol> > <li>cypress/e2e/Regression/ClientSide/OtherUIFeatures/GlobalSearch_spec.js</ol> > <a href="https://internal.appsmith.com/app/cypress-dashboard/identified-flaky-tests-65890b3c81d7400d08fa9ee3?branch=master" target="_blank">List of identified flaky tests</a>. > <hr>Thu, 23 Jan 2025 11:42:24 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced `DismissibleTab` component with interactive close and click functionality. - Added `EditableEntityName` component for editing entity names with validation. - Created `EditableDismissibleTab` component combining dismissible and editable behaviors. - Added new Storybook stories for `DismissibleTab`, `EditableDismissibleTab`, and `EditableEntityName` components. - **Improvements** - Enhanced design system with new styled components for better interactivity and appearance. - **Refactoring** - Reorganized hook and component imports. - Updated export statements in various files to improve module accessibility. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
72251cba2e
commit
efceb1e390
|
|
@ -0,0 +1,21 @@
|
|||
/* eslint-disable no-console */
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { DismissibleTab } from ".";
|
||||
|
||||
const meta: Meta<typeof DismissibleTab> = {
|
||||
title: "ADS/Components/Dismissible Tab",
|
||||
component: DismissibleTab,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof DismissibleTab>;
|
||||
|
||||
export const Basic: Story = {
|
||||
args: {
|
||||
isActive: true,
|
||||
dataTestId: "t--dismissible-tab",
|
||||
children: "Dismissible tab",
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import styled from "styled-components";
|
||||
|
||||
import { Button as ADSButton } from "..";
|
||||
|
||||
export const Tab = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
gap: var(--ads-v2-spaces-2);
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
color: var(--ads-v2-color-fg);
|
||||
cursor: pointer;
|
||||
|
||||
border-top-left-radius: var(--ads-v2-border-radius);
|
||||
border-top-right-radius: var(--ads-v2-border-radius);
|
||||
border-left: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
border-top: 3px solid transparent;
|
||||
|
||||
padding: var(--ads-v2-spaces-3);
|
||||
padding-top: 6px;
|
||||
|
||||
&.active {
|
||||
background: var(--ads-v2-colors-control-field-default-bg);
|
||||
border-top-color: var(--ads-v2-color-bg-brand);
|
||||
border-left-color: var(--ads-v2-color-border-muted);
|
||||
border-right-color: var(--ads-v2-color-border-muted);
|
||||
|
||||
span {
|
||||
font-weight: var(--ads-v2-font-weight-bold);
|
||||
}
|
||||
}
|
||||
|
||||
& > .tab-close {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover > .tab-close,
|
||||
&:focus-within > .tab-close,
|
||||
&.active > .tab-close {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export const CloseButton = styled(ADSButton)`
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
padding: var(--ads-v2-spaces-1);
|
||||
max-width: 16px;
|
||||
max-height: 16px;
|
||||
`;
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import React from "react";
|
||||
|
||||
import clsx from "classnames";
|
||||
|
||||
import { Icon } from "..";
|
||||
|
||||
import * as Styled from "./DismissibleTab.styles";
|
||||
import { DATA_TEST_ID } from "./constants";
|
||||
|
||||
import type { DismissibleTabProps } from "./DismissibleTab.types";
|
||||
|
||||
export const DismissibleTab = ({
|
||||
children,
|
||||
dataTestId,
|
||||
isActive,
|
||||
onClick,
|
||||
onClose,
|
||||
onDoubleClick,
|
||||
}: DismissibleTabProps) => {
|
||||
return (
|
||||
<Styled.Tab
|
||||
className={clsx("editor-tab", isActive && "active")}
|
||||
data-testid={dataTestId}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
>
|
||||
{children}
|
||||
<Styled.CloseButton
|
||||
aria-label="Close tab"
|
||||
className="tab-close"
|
||||
data-testid={DATA_TEST_ID.CLOSE_BUTTON}
|
||||
isIconButton
|
||||
kind="tertiary"
|
||||
onClick={onClose}
|
||||
role="tab"
|
||||
size="sm"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon name="close-line" />
|
||||
</Styled.CloseButton>
|
||||
</Styled.Tab>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import type React from "react";
|
||||
|
||||
export interface DismissibleTabProps {
|
||||
children: React.ReactNode;
|
||||
dataTestId?: string;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
onClose: (e: React.MouseEvent) => void;
|
||||
onDoubleClick?: () => void;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export const DATA_TEST_ID = {
|
||||
CLOSE_BUTTON: "t--tab-close-btn",
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { DismissibleTab } from "./DismissibleTab";
|
||||
export type { DismissibleTabProps } from "./DismissibleTab.types";
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/* eslint-disable no-console */
|
||||
import React from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { EditableDismissibleTab } from ".";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Icon } from "../..";
|
||||
|
||||
const meta: Meta<typeof EditableDismissibleTab> = {
|
||||
title: "ADS/Templates/Editable Dismissible Tab",
|
||||
component: EditableDismissibleTab,
|
||||
};
|
||||
|
||||
const EntityIcon = styled.div`
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg,
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const JSIcon = () => {
|
||||
return (
|
||||
<EntityIcon>
|
||||
<Icon name="js-yellow" size="md" />
|
||||
</EntityIcon>
|
||||
);
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof EditableDismissibleTab>;
|
||||
|
||||
export const Basic: Story = {
|
||||
args: {
|
||||
isActive: true,
|
||||
dataTestId: "t--dismissible-tab",
|
||||
icon: JSIcon(),
|
||||
name: "Hello",
|
||||
|
||||
onNameSave: console.log,
|
||||
validateName: (name: string) =>
|
||||
name.length < 3 ? "Name must be at least 3 characters" : null,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import React from "react";
|
||||
import { noop } from "lodash";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
|
||||
import { DismissibleTab } from "../..";
|
||||
import { EditableEntityName } from "..";
|
||||
|
||||
import type { EditableDismissibleTabProps } from "./EditableDismissibleTab.types";
|
||||
|
||||
export const EditableDismissibleTab = (props: EditableDismissibleTabProps) => {
|
||||
const {
|
||||
dataTestId,
|
||||
icon,
|
||||
isActive,
|
||||
isEditable = true,
|
||||
isLoading,
|
||||
name,
|
||||
onClick,
|
||||
onClose,
|
||||
onNameSave,
|
||||
validateName,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
setFalse: exitEditMode,
|
||||
setTrue: enterEditMode,
|
||||
value: isEditing,
|
||||
} = useBoolean(false);
|
||||
|
||||
const handleDoubleClick = isEditable ? enterEditMode : noop;
|
||||
|
||||
return (
|
||||
<DismissibleTab
|
||||
dataTestId={dataTestId}
|
||||
isActive={isActive}
|
||||
onClick={onClick}
|
||||
onClose={onClose}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
<EditableEntityName
|
||||
icon={icon}
|
||||
isEditing={isEditing}
|
||||
isLoading={isLoading}
|
||||
name={name}
|
||||
onExitEditing={exitEditMode}
|
||||
onNameSave={onNameSave}
|
||||
validateName={validateName}
|
||||
/>
|
||||
</DismissibleTab>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import type React from "react";
|
||||
|
||||
export interface EditableDismissibleTabProps {
|
||||
dataTestId?: string;
|
||||
icon: React.ReactNode;
|
||||
isActive: boolean;
|
||||
isEditable?: boolean;
|
||||
isLoading: boolean;
|
||||
name: string;
|
||||
onClick: () => void;
|
||||
onClose: () => void;
|
||||
onNameSave: (name: string) => void;
|
||||
validateName: (name: string) => string | null;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { EditableDismissibleTab } from "./EditableDismissibleTab";
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/* eslint-disable no-console */
|
||||
import React from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Icon } from "../..";
|
||||
import { EditableEntityName } from ".";
|
||||
|
||||
const EntityIcon = styled.div`
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg,
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const JSIcon = () => {
|
||||
return (
|
||||
<EntityIcon>
|
||||
<Icon name="js-yellow" size="md" />
|
||||
</EntityIcon>
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<typeof EditableEntityName> = {
|
||||
title: "ADS/Templates/Editable Entity Name",
|
||||
component: EditableEntityName,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof EditableEntityName>;
|
||||
|
||||
export const Basic: Story = {
|
||||
args: {
|
||||
name: "Hello",
|
||||
onNameSave: console.log,
|
||||
onExitEditing: console.log,
|
||||
icon: JSIcon(),
|
||||
inputTestId: "t--editable-name",
|
||||
isEditing: true,
|
||||
isLoading: false,
|
||||
validateName: (name: string) =>
|
||||
name.length < 3 ? "Name must be at least 3 characters" : null,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import styled from "styled-components";
|
||||
import { Text as ADSText } from "../../Text";
|
||||
|
||||
export const Root = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
gap: var(--ads-v2-spaces-2);
|
||||
`;
|
||||
|
||||
export const Text = styled(ADSText)`
|
||||
min-width: 3ch;
|
||||
bottom: -0.5px;
|
||||
`;
|
||||
|
||||
export const IconContainer = styled.div`
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 12px;
|
||||
}
|
||||
`;
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import React, { useMemo } from "react";
|
||||
|
||||
import { Spinner, Tooltip } from "../..";
|
||||
import { useEditableText } from "../../__hooks__";
|
||||
|
||||
import * as Styled from "./EditableEntityName.styles";
|
||||
|
||||
import type { EditableEntityNameProps } from "./EditableEntityName.types";
|
||||
|
||||
export const EditableEntityName = ({
|
||||
icon,
|
||||
inputTestId,
|
||||
isEditing,
|
||||
isLoading = false,
|
||||
name,
|
||||
onExitEditing,
|
||||
onNameSave,
|
||||
validateName,
|
||||
}: EditableEntityNameProps) => {
|
||||
const [
|
||||
inputRef,
|
||||
editableName,
|
||||
validationError,
|
||||
handleKeyUp,
|
||||
handleTitleChange,
|
||||
] = useEditableText(isEditing, name, onExitEditing, validateName, onNameSave);
|
||||
|
||||
const inputProps = useMemo(
|
||||
() => ({
|
||||
["data-testid"]: inputTestId,
|
||||
onKeyUp: handleKeyUp,
|
||||
onChange: handleTitleChange,
|
||||
autoFocus: true,
|
||||
style: {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
left: -1,
|
||||
top: -5,
|
||||
},
|
||||
}),
|
||||
[handleKeyUp, handleTitleChange, inputTestId],
|
||||
);
|
||||
|
||||
return (
|
||||
<Styled.Root>
|
||||
{isLoading ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
<Styled.IconContainer>{icon}</Styled.IconContainer>
|
||||
)}
|
||||
<Tooltip content={validationError} visible={Boolean(validationError)}>
|
||||
<Styled.Text
|
||||
aria-invalid={Boolean(validationError)}
|
||||
inputProps={inputProps}
|
||||
inputRef={inputRef}
|
||||
isEditable={isEditing}
|
||||
kind="body-s"
|
||||
>
|
||||
{editableName}
|
||||
</Styled.Text>
|
||||
</Tooltip>
|
||||
</Styled.Root>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import type React from "react";
|
||||
|
||||
export interface EditableEntityNameProps {
|
||||
icon: React.ReactNode;
|
||||
inputTestId?: string;
|
||||
isEditing: boolean;
|
||||
isLoading?: boolean;
|
||||
name: string;
|
||||
onExitEditing: () => void;
|
||||
onNameSave: (name: string) => void;
|
||||
validateName: (name: string) => string | null;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { EditableEntityName } from "./EditableEntityName";
|
||||
|
|
@ -5,7 +5,7 @@ import { Tooltip } from "../../../Tooltip";
|
|||
|
||||
import type { EntityItemProps } from "./EntityItem.types";
|
||||
import { EntityEditableName } from "./EntityItem.styles";
|
||||
import { useEditableText } from "../Editable";
|
||||
import { useEditableText } from "../../../__hooks__/useEditableText";
|
||||
import clx from "classnames";
|
||||
|
||||
export const EntityItem = (props: EntityItemProps) => {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ export { EmptyState } from "./EmptyState";
|
|||
export { NoSearchResults } from "./NoSearchResults";
|
||||
export * from "./ExplorerContainer";
|
||||
export * from "./EntityItem";
|
||||
export { useEditableText } from "./Editable";
|
||||
export { useEditableText } from "../../__hooks__/useEditableText";
|
||||
export * from "./EntityGroupsList";
|
||||
export * from "./EntityListTree";
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export * from "./IDEHeader";
|
||||
export * from "./EntityExplorer";
|
||||
export * from "./Sidebar";
|
||||
export * from "./EditableEntityName";
|
||||
export * from "./EditableDismissibleTab";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
export { useDOMRef } from "./useDomRef";
|
||||
export { useEditableText } from "./useEditableText";
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import React from "react";
|
||||
import { renderHook, act } from "@testing-library/react-hooks";
|
||||
import { useEditableText } from "./useEditableText";
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { Text } from "../../..";
|
||||
import { Text } from "../..";
|
||||
|
||||
import { useEditableText } from "./useEditableText";
|
||||
|
||||
describe("useEditableText", () => {
|
||||
const mockExitEditing = jest.fn();
|
||||
|
|
@ -7,8 +7,10 @@ import {
|
|||
useRef,
|
||||
type RefObject,
|
||||
} from "react";
|
||||
|
||||
import { usePrevious } from "@mantine/hooks";
|
||||
import { useEventCallback, useEventListener } from "usehooks-ts";
|
||||
|
||||
import { normaliseName } from "./utils";
|
||||
|
||||
export function useEditableText(
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
import "./__theme__/default/index.css";
|
||||
|
||||
export * from "./AnnouncementPopover";
|
||||
export * from "./AnnouncementModal";
|
||||
export * from "./AnnouncementPopover";
|
||||
export * from "./Avatar";
|
||||
export * from "./Button";
|
||||
export * from "./Badge";
|
||||
export * from "./Banner";
|
||||
export * from "./Button";
|
||||
export * from "./Callout";
|
||||
export * from "./Checkbox";
|
||||
export * from "./Collapsible";
|
||||
export * from "./DatePicker";
|
||||
export * from "./DismissibleTab";
|
||||
export * from "./Divider";
|
||||
export * from "./Flex";
|
||||
export * from "./FormControl";
|
||||
|
|
@ -31,10 +33,9 @@ export * from "./Switch";
|
|||
export * from "./Tab";
|
||||
export * from "./Table";
|
||||
export * from "./Tag";
|
||||
export * from "./Templates";
|
||||
export * from "./Text";
|
||||
export * from "./Toast";
|
||||
export * from "./ToggleButton";
|
||||
export * from "./Tooltip";
|
||||
export * from "./ToggleButtonGroup";
|
||||
export * from "./Templates";
|
||||
export * from "./Badge";
|
||||
export * from "./Tooltip";
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user