@manuscripts/style-guide 3.5.4 → 3.5.6

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.
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useFocusCycle = void 0;
4
+ exports.getFocusableElements = getFocusableElements;
5
+ exports.trapFocus = trapFocus;
6
+ const react_1 = require("react");
7
+ function getFocusableElements(element) {
8
+ const selectors = [
9
+ 'button:not([disabled])',
10
+ 'input:not([disabled])',
11
+ 'textarea:not([disabled])',
12
+ 'select:not([disabled])',
13
+ 'a[href]',
14
+ '[tabindex]:not([tabindex="-1"])',
15
+ ];
16
+ return Array.from(element.querySelectorAll(selectors.join(','))).filter((el) => {
17
+ const style = window.getComputedStyle(el);
18
+ return (el instanceof HTMLElement &&
19
+ el.offsetParent &&
20
+ style.visibility !== 'hidden' &&
21
+ style.display !== 'none');
22
+ });
23
+ }
24
+ function trapFocus(e, element) {
25
+ if (e.key !== 'Tab' ||
26
+ !element ||
27
+ !element?.contains(document.activeElement)) {
28
+ return;
29
+ }
30
+ const focusableElements = getFocusableElements(element);
31
+ if (focusableElements.length === 0) {
32
+ return;
33
+ }
34
+ const firstElement = focusableElements[0];
35
+ const lastElement = focusableElements[focusableElements.length - 1];
36
+ const activeElement = document.activeElement;
37
+ if (!e.shiftKey && activeElement === lastElement) {
38
+ e.preventDefault();
39
+ firstElement.focus();
40
+ }
41
+ else if (e.shiftKey && activeElement === firstElement) {
42
+ e.preventDefault();
43
+ lastElement.focus();
44
+ }
45
+ }
46
+ const useFocusCycle = (containerRef) => {
47
+ (0, react_1.useEffect)(() => {
48
+ const handleKeyDown = (e) => trapFocus(e, containerRef.current);
49
+ document.addEventListener('keydown', handleKeyDown);
50
+ return () => document.removeEventListener('keydown', handleKeyDown);
51
+ }, [containerRef]);
52
+ };
53
+ exports.useFocusCycle = useFocusCycle;
package/dist/cjs/index.js CHANGED
@@ -87,6 +87,7 @@ __exportStar(require("./components/Badge"), exports);
87
87
  __exportStar(require("./components/NavDropdown"), exports);
88
88
  __exportStar(require("./components/Dropdown"), exports);
89
89
  __exportStar(require("./components/LoadingOverlay"), exports);
90
+ __exportStar(require("./components/List"), exports);
90
91
  __exportStar(require("./components/Text"), exports);
91
92
  __exportStar(require("./components/RelativeDate"), exports);
92
93
  __exportStar(require("./components/Menus"), exports);
@@ -97,5 +98,6 @@ __exportStar(require("./components/SelectedItemsBox"), exports);
97
98
  __exportStar(require("./hooks/use-dropdown"), exports);
98
99
  __exportStar(require("./hooks/use-menus"), exports);
99
100
  __exportStar(require("./hooks/use-scroll-detection"), exports);
101
+ __exportStar(require("./hooks/use-focus-cycle"), exports);
100
102
  __exportStar(require("./lib/files"), exports);
101
103
  __exportStar(require("./lib/menus"), exports);
@@ -34,6 +34,12 @@ const miniBtnStyles = css `
34
34
  margin: 0 ${(props) => props.theme.grid.unit}px;
35
35
  line-height: 1;
36
36
  `;
37
+ export const outlineStyle = css `
38
+ &:not([disabled]):focus-visible {
39
+ outline: 3px solid ${(props) => props.theme.colors.outline.focus};
40
+ outline-offset: 4px;
41
+ }
42
+ `;
37
43
  const btnStyles = css `
38
44
  align-items: center;
39
45
  background: transparent;
@@ -68,6 +74,8 @@ const btnStyles = css `
68
74
  :disabled {
69
75
  ${disabledBtnStyles}
70
76
  }
77
+
78
+ ${outlineStyle}
71
79
  `;
72
80
  const btnColors = (color, bg, border, important) => `
73
81
  ${color && 'color: ' + color}
@@ -92,11 +100,6 @@ export const SecondaryButton = styled(ButtonTemplate) `
92
100
  &:not([disabled]):active {
93
101
  ${(props) => btnColors(props.theme.colors.button.secondary.color.active, props.theme.colors.button.secondary.background.active, props.theme.colors.button.secondary.border.active, false)}
94
102
  }
95
-
96
- &:focus-visible {
97
- outline: 3px solid ${(props) => props.theme.colors.outline.focus};
98
- outline-offset: 4px;
99
- }
100
103
  `;
101
104
  export const PrimaryButton = styled(ButtonTemplate) `
102
105
  ${(props) => btnColors(props.theme.colors.button.primary.color.default, props.theme.colors.button.primary.background.default, props.theme.colors.button.primary.border.default, false)}
@@ -105,11 +108,6 @@ export const PrimaryButton = styled(ButtonTemplate) `
105
108
  ${(props) => btnColors(props.theme.colors.button.primary.color.hover, props.theme.colors.button.primary.background.hover, props.theme.colors.button.primary.border.hover, false)}
106
109
  }
107
110
 
108
- &:focus-visible {
109
- outline: 3px solid ${(props) => props.theme.colors.outline.focus} !important;
110
- outline-offset: 4px;
111
- }
112
-
113
111
  &:not([disabled]):active {
114
112
  ${(props) => btnColors(props.theme.colors.button.primary.color.active, props.theme.colors.button.primary.background.active, props.theme.colors.button.primary.border.active, false)}
115
113
  }
@@ -52,6 +52,7 @@ export const CheckboxLabel = styled.label `
52
52
  border: 1.5px solid ${(props) => props.theme.colors.border.field.default};
53
53
  border-radius: 3px;
54
54
  margin-right: 10px;
55
+ margin-left: 4px;
55
56
  text-align: center;
56
57
  background: ${(props) => props.theme.colors.background.primary};
57
58
  }
@@ -68,7 +69,8 @@ export const CheckboxLabel = styled.label `
68
69
  }
69
70
 
70
71
  input:focus-visible + div::before {
71
- outline: none;
72
+ outline: 2px solid ${(props) => props.theme.colors.outline.focus};
73
+ outline-offset: 2px;
72
74
  }
73
75
 
74
76
  input:disabled + div::before {
@@ -46,9 +46,13 @@ export const DraggableModal = ({ children, isOpen, onRequestClose, hideOverlay }
46
46
  content: {
47
47
  left: '0',
48
48
  bottom: '0',
49
+ top: 'auto',
50
+ margin: '0',
49
51
  transition: 'none',
50
52
  position: 'fixed',
51
53
  width: '100%',
54
+ maxWidth: '100%',
55
+ maxHeight: 'none',
52
56
  },
53
57
  }, children: _jsx(DraggableModalContainer, { onMouseDown: mouseDown, onMouseMove: mouseMove, "data-cy": "find-replace-modal", children: children }) }));
54
58
  };
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import styled, { keyframes } from 'styled-components';
3
+ import { SecondaryButton } from './Button';
4
+ import { withFocusTrap, withListNavigation, withNavigableListItem, } from './List';
3
5
  const slideIn = keyframes `
4
6
  from {
5
7
  transform: translateX(100%);
@@ -8,7 +10,7 @@ const slideIn = keyframes `
8
10
  transform: translateX(0);
9
11
  }
10
12
  `;
11
- const DrawerContainer = styled.div `
13
+ const DrawerContainer = withFocusTrap(styled.div `
12
14
  width: ${(props) => props.width || '300px'};
13
15
  background: ${(props) => props.theme.colors.background.primary};
14
16
  height: 100%;
@@ -19,9 +21,10 @@ const DrawerContainer = styled.div `
19
21
  right: 0;
20
22
  top: 0;
21
23
  z-index: 1;
22
- `;
23
- const DrawerBackButton = styled.button `
24
- padding: 8px 16px;
24
+ `);
25
+ const DrawerBackButton = styled(SecondaryButton) `
26
+ margin: 0 12px;
27
+ padding: 8px 4px;
25
28
  border: none;
26
29
  background: none;
27
30
  font-weight: ${(props) => props.theme.font.weight.normal};
@@ -30,7 +33,7 @@ const DrawerBackButton = styled.button `
30
33
  color: ${(props) => props.theme.colors.brand.default};
31
34
  cursor: pointer;
32
35
  display: flex;
33
- align-items: center;
36
+ justify-content: start;
34
37
  gap: 4px;
35
38
 
36
39
  &:hover {
@@ -38,13 +41,13 @@ const DrawerBackButton = styled.button `
38
41
  opacity: 0.8;
39
42
  }
40
43
  `;
41
- export const DrawerItemsList = styled.ul `
44
+ export const DrawerItemsList = withListNavigation(styled.ul `
42
45
  list-style: none;
43
- padding: 0;
46
+ padding: 8px;
44
47
  margin: 0;
45
48
  overflow-y: auto;
46
- `;
47
- export const DrawerListItem = styled.li `
49
+ `);
50
+ export const DrawerListItem = withNavigableListItem(styled.li `
48
51
  padding: 8px 16px;
49
52
  cursor: pointer;
50
53
  display: flex;
@@ -59,7 +62,7 @@ export const DrawerListItem = styled.li `
59
62
  &:last-child {
60
63
  border-bottom: none;
61
64
  }
62
- `;
65
+ `);
63
66
  export const DrawerIcon = styled.span `
64
67
  width: 20px;
65
68
  height: 20px;
@@ -73,6 +73,11 @@ export const InspectorTab = styled(Tab) `
73
73
  border-color: ${(props) => props.theme.colors.brand.default};
74
74
  color: ${(props) => props.theme.colors.brand.default};
75
75
  }
76
+
77
+ &[data-headlessui-state~='selected'][data-headlessui-state~='focus'] {
78
+ outline: 2px solid ${(props) => props.theme.colors.outline.focus};
79
+ outline-offset: -2px;
80
+ }
76
81
  }
77
82
  `;
78
83
  export const InspectorTabPanelHeading = styled.div `
@@ -0,0 +1,187 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useCallback, useLayoutEffect, useRef, } from 'react';
3
+ import styled from 'styled-components';
4
+ import { outlineStyle } from './Button';
5
+ import { getFocusableElements, trapFocus } from '../hooks/use-focus-cycle';
6
+ function mergeRefs(...refs) {
7
+ return (value) => {
8
+ refs.forEach((ref) => {
9
+ if (typeof ref === 'function') {
10
+ ref(value);
11
+ }
12
+ else if (ref != null) {
13
+ ;
14
+ ref.current = value;
15
+ }
16
+ });
17
+ };
18
+ }
19
+ function handleTabWithinListItem(e, items) {
20
+ if (e.key !== 'Tab') {
21
+ return false;
22
+ }
23
+ const activeElement = document.activeElement;
24
+ const currentItem = activeElement.closest('[data-list-item]');
25
+ const isOnInternalElement = currentItem &&
26
+ currentItem !== activeElement &&
27
+ currentItem.contains(activeElement);
28
+ if (currentItem && isOnInternalElement) {
29
+ const focusableElements = getFocusableElements(currentItem);
30
+ const currentIndex = focusableElements.indexOf(activeElement);
31
+ const isFirstElement = currentIndex === 0;
32
+ const isLastElement = currentIndex === focusableElements.length - 1;
33
+ if (e.shiftKey && isFirstElement) {
34
+ e.preventDefault();
35
+ currentItem.focus();
36
+ return true;
37
+ }
38
+ else if (!e.shiftKey && isLastElement) {
39
+ const currentIndex = items.indexOf(currentItem);
40
+ const nextItem = items[currentIndex + 1];
41
+ if (nextItem) {
42
+ e.preventDefault();
43
+ nextItem.focus();
44
+ return true;
45
+ }
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+ export function withListNavigation(Component) {
51
+ return styled(forwardRef((props, forwardedRef) => {
52
+ const containerRef = useRef(null);
53
+ const tappingOutIndex = useRef(0);
54
+ const getListItems = useCallback(() => {
55
+ const element = containerRef.current;
56
+ if (!element) {
57
+ return [];
58
+ }
59
+ return Array.from(element.querySelectorAll('[data-list-item]'));
60
+ }, []);
61
+ const getColumnCount = useCallback(() => {
62
+ const element = containerRef.current;
63
+ if (!element) {
64
+ return 1;
65
+ }
66
+ const gridStyle = window.getComputedStyle(element);
67
+ return gridStyle
68
+ .getPropertyValue('grid-template-columns')
69
+ .split(' ')
70
+ .filter((val) => val !== '').length;
71
+ }, []);
72
+ const handleKeyDown = useCallback((e) => {
73
+ const items = getListItems();
74
+ const handled = handleTabWithinListItem(e, items);
75
+ if (handled) {
76
+ return;
77
+ }
78
+ const currentIndex = items.indexOf(document.activeElement);
79
+ const element = containerRef.current;
80
+ const columnCount = getColumnCount();
81
+ if (currentIndex === -1 || !element) {
82
+ return;
83
+ }
84
+ let newIndex = currentIndex;
85
+ switch (e.key) {
86
+ case 'ArrowUp': {
87
+ e.preventDefault();
88
+ newIndex =
89
+ (currentIndex - columnCount + items.length) % items.length;
90
+ break;
91
+ }
92
+ case 'ArrowDown': {
93
+ e.preventDefault();
94
+ newIndex = (currentIndex + columnCount) % items.length;
95
+ break;
96
+ }
97
+ case 'ArrowLeft': {
98
+ if (columnCount > 1) {
99
+ e.preventDefault();
100
+ newIndex = (currentIndex - 1 + items.length) % items.length;
101
+ }
102
+ break;
103
+ }
104
+ case 'ArrowRight': {
105
+ if (columnCount > 1) {
106
+ e.preventDefault();
107
+ newIndex = (currentIndex + 1) % items.length;
108
+ }
109
+ break;
110
+ }
111
+ case 'Tab': {
112
+ tappingOutIndex.current = currentIndex;
113
+ break;
114
+ }
115
+ }
116
+ if (currentIndex !== newIndex) {
117
+ items[currentIndex].setAttribute('tabindex', '-1');
118
+ items[newIndex].setAttribute('tabindex', '0');
119
+ items[newIndex].focus();
120
+ }
121
+ }, [getListItems, getColumnCount, containerRef]);
122
+ const handelOnBlur = useCallback(() => {
123
+ if (tappingOutIndex.current) {
124
+ const items = getListItems();
125
+ items[tappingOutIndex.current].setAttribute('tabindex', '-1');
126
+ items[0].setAttribute('tabindex', '0');
127
+ tappingOutIndex.current = 0;
128
+ }
129
+ }, [getListItems]);
130
+ useLayoutEffect(() => {
131
+ const element = containerRef.current;
132
+ if (!element) {
133
+ return;
134
+ }
135
+ const firstItem = element.querySelector('[data-list-item]');
136
+ if (!firstItem) {
137
+ return;
138
+ }
139
+ if (firstItem.tabIndex === 0) {
140
+ return;
141
+ }
142
+ const tabbedElement = element.querySelector('[data-list-item][tabindex="0"]');
143
+ if (tabbedElement) {
144
+ tabbedElement.tabIndex = -1;
145
+ }
146
+ firstItem.tabIndex = 0;
147
+ });
148
+ return (_jsx(Component, { ref: mergeRefs(containerRef, forwardedRef), ...props, onKeyDown: handleKeyDown, onBlur: handelOnBlur }));
149
+ })) `
150
+ outline: none;
151
+ `;
152
+ }
153
+ export function withNavigableListItem(Component) {
154
+ return styled(forwardRef((props, forwardedRef) => {
155
+ const listItemRef = useRef(null);
156
+ const handleKeyDown = useCallback((e) => {
157
+ const listItem = listItemRef.current;
158
+ const element = e.target;
159
+ if (element && listItem && element === listItem && e.key === 'Enter') {
160
+ const interactiveElements = [
161
+ 'BUTTON',
162
+ 'A',
163
+ 'INPUT',
164
+ 'TEXTAREA',
165
+ 'SELECT',
166
+ ];
167
+ if (!interactiveElements.includes(listItem.tagName)) {
168
+ e.preventDefault();
169
+ listItem.click();
170
+ }
171
+ }
172
+ }, []);
173
+ return (_jsx(Component, { tabIndex: -1, "data-list-item": true, ref: mergeRefs(listItemRef, forwardedRef), onKeyDown: handleKeyDown, ...props }));
174
+ })) `
175
+ &:not([disabled]):focus-visible {
176
+ background-color: ${(props) => props.theme.colors.background.fifth};
177
+ }
178
+ ${outlineStyle}
179
+ `;
180
+ }
181
+ export function withFocusTrap(Component) {
182
+ return forwardRef((props, forwardedRef) => {
183
+ const containerRef = useRef(null);
184
+ const handleKeyDown = useCallback((e) => trapFocus(e, containerRef.current), []);
185
+ return (_jsx(Component, { ref: mergeRefs(containerRef, forwardedRef), onKeyDown: handleKeyDown, ...props }));
186
+ });
187
+ }
@@ -1,26 +1,17 @@
1
- import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- import Modal from 'react-modal';
3
- export const LoadingOverlay = ({ children, }) => (_jsx(_Fragment, { children: _jsx(Modal, { isOpen: true, style: {
4
- overlay: {
5
- position: 'fixed',
6
- top: 0,
7
- left: 0,
8
- right: 0,
9
- bottom: 0,
10
- backgroundColor: 'white',
11
- zIndex: 20,
12
- },
13
- content: {
14
- position: 'fixed',
15
- top: 0,
16
- left: 0,
17
- right: 0,
18
- bottom: 0,
19
- background: 'white',
20
- display: 'flex',
21
- flexDirection: 'column',
22
- padding: 0,
23
- overflow: 'auto',
24
- border: 'none',
25
- },
26
- }, children: _jsx(_Fragment, { children: children }) }) }));
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import styled from 'styled-components';
3
+ export const LoadingOverlay = ({ children, }) => _jsx(Overlay, { children: children });
4
+ const Overlay = styled.div `
5
+ position: fixed;
6
+ top: 0;
7
+ left: 0;
8
+ right: 0;
9
+ bottom: 0;
10
+ background: white;
11
+ display: flex;
12
+ flex-direction: column;
13
+ padding: 0;
14
+ overflow: auto;
15
+ border: none;
16
+ z-index: 20;
17
+ `;
@@ -53,6 +53,11 @@ const RemoveButton = styled.button `
53
53
  &:hover {
54
54
  background: black;
55
55
  }
56
+
57
+ &:not([disabled]):focus-visible {
58
+ outline: 1px solid ${(props) => props.theme.colors.outline.focus};
59
+ outline-offset: 2px;
60
+ }
56
61
  `;
57
62
  const Input = styled.input `
58
63
  ${commonStyles}
@@ -115,5 +120,5 @@ export const MultiValueInput = ({ id, inputType, placeholder = '', initialValues
115
120
  : inputType === 'number'
116
121
  ? 'Enter number and press enter'
117
122
  : 'Enter text and press enter';
118
- return (_jsxs(Container, { children: [values.map((value, index) => (_jsxs(Chip, { children: [value, _jsx(RemoveButton, { onMouseUp: () => handleRemoveValue(index), children: "\u00D7" })] }, index))), _jsx(Input, { id: id, type: inputType, value: currentValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onBlur: handleBlur, placeholder: xplaceholder })] }));
123
+ return (_jsxs(Container, { children: [values.map((value, index) => (_jsxs(Chip, { children: [value, _jsx(RemoveButton, { type: 'button', onClick: () => handleRemoveValue(index), children: "\u00D7" })] }, index))), _jsx(Input, { id: id, type: inputType, value: currentValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onBlur: handleBlur, placeholder: xplaceholder })] }));
119
124
  };
@@ -52,6 +52,11 @@ const RemoveButton = styled.button `
52
52
  padding: 0;
53
53
  font-family: ${(props) => props.theme.font.family.sans};
54
54
  font-weight: ${(props) => props.theme.font.weight.light};
55
+
56
+ &:not([disabled]):focus-visible {
57
+ outline: 1px solid ${(props) => props.theme.colors.outline.focus};
58
+ outline-offset: 2px;
59
+ }
55
60
  `;
56
61
  export const SelectedItemsBox = ({ items, onRemove, placeholder = 'No items selected', ...props }) => {
57
62
  return (_jsx(BoxContainer, { ...props, children: items.length > 0 ? (_jsx(ItemsList, { children: items.map((item) => (_jsxs(Item, { "data-cy": "item", children: [item.label, _jsx(RemoveButton, { onClick: () => onRemove(item.id), "aria-label": `Remove ${item.label}`, children: "x" })] }, item.id))) })) : (_jsx(EmptyContainer, { children: _jsx(Placeholder, { children: placeholder }) })) }));