@atlaskit/navigation-system 11.3.0 → 11.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/ui/page-layout/calculate-layout-area-states.js +76 -37
  3. package/dist/cjs/ui/page-layout/chat-panel.js +6 -3
  4. package/dist/cjs/ui/page-layout/layout-area-sizing-context.js +2 -2
  5. package/dist/cjs/ui/page-layout/layout-area-sizing-provider.js +243 -247
  6. package/dist/cjs/ui/page-layout/layout-area-sizing-runtime.js +468 -0
  7. package/dist/cjs/ui/page-layout/main/main.js +1 -1
  8. package/dist/cjs/ui/page-layout/panel-splitter/panel-splitter.js +239 -99
  9. package/dist/cjs/ui/page-layout/panel-splitter/provider.js +3 -1
  10. package/dist/cjs/ui/page-layout/root.compiled.css +14 -3
  11. package/dist/cjs/ui/page-layout/root.js +43 -17
  12. package/dist/cjs/ui/page-layout/side-nav/side-nav.js +14 -3
  13. package/dist/cjs/ui/page-layout/use-css-layout-area-states.js +117 -0
  14. package/dist/cjs/ui/page-layout/use-layout-area-sizing.js +3 -1
  15. package/dist/es2019/ui/page-layout/calculate-layout-area-states.js +51 -17
  16. package/dist/es2019/ui/page-layout/chat-panel.js +29 -5
  17. package/dist/es2019/ui/page-layout/layout-area-sizing-context.js +2 -2
  18. package/dist/es2019/ui/page-layout/layout-area-sizing-provider.js +171 -241
  19. package/dist/es2019/ui/page-layout/layout-area-sizing-runtime.js +419 -0
  20. package/dist/es2019/ui/page-layout/main/main.js +2 -2
  21. package/dist/es2019/ui/page-layout/panel-splitter/panel-splitter.js +60 -10
  22. package/dist/es2019/ui/page-layout/panel-splitter/provider.js +3 -1
  23. package/dist/es2019/ui/page-layout/root.compiled.css +14 -3
  24. package/dist/es2019/ui/page-layout/root.js +30 -3
  25. package/dist/es2019/ui/page-layout/side-nav/side-nav.js +23 -3
  26. package/dist/es2019/ui/page-layout/use-css-layout-area-states.js +73 -0
  27. package/dist/es2019/ui/page-layout/use-layout-area-sizing.js +3 -1
  28. package/dist/esm/ui/page-layout/calculate-layout-area-states.js +75 -37
  29. package/dist/esm/ui/page-layout/chat-panel.js +7 -4
  30. package/dist/esm/ui/page-layout/layout-area-sizing-context.js +2 -2
  31. package/dist/esm/ui/page-layout/layout-area-sizing-provider.js +243 -248
  32. package/dist/esm/ui/page-layout/layout-area-sizing-runtime.js +459 -0
  33. package/dist/esm/ui/page-layout/main/main.js +2 -2
  34. package/dist/esm/ui/page-layout/panel-splitter/panel-splitter.js +238 -98
  35. package/dist/esm/ui/page-layout/panel-splitter/provider.js +3 -1
  36. package/dist/esm/ui/page-layout/root.compiled.css +14 -3
  37. package/dist/esm/ui/page-layout/root.js +42 -16
  38. package/dist/esm/ui/page-layout/side-nav/side-nav.js +14 -3
  39. package/dist/esm/ui/page-layout/use-css-layout-area-states.js +110 -0
  40. package/dist/esm/ui/page-layout/use-layout-area-sizing.js +3 -1
  41. package/dist/types/ui/page-layout/calculate-layout-area-states.d.ts +7 -2
  42. package/dist/types/ui/page-layout/layout-area-sizing-context.d.ts +10 -3
  43. package/dist/types/ui/page-layout/layout-area-sizing-provider.d.ts +6 -0
  44. package/dist/types/ui/page-layout/layout-area-sizing-runtime.d.ts +8 -0
  45. package/dist/types/ui/page-layout/panel-splitter/panel-splitter-context.d.ts +4 -0
  46. package/dist/types/ui/page-layout/panel-splitter/provider.d.ts +1 -1
  47. package/dist/types/ui/page-layout/root.d.ts +2 -0
  48. package/dist/types/ui/page-layout/use-css-layout-area-states.d.ts +8 -0
  49. package/dist/types/ui/page-layout/use-layout-area-sizing.d.ts +2 -0
  50. package/package.json +12 -6
@@ -0,0 +1,419 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { bind } from 'bind-event-listener';
3
+ import { calculateLayoutAreaStates } from './calculate-layout-area-states';
4
+ import { LayoutAreaSizingContext } from './layout-area-sizing-context';
5
+ import { layoutAreaStateContexts } from './layout-area-state-contexts';
6
+ import { resolveLayoutWidth } from './resolve-layout-width';
7
+ import { useOccupiedLayoutWidth } from './use-occupied-layout-width';
8
+ const defaultMainMinWidth = 320;
9
+ function useStableAreaState(state) {
10
+ const {
11
+ mode,
12
+ width,
13
+ minWidth
14
+ } = state !== null && state !== void 0 ? state : {};
15
+ return useMemo(() => mode !== undefined && width !== undefined && minWidth !== undefined ? {
16
+ mode,
17
+ width,
18
+ minWidth
19
+ } : undefined, [mode, width, minWidth]);
20
+ }
21
+ export function LayoutAreaSizingRuntime({
22
+ children,
23
+ layoutRef,
24
+ onReady,
25
+ onUpdate
26
+ }) {
27
+ const occupiedWidth = useOccupiedLayoutWidth(layoutRef);
28
+ const [dimensions, setDimensions] = useState(null);
29
+ const [mainMinWidth, setMainMinWidth] = useState(defaultMainMinWidth);
30
+ const [areas, setAreas] = useState({});
31
+ const [resizeSession, setResizeSession] = useState(null);
32
+ const areaStatesRef = React.useRef({});
33
+ const configsRef = React.useRef({});
34
+ const [hasInteraction, setHasInteraction] = useState(false);
35
+ const adoptedConstraints = React.useRef(null);
36
+ // Preserve the CSS handoff only while its available space is unchanged.
37
+ const isPreservingInitialLayout = !hasInteraction && (!adoptedConstraints.current || adoptedConstraints.current.occupiedWidth === occupiedWidth && adoptedConstraints.current.mainMinWidth === mainMinWidth);
38
+ const [initialStates, setInitialStates] = useState({});
39
+ const inputsRef = React.useRef({
40
+ dimensions,
41
+ occupiedWidth,
42
+ mainMinWidth,
43
+ areas,
44
+ resizeSession
45
+ });
46
+ useEffect(() => {
47
+ inputsRef.current = {
48
+ dimensions,
49
+ occupiedWidth,
50
+ mainMinWidth,
51
+ areas,
52
+ resizeSession
53
+ };
54
+ }, [dimensions, occupiedWidth, mainMinWidth, areas, resizeSession]);
55
+ useEffect(() => {
56
+ if (adoptedConstraints.current && (adoptedConstraints.current.occupiedWidth !== occupiedWidth || adoptedConstraints.current.mainMinWidth !== mainMinWidth)) {
57
+ setHasInteraction(true);
58
+ }
59
+ setResizeSession(null);
60
+ }, [mainMinWidth, occupiedWidth]);
61
+ useEffect(() => {
62
+ const root = layoutRef === null || layoutRef === void 0 ? void 0 : layoutRef.current;
63
+ let previous = null;
64
+ // Match the actual rem-based grid breakpoint, including browser font settings.
65
+ const inlineChatQuery = window.matchMedia('(min-width: 40rem)');
66
+ const inlineSideNavQuery = window.matchMedia('(min-width: 64rem)');
67
+ const updateDimensions = contentWidth => {
68
+ var _style$width, _style$paddingLeft, _style$paddingRight, _style$borderLeftWidt, _style$borderRightWid, _previous;
69
+ const style = root && contentWidth === undefined ? getComputedStyle(root) : null;
70
+ const computedWidth = Number.parseFloat((_style$width = style === null || style === void 0 ? void 0 : style.width) !== null && _style$width !== void 0 ? _style$width : '');
71
+ const padding = (parseFloat((_style$paddingLeft = style === null || style === void 0 ? void 0 : style.paddingLeft) !== null && _style$paddingLeft !== void 0 ? _style$paddingLeft : '') || 0) + (parseFloat((_style$paddingRight = style === null || style === void 0 ? void 0 : style.paddingRight) !== null && _style$paddingRight !== void 0 ? _style$paddingRight : '') || 0);
72
+ const border = (parseFloat((_style$borderLeftWidt = style === null || style === void 0 ? void 0 : style.borderLeftWidth) !== null && _style$borderLeftWidt !== void 0 ? _style$borderLeftWidt : '') || 0) + (parseFloat((_style$borderRightWid = style === null || style === void 0 ? void 0 : style.borderRightWidth) !== null && _style$borderRightWid !== void 0 ? _style$borderRightWid : '') || 0);
73
+ const layoutWidth = contentWidth !== null && contentWidth !== void 0 ? contentWidth : root ?
74
+ // Keep fractional CSS pixels, just as ResizeObserver does. Otherwise
75
+ // its initial delivery could be mistaken for a viewport resize.
76
+ Math.max(0, Number.isFinite(computedWidth) ? computedWidth - ((style === null || style === void 0 ? void 0 : style.boxSizing) === 'border-box' ? padding + border : 0) : root.clientWidth - padding) : window.innerWidth;
77
+ const next = {
78
+ viewportWidth: window.innerWidth,
79
+ layoutWidth,
80
+ isChatPanelOverlay: !inlineChatQuery.matches,
81
+ isSideNavOverlay: !inlineSideNavQuery.matches
82
+ };
83
+ if (((_previous = previous) === null || _previous === void 0 ? void 0 : _previous.viewportWidth) === next.viewportWidth && previous.layoutWidth === next.layoutWidth && previous.isChatPanelOverlay === next.isChatPanelOverlay && previous.isSideNavOverlay === next.isSideNavOverlay) {
84
+ return;
85
+ }
86
+ if (previous) {
87
+ setHasInteraction(true);
88
+ }
89
+ previous = next;
90
+ setDimensions(next);
91
+ setResizeSession(null);
92
+ };
93
+ updateDimensions();
94
+ const observer = new ResizeObserver(entries => {
95
+ const entry = entries.find(entry => entry.target === root);
96
+ if (entry) {
97
+ updateDimensions(entry.contentRect.width);
98
+ }
99
+ });
100
+ if (root) {
101
+ observer.observe(root);
102
+ }
103
+ const unbindResize = bind(window, {
104
+ type: 'resize',
105
+ listener: () => updateDimensions()
106
+ });
107
+ const unbindQuery = bind(inlineChatQuery, {
108
+ type: 'change',
109
+ listener: () => updateDimensions()
110
+ });
111
+ const unbindSideNavQuery = bind(inlineSideNavQuery, {
112
+ type: 'change',
113
+ listener: () => updateDimensions()
114
+ });
115
+ return () => {
116
+ observer.disconnect();
117
+ unbindResize();
118
+ unbindQuery();
119
+ unbindSideNavQuery();
120
+ };
121
+ }, [layoutRef]);
122
+ const registerArea = useCallback((area, config) => {
123
+ const previous = configsRef.current[area];
124
+ if (adoptedConstraints.current && (!previous || previous.isOpen !== config.isOpen || previous.requestedWidth !== config.requestedWidth || previous.minWidth !== config.minWidth || previous.maxWidth !== config.maxWidth || previous.defaultWidth !== config.defaultWidth || previous.compactDefaultWidth !== config.compactDefaultWidth)) {
125
+ setHasInteraction(true);
126
+ }
127
+ configsRef.current[area] = config;
128
+ setResizeSession(current => {
129
+ // A changed layout/configuration needs a fresh allocation. A controlled
130
+ // acknowledgement for the resized area keeps its siblings fixed.
131
+ return previous && (previous.isOpen === config.isOpen || area === 'side-nav' && !config.isOpen && (current === null || current === void 0 ? void 0 : current.area) !== 'side-nav') && previous.minWidth === config.minWidth && previous.maxWidth === config.maxWidth && previous.defaultWidth === config.defaultWidth && previous.compactDefaultWidth === config.compactDefaultWidth && (previous.requestedWidth === config.requestedWidth || (current === null || current === void 0 ? void 0 : current.area) === area && current.completedWidth === config.requestedWidth || area === 'side-nav' && !config.isOpen && (current === null || current === void 0 ? void 0 : current.area) !== 'side-nav') ? current : null;
132
+ });
133
+ setAreas(current => {
134
+ const existing = current[area];
135
+ return {
136
+ ...current,
137
+ [area]: {
138
+ ...existing,
139
+ ...config,
140
+ liveResize: config.isOpen ? existing === null || existing === void 0 ? void 0 : existing.liveResize : undefined
141
+ }
142
+ };
143
+ });
144
+ }, []);
145
+ const unregisterArea = useCallback(area => {
146
+ if (adoptedConstraints.current) {
147
+ setHasInteraction(true);
148
+ }
149
+ delete configsRef.current[area];
150
+ setResizeSession(null);
151
+ setAreas(current => {
152
+ if (!current[area]) {
153
+ return current;
154
+ }
155
+ // Closing uses isOpen: false and retains preferences. Unregistering means
156
+ // the owning component unmounted; a replacement must start with its own defaults.
157
+ const next = {
158
+ ...current
159
+ };
160
+ delete next[area];
161
+ return next;
162
+ });
163
+ }, []);
164
+ const captureSiblingWidths = useCallback((area, mode = 'inline') => {
165
+ const baselineWidths = {};
166
+ const overlayModes = {};
167
+ for (const areaName of ['side-nav', 'panel', 'chat-panel']) {
168
+ const state = areaStatesRef.current[areaName];
169
+ if (state) {
170
+ overlayModes[areaName] = state.mode;
171
+ }
172
+ if ((state === null || state === void 0 ? void 0 : state.mode) === 'inline') {
173
+ baselineWidths[areaName] = state.width;
174
+ }
175
+ }
176
+ return {
177
+ area,
178
+ baselineWidths,
179
+ overlayModes: mode === 'overlay' ? overlayModes : undefined
180
+ };
181
+ }, []);
182
+ const startResize = useCallback((area, mode) => {
183
+ setHasInteraction(true);
184
+ setResizeSession(captureSiblingWidths(area, mode));
185
+
186
+ // Start from the effective width, which may already be compressed below the preferred width.
187
+ // Reapplying the preferred width here would move the splitter away from the pointer.
188
+ const renderedState = areaStatesRef.current[area];
189
+ if ((renderedState === null || renderedState === void 0 ? void 0 : renderedState.mode) === mode) {
190
+ setAreas(current => {
191
+ const existing = current[area];
192
+ return existing ? {
193
+ ...current,
194
+ [area]: {
195
+ ...existing,
196
+ liveResize: {
197
+ mode,
198
+ width: renderedState.width
199
+ }
200
+ }
201
+ } : current;
202
+ });
203
+ }
204
+ }, [captureSiblingWidths]);
205
+ const resize = useCallback((area, mode, width) => {
206
+ setAreas(current => {
207
+ var _existing$liveResize;
208
+ const existing = current[area];
209
+ if ((existing === null || existing === void 0 ? void 0 : (_existing$liveResize = existing.liveResize) === null || _existing$liveResize === void 0 ? void 0 : _existing$liveResize.mode) === mode && existing.liveResize.width === width) {
210
+ return current;
211
+ }
212
+ return existing ? {
213
+ ...current,
214
+ [area]: {
215
+ ...existing,
216
+ liveResize: {
217
+ mode,
218
+ width
219
+ }
220
+ }
221
+ } : current;
222
+ });
223
+ }, []);
224
+ const completeResize = useCallback((area, mode, width) => {
225
+ setHasInteraction(true);
226
+ if (mode === 'overlay') {
227
+ const session = inputsRef.current.resizeSession;
228
+ setResizeSession({
229
+ ...((session === null || session === void 0 ? void 0 : session.area) === area && session.overlayModes ? session : captureSiblingWidths(area, mode)),
230
+ completedWidth: width
231
+ });
232
+ }
233
+ if (mode === 'inline') {
234
+ var _inputs$resizeSession, _inputs$areas$area;
235
+ const inputs = inputsRef.current;
236
+ const session = ((_inputs$resizeSession = inputs.resizeSession) === null || _inputs$resizeSession === void 0 ? void 0 : _inputs$resizeSession.area) === area && (_inputs$areas$area = inputs.areas[area]) !== null && _inputs$areas$area !== void 0 && _inputs$areas$area.liveResize ? inputs.resizeSession : captureSiblingWidths(area);
237
+ const existing = inputs.areas[area];
238
+ if (inputs.dimensions && existing) {
239
+ // Keyboard completion has no preview; calculate it using the accepted
240
+ // request, not the parent's previous controlled width. Pointer completion
241
+ // keeps below-minimum cascade results even though its payload is clamped.
242
+ const completed = existing.liveResize ? areaStatesRef.current : calculateLayoutAreaStates({
243
+ ...inputs.dimensions,
244
+ occupiedWidth: inputs.occupiedWidth,
245
+ mainMinWidth: inputs.mainMinWidth,
246
+ areas: {
247
+ ...inputs.areas,
248
+ [area]: {
249
+ ...existing,
250
+ liveResize: {
251
+ mode,
252
+ width
253
+ }
254
+ }
255
+ },
256
+ resizeSession: session
257
+ });
258
+ const completedWidths = {};
259
+ for (const name of ['side-nav', 'panel', 'chat-panel']) {
260
+ var _completed$name;
261
+ if (((_completed$name = completed[name]) === null || _completed$name === void 0 ? void 0 : _completed$name.mode) === 'inline') {
262
+ completedWidths[name] = completed[name].width;
263
+ }
264
+ }
265
+ setResizeSession({
266
+ ...session,
267
+ completedWidth: width,
268
+ completedWidths
269
+ });
270
+ }
271
+ }
272
+ setAreas(current => {
273
+ const existing = current[area];
274
+ if (!existing) {
275
+ return current;
276
+ }
277
+ if (mode === 'overlay') {
278
+ return {
279
+ ...current,
280
+ [area]: {
281
+ ...existing,
282
+ liveResize: undefined,
283
+ preferredOverlayWidth: width
284
+ }
285
+ };
286
+ }
287
+ return {
288
+ ...current,
289
+ [area]: {
290
+ ...existing,
291
+ liveResize: undefined,
292
+ preferredInlineWidth: width
293
+ }
294
+ };
295
+ });
296
+ }, [captureSiblingWidths]);
297
+ useEffect(() => {
298
+ // Loading the interaction engine is not a layout event. Adopt the CSS layout
299
+ // without changing widths or modes; subsequent changes to configuration or
300
+ // available space may redistribute it. Content stays mounted during the handoff.
301
+ const root = layoutRef === null || layoutRef === void 0 ? void 0 : layoutRef.current;
302
+ if (onUpdate && root && dimensions && isPreservingInitialLayout) {
303
+ const measured = {};
304
+ const selectors = {
305
+ 'side-nav': ':scope > nav[data-layout-slot]',
306
+ panel: '[data-layout-with-panel-slot]',
307
+ 'chat-panel': ':scope > [data-layout-chat-panel]'
308
+ };
309
+ for (const name of ['side-nav', 'panel', 'chat-panel']) {
310
+ const element = root.querySelector(selectors[name]);
311
+ const config = areas[name];
312
+ if (!element || !(config !== null && config !== void 0 && config.isOpen)) {
313
+ continue;
314
+ }
315
+ const style = getComputedStyle(element);
316
+ const mode = style.gridArea.split('/')[0].trim() === name ? 'inline' : 'overlay';
317
+ measured[name] = {
318
+ mode,
319
+ width: element.getBoundingClientRect().width,
320
+ minWidth: resolveLayoutWidth(config.minWidth, dimensions.viewportWidth)
321
+ };
322
+ }
323
+ setInitialStates(measured);
324
+ }
325
+ }, [areas, dimensions, isPreservingInitialLayout, layoutRef, onUpdate]);
326
+ const areaStates = useMemo(() => {
327
+ var _initialStates$sideN, _initialStates$panel, _initialStates$chatP;
328
+ if (!dimensions) {
329
+ return {};
330
+ }
331
+ const allocated = calculateLayoutAreaStates({
332
+ ...dimensions,
333
+ occupiedWidth,
334
+ mainMinWidth,
335
+ areas,
336
+ resizeSession,
337
+ initialModes: onUpdate && layoutRef !== null && layoutRef !== void 0 && layoutRef.current && isPreservingInitialLayout ? {
338
+ 'side-nav': (_initialStates$sideN = initialStates['side-nav']) === null || _initialStates$sideN === void 0 ? void 0 : _initialStates$sideN.mode,
339
+ panel: (_initialStates$panel = initialStates.panel) === null || _initialStates$panel === void 0 ? void 0 : _initialStates$panel.mode,
340
+ 'chat-panel': (_initialStates$chatP = initialStates['chat-panel']) === null || _initialStates$chatP === void 0 ? void 0 : _initialStates$chatP.mode
341
+ } : undefined
342
+ });
343
+ if (onUpdate && layoutRef !== null && layoutRef !== void 0 && layoutRef.current && isPreservingInitialLayout) {
344
+ for (const name of ['side-nav', 'panel', 'chat-panel']) {
345
+ if (initialStates[name] && allocated[name]) {
346
+ allocated[name] = {
347
+ ...allocated[name],
348
+ ...initialStates[name]
349
+ };
350
+ } else {
351
+ delete allocated[name];
352
+ }
353
+ }
354
+ }
355
+ return allocated;
356
+ }, [areas, mainMinWidth, resizeSession, dimensions, occupiedWidth, isPreservingInitialLayout, initialStates, layoutRef, onUpdate]);
357
+ useEffect(() => {
358
+ var _areas$sideNav;
359
+ if (hasInteraction && (resizeSession === null || resizeSession === void 0 ? void 0 : resizeSession.area) !== 'side-nav' && (_areas$sideNav = areas['side-nav']) !== null && _areas$sideNav !== void 0 && _areas$sideNav.isOpen && !areaStates['side-nav'] && dimensions) {
360
+ var _areas$sideNav$onReq, _areas$sideNav2;
361
+ (_areas$sideNav$onReq = (_areas$sideNav2 = areas['side-nav']).onRequestClose) === null || _areas$sideNav$onReq === void 0 ? void 0 : _areas$sideNav$onReq.call(_areas$sideNav2);
362
+ }
363
+ }, [areaStates, areas, dimensions, hasInteraction, resizeSession]);
364
+ useEffect(() => {
365
+ areaStatesRef.current = areaStates;
366
+ }, [areaStates]);
367
+
368
+ // Read bounds lazily: changing a sibling's capacity must not publish new render
369
+ // state or recreate drag listeners for a region whose own width did not change.
370
+ const getResizeBounds = useCallback(area => {
371
+ var _areaStatesRef$curren;
372
+ const bounds = (_areaStatesRef$curren = areaStatesRef.current[area]) === null || _areaStatesRef$curren === void 0 ? void 0 : _areaStatesRef$curren.resizeBounds;
373
+ return bounds ? {
374
+ min: `${bounds.min}px`,
375
+ max: `${bounds.max}px`
376
+ } : undefined;
377
+ }, []);
378
+ const value = useMemo(() => ({
379
+ getResizeBounds,
380
+ registerArea,
381
+ unregisterArea,
382
+ startResize,
383
+ resize,
384
+ completeResize,
385
+ setMainMinWidth
386
+ }), [completeResize, getResizeBounds, registerArea, resize, startResize, unregisterArea]);
387
+ const sideNavState = useStableAreaState(areaStates['side-nav']);
388
+ const panelState = useStableAreaState(areaStates.panel);
389
+ const chatPanelState = useStableAreaState(areaStates['chat-panel']);
390
+ const SideNavStateProvider = layoutAreaStateContexts['side-nav'].Provider;
391
+ const PanelStateProvider = layoutAreaStateContexts.panel.Provider;
392
+ const ChatPanelStateProvider = layoutAreaStateContexts['chat-panel'].Provider;
393
+ useEffect(() => {
394
+ onReady === null || onReady === void 0 ? void 0 : onReady(value);
395
+ return () => onReady === null || onReady === void 0 ? void 0 : onReady(null);
396
+ }, [onReady, value]);
397
+ useEffect(() => {
398
+ if (!adoptedConstraints.current && (sideNavState || panelState || chatPanelState)) {
399
+ adoptedConstraints.current = {
400
+ occupiedWidth,
401
+ mainMinWidth
402
+ };
403
+ }
404
+ onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate({
405
+ 'side-nav': sideNavState,
406
+ panel: panelState,
407
+ 'chat-panel': chatPanelState
408
+ });
409
+ }, [onUpdate, sideNavState, panelState, chatPanelState, occupiedWidth, mainMinWidth]);
410
+ return /*#__PURE__*/React.createElement(LayoutAreaSizingContext.Provider, {
411
+ value: value
412
+ }, /*#__PURE__*/React.createElement(SideNavStateProvider, {
413
+ value: sideNavState
414
+ }, /*#__PURE__*/React.createElement(PanelStateProvider, {
415
+ value: panelState
416
+ }, /*#__PURE__*/React.createElement(ChatPanelStateProvider, {
417
+ value: chatPanelState
418
+ }, children))));
419
+ }
@@ -6,7 +6,7 @@ import { Fragment } from 'react';
6
6
  import { fg } from '@atlaskit/platform-feature-flags/fg';
7
7
  import { useSkipLink } from '../../../context/skip-links/use-skip-link';
8
8
  import { mainMinimumWidthVar } from '../constants';
9
- import { gridRootId } from '../root';
9
+ import { gridRootFitId, gridRootId } from '../root';
10
10
  import { useLayoutId } from '../use-layout-id';
11
11
  import { useLayoutMainSizing } from '../use-layout-main-sizing';
12
12
  const contentInsetBlockStartStatic = `calc(var(--n_bnrM, 0px) + var(--n_tNvM, 0px))`;
@@ -34,7 +34,7 @@ export function Main({
34
34
  return /*#__PURE__*/React.createElement(Fragment, null, fg('platform-dst-chat-panel-layout') &&
35
35
  /*#__PURE__*/
36
36
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-global-styles
37
- React.createElement("style", null, `#${gridRootId} { ${mainMinimumWidthVar}: ${minWidth}px; }`), /*#__PURE__*/React.createElement("div", {
37
+ React.createElement("style", null, `#${gridRootId}, #${gridRootFitId} { ${mainMinimumWidthVar}: ${minWidth}px; }`), /*#__PURE__*/React.createElement("div", {
38
38
  id: id,
39
39
  "data-layout-slot": true,
40
40
  className: ax([mainElementStyles.root, xcss]),
@@ -134,6 +134,7 @@ const PortaledPanelSplitter = ({
134
134
  onCompleteResize,
135
135
  onResizeStartInternal,
136
136
  onResizeInternal,
137
+ prepareResize,
137
138
  getResizeBounds,
138
139
  panel,
139
140
  portal,
@@ -282,8 +283,9 @@ const PortaledPanelSplitter = ({
282
283
  });
283
284
  const resizingWidth = `clamp(${resizeBounds.min}, ${targetWidth}px, ${resizeBounds.max})`;
284
285
  if (fg('platform-dst-chat-panel-layout') && onResizeInternal) {
285
- // Let the sizing provider constrain the width against Main's available space.
286
- const width = Math.max(convertResizeBoundToPixels(resizeBounds.min), Math.min(targetWidth, convertResizeBoundToPixels(resizeBounds.max)));
286
+ // The allocator needs below-minimum requests for the local-to-chat cascade.
287
+ const currentBounds = getResizeBounds();
288
+ const width = Math.max(0, Math.min(targetWidth, prepareResize ? window.innerWidth : convertResizeBoundToPixels(currentBounds.max)));
287
289
  if (width !== lastManagedResizeWidth) {
288
290
  lastManagedResizeWidth = width;
289
291
  onResizeInternal(width);
@@ -295,12 +297,17 @@ const PortaledPanelSplitter = ({
295
297
  }
296
298
  source.data.resizingWidth = resizingWidth;
297
299
  },
298
- onDrop({
300
+ async onDrop({
299
301
  source
300
302
  }) {
301
303
  var _resizingElementRef$c2;
302
304
  invariant(isPanelSplitterDragData(source.data));
303
305
  preventUnhandled.stop();
306
+ if (prepareResize) {
307
+ if (!(await prepareResize()) || !panel.isConnected) {
308
+ return;
309
+ }
310
+ }
304
311
 
305
312
  // PDD may deliver its final throttled drag update immediately before drop.
306
313
  // Commit that update once before measuring; do not persist a previous frame.
@@ -325,7 +332,7 @@ const PortaledPanelSplitter = ({
325
332
  resizingElementRef === null || resizingElementRef === void 0 ? void 0 : (_resizingElementRef$c2 = resizingElementRef.current) === null || _resizingElementRef$c2 === void 0 ? void 0 : _resizingElementRef$c2.style.removeProperty(resizingCssVar);
326
333
  }
327
334
  }));
328
- }, [onCompleteResize, onResizeStartInternal, onResizeInternal, onResizeStart, onResizeEnd, panel, resizingCssVar, resizingElementRef, legacyPanelWidth, position, openLayerObserver, panelId, getResizeBounds]);
335
+ }, [onCompleteResize, onResizeStartInternal, onResizeInternal, prepareResize, onResizeStart, onResizeEnd, panel, resizingCssVar, resizingElementRef, legacyPanelWidth, position, openLayerObserver, panelId, getResizeBounds]);
329
336
  const onResizeStartStableRef = useStableRef(onResizeStart);
330
337
  const onResizeEndStableRef = useStableRef(onResizeEnd);
331
338
  const [keyboardResizeManager] = useState(() => createKeyboardResizeManager({
@@ -340,21 +347,41 @@ const PortaledPanelSplitter = ({
340
347
  return (_onResizeEndStableRef = onResizeEndStableRef.current) === null || _onResizeEndStableRef === void 0 ? void 0 : _onResizeEndStableRef.call(onResizeEndStableRef, ...args);
341
348
  }
342
349
  }));
343
- const handleSliderInputChange = useCallback(event => {
350
+ const focusSequence = useRef(0);
351
+ const keyboardBoundsReady = useRef(false);
352
+ const pendingKeyboardWidth = useRef(null);
353
+ const handleSliderInputChange = useCallback(async (event, pendingKey) => {
344
354
  const isManagedResize = fg('platform-dst-chat-panel-layout') && onResizeInternal;
345
- let value = isManagedResize ? parseFloat(event.target.value) : parseInt(event.target.value);
355
+ const sequence = focusSequence.current;
356
+ let value = isManagedResize ? parseFloat(event.currentTarget.value) : parseInt(event.currentTarget.value);
357
+ if (prepareResize) {
358
+ if (!(await prepareResize()) || !panel.isConnected || sequence !== focusSequence.current) {
359
+ return;
360
+ }
361
+ }
346
362
  if (isManagedResize) {
347
363
  // The available region may have changed since focus, including without a
348
364
  // window resize. Use the allocator's current bounds for the public payload.
349
365
  const bounds = getResizeBounds();
350
366
  const min = convertResizeBoundToPixels(bounds.min);
351
367
  const max = convertResizeBoundToPixels(bounds.max);
368
+ if (pendingKey) {
369
+ var _pendingKeyboardWidth;
370
+ // Native range inputs would apply these keys to the placeholder bounds.
371
+ // Preserve the intent until the chunk provides the real bounds instead.
372
+ const current = (_pendingKeyboardWidth = pendingKeyboardWidth.current) !== null && _pendingKeyboardWidth !== void 0 ? _pendingKeyboardWidth : getPixelWidth(panel);
373
+ const isRtl = getTextDirection(panel) === 'rtl';
374
+ const increase = pendingKey === 'ArrowUp' || pendingKey === (isRtl ? 'ArrowLeft' : 'ArrowRight') || pendingKey === 'PageUp';
375
+ const step = pendingKey.startsWith('Page') ? Math.max(20, (max - min) / 10) : 20;
376
+ value = pendingKey === 'End' ? max : pendingKey === 'Home' ? min : current + (increase ? step : -step);
377
+ }
352
378
  value = Math.max(min, Math.min(value, max));
353
379
  setRangeInputBounds({
354
380
  min,
355
381
  max
356
382
  });
357
383
  }
384
+ pendingKeyboardWidth.current = value;
358
385
  setRangeInputValue(value);
359
386
 
360
387
  /**
@@ -366,10 +393,24 @@ const PortaledPanelSplitter = ({
366
393
  initialWidth: panelWidth,
367
394
  finalWidth: value
368
395
  });
369
- }, [getResizeBounds, onCompleteResize, onResizeInternal, panelWidth, keyboardResizeManager]);
396
+ }, [getResizeBounds, onCompleteResize, onResizeInternal, panelWidth, keyboardResizeManager, panel, prepareResize]);
397
+ const handleSliderKeyDown = useCallback(event => {
398
+ if (prepareResize && !keyboardBoundsReady.current && ['Home', 'End', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'PageUp', 'PageDown'].includes(event.key)) {
399
+ event.preventDefault();
400
+ void handleSliderInputChange(event, event.key);
401
+ }
402
+ }, [handleSliderInputChange, prepareResize]);
370
403
  const resizeEventListenerCleanupFn = useRef(null);
371
- const handleSliderFocus = useCallback(() => {
404
+ const handleSliderFocus = useCallback(async () => {
372
405
  var _resizeEventListenerC;
406
+ const sequence = ++focusSequence.current;
407
+ keyboardBoundsReady.current = false;
408
+ pendingKeyboardWidth.current = null;
409
+ if (prepareResize) {
410
+ if (!(await prepareResize()) || !panel.isConnected || sequence !== focusSequence.current) {
411
+ return;
412
+ }
413
+ }
373
414
  /**
374
415
  * We are only updating the range input's properties when the element is focused, as a performance optimisation.
375
416
  * It isn't necessary to recalculate these values until the element is going to be used.
@@ -379,7 +420,9 @@ const PortaledPanelSplitter = ({
379
420
  (_resizeEventListenerC = resizeEventListenerCleanupFn.current) === null || _resizeEventListenerC === void 0 ? void 0 : _resizeEventListenerC.call(resizeEventListenerCleanupFn);
380
421
 
381
422
  // Update the range input value to match the current panel width.
382
- setRangeInputValue(panelWidth);
423
+ const width = prepareResize ? getPixelWidth(panel) : panelWidth;
424
+ pendingKeyboardWidth.current = width;
425
+ setRangeInputValue(width);
383
426
  const resizeBounds = getResizeBounds();
384
427
 
385
428
  // Update the range input bounds to ensure they are accurate (in case the viewport was previously resized)
@@ -387,6 +430,7 @@ const PortaledPanelSplitter = ({
387
430
  min: convertResizeBoundToPixels(resizeBounds.min),
388
431
  max: convertResizeBoundToPixels(resizeBounds.max)
389
432
  });
433
+ keyboardBoundsReady.current = true;
390
434
 
391
435
  /**
392
436
  * _While the slider element is focused_, add a window resize event listener to update the range input bounds
@@ -405,9 +449,11 @@ const PortaledPanelSplitter = ({
405
449
  });
406
450
  }
407
451
  });
408
- }, [panelWidth, getResizeBounds]);
452
+ }, [panelWidth, getResizeBounds, panel, prepareResize]);
409
453
  const handleSliderBlur = useCallback(() => {
410
454
  var _resizeEventListenerC2;
455
+ focusSequence.current++;
456
+ keyboardBoundsReady.current = false;
411
457
  // Remove the resize event listener when the range input is blurred.
412
458
  (_resizeEventListenerC2 = resizeEventListenerCleanupFn.current) === null || _resizeEventListenerC2 === void 0 ? void 0 : _resizeEventListenerC2.call(resizeEventListenerCleanupFn);
413
459
  }, []);
@@ -432,6 +478,7 @@ const PortaledPanelSplitter = ({
432
478
  }, /*#__PURE__*/React.createElement("div", {
433
479
  ref: splitterRef,
434
480
  "data-testid": testId,
481
+ "data-panel-splitter": prepareResize ? '' : undefined,
435
482
  onDoubleClick: onDoubleClick,
436
483
  className: ax([grabAreaStyles.root, fg('platform-dst-tokens-finesse') && grabAreaStyles.rootFinesse])
437
484
  }, /*#__PURE__*/React.createElement(VisuallyHidden, null, /*#__PURE__*/React.createElement("input", {
@@ -443,6 +490,7 @@ const PortaledPanelSplitter = ({
443
490
  "aria-valuetext": ariaValueText,
444
491
  "aria-labelledby": labelId,
445
492
  onChange: handleSliderInputChange,
493
+ onKeyDown: prepareResize ? handleSliderKeyDown : undefined,
446
494
  onFocus: handleSliderFocus,
447
495
  onBlur: handleSliderBlur
448
496
  }), /*#__PURE__*/React.createElement("span", {
@@ -488,6 +536,7 @@ export const PanelSplitter = ({
488
536
  onResizeStartInternal,
489
537
  onResizeInternal,
490
538
  getResizeBounds,
539
+ prepareResize,
491
540
  resizingCssVar,
492
541
  resizingElementRef,
493
542
  position,
@@ -587,6 +636,7 @@ export const PanelSplitter = ({
587
636
  onCompleteResize: onCompleteResize,
588
637
  onResizeStartInternal: onResizeStartInternal,
589
638
  onResizeInternal: onResizeInternal,
639
+ prepareResize: prepareResize,
590
640
  getResizeBounds: getResizeBounds,
591
641
  resizingCssVar: resizingCssVar,
592
642
  resizingElementRef: resizingElementRef,
@@ -11,6 +11,7 @@ export const PanelSplitterProvider = ({
11
11
  onCompleteResize,
12
12
  onResizeStartInternal,
13
13
  onResizeInternal,
14
+ prepareResize,
14
15
  getResizeBounds,
15
16
  resizingCssVar,
16
17
  panelRef,
@@ -28,6 +29,7 @@ export const PanelSplitterProvider = ({
28
29
  onCompleteResize,
29
30
  onResizeStartInternal,
30
31
  onResizeInternal,
32
+ prepareResize,
31
33
  getResizeBounds,
32
34
  resizingCssVar,
33
35
  position,
@@ -36,7 +38,7 @@ export const PanelSplitterProvider = ({
36
38
  isEnabled,
37
39
  portalRef: typeof providedPortalRef !== 'undefined' ? providedPortalRef : portalRef,
38
40
  shortcut
39
- }), [panelId, panelWidth, onCompleteResize, onResizeStartInternal, onResizeInternal, getResizeBounds, resizingCssVar, position, panelRef, resizingElementRef, isEnabled, providedPortalRef, shortcut]);
41
+ }), [panelId, panelWidth, onCompleteResize, onResizeStartInternal, onResizeInternal, prepareResize, getResizeBounds, resizingCssVar, position, panelRef, resizingElementRef, isEnabled, providedPortalRef, shortcut]);
40
42
  return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(PanelSplitterContext.Provider, {
41
43
  value: context
42
44
  }, children), typeof providedPortalRef !== 'undefined' ? null : /*#__PURE__*/React.createElement("div", {
@@ -1,13 +1,24 @@
1
1
  ._18m9zryt{overflow-y:clip}
2
+ ._1bsb1osq{width:100%}
3
+ ._1bsb1raa{width:max(0px,calc(var(--n_layoutW) - var(--n_mainMinW) - var(--n_fitAside, 0px) - var(--n_fitRibbon, 0px) - var(--n_fitPanel, 0px)))}
4
+ ._1bsb4nr3{width:var(--n_layoutW)}
5
+ ._1bsbjwgu{width:max(0px,calc(100% - var(--n_fitNav)))}
2
6
  ._1ciragmp >:not([data-layout-slot]){display:none!important}
3
7
  ._1cwmtye8{--n_mainMinW:20rem}
4
8
  ._1e0c11p5{display:grid}
9
+ ._1f381mry{container-name:navigation-chat-space}
10
+ ._1f38hwoh{container-name:navigation-main-space}
11
+ ._1f38hx13{container-name:navigation-layout-width}
5
12
  ._1lmcq9em{grid-template-areas:"banner" "top-bar" "main" "aside"}
6
13
  ._1reozryt{overflow-x:clip}
7
14
  ._1tke1kxc{min-height:100vh}
8
15
  ._1tvnagmp >:not([data-layout-slot]):not(dialog):not([popover]){display:none!important}
9
16
  ._2z0516ab{grid-template-rows:auto auto 1fr auto}
17
+ ._5to3grho{--n_fitNav:0px}
18
+ ._7cca15wb{container-type:inline-size}
19
+ ._82d6okmw{--n_layoutW:100cqw}
20
+ ._vchh1ntv{box-sizing:content-box}
10
21
  ._yv0ei47z{grid-template-columns:minmax(0,1fr)}
11
- @media (min-width:40rem){._12ag50j2{grid-template-areas:"banner banner" "top-bar chat-panel" "main chat-panel" "aside chat-panel"}._71dlqwpl{grid-template-columns:minmax(var(--n_mainMinW),1fr) minmax(0,max-content)}}
12
- @media (min-width:64rem){._12fk1aio{grid-template-areas:"banner banner banner banner" "ribbon top-bar top-bar top-bar" "ribbon side-nav main aside"}._12qzrxre{grid-template-rows:auto auto 3fr}._1rqteala{grid-template-columns:auto auto minmax(0,1fr) auto}._12fkqmyk{grid-template-areas:"banner banner banner banner banner" "ribbon top-bar top-bar top-bar chat-panel" "ribbon side-nav main aside chat-panel"}._1rqt8mqp{grid-template-columns:auto auto minmax(var(--n_mainMinW),1fr) auto minmax(0,max-content)}}
13
- @media (min-width:90rem){._xkmggc7l{grid-template-areas:"banner banner banner banner banner banner" "ribbon top-bar top-bar top-bar top-bar chat-panel" "ribbon side-nav main aside panel chat-panel"}._tyve1i18{grid-template-columns:auto auto minmax(var(--n_mainMinW),1fr) auto auto minmax(0,max-content)}._xkmgks3h{grid-template-areas:"banner banner banner banner banner" "ribbon top-bar top-bar top-bar top-bar" "ribbon side-nav main aside panel"}._jbc7rxre{grid-template-rows:auto auto 3fr}._tyve1nco{grid-template-columns:auto auto minmax(0,1fr) auto auto}}
22
+ @media (min-width:40rem){._12ag50j2{grid-template-areas:"banner banner" "top-bar chat-panel" "main chat-panel" "aside chat-panel"}._71dl19sb{grid-template-columns:minmax(var(--n_mainMinW),1fr) var(--n_chatTrack,0)}}
23
+ @media (min-width:64rem){._12fk1aio{grid-template-areas:"banner banner banner banner" "ribbon top-bar top-bar top-bar" "ribbon side-nav main aside"}._12qzrxre{grid-template-rows:auto auto 3fr}._1rqteala{grid-template-columns:auto auto minmax(0,1fr) auto}._12fkqmyk{grid-template-areas:"banner banner banner banner banner" "ribbon top-bar top-bar top-bar chat-panel" "ribbon side-nav main aside chat-panel"}._1rqttswr{grid-template-columns:auto auto minmax(var(--n_mainMinW),1fr) auto var(--n_chatTrack,0)}}
24
+ @media (min-width:90rem){._xkmggc7l{grid-template-areas:"banner banner banner banner banner banner" "ribbon top-bar top-bar top-bar top-bar chat-panel" "ribbon side-nav main aside panel chat-panel"}._tyve87m4{grid-template-columns:auto auto minmax(var(--n_mainMinW),1fr) auto auto var(--n_chatTrack,0)}._xkmgks3h{grid-template-areas:"banner banner banner banner banner" "ribbon top-bar top-bar top-bar top-bar" "ribbon side-nav main aside panel"}._jbc7rxre{grid-template-rows:auto auto 3fr}._tyve1nco{grid-template-columns:auto auto minmax(0,1fr) auto auto}}