Skip to content

Commit

Permalink
feat(Modal): add slug to Modal, ComposedModal
Browse files Browse the repository at this point in the history
  • Loading branch information
tw15egan committed Dec 7, 2023
1 parent 670dffc commit 286e958
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 10 deletions.
24 changes: 21 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': open,
[`${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
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;
79 changes: 78 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,43 @@ export const _Combobox = {
),
};

export const _ComposedModal = {
args: args,
argTypes: argTypes,
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 +371,39 @@ export const _FilterableMultiselect = {
),
};

export const _Modal = {
args: args,
argTypes: argTypes,
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;
}
38 changes: 37 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,42 @@
margin: 0;
}

// Slug styles

.#{$prefix}--modal.#{$prefix}--modal--slug {
background: $slug-overlay;
}

.#{$prefix}--modal--slug .#{$prefix}--modal-container {
@include ai-gradient('bottom', 50%);

box-shadow: 0 -45px 100px 0 rgba(0, 0, 0, 0.25),
0 4px 4px 0 rgba(0, 0, 0, 0.25);
}

// 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 ai-gradient('bottom', 50%, 64px);
}

.#{$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
11 changes: 8 additions & 3 deletions packages/styles/scss/utilities/_ai-gradient.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
/// @param {Number} $width - Percentage width of gradient with regards to parent component
/// @example @include ai-gradient('right', '33%');
/// @group utilities
@mixin ai-gradient($direction: 'right', $width: 33%) {
@mixin ai-gradient($direction: 'right', $width: 33%, $offset: 0) {
$deg: 0;
@if $direction == 'bottom' {
$deg: 0deg;
Expand All @@ -26,15 +26,20 @@
$deg: 270deg;
}

$start: 0%;
@if $offset != 0 {
$start: calc(0% + #{$offset});
}

background-image: linear-gradient(
$deg,
theme.$ai-gradient-start-01 0%,
theme.$ai-gradient-start-01 $start,
theme.$ai-gradient-end $width,
transparent 100%
),
linear-gradient(
$deg,
theme.$ai-gradient-start-02 0%,
theme.$ai-gradient-start-02 $start,
theme.$ai-gradient-end $width,
transparent 100%
);
Expand Down
2 changes: 2 additions & 0 deletions packages/themes/src/g10.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ export const slugCalloutAuraEndSelected = rgba(white, 0);
export const aiGradientStart01 = rgba(coolGray10, 0.5);
export const aiGradientStart02 = rgba(blue10, 0.5);
export const aiGradientEnd = rgba(white, 0);
// Slug modal overlay
export const slugOverlay = 'rgba(17, 17, 17, 0.1)';

export {
// Type
Expand Down
2 changes: 2 additions & 0 deletions packages/themes/src/g100.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ export const slugCalloutAuraEndSelected = rgba(gray100, 0);
export const aiGradientStart01 = rgba(blue20, 0.2);
export const aiGradientStart02 = 'transparent';
export const aiGradientEnd = 'rgba(38, 38, 38, 0)';
// Slug modal overlay
export const slugOverlay = overlay;

export {
// Type
Expand Down
2 changes: 2 additions & 0 deletions packages/themes/src/g90.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ export const slugCalloutAuraEndSelected = rgba(gray100, 0);
export const aiGradientStart01 = rgba(blue20, 0.2);
export const aiGradientStart02 = 'transparent';
export const aiGradientEnd = 'rgba(38, 38, 38, 0)';
// Slug modal overlay
export const slugOverlay = overlay;

export {
// Type
Expand Down
1 change: 1 addition & 0 deletions packages/themes/src/tokens/v11TokenGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ export const ai = TokenGroup.create({
'ai-gradient-start-01',
'ai-gradient-start-02',
'ai-gradient-end',
'slug-overlay',
],
});

Expand Down
2 changes: 2 additions & 0 deletions packages/themes/src/white.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ export const slugCalloutAuraEndSelected = rgba(white, 0);
export const aiGradientStart01 = rgba(coolGray10, 0.5);
export const aiGradientStart02 = rgba(blue10, 0.5);
export const aiGradientEnd = rgba(white, 0);
// Slug modal overlay
export const slugOverlay = 'rgba(17, 17, 17, 0.1)';

// Type
export {
Expand Down

0 comments on commit 286e958

Please sign in to comment.