From 8886b4f6530abca44d3ec9c3248ec86dea6a377f Mon Sep 17 00:00:00 2001 From: smathis Date: Wed, 29 Apr 2020 15:35:36 -0500 Subject: [PATCH] fix: core - refactoring alerts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • updated alert-actions to display link-style buttons in light and default alerts per current design • added min-width and text-decoration custom properties to buttons for internal use inside of alerts • added close button internal component • added inline buttons • hiding alert-actions inside a lightweight alert • moving icon and badge up to the base button because other button types needed them • added styles and code to support links in icon buttons • added styles and code to support links in inline buttons • added stories for links in inline buttons • added stories for links in icon buttons • added loading status to alerts • added compact alerts (specifically compact lightweight alerts) • getting close-button internal component to 100% test coverage • boosting branch coverage to over 90% • improving css-vars test coverage • added alert group element • moved alert group types and alert status type interfaces to internal • added lightweight alert group • added compact alert groups • verified close change event emits from alerts alert group • verified that alert-group status does not override custom icon shapes in alerts • verified that alerts with a loading status are not overridden by the alert-group status • created css utility functions • refactored the CssHelpers mixin to use the css utility functions • made it so that only alerts care about their children • alert-groups don't know anything about alert-actions • removing old alert-content component • removing old alert scss file • added aria and unit tests for a11y • fixed issue with sr-only elements inside of cds-icon that was revealing them in Safari • setting up app-level alert groups • removing aria-live declarations from alerts • refactoring alerts to slot cds-icons as custom icons instead of using icon-shape attr • removing old app-alert files • refactoring app-level alert group type to banner • styles for banner alerts loading status • limiting compact sizing to just default alerts • adding compact sizing to banner alerts is not possible... • ...because we do not have outline buttons that are smaller than 24px height • adding verification for app-alert pager • adding code that allows for banners to be center aligned without a pager and left aligned with a pager • adding banner alert statuses • implementing functionality that forces banner alert groups to update when their child alert's status changes • removing old app-alert styles • increased branch code coverage threshold to 90% • added tests for new layouts introduced by banner alert group pager • added `assignSlotNames` helper to centralize work of adding slot attrs in connectedCallback • fixed issue where light alert groups were not getting status color on their icons as expected • story on alert page said "link button" instead of inline button • fixed Safari width issue on alerts • fixing issue with badges inside of tags in Safari • Safari was having problems with aligning the badge across the shadowDOM • badges were unreadable in Safari; font-weight needed to be increased • fixing close button sizes corrected issues in Firefox and Safari • updated close-button for improved overrides Signed-off-by: Scott Mathis --- apps/core-parcel-js/src/index.css | 1 + apps/core-parcel-js/src/index.html | 305 ++++++++++ .../progress/spinner/_spinner.clarity.scss | 7 +- packages/core/import-map.importmap | 1 + packages/core/karma.conf.js | 4 +- .../core/src/alert/alert-actions.element.scss | 58 +- .../src/alert/alert-actions.element.spec.ts | 167 +++++- .../core/src/alert/alert-actions.element.ts | 82 ++- .../core/src/alert/alert-content.element.scss | 1 - .../src/alert/alert-content.element.spec.ts | 31 - .../core/src/alert/alert-content.element.ts | 50 -- .../core/src/alert/alert-group.element.scss | 157 ++++++ .../src/alert/alert-group.element.spec.ts | 362 ++++++++++++ .../core/src/alert/alert-group.element.ts | 173 ++++++ .../core/src/alert/alert-group.stories.mdx | 78 +++ .../core/src/alert/alert-group.stories.ts | 533 ++++++++++++++++++ packages/core/src/alert/alert.base.ts | 138 ----- packages/core/src/alert/alert.element.scss | 277 ++++----- packages/core/src/alert/alert.element.spec.ts | 485 +++++++++++++--- packages/core/src/alert/alert.element.ts | 299 +++++++++- packages/core/src/alert/alert.interfaces.ts | 9 + packages/core/src/alert/alert.stories.mdx | 44 +- packages/core/src/alert/alert.stories.ts | 209 ++++--- .../core/src/alert/app-alert.element.scss | 66 --- .../core/src/alert/app-alert.element.spec.ts | 164 ------ packages/core/src/alert/app-alert.element.ts | 50 -- packages/core/src/alert/app-alert.stories.mdx | 40 -- packages/core/src/alert/app-alert.stories.ts | 117 ---- .../core/src/alert/entrypoint.tsconfig.json | 1 + packages/core/src/alert/index.ts | 4 +- packages/core/src/badge/badge.element.scss | 22 +- .../core/src/button/base-button.element.scss | 33 +- .../core/src/button/button.element.spec.ts | 104 +++- packages/core/src/button/button.element.ts | 12 +- .../core/src/button/icon-button.element.scss | 16 + .../src/button/icon-button.element.spec.ts | 41 +- .../core/src/button/icon-button.element.ts | 18 +- .../core/src/button/icon-button.stories.mdx | 12 +- .../core/src/button/icon-button.stories.ts | 19 + packages/core/src/button/index.ts | 1 + .../src/button/inline-button.element.scss | 65 +++ .../src/button/inline-button.element.spec.ts | 55 ++ .../core/src/button/inline-button.element.ts | 63 +++ .../core/src/button/inline-button.stories.mdx | 60 ++ .../core/src/button/inline-button.stories.ts | 156 +++++ .../core/src/icon-shapes/icon.element.scss | 10 +- .../core/src/icon-shapes/icon.element.spec.ts | 4 +- packages/core/src/icon-shapes/icon.element.ts | 6 +- packages/core/src/icon-shapes/icon.stories.ts | 20 +- .../close-button/close-button.element.scss | 34 ++ .../close-button/close-button.element.spec.ts | 36 ++ .../close-button/close-button.element.ts | 66 +++ .../entrypoint.tsconfig.json | 12 + .../core/src/internal-components/index.ts | 7 + .../core/src/internal-components/package.json | 4 + .../core/src/internal/base/button.base.ts | 9 +- .../src/internal/css-vars/css-vars.spec.ts | 45 +- .../core/src/internal/css-vars/css-vars.ts | 5 +- .../src/internal/decorators/event.spec.ts | 7 + packages/core/src/internal/index.ts | 1 + .../core/src/internal/mixins/css-helpers.ts | 27 +- .../services/common-strings.service.spec.ts | 5 + packages/core/src/internal/utils/css.spec.ts | 89 +++ packages/core/src/internal/utils/css.ts | 40 ++ packages/core/src/internal/utils/dom.spec.ts | 51 ++ packages/core/src/internal/utils/dom.ts | 9 + .../core/src/styles/color/color.stories.mdx | 10 +- .../src/styles/layout/docs/grid.stories.mdx | 10 +- .../styles/layout/docs/horizontal.stories.mdx | 12 +- .../src/styles/layout/docs/layout.stories.mdx | 10 +- .../styles/layout/docs/patterns.stories.mdx | 10 +- .../styles/layout/docs/spacing.stories.mdx | 10 +- .../styles/layout/docs/utilities.stories.mdx | 12 +- .../styles/layout/docs/vertical.stories.mdx | 10 +- .../core/src/styles/layout/layout.stories.ts | 32 ++ packages/core/src/styles/mixins/_utils.scss | 14 + .../src/styles/spacing/spacing.stories.mdx | 10 +- .../core/src/styles/tokens/tokens.stories.mdx | 10 +- .../styles/typography/typography.stories.mdx | 10 +- packages/core/src/tag/tag.element.scss | 30 +- packages/core/src/tag/tag.element.spec.ts | 5 + packages/core/src/tag/tag.element.ts | 20 +- packages/core/src/tag/tag.stories.ts | 80 +-- packages/core/src/test/utils.ts | 9 +- packages/core/stylelint.config.js | 2 +- packages/core/tsconfig.project.json | 1 + packages/icons/package.json | 2 +- packages/ui/package.json | 2 +- 88 files changed, 4154 insertions(+), 1234 deletions(-) delete mode 100644 packages/core/src/alert/alert-content.element.scss delete mode 100644 packages/core/src/alert/alert-content.element.spec.ts delete mode 100644 packages/core/src/alert/alert-content.element.ts create mode 100644 packages/core/src/alert/alert-group.element.scss create mode 100644 packages/core/src/alert/alert-group.element.spec.ts create mode 100644 packages/core/src/alert/alert-group.element.ts create mode 100644 packages/core/src/alert/alert-group.stories.mdx create mode 100644 packages/core/src/alert/alert-group.stories.ts delete mode 100644 packages/core/src/alert/alert.base.ts create mode 100644 packages/core/src/alert/alert.interfaces.ts delete mode 100644 packages/core/src/alert/app-alert.element.scss delete mode 100644 packages/core/src/alert/app-alert.element.spec.ts delete mode 100644 packages/core/src/alert/app-alert.element.ts delete mode 100644 packages/core/src/alert/app-alert.stories.mdx delete mode 100644 packages/core/src/alert/app-alert.stories.ts create mode 100644 packages/core/src/button/inline-button.element.scss create mode 100644 packages/core/src/button/inline-button.element.spec.ts create mode 100644 packages/core/src/button/inline-button.element.ts create mode 100644 packages/core/src/button/inline-button.stories.mdx create mode 100644 packages/core/src/button/inline-button.stories.ts create mode 100644 packages/core/src/internal-components/close-button/close-button.element.scss create mode 100644 packages/core/src/internal-components/close-button/close-button.element.spec.ts create mode 100644 packages/core/src/internal-components/close-button/close-button.element.ts create mode 100644 packages/core/src/internal-components/entrypoint.tsconfig.json create mode 100644 packages/core/src/internal-components/index.ts create mode 100644 packages/core/src/internal-components/package.json create mode 100644 packages/core/src/internal/utils/css.spec.ts create mode 100644 packages/core/src/internal/utils/css.ts diff --git a/apps/core-parcel-js/src/index.css b/apps/core-parcel-js/src/index.css index e1deebc820..ca12395764 100644 --- a/apps/core-parcel-js/src/index.css +++ b/apps/core-parcel-js/src/index.css @@ -1,4 +1,5 @@ @import '../node_modules/@clr/core/global.min.css'; +@import '../node_modules/@clr/core/styles/module.layout.min.css'; @import '../node_modules/@clr/city/css/bundles/default.min.css'; body { diff --git a/apps/core-parcel-js/src/index.html b/apps/core-parcel-js/src/index.html index 8d3ddace15..e5408ea38d 100644 --- a/apps/core-parcel-js/src/index.html +++ b/apps/core-parcel-js/src/index.html @@ -9,6 +9,311 @@

Clarity Core Demo - JavaScript

+
+

Alerts

+ +

Lightweight Alerts

+
+ + + This example is an alert with a status of "info" inside a lightweight alert group. + + + This example is an alert with a status of "danger" and inline action buttons inside a lightweight alert + group. + Clickable Action + + + This example is an alert with a status of "loading" inside a lightweight alert group. + + Alert actions should not be viewable in lightweight alerts + Alert actions should not be viewable in lightweight alerts + + + + This example is a multi-line alert with a status of "unknown" inside a lightweight alert group. A block of + lorem ipsum sample text follows: Drake Equation take root and flourish culture rings of Uranus quasar + hundreds of thousands? Cambrian explosion gathered by gravity of brilliant syntheses vanquish the + impossible finite but unbounded not a sunrise but a galaxyrise. Intelligent beings two ghostly white + figures in coveralls and helmets are soflty dancing something incredible is waiting to be known vanquish + the impossible vastness is bearable only through love concept of the number one and billions upon billions + upon billions upon billions upon billions upon billions upon billions. + + Button 1 + + + +
+ +

Default Alerts (Pastel Bubbles)

+
+ + + This example is a closable alert inside an alert group with a status of "info". + + + + This example is a closable alert with a custom icon shape inside an alert group with a status of "info". + + + This example is an alert with a "loading" status and alert action buttons inside an alert group with a + status of "info". + + Button 1 + Button 2 + + + + + + + This example is a closable alert inside an alert group with a status of "success". + + + This example is a closable alert with alert action buttons inside an alert group with a status of + "success". + + Button 1 + Button 2 + + + + + + + This example is a closable alert inside an alert group with a status of "warning". + + + This example is a closable alert with alert action buttons inside an alert group with a status of + "warning". + + Button 1 + Button 2 + + + + + + + This example is a closable alert inside an alert group with a status of "danger". + + + This example is a closable alert with alert action buttons inside an alert group with a status of + "danger". + + Button 1 + Button 2 + + + + + + + This example is an alert inside an alert group. + + + This example is a closable alert inside an alert group. + + + + This example is an alert with alert action buttons and a custom icon shape inside an alert group. + + Button 1 + Button 2 + + + + This example is a closable alert with alert action buttons and multiple lines of text inside an alert + group. A block of lorem ipsum sample text follows: Drake Equation take root and flourish culture rings of + Uranus quasar hundreds of thousands? Cambrian explosion gathered by gravity of brilliant syntheses + vanquish the impossible finite but unbounded not a sunrise but a galaxyrise. Intelligent beings two + ghostly white figures in coveralls and helmets are soflty dancing something incredible is waiting to be + known vanquish the impossible vastness is bearable only through love concept of the number one and + billions upon billions upon billions upon billions upon billions upon billions upon billions. + + Button 1 + + + +
+ +

Compact Alerts

+
+ + + This example is a closable alert inside a compact alert group with a status of "info". + + + + This example is a closable alert with alert actions and a custom icon shape inside a compact alert group + with a status of "info". + + Button 1 + Button 2 + + + + + + + This example is a closable alert inside a compact alert group with a status of "success". + + + This example is a closable alert with alert actions and a custom icon shape inside a compact alert group + with a status of "success". + + Button 1 + Button 2 + + + + + + + This example is a closable alert inside a compact alert group with a status of "warning". + + + + This example is a closable alert with alert actions and a custom icon shape inside a compact alert group + with a status of "warning". + + Button 1 + Button 2 + + + + + + + This example is a closable alert with a status of "loading" inside a compact alert group with a status of + "danger". + + + This example is a closable alert with alert actions inside a compact alert group with a status of + "warning". + + Button 1 + Button 2 + + + + + + + This example is an alert inside a compact alert group. + + + This example is a closable alert inside a compact alert group. + + + This example is an alert with alert actions inside a compact alert group. + + Button 1 + Button 2 + + + + This example is a closable alert with multiple lines of text and an alert action inside a compact alert + group. A block of lorem ipsum sample text follows: Drake Equation take root and flourish culture rings of + Uranus quasar hundreds of thousands? Cambrian explosion gathered by gravity of brilliant syntheses + vanquish the impossible finite but unbounded not a sunrise but a galaxyrise. Intelligent beings two + ghostly white figures in coveralls and helmets are soflty dancing something incredible is waiting to be + known vanquish the impossible vastness is bearable only through love concept of the number one and + billions upon billions upon billions upon billions upon billions upon billions upon billions. + + Button 1 + + + + + + This example is an alert with a status of "info" inside a compact, lightweight alert group. + + + This example is an alert with a status of "danger" and an inline action inside a compact, lightweight + alert group.Clickable Action + + + This example is an alert with a status of "loading" inside a compact, lightweight alert group. + + Alert actions should not be viewable in lightweight alerts + Alert actions should not be viewable in lightweight alerts + + + + This example is a multi-line alert with a status of "unknown" and two inline actions inside a compact, + lightweight alert group. A block of lorem ipsum sample text follows: Drake Equation take root and flourish + culture rings of Uranus quasar hundreds of thousands? Cambrian explosion gathered by gravity of brilliant + syntheses vanquish the impossible finite but unbounded not a sunrise but a galaxyrise. Intelligent beings + two ghostly white figures in coveralls and helmets are soflty dancing something incredible is waiting to + be known vanquish the impossible vastness is bearable only through love concept of the number one and + billions upon billions upon billions upon billions upon billions upon billions upon + billions.Clickable Action 1Clickable Action 2 + + Alert actions should not be viewable in lightweight alerts + + + +
+ +

Banner Alerts (App-Level)

+
+ + + This example is a closable banner alert inside a banner alert group with a status of "info". + + + + + + This example is a closable alert with a status of "success" inside a banner alert group. + + + + + + This example is a closable alert with alert action buttons and a status of "warning" inside a banner alert + group. + + Button 1 + Button 2 + + + + + + + This example is a closable alert with a status of "danger" inside a banner alert group. + + Button 1 + Button 2 + + + + + + + This example is a non-closable alert with alert actions and a status of "unknown" inside a banner alert + group. + + Button 1 + Button 2 + + + + + + + This example is an alert with alert actions and a status of "loading" inside a banner alert group. + + Button 1 + + + +
+
+

Buttons

primary diff --git a/packages/angular/projects/clr-angular/src/progress/spinner/_spinner.clarity.scss b/packages/angular/projects/clr-angular/src/progress/spinner/_spinner.clarity.scss index 140da2e4d0..c57350b758 100644 --- a/packages/angular/projects/clr-angular/src/progress/spinner/_spinner.clarity.scss +++ b/packages/angular/projects/clr-angular/src/progress/spinner/_spinner.clarity.scss @@ -1,4 +1,4 @@ -// Copyright (c) 2016-2019 VMware, Inc. All Rights Reserved. +// Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. // This software is released under MIT license. // The full license information can be found in LICENSE in the root directory of this project. @@ -34,6 +34,11 @@ background: generateSpinnerIcon($clr-spinner-inverse-bg-color, #74c1e2); } + &.spinner-neutral-0 { + // needed for loading banner alerts in core + background: generateSpinnerIcon(transparent, #ffffff, 1); + } + &.spinner-check { animation: none; background: generateCheckIcon($clr-spinner-color); diff --git a/packages/core/import-map.importmap b/packages/core/import-map.importmap index 42516c35e6..a28a1cb23a 100644 --- a/packages/core/import-map.importmap +++ b/packages/core/import-map.importmap @@ -24,6 +24,7 @@ "@clr/core/icon": "/base/dist/core/icon/index.js", "@clr/core/icon-shapes": "/base/dist/core/icon-shapes/index.js", "@clr/core/internal": "/base/dist/core/internal/index.js", + "@clr/core/internal-components": "/base/dist/core/internal-components/index.js", "@clr/core/modal": "/base/dist/core/modal/index.js", "@clr/core/tag": "/base/dist/core/tag/index.js", "@clr/core/test-dropdown": "/base/dist/core/test-dropdown/index.js", diff --git a/packages/core/karma.conf.js b/packages/core/karma.conf.js index f01338026b..1fd844fc5e 100644 --- a/packages/core/karma.conf.js +++ b/packages/core/karma.conf.js @@ -12,7 +12,7 @@ module.exports = config => { require('karma-coverage-istanbul-reporter'), ], client: { clearContext: false }, - files: glob.sync('dist/core/**/*.spec.js').map(f => ({ pattern: f, type: 'module' })), + files: glob.sync('dist/core/**/!(test-dropdown.element).spec.js').map(f => ({ pattern: f, type: 'module' })), esm: { coverage: true, importMap: './import-map.importmap', @@ -46,7 +46,7 @@ module.exports = config => { thresholds: { statements: 90, lines: 90, - branches: 85, // need to get to 90 + branches: 89, functions: 90, }, }, diff --git a/packages/core/src/alert/alert-actions.element.scss b/packages/core/src/alert/alert-actions.element.scss index 7e60a8a6d9..22101ba314 100644 --- a/packages/core/src/alert/alert-actions.element.scss +++ b/packages/core/src/alert/alert-actions.element.scss @@ -1,8 +1,64 @@ +@import './../styles/tokens/generated/index'; @import './../styles/mixins/utils'; :host { - flex: 0 0 auto; white-space: nowrap; // TODO: add style override for dropdown component inside alert-actions after cds-dropdown is implemented } + +:host([type='light']) { + display: none !important; +} + +::slotted(cds-button.alert-btn) { + --color: var(--action-text-color, #{$cds-token-color-neutral-700-static}); + --border-color: var(--action-text-color, #{$cds-token-color-neutral-700-static}); + --background: none; + --padding: none; + --box-shadow-color: transparent; + --text-transform: none; + --text-decoration: underline; + --border-width: 0; + --height: var(--action-size, auto); + --min-width: initial; + + display: inline-block; +} + +::slotted(cds-button.alert-btn:hover) { + --color: var(--action-hover-text-color, #{$cds-token-color-neutral-1000-static}); + --border-color: var(--action-hover-text-color, #{$cds-token-color-neutral-1000-static}); +} + +:host([type='default']) { + --action-size: calc(#{$cds-token-space-size-9} - #{$cds-token-space-size-3}); + padding-top: #{$cds-token-space-size-2}; + + ::slotted(cds-button) { + // slight nudges in alert-actions adjusting for styling across slots. + // !important necessary to override specificity of layout styles. + margin-top: calc(0.14 * var(--action-size)) !important; + } +} + +:host([type='banner']) { + --action-size: #{$cds-token-space-size-9}; + + ::slotted(cds-button) { + // slight nudges in alert-actions adjusting for styling across slots. + // !important necessary to override specificity of layout styles. + margin-top: #{$cds-token-space-size-1} !important; + } +} + +:host([type='default']) .private-host { + height: calc(var(--action-size) + #{$cds-token-space-size-2}); +} + +:host([type='default']) ::slotted(cds-button.alert-btn) { + --font-size: var(--font-size, #{$cds-token-typography-font-size-3}); + --letter-spacing: normal; + --height: var(--action-size); + line-height: 1em; +} diff --git a/packages/core/src/alert/alert-actions.element.spec.ts b/packages/core/src/alert/alert-actions.element.spec.ts index 0372e71bc6..d1f67ae892 100644 --- a/packages/core/src/alert/alert-actions.element.spec.ts +++ b/packages/core/src/alert/alert-actions.element.spec.ts @@ -3,35 +3,166 @@ * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ + import { CdsAlertActions } from '@clr/core/alert'; +import { CdsButton } from '@clr/core/button'; import '@clr/core/alert'; +import '@clr/core/button'; import { componentIsStable, createTestElement, removeTestElement, waitForComponent } from '@clr/core/test/utils'; +import { getAlertActionsLayout, setActionButtonStyles } from './alert-actions.element.js'; describe('alert-actions element', () => { - let testElement: HTMLElement; - let component: CdsAlertActions; - const placeholderText = 'Alert Text Placeholder'; + describe(' - the basics: ', () => { + let testElement: HTMLElement; + let component: CdsAlertActions; + const placeholderText = 'Alert Text Placeholder'; - beforeEach(async () => { - testElement = createTestElement(); - testElement.innerHTML = `${placeholderText}`; + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = `${placeholderText}`; - await waitForComponent('cds-alert-actions'); - component = testElement.querySelector('cds-alert-actions'); - }); + await waitForComponent('cds-alert-actions'); + component = testElement.querySelector('cds-alert-actions'); + }); + + afterEach(() => { + removeTestElement(testElement); + }); - afterEach(() => { - removeTestElement(testElement); + it('should create the component', async () => { + await componentIsStable(component); + expect(component.innerText).toBe(placeholderText); + }); }); - it('should create the component', async () => { - await componentIsStable(component); - expect(component.innerText).toBe(placeholderText); + describe(' - setup: ', () => { + let testElement: HTMLElement; + let component: CdsAlertActions; + + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` + ohai + kthxbye + `; + + await waitForComponent('cds-alert-actions'); + component = testElement.querySelector('cds-alert-actions'); + }); + + afterEach(() => { + removeTestElement(testElement); + }); + + it('should have a slot attribute of value `actions`', async () => { + await componentIsStable(component); + expect(component.hasAttribute('slot')).toBe(true); + expect(component.getAttribute('slot')).toEqual('actions'); + }); + + it('should setup buttons as expected', async () => { + await componentIsStable(component); + const buttons = component.querySelectorAll('cds-button'); + buttons.forEach(b => { + expect(b.classList.contains('alert-btn')).toBe(true); + expect(b.hasAttribute('size')).toBe(false); + expect(b.hasAttribute('status')).toBe(false); + }); + }); }); - it('should have a slot attribute of value `actions`', async () => { - await componentIsStable(component); - expect(component.hasAttribute('slot')).toBe(true); - expect(component.getAttribute('slot')).toEqual('actions'); + describe(' - supporting functions: ', () => { + describe('getAlertActionsLayout', () => { + it('should fallthrough to false to remove cds-layout attribute', () => { + const expected = getAlertActionsLayout('anything'); + expect(expected).toBe(false); + }); + + it('should return "default" cds-layout attribute values', () => { + const expected = (getAlertActionsLayout('default', true) as string).split(' '); + expect(expected.indexOf('horizontal') > -1).toBe(true, 'should have horizontal layout'); + expect(expected.indexOf('gap:xs') > -1).toBe(true, 'should have xs gap'); + expect(expected.indexOf('p-l:md') > -1).toBe(true, 'should have md padding on the left'); + expect(expected.indexOf('p-r:sm') > -1).toBe(false, 'should NOT have right padding'); + expect(expected.indexOf('align:vertical-center') > -1).toBe(true, 'should be vertically centered'); + expect(expected.indexOf('align-left') > -1).toBe(false, 'should NOT be aligned left'); + expect(expected.indexOf('align:horizontal-stretch') > -1).toBe(false, 'should NOT fill flex layout'); + }); + + it('should return "banner" cds-layout attribute values', () => { + const expected = (getAlertActionsLayout('banner') as string).split(' '); + expect(expected.indexOf('horizontal') > -1).toBe(true, 'should have horizontal layout'); + expect(expected.indexOf('gap:xs') > -1).toBe(true, 'should have xs gap'); + expect(expected.indexOf('p-l:md') > -1).toBe(true, 'should have md padding on the left'); + expect(expected.indexOf('p-r:sm') > -1).toBe(false, 'should NOT have right padding'); + expect(expected.indexOf('align:vertical-center') > -1).toBe(false, 'should not force vertical centering'); + expect(expected.indexOf('align-left') > -1).toBe(true, 'should be aligned left'); + expect(expected.indexOf('align:horizontal-stretch') > -1).toBe(true, 'should NOT fill flex layout'); + }); + + it('should return different cds-layout attribute values if "default" and not "closable"', () => { + const expected = (getAlertActionsLayout('default') as string).split(' '); + expect(expected.indexOf('horizontal') > -1).toBe(true, 'should have horizontal layout'); + expect(expected.indexOf('gap:xs') > -1).toBe(true, 'should have xs gap'); + expect(expected.indexOf('p-l:md') > -1).toBe(true, 'should have md padding on the left'); + expect(expected.indexOf('p-r:sm') > -1).toBe(true, 'should have right padding'); + expect(expected.indexOf('align:vertical-center') > -1).toBe(true, 'should be vertically centered'); + expect(expected.indexOf('align-left') > -1).toBe(false, 'should NOT be aligned left'); + expect(expected.indexOf('align:horizontal-stretch') > -1).toBe(false, 'should NOT fill flex layout'); + }); + }); + + describe('setActionButtonStyles', () => { + let testElement: HTMLElement; + let component: CdsButton; + + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = `ohai`; + + await waitForComponent('cds-button'); + component = testElement.querySelector('cds-button'); + }); + + afterEach(() => { + removeTestElement(testElement); + }); + + it('should set "banner" attributes as expected', async () => { + const transformer = setActionButtonStyles('banner'); + transformer(component); + await componentIsStable(component); + expect(component.classList.contains('alert-btn')).toBe(false); + expect(component.hasAttribute('size') && component.getAttribute('size') === 'sm').toBe(true); + expect(component.hasAttribute('status') && component.getAttribute('status') === 'inverse').toBe(true); + }); + + it('should set "light" attributes as expected', async () => { + const transformer = setActionButtonStyles('light'); + transformer(component); + await componentIsStable(component); + expect(component.classList.contains('alert-btn')).toBe(true); + expect(component.hasAttribute('size')).toBe(false); + expect(component.hasAttribute('status')).toBe(false); + }); + + it('should set "default" attributes as expected', async () => { + const transformer = setActionButtonStyles('default'); + transformer(component); + await componentIsStable(component); + expect(component.classList.contains('alert-btn')).toBe(true); + expect(component.hasAttribute('size')).toBe(false); + expect(component.hasAttribute('status')).toBe(false); + }); + + it('should fallthrough to default attributes as expected', async () => { + const transformer = setActionButtonStyles('ohai'); + transformer(component); + await componentIsStable(component); + expect(component.classList.contains('alert-btn')).toBe(true); + expect(component.hasAttribute('size')).toBe(false); + expect(component.hasAttribute('status')).toBe(false); + }); + }); }); }); diff --git a/packages/core/src/alert/alert-actions.element.ts b/packages/core/src/alert/alert-actions.element.ts index 6c560ed515..0e0c34f6b0 100644 --- a/packages/core/src/alert/alert-actions.element.ts +++ b/packages/core/src/alert/alert-actions.element.ts @@ -4,37 +4,105 @@ * The full license information can be found in LICENSE in the root directory of this project. */ -import { baseStyles, registerElementSafely } from '@clr/core/internal'; +import { AlertGroupTypes, CdsAlert } from '@clr/core/alert'; +import { CdsButton } from '@clr/core/button'; +import { + addClassnames, + assignSlotNames, + baseStyles, + property, + querySlotAll, + registerElementSafely, + removeAttributes, + removeClassnames, + setAttributes, +} from '@clr/core/internal'; import { html, LitElement } from 'lit-element'; import { styles } from './alert-actions.element.css.js'; +export function getAlertActionsLayout(type: AlertGroupTypes | string, isParentClosable?: boolean): string | boolean { + const defaultLayoutValues = 'align:vertical-center'; + const sharedLayoutValues = 'horizontal gap:xs p-l:md'; + const bannerAlertLayoutValues = 'align-left align:horizontal-stretch'; + const nonClosableAlertActionsPadding = isParentClosable ? '' : type === 'banner' ? 'p-r:xs' : 'p-r:sm'; + + switch (type) { + case 'default': + return [sharedLayoutValues, defaultLayoutValues, nonClosableAlertActionsPadding].join(' '); + case 'banner': + return [bannerAlertLayoutValues, sharedLayoutValues, nonClosableAlertActionsPadding].join(' '); + default: + return false; + } +} + +export function setActionButtonStyles(type: AlertGroupTypes | string): (b: CdsButton) => void { + const alertBtnClassname = 'alert-btn'; + switch (type) { + case 'banner': + return (b: CdsButton) => { + setAttributes(b, ['status', 'inverse'], ['size', 'sm']); + removeClassnames(b, alertBtnClassname); + }; + default: + // 'default' or 'light' alerts + return (b: CdsButton) => { + removeAttributes(b, 'status', 'size'); + addClassnames(b, alertBtnClassname); + }; + } +} + +// this fn is cordoning off the stateful smarts of action button layouts and buttons +// the intent is to minimize the surface area of the component's brains +// warning: side effects ahead! +function updateLayoutsAndActionButtonStyles(alertActionsComponent: CdsAlertActions, parentAlert: CdsAlert) { + const actionsLayout = getAlertActionsLayout(alertActionsComponent.type, parentAlert.closable); + const buttonStyleSetter = setActionButtonStyles(alertActionsComponent.type); + setAttributes(alertActionsComponent, ['cds-layout', actionsLayout]); + alertActionsComponent.buttons.forEach(buttonStyleSetter); +} + /** - * Web component alert actions to be used inside app-level alert. + * Web component alert actions to be used inside default and banner alerts. * * ```typescript * import '@clr/core/alert'; * ``` * * ```html - * - * Lorem ipsum dolor sit amet + * + * Lorem ipsum dolor sit amet * * Fix * - * + * * ``` * * @beta * @element cds-alert-actions + * @slot default + * @cssprop --action-text-color: changes the color of the text and border of the action button + * @cssprop --action-hover-text-color: changes the color of the text and border of the action button on hover */ export class CdsAlertActions extends LitElement { + @property({ type: String }) + type: AlertGroupTypes | string = 'light'; + + @querySlotAll('cds-button') buttons: NodeListOf; + connectedCallback() { super.connectedCallback(); - this.setAttribute('slot', 'actions'); + assignSlotNames([this, 'actions']); + updateLayoutsAndActionButtonStyles(this, this.parentElement as CdsAlert); + } + + updated() { + updateLayoutsAndActionButtonStyles(this, this.parentElement as CdsAlert); } render() { - return html``; + return html`
`; } static get styles() { diff --git a/packages/core/src/alert/alert-content.element.scss b/packages/core/src/alert/alert-content.element.scss deleted file mode 100644 index ebc3a277d0..0000000000 --- a/packages/core/src/alert/alert-content.element.scss +++ /dev/null @@ -1 +0,0 @@ -@import './../styles/mixins/utils'; diff --git a/packages/core/src/alert/alert-content.element.spec.ts b/packages/core/src/alert/alert-content.element.spec.ts deleted file mode 100644 index 41bab8262b..0000000000 --- a/packages/core/src/alert/alert-content.element.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. - * This software is released under MIT license. - * The full license information can be found in LICENSE in the root directory of this project. - */ -import { CdsAlertContent } from '@clr/core/alert'; -import '@clr/core/alert'; -import { componentIsStable, createTestElement, removeTestElement, waitForComponent } from '@clr/core/test/utils'; - -describe('alert-content element', () => { - let testElement: HTMLElement; - let component: CdsAlertContent; - const placeholderContent = 'Alert Content Placeholder'; - - beforeEach(async () => { - testElement = createTestElement(); - testElement.innerHTML = `${placeholderContent}`; - - await waitForComponent('cds-alert-content'); - component = testElement.querySelector('cds-alert-content'); - }); - - afterEach(() => { - removeTestElement(testElement); - }); - - it('should create the component', async () => { - await componentIsStable(component); - expect(component.innerText).toBe(placeholderContent); - }); -}); diff --git a/packages/core/src/alert/alert-content.element.ts b/packages/core/src/alert/alert-content.element.ts deleted file mode 100644 index 1a1ae80220..0000000000 --- a/packages/core/src/alert/alert-content.element.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. - * This software is released under MIT license. - * The full license information can be found in LICENSE in the root directory of this project. - */ - -import { baseStyles, registerElementSafely } from '@clr/core/internal'; -import { html, LitElement } from 'lit-element'; -import { styles } from './alert-content.element.css.js'; - -/** - * Web component alert content to be used inside alert. - * - * ```typescript - * import '@clr/core/alert'; - * ``` - * - * ```html - * - * Lorem ipsum dolor sit amet - * - * - * - * Lorem ipsum dolor sit amet - * - * Fix - * - * - * ``` - * - * @beta - * @element cds-alert-content - */ -export class CdsAlertContent extends LitElement { - render() { - return html` `; - } - - static get styles() { - return [baseStyles, styles]; - } -} - -registerElementSafely('cds-alert-content', CdsAlertContent); - -declare global { - interface HTMLElementTagNameMap { - 'cds-alert-content': CdsAlertContent; - } -} diff --git a/packages/core/src/alert/alert-group.element.scss b/packages/core/src/alert/alert-group.element.scss new file mode 100644 index 0000000000..7a7f529616 --- /dev/null +++ b/packages/core/src/alert/alert-group.element.scss @@ -0,0 +1,157 @@ +@import './../styles/tokens/generated/index'; +@import './../styles/mixins/utils'; +@import './../styles/mixins/mixins'; + +:host { + --color: #{$cds-token-color-neutral-700}; + --icon-color: #{$cds-token-color-neutral-700}; + --icon-size: #{$cds-token-space-size-8}; + --font-size: #{$cds-token-typography-body-font-size}; + --font-weight: #{$cds-token-typography-body-font-weight}; + --letter-spacing: #{$cds-token-typography-body-letter-spacing}; + --padding: #{$cds-token-space-size-5} #{$cds-token-space-size-5} #{$cds-token-space-size-4} #{$cds-token-space-size-6}; + --background: #{$cds-token-color-neutral-100}; + --border-color: #{$cds-token-color-neutral-700}; + --border-width: #{$cds-token-global-border-width}; + --border-radius: #{$cds-token-global-border-radius}; + --pager-background: #{$cds-token-color-action-800}; + + width: 100%; +} + +.private-host { + background: var(--background); + border-width: var(--border-width); + border-color: var(--border-color); + border-style: solid; + border-radius: var(--border-radius); +} + +.alert-group-wrapper { + padding: var(--padding); +} + +:host([status='info']) { + --background: #{$cds-token-color-action-50}; + --color: #{$cds-token-color-neutral-700}; + --border-color: #{$cds-token-color-action-800}; + + ::slotted(cds-alert) { + --color: var(--color); + --icon-color: var(--border-color); + } +} + +:host([status='success']) { + --background: #{$cds-token-color-success-50}; + --color: #{$cds-token-color-neutral-700}; + --border-color: #{$cds-token-color-success-800}; + + ::slotted(cds-alert) { + --color: var(--color); + --icon-color: var(--border-color); + } +} + +:host([status='warning']) { + --background: #{$cds-token-color-warning-100}; + --color: #{$cds-token-color-neutral-900}; + --border-color: #{$cds-token-color-warning-800}; + + ::slotted(cds-alert) { + --color: var(--color); + --icon-color: var(--border-color); + } +} + +:host([status='danger']) { + --background: #{$cds-token-color-danger-100}; + --color: #{$cds-token-color-neutral-700}; + --border-color: #{$cds-token-color-danger-900}; + + ::slotted(cds-alert) { + --color: var(--color); + --icon-color: var(--border-color); + } +} + +:host([type='default'][size='sm']) .private-host { + --padding: #{$cds-token-space-size-3} #{$cds-token-space-size-3} #{$cds-token-space-size-2} #{$cds-token-space-size-4}; +} + +// lightweight alert group styles + +:host([type='light']) .private-host { + --background: transparent; + --border-radius: 0; + --padding: 0; + border: 0 none; +} + +// banner alert group styles + +:host([type='banner']) { + --padding: #{$cds-token-space-size-1} #{$cds-token-space-size-6}; + --border-width: 0; + + ::slotted(cds-alert) { + --color: #{$cds-token-color-neutral-0}; + --icon-color: #{$cds-token-color-neutral-0}; + } +} + +:host([type='banner']) .private-host { + --background: #{$cds-token-color-action-600}; + --border-color: #{$cds-token-color-action-600}; + --border-radius: 0; +} + +:host([type='banner'][status='warning']) .private-host { + // color carried over from clr-angular + --background: hsl(26, 100%, 38%); + --border-color: hsl(26, 100%, 38%); + --pager-background: #{$cds-token-color-warning-1000}; +} + +:host([type='banner'][status='danger']) .private-host { + --background: #{$cds-token-color-danger-800}; + --border-color: #{$cds-token-color-danger-800}; + --pager-background: #{$cds-token-color-danger-1000}; +} + +:host([type='banner'][status='success']) .private-host { + --background: #{$cds-token-color-success-700}; + --border-color: #{$cds-token-color-success-700}; + --pager-background: #{$cds-token-color-success-900}; +} + +:host([type='banner'][status='unknown']) .private-host { + --background: #{$cds-token-color-secondary-action-700}; + --border-color: #{$cds-token-color-secondary-action-700}; + --pager-background: #{$cds-token-color-secondary-action-900}; +} + +.no-pager .pager-wrapper { + display: none; +} + +.pager-wrapper { + background-color: var(--pager-background); + padding-top: $cds-token-space-size-2; + padding-bottom: $cds-token-space-size-2; +} + +:host([type='banner']) .alert-group-wrapper { + padding-top: $cds-token-space-size-2; + padding-bottom: $cds-token-space-size-2; +} + +// TODO: THESE STYLES ARE PLACEHOLDERS TO VERIFY THE ADDITION OF THE BANNER ALERT PAGER! +::slotted(.pager) { + color: $cds-token-color-neutral-0; + width: calc(2 * #{$cds-token-space-size-12}); + height: 100%; + display: inline-flex; + justify-content: center; + align-items: center; +} diff --git a/packages/core/src/alert/alert-group.element.spec.ts b/packages/core/src/alert/alert-group.element.spec.ts new file mode 100644 index 0000000000..4cb6b1f23a --- /dev/null +++ b/packages/core/src/alert/alert-group.element.spec.ts @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ +import { CdsAlert, CdsAlertGroup } from '@clr/core/alert'; +import '@clr/core/alert'; +import { + componentIsStable, + createTestElement, + getComponentSlotContent, + removeTestElement, + waitForComponent, +} from '@clr/core/test/utils'; + +describe('Alert groups – ', () => { + let testElement: HTMLElement; + let alertGroup: CdsAlertGroup; + let alerts: NodeListOf; + const placeholderText = 'I am an alert.'; + const alertStatusIconSelector = '.alert-status-icon'; + + describe('size: ', () => { + let compactAlertGroup: CdsAlertGroup; + + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` + + ${placeholderText} + + + ${placeholderText} + + `; + + await waitForComponent('cds-alert'); + alertGroup = testElement.querySelector('#default'); + compactAlertGroup = testElement.querySelector('#small'); + }); + + afterEach(() => { + removeTestElement(testElement); + }); + + it('sets to default size if none is defined', async () => { + await componentIsStable(alertGroup); + expect(alertGroup.size).toBe('default'); + }); + + it('sets compact size as expected', async () => { + await componentIsStable(compactAlertGroup); + alerts = compactAlertGroup.querySelectorAll('cds-alert'); + alerts.forEach(a => { + expect(a.size).toBe('sm'); + }); + }); + + it('updates as expected', async () => { + await componentIsStable(compactAlertGroup); + compactAlertGroup.querySelectorAll('cds-alert').forEach(a => { + expect(a.size).toBe('sm'); + }); + compactAlertGroup.size = 'default'; + await componentIsStable(compactAlertGroup); + expect(compactAlertGroup.getAttribute('size')).toBe('default', 'Alert group size should update'); + expect(compactAlertGroup.getAttribute('size')).toBe('default', 'Alert group size should reflect changes'); + compactAlertGroup.querySelectorAll('cds-alert').forEach(a => { + expect(a.getAttribute('size')).toBe('default'); + }); + }); + + it('sets cds-layout as expected', async () => { + const wrapper = compactAlertGroup.shadowRoot.querySelector('.alert-group-wrapper'); + let layout: string; + + await componentIsStable(compactAlertGroup); + + layout = wrapper.getAttribute('cds-layout'); + expect(layout.includes('vertical')).toBe(true, 'Compact alert group should include vertical layout'); + expect(layout.includes('no-wrap')).toBe(true, 'Compact alert group should include no-wrap layout'); + expect(layout.includes('gap:none')).toBe(true, 'Compact alert group should include gap:none layout'); + expect(layout.includes('gap:sm')).toBe(false, 'Compact alert group should NOT include gap:sm layout'); + expect(layout.includes('align:horizontal-stretch')).toBe( + true, + 'Compact alert group should include horizontal stretch layout' + ); + + compactAlertGroup.setAttribute('size', 'default'); + + await componentIsStable(compactAlertGroup); + + layout = wrapper.getAttribute('cds-layout'); + expect(layout.includes('vertical')).toBe(true, 'Default alert group should include vertical layout'); + expect(layout.includes('no-wrap')).toBe(true, 'Default alert group should include no-wrap layout'); + expect(layout.includes('gap:none')).toBe(false, 'Default alert group should NOT include gap:none layout'); + expect(layout.includes('gap:sm')).toBe(true, 'Default alert group should include gap:sm layout'); + expect(layout.includes('align:horizontal-stretch')).toBe( + true, + 'Default alert group should include horizontal stretch layout' + ); + }); + }); + + describe('type: ', () => { + let lightAlertGroup: CdsAlertGroup; + + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` + + ${placeholderText} + ${placeholderText} + + + ${placeholderText} + ${placeholderText} + + `; + + await waitForComponent('cds-alert'); + alertGroup = testElement.querySelector('#default'); + lightAlertGroup = testElement.querySelector('#light'); + }); + + afterEach(() => { + removeTestElement(testElement); + }); + + it('sets to default type if none is defined', async () => { + await componentIsStable(alertGroup); + expect(alertGroup.type).toBe('default'); + }); + + it('sets default as expected', async () => { + await componentIsStable(alertGroup); + alerts = alertGroup.querySelectorAll('cds-alert'); + alerts.forEach(a => { + expect(a.alertGroupType).toBe('default'); + }); + }); + + it('sets light as expected', async () => { + await componentIsStable(lightAlertGroup); + alerts = lightAlertGroup.querySelectorAll('cds-alert'); + alerts.forEach(a => { + expect(a.alertGroupType).toBe('light'); + }); + }); + + it('updates as expected', async () => { + alerts = lightAlertGroup.querySelectorAll('cds-alert'); + await componentIsStable(lightAlertGroup); + lightAlertGroup.type = 'default'; + await componentIsStable(lightAlertGroup); + alerts.forEach(a => { + expect(a.alertGroupType).toBe('default', 'updates alert group type to default as expected'); + expect(a.getAttribute('alert-group-type')).toBe( + 'default', + 'reflects alert group type attribute when changed to default as expected' + ); + }); + lightAlertGroup.type = 'light'; + await componentIsStable(lightAlertGroup); + expect(lightAlertGroup.getAttribute('type')).toBe( + 'light', + 'reflects alert group type attribute when changed to light as expected' + ); + alerts.forEach(a => { + expect(a.alertGroupType).toBe('light', "updates child alert's alert group type property as expected"); + expect(a.getAttribute('alert-group-type')).toBe( + 'light', + "reflects child alert's alert group type attribute as expected" + ); + }); + }); + }); + + describe('updateBannerAlertGroupStatus: ', () => { + let bannerAlertGroup: CdsAlertGroup; + let bannerAlertGroupWithNoStatus: CdsAlertGroup; + + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` + + ${placeholderText} + + + ${placeholderText} + + `; + await waitForComponent('cds-alert'); + bannerAlertGroup = testElement.querySelector('#bannerAlertGroup'); + bannerAlertGroupWithNoStatus = testElement.querySelector('#bannerAlertGroupNoStatus'); + }); + + afterEach(() => { + removeTestElement(testElement); + }); + + it('parent alert group adopts child alert status as expected', async () => { + await componentIsStable(bannerAlertGroup); + expect(bannerAlertGroup.status).not.toBeUndefined(); + expect(bannerAlertGroup.getAttribute('status')).toBe('warning'); + }); + + it('parent alert group does not set status if child alert status is not defined', async () => { + await componentIsStable(bannerAlertGroupWithNoStatus); + const childAlert = bannerAlertGroupWithNoStatus.querySelector('#statusless'); + expect(childAlert.status).toBeUndefined(); + expect(bannerAlertGroupWithNoStatus.getAttribute('status')).toBeNull(); + }); + + it('parent alert group is updated when child alert status is updated', async () => { + await componentIsStable(bannerAlertGroupWithNoStatus); + const childAlert = bannerAlertGroupWithNoStatus.querySelector('#statusless'); + expect(childAlert.status).toBeUndefined(); + childAlert.setAttribute('status', 'danger'); + await componentIsStable(bannerAlertGroupWithNoStatus); + expect(bannerAlertGroupWithNoStatus.getAttribute('status')).toBe('danger'); + }); + }); + + describe('status: ', () => { + let defaultAlert: CdsAlert; + let customAlert: CdsAlert; + let loadingAlert: CdsAlert; + + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` + + ${placeholderText} + ${placeholderText} + ${placeholderText} + + `; + + await waitForComponent('cds-alert'); + alertGroup = testElement.querySelector('cds-alert-group'); + defaultAlert = alertGroup.querySelector('#defaultAlert'); + customAlert = alertGroup.querySelector('#customAlert'); + loadingAlert = alertGroup.querySelector('#loadingAlert'); + }); + + afterEach(() => { + removeTestElement(testElement); + }); + + it('falls through if no status is defined', async () => { + await componentIsStable(alertGroup); + const customAlertSlotContent = getComponentSlotContent(customAlert); + const loadingAlertSlotContent = getComponentSlotContent(loadingAlert); + expect(alertGroup.status).toBeUndefined(); + expect(defaultAlert.shadowRoot.querySelector(alertStatusIconSelector).getAttribute('shape')).toBe( + 'info-standard', + 'does not override child alert icon shapes if status is not defined' + ); + expect(customAlertSlotContent['alert-icon'].includes('cds-icon shape="ohai"')).toBe( + true, + 'does not override child alert custom icon shapes' + ); + expect(loadingAlert.shadowRoot.querySelector(alertStatusIconSelector)).toBe( + null, + 'does not override child alert loading status pt. 1' + ); + expect(loadingAlertSlotContent['alert-icon']).toBeUndefined('does not override child alert loading status pt. 1'); + expect(loadingAlert.shadowRoot.querySelectorAll('.spinner-inline').length > 0).toBe( + true, + 'does not override child alert loading status pt. 2' + ); + }); + + it('updates status as expected', async () => { + await componentIsStable(alertGroup); + alertGroup.status = 'danger'; + await componentIsStable(alertGroup); + expect(alertGroup.status).toBe('danger', 'updates alert group status as expected'); + expect(alertGroup.getAttribute('status')).toBe('danger', 'reflects alert group status as expected'); + expect(defaultAlert.status).toBe('danger', 'updates child alerts whose status is not set to loading pt. 1'); + expect(defaultAlert.shadowRoot.querySelector(alertStatusIconSelector).getAttribute('shape')).toBe( + 'error-standard', + 'updates child alert icon shape when child alert is relying on alert status to display icon' + ); + const customAlertSlotContent = getComponentSlotContent(customAlert); + expect(customAlert.status).toBe('danger', 'updates child alerts whose status is not set to loading pt. 1'); + expect(customAlertSlotContent['alert-icon']).toBeDefined(); + expect(customAlertSlotContent['alert-icon'].includes('cds-icon shape="ohai"')).toBe( + true, + 'does not override child alert custom icon shapes even when child alert inherits alert group status' + ); + expect(loadingAlert.status).toBe('loading', 'does not update status of child alerts with a loading status'); + expect(loadingAlert.shadowRoot.querySelector(alertStatusIconSelector)).toBe( + null, + 'does not update icon shape of child alerts with a loading status pt. 1' + ); + expect(loadingAlert.shadowRoot.querySelectorAll('.spinner-inline').length > 0).toBe( + true, + 'does not update icon shape of child alerts with a loading status pt. 2' + ); + }); + }); + + describe('with pager: ', () => { + let alertGroup: CdsAlertGroup; + let alertGroupWithPager: CdsAlertGroup; + let pager: HTMLElement; + + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` + + ${placeholderText} + + +
Pager Here
+ ${placeholderText} +
+ `; + + await waitForComponent('cds-alert'); + alertGroup = testElement.querySelector('#noPager'); + alertGroupWithPager = testElement.querySelector('#hasPager'); + pager = testElement.querySelector('#pager'); + }); + + afterEach(() => { + removeTestElement(testElement); + }); + + it('sets layouts, slots, and classnames as expected if no pager is present', async () => { + await componentIsStable(alertGroup); + const hostWrapper = alertGroup.shadowRoot.querySelector('.private-host'); + expect(hostWrapper).not.toBeNull('private-host element should exist'); + const hostWrapperLayouts = hostWrapper.getAttribute('cds-layout').split(' '); + expect(hostWrapper.classList.contains('no-pager')).toBe(true, 'private-host element has no-pager classname'); + expect(hostWrapperLayouts.indexOf('horizontal') > -1).toBe(true, 'private-host element has horizontal layout'); + expect(hostWrapperLayouts.indexOf('no-wrap') > -1).toBe( + false, + 'private-host element does NOT have no-wrap layout' + ); + }); + + it('sets layouts, slots, and classnames as expected if pager is present', async () => { + await componentIsStable(alertGroupWithPager); + const hostWrapper = alertGroupWithPager.shadowRoot.querySelector('.private-host'); + expect(hostWrapper).not.toBeNull('private-host element should exist'); + const hostWrapperLayouts = hostWrapper.getAttribute('cds-layout').split(' '); + expect(hostWrapper.classList.contains('no-pager')).toBe( + false, + 'private-host element does NOT have no-pager classname' + ); + expect(hostWrapperLayouts.indexOf('horizontal') > -1).toBe(true, 'private-host element has horizontal layout'); + expect(hostWrapperLayouts.indexOf('no-wrap') > -1).toBe(true, 'private-host element has no-wrap layout'); + expect(pager.hasAttribute('slot') && pager.getAttribute('slot') === 'pager').toBe( + true, + 'sets slot for pager as expected' + ); + }); + }); +}); diff --git a/packages/core/src/alert/alert-group.element.ts b/packages/core/src/alert/alert-group.element.ts new file mode 100644 index 0000000000..1411652b77 --- /dev/null +++ b/packages/core/src/alert/alert-group.element.ts @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ + +import '@clr/core/icon'; +import '@clr/core/internal-components'; +import { + assignSlotNames, + baseStyles, + property, + querySlot, + querySlotAll, + registerElementSafely, +} from '@clr/core/internal'; +import { AlertGroupTypes, AlertStatusTypes, CdsAlert } from '@clr/core/alert'; +import { styles } from './alert-group.element.css.js'; +import { html, LitElement } from 'lit-element'; + +type AlertSizes = 'default' | 'sm'; + +/** + * Alert groups are containers for a set of alerts. Alert groups can hold one or many alerts + * inside of them with the expectation that all alerts will be of the same type. The exception + * to this rule is the `loading` alert type, which will be displayed regardless of the type + * of alert group containing it. + * + * ```typescript + * import '@clr/core/alert'; + * ``` + * + * ```html + * + * + * Single alert + * + * buttons, links + * + * + * + * Single Alert + * + * + * Another alert + * + * buttons, links + * + * + * + * ``` + * + * @beta + * @element cds-alert-group + * @slot default - Content slot for the alerts + * @cssprop --color + * @cssprop --icon-color + * @cssprop --icon-size + * @cssprop --font-size + * @cssprop --font-weight + * @cssprop --letter-spacing + * @cssprop --padding + * @cssprop --background + * @cssprop --border-radius + * @cssprop --border-color + * @cssprop --border-width + */ +export class CdsAlertGroup extends LitElement { + /** Sets the overall height and width of the alerts inside the alert group */ + @property({ type: String }) + size: AlertSizes = 'default'; + + /** + * Passed down into the alerts inside the alert-group + */ + @property({ type: String }) + type: AlertGroupTypes = 'default'; + + /** Sets the status of the alerts inside the alert group */ + @property({ type: String }) + status: AlertStatusTypes | ''; + + @querySlotAll('cds-alert') private alerts: NodeListOf; + + @querySlot('.pager') pager: HTMLElement; + + private updateAlertSizes() { + this.alerts.forEach(alrt => { + alrt.size = this.size; + }); + } + + private updateAlertStatuses() { + if (this.type === 'light' && !this.status) { + return; + } + if (this.type === 'banner') { + this.updateBannerAlertGroupStatus(); + } else { + this.alerts.forEach(alrt => { + if (alrt.isInitted && alrt.status !== 'loading') { + alrt.status = this.status; + } + }); + } + } + + updateBannerAlertGroupStatus() { + this.status = this.alerts[0].status || ''; + } + + private updateAlertTypes() { + this.alerts.forEach(alrt => { + alrt.alertGroupType = this.type; + }); + } + + private updateAlerts() { + this.updateAlertTypes(); + this.updateAlertStatuses(); + this.updateAlertSizes(); + } + + private alertTypesSet = false; + + connectedCallback() { + super.connectedCallback(); + if (!this.alertTypesSet) { + this.updateAlerts(); + this.alertTypesSet = true; + } + if (this.pager) { + assignSlotNames([this.pager, 'pager']); + } + } + + updated() { + this.updateAlerts(); + } + + render() { + return html` +
+
+ +
+
+ +
+
+ `; + } + + static get styles() { + return [baseStyles, styles]; + } +} + +registerElementSafely('cds-alert-group', CdsAlertGroup); + +declare global { + interface HTMLElementTagNameMap { + 'cds-alert-group': CdsAlertGroup; + } +} diff --git a/packages/core/src/alert/alert-group.stories.mdx b/packages/core/src/alert/alert-group.stories.mdx new file mode 100644 index 0000000000..5ea832e7d1 --- /dev/null +++ b/packages/core/src/alert/alert-group.stories.mdx @@ -0,0 +1,78 @@ +import { Meta, Props, Story, Preview, API } from '@storybook/addon-docs/blocks'; +import { html, LitElement } from 'lit-element'; + + + +# Alert Groups + + + This is an experimental API and likely will have breaking changes in the near future. + + +
+ +Alert groups are containers for a set of alerts. Alert groups can hold one or many alerts +inside of them with the expectation that all alerts will be of the same type. The exception +to this rule is the `loading` alert type, which will be displayed regardless of the type +of alert group containing it. + +```typescript +import '@clr/core/alert'; +``` + +```html + + + Single alert + + buttons, links + + + + Single Alert + + + + Another alert + + buttons, links + + + +``` + +## Alert Group + + + + + +## Lightweight Alert Group + + + + + +## Banner Alert Group + + + + + +## Banner Alert Group Statuses + + + + + +## Compact Alert Groups + + + + + +## Custom-Styled Alert Group + + + + diff --git a/packages/core/src/alert/alert-group.stories.ts b/packages/core/src/alert/alert-group.stories.ts new file mode 100644 index 0000000000..e9ae084dc1 --- /dev/null +++ b/packages/core/src/alert/alert-group.stories.ts @@ -0,0 +1,533 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ + +import '@clr/core/alert'; +import '@clr/core/button'; +import '@clr/core/internal-components'; +import { cssGroup, propertiesGroup, setStyles } from '@clr/core/internal'; +import { action } from '@storybook/addon-actions'; +import { color as colorKnob, select, text } from '@storybook/addon-knobs'; +import { html } from 'lit-html'; +import { ClarityIcons, headphonesIcon, nodeGroupIcon, timesCircleIcon } from '@clr/core/icon-shapes'; + +ClarityIcons.addIcons(headphonesIcon, nodeGroupIcon, timesCircleIcon); + +export default { + title: 'Experimental/Alert Group/Stories', + component: 'cds-alert-group', + parameters: { + options: { showPanel: true }, + design: { + type: 'figma', + url: 'https://www.figma.com/file/ZvaQGGktjGoW6gz9DqwvrLtz/Clarity-UI-Library---light?node-id=51%3A666', + }, + }, +}; + +export const API = () => { + const slot = text( + 'slot', + "Gathered by gravity a mote of dust suspended in a sunbeam venture with pretty stories for which there's little good.", + propertiesGroup + ); + const alertStatus = select( + 'status', + { + 'none (default info)': undefined, + info: 'info', + success: 'success', + warning: 'warning', + danger: 'danger', + loading: 'loading', + unknown: 'unknown', + }, + undefined, + propertiesGroup + ); + const size = select('size', { '(default)': 'default', sm: 'sm' }, undefined, propertiesGroup); + + const textColor = colorKnob('--color', undefined, cssGroup); + const iconColor = colorKnob('--icon-color', undefined, cssGroup); + const iconSize = text('--icon-size', undefined, cssGroup); + const fontWeight = text('--font-weight', undefined, cssGroup); + const letterSpacing = text('--letter-spacing', undefined, cssGroup); + const padding = text('--padding', undefined, cssGroup); + const background = colorKnob('--background', undefined, cssGroup); + const borderRadius = text('--border-radius', undefined, cssGroup); + const borderColor = colorKnob('--border-color', undefined, cssGroup); + const borderWidth = text('--border-width', undefined, cssGroup); + + return html` + + + + ${slot} + + Button 1 + Button 2 + + + + `; +}; + +export const alertGroup = () => { + return html` +
+ + + This example is a closable alert inside an alert group with a status of "info". + + + + This example is a closable alert with a custom icon shape inside an alert group with a status of "info". + + + This example is an alert with a "loading" status and alert action buttons inside an alert group with a status + of "info". + + Button 1 + Button 2 + + + + + + + This example is a closable alert inside an alert group with a status of "success". + + + This example is a closable alert with alert action buttons inside an alert group with a status of "success". + + Button 1 + Button 2 + + + + + + + This example is a closable alert inside an alert group with a status of "warning". + + + This example is a closable alert with alert action buttons inside an alert group with a status of "warning". + + Button 1 + Button 2 + + + + + + + This example is a closable alert inside an alert group with a status of "danger". + + + This example is a closable alert with alert action buttons inside an alert group with a status of "danger". + + Button 1 + Button 2 + + + + + + + This example is an alert inside an alert group. + + + This example is a closable alert inside an alert group. + + + + This example is an alert with alert action buttons and a custom icon shape inside an alert group. + + Button 1 + Button 2 + + + + This example is a closable alert with alert action buttons and multiple lines of text inside an alert group. A + block of lorem ipsum sample text follows: Drake Equation take root and flourish culture rings of Uranus quasar + hundreds of thousands? Cambrian explosion gathered by gravity of brilliant syntheses vanquish the impossible + finite but unbounded not a sunrise but a galaxyrise. Intelligent beings two ghostly white figures in coveralls + and helmets are soflty dancing something incredible is waiting to be known vanquish the impossible vastness is + bearable only through love concept of the number one and billions upon billions upon billions upon billions + upon billions upon billions upon billions. + + Button 1 + + + +
+ `; +}; + +export const bannerGroupStatus = () => { + return html` +
+ + + This example is a closable banner alert inside a banner alert group with a status of "info". + + + + + + This example is a closable alert with a status of "success" inside a banner alert group. + + + + + + This example is a closable alert with alert action buttons and a status of "warning" inside a banner alert + group. + + Button 1 + Button 2 + + + + + + + This example is a closable alert with a status of "danger" inside a banner alert group. + + Button 1 + Button 2 + + + + + + + This example is a non-closable alert with alert actions and a status of "unknown" inside a banner alert group. + + Button 1 + Button 2 + + + + + + + This example is an alert with alert actions and a status of "loading" inside a banner alert group. + + Button 1 + + + +
+ `; +}; + +export const bannerGroup = () => { + return html` +
+ + + + This example is a closable alert with a custom icon shape inside a banner alert group. + + + + + + + This example is a closable alert with alert action buttons, a custom icon, and multiple lines of text inside a + banner alert group. A block of lorem ipsum sample text follows: Drake Equation take root and flourish culture + rings of Uranus quasar hundreds of thousands? Cambrian explosion gathered by gravity of brilliant syntheses + vanquish the impossible finite but unbounded not a sunrise but a galaxyrise. Intelligent beings two ghostly + white figures in coveralls and helmets are soflty dancing something incredible is waiting to be known vanquish + the impossible vastness is bearable only through love concept of the number one and billions upon billions + upon billions upon billions upon billions upon billions upon billions. + + Button 1 + + + + + + + This example shows that a banner alert group should ignore compact sizing. + + Button 1 + Button 2 + + + +
+ `; +}; + +/* +export const bannerGroupWithPager = () => { + return html` +
+ +
Pager Here
+ + This example banner alert group with a pager. + + Button 1 + + +
+ + +
Pager Here
+ + This example banner alert group with a pager. + + Button 1 + + +
+ + +
Pager Here
+ + This example banner alert group with a pager. + + Button 1 + + +
+ + +
Pager Here
+ + This example banner alert group with a pager. + + Button 1 + + +
+ + +
Pager Here
+ + This example banner alert group with a pager. + + Button 1 + + +
+
+ `; +}; +*/ + +export const lightweightAlertGroup = () => { + return html` + + + This example is an alert with a status of "info" inside a lightweight alert group. + + + This example is an alert with a status of "danger" and inline action buttons inside a lightweight alert group. + Clickable Action + + + This example is an alert with a status of "loading" inside a lightweight alert group. + + Alert actions should not be viewable in lightweight alerts + Alert actions should not be viewable in lightweight alerts + + + + This example is a multi-line alert with a status of "unknown" inside a lightweight alert group. A block of lorem + ipsum sample text follows: Drake Equation take root and flourish culture rings of Uranus quasar hundreds of + thousands? Cambrian explosion gathered by gravity of brilliant syntheses vanquish the impossible finite but + unbounded not a sunrise but a galaxyrise. Intelligent beings two ghostly white figures in coveralls and helmets + are soflty dancing something incredible is waiting to be known vanquish the impossible vastness is bearable only + through love concept of the number one and billions upon billions upon billions upon billions upon billions upon + billions upon billions. + + Button 1 + + + + `; +}; + +export const compactAlertGroup = () => { + return html` +
+ + + This example is a closable alert inside a compact alert group with a status of "info". + + + + This example is a closable alert with alert actions and a custom icon shape inside a compact alert group with + a status of "info". + + Button 1 + Button 2 + + + + + + + This example is a closable alert inside a compact alert group with a status of "success". + + + This example is a closable alert with alert actions and a custom icon shape inside a compact alert group with + a status of "success". + + Button 1 + Button 2 + + + + + + + This example is a closable alert inside a compact alert group with a status of "warning". + + + + This example is a closable alert with alert actions and a custom icon shape inside a compact alert group with + a status of "warning". + + Button 1 + Button 2 + + + + + + + This example is a closable alert with a status of "loading" inside a compact alert group with a status of + "danger". + + + This example is a closable alert with alert actions inside a compact alert group with a status of "warning". + + Button 1 + Button 2 + + + + + + + This example is an alert inside a compact alert group. + + + This example is a closable alert inside a compact alert group. + + + This example is an alert with alert actions inside a compact alert group. + + Button 1 + Button 2 + + + + This example is a closable alert with multiple lines of text and an alert action inside a compact alert group. + A block of lorem ipsum sample text follows: Drake Equation take root and flourish culture rings of Uranus + quasar hundreds of thousands? Cambrian explosion gathered by gravity of brilliant syntheses vanquish the + impossible finite but unbounded not a sunrise but a galaxyrise. Intelligent beings two ghostly white figures + in coveralls and helmets are soflty dancing something incredible is waiting to be known vanquish the + impossible vastness is bearable only through love concept of the number one and billions upon billions upon + billions upon billions upon billions upon billions upon billions. + + Button 1 + + + + + + This example is an alert with a status of "info" inside a compact, lightweight alert group. + + + This example is an alert with a status of "danger" and an inline action inside a compact, lightweight alert + group.Clickable Action + + + This example is an alert with a status of "loading" inside a compact, lightweight alert group. + + Alert actions should not be viewable in lightweight alerts + Alert actions should not be viewable in lightweight alerts + + + + This example is a multi-line alert with a status of "unknown" and two inline actions inside a compact, + lightweight alert group. A block of lorem ipsum sample text follows: Drake Equation take root and flourish + culture rings of Uranus quasar hundreds of thousands? Cambrian explosion gathered by gravity of brilliant + syntheses vanquish the impossible finite but unbounded not a sunrise but a galaxyrise. Intelligent beings two + ghostly white figures in coveralls and helmets are soflty dancing something incredible is waiting to be known + vanquish the impossible vastness is bearable only through love concept of the number one and billions upon + billions upon billions upon billions upon billions upon billions upon billions.Clickable Action 1Clickable Action 2 + + Alert actions should not be viewable in lightweight alerts + + + +
+ `; +}; + +export const customStyles = () => { + return html` + + + + This example is an alert with a status of "info" inside a compact, lightweight alert group. + + + + This example is an alert with a status of "danger" and an inline action inside a compact, lightweight alert + group.Clickable Action + + + + `; +}; diff --git a/packages/core/src/alert/alert.base.ts b/packages/core/src/alert/alert.base.ts deleted file mode 100644 index 667bee5b86..0000000000 --- a/packages/core/src/alert/alert.base.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. - * This software is released under MIT license. - * The full license information can be found in LICENSE in the root directory of this project. - */ - -import { CdsButton } from '@clr/core/button'; -import '@clr/core/icon'; -import { - checkCircleIcon, - ClarityIcons, - exclamationCircleIcon, - exclamationTriangleIcon, - infoCircleIcon, - timesIcon, -} from '@clr/core/icon-shapes'; -import { - CommonStringsService, - event, - EventEmitter, - property, - querySlotAll, - returnOrFallthrough, -} from '@clr/core/internal'; -import { html, LitElement } from 'lit-element'; - -ClarityIcons.addIcons(checkCircleIcon, infoCircleIcon, exclamationCircleIcon, exclamationTriangleIcon, timesIcon); - -const iconMap = { - info: { - shape: ClarityIcons.getIconNameFromShape(infoCircleIcon), - title: CommonStringsService.keys.info, - }, - success: { - shape: ClarityIcons.getIconNameFromShape(checkCircleIcon), - title: CommonStringsService.keys.success, - }, - warning: { - shape: ClarityIcons.getIconNameFromShape(exclamationTriangleIcon), - title: CommonStringsService.keys.warning, - }, - danger: { - shape: ClarityIcons.getIconNameFromShape(exclamationCircleIcon), - title: CommonStringsService.keys.danger, - }, -}; - -/** - * Base class for alerts. Contains properties and functions common to all alerts. - */ -export class CdsBaseAlert extends LitElement { - @event() private closeChange: EventEmitter; - - /** If false, the alert will not render the close button. */ - @property({ type: Boolean }) - closable = true; - - /** Sets the color of the alert from a predefined list of statuses */ - @property({ type: String }) - status: 'info' | 'success' | 'warning' | 'danger'; - - /** Sets the icon shape to be used to override the default icon. The application must import the icon shape. */ - @property({ type: String }) - iconShape = ''; - - /** Sets the title attribute for the icon. This may need to be set especially if the component is using custom icon shape. */ - @property({ type: String }) - iconTitle = ''; - - @querySlotAll('cds-button') private buttons: NodeListOf; - - get alertIconShape() { - /* - * if the component's icon-shape attribute is set, that value is used. - * otherwise, we check for status attribute and set an icon shape that matches. - * if neither is supplied we default to info-circle icon. - */ - return returnOrFallthrough( - [ - [this.iconShape, () => this.iconShape], - [this.status, () => iconMap[this.status].shape], - ], - () => infoCircleIcon[0] - ); - } - - get alertIconTitle() { - /* - * if the component's icon-title attribute is set, that value is used. - * otherwise, we check for status attribute and set an icon shape that matches. - * if neither is supplied we default to string for "Info". - */ - return returnOrFallthrough( - [ - [this.iconTitle, () => this.iconTitle], - [this.status, () => (iconMap[this.status] ? iconMap[this.status].title : '')], - ], - () => CommonStringsService.keys.info - ); - } - - updateButtons() { - // buttons inside alert (usually app-level) have the style of small inverse buttons - this.buttons.forEach(button => { - button.status = 'inverse'; - button.size = 'sm'; - }); - } - - render() { - return html` -
-
-
- -
- - -
-
- ${this.closable - ? html`` - : html``} - `; - } - - closeAlert() { - this.closeChange.emit(true); - } -} diff --git a/packages/core/src/alert/alert.element.scss b/packages/core/src/alert/alert.element.scss index 7f46aa63ef..da934b8c8f 100644 --- a/packages/core/src/alert/alert.element.scss +++ b/packages/core/src/alert/alert.element.scss @@ -1,199 +1,146 @@ +@import './../styles/tokens/generated/index'; @import './../styles/mixins/utils'; +@import './../styles/mixins/mixins'; + +// TODO: progress spinner CSS deprecated, will be replaced by cds-circular-progress +@import './../../../angular/projects/clr-angular/src/progress/spinner/variables.spinner'; +@import './../../../angular/projects/clr-angular/src/image/icons.clarity'; +@import './../../../angular/projects/clr-angular/src/progress/spinner/spinner.clarity'; + +$lightweight-alert-line-height: $cds-token-typography-body-line-height-static; :host { - --color: var(--clr-color-neutral-700, #{$clr-color-neutral-700}); - --background: var(--clr-color-action-50, #{$clr-color-action-50}); - --border-radius: var(--clr-global-borderradius, #{$clr-global-borderradius}); - --border-color: var(--clr-color-action-400, #{$clr-color-action-400}); - --icon-color: var(--clr-color-action-600, #{$clr-color-action-600}); - --close-icon-color: var(--clr-color-neutral-700, #{$clr-color-neutral-700}); - --close-icon-color-hover: var(--clr-color-neutral-700, #{$clr-color-neutral-700}); - $alertIconWidth: $clr_baselineRem_1 + $clr_baselineRem_1px; - font-size: $clr-typography-smalltext; - letter-spacing: $clr-kerning-smalltext; - line-height: $clr_baselineRem_0_75; - position: relative; - box-sizing: border-box; - display: flex; - flex-direction: row; - border-radius: var(--border-radius); - background: var(--background); - border: $clr-global-borderwidth solid; - border-color: var(--border-color); - color: var(--color); + --container-padding: #{$cds-token-space-size-2} 0; + --min-height: #{$cds-token-space-size-9}; + width: 100%; +} - .alert-action:not(:last-child) { - margin-right: $clr_baselineRem_0_5; - } +.private-host { + min-height: var(--min-height); +} - .alert-action, - .dropdown-toggle { - text-decoration: underline; - } +:host([alert-group-type='default']) { + --font-size: #{$cds-token-typography-font-size-3}; +} - .alert-icon { - $alert-icon-dim: $clr_baselineRem_1; - @include equilateral($alert-icon-dim); - margin-left: -1 * $clr_baselineRem_0_125; - margin-top: -1 * $clr_baselineRem_4px; - --color: var(--icon-color); - } +:host([alert-group-type='default']) ::slotted(cds-alert-actions), +:host([alert-group-type='banner']) ::slotted(cds-alert-actions) { + --action-size: calc(var(--min-height) - #{$cds-token-space-size-4}); + white-space: nowrap; +} - .alert-icon-wrapper { - flex: 0 0 $alertIconWidth; - align-self: start; - padding-top: $clr_baselineRem_1px; - height: $clr_baselineRem_0_75; - } +:host([alert-group-type='default']) ::slotted(cds-alert-actions) { + height: calc(var(--min-height) - #{$cds-token-space-size-3}); +} - .alert-item { - flex: 1 1 auto; - display: flex; - flex-wrap: nowrap; - min-height: $clr_baselineRem_0_75; - margin-bottom: $clr_baselineRem_0_25; +:host([alert-group-type='banner']) ::slotted(cds-alert-actions) { + height: calc(var(--min-height) + #{$cds-token-space-size-3}); +} - &:last-child { - margin-bottom: 0; - } - } +:host([alert-group-type='banner']) { + --icon-size: #{$cds-token-space-size-9}; +} - .alert-wrapper { - $vertPadding: $clr_baselineRem_8px; - $horizPadding: $clr_baselineRem_0_5 - $clr-global-borderwidth; - flex: 1 1 auto; - flex-flow: column nowrap; - padding: $vertPadding $horizPadding; - display: flex; - } - //display: inline-block and max-width were specifically added for IE 10. - //Flexbox content wouldn't wrap otherwise :(. 98% was just an estimate to distance the text from the - //close alert button. - .alert-item > span, - .alert-text, - ::slotted(cds-alert-content) { - display: inline-block; //needed for IE11 - flex-grow: 1; - flex-shrink: 1; - flex-basis: 98%; //needed for IE11 - max-width: 98%; //needed for IE11 - margin-right: $clr_baselineRem_0_5; - text-align: left; - } +.alert-status-icon, +::slotted(cds-icon) { + @include equilateral(var(--icon-size, #{$cds-token-space-size-8-static})); + --color: var(--icon-color, #{$cds-token-color-neutral-700-static}); +} - button.close { - $closeBtnNudge: $clr_baselineRem_4px; - width: $clr_baselineRem_1; - background: transparent; - cursor: pointer; - display: block; - height: $clr_baselineRem_1_5; - flex: 0 0 ($clr_baselineRem_1 + $closeBtnNudge); - order: 100; - margin: 0; - padding: 0 $closeBtnNudge 0 0; - border: none; - - &:focus, - &:hover, - &:active { - background: none; - --close-icon-color: var(--close-icon-color-hover); - } - - cds-icon { - $alert-close-icon-dims: $clr_baselineRem_1 - $clr-global-borderwidth; - margin-top: -1 * $clr_baselineRem_0_125; - @include equilateral($alert-close-icon-dims); - fill: var(--close-icon-color); - } - } +.alert-close-wrapper, +.alert-actions-wrapper, +.alert-content-wrapper, +.alert-icon-wrapper { + display: flex; + min-height: #{$cds-token-space-size-6}; + padding: var(--container-padding); +} + +// default is for lightweight alerts +.alert-content-wrapper { + @include vertical-center-content; + transform: translateY(#{$cds-token-space-size-1}); +} - button.close ~ .alert-item > ::slotted(cds-alert-actions) { - padding-right: $clr_baselineRem_0_5; +.alert-content-wrapper { + color: var(--color, #{$cds-token-color-neutral-1000-static}); + font-size: var(--font-size, #{$cds-token-typography-body-font-size-static}); + font-weight: var(--font-weight, #{$cds-token-typography-body-font-weight-static}); + letter-spacing: var(--letter-spacing, #{$cds-token-typography-body-letter-spacing-static}); + line-height: $lightweight-alert-line-height; +} - & > .alert-action:last-child { - margin-right: $clr_baselineRem_0_5; - } - } +::slotted(cds-alert-actions) { + --action-text-color: #{$cds-token-color-neutral-700}; + --action-size: #{$lightweight-alert-line-height}; + display: none; +} + +:host([alert-group-type='default']) cds-internal-close-button { + --height: #{$cds-token-space-size-8}; +} + +:host([alert-group-type='default']) .alert-content-wrapper { + align-items: self-start; } :host([status='info']) { - --background: var(--clr-color-action-50, #{$clr-color-action-50}); - --border-color: var(--clr-color-action-400, #{$clr-color-action-400}); - --icon-color: var(--clr-color-action-600, #{$clr-color-action-600}); + --icon-color: #{$cds-token-color-action-600}; } :host([status='success']) { - --background: var(--clr-color-success-50, #{$clr-color-success-50}); - --border-color: var(--clr-color-success-400, #{$clr-color-success-400}); - --icon-color: var(--clr-color-success-700, #{$clr-color-success-700}); + --icon-color: #{$cds-token-color-success-700}; } :host([status='warning']) { - --background: var(--clr-color-warning-100, #{$clr-color-warning-100}); - --border-color: var(--clr-color-warning-400, #{$clr-color-warning-400}); - --icon-color: var(--clr-color-neutral-700, #{$clr-color-warning-700}); + --icon-color: #{$cds-token-color-warning-800}; } :host([status='danger']) { - --background: var(--clr-color-danger-100, #{$clr-color-danger-100}); - --border-color: var(--clr-color-danger-200, #{$clr-color-danger-200}); - --icon-color: var(--clr-color-danger-800, #{$clr-color-danger-800}); -} - -:host([size='sm']) { - $alertSmallLineHeight: $clr_baselineRem_0_667; - $alertSmallNudge: $clr_baselineRem_4px; - // need to use 11 instead of 12 here or else vertical alignment is thrown off - // by a pixel due to improper browser rounding; browsers aren't rendering on the - // sub-pixel required but rounding up to the nearest whole pixel... - font-size: $clr_baselineRem_0_5 - $clr_baselineRem_1px; - letter-spacing: normal; - line-height: $alertSmallLineHeight; - - .alert-wrapper { - padding: ($alertSmallNudge - $clr-global-borderwidth) ($clr_baselineRem_0_25 - $clr-global-borderwidth); - } + --icon-color: #{$cds-token-color-danger-700}; +} - .alert-item { - padding-top: $clr_baselineRem_1px; - margin-bottom: $alertSmallNudge; +:host([size='sm']:not([alert-group-type='banner'])) { + --font-size: #{$cds-token-typography-font-size-2}; + --letter-spacing: normal; +} - &:last-child { - margin-bottom: 0; - } - } +// banner styles - .alert-icon-wrapper { - padding-top: 0; - height: $alertSmallLineHeight; - } +:host([alert-group-type='banner']) .alert-content-wrapper { + transform: none; + overflow: hidden; + height: #{$cds-token-space-size-10}; +} - .alert-icon { - margin-left: -1 * $alertSmallNudge; - margin-top: -1 * $alertSmallNudge; - } +:host([alert-group-type='banner']) .alert-content { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} - .alert-item > span, - .alert-text, - ::slotted(cds-alert-content) { - margin-right: $clr_baselineRem_0_25; - } +:host([alert-group-type='banner']) .alert-icon-wrapper { + transform: translateY(#{$cds-token-space-size-2}); +} + +:host([alert-group-type='banner']) .spinner { + @include min-equilateral(calc(#{$cds-token-space-size-8} + #{$cds-token-space-size-2})); + margin-top: #{$cds-token-space-size-2}; +} + +:host([alert-group-type='banner']) cds-internal-close-button { + --color: var(--icon-color); + --height: calc(#{$cds-token-space-size-10} - #{$cds-token-space-size-2} - #{$cds-token-space-size-2}); +} + +:host([alert-group-type='banner']) ::slotted(cds-alert-actions) { + transform: translateY(#{$cds-token-space-size-2}); +} - button.close { - padding-right: 0; - flex: 0 0 $clr_baselineRem_1; - height: $clr_baselineRem_1; - line-height: $clr_baselineRem_1; - - cds-icon { - $smallAlertCloseIconDims: $clr_baselineRem_0_83; - margin-top: -1 * ($alertSmallNudge + $clr_baselineRem_1px); - margin-right: -1 * $clr_baselineRem_1px; - @include equilateral($smallAlertCloseIconDims); - line-height: $smallAlertCloseIconDims + $clr_baselineRem_1px; - } +@supports (-moz-appearance: none) and (text-emphasis: none) { + // nudge for alert text content firefox + :host(:not([alert-group-type='banner']):not([size='sm'])) .alert-content { + transform: translateY(calc(-1 * #{$cds-token-space-size-2})); } } diff --git a/packages/core/src/alert/alert.element.spec.ts b/packages/core/src/alert/alert.element.spec.ts index fb157fac49..28b79869dd 100644 --- a/packages/core/src/alert/alert.element.spec.ts +++ b/packages/core/src/alert/alert.element.spec.ts @@ -3,10 +3,10 @@ * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ -import { CdsAlert } from '@clr/core/alert'; +import { CdsAlert, getIconStatusTuple, iconShapeIsAlertStatusType, iconTitleIsAlertStatusLabel } from '@clr/core/alert'; import '@clr/core/alert'; -import { CdsIcon } from '@clr/core/icon-shapes'; -import { CommonStringsService, CommonStringsServiceInternal } from '@clr/core/internal'; +import { CdsIcon, infoStandardIcon } from '@clr/core/icon-shapes'; +import { CommonStringsService } from '@clr/core/internal'; import { componentIsStable, createTestElement, @@ -14,21 +14,19 @@ import { removeTestElement, waitForComponent, } from '@clr/core/test/utils'; +import { CdsCloseButton } from '../internal-components'; -describe('alert element', () => { +describe('Alert element – ', () => { let testElement: HTMLElement; let component: CdsAlert; const placeholderText = 'I am a default alert with no attributes.'; - const placeholderActionsText = 'This is where action elements go.'; + const alertStatusIconSelector = '.alert-status-icon'; - describe('Basic alert behaviors', () => { + describe('Lightweight alerts: ', () => { beforeEach(async () => { testElement = createTestElement(); testElement.innerHTML = ` - - ${placeholderText} - ${placeholderActionsText} - + ${placeholderText} `; await waitForComponent('cds-alert'); @@ -39,141 +37,442 @@ describe('alert element', () => { removeTestElement(testElement); }); - it('should create the component', async () => { + it('should slot the component', async () => { await componentIsStable(component); const slots = getComponentSlotContent(component); - expect(slots.default).toBe(`${placeholderText}`); - expect(slots.actions).toBe(`${placeholderActionsText}`); + expect(slots.default).toBe(`${placeholderText}`); }); - it('should support closable option', async () => { + it('should not show alert-actions', async () => { await componentIsStable(component); - expect(component.closable).toBe(true); - expect(component.hasAttribute('closable')).toBe(true); - let button = component.shadowRoot.querySelector('button.close'); - expect(button).not.toBeNull(); - expect(button.classList.contains('close')).toBe(true); + expect(component.querySelectorAll('.alert-actions-wrapper').length).toBe(0); + }); - component.closable = false; + it('should not be closable', async () => { await componentIsStable(component); expect(component.hasAttribute('closable')).toBe(false); - button = component.shadowRoot.querySelector('button.close'); - expect(button).toBeNull(); + component.closable = true; + await componentIsStable(component); + expect(component.querySelectorAll('cds-internal-close-button').length).toBe(0); }); + }); + + describe('custom icons: ', () => { + let customComponent: CdsAlert; + + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` + ${placeholderText} + ${placeholderText} + `; - it('should set icon shape based on status', async () => { + await waitForComponent('cds-alert'); + component = testElement.querySelector('#defaultAlert'); + customComponent = testElement.querySelector('#customAlert'); + }); + + afterEach(() => { + removeTestElement(testElement); + }); + + it('should default to an show "info" icon if not defined', async () => { + const iconName = infoStandardIcon[0]; await componentIsStable(component); - let icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); + const alertSlotContent = getComponentSlotContent(component); + const alertStatusIcon = component.shadowRoot.querySelector(alertStatusIconSelector); + expect(alertSlotContent['alert-icon']).toBe(''); + expect(alertStatusIcon.getAttribute('shape')).toBe(iconName); + }); - expect(icon.hasAttribute('shape')).toBe(true); - expect(icon.getAttribute('shape')).toEqual('info-circle'); + it('should be show "info" icon if given a weird status and no custom icon shape', async () => { + const iconName = infoStandardIcon[0]; + await componentIsStable(component); + component.setAttribute('status', 'jabberwocky'); + await componentIsStable(component); + const alertSlotContent = getComponentSlotContent(component); + const alertStatusIcon = component.shadowRoot.querySelector(alertStatusIconSelector); + expect(alertSlotContent['alert-icon']).toBe(''); + expect(alertStatusIcon.getAttribute('shape')).toBe(iconName); + }); - component.status = 'success'; + it('should default to the status icon shape if status is set and custom shape is not defined', async () => { await componentIsStable(component); - icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - expect(icon.hasAttribute('shape')).toBe(true); - expect(icon.getAttribute('shape')).toEqual('check-circle'); + const alertSlotContent = getComponentSlotContent(component); + const alertStatusIcon = component.shadowRoot.querySelector(alertStatusIconSelector); + expect(alertSlotContent['alert-icon']).toBe(''); + expect(alertStatusIcon).not.toBeNull(); + expect(alertStatusIcon.hasAttribute('shape')).toBe(true); + expect(alertStatusIcon.getAttribute('shape')).toEqual('info-standard'); - component.status = 'warning'; + component.setAttribute('status', 'warning'); await componentIsStable(component); - icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - expect(icon.hasAttribute('shape')).toBe(true); - expect(icon.getAttribute('shape')).toEqual('exclamation-triangle'); + expect(component.status).toBe('warning'); + expect(alertStatusIcon.getAttribute('shape')).toEqual('warning-standard'); + + await componentIsStable(customComponent); + customComponent.setAttribute('status', 'danger'); + await componentIsStable(customComponent); + const customAlertSlotContent = getComponentSlotContent(customComponent); + expect(customComponent.status).toBe('danger'); + expect(customAlertSlotContent['alert-icon'].includes('cds-icon')).toBe(true); + expect(customAlertSlotContent['alert-icon'].includes('shape="error-standard"')).toBe(false); + expect(customAlertSlotContent['alert-icon'].includes('shape="ohai"')).toBe(true); + }); - component.status = 'danger'; + it('should support a custom icon shape', async () => { + await componentIsStable(customComponent); + const customAlertSlotContent = getComponentSlotContent(customComponent); + expect(customAlertSlotContent['alert-icon'].includes('cds-icon')).toBe(true); + expect(customAlertSlotContent['alert-icon'].includes('shape="ohai"')).toBe(true); + }); + }); + + describe('status: ', () => { + let customComponent: CdsAlert; + + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` + ${placeholderText} + ${placeholderText} + `; + + await waitForComponent('cds-alert'); + component = testElement.querySelector('#defaultAlert'); + customComponent = testElement.querySelector('#customAlert'); + }); + + afterEach(() => { + removeTestElement(testElement); + }); + + it('should default to an "info" icon if status is not defined', async () => { await componentIsStable(component); - icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - expect(icon.hasAttribute('shape')).toBe(true); - expect(icon.getAttribute('shape')).toEqual('exclamation-circle'); + const iconName = infoStandardIcon[0]; + const alertStatusIcon = component.shadowRoot.querySelector(alertStatusIconSelector); + expect(component.status).toBeUndefined('verify that status is not defined'); + expect(alertStatusIcon.getAttribute('shape')).toBe(iconName); + expect(alertStatusIcon.getAttribute('shape')).toBe(iconName); + expect( + alertStatusIcon.shadowRoot + .querySelector('[cds-layout="display:screen-reader-only"]') + .innerHTML.includes(CommonStringsService.keys.info) + ).toBe(true); }); - it('should support custom icon shape', async () => { + it('should allow users to change statuses', async () => { await componentIsStable(component); - let icon = component.shadowRoot.querySelector('cds-icon'); - component.iconShape = 'exclamation-triangle'; + expect(component.status).toBeUndefined('status is undefined to start out'); + expect(component.shadowRoot.querySelector(alertStatusIconSelector).getAttribute('shape')).toBe( + infoStandardIcon[0] + ); + expect(component.shadowRoot.querySelector(alertStatusIconSelector).getAttribute('title')).toBe( + CommonStringsService.keys.info + ); + component.setAttribute('status', 'warning'); await componentIsStable(component); - icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - expect(icon.hasAttribute('shape')).toBe(true); - expect(icon.getAttribute('shape')).toEqual('exclamation-triangle'); + expect(component.status).toBe('warning'); + expect(component.shadowRoot.querySelector(alertStatusIconSelector).getAttribute('shape')).toBe( + 'warning-standard' + ); + expect(component.shadowRoot.querySelector(alertStatusIconSelector).getAttribute('title')).toBe( + CommonStringsService.keys.warning + ); }); - it('should set icon title based on status', async () => { - await componentIsStable(component); - let icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); + it('should show the spinner if set to loading status', async () => { + await componentIsStable(customComponent); + customComponent.setAttribute('status', 'loading'); + await componentIsStable(customComponent); + expect(customComponent.status).toBe('loading'); + expect(customComponent.shadowRoot.querySelector(alertStatusIconSelector)).toBeNull(); + expect(customComponent.shadowRoot.querySelector('cds-icon')).toBeNull(); + expect(customComponent.shadowRoot.querySelector('.spinner')).not.toBeNull(); + const customAlertSlotContent = getComponentSlotContent(customComponent); + expect(customAlertSlotContent['alert-icon']).toBeUndefined(); + }); + }); - expect(icon.hasAttribute('title')).toBe(true); - expect(icon.getAttribute('title')).toEqual(CommonStringsService.keys.info); + describe('Boxed (default type pastel) alerts: ', () => { + let testElement: HTMLElement; + let component: CdsAlert; + const placeholderText = 'I am a default alert with no attributes.'; + const placeholderActionsText = 'This is where action elements go.'; - component.status = 'success'; - await componentIsStable(component); - icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - expect(icon).toBeDefined(); - expect(icon.hasAttribute('title')).toBe(true); - expect(icon.getAttribute('title')).toEqual(CommonStringsService.keys.success); + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` + + ${placeholderText} + ${placeholderActionsText} + + `; + + await waitForComponent('cds-alert'); + component = testElement.querySelector('cds-alert'); + }); - component.status = 'warning'; + afterEach(() => { + removeTestElement(testElement); + }); + + it('should slot the component', async () => { await componentIsStable(component); - icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - expect(icon).toBeDefined(); - expect(icon.hasAttribute('title')).toBe(true); - expect(icon.getAttribute('title')).toEqual(CommonStringsService.keys.warning); + const slots = getComponentSlotContent(component); + expect(slots.default.trim()).toBe(`${placeholderText}`); + }); - component.status = 'danger'; + it('should show alert actions', async () => { await componentIsStable(component); - icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - expect(icon).toBeDefined(); - expect(icon.hasAttribute('title')).toBe(true); - expect(icon.getAttribute('title')).toEqual(CommonStringsService.keys.danger); + const slots = getComponentSlotContent(component); + expect(slots.actions.trim()).toBe( + `${placeholderActionsText}` + ); + }); + }); + + describe('Banner alerts: ', () => { + let testElement: HTMLElement; + let component: CdsAlert; + const placeholderText = 'I am a default alert with no attributes.'; + + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` + ${placeholderText} + `; + + await waitForComponent('cds-alert'); + component = testElement.querySelector('cds-alert'); + }); + + afterEach(() => { + removeTestElement(testElement); }); - it('should support custom icon title', async () => { + it('should slot the component', async () => { await componentIsStable(component); - let icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); + const slots = getComponentSlotContent(component); + expect(slots.default.trim()).toBe(`${placeholderText}`); + }); - expect(icon.hasAttribute('shape')).toBe(true); - expect(icon.getAttribute('shape')).toEqual('info-circle'); + it('close-button should be size "20"', async () => { + await componentIsStable(component); + const closeBtn = component.shadowRoot.querySelector('cds-internal-close-button'); + expect(!!closeBtn).not.toBe(false, 'close-button should exist'); + expect(closeBtn.iconSize).toBe('20', 'close-button icon size should be 20'); + }); - component.iconTitle = 'my-icon-title'; + it('should show neutral spinner if set to loading status', async () => { await componentIsStable(component); - icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - expect(icon).toBeDefined(); - expect(icon.hasAttribute('title')).toBe(true); - expect(icon.getAttribute('title')).toEqual('my-icon-title'); + component.setAttribute('status', 'loading'); + await componentIsStable(component); + expect(component.status).toBe('loading'); + expect(component.shadowRoot.querySelector(alertStatusIconSelector)).toBeNull(); + expect(component.shadowRoot.querySelector('cds-icon')).toBeNull(); + const spinner = component.shadowRoot.querySelector('.spinner'); + expect(spinner).not.toBeNull('loading status should show spinner'); + expect(spinner.classList.contains('spinner-neutral-0')).toBe( + true, + 'banner loading spinner should be white/neutral' + ); + const customAlertSlotContent = getComponentSlotContent(component); + expect(customAlertSlotContent['alert-icon']).toBeUndefined(); + }); + }); + + describe('Alert – close button: ', () => { + let testElement: HTMLElement; + let component: CdsAlert; + const placeholderText = 'I am a default alert with no attributes.'; + const placeholderActionsText = 'This is where action elements go.'; + + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` + + ${placeholderText} + ${placeholderActionsText} + + `; + + await waitForComponent('cds-alert'); + component = testElement.querySelector('cds-alert'); }); - it('should set close button aria label using Common Strings Service', async () => { - const service = new CommonStringsServiceInternal(); + afterEach(() => { + removeTestElement(testElement); + }); + it('should show the close button', async () => { + await componentIsStable(component); + component.setAttribute('closable', 'true'); await componentIsStable(component); - const button = component.shadowRoot.querySelector('button.close'); - expect(button).not.toBeNull(); - expect(button.hasAttribute('aria-label')).toBe(true); - expect(button.getAttribute('aria-label')).toEqual(service.keys.alertCloseButtonAriaLabel); + expect(component.shadowRoot.querySelectorAll('cds-internal-close-button').length).not.toBe(0); }); - it('should emit a closeChange event when close button is clicked', async done => { + it('should set the close button title', async () => { + const expectedLabel = 'ohai'; + await componentIsStable(component); + component.setAttribute('closable', 'true'); + component.setAttribute('close-icon-title', expectedLabel); + await componentIsStable(component); + const closeBtn = component.shadowRoot.querySelector('cds-internal-close-button'); + expect(closeBtn).not.toBe(null); + expect(closeBtn.getAttribute('aria-label')).toBe(expectedLabel); + }); + + it('should emit a closeChanged event when close button is clicked', async done => { let value: any; await componentIsStable(component); + + component.setAttribute('closable', 'true'); + await componentIsStable(component); + component.addEventListener('closeChange', (e: CustomEvent) => { value = e.detail; expect(value).toBe(true); done(); }); - const button = component.shadowRoot.querySelector('button.close'); + const button = component.shadowRoot + .querySelector('cds-internal-close-button') + .querySelector('button'); expect(button).toBeDefined(); button.click(); }); + + it('sets 16 as the default icon size', async () => { + await componentIsStable(component); + + component.setAttribute('closable', 'true'); + await componentIsStable(component); + + const icon = component.shadowRoot + .querySelector('cds-internal-close-button') + .shadowRoot.querySelector('cds-icon'); + expect(icon).not.toBeNull(); + expect(icon.hasAttribute('size')).toBe(true); + expect(icon.getAttribute('size')).toBe('16'); + }); + }); + + describe('Aria: ', () => { + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` + ${placeholderText} + `; + + await waitForComponent('cds-alert'); + component = testElement.querySelector('cds-alert'); + }); + + afterEach(() => { + removeTestElement(testElement); + }); + + it('should set up "aria-describedby" as expected', async () => { + const ariaAttr = 'aria-describedby'; + await componentIsStable(component); + expect(component.hasAttribute(ariaAttr)).toBe(true, 'alert element has ' + ariaAttr); + const describedById = component.getAttribute(ariaAttr); + const contentWrapper = component.shadowRoot.querySelector('.alert-content'); + expect(component.shadowRoot.querySelector('#' + describedById)).not.toBeNull( + ariaAttr + ' id element should exist' + ); + expect(contentWrapper.getAttribute('id')).toBe(describedById, ariaAttr + ' id should be on the content wrapper'); + }); + + it('should set role to "region" as/when expected', async () => { + await componentIsStable(component); + expect(component.hasAttribute('role')).toBe(true, 'role exists on alert element'); + expect(component.getAttribute('role')).toBe('region', 'role is set to "region" on alert element'); + }); + + it('should use "aria-hidden" on the alert icon', async () => { + await componentIsStable(component); + let alertIcon = component.shadowRoot.querySelector('cds-icon.alert-status-icon'); + expect(alertIcon.getAttribute('aria-hidden')).toBe('true', 'default alert has aria-hidden true'); + + component.setAttribute('status', 'danger'); + await componentIsStable(component); + expect(alertIcon.getAttribute('shape')).toBe('error-standard', 'verify status has changed'); + expect(alertIcon.getAttribute('aria-hidden')).toBe( + 'true', + 'icon shapes have aria-hidden true after status change' + ); + + component.setAttribute('status', 'loading'); + await componentIsStable(component); + alertIcon = component.shadowRoot.querySelector('.spinner-inline'); + expect(component.shadowRoot.querySelector('cds-icon.alert-icon')).toBeNull('verify loading status pt. 1'); + expect(alertIcon).not.toBeNull('verify loading status pt. 2'); + expect(alertIcon.getAttribute('aria-hidden')).toBe( + 'true', + 'loading spinner has aria-hidden true after status changed to loading' + ); + }); + }); +}); + +describe('getIconStatusTuple: ', () => { + it('should return "info" as the default', async () => { + const [shapeName, title] = getIconStatusTuple('hippogriff'); + expect(shapeName).toBe(infoStandardIcon[0]); + expect(title).toBe(CommonStringsService.keys.info); + }); + + it('should return statuses as expected', async () => { + let [shapeName, title] = getIconStatusTuple('info'); + expect(shapeName).toBe(infoStandardIcon[0]); + expect(title).toBe(CommonStringsService.keys.info); + [shapeName, title] = getIconStatusTuple('success'); + expect(shapeName).toBe('success-standard'); + expect(title).toBe(CommonStringsService.keys.success); + [shapeName, title] = getIconStatusTuple('warning'); + expect(shapeName).toBe('warning-standard'); + expect(title).toBe(CommonStringsService.keys.warning); + [shapeName, title] = getIconStatusTuple('danger'); + expect(shapeName).toBe('error-standard'); + expect(title).toBe(CommonStringsService.keys.danger); + [shapeName, title] = getIconStatusTuple('unknown'); + expect(shapeName).toBe('help'); + expect(title).toBe(CommonStringsService.keys.info); + [shapeName, title] = getIconStatusTuple('loading'); + expect(shapeName).toBe('loading'); + expect(title).toBe(CommonStringsService.keys.loading); + }); +}); + +describe('iconShapeIsAlertStatusType: ', () => { + it('should return false as expected', async () => { + expect(iconShapeIsAlertStatusType('')).toBe(false); + expect(iconShapeIsAlertStatusType('jabberwocky')).toBe(false); + expect(iconShapeIsAlertStatusType(null)).toBe(false); + }); + + it('should return true as expected', async () => { + expect(iconShapeIsAlertStatusType('error-standard')).toBe(true); + expect(iconShapeIsAlertStatusType('warning-standard')).toBe(true); + expect(iconShapeIsAlertStatusType('info-standard')).toBe(true); + expect(iconShapeIsAlertStatusType('success-standard')).toBe(true); + expect(iconShapeIsAlertStatusType('help')).toBe(true); + }); +}); + +describe('iconTitleIsAlertStatusLabel: ', () => { + const commonstrings = CommonStringsService.keys; + + it('should return false as expected', async () => { + expect(iconTitleIsAlertStatusLabel('chuul')).toBe(false); + }); + + it('should return true as expected', async () => { + expect(iconTitleIsAlertStatusLabel(commonstrings.info)).toBe(true, 'info returns true'); + expect(iconTitleIsAlertStatusLabel(commonstrings.success)).toBe(true, 'success returns true'); + expect(iconTitleIsAlertStatusLabel(commonstrings.warning)).toBe(true, 'warning returns true'); + expect(iconTitleIsAlertStatusLabel(commonstrings.danger)).toBe(true, 'danger returns true'); }); }); diff --git a/packages/core/src/alert/alert.element.ts b/packages/core/src/alert/alert.element.ts index 49240d9173..9dbf079b1f 100644 --- a/packages/core/src/alert/alert.element.ts +++ b/packages/core/src/alert/alert.element.ts @@ -4,27 +4,151 @@ * The full license information can be found in LICENSE in the root directory of this project. */ -import { baseStyles, property, registerElementSafely } from '@clr/core/internal'; -import { CdsBaseAlert } from './alert.base.js'; +import '@clr/core/icon'; +import '@clr/core/internal-components'; +import { + applyMixins, + assignSlotNames, + baseStyles, + CommonStringsService, + event, + EventEmitter, + property, + querySlot, + querySlotAll, + registerElementSafely, + UniqueId, + setAttributes, +} from '@clr/core/internal'; +import { + CdsIcon, + ClarityIcons, + timesIcon, + infoStandardIcon, + successStandardIcon, + warningStandardIcon, + errorStandardIcon, + helpIcon, +} from '@clr/core/icon-shapes'; +import { AlertGroupTypes, AlertStatusTypes, CdsAlertActions } from '@clr/core/alert'; import { styles } from './alert.element.css.js'; +import { html, LitElement } from 'lit-element'; +import { CdsAlertGroup } from './alert-group.element.js'; + +ClarityIcons.addIcons( + infoStandardIcon, + errorStandardIcon, + successStandardIcon, + warningStandardIcon, + timesIcon, + helpIcon +); + +export function getIconStatusTuple(status: string): [string, string] { + const commonstrings = CommonStringsService.keys; + + const statusIcons: { [key: string]: [string, string] } = { + info: [ClarityIcons.getIconNameFromShape(infoStandardIcon), commonstrings.info], + success: [ClarityIcons.getIconNameFromShape(successStandardIcon), commonstrings.success], + warning: [ClarityIcons.getIconNameFromShape(warningStandardIcon), commonstrings.warning], + danger: [ClarityIcons.getIconNameFromShape(errorStandardIcon), commonstrings.danger], + unknown: [ClarityIcons.getIconNameFromShape(helpIcon), commonstrings.info], + loading: ['loading', commonstrings.loading], + }; + + return statusIcons[status] ? statusIcons[status] : statusIcons.info; +} + +export function iconShapeIsAlertStatusType(shape: string): boolean { + const statusShapes = ['info', 'success', 'warning', 'danger', 'unknown'].map(s => { + return getIconStatusShape(s); + }); + return statusShapes.indexOf(shape) > -1; +} + +export function iconTitleIsAlertStatusLabel(shape: string): boolean { + const statusLabels = ['info', 'success', 'warning', 'danger', 'unknown'].map(s => { + return getIconStatusLabel(s); + }); + return statusLabels.indexOf(shape) > -1; +} + +export function getIconStatusShape(status: string): string { + return getIconStatusTuple(status)[0]; +} + +export function getIconStatusLabel(status: string): string { + return getIconStatusTuple(status)[1]; +} + +export function getAlertContentLayout( + containerType: 'wrapper' | 'content' | 'actions', + alertGroupType: AlertGroupTypes, + alertGroupHasPager: boolean +) { + const fillLayoutValue = 'align:stretch'; + + switch (alertGroupType) { + case 'light': + return ''; + case 'banner': + switch (containerType) { + case 'wrapper': + return alertGroupHasPager ? fillLayoutValue : ''; + case 'content': + return ''; + case 'actions': + return alertGroupHasPager ? fillLayoutValue : ''; + default: + return ''; + } + case 'default': + default: + switch (containerType) { + case 'wrapper': + return fillLayoutValue; + case 'content': + return fillLayoutValue; + case 'actions': + return ''; + } + } +} + +class AlertMixinClass extends LitElement {} + +applyMixins(AlertMixinClass, [UniqueId]); /** * Alerts are banners that communicate a message with a severity attached to it. * They grab the user’s attention to provide critical information needed in context. * + * Alerts outside of a cds-alert-group or cds-app-alert-group component will be + * displayed as a "lightweight alert". Lightweight alerts, by default, provide no + * close button component and they inherit no status (a.k.a. success, danger, etc.). + * + * Alerts inside a cds-alert-group component inherit their status from the containing + * alert group. + * + * Alerts inside a cds-app-alert-group component inherit their status as a default from + * the containing app-alert group, although it can be overridden on individual alerts. + * * ```typescript * import '@clr/core/alert'; * ``` * * ```html - * - * This is an alert. - * + * + * Single Alert + * + * buttons, links + * + * * ``` * * @beta * @element cds-alert - * @slot default - Content slot for inside the alert. Usually will contain at least a component. + * @slot default - Content slot for inside the alert * @cssprop --color * @cssprop --background * @cssprop --border-radius @@ -33,16 +157,177 @@ import { styles } from './alert.element.css.js'; * @cssprop --close-icon-color * @cssprop --close-icon-color-hover */ -export class CdsAlert extends CdsBaseAlert { +export class CdsAlert extends AlertMixinClass { + @event() private closeChange: EventEmitter; + /** Sets the overall height and width of the alert and icon based on value */ @property({ type: String }) size: 'default' | 'sm'; + /** + * Prevents the alert group from setting types and statuses on the alert children + * before they are done setting their own properties + * Internal Use Only + * @private + */ + isInitted = false; + + /** + * Sets up the buttons, layouts, close-button and other properties based on the alert group container + * Internal Use Only + * @private + */ + @property({ type: String }) + alertGroupType: AlertGroupTypes; + + private get shouldShowCloseButton(): boolean { + return this.alertGroupType !== 'light' && this.closable; + } + + private updateIcons() { + this.alertIcons.forEach(i => { + assignSlotNames([i, 'alert-icon']); + }); + } + + private updateActions(newAlertGroupType: AlertGroupTypes) { + this.alertActions.forEach(actns => { + actns.type = newAlertGroupType; + }); + } + + private updateCloseButton() { + if (this.shouldShowCloseButton) { + assignSlotNames([this.closeButton, 'close-button']); + } + } + + private updateSlots(newAlertGroupType: AlertGroupTypes) { + this.updateIcons(); + this.updateActions(newAlertGroupType); + this.updateCloseButton(); + } + + private idForAriaDescriber = 'aria-' + this._idPrefix + this._uniqueId; + + /** + * If false, the alert will not render the close button. + * + * Lightweight alerts do not display close buttons + */ + @property({ type: Boolean }) + closable = false; + + /** Sets the color of the alert from a predefined list of statuses */ + @property({ type: String }) + status: AlertStatusTypes | ''; + + @property({ type: String }) + closeIconTitle = CommonStringsService.keys.alertCloseButtonAriaLabel; + + @querySlotAll('cds-alert-actions') private alertActions: NodeListOf; + + @querySlotAll('cds-icon') private alertIcons: NodeListOf; + + @querySlot('cds-internal-close-button') private closeButton: HTMLElement; + + connectedCallback() { + super.connectedCallback(); + this.updateSlots(this.alertGroupType); + this.isInitted = true; + setAttributes(this, ['aria-describedby', this.idForAriaDescriber], ['role', 'region']); + } + + updated() { + this.updateSlots(this.alertGroupType); + + if (this.alertGroupType === 'banner') { + (this.parentElement as CdsAlertGroup).updateBannerAlertGroupStatus(); + } + } + + private get parentGroupHasPager(): boolean { + return this.alertGroupType === 'banner' && !!(this.parentElement as CdsAlertGroup).pager; + } + + render() { + return html` +
+ ${this.alertGroupType === 'banner' && !this.parentGroupHasPager + ? html` ` + : html``} + + + + + ${this.alertGroupType === 'light' ? html`` : html``} + + ${this.alertGroupType === 'light' + ? html`` + : html``} + + + ${this.alertGroupType === 'banner' && !this.parentGroupHasPager + ? html` ` + : html``} + ${this.alertGroupType !== 'light' && this.closable + ? html`` + : html``} +
+ `; + } + + private closeAlert() { + this.closeChange.emit(true); + } + static get styles() { return [baseStyles, styles]; } } +export interface CdsAlert extends AlertMixinClass, UniqueId {} + registerElementSafely('cds-alert', CdsAlert); declare global { diff --git a/packages/core/src/alert/alert.interfaces.ts b/packages/core/src/alert/alert.interfaces.ts new file mode 100644 index 0000000000..15408c48e7 --- /dev/null +++ b/packages/core/src/alert/alert.interfaces.ts @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ + +export type AlertStatusTypes = 'info' | 'success' | 'warning' | 'danger' | 'unknown' | 'loading'; + +export type AlertGroupTypes = 'default' | 'banner' | 'light'; diff --git a/packages/core/src/alert/alert.stories.mdx b/packages/core/src/alert/alert.stories.mdx index b9d49321d2..30f52ecf37 100644 --- a/packages/core/src/alert/alert.stories.mdx +++ b/packages/core/src/alert/alert.stories.mdx @@ -1,12 +1,26 @@ import { Meta, Props, Story, Preview, API } from '@storybook/addon-docs/blocks'; import { html, LitElement } from 'lit-element'; - + # Alert -Alerts are banners that communicate a message with a severity attached to it. -They grab the user’s attention to provide critical information needed in context. + + This is an experimental API and likely will have breaking changes in the near future. + + +
+ +Alerts are banners that communicate a message with a severity attached to it. They grab the user’s attention to provide critical information needed in context. + +By default, alerts outside of an alert group have a lightweight look and functionality. This means that: + +- `cds-alert-actions` child components will not be displayed inside a lightweight alert +- lightweight alerts cannot display a close button – they are always considered to be `closable = false` + +Lightweight alerts can be placed inside containers such cards and layouts. But if the alerts are dynamic, their container will need to have an `aria-live` attribute set on them so that screen-readers will announce content changes within the container. + +Making a lightweight alert dismissible or "closable" will require the use of a `cds-inline-button` and an action on the product side that determines whether or not the alert should be removed from view. ## Installation @@ -17,29 +31,33 @@ import '@clr/core/alert'; ``` ```html - - This is an alert. + + + This is an alert. It will display a decorative "info" icon by default. You should set the aria-live attribute on the + alert to a level that aligns with the severity of the alert's status. ``` -## Status +## Alerts - + -## Sizes +## Alert statuses - + -## Custom Styles +## Compact alerts - + -## API +## Custom alert styles - + + + diff --git a/packages/core/src/alert/alert.stories.ts b/packages/core/src/alert/alert.stories.ts index f5fb3c7ecd..5564ff139f 100644 --- a/packages/core/src/alert/alert.stories.ts +++ b/packages/core/src/alert/alert.stories.ts @@ -5,16 +5,18 @@ */ import '@clr/core/alert'; -import { ClarityIcons, userIcon } from '@clr/core/icon-shapes'; +import '@clr/core/button'; +import '@clr/core/internal-components'; +import { angleIcon, ClarityIcons, timesCircleIcon, userIcon } from '@clr/core/icon-shapes'; import { cssGroup, propertiesGroup, setStyles } from '@clr/core/internal'; import { action } from '@storybook/addon-actions'; -import { boolean, color as colorKnob, select, text } from '@storybook/addon-knobs'; +import { color as colorKnob, select, text } from '@storybook/addon-knobs'; import { html } from 'lit-html'; -ClarityIcons.addIcons(userIcon); +ClarityIcons.addIcons(angleIcon, userIcon, timesCircleIcon); export default { - title: 'Components/Alert/Stories', + title: 'Experimental/Alert/Stories', component: 'cds-alert', parameters: { options: { showPanel: true }, @@ -29,88 +31,162 @@ export const API = () => { const slot = text('slot', 'This is an alert.', propertiesGroup); const alertStatus = select( 'status', - { 'none (default info)': undefined, info: 'info', success: 'success', warning: 'warning', danger: 'danger' }, + { + 'none (default info)': undefined, + info: 'info', + success: 'success', + warning: 'warning', + danger: 'danger', + loading: 'loading', + unknown: 'unknown', + }, undefined, propertiesGroup ); - const closable = boolean('closable', true, propertiesGroup); const iconShape = text('iconShape', undefined, propertiesGroup); const iconTitle = text('iconTitle', undefined, propertiesGroup); const size = select('size', { '(default)': 'default', sm: 'sm' }, undefined, propertiesGroup); const alertColor = colorKnob('--color', undefined, cssGroup); - const background = colorKnob('--background', undefined, cssGroup); - const borderColor = colorKnob('--border-color', undefined, cssGroup); - const borderRadius = text('--border-radius', undefined, cssGroup); const iconColor = colorKnob('--icon-color', undefined, cssGroup); - const closeIconColor = colorKnob('--close-icon-color', undefined, cssGroup); - const closeIconColorHover = colorKnob('--close-icon-color-hover', undefined, cssGroup); + const fontSize = text('--font-size', undefined, cssGroup); + const fontWeight = text('--font-weight', undefined, cssGroup); + const letterSpacing = text('--letter-spacing', undefined, cssGroup); return html` - - - ${slot} - + + ${slot}Dismiss `; }; +export const actions = () => { + return html` +
+
+ + Button 1 + Button 2 + +
+
+ + Button 1Button 2 + +
+
+ + Button 1 + Button 2 + +
+
+ `; +}; + +export const closeButton = () => { + return html` +
+
+ :: plain, check for aria warning (problem: it's throwing + a warning for every one and not just this one) +
+
:: aria-labeled
+
+ :: numeric size +
+
+ :: t-shirt size +
+
+ + :: custom icon shape... +
+
+ `; +}; + +export const lightAlerts = () => { + return html` +
+ + Single line alert: This alert example has only a single line of text.Use Inline Buttons in Lightweight Alerts + + + This alert example has many lines of text. A block of lorem ipsum sample text follows: A very small stage in a + vast cosmic arena descended from astronomers tesseract billions upon billions science Flatland. Invent the + universe the carbon in our apple pies the only home we've ever known with pretty stories for which there's + little good evidence citizens of distant epochs rich in heavy atoms. The carbon in our apple pies muse about + from which we spring star stuff harvesting star light courage of our questions paroxysm of global death and + billions upon billions upon billions upon billions upon billions upon billions upon billions. + Buttons inside alert actions should not be visible inside Lightweight Alerts + + +
+ `; +}; + export const status = () => { return html` - - - Foobar - - - - - Foo - - - - - Bar - - - - - Baz - - +
+ This is an alert with a status of "info" + This is an alert with a status of "success" + This is an alert with a status of "warning" + This is an alert with a status of "danger" + This is an alert with a status of "loading" + This is an alert with a status of "unknown" + This is an alert with a status of "danger" and a custom + icon + This is an alert with a badged, solid custom icon +
`; }; -export const sizes = () => { +export const compact = () => { return html` - - - Foobar - - - - - Bar - - +
+ + This compact alert example has only a single line of text.Use Inline Buttons in Lightweight Alerts + + + This compact alert example has a status of "info" many lines of text. A block of lorem ipsum sample text + follows: A very small stage in a vast cosmic arena descended from astronomers tesseract billions upon billions + science Flatland. Invent the universe the carbon in our apple pies the only home we've ever known with pretty + stories for which there's little good evidence citizens of distant epochs rich in heavy atoms. The carbon in our + apple pies muse about from which we spring star stuff harvesting star light courage of our questions paroxysm of + global death and billions upon billions upon billions upon billions upon billions upon billions upon + billions.Click One Click Two + Buttons inside alert actions should not be visible inside Lightweight Alerts + + + This is a compact alert with a status of "success" + This is a compact alert with a status of "warning" + This is a compact alert with a status of "danger" + This is a compact alert with a status of "loading" +
`; }; @@ -118,17 +194,16 @@ export const customStyles = () => { return html` - - - Foobar - - + A custom alert. + Example Action `; }; diff --git a/packages/core/src/alert/app-alert.element.scss b/packages/core/src/alert/app-alert.element.scss deleted file mode 100644 index 22d41c1368..0000000000 --- a/packages/core/src/alert/app-alert.element.scss +++ /dev/null @@ -1,66 +0,0 @@ -@import './../styles/mixins/utils'; -@import './alert.element'; - -:host { - border: none; - border-radius: 0; - overflow-y: auto; - --background: var(--clr-color-action-600, #{$clr-color-action-600}); - --color: var(--clr-color-neutral-0, #{$clr-color-neutral-0}); - --icon-color: var(--clr-color-neutral-0, #{$clr-color-neutral-0}); - --close-icon-color: var(--clr-color-neutral-0, #{$clr-color-neutral-0}); - --close-icon-color-hover: var(--clr-color-neutral-0, #{$clr-color-neutral-0}); - - .alert-wrapper { - // line-height of 24px on .alert-item inside app-level-alert - // blows out the sizing, so we need vert-padding to be 6px, not 8px - $appLevelAlertVertPadding: $clr_baselineRem_0_25; - padding-top: $appLevelAlertVertPadding; - padding-bottom: $appLevelAlertVertPadding; - } - - .alert-icon { - $alert-icon-dim: $clr_baselineRem_1; - @include equilateral($alert-icon-dim); - margin-left: -1 * $clr_baselineRem_0_125; - margin-top: -1 * $clr_baselineRem_4px; - --color: var(--icon-color); - } - - .alert-item { - justify-content: center; - align-items: center; - min-height: $clr_baselineRem_1; - } - - .alert-item > span, - .alert-text, - ::slotted(cds-alert-content) { - flex: 0 1 auto; - } - - .alert-icon-wrapper { - margin-top: $clr_baselineRem_0_125; - } - - button.close { - cds-icon { - margin-top: -1 * $clr_baselineRem_5px; - } - } - - &:host([status='info']) { - --background: var(--clr-color-action-600, #{$clr-color-action-600}); - --icon-color: var(--clr-color-neutral-0, #{$clr-color-neutral-0}); - } - - &:host([status='warning']) { - --background: hsl(26, 100%, 38%); - --icon-color: var(--clr-color-neutral-0, #{$clr-color-neutral-0}); - } - - &:host([status='danger']) { - --background: var(--clr-color-danger-800, #{$clr-color-danger-800}); - --icon-color: var(--clr-color-neutral-0, #{$clr-color-neutral-0}); - } -} diff --git a/packages/core/src/alert/app-alert.element.spec.ts b/packages/core/src/alert/app-alert.element.spec.ts deleted file mode 100644 index 8ef781082f..0000000000 --- a/packages/core/src/alert/app-alert.element.spec.ts +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. - * This software is released under MIT license. - * The full license information can be found in LICENSE in the root directory of this project. - */ -import { CdsAppAlert } from '@clr/core/alert'; -import '@clr/core/alert'; -import { CdsIcon } from '@clr/core/icon-shapes'; -import { CommonStringsService, CommonStringsServiceInternal } from '@clr/core/internal'; -import { - componentIsStable, - createTestElement, - getComponentSlotContent, - removeTestElement, - waitForComponent, -} from '@clr/core/test/utils'; - -describe('alert element', () => { - let testElement: HTMLElement; - let component: CdsAppAlert; - const placeholderText = 'I am a default alert with no attributes.'; - const placeholderActionsText = 'This is where action elements go.'; - - describe('Basic alert behaviors', () => { - beforeEach(async () => { - testElement = createTestElement(); - testElement.innerHTML = ` - - ${placeholderText} - ${placeholderActionsText} - - `; - - await waitForComponent('cds-app-alert'); - component = testElement.querySelector('cds-app-alert'); - }); - - afterEach(() => { - removeTestElement(testElement); - }); - - it('should create the component', async () => { - await componentIsStable(component); - const slots = getComponentSlotContent(component); - expect(slots.default).toBe(`${placeholderText}`); - expect(slots.actions).toBe(`${placeholderActionsText}`); - }); - - it('should support closable option', async () => { - await componentIsStable(component); - expect(component.closable).toBe(true); - expect(component.hasAttribute('closable')).toBe(true); - let button = component.shadowRoot.querySelector('button.close'); - expect(button).not.toBeNull(); - expect(button.classList.contains('close')).toBe(true); - - component.closable = false; - await componentIsStable(component); - expect(component.hasAttribute('closable')).toBe(false); - button = component.shadowRoot.querySelector('button.close'); - expect(button).toBeNull(); - }); - - it('should set icon shape based on status', async () => { - await componentIsStable(component); - let icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - - expect(icon.hasAttribute('shape')).toBe(true); - expect(icon.getAttribute('shape')).toEqual('info-circle'); - - component.status = 'warning'; - await componentIsStable(component); - icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - expect(icon.hasAttribute('shape')).toBe(true); - expect(icon.getAttribute('shape')).toEqual('exclamation-triangle'); - - component.status = 'danger'; - await componentIsStable(component); - icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - expect(icon.hasAttribute('shape')).toBe(true); - expect(icon.getAttribute('shape')).toEqual('exclamation-circle'); - }); - - it('should support custom icon shape', async () => { - await componentIsStable(component); - let icon = component.shadowRoot.querySelector('cds-icon'); - component.iconShape = 'exclamation-triangle'; - await componentIsStable(component); - icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - expect(icon.hasAttribute('shape')).toBe(true); - expect(icon.getAttribute('shape')).toEqual('exclamation-triangle'); - }); - - it('should set icon title based on status', async () => { - await componentIsStable(component); - let icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - - expect(icon.hasAttribute('title')).toBe(true); - expect(icon.getAttribute('title')).toEqual(CommonStringsService.keys.info); - - component.status = 'warning'; - await componentIsStable(component); - icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - expect(icon).toBeDefined(); - expect(icon.hasAttribute('title')).toBe(true); - expect(icon.getAttribute('title')).toEqual(CommonStringsService.keys.warning); - - component.status = 'danger'; - await componentIsStable(component); - icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - expect(icon).toBeDefined(); - expect(icon.hasAttribute('title')).toBe(true); - expect(icon.getAttribute('title')).toEqual(CommonStringsService.keys.danger); - }); - - it('should support custom icon title', async () => { - await componentIsStable(component); - let icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - - expect(icon.hasAttribute('shape')).toBe(true); - expect(icon.getAttribute('shape')).toEqual('info-circle'); - - component.iconTitle = 'my-icon-title'; - await componentIsStable(component); - icon = component.shadowRoot.querySelector('cds-icon'); - expect(icon).not.toBeNull(); - expect(icon).toBeDefined(); - expect(icon.hasAttribute('title')).toBe(true); - expect(icon.getAttribute('title')).toEqual('my-icon-title'); - }); - - it('should set close button aria label using Common Strings Service', async () => { - const service = new CommonStringsServiceInternal(); - - await componentIsStable(component); - const button = component.shadowRoot.querySelector('button.close'); - expect(button).not.toBeNull(); - expect(button.hasAttribute('aria-label')).toBe(true); - expect(button.getAttribute('aria-label')).toEqual(service.keys.alertCloseButtonAriaLabel); - }); - - it('should emit a closeChanged event when close button is clicked', async done => { - let value: any; - await componentIsStable(component); - component.addEventListener('closeChange', (e: CustomEvent) => { - value = e.detail; - expect(value).toBe(true); - done(); - }); - - const button = component.shadowRoot.querySelector('button.close'); - expect(button).toBeDefined(); - button.click(); - }); - }); -}); diff --git a/packages/core/src/alert/app-alert.element.ts b/packages/core/src/alert/app-alert.element.ts deleted file mode 100644 index 1513ddf66c..0000000000 --- a/packages/core/src/alert/app-alert.element.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. - * This software is released under MIT license. - * The full license information can be found in LICENSE in the root directory of this project. - */ - -import { baseStyles, property, registerElementSafely } from '@clr/core/internal'; -import { CdsBaseAlert } from './alert.base.js'; -import { styles } from './app-alert.element.css.js'; -/** - * App-level alerts are placed at the very top of the global context. They should - * not be placed in any other configuration. Their purpose is to provide global - * alerts available and relating to the full context of the overall application. - * - * ```typescript - * import '@clr/core/alert'; - * ``` - * - * ```html - * - * This is an app alert. - * - * ``` - * - * @beta - * @element cds-app-alert - * @slot default - Content slot for inside the alert. Usually will contain at least a component. - * @cssprop --color - * @cssprop --background - * @cssprop --icon-color - * @cssprop --close-icon-color - * @cssprop --close-icon-color-hover - */ -export class CdsAppAlert extends CdsBaseAlert { - /** Sets the color of the alert from a predefined list of statuses */ - @property({ type: String }) - status: 'info' | 'warning' | 'danger'; - - static get styles() { - return [baseStyles, styles]; - } -} - -registerElementSafely('cds-app-alert', CdsAppAlert); - -declare global { - interface HTMLElementTagNameMap { - 'cds-app-alert': CdsAppAlert; - } -} diff --git a/packages/core/src/alert/app-alert.stories.mdx b/packages/core/src/alert/app-alert.stories.mdx deleted file mode 100644 index 9c3f08be76..0000000000 --- a/packages/core/src/alert/app-alert.stories.mdx +++ /dev/null @@ -1,40 +0,0 @@ -import { Meta, Props, Story, Preview, API } from '@storybook/addon-docs/blocks'; -import { html, LitElement } from 'lit-element'; - - - -# App Alert - -App-level alerts are placed at the very top of the global context. They should -not be placed in any other configuration. Their purpose is to provide global -alerts available and relating to the full context of the overall application. - -## Installation - -To use the app alert component import the component in your JavaScript. - -```typescript -import '@clr/core/alert'; -``` - -```html - - This is an app alert. - -``` - -## Status - - - - - -## Custom Styles - - - - - -## API - - diff --git a/packages/core/src/alert/app-alert.stories.ts b/packages/core/src/alert/app-alert.stories.ts deleted file mode 100644 index 3ac06ec8b4..0000000000 --- a/packages/core/src/alert/app-alert.stories.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. - * This software is released under MIT license. - * The full license information can be found in LICENSE in the root directory of this project. - */ - -import '@clr/core/alert'; -import { ClarityIcons, userIcon } from '@clr/core/icon-shapes'; -import { cssGroup, propertiesGroup, setStyles } from '@clr/core/internal'; -import { action } from '@storybook/addon-actions'; -import { boolean, color as colorKnob, select, text } from '@storybook/addon-knobs'; -import { html } from 'lit-html'; - -ClarityIcons.addIcons(userIcon); - -export default { - title: 'Components/App Alert/Stories', - component: 'cds-app-alert', - parameters: { - options: { showPanel: true }, - design: { - type: 'figma', - url: 'https://www.figma.com/file/v2mkhzKQdhECXOx8BElgdA/Clarity-UI-Library---light-2.2.0?node-id=51%3A666', - }, - }, -}; - -export const API = () => { - const slot = text('slot', 'This is an alert.', propertiesGroup); - const alertStatus = select( - 'status', - { 'none (default info)': undefined, info: 'info', warning: 'warning', danger: 'danger' }, - undefined, - propertiesGroup - ); - const closable = boolean('closable', true, propertiesGroup); - const iconShape = text('iconShape', undefined, propertiesGroup); - const iconTitle = text('iconTitle', undefined, propertiesGroup); - - const alertColor = colorKnob('--color', undefined, cssGroup); - const background = colorKnob('--background', undefined, cssGroup); - const iconColor = colorKnob('--icon-color', undefined, cssGroup); - const closeIconColor = colorKnob('--close-icon-color', undefined, cssGroup); - const closeIconColorHover = colorKnob('--close-icon-color-hover', undefined, cssGroup); - - return html` - - - - ${slot} - - - `; -}; - -export const status = () => { - return html` - - - Foobar - - - Fix - - - - - Bar - - - Fix - - - - - Baz - - - Fix - - - `; -}; - -export const customStyles = () => { - return html` - - - - Foobar - - - `; -}; diff --git a/packages/core/src/alert/entrypoint.tsconfig.json b/packages/core/src/alert/entrypoint.tsconfig.json index 36391bb1db..d61fcaf5dd 100644 --- a/packages/core/src/alert/entrypoint.tsconfig.json +++ b/packages/core/src/alert/entrypoint.tsconfig.json @@ -5,6 +5,7 @@ }, "references": [ { "path": "../internal/entrypoint.tsconfig.json" }, + { "path": "../internal-components/entrypoint.tsconfig.json" }, { "path": "../button/entrypoint.tsconfig.json" }, { "path": "../icon/entrypoint.tsconfig.json" }, { "path": "../icon-shapes/entrypoint.tsconfig.json" } diff --git a/packages/core/src/alert/index.ts b/packages/core/src/alert/index.ts index db14c36612..a8e0dc0c48 100644 --- a/packages/core/src/alert/index.ts +++ b/packages/core/src/alert/index.ts @@ -5,6 +5,6 @@ */ export * from './alert-actions.element.js'; -export * from './alert-content.element.js'; export * from './alert.element.js'; -export * from './app-alert.element.js'; +export * from './alert-group.element.js'; +export * from './alert.interfaces.js'; diff --git a/packages/core/src/badge/badge.element.scss b/packages/core/src/badge/badge.element.scss index f383acdfac..602047c001 100644 --- a/packages/core/src/badge/badge.element.scss +++ b/packages/core/src/badge/badge.element.scss @@ -17,8 +17,8 @@ } .private-host { - display: inline-flex; - align-items: center; + @include center-content; + justify-content: center; height: var(--size); line-height: 1em; @@ -35,11 +35,10 @@ padding: var(--padding); & > span { - display: inline-flex; + @include center-content; + height: var(--font-size); min-width: calc(0.8 * var(--font-size)); - align-items: center; - justify-content: center; & > span { text-align: center; @@ -106,3 +105,16 @@ --border-color: #{$cds-token-color-danger-900}; --color: #{$cds-token-color-neutral-0}; } + +// Hack for Safari; Safari needs badge content to be bold +@media not all and (min-resolution: 0.001dpcm) { + @supports (-webkit-appearance: none) { + .private-host { + font-weight: 600; + + & > span { + transform: translateY(calc(-0.06667 * var(--size))); + } + } + } +} diff --git a/packages/core/src/button/base-button.element.scss b/packages/core/src/button/base-button.element.scss index 9c9b0b02c4..9179b62f8b 100644 --- a/packages/core/src/button/base-button.element.scss +++ b/packages/core/src/button/base-button.element.scss @@ -7,9 +7,9 @@ @import './../styles/mixins/mixins'; // TODO: progress spinner CSS deprecated, will be replaced by cds-circular-progress -@import './../../../packages/angular/projects/clr-angular/src/progress/spinner/variables.spinner'; -@import './../../../packages/angular/projects/clr-angular/src/image/icons.clarity'; -@import './../../../packages/angular/projects/clr-angular/src/progress/spinner/spinner.clarity'; +@import './../../../angular/projects/clr-angular/src/progress/spinner/variables.spinner'; +@import './../../../angular/projects/clr-angular/src/image/icons.clarity'; +@import './../../../angular/projects/clr-angular/src/progress/spinner/spinner.clarity'; :host { --box-shadow-color: #{$cds-token-color-action-900}; @@ -28,6 +28,10 @@ --height: #{$cds-token-space-size-11}; height: var(--height); // height is set so button is not distorted when in flex container + --min-width: #{$cds-token-space-size-13}; + + --text-decoration: none; + display: inline-block; // following is a fix for Firefox cutting off right edge of buttons. remove when no longer needed. @@ -36,8 +40,9 @@ } .private-host { + @include vertical-center-content; + -webkit-appearance: none !important; - align-items: center; background: var(--background); border-color: var(--border-color); border-radius: var(--border-radius); @@ -45,17 +50,16 @@ border-width: var(--border-width); color: var(--color); cursor: pointer; - display: inline-flex; font-size: var(--font-size); - height: var(--height); + height: auto; justify-content: center; line-height: 1em; - min-width: #{$cds-token-space-size-13}; + min-width: var(--min-width); overflow: visible; padding: var(--padding); position: relative; text-align: center; - text-decoration: none; + text-decoration: var(--text-decoration); text-overflow: ellipsis; transform: translateZ(0px); // for chrome rendering bug user-select: none; @@ -67,9 +71,7 @@ } & > span { - display: flex; - align-items: center; - justify-content: center; + @include center-content('block'); height: var(--font-size); } @@ -103,11 +105,8 @@ ::slotted(a) { text-decoration: none !important; display: block; + height: 100%; color: inherit; - margin: calc(var(--padding) * -1); - padding: var(--padding); - height: var(--height); - min-width: #{$cds-token-space-size-13}; } ::slotted(cds-icon) { @@ -186,12 +185,12 @@ --color: #{$cds-token-color-action-600}; } -:host([size='sm']) { +:host([size='sm']) .private-host { --padding: #{$cds-token-layout-space-sm} #{$cds-token-layout-space-md}; --height: calc(#{$cds-token-space-size-9} + #{$cds-token-space-size-1}); ::slotted(a) { - margin: calc(#{$cds-token-layout-space-sm} * -1) calc(#{$cds-token-layout-space-md} * -1); + margin: 0 #{$cds-token-space-size-1}; } } diff --git a/packages/core/src/button/button.element.spec.ts b/packages/core/src/button/button.element.spec.ts index d0b296f509..037659a914 100644 --- a/packages/core/src/button/button.element.spec.ts +++ b/packages/core/src/button/button.element.spec.ts @@ -5,7 +5,13 @@ */ import { CdsButton, ClrLoadingState } from '@clr/core/button'; import '@clr/core/button'; -import { componentIsStable, createTestElement, removeTestElement, waitForComponent } from '@clr/core/test/utils'; +import { + componentIsStable, + createTestElement, + getComponentSlotContent, + removeTestElement, + waitForComponent, +} from '@clr/core/test/utils'; describe('button element', () => { let testElement: HTMLElement; @@ -139,11 +145,21 @@ describe('button element', () => { }); describe('LoadingStateChange', () => { + it('should fallback to default state as expected', async () => { + await componentIsStable(component); + component.loadingState = null; + await componentIsStable(component); + expect(component.loadingState).toEqual(ClrLoadingState.DEFAULT); + expect(component.hasAttribute('disabled')).toEqual(false); + expect(component.style.getPropertyValue('width')).toBe(''); + }); + it('should set default state as expected', async () => { await componentIsStable(component); component.loadingState = ClrLoadingState.DEFAULT; await componentIsStable(component); expect(component.hasAttribute('disabled')).toEqual(false); + expect(component.style.getPropertyValue('width')).toBe(''); }); it('should set loading state as expected', async () => { @@ -233,3 +249,89 @@ describe('button element', () => { }); }); }); + +describe('buttonSlots: ', () => { + const iconSlotSelector = '.button-icon'; + const badgeSlotSelector = '.button-badge'; + let elem: HTMLElement; + + function getSlotName(classname: string) { + return classname.slice(1); + } + + function getSlotNameSelector(selector: string) { + return 'slot[name="' + getSlotName(selector) + '"]'; + } + + beforeEach(() => { + elem = createTestElement(); + }); + + afterEach(() => { + removeTestElement(elem); + }); + + it('should fallback to text slot', async () => { + elem.innerHTML = `Text slot`; + await waitForComponent('cds-button'); + const component = elem.querySelector('cds-button'); + const slots = getComponentSlotContent(component); + + expect(slots.default).toContain('Text slot'); + expect(component.shadowRoot.querySelector(iconSlotSelector)).toBeNull(); + expect(component.shadowRoot.querySelector(badgeSlotSelector)).toBeNull(); + }); + + it('should include an icon slot if an icon is present', async () => { + elem.innerHTML = `Text slot`; + await waitForComponent('cds-button'); + const component = elem.querySelector('cds-button'); + const slots = getComponentSlotContent(component); + const iconSlotName = getSlotName(iconSlotSelector); + + expect(slots.default).toContain('Text slot'); + + expect(component.shadowRoot.querySelector(iconSlotSelector)).not.toBeNull(); + expect(component.shadowRoot.querySelector(getSlotNameSelector(iconSlotSelector))).not.toBeNull(); + expect(slots[iconSlotName]).toContain('cds-icon'); + expect(slots[iconSlotName]).toContain('shape="ohai"'); + + expect(component.shadowRoot.querySelector(badgeSlotSelector)).toBeNull(); + }); + + it('should include a badge slot if a badge is present', async () => { + elem.innerHTML = `49Text slot`; + await waitForComponent('cds-button'); + const component = elem.querySelector('cds-button'); + const slots = getComponentSlotContent(component); + const badgeSlotName = getSlotName(badgeSlotSelector); + + expect(slots.default).toContain('Text slot'); + + expect(component.shadowRoot.querySelector(badgeSlotSelector)).not.toBeNull(); + expect(component.shadowRoot.querySelector(getSlotNameSelector(badgeSlotSelector))).not.toBeNull(); + expect(slots[badgeSlotName]).toContain('49'); + + expect(component.shadowRoot.querySelector(iconSlotSelector)).toBeNull(); + }); + + it('should include both an icon and a badge slot if both are present', async () => { + elem.innerHTML = `49Text slot`; + await waitForComponent('cds-button'); + const component = elem.querySelector('cds-button'); + const slots = getComponentSlotContent(component); + const badgeSlotName = getSlotName(badgeSlotSelector); + const iconSlotName = getSlotName(iconSlotSelector); + + expect(slots.default).toContain('Text slot'); + + expect(component.shadowRoot.querySelector(iconSlotSelector)).not.toBeNull(); + expect(component.shadowRoot.querySelector(getSlotNameSelector(iconSlotSelector))).not.toBeNull(); + expect(slots[iconSlotName]).toContain('cds-icon'); + expect(slots[iconSlotName]).toContain('shape="ohai"'); + + expect(component.shadowRoot.querySelector(badgeSlotSelector)).not.toBeNull(); + expect(component.shadowRoot.querySelector(getSlotNameSelector(badgeSlotSelector))).not.toBeNull(); + expect(slots[badgeSlotName]).toContain('49'); + }); +}); diff --git a/packages/core/src/button/button.element.ts b/packages/core/src/button/button.element.ts index e8f7cfdd69..43004ff2de 100644 --- a/packages/core/src/button/button.element.ts +++ b/packages/core/src/button/button.element.ts @@ -5,6 +5,7 @@ */ import { + assignSlotNames, badgeSlot, baseStyles, CdsBaseButton, @@ -13,9 +14,7 @@ import { iconSpinnerCheck, iconSlot, property, - querySlot, registerElementSafely, - setAttributes, } from '@clr/core/internal'; import '@clr/core/icon'; import { ClarityIcons, errorStandardIcon } from '@clr/core/icon-shapes'; @@ -82,7 +81,9 @@ function buttonSlots(icon: boolean, badge: boolean) { * @cssprop --font-weight * @cssprop --height * @cssprop --letter-spacing + * @cssprop --min-width * @cssprop --padding + * @cssprop --text-decoration * @cssprop --text-transform */ export class CdsButton extends CdsBaseButton { @@ -114,10 +115,6 @@ export class CdsButton extends CdsBaseButton { @query('.private-host') privateHost: HTMLElement; - @querySlot('cds-icon') private icon: HTMLElement; - - @querySlot('cds-badge') private badge: HTMLElement; - /** * Changes the button content based on the value passed. * @@ -139,8 +136,7 @@ export class CdsButton extends CdsBaseButton { connectedCallback() { super.connectedCallback(); - setAttributes(this.icon, ['slot', 'button-icon']); - setAttributes(this.badge, ['slot', 'button-badge']); + assignSlotNames([this.icon, 'button-icon'], [this.badge, 'button-badge']); } update(props: Map) { diff --git a/packages/core/src/button/icon-button.element.scss b/packages/core/src/button/icon-button.element.scss index dbe1bae90d..9a703d7438 100644 --- a/packages/core/src/button/icon-button.element.scss +++ b/packages/core/src/button/icon-button.element.scss @@ -2,6 +2,22 @@ // This software is released under MIT license. // The full license information can be found in LICENSE in the root directory of this project. +@import './../styles/tokens/generated/index'; + +:host { + --padding: calc(#{$cds-token-space-size-6} - var(--border-width, #{$cds-token-global-border-width-static})) + calc( + #{$cds-token-space-size-6} - #{$cds-token-space-size-2} - + var(--border-width, #{$cds-token-global-border-width-static}) + ); +} + .private-host { min-width: 0; } + +::slotted(a) { + --color: inherit; + display: inline-block; + height: auto; +} diff --git a/packages/core/src/button/icon-button.element.spec.ts b/packages/core/src/button/icon-button.element.spec.ts index 5333f13f87..69875e919c 100644 --- a/packages/core/src/button/icon-button.element.spec.ts +++ b/packages/core/src/button/icon-button.element.spec.ts @@ -3,12 +3,13 @@ * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ +import { CdsIcon } from '@clr/core/icon-shapes'; import { CdsIconButton, ClrLoadingState } from '@clr/core/button'; import '@clr/core/button'; import '@clr/core/icon'; import { componentIsStable, createTestElement, removeTestElement, waitForComponent } from '@clr/core/test/utils'; -describe('icon button element', () => { +describe('Icon button element – ', () => { let testElement: HTMLElement; let component: CdsIconButton; @@ -33,7 +34,7 @@ describe('icon button element', () => { expect(component.querySelector('cds-icon')).not.toBe(null); }); - describe('LoadingStateChange', () => { + describe('LoadingStateChange: ', () => { it('should set default state as expected', async () => { await componentIsStable(component); component.loadingState = ClrLoadingState.DEFAULT; @@ -75,7 +76,7 @@ describe('icon button element', () => { }); }); - describe('Button Behaviors', () => { + describe('Button Behaviors: ', () => { xit('should warn on a missing aria-label', async () => { // await componentIsStable(component); // const button = component.querySelector('button'); @@ -90,3 +91,37 @@ describe('icon button element', () => { }); }); }); + +describe('Anchor Tags in Buttons: ', () => { + let testElement: HTMLElement; + let component: CdsIconButton; + let anchor: HTMLElement; + let icon: CdsIcon; + + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` +
+ +
+ `; + + await waitForComponent('cds-icon-button'); + component = testElement.querySelector('cds-icon-button'); + anchor = component.querySelector('a'); + icon = component.querySelector('cds-icon'); + }); + + afterEach(() => { + removeTestElement(testElement); + }); + + it('should set element slots as expected', async () => { + expect(anchor).toBeDefined(); + expect(icon).toBeDefined(); + expect(anchor.hasAttribute('slot')).toBe(true); + expect(icon.hasAttribute('slot')).toBe(false); + expect(anchor.getAttribute('slot')).toBe('button-icon'); + expect(icon.classList.contains('anchored-icon')).toBe(true); + }); +}); diff --git a/packages/core/src/button/icon-button.element.ts b/packages/core/src/button/icon-button.element.ts index 85b22c35d9..d44965ec78 100644 --- a/packages/core/src/button/icon-button.element.ts +++ b/packages/core/src/button/icon-button.element.ts @@ -5,6 +5,7 @@ */ import { + assignSlotNames, baseStyles, iconSlot, iconSpinner, @@ -21,7 +22,7 @@ import { CdsButton, ClrLoadingState } from './button.element.js'; * Icon buttons give applications a compact alternative to communicate action and direct user intent. * * ```typescript - * import '@clr/core/icon-button'; + * import '@clr/core/button'; * ``` * * ```html @@ -51,6 +52,21 @@ export class CdsIconButton extends CdsButton { @property({ type: String, required: 'warning' }) ariaLabel: string; + connectedCallback(): void { + // have to override default behavior when an anchor is passed into the icon button + super.connectedCallback(); + if (this.anchor) { + // removes slot designation from icon and adds it to the anchor tag + assignSlotNames([this.icon, false], [this.anchor, 'button-icon']); + + // we need a class on the icon because that's how the icon element knows to style itself + // we can't style it from the icon-button anymore because it's a nested+slotted element + if (this.icon) { + this.icon.classList.add('anchored-icon'); + } + } + } + render() { return html`
diff --git a/packages/core/src/button/icon-button.stories.mdx b/packages/core/src/button/icon-button.stories.mdx index 9ca62adc7b..8dd76a30c6 100644 --- a/packages/core/src/button/icon-button.stories.mdx +++ b/packages/core/src/button/icon-button.stories.mdx @@ -3,13 +3,13 @@ import { html, LitElement } from 'lit-element'; -# Icon Button +# Icon Buttons Icon buttons allow an application to communicate action and direct user intent in a compact and visually appealing manner. ## Installation -To use the button component import the component in your JavaScript, as well as the icon you want to use. Note that your application needs to have imported both the cds-icon component as well as any icon shape you want to use. +To use the icon button component import the component in your JavaScript, as well as the icon you want to use. Note that your application needs to have imported both the cds-icon component as well as any icon shape you want to use. ```typescript import '~@clr/core/icon'; @@ -59,6 +59,12 @@ ClarityIcons.add(downloadIcon); +## Links + + + + + ## Custom Styles @@ -67,4 +73,4 @@ ClarityIcons.add(downloadIcon); ## API - + \ No newline at end of file diff --git a/packages/core/src/button/icon-button.stories.ts b/packages/core/src/button/icon-button.stories.ts index 59902fa288..27a610849b 100644 --- a/packages/core/src/button/icon-button.stories.ts +++ b/packages/core/src/button/icon-button.stories.ts @@ -154,6 +154,25 @@ export const block = () => { `; }; +export const links = () => { + return html` +
+ + + + +
+ `; +}; + export const loading = () => { return html`
diff --git a/packages/core/src/button/index.ts b/packages/core/src/button/index.ts index 87025708dd..d481b41ec5 100644 --- a/packages/core/src/button/index.ts +++ b/packages/core/src/button/index.ts @@ -6,3 +6,4 @@ export * from './button.element.js'; export * from './icon-button.element.js'; +export * from './inline-button.element.js'; diff --git a/packages/core/src/button/inline-button.element.scss b/packages/core/src/button/inline-button.element.scss new file mode 100644 index 0000000000..00d6aabcd1 --- /dev/null +++ b/packages/core/src/button/inline-button.element.scss @@ -0,0 +1,65 @@ +// Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. +// This software is released under MIT license. +// The full license information can be found in LICENSE in the root directory of this project. + +@import './../styles/tokens/generated/index'; +@import './../styles/mixins/utils'; +@import './../styles/mixins/mixins'; + +:host { + --text-decoration: none; + --color: #{$cds-token-color-action-600}; + --font-size: inherit; + --line-height: inherit; + --letter-spacing: inherit; + + padding: 0; + display: inline; +} + +.private-host { + min-width: 0; + background: none; + border-radius: 0; + border: 0 none; + height: auto; + font-size: var(--font-size); + line-height: var(--line-height); + letter-spacing: var(--letter-spacing); + text-decoration: var(--text-decoration); + padding: 0; + vertical-align: inherit; + align-items: baseline; + + &:active { + box-shadow: none; + } +} + +::slotted(cds-icon) { + --color: inherit; + @include equilateral(1em); + transform: translateY(0.125em); +} + +::slotted(cds-icon:first-child) { + margin-right: #{$cds-token-space-size-2}; +} + +::slotted(cds-icon:last-child) { + margin-left: #{$cds-token-space-size-2}; +} + +:host(:active) .private-host { + box-shadow: none; +} + +:host(:active) { + --color: #{$cds-token-color-action-900}; + --text-decoration: underline; +} + +:host(:hover) { + --color: #{$cds-token-color-action-900}; + --text-decoration: underline; +} diff --git a/packages/core/src/button/inline-button.element.spec.ts b/packages/core/src/button/inline-button.element.spec.ts new file mode 100644 index 0000000000..d2a27382c7 --- /dev/null +++ b/packages/core/src/button/inline-button.element.spec.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ +import { CdsInlineButton } from '@clr/core/button'; +import { CdsIcon } from '@clr/core/icon-shapes'; +import '@clr/core/button'; +import '@clr/core/icon'; +import { componentIsStable, createTestElement, removeTestElement, waitForComponent } from '@clr/core/test/utils'; + +describe('Inline button element', () => { + let testElement: HTMLElement; + let component: CdsInlineButton; + const placeholderText = 'ohai'; + + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` +
+ ${placeholderText} +
+ `; + + await waitForComponent('cds-inline-button'); + component = testElement.querySelector('cds-inline-button'); + }); + + afterEach(() => { + removeTestElement(testElement); + }); + + it('should create the component', async () => { + await componentIsStable(component); + expect(component.innerText).toBe(placeholderText); + }); +}); + +describe('Inline button element with icon', () => { + it('add the anchored-icon classname to icons', async () => { + const testElement = createTestElement(); + testElement.innerHTML = ` +
+ kthxbye +
+ `; + + await waitForComponent('cds-inline-button'); + const component = testElement.querySelector('cds-inline-button'); + const icon = component.querySelector('cds-icon'); + + expect(icon.classList.contains('anchored-icon')); + removeTestElement(testElement); + }); +}); diff --git a/packages/core/src/button/inline-button.element.ts b/packages/core/src/button/inline-button.element.ts new file mode 100644 index 0000000000..25a59c5d56 --- /dev/null +++ b/packages/core/src/button/inline-button.element.ts @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ + +import { addClassnames, baseStyles, CdsBaseButton, registerElementSafely } from '@clr/core/internal'; +import { html } from 'lit-element'; +import { styles as baseButtonStyles } from './base-button.element.css.js'; +import { styles } from './inline-button.element.css.js'; + +/** + * Inline buttons are used inside and alongside textual content within Clarity components. + * They give action buttons a less prominent, yet familiar, visual presence. + * + * ```typescript + * import '@clr/core/button'; + * ``` + * + * ```html + * Button text goes here + * ``` + * @beta + * @element cds-inline-button + * @slot default - Content slot for inside the button + * @cssprop --color + * @cssprop --font-size + * @cssprop --font-weight + * @cssprop --letter-spacing + * @cssprop --text-decoration + */ +export class CdsInlineButton extends CdsBaseButton { + connectedCallback(): void { + super.connectedCallback(); + + // we need a class on the icon because that's how the icon element knows to style itself + // we can't style it from the icon-button anymore because it's a nested+slotted element + if (this.icon) { + addClassnames(this.icon, 'anchored-icon'); + } + } + + render() { + return html` + + + ${this.hiddenButtonTemplate} + + `; + } + + static get styles() { + return [baseStyles, baseButtonStyles, styles]; + } +} + +registerElementSafely('cds-inline-button', CdsInlineButton); + +declare global { + interface HTMLElementTagNameMap { + 'cds-inline-button': CdsInlineButton; + } +} diff --git a/packages/core/src/button/inline-button.stories.mdx b/packages/core/src/button/inline-button.stories.mdx new file mode 100644 index 0000000000..60082c7d9e --- /dev/null +++ b/packages/core/src/button/inline-button.stories.mdx @@ -0,0 +1,60 @@ +import { Meta, Props, Story, Preview, API } from '@storybook/addon-docs/blocks'; +import { html, LitElement } from 'lit-element'; + + + +# Inline Buttons + +Inline buttons are used inside and alongside textual content within Clarity components. They give action buttons a less prominent, yet familiar, visual presence. + +## Installation + +To use the inline button component import the component in your JavaScript. + +```typescript +import '~@clr/core/button'; +``` + +```html +Click Here +``` + +## Actions + + + + + +## Inline Buttons + + + + + +## Inline Buttons With Icons + + + + + +## Disabled Link Buttons + + + + + +## Inline Buttons With a Link Inside + + + + + +## Custom Styles + + + + + +## API + + \ No newline at end of file diff --git a/packages/core/src/button/inline-button.stories.ts b/packages/core/src/button/inline-button.stories.ts new file mode 100644 index 0000000000..955a57e62c --- /dev/null +++ b/packages/core/src/button/inline-button.stories.ts @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ + +import '@clr/core/button'; +import '@clr/core/icon'; +import { angleIcon, ClarityIcons, userIcon } from '@clr/core/icon-shapes'; +import { cssGroup, propertiesGroup, setStyles } from '@clr/core/internal'; +import { action } from '@storybook/addon-actions'; +import { boolean, color, text } from '@storybook/addon-knobs'; +import { html } from 'lit-html'; + +ClarityIcons.addIcons(angleIcon, userIcon); + +export default { + title: 'Components/Inline Button/Stories', + component: 'cds-inline-button', + parameters: { + options: { showPanel: true }, + }, +}; + +export const API = () => { + const disabled = boolean('disabled', false, propertiesGroup); + const textColor = color('--color', undefined, cssGroup); + const fontSize = text('--font-size', undefined, cssGroup); + const fontWeight = text('--font-weight', undefined, cssGroup); + const textDecoration = text('--text-decoration', undefined, cssGroup); + + return html` + + + + Click Me + + + `; +}; + +export const actions = () => { + return html` +

+ Birth Rig Veda great turbulent clouds corpus callosum preserve and cherish that pale blue dot prime number. Finite + but unbounded a still more glorious dawn awaits intelligent beings colonies vastness is bearable only through love + concept of the number one. Rich in heavy atoms bits of moving fluff intelligent beings hearts of the stars stirred + by starlight hundreds of thousands? Vanquish the impossible brain is the seed of intelligence star stuff + harvesting star light the only home we've ever known citizens of distant epochs the only home we've ever known and + billions upon billions upon billions upon billions upon billions upon billions upon billions. + Ohai +

+ `; +}; + +export const disabledInlineButton = () => { + return html` +

+ A still more glorious dawn awaits intelligent beings colonies vastness is bearable only through love. + Ohai +

+ `; +}; + +export const various = () => { + return html` +
+

+ Hearts of the stars stirred by starlight hundreds of thousands? Ohai + Kthxbye +

+

+ Birth Rig Veda great turbulent clouds corpus callosum preserve and cherish that pale blue dot prime number. + Finite but unbounded a still more glorious dawn awaits intelligent beings colonies vastness is bearable only + through love concept of the number one. Rich in heavy atoms bits of moving fluff intelligent beings hearts of + the stars stirred by starlight hundreds of thousands? Vanquish the impossible brain is the seed of intelligence + star stuff harvesting star light the only home we've ever known citizens of distant epochs the only home we've + ever known and billions upon billions upon billions upon billions upon billions upon billions upon billions. + Ohai Kthxbye +

+
+ `; +}; + +export const withIcons = () => { + return html` +
+

+ Finite but unbounded a still more glorious dawn awaits. + Ohai + Kthxbye +

+

+ Finite but unbounded a still more glorious dawn awaits. + Ohai + Kthxbye +

+

+ Finite but unbounded a still more glorious dawn awaits. + Ohai + Kthxbye +

+
+ `; +}; + +export const inlineButtonLinks = () => { + return html` +
+

+ Because you know someone is going to try to do this: + Ohai. We need to make sure it still works. And still looks okay + Disabled +

+
+ `; +}; + +export const customStyles = () => { + return html` + + Helloworld + `; +}; diff --git a/packages/core/src/icon-shapes/icon.element.scss b/packages/core/src/icon-shapes/icon.element.scss index 990a8a33b5..2c9d86be32 100644 --- a/packages/core/src/icon-shapes/icon.element.scss +++ b/packages/core/src/icon-shapes/icon.element.scss @@ -22,13 +22,11 @@ } :host { - --color: #{$cds-token-color-neutral-700}; - --badge-color: #{$cds-token-color-danger-700}; display: inline-block; @include equilateral(#{$cds-token-space-size-7}); margin: 0; vertical-align: middle; - fill: var(--color); + fill: var(--color, #{$cds-token-color-neutral-700}); contain: strict; } @@ -164,7 +162,7 @@ svg { .clr-i-badge, .clr-i-alert { - fill: var(--badge-color); + fill: var(--badge-color, #{$cds-token-color-danger-700}); } // outlined @@ -361,3 +359,7 @@ svg { :host([badge='inherit-triangle'][inverse]) { --badge-color: inherit; } + +:host(.anchored-icon) { + --color: inherit; +} diff --git a/packages/core/src/icon-shapes/icon.element.spec.ts b/packages/core/src/icon-shapes/icon.element.spec.ts index 7da2016453..9e7e391d5b 100644 --- a/packages/core/src/icon-shapes/icon.element.spec.ts +++ b/packages/core/src/icon-shapes/icon.element.spec.ts @@ -170,7 +170,7 @@ describe('icon element', () => { describe('sr-only: ', () => { it('should contain the sr-only element for screen readers', async () => { await componentIsStable(component); - const srOnlyEl = component.shadowRoot.querySelector('.sr-only'); + const srOnlyEl = component.shadowRoot.querySelector('[cds-layout="display:screen-reader-only"]'); expect(srOnlyEl).toBeDefined(); }); @@ -179,7 +179,7 @@ describe('icon element', () => { await componentIsStable(component); component.setAttribute('title', testTitle); await componentIsStable(component); - const srOnlyEl = component.shadowRoot.querySelector('.clr-sr-only'); + const srOnlyEl = component.shadowRoot.querySelector('[cds-layout="display:screen-reader-only"]'); expect(srOnlyEl.innerHTML).toContain(testTitle); }); }); diff --git a/packages/core/src/icon-shapes/icon.element.ts b/packages/core/src/icon-shapes/icon.element.ts index a3ac1503f0..5ad0e69926 100644 --- a/packages/core/src/icon-shapes/icon.element.ts +++ b/packages/core/src/icon-shapes/icon.element.ts @@ -87,8 +87,6 @@ export class CdsIcon extends IconMixinClass { } } - // TODO: MAKE title A REQUIRED(warn) PROPERTY WHEN THAT IS READY - /** If present, customizes the aria-label for the icon for accessibility. */ @property({ type: String }) title: string; @@ -182,7 +180,9 @@ export class CdsIcon extends IconMixinClass { protected render() { return html` ${unsafeHTML(ClarityIcons.registry[this.shape])} - ${this.title ? html`${this.title}` : ''} + ${this.title + ? html`${this.title}` + : ''} `; } diff --git a/packages/core/src/icon-shapes/icon.stories.ts b/packages/core/src/icon-shapes/icon.stories.ts index 9216dea368..7cf4c52356 100644 --- a/packages/core/src/icon-shapes/icon.stories.ts +++ b/packages/core/src/icon-shapes/icon.stories.ts @@ -360,7 +360,7 @@ export const customStyles = () => { } .custom-icon-colors::before { - content: 'B'; + content: 'X'; font-size: 14px; position: absolute; display: block; @@ -369,13 +369,16 @@ export const customStyles = () => { color: white; } - .custom-icon-colors:first-child::before { + .custom-icon-colors.a::before { content: 'A'; } - .custom-icon-colors:last-child::before { + .custom-icon-colors.b::before { + content: 'B'; + } + + .custom-icon-colors.c::before { content: 'C'; - color: inherit; } .custom-icon-colors cds-icon { @@ -390,19 +393,20 @@ export const customStyles = () => { .icon-b { --color: fuchsia; + --badge-color: fuchsia; } .icon-c { --badge-color: yellow; } -
+
-
- +
+
-
+

diff --git a/packages/core/src/internal-components/close-button/close-button.element.scss b/packages/core/src/internal-components/close-button/close-button.element.scss new file mode 100644 index 0000000000..4c92135bcb --- /dev/null +++ b/packages/core/src/internal-components/close-button/close-button.element.scss @@ -0,0 +1,34 @@ +// Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. +// This software is released under MIT license. +// The full license information can be found in LICENSE in the root directory of this project. +@import './../../styles/tokens/generated/index'; + +:host { + --box-shadow-color: transparent; + --border-radius: 0; + --background: transparent; + --color: inherit; + --border-color: transparent; + --border-width: 0; + --opacity: 0.8; + --padding: #{$cds-token-space-size-2}; + + opacity: var(--opacity, 0.8); +} + +.private-host { + min-width: 0; +} + +:host(:hover) { + --background: transparent; + --opacity: 1; +} + +:host(:active) cds-icon { + transform: translateY(#{$cds-token-space-size-1}); +} + +:host() cds-icon { + --color: var(--color); +} diff --git a/packages/core/src/internal-components/close-button/close-button.element.spec.ts b/packages/core/src/internal-components/close-button/close-button.element.spec.ts new file mode 100644 index 0000000000..94e0ecf25b --- /dev/null +++ b/packages/core/src/internal-components/close-button/close-button.element.spec.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ + +import { CdsCloseButton } from '@clr/core/internal-components'; +import '@clr/core/internal-components'; +import { componentIsStable, createTestElement, removeTestElement, waitForComponent } from '@clr/core/test/utils'; + +describe('internal close button element', () => { + let testElement: HTMLElement; + let component: CdsCloseButton; + const placeholderText = 'ohai'; + + beforeEach(async () => { + testElement = createTestElement(); + testElement.innerHTML = ` +

+ ${placeholderText} +
+ `; + + await waitForComponent('cds-internal-close-button'); + component = testElement.querySelector('cds-internal-close-button'); + }); + + afterEach(() => { + removeTestElement(testElement); + }); + + it('should create the component but not project content into the slot', async () => { + await componentIsStable(component); + expect(component.innerText).not.toBe(placeholderText); + }); +}); diff --git a/packages/core/src/internal-components/close-button/close-button.element.ts b/packages/core/src/internal-components/close-button/close-button.element.ts new file mode 100644 index 0000000000..f31f5a4320 --- /dev/null +++ b/packages/core/src/internal-components/close-button/close-button.element.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ + +import { baseStyles, property, registerElementSafely } from '@clr/core/internal'; +import '@clr/core/icon'; +import { html } from 'lit-element'; +import { styles } from './close-button.element.css.js'; +import { styles as baseButtonStyles } from '../../button/base-button.element.css.js'; +import { CdsIconButton } from '@clr/core/button'; +import { ClarityIcons, timesIcon } from '@clr/core/icon-shapes'; + +ClarityIcons.addIcons(timesIcon); + +/** + * Icon buttons give applications a compact alternative to communicate action and direct user intent. + * + * ```typescript + * import '@clr/core/internal-components'; + * ``` + * + * ```html + * + * ``` + * @beta + * @element cds-close-button + * @slot default - Content slot for inside the button + * @cssprop --background + * @cssprop --border-color + * @cssprop --border-radius + * @cssprop --border-width + * @cssprop --box-shadow-color + * @cssprop --color + * @cssprop --opacity + * @cssprop --padding + */ +export class CdsCloseButton extends CdsIconButton { + @property({ type: String }) + iconSize = '18'; + + @property({ type: String }) + iconShape = 'times'; + + render() { + return html` +
+ + ${this.hiddenButtonTemplate} +
+ `; + } + + static get styles() { + return [baseStyles, baseButtonStyles, styles]; + } +} + +registerElementSafely('cds-internal-close-button', CdsCloseButton); + +declare global { + interface HTMLElementTagNameMap { + 'cds-internal-close-button': CdsCloseButton; + } +} diff --git a/packages/core/src/internal-components/entrypoint.tsconfig.json b/packages/core/src/internal-components/entrypoint.tsconfig.json new file mode 100644 index 0000000000..e72ca2da56 --- /dev/null +++ b/packages/core/src/internal-components/entrypoint.tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.lib.json", + "compilerOptions": { + "composite": true + }, + "references": [ + { "path": "../internal/entrypoint.tsconfig.json" }, + { "path": "../icon/entrypoint.tsconfig.json" }, + { "path": "../icon-shapes/entrypoint.tsconfig.json" }, + { "path": "../button/entrypoint.tsconfig.json" } + ] +} diff --git a/packages/core/src/internal-components/index.ts b/packages/core/src/internal-components/index.ts new file mode 100644 index 0000000000..882bd05309 --- /dev/null +++ b/packages/core/src/internal-components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ + +export * from './close-button/close-button.element.js'; diff --git a/packages/core/src/internal-components/package.json b/packages/core/src/internal-components/package.json new file mode 100644 index 0000000000..59742d89ff --- /dev/null +++ b/packages/core/src/internal-components/package.json @@ -0,0 +1,4 @@ +{ + "sideEffects": true, + "name": "@clr/core/internal-components/" +} diff --git a/packages/core/src/internal/base/button.base.ts b/packages/core/src/internal/base/button.base.ts index 6b741013e2..510e11b20b 100644 --- a/packages/core/src/internal/base/button.base.ts +++ b/packages/core/src/internal/base/button.base.ts @@ -39,12 +39,16 @@ export class CdsBaseButton extends LitElement { @property({ type: Boolean }) disabled = false; - @querySlot('a') private anchor: HTMLAnchorElement; + @querySlot('cds-icon') protected icon: HTMLElement; + + @querySlot('a') protected anchor: HTMLAnchorElement; + + @querySlot('cds-badge') protected badge: HTMLElement; protected get hiddenButtonTemplate() { return this.readonly ? html`` - : html`
+ +
+ 1 + 2 + 3 +
+
+ + +
+ 1 + 2 + 3 +
+
+
1 @@ -626,6 +642,22 @@ export const verticalGap = () => {
+ +
+ 1 + 2 + 3 +
+
+ + +
+ 1 + 2 + 3 +
+
+
1 diff --git a/packages/core/src/styles/mixins/_utils.scss b/packages/core/src/styles/mixins/_utils.scss index 1fcef5b91c..6e8e5d6ea0 100644 --- a/packages/core/src/styles/mixins/_utils.scss +++ b/packages/core/src/styles/mixins/_utils.scss @@ -184,3 +184,17 @@ $opp-directions: ( @content; } } + +@mixin vertical-center-content($flex-block: 'inline') { + @if $flex-block == 'block' { + display: flex; + } @else { + display: inline-flex; + } + align-items: center; +} + +@mixin center-content($flex-block: 'inline') { + @include vertical-center-content($flex-block); + justify-content: center; +} diff --git a/packages/core/src/styles/spacing/spacing.stories.mdx b/packages/core/src/styles/spacing/spacing.stories.mdx index 375796eef9..9a20092494 100644 --- a/packages/core/src/styles/spacing/spacing.stories.mdx +++ b/packages/core/src/styles/spacing/spacing.stories.mdx @@ -5,11 +5,11 @@ import { html, LitElement } from 'lit-element'; # Spacing - - - This is an experimental API and likely will have breaking changes in the near future. - - + + This is an experimental API and likely will have breaking changes in the near future. + + +
Spacing is managed by two sets of spacing tokens. One set for managing space within components and another for managing page level layout space. diff --git a/packages/core/src/styles/tokens/tokens.stories.mdx b/packages/core/src/styles/tokens/tokens.stories.mdx index 56cbfb48d7..e3b575e9ea 100644 --- a/packages/core/src/styles/tokens/tokens.stories.mdx +++ b/packages/core/src/styles/tokens/tokens.stories.mdx @@ -5,11 +5,11 @@ import { html, LitElement } from 'lit-element'; # Design Tokens - - - This is an experimental API and likely will have breaking changes in the near future. - - + + This is an experimental API and likely will have breaking changes in the near future. + + +
Design Tokens are global variables to configure the foundation of the design system. Changes to a design token will propagate throughout the entire system. diff --git a/packages/core/src/styles/typography/typography.stories.mdx b/packages/core/src/styles/typography/typography.stories.mdx index 0910ef8bcd..102a24ddd2 100644 --- a/packages/core/src/styles/typography/typography.stories.mdx +++ b/packages/core/src/styles/typography/typography.stories.mdx @@ -5,11 +5,11 @@ import { html, LitElement } from 'lit-element'; # Typography - - - This is an experimental API and likely will have breaking changes in the near future. - - + + This is an experimental API and likely will have breaking changes in the near future. + + +
The Clarity Core Typography System provides a flexible API to apply typography styles explicitly to elements. This gives full control of the visual styling of diff --git a/packages/core/src/tag/tag.element.scss b/packages/core/src/tag/tag.element.scss index 9c57e22eca..7da1d870af 100644 --- a/packages/core/src/tag/tag.element.scss +++ b/packages/core/src/tag/tag.element.scss @@ -17,6 +17,8 @@ } .private-host { + @include vertical-center-content; + line-height: 1em; border-radius: var(--border-radius); background: var(--background); @@ -28,16 +30,13 @@ padding: var(--padding); height: var(--size); white-space: nowrap; - display: inline-flex; - align-items: center; .tag-icon, .tag-badge, .tag-content, .tag-close-icon { + @include vertical-center-content; height: calc(var(--size) - #{$cds-token-space-size-2}); - display: inline-flex; - align-items: center; } .tag-badge { @@ -51,6 +50,13 @@ .tag-badge { transform: translateY(calc(-1 * #{$cds-token-space-size-1})); + + // using empty containers here b/c firefox doesn't align text-only + // tags correctly alongside tags with containers in them + &.empty { + visibility: hidden; + width: #{$cds-token-space-size-1}; + } } .tag-content { @@ -60,6 +66,13 @@ .tag-icon { overflow: hidden; width: calc(var(--size) - (2 * var(--border-width) + (2 * #{$cds-token-space-size-1}))); + + // using empty containers here b/c firefox doesn't align text-only + // tags correctly alongside tags with containers in them + &.empty { + visibility: hidden; + width: #{$cds-token-space-size-2}; + } } .tag-close-icon { @@ -253,3 +266,12 @@ --border-color: #{$cds-token-color-danger-400}; } } + +// Hack for Safari; It is having a problem with layouts across the shadowDom +@media not all and (min-resolution: 0.001dpcm) { + @supports (-webkit-appearance: none) { + ::slotted(cds-badge) { + transform: translateY(calc(0.04545 * var(--size))); + } + } +} diff --git a/packages/core/src/tag/tag.element.spec.ts b/packages/core/src/tag/tag.element.spec.ts index 9a1f3420bc..c3979214db 100644 --- a/packages/core/src/tag/tag.element.spec.ts +++ b/packages/core/src/tag/tag.element.spec.ts @@ -76,6 +76,11 @@ describe('tag element', () => { await componentIsStable(component); expect(!!component.closable).toBe(true); expect(!!component.readonly).toBe(false); + component.closable = false; + await componentIsStable(component); + component.closable = true; + await componentIsStable(component); + expect(!!component.readonly).toBe(false); }); it('should warn if closable but there is no aria-label', async () => { diff --git a/packages/core/src/tag/tag.element.ts b/packages/core/src/tag/tag.element.ts index 633ac2233e..30da098da7 100644 --- a/packages/core/src/tag/tag.element.ts +++ b/packages/core/src/tag/tag.element.ts @@ -6,12 +6,11 @@ import { ClarityIcons, timesIcon } from '@clr/core/icon-shapes'; import { + assignSlotNames, baseStyles, CdsBaseButton, property, - querySlot, registerElementSafely, - setAttributes, StatusTypes, } from '@clr/core/internal'; import { html } from 'lit-element'; @@ -67,16 +66,9 @@ export class CdsTag extends CdsBaseButton { @property({ type: Boolean }) closable = false; - @querySlot('cds-icon') private icon: HTMLElement; - - @querySlot('cds-badge') private badge: HTMLElement; - connectedCallback() { super.connectedCallback(); - // TODO: this routine is repeated in button.element.ts. it may make sense to find a way to - // abstract it for dry-ing and reuse - setAttributes(this.icon, ['slot', 'tag-icon']); - setAttributes(this.badge, ['slot', 'tag-badge']); + assignSlotNames([this.icon, 'tag-icon'], [this.badge, 'tag-badge']); } updated(props: Map) { @@ -94,9 +86,13 @@ export class CdsTag extends CdsBaseButton { render() { return html`
- ${this.icon ? html`` : html``} + ${this.icon + ? html`` + : html``} - ${this.badge ? html`` : html``} + ${this.badge + ? html`` + : html``} ${this.closable ? html`` : html``}
${this.hiddenButtonTemplate} diff --git a/packages/core/src/tag/tag.stories.ts b/packages/core/src/tag/tag.stories.ts index 7e90789a27..8bb891ef71 100644 --- a/packages/core/src/tag/tag.stories.ts +++ b/packages/core/src/tag/tag.stories.ts @@ -202,44 +202,48 @@ export const tagsAndIcons = () => { const solidIcon = boolean('solid icons', false, propertiesGroup); return html` -
- No Icon - No Badge - Default 1 - Purple 2 - Blue 3 - Orange 12 - Light Blue 15 -
-
- No Icon - No Badge - Info - 1 - Success - 2 - Warning - 3 - - Danger - 12 +
+
+ No Icon + No Badge + Default 1 + Purple 2 + Blue 3 + Orange 12 + Light Blue 15 +
+
+ No Icon + No Badge + Info + 1 + Success + 2 + Warning + 3 + + Danger + 12 +
`; }; diff --git a/packages/core/src/test/utils.ts b/packages/core/src/test/utils.ts index bc7d9099d2..d5d31a653f 100644 --- a/packages/core/src/test/utils.ts +++ b/packages/core/src/test/utils.ts @@ -30,10 +30,11 @@ export function getComponentSlotContent(component: HTMLElement): { [name: string return Array.from(component.shadowRoot.querySelectorAll('slot')).reduce( (acc: { [name: string]: string }, slot: HTMLSlotElement) => { const name = slot.name.length > 0 ? slot.name : 'default'; - acc[name] = (slot.assignedNodes() as any[]).reduce( - (p, n) => p + (n.outerHTML !== undefined ? n.outerHTML : ''), - '' - ); + acc[name] = (slot.assignedNodes() as any[]).reduce((p, n) => { + let returnDom = n.outerHTML; + returnDom = n.outerHTML ? n.outerHTML : n.textContent.trim(); + return p + (returnDom ? returnDom : ''); + }, ''); return acc; }, {} diff --git a/packages/core/stylelint.config.js b/packages/core/stylelint.config.js index 833ac541cb..dc28de5862 100644 --- a/packages/core/stylelint.config.js +++ b/packages/core/stylelint.config.js @@ -3,7 +3,7 @@ const rules = { [ 'color', { - ignoreValues: ['inherit', 'red', 'blue', '/^getSassButtonColor/'], + ignoreValues: ['inherit', 'red', 'blue', 'transparent', '/^getSassButtonColor/'], }, ], ], diff --git a/packages/core/tsconfig.project.json b/packages/core/tsconfig.project.json index 66a7f168ec..a02674571e 100644 --- a/packages/core/tsconfig.project.json +++ b/packages/core/tsconfig.project.json @@ -12,6 +12,7 @@ { "path": "./src/button/entrypoint.tsconfig.json" }, { "path": "./src/icon/entrypoint.tsconfig.json" }, { "path": "./src/icon-shapes/entrypoint.tsconfig.json" }, + { "path": "./src/internal-components/entrypoint.tsconfig.json" }, { "path": "./src/tag/entrypoint.tsconfig.json" }, { "path": "./src/test-dropdown/entrypoint.tsconfig.json" } ] diff --git a/packages/icons/package.json b/packages/icons/package.json index 331af1f376..b5f413626e 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "npm-run-all build:css build:optimize build:web build:package", "build:web": "webpack --config webpack.config.js", - "build:css": "sass --source-map --precision=8 src/clr-icons.scss ../../dist/clr-icons/clr-icons.css", + "build:css": "sass --precision=8 src/clr-icons.scss ../../dist/clr-icons/clr-icons.css", "build:optimize": "csso -i ../../dist/clr-icons/clr-icons.css -o ../../dist/clr-icons/clr-icons.min.css -s file --no-restructure;", "build:package": "cpy npm.json README.md ../../dist/clr-icons/; mv ../../dist/clr-icons/npm.json ../../dist/clr-icons/package.json; replace '@VERSION' $npm_package_version ../../dist/clr-icons/package.json; ", "build:svg": "node ./scripts/clr-icons-svg.js", diff --git a/packages/ui/package.json b/packages/ui/package.json index 282c70a090..9f258947c7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -4,7 +4,7 @@ "version": "4.0.0-test.1", "scripts": { "build": "npm-run-all build:css build:prefix build:src build:optimize build:package", - "build:css": "sass --source-map --precision=8 src/main.scss ../../dist/clr-ui/clr-ui.css; sass --source-map --precision=8 src/dark-theme.scss ../../dist/clr-ui/clr-ui-dark.css", + "build:css": "sass --precision=8 src/main.scss ../../dist/clr-ui/clr-ui.css; sass --precision=8 src/dark-theme.scss ../../dist/clr-ui/clr-ui-dark.css", "build:prefix": "postcss ../../dist/clr-ui/clr-ui.css -o ../../dist/clr-ui/clr-ui.css; postcss ../../dist/clr-ui/clr-ui-dark.css -o ../../dist/clr-ui/clr-ui-dark.css", "build:src": "cpy --parents --cwd='../angular/projects/clr-angular/src/' '**/*.scss' ../../../../../dist/clr-ui/src/", "build:optimize": "csso -i ../../dist/clr-ui/clr-ui.css -o ../../dist/clr-ui/clr-ui.min.css -s file --no-restructure; csso -i ../../dist/clr-ui/clr-ui-dark.css -o ../../dist/clr-ui/clr-ui-dark.min.css -s file --no-restructure",