diff --git a/examples/next-with-app-router/package.json b/examples/next-with-app-router/package.json index 671cfc9057..e476206107 100644 --- a/examples/next-with-app-router/package.json +++ b/examples/next-with-app-router/package.json @@ -15,7 +15,8 @@ "@lmc-eu/spirit-web-react": "workspace:^", "next": "14.2.23", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "sass": "^1.83.0" }, "devDependencies": { "@next/eslint-plugin-next": "14.2.23", diff --git a/packages/web-react/src/components/Button/Button.tsx b/packages/web-react/src/components/Button/Button.tsx index f5d4cba1e7..9ee16ac5ba 100644 --- a/packages/web-react/src/components/Button/Button.tsx +++ b/packages/web-react/src/components/Button/Button.tsx @@ -34,7 +34,7 @@ const _Button = ( const { buttonProps } = useButtonAriaProps(restProps); const { classProps, props: modifiedProps } = useButtonStyleProps(restProps); - const { styleProps, props: otherProps } = useStyleProps(modifiedProps); + const { styleProps, props: otherProps } = useStyleProps({ ElementTag, ...modifiedProps }); return ( ( const Button = forwardRef>(_Button); +Button.spiritComponent = 'Button'; + export default Button; diff --git a/packages/web-react/src/components/Tooltip/README.md b/packages/web-react/src/components/Tooltip/README.md index 2ba732cbcf..559711bb66 100644 --- a/packages/web-react/src/components/Tooltip/README.md +++ b/packages/web-react/src/components/Tooltip/README.md @@ -30,7 +30,7 @@ const [open, setOpen] = React.useState(false); ; ``` -### Trigger +### TooltipTrigger You can choose whether you want to open the tooltip on `click` and/or `hover`. By default, both options are active, e.g., `trigger={['click', 'hover']}`. @@ -60,6 +60,7 @@ const [open, setOpen] = React.useState(false); | Attribute | Type | Default | Required | Description | | ------------------------------- | ----------------------------------------------------------------- | -------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `children` | `ReactNode` | — | ✓ | Tooltip children's nodes - `TooltipTrigger` and `TooltipPopover` | +| `elementType` | `ElementType` | "button" | ✕ | Type of element used as trigger | | `enableFlipping` | `bool` | true | ✕ | Enables [flipping][floating-ui-flip] of the element’s placement when it starts to overflow its boundary area. For example `top` can be flipped to `bottom`. | | `enableFlippingCrossAxis` | `bool` | true | ✕ | Enables flipping on the [cross axis][floating-ui-flip-cross-axis], the axis perpendicular to main axis. For example `top-end` can be flipped to the `top-start`. | | `enableShifting` | `bool` | true | ✕ | Enables [shifting][floating-ui-shift] of the element to keep it inside the boundary area by adjusting its position. | @@ -68,11 +69,11 @@ const [open, setOpen] = React.useState(false); | `flipFallbackPlacements` | `string` | - | ✕ | This describes a list of [explicit placements][floating-ui-flip-fallback-placements] to try if the initial placement doesn’t fit on the axes in which overflow is checked. For example you can set `"top, right, bottom"` | | `id` | `string` | - | ✓ | Tooltip id | | `isDismissible` | `bool` | false | ✕ | Make tooltip dismissible | +| `isFocusableOnHover` | `bool` | false | ✕ | Allows you to mouse over a tooltip without closing it. We suggest turning off the `click` trigger if you use this feature. | | `isOpen` | `bool` | - | ✓ | Open state | | `onToggle` | `() => void` | - | ✓ | Function for toggle open state of dropdown | | `placement` | [Placement Dictionary][dictionary-placement] | "bottom" | ✕ | Placement of tooltip | | `positionStrategy` | \[`absolute` \| `fixed`] ([Strategy type][use-floating-strategy]) | "absolute" | ✕ | This is the type of CSS position property to use. | -| `isFocusableOnHover` | `bool` | false | ✕ | Allows you to mouse over a tooltip without closing it. We suggest turning off the `click` trigger if you use this feature. | | `trigger` | \[`click` \| `hover` \| `manual`] | \["click", "hover" ] | ✕ | How tooltip is triggered: `click`, `hover`, `manual`. You may pass multiple triggers. If you pass `manual`, there will be no toggle functionality and you should provide your own toggle solution. | On top of the API options, the components accept [additional attributes][readme-additional-attributes]. diff --git a/packages/web-react/src/components/Tooltip/TooltipTrigger.tsx b/packages/web-react/src/components/Tooltip/TooltipTrigger.tsx index b8b86e98f9..a17853f977 100644 --- a/packages/web-react/src/components/Tooltip/TooltipTrigger.tsx +++ b/packages/web-react/src/components/Tooltip/TooltipTrigger.tsx @@ -1,34 +1,29 @@ 'use client'; -import React, { ElementType, ReactNode } from 'react'; +import React from 'react'; import { useStyleProps } from '../../hooks'; -import { StyleProps, TransferProps } from '../../types'; +import { TooltipTriggerProps } from '../../types'; import { useTooltipContext } from './TooltipContext'; -interface TooltipTriggerProps extends StyleProps, TransferProps { - elementType?: ElementType | string; - children?: string | ReactNode | ((props: { isOpen: boolean }) => ReactNode); -} - -const defaultProps: TooltipTriggerProps = { +const defaultProps: Partial = { elementType: 'button', children: null, }; const TooltipTrigger = (props: TooltipTriggerProps) => { const propsWithDefaults = { ...defaultProps, ...props }; - const { elementType = 'button', children, ...rest } = propsWithDefaults; + const { elementType: ElementTag = 'button', children, ...rest } = propsWithDefaults; const { id, isOpen, triggerRef, getReferenceProps } = useTooltipContext(); - const Component = elementType; - - const { styleProps: triggerStyleProps, props: transferProps } = useStyleProps(rest); + const { styleProps: triggerStyleProps, props: transferProps } = useStyleProps({ ElementTag, ...rest }); return ( - + {typeof children === 'function' ? children({ isOpen }) : children} - + ); }; +TooltipTrigger.spiritComponent = 'TooltipTrigger'; + export default TooltipTrigger; diff --git a/packages/web-react/src/hooks/styleProps.ts b/packages/web-react/src/hooks/styleProps.ts index 791f78f6f0..c6a0aa3782 100644 --- a/packages/web-react/src/hooks/styleProps.ts +++ b/packages/web-react/src/hooks/styleProps.ts @@ -5,8 +5,15 @@ import ClassNamePrefixContext from '../context/ClassNamePrefixContext'; import { StyleProps } from '../types'; import { useStyleUtilities } from './useStyleUtilities'; +export type UnsafeStylePropsResult = { + UNSAFE_className?: string; + UNSAFE_style?: CSSProperties; + className?: string; + style?: CSSProperties; +}; + export type StylePropsResult = { - styleProps: HTMLAttributes; + styleProps: HTMLAttributes | UnsafeStylePropsResult; props: HTMLAttributes; }; @@ -15,46 +22,56 @@ export function useStyleProps( additionalUtilities?: Record, ): StylePropsResult { const classNamePrefix = useContext(ClassNamePrefixContext); - const { UNSAFE_className, UNSAFE_style, ...otherProps } = props; + const { UNSAFE_className, UNSAFE_style, ElementTag, ...otherProps } = props; const { styleUtilities, props: modifiedProps } = useStyleUtilities(otherProps, classNamePrefix, additionalUtilities); const style: CSSProperties = { ...UNSAFE_style }; - // Want to check if className prop exists, but not to define it in StyleProps type - // @ts-expect-error Property 'className' does not exist on type 'Omit'. - if (modifiedProps.className) { - warning( - false, - 'The className prop is unsafe and is unsupported in Spirit Web React. ' + - 'Please use style props with Spirit Design Tokens, or UNSAFE_className if you absolutely must do something custom. ' + - 'Note that this may break in future versions due to DOM structure changes.', - ); - - // @ts-expect-error same as above, let me live my life - delete modifiedProps.className; - } + if (typeof ElementTag === 'string' || !ElementTag?.spiritComponent) { + // Want to check if className prop exists, but not to define it in StyleProps type + // @ts-expect-error Property 'className' does not exist on type 'Omit'. + if (modifiedProps.className) { + warning( + false, + 'The className prop is unsafe and is unsupported in Spirit Web React. ' + + 'Please use style props with Spirit Design Tokens, or UNSAFE_className if you absolutely must do something custom. ' + + 'Note that this may break in future versions due to DOM structure changes.', + ); - // Want to check if style prop exists, but not to define it in StyleProps type - // @ts-expect-error Property 'style' does not exist on type 'Omit'. - if (modifiedProps.style) { - warning( - false, - 'The style prop is unsafe and is unsupported in Spirit Web React. ' + - 'Please use style props with Spirit Design Tokens, or UNSAFE_style if you absolutely must do something custom. ' + - 'Note that this may break in future versions due to DOM structure changes.', - ); - - // @ts-expect-error same as above, let me live my life - delete modifiedProps.style; - } + // @ts-expect-error same as above, let me live my life + delete modifiedProps.className; + } - const styleProps = { - style: Object.keys(style).length > 0 ? style : undefined, - className: classNames(UNSAFE_className, ...styleUtilities) || undefined, - }; + // Want to check if style prop exists, but not to define it in StyleProps type + // @ts-expect-error Property 'style' does not exist on type 'Omit'. + if (modifiedProps.style) { + warning( + false, + 'The style prop is unsafe and is unsupported in Spirit Web React. ' + + 'Please use style props with Spirit Design Tokens, or UNSAFE_style if you absolutely must do something custom. ' + + 'Note that this may break in future versions due to DOM structure changes.', + ); + + // @ts-expect-error same as above, let me live my life + delete modifiedProps.style; + } + + const styleProps = { + style: Object.keys(style).length > 0 ? style : undefined, + className: classNames(UNSAFE_className, ...styleUtilities) || undefined, + }; + + return { + styleProps, + props: { ...(modifiedProps as HTMLAttributes) }, + }; + } return { - styleProps, + styleProps: { + ...(UNSAFE_style !== undefined && { UNSAFE_style }), + ...(UNSAFE_className !== undefined && { UNSAFE_className }), + }, props: modifiedProps as HTMLAttributes, }; } diff --git a/packages/web-react/src/types/shared/NamedExoticComponent.d.ts b/packages/web-react/src/types/shared/NamedExoticComponent.d.ts new file mode 100644 index 0000000000..b62a6b6cfb --- /dev/null +++ b/packages/web-react/src/types/shared/NamedExoticComponent.d.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +import type { ExoticComponent, FC, StaticLifecycle } from 'react'; + +declare global { + namespace React { + interface NamedExoticComponent

extends ExoticComponent

{ + spiritComponent?: string; + } + + interface FunctionComponent

extends FC

{ + spiritComponent?: string; + } + + interface ComponentClass

extends StaticLifecycle { + spiritComponent?: string; + } + } +} diff --git a/packages/web-react/src/types/shared/style.ts b/packages/web-react/src/types/shared/style.ts index 3bf1577dd0..96151d2ccc 100644 --- a/packages/web-react/src/types/shared/style.ts +++ b/packages/web-react/src/types/shared/style.ts @@ -1,4 +1,4 @@ -import { CSSProperties } from 'react'; +import { CSSProperties, ElementType } from 'react'; import { SpacingStyleProp } from '../../constants'; import { BreakpointToken, SpaceToken } from './tokens'; @@ -20,7 +20,10 @@ export interface SpacingCSSProperties extends CSSProperties { [index: `--${string}`]: string | undefined | number; } +type ElementTagType = string | ElementType; + export interface StyleProps extends SpacingProps { + ElementTag?: ElementTagType; // For backward compatibility! /** Sets the CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. Only use as a **last resort**. Use style props instead. */ UNSAFE_className?: string; diff --git a/packages/web-react/src/types/tooltip.ts b/packages/web-react/src/types/tooltip.ts index f961b7d2c1..519db1fe8b 100644 --- a/packages/web-react/src/types/tooltip.ts +++ b/packages/web-react/src/types/tooltip.ts @@ -1,5 +1,6 @@ import { Placement, Strategy } from '@floating-ui/react'; -import { ChildrenProps, ClickEvent, StyleProps } from './shared'; +import { ElementType, ReactNode } from 'react'; +import { ChildrenProps, ClickEvent, StyleProps, TransferProps } from './shared'; export const TOOLTIP_TRIGGER = { CLICK: 'click', @@ -11,6 +12,11 @@ export const TOOLTIP_TRIGGER = { export type TooltipTriggerType = 'click' | 'hover' | 'manual'; +export interface TooltipTriggerProps extends StyleProps, TransferProps { + elementType?: ElementType | string; + children?: string | ReactNode | ((props: { isOpen: boolean }) => ReactNode); +} + export interface UncontrolledTooltipProps extends BaseTooltipProps {} export interface TooltipCloseButtonProps extends StyleProps { diff --git a/yarn.lock b/yarn.lock index 46ffb0a738..164eaec7e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,6 +40,7 @@ __metadata: next: "npm:14.2.23" react: "npm:^18" react-dom: "npm:^18" + sass: "npm:^1.83.0" typescript: "npm:5.6.3" languageName: unknown linkType: soft @@ -11724,6 +11725,15 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^4.0.0": + version: 4.0.2 + resolution: "chokidar@npm:4.0.2" + dependencies: + readdirp: "npm:^4.0.1" + checksum: 10/fc25d20d72ee0e74b5be1fd9df366dc8aa17709a59c364c321b6f35b6d2fd8c65d01bda74eb42ffd61ad7807e5de5e673c6bd503c2ed0ab2a79be5cb51d4c259 + languageName: node + linkType: hard + "chokidar@npm:^4.0.1": version: 4.0.1 resolution: "chokidar@npm:4.0.1" @@ -27614,6 +27624,23 @@ __metadata: languageName: node linkType: hard +"sass@npm:^1.83.0": + version: 1.83.0 + resolution: "sass@npm:1.83.0" + dependencies: + "@parcel/watcher": "npm:^2.4.1" + chokidar: "npm:^4.0.0" + immutable: "npm:^5.0.2" + source-map-js: "npm:>=0.6.2 <2.0.0" + dependenciesMeta: + "@parcel/watcher": + optional: true + bin: + sass: sass.js + checksum: 10/cae7c489ffeb1324ac7e766dda60206a6d7a318d0689b490290a32a6414ef1fd0f376f92d45fb1610e507baa6f6594f1a61d4d706df2f105122ff9a83d2a28e1 + languageName: node + linkType: hard + "sax@npm:~1.2.4": version: 1.2.4 resolution: "sax@npm:1.2.4" @@ -28228,7 +28255,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": +"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 10/ff9d8c8bf096d534a5b7707e0382ef827b4dd360a577d3f34d2b9f48e12c9d230b5747974ee7c607f0df65113732711bb701fe9ece3c7edbd43cb2294d707df3