PluginProbe
CryptX / 4.2.1
CryptX v4.2.1
4.2.1 4.2.0 4.1.1 trunk 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.9 2.0 2.1 2.2 2.3 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 All 93 releases
cryptx / src / components / Tabs.js

Tabs.js in CryptX 4.2.1, at src/components/Tabs.js

126 lines 3.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * The tab bar.
3 *
4 * Built by hand rather than with TabPanel, because the address has to carry
5 * the tab: a bookmark, a reload and a link shared with a colleague should all
6 * arrive in the same place. TabPanel keeps that state to itself.
7 *
8 * On a narrow screen the bar scrolls sideways instead of wrapping, which keeps
9 * the tabs on one line and the content where the eye expects it.
10 */
11
12 import { useEffect, useRef, useState } from '@wordpress/element';
13
14 export default function Tabs( { tabs, active, onSelect } ) {
15 const listRef = useRef( null );
16 const [ atEnd, setAtEnd ] = useState( false );
17
18 // Keep the selected tab in view. Arriving with ?tab=advanced on a phone
19 // otherwise showed the first tab underlined off-screen and the last one cut
20 // in half, with nothing to say the bar could be scrolled at all.
21 useEffect( () => {
22 const list = listRef.current;
23
24 if ( ! list ) {
25 return undefined;
26 }
27
28 // scrollLeft by hand rather than scrollIntoView: the latter walks up the
29 // ancestors and scrolls whatever it finds, which on a phone dragged the
30 // whole admin page a few pixels sideways every time a tab was picked.
31 // This touches nothing but the bar itself.
32 const activeTab = list.querySelector( '.is-active' );
33
34 if ( activeTab ) {
35 const overflowRight =
36 activeTab.offsetLeft +
37 activeTab.offsetWidth -
38 ( list.scrollLeft + list.clientWidth );
39 const overflowLeft = list.scrollLeft - activeTab.offsetLeft;
40
41 if ( overflowRight > 0 ) {
42 list.scrollLeft += overflowRight + 16;
43 } else if ( overflowLeft > 0 ) {
44 list.scrollLeft -= overflowLeft + 16;
45 }
46 }
47
48 // The fade at the right edge announces "there is more". Once the end is
49 // reached it would be announcing something untrue, so it is dropped.
50 const update = () => {
51 const remaining =
52 list.scrollWidth - list.scrollLeft - list.clientWidth;
53 setAtEnd( remaining <= 1 );
54 };
55
56 update();
57 list.addEventListener( 'scroll', update, { passive: true } );
58 window.addEventListener( 'resize', update );
59
60 return () => {
61 list.removeEventListener( 'scroll', update );
62 window.removeEventListener( 'resize', update );
63 };
64 }, [ active, tabs ] );
65
66 /**
67 * Left and right move between tabs, as the tab pattern expects.
68 *
69 * @param {Object} event The keyboard event.
70 * @param {number} index Position of the focused tab.
71 */
72 const onKeyDown = ( event, index ) => {
73 let next = null;
74
75 if ( event.key === 'ArrowRight' ) {
76 next = ( index + 1 ) % tabs.length;
77 } else if ( event.key === 'ArrowLeft' ) {
78 next = ( index - 1 + tabs.length ) % tabs.length;
79 } else if ( event.key === 'Home' ) {
80 next = 0;
81 } else if ( event.key === 'End' ) {
82 next = tabs.length - 1;
83 }
84
85 if ( next === null ) {
86 return;
87 }
88
89 event.preventDefault();
90 onSelect( tabs[ next ].id );
91 listRef.current?.querySelectorAll( '[role="tab"]' )[ next ]?.focus();
92 };
93
94 return (
95 <div
96 className={ 'cryptx-tabs' + ( atEnd ? ' is-scrolled-end' : '' ) }
97 role="tablist"
98 ref={ listRef }
99 >
100 { tabs.map( ( tab, index ) => {
101 const isActive = tab.id === active;
102
103 return (
104 <button
105 key={ tab.id }
106 type="button"
107 role="tab"
108 id={ `cryptx-tab-${ tab.id }` }
109 aria-selected={ isActive }
110 aria-controls={ `cryptx-panel-${ tab.id }` }
111 tabIndex={ isActive ? 0 : -1 }
112 className={
113 'cryptx-tabs__tab' +
114 ( isActive ? ' is-active' : '' )
115 }
116 onClick={ () => onSelect( tab.id ) }
117 onKeyDown={ ( event ) => onKeyDown( event, index ) }
118 >
119 { tab.label }
120 </button>
121 );
122 } ) }
123 </div>
124 );
125 }
126