Skip to content

Commit

Permalink
[#285] strictFunctionTypes: true, fix 21 errors
Browse files Browse the repository at this point in the history
  • Loading branch information
darkwebdev committed Nov 22, 2023
1 parent 1163391 commit 7cc9914
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 88 deletions.
6 changes: 0 additions & 6 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,6 @@
"asyncArrow": "always"
}],
"template-curly-spacing": ["error", "never"],
"no-extra-parens": ["error", "all", {
"nestedBinaryExpressions": false,
"conditionalAssign": true,
"returnAssign": true,
"ignoreJSX": "all"
}],
"react/jsx-indent-props": [2, 4],
"react/jsx-indent": [2, 4],

Expand Down
57 changes: 41 additions & 16 deletions src/common/component-utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,49 @@
import { Children, FC, isValidElement, JSXElementConstructor, ReactNode } from 'react'
import { Children, FC, ForwardRefExoticComponent, isValidElement, JSXElementConstructor, ReactNode } from 'react'
import './array.polyfill.flat' // for Mobile Safari 11

export const findComponent = (nodes: ReactNode = [], componentType: FC<any>): ReactNode | undefined =>
Children.toArray(nodes).find(child =>
isValidElement(child) && child.type === componentType) || null
const componentName =
(component: FC<any> | JSXElementConstructor<any> | ForwardRefExoticComponent<any> | string): string | undefined => {
switch (typeof component) {
case 'string':
return component
case 'function':
return component.name
case 'symbol':
return (component as any).displayName
default:
return undefined
}
}

export const excludeComponent = (nodes: ReactNode = [], componentType: FC<any>): ReactNode[] =>
Children.toArray(nodes).filter(child =>
isValidElement(child) && child.type !== componentType)
// Returns props of a valid ReactElement
export const elementProps = (node: ReactNode): Record<string, any> => isValidElement(node) ? node.props : {}

export const filterByType = (nodes: ReactNode = [], componentType: FC<any> | FC<any>[]): ReactNode[] => {
const types = [componentType.toString()].flat()
return Children
.toArray(nodes)
.filter(child => isValidElement(child) && types.includes(child.type as string))
}
// Returns component type name or null
export const elementType = (node: ReactNode): string | null =>
isValidElement(node) ? componentName(node.type) : null

// Finds first node of certain component type
export const findComponent = (nodes: ReactNode = [], componentType: FC<any>): ReactNode | undefined =>
Children.toArray(nodes).find(child => elementType(child) === componentName(componentType)) || null

// Filters nodes by predicate
export const filterBy = (nodes: ReactNode = [], predicate: (el: ReactNode) => boolean): ReactNode[] =>
Children.toArray(nodes).filter(predicate)

export const elementProps = (node: ReactNode): Record<string, any> => isValidElement(node) ? node.props : {}
export const elementType = (node: ReactNode): string | JSXElementConstructor<any> | null =>
isValidElement(node) ? node.type : null
// Filters nodes by component type(s)
export const filterByType = (nodes: ReactNode = [], componentType: FC<any> | FC<any>[]): ReactNode[] => {
const types = [componentType]
.flat()
.map(comp => typeof comp === 'function' && comp.name)
.filter(Boolean)
return filterBy(nodes, child => types.includes(elementType(child)))
}

// Filters out nodes of certain component type(s)
export const excludeComponent = (nodes: ReactNode = [], componentType: FC<any> | FC<any>[]): ReactNode[] => {
const types = [componentType]
.flat()
.map(comp => typeof comp === 'function' && comp.name)
.filter(Boolean)
return filterBy(nodes, child => !types.includes(elementType(child)))
}
8 changes: 4 additions & 4 deletions src/common/tooltip-utils/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { cloneElement, ComponentProps, FC, isValidElement, RefObject } from 'react'
import classNames from 'classnames'
import { withForwardRef } from '../component-utils/forwardRef'
import { findComponent, excludeComponent } from '../component-utils'
import { findComponent, excludeComponent, elementProps } from '../component-utils'
import { TooltipType } from './types'
import TooltipHost from './tooltip-host'

Expand All @@ -27,12 +27,11 @@ const Tooltip: FC<TooltipProps> = ({
}

const host =
isValidElement(content) &&
isValidElement(originalHostComponent) &&
cloneElement(originalHostComponent, {
className: `${type}__host`,
'aria-expanded': isExpanded,
'aria-describedby': content?.props?.id,
'aria-describedby': elementProps(content).id,
...originalHostComponent.props
})

Expand All @@ -42,7 +41,8 @@ const Tooltip: FC<TooltipProps> = ({
ref={forwardedRef}
className={classNames(className, type, {
[`${type}--expanded`]: isExpanded
})}>
})}
>
{host}
{content}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,6 @@ exports[`Storyshots ebay-infotip Modal 1`] = `
arial-modal="true"
class="drawer-dialog dialog--mini__overlay drawer-dialog--mask-fade-slow"
hidden=""
mode="mini"
role="dialog"
>
<div
Expand Down
43 changes: 40 additions & 3 deletions src/ebay-infotip/__tests__/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import React from 'react'
import requireContext from 'node-require-context'
import { render, fireEvent, RenderResult } from '@testing-library/react'
import { initStoryshots } from '../../../config/jest/storyshots'
import { EbayInfotip, EbayInfotipContent, EbayInfotipHeading } from '../index'
import { EbayInfotip, EbayInfotipContent, EbayInfotipHeading, EbayInfotipHost, EbayInfotipProps } from '../index'

jest.mock('../../common/random-id', () => ({ randomId: () => 'abc123' }))

const renderComponent = (props?: any) => render(
<EbayInfotip {...props}>
const renderComponent = (props?: Partial<EbayInfotipProps>) => render(
<EbayInfotip a11yCloseText="Close" {...props}>
<EbayInfotipHeading>Title</EbayInfotipHeading>
<EbayInfotipContent>
<p>Info content</p>
Expand Down Expand Up @@ -63,6 +63,43 @@ describe('<EbayInfotip>', () => {
})
})

describe('on modal variant', () => {
it('should fire an event', () => {
const spy = jest.fn()
const wrapper = renderComponent({ variant: 'modal', onExpand: spy })
fireEvent.click(wrapper.container.querySelector('button.infotip__host'))

expect(spy).toBeCalled()
})
})

describe('on custom button content', () => {
it('should overwrite aria-label', () => {
const wrapper = render(
<EbayInfotip
pointer="top-left"
a11yCloseText="Close"
aria-label="Wrong aria-label, should be overwritten"
>
<EbayInfotipHost aria-label="Click to open infotip">
{({ icon }: any) => (
<span>
{icon}
<span style={{ marginLeft: 5 }}>Click me</span>
</span>
)}
</EbayInfotipHost>
<EbayInfotipContent>
<EbayInfotipHeading>Title</EbayInfotipHeading>
<p>Use Access Key &apos;S&apos; to display settings.</p>
</EbayInfotipContent>
</EbayInfotip>
)

expect(wrapper.container.querySelector('button.infotip__host')).toHaveAttribute('aria-label', 'Click to open infotip')
})
})

describe('on using the infotip with no content', () => {
it('should throw an error', () => {
jest.spyOn(console, 'error').mockImplementation(() => null)
Expand Down
8 changes: 2 additions & 6 deletions src/ebay-infotip/ebay-infotip-host.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { ComponentProps, FC, RefObject, ReactNode } from 'react'
import classNames from 'classnames/dedupe'
import { EbayIcon, Icon } from '../ebay-icon'
import { withForwardRef } from '../common/component-utils/forwardRef'
import { withForwardRef } from '../common/component-utils'
import { Variant } from './types'

export type InfotipHostProps = ComponentProps<'button'> & {
Expand All @@ -21,11 +21,7 @@ const EbayInfotipHost: FC<InfotipHostProps> = ({
}) => {
const classPrefix = variant === 'modal' ? 'dialog--mini' : 'infotip'
const buttonIcon = <EbayIcon name={icon} />
let buttonContent = children

if (children instanceof Function) {
buttonContent = children({ icon: buttonIcon })
}
const buttonContent = children instanceof Function ? children({ icon: buttonIcon }) : children

return (
<button
Expand Down
63 changes: 26 additions & 37 deletions src/ebay-infotip/ebay-infotip.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,13 @@
import React, { cloneElement, createElement, CSSProperties, FC, useRef, ReactNode, isValidElement } from 'react'
import React, { cloneElement, createElement, FC, useRef, isValidElement } from 'react'
import classNames from 'classnames'
import { elementProps, findComponent } from '../common/component-utils'
import { Tooltip, TooltipHost, TooltipContent, PointerDirection, useTooltip } from '../common/tooltip-utils'
import { Tooltip, TooltipHost, TooltipContent, useTooltip } from '../common/tooltip-utils'
import { EbayDrawerDialog } from '../ebay-drawer-dialog'
import { EbayDialogHeader } from '../ebay-dialog-base'
import EbayInfotipHost, { InfotipHostProps } from './ebay-infotip-host'
import { Icon } from '../ebay-icon'
import { Variant } from './types'
import { EbayInfotipHeading, EbayInfotipContent } from './index'
import { EbayInfotipHeading, EbayInfotipContent, EbayInfotipProps } from './index'

type InfotipProps = {
variant?: Variant;
icon?: Icon;
disabled?: boolean;
initialExpanded?: boolean;
pointer?: PointerDirection;
overlayStyle?: CSSProperties;
onExpand?: () => void;
onCollapse?: () => void;
a11yCloseText: string;
'aria-label'?: string;
className?: string;
children?: ReactNode;
a11yMaximizeText?:string;
a11yMinimizeText?:string;
};

const EbayInfotip: FC<InfotipProps> = ({
const EbayInfotip: FC<EbayInfotipProps> = ({
variant = 'default',
pointer,
overlayStyle,
Expand All @@ -41,7 +22,7 @@ const EbayInfotip: FC<InfotipProps> = ({
className,
a11yMaximizeText,
a11yMinimizeText
}: InfotipProps) => {
}: EbayInfotipProps) => {
const buttonRef = useRef()
const {
isExpanded,
Expand All @@ -53,7 +34,7 @@ const EbayInfotip: FC<InfotipProps> = ({
const containerRef = useRef()
const heading = findComponent(children, EbayInfotipHeading)
const content = findComponent(children, EbayInfotipContent)
const button = findComponent(children, EbayInfotipHost) || createElement(EbayInfotipHost)
const button = findComponent(children, EbayInfotipHost)

const toggleTooltip = () => {
if (isExpanded) {
Expand All @@ -69,25 +50,33 @@ const EbayInfotip: FC<InfotipProps> = ({

const { children: contentChildren, ...contentProps } = elementProps(content)

const buttonProps = {
ref: buttonRef,
onClick: toggleTooltip,
disabled,
variant,
'aria-label': ariaLabel,
'aria-expanded': isExpanded,
icon
}

const hostButton = isValidElement(button) ?
cloneElement<InfotipHostProps>(button, {
...buttonProps,
...button.props
}) :
createElement(EbayInfotipHost, { ...buttonProps })

return (
<>

<Tooltip
type="infotip"
isExpanded={isExpanded}
className={classNames(className, { 'dialog--mini': isModal })}
ref={containerRef}>
ref={containerRef}
>
<TooltipHost>
{isValidElement(button) && cloneElement<InfotipHostProps>(button, {
ref: buttonRef,
onClick: toggleTooltip,
disabled,
variant,
'aria-label': ariaLabel,
'aria-expanded': isExpanded,
icon,
...elementProps(button)
})}
{hostButton}
</TooltipHost>
{!isModal && (
<TooltipContent
Expand Down
2 changes: 1 addition & 1 deletion src/ebay-infotip/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export { default as EbayInfotip } from './ebay-infotip'
export { default as EbayInfotipHost } from './ebay-infotip-host'
export { default as EbayInfotipHeading } from './ebay-infotip-heading'
export { default as EbayInfotipContent } from './ebay-infotip-content'
export { Variant } from './types'
export type { Variant, EbayInfotipProps } from './types'
21 changes: 21 additions & 0 deletions src/ebay-infotip/types.ts
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
import { CSSProperties, ReactNode } from 'react'
import { PointerDirection } from '../common/tooltip-utils'
import { Icon } from '../ebay-icon'

export type Variant = 'default' | 'modal'

export type EbayInfotipProps = {
variant?: Variant;
icon?: Icon;
disabled?: boolean;
initialExpanded?: boolean;
pointer?: PointerDirection;
overlayStyle?: CSSProperties;
onExpand?: () => void;
onCollapse?: () => void;
a11yCloseText: string;
'aria-label'?: string;
className?: string;
children?: ReactNode;
a11yMaximizeText?:string;
a11yMinimizeText?:string;
}
5 changes: 2 additions & 3 deletions src/ebay-page-notice/page-notice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import React, {
} from 'react'
import NoticeContent from '../common/notice-utils/notice-content'
import { EbayNoticeContent } from '../ebay-notice-base/components/ebay-notice-content'
import { elementProps, findComponent } from '../common/component-utils'
import { EbayIcon, Icon } from '../ebay-icon'
import { EbayPageNoticeFooter } from './index'
import { elementProps, elementType } from '../common/component-utils'

export type PageNoticeStatus = 'general' | 'attention' | 'confirmation' | 'information'
export type Props = ComponentProps<'section'> & {
Expand All @@ -31,8 +31,7 @@ const EbayPageNotice: FC<Props> = ({
...rest
}) => {
const [dismissed, setDismissed] = useState(false)
const childrenArray = React.Children.toArray(children)
const content = childrenArray.find(child => elementType(child) === EbayNoticeContent)
const content = findComponent(children, EbayNoticeContent)

if (!content) {
throw new Error(`EbayPageNotice: Please use a EbayNoticeContent that defines the content of the notice`)
Expand Down
Loading

0 comments on commit 7cc9914

Please sign in to comment.