From 6189bfda1dc910a7d725d89c1c47baacb84f3eb7 Mon Sep 17 00:00:00 2001 From: James Brooks Date: Thu, 1 May 2025 11:04:38 +0100 Subject: [PATCH 1/4] Canon - Support left/right elements in TextField Signed-off-by: James Brooks --- .changeset/khaki-grapes-sink.md | 5 ++ packages/canon/css/components.css | 42 ++++++++++++---- packages/canon/css/styles.css | 42 ++++++++++++---- packages/canon/css/textfield.css | 42 ++++++++++++---- packages/canon/src/components/Icon/icons.ts | 4 ++ packages/canon/src/components/Icon/types.ts | 2 + .../TextField/TextField.stories.tsx | 50 +++++++++++++++++++ .../components/TextField/TextField.styles.css | 41 +++++++++++---- .../src/components/TextField/TextField.tsx | 33 +++++++++--- .../canon/src/components/TextField/types.ts | 10 ++++ 10 files changed, 222 insertions(+), 49 deletions(-) create mode 100644 .changeset/khaki-grapes-sink.md 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'>; } From 5605be41f43f3b0069dfad5f0c99462f91428fdb Mon Sep 17 00:00:00 2001 From: James Brooks Date: Thu, 1 May 2025 13:37:32 +0100 Subject: [PATCH 2/4] Update API report Signed-off-by: James Brooks --- packages/canon/report.api.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 765a77148f..a1dc9f591a 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -744,6 +744,7 @@ export type IconNames = | 'chevron-left' | 'chevron-right' | 'chevron-up' + | 'close' | 'cloud' | 'code' | 'discord' @@ -759,6 +760,7 @@ export type IconNames = | 'heart' | 'moon' | 'plus' + | 'search' | 'sidebar-fold' | 'sidebar-unfold' | 'sparkling' @@ -1219,7 +1221,9 @@ export interface TextFieldProps description?: string; error?: string | null; label?: string; + leftElementProps?: React.ComponentPropsWithoutRef<'div'>; name: string; + rightElementProps?: React.ComponentPropsWithoutRef<'div'>; size?: 'small' | 'medium' | Partial>; } From 6f150a3bb7c31f35fae1ac0b16436843839f2352 Mon Sep 17 00:00:00 2001 From: James Brooks Date: Thu, 1 May 2025 14:31:32 +0100 Subject: [PATCH 3/4] Spacing tweaks Signed-off-by: James Brooks --- .../canon/src/components/TextField/TextField.stories.tsx | 2 +- .../canon/src/components/TextField/TextField.styles.css | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/canon/src/components/TextField/TextField.stories.tsx b/packages/canon/src/components/TextField/TextField.stories.tsx index 45a8e1436f..8c876eab27 100644 --- a/packages/canon/src/components/TextField/TextField.stories.tsx +++ b/packages/canon/src/components/TextField/TextField.stories.tsx @@ -155,7 +155,7 @@ export const DisabledWithLeftAndRightElements: Story = { ...WithLeftAndRightElements.args, disabled: true, rightElementProps: { - children: , + children: , }, }, }; diff --git a/packages/canon/src/components/TextField/TextField.styles.css b/packages/canon/src/components/TextField/TextField.styles.css index f30192f274..3830f084b2 100644 --- a/packages/canon/src/components/TextField/TextField.styles.css +++ b/packages/canon/src/components/TextField/TextField.styles.css @@ -53,23 +53,23 @@ .canon-TextFieldInputWrapper { display: flex; align-items: center; + 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); } .canon-TextFieldInputLeftElement { - padding-left: var(--canon-space-4); + padding-right: var(--canon-space-1); } .canon-TextFieldInputRightElement { - padding-right: var(--canon-space-4); + padding-left: var(--canon-space-1); } .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); From 01f101ca79029fc4792e136a34dcf09e128e8446 Mon Sep 17 00:00:00 2001 From: James Brooks Date: Thu, 1 May 2025 17:12:01 +0100 Subject: [PATCH 4/4] Simpler API Signed-off-by: James Brooks --- .changeset/khaki-grapes-sink.md | 2 +- packages/canon/css/components.css | 46 ++++++++++---- packages/canon/css/styles.css | 46 ++++++++++---- packages/canon/css/textfield.css | 46 ++++++++++---- packages/canon/report.api.md | 4 +- .../TextField/TextField.stories.tsx | 60 +++++++------------ .../components/TextField/TextField.styles.css | 43 ++++++++++--- .../src/components/TextField/TextField.tsx | 33 ++++------ .../canon/src/components/TextField/types.ts | 9 +-- 9 files changed, 181 insertions(+), 108 deletions(-) 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; }