1
1
mirror of https://github.com/primer/css.git synced 2024-11-12 22:06:08 +03:00

Overlay component (#1998)

* Add styles, documentation, and storybook story for the new dialog component

* Stylelint auto-fixes

* Create slow-poets-mate.md

* Update slow-poets-mate.md

* update namespace

* Stylelint auto-fixes

* menu specifics

* starting over

* a new paradigm

* fully responsive

* push for testing

* auto width, visibility hidden

* Overlay stories

* dialog stories

* cleanup

* remove popover

* remove unused import

* docs update

* min() to the rescue!

* thin scrollbar?

* remove comment

* Update src/overlay/overlay.scss

Co-authored-by: simurai <simurai@github.com>

* address feedback

* handle form elements

* test permissions

* lint

Co-authored-by: Actions Auto Build <actions@github.com>
Co-authored-by: Katie Langerman <langermank@github.com>
Co-authored-by: simurai <simurai@github.com>
This commit is contained in:
Adrián Bolonio 2022-05-10 04:59:39 +01:00 committed by GitHub
parent ab6f0840f0
commit 51e087aa31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1381 additions and 365 deletions

View File

@ -0,0 +1,5 @@
---
"@primer/css": minor
---
Styles for the new Dialog Component

View File

@ -0,0 +1,58 @@
---
title: Dialog
path: components/dialog
status: Alpha
source: 'https://github.com/github/github/tree/master/app/assets/stylesheets/experiments/modal-dialog.scss'
bundle: dialog
---
Please note that the `<div>` element with `id="fake-container"` is not included in the component.
```html live
<div id="fake-container" style="height: 400px;">
<button class="btn"><span>Open dialog</span></button>
<div
id="overlay-backdrop"
class="Overlay-backdrop Overlay-backdrop--center"
role="dialog"
aria-labelledby="title-id"
aria-describedby="description-id"
data-focus-trap="active"
>
<div
class="Overlay Overlay--width-medium Overlay--height-medium Overlay--motion-scaleFade"
data-focus-trap="active"
open=""
>
<header class="Overlay-header">
<div class="Overlay-headerContentWrap">
<div class="Overlay-titleWrap">
<h1 id="title-id" class="Overlay-title">This is the title of the dialog</h1>
<h2 id="description-id" class="Overlay-description">This is the subtitle of the dialog</h2>
</div>
<div class="Overlay-actionWrap">
<button class="Overlay-closeButton" aria-label="Close">
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<path
fill-rule="evenodd"
d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"
></path>
</svg>
</button>
</div>
</div>
</header>
<div class="Overlay-body">
This is the body of the dialogThis is the body of the dialogThis is the body of the dialog This is the body of
the dialog This is the body of the dialog This is the body of the dialog This is the body of the dialog This is
the body of the dialog This is the body of the dialog
</div>
<footer class="Overlay-footer Overlay-footer--divided Overlay-footer--alignEnd">
<button class="btn"><span>Continue</span></button>
</footer>
</div>
</div>
</div>
```
[aria attributes]: https://www.w3.org/TR/html-aria/#allowed-aria-roles-states-and-properties

View File

@ -0,0 +1,388 @@
import React from 'react'
import clsx from 'clsx'
import {OverlayTemplate} from '../../ui-patterns/Overlay/Overlay.stories'
export default {
title: 'Components/Dialog',
parameters: {
layout: 'padded'
},
excludeStories: ['DialogTemplate'],
argTypes: {
title: {
name: 'title',
type: {name: 'string', required: false},
description: 'The heading element of the dialog',
defaultValue: '',
table: {
category: 'HTML'
}
},
description: {
name: 'description',
type: 'string',
description: 'The sub-heading element of the dialog',
defaultValue: '',
table: {
category: 'HTML'
}
},
toggleOverlay: {
control: {type: 'boolean'},
description: 'show/hide overlay',
defaultValue: false,
table: {
category: 'Demo'
}
},
showCloseButton: {
control: {type: 'boolean'},
description: 'show/hide close button',
defaultValue: true,
table: {
category: 'Demo'
}
},
showFooterButton: {
control: {type: 'boolean'},
description: 'show/hide footer button',
defaultValue: false,
table: {
category: 'Demo'
}
},
width: {
options: [0, 1, 2, 3, 4, 5], // iterator
mapping: [
'Overlay--width-auto',
'Overlay--width-small',
'Overlay--width-medium',
'Overlay--width-large',
'Overlay--width-xlarge',
'Overlay--width-xxlarge'
], // values
control: {
type: 'inline-radio',
labels: ['auto', 'small', 'medium', 'large', 'xlarge', 'xxlarge']
},
description: 'Width options: small: 256px, medium: 320px, large: 480px, xlarge: 640px, xxlarge: 960px',
table: {
category: 'CSS'
}
},
height: {
options: [0, 1, 2, 3, 4, 5], // iterator
mapping: [
'Overlay--height-auto',
'Overlay--height-xsmall',
'Overlay--height-small',
'Overlay--height-medium',
'Overlay--height-large',
'Overlay--height-xlarge'
], // values
control: {
type: 'inline-radio',
labels: ['auto', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'xxlarge']
},
description:
'Height options: auto: adjusts to content, xsmall: 192px, small: 256px, medium: 320px, large: 432px, xlarge: 600px',
table: {
category: 'CSS'
}
},
headerVariant: {
options: [0, 1], // iterator
mapping: ['', 'Overlay-header--large'], // values
control: {
type: 'inline-radio',
labels: ['medium (default)', 'large']
},
description: 'medium (default), large header/description font-size + spacing',
table: {
category: 'CSS'
}
},
bodyPaddingVariant: {
options: [0, 1, 2], // iterator
mapping: ['', 'Overlay-body--paddingCondensed', 'Overlay-body--paddingNone'], // values
control: {
type: 'inline-radio',
labels: ['normal (default)', 'condensed', 'none']
},
description: 'body spacing',
table: {
category: 'CSS'
}
},
variantNarrow: {
options: [0, 1, 2, 3], // iterator
mapping: [
'Overlay-backdrop--center-whenNarrow',
'Overlay-backdrop--anchor-whenNarrow',
'Overlay-backdrop--side-whenNarrow',
'Overlay-backdrop--full-whenNarrow'
], // values
control: {
type: 'inline-radio',
labels: ['center', 'anchored', 'side', 'full']
},
description: '',
table: {
category: 'Variant'
}
},
variantRegular: {
options: [0, 1, 2, 3], // iterator
mapping: [
'Overlay-backdrop--center',
'Overlay-backdrop--anchor',
'Overlay-backdrop--side',
'Overlay-backdrop--full'
], // values
control: {
type: 'inline-radio',
labels: ['center', 'anchored', 'side', 'full']
},
description: '',
table: {
category: 'Variant'
}
},
placementNarrow: {
options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
mapping: [
'Overlay-backdrop--placement-top-whenNarrow',
'',
'',
'',
'Overlay-backdrop--placement-bottom-whenNarrow',
'',
'',
'',
'Overlay-backdrop--placement-right-whenNarrow',
'',
'',
'',
'Overlay-backdrop--placement-left-whenNarrow',
'',
'',
'',
''
],
control: {
type: 'inline-radio',
labels: [
'top',
'top-start',
'top-center',
'top-end',
'bottom',
'bottom-start',
'bottom-center',
'bottom-end',
'right',
'right-start',
'right-center',
'right-end',
'left',
'left-start',
'left-center',
'left-end',
'reset'
]
},
description: 'Positions overlay for narrow viewport range',
table: {
category: 'Placement'
}
},
placementRegular: {
options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
mapping: [
'Overlay-backdrop--placement-top',
'',
'',
'',
'Overlay-backdrop--placement-bottom',
'',
'',
'',
'Overlay-backdrop--placement-right',
'',
'',
'',
'Overlay-backdrop--placement-left',
'',
'',
'',
''
],
control: {
type: 'inline-radio',
labels: [
'top',
'top-start',
'top-center',
'top-end',
'bottom',
'bottom-start',
'bottom-center',
'bottom-end',
'right',
'right-start',
'right-center',
'right-end',
'left',
'left-start',
'left-center',
'left-end',
'reset'
]
},
description: 'Positions overlay for narrow viewport range',
table: {
category: 'Placement'
}
},
headerRegion: {
control: {type: 'boolean'},
description:
'A header region may be used to provide context to the user by displaying a title, description, and offering an easy-to-escape route with a Close button. Headers may also provide ways for the user to interact with the content, such as with search and tabs.',
defaultValue: true,
table: {
category: 'Demo'
}
},
footerRegion: {
control: {type: 'boolean'},
description:
'The footer region may be used to show confirmation actions, navigation links, or other important elements that should appear outside of the content scrolling region.',
defaultValue: true,
table: {
category: 'Demo'
}
},
showFooterDivider: {
control: {type: 'boolean'},
defaultValue: false,
description: 'Show dividers above footer',
table: {
category: 'CSS'
}
},
showHeaderDivider: {
control: {type: 'boolean'},
defaultValue: false,
description: 'Show dividers below header',
table: {
category: 'CSS'
}
},
headerContentSlot: {
description: 'Slot for additional header content',
control: {type: 'string'},
table: {
category: 'HTML'
}
},
actionContentSlot: {
description: 'Slot for additional header action',
control: {type: 'string'},
table: {
category: 'HTML'
}
},
motion: {
options: [0, 1], // iterator
mapping: [null, 'Overlay--motion-scaleFade'], // values
control: {
type: 'inline-radio',
labels: ['none', 'scaleFade']
},
description: 'Animation options for show/hide overlay',
table: {
category: 'CSS'
}
},
footerContentAlign: {
options: [0, 1, 2], // iterator
mapping: ['Overlay-footer--alignStart', 'Overlay-footer--alignCenter', 'Overlay-footer--alignEnd'], // values
control: {
type: 'inline-radio',
labels: ['start', 'center', 'end']
},
description: 'Align footer contents',
table: {
category: 'CSS'
}
},
role: {
description: 'Semantic role',
control: {type: 'string'},
table: {
category: 'HTML'
}
},
ariaLabelledy: {
description: 'aria-labelledby',
control: {type: 'string'},
table: {
category: 'HTML'
}
},
ariaDescribedby: {
description: 'aria-describedby',
control: {type: 'string'},
table: {
category: 'HTML'
}
},
dataFocusTrap: {
description: 'data-focus-trap',
control: {type: 'string'},
table: {
category: 'HTML'
}
},
titleId: {
description: 'title id',
control: {type: 'string'},
table: {
category: 'HTML'
}
},
descriptionId: {
description: 'description id',
control: {type: 'string'},
table: {
category: 'HTML'
}
}
}
}
export const Playground = OverlayTemplate.bind({})
Playground.args = {
...OverlayTemplate.args,
title: 'Dialog title',
description: 'Optional dialog description',
role: 'dialog',
width: 2,
height: 3,
ariaLabelledby: 'title-id',
ariaDescribedby: 'description-id',
dataFocusTrap: 'active',
footerContentAlign: 2,
showCloseButton: true,
headerVariant: 0,
bodyPaddingVariant: 0,
motion: 1,
descriptionId: 'description-id',
titleId: 'title-id',
showFooterDivider: false,
children: <p>Dialog body</p>,
headerRegion: true,
variantNarrow: 3,
variantRegular: 0
}

View File

@ -1,135 +0,0 @@
import React from 'react'
import clsx from 'clsx'
// import { StoryTemplateName } from './OtherStoryFile.stories' // import stories for component compositions
export default {
title: 'Components/Popover',
excludeStories: ['PopoverTemplateName'],
layout: 'padded',
argTypes: {
size: {
options: [0, 1], // iterator
mapping: ['', 'Popover-message--large'], // values
control: {
type: 'select',
labels: ['default', 'large']
},
table: {
category: 'CSS'
}
},
caretPosition: {
options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // iterator
mapping: [
'',
'Popover-message--bottom',
'Popover-message--bottom-left',
'Popover-message--bottom-right',
'Popover-message--left',
'Popover-message--left-bottom',
'Popover-message--left-top',
'Popover-message--right',
'Popover-message--right-bottom',
'Popover-message--right-top',
'Popover-message--top-left',
'Popover-message--top-right',
'Popover-message--no-caret'
], // values
control: {
type: 'inline-radio',
labels: [
'default (top)',
'bottom',
'bottom_left',
'bottom_right',
'left',
'left_bottom',
'left_top',
'right',
'right_bottom',
'right_top',
'top_left',
'top_right',
'none'
]
},
table: {
category: 'CSS'
}
},
messagePosition: {
options: ['position-relative', 'position-absolute'],
control: {
type: 'inline-radio'
},
description: '',
table: {
category: 'CSS'
}
},
headingText: {
name: 'headingText',
type: 'string',
description: 'string',
table: {
category: 'HTML'
}
},
primerUtilities: {
name: 'headingText',
type: 'string',
description: 'Primer utility classes',
table: {
category: 'HTML'
}
},
tag: {
options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
control: {type: 'inline-radio'},
description: 'h4 default',
table: {
category: 'HTML'
}
}
}
}
export const PopoverTemplateName = ({
size,
caretPosition,
messagePosition,
headingText,
tag,
children,
trigger,
triggerBottom,
primerUtilities
}) => (
<>
{trigger}
<div className={clsx('Popover', 'right-0', 'left-0', `${messagePosition}`)}>
<div
className={clsx('Popover-message', 'color-shadow-large', `${primerUtilities}`, `${size}`, `${caretPosition}`)}
>
{tag === 'h1' && <h1 className="mb-2">{headingText}</h1>}
{tag === 'h2' && <h2 className="mb-2">{headingText}</h2>}
{tag === 'h3' && <h3 className="mb-2">{headingText}</h3>}
{tag === 'h4' && <h4 className="mb-2">{headingText}</h4>}
{tag === 'h5' && <h5 className="mb-2">{headingText}</h5>}
{tag === 'h6' && <h6 className="mb-2">{headingText}</h6>}
{children}
</div>
</div>
{triggerBottom}
</>
)
export const Playground = PopoverTemplateName.bind({})
Playground.args = {
tag: 'h4',
headingText: 'Popover heading',
size: '',
caretPosition: '',
messagePosition: 'position-relative',
primerUtilities: 'text-left p-4 mt-2 mx-aut'
}

View File

@ -1,229 +0,0 @@
import React from 'react'
import clsx from 'clsx'
import {PopoverTemplateName} from './Popover.stories.jsx'
export default {
title: 'Components/Popover/Features',
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/GCvY3Qv8czRgZgvl1dG6lp/Primer-Web?node-id=410%3A3890'
}
}
}
export const Bottom = PopoverTemplateName.bind({})
Bottom.args = {
tag: 'h4',
headingText: 'Popover heading',
size: '',
caretPosition: 'Popover-message--bottom',
messagePosition: 'position-relative',
triggerBottom: [<button class="btn btn-primary">Trigger button</button>],
primerUtilities: 'p-4 mx-auto mb-2 text-left'
}
Bottom.decorators = [
Story => (
<div className="position-relative text-center">
<Story />
</div>
)
]
export const BottomRight = PopoverTemplateName.bind({})
BottomRight.args = {
tag: 'h4',
headingText: 'Popover heading',
size: '',
caretPosition: 'Popover-message--bottom-right',
messagePosition: 'position-relative',
triggerBottom: [<button class="btn btn-primary">Trigger button</button>],
primerUtilities: 'p-4 mb-2 text-left'
}
BottomRight.decorators = [
Story => (
<div className="position-relative text-right">
<Story />
</div>
)
]
export const BottomLeft = PopoverTemplateName.bind({})
BottomLeft.args = {
tag: 'h4',
headingText: 'Popover heading',
size: '',
caretPosition: 'Popover-message--bottom-left',
messagePosition: 'position-relative',
triggerBottom: [<button class="btn btn-primary">Trigger button</button>],
primerUtilities: 'p-4 mb-2'
}
BottomLeft.decorators = [
Story => (
<div className="position-relative">
<Story />
</div>
)
]
export const Left = PopoverTemplateName.bind({})
Left.args = {
tag: 'h4',
headingText: 'Popover heading',
size: '',
caretPosition: 'Popover-message--left',
messagePosition: 'position-relative',
trigger: [<button class="btn btn-primary">Trigger button</button>],
primerUtilities: 'p-4 ml-2'
}
Left.decorators = [
Story => (
<div className="d-flex flex-justify-center flex-items-center">
<Story />
</div>
)
]
export const LeftBottom = PopoverTemplateName.bind({})
LeftBottom.args = {
tag: 'h4',
headingText: 'Popover heading',
size: '',
caretPosition: 'Popover-message--left-bottom',
messagePosition: 'position-relative',
trigger: [<button class="btn btn-primary">Trigger button</button>],
primerUtilities: 'p-4 ml-2'
}
LeftBottom.decorators = [
Story => (
<div className="d-flex flex-justify-center flex-items-end">
<Story />
</div>
)
]
export const LeftTop = PopoverTemplateName.bind({})
LeftTop.args = {
tag: 'h4',
headingText: 'Popover heading',
size: '',
caretPosition: 'Popover-message--left-top',
messagePosition: 'position-relative',
trigger: [<button class="btn btn-primary">Trigger button</button>],
primerUtilities: 'p-4 ml-2'
}
LeftTop.decorators = [
Story => (
<div className="d-flex flex-justify-center flex-items-start">
<Story />
</div>
)
]
export const Right = PopoverTemplateName.bind({})
Right.args = {
tag: 'h4',
headingText: 'Popover heading',
size: '',
caretPosition: 'Popover-message--right',
messagePosition: 'position-relative',
triggerBottom: [<button class="btn btn-primary">Trigger button</button>],
primerUtilities: 'p-4 mr-2'
}
Right.decorators = [
Story => (
<div className="d-flex flex-justify-center flex-items-center">
<Story />
</div>
)
]
export const RightBottom = PopoverTemplateName.bind({})
RightBottom.args = {
tag: 'h4',
headingText: 'Popover heading',
size: '',
caretPosition: 'Popover-message--right-bottom',
messagePosition: 'position-relative',
triggerBottom: [<button class="btn btn-primary">Trigger button</button>],
primerUtilities: 'p-4 mr-2'
}
RightBottom.decorators = [
Story => (
<div className="d-flex flex-justify-center flex-items-end">
<Story />
</div>
)
]
export const RightTop = PopoverTemplateName.bind({})
RightTop.args = {
tag: 'h4',
headingText: 'Popover heading',
size: '',
caretPosition: 'Popover-message--right-top',
messagePosition: 'position-relative',
triggerBottom: [<button class="btn btn-primary">Trigger button</button>],
primerUtilities: 'p-4 mr-2'
}
RightTop.decorators = [
Story => (
<div className="d-flex flex-justify-center flex-items-start">
<Story />
</div>
)
]
export const TopLeft = PopoverTemplateName.bind({})
TopLeft.args = {
tag: 'h4',
headingText: 'Popover heading',
size: '',
caretPosition: 'Popover-message--top-left',
messagePosition: 'position-relative',
trigger: [<button class="btn btn-primary">Trigger button</button>],
primerUtilities: 'p-4 mt-2'
}
TopLeft.decorators = [
Story => (
<div className="position-relative pl-2">
<Story />
</div>
)
]
export const TopRight = PopoverTemplateName.bind({})
TopRight.args = {
tag: 'h4',
headingText: 'Popover heading',
size: '',
caretPosition: 'Popover-message--top-right',
messagePosition: 'position-relative',
trigger: [<button class="btn btn-primary">Trigger button</button>],
primerUtilities: 'text-left p-4 mt-2'
}
TopRight.decorators = [
Story => (
<div className="position-relative text-right pr-2">
<Story />
</div>
)
]
export const Large = PopoverTemplateName.bind({})
Large.args = {
tag: 'h4',
headingText: 'Popover heading',
size: 'Popover-message--large',
caretPosition: 'Popover-message--bottom',
messagePosition: 'position-relative',
triggerBottom: [<button class="btn btn-primary">Trigger button</button>],
primerUtilities: 'p-4 mx-auto mb-2 text-left'
}
Large.decorators = [
Story => (
<div className="position-relative text-center">
<Story />
</div>
)
]

View File

@ -0,0 +1,518 @@
import clsx from 'clsx'
import React from 'react'
import ConditionalWrapper from '../../helpers/ConditionalWrapper'
import {PatternFullBleed} from '../ActionList/ActionListFeatures.stories.jsx'
const variant = {}
export default {
title: 'UI Patterns/Overlay',
parameters: {
layout: 'padded'
},
excludeStories: ['OverlayTemplate'],
argTypes: {
title: {
name: 'title',
type: {name: 'string', required: false},
description: 'The heading element of the dialog',
defaultValue: '',
table: {
category: 'HTML'
}
},
description: {
name: 'description',
type: 'string',
description: 'The sub-heading element of the dialog',
defaultValue: '',
table: {
category: 'HTML'
}
},
toggleOverlay: {
control: {type: 'boolean'},
description: 'show/hide overlay',
defaultValue: false,
table: {
category: 'Demo'
}
},
showCloseButton: {
control: {type: 'boolean'},
description: 'show/hide close button',
defaultValue: false,
table: {
category: 'Demo'
}
},
showFooterButton: {
control: {type: 'boolean'},
description: 'show/hide footer button',
defaultValue: false,
table: {
category: 'Demo'
}
},
width: {
options: [0, 1, 2, 3, 4, 5], // iterator
mapping: [
'Overlay--width-auto',
'Overlay--width-small',
'Overlay--width-medium',
'Overlay--width-large',
'Overlay--width-xlarge',
'Overlay--width-xxlarge'
], // values
control: {
type: 'inline-radio',
labels: ['auto', 'small', 'medium', 'large', 'xlarge', 'xxlarge']
},
description: 'Width options: small: 256px, medium: 320px, large: 480px, xlarge: 640px, xxlarge: 960px',
table: {
category: 'CSS'
}
},
height: {
options: [0, 1, 2, 3, 4, 5], // iterator
mapping: [
'Overlay--height-auto',
'Overlay--height-xsmall',
'Overlay--height-small',
'Overlay--height-medium',
'Overlay--height-large',
'Overlay--height-xlarge'
], // values
control: {
type: 'inline-radio',
labels: ['auto', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'xxlarge']
},
description:
'Height options: auto: adjusts to content, xsmall: 192px, small: 256px, medium: 320px, large: 432px, xlarge: 600px',
table: {
category: 'CSS'
}
},
headerVariant: {
options: [0, 1], // iterator
mapping: ['', 'Overlay-header--large'], // values
control: {
type: 'inline-radio',
labels: ['medium (default)', 'large']
},
description: 'medium (default), large header/description font-size + spacing',
table: {
category: 'CSS'
}
},
bodyPaddingVariant: {
options: [0, 1, 2], // iterator
mapping: ['', 'Overlay-body--paddingCondensed', 'Overlay-body--paddingNone'], // values
control: {
type: 'inline-radio',
labels: ['normal (default)', 'condensed', 'none']
},
description: 'body spacing',
table: {
category: 'CSS'
}
},
variantNarrow: {
options: [0, 1, 2, 3], // iterator
mapping: [
'Overlay-backdrop--center-whenNarrow',
'Overlay-backdrop--anchor-whenNarrow',
'Overlay-backdrop--side-whenNarrow',
'Overlay-backdrop--full-whenNarrow'
], // values
control: {
type: 'inline-radio',
labels: ['center', 'anchored', 'side', 'full']
},
description: '',
table: {
category: 'Variant'
}
},
variantRegular: {
options: [0, 1, 2, 3], // iterator
mapping: [
'Overlay-backdrop--center',
'Overlay-backdrop--anchor',
'Overlay-backdrop--side',
'Overlay-backdrop--full'
], // values
control: {
type: 'inline-radio',
labels: ['center', 'anchored', 'side', 'full']
},
description: '',
table: {
category: 'Variant'
}
},
placementNarrow: {
options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
mapping: [
'Overlay-backdrop--placement-top-whenNarrow',
'',
'',
'',
'Overlay-backdrop--placement-bottom-whenNarrow',
'',
'',
'',
'Overlay-backdrop--placement-right-whenNarrow',
'',
'',
'',
'Overlay-backdrop--placement-left-whenNarrow',
'',
'',
'',
''
],
control: {
type: 'inline-radio',
labels: [
'top',
'top-start',
'top-center',
'top-end',
'bottom',
'bottom-start',
'bottom-center',
'bottom-end',
'right',
'right-start',
'right-center',
'right-end',
'left',
'left-start',
'left-center',
'left-end',
'reset'
]
},
description: 'Positions overlay for narrow viewport range',
table: {
category: 'Placement'
}
},
placementRegular: {
options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
mapping: [
'Overlay-backdrop--placement-top',
'',
'',
'',
'Overlay-backdrop--placement-bottom',
'',
'',
'',
'Overlay-backdrop--placement-right',
'',
'',
'',
'Overlay-backdrop--placement-left',
'',
'',
'',
''
],
control: {
type: 'inline-radio',
labels: [
'top',
'top-start',
'top-center',
'top-end',
'bottom',
'bottom-start',
'bottom-center',
'bottom-end',
'right',
'right-start',
'right-center',
'right-end',
'left',
'left-start',
'left-center',
'left-end',
'reset'
]
},
description: 'Positions overlay for narrow viewport range',
table: {
category: 'Placement'
}
},
headerRegion: {
control: {type: 'boolean'},
description:
'A header region may be used to provide context to the user by displaying a title, description, and offering an easy-to-escape route with a Close button. Headers may also provide ways for the user to interact with the content, such as with search and tabs.',
defaultValue: true,
table: {
category: 'Demo'
}
},
footerRegion: {
control: {type: 'boolean'},
description:
'The footer region may be used to show confirmation actions, navigation links, or other important elements that should appear outside of the content scrolling region.',
defaultValue: true,
table: {
category: 'Demo'
}
},
showFooterDivider: {
control: {type: 'boolean'},
defaultValue: false,
description: 'Show dividers above footer',
table: {
category: 'CSS'
}
},
showHeaderDivider: {
control: {type: 'boolean'},
defaultValue: false,
description: 'Show dividers below header',
table: {
category: 'CSS'
}
},
headerContentSlot: {
description: 'Slot for additional header content',
control: {type: 'string'},
table: {
category: 'HTML'
}
},
actionContentSlot: {
description: 'Slot for additional header action',
control: {type: 'string'},
table: {
category: 'HTML'
}
},
motion: {
options: [0, 1], // iterator
mapping: [null, 'Overlay--motion-scaleFade'], // values
control: {
type: 'inline-radio',
labels: ['none', 'scaleFade']
},
description: 'Animation options for show/hide overlay',
table: {
category: 'CSS'
}
},
footerContentAlign: {
options: [0, 1, 2], // iterator
mapping: ['Overlay-footer--alignStart', 'Overlay-footer--alignCenter', 'Overlay-footer--alignEnd'], // values
control: {
type: 'inline-radio',
labels: ['start', 'center', 'end']
},
description: 'Align footer contents',
table: {
category: 'CSS'
}
},
role: {
description: 'Semantic role',
control: {type: 'string'},
table: {
category: 'HTML'
}
},
ariaLabelledy: {
description: 'aria-labelledby',
control: {type: 'string'},
table: {
category: 'HTML'
}
},
ariaDescribedby: {
description: 'aria-describedby',
control: {type: 'string'},
table: {
category: 'HTML'
}
},
dataFocusTrap: {
description: 'data-focus-trap',
control: {type: 'string'},
table: {
category: 'HTML'
}
},
titleId: {
description: 'title id',
control: {type: 'string'},
table: {
category: 'HTML'
}
},
descriptionId: {
description: 'description id',
control: {type: 'string'},
table: {
category: 'HTML'
}
}
}
}
const focusMethod = function getFocus() {
const dialog = document.getElementById('overlay-backdrop')[0]
dialog.focus()
}
const toggleDialog = () => {
const dialog = document.getElementById('overlay-backdrop')
dialog.classList.toggle('Overlay--hidden')
focusMethod()
}
export const OverlayTemplate = ({
title,
description,
toggleOverlay,
width,
height,
showFooterDivider,
showHeaderDivider,
headerRegion,
footerRegion,
headerContentSlot,
motion,
footerContentAlign,
showCloseButton,
showFooterButton,
actionContentSlot,
headerVariant,
bodyPaddingVariant,
role,
ariaLabelledby,
ariaDescribedby,
dataFocusTrap,
children,
titleId,
descriptionId,
variantNarrow,
variantRegular,
placementNarrow,
placementRegular
}) => (
<>
<button class="btn" onClick={toggleDialog}>
<span>Open overlay</span>
</button>
<div
id="overlay-backdrop"
className={clsx(
toggleOverlay && 'Overlay--hidden',
variantNarrow && `${variantNarrow}`,
variantRegular && `${variantRegular}`,
placementNarrow && `${placementNarrow}`,
placementRegular && `${placementRegular}`
)}
>
<div
className={clsx(
'Overlay',
width && `${width}`,
height && `${height}`,
motion && `${motion}`,
variantNarrow && 'Overlay-whenNarrow'
)}
data-focus-trap={dataFocusTrap}
role={role}
aria-labelledby={ariaLabelledby}
aria-describedby={ariaDescribedby}
open
>
{headerRegion && (
<header
className={clsx(
'Overlay-header',
showHeaderDivider && 'Overlay-header--divided',
headerVariant && `${headerVariant}`
)}
>
<div className="Overlay-headerContentWrap">
<div className="Overlay-titleWrap">
{title && (
<h1 id={titleId} className="Overlay-title">
{title}
</h1>
)}
{description && (
<h2 id={descriptionId} className="Overlay-description">
{description}
</h2>
)}
</div>
{showCloseButton && (
<div className="Overlay-actionWrap">
{actionContentSlot && <div dangerouslySetInnerHTML={{__html: actionContentSlot}} />}
<button className="Overlay-closeButton" aria-label="Close" onClick={toggleDialog}>
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<path
fill-rule="evenodd"
d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"
></path>
</svg>
</button>
</div>
)}
</div>
{headerContentSlot && (
<div className="Overlay-headerContentSlot" dangerouslySetInnerHTML={{__html: headerContentSlot}} />
)}
</header>
)}
<div className={clsx('Overlay-body', bodyPaddingVariant && `${bodyPaddingVariant}`)}>{children}</div>
{footerRegion && (
<footer
className={clsx(
'Overlay-footer',
showFooterDivider && 'Overlay-footer--divided',
footerContentAlign && `${footerContentAlign}`
)}
>
{showFooterButton && (
<button class="btn" onClick={toggleDialog}>
<span>Continue</span>
</button>
)}
</footer>
)}
</div>
</div>
</>
)
export const Playground = OverlayTemplate.bind({})
Playground.storyName = 'Playground'
Playground.args = {
title: 'This is the title of the dialog',
description: 'This is the subtitle of the dialog',
motion: 1,
footerContentAlign: 2,
showCloseButton: true,
showFooterButton: false,
headerContentSlot: '',
actionContentSlot: '',
headerVariant: 0,
bodyPaddingVariant: 0,
width: 1,
height: 3,
headerVariant: 0,
headerRegion: true,
footerRegion: true,
showFooterDivider: false,
showHeaderDivider: false,
role: '',
ariaDescribedby: '',
dataFocusTrap: ''
}

View File

@ -24,6 +24,7 @@
@import '../pagination/index.scss';
@import '../tooltips/index.scss';
@import '../truncate/index.scss';
@import '../overlay/index.scss';
// Utilities always go last so that they can override components
@import '../utilities/index.scss';

24
src/overlay/README.md Normal file
View File

@ -0,0 +1,24 @@
---
bundle: 'overlay'
generated: true
---
# Primer CSS: `overlay` bundle
## Usage
Primer CSS source files are written in [SCSS]. To include this Primer CSS module in your own build, ensure that your `node_modules` directory is listed in your Sass include paths, then import it with:
```scss
@import '@primer/css/overlay/index.scss';
```
## Build
The `@primer/css` npm package includes a standalone CSS build of this module in `dist/overlay.css`.
## License
[MIT](https://github.com/primer/css/blob/main/LICENSE) &copy; [GitHub](https://github.com/)
[scss]: https://sass-lang.com/documentation/syntax#scss

2
src/overlay/index.scss Normal file
View File

@ -0,0 +1,2 @@
@import '../support/index.scss';
@import './overlay.scss';

384
src/overlay/overlay.scss Normal file
View File

@ -0,0 +1,384 @@
// stylelint-disable selector-max-compound-selectors, max-nesting-depth, selector-max-specificity, primer/borders
// replace with primitive
$primer-borderRadius-large: 0.75rem;
.Overlay--hidden {
display: none !important;
}
.Overlay--visibilityHidden {
height: 0;
overflow: hidden;
visibility: hidden;
opacity: 0;
}
.Overlay {
display: flex;
min-width: 192px;
flex-direction: column;
background-color: var(--color-canvas-overlay);
border-radius: $primer-borderRadius-large;
box-shadow: var(--color-overlay-shadow);
opacity: 1;
&.Overlay--height-auto {
height: auto;
}
&.Overlay--height-xsmall {
height: min(192px, 100% - 2rem);
}
&.Overlay--height-small {
height: min(256px, 100% - 2rem);
}
&.Overlay--height-medium {
height: min(320px, 100% - 2rem);
}
&.Overlay--height-large {
height: min(432px, 100% - 2rem);
}
&.Overlay--height-xlarge {
height: min(600px, 100% - 2rem);
}
&.Overlay--width-auto {
width: auto;
}
&.Overlay--width-small {
width: min(256px, 100% - 2rem);
}
&.Overlay--width-medium {
width: min(320px, 100% - 2rem);
}
&.Overlay--width-large {
// stylelint-disable-next-line primer/responsive-widths
width: min(480px, 100% - 2rem);
}
&.Overlay--width-xlarge {
// stylelint-disable-next-line primer/responsive-widths
width: min(640px, 100% - 2rem);
}
&.Overlay--width-xxlarge {
// stylelint-disable-next-line primer/responsive-widths
width: min(960px, 100% - 2rem);
}
&.Overlay--motion-scaleFade {
@media screen and (prefers-reduced-motion: no-preference) {
animation: 200ms cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running Overlay--motion-scaleFade;
}
}
@keyframes Overlay--motion-scaleFade {
0% {
opacity: 0;
transform: scale(0.5);
}
100% {
opacity: 1;
transform: scale(1);
}
}
}
// for <form> element that wraps entire contents of overlay
.Overlay-form {
display: flex;
overflow: auto;
flex-direction: column;
flex-grow: 1;
}
.Overlay-header {
z-index: 1;
display: flex;
flex-direction: column;
&.Overlay-header--divided {
padding-bottom: $spacer-2;
// stylelint-disable-next-line primer/box-shadow
box-shadow: inset 0 #{-$border-width} var(--color-border-default);
+ .Overlay-body {
padding-top: $spacer-3;
}
}
&.Overlay-header--large {
.Overlay-headerContentWrap {
.Overlay-titleWrap {
gap: $spacer-2;
.Overlay-title {
font-size: $h3-size;
}
.Overlay-description {
font-size: $body-font-size;
}
}
}
}
.Overlay-headerContentWrap {
display: flex;
align-items: flex-start;
gap: $spacer-2;
padding: $spacer-2 $spacer-2 0 $spacer-2;
.Overlay-actionWrap {
display: flex;
flex-direction: row;
gap: $spacer-2;
}
.Overlay-titleWrap {
display: flex;
padding: ($spacer-2 * 0.75) 0 ($spacer-2 * 0.75) $spacer-2;
flex-direction: column;
flex-grow: 1;
gap: $spacer-1;
.Overlay-title {
margin: 0;
font-size: $body-font-size;
font-weight: $font-weight-bold;
}
.Overlay-description {
margin: 0;
font-size: $font-size-small;
font-weight: $font-weight-normal;
color: var(--color-fg-muted);
}
}
}
}
// generic body content slot
.Overlay-body {
padding: $spacer-3;
padding-top: 0;
overflow-y: auto;
scrollbar-width: thin;
font-size: $body-font-size;
flex-grow: 1;
&.Overlay-body--paddingCondensed {
padding: $spacer-2;
padding-top: 0;
}
&.Overlay-body--paddingNone {
padding: 0;
}
}
// generic footer slot
.Overlay-footer {
z-index: 1;
display: flex;
padding: 0 $spacer-3 $spacer-3 $spacer-3;
flex-direction: row;
flex-shrink: 0;
flex-wrap: wrap;
&.Overlay-footer--divided {
padding-top: $spacer-3;
// stylelint-disable-next-line primer/box-shadow
box-shadow: inset 0 $border-width var(--color-border-default);
}
&.Overlay-footer--alignStart {
justify-content: flex-start;
gap: $spacer-2;
}
&.Overlay-footer--alignCenter {
justify-content: center;
gap: $spacer-2;
}
&.Overlay-footer--alignEnd {
justify-content: flex-end;
gap: $spacer-2;
}
}
// TODO: replace with refactored IconButton
.Overlay-closeButton {
position: relative;
display: grid;
width: $spacer-5;
height: $spacer-5;
padding: 0;
color: var(--color-fg-muted);
cursor: pointer;
user-select: none;
background-color: transparent;
border: $border-width $border-style transparent;
border-radius: $border-radius;
transition: 0.2s cubic-bezier(0.3, 0, 0.5, 1);
transition-property: color, background-color, border-color;
place-content: center;
align-self: flex-start;
flex-shrink: 0;
&:hover,
&:focus {
background-color: var(--color-btn-hover-bg);
border: $border-width $border-style var(--color-btn-hover-bg);
}
}
@mixin Overlay-backdrop() {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
display: flex;
background-color: var(--color-neutral-muted);
}
@mixin Overlay-backdrop--transparent() {
position: absolute;
background-color: transparent;
}
// variants must be mixins so we can extend within a media query (@extend is not supported inside media queries)
// border-radius repeats within placement options to ensure the original radius is reset when two classes co-exist
// center
@mixin Overlay-backdrop--center {
@include Overlay-backdrop;
align-items: center;
justify-content: center;
}
// anchor
@mixin Overlay-backdrop--anchor {
@include Overlay-backdrop--transparent;
.Overlay {
width: auto;
}
}
// anchor side(s)
@mixin Overlay-backdrop--side($responsiveVariant: '') {
@include Overlay-backdrop;
// default left
align-items: center;
justify-content: left;
&.Overlay-backdrop--placement-left#{$responsiveVariant} {
align-items: center;
justify-content: left;
.Overlay#{$responsiveVariant} {
border-radius: $primer-borderRadius-large;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
&.Overlay-backdrop--placement-right#{$responsiveVariant} {
align-items: center;
justify-content: right;
.Overlay#{$responsiveVariant} {
border-radius: $primer-borderRadius-large;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
&.Overlay-backdrop--placement-bottom#{$responsiveVariant} {
align-items: end;
justify-content: center;
.Overlay#{$responsiveVariant} {
border-radius: $primer-borderRadius-large;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
}
&.Overlay-backdrop--placement-top#{$responsiveVariant} {
align-items: start;
justify-content: center;
.Overlay#{$responsiveVariant} {
border-radius: $primer-borderRadius-large;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
}
}
// full width
@mixin Overlay-backdrop--full {
@include Overlay-backdrop;
.Overlay {
width: 100%;
max-width: 100vw;
height: 100%;
max-height: 100vh;
border-radius: unset;
flex-grow: 1;
}
}
// Overlay variant classnames
.Overlay-backdrop--center {
@include Overlay-backdrop--center;
}
.Overlay-backdrop--anchor {
@include Overlay-backdrop--anchor;
}
.Overlay-backdrop--side {
@include Overlay-backdrop--side;
}
.Overlay-backdrop--full {
@include Overlay-backdrop--full;
}
// responsive variants
// up to 767px
@media (max-width: #{map-get($breakpoints, 'md') - 0.02px}) {
.Overlay-backdrop--center-whenNarrow {
@include Overlay-backdrop--center;
}
.Overlay-backdrop--anchor-whenNarrow {
@include Overlay-backdrop--anchor;
}
.Overlay-backdrop--side-whenNarrow {
@include Overlay-backdrop--side('-whenNarrow');
}
.Overlay-backdrop--full-whenNarrow {
@include Overlay-backdrop--full;
}
}

View File

@ -24,4 +24,4 @@
@import '../select-menu/index.scss';
@import '../subhead/index.scss';
@import '../timeline/index.scss';
@import '../toasts/index.scss'
@import '../toasts/index.scss';