Skip to content

Commit fcb53eb

Browse files
daiyippyglove authors
authored andcommitted
Enhance HTML controls.
* `pg.views.html.controls.Label`: - Use `<span>` for labels without links, so it better supports nesting. * `pg.views.html.controls.TabControl`: - Add an optional 'name' to `Tab` so we could select that tab by name. - Add `insert` method. - Add `select` method. PiperOrigin-RevId: 705147063
1 parent 7b06665 commit fcb53eb

File tree

5 files changed

+123
-11
lines changed

5 files changed

+123
-11
lines changed

pyglove/core/views/html/controls/label.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def _on_bound(self):
7474

7575
def _to_html(self, **kwargs) -> Html:
7676
text_elem = Html.element(
77-
'a',
77+
'a' if self.link is not None else 'span',
7878
[self.text],
7979
id=self.element_id(),
8080
href=self.link,

pyglove/core/views/html/controls/label_test.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def test_text_only(self):
3434
label = label_lib.Label('foo')
3535
self.assertIsNone(label.tooltip)
3636
self.assertIsNone(label.link)
37-
self.assert_html_content(label, '<a class="label">foo</a>')
37+
self.assert_html_content(label, '<span class="label">foo</span>')
3838
with self.assertRaisesRegex(ValueError, 'Non-interactive .*'):
3939
label.update('bar')
4040

@@ -54,7 +54,7 @@ def test_with_tooltip(self):
5454
self.assert_html_content(
5555
label,
5656
(
57-
'<div class="label-container"><a class="label">foo</a>'
57+
'<div class="label-container"><span class="label">foo</span>'
5858
'<span class="tooltip">bar</span></div>'
5959
)
6060
)
@@ -130,7 +130,7 @@ def test_update(self):
130130

131131
def test_badge(self):
132132
badge = label_lib.Badge('foo')
133-
self.assert_html_content(badge, '<a class="label badge">foo</a>')
133+
self.assert_html_content(badge, '<span class="label badge">foo</span>')
134134

135135

136136
class LabelGroupTest(TestCase):
@@ -140,9 +140,9 @@ def test_basic(self):
140140
self.assert_html_content(
141141
group,
142142
(
143-
'<div class="label-group"><a class="label group-name">baz</a>'
144-
'<a class="label group-value">foo</a>'
145-
'<a class="label group-value">bar</a></div>'
143+
'<div class="label-group"><span class="label group-name">baz</span>'
144+
'<span class="label group-value">foo</span>'
145+
'<span class="label group-value">bar</span></div>'
146146
)
147147
)
148148

pyglove/core/views/html/controls/progress_bar_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ def test_basic(self):
4242
f'<div class="sub-progress foo" id="{bar["foo"].element_id()}">'
4343
'</div><div class="sub-progress bar" '
4444
f'id="{bar["bar"].element_id()}"></div></div>'
45-
'<div class="label-container"><a class="label progress-label"'
46-
f' id="{bar._progress_label.element_id()}">n/a</a><span class='
45+
'<div class="label-container"><span class="label progress-label"'
46+
f' id="{bar._progress_label.element_id()}">n/a</span><span class='
4747
f'"tooltip" id="{bar._progress_label.tooltip.element_id()}">'
4848
'Not started</span></div></div>'
4949
)

pyglove/core/views/html/controls/tab.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414
"""Tab control."""
1515

16-
from typing import Annotated, List, Literal, Union
16+
from typing import Annotated, List, Literal, Optional, Union
1717

1818
from pyglove.core.symbolic import flags as pg_flags
1919
from pyglove.core.symbolic import object as pg_object
@@ -44,6 +44,11 @@ class Tab(pg_object.Object):
4444
'The CSS classes of the tab.'
4545
] = []
4646

47+
name: Annotated[
48+
Optional[str],
49+
'An optional name that can be used to identify a tab under a tab control'
50+
] = None
51+
4752

4853
@pg_object.use_init_args(
4954
['tabs', 'selected', 'tab_position', 'id', 'css_classes', 'styles']
@@ -87,10 +92,84 @@ def append(self, tab: Tab) -> None:
8792
position='beforeend',
8893
)
8994

95+
def insert(self, index_or_name: Union[int, str], tab: Tab) -> None:
96+
"""Inserts a tab before a tab identified by index or name."""
97+
index = self.indexof(index_or_name)
98+
if index == -1:
99+
raise ValueError(f'Tab not found: {index_or_name!r}')
100+
with pg_flags.notify_on_change(False):
101+
self.tabs.insert(index, tab)
102+
103+
self._insert_adjacent_html(
104+
f"""
105+
const elem = document.querySelectorAll('#{self.element_id()}-button-group > .tab-button')[{index}];
106+
""",
107+
self._tab_button(tab, len(self.tabs) - 1),
108+
position='beforebegin',
109+
)
110+
self._insert_adjacent_html(
111+
f"""
112+
const elem = document.querySelectorAll('#{self.element_id()}-content-group > .tab-content')[{index}];
113+
""",
114+
self._tab_content(tab, len(self.tabs) - 1),
115+
position='beforebegin',
116+
)
117+
118+
def indexof(self, index_or_name: Union[int, str]) -> int:
119+
if isinstance(index_or_name, int):
120+
index = index_or_name
121+
if index >= len(self.tabs):
122+
return len(self.tabs) - 1
123+
elif index < -len(self.tabs):
124+
return -1
125+
elif index < 0:
126+
index = index + len(self.tabs)
127+
return index
128+
else:
129+
name = index_or_name
130+
assert isinstance(name, str), name
131+
for i, tab in enumerate(self.tabs):
132+
if tab.name == name:
133+
return i
134+
return -1
135+
90136
def extend(self, tabs: List[Tab]) -> None:
91137
for tab in tabs:
92138
self.append(tab)
93139

140+
def select(
141+
self,
142+
index_or_name: Union[int, str, List[str]]) -> Union[int, str]:
143+
"""Selects a tab identified by an index or name.
144+
145+
Args:
146+
index_or_name: The index or name of the tab to select. If a list of names
147+
is provided, the first name in the list that is found will be selected.
148+
149+
Returns:
150+
The index (if the index was provided) or name of the selected tab.
151+
"""
152+
selected_name = index_or_name if isinstance(index_or_name, str) else None
153+
index = -1
154+
if isinstance(index_or_name, list):
155+
for name in index_or_name:
156+
index = self.indexof(name)
157+
if index != -1:
158+
selected_name = name
159+
break
160+
else:
161+
index = self.indexof(index_or_name)
162+
if index == -1:
163+
raise ValueError(f'Tab not found: {index_or_name!r}')
164+
self._sync_members(selected=index)
165+
self._run_javascript(
166+
f"""
167+
const tabButtons = document.querySelectorAll('#{self.element_id()}-button-group > .tab-button');
168+
tabButtons[{index}].click();
169+
"""
170+
)
171+
return selected_name or index
172+
94173
def _to_html(self, **kwargs):
95174
return Html.element(
96175
'table',

pyglove/core/views/html/controls/tab_test.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_basic(self):
3636
elem_id = tab.element_id()
3737
self.assert_html_content(
3838
tab,
39-
f"""<table class="tab-control"><tr><td><div class="tab-button-group top" id="{elem_id}-button-group"><button class="tab-button selected foo" onclick="openTab(event, '{elem_id}', '{elem_id}-0')"><a class="label">foo</a></button><button class="tab-button" onclick="openTab(event, '{elem_id}', '{elem_id}-1')"><a class="label">bar</a></button></div></td></tr><tr><td><div class="tab-content-group top" id="{elem_id}-content-group"><div class="tab-content selected foo" id="{elem_id}-0"><h1>foo</h1></div><div class="tab-content" id="{elem_id}-1"><h1>bar</h1></div></div></td></tr></table>"""
39+
f"""<table class="tab-control"><tr><td><div class="tab-button-group top" id="{elem_id}-button-group"><button class="tab-button selected foo" onclick="openTab(event, '{elem_id}', '{elem_id}-0')"><span class="label">foo</span></button><button class="tab-button" onclick="openTab(event, '{elem_id}', '{elem_id}-1')"><span class="label">bar</span></button></div></td></tr><tr><td><div class="tab-content-group top" id="{elem_id}-content-group"><div class="tab-content selected foo" id="{elem_id}-0"><h1>foo</h1></div><div class="tab-content" id="{elem_id}-1"><h1>bar</h1></div></div></td></tr></table>"""
4040
)
4141
with tab.track_scripts() as scripts:
4242
tab.extend([
@@ -48,6 +48,39 @@ def test_basic(self):
4848
tab_lib.Tab('qux', base.Html('<h1>qux</h1>')),
4949
])
5050
self.assertEqual(len(scripts), 6)
51+
with tab.track_scripts() as scripts:
52+
tab.insert(0, tab_lib.Tab('x', 'foo', name='x'))
53+
self.assertEqual(len(scripts), 2)
54+
self.assertEqual(len(tab.tabs), 5)
55+
self.assertEqual(tab.indexof(-1), 4)
56+
self.assertEqual(tab.indexof(3), 3)
57+
self.assertEqual(tab.indexof(10), 4)
58+
self.assertEqual(tab.indexof(-10), -1)
59+
self.assertEqual(tab.indexof('x'), 0)
60+
self.assertEqual(tab.indexof('y'), -1)
61+
self.assertEqual(tab.select(0), 0)
62+
self.assertEqual(tab.select('x'), 'x')
63+
self.assertEqual(tab.select(['y', 'x']), 'x')
64+
65+
with self.assertRaisesRegex(ValueError, 'Tab not found'):
66+
tab.select('y')
67+
with self.assertRaisesRegex(ValueError, 'Tab not found'):
68+
tab.insert('y', tab_lib.Tab('z', 'bar'))
69+
70+
with tab.track_scripts() as scripts:
71+
tab.insert('x', tab_lib.Tab('y', 'bar'))
72+
self.assertEqual(len(scripts), 2)
73+
self.assertEqual(len(tab.tabs), 6)
74+
75+
with tab.track_scripts() as scripts:
76+
tab.select(3)
77+
self.assertEqual(len(scripts), 1)
78+
self.assertEqual(tab.selected, 3)
79+
80+
with tab.track_scripts() as scripts:
81+
tab.select('x')
82+
self.assertEqual(len(scripts), 1)
83+
self.assertEqual(tab.selected, 1)
5184

5285

5386
if __name__ == '__main__':

0 commit comments

Comments
 (0)