• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SAP / ui5-webcomponents-react / 9193335355

22 May 2024 02:35PM CUT coverage: 88.473% (-0.03%) from 88.506%
9193335355

Pull #5836

github

web-flow
Merge df0b87a11 into 25be46dfb
Pull Request #5836: fix(ExpandableText): announce state of "Show More" button correctly with screen readers

3032 of 3990 branches covered (75.99%)

5419 of 6125 relevant lines covered (88.47%)

29034.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.33
/packages/main/src/components/ExpandableText/index.tsx
1
'use client';
2

3
import { useI18nBundle, useIsomorphicId, useStylesheet } from '@ui5/webcomponents-react-base';
4
import { clsx } from 'clsx';
5
import React, { forwardRef, useEffect, useRef, useState } from 'react';
6
import { createPortal } from 'react-dom';
7
import { CLOSE_POPOVER, SHOW_FULL_TEXT, SHOW_LESS, SHOW_MORE } from '../../i18n/i18n-defaults.js';
8
import { useCanRenderPortal } from '../../internal/ssr.js';
9
import { getUi5TagWithSuffix } from '../../internal/utils.js';
10
import type { CommonProps } from '../../types/index.js';
11
import type { LinkDomRef } from '../../webComponents/index.js';
12
import { Link } from '../../webComponents/index.js';
13
import { ResponsivePopover } from '../../webComponents/ResponsivePopover/index.js';
14
import type { TextPropTypes } from '../Text/index.js';
15
import { Text } from '../Text/index.js';
16
import { classNames, styleData } from './ExpandableText.module.css.js';
17

18
export interface ExpandableTextPropTypes
19
  extends Omit<TextPropTypes, 'maxLines' | 'wrapping' | 'children'>,
20
    CommonProps {
21
  /**
22
   * Determines the text to be displayed.
23
   */
24
  children?: string;
25
  /**
26
   * Specifies the maximum number of characters from the beginning of the text field that are shown initially.
27
   *
28
   * @default 100
29
   */
30
  maxCharacters?: number;
31
  /**
32
   * Determines if the full text should be displayed inside a `ResponsivePopover` or in-place.
33
   */
34
  showOverflowInPopover?: boolean;
35
  /**
36
   * Defines where modals are rendered into via `React.createPortal`.
37
   *
38
   * You can find out more about this [here](https://sap.github.io/ui5-webcomponents-react/?path=/docs/knowledge-base-working-with-portals--page).
39
   *
40
   * @default document.body
41
   */
42
  portalContainer?: Element;
43
}
44

45
/**
46
 * The `ExpandableText` component can be used to display long texts inside a table, list or form.
47
 *
48
 * Initially, only the first characters from the text are shown with a "Show More" link which allows the full text to be displayed. The `showOverflowInPopover` property determines if the full text will be displayed expanded in place (default) or in a popover (`showOverflowInPopover: true`). If the text is expanded a "Show Less" link is displayed, which allows collapsing the text field.
49
 *
50
 * @since 1.23.0
51
 */
52
const ExpandableText = forwardRef<HTMLSpanElement, ExpandableTextPropTypes>((props, ref) => {
445✔
53
  const {
54
    children,
55
    emptyIndicator,
56
    renderWhitespace,
57
    hyphenated,
58
    showOverflowInPopover,
59
    maxCharacters = 100,
61✔
60
    portalContainer,
61
    className,
62
    ...rest
63
  } = props;
116✔
64

65
  useStylesheet(styleData, ExpandableText.displayName);
116✔
66

67
  const [collapsed, setCollapsed] = useState(true);
116✔
68
  const [popoverOpen, setPopoverOpen] = useState(false);
116✔
69
  const linkRef = useRef<LinkDomRef>(null);
116✔
70
  const uniqueId = useIsomorphicId();
116✔
71
  const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
116✔
72
  const trimmedChildren = renderWhitespace ? children : children?.replace(/\s+/g, ' ').trim();
116✔
73
  const isOverflow = trimmedChildren?.length >= maxCharacters;
116✔
74
  const strippedChildren =
75
    isOverflow && (collapsed || showOverflowInPopover) ? trimmedChildren?.slice(0, maxCharacters) : children;
116✔
76

77
  const handleClick = () => {
116✔
78
    if (showOverflowInPopover) {
14✔
79
      setPopoverOpen((prev) => !prev);
6✔
80
    }
81
    setCollapsed((prev) => !prev);
14✔
82
  };
83

84
  const closePopover = () => {
116✔
85
    setCollapsed(true);
×
86
    setPopoverOpen(false);
×
87
  };
88

89
  useEffect(() => {
116✔
90
    const tagName = getUi5TagWithSuffix('ui5-link');
53✔
91
    void customElements.whenDefined(tagName).then(() => {
53✔
92
      if (linkRef.current) {
53✔
93
        if (showOverflowInPopover) {
36✔
94
          linkRef.current.accessibilityAttributes = { hasPopup: 'Dialog' };
9✔
95
        } else {
96
          linkRef.current.accessibilityAttributes = { expanded: !collapsed };
27✔
97
        }
98
      }
99
    });
100
  }, [collapsed, showOverflowInPopover]);
101

102
  const canRenderPortal = useCanRenderPortal();
116✔
103
  if (showOverflowInPopover && !canRenderPortal) {
116✔
104
    return null;
3✔
105
  }
106
  return (
113✔
107
    <span className={clsx(classNames.expandableText, className)} {...rest} ref={ref}>
108
      <Text
109
        emptyIndicator={emptyIndicator}
110
        renderWhitespace={renderWhitespace}
111
        hyphenated={hyphenated}
112
        className={classNames.text}
113
      >
114
        {strippedChildren}
115
      </Text>
116
      {isOverflow && (
190✔
117
        <>
118
          <span className={classNames.ellipsis}>{showOverflowInPopover || collapsed ? '… ' : ' '}</span>
219✔
119
          <Link
120
            accessibleName={
121
              showOverflowInPopover
77✔
122
                ? collapsed
12✔
123
                  ? i18nBundle.getText(SHOW_FULL_TEXT)
124
                  : i18nBundle.getText(CLOSE_POPOVER)
125
                : undefined
126
            }
127
            accessibleRole="button"
128
            onClick={handleClick}
129
            ref={linkRef}
130
            id={`${uniqueId}-link`}
131
          >
132
            {collapsed ? i18nBundle.getText(SHOW_MORE) : i18nBundle.getText(SHOW_LESS)}
77✔
133
          </Link>
134
        </>
135
      )}
136
      {showOverflowInPopover &&
128✔
137
        popoverOpen &&
138
        createPortal(
139
          <ResponsivePopover
140
            opener={`${uniqueId}-link`}
141
            open
142
            onAfterClose={closePopover}
143
            className={classNames.popover}
144
          >
145
            <Text renderWhitespace={renderWhitespace} hyphenated={hyphenated} className={classNames.text}>
146
              {children}
147
            </Text>
148
          </ResponsivePopover>,
149
          portalContainer ?? document.body
6✔
150
        )}
151
    </span>
152
  );
153
});
154

155
ExpandableText.displayName = 'ExpandableText';
445✔
156

157
export { ExpandableText };
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc