From c65b980ada20a3322e7d8fb91d305ccf9901638e Mon Sep 17 00:00:00 2001 From: Elena Baidakova Date: Wed, 19 Oct 2022 15:20:15 +0400 Subject: [PATCH] Added tabs component (#15652) closes TryGhost/Team#2086 --- .../admin/app/components/posts/analytics.hbs | 70 +++++------ ghost/admin/app/components/tabs/tab-panel.hbs | 11 ++ ghost/admin/app/components/tabs/tab-panel.js | 13 ++ ghost/admin/app/components/tabs/tab.hbs | 13 ++ ghost/admin/app/components/tabs/tab.js | 24 ++++ ghost/admin/app/components/tabs/tabs.hbs | 30 +++++ ghost/admin/app/components/tabs/tabs.js | 65 ++++++++++ ghost/admin/app/styles/layouts/content.css | 117 +++++++++++++++++- .../integration/components/tabs/tabs-test.js | 105 ++++++++++++++++ 9 files changed, 410 insertions(+), 38 deletions(-) create mode 100644 ghost/admin/app/components/tabs/tab-panel.hbs create mode 100644 ghost/admin/app/components/tabs/tab-panel.js create mode 100644 ghost/admin/app/components/tabs/tab.hbs create mode 100644 ghost/admin/app/components/tabs/tab.js create mode 100644 ghost/admin/app/components/tabs/tabs.hbs create mode 100644 ghost/admin/app/components/tabs/tabs.js create mode 100644 ghost/admin/tests/integration/components/tabs/tabs-test.js diff --git a/ghost/admin/app/components/posts/analytics.hbs b/ghost/admin/app/components/posts/analytics.hbs index 12877b6fb7..d821d6e09b 100644 --- a/ghost/admin/app/components/posts/analytics.hbs +++ b/ghost/admin/app/components/posts/analytics.hbs @@ -1,5 +1,4 @@
-
@@ -42,53 +41,52 @@
-

- Engagement -

-
+ {{#if this.post.hasBeenEmailed}} -
- -

{{format-number this.post.email.emailCount}}

-

Sent

-
-
+ +

Sent

+

{{format-number this.post.email.emailCount}}

+
+ + Sent {{#if this.post.showEmailOpenAnalytics }} -
- -

{{format-number this.post.email.openedCount}}

-

Opened — {{this.post.email.openRate}}%

-
-
+ +

Opened

+

{{format-number this.post.email.openedCount}} {{this.post.email.openRate}}%

+
+ + Opened {{/if}} {{#if this.post.showEmailClickAnalytics }} -
- -

{{format-number this.post.count.clicks}}

-

Clicked — {{this.post.clickRate}}%

-
-
+ +

Clicked

+

{{format-number this.post.count.clicks}} {{this.post.clickRate}}%

+
+ + Clicked {{/if}} {{/if}} - {{#if this.post.showAttributionAnalytics }} -
- -

{{format-number this.post.count.conversions}}

-

{{gh-pluralize this.post.count.conversions "conversions" without-count=true}}

-
-
+ {{#if this.post.showAudienceFeedback }} + +

More
like
this

+

{{format-number this.post.count.positive_feedback}} {{this.post.count.sentiment}}%

+
+ + More like this {{/if}} - {{#if this.post.showAudienceFeedback }} -
-

{{format-number this.post.count.positive_feedback}}

-

More like this{{this.post.count.sentiment}}%

-
+ {{#if this.post.showAttributionAnalytics }} + +

{{gh-pluralize this.post.count.conversions "Conversions" without-count=true}}

+

{{format-number this.post.count.conversions}}

+
+ + Conversions {{/if}} -
+ {{#if this.isLoaded }} {{#if this.showLinks }} diff --git a/ghost/admin/app/components/tabs/tab-panel.hbs b/ghost/admin/app/components/tabs/tab-panel.hbs new file mode 100644 index 0000000000..d48c7b24d8 --- /dev/null +++ b/ghost/admin/app/components/tabs/tab-panel.hbs @@ -0,0 +1,11 @@ +
+ {{#if this.isSelectedTab}} + {{yield}} + {{/if}} +
diff --git a/ghost/admin/app/components/tabs/tab-panel.js b/ghost/admin/app/components/tabs/tab-panel.js new file mode 100644 index 0000000000..57ba1dce13 --- /dev/null +++ b/ghost/admin/app/components/tabs/tab-panel.js @@ -0,0 +1,13 @@ +import Component from '@glimmer/component'; + +export default class TabPanelComponent extends Component { + index = this.args.index(); + + get isSelectedTab() { + return this.args.selectedIndex === this.index; + } + + get tabId() { + return this.args.tabIds[this.index]; + } +} diff --git a/ghost/admin/app/components/tabs/tab.hbs b/ghost/admin/app/components/tabs/tab.hbs new file mode 100644 index 0000000000..9bfe05dd66 --- /dev/null +++ b/ghost/admin/app/components/tabs/tab.hbs @@ -0,0 +1,13 @@ + diff --git a/ghost/admin/app/components/tabs/tab.js b/ghost/admin/app/components/tabs/tab.js new file mode 100644 index 0000000000..8f8c334085 --- /dev/null +++ b/ghost/admin/app/components/tabs/tab.js @@ -0,0 +1,24 @@ +import Component from '@glimmer/component'; +import {action} from '@ember/object'; +import {guidFor} from '@ember/object/internals'; + +export default class TabComponent extends Component { + id = this.args.id(`tab-${guidFor(this)}`); + index = this.args.index(); + + get isSelectedTab() { + return this.args.selectedIndex === this.index; + } + + @action + handleClick() { + return this.args.onSelect(this.index); + } + + @action + handleKeyup(event) { + event.stopPropagation(); + event.preventDefault(); + return this.args.onKeyup(event, this.index); + } +} diff --git a/ghost/admin/app/components/tabs/tabs.hbs b/ghost/admin/app/components/tabs/tabs.hbs new file mode 100644 index 0000000000..8acac61084 --- /dev/null +++ b/ghost/admin/app/components/tabs/tabs.hbs @@ -0,0 +1,30 @@ +
+
+ {{yield + (hash + tab=( + component "tabs/tab" + selectedIndex=this.selectedIndex + onSelect=this.handleSelect + onKeyup=this.handleKeyup + id=this.addTabId + index=this.addTabIndex + ) + ) + }} +
+ + {{yield + (hash + tabPanel=( + component "tabs/tab-panel" + selectedIndex=this.selectedIndex + index=this.addPanelIndex + tabIds=this.tabIds + ) + ) + }} +
diff --git a/ghost/admin/app/components/tabs/tabs.js b/ghost/admin/app/components/tabs/tabs.js new file mode 100644 index 0000000000..d7412fe6b9 --- /dev/null +++ b/ghost/admin/app/components/tabs/tabs.js @@ -0,0 +1,65 @@ +import Component from '@glimmer/component'; +import {action} from '@ember/object'; +import {tracked} from '@glimmer/tracking'; + +export default class TabsComponent extends Component { + tabIds = []; + tabCounter = 0; + tabPanelCounter = 0; + @tracked selectedIndex = this.args.defaultIndex ?? 0; + + @action + handleSelect(index) { + this.selectedIndex = index; + } + + @action + handleKeyup(event, index) { + switch (event.key) { + case 'ArrowLeft': + this.selectedIndex = this.tabIds[index - 1] ? index - 1 : this.tabIds.length - 1; + break; + + case 'ArrowRight': + this.selectedIndex = this.tabIds[index + 1] ? index + 1 : 0; + break; + + case 'Home': + this.selectedIndex = 0; + break; + + case 'End': + this.selectedIndex = this.tabIds.length - 1; + break; + + default: + break; + } + + const selectedNode = document.getElementById(this.tabIds[this.selectedIndex]); + selectedNode.focus(); + } + + @action + addTabId(id) { + this.tabIds.push(id); + + return id; + } + + @action + addTabIndex() { + const index = this.tabCounter; + this.tabCounter = this.tabCounter + 1; + + return index; + } + + @action + addPanelIndex() { + const index = this.tabPanelCounter; + this.tabPanelCounter = this.tabPanelCounter + 1; + + return index; + } +} diff --git a/ghost/admin/app/styles/layouts/content.css b/ghost/admin/app/styles/layouts/content.css index a115030323..991b6d6f22 100644 --- a/ghost/admin/app/styles/layouts/content.css +++ b/ghost/admin/app/styles/layouts/content.css @@ -467,7 +467,7 @@ a.gh-post-list-signups.active:hover > span, a.gh-post-list-conversions.active:ho .feature-memberAttribution .gh-posts-sends-header { width: 80px; } - + .feature-memberAttribution .gh-posts-opens-header { width: 100px; } @@ -635,6 +635,11 @@ a.gh-post-list-signups.active:hover > span, a.gh-post-list-conversions.active:ho .hide-when-small { display: none; } + + .visible-when-small::first-letter{ + display: inline-block; + text-transform: uppercase; + } } @media (max-width: 800px) { @@ -1408,7 +1413,7 @@ a.gh-post-list-cta.stats.is-hovered:hover > * { border-bottom: 1px solid var(--whitegrey-d1); padding-left: 0; padding-bottom: 2rem; - padding-top: 2rem; + padding-top: 2rem; } .gh-post-analytics-item:first-child { @@ -1430,3 +1435,111 @@ a.gh-post-list-cta.stats.is-hovered:hover > * { font-size: 1.8rem; } } + +.gh-tabs-analytics { + overflow: hidden; + margin-bottom: 22px; + border-radius: 5px; + border: 1px solid #ECEEF0; +} + +.gh-tabs-analytics .tab{ + display: flex; + flex-direction: column; + justify-content: flex-start; + border: 1px solid transparent; + border-bottom: none; + padding: 16px 20px; + text-align: left; +} + +.gh-tabs-analytics .tab-selected{ + box-sizing: border-box; + background: #ffffff; + border: 1px solid #E7E9EB; + border-bottom: none; + box-shadow: 0 4px 7px rgba(0, 0, 0, 0.05), 0 1px 0 0 #ffffff; + border-radius: 5px 5px 0 0; +} + +.gh-tabs-analytics .tab-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); + padding: 8px 8px 0px; + background-color: #F7F8F9; + box-shadow: inset 0 -1px 0 #eceef0; +} + +.gh-tabs-analytics .tab-panel-selected { + padding: 12px 26px; + /* help to hide shadow from selected tab */ + opacity: 0.99999; + background-color: #ffffff; +} + +.gh-tabs-analytics h3 { + display: flex; + flex-direction: row; + gap: 3px; + font-size: 1.5rem; + font-weight: 700; + line-height: 1.2; +} + +.gh-tabs-analytics p { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 4px; + margin: 0; + font-size: 2.4rem; + font-weight: 600; + line-height: 1.2; +} + +.gh-tabs-analytics strong { + color: #909CAB; + font-size: 1.5rem; + font-weight: 500; + line-height: 1.2; +} + +@media (max-width: 1200px) { + .gh-tabs-analytics .tab{ + padding: 8px 10px; + } + + .gh-tabs-analytics .tab-panel-selected { + padding: 12px 18px; + } + + .gh-tabs-analytics .tab-list { + grid-template-columns: repeat(auto-fit, minmax(60px, 1fr)); + } + + .gh-tabs-analytics h3 { + font-size: 1.2rem; + } + + .gh-tabs-analytics p { + font-size: 1.6rem; + } +} + +@media (max-width: 440px) { + .gh-tabs-analytics .tab-list { + padding: 4px 4px 0px; + } + + .gh-tabs-analytics .tab-panel-selected { + padding: 12px 14px; + } + + .gh-tabs-analytics p { + font-size: 1.2rem; + } + + .gh-tabs-analytics strong { + font-size: 1.2rem; + } +} diff --git a/ghost/admin/tests/integration/components/tabs/tabs-test.js b/ghost/admin/tests/integration/components/tabs/tabs-test.js new file mode 100644 index 0000000000..bc66ddbed9 --- /dev/null +++ b/ghost/admin/tests/integration/components/tabs/tabs-test.js @@ -0,0 +1,105 @@ +import {click, findAll, render, triggerKeyEvent} from '@ember/test-helpers'; +import {describe, it} from 'mocha'; +import {expect} from 'chai'; +import {hbs} from 'ember-cli-htmlbars'; +import {setupRenderingTest} from 'ember-mocha'; + +describe('Integration: Component: tabs/tabs', function () { + setupRenderingTest(); + + it('renders', async function () { + await render(hbs` + + Tab 1 + Tab 2 + + Content 1 + Content 2 + `); + + const tabButtons = findAll('.tab'); + const tabPanels = findAll('.tab-panel'); + + expect(findAll('.test-tab').length).to.equal(1); + expect(findAll('.tab-list').length).to.equal(1); + expect(tabPanels.length).to.equal(2); + expect(tabButtons.length).to.equal(2); + + expect(findAll('.tab-selected').length).to.equal(1); + expect(findAll('.tab-panel-selected').length).to.equal(1); + expect(tabButtons[0]).to.have.class('tab-selected'); + expect(tabPanels[0]).to.have.class('tab-panel-selected'); + + expect(tabButtons[0]).to.have.trimmed.text('Tab 1'); + expect(tabButtons[1]).to.have.trimmed.text('Tab 2'); + + expect(tabPanels[0]).to.have.trimmed.text('Content 1'); + expect(tabPanels[1]).to.have.trimmed.text(''); + }); + + it('renders expected content on click', async function () { + await render(hbs` + + Tab 1 + Tab 2 + + Content 1 + Content 2 + `); + + const tabButtons = findAll('.tab'); + const tabPanels = findAll('.tab-panel'); + + await click(tabButtons[1]); + + expect(findAll('.tab-selected').length).to.equal(1); + expect(findAll('.tab-panel-selected').length).to.equal(1); + expect(tabButtons[1]).to.have.class('tab-selected'); + expect(tabPanels[1]).to.have.class('tab-panel-selected'); + + expect(tabPanels[0]).to.have.trimmed.text(''); + expect(tabPanels[1]).to.have.trimmed.text('Content 2'); + }); + + it('renders expected content on keyup event', async function () { + await render(hbs` + + Tab 0 + Tab 1 + Tab 2 + + Content 0 + Content 1 + Content 2 + `); + + const tabButtons = findAll('.tab'); + const tabPanels = findAll('.tab-panel'); + + const isTabRenders = (num) => { + expect(tabButtons[num]).to.have.class('tab-selected'); + expect(tabPanels[num]).to.have.class('tab-panel-selected'); + + expect(tabPanels[num]).to.have.trimmed.text(`Content ${num}`); + }; + + await triggerKeyEvent(tabButtons[0], 'keyup', 'ArrowRight'); + await triggerKeyEvent(tabButtons[1], 'keyup', 'ArrowRight'); + isTabRenders(2); + + await triggerKeyEvent(tabButtons[2], 'keyup', 'ArrowRight'); + isTabRenders(0); + + await triggerKeyEvent(tabButtons[0], 'keyup', 'ArrowLeft'); + isTabRenders(2); + + await triggerKeyEvent(tabButtons[2], 'keyup', 'ArrowLeft'); + isTabRenders(1); + + await triggerKeyEvent(tabButtons[0], 'keyup', 'Home'); + isTabRenders(0); + + await triggerKeyEvent(tabButtons[0], 'keyup', 'End'); + isTabRenders(2); + }); +});