diff --git a/.changeset/khaki-grapes-sink.md b/.changeset/khaki-grapes-sink.md index 774fc5ed63..d96521e029 100644 --- a/.changeset/khaki-grapes-sink.md +++ b/.changeset/khaki-grapes-sink.md @@ -2,4 +2,4 @@ '@backstage/canon': patch --- -Added new leftElementProps/rightElementProps properties to the TextField to make it easier to accessorize inputs. +Added new icon and onClear props 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 cedd45825c..c122251b5b 100644 --- a/packages/canon/css/components.css +++ b/packages/canon/css/components.css @@ -568,6 +568,7 @@ } .canon-TextFieldInputWrapper { + padding: 0 var(--canon-space-3); border-radius: var(--canon-radius-3); border: 1px solid var(--canon-border); background-color: var(--canon-bg-surface-1); @@ -575,16 +576,15 @@ display: flex; } -.canon-TextFieldInputLeftElement { - padding-left: var(--canon-space-4); -} - -.canon-TextFieldInputRightElement { - padding-right: var(--canon-space-4); +.canon-TextFieldInputIcon { + padding-right: var(--canon-space-1); + width: 1.5rem; + height: 1.5rem; + color: var(--canon-fg-primary); + display: block; } .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); @@ -597,11 +597,31 @@ transition: border-color .2s ease-in-out, outline-color .2s ease-in-out; } +.canon-TextFieldInput[type="search"]::-webkit-search-cancel-button, .canon-TextFieldInput[type="search"]::-webkit-search-decoration { + appearance: none; +} + +.canon-TextFieldClearButton { + padding: 0 0 0 var(--canon-space-1); + vertical-align: middle; + background: none; + border: none; + display: none; +} + +.canon-TextFieldInput[data-filled] + .canon-TextFieldClearButton { + display: inline-block; +} + +.canon-TextFieldClearButtonIcon { + display: block; +} + .canon-TextFieldInput::placeholder { color: var(--canon-fg-secondary); } -.canon-TextFieldInput:focus-visible { +.canon-TextFieldInput[data-focused] { outline-color: var(--canon-border-pressed); outline-width: 0; } @@ -610,20 +630,24 @@ border-color: var(--canon-border-hover); } -.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:focus-visible) { +.canon-TextField[data-focused] .canon-TextFieldInputWrapper { border-color: var(--canon-border-pressed); } -.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-invalid]) { +.canon-TextField[data-invalid] .canon-TextFieldInputWrapper { border-color: var(--canon-fg-danger); } -.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-disabled]) { +.canon-TextField[data-disabled] .canon-TextFieldInputWrapper { opacity: .5; cursor: not-allowed; border: 1px solid var(--canon-border-disabled); } +.canon-TextField[data-disabled] .canon-TextFieldClearButton { + cursor: inherit; +} + .canon-TextFieldInputWrapper[data-size="small"] { height: 2rem; } diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css index 282291c735..d1f3ba045d 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -9792,6 +9792,7 @@ } .canon-TextFieldInputWrapper { + padding: 0 var(--canon-space-3); border-radius: var(--canon-radius-3); border: 1px solid var(--canon-border); background-color: var(--canon-bg-surface-1); @@ -9799,16 +9800,15 @@ display: flex; } -.canon-TextFieldInputLeftElement { - padding-left: var(--canon-space-4); -} - -.canon-TextFieldInputRightElement { - padding-right: var(--canon-space-4); +.canon-TextFieldInputIcon { + padding-right: var(--canon-space-1); + width: 1.5rem; + height: 1.5rem; + color: var(--canon-fg-primary); + display: block; } .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); @@ -9821,11 +9821,31 @@ transition: border-color .2s ease-in-out, outline-color .2s ease-in-out; } +.canon-TextFieldInput[type="search"]::-webkit-search-cancel-button, .canon-TextFieldInput[type="search"]::-webkit-search-decoration { + appearance: none; +} + +.canon-TextFieldClearButton { + padding: 0 0 0 var(--canon-space-1); + vertical-align: middle; + background: none; + border: none; + display: none; +} + +.canon-TextFieldInput[data-filled] + .canon-TextFieldClearButton { + display: inline-block; +} + +.canon-TextFieldClearButtonIcon { + display: block; +} + .canon-TextFieldInput::placeholder { color: var(--canon-fg-secondary); } -.canon-TextFieldInput:focus-visible { +.canon-TextFieldInput[data-focused] { outline-color: var(--canon-border-pressed); outline-width: 0; } @@ -9834,20 +9854,24 @@ border-color: var(--canon-border-hover); } -.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:focus-visible) { +.canon-TextField[data-focused] .canon-TextFieldInputWrapper { border-color: var(--canon-border-pressed); } -.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-invalid]) { +.canon-TextField[data-invalid] .canon-TextFieldInputWrapper { border-color: var(--canon-fg-danger); } -.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-disabled]) { +.canon-TextField[data-disabled] .canon-TextFieldInputWrapper { opacity: .5; cursor: not-allowed; border: 1px solid var(--canon-border-disabled); } +.canon-TextField[data-disabled] .canon-TextFieldClearButton { + cursor: inherit; +} + .canon-TextFieldInputWrapper[data-size="small"] { height: 2rem; } diff --git a/packages/canon/css/textfield.css b/packages/canon/css/textfield.css index 511873980d..d55b5b6f2c 100644 --- a/packages/canon/css/textfield.css +++ b/packages/canon/css/textfield.css @@ -35,6 +35,7 @@ } .canon-TextFieldInputWrapper { + padding: 0 var(--canon-space-3); border-radius: var(--canon-radius-3); border: 1px solid var(--canon-border); background-color: var(--canon-bg-surface-1); @@ -42,16 +43,15 @@ display: flex; } -.canon-TextFieldInputLeftElement { - padding-left: var(--canon-space-4); -} - -.canon-TextFieldInputRightElement { - padding-right: var(--canon-space-4); +.canon-TextFieldInputIcon { + padding-right: var(--canon-space-1); + width: 1.5rem; + height: 1.5rem; + color: var(--canon-fg-primary); + display: block; } .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); @@ -64,11 +64,31 @@ transition: border-color .2s ease-in-out, outline-color .2s ease-in-out; } +.canon-TextFieldInput[type="search"]::-webkit-search-cancel-button, .canon-TextFieldInput[type="search"]::-webkit-search-decoration { + appearance: none; +} + +.canon-TextFieldClearButton { + padding: 0 0 0 var(--canon-space-1); + vertical-align: middle; + background: none; + border: none; + display: none; +} + +.canon-TextFieldInput[data-filled] + .canon-TextFieldClearButton { + display: inline-block; +} + +.canon-TextFieldClearButtonIcon { + display: block; +} + .canon-TextFieldInput::placeholder { color: var(--canon-fg-secondary); } -.canon-TextFieldInput:focus-visible { +.canon-TextFieldInput[data-focused] { outline-color: var(--canon-border-pressed); outline-width: 0; } @@ -77,20 +97,24 @@ border-color: var(--canon-border-hover); } -.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:focus-visible) { +.canon-TextField[data-focused] .canon-TextFieldInputWrapper { border-color: var(--canon-border-pressed); } -.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-invalid]) { +.canon-TextField[data-invalid] .canon-TextFieldInputWrapper { border-color: var(--canon-fg-danger); } -.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-disabled]) { +.canon-TextField[data-disabled] .canon-TextFieldInputWrapper { opacity: .5; cursor: not-allowed; border: 1px solid var(--canon-border-disabled); } +.canon-TextField[data-disabled] .canon-TextFieldClearButton { + cursor: inherit; +} + .canon-TextFieldInputWrapper[data-size="small"] { height: 2rem; } diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index a1dc9f591a..da1b5015b4 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -1220,10 +1220,10 @@ export interface TextFieldProps className?: string; description?: string; error?: string | null; + icon?: IconNames; label?: string; - leftElementProps?: React.ComponentPropsWithoutRef<'div'>; name: string; - rightElementProps?: React.ComponentPropsWithoutRef<'div'>; + onClear?: React.MouseEventHandler; size?: 'small' | 'medium' | Partial>; } diff --git a/packages/canon/src/components/TextField/TextField.stories.tsx b/packages/canon/src/components/TextField/TextField.stories.tsx index 8c876eab27..df2eb63be3 100644 --- a/packages/canon/src/components/TextField/TextField.stories.tsx +++ b/packages/canon/src/components/TextField/TextField.stories.tsx @@ -15,27 +15,8 @@ */ 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', @@ -129,33 +110,34 @@ export const WithErrorAndDescription: Story = { }, }; -export const WithLeftAndRightElements: Story = { +export const WithIcon: Story = { args: { ...WithLabel.args, placeholder: 'Search...', - leftElementProps: { - children: , - }, - rightElementProps: { - children: , - }, + icon: 'search', }, }; -export const WithLeftAndRightElementsAndHelpText: Story = { +export const DisabledWithIcon: Story = { args: { - ...WithLeftAndRightElements.args, - error: 'Failed to search', - description: 'Enter some text to search', - }, -}; - -export const DisabledWithLeftAndRightElements: Story = { - args: { - ...WithLeftAndRightElements.args, + ...WithIcon.args, + disabled: true, + }, +}; + +export const WithOnClear: Story = { + args: { + ...WithLabel.args, + placeholder: 'Search...', + type: 'search', + onClear: () => null, + }, +}; + +export const DisabledWithOnClear: Story = { + args: { + ...WithOnClear.args, + defaultValue: 'Testing', 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 3830f084b2..ae901c5cc1 100644 --- a/packages/canon/src/components/TextField/TextField.styles.css +++ b/packages/canon/src/components/TextField/TextField.styles.css @@ -59,12 +59,12 @@ background-color: var(--canon-bg-surface-1); } -.canon-TextFieldInputLeftElement { +.canon-TextFieldInputIcon { + display: block; padding-right: var(--canon-space-1); -} - -.canon-TextFieldInputRightElement { - padding-left: var(--canon-space-1); + width: 1.5rem; + height: 1.5rem; + color: var(--canon-fg-primary); } .canon-TextFieldInput { @@ -80,11 +80,32 @@ cursor: inherit; } +.canon-TextFieldInput[type='search']::-webkit-search-cancel-button, +.canon-TextFieldInput[type='search']::-webkit-search-decoration { + appearance: none; +} + +.canon-TextFieldClearButton { + display: none; + padding: 0 0 0 var(--canon-space-1); + background: none; + border: none; + vertical-align: middle; +} + +.canon-TextFieldInput[data-filled] + .canon-TextFieldClearButton { + display: inline-block; +} + +.canon-TextFieldClearButtonIcon { + display: block; +} + .canon-TextFieldInput::placeholder { color: var(--canon-fg-secondary); } -.canon-TextFieldInput:focus-visible { +.canon-TextFieldInput[data-focused] { outline-color: var(--canon-border-pressed); outline-width: 0px; } @@ -93,20 +114,24 @@ border-color: var(--canon-border-hover); } -.canon-TextFieldInputWrapper:has(> .canon-TextFieldInput:focus-visible) { +.canon-TextField[data-focused] .canon-TextFieldInputWrapper { border-color: var(--canon-border-pressed); } -.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-invalid]) { +.canon-TextField[data-invalid] .canon-TextFieldInputWrapper { border-color: var(--canon-fg-danger); } -.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-disabled]) { +.canon-TextField[data-disabled] .canon-TextFieldInputWrapper { opacity: 0.5; cursor: not-allowed; border: 1px solid var(--canon-border-disabled); } +.canon-TextField[data-disabled] .canon-TextFieldClearButton { + cursor: inherit; +} + .canon-TextFieldInputWrapper[data-size='small'] { height: 2rem; } diff --git a/packages/canon/src/components/TextField/TextField.tsx b/packages/canon/src/components/TextField/TextField.tsx index 83640c153a..bb32033da4 100644 --- a/packages/canon/src/components/TextField/TextField.tsx +++ b/packages/canon/src/components/TextField/TextField.tsx @@ -20,6 +20,7 @@ import { useResponsiveValue } from '../../hooks/useResponsiveValue'; import clsx from 'clsx'; import type { TextFieldProps } from './types'; +import { Icon } from '../Icon'; /** @public */ export const TextField = forwardRef( @@ -33,8 +34,8 @@ export const TextField = forwardRef( required, style, disabled, - leftElementProps, - rightElementProps, + icon, + onClear, ...rest } = props; @@ -60,29 +61,21 @@ export const TextField = forwardRef( )}
- {leftElementProps ? ( -
- ) : null} + {icon && } - {rightElementProps ? ( -
- ) : null} + {onClear && ( + + )}
{description && ( diff --git a/packages/canon/src/components/TextField/types.ts b/packages/canon/src/components/TextField/types.ts index fbec9fde4b..8111749845 100644 --- a/packages/canon/src/components/TextField/types.ts +++ b/packages/canon/src/components/TextField/types.ts @@ -15,6 +15,7 @@ */ import type { Breakpoint } from '../../types'; +import type { IconNames } from '../Icon'; /** @public */ export interface TextFieldProps @@ -51,12 +52,12 @@ export interface TextFieldProps error?: string | null; /** - * Props for an element to render on the left of the input + * An icon to render before the input */ - leftElementProps?: React.ComponentPropsWithoutRef<'div'>; + icon?: IconNames; /** - * Props for an element to render on the right of the input + * Handler to call when the clear button is pressed */ - rightElementProps?: React.ComponentPropsWithoutRef<'div'>; + onClear?: React.MouseEventHandler; }