Skip to content

Commit 05dd4e4

Browse files
authored
Merge pull request #1495 from pbugnion/selection-container-allow-none
Allow `selected_index=none` in selection container widgets
2 parents b1aff6e + 95cc187 commit 05dd4e4

File tree

8 files changed

+245
-29
lines changed

8 files changed

+245
-29
lines changed

docs/source/examples/Widget List.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,7 +1071,7 @@
10711071
"\n",
10721072
"Unlike the rest of the widgets discussed earlier, the container widgets `Accordion` and `Tab` update their `selected_index` attribute when the user changes which accordion or tab is selected. That means that you can both see what the user is doing *and* programmatically set what the user sees by setting the value of `selected_index`.\n",
10731073
"\n",
1074-
"Setting `selected_index = -1` (or any value outside the range of available accordions or tabs) closes all of the accordions or deselects all tabs."
1074+
"Setting `selected_index = None` closes all of the accordions or deselects all tabs."
10751075
]
10761076
},
10771077
{
@@ -1100,7 +1100,7 @@
11001100
},
11011101
"outputs": [],
11021102
"source": [
1103-
"accordion.selected_index = -1"
1103+
"accordion.selected_index = None"
11041104
]
11051105
},
11061106
{
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright (c) Jupyter Development Team.
2+
# Distributed under the terms of the Modified BSD License.
3+
4+
from unittest import TestCase
5+
6+
from traitlets import TraitError
7+
8+
from ipywidgets.widgets import Accordion, HTML
9+
10+
11+
class TestAccordion(TestCase):
12+
13+
def setUp(self):
14+
self.children = [HTML('0'), HTML('1')]
15+
16+
def test_selected_index_none(self):
17+
accordion = Accordion(self.children, selected_index=None)
18+
state = accordion.get_state()
19+
assert state['selected_index'] is None
20+
21+
def test_selected_index(self):
22+
accordion = Accordion(self.children, selected_index=1)
23+
state = accordion.get_state()
24+
assert state['selected_index'] == 1
25+
26+
def test_selected_index_out_of_bounds(self):
27+
with self.assertRaises(TraitError):
28+
Accordion(self.children, selected_index=-1)

ipywidgets/widgets/widget_selectioncontainer.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,27 @@
99

1010
from .widget_box import Box, register
1111
from .widget_core import CoreWidget
12-
from traitlets import Unicode, Dict, CInt
12+
from traitlets import Unicode, Dict, CInt, TraitError, validate
1313
from ipython_genutils.py3compat import unicode_type
1414

1515

1616
class _SelectionContainer(Box, CoreWidget):
1717
"""Base class used to display multiple child widgets."""
1818
_titles = Dict(help="Titles of the pages").tag(sync=True)
19-
selected_index = CInt(help="The index of the selected page.").tag(sync=True)
19+
selected_index = CInt(
20+
help="""The index of the selected page.
21+
22+
This is either an integer selecting a particular sub-widget,
23+
or None to have no widgets selected.""",
24+
allow_none=True
25+
).tag(sync=True)
26+
27+
@validate('selected_index')
28+
def _validated_index(self, proposal):
29+
if proposal.value is None or 0 <= proposal.value < len(self.children):
30+
return proposal.value
31+
else:
32+
raise TraitError('Invalid selection: index out of bounds')
2033

2134
# Public methods
2235
def set_title(self, index, title):

packages/controls/src/phosphor/currentselection.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ class Selection<T> {
9393
*/
9494
set value(value: T) {
9595
if (value === null) {
96-
this.index = -1;
96+
this.index = null;
9797
} else {
9898
this.index = ArrayExt.firstIndexOf(this._array, value);
9999
}
@@ -103,9 +103,9 @@ class Selection<T> {
103103
* Get the index of the currently selected item.
104104
*
105105
* #### Notes
106-
* This will be `-1` if no item is selected.
106+
* This will be `null` if no item is selected.
107107
*/
108-
get index(): number {
108+
get index(): number | null {
109109
return this._index;
110110
}
111111

@@ -115,14 +115,19 @@ class Selection<T> {
115115
* @param index - The index to select.
116116
*
117117
* #### Notes
118-
* If the value is out of range, the index will be set to `-1`, which
118+
* If the value is out of range, the index will be set to `null`, which
119119
* indicates no item is selected.
120120
*/
121-
set index(index: number) {
121+
set index(index: number | null) {
122122
// Coerce the value to an index.
123-
let i = Math.floor(index);
124-
if (i < 0 || i >= this._array.length) {
125-
i = -1;
123+
let i;
124+
if (index !== null) {
125+
i = Math.floor(index);
126+
if (i < 0 || i >= this._array.length) {
127+
i = null;
128+
}
129+
} else {
130+
i = null;
126131
}
127132

128133
// Bail early if the index will not change.
@@ -194,7 +199,7 @@ class Selection<T> {
194199

195200
// Handle the behavior where the new item is always selected,
196201
// or the behavior where the new item is selected if needed.
197-
if (bh === 'select-item' || (bh === 'select-item-if-needed' && ci === -1)) {
202+
if (bh === 'select-item' || (bh === 'select-item-if-needed' && ci === null)) {
198203
this._index = i;
199204
this._value = item;
200205
this._previousValue = cv;
@@ -240,19 +245,19 @@ class Selection<T> {
240245
let pv = this._value;
241246

242247
// Reset the current index and previous item.
243-
this._index = -1;
248+
this._index = null;
244249
this._value = null;
245250
this._previousValue = null;
246251

247252
// If no item was selected, there's nothing else to do.
248-
if (pi === -1) {
253+
if (pi === null) {
249254
return;
250255
}
251256

252257
// Emit the current changed signal.
253258
this._selectionChanged.emit({
254259
previousIndex: pi, previousValue: pv,
255-
currentIndex: -1, currentValue: null
260+
currentIndex: this._index, currentValue: this._value
256261
});
257262
}
258263

@@ -283,12 +288,12 @@ class Selection<T> {
283288
// No item gets selected if the vector is empty.
284289
if (this._array.length === 0) {
285290
// Reset the current index and previous item.
286-
this._index = -1;
291+
this._index = null;
287292
this._value = null;
288293
this._previousValue = null;
289294
this._selectionChanged.emit({
290295
previousIndex: i, previousValue: item,
291-
currentIndex: -1, currentValue: null
296+
currentIndex: this._index, currentValue: this._value
292297
});
293298
return;
294299
}
@@ -334,12 +339,12 @@ class Selection<T> {
334339
}
335340

336341
// Otherwise, no item gets selected.
337-
this._index = -1;
342+
this._index = null;
338343
this._value = null;
339344
this._previousValue = null;
340345
this._selectionChanged.emit({
341346
previousIndex: i, previousValue: item,
342-
currentIndex: -1, currentValue: null
347+
currentIndex: this._index, currentValue: this._value
343348
});
344349
}
345350

@@ -348,7 +353,7 @@ class Selection<T> {
348353
*/
349354
private _updateSelectedValue() {
350355
let i = this._index;
351-
this._value = i !== -1 ? this._array[i] : null;
356+
this._value = i !== null ? this._array[i] : null;
352357
}
353358

354359
private _array: ReadonlyArray<T> = null;

packages/controls/src/phosphor/tabpanel.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,20 +120,22 @@ class TabPanel extends Widget {
120120
* Get the index of the currently selected tab.
121121
*
122122
* #### Notes
123-
* This will be `-1` if no tab is selected.
123+
* This will be `null` if no tab is selected.
124124
*/
125-
get currentIndex(): number {
126-
return this.tabBar.currentIndex;
125+
get currentIndex(): number | null {
126+
const currentIndex = this.tabBar.currentIndex;
127+
// Phosphor tab bars have an index of -1 if no tab is selected
128+
return (currentIndex === -1 ? null : currentIndex);
127129
}
128130

129131
/**
130132
* Set the index of the currently selected tab.
131133
*
132134
* #### Notes
133-
* If the index is out of range, it will be set to `-1`.
135+
* If the index is out of range, it will be set to `null`.
134136
*/
135-
set currentIndex(value: number) {
136-
this.tabBar.currentIndex = value;
137+
set currentIndex(value: number | null) {
138+
this.tabBar.currentIndex = (value === null ? -1 : value);
137139
}
138140

139141
/**

packages/controls/src/widget_selectioncontainer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ class AccordionView extends DOMWidgetView {
156156
// tabs before updating so we don't get spurious changes in the index,
157157
// which would then set off another sync cycle.
158158
this.updatingChildren = true;
159-
this.pWidget.selection.index = -1;
159+
this.pWidget.selection.index = null;
160160
this.children_views.update(this.model.get('children'));
161161
this.update_selected_index();
162162
this.updatingChildren = false;
@@ -339,7 +339,7 @@ class TabView extends DOMWidgetView {
339339
// tabs before updating so we don't get spurious changes in the index,
340340
// which would then set off another sync cycle.
341341
this.updatingTabs = true;
342-
this.pWidget.currentIndex = -1;
342+
this.pWidget.currentIndex = null;
343343
this.childrenViews.update(this.model.get('children'));
344344
this.pWidget.currentIndex = this.model.get('selected_index');
345345
this.updatingTabs = false;

packages/controls/test/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
// Distributed under the terms of the Modified BSD License.
33

44
import './widget_date_test';
5+
import './phosphor/currentselection_test';

0 commit comments

Comments
 (0)