PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
← All changes | build/modules/block-library/tabs/view.js +81 -113 23.2.1 → trunk View file →
@@ -4,42 +4,10 @@
4 4 getContext,
5 5 getElement,
6 6 withSyncEvent
7 7 } from "@wordpress/interactivity";
8 -function createReadOnlyProxy(obj) {
9 - const arrayMutationMethods = /* @__PURE__ */ new Set([
10 - "push",
11 - "pop",
12 - "shift",
13 - "unshift",
14 - "splice",
15 - "sort",
16 - "reverse",
17 - "copyWithin",
18 - "fill"
19 - ]);
20 - return new Proxy(obj, {
21 - get(target, prop) {
22 - if (Array.isArray(target) && arrayMutationMethods.has(prop)) {
23 - return () => {
24 - };
25 - }
26 - const value = target[prop];
27 - if (typeof value === "object" && value !== null) {
28 - return createReadOnlyProxy(value);
29 - }
30 - return value;
31 - },
32 - set() {
33 - return false;
34 - },
35 - deleteProperty() {
36 - return false;
37 - }
38 - });
39 -}
40 -var { actions: privateActions, state: privateState } = store(
41 - "core/tabs/private",
8 +var { actions, state } = store(
9 + "core/tabs",
42 10 {
43 11 state: {
44 12 /**
45 13 * Gets a contextually aware list of tabs for the current tabs block.
@@ -48,9 +16,9 @@
48 16 */
49 17 get tabsList() {
50 18 const context = getContext();
51 19 const tabsId = context?.tabsId;
52 - const tabsList = privateState[tabsId];
20 + const tabsList = state[tabsId];
53 21 return tabsList;
54 22 },
55 23 /**
56 24 * Gets the index of the active tab element whether it
@@ -63,10 +31,10 @@
63 31 const tabId = attributes?.id?.replace("tab__", "") || null;
64 32 if (!tabId) {
65 33 return null;
66 34 }
67 - const { tabsList } = privateState;
68 - const tabIndex = tabsList.findIndex((t) => t.id === tabId);
35 + const { tabsList } = state;
36 + const tabIndex = tabsList.findIndex((t) => t === tabId);
69 37 return tabIndex;
70 38 },
71 39 /**
72 40 * Whether the tab panel or tab label is the active tab.
@@ -74,19 +42,37 @@
74 42 * @type {boolean}
75 43 */
76 44 get isActiveTab() {
77 45 const { activeTabIndex } = getContext();
78 - const { tabIndex } = privateState;
46 + const { tabIndex } = state;
79 47 return activeTabIndex === tabIndex;
80 48 },
81 49 /**
82 - * The value of the tabindex attribute for tab buttons.
83 - * Only the active tab should be in the tab sequence.
50 + * The value of the hidden attribute for tab panels.
84 51 *
52 + * Inactive panels use the `until-found` state rather than being
53 + * hidden outright, so their content stays reachable by the
54 + * browser's find-in-page and by fragment navigation. The browser
55 + * fires `beforematch` on the panel before revealing it, which is
56 + * where the matching tab gets activated.
57 + *
58 + * @type {string|null}
59 + */
60 + get isHidden() {
61 + return state.isActiveTab ? null : "until-found";
62 + },
63 + /**
64 + * The value of the tabindex attribute for tab buttons and tab
65 + * panels. Only the active tab should be in the tab sequence.
66 + *
67 + * An inactive panel is hidden with `until-found`, which leaves it
68 + * in the layout, so it stays focusable unless it is taken out of
69 + * the tab sequence here.
70 + *
85 71 * @type {number}
86 72 */
87 73 get tabIndexAttribute() {
88 - return privateState.isActiveTab ? 0 : -1;
74 + return state.isActiveTab ? 0 : -1;
89 75 }
90 76 },
91 77 actions: {
92 78 /**
@@ -94,26 +80,24 @@
94 80 *
95 81 * @param {KeyboardEvent} event The keydown event.
96 82 */
97 83 handleTabKeyDown: withSyncEvent((event) => {
98 - const context = getContext();
99 - const { isVertical } = context;
100 - const { tabIndex } = privateState;
84 + const { tabIndex, tabsList } = state;
101 85 if (tabIndex === null) {
102 86 return;
103 87 }
104 - if (event.key === "ArrowRight" && !isVertical) {
88 + if (event.key === "ArrowRight") {
105 89 event.preventDefault();
106 - privateActions.moveFocus(tabIndex + 1);
107 - } else if (event.key === "ArrowLeft" && !isVertical) {
90 + actions.moveFocus(tabIndex + 1);
91 + } else if (event.key === "ArrowLeft") {
108 92 event.preventDefault();
109 - privateActions.moveFocus(tabIndex - 1);
110 - } else if (event.key === "ArrowDown" && isVertical) {
93 + actions.moveFocus(tabIndex - 1);
94 + } else if (event.key === "Home") {
111 95 event.preventDefault();
112 - privateActions.moveFocus(tabIndex + 1);
113 - } else if (event.key === "ArrowUp" && isVertical) {
96 + actions.moveFocus(0);
97 + } else if (event.key === "End") {
114 98 event.preventDefault();
115 - privateActions.moveFocus(tabIndex - 1);
99 + actions.moveFocus(tabsList.length - 1);
116 100 }
117 101 }),
118 102 /**
119 103 * Handles the click event for the tab label.
@@ -121,20 +105,32 @@
121 105 * @param {MouseEvent} event The click event.
122 106 */
123 107 handleTabClick: withSyncEvent((event) => {
124 108 event.preventDefault();
125 - const { tabIndex } = privateState;
109 + const { tabIndex } = state;
126 110 if (tabIndex !== null) {
127 - privateActions.setActiveTab(tabIndex);
111 + actions.setActiveTab(tabIndex);
128 112 }
129 113 }),
130 114 /**
115 + * Activates the tab whose panel the browser is about to reveal.
116 + *
117 + * Fired on a panel hidden with `until-found` when find-in-page or
118 + * a fragment navigation matches content inside it.
119 + */
120 + handleBeforeMatch: () => {
121 + const { tabIndex } = state;
122 + if (tabIndex !== null) {
123 + actions.setActiveTab(tabIndex);
124 + }
125 + },
126 + /**
131 127 * Moves focus to a specific tab without activating it.
132 128 *
133 129 * @param {number} tabIndex The index to move focus to.
134 130 */
135 131 moveFocus: (tabIndex) => {
136 - const { tabsList } = privateState;
132 + const { tabsList } = state;
137 133 if (!tabsList || tabsList.length === 0) {
138 134 return;
139 135 }
140 136 let newIndex = tabIndex;
@@ -142,9 +138,9 @@
142 138 newIndex = tabsList.length - 1;
143 139 } else if (newIndex >= tabsList.length) {
144 140 newIndex = 0;
145 141 }
146 - const tabId = tabsList[newIndex].id;
142 + const tabId = tabsList[newIndex];
147 143 const tabElement = document.getElementById("tab__" + tabId);
148 144 if (tabElement) {
149 145 tabElement.focus();
150 146 }
@@ -149,15 +145,15 @@
149 145 tabElement.focus();
150 146 }
151 147 },
152 148 /**
153 - * Sets the active tab index (internal implementation).
149 + * Sets the active tab index.
154 150 *
155 151 * @param {number} tabIndex The index of the active tab.
156 152 * @param {boolean} scrollToTab Whether to scroll to the tab element.
157 153 */
158 154 setActiveTab: (tabIndex, scrollToTab = false) => {
159 - const { tabsList } = privateState;
155 + const { tabsList } = state;
160 156 if (!tabsList || tabsList.length === 0) {
161 157 return;
162 158 }
163 159 let newIndex = tabIndex;
@@ -168,9 +164,9 @@
168 164 }
169 165 const context = getContext();
170 166 context.activeTabIndex = newIndex;
171 167 if (scrollToTab) {
172 - const tabId = tabsList[newIndex].id;
168 + const tabId = tabsList[newIndex];
173 169 const tabElement = document.getElementById(tabId);
174 170 if (tabElement) {
175 171 setTimeout(() => {
176 172 tabElement.scrollIntoView({ behavior: "smooth" });
@@ -179,23 +175,37 @@
179 175 }
180 176 }
181 177 },
182 178 callbacks: {
183 - /**
184 - * When the tabs are initialized, we need to check if there is a hash in the url and if so if it exists in the current tabsList, set the active tab to that index.
185 - *
186 - */
187 - onTabsInit: () => {
188 - const { tabsList } = privateState;
189 - if (tabsList.length === 0) {
179 + activateTabByHash: () => {
180 + const { tabsList } = state;
181 + if (!tabsList || tabsList.length === 0) {
190 182 return;
191 183 }
192 - const { hash } = window.location;
193 - const tabId = hash.replace("#", "");
194 - const tabIndex = tabsList.findIndex((t) => t.id === tabId);
195 - if (tabIndex >= 0) {
196 - privateActions.setActiveTab(tabIndex, true);
184 + const targetElement = document.querySelector(":target");
185 + if (!targetElement) {
186 + return;
197 187 }
188 + const panelIndex = tabsList.findIndex(
189 + (t) => t === targetElement.id
190 + );
191 + if (panelIndex >= 0) {
192 + actions.setActiveTab(panelIndex, true);
193 + return;
194 + }
195 + let panel = targetElement.closest(".wp-block-tab-panel");
196 + let tabIndex = -1;
197 + while (panel && tabIndex < 0) {
198 + tabIndex = tabsList.findIndex((t) => t === panel.id);
199 + panel = panel.parentElement?.closest(".wp-block-tab-panel") ?? null;
200 + }
201 + if (tabIndex < 0) {
202 + return;
203 + }
204 + actions.setActiveTab(tabIndex);
205 + window.setTimeout(() => {
206 + targetElement.scrollIntoView();
207 + }, 0);
198 208 }
199 209 }
200 210 },
201 211 {
@@ -201,47 +211,5 @@
201 211 {
202 212 lock: true
203 213 }
204 214 );
205 -store("core/tabs", {
206 - state: {
207 - /**
208 - * Gets a contextually aware list of tabs for the current tabs block.
209 - * Public API for third-party access.
210 - *
211 - * @type {Array}
212 - */
213 - get tabsList() {
214 - return createReadOnlyProxy(privateState.tabsList);
215 - },
216 - /**
217 - * Gets the index of the active tab element whether it
218 - * is a tab label or tab panel.
219 - *
220 - * @type {number|null}
221 - */
222 - get tabIndex() {
223 - return privateState.tabIndex;
224 - },
225 - /**
226 - * Whether the tab panel or tab label is the active tab.
227 - *
228 - * @type {boolean}
229 - */
230 - get isActiveTab() {
231 - return privateState.isActiveTab;
232 - }
233 - },
234 - actions: {
235 - /**
236 - * Sets the active tab index.
237 - * Public API for third-party programmatic tab activation.
238 - *
239 - * @param {number} tabIndex The index of the active tab.
240 - * @param {boolean} scrollToTab Whether to scroll to the tab element.
241 - */
242 - setActiveTab: (tabIndex, scrollToTab = false) => {
243 - privateActions.setActiveTab(tabIndex, scrollToTab);
244 - }
245 - }
246 -});
247 215 //# sourceMappingURL=view.js.map