@douyinfe/semi-foundation 2.98.0 → 2.99.0

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.
@@ -650,7 +650,9 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
650
650
  } else if (selectedKeys.size && !multiple) {
651
651
  inputValue = this.renderDisplayText([...selectedKeys][0]);
652
652
  }
653
- this._adapter.updateStates({ inputValue });
653
+ // Reset isSearching immediately in close() instead of relying on afterClose callback
654
+ // This prevents timing issues where open() clears inputValue but isSearching is still true
655
+ this._adapter.updateStates({ inputValue, isSearching: false });
654
656
  !multiple && this.toggle2SearchInput(false);
655
657
  !multiple && this._adapter.updateFocusState(false);
656
658
  }
@@ -520,6 +520,8 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
520
520
  const formAllowEmpty = this.getProp('allowEmpty');
521
521
 
522
522
  // priority at Field level
523
+ // NOTE: Keep legacy semantics here to avoid implicit breaking changes.
524
+ // (Field-level allowEmpty overriding behavior is handled during register/restore paths.)
523
525
  const allowEmpty = fieldAllowEmpty ? fieldAllowEmpty : formAllowEmpty;
524
526
 
525
527
  ObjectUtil.set(this.data.values, field, value, allowEmpty);
@@ -454,8 +454,11 @@ class CascaderFoundation extends _foundation.default {
454
454
  } else if (selectedKeys.size && !multiple) {
455
455
  inputValue = this.renderDisplayText([...selectedKeys][0]);
456
456
  }
457
+ // Reset isSearching immediately in close() instead of relying on afterClose callback
458
+ // This prevents timing issues where open() clears inputValue but isSearching is still true
457
459
  this._adapter.updateStates({
458
- inputValue
460
+ inputValue,
461
+ isSearching: false
459
462
  });
460
463
  !multiple && this.toggle2SearchInput(false);
461
464
  !multiple && this._adapter.updateFocusState(false);
@@ -493,6 +493,8 @@ class FormFoundation extends _foundation.default {
493
493
  */
494
494
  const formAllowEmpty = this.getProp('allowEmpty');
495
495
  // priority at Field level
496
+ // NOTE: Keep legacy semantics here to avoid implicit breaking changes.
497
+ // (Field-level allowEmpty overriding behavior is handled during register/restore paths.)
496
498
  const allowEmpty = fieldAllowEmpty ? fieldAllowEmpty : formAllowEmpty;
497
499
  ObjectUtil.set(this.data.values, field, value, allowEmpty);
498
500
  /**
@@ -76,7 +76,7 @@
76
76
  color: unset;
77
77
  }
78
78
  .semi-popover-wrapper[x-placement=top] .semi-popover-icon-arrow {
79
- left: 50%;
79
+ left: var(--semi-tooltip-arrow-offset-x, 50%);
80
80
  transform: translateX(-50%);
81
81
  bottom: -7px;
82
82
  }
@@ -114,7 +114,7 @@
114
114
  width: 8px;
115
115
  height: 24px;
116
116
  right: -7px;
117
- top: 50%;
117
+ top: var(--semi-tooltip-arrow-offset-y, 50%);
118
118
  transform: translateY(-50%);
119
119
  }
120
120
  .semi-popover-wrapper[x-placement=left].semi-popover-with-arrow,
@@ -146,7 +146,7 @@
146
146
  width: 8px;
147
147
  height: 24px;
148
148
  left: -7px;
149
- top: 50%;
149
+ top: var(--semi-tooltip-arrow-offset-y, 50%);
150
150
  transform: translateY(-50%) rotate(180deg);
151
151
  }
152
152
  .semi-popover-wrapper[x-placement=right].semi-popover-with-arrow,
@@ -175,7 +175,7 @@
175
175
  }
176
176
  .semi-popover-wrapper[x-placement=bottom] .semi-popover-icon-arrow {
177
177
  top: -7px;
178
- left: 50%;
178
+ left: var(--semi-tooltip-arrow-offset-x, 50%);
179
179
  transform: translateX(-50%) rotate(180deg);
180
180
  }
181
181
  .semi-popover-wrapper[x-placement=bottom].semi-popover-with-arrow,
@@ -144,6 +144,27 @@ $module: #{$prefix}-table;
144
144
  border-left: $width-table_base_border $border-table_base-borderStyle $color-table-border-default;
145
145
  }
146
146
  }
147
+
148
+ // Fix #441 (RTL): when horizontal scroll is enabled and table is NOT scrolled to the far left,
149
+ // the outer left border can be outside of viewport (the real border is rendered by the last column cell).
150
+ // Draw an overlay left border on the container to keep it visible.
151
+ // When scrolled to the far left, `.semi-table-scroll-position-left` exists and we should not draw it.
152
+ &:not(.#{$module}-scroll-position-left) {
153
+ & > .#{$module}-container {
154
+ &::after {
155
+ content: '';
156
+ position: absolute;
157
+ top: 0;
158
+ left: 0;
159
+ bottom: 0;
160
+ width: $width-table_base_border;
161
+ background-color: $color-table-border-default;
162
+ display: block;
163
+ z-index: $z-table_fixed_column + 2;
164
+ pointer-events: none;
165
+ }
166
+ }
167
+ }
147
168
  }
148
169
 
149
170
  &-fixed {
@@ -416,6 +416,21 @@
416
416
  border-right: 1px solid var(--semi-color-border);
417
417
  border-bottom: 1px solid var(--semi-color-border);
418
418
  }
419
+ .semi-table-bordered:not(.semi-table-scroll-position-right) > .semi-table-container::after {
420
+ content: "";
421
+ position: absolute;
422
+ top: 0;
423
+ right: 0;
424
+ bottom: 0;
425
+ width: 1px;
426
+ background-color: var(--semi-color-border);
427
+ display: block;
428
+ z-index: 103;
429
+ pointer-events: none;
430
+ }
431
+ .semi-table-bordered:not(.semi-table-scroll-position-right) > .semi-table-container > .semi-table-header {
432
+ box-shadow: inset-1px 0 0 0 var(--semi-color-border);
433
+ }
419
434
  :where(.semi-table-bordered > .semi-table-container) > .semi-table-body > .semi-table-placeholder {
420
435
  border-right: 1px solid var(--semi-color-border);
421
436
  }
@@ -435,6 +450,7 @@
435
450
  position: sticky;
436
451
  left: 0px;
437
452
  z-index: 1;
453
+ box-sizing: border-box;
438
454
  padding: 16px 12px;
439
455
  color: var(--semi-color-text-2);
440
456
  font-size: 14px;
@@ -592,6 +608,18 @@
592
608
  border-right: 0;
593
609
  border-left: 1px solid var(--semi-color-border);
594
610
  }
611
+ .semi-table-wrapper-rtl .semi-table-bordered:not(.semi-table-scroll-position-left) > .semi-table-container::after {
612
+ content: "";
613
+ position: absolute;
614
+ top: 0;
615
+ left: 0;
616
+ bottom: 0;
617
+ width: 1px;
618
+ background-color: var(--semi-color-border);
619
+ display: block;
620
+ z-index: 103;
621
+ pointer-events: none;
622
+ }
595
623
  .semi-table-wrapper-rtl .semi-table-fixed > .semi-table-tbody > .semi-table-row-expand > .semi-table-row-cell > .semi-table-expand-inner, .semi-table-wrapper-rtl .semi-table-fixed > .semi-table-tbody > .semi-table-row-section > .semi-table-row-cell > .semi-table-section-inner {
596
624
  left: auto;
597
625
  right: 0;
@@ -555,6 +555,35 @@ $module: #{$prefix}-table;
555
555
  }
556
556
  }
557
557
 
558
+ // Fix #441: when horizontal scroll is enabled (e.g. scroll.x='101%') and table is NOT scrolled to the far right,
559
+ // the outer right border can be outside of viewport because the real right border is rendered by the
560
+ // last column cell border (which is scrollable). Draw an overlay right border on the container to keep it visible.
561
+ // When scrolled to the far right, `.semi-table-scroll-position-right` exists and we should not draw it to avoid
562
+ // double borders.
563
+ &:not(.#{$module}-scroll-position-right) {
564
+ & > .#{$module}-container {
565
+ &::after {
566
+ content: '';
567
+ position: absolute;
568
+ top: 0;
569
+ right: 0;
570
+ bottom: 0;
571
+ width: $width-table_base_border;
572
+ background-color: $color-table-border-default;
573
+ display: block;
574
+ // Make sure the overlay border stays above table content/fixed columns
575
+ z-index: $z-table_fixed_column + 2;
576
+ pointer-events: none;
577
+ }
578
+
579
+ // Ensure header shows a visible right border even when the container overlay
580
+ // is covered by native scrollbars in some browsers.
581
+ & > .#{$module}-header {
582
+ box-shadow: inset -$width-table_base_border 0 0 0 $color-table-border-default;
583
+ }
584
+ }
585
+ }
586
+
558
587
  :where(& > .#{$module}-container) {
559
588
  & > .#{$module}-body > .#{$module}-placeholder {
560
589
  border-right: $width-table_base_border $border-table_base-borderStyle $color-table-border-default;
@@ -589,6 +618,9 @@ $module: #{$prefix}-table;
589
618
  position: sticky;
590
619
  left: 0px;
591
620
  z-index: 1;
621
+ // In bordered mode, placeholder may receive an extra side border to complete the outer frame.
622
+ // Use border-box to avoid 1px horizontal overflow (and unwanted horizontal scrollbar) in empty data + scroll.y cases.
623
+ box-sizing: border-box;
592
624
  padding: #{$spacing-table-paddingY} #{$spacing-table-paddingX};
593
625
  color: $color-table_placeholder-text-default;
594
626
  font-size: #{$font-table_base-fontSize};
@@ -23,7 +23,7 @@
23
23
 
24
24
  &[x-placement='top'] {
25
25
  .#{$module-icon} {
26
- left: 50%;
26
+ left: var(--semi-tooltip-arrow-offset-x, 50%);
27
27
  transform: translateX(-50%);
28
28
  bottom: (-$height-tooltip_arrow + $spacing-tooltip_arrow_offset-y);
29
29
  }
@@ -61,7 +61,7 @@
61
61
  width: $width-tooltip_arrow_vertical;
62
62
  height: $height-tooltip_arrow_vertical;
63
63
  right: (-$width-tooltip_arrow_vertical + $spacing-tooltip_arrow_offset-x);
64
- top: 50%;
64
+ top: var(--semi-tooltip-arrow-offset-y, 50%);
65
65
  transform: translateY(-50%);
66
66
  }
67
67
 
@@ -93,7 +93,7 @@
93
93
  width: $width-tooltip_arrow_vertical;
94
94
  height: $height-tooltip_arrow_vertical;
95
95
  left: -$width-tooltip_arrow_vertical + $spacing-tooltip_arrow_offset-x;
96
- top: 50%;
96
+ top: var(--semi-tooltip-arrow-offset-y, 50%);
97
97
  transform: translateY(-50%) rotate(180deg);
98
98
  }
99
99
 
@@ -122,7 +122,7 @@
122
122
  &[x-placement='bottom'] {
123
123
  .#{$module-icon} {
124
124
  top: (-$height-tooltip_arrow + $spacing-tooltip_arrow_offset-y);
125
- left: 50%;
125
+ left: var(--semi-tooltip-arrow-offset-x, 50%);
126
126
  transform: translateX(-50%) rotate(180deg);
127
127
  }
128
128
 
@@ -683,11 +683,75 @@ class Tooltip extends _foundation.default {
683
683
  top = position.includes('Top') ? top + offsetY : top - offsetY;
684
684
  }
685
685
  }
686
+ // Handle arrowPointAtCenter for center positions (top/bottom/left/right)
687
+ // For center positions, the arrow needs to point at trigger center, not Popover center
688
+ let cssArrowOffsetX;
689
+ let cssArrowOffsetY;
690
+ if (showArrow) {
691
+ const isCenterPosition = ['top', 'bottom', 'left', 'right'].includes(position);
692
+ if (isCenterPosition) {
693
+ if (arrowPointAtCenter) {
694
+ // arrowPointAtCenter=true: arrow should point at trigger center
695
+ // Calculate arrow position relative to Popover left/top edge
696
+ if ((position === 'top' || position === 'bottom') && wrapperRect.width > 0) {
697
+ // Popover center is at `left`, trigger center is at `middleX`
698
+ // Arrow position from Popover left edge:
699
+ // = middleX - (left - wrapperRect.width/2)
700
+ // = middleX - left + wrapperRect.width/2
701
+ // Percentage: (middleX - left) / wrapperRect.width + 0.5
702
+ const arrowOffsetPercent = (middleX - left) / wrapperRect.width + 0.5;
703
+ // Clamp to valid range to prevent arrow going outside Popover
704
+ const minOffset = (horizontalArrowWidth / 2 + positionOffsetX) / wrapperRect.width;
705
+ const maxOffset = 1 - minOffset;
706
+ const clampedOffset = Math.max(minOffset, Math.min(maxOffset, arrowOffsetPercent));
707
+ // Only set CSS variable if different from default 50%
708
+ if (Math.abs(clampedOffset - 0.5) > 0.01) {
709
+ cssArrowOffsetX = `${clampedOffset * 100}%`;
710
+ }
711
+ }
712
+ if ((position === 'left' || position === 'right') && wrapperRect.height > 0) {
713
+ const arrowOffsetPercent = (middleY - top) / wrapperRect.height + 0.5;
714
+ const minOffset = (verticalArrowHeight / 2 + positionOffsetY) / wrapperRect.height;
715
+ const maxOffset = 1 - minOffset;
716
+ const clampedOffset = Math.max(minOffset, Math.min(maxOffset, arrowOffsetPercent));
717
+ if (Math.abs(clampedOffset - 0.5) > 0.01) {
718
+ cssArrowOffsetY = `${clampedOffset * 100}%`;
719
+ }
720
+ }
721
+ } else {
722
+ // arrowPointAtCenter=false: arrow should be at Popover edge
723
+ // Determine which edge based on trigger position in viewport
724
+ if ((position === 'top' || position === 'bottom') && wrapperRect.width > 0) {
725
+ const offsetXWithArrow = positionOffsetX + horizontalArrowWidth / 2;
726
+ if (isTriggerNearLeft) {
727
+ cssArrowOffsetX = `${offsetXWithArrow / wrapperRect.width * 100}%`;
728
+ } else {
729
+ cssArrowOffsetX = `${(wrapperRect.width - offsetXWithArrow) / wrapperRect.width * 100}%`;
730
+ }
731
+ }
732
+ if ((position === 'left' || position === 'right') && wrapperRect.height > 0) {
733
+ const offsetYWithArrow = positionOffsetY + verticalArrowHeight / 2;
734
+ if (isTriggerNearTop) {
735
+ cssArrowOffsetY = `${offsetYWithArrow / wrapperRect.height * 100}%`;
736
+ } else {
737
+ cssArrowOffsetY = `${(wrapperRect.height - offsetYWithArrow) / wrapperRect.height * 100}%`;
738
+ }
739
+ }
740
+ }
741
+ }
742
+ }
686
743
  // The left/top value here must be rounded, otherwise it will cause the small triangle to shake
687
744
  const style = {
688
745
  left: this._roundPixel(left),
689
746
  top: this._roundPixel(top)
690
747
  };
748
+ // Add CSS variables for arrow positioning
749
+ if (cssArrowOffsetX) {
750
+ style['--semi-tooltip-arrow-offset-x'] = cssArrowOffsetX;
751
+ }
752
+ if (cssArrowOffsetY) {
753
+ style['--semi-tooltip-arrow-offset-y'] = cssArrowOffsetY;
754
+ }
691
755
  let transform = '';
692
756
  if (translateX != null) {
693
757
  transform += `translateX(${translateX * 100}%) `;
@@ -91,7 +91,7 @@
91
91
  color: rgba(var(--semi-grey-7), 1);
92
92
  }
93
93
  .semi-tooltip-wrapper[x-placement=top] .semi-tooltip-icon-arrow {
94
- left: 50%;
94
+ left: var(--semi-tooltip-arrow-offset-x, 50%);
95
95
  transform: translateX(-50%);
96
96
  bottom: -6px;
97
97
  }
@@ -129,7 +129,7 @@
129
129
  width: 7px;
130
130
  height: 24px;
131
131
  right: -6px;
132
- top: 50%;
132
+ top: var(--semi-tooltip-arrow-offset-y, 50%);
133
133
  transform: translateY(-50%);
134
134
  }
135
135
  .semi-tooltip-wrapper[x-placement=left].semi-tooltip-with-arrow,
@@ -161,7 +161,7 @@
161
161
  width: 7px;
162
162
  height: 24px;
163
163
  left: -6px;
164
- top: 50%;
164
+ top: var(--semi-tooltip-arrow-offset-y, 50%);
165
165
  transform: translateY(-50%) rotate(180deg);
166
166
  }
167
167
  .semi-tooltip-wrapper[x-placement=right].semi-tooltip-with-arrow,
@@ -190,7 +190,7 @@
190
190
  }
191
191
  .semi-tooltip-wrapper[x-placement=bottom] .semi-tooltip-icon-arrow {
192
192
  top: -6px;
193
- left: 50%;
193
+ left: var(--semi-tooltip-arrow-offset-x, 50%);
194
194
  transform: translateX(-50%) rotate(180deg);
195
195
  }
196
196
  .semi-tooltip-wrapper[x-placement=bottom].semi-tooltip-with-arrow,
@@ -447,8 +447,11 @@ export default class CascaderFoundation extends BaseFoundation {
447
447
  } else if (selectedKeys.size && !multiple) {
448
448
  inputValue = this.renderDisplayText([...selectedKeys][0]);
449
449
  }
450
+ // Reset isSearching immediately in close() instead of relying on afterClose callback
451
+ // This prevents timing issues where open() clears inputValue but isSearching is still true
450
452
  this._adapter.updateStates({
451
- inputValue
453
+ inputValue,
454
+ isSearching: false
452
455
  });
453
456
  !multiple && this.toggle2SearchInput(false);
454
457
  !multiple && this._adapter.updateFocusState(false);
@@ -484,6 +484,8 @@ export default class FormFoundation extends BaseFoundation {
484
484
  */
485
485
  const formAllowEmpty = this.getProp('allowEmpty');
486
486
  // priority at Field level
487
+ // NOTE: Keep legacy semantics here to avoid implicit breaking changes.
488
+ // (Field-level allowEmpty overriding behavior is handled during register/restore paths.)
487
489
  const allowEmpty = fieldAllowEmpty ? fieldAllowEmpty : formAllowEmpty;
488
490
  ObjectUtil.set(this.data.values, field, value, allowEmpty);
489
491
  /**
@@ -76,7 +76,7 @@
76
76
  color: unset;
77
77
  }
78
78
  .semi-popover-wrapper[x-placement=top] .semi-popover-icon-arrow {
79
- left: 50%;
79
+ left: var(--semi-tooltip-arrow-offset-x, 50%);
80
80
  transform: translateX(-50%);
81
81
  bottom: -7px;
82
82
  }
@@ -114,7 +114,7 @@
114
114
  width: 8px;
115
115
  height: 24px;
116
116
  right: -7px;
117
- top: 50%;
117
+ top: var(--semi-tooltip-arrow-offset-y, 50%);
118
118
  transform: translateY(-50%);
119
119
  }
120
120
  .semi-popover-wrapper[x-placement=left].semi-popover-with-arrow,
@@ -146,7 +146,7 @@
146
146
  width: 8px;
147
147
  height: 24px;
148
148
  left: -7px;
149
- top: 50%;
149
+ top: var(--semi-tooltip-arrow-offset-y, 50%);
150
150
  transform: translateY(-50%) rotate(180deg);
151
151
  }
152
152
  .semi-popover-wrapper[x-placement=right].semi-popover-with-arrow,
@@ -175,7 +175,7 @@
175
175
  }
176
176
  .semi-popover-wrapper[x-placement=bottom] .semi-popover-icon-arrow {
177
177
  top: -7px;
178
- left: 50%;
178
+ left: var(--semi-tooltip-arrow-offset-x, 50%);
179
179
  transform: translateX(-50%) rotate(180deg);
180
180
  }
181
181
  .semi-popover-wrapper[x-placement=bottom].semi-popover-with-arrow,
@@ -144,6 +144,27 @@ $module: #{$prefix}-table;
144
144
  border-left: $width-table_base_border $border-table_base-borderStyle $color-table-border-default;
145
145
  }
146
146
  }
147
+
148
+ // Fix #441 (RTL): when horizontal scroll is enabled and table is NOT scrolled to the far left,
149
+ // the outer left border can be outside of viewport (the real border is rendered by the last column cell).
150
+ // Draw an overlay left border on the container to keep it visible.
151
+ // When scrolled to the far left, `.semi-table-scroll-position-left` exists and we should not draw it.
152
+ &:not(.#{$module}-scroll-position-left) {
153
+ & > .#{$module}-container {
154
+ &::after {
155
+ content: '';
156
+ position: absolute;
157
+ top: 0;
158
+ left: 0;
159
+ bottom: 0;
160
+ width: $width-table_base_border;
161
+ background-color: $color-table-border-default;
162
+ display: block;
163
+ z-index: $z-table_fixed_column + 2;
164
+ pointer-events: none;
165
+ }
166
+ }
167
+ }
147
168
  }
148
169
 
149
170
  &-fixed {
@@ -416,6 +416,21 @@
416
416
  border-right: 1px solid var(--semi-color-border);
417
417
  border-bottom: 1px solid var(--semi-color-border);
418
418
  }
419
+ .semi-table-bordered:not(.semi-table-scroll-position-right) > .semi-table-container::after {
420
+ content: "";
421
+ position: absolute;
422
+ top: 0;
423
+ right: 0;
424
+ bottom: 0;
425
+ width: 1px;
426
+ background-color: var(--semi-color-border);
427
+ display: block;
428
+ z-index: 103;
429
+ pointer-events: none;
430
+ }
431
+ .semi-table-bordered:not(.semi-table-scroll-position-right) > .semi-table-container > .semi-table-header {
432
+ box-shadow: inset-1px 0 0 0 var(--semi-color-border);
433
+ }
419
434
  :where(.semi-table-bordered > .semi-table-container) > .semi-table-body > .semi-table-placeholder {
420
435
  border-right: 1px solid var(--semi-color-border);
421
436
  }
@@ -435,6 +450,7 @@
435
450
  position: sticky;
436
451
  left: 0px;
437
452
  z-index: 1;
453
+ box-sizing: border-box;
438
454
  padding: 16px 12px;
439
455
  color: var(--semi-color-text-2);
440
456
  font-size: 14px;
@@ -592,6 +608,18 @@
592
608
  border-right: 0;
593
609
  border-left: 1px solid var(--semi-color-border);
594
610
  }
611
+ .semi-table-wrapper-rtl .semi-table-bordered:not(.semi-table-scroll-position-left) > .semi-table-container::after {
612
+ content: "";
613
+ position: absolute;
614
+ top: 0;
615
+ left: 0;
616
+ bottom: 0;
617
+ width: 1px;
618
+ background-color: var(--semi-color-border);
619
+ display: block;
620
+ z-index: 103;
621
+ pointer-events: none;
622
+ }
595
623
  .semi-table-wrapper-rtl .semi-table-fixed > .semi-table-tbody > .semi-table-row-expand > .semi-table-row-cell > .semi-table-expand-inner, .semi-table-wrapper-rtl .semi-table-fixed > .semi-table-tbody > .semi-table-row-section > .semi-table-row-cell > .semi-table-section-inner {
596
624
  left: auto;
597
625
  right: 0;
@@ -555,6 +555,35 @@ $module: #{$prefix}-table;
555
555
  }
556
556
  }
557
557
 
558
+ // Fix #441: when horizontal scroll is enabled (e.g. scroll.x='101%') and table is NOT scrolled to the far right,
559
+ // the outer right border can be outside of viewport because the real right border is rendered by the
560
+ // last column cell border (which is scrollable). Draw an overlay right border on the container to keep it visible.
561
+ // When scrolled to the far right, `.semi-table-scroll-position-right` exists and we should not draw it to avoid
562
+ // double borders.
563
+ &:not(.#{$module}-scroll-position-right) {
564
+ & > .#{$module}-container {
565
+ &::after {
566
+ content: '';
567
+ position: absolute;
568
+ top: 0;
569
+ right: 0;
570
+ bottom: 0;
571
+ width: $width-table_base_border;
572
+ background-color: $color-table-border-default;
573
+ display: block;
574
+ // Make sure the overlay border stays above table content/fixed columns
575
+ z-index: $z-table_fixed_column + 2;
576
+ pointer-events: none;
577
+ }
578
+
579
+ // Ensure header shows a visible right border even when the container overlay
580
+ // is covered by native scrollbars in some browsers.
581
+ & > .#{$module}-header {
582
+ box-shadow: inset -$width-table_base_border 0 0 0 $color-table-border-default;
583
+ }
584
+ }
585
+ }
586
+
558
587
  :where(& > .#{$module}-container) {
559
588
  & > .#{$module}-body > .#{$module}-placeholder {
560
589
  border-right: $width-table_base_border $border-table_base-borderStyle $color-table-border-default;
@@ -589,6 +618,9 @@ $module: #{$prefix}-table;
589
618
  position: sticky;
590
619
  left: 0px;
591
620
  z-index: 1;
621
+ // In bordered mode, placeholder may receive an extra side border to complete the outer frame.
622
+ // Use border-box to avoid 1px horizontal overflow (and unwanted horizontal scrollbar) in empty data + scroll.y cases.
623
+ box-sizing: border-box;
592
624
  padding: #{$spacing-table-paddingY} #{$spacing-table-paddingX};
593
625
  color: $color-table_placeholder-text-default;
594
626
  font-size: #{$font-table_base-fontSize};
@@ -23,7 +23,7 @@
23
23
 
24
24
  &[x-placement='top'] {
25
25
  .#{$module-icon} {
26
- left: 50%;
26
+ left: var(--semi-tooltip-arrow-offset-x, 50%);
27
27
  transform: translateX(-50%);
28
28
  bottom: (-$height-tooltip_arrow + $spacing-tooltip_arrow_offset-y);
29
29
  }
@@ -61,7 +61,7 @@
61
61
  width: $width-tooltip_arrow_vertical;
62
62
  height: $height-tooltip_arrow_vertical;
63
63
  right: (-$width-tooltip_arrow_vertical + $spacing-tooltip_arrow_offset-x);
64
- top: 50%;
64
+ top: var(--semi-tooltip-arrow-offset-y, 50%);
65
65
  transform: translateY(-50%);
66
66
  }
67
67
 
@@ -93,7 +93,7 @@
93
93
  width: $width-tooltip_arrow_vertical;
94
94
  height: $height-tooltip_arrow_vertical;
95
95
  left: -$width-tooltip_arrow_vertical + $spacing-tooltip_arrow_offset-x;
96
- top: 50%;
96
+ top: var(--semi-tooltip-arrow-offset-y, 50%);
97
97
  transform: translateY(-50%) rotate(180deg);
98
98
  }
99
99
 
@@ -122,7 +122,7 @@
122
122
  &[x-placement='bottom'] {
123
123
  .#{$module-icon} {
124
124
  top: (-$height-tooltip_arrow + $spacing-tooltip_arrow_offset-y);
125
- left: 50%;
125
+ left: var(--semi-tooltip-arrow-offset-x, 50%);
126
126
  transform: translateX(-50%) rotate(180deg);
127
127
  }
128
128
 
@@ -676,11 +676,75 @@ export default class Tooltip extends BaseFoundation {
676
676
  top = position.includes('Top') ? top + offsetY : top - offsetY;
677
677
  }
678
678
  }
679
+ // Handle arrowPointAtCenter for center positions (top/bottom/left/right)
680
+ // For center positions, the arrow needs to point at trigger center, not Popover center
681
+ let cssArrowOffsetX;
682
+ let cssArrowOffsetY;
683
+ if (showArrow) {
684
+ const isCenterPosition = ['top', 'bottom', 'left', 'right'].includes(position);
685
+ if (isCenterPosition) {
686
+ if (arrowPointAtCenter) {
687
+ // arrowPointAtCenter=true: arrow should point at trigger center
688
+ // Calculate arrow position relative to Popover left/top edge
689
+ if ((position === 'top' || position === 'bottom') && wrapperRect.width > 0) {
690
+ // Popover center is at `left`, trigger center is at `middleX`
691
+ // Arrow position from Popover left edge:
692
+ // = middleX - (left - wrapperRect.width/2)
693
+ // = middleX - left + wrapperRect.width/2
694
+ // Percentage: (middleX - left) / wrapperRect.width + 0.5
695
+ const arrowOffsetPercent = (middleX - left) / wrapperRect.width + 0.5;
696
+ // Clamp to valid range to prevent arrow going outside Popover
697
+ const minOffset = (horizontalArrowWidth / 2 + positionOffsetX) / wrapperRect.width;
698
+ const maxOffset = 1 - minOffset;
699
+ const clampedOffset = Math.max(minOffset, Math.min(maxOffset, arrowOffsetPercent));
700
+ // Only set CSS variable if different from default 50%
701
+ if (Math.abs(clampedOffset - 0.5) > 0.01) {
702
+ cssArrowOffsetX = `${clampedOffset * 100}%`;
703
+ }
704
+ }
705
+ if ((position === 'left' || position === 'right') && wrapperRect.height > 0) {
706
+ const arrowOffsetPercent = (middleY - top) / wrapperRect.height + 0.5;
707
+ const minOffset = (verticalArrowHeight / 2 + positionOffsetY) / wrapperRect.height;
708
+ const maxOffset = 1 - minOffset;
709
+ const clampedOffset = Math.max(minOffset, Math.min(maxOffset, arrowOffsetPercent));
710
+ if (Math.abs(clampedOffset - 0.5) > 0.01) {
711
+ cssArrowOffsetY = `${clampedOffset * 100}%`;
712
+ }
713
+ }
714
+ } else {
715
+ // arrowPointAtCenter=false: arrow should be at Popover edge
716
+ // Determine which edge based on trigger position in viewport
717
+ if ((position === 'top' || position === 'bottom') && wrapperRect.width > 0) {
718
+ const offsetXWithArrow = positionOffsetX + horizontalArrowWidth / 2;
719
+ if (isTriggerNearLeft) {
720
+ cssArrowOffsetX = `${offsetXWithArrow / wrapperRect.width * 100}%`;
721
+ } else {
722
+ cssArrowOffsetX = `${(wrapperRect.width - offsetXWithArrow) / wrapperRect.width * 100}%`;
723
+ }
724
+ }
725
+ if ((position === 'left' || position === 'right') && wrapperRect.height > 0) {
726
+ const offsetYWithArrow = positionOffsetY + verticalArrowHeight / 2;
727
+ if (isTriggerNearTop) {
728
+ cssArrowOffsetY = `${offsetYWithArrow / wrapperRect.height * 100}%`;
729
+ } else {
730
+ cssArrowOffsetY = `${(wrapperRect.height - offsetYWithArrow) / wrapperRect.height * 100}%`;
731
+ }
732
+ }
733
+ }
734
+ }
735
+ }
679
736
  // The left/top value here must be rounded, otherwise it will cause the small triangle to shake
680
737
  const style = {
681
738
  left: this._roundPixel(left),
682
739
  top: this._roundPixel(top)
683
740
  };
741
+ // Add CSS variables for arrow positioning
742
+ if (cssArrowOffsetX) {
743
+ style['--semi-tooltip-arrow-offset-x'] = cssArrowOffsetX;
744
+ }
745
+ if (cssArrowOffsetY) {
746
+ style['--semi-tooltip-arrow-offset-y'] = cssArrowOffsetY;
747
+ }
684
748
  let transform = '';
685
749
  if (translateX != null) {
686
750
  transform += `translateX(${translateX * 100}%) `;
@@ -91,7 +91,7 @@
91
91
  color: rgba(var(--semi-grey-7), 1);
92
92
  }
93
93
  .semi-tooltip-wrapper[x-placement=top] .semi-tooltip-icon-arrow {
94
- left: 50%;
94
+ left: var(--semi-tooltip-arrow-offset-x, 50%);
95
95
  transform: translateX(-50%);
96
96
  bottom: -6px;
97
97
  }
@@ -129,7 +129,7 @@
129
129
  width: 7px;
130
130
  height: 24px;
131
131
  right: -6px;
132
- top: 50%;
132
+ top: var(--semi-tooltip-arrow-offset-y, 50%);
133
133
  transform: translateY(-50%);
134
134
  }
135
135
  .semi-tooltip-wrapper[x-placement=left].semi-tooltip-with-arrow,
@@ -161,7 +161,7 @@
161
161
  width: 7px;
162
162
  height: 24px;
163
163
  left: -6px;
164
- top: 50%;
164
+ top: var(--semi-tooltip-arrow-offset-y, 50%);
165
165
  transform: translateY(-50%) rotate(180deg);
166
166
  }
167
167
  .semi-tooltip-wrapper[x-placement=right].semi-tooltip-with-arrow,
@@ -190,7 +190,7 @@
190
190
  }
191
191
  .semi-tooltip-wrapper[x-placement=bottom] .semi-tooltip-icon-arrow {
192
192
  top: -6px;
193
- left: 50%;
193
+ left: var(--semi-tooltip-arrow-offset-x, 50%);
194
194
  transform: translateX(-50%) rotate(180deg);
195
195
  }
196
196
  .semi-tooltip-wrapper[x-placement=bottom].semi-tooltip-with-arrow,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@douyinfe/semi-foundation",
3
- "version": "2.98.0",
3
+ "version": "2.99.0",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "clean": "rimraf lib",
@@ -14225,8 +14225,8 @@
14225
14225
  }
14226
14226
  },
14227
14227
  "dependencies": {
14228
- "@douyinfe/semi-animation": "2.98.0",
14229
- "@douyinfe/semi-json-viewer-core": "2.98.0",
14228
+ "@douyinfe/semi-animation": "2.99.0",
14229
+ "@douyinfe/semi-json-viewer-core": "2.99.0",
14230
14230
  "@mdx-js/mdx": "^3.0.1",
14231
14231
  "async-validator": "^3.5.0",
14232
14232
  "classnames": "^2.2.6",
@@ -14247,7 +14247,7 @@
14247
14247
  "*.scss",
14248
14248
  "*.css"
14249
14249
  ],
14250
- "gitHead": "e33a947a4e0745a7ad15d3e773355cc19d23b174",
14250
+ "gitHead": "c62861b574b51ae39afe7da796ab4f14d1462ef2",
14251
14251
  "devDependencies": {
14252
14252
  "@babel/plugin-transform-runtime": "^7.15.8",
14253
14253
  "@babel/preset-env": "^7.15.8",
package/table/rtl.scss CHANGED
@@ -144,6 +144,27 @@ $module: #{$prefix}-table;
144
144
  border-left: $width-table_base_border $border-table_base-borderStyle $color-table-border-default;
145
145
  }
146
146
  }
147
+
148
+ // Fix #441 (RTL): when horizontal scroll is enabled and table is NOT scrolled to the far left,
149
+ // the outer left border can be outside of viewport (the real border is rendered by the last column cell).
150
+ // Draw an overlay left border on the container to keep it visible.
151
+ // When scrolled to the far left, `.semi-table-scroll-position-left` exists and we should not draw it.
152
+ &:not(.#{$module}-scroll-position-left) {
153
+ & > .#{$module}-container {
154
+ &::after {
155
+ content: '';
156
+ position: absolute;
157
+ top: 0;
158
+ left: 0;
159
+ bottom: 0;
160
+ width: $width-table_base_border;
161
+ background-color: $color-table-border-default;
162
+ display: block;
163
+ z-index: $z-table_fixed_column + 2;
164
+ pointer-events: none;
165
+ }
166
+ }
167
+ }
147
168
  }
148
169
 
149
170
  &-fixed {
package/table/table.scss CHANGED
@@ -555,6 +555,35 @@ $module: #{$prefix}-table;
555
555
  }
556
556
  }
557
557
 
558
+ // Fix #441: when horizontal scroll is enabled (e.g. scroll.x='101%') and table is NOT scrolled to the far right,
559
+ // the outer right border can be outside of viewport because the real right border is rendered by the
560
+ // last column cell border (which is scrollable). Draw an overlay right border on the container to keep it visible.
561
+ // When scrolled to the far right, `.semi-table-scroll-position-right` exists and we should not draw it to avoid
562
+ // double borders.
563
+ &:not(.#{$module}-scroll-position-right) {
564
+ & > .#{$module}-container {
565
+ &::after {
566
+ content: '';
567
+ position: absolute;
568
+ top: 0;
569
+ right: 0;
570
+ bottom: 0;
571
+ width: $width-table_base_border;
572
+ background-color: $color-table-border-default;
573
+ display: block;
574
+ // Make sure the overlay border stays above table content/fixed columns
575
+ z-index: $z-table_fixed_column + 2;
576
+ pointer-events: none;
577
+ }
578
+
579
+ // Ensure header shows a visible right border even when the container overlay
580
+ // is covered by native scrollbars in some browsers.
581
+ & > .#{$module}-header {
582
+ box-shadow: inset -$width-table_base_border 0 0 0 $color-table-border-default;
583
+ }
584
+ }
585
+ }
586
+
558
587
  :where(& > .#{$module}-container) {
559
588
  & > .#{$module}-body > .#{$module}-placeholder {
560
589
  border-right: $width-table_base_border $border-table_base-borderStyle $color-table-border-default;
@@ -589,6 +618,9 @@ $module: #{$prefix}-table;
589
618
  position: sticky;
590
619
  left: 0px;
591
620
  z-index: 1;
621
+ // In bordered mode, placeholder may receive an extra side border to complete the outer frame.
622
+ // Use border-box to avoid 1px horizontal overflow (and unwanted horizontal scrollbar) in empty data + scroll.y cases.
623
+ box-sizing: border-box;
592
624
  padding: #{$spacing-table-paddingY} #{$spacing-table-paddingX};
593
625
  color: $color-table_placeholder-text-default;
594
626
  font-size: #{$font-table_base-fontSize};
@@ -23,7 +23,7 @@
23
23
 
24
24
  &[x-placement='top'] {
25
25
  .#{$module-icon} {
26
- left: 50%;
26
+ left: var(--semi-tooltip-arrow-offset-x, 50%);
27
27
  transform: translateX(-50%);
28
28
  bottom: (-$height-tooltip_arrow + $spacing-tooltip_arrow_offset-y);
29
29
  }
@@ -61,7 +61,7 @@
61
61
  width: $width-tooltip_arrow_vertical;
62
62
  height: $height-tooltip_arrow_vertical;
63
63
  right: (-$width-tooltip_arrow_vertical + $spacing-tooltip_arrow_offset-x);
64
- top: 50%;
64
+ top: var(--semi-tooltip-arrow-offset-y, 50%);
65
65
  transform: translateY(-50%);
66
66
  }
67
67
 
@@ -93,7 +93,7 @@
93
93
  width: $width-tooltip_arrow_vertical;
94
94
  height: $height-tooltip_arrow_vertical;
95
95
  left: -$width-tooltip_arrow_vertical + $spacing-tooltip_arrow_offset-x;
96
- top: 50%;
96
+ top: var(--semi-tooltip-arrow-offset-y, 50%);
97
97
  transform: translateY(-50%) rotate(180deg);
98
98
  }
99
99
 
@@ -122,7 +122,7 @@
122
122
  &[x-placement='bottom'] {
123
123
  .#{$module-icon} {
124
124
  top: (-$height-tooltip_arrow + $spacing-tooltip_arrow_offset-y);
125
- left: 50%;
125
+ left: var(--semi-tooltip-arrow-offset-x, 50%);
126
126
  transform: translateX(-50%) rotate(180deg);
127
127
  }
128
128
 
@@ -674,12 +674,90 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
674
674
  }
675
675
  }
676
676
 
677
+ // Handle arrowPointAtCenter for center positions (top/bottom/left/right)
678
+ // For center positions, the arrow needs to point at trigger center, not Popover center
679
+ let cssArrowOffsetX: string | undefined;
680
+ let cssArrowOffsetY: string | undefined;
681
+
682
+ if (showArrow) {
683
+ const isCenterPosition = ['top', 'bottom', 'left', 'right'].includes(position);
684
+
685
+ if (isCenterPosition) {
686
+ if (arrowPointAtCenter) {
687
+ // arrowPointAtCenter=true: arrow should point at trigger center
688
+ // Calculate arrow position relative to Popover left/top edge
689
+
690
+ if ((position === 'top' || position === 'bottom') && wrapperRect.width > 0) {
691
+ // Popover center is at `left`, trigger center is at `middleX`
692
+ // Arrow position from Popover left edge:
693
+ // = middleX - (left - wrapperRect.width/2)
694
+ // = middleX - left + wrapperRect.width/2
695
+ // Percentage: (middleX - left) / wrapperRect.width + 0.5
696
+ const arrowOffsetPercent = (middleX - left) / wrapperRect.width + 0.5;
697
+
698
+ // Clamp to valid range to prevent arrow going outside Popover
699
+ const minOffset = (horizontalArrowWidth / 2 + positionOffsetX) / wrapperRect.width;
700
+ const maxOffset = 1 - minOffset;
701
+ const clampedOffset = Math.max(minOffset, Math.min(maxOffset, arrowOffsetPercent));
702
+
703
+ // Only set CSS variable if different from default 50%
704
+ if (Math.abs(clampedOffset - 0.5) > 0.01) {
705
+ cssArrowOffsetX = `${clampedOffset * 100}%`;
706
+ }
707
+ }
708
+
709
+ if ((position === 'left' || position === 'right') && wrapperRect.height > 0) {
710
+ const arrowOffsetPercent = (middleY - top) / wrapperRect.height + 0.5;
711
+
712
+ const minOffset = (verticalArrowHeight / 2 + positionOffsetY) / wrapperRect.height;
713
+ const maxOffset = 1 - minOffset;
714
+ const clampedOffset = Math.max(minOffset, Math.min(maxOffset, arrowOffsetPercent));
715
+
716
+ if (Math.abs(clampedOffset - 0.5) > 0.01) {
717
+ cssArrowOffsetY = `${clampedOffset * 100}%`;
718
+ }
719
+ }
720
+ } else {
721
+ // arrowPointAtCenter=false: arrow should be at Popover edge
722
+ // Determine which edge based on trigger position in viewport
723
+
724
+ if ((position === 'top' || position === 'bottom') && wrapperRect.width > 0) {
725
+ const offsetXWithArrow = positionOffsetX + horizontalArrowWidth / 2;
726
+
727
+ if (isTriggerNearLeft) {
728
+ cssArrowOffsetX = `${(offsetXWithArrow / wrapperRect.width) * 100}%`;
729
+ } else {
730
+ cssArrowOffsetX = `${((wrapperRect.width - offsetXWithArrow) / wrapperRect.width) * 100}%`;
731
+ }
732
+ }
733
+
734
+ if ((position === 'left' || position === 'right') && wrapperRect.height > 0) {
735
+ const offsetYWithArrow = positionOffsetY + verticalArrowHeight / 2;
736
+
737
+ if (isTriggerNearTop) {
738
+ cssArrowOffsetY = `${(offsetYWithArrow / wrapperRect.height) * 100}%`;
739
+ } else {
740
+ cssArrowOffsetY = `${((wrapperRect.height - offsetYWithArrow) / wrapperRect.height) * 100}%`;
741
+ }
742
+ }
743
+ }
744
+ }
745
+ }
746
+
677
747
  // The left/top value here must be rounded, otherwise it will cause the small triangle to shake
678
748
  const style: Record<string, string | number> = {
679
749
  left: this._roundPixel(left),
680
750
  top: this._roundPixel(top),
681
751
  };
682
752
 
753
+ // Add CSS variables for arrow positioning
754
+ if (cssArrowOffsetX) {
755
+ style['--semi-tooltip-arrow-offset-x'] = cssArrowOffsetX;
756
+ }
757
+ if (cssArrowOffsetY) {
758
+ style['--semi-tooltip-arrow-offset-y'] = cssArrowOffsetY;
759
+ }
760
+
683
761
  let transform = '';
684
762
 
685
763
  if (translateX != null) {