From 93952b98d46b7c61d2b5bba755be2cc806a8aded Mon Sep 17 00:00:00 2001 From: danoro96 <52253150+danoro96@users.noreply.github.com> Date: Mon, 4 Dec 2023 14:42:58 -0700 Subject: [PATCH] refactor: Add header panel types (#15096) * refactor: add header panel types * refactor: Fix README.md * refactor: remove extra list start * made props optional * fix(headerpanel): update typings, add comments --------- Co-authored-by: Alison Joseph Co-authored-by: Andrea N. Cardona Co-authored-by: Taylor Jones Co-authored-by: Taylor Jones --- .all-contributorsrc | 9 + .../src/components/UIShell/HeaderPanel.js | 132 ------------- .../src/components/UIShell/HeaderPanel.tsx | 180 ++++++++++++++++++ 3 files changed, 189 insertions(+), 132 deletions(-) delete mode 100644 packages/react/src/components/UIShell/HeaderPanel.js create mode 100644 packages/react/src/components/UIShell/HeaderPanel.tsx diff --git a/.all-contributorsrc b/.all-contributorsrc index 17a90740dd7d..f4997f9b1376 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1335,6 +1335,15 @@ ] }, { + "login": "danoro96", + "name": "Daniel Castillo", + "avatar_url": "https://avatars.githubusercontent.com/u/52253150?v=4", + "profile": "https://github.com/danoro96", + "contributions": [ + "code" + ] + }, + { "login": "kuri-sun", "name": "Ruki", "avatar_url": "https://avatars.githubusercontent.com/u/62743644?v=4", diff --git a/packages/react/src/components/UIShell/HeaderPanel.js b/packages/react/src/components/UIShell/HeaderPanel.js deleted file mode 100644 index 467cfcc3b22b..000000000000 --- a/packages/react/src/components/UIShell/HeaderPanel.js +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2023 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React, { useRef, useState } from 'react'; -import cx from 'classnames'; -import PropTypes from 'prop-types'; -import { usePrefix } from '../../internal/usePrefix'; -import { keys, match } from '../../internal/keyboard'; -import { useWindowEvent } from '../../internal/useEvent'; -import { useMergedRefs } from '../../internal/useMergedRefs'; - -const noopFn = () => {}; -const HeaderPanel = React.forwardRef(function HeaderPanel( - { - children, - className: customClassName, - expanded, - addFocusListeners = true, - onHeaderPanelFocus = noopFn, - href, - ...other - }, - ref -) { - const prefix = usePrefix(); - const headerPanelReference = useRef(null); - const headerPanelRef = useMergedRefs([headerPanelReference, ref]); - - const controlled = useRef(expanded !== undefined).current; - const [expandedState, setExpandedState] = useState(expanded); - const expandedProp = controlled ? expanded : expandedState; - - const [lastClickedElement, setLastClickedElement] = useState(null); - - const className = cx(`${prefix}--header-panel`, { - [`${prefix}--header-panel--expanded`]: expandedProp, - [customClassName]: !!customClassName, - }); - - const eventHandlers = {}; - - if (addFocusListeners) { - eventHandlers.onBlur = (event) => { - if ( - !event.currentTarget.contains(event.relatedTarget) && - !lastClickedElement?.classList?.contains('cds--switcher__item-link') - ) { - setExpandedState(false); - setLastClickedElement(null); - if (expanded) { - onHeaderPanelFocus(); - } - } - }; - eventHandlers.onKeyDown = (event) => { - if (match(event, keys.Escape)) { - setExpandedState(false); - onHeaderPanelFocus(); - if (href) { - window.location.href = href; - } - } - }; - } - - useWindowEvent('click', () => { - const focusedElement = document.activeElement; - setLastClickedElement(focusedElement); - - if ( - children.type?.displayName === 'Switcher' && - !focusedElement?.closest(`.${prefix}--header-panel--expanded`) && - !focusedElement?.closest(`.${prefix}--header__action`) && - !headerPanelRef?.current?.classList.contains(`${prefix}--switcher`) && - expanded - ) { - setExpandedState(false); - onHeaderPanelFocus(); - } - }); - - return ( -
- {children} -
- ); -}); - -HeaderPanel.propTypes = { - /** - * Specify whether focus and blur listeners are added. They are by default. - */ - addFocusListeners: PropTypes.bool, - - /** - * The content that will render inside of the `HeaderPanel` - */ - children: PropTypes.node, - - /** - * Optionally provide a custom class to apply to the underlying `
  • ` node - */ - className: PropTypes.string, - - /** - * Specify whether the panel is expanded - */ - expanded: PropTypes.bool, - - /** - * Provide the `href` to the id of the element on your package that could - * be target. - */ - href: PropTypes.string, - - /** - * An optional listener that is called a callback to collapse the HeaderPanel - */ - onHeaderPanelFocus: PropTypes.func, -}; - -HeaderPanel.displayName = 'HeaderPanel'; - -export default HeaderPanel; diff --git a/packages/react/src/components/UIShell/HeaderPanel.tsx b/packages/react/src/components/UIShell/HeaderPanel.tsx new file mode 100644 index 000000000000..6581045269b5 --- /dev/null +++ b/packages/react/src/components/UIShell/HeaderPanel.tsx @@ -0,0 +1,180 @@ +/** + * Copyright IBM Corp. 2016, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import cx from 'classnames'; +import PropTypes from 'prop-types'; +import React, { + useRef, + useState, + ReactNode, + type ComponentProps, + type ForwardedRef, +} from 'react'; +import { usePrefix } from '../../internal/usePrefix'; +import { keys, match } from '../../internal/keyboard'; +import { useWindowEvent } from '../../internal/useEvent'; +import { useMergedRefs } from '../../internal/useMergedRefs'; + +interface HeaderPanelProps { + /** + * Specify whether focus and blur listeners are added. They are by default. + */ + addFocusListeners?: boolean; + + /** + * The content that will render inside of the `HeaderPanel` + */ + children?: ReactNode; + + /** + * Optionally provide a custom class to apply to the underlying `
  • ` node + */ + className?: string; + + /** + * Specify whether the panel is expanded + */ + expanded?: boolean; + + /** + * Provide the `href` to the id of the element on your package that could + * be target. + */ + href?: string; + + /** + * An optional listener that is called a callback to collapse the HeaderPanel + */ + onHeaderPanelFocus?: () => void; +} + +const noopFn = () => {}; +const HeaderPanel: React.FC = React.forwardRef( + function HeaderPanel( + { + children, + className: customClassName, + expanded, + addFocusListeners = true, + onHeaderPanelFocus = noopFn, + href, + ...rest + }, + ref: ForwardedRef + ) { + const prefix = usePrefix(); + const headerPanelReference = useRef(null); + const headerPanelRef = useMergedRefs([headerPanelReference, ref]); + + const controlled = useRef(expanded !== undefined).current; + const [expandedState, setExpandedState] = useState(expanded); + const expandedProp = controlled ? expanded : expandedState; + + const [lastClickedElement, setLastClickedElement] = + useState(null); + + const className = cx(`${prefix}--header-panel`, { + [`${prefix}--header-panel--expanded`]: expandedProp, + [customClassName as string]: !!customClassName, + }); + + const eventHandlers: Partial< + Pick, 'onBlur' | 'onKeyDown'> + > = {}; + + if (addFocusListeners) { + eventHandlers.onBlur = (event) => { + if ( + !event.currentTarget.contains(event.relatedTarget) && + !lastClickedElement?.classList?.contains('cds--switcher__item-link') + ) { + setExpandedState(false); + setLastClickedElement(null); + if (expanded) { + onHeaderPanelFocus(); + } + } + }; + eventHandlers.onKeyDown = (event) => { + if (match(event, keys.Escape)) { + setExpandedState(false); + onHeaderPanelFocus(); + if (href) { + window.location.href = href; + } + } + }; + } + + useWindowEvent('click', () => { + const focusedElement = document.activeElement as HTMLElement; + setLastClickedElement(focusedElement); + + const childJsxElement = children as JSX.Element; + + if ( + childJsxElement.type?.displayName === 'Switcher' && + !focusedElement?.closest(`.${prefix}--header-panel--expanded`) && + !focusedElement?.closest(`.${prefix}--header__action`) && + !headerPanelReference?.current?.classList.contains( + `${prefix}--switcher` + ) && + expanded + ) { + setExpandedState(false); + onHeaderPanelFocus(); + } + }); + + return ( +
    + {children} +
    + ); + } +); + +HeaderPanel.propTypes = { + /** + * Specify whether focus and blur listeners are added. They are by default. + */ + addFocusListeners: PropTypes.bool, + + /** + * The content that will render inside of the `HeaderPanel` + */ + children: PropTypes.node, + + /** + * Optionally provide a custom class to apply to the underlying `
  • ` node + */ + className: PropTypes.string, + + /** + * Specify whether the panel is expanded + */ + expanded: PropTypes.bool, + + /** + * Provide the `href` to the id of the element on your package that could + * be target. + */ + href: PropTypes.string, + + /** + * An optional listener that is called a callback to collapse the HeaderPanel + */ + onHeaderPanelFocus: PropTypes.func, +}; + +HeaderPanel.displayName = 'HeaderPanel'; + +export default HeaderPanel;