diff --git a/packages/canon/src/components/Select/Select.stories.tsx b/packages/canon/src/components/Select/Select.stories.tsx index d9de7ddb4a..d04570ace7 100644 --- a/packages/canon/src/components/Select/Select.stories.tsx +++ b/packages/canon/src/components/Select/Select.stories.tsx @@ -95,13 +95,6 @@ export const DisabledOption: Story = { }, }; -export const NoLabel: Story = { - args: { - ...Preview.args, - label: undefined, - }, -}; - export const NoOptions: Story = { args: { ...Preview.args, @@ -109,13 +102,6 @@ export const NoOptions: Story = { }, }; -export const Small: Story = { - args: { - ...Preview.args, - size: 'small', - }, -}; - export const WithValue: Story = { args: { ...Preview.args, @@ -263,65 +249,9 @@ export const WithManyOptions: Story = { }, }; -async function validateFont(value: string) { - // Mimic a server response - await new Promise(resolve => { - setTimeout(resolve, 500); - }); - - const restrictedFonts = ['comic-sans']; - if (restrictedFonts.includes(value)) { - return { error: 'This font should not be allowed.' }; - } - - return { success: true }; -} - -export const ShowErrorOnSubmit: Story = { +export const withErrorAndDescription: Story = { args: { ...Preview.args, - label: 'Font Family (select Comic sans to see error)', - options: [...fontOptions, { value: 'comic-sans', label: 'Comic sans' }], - required: true, + error: 'Invalid font family', }, - decorators: [ - Story => { - const [errors, setErrors] = useState({}); - const [loading, setLoading] = useState(false); - - return ( -
{ - event.preventDefault(); - const formData = new FormData(event.currentTarget); - const fontValue = formData.get('font') as string; - - setLoading(true); - const response = await validateFont(fontValue); - if (response.error) { - setErrors({ - font: response.error, - }); - } else { - setErrors({}); - } - - setLoading(false); - }} - > - - - - ); - }, - ], }; diff --git a/packages/canon/src/components/Select/Select.styles.css b/packages/canon/src/components/Select/Select.styles.css index b1a7e53b9a..19fa327bf5 100644 --- a/packages/canon/src/components/Select/Select.styles.css +++ b/packages/canon/src/components/Select/Select.styles.css @@ -14,21 +14,21 @@ * limitations under the License. */ -.canon-SelectFieldRoot { +.canon-Select { display: flex; flex-direction: column; font-family: var(--canon-font-regular); width: 100%; } -.canon-SelectFieldLabel { +.canon-Select--label { 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-SelectFieldDescription { +.canon-Select--description { font-size: var(--canon-font-size-2); font-weight: var(--canon-font-weight-regular); color: var(--canon-fg-secondary); @@ -36,7 +36,7 @@ padding-top: var(--canon-space-1_5); } -.canon-SelectFieldError { +.canon-Select--error { font-size: var(--canon-font-size-2); font-weight: var(--canon-font-weight-regular); color: var(--canon-fg-danger); @@ -44,7 +44,7 @@ padding-top: var(--canon-space-1_5); } -.canon-SelectTrigger { +.canon-Select--trigger { box-sizing: border-box; border-radius: var(--canon-radius-3); border: 1px solid var(--canon-border); @@ -62,43 +62,41 @@ cursor: pointer; } -.canon-SelectTrigger::placeholder { +.canon-Select--trigger::placeholder { color: var(--canon-fg-secondary); } -.canon-SelectTrigger:hover { +.canon-Select--trigger:hover { border-color: var(--canon-border-hover); } -.canon-SelectTrigger:focus-visible { +.canon-Select--trigger:focus-visible { border-color: var(--canon-border-pressed); outline: 0; } -.canon-SelectTrigger[data-invalid] { +.canon-Select--trigger[data-invalid] { border-color: var(--canon-fg-danger); } -.canon-SelectTrigger[data-invalid]:hover { +.canon-Select--trigger[data-invalid]:hover { border-width: 2px; } -.canon-SelectTrigger[data-invalid]:focus-visible { +.canon-Select--trigger[data-invalid]:focus-visible { border-width: 2px; } -.canon-SelectTrigger[data-disabled] { +.canon-Select--trigger[data-disabled] { cursor: not-allowed; border-color: var(--canon-border-disabled); color: var(--canon-fg-disabled); } -.canon-SelectTrigger--size-small, -.canon-SelectItem--size-small { +.canon-Select--trigger-size-small { height: 2rem; } -.canon-SelectTrigger--size-medium, -.canon-SelectItem--size-medium { +.canon-Select--trigger-size-medium { height: 3rem; } @@ -107,11 +105,11 @@ transition: transform 0.2s ease; } -.canon-SelectTrigger[data-popup-open] .canon-SelectIcon { +.canon-Select--trigger[data-popup-open] .canon-SelectIcon { transform: rotate(180deg); } -.canon-SelectPopup { +.canon-Select--popup { box-sizing: border-box; max-height: var(--available-height); overflow-y: auto; @@ -126,13 +124,13 @@ transition: transform 150ms, opacity 150ms; } -.canon-SelectPopup[data-starting-style], -.canon-SelectPopup[data-ending-style] { +.canon-Select--popup[data-starting-style], +.canon-Select--popup[data-ending-style] { opacity: 0; transform: scale(0.9); } -.canon-SelectItem { +.canon-Select--item { position: relative; width: var(--anchor-width); display: grid; @@ -150,13 +148,13 @@ outline: none; } -.canon-SelectItem[data-highlighted] { +.canon-Select--item[data-highlighted] { z-index: 0; position: relative; color: var(--canon-fg-primary); } -.canon-SelectItem[data-highlighted]::before { +.canon-Select--item[data-highlighted]::before { content: ''; z-index: -1; position: absolute; @@ -166,19 +164,26 @@ background-color: var(--canon-bg-tint-hover); } -.canon-SelectItem[data-disabled] { +.canon-Select--item[data-disabled] { cursor: not-allowed; color: var(--canon-fg-disabled); } -.canon-SelectItemIndicator { +.canon-Select--item-indicator { grid-area: icon; display: flex; align-items: center; justify-content: center; } -.canon-SelectItemText { +.canon-Select--item-text { flex: 1; grid-area: text; } + +.canon-Select--required { + color: var(--canon-fg-secondary); + font-size: var(--canon-font-size-2); + font-weight: var(--canon-font-weight-regular); + margin-left: var(--canon-space-1); +} diff --git a/packages/canon/src/components/Select/Select.tsx b/packages/canon/src/components/Select/Select.tsx index bcacefd02e..835bb8303a 100644 --- a/packages/canon/src/components/Select/Select.tsx +++ b/packages/canon/src/components/Select/Select.tsx @@ -16,76 +16,77 @@ import React from 'react'; import { Select as SelectPrimitive } from '@base-ui-components/react/select'; -import { Field } from '@base-ui-components/react/field'; import { Icon } from '../Icon'; import clsx from 'clsx'; import './Select.styles.css'; import { SelectProps } from './types'; +import { useResponsiveValue } from '../../hooks/useResponsiveValue'; /** @public */ -export const Select = React.forwardRef( +export const Select = React.forwardRef( (props, ref) => { const { className, - name, label, description, - value, - defaultValue, - onValueChange, - onOpenChange, options, placeholder = 'Select an option', size = 'medium', - disabled = false, - required = false, + required, + error, style, + ...rest } = props; + + // Get the responsive value for the variant + const responsiveSize = useResponsiveValue(size); + + // Generate unique IDs for accessibility + const selectId = React.useId(); + const descriptionId = React.useId(); + const errorId = React.useId(); + return ( - +
{label && ( - {label} - )} - - + {label} + {required && ( + )} + + )} + + - + - + {options?.map(option => ( - + - + {option.label} @@ -95,12 +96,16 @@ export const Select = React.forwardRef( {description && ( - +

{description} - +

)} - - + {error && ( + + )} +
); }, ); diff --git a/packages/canon/src/components/Select/types.ts b/packages/canon/src/components/Select/types.ts index 960129da70..adf8568b98 100644 --- a/packages/canon/src/components/Select/types.ts +++ b/packages/canon/src/components/Select/types.ts @@ -15,6 +15,7 @@ */ import { Breakpoint } from '@backstage/canon'; +import { ChangeEvent, FocusEvent } from 'react'; /** @public */ export interface SelectProps { @@ -91,4 +92,19 @@ export interface SelectProps { * The style of the select field */ style?: React.CSSProperties; + + /** + * The error message of the select field + */ + error?: string; + + /** + * onChange handler for form integration + */ + onChange?: (event: ChangeEvent) => void; + + /** + * onBlur handler for form integration + */ + onBlur?: (event: FocusEvent) => void; } diff --git a/packages/canon/src/components/TextField/TextField.tsx b/packages/canon/src/components/TextField/TextField.tsx index 1b6cf21e0a..d144781983 100644 --- a/packages/canon/src/components/TextField/TextField.tsx +++ b/packages/canon/src/components/TextField/TextField.tsx @@ -31,6 +31,7 @@ export const TextField = forwardRef( description, error, required, + style, ...rest } = props; @@ -43,7 +44,11 @@ export const TextField = forwardRef( const errorId = React.useId(); return ( -
+
{label && (