Skip to content

Commit

Permalink
feat(Modal): add slug to Modal, ComposedModal (#15350)
Browse files Browse the repository at this point in the history
* feat(Modal): add slug to Modal, ComposedModal

* test(snapshot): update snapshots

* docs(Slug): add slug to controls table

* fix(Modal): remove shadows, use callout gradient

* test(snapshot): update snapshots

* fix(Modal): adjust background, fix conditional class
  • Loading branch information
tw15egan committed Dec 13, 2023
1 parent beede51 commit 3748b40
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 15 deletions.
6 changes: 6 additions & 0 deletions packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,9 @@ Map {
],
"type": "oneOf",
},
"slug": Object {
"type": "node",
},
},
"render": [Function],
},
Expand Down Expand Up @@ -4833,6 +4836,9 @@ Map {
],
"type": "oneOf",
},
"slug": Object {
"type": "node",
},
},
"render": [Function],
},
Expand Down
29 changes: 26 additions & 3 deletions packages/react/src/components/ComposedModal/ComposedModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import React, {
type RefObject,
} from 'react';
import { isElement } from 'react-is';
import PropTypes from 'prop-types';
import PropTypes, { ReactNodeLike } from 'prop-types';
import { ModalHeader, type ModalHeaderProps } from './ModalHeader';
import { ModalFooter, type ModalFooterProps } from './ModalFooter';

Expand Down Expand Up @@ -180,6 +180,11 @@ export interface ComposedModalProps extends HTMLAttributes<HTMLDivElement> {
selectorsFloatingMenus?: Array<string | null | undefined>;

size?: 'xs' | 'sm' | 'md' | 'lg';

/**
* **Experimental**: Provide a `Slug` component to be rendered inside the `ComposedModal` component
*/
slug?: ReactNodeLike;
}

const ComposedModal = React.forwardRef<HTMLDivElement, ComposedModalProps>(
Expand All @@ -200,6 +205,7 @@ const ComposedModal = React.forwardRef<HTMLDivElement, ComposedModalProps>(
selectorsFloatingMenus,
size,
launcherButtonRef,
slug,
...rest
},
ref
Expand Down Expand Up @@ -270,8 +276,11 @@ const ComposedModal = React.forwardRef<HTMLDivElement, ComposedModalProps>(

const modalClass = cx(
`${prefix}--modal`,
isOpen && 'is-visible',
danger && `${prefix}--modal--danger`,
{
'is-visible': isOpen,
[`${prefix}--modal--danger`]: danger,
[`${prefix}--modal--slug`]: slug,
},
customClassName
);

Expand Down Expand Up @@ -344,6 +353,14 @@ const ComposedModal = React.forwardRef<HTMLDivElement, ComposedModalProps>(
}
}, [open, selectorPrimaryFocus, isOpen]);

// Slug is always size `lg`
let normalizedSlug;
if (slug && slug['type']?.displayName === 'Slug') {
normalizedSlug = React.cloneElement(slug as React.ReactElement<any>, {
size: 'lg',
});
}

return (
<div
{...rest}
Expand All @@ -369,6 +386,7 @@ const ComposedModal = React.forwardRef<HTMLDivElement, ComposedModalProps>(
Focus sentinel
</button>
<div ref={innerModal} className={`${prefix}--modal-container-body`}>
{normalizedSlug}
{childrenWithProps}
</div>
{/* Non-translatable: Focus-wrap code makes this `<button>` not actually read by screen readers */}
Expand Down Expand Up @@ -466,6 +484,11 @@ ComposedModal.propTypes = {
* Specify the size variant.
*/
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),

/**
* **Experimental**: Provide a `Slug` component to be rendered inside the `ComposedModal` component
*/
slug: PropTypes.node,
};

export default ComposedModal;
23 changes: 22 additions & 1 deletion packages/react/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

import PropTypes from 'prop-types';
import PropTypes, { ReactNodeLike } from 'prop-types';
import React, { useRef, useEffect } from 'react';
import classNames from 'classnames';
import { Close } from '@carbon/icons-react';
Expand Down Expand Up @@ -205,6 +205,11 @@ export interface ModalProps extends ReactAttr<HTMLDivElement> {
* Specify the size variant.
*/
size?: ModalSize;

/**
* **Experimental**: Provide a `Slug` component to be rendered inside the `Modal` component
*/
slug?: ReactNodeLike;
}

const Modal = React.forwardRef(function Modal(
Expand Down Expand Up @@ -239,6 +244,7 @@ const Modal = React.forwardRef(function Modal(
loadingDescription,
loadingIconDescription,
onLoadingSuccess = noopFn,
slug,
...rest
}: ModalProps,
ref: React.LegacyRef<HTMLDivElement>
Expand Down Expand Up @@ -323,6 +329,7 @@ const Modal = React.forwardRef(function Modal(
[`${prefix}--modal-tall`]: !passiveModal,
'is-visible': open,
[`${prefix}--modal--danger`]: danger,
[`${prefix}--modal--slug`]: slug,
},
className
);
Expand Down Expand Up @@ -418,6 +425,14 @@ const Modal = React.forwardRef(function Modal(
}
}, [open, selectorPrimaryFocus, danger, prefix]);

// Slug is always size `lg`
let normalizedSlug;
if (slug && slug['type']?.displayName === 'Slug') {
normalizedSlug = React.cloneElement(slug as React.ReactElement<any>, {
size: 'lg',
});
}

const modalButton = (
<button
className={modalCloseButtonClass}
Expand Down Expand Up @@ -460,6 +475,7 @@ const Modal = React.forwardRef(function Modal(
className={`${prefix}--modal-header__heading`}>
{modalHeading}
</Text>
{normalizedSlug}
{!passiveModal && modalButton}
</div>
<div
Expand Down Expand Up @@ -752,6 +768,11 @@ Modal.propTypes = {
* Specify the size variant.
*/
size: PropTypes.oneOf(ModalSizes),

/**
* **Experimental**: Provide a `Slug` component to be rendered inside the `Modal` component
*/
slug: PropTypes.node,
};

export default Modal;
87 changes: 86 additions & 1 deletion packages/react/src/components/Slug/Slug-examples.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@ import Button from '../Button';
import Checkbox from '../Checkbox';
import CheckboxGroup from '../CheckboxGroup';
import ComboBox from '../ComboBox';
import {
ComposedModal,
ModalBody,
ModalHeader,
ModalFooter,
} from '../ComposedModal';
import DatePicker from '../DatePicker';
import DatePickerInput from '../DatePickerInput';
import Dropdown from '../Dropdown';
import Modal from '../Modal';
import { MultiSelect, FilterableMultiSelect } from '../MultiSelect';
import { NumberInput } from '../NumberInput';
import RadioButton from '../RadioButton';
Expand Down Expand Up @@ -146,7 +153,7 @@ const items = [
];

const slug = (
<Slug>
<Slug className="slug-container">
<SlugContent>
<div>
<p className="secondary">AI Explained</p>
Expand Down Expand Up @@ -268,6 +275,47 @@ export const _Combobox = {
),
};

export const _ComposedModal = {
argTypes: {
slug: {
description:
'**Experimental**: Provide a `Slug` component to be rendered inside the component',
},
},
render: () => (
<div className="slug-modal">
<ComposedModal slug={slug} open>
<ModalHeader label="Account resources" title="Add a custom domain" />
<ModalBody>
<p style={{ marginBottom: '1rem' }}>
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.
</p>
<TextInput
data-modal-primary-focus
id="text-input-1"
labelText="Domain name"
placeholder="e.g. github.com"
style={{ marginBottom: '1rem' }}
/>
<Select id="select-1" defaultValue="us-south" labelText="Region">
<SelectItem value="us-south" text="US South" />
<SelectItem value="us-east" text="US East" />
</Select>
</ModalBody>
<ModalFooter
primaryButtonText="Add"
secondaryButtons={[
{ buttonText: 'Keep both' },
{ buttonText: 'Rename' },
]}
/>
</ComposedModal>
</div>
),
};

export const _DatePicker = {
args: args,
argTypes: argTypes,
Expand Down Expand Up @@ -327,6 +375,43 @@ export const _FilterableMultiselect = {
),
};

export const _Modal = {
argTypes: {
slug: {
description:
'**Experimental**: Provide a `Slug` component to be rendered inside the component',
},
},
render: () => (
<div className="slug-modal">
<Modal
open
modalHeading="Add a custom domain"
modalLabel="Account resources"
primaryButtonText="Add"
secondaryButtonText="Cancel"
slug={slug}>
<p>
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.
</p>
<TextInput
data-modal-primary-focus
id="text-input-1"
labelText="Domain name"
placeholder="e.g. github.com"
/>
<Select id="select-1" defaultValue="us-south" labelText="Region">
<SelectItem value="us-south" text="US South" />
<SelectItem value="us-east" text="US East" />
</Select>
<TextArea labelText="Comments" />
</Modal>
</div>
),
};

export const _Multiselect = {
args: args,
argTypes: argTypes,
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/components/Slug/Slug.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const content = <span>AI was used to generate this content</span>;

export const Default = () => (
<>
<div className="slug-container-example">
<div className="slug-container slug-container-example">
<Slug autoAlign size="mini">
<SlugContent>{aiContent}</SlugContent>
</Slug>
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/components/Slug/slug-story.scss
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,7 @@
.slug-check-radio-container fieldset.cds--checkbox-group:not(:first-of-type) {
margin-top: 2rem;
}

.slug-modal .cds--form-item {
margin-top: 1rem;
}
34 changes: 33 additions & 1 deletion packages/styles/scss/components/modal/_modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@use '../../spacing' as *;
@use '../../theme' as *;
@use '../../type' as *;
@use '../../utilities/ai-gradient' as *;
@use '../../utilities/convert';
@use '../../utilities/component-reset';
@use '../../utilities/focus-outline' as *;
Expand Down Expand Up @@ -155,7 +156,6 @@
.#{$prefix}--modal-container {
position: fixed;
display: grid;
overflow: hidden;
background-color: $layer;
block-size: 100%;
grid-template-columns: 100%;
Expand Down Expand Up @@ -485,6 +485,38 @@
margin: 0;
}

// Slug styles
.#{$prefix}--modal--slug .#{$prefix}--modal-container {
@include callout-gradient('default');

background-color: $layer;
}

// Start the gradient 64px from bottom only when two buttons are present
.#{$prefix}--modal--slug
.#{$prefix}--modal-container:has(
.#{$prefix}--btn-set:not(.#{$prefix}--modal-footer--three-button)
> button:not(:only-child)
) {
@include callout-gradient('default', 64px);

background-color: $layer;
}

.#{$prefix}--modal--slug .#{$prefix}--slug {
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
}

.#{$prefix}--modal-header > .#{$prefix}--slug:has(+ .#{$prefix}--modal-close),
.#{$prefix}--modal-header > .#{$prefix}--modal-close ~ .#{$prefix}--slug,
.#{$prefix}--modal--slug
.#{$prefix}--modal-container-body
> .#{$prefix}--slug {
inset-inline-end: convert.to-rem(48px);
}

// Windows HCM fix
/* stylelint-disable */
.#{$prefix}--modal-close__icon {
Expand Down

0 comments on commit 3748b40

Please sign in to comment.