diff options
author | Yoshie Muranaka <yoshiemuranaka@gmail.com> | 2019-09-10 08:09:43 -0700 |
---|---|---|
committer | Yoshie Muranaka <yoshiemuranaka@gmail.com> | 2019-09-24 10:12:54 -0700 |
commit | 5b8cef81cfa896ecdb0b6ad9d43c78d67d087a62 (patch) | |
tree | d8cd74dcadcacde0437d97b694db6c6e5deed096 /app/common | |
parent | 9e961fab86142547d3c3ab8b130b4089bd9fe756 (diff) | |
download | phosphor-webui-5b8cef81cfa896ecdb0b6ad9d43c78d67d087a62.tar.gz phosphor-webui-5b8cef81cfa896ecdb0b6ad9d43c78d67d087a62.zip |
Add batch action functionality to table Component
These changes aren't currently implemented on any table.
It will be added to event logs and local user table.
- Create tableCheckbox component to handle custom styles
and indeterminate state which needs to be set with JS
- Update tableComponent layout to allow transition for
toolbar. Updated user-accounts layout and styles to
account for these changes
Tested on Chrome, Safari, Firefox, Edge, IE
Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: Ic57a090db1ef66f9d33facfdc425db868ae8d8c6
Diffstat (limited to 'app/common')
-rw-r--r-- | app/common/components/table/table-checkbox.js | 67 | ||||
-rw-r--r-- | app/common/components/table/table-toolbar.js | 112 | ||||
-rw-r--r-- | app/common/components/table/table.html | 223 | ||||
-rw-r--r-- | app/common/components/table/table.js | 170 | ||||
-rwxr-xr-x | app/common/styles/base/variables.scss | 10 | ||||
-rw-r--r-- | app/common/styles/components/table.scss | 126 |
6 files changed, 584 insertions, 124 deletions
diff --git a/app/common/components/table/table-checkbox.js b/app/common/components/table/table-checkbox.js new file mode 100644 index 0000000..b15e4ed --- /dev/null +++ b/app/common/components/table/table-checkbox.js @@ -0,0 +1,67 @@ +window.angular && (function(angular) { + 'use strict'; + + /** + * + * tableCheckbox Component + * + */ + + const controller = function($element) { + // <input> element ref needed to add indeterminate state + let inputEl; + + /** + * Callback when the input select value changes + */ + this.onSelectChange = () => { + const checked = this.ngModel; + this.emitChange({checked}); + }; + + /** + * onChanges Component lifecycle hook + */ + this.$onChanges = (onChangesObj) => { + const indeterminateChange = onChangesObj.indeterminate; + if (indeterminateChange && inputEl) { + inputEl.prop('indeterminate', this.indeterminate); + } + }; + + /** + * postLink Component lifecycle hook + */ + this.$postLink = () => { + inputEl = $element.find('input'); + }; + }; + + /** + * Component template + */ + const template = ` + <div class="bmc-table__checkbox-container"> + <label class="bmc-table__checkbox" + ng-class="{ + 'checked': $ctrl.ngModel, + 'indeterminate': $ctrl.indeterminate + }"> + <input type="checkbox" + class="bmc-table__checkbox-input" + ng-model="$ctrl.ngModel" + ng-change="$ctrl.onSelectChange()" + aria-label="Select row"/> + <span class="screen-reader-offscreen">Select row</span> + </label> + </div>` + + /** + * Register tableCheckbox component + */ + angular.module('app.common.components').component('tableCheckbox', { + controller: ['$element', controller], + template, + bindings: {ngModel: '=', indeterminate: '<', emitChange: '&'} + }) +})(window.angular);
\ No newline at end of file diff --git a/app/common/components/table/table-toolbar.js b/app/common/components/table/table-toolbar.js new file mode 100644 index 0000000..830bbd1 --- /dev/null +++ b/app/common/components/table/table-toolbar.js @@ -0,0 +1,112 @@ +window.angular && (function(angular) { + 'use strict'; + + /** + * + * tableToolbar Component + * + * To use: + * The <table-toolbar> component expects an 'actions' attribute + * that should be an array of action objects. + * Each action object should have 'type', 'label', and 'file' + * properties. + * + * actions: [ + * {type: 'Edit', label: 'Edit', file: 'icon-edit.svg'}, + * {type: 'Delete', label: 'Edit', file: 'icon-trashcan.svg'} + * ] + * + * The 'type' property is a string value that will be emitted to the + * parent component when clicked. + * + * The 'label' property is a string value that will render as text in + * the button + * + * The 'file' property is a string value of the filename of the svg icon + * to provide <icon> directive. + * + */ + + const controller = function() { + /** + * Set defaults if properties undefined + * @param {[]} actions + */ + const setActions = (actions = []) => { + return actions + .map((action) => { + if (action.type === undefined) { + return; + } + if (action.file === undefined) { + action.file = null; + } + return action; + }) + .filter((action) => action); + }; + + /** + * Callback when button action clicked + * Emits the action type to the parent component + */ + this.onClick = (action) => { + this.emitAction({action}); + }; + + this.onClickClose = () => { + this.emitClose(); + }; + + /** + * onInit Component lifecycle hook + */ + this.$onInit = () => { + this.actions = setActions(this.actions); + }; + }; + + /** + * Component template + */ + const template = ` + <div class="bmc-table__toolbar" ng-if="$ctrl.active"> + <p class="toolbar__selection-count">{{$ctrl.selectionCount}} selected</p> + <div class="toolbar__batch-actions" ng-show="$ctrl.actions.length > 0"> + <button + class="btn btn-tertiary" + type="button" + aria-label="{{action.label}}" + ng-repeat="action in $ctrl.actions" + ng-click="$ctrl.onClick(action.type)"> + <icon ng-if="action.file !== null" + ng-file="{{action.file}}" + aria-hidden="true"> + </icon> + {{action.label || action.type}} + </button> + <button + class="btn btn-tertiary btn-close" + type="button" + aria-label="Cancel" + ng-click="$ctrl.onClickClose()"> + Cancel + </button> + </div> + </div>` + + /** + * Register tableToolbar component + */ + angular.module('app.common.components').component('tableToolbar', { + controller, + template, + bindings: { + actions: '<', + selectionCount: '<', + active: '<', + emitAction: '&', + emitClose: '&' + } + }) +})(window.angular);
\ No newline at end of file diff --git a/app/common/components/table/table.html b/app/common/components/table/table.html index 96ca870..7d906a1 100644 --- a/app/common/components/table/table.html +++ b/app/common/components/table/table.html @@ -1,94 +1,131 @@ -<table class="bmc-table {{$ctrl.size}}"> - <thead class="bmc-table__head"> - <!-- Header row (non-sortable) --> - <tr ng-if="!$ctrl.sortable"> - <th ng-repeat="headerItem in $ctrl.header" - class="bmc-table__column-header"> - {{headerItem.label}} - </th> - </tr> - <!-- Header row (sortable) --> - <tr ng-if="$ctrl.sortable"> - <th ng-repeat="headerItem in $ctrl.header track by $index" - class="bmc-table__column-header"> - <span ng-if="!headerItem.sortable"> - {{headerItem.label}} - </span> - <span ng-if="headerItem.sortable" - ng-click="$ctrl.onClickSort($index)" - class="bmc-table__column-header--sortable"> - {{headerItem.label}} - <!-- Sort icons --> - <button class="sort-icon" - type="button" - aria-label="sort {{headerItem.label}}"> - <icon file="icon-arrow--up.svg" - ng-if="$index === $ctrl.activeSort" - ng-class="{ - 'sort-icon--descending': !$ctrl.sortAscending, - 'sort-icon--ascending' : $ctrl.sortAscending }" - class="sort-icon--active" - aria-hidden="true"></icon> - <span ng-if="$index !== $ctrl.activeSort" - class="sort-icon--inactive" - aria-hidden="true"> - <icon file="icon-arrow--up.svg"></icon> - <icon file="icon-arrow--down.svg"></icon> - </span> +<div class="bmc-table__container"> + <table-toolbar ng-if="$ctrl.selectable" + selection-count="$ctrl.selectedRows.size" + active="$ctrl.selectedRows.size > 0" + actions="$ctrl.batchActions" + emit-action="$ctrl.onEmitBatchAction(action)" + emit-close="$ctrl.onToolbarClose()" + ng-animate-children="true"> + </table-toolbar> + <table class="bmc-table {{$ctrl.size}}" + ng-class="{ + 'bmc-table--sortable': $ctrl.sortable, + 'bmc-table--expandable': $ctrl.expandable, + 'bmc-table--selectable': $ctrl.selectable, + 'bmc-table--row-actions-enabled': '$ctrl.rowActionsEnabled', + }"> + <thead class="bmc-table__head"> + <!-- Header row --> + <tr> + <!-- Expandable empty cell --> + <th ng-if="$ctrl.expandable" + class="bmc-table__column-header"></th> + <!-- Select checkbox --> + <th ng-if="$ctrl.selectable" + class="bmc-table__column-header"> + <table-checkbox ng-model="$ctrl.selectHeaderCheckbox" + indeterminate="$ctrl.someSelected" + emit-change="$ctrl.onHeaderSelectChange(checked)"> + </table-checkbox> + </th> + <!-- Header items --> + <th ng-repeat="headerItem in $ctrl.header track by $index" + class="bmc-table__column-header"> + <span ng-if="!$ctrl.sortable || !headerItem.sortable"> + {{headerItem.label}} + </span> + <span ng-if="$ctrl.sortable && headerItem.sortable" + ng-click="$ctrl.onClickSort($index)" + class="bmc-table__column-header--sortable"> + {{headerItem.label}} + <!-- Sort icons --> + <button class="sort-icon" + type="button" + aria-label="sort {{headerItem.label}}"> + <icon file="icon-arrow--up.svg" + ng-if="$index === $ctrl.activeSort" + ng-class="{ + 'sort-icon--descending': !$ctrl.sortAscending, + 'sort-icon--ascending' : $ctrl.sortAscending }" + class="sort-icon--active" + aria-hidden="true"></icon> + <span ng-if="$index !== $ctrl.activeSort" + class="sort-icon--inactive" + aria-hidden="true"> + <icon file="icon-arrow--up.svg"></icon> + <icon file="icon-arrow--down.svg"></icon> + </span> + </button> + </span> + </th> + <!-- Row actions empty cell --> + <th ng-if="$ctrl.rowActionsEnabled" + class="bmc-table__column-header"></th> + </tr> + </thead> + <tbody class="bmc-table__body"> + <!-- Data rows --> + <tr ng-if="$ctrl.data.length > 0" + ng-repeat-start="row in $ctrl.data track by $index" + class="bmc-table__row" + ng-class="{ + 'bmc-table__row--expanded': $ctrl.expandedRows.has($index), + 'bmc-table__row--selected': $ctrl.selectedRows.has($index), + }"> + <!-- Row expansion trigger --> + <td ng-if="$ctrl.expandable" + class="bmc-table__cell"> + <button type="button" + class="btn btn--expand" + aria-label="expand row" + ng-click="$ctrl.onClickExpand($index)"> + <icon file="icon-chevron-right.svg" aria-hidden="true"></icon> </button> - </span> - </th> - </tr> - </thead> - <tbody class="bmc-table__body"> - <!-- Data rows --> - <tr ng-if="$ctrl.data.length > 0" - ng-repeat-start="row in $ctrl.data track by $index" - class="bmc-table__row" - ng-class="{ - 'bmc-table__row--expanded': $ctrl.expandedRows.has($index) - }"> - <!-- Row expansion trigger --> - <td ng-if="$ctrl.expandable" - class="bmc-table__cell"> - <button type="button" - class="btn btn--expand" - aria-label="expand row" - ng-click="$ctrl.onClickExpand($index)"> - <icon file="icon-chevron-right.svg" aria-hidden="true"></icon> - </button> - </td> - <!-- Row item --> - <td ng-repeat="item in row.uiData track by $index" - class="bmc-table__cell"> - <ng-bind-html ng-bind-html="item"></ng-bind-html> - </td> - <!-- Row Actions --> - <td ng-if="$ctrl.rowActionsEnabled" - class="bmc-table__cell bmc-table__row-actions"> - <table-actions - actions="row.actions" - emit-action="$ctrl.onEmitTableAction(action, row)"> - </table-actions> - </td> - </tr> - <!-- Expansion row --> - <tr ng-repeat-end - ng-if="$ctrl.expandedRows.has($index)" - class="bmc-table__expansion-row"> - <td class="bmc-table__cell"></td> - <td class="bmc-table__cell" - colspan="{{$ctrl.header.length - 1}}"> - <ng-bind-html - ng-bind-html="row.expandContent || 'No data'"> - </ng-bind-html> - </td> - </tr> - <!-- Empty table --> - <tr ng-if="$ctrl.data.length === 0" - class="bmc-table__expansion-row"> - <td class="bmc-table__cell" - colspan="{{$ctrl.header.length}}">No data</td> - </tr> - </tbody> -</table>
\ No newline at end of file + </td> + <!-- Row checkbox --> + <td ng-if="$ctrl.selectable" + class="bmc-table__cell"> + <table-checkbox ng-if="row.selectable" + ng-model="row.selected" + emit-change="$ctrl.onRowSelectChange($index)"> + </table-checkbox> + </td> + <!-- Row item --> + <td ng-repeat="item in row.uiData track by $index" + class="bmc-table__cell"> + <ng-bind-html ng-bind-html="item"></ng-bind-html> + </td> + <!-- Row Actions --> + <td ng-if="$ctrl.rowActionsEnabled" + class="bmc-table__cell bmc-table__row-actions"> + <table-actions + actions="row.actions" + emit-action="$ctrl.onEmitRowAction(action, row)"> + </table-actions> + </td> + </tr> + <!-- Expansion row --> + <tr ng-repeat-end + ng-if="$ctrl.expandedRows.has($index)" + class="bmc-table__expansion-row"> + <td class="bmc-table__cell"></td> + <td class="bmc-table__cell" + colspan="{{$ctrl.header.length + $ctrl.sortable + + $ctrl.expandable + $ctrl.rowActionsEnabled}}"> + <ng-bind-html + ng-bind-html="row.expandContent || 'No data'"> + </ng-bind-html> + </td> + </tr> + <!-- Empty table --> + <tr ng-if="$ctrl.data.length === 0" + class="bmc-table__expansion-row"> + <td class="bmc-table__cell" + colspan="{{$ctrl.header.length + $ctrl.sortable + + $ctrl.expandable + $ctrl.rowActionsEnabled}}"> + No data + </td> + </tr> + </tbody> + </table> +</div>
\ No newline at end of file diff --git a/app/common/components/table/table.js b/app/common/components/table/table.js index 5db05b6..2063555 100644 --- a/app/common/components/table/table.js +++ b/app/common/components/table/table.js @@ -18,9 +18,12 @@ window.angular && (function(angular) { * Each row object can optionally have an 'expandContent' property * that should be a string value and can contain valid HTML. To render * the expanded content, set 'expandable' attribute to true. + * Each row object can optionally have a 'selectable' property. Defaults + * to true if table is selectable. If a particular row should not + * be selectable, set to false. * * data = [ - * { uiData: ['root', 'Admin', 'enabled' ] }, + * { uiData: ['root', 'Admin', 'enabled' ], selectable: false }, * { uiData: ['user1', 'User', 'disabled' ] } * ] * @@ -47,6 +50,10 @@ window.angular && (function(angular) { * The 'expandable' attribute should be a boolean value. If true each * row object in data array should contain a 'expandContent' property * + * The 'selectable' attribute should be a boolean value. + * If 'selectable' is true, include 'batch-actions' property that should + * be an array of actions to provide <table-toolbar> component. + * * The 'size' attribute which can be set to 'small' which will * render a smaller font size in the table. * @@ -56,6 +63,11 @@ window.angular && (function(angular) { this.sortAscending = true; this.activeSort; this.expandedRows = new Set(); + this.selectedRows = new Set(); + this.selectHeaderCheckbox = false; + this.someSelected = false; + + let selectableRowCount = 0; /** * Sorts table data @@ -89,34 +101,99 @@ window.angular && (function(angular) { return column; }) } - if (this.rowActionsEnabled) { - // If table actions are enabled push an empty - // string to the header array to account for additional - // table actions cell - this.header.push({label: '', sortable: false}); - } - if (this.expandable) { - // If table is expandable, push an empty string to the - // header array to account for additional expansion cell - this.header.unshift({label: '', sortable: false}); + }; + + /** + * Prep data + * When data binding changes, make adjustments to account for + * optional configurations and undefined values + */ + const prepData = () => { + selectableRowCount = 0; + this.data.forEach((row) => { + if (row.uiData === undefined) { + // Check for undefined 'uiData' property for each item in data + // array + row.uiData = []; + } + if (this.selectable) { + // If table is selectable check row for 'selectable' property + row.selectable = row.selectable === undefined ? true : row.selectable; + if (row.selectable) { + selectableRowCount++; + row.selected = false; + } + } + }); + if (this.sortable) { + if (this.activeSort !== undefined || this.defaultSort !== undefined) { + // apply default or active sort if one is defined + this.activeSort = this.defaultSort !== undefined ? this.defaultSort : + this.activeSort; + sortData(); + } } }; /** + * Select all rows + * Sets each selectable row selected property to true + * and adds index to selectedRow Set + */ + const selectAllRows = () => { + this.selectHeaderCheckbox = true; + this.someSelected = false; + this.data.forEach((row, index) => { + if (!row.selected && row.selectable) { + row.selected = true; + this.selectedRows.add(index); + } + }) + }; + + /** + * Deselect all rows + * Sets each row selected property to false + * and clears selectedRow Set + */ + const deselectAllRows = () => { + this.selectHeaderCheckbox = false; + this.someSelected = false; + this.selectedRows.clear(); + this.data.forEach((row) => { + if (row.selectable) { + row.selected = false; + } + }) + }; + + /** * Callback when table row action clicked * Emits user desired action and associated row data to * parent controller * @param {string} action : action type * @param {any} row : user object */ - this.onEmitTableAction = (action, row) => { + this.onEmitRowAction = (action, row) => { if (action !== undefined && row !== undefined) { const value = {action, row}; - this.emitAction({value}); + this.emitRowAction({value}); } }; /** + * Callback when batch action clicked from toolbar + * Emits the action type and the selected row data to + * parent controller + * @param {string} action : action type + */ + this.onEmitBatchAction = (action) => { + const filteredRows = this.data.filter((row) => row.selected); + const value = {action, filteredRows}; + this.emitBatchAction({value}); + }; + + /** * Callback when sortable table header clicked * @param {number} index : index of header item */ @@ -146,6 +223,49 @@ window.angular && (function(angular) { }; /** + * Callback when select checkbox clicked + * @param {number} row : index of selected row + */ + this.onRowSelectChange = (row) => { + if (this.selectedRows.has(row)) { + this.selectedRows.delete(row); + } else { + this.selectedRows.add(row); + } + if (this.selectedRows.size === 0) { + this.someSelected = false; + this.selectHeaderCheckbox = false; + deselectAllRows(); + } else if (this.selectedRows.size === selectableRowCount) { + this.someSelected = false; + this.selectHeaderCheckbox = true; + selectAllRows(); + } else { + this.someSelected = true; + } + }; + + /** + * Callback when header select box value changes + */ + this.onHeaderSelectChange = (checked) => { + this.selectHeaderCheckbox = checked; + if (this.selectHeaderCheckbox) { + selectAllRows(); + } else { + deselectAllRows(); + } + }; + + /** + * Callback when cancel/close button closed + * from toolbar + */ + this.onToolbarClose = () => { + deselectAllRows(); + }; + + /** * onInit Component lifecycle hook * Checking for undefined values */ @@ -157,32 +277,19 @@ window.angular && (function(angular) { this.rowActionsEnabled === undefined ? false : this.rowActionsEnabled; this.size = this.size === undefined ? '' : this.size; this.expandable = this.expandable === undefined ? false : this.expandable; - - // Check for undefined 'uiData' property for each item in data array - this.data = this.data.map((row) => { - if (row.uiData === undefined) { - row.uiData = []; - } - return row; - }) + this.selectable = this.selectable === undefined ? false : this.selectable; prepTable(); }; /** * onChanges Component lifecycle hook - * Check for changes in the data array and apply - * default or active sort if one is defined */ this.$onChanges = (onChangesObj) => { const dataChange = onChangesObj.data; if (dataChange) { - if (this.activeSort !== undefined || this.defaultSort !== undefined) { - this.activeSort = this.defaultSort !== undefined ? this.defaultSort : - this.activeSort; - sortData(); - } + prepData(); } - } + }; }; /** @@ -199,7 +306,10 @@ window.angular && (function(angular) { sortable: '<', // boolean defaultSort: '<', // number (index of sort) expandable: '<', // boolean - emitAction: '&' + selectable: '<', // boolean + batchActions: '<', // Array + emitRowAction: '&', + emitBatchAction: '&' } }) })(window.angular); diff --git a/app/common/styles/base/variables.scss b/app/common/styles/base/variables.scss index 0e09f86..55a3eed 100755 --- a/app/common/styles/base/variables.scss +++ b/app/common/styles/base/variables.scss @@ -3,4 +3,12 @@ $duration--fast-02: 110ms; //Micro-interactions such as fade $duration--moderate-01: 150ms; //Micro-interactions, small expansion, short distance movements $duration--moderate-02: 240ms; //Expansion, system communication, toast $duration--slow-01: 400ms; //Large expansion, important system notifications -$duration--slow-02: 700ms; //Background dimming
\ No newline at end of file +$duration--slow-02: 700ms; //Background dimming + +// https://www.carbondesignsystem.com/guidelines/motion/basics/#easing +$standard-easing--productive: cubic-bezier(0.2, 0, 0.38, 0.9); +$standard-easing--expressive: cubic-bezier(0.4, 0.14, 0.3, 1); +$entrance-easing--productive: cubic-bezier(0, 0, 0.38, 0.9); +$entrance-easing--expressive: cubic-bezier(0, 0, 0.3, 1); +$exit-easing--productive: cubic-bezier(0.2, 0, 1, 0.9); +$exit-easing--expressive: cubic-bezier(0.4, 0.14, 1, 1);
\ No newline at end of file diff --git a/app/common/styles/components/table.scss b/app/common/styles/components/table.scss index 0178486..5aa4915 100644 --- a/app/common/styles/components/table.scss +++ b/app/common/styles/components/table.scss @@ -147,6 +147,10 @@ } } +.bmc-table__container { + position: relative; +} + .bmc-table { width: 100%; &.small { @@ -223,4 +227,126 @@ .bmc-table__row-actions { text-align: right; +} + +.bmc-table__checkbox-container { + position: relative; + display: inline-block; + width: 1rem; + height: 1rem; +} + +.bmc-table__checkbox { + margin: 0; + line-height: 1; + position: absolute; + width: 1rem; + height: 1rem; + top: 0; + right: 0; + cursor: pointer; + &::before { + // checkbox border + box-sizing: border-box; + content: ""; + width: 1rem; + height: 1rem; + position: absolute; + left: 0; + top: 0.15rem; + background-color: transparent; + border: 2px solid $border-color-02; + border-radius: 1px; + } + &::after { + // checkbox check mark + content: ""; + position: absolute; + left: 0.2rem; + top: 0.15rem; + width: 0.6rem; + height: 0.3rem; + background: none; + border-left: 2px solid $primary-light; + border-bottom: 2px solid $primary-light; + transform: scale(0) rotate(-45deg); + transform-origin: bottom right; + } + &.checked::before , + &.indeterminate::before { + background: $primary-accent; + border-color: $primary-accent; + } + &.checked::after { + transform: scale(1) rotate(-45deg); + } + &.indeterminate::after { + transform: scale(1) rotate(0deg); + border-left: 0 solid $primary-light; + border-bottom: 2px solid $primary-light; + top: 0.4rem; + } +} + +.bmc-table__checkbox-input { + position: absolute; + opacity: 0; + height: 0; + width: 0; + margin: 0; +} + +.bmc-table__toolbar { + position: absolute; + left: 0; + right: 1px; + display: flex; + flex-direction: row; + justify-content: space-between; + color: $text-03; + background-color: $primary-accent; + max-height: 40px; + padding-left: 1em; + padding-top: 0.5em; + padding-right: 0; + padding-bottom: 0.5em; + transform: translateY(-40px); + &.ng-animate { + transition: transform $duration--moderate-02 $standard-easing--productive; + } + &.ng-enter { + transform: translateY(0); + } + &.ng-enter.ng-enter-active { + transform: translateY(-40px); + } + &.ng-leave { + transform: translateY(-40px); + } + &.ng-leave.ng-leave-active { + transform: translateY(0); + } + .btn-tertiary { + color: $text-03; + padding-top:0; + padding-bottom:0; + position: relative; + .icon { + fill: $text-03; + margin: 0; + } + } +} + +.toolbar__batch-actions { + .btn-close { + border-top: none; + border-bottom: none; + border-left: 2px solid $primary-light; + margin-left: 0.5em; + } +} + +.toolbar__selection-count { + margin: 0; }
\ No newline at end of file |