@khanacademy/wonder-blocks-tabs 0.1.1 → 0.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,17 @@
1
1
  # @khanacademy/wonder-blocks-tabs
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 86e1901: Override border on different states (focus-visible, hover, press) to account for the `IconButton` change from `outline` to `border` + `boxShadow`.
8
+
9
+ ## 0.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 27f6298: `NavigationTabs`: Add `animated` prop to enable transition animation. Defaults to false.
14
+
3
15
  ## 0.1.1
4
16
 
5
17
  ### Patch Changes
@@ -49,6 +49,8 @@ export declare const NavigationTabItem: React.ForwardRefExoticComponent<Readonly
49
49
  /**
50
50
  * If the `NavigationTabItem` is the current page. If `true`, current
51
51
  * styling and aria-current=page will be applied to the Link.
52
+ *
53
+ * Note: NavigationTabs provides the styling for the current tab item.
52
54
  */
53
55
  current?: boolean;
54
56
  /**
@@ -64,4 +64,9 @@ export declare const NavigationTabs: React.ForwardRefExoticComponent<Readonly<im
64
64
  root?: StyleType;
65
65
  list?: StyleType;
66
66
  };
67
+ /**
68
+ * Whether to include animation in the `NavigationTabs`. This should be false
69
+ * if the user has `prefers-reduced-motion` opted in. Defaults to `false`.
70
+ */
71
+ animated?: boolean;
67
72
  } & React.RefAttributes<HTMLElement>>;
package/dist/es/index.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import _extends from '@babel/runtime/helpers/extends';
2
2
  import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/objectWithoutPropertiesLoose';
3
- import { addStyle } from '@khanacademy/wonder-blocks-core';
3
+ import { addStyle, useOnMountEffect, View } from '@khanacademy/wonder-blocks-core';
4
4
  import { sizing, semanticColor, breakpoint } from '@khanacademy/wonder-blocks-tokens';
5
5
  import { StyleSheet } from 'aphrodite';
6
6
  import * as React from 'react';
7
7
  import { styles as styles$2 } from '@khanacademy/wonder-blocks-typography';
8
8
 
9
- const _excluded$1 = ["id", "testId", "children", "aria-label", "aria-labelledby", "styles"];
9
+ const _excluded$1 = ["id", "testId", "children", "aria-label", "aria-labelledby", "styles", "animated"];
10
10
  const StyledNav = addStyle("nav");
11
11
  const StyledUl = addStyle("ul");
12
+ const StyledDiv = addStyle("div");
12
13
  const NavigationTabs = React.forwardRef(function NavigationTabs(props, ref) {
13
14
  const {
14
15
  id,
@@ -16,9 +17,56 @@ const NavigationTabs = React.forwardRef(function NavigationTabs(props, ref) {
16
17
  children,
17
18
  "aria-label": ariaLabel,
18
19
  "aria-labelledby": ariaLabelledBy,
19
- styles: stylesProp
20
+ styles: stylesProp,
21
+ animated = false
20
22
  } = props,
21
23
  otherProps = _objectWithoutPropertiesLoose(props, _excluded$1);
24
+ const listRef = React.useRef(null);
25
+ const indicatorIsReady = React.useRef(false);
26
+ const [underlineStyle, setUnderlineStyle] = React.useState({
27
+ left: 0,
28
+ width: 0
29
+ });
30
+ const updateUnderlineStyle = React.useCallback(() => {
31
+ if (!listRef.current) {
32
+ return;
33
+ }
34
+ const activeTab = Array.from(listRef.current.children).find(child => {
35
+ var _child$children$;
36
+ return (_child$children$ = child.children[0]) == null ? void 0 : _child$children$.ariaCurrent;
37
+ });
38
+ if (activeTab) {
39
+ const tabRect = activeTab.getBoundingClientRect();
40
+ const parentRect = listRef.current.getBoundingClientRect();
41
+ const zoomFactor = parentRect.width / listRef.current.offsetWidth;
42
+ const left = (tabRect.left - parentRect.left) / zoomFactor;
43
+ const width = tabRect.width / zoomFactor;
44
+ setUnderlineStyle({
45
+ left,
46
+ width
47
+ });
48
+ }
49
+ }, [setUnderlineStyle, listRef]);
50
+ useOnMountEffect(() => {
51
+ if (!listRef.current || !window.ResizeObserver) {
52
+ return;
53
+ }
54
+ const observer = new window.ResizeObserver(([entry]) => {
55
+ if (entry) {
56
+ updateUnderlineStyle();
57
+ if (!indicatorIsReady.current) {
58
+ indicatorIsReady.current = true;
59
+ }
60
+ }
61
+ });
62
+ observer.observe(listRef.current);
63
+ return () => {
64
+ observer.disconnect();
65
+ };
66
+ });
67
+ React.useEffect(() => {
68
+ updateUnderlineStyle();
69
+ }, [children, updateUnderlineStyle]);
22
70
  return React.createElement(StyledNav, _extends({
23
71
  id: id,
24
72
  "data-testid": testId,
@@ -26,14 +74,26 @@ const NavigationTabs = React.forwardRef(function NavigationTabs(props, ref) {
26
74
  "aria-labelledby": ariaLabelledBy,
27
75
  ref: ref,
28
76
  style: [styles$1.nav, stylesProp == null ? void 0 : stylesProp.root]
29
- }, otherProps), React.createElement(StyledUl, {
30
- style: [styles$1.list, stylesProp == null ? void 0 : stylesProp.list]
31
- }, children));
77
+ }, otherProps), React.createElement(StyledDiv, {
78
+ style: styles$1.contents
79
+ }, React.createElement(StyledUl, {
80
+ style: [styles$1.list, stylesProp == null ? void 0 : stylesProp.list],
81
+ ref: listRef
82
+ }, children), indicatorIsReady.current && React.createElement(View, {
83
+ style: [{
84
+ transform: `translateX(${underlineStyle.left}px)`,
85
+ width: `${underlineStyle.width}px`
86
+ }, styles$1.currentUnderline, animated && styles$1.underlineTransition],
87
+ role: "presentation"
88
+ })));
32
89
  });
33
90
  const styles$1 = StyleSheet.create({
34
91
  nav: {
35
92
  overflowX: "auto"
36
93
  },
94
+ contents: {
95
+ position: "relative"
96
+ },
37
97
  list: {
38
98
  paddingInline: sizing.size_050,
39
99
  paddingBlock: sizing.size_0,
@@ -41,6 +101,16 @@ const styles$1 = StyleSheet.create({
41
101
  display: "flex",
42
102
  gap: sizing.size_200,
43
103
  flexWrap: "nowrap"
104
+ },
105
+ currentUnderline: {
106
+ position: "absolute",
107
+ bottom: 0,
108
+ left: 0,
109
+ height: sizing.size_050,
110
+ backgroundColor: semanticColor.action.secondary.progressive.default.foreground
111
+ },
112
+ underlineTransition: {
113
+ transition: "transform 0.3s ease, width 0.3s ease"
44
114
  }
45
115
  });
46
116
 
@@ -90,9 +160,8 @@ const styles = StyleSheet.create({
90
160
  }
91
161
  },
92
162
  current: {
93
- boxShadow: `inset 0 -${sizing.size_050} 0 0 ${semanticColor.action.secondary.progressive.default.foreground}`,
94
163
  [":has(a:hover)"]: {
95
- boxShadow: `inset 0 -${sizing.size_050} 0 0 ${semanticColor.action.secondary.progressive.default.foreground}`
164
+ boxShadow: "none"
96
165
  }
97
166
  },
98
167
  currentLink: {
@@ -108,17 +177,20 @@ const styles = StyleSheet.create({
108
177
  textDecoration: "none",
109
178
  ":hover": {
110
179
  textDecoration: "none",
180
+ border: "none",
111
181
  outline: "none",
112
182
  color: semanticColor.action.secondary.progressive.default.foreground,
113
183
  backgroundColor: "transparent"
114
184
  },
115
185
  ":active": {
116
186
  textDecoration: "none",
187
+ border: "none",
117
188
  outline: "none",
118
189
  color: semanticColor.action.secondary.progressive.press.foreground
119
190
  },
120
191
  ":focus-visible": {
121
192
  color: semanticColor.action.secondary.progressive.default.foreground,
193
+ border: "none",
122
194
  outline: "none",
123
195
  boxShadow: `0 0 0 ${sizing.size_025} ${semanticColor.focus.inner}, 0 0 0 ${sizing.size_050} ${semanticColor.focus.outer}`,
124
196
  borderRadius: 0
package/dist/index.js CHANGED
@@ -34,9 +34,10 @@ var _extends__default = /*#__PURE__*/_interopDefaultLegacy(_extends);
34
34
  var _objectWithoutPropertiesLoose__default = /*#__PURE__*/_interopDefaultLegacy(_objectWithoutPropertiesLoose);
35
35
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
36
36
 
37
- const _excluded$1 = ["id", "testId", "children", "aria-label", "aria-labelledby", "styles"];
37
+ const _excluded$1 = ["id", "testId", "children", "aria-label", "aria-labelledby", "styles", "animated"];
38
38
  const StyledNav = wonderBlocksCore.addStyle("nav");
39
39
  const StyledUl = wonderBlocksCore.addStyle("ul");
40
+ const StyledDiv = wonderBlocksCore.addStyle("div");
40
41
  const NavigationTabs = React__namespace.forwardRef(function NavigationTabs(props, ref) {
41
42
  const {
42
43
  id,
@@ -44,9 +45,56 @@ const NavigationTabs = React__namespace.forwardRef(function NavigationTabs(props
44
45
  children,
45
46
  "aria-label": ariaLabel,
46
47
  "aria-labelledby": ariaLabelledBy,
47
- styles: stylesProp
48
+ styles: stylesProp,
49
+ animated = false
48
50
  } = props,
49
51
  otherProps = _objectWithoutPropertiesLoose__default["default"](props, _excluded$1);
52
+ const listRef = React__namespace.useRef(null);
53
+ const indicatorIsReady = React__namespace.useRef(false);
54
+ const [underlineStyle, setUnderlineStyle] = React__namespace.useState({
55
+ left: 0,
56
+ width: 0
57
+ });
58
+ const updateUnderlineStyle = React__namespace.useCallback(() => {
59
+ if (!listRef.current) {
60
+ return;
61
+ }
62
+ const activeTab = Array.from(listRef.current.children).find(child => {
63
+ var _child$children$;
64
+ return (_child$children$ = child.children[0]) == null ? void 0 : _child$children$.ariaCurrent;
65
+ });
66
+ if (activeTab) {
67
+ const tabRect = activeTab.getBoundingClientRect();
68
+ const parentRect = listRef.current.getBoundingClientRect();
69
+ const zoomFactor = parentRect.width / listRef.current.offsetWidth;
70
+ const left = (tabRect.left - parentRect.left) / zoomFactor;
71
+ const width = tabRect.width / zoomFactor;
72
+ setUnderlineStyle({
73
+ left,
74
+ width
75
+ });
76
+ }
77
+ }, [setUnderlineStyle, listRef]);
78
+ wonderBlocksCore.useOnMountEffect(() => {
79
+ if (!listRef.current || !window.ResizeObserver) {
80
+ return;
81
+ }
82
+ const observer = new window.ResizeObserver(([entry]) => {
83
+ if (entry) {
84
+ updateUnderlineStyle();
85
+ if (!indicatorIsReady.current) {
86
+ indicatorIsReady.current = true;
87
+ }
88
+ }
89
+ });
90
+ observer.observe(listRef.current);
91
+ return () => {
92
+ observer.disconnect();
93
+ };
94
+ });
95
+ React__namespace.useEffect(() => {
96
+ updateUnderlineStyle();
97
+ }, [children, updateUnderlineStyle]);
50
98
  return React__namespace.createElement(StyledNav, _extends__default["default"]({
51
99
  id: id,
52
100
  "data-testid": testId,
@@ -54,14 +102,26 @@ const NavigationTabs = React__namespace.forwardRef(function NavigationTabs(props
54
102
  "aria-labelledby": ariaLabelledBy,
55
103
  ref: ref,
56
104
  style: [styles$1.nav, stylesProp == null ? void 0 : stylesProp.root]
57
- }, otherProps), React__namespace.createElement(StyledUl, {
58
- style: [styles$1.list, stylesProp == null ? void 0 : stylesProp.list]
59
- }, children));
105
+ }, otherProps), React__namespace.createElement(StyledDiv, {
106
+ style: styles$1.contents
107
+ }, React__namespace.createElement(StyledUl, {
108
+ style: [styles$1.list, stylesProp == null ? void 0 : stylesProp.list],
109
+ ref: listRef
110
+ }, children), indicatorIsReady.current && React__namespace.createElement(wonderBlocksCore.View, {
111
+ style: [{
112
+ transform: `translateX(${underlineStyle.left}px)`,
113
+ width: `${underlineStyle.width}px`
114
+ }, styles$1.currentUnderline, animated && styles$1.underlineTransition],
115
+ role: "presentation"
116
+ })));
60
117
  });
61
118
  const styles$1 = aphrodite.StyleSheet.create({
62
119
  nav: {
63
120
  overflowX: "auto"
64
121
  },
122
+ contents: {
123
+ position: "relative"
124
+ },
65
125
  list: {
66
126
  paddingInline: wonderBlocksTokens.sizing.size_050,
67
127
  paddingBlock: wonderBlocksTokens.sizing.size_0,
@@ -69,6 +129,16 @@ const styles$1 = aphrodite.StyleSheet.create({
69
129
  display: "flex",
70
130
  gap: wonderBlocksTokens.sizing.size_200,
71
131
  flexWrap: "nowrap"
132
+ },
133
+ currentUnderline: {
134
+ position: "absolute",
135
+ bottom: 0,
136
+ left: 0,
137
+ height: wonderBlocksTokens.sizing.size_050,
138
+ backgroundColor: wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground
139
+ },
140
+ underlineTransition: {
141
+ transition: "transform 0.3s ease, width 0.3s ease"
72
142
  }
73
143
  });
74
144
 
@@ -118,9 +188,8 @@ const styles = aphrodite.StyleSheet.create({
118
188
  }
119
189
  },
120
190
  current: {
121
- boxShadow: `inset 0 -${wonderBlocksTokens.sizing.size_050} 0 0 ${wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground}`,
122
191
  [":has(a:hover)"]: {
123
- boxShadow: `inset 0 -${wonderBlocksTokens.sizing.size_050} 0 0 ${wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground}`
192
+ boxShadow: "none"
124
193
  }
125
194
  },
126
195
  currentLink: {
@@ -136,17 +205,20 @@ const styles = aphrodite.StyleSheet.create({
136
205
  textDecoration: "none",
137
206
  ":hover": {
138
207
  textDecoration: "none",
208
+ border: "none",
139
209
  outline: "none",
140
210
  color: wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground,
141
211
  backgroundColor: "transparent"
142
212
  },
143
213
  ":active": {
144
214
  textDecoration: "none",
215
+ border: "none",
145
216
  outline: "none",
146
217
  color: wonderBlocksTokens.semanticColor.action.secondary.progressive.press.foreground
147
218
  },
148
219
  ":focus-visible": {
149
220
  color: wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground,
221
+ border: "none",
150
222
  outline: "none",
151
223
  boxShadow: `0 0 0 ${wonderBlocksTokens.sizing.size_025} ${wonderBlocksTokens.semanticColor.focus.inner}, 0 0 0 ${wonderBlocksTokens.sizing.size_050} ${wonderBlocksTokens.semanticColor.focus.outer}`,
152
224
  borderRadius: 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-tabs",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "design": "v1",
5
5
  "description": "Tabs are used to control what content is shown",
6
6
  "main": "dist/index.js",