mirror of
https://github.com/enso-org/enso.git
synced 2024-12-19 15:12:26 +03:00
Fix Visuals of The Selector Component (#11628)
This PR fixes a bug when Selector component has wrong visuals when selected Also, this PR fixes visual inconsistencies and bugs in this component.
This commit is contained in:
parent
7c5e69219c
commit
8891051475
@ -21,12 +21,30 @@ const sharedConfig: Partial<ReactStorybookConfig> = {
|
||||
env: { FRAMEWORK: framework },
|
||||
|
||||
previewHead: (head) => {
|
||||
return `
|
||||
return /*html*/ `
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=M+PLUS+1:wght@300;400;500;600;700&display=swap"
|
||||
rel="preload"
|
||||
as="style"
|
||||
crossorigin
|
||||
/>
|
||||
|
||||
<script>
|
||||
window.global = window;
|
||||
|
||||
// Pass environment variables to the storybook
|
||||
window.ENV = {
|
||||
// The framework used to render the story
|
||||
// Used by the preview to determine which framework to use
|
||||
FRAMEWORK: '${framework}',
|
||||
}
|
||||
|
||||
// Allow React DevTools to work in Storybook
|
||||
if (window.parent !== window) {
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__
|
||||
}
|
||||
</script>
|
||||
${head}
|
||||
`
|
||||
|
@ -9,6 +9,8 @@ import { useLayoutEffect, useState } from 'react'
|
||||
import invariant from 'tiny-invariant'
|
||||
import UIProviders from '../src/dashboard/components/UIProviders'
|
||||
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { createQueryClient } from 'enso-common/src/queryClient'
|
||||
import { MotionGlobalConfig } from 'framer-motion'
|
||||
import z from 'zod'
|
||||
import '../src/dashboard/tailwind.css'
|
||||
@ -68,6 +70,11 @@ const reactPreview: ReactPreview = {
|
||||
<div id="enso-portal-root" className="enso-portal-root" />
|
||||
</>
|
||||
),
|
||||
|
||||
(Story, context) => {
|
||||
const [queryClient] = useState(() => createQueryClient())
|
||||
return <QueryClientProvider client={queryClient}>{Story(context)}</QueryClientProvider>
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
@ -114,7 +114,7 @@ AnimatedBackground.Item = memo(function AnimatedBackgroundItem(props: AnimatedBa
|
||||
const isActive = isSelected ?? activeValue === value
|
||||
|
||||
return (
|
||||
<div className={twJoin('relative', className)}>
|
||||
<div className={twJoin('relative *:isolate', className)}>
|
||||
<AnimatedBackgroundItemUnderlay
|
||||
isActive={isActive}
|
||||
underlayElement={underlayElement}
|
||||
@ -122,7 +122,7 @@ AnimatedBackground.Item = memo(function AnimatedBackgroundItem(props: AnimatedBa
|
||||
transition={transition}
|
||||
/>
|
||||
|
||||
<div className="isolate contents">{children}</div>
|
||||
<div className="isolate contents *:isolate">{children}</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
@ -0,0 +1,37 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { userEvent, within } from '@storybook/test'
|
||||
import { z } from 'zod'
|
||||
import { Form } from '../../Form'
|
||||
import type { MultiSelectorProps } from './MultiSelector.tsx'
|
||||
import { MultiSelector } from './MultiSelector.tsx'
|
||||
|
||||
type Props = MultiSelectorProps<typeof schema, 'value'>
|
||||
type Story = StoryObj<Props>
|
||||
|
||||
const schema = z.object({ value: z.array(z.enum(['one', 'two', 'three'])) })
|
||||
|
||||
export default {
|
||||
title: 'Components/AriaComponents/Inputs/MultiSelector',
|
||||
component: MultiSelector,
|
||||
render: (args) => <MultiSelector {...args} />,
|
||||
tags: ['autodocs'],
|
||||
decorators: [(Story, context) => <Form schema={schema}>{Story(context)}</Form>],
|
||||
args: { name: 'value', items: ['one', 'two', 'three'] },
|
||||
} as Meta<Props>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const TwoColumns: Story = { args: { columns: 2 } }
|
||||
|
||||
export const SelectedItems: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
await userEvent.click(canvas.getByText('one'))
|
||||
|
||||
await userEvent.click(canvas.getByText('two'))
|
||||
|
||||
await userEvent.click(canvas.getByText('three'))
|
||||
|
||||
await userEvent.click(canvas.getByText('one'))
|
||||
},
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Form } from '#/components/AriaComponents'
|
||||
import { userEvent, within } from '@storybook/test'
|
||||
import type { SelectorProps } from './Selector'
|
||||
import { Selector } from './Selector'
|
||||
|
||||
// Schema for our form
|
||||
const schema = z.object({
|
||||
plan: z.enum(['basic', 'pro', 'enterprise']),
|
||||
})
|
||||
|
||||
type Props = SelectorProps<typeof schema, 'plan'>
|
||||
|
||||
export default {
|
||||
title: 'Components/AriaComponents/Inputs/Selector',
|
||||
component: Selector,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
render: (args) => <Selector {...args} />,
|
||||
args: {
|
||||
name: 'plan',
|
||||
items: ['basic', 'pro', 'enterprise'],
|
||||
},
|
||||
decorators: [
|
||||
(Story, context) => (
|
||||
<Form schema={schema} className="w-96" defaultValues={{ plan: 'basic' }}>
|
||||
{Story(context)}
|
||||
</Form>
|
||||
),
|
||||
],
|
||||
} as Meta<Props>
|
||||
|
||||
type Story = StoryObj<Props>
|
||||
|
||||
// Basic usage
|
||||
export const Default: Story = {}
|
||||
|
||||
// Different rounded variants
|
||||
export const VisualVariants: Story = {
|
||||
render: (args) => {
|
||||
return (
|
||||
<div className="w-full space-y-12">
|
||||
<div className="w-full space-y-2">
|
||||
{(['outline'] as const).map((variant) => (
|
||||
<Selector {...args} key={variant} label={variant} variant={variant} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-2">
|
||||
{(['medium', 'small'] as const).map((size) => (
|
||||
<Selector {...args} key={size} label={size} size={size} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-2">
|
||||
{(
|
||||
['medium', 'xxxlarge', 'none', 'small', 'large', 'xlarge', 'xxlarge', 'full'] as const
|
||||
).map((rounded) => (
|
||||
<Selector {...args} key={rounded} label={rounded} rounded={rounded} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-2">
|
||||
<Selector {...args} label="Invalid" isInvalid />
|
||||
<Selector {...args} label="Required" isRequired />
|
||||
<Selector {...args} label="Disabled" isDisabled />
|
||||
<Selector {...args} label="Readonly" isReadOnly />
|
||||
<Selector {...args} label="Invalid & Disabled" isInvalid isDisabled />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Interactions: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
|
||||
await userEvent.click(canvas.getByText('enterprise'))
|
||||
},
|
||||
}
|
@ -49,6 +49,7 @@ export const SELECTOR_STYLES = tv({
|
||||
readOnly: { true: 'cursor-default' },
|
||||
size: {
|
||||
medium: { base: '' },
|
||||
small: { base: '' },
|
||||
},
|
||||
rounded: {
|
||||
none: 'rounded-none',
|
||||
@ -92,6 +93,7 @@ export const Selector = forwardRef(function Selector<
|
||||
label,
|
||||
size,
|
||||
rounded,
|
||||
variant,
|
||||
isRequired = false,
|
||||
isInvalid = false,
|
||||
fieldVariants,
|
||||
@ -108,6 +110,7 @@ export const Selector = forwardRef(function Selector<
|
||||
rounded,
|
||||
readOnly: inputProps.readOnly,
|
||||
disabled: isDisabled || formInstance.formState.isSubmitting,
|
||||
variant,
|
||||
})
|
||||
|
||||
return (
|
||||
@ -157,7 +160,14 @@ export const Selector = forwardRef(function Selector<
|
||||
>
|
||||
<AnimatedBackground value={String(items.indexOf(value))}>
|
||||
{items.map((item, i) => (
|
||||
<SelectorOption key={i} value={String(i)} label={children(item)} />
|
||||
<SelectorOption
|
||||
key={i}
|
||||
value={String(i)}
|
||||
label={children(item)}
|
||||
rounded={rounded}
|
||||
size={size}
|
||||
variant={variant}
|
||||
/>
|
||||
))}
|
||||
</AnimatedBackground>
|
||||
</RadioGroup>
|
||||
|
@ -16,7 +16,7 @@ export interface SelectorOptionProps
|
||||
}
|
||||
|
||||
export const SELECTOR_OPTION_STYLES = tv({
|
||||
base: 'flex flex-1 w-full min-h-8 cursor-pointer',
|
||||
base: 'flex flex-1 w-full cursor-pointer',
|
||||
variants: {
|
||||
rounded: {
|
||||
// specified in compoundSlots
|
||||
@ -30,69 +30,126 @@ export const SELECTOR_OPTION_STYLES = tv({
|
||||
full: '',
|
||||
},
|
||||
size: {
|
||||
medium: { radio: 'px-[9px] py-[3.5px]' },
|
||||
small: { radio: 'px-[7px] py-[1.5px]' },
|
||||
medium: { base: 'min-h-[31px]', radio: 'px-[9px] py-[3.5px]' },
|
||||
small: { base: 'min-h-6', radio: 'px-[7px] py-[1.5px]' },
|
||||
},
|
||||
variant: {
|
||||
primary: {
|
||||
isHovered: {
|
||||
true: { radio: '' },
|
||||
false: { radio: '' },
|
||||
},
|
||||
isSelected: {
|
||||
// specified in compoundVariants
|
||||
true: { radio: '' },
|
||||
false: { radio: '' },
|
||||
},
|
||||
isFocusVisible: {
|
||||
// specified in compoundVariants
|
||||
true: {
|
||||
radio:
|
||||
'overflow-clip outline outline-2 outline-transparent outline-offset-[-2px] [&:not(:selected)]:bg-primary/5 selected:text-white pressed:bg-primary/10 focus-visible:outline-primary focus-visible:outline-offset-0',
|
||||
'outline outline-2 outline-transparent outline-offset-[-6px] focus-visible:outline-primary focus-visible:outline-offset-[2px] transition-[outline-offset] duration-200',
|
||||
},
|
||||
false: { radio: '' },
|
||||
},
|
||||
|
||||
isPressed: {
|
||||
// specified in compoundVariants
|
||||
true: { radio: '' },
|
||||
false: { radio: '' },
|
||||
},
|
||||
|
||||
variant: {
|
||||
// specified in compoundVariants
|
||||
outline: {
|
||||
base: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
slots: {
|
||||
animation: 'bg-primary',
|
||||
radio: TEXT_STYLE({
|
||||
className: 'flex flex-1 w-full items-center justify-center transition-colors duration-200',
|
||||
className:
|
||||
'relative flex flex-1 w-full items-center justify-center transition-colors duration-200',
|
||||
variant: 'body',
|
||||
}),
|
||||
hover: 'absolute inset-x-0 inset-y-0 transition-colors duration-200',
|
||||
},
|
||||
compoundSlots: [
|
||||
{
|
||||
slots: ['radio', 'animation', 'base'],
|
||||
slots: ['radio', 'animation', 'base', 'hover'],
|
||||
rounded: 'none',
|
||||
class: 'rounded-none',
|
||||
},
|
||||
{
|
||||
slots: ['radio', 'animation', 'base'],
|
||||
slots: ['radio', 'animation', 'base', 'hover'],
|
||||
rounded: 'small',
|
||||
class: 'rounded-sm',
|
||||
},
|
||||
{
|
||||
slots: ['radio', 'animation', 'base'],
|
||||
slots: ['radio', 'animation', 'base', 'hover'],
|
||||
rounded: 'medium',
|
||||
class: 'rounded-md',
|
||||
},
|
||||
{
|
||||
slots: ['radio', 'animation', 'base'],
|
||||
slots: ['radio', 'animation', 'base', 'hover'],
|
||||
rounded: 'large',
|
||||
class: 'rounded-lg',
|
||||
},
|
||||
{
|
||||
slots: ['radio', 'animation', 'base'],
|
||||
slots: ['radio', 'animation', 'base', 'hover'],
|
||||
rounded: 'xlarge',
|
||||
class: 'rounded-xl',
|
||||
},
|
||||
{
|
||||
slots: ['radio', 'animation', 'base'],
|
||||
slots: ['radio', 'animation', 'base', 'hover'],
|
||||
rounded: 'xxlarge',
|
||||
class: 'rounded-2xl',
|
||||
},
|
||||
{
|
||||
slots: ['radio', 'animation', 'base'],
|
||||
slots: ['radio', 'animation', 'base', 'hover'],
|
||||
rounded: 'xxxlarge',
|
||||
class: 'rounded-3xl',
|
||||
},
|
||||
{
|
||||
slots: ['radio', 'animation', 'base'],
|
||||
slots: ['radio', 'animation', 'base', 'hover'],
|
||||
rounded: 'full',
|
||||
class: 'rounded-full',
|
||||
},
|
||||
],
|
||||
compoundVariants: [
|
||||
{
|
||||
variant: 'outline',
|
||||
isSelected: true,
|
||||
class: { radio: TEXT_STYLE({ variant: 'body', color: 'invert' }) },
|
||||
},
|
||||
{
|
||||
variant: 'outline',
|
||||
isHovered: true,
|
||||
isSelected: false,
|
||||
class: { hover: 'bg-primary/5' },
|
||||
},
|
||||
{
|
||||
variant: 'outline',
|
||||
isPressed: true,
|
||||
class: { hover: 'bg-primary/10' },
|
||||
},
|
||||
{
|
||||
variant: 'outline',
|
||||
isSelected: false,
|
||||
class: { radio: TEXT_STYLE({ variant: 'body', color: 'primary' }) },
|
||||
},
|
||||
{
|
||||
size: 'small',
|
||||
class: { hover: 'inset-[2px]' },
|
||||
},
|
||||
{
|
||||
size: 'medium',
|
||||
class: { hover: 'inset-[3px]' },
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
size: 'medium',
|
||||
rounded: 'xxxlarge',
|
||||
variant: 'primary',
|
||||
variant: 'outline',
|
||||
},
|
||||
})
|
||||
|
||||
@ -124,13 +181,19 @@ export const SelectorOption = memo(
|
||||
ref={ref}
|
||||
{...radioProps}
|
||||
value={value}
|
||||
className={(renderProps) =>
|
||||
styles.radio({
|
||||
className={(renderProps) => {
|
||||
return styles.radio({
|
||||
className: typeof className === 'function' ? className(renderProps) : className,
|
||||
...renderProps,
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{({ isHovered, isSelected, isPressed }) => (
|
||||
<>
|
||||
{label}
|
||||
<div className={styles.hover({ isHovered, isSelected, isPressed })} />
|
||||
</>
|
||||
)}
|
||||
</Radio>
|
||||
</AnimatedBackground.Item>
|
||||
)
|
||||
|
@ -565,6 +565,7 @@ export default [
|
||||
'jsdoc/require-file-overview': 'off',
|
||||
'@typescript-eslint/no-magic-numbers': 'off',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user