Improve controlled experience

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-04-04 16:54:32 +01:00
parent c2a8800a68
commit d824f31baf
6 changed files with 193 additions and 71 deletions
+29 -22
View File
@@ -697,21 +697,21 @@
width: 100%;
}
.canon-SelectFieldRoot {
.canon-Select {
font-family: var(--canon-font-regular);
flex-direction: column;
width: 100%;
display: flex;
}
.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);
@@ -719,7 +719,7 @@
margin: 0;
}
.canon-SelectFieldError {
.canon-Select--error {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-danger);
@@ -727,7 +727,7 @@
margin: 0;
}
.canon-SelectTrigger {
.canon-Select--trigger {
box-sizing: border-box;
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
@@ -745,38 +745,38 @@
display: flex;
}
.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-SelectTrigger[data-invalid]:focus-visible {
.canon-Select--trigger[data-invalid]:hover, .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;
}
@@ -785,11 +785,11 @@
transition: transform .2s;
}
.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);
background-color: var(--canon-bg-surface-1);
@@ -804,12 +804,12 @@
box-shadow: 0 4px 12px #0003;
}
.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(.9);
}
.canon-SelectItem {
.canon-Select--item {
width: var(--anchor-width);
padding-block: var(--canon-space-2);
padding-inline: var(--canon-space-4);
@@ -827,13 +827,13 @@
position: relative;
}
.canon-SelectItem[data-highlighted] {
.canon-Select--item[data-highlighted] {
z-index: 0;
color: var(--canon-fg-primary);
position: relative;
}
.canon-SelectItem[data-highlighted]:before {
.canon-Select--item[data-highlighted]:before {
content: "";
z-index: -1;
background-color: var(--canon-bg-tint-hover);
@@ -843,19 +843,26 @@
inset-inline: .25rem;
}
.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;
justify-content: center;
align-items: center;
display: flex;
}
.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);
}
+29 -22
View File
@@ -1,18 +1,18 @@
.canon-SelectFieldRoot {
.canon-Select {
font-family: var(--canon-font-regular);
flex-direction: column;
width: 100%;
display: flex;
}
.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);
@@ -20,7 +20,7 @@
margin: 0;
}
.canon-SelectFieldError {
.canon-Select--error {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-danger);
@@ -28,7 +28,7 @@
margin: 0;
}
.canon-SelectTrigger {
.canon-Select--trigger {
box-sizing: border-box;
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
@@ -46,38 +46,38 @@
display: flex;
}
.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-SelectTrigger[data-invalid]:focus-visible {
.canon-Select--trigger[data-invalid]:hover, .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;
}
@@ -86,11 +86,11 @@
transition: transform .2s;
}
.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);
background-color: var(--canon-bg-surface-1);
@@ -105,12 +105,12 @@
box-shadow: 0 4px 12px #0003;
}
.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(.9);
}
.canon-SelectItem {
.canon-Select--item {
width: var(--anchor-width);
padding-block: var(--canon-space-2);
padding-inline: var(--canon-space-4);
@@ -128,13 +128,13 @@
position: relative;
}
.canon-SelectItem[data-highlighted] {
.canon-Select--item[data-highlighted] {
z-index: 0;
color: var(--canon-fg-primary);
position: relative;
}
.canon-SelectItem[data-highlighted]:before {
.canon-Select--item[data-highlighted]:before {
content: "";
z-index: -1;
background-color: var(--canon-bg-tint-hover);
@@ -144,19 +144,26 @@
inset-inline: .25rem;
}
.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;
justify-content: center;
align-items: center;
display: flex;
}
.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);
}
+29 -22
View File
@@ -9903,21 +9903,21 @@
width: 100%;
}
.canon-SelectFieldRoot {
.canon-Select {
font-family: var(--canon-font-regular);
flex-direction: column;
width: 100%;
display: flex;
}
.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);
@@ -9925,7 +9925,7 @@
margin: 0;
}
.canon-SelectFieldError {
.canon-Select--error {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-danger);
@@ -9933,7 +9933,7 @@
margin: 0;
}
.canon-SelectTrigger {
.canon-Select--trigger {
box-sizing: border-box;
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
@@ -9951,38 +9951,38 @@
display: flex;
}
.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-SelectTrigger[data-invalid]:focus-visible {
.canon-Select--trigger[data-invalid]:hover, .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;
}
@@ -9991,11 +9991,11 @@
transition: transform .2s;
}
.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);
background-color: var(--canon-bg-surface-1);
@@ -10010,12 +10010,12 @@
box-shadow: 0 4px 12px #0003;
}
.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(.9);
}
.canon-SelectItem {
.canon-Select--item {
width: var(--anchor-width);
padding-block: var(--canon-space-2);
padding-inline: var(--canon-space-4);
@@ -10033,13 +10033,13 @@
position: relative;
}
.canon-SelectItem[data-highlighted] {
.canon-Select--item[data-highlighted] {
z-index: 0;
color: var(--canon-fg-primary);
position: relative;
}
.canon-SelectItem[data-highlighted]:before {
.canon-Select--item[data-highlighted]:before {
content: "";
z-index: -1;
background-color: var(--canon-bg-tint-hover);
@@ -10049,19 +10049,26 @@
inset-inline: .25rem;
}
.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;
justify-content: center;
align-items: center;
display: flex;
}
.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);
}
+6 -1
View File
@@ -4,8 +4,10 @@
```ts
import { Breakpoint as Breakpoint_2 } from '@backstage/canon';
import { ChangeEvent } from 'react';
import { Context } from 'react';
import type { CSSProperties } from 'react';
import { FocusEvent as FocusEvent_2 } from 'react';
import { ForwardRefExoticComponent } from 'react';
import { Menu as Menu_2 } from '@base-ui-components/react/menu';
import { default as React_2 } from 'react';
@@ -915,7 +917,7 @@ export const ScrollArea: {
// @public (undocumented)
export const Select: React_2.ForwardRefExoticComponent<
SelectProps & React_2.RefAttributes<HTMLSelectElement>
SelectProps & React_2.RefAttributes<HTMLDivElement>
>;
// @public (undocumented)
@@ -924,8 +926,11 @@ export interface SelectProps {
defaultValue?: string;
description?: string;
disabled?: boolean;
error?: string;
label?: string;
name: string;
onBlur?: (event: FocusEvent_2<HTMLSelectElement>) => void;
onChange?: (event: ChangeEvent<HTMLSelectElement>) => void;
onOpenChange?: (open: boolean) => void;
onValueChange?: (value: string) => void;
options?: Array<{
@@ -13,11 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useState } from 'react';
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Select } from './Select';
import { Form } from '@base-ui-components/react/form';
import { Button } from '../Button';
import { Flex } from '../Flex';
const meta = {
+99 -1
View File
@@ -34,7 +34,7 @@ type Inputs = {
city: string;
};
export const Default: Story = {
export const Uncontrolled: Story = {
render: () => {
const {
register,
@@ -96,3 +96,101 @@ export const Default: Story = {
);
},
};
export const Controlled: Story = {
render: () => {
const {
register,
handleSubmit,
control,
formState: { errors },
} = useForm<Inputs>();
const [firstname, setFirstname] = React.useState('John');
const [lastname, setLastname] = React.useState('Doe');
const [city, setCity] = React.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>
);
},
};