From 3496adb519699fac700e6edd6b5ac86e694498db Mon Sep 17 00:00:00 2001 From: Morgan CUGERONE Date: Wed, 17 May 2023 09:09:04 +0200 Subject: [PATCH 1/2] [MC,AK] Add a SN branded segmented controls comp Initial commit This is taken of a component designed by the Pegasus Team for the Research Impacts application. It has been, in the end, trade with another component (underline-nav), but it seemed to have raised interest lately in the FED discipline and therefore maybe worth while getting into Elements Design System. Document is still Work In Progress! Co-authored-by: Morgan Cugerone Co-authored-by: Andreas Koscinski --- .../HISTORY.md | 4 + .../README.md | 45 + .../demo/context.json | 91 ++ .../demo/dist/index.html | 1310 +++++++++++++++++ .../demo/index.hbs | 36 + .../demo/main.js | 11 + .../demo/main.scss | 9 + .../js/__tests__/segmented-controls.test.js | 81 + .../js/index.js | 51 + .../package.json | 9 + .../scss/50-components/_enhanced.scss | 113 ++ .../view/segmentedControls.hbs | 13 + 12 files changed, 1773 insertions(+) create mode 100644 toolkits/springernature/packages/springernature-segmented-controls/HISTORY.md create mode 100644 toolkits/springernature/packages/springernature-segmented-controls/README.md create mode 100644 toolkits/springernature/packages/springernature-segmented-controls/demo/context.json create mode 100644 toolkits/springernature/packages/springernature-segmented-controls/demo/dist/index.html create mode 100644 toolkits/springernature/packages/springernature-segmented-controls/demo/index.hbs create mode 100644 toolkits/springernature/packages/springernature-segmented-controls/demo/main.js create mode 100644 toolkits/springernature/packages/springernature-segmented-controls/demo/main.scss create mode 100644 toolkits/springernature/packages/springernature-segmented-controls/js/__tests__/segmented-controls.test.js create mode 100644 toolkits/springernature/packages/springernature-segmented-controls/js/index.js create mode 100644 toolkits/springernature/packages/springernature-segmented-controls/package.json create mode 100644 toolkits/springernature/packages/springernature-segmented-controls/scss/50-components/_enhanced.scss create mode 100644 toolkits/springernature/packages/springernature-segmented-controls/view/segmentedControls.hbs diff --git a/toolkits/springernature/packages/springernature-segmented-controls/HISTORY.md b/toolkits/springernature/packages/springernature-segmented-controls/HISTORY.md new file mode 100644 index 000000000..4b625b7c3 --- /dev/null +++ b/toolkits/springernature/packages/springernature-segmented-controls/HISTORY.md @@ -0,0 +1,4 @@ +# History + +## 0.1.0 (2023-05-17) + * Adds segmented control component along with README diff --git a/toolkits/springernature/packages/springernature-segmented-controls/README.md b/toolkits/springernature/packages/springernature-segmented-controls/README.md new file mode 100644 index 000000000..5555b2060 --- /dev/null +++ b/toolkits/springernature/packages/springernature-segmented-controls/README.md @@ -0,0 +1,45 @@ +# Springer Nature Segmented Controls + +[![NPM version][badge-npm]][info-npm] + +TBD + +## When to use this component + +TBD + +## Installation + +To use the Segmented Controls component, enter the following command in your Terminal: + +``` +npm install @springernature/springernature-segmented-controls +``` + +Import the installed component code in your enhanced `scss` file: + +```scss +// Include this with your other settings +@import '@springernature/brand-context/springernature/scss/10-settings/colors/default.scss'; + +// Include this with your other components +@import '@springernature/springernature-segmented-controls/scss/50-components/enhanced; +``` + +## How it works + +TBD + +## Template + +Find a configurable template in the [`view` folder](https://github.com/springernature/frontend-toolkits/tree/main/toolkits/springernature/packages/springernature-segmented-controls/view). + +You can see an example in the [`demo` folder](https://github.com/springernature/frontend-toolkits/tree/main/toolkits/springernature/packages/springernature-segmented-controls/demo). + +## Help improve this page + +If you’ve got a question, idea or suggestion about how to improve this component +or guidance, post in the [#design-systems Slack channel](https://springernature.slack.com/archives/C75DHBTBP). + +[info-npm]: https://www.npmjs.com/package/@springernature/springernature-segmented-controls +[badge-npm]: https://img.shields.io/npm/v/@springernature/springernature-segmented-controls.svg diff --git a/toolkits/springernature/packages/springernature-segmented-controls/demo/context.json b/toolkits/springernature/packages/springernature-segmented-controls/demo/context.json new file mode 100644 index 000000000..467e21c82 --- /dev/null +++ b/toolkits/springernature/packages/springernature-segmented-controls/demo/context.json @@ -0,0 +1,91 @@ +{ + "defaultWith2Options": { + "legend":"Mode", + "name":"mode", + "options":[ + { + "value":"light", + "id":"mode-light", + "label":"Light", + "checked":true + }, + { + "value":"dark", + "id":"mode-dark", + "label":"Dark", + "checked":false + } + ] + }, + "defaultWith3Options": { + "legend":"Size", + "name":"size", + "options":[ + { + "value":"small", + "id":"size-small", + "label":"Small", + "checked":true + }, + { + "value":"medium", + "id":"size-medium", + "label":"Medium", + "checked":false + }, + { + "value":"large", + "id":"size-large", + "label":"Large", + "checked":false + } + ] + }, + "secondaryWith3Options": { + "legend":"Show only", + "modifierClass":"c-segmented-controls--secondary", + "name":"show", + "options":[ + { + "value":"todo", + "id":"show-todo", + "label":"To do", + "checked":true + }, + { + "value":"progress", + "id":"show-progress", + "label":"In progress", + "checked":false + }, + { + "value":"done", + "id":"show-done", + "label":"Done", + "checked":false + } + ] + }, + "secondaryWith2OptionsAndJavascript": { + "legend":"Frequency", + "modifierClass":"c-segmented-controls--secondary", + "name":"frequency", + "options":[ + { + "value":"weekly", + "id":"frequency-weekly", + "label":"Weekly", + "checked":true + }, + { + "value":"monthly", + "id":"frequency-monthly", + "label":"Monthly", + "checked":false + } + ] + }, + "dynamicPartials": { + "segmentedControls": "./toolkits/springernature/packages/springernature-segmented-controls/view/segmentedControls.hbs" + } +} \ No newline at end of file diff --git a/toolkits/springernature/packages/springernature-segmented-controls/demo/dist/index.html b/toolkits/springernature/packages/springernature-segmented-controls/demo/dist/index.html new file mode 100644 index 000000000..5ed7ceccf --- /dev/null +++ b/toolkits/springernature/packages/springernature-segmented-controls/demo/dist/index.html @@ -0,0 +1,1310 @@ + + + + + @springernature/springernature-segmented-controls + + + +
+
    +
  • + Default Segmented controls with 2 options +
    +
    +
    + + Mode + +
    + + +
    +
    + + +
    +
    +
    +
    +
  • +
  • + Default Segmented controls with 3 options +
    +
    +
    + + Size + +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
  • +
  • + Secondary Segmented controls with 3 options +
    +
    +
    + + Show only + +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
  • +
  • + Secondary Segmented controls with 2 options and Javascript +
    +
    +
    + + Frequency + +
    + + +
    +
    + + +
    +
    +
    +
    +
  • +
+
+ + + + \ No newline at end of file diff --git a/toolkits/springernature/packages/springernature-segmented-controls/demo/index.hbs b/toolkits/springernature/packages/springernature-segmented-controls/demo/index.hbs new file mode 100644 index 000000000..025dfd208 --- /dev/null +++ b/toolkits/springernature/packages/springernature-segmented-controls/demo/index.hbs @@ -0,0 +1,36 @@ +
+
    +
  • + Default Segmented controls with 2 options +
    + {{#with defaultWith2Options}} + {{> segmentedControls}} + {{/with}} +
    +
  • +
  • + Default Segmented controls with 3 options +
    + {{#with defaultWith3Options}} + {{> segmentedControls}} + {{/with}} +
    +
  • +
  • + Secondary Segmented controls with 3 options +
    + {{#with secondaryWith3Options}} + {{> segmentedControls}} + {{/with}} +
    +
  • +
  • + Secondary Segmented controls with 2 options and Javascript +
    + {{#with secondaryWith2OptionsAndJavascript}} + {{> segmentedControls}} + {{/with}} +
    +
  • +
+
diff --git a/toolkits/springernature/packages/springernature-segmented-controls/demo/main.js b/toolkits/springernature/packages/springernature-segmented-controls/demo/main.js new file mode 100644 index 000000000..e0b30b180 --- /dev/null +++ b/toolkits/springernature/packages/springernature-segmented-controls/demo/main.js @@ -0,0 +1,11 @@ +import {segmentedControls} from '../js/index'; + +document.addEventListener('DOMContentLoaded', () => { + const frequencyInput = document.querySelector('[name="frequency"]'); + const segmentedControlsContainer = frequencyInput.closest('[data-segmented-controls]'); + + segmentedControls(segmentedControlsContainer, function segmentedControlsCallback(checkedValue) { + const label = segmentedControlsContainer.querySelector('[for="frequency-' + checkedValue + '"]'); + alert('You picked: ' + label.textContent.trim()); + }).init(); +}); diff --git a/toolkits/springernature/packages/springernature-segmented-controls/demo/main.scss b/toolkits/springernature/packages/springernature-segmented-controls/demo/main.scss new file mode 100644 index 000000000..eb8963489 --- /dev/null +++ b/toolkits/springernature/packages/springernature-segmented-controls/demo/main.scss @@ -0,0 +1,9 @@ +@import '../node_modules/@springernature/brand-context/default/scss/core.scss'; +@import '../node_modules/@springernature/brand-context/default/scss/enhanced.scss'; + +@import '../node_modules/@springernature/brand-context/springernature/scss/10-settings/colors/default.scss'; + +@import '../scss/50-components/enhanced'; + +@import '../node_modules/@springernature/brand-context/default/scss/60-utilities/lists.scss'; +@import '../node_modules/@springernature/brand-context/default/scss/60-utilities/spacing.scss'; diff --git a/toolkits/springernature/packages/springernature-segmented-controls/js/__tests__/segmented-controls.test.js b/toolkits/springernature/packages/springernature-segmented-controls/js/__tests__/segmented-controls.test.js new file mode 100644 index 000000000..5ea04086c --- /dev/null +++ b/toolkits/springernature/packages/springernature-segmented-controls/js/__tests__/segmented-controls.test.js @@ -0,0 +1,81 @@ +const {segmentedControls} = require('../'); + +const fixture = ` +
+
+ + Display + +
+ + +
+
+ + +
+
+
+`; + +const fixture2 = fixture.replace(' checked=""', ''); + +describe('Segmented controls component', () => { + let segmentedControlsContainer; + let myCallback; + beforeEach(() => { + myCallback = jest.fn(); + document.body.innerHTML = fixture; + segmentedControlsContainer = document.querySelector('[data-segmented-controls]'); + }); + + it('does not error if can\'t match data-segmented-controls', () => { + expect(() => { + segmentedControls().init(); + }).not.toThrow(); + }); + + it('does not error if onCheckedRadioChange isn\'t given', () => { + expect(() => { + segmentedControls(segmentedControlsContainer).init(); + }).not.toThrow(); + }); + + it('expects onCheckedRadioChange to be a function', () => { + expect(() => { + segmentedControls(segmentedControlsContainer, 'notAFunction').init(); + }).toThrow(); + }); + + it('expects onCheckedRadioChange not to be called, when clicking already checked option', () => { + segmentedControls(segmentedControlsContainer, myCallback).init(); + const label = segmentedControlsContainer.querySelector('input[checked] + label'); + label.click(); + expect(myCallback).not.toHaveBeenCalled(); + }); + + it('expects onCheckedRadioChange to have been called when clicked an unchecked option', () => { + segmentedControls(segmentedControlsContainer, myCallback).init(); + const label = segmentedControlsContainer.querySelector('input:not(:checked) + label'); + label.click(); + expect(myCallback).toHaveBeenCalled(); + }); + + it('expects onCheckedRadioChange to have called with checked option\'s value when clicked an unchecked option', () => { + segmentedControls(segmentedControlsContainer, myCallback).init(); + const label = segmentedControlsContainer.querySelector('input:not(:checked) + label'); + label.click(); + expect(myCallback).toHaveBeenCalledWith('table'); + }); + + it('expects onCheckedRadioChange to have been called when clicked an unchecked option and when none is checked', () => { + // Removes the checked + document.body.innerHTML = fixture2; + + segmentedControlsContainer = document.querySelector('[data-segmented-controls]'); + segmentedControls(segmentedControlsContainer, myCallback).init(); + const label = segmentedControlsContainer.querySelector('input[value="graph"]'); + label.click(); + expect(myCallback).toHaveBeenCalledWith('graph'); + }); +}); diff --git a/toolkits/springernature/packages/springernature-segmented-controls/js/index.js b/toolkits/springernature/packages/springernature-segmented-controls/js/index.js new file mode 100644 index 000000000..ca48cafc3 --- /dev/null +++ b/toolkits/springernature/packages/springernature-segmented-controls/js/index.js @@ -0,0 +1,51 @@ +function segmentedControls(container, onCheckedRadioChange) { + let radioButtons; + let checkedValue; + + return { + init + }; + + function init() { + if (!container || !onCheckedRadioChange) { + return; + } + if (typeof onCheckedRadioChange !== 'function') { + throw new TypeError('onChangeCallback should be a function'); + } + radioButtons = container.querySelectorAll('input'); + const checked = container.querySelector('input[checked]'); + checkedValue = checked ? checked.value : null; + + Array.prototype.forEach.call(radioButtons, function (element) { + element.addEventListener('click', onClickCallback); + + // In standby now since browser vendors seem to transfer keyboard + // interaction to the click handler. + // Caveat: This seems to not be the case of JSDOM (used underneath of Jest). + // element.addEventListener('keydown', function (event) { + // if (event.key === ' ' || event.key === 'Spacebar' || event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'ArrowRight' || event.key === 'ArrowLeft') { + // onClickCallback(event); + // } + // }); + }); + } + + // eslint-disable-next-line unicorn/consistent-function-scoping + function onClickCallback(_event) { + for (const radioButton of radioButtons) { + if (radioButton.checked) { + if (radioButton.value !== checkedValue) { + checkedValue = radioButton.value; + onCheckedRadioChange(checkedValue); + break; + } + } + } + } +} + +export { + segmentedControls +}; + diff --git a/toolkits/springernature/packages/springernature-segmented-controls/package.json b/toolkits/springernature/packages/springernature-segmented-controls/package.json new file mode 100644 index 000000000..da3c1769d --- /dev/null +++ b/toolkits/springernature/packages/springernature-segmented-controls/package.json @@ -0,0 +1,9 @@ +{ + "name": "@springernature/springernature-segmented-controls", + "version": "0.1.0", + "license": "MIT", + "description": "Linear set of two or more segments, each of which functions as a button", + "keywords": [], + "author": "Springer Nature", + "brandContext": "^32.0.0" +} \ No newline at end of file diff --git a/toolkits/springernature/packages/springernature-segmented-controls/scss/50-components/_enhanced.scss b/toolkits/springernature/packages/springernature-segmented-controls/scss/50-components/_enhanced.scss new file mode 100644 index 000000000..47112bdbd --- /dev/null +++ b/toolkits/springernature/packages/springernature-segmented-controls/scss/50-components/_enhanced.scss @@ -0,0 +1,113 @@ +$segmented-controls--border-radius: 4px; +$segmented-controls--container-background-color: $context--grey-blue-light1; +$segmented-controls--container-border-color: map-get($context--colors, 'grey-mid1'); + +$segmented-controls--container-border-width-style: 1px solid; +$segmented-controls--container-padding: spacing(4) spacing(2); +$segmented-controls--label-margin: spacing(0) spacing(2); +$segmented-controls--label-padding: spacing(2) spacing(16); +$segmented-controls--label-border-width: spacing(4); + +$segmented-controls--default-label: ( + 'color': map-get($context--colors, 'grey-dark2'), + 'borderColor': transparent, + 'boxShadow': 0 0 0 1px transparent, + 'checkedColor': map-get($context--colors, 'white'), + 'checkedBackgroundColor': $context--universal-blue, + 'checkedBoxShadow': 0 0 0 1px $context--universal-blue, + 'hoverColor': $context--universal-blue, + 'hoverBackgroundColor': map-get($context--colors, 'white'), + 'hoverBoxShadow': 0 0 0 1px $context--universal-blue, + 'focusBorderColor': $context--focus-color, + 'focusBoxShadow': none +); + +$segmented-controls--secondary-label: ( + 'color': map-get($context--colors, 'grey-dark2'), + 'borderColor': transparent, + 'boxShadow': 0 0 0 1px transparent, + 'checkedColor': $context--universal-blue, + 'checkedBackgroundColor': map-get($context--colors, 'white'), + 'checkedBoxShadow': 0 0 0 1px $context--universal-blue, + 'hoverColor': map-get($context--colors, 'white'), + 'hoverBackgroundColor': $context--universal-blue, + 'hoverBoxShadow': 0 0 0 1px transparent, + 'focusBorderColor': $context--focus-color, + 'focusBoxShadow': none +); + +@mixin segmented-controls-base { + display: inline-flex; + border: $segmented-controls--container-border-width-style $segmented-controls--container-border-color; + border-radius: $segmented-controls--border-radius; + padding: $segmented-controls--container-padding; + background-color: $segmented-controls--container-background-color; + font-family: $context--font-family-sans; + + fieldset { + padding: 0; + border: 0; + display: inline-flex; + } + + legend, + input { + @include u-visually-hidden; + } + + label { + float: left; + display: block; + margin: $segmented-controls--label-margin; + border-radius: $segmented-controls--border-radius; + border-width: $segmented-controls--label-border-width; + border-style: solid; + padding: $segmented-controls--label-padding; + line-height: 1.5; + } + + input + label:hover { + cursor: pointer; + } +} + +@mixin segmented-controls-theme($theme) { + label { + color: map-get($theme, 'color'); + border-color: map-get($theme, 'borderColor'); + box-shadow: map-get($theme, 'boxShadow'); + } + + input:checked + label { + background-color: map-get($theme, 'checkedBackgroundColor'); + color: map-get($theme, 'checkedColor'); + box-shadow: map-get($theme, 'checkedBoxShadow'); + } + + input + label:hover { + color: map-get($theme, 'hoverColor'); + background-color: map-get($theme, 'hoverBackgroundColor'); + box-shadow: map-get($theme, 'hoverBoxShadow'); + } + + input:focus + label { + border-color: map-get($theme, 'focusBorderColor'); + box-shadow: map-get($theme, 'focusBoxShadow'); + } +} + +@mixin segmented-control($theme: false) { + @include segmented-controls-base; + + @if $theme { + @include segmented-controls-theme($theme); + } +} + +.c-segmented-controls { + @include segmented-control($segmented-controls--default-label); +} + +.c-segmented-controls--secondary { + @include segmented-control($segmented-controls--secondary-label); +} diff --git a/toolkits/springernature/packages/springernature-segmented-controls/view/segmentedControls.hbs b/toolkits/springernature/packages/springernature-segmented-controls/view/segmentedControls.hbs new file mode 100644 index 000000000..4af9bed6c --- /dev/null +++ b/toolkits/springernature/packages/springernature-segmented-controls/view/segmentedControls.hbs @@ -0,0 +1,13 @@ +
+
+ + {{legend}} + + {{#options}} +
+ + +
+ {{/options}} +
+
From 2161bde28d68e63727acb89af24c1675d54155fe Mon Sep 17 00:00:00 2001 From: Morgan CUGERONE Date: Wed, 17 May 2023 09:53:57 +0200 Subject: [PATCH 2/2] [MC] Fix linting issues --- .../demo/dist/index.html | 10 +++++++--- .../springernature-segmented-controls/demo/main.js | 9 ++++++--- .../js/__tests__/segmented-controls.test.js | 2 +- .../js/{index.js => segmented-controls.js} | 0 4 files changed, 14 insertions(+), 7 deletions(-) rename toolkits/springernature/packages/springernature-segmented-controls/js/{index.js => segmented-controls.js} (100%) diff --git a/toolkits/springernature/packages/springernature-segmented-controls/demo/dist/index.html b/toolkits/springernature/packages/springernature-segmented-controls/demo/dist/index.html index 5ed7ceccf..10f645f09 100644 --- a/toolkits/springernature/packages/springernature-segmented-controls/demo/dist/index.html +++ b/toolkits/springernature/packages/springernature-segmented-controls/demo/dist/index.html @@ -1297,10 +1297,14 @@ document.addEventListener('DOMContentLoaded', function () { var frequencyInput = document.querySelector('[name="frequency"]'); var segmentedControlsContainer = frequencyInput.closest('[data-segmented-controls]'); - segmentedControls(segmentedControlsContainer, function segmentedControlsCallback(checkedValue) { - var label = segmentedControlsContainer.querySelector('[for="frequency-' + checkedValue + '"]'); + + function segmentedControlsCallback(checkedValue) { + var label = segmentedControlsContainer.querySelector('[for="frequency-' + checkedValue + '"]'); // eslint-disable-next-line no-alert + alert('You picked: ' + label.textContent.trim()); - }).init(); + } + + segmentedControls(segmentedControlsContainer, segmentedControlsCallback).init(); }); })(); diff --git a/toolkits/springernature/packages/springernature-segmented-controls/demo/main.js b/toolkits/springernature/packages/springernature-segmented-controls/demo/main.js index e0b30b180..ca1d8ef18 100644 --- a/toolkits/springernature/packages/springernature-segmented-controls/demo/main.js +++ b/toolkits/springernature/packages/springernature-segmented-controls/demo/main.js @@ -1,11 +1,14 @@ -import {segmentedControls} from '../js/index'; +import {segmentedControls} from '../js/segmented-controls'; document.addEventListener('DOMContentLoaded', () => { const frequencyInput = document.querySelector('[name="frequency"]'); const segmentedControlsContainer = frequencyInput.closest('[data-segmented-controls]'); - segmentedControls(segmentedControlsContainer, function segmentedControlsCallback(checkedValue) { + function segmentedControlsCallback(checkedValue) { const label = segmentedControlsContainer.querySelector('[for="frequency-' + checkedValue + '"]'); + // eslint-disable-next-line no-alert alert('You picked: ' + label.textContent.trim()); - }).init(); + } + + segmentedControls(segmentedControlsContainer, segmentedControlsCallback).init(); }); diff --git a/toolkits/springernature/packages/springernature-segmented-controls/js/__tests__/segmented-controls.test.js b/toolkits/springernature/packages/springernature-segmented-controls/js/__tests__/segmented-controls.test.js index 5ea04086c..11574417a 100644 --- a/toolkits/springernature/packages/springernature-segmented-controls/js/__tests__/segmented-controls.test.js +++ b/toolkits/springernature/packages/springernature-segmented-controls/js/__tests__/segmented-controls.test.js @@ -1,4 +1,4 @@ -const {segmentedControls} = require('../'); +const {segmentedControls} = require('../segmented-controls'); const fixture = `
diff --git a/toolkits/springernature/packages/springernature-segmented-controls/js/index.js b/toolkits/springernature/packages/springernature-segmented-controls/js/segmented-controls.js similarity index 100% rename from toolkits/springernature/packages/springernature-segmented-controls/js/index.js rename to toolkits/springernature/packages/springernature-segmented-controls/js/segmented-controls.js