diff --git a/libraries/core-react/src/Accordion/Accordion.jsx b/libraries/core-react/src/Accordion/Accordion.jsx index 6ef1751f44..d4bce75267 100644 --- a/libraries/core-react/src/Accordion/Accordion.jsx +++ b/libraries/core-react/src/Accordion/Accordion.jsx @@ -2,42 +2,50 @@ import React, { forwardRef, useMemo, useState } from 'react' import createId from 'lodash.uniqueid' import { commonPropTypes, commonDefaultProps } from './Accordion.propTypes' -const Accordion = forwardRef(function Accordion( - { headerLevel, chevronPosition, children, ...props }, - ref, -) { - const accordionId = useMemo(() => createId('accordion-'), []) - - const [focusVisible, setFocusVisible] = useState(true) - - const handleFocusVisible = (isFocusVisible) => { - setFocusVisible(isFocusVisible) - } - - const AccordionItems = React.Children.map(children, (child, index) => { - return React.cloneElement(child, { - accordionId, - index, - headerLevel, - chevronPosition, - focusVisible, - handleFocusVisible, +const Accordion = forwardRef( + /** + * @param {import('./Accordion.propTypes').CommonProps & React.HTMLAttributes} props + * @param ref + */ + function Accordion({ headerLevel, chevronPosition, children, ...rest }, ref) { + const accordionId = useMemo(() => createId('accordion-'), []) + + const [focusVisible, setFocusVisible] = useState(true) + + const handleFocusVisible = (isFocusVisible) => { + setFocusVisible(isFocusVisible) + } + + const AccordionItems = React.Children.map(children, (child, index) => { + return React.cloneElement( + /** @type {React.ReactElement} */ (child), + { + accordionId, + index, + headerLevel, + chevronPosition, + focusVisible, + handleFocusVisible, + }, + ) }) - }) - return ( -
- {AccordionItems} -
- ) -}) + return ( +
+ {AccordionItems} +
+ ) + }, +) Accordion.displayName = 'eds-accordion' +// @ts-ignore Accordion.propTypes = { ...commonPropTypes, } +// @ts-ignore Accordion.defaultProps = { ...commonDefaultProps, } diff --git a/libraries/core-react/src/Accordion/Accordion.propTypes.js b/libraries/core-react/src/Accordion/Accordion.propTypes.js index 75a0926b47..eb35af9eb1 100644 --- a/libraries/core-react/src/Accordion/Accordion.propTypes.js +++ b/libraries/core-react/src/Accordion/Accordion.propTypes.js @@ -1,5 +1,11 @@ import PropTypes from 'prop-types' +/** + * @typedef CommonProps + * @prop {'left' | 'right'} [chevronPosition] Which side the chevron should be on + * @prop {'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'} [headerLevel] The header level, i.e. h1, h2, h3 etc. + */ + export const commonPropTypes = { /** Which side the chevron should be on */ chevronPosition: PropTypes.oneOf(['left', 'right']), diff --git a/libraries/core-react/src/Accordion/Accordion.test.jsx b/libraries/core-react/src/Accordion/Accordion.test.tsx similarity index 100% rename from libraries/core-react/src/Accordion/Accordion.test.jsx rename to libraries/core-react/src/Accordion/Accordion.test.tsx diff --git a/libraries/core-react/src/Accordion/AccordionHeader.jsx b/libraries/core-react/src/Accordion/AccordionHeader.jsx index cc44f127c4..7bac6ddb33 100644 --- a/libraries/core-react/src/Accordion/AccordionHeader.jsx +++ b/libraries/core-react/src/Accordion/AccordionHeader.jsx @@ -70,92 +70,113 @@ const AccordionHeaderTitle = styled.span` AccordionHeaderTitle.displayName = 'eds-accordion-headertitle' -const AccordionHeader = forwardRef(function AccordionHeader( - { - parentIndex, - headerLevel, - chevronPosition, - panelId, - id, - isExpanded, - children, - toggleExpanded, - disabled, - focusVisible, - handleFocusVisible, - ...props - }, - ref, -) { - const handleClick = () => { - handleFocusVisible(false) - if (!disabled) { - toggleExpanded() +/** + * @typedef Props + * @prop {string} [id] The id of the button that toggles expansion + * @prop {boolean} [isExpanded] Is AccordionItem expanded + * @prop {string} [panelId] The panel that is controlled by the HeaderButton + * @prop {React.ReactNode} children + * @prop {number} [parentIndex] The index of the parent AccordionItem + * @prop {boolean} [disabled] accordion item is disabled + * @prop {boolean} [focusVisible] + * @prop {(isFocusVisible: boolean) => void} [handleFocusVisible] + * @prop {() => void} [toggleExpanded] + */ + +const AccordionHeader = forwardRef( + /** + * @param {Props & import('./Accordion.propTypes').CommonProps & React.HTMLAttributes} props + * @param ref + */ + function AccordionHeader( + { + parentIndex, + headerLevel, + chevronPosition, + panelId, + id, + isExpanded, + children, + toggleExpanded, + disabled, + focusVisible, + handleFocusVisible, + ...rest + }, + ref, + ) { + const handleClick = () => { + handleFocusVisible(false) + if (!disabled) { + toggleExpanded() + } } - } - - const handleMouseDown = () => { - handleFocusVisible(false) - } - - const handleKeyDown = (event) => { - const { key } = event - handleFocusVisible(true) - if (key === 'Enter' || key === ' ') { - toggleExpanded() - event.preventDefault() + + const handleMouseDown = () => { + handleFocusVisible(false) + } + + const handleKeyDown = (event) => { + const { key } = event + handleFocusVisible(true) + if (key === 'Enter' || key === ' ') { + toggleExpanded() + event.preventDefault() + } } - } - - const chevron = ( - - ) - - const headerChildren = React.Children.map(children, (child) => { + + const chevron = ( + + ) + + const headerChildren = React.Children.map(children, (child) => { + return ( + (typeof child === 'string' && ( + + {child} + + )) || + // @ts-ignore + (child.type.displayName === 'eds-accordion-headertitle' && + React.cloneElement(/** @type {React.ReactElement} */ (child), { + isExpanded, + disabled, + })) || + child + ) + }) + + const newChildren = [chevron, headerChildren] + return ( - (typeof child === 'string' && ( - - {child} - - )) || - (child.type.displayName === 'eds-accordion-headertitle' && - React.cloneElement(child, { - isExpanded, - disabled, - })) || - child + + {chevronPosition === 'left' ? newChildren : newChildren.reverse()} + ) - }) - - const newChildren = [chevron, headerChildren] - - return ( - - {chevronPosition === 'left' ? newChildren : newChildren.reverse()} - - ) -}) + }, +) AccordionHeader.displayName = 'eds-accordion-header' +// @ts-ignore AccordionHeader.propTypes = { ...commonPropTypes, /** The id of the button that toggles expansion */ @@ -174,8 +195,10 @@ AccordionHeader.propTypes = { focusVisible: PropTypes.bool, /** @ignore */ handleFocusVisible: PropTypes.func, + toggleExpanded: PropTypes.func, } +// @ts-ignore AccordionHeader.defaultProps = { ...commonDefaultProps, id: '', @@ -185,6 +208,7 @@ AccordionHeader.defaultProps = { disabled: false, focusVisible: true, handleFocusVisible: () => {}, + toggleExpanded: () => {}, } export { AccordionHeader, AccordionHeaderTitle } diff --git a/libraries/core-react/src/Accordion/AccordionItem.jsx b/libraries/core-react/src/Accordion/AccordionItem.jsx index ea7cf02271..03a5a74e4f 100644 --- a/libraries/core-react/src/Accordion/AccordionItem.jsx +++ b/libraries/core-react/src/Accordion/AccordionItem.jsx @@ -2,60 +2,78 @@ import React, { forwardRef, useState } from 'react' import PropTypes from 'prop-types' import { commonPropTypes, commonDefaultProps } from './Accordion.propTypes' -const AccordionItem = forwardRef(function AccordionItem( - { - headerLevel, - chevronPosition, - index, - accordionId, - isExpanded, - children, - disabled, - focusVisible, - handleFocusVisible, - ...props - }, - ref, -) { - const [expanded, setExpanded] = useState(isExpanded) +/** + * @typedef Props + * @prop {string} [accordionId] The ID of the {@link Accordion} + * @prop {boolean} [isExpanded] Is {@link AccordionItem} expanded + * @prop {number} [index] The {@link AccordionItem}’s index in the {@link Accordion} + * @prop {React.ReactNode} [children] + * @prop {boolean} [disabled] {@link AccordionItem} is disabled + * @prop {boolean} [focusVisible] + * @prop {(isFocusVisible: boolean) => void} [handleFocusVisible] + */ + +const AccordionItem = forwardRef( + /** + * @param {Props & import('./Accordion.propTypes').CommonProps & React.HTMLAttributes} props + * @param ref + */ + function AccordionItem( + { + headerLevel, + chevronPosition, + index, + accordionId, + isExpanded, + children, + disabled, + focusVisible, + handleFocusVisible, + ...rest + }, + ref, + ) { + const [expanded, setExpanded] = useState(isExpanded) - const toggleExpanded = () => { - setExpanded(!expanded) - } + const toggleExpanded = () => { + setExpanded(!expanded) + } - const Children = React.Children.map(children, (child, childIndex) => { - const headerId = `${accordionId}-header-${index + 1}` - const panelId = `${accordionId}-panel-${index + 1}` + const Children = React.Children.map(children, (child, childIndex) => { + const headerId = `${accordionId}-header-${index + 1}` + const panelId = `${accordionId}-panel-${index + 1}` - return childIndex === 0 - ? React.cloneElement(child, { - isExpanded: expanded, - toggleExpanded, - id: headerId, - panelId, - headerLevel, - chevronPosition, - parentIndex: index, - disabled, - focusVisible, - handleFocusVisible, - }) - : React.cloneElement(child, { - hidden: !expanded, - id: panelId, - headerId, - }) - }) + return childIndex === 0 + ? React.cloneElement(/** @type {React.ReactElement} */ (child), { + isExpanded: expanded, + toggleExpanded, + id: headerId, + panelId, + headerLevel, + chevronPosition, + parentIndex: index, + disabled, + focusVisible, + handleFocusVisible, + }) + : React.cloneElement(/** @type {React.ReactElement} */ (child), { + hidden: !expanded, + id: panelId, + headerId, + }) + }) - return ( -
- {Children} -
- ) -}) + return ( +
+ {Children} +
+ ) + }, +) AccordionItem.displayName = 'eds-accordion-item' +// @ts-ignore AccordionItem.propTypes = { ...commonPropTypes, /** The ID of the Accordion */ @@ -74,6 +92,7 @@ AccordionItem.propTypes = { handleFocusVisible: PropTypes.func, } +// @ts-ignore AccordionItem.defaultProps = { ...commonDefaultProps, disabled: false, diff --git a/libraries/core-react/src/Accordion/AccordionPanel.jsx b/libraries/core-react/src/Accordion/AccordionPanel.jsx index b72adbf6b1..5023242c5f 100644 --- a/libraries/core-react/src/Accordion/AccordionPanel.jsx +++ b/libraries/core-react/src/Accordion/AccordionPanel.jsx @@ -23,22 +23,33 @@ const StyledAccordionPanel = styled.div.attrs(({ headerId }) => ({ box-sizing: border-box; ` -const AccordionPanel = forwardRef(function AccordionPanel( - { id, headerId, hidden, children, ...props }, - ref, -) { - return ( - - ) -}) +/** + * @typedef Props + * @prop {string} [headerId] The ID of the element that controls the panel + * @prop {string} [id] The ID of the panel + * @prop {boolean} [hidden] If `true`, the panel will be hidden + * @prop {React.ReactNode} children + */ + +const AccordionPanel = forwardRef( + /** + * @param {Props & React.HTMLAttributes} props + * @param ref + */ + function AccordionPanel({ id, headerId, hidden, children, ...rest }, ref) { + return ( + + ) + }, +) AccordionPanel.displayName = 'eds-accordion-panel' diff --git a/libraries/core-react/src/Accordion/index.js b/libraries/core-react/src/Accordion/index.js index de8207eea1..6809596e54 100644 --- a/libraries/core-react/src/Accordion/index.js +++ b/libraries/core-react/src/Accordion/index.js @@ -1,8 +1,12 @@ -import { Accordion } from './Accordion' +import { Accordion as AccordionComponent } from './Accordion' import { AccordionItem } from './AccordionItem' import { AccordionHeaderTitle, AccordionHeader } from './AccordionHeader' import { AccordionPanel } from './AccordionPanel' +/** @type {typeof import('./types').Accordion} */ +// @ts-ignore +const Accordion = AccordionComponent + Accordion.AccordionItem = AccordionItem Accordion.AccordionHeader = AccordionHeader Accordion.AccordionHeaderTitle = AccordionHeaderTitle diff --git a/libraries/core-react/src/Accordion/types.ts b/libraries/core-react/src/Accordion/types.ts new file mode 100644 index 0000000000..1fe88b5fe8 --- /dev/null +++ b/libraries/core-react/src/Accordion/types.ts @@ -0,0 +1,11 @@ +import { Accordion as AccordionComponent } from './Accordion' +import { AccordionItem } from './AccordionItem' +import { AccordionHeaderTitle, AccordionHeader } from './AccordionHeader' +import { AccordionPanel } from './AccordionPanel' + +export declare const Accordion: typeof AccordionComponent & { + AccordionItem: typeof AccordionItem + AccordionHeaderTitle: typeof AccordionHeaderTitle + AccordionHeader: typeof AccordionHeader + AccordionPanel: typeof AccordionPanel +}