diff --git a/packages/canon/src/components/Select/Select.stories.tsx b/packages/canon/src/components/Select/Select.stories.tsx index 000ba180dc..179959d649 100644 --- a/packages/canon/src/components/Select/Select.stories.tsx +++ b/packages/canon/src/components/Select/Select.stories.tsx @@ -49,9 +49,16 @@ export const Preview: Story = { }, }; +export const WithLabel: Story = { + args: { + ...Default.args, + label: 'Font Family', + }, +}; + export const WithDescription: Story = { args: { - ...Preview.args, + ...WithLabel.args, description: 'Choose a font family for your document', }, }; @@ -71,14 +78,14 @@ export const Sizes: Story = { export const Required: Story = { args: { ...Preview.args, - required: true, + isRequired: true, }, }; export const Disabled: Story = { args: { ...Preview.args, - disabled: true, + isDisabled: true, }, }; @@ -102,15 +109,15 @@ export const NoOptions: Story = { export const WithValue: Story = { args: { ...Preview.args, - value: 'mono', - defaultValue: 'serif', + selectedKey: 'mono', + defaultSelectedKey: 'serif', }, }; export const WithDefaultValue: Story = { args: { ...Preview.args, - defaultValue: 'serif', + defaultSelectedKey: 'serif', options: fontOptions, name: 'font', }, @@ -246,12 +253,12 @@ export const WithManyOptions: Story = { }, }; -export const WithErrorAndDescription: Story = { - args: { - ...Preview.args, - error: 'Invalid font family', - }, -}; +// export const WithErrorAndDescription: Story = { +// args: { +// ...Preview.args, +// error: 'Invalid font family', +// }, +// }; export const WithLongOptionNames: Story = { args: { @@ -286,6 +293,6 @@ export const WithLongOptionNames: Story = { placeholder: 'Select a document template', name: 'template', style: { maxWidth: 400 }, - value: 'annual-report-2024', + selectedKey: 'annual-report-2024', }, }; diff --git a/packages/canon/src/components/Select/Select.styles.css b/packages/canon/src/components/Select/Select.styles.css index 09730644be..80f348e5ae 100644 --- a/packages/canon/src/components/Select/Select.styles.css +++ b/packages/canon/src/components/Select/Select.styles.css @@ -14,39 +14,8 @@ * limitations under the License. */ -.bui-Select { - display: flex; - flex-direction: column; - font-family: var(--bui-font-regular); - width: 100%; -} - -.bui-SelectLabel { - font-size: var(--bui-font-size-2); - font-weight: var(--bui-font-weight-regular); - color: var(--bui-fg-primary); - margin-bottom: var(--bui-space-1_5); - cursor: pointer; -} -.bui-SelectLabel[data-disabled] { - cursor: default; -} - -.bui-SelectDescription { - font-size: var(--bui-font-size-2); - font-weight: var(--bui-font-weight-regular); - color: var(--bui-fg-secondary); - margin: 0; - padding-top: var(--bui-space-1_5); -} - -.bui-SelectError { - font-size: var(--bui-font-size-2); - font-weight: var(--bui-font-weight-regular); - color: var(--bui-fg-danger); - margin: 0; - padding-top: var(--bui-space-1_5); -} +/* .bui-Select { +} */ .bui-SelectTrigger { box-sizing: border-box; diff --git a/packages/canon/src/components/Select/Select.tsx b/packages/canon/src/components/Select/Select.tsx index 07545bb3dd..9d2453970c 100644 --- a/packages/canon/src/components/Select/Select.tsx +++ b/packages/canon/src/components/Select/Select.tsx @@ -14,13 +14,20 @@ * limitations under the License. */ -import { forwardRef, useCallback, useId, useRef, MouseEvent } from 'react'; -import { Select as SelectPrimitive } from '@base-ui-components/react/select'; -import { Icon } from '../Icon'; +import { forwardRef, useEffect } from 'react'; +import { + Select as AriaSelect, + SelectValue, + Button, + Popover, + ListBox, + ListBoxItem, +} from 'react-aria-components'; import clsx from 'clsx'; import './Select.styles.css'; import { SelectProps } from './types'; import { useStyles } from '../../hooks/useStyles'; +import { FieldLabel } from '../FieldLabel'; /** @public */ export const Select = forwardRef((props, ref) => { @@ -31,102 +38,57 @@ export const Select = forwardRef((props, ref) => { options, placeholder = 'Select an option', size = 'medium', - required, - error, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, + isRequired, + secondaryLabel, style, ...rest } = props; + const { classNames: popoverClassNames } = useStyles('Popover'); + const { classNames: listClassNames } = useStyles('List'); const { classNames, dataAttributes } = useStyles('Select', { size, }); - // Generate unique IDs for accessibility - const selectId = useId(); - const descriptionId = useId(); - const errorId = useId(); + useEffect(() => { + if (!label && !ariaLabel && !ariaLabelledBy) { + console.warn( + 'TextField requires either a visible label, aria-label, or aria-labelledby for accessibility', + ); + } + }, [label, ariaLabel, ariaLabelledBy]); - const triggerRef = useRef(null); - - const handleLabelClick = useCallback( - (e: MouseEvent) => { - if (!props.disabled && triggerRef.current) { - e.preventDefault(); - triggerRef.current.focus(); - } - }, - [props.disabled], - ); + // If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required. + const secondaryLabelText = secondaryLabel || (isRequired ? 'Required' : null); return ( -
- {label && ( - - )} - - - - - - - - - - - - {options?.map(option => ( - - - - - - {option.label} - - - ))} - - - - - {description && ( -

- {description} -

- )} - {error && ( - - )} -
+ + + + + + {options?.map(option => ( + + {option.label} + + ))} + + + ); }); diff --git a/packages/canon/src/components/Select/types.ts b/packages/canon/src/components/Select/types.ts index 6a5e2f34c3..00cd56e1ea 100644 --- a/packages/canon/src/components/Select/types.ts +++ b/packages/canon/src/components/Select/types.ts @@ -15,14 +15,23 @@ */ import { Breakpoint } from '../../'; -import { ChangeEvent, FocusEvent } from 'react'; +import { ReactNode } from 'react'; +import type { SelectProps as AriaSelectProps } from 'react-aria-components'; +import type { FieldLabelProps } from '../FieldLabel/types'; + +interface SelectOption { + name: string; + value: string; +} /** @public */ -export interface SelectProps { +export interface SelectProps + extends AriaSelectProps, + Omit { /** - * The class name of the select field + * An icon to render before the input */ - className?: string; + icon?: ReactNode; /** * The size of the select field @@ -30,81 +39,8 @@ export interface SelectProps { */ size?: 'small' | 'medium' | Partial>; - /** - * The label of the select field - */ - label?: string; - - /** - * The description of the select field - */ - description?: string; - - /** - * The name of the select field - */ - name: string; - - /** - * Whether the select field should ignore user input - * @defaultValue false - */ - disabled?: boolean; - - /** - * Whether the select field is required - * @defaultValue false - */ - required?: boolean; - /** * The options of the select field */ options?: Array<{ value: string; label: string; disabled?: boolean }>; - - /** - * The current value of the select field - */ - value?: string; - - /** - * The default value of the select field, if nothing has been selected yet - */ - defaultValue?: string; - - /** - * A placeholder text to show if nothing has been selected and there's no default value - * @defaultValue 'Select an option' - */ - placeholder?: string; - - /** - * Callback that is called when the value of the select field changes - */ - onValueChange?: (value: string) => void; - - /** - * Callback that is called when the select field is opened or closed - */ - onOpenChange?: (open: boolean) => void; - - /** - * 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/css/components.css b/packages/canon/src/css/components.css index 9cde5a56e6..2039735585 100644 --- a/packages/canon/src/css/components.css +++ b/packages/canon/src/css/components.css @@ -14,6 +14,11 @@ * limitations under the License. */ +/* Generic component styles */ +@import './generic/popover.css'; +@import './generic/list.css'; + +/* Component styles */ @import '../components/Avatar/Avatar.styles.css'; @import '../components/Box/styles.css'; @import '../components/Button/styles.css'; diff --git a/packages/canon/src/css/generic/list.css b/packages/canon/src/css/generic/list.css new file mode 100644 index 0000000000..5fa7875fa6 --- /dev/null +++ b/packages/canon/src/css/generic/list.css @@ -0,0 +1,23 @@ +/* + * 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. + */ + +.bui-List { + background-color: green; +} + +.bui-ListRow { + padding: var(--bui-space-2); +} diff --git a/packages/canon/src/css/generic/popover.css b/packages/canon/src/css/generic/popover.css new file mode 100644 index 0000000000..01953fbcc9 --- /dev/null +++ b/packages/canon/src/css/generic/popover.css @@ -0,0 +1,22 @@ +/* + * 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. + */ + +.bui-Popover { + background-color: var(--bui-bg-surface-1); + padding: var(--bui-space-2); + border-radius: var(--bui-radius-2); + border: 1px solid var(--bui-border); +} diff --git a/packages/canon/src/utils/componentDefinitions.ts b/packages/canon/src/utils/componentDefinitions.ts index ecba9646f0..c0371d28de 100644 --- a/packages/canon/src/utils/componentDefinitions.ts +++ b/packages/canon/src/utils/componentDefinitions.ts @@ -128,6 +128,12 @@ export const componentDefinitions = { weight: ['regular', 'bold'] as const, }, }, + List: { + classNames: { + root: 'bui-List', + row: 'bui-ListRow', + }, + }, Menu: { classNames: { trigger: 'bui-MenuTrigger', @@ -147,6 +153,11 @@ export const componentDefinitions = { separator: 'bui-MenuSeparator', }, }, + Popover: { + classNames: { + root: 'bui-Popover', + }, + }, RadioGroup: { classNames: { root: 'bui-RadioGroup', @@ -171,7 +182,6 @@ export const componentDefinitions = { Select: { classNames: { root: 'bui-Select', - required: 'bui-SelectRequired', trigger: 'bui-SelectTrigger', value: 'bui-SelectValue', icon: 'bui-SelectIcon',