braid-design-system 33.2.0 → 33.2.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # braid-design-system
2
2
 
3
+ ## 33.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - **MenuRenderer**: Ensure menu is visible, even when its trigger element is inside a container with overflow hidden. ([#1658](https://github.com/seek-oss/braid-design-system/pull/1658))
8
+
9
+ - **MenuRenderer, OverflowMenu:** Provide improved scroll affordance ([#1661](https://github.com/seek-oss/braid-design-system/pull/1661))
10
+
11
+ Introduce scroll affordance to menus, providing a visual cue that there are more items overflowing vertically.
12
+
13
+ - **OverflowMenu**: Simplify internal layout. ([#1658](https://github.com/seek-oss/braid-design-system/pull/1658))
14
+
15
+ Refactor the internal layout of `OverflowMenu` to improve the alignment of the menu with the button.
16
+
3
17
  ## 33.2.0
4
18
 
5
19
  ### Minor Changes
@@ -15,6 +15,7 @@ const actionTypes = {
15
15
  MENU_TRIGGER_CLICK: 12,
16
16
  MENU_TRIGGER_TAB: 13,
17
17
  MENU_TRIGGER_ESCAPE: 14,
18
- BACKDROP_CLICK: 15
18
+ BACKDROP_CLICK: 15,
19
+ WINDOW_RESIZE: 16
19
20
  };
20
21
  exports.actionTypes = actionTypes;
@@ -14,7 +14,8 @@ const actionTypes = {
14
14
  MENU_TRIGGER_CLICK: 12,
15
15
  MENU_TRIGGER_TAB: 13,
16
16
  MENU_TRIGGER_ESCAPE: 14,
17
- BACKDROP_CLICK: 15
17
+ BACKDROP_CLICK: 15,
18
+ WINDOW_RESIZE: 16
18
19
  };
19
20
  export {
20
21
  actionTypes
@@ -8,11 +8,14 @@ const lib_components_MenuItemDivider_MenuItemDivider_cjs = require("../MenuItemD
8
8
  const lib_components_private_normalizeKey_cjs = require("../private/normalizeKey.cjs");
9
9
  const lib_components_private_getNextIndex_cjs = require("../private/getNextIndex.cjs");
10
10
  const lib_components_private_Overlay_Overlay_cjs = require("../private/Overlay/Overlay.cjs");
11
+ const lib_components_private_ScrollContainer_ScrollContainer_cjs = require("../private/ScrollContainer/ScrollContainer.cjs");
11
12
  const lib_components_MenuRenderer_MenuRenderer_actions_cjs = require("./MenuRenderer.actions.cjs");
12
13
  const lib_components_MenuRenderer_MenuRendererContext_cjs = require("./MenuRendererContext.cjs");
13
14
  const lib_components_MenuRenderer_MenuRendererItemContext_cjs = require("./MenuRendererItemContext.cjs");
14
15
  const lib_components_private_buildDataAttributes_cjs = require("../private/buildDataAttributes.cjs");
15
16
  const lib_components_MenuRenderer_MenuRenderer_css_cjs = require("./MenuRenderer.css.cjs");
17
+ const lib_components_BraidPortal_BraidPortal_cjs = require("../BraidPortal/BraidPortal.cjs");
18
+ const dynamic = require("@vanilla-extract/dynamic");
16
19
  const _interopDefaultCompat = (e) => e && typeof e === "object" && "default" in e ? e : { default: e };
17
20
  const assert__default = /* @__PURE__ */ _interopDefaultCompat(assert);
18
21
  const {
@@ -31,14 +34,29 @@ const {
31
34
  MENU_TRIGGER_CLICK,
32
35
  MENU_TRIGGER_TAB,
33
36
  MENU_TRIGGER_ESCAPE,
34
- BACKDROP_CLICK
37
+ BACKDROP_CLICK,
38
+ WINDOW_RESIZE
35
39
  } = lib_components_MenuRenderer_MenuRenderer_actions_cjs.actionTypes;
40
+ const getPosition = (element) => {
41
+ if (!element) {
42
+ return void 0;
43
+ }
44
+ const { top, bottom, left, right } = element.getBoundingClientRect();
45
+ const { scrollX, scrollY, innerWidth, innerHeight } = window;
46
+ return {
47
+ top: innerHeight - top - scrollY,
48
+ bottom: bottom + scrollY,
49
+ left: left + scrollX,
50
+ right: innerWidth - right - scrollX
51
+ };
52
+ };
36
53
  const CLOSED_INDEX = -1;
37
54
  const CLOSE_REASON_EXIT = { reason: "exit" };
38
55
  const initialState = {
39
56
  open: false,
40
57
  highlightIndex: CLOSED_INDEX,
41
- closeReason: CLOSE_REASON_EXIT
58
+ closeReason: CLOSE_REASON_EXIT,
59
+ triggerPosition: void 0
42
60
  };
43
61
  const MenuRenderer = ({
44
62
  onOpen,
@@ -53,6 +71,7 @@ const MenuRenderer = ({
53
71
  data,
54
72
  ...restProps
55
73
  }) => {
74
+ const menuContainerRef = React.useRef(null);
56
75
  const buttonRef = React.useRef(null);
57
76
  const lastOpen = React.useRef(false);
58
77
  const items = lib_utils_flattenChildren_cjs.flattenChildren(children);
@@ -64,83 +83,90 @@ const MenuRenderer = ({
64
83
  ),
65
84
  "All child nodes within a menu component must be a MenuItem, MenuItemLink, MenuItemCheckbox or MenuItemDivider: https://seek-oss.github.io/braid-design-system/components/MenuRenderer"
66
85
  );
67
- const [{ open, highlightIndex, closeReason }, dispatch] = React.useReducer(
68
- (state, action) => {
69
- switch (action.type) {
70
- case MENU_TRIGGER_UP:
71
- case MENU_ITEM_UP: {
72
- return {
73
- ...state,
74
- open: true,
75
- closeReason: CLOSE_REASON_EXIT,
76
- highlightIndex: lib_components_private_getNextIndex_cjs.getNextIndex(-1, state.highlightIndex, itemCount)
77
- };
78
- }
79
- case MENU_TRIGGER_DOWN:
80
- case MENU_ITEM_DOWN: {
81
- return {
82
- ...state,
83
- open: true,
84
- closeReason: CLOSE_REASON_EXIT,
85
- highlightIndex: lib_components_private_getNextIndex_cjs.getNextIndex(1, state.highlightIndex, itemCount)
86
- };
87
- }
88
- case BACKDROP_CLICK:
89
- case MENU_TRIGGER_ESCAPE:
90
- case MENU_TRIGGER_TAB:
91
- case MENU_ITEM_ESCAPE:
92
- case MENU_ITEM_TAB: {
93
- return {
94
- ...state,
95
- open: false,
96
- closeReason: CLOSE_REASON_EXIT,
97
- highlightIndex: CLOSED_INDEX
98
- };
99
- }
100
- case MENU_ITEM_ENTER:
101
- case MENU_ITEM_SPACE:
102
- case MENU_ITEM_CLICK: {
103
- if ("formElement" in action && action.formElement) {
104
- return state;
105
- }
106
- return {
107
- ...state,
108
- open: false,
109
- closeReason: {
110
- reason: "selection",
111
- index: action.index,
112
- id: action.id
113
- },
114
- highlightIndex: CLOSED_INDEX
115
- };
116
- }
117
- case MENU_ITEM_HOVER: {
118
- return { ...state, highlightIndex: action.value };
119
- }
120
- case MENU_TRIGGER_ENTER:
121
- case MENU_TRIGGER_SPACE: {
122
- const nextOpen = !state.open;
123
- return {
124
- ...state,
125
- open: nextOpen,
126
- closeReason: CLOSE_REASON_EXIT,
127
- highlightIndex: nextOpen ? 0 : CLOSED_INDEX
128
- };
129
- }
130
- case MENU_TRIGGER_CLICK: {
131
- const nextOpen = !state.open;
132
- return {
133
- ...state,
134
- open: nextOpen,
135
- closeReason: CLOSE_REASON_EXIT
136
- };
137
- }
138
- default:
86
+ const [{ open, highlightIndex, closeReason, triggerPosition }, dispatch] = React.useReducer((state, action) => {
87
+ switch (action.type) {
88
+ case MENU_TRIGGER_UP:
89
+ case MENU_ITEM_UP: {
90
+ return {
91
+ ...state,
92
+ open: true,
93
+ closeReason: CLOSE_REASON_EXIT,
94
+ highlightIndex: lib_components_private_getNextIndex_cjs.getNextIndex(-1, state.highlightIndex, itemCount),
95
+ triggerPosition: getPosition(menuContainerRef.current)
96
+ };
97
+ }
98
+ case MENU_TRIGGER_DOWN:
99
+ case MENU_ITEM_DOWN: {
100
+ return {
101
+ ...state,
102
+ open: true,
103
+ closeReason: CLOSE_REASON_EXIT,
104
+ highlightIndex: lib_components_private_getNextIndex_cjs.getNextIndex(1, state.highlightIndex, itemCount),
105
+ triggerPosition: getPosition(menuContainerRef.current)
106
+ };
107
+ }
108
+ case BACKDROP_CLICK:
109
+ case MENU_TRIGGER_ESCAPE:
110
+ case MENU_TRIGGER_TAB:
111
+ case MENU_ITEM_ESCAPE:
112
+ case MENU_ITEM_TAB: {
113
+ return {
114
+ ...state,
115
+ open: false,
116
+ closeReason: CLOSE_REASON_EXIT,
117
+ highlightIndex: CLOSED_INDEX
118
+ };
119
+ }
120
+ case MENU_ITEM_ENTER:
121
+ case MENU_ITEM_SPACE:
122
+ case MENU_ITEM_CLICK: {
123
+ if ("formElement" in action && action.formElement) {
139
124
  return state;
125
+ }
126
+ return {
127
+ ...state,
128
+ open: false,
129
+ closeReason: {
130
+ reason: "selection",
131
+ index: action.index,
132
+ id: action.id
133
+ },
134
+ highlightIndex: CLOSED_INDEX
135
+ };
140
136
  }
141
- },
142
- initialState
143
- );
137
+ case MENU_ITEM_HOVER: {
138
+ return { ...state, highlightIndex: action.value };
139
+ }
140
+ case MENU_TRIGGER_ENTER:
141
+ case MENU_TRIGGER_SPACE: {
142
+ const nextOpen = !state.open;
143
+ return {
144
+ ...state,
145
+ open: nextOpen,
146
+ closeReason: CLOSE_REASON_EXIT,
147
+ highlightIndex: nextOpen ? 0 : CLOSED_INDEX,
148
+ triggerPosition: getPosition(menuContainerRef.current)
149
+ };
150
+ }
151
+ case MENU_TRIGGER_CLICK: {
152
+ const nextOpen = !state.open;
153
+ return {
154
+ ...state,
155
+ open: nextOpen,
156
+ closeReason: CLOSE_REASON_EXIT,
157
+ triggerPosition: getPosition(menuContainerRef.current)
158
+ };
159
+ }
160
+ case WINDOW_RESIZE: {
161
+ return {
162
+ ...state,
163
+ triggerPosition: getPosition(menuContainerRef.current)
164
+ };
165
+ }
166
+ default:
167
+ return state;
168
+ }
169
+ }, initialState);
144
170
  React.useEffect(() => {
145
171
  if (lastOpen.current === open) {
146
172
  return;
@@ -157,6 +183,17 @@ const MenuRenderer = ({
157
183
  buttonRef.current.focus();
158
184
  }
159
185
  };
186
+ React.useEffect(() => {
187
+ const handleResize = () => {
188
+ dispatch({ type: WINDOW_RESIZE });
189
+ };
190
+ if (open) {
191
+ window.addEventListener("resize", handleResize);
192
+ }
193
+ return () => {
194
+ window.removeEventListener("resize", handleResize);
195
+ };
196
+ }, [open]);
160
197
  const onTriggerKeyUp = (event) => {
161
198
  const targetKey = lib_components_private_normalizeKey_cjs.normalizeKey(event);
162
199
  if (targetKey === " " && /firefox|iceweasel|fxios/i.test(navigator.userAgent)) {
@@ -198,41 +235,48 @@ const MenuRenderer = ({
198
235
  dispatch({ type: MENU_TRIGGER_CLICK });
199
236
  }
200
237
  };
201
- return /* @__PURE__ */ jsxRuntime.jsxs(lib_components_Box_Box_cjs.Box, { ...lib_components_private_buildDataAttributes_cjs.buildDataAttributes({ data, validateRestProps: restProps }), children: [
202
- /* @__PURE__ */ jsxRuntime.jsxs(lib_components_Box_Box_cjs.Box, { position: "relative", children: [
203
- trigger(triggerProps, { open }),
204
- /* @__PURE__ */ jsxRuntime.jsx(
205
- Menu,
206
- {
207
- open,
208
- align,
209
- width,
210
- placement,
211
- offsetSpace,
212
- highlightIndex,
213
- reserveIconSpace,
214
- focusTrigger,
215
- dispatch,
216
- children: items
217
- }
218
- )
219
- ] }),
220
- open ? /* @__PURE__ */ jsxRuntime.jsx(
221
- lib_components_Box_Box_cjs.Box,
222
- {
223
- onClick: (event) => {
224
- event.stopPropagation();
225
- event.preventDefault();
226
- dispatch({ type: BACKDROP_CLICK });
227
- },
228
- position: "fixed",
229
- zIndex: "dropdownBackdrop",
230
- top: 0,
231
- left: 0,
232
- className: lib_components_MenuRenderer_MenuRenderer_css_cjs.backdrop
233
- }
234
- ) : null
235
- ] });
238
+ return /* @__PURE__ */ jsxRuntime.jsxs(
239
+ lib_components_Box_Box_cjs.Box,
240
+ {
241
+ ...lib_components_private_buildDataAttributes_cjs.buildDataAttributes({ data, validateRestProps: restProps }),
242
+ ref: menuContainerRef,
243
+ children: [
244
+ trigger(triggerProps, { open }),
245
+ open ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
246
+ /* @__PURE__ */ jsxRuntime.jsx(lib_components_BraidPortal_BraidPortal_cjs.BraidPortal, { children: /* @__PURE__ */ jsxRuntime.jsx(
247
+ Menu,
248
+ {
249
+ align,
250
+ width,
251
+ placement,
252
+ offsetSpace,
253
+ highlightIndex,
254
+ reserveIconSpace,
255
+ focusTrigger,
256
+ dispatch,
257
+ triggerPosition,
258
+ children: items
259
+ }
260
+ ) }),
261
+ /* @__PURE__ */ jsxRuntime.jsx(
262
+ lib_components_Box_Box_cjs.Box,
263
+ {
264
+ onClick: (event) => {
265
+ event.stopPropagation();
266
+ event.preventDefault();
267
+ dispatch({ type: BACKDROP_CLICK });
268
+ },
269
+ position: "fixed",
270
+ zIndex: "dropdownBackdrop",
271
+ top: 0,
272
+ left: 0,
273
+ className: lib_components_MenuRenderer_MenuRenderer_css_cjs.backdrop
274
+ }
275
+ )
276
+ ] }) : null
277
+ ]
278
+ }
279
+ );
236
280
  };
237
281
  const isDivider = (node) => typeof node === "object" && node !== null && "type" in node && node.type === lib_components_MenuItemDivider_MenuItemDivider_cjs.MenuItemDivider;
238
282
  const borderRadius = "large";
@@ -242,14 +286,18 @@ function Menu({
242
286
  width,
243
287
  placement,
244
288
  children,
245
- open,
246
289
  dispatch,
247
290
  focusTrigger,
248
291
  highlightIndex,
249
292
  reserveIconSpace,
293
+ triggerPosition,
250
294
  position = "absolute"
251
295
  }) {
252
296
  let dividerCount = 0;
297
+ const inlineVars = triggerPosition && dynamic.assignInlineVars({
298
+ [lib_components_MenuRenderer_MenuRenderer_css_cjs.triggerVars[placement]]: `${triggerPosition[placement]}px`,
299
+ [lib_components_MenuRenderer_MenuRenderer_css_cjs.triggerVars[align]]: `${triggerPosition[align]}px`
300
+ });
253
301
  return /* @__PURE__ */ jsxRuntime.jsx(lib_components_MenuRenderer_MenuRendererContext_cjs.MenuRendererContext.Provider, { value: { reserveIconSpace }, children: /* @__PURE__ */ jsxRuntime.jsxs(
254
302
  lib_components_Box_Box_cjs.Box,
255
303
  {
@@ -259,19 +307,19 @@ function Menu({
259
307
  boxShadow: placement === "top" ? "small" : "medium",
260
308
  borderRadius,
261
309
  background: "surface",
310
+ paddingY: "xxsmall",
262
311
  marginTop: placement === "bottom" ? offsetSpace : void 0,
263
312
  marginBottom: placement === "top" ? offsetSpace : void 0,
264
313
  transition: "fast",
265
- right: align === "right" ? 0 : void 0,
266
- opacity: !open ? 0 : void 0,
267
314
  overflow: "hidden",
315
+ style: inlineVars,
268
316
  className: [
269
- !open && lib_components_MenuRenderer_MenuRenderer_css_cjs.menuIsClosed,
270
- width !== "content" && lib_components_MenuRenderer_MenuRenderer_css_cjs.width[width],
271
- placement === "top" && lib_components_MenuRenderer_MenuRenderer_css_cjs.placementBottom
317
+ lib_components_MenuRenderer_MenuRenderer_css_cjs.menuPosition,
318
+ lib_components_MenuRenderer_MenuRenderer_css_cjs.animation,
319
+ width !== "content" && lib_components_MenuRenderer_MenuRenderer_css_cjs.width[width]
272
320
  ],
273
321
  children: [
274
- /* @__PURE__ */ jsxRuntime.jsx(lib_components_Box_Box_cjs.Box, { paddingY: lib_components_MenuRenderer_MenuRenderer_css_cjs.menuYPadding, className: lib_components_MenuRenderer_MenuRenderer_css_cjs.menuHeightLimit, children: React.Children.map(children, (item, i) => {
322
+ /* @__PURE__ */ jsxRuntime.jsx(lib_components_private_ScrollContainer_ScrollContainer_cjs.ScrollContainer, { direction: "vertical", fadeSize: "small", children: /* @__PURE__ */ jsxRuntime.jsx(lib_components_Box_Box_cjs.Box, { className: lib_components_MenuRenderer_MenuRenderer_css_cjs.menuHeightLimit, children: React.Children.map(children, (item, i) => {
275
323
  if (isDivider(item)) {
276
324
  dividerCount++;
277
325
  return item;
@@ -290,7 +338,7 @@ function Menu({
290
338
  },
291
339
  menuItemIndex
292
340
  );
293
- }) }),
341
+ }) }) }),
294
342
  /* @__PURE__ */ jsxRuntime.jsx(
295
343
  lib_components_private_Overlay_Overlay_cjs.Overlay,
296
344
  {
@@ -8,10 +8,26 @@ const backdrop = css.style({
8
8
  width: "100vw",
9
9
  height: "100vh"
10
10
  }, "backdrop");
11
- const menuIsClosed = css.style({
12
- transform: `translateY(${cssUtils.calc(lib_themes_vars_css_cjs.vars.grid).negate().multiply(2)})`,
13
- visibility: "hidden"
14
- }, "menuIsClosed");
11
+ const triggerVars = {
12
+ top: css.createVar("triggerVars_top"),
13
+ left: css.createVar("triggerVars_left"),
14
+ bottom: css.createVar("triggerVars_bottom"),
15
+ right: css.createVar("triggerVars_right")
16
+ };
17
+ const menuPosition = css.style({
18
+ top: triggerVars.bottom,
19
+ bottom: triggerVars.top,
20
+ left: triggerVars.left,
21
+ right: triggerVars.right
22
+ }, "menuPosition");
23
+ const animation = css.style({
24
+ animation: `${css.keyframes({
25
+ from: {
26
+ transform: `translateY(${cssUtils.calc(lib_themes_vars_css_cjs.vars.grid).negate().multiply(2)})`,
27
+ opacity: 0
28
+ }
29
+ })} .125s ease forwards`
30
+ }, "animation");
15
31
  const widthVar = css.createVar("widthVar");
16
32
  const baseWidth = css.style({
17
33
  width: cssUtils.calc(widthVar).divide(4).toString()
@@ -30,18 +46,13 @@ const width = css.styleVariants({
30
46
  [widthVar]: w
31
47
  }
32
48
  }], "width");
33
- const placementBottom = css.style({
34
- bottom: "100%"
35
- }, "placementBottom");
36
- const menuYPadding = "xxsmall";
37
49
  const menuHeightLimit = css.style({
38
- maxHeight: cssUtils.calc(lib_themes_vars_css_cjs.vars.touchableSize).multiply(9.5).add(lib_themes_vars_css_cjs.vars.space[menuYPadding]).toString(),
39
- overflowY: "auto"
50
+ maxHeight: cssUtils.calc(lib_themes_vars_css_cjs.vars.touchableSize).multiply(9.5).toString()
40
51
  }, "menuHeightLimit");
41
52
  fileScope.endFileScope();
53
+ exports.animation = animation;
42
54
  exports.backdrop = backdrop;
43
55
  exports.menuHeightLimit = menuHeightLimit;
44
- exports.menuIsClosed = menuIsClosed;
45
- exports.menuYPadding = menuYPadding;
46
- exports.placementBottom = placementBottom;
56
+ exports.menuPosition = menuPosition;
57
+ exports.triggerVars = triggerVars;
47
58
  exports.width = width;
@@ -1,5 +1,5 @@
1
1
  import { setFileScope, endFileScope } from "@vanilla-extract/css/fileScope";
2
- import { style, createVar, styleVariants } from "@vanilla-extract/css";
2
+ import { style, createVar, keyframes, styleVariants } from "@vanilla-extract/css";
3
3
  import { calc } from "@vanilla-extract/css-utils";
4
4
  import { vars } from "../../themes/vars.css.mjs";
5
5
  setFileScope("src/lib/components/MenuRenderer/MenuRenderer.css.ts", "braid-design-system");
@@ -7,10 +7,26 @@ const backdrop = style({
7
7
  width: "100vw",
8
8
  height: "100vh"
9
9
  }, "backdrop");
10
- const menuIsClosed = style({
11
- transform: `translateY(${calc(vars.grid).negate().multiply(2)})`,
12
- visibility: "hidden"
13
- }, "menuIsClosed");
10
+ const triggerVars = {
11
+ top: createVar("triggerVars_top"),
12
+ left: createVar("triggerVars_left"),
13
+ bottom: createVar("triggerVars_bottom"),
14
+ right: createVar("triggerVars_right")
15
+ };
16
+ const menuPosition = style({
17
+ top: triggerVars.bottom,
18
+ bottom: triggerVars.top,
19
+ left: triggerVars.left,
20
+ right: triggerVars.right
21
+ }, "menuPosition");
22
+ const animation = style({
23
+ animation: `${keyframes({
24
+ from: {
25
+ transform: `translateY(${calc(vars.grid).negate().multiply(2)})`,
26
+ opacity: 0
27
+ }
28
+ })} .125s ease forwards`
29
+ }, "animation");
14
30
  const widthVar = createVar("widthVar");
15
31
  const baseWidth = style({
16
32
  width: calc(widthVar).divide(4).toString()
@@ -29,20 +45,15 @@ const width = styleVariants({
29
45
  [widthVar]: w
30
46
  }
31
47
  }], "width");
32
- const placementBottom = style({
33
- bottom: "100%"
34
- }, "placementBottom");
35
- const menuYPadding = "xxsmall";
36
48
  const menuHeightLimit = style({
37
- maxHeight: calc(vars.touchableSize).multiply(9.5).add(vars.space[menuYPadding]).toString(),
38
- overflowY: "auto"
49
+ maxHeight: calc(vars.touchableSize).multiply(9.5).toString()
39
50
  }, "menuHeightLimit");
40
51
  endFileScope();
41
52
  export {
53
+ animation,
42
54
  backdrop,
43
55
  menuHeightLimit,
44
- menuIsClosed,
45
- menuYPadding,
46
- placementBottom,
56
+ menuPosition,
57
+ triggerVars,
47
58
  width
48
59
  };