Canon - Support left/right elements in TextField

Signed-off-by: James Brooks <jamesbrooks@spotify.com>
This commit is contained in:
James Brooks
2025-05-01 11:04:38 +01:00
parent 6414320f91
commit 6189bfda1d
10 changed files with 222 additions and 49 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/canon': patch
---
Added new leftElementProps/rightElementProps properties to the TextField to make it easier to accessorize inputs.
+31 -11
View File
@@ -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;
}
+31 -11
View File
@@ -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;
}
+31 -11
View File
@@ -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;
}
@@ -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,
@@ -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'
@@ -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 (
<button
{...props}
style={{
padding: 0,
background: 'none',
border: 'none',
verticalAlign: 'middle',
...props.style,
}}
>
<Icon name="close" style={{ display: 'block' }} />
</button>
);
};
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: <Icon name="search" style={{ display: 'block' }} />,
},
rightElementProps: {
children: <CloseButton />,
},
},
};
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: <CloseButton />,
},
},
};
@@ -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;
}
@@ -33,6 +33,8 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
required,
style,
disabled,
leftElementProps,
rightElementProps,
...rest
} = props;
@@ -57,12 +59,31 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
)}
</Field.Label>
)}
<Field.Control
className="canon-TextFieldInput"
data-size={responsiveSize}
required={required}
{...rest}
/>
<div className="canon-TextFieldInputWrapper" data-size={responsiveSize}>
{leftElementProps ? (
<div
{...leftElementProps}
className={clsx(
'canon-TextFieldInputLeftElement',
leftElementProps.className,
)}
/>
) : null}
<Field.Control
className="canon-TextFieldInput"
required={required}
{...rest}
/>
{rightElementProps ? (
<div
{...rightElementProps}
className={clsx(
'canon-TextFieldInputRightElement',
rightElementProps.className,
)}
/>
) : null}
</div>
{description && (
<Field.Description className="canon-TextFieldDescription">
{description}
@@ -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'>;
}