Skip to content

Commit c284ed0

Browse files
authored
Tabs: Improve Tab button UX for long tabs. (#103)
* Improve Tab button UX for long tabs. * Use CSS class. * Localize aria label. * Fix mismatch validation error. * Fix mismatch validation error.
1 parent 1ed15cb commit c284ed0

5 files changed

Lines changed: 266 additions & 18 deletions

File tree

tabs/src/tabs/edit.js

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -178,23 +178,35 @@ function TabsEdit( {
178178
/>
179179
</BlockControls>
180180
<div { ...blockProps }>
181-
<div role="tablist">
182-
{ tabBlocks.map( ( tabBlock, index ) => {
183-
const tabNumber = index + 1;
184-
return (
185-
<TabButton
186-
key={ tabBlock.clientId }
187-
clientId={ tabBlock.clientId }
188-
isActiveTab={
189-
! hasTabSelected && activeTab === tabNumber
190-
}
191-
tabNumber={ tabNumber }
192-
setActiveTab={ setAttributes.bind( null, {
193-
activeTab: tabNumber,
194-
} ) }
195-
/>
196-
);
197-
} ) }
181+
<div className="tabs-container">
182+
<div
183+
className="scroll-arrow scroll-arrow-left"
184+
aria-label={ __( 'Scroll tabs left', 'tabs' ) }
185+
role="button"
186+
></div>
187+
<div role="tablist" className="tablist-wrapper">
188+
{ tabBlocks.map( ( tabBlock, index ) => {
189+
const tabNumber = index + 1;
190+
return (
191+
<TabButton
192+
key={ tabBlock.clientId }
193+
clientId={ tabBlock.clientId }
194+
isActiveTab={
195+
! hasTabSelected && activeTab === tabNumber
196+
}
197+
tabNumber={ tabNumber }
198+
setActiveTab={ setAttributes.bind( null, {
199+
activeTab: tabNumber,
200+
} ) }
201+
/>
202+
);
203+
} ) }
204+
</div>
205+
<div
206+
className="scroll-arrow scroll-arrow-right"
207+
aria-label={ __( 'Scroll tabs right', 'tabs' ) }
208+
role="button"
209+
></div>
198210
</div>
199211
<InnerBlocks
200212
__experimentalCaptureToolbars

tabs/src/tabs/editor.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
}
1313
}
1414

15+
// Hide scroll arrows in editor
16+
.scroll-arrow {
17+
display: none !important;
18+
}
19+
1520
.tab-button-text {
1621
display: inline-block;
1722
min-width: 60px;

tabs/src/tabs/save.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* WordPress dependencies
33
*/
44
import { InnerBlocks, RichText, useBlockProps } from '@wordpress/block-editor';
5+
import { __ } from '@wordpress/i18n';
56

67
function TabButton( { isSelected, tabNumber, title } ) {
78
return (
@@ -41,7 +42,21 @@ export default function save( { attributes: { tabs } } ) {
4142

4243
return (
4344
<div { ...blockProps }>
44-
<div role="tablist">{ tabButtons }</div>
45+
<div className="tabs-container">
46+
<div
47+
className="scroll-arrow scroll-arrow-left"
48+
role="button"
49+
aria-label={ __( 'Scroll tabs left', 'tabs' ) }
50+
></div>
51+
<div role="tablist" className="tablist-wrapper">
52+
{ tabButtons }
53+
</div>
54+
<div
55+
className="scroll-arrow scroll-arrow-right"
56+
role="button"
57+
aria-label={ __( 'Scroll tabs right', 'tabs' ) }
58+
></div>
59+
</div>
4560
<InnerBlocks.Content />
4661
</div>
4762
);

tabs/src/tabs/style.scss

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,91 @@
55
@use "@wordpress/base-styles/mixins" as *;
66

77
.wp-block-wpcomsp-tabs {
8+
.tabs-container {
9+
display: flex;
10+
align-items: center;
11+
position: relative;
12+
width: 100%;
13+
}
14+
15+
.scroll-arrow {
16+
display: none;
17+
position: absolute;
18+
top: 50%;
19+
transform: translateY(-50%);
20+
z-index: 10;
21+
background: rgba(255, 255, 255, 0.9);
22+
border: 1px solid #ccc;
23+
border-radius: 4px;
24+
padding: 8px 12px;
25+
cursor: pointer;
26+
font-size: 16px;
27+
font-weight: bold;
28+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
29+
transition: all 0.2s ease;
30+
31+
&.hidden {
32+
display: none !important;
33+
}
34+
35+
&::before {
36+
content: '';
37+
display: inline-block;
38+
width: 0;
39+
height: 0;
40+
border-style: solid;
41+
}
42+
43+
&:hover {
44+
background: rgba(255, 255, 255, 1);
45+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
46+
}
47+
48+
&:disabled {
49+
opacity: 0.5;
50+
cursor: not-allowed;
51+
}
52+
53+
&.scroll-arrow-left {
54+
left: 0;
55+
56+
&::before {
57+
border-width: 6px 8px 6px 0;
58+
border-color: transparent #333 transparent transparent;
59+
}
60+
}
61+
62+
&.scroll-arrow-right {
63+
right: 0;
64+
65+
&::before {
66+
border-width: 6px 0 6px 8px;
67+
border-color: transparent transparent transparent #333;
68+
}
69+
}
70+
}
71+
72+
.tablist-wrapper {
73+
flex: 1;
74+
overflow: hidden;
75+
position: relative;
76+
}
77+
878
[role="tablist"] {
979
min-width: 100%;
1080
display: flex;
1181
flex-direction: column;
1282
gap: 1px;
83+
overflow-x: auto;
84+
overflow-y: hidden;
85+
scroll-behavior: smooth;
86+
-webkit-overflow-scrolling: touch;
87+
scrollbar-width: none;
88+
-ms-overflow-style: none;
89+
90+
&::-webkit-scrollbar {
91+
display: none;
92+
}
1393
}
1494

1595
.tab {
@@ -65,13 +145,29 @@
65145
}
66146

67147
@include break-mobile {
148+
.scroll-arrow {
149+
display: block;
150+
}
151+
68152
[role="tablist"] {
69153
flex-direction: row;
154+
overflow-x: auto;
155+
overflow-y: hidden;
156+
scroll-behavior: smooth;
157+
-webkit-overflow-scrolling: touch;
158+
scrollbar-width: none;
159+
-ms-overflow-style: none;
160+
161+
&::-webkit-scrollbar {
162+
display: none;
163+
}
70164
}
71165

72166
.tab {
73167
max-width: 22%;
74168
text-align: left;
169+
flex-shrink: 0;
170+
min-width: fit-content;
75171
}
76172

77173
.tab .tab-button-text {

tabs/src/tabs/view.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,118 @@ class TabsAutomatic {
126126
}
127127
}
128128

129+
class TabsScrollHandler {
130+
constructor( container ) {
131+
this.container = container;
132+
this.tablist = container.querySelector( '[role="tablist"]' );
133+
this.leftArrow = container.querySelector( '.scroll-arrow-left' );
134+
this.rightArrow = container.querySelector( '.scroll-arrow-right' );
135+
136+
if ( ! this.tablist || ! this.leftArrow || ! this.rightArrow ) {
137+
return;
138+
}
139+
140+
this.init();
141+
}
142+
143+
init() {
144+
this.updateArrowVisibility();
145+
this.bindEvents();
146+
this.handleResize();
147+
}
148+
149+
bindEvents() {
150+
// Scroll arrow click events
151+
this.leftArrow.addEventListener( 'click', () => {
152+
if ( ! this.leftArrow.classList.contains( 'hidden' ) ) {
153+
this.scrollLeft();
154+
}
155+
} );
156+
157+
this.rightArrow.addEventListener( 'click', () => {
158+
if ( ! this.rightArrow.classList.contains( 'hidden' ) ) {
159+
this.scrollRight();
160+
}
161+
} );
162+
163+
// Keyboard support for scroll arrows
164+
this.leftArrow.addEventListener( 'keydown', ( event ) => {
165+
if ( ( event.key === 'Enter' || event.key === ' ' ) && ! this.leftArrow.classList.contains( 'hidden' ) ) {
166+
event.preventDefault();
167+
this.scrollLeft();
168+
}
169+
} );
170+
171+
this.rightArrow.addEventListener( 'keydown', ( event ) => {
172+
if ( ( event.key === 'Enter' || event.key === ' ' ) && ! this.rightArrow.classList.contains( 'hidden' ) ) {
173+
event.preventDefault();
174+
this.scrollRight();
175+
}
176+
} );
177+
178+
// Tablist scroll event
179+
this.tablist.addEventListener( 'scroll', () => {
180+
this.updateArrowVisibility();
181+
} );
182+
183+
// Window resize event
184+
window.addEventListener( 'resize', () => {
185+
this.handleResize();
186+
} );
187+
}
188+
189+
scrollLeft() {
190+
const scrollAmount = this.tablist.clientWidth * 0.8;
191+
this.tablist.scrollBy( {
192+
left: -scrollAmount,
193+
behavior: 'smooth'
194+
} );
195+
}
196+
197+
scrollRight() {
198+
const scrollAmount = this.tablist.clientWidth * 0.8;
199+
this.tablist.scrollBy( {
200+
left: scrollAmount,
201+
behavior: 'smooth'
202+
} );
203+
}
204+
205+
updateArrowVisibility() {
206+
const { scrollLeft, scrollWidth, clientWidth } = this.tablist;
207+
const isScrollable = scrollWidth > clientWidth;
208+
209+
// Show/hide arrows based on scrollability
210+
if ( ! isScrollable ) {
211+
this.leftArrow.classList.add( 'hidden' );
212+
this.rightArrow.classList.add( 'hidden' );
213+
return;
214+
}
215+
216+
// Show/hide arrows based on scroll position
217+
// Left arrow: hide when at the beginning, show when there's content to scroll left
218+
if ( scrollLeft <= 0 ) {
219+
this.leftArrow.classList.add( 'hidden' );
220+
} else {
221+
this.leftArrow.classList.remove( 'hidden' );
222+
}
223+
224+
// Right arrow: hide when at the end, show when there's content to scroll right
225+
if ( scrollLeft >= scrollWidth - clientWidth - 1 ) {
226+
this.rightArrow.classList.add( 'hidden' );
227+
} else {
228+
this.rightArrow.classList.remove( 'hidden' );
229+
}
230+
}
231+
232+
handleResize() {
233+
// Debounce resize handling
234+
clearTimeout( this.resizeTimeout );
235+
this.resizeTimeout = setTimeout( () => {
236+
this.updateArrowVisibility();
237+
}, 100 );
238+
}
239+
}
240+
129241
// Initialize tablists.
130242
window.addEventListener( 'load', function () {
131243
const tablists = document.querySelectorAll(
@@ -134,4 +246,12 @@ window.addEventListener( 'load', function () {
134246
for ( let i = 0; i < tablists.length; i++ ) {
135247
new TabsAutomatic( tablists[ i ] );
136248
}
249+
250+
// Initialize scroll arrows for tabs
251+
const tabsContainers = document.querySelectorAll(
252+
'.wp-block-wpcomsp-tabs .tabs-container'
253+
);
254+
for ( let i = 0; i < tabsContainers.length; i++ ) {
255+
new TabsScrollHandler( tabsContainers[ i ] );
256+
}
137257
} );

0 commit comments

Comments
 (0)