diff --git a/packages/dnb-eufemia/src/components/badge/Badge.tsx b/packages/dnb-eufemia/src/components/badge/Badge.tsx index 031d9fe4dfc..abce6705145 100644 --- a/packages/dnb-eufemia/src/components/badge/Badge.tsx +++ b/packages/dnb-eufemia/src/components/badge/Badge.tsx @@ -9,7 +9,7 @@ import { createSkeletonClass } from '../skeleton/SkeletonHelper' import Context from '../../shared/Context' import { ISpacingProps } from '../../shared/interfaces' import { SkeletonShow } from '../skeleton/Skeleton' -import { extendPropsWithContext } from '../../shared/component-helper' +import { usePropsWithContext } from '../../shared/component-helper' import { warn } from '../../shared/component-helper' export interface BadgeProps { @@ -78,9 +78,10 @@ export const defaultProps = { function Badge(localProps: BadgeAndISpacingProps) { // Every component should have a context const context = React.useContext(Context) + // Extract additional props from global context - const { children, ...props } = extendPropsWithContext( - { ...defaultProps, ...localProps }, + const { children, ...props } = usePropsWithContext( + localProps, defaultProps, context?.Badge ) diff --git a/packages/dnb-eufemia/src/components/button/Button.d.ts b/packages/dnb-eufemia/src/components/button/Button.d.ts index ed54417a333..6e6d989b149 100644 --- a/packages/dnb-eufemia/src/components/button/Button.d.ts +++ b/packages/dnb-eufemia/src/components/button/Button.d.ts @@ -72,12 +72,7 @@ export type ButtonOnClick = string | ((...args: any[]) => any); /** * NB: Do not change the docs (comments) in here. The docs are updated during build time by "generateTypes.js" and "fetchPropertiesFromDocs.js". */ -export interface ButtonProps - extends Omit< - React.HTMLProps, - 'disabled' | 'size' | 'title' | 'wrap' - >, - Partial { +export type ButtonProps = { /** * The content of the button can be a string or a React Element. */ @@ -230,7 +225,11 @@ export interface ButtonProps * Will be called on a click event. Returns an object with the native event: `{ event }`. */ on_click?: ButtonOnClick; -} +} & Partial< + DataAttributeTypes & + Partial> +>; + export default class Button extends React.Component { render(): JSX.Element; } diff --git a/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap b/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap index d666f4f1ced..211785f32e8 100644 --- a/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap +++ b/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap @@ -824,10 +824,8 @@ exports[`Dialog component snapshot should match component snapshot 1`] = ` onKeyDown={[Function]} onTouchStart={[Function]} > - + - + diff --git a/packages/dnb-eufemia/src/components/drawer/DrawerContent.tsx b/packages/dnb-eufemia/src/components/drawer/DrawerContent.tsx index 340b455dd8f..edd6e6edb70 100644 --- a/packages/dnb-eufemia/src/components/drawer/DrawerContent.tsx +++ b/packages/dnb-eufemia/src/components/drawer/DrawerContent.tsx @@ -27,7 +27,6 @@ export default function DrawerContent({ containerPlacement = 'right', preventCoreStyle = false, className = null, - class: _className = null, spacing = true, fullscreen = 'auto', noAnimation = false, @@ -61,8 +60,7 @@ export default function DrawerContent({ isTrue(noAnimationOnMobile) && `dnb-drawer--no-animation-on-mobile`, `dnb-drawer--${containerPlacement || 'right'}`, - className, - _className + className ), style: (minWidth || maxWidth) && { minWidth, maxWidth }, onClick: context?.preventClick, diff --git a/packages/dnb-eufemia/src/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap b/packages/dnb-eufemia/src/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap index 962dce432db..32026e90415 100644 --- a/packages/dnb-eufemia/src/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap +++ b/packages/dnb-eufemia/src/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap @@ -814,7 +814,7 @@ exports[`Drawer component snapshot should match component snapshot 1`] = ` noAnimation={true} spacing={true} > - - + - {content} - - ) - } - - return - } -} diff --git a/packages/dnb-eufemia/src/components/help-button/HelpButton.tsx b/packages/dnb-eufemia/src/components/help-button/HelpButton.tsx new file mode 100644 index 00000000000..fc044549478 --- /dev/null +++ b/packages/dnb-eufemia/src/components/help-button/HelpButton.tsx @@ -0,0 +1,62 @@ +/** + * Web HelpButton Component + * + */ + +import React from 'react' +import Context from '../../shared/Context' +import Modal from '../modal/Modal' +import HelpButtonInstance from './HelpButtonInstance' +import { ButtonProps } from '../button/Button' +import { ModalProps } from '../modal/types' +import { usePropsWithContext } from '../../shared/hooks' + +const defaultProps = { + variant: 'secondary', + icon_position: 'left', +} + +export type HelpButtonProps = { + modal_content?: React.ReactNode + modal_props?: ModalProps +} & ButtonProps + +export default function HelpButton(localProps: HelpButtonProps) { + const getContent = (props: HelpButtonProps) => { + if (props.modal_content) { + return props.modal_content + } + return typeof props.children === 'function' + ? props.children(props) + : props.children + } + + const context = React.useContext(Context) + const props = usePropsWithContext(localProps, defaultProps) + const content = getContent(props) + + const { + modal_content, // eslint-disable-line + children, // eslint-disable-line + modal_props, + ...params + } = props + + if (params.icon === null) { + params.icon = 'question' + } + + if (content) { + if (!params.title) { + params.title = context.getTranslation(props).HelpButton.title + } + + return ( + + {content} + + ) + } + + return +} diff --git a/packages/dnb-eufemia/src/components/help-button/HelpButtonInstance.js b/packages/dnb-eufemia/src/components/help-button/HelpButtonInstance.js deleted file mode 100644 index 27d7ff3e97b..00000000000 --- a/packages/dnb-eufemia/src/components/help-button/HelpButtonInstance.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Web HelpButton Component - * - */ - -import React from 'react' -import PropTypes from 'prop-types' -import classnames from 'classnames' -import { - convertJsxToString, - extendPropsWithContext, -} from '../../shared/component-helper' -import Context from '../../shared/Context' -import { - spacingPropTypes, - createSpacingClasses, -} from '../space/SpacingHelper' -import Button, { buttonVariantPropType } from '../button/Button' - -export default class HelpButtonInstance extends React.PureComponent { - static contextType = Context - - static propTypes = { - id: PropTypes.string, - ...buttonVariantPropType, - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.node, - PropTypes.func, - ]), - icon_position: PropTypes.oneOf(['left', 'right']), - ...spacingPropTypes, - className: PropTypes.string, - class: PropTypes.string, - } - - static defaultProps = { - id: null, - variant: 'secondary', - icon: null, - icon_position: 'left', - className: null, - class: null, - } - - render() { - // use only the props from context, who are available here anyway - const props = extendPropsWithContext( - this.props, - HelpButtonInstance.defaultProps, - this.context.FormRow, - this.context.HelpButton - ) - - const { - size, - icon, - on_click, - className, - class: _className, - ...rest - } = props - - const params = { - className: classnames( - 'dnb-help-button', - createSpacingClasses(props), - className, - _className - ), - size, - icon, - ...rest, - } - - const isPotensialHelpButton = - !params.text || params.variant === 'tertiary' - - if (isPotensialHelpButton && !params.icon && params.icon !== false) { - params.icon = 'question' - } - - const isHelpButton = - isPotensialHelpButton && - ['question', 'information'].includes(String(params.icon)) - - if (isHelpButton) { - if (!params['aria-roledescription']) { - params['aria-roledescription'] = this.context.getTranslation( - this.props - ).HelpButton.aria_role - } - } - - if (!params.text && !params['aria-label']) { - let ariaLabel = convertJsxToString(props.title || props.children) - if (!ariaLabel) { - ariaLabel = this.context.getTranslation(this.props).HelpButton - .title - } - params['aria-label'] = ariaLabel - } - - if (icon === 'information' && !size) { - params.icon_size = 'medium' - } - if (params.title && !params.tooltip && params.tooltip !== false) { - params.tooltip = params.title - } - if (params.tooltip) { - params.title = null - } - - return - - - - -`; - exports[`HelpButton scss have to match default theme snapshot 1`] = ` "/* * Np theme is provided diff --git a/packages/dnb-eufemia/src/components/help-button/stories/HelpButton.stories.js b/packages/dnb-eufemia/src/components/help-button/stories/HelpButton.stories.tsx similarity index 99% rename from packages/dnb-eufemia/src/components/help-button/stories/HelpButton.stories.js rename to packages/dnb-eufemia/src/components/help-button/stories/HelpButton.stories.tsx index 4c6685df5aa..553ec799cda 100644 --- a/packages/dnb-eufemia/src/components/help-button/stories/HelpButton.stories.js +++ b/packages/dnb-eufemia/src/components/help-button/stories/HelpButton.stories.tsx @@ -6,7 +6,7 @@ import React from 'react' import { Wrapper, Box } from 'storybook-utils/helpers' -import { HelpButton, Modal, Button, Section, Input } from '../../' +import { HelpButton, Modal, Button, Section, Input } from '../..' export default { title: 'Eufemia/Components/HelpButton', diff --git a/packages/dnb-eufemia/src/components/info-card/InfoCard.tsx b/packages/dnb-eufemia/src/components/info-card/InfoCard.tsx index 4a191ccdcbc..ae19a184da3 100644 --- a/packages/dnb-eufemia/src/components/info-card/InfoCard.tsx +++ b/packages/dnb-eufemia/src/components/info-card/InfoCard.tsx @@ -102,8 +102,6 @@ export const defaultProps = { centered: false, skeleton: false, icon: LightbulbIcon, - closeButtonAttributes: null, - acceptButtonAttributes: null, } const InfoCard = (localProps: InfoCardProps & ISpacingProps) => { @@ -127,10 +125,9 @@ const InfoCard = (localProps: InfoCardProps & ISpacingProps) => { closeButtonAttributes, acceptButtonAttributes, ...props - } = usePropsWithContext( - { ...defaultProps, ...localProps }, - { skeleton: context?.skeleton } - ) + } = usePropsWithContext(localProps, defaultProps, { + skeleton: context?.skeleton, + }) const skeletonClasses = createSkeletonClass('shape', skeleton, context) const spacingClasses = createSpacingClasses(props) diff --git a/packages/dnb-eufemia/src/components/modal/Modal.tsx b/packages/dnb-eufemia/src/components/modal/Modal.tsx index e65bdf7ec98..ff3757fa195 100644 --- a/packages/dnb-eufemia/src/components/modal/Modal.tsx +++ b/packages/dnb-eufemia/src/components/modal/Modal.tsx @@ -32,6 +32,7 @@ import { classWithCamelCaseProps, ToCamelCasePartial, } from '../../shared/helpers/withCamelCaseProps' +import { ButtonProps } from '../button/Button' export const ANIMATION_DURATION = 300 @@ -40,7 +41,9 @@ interface ModalState { modalActive: boolean } -export type ModalPropTypes = ModalProps & ISpacingProps & ScrollViewProps +export type ModalPropTypes = ModalProps & + ISpacingProps & + Omit class Modal extends React.PureComponent< ModalPropTypes & ToCamelCasePartial, @@ -462,7 +465,7 @@ class Modal extends React.PureComponent< icon_position: trigger_icon_position, class: trigger_class, ...trigger_attributes, - } + } as ButtonProps if (isTrue(disabled)) { triggerAttributes.disabled = true } @@ -481,7 +484,13 @@ class Modal extends React.PureComponent< fallbackTitle = this.context.translation.HelpButton.title } - const TriggerButton = trigger ? trigger : HelpButtonInstance + const TriggerButton = trigger + ? (trigger as React.FC) + : HelpButtonInstance + + const title = ( + !triggerAttributes.text ? rest.title || fallbackTitle : null + ) as string return ( <> @@ -489,11 +498,7 @@ class Modal extends React.PureComponent< any) + children?: React.ReactNode } interface ModalRootState { diff --git a/packages/dnb-eufemia/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap b/packages/dnb-eufemia/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap index d5012bc8afb..0824a284331 100644 --- a/packages/dnb-eufemia/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap +++ b/packages/dnb-eufemia/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap @@ -816,10 +816,8 @@ exports[`Modal component have to match snapshot 1`] = ` onTouchStart={[Function]} title="modal_title" > - + - + diff --git a/packages/dnb-eufemia/src/components/visually-hidden/VisuallyHidden.tsx b/packages/dnb-eufemia/src/components/visually-hidden/VisuallyHidden.tsx index f81f3677c3e..f03ded02ab6 100644 --- a/packages/dnb-eufemia/src/components/visually-hidden/VisuallyHidden.tsx +++ b/packages/dnb-eufemia/src/components/visually-hidden/VisuallyHidden.tsx @@ -3,7 +3,8 @@ import classnames from 'classnames' // Shared import Context from '../../shared/Context' -import { extendPropsWithContext } from '../../shared/component-helper' +import { usePropsWithContext } from '../../shared/hooks/usePropsWithContext' +import { DynamicElement } from '../../shared/interfaces' export interface VisuallyHiddenProps { /** @@ -28,12 +29,10 @@ export interface VisuallyHiddenProps { * Root element of the component * Default: span */ - element?: string | React.ReactNode + element?: DynamicElement } export const defaultProps = { - className: null, - children: null, focusable: false, element: 'span', } @@ -44,8 +43,8 @@ const VisuallyHidden = (localProps: VisuallyHiddenProps) => { // Extract additional props from global context const { element, children, className, focusable, ...props } = - extendPropsWithContext( - { ...defaultProps, ...localProps }, + usePropsWithContext( + localProps, defaultProps, context?.translation?.VisuallyHidden, context?.VisuallyHidden @@ -58,7 +57,7 @@ const VisuallyHidden = (localProps: VisuallyHiddenProps) => { : 'dnb-visually-hidden--default', className ) - const Element = element + const Element = element || 'span' return ( , 'title'> { +export type ScrollViewProps = { className?: string - children?: string | React.ReactNode | ((...args: any[]) => any) - innerRef?: React.RefObject | React.ForwardedRef - class?: string + children?: React.ReactNode + style?: React.CSSProperties + innerRef?: React.ForwardedRef +} & ISpacingProps & + Partial, 'title'>> + +const defaultProps = { + children: null, } -class ScrollView extends React.PureComponent { - static tagName = 'dnb-scroll-view' - static contextType = Context - ref: React.RefObject +function ScrollView(localProps: ScrollViewProps) { + const context = React.useContext(Context) - static getContent(props) { - if (props.text) return props.text - return processChildren(props) - } + // use only the props from context, who are available here anyway + const props = usePropsWithContext( + localProps, + defaultProps, + context.FormRow, + context.ScrollView + ) - static defaultProps = { - className: null, - children: null, - innerRef: null, + const { children, className = null, innerRef, ...attributes } = props - class: null, + const mainParams: React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + > = { + className: classnames( + 'dnb-scroll-view', + createSpacingClasses(props), + className + ), + ...(attributes as React.HTMLAttributes), } - constructor(props) { - super(props) - this.ref = props.innerRef || React.createRef() + if (innerRef) { + mainParams.ref = innerRef as React.RefObject } - render() { - // use only the props from context, who are available here anyway - const props = extendPropsWithContext( - this.props, - ScrollView.defaultProps, - this.context.FormRow, - this.context.ScrollView - ) - - const { - className = null, - class: _className, - innerRef = null, // eslint-disable-line - ...attributes - } = props - - const contentToRender = ScrollView.getContent(props) + validateDOMAttributes(props, mainParams) - const mainParams = { - className: classnames( - 'dnb-scroll-view', - createSpacingClasses(props), - _className, - className - ), - ...attributes, - } - - validateDOMAttributes(this.props, mainParams) - - return ( -
- {contentToRender} -
- ) - } + return
{children}
} -export default React.forwardRef(function ScrollViewRef( - props: ScrollViewProps, - ref -) { - return +export default React.forwardRef((props: ScrollViewProps, ref) => { + return }) diff --git a/packages/dnb-eufemia/src/shared/Context.tsx b/packages/dnb-eufemia/src/shared/Context.tsx index b4b138d6094..8f877fce90a 100644 --- a/packages/dnb-eufemia/src/shared/Context.tsx +++ b/packages/dnb-eufemia/src/shared/Context.tsx @@ -28,6 +28,7 @@ import type { TooltipProps } from '../components/tooltip/types' // All TypeScript based Eufemia elements import type { AnchorProps } from '../elements/Anchor' +import type { ScrollViewProps } from '../fragments/scroll-view/ScrollView' export type ContextProps = { // -- All TypeScript based Eufemia components -- @@ -49,6 +50,7 @@ export type ContextProps = { Drawer?: Partial Dialog?: Partial Tooltip?: Partial + ScrollView?: Partial // -- TODO: Not converted yet -- diff --git a/packages/dnb-eufemia/src/shared/__tests__/component-helper.test.js b/packages/dnb-eufemia/src/shared/__tests__/component-helper.test.js index 0196a502646..6913d5d8db6 100644 --- a/packages/dnb-eufemia/src/shared/__tests__/component-helper.test.js +++ b/packages/dnb-eufemia/src/shared/__tests__/component-helper.test.js @@ -9,7 +9,6 @@ import { registerElement } from '../custom-element' import { isTrue, extend, - extendPropsWithContext, defineNavigator, validateDOMAttributes, processChildren, @@ -338,21 +337,6 @@ describe('"extend" should', () => { }) }) -describe('"extendPropsWithContext" should', () => { - it('extend prop from other context object', () => { - expect( - extendPropsWithContext( - { key: { x: 'y' }, foo: null }, // given props - { key: { x: 'y' }, foo: null }, // default props - { key: 'I can’t replace You', foo: 'bar' } - ) - ).toEqual({ - key: { x: 'y' }, - foo: 'bar', // because the prop was null, we get bar - }) - }) -}) - describe('"isTrue" should', () => { it('return true if we provide true as boolean', () => { expect(isTrue(true)).toBe(true) diff --git a/packages/dnb-eufemia/src/shared/component-helper.js b/packages/dnb-eufemia/src/shared/component-helper.js index 44276c07fbb..561083d35fc 100644 --- a/packages/dnb-eufemia/src/shared/component-helper.js +++ b/packages/dnb-eufemia/src/shared/component-helper.js @@ -15,7 +15,9 @@ import { import { getPreviousSibling } from './helpers/getPreviousSibling' import { init } from './Eufemia' +export { usePropsWithContext } from './hooks/usePropsWithContext' export { InteractionInvalidation } from './helpers/InteractionInvalidation' +export { extendPropsWithContext } from './helpers/extendPropsWithContext' export { registerElement } from './custom-element' export { getPreviousSibling, warn } @@ -277,37 +279,6 @@ export const extend = (...objects) => { }, first) } -// extends props from a given context -// but give the context second priority only -export const extendPropsWithContext = ( - props, - defaults = {}, - ...contexts -) => { - const context = contexts.reduce((acc, cur) => { - if (cur) { - acc = { ...acc, ...cur } - } - return acc - }, {}) - - return { - ...props, - ...Object.entries(context).reduce((acc, [key, value]) => { - if ( - // check if a prop of the same name exists - typeof props[key] !== 'undefined' && - // and if it was NOT defined as a component prop, because its still the same as the defaults - props[key] === defaults[key] - ) { - // then we use the context value - acc[key] = value - } - return acc - }, {}), - } -} - // check if value is "truthy" export const isTrue = (value) => { if ( diff --git a/packages/dnb-eufemia/src/shared/helpers/__tests__/extendPropsWithContext.test.ts b/packages/dnb-eufemia/src/shared/helpers/__tests__/extendPropsWithContext.test.ts new file mode 100644 index 00000000000..17e2b865fd5 --- /dev/null +++ b/packages/dnb-eufemia/src/shared/helpers/__tests__/extendPropsWithContext.test.ts @@ -0,0 +1,16 @@ +import { extendPropsWithContext } from '../extendPropsWithContext' + +describe('"extendPropsWithContext" should', () => { + it('extend prop from other context object', () => { + expect( + extendPropsWithContext( + { key: { x: 'y' }, foo: null }, // given props + { key: { x: 'y' }, foo: null }, // default props + { key: 'I can’t replace You', foo: 'bar' } + ) + ).toEqual({ + key: { x: 'y' }, + foo: 'bar', // because the prop was null, we get bar + }) + }) +}) diff --git a/packages/dnb-eufemia/src/shared/helpers/extendPropsWithContext.ts b/packages/dnb-eufemia/src/shared/helpers/extendPropsWithContext.ts new file mode 100644 index 00000000000..5036f7df538 --- /dev/null +++ b/packages/dnb-eufemia/src/shared/helpers/extendPropsWithContext.ts @@ -0,0 +1,40 @@ +export type DefaultsProps = Record +export type Contexts = Array> + +/** + * Extends props from a given context + * but give the context second priority only + * + * @param props object of component properties + * @param defaults object of potential default values + * @param contexts the rest of all context to merge + * @returns merged properties + */ +export function extendPropsWithContext( + props: Props, + defaults: DefaultsProps = {}, + ...contexts: Contexts +) { + const context = contexts.reduce((acc, cur) => { + if (cur) { + acc = { ...acc, ...cur } + } + return acc + }, {}) + + return { + ...props, + ...Object.entries(context).reduce((acc, [key, value]) => { + if ( + // check if a prop of the same name exists + typeof props[key] !== 'undefined' && + // and if it was NOT defined as a component prop, because its still the same as the defaults + props[key] === defaults[key] + ) { + // then we use the context value + acc[key] = value + } + return acc + }, {}), + } +} diff --git a/packages/dnb-eufemia/src/shared/interfaces.tsx b/packages/dnb-eufemia/src/shared/interfaces.tsx index 3a377f325bb..a23da138118 100644 --- a/packages/dnb-eufemia/src/shared/interfaces.tsx +++ b/packages/dnb-eufemia/src/shared/interfaces.tsx @@ -30,3 +30,7 @@ export type DataAttributeTypes = { */ // [property: `data-${string}`]: string } + +export type DynamicElement = + | keyof JSX.IntrinsicElements + | React.FunctionComponent