@jetbrains/ring-ui 7.0.93 → 7.0.95

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.
@@ -80,6 +80,7 @@ export interface AuthConfig extends TokenValidatorConfig {
80
80
  translations?: AuthTranslations | null | undefined;
81
81
  userParams?: RequestParams | undefined;
82
82
  waitForRedirectTimeout: number;
83
+ rpInitiatedLogout: boolean;
83
84
  }
84
85
  type AuthPayloadMap = {
85
86
  userChange: [AuthUser | undefined | void, void];
@@ -120,6 +121,7 @@ declare class Auth implements HTTPAuth {
120
121
  static DEFAULT_CONFIG: Omit<AuthConfig, "serverUri">;
121
122
  static API_PATH: string;
122
123
  static API_AUTH_PATH: string;
124
+ static API_LOGOUT_PATH: string;
123
125
  static API_PROFILE_PATH: string;
124
126
  static CLOSE_BACKEND_DOWN_MESSAGE: string;
125
127
  static CLOSE_WINDOW_MESSAGE: string;
@@ -187,7 +189,10 @@ declare class Auth implements HTTPAuth {
187
189
  private _extractErrorMessage;
188
190
  private _showBackendDownDialog;
189
191
  /**
190
- * Wipe accessToken and redirect to auth page with required authorization
192
+ * Wipe accessToken and redirect to logout endpoint.
193
+ * Uses RP-initiated logout flow (oauth2/logout) when rpInitiatedLogout config is enabled,
194
+ * falls back to oauth2/auth redirect otherwise.
195
+ * See: https://youtrack.jetbrains.com/projects/HUB/articles/HUB-A-43#rp-initiated-logout
191
196
  */
192
197
  logout(extraParams?: Record<string, unknown>): Promise<void>;
193
198
  private _runEmbeddedLogin;
@@ -43,12 +43,14 @@ const DEFAULT_CONFIG = {
43
43
  onBackendDown: () => () => { },
44
44
  defaultExpiresIn: DEFAULT_EXPIRES_TIMEOUT,
45
45
  waitForRedirectTimeout: DEFAULT_WAIT_FOR_REDIRECT_TIMEOUT,
46
+ rpInitiatedLogout: true,
46
47
  translations: null,
47
48
  };
48
49
  class Auth {
49
50
  static DEFAULT_CONFIG = DEFAULT_CONFIG;
50
51
  static API_PATH = 'api/rest/';
51
52
  static API_AUTH_PATH = 'oauth2/auth';
53
+ static API_LOGOUT_PATH = 'oauth2/logout';
52
54
  static API_PROFILE_PATH = 'users/me';
53
55
  static CLOSE_BACKEND_DOWN_MESSAGE = 'backend-check-succeeded';
54
56
  static CLOSE_WINDOW_MESSAGE = 'close-login-window';
@@ -106,6 +108,7 @@ class Auth {
106
108
  this._domainStorage = new AuthStorage({ messagePrefix: 'domain-message-' });
107
109
  this._requestBuilder = new AuthRequestBuilder({
108
110
  authorization: this.config.serverUri + Auth.API_PATH + Auth.API_AUTH_PATH,
111
+ logout: this.config.serverUri + Auth.API_PATH + Auth.API_LOGOUT_PATH,
109
112
  clientId,
110
113
  redirect,
111
114
  redirectUri,
@@ -633,20 +636,24 @@ class Auth {
633
636
  });
634
637
  }
635
638
  /**
636
- * Wipe accessToken and redirect to auth page with required authorization
639
+ * Wipe accessToken and redirect to logout endpoint.
640
+ * Uses RP-initiated logout flow (oauth2/logout) when rpInitiatedLogout config is enabled,
641
+ * falls back to oauth2/auth redirect otherwise.
642
+ * See: https://youtrack.jetbrains.com/projects/HUB/articles/HUB-A-43#rp-initiated-logout
637
643
  */
638
644
  async logout(extraParams) {
639
- const requestParams = {
640
- request_credentials: 'required',
641
- ...extraParams,
642
- };
643
645
  await this._checkBackendsStatusesIfEnabled();
644
646
  await this.listeners.trigger('logout');
645
647
  this._updateDomainUser(null);
646
648
  await this._storage?.wipeToken();
647
- const authRequest = await this._requestBuilder?.prepareAuthRequest(requestParams);
648
- if (authRequest) {
649
- this._redirectCurrentPage(authRequest.url);
649
+ const request = this.config.rpInitiatedLogout
650
+ ? await this._requestBuilder?.prepareLogoutRequest(extraParams)
651
+ : await this._requestBuilder?.prepareAuthRequest({
652
+ request_credentials: 'required',
653
+ ...extraParams,
654
+ });
655
+ if (request) {
656
+ this._redirectCurrentPage(request.url);
650
657
  }
651
658
  }
652
659
  async _runEmbeddedLogin() {
@@ -3,6 +3,7 @@ import type { AuthState } from './storage';
3
3
  import type AuthStorage from './storage';
4
4
  export interface AuthRequestBuilderConfig {
5
5
  authorization: string;
6
+ logout?: string | null | undefined;
6
7
  redirectUri?: string | null | undefined;
7
8
  requestCredentials?: string | null | undefined;
8
9
  clientId?: string | null | undefined;
@@ -38,6 +39,17 @@ export default class AuthRequestBuilder {
38
39
  url: string;
39
40
  stateId: string;
40
41
  }>;
42
+ /**
43
+ * Build a logout URL for RP-initiated logout flow.
44
+ * See: https://youtrack.jetbrains.com/projects/HUB/articles/HUB-A-43#rp-initiated-logout
45
+ *
46
+ * @param {object=} extraParams additional query parameters for logout request
47
+ * @return {Promise.<{url: string, stateId: string}>} logout URL with required parameters
48
+ */
49
+ prepareLogoutRequest(extraParams?: Record<string, unknown> | null | undefined): Promise<{
50
+ url: string;
51
+ stateId: string;
52
+ }>;
41
53
  /**
42
54
  * @param {string} id
43
55
  * @param {StoredState} storedState
@@ -51,6 +51,33 @@ export default class AuthRequestBuilder {
51
51
  stateId,
52
52
  };
53
53
  }
54
+ /**
55
+ * Build a logout URL for RP-initiated logout flow.
56
+ * See: https://youtrack.jetbrains.com/projects/HUB/articles/HUB-A-43#rp-initiated-logout
57
+ *
58
+ * @param {object=} extraParams additional query parameters for logout request
59
+ * @return {Promise.<{url: string, stateId: string}>} logout URL with required parameters
60
+ */
61
+ async prepareLogoutRequest(extraParams) {
62
+ if (!this.config.logout) {
63
+ throw new Error('Logout URL is not configured');
64
+ }
65
+ // eslint-disable-next-line no-underscore-dangle
66
+ const stateId = AuthRequestBuilder._uuid();
67
+ const logoutParams = {
68
+ client_id: this.config.clientId,
69
+ state: stateId,
70
+ ...extraParams,
71
+ };
72
+ await this._saveState(stateId, {
73
+ restoreLocation: window.location.href,
74
+ scopes: [...this.config.scopes],
75
+ });
76
+ return {
77
+ url: encodeURL(this.config.logout, logoutParams),
78
+ stateId,
79
+ };
80
+ }
54
81
  /**
55
82
  * @param {string} id
56
83
  * @param {StoredState} storedState
@@ -6,7 +6,8 @@ import { fontSizes } from '../avatar/avatar-info';
6
6
  import styles from './avatar-stack.css';
7
7
  export default function AvatarStack({ children, className, size = Size.Size20, extraItems, dropdownMenuProps, ...restProps }) {
8
8
  const [dropdownOpen, setDropdownOpen] = useState(false);
9
- return (<div className={classNames(styles.avatarStack, className, styles[`size${size}`], {
9
+ const sizeClass = size !== Size.Size16 && size !== Size.Size18 && size !== Size.Size48 ? styles[`size${size}`] : undefined;
10
+ return (<div className={classNames(styles.avatarStack, className, sizeClass, {
10
11
  [styles.hovered]: dropdownOpen,
11
12
  })} {...restProps} style={{ height: size, ...restProps.style }}>
12
13
  {Children.map(children, (child, index) => (<div className={styles.item} style={{ '--ring-avatar-stack-index': index }}>
@@ -4,7 +4,7 @@ export function getButtonClasses({ className, active, disabled, loader, primary,
4
4
  const iconOnly = (icon || iconRight) && !children;
5
5
  const primaryBlock = primary && !inline;
6
6
  const withNormalIcon = iconOnly && inline && !active && !danger && !primary && !disabled;
7
- return classNames(styles.button, className, styles[`height${height}`], inline ? styles.inline : styles.block, {
7
+ return classNames(styles.button, className, height && styles[`height${height}`], inline ? styles.inline : styles.block, {
8
8
  [styles.active]: active,
9
9
  [styles.danger]: danger,
10
10
  [styles.delayed]: delayed,
@@ -64,7 +64,6 @@ class DataList extends PureComponent {
64
64
  const classes = classNames(className, {
65
65
  [styles.dataList]: true,
66
66
  [styles.disabledHover]: disabledHover,
67
- [styles.multiSelection]: selection.getSelected().size > 0,
68
67
  });
69
68
  return (<div className={styles.dataListWrapper} data-test='ring-data-list' ref={innerRef}>
70
69
  {focused && <Shortcuts map={shortcutsMap} scope={this.shortcutsScope}/>}
@@ -75,7 +75,7 @@ export default class DateInput extends React.PureComponent {
75
75
  .replace('{{name}}', name);
76
76
  }
77
77
  })();
78
- const classes = classNames(styles.filter, styles[`${name}Input`], divider && styles[`${name}InputWithDivider`], 'ring-js-shortcuts');
78
+ const classes = classNames(styles.filter, name !== 'time' && styles[`${name}Input`], divider && (name === 'from' || name === 'time') && styles[`${name}InputWithDivider`], 'ring-js-shortcuts');
79
79
  return (<Input autoComplete='off' borderless height={ControlsHeight.L} data-name={name} inputRef={this.inputRef} className={classes} value={displayText} onChange={this.handleChange} onFocus={onActivate} onKeyDown={this.handleKeyDown} onClear={onClear} placeholder={placeholder}/>);
80
80
  }
81
81
  }
@@ -151,7 +151,7 @@ export default class DatePicker extends PureComponent {
151
151
  <Icon glyph={chevronDownIcon} className={styles.chevronDownIcon}/>
152
152
  </div>);
153
153
  const { className, popupClassName, clear, inline, dropdownProps, translations, ...datePopupProps } = this.props;
154
- const classes = classNames(styles.datePicker, className, styles[`size${this.props.size}`], {
154
+ const classes = classNames(styles.datePicker, className, this.props.size && styles[`size${this.props.size}`], {
155
155
  [styles.inline]: inline,
156
156
  });
157
157
  return (<Dropdown className={classes} disabled={this.props.disabled} data-test='ring-date-picker' anchor={inline ? (<Link data-test-ring-dropdown-anchor className={styles.anchor} disabled={this.props.disabled ?? false} pseudo>
@@ -43,7 +43,10 @@ export default class Day extends Component {
43
43
  render() {
44
44
  const { day, from, currentRange, activeRange, empty, locale } = this.props;
45
45
  const reverse = activeRange && activeRange[1] === from;
46
- const dayInWeek = getDayNumInWeek(locale, getDay(day)) + 1;
46
+ // eslint-disable-next-line no-magic-numbers
47
+ const dayInWeek = (getDayNumInWeek(locale, getDay(day)) + 1);
48
+ // eslint-disable-next-line no-magic-numbers
49
+ const dayInWeekClass = dayInWeek !== 3 && dayInWeek !== 4 && styles[`Day${dayInWeek}`];
47
50
  function makeSpreadRange(range) {
48
51
  return range && [range[0], addDays(range[1], 1)];
49
52
  }
@@ -52,7 +55,7 @@ export default class Day extends Component {
52
55
  const activeSpreadRange = makeSpreadRange(activeRange);
53
56
  return (
54
57
  // TODO make keyboard navigation actually work
55
- <button type='button' className={classNames(styles.day, styles[`Day${dayInWeek}`], {
58
+ <button type='button' className={classNames(styles.day, dayInWeekClass, {
56
59
  [styles.current]: ['date', 'from', 'to'].some(this.is),
57
60
  [styles.active]: !disabled && this.is('activeDate'),
58
61
  [styles.weekend]: [weekdays.SA, weekdays.SU].includes(getDay(day)),
@@ -127,7 +127,6 @@ export default class Dialog extends PureComponent {
127
127
  [styles.closeButtonInside]: closeButtonInside,
128
128
  })} iconClassName={classNames(styles.closeIcon, {
129
129
  [styles.closeIconOutside]: !closeButtonInside,
130
- [styles.closeIconInside]: closeButtonInside,
131
130
  })} onClick={this.onCloseClick} title={closeButtonTitle} aria-label={closeButtonTitle || 'close dialog'}/>)}
132
131
  </AdaptiveIsland>
133
132
  </div>
@@ -44,8 +44,8 @@ export const EditableHeading = (props) => {
44
44
  [styles.multiline]: multiline,
45
45
  [styles.selectionMode]: isInSelectionMode,
46
46
  });
47
- const headingClasses = classNames(styles.heading, headingClassName, styles[`size${size}`]);
48
- const inputClasses = classNames('ring-js-shortcuts', styles.input, styles.textarea, { [styles.textareaNotOverflow]: !isOverflow }, inputStyles[`size${size}`], styles[`level${level}`], inputClassName);
47
+ const headingClasses = classNames(styles.heading, headingClassName, size !== Size.AUTO && styles[`size${size}`]);
48
+ const inputClasses = classNames('ring-js-shortcuts', styles.input, styles.textarea, { [styles.textareaNotOverflow]: !isOverflow }, size !== Size.AUTO && inputStyles[`size${size}`], level !== Levels.H4 && styles[`level${level}`], inputClassName);
49
49
  const stretch = (el) => {
50
50
  if (!el || !el.style) {
51
51
  return;
@@ -114,7 +114,7 @@ export const EditableHeading = (props) => {
114
114
  {!disabled && isEditing ? (<>
115
115
  <Shortcuts map={shortcutsMap} scope={shortcutsScope} disabled={isShortcutsDisabled}/>
116
116
 
117
- {!multiline ? (<input className={inputClasses} value={children} autoFocus={autoFocus} data-test={dataTest} disabled={isSaving} onChange={onChange} {...restProps} onFocus={onInputFocus} onBlur={onInputBlur}/>) : (<div className={classNames(styles.textareaWrapper, inputStyles[`size${size}`])}>
117
+ {!multiline ? (<input className={inputClasses} value={children} autoFocus={autoFocus} data-test={dataTest} disabled={isSaving} onChange={onChange} {...restProps} onFocus={onInputFocus} onBlur={onInputBlur}/>) : (<div className={classNames(styles.textareaWrapper, size !== Size.AUTO && inputStyles[`size${size}`])}>
118
118
  <textarea ref={textAreaRef} className={inputClasses} value={children} autoFocus={autoFocus} data-test={dataTest} disabled={isSaving} onChange={onInputChange} {...restProps} onFocus={onInputFocus} onBlur={onInputBlur} onScroll={onInputScroll} style={{ maxHeight: maxInputRows ? `${maxInputRows}lh` : '' }}/>
119
119
  {!isScrolledToBottom && <div className={styles.textareaFade}/>}
120
120
  </div>)}
@@ -137,7 +137,7 @@ export const EditableHeading = (props) => {
137
137
  </>)}
138
138
  </div>
139
139
 
140
- {isEditing && error && <div className={classNames(styles.errorText, inputStyles[`size${size}`])}>{error}</div>}
140
+ {isEditing && error && (<div className={classNames(styles.errorText, size !== Size.AUTO && inputStyles[`size${size}`])}>{error}</div>)}
141
141
  </>);
142
142
  };
143
143
  export default React.memo(EditableHeading);
@@ -48,13 +48,11 @@ export default class Profile extends PureComponent {
48
48
  showApplyChangedUser && {
49
49
  rgItemType,
50
50
  label: translations?.applyChangedUser ?? translate('applyChangedUser'),
51
- className: styles.profileMenuItem,
52
51
  onClick: onRevertPostponement,
53
52
  },
54
53
  showLogIn && {
55
54
  rgItemType,
56
55
  label: translations?.login ?? translate('login'),
57
- className: styles.profileMenuItem,
58
56
  onClick: onRevertPostponement,
59
57
  },
60
58
  {
@@ -67,7 +65,6 @@ export default class Profile extends PureComponent {
67
65
  showSwitchUser && {
68
66
  rgItemType,
69
67
  label: translations?.switchUser ?? translate('switchUser'),
70
- className: styles.profileMenuItem,
71
68
  onClick: onSwitchUser,
72
69
  },
73
70
  showLogOut && {
@@ -1,7 +1,7 @@
1
1
  import { Children, Component } from 'react';
2
2
  import classNames from 'classnames';
3
3
  import styles from './header.css';
4
- const wrapChild = (child) => child && <div className={styles.trayItem}>{child}</div>;
4
+ const wrapChild = (child) => child && <div>{child}</div>;
5
5
  export default class Tray extends Component {
6
6
  render() {
7
7
  const { children, className, ...restProps } = this.props;
@@ -47,8 +47,7 @@ export default class Icon extends PureComponent {
47
47
  if (!Glyph) {
48
48
  return null;
49
49
  }
50
- const classes = classNames(styles.icon, {
51
- [styles[color]]: !!color,
50
+ const classes = classNames(styles.icon, color && styles[color], {
52
51
  [styles.loading]: loading,
53
52
  }, className);
54
53
  return (<span data-test='ring-icon' {...restProps} className={classes}>
@@ -7,7 +7,7 @@ declare function noop(): void;
7
7
  * @name Input
8
8
  */
9
9
  declare enum Size {
10
- AUTO = "Auto",
10
+ AUTO = "AUTO",
11
11
  S = "S",
12
12
  M = "M",
13
13
  L = "L",
@@ -17,7 +17,7 @@ function noop() { }
17
17
  */
18
18
  var Size;
19
19
  (function (Size) {
20
- Size["AUTO"] = "Auto";
20
+ Size["AUTO"] = "AUTO";
21
21
  Size["S"] = "S";
22
22
  Size["M"] = "M";
23
23
  Size["L"] = "L";
@@ -100,7 +100,7 @@ export class Input extends PureComponent {
100
100
  label, labelType, error, help, className, inputClassName, children, value, onClear, disabled, inputRef, onChange, enableShortcuts, id, placeholder, icon, translations, height = typeof this.context === 'function' ? this.context() : this.context, beforeInput, afterInput, autogrow, ...restProps } = this.props;
101
101
  const { empty } = this.state;
102
102
  const clearable = !!onClear;
103
- const classes = classNames(className, styles.outerContainer, [styles[`size${size}`]], [styles[`height${height}`]], {
103
+ const classes = classNames(className, styles.outerContainer, size && size !== Size.AUTO && styles[`size${size}`], styles[`height${height}`], {
104
104
  'ring-js-shortcuts': enableShortcuts === true,
105
105
  [styles.error]: error !== null && error !== undefined,
106
106
  [styles.empty]: empty,
@@ -691,16 +691,23 @@ export default class QueryAssist extends Component {
691
691
  const renderLoader = this.props.loader !== false && this.state.loading;
692
692
  const renderGlass = glass && !renderLoader;
693
693
  const actions = this.renderActions();
694
- const containerClasses = classNames(className, inputStyles[`size${huge ? Size.FULL : size}`], {
694
+ let sizeClass;
695
+ if (huge) {
696
+ sizeClass = inputStyles[`size${Size.FULL}`];
697
+ }
698
+ else if (size !== Size.AUTO) {
699
+ sizeClass = inputStyles[`size${size}`];
700
+ }
701
+ else {
702
+ sizeClass = undefined;
703
+ }
704
+ const containerClasses = classNames(className, sizeClass, {
695
705
  [styles.queryAssist]: true,
696
- [styles.withIcon]: (renderGlass && !huge) || renderLoader,
697
706
  [styles.huge]: huge,
698
707
  [styles.queryAssistDisabled]: this.props.disabled,
699
708
  });
700
709
  const inputClasses = classNames(this.props.inputClassName, {
701
710
  [`${styles.input} ring-js-shortcuts`]: true,
702
- [styles.inputGap]: actions.length || (this.isRenderingGlassOrLoader() && !glass),
703
- [styles.inputGap2]: actions.length === 2, // TODO: replace with flex-box layout
704
711
  [styles.inputRevertOrder]: !glass || huge,
705
712
  });
706
713
  const placeholderStyles = classNames({
@@ -715,7 +722,7 @@ export default class QueryAssist extends Component {
715
722
 
716
723
  {renderGlass && !huge && (<Icon glyph={searchIcon} className={classNames(styles.icon, this.props.searchButtonClassName)} title={translations?.searchTitle ?? translate('searchTitle')} ref={this.glassRef} data-test='query-assist-search-icon'/>)}
717
724
 
718
- {renderLoader && (<div className={classNames(styles.icon, styles.loader, {
725
+ {renderLoader && (<div className={classNames(styles.icon, {
719
726
  [styles.loaderOnTheRight]: !glass && !huge,
720
727
  [styles.loaderActive]: renderLoader,
721
728
  })} ref={this.loaderRef}>
@@ -739,7 +746,7 @@ export default class QueryAssist extends Component {
739
746
  <PopupMenu hidden={!this.state.showPopup} onCloseAttempt={this.closePopup} ref={this.popupRef} anchorElement={this.node} keepMounted attached className={this.props.popupClassName} directions={[PopupMenu.PopupProps.Directions.BOTTOM_RIGHT, PopupMenu.PopupProps.Directions.BOTTOM_LEFT]} data={useCustomItemRender ? this.state.suggestions : this.renderSuggestions()} data-test='ring-query-assist-popup' hint={this.props.hint} shortcutsMap={this.listShortcutsMap} hintOnSelection={this.props.hintOnSelection} left={this.getPopupOffset(this.state.suggestions)} maxHeight={PopupMenu.PopupProps.MaxHeight.SCREEN} onMouseDown={this.trackPopupMouseState} onMouseUp={this.trackPopupMouseState} onSelect={item => this.handleComplete(item)} {...this.props.menuProps}/>
740
747
 
741
748
  {glass && huge && (<div className={styles.rightSearchButton} data-test='query-assist-search-button'>
742
- <Icon glyph={searchIcon} className={classNames(styles.rightSearchIcon, this.props.searchButtonClassName)} title={translations?.searchTitle ?? translate('searchTitle')} onClick={this.handleApply} ref={this.glassRef} data-test='query-assist-search-icon'/>
749
+ <Icon glyph={searchIcon} className={classNames(this.props.searchButtonClassName)} title={translations?.searchTitle ?? translate('searchTitle')} onClick={this.handleApply} ref={this.glassRef} data-test='query-assist-search-icon'/>
743
750
  </div>)}
744
751
  </div>)}
745
752
  </I18nContext.Consumer>
@@ -801,7 +801,8 @@ export default class Select extends Component {
801
801
  const dataTest = this.props['data-test'];
802
802
  const { selectedLabel } = this.props;
803
803
  const { shortcutsEnabled } = this.state;
804
- const classes = classNames(styles.select, 'ring-js-shortcuts', this.props.className, styles[`height${this.getHeight()}`], {
804
+ const height = this.getHeight();
805
+ const classes = classNames(styles.select, 'ring-js-shortcuts', this.props.className, height !== ControlsHeight.M && styles[`height${height}`], {
805
806
  [styles[`size${this.props.size}`]]: this.props.type !== Type.INLINE,
806
807
  [styles.disabled]: this.props.disabled,
807
808
  });
@@ -836,7 +837,7 @@ export default class Select extends Component {
836
837
  : undefined} icon={this.props.filterIcon} afterInput={iconsNode}/>
837
838
  {this._renderPopup()}
838
839
  </div>
839
- {this.props.error && (<div className={classNames(inputStyles.errorText, inputStyles[`size${this.props.size}`])}>
840
+ {this.props.error && (<div className={classNames(inputStyles.errorText, this.props.size !== Size.AUTO && inputStyles[`size${this.props.size}`])}>
840
841
  {this.props.error}
841
842
  </div>)}
842
843
  </>);
@@ -47,9 +47,7 @@ export default class Header extends PureComponent {
47
47
  </th>
48
48
  </tr>)}
49
49
 
50
- <tr className={style.subHeader} data-test='ring-table-header-row'>
51
- {regularCells}
52
- </tr>
50
+ <tr data-test='ring-table-header-row'>{regularCells}</tr>
53
51
  </thead>);
54
52
  }
55
53
  }
@@ -135,14 +135,10 @@ export class Table extends PureComponent {
135
135
  headerProps.checked = selectedSize > 0 && selectedSize === allSelectedSize;
136
136
  headerProps.onCheckboxChange = this.onCheckboxChange;
137
137
  headerProps.checkboxDisabled = this.props.data.length === 0;
138
- const wrapperClasses = classNames({
139
- [style.tableWrapper]: true,
140
- [style.loading]: loading,
141
- }, this.props.wrapperClassName);
138
+ const wrapperClasses = classNames(style.tableWrapper, this.props.wrapperClassName);
142
139
  const classes = classNames(this.props.className, {
143
140
  [style.table]: true,
144
141
  [style.wideFirstColumn]: this.props.wideFirstColumn,
145
- [style.multiSelection]: selection.getSelected().size > 0,
146
142
  [style.userSelectNone]: this.state.userSelectNone,
147
143
  [style.disabledHover]: this.props.disabledHover,
148
144
  });
@@ -61,7 +61,7 @@ export const MoreButton = memo(({ items, selected, onSelect, moreClassName, more
61
61
  </div>);
62
62
  });
63
63
  MoreButton.displayName = 'MoreButton';
64
- export const FakeMoreButton = memo(({ moreClassName, moreActiveClassName, hasActiveChildren }) => (<div className={classNames(styles.moreButton, styles.title)}>
64
+ export const FakeMoreButton = memo(({ moreClassName, moreActiveClassName, hasActiveChildren }) => (<div className={styles.title}>
65
65
  <AnchorLink moreClassName={moreClassName} moreActiveClassName={moreActiveClassName} hasActiveChildren={hasActiveChildren} tabIndex={-1} disabled/>
66
66
  </div>));
67
67
  FakeMoreButton.displayName = 'FakeMoreButton';
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
  import classNames from 'classnames';
3
3
  import TabLink from './tab-link';
4
4
  import { CustomItem } from './custom-item';
5
+ import { getTabId } from './tabs.utils';
5
6
  import styles from './tabs.css';
6
7
  const TabTitle = React.memo(function TabTitle({ selected, child, handleSelect, collapsed = false, tabIndex, }) {
7
8
  if (child === null || typeof child !== 'object' || child.type === CustomItem) {
@@ -15,7 +16,7 @@ const TabTitle = React.memo(function TabTitle({ selected, child, handleSelect, c
15
16
  return (<TabLink title={title} isSelected={selected} active href={href} className={titleClasses} disabled={disabled} onPlainLeftClick={handleSelect} tabIndex={tabIndex} collapsed={collapsed} {...titleProps}/>);
16
17
  });
17
18
  const getTabTitles = ({ items, selected = '0', collapsed, onSelect = () => undefined, ...props }) => items.map((tab, index) => {
18
- const key = tab.props.id || String(index);
19
+ const key = getTabId(tab, index);
19
20
  const isSelected = selected === key;
20
21
  return (<TabTitle key={key} handleSelect={onSelect(key)} selected={isSelected} child={tab} collapsed={collapsed} {...props}/>);
21
22
  });
@@ -16,7 +16,11 @@ export interface TabsProps extends Omit<CollapsibleTabsProps, 'onSelect' | 'chil
16
16
  }
17
17
  declare class Tabs extends PureComponent<TabsProps> {
18
18
  handleSelect: (arg: string) => () => void | undefined;
19
- getTabTitle: (child: ReactElement<TabProps>, i: number) => React.JSX.Element;
19
+ getSelectedItem(): {
20
+ selectedItem: ReactElement<TabProps, string | React.JSXElementConstructor<any>>;
21
+ selectedKey: string;
22
+ };
23
+ getTabTitle(selectedKey: string, child: ReactElement<TabProps>, i: number): React.JSX.Element;
20
24
  render(): React.JSX.Element;
21
25
  }
22
26
  export type TabsAttrs = React.JSX.LibraryManagedAttributes<typeof Tabs, TabsProps>;
@@ -6,34 +6,44 @@ import dataTests from '../global/data-tests';
6
6
  import TabLink from './tab-link';
7
7
  import CollapsibleTabs from './collapsible-tabs';
8
8
  import { CustomItem } from './custom-item';
9
+ import { getTabId } from './tabs.utils';
9
10
  import styles from './tabs.css';
10
11
  export { CustomItem };
11
12
  class Tabs extends PureComponent {
12
13
  handleSelect = memoize((key) => () => this.props.onSelect?.(key));
13
- getTabTitle = (child, i) => {
14
+ getSelectedItem() {
15
+ const { selected, children } = this.props;
16
+ const childrenArray = React.Children.toArray(children).filter(Boolean);
17
+ const selectedIndex = childrenArray.findIndex((tab, i) => getTabId(tab, i) === selected);
18
+ const actualSelectedIndex = selectedIndex === -1 ? childrenArray.findIndex(tab => tab.type !== CustomItem) : selectedIndex;
19
+ const selectedItem = childrenArray[actualSelectedIndex];
20
+ return { selectedItem, selectedKey: getTabId(selectedItem, actualSelectedIndex) };
21
+ }
22
+ getTabTitle(selectedKey, child, i) {
14
23
  if (child === null || typeof child !== 'object' || child.type === CustomItem) {
15
24
  return child;
16
25
  }
17
- const { selected, onSelect } = this.props;
18
- const { title, titleProps, id, disabled, href, className, activeClassName } = child.props;
19
- const key = id || String(i);
20
- const isSelected = key === selected;
26
+ const { onSelect } = this.props;
27
+ const { title, titleProps, disabled, href, className, activeClassName } = child.props;
28
+ const key = getTabId(child, i);
29
+ const isSelected = key === selectedKey;
21
30
  const titleClasses = classNames(styles.title, className, isSelected && activeClassName, {
22
31
  [styles.selected]: isSelected,
23
32
  });
24
33
  return (<TabLink title={title} isSelected={isSelected} key={key} href={href} className={titleClasses} disabled={disabled} onPlainLeftClick={onSelect ? this.handleSelect(key) : undefined} {...titleProps}/>);
25
- };
34
+ }
26
35
  render() {
27
36
  const { className, tabContainerClassName, children, selected, autoCollapse, 'data-test': dataTest, onSelect, ...restProps } = this.props;
28
37
  const classes = classNames(styles.tabs, className);
29
38
  const childrenArray = React.Children.toArray(children).filter(Boolean);
39
+ const { selectedItem, selectedKey } = this.getSelectedItem();
30
40
  return (<div className={classes} data-test={dataTests('ring-dumb-tabs', dataTest)}>
31
- {autoCollapse === true ? (<CollapsibleTabs {...restProps} onSelect={onSelect ? this.handleSelect : undefined} selected={selected}>
41
+ {autoCollapse === true ? (<CollapsibleTabs {...restProps} onSelect={onSelect ? this.handleSelect : undefined} selected={selectedKey}>
32
42
  {childrenArray}
33
- </CollapsibleTabs>) : (<div className={styles.titles}>{childrenArray.map(this.getTabTitle)}</div>)}
34
- <div className={classNames(tabContainerClassName)}>
35
- {childrenArray.find(({ props }, i) => (props.id || String(i)) === selected)}
36
- </div>
43
+ </CollapsibleTabs>) : (<div className={styles.titles}>
44
+ {childrenArray.map((tab, index) => this.getTabTitle(selectedKey, tab, index))}
45
+ </div>)}
46
+ <div className={classNames(tabContainerClassName)}>{selectedItem}</div>
37
47
  </div>);
38
48
  }
39
49
  }
@@ -3,13 +3,19 @@ import Link from '../link/link';
3
3
  import styles from './tabs.css';
4
4
  function TabLink({ isSelected, title, collapsed, ...restProps }) {
5
5
  const renderedTitle = typeof title === 'function' ? title(isSelected, collapsed) : title;
6
- return (<Link {...restProps}>
6
+ return (<Link {...restProps} data-test-selected={isSelected}>
7
7
  <div className={styles.container}>
8
8
  <span className={styles.visible}>{renderedTitle}</span>
9
9
  {/* hack for preserving constant tab width*/}
10
- <span className={styles.hidden}>{renderedTitle}</span>
11
- <span className={styles.hiddenBold}>{renderedTitle}</span>
12
- <span className={styles.hiddenRegular}>{renderedTitle}</span>
10
+ <span aria-hidden className={styles.hidden}>
11
+ {renderedTitle}
12
+ </span>
13
+ <span aria-hidden className={styles.hiddenBold}>
14
+ {renderedTitle}
15
+ </span>
16
+ <span aria-hidden className={styles.hiddenRegular}>
17
+ {renderedTitle}
18
+ </span>
13
19
  </div>
14
20
  </Link>);
15
21
  }
@@ -0,0 +1,3 @@
1
+ import type { ReactElement } from 'react';
2
+ import type { TabProps } from './tab';
3
+ export declare const getTabId: (tab: ReactElement<TabProps> | undefined, index: number) => string;
@@ -0,0 +1 @@
1
+ export const getTabId = (tab, index) => tab?.props.id || String(index);
@@ -229,7 +229,7 @@ export default class TagsInput extends PureComponent {
229
229
  render() {
230
230
  const { focused, tags, activeIndex } = this.state;
231
231
  const { disabled, canNotBeEmpty, allowAddNewTags, filter, size, labelType, height = typeof this.context === 'function' ? this.context() : this.context, label, } = this.props;
232
- const classes = classNames(styles.tagsInput, [inputStyles[`size${size}`]], [inputStyles[`height${height}`]], {
232
+ const classes = classNames(styles.tagsInput, size !== Size.AUTO && [inputStyles[`size${size}`]], [inputStyles[`height${height}`]], {
233
233
  [styles.tagsInputDisabled]: disabled,
234
234
  [styles.tagsInputFocused]: focused,
235
235
  }, this.props.className);
@@ -240,7 +240,7 @@ export default class TagsInput extends PureComponent {
240
240
  {label}
241
241
  </ControlLabel>)}
242
242
 
243
- <TagsList tags={tags} activeIndex={activeIndex} disabled={disabled} canNotBeEmpty={canNotBeEmpty} handleRemove={this.handleRemove} className={styles.tagsList} tagClassName={styles.tag} handleClick={this.handleClick} customTagComponent={this.props.customTagComponent}>
243
+ <TagsList tags={tags} activeIndex={activeIndex} disabled={disabled} canNotBeEmpty={canNotBeEmpty} handleRemove={this.handleRemove} className={styles.tagsList} handleClick={this.handleClick} customTagComponent={this.props.customTagComponent}>
244
244
  <Select id={this.id} ref={this.selectRef} size={Select.Size.AUTO} type={Select.Type.INPUT_WITHOUT_CONTROLS} inputPlaceholder={this.props.placeholder} data={this.state.suggestions} className={classNames(styles.tagsSelect)} onSelect={this.addTag} onFocus={this._focusHandler} onBlur={this._blurHandler} renderOptimization={this.props.renderOptimization} add={allowAddNewTags ? { prefix: 'Add new tag' } : undefined} onAdd={allowAddNewTags ? this.handleTagCreation : undefined} filter={filter} maxHeight={this.props.maxPopupHeight} minWidth={this.props.minPopupWidth} top={POPUP_VERTICAL_SHIFT} loading={this.state.loading} onFilter={this.loadSuggestions} onBeforeOpen={this.loadSuggestions} onKeyDown={this.handleKeyDown} disabled={this.props.disabled} loadingMessage={this.props.loadingMessage} notFoundMessage={this.props.notFoundMessage} hint={this.props.hint}/>
245
245
  </TagsList>
246
246
  </div>);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetbrains/ring-ui",
3
- "version": "7.0.93",
3
+ "version": "7.0.95",
4
4
  "description": "JetBrains UI library",
5
5
  "author": {
6
6
  "name": "JetBrains"
@@ -63,7 +63,8 @@
63
63
  "postpublish": "pinst --enable",
64
64
  "postrelease-ci": "git push --follow-tags",
65
65
  "prea11y-audit": "playwright install",
66
- "prebuild": "rimraf components && tsc --project tsconfig-build.json && cpy './**/*' '!**/*.stories.*' '!**/*.figma.*' '!**/*.ts' '!**/*.tsx' '!**/test-helpers/mocks/**' ../components --parents --cwd=src/",
66
+ "prebuild": "rimraf components && npm run prebuild:tsc && cpy './**/*' '!**/*.stories.*' '!**/*.figma.*' '!**/*.ts' '!**/*.tsx' '!**/test-helpers/mocks/**' ../components --parents --cwd=src/",
67
+ "prebuild:tsc": "(npm run type-check:create-d-ts && tsc --project tsconfig-build.json) ; npm run type-check:cleanup-d-ts",
67
68
  "prepare": "webpack -c .storybook/custom-header/webpack.config.js",
68
69
  "prepublishOnly": "pinst --disable",
69
70
  "prerelease-built-ci": "node scripts/prepare-built-package.js",
@@ -79,7 +80,11 @@
79
80
  "storybook-debug": "node --inspect-brk node_modules/@storybook/react/bin -p 9999",
80
81
  "stylelint": "stylelint --ignore-path .stylelintignore '**/*.css'",
81
82
  "test": "vitest src",
82
- "type-check": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig-build.json",
83
+ "type-check": "(npm run type-check:create-d-ts && npm run type-check:main && npm run type-check:build) ; npm run type-check:cleanup-d-ts",
84
+ "type-check:create-d-ts": "npx tcm src && npx tcm .storybook",
85
+ "type-check:main": "tsc --noEmit -p tsconfig.json",
86
+ "type-check:build": "tsc --noEmit -p tsconfig-build.json",
87
+ "type-check:cleanup-d-ts": "rimraf src/**/*.css.d.ts .storybook/*.css.d.ts .storybook/**/*.css.d.ts",
83
88
  "update-styles": "node scripts/update-styles.mjs",
84
89
  "validate-tc-config": "mvn --file .teamcity/pom.xml org.jetbrains.teamcity:teamcity-configs-maven-plugin:generate -e"
85
90
  },
@@ -101,6 +106,7 @@
101
106
  "@jetbrains/logos": "3.0.0-canary.734b213.0",
102
107
  "@jetbrains/rollup-css-plugin": "./packages/rollup-css-plugin",
103
108
  "@jetbrains/stylelint-config": "^4.0.2",
109
+ "@jetbrains/typescript-plugin-css-modules": "^5.3.1",
104
110
  "@primer/octicons": "^19.22.0",
105
111
  "@rollup/plugin-babel": "^6.1.0",
106
112
  "@rollup/plugin-json": "^6.1.0",
@@ -173,6 +179,7 @@
173
179
  "svg-inline-loader": "^0.8.2",
174
180
  "teamcity-service-messages": "^0.1.14",
175
181
  "terser-webpack-plugin": "^5.3.16",
182
+ "typed-css-modules": "^0.9.1",
176
183
  "typescript": "~5.9.3",
177
184
  "typescript-eslint": "^8.55.0",
178
185
  "vitest": "^4.0.18",
package/typings.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- declare module '*.css' {
2
- const styles: Record<string, string>;
3
- export default styles;
4
- }
5
-
6
1
  declare module '*.svg' {
7
2
  const source: string;
8
3
  export default source;