diff --git a/toolkits/nature/packages/nature-sort-by/HISTORY.md b/toolkits/nature/packages/nature-sort-by/HISTORY.md index 3969878f0..f2e57ebf9 100644 --- a/toolkits/nature/packages/nature-sort-by/HISTORY.md +++ b/toolkits/nature/packages/nature-sort-by/HISTORY.md @@ -1,5 +1,8 @@ # History +## 3.0.0 (2022-02-14) + * BREAKING: Performs a complete rewrite of the component. Component no longer is a dropdown, now simpler implementation with radio buttons in a form. + ## 2.1.0 (2022-02-02) * FEATURE: add view template and demo @@ -8,6 +11,5 @@ ## 1.1.0 (2021-06-30) * Ensures radio is checked before page reloads - ## 1.0.0 (2021-05-19) * Initial commit diff --git a/toolkits/nature/packages/nature-sort-by/README.md b/toolkits/nature/packages/nature-sort-by/README.md index f12a9192b..675f9382e 100644 --- a/toolkits/nature/packages/nature-sort-by/README.md +++ b/toolkits/nature/packages/nature-sort-by/README.md @@ -2,7 +2,7 @@ [![NPM version][badge-npm]][info-npm] -A sort by dropdown for use on Nature product pages that contain a sortable list of content such as search results pages. +The Nature Sort by component lets users change the order of a list of content items. For example, to sort search results by date or relevance. ## Installation @@ -25,34 +25,17 @@ sortBy(); ```scss @import '@springernature/nature-sort-by/scss/10-settings/sort-by'; @import '@springernature/nature-sort-by/scss/50-components/sort-by'; - -@import '@springernature/brand-context/default/scss/60-utilities/flex.scss'; -@import '@springernature/brand-context/default/scss/60-utilities/hiding.scss'; -@import '@springernature/brand-context/default/scss/60-utilities/spacing.scss'; - ``` -> **NOTE** The component require the use of the utility classes shown above +Insert the component's HTML inside an HTML form in your application. See an example [template](#template) below. ## Configuration -To configure the component the following html attributes will need to be set: - -| Name | Description | -|------------------------|---------------------------------------------------------------------------------------------| -| data-sort-by-trigger | This should be set on the html element that is clicked in order to open the dropdown menu | -| data-sort-by-target | This should be set on the containing html element of the sort by dropdown menu | -| data-sort-by-radio | This should be set on each list item within the sort by dropdown menu | -| { + beforeEach(() => { + document.body.innerHTML = ` +
+
+ Sort by: + + + + + + + +
+
+ `; + }); + + test('Should submit form if radio input checked', () => { + const form = document.querySelector('form'); + const spy = jest.fn(); + form.addEventListener('submit', event => { + event.preventDefault(); + spy(); + }); + sortBy(); + const input = document.querySelector('input'); + input.dispatchEvent(new Event('change')); + expect(spy).toHaveBeenCalled(); + }); +}); diff --git a/toolkits/nature/packages/nature-sort-by/__tests__/sort-by.spec.js b/toolkits/nature/packages/nature-sort-by/__tests__/sort-by.spec.js deleted file mode 100644 index 164653245..000000000 --- a/toolkits/nature/packages/nature-sort-by/__tests__/sort-by.spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import {sortBy} from '../js/sort-by'; - -describe('sortBy', () => { - const element = {}; - - beforeEach(() => { - document.body.innerHTML = ` -
- - Sort by trigger - -
- -
- -
- `; - - element.target1 = document.querySelector('#sort-by'); - - element.button1 = document.querySelector('[data-sort-by-trigger]'); - }); - - afterEach(() => { - document.body.innerHTML = ''; - }); - - test('should set role for button', () => { - // Given - sortBy(); - // Then - expect(element.button1.getAttribute('role')).toBe('button'); - }); - - test('should append target after trigger', () => { - // Given - sortBy(); - // When - element.targetAfterButton1 = element.button1.nextSibling - // Then - expect(element.targetAfterButton1).toBe(element.target1); - }); -}); diff --git a/toolkits/nature/packages/nature-sort-by/demo/context.json b/toolkits/nature/packages/nature-sort-by/demo/context.json index 9b987fa27..eca65ec9c 100644 --- a/toolkits/nature/packages/nature-sort-by/demo/context.json +++ b/toolkits/nature/packages/nature-sort-by/demo/context.json @@ -3,15 +3,20 @@ { "value": "relevance", "label": "Relevance", - "checked": true + "name": "order", + "checked": false }, { "value": "desc", - "label": "Most recent" - }, + "label": "Date published (new to old)", + "name": "order", + "checked": true + }, { "value": "asc", - "label": "Oldest first" + "label": "Date published (old to new)", + "name": "order", + "checked": false } ], "dynamicPartials": { diff --git a/toolkits/nature/packages/nature-sort-by/demo/dist/index.html b/toolkits/nature/packages/nature-sort-by/demo/dist/index.html index 6149b74b2..49b10c565 100644 --- a/toolkits/nature/packages/nature-sort-by/demo/dist/index.html +++ b/toolkits/nature/packages/nature-sort-by/demo/dist/index.html @@ -3,6 +3,7 @@ (function(e){var t=e.documentElement,n=e.implementation;t.className='js';})(document) + @springernature/nature-sort-by -
- -
- -
- -
-
-
- function _defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } - } - - function _createClass(Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps); - if (staticProps) _defineProperties(Constructor, staticProps); - Object.defineProperty(Constructor, "prototype", { - writable: false - }); - return Constructor; - } - - var makeArray = function makeArray(iterable) { - var list = []; - - if (!iterable) { - return list; - } - - Array.prototype.forEach.call(iterable, function (item) { - list.push(item); - }); - return list; - }; - - /** - * create-event.js - * Wrapper for customEvent - * Generate namespaced events - */ - /** - * Polyfill customEvent - * @function customEvent - * @param {String} eventName namespaced name of the event - * @param {Object=} _parameters customEvent options - * @return {Event} - */ - - var customEvent = function customEvent(eventName, _parameters) { - _parameters = _parameters || { - bubbles: false, - cancelable: false, - detail: null - }; - var event = document.createEvent('CustomEvent'); - event.initCustomEvent(eventName, _parameters.bubbles, _parameters.cancelable, _parameters.detail); - return event; - }; - /** - * Create a custom event - * @function createEvent - * @param {String} eventName name of the event - * @param {String} namespace namespace e.g. component name - * @param {Object=} _parameters customEvent options - * @return {Event} - */ - - - var createEvent = function createEvent(eventName, namespace, _parameters) { - var event; - - if (namespace === undefined) { - throw new Error('Missing namespace in `createEvent` function'); - } - - if (typeof window.CustomEvent === 'function') { - event = new CustomEvent("".concat(namespace, ":").concat(eventName), _parameters); - } else { - event = customEvent("".concat(namespace, ":").concat(eventName), _parameters); - } - - return event; - }; - - /** - * Local Constants - */ - - var defaultOptions = { - TARGET_HIDE_CLASS: 'u-js-hide', - TRIGGER_OPEN_CLASS: 'is-open', - TRIGGER_OPEN_LABEL: undefined, - CLOSE_ON_FOCUS_OUT: true, - AUTOFOCUS: null, - OPEN_EVENT: false, - DEFAULT_OPEN: false - }; - - var Expander = /*#__PURE__*/function () { - function Expander(trigger, target) { - var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - - _classCallCheck(this, Expander); - - this._options = Object.assign({}, defaultOptions, options); - this._autoFocusElement = trigger; - this._triggerEl = trigger; - this._targetEl = target; - this._originalTriggerText = trigger.textContent; - this._isOpen = this._options.DEFAULT_OPEN; - this._handleButtonClick = this._handleButtonClick.bind(this); - this._handleButtonKeydown = this._handleButtonKeydown.bind(this); - this._handleDocumentClick = this._handleDocumentClick.bind(this); - this._handleDocumentKeydown = this._handleDocumentKeydown.bind(this); - } - /** - * Event Handlers - */ - - - _createClass(Expander, [{ - key: "_handleButtonClick", - value: function _handleButtonClick(event) { - event.preventDefault(); - - if (this._isOpen) { - this.close(); - } else { - this.open(); - } - } - }, { - key: "_handleButtonKeydown", - value: function _handleButtonKeydown(event) { - if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') { - event.preventDefault(); - - if (this._isOpen) { - this.close(); - } else { - this.open(); - } - } - } - }, { - key: "_handleDocumentKeydown", - value: function _handleDocumentKeydown(event) { - var _this = this; - - if (event.key === 'Escape') { - this.close(); - - this._triggerEl.focus(); - } - - if (this._options.CLOSE_ON_FOCUS_OUT) { - if (event.key === 'Tab' && event.shiftKey === true) { - if (event.target === this._targetTabbableItems[0] || event.target === this._triggerEl || event.target === this._targetEl) { - event.preventDefault(); - window.requestAnimationFrame(function () { - _this.close(); - - _this._triggerEl.focus(); - }); - } - } - - if (event.key === 'Tab' && event.shiftKey === false) { - if (event.target === this._targetTabbableItems[this._targetTabbableItems.length - 1]) { - event.preventDefault(); - window.requestAnimationFrame(function () { - _this.close(); - - _this._triggerEl.focus(); - }); - } - } - } - } - }, { - key: "_handleDocumentClick", - value: function _handleDocumentClick(event) { - var target = event.target; - - if (target === this._targetEl || target === this._triggerEl || this._targetEl.contains(target) || this._triggerEl.contains(target)) { - return; - } - - this.close(); - } - /** - * Temporary Event Listeners - */ - - }, { - key: "_setupTemporaryEventListeners", - value: function _setupTemporaryEventListeners() { - document.addEventListener('keydown', this._handleDocumentKeydown); - - if (this._options.CLOSE_ON_FOCUS_OUT) { - document.addEventListener('click', this._handleDocumentClick); - } - } - }, { - key: "_removeTemporaryEventListeners", - value: function _removeTemporaryEventListeners() { - document.removeEventListener('keydown', this._handleDocumentKeydown); - - if (this._options.CLOSE_ON_FOCUS_OUT) { - document.removeEventListener('click', this._handleDocumentClick); - } - } - /** - * Attributes - */ - - }, { - key: "_updateAttributes", - value: function _updateAttributes() { - // eslint-disable-next-line unicorn/consistent-function-scoping - this._triggerEl.setAttribute('aria-expanded', this._isOpen.toString()); - - if (this._isOpen) { - this._targetEl.removeAttribute('hidden'); - } else { - this._targetEl.setAttribute('hidden', ''); - } - } - /** - * Class attributes - */ - - }, { - key: "_updateClassAttributes", - value: function _updateClassAttributes() { - if (this._isOpen) { - this._triggerEl.classList.add(this._options.TRIGGER_OPEN_CLASS); - - this._targetEl.classList.remove(this._options.TARGET_HIDE_CLASS); - } else { - this._triggerEl.classList.remove(this._options.TRIGGER_OPEN_CLASS); - - this._targetEl.classList.add(this._options.TARGET_HIDE_CLASS); - } - } - /** - * Trigger Label - */ - - }, { - key: "_updateTriggerLabel", - value: function _updateTriggerLabel() { - if (this._options.TRIGGER_OPEN_LABEL) { - this._triggerEl.textContent = this._isOpen ? this._options.TRIGGER_OPEN_LABEL : this._originalTriggerText; - } - } - /** - * Tabbable Items - */ - - }, { - key: "_updateTabbableItems", - value: function _updateTabbableItems() { - this._targetTabbableItems = makeArray(this._targetEl.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')).filter(function (element) { - return window.getComputedStyle(element).getPropertyValue('visibility') !== 'hidden'; - }); - } - /** - * AutoFocus - */ - - }, { - key: "_handleAutoFocus", - value: function _handleAutoFocus() { - if (this._options.AUTOFOCUS === 'target') { - this._autoFocusElement = this._targetEl; - - this._targetEl.setAttribute('tabindex', '-1'); - } - - if (this._options.AUTOFOCUS === 'firstTabbable') { - this._autoFocusElement = this._targetTabbableItems.length > 0 && this._targetTabbableItems[0]; - - if (this._autoFocusElement.setSelectionRange) { - this._autoFocusElement.setSelectionRange(0, this._autoFocusElement.value.length); - } - } - - this._autoFocusElement.focus(); - } - /** - * @public - */ - - /** - * Toggling - */ - - }, { - key: "open", - value: function open() { - if (this._isOpen) { - return; - } - - this._isOpen = true; - - this._updateTriggerLabel(); - - this._updateAttributes(); - - this._updateClassAttributes(); - - this._updateTabbableItems(); - - this._setupTemporaryEventListeners(); - - this._handleAutoFocus(); - - if (this._options.OPEN_EVENT) { - var event = createEvent('open', 'globalExpander', { - bubbles: false - }); - - this._triggerEl.dispatchEvent(event); - } - } - }, { - key: "close", - value: function close() { - if (!this._isOpen) { - return; - } - - this._isOpen = false; - - this._updateTriggerLabel(); - - this._updateAttributes(); - - this._updateClassAttributes(); - - this._removeTemporaryEventListeners(); - } - }, { - key: "init", - value: function init() { - if (this._triggerEl.tagName === 'A' && this._triggerEl.getAttribute('href').charAt(0) === '#') { - // eslint-disable-next-line no-script-url - this._triggerEl.setAttribute('href', 'javascript:;'); - - this._triggerEl.setAttribute('role', 'button'); - } // Warn screen reader users when you are stealing focus - - - if (this._options.AUTOFOCUS) { - this._triggerEl.setAttribute('aria-haspopup', 'true'); - } - - this._updateTriggerLabel(); - - this._updateAttributes(); - - this._updateClassAttributes(); - - this._triggerEl.addEventListener('click', this._handleButtonClick); - - this._triggerEl.addEventListener('keydown', this._handleButtonKeydown); - } - }]); - - return Expander; - }(); - - var generateParameters = function generateParameters(value) { - var parameters = new URL(document.location).searchParams; - parameters.set('order', value); - return parameters; - }; - - var sortBy = function sortBy() { - var trigger = document.querySelector('[data-sort-by-trigger]'); - var targetElement = document.querySelector('[data-sort-by-target]'); - var radios = makeArray(document.querySelectorAll('[data-sort-by-radio]')); - - if (!trigger || !targetElement || !radios) { - return; - } - - radios.forEach(function (element) { - element.addEventListener('click', function () { - var value = element.querySelector('input').value; - var parameters = generateParameters(value); - window.location.replace('/search?' + parameters); - }); - }); - trigger.setAttribute('role', 'button'); - trigger.insertAdjacentElement('afterend', targetElement); - targetElement.classList.add('has-tethered'); - var expander = new Expander(trigger, targetElement); - expander.init(); - return expander; - }; - - sortBy(); + diff --git a/toolkits/nature/packages/nature-sort-by/demo/main.js b/toolkits/nature/packages/nature-sort-by/demo/main.js index d75cabd27..dfc247ec4 100644 --- a/toolkits/nature/packages/nature-sort-by/demo/main.js +++ b/toolkits/nature/packages/nature-sort-by/demo/main.js @@ -1,3 +1,3 @@ -import {sortBy} from '../js/sort-by'; +import {sortBy} from '../js/sort-by.js'; sortBy(); diff --git a/toolkits/nature/packages/nature-sort-by/js/sort-by.js b/toolkits/nature/packages/nature-sort-by/js/sort-by.js index 6081939e7..1a88970f1 100644 --- a/toolkits/nature/packages/nature-sort-by/js/sort-by.js +++ b/toolkits/nature/packages/nature-sort-by/js/sort-by.js @@ -1,39 +1,20 @@ -import {Expander} from '@springernature/global-expander/js/expander'; import {makeArray} from '@springernature/global-javascript/src/helpers'; -const generateParameters = value => { - const parameters = new URL(document.location).searchParams; - parameters.set('order', value); - return parameters; -}; - -const sortBy = () => { - const trigger = document.querySelector('[data-sort-by-trigger]'); - const targetElement = document.querySelector('[data-sort-by-target]'); - const radios = makeArray(document.querySelectorAll('[data-sort-by-radio]')); - - if (!trigger || !targetElement || !radios) { - return; - } - - radios.forEach(element => { - element.addEventListener('click', () => { - const value = element.querySelector('input').value; - const parameters = generateParameters(value); - window.location.replace('/search?' + parameters); +function addInputListeners(inputs, submitButton) { + inputs.forEach(input => { + input.addEventListener('change', () => { + submitButton.click(); }); }); - - trigger.setAttribute('role', 'button'); - - trigger.insertAdjacentElement('afterend', targetElement); - targetElement.classList.add('has-tethered'); - - const expander = new Expander(trigger, targetElement); - - expander.init(); - - return expander; -}; +} + +function sortBy() { + const forms = makeArray(document.querySelectorAll('[data-sort-by]')); + forms.forEach(form => { + const radioInputs = makeArray(form.querySelectorAll('[data-sort-by-input]')); + const submitButton = form.querySelector('[data-sort-by-button]'); + addInputListeners(radioInputs, submitButton); + }); +} export {sortBy}; diff --git a/toolkits/nature/packages/nature-sort-by/package.json b/toolkits/nature/packages/nature-sort-by/package.json index ab6aac10a..70858cb61 100644 --- a/toolkits/nature/packages/nature-sort-by/package.json +++ b/toolkits/nature/packages/nature-sort-by/package.json @@ -1,13 +1,13 @@ { "name": "@springernature/nature-sort-by", - "version": "2.1.0", + "version": "3.0.0", "license": "MIT", "description": "Nature sort by ui dropdown", "keywords": [], "author": "Springer Nature", - "brandContext": "^18.0.0", + "brandContext": "^20.0.0", "dependencies": { - "@springernature/global-javascript": "^3.0.2", - "@springernature/global-expander": "^4.0.2" + "@springernature/global-javascript": "^3.0.3", + "@springernature/global-expander": "^4.2.1" } } diff --git a/toolkits/nature/packages/nature-sort-by/scss/10-settings/sort-by.scss b/toolkits/nature/packages/nature-sort-by/scss/10-settings/sort-by.scss index 413066bce..f10e0e824 100644 --- a/toolkits/nature/packages/nature-sort-by/scss/10-settings/sort-by.scss +++ b/toolkits/nature/packages/nature-sort-by/scss/10-settings/sort-by.scss @@ -1,10 +1,3 @@ -$sort-by--menu-background-colour: #000; -$sort-by--menu-border-colour: greyscale(5); -$sort-by--menu-text-colour: greyscale(90); -$sort-by--menu-padding: spacing(16) 0; -$sort-by--menu-font-size: .875rem; -$sort-by--menu-line-height: 1.2; -$sort-by--button-font-weight: 700; -$sort-by--button-svg-margin: spacing(16); -$sort-by--button-list-item-spacing: spacing(8) spacing(16); -$sort-by--menu-min-width: 180px; +$sort-by--font-size: $context--font-size-xs; +$sort-by--line-height: $context--line-height-tight; +$sort-by--heading-font-weight: $context--font-weight-bold; diff --git a/toolkits/nature/packages/nature-sort-by/scss/50-components/sort-by.scss b/toolkits/nature/packages/nature-sort-by/scss/50-components/sort-by.scss index c04ee1c68..f38026f2b 100644 --- a/toolkits/nature/packages/nature-sort-by/scss/50-components/sort-by.scss +++ b/toolkits/nature/packages/nature-sort-by/scss/50-components/sort-by.scss @@ -1,69 +1,59 @@ .c-sort-by { - position: relative; + font-size: $sort-by--font-size; + line-height: $sort-by--line-height; +} - a { - @include u-link-inherit(); - } +.c-sort-by__heading { + font-weight: $sort-by--heading-font-weight; + margin-bottom: spacing(8); } -.c-sort-by__menu { - background-color: $sort-by--menu-background-colour; - border-bottom: 1px solid $sort-by--menu-border-colour; // non-js - color: $sort-by--menu-text-colour; - padding: $sort-by--menu-padding; - font-size: $sort-by--menu-font-size; - line-height: $sort-by--menu-line-height; +.c-sort-by__input-container { + margin-bottom: spacing(8); } -.c-sort-by__button { - font-weight: $sort-by--button-font-weight; - display: flex; - justify-content: space-between; - align-items: center; +[class~='c-sort-by__input-container']:last-of-type { + margin-bottom: 0; +} - > svg { - margin-left: $sort-by--button-svg-margin; - transition-duration: .2s; - } +.c-sort-by__input { + vertical-align: middle; + margin-top: -1px; +} - &.is-open > svg { - transform: rotate(180deg); - } +.c-sort-by__button { + margin-top: spacing(8); } -.c-sort-by__list { - @include u-list-reset; - padding: 0; - margin: 0; +.js .c-sort-by__button { + display: none; } -.c-sort-by__list-item { - padding: $sort-by--button-list-item-spacing; +@include media-query('md') { + .c-sort-by { + display: flex; + align-items: center; + } - & > label { - cursor: pointer; + .c-sort-by__heading { + float: left; // needed because browsers have special rendering rules for legend elements: https://stackoverflow.com/questions/5818960/why-wont-my-legend-element-display-inline + margin-bottom: 0; } -} -/** - JavaScript Enhancements - */ + .c-sort-by__input-container { + margin-bottom: 0; + } -.c-sort-by__menu.has-tethered { - position: absolute; - top: 100%; // bottom of the tether element - transform: translateY(4px); - z-index: 1; - left: 0; - width: 100%; - border-radius: 0 0 2px 2px; - border-bottom: 0; + .c-sort-by__input { + margin-left: spacing(16); + } - @include media-query('sm') { - width: auto; + .c-sort-by__label { + padding-left: spacing(4); } - @include media-query('md') { - min-width: $sort-by--menu-min-width; + .c-sort-by__button { + margin-left: spacing(16); + margin-top: 0; } } diff --git a/toolkits/nature/packages/nature-sort-by/view/sort-by.hbs b/toolkits/nature/packages/nature-sort-by/view/sort-by.hbs index 89c5c7e8f..f58cc157e 100644 --- a/toolkits/nature/packages/nature-sort-by/view/sort-by.hbs +++ b/toolkits/nature/packages/nature-sort-by/view/sort-by.hbs @@ -1,28 +1,10 @@ -
- -
- -
- -
+
+ Sort by: + {{#each sortOrderFilter as |listElement|}} +
+ +
-
\ No newline at end of file + {{/each}} + +