diff --git a/.changeset/khaki-waves-lick.md b/.changeset/khaki-waves-lick.md new file mode 100644 index 0000000000..a299424170 --- /dev/null +++ b/.changeset/khaki-waves-lick.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': minor +--- + +Move TextField component to use react Aria under the hood. Introducing a new FieldLabel component to help build custom fields. diff --git a/packages/canon/.storybook/main.ts b/packages/canon/.storybook/main.ts index e58c0f182f..3020ade96e 100644 --- a/packages/canon/.storybook/main.ts +++ b/packages/canon/.storybook/main.ts @@ -9,10 +9,7 @@ function getAbsolutePath(value: string): string { return dirname(require.resolve(join(value, 'package.json'))); } const config: StorybookConfig = { - stories: [ - '../src/components/**/*.stories.@(js|jsx|mjs|ts|tsx)', - '../src/stories/**/*.stories.@(js|jsx|mjs|ts|tsx)', - ], + stories: ['../src/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'], staticDirs: ['../static'], addons: [ getAbsolutePath('@storybook/addon-webpack5-compiler-swc'), diff --git a/packages/canon/css/components.css b/packages/canon/css/components.css index 4ea92e4d86..d420031ada 100644 --- a/packages/canon/css/components.css +++ b/packages/canon/css/components.css @@ -144,80 +144,6 @@ height: 1.25rem; } -.canon-CollapsiblePanel { - height: var(--collapsible-panel-height); - transition: all .15s ease-out; - display: flex; - overflow: hidden; - - &[data-starting-style], &[data-ending-style] { - height: 0; - } -} - -.canon-DataTableRoot { - gap: var(--canon-space-3); - flex-direction: column; - display: flex; -} - -.canon-DataTablePagination { - padding-top: var(--canon-space-5); - justify-content: space-between; - align-items: center; - display: flex; -} - -.canon-DataTablePagination--left { - justify-content: space-between; - align-items: center; - display: flex; -} - -.canon-DataTablePagination--right { - justify-content: space-between; - align-items: center; - gap: var(--canon-space-2); - display: flex; -} - -.canon-DataTablePagination--select { - min-width: 10.5rem; -} - -.canon-Flex { - min-width: 0; - display: flex; -} - -.canon-Grid { - display: grid; -} - -.canon-Container { - max-width: 120rem; - padding-inline: var(--canon-space-4); - margin-inline: auto; - transition: padding .2s ease-in-out; -} - -@media (width >= 640px) { - .canon-Container { - padding-inline: var(--canon-space-8); - } -} - -@media (width >= 1024px) { - .canon-Container { - padding-inline: var(--canon-space-12); - } -} - -.canon-Icon { - width: 1rem; - height: 1rem; -} - .canon-CheckboxRoot { width: 1rem; height: 1rem; @@ -271,6 +197,475 @@ display: flex; } +.canon-CollapsiblePanel { + height: var(--collapsible-panel-height); + transition: all .15s ease-out; + display: flex; + overflow: hidden; + + &[data-starting-style], &[data-ending-style] { + height: 0; + } +} + +.canon-Container { + max-width: 120rem; + padding-inline: var(--canon-space-4); + margin-inline: auto; + transition: padding .2s ease-in-out; +} + +@media (width >= 640px) { + .canon-Container { + padding-inline: var(--canon-space-8); + } +} + +@media (width >= 1024px) { + .canon-Container { + padding-inline: var(--canon-space-12); + } +} + +.canon-DataTableRoot { + gap: var(--canon-space-3); + flex-direction: column; + display: flex; +} + +.canon-DataTablePagination { + padding-top: var(--canon-space-5); + justify-content: space-between; + align-items: center; + display: flex; +} + +.canon-DataTablePagination--left { + justify-content: space-between; + align-items: center; + display: flex; +} + +.canon-DataTablePagination--right { + justify-content: space-between; + align-items: center; + gap: var(--canon-space-2); + display: flex; +} + +.canon-DataTablePagination--select { + min-width: 10.5rem; +} + +.canon-FieldLabel { + margin-bottom: var(--canon-space-3); + gap: var(--canon-space-1); + flex-direction: column; + display: flex; +} + +.canon-FieldLabelLabel { + color: var(--canon-fg-primary); + cursor: pointer; + font-weight: var(--canon-font-weight-regular); + font-size: var(--canon-font-size-2); + margin-right: auto; +} + +.canon-FieldLabelSecondaryLabel { + color: var(--canon-fg-secondary); + font-weight: var(--canon-font-weight-regular); + margin-left: var(--canon-space-1); +} + +.canon-FieldLabelDescription { + font-weight: var(--canon-font-weight-regular); + font-size: var(--canon-font-size-2); + color: var(--canon-fg-secondary); + margin: 0; +} + +.canon-Flex { + min-width: 0; + display: flex; +} + +.canon-Grid { + display: grid; +} + +.canon-Heading { + font-family: var(--canon-font-regular); + color: var(--canon-fg-primary); + margin: 0; + padding: 0; + line-height: 100%; +} + +.canon-Heading[data-variant="display"] { + font-size: var(--canon-font-size-10); + font-weight: var(--canon-font-weight-bold); +} + +.canon-Heading[data-variant="title1"] { + font-size: var(--canon-font-size-9); + font-weight: var(--canon-font-weight-bold); +} + +.canon-Heading[data-variant="title2"] { + font-size: var(--canon-font-size-8); + font-weight: var(--canon-font-weight-bold); +} + +.canon-Heading[data-variant="title3"] { + font-size: var(--canon-font-size-7); + font-weight: var(--canon-font-weight-bold); +} + +.canon-Heading[data-variant="title4"] { + font-size: var(--canon-font-size-6); + font-weight: var(--canon-font-weight-bold); +} + +.canon-Heading[data-variant="title5"] { + font-size: var(--canon-font-size-5); + font-weight: var(--canon-font-weight-bold); +} + +.canon-Heading[data-color="primary"] { + color: var(--canon-fg-primary); +} + +.canon-Heading[data-color="secondary"] { + color: var(--canon-fg-secondary); +} + +.canon-Heading[data-truncate] { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.canon-Icon { + width: 1rem; + height: 1rem; +} + +.canon-IconButton { + user-select: none; + font-family: var(--canon-font-regular); + font-weight: var(--canon-font-weight-bold); + cursor: pointer; + border-radius: var(--canon-radius-2); + justify-content: center; + align-items: center; + gap: var(--canon-space-1_5); + border: none; + padding: 0; + display: inline-flex; + + &:disabled { + cursor: not-allowed; + } +} + +.canon-IconButton[data-variant="primary"] { + background-color: var(--canon-bg-solid); + color: var(--canon-fg-solid); + transition: background-color .15s, box-shadow .15s; + + &:hover { + background-color: var(--canon-bg-solid-hover); + } + + &:active { + background-color: var(--canon-bg-solid-pressed); + } + + &:focus-visible { + outline: 2px solid var(--canon-ring); + outline-offset: 2px; + } + + &:disabled { + background-color: var(--canon-bg-solid-disabled); + color: var(--canon-fg-solid-disabled); + } +} + +.canon-IconButton[data-variant="secondary"] { + background-color: var(--canon-bg-surface-1); + box-shadow: inset 0 0 0 1px var(--canon-border); + color: var(--canon-fg-primary); + transition: box-shadow .15s; + + &:hover { + box-shadow: inset 0 0 0 1px var(--canon-border-hover); + } + + &:active { + box-shadow: inset 0 0 0 1px var(--canon-border-pressed); + } + + &:focus-visible { + box-shadow: inset 0 0 0 2px var(--canon-ring); + outline: none; + transition: none; + } + + &:disabled { + box-shadow: inset 0 0 0 1px var(--canon-border-disabled); + color: var(--canon-fg-disabled); + } +} + +.canon-IconButton[data-size="medium"] { + font-size: var(--canon-font-size-4); + width: 40px; + height: 40px; +} + +.canon-IconButton[data-size="small"] { + font-size: var(--canon-font-size-3); + width: 32px; + height: 32px; +} + +.canon-IconButtonIcon[data-size="small"], .canon-IconButtonIcon[data-size="small"] svg { + width: 1rem; + height: 1rem; +} + +.canon-IconButtonIcon[data-size="medium"], .canon-IconButtonIcon[data-size="medium"] svg { + width: 1.25rem; + height: 1.25rem; +} + +.canon-Link { + font-family: var(--canon-font-regular); + color: var(--canon-fg-link); + cursor: pointer; + margin: 0; + padding: 0; + text-decoration-line: none; + + &:hover { + color: var(--canon-fg-link-hover); + text-underline-offset: calc(.025em + 2px); + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-thickness: min(2px, max(1px, .05em)); + text-decoration-color: color-mix(in srgb, var(--canon-fg-link-hover) 30%, transparent); + } +} + +.canon-Link[data-variant="body"] { + font-size: var(--canon-font-size-3); + line-height: 140%; +} + +.canon-Link[data-variant="subtitle"] { + font-size: var(--canon-font-size-4); + line-height: 140%; +} + +.canon-Link[data-variant="caption"] { + font-size: var(--canon-font-size-2); + line-height: 140%; +} + +.canon-Link[data-variant="label"] { + font-size: var(--canon-font-size-1); + line-height: 140%; +} + +.canon-Link[data-weight="regular"] { + font-weight: var(--canon-font-weight-regular); +} + +.canon-Link[data-weight="bold"] { + font-weight: var(--canon-font-weight-bold); +} + +.canon-MenuPositioner { + outline: 0; +} + +.canon-MenuPopup { + background-color: var(--canon-bg-surface-1); + border: 1px solid var(--canon-border); + color: var(--canon-fg-primary); + transform-origin: var(--transform-origin); + max-width: min(var(--available-width), 340px); + max-height: min(var(--available-height), 500px); + padding-bottom: var(--canon-space-1); + border-radius: .375rem; + outline: none; + flex-direction: column; + transition: transform .15s, opacity .15s; + display: flex; + position: relative; + overflow: auto; + + &[data-starting-style], &[data-ending-style] { + opacity: 0; + transform: scale(.9); + } +} + +.canon-MenuItem { + user-select: none; + align-items: center; + gap: var(--canon-space-2); + height: 32px; + color: var(--canon-fg-primary); + border-radius: var(--canon-radius-2); + margin-inline: var(--canon-space-1); + padding-inline: var(--canon-space-2); + font-size: var(--canon-font-size-3); + cursor: pointer; + outline: 0; + flex-shrink: 0; + text-decoration: none; + display: flex; + + &:first-child { + margin-top: var(--canon-space-1); + } + + &[data-highlighted] { + background-color: var(--canon-gray-3); + } +} + +.canon-MenuSubmenuTrigger { + user-select: none; + justify-content: space-between; + align-items: center; + gap: var(--canon-space-2); + height: 32px; + color: var(--canon-fg-primary); + border-radius: var(--canon-radius-2); + margin-inline: var(--canon-space-1); + padding-inline: var(--canon-space-2); + font-size: var(--canon-font-size-3); + cursor: pointer; + outline: 0; + flex-shrink: 0; + text-decoration: none; + display: flex; + + & .canon-Icon { + color: var(--canon-fg-secondary); + } + + &:first-child { + margin-top: var(--canon-space-1); + } + + &[data-popup-open], &[data-highlighted] { + background-color: var(--canon-gray-3); + + & .canon-Icon { + color: var(--canon-fg-primary); + } + } +} + +.canon-MenuSeparator { + background-color: var(--color-gray-200); + height: 1px; + margin: .375rem 1rem; +} + +.canon-SubmenuComboboxSearch { + padding-inline: var(--canon-space-3); + border: none; + border-bottom: 1px solid var(--canon-border); + background-color: var(--canon-bg-surface-1); + width: 100%; + height: 32px; + color: var(--canon-fg-primary); + line-height: 140%; + font-size: var(--canon-font-size-3); + z-index: 1; + outline: none; + position: sticky; + top: 0; + + &::placeholder { + color: var(--canon-fg-secondary); + } + + &:disabled { + opacity: .6; + cursor: not-allowed; + } +} + +.canon-SubmenuComboboxItems { + padding-top: var(--canon-space-2); + outline: none; + flex-direction: column; + display: flex; + overflow-y: auto; +} + +.canon-SubmenuComboboxNoResults { + padding-inline: var(--canon-space-3); + padding-top: var(--canon-space-2); + padding-bottom: var(--canon-space-4); + color: var(--canon-fg-secondary); + font-size: var(--canon-font-size-3); +} + +.canon-SubmenuComboboxItem { + user-select: none; + justify-content: space-between; + align-items: center; + gap: var(--canon-space-2); + height: 32px; + color: var(--canon-fg-primary); + border-radius: var(--canon-radius-2); + margin-inline: var(--canon-space-1); + padding-inline: var(--canon-space-2); + font-size: var(--canon-font-size-3); + cursor: pointer; + outline: 0; + flex-shrink: 0; + text-decoration: none; + display: flex; + + &[data-highlighted] { + background-color: var(--canon-gray-3); + } + + &[data-disabled] { + opacity: .5; + cursor: not-allowed; + } +} + +.canon-SubmenuComboboxItemCheckbox { + width: 16px; + height: 16px; + color: var(--canon-fg-primary); + border-radius: var(--canon-radius-2); + border: 1px solid var(--canon-border); + background: var(--canon-bg-surface-1); + flex-shrink: 0; + justify-content: center; + align-items: center; + display: flex; +} + +.canon-SubmenuComboboxItemLabel { + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + overflow: hidden; +} + .canon-TableRoot { caption-side: bottom; border-collapse: collapse; @@ -487,148 +882,6 @@ overflow: hidden; } -.canon-Heading { - font-family: var(--canon-font-regular); - color: var(--canon-fg-primary); - margin: 0; - padding: 0; - line-height: 100%; -} - -.canon-Heading[data-variant="display"] { - font-size: var(--canon-font-size-10); - font-weight: var(--canon-font-weight-bold); -} - -.canon-Heading[data-variant="title1"] { - font-size: var(--canon-font-size-9); - font-weight: var(--canon-font-weight-bold); -} - -.canon-Heading[data-variant="title2"] { - font-size: var(--canon-font-size-8); - font-weight: var(--canon-font-weight-bold); -} - -.canon-Heading[data-variant="title3"] { - font-size: var(--canon-font-size-7); - font-weight: var(--canon-font-weight-bold); -} - -.canon-Heading[data-variant="title4"] { - font-size: var(--canon-font-size-6); - font-weight: var(--canon-font-weight-bold); -} - -.canon-Heading[data-variant="title5"] { - font-size: var(--canon-font-size-5); - font-weight: var(--canon-font-weight-bold); -} - -.canon-Heading[data-color="primary"] { - color: var(--canon-fg-primary); -} - -.canon-Heading[data-color="secondary"] { - color: var(--canon-fg-secondary); -} - -.canon-Heading[data-truncate] { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -} - -.canon-IconButton { - user-select: none; - font-family: var(--canon-font-regular); - font-weight: var(--canon-font-weight-bold); - cursor: pointer; - border-radius: var(--canon-radius-2); - justify-content: center; - align-items: center; - gap: var(--canon-space-1_5); - border: none; - padding: 0; - display: inline-flex; - - &:disabled { - cursor: not-allowed; - } -} - -.canon-IconButton[data-variant="primary"] { - background-color: var(--canon-bg-solid); - color: var(--canon-fg-solid); - transition: background-color .15s, box-shadow .15s; - - &:hover { - background-color: var(--canon-bg-solid-hover); - } - - &:active { - background-color: var(--canon-bg-solid-pressed); - } - - &:focus-visible { - outline: 2px solid var(--canon-ring); - outline-offset: 2px; - } - - &:disabled { - background-color: var(--canon-bg-solid-disabled); - color: var(--canon-fg-solid-disabled); - } -} - -.canon-IconButton[data-variant="secondary"] { - background-color: var(--canon-bg-surface-1); - box-shadow: inset 0 0 0 1px var(--canon-border); - color: var(--canon-fg-primary); - transition: box-shadow .15s; - - &:hover { - box-shadow: inset 0 0 0 1px var(--canon-border-hover); - } - - &:active { - box-shadow: inset 0 0 0 1px var(--canon-border-pressed); - } - - &:focus-visible { - box-shadow: inset 0 0 0 2px var(--canon-ring); - outline: none; - transition: none; - } - - &:disabled { - box-shadow: inset 0 0 0 1px var(--canon-border-disabled); - color: var(--canon-fg-disabled); - } -} - -.canon-IconButton[data-size="medium"] { - font-size: var(--canon-font-size-4); - width: 40px; - height: 40px; -} - -.canon-IconButton[data-size="small"] { - font-size: var(--canon-font-size-3); - width: 32px; - height: 32px; -} - -.canon-IconButtonIcon[data-size="small"], .canon-IconButtonIcon[data-size="small"] svg { - width: 1rem; - height: 1rem; -} - -.canon-IconButtonIcon[data-size="medium"], .canon-IconButtonIcon[data-size="medium"] svg { - width: 1.25rem; - height: 1.25rem; -} - .canon-TextField { font-family: var(--canon-font-regular); flex-direction: column; @@ -636,86 +889,26 @@ display: flex; } -.canon-TextFieldLabelWrapper { - margin-bottom: var(--canon-space-3); - gap: var(--canon-space-1); - flex-direction: column; - display: flex; -} - -.canon-TextFieldLabelWrapper[data-hidden] { - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - position: absolute; - overflow: hidden; -} - -.canon-TextFieldLabel { - color: var(--canon-fg-primary); - cursor: pointer; - margin-right: auto; -} - -.canon-TextFieldLabel[data-size="small"] { - font-weight: var(--canon-font-weight-regular); - font-size: var(--canon-font-size-2); -} - -.canon-TextFieldLabel[data-size="medium"] { - font-weight: var(--canon-font-weight-bold); - font-size: var(--canon-font-size-3); -} - -.canon-TextFieldLabel[data-disabled] { - cursor: default; -} - -.canon-TextFieldSecondaryLabel { - color: var(--canon-fg-secondary); - font-weight: var(--canon-font-weight-regular); - margin-left: var(--canon-space-1); -} - -.canon-TextFieldDescription { - font-weight: var(--canon-font-weight-regular); - color: var(--canon-fg-secondary); - margin: 0; -} - -.canon-TextFieldDescription[data-size="small"] { - font-size: var(--canon-font-size-2); -} - -.canon-TextFieldDescription[data-size="medium"] { - font-size: var(--canon-font-size-3); -} - -.canon-TextFieldError { - font-size: var(--canon-font-size-2); - font-weight: var(--canon-font-weight-regular); - color: var(--canon-fg-danger); - padding-top: var(--canon-space-1_5); - margin: 0; -} - .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); - align-items: center; - display: flex; + position: relative; +} + +.canon-TextFieldInputWrapper[data-size="small"] { + height: 2rem; +} + +.canon-TextFieldInputWrapper[data-size="medium"] { + height: 2.5rem; } .canon-TextFieldIcon { + left: var(--canon-space-3); margin-right: var(--canon-space-1); color: var(--canon-fg-primary); flex-shrink: 0; + position: absolute; + top: 50%; + transform: translateY(-50%); } .canon-TextFieldIcon[data-size="small"], .canon-TextFieldIcon[data-size="small"] svg { @@ -729,6 +922,10 @@ } .canon-TextFieldInput { + 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); font-size: var(--canon-font-size-3); font-family: var(--canon-font-regular); font-weight: var(--canon-font-weight-regular); @@ -736,300 +933,48 @@ width: 100%; height: 100%; cursor: inherit; - background: none; - border: none; - padding: 0; + align-items: center; transition: border-color .2s ease-in-out, outline-color .2s ease-in-out; -} - -.canon-TextFieldInput:not([data-filled]):has( + .canon-TextFieldClearButton) { - padding-right: 1.25rem; -} - -.canon-TextFieldInput[type="search"]::-webkit-search-cancel-button, .canon-TextFieldInput[type="search"]::-webkit-search-decoration { - appearance: none; -} - -.canon-TextFieldClearButton { - margin-left: var(--canon-space-1); - vertical-align: middle; - color: var(--canon-fg-primary); - background: none; - border: none; - padding: 0; - display: none; -} - -.canon-TextFieldInput[data-filled] + .canon-TextFieldClearButton { - display: inline-block; -} - -.canon-TextFieldClearButtonIcon { - display: block; + display: flex; } .canon-TextFieldInput::placeholder { color: var(--canon-fg-secondary); } +.canon-TextFieldInput[data-icon] { + padding-left: var(--canon-space-8); +} + .canon-TextFieldInput[data-focused] { outline-color: var(--canon-border-pressed); outline-width: 0; } -.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:hover) { +.canon-TextFieldInput[data-hovered] { border-color: var(--canon-border-hover); } -.canon-TextField[data-focused] .canon-TextFieldInputWrapper { +.canon-TextFieldInput[data-focused] { border-color: var(--canon-border-pressed); + outline-width: 0; } -.canon-TextField[data-invalid] .canon-TextFieldInputWrapper { +.canon-TextFieldInput[data-invalid] { border-color: var(--canon-fg-danger); } -.canon-TextField[data-disabled] .canon-TextFieldInputWrapper { +.canon-TextFieldInput[data-disabled] { 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; -} - -.canon-TextFieldInputWrapper[data-size="medium"] { - height: 2.5rem; -} - -.canon-MenuPositioner { - outline: 0; -} - -.canon-MenuPopup { - background-color: var(--canon-bg-surface-1); - border: 1px solid var(--canon-border); - color: var(--canon-fg-primary); - transform-origin: var(--transform-origin); - max-width: min(var(--available-width), 340px); - max-height: min(var(--available-height), 500px); - padding-bottom: var(--canon-space-1); - border-radius: .375rem; - outline: none; - flex-direction: column; - transition: transform .15s, opacity .15s; - display: flex; - position: relative; - overflow: auto; - - &[data-starting-style], &[data-ending-style] { - opacity: 0; - transform: scale(.9); - } -} - -.canon-MenuItem { - user-select: none; - align-items: center; - gap: var(--canon-space-2); - height: 32px; - color: var(--canon-fg-primary); - border-radius: var(--canon-radius-2); - margin-inline: var(--canon-space-1); - padding-inline: var(--canon-space-2); - font-size: var(--canon-font-size-3); - cursor: pointer; - outline: 0; - flex-shrink: 0; - text-decoration: none; - display: flex; - - &:first-child { - margin-top: var(--canon-space-1); - } - - &[data-highlighted] { - background-color: var(--canon-gray-3); - } -} - -.canon-MenuSubmenuTrigger { - user-select: none; - justify-content: space-between; - align-items: center; - gap: var(--canon-space-2); - height: 32px; - color: var(--canon-fg-primary); - border-radius: var(--canon-radius-2); - margin-inline: var(--canon-space-1); - padding-inline: var(--canon-space-2); - font-size: var(--canon-font-size-3); - cursor: pointer; - outline: 0; - flex-shrink: 0; - text-decoration: none; - display: flex; - - & .canon-Icon { - color: var(--canon-fg-secondary); - } - - &:first-child { - margin-top: var(--canon-space-1); - } - - &[data-popup-open], &[data-highlighted] { - background-color: var(--canon-gray-3); - - & .canon-Icon { - color: var(--canon-fg-primary); - } - } -} - -.canon-MenuSeparator { - background-color: var(--color-gray-200); - height: 1px; - margin: .375rem 1rem; -} - -.canon-SubmenuComboboxSearch { - padding-inline: var(--canon-space-3); - border: none; - border-bottom: 1px solid var(--canon-border); - background-color: var(--canon-bg-surface-1); - width: 100%; - height: 32px; - color: var(--canon-fg-primary); - line-height: 140%; - font-size: var(--canon-font-size-3); - z-index: 1; - outline: none; - position: sticky; - top: 0; - - &::placeholder { - color: var(--canon-fg-secondary); - } - - &:disabled { - opacity: .6; - cursor: not-allowed; - } -} - -.canon-SubmenuComboboxItems { - padding-top: var(--canon-space-2); - outline: none; - flex-direction: column; - display: flex; - overflow-y: auto; -} - -.canon-SubmenuComboboxNoResults { - padding-inline: var(--canon-space-3); - padding-top: var(--canon-space-2); - padding-bottom: var(--canon-space-4); - color: var(--canon-fg-secondary); - font-size: var(--canon-font-size-3); -} - -.canon-SubmenuComboboxItem { - user-select: none; - justify-content: space-between; - align-items: center; - gap: var(--canon-space-2); - height: 32px; - color: var(--canon-fg-primary); - border-radius: var(--canon-radius-2); - margin-inline: var(--canon-space-1); - padding-inline: var(--canon-space-2); - font-size: var(--canon-font-size-3); - cursor: pointer; - outline: 0; - flex-shrink: 0; - text-decoration: none; - display: flex; - - &[data-highlighted] { - background-color: var(--canon-gray-3); - } - - &[data-disabled] { - opacity: .5; - cursor: not-allowed; - } -} - -.canon-SubmenuComboboxItemCheckbox { - width: 16px; - height: 16px; - color: var(--canon-fg-primary); - border-radius: var(--canon-radius-2); - border: 1px solid var(--canon-border); - background: var(--canon-bg-surface-1); - flex-shrink: 0; - justify-content: center; - align-items: center; - display: flex; -} - -.canon-SubmenuComboboxItemLabel { - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; - overflow: hidden; -} - -.canon-Link { - font-family: var(--canon-font-regular); - color: var(--canon-fg-link); - cursor: pointer; - margin: 0; - padding: 0; - text-decoration-line: none; - - &:hover { - color: var(--canon-fg-link-hover); - text-underline-offset: calc(.025em + 2px); - text-decoration-line: underline; - text-decoration-style: solid; - text-decoration-thickness: min(2px, max(1px, .05em)); - text-decoration-color: color-mix(in srgb, var(--canon-fg-link-hover) 30%, transparent); - } -} - -.canon-Link[data-variant="body"] { - font-size: var(--canon-font-size-3); - line-height: 140%; -} - -.canon-Link[data-variant="subtitle"] { - font-size: var(--canon-font-size-4); - line-height: 140%; -} - -.canon-Link[data-variant="caption"] { +.canon-TextFieldError { + color: var(--canon-fg-danger); font-size: var(--canon-font-size-2); - line-height: 140%; -} - -.canon-Link[data-variant="label"] { - font-size: var(--canon-font-size-1); - line-height: 140%; -} - -.canon-Link[data-weight="regular"] { font-weight: var(--canon-font-weight-regular); -} - -.canon-Link[data-weight="bold"] { - font-weight: var(--canon-font-weight-bold); + margin-top: var(--canon-space-2); } .canon-TooltipPopup { diff --git a/packages/canon/css/fieldlabel.css b/packages/canon/css/fieldlabel.css new file mode 100644 index 0000000000..1b708ed580 --- /dev/null +++ b/packages/canon/css/fieldlabel.css @@ -0,0 +1,27 @@ +.canon-FieldLabel { + margin-bottom: var(--canon-space-3); + gap: var(--canon-space-1); + flex-direction: column; + display: flex; +} + +.canon-FieldLabelLabel { + color: var(--canon-fg-primary); + cursor: pointer; + font-weight: var(--canon-font-weight-regular); + font-size: var(--canon-font-size-2); + margin-right: auto; +} + +.canon-FieldLabelSecondaryLabel { + color: var(--canon-fg-secondary); + font-weight: var(--canon-font-weight-regular); + margin-left: var(--canon-space-1); +} + +.canon-FieldLabelDescription { + font-weight: var(--canon-font-weight-regular); + font-size: var(--canon-font-size-2); + color: var(--canon-fg-secondary); + margin: 0; +} diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css index 0bb27fbd56..b2d111b185 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -9368,80 +9368,6 @@ height: 1.25rem; } -.canon-CollapsiblePanel { - height: var(--collapsible-panel-height); - transition: all .15s ease-out; - display: flex; - overflow: hidden; - - &[data-starting-style], &[data-ending-style] { - height: 0; - } -} - -.canon-DataTableRoot { - gap: var(--canon-space-3); - flex-direction: column; - display: flex; -} - -.canon-DataTablePagination { - padding-top: var(--canon-space-5); - justify-content: space-between; - align-items: center; - display: flex; -} - -.canon-DataTablePagination--left { - justify-content: space-between; - align-items: center; - display: flex; -} - -.canon-DataTablePagination--right { - justify-content: space-between; - align-items: center; - gap: var(--canon-space-2); - display: flex; -} - -.canon-DataTablePagination--select { - min-width: 10.5rem; -} - -.canon-Flex { - min-width: 0; - display: flex; -} - -.canon-Grid { - display: grid; -} - -.canon-Container { - max-width: 120rem; - padding-inline: var(--canon-space-4); - margin-inline: auto; - transition: padding .2s ease-in-out; -} - -@media (width >= 640px) { - .canon-Container { - padding-inline: var(--canon-space-8); - } -} - -@media (width >= 1024px) { - .canon-Container { - padding-inline: var(--canon-space-12); - } -} - -.canon-Icon { - width: 1rem; - height: 1rem; -} - .canon-CheckboxRoot { width: 1rem; height: 1rem; @@ -9495,6 +9421,475 @@ display: flex; } +.canon-CollapsiblePanel { + height: var(--collapsible-panel-height); + transition: all .15s ease-out; + display: flex; + overflow: hidden; + + &[data-starting-style], &[data-ending-style] { + height: 0; + } +} + +.canon-Container { + max-width: 120rem; + padding-inline: var(--canon-space-4); + margin-inline: auto; + transition: padding .2s ease-in-out; +} + +@media (width >= 640px) { + .canon-Container { + padding-inline: var(--canon-space-8); + } +} + +@media (width >= 1024px) { + .canon-Container { + padding-inline: var(--canon-space-12); + } +} + +.canon-DataTableRoot { + gap: var(--canon-space-3); + flex-direction: column; + display: flex; +} + +.canon-DataTablePagination { + padding-top: var(--canon-space-5); + justify-content: space-between; + align-items: center; + display: flex; +} + +.canon-DataTablePagination--left { + justify-content: space-between; + align-items: center; + display: flex; +} + +.canon-DataTablePagination--right { + justify-content: space-between; + align-items: center; + gap: var(--canon-space-2); + display: flex; +} + +.canon-DataTablePagination--select { + min-width: 10.5rem; +} + +.canon-FieldLabel { + margin-bottom: var(--canon-space-3); + gap: var(--canon-space-1); + flex-direction: column; + display: flex; +} + +.canon-FieldLabelLabel { + color: var(--canon-fg-primary); + cursor: pointer; + font-weight: var(--canon-font-weight-regular); + font-size: var(--canon-font-size-2); + margin-right: auto; +} + +.canon-FieldLabelSecondaryLabel { + color: var(--canon-fg-secondary); + font-weight: var(--canon-font-weight-regular); + margin-left: var(--canon-space-1); +} + +.canon-FieldLabelDescription { + font-weight: var(--canon-font-weight-regular); + font-size: var(--canon-font-size-2); + color: var(--canon-fg-secondary); + margin: 0; +} + +.canon-Flex { + min-width: 0; + display: flex; +} + +.canon-Grid { + display: grid; +} + +.canon-Heading { + font-family: var(--canon-font-regular); + color: var(--canon-fg-primary); + margin: 0; + padding: 0; + line-height: 100%; +} + +.canon-Heading[data-variant="display"] { + font-size: var(--canon-font-size-10); + font-weight: var(--canon-font-weight-bold); +} + +.canon-Heading[data-variant="title1"] { + font-size: var(--canon-font-size-9); + font-weight: var(--canon-font-weight-bold); +} + +.canon-Heading[data-variant="title2"] { + font-size: var(--canon-font-size-8); + font-weight: var(--canon-font-weight-bold); +} + +.canon-Heading[data-variant="title3"] { + font-size: var(--canon-font-size-7); + font-weight: var(--canon-font-weight-bold); +} + +.canon-Heading[data-variant="title4"] { + font-size: var(--canon-font-size-6); + font-weight: var(--canon-font-weight-bold); +} + +.canon-Heading[data-variant="title5"] { + font-size: var(--canon-font-size-5); + font-weight: var(--canon-font-weight-bold); +} + +.canon-Heading[data-color="primary"] { + color: var(--canon-fg-primary); +} + +.canon-Heading[data-color="secondary"] { + color: var(--canon-fg-secondary); +} + +.canon-Heading[data-truncate] { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.canon-Icon { + width: 1rem; + height: 1rem; +} + +.canon-IconButton { + user-select: none; + font-family: var(--canon-font-regular); + font-weight: var(--canon-font-weight-bold); + cursor: pointer; + border-radius: var(--canon-radius-2); + justify-content: center; + align-items: center; + gap: var(--canon-space-1_5); + border: none; + padding: 0; + display: inline-flex; + + &:disabled { + cursor: not-allowed; + } +} + +.canon-IconButton[data-variant="primary"] { + background-color: var(--canon-bg-solid); + color: var(--canon-fg-solid); + transition: background-color .15s, box-shadow .15s; + + &:hover { + background-color: var(--canon-bg-solid-hover); + } + + &:active { + background-color: var(--canon-bg-solid-pressed); + } + + &:focus-visible { + outline: 2px solid var(--canon-ring); + outline-offset: 2px; + } + + &:disabled { + background-color: var(--canon-bg-solid-disabled); + color: var(--canon-fg-solid-disabled); + } +} + +.canon-IconButton[data-variant="secondary"] { + background-color: var(--canon-bg-surface-1); + box-shadow: inset 0 0 0 1px var(--canon-border); + color: var(--canon-fg-primary); + transition: box-shadow .15s; + + &:hover { + box-shadow: inset 0 0 0 1px var(--canon-border-hover); + } + + &:active { + box-shadow: inset 0 0 0 1px var(--canon-border-pressed); + } + + &:focus-visible { + box-shadow: inset 0 0 0 2px var(--canon-ring); + outline: none; + transition: none; + } + + &:disabled { + box-shadow: inset 0 0 0 1px var(--canon-border-disabled); + color: var(--canon-fg-disabled); + } +} + +.canon-IconButton[data-size="medium"] { + font-size: var(--canon-font-size-4); + width: 40px; + height: 40px; +} + +.canon-IconButton[data-size="small"] { + font-size: var(--canon-font-size-3); + width: 32px; + height: 32px; +} + +.canon-IconButtonIcon[data-size="small"], .canon-IconButtonIcon[data-size="small"] svg { + width: 1rem; + height: 1rem; +} + +.canon-IconButtonIcon[data-size="medium"], .canon-IconButtonIcon[data-size="medium"] svg { + width: 1.25rem; + height: 1.25rem; +} + +.canon-Link { + font-family: var(--canon-font-regular); + color: var(--canon-fg-link); + cursor: pointer; + margin: 0; + padding: 0; + text-decoration-line: none; + + &:hover { + color: var(--canon-fg-link-hover); + text-underline-offset: calc(.025em + 2px); + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-thickness: min(2px, max(1px, .05em)); + text-decoration-color: color-mix(in srgb, var(--canon-fg-link-hover) 30%, transparent); + } +} + +.canon-Link[data-variant="body"] { + font-size: var(--canon-font-size-3); + line-height: 140%; +} + +.canon-Link[data-variant="subtitle"] { + font-size: var(--canon-font-size-4); + line-height: 140%; +} + +.canon-Link[data-variant="caption"] { + font-size: var(--canon-font-size-2); + line-height: 140%; +} + +.canon-Link[data-variant="label"] { + font-size: var(--canon-font-size-1); + line-height: 140%; +} + +.canon-Link[data-weight="regular"] { + font-weight: var(--canon-font-weight-regular); +} + +.canon-Link[data-weight="bold"] { + font-weight: var(--canon-font-weight-bold); +} + +.canon-MenuPositioner { + outline: 0; +} + +.canon-MenuPopup { + background-color: var(--canon-bg-surface-1); + border: 1px solid var(--canon-border); + color: var(--canon-fg-primary); + transform-origin: var(--transform-origin); + max-width: min(var(--available-width), 340px); + max-height: min(var(--available-height), 500px); + padding-bottom: var(--canon-space-1); + border-radius: .375rem; + outline: none; + flex-direction: column; + transition: transform .15s, opacity .15s; + display: flex; + position: relative; + overflow: auto; + + &[data-starting-style], &[data-ending-style] { + opacity: 0; + transform: scale(.9); + } +} + +.canon-MenuItem { + user-select: none; + align-items: center; + gap: var(--canon-space-2); + height: 32px; + color: var(--canon-fg-primary); + border-radius: var(--canon-radius-2); + margin-inline: var(--canon-space-1); + padding-inline: var(--canon-space-2); + font-size: var(--canon-font-size-3); + cursor: pointer; + outline: 0; + flex-shrink: 0; + text-decoration: none; + display: flex; + + &:first-child { + margin-top: var(--canon-space-1); + } + + &[data-highlighted] { + background-color: var(--canon-gray-3); + } +} + +.canon-MenuSubmenuTrigger { + user-select: none; + justify-content: space-between; + align-items: center; + gap: var(--canon-space-2); + height: 32px; + color: var(--canon-fg-primary); + border-radius: var(--canon-radius-2); + margin-inline: var(--canon-space-1); + padding-inline: var(--canon-space-2); + font-size: var(--canon-font-size-3); + cursor: pointer; + outline: 0; + flex-shrink: 0; + text-decoration: none; + display: flex; + + & .canon-Icon { + color: var(--canon-fg-secondary); + } + + &:first-child { + margin-top: var(--canon-space-1); + } + + &[data-popup-open], &[data-highlighted] { + background-color: var(--canon-gray-3); + + & .canon-Icon { + color: var(--canon-fg-primary); + } + } +} + +.canon-MenuSeparator { + background-color: var(--color-gray-200); + height: 1px; + margin: .375rem 1rem; +} + +.canon-SubmenuComboboxSearch { + padding-inline: var(--canon-space-3); + border: none; + border-bottom: 1px solid var(--canon-border); + background-color: var(--canon-bg-surface-1); + width: 100%; + height: 32px; + color: var(--canon-fg-primary); + line-height: 140%; + font-size: var(--canon-font-size-3); + z-index: 1; + outline: none; + position: sticky; + top: 0; + + &::placeholder { + color: var(--canon-fg-secondary); + } + + &:disabled { + opacity: .6; + cursor: not-allowed; + } +} + +.canon-SubmenuComboboxItems { + padding-top: var(--canon-space-2); + outline: none; + flex-direction: column; + display: flex; + overflow-y: auto; +} + +.canon-SubmenuComboboxNoResults { + padding-inline: var(--canon-space-3); + padding-top: var(--canon-space-2); + padding-bottom: var(--canon-space-4); + color: var(--canon-fg-secondary); + font-size: var(--canon-font-size-3); +} + +.canon-SubmenuComboboxItem { + user-select: none; + justify-content: space-between; + align-items: center; + gap: var(--canon-space-2); + height: 32px; + color: var(--canon-fg-primary); + border-radius: var(--canon-radius-2); + margin-inline: var(--canon-space-1); + padding-inline: var(--canon-space-2); + font-size: var(--canon-font-size-3); + cursor: pointer; + outline: 0; + flex-shrink: 0; + text-decoration: none; + display: flex; + + &[data-highlighted] { + background-color: var(--canon-gray-3); + } + + &[data-disabled] { + opacity: .5; + cursor: not-allowed; + } +} + +.canon-SubmenuComboboxItemCheckbox { + width: 16px; + height: 16px; + color: var(--canon-fg-primary); + border-radius: var(--canon-radius-2); + border: 1px solid var(--canon-border); + background: var(--canon-bg-surface-1); + flex-shrink: 0; + justify-content: center; + align-items: center; + display: flex; +} + +.canon-SubmenuComboboxItemLabel { + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + overflow: hidden; +} + .canon-TableRoot { caption-side: bottom; border-collapse: collapse; @@ -9711,148 +10106,6 @@ overflow: hidden; } -.canon-Heading { - font-family: var(--canon-font-regular); - color: var(--canon-fg-primary); - margin: 0; - padding: 0; - line-height: 100%; -} - -.canon-Heading[data-variant="display"] { - font-size: var(--canon-font-size-10); - font-weight: var(--canon-font-weight-bold); -} - -.canon-Heading[data-variant="title1"] { - font-size: var(--canon-font-size-9); - font-weight: var(--canon-font-weight-bold); -} - -.canon-Heading[data-variant="title2"] { - font-size: var(--canon-font-size-8); - font-weight: var(--canon-font-weight-bold); -} - -.canon-Heading[data-variant="title3"] { - font-size: var(--canon-font-size-7); - font-weight: var(--canon-font-weight-bold); -} - -.canon-Heading[data-variant="title4"] { - font-size: var(--canon-font-size-6); - font-weight: var(--canon-font-weight-bold); -} - -.canon-Heading[data-variant="title5"] { - font-size: var(--canon-font-size-5); - font-weight: var(--canon-font-weight-bold); -} - -.canon-Heading[data-color="primary"] { - color: var(--canon-fg-primary); -} - -.canon-Heading[data-color="secondary"] { - color: var(--canon-fg-secondary); -} - -.canon-Heading[data-truncate] { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -} - -.canon-IconButton { - user-select: none; - font-family: var(--canon-font-regular); - font-weight: var(--canon-font-weight-bold); - cursor: pointer; - border-radius: var(--canon-radius-2); - justify-content: center; - align-items: center; - gap: var(--canon-space-1_5); - border: none; - padding: 0; - display: inline-flex; - - &:disabled { - cursor: not-allowed; - } -} - -.canon-IconButton[data-variant="primary"] { - background-color: var(--canon-bg-solid); - color: var(--canon-fg-solid); - transition: background-color .15s, box-shadow .15s; - - &:hover { - background-color: var(--canon-bg-solid-hover); - } - - &:active { - background-color: var(--canon-bg-solid-pressed); - } - - &:focus-visible { - outline: 2px solid var(--canon-ring); - outline-offset: 2px; - } - - &:disabled { - background-color: var(--canon-bg-solid-disabled); - color: var(--canon-fg-solid-disabled); - } -} - -.canon-IconButton[data-variant="secondary"] { - background-color: var(--canon-bg-surface-1); - box-shadow: inset 0 0 0 1px var(--canon-border); - color: var(--canon-fg-primary); - transition: box-shadow .15s; - - &:hover { - box-shadow: inset 0 0 0 1px var(--canon-border-hover); - } - - &:active { - box-shadow: inset 0 0 0 1px var(--canon-border-pressed); - } - - &:focus-visible { - box-shadow: inset 0 0 0 2px var(--canon-ring); - outline: none; - transition: none; - } - - &:disabled { - box-shadow: inset 0 0 0 1px var(--canon-border-disabled); - color: var(--canon-fg-disabled); - } -} - -.canon-IconButton[data-size="medium"] { - font-size: var(--canon-font-size-4); - width: 40px; - height: 40px; -} - -.canon-IconButton[data-size="small"] { - font-size: var(--canon-font-size-3); - width: 32px; - height: 32px; -} - -.canon-IconButtonIcon[data-size="small"], .canon-IconButtonIcon[data-size="small"] svg { - width: 1rem; - height: 1rem; -} - -.canon-IconButtonIcon[data-size="medium"], .canon-IconButtonIcon[data-size="medium"] svg { - width: 1.25rem; - height: 1.25rem; -} - .canon-TextField { font-family: var(--canon-font-regular); flex-direction: column; @@ -9860,86 +10113,26 @@ display: flex; } -.canon-TextFieldLabelWrapper { - margin-bottom: var(--canon-space-3); - gap: var(--canon-space-1); - flex-direction: column; - display: flex; -} - -.canon-TextFieldLabelWrapper[data-hidden] { - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - position: absolute; - overflow: hidden; -} - -.canon-TextFieldLabel { - color: var(--canon-fg-primary); - cursor: pointer; - margin-right: auto; -} - -.canon-TextFieldLabel[data-size="small"] { - font-weight: var(--canon-font-weight-regular); - font-size: var(--canon-font-size-2); -} - -.canon-TextFieldLabel[data-size="medium"] { - font-weight: var(--canon-font-weight-bold); - font-size: var(--canon-font-size-3); -} - -.canon-TextFieldLabel[data-disabled] { - cursor: default; -} - -.canon-TextFieldSecondaryLabel { - color: var(--canon-fg-secondary); - font-weight: var(--canon-font-weight-regular); - margin-left: var(--canon-space-1); -} - -.canon-TextFieldDescription { - font-weight: var(--canon-font-weight-regular); - color: var(--canon-fg-secondary); - margin: 0; -} - -.canon-TextFieldDescription[data-size="small"] { - font-size: var(--canon-font-size-2); -} - -.canon-TextFieldDescription[data-size="medium"] { - font-size: var(--canon-font-size-3); -} - -.canon-TextFieldError { - font-size: var(--canon-font-size-2); - font-weight: var(--canon-font-weight-regular); - color: var(--canon-fg-danger); - padding-top: var(--canon-space-1_5); - margin: 0; -} - .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); - align-items: center; - display: flex; + position: relative; +} + +.canon-TextFieldInputWrapper[data-size="small"] { + height: 2rem; +} + +.canon-TextFieldInputWrapper[data-size="medium"] { + height: 2.5rem; } .canon-TextFieldIcon { + left: var(--canon-space-3); margin-right: var(--canon-space-1); color: var(--canon-fg-primary); flex-shrink: 0; + position: absolute; + top: 50%; + transform: translateY(-50%); } .canon-TextFieldIcon[data-size="small"], .canon-TextFieldIcon[data-size="small"] svg { @@ -9953,6 +10146,10 @@ } .canon-TextFieldInput { + 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); font-size: var(--canon-font-size-3); font-family: var(--canon-font-regular); font-weight: var(--canon-font-weight-regular); @@ -9960,300 +10157,48 @@ width: 100%; height: 100%; cursor: inherit; - background: none; - border: none; - padding: 0; + align-items: center; transition: border-color .2s ease-in-out, outline-color .2s ease-in-out; -} - -.canon-TextFieldInput:not([data-filled]):has( + .canon-TextFieldClearButton) { - padding-right: 1.25rem; -} - -.canon-TextFieldInput[type="search"]::-webkit-search-cancel-button, .canon-TextFieldInput[type="search"]::-webkit-search-decoration { - appearance: none; -} - -.canon-TextFieldClearButton { - margin-left: var(--canon-space-1); - vertical-align: middle; - color: var(--canon-fg-primary); - background: none; - border: none; - padding: 0; - display: none; -} - -.canon-TextFieldInput[data-filled] + .canon-TextFieldClearButton { - display: inline-block; -} - -.canon-TextFieldClearButtonIcon { - display: block; + display: flex; } .canon-TextFieldInput::placeholder { color: var(--canon-fg-secondary); } +.canon-TextFieldInput[data-icon] { + padding-left: var(--canon-space-8); +} + .canon-TextFieldInput[data-focused] { outline-color: var(--canon-border-pressed); outline-width: 0; } -.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:hover) { +.canon-TextFieldInput[data-hovered] { border-color: var(--canon-border-hover); } -.canon-TextField[data-focused] .canon-TextFieldInputWrapper { +.canon-TextFieldInput[data-focused] { border-color: var(--canon-border-pressed); + outline-width: 0; } -.canon-TextField[data-invalid] .canon-TextFieldInputWrapper { +.canon-TextFieldInput[data-invalid] { border-color: var(--canon-fg-danger); } -.canon-TextField[data-disabled] .canon-TextFieldInputWrapper { +.canon-TextFieldInput[data-disabled] { 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; -} - -.canon-TextFieldInputWrapper[data-size="medium"] { - height: 2.5rem; -} - -.canon-MenuPositioner { - outline: 0; -} - -.canon-MenuPopup { - background-color: var(--canon-bg-surface-1); - border: 1px solid var(--canon-border); - color: var(--canon-fg-primary); - transform-origin: var(--transform-origin); - max-width: min(var(--available-width), 340px); - max-height: min(var(--available-height), 500px); - padding-bottom: var(--canon-space-1); - border-radius: .375rem; - outline: none; - flex-direction: column; - transition: transform .15s, opacity .15s; - display: flex; - position: relative; - overflow: auto; - - &[data-starting-style], &[data-ending-style] { - opacity: 0; - transform: scale(.9); - } -} - -.canon-MenuItem { - user-select: none; - align-items: center; - gap: var(--canon-space-2); - height: 32px; - color: var(--canon-fg-primary); - border-radius: var(--canon-radius-2); - margin-inline: var(--canon-space-1); - padding-inline: var(--canon-space-2); - font-size: var(--canon-font-size-3); - cursor: pointer; - outline: 0; - flex-shrink: 0; - text-decoration: none; - display: flex; - - &:first-child { - margin-top: var(--canon-space-1); - } - - &[data-highlighted] { - background-color: var(--canon-gray-3); - } -} - -.canon-MenuSubmenuTrigger { - user-select: none; - justify-content: space-between; - align-items: center; - gap: var(--canon-space-2); - height: 32px; - color: var(--canon-fg-primary); - border-radius: var(--canon-radius-2); - margin-inline: var(--canon-space-1); - padding-inline: var(--canon-space-2); - font-size: var(--canon-font-size-3); - cursor: pointer; - outline: 0; - flex-shrink: 0; - text-decoration: none; - display: flex; - - & .canon-Icon { - color: var(--canon-fg-secondary); - } - - &:first-child { - margin-top: var(--canon-space-1); - } - - &[data-popup-open], &[data-highlighted] { - background-color: var(--canon-gray-3); - - & .canon-Icon { - color: var(--canon-fg-primary); - } - } -} - -.canon-MenuSeparator { - background-color: var(--color-gray-200); - height: 1px; - margin: .375rem 1rem; -} - -.canon-SubmenuComboboxSearch { - padding-inline: var(--canon-space-3); - border: none; - border-bottom: 1px solid var(--canon-border); - background-color: var(--canon-bg-surface-1); - width: 100%; - height: 32px; - color: var(--canon-fg-primary); - line-height: 140%; - font-size: var(--canon-font-size-3); - z-index: 1; - outline: none; - position: sticky; - top: 0; - - &::placeholder { - color: var(--canon-fg-secondary); - } - - &:disabled { - opacity: .6; - cursor: not-allowed; - } -} - -.canon-SubmenuComboboxItems { - padding-top: var(--canon-space-2); - outline: none; - flex-direction: column; - display: flex; - overflow-y: auto; -} - -.canon-SubmenuComboboxNoResults { - padding-inline: var(--canon-space-3); - padding-top: var(--canon-space-2); - padding-bottom: var(--canon-space-4); - color: var(--canon-fg-secondary); - font-size: var(--canon-font-size-3); -} - -.canon-SubmenuComboboxItem { - user-select: none; - justify-content: space-between; - align-items: center; - gap: var(--canon-space-2); - height: 32px; - color: var(--canon-fg-primary); - border-radius: var(--canon-radius-2); - margin-inline: var(--canon-space-1); - padding-inline: var(--canon-space-2); - font-size: var(--canon-font-size-3); - cursor: pointer; - outline: 0; - flex-shrink: 0; - text-decoration: none; - display: flex; - - &[data-highlighted] { - background-color: var(--canon-gray-3); - } - - &[data-disabled] { - opacity: .5; - cursor: not-allowed; - } -} - -.canon-SubmenuComboboxItemCheckbox { - width: 16px; - height: 16px; - color: var(--canon-fg-primary); - border-radius: var(--canon-radius-2); - border: 1px solid var(--canon-border); - background: var(--canon-bg-surface-1); - flex-shrink: 0; - justify-content: center; - align-items: center; - display: flex; -} - -.canon-SubmenuComboboxItemLabel { - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; - overflow: hidden; -} - -.canon-Link { - font-family: var(--canon-font-regular); - color: var(--canon-fg-link); - cursor: pointer; - margin: 0; - padding: 0; - text-decoration-line: none; - - &:hover { - color: var(--canon-fg-link-hover); - text-underline-offset: calc(.025em + 2px); - text-decoration-line: underline; - text-decoration-style: solid; - text-decoration-thickness: min(2px, max(1px, .05em)); - text-decoration-color: color-mix(in srgb, var(--canon-fg-link-hover) 30%, transparent); - } -} - -.canon-Link[data-variant="body"] { - font-size: var(--canon-font-size-3); - line-height: 140%; -} - -.canon-Link[data-variant="subtitle"] { - font-size: var(--canon-font-size-4); - line-height: 140%; -} - -.canon-Link[data-variant="caption"] { +.canon-TextFieldError { + color: var(--canon-fg-danger); font-size: var(--canon-font-size-2); - line-height: 140%; -} - -.canon-Link[data-variant="label"] { - font-size: var(--canon-font-size-1); - line-height: 140%; -} - -.canon-Link[data-weight="regular"] { font-weight: var(--canon-font-weight-regular); -} - -.canon-Link[data-weight="bold"] { - font-weight: var(--canon-font-weight-bold); + margin-top: var(--canon-space-2); } .canon-TooltipPopup { diff --git a/packages/canon/css/textfield.css b/packages/canon/css/textfield.css index bdc5a23dbd..fdb3621016 100644 --- a/packages/canon/css/textfield.css +++ b/packages/canon/css/textfield.css @@ -5,86 +5,26 @@ display: flex; } -.canon-TextFieldLabelWrapper { - margin-bottom: var(--canon-space-3); - gap: var(--canon-space-1); - flex-direction: column; - display: flex; -} - -.canon-TextFieldLabelWrapper[data-hidden] { - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - position: absolute; - overflow: hidden; -} - -.canon-TextFieldLabel { - color: var(--canon-fg-primary); - cursor: pointer; - margin-right: auto; -} - -.canon-TextFieldLabel[data-size="small"] { - font-weight: var(--canon-font-weight-regular); - font-size: var(--canon-font-size-2); -} - -.canon-TextFieldLabel[data-size="medium"] { - font-weight: var(--canon-font-weight-bold); - font-size: var(--canon-font-size-3); -} - -.canon-TextFieldLabel[data-disabled] { - cursor: default; -} - -.canon-TextFieldSecondaryLabel { - color: var(--canon-fg-secondary); - font-weight: var(--canon-font-weight-regular); - margin-left: var(--canon-space-1); -} - -.canon-TextFieldDescription { - font-weight: var(--canon-font-weight-regular); - color: var(--canon-fg-secondary); - margin: 0; -} - -.canon-TextFieldDescription[data-size="small"] { - font-size: var(--canon-font-size-2); -} - -.canon-TextFieldDescription[data-size="medium"] { - font-size: var(--canon-font-size-3); -} - -.canon-TextFieldError { - font-size: var(--canon-font-size-2); - font-weight: var(--canon-font-weight-regular); - color: var(--canon-fg-danger); - padding-top: var(--canon-space-1_5); - margin: 0; -} - .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); - align-items: center; - display: flex; + position: relative; +} + +.canon-TextFieldInputWrapper[data-size="small"] { + height: 2rem; +} + +.canon-TextFieldInputWrapper[data-size="medium"] { + height: 2.5rem; } .canon-TextFieldIcon { + left: var(--canon-space-3); margin-right: var(--canon-space-1); color: var(--canon-fg-primary); flex-shrink: 0; + position: absolute; + top: 50%; + transform: translateY(-50%); } .canon-TextFieldIcon[data-size="small"], .canon-TextFieldIcon[data-size="small"] svg { @@ -98,6 +38,10 @@ } .canon-TextFieldInput { + 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); font-size: var(--canon-font-size-3); font-family: var(--canon-font-regular); font-weight: var(--canon-font-weight-regular); @@ -105,73 +49,46 @@ width: 100%; height: 100%; cursor: inherit; - background: none; - border: none; - padding: 0; + align-items: center; transition: border-color .2s ease-in-out, outline-color .2s ease-in-out; -} - -.canon-TextFieldInput:not([data-filled]):has( + .canon-TextFieldClearButton) { - padding-right: 1.25rem; -} - -.canon-TextFieldInput[type="search"]::-webkit-search-cancel-button, .canon-TextFieldInput[type="search"]::-webkit-search-decoration { - appearance: none; -} - -.canon-TextFieldClearButton { - margin-left: var(--canon-space-1); - vertical-align: middle; - color: var(--canon-fg-primary); - background: none; - border: none; - padding: 0; - display: none; -} - -.canon-TextFieldInput[data-filled] + .canon-TextFieldClearButton { - display: inline-block; -} - -.canon-TextFieldClearButtonIcon { - display: block; + display: flex; } .canon-TextFieldInput::placeholder { color: var(--canon-fg-secondary); } +.canon-TextFieldInput[data-icon] { + padding-left: var(--canon-space-8); +} + .canon-TextFieldInput[data-focused] { outline-color: var(--canon-border-pressed); outline-width: 0; } -.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:hover) { +.canon-TextFieldInput[data-hovered] { border-color: var(--canon-border-hover); } -.canon-TextField[data-focused] .canon-TextFieldInputWrapper { +.canon-TextFieldInput[data-focused] { border-color: var(--canon-border-pressed); + outline-width: 0; } -.canon-TextField[data-invalid] .canon-TextFieldInputWrapper { +.canon-TextFieldInput[data-invalid] { border-color: var(--canon-fg-danger); } -.canon-TextField[data-disabled] .canon-TextFieldInputWrapper { +.canon-TextFieldInput[data-disabled] { 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; -} - -.canon-TextFieldInputWrapper[data-size="medium"] { - height: 2.5rem; +.canon-TextFieldError { + color: var(--canon-fg-danger); + font-size: var(--canon-font-size-2); + font-weight: var(--canon-font-weight-regular); + margin-top: var(--canon-space-2); } diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index c4278a6e94..4db3a81bbc 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -18,7 +18,6 @@ import { ForwardRefExoticComponent } from 'react'; import { HTMLAttributes } from 'react'; import { JSX as JSX_2 } from 'react/jsx-runtime'; import { Menu as Menu_2 } from '@base-ui-components/react/menu'; -import type { MouseEventHandler } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RefAttributes } from 'react'; @@ -28,6 +27,7 @@ import type { SwitchProps as SwitchProps_2 } from 'react-aria-components'; import { Table as Table_2 } from '@tanstack/react-table'; import { Tabs as Tabs_2 } from '@base-ui-components/react/tabs'; import { TdHTMLAttributes } from 'react'; +import type { TextFieldProps as TextFieldProps_2 } from 'react-aria-components'; import { ThHTMLAttributes } from 'react'; import { Tooltip as Tooltip_2 } from '@base-ui-components/react/tooltip'; import type { useRender } from '@base-ui-components/react/use-render'; @@ -356,6 +356,20 @@ export type EnumPropDef = { required?: boolean; }; +// @public (undocumented) +export const FieldLabel: ForwardRefExoticComponent< + FieldLabelProps & RefAttributes +>; + +// @public (undocumented) +export interface FieldLabelProps { + description?: string | null; + htmlFor?: string; + id?: string; + label?: string | null; + secondaryLabel?: string | null; +} + // @public (undocumented) export const Flex: ForwardRefExoticComponent< FlexProps & RefAttributes @@ -1286,17 +1300,9 @@ export const TextField: ForwardRefExoticComponent< // @public (undocumented) export interface TextFieldProps - extends Omit, 'size'> { - className?: string; - description?: string; - error?: string | null; - hideLabelAndDescription?: boolean; + extends TextFieldProps_2, + Omit { icon?: ReactNode; - label?: string; - labelSize?: 'small' | 'medium'; - name: string; - onClear?: MouseEventHandler; - secondaryLabel?: string; size?: 'small' | 'medium' | Partial>; } diff --git a/packages/canon/src/components/FieldLabel/FieldLabel.stories.tsx b/packages/canon/src/components/FieldLabel/FieldLabel.stories.tsx new file mode 100644 index 0000000000..a82639eca0 --- /dev/null +++ b/packages/canon/src/components/FieldLabel/FieldLabel.stories.tsx @@ -0,0 +1,65 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Meta, StoryObj } from '@storybook/react'; +import { FieldLabel } from './FieldLabel'; + +const meta = { + title: 'Forms/FieldLabel', + component: FieldLabel, + argTypes: { + label: { + control: 'text', + }, + secondaryLabel: { + control: 'text', + }, + description: { + control: 'text', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Label', + }, +}; + +export const WithSecondaryLabel: Story = { + args: { + ...Default.args, + secondaryLabel: 'Secondary Label', + }, +}; + +export const WithDescription: Story = { + args: { + ...Default.args, + description: 'Description', + }, +}; + +export const WithAllFields: Story = { + args: { + ...Default.args, + secondaryLabel: 'Secondary Label', + description: 'Description', + }, +}; diff --git a/packages/canon/src/components/FieldLabel/FieldLabel.styles.css b/packages/canon/src/components/FieldLabel/FieldLabel.styles.css new file mode 100644 index 0000000000..174e7f7326 --- /dev/null +++ b/packages/canon/src/components/FieldLabel/FieldLabel.styles.css @@ -0,0 +1,27 @@ +.canon-FieldLabel { + display: flex; + flex-direction: column; + margin-bottom: var(--canon-space-3); + gap: var(--canon-space-1); +} + +.canon-FieldLabelLabel { + color: var(--canon-fg-primary); + margin-right: auto; + cursor: pointer; + font-weight: var(--canon-font-weight-regular); + font-size: var(--canon-font-size-2); +} + +.canon-FieldLabelSecondaryLabel { + color: var(--canon-fg-secondary); + font-weight: var(--canon-font-weight-regular); + margin-left: var(--canon-space-1); +} + +.canon-FieldLabelDescription { + font-weight: var(--canon-font-weight-regular); + font-size: var(--canon-font-size-2); + color: var(--canon-fg-secondary); + margin: 0; +} diff --git a/packages/canon/src/components/FieldLabel/FieldLabel.tsx b/packages/canon/src/components/FieldLabel/FieldLabel.tsx new file mode 100644 index 0000000000..8fee7dffed --- /dev/null +++ b/packages/canon/src/components/FieldLabel/FieldLabel.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Label } from 'react-aria-components'; +import { forwardRef } from 'react'; +import type { FieldLabelProps } from './types'; + +/** @public */ +export const FieldLabel = forwardRef( + (props: FieldLabelProps, ref) => { + const { label, secondaryLabel, description, htmlFor, id, ...rest } = props; + + if (!label) return null; + + return ( +
+ {label && ( + + )} + {description && ( +
{description}
+ )} +
+ ); + }, +); + +FieldLabel.displayName = 'FieldLabel'; diff --git a/packages/canon/src/components/FieldLabel/index.ts b/packages/canon/src/components/FieldLabel/index.ts new file mode 100644 index 0000000000..b9cebb1bf7 --- /dev/null +++ b/packages/canon/src/components/FieldLabel/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './FieldLabel'; +export * from './types'; diff --git a/packages/canon/src/components/FieldLabel/types.ts b/packages/canon/src/components/FieldLabel/types.ts new file mode 100644 index 0000000000..dc4e444133 --- /dev/null +++ b/packages/canon/src/components/FieldLabel/types.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @public */ +export interface FieldLabelProps { + /** + * The label of the text field + */ + label?: string | null; + + /** + * The secondary label of the text field + */ + secondaryLabel?: string | null; + + /** + * The description of the text field + */ + description?: string | null; + + /** + * The HTML for attribute of the text field + */ + htmlFor?: string; + + /** + * The id of the text field + */ + id?: string; +} diff --git a/packages/canon/src/components/TextField/TextField.stories.tsx b/packages/canon/src/components/TextField/TextField.stories.tsx index f052377acb..e07aa6ea05 100644 --- a/packages/canon/src/components/TextField/TextField.stories.tsx +++ b/packages/canon/src/components/TextField/TextField.stories.tsx @@ -16,19 +16,21 @@ import type { Meta, StoryObj } from '@storybook/react'; import { TextField } from './TextField'; -import { Flex } from '../Flex'; +import { Form } from 'react-aria-components'; import { Icon } from '../Icon'; +import { Flex } from '../Flex'; +import { FieldLabel } from '../FieldLabel'; const meta = { - title: 'Components/TextField', + title: 'Forms/TextField', component: TextField, argTypes: { - secondaryLabel: { - control: 'text', - }, - required: { + isRequired: { control: 'boolean', }, + icon: { + control: 'object', + }, }, } satisfies Meta; @@ -38,14 +40,26 @@ type Story = StoryObj; export const Default: Story = { args: { name: 'url', - placeholder: 'Enter a URL', + defaultValue: 'Enter a URL', style: { maxWidth: '300px', }, }, }; -export const Filled: Story = { +export const Sizes: Story = { + args: { + ...Default.args, + }, + render: args => ( + + } /> + } /> + + ), +}; + +export const DefaultValue: Story = { args: { ...Default.args, defaultValue: 'https://example.com', @@ -69,82 +83,21 @@ export const WithDescription: Story = { export const Required: Story = { args: { ...WithLabel.args, - required: true, - }, -}; - -export const LabelSizes: Story = { - args: { - ...Default.args, - label: 'Label', - description: 'Description', - required: true, - }, - render: args => ( - - - - - ), -}; - -export const HideLabelAndDescription: Story = { - args: { - ...WithLabel.args, - hideLabelAndDescription: true, + isRequired: true, }, }; export const Disabled: Story = { - args: { - ...WithLabel.args, - disabled: true, - }, -}; - -export const Sizes: Story = { args: { ...Default.args, - label: 'Label', - description: 'Description', - }, - render: args => ( - - } /> - } /> - - ), -}; - -export const Responsive: Story = { - args: { - ...WithLabel.args, - size: { - initial: 'small', - sm: 'medium', - }, - }, -}; - -export const WithError: Story = { - args: { - ...WithLabel.args, - error: 'Invalid URL', - }, -}; - -export const WithErrorAndDescription: Story = { - args: { - ...WithLabel.args, - error: 'Invalid URL', - description: 'Description', + isDisabled: true, }, }; export const WithIcon: Story = { args: { - ...WithLabel.args, - placeholder: 'Search...', + ...Default.args, + defaultValue: 'Search...', icon: , }, }; @@ -152,23 +105,42 @@ export const WithIcon: Story = { export const DisabledWithIcon: Story = { args: { ...WithIcon.args, - disabled: true, + isDisabled: true, }, }; -export const WithOnClear: Story = { +export const ShowError: Story = { args: { ...WithLabel.args, - placeholder: 'Search...', - type: 'search', - onClear: () => console.log('Cleared!'), + }, + render: args => ( +
+ + + ), +}; + +export const Validation: Story = { + args: { + ...WithLabel.args, + validate: value => (value === 'admin' ? 'Nice try!' : null), }, }; -export const DisabledWithOnClear: Story = { - args: { - ...WithOnClear.args, - defaultValue: 'Testing', - disabled: true, - }, +export const CustomField: Story = { + render: () => ( + <> + + + + ), }; diff --git a/packages/canon/src/components/TextField/TextField.styles.css b/packages/canon/src/components/TextField/TextField.styles.css index d7bc147ba1..9f2039b3af 100644 --- a/packages/canon/src/components/TextField/TextField.styles.css +++ b/packages/canon/src/components/TextField/TextField.styles.css @@ -21,83 +21,23 @@ width: 100%; } -.canon-TextFieldLabelWrapper { - display: flex; - flex-direction: column; - margin-bottom: var(--canon-space-3); - gap: var(--canon-space-1); -} - -.canon-TextFieldLabelWrapper[data-hidden] { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - -.canon-TextFieldLabel { - color: var(--canon-fg-primary); - margin-right: auto; - cursor: pointer; -} - -.canon-TextFieldLabel[data-size='small'] { - font-weight: var(--canon-font-weight-regular); - font-size: var(--canon-font-size-2); -} - -.canon-TextFieldLabel[data-size='medium'] { - font-weight: var(--canon-font-weight-bold); - font-size: var(--canon-font-size-3); -} - -.canon-TextFieldLabel[data-disabled] { - cursor: default; -} - -.canon-TextFieldSecondaryLabel { - color: var(--canon-fg-secondary); - font-weight: var(--canon-font-weight-regular); - margin-left: var(--canon-space-1); -} - -.canon-TextFieldDescription { - font-weight: var(--canon-font-weight-regular); - color: var(--canon-fg-secondary); - margin: 0; -} - -.canon-TextFieldDescription[data-size='small'] { - font-size: var(--canon-font-size-2); -} - -.canon-TextFieldDescription[data-size='medium'] { - font-size: var(--canon-font-size-3); -} - -.canon-TextFieldError { - font-size: var(--canon-font-size-2); - font-weight: var(--canon-font-weight-regular); - color: var(--canon-fg-danger); - margin: 0; - padding-top: var(--canon-space-1_5); -} - .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); + position: relative; +} + +.canon-TextFieldInputWrapper[data-size='small'] { + height: 2rem; +} + +.canon-TextFieldInputWrapper[data-size='medium'] { + height: 2.5rem; } .canon-TextFieldIcon { + position: absolute; + left: var(--canon-space-3); + top: 50%; + transform: translateY(-50%); margin-right: var(--canon-space-1); color: var(--canon-fg-primary); flex-shrink: 0; @@ -116,8 +56,13 @@ } .canon-TextFieldInput { - border: none; - background: none; + 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); + font-size: var(--canon-font-size-3); font-family: var(--canon-font-regular); font-weight: var(--canon-font-weight-regular); @@ -126,71 +71,43 @@ width: 100%; height: 100%; cursor: inherit; - padding: 0; -} - -.canon-TextFieldInput:not([data-filled]):has(+ .canon-TextFieldClearButton) { - padding-right: 1.25rem; -} - -.canon-TextFieldInput[type='search']::-webkit-search-cancel-button, -.canon-TextFieldInput[type='search']::-webkit-search-decoration { - appearance: none; -} - -.canon-TextFieldClearButton { - display: none; - padding: 0; - margin-left: var(--canon-space-1); - background: none; - border: none; - vertical-align: middle; - color: var(--canon-fg-primary); -} - -.canon-TextFieldInput[data-filled] + .canon-TextFieldClearButton { - display: inline-block; -} - -.canon-TextFieldClearButtonIcon { - display: block; } .canon-TextFieldInput::placeholder { color: var(--canon-fg-secondary); } +.canon-TextFieldInput[data-icon] { + padding-left: var(--canon-space-8); +} + .canon-TextFieldInput[data-focused] { outline-color: var(--canon-border-pressed); outline-width: 0px; } -.canon-TextFieldInputWrapper:has(> .canon-TextFieldInput:hover) { +.canon-TextFieldInput[data-hovered] { border-color: var(--canon-border-hover); } -.canon-TextField[data-focused] .canon-TextFieldInputWrapper { +.canon-TextFieldInput[data-focused] { border-color: var(--canon-border-pressed); + outline-width: 0px; } -.canon-TextField[data-invalid] .canon-TextFieldInputWrapper { +.canon-TextFieldInput[data-invalid] { border-color: var(--canon-fg-danger); } -.canon-TextField[data-disabled] .canon-TextFieldInputWrapper { +.canon-TextFieldInput[data-disabled] { 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; -} - -.canon-TextFieldInputWrapper[data-size='medium'] { - height: 2.5rem; +.canon-TextFieldError { + color: var(--canon-fg-danger); + font-size: var(--canon-font-size-2); + font-weight: var(--canon-font-weight-regular); + margin-top: var(--canon-space-2); } diff --git a/packages/canon/src/components/TextField/TextField.tsx b/packages/canon/src/components/TextField/TextField.tsx index 07efc47100..444e76e983 100644 --- a/packages/canon/src/components/TextField/TextField.tsx +++ b/packages/canon/src/components/TextField/TextField.tsx @@ -14,110 +14,80 @@ * limitations under the License. */ -import { Field } from '@base-ui-components/react/field'; -import { forwardRef } from 'react'; +import { forwardRef, useEffect } from 'react'; +import { + Input, + TextField as AriaTextField, + FieldError, +} from 'react-aria-components'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; import clsx from 'clsx'; +import { FieldLabel } from '../FieldLabel'; import type { TextFieldProps } from './types'; -import { Icon } from '../Icon'; /** @public */ export const TextField = forwardRef( - (props: TextFieldProps, ref) => { + (props, ref) => { const { className, + icon, size = 'small', label, - labelSize = 'small', secondaryLabel, description, - hideLabelAndDescription, - error, - required, - style, - disabled, - icon, - onClear, + isRequired, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, ...rest } = props; + useEffect(() => { + if (!label && !ariaLabel && !ariaLabelledBy) { + console.warn( + 'TextField requires either a visible label, aria-label, or aria-labelledby for accessibility', + ); + } + }, [label, ariaLabel, ariaLabelledBy]); + // Get the responsive value for the variant const responsiveSize = useResponsiveValue(size); // If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required. - const secondaryLabelText = secondaryLabel || (required ? 'Required' : null); + const secondaryLabelText = + secondaryLabel || (isRequired ? 'Required' : null); return ( - - {label && ( -
- {label && ( - - {label} - {secondaryLabelText && ( - - )} - - )} - {description && ( - - {description} - - )} -
- )} +
{icon && ( )} - - {onClear && ( - - )}
- {error && ( - - {error} - - )} -
+ + ); }, ); diff --git a/packages/canon/src/components/TextField/index.ts b/packages/canon/src/components/TextField/index.ts index 29df00ccaf..95a838fb46 100644 --- a/packages/canon/src/components/TextField/index.ts +++ b/packages/canon/src/components/TextField/index.ts @@ -15,4 +15,4 @@ */ export * from './TextField'; -export type { TextFieldProps } from './types'; +export * from './types'; diff --git a/packages/canon/src/components/TextField/types.ts b/packages/canon/src/components/TextField/types.ts index 8a81492f2f..92fa077389 100644 --- a/packages/canon/src/components/TextField/types.ts +++ b/packages/canon/src/components/TextField/types.ts @@ -14,64 +14,23 @@ * limitations under the License. */ +import type { TextFieldProps as AriaTextFieldProps } from 'react-aria-components'; +import { ReactNode } from 'react'; import type { Breakpoint } from '../../types'; -import type { ReactNode, MouseEventHandler } from 'react'; +import type { FieldLabelProps } from '../FieldLabel/types'; + /** @public */ export interface TextFieldProps - extends Omit, 'size'> { - /** - * The class name of the text field - */ - className?: string; - - /** - * The size of the text field - * @defaultValue 'medium' - */ - size?: 'small' | 'medium' | Partial>; - - /** - * The label of the text field - */ - label?: string; - - /** - * The secondary label of the text field - */ - secondaryLabel?: string; - - /** - * The size of the label and description - */ - labelSize?: 'small' | 'medium'; - - /** - * Hide the label and description but still visible to screen readers - */ - hideLabelAndDescription?: boolean; - - /** - * The description of the text field - */ - description?: string; - - /** - * The name of the text field - */ - name: string; - - /** - * The error message of the text field - */ - error?: string | null; - + extends AriaTextFieldProps, + Omit { /** * An icon to render before the input */ icon?: ReactNode; /** - * Handler to call when the clear button is pressed + * The size of the text field + * @defaultValue 'medium' */ - onClear?: MouseEventHandler; + size?: 'small' | 'medium' | Partial>; } diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css index 609ebea769..c85c1bbdd9 100644 --- a/packages/canon/src/css/components.css +++ b/packages/canon/src/css/components.css @@ -17,14 +17,19 @@ @import '../components/Avatar/Avatar.styles.css'; @import '../components/Box/styles.css'; @import '../components/Button/styles.css'; +@import '../components/Checkbox/styles.css'; @import '../components/Collapsible/Collapsible.styles.css'; +@import '../components/Container/styles.css'; @import '../components/DataTable/Root/DataTableRoot.styles.css'; @import '../components/DataTable/Pagination/DataTablePagination.styles.css'; +@import '../components/FieldLabel/FieldLabel.styles.css'; @import '../components/Flex/styles.css'; @import '../components/Grid/styles.css'; -@import '../components/Container/styles.css'; +@import '../components/Heading/styles.css'; @import '../components/Icon/styles.css'; -@import '../components/Checkbox/styles.css'; +@import '../components/IconButton/styles.css'; +@import '../components/Link/styles.css'; +@import '../components/Menu/Menu.styles.css'; @import '../components/Table/styles.css'; @import '../components/Table/TableCell/TableCell.styles.css'; @import '../components/Table/TableCellText/TableCellText.styles.css'; @@ -32,11 +37,7 @@ @import '../components/Table/TableCellProfile/TableCellProfile.styles.css'; @import '../components/Tabs/Tabs.styles.css'; @import '../components/Text/styles.css'; -@import '../components/Heading/styles.css'; -@import '../components/IconButton/styles.css'; @import '../components/TextField/TextField.styles.css'; -@import '../components/Menu/Menu.styles.css'; -@import '../components/Link/styles.css'; @import '../components/Tooltip/Tooltip.styles.css'; @import '../components/ScrollArea/ScrollArea.styles.css'; @import '../components/Select/Select.styles.css'; diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts index 310a7896c8..9b5a454254 100644 --- a/packages/canon/src/index.ts +++ b/packages/canon/src/index.ts @@ -36,6 +36,7 @@ export * from './components/Avatar'; export * from './components/Button'; export * from './components/Collapsible'; export * from './components/DataTable'; +export * from './components/FieldLabel'; export * from './components/Icon'; export * from './components/IconButton'; export * from './components/Checkbox'; diff --git a/packages/canon/src/stories/Form.stories.tsx b/packages/canon/src/stories/Form.stories.tsx deleted file mode 100644 index 3bffca6554..0000000000 --- a/packages/canon/src/stories/Form.stories.tsx +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { useState } from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; -import { useForm, SubmitHandler, Controller } from 'react-hook-form'; -import { TextField } from '../components/TextField'; -import { Button } from '../components/Button'; -import { Select } from '../components/Select'; - -const meta = { - title: 'Examples/Form', -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -type Inputs = { - firstname: string; - lastname: string; - city: string; -}; - -export const Uncontrolled: Story = { - render: () => { - const { - register, - handleSubmit, - control, - formState: { errors }, - } = useForm(); - - const onSubmit: SubmitHandler = data => console.log(data); - - return ( -
- - - { - return ( - - ); - }} - /> - - - ); - }, -};