refactor(ui): adopt flexbox positioning pattern for input components
Refactored PasswordField, SearchField, and MenuAutocomplete to use a consistent flexbox positioning approach, improving maintainability and visual consistency. Key changes: - Replaced absolute positioning with flexbox layouts - Moved border and background styling from input to wrapper - Used grid-centered containers for icons and buttons - Added component-specific CSS variables for item height Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Improved visual consistency of PasswordField, SearchField, and MenuAutocomplete components.
|
||||
@@ -628,7 +628,10 @@ export const componentDefinitions: {
|
||||
readonly PasswordField: {
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-PasswordField';
|
||||
readonly inputVisibility: 'bui-InputVisibility';
|
||||
readonly inputWrapper: 'bui-PasswordFieldInputWrapper';
|
||||
readonly input: 'bui-PasswordFieldInput';
|
||||
readonly inputIcon: 'bui-PasswordFieldIcon';
|
||||
readonly inputVisibility: 'bui-PasswordFieldVisibility';
|
||||
};
|
||||
readonly dataAttributes: {
|
||||
readonly size: readonly ['small', 'medium'];
|
||||
@@ -650,7 +653,7 @@ export const componentDefinitions: {
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-SearchField';
|
||||
readonly clear: 'bui-SearchFieldClear';
|
||||
readonly inputWrapper: 'bui-SearchFieldWrapper';
|
||||
readonly inputWrapper: 'bui-SearchFieldInputWrapper';
|
||||
readonly input: 'bui-SearchFieldInput';
|
||||
readonly inputIcon: 'bui-SearchFieldInputIcon';
|
||||
};
|
||||
|
||||
@@ -188,48 +188,56 @@
|
||||
}
|
||||
|
||||
.bui-MenuSearchField {
|
||||
position: relative;
|
||||
font-family: var(--bui-font-regular);
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--bui-border);
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
height: 2rem;
|
||||
|
||||
&[data-empty] {
|
||||
.bui-MenuSearchFieldClear {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bui-MenuSearchFieldInput {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 var(--bui-space-3);
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--bui-border);
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
background-color: transparent;
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-family: var(--bui-font-regular);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
color: var(--bui-fg-primary);
|
||||
width: 100%;
|
||||
height: 2rem;
|
||||
cursor: inherit;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
cursor: inherit;
|
||||
|
||||
&::-webkit-search-cancel-button,
|
||||
&::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bui-fg-secondary);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-inline: var(--bui-space-3) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-MenuSearchFieldClear {
|
||||
position: absolute;
|
||||
right: var(--bui-space-2);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
@@ -237,6 +245,12 @@
|
||||
cursor: pointer;
|
||||
color: var(--bui-fg-secondary);
|
||||
transition: color 0.2s ease-in-out;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--bui-fg-primary);
|
||||
}
|
||||
|
||||
& > svg {
|
||||
width: 1rem;
|
||||
|
||||
@@ -23,52 +23,136 @@
|
||||
font-family: var(--bui-font-regular);
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
/* Reserve space for browser/password manager icon (e.g. 1Password) */
|
||||
--bui-passwordmanager-icon-width: var(--bui-space-1);
|
||||
|
||||
&[data-size='small'] {
|
||||
--password-field-item-height: 2rem;
|
||||
}
|
||||
|
||||
&[data-size='medium'] {
|
||||
--password-field-item-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-InputVisibility {
|
||||
position: absolute;
|
||||
right: var(--bui-passwordmanager-icon-width);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
.bui-PasswordFieldInputWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: var(--bui-radius-2);
|
||||
border: 1px solid var(--bui-border);
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out;
|
||||
|
||||
&[data-size='small'] {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
&[data-size='medium'] {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--bui-border-pressed);
|
||||
outline-width: 0px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bui-border-hover);
|
||||
}
|
||||
|
||||
&:has([data-invalid]) {
|
||||
border-color: var(--bui-fg-danger);
|
||||
}
|
||||
|
||||
&:has([data-disabled]) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
border: 1px solid var(--bui-border-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-PasswordFieldIcon {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
color: var(--bui-fg-primary);
|
||||
pointer-events: none;
|
||||
width: var(--password-field-item-height);
|
||||
height: var(--password-field-item-height);
|
||||
|
||||
& svg {
|
||||
.bui-PasswordField[data-size='small'] & {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.bui-PasswordField[data-size='medium'] & {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bui-PasswordFieldInput {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-family: var(--bui-font-regular);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
color: var(--bui-fg-primary);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
|
||||
&::-webkit-search-cancel-button,
|
||||
&::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bui-fg-secondary);
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.bui-PasswordField[data-size='small'] & {
|
||||
padding-inline: var(--bui-space-3) 0;
|
||||
}
|
||||
|
||||
.bui-PasswordField[data-size='medium'] & {
|
||||
padding-inline: var(--bui-space-4) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bui-PasswordFieldVisibility {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bui-fg-primary);
|
||||
width: var(--password-field-item-height);
|
||||
height: var(--password-field-item-height);
|
||||
|
||||
/* Size: small */
|
||||
&[data-size='small'] {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
&[data-size='small'] svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
& svg {
|
||||
.bui-PasswordField[data-size='small'] & {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
/* Size: medium */
|
||||
&[data-size='medium'] {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
.bui-PasswordField[data-size='medium'] & {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
&[data-size='medium'] svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure input has enough right padding for our toggle + PM icon */
|
||||
.bui-PasswordField .bui-InputWrapper[data-size='small'] .bui-Input {
|
||||
padding-right: calc(2rem + var(--bui-passwordmanager-icon-width));
|
||||
}
|
||||
|
||||
.bui-PasswordField .bui-InputWrapper[data-size='medium'] .bui-Input {
|
||||
padding-right: calc(2.5rem + var(--bui-passwordmanager-icon-width));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import type { PasswordFieldProps } from './types';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import { RiEyeLine, RiEyeOffLine } from '@remixicon/react';
|
||||
import stylesPasswordField from './PasswordField.module.css';
|
||||
import stylesTextField from '../TextField/TextField.module.css';
|
||||
|
||||
/** @public */
|
||||
export const PasswordField = forwardRef<HTMLDivElement, PasswordFieldProps>(
|
||||
@@ -47,8 +46,6 @@ export const PasswordField = forwardRef<HTMLDivElement, PasswordFieldProps>(
|
||||
}
|
||||
}, [label, ariaLabel, ariaLabelledBy]);
|
||||
|
||||
const { classNames: textFieldClassNames } = useStyles('TextField');
|
||||
|
||||
const {
|
||||
classNames: classNamesPasswordField,
|
||||
dataAttributes,
|
||||
@@ -96,16 +93,16 @@ export const PasswordField = forwardRef<HTMLDivElement, PasswordFieldProps>(
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
textFieldClassNames.inputWrapper,
|
||||
stylesTextField[textFieldClassNames.inputWrapper],
|
||||
classNamesPasswordField.inputWrapper,
|
||||
stylesPasswordField[classNamesPasswordField.inputWrapper],
|
||||
)}
|
||||
data-size={dataAttributes['data-size']}
|
||||
>
|
||||
{icon && (
|
||||
<div
|
||||
className={clsx(
|
||||
textFieldClassNames.inputIcon,
|
||||
stylesTextField[textFieldClassNames.inputIcon],
|
||||
classNamesPasswordField.inputIcon,
|
||||
stylesPasswordField[classNamesPasswordField.inputIcon],
|
||||
)}
|
||||
data-size={dataAttributes['data-size']}
|
||||
aria-hidden="true"
|
||||
@@ -113,36 +110,29 @@ export const PasswordField = forwardRef<HTMLDivElement, PasswordFieldProps>(
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
textFieldClassNames.inputAction,
|
||||
stylesTextField[textFieldClassNames.inputAction],
|
||||
)}
|
||||
>
|
||||
<RAButton
|
||||
data-size={dataAttributes['data-size']}
|
||||
data-variant={'tertiary'}
|
||||
aria-label={isVisible ? 'Hide value' : 'Show value'}
|
||||
aria-controls={isVisible ? 'text' : 'password'}
|
||||
aria-expanded={isVisible}
|
||||
onPress={() => setIsVisible(v => !v)}
|
||||
className={clsx(
|
||||
classNamesPasswordField.inputVisibility,
|
||||
stylesPasswordField[classNamesPasswordField.inputVisibility],
|
||||
)}
|
||||
>
|
||||
{isVisible ? <RiEyeLine /> : <RiEyeOffLine />}
|
||||
</RAButton>
|
||||
</div>
|
||||
<Input
|
||||
className={clsx(
|
||||
textFieldClassNames.input,
|
||||
stylesTextField[textFieldClassNames.input],
|
||||
classNamesPasswordField.input,
|
||||
stylesPasswordField[classNamesPasswordField.input],
|
||||
)}
|
||||
{...(icon && { 'data-icon': true })}
|
||||
placeholder={placeholder}
|
||||
type={isVisible ? 'text' : 'password'}
|
||||
/>
|
||||
<RAButton
|
||||
data-size={dataAttributes['data-size']}
|
||||
data-variant={'tertiary'}
|
||||
aria-label={isVisible ? 'Hide value' : 'Show value'}
|
||||
aria-controls={isVisible ? 'text' : 'password'}
|
||||
aria-expanded={isVisible}
|
||||
onPress={() => setIsVisible(v => !v)}
|
||||
className={clsx(
|
||||
classNamesPasswordField.inputVisibility,
|
||||
stylesPasswordField[classNamesPasswordField.inputVisibility],
|
||||
)}
|
||||
>
|
||||
{isVisible ? <RiEyeLine /> : <RiEyeOffLine />}
|
||||
</RAButton>
|
||||
</div>
|
||||
<FieldError />
|
||||
</AriaTextField>
|
||||
|
||||
@@ -25,6 +25,14 @@
|
||||
flex: 1;
|
||||
flex-shrink: 0;
|
||||
|
||||
&[data-size='small'] {
|
||||
--search-field-item-height: 2rem;
|
||||
}
|
||||
|
||||
&[data-size='medium'] {
|
||||
--search-field-item-height: 2.5rem;
|
||||
}
|
||||
|
||||
&[data-empty] {
|
||||
.bui-SearchFieldClear {
|
||||
display: none;
|
||||
@@ -65,7 +73,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bui-SearchFieldWrapper {
|
||||
.bui-SearchFieldInputWrapper {
|
||||
.bui-SearchFieldInput[data-icon] {
|
||||
padding-right: 0px;
|
||||
}
|
||||
@@ -74,102 +82,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bui-SearchFieldWrapper {
|
||||
position: relative;
|
||||
|
||||
.bui-SearchFieldInput[data-icon] {
|
||||
padding-right: var(--bui-space-6);
|
||||
}
|
||||
|
||||
&[data-size='small'] .bui-SearchFieldInput {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
&[data-size='medium'] .bui-SearchFieldInput {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
&[data-size='small'] .bui-SearchFieldInput[data-icon] {
|
||||
padding-left: var(--bui-space-8);
|
||||
}
|
||||
|
||||
&[data-size='medium'] .bui-SearchFieldInput[data-icon] {
|
||||
padding-left: var(--bui-space-9);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-SearchFieldInputIcon {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin-right: var(--bui-space-1);
|
||||
color: var(--bui-fg-primary);
|
||||
pointer-events: none;
|
||||
/* To animate the icon when the input is collapsed */
|
||||
transition: left 0.2s ease-in-out;
|
||||
|
||||
&[data-size='small'] {
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
&[data-size='medium'] {
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
&[data-size='small'] svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
&[data-size='medium'] svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-SearchFieldInput {
|
||||
.bui-SearchFieldInputWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 var(--bui-space-3);
|
||||
border-radius: var(--bui-radius-2);
|
||||
border: 1px solid var(--bui-border);
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-family: var(--bui-font-regular);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
color: var(--bui-fg-primary);
|
||||
transition: padding 0.3s ease-in-out, border-color 0.2s ease-in-out,
|
||||
outline-color 0.2s ease-in-out;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: inherit;
|
||||
transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out;
|
||||
|
||||
&::-webkit-search-cancel-button,
|
||||
&::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
&[data-size='small'] {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bui-fg-secondary);
|
||||
&[data-size='medium'] {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
&[data-focused] {
|
||||
outline-color: var(--bui-border-pressed);
|
||||
outline-width: 0px;
|
||||
}
|
||||
|
||||
&[data-hovered] {
|
||||
border-color: var(--bui-border-hover);
|
||||
}
|
||||
|
||||
&[data-focused] {
|
||||
&:focus-within {
|
||||
border-color: var(--bui-border-pressed);
|
||||
outline-width: 0px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bui-border-hover);
|
||||
}
|
||||
|
||||
&[data-invalid] {
|
||||
border-color: var(--bui-fg-danger);
|
||||
}
|
||||
@@ -181,14 +118,75 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bui-SearchFieldClear {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
.bui-SearchFieldInputIcon {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
color: var(--bui-fg-primary);
|
||||
pointer-events: none;
|
||||
width: var(--search-field-item-height);
|
||||
height: var(--search-field-item-height);
|
||||
/* To animate the icon when the input is collapsed */
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
|
||||
& svg {
|
||||
.bui-SearchField[data-size='small'] & {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.bui-SearchField[data-size='medium'] & {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bui-SearchFieldInput {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-family: var(--bui-font-regular);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
color: var(--bui-fg-primary);
|
||||
transition: padding 0.3s ease-in-out;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
cursor: inherit;
|
||||
|
||||
&::-webkit-search-cancel-button,
|
||||
&::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bui-fg-secondary);
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.bui-SearchField[data-size='small'] & {
|
||||
padding-inline: var(--bui-space-3) 0;
|
||||
}
|
||||
|
||||
.bui-SearchField[data-size='medium'] & {
|
||||
padding-inline: var(--bui-space-3) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bui-SearchFieldClear {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
@@ -196,21 +194,13 @@
|
||||
cursor: pointer;
|
||||
color: var(--bui-fg-secondary);
|
||||
transition: color 0.2s ease-in-out;
|
||||
width: var(--search-field-item-height);
|
||||
height: var(--search-field-item-height);
|
||||
|
||||
&:hover {
|
||||
color: var(--bui-fg-primary);
|
||||
}
|
||||
|
||||
&[data-size='small'] {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
&[data-size='medium'] {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
& svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
|
||||
@@ -267,7 +267,10 @@ export const componentDefinitions = {
|
||||
PasswordField: {
|
||||
classNames: {
|
||||
root: 'bui-PasswordField',
|
||||
inputVisibility: 'bui-InputVisibility',
|
||||
inputWrapper: 'bui-PasswordFieldInputWrapper',
|
||||
input: 'bui-PasswordFieldInput',
|
||||
inputIcon: 'bui-PasswordFieldIcon',
|
||||
inputVisibility: 'bui-PasswordFieldVisibility',
|
||||
},
|
||||
dataAttributes: {
|
||||
size: ['small', 'medium'] as const,
|
||||
@@ -289,7 +292,7 @@ export const componentDefinitions = {
|
||||
classNames: {
|
||||
root: 'bui-SearchField',
|
||||
clear: 'bui-SearchFieldClear',
|
||||
inputWrapper: 'bui-SearchFieldWrapper',
|
||||
inputWrapper: 'bui-SearchFieldInputWrapper',
|
||||
input: 'bui-SearchFieldInput',
|
||||
inputIcon: 'bui-SearchFieldInputIcon',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user