From 2c642590f6b4f9ae08b8ff406ff032102e42f177 Mon Sep 17 00:00:00 2001 From: Ming Xiao Date: Fri, 5 Jan 2018 14:31:38 -0800 Subject: [PATCH] Bring dropdown and checkbox dropdown into PUI [#154075625] Signed-off-by: Reid Mitchell --- .../dropdown/dropdown-item_spec.js | 168 ++++----- .../dropdown/dropdown_spec.js | 8 +- .../notifications_spec.js} | 3 +- src/css/buttons/buttons.scss | 4 - src/css/dropdowns/dropdown-flat.scss | 19 + src/css/dropdowns/dropdown-lg.scss | 43 +++ src/css/dropdowns/dropdown-link.scss | 16 + src/css/dropdowns/dropdown-sm.scss | 40 +++ src/css/dropdowns/dropdown-split.scss | 41 +++ src/css/dropdowns/dropdowns.scss | 334 ++++-------------- src/css/notifications/index.js | 5 + src/css/notifications/notifications.scss | 87 +++++ .../checkbox-dropdown/checkbox-dropdown.js | 6 +- .../dropdowns/{dropdowns.js => dropdown.js} | 106 ++---- src/react/dropdowns/dropdown_item.js | 55 +++ src/react/dropdowns/index.js | 3 +- src/react/notifications/notifications.js | 47 ++- 17 files changed, 541 insertions(+), 444 deletions(-) rename spec/pivotal-ui-react/{notification/notification_spec.js => notifications/notifications_spec.js} (99%) create mode 100644 src/css/dropdowns/dropdown-flat.scss create mode 100644 src/css/dropdowns/dropdown-lg.scss create mode 100644 src/css/dropdowns/dropdown-link.scss create mode 100644 src/css/dropdowns/dropdown-sm.scss create mode 100644 src/css/dropdowns/dropdown-split.scss create mode 100644 src/css/notifications/index.js create mode 100644 src/css/notifications/notifications.scss rename src/react/dropdowns/{dropdowns.js => dropdown.js} (60%) create mode 100644 src/react/dropdowns/dropdown_item.js diff --git a/spec/pivotal-ui-react/dropdown/dropdown-item_spec.js b/spec/pivotal-ui-react/dropdown/dropdown-item_spec.js index 9bcdcc29e..754971e55 100644 --- a/spec/pivotal-ui-react/dropdown/dropdown-item_spec.js +++ b/spec/pivotal-ui-react/dropdown/dropdown-item_spec.js @@ -1,140 +1,148 @@ import '../spec_helper'; import {DropdownItem} from '../../../src/react/dropdowns'; -import {findByClass, findByTag, clickOn} from '../spec_helper'; - describe('DropdownItem', () => { - let result; - const renderComponent = props => ReactDOM.render( - DropdownItem Text, root - ); - - it('passes through header', () => { - result = renderComponent({header: true}); - expect(findByClass(result, 'dropdown-header')).toContainText('DropdownItem Text'); - }); + let subject; - it('passes through divider', () => { - result = renderComponent({divider: true}); - expect(findByClass(result, 'divider')).toBeDefined(); + beforeEach(() => { + subject = ReactDOM.render(DropdownItem Text, root); }); - it('passes through className and style to the li ', () => { - result = renderComponent({ - className: 'test-item-class', - style: {opacity: '0.5'} + describe('header', () => { + beforeEach(() => { + subject::setProps({header: true}); }); - const listItem = findByTag(result, 'li'); - expect(listItem).toHaveClass('test-item-class'); - expect(listItem).toHaveCss({opacity: '0.5'}); + it('passes through header', () => { + expect('#root .dropdown-header').toContainText('DropdownItem Text'); + }); }); - it('passes through id to the anchor if an href is provided', () => { - result = renderComponent({id: 'test-item-id'}); + describe('divider', () => { + beforeEach(() => { + subject::setProps({divider: true}); + }); - let listItem = findByTag(result, 'li'); - expect(listItem).not.toHaveAttr('id'); + it('passes through divider', () => { + expect('#root .divider').toExist(); + }); + }); - result = renderComponent({id: 'test-item-id', href: 'test'}); + describe('className', () => { + beforeEach(() => { + subject::setProps({className: 'test-item-class'}); + }); - listItem = findByTag(result, 'li'); - expect(listItem).not.toHaveAttr('id'); - expect(listItem.querySelector('a')).toHaveAttr('id', 'test-item-id'); + it('passes through className to the li', () => { + expect('#root li').toHaveClass('test-item-class'); + }); }); - it('passes through href and target to the anchor', () => { - result = renderComponent({href: 'test', target: '_blank'}); - expect(findByTag(result, 'a')).toHaveAttr('href', 'test'); - expect(findByTag(result, 'a')).toHaveAttr('target', '_blank'); + describe('style', () => { + beforeEach(() => { + subject::setProps({style: {opacity: '0.5'}}); + }); + + it('passes through style to the li', () => { + expect('#root li').toHaveCss({opacity: '0.5'}); + }); }); - describe('onSelect handling', () => { - let handleSelectSpy; + describe('id', () => { + beforeEach(() => { + subject::setProps({id: 'test-item-id'}); + }); + describe('with href', () => { - it('passes through onSelect on anchor click', () => { - handleSelectSpy = jasmine.createSpy('handleSelect'); - const eventKey = '1'; - result = renderComponent({href: 'test', onSelect: handleSelectSpy, eventKey}); + beforeEach(() => { + subject::setProps({href: 'test'}); + }); - clickOn(findByTag(result, 'a')); - expect(handleSelectSpy).toHaveBeenCalled(); + it('passes through id to the anchor', () => { + expect('#root li a').toHaveAttr('id', 'test-item-id'); }); }); describe('without href', () => { - it('passes through onSelect on list item click', () => { - handleSelectSpy = jasmine.createSpy('handleSelect'); - const eventKey = '1'; - result = renderComponent({onSelect: handleSelectSpy, eventKey}); - - clickOn(findByTag(result, 'li')); - expect(handleSelectSpy).toHaveBeenCalled(); + it('does not pass through id to the anchor', () => { + expect('#root li').not.toHaveAttr('id'); + expect('#root li a').not.toExist(); }); }); + }); - describe('with disabled prop', () => { - it('does not call onSelect handler', () => { - handleSelectSpy = jasmine.createSpy('handleSelect'); - result = renderComponent({href: 'test', onSelect: handleSelectSpy, disabled: true}); + describe('with href and target', () => { + beforeEach(() => { + subject::setProps({href: 'test', target: '_blank'}); + }); + + it('passes through href and target to the anchor', () => { + expect('#root a').toHaveAttr('href', 'test'); + expect('#root a').toHaveAttr('target', '_blank'); + }); + }); - expect(findByTag(result, 'li')).toHaveClass('disabled'); - expect(findByTag(result, 'a')).toHaveAttr('disabled'); + describe('onSelect handling', () => { + let handleSelectSpy; + const eventKey = '1'; + + beforeEach(() => { + handleSelectSpy = jasmine.createSpy('handleSelect'); + subject::setProps({onSelect: handleSelectSpy, eventKey}); + }); - clickOn(findByTag(result, 'a')); + describe('when li is clicked', () => { + beforeEach(() => { + $('#root li').click(); + }); - expect(handleSelectSpy).not.toHaveBeenCalled(); + it('calls onSelect on li click', () => { + expect(handleSelectSpy).toHaveBeenCalledWith(jasmine.any(Object), eventKey); }); }); }); describe('onClick handling', () => { let handleClickSpy; - describe('with href', () => { - it('passes through onClick on anchor click', () => { - handleClickSpy = jasmine.createSpy('handleClick'); - result = renderComponent({href: 'test', onClick: handleClickSpy}); - clickOn(findByTag(result, 'a')); - expect(handleClickSpy).toHaveBeenCalled(); - }); + beforeEach(() => { + handleClickSpy = jasmine.createSpy('handleClick'); + subject::setProps({onClick: handleClickSpy}); }); - describe('without href', () => { - it('passes through onClick on list item click', () => { - handleClickSpy = jasmine.createSpy('handleClick'); - result = renderComponent({onClick: handleClickSpy}); + describe('when li is clicked', () => { + beforeEach(() => { + $('#root li').click(); + }); - clickOn(findByTag(result, 'li')); - expect(handleClickSpy).toHaveBeenCalled(); + it('calls onClick on li click', () => { + expect(handleClickSpy).toHaveBeenCalledWith(jasmine.any(Object)); }); }); describe('with disabled prop', () => { - it('does not call onClick handler', () => { - handleClickSpy = jasmine.createSpy('handleClick'); - result = renderComponent({href: 'test', onClick: handleClickSpy, disabled: true}); - - expect(findByTag(result, 'li')).toHaveClass('disabled'); - expect(findByTag(result, 'a')).toHaveAttr('disabled'); + beforeEach(() => { + subject::setProps({href: 'test', disabled: true}); + }); - clickOn(findByTag(result, 'a')); + it('does not call onClick handler', () => { + expect('#root li').toHaveClass('disabled'); + expect('#root a').toHaveAttr('disabled'); + $('#root a').click(); expect(handleClickSpy).not.toHaveBeenCalled(); }); }); }); - describe('when an a tag is passed in as a child', () => { beforeEach(() => { - result = ReactDOM.render( + subject = ReactDOM.render( link, root ); it('renders the child link', () => { - const listItem = findByTag(result, 'li'); - expect(listItem.querySelector('a')).toHaveAttr('id', 'custom'); + expect('#root li a').toHaveAttr('id', 'custom'); }); }); }); diff --git a/spec/pivotal-ui-react/dropdown/dropdown_spec.js b/spec/pivotal-ui-react/dropdown/dropdown_spec.js index cef3d86a3..e290f5fc1 100644 --- a/spec/pivotal-ui-react/dropdown/dropdown_spec.js +++ b/spec/pivotal-ui-react/dropdown/dropdown_spec.js @@ -27,10 +27,10 @@ describe('Dropdown', () => { }); it('correctly styles the dropdown-toggle, and adds a chevron icon', () => { - expect('.dropdown-toggle').toHaveText('Dropping'); - expect('.dropdown-toggle').toHaveClass('test-btn-class'); - expect('.dropdown-toggle').toHaveAttr('aria-haspopup', 'true'); - expect('.dropdown-toggle').toHaveAttr('aria-label', 'Nessun Dorma'); + expect('.dropdown button').toHaveText('Dropping'); + expect('.dropdown button').toHaveClass('test-btn-class'); + expect('.dropdown button').toHaveAttr('aria-haspopup', 'true'); + expect('.dropdown button').toHaveAttr('aria-label', 'Nessun Dorma'); expect('.icon-chevron_down').toExist(); }); diff --git a/spec/pivotal-ui-react/notification/notification_spec.js b/spec/pivotal-ui-react/notifications/notifications_spec.js similarity index 99% rename from spec/pivotal-ui-react/notification/notification_spec.js rename to spec/pivotal-ui-react/notifications/notifications_spec.js index dfa3c220f..e753a11c2 100644 --- a/spec/pivotal-ui-react/notification/notification_spec.js +++ b/spec/pivotal-ui-react/notifications/notifications_spec.js @@ -1,8 +1,7 @@ import '../spec_helper'; import {Notifications, NotificationItem, AlertNotifications} from '../../../src/react/notifications'; - -describe('Notification', () => { +describe('Notifications', () => { const props = { className: 'test-class', id: 'test-id', diff --git a/src/css/buttons/buttons.scss b/src/css/buttons/buttons.scss index 1544fb57f..726abbe3d 100644 --- a/src/css/buttons/buttons.scss +++ b/src/css/buttons/buttons.scss @@ -35,11 +35,7 @@ &:hover, &:focus, &:hover:focus { - outline: none; - .icon { - - } } // Button Content Rules diff --git a/src/css/dropdowns/dropdown-flat.scss b/src/css/dropdowns/dropdown-flat.scss new file mode 100644 index 000000000..5db574ce3 --- /dev/null +++ b/src/css/dropdowns/dropdown-flat.scss @@ -0,0 +1,19 @@ +@import "../pui-variables"; + +.dropdown.dropdown-flat { + + .dropdown-toggle { + border: 1px solid transparent; + background-color: transparent; + text-transform: uppercase; + color: $btn-default-color; + font-weight: $btn-font-weight; + letter-spacing: $btn-letter-spacing; + } + + &.dropdown-open { + .dropdown-toggle { + border: 1px solid transparent; + } + } +} \ No newline at end of file diff --git a/src/css/dropdowns/dropdown-lg.scss b/src/css/dropdowns/dropdown-lg.scss new file mode 100644 index 000000000..290138e52 --- /dev/null +++ b/src/css/dropdowns/dropdown-lg.scss @@ -0,0 +1,43 @@ +@import "../pui-variables"; + +.dropdown.dropdown-lg { + .dropdown-icon-col { + height: 40px; + + .dropdown-toggle { + width: 40px; + } + } + + .dropdown-toggle { + height: ($base-unit*5); + line-height: ($base-unit*5) - 2px; + font-size: 16px; + } + + a.dropdown-label { + height: ($base-unit*5); + line-height: ($base-unit*5) - 2px; + font-size: 16px; + } + + .dropdown-menu { + li { + padding: $base-unit; + line-height: 24px; + font-size: 16px; + } + } + + &.dropdown-split .dropdown-toggle { + width: 40px; + } + + .icon-toggle { + font-size: 20px; + } + + &.dropdown-icon-only:not(.dropdown-split) { + width: 40px; + } +} diff --git a/src/css/dropdowns/dropdown-link.scss b/src/css/dropdowns/dropdown-link.scss new file mode 100644 index 000000000..28cefa9a0 --- /dev/null +++ b/src/css/dropdowns/dropdown-link.scss @@ -0,0 +1,16 @@ +@import "../pui-variables"; + +.dropdown.dropdown-link { + width: initial; + white-space: nowrap; + + .dropdown-toggle, .dropdown-icon-col { + border: 1px solid transparent; + color: $link-color; + background-color: transparent; + } + + .icon-toggle svg { + fill: $link-color; + } +} \ No newline at end of file diff --git a/src/css/dropdowns/dropdown-sm.scss b/src/css/dropdowns/dropdown-sm.scss new file mode 100644 index 000000000..0a13fbe77 --- /dev/null +++ b/src/css/dropdowns/dropdown-sm.scss @@ -0,0 +1,40 @@ +@import "../pui-variables"; + +.dropdown.dropdown-sm { + .dropdown-icon-col { + height: 24px; + } + + .dropdown-toggle { + height: ($base-unit*3); + line-height: ($base-unit*3) - 2px; + font-size: 12px; + } + + a.dropdown-label { + height: ($base-unit*3); + line-height: ($base-unit*3) - 2px; + font-size: 12px; + } + + .dropdown-menu { + li { + padding: $base-unit; + line-height: 16px; + font-size: 12px; + } + } + + &.dropdown-split .dropdown-toggle { + width: 24px; + padding: 11px; + } + + .icon-toggle { + font-size: 14px; + } + + &.dropdown-icon-only:not(.dropdown-split) { + width: 24px; + } +} diff --git a/src/css/dropdowns/dropdown-split.scss b/src/css/dropdowns/dropdown-split.scss new file mode 100644 index 000000000..a2cbfa3c2 --- /dev/null +++ b/src/css/dropdowns/dropdown-split.scss @@ -0,0 +1,41 @@ +@import "../pui-variables"; + +.dropdown.dropdown-split { + > .grid { + width: 100%; + + .dropdown-toggle { + border: 1px solid $input-border; + border-radius: 0 2px 2px 0; + width: 32px; + + .icon-toggle { + justify-content: center; + pointer-events: none; + font-size: 16px; + } + } + } + + .dropdown-label { + border: 1px solid $input-border; + border-radius: 2px 0 0 2px; + border-right: none; + width: 100%; + padding: 0 $base-unit; + line-height: ($base-unit*4) - 2px; + font-size: 14px; + display: inline-block; + color: inherit; + } + + .dropdown-toggle { + border-radius: 0 2px 2px 0; + } + + &:not(.dropdown-closed) .dropdown-toggle:focus ~ .dropdown-label, + &.dropdown-open .dropdown-label { + border: 1px solid $input-border-focus; + border-right: none; + } +} \ No newline at end of file diff --git a/src/css/dropdowns/dropdowns.scss b/src/css/dropdowns/dropdowns.scss index 6c372beac..3fa1ad274 100644 --- a/src/css/dropdowns/dropdowns.scss +++ b/src/css/dropdowns/dropdowns.scss @@ -1,15 +1,19 @@ @import "../pui-variables"; +@import "./dropdown-sm"; +@import "./dropdown-lg"; +@import "./dropdown-link"; +@import "./dropdown-flat"; +@import "./dropdown-split"; .dropdown { - display: block; - width:100%; + width: 100%; position: relative; + .dropdown-toggle { display: inline-flex; - width:100%; + width: 100%; vertical-align: top; - padding:0 (($base-unit*4)- 1px) 0 $base-unit; //32px - 1px border - height:($base-unit*4); + height: ($base-unit*4); line-height: ($base-unit*4) - 2px; font-size: 14px; background-color: #fff; @@ -18,44 +22,61 @@ text-align: left; color: $dark-2; -webkit-appearance: none; - cursor:pointer; + cursor: pointer; + position: relative; + transition: none; + &:focus { -webkit-appearance: none; box-shadow: none; - outline:none; + outline: none; + } + + .icon-toggle { + pointer-events: none; + font-size: 18px; + + svg { + fill: $dark-5; + } } } - &.dropdown-split .icon { - pointer-events: none; - position: absolute; - right: 8px; - bottom: 8px; - font-size: 16px; + &:not(.dropdown-split):not(.dropdown-icon-only) { + .dropdown-toggle { + padding-left: $base-unit; + justify-content: space-between; + + .icon-toggle { + margin-left: 5px; + } + } } &:not(.dropdown-closed):not(.dropdown-flat):not(.dropdown-link) .dropdown-toggle:focus { border: 1px solid $input-border-focus; ~ .dropdown-menu { - display:block; + display: block; } } &.dropdown-icon-only:not(.dropdown-split) { - width:32px; - - .dropdown-toggle { - padding: 0 8px; - } + width: 32px; .dropdown-menu { width: initial; } } - .icon svg { - fill:$dark-5; + .scrim { + position: fixed; + top: 0; + bottom: 0; + right: 0; + left: 0; + z-index: $zindex-dropdown; } + //Dropdown menu - default connected .dropdown-menu { display: none; @@ -63,35 +84,40 @@ left: 0; text-align: left; top: 100%; - width:100%; + width: 100%; z-index: $zindex-dropdown; - margin-top:-1px; - background-color:#fff; - border-top:none; + margin-top: -1px; + background-color: #fff; + border-top: none; border-left: 1px solid $input-border-focus; border-right: 1px solid $input-border-focus; border-bottom: 1px solid $input-border-focus; border-radius: 0 0 2px 2px; - cursor:pointer; + cursor: pointer; + ul { list-style: none; //UL reset - margin:0; - padding:0; - width:100%; + margin: 0; + padding: 0; + width: 100%; + li { - margin:0; - padding:($base-unit/2) $base-unit; + margin: 0; + padding: ($base-unit/2) $base-unit; line-height: 24px; - font-size:14px; + font-size: 14px; + &:hover { - background-color:$dark-11; + background-color: $dark-11; } + &:focus { - background-color:$dark-10; - outline:none; + background-color: $dark-10; + outline: none; } + a { - color:inherit; + color: inherit; display: block; } } @@ -99,34 +125,41 @@ //Dropdown Menu Floats &.dropdown-menu-float { - margin-top:2px; + margin-top: 2px; border: 1px solid $input-border-focus; - border-radius:2px; + border-radius: 2px; &.dropdown-menu-left, &.dropdown-menu-right { width: initial; + ul { - width:initial; + width: initial; + li { white-space: nowrap; } } } + &.dropdown-menu-left { - left:0; + left: 0; + ul { - float:left; + float: left; } } + &.dropdown-menu-right { - right:0; + right: 0; left: inherit; text-align: right; + ul { - float:right; + float: right; } } } + //Dropdown Scroll &.dropdown-menu-scroll { max-height: ($base-unit * 20); @@ -143,220 +176,5 @@ border: 1px solid $input-border-focus; } } - - //Dropdown Split - &.dropdown-split { - display: inline-flex; - flex-direction: row-reverse; - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - .dropdown-label { - border: 1px solid $input-border; - border-radius:2px 0 0 2px; - border-right: none; - width:100%; - padding:0 $base-unit; - height:($base-unit*4); - line-height: ($base-unit*4) - 2px; - font-size:14px; - } - - a.dropdown-label { - color:inherit; - } - .dropdown-toggle { - display: flex; - border-radius:0 2px 2px 0; - } - &:not(.dropdown-closed) .dropdown-toggle:focus ~ .dropdown-label, - &.dropdown-open .dropdown-label { - border: 1px solid $input-border-focus; - border-right: none; - } - } - - //Dropdown Flat - &.dropdown-flat { - .dropdown-toggle { - border:1px solid transparent; - background-color: transparent; - text-transform: uppercase; - color: $btn-default-color; - font-weight: $btn-font-weight; - letter-spacing: $btn-letter-spacing; - } - } - //Dropdown Link - &.dropdown-link { - width:initial; - white-space: nowrap; - .dropdown-toggle { - border: 1px solid transparent; - color: $link-color; - background-color: transparent; - } - .icon svg { - fill:$link-color; - } - } - &.dropdown-split .dropdown-toggle { - width:32px; - padding:($base-unit*2) - 1px; //16px - 1px border - } - - //Dropdown Large - &.dropdown-lg { - .dropdown-toggle { - padding:0 (($base-unit*5)- 1px) 0 $base-unit; - height:($base-unit*5); - line-height: ($base-unit*5) - 2px; - font-size: 16px; - } - .dropdown-label { - height:($base-unit*5); - line-height: ($base-unit*5) - 2px; - font-size: 16px; - } - .dropdown-menu { - li { - padding:$base-unit; - line-height: 24px; - font-size:16px; - } - } - &.dropdown-split .dropdown-toggle{ - width:40px; - padding:18px; - } - .icon { - font-size: 20px; - right:10px; - bottom:10px; - } - - &.dropdown-icon-only:not(.dropdown-split) { - width:40px; - } - } - - //Dropdown Small - &.dropdown-sm { - .dropdown-toggle { - padding:0 (($base-unit*3)- 1px) 0 $base-unit; - height:($base-unit*3); - line-height: ($base-unit*3) - 2px; - font-size: 12px; - } - .dropdown-label { - height:($base-unit*3); - line-height: ($base-unit*3) - 2px; - font-size: 12px; - } - .dropdown-menu { - li { - padding:$base-unit; - line-height: 16px; - font-size:12px; - } - } - &.dropdown-split .dropdown-toggle{ - width:24px; - padding:11px; - } - .icon { - font-size: 14px; - right:5px; - bottom:5px; - } - - &.dropdown-icon-only:not(.dropdown-split) { - width:24px; - } - } - - .scrim { - position: fixed; - top: 0; - bottom: 0; - right: 0; - left: 0; - z-index: $zindex-dropdown; - } -} - -.form-group .dropdown .icon { - pointer-events: none; - position: absolute; - right: 7px; - bottom: 7px; - font-size: 18px; -} - -.dropdown-notifications { - &.open { - .btn-link { - &.dropdown-toggle { - box-shadow: none; - fill: $neutral-6; - } - } - - .dropdown-menu > ul:before { - left: 22px; - } - } - - .dropdown-toggle { - font-size: $notifications-bell-default-size; - fill: $notifications-bell-default-color; - - .dropdown-notifications-title { - margin: 0; - position: relative; - } - - .dropdown-notifications-badge { - background-color: $notifications-badge-default-bg; - border-radius: 0.75em; - color: $neutral-11; - font-weight: $notifications-badge-font-weight; - line-height: 0.75em; - min-width: 1.25em; - padding: 0.25em; - text-align: center; - } - - .dropdown-notifications-alert .icon-warning { - height: 1.4em; - width: 1.4em; - fill: $notifications-alert-default-color; - } - .dropdown-notifications-alert { - padding: 0; - } - - .dropdown-notifications-alert, .dropdown-notifications-badge { - font-size: 0.55em; - left: 1.25em; - position: absolute; - bottom: 0; - } - } -} - -.dropdown-notifications-none { - padding: 20px; - text-align: center; - width: 200px; - - .icon { - font-size: 42px; - svg { - height: 1.4em; - width: 1.4em; - fill: $neutral-6; - } - } } diff --git a/src/css/notifications/index.js b/src/css/notifications/index.js new file mode 100644 index 000000000..2344a03be --- /dev/null +++ b/src/css/notifications/index.js @@ -0,0 +1,5 @@ +try { + require('../dropdowns'); + require('./notifications.css'); +} catch(e) { +} diff --git a/src/css/notifications/notifications.scss b/src/css/notifications/notifications.scss new file mode 100644 index 000000000..4b5ccb6fe --- /dev/null +++ b/src/css/notifications/notifications.scss @@ -0,0 +1,87 @@ +@import "../pui-variables"; + +.dropdown.dropdown-notifications { + &.open { + .btn-link { + &.dropdown-toggle { + box-shadow: none; + fill: $neutral-6; + } + } + + .dropdown-menu > ul:before { + left: 22px; + } + } + + .dropdown-toggle { + font-size: $notifications-bell-default-size; + fill: $notifications-bell-default-color; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + + .dropdown-notifications-title { + margin: 0; + position: relative; + display: flex; + align-items: center; + justify-content: center; + + .dropdown-notifications-alert, .dropdown-notifications-badge { + font-size: 0.55em; + left: 1.25em; + position: absolute; + bottom: 0; + } + } + + .dropdown-notifications-badge { + background-color: $notifications-badge-default-bg; + border-radius: 0.75em; + color: $neutral-11; + font-weight: $notifications-badge-font-weight; + line-height: 0.75em; + min-width: 1.25em; + padding: 0.25em; + text-align: center; + } + + .dropdown-notifications-has-notifications, .dropdown-notifications-has-alerts { + svg { + fill: $navy-5; + } + } + + .dropdown-notifications-alert .icon-warning { + height: 1.4em; + width: 1.4em; + fill: $notifications-alert-default-color; + } + + .dropdown-notifications-alert { + padding: 0; + } + + .icon { + font-size: inherit; + } + } +} + +.dropdown-notifications-none { + padding: 20px; + text-align: center; + width: 200px; + + .icon { + font-size: 42px; + svg { + height: 1.4em; + width: 1.4em; + fill: $neutral-6; + } + } +} + diff --git a/src/react/checkbox-dropdown/checkbox-dropdown.js b/src/react/checkbox-dropdown/checkbox-dropdown.js index c16896844..1d72cd826 100644 --- a/src/react/checkbox-dropdown/checkbox-dropdown.js +++ b/src/react/checkbox-dropdown/checkbox-dropdown.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {Dropdown, DropdownItem} from '../dropdowns'; import {Checkbox} from '../checkbox'; +import classnames from 'classnames'; function doNothing() { }; @@ -72,7 +73,8 @@ export class CheckboxDropdown extends React.Component { } render() { - const {labels, onChange, ...dropDownProps} = this.props; + // eslint-disable-next-line no-unused-vars + const {labels, onChange, className, ...dropDownProps} = this.props; const {options} = this.state; const dropdownItems = labels.map(label => { @@ -96,7 +98,7 @@ export class CheckboxDropdown extends React.Component { const title = {this.getTitle()}; - return ( + return ( this.toggleAll(e)} checked={this.allSelected()}> diff --git a/src/react/dropdowns/dropdowns.js b/src/react/dropdowns/dropdown.js similarity index 60% rename from src/react/dropdowns/dropdowns.js rename to src/react/dropdowns/dropdown.js index 8b6306c75..83cd08d01 100644 --- a/src/react/dropdowns/dropdowns.js +++ b/src/react/dropdowns/dropdown.js @@ -5,9 +5,21 @@ import {default as mixin} from '../mixins'; import Scrim from '../mixins/mixins/scrim_mixin'; import Transition from '../mixins/mixins/transition_mixin'; import {Icon} from '../iconography'; - -const defaultToggleNode = (showIcon, icon) => { - if (showIcon) return ; +import {Grid, FlexCol} from '../flex-grids'; + +const defaultToggleNode = (showIcon, icon, onClick, size, title, className, ariaLabel) => { + return ( + + ); }; export class Dropdown extends mixin(React.Component).with(Scrim, Transition) { @@ -16,6 +28,8 @@ export class Dropdown extends mixin(React.Component).with(Scrim, Transition) { this.state = { open: false }; + + this.click = this.click.bind(this); } static propTypes = { @@ -88,17 +102,19 @@ export class Dropdown extends mixin(React.Component).with(Scrim, Transition) { render() { const { - blockingScrim, border, buttonAriaLabel, buttonClassName, splitClassName, children, className, closeOnMenuClick, disableScrim, showIcon, - flat, link, labelAriaLabel, menuAlign, size, href, icon, onClick, onSplitClick, onEntered, onExited, split, title, toggle, - floatMenu, scroll, ...props + // eslint-disable-next-line no-unused-vars + closeOnMenuClick, onClick, onSplitClick, onEntered, onExited, + blockingScrim, border, buttonAriaLabel, buttonClassName, splitClassName, children, className, disableScrim, showIcon, + flat, link, labelAriaLabel, menuAlign, size, href, icon, split, title, toggle, floatMenu, scroll, ...props } = this.props; + const {open} = this.state; const buttonStyleClasses = classnames('dropdown-toggle', buttonClassName); const noTitle = typeof title === 'undefined' || title === null || title.length === 0; const forceIcon = noTitle || split; const iconVisible = forceIcon || showIcon; - const toggleNode = toggle ? toggle : defaultToggleNode(iconVisible, icon); + const toggleNode = toggle ? toggle : defaultToggleNode(iconVisible, icon, this.click, size, !split && title, buttonStyleClasses, buttonAriaLabel); const menuVisibility = open ? 'dropdown-open' : 'dropdown-closed'; const dropdownClasses = classnames('dropdown', { @@ -107,15 +123,15 @@ export class Dropdown extends mixin(React.Component).with(Scrim, Transition) { 'dropdown-link': link, 'dropdown-lg': size === 'large', 'dropdown-sm': size === 'small', - 'dropdown-icon-only' : !split && noTitle + 'dropdown-icon-only': !split && noTitle }, menuVisibility, className); const dropdownMenuClasses = classnames('dropdown-menu', { - 'dropdown-border' : border, - 'dropdown-menu-right' : menuAlign === 'right', - 'dropdown-menu-left' : menuAlign === 'left', - 'dropdown-menu-float' : split || flat || link || floatMenu || noTitle || menuAlign !== 'none', + 'dropdown-border': border, + 'dropdown-menu-right': menuAlign === 'right', + 'dropdown-menu-left': menuAlign === 'left', + 'dropdown-menu-float': split || flat || link || floatMenu || noTitle || menuAlign !== 'none', 'dropdown-menu-scroll': scroll } ); @@ -126,64 +142,18 @@ export class Dropdown extends mixin(React.Component).with(Scrim, Transition) { const splitProps = {href, 'aria-label': labelAriaLabel}; return (
- - {toggleNode} - {split && {title}} + {split ? + + {title} + + + {toggleNode} + + + : toggleNode} {(blockingScrim && open && !disableScrim) &&
} {dropdownOptions}
); } } - -export class DropdownItem extends React.Component { - static propTypes = { - className: PropTypes.string, - style: PropTypes.object, - href: PropTypes.string, - header: PropTypes.bool, - divider: PropTypes.bool, - disabled: PropTypes.bool, - eventKey: PropTypes.string, - onSelect: PropTypes.func, - onClick: PropTypes.func, - }; - - componentDidMount() { - require('../../css/dropdowns'); - } - - handleClick = event => { - const {href, disabled, onClick, onSelect, eventKey} = this.props; - if (disabled) return; - - - if (!href) { - event.preventDefault(); - } - - if (onSelect) { - onSelect(event, eventKey); - } - - if (onClick) { - onClick(event); - } - }; - - render() { - const {children, className, eventKey, style, href, header, divider, disabled, onClick, onSelect, ...anchorProps} = this.props; - - if (header) return (
  • {children}
  • ); - if (divider) return (
  • ); - - const anchor = href ? {children} : children; - const disabledClass = disabled ? 'disabled' : ''; - const dropdownItemClass = classnames(className, disabledClass); - - return (
  • - {anchor} -
  • ); - } -} diff --git a/src/react/dropdowns/dropdown_item.js b/src/react/dropdowns/dropdown_item.js new file mode 100644 index 000000000..00acb6025 --- /dev/null +++ b/src/react/dropdowns/dropdown_item.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +export class DropdownItem extends React.Component { + static propTypes = { + className: PropTypes.string, + style: PropTypes.object, + href: PropTypes.string, + header: PropTypes.bool, + divider: PropTypes.bool, + disabled: PropTypes.bool, + eventKey: PropTypes.string, + onSelect: PropTypes.func, + onClick: PropTypes.func, + }; + + componentDidMount() { + require('../../css/dropdowns'); + } + + handleClick = event => { + const {href, disabled, onClick, onSelect, eventKey} = this.props; + if (disabled) return; + + + if (!href) { + event.preventDefault(); + } + + if (onSelect) { + onSelect(event, eventKey); + } + + if (onClick) { + onClick(event); + } + }; + + render() { + // eslint-disable-next-line no-unused-vars + const {children, className, eventKey, style, href, header, divider, disabled, onClick, onSelect, ...anchorProps} = this.props; + + if (header) return (
  • {children}
  • ); + if (divider) return (
  • ); + + const anchor = href ? {children} : children; + const disabledClass = disabled ? 'disabled' : ''; + const dropdownItemClass = classnames(className, disabledClass); + + return (
  • + {anchor} +
  • ); + } +} diff --git a/src/react/dropdowns/index.js b/src/react/dropdowns/index.js index 282207794..5208177d5 100644 --- a/src/react/dropdowns/index.js +++ b/src/react/dropdowns/index.js @@ -1 +1,2 @@ -export {Dropdown, DropdownItem} from './dropdowns'; \ No newline at end of file +export {Dropdown} from './dropdown'; +export {DropdownItem} from './dropdown_item'; \ No newline at end of file diff --git a/src/react/notifications/notifications.js b/src/react/notifications/notifications.js index 455af89ba..aa08b302d 100644 --- a/src/react/notifications/notifications.js +++ b/src/react/notifications/notifications.js @@ -5,34 +5,38 @@ import {Dropdown, DropdownItem} from '../dropdowns'; import classnames from 'classnames'; import {mergeProps} from '../helpers'; +const defaultChild = (icon, message) =>
  • +
    + +

    {message}

    +
    +
  • ; + export class Notifications extends React.PureComponent { static propTypes = { size: PropTypes.oneOf(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) }; - static defaultProps = { - children:
  • -
    - -

    no notifications

    -
    -
  • - }; + componentDidMount() { + require('../../css/notifications'); + } render() { const {size, children, ...others} = this.props; const props = mergeProps(others, {className: 'dropdown-notifications dropdown-icon-only'}); const numChildren = React.Children.count(children); - const badge = numChildren > 1 ? {numChildren} : null; - const dropdownTitleClasses = classnames('dropdown-notifications-title', size); + const badge = numChildren > 0 ? {numChildren} : null; + const dropdownTitleClasses = classnames('dropdown-notifications-title', size, {'dropdown-notifications-has-notifications': numChildren > 0}); const dropdownTitle = (
    {badge}
    ); - - return {children}; + + return + {children || defaultChild('add', 'no notifications')} + ; } } @@ -40,21 +44,12 @@ export class AlertNotifications extends React.PureComponent { static propTypes = { size: PropTypes.oneOf(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) }; - - static defaultProps = { - children:
  • -
    - -

    no alerts

    -
    -
  • - }; render() { const {size, children, ...others} = this.props; const numChildren = React.Children.count(children); - const badge = numChildren > 1 && ; - const dropdownTitleClasses = classnames('dropdown-notifications-title', size); + const badge = numChildren > 0 && ; + const dropdownTitleClasses = classnames('dropdown-notifications-title', size, {'dropdown-notifications-has-alerts': numChildren > 0}); const dropdownTitle = (
    @@ -62,8 +57,10 @@ export class AlertNotifications extends React.PureComponent {
    ); const props = mergeProps(others, {className: 'dropdown-notifications dropdown-icon-only'}); - - return {children}; + + return + {children || defaultChild('notifications', 'no alerts')} + ; } }