From b4943466e73fce581e6de85c096a32b31789ceab Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:11:04 -0500 Subject: [PATCH] refactor: [M3-8637] - Make `EditableText` UI component pure (#11333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial idea * improve comment * fix spelling * fix spelling * use `to` rather than `link` * use `to` rather than `link` * fix comment * use the new prop in Cloud Manager * impliment some changes from Jaalah's component * add changeset * thanks @abailly-akamai 🙏 * clean up a bit * final clean up --------- Co-authored-by: Banks Nussman --- .../Breadcrumb/FinalCrumb.styles.tsx | 4 +- .../src/components/Breadcrumb/FinalCrumb.tsx | 13 ++++- packages/manager/src/components/Link.tsx | 2 +- .../pr-11333-changed-1733176201231.md | 5 ++ .../EditableText/EditableText.stories.tsx | 20 +++++++ .../components/EditableText/EditableText.tsx | 53 ++++++++++++++----- 6 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 packages/ui/.changeset/pr-11333-changed-1733176201231.md diff --git a/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx b/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx index 57c84f428bb..af43289b40f 100644 --- a/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx +++ b/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx @@ -1,6 +1,8 @@ import { EditableText, H1Header } from '@linode/ui'; import { styled } from '@mui/material'; +import type { EditableTextProps } from '@linode/ui'; + export const StyledDiv = styled('div', { label: 'StyledDiv' })({ display: 'flex', flexDirection: 'column', @@ -8,7 +10,7 @@ export const StyledDiv = styled('div', { label: 'StyledDiv' })({ export const StyledEditableText = styled(EditableText, { label: 'StyledEditableText', -})(({ theme }) => ({ +})(({ theme }) => ({ '& > div': { width: 250, }, diff --git a/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx b/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx index 5cb20414d84..729f1222230 100644 --- a/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx +++ b/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx @@ -1,11 +1,13 @@ import * as React from 'react'; +import { Link } from '../Link'; import { StyledDiv, StyledEditableText, StyledH1Header, } from './FinalCrumb.styles'; -import { EditableProps, LabelProps } from './types'; + +import type { EditableProps, LabelProps } from './types'; interface Props { crumb: string; @@ -22,6 +24,13 @@ export const FinalCrumb = React.memo((props: Props) => { onEditHandlers, } = props; + const linkProps = labelOptions?.linkTo + ? { + LinkComponent: Link, + labelLink: labelOptions.linkTo, + } + : {}; + if (onEditHandlers) { return ( { disabledBreadcrumbEditButton={disabledBreadcrumbEditButton} errorText={onEditHandlers.errorText} handleAnalyticsEvent={onEditHandlers.handleAnalyticsEvent} - labelLink={labelOptions && labelOptions.linkTo} onCancel={onEditHandlers.onCancel} onEdit={onEditHandlers.onEdit} text={onEditHandlers.editableTextTitle} + {...linkProps} /> ); } diff --git a/packages/manager/src/components/Link.tsx b/packages/manager/src/components/Link.tsx index 8242c791db1..ccdd26198a9 100644 --- a/packages/manager/src/components/Link.tsx +++ b/packages/manager/src/components/Link.tsx @@ -46,7 +46,7 @@ export interface LinkProps extends Omit<_LinkProps, 'to'> { * @example "/profile/display" * @example "https://linode.com" */ - to: TanStackLinkProps['to'] | (string & {}); + to: Exclude; } /** diff --git a/packages/ui/.changeset/pr-11333-changed-1733176201231.md b/packages/ui/.changeset/pr-11333-changed-1733176201231.md new file mode 100644 index 00000000000..e142471fe05 --- /dev/null +++ b/packages/ui/.changeset/pr-11333-changed-1733176201231.md @@ -0,0 +1,5 @@ +--- +"@linode/ui": Changed +--- + +Update `EditableText` to not use `react-router-dom` and accept a `LinkComponent` prop ([#11333](https://github.com/linode/manager/pull/11333)) diff --git a/packages/ui/src/components/EditableText/EditableText.stories.tsx b/packages/ui/src/components/EditableText/EditableText.stories.tsx index 7ca7bd9a59c..94bd44723f0 100644 --- a/packages/ui/src/components/EditableText/EditableText.stories.tsx +++ b/packages/ui/src/components/EditableText/EditableText.stories.tsx @@ -42,6 +42,26 @@ export const WithSuffix: Story = { }, }; +/** + * Pretend this is `react-router-dom`'s Link component. + * This is just an example to show usage with `EditableText` + */ +const Link = ( + props: React.PropsWithChildren<{ className?: string; to?: string }> +) => { + return ; +}; + +export const WithCustomLinkComponent: Story = { + args: { + LinkComponent: Link, + labelLink: 'https://linode.com', + onCancel: action('onCancel'), + text: 'I have a link', + }, + render: (args) => , +}; + const meta: Meta = { component: EditableText, title: 'Components/Input/Editable Text', diff --git a/packages/ui/src/components/EditableText/EditableText.tsx b/packages/ui/src/components/EditableText/EditableText.tsx index b07374a8b4e..f5be63e05c6 100644 --- a/packages/ui/src/components/EditableText/EditableText.tsx +++ b/packages/ui/src/components/EditableText/EditableText.tsx @@ -1,8 +1,7 @@ import Check from '@mui/icons-material/Check'; import Close from '@mui/icons-material/Close'; import Edit from '@mui/icons-material/Edit'; -import * as React from 'react'; -import { Link } from 'react-router-dom'; +import React from 'react'; import { makeStyles } from 'tss-react/mui'; import { Button } from '../Button'; @@ -12,6 +11,7 @@ import { TextField } from '../TextField'; import type { TextFieldProps } from '../TextField'; import type { Theme } from '@mui/material/styles'; +import type { PropsWithChildren } from 'react'; const useStyles = makeStyles()( (theme: Theme, _params, classes) => ({ @@ -102,18 +102,23 @@ const useStyles = makeStyles()( }) ); -interface Props { +interface BaseProps extends Omit { + /** + * The class name to apply to the container + */ className?: string; + /** + * Whether to disable the Breadcrumb edit button + */ disabledBreadcrumbEditButton?: boolean; + /** + * The error text to display + */ errorText?: string; /** * Send event analytics */ handleAnalyticsEvent?: () => void; - /** - * Optional link for the text when it is not in editing mode - */ - labelLink?: string; /** * Function to cancel editing and restore text to previous text */ @@ -121,7 +126,7 @@ interface Props { /** * The function to handle saving edited text */ - onEdit: (text: string) => Promise; + onEdit: (_text: string) => Promise; /** * The text inside the textbox */ @@ -132,14 +137,38 @@ interface Props { textSuffix?: string; } -interface PassThroughProps extends Props, Omit {} +interface PropsWithoutLink extends BaseProps { + LinkComponent?: never; + labelLink?: never; +} + +interface PropsWithLink extends BaseProps { + /** + * A custom Link component that is required when passing a `labelLink` prop + * + * The component you pass must accept `className`, `to`, and `children` as props + * - `to` is just the `labelLink` prop forwarded to this Link component + * - `className` should be passed to your Link so that it has the correct styles + * - `children` contains the link's text/children + */ + LinkComponent: React.ComponentType< + PropsWithChildren<{ className?: string; to: string }> + >; + /** + * Optional link for the text when it is not in editing mode + */ + labelLink: string; +} + +export type EditableTextProps = PropsWithLink | PropsWithoutLink; -export const EditableText = (props: PassThroughProps) => { +export const EditableText = (props: EditableTextProps) => { const { classes } = useStyles(); const [isEditing, setIsEditing] = React.useState(Boolean(props.errorText)); const [text, setText] = React.useState(props.text); const { + LinkComponent, className, disabledBreadcrumbEditButton, errorText, @@ -220,9 +249,9 @@ export const EditableText = (props: PassThroughProps) => { data-testid={'editable-text'} > {!!labelLink ? ( - + {labelText} - + ) : ( labelText )}