Skip to content

Commit a6cfd79

Browse files
roed314claude
andcommitted
Address review of active-constraint highlighting
Emit exactly one class attribute per rendered input. Class names now go through a first-class `classes` argument on SearchBox, merged with `advanced` and the `search_*` classes and deduplicated; any class written into `extra` is merged too, so a tag can no longer end up with two class attributes (invalid HTML, and browsers discard one of them, which was making the feature silently fail on the hypergeometric and CMF forms). The existing `extra=['class=...']` uses migrate to `classes`, and `extra` is copied rather than shared. Stop marking presentation-only controls as constraints. The trace and Euler tables' `n`, `n_primality` and `view_modp` choose which columns are displayed, not which rows match, so they are no longer highlighted; this includes the `n_primality=primes` that set_Trn inserts on every trace search. `an_modulo` is contextual, since it only enters the query through the trace constraints, so `is_constraint` now also accepts a function of `info`. Keep `search_active` describing the results on screen. The JS that retagged inputs as the user typed is gone, so the highlight no longer claims a constraint that has not been submitted; the search button's existing fresh/stale signal still reports that the form was edited. Mark active checkboxes with a box-shadow ring rather than an outline, so the keyboard focus indicator survives, in a darker shade with usable contrast, and tint text inputs with background-color so the shorthand does not reset a background image. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent ec86948 commit a6cfd79

14 files changed

Lines changed: 435 additions & 94 deletions

File tree

lmfdb/classical_modular_forms/main.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,18 @@ def trace_postprocess(res, info, query):
10191019
return res
10201020
def space_trace_postprocess(res, info, query):
10211021
return trace_postprocess(res, info, query, True)
1022+
def an_modulo_is_constraint(info):
1023+
"""
1024+
Whether the ``an_modulo`` input is restricting which results are shown.
1025+
1026+
The modulus enters the query only through the trace constraints, which it
1027+
turns from equalities into congruences. Without any trace constraints it
1028+
just controls whether the traces that are displayed are reduced (together
1029+
with ``view_modp``), so it should not be marked as an active constraint.
1030+
"""
1031+
an_constraints = info.get('an_constraints') or ''
1032+
return bool(an_constraints.strip())
1033+
10221034
def process_an_constraints(info, query, qfield='traces', nshift=None):
10231035
q = info.get('an_modulo','').strip()
10241036
if q:
@@ -1593,7 +1605,8 @@ def __init__(self):
15931605

15941606
weight_quantifier = ParityMod(
15951607
name='weight_parity',
1596-
extra=['class="simult_select"', 'onchange="simult_change(event);"'])
1608+
classes=["simult_select"],
1609+
extra=['onchange="simult_change(event);"'])
15971610

15981611
weight = TextBoxWithSelect(
15991612
name='weight',
@@ -1605,7 +1618,8 @@ def __init__(self):
16051618

16061619
character_quantifier = ParityMod(
16071620
name='char_parity',
1608-
extra=['class="simult_select"', 'onchange="simult_change(event);"'])
1621+
classes=["simult_select"],
1622+
extra=['onchange="simult_change(event);"'])
16091623

16101624
character = TextBoxWithSelect(
16111625
name='char_label',
@@ -1757,18 +1771,22 @@ def __init__(self):
17571771

17581772
results = CountBox()
17591773

1774+
# The columns shown in the trace table, and how their entries are
1775+
# displayed, do not affect which newforms match the search
17601776
trace_coldisplay = TextBox(
17611777
name='n',
17621778
label='Columns to display',
17631779
example='1-40',
1764-
example_span='3,7,19, 40-90')
1780+
example_span='3,7,19, 40-90',
1781+
is_constraint=False)
17651782

17661783
trace_primality = SelectBox(
17671784
name='n_primality',
17681785
label='Show',
17691786
options=[('', 'primes only'),
17701787
('prime_powers', 'prime powers'),
1771-
('all', 'all')])
1788+
('all', 'all')],
1789+
is_constraint=False)
17721790

17731791
trace_an_constraints = TextBox(
17741792
name='an_constraints',
@@ -1779,13 +1797,15 @@ def __init__(self):
17791797
trace_an_moduli = TextBox(
17801798
name='an_modulo',
17811799
label='Modulo',
1782-
example_span='5, 16')
1800+
example_span='5, 16',
1801+
is_constraint=an_modulo_is_constraint)
17831802

17841803
trace_view = SelectBox(
17851804
name='view_modp',
17861805
label='View',
17871806
options=[('', 'integers'),
1788-
('reductions', 'reductions')])
1807+
('reductions', 'reductions')],
1808+
is_constraint=False)
17891809

17901810
self.browse_array = [
17911811
[level, weight],

lmfdb/classical_modular_forms/test_cmf2.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,36 @@ def test_traces(self):
184184
for elt in map(str,[17,0,-80,60,3780,-1200]):
185185
assert elt in page.get_data(as_text=True)
186186

187+
def test_traces_active_classes(self):
188+
# Only the inputs that select which newforms are shown should be
189+
# marked; the rest choose which columns of traces are displayed
190+
url = '/ModularForm/GL2/Q/holomorphic/?level=11&search_type=Traces'
191+
self.assertEqual(self.search_classes(url, 'level'),
192+
{'search_constraint', 'search_active'})
193+
self.assertEqual(self.search_classes(url, 'weight'), {'search_constraint'})
194+
# set_Trn defaults n_primality to primes on every trace search, so it
195+
# would otherwise look filled in even when the user never touched it
196+
for name in ['n', 'n_primality', 'view_modp', 'an_modulo']:
197+
self.assertEqual(self.search_classes(url, name), set())
198+
199+
# The modulus does constrain the results once there is a trace
200+
# constraint for it to be applied to
201+
url = ('/ModularForm/GL2/Q/holomorphic/?level=244&weight=4&search_type=Traces'
202+
'&an_constraints=a3%3D0&an_modulo=3')
203+
for name in ['an_constraints', 'an_modulo']:
204+
self.assertEqual(self.search_classes(url, name),
205+
{'search_constraint', 'search_active'})
206+
207+
def test_simult_select_classes(self):
208+
# simult_select drives simult_change() in the CMF search template, and
209+
# has to survive alongside the classes marking active constraints
210+
url = '/ModularForm/GL2/Q/holomorphic/?level=11&weight=2'
211+
page = self.tc.get(url, follow_redirects=True).get_data(as_text=True)
212+
assert 'onchange="simult_change(event);"' in page
213+
for name in ['weight_parity', 'char_parity']:
214+
self.assertEqual(self.search_classes(url, name),
215+
{'simult_select', 'search_constraint'})
216+
187217
def test_trivial_searches(self):
188218
from sage.all import Subsets
189219
for begin in [

lmfdb/hypergm/main.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -732,31 +732,31 @@ def __init__(self):
732732
label="Degree",
733733
knowl="hgm.degree",
734734
example="4",
735-
extra=['class="family"'])
735+
classes=["family"])
736736
weight = TextBox(
737737
name="weight",
738738
label="Weight",
739739
knowl="hgm.weight",
740740
example="3",
741-
extra=['class="family"'])
741+
classes=["family"])
742742
famhodge = TextBox(
743743
name="famhodge",
744744
label="Family Hodge vector",
745745
knowl="hgm.hodge_vector",
746746
example="[1,1,1,1]",
747-
extra=['class="family"'])
747+
classes=["family"])
748748
A = TextBox(
749749
name="A",
750750
label="$A$",
751751
knowl="hgm.defining_parameters",
752752
example="[3,2,2]",
753-
extra=['class="family"'])
753+
classes=["family"])
754754
B = TextBox(
755755
name="B",
756756
label="$B$",
757757
knowl="hgm.defining_parameters",
758758
example="[6,4]",
759-
extra=['class="family"'])
759+
classes=["family"])
760760
p = SelectBox(
761761
name="p",
762762
label="Prime $p$",
@@ -770,25 +770,25 @@ def __init__(self):
770770
label="$A_p$",
771771
knowl="hgm.defining_parameter_ppart",
772772
example="[2,2,1,1]",
773-
extra=['class="family"'])
773+
classes=["family"])
774774
Bp = TextBox(
775775
name="Bp",
776776
label="$B_p$",
777777
knowl="hgm.defining_parameter_ppart",
778778
example="[2,2,1,1]",
779-
extra=['class="family"'])
779+
classes=["family"])
780780
Apperp = TextBox(
781781
name="Apperp",
782782
label=r"$A^\perp_p$",
783783
knowl="hgm.defining_parameter_primetoppart",
784784
example="[2,2,1,1]",
785-
extra=['class="family"'])
785+
classes=["family"])
786786
Bpperp = TextBox(
787787
name="Bpperp",
788788
label=r"$B^\perp_p$",
789789
knowl="hgm.defining_parameter_primetoppart",
790790
example="[2,2,1,1]",
791-
extra=['class="family"'])
791+
classes=["family"])
792792
spacer = BasicSpacer("")
793793

794794
conductor = TextBox(
@@ -797,27 +797,27 @@ def __init__(self):
797797
knowl="hgm.conductor",
798798
example="64",
799799
example_span="a value, like 32, a list, like 32,64, or a range like 1..10000",
800-
extra=['class="motive"'])
800+
classes=["motive"])
801801
hodge = TextBox(
802802
name="hodge",
803803
label="Hodge vector",
804804
knowl="hgm.hodge_vector",
805805
example="[1,1,1,1]",
806-
extra=['class="motive"'])
806+
classes=["motive"])
807807
t = TextBox(
808808
name="t",
809809
label="Specialization point $t$",
810810
knowl="hgm.specpoint",
811811
example="3/2",
812812
example_span="3/2 (1 has an associated degree drop and is always in the database)",
813-
extra=['class="motive"'])
813+
classes=["motive"])
814814
sign = TextBoxNoEg(
815815
name="sign",
816816
label=r"Root number $\epsilon$",
817817
knowl="lfunction.sign",
818818
example="-1",
819819
example_span="1 or -1, with -1 occurring only in the symplectic case",
820-
extra=['class="motive"'])
820+
classes=["motive"])
821821
# The following two boxes are not yet enabled
822822
# generic = YesNoBox(
823823
# name="generic",

lmfdb/hypergm/test_hgm.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ def test_search_degree(self):
6464
self.check_args("/Motive/Hypergeometric/Q/?degree=4&search_type=Family", ["A5_B3.2.1","A10_B4.2.1"])
6565
self.not_check_args("/Motive/Hypergeometric/Q/?degree=4&search_type=Family", "A2_B1")
6666

67+
def test_search_active_classes(self):
68+
# The family class drives the Enter-key behavior in hgm-index.html, so
69+
# it has to survive alongside the classes marking active constraints
70+
url = "/Motive/Hypergeometric/Q/?degree=4&search_type=Family"
71+
self.assertEqual(self.search_classes(url, "degree"),
72+
{"family", "search_constraint", "search_active"})
73+
self.assertEqual(self.search_classes(url, "weight"),
74+
{"family", "search_constraint"})
75+
6776
def test_search_weight(self):
6877
self.check_args("/Motive/Hypergeometric/Q/?weight=3&search_type=Family", "A5_B6.6")
6978
self.not_check_args("/Motive/Hypergeometric/Q/?weight=3&search_type=Family", "A3_B4")

lmfdb/lfunctions/main.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@
4141
from lmfdb.lfunctions import l_function_page
4242
from lmfdb.maass_forms.plot import paintSvgMaass
4343
from lmfdb.classical_modular_forms.web_newform import convert_newformlabel_from_conrey
44-
from lmfdb.classical_modular_forms.main import set_Trn, process_an_constraints
44+
from lmfdb.classical_modular_forms.main import (
45+
an_modulo_is_constraint,
46+
process_an_constraints,
47+
set_Trn,
48+
)
4549
from lmfdb.artin_representations.main import parse_artin_label
4650
from lmfdb.utils.search_parsing import (
4751
parse_bool, parse_ints, parse_ints_to_list, parse_floats, parse_noop, parse_mod1,
@@ -616,24 +620,30 @@ def __init__(self, force_rational=False):
616620
self.refine_array += [[algebraic]]
617621

618622
if force_rational:
623+
# The columns shown in the trace and Euler factor tables, and how
624+
# their entries are displayed, do not affect which L-functions
625+
# match the search
619626
trace_coldisplay = TextBox(
620627
name='n',
621628
label='Columns to display',
622629
example='1-40',
623-
example_span='3,7,19, 40-90')
630+
example_span='3,7,19, 40-90',
631+
is_constraint=False)
624632

625633
euler_coldisplay = TextBox(
626634
name='n',
627635
label='Columns to display',
628636
example='2-11',
629-
example_span='3,7,19')
637+
example_span='3,7,19',
638+
is_constraint=False)
630639

631640
trace_primality = SelectBox(
632641
name='n_primality',
633642
label='Show',
634643
options=[('', 'primes only'),
635644
('prime_powers', 'prime powers'),
636-
('all', 'all')])
645+
('all', 'all')],
646+
is_constraint=False)
637647

638648
trace_an_constraints = TextBox(
639649
name='an_constraints',
@@ -650,13 +660,15 @@ def __init__(self, force_rational=False):
650660
trace_an_moduli = TextBox(
651661
name='an_modulo',
652662
label='Modulo',
653-
example_span='5, 16')
663+
example_span='5, 16',
664+
is_constraint=an_modulo_is_constraint)
654665

655666
trace_view = SelectBox(
656667
name='view_modp',
657668
label='View',
658669
options=[('', 'integers'),
659-
('reductions', 'reductions')])
670+
('reductions', 'reductions')],
671+
is_constraint=False)
660672

661673
self.traces_array = [
662674
RowSpacer(22),

lmfdb/lfunctions/test_lfunctions.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,36 @@ def test_trace_search_mod_q(self):
614614
assert '2-37-1.1-c1-0-1' in L.get_data(as_text=True)
615615
assert '2-37-1.1-c1-0-0' not in L.get_data(as_text=True)
616616

617+
# ------------------------------------------------------
618+
# Testing which inputs are marked as active constraints
619+
# ------------------------------------------------------
620+
621+
def test_trace_active_classes(self):
622+
# Only the inputs that select which L-functions are shown should be
623+
# marked; the rest choose which columns of traces are displayed
624+
url = '/L/rational?degree=2&search_type=Traces'
625+
self.assertEqual(self.search_classes(url, 'degree'),
626+
{'search_constraint', 'search_active'})
627+
for name in ['n', 'n_primality', 'view_modp', 'an_modulo']:
628+
self.assertEqual(self.search_classes(url, name), set())
629+
630+
# The modulus does constrain the results once there is a trace
631+
# constraint for it to be applied to, unlike the reductions view
632+
url = ('L/rational?conductor=37&degree=2&search_type=Traces'
633+
'&an_constraints=a11+%3D1&an_modulo=3&view_modp=reductions')
634+
for name in ['conductor', 'an_constraints', 'an_modulo']:
635+
self.assertEqual(self.search_classes(url, name),
636+
{'search_constraint', 'search_active'})
637+
self.assertEqual(self.search_classes(url, 'view_modp'), set())
638+
639+
def test_euler_active_classes(self):
640+
url = '/L/rational?degree=2&search_type=Euler&n=2-11'
641+
self.assertEqual(self.search_classes(url, 'degree'),
642+
{'search_constraint', 'search_active'})
643+
self.assertEqual(self.search_classes(url, 'n'), set())
644+
self.assertEqual(self.search_classes(url, 'euler_constraints'),
645+
{'search_constraint'})
646+
617647
# ------------------------------------------------------
618648
# Testing units not tested above
619649
# ------------------------------------------------------

lmfdb/templates/diagram_search_form.html

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,4 @@ <h2 id='search_h2'>{{KNOWL('intro.search', search_header)}}
5252

5353
$("input").on("input", set_fresh);
5454
$("select").on("change", set_fresh);
55-
56-
function update_search_active() {
57-
var box = $(this);
58-
if (box.is("[type=checkbox]")) {
59-
box.toggleClass("search_active", box.prop("checked"));
60-
} else {
61-
box.toggleClass("search_active", Boolean(box.val()));
62-
}
63-
}
64-
$(".search_constraint").on("input change", update_search_active);
6555
</script>

lmfdb/templates/refine_search_form.html

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,5 @@ <h2 id='search_h2'>{{KNOWL('intro.search', search_header)}}
4141

4242
$("input").on("input", set_fresh);
4343
$("select").on("change", set_fresh);
44-
45-
function update_search_active() {
46-
var box = $(this);
47-
if (box.is("[type=checkbox]")) {
48-
box.toggleClass("search_active", box.prop("checked"));
49-
} else {
50-
box.toggleClass("search_active", Boolean(box.val()));
51-
}
52-
}
53-
$(".search_constraint").on("input change", update_search_active);
5444
</script>
5545

lmfdb/templates/style.css

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2335,11 +2335,12 @@ button.search_fresh {
23352335
}
23362336

23372337
/* Highlight inputs that are actively constraining the displayed search results */
2338-
input.search_active, select.search_active {
2339-
background: {{color.search_active_background}};
2338+
input.search_active:not([type=checkbox]), select.search_active {
2339+
background-color: {{color.search_active_background}};
23402340
}
2341-
/* Checkboxes are rendered natively, so mark them with a ring instead */
2341+
/* Checkboxes are rendered natively, so mark them with a ring instead. This
2342+
uses box-shadow rather than outline so that the browser's keyboard focus
2343+
indicator, which outline would replace, stays visible. */
23422344
input[type=checkbox].search_active {
2343-
outline: 3px solid {{color.search_active_background}};
2344-
outline-offset: 1px;
2345+
box-shadow: 0 0 0 2px {{color.search_active_ring}};
23452346
}

0 commit comments

Comments
 (0)