Skip to content

Commit b280519

Browse files
committed
Add tag builder options (#621)
* Add tag filter * Add focus helpers * Cleanup & lint * Split popup menu into its own component * Add detail component * Update search menu to use Detail component * Add docs and fix issues * Add menu tests * lint * Add lodash to dependencies * Bump & lint * Update snapshot * Update changelog * Fix version
1 parent b6e1282 commit b280519

32 files changed

+2677
-8
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@
5757

5858
- Upgrade dependencies #450
5959

60+
## react-components 5.34.9 (2022-07-28)
61+
62+
- [Menu] Adding Detail, Menu, Menu.Search, and Tag.Search components (by [@krable55](https://github.com/krable55) in [#621](https://github.com/puppetlabs/design-system/pull/621))
63+
6064
## react-components 5.33.9 (2022-07-28)
6165

6266
- Adding optional tooltip to ButtonSelect component (by [@alex501212](https://github.com/alex501212) in [#591](https://github.com/puppetlabs/design-system/pull/591))

packages/data-grid/src/__test__/__snapshots__/tagBuilder.test.jsx.snap

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ exports[`Snapshot test Check component matches previous HTML snapshot 1`] = `
3636
type="primary"
3737
>
3838
<div
39+
class="dg-filter-tag dg-filter-tag-0"
3940
className="rc-tag rc-tag-primary rc-tag-bold"
4041
>
4142
<div
@@ -122,6 +123,7 @@ exports[`Snapshot test Check component matches previous HTML snapshot 1`] = `
122123
type="primary"
123124
>
124125
<div
126+
class="dg-filter-tag dg-filter-tag-1"
125127
className="rc-tag rc-tag-primary rc-tag-bold"
126128
>
127129
<div

packages/design-system-website/styleguide.config.js

+4
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@ module.exports = {
164164
'**/toolbar/Actions.js',
165165
'**/logo/logos.js',
166166
'**/breadcrumb/BreadcrumbSection.js',
167+
'**/tag/Search.js',
168+
'**/menu/Arrow.js',
169+
'**/menu/Container.js',
170+
'**/menu/Trigger.js',
167171
'**/tooltips/Tooltip.js',
168172
],
169173
},

packages/react-components/package-lock.json

+9-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-components/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"@puppet/sass-variables": "^2.0.0-alpha.3",
8282
"classnames": "^2.3.1",
8383
"hoist-non-react-statics": "^3.3.1",
84+
"lodash": "^4.17.21",
8485
"prop-types": "^15.7.2",
8586
"react-modal": "^3.14.3",
8687
"react-popper": "^2.2.5",

packages/react-components/source/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Columns from './react/library/columns';
1313
import ConfirmationModal from './react/library/confirmation-modal';
1414
import Content from './react/library/content';
1515
import Copy from './react/library/copy';
16+
import Detail from './react/library/detail';
1617
import Drawer from './react/library/drawer';
1718
import Form from './react/library/form';
1819
import Heading from './react/library/heading';
@@ -57,6 +58,8 @@ export {
5758
Content,
5859
Copy,
5960
Drawer,
61+
Detail,
62+
Filters,
6063
Form,
6164
Heading,
6265
Icon,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import React, {
2+
useEffect,
3+
useRef,
4+
useContext,
5+
useState,
6+
useCallback,
7+
} from 'react';
8+
import classNames from 'classnames';
9+
import { RovingFocusContext, getTabIndexId } from './useRovingFocus';
10+
import MenuContext from '../internal/popup-menus/menu-context';
11+
import { ENTER_KEY_CODE } from '../constants';
12+
13+
/**
14+
* @description HOC that adds roving focus to a component. Meant to be used with the RovingFocusProvider, it wraps the menu item component and adds the necessary props and event handlers to make it focusable, keeping the focus order according to it's visual position on the page.
15+
* @param {ReactComponent} WrappedComponent - The React component to wrap. Button, Checkbox, Link, etc.
16+
* @note The wrapped component must accept an inputRef prop and forward it to the element that should be focused.
17+
* @returns {
18+
* RovingFocusContext.Consumer > MenuItemHOC(WrappedComponent)}
19+
*/
20+
const asFocusItem = WrappedComponent => {
21+
// Wrap the component in a providers that uses the context props
22+
const MenuItem = componentProps => {
23+
const { closeMenu, closeOnSelect } = useContext(MenuContext);
24+
const {
25+
currentFocus,
26+
setFocus,
27+
addTarget,
28+
removeTarget,
29+
indexes,
30+
} = useContext(RovingFocusContext);
31+
32+
const [ref, setRef] = useState(null);
33+
const { current: id } = useRef(getTabIndexId(componentProps));
34+
const [position, setPosition] = useState(null);
35+
const index = indexes[id];
36+
const focus = index === currentFocus;
37+
38+
const setRefNode = useCallback(
39+
node => {
40+
setRef(node);
41+
if (node) {
42+
setPosition(node.getBoundingClientRect());
43+
}
44+
},
45+
[currentFocus, focus],
46+
);
47+
48+
useEffect(() => {
49+
const node = { id };
50+
// Register the node position on the page
51+
if (position) {
52+
node.x = position.x;
53+
node.y = position.y;
54+
addTarget(node);
55+
}
56+
return () => removeTarget(node);
57+
}, [position]);
58+
59+
useEffect(() => {
60+
if (focus && ref) {
61+
// Move element into view when it is focused and apply focus styles
62+
ref.className = classNames(ref.className, { focus });
63+
ref.focus();
64+
} else if (!focus && ref) {
65+
// Remove focus styles when the element is not focused
66+
ref.className = ref.className.replace(' focus', '');
67+
}
68+
}, [focus]);
69+
70+
// Wrap the component's onClick handler to also set the focus onClick
71+
const handleSelect = (...args) => {
72+
const { onClick } = componentProps;
73+
setFocus(index);
74+
if (onClick) {
75+
onClick(...args);
76+
if (closeMenu && closeOnSelect) closeMenu();
77+
}
78+
};
79+
80+
const handleKeyDown = (...args) => {
81+
const [event] = args;
82+
setFocus(index);
83+
84+
const { onClick } = componentProps;
85+
if (onClick && event && event.keyCode === ENTER_KEY_CODE) {
86+
onClick(...args);
87+
if (closeMenu && closeOnSelect) closeMenu();
88+
}
89+
};
90+
91+
return (
92+
<WrappedComponent
93+
{...componentProps}
94+
inputRef={setRefNode}
95+
onClick={handleSelect}
96+
className={classNames(componentProps.className, { focus })}
97+
onKeyDown={handleKeyDown}
98+
role="menuitem"
99+
tabIndex={focus ? 0 : -1}
100+
/>
101+
);
102+
};
103+
104+
MenuItem.displayName = `FocusItem`;
105+
MenuItem.defaultProps = {
106+
...WrappedComponent.defaultProps,
107+
};
108+
return MenuItem;
109+
};
110+
111+
export default asFocusItem;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { useRef, useState, useEffect } from 'react';
2+
import { usePopper } from 'react-popper';
3+
import { uniqueId } from 'lodash';
4+
5+
/**
6+
*@description Hook that returns the necessary refs and event handlers to make a popper.js menu component.
7+
* @param {popperOptions} param.popperOptions - Options to pass to the popper instance
8+
* @link https://popper.js.org/docs/v2/constructors/
9+
*/
10+
const useMenu = ({ popperOptions }) => {
11+
/** Ref of the menu */
12+
const [menuRef, setMenu] = useState(null);
13+
14+
/** Ref of the trigger (button, select, etc.) */
15+
const [triggerRef, setTrigger] = useState(null);
16+
17+
/** Ref of the optional arrow element */
18+
const [arrowRef, setArrowRef] = useState(null);
19+
20+
const [closeOnSelect, setCloseOnSelect] = useState(true);
21+
22+
const { current: menuId } = useRef(uniqueId(`menu-`));
23+
const { current: menuTriggerId } = useRef(uniqueId(`menu-trigger-`));
24+
const { current: menuArrowId } = useRef(uniqueId(`menu-arrow-`));
25+
26+
const popperModifiers = [
27+
{
28+
name: 'flip',
29+
enabled: true,
30+
},
31+
{
32+
name: 'arrow',
33+
enabled: !!arrowRef,
34+
options: {
35+
element: arrowRef,
36+
offset: [0, 6],
37+
},
38+
},
39+
{
40+
name: 'offset',
41+
options: {
42+
offset: [0, 6],
43+
},
44+
},
45+
{
46+
name: 'preventOverflow',
47+
options: {
48+
rootBoundary: 'document',
49+
padding: 0,
50+
},
51+
},
52+
];
53+
54+
const { styles, attributes, update } = usePopper(triggerRef, menuRef, {
55+
placement: 'bottom-start',
56+
modifiers: popperModifiers,
57+
strategy: 'absolute',
58+
...popperOptions,
59+
});
60+
61+
// Focus on a node
62+
const focusOnNode = node => () => {
63+
if (node) node.focus();
64+
};
65+
66+
const focusMenu = focusOnNode(menuRef);
67+
const focusTrigger = focusOnNode(triggerRef);
68+
69+
useEffect(() => {
70+
// Update the menu after a click or keydown event to ensure the menu is positioned correctly after the DOM has updated
71+
const handleUpdate = () => {
72+
if (update) setTimeout(update, 10);
73+
};
74+
75+
document.addEventListener('click', handleUpdate, true);
76+
document.addEventListener('keydown', handleUpdate, true);
77+
document.addEventListener('scroll', handleUpdate, true);
78+
document.addEventListener('resize', handleUpdate, true);
79+
80+
return () => {
81+
document.removeEventListener('click', handleUpdate, true);
82+
document.removeEventListener('keydown', handleUpdate, true);
83+
document.removeEventListener('scroll', handleUpdate, true);
84+
document.removeEventListener('resize', handleUpdate, true);
85+
};
86+
}, [triggerRef, update]);
87+
88+
return {
89+
menuRef: setMenu,
90+
currentMenuRef: menuRef,
91+
triggerRef: setTrigger,
92+
arrowRef: setArrowRef,
93+
focusMenu,
94+
focusTrigger,
95+
setCloseOnSelect,
96+
closeOnSelect,
97+
styles,
98+
attributes,
99+
update,
100+
/** Id to be set on menu container. Needed for closing menu on click outside */
101+
menuId,
102+
/** Id to be set on menu trigger. Needed for aria-controls */
103+
menuTriggerId,
104+
/** Id to be set on menu arrow. Needed for aria-controls */
105+
menuArrowId,
106+
};
107+
};
108+
109+
export default useMenu;

0 commit comments

Comments
 (0)