Skip to content

Commit

Permalink
UNSAFE classname test
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelklibani committed Jan 16, 2025
1 parent fd239fd commit ae48bf1
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 54 deletions.
3 changes: 2 additions & 1 deletion examples/next-with-app-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion packages/web-react/src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const _Button = <T extends ElementType = 'button', C = void, S = void>(

const { buttonProps } = useButtonAriaProps(restProps);
const { classProps, props: modifiedProps } = useButtonStyleProps(restProps);
const { styleProps, props: otherProps } = useStyleProps(modifiedProps);
const { styleProps, props: otherProps } = useStyleProps({ ElementTag, ...modifiedProps });

return (
<ElementTag
Expand All @@ -52,4 +52,6 @@ const _Button = <T extends ElementType = 'button', C = void, S = void>(

const Button = forwardRef<HTMLButtonElement, SpiritButtonProps<ElementType>>(_Button);

Button.spiritComponent = 'Button';

export default Button;
5 changes: 3 additions & 2 deletions packages/web-react/src/components/Tooltip/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const [open, setOpen] = React.useState(false);
</Tooltip>;
```

### 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']}`.
Expand Down Expand Up @@ -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. |
Expand All @@ -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].
Expand Down
23 changes: 9 additions & 14 deletions packages/web-react/src/components/Tooltip/TooltipTrigger.tsx
Original file line number Diff line number Diff line change
@@ -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<TooltipTriggerProps> = {
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 (
<Component {...transferProps} {...triggerStyleProps} id={id} ref={triggerRef} {...getReferenceProps()}>
<ElementTag {...transferProps} {...triggerStyleProps} id={id} ref={triggerRef} {...getReferenceProps()}>
{typeof children === 'function' ? children({ isOpen }) : children}
</Component>
</ElementTag>
);
};

TooltipTrigger.spiritComponent = 'TooltipTrigger';

export default TooltipTrigger;
83 changes: 50 additions & 33 deletions packages/web-react/src/hooks/styleProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>;
styleProps: HTMLAttributes<HTMLElement> | UnsafeStylePropsResult;
props: HTMLAttributes<HTMLElement>;
};

Expand All @@ -15,46 +22,56 @@ export function useStyleProps<T extends StyleProps>(
additionalUtilities?: Record<string, string>,
): 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<T, "UNSAFE_className" | "UNSAFE_style">'.
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<T, "UNSAFE_className" | "UNSAFE_style">'.
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<T, "UNSAFE_className" | "UNSAFE_style">'.
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<T, "UNSAFE_className" | "UNSAFE_style">'.
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<HTMLElement>) },
};
}

return {
styleProps,
styleProps: {
...(UNSAFE_style !== undefined && { UNSAFE_style }),
...(UNSAFE_className !== undefined && { UNSAFE_className }),
},
props: modifiedProps as HTMLAttributes<HTMLElement>,
};
}
19 changes: 19 additions & 0 deletions packages/web-react/src/types/shared/NamedExoticComponent.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* eslint-disable @typescript-eslint/ban-types */

import type { ExoticComponent, FC, StaticLifecycle } from 'react';

declare global {
namespace React {
interface NamedExoticComponent<P = {}> extends ExoticComponent<P> {
spiritComponent?: string;
}

interface FunctionComponent<P = {}> extends FC<P> {
spiritComponent?: string;
}

interface ComponentClass<P = {}, S = {}> extends StaticLifecycle<P, S> {
spiritComponent?: string;
}
}
}
5 changes: 4 additions & 1 deletion packages/web-react/src/types/shared/style.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CSSProperties } from 'react';
import { CSSProperties, ElementType } from 'react';
import { SpacingStyleProp } from '../../constants';
import { BreakpointToken, SpaceToken } from './tokens';

Expand All @@ -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;
Expand Down
8 changes: 7 additions & 1 deletion packages/web-react/src/types/tooltip.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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 {
Expand Down
29 changes: 28 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit ae48bf1

Please sign in to comment.