diff --git a/.changeset/khaki-grapes-sink.md b/.changeset/khaki-grapes-sink.md new file mode 100644 index 0000000000..774fc5ed63 --- /dev/null +++ b/.changeset/khaki-grapes-sink.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': patch +--- + +Added new leftElementProps/rightElementProps properties to the TextField to make it easier to accessorize inputs. diff --git a/packages/canon/css/components.css b/packages/canon/css/components.css index 1fc7f275c3..cedd45825c 100644 --- a/packages/canon/css/components.css +++ b/packages/canon/css/components.css @@ -567,16 +567,33 @@ margin: 0; } -.canon-TextFieldInput { +.canon-TextFieldInputWrapper { border-radius: var(--canon-radius-3); border: 1px solid var(--canon-border); - padding: 0 var(--canon-space-4); background-color: var(--canon-bg-surface-1); + align-items: center; + display: flex; +} + +.canon-TextFieldInputLeftElement { + padding-left: var(--canon-space-4); +} + +.canon-TextFieldInputRightElement { + padding-right: var(--canon-space-4); +} + +.canon-TextFieldInput { + padding: 0 var(--canon-space-4); font-size: var(--canon-font-size-3); font-family: var(--canon-font-regular); font-weight: var(--canon-font-weight-regular); color: var(--canon-fg-primary); width: 100%; + height: 100%; + cursor: inherit; + background: none; + border: none; transition: border-color .2s ease-in-out, outline-color .2s ease-in-out; } @@ -584,31 +601,34 @@ color: var(--canon-fg-secondary); } -.canon-TextFieldInput:hover { - border-color: var(--canon-border-hover); -} - .canon-TextFieldInput:focus-visible { outline-color: var(--canon-border-pressed); - border-color: var(--canon-border-pressed); outline-width: 0; } -.canon-TextFieldInput[data-invalid] { +.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:hover) { + border-color: var(--canon-border-hover); +} + +.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:focus-visible) { + border-color: var(--canon-border-pressed); +} + +.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-invalid]) { border-color: var(--canon-fg-danger); } -.canon-TextFieldInput[data-disabled] { +.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-disabled]) { opacity: .5; cursor: not-allowed; border: 1px solid var(--canon-border-disabled); } -.canon-TextFieldInput[data-size="small"] { +.canon-TextFieldInputWrapper[data-size="small"] { height: 2rem; } -.canon-TextFieldInput[data-size="medium"] { +.canon-TextFieldInputWrapper[data-size="medium"] { height: 2.5rem; } diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css index 3382c15675..282291c735 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -9791,16 +9791,33 @@ margin: 0; } -.canon-TextFieldInput { +.canon-TextFieldInputWrapper { border-radius: var(--canon-radius-3); border: 1px solid var(--canon-border); - padding: 0 var(--canon-space-4); background-color: var(--canon-bg-surface-1); + align-items: center; + display: flex; +} + +.canon-TextFieldInputLeftElement { + padding-left: var(--canon-space-4); +} + +.canon-TextFieldInputRightElement { + padding-right: var(--canon-space-4); +} + +.canon-TextFieldInput { + padding: 0 var(--canon-space-4); font-size: var(--canon-font-size-3); font-family: var(--canon-font-regular); font-weight: var(--canon-font-weight-regular); color: var(--canon-fg-primary); width: 100%; + height: 100%; + cursor: inherit; + background: none; + border: none; transition: border-color .2s ease-in-out, outline-color .2s ease-in-out; } @@ -9808,31 +9825,34 @@ color: var(--canon-fg-secondary); } -.canon-TextFieldInput:hover { - border-color: var(--canon-border-hover); -} - .canon-TextFieldInput:focus-visible { outline-color: var(--canon-border-pressed); - border-color: var(--canon-border-pressed); outline-width: 0; } -.canon-TextFieldInput[data-invalid] { +.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:hover) { + border-color: var(--canon-border-hover); +} + +.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:focus-visible) { + border-color: var(--canon-border-pressed); +} + +.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-invalid]) { border-color: var(--canon-fg-danger); } -.canon-TextFieldInput[data-disabled] { +.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-disabled]) { opacity: .5; cursor: not-allowed; border: 1px solid var(--canon-border-disabled); } -.canon-TextFieldInput[data-size="small"] { +.canon-TextFieldInputWrapper[data-size="small"] { height: 2rem; } -.canon-TextFieldInput[data-size="medium"] { +.canon-TextFieldInputWrapper[data-size="medium"] { height: 2.5rem; } diff --git a/packages/canon/css/textfield.css b/packages/canon/css/textfield.css index 8315d48bc6..511873980d 100644 --- a/packages/canon/css/textfield.css +++ b/packages/canon/css/textfield.css @@ -34,16 +34,33 @@ margin: 0; } -.canon-TextFieldInput { +.canon-TextFieldInputWrapper { border-radius: var(--canon-radius-3); border: 1px solid var(--canon-border); - padding: 0 var(--canon-space-4); background-color: var(--canon-bg-surface-1); + align-items: center; + display: flex; +} + +.canon-TextFieldInputLeftElement { + padding-left: var(--canon-space-4); +} + +.canon-TextFieldInputRightElement { + padding-right: var(--canon-space-4); +} + +.canon-TextFieldInput { + padding: 0 var(--canon-space-4); font-size: var(--canon-font-size-3); font-family: var(--canon-font-regular); font-weight: var(--canon-font-weight-regular); color: var(--canon-fg-primary); width: 100%; + height: 100%; + cursor: inherit; + background: none; + border: none; transition: border-color .2s ease-in-out, outline-color .2s ease-in-out; } @@ -51,31 +68,34 @@ color: var(--canon-fg-secondary); } -.canon-TextFieldInput:hover { - border-color: var(--canon-border-hover); -} - .canon-TextFieldInput:focus-visible { outline-color: var(--canon-border-pressed); - border-color: var(--canon-border-pressed); outline-width: 0; } -.canon-TextFieldInput[data-invalid] { +.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:hover) { + border-color: var(--canon-border-hover); +} + +.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:focus-visible) { + border-color: var(--canon-border-pressed); +} + +.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-invalid]) { border-color: var(--canon-fg-danger); } -.canon-TextFieldInput[data-disabled] { +.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-disabled]) { opacity: .5; cursor: not-allowed; border: 1px solid var(--canon-border-disabled); } -.canon-TextFieldInput[data-size="small"] { +.canon-TextFieldInputWrapper[data-size="small"] { height: 2rem; } -.canon-TextFieldInput[data-size="medium"] { +.canon-TextFieldInputWrapper[data-size="medium"] { height: 2.5rem; } diff --git a/packages/canon/src/components/Icon/icons.ts b/packages/canon/src/components/Icon/icons.ts index 615c918db7..e43cc2e7d6 100644 --- a/packages/canon/src/components/Icon/icons.ts +++ b/packages/canon/src/components/Icon/icons.ts @@ -72,6 +72,8 @@ import { RiGithubLine, RiDiscordLine, RiYoutubeLine, + RiCloseLine, + RiSearchLine, } from '@remixicon/react'; /** @public */ @@ -103,6 +105,7 @@ export const icons: IconMap = { 'chevron-left': RiArrowLeftSLine, 'chevron-right': RiArrowRightSLine, 'chevron-up': RiArrowUpSLine, + close: RiCloseLine, cloud: RiCloudLine, code: RiCodeLine, discord: RiDiscordLine, @@ -118,6 +121,7 @@ export const icons: IconMap = { heart: RiHeartLine, moon: RiMoonLine, plus: RiAddLine, + search: RiSearchLine, 'sidebar-fold': RiSidebarFoldLine, 'sidebar-unfold': RiSidebarUnfoldLine, sparkling: RiSparklingLine, diff --git a/packages/canon/src/components/Icon/types.ts b/packages/canon/src/components/Icon/types.ts index 4870a1afc4..e97ea9c454 100644 --- a/packages/canon/src/components/Icon/types.ts +++ b/packages/canon/src/components/Icon/types.ts @@ -46,6 +46,7 @@ export type IconNames = | 'chevron-left' | 'chevron-right' | 'chevron-up' + | 'close' | 'cloud' | 'code' | 'discord' @@ -61,6 +62,7 @@ export type IconNames = | 'heart' | 'moon' | 'plus' + | 'search' | 'sidebar-fold' | 'sidebar-unfold' | 'sparkling' diff --git a/packages/canon/src/components/TextField/TextField.stories.tsx b/packages/canon/src/components/TextField/TextField.stories.tsx index 8594a8c650..45a8e1436f 100644 --- a/packages/canon/src/components/TextField/TextField.stories.tsx +++ b/packages/canon/src/components/TextField/TextField.stories.tsx @@ -15,8 +15,27 @@ */ import type { Meta, StoryObj } from '@storybook/react'; +import type { ComponentPropsWithoutRef } from 'react'; import { TextField } from './TextField'; import { Flex } from '../Flex'; +import { Icon } from '../Icon'; + +const CloseButton = (props: ComponentPropsWithoutRef<'button'>) => { + return ( + + ); +}; const meta = { title: 'Components/TextField', @@ -109,3 +128,34 @@ export const WithErrorAndDescription: Story = { description: 'Description', }, }; + +export const WithLeftAndRightElements: Story = { + args: { + ...WithLabel.args, + placeholder: 'Search...', + leftElementProps: { + children: , + }, + rightElementProps: { + children: , + }, + }, +}; + +export const WithLeftAndRightElementsAndHelpText: Story = { + args: { + ...WithLeftAndRightElements.args, + error: 'Failed to search', + description: 'Enter some text to search', + }, +}; + +export const DisabledWithLeftAndRightElements: Story = { + args: { + ...WithLeftAndRightElements.args, + disabled: true, + rightElementProps: { + children: , + }, + }, +}; diff --git a/packages/canon/src/components/TextField/TextField.styles.css b/packages/canon/src/components/TextField/TextField.styles.css index 5acc8480f3..f30192f274 100644 --- a/packages/canon/src/components/TextField/TextField.styles.css +++ b/packages/canon/src/components/TextField/TextField.styles.css @@ -29,6 +29,7 @@ margin-right: auto; cursor: pointer; } + .canon-TextFieldLabel[data-disabled] { cursor: default; } @@ -49,48 +50,68 @@ padding-top: var(--canon-space-1_5); } -.canon-TextFieldInput { +.canon-TextFieldInputWrapper { + display: flex; + align-items: center; border-radius: var(--canon-radius-3); border: 1px solid var(--canon-border); - padding: 0 var(--canon-space-4); background-color: var(--canon-bg-surface-1); +} + +.canon-TextFieldInputLeftElement { + padding-left: var(--canon-space-4); +} + +.canon-TextFieldInputRightElement { + padding-right: var(--canon-space-4); +} + +.canon-TextFieldInput { + border: none; + background: none; + padding: 0 var(--canon-space-4); font-size: var(--canon-font-size-3); font-family: var(--canon-font-regular); font-weight: var(--canon-font-weight-regular); color: var(--canon-fg-primary); transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out; width: 100%; + height: 100%; + cursor: inherit; } .canon-TextFieldInput::placeholder { color: var(--canon-fg-secondary); } -.canon-TextFieldInput:hover { - border-color: var(--canon-border-hover); -} - .canon-TextFieldInput:focus-visible { outline-color: var(--canon-border-pressed); outline-width: 0px; +} + +.canon-TextFieldInputWrapper:has(> .canon-TextFieldInput:hover) { + border-color: var(--canon-border-hover); +} + +.canon-TextFieldInputWrapper:has(> .canon-TextFieldInput:focus-visible) { border-color: var(--canon-border-pressed); } -.canon-TextFieldInput[data-invalid] { +.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-invalid]) { border-color: var(--canon-fg-danger); } -.canon-TextFieldInput[data-disabled] { +.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-disabled]) { opacity: 0.5; cursor: not-allowed; border: 1px solid var(--canon-border-disabled); } -.canon-TextFieldInput[data-size='small'] { +.canon-TextFieldInputWrapper[data-size='small'] { height: 2rem; } -.canon-TextFieldInput[data-size='medium'] { +.canon-TextFieldInputWrapper[data-size='medium'] { height: 2.5rem; } diff --git a/packages/canon/src/components/TextField/TextField.tsx b/packages/canon/src/components/TextField/TextField.tsx index 132d6ab1c6..83640c153a 100644 --- a/packages/canon/src/components/TextField/TextField.tsx +++ b/packages/canon/src/components/TextField/TextField.tsx @@ -33,6 +33,8 @@ export const TextField = forwardRef( required, style, disabled, + leftElementProps, + rightElementProps, ...rest } = props; @@ -57,12 +59,31 @@ export const TextField = forwardRef( )} )} - +
+ {leftElementProps ? ( +
+ ) : null} + + {rightElementProps ? ( +
+ ) : null} +
{description && ( {description} diff --git a/packages/canon/src/components/TextField/types.ts b/packages/canon/src/components/TextField/types.ts index 147d398c99..fbec9fde4b 100644 --- a/packages/canon/src/components/TextField/types.ts +++ b/packages/canon/src/components/TextField/types.ts @@ -49,4 +49,14 @@ export interface TextFieldProps * The error message of the text field */ error?: string | null; + + /** + * Props for an element to render on the left of the input + */ + leftElementProps?: React.ComponentPropsWithoutRef<'div'>; + + /** + * Props for an element to render on the right of the input + */ + rightElementProps?: React.ComponentPropsWithoutRef<'div'>; }