Skip to content

Commit

Permalink
refactor(Tearsheet): convert tearsheet shell to typescript (#4751)
Browse files Browse the repository at this point in the history
* refactor: start of tearsheet migration

* chore: tearsheet ts migration work

* refactor(Tearsheet): add stack types and proper element types in Wrap

* fix(TearsheetShell): change  value to undefined

* fix: update tearsheet narrow types

* chore: update cspell

Co-authored-by: elysia <[email protected]>

* refactor: remove prop specified in separate type

---------

Co-authored-by: elysia <[email protected]>
  • Loading branch information
matthewgallo and elycheea authored Apr 30, 2024
1 parent 63becd7 commit 1871965
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 50 deletions.
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"dasharray",
"data-testid",
"datagrid",
"denormalized",
"disttag",
"disttags",
"dragbar",
Expand Down
23 changes: 7 additions & 16 deletions packages/ibm-products/src/components/Tearsheet/TearsheetNarrow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

// Import portions of React that are needed.
import React, { ReactNode, PropsWithChildren } from 'react';
import React, { ReactNode, PropsWithChildren, ForwardedRef } from 'react';

// Other standard imports.
import PropTypes from 'prop-types';
Expand All @@ -25,16 +25,12 @@ import {
tearsheetHasCloseIcon,
TearsheetShell,
tearsheetShellWideProps as blocked,
CloseIconDescriptionTypes,
} from './TearsheetShell';

import { portalType } from './TearsheetShell';

type closeIconDescriptionTypes = {
hasCloseIcon: true;
closeIconDescription: string;
};

interface TearsheetNarrowProps extends PropsWithChildren {
interface TearsheetNarrowBaseProps extends PropsWithChildren {
/**
* The navigation actions to be shown as buttons in the action area at the
* bottom of the tearsheet. Each action is specified as an object with
Expand All @@ -61,14 +57,6 @@ interface TearsheetNarrowProps extends PropsWithChildren {
*/
className?: string;

/**
* The accessibility title for the close icon (if shown).
*
* **Note:** This prop is only required if a close icon is shown, i.e. if
* there are a no navigation actions and/or hasCloseIcon is true.
*/
closeIconDescription?: closeIconDescriptionTypes;

/**
* A description of the flow, displayed in the header area of the tearsheet.
*/
Expand Down Expand Up @@ -123,6 +111,9 @@ interface TearsheetNarrowProps extends PropsWithChildren {
verticalPosition?: 'normal' | 'lower';
}

type TearsheetNarrowProps = TearsheetNarrowBaseProps &
CloseIconDescriptionTypes;

const componentName = 'TearsheetNarrow';

// NOTE: the component SCSS is not imported here: it is rolled up separately.
Expand All @@ -147,7 +138,7 @@ export let TearsheetNarrow = React.forwardRef(
verticalPosition = defaults.verticalPosition,
...rest
}: TearsheetNarrowProps,
ref
ref: ForwardedRef<HTMLDivElement>
) => (
<TearsheetShell
{...{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
/**
* Copyright IBM Corp. 2020, 2023
* Copyright IBM Corp. 2020, 2024
*
* 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 portions of React that are needed.
import React, { useEffect, useState, useRef } from 'react';
import React, {
useEffect,
useState,
useRef,
PropsWithChildren,
ReactNode,
ForwardedRef,
MutableRefObject,
} from 'react';
import { useResizeObserver } from '../../global/js/hooks/useResizeObserver';

// Other standard imports.
Expand All @@ -23,6 +31,7 @@ import {
Layer,
ModalHeader,
usePrefix,
type ButtonProps,
} from '@carbon/react';

import { ActionSet } from '../ActionSet';
Expand All @@ -36,6 +45,119 @@ const componentName = 'TearsheetShell';

const maxDepth = 3;

interface TearsheetShellProps extends PropsWithChildren {
actions?: ButtonProps[];

ariaLabel?: string;

/**
* An optional class or classes to be added to the outermost element.
*/
className?: string;

/**
* A description of the flow, displayed in the header area of the tearsheet.
*/
description?: ReactNode;

/**
* Enable a close icon ('x') in the header area of the tearsheet. By default,
* (when this prop is omitted, or undefined or null) a tearsheet does not
* display a close icon if there are navigation actions ("transactional
* tearsheet") and displays one if there are no navigation actions ("passive
* tearsheet"), and that behavior can be overridden if required by setting
* this prop to either true or false.
*/
hasCloseIcon?: boolean;

/**
* The content for the header actions area, displayed alongside the title in
* the header area of the tearsheet. This is typically a drop-down, or a set
* of small buttons, or similar. NB the headerActions is only applicable for
* wide tearsheets.
*/
headerActions?: ReactNode;

/**
* The content for the influencer section of the tearsheet, displayed
* alongside the main content. This is typically a menu, or filter, or
* progress indicator, or similar. NB the influencer is only applicable for
* wide tearsheets.
*/
influencer?: ReactNode;

/**
* The position of the influencer section, 'left' or 'right'.
*/
influencerPosition?: 'left' | 'right';

/**
* The width of the influencer: 'narrow' (the default) is 256px, and 'wide'
* is 320px.
*/
influencerWidth?: 'narrow' | 'wide';

/**
* A label for the tearsheet, displayed in the header area of the tearsheet
* to maintain context for the tearsheet (e.g. as the title changes from page
* to page of a multi-page task).
*/
label?: ReactNode;

/**
* Navigation content, such as a set of tabs, to be displayed at the bottom
* of the header area of the tearsheet. NB the navigation is only applicable
* for wide tearsheets.
*/
navigation?: ReactNode;

/**
* An optional handler that is called when the user closes the tearsheet (by
* clicking the close button, if enabled, or clicking outside, if enabled).
* Returning `false` here prevents the modal from closing.
*/
onClose?: () => void;

/**
* Specifies whether the tearsheet is currently open.
*/
open?: boolean;

/**
* The DOM element that the tearsheet should be rendered within. Defaults to document.body.
*/
portalTarget?: ReactNode;

selectorPrimaryFocus?: string;

/**
* Specifies the width of the tearsheet, 'narrow' or 'wide'.
*/
size: 'narrow' | 'wide';

/**
* **Experimental:** Provide a `Slug` component to be rendered inside the `Tearsheet` component
*/
slug?: ReactNode;

/**
* The main title of the tearsheet, displayed in the header area.
*/
title?: ReactNode;

verticalPosition?: 'normal' | 'lower';
}

export type CloseIconDescriptionTypes =
| {
hasCloseIcon?: false;
closeIconDescription?: string;
}
| {
hasCloseIcon: true;
closeIconDescription: string;
};

// NOTE: the component SCSS is not imported here: it is rolled up separately.

// Global data structure to communicate the state of tearsheet stacking
Expand All @@ -47,7 +169,20 @@ const maxDepth = 3;
// The 'sizes' array contains an array of the sizes for every stacked tearsheet.
// This is so we can opt-out of including the stacking scale effect when there
// are stacked tearsheets with mixed sizes (ie, using wide and narrow together)
const stack = { open: [], all: [], sizes: [] };
type stackTypes = {
open: Array<{
(a: number, b: number): void;
checkFocus?: () => void;
claimFocus?: () => void;
}>;
all: Array<{
(a: number, b: number): void;
checkFocus?: () => void;
claimFocus?: () => void;
}>;
sizes: Array<string>;
};
const stack: stackTypes = { open: [], all: [], sizes: [] };

// these props are only applicable when size='wide'
export const tearsheetShellWideProps = [
Expand Down Expand Up @@ -96,8 +231,8 @@ export const TearsheetShell = React.forwardRef(
verticalPosition,
// Collect any other property values passed in.
...rest
},
ref
}: TearsheetShellProps & CloseIconDescriptionTypes,
ref: ForwardedRef<HTMLDivElement>
) => {
const carbonPrefix = usePrefix();
const bcModalHeader = `${carbonPrefix}--modal-header`;
Expand All @@ -108,6 +243,8 @@ export const TearsheetShell = React.forwardRef(
const modalRef = ref || localRef;
const { width } = useResizeObserver(resizer);
const { firstElement, keyDownListener } = useFocus(modalRef);
const modalRefValue = (modalRef as MutableRefObject<HTMLDivElement>)
.current;

const wide = size === 'wide';

Expand All @@ -116,7 +253,7 @@ export const TearsheetShell = React.forwardRef(
const [position, setPosition] = useState(0);

// Keep a record of the previous value of depth.
const prevDepth = useRef();
const prevDepth = useRef<number>();
useEffect(() => {
prevDepth.current = depth;
});
Expand All @@ -127,7 +264,7 @@ export const TearsheetShell = React.forwardRef(

// Callback that will be called whenever the stacking order changes.
// position is 1-based with 0 indicating closed.
function handleStackChange(newDepth, newPosition) {
function handleStackChange(newDepth: number, newPosition: number) {
setDepth(newDepth);
setPosition(newPosition);
}
Expand All @@ -137,8 +274,8 @@ export const TearsheetShell = React.forwardRef(
if (
open &&
position === depth &&
modalRef.current &&
!modalRef.current.contains(document.activeElement)
modalRefValue &&
!modalRefValue.contains(document.activeElement)
) {
handleStackChange.claimFocus();
}
Expand Down Expand Up @@ -174,7 +311,7 @@ export const TearsheetShell = React.forwardRef(
Math.min(stack.open.length, maxDepth),
stack.open.indexOf(handler) + 1
);
handler.checkFocus();
handler.checkFocus?.();
});

// Register this tearsheet's stack change callback/listener.
Expand Down Expand Up @@ -213,7 +350,7 @@ export const TearsheetShell = React.forwardRef(
// If something within us is receiving focus but we are not the topmost
// stacked tearsheet, transfer focus to the topmost tearsheet instead
if (position < depth) {
stack.open[stack.open.length - 1].claimFocus();
stack.open[stack.open.length - 1].claimFocus?.();
}
}

Expand Down Expand Up @@ -257,7 +394,7 @@ export const TearsheetShell = React.forwardRef(
className={cx(bc, className, {
[`${bc}--stacked-${position}-of-${depth}`]:
// Don't apply this on the initial open of a single tearsheet.
depth > 1 || (depth === 1 && prevDepth.current > 1),
depth > 1 || (depth === 1 && (prevDepth?.current ?? 0) > 1),
[`${bc}--wide`]: wide,
[`${bc}--narrow`]: !wide,
[`${bc}--has-slug`]: slug,
Expand Down Expand Up @@ -339,7 +476,9 @@ export const TearsheetShell = React.forwardRef(
<Wrap className={`${bc}__main`} alwaysRender={includeActions}>
<Wrap
className={`${bc}__content`}
alwaysRender={influencer && influencerPosition === 'right'}
alwaysRender={
!!(influencer && influencerPosition === 'right')
}
>
{children}
</Wrap>
Expand All @@ -357,7 +496,7 @@ export const TearsheetShell = React.forwardRef(
<Wrap className={`${bc}__button-container`}>
<ActionSet
actions={actions}
buttonSize={wide ? '2xl' : null}
buttonSize={wide ? '2xl' : undefined}
className={`${bc}__buttons`}
size={wide ? '2xl' : 'lg'}
aria-hidden={!open}
Expand Down Expand Up @@ -418,6 +557,7 @@ TearsheetShell.propTypes = {
*
* See https://react.carbondesignsystem.com/?path=/docs/components-button--default#component-api
*/
/**@ts-ignore*/
actions: PropTypes.arrayOf(
// NB we don't include the validator here, as the component wrapping this
// one should ensure appropriate validation is done.
Expand Down Expand Up @@ -453,6 +593,7 @@ TearsheetShell.propTypes = {
* **Note:** This prop is only required if a close icon is shown, i.e. if
* there are a no navigation actions and/or hasCloseIcon is true.
*/
/**@ts-ignore*/
closeIconDescription: PropTypes.string.isRequired.if(
({ actions, hasCloseIcon }) => tearsheetHasCloseIcon(actions, hasCloseIcon)
),
Expand All @@ -470,6 +611,7 @@ TearsheetShell.propTypes = {
* tearsheet"), and that behavior can be overridden if required by setting
* this prop to either true or false.
*/
/**@ts-ignore*/
hasCloseIcon: PropTypes.bool,

/**
Expand Down Expand Up @@ -528,11 +670,13 @@ TearsheetShell.propTypes = {
/**
* The DOM element that the tearsheet should be rendered within. Defaults to document.body.
*/
/**@ts-ignore*/
portalTarget: portalType,

/**
* Specifies the width of the tearsheet, 'narrow' or 'wide'.
*/
/**@ts-ignore*/
size: PropTypes.oneOf(['narrow', 'wide']).isRequired,

/**
Expand Down
Loading

0 comments on commit 1871965

Please sign in to comment.