Repository: manusa/electronim
Pull request #215 (2022-11-19 06:43:48)

✨ settings uses Material 3 text fields and buttons

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=manusa_electronim&pullRequest=215)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=215&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=215&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=215&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=215&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=215&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=215&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=215&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=215&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=215&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=215&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=215&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=215&resolved=false&types=CODE_SMELL)

[![91.8%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90-16px.png '91.8%')](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=215&metric=new_coverage&view=list) [91.8% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=215&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=215&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=215&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/src/chrome-tabs/chrome-tabs.browser.css b/src/chrome-tabs/chrome-tabs.browser.css
index 77d76f4..629d377 100644
--- a/src/chrome-tabs/chrome-tabs.browser.css
+++ b/src/chrome-tabs/chrome-tabs.browser.css
@@ -60,7 +60,7 @@ body {
   padding: 0 8px 0 0;
 }
 
-.tab-container .menu__button {
+.tab-container .menu__button.material3.icon-button {
   margin: 0 12px 0 8px;
   z-index: 2;
 }
diff --git a/src/components/icon-button.mjs b/src/components/icon-button.mjs
index 7b62333..de0fa74 100644
--- a/src/components/icon-button.mjs
+++ b/src/components/icon-button.mjs
@@ -30,8 +30,9 @@ export const IconButton = ({
   className = '',
   ...properties
 }) => html`
-  <a href='#' ...${properties}
+  <a
     class=${`material3 icon-button ${disabled ? 'disabled ' : ''}${className}`}
     disabled=${disabled} onClick=${disabled ? null : onClick}
+    ...${properties}
   >${icon}</a>
 `;
diff --git a/src/components/index.mjs b/src/components/index.mjs
index aa98b71..04bd098 100644
--- a/src/components/index.mjs
+++ b/src/components/index.mjs
@@ -24,5 +24,5 @@ export {IconButton} from './icon-button.mjs';
 export {Logo} from './electronim.mjs';
 export {Menu} from './menu.mjs';
 export {NavigationRail} from './navigation-rail.mjs';
-export {Panel} from './panel.mjs';
+export {TextField} from './text-field.mjs';
 export {TopAppBar} from './top-app-bar.mjs';
diff --git a/src/components/menu.mjs b/src/components/menu.mjs
index bcb2d03..03954e8 100644
--- a/src/components/menu.mjs
+++ b/src/components/menu.mjs
@@ -16,10 +16,9 @@
 import {html} from './index.mjs';
 
 /**
- A menu based on Material design (3) guidelines.
-
+ * A menu based on Material design (3) guidelines.
+ *
  * @param children the collection of Menu.Item components to be rendered.
- * @constructor
  */
 export const Menu = ({children}) => html`
   <div class='material3 menu elevation-2 surface body-large'>
diff --git a/src/components/navigation-rail.mjs b/src/components/navigation-rail.mjs
index 7789bbb..3cbb2f6 100644
--- a/src/components/navigation-rail.mjs
+++ b/src/components/navigation-rail.mjs
@@ -15,6 +15,11 @@
  */
 import {html, useState, IconButton} from './index.mjs';
 
+/**
+ * A Navigation Rail based on Material design (3) guidelines.
+ *
+ * @param children the collection of NavigationRail.Button components to be rendered.
+ */
 export const NavigationRail = ({
   children
 }) => {
diff --git a/src/components/panel.mjs b/src/components/panel.mjs
deleted file mode 100644
index 0e2bacb..0000000
--- a/src/components/panel.mjs
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
-   Copyright 2022 Marc Nuri San Felix
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
- */
-import {html} from './index.mjs';
-
-export const Panel = ({heading, className = '', children}) => (html`
-  <nav class=${`panel ${className}`}>
-    ${heading && html`<p class='panel-heading'>${heading}</p>`}
-    ${children}
-  </nav>
-`);
-
-Panel.Block = ({className = '', children, ...properties}) => (html`
-  <div class=${`panel-block ${className}`} ...${properties}>${children}</div>
-`);
diff --git a/src/components/text-field.mjs b/src/components/text-field.mjs
new file mode 100644
index 0000000..3fc82a4
--- /dev/null
+++ b/src/components/text-field.mjs
@@ -0,0 +1,64 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+import {html, useState} from './index.mjs';
+
+export const TextField = ({
+  type = TextField.types.outlined,
+  label,
+  placeholder,
+  onInput,
+  onKeyDown,
+  value,
+  hasError = false
+  /* eslint-disable-next-line no-warning-comments */
+  // TODO disabled state
+}) => {
+  const [focused, setFocused] = useState(false);
+  const onFocus = () => setFocused(true);
+  const onBlur = () => setFocused(false);
+  let textFieldClass = `material3 text-field ${type}`;
+  if (focused) {
+    textFieldClass += ' focused';
+  }
+  if (label) {
+    textFieldClass += ' has-label';
+  }
+  if (value && value.length > 0) {
+    textFieldClass += ' populated';
+  } else {
+    textFieldClass += ' empty';
+  }
+  if (hasError) {
+    textFieldClass += ' errored';
+  }
+  return html`
+    <div class=${textFieldClass}>
+      ${label && html`<span class='text-field__label'>${label}</span>`}
+      ${placeholder && html`<div class='text-field__placeholder'>${placeholder}</div>`}
+      <input
+        type='text'
+        class='text-field__input'
+        onFocus=${onFocus} onBlur=${onBlur}
+        value=${value} onInput=${onInput} onKeyDown=${onKeyDown}
+      />
+    </div>
+  `;
+};
+
+TextField.types = {
+  filled: 'filled', // Not implemented yet
+  outlined: 'outlined'
+};
diff --git a/src/settings/settings.browser.css b/src/settings/settings.browser.css
index b2d4846..5cc6209 100644
--- a/src/settings/settings.browser.css
+++ b/src/settings/settings.browser.css
@@ -36,19 +36,6 @@
     margin: 0 0 12px 0;
 }
 
-.settings__button.button {
-    background: none;
-    border-color: transparent;
-}
-
-.settings__button.button:hover {
-    border-color: var(--border-color);
-}
-
-.settings__button.button:not(:last-child) {
-    margin-right: var(--default-spacing);
-}
-
 .settings__tab {
     flex-direction: column;
 }
@@ -59,6 +46,7 @@
     display: flex;
     align-items: center;
     justify-content: flex-start;
+    gap: var(--default-spacing);
     width: 100%;
 }
 
@@ -77,9 +65,16 @@
     margin: var(--margin) var(--margin) var(--default-spacing) var(--margin);
 }
 
-.settings__new-tab .control,
-.settings__tab .control {
-    margin-right: var(--default-spacing);
+.settings__tab .expand-button {
+    transition: transform 0.3s ease-out;
+}
+
+.settings__tab--expanded .expand-button {
+    transform: rotate(180deg);
+}
+
+.settings__new-tab .material3.text-field,
+.settings__tab .material3.text-field {
     flex: 1;
 }
 
diff --git a/src/settings/settings.services.browser.mjs b/src/settings/settings.services.browser.mjs
index 69a6972..ac0bd44 100644
--- a/src/settings/settings.services.browser.mjs
+++ b/src/settings/settings.services.browser.mjs
@@ -13,34 +13,22 @@
    See the License for the specific language governing permissions and
    limitations under the License.
  */
-import {Card, Checkbox, html} from '../components/index.mjs';
+import {html, Card, Checkbox, IconButton, TextField} from '../components/index.mjs';
 import {ACTIONS, addTab, isPaneActive, setTabProperty, toggleTabProperty} from './settings.reducer.browser.mjs';
 
-const disabledIcon = disabled => (disabled === true ? 'fa-eye-slash' : 'fa-eye');
+const disabledIcon = disabled => (disabled === true ? '\ue8f5' : '\ue8f4');
+const notificationIcon = disabled => (disabled === true ? '\ue7f6' : '\ue7f4');
 const sandboxedIcon = sandboxed => (sandboxed === true ? 'fa-lock' : 'fa-lock-open');
-const newTabClass = state => {
-  if (state.newTabValue.length === 0) {
-    return '';
-  }
-  return state.newTabValid ? 'is-success' : 'is-danger';
-};
-
-const SettingsButton = ({icon, disabled = false, title, onClick}) => (html`
-  <button class='settings__button button' disabled=${disabled} onClick=${onClick}>
-    <span class='icon is-medium' title='${title}'>
-      <i class='fas ${icon}'></i>
-    </span>
-  </button>
-`);
 
 const ExpandButton = ({dispatch, id, expanded = false}) => {
   const properties = {
-    icon: expanded ? 'fa-chevron-down' : 'fa-chevron-right',
+    className: 'expand-button',
+    icon: '\ue5cf',
     title: expanded ? 'Collapse' : 'Expand (show advanced settings)',
     onClick: () => dispatch({type: ACTIONS.TOGGLE_TAB_EXPANDED, payload: id})
   };
   return html`
-    <${SettingsButton} ...${properties} />
+    <${IconButton} ...${properties} />
   `;
 };
 
@@ -60,23 +48,23 @@ const TabEntry = ({
   dispatch, invalidTabs, id, expanded, url, disabled, disableNotifications = false, ...tab
 }) => (html`
   <${Card.Divider} />
-  <div class='settings__tab ${expanded && 'settings__tab--expanded'}' data-id=${id}>
+  <div class='settings__tab ${expanded ? 'settings__tab--expanded' : ''}' data-id=${id}>
     <div class='settings__tab-main'>
       <${ExpandButton} dispatch=${dispatch} id=${id} expanded=${expanded} />
-      <div class='control'>
-        <input type='text' class=${`input ${invalidTabs.has(id) ? 'is-danger' : ''}`} name='tabs'
-          oninput=${({target: {value}}) => setTabProperty({dispatch, property: 'url', value, id})}
-          value=${url} />
-      </div>
-      <${SettingsButton} icon=${disabledIcon(disabled)}
+      <${TextField}
+        hasError=${invalidTabs.has(id)}
+        onInput=${({target: {value}}) => setTabProperty({dispatch, property: 'url', value, id})}
+        value=${url}
+      />
+      <${IconButton} icon=${disabledIcon(disabled)} data-testid='button-disable'
         title=${disabled ? 'Tab disabled. Click to enable' : 'Tab enabled. Click to disable'}
         onClick=${toggleTabProperty(dispatch, 'disabled', id)}
       />
-      <${SettingsButton} icon=${disableNotifications ? 'fa-bell-slash' : 'fa-bell'}
+      <${IconButton} icon=${notificationIcon(disableNotifications)} data-testid='button-notification'
         title=${disableNotifications ? 'Notifications disabled. Click to enable' : 'Notifications enabled. Click to disable'}
         onClick=${toggleTabProperty(dispatch, 'disableNotifications', id)}
       />
-      <${SettingsButton} icon='fa-trash' title='Delete tab'
+      <${IconButton} icon='\ue872' title='Delete tab' data-testid='button-delete'
         onClick=${() => dispatch({type: ACTIONS.REMOVE, payload: {id}})}
       /> 
     </div>
@@ -97,21 +85,19 @@ export const ServicesPane = ({dispatch, state}) => {
       onAddTab();
     }
   };
-
   return isPaneActive(state)(ServicesPane.id) && html`
     <h2 class='title'>Services</h2>
     <${Card} className='settings__services'>
       <div class='settings__new-tab'>
-        <div class="control">
-          <input type="text"
-            class="input ${newTabClass(state)}"
-            placeholder="https://web.whatsapp.com"
+        <${TextField}
+            label='Service URL'
+            placeholder='https://web.whatsapp.com' 
             value=${state.newTabValue}
-            oninput=${onNewTabInput}
-            onkeydown=${onNewKeyDown}
-          />
-        </div>
-        <${SettingsButton} icon='fa-plus' onClick=${onAddTab} disabled=${!state.newTabValid} />
+            onInput=${onNewTabInput}
+            onKeyDown=${onNewKeyDown}
+            hasError=${state.newTabValue.length !== 0 && !state.newTabValid}
+        />
+        <${IconButton} icon='\ue145' onClick=${onAddTab} disabled=${!state.newTabValid} />
       </div>
       <div class="settings__tabs container field">
         ${state.tabs.map(tab => (html`
diff --git a/src/styles/components/icon-button.css b/src/styles/components/icon-button.css
index e1ced0c..9091f9d 100644
--- a/src/styles/components/icon-button.css
+++ b/src/styles/components/icon-button.css
@@ -28,10 +28,13 @@
   outline: none;
   text-decoration: none;
   width: var(--material3-icon-button-size);
+  margin: 0 calc(var(--material3-icon-button-size) * 0.3333); /* Account for state layer size */
   height: var(--material3-icon-button-size);
   font-size: var(--material3-icon-button-size);
   line-height: var(--material3-icon-button-size);
   text-align: center;
+  user-select: none;
+  cursor: pointer;
 }
 
 .electronim .material3.icon-button::before {
diff --git a/src/styles/components/index.css b/src/styles/components/index.css
index b2bc6d1..7e404fd 100644
--- a/src/styles/components/index.css
+++ b/src/styles/components/index.css
@@ -18,4 +18,5 @@
 @import './icon-button.css';
 @import './menu.css';
 @import './navigation-rail.css';
+@import './text-field.css';
 @import './top-bar.css';
diff --git a/src/styles/components/text-field.css b/src/styles/components/text-field.css
new file mode 100644
index 0000000..f89dab7
--- /dev/null
+++ b/src/styles/components/text-field.css
@@ -0,0 +1,163 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+
+:root {
+  /*--material3-text-field-height: 56px;*/
+  /* Material 3 default height is too big for us, reduce it to something more appealing */
+  --material3-text-field-height: 40px;
+  --material3-text-field-padding-no-icon: 16px;
+  --material3-text-field-outlined-border: solid 1px transparent;
+  --material3-text-field-outlined-border-radius: 4px;
+  --material3-text-field-outlined-border-color: var(--md-sys-color-outline);
+  --material3-text-field-outlined-border-color-focused: var(--md-sys-color-primary);
+  --material3-text-field-outlined-border-color-errored: var(--md-sys-color-error);
+  --material3-text-field-transition-duration: 0.2s;
+}
+
+.electronim .material3.text-field {
+  position: relative;
+  height: var(--material3-text-field-height);
+  display: flex;
+  padding: 0 var(--material3-text-field-padding-no-icon);
+  align-items: center;
+}
+
+.electronim .material3.text-field.outlined {
+  border: var(--material3-text-field-outlined-border);
+  border-color: var(--material3-text-field-outlined-border-color);
+  border-radius: var(--material3-text-field-outlined-border-radius);
+}
+
+.electronim .material3.text-field.outlined.focused {
+  border-color: var(--material3-text-field-outlined-border-color-focused);
+}
+
+.electronim .material3.text-field.outlined.errored {
+  border-color: var(--material3-text-field-outlined-border-color-errored);
+}
+
+.electronim .material3.text-field.outlined.has-label.populated,
+.electronim .material3.text-field.outlined.has-label.focused,
+.electronim .material3.text-field.outlined.has-label.errored{
+  border-top-color: transparent;
+}
+
+.electronim .material3.text-field .text-field__label {
+  background: none;
+  pointer-events: none;
+  font-family: var(--md-sys-typescale-body-large-font);
+  font-size: var(--md-sys-typescale-body-large-size);
+  font-weight: var(--md-sys-typescale-body-large-weight);
+}
+
+.electronim .material3.text-field.outlined .text-field__label {
+  --label-offset: 9px;
+  position: absolute;
+  top: calc(-1 * var(--label-offset));
+  left: 0;
+  width: 100%;
+  max-height: 100%;
+  display: flex;
+  gap: 4px;
+  color: var(--md-sys-color-on-surface);
+  line-height: calc(var(--material3-text-field-height) + var(--label-offset) * 2);
+  transition:
+    line-height var(--material3-text-field-transition-duration) ease,
+    font-size var(--material3-text-field-transition-duration) ease,
+    color var(--material3-text-field-transition-duration) ease;
+}
+
+.electronim .material3.text-field.outlined.focused .text-field__label,
+.electronim .material3.text-field.outlined.populated .text-field__label {
+  color: var(--md-sys-color-primary);
+  font-size: var(--md-sys-typescale-body-small-size);
+  line-height: 16px;
+}
+
+.electronim .material3.text-field.outlined.errored .text-field__label {
+  color: var(--md-sys-color-error);
+}
+
+.electronim .material3.text-field.outlined .text-field__label::before,
+.electronim .material3.text-field.outlined .text-field__label::after {
+  content: '';
+  display: block;
+  margin-top: 8px;
+  border-top: var(--material3-text-field-outlined-border);
+}
+
+.electronim .material3.text-field.outlined .text-field__label::before {
+  min-width: calc(var(--material3-text-field-padding-no-icon) - 4px);
+  border-top-left-radius: var(--material3-text-field-outlined-border-radius);
+}
+
+.electronim .material3.text-field.outlined .text-field__label::after {
+  flex: 1;
+  border-top-right-radius: var(--material3-text-field-outlined-border-radius);
+}
+
+.electronim .material3.text-field.outlined.populated .text-field__label::before,
+.electronim .material3.text-field.outlined.populated .text-field__label::after {
+  border-color: var(--material3-text-field-outlined-border-color);
+}
+
+.electronim .material3.text-field.outlined.focused .text-field__label::before,
+.electronim .material3.text-field.outlined.focused .text-field__label::after {
+  border-color: var(--material3-text-field-outlined-border-color-focused);
+}
+
+.electronim .material3.text-field.outlined.errored .text-field__label::before,
+.electronim .material3.text-field.outlined.errored .text-field__label::after {
+  border-color: var(--material3-text-field-outlined-border-color-errored);
+}
+
+.electronim .material3.text-field .text-field__placeholder {
+  display: none;
+  position: absolute;
+  color: var(--md-sys-color-on-surface-variant);
+  font-family: var(--md-sys-typescale-body-large-font);
+  font-size: var(--md-sys-typescale-body-large-size);
+}
+
+.electronim .material3.text-field.focused.empty .text-field__placeholder {
+  display: block;
+}
+
+.electronim .material3.text-field .text-field__input {
+  z-index: 1;
+  flex: 1;
+  background: none;
+  padding: 0;
+  border: 0;
+  outline: none;
+  height: var(--material3-text-field-height);
+  line-height: var(--material3-text-field-height);
+}
+
+.electronim .material3.text-field.outlined .text-field__input {
+  color: var(--md-sys-color-on-surface);
+  font-family: var(--md-sys-typescale-body-large-font);
+  font-size: var(--md-sys-typescale-body-large-size);
+  line-height: var(--md-sys-typescale-body-large-line-height);
+}
+
+.electronim .material3.text-field.errored .text-field__input {
+  color: var(--md-sys-color-error);
+}
+
+.electronim .material3.text-field.focused.errored .text-field__input {
+  color: var(--md-sys-color-on-surface);
+}


Test patch:
diff --git a/src/settings/__tests__/settings.browser.test.mjs b/src/settings/__tests__/settings.browser.test.mjs
index d8b2e4f..5a603f8 100644
--- a/src/settings/__tests__/settings.browser.test.mjs
+++ b/src/settings/__tests__/settings.browser.test.mjs
@@ -16,7 +16,7 @@
 import {jest} from '@jest/globals';
 import {loadDOM} from '../../__tests__/index.mjs';
 import {ipcRenderer} from './settings.browser.mjs';
-import {fireEvent, waitFor} from '@testing-library/dom';
+import {fireEvent, getByTestId, getByText, waitFor} from '@testing-library/dom';
 
 describe('Settings in Browser test suite', () => {
   let mockIpcRenderer;
@@ -76,8 +76,8 @@ describe('Settings in Browser test suite', () => {
     let $addTabButton;
     let $submitButton;
     beforeEach(() => {
-      $input = document.querySelector('.settings__new-tab input[type="text"].input');
-      $addTabButton = document.querySelector('.settings__new-tab .button');
+      $input = document.querySelector('.settings__new-tab .material3.text-field input');
+      $addTabButton = document.querySelector('.settings__new-tab .material3.icon-button');
       $tabContainer = document.querySelector('.settings__tabs');
       $submitButton = document.querySelector('.settings__submit');
     });
@@ -113,7 +113,7 @@ describe('Settings in Browser test suite', () => {
         // Then
         expect($tabContainer.querySelectorAll('.settings__tab').length).toBe(2);
         await waitFor(() =>
-          expect($input.classList.contains('is-danger')).toBe(true));
+          expect($input.parentElement.classList.contains('errored')).toBe(true));
         expect($tabContainer.querySelectorAll('.settings__tab .settings__tab-main input').length).toBe(2);
         expect($input.value).toBe('invalid:1337:url');
         expect($addTabButton.hasAttribute('disabled')).toBe(true);
@@ -152,14 +152,13 @@ describe('Settings in Browser test suite', () => {
       });
     });
     describe('input event', () => {
-      test('Valid URL, should add success class', async () => {
+      test('Valid URL, should enable addTabButton', async () => {
         // When
         fireEvent.input($input, {target: {value: 'http://info.cern.ch'}});
         // Then
         await waitFor(() =>
-          expect($input.classList.contains('is-success')).toBe(true));
-        expect($input.classList.contains('is-danger')).toBe(false);
-        expect($addTabButton.hasAttribute('disabled')).toBe(false);
+          expect($addTabButton.hasAttribute('disabled')).toBe(false));
+        expect($input.parentElement.classList.contains('errored')).toBe(false);
         expect($submitButton.hasAttribute('disabled')).toBe(true);
       });
       test('Invalid URL, should add danger class', async () => {
@@ -167,8 +166,7 @@ describe('Settings in Browser test suite', () => {
         fireEvent.input($input, {target: {value: 'http://invalid:1337:url'}});
         // Then
         await waitFor(() =>
-          expect($input.classList.contains('is-danger')).toBe(true));
-        expect($input.classList.contains('is-success')).toBe(false);
+          expect($input.parentElement.classList.contains('errored')).toBe(true));
         expect($addTabButton.hasAttribute('disabled')).toBe(true);
         expect($submitButton.hasAttribute('disabled')).toBe(true);
       });
@@ -176,6 +174,7 @@ describe('Settings in Browser test suite', () => {
     test('addTabButton, click with valid URL, should add tab', async () => {
       // Given
       fireEvent.input($input, {target: {value: 'info.cern.ch'}});
+      await waitFor(() => expect($addTabButton.getAttribute('disabled')).toBeFalsy());
       // When
       fireEvent.click($addTabButton);
       // Then
@@ -189,50 +188,50 @@ describe('Settings in Browser test suite', () => {
     });
   });
   describe('Tab events', () => {
-    describe('Disable icon click', () => {
+    describe('Disable icon click (sequential test - ordered)', () => {
       let $disableIcon;
       beforeEach(() => {
-        $disableIcon = document.querySelector('.settings__tabs .icon .fa-eye');
+        $disableIcon = getByTestId(document.querySelector('.settings__tab'), 'button-disable');
       });
       test('with enabled tab, should disable', async () => {
         // When
         fireEvent.click($disableIcon);
         // Then
-        await waitFor(() => expect($disableIcon.classList.contains('fa-eye')).toBe(false));
-        expect($disableIcon.classList.contains('fa-eye-slash')).toBe(true);
+        await waitFor(() => expect($disableIcon.textContent).not.toBe('\ue8f4'));
+        expect($disableIcon.textContent).toBe('\ue8f5');
       });
       test('with disabled tab, should enable', async () => {
         // When
         fireEvent.click($disableIcon);
         // Then
-        await waitFor(() => expect($disableIcon.classList.contains('fa-eye-slash')).toBe(false));
-        expect($disableIcon.classList.contains('fa-eye')).toBe(true);
+        await waitFor(() => expect($disableIcon.textContent).not.toBe('\ue8f5'));
+        expect($disableIcon.textContent).toBe('\ue8f4');
       });
     });
     test('Notification disabled icon click, should enable notification', async () => {
       // Given
-      const $notificationEnabledIcon = document.querySelector('.settings__tabs .icon .fa-bell-slash');
+      const $notificationEnabledIcon = getByText(document.querySelector('.settings__tabs'), '\ue7f6');
       // When
       fireEvent.click($notificationEnabledIcon);
       // Then
       await waitFor(() =>
-        expect($notificationEnabledIcon.classList.contains('fa-bell-slash')).toBe(false));
-      expect($notificationEnabledIcon.classList.contains('fa-bell')).toBe(true);
+        expect($notificationEnabledIcon.textContent).not.toBe('\ue7f6'));
+      expect($notificationEnabledIcon.textContent).toBe('\ue7f4');
     });
     test('Notification enabled icon click, should disable notification', async () => {
       // Given
-      const $notificationEnabledIcon = document.querySelector('.settings__tabs .icon .fa-bell');
+      const $notificationEnabledIcon = getByText(document.querySelector('.settings__tabs'), '\ue7f4');
       // When
       fireEvent.click($notificationEnabledIcon);
       // Then
       await waitFor(() =>
-        expect($notificationEnabledIcon.classList.contains('fa-bell')).toBe(false));
-      expect($notificationEnabledIcon.classList.contains('fa-bell-slash')).toBe(true);
+        expect($notificationEnabledIcon.textContent).not.toBe('\ue7f4'));
+      expect($notificationEnabledIcon.textContent).toBe('\ue7f6');
     });
     test('Trash icon click, should remove tab', async () => {
       // Given
       const $tabContainer = document.querySelector('.settings__tabs');
-      const $trashIcon = $tabContainer.querySelector('.icon .fa-trash');
+      const $trashIcon = getByText($tabContainer.querySelector('.settings__tab'), '\ue872');
       const initialChildren = $tabContainer.querySelectorAll('.settings__tab').length;
       expect(initialChildren).toBeGreaterThan(0);
       // When
@@ -244,37 +243,36 @@ describe('Settings in Browser test suite', () => {
     describe('Expand/Collapse icon click', () => {
       test('collapsed tab, should expand tab', async () => {
         // Given
-        const $expandIcon = document.querySelector('.settings__tab[data-id="1"] .icon .fa-chevron-right');
-        const $collapsedTab = $expandIcon.closest('.settings__tab');
+        const $toggleIcon = getByText(document.querySelector('.settings__tab[data-id="1"]'), '\ue5cf');
+        const $collapsedTab = $toggleIcon.closest('.settings__tab');
         expect($collapsedTab.classList.contains('settings__tab--expanded')).toBe(false);
         // When
-        fireEvent.click($expandIcon);
+        fireEvent.click($toggleIcon);
         // Then
         await waitFor(() =>
-          expect($expandIcon.classList.contains('fa-chevron-down')).toBe(true));
+          expect($toggleIcon.getAttribute('title')).toStartWith('Collapse'));
         expect($collapsedTab.classList.contains('settings__tab--expanded')).toBe(true);
       });
       test('expanded tab, should collapse tab', async () => {
         // Given
-        const $tabIcon = document.querySelector('.settings__tab[data-id="2"] .icon');
-        fireEvent.click($tabIcon);
-        await waitFor(() => expect($tabIcon.title).toEqual('Collapse'));
-        const $expandedTab = $tabIcon.closest('.settings__tab');
+        const $toggleIcon = getByText(document.querySelector('.settings__tab[data-id="2"]'), '\ue5cf');
+        fireEvent.click($toggleIcon);
+        await waitFor(() => expect($toggleIcon.title).toEqual('Collapse'));
+        const $expandedTab = $toggleIcon.closest('.settings__tab');
         expect($expandedTab.classList.contains('settings__tab--expanded')).toBe(true);
-        const $collapseIcon = $tabIcon.querySelector('.fa-chevron-down');
         // When
-        fireEvent.click($collapseIcon);
+        fireEvent.click($toggleIcon);
         // Then
         await waitFor(() =>
-          expect($collapseIcon.classList.contains('fa-chevron-right')).toBe(true));
+          expect($toggleIcon.getAttribute('title')).toStartWith('Expand'));
         expect($expandedTab.classList.contains('settings__tab--expanded')).toBe(false);
       });
       describe('Advanced settings', () => {
         test('Sandbox checkbox click, sandboxed session, should unlock', async () => {
           // Given
-          const $tabIcon = document.querySelector('.settings__tab[data-id="1"] .icon');
-          fireEvent.click($tabIcon);
-          await waitFor(() => expect($tabIcon.title).toEqual('Collapse'));
+          const $toggleIcon = document.querySelector('.settings__tab[data-id="1"] .expand-button');
+          fireEvent.click($toggleIcon);
+          await waitFor(() => expect($toggleIcon.title).toEqual('Collapse'));
           const $lockCheckBox = document.querySelector('.settings__tab-advanced .checkbox .fa-lock');
           // When
           fireEvent.click($lockCheckBox);
@@ -285,9 +283,9 @@ describe('Settings in Browser test suite', () => {
         });
         test('Sandbox checkbox click, non-sandboxed session, should lock', async () => {
           // Given
-          const $tabIcon = document.querySelector('.settings__tab[data-id="2"] .icon');
-          fireEvent.click($tabIcon);
-          await waitFor(() => expect($tabIcon.title).toEqual('Collapse'));
+          const $toggleIcon = document.querySelector('.settings__tab[data-id="2"] .expand-button');
+          fireEvent.click($toggleIcon);
+          await waitFor(() => expect($toggleIcon.title).toEqual('Collapse'));
           const $lockCheckBox = document.querySelector('.settings__tab-advanced .checkbox .fa-lock-open');
           // When
           fireEvent.click($lockCheckBox);
@@ -302,7 +300,7 @@ describe('Settings in Browser test suite', () => {
       let $input;
       let $submitButton;
       beforeEach(() => {
-        $input = document.querySelector('.settings__tab[data-id="1"] .input');
+        $input = document.querySelector('.settings__tab[data-id="1"] .text-field input');
         $submitButton = document.querySelector('.settings__submit');
       });
       test('with valid URL, should enable save button', async () => {