From ffc385b47a7c9e774798566ad92139bd4501071a Mon Sep 17 00:00:00 2001 From: TJ Egan Date: Tue, 13 Feb 2024 13:27:53 -0500 Subject: [PATCH 1/9] refactor(Modal): use mask-image for overflow indicator (#15734) --- .../ComposedModal/ComposedModal.tsx | 19 +++--- packages/react/src/components/Modal/Modal.tsx | 3 - .../styles/scss/components/modal/_modal.scss | 58 ++++--------------- 3 files changed, 17 insertions(+), 63 deletions(-) diff --git a/packages/react/src/components/ComposedModal/ComposedModal.tsx b/packages/react/src/components/ComposedModal/ComposedModal.tsx index 35d3f4ab4dfe..d77d54658c13 100644 --- a/packages/react/src/components/ComposedModal/ComposedModal.tsx +++ b/packages/react/src/components/ComposedModal/ComposedModal.tsx @@ -62,18 +62,13 @@ export const ModalBody = React.forwardRef( : {}; return ( - <> -
- {children} -
- {hasScrollingContent && ( -
- )} - +
+ {children} +
); } ); diff --git a/packages/react/src/components/Modal/Modal.tsx b/packages/react/src/components/Modal/Modal.tsx index 33b3cf03e808..0af0f7b8f80f 100644 --- a/packages/react/src/components/Modal/Modal.tsx +++ b/packages/react/src/components/Modal/Modal.tsx @@ -488,9 +488,6 @@ const Modal = React.forwardRef(function Modal( {...hasScrollingContentProps}> {children}
- {hasScrollingContent && ( -
- )} {!passiveModal && ( {Array.isArray(secondaryButtons) && secondaryButtons.length <= 2 diff --git a/packages/styles/scss/components/modal/_modal.scss b/packages/styles/scss/components/modal/_modal.scss index 3deba95eb14e..f9194d6a7575 100644 --- a/packages/styles/scss/components/modal/_modal.scss +++ b/packages/styles/scss/components/modal/_modal.scss @@ -350,60 +350,22 @@ .#{$prefix}--modal-scroll-content { // Required to accommodate focus outline's negative offset when scrolling in Chrome border-block-end: 2px solid transparent; + mask-image: linear-gradient( + to bottom, + $layer calc(100% - 80px), + transparent calc(100% - 48px), + transparent 100% + ), + linear-gradient(to left, $layer 0, 16px, transparent 16px), + linear-gradient(to right, $layer 0, 2px, transparent 2px), + linear-gradient(to top, $layer 0, 2px, transparent 2px); } + // Required so overflow-indicator disappears at end of content .#{$prefix}--modal-scroll-content > *:last-child { padding-block-end: $spacing-06; } - .#{$prefix}--modal-content--overflow-indicator { - position: absolute; - background: $layer; - block-size: convert.to-rem(48px); - grid-column: 1/-1; - grid-row: 2/-2; - inline-size: calc(100% - $spacing-05); - inset-block-end: 0; - inset-inline-start: 0; - pointer-events: none; - - &::before { - position: absolute; - background-image: linear-gradient(to bottom, transparent, $layer); - block-size: convert.to-rem(32px); - content: ''; - inline-size: 100%; - inset-block-start: -32px; - pointer-events: none; - } - } - - // Safari-only media query - // won't appear correctly with CSS custom properties - // see: code snippet and tabs overflow indicators - @media not all and (min-resolution >= 0.001dpcm) { - @supports (-webkit-appearance: none) and (stroke-color: transparent) { - .#{$prefix}--modal-content--overflow-indicator { - background-image: linear-gradient(to bottom, rgba($layer, 0), $layer); - } - } - } - - .#{$prefix}--modal-content:focus - ~ .#{$prefix}--modal-content--overflow-indicator { - margin: 0 2px 2px; - } - - @media screen and (-ms-high-contrast: active) { - .#{$prefix}--modal-scroll-content > *:last-child { - padding-block-end: 0; - } - - .#{$prefix}--modal-content--overflow-indicator { - display: none; - } - } - // ----------------------------- // Modal footer // ----------------------------- From 04e231fabe07d1f51b67a73a6f38ec7d479bb05e Mon Sep 17 00:00:00 2001 From: Guilherme Datilio Ribeiro Date: Tue, 13 Feb 2024 17:18:30 -0300 Subject: [PATCH 2/9] `Tabs` - Fixed arrow navigation (#15632) * fix: fixed keydown bug * fix: removed stories --- packages/react/src/components/Tabs/Tabs.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/Tabs/Tabs.tsx b/packages/react/src/components/Tabs/Tabs.tsx index 13af84703e12..c058ce31a4dc 100644 --- a/packages/react/src/components/Tabs/Tabs.tsx +++ b/packages/react/src/components/Tabs/Tabs.tsx @@ -404,7 +404,9 @@ function TabList({ ) { event.preventDefault(); - const activeTabs: TabElement[] = tabs.current.filter( + const filtredTabs = tabs.current.filter((tab) => tab !== null); + + const activeTabs: TabElement[] = filtredTabs.filter( (tab) => !tab.disabled ); @@ -420,7 +422,6 @@ function TabList({ } else if (activation === 'manual') { setActiveIndex(nextIndex); } - tabs.current[nextIndex]?.focus(); } } From 56d82b5f773e95934e888a63aadade31830514f1 Mon Sep 17 00:00:00 2001 From: Shubham L <54625156+slokhande310@users.noreply.github.com> Date: Wed, 14 Feb 2024 01:50:00 +0530 Subject: [PATCH 3/9] Fix #15607: Replaced with
, Removed unnecessary styling (#15610) * Fix #15607: Replaced with
, Removed unnecessary styling of that block * fix(Tile): adjust types, fix below the fold span --------- Co-authored-by: Andrea N. Cardona Co-authored-by: TJ Egan --- packages/react/src/components/Tile/Tile.tsx | 12 ++++++------ packages/styles/scss/components/tile/_tile.scss | 4 ---- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/react/src/components/Tile/Tile.tsx b/packages/react/src/components/Tile/Tile.tsx index 555ec2c2e5a1..89c4e4b02b8d 100644 --- a/packages/react/src/components/Tile/Tile.tsx +++ b/packages/react/src/components/Tile/Tile.tsx @@ -990,15 +990,15 @@ export interface TileAboveTheFoldContentProps { } export const TileAboveTheFoldContent = React.forwardRef< - HTMLSpanElement, + HTMLDivElement, TileAboveTheFoldContentProps >(function TilAboveTheFoldContent({ children }, ref) { const prefix = usePrefix(); return ( - +
{children} - +
); }); @@ -1018,15 +1018,15 @@ export interface TileBelowTheFoldContentProps { } export const TileBelowTheFoldContent = React.forwardRef< - HTMLSpanElement, + HTMLDivElement, TileBelowTheFoldContentProps >(function TileBelowTheFoldContent({ children }, ref) { const prefix = usePrefix(); return ( - +
{children} - +
); }); diff --git a/packages/styles/scss/components/tile/_tile.scss b/packages/styles/scss/components/tile/_tile.scss index c6c53daac9da..7e87c9157f10 100644 --- a/packages/styles/scss/components/tile/_tile.scss +++ b/packages/styles/scss/components/tile/_tile.scss @@ -274,10 +274,6 @@ $-icon-container-size: calc(#{layout.density('padding-inline')} * 2 + 1rem); @include focus-outline('outline'); } - .#{$prefix}--tile-content__above-the-fold { - display: block; - } - .#{$prefix}--tile-content__below-the-fold { display: block; opacity: 0; From 6d3288574d7522f110c4ac9a93116fe434640b18 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Tue, 13 Feb 2024 21:20:59 +0100 Subject: [PATCH 4/9] docs(global-theme): clarify that styles won't be applied automatically (#15609) * docs(global-theme): clarify that styles won't be applied automatically * docs(global-theme): rephrase --- packages/react/src/components/Theme/Theme.mdx | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/react/src/components/Theme/Theme.mdx b/packages/react/src/components/Theme/Theme.mdx index 197c60c7a0a1..e059b8a06b7f 100644 --- a/packages/react/src/components/Theme/Theme.mdx +++ b/packages/react/src/components/Theme/Theme.mdx @@ -42,19 +42,48 @@ You also get this file when you import `@carbon/react` directly in your Sass. ## Setting the global theme -Use the `GlobalTheme` component to set the theme for your entire project. By -default, the global theme will be set to the `white` theme. You can change the -global theme value by using the `theme` prop on `GlobalTheme`: +To the set the theme for your entire project, the `GlobalTheme` component can be used. + +Please note that in contrast to `Theme` this does not apply any styles on its own but rather sets the context's theme according to the value you pass to its `theme` prop. + +This is due to the various options of applying global css custom properties which differ from application to application. Depending on your architecture you may want to apply a class to the `` or add a custom data attribute to your `` element: ```jsx +import { useEffect } from 'react'; import { GlobalTheme } from '@carbon/react'; - - -; +function App() { + const theme = 'g100'; // ← your implementation, e.g. fetching user settings + + useEffect(() => { + document.documentElement.dataset.carbonTheme = theme; + }, [theme]); + + return ( + + + ; + ); +} ``` -Note: this component should be used at the root of your app. +```scss +@use '@carbon/styles/scss/theme'; +@use '@carbon/styles/scss/themes'; + +:root[data-carbon-theme="g10"] { + @include theme.theme(themes.$g10); +} + +:root[data-carbon-theme="g100"] { + @include theme.theme(themes.$g100); +} +``` + +This way, the `GlobalTheme` component is used to "synchronize" the state of the application's context and your scss, so that other components and hooks like useTheme can work properly. +For this reason, the component should be used as a wrapper of the root of your application. + +By default, the global theme is set to `white`. ## Setting an inline theme From 754ff7965e3693bd445e8a4d46b4b31bc7e91417 Mon Sep 17 00:00:00 2001 From: Matias Borghi Date: Tue, 13 Feb 2024 17:23:51 -0300 Subject: [PATCH 5/9] Add Typescript types to PaginationNav (#15407) * refactor: change extensions to ts and tsx * feat: add TypeScript types to PaginationNav * fix: add disableOverflow typescript type into PaginationOverflowProps * feat: add types of translateWithId function * refactor: renamed some function/variables --------- Co-authored-by: TJ Egan --- .../components/PaginationNav/PaginationNav.js | 503 -------------- .../PaginationNav/PaginationNav.tsx | 650 ++++++++++++++++++ .../PaginationNav/{index.js => index.ts} | 0 3 files changed, 650 insertions(+), 503 deletions(-) delete mode 100644 packages/react/src/components/PaginationNav/PaginationNav.js create mode 100644 packages/react/src/components/PaginationNav/PaginationNav.tsx rename packages/react/src/components/PaginationNav/{index.js => index.ts} (100%) diff --git a/packages/react/src/components/PaginationNav/PaginationNav.js b/packages/react/src/components/PaginationNav/PaginationNav.js deleted file mode 100644 index 72820a777b7b..000000000000 --- a/packages/react/src/components/PaginationNav/PaginationNav.js +++ /dev/null @@ -1,503 +0,0 @@ -/** - * Copyright IBM Corp. 2020 - * - * 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 PropTypes from 'prop-types'; -import React, { useState, useEffect, useRef } from 'react'; -import classnames from 'classnames'; -import { - CaretRight, - CaretLeft, - OverflowMenuHorizontal, -} from '@carbon/icons-react'; -import { IconButton } from '../IconButton'; -import { usePrefix } from '../../internal/usePrefix'; - -const translationIds = { - 'carbon.pagination-nav.next': 'Next', - 'carbon.pagination-nav.previous': 'Previous', - 'carbon.pagination-nav.item': 'Page', - 'carbon.pagination-nav.active': 'Active', - 'carbon.pagination-nav.of': 'of', -}; - -function translateWithId(messageId) { - return translationIds[messageId]; -} - -// https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state -function usePrevious(value) { - const ref = useRef(); - - useEffect(() => { - ref.current = value; - }); - - return ref.current; -} - -function getCuts(page, totalItems, itemsThatFit, splitPoint = null) { - if (itemsThatFit >= totalItems) { - return { - front: 0, - back: 0, - }; - } - - const split = splitPoint || Math.ceil(itemsThatFit / 2) - 1; - - let frontHidden = page + 1 - split; - let backHidden = totalItems - page - (itemsThatFit - split) + 1; - - if (frontHidden <= 1) { - backHidden -= frontHidden <= 0 ? Math.abs(frontHidden) + 1 : 0; - frontHidden = 0; - } - - if (backHidden <= 1) { - frontHidden -= backHidden <= 0 ? Math.abs(backHidden) + 1 : 0; - backHidden = 0; - } - - return { - front: frontHidden, - back: backHidden, - }; -} - -function DirectionButton({ direction, label, disabled, onClick }) { - const prefix = usePrefix(); - - return ( -
  • - - {direction === 'forward' ? : } - -
  • - ); -} - -function PaginationItem({ - page, - isActive, - onClick, - translateWithId: t = translateWithId, -}) { - const prefix = usePrefix(); - const itemLabel = t('carbon.pagination-nav.item'); - - return ( -
  • - -
  • - ); -} - -function PaginationOverflow({ - fromIndex, - count, - onSelect, - // eslint-disable-next-line react/prop-types - disableOverflow, - translateWithId: t = translateWithId, -}) { - const prefix = usePrefix(); - - //If overflow is disabled, return a select tag with no select options - if (disableOverflow === true && count > 1) { - return ( -
  • -
    - {/* eslint-disable-next-line jsx-a11y/no-onchange */} - -
    - -
    -
    -
  • - ); - } - - if (count > 1) { - return ( -
  • -
    - {/* eslint-disable-next-line jsx-a11y/no-onchange */} - -
    - -
    -
    -
  • - ); - } - - if (count === 1) { - return ( - { - onSelect(fromIndex); - }} - /> - ); - } - - return null; -} - -const PaginationNav = React.forwardRef(function PaginationNav( - { - className, - onChange = () => {}, - totalItems, - disableOverflow, - itemsShown = 10, - page = 0, - loop = false, - translateWithId: t = translateWithId, - ...rest - }, - ref -) { - const [currentPage, setCurrentPage] = useState(page); - const [itemsThatFit, setItemsThatFit] = useState( - itemsShown >= 4 ? itemsShown : 4 - ); - const [cuts, setCuts] = useState( - getCuts(currentPage, totalItems, itemsThatFit) - ); - const prevPage = usePrevious(currentPage); - const prefix = usePrefix(); - const [isOverflowDisabled, setIsOverFlowDisabled] = useState(disableOverflow); - function jumpToItem(index) { - if (index >= 0 && index < totalItems) { - setCurrentPage(index); - onChange(index); - } - } - - function jumpToNext() { - const nextIndex = currentPage + 1; - - if (nextIndex >= totalItems) { - if (loop) { - jumpToItem(0); - } - } else { - jumpToItem(nextIndex); - } - } - - function jumpToPrevious() { - const previousIndex = currentPage - 1; - - if (previousIndex < 0) { - if (loop) { - jumpToItem(totalItems - 1); - } - } else { - jumpToItem(previousIndex); - } - } - - function pageWouldBeHidden(page) { - const startOffset = itemsThatFit <= 4 && page > 1 ? 0 : 1; - - const wouldBeHiddenInFront = page >= startOffset && page <= cuts.front; - const wouldBeHiddenInBack = - page >= totalItems - cuts.back - 1 && page <= totalItems - 2; - - return wouldBeHiddenInFront || wouldBeHiddenInBack; - } - - // jump to new page if props.page is updated - useEffect(() => { - setCurrentPage(page); - }, [page]); - - // re-calculate cuts if props.totalItems or props.itemsShown change - useEffect(() => { - setItemsThatFit(itemsShown >= 4 ? itemsShown : 4); - setCuts(getCuts(currentPage, totalItems, itemsShown)); - }, [totalItems, itemsShown]); // eslint-disable-line react-hooks/exhaustive-deps - - // update cuts if necessary whenever currentPage changes - useEffect(() => { - if (pageWouldBeHidden(currentPage)) { - const delta = currentPage - prevPage || 0; - - if (delta > 0) { - const splitPoint = itemsThatFit - 3; - setCuts(getCuts(currentPage, totalItems, itemsThatFit, splitPoint)); - } else { - const splitPoint = itemsThatFit > 4 ? 2 : 1; - setCuts(getCuts(currentPage, totalItems, itemsThatFit, splitPoint)); - } - } - }, [currentPage]); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - setIsOverFlowDisabled(disableOverflow); - }, [disableOverflow]); - - const classNames = classnames(`${prefix}--pagination-nav`, className); - - const backwardButtonDisabled = !loop && currentPage === 0; - const forwardButtonDisabled = !loop && currentPage === totalItems - 1; - - const startOffset = itemsThatFit <= 4 && currentPage > 1 ? 0 : 1; - - return ( - - ); -}); - -DirectionButton.propTypes = { - /** - * The direction this button represents ("forward" or "backward"). - */ - direction: PropTypes.oneOf(['forward', 'backward']), - - /** - * Whether or not the button should be disabled. - */ - disabled: PropTypes.bool, - - /** - * The label shown in the button's tooltip. - */ - label: PropTypes.string, - - /** - * The callback function called when the button is clicked. - */ - onClick: PropTypes.func, -}; - -PaginationItem.propTypes = { - /** - * Whether or not this is the currently active page. - */ - isActive: PropTypes.bool, - - /** - * The callback function called when the item is clicked. - */ - onClick: PropTypes.func, - - /** - * The page number this item represents. - */ - page: PropTypes.number, - - /** - * Specify a custom translation function that takes in a message identifier - * and returns the localized string for the message - */ - translateWithId: PropTypes.func, -}; - -PaginationOverflow.propTypes = { - /** - * How many items to display in this overflow. - */ - count: PropTypes.number, - - /** - * From which index on this overflow should start displaying pages. - */ - fromIndex: PropTypes.number, - - /** - * The callback function called when the user selects a page from the overflow. - */ - onSelect: PropTypes.func, - - /** - * Specify a custom translation function that takes in a message identifier - * and returns the localized string for the message - */ - translateWithId: PropTypes.func, -}; - -PaginationNav.displayName = 'PaginationNav'; -PaginationNav.propTypes = { - /** - * Additional CSS class names. - */ - className: PropTypes.string, - - /** - * If true, the '...' pagination overflow will not render page links between the first and last rendered buttons. - * Set this to true if you are having performance problems with large data sets. - */ - disableOverflow: PropTypes.bool, // eslint-disable-line react/prop-types - - /** - * The number of items to be shown. - */ - itemsShown: PropTypes.number, - - /** - * Whether user should be able to loop through the items when reaching first / last. - */ - loop: PropTypes.bool, - - /** - * The callback function called when the current page changes. - */ - onChange: PropTypes.func, - - /** - * The index of current page. - */ - page: PropTypes.number, - - /** - * The total number of items. - */ - totalItems: PropTypes.number, - - /** - * Specify a custom translation function that takes in a message identifier - * and returns the localized string for the message - */ - translateWithId: PropTypes.func, -}; - -export default PaginationNav; diff --git a/packages/react/src/components/PaginationNav/PaginationNav.tsx b/packages/react/src/components/PaginationNav/PaginationNav.tsx new file mode 100644 index 000000000000..8daabb8f40cd --- /dev/null +++ b/packages/react/src/components/PaginationNav/PaginationNav.tsx @@ -0,0 +1,650 @@ +/** + * Copyright IBM Corp. 2020 + * + * 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 PropTypes from 'prop-types'; +import React, { useState, useEffect, useRef } from 'react'; +import classnames from 'classnames'; +import { + CaretRight, + CaretLeft, + OverflowMenuHorizontal, +} from '@carbon/icons-react'; +import { IconButton } from '../IconButton'; +import { usePrefix } from '../../internal/usePrefix'; + +const translationIds = { + 'carbon.pagination-nav.next': 'Next', + 'carbon.pagination-nav.previous': 'Previous', + 'carbon.pagination-nav.item': 'Page', + 'carbon.pagination-nav.active': 'Active', + 'carbon.pagination-nav.of': 'of', +}; + +function translateWithId(messageId: string): string { + return translationIds[messageId]; +} + +// https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state +function usePrevious(value: number) { + const ref = useRef(null); + + useEffect(() => { + ref.current = value; + }); + + return ref.current; +} + +function calculateCuts( + page: number, + totalItems: number, + itemsDisplayedOnPage: number, + splitPoint: number | null = null +) { + if (itemsDisplayedOnPage >= totalItems) { + return { + front: 0, + back: 0, + }; + } + + const split = splitPoint || Math.ceil(itemsDisplayedOnPage / 2) - 1; + + let frontHidden = page + 1 - split; + let backHidden = totalItems - page - (itemsDisplayedOnPage - split) + 1; + + if (frontHidden <= 1) { + backHidden -= frontHidden <= 0 ? Math.abs(frontHidden) + 1 : 0; + frontHidden = 0; + } + + if (backHidden <= 1) { + frontHidden -= backHidden <= 0 ? Math.abs(backHidden) + 1 : 0; + backHidden = 0; + } + + return { + front: frontHidden, + back: backHidden, + }; +} + +interface DirectionButtonProps { + /** + * The direction this button represents ("forward" or "backward"). + */ + direction?: 'forward' | 'backward'; + + /** + * Whether or not the button should be disabled. + */ + disabled?: boolean; + + /** + * The label shown in the button's tooltip. + */ + label?: string; + + /** + * The callback function called when the button is clicked. + */ + onClick?: React.MouseEventHandler; +} + +function DirectionButton({ + direction, + label, + disabled, + onClick, +}: DirectionButtonProps) { + const prefix = usePrefix(); + + return ( +
  • + + {direction === 'forward' ? : } + +
  • + ); +} + +interface PaginationItemProps { + /** + * Whether or not this is the currently active page. + */ + isActive?: boolean; + + /** + * The callback function called when the item is clicked. + */ + onClick?: React.MouseEventHandler; + + /** + * The page number this item represents. + */ + page?: number; + + /** + * Specify a custom translation function that takes in a message identifier + * and returns the localized string for the message + */ + translateWithId?: (id: string) => string; +} + +function PaginationItem({ + page, + isActive, + onClick, + translateWithId: t = translateWithId, +}: PaginationItemProps) { + const prefix = usePrefix(); + const itemLabel = t('carbon.pagination-nav.item'); + + return ( +
  • + +
  • + ); +} + +interface PaginationOverflowProps { + /** + * How many items to display in this overflow. + */ + count?: number; + + /** + * From which index on this overflow should start displaying pages. + */ + fromIndex?: number; + + /** + * The callback function called when the user selects a page from the overflow. + */ + onSelect?: (id: number) => void; + + /** + * If true, the '...' pagination overflow will not render page links between the first and last rendered buttons. + * Set this to true if you are having performance problems with large data sets. + */ + disableOverflow?: boolean; + + /** + * Specify a custom translation function that takes in a message identifier + * and returns the localized string for the message + */ + translateWithId?: (id: string) => string; +} + +function PaginationOverflow({ + fromIndex = NaN, + count = NaN, + onSelect, + // eslint-disable-next-line react/prop-types + disableOverflow, + translateWithId: t = translateWithId, +}: PaginationOverflowProps) { + const prefix = usePrefix(); + + //If overflow is disabled, return a select tag with no select options + if (disableOverflow === true && count > 1) { + return ( +
  • +
    + {/* eslint-disable-next-line jsx-a11y/no-onchange */} + +
    + +
    +
    +
  • + ); + } + + if (count > 1) { + return ( +
  • +
    + {/* eslint-disable-next-line jsx-a11y/no-onchange */} + +
    + +
    +
    +
  • + ); + } + + if (count === 1) { + return ( + { + onSelect?.(fromIndex); + }} + /> + ); + } + + return null; +} + +interface PaginationNavProps + extends Omit, 'onChange'> { + /** + * Additional CSS class names. + */ + className?: string; + + /** + * If true, the '...' pagination overflow will not render page links between the first and last rendered buttons. + * Set this to true if you are having performance problems with large data sets. + */ + disableOverflow?: boolean; + + /** + * The number of items to be shown. + */ + itemsShown?: number; + + /** + * Whether user should be able to loop through the items when reaching first / last. + */ + loop?: boolean; + + /** + * The callback function called when the current page changes. + */ + onChange?: (data: number) => void; + + /** + * The index of current page. + */ + page?: number; + + /** + * The total number of items. + */ + totalItems?: number; + + /** + * Specify a custom translation function that takes in a message identifier + * and returns the localized string for the message + */ + translateWithId?: (id: string) => string; +} + +const PaginationNav = React.forwardRef( + function PaginationNav( + { + className, + onChange = () => {}, + totalItems = NaN, + disableOverflow, + itemsShown = 10, + page = 0, + loop = false, + translateWithId: t = translateWithId, + ...rest + }, + ref + ) { + const [currentPage, setCurrentPage] = useState(page); + const [itemsDisplayedOnPage, setItemsDisplayedOnPage] = useState( + itemsShown >= 4 ? itemsShown : 4 + ); + const [cuts, setCuts] = useState( + calculateCuts(currentPage, totalItems, itemsDisplayedOnPage) + ); + const prevPage = usePrevious(currentPage); + const prefix = usePrefix(); + const [isOverflowDisabled, setIsOverFlowDisabled] = + useState(disableOverflow); + function jumpToItem(index: number) { + if (index >= 0 && index < totalItems) { + setCurrentPage(index); + onChange(index); + } + } + + function jumpToNext() { + const nextIndex = currentPage + 1; + + if (nextIndex >= totalItems) { + if (loop) { + jumpToItem(0); + } + } else { + jumpToItem(nextIndex); + } + } + + function jumpToPrevious() { + const previousIndex = currentPage - 1; + + if (previousIndex < 0) { + if (loop) { + jumpToItem(totalItems - 1); + } + } else { + jumpToItem(previousIndex); + } + } + + function pageWouldBeHidden(page: number) { + const startOffset = itemsDisplayedOnPage <= 4 && page > 1 ? 0 : 1; + + const wouldBeHiddenInFront = page >= startOffset && page <= cuts.front; + const wouldBeHiddenInBack = + page >= totalItems - cuts.back - 1 && page <= totalItems - 2; + + return wouldBeHiddenInFront || wouldBeHiddenInBack; + } + + // jump to new page if props.page is updated + useEffect(() => { + setCurrentPage(page); + }, [page]); + + // re-calculate cuts if props.totalItems or props.itemsShown change + useEffect(() => { + setItemsDisplayedOnPage(itemsShown >= 4 ? itemsShown : 4); + setCuts(calculateCuts(currentPage, totalItems, itemsShown)); + }, [totalItems, itemsShown]); // eslint-disable-line react-hooks/exhaustive-deps + + // update cuts if necessary whenever currentPage changes + useEffect(() => { + if (pageWouldBeHidden(currentPage)) { + const delta = currentPage - (prevPage || 0); + + if (delta > 0) { + const splitPoint = itemsDisplayedOnPage - 3; + setCuts( + calculateCuts( + currentPage, + totalItems, + itemsDisplayedOnPage, + splitPoint + ) + ); + } else { + const splitPoint = itemsDisplayedOnPage > 4 ? 2 : 1; + setCuts( + calculateCuts( + currentPage, + totalItems, + itemsDisplayedOnPage, + splitPoint + ) + ); + } + } + }, [currentPage]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + setIsOverFlowDisabled(disableOverflow); + }, [disableOverflow]); + + const classNames = classnames(`${prefix}--pagination-nav`, className); + + const backwardButtonDisabled = !loop && currentPage === 0; + const forwardButtonDisabled = !loop && currentPage === totalItems - 1; + + const startOffset = itemsDisplayedOnPage <= 4 && currentPage > 1 ? 0 : 1; + + return ( + + ); + } +); + +DirectionButton.propTypes = { + /** + * The direction this button represents ("forward" or "backward"). + */ + direction: PropTypes.oneOf(['forward', 'backward']), + + /** + * Whether or not the button should be disabled. + */ + disabled: PropTypes.bool, + + /** + * The label shown in the button's tooltip. + */ + label: PropTypes.string, + + /** + * The callback function called when the button is clicked. + */ + onClick: PropTypes.func, +}; + +PaginationItem.propTypes = { + /** + * Whether or not this is the currently active page. + */ + isActive: PropTypes.bool, + + /** + * The callback function called when the item is clicked. + */ + onClick: PropTypes.func, + + /** + * The page number this item represents. + */ + page: PropTypes.number, + + /** + * Specify a custom translation function that takes in a message identifier + * and returns the localized string for the message + */ + translateWithId: PropTypes.func, +}; + +PaginationOverflow.propTypes = { + /** + * How many items to display in this overflow. + */ + count: PropTypes.number, + + /** + * From which index on this overflow should start displaying pages. + */ + fromIndex: PropTypes.number, + + /** + * The callback function called when the user selects a page from the overflow. + */ + onSelect: PropTypes.func, + + /** + * Specify a custom translation function that takes in a message identifier + * and returns the localized string for the message + */ + translateWithId: PropTypes.func, +}; + +PaginationNav.displayName = 'PaginationNav'; +PaginationNav.propTypes = { + /** + * Additional CSS class names. + */ + className: PropTypes.string, + + /** + * If true, the '...' pagination overflow will not render page links between the first and last rendered buttons. + * Set this to true if you are having performance problems with large data sets. + */ + disableOverflow: PropTypes.bool, // eslint-disable-line react/prop-types + + /** + * The number of items to be shown. + */ + itemsShown: PropTypes.number, + + /** + * Whether user should be able to loop through the items when reaching first / last. + */ + loop: PropTypes.bool, + + /** + * The callback function called when the current page changes. + */ + onChange: PropTypes.func, + + /** + * The index of current page. + */ + page: PropTypes.number, + + /** + * The total number of items. + */ + totalItems: PropTypes.number, + + /** + * Specify a custom translation function that takes in a message identifier + * and returns the localized string for the message + */ + translateWithId: PropTypes.func, +}; + +export default PaginationNav; diff --git a/packages/react/src/components/PaginationNav/index.js b/packages/react/src/components/PaginationNav/index.ts similarity index 100% rename from packages/react/src/components/PaginationNav/index.js rename to packages/react/src/components/PaginationNav/index.ts From 962ebc136e451c7254b640a67321d415d6526695 Mon Sep 17 00:00:00 2001 From: Bill Keese Date: Wed, 14 Feb 2024 05:26:27 +0900 Subject: [PATCH 6/9] fix: TableToolbarSearch parameter types (#15422) * fix: tabletoolbarsearch parameters Fix TableToolbarSearch parameters, in particular missing closeButtonLabelText. Also refactor the code as done in @types/carbon-components-react: Make TableToolbarSearch parameters extend SearchParameters. IMO the TableToolbarSearch parameters have a lot of unnecessary differences from the Search parameters, but I didn't change that in this commit. One open question is why the initial expandedState is based on defaultValue. That seems like a mistake. If it is, I can roll it back in this PR. Refs #14471. * chore: remove todo, i guess current code makes sense --------- Co-authored-by: TJ Egan --- .../DataTable/TableToolbarSearch.tsx | 93 ++++++++----------- 1 file changed, 40 insertions(+), 53 deletions(-) diff --git a/packages/react/src/components/DataTable/TableToolbarSearch.tsx b/packages/react/src/components/DataTable/TableToolbarSearch.tsx index d23f0d5d6208..71a276523246 100644 --- a/packages/react/src/components/DataTable/TableToolbarSearch.tsx +++ b/packages/react/src/components/DataTable/TableToolbarSearch.tsx @@ -17,12 +17,18 @@ import React, { ReactNode, RefObject, } from 'react'; -import Search from '../Search'; +import Search, { SearchProps } from '../Search'; import setupGetInstanceId from './tools/instanceId'; import { usePrefix } from '../../internal/usePrefix'; import { noopFn } from '../../internal/noopFn'; +import { InternationalProps } from '../../types/common'; const getInstanceId = setupGetInstanceId(); + +export type TableToolbarTranslationKey = + | 'carbon.table.toolbar.search.label' + | 'carbon.table.toolbar.search.placeholder'; + const translationKeys = { 'carbon.table.toolbar.search.label': 'Filter table', 'carbon.table.toolbar.search.placeholder': 'Filter table', @@ -32,14 +38,23 @@ const translateWithId = (id: string): string => { return translationKeys[id]; }; -export interface TableToolbarSearchProps { - children?: ReactNode; +type ExcludedInheritedProps = + | 'defaultValue' + | 'labelText' + | 'onBlur' + | 'onChange' + | 'onExpand' + | 'onFocus' + | 'tabIndex'; - /** - * Provide an optional class name for the search container - */ - className?: string; +export type TableToolbarSearchHandleExpand = ( + event: FocusEvent, + newValue?: boolean +) => void; +export interface TableToolbarSearchProps + extends Omit, + InternationalProps { /** * Specifies if the search should initially render in an expanded state */ @@ -50,35 +65,25 @@ export interface TableToolbarSearchProps { */ defaultValue?: string; - /** - * Specifies if the search should be disabled - */ - disabled?: boolean; - /** * Specifies if the search should expand */ expanded?: boolean; - /** - * Provide an optional id for the search container - */ - id?: string; - /** * Provide an optional label text for the Search component icon */ - labelText?: string; + labelText?: ReactNode; /** * Provide an optional function to be called when the search input loses focus, this will be * passed the event as the first parameter and a function to handle the expanding of the search * input as the second */ - onBlur?: ( + onBlur?( event: FocusEvent, - handleExpand: (event: FocusEvent, value: boolean) => void - ) => void; + handleExpand: TableToolbarSearchHandleExpand + ): void; /** * Provide an optional hook that is called each time the input is updated @@ -88,54 +93,32 @@ export interface TableToolbarSearchProps { value?: string ) => void; - /** - * Optional callback called when the search value is cleared. - */ - onClear?: () => void; - /** * Provide an optional hook that is called each time the input is expanded */ - onExpand?: (event: FocusEvent, value: boolean) => void; + onExpand?(event: FocusEvent, newExpand: boolean): void; /** * Provide an optional function to be called when the search input gains focus, this will be * passed the event as the first parameter and a function to handle the expanding of the search * input as the second. */ - onFocus?: ( + onFocus?( event: FocusEvent, - handleExpand: (event: FocusEvent, value: boolean) => void - ) => void; + handleExpand: TableToolbarSearchHandleExpand + ): void; /** - * Whether the search should be allowed to expand + * Whether the search should be allowed to expand. */ persistent?: boolean; - /** - * Provide an optional placeholder text for the Search component - */ - placeholder?: string; - /** * Provide an optional className for the overall container of the Search */ searchContainerClass?: string; - /** - * Specify the size of the Search - */ - size?: 'sm' | 'md' | 'lg'; - - /** - * Optional prop to specify the tabIndex of the (in expanded state) or the container (in collapsed state) - */ tabIndex?: number | string; - /** - * Provide custom text for the component for each translation id - */ - translateWithId?: (id: string) => string; } const TableToolbarSearch = ({ @@ -160,9 +143,11 @@ const TableToolbarSearch = ({ ...rest }: TableToolbarSearchProps) => { const { current: controlled } = useRef(expandedProp !== undefined); - const [expandedState, setExpandedState] = useState< - string | boolean | undefined - >(defaultExpanded || defaultValue); + + const [expandedState, setExpandedState] = useState( + Boolean(defaultExpanded || defaultValue) + ); + const expanded = controlled ? expandedProp : expandedState; const [value, setValue] = useState(defaultValue || ''); const uniqueId = useMemo(getInstanceId, []); @@ -218,8 +203,10 @@ const TableToolbarSearch = ({ } }; - const handleOnFocus = (event) => handleExpand(event, true); - const handleOnBlur = (event) => !value && handleExpand(event, false); + const handleOnFocus = (event: FocusEvent) => + handleExpand(event, true); + const handleOnBlur = (event: FocusEvent) => + !value && handleExpand(event, false); return ( Date: Tue, 13 Feb 2024 20:48:04 +0000 Subject: [PATCH 7/9] chore(deps): bump actions/setup-node from 4.0.1 to 4.0.2 (#15725) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.1 to 4.0.2. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8...60edb5dd545a775178f52524783378180af0d1f8) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Taylor Jones --- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/deploy-packages.yml | 4 ++-- .github/workflows/deploy-react-storybook.yml | 2 +- .github/workflows/nightly-release.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/sync-generated-files.yml | 2 +- .github/workflows/v10-ci.yml | 10 +++++----- .github/workflows/v10-deploy-react-storybook.yml | 2 +- .github/workflows/v10-release.yml | 2 +- .github/workflows/version.yml | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd6b3857f34f..8a17318f04e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Run yarn dedupe @@ -30,7 +30,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies @@ -43,7 +43,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies @@ -58,7 +58,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0 @@ -85,7 +85,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0 @@ -125,7 +125,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0 @@ -188,7 +188,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0 diff --git a/.github/workflows/deploy-packages.yml b/.github/workflows/deploy-packages.yml index 6781ae9709d9..785930000e5f 100644 --- a/.github/workflows/deploy-packages.yml +++ b/.github/workflows/deploy-packages.yml @@ -21,7 +21,7 @@ jobs: repository: carbon-design-system/design-language-website ref: master - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' @@ -63,7 +63,7 @@ jobs: repository: carbon-design-system/gatsby-theme-carbon ref: main - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/deploy-react-storybook.yml b/.github/workflows/deploy-react-storybook.yml index 02b5211b8c97..d04a0b891d6f 100644 --- a/.github/workflows/deploy-react-storybook.yml +++ b/.github/workflows/deploy-react-storybook.yml @@ -30,7 +30,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies diff --git a/.github/workflows/nightly-release.yml b/.github/workflows/nightly-release.yml index d9f118448b2a..2816a90fe7ae 100644 --- a/.github/workflows/nightly-release.yml +++ b/.github/workflows/nightly-release.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4bca12705be0..21ae99786698 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/sync-generated-files.yml b/.github/workflows/sync-generated-files.yml index 7bc3159fb16b..5f93e2234309 100644 --- a/.github/workflows/sync-generated-files.yml +++ b/.github/workflows/sync-generated-files.yml @@ -9,7 +9,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies diff --git a/.github/workflows/v10-ci.yml b/.github/workflows/v10-ci.yml index c8c27ad4f281..1bc0c4d41a4d 100644 --- a/.github/workflows/v10-ci.yml +++ b/.github/workflows/v10-ci.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Run yarn dedupe @@ -29,7 +29,7 @@ jobs: with: ref: v10 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies @@ -44,7 +44,7 @@ jobs: with: ref: v10 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies @@ -61,7 +61,7 @@ jobs: with: ref: v10 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0 @@ -89,7 +89,7 @@ jobs: with: ref: v10 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0 diff --git a/.github/workflows/v10-deploy-react-storybook.yml b/.github/workflows/v10-deploy-react-storybook.yml index 5c23c5f3b198..1b4190f98bd1 100644 --- a/.github/workflows/v10-deploy-react-storybook.yml +++ b/.github/workflows/v10-deploy-react-storybook.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies diff --git a/.github/workflows/v10-release.yml b/.github/workflows/v10-release.yml index 60ba2e4423fd..acc9af5c9163 100644 --- a/.github/workflows/v10-release.yml +++ b/.github/workflows/v10-release.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 57c4b01b20c7..8095a7fecace 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -32,7 +32,7 @@ jobs: with: fetch-depth: '0' - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' From 0442850dc4676953421d090d36cc576e3c2028b8 Mon Sep 17 00:00:00 2001 From: TJ Egan Date: Tue, 13 Feb 2024 16:52:23 -0500 Subject: [PATCH 8/9] style(keyframes): prefix all keyframes to avoid collisions (#15682) --- packages/react/src/components/Copy/Copy-test.js | 14 ++++++++------ packages/react/src/components/Copy/Copy.js | 2 +- .../src/components/CopyButton/CopyButton-test.js | 9 +++++---- .../components/code-snippet/_code-snippet.scss | 4 ++-- .../scss/components/copy-button/_copy-button.scss | 4 ++-- .../components/inline-loading/_inline-loading.scss | 2 +- .../scss/components/inline-loading/_keyframes.scss | 4 +++- .../styles/scss/components/loading/_animation.scss | 11 ++++++----- .../styles/scss/components/loading/_loading.scss | 10 +++++----- .../components/progress-bar/_progress-bar.scss | 8 ++++---- packages/styles/scss/utilities/_keyframes.scss | 7 ++++--- packages/styles/scss/utilities/_skeleton.scss | 5 +++-- packages/styles/scss/utilities/_tooltip.scss | 5 +++-- 13 files changed, 47 insertions(+), 38 deletions(-) diff --git a/packages/react/src/components/Copy/Copy-test.js b/packages/react/src/components/Copy/Copy-test.js index 291c2b5b705d..dfbcdd9239ac 100644 --- a/packages/react/src/components/Copy/Copy-test.js +++ b/packages/react/src/components/Copy/Copy-test.js @@ -13,6 +13,8 @@ import { Copy as CopyIcon } from '@carbon/icons-react'; jest.useFakeTimers(); +const prefix = 'cds'; + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); describe('Copy', () => { @@ -95,15 +97,15 @@ describe('Feedback', () => { const button = screen.getByTestId('copy-button-5'); await user.click(button); - expect(button).toHaveClass('cds--copy-btn--animating'); + expect(button).toHaveClass(`${prefix}--copy-btn--animating`); // eslint-disable-next-line testing-library/no-unnecessary-act act(() => { jest.runAllTimers(); fireEvent.animationEnd(screen.getByTestId('copy-button-5'), { - animationName: 'hide-feedback', + animationName: `${prefix}--hide-feedback`, }); }); - expect(button).not.toHaveClass('cds--copy-btn--animating'); + expect(button).not.toHaveClass(`${prefix}--copy-btn--animating`); }); it('should be able to specify the feedback message', async () => { @@ -134,14 +136,14 @@ describe('Feedback', () => { const button = screen.getByTestId('copy-button-7'); await user.click(button); - expect(button).toHaveClass('cds--copy-btn--animating'); + expect(button).toHaveClass(`${prefix}--copy-btn--animating`); // eslint-disable-next-line testing-library/no-unnecessary-act act(() => { jest.runAllTimers(); fireEvent.animationEnd(screen.getByTestId('copy-button-7'), { - animationName: 'hide-feedback', + animationName: `${prefix}--hide-feedback`, }); }); - expect(button).not.toHaveClass('cds--copy-btn--animating'); + expect(button).not.toHaveClass(`${prefix}--copy-btn--animating`); }); }); diff --git a/packages/react/src/components/Copy/Copy.js b/packages/react/src/components/Copy/Copy.js index 10b4976a79f4..55c57d5404e1 100644 --- a/packages/react/src/components/Copy/Copy.js +++ b/packages/react/src/components/Copy/Copy.js @@ -44,7 +44,7 @@ export default function Copy({ }, [handleFadeOut]); const handleAnimationEnd = (event) => { - if (event.animationName === 'hide-feedback') { + if (event.animationName === `${prefix}--hide-feedback`) { setAnimation(''); } }; diff --git a/packages/react/src/components/CopyButton/CopyButton-test.js b/packages/react/src/components/CopyButton/CopyButton-test.js index 9d04876a1b77..e0c52694fdbd 100644 --- a/packages/react/src/components/CopyButton/CopyButton-test.js +++ b/packages/react/src/components/CopyButton/CopyButton-test.js @@ -12,6 +12,7 @@ import CopyButton from '../CopyButton'; jest.useFakeTimers(); const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); +const prefix = 'cds'; describe('CopyButton', () => { it('should set tabIndex if one is passed via props', () => { @@ -77,12 +78,12 @@ describe('Feedback', () => { const button = screen.getByTestId('copy-btn-5'); await user.click(button); - expect(button).toHaveClass('cds--copy-btn--animating'); + expect(button).toHaveClass(`${prefix}--copy-btn--animating`); // eslint-disable-next-line testing-library/no-unnecessary-act act(() => { jest.runAllTimers(); fireEvent.animationEnd(screen.getByTestId('copy-btn-5'), { - animationName: 'hide-feedback', + animationName: `${prefix}--hide-feedback`, }); }); }); @@ -113,12 +114,12 @@ describe('Feedback', () => { const button = screen.getByTestId('copy-btn-7'); await user.click(button); - expect(button).toHaveClass('cds--copy-btn--animating'); + expect(button).toHaveClass(`${prefix}--copy-btn--animating`); // eslint-disable-next-line testing-library/no-unnecessary-act act(() => { jest.runAllTimers(); fireEvent.animationEnd(screen.getByTestId('copy-btn-7'), { - animationName: 'hide-feedback', + animationName: `${prefix}--hide-feedback`, }); }); }); diff --git a/packages/styles/scss/components/code-snippet/_code-snippet.scss b/packages/styles/scss/components/code-snippet/_code-snippet.scss index 7dc757b43969..d66d0023125b 100644 --- a/packages/styles/scss/components/code-snippet/_code-snippet.scss +++ b/packages/styles/scss/components/code-snippet/_code-snippet.scss @@ -117,13 +117,13 @@ $copy-btn-feedback: $background-inverse !default; .#{$prefix}--snippet--inline.#{$prefix}--copy-btn--animating.#{$prefix}--copy-btn--fade-out::before, .#{$prefix}--snippet--inline.#{$prefix}--copy-btn--animating.#{$prefix}--copy-btn--fade-out .#{$prefix}--copy-btn__feedback { - animation: $duration-fast-02 motion(standard, productive) hide-feedback; + animation: $duration-fast-02 motion(standard, productive) #{$prefix}--hide-feedback; } .#{$prefix}--snippet--inline.#{$prefix}--copy-btn--animating.#{$prefix}--copy-btn--fade-in::before, .#{$prefix}--snippet--inline.#{$prefix}--copy-btn--animating.#{$prefix}--copy-btn--fade-in .#{$prefix}--copy-btn__feedback { - animation: $duration-fast-02 motion(standard, productive) show-feedback; + animation: $duration-fast-02 motion(standard, productive) #{$prefix}--show-feedback; } .#{$prefix}--snippet--inline code { diff --git a/packages/styles/scss/components/copy-button/_copy-button.scss b/packages/styles/scss/components/copy-button/_copy-button.scss index 9bda42e837c6..57fa28b1ddc0 100644 --- a/packages/styles/scss/components/copy-button/_copy-button.scss +++ b/packages/styles/scss/components/copy-button/_copy-button.scss @@ -73,13 +73,13 @@ &.#{$prefix}--copy-btn--animating.#{$prefix}--copy-btn--fade-out .#{$prefix}--copy-btn__feedback { // https://github.com/stylelint/stylelint/issues/2363 - animation: $duration-fast-02 motion(standard, productive) hide-feedback; + animation: $duration-fast-02 motion(standard, productive) #{$prefix}--hide-feedback; } &.#{$prefix}--copy-btn--animating.#{$prefix}--copy-btn--fade-in::before, &.#{$prefix}--copy-btn--animating.#{$prefix}--copy-btn--fade-in .#{$prefix}--copy-btn__feedback { - animation: $duration-fast-02 motion(standard, productive) show-feedback; + animation: $duration-fast-02 motion(standard, productive) #{$prefix}--show-feedback; } } diff --git a/packages/styles/scss/components/inline-loading/_inline-loading.scss b/packages/styles/scss/components/inline-loading/_inline-loading.scss index e8b538a9d6bd..29548d477a54 100644 --- a/packages/styles/scss/components/inline-loading/_inline-loading.scss +++ b/packages/styles/scss/components/inline-loading/_inline-loading.scss @@ -61,7 +61,7 @@ $-loading-gap-small: 110; .#{$prefix}--inline-loading__checkmark { animation-duration: 250ms; animation-fill-mode: forwards; - animation-name: stroke; + animation-name: #{$prefix}--stroke; fill: none; stroke: $interactive; stroke-dasharray: 12; diff --git a/packages/styles/scss/components/inline-loading/_keyframes.scss b/packages/styles/scss/components/inline-loading/_keyframes.scss index 498da41773eb..06ee0cb9bc10 100644 --- a/packages/styles/scss/components/inline-loading/_keyframes.scss +++ b/packages/styles/scss/components/inline-loading/_keyframes.scss @@ -5,7 +5,9 @@ // LICENSE file in the root directory of this source tree. // -@keyframes stroke { +@use '../../config' as *; + +@keyframes #{prefix}--stroke { 100% { stroke-dashoffset: 0; } diff --git a/packages/styles/scss/components/loading/_animation.scss b/packages/styles/scss/components/loading/_animation.scss index 17cd41543b43..f29e9936d2ae 100644 --- a/packages/styles/scss/components/loading/_animation.scss +++ b/packages/styles/scss/components/loading/_animation.scss @@ -6,19 +6,20 @@ // @use '../../motion'; +@use '../../config' as *; @mixin spin { // Animate the container animation-duration: 690ms; animation-fill-mode: forwards; animation-iteration-count: infinite; - animation-name: rotate; + animation-name: #{$prefix}--rotate; animation-timing-function: linear; // Animate the stroke svg circle { animation-duration: 10ms; - animation-name: init-stroke; + animation-name: #{$prefix}--init-stroke; animation-timing-function: motion.$standard-easing; @media screen and (prefers-reduced-motion: reduce) { @@ -29,15 +30,15 @@ @mixin stop { // Animate the container - animation: rotate-end-p1 700ms motion.$ease-out forwards, - rotate-end-p2 700ms motion.$ease-out 700ms forwards; + animation: #{$prefix}--rotate-end-p1 700ms motion.$ease-out forwards, + #{$prefix}--rotate-end-p2 700ms motion.$ease-out 700ms forwards; // Animate the stroke svg circle { animation-delay: 700ms; animation-duration: 700ms; animation-fill-mode: forwards; - animation-name: stroke-end; + animation-name: #{$prefix}--stroke-end; animation-timing-function: motion.$ease-out; @media screen and (prefers-reduced-motion: reduce) { diff --git a/packages/styles/scss/components/loading/_loading.scss b/packages/styles/scss/components/loading/_loading.scss index 9c75ca966e64..4b527be8de2b 100644 --- a/packages/styles/scss/components/loading/_loading.scss +++ b/packages/styles/scss/components/loading/_loading.scss @@ -103,7 +103,7 @@ display: none; } - @keyframes rotate { + @keyframes #{$prefix}--rotate { 0% { transform: rotate(0deg); } @@ -113,20 +113,20 @@ } } - @keyframes rotate-end-p1 { + @keyframes #{$prefix}--rotate-end-p1 { 100% { transform: rotate(360deg); } } - @keyframes rotate-end-p2 { + @keyframes #{$prefix}--rotate-end-p2 { 100% { transform: rotate(-360deg); } } /* Stroke animations */ - @keyframes init-stroke { + @keyframes #{$prefix}--init-stroke { 0% { stroke-dashoffset: loading-progress($circumference, 0); } @@ -136,7 +136,7 @@ } } - @keyframes stroke-end { + @keyframes #{$prefix}--stroke-end { 0% { stroke-dashoffset: loading-progress($circumference, 81); } diff --git a/packages/styles/scss/components/progress-bar/_progress-bar.scss b/packages/styles/scss/components/progress-bar/_progress-bar.scss index df661abb300a..063dfc5abeb1 100644 --- a/packages/styles/scss/components/progress-bar/_progress-bar.scss +++ b/packages/styles/scss/components/progress-bar/_progress-bar.scss @@ -75,7 +75,7 @@ position: absolute; animation-duration: 1400ms; animation-iteration-count: infinite; - animation-name: progress-bar-indeterminate; + animation-name: #{$prefix}--progress-bar-indeterminate; animation-timing-function: linear; background-image: linear-gradient( 90deg, @@ -91,7 +91,7 @@ [dir='rtl'] .#{$prefix}--progress-bar--indeterminate .#{$prefix}--progress-bar__track::after { - animation-name: progress-bar-indeterminate-rtl; + animation-name: #{$prefix}--progress-bar-indeterminate-rtl; } .#{$prefix}--progress-bar__helper-text { @@ -139,7 +139,7 @@ margin-inline-end: 0; } - @keyframes progress-bar-indeterminate { + @keyframes #{$prefix}--progress-bar-indeterminate { 0% { background-position-x: 25%; } @@ -150,7 +150,7 @@ } } - @keyframes progress-bar-indeterminate-rtl { + @keyframes #{$prefix}--progress-bar-indeterminate-rtl { 0% { background-position-x: -105%; } diff --git a/packages/styles/scss/utilities/_keyframes.scss b/packages/styles/scss/utilities/_keyframes.scss index afaea6ed0237..d0bc18d7c7cd 100644 --- a/packages/styles/scss/utilities/_keyframes.scss +++ b/packages/styles/scss/utilities/_keyframes.scss @@ -4,6 +4,7 @@ // This source code is licensed under the Apache-2.0 license found in the // LICENSE file in the root directory of this source tree. // +@use '../config' as *; @mixin content-visible { opacity: 1; @@ -15,7 +16,7 @@ visibility: hidden; } -@keyframes hide-feedback { +@keyframes #{$prefix}--hide-feedback { 0% { @include content-visible; } @@ -25,7 +26,7 @@ } } -@keyframes show-feedback { +@keyframes #{$prefix}--show-feedback { 0% { @include content-hidden; } @@ -35,7 +36,7 @@ } } -@keyframes skeleton { +@keyframes #{$prefix}--skeleton { 0% { opacity: 0.3; transform: scaleX(0); diff --git a/packages/styles/scss/utilities/_skeleton.scss b/packages/styles/scss/utilities/_skeleton.scss index 3fd619511050..05efa94c3d91 100644 --- a/packages/styles/scss/utilities/_skeleton.scss +++ b/packages/styles/scss/utilities/_skeleton.scss @@ -7,6 +7,7 @@ @use 'keyframes'; @use '../theme' as *; +@use '../config' as *; /// Skeleton loading animation /// @access public @@ -30,7 +31,7 @@ &::before { position: absolute; - animation: 3000ms ease-in-out skeleton infinite; + animation: 3000ms ease-in-out #{$prefix}--skeleton infinite; background: $skeleton-element; block-size: 100%; content: ''; @@ -55,7 +56,7 @@ &::before { position: absolute; - animation: 3000ms ease-in-out skeleton infinite; + animation: 3000ms ease-in-out #{$prefix}--skeleton infinite; background: $skeleton-element; block-size: 100%; content: ''; diff --git a/packages/styles/scss/utilities/_tooltip.scss b/packages/styles/scss/utilities/_tooltip.scss index 09d83167e450..8a453b9c1e03 100644 --- a/packages/styles/scss/utilities/_tooltip.scss +++ b/packages/styles/scss/utilities/_tooltip.scss @@ -182,7 +182,7 @@ opacity: 1; } - @keyframes tooltip-fade { + @keyframes #{$prefix}--tooltip-fade { from { opacity: 0; } @@ -202,7 +202,8 @@ .#{$prefix}--assistive-text, + .#{$prefix}--assistive-text, &.#{$prefix}--tooltip--a11y::before { - animation: tooltip-fade $duration-fast-01 motion(standard, productive); + animation: #{$prefix}--tooltip-fade $duration-fast-01 + motion(standard, productive); } } From 1ca2ae7dd02208b00f44227ecf72427fcb1f358d Mon Sep 17 00:00:00 2001 From: TJ Egan Date: Wed, 14 Feb 2024 12:29:57 -0500 Subject: [PATCH 9/9] fix(Modal): add tabindex to content body when content is scrollable, storybook enhancements (#15515) * fix(Modal): add tabIndex if content is scrollable * fix(Modal): add tabIndex is content is scrollable, storybook fixes * chore(test): update snapshots * chore(test): revert accidentaly prop changes * fix(Slug): update Slug Modal examples to show open/close button --- .../ComposedModal/ComposedModal.stories.js | 430 +++++++------ .../ComposedModal/ComposedModal.tsx | 51 +- .../src/components/Modal/Modal.stories.js | 585 ++++++++++-------- packages/react/src/components/Modal/Modal.tsx | 49 +- .../components/Slug/Slug-examples.stories.js | 129 ++-- 5 files changed, 685 insertions(+), 559 deletions(-) diff --git a/packages/react/src/components/ComposedModal/ComposedModal.stories.js b/packages/react/src/components/ComposedModal/ComposedModal.stories.js index 25b74d738095..7f47641fb028 100644 --- a/packages/react/src/components/ComposedModal/ComposedModal.stories.js +++ b/packages/react/src/components/ComposedModal/ComposedModal.stories.js @@ -41,99 +41,111 @@ export default { }; export const Default = () => { + const [open, setOpen] = useState(true); return ( - - - -

    - Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

    - - -
    - -
    + <> + + setOpen(false)}> + + +

    + Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

    + + +
    + +
    + ); }; export const FullWidth = () => { + const [open, setOpen] = useState(true); return ( - - - - - - - - Column A - - - Column B - - - Column C - - - - - - Row 1 - Row 1 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc - dui magna, finibus id tortor sed, aliquet bibendum augue. Aenean - posuere sem vel euismod dignissim. Nulla ut cursus dolor. - Pellentesque vulputate nisl a porttitor interdum. - - - - Row 2 - Row 2 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc - dui magna, finibus id tortor sed, aliquet bibendum augue. Aenean - posuere sem vel euismod dignissim. Nulla ut cursus dolor. - Pellentesque vulputate nisl a porttitor interdum. - - - - Row 3 - Row 3 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc - dui magna, finibus id tortor sed, aliquet bibendum augue. Aenean - posuere sem vel euismod dignissim. Nulla ut cursus dolor. - Pellentesque vulputate nisl a porttitor interdum. - - - - - - - + <> + + setOpen(false)} isFullWidth> + + + + + + + Column A + + + Column B + + + Column C + + + + + + Row 1 + Row 1 + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc + dui magna, finibus id tortor sed, aliquet bibendum augue. + Aenean posuere sem vel euismod dignissim. Nulla ut cursus + dolor. Pellentesque vulputate nisl a porttitor interdum. + + + + Row 2 + Row 2 + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc + dui magna, finibus id tortor sed, aliquet bibendum augue. + Aenean posuere sem vel euismod dignissim. Nulla ut cursus + dolor. Pellentesque vulputate nisl a porttitor interdum. + + + + Row 3 + Row 3 + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc + dui magna, finibus id tortor sed, aliquet bibendum augue. + Aenean posuere sem vel euismod dignissim. Nulla ut cursus + dolor. Pellentesque vulputate nisl a porttitor interdum. + + + + + + + + ); }; export const PassiveModal = () => { + const [open, setOpen] = useState(true); return ( - - - - + <> + + setOpen(false)}> + + + + ); }; @@ -201,75 +213,80 @@ export const WithStateManager = () => { }; export const WithScrollingContent = () => { + const [open, setOpen] = useState(true); return ( - - - -

    - Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus eu - nibh odio. Nunc a consequat est, id porttitor sapien. Proin vitae leo - vitae orci tincidunt auctor eget eget libero. Ut tincidunt ultricies - fringilla. Aliquam erat volutpat. Aenean arcu odio, elementum vel - vehicula vitae, porttitor ac lorem. Sed viverra elit ac risus - tincidunt fermentum. Ut sollicitudin nibh id risus ornare ornare. - Etiam gravida orci ut lectus dictum, quis ultricies felis mollis. - Mauris nec commodo est, nec faucibus nibh. Nunc commodo ante quis - pretium consectetur. Ut ac nisl vitae mi mattis vulputate a at elit. - Nullam porttitor ex eget mi feugiat mattis. Nunc non sodales magna. - Proin ornare tellus quis hendrerit egestas. Donec pharetra leo nec - molestie sollicitudin.{' '} -

    - -
    - -
    - - (item ? item.text : '')} - /> -
    - -
    + <> + + setOpen(false)}> + + +

    + Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus + eu nibh odio. Nunc a consequat est, id porttitor sapien. Proin vitae + leo vitae orci tincidunt auctor eget eget libero. Ut tincidunt + ultricies fringilla. Aliquam erat volutpat. Aenean arcu odio, + elementum vel vehicula vitae, porttitor ac lorem. Sed viverra elit + ac risus tincidunt fermentum. Ut sollicitudin nibh id risus ornare + ornare. Etiam gravida orci ut lectus dictum, quis ultricies felis + mollis. Mauris nec commodo est, nec faucibus nibh. Nunc commodo ante + quis pretium consectetur. Ut ac nisl vitae mi mattis vulputate a at + elit. Nullam porttitor ex eget mi feugiat mattis. Nunc non sodales + magna. Proin ornare tellus quis hendrerit egestas. Donec pharetra + leo nec molestie sollicitudin.{' '} +

    + +
    + +
    + + (item ? item.text : '')} + /> +
    + +
    + ); }; export const WithInlineLoading = () => { + const [open, setOpen] = useState(true); const [status, setStatus] = useState('inactive'); const [description, setDescription] = useState('Submitting...'); @@ -296,70 +313,77 @@ export const WithInlineLoading = () => { }; return ( - - - -

    - Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

    - + + setOpen(false)}> + + +

    + Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

    + + +
    + - -
    - -
    + + ); }; export const Playground = (args) => { + const [open, setOpen] = useState(true); return ( - - - -

    - Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

    - + + setOpen(false)}> + + +

    + Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

    + + +
    + - -
    - -
    + + ); }; diff --git a/packages/react/src/components/ComposedModal/ComposedModal.tsx b/packages/react/src/components/ComposedModal/ComposedModal.tsx index d77d54658c13..a5295638ceae 100644 --- a/packages/react/src/components/ComposedModal/ComposedModal.tsx +++ b/packages/react/src/components/ComposedModal/ComposedModal.tsx @@ -13,11 +13,12 @@ import { isElement } from 'react-is'; import PropTypes, { ReactNodeLike } from 'prop-types'; import { ModalHeader, type ModalHeaderProps } from './ModalHeader'; import { ModalFooter, type ModalFooterProps } from './ModalFooter'; +import debounce from 'lodash.debounce'; +import useIsomorphicEffect from '../../internal/useIsomorphicEffect'; +import mergeRefs from '../../tools/mergeRefs'; import cx from 'classnames'; - import toggleClass from '../../tools/toggleClass'; import requiredIfGivenPropIsTruthy from '../../prop-types/requiredIfGivenPropIsTruthy'; - import wrapFocus from '../../internal/wrapFocus'; import { usePrefix } from '../../internal/usePrefix'; import { keys, match } from '../../internal/keyboard'; @@ -50,23 +51,49 @@ export const ModalBody = React.forwardRef( ref ) { const prefix = usePrefix(); - const contentClass = cx( - `${prefix}--modal-content`, - hasForm && `${prefix}--modal-content--with-form`, - hasScrollingContent && `${prefix}--modal-scroll-content`, - customClassName - ); + const contentRef = useRef(null); + const [isScrollable, setIsScrollable] = useState(false); + const contentClass = cx({ + [`${prefix}--modal-content`]: true, + [`${prefix}--modal-content--with-form`]: hasForm, + [`${prefix}--modal-scroll-content`]: hasScrollingContent || isScrollable, + customClassName, + }); + + useIsomorphicEffect(() => { + if (contentRef.current) { + setIsScrollable( + contentRef.current.scrollHeight > contentRef.current.clientHeight + ); + } + + function handler() { + if (contentRef.current) { + setIsScrollable( + contentRef.current.scrollHeight > contentRef.current.clientHeight + ); + } + } + + const debouncedHandler = debounce(handler, 200); + window.addEventListener('resize', debouncedHandler); + return () => { + debouncedHandler.cancel(); + window.removeEventListener('resize', debouncedHandler); + }; + }, []); - const hasScrollingContentProps = hasScrollingContent - ? { tabIndex: 0, role: 'region' } - : {}; + const hasScrollingContentProps = + hasScrollingContent || isScrollable + ? { tabIndex: 0, role: 'region' } + : {}; return (
    + ref={mergeRefs(contentRef, ref)}> {children}
    ); diff --git a/packages/react/src/components/Modal/Modal.stories.js b/packages/react/src/components/Modal/Modal.stories.js index c3d5961c4244..fb60764d3560 100644 --- a/packages/react/src/components/Modal/Modal.stories.js +++ b/packages/react/src/components/Modal/Modal.stories.js @@ -35,128 +35,143 @@ export default { }; export const Default = () => { + const [open, setOpen] = useState(true); return ( - -

    - Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

    - - - - (item ? item.text : '')} - /> -
    + <> + + setOpen(false)} + modalHeading="Add a custom domain" + modalLabel="Account resources" + primaryButtonText="Add" + secondaryButtonText="Cancel"> +

    + Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

    + + + + (item ? item.text : '')} + /> +
    + ); }; export const FullWidth = () => { + const [open, setOpen] = useState(true); return ( - - - - - - Column A - - - Column B - - - Column C - - - - - - Row 1 - Row 1 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc dui - magna, finibus id tortor sed, aliquet bibendum augue. Aenean - posuere sem vel euismod dignissim. Nulla ut cursus dolor. - Pellentesque vulputate nisl a porttitor interdum. - - - - Row 2 - Row 2 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc dui - magna, finibus id tortor sed, aliquet bibendum augue. Aenean - posuere sem vel euismod dignissim. Nulla ut cursus dolor. - Pellentesque vulputate nisl a porttitor interdum. - - - - Row 3 - Row 3 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc dui - magna, finibus id tortor sed, aliquet bibendum augue. Aenean - posuere sem vel euismod dignissim. Nulla ut cursus dolor. - Pellentesque vulputate nisl a porttitor interdum. - - - - - + <> + + setOpen(false)} + isFullWidth + modalHeading="Full Width Modal" + modalLabel="An example of a modal with no padding" + primaryButtonText="Add" + secondaryButtonText="Cancel"> + + + + + Column A + + + Column B + + + Column C + + + + + + Row 1 + Row 1 + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc + dui magna, finibus id tortor sed, aliquet bibendum augue. Aenean + posuere sem vel euismod dignissim. Nulla ut cursus dolor. + Pellentesque vulputate nisl a porttitor interdum. + + + + Row 2 + Row 2 + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc + dui magna, finibus id tortor sed, aliquet bibendum augue. Aenean + posuere sem vel euismod dignissim. Nulla ut cursus dolor. + Pellentesque vulputate nisl a porttitor interdum. + + + + Row 3 + Row 3 + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc + dui magna, finibus id tortor sed, aliquet bibendum augue. Aenean + posuere sem vel euismod dignissim. Nulla ut cursus dolor. + Pellentesque vulputate nisl a porttitor interdum. + + + + + + ); }; export const DangerModal = () => { + const [open, setOpen] = useState(true); return ( - + <> + + setOpen(false)} + danger + modalHeading="Are you sure you want to delete this custom domain?" + modalLabel="Account resources" + primaryButtonText="Delete" + secondaryButtonText="Cancel" + /> + ); }; @@ -199,156 +214,170 @@ const modalFooter = (numberOfButtons) => { }; export const WithScrollingContent = () => { + const [open, setOpen] = useState(true); return ( - -

    - Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus eu - nibh odio. Nunc a consequat est, id porttitor sapien. Proin vitae leo - vitae orci tincidunt auctor eget eget libero. Ut tincidunt ultricies - fringilla. Aliquam erat volutpat. Aenean arcu odio, elementum vel - vehicula vitae, porttitor ac lorem. Sed viverra elit ac risus tincidunt - fermentum. Ut sollicitudin nibh id risus ornare ornare. Etiam gravida - orci ut lectus dictum, quis ultricies felis mollis. Mauris nec commodo - est, nec faucibus nibh. Nunc commodo ante quis pretium consectetur. Ut - ac nisl vitae mi mattis vulputate a at elit. Nullam porttitor ex eget mi - feugiat mattis. Nunc non sodales magna. Proin ornare tellus quis - hendrerit egestas. Donec pharetra leo nec molestie sollicitudin.{' '} -

    - -
    - -
    - - (item ? item.text : '')} - /> -
    + <> + + setOpen(false)} + hasScrollingContent + modalHeading="Add a custom domain" + modalLabel="Account resources" + primaryButtonText="Add" + secondaryButtonText="Cancel"> +

    + Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus eu + nibh odio. Nunc a consequat est, id porttitor sapien. Proin vitae leo + vitae orci tincidunt auctor eget eget libero. Ut tincidunt ultricies + fringilla. Aliquam erat volutpat. Aenean arcu odio, elementum vel + vehicula vitae, porttitor ac lorem. Sed viverra elit ac risus + tincidunt fermentum. Ut sollicitudin nibh id risus ornare ornare. + Etiam gravida orci ut lectus dictum, quis ultricies felis mollis. + Mauris nec commodo est, nec faucibus nibh. Nunc commodo ante quis + pretium consectetur. Ut ac nisl vitae mi mattis vulputate a at elit. + Nullam porttitor ex eget mi feugiat mattis. Nunc non sodales magna. + Proin ornare tellus quis hendrerit egestas. Donec pharetra leo nec + molestie sollicitudin.{' '} +

    + +
    + +
    + + (item ? item.text : '')} + /> +
    + ); }; export const Playground = ({ numberOfButtons, ...args }) => { + const [open, setOpen] = useState(true); return ( - -

    - Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

    - - - {args.hasScrollingContent && ( - <> -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id - accumsan augue. Phasellus consequat augue vitae tellus tincidunt - posuere. Curabitur justo urna, consectetur vel elit iaculis, - ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie - tellus. Quisque consectetur non risus eu rutrum.{' '} -

    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id - accumsan augue. Phasellus consequat augue vitae tellus tincidunt - posuere. Curabitur justo urna, consectetur vel elit iaculis, - ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie - tellus. Quisque consectetur non risus eu rutrum.{' '} -

    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id - accumsan augue. Phasellus consequat augue vitae tellus tincidunt - posuere. Curabitur justo urna, consectetur vel elit iaculis, - ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie - tellus. Quisque consectetur non risus eu rutrum.{' '} -

    -

    Lorem ipsum

    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id - accumsan augue. Phasellus consequat augue vitae tellus tincidunt - posuere. Curabitur justo urna, consectetur vel elit iaculis, - ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie - tellus. Quisque consectetur non risus eu rutrum.{' '} -

    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id - accumsan augue. Phasellus consequat augue vitae tellus tincidunt - posuere. Curabitur justo urna, consectetur vel elit iaculis, - ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie - tellus. Quisque consectetur non risus eu rutrum.{' '} -

    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id - accumsan augue. Phasellus consequat augue vitae tellus tincidunt - posuere. Curabitur justo urna, consectetur vel elit iaculis, - ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie - tellus. Quisque consectetur non risus eu rutrum.{' '} -

    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id - accumsan augue. Phasellus consequat augue vitae tellus tincidunt - posuere. Curabitur justo urna, consectetur vel elit iaculis, - ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie - tellus. Quisque consectetur non risus eu rutrum.{' '} -

    - - )} -
    + <> + + { + action(e); + setOpen(false); + }} + modalHeading="Add a custom domain" + primaryButtonText="Add" + secondaryButtonText="Cancel" + aria-label="Modal content" + open={open} + {...args} + {...modalFooter(numberOfButtons)}> +

    + Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

    + + + {args.hasScrollingContent && ( + <> +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis + molestie tellus. Quisque consectetur non risus eu rutrum.{' '} +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis + molestie tellus. Quisque consectetur non risus eu rutrum.{' '} +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis + molestie tellus. Quisque consectetur non risus eu rutrum.{' '} +

    +

    Lorem ipsum

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis + molestie tellus. Quisque consectetur non risus eu rutrum.{' '} +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis + molestie tellus. Quisque consectetur non risus eu rutrum.{' '} +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis + molestie tellus. Quisque consectetur non risus eu rutrum.{' '} +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis + molestie tellus. Quisque consectetur non risus eu rutrum.{' '} +

    + + )} +
    + ); }; @@ -390,9 +419,6 @@ Playground.argTypes = { onKeyDown: { action: 'onKeyDown', }, - onRequestClose: { - action: 'onRequestClose', - }, onRequestSubmit: { action: 'onRequestSubmit', }, @@ -491,11 +517,17 @@ export const WithStateManager = () => { }; export const PassiveModal = () => { + const [open, setOpen] = useState(true); return ( - + <> + + setOpen(false)} + passiveModal + modalHeading="You have been successfully signed out" + /> + ); }; @@ -525,18 +557,23 @@ export const WithInlineLoading = () => { setDescription('Deleting...'); }; + const [open, setOpen] = useState(true); return ( - + <> + + setOpen(false)} + danger + modalHeading="Are you sure you want to delete this custom domain?" + modalLabel="Account resources" + primaryButtonText="Delete" + secondaryButtonText="Cancel" + onRequestSubmit={submit} + loadingStatus={status} + loadingDescription={description} + onLoadingSuccess={resetStatus} + /> + ); }; diff --git a/packages/react/src/components/Modal/Modal.tsx b/packages/react/src/components/Modal/Modal.tsx index 0af0f7b8f80f..98381d541251 100644 --- a/packages/react/src/components/Modal/Modal.tsx +++ b/packages/react/src/components/Modal/Modal.tsx @@ -6,7 +6,7 @@ */ import PropTypes, { ReactNodeLike } from 'prop-types'; -import React, { useRef, useEffect } from 'react'; +import React, { useRef, useEffect, useState } from 'react'; import classNames from 'classnames'; import { Close } from '@carbon/icons-react'; import toggleClass from '../../tools/toggleClass'; @@ -17,6 +17,8 @@ import requiredIfGivenPropIsTruthy from '../../prop-types/requiredIfGivenPropIsT import wrapFocus, { elementOrParentIsFloatingMenu, } from '../../internal/wrapFocus'; +import debounce from 'lodash.debounce'; +import useIsomorphicEffect from '../../internal/useIsomorphicEffect'; import setupGetInstanceId from '../../tools/setupGetInstanceId'; import { usePrefix } from '../../internal/usePrefix'; import { keys, match } from '../../internal/keyboard'; @@ -253,9 +255,11 @@ const Modal = React.forwardRef(function Modal( const prefix = usePrefix(); const button = useRef(null); const secondaryButton = useRef(); + const contentRef = useRef(null); const innerModal = useRef(null); const startTrap = useRef(null); const endTrap = useRef(null); + const [isScrollable, setIsScrollable] = useState(false); const modalInstanceId = `modal-${getInstanceId()}`; const modalLabelId = `${prefix}--modal-header__label--${modalInstanceId}`; const modalHeadingId = `${prefix}--modal-header__heading--${modalInstanceId}`; @@ -341,7 +345,7 @@ const Modal = React.forwardRef(function Modal( }); const contentClasses = classNames(`${prefix}--modal-content`, { - [`${prefix}--modal-scroll-content`]: hasScrollingContent, + [`${prefix}--modal-scroll-content`]: hasScrollingContent || isScrollable, }); const footerClasses = classNames(`${prefix}--modal-footer`, { @@ -358,14 +362,15 @@ const Modal = React.forwardRef(function Modal( modalLabelStr || ariaLabelProp || modalAriaLabel || modalHeadingStr; const getAriaLabelledBy = modalLabel ? modalLabelId : modalHeadingId; - const hasScrollingContentProps = hasScrollingContent - ? { - tabIndex: 0, - role: 'region', - 'aria-label': ariaLabel, - 'aria-labelledby': getAriaLabelledBy, - } - : {}; + const hasScrollingContentProps = + hasScrollingContent || isScrollable + ? { + tabIndex: 0, + role: 'region', + 'aria-label': ariaLabel, + 'aria-labelledby': getAriaLabelledBy, + } + : {}; const alertDialogProps: ReactAttr = {}; if (alert && passiveModal) { @@ -426,6 +431,29 @@ const Modal = React.forwardRef(function Modal( } }, [open, selectorPrimaryFocus, danger, prefix]); + useIsomorphicEffect(() => { + if (contentRef.current) { + setIsScrollable( + contentRef.current.scrollHeight > contentRef.current.clientHeight + ); + } + + function handler() { + if (contentRef.current) { + setIsScrollable( + contentRef.current.scrollHeight > contentRef.current.clientHeight + ); + } + } + + const debouncedHandler = debounce(handler, 200); + window.addEventListener('resize', debouncedHandler); + return () => { + debouncedHandler.cancel(); + window.removeEventListener('resize', debouncedHandler); + }; + }, []); + // Slug is always size `lg` let normalizedSlug; if (slug && slug['type']?.displayName === 'Slug') { @@ -483,6 +511,7 @@ const Modal = React.forwardRef(function Modal( {!passiveModal && modalButton}
    diff --git a/packages/react/src/components/Slug/Slug-examples.stories.js b/packages/react/src/components/Slug/Slug-examples.stories.js index 8e8e92a506a6..9a56b41354b0 100644 --- a/packages/react/src/components/Slug/Slug-examples.stories.js +++ b/packages/react/src/components/Slug/Slug-examples.stories.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import Button from '../Button'; import Checkbox from '../Checkbox'; import CheckboxGroup from '../CheckboxGroup'; @@ -282,38 +282,42 @@ export const _ComposedModal = { '**Experimental**: Provide a `Slug` component to be rendered inside the component', }, }, - render: () => ( -
    - - - -

    - Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

    - { + const [open, setOpen] = useState(true); // eslint-disable-line + return ( +
    + + setOpen(false)} slug={slug}> + + +

    + Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a + shared domain, a shared subdomain, or a shared domain and host. +

    + + +
    + - - - -
    -
    - ), +
    +
    + ); + }, }; export const _DatePicker = { @@ -382,34 +386,39 @@ export const _Modal = { '**Experimental**: Provide a `Slug` component to be rendered inside the component', }, }, - render: () => ( -
    - -

    - Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

    - - -