@instructure/ui-drilldown 8.21.1-snapshot.10
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 +36 -0
- package/LICENSE.md +27 -0
- package/README.md +30 -0
- package/es/Drilldown/DrilldownGroup/index.js +56 -0
- package/es/Drilldown/DrilldownGroup/props.js +42 -0
- package/es/Drilldown/DrilldownLocator.js +144 -0
- package/es/Drilldown/DrilldownOption/index.js +59 -0
- package/es/Drilldown/DrilldownOption/props.js +46 -0
- package/es/Drilldown/DrilldownPage/index.js +56 -0
- package/es/Drilldown/DrilldownPage/props.js +41 -0
- package/es/Drilldown/DrilldownSeparator/index.js +52 -0
- package/es/Drilldown/DrilldownSeparator/props.js +30 -0
- package/es/Drilldown/__examples__/Drilldown.examples.js +299 -0
- package/es/Drilldown/index.js +1241 -0
- package/es/Drilldown/locator.js +26 -0
- package/es/Drilldown/props.js +67 -0
- package/es/Drilldown/styles.js +82 -0
- package/es/Drilldown/theme.js +43 -0
- package/es/index.js +24 -0
- package/lib/Drilldown/DrilldownGroup/index.js +42 -0
- package/lib/Drilldown/DrilldownGroup/props.js +56 -0
- package/lib/Drilldown/DrilldownLocator.js +157 -0
- package/lib/Drilldown/DrilldownOption/index.js +45 -0
- package/lib/Drilldown/DrilldownOption/props.js +57 -0
- package/lib/Drilldown/DrilldownPage/index.js +66 -0
- package/lib/Drilldown/DrilldownPage/props.js +56 -0
- package/lib/Drilldown/DrilldownSeparator/index.js +38 -0
- package/lib/Drilldown/DrilldownSeparator/props.js +41 -0
- package/lib/Drilldown/__examples__/Drilldown.examples.js +289 -0
- package/lib/Drilldown/index.js +1253 -0
- package/lib/Drilldown/locator.js +40 -0
- package/lib/Drilldown/props.js +83 -0
- package/lib/Drilldown/styles.js +90 -0
- package/lib/Drilldown/theme.js +51 -0
- package/lib/index.js +13 -0
- package/lib/package.json +1 -0
- package/package.json +58 -0
- package/src/Drilldown/DrilldownGroup/index.tsx +64 -0
- package/src/Drilldown/DrilldownGroup/props.ts +149 -0
- package/src/Drilldown/DrilldownLocator.ts +101 -0
- package/src/Drilldown/DrilldownOption/index.tsx +68 -0
- package/src/Drilldown/DrilldownOption/props.ts +224 -0
- package/src/Drilldown/DrilldownPage/index.tsx +58 -0
- package/src/Drilldown/DrilldownPage/props.ts +129 -0
- package/src/Drilldown/DrilldownSeparator/index.tsx +60 -0
- package/src/Drilldown/DrilldownSeparator/props.ts +65 -0
- package/src/Drilldown/README.md +1515 -0
- package/src/Drilldown/__examples__/Drilldown.examples.tsx +318 -0
- package/src/Drilldown/index.tsx +1492 -0
- package/src/Drilldown/locator.ts +28 -0
- package/src/Drilldown/props.ts +378 -0
- package/src/Drilldown/styles.ts +95 -0
- package/src/Drilldown/theme.ts +47 -0
- package/src/index.ts +26 -0
- package/tsconfig.build.json +30 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +4 -0
- package/types/Drilldown/DrilldownGroup/index.d.ts +59 -0
- package/types/Drilldown/DrilldownGroup/index.d.ts.map +1 -0
- package/types/Drilldown/DrilldownGroup/props.d.ts +65 -0
- package/types/Drilldown/DrilldownGroup/props.d.ts.map +1 -0
- package/types/Drilldown/DrilldownLocator.d.ts +652 -0
- package/types/Drilldown/DrilldownLocator.d.ts.map +1 -0
- package/types/Drilldown/DrilldownOption/index.d.ts +134 -0
- package/types/Drilldown/DrilldownOption/index.d.ts.map +1 -0
- package/types/Drilldown/DrilldownOption/props.d.ts +116 -0
- package/types/Drilldown/DrilldownOption/props.d.ts.map +1 -0
- package/types/Drilldown/DrilldownPage/index.d.ts +45 -0
- package/types/Drilldown/DrilldownPage/index.d.ts.map +1 -0
- package/types/Drilldown/DrilldownPage/props.d.ts +51 -0
- package/types/Drilldown/DrilldownPage/props.d.ts.map +1 -0
- package/types/Drilldown/DrilldownSeparator/index.d.ts +27 -0
- package/types/Drilldown/DrilldownSeparator/index.d.ts.map +1 -0
- package/types/Drilldown/DrilldownSeparator/props.d.ts +18 -0
- package/types/Drilldown/DrilldownSeparator/props.d.ts.map +1 -0
- package/types/Drilldown/__examples__/Drilldown.examples.d.ts +5 -0
- package/types/Drilldown/__examples__/Drilldown.examples.d.ts.map +1 -0
- package/types/Drilldown/index.d.ts +246 -0
- package/types/Drilldown/index.d.ts.map +1 -0
- package/types/Drilldown/locator.d.ts +4 -0
- package/types/Drilldown/locator.d.ts.map +1 -0
- package/types/Drilldown/props.d.ts +189 -0
- package/types/Drilldown/props.d.ts.map +1 -0
- package/types/Drilldown/styles.d.ts +15 -0
- package/types/Drilldown/styles.d.ts.map +1 -0
- package/types/Drilldown/theme.d.ts +10 -0
- package/types/Drilldown/theme.d.ts.map +1 -0
- package/types/index.d.ts +3 -0
- package/types/index.d.ts.map +1 -0
|
@@ -0,0 +1,1492 @@
|
|
|
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
|
+
/** @jsx jsx */
|
|
26
|
+
/** @jsxFrag React.Fragment */
|
|
27
|
+
import React, { Component, ReactElement } from 'react'
|
|
28
|
+
|
|
29
|
+
import { warn, error } from '@instructure/console'
|
|
30
|
+
import { testable } from '@instructure/ui-testable'
|
|
31
|
+
import { contains, containsActiveElement } from '@instructure/ui-dom-utils'
|
|
32
|
+
import {
|
|
33
|
+
callRenderProp,
|
|
34
|
+
matchComponentTypes,
|
|
35
|
+
omitProps,
|
|
36
|
+
safeCloneElement,
|
|
37
|
+
withDeterministicId
|
|
38
|
+
} from '@instructure/ui-react-utils'
|
|
39
|
+
|
|
40
|
+
import { View } from '@instructure/ui-view'
|
|
41
|
+
import { Options } from '@instructure/ui-options'
|
|
42
|
+
import type {
|
|
43
|
+
OptionsProps,
|
|
44
|
+
OptionsItemProps,
|
|
45
|
+
OptionsSeparatorProps
|
|
46
|
+
} from '@instructure/ui-options'
|
|
47
|
+
import { Popover } from '@instructure/ui-popover'
|
|
48
|
+
import { Selectable } from '@instructure/ui-selectable'
|
|
49
|
+
import type { SelectableRender } from '@instructure/ui-selectable'
|
|
50
|
+
import {
|
|
51
|
+
IconArrowOpenStartSolid,
|
|
52
|
+
IconArrowOpenEndSolid,
|
|
53
|
+
IconCheckSolid
|
|
54
|
+
} from '@instructure/ui-icons'
|
|
55
|
+
|
|
56
|
+
import { withStyle, jsx } from '@instructure/emotion'
|
|
57
|
+
|
|
58
|
+
import { DrilldownSeparator } from './DrilldownSeparator'
|
|
59
|
+
import { DrilldownOption } from './DrilldownOption'
|
|
60
|
+
import type { DrilldownOptionValue } from './DrilldownOption/props'
|
|
61
|
+
import { DrilldownGroup } from './DrilldownGroup'
|
|
62
|
+
import type { DrilldownGroupProps, GroupChildren } from './DrilldownGroup/props'
|
|
63
|
+
import { DrilldownPage } from './DrilldownPage'
|
|
64
|
+
import type { DrilldownPageProps, PageChildren } from './DrilldownPage/props'
|
|
65
|
+
|
|
66
|
+
import generateStyle from './styles'
|
|
67
|
+
import generateComponentTheme from './theme'
|
|
68
|
+
|
|
69
|
+
import { propTypes, allowedProps } from './props'
|
|
70
|
+
|
|
71
|
+
import type {
|
|
72
|
+
DrilldownProps,
|
|
73
|
+
DrilldownStyleProps,
|
|
74
|
+
DrilldownState,
|
|
75
|
+
PageChild,
|
|
76
|
+
OptionChild,
|
|
77
|
+
GroupChild,
|
|
78
|
+
SeparatorChild
|
|
79
|
+
} from './props'
|
|
80
|
+
|
|
81
|
+
// Additional data we need to track on the Options,
|
|
82
|
+
// but shouldn't be settable via props
|
|
83
|
+
type OptionData = {
|
|
84
|
+
groupProps?: DrilldownGroupProps
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Contains the Option ComponentElement and the extra data we need track on it,
|
|
88
|
+
// but don't want to expose as props
|
|
89
|
+
type MappedOption = OptionChild & OptionData
|
|
90
|
+
|
|
91
|
+
// Contains the props object of the Page
|
|
92
|
+
// with the `children` transformed into an array
|
|
93
|
+
type MappedPage = DrilldownPageProps & {
|
|
94
|
+
children: PageChildren[]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// An object with the mapped Pages with their id as keys
|
|
98
|
+
type PageMap = Record<string, MappedPage>
|
|
99
|
+
|
|
100
|
+
// A Map width the selected options in a group, with the id as key and their value
|
|
101
|
+
type SelectedGroupOptionsMap = Map<string, DrilldownOptionValue>
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
---
|
|
105
|
+
category: components
|
|
106
|
+
experimental: true
|
|
107
|
+
---
|
|
108
|
+
@tsProps
|
|
109
|
+
**/
|
|
110
|
+
@withDeterministicId()
|
|
111
|
+
@withStyle(generateStyle, generateComponentTheme)
|
|
112
|
+
@testable()
|
|
113
|
+
class Drilldown extends Component<DrilldownProps, DrilldownState> {
|
|
114
|
+
static readonly componentId = 'Drilldown'
|
|
115
|
+
|
|
116
|
+
static propTypes = propTypes
|
|
117
|
+
static allowedProps = allowedProps
|
|
118
|
+
static defaultProps = {
|
|
119
|
+
disabled: false,
|
|
120
|
+
rotateFocus: true,
|
|
121
|
+
as: 'ul',
|
|
122
|
+
role: 'menu',
|
|
123
|
+
overflowX: 'auto',
|
|
124
|
+
overflowY: 'auto',
|
|
125
|
+
placement: 'bottom center',
|
|
126
|
+
defaultShow: false,
|
|
127
|
+
shouldHideOnSelect: true,
|
|
128
|
+
shouldContainFocus: false,
|
|
129
|
+
shouldReturnFocus: true,
|
|
130
|
+
withArrow: true,
|
|
131
|
+
offsetX: 0,
|
|
132
|
+
offsetY: 0
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
static Group = DrilldownGroup
|
|
136
|
+
static Option = DrilldownOption
|
|
137
|
+
static Page = DrilldownPage
|
|
138
|
+
static Separator = DrilldownSeparator
|
|
139
|
+
|
|
140
|
+
private _drilldownRef: HTMLDivElement | null = null
|
|
141
|
+
private _popover: Popover | null = null
|
|
142
|
+
private _trigger: (React.ReactInstance & { focus?: () => void }) | null = null
|
|
143
|
+
private _containerElement: HTMLDivElement | null = null
|
|
144
|
+
|
|
145
|
+
readonly _id: string
|
|
146
|
+
readonly _triggerId: string
|
|
147
|
+
readonly _headerBackId: string
|
|
148
|
+
readonly _headerTitleId: string
|
|
149
|
+
readonly _headerTitleLabelId: string
|
|
150
|
+
readonly _headerActionId: string
|
|
151
|
+
|
|
152
|
+
// Array of visited page ids in "breadcrumbs" fashion
|
|
153
|
+
private readonly _pageHistory: string[]
|
|
154
|
+
|
|
155
|
+
// Map of the active options on the page (includes header options too)
|
|
156
|
+
_activeOptionsMap: {
|
|
157
|
+
[optionId: string]: MappedOption
|
|
158
|
+
} = {}
|
|
159
|
+
|
|
160
|
+
// Map of the selected options in groups (grouped by group id)
|
|
161
|
+
_selectedGroupOptionsMap: {
|
|
162
|
+
[groupId: string]: SelectedGroupOptionsMap
|
|
163
|
+
} = {}
|
|
164
|
+
|
|
165
|
+
ref: HTMLDivElement | Element | null = null
|
|
166
|
+
|
|
167
|
+
handleRef = (el: HTMLDivElement | Element | null) => {
|
|
168
|
+
// ref and elementRef have to be set together for the same element
|
|
169
|
+
const { elementRef } = this.props
|
|
170
|
+
|
|
171
|
+
this.ref = el as HTMLDivElement
|
|
172
|
+
|
|
173
|
+
if (typeof elementRef === 'function') {
|
|
174
|
+
elementRef(el as HTMLDivElement)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
handleDrilldownRef = (el: Element | null) => {
|
|
179
|
+
const { drilldownRef } = this.props
|
|
180
|
+
|
|
181
|
+
this._drilldownRef = el as HTMLDivElement
|
|
182
|
+
|
|
183
|
+
if (typeof drilldownRef === 'function') {
|
|
184
|
+
drilldownRef(el as HTMLDivElement)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// setting ref for "non-popover" version, the drilldown itself
|
|
188
|
+
// (if a trigger is provided, the Popover sets the ref)
|
|
189
|
+
if (!this.props.trigger) {
|
|
190
|
+
this.handleRef(el as HTMLDivElement)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
constructor(props: DrilldownProps) {
|
|
195
|
+
super(props)
|
|
196
|
+
|
|
197
|
+
this.state = {
|
|
198
|
+
isShowingPopover: props.trigger ? !!props.show : false,
|
|
199
|
+
activePageId: props.rootPageId,
|
|
200
|
+
highlightedOptionId: undefined,
|
|
201
|
+
lastSelectedId: undefined
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this._pageHistory = [props.rootPageId]
|
|
205
|
+
|
|
206
|
+
this.setDefaultSelected()
|
|
207
|
+
|
|
208
|
+
this._id = props.id || props.deterministicId!()
|
|
209
|
+
this._triggerId = props.deterministicId!('Drilldown-Trigger')
|
|
210
|
+
this._headerBackId = props.deterministicId!('DrilldownHeader-Back')
|
|
211
|
+
this._headerTitleId = props.deterministicId!('DrilldownHeader-Title')
|
|
212
|
+
this._headerTitleLabelId = props.deterministicId!(
|
|
213
|
+
'DrilldownHeader-Title-Label'
|
|
214
|
+
)
|
|
215
|
+
this._headerActionId = props.deterministicId!('DrilldownHeader-Action')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
componentDidMount() {
|
|
219
|
+
this.props.makeStyles?.(this.makeStylesVariables)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
componentDidUpdate(_prevProps: DrilldownProps, prevState: DrilldownState) {
|
|
223
|
+
this.props.makeStyles?.(this.makeStylesVariables)
|
|
224
|
+
|
|
225
|
+
if (prevState.activePageId !== this.state.activePageId) {
|
|
226
|
+
// on page change with mouse navigation, some option can get rendered
|
|
227
|
+
// under the cursor and get focused, so we need to wait a tick to see
|
|
228
|
+
// if anything gets focused, otherwise we focus the whole drilldown
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
if (!this.focused()) {
|
|
231
|
+
this.focus()
|
|
232
|
+
}
|
|
233
|
+
}, 0)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// if the current page was removed
|
|
237
|
+
if (!this.currentPage) {
|
|
238
|
+
if (this.previousPage) {
|
|
239
|
+
this.goToPreviousPage()
|
|
240
|
+
} else {
|
|
241
|
+
this.goToPage(this.props.rootPageId)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (
|
|
246
|
+
this.state.highlightedOptionId &&
|
|
247
|
+
!this.getPageChildById(this.state.highlightedOptionId)
|
|
248
|
+
) {
|
|
249
|
+
this.setState({
|
|
250
|
+
highlightedOptionId: undefined
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
get makeStylesVariables(): DrilldownStyleProps {
|
|
256
|
+
return {
|
|
257
|
+
hasHighlightedOption: !!this.state.highlightedOptionId
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
get activeOptionIds() {
|
|
262
|
+
return Object.keys(this._activeOptionsMap)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
get activeOptions() {
|
|
266
|
+
return Object.values(this._activeOptionsMap)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
get pages(): PageChild[] {
|
|
270
|
+
const { children } = this.props
|
|
271
|
+
return React.Children.toArray(children || []) as PageChild[]
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
get pageMap(): PageMap | undefined {
|
|
275
|
+
const { children } = this.props
|
|
276
|
+
|
|
277
|
+
if (!children) {
|
|
278
|
+
return undefined
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const map: PageMap | undefined = {}
|
|
282
|
+
|
|
283
|
+
this.pages.forEach((page) => {
|
|
284
|
+
const { props } = page
|
|
285
|
+
const { children } = props
|
|
286
|
+
|
|
287
|
+
map[props.id] = {
|
|
288
|
+
...props,
|
|
289
|
+
children: React.Children.toArray(children || []) as PageChildren[]
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
return map
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
get isOnRootPage() {
|
|
297
|
+
return this.state.activePageId === this.props.rootPageId
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
get currentPage(): MappedPage | undefined {
|
|
301
|
+
return this.getPageById(this.state.activePageId)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
get previousPage(): MappedPage | undefined {
|
|
305
|
+
const previousPageId = this._pageHistory[this._pageHistory.length - 2]
|
|
306
|
+
return this.getPageById(previousPageId)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Returns the navigation methods and the page history.
|
|
310
|
+
// These are used in some callbacks to expose the navigation logic.
|
|
311
|
+
get exposedNavigationProps() {
|
|
312
|
+
const { goToPage, goToPreviousPage } = this
|
|
313
|
+
|
|
314
|
+
// we make a copy of the array so the original history
|
|
315
|
+
// cannot be modified from the outside
|
|
316
|
+
const pageHistory = [...this._pageHistory]
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
pageHistory,
|
|
320
|
+
goToPage,
|
|
321
|
+
goToPreviousPage
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
get currentPageAriaLabel() {
|
|
326
|
+
// return, if explicitly set
|
|
327
|
+
if (this.props['aria-labelledby']) {
|
|
328
|
+
return this.props['aria-labelledby']
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// if it has title, point to the title label content
|
|
332
|
+
if (this.currentPage?.renderTitle) {
|
|
333
|
+
return this._headerTitleLabelId
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// if root page has no title, try the trigger, if exists
|
|
337
|
+
if (this.isOnRootPage && this.props.trigger) {
|
|
338
|
+
return this._triggerId
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return undefined
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
getChildrenArray<C extends PageChildren | GroupChildren = PageChildren>(
|
|
345
|
+
children?: C | C[]
|
|
346
|
+
) {
|
|
347
|
+
if (!children) {
|
|
348
|
+
return []
|
|
349
|
+
}
|
|
350
|
+
return Array.isArray(children) ? children : ([children] as C[])
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
getPageById(id?: string): MappedPage | undefined {
|
|
354
|
+
return this.pageMap && id ? this.pageMap[id] : undefined
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
getPageChildById(id?: string) {
|
|
358
|
+
return id ? this._activeOptionsMap[id] : undefined
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
setDefaultSelected() {
|
|
362
|
+
this.pages.forEach((page) => {
|
|
363
|
+
const { children } = page.props
|
|
364
|
+
|
|
365
|
+
this.getChildrenArray(children).forEach((child) => {
|
|
366
|
+
if (matchComponentTypes<GroupChild>(child, [DrilldownGroup])) {
|
|
367
|
+
const {
|
|
368
|
+
id: groupId,
|
|
369
|
+
selectableType,
|
|
370
|
+
defaultSelected: groupDefaultSelected = [],
|
|
371
|
+
children: groupChildren
|
|
372
|
+
} = child.props
|
|
373
|
+
|
|
374
|
+
if (!selectableType) {
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (selectableType === 'single' && groupDefaultSelected.length > 1) {
|
|
379
|
+
error(
|
|
380
|
+
false,
|
|
381
|
+
`Radio type selectable groups can have only one item selected! Group with id "${groupId}" cannot select multiple items: [${groupDefaultSelected.join(
|
|
382
|
+
', '
|
|
383
|
+
)}]!`
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this._selectedGroupOptionsMap[groupId] = new Map()
|
|
390
|
+
|
|
391
|
+
this.getChildrenArray(groupChildren)?.forEach((groupChild) => {
|
|
392
|
+
if (
|
|
393
|
+
matchComponentTypes<OptionChild>(groupChild, [DrilldownOption])
|
|
394
|
+
) {
|
|
395
|
+
const {
|
|
396
|
+
id: optionId,
|
|
397
|
+
value: optionValue,
|
|
398
|
+
defaultSelected: optionDefaultSelected
|
|
399
|
+
} = groupChild.props
|
|
400
|
+
|
|
401
|
+
// if explicitly set to false, it should override the group's
|
|
402
|
+
if (optionDefaultSelected === false) {
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const isGroupDefaultSelected =
|
|
407
|
+
typeof optionValue !== 'undefined' &&
|
|
408
|
+
groupDefaultSelected
|
|
409
|
+
.filter((value) => typeof value !== 'undefined')
|
|
410
|
+
.includes(optionValue)
|
|
411
|
+
|
|
412
|
+
if (optionDefaultSelected || isGroupDefaultSelected) {
|
|
413
|
+
this._selectedGroupOptionsMap[groupId].set(
|
|
414
|
+
optionId,
|
|
415
|
+
optionValue
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
get selectedGroupOptionIdsArray() {
|
|
426
|
+
return Object.values(this._selectedGroupOptionsMap)
|
|
427
|
+
.map((groupIdMap) => Array.from(groupIdMap.keys()))
|
|
428
|
+
.flat()
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
get headerChildren() {
|
|
432
|
+
const { currentPage } = this
|
|
433
|
+
const { styles, deterministicId } = this.props
|
|
434
|
+
|
|
435
|
+
const headerChildren: PageChildren[] = []
|
|
436
|
+
|
|
437
|
+
if (!currentPage) {
|
|
438
|
+
return headerChildren
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const {
|
|
442
|
+
children,
|
|
443
|
+
renderBackButtonLabel,
|
|
444
|
+
renderTitle,
|
|
445
|
+
renderActionLabel,
|
|
446
|
+
onHeaderActionClicked,
|
|
447
|
+
withoutHeaderSeparator
|
|
448
|
+
} = currentPage
|
|
449
|
+
|
|
450
|
+
// Back navigation option
|
|
451
|
+
if (this.previousPage) {
|
|
452
|
+
const prevPageTitle = callRenderProp(this.previousPage.renderTitle)
|
|
453
|
+
|
|
454
|
+
const backButtonLabel: React.ReactNode = callRenderProp(
|
|
455
|
+
renderBackButtonLabel,
|
|
456
|
+
prevPageTitle
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
headerChildren.push(
|
|
460
|
+
<DrilldownOption
|
|
461
|
+
id={this._headerBackId}
|
|
462
|
+
onOptionClick={this.handleBackButtonClick}
|
|
463
|
+
>
|
|
464
|
+
<div css={styles?.headerBack} role="presentation">
|
|
465
|
+
{backButtonLabel}
|
|
466
|
+
</div>
|
|
467
|
+
</DrilldownOption>
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Header title
|
|
472
|
+
if (renderTitle) {
|
|
473
|
+
const title = callRenderProp(renderTitle)
|
|
474
|
+
|
|
475
|
+
if (title) {
|
|
476
|
+
headerChildren.push(
|
|
477
|
+
<DrilldownOption
|
|
478
|
+
id={this._headerTitleId}
|
|
479
|
+
role="presentation"
|
|
480
|
+
aria-hidden="true"
|
|
481
|
+
>
|
|
482
|
+
<div css={styles?.headerTitle} id={this._headerTitleLabelId}>
|
|
483
|
+
{title}
|
|
484
|
+
</div>
|
|
485
|
+
</DrilldownOption>
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Action label
|
|
491
|
+
if (renderActionLabel) {
|
|
492
|
+
const actionLabel = callRenderProp(renderActionLabel)
|
|
493
|
+
|
|
494
|
+
if (actionLabel) {
|
|
495
|
+
headerChildren.push(
|
|
496
|
+
<DrilldownOption
|
|
497
|
+
id={this._headerActionId}
|
|
498
|
+
themeOverride={{ color: styles?.headerActionColor }}
|
|
499
|
+
onOptionClick={(event) => {
|
|
500
|
+
if (typeof onHeaderActionClicked === 'function') {
|
|
501
|
+
onHeaderActionClicked(event)
|
|
502
|
+
}
|
|
503
|
+
}}
|
|
504
|
+
>
|
|
505
|
+
{actionLabel}
|
|
506
|
+
</DrilldownOption>
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// header separator
|
|
512
|
+
if (
|
|
513
|
+
headerChildren.length > 0 &&
|
|
514
|
+
children.length > 0 &&
|
|
515
|
+
!withoutHeaderSeparator
|
|
516
|
+
) {
|
|
517
|
+
headerChildren.push(
|
|
518
|
+
<DrilldownSeparator
|
|
519
|
+
id={deterministicId!('DrilldownHeader-Separator')}
|
|
520
|
+
/>
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return headerChildren
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
get shown() {
|
|
528
|
+
return this.props.trigger ? this.state.isShowingPopover : true
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
show = (event: React.SyntheticEvent) => {
|
|
532
|
+
if (this._popover) {
|
|
533
|
+
this._popover.show(event as React.UIEvent | React.FocusEvent)
|
|
534
|
+
this.setState({ isShowingPopover: true })
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
hide = (event: React.SyntheticEvent) => {
|
|
539
|
+
if (this._popover) {
|
|
540
|
+
this._popover.hide(event as React.UIEvent | React.FocusEvent)
|
|
541
|
+
this.setState({ isShowingPopover: false })
|
|
542
|
+
this.reset()
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
reset() {
|
|
547
|
+
this._activeOptionsMap = {}
|
|
548
|
+
this.setState({ highlightedOptionId: undefined })
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
public focus() {
|
|
552
|
+
if (this.shown) {
|
|
553
|
+
error(
|
|
554
|
+
!!this._drilldownRef?.focus,
|
|
555
|
+
'[Drilldown] Could not focus the drilldown.'
|
|
556
|
+
)
|
|
557
|
+
this._drilldownRef?.focus()
|
|
558
|
+
} else {
|
|
559
|
+
error(!!this._trigger?.focus, '[Drilldown] Could not focus the trigger.')
|
|
560
|
+
this._trigger!.focus!()
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
public focused() {
|
|
565
|
+
if (this.shown) {
|
|
566
|
+
return containsActiveElement(this._drilldownRef)
|
|
567
|
+
} else {
|
|
568
|
+
return containsActiveElement(this._trigger)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
focusOption(id: string) {
|
|
573
|
+
const container = this._containerElement
|
|
574
|
+
|
|
575
|
+
const optionElement = container?.querySelector(
|
|
576
|
+
`[id="${id}"]`
|
|
577
|
+
) as HTMLSpanElement
|
|
578
|
+
|
|
579
|
+
optionElement?.focus()
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
handleOptionHighlight = (
|
|
583
|
+
_event: React.SyntheticEvent,
|
|
584
|
+
{ id, direction }: { id?: string; direction?: -1 | 1 }
|
|
585
|
+
) => {
|
|
586
|
+
const { rotateFocus } = this.props
|
|
587
|
+
const { highlightedOptionId } = this.state
|
|
588
|
+
|
|
589
|
+
// if id exists, use that, or calculate from direction
|
|
590
|
+
let highlightId = this.getPageChildById(id) ? id : undefined
|
|
591
|
+
|
|
592
|
+
if (!highlightId) {
|
|
593
|
+
if (!highlightedOptionId) {
|
|
594
|
+
// nothing highlighted yet, highlight first option
|
|
595
|
+
highlightId = this.activeOptionIds[0]
|
|
596
|
+
} else if (direction) {
|
|
597
|
+
// if it has direction, find next id based on it
|
|
598
|
+
const index = this.activeOptionIds.indexOf(highlightedOptionId)
|
|
599
|
+
const newIndex = index + direction
|
|
600
|
+
|
|
601
|
+
highlightId = index > -1 ? this.activeOptionIds[newIndex] : undefined
|
|
602
|
+
|
|
603
|
+
if (rotateFocus) {
|
|
604
|
+
const lastOptionsIndex = this.activeOptionIds.length - 1
|
|
605
|
+
|
|
606
|
+
if (newIndex < 0) {
|
|
607
|
+
highlightId = this.activeOptionIds[lastOptionsIndex]
|
|
608
|
+
}
|
|
609
|
+
if (newIndex > lastOptionsIndex) {
|
|
610
|
+
highlightId = this.activeOptionIds[0]
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (highlightId) {
|
|
617
|
+
// only highlight if id exists as a valid option
|
|
618
|
+
this.setState({ highlightedOptionId: highlightId }, () => {
|
|
619
|
+
this.focusOption(highlightId!)
|
|
620
|
+
})
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Navigates to the page and also returns the new and old pageIds
|
|
625
|
+
public goToPage = (newPageId: string) => {
|
|
626
|
+
if (!newPageId) {
|
|
627
|
+
warn(false, `Cannot go to page because there was no page id provided.`)
|
|
628
|
+
return undefined
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// TS complains that it cannot be true, but since it is an exposed method,
|
|
632
|
+
// it is better if we provide a warning for this case too
|
|
633
|
+
if (typeof newPageId !== 'string') {
|
|
634
|
+
warn(
|
|
635
|
+
false,
|
|
636
|
+
`Cannot go to page because parameter newPageId has to be string (valid page id). Current newPageId is "${typeof newPageId}".`
|
|
637
|
+
)
|
|
638
|
+
return undefined
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (!this.pageMap?.[newPageId]) {
|
|
642
|
+
warn(
|
|
643
|
+
false,
|
|
644
|
+
`Cannot go to page because page with id: "${newPageId}" doesn't exist.`
|
|
645
|
+
)
|
|
646
|
+
return undefined
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// the last page id in the history is the current one,
|
|
650
|
+
// it will become the "prevPage"
|
|
651
|
+
const prevPageId = this._pageHistory[this._pageHistory.length - 1]
|
|
652
|
+
const idxInHistory = this._pageHistory.indexOf(newPageId)
|
|
653
|
+
|
|
654
|
+
if (idxInHistory < 0) {
|
|
655
|
+
// if it is not in the page history, we have to add it
|
|
656
|
+
this._pageHistory.push(newPageId)
|
|
657
|
+
} else {
|
|
658
|
+
// if it was already in the history, we go back to that page,
|
|
659
|
+
// and clear the rest from the history
|
|
660
|
+
this._pageHistory.splice(idxInHistory + 1, this._pageHistory.length - 1)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
this.setState({
|
|
664
|
+
activePageId: newPageId,
|
|
665
|
+
highlightedOptionId: undefined
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
return { prevPageId, newPageId }
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
public goToPreviousPage = () => {
|
|
672
|
+
if (!this.previousPage) {
|
|
673
|
+
warn(
|
|
674
|
+
false,
|
|
675
|
+
`There is no previous page to go to. The current page history is: [${this._pageHistory.join(
|
|
676
|
+
', '
|
|
677
|
+
)}].`
|
|
678
|
+
)
|
|
679
|
+
return undefined
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// If there is a previous page, goToPage will succeed and return the data
|
|
683
|
+
const { newPageId, prevPageId } = this.goToPage(this.previousPage.id)!
|
|
684
|
+
return { newPageId, prevPageId }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
handleBackButtonClick = () => {
|
|
688
|
+
const { onBackButtonClicked } = this.currentPage!
|
|
689
|
+
|
|
690
|
+
// we only display the back button when there is a page to go back to
|
|
691
|
+
const { newPageId, prevPageId } = this.goToPreviousPage()!
|
|
692
|
+
|
|
693
|
+
if (typeof onBackButtonClicked === 'function') {
|
|
694
|
+
onBackButtonClicked(newPageId, prevPageId)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
handleGroupOptionSelected(
|
|
699
|
+
event: React.SyntheticEvent,
|
|
700
|
+
selectedOption: MappedOption
|
|
701
|
+
) {
|
|
702
|
+
const { id: optionId, value } = selectedOption.props
|
|
703
|
+
const {
|
|
704
|
+
id: groupId,
|
|
705
|
+
selectableType,
|
|
706
|
+
onSelect: groupOnSelect
|
|
707
|
+
} = selectedOption.groupProps!
|
|
708
|
+
|
|
709
|
+
const { onSelect } = this.props
|
|
710
|
+
|
|
711
|
+
if (this._selectedGroupOptionsMap[groupId].has(optionId)) {
|
|
712
|
+
// toggle off, if already selected
|
|
713
|
+
this._selectedGroupOptionsMap[groupId].delete(optionId)
|
|
714
|
+
} else {
|
|
715
|
+
if (selectableType === 'multiple') {
|
|
716
|
+
// "checkbox"
|
|
717
|
+
this._selectedGroupOptionsMap[groupId].set(optionId, value)
|
|
718
|
+
} else if (selectableType === 'single') {
|
|
719
|
+
// "radio"
|
|
720
|
+
this._selectedGroupOptionsMap[groupId] = new Map()
|
|
721
|
+
this._selectedGroupOptionsMap[groupId].set(optionId, value)
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// needed for rerender
|
|
726
|
+
this.setState({ lastSelectedId: optionId })
|
|
727
|
+
|
|
728
|
+
const { groupProps, ...option } = selectedOption
|
|
729
|
+
const selectedOptionValuesInGroup = Array.from(
|
|
730
|
+
this._selectedGroupOptionsMap[groupId].values()
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
if (typeof groupOnSelect === 'function') {
|
|
734
|
+
groupOnSelect(event, {
|
|
735
|
+
value: selectedOptionValuesInGroup,
|
|
736
|
+
isSelected: this._selectedGroupOptionsMap[groupId].has(optionId),
|
|
737
|
+
selectedOption: option,
|
|
738
|
+
drilldown: this
|
|
739
|
+
})
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (typeof onSelect === 'function') {
|
|
743
|
+
onSelect(event, {
|
|
744
|
+
value: selectedOptionValuesInGroup,
|
|
745
|
+
isSelected: this._selectedGroupOptionsMap[groupId].has(optionId),
|
|
746
|
+
selectedOption: option,
|
|
747
|
+
drilldown: this
|
|
748
|
+
})
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
handleOptionSelect = (
|
|
753
|
+
event: React.SyntheticEvent,
|
|
754
|
+
{ id }: { id?: string }
|
|
755
|
+
) => {
|
|
756
|
+
const selectedOption = this.getPageChildById(id)
|
|
757
|
+
|
|
758
|
+
if (
|
|
759
|
+
!id ||
|
|
760
|
+
!selectedOption ||
|
|
761
|
+
selectedOption.props.disabled ||
|
|
762
|
+
(event.target as HTMLElement).getAttribute('disabled') ||
|
|
763
|
+
(event.target as HTMLElement).getAttribute('aria-disabled')
|
|
764
|
+
) {
|
|
765
|
+
event.preventDefault()
|
|
766
|
+
event.stopPropagation()
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const { shouldHideOnSelect, onSelect } = this.props
|
|
771
|
+
|
|
772
|
+
const { groupProps, ...selectedOptionChild } = selectedOption
|
|
773
|
+
const { subPageId, href, value, onOptionClick } = selectedOptionChild.props
|
|
774
|
+
|
|
775
|
+
if (typeof onOptionClick === 'function') {
|
|
776
|
+
onOptionClick(event, {
|
|
777
|
+
optionId: id,
|
|
778
|
+
drilldown: this,
|
|
779
|
+
...this.exposedNavigationProps
|
|
780
|
+
})
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (subPageId) {
|
|
784
|
+
this.goToPage(subPageId)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (href) {
|
|
788
|
+
const optionEl = this._drilldownRef?.querySelector(
|
|
789
|
+
`#${id}`
|
|
790
|
+
) as HTMLLinkElement
|
|
791
|
+
const isLink = optionEl.tagName.toLowerCase() === 'a'
|
|
792
|
+
|
|
793
|
+
// we need this check because in some cases
|
|
794
|
+
// we ignore href prop in renderOption()
|
|
795
|
+
if (isLink && optionEl?.href) {
|
|
796
|
+
optionEl.click()
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (groupProps?.selectableType) {
|
|
801
|
+
this.handleGroupOptionSelected(event, selectedOption)
|
|
802
|
+
} else {
|
|
803
|
+
if (typeof onSelect === 'function') {
|
|
804
|
+
onSelect(event, {
|
|
805
|
+
value,
|
|
806
|
+
isSelected: true,
|
|
807
|
+
selectedOption: selectedOptionChild,
|
|
808
|
+
drilldown: this
|
|
809
|
+
})
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// should prevent closing on page navigation
|
|
814
|
+
if (shouldHideOnSelect && !subPageId && id !== this._headerBackId) {
|
|
815
|
+
this.hide(event as React.UIEvent)
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Setting extra logic for keyboard navigation.
|
|
820
|
+
// Enter, Esc and up/down/home/end keys are handled by Selectable.
|
|
821
|
+
handleKeyDown = (event: React.KeyboardEvent) => {
|
|
822
|
+
const id = (event.target as HTMLElement).id
|
|
823
|
+
const option = this.getPageChildById(id)
|
|
824
|
+
|
|
825
|
+
// On Space...
|
|
826
|
+
if ([' ', 'space', 'Space'].includes(event.key)) {
|
|
827
|
+
// we need to preventDefault so the page doesn't scroll on Space
|
|
828
|
+
event.preventDefault()
|
|
829
|
+
event.stopPropagation()
|
|
830
|
+
|
|
831
|
+
// for options, Space it has to work as Enter (get selected)
|
|
832
|
+
if (option) {
|
|
833
|
+
this.handleOptionSelect(event, { id })
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// On Right arrow...
|
|
838
|
+
if (event.key === 'ArrowRight') {
|
|
839
|
+
// if the option has subpage, we open it
|
|
840
|
+
if (option?.props.subPageId) {
|
|
841
|
+
this.handleOptionSelect(event, { id })
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// On Left arrow...
|
|
846
|
+
if (event.key === 'ArrowLeft') {
|
|
847
|
+
// if it is possible, we go a level up in the history
|
|
848
|
+
if (this._pageHistory.length > 1) {
|
|
849
|
+
this.goToPreviousPage()
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// if on root page and popover is open, we close it
|
|
853
|
+
if (this.isOnRootPage && this._popover?.shown) {
|
|
854
|
+
this._popover.hide(event)
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
handleToggle = (event: React.UIEvent | React.FocusEvent, shown: boolean) => {
|
|
860
|
+
const { onToggle } = this.props
|
|
861
|
+
|
|
862
|
+
this.setState({ isShowingPopover: shown })
|
|
863
|
+
|
|
864
|
+
if (typeof onToggle === 'function') {
|
|
865
|
+
onToggle(event, {
|
|
866
|
+
shown,
|
|
867
|
+
drilldown: this,
|
|
868
|
+
...this.exposedNavigationProps
|
|
869
|
+
})
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
renderList(
|
|
874
|
+
getOptionProps: SelectableRender['getOptionProps'],
|
|
875
|
+
getDisabledOptionProps: SelectableRender['getDisabledOptionProps']
|
|
876
|
+
) {
|
|
877
|
+
const { currentPage, headerChildren } = this
|
|
878
|
+
|
|
879
|
+
if (!currentPage) {
|
|
880
|
+
return null
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const pageChildren: PageChildren[] = [
|
|
884
|
+
...headerChildren,
|
|
885
|
+
...currentPage.children
|
|
886
|
+
]
|
|
887
|
+
|
|
888
|
+
// for tracking if the last item was a Separator or not
|
|
889
|
+
let lastItemWasSeparator = false
|
|
890
|
+
|
|
891
|
+
return pageChildren.map((child, index) => {
|
|
892
|
+
/**
|
|
893
|
+
* --- RENDER GROUP ---
|
|
894
|
+
*/
|
|
895
|
+
if (matchComponentTypes<GroupChild>(child, [DrilldownGroup])) {
|
|
896
|
+
const isFirstChild = index === 0
|
|
897
|
+
const isLastChild = index === pageChildren.length - 1
|
|
898
|
+
const afterSeparator = lastItemWasSeparator
|
|
899
|
+
|
|
900
|
+
const needsFirstSeparator =
|
|
901
|
+
!child.props.withoutSeparators && !isFirstChild && !afterSeparator
|
|
902
|
+
const needsLastSeparator =
|
|
903
|
+
!child.props.withoutSeparators && !isLastChild
|
|
904
|
+
|
|
905
|
+
lastItemWasSeparator = needsLastSeparator
|
|
906
|
+
|
|
907
|
+
return this.renderGroup(
|
|
908
|
+
child,
|
|
909
|
+
getOptionProps,
|
|
910
|
+
getDisabledOptionProps,
|
|
911
|
+
// for rendering separators appropriately
|
|
912
|
+
needsFirstSeparator,
|
|
913
|
+
needsLastSeparator
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* --- RENDER SEPARATOR ---
|
|
918
|
+
*/
|
|
919
|
+
} else if (
|
|
920
|
+
matchComponentTypes<SeparatorChild>(child, [DrilldownSeparator])
|
|
921
|
+
) {
|
|
922
|
+
// if the last item was separator, we don't want to duplicate it
|
|
923
|
+
if (lastItemWasSeparator) {
|
|
924
|
+
return null
|
|
925
|
+
}
|
|
926
|
+
lastItemWasSeparator = true
|
|
927
|
+
return this.renderSeparator(child)
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* --- RENDER OPTION ---
|
|
931
|
+
*/
|
|
932
|
+
} else if (matchComponentTypes<OptionChild>(child, [DrilldownOption])) {
|
|
933
|
+
lastItemWasSeparator = false
|
|
934
|
+
return this.renderOption(child, getOptionProps, getDisabledOptionProps)
|
|
935
|
+
} else {
|
|
936
|
+
return null
|
|
937
|
+
}
|
|
938
|
+
})
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
renderSeparator(separator: SeparatorChild) {
|
|
942
|
+
const { id, themeOverride, ...props } = separator.props
|
|
943
|
+
return (
|
|
944
|
+
<Options.Separator
|
|
945
|
+
{...props}
|
|
946
|
+
id={id}
|
|
947
|
+
key={id}
|
|
948
|
+
role="separator"
|
|
949
|
+
// we pass the themeOverride to Options.Separator
|
|
950
|
+
themeOverride={themeOverride}
|
|
951
|
+
/>
|
|
952
|
+
)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
renderOption(
|
|
956
|
+
option: OptionChild,
|
|
957
|
+
getOptionProps: SelectableRender['getOptionProps'],
|
|
958
|
+
getDisabledOptionProps: SelectableRender['getDisabledOptionProps'],
|
|
959
|
+
groupProps?: DrilldownGroupProps
|
|
960
|
+
) {
|
|
961
|
+
const { styles } = this.props
|
|
962
|
+
let isSelected = false
|
|
963
|
+
|
|
964
|
+
const {
|
|
965
|
+
id,
|
|
966
|
+
children,
|
|
967
|
+
href,
|
|
968
|
+
as,
|
|
969
|
+
role,
|
|
970
|
+
subPageId,
|
|
971
|
+
disabled,
|
|
972
|
+
renderLabelInfo,
|
|
973
|
+
renderBeforeLabel,
|
|
974
|
+
renderAfterLabel,
|
|
975
|
+
beforeLabelContentVAlign,
|
|
976
|
+
afterLabelContentVAlign,
|
|
977
|
+
description,
|
|
978
|
+
descriptionRole,
|
|
979
|
+
elementRef,
|
|
980
|
+
themeOverride
|
|
981
|
+
} = option.props
|
|
982
|
+
|
|
983
|
+
if (!id) {
|
|
984
|
+
warn(
|
|
985
|
+
false,
|
|
986
|
+
`Drilldown.Option without id won't be rendered. It is needed to internally track the options.`
|
|
987
|
+
)
|
|
988
|
+
return null
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (this.getPageChildById(id)) {
|
|
992
|
+
warn(
|
|
993
|
+
false,
|
|
994
|
+
`Duplicate id: "${id}"! Make sure all options have unique ids, otherwise they won't be rendered.`
|
|
995
|
+
)
|
|
996
|
+
return null
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Props passed to the Option.Item
|
|
1000
|
+
let optionProps: Partial<OptionsItemProps> = {
|
|
1001
|
+
// passthrough props
|
|
1002
|
+
...omitProps(option.props, [
|
|
1003
|
+
...DrilldownOption.allowedProps,
|
|
1004
|
+
...Options.Item.allowedProps
|
|
1005
|
+
]),
|
|
1006
|
+
// props from selectable
|
|
1007
|
+
...getOptionProps({
|
|
1008
|
+
id,
|
|
1009
|
+
// aria-selected is only valid for these roles, otherwise we need to unset it
|
|
1010
|
+
...(role &&
|
|
1011
|
+
![
|
|
1012
|
+
'gridcell',
|
|
1013
|
+
'option',
|
|
1014
|
+
'row',
|
|
1015
|
+
'tab',
|
|
1016
|
+
'columnheader',
|
|
1017
|
+
'rowheader',
|
|
1018
|
+
'treeitem'
|
|
1019
|
+
].includes(role) && {
|
|
1020
|
+
'aria-selected': undefined
|
|
1021
|
+
})
|
|
1022
|
+
}),
|
|
1023
|
+
// we pass the themeOverride to Options.Item
|
|
1024
|
+
themeOverride,
|
|
1025
|
+
// directly passed props
|
|
1026
|
+
renderBeforeLabel,
|
|
1027
|
+
renderAfterLabel,
|
|
1028
|
+
beforeLabelContentVAlign,
|
|
1029
|
+
afterLabelContentVAlign,
|
|
1030
|
+
description,
|
|
1031
|
+
descriptionRole,
|
|
1032
|
+
as,
|
|
1033
|
+
role,
|
|
1034
|
+
elementRef,
|
|
1035
|
+
variant: 'default',
|
|
1036
|
+
tabIndex: -1
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// extra data we need track on the option,
|
|
1040
|
+
// but don't want to expose as props
|
|
1041
|
+
const optionData: OptionData = {
|
|
1042
|
+
groupProps: groupProps
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const isOptionDisabled =
|
|
1046
|
+
id !== this._headerBackId && // Back nav is never disabled
|
|
1047
|
+
(this.props.disabled ||
|
|
1048
|
+
this.currentPage?.disabled ||
|
|
1049
|
+
groupProps?.disabled ||
|
|
1050
|
+
disabled)
|
|
1051
|
+
|
|
1052
|
+
// display option as disabled
|
|
1053
|
+
if (isOptionDisabled) {
|
|
1054
|
+
optionProps.variant = 'disabled'
|
|
1055
|
+
optionProps = { ...optionProps, ...getDisabledOptionProps() }
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// track as valid active option if not the title
|
|
1059
|
+
if (id !== this._headerTitleId) {
|
|
1060
|
+
this._activeOptionsMap[id] = { ...option, ...optionData }
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const customRole =
|
|
1064
|
+
role !== DrilldownOption.defaultProps.role ? role : undefined
|
|
1065
|
+
|
|
1066
|
+
// BEFORE/AFTER elements:
|
|
1067
|
+
// we set a few manually on Options.Item,
|
|
1068
|
+
// the rest are passed directly
|
|
1069
|
+
if (subPageId) {
|
|
1070
|
+
optionProps.renderAfterLabel = <IconArrowOpenEndSolid />
|
|
1071
|
+
optionProps['aria-haspopup'] = true
|
|
1072
|
+
optionProps.role = customRole || 'button'
|
|
1073
|
+
warn(
|
|
1074
|
+
!renderAfterLabel,
|
|
1075
|
+
`The prop "renderAfterLabel" is reserved on item with id: "${id}". When it has "subPageId" provided, a navigation arrow will render after the label.`
|
|
1076
|
+
)
|
|
1077
|
+
}
|
|
1078
|
+
if (id === this._headerBackId) {
|
|
1079
|
+
optionProps.renderBeforeLabel = <IconArrowOpenStartSolid />
|
|
1080
|
+
}
|
|
1081
|
+
if (groupProps?.selectableType) {
|
|
1082
|
+
isSelected = this._selectedGroupOptionsMap[groupProps.id].has(id)
|
|
1083
|
+
optionProps['aria-checked'] = isSelected
|
|
1084
|
+
|
|
1085
|
+
optionProps.renderBeforeLabel = (
|
|
1086
|
+
<IconCheckSolid
|
|
1087
|
+
style={{
|
|
1088
|
+
opacity: isSelected ? 1 : 0
|
|
1089
|
+
}}
|
|
1090
|
+
/>
|
|
1091
|
+
)
|
|
1092
|
+
warn(
|
|
1093
|
+
!renderBeforeLabel,
|
|
1094
|
+
`The prop "renderBeforeLabel" is reserved on item with id: "${id}". When this option is a selectable member of a Drilldown.Group, selection indicator will render before the label.`
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
// setting aria roles and attributes for selectable group items
|
|
1098
|
+
if (groupProps.selectableType === 'single') {
|
|
1099
|
+
optionProps.role = customRole || 'menuitemradio'
|
|
1100
|
+
}
|
|
1101
|
+
if (groupProps.selectableType === 'multiple') {
|
|
1102
|
+
optionProps.role = customRole || 'menuitemcheckbox'
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// display option as highlighted
|
|
1107
|
+
if (id === this.state.highlightedOptionId) {
|
|
1108
|
+
optionProps.variant = 'highlighted'
|
|
1109
|
+
|
|
1110
|
+
if (isOptionDisabled) {
|
|
1111
|
+
optionProps.variant = 'highlighted-disabled'
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (href) {
|
|
1116
|
+
if (subPageId) {
|
|
1117
|
+
warn(
|
|
1118
|
+
false,
|
|
1119
|
+
`Drilldown.Option with id "${id}" has subPageId, so it will ignore the "href" property.`
|
|
1120
|
+
)
|
|
1121
|
+
} else if (groupProps?.selectableType) {
|
|
1122
|
+
warn(
|
|
1123
|
+
false,
|
|
1124
|
+
`Drilldown.Option with id "${id}" is in a selectable group, so it will ignore the "href" property.`
|
|
1125
|
+
)
|
|
1126
|
+
} else {
|
|
1127
|
+
optionProps.href = href
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
const optionLabel = callRenderProp(children, {
|
|
1132
|
+
id,
|
|
1133
|
+
variant: optionProps.variant,
|
|
1134
|
+
isSelected
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
if (!optionLabel) {
|
|
1138
|
+
warn(
|
|
1139
|
+
false,
|
|
1140
|
+
`There are no "children" prop provided for option with id: "${id}", so it won't be rendered.`
|
|
1141
|
+
)
|
|
1142
|
+
return null
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const renderLabelProps = {
|
|
1146
|
+
variant: optionProps.variant,
|
|
1147
|
+
vAlign: afterLabelContentVAlign,
|
|
1148
|
+
as,
|
|
1149
|
+
role: optionProps.role,
|
|
1150
|
+
isSelected
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// we need to bind our own option props the render functions
|
|
1154
|
+
if (
|
|
1155
|
+
typeof optionProps.renderBeforeLabel === 'function' &&
|
|
1156
|
+
!optionProps.renderBeforeLabel?.prototype?.isReactComponent
|
|
1157
|
+
) {
|
|
1158
|
+
optionProps.renderBeforeLabel = optionProps.renderBeforeLabel.bind(
|
|
1159
|
+
null,
|
|
1160
|
+
renderLabelProps
|
|
1161
|
+
)
|
|
1162
|
+
}
|
|
1163
|
+
if (
|
|
1164
|
+
typeof optionProps.renderAfterLabel === 'function' &&
|
|
1165
|
+
!optionProps.renderAfterLabel?.prototype?.isReactComponent
|
|
1166
|
+
) {
|
|
1167
|
+
optionProps.renderAfterLabel = optionProps.renderAfterLabel.bind(
|
|
1168
|
+
null,
|
|
1169
|
+
renderLabelProps
|
|
1170
|
+
)
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const labelInfo =
|
|
1174
|
+
renderLabelInfo && callRenderProp(renderLabelInfo, renderLabelProps)
|
|
1175
|
+
|
|
1176
|
+
const vAlignMap = {
|
|
1177
|
+
start: 'flex-start',
|
|
1178
|
+
center: 'center',
|
|
1179
|
+
end: 'flex-end'
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const labelAriaId = `${id}__label`
|
|
1183
|
+
const infoAriaId = `${id}__info`
|
|
1184
|
+
|
|
1185
|
+
const labelledby = option.props['aria-labelledby'] || labelAriaId
|
|
1186
|
+
const describedby =
|
|
1187
|
+
option.props['aria-describedby'] || (labelInfo ? infoAriaId : undefined)
|
|
1188
|
+
|
|
1189
|
+
return (
|
|
1190
|
+
<Options.Item
|
|
1191
|
+
{...optionProps}
|
|
1192
|
+
key={id}
|
|
1193
|
+
aria-labelledby={labelledby}
|
|
1194
|
+
aria-describedby={describedby}
|
|
1195
|
+
>
|
|
1196
|
+
<div css={styles?.optionContainer} role="none">
|
|
1197
|
+
<span css={styles?.optionContent} role="none" id={labelAriaId}>
|
|
1198
|
+
{optionLabel}
|
|
1199
|
+
</span>
|
|
1200
|
+
|
|
1201
|
+
{labelInfo ? (
|
|
1202
|
+
<span
|
|
1203
|
+
css={styles?.optionLabelInfo}
|
|
1204
|
+
role="presentation"
|
|
1205
|
+
style={{ alignSelf: vAlignMap[afterLabelContentVAlign!] }}
|
|
1206
|
+
>
|
|
1207
|
+
{/* this container span is needed for correct vAlign */}
|
|
1208
|
+
<span role="note" id={infoAriaId}>
|
|
1209
|
+
{labelInfo}
|
|
1210
|
+
</span>
|
|
1211
|
+
</span>
|
|
1212
|
+
) : null}
|
|
1213
|
+
</div>
|
|
1214
|
+
</Options.Item>
|
|
1215
|
+
)
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
renderGroup(
|
|
1219
|
+
group: GroupChild,
|
|
1220
|
+
getOptionProps: SelectableRender['getOptionProps'],
|
|
1221
|
+
getDisabledOptionProps: SelectableRender['getDisabledOptionProps'],
|
|
1222
|
+
needsFirstSeparator: boolean,
|
|
1223
|
+
needsLastSeparator: boolean
|
|
1224
|
+
) {
|
|
1225
|
+
const {
|
|
1226
|
+
id,
|
|
1227
|
+
children,
|
|
1228
|
+
renderGroupTitle,
|
|
1229
|
+
themeOverride,
|
|
1230
|
+
selectableType,
|
|
1231
|
+
role,
|
|
1232
|
+
as,
|
|
1233
|
+
elementRef
|
|
1234
|
+
} = group.props
|
|
1235
|
+
|
|
1236
|
+
if (!children) {
|
|
1237
|
+
return null
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
const groupChildren: (
|
|
1241
|
+
| React.ReactElement<OptionsProps>
|
|
1242
|
+
| React.ReactElement<OptionsSeparatorProps>
|
|
1243
|
+
)[] = []
|
|
1244
|
+
|
|
1245
|
+
// add a separator above
|
|
1246
|
+
if (needsFirstSeparator) {
|
|
1247
|
+
groupChildren.push(<Options.Separator />)
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (selectableType && !this._selectedGroupOptionsMap[id]) {
|
|
1251
|
+
this._selectedGroupOptionsMap[id] = new Map()
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// create a sublist as a group
|
|
1255
|
+
// (a wrapping list item will be created by Options)
|
|
1256
|
+
groupChildren.push(
|
|
1257
|
+
<Options
|
|
1258
|
+
id={id}
|
|
1259
|
+
key={id}
|
|
1260
|
+
role={role}
|
|
1261
|
+
as={as || this.props.as}
|
|
1262
|
+
renderLabel={renderGroupTitle}
|
|
1263
|
+
elementRef={elementRef}
|
|
1264
|
+
// we pass the themeOverride to Options
|
|
1265
|
+
themeOverride={themeOverride}
|
|
1266
|
+
>
|
|
1267
|
+
{this.getChildrenArray(children).map((child) => {
|
|
1268
|
+
if (
|
|
1269
|
+
matchComponentTypes<SeparatorChild>(child, [DrilldownSeparator])
|
|
1270
|
+
) {
|
|
1271
|
+
return this.renderSeparator(child)
|
|
1272
|
+
} else if (
|
|
1273
|
+
matchComponentTypes<OptionChild>(child, [DrilldownOption])
|
|
1274
|
+
) {
|
|
1275
|
+
return this.renderOption(
|
|
1276
|
+
child,
|
|
1277
|
+
getOptionProps,
|
|
1278
|
+
getDisabledOptionProps,
|
|
1279
|
+
group.props
|
|
1280
|
+
)
|
|
1281
|
+
} else {
|
|
1282
|
+
return null
|
|
1283
|
+
}
|
|
1284
|
+
})}
|
|
1285
|
+
</Options>
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
// add a separator below
|
|
1289
|
+
if (needsLastSeparator) {
|
|
1290
|
+
groupChildren.push(<Options.Separator />)
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
return groupChildren
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
renderPage() {
|
|
1297
|
+
const {
|
|
1298
|
+
styles,
|
|
1299
|
+
overflowY,
|
|
1300
|
+
overflowX,
|
|
1301
|
+
height,
|
|
1302
|
+
width,
|
|
1303
|
+
maxHeight,
|
|
1304
|
+
maxWidth,
|
|
1305
|
+
role,
|
|
1306
|
+
as,
|
|
1307
|
+
label
|
|
1308
|
+
} = this.props
|
|
1309
|
+
|
|
1310
|
+
if (!this.currentPage) {
|
|
1311
|
+
return null
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
return (
|
|
1315
|
+
<Selectable
|
|
1316
|
+
isShowingOptions={this.shown}
|
|
1317
|
+
highlightedOptionId={this.state.highlightedOptionId}
|
|
1318
|
+
selectedOptionId={this.selectedGroupOptionIdsArray}
|
|
1319
|
+
onRequestShowOptions={this.show}
|
|
1320
|
+
onRequestHideOptions={this.hide}
|
|
1321
|
+
onRequestSelectOption={this.handleOptionSelect}
|
|
1322
|
+
onRequestHighlightOption={this.handleOptionHighlight}
|
|
1323
|
+
onRequestHighlightFirstOption={(event) => {
|
|
1324
|
+
const firstOptionId = this.activeOptionIds[0]
|
|
1325
|
+
this.handleOptionHighlight(event, { id: firstOptionId })
|
|
1326
|
+
}}
|
|
1327
|
+
onRequestHighlightLastOption={(event) => {
|
|
1328
|
+
const lastOptionId =
|
|
1329
|
+
this.activeOptionIds[this.activeOptionIds.length - 1]
|
|
1330
|
+
this.handleOptionHighlight(event, { id: lastOptionId })
|
|
1331
|
+
}}
|
|
1332
|
+
>
|
|
1333
|
+
{({
|
|
1334
|
+
// TODO: figure out what other Selectable props we need, if we want to add a Select version for drilldown:
|
|
1335
|
+
// getRootProps, - we probably don't need this
|
|
1336
|
+
// getLabelProps, - do we need label?
|
|
1337
|
+
// getDescriptionProps, - might be nice for assistiveText like in Select
|
|
1338
|
+
// getInputProps, - hidden input for a11y? role="combobox" might be needed
|
|
1339
|
+
getTriggerProps,
|
|
1340
|
+
getListProps,
|
|
1341
|
+
getOptionProps,
|
|
1342
|
+
getDisabledOptionProps
|
|
1343
|
+
}) => (
|
|
1344
|
+
<View
|
|
1345
|
+
as="div"
|
|
1346
|
+
elementRef={this.handleDrilldownRef}
|
|
1347
|
+
tabIndex={0}
|
|
1348
|
+
css={styles?.drilldown}
|
|
1349
|
+
position="relative"
|
|
1350
|
+
borderRadius="small"
|
|
1351
|
+
width={width}
|
|
1352
|
+
maxWidth={maxWidth}
|
|
1353
|
+
role={role}
|
|
1354
|
+
aria-label={label}
|
|
1355
|
+
aria-labelledby={this.currentPageAriaLabel}
|
|
1356
|
+
{...getTriggerProps({
|
|
1357
|
+
id: this._id,
|
|
1358
|
+
// We need to override these aria attributes added by Selectable,
|
|
1359
|
+
// since Drilldown is not a combobox and has no popup
|
|
1360
|
+
'aria-haspopup': false,
|
|
1361
|
+
'aria-expanded': undefined,
|
|
1362
|
+
onKeyDown: this.handleKeyDown,
|
|
1363
|
+
onBlur: (event: React.FocusEvent) => {
|
|
1364
|
+
const target = event.currentTarget
|
|
1365
|
+
const related = event.relatedTarget
|
|
1366
|
+
const containsRelated = contains(
|
|
1367
|
+
target as Node | Window,
|
|
1368
|
+
related as Node | Window
|
|
1369
|
+
)
|
|
1370
|
+
|
|
1371
|
+
if (
|
|
1372
|
+
!related ||
|
|
1373
|
+
related === this._drilldownRef ||
|
|
1374
|
+
(related !== target && !containsRelated)
|
|
1375
|
+
) {
|
|
1376
|
+
this.setState({ highlightedOptionId: undefined })
|
|
1377
|
+
}
|
|
1378
|
+
},
|
|
1379
|
+
onMouseLeave: () => {
|
|
1380
|
+
this.setState({ highlightedOptionId: undefined })
|
|
1381
|
+
}
|
|
1382
|
+
})}
|
|
1383
|
+
>
|
|
1384
|
+
<View
|
|
1385
|
+
as="div"
|
|
1386
|
+
overflowY={overflowY}
|
|
1387
|
+
overflowX={overflowX}
|
|
1388
|
+
height={height}
|
|
1389
|
+
width={width}
|
|
1390
|
+
maxHeight={maxHeight}
|
|
1391
|
+
maxWidth={maxWidth}
|
|
1392
|
+
css={styles?.container}
|
|
1393
|
+
borderRadius="small"
|
|
1394
|
+
role="presentation"
|
|
1395
|
+
elementRef={(element) => {
|
|
1396
|
+
this._containerElement = element as HTMLDivElement
|
|
1397
|
+
}}
|
|
1398
|
+
>
|
|
1399
|
+
<Options {...getListProps()} role="presentation" as={as}>
|
|
1400
|
+
{this.renderList(getOptionProps, getDisabledOptionProps)}
|
|
1401
|
+
</Options>
|
|
1402
|
+
</View>
|
|
1403
|
+
</View>
|
|
1404
|
+
)}
|
|
1405
|
+
</Selectable>
|
|
1406
|
+
)
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
render() {
|
|
1410
|
+
// clear temporary option store
|
|
1411
|
+
this._activeOptionsMap = {}
|
|
1412
|
+
|
|
1413
|
+
const {
|
|
1414
|
+
show,
|
|
1415
|
+
defaultShow,
|
|
1416
|
+
placement,
|
|
1417
|
+
withArrow,
|
|
1418
|
+
shouldContainFocus,
|
|
1419
|
+
shouldReturnFocus,
|
|
1420
|
+
trigger,
|
|
1421
|
+
mountNode,
|
|
1422
|
+
constrain,
|
|
1423
|
+
positionTarget,
|
|
1424
|
+
popoverRef,
|
|
1425
|
+
disabled,
|
|
1426
|
+
onDismiss,
|
|
1427
|
+
onFocus,
|
|
1428
|
+
onMouseOver,
|
|
1429
|
+
offsetX,
|
|
1430
|
+
offsetY
|
|
1431
|
+
} = this.props
|
|
1432
|
+
|
|
1433
|
+
return trigger ? (
|
|
1434
|
+
<Popover
|
|
1435
|
+
isShowingContent={show}
|
|
1436
|
+
defaultIsShowingContent={defaultShow}
|
|
1437
|
+
shouldCloseOnDocumentClick={true}
|
|
1438
|
+
onHideContent={(event, { documentClick }) => {
|
|
1439
|
+
if (typeof onDismiss === 'function') {
|
|
1440
|
+
onDismiss(event, documentClick)
|
|
1441
|
+
}
|
|
1442
|
+
this.reset()
|
|
1443
|
+
this.handleToggle(event, false)
|
|
1444
|
+
}}
|
|
1445
|
+
onShowContent={(event) => this.handleToggle(event, true)}
|
|
1446
|
+
mountNode={mountNode}
|
|
1447
|
+
placement={placement}
|
|
1448
|
+
withArrow={withArrow}
|
|
1449
|
+
positionTarget={positionTarget}
|
|
1450
|
+
constrain={constrain}
|
|
1451
|
+
shouldContainFocus={shouldContainFocus}
|
|
1452
|
+
shouldReturnFocus={shouldReturnFocus}
|
|
1453
|
+
id={this._id}
|
|
1454
|
+
on={['click']}
|
|
1455
|
+
onFocus={onFocus}
|
|
1456
|
+
onMouseOver={onMouseOver}
|
|
1457
|
+
offsetX={offsetX}
|
|
1458
|
+
offsetY={offsetY}
|
|
1459
|
+
elementRef={(element) => {
|
|
1460
|
+
// setting ref for "Popover" version, the popover root
|
|
1461
|
+
// (if there is no trigger, we set it in handleDrilldownRef)
|
|
1462
|
+
this.handleRef(element)
|
|
1463
|
+
}}
|
|
1464
|
+
ref={(el) => {
|
|
1465
|
+
this._popover = el
|
|
1466
|
+
if (typeof popoverRef === 'function') {
|
|
1467
|
+
popoverRef(el)
|
|
1468
|
+
}
|
|
1469
|
+
}}
|
|
1470
|
+
renderTrigger={safeCloneElement(trigger as ReactElement, {
|
|
1471
|
+
ref: (el: (React.ReactInstance & { ref?: Element }) | null) => {
|
|
1472
|
+
this._trigger = el
|
|
1473
|
+
},
|
|
1474
|
+
'aria-haspopup': this.props.role,
|
|
1475
|
+
id: this._triggerId,
|
|
1476
|
+
disabled: !!((trigger as ReactElement).props.disabled || disabled),
|
|
1477
|
+
'aria-disabled':
|
|
1478
|
+
(trigger as ReactElement).props.disabled || disabled
|
|
1479
|
+
? 'true'
|
|
1480
|
+
: undefined
|
|
1481
|
+
})}
|
|
1482
|
+
>
|
|
1483
|
+
{this.renderPage()}
|
|
1484
|
+
</Popover>
|
|
1485
|
+
) : (
|
|
1486
|
+
this.renderPage()
|
|
1487
|
+
)
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
export default Drilldown
|
|
1492
|
+
export { Drilldown }
|