Add new FieldLabel component
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -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.
|
||||
+499
-554
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
.canon-TextFieldLabelWrapper {
|
||||
margin-bottom: var(--canon-space-3);
|
||||
gap: var(--canon-space-1);
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.canon-TextFieldLabel {
|
||||
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-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);
|
||||
font-size: var(--canon-font-size-2);
|
||||
color: var(--canon-fg-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
+499
-554
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,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';
|
||||
@@ -26,6 +25,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 } 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';
|
||||
@@ -354,6 +354,18 @@ export type EnumPropDef<T> = {
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const FieldLabel: ForwardRefExoticComponent<
|
||||
FieldLabelProps & RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface FieldLabelProps {
|
||||
description?: string | null;
|
||||
label?: string | null;
|
||||
secondaryLabel?: string | null;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const Flex: ForwardRefExoticComponent<
|
||||
FlexProps & RefAttributes<HTMLDivElement>
|
||||
@@ -408,6 +420,14 @@ export interface FlexProps extends SpaceProps {
|
||||
// @public (undocumented)
|
||||
export type FlexWrap = 'wrap' | 'nowrap' | 'wrap-reverse';
|
||||
|
||||
// @public (undocumented)
|
||||
export interface FormInputProps
|
||||
extends Omit<TextFieldProps, 'size'>,
|
||||
FieldLabelProps {
|
||||
icon?: ReactNode;
|
||||
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const gapPropDefs: {
|
||||
gap: {
|
||||
@@ -1276,25 +1296,9 @@ export { Text_2 as Text };
|
||||
|
||||
// @public (undocumented)
|
||||
export const TextField: ForwardRefExoticComponent<
|
||||
TextFieldProps & RefAttributes<HTMLDivElement>
|
||||
FormInputProps & RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface TextFieldProps
|
||||
extends Omit<React.ComponentPropsWithoutRef<'input'>, 'size'> {
|
||||
className?: string;
|
||||
description?: string;
|
||||
error?: string | null;
|
||||
hideLabelAndDescription?: boolean;
|
||||
icon?: ReactNode;
|
||||
label?: string;
|
||||
labelSize?: 'small' | 'medium';
|
||||
name: string;
|
||||
onClear?: MouseEventHandler<HTMLButtonElement>;
|
||||
secondaryLabel?: string;
|
||||
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface TextProps
|
||||
extends Omit<useRender.ComponentProps<'p'>, 'color'> {
|
||||
|
||||
@@ -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<typeof FieldLabel>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
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',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
.canon-TextFieldLabelWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: var(--canon-space-3);
|
||||
gap: var(--canon-space-1);
|
||||
}
|
||||
|
||||
.canon-TextFieldLabel {
|
||||
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-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);
|
||||
font-size: var(--canon-font-size-2);
|
||||
color: var(--canon-fg-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
@@ -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<HTMLDivElement, FieldLabelProps>(
|
||||
(props: FieldLabelProps, ref) => {
|
||||
const { label, secondaryLabel, description } = props;
|
||||
|
||||
if (!label) return null;
|
||||
|
||||
return (
|
||||
<div className="canon-TextFieldLabelWrapper" ref={ref}>
|
||||
{label && (
|
||||
<Label className="canon-TextFieldLabel">
|
||||
{label}
|
||||
{secondaryLabel && (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="canon-TextFieldSecondaryLabel"
|
||||
>
|
||||
({secondaryLabel})
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
)}
|
||||
{description && (
|
||||
<div className="canon-TextFieldDescription">{description}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FieldLabel.displayName = 'FieldLabel';
|
||||
+2
-2
@@ -14,5 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './TextField';
|
||||
export type { TextFieldProps } from './types';
|
||||
export * from './FieldLabel';
|
||||
export * from './types';
|
||||
+10
-10
@@ -14,20 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputProps } from 'react-aria-components';
|
||||
import { ReactNode } from 'react';
|
||||
import type { Breakpoint } from '../../types';
|
||||
|
||||
/** @public */
|
||||
export interface FormInputProps extends Omit<InputProps, 'size'> {
|
||||
export interface FieldLabelProps {
|
||||
/**
|
||||
* An icon to render before the input
|
||||
* The label of the text field
|
||||
*/
|
||||
icon?: ReactNode;
|
||||
label?: string | null;
|
||||
|
||||
/**
|
||||
* The size of the text field
|
||||
* @defaultValue 'medium'
|
||||
* The secondary label of the text field
|
||||
*/
|
||||
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
|
||||
secondaryLabel?: string | null;
|
||||
|
||||
/**
|
||||
* The description of the text field
|
||||
*/
|
||||
description?: string | null;
|
||||
}
|
||||
@@ -1,87 +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 type { Meta, StoryObj } from '@storybook/react';
|
||||
import { FormInput } from './FormInput';
|
||||
import { Icon } from '../Icon';
|
||||
import { Flex } from '../Flex';
|
||||
|
||||
const meta = {
|
||||
title: 'Forms/FormInput',
|
||||
component: FormInput,
|
||||
argTypes: {
|
||||
required: {
|
||||
control: 'boolean',
|
||||
},
|
||||
icon: {
|
||||
control: 'object',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof FormInput>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
name: 'url',
|
||||
placeholder: 'Enter a URL',
|
||||
style: {
|
||||
maxWidth: '300px',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Sizes: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
render: args => (
|
||||
<Flex direction="row" gap="4" style={{ width: '100%', maxWidth: '600px' }}>
|
||||
<FormInput {...args} size="small" icon={<Icon name="sparkling" />} />
|
||||
<FormInput {...args} size="medium" icon={<Icon name="sparkling" />} />
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
||||
export const Filled: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
defaultValue: 'https://example.com',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
placeholder: 'Search...',
|
||||
icon: <Icon name="search" />,
|
||||
},
|
||||
};
|
||||
|
||||
export const DisabledWithIcon: Story = {
|
||||
args: {
|
||||
...WithIcon.args,
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
@@ -1,91 +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.
|
||||
*/
|
||||
|
||||
.canon-FormInput {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 var(--canon-space-3);
|
||||
border-radius: var(--canon-radius-3);
|
||||
border: 1px solid var(--canon-border);
|
||||
background-color: var(--canon-bg-surface-1);
|
||||
}
|
||||
|
||||
.canon-FormInputIcon {
|
||||
margin-right: var(--canon-space-1);
|
||||
color: var(--canon-fg-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.canon-FormInputIcon[data-size='small'],
|
||||
.canon-FormInputIcon[data-size='small'] svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.canon-FormInputIcon[data-size='medium'],
|
||||
.canon-FormInputIcon[data-size='medium'] svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.canon-Input {
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: var(--canon-font-size-3);
|
||||
font-family: var(--canon-font-regular);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-primary);
|
||||
transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.canon-Input::placeholder {
|
||||
color: var(--canon-fg-secondary);
|
||||
}
|
||||
|
||||
.canon-FormInput[data-size='small'] {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.canon-FormInput[data-size='medium'] {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.canon-Input[data-focused] {
|
||||
outline-color: var(--canon-border-pressed);
|
||||
outline-width: 0px;
|
||||
}
|
||||
|
||||
.canon-FormInput:has(> .canon-Input:hover) {
|
||||
border-color: var(--canon-border-hover);
|
||||
}
|
||||
|
||||
.canon-FormInput[data-focused] {
|
||||
border-color: var(--canon-border-pressed);
|
||||
}
|
||||
|
||||
.canon-FormInput[data-invalid] {
|
||||
border-color: var(--canon-fg-danger);
|
||||
}
|
||||
|
||||
.canon-FormInput[data-disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
border: 1px solid var(--canon-border-disabled);
|
||||
}
|
||||
@@ -1,48 +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 { forwardRef } from 'react';
|
||||
import { Input } from 'react-aria-components';
|
||||
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import type { FormInputProps } from './types';
|
||||
|
||||
/** @public */
|
||||
export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||
(props: FormInputProps, ref) => {
|
||||
const { className, icon, size = 'small', ...rest } = props;
|
||||
|
||||
// Get the responsive value for the variant
|
||||
const responsiveSize = useResponsiveValue(size);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('canon-FormInput', className)}
|
||||
data-size={responsiveSize}
|
||||
>
|
||||
{icon && (
|
||||
<div className="canon-FormInputIcon" aria-hidden="true">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<Input className="canon-Input" {...rest} ref={ref} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FormInput.displayName = 'FormInput';
|
||||
@@ -21,34 +21,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.canon-TextFieldLabelWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: var(--canon-space-3);
|
||||
gap: var(--canon-space-1);
|
||||
}
|
||||
|
||||
.canon-TextFieldLabel {
|
||||
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-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);
|
||||
font-size: var(--canon-font-size-2);
|
||||
color: var(--canon-fg-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.canon-TextFieldInputWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ import { forwardRef, useEffect } from 'react';
|
||||
import {
|
||||
Input,
|
||||
TextField as AriaTextField,
|
||||
Label,
|
||||
FieldError,
|
||||
} from 'react-aria-components';
|
||||
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
|
||||
import clsx from 'clsx';
|
||||
import { FieldLabel } from '../FieldLabel';
|
||||
|
||||
import type { FormInputProps } from './types';
|
||||
|
||||
@@ -64,26 +64,11 @@ export const TextField = forwardRef<HTMLDivElement, FormInputProps>(
|
||||
{...rest}
|
||||
ref={ref}
|
||||
>
|
||||
{label && (
|
||||
<div className="canon-TextFieldLabelWrapper">
|
||||
{label && (
|
||||
<Label className="canon-TextFieldLabel">
|
||||
{label}
|
||||
{secondaryLabelText && (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="canon-TextFieldSecondaryLabel"
|
||||
>
|
||||
({secondaryLabelText})
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
)}
|
||||
{description && (
|
||||
<div className="canon-TextFieldDescription">{description}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<FieldLabel
|
||||
label={label}
|
||||
secondaryLabel={secondaryLabelText}
|
||||
description={description}
|
||||
/>
|
||||
<div className="canon-TextFieldInputWrapper" data-size={responsiveSize}>
|
||||
{icon && (
|
||||
<div
|
||||
|
||||
@@ -15,4 +15,4 @@
|
||||
*/
|
||||
|
||||
export * from './TextField';
|
||||
export type { TextFieldProps } from './types';
|
||||
export * from './types';
|
||||
|
||||
@@ -17,9 +17,12 @@
|
||||
import type { TextFieldProps } from 'react-aria-components';
|
||||
import { ReactNode } from 'react';
|
||||
import type { Breakpoint } from '../../types';
|
||||
import type { FieldLabelProps } from '../FieldLabel/types';
|
||||
|
||||
/** @public */
|
||||
export interface FormInputProps extends Omit<TextFieldProps, 'size'> {
|
||||
export interface FormInputProps
|
||||
extends Omit<TextFieldProps, 'size'>,
|
||||
FieldLabelProps {
|
||||
/**
|
||||
* An icon to render before the input
|
||||
*/
|
||||
@@ -30,24 +33,4 @@ export interface FormInputProps extends Omit<TextFieldProps, 'size'> {
|
||||
* @defaultValue 'medium'
|
||||
*/
|
||||
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
|
||||
|
||||
/**
|
||||
* The label of the text field
|
||||
*/
|
||||
label?: string;
|
||||
|
||||
/**
|
||||
* The secondary label of the text field
|
||||
*/
|
||||
secondaryLabel?: string;
|
||||
|
||||
/**
|
||||
* The description of the text field
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* Whether the field is required
|
||||
*/
|
||||
isRequired?: boolean;
|
||||
}
|
||||
|
||||
@@ -17,15 +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/FormInput/FormInput.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';
|
||||
@@ -33,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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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: 'Forms/Form',
|
||||
} satisfies Meta;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
type Inputs = {
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
city: string;
|
||||
};
|
||||
|
||||
export const Uncontrolled: Story = {
|
||||
render: () => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<Inputs>();
|
||||
|
||||
const onSubmit: SubmitHandler<Inputs> = data => console.log(data);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="First Name"
|
||||
{...register('firstname', {
|
||||
required: 'First name is required',
|
||||
maxLength: { value: 80, message: 'Max length is 80 characters' },
|
||||
})}
|
||||
error={errors.firstname?.message}
|
||||
/>
|
||||
<TextField
|
||||
label="Last Name"
|
||||
{...register('lastname', {
|
||||
required: 'Last name is required',
|
||||
maxLength: { value: 100, message: 'Max length is 100 characters' },
|
||||
})}
|
||||
error={errors.lastname?.message}
|
||||
/>
|
||||
<Controller
|
||||
name="city"
|
||||
control={control}
|
||||
rules={{ required: 'New city is required' }}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Select
|
||||
label="New City"
|
||||
options={[
|
||||
{ value: 'london', label: 'London' },
|
||||
{ value: 'paris', label: 'Paris' },
|
||||
{ value: 'new-york', label: 'New York' },
|
||||
]}
|
||||
name={field.name}
|
||||
onValueChange={field.onChange}
|
||||
error={errors.city?.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Controlled: Story = {
|
||||
render: () => {
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<Inputs>();
|
||||
|
||||
const [firstname, setFirstname] = useState('John');
|
||||
const [lastname, setLastname] = useState('Doe');
|
||||
const [city, setCity] = useState('london');
|
||||
|
||||
const onSubmit: SubmitHandler<Inputs> = data => {
|
||||
console.log('data', data);
|
||||
setFirstname(data.firstname);
|
||||
setLastname(data.lastname);
|
||||
setCity(data.city);
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name="firstname"
|
||||
control={control}
|
||||
defaultValue={firstname}
|
||||
rules={{ required: 'First name is required' }}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<TextField
|
||||
label="First Name"
|
||||
name={field.name}
|
||||
value={firstname}
|
||||
onChange={e => {
|
||||
field.onChange(e);
|
||||
setFirstname(e.target.value);
|
||||
}}
|
||||
error={errors.firstname?.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="lastname"
|
||||
control={control}
|
||||
defaultValue={lastname}
|
||||
rules={{ required: 'Last name is required' }}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<TextField
|
||||
label="Last Name"
|
||||
name={field.name}
|
||||
value={lastname}
|
||||
onChange={e => {
|
||||
field.onChange(e);
|
||||
setLastname(e.target.value);
|
||||
}}
|
||||
error={errors.lastname?.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="city"
|
||||
control={control}
|
||||
defaultValue={city}
|
||||
rules={{ required: 'New city is required' }}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Select
|
||||
label="New City"
|
||||
options={[
|
||||
{ value: 'london', label: 'London' },
|
||||
{ value: 'paris', label: 'Paris' },
|
||||
{ value: 'new-york', label: 'New York' },
|
||||
]}
|
||||
name={field.name}
|
||||
value={city}
|
||||
onValueChange={field.onChange}
|
||||
error={errors.city?.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user