@instructure/ui-tabs 11.6.0 → 11.6.1-snapshot-129

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 (134) hide show
  1. package/CHANGELOG.md +48 -308
  2. package/es/Tabs/{Panel → v1/Panel}/index.js +2 -2
  3. package/es/Tabs/{Tab → v1/Tab}/index.js +2 -2
  4. package/es/Tabs/{index.js → v1/index.js} +2 -2
  5. package/es/Tabs/v2/Panel/index.js +126 -0
  6. package/es/Tabs/v2/Panel/props.js +26 -0
  7. package/es/Tabs/v2/Panel/styles.js +81 -0
  8. package/es/Tabs/v2/Tab/index.js +118 -0
  9. package/es/Tabs/v2/Tab/props.js +26 -0
  10. package/es/Tabs/v2/Tab/styles.js +145 -0
  11. package/es/Tabs/v2/index.js +396 -0
  12. package/es/Tabs/v2/props.js +26 -0
  13. package/es/Tabs/v2/styles.js +145 -0
  14. package/es/{index.js → exports/a.js} +3 -3
  15. package/es/exports/b.js +26 -0
  16. package/lib/Tabs/v1/Panel/index.js +132 -0
  17. package/lib/Tabs/v1/Tab/index.js +125 -0
  18. package/lib/Tabs/v1/index.js +410 -0
  19. package/lib/Tabs/{Panel → v2/Panel}/index.js +3 -4
  20. package/lib/Tabs/v2/Panel/props.js +31 -0
  21. package/lib/Tabs/v2/Panel/styles.js +87 -0
  22. package/lib/Tabs/{Tab → v2/Tab}/index.js +3 -4
  23. package/lib/Tabs/v2/Tab/props.js +31 -0
  24. package/lib/Tabs/v2/Tab/styles.js +151 -0
  25. package/lib/Tabs/{index.js → v2/index.js} +5 -6
  26. package/lib/Tabs/v2/props.js +31 -0
  27. package/lib/Tabs/v2/styles.js +151 -0
  28. package/lib/{index.js → exports/a.js} +4 -4
  29. package/lib/exports/b.js +26 -0
  30. package/package.json +46 -24
  31. package/src/Tabs/{Panel → v1/Panel}/index.tsx +2 -2
  32. package/src/Tabs/{Tab → v1/Tab}/index.tsx +3 -3
  33. package/src/Tabs/{Tab → v1/Tab}/props.ts +1 -1
  34. package/src/Tabs/{index.tsx → v1/index.tsx} +3 -3
  35. package/src/Tabs/{props.ts → v1/props.ts} +1 -1
  36. package/src/Tabs/v2/Panel/index.tsx +138 -0
  37. package/src/Tabs/v2/Panel/props.ts +100 -0
  38. package/src/Tabs/v2/Panel/styles.ts +92 -0
  39. package/src/Tabs/v2/README.md +559 -0
  40. package/src/Tabs/v2/Tab/index.tsx +123 -0
  41. package/src/Tabs/v2/Tab/props.ts +80 -0
  42. package/src/Tabs/v2/Tab/styles.ts +161 -0
  43. package/src/Tabs/v2/index.tsx +547 -0
  44. package/src/Tabs/v2/props.ts +126 -0
  45. package/src/Tabs/v2/styles.ts +156 -0
  46. package/src/{index.ts → exports/a.ts} +6 -6
  47. package/src/exports/b.ts +31 -0
  48. package/tsconfig.build.tsbuildinfo +1 -1
  49. package/types/Tabs/v1/Panel/index.d.ts.map +1 -0
  50. package/types/Tabs/v1/Panel/props.d.ts.map +1 -0
  51. package/types/Tabs/v1/Panel/styles.d.ts.map +1 -0
  52. package/types/Tabs/v1/Panel/theme.d.ts.map +1 -0
  53. package/types/Tabs/{Tab → v1/Tab}/index.d.ts +1 -1
  54. package/types/Tabs/v1/Tab/index.d.ts.map +1 -0
  55. package/types/Tabs/{Tab → v1/Tab}/props.d.ts +1 -1
  56. package/types/Tabs/v1/Tab/props.d.ts.map +1 -0
  57. package/types/Tabs/v1/Tab/styles.d.ts.map +1 -0
  58. package/types/Tabs/v1/Tab/theme.d.ts.map +1 -0
  59. package/types/Tabs/{index.d.ts → v1/index.d.ts} +1 -1
  60. package/types/Tabs/v1/index.d.ts.map +1 -0
  61. package/types/Tabs/{props.d.ts → v1/props.d.ts} +1 -1
  62. package/types/Tabs/v1/props.d.ts.map +1 -0
  63. package/types/Tabs/v1/styles.d.ts.map +1 -0
  64. package/types/Tabs/v1/theme.d.ts.map +1 -0
  65. package/types/Tabs/v2/Panel/index.d.ts +46 -0
  66. package/types/Tabs/v2/Panel/index.d.ts.map +1 -0
  67. package/types/Tabs/v2/Panel/props.d.ts +46 -0
  68. package/types/Tabs/v2/Panel/props.d.ts.map +1 -0
  69. package/types/Tabs/v2/Panel/styles.d.ts +19 -0
  70. package/types/Tabs/v2/Panel/styles.d.ts.map +1 -0
  71. package/types/Tabs/v2/Tab/index.d.ts +43 -0
  72. package/types/Tabs/v2/Tab/index.d.ts.map +1 -0
  73. package/types/Tabs/v2/Tab/props.d.ts +33 -0
  74. package/types/Tabs/v2/Tab/props.d.ts.map +1 -0
  75. package/types/Tabs/v2/Tab/styles.d.ts +20 -0
  76. package/types/Tabs/v2/Tab/styles.d.ts.map +1 -0
  77. package/types/Tabs/v2/index.d.ts +80 -0
  78. package/types/Tabs/v2/index.d.ts.map +1 -0
  79. package/types/Tabs/v2/props.d.ts +68 -0
  80. package/types/Tabs/v2/props.d.ts.map +1 -0
  81. package/types/Tabs/v2/styles.d.ts +19 -0
  82. package/types/Tabs/v2/styles.d.ts.map +1 -0
  83. package/types/exports/a.d.ts +7 -0
  84. package/types/exports/a.d.ts.map +1 -0
  85. package/types/exports/b.d.ts +7 -0
  86. package/types/exports/b.d.ts.map +1 -0
  87. package/types/Tabs/Panel/index.d.ts.map +0 -1
  88. package/types/Tabs/Panel/props.d.ts.map +0 -1
  89. package/types/Tabs/Panel/styles.d.ts.map +0 -1
  90. package/types/Tabs/Panel/theme.d.ts.map +0 -1
  91. package/types/Tabs/Tab/index.d.ts.map +0 -1
  92. package/types/Tabs/Tab/props.d.ts.map +0 -1
  93. package/types/Tabs/Tab/styles.d.ts.map +0 -1
  94. package/types/Tabs/Tab/theme.d.ts.map +0 -1
  95. package/types/Tabs/index.d.ts.map +0 -1
  96. package/types/Tabs/props.d.ts.map +0 -1
  97. package/types/Tabs/styles.d.ts.map +0 -1
  98. package/types/Tabs/theme.d.ts.map +0 -1
  99. package/types/index.d.ts +0 -7
  100. package/types/index.d.ts.map +0 -1
  101. /package/es/Tabs/{Panel → v1/Panel}/props.js +0 -0
  102. /package/es/Tabs/{Panel → v1/Panel}/styles.js +0 -0
  103. /package/es/Tabs/{Panel → v1/Panel}/theme.js +0 -0
  104. /package/es/Tabs/{Tab → v1/Tab}/props.js +0 -0
  105. /package/es/Tabs/{Tab → v1/Tab}/styles.js +0 -0
  106. /package/es/Tabs/{Tab → v1/Tab}/theme.js +0 -0
  107. /package/es/Tabs/{props.js → v1/props.js} +0 -0
  108. /package/es/Tabs/{styles.js → v1/styles.js} +0 -0
  109. /package/es/Tabs/{theme.js → v1/theme.js} +0 -0
  110. /package/lib/Tabs/{Panel → v1/Panel}/props.js +0 -0
  111. /package/lib/Tabs/{Panel → v1/Panel}/styles.js +0 -0
  112. /package/lib/Tabs/{Panel → v1/Panel}/theme.js +0 -0
  113. /package/lib/Tabs/{Tab → v1/Tab}/props.js +0 -0
  114. /package/lib/Tabs/{Tab → v1/Tab}/styles.js +0 -0
  115. /package/lib/Tabs/{Tab → v1/Tab}/theme.js +0 -0
  116. /package/lib/Tabs/{props.js → v1/props.js} +0 -0
  117. /package/lib/Tabs/{styles.js → v1/styles.js} +0 -0
  118. /package/lib/Tabs/{theme.js → v1/theme.js} +0 -0
  119. /package/src/Tabs/{Panel → v1/Panel}/props.ts +0 -0
  120. /package/src/Tabs/{Panel → v1/Panel}/styles.ts +0 -0
  121. /package/src/Tabs/{Panel → v1/Panel}/theme.ts +0 -0
  122. /package/src/Tabs/{README.md → v1/README.md} +0 -0
  123. /package/src/Tabs/{Tab → v1/Tab}/styles.ts +0 -0
  124. /package/src/Tabs/{Tab → v1/Tab}/theme.ts +0 -0
  125. /package/src/Tabs/{styles.ts → v1/styles.ts} +0 -0
  126. /package/src/Tabs/{theme.ts → v1/theme.ts} +0 -0
  127. /package/types/Tabs/{Panel → v1/Panel}/index.d.ts +0 -0
  128. /package/types/Tabs/{Panel → v1/Panel}/props.d.ts +0 -0
  129. /package/types/Tabs/{Panel → v1/Panel}/styles.d.ts +0 -0
  130. /package/types/Tabs/{Panel → v1/Panel}/theme.d.ts +0 -0
  131. /package/types/Tabs/{Tab → v1/Tab}/styles.d.ts +0 -0
  132. /package/types/Tabs/{Tab → v1/Tab}/theme.d.ts +0 -0
  133. /package/types/Tabs/{styles.d.ts → v1/styles.d.ts} +0 -0
  134. /package/types/Tabs/{theme.d.ts → v1/theme.d.ts} +0 -0
@@ -0,0 +1,547 @@
1
+ /*
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2015 - present Instructure, Inc.
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import {
26
+ cloneElement,
27
+ Children,
28
+ Component,
29
+ ComponentElement,
30
+ ReactElement
31
+ } from 'react'
32
+
33
+ import keycode from 'keycode'
34
+
35
+ import { View } from '@instructure/ui-view/latest'
36
+ import type { ViewOwnProps } from '@instructure/ui-view/latest'
37
+ import {
38
+ matchComponentTypes,
39
+ safeCloneElement,
40
+ passthroughProps
41
+ } from '@instructure/ui-react-utils'
42
+ import { logError as error } from '@instructure/console'
43
+ import { uid } from '@instructure/uid'
44
+ import { Focusable } from '@instructure/ui-focusable'
45
+ import { getBoundingClientRect } from '@instructure/ui-dom-utils'
46
+ import type { RectType } from '@instructure/ui-dom-utils'
47
+ import { debounce } from '@instructure/debounce'
48
+ import type { Debounced } from '@instructure/debounce'
49
+ import { px } from '@instructure/ui-utils'
50
+
51
+ import { withStyle } from '@instructure/emotion'
52
+
53
+ import generateStyle from './styles'
54
+
55
+ import { Tab } from './Tab'
56
+ import { Panel } from './Panel'
57
+
58
+ import { allowedProps } from './props'
59
+ import type { TabsProps, TabsState } from './props'
60
+
61
+ import type { TabsTabProps } from './Tab/props'
62
+ import type { TabsPanelProps } from './Panel/props'
63
+
64
+ type TabChild = ComponentElement<TabsTabProps, any>
65
+ type PanelChild = ComponentElement<TabsPanelProps, Panel>
66
+
67
+ /**
68
+ ---
69
+ category: components
70
+ ---
71
+ **/
72
+ @withStyle(generateStyle)
73
+ class Tabs extends Component<TabsProps, TabsState> {
74
+ static readonly componentId = 'Tabs'
75
+
76
+ static allowedProps = allowedProps
77
+
78
+ static defaultProps = {
79
+ variant: 'default',
80
+ shouldFocusOnRender: false,
81
+ tabOverflow: 'stack'
82
+ }
83
+
84
+ static Panel = Panel
85
+ static Tab = Tab
86
+
87
+ private _tabList: Element | null = null
88
+ private _focusable: Focusable | null = null
89
+ private _tabListPosition?: RectType
90
+ private _debounced?: Debounced<typeof this.handleResize>
91
+ private _resizeListener?: ResizeObserver
92
+
93
+ ref: Element | null = null
94
+
95
+ handleRef = (el: Element | null) => {
96
+ const { elementRef } = this.props
97
+
98
+ this.ref = el
99
+
100
+ if (typeof elementRef === 'function') {
101
+ elementRef(el)
102
+ }
103
+ }
104
+
105
+ constructor(props: TabsProps) {
106
+ super(props)
107
+
108
+ this.state = {
109
+ withTabListOverflow: false,
110
+ showStartOverLay: false,
111
+ showEndOverLay: false
112
+ }
113
+ }
114
+
115
+ componentDidMount() {
116
+ if (this.props.tabOverflow === 'scroll' && this._tabList) {
117
+ this.startScrollOverflow()
118
+ }
119
+
120
+ if (this.props.shouldFocusOnRender) {
121
+ this.focus()
122
+ }
123
+
124
+ this.props.makeStyles?.()
125
+ }
126
+
127
+ componentDidUpdate(prevProps: TabsProps, prevState: TabsState) {
128
+ if (this.props.shouldFocusOnRender && !prevProps.shouldFocusOnRender) {
129
+ this.focus()
130
+ }
131
+
132
+ // start event listeners for scroll overflow
133
+ if (
134
+ prevProps.tabOverflow === 'stack' &&
135
+ this.props.tabOverflow === 'scroll'
136
+ ) {
137
+ this.startScrollOverflow()
138
+ }
139
+
140
+ // cancel event listeners for scroll overflow
141
+ if (
142
+ prevProps.tabOverflow === 'scroll' &&
143
+ this.props.tabOverflow === 'stack'
144
+ ) {
145
+ this.cancelScrollOverflow()
146
+ }
147
+
148
+ // we need to recalculate the scroll overflow if the style changes
149
+ if (
150
+ this.props.tabOverflow === 'scroll' &&
151
+ prevProps.styles !== this.props.styles
152
+ ) {
153
+ this.handleResize()
154
+ }
155
+
156
+ // when tabList is set as overflown,
157
+ // make sure active tab is always visible
158
+ if (
159
+ this.props.tabOverflow === 'scroll' &&
160
+ this._tabList &&
161
+ !prevState.withTabListOverflow &&
162
+ this.state.withTabListOverflow
163
+ ) {
164
+ const activeTabEl = this._tabList.querySelector('[aria-selected="true"]')
165
+ this.showActiveTabIfOverlayed(activeTabEl)
166
+ }
167
+
168
+ this.props.makeStyles?.()
169
+ }
170
+
171
+ componentWillUnmount() {
172
+ this.cancelScrollOverflow()
173
+ }
174
+
175
+ startScrollOverflow() {
176
+ this.handleResize()
177
+
178
+ this._debounced = debounce(this.handleResize, 300, {
179
+ leading: true,
180
+ trailing: true
181
+ })
182
+ this._tabListPosition = getBoundingClientRect(this._tabList)
183
+ this._resizeListener = new ResizeObserver((entries) => {
184
+ for (const entry of entries) {
185
+ const { width: newWidth } = entry.contentRect
186
+
187
+ if (this._tabListPosition!.width !== newWidth) {
188
+ this._debounced?.()
189
+ }
190
+ }
191
+ })
192
+
193
+ this._resizeListener.observe(this._tabList!)
194
+ }
195
+
196
+ cancelScrollOverflow() {
197
+ if (this._resizeListener) {
198
+ this._resizeListener.disconnect()
199
+ }
200
+
201
+ if (this._debounced) {
202
+ this._debounced.cancel()
203
+ }
204
+ }
205
+
206
+ getOverlayWidth() {
207
+ const { variant, tabOverflow, styles } = this.props
208
+
209
+ if (styles && tabOverflow === 'scroll') {
210
+ if (variant === 'default') {
211
+ return px(styles?.scrollOverlayWidthDefault)
212
+ } else {
213
+ return px(styles?.scrollOverlayWidthSecondary)
214
+ }
215
+ }
216
+
217
+ return 0
218
+ }
219
+
220
+ showActiveTabIfOverlayed(activeTabEl: Element | null) {
221
+ if (
222
+ this._tabList &&
223
+ this._tabListPosition &&
224
+ typeof this._tabList.scrollTo === 'function' // test for scrollTo support
225
+ ) {
226
+ const tabPosition = getBoundingClientRect(activeTabEl)
227
+ const tabListPosition = this._tabListPosition
228
+
229
+ const tabListBoundStart = tabListPosition.left + this.getOverlayWidth()
230
+ const tabListBoundEnd = tabListPosition.right + this.getOverlayWidth()
231
+
232
+ const tabPositionStart = tabPosition.left
233
+ const tabPositionEnd = tabPosition.right
234
+
235
+ if (tabListBoundEnd > tabPositionEnd) {
236
+ const offset = Math.round(tabListBoundEnd - tabPositionEnd)
237
+ this._tabList.scrollTo({
238
+ top: 0,
239
+ left: this._tabList.scrollLeft + offset,
240
+ behavior: 'smooth'
241
+ })
242
+ } else if (tabListBoundStart > tabPositionStart) {
243
+ const offset = Math.round(tabListBoundStart - tabPositionStart)
244
+ this._tabList.scrollTo({
245
+ top: 0,
246
+ left: this._tabList.scrollLeft - offset,
247
+ behavior: 'smooth'
248
+ })
249
+ }
250
+ }
251
+ }
252
+
253
+ handleTabClick: TabsTabProps['onClick'] = (event, { index }) => {
254
+ const nextTab = this.getNextTab(index, 0)
255
+ this.fireOnChange(event, nextTab)
256
+ }
257
+
258
+ handleTabKeyDown: TabsTabProps['onKeyDown'] = (event, { index }) => {
259
+ let nextTab
260
+
261
+ if (
262
+ event.keyCode === keycode.codes.up ||
263
+ event.keyCode === keycode.codes.left
264
+ ) {
265
+ // Select next tab to the left
266
+ nextTab = this.getNextTab(index, -1)
267
+ } else if (
268
+ event.keyCode === keycode.codes.down ||
269
+ event.keyCode === keycode.codes.right
270
+ ) {
271
+ // Select next tab to the right
272
+ nextTab = this.getNextTab(index, 1)
273
+ }
274
+ if (nextTab) {
275
+ event.preventDefault()
276
+ this.fireOnChange(event, nextTab)
277
+ }
278
+ }
279
+
280
+ handleResize = () => {
281
+ this.setState({
282
+ withTabListOverflow:
283
+ this._tabList!.scrollWidth > (this._tabList as HTMLElement).offsetWidth
284
+ })
285
+
286
+ this._tabListPosition = getBoundingClientRect(this._tabList)
287
+ }
288
+
289
+ getNextTab(
290
+ startIndex: number,
291
+ step: -1 | 0 | 1
292
+ ): { index: number; id?: string } {
293
+ const tabs = Children.toArray(this.props.children).map(
294
+ (child) => matchComponentTypes<PanelChild>(child, [Panel]) && child
295
+ ) as PanelChild[]
296
+ const count = tabs.length
297
+ const change = step < 0 ? step + count : step
298
+
299
+ error(
300
+ startIndex >= 0 && startIndex < count,
301
+ `[Tabs] Invalid tab index: '${startIndex}'.`
302
+ )
303
+
304
+ let nextIndex = startIndex
305
+ let nextTab: PanelChild
306
+
307
+ do {
308
+ nextIndex = (nextIndex + change) % count
309
+ nextTab = tabs[nextIndex]
310
+ } while (nextTab && nextTab.props && nextTab.props.isDisabled)
311
+
312
+ error(
313
+ nextIndex >= 0 && nextIndex < count,
314
+ `[Tabs] Invalid tab index: '${nextIndex}'.`
315
+ )
316
+ return { index: nextIndex, id: nextTab.props.id }
317
+ }
318
+
319
+ fireOnChange(
320
+ event: React.MouseEvent<ViewOwnProps> | React.KeyboardEvent<ViewOwnProps>,
321
+ { index, id }: { index: number; id?: string }
322
+ ) {
323
+ if (typeof this.props.onRequestTabChange === 'function') {
324
+ this.props.onRequestTabChange(event, { index, id })
325
+ }
326
+
327
+ // this is needed because keypress cancels scrolling. So we have to trigger the scrolling
328
+ // one "tick" later than the keypress
329
+ setTimeout(() => {
330
+ if (this.state.withTabListOverflow) {
331
+ const tab = id
332
+ ? this._tabList!.querySelector(`#tab-${CSS.escape(id)}`)
333
+ : null
334
+ this.showActiveTabIfOverlayed(tab)
335
+ }
336
+ }, 0)
337
+ }
338
+
339
+ createTab(
340
+ index: number,
341
+ generatedId: string,
342
+ selected: boolean,
343
+ panel: PanelChild
344
+ ): TabChild {
345
+ const id = panel.props.id || generatedId
346
+
347
+ return (
348
+ <Tab
349
+ variant={this.props.variant}
350
+ key={`tab-${index}`}
351
+ id={`tab-${id}`}
352
+ controls={panel.props.id || `panel-${id}`}
353
+ index={index}
354
+ isSelected={selected}
355
+ isDisabled={panel.props.isDisabled}
356
+ onClick={this.handleTabClick}
357
+ onKeyDown={this.handleTabKeyDown}
358
+ isOverflowScroll={this.props.tabOverflow === 'scroll'}
359
+ >
360
+ {panel.props.renderTitle}
361
+ </Tab>
362
+ )
363
+ }
364
+
365
+ clonePanel(
366
+ index: number,
367
+ generatedId: string,
368
+ selected: boolean,
369
+ panel: PanelChild,
370
+ activePanel?: PanelChild
371
+ ) {
372
+ const id = panel.props.id || generatedId
373
+
374
+ // fixHeight can be 0, so simply `fixheight` could return falsy value
375
+ const hasFixedHeight = typeof this.props.fixHeight !== 'undefined'
376
+
377
+ const commonProps = {
378
+ id: panel.props.id || `panel-${id}`,
379
+ labelledBy: `tab-${id}`,
380
+ isSelected: selected,
381
+ variant: this.props.variant,
382
+ maxHeight: !hasFixedHeight ? this.props.maxHeight : undefined,
383
+ minHeight: !hasFixedHeight ? this.props.minHeight : '100%'
384
+ }
385
+
386
+ let activePanelClone = null
387
+ if (activePanel !== undefined) {
388
+ // cloning active panel with a proper custom key as a workaround because
389
+ // safeCloneElement overwrites it with the key from the original element
390
+ activePanelClone = cloneElement(activePanel as ReactElement<any>, {
391
+ key: `panel-${index}`
392
+ })
393
+
394
+ return safeCloneElement(activePanelClone, {
395
+ padding: activePanelClone.props.padding || this.props.padding,
396
+ textAlign: activePanelClone.props.textAlign || this.props.textAlign,
397
+ ...commonProps
398
+ } as TabsPanelProps & { key: string }) as PanelChild
399
+ } else {
400
+ return safeCloneElement(panel, {
401
+ key: `panel-${index}`,
402
+ padding: panel.props.padding || this.props.padding,
403
+ textAlign: panel.props.textAlign || this.props.textAlign,
404
+ ...commonProps
405
+ } as TabsPanelProps & { key: string }) as PanelChild
406
+ }
407
+ }
408
+
409
+ handleFocusableRef = (el: Focusable | null) => {
410
+ this._focusable = el
411
+ }
412
+
413
+ handleTabListRef = (el: Element | null) => {
414
+ this._tabList = el
415
+ }
416
+
417
+ focus() {
418
+ this._focusable &&
419
+ typeof this._focusable.focus === 'function' &&
420
+ this._focusable.focus()
421
+ }
422
+
423
+ handleScroll = (
424
+ event: React.UIEvent<ViewOwnProps> & React.UIEvent<HTMLElement>
425
+ ) => {
426
+ if (
427
+ this.props.tabOverflow !== 'scroll' ||
428
+ !this.state.withTabListOverflow
429
+ ) {
430
+ event.preventDefault()
431
+ return
432
+ }
433
+ const tabList = event.currentTarget as HTMLElement
434
+ const scrollLeftMax = Math.round(
435
+ tabList.scrollWidth - getBoundingClientRect(tabList).width
436
+ )
437
+
438
+ const scrollLeft = Math.floor(Math.abs(tabList.scrollLeft))
439
+ this.setState({
440
+ showStartOverLay: scrollLeft > 0,
441
+ showEndOverLay: scrollLeft < scrollLeftMax
442
+ })
443
+ }
444
+ render() {
445
+ const panels: PanelChild[] = []
446
+ const tabs: TabChild[] = []
447
+ const {
448
+ children,
449
+ elementRef,
450
+ maxWidth,
451
+ variant,
452
+ margin,
453
+ screenReaderLabel,
454
+ onRequestTabChange,
455
+ tabOverflow,
456
+ styles,
457
+ ...props
458
+ } = this.props
459
+
460
+ const activePanels = (Children.toArray(children) as PanelChild[])
461
+ .filter((child) => matchComponentTypes<PanelChild>(child, [Panel]))
462
+ .filter((child) => child.props.active)
463
+
464
+ if (activePanels.length > 1) {
465
+ error(false, `[Tabs] Only one Panel can be marked as active.`)
466
+ }
467
+
468
+ const selectedChildIndex = (Children.toArray(children) as PanelChild[])
469
+ .filter((child) => matchComponentTypes<PanelChild>(child, [Panel]))
470
+ .findIndex((child) => child.props.isSelected && !child.props.isDisabled)
471
+
472
+ const selectedIndex = selectedChildIndex >= 0 ? selectedChildIndex : 0
473
+ Children.toArray(children).map((child, index) => {
474
+ if (matchComponentTypes<PanelChild>(child, [Panel])) {
475
+ const selected =
476
+ !child.props.isDisabled &&
477
+ (child.props.isSelected || selectedIndex === index)
478
+ const id = uid()
479
+
480
+ tabs.push(this.createTab(index, id, selected, child))
481
+ if (activePanels.length === 1) {
482
+ panels.push(
483
+ this.clonePanel(index, id, selected, child, activePanels[0])
484
+ )
485
+ } else {
486
+ panels.push(this.clonePanel(index, id, selected, child))
487
+ }
488
+ } else {
489
+ panels.push(child as PanelChild)
490
+ }
491
+ })
492
+
493
+ const withScrollFade =
494
+ tabOverflow === 'scroll' && this.state.withTabListOverflow
495
+
496
+ // suppress overlay whenever final Tab is active, or Firefox will cover it
497
+ const startScrollOverlay = this.state.showStartOverLay ? (
498
+ <span key="start-overlay" css={styles?.startScrollOverlay} />
499
+ ) : null
500
+
501
+ const endScrollOverlay = this.state.showEndOverLay ? (
502
+ <span key="end-overlay" css={styles?.endScrollOverlay} />
503
+ ) : null
504
+
505
+ return (
506
+ <View
507
+ {...passthroughProps(props)}
508
+ elementRef={this.handleRef}
509
+ maxWidth={maxWidth}
510
+ margin={margin}
511
+ as="div"
512
+ css={styles?.container}
513
+ data-cid="Tabs"
514
+ >
515
+ <Focusable ref={this.handleFocusableRef}>
516
+ {() => (
517
+ <View
518
+ as="div"
519
+ position="relative"
520
+ borderRadius="medium"
521
+ shouldAnimateFocus={false}
522
+ css={styles?.tabs}
523
+ >
524
+ <View
525
+ as="div"
526
+ role="tablist"
527
+ css={styles?.tabList}
528
+ aria-label={screenReaderLabel}
529
+ elementRef={this.handleTabListRef}
530
+ onScroll={this.handleScroll}
531
+ >
532
+ {tabs}
533
+ {withScrollFade && startScrollOverlay}
534
+ {withScrollFade && endScrollOverlay}
535
+ </View>
536
+ </View>
537
+ )}
538
+ </Focusable>
539
+
540
+ <div css={styles?.panelsContainer}>{panels}</div>
541
+ </View>
542
+ )
543
+ }
544
+ }
545
+
546
+ export default Tabs
547
+ export { Tabs, Panel }
@@ -0,0 +1,126 @@
1
+ /*
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2015 - present Instructure, Inc.
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+ import React from 'react'
25
+ import type {
26
+ Spacing,
27
+ WithStyleProps,
28
+ ComponentStyle
29
+ } from '@instructure/emotion'
30
+ import type { OtherHTMLAttributes, TabsTheme } from '@instructure/shared-types'
31
+ import type { TextDirectionContextConsumerProps } from '@instructure/ui-i18n'
32
+ import type { ViewOwnProps } from '@instructure/ui-view/latest'
33
+
34
+ type TabsOwnProps = {
35
+ /**
36
+ * children of type `Tabs.Panel`
37
+ */
38
+ children?: React.ReactNode // TODO: oneOf([Panel, null])
39
+ variant?: 'default' | 'secondary'
40
+ /**
41
+ * A screen ready only label for the list of tabs
42
+ */
43
+ screenReaderLabel?: string
44
+ /**
45
+ * Called when the selected tab should change.
46
+ * @param tabData.index - The zero-based index of the tab that was selected
47
+ * @param tabData.id - The HTML `id` of the tab that was selected
48
+ */
49
+ onRequestTabChange?: (
50
+ event: React.MouseEvent<ViewOwnProps> | React.KeyboardEvent<ViewOwnProps>,
51
+ tabData: { index: number; id?: string }
52
+ ) => void
53
+ maxWidth?: string | number
54
+ maxHeight?: string | number
55
+ minHeight?: string | number
56
+ fixHeight?: string | number
57
+ /**
58
+ * Valid values are `0`, `none`, `auto`, `xxx-small`, `xx-small`, `x-small`,
59
+ * `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via
60
+ * familiar CSS-like shorthand. For example: `margin="small auto large"`.
61
+ */
62
+ margin?: Spacing
63
+ /**
64
+ * Valid values are `0`, `none`, `xxx-small`, `xx-small`, `x-small`,
65
+ * `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via
66
+ * familiar CSS-like shorthand. For example: `padding="small x-large large"`.
67
+ */
68
+ padding?: Spacing
69
+ textAlign?: 'start' | 'center' | 'end'
70
+ /**
71
+ * provides a reference to the underlying html root element
72
+ */
73
+ elementRef?: (element: Element | null) => void
74
+ /**
75
+ * Choose whether Tabs should stack or scroll when they exceed the width of their
76
+ * container.
77
+ */
78
+ tabOverflow?: 'stack' | 'scroll'
79
+ shouldFocusOnRender?: boolean
80
+ }
81
+
82
+ type PropKeys = keyof TabsOwnProps
83
+
84
+ type AllowedPropKeys = Readonly<Array<PropKeys>>
85
+
86
+ type TabsProps = TabsOwnProps &
87
+ TextDirectionContextConsumerProps &
88
+ WithStyleProps<TabsTheme, TabsStyle> &
89
+ OtherHTMLAttributes<TabsOwnProps>
90
+
91
+ type TabsStyle = ComponentStyle<
92
+ | 'tabs'
93
+ | 'container'
94
+ | 'tabList'
95
+ | 'panelsContainer'
96
+ | 'startScrollOverlay'
97
+ | 'endScrollOverlay'
98
+ > & {
99
+ scrollOverlayWidthDefault: string
100
+ scrollOverlayWidthSecondary: string
101
+ }
102
+
103
+ type TabsState = {
104
+ withTabListOverflow: boolean
105
+ showStartOverLay: boolean
106
+ showEndOverLay: boolean
107
+ }
108
+ const allowedProps: AllowedPropKeys = [
109
+ 'children',
110
+ 'variant',
111
+ 'screenReaderLabel',
112
+ 'onRequestTabChange',
113
+ 'maxWidth',
114
+ 'maxHeight',
115
+ 'minHeight',
116
+ 'fixHeight',
117
+ 'margin',
118
+ 'padding',
119
+ 'textAlign',
120
+ 'elementRef',
121
+ 'tabOverflow',
122
+ 'shouldFocusOnRender'
123
+ ]
124
+
125
+ export type { TabsProps, TabsState, TabsStyle }
126
+ export { allowedProps }