Add new TextField component
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -404,6 +404,36 @@
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.canon-FieldRoot {
|
||||
font-family: var(--canon-font-regular);
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.canon-FieldLabel {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-primary);
|
||||
margin-bottom: var(--canon-space-1_5);
|
||||
}
|
||||
|
||||
.canon-FieldDescription {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-secondary);
|
||||
padding-top: var(--canon-space-1_5);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.canon-FieldError {
|
||||
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-Input {
|
||||
border-radius: var(--canon-radius-3);
|
||||
border: 1px solid var(--canon-border);
|
||||
@@ -435,52 +465,20 @@
|
||||
border-color: var(--canon-fg-danger);
|
||||
}
|
||||
|
||||
.canon-Input--size-sm {
|
||||
.canon-Input[data-disabled] {
|
||||
opacity: .5;
|
||||
cursor: not-allowed;
|
||||
border: 1px solid var(--canon-border-disabled);
|
||||
}
|
||||
|
||||
.canon-Input--size-small {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.canon-Input--size-md {
|
||||
.canon-Input--size-medium {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.canon-FieldRoot {
|
||||
font-family: var(--canon-font-regular);
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.canon-FieldLabel {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-primary);
|
||||
margin-bottom: var(--canon-space-1_5);
|
||||
}
|
||||
|
||||
.canon-FieldDescription {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-secondary);
|
||||
padding-top: var(--canon-space-1_5);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.canon-FieldError {
|
||||
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-FieldValidity {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-secondary);
|
||||
padding-top: var(--canon-space-1_5);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.canon-MenuTrigger {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--color-gray-200);
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
.canon-FieldRoot {
|
||||
font-family: var(--canon-font-regular);
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.canon-FieldLabel {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-primary);
|
||||
margin-bottom: var(--canon-space-1_5);
|
||||
}
|
||||
|
||||
.canon-FieldDescription {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-secondary);
|
||||
padding-top: var(--canon-space-1_5);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.canon-FieldError {
|
||||
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-FieldValidity {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-secondary);
|
||||
padding-top: var(--canon-space-1_5);
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
.canon-Input {
|
||||
border-radius: var(--canon-radius-3);
|
||||
border: 1px solid var(--canon-border);
|
||||
padding: 0 var(--canon-space-4);
|
||||
background-color: var(--canon-bg-surface-1);
|
||||
font-size: var(--canon-font-size-3);
|
||||
font-family: var(--canon-font-regular);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-primary);
|
||||
width: 100%;
|
||||
transition: border-color .2s ease-in-out, outline-color .2s ease-in-out;
|
||||
}
|
||||
|
||||
.canon-Input::placeholder {
|
||||
color: var(--canon-fg-secondary);
|
||||
}
|
||||
|
||||
.canon-Input:hover {
|
||||
border-color: var(--canon-border-hover);
|
||||
}
|
||||
|
||||
.canon-Input:focus-visible {
|
||||
outline-color: var(--canon-border-pressed);
|
||||
border-color: var(--canon-border-pressed);
|
||||
outline-width: 0;
|
||||
}
|
||||
|
||||
.canon-Input[data-invalid] {
|
||||
border-color: var(--canon-fg-danger);
|
||||
}
|
||||
|
||||
.canon-Input--size-sm {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.canon-Input--size-md {
|
||||
height: 2.5rem;
|
||||
}
|
||||
@@ -9605,6 +9605,36 @@
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.canon-FieldRoot {
|
||||
font-family: var(--canon-font-regular);
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.canon-FieldLabel {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-primary);
|
||||
margin-bottom: var(--canon-space-1_5);
|
||||
}
|
||||
|
||||
.canon-FieldDescription {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-secondary);
|
||||
padding-top: var(--canon-space-1_5);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.canon-FieldError {
|
||||
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-Input {
|
||||
border-radius: var(--canon-radius-3);
|
||||
border: 1px solid var(--canon-border);
|
||||
@@ -9636,52 +9666,20 @@
|
||||
border-color: var(--canon-fg-danger);
|
||||
}
|
||||
|
||||
.canon-Input--size-sm {
|
||||
.canon-Input[data-disabled] {
|
||||
opacity: .5;
|
||||
cursor: not-allowed;
|
||||
border: 1px solid var(--canon-border-disabled);
|
||||
}
|
||||
|
||||
.canon-Input--size-small {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.canon-Input--size-md {
|
||||
.canon-Input--size-medium {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.canon-FieldRoot {
|
||||
font-family: var(--canon-font-regular);
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.canon-FieldLabel {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-primary);
|
||||
margin-bottom: var(--canon-space-1_5);
|
||||
}
|
||||
|
||||
.canon-FieldDescription {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-secondary);
|
||||
padding-top: var(--canon-space-1_5);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.canon-FieldError {
|
||||
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-FieldValidity {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-secondary);
|
||||
padding-top: var(--canon-space-1_5);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.canon-MenuTrigger {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--color-gray-200);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
.canon-FieldRoot {
|
||||
font-family: var(--canon-font-regular);
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.canon-FieldLabel {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-primary);
|
||||
margin-bottom: var(--canon-space-1_5);
|
||||
}
|
||||
|
||||
.canon-FieldDescription {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-secondary);
|
||||
padding-top: var(--canon-space-1_5);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.canon-FieldError {
|
||||
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-Input {
|
||||
border-radius: var(--canon-radius-3);
|
||||
border: 1px solid var(--canon-border);
|
||||
padding: 0 var(--canon-space-4);
|
||||
background-color: var(--canon-bg-surface-1);
|
||||
font-size: var(--canon-font-size-3);
|
||||
font-family: var(--canon-font-regular);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-primary);
|
||||
width: 100%;
|
||||
transition: border-color .2s ease-in-out, outline-color .2s ease-in-out;
|
||||
}
|
||||
|
||||
.canon-Input::placeholder {
|
||||
color: var(--canon-fg-secondary);
|
||||
}
|
||||
|
||||
.canon-Input:hover {
|
||||
border-color: var(--canon-border-hover);
|
||||
}
|
||||
|
||||
.canon-Input:focus-visible {
|
||||
outline-color: var(--canon-border-pressed);
|
||||
border-color: var(--canon-border-pressed);
|
||||
outline-width: 0;
|
||||
}
|
||||
|
||||
.canon-Input[data-invalid] {
|
||||
border-color: var(--canon-fg-danger);
|
||||
}
|
||||
|
||||
.canon-Input[data-disabled] {
|
||||
opacity: .5;
|
||||
cursor: not-allowed;
|
||||
border: 1px solid var(--canon-border-disabled);
|
||||
}
|
||||
|
||||
.canon-Input--size-small {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.canon-Input--size-medium {
|
||||
height: 2.5rem;
|
||||
}
|
||||
@@ -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 React from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Field } from './Field';
|
||||
import { Input } from '../Input/Input';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Field',
|
||||
component: Field.Root,
|
||||
} satisfies Meta<typeof Field.Root>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Field.Root>
|
||||
<Field.Label>Label</Field.Label>
|
||||
<Input />
|
||||
<Field.Error>Error</Field.Error>
|
||||
</Field.Root>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithLabelAndDescription: Story = {
|
||||
render: () => (
|
||||
<Field.Root>
|
||||
<Field.Label>Label</Field.Label>
|
||||
<Input />
|
||||
<Field.Description>Description</Field.Description>
|
||||
</Field.Root>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithError: Story = {
|
||||
render: () => (
|
||||
<Field.Root
|
||||
validate={value =>
|
||||
value !== 'Backstage' ? 'Please enter a different name' : null
|
||||
}
|
||||
validationMode="onChange"
|
||||
>
|
||||
<Field.Label>Name</Field.Label>
|
||||
<Input />
|
||||
<Field.Description>
|
||||
An error will show if the value is not Backstage
|
||||
</Field.Description>
|
||||
<Field.Error match="customError">Error</Field.Error>
|
||||
</Field.Root>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithValidity: Story = {
|
||||
render: () => (
|
||||
<Field.Root
|
||||
validate={value =>
|
||||
value !== 'Backstage' ? 'Please enter a different name' : null
|
||||
}
|
||||
>
|
||||
<Field.Label>Name</Field.Label>
|
||||
<Input />
|
||||
<Field.Description>
|
||||
An error will show if the value is not Backstage
|
||||
</Field.Description>
|
||||
<Field.Validity>
|
||||
{validityState => (
|
||||
<div>{validityState.value ? 'Not Backstage' : 'Backstage'}</div>
|
||||
)}
|
||||
</Field.Validity>
|
||||
</Field.Root>
|
||||
),
|
||||
};
|
||||
@@ -1,53 +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-FieldRoot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: var(--canon-font-regular);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.canon-FieldLabel {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-primary);
|
||||
margin-bottom: var(--canon-space-1_5);
|
||||
}
|
||||
|
||||
.canon-FieldDescription {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-secondary);
|
||||
margin: 0;
|
||||
padding-top: var(--canon-space-1_5);
|
||||
}
|
||||
|
||||
.canon-FieldError {
|
||||
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-FieldValidity {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-secondary);
|
||||
margin: 0;
|
||||
padding-top: var(--canon-space-1_5);
|
||||
}
|
||||
@@ -1,92 +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 React from 'react';
|
||||
import { Field as FieldPrimitive } from '@base-ui-components/react/field';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const FieldRoot = React.forwardRef<
|
||||
React.ElementRef<typeof FieldPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof FieldPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<FieldPrimitive.Root
|
||||
ref={ref}
|
||||
className={clsx('canon-FieldRoot', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
FieldRoot.displayName = FieldPrimitive.Root.displayName;
|
||||
|
||||
const FieldLabel = React.forwardRef<
|
||||
React.ElementRef<typeof FieldPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof FieldPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<FieldPrimitive.Label
|
||||
ref={ref}
|
||||
className={clsx('canon-FieldLabel', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
FieldLabel.displayName = FieldPrimitive.Label.displayName;
|
||||
|
||||
const FieldDescription = React.forwardRef<
|
||||
React.ElementRef<typeof FieldPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof FieldPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<FieldPrimitive.Description
|
||||
ref={ref}
|
||||
className={clsx('canon-FieldDescription', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
FieldDescription.displayName = FieldPrimitive.Description.displayName;
|
||||
|
||||
const FieldError = React.forwardRef<
|
||||
React.ElementRef<typeof FieldPrimitive.Error>,
|
||||
React.ComponentPropsWithoutRef<typeof FieldPrimitive.Error>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<FieldPrimitive.Error
|
||||
ref={ref}
|
||||
className={clsx('canon-FieldError', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
FieldError.displayName = FieldPrimitive.Error.displayName;
|
||||
|
||||
const FieldValidity = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof FieldPrimitive.Validity> & {
|
||||
className?: string;
|
||||
}) => (
|
||||
<FieldPrimitive.Validity {...props}>
|
||||
{validityState => (
|
||||
<div className={clsx('canon-FieldValidity', className)}>
|
||||
{children(validityState)}
|
||||
</div>
|
||||
)}
|
||||
</FieldPrimitive.Validity>
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export const Field = {
|
||||
Root: FieldRoot,
|
||||
Label: FieldLabel,
|
||||
Description: FieldDescription,
|
||||
Error: FieldError,
|
||||
Validity: FieldValidity,
|
||||
};
|
||||
@@ -1,41 +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 React from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Input } from './Input';
|
||||
import { Flex } from '../Flex';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Input',
|
||||
component: Input,
|
||||
} satisfies Meta<typeof Input>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Primary: Story = {
|
||||
render: () => <Input className="canon-Input" />,
|
||||
};
|
||||
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<Flex>
|
||||
<Input size="sm" />
|
||||
<Input size="md" />
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
@@ -1,43 +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 React, { ElementRef, forwardRef } from 'react';
|
||||
import { Input as InputPrimitive } from '@base-ui-components/react/input';
|
||||
import clsx from 'clsx';
|
||||
import type { InputProps } from './types';
|
||||
|
||||
/** @public */
|
||||
const Input = forwardRef<ElementRef<typeof InputPrimitive>, InputProps>(
|
||||
(props, ref) => {
|
||||
const { size = 'md', className, ...rest } = props;
|
||||
|
||||
return (
|
||||
<InputPrimitive
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
'canon-Input',
|
||||
size === 'sm' ? 'canon-Input--size-sm' : 'canon-Input--size-md',
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Input.displayName = InputPrimitive.displayName;
|
||||
|
||||
export { Input };
|
||||
@@ -1,18 +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.
|
||||
*/
|
||||
|
||||
export { Input } from './Input';
|
||||
export type { InputProps } from './types';
|
||||
+16
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
* 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.
|
||||
@@ -14,9 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Input } from '@base-ui-components/react/input';
|
||||
import type { PropDef, GetPropDefTypes } from '../../props/prop-def';
|
||||
|
||||
/** @public */
|
||||
export interface InputProps extends Omit<Input.Props, 'size'> {
|
||||
size?: 'sm' | 'md';
|
||||
}
|
||||
export const textFieldPropDefs = {
|
||||
size: {
|
||||
type: 'enum',
|
||||
values: ['small', 'medium'],
|
||||
className: 'canon-Button--size',
|
||||
default: 'medium',
|
||||
responsive: true,
|
||||
},
|
||||
} satisfies {
|
||||
size: PropDef<'small' | 'medium'>;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type TextFieldOwnProps = GetPropDefTypes<typeof textFieldPropDefs>;
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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 React, { useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { TextField } from './TextField';
|
||||
import { Form } from '@base-ui-components/react/form';
|
||||
import { Button } from '../Button';
|
||||
import { userEvent, waitFor, within, expect } from '@storybook/test';
|
||||
import { Flex } from '../Flex';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/TextField',
|
||||
component: TextField,
|
||||
} satisfies Meta<typeof TextField>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
name: 'url',
|
||||
placeholder: 'Enter a URL',
|
||||
},
|
||||
};
|
||||
|
||||
export const Filled: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
defaultValue: 'https://example.com',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLabel: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
label: 'Label',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithDescription: Story = {
|
||||
args: {
|
||||
...WithLabel.args,
|
||||
description: 'Description',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
...WithLabel.args,
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Sizes: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
label: 'Label',
|
||||
description: 'Description',
|
||||
},
|
||||
render: args => (
|
||||
<Flex direction="row" gap="4">
|
||||
<TextField {...args} size="small" />
|
||||
<TextField {...args} size="medium" />
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
||||
export const Responsive: Story = {
|
||||
args: {
|
||||
...WithLabel.args,
|
||||
size: {
|
||||
initial: 'small',
|
||||
sm: 'medium',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async function submitForm(value: string) {
|
||||
// Mimic a server response
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 200);
|
||||
});
|
||||
|
||||
try {
|
||||
const url = new URL(value);
|
||||
|
||||
if (url.hostname.endsWith('example.com')) {
|
||||
return { error: 'The example domain is not allowed' };
|
||||
}
|
||||
} catch {
|
||||
return { error: 'This is not a valid URL' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export const ShowErrorOnSubmit: Story = {
|
||||
args: {
|
||||
...WithLabel.args,
|
||||
pattern: 'https?://.*',
|
||||
type: 'url',
|
||||
required: true,
|
||||
label: 'Homepage',
|
||||
},
|
||||
decorators: [
|
||||
Story => {
|
||||
const [errors, setErrors] = useState({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<Form
|
||||
errors={errors}
|
||||
onClearErrors={setErrors}
|
||||
onSubmit={async event => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const value = formData.get('url') as string;
|
||||
|
||||
setLoading(true);
|
||||
const response = await submitForm(value);
|
||||
const serverErrors = {
|
||||
url: response.error,
|
||||
};
|
||||
|
||||
setErrors(serverErrors);
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
size="small"
|
||||
style={{ marginTop: '0.75rem' }}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
},
|
||||
],
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = canvas.getByLabelText('Homepage', {
|
||||
selector: 'input',
|
||||
});
|
||||
|
||||
await userEvent.type(input, 'https://example.com', {
|
||||
delay: 20,
|
||||
});
|
||||
|
||||
const submitButton = canvas.getByRole('button');
|
||||
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
canvas.getByText('The example domain is not allowed'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
};
|
||||
+38
-2
@@ -14,6 +14,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.canon-FieldRoot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: var(--canon-font-regular);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.canon-FieldLabel {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-primary);
|
||||
margin-bottom: var(--canon-space-1_5);
|
||||
}
|
||||
|
||||
.canon-FieldDescription {
|
||||
font-size: var(--canon-font-size-2);
|
||||
font-weight: var(--canon-font-weight-regular);
|
||||
color: var(--canon-fg-secondary);
|
||||
margin: 0;
|
||||
padding-top: var(--canon-space-1_5);
|
||||
}
|
||||
|
||||
.canon-FieldError {
|
||||
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-Input {
|
||||
border-radius: var(--canon-radius-3);
|
||||
border: 1px solid var(--canon-border);
|
||||
@@ -45,10 +75,16 @@
|
||||
border-color: var(--canon-fg-danger);
|
||||
}
|
||||
|
||||
.canon-Input--size-sm {
|
||||
.canon-Input[data-disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
border: 1px solid var(--canon-border-disabled);
|
||||
}
|
||||
|
||||
.canon-Input--size-small {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.canon-Input--size-md {
|
||||
.canon-Input--size-medium {
|
||||
height: 2.5rem;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 React, { forwardRef } from 'react';
|
||||
import { Field } from '@base-ui-components/react/field';
|
||||
import { Input } from '@base-ui-components/react/input';
|
||||
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import type { TextFieldProps } from './types';
|
||||
|
||||
/** @public */
|
||||
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
(props: TextFieldProps, ref) => {
|
||||
const {
|
||||
className,
|
||||
size = 'medium',
|
||||
label,
|
||||
description,
|
||||
name,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
// Get the responsive value for the variant
|
||||
const responsiveSize = useResponsiveValue(size);
|
||||
|
||||
return (
|
||||
<Field.Root className={clsx('canon-FieldRoot', className)} name={name}>
|
||||
{label && (
|
||||
<Field.Label className={clsx('canon-FieldLabel', className)}>
|
||||
{label}
|
||||
</Field.Label>
|
||||
)}
|
||||
<Input
|
||||
ref={ref}
|
||||
type={name}
|
||||
className={clsx(
|
||||
'canon-Input',
|
||||
`canon-Input--size-${responsiveSize}`,
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
{description && (
|
||||
<Field.Description
|
||||
className={clsx('canon-FieldDescription', className)}
|
||||
>
|
||||
{description}
|
||||
</Field.Description>
|
||||
)}
|
||||
<Field.Error className={clsx('canon-FieldError', className)} />
|
||||
</Field.Root>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TextField.displayName = 'TextField';
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './Field';
|
||||
export * from './TextField';
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 type { TextFieldOwnProps } from './TextField.props';
|
||||
|
||||
export interface TextFieldProps
|
||||
extends Omit<React.ComponentPropsWithoutRef<'input'>, 'size'> {
|
||||
/**
|
||||
* The class name of the text field
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The size of the text field
|
||||
* @defaultValue 'medium'
|
||||
*/
|
||||
size?: TextFieldOwnProps['size'];
|
||||
|
||||
/**
|
||||
* The label of the text field
|
||||
*/
|
||||
label?: string;
|
||||
|
||||
/**
|
||||
* The description of the text field
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* The name of the text field
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
@@ -25,8 +25,7 @@
|
||||
@import '../components/Text/styles.css';
|
||||
@import '../components/Heading/styles.css';
|
||||
@import '../components/IconButton/styles.css';
|
||||
@import '../components/Input/Input.styles.css';
|
||||
@import '../components/Field/Field.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';
|
||||
|
||||
@@ -37,8 +37,7 @@ export * from './components/Icon';
|
||||
export * from './components/IconButton';
|
||||
export * from './components/Checkbox';
|
||||
export * from './components/Table';
|
||||
export * from './components/Input';
|
||||
export * from './components/Field';
|
||||
export * from './components/TextField';
|
||||
export * from './components/Tooltip';
|
||||
export * from './components/Menu';
|
||||
export * from './components/ScrollArea';
|
||||
|
||||
Reference in New Issue
Block a user