Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/unified-radix-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@strapi/design-system": patch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import rewrite is mechanical, but radix-ui@1.6.0 bundles the current primitive versions while the individual packages were pinned to ~2023 versions. It's ~2 years of Radix behavior changes landing at once — focus handling, pointer-events on dismissable layers, positioning — i.e. a real runtime upgrade for every overlay/menu/dialog in consumers, not just a dependency rename.

The description should also say this explicitly: Radix primitives are upgraded from the 1.0.x era to the versions bundled in radix-ui@1.6.0, and the radix install nests React 19 types — consumers on React 18 may want an @types/react resolution like the one added to this repo's root package.json.

"@strapi/ui-primitives": patch
---

chore: migrate from `@radix-ui/*` to the unified `radix-ui` package
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@
"typescript": "^5.1.3"
},
"packageManager": "yarn@3.6.4",
"resolutions": {
"@types/react": "18.3.3"
},
"dependencies": {
"@vueless/storybook-dark-mode": "9.0.6"
}
Expand Down
18 changes: 1 addition & 17 deletions packages/design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,10 @@
"@floating-ui/react-dom": "2.1.0",
"@internationalized/date": "3.5.4",
"@internationalized/number": "3.5.3",
"@radix-ui/react-accordion": "1.1.2",
"@radix-ui/react-alert-dialog": "1.0.5",
"@radix-ui/react-avatar": "1.0.4",
"@radix-ui/react-checkbox": "1.0.4",
"@radix-ui/react-dialog": "1.0.5",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-dropdown-menu": "2.0.6",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.4",
"@radix-ui/react-popover": "1.0.7",
"@radix-ui/react-progress": "1.0.3",
"@radix-ui/react-radio-group": "1.1.3",
"@radix-ui/react-scroll-area": "1.0.5",
"@radix-ui/react-switch": "1.0.3",
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@strapi/ui-primitives": "2.2.1",
"@uiw/react-codemirror": "4.22.2",
"lodash": "4.18.1",
"radix-ui": "1.6.0",
"react-remove-scroll": "2.5.10"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import * as RadixAccordion from '@radix-ui/react-accordion';
import { Accordion as RadixAccordion } from 'radix-ui';
import { CaretDown } from '@strapi/icons';
import { css, keyframes, styled } from 'styled-components';

Expand Down
3 changes: 1 addition & 2 deletions packages/design-system/src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as React from 'react';

import * as Avatar from '@radix-ui/react-avatar';
import * as Tooltip from '@radix-ui/react-tooltip';
import { Avatar, Tooltip } from 'radix-ui';
import { css, styled } from 'styled-components';

import { useControllableState } from '../../hooks/useControllableState';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import * as BaseCheckbox from '@radix-ui/react-checkbox';
import { Checkbox as BaseCheckbox } from 'radix-ui';
import { Minus } from '@strapi/icons';
import { styled, useTheme } from 'styled-components';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import {
parseAbsolute,
getLocalTimeZone,
} from '@internationalized/date';
import { useFocusGuards } from '@radix-ui/react-focus-guards';
import { FocusScope } from '@radix-ui/react-focus-scope';
import { FocusGuards, FocusScope as FocusScopePrimitive } from 'radix-ui/internal';

const FocusScope = FocusScopePrimitive.FocusScope;
const useFocusGuards = FocusGuards.useFocusGuards;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: these const re-exports are declared in the middle of the import block (imports continue below on line 23). Valid JS since imports hoist, and oxlint doesn't flag it, but it reads oddly. Suggest either moving the aliases below the last import, or destructuring in place where possible. Same pattern in Combobox.tsx, Select.tsx, Collection.tsx, and DesignSystemProvider.tsx.

import { Calendar, Cross } from '@strapi/icons';
import { composeEventHandlers } from '@strapi/ui-primitives';
import { RemoveScroll } from 'react-remove-scroll';
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import * as AlertDialog from '@radix-ui/react-alert-dialog';
import { AlertDialog } from 'radix-ui';
import { styled } from 'styled-components';

import { setOpacity } from '../../helpers/setOpacity';
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import * as Dialog from '@radix-ui/react-dialog';
import { Dialog } from 'radix-ui';
import { Cross } from '@strapi/icons';
import { styled } from 'styled-components';

Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/components/Popover/Popover.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import * as Popover from '@radix-ui/react-popover';
import { Popover } from 'radix-ui';
import { styled } from 'styled-components';

import { stripReactIdOfColon } from '../../helpers/strings';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import * as Progress from '@radix-ui/react-progress';
import { Progress } from 'radix-ui';
import { styled } from 'styled-components';

type Size = 'S' | 'M';
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/components/Radio/Radio.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import * as RadioGroup from '@radix-ui/react-radio-group';
import { RadioGroup } from 'radix-ui';
import { styled } from 'styled-components';

import { useId } from '../../hooks/useId';
Expand Down
4 changes: 2 additions & 2 deletions packages/design-system/src/components/SimpleMenu/Menu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { DropdownMenu } from 'radix-ui';
import { CaretDown, ChevronRight } from '@strapi/icons';
import { styled, css, type DefaultTheme } from 'styled-components';

Expand Down Expand Up @@ -146,7 +146,7 @@ const DropdownMenuContent = styled(DropdownMenu.Content)`
* -----------------------------------------------------------------------------------------------*/
export type ItemVariant = 'danger' | 'default';

interface ItemSharedProps extends Pick<DropdownMenu.MenuItemProps, 'disabled' | 'onSelect'> {
interface ItemSharedProps extends Pick<DropdownMenu.DropdownMenuItemProps, 'disabled' | 'onSelect'> {
children?: React.ReactNode;
isExternal?: boolean;
isFocused?: boolean;
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/components/Switch/Switch.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import * as RadixSwitch from '@radix-ui/react-switch';
import { Switch as RadixSwitch } from 'radix-ui';
import { composeEventHandlers } from '@strapi/ui-primitives';
import { styled } from 'styled-components';

Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import * as Tabs from '@radix-ui/react-tabs';
import { Tabs } from 'radix-ui';
import { css, styled } from 'styled-components';

import { createContext } from '../../helpers/context';
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import * as Tooltip from '@radix-ui/react-tooltip';
import { Tooltip } from 'radix-ui';
import { styled } from 'styled-components';

import { Typography } from '../../primitives/Typography';
Expand Down
15 changes: 11 additions & 4 deletions packages/design-system/src/helpers/handleResponsiveValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,14 @@ const handleResponsiveValues = (values: ResponsiveProps, theme: DefaultTheme) =>
*/
acc.initial = {
...acc.initial,
...convertCssPropertiesToCssValues(cssProperty, value, themeSection),
// `radix-ui`'s `@radix-ui/react-primitive` augments `React.CSSProperties` with a
// `--radix-*` index signature, which collapses `DefaultThemeOrCSSProp<any, any>` from
// `any` to a concrete union. Cast keeps the original (intentionally permissive) behaviour.
...convertCssPropertiesToCssValues(
cssProperty,
value as DefaultThemeOrCSSProp<any, any> | Array<DefaultThemeOrCSSProp<any, any>>,
themeSection,
),
};
}
}
Expand Down Expand Up @@ -230,7 +237,7 @@ const convertCssPropertiesToCssValues = (
* padding-inline-end: 4;
* ```
*/
const shorthandValues = fillCssValues(value);
const shorthandValues = fillCssValues(value as DefaultThemeOrCSSProp<'spaces', 'margin' | 'padding'>[]);

return property.reduce((acc, prop, index) => {
acc[prop] = extractStyleFromTheme(themeSection, shorthandValues[index], shorthandValues[index]);
Expand All @@ -252,13 +259,13 @@ const convertCssPropertiesToCssValues = (
* ```
*/
return property.reduce((acc, prop) => {
acc[prop] = extractStyleFromTheme(themeSection, value, value);
acc[prop] = extractStyleFromTheme(themeSection, value as string | number | symbol | undefined, value);

return acc;
}, {});
} else if (!Array.isArray(property) && !Array.isArray(value)) {
return {
[property]: extractStyleFromTheme(themeSection, value, value),
[property]: extractStyleFromTheme(themeSection, value as string | number | symbol | undefined, value),
};
} else {
console.warn(
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/hooks/useControllableState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import { useCallbackRef } from '@radix-ui/react-use-callback-ref';
import { useCallbackRef } from 'radix-ui/internal';

type UseControllableStateParams<T> = {
prop?: T | undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Provider as TooltipProvider, TooltipProviderProps } from '@radix-ui/react-tooltip';
import { Tooltip } from 'radix-ui';

const TooltipProvider = Tooltip.Provider;
type TooltipProviderProps = Tooltip.TooltipProviderProps;
import { DefaultTheme, ThemeProvider } from 'styled-components';

import { LiveRegions } from '../components/LiveRegions';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as DismissibleLayerPrimitive from '@radix-ui/react-dismissable-layer';
import { DismissableLayer as DismissibleLayerPrimitive } from 'radix-ui/internal';

export const DismissibleLayer = DismissibleLayerPrimitive.Root;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import * as ScrollArea from '@radix-ui/react-scroll-area';
import { ScrollArea } from 'radix-ui';
import { styled } from 'styled-components';

type ScrollAreaElement = HTMLDivElement;
Expand Down
18 changes: 1 addition & 17 deletions packages/primitives/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,9 @@
"watch": "vite build --watch"
},
"dependencies": {
"@radix-ui/number": "1.0.1",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-collection": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.4",
"@radix-ui/react-popper": "1.1.3",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-use-controllable-state": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1",
"@radix-ui/react-use-previous": "1.0.1",
"@radix-ui/react-visually-hidden": "1.0.3",
"@tanstack/react-virtual": "^3.10.8",
"aria-hidden": "1.2.4",
"radix-ui": "1.6.0",
"react-remove-scroll": "2.5.10"
},
"devDependencies": {
Expand Down
8 changes: 5 additions & 3 deletions packages/primitives/src/components/Collection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
import * as React from 'react';
import { type ComponentPropsWithoutRef } from 'react';

import { useComposedRefs } from '@radix-ui/react-compose-refs';
import { createContextScope } from '@radix-ui/react-context';
import { Slot } from '@radix-ui/react-slot';
import { Slot as SlotPrimitive } from 'radix-ui';
import { Context, useComposedRefs } from 'radix-ui/internal';

const createContextScope = Context.createContextScope;
const Slot = SlotPrimitive.Slot;

type SlotProps = ComponentPropsWithoutRef<typeof Slot>;
type CollectionElement = HTMLElement;
Expand Down
54 changes: 33 additions & 21 deletions packages/primitives/src/components/Combobox/Combobox.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import * as React from 'react';
import { useId } from 'react';

import { composeEventHandlers } from '@radix-ui/primitive';
import { useComposedRefs } from '@radix-ui/react-compose-refs';
import { createContext } from '@radix-ui/react-context';
import { DismissableLayer } from '@radix-ui/react-dismissable-layer';
import { useFocusGuards } from '@radix-ui/react-focus-guards';
import { FocusScope } from '@radix-ui/react-focus-scope';
import * as PopperPrimitive from '@radix-ui/react-popper';
import { Portal as PortalPrimitive } from '@radix-ui/react-portal';
import { Primitive } from '@radix-ui/react-primitive';
import { useControllableState } from '@radix-ui/react-use-controllable-state';
import { useLayoutEffect } from '@radix-ui/react-use-layout-effect';
import { useId, type ComponentPropsWithoutRef } from 'react';

import { Portal as RadixPortal } from 'radix-ui';
import {
Context,
DismissableLayer as DismissableLayerPrimitive,
FocusGuards,
FocusScope as FocusScopePrimitive,
Popper as PopperPrimitive,
Primitive,
composeEventHandlers,
useComposedRefs,
useControllableState,
useLayoutEffect,
} from 'radix-ui/internal';

const PortalPrimitive = RadixPortal.Portal;
const DismissableLayer = DismissableLayerPrimitive.DismissableLayer;
const FocusScope = FocusScopePrimitive.FocusScope;
const createContext = Context.createContext;
const useFocusGuards = FocusGuards.useFocusGuards;
import { hideOthers } from 'aria-hidden';
import * as ReactDOM from 'react-dom';
import { RemoveScroll } from 'react-remove-scroll';
Expand All @@ -22,8 +30,6 @@

import { VirtualizedViewport } from './VirtualizedViewport';

import type { ComponentPropsWithoutRef } from '@radix-ui/react-primitive';

const OPEN_KEYS = [' ', 'Enter', 'ArrowUp', 'ArrowDown'];
const SELECTION_KEYS = ['Enter'];

Expand Down Expand Up @@ -107,6 +113,7 @@
disabled?: boolean;
locale?: string;
onOpenChange?(open: boolean): void;
// TODO: This might need to be `value: string | undefined`

Check warning on line 116 in packages/primitives/src/components/Combobox/Combobox.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=strapi_design-system&issues=AZ9BXibghxwb68-Ofw5p&open=AZ9BXibghxwb68-Ofw5p&pullRequest=2033
onValueChange?(value: string): void;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO can be resolved in this PR — the answer is yes, the type should be onValueChange?(value: string | undefined): void.

Evidence that undefined is genuinely delivered at runtime (pre-existing behavior, also on main):

  • The internal context already types it that way (onValueChange(value: string | undefined), line 80).
  • The Escape-clear paths call context.onValueChange(undefined) (lines 547 and 679), which flows through setValueuseControllableState.onChange → the consumer's onValueChange.

So the public string type has been lying since before this PR; the new v1.2 generics just forced the cast that exposes it. Widening the prop type here:

  • lets you delete the as ((value: string | undefined) => void) | undefined cast on line 218,
  • automatically propagates to the design-system Combobox (its onChange is typed as ComboboxPrimitive.RootProps['onValueChange']),
  • won't break consumers' typechecks — method-style signatures are bivariant, so existing (value: string) => void handlers still assign.

Suggest doing it now rather than merging a known-incorrect type with a TODO.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a type "breaking change". I will need first to do some checks in Strapi monorepo to see how is used there.

onTextValueChange?(textValue: string): void;
textValue?: string;
Expand All @@ -115,6 +122,7 @@
value?: string;
defaultFilterValue?: string;
filterValue?: string;
// TODO: This might need to be `value: string | undefined`

Check warning on line 125 in packages/primitives/src/components/Combobox/Combobox.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=strapi_design-system&issues=AZ9BXibghxwb68-Ofw5q&open=AZ9BXibghxwb68-Ofw5q&pullRequest=2033
onFilterValueChange?(value: string): void;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO can also be resolved now. Unlike onValueChange, none of the 8 internal context.onFilterValueChange(...) call sites ever pass undefined — they all pass strings. So strictly, string is accurate today.

That said, the state is declared useControllableState<string | undefined> and the context types it (value: string | undefined) => void (line 95), so I'd still widen the public type to string | undefined for consistency — it lets you drop the cast on line 230 and matches the actual state domain (filterValue can legitimately be undefined, see the context.filterValue === undefined checks). Either way, the TODO should not ship.

isPrintableCharacter?: (str: string) => boolean;
visible?: boolean;
Expand Down Expand Up @@ -200,23 +208,27 @@
*/
const [open = false, setOpen] = useControllableState({
prop: openProp,
defaultProp: defaultOpen,
defaultProp: defaultOpen ?? false,
onChange: onOpenChange,
caller: COMBOBOX_NAME,
});
const [value, setValue] = useControllableState({
const [value, setValue] = useControllableState<string | undefined>({
prop: valueProp,
defaultProp: defaultValue,
onChange: onValueChange,
onChange: onValueChange as ((value: string | undefined) => void) | undefined,
caller: COMBOBOX_NAME,
});
const [textValue, setTextValue] = useControllableState({
prop: textValueProp,
defaultProp: allowCustomValue && !defaultTextValue ? valueProp : defaultTextValue,
defaultProp: (allowCustomValue && !defaultTextValue ? valueProp : defaultTextValue) as string,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This as string cast is unsound: valueProp and defaultTextValue are both optional, so the whole expression can be undefined — and undefined is a meaningful state here (placeholder rendering checks context.textValue === undefined, e.g. the data-placeholder logic).

Suggest typing it explicitly like the sibling states instead of casting the value:

const [textValue, setTextValue] = useControllableState<string | undefined>({
  prop: textValueProp,
  defaultProp: allowCustomValue && !defaultTextValue ? valueProp : defaultTextValue,
  onChange: onTextValueChange as ((value: string | undefined) => void) | undefined,
  caller: COMBOBOX_NAME,
});

(And if onTextValueChange gets the same string | undefined widening as the other two callbacks, that cast disappears too.)

onChange: onTextValueChange,
caller: COMBOBOX_NAME,
});
const [filterValue, setFilterValue] = useControllableState({
const [filterValue, setFilterValue] = useControllableState<string | undefined>({
prop: filterValueProp,
defaultProp: defaultFilterValue,
onChange: onFilterValueChange,
onChange: onFilterValueChange as ((value: string | undefined) => void) | undefined,
caller: COMBOBOX_NAME,
});

const id = useId();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import * as React from 'react';
import { type ComponentPropsWithoutRef } from 'react';

import { useComposedRefs } from '@radix-ui/react-compose-refs';
import { Primitive } from '@radix-ui/react-primitive';
import { useVirtualizer } from '@tanstack/react-virtual';

import type { ComponentPropsWithoutRef } from '@radix-ui/react-primitive';
import { Primitive, useComposedRefs } from 'radix-ui/internal';

/**
* VirtualizedViewport - Renders only visible items for performance optimization
Expand Down
Loading
Loading