Project

General

Profile

Patch #43641 » 0003-Hide-sticky-header-when-table-is-outside-viewport.patch

Mizuki ISHIKAWA, 2026-04-14 02:50

View differences:

app/javascript/controllers/sticky_table_header_controller.js
11 11
  static values = {sticky: Boolean}
12 12

  
13 13
  connect() {
14
    if (!this.isIntersecting && !this.isHeaderOverflowX) {
15
      this.stickyValue = true;
16
    }
17

  
14
    this.headIntersecting = this.isHeadIntersecting;
15
    this.tableIntersecting = this.isTableIntersecting;
16
    this.updateStickyVisibility();
18 17
    this.observe();
19 18
  }
20 19

  
......
31 30
  }
32 31

  
33 32
  disconnect() {
34
    this.intersectionObserver?.disconnect();
33
    this.headIntersectionObserver?.disconnect();
34
    this.tableIntersectionObserver?.disconnect();
35 35
    this.resizeObserver?.disconnect();
36 36
  }
37 37

  
38 38
  observe() {
39
    this.intersectionObserver = new IntersectionObserver(entries => {
39
    this.headIntersectionObserver = new IntersectionObserver(entries => {
40 40
      entries.forEach(entry => {
41
        if (!entry.isIntersecting && !this.isHeaderOverflowX) {
42
          this.stickyValue = true;
43
        }
44
        if (entry.isIntersecting && !this.isHeaderOverflowX) {
45
          this.stickyValue = false;
46
        }
41
        this.headIntersecting = entry.isIntersecting;
42
        this.updateStickyVisibility();
43
      })
44
    }, {
45
      rootMargin: `-${this.stickyTopOffset}px 0px 0px 0px`
46
    });
47

  
48
    this.tableIntersectionObserver = new IntersectionObserver(entries => {
49
      entries.forEach(entry => {
50
        this.tableIntersecting = entry.isIntersecting;
51
        this.updateStickyVisibility();
47 52
      })
48 53
    }, {
49 54
      rootMargin: `-${this.stickyTopOffset}px 0px 0px 0px`
50 55
    });
51 56

  
52 57
    this.resizeObserver = new ResizeObserver(() => {
53
      if (!this.isIntersecting && this.isHeaderOverflowX) {
54
        this.stickyValue = false;
55
      }
56
      if (!this.isIntersecting && !this.isHeaderOverflowX) {
57
        this.stickyValue = true;
58
      }
58
      this.headIntersecting = this.isHeadIntersecting;
59
      this.tableIntersecting = this.isTableIntersecting;
60
      this.updateStickyVisibility();
59 61
      this.syncWidth();
60 62
    });
61 63

  
62
    this.intersectionObserver.observe(this.headTarget);
64
    this.headIntersectionObserver.observe(this.headTarget);
65
    this.tableIntersectionObserver.observe(this.originalTable);
63 66
    this.resizeObserver.observe(this.headTarget);
64 67
  }
65 68

  
69
  updateStickyVisibility() {
70
    this.stickyValue = !this.headIntersecting && this.tableIntersecting && !this.isHeaderOverflowX;
71
  }
72

  
66 73
  syncWidth(e) {
67 74
    const headRect = this.headTarget.getBoundingClientRect()
68 75

  
......
90 97
    }
91 98
  }
92 99

  
93
  get isIntersecting() {
100
  get isHeadIntersecting() {
94 101
    return this.headTarget.getBoundingClientRect().top > this.stickyTopOffset
95 102
  }
96 103

  
104
  get isTableIntersecting() {
105
    const tableRect = this.originalTable.getBoundingClientRect();
106

  
107
    return tableRect.bottom > this.stickyTopOffset && tableRect.top < window.innerHeight
108
  }
109

  
97 110
  get isHeaderOverflowX() {
98 111
    return this.element.scrollWidth > this.element.clientWidth;
99 112
  }
100 113

  
114
  get originalTable() {
115
    return this.headTarget.closest('table');
116
  }
117

  
101 118
  get stickyTopOffset() {
102 119
    this.prepare()
103 120

  
test/system/sticky_table_header_test.rb
65 65
    assert page.has_no_css?('table.list.sticky thead', visible: true)
66 66
  end
67 67

  
68
  def test_sticky_table_header_is_hidden_when_issue_list_is_below_viewport
69
    log_user('jsmith', 'jsmith')
70
    page.current_window.resize_to(1600, 300)
71

  
72
    visit '/projects/ecookbook/issues'
73
    # expand the filters to make the issue list further down the page
74
    find('fieldset#options > legend').click
75
    assert page.has_css?('fieldset#options > div', visible: true)
76

  
77
    assert issue_table_below_viewport?
78
    assert page.has_no_css?('table.list.sticky thead', visible: true)
79
  end
80

  
68 81
  private
69 82

  
70 83
  def scroll_issue_list_header_out_of_view
......
86 99
    JS
87 100
  end
88 101

  
102
  def issue_table_below_viewport?
103
    page.evaluate_script(<<~JS)
104
      (function() {
105
        const table = document.querySelector('table.list.issues');
106
        return table.getBoundingClientRect().top >= window.innerHeight;
107
      })();
108
    JS
109
  end
110

  
89 111
  def issue_table_head_width
90 112
    page.evaluate_script(<<~JS)
91 113
      (function() {
(6-6/8)