diff --git a/apps/storybook/.storybook/main.ts b/apps/storybook/.storybook/main.ts index 42d5c7b5b..6128d7142 100644 --- a/apps/storybook/.storybook/main.ts +++ b/apps/storybook/.storybook/main.ts @@ -8,7 +8,10 @@ const require = createRequire(import.meta.url); const config: StorybookConfig = { stories: ['../../../packages/**!(node_modules|dist)/src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: [getAbsolutePath('@storybook/addon-docs')], + addons: [ + getAbsolutePath('@storybook/addon-docs'), + getAbsolutePath('storybook-addon-pseudo-states'), + ], core: { builder: getAbsolutePath('@storybook/builder-vite'), diff --git a/apps/storybook/package.json b/apps/storybook/package.json index 119ca4ec9..ea6cc41c4 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -27,6 +27,7 @@ "eslint": "catalog:", "globals": "^16.5.0", "storybook": "^9.1.20", + "storybook-addon-pseudo-states": "9.1.20", "typescript": "catalog:", "typescript-eslint": "^8.59.4", "vite": "^7.3.5" diff --git a/packages/core/__tests__/screenshots/interaction--test-bed-1-chrome-darwin-.png b/packages/core/__tests__/screenshots/interaction--test-bed-1-chrome-darwin-.png new file mode 100644 index 000000000..710ee87a4 Binary files /dev/null and b/packages/core/__tests__/screenshots/interaction--test-bed-1-chrome-darwin-.png differ diff --git a/packages/core/__tests__/screenshots/interaction--test-bed-1-edge-darwin-.png b/packages/core/__tests__/screenshots/interaction--test-bed-1-edge-darwin-.png new file mode 100644 index 000000000..710ee87a4 Binary files /dev/null and b/packages/core/__tests__/screenshots/interaction--test-bed-1-edge-darwin-.png differ diff --git a/packages/core/__tests__/screenshots/interaction--test-bed-1-firefox-darwin-.png b/packages/core/__tests__/screenshots/interaction--test-bed-1-firefox-darwin-.png new file mode 100644 index 000000000..b8878649b Binary files /dev/null and b/packages/core/__tests__/screenshots/interaction--test-bed-1-firefox-darwin-.png differ diff --git a/packages/core/__tests__/screenshots/interaction--test-bed-1-safari-darwin-.png b/packages/core/__tests__/screenshots/interaction--test-bed-1-safari-darwin-.png new file mode 100644 index 000000000..6c96cc1d1 Binary files /dev/null and b/packages/core/__tests__/screenshots/interaction--test-bed-1-safari-darwin-.png differ diff --git a/packages/core/src/components/button/button.css.ts b/packages/core/src/components/button/button.css.ts index d366ecb3b..0fb4ece1e 100644 --- a/packages/core/src/components/button/button.css.ts +++ b/packages/core/src/components/button/button.css.ts @@ -1,7 +1,6 @@ import { createVar } from '@vanilla-extract/css'; import type { RecipeVariants } from '@vanilla-extract/recipes'; -import { interaction } from '~/styles/mixins/interactions.css'; import { componentRecipe } from '~/styles/mixins/layer-style.css'; import { when } from '~/styles/mixins/logical-states'; import { typography } from '~/styles/mixins/typography.css'; @@ -17,8 +16,6 @@ const variables = { export const root = componentRecipe({ base: [ - interaction(), - { display: 'inline-flex', alignItems: 'center', diff --git a/packages/core/src/components/button/button.tsx b/packages/core/src/components/button/button.tsx index ad9bdcc0b..fd0db2dd0 100644 --- a/packages/core/src/components/button/button.tsx +++ b/packages/core/src/components/button/button.tsx @@ -7,6 +7,7 @@ import { createSplitProps } from '~/utils/create-split-props'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VaporUIComponentProps } from '~/utils/types'; +import { Interaction } from '../interaction'; import type { ButtonVariants } from './button.css'; import * as styles from './button.css'; @@ -22,11 +23,13 @@ export const Button = forwardRef((props, ref) => { ]); return ( - + + + ); }); Button.displayName = 'Button'; diff --git a/packages/core/src/components/checkbox/checkbox.css.ts b/packages/core/src/components/checkbox/checkbox.css.ts index 751db2cee..018c5f433 100644 --- a/packages/core/src/components/checkbox/checkbox.css.ts +++ b/packages/core/src/components/checkbox/checkbox.css.ts @@ -1,7 +1,6 @@ import { createVar } from '@vanilla-extract/css'; import type { RecipeVariants } from '@vanilla-extract/recipes'; -import { interaction } from '~/styles/mixins/interactions.css'; import { componentRecipe, componentStyle } from '~/styles/mixins/layer-style.css'; import { when } from '~/styles/mixins/logical-states'; import { vars } from '~/styles/themes.css'; @@ -10,7 +9,6 @@ const boxShadowColor = createVar('box-shadow-color'); export const root = componentRecipe({ base: [ - interaction(), { position: 'relative', diff --git a/packages/core/src/components/checkbox/checkbox.tsx b/packages/core/src/components/checkbox/checkbox.tsx index b1b5e6c9a..b67fe26bf 100644 --- a/packages/core/src/components/checkbox/checkbox.tsx +++ b/packages/core/src/components/checkbox/checkbox.tsx @@ -14,6 +14,7 @@ import { createDataAttributes } from '~/utils/data-attributes'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VaporUIComponentProps } from '~/utils/types'; +import { Interaction } from '../interaction'; import type { RootVariants } from './checkbox.css'; import * as styles from './checkbox.css'; @@ -58,7 +59,11 @@ export const CheckboxRoot = forwardRef((props, }, }); - return {root}; + return ( + + {root} + + ); }); CheckboxRoot.displayName = 'Checkbox.Root'; diff --git a/packages/core/src/components/interaction/index.ts b/packages/core/src/components/interaction/index.ts new file mode 100644 index 000000000..601a8e0a3 --- /dev/null +++ b/packages/core/src/components/interaction/index.ts @@ -0,0 +1 @@ +export * from './interaction'; diff --git a/packages/core/src/components/interaction/interaction.css.ts b/packages/core/src/components/interaction/interaction.css.ts new file mode 100644 index 000000000..5ced6d7a6 --- /dev/null +++ b/packages/core/src/components/interaction/interaction.css.ts @@ -0,0 +1,94 @@ +import { createGlobalVar } from '@vanilla-extract/css'; +import { calc } from '@vanilla-extract/css-utils'; +import type { RecipeVariants } from '@vanilla-extract/recipes'; + +import { componentRecipe, componentStyle } from '~/styles/mixins/layer-style.css'; +import { vars } from '~/styles/themes.css'; + +const variables = { + ratio: createGlobalVar('ratio'), + opacity: createGlobalVar('overlay-opacity'), +}; + +export const overlay = componentStyle({ + position: 'absolute', + top: 0, + left: 0, + transition: 'opacity 150ms ease', + opacity: variables.opacity, + border: 'none', + borderRadius: 'inherit', + backgroundColor: vars.color.gray[900], + pointerEvents: 'none', + width: '100%', + height: '100%', +}); + +export const root = componentRecipe({ + base: { + position: 'relative', + selectors: { + '&:focus': { outline: 'none' }, + '&:focus-visible': { outline: 'none' }, + }, + vars: { + [variables.ratio]: '0.08', + [variables.opacity]: '0', + }, + }, + defaultVariants: { scale: 'normal', type: 'default' }, + variants: { + scale: { + normal: {}, + light: { vars: { [variables.ratio]: '0.04' } }, + }, + type: { + default: { + selectors: { + '&:active': { + vars: { [variables.opacity]: calc.multiply(variables.ratio, 2) }, + }, + '&:focus-visible': { + outline: `2px solid ${vars.color.foreground.normal[200]}`, + outlineOffset: '2px', + }, + }, + '@media': { + '(hover: hover)': { + selectors: { + '&:hover': { vars: { [variables.opacity]: variables.ratio } }, + '&:active': { + vars: { [variables.opacity]: calc.multiply(variables.ratio, 2) }, + }, + }, + }, + }, + }, + form: { + transition: 'box-shadow 150ms cubic-bezier(.4,0,.2,1)', + selectors: { + '&:focus': { + boxShadow: `inset 0 0 0 0.0625rem ${vars.color.border.primary}`, + }, + }, + '@media': { + '(hover: hover)': { + selectors: { + '&:hover:not(:focus)': { + boxShadow: `inset 0 0 0 0.0625rem color-mix(in srgb, ${vars.color.gray[900]} 32%, transparent)`, + }, + }, + }, + }, + }, + roving: { + selectors: { + '&[data-highlighted]': { vars: { [variables.opacity]: '0.08' } }, + '&[data-highlighted]:active': { vars: { [variables.opacity]: '0.16' } }, + }, + }, + }, + }, +}); + +export type InteractionVariants = NonNullable>; diff --git a/packages/core/src/components/interaction/interaction.stories.tsx b/packages/core/src/components/interaction/interaction.stories.tsx new file mode 100644 index 000000000..6f0cfe4a8 --- /dev/null +++ b/packages/core/src/components/interaction/interaction.stories.tsx @@ -0,0 +1,95 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { Box } from '../box'; +import { VStack } from '../v-stack'; +import { Interaction } from './interaction'; + +export default { + title: 'Interaction', + component: Interaction, + argTypes: { + scale: { control: 'inline-radio', options: ['normal', 'light'] }, + type: { control: 'inline-radio', options: ['default', 'form', 'roving'] }, + }, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ( + + + Interactive button + + + ), +}; + +export const TestBed: Story = { + parameters: { + pseudo: { + hover: ['#default-hover', '#light-hover', '#form-hover'], + focus: '#form-focus', + focusVisible: '#default-focus-visible', + active: ['#default-active', '#roving-active'], + }, + }, + render: (args) => { + const boxCss = { display: 'inline-block', padding: '16px', border: '1px solid' } as const; + return ( + + + default / rest + + + + default / hover + + + + + default / focus-visible + + + + + default / active + + + + + light / rest + + + + light / hover + + + + + + + + + + + + + + + roving / rest + + + + roving / highlighted + + + + + roving / highlighted + active + + + + ); + }, +}; diff --git a/packages/core/src/components/interaction/interaction.test.tsx b/packages/core/src/components/interaction/interaction.test.tsx new file mode 100644 index 000000000..53201bdb0 --- /dev/null +++ b/packages/core/src/components/interaction/interaction.test.tsx @@ -0,0 +1,70 @@ +import { createRef } from 'react'; + +import { render } from '@testing-library/react'; +import { axe } from 'vitest-axe'; + +import { Interaction } from './interaction'; + +describe('Interaction', () => { + it('should have no a11y violations', async () => { + const rendered = render( + + + , + ); + const result = await axe(rendered.container); + + expect(result).toHaveNoViolations(); + }); + + it('should forward refs correctly', () => { + const ref = createRef(); + render( + + + , + ); + + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); + + it('should apply custom class names', () => { + const customClassName = 'custom-interaction'; + const rendered = render( + + + , + ); + const interactionElement = rendered.queryByTestId('interaction'); + + expect(interactionElement).toHaveClass(customClassName); + }); + + it('should render children correctly', () => { + const rendered = render( + + + , + ); + const childElement = rendered.getByText('Child Element'); + const interactionElement = rendered.queryByTestId('interaction'); + + expect(childElement).toBeInTheDocument(); + expect(interactionElement).toBeInTheDocument(); + }); + + describe('prop: type', () => { + it('should not render children when type is form', () => { + const rendered = render( + + + , + ); + + const interactionElement = rendered.queryByTestId('interaction'); + + expect(interactionElement).toBeNull(); + expect(interactionElement).not.toBeInTheDocument(); + }); + }); +}); diff --git a/packages/core/src/components/interaction/interaction.tsx b/packages/core/src/components/interaction/interaction.tsx new file mode 100644 index 000000000..9fbdc16ab --- /dev/null +++ b/packages/core/src/components/interaction/interaction.tsx @@ -0,0 +1,67 @@ +import { Children, Fragment, cloneElement, isValidElement } from 'react'; + +import { useRenderElement } from '~/hooks/use-render-element'; +import { cn } from '~/utils/cn'; +import { createSplitProps } from '~/utils/create-split-props'; +import { resolveStyles } from '~/utils/resolve-styles'; +import type { VaporUIComponentProps } from '~/utils/types'; + +import * as styles from './interaction.css'; +import type { InteractionVariants } from './interaction.css'; + +/* ------------------------------------------------------------------------------------------------- + * Interaction + * -----------------------------------------------------------------------------------------------*/ + +export const Interaction = (props: Interaction.Props) => { + const { + className: classNameProp, + children: childrenProp, + ...componentProps + } = resolveStyles(props); + + const [variantProps, otherProps] = createSplitProps()(componentProps, [ + 'scale', + 'type', + ]); + + const child = Children.only(childrenProp); + + if (!isValidElement(child)) { + throw new Error(' child must be a single React element'); + } + + const element = useRenderElement({ + defaultTagName: 'div', + state: { interaction: true }, + props: { + role: 'presentation', + 'aria-hidden': 'true', + className: cn(styles.overlay, classNameProp), + ...otherProps, + }, + }); + + const { type } = variantProps; + const { className, children } = child.props; + + return cloneElement(child, { + className: cn(styles.root(variantProps), className), + children: + type === 'form' ? ( + children + ) : ( + + {element} + {children} + + ), + }); +}; + +/* -----------------------------------------------------------------------------------------------*/ + +export namespace Interaction { + export type State = {}; + export type Props = VaporUIComponentProps<'div', State> & InteractionVariants; +} diff --git a/packages/core/src/components/menu/menu.css.ts b/packages/core/src/components/menu/menu.css.ts index c1f35476b..2d098722b 100644 --- a/packages/core/src/components/menu/menu.css.ts +++ b/packages/core/src/components/menu/menu.css.ts @@ -1,5 +1,4 @@ import { foregrounds } from '~/styles/mixins/foreground.css'; -import { interaction } from '~/styles/mixins/interactions.css'; import { componentStyle } from '~/styles/mixins/layer-style.css'; import { when } from '~/styles/mixins/logical-states'; import { typography } from '~/styles/mixins/typography.css'; @@ -39,7 +38,6 @@ export const popup = componentStyle({ export const subPopup = popup; export const item = componentStyle([ - interaction({ type: 'roving' }), typography({ style: 'body2' }), foregrounds({ color: 'normal-200' }), diff --git a/packages/core/src/components/menu/menu.tsx b/packages/core/src/components/menu/menu.tsx index b8d46979d..4209ea7aa 100644 --- a/packages/core/src/components/menu/menu.tsx +++ b/packages/core/src/components/menu/menu.tsx @@ -14,6 +14,7 @@ import { createRender } from '~/utils/create-renderer'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VaporUIComponentProps } from '~/utils/types'; +import { Interaction } from '../interaction'; import * as styles from './menu.css'; type MenuContext = Pick; @@ -138,12 +139,14 @@ export const MenuItem = forwardRef((props, ref) => const disabled = disabledProp || contextDisabled; return ( - + + + ); }); MenuItem.displayName = 'Menu.Item'; @@ -237,15 +240,17 @@ export const MenuSubmenuTriggerItem = forwardRef - {children} - - - + + + {children} + + + + ); }, ); @@ -318,14 +323,16 @@ export const MenuCheckboxItemPrimitive = forwardRef - {children} - + + + {children} + + ); }, ); @@ -399,12 +406,14 @@ export const MenuRadioItemPrimitive = forwardRef + + + ); }, ); diff --git a/packages/core/src/components/multi-select/multi-select.css.ts b/packages/core/src/components/multi-select/multi-select.css.ts index 542db44b1..813603f3a 100644 --- a/packages/core/src/components/multi-select/multi-select.css.ts +++ b/packages/core/src/components/multi-select/multi-select.css.ts @@ -2,7 +2,6 @@ import { createVar } from '@vanilla-extract/css'; import type { RecipeVariants } from '@vanilla-extract/recipes'; import { foregrounds } from '~/styles/mixins/foreground.css'; -import { interaction } from '~/styles/mixins/interactions.css'; import { componentRecipe, componentStyle } from '~/styles/mixins/layer-style.css'; import { when } from '~/styles/mixins/logical-states'; import { typography } from '~/styles/mixins/typography.css'; @@ -12,7 +11,6 @@ const boxShadowColor = createVar('box-shadow-color'); export const trigger = componentRecipe({ base: [ - interaction(), { display: 'flex', alignItems: 'center', @@ -192,7 +190,6 @@ export const popup = componentStyle({ }); export const item = componentStyle([ - interaction({ type: 'roving' }), typography({ style: 'body2' }), { display: 'flex', diff --git a/packages/core/src/components/multi-select/multi-select.tsx b/packages/core/src/components/multi-select/multi-select.tsx index ba645ee6e..0fcaea859 100644 --- a/packages/core/src/components/multi-select/multi-select.tsx +++ b/packages/core/src/components/multi-select/multi-select.tsx @@ -16,6 +16,7 @@ import { resolveStyles } from '~/utils/resolve-styles'; import type { VaporUIComponentProps } from '~/utils/types'; import { Badge } from '../badge'; +import { Interaction } from '../interaction'; import type { TriggerVariants } from './multi-select.css'; import * as styles from './multi-select.css'; @@ -73,16 +74,18 @@ export const MultiSelectTriggerPrimitive = forwardRef< const dataAttrs = createDataAttributes({ required, invalid }); return ( - + + + ); }); MultiSelectTriggerPrimitive.displayName = 'MultiSelect.TriggerPrimitive'; @@ -316,7 +319,13 @@ export const MultiSelectItemPrimitive = forwardRef + + + ); }, ); diff --git a/packages/core/src/components/navigation-menu/navigation-menu.css.ts b/packages/core/src/components/navigation-menu/navigation-menu.css.ts index 1758f77cf..7ab881be7 100644 --- a/packages/core/src/components/navigation-menu/navigation-menu.css.ts +++ b/packages/core/src/components/navigation-menu/navigation-menu.css.ts @@ -1,7 +1,6 @@ import { createVar } from '@vanilla-extract/css'; import type { RecipeVariants } from '@vanilla-extract/recipes'; -import { interaction } from '~/styles/mixins/interactions.css'; import { componentRecipe, componentStyle } from '~/styles/mixins/layer-style.css'; import { when } from '~/styles/mixins/logical-states'; import { vars } from '~/styles/themes.css'; @@ -25,8 +24,6 @@ export const list = componentRecipe({ export const link = componentRecipe({ base: [ - interaction(), - { display: 'flex', alignItems: 'center', diff --git a/packages/core/src/components/navigation-menu/navigation-menu.tsx b/packages/core/src/components/navigation-menu/navigation-menu.tsx index 2ff5cb83e..07b5030ae 100644 --- a/packages/core/src/components/navigation-menu/navigation-menu.tsx +++ b/packages/core/src/components/navigation-menu/navigation-menu.tsx @@ -18,6 +18,7 @@ import { createDataAttributes } from '~/utils/data-attributes'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VaporUIComponentProps } from '~/utils/types'; +import { Interaction } from '../interaction'; import type { LinkVariants, ListVariants } from './navigation-menu.css'; import * as styles from './navigation-menu.css'; @@ -124,16 +125,18 @@ export const NavigationMenuLink = forwardRef + + + ); }, ); @@ -154,13 +157,15 @@ export const NavigationMenuTriggerPrimitive = forwardRef< const dataAttrs = createDataAttributes({ disabled }); return ( - + + + ); }); NavigationMenuTriggerPrimitive.displayName = 'NavigationMenu.TriggerPrimitive'; diff --git a/packages/core/src/components/pagination/pagination.css.ts b/packages/core/src/components/pagination/pagination.css.ts index 478c6caba..3ce5ce991 100644 --- a/packages/core/src/components/pagination/pagination.css.ts +++ b/packages/core/src/components/pagination/pagination.css.ts @@ -1,6 +1,5 @@ import type { RecipeVariants } from '@vanilla-extract/recipes'; -import { interaction } from '~/styles/mixins/interactions.css'; import { componentRecipe, componentStyle } from '~/styles/mixins/layer-style.css'; import { when } from '~/styles/mixins/logical-states'; import { typography } from '~/styles/mixins/typography.css'; @@ -87,7 +86,7 @@ export const currentPage = componentRecipe({ export const button = componentRecipe({ base: [ baseElement, - interaction(), + { selectors: { '&[data-current]': { diff --git a/packages/core/src/components/pagination/pagination.tsx b/packages/core/src/components/pagination/pagination.tsx index d60962eed..7a19f9ad2 100644 --- a/packages/core/src/components/pagination/pagination.tsx +++ b/packages/core/src/components/pagination/pagination.tsx @@ -21,6 +21,7 @@ import { createSplitProps } from '~/utils/create-split-props'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VaporUIComponentProps } from '~/utils/types'; +import { Interaction } from '../interaction'; import * as styles from './pagination.css'; import type { ButtonVariants } from './pagination.css'; @@ -184,7 +185,7 @@ export const PaginationButtonPrimitive = forwardRef< [disabled, current], ); - return useRenderElement({ + const element = useRenderElement({ ref, render, defaultTagName: 'button', @@ -198,6 +199,8 @@ export const PaginationButtonPrimitive = forwardRef< ...props, }, }); + + return {element}; }); PaginationButtonPrimitive.displayName = 'Pagination.ButtonPrimitive'; @@ -236,7 +239,7 @@ export const PaginationPreviousPrimitive = forwardRef< const state: PaginationPreviousPrimitive.State = useMemo(() => ({ disabled }), [disabled]); - return useRenderElement({ + const element = useRenderElement({ ref, render, defaultTagName: 'button', @@ -250,6 +253,8 @@ export const PaginationPreviousPrimitive = forwardRef< ...componentProps, }, }); + + return {element}; }); PaginationPreviousPrimitive.displayName = 'Pagination.PreviousPrimitive'; @@ -307,7 +312,7 @@ export const PaginationNextPrimitive = forwardRef ({ disabled }), [disabled]); - return useRenderElement({ + const element = useRenderElement({ ref, render, defaultTagName: 'button', @@ -321,6 +326,8 @@ export const PaginationNextPrimitive = forwardRef{element}; }, ); PaginationNextPrimitive.displayName = 'Pagination.NextPrimitive'; diff --git a/packages/core/src/components/radio-card/radio-card.css.ts b/packages/core/src/components/radio-card/radio-card.css.ts index 3f170cb93..283b99ce6 100644 --- a/packages/core/src/components/radio-card/radio-card.css.ts +++ b/packages/core/src/components/radio-card/radio-card.css.ts @@ -1,7 +1,6 @@ import { createVar } from '@vanilla-extract/css'; import type { RecipeVariants } from '@vanilla-extract/recipes'; -import { interaction } from '~/styles/mixins/interactions.css'; import { componentRecipe } from '~/styles/mixins/layer-style.css'; import { when } from '~/styles/mixins/logical-states'; import { vars } from '~/styles/themes.css'; @@ -10,7 +9,6 @@ const boxShadowColor = createVar('box-shadow-color'); export const root = componentRecipe({ base: [ - interaction(), { display: 'flex', alignItems: 'center', diff --git a/packages/core/src/components/radio-card/radio-card.tsx b/packages/core/src/components/radio-card/radio-card.tsx index 8e92e5e00..c52b77136 100644 --- a/packages/core/src/components/radio-card/radio-card.tsx +++ b/packages/core/src/components/radio-card/radio-card.tsx @@ -10,6 +10,7 @@ import { createDataAttributes } from '~/utils/data-attributes'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VaporUIComponentProps } from '~/utils/types'; +import { Interaction } from '../interaction'; import { useRadioGroupContext } from '../radio-group'; import type { RadioCardVariants } from './radio-card.css'; import * as styles from './radio-card.css'; @@ -28,13 +29,15 @@ export const RadioCard = forwardRef((props, ref) = const dataAttrs = createDataAttributes({ invalid }); return ( - + + + ); }); RadioCard.displayName = 'RadioCard'; diff --git a/packages/core/src/components/radio/radio.css.ts b/packages/core/src/components/radio/radio.css.ts index fa356b091..9dee173c6 100644 --- a/packages/core/src/components/radio/radio.css.ts +++ b/packages/core/src/components/radio/radio.css.ts @@ -1,7 +1,6 @@ import { createVar } from '@vanilla-extract/css'; import type { RecipeVariants } from '@vanilla-extract/recipes'; -import { interaction } from '~/styles/mixins/interactions.css'; import { componentRecipe, componentStyle } from '~/styles/mixins/layer-style.css'; import { when } from '~/styles/mixins/logical-states'; import { vars } from '~/styles/themes.css'; @@ -10,7 +9,6 @@ const boxShadowColor = createVar('box-shadow-color'); export const root = componentRecipe({ base: [ - interaction(), { position: 'relative', diff --git a/packages/core/src/components/radio/radio.tsx b/packages/core/src/components/radio/radio.tsx index 40be6fdbc..135265ea0 100644 --- a/packages/core/src/components/radio/radio.tsx +++ b/packages/core/src/components/radio/radio.tsx @@ -12,6 +12,7 @@ import { createDataAttributes } from '~/utils/data-attributes'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VaporUIComponentProps } from '~/utils/types'; +import { Interaction } from '../interaction'; import { useRadioGroupContext } from '../radio-group'; import type { RootVariants } from './radio.css'; import * as styles from './radio.css'; @@ -43,15 +44,17 @@ export const RadioRoot = forwardRef((props, ref) = }); return ( - - {children} - + + + {children} + + ); }); RadioRoot.displayName = 'Radio.Root'; diff --git a/packages/core/src/components/segmented-control/segmented-control.css.ts b/packages/core/src/components/segmented-control/segmented-control.css.ts index 666871ad1..73b9b3d0a 100644 --- a/packages/core/src/components/segmented-control/segmented-control.css.ts +++ b/packages/core/src/components/segmented-control/segmented-control.css.ts @@ -1,7 +1,6 @@ import { createVar } from '@vanilla-extract/css'; import { foregrounds } from '~/styles/mixins/foreground.css'; -import { interaction } from '~/styles/mixins/interactions.css'; import { componentRecipe, componentStyle } from '~/styles/mixins/layer-style.css'; import { when } from '~/styles/mixins/logical-states'; import { typography } from '~/styles/mixins/typography.css'; @@ -51,7 +50,6 @@ export const root = componentRecipe({ export const item = componentRecipe({ base: [ - interaction(), typography({ style: 'subtitle1' }), foregrounds({ color: 'secondary-200' }), { diff --git a/packages/core/src/components/segmented-control/segmented-control.tsx b/packages/core/src/components/segmented-control/segmented-control.tsx index 730b42370..611e344ea 100644 --- a/packages/core/src/components/segmented-control/segmented-control.tsx +++ b/packages/core/src/components/segmented-control/segmented-control.tsx @@ -19,6 +19,7 @@ import { createSplitProps } from '~/utils/create-split-props'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VaporUIComponentProps } from '~/utils/types'; +import { Interaction } from '../interaction'; import * as styles from './segmented-control.css'; import type { RootVariants } from './segmented-control.css'; @@ -222,15 +223,17 @@ export const SegmentedControlItem = forwardRef} - value={value} - disabled={disabled} - nativeButton={true} - className={cn(styles.item({ size }), className)} - {...componentProps} - /> + + } + value={value} + disabled={disabled} + nativeButton={true} + className={cn(styles.item({ size }), className)} + {...componentProps} + /> + ); }, ); diff --git a/packages/core/src/components/select/select.css.ts b/packages/core/src/components/select/select.css.ts index 0b485ebba..1b80bd774 100644 --- a/packages/core/src/components/select/select.css.ts +++ b/packages/core/src/components/select/select.css.ts @@ -2,7 +2,6 @@ import { createVar } from '@vanilla-extract/css'; import type { RecipeVariants } from '@vanilla-extract/recipes'; import { foregrounds } from '~/styles/mixins/foreground.css'; -import { interaction } from '~/styles/mixins/interactions.css'; import { componentRecipe, componentStyle } from '~/styles/mixins/layer-style.css'; import { when } from '~/styles/mixins/logical-states'; import { typography } from '~/styles/mixins/typography.css'; @@ -12,7 +11,6 @@ const boxShadowColor = createVar('box-shadow-color'); export const trigger = componentRecipe({ base: [ - interaction(), { display: 'flex', alignItems: 'center', @@ -151,7 +149,6 @@ export const popup = componentStyle({ }); export const item = componentStyle([ - interaction({ type: 'roving' }), typography({ style: 'body2' }), { display: 'flex', diff --git a/packages/core/src/components/select/select.tsx b/packages/core/src/components/select/select.tsx index 95401b65b..7bf982c15 100644 --- a/packages/core/src/components/select/select.tsx +++ b/packages/core/src/components/select/select.tsx @@ -15,6 +15,7 @@ import { createDataAttributes } from '~/utils/data-attributes'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VaporUIComponentProps } from '~/utils/types'; +import { Interaction } from '../interaction'; import * as styles from './select.css'; import type { TriggerVariants } from './select.css'; @@ -64,16 +65,18 @@ export const SelectTriggerPrimitive = forwardRef + + + ); }, ); @@ -280,7 +283,13 @@ export const SelectItemPrimitive = forwardRef + + + ); }, ); diff --git a/packages/core/src/components/switch/switch.css.ts b/packages/core/src/components/switch/switch.css.ts index c814298ee..a1ff0af51 100644 --- a/packages/core/src/components/switch/switch.css.ts +++ b/packages/core/src/components/switch/switch.css.ts @@ -1,13 +1,11 @@ import type { RecipeVariants } from '@vanilla-extract/recipes'; -import { interaction } from '~/styles/mixins/interactions.css'; import { componentRecipe } from '~/styles/mixins/layer-style.css'; import { when } from '~/styles/mixins/logical-states'; import { vars } from '~/styles/themes.css'; export const control = componentRecipe({ base: [ - interaction(), { position: 'relative', display: 'flex', diff --git a/packages/core/src/components/switch/switch.tsx b/packages/core/src/components/switch/switch.tsx index 9859ace83..0e7bdc56a 100644 --- a/packages/core/src/components/switch/switch.tsx +++ b/packages/core/src/components/switch/switch.tsx @@ -13,6 +13,7 @@ import { createDataAttributes } from '~/utils/data-attributes'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VaporUIComponentProps } from '~/utils/types'; +import { Interaction } from '../interaction'; import type { ControlVariants } from './switch.css'; import * as styles from './switch.css'; @@ -47,16 +48,18 @@ export const SwitchRoot = forwardRef((props, ref) return ( - - {children} - + + + {children} + + ); }); diff --git a/packages/core/src/components/tabs/tabs.css.ts b/packages/core/src/components/tabs/tabs.css.ts index 293cfc9a8..19f71a977 100644 --- a/packages/core/src/components/tabs/tabs.css.ts +++ b/packages/core/src/components/tabs/tabs.css.ts @@ -2,7 +2,6 @@ import { createVar } from '@vanilla-extract/css'; import type { RecipeVariants } from '@vanilla-extract/recipes'; import { foregrounds } from '~/styles/mixins/foreground.css'; -import { interaction } from '~/styles/mixins/interactions.css'; import { componentRecipe } from '~/styles/mixins/layer-style.css'; import { when } from '~/styles/mixins/logical-states'; import { typography } from '~/styles/mixins/typography.css'; @@ -91,7 +90,6 @@ export const list = componentRecipe({ export const button = componentRecipe({ base: [ foregrounds({ color: 'normal-100' }), - interaction({ scale: 'light' }), typography({ style: 'subtitle1' }), { position: 'relative', diff --git a/packages/core/src/components/tabs/tabs.tsx b/packages/core/src/components/tabs/tabs.tsx index a97fe8931..630a4ad9d 100644 --- a/packages/core/src/components/tabs/tabs.tsx +++ b/packages/core/src/components/tabs/tabs.tsx @@ -11,6 +11,7 @@ import { createSplitProps } from '~/utils/create-split-props'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VaporUIComponentProps } from '~/utils/types'; +import { Interaction } from '../interaction'; import * as styles from './tabs.css'; import type { ButtonVariants, ListVariants } from './tabs.css'; @@ -119,12 +120,14 @@ export const TabsButton = forwardRef((props, ref) const disabled = disabledProp || rootDisabled; return ( - + + + ); }); TabsButton.displayName = 'Tabs.Button'; diff --git a/packages/core/src/components/text-input/text-input.css.ts b/packages/core/src/components/text-input/text-input.css.ts index 34fa36676..c7c44ffe0 100644 --- a/packages/core/src/components/text-input/text-input.css.ts +++ b/packages/core/src/components/text-input/text-input.css.ts @@ -1,7 +1,6 @@ import { createVar } from '@vanilla-extract/css'; import type { RecipeVariants } from '@vanilla-extract/recipes'; -import { interaction } from '~/styles/mixins/interactions.css'; import { componentRecipe } from '~/styles/mixins/layer-style.css'; import { when } from '~/styles/mixins/logical-states'; import { vars } from '~/styles/themes.css'; @@ -10,8 +9,6 @@ const boxShadowColor = createVar('box-shadow-color'); export const root = componentRecipe({ base: [ - interaction({ type: 'form' }), - { outline: 0, boxShadow: `inset 0 0 0 0.0625rem ${boxShadowColor}`, diff --git a/packages/core/src/components/text-input/text-input.tsx b/packages/core/src/components/text-input/text-input.tsx index 16cc8fce0..d2b6df2c3 100644 --- a/packages/core/src/components/text-input/text-input.tsx +++ b/packages/core/src/components/text-input/text-input.tsx @@ -12,6 +12,7 @@ import { createDataAttributes } from '~/utils/data-attributes'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VaporUIComponentProps } from '~/utils/types'; +import { Interaction } from '../interaction'; import type { RootVariants } from './text-input.css'; import * as styles from './text-input.css'; @@ -55,15 +56,17 @@ export const TextInput = forwardRef((props, ref) = const dataAttrs = createDataAttributes({ disabled, readOnly, required, invalid }); return ( - + + + ); }); TextInput.displayName = 'TextInput'; diff --git a/packages/core/src/components/textarea/textarea.css.ts b/packages/core/src/components/textarea/textarea.css.ts index 75cd3b53c..c5cde7880 100644 --- a/packages/core/src/components/textarea/textarea.css.ts +++ b/packages/core/src/components/textarea/textarea.css.ts @@ -1,7 +1,6 @@ import { createVar } from '@vanilla-extract/css'; import type { RecipeVariants } from '@vanilla-extract/recipes'; -import { interaction } from '~/styles/mixins/interactions.css'; import { componentRecipe } from '~/styles/mixins/layer-style.css'; import { when } from '~/styles/mixins/logical-states'; import { typography } from '~/styles/mixins/typography.css'; @@ -11,8 +10,6 @@ const boxShadowColor = createVar('box-shadow-color'); export const textarea = componentRecipe({ base: [ - interaction({ type: 'form' }), - { boxShadow: `inset 0 0 0 0.0625rem ${boxShadowColor}`, borderRadius: vars.size.borderRadius['300'], diff --git a/packages/core/src/components/textarea/textarea.tsx b/packages/core/src/components/textarea/textarea.tsx index f15e38d64..842730611 100644 --- a/packages/core/src/components/textarea/textarea.tsx +++ b/packages/core/src/components/textarea/textarea.tsx @@ -13,6 +13,7 @@ import { createSplitProps } from '~/utils/create-split-props'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VaporUIComponentProps } from '~/utils/types'; +import { Interaction } from '../interaction'; import type { TextareaVariants } from './textarea.css'; import * as styles from './textarea.css'; @@ -66,7 +67,7 @@ export const Textarea = forwardRef((props, ref) => [disabled, readOnly, required, invalid], ); - return useRenderElement({ + const element = useRenderElement({ ref: composedRef, state, render: render || } />, @@ -78,6 +79,8 @@ export const Textarea = forwardRef((props, ref) => ...otherProps, }, }); + + return {element}; }); Textarea.displayName = 'Textarea'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c2371cf79..25078930b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -16,6 +16,7 @@ export * from './components/grid'; export * from './components/h-stack'; export * from './components/icon-button'; export * from './components/input-group'; +export * from './components/interaction'; export * from './components/menu'; export * from './components/multi-select'; export * from './components/navigation-menu'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbb5e44c1..7a5dbeb2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -205,6 +205,9 @@ importers: storybook: specifier: ^9.1.20 version: 9.1.20(@testing-library/dom@10.4.1)(prettier@3.8.4)(vite@7.3.5(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(tsx@4.21.0)) + storybook-addon-pseudo-states: + specifier: 9.1.20 + version: 9.1.20(storybook@9.1.20(@testing-library/dom@10.4.1)(prettier@3.8.4)(vite@7.3.5(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(tsx@4.21.0))) typescript: specifier: 'catalog:' version: 5.9.3 @@ -8374,6 +8377,11 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + storybook-addon-pseudo-states@9.1.20: + resolution: {integrity: sha512-bqVggqXvE2krhcY7pJcxOPVP/klUcsnWamP9P/cVWdW9ah3vufNY3JpJPQxCt/6OKbaEIZ5QYJkA4bnVi9DEXg==} + peerDependencies: + storybook: ^9.1.20 + storybook@9.1.20: resolution: {integrity: sha512-6rME2tww6PFhm96iG2Xx44yzwLDWBiDWy+kJ2ub6x90werSTOiuo+tZJ94BgCfFutR0tEfLRIq59s+Zg6YyChA==} hasBin: true @@ -13271,8 +13279,8 @@ snapshots: '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.28.4 + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 '@types/aria-query': 5.0.4 aria-query: 5.3.0 dom-accessibility-api: 0.5.16 @@ -18442,6 +18450,10 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + storybook-addon-pseudo-states@9.1.20(storybook@9.1.20(@testing-library/dom@10.4.1)(prettier@3.8.4)(vite@7.3.5(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(tsx@4.21.0))): + dependencies: + storybook: 9.1.20(@testing-library/dom@10.4.1)(prettier@3.8.4)(vite@7.3.5(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(tsx@4.21.0)) + storybook@9.1.20(@testing-library/dom@10.4.1)(prettier@3.8.4)(vite@7.3.5(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(tsx@4.21.0)): dependencies: '@storybook/global': 5.0.0