diff --git a/CHANGELOG.md b/CHANGELOG.md index 225fb9de03..1b411f9a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [6.21.0](https://github.com/gravity-ui/uikit/compare/v6.20.1...v6.21.0) (2024-07-16) + + +### Features + +* **PinInput:** form support ([#1686](https://github.com/gravity-ui/uikit/issues/1686)) ([b82262e](https://github.com/gravity-ui/uikit/commit/b82262e6ac3ac656f0d87a6275a081f9990aeb6a)) +* **Select:** new label and value resize behaviour ([#1694](https://github.com/gravity-ui/uikit/issues/1694)) ([891fa88](https://github.com/gravity-ui/uikit/commit/891fa886ce5b2d9c8753abbfe49f833ff35619a6)) +* **TreeList:** add ListTreeItemType interface export and id argument to renderItem prop ([#1707](https://github.com/gravity-ui/uikit/issues/1707)) ([de544b8](https://github.com/gravity-ui/uikit/commit/de544b871f722fe63f677e17f4272767bebf973e)) +* **TreeSelect:** add placeholder prop ([#1705](https://github.com/gravity-ui/uikit/issues/1705)) ([88696a3](https://github.com/gravity-ui/uikit/commit/88696a3f529c44dc9bd17024c926a77fea67acaa)) +* **useControlledState:** support update callback with additional params ([#1688](https://github.com/gravity-ui/uikit/issues/1688)) ([8bff882](https://github.com/gravity-ui/uikit/commit/8bff8821b4e8bb20e1bedb01c07a3ba8f22dfe16)) +* **useResizeObserver:** support box option ([#1687](https://github.com/gravity-ui/uikit/issues/1687)) ([a178dff](https://github.com/gravity-ui/uikit/commit/a178dffefb2d1cfa481417a2dea6297034ae22a0)) + + +### Bug Fixes + +* **Avatar:** update text font weight ([#1684](https://github.com/gravity-ui/uikit/issues/1684)) ([0ae513a](https://github.com/gravity-ui/uikit/commit/0ae513a2e2a595f2cc7f5d0b2fe532a6f7f46b03)) +* **ListItemView:** div -> li default list item html tag ([#1698](https://github.com/gravity-ui/uikit/issues/1698)) ([07a16c9](https://github.com/gravity-ui/uikit/commit/07a16c959198bedfa45dbb14b5ad804bd11a6d82)) +* **Sheet:** fix incorrect content height calculation ([#1700](https://github.com/gravity-ui/uikit/issues/1700)) ([7e4dd23](https://github.com/gravity-ui/uikit/commit/7e4dd2374cb83f72168e0b2fc416fa76dd6fbe6e)) +* **TextArea:** fix content width & height styles ([#1690](https://github.com/gravity-ui/uikit/issues/1690)) ([94979cf](https://github.com/gravity-ui/uikit/commit/94979cf8c43194dd347481929dc1e217ac2bd84b)) +* **TreeSelect:** add disabled prop ([#1697](https://github.com/gravity-ui/uikit/issues/1697)) ([f9650da](https://github.com/gravity-ui/uikit/commit/f9650dab0b999e5efa03e2e8f513e62074083f67)) +* **TreeSelect:** fix page gap on component focus in some cases ([#1708](https://github.com/gravity-ui/uikit/issues/1708)) ([cd4eb93](https://github.com/gravity-ui/uikit/commit/cd4eb93375ad7a6cfc6e4c87a92e3a20679b3047)) +* **useList:** fix disabled elements activate logic ([#1706](https://github.com/gravity-ui/uikit/issues/1706)) ([f12d49f](https://github.com/gravity-ui/uikit/commit/f12d49f125c189ff6ceaba86b5000920a811586d)) + ## [6.20.1](https://github.com/gravity-ui/uikit/compare/v6.20.0...v6.20.1) (2024-07-01) diff --git a/CODEOWNERS b/CODEOWNERS index 3a004158c2..fa0631e2e0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,6 +3,7 @@ /src/components/Alert @IsaevAlexandr /src/components/ArrowToggle @Marginy605 /src/components/Avatar @DakEnviy +/src/components/AvatarStack @ogonkov #/src/components/Breadcrumbs /src/components/Button @amje /src/components/Card @Lunory diff --git a/package-lock.json b/package-lock.json index b31f868100..a28de9d6a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gravity-ui/uikit", - "version": "6.20.1", + "version": "6.21.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gravity-ui/uikit", - "version": "6.20.1", + "version": "6.21.0", "license": "MIT", "dependencies": { "@bem-react/classname": "^1.6.0", diff --git a/package.json b/package.json index 90859c437f..096cfcc0a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gravity-ui/uikit", - "version": "6.20.1", + "version": "6.21.0", "description": "Gravity UI base styling and components", "keywords": [ "component", @@ -108,7 +108,7 @@ "lint": "run-p lint:*", "typecheck": "tsc --noEmit", "prepublishOnly": "npm run build && npm pkg delete engines", - "playwright:install": "playwright install --with-deps", + "playwright:install": "playwright install chromium webkit --with-deps", "playwright": "playwright test --config=playwright/playwright.config.ts", "playwright:update": "npm run playwright -- -u", "playwright:docker": "./scripts/playwright-docker.sh 'npm run playwright'", diff --git a/src/components/Avatar/Avatar.scss b/src/components/Avatar/Avatar.scss index c7081cf759..cf51bd03e3 100644 --- a/src/components/Avatar/Avatar.scss +++ b/src/components/Avatar/Avatar.scss @@ -10,6 +10,7 @@ $block: '.#{variables.$ns}avatar'; --_--color: var(--g-color-text-misc); --_--font-size: var(--g-text-body-1-font-size); --_--line-height: var(--g-text-body-1-line-height); + --_--font-weight: var(--g-text-body-font-weight); overflow: hidden; display: inline-flex; @@ -39,7 +40,7 @@ $block: '.#{variables.$ns}avatar'; color: var(--g-avatar-color, var(--_--color)); font-size: var(--g-avatar-font-size, var(--_--font-size)); line-height: var(--g-avatar-line-height, var(--_--line-height)); - font-weight: 500; + font-weight: var(--_--font-weight); } &_with-border, @@ -75,17 +76,20 @@ $block: '.#{variables.$ns}avatar'; &_s { --_--font-size: var(--g-text-caption-1-font-size); --_--line-height: var(--g-text-caption-1-line-height); + --_--font-weight: var(--g-text-caption-font-weight); } &_m, &_l { - --_--font-size: var(--g-text-body-1-font-size); - --_--line-height: var(--g-text-body-1-line-height); + --_--font-size: var(--g-text-subheader-1-font-size); + --_--line-height: var(--g-text-subheader-1-line-height); + --_--font-weight: var(--g-text-subheader-font-weight); } &_xl { - --_--font-size: var(--g-text-body-2-font-size); - --_--line-height: var(--g-text-body-2-line-height); + --_--font-size: var(--g-text-subheader-2-font-size); + --_--line-height: var(--g-text-subheader-2-line-height); + --_--font-weight: var(--g-text-subheader-font-weight); } } diff --git a/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-Showcase-1-chromium-linux.png b/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-Showcase-1-chromium-linux.png index eef174dd24..b4a9685df5 100644 Binary files a/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-Showcase-1-chromium-linux.png and b/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-Showcase-1-chromium-linux.png differ diff --git a/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-Showcase-1-webkit-linux.png b/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-Showcase-1-webkit-linux.png index 969abb6b18..3aad9739f9 100644 Binary files a/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-Showcase-1-webkit-linux.png and b/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-Showcase-1-webkit-linux.png differ diff --git a/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-TextInitials-1-chromium-linux.png b/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-TextInitials-1-chromium-linux.png index 85aaad554b..e8015e0799 100644 Binary files a/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-TextInitials-1-chromium-linux.png and b/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-TextInitials-1-chromium-linux.png differ diff --git a/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-TextInitials-1-webkit-linux.png b/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-TextInitials-1-webkit-linux.png index 00e4d243d7..c36ea7324d 100644 Binary files a/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-TextInitials-1-webkit-linux.png and b/src/components/Avatar/__snapshots__/Avatar.visual.test.tsx-snapshots/Avatar-render-story-TextInitials-1-webkit-linux.png differ diff --git a/src/components/AvatarStack/AvatarStack.scss b/src/components/AvatarStack/AvatarStack.scss new file mode 100644 index 0000000000..b0663c6a2e --- /dev/null +++ b/src/components/AvatarStack/AvatarStack.scss @@ -0,0 +1,61 @@ +@use '../../../styles/mixins'; +@use '../Avatar/variables' as avatar-variables; +@use '../variables'; + +$block: '.#{variables.$ns}avatar-stack'; + +#{$block} { + --_--more-button-size: #{avatar-variables.$default-size}; + --_--more-button-border-width: 1px; + + display: inline-flex; + justify-content: flex-end; + flex-direction: row-reverse; + + margin: 0; + padding: 0; + + &_overlap-size_s { + --_--overlap: var(--g-spacing-1); + } + + &_overlap-size_m { + --_--overlap: var(--g-spacing-2); + } + + &_overlap-size_l { + --_--overlap: var(--g-spacing-3); + } + + &__item { + display: flex; + z-index: 0; + border-radius: 100%; + + &:not(:first-child) { + margin-inline-end: calc(-1 * var(--_--overlap)); + } + } + + &__more-button { + @include mixins.button-reset; + + border-radius: 100%; + + width: var(--_--more-button-size); + height: var(--_--more-button-size); + + &:focus-visible { + outline: var(--g-color-line-focus) solid 2px; + outline-offset: 0; + } + + &_size { + @each $size-name, $size-value in avatar-variables.$sizes { + &_#{$size-name} { + --_--more-button-size: #{$size-value}; + } + } + } + } +} diff --git a/src/components/AvatarStack/AvatarStack.tsx b/src/components/AvatarStack/AvatarStack.tsx new file mode 100644 index 0000000000..3e03fb63c5 --- /dev/null +++ b/src/components/AvatarStack/AvatarStack.tsx @@ -0,0 +1,69 @@ +import React from 'react'; + +import {Avatar} from '../Avatar'; +import {block} from '../utils/cn'; + +import {AvatarStackItem} from './AvatarStackItem'; +import {AvatarStackMoreButton} from './AvatarStackMoreButton'; +import i18n from './i18n'; +import type {AvatarStackProps} from './types'; + +import './AvatarStack.scss'; + +const b = block('avatar-stack'); + +const AvatarStackComponent = ({ + max = 3, + overlapSize = 's', + size, + children, + className, + renderMore, +}: AvatarStackProps) => { + const visibleItems: React.ReactElement[] = []; + let moreItems = 0; + + React.Children.forEach(children, (child) => { + if (!React.isValidElement(child)) { + return; + } + + const item = {child}; + + if (visibleItems.length <= max) { + visibleItems.unshift(item); + } else { + moreItems += 1; + } + }); + + const hasMoreButton = moreItems > 0; + /** Avatars + more button, or just avatars, when avatars count is equal to `max` or less */ + const normalOverflow = moreItems >= 1; + + return ( + // Safari remove role=list with some styles, applied to li items, so we need + // to restore role manually + // eslint-disable-next-line jsx-a11y/no-redundant-roles + + ); +}; + +AvatarStackComponent.displayName = 'AvatarStack'; + +export const AvatarStack = Object.assign(AvatarStackComponent, {MoreButton: AvatarStackMoreButton}); diff --git a/src/components/AvatarStack/AvatarStackItem.tsx b/src/components/AvatarStack/AvatarStackItem.tsx new file mode 100644 index 0000000000..b21aaa415f --- /dev/null +++ b/src/components/AvatarStack/AvatarStackItem.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import {block} from '../utils/cn'; + +const b = block('avatar-stack'); + +type Props = React.PropsWithChildren<{}>; + +export const AvatarStackItem = ({children}: Props) => { + return
  • {children}
  • ; +}; + +AvatarStackItem.displayName = 'AvatarStack.Item'; diff --git a/src/components/AvatarStack/AvatarStackMoreButton.tsx b/src/components/AvatarStack/AvatarStackMoreButton.tsx new file mode 100644 index 0000000000..c49d410a98 --- /dev/null +++ b/src/components/AvatarStack/AvatarStackMoreButton.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import type {AvatarSize} from '../Avatar'; +import {Avatar, DEFAULT_AVATAR_SIZE} from '../Avatar'; +import {block} from '../utils/cn'; + +import i18n from './i18n'; + +const b = block('avatar-stack'); + +export type AvatarStackMoreButtonProps = Pick< + React.HTMLProps, + 'className' | 'onClick' | 'aria-label' +> & { + size?: AvatarSize; + count: number; +}; + +export const AvatarStackMoreButton = React.forwardRef< + HTMLButtonElement, + AvatarStackMoreButtonProps +>(({className, size = DEFAULT_AVATAR_SIZE, onClick, count, 'aria-label': ariaLabel}, ref) => { + return ( + + ); +}); + +AvatarStackMoreButton.displayName = 'AvatarStack.MoreButton'; diff --git a/src/components/AvatarStack/README.md b/src/components/AvatarStack/README.md new file mode 100644 index 0000000000..46198c1e3a --- /dev/null +++ b/src/components/AvatarStack/README.md @@ -0,0 +1,52 @@ + + +# AvatarStack + + + +```ts +import {AvatarStack} from '@gravity-ui/uikit'; +``` + +Stack of images with overlap over next image and optional control. This is usually users avatars. + +## Usage + +Component is not limit you to what components to render, basic usage is: + +```tsx + + + + + +``` + +## Properties + +| Name | Description | Type | Default | +| :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------: | :-----: | +| max | How much avatars should be visible before more button. If avatars count is only 1 short from `max`, than more button would be replaced with avatar. | `number` | 3 | +| overlapSize | How much each item should overlap next one. `s` recommended for `Avatar`'s of sizes `xs`-`m`, `m` recomended for `l` size avatars and `l` overlap for `xl` avatars | `s`, `m`, `l` | `s` | +| size | Size for control displaying extra avatars. Value same to `Avatar` size. | `AvatarSize` | | +| className | Class name of root DOM node | `string` | | +| children | List of avatars, probably with some extra wrappers | `Object[]` | | +| renderMore | Custom render for control displaying extra avatars | `function(options: {count: number}): ReactElement` | | + +### AvatarStack.MoreButton + +Component for overriding more button + +```tsx + ( + + + + )} +> + + + + +``` diff --git a/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButton-dark-chromium-linux.png b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButton-dark-chromium-linux.png new file mode 100644 index 0000000000..c9d1bcd627 Binary files /dev/null and b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButton-dark-chromium-linux.png differ diff --git a/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButton-dark-webkit-linux.png b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButton-dark-webkit-linux.png new file mode 100644 index 0000000000..cafbd09942 Binary files /dev/null and b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButton-dark-webkit-linux.png differ diff --git a/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButton-light-chromium-linux.png b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButton-light-chromium-linux.png new file mode 100644 index 0000000000..7c9ffe1618 Binary files /dev/null and b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButton-light-chromium-linux.png differ diff --git a/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButton-light-webkit-linux.png b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButton-light-webkit-linux.png new file mode 100644 index 0000000000..697111e72f Binary files /dev/null and b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButton-light-webkit-linux.png differ diff --git a/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-dark-chromium-linux.png b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-dark-chromium-linux.png new file mode 100644 index 0000000000..9584de0536 Binary files /dev/null and b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-dark-chromium-linux.png differ diff --git a/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-dark-webkit-linux.png b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-dark-webkit-linux.png new file mode 100644 index 0000000000..d40c16dffc Binary files /dev/null and b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-dark-webkit-linux.png differ diff --git a/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-light-chromium-linux.png b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-light-chromium-linux.png new file mode 100644 index 0000000000..6d1abcf1b5 Binary files /dev/null and b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-light-chromium-linux.png differ diff --git a/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-light-webkit-linux.png b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-light-webkit-linux.png new file mode 100644 index 0000000000..3ab541002c Binary files /dev/null and b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-light-webkit-linux.png differ diff --git a/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-SingleItem-dark-chromium-linux.png b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-SingleItem-dark-chromium-linux.png new file mode 100644 index 0000000000..4346f27ec6 Binary files /dev/null and b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-SingleItem-dark-chromium-linux.png differ diff --git a/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-SingleItem-dark-webkit-linux.png b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-SingleItem-dark-webkit-linux.png new file mode 100644 index 0000000000..cef93a0a01 Binary files /dev/null and b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-SingleItem-dark-webkit-linux.png differ diff --git a/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-SingleItem-light-chromium-linux.png b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-SingleItem-light-chromium-linux.png new file mode 100644 index 0000000000..5f0b07b72a Binary files /dev/null and b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-SingleItem-light-chromium-linux.png differ diff --git a/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-SingleItem-light-webkit-linux.png b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-SingleItem-light-webkit-linux.png new file mode 100644 index 0000000000..2238b45c9d Binary files /dev/null and b/src/components/AvatarStack/__snapshots__/AvatarStack.visual.test.tsx-snapshots/AvatarStack-render-story-SingleItem-light-webkit-linux.png differ diff --git a/src/components/AvatarStack/__stories__/AvatarStack.stories.tsx b/src/components/AvatarStack/__stories__/AvatarStack.stories.tsx new file mode 100644 index 0000000000..30c1d63c70 --- /dev/null +++ b/src/components/AvatarStack/__stories__/AvatarStack.stories.tsx @@ -0,0 +1,139 @@ +import React from 'react'; + +import {faker} from '@faker-js/faker/locale/en'; +import type {Meta, StoryObj} from '@storybook/react'; + +import type {AvatarSize} from '../../Avatar'; +import {AVATAR_SIZES, Avatar, DEFAULT_AVATAR_SIZE} from '../../Avatar'; +import {Tooltip} from '../../Tooltip'; +import {AvatarStack} from '../AvatarStack'; + +type ComponentType = React.ComponentProps & { + avatarSize: AvatarSize; + randomAvatar: boolean; +}; + +const imgUrl = + 'data:image/png;base64,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'; + +function getChildren({ + count = faker.number.int({min: 1, max: 30}), + avatarSize = DEFAULT_AVATAR_SIZE, + randomAvatar, +}: Partial<{count: number; avatarSize: AvatarSize; randomAvatar: boolean}>) { + return faker.helpers.uniqueArray( + () => ( + + ), + count, + ); +} + +const meta: Meta = { + title: 'Components/Data Display/AvatarStack', + component: AvatarStack, + args: { + overlapSize: 's', + avatarSize: DEFAULT_AVATAR_SIZE, + randomAvatar: true, + }, + argTypes: { + avatarSize: { + control: 'select', + options: Object.keys(AVATAR_SIZES), + name: 'Size of avatar', + description: 'Not part of component API', + }, + randomAvatar: { + control: 'boolean', + name: 'Use random avatars', + description: 'Not part of component API', + }, + }, + parameters: { + a11y: { + element: '#storybook-root', + config: { + rules: [ + { + id: 'color-contrast', + enabled: false, + selector: '.g-avatar__text', + }, + ], + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +function getTemplate({count}: Partial<{count: number}> = {}) { + // eslint-disable-next-line react/display-name + return (args: ComponentType) => { + const {avatarSize, randomAvatar, size, ...props} = args; + return ( + + {getChildren({count, avatarSize, randomAvatar})} + + ); + }; +} + +export const Default: Story = { + render: getTemplate(), +}; + +export const SingleItem: Story = { + render: getTemplate({count: 1}), +}; + +export const MoreButton: Story = { + render: getTemplate({count: 6}), +}; + +export const MoreButtonOmit: Story = { + render: getTemplate({count: 4}), + parameters: { + docs: { + description: { + component: 'In case when only one avatar is hidden, we omit rendering more button', + }, + }, + }, + args: { + max: 3, + }, +}; + +export const CustomMoreButton: Story = { + render(args) { + const {avatarSize, randomAvatar, size, ...props} = args; + + return ( + ( + + + + )} + > + {getChildren({count: 26, avatarSize, randomAvatar})} + + ); + }, +}; diff --git a/src/components/AvatarStack/__tests__/AvatarStack.visual.test.tsx b/src/components/AvatarStack/__tests__/AvatarStack.visual.test.tsx new file mode 100644 index 0000000000..fb3cded5ab --- /dev/null +++ b/src/components/AvatarStack/__tests__/AvatarStack.visual.test.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import {test} from '~playwright/core'; + +import {AvatarStackStories} from './stories'; + +test.describe('AvatarStack', () => { + test('render story ', async ({mount, expectScreenshot}) => { + await mount(); + + await expectScreenshot(); + }); + + test('render story ', async ({mount, expectScreenshot}) => { + await mount(); + + await expectScreenshot(); + }); + + test('render story ', async ({mount, expectScreenshot}) => { + await mount(); + + await expectScreenshot(); + }); +}); diff --git a/src/components/AvatarStack/__tests__/stories.ts b/src/components/AvatarStack/__tests__/stories.ts new file mode 100644 index 0000000000..9662f19af8 --- /dev/null +++ b/src/components/AvatarStack/__tests__/stories.ts @@ -0,0 +1,5 @@ +import {composeStories} from '@storybook/react'; + +import * as CSFStories from '../__stories__/AvatarStack.stories'; + +export const AvatarStackStories = composeStories(CSFStories); diff --git a/src/components/AvatarStack/i18n/en.json b/src/components/AvatarStack/i18n/en.json new file mode 100644 index 0000000000..7a925041fb --- /dev/null +++ b/src/components/AvatarStack/i18n/en.json @@ -0,0 +1,3 @@ +{ + "more": ["and {{count}} more", "and {{count}} more", "and {{count}} more"] +} diff --git a/src/components/AvatarStack/i18n/index.ts b/src/components/AvatarStack/i18n/index.ts new file mode 100644 index 0000000000..600a77e666 --- /dev/null +++ b/src/components/AvatarStack/i18n/index.ts @@ -0,0 +1,8 @@ +import {addComponentKeysets} from '../../utils/addComponentKeysets'; + +import en from './en.json'; +import ru from './ru.json'; + +const COMPONENT = 'AvatarStack'; + +export default addComponentKeysets({en, ru}, COMPONENT); diff --git a/src/components/AvatarStack/i18n/ru.json b/src/components/AvatarStack/i18n/ru.json new file mode 100644 index 0000000000..8d7e17ac08 --- /dev/null +++ b/src/components/AvatarStack/i18n/ru.json @@ -0,0 +1,3 @@ +{ + "more": ["И eщё {{count}}", "И eщё {{count}}", "И eщё {{count}}"] +} diff --git a/src/components/AvatarStack/index.ts b/src/components/AvatarStack/index.ts new file mode 100644 index 0000000000..370ed35cb0 --- /dev/null +++ b/src/components/AvatarStack/index.ts @@ -0,0 +1,2 @@ +export {AvatarStack} from './AvatarStack'; +export type {AvatarStackProps, AvatarStackOverlapSize} from './types'; diff --git a/src/components/AvatarStack/types.ts b/src/components/AvatarStack/types.ts new file mode 100644 index 0000000000..c6e1af6bd9 --- /dev/null +++ b/src/components/AvatarStack/types.ts @@ -0,0 +1,41 @@ +import type React from 'react'; + +import type {AvatarSize} from '../Avatar'; + +export type AvatarStackOverlapSize = 's' | 'm' | 'l'; + +export interface AvatarStackProps { + /** Amount of avatars to be shown before more button. Default 3. */ + max?: number; + /** + * How much each avatar should overlap next one + * | Avatar sizes | Recommended overlap | + * | :----------: | :-----------------: | + * | `xs`-`m` | `s` | + * | `l` | `m` | + * | `xl` | `l` | + */ + overlapSize?: AvatarStackOverlapSize; + /** + * Size for control displaying count of extra avatars + */ + size?: AvatarSize; + className?: string; + /** + * Children would be wrapped for "stacking" + * @example + * + * + * + * + */ + children?: React.ReactNode; + /** + * Custom render for control displaying extra data + * @example + * }> + * + * + */ + renderMore?: (options: {count: number}) => React.ReactElement; +} diff --git a/src/components/Button/README.md b/src/components/Button/README.md index ad8a85289b..7b854c711a 100644 --- a/src/components/Button/README.md +++ b/src/components/Button/README.md @@ -199,7 +199,6 @@ To add an icon to the `Button`, you should use the [`Icon`](../Icon) component, `} diff --git a/src/components/PinInput/PinInput.tsx b/src/components/PinInput/PinInput.tsx index 757299e861..211dcd9286 100644 --- a/src/components/PinInput/PinInput.tsx +++ b/src/components/PinInput/PinInput.tsx @@ -3,13 +3,17 @@ import React from 'react'; import {KeyCode} from '../../constants'; -import {useControlledState, useUniqId} from '../../hooks'; +import {useControlledState, useFocusWithin, useUniqId} from '../../hooks'; +import {useFormResetHandler} from '../../hooks/private'; import type {TextInputProps, TextInputSize} from '../controls'; import {TextInput} from '../controls'; import {OuterAdditionalContent} from '../controls/common/OuterAdditionalContent/OuterAdditionalContent'; import {useDirection} from '../theme'; -import type {AriaLabelingProps, DOMProps, QAProps} from '../types'; +import type {AriaLabelingProps, DOMProps, FocusEventHandlers, QAProps} from '../types'; import {block} from '../utils/cn'; +import {filterDOMProps} from '../utils/filterDOMProps'; + +import i18n from './i18n'; import './PinInput.scss'; @@ -20,7 +24,7 @@ export interface PinInputApi { focus: () => void; } -export interface PinInputProps extends DOMProps, AriaLabelingProps, QAProps { +export interface PinInputProps extends DOMProps, AriaLabelingProps, QAProps, FocusEventHandlers { value?: string[]; defaultValue?: string[]; onUpdate?: (value: string[]) => void; @@ -30,6 +34,7 @@ export interface PinInputProps extends DOMProps, AriaLabelingProps, QAProps { type?: PinInputType; id?: string; name?: string; + form?: string; placeholder?: string; disabled?: boolean; autoFocus?: boolean; @@ -60,11 +65,14 @@ export const PinInput = React.forwardRef((props, defaultValue, onUpdate, onUpdateComplete, + onFocus, + onBlur, length = 4, size = 'm', type = 'numeric', - id, + id: idProp, name, + form, placeholder, disabled, autoFocus, @@ -78,6 +86,7 @@ export const PinInput = React.forwardRef((props, className, style, qa, + ...otherProps } = props; const refs = React.useRef>({}); const [activeIndex, setActiveIndex] = React.useState(0); @@ -245,41 +254,84 @@ export const PinInput = React.forwardRef((props, [activeIndex], ); + const formInputRef = useFormResetHandler({initialValue: values, onReset: setValues}); + + const {focusWithinProps} = useFocusWithin({ + onFocusWithin: onFocus, + onBlurWithin: onBlur, + }); + + let id = useUniqId(); + if (idProp) { + id = idProp; + } + return ( -
    +
    - {Array.from({length}).map((__, i) => ( -
    - -
    - ))} + {Array.from({length}).map((__, i) => { + const inputId = `${id}-${i}`; + const ariaLabelledBy = + props['aria-labelledby'] || props['aria-label'] + ? [inputId, props['aria-labelledby'] || id].join(' ') + : undefined; + return ( +
    + +
    + ); + })} + {name ? ( + + ) : null}
    void` | | -| onUpdateComplete | Callback fired when any of inputs change and all of them are filled | `(value: string[]) => void` | | -| otp | When set to `true` adds `autocomplete="one-time-code"` to inputs | `boolean` | | -| placeholder | Placeholder for inputs | `string` | | -| qa | HTML `data-qa` attribute, for test purposes | `string` | | -| responsive | Parent's width distributed evenly between inputs | `boolean` | | -| size | Size of input fields | `"s"` `"m"` `"l"` `"xl"` | `"m"` | -| style | HTML `style` attribute | `React.CSSProperties` | | -| type | What type of input value is allowed | `"numeric"` `"alphanumeric"` | `"numeric"` | -| validationState | Validation state. Affect component's appearance | `"invalid"` | | -| value | Current value for controlled component | `string[]` | | +| Name | Description | Type | Default | +| :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------: | :---------: | +| apiRef | Ref to the [API](#api) | `React.RefObject` | | +| aria-describedby | HTML `aria-describedby` attribute | `string` | | +| aria-label | HTML `aria-label` attribute | `string` | | +| aria-labelledby | HTML `aria-labelledby` attribute | `string` | | +| autoFocus | Whether or not to focus the first input on initial render | `boolean` | | +| className | HTML `class` attribute | `string` | | +| defaultValue | Initial value for uncontrolled component | `string[]` | | +| disabled | Toggles `disabled` state | `boolean` | | +| errorMessage | Error text placed under the bottom-start corner that shares space with the note container. Only visible when `validationState` is set to `"invalid"` | `React.ReactNode` | | +| id | HTML `id` attribute prefix for inputs. Resulting id will also contain `"-${index}"` part | `string` | | +| length | Number of input fields | `number` | `4` | +| mask | When set to `true` mask input values like password field | `boolean` | | +| name | HTML `name` attribute for input | `string` | | +| form | The associate form of the underlying input element. | `string` | | +| note | An element placed under the bottom-end corner that shares space with the error container | `React.ReactNode` | | +| onUpdate | Callback fired when any of inputs change | `(value: string[]) => void` | | +| onUpdateComplete | Callback fired when any of inputs change and all of them are filled | `(value: string[]) => void` | | +| otp | When set to `true` adds `autocomplete="one-time-code"` to inputs | `boolean` | | +| placeholder | Placeholder for inputs | `string` | | +| qa | HTML `data-qa` attribute, for test purposes | `string` | | +| responsive | Parent's width distributed evenly between inputs | `boolean` | | +| size | Size of input fields | `"s"` `"m"` `"l"` `"xl"` | `"m"` | +| style | HTML `style` attribute | `React.CSSProperties` | | +| type | What type of input value is allowed | `"numeric"` `"alphanumeric"` | `"numeric"` | +| validationState | Validation state. Affect component's appearance | `"invalid"` | | +| value | Current value for controlled component | `string[]` | | +| `onFocus` | Callback fired when the component receives focus | `(event: React.FocusEvent) => void` | | +| `onBlur` | Callback fired when the component loses focus | `(event: React.FocusEvent) => void` | | diff --git a/src/components/PinInput/__stories__/PinInput.stories.tsx b/src/components/PinInput/__stories__/PinInput.stories.tsx index 7f76309747..025c6e5880 100644 --- a/src/components/PinInput/__stories__/PinInput.stories.tsx +++ b/src/components/PinInput/__stories__/PinInput.stories.tsx @@ -5,7 +5,8 @@ import type {Meta, StoryObj} from '@storybook/react'; import {Showcase} from '../../../demo/Showcase'; import {ShowcaseItem} from '../../../demo/ShowcaseItem'; -import type {PinInputProps} from '../PinInput'; +import {Flex} from '../../layout'; +import type {PinInputApi, PinInputProps} from '../PinInput'; import {PinInput} from '../PinInput'; export default { @@ -19,6 +20,8 @@ export const Default: Story = { args: { onUpdate: action('onUpdate'), onUpdateComplete: action('onUpdateComplete'), + onFocus: action('onFocus'), + onBlur: action('onBlur'), 'aria-label': 'PIN code', }, }; @@ -133,3 +136,36 @@ export const Responsive: Story = { responsive: true, }, }; + +export const WithLabel = { + render: function WithLabel(args) { + const id = args.id ?? 'pin-input'; + const labelId = React.useId(); + const refApi = React.useRef(null); + /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */ + return ( + + + + + ); + /* eslint-enable jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */ + }, + args: { + ...Default.args, + }, +} satisfies Story; diff --git a/src/components/PinInput/__tests__/PinInput.test.tsx b/src/components/PinInput/__tests__/PinInput.test.tsx index 6e92c2a56a..0c1aa48dd9 100644 --- a/src/components/PinInput/__tests__/PinInput.test.tsx +++ b/src/components/PinInput/__tests__/PinInput.test.tsx @@ -287,4 +287,93 @@ describe('PinInput', () => { expect(inputs[1]).toHaveFocus(); }); }); + + describe('Form', () => { + test('should submit empty value by default', async () => { + let value; + const onSubmit = jest.fn((e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + value = [...formData.entries()]; + }); + render( +
    + + + , + ); + await userEvent.click(screen.getByTestId('submit')); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(value).toEqual([['pin-field', '']]); + }); + + test('should submit default value', async () => { + let value; + const onSubmit = jest.fn((e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + value = [...formData.entries()]; + }); + + render( +
    + + + , + ); + await userEvent.click(screen.getByTestId('submit')); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(value).toEqual([['pin-field', '123']]); + }); + + test('should submit controlled value', async () => { + let value; + const onSubmit = jest.fn((e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + value = [...formData.entries()]; + }); + render( +
    + + + , + ); + await userEvent.click(screen.getByTestId('submit')); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(value).toEqual([['pin-field', '123']]); + }); + test('supports form reset', async () => { + function Test() { + const [value, setValue] = React.useState(['1', '2', '3']); + return ( +
    + + + + ); + } + + render(); + // eslint-disable-next-line testing-library/no-node-access + const inputs = document.querySelectorAll('[name=pin-field]'); + expect(inputs.length).toBe(1); + expect(inputs[0]).toHaveValue('123'); + + await userEvent.tab(); + await userEvent.keyboard('4587'); + + expect(inputs[0]).toHaveValue('4587'); + + const button = screen.getByTestId('reset'); + await userEvent.click(button); + expect(inputs[0]).toHaveValue('123'); + }); + }); }); diff --git a/src/components/PinInput/i18n/en.json b/src/components/PinInput/i18n/en.json new file mode 100644 index 0000000000..33fd0a2240 --- /dev/null +++ b/src/components/PinInput/i18n/en.json @@ -0,0 +1,3 @@ +{ + "label_one-of": "{{number}} of {{count}}, " +} diff --git a/src/components/PinInput/i18n/index.ts b/src/components/PinInput/i18n/index.ts new file mode 100644 index 0000000000..c6fc95f1e8 --- /dev/null +++ b/src/components/PinInput/i18n/index.ts @@ -0,0 +1,8 @@ +import {addComponentKeysets} from '../../utils/addComponentKeysets'; + +import en from './en.json'; +import ru from './ru.json'; + +const COMPONENT = 'PinInput'; + +export default addComponentKeysets({en, ru}, COMPONENT); diff --git a/src/components/PinInput/i18n/ru.json b/src/components/PinInput/i18n/ru.json new file mode 100644 index 0000000000..e2d264141f --- /dev/null +++ b/src/components/PinInput/i18n/ru.json @@ -0,0 +1,3 @@ +{ + "label_one-of": "{{number}} из {{count}}, " +} diff --git a/src/components/Sheet/SheetContent.tsx b/src/components/Sheet/SheetContent.tsx index 63e549acc8..0f3cdf62b8 100644 --- a/src/components/Sheet/SheetContent.tsx +++ b/src/components/Sheet/SheetContent.tsx @@ -93,7 +93,7 @@ class SheetContent extends React.Component { + private getAvailableContentHeight = (sheetHeight: number) => { const availableViewportHeight = window.innerHeight * MAX_CONTENT_HEIGHT_FROM_VIEWPORT_COEFFICIENT - this.sheetTopHeight; - const resultHeight = + const availableContentHeight = sheetHeight >= availableViewportHeight ? availableViewportHeight : sheetHeight; - return resultHeight; + return availableContentHeight; }; private show = () => { @@ -435,22 +435,22 @@ class SheetContent extends React.Component sheetHeight + this.state.prevSheetHeight > sheetContentHeight ? `height 0s ease ${TRANSITION_DURATION}` : 'none'; - this.sheetContentRef.current.style.height = `${resultHeight - this.sheetTopHeight}px`; - this.sheetRef.current.style.transform = `translate3d(0, -${resultHeight}px, 0)`; - this.setState({prevSheetHeight: sheetHeight, inWindowResizeScope: false}); + this.sheetContentRef.current.style.height = `${availableContentHeight}px`; + this.sheetRef.current.style.transform = `translate3d(0, -${availableContentHeight + this.sheetTopHeight}px, 0)`; + this.setState({prevSheetHeight: sheetContentHeight, inWindowResizeScope: false}); }; private addListeners() { diff --git a/src/components/Sheet/__snapshots__/Sheet.visual.test.tsx-snapshots/Sheet-render-story-Default-dark-chromium-linux.png b/src/components/Sheet/__snapshots__/Sheet.visual.test.tsx-snapshots/Sheet-render-story-Default-dark-chromium-linux.png new file mode 100644 index 0000000000..21ceb9988d Binary files /dev/null and b/src/components/Sheet/__snapshots__/Sheet.visual.test.tsx-snapshots/Sheet-render-story-Default-dark-chromium-linux.png differ diff --git a/src/components/Sheet/__snapshots__/Sheet.visual.test.tsx-snapshots/Sheet-render-story-Default-dark-webkit-linux.png b/src/components/Sheet/__snapshots__/Sheet.visual.test.tsx-snapshots/Sheet-render-story-Default-dark-webkit-linux.png new file mode 100644 index 0000000000..34fe049d7e Binary files /dev/null and b/src/components/Sheet/__snapshots__/Sheet.visual.test.tsx-snapshots/Sheet-render-story-Default-dark-webkit-linux.png differ diff --git a/src/components/Sheet/__snapshots__/Sheet.visual.test.tsx-snapshots/Sheet-render-story-Default-light-chromium-linux.png b/src/components/Sheet/__snapshots__/Sheet.visual.test.tsx-snapshots/Sheet-render-story-Default-light-chromium-linux.png new file mode 100644 index 0000000000..197488bf20 Binary files /dev/null and b/src/components/Sheet/__snapshots__/Sheet.visual.test.tsx-snapshots/Sheet-render-story-Default-light-chromium-linux.png differ diff --git a/src/components/Sheet/__snapshots__/Sheet.visual.test.tsx-snapshots/Sheet-render-story-Default-light-webkit-linux.png b/src/components/Sheet/__snapshots__/Sheet.visual.test.tsx-snapshots/Sheet-render-story-Default-light-webkit-linux.png new file mode 100644 index 0000000000..8d3788f05f Binary files /dev/null and b/src/components/Sheet/__snapshots__/Sheet.visual.test.tsx-snapshots/Sheet-render-story-Default-light-webkit-linux.png differ diff --git a/src/components/Sheet/__stories__/DefaultShowcase/DefaultShowcase.stories.tsx b/src/components/Sheet/__stories__/DefaultShowcase/DefaultShowcase.stories.tsx index cdd4bb1769..7b0f1bde4a 100644 --- a/src/components/Sheet/__stories__/DefaultShowcase/DefaultShowcase.stories.tsx +++ b/src/components/Sheet/__stories__/DefaultShowcase/DefaultShowcase.stories.tsx @@ -6,6 +6,7 @@ import {Button, Checkbox, TextInput} from '../../../'; import {cn} from '../../../utils/cn'; import {Sheet} from '../../Sheet'; import type {SheetProps} from '../../Sheet'; +import {DEFAULT_SHEET_QA} from '../constants'; import './DefaultShowcase.scss'; @@ -71,6 +72,7 @@ export const Default: StoryFn = ({ visible={visible} onClose={() => setVisible(false)} title={withTitle ? 'Sheet title' : undefined} + qa={DEFAULT_SHEET_QA} >
    diff --git a/src/components/Sheet/__stories__/constants.ts b/src/components/Sheet/__stories__/constants.ts new file mode 100644 index 0000000000..b4fdb4ce94 --- /dev/null +++ b/src/components/Sheet/__stories__/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_SHEET_QA = 'default-sheet-qa'; diff --git a/src/components/Sheet/__tests__/Sheet.visual.test.tsx b/src/components/Sheet/__tests__/Sheet.visual.test.tsx new file mode 100644 index 0000000000..dd3192f7f8 --- /dev/null +++ b/src/components/Sheet/__tests__/Sheet.visual.test.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import {expect} from '@playwright/test'; + +import {test} from '~playwright/core'; + +import {DEFAULT_SHEET_QA} from '../__stories__/constants'; + +import {SheetStories} from './helpersPlaywright'; + +test.describe('Sheet', () => { + test('render story: ', async ({page, mount, expectScreenshot}) => { + await mount(); + + await page.getByRole('button').click(); + + const sheetLocator = page.locator(`[data-qa=${DEFAULT_SHEET_QA}]`); + + await expect(sheetLocator).toBeVisible(); + + await expectScreenshot({ + animations: 'disabled', + component: sheetLocator, + }); + }); +}); diff --git a/src/components/Sheet/__tests__/helpersPlaywright.tsx b/src/components/Sheet/__tests__/helpersPlaywright.tsx new file mode 100644 index 0000000000..0da21bf54e --- /dev/null +++ b/src/components/Sheet/__tests__/helpersPlaywright.tsx @@ -0,0 +1,5 @@ +import {composeStories} from '@storybook/react'; + +import * as stories from '../__stories__/DefaultShowcase/DefaultShowcase.stories'; + +export const SheetStories = composeStories(stories); diff --git a/src/components/TreeList/TreeList.tsx b/src/components/TreeList/TreeList.tsx index a31acea0e9..b48fa298d6 100644 --- a/src/components/TreeList/TreeList.tsx +++ b/src/components/TreeList/TreeList.tsx @@ -78,6 +78,7 @@ export const TreeList = ({ if (propsRenderItem) { return propsRenderItem({ + id: itemId, data: renderState.data, props: renderState.props, context: renderState.context, diff --git a/src/components/TreeList/__stories__/stories/WithDndListStory.tsx b/src/components/TreeList/__stories__/stories/WithDndListStory.tsx index ee4d963a23..22bcb3827d 100644 --- a/src/components/TreeList/__stories__/stories/WithDndListStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithDndListStory.tsx @@ -28,6 +28,7 @@ const DraggableListItem = ({ {...provided?.draggableProps} ref={provided?.innerRef} {...props} + role="option" /> ); }; diff --git a/src/components/TreeList/types.ts b/src/components/TreeList/types.ts index 466bcdec5f..9dbb33aa22 100644 --- a/src/components/TreeList/types.ts +++ b/src/components/TreeList/types.ts @@ -12,6 +12,7 @@ import type { } from '../useList'; export type TreeListRenderItem = (props: { + id: ListItemId; data: T; // required item props to render props: RenderItemProps; diff --git a/src/components/TreeSelect/TreeSelect.tsx b/src/components/TreeSelect/TreeSelect.tsx index db8f814f1b..73084f2ebd 100644 --- a/src/components/TreeSelect/TreeSelect.tsx +++ b/src/components/TreeSelect/TreeSelect.tsx @@ -47,6 +47,8 @@ export const TreeSelect = React.forwardRef(function TreeSelect( items, value: propsValue, defaultValue, + placeholder, + disabled = false, withExpandedState = true, defaultExpandedState = 'expanded', onClose, @@ -128,7 +130,8 @@ export const TreeSelect = React.forwardRef(function TreeSelect( // restoring focus when popup opens React.useLayoutEffect(() => { if (open) { - containerRef.current?.focus(); + // for some reason popup position on page may be wrong calculated. `preventScroll` prevent page gap in that cases + containerRef.current?.focus({preventScroll: true}); } return () => list.state.setActiveItemId(undefined); // reset active item on popup close @@ -152,11 +155,13 @@ export const TreeSelect = React.forwardRef(function TreeSelect( const controlProps: TreeSelectRenderControlProps = { list, open, + placeholder, toggleOpen, clearValue: () => list.state.setSelected({}), ref: handleControlRef, size, value, + disabled, id: treeSelectId, activeItemId: list.state.activeItemId, title, diff --git a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx index 7437235538..5c5c25035f 100644 --- a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx +++ b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx @@ -10,6 +10,8 @@ import type {TreeSelectProps} from '../types'; import {InfinityScrollExample} from './components/InfinityScrollExample'; import type {InfinityScrollExampleProps} from './components/InfinityScrollExample'; +import {WithDisabledElementsExample} from './components/WithDisabledElementsExample'; +import type {WithDisabledElementsExampleProps} from './components/WithDisabledElementsExample'; import {WithDndListExample} from './components/WithDndListExample'; import type {WithDndListExampleProps} from './components/WithDndListExample'; import {WithFiltrationAndControlsExample} from './components/WithFiltrationAndControlsExample'; @@ -51,6 +53,7 @@ const DefaultTemplate: StoryFn< x} onItemClick={({id, list}) => { @@ -114,3 +117,12 @@ WithDndList.parameters = { // https://github.com/atlassian/react-beautiful-dnd/issues/2350 disableStrictMode: true, }; + +const WithDisabledElementsTemplate: StoryFn = (props) => { + return ; +}; +export const WithDisabledElements = WithDisabledElementsTemplate.bind({}); + +WithDisabledElements.args = { + size: 'l', +}; diff --git a/src/components/TreeSelect/__stories__/components/WithDisabledElementsExample.tsx b/src/components/TreeSelect/__stories__/components/WithDisabledElementsExample.tsx new file mode 100644 index 0000000000..38c991e6ae --- /dev/null +++ b/src/components/TreeSelect/__stories__/components/WithDisabledElementsExample.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import type {ListItemType} from '../../../useList'; +import {TreeSelect} from '../../TreeSelect'; +import type {TreeSelectProps} from '../../types'; + +interface Entity { + text: string; + id: string; +} + +export interface WithDisabledElementsExampleProps + extends Omit, 'items' | 'mapItemDataToProps'> {} + +const items: ListItemType[] = [ + { + data: {id: '1', text: 'default disabled'}, + disabled: true, + }, + { + data: {id: '2', text: 'two'}, + disabled: true, + }, + { + data: {id: '3', text: 'default selected'}, + }, + { + data: {id: '4', text: 'four'}, + disabled: true, + }, + { + data: {id: '5', text: 'five'}, + }, +]; + +export const WithDisabledElementsExample = ({...props}: WithDisabledElementsExampleProps) => { + const containerRef = React.useRef(null); + + return ( + id} + containerRef={containerRef} + mapItemDataToProps={({text}) => ({title: text})} + /> + ); +}; diff --git a/src/components/TreeSelect/types.ts b/src/components/TreeSelect/types.ts index 23e1833c23..8d95ba67e8 100644 --- a/src/components/TreeSelect/types.ts +++ b/src/components/TreeSelect/types.ts @@ -15,6 +15,8 @@ import type {UseListParsedStateProps} from '../useList/hooks/useListParsedState' export type TreeSelectRenderControlProps = { list: UseListResult; open: boolean; + disabled?: boolean; + placeholder?: string; toggleOpen(): void; clearValue(): void; ref: React.Ref; @@ -44,6 +46,8 @@ export interface TreeSelectProps */ title?: string; value?: ListItemId[]; + disabled?: boolean; + placeholder?: string; defaultValue?: ListItemId[] | undefined; popupClassName?: string; popupWidth?: SelectPopupProps['width']; diff --git a/src/components/controls/TextArea/TextArea.scss b/src/components/controls/TextArea/TextArea.scss index 2e77a9277c..72dc5a1ca7 100644 --- a/src/components/controls/TextArea/TextArea.scss +++ b/src/components/controls/TextArea/TextArea.scss @@ -22,7 +22,10 @@ $block: '.#{variables.$ns}text-area'; &__content { box-sizing: border-box; display: flex; - width: 100%; + + width: inherit; + height: inherit; + background-color: var(--g-text-area-background-color, var(--_--background-color)); border-width: var(--g-text-area-border-width, var(--_--border-width)); border-style: solid; diff --git a/src/components/controls/TextArea/__stories__/TextAreaShowcase.scss b/src/components/controls/TextArea/__stories__/TextAreaShowcase.scss index ed58d9c8e1..2358d7e6f5 100644 --- a/src/components/controls/TextArea/__stories__/TextAreaShowcase.scss +++ b/src/components/controls/TextArea/__stories__/TextAreaShowcase.scss @@ -29,6 +29,10 @@ padding: 20px; } + &__custom-height { + height: 333px; + } + &__title { grid-area: title; margin: 0; diff --git a/src/components/controls/TextArea/__stories__/TextAreaShowcase.tsx b/src/components/controls/TextArea/__stories__/TextAreaShowcase.tsx index 5fbacd8127..3c0a2918d1 100644 --- a/src/components/controls/TextArea/__stories__/TextAreaShowcase.tsx +++ b/src/components/controls/TextArea/__stories__/TextAreaShowcase.tsx @@ -39,6 +39,11 @@ export function TextAreaShowcase() { maxRows={4} hasClear /> +