diff --git a/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _disabled.snap.png b/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _disabled.snap.png new file mode 100644 index 0000000000..b1ac2f0d5f Binary files /dev/null and b/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _disabled.snap.png differ diff --git a/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _placement.snap.png b/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _placement.snap.png new file mode 100644 index 0000000000..ac78e3e673 Binary files /dev/null and b/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _placement.snap.png differ diff --git a/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _size.snap.png b/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _size.snap.png new file mode 100644 index 0000000000..d8e980fe78 Binary files /dev/null and b/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _size.snap.png differ diff --git a/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _view.snap.png b/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _view.snap.png new file mode 100644 index 0000000000..c5dca9bc5a Binary files /dev/null and b/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _view.snap.png differ diff --git a/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- simple.snap.png b/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- simple.snap.png new file mode 100644 index 0000000000..9f93d123e5 Binary files /dev/null and b/cypress/snapshots/b2c/components/Slider/Slider.component-test.tsx/plasma-web Slider -- simple.snap.png differ diff --git a/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _disabled.snap.png b/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _disabled.snap.png new file mode 100644 index 0000000000..910823a0e1 Binary files /dev/null and b/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _disabled.snap.png differ diff --git a/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _placement.snap.png b/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _placement.snap.png new file mode 100644 index 0000000000..b86e7455f8 Binary files /dev/null and b/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _placement.snap.png differ diff --git a/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _size.snap.png b/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _size.snap.png new file mode 100644 index 0000000000..104cc896a6 Binary files /dev/null and b/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _size.snap.png differ diff --git a/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _view.snap.png b/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _view.snap.png new file mode 100644 index 0000000000..878310226d Binary files /dev/null and b/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- _view.snap.png differ diff --git a/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- simple.snap.png b/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- simple.snap.png new file mode 100644 index 0000000000..5a2dd31dc8 Binary files /dev/null and b/cypress/snapshots/web/components/Slider/Slider.component-test.tsx/plasma-web Slider -- simple.snap.png differ diff --git a/packages/caldera-online/api/caldera-online.api.md b/packages/caldera-online/api/caldera-online.api.md index 5a9f56764d..26e91a9dc8 100644 --- a/packages/caldera-online/api/caldera-online.api.md +++ b/packages/caldera-online/api/caldera-online.api.md @@ -231,8 +231,8 @@ id?: string | undefined; disabled?: boolean | undefined; label?: ReactNode; role?: string | undefined; -contentLeft?: string | number | boolean | ReactElement> | FunctionComponent | ReactFragment | ReactPortal | ComponentClass | null | undefined; -contentRight?: string | number | boolean | ReactElement> | FunctionComponent | ReactFragment | ReactPortal | ComponentClass | null | undefined; +contentLeft?: string | number | boolean | ReactFragment | ReactPortal | ReactElement> | FunctionComponent | ComponentClass | null | undefined; +contentRight?: string | number | boolean | ReactFragment | ReactPortal | ReactElement> | FunctionComponent | ComponentClass | null | undefined; name?: string | undefined; checked?: boolean | undefined; text?: string | undefined; diff --git a/packages/plasma-b2c/api/plasma-b2c.api.md b/packages/plasma-b2c/api/plasma-b2c.api.md index 1e4428d5ce..74bffb6375 100644 --- a/packages/plasma-b2c/api/plasma-b2c.api.md +++ b/packages/plasma-b2c/api/plasma-b2c.api.md @@ -92,6 +92,7 @@ import { defaultValidate } from '@salutejs/plasma-hope'; import { DisabledProps } from '@salutejs/plasma-core'; import { DividerProps } from '@salutejs/plasma-new-hope/styled-components'; import { dividerTokens } from '@salutejs/plasma-new-hope/styled-components'; +import { DoubleSliderProps } from '@salutejs/plasma-new-hope/styled-components'; import { DrawerContentProps } from '@salutejs/plasma-new-hope/styled-components'; import { DrawerFooterProps } from '@salutejs/plasma-new-hope/styled-components'; import { DrawerHeaderProps } from '@salutejs/plasma-new-hope/styled-components'; @@ -199,6 +200,7 @@ import { setRef } from '@salutejs/plasma-core'; import { shadows } from '@salutejs/plasma-core'; import { ShiftProps } from '@salutejs/plasma-core'; import { ShowToastArgs } from '@salutejs/plasma-new-hope/styled-components'; +import { SingleSliderProps } from '@salutejs/plasma-new-hope/styled-components'; import { SkeletonGradientProps } from '@salutejs/plasma-core'; import { SkeletonGradientProps as SkeletonGradientProps_2 } from '@salutejs/plasma-new-hope/styled-components'; import { SkeletonSizeProps } from '@salutejs/plasma-new-hope/types/components/Skeleton/Skeleton.types'; @@ -773,11 +775,11 @@ l: string; view: { default: string; }; -}> & ((Omit, "type" | "target" | "onChange" | "size" | "value" | "checked" | "minLength" | "maxLength"> & CustomComboboxProps & { +}> & ((Omit, "onChange" | "type" | "target" | "size" | "value" | "checked" | "minLength" | "maxLength"> & CustomComboboxProps & { valueType?: "single" | undefined; value?: ComboboxPrimitiveValue | undefined; onChangeValue?: ((value?: ComboboxPrimitiveValue | undefined) => void) | undefined; -} & RefAttributes) | (Omit, "type" | "target" | "onChange" | "size" | "value" | "checked" | "minLength" | "maxLength"> & CustomComboboxProps & { +} & RefAttributes) | (Omit, "onChange" | "type" | "target" | "size" | "value" | "checked" | "minLength" | "maxLength"> & CustomComboboxProps & { valueType: "multiple"; value?: ComboboxPrimitiveValue[] | undefined; onChangeValue?: ((value?: ComboboxPrimitiveValue[] | undefined) => void) | undefined; @@ -1125,7 +1127,7 @@ headline5: string; // @public const Image_2: FunctionComponent & ImgHTMLAttributes & { -base?: "div" | "img" | undefined; +base?: "img" | "div" | undefined; ratio?: "16 / 9" | "1 / 1" | "1/1" | "3 / 4" | "3/4" | "4 / 3" | "4/3" | "9 / 16" | "9/16" | "16/9" | "1 / 2" | "1/2" | "2 / 1" | "2/1" | undefined; customRatio?: string | undefined; } & RefAttributes>; @@ -1558,8 +1560,22 @@ export { ShowToastArgs } export { SkeletonGradientProps } -// @public (undocumented) -export const Slider: (props: SliderProps) => JSX.Element; +// @public +export const Slider: FunctionComponent & ((SingleSliderProps & RefAttributes) | (DoubleSliderProps & RefAttributes))>; export { SliderProps } diff --git a/packages/plasma-b2c/src/components/Slider/Slider.component-test.tsx b/packages/plasma-b2c/src/components/Slider/Slider.component-test.tsx index 4c9a39bbd8..1f6917482a 120000 --- a/packages/plasma-b2c/src/components/Slider/Slider.component-test.tsx +++ b/packages/plasma-b2c/src/components/Slider/Slider.component-test.tsx @@ -1 +1 @@ -../../../../plasma-core/src/components/Slider/Slider.component-test.tsx \ No newline at end of file +../../../../plasma-web/src/components/Slider/Slider.component-test.tsx \ No newline at end of file diff --git a/packages/plasma-b2c/src/components/Slider/Slider.config.ts b/packages/plasma-b2c/src/components/Slider/Slider.config.ts new file mode 100644 index 0000000000..065c9212f8 --- /dev/null +++ b/packages/plasma-b2c/src/components/Slider/Slider.config.ts @@ -0,0 +1,241 @@ +import { css, sliderTokens } from '@salutejs/plasma-new-hope/styled-components'; + +export const config = { + defaults: { + view: 'default', + size: 'm', + }, + variations: { + view: { + default: css` + ${sliderTokens.labelColor}: var(--text-primary); + + ${sliderTokens.rangeValueColor}: var(--text-secondary); + + ${sliderTokens.thumbBorderColor}: var(--surface-solid-tertiary); + ${sliderTokens.thumbBackgroundColor}: var(--on-light-surface-solid-card); + ${sliderTokens.thumbFocusBorderColor}: var(--surface-solid-default); + + ${sliderTokens.railBackgroundColor}: var(--surface-solid-tertiary); + + ${sliderTokens.fillColor}: var(--surface-solid-default); + + ${sliderTokens.textFieldColor}: var(--text-secondary); + ${sliderTokens.textFieldBackgroundColor}: var(--surface-transparent-primary); + ${sliderTokens.textFieldCaretColor}: var(--text-primary); + ${sliderTokens.textFieldPlaceholderColor}: var(--text-secondary); + ${sliderTokens.textFiledFocusColor}: var(--text-primary); + ${sliderTokens.textFieldActiveColor}: var(--text-primary); + `, + accent: css` + ${sliderTokens.labelColor}: var(--text-primary); + + ${sliderTokens.rangeValueColor}: var(--text-secondary); + + ${sliderTokens.thumbBorderColor}: var(--surface-solid-tertiary); + ${sliderTokens.thumbBackgroundColor}: var(--on-light-surface-solid-card); + ${sliderTokens.thumbFocusBorderColor}: var(--surface-accent); + + ${sliderTokens.railBackgroundColor}: var(--surface-solid-tertiary); + + ${sliderTokens.fillColor}: var(--surface-accent); + + ${sliderTokens.textFieldColor}: var(--text-secondary); + ${sliderTokens.textFieldBackgroundColor}: var(--surface-transparent-primary); + ${sliderTokens.textFieldCaretColor}: var(--text-primary); + ${sliderTokens.textFieldPlaceholderColor}: var(--text-secondary); + ${sliderTokens.textFiledFocusColor}: var(--text-primary); + ${sliderTokens.textFieldActiveColor}: var(--text-primary); + `, + gradient: css` + ${sliderTokens.labelColor}: var(--text-primary); + + ${sliderTokens.rangeValueColor}: var(--text-secondary); + + ${sliderTokens.thumbBorderColor}: var(--surface-solid-tertiary); + ${sliderTokens.thumbBackgroundColor}: var(--on-light-surface-solid-card); + ${sliderTokens.thumbFocusBorderColor}: var(--surface-accent-gradient); + + ${sliderTokens.railBackgroundColor}: var(--surface-solid-tertiary); + + ${sliderTokens.fillColor}: var(--surface-accent-gradient); + + ${sliderTokens.textFieldColor}: var(--text-secondary); + ${sliderTokens.textFieldBackgroundColor}: var(--surface-transparent-primary); + ${sliderTokens.textFieldCaretColor}: var(--text-primary); + ${sliderTokens.textFieldPlaceholderColor}: var(--text-secondary); + ${sliderTokens.textFiledFocusColor}: var(--text-primary); + ${sliderTokens.textFieldActiveColor}: var(--text-primary); + `, + }, + size: { + l: css` + ${sliderTokens.height}: 1.5rem; + ${sliderTokens.doubleWrapperGap}: 0.375rem; + + ${sliderTokens.labelWrapperGap}: 0.25rem; + ${sliderTokens.labelWrapperMarginBottom}: 0.25rem; + ${sliderTokens.labelWrapperMarginRight}: 0.875rem; + + ${sliderTokens.labelFontFamily}: var(--plasma-typo-body-l-font-family); + ${sliderTokens.labelFontSize}: var(--plasma-typo-body-l-font-size); + ${sliderTokens.labelFontStyle}: var(--plasma-typo-body-l-font-style); + ${sliderTokens.labelFontWeight}: var(--plasma-typo-body-l-font-weight); + ${sliderTokens.labelLetterSpacing}: var(--plasma-typo-body-l-letter-spacing); + ${sliderTokens.labelLineHeight}: var(--plasma-typo-body-l-line-height); + + ${sliderTokens.rangeMinValueMargin}: 0.75rem; + ${sliderTokens.rangeMaxValueMargin}: 0.75rem; + ${sliderTokens.rangeValueBottomOffset}: -1.25rem; + + ${sliderTokens.rangeValueFontFamily}: var(--plasma-typo-body-s-font-family); + ${sliderTokens.rangeValueFontSize}: var(--plasma-typo-body-s-font-size); + ${sliderTokens.rangeValueFontStyle}: var(--plasma-typo-body-s-font-style); + ${sliderTokens.rangeValueFontWeight}: var(--plasma-typo-body-s-font-weight); + ${sliderTokens.rangeValueLetterSpacing}: var(--plasma-typo-body-s-letter-spacing); + ${sliderTokens.rangeValueLineHeight}: var(--plasma-typo-body-s-line-height); + + ${sliderTokens.thumbSize}: 1.5rem; + ${sliderTokens.thumbBorder}: 0.125rem solid; + + ${sliderTokens.currentValueTopOffset}: 1.625rem; + + ${sliderTokens.currentValueFontFamily}: var(--plasma-typo-text-s-font-family); + ${sliderTokens.currentValueFontSize}: var(--plasma-typo-text-s-font-size); + ${sliderTokens.currentValueFontStyle}: var(--plasma-typo-text-s-font-style); + ${sliderTokens.currentValueFontWeight}: var(--plasma-typo-text-s-font-weight); + ${sliderTokens.currentValueLetterSpacing}: var(--plasma-typo-text-s-letter-spacing); + ${sliderTokens.currentValueLineHeight}: var(--plasma-typo-text-s-line-height); + + ${sliderTokens.railHeight}: 0.25rem; + ${sliderTokens.railBorderRadius}: 0.125rem; + ${sliderTokens.railIndent}: 0.75rem; + + ${sliderTokens.textFieldWrapperGap}: 0.125rem; + + ${sliderTokens.textFieldHeight}: 3.5rem; + ${sliderTokens.textFieldPadding}: 1.25rem 1rem 1.25rem 1rem; + ${sliderTokens.textFieldBorderRadius}: 0.75rem; + ${sliderTokens.textFieldFontFamily}: var(--plasma-typo-body-l-font-family); + ${sliderTokens.textFieldFontSize}: var(--plasma-typo-body-l-font-size); + ${sliderTokens.textFieldFontStyle}: var(--plasma-typo-body-l-font-style); + ${sliderTokens.textFieldFontWeight}: var(--plasma-typo-body-l-font-weight); + ${sliderTokens.textFieldLetterSpacing}: var(--plasma-typo-body-l-letter-spacing); + ${sliderTokens.textFieldLineHeight}: var(--plasma-typo-body-l-line-height); + `, + m: css` + ${sliderTokens.height}: 1.5rem; + ${sliderTokens.doubleWrapperGap}: 0.375rem; + + ${sliderTokens.labelWrapperGap}: 0.25rem; + ${sliderTokens.labelWrapperMarginBottom}: 0.25rem; + ${sliderTokens.labelWrapperMarginRight}: 0.875rem; + + ${sliderTokens.labelFontFamily}: var(--plasma-typo-body-m-font-family); + ${sliderTokens.labelFontSize}: var(--plasma-typo-body-m-font-size); + ${sliderTokens.labelFontStyle}: var(--plasma-typo-body-m-font-style); + ${sliderTokens.labelFontWeight}: var(--plasma-typo-body-m-font-weight); + ${sliderTokens.labelLetterSpacing}: var(--plasma-typo-body-m-letter-spacing); + ${sliderTokens.labelLineHeight}: var(--plasma-typo-body-m-line-height); + + ${sliderTokens.rangeMinValueMargin}: 0.75rem; + ${sliderTokens.rangeMaxValueMargin}: 0.75rem; + ${sliderTokens.rangeValueBottomOffset}: -1.25rem; + + ${sliderTokens.rangeValueFontFamily}: var(--plasma-typo-body-s-font-family); + ${sliderTokens.rangeValueFontSize}: var(--plasma-typo-body-s-font-size); + ${sliderTokens.rangeValueFontStyle}: var(--plasma-typo-body-s-font-style); + ${sliderTokens.rangeValueFontWeight}: var(--plasma-typo-body-s-font-weight); + ${sliderTokens.rangeValueLetterSpacing}: var(--plasma-typo-body-s-letter-spacing); + ${sliderTokens.rangeValueLineHeight}: var(--plasma-typo-body-s-line-height); + + ${sliderTokens.thumbSize}: 1.5rem; + ${sliderTokens.thumbBorder}: 0.125rem solid; + + ${sliderTokens.currentValueTopOffset}: 1.75rem; + + ${sliderTokens.currentValueFontFamily}: var(--plasma-typo-text-xs-font-family); + ${sliderTokens.currentValueFontSize}: var(--plasma-typo-text-xs-font-size); + ${sliderTokens.currentValueFontStyle}: var(--plasma-typo-text-xs-font-style); + ${sliderTokens.currentValueFontWeight}: var(--plasma-typo-text-xs-font-weight); + ${sliderTokens.currentValueLetterSpacing}: var(--plasma-typo-text-xs-letter-spacing); + ${sliderTokens.currentValueLineHeight}: var(--plasma-typo-text-xs-line-height); + + ${sliderTokens.railHeight}: 0.25rem; + ${sliderTokens.railBorderRadius}: 0.125rem; + ${sliderTokens.railIndent}: 0.75rem; + + ${sliderTokens.textFieldWrapperGap}: 0.125rem; + + ${sliderTokens.textFieldHeight}: 3rem; + ${sliderTokens.textFieldPadding}: 0.875rem 1rem 0.875rem 1rem; + ${sliderTokens.textFieldBorderRadius}: 0.75rem; + ${sliderTokens.textFieldFontFamily}: var(--plasma-typo-body-m-font-family); + ${sliderTokens.textFieldFontSize}: var(--plasma-typo-body-m-font-size); + ${sliderTokens.textFieldFontStyle}: var(--plasma-typo-body-m-font-style); + ${sliderTokens.textFieldFontWeight}: var(--plasma-typo-body-m-font-weight); + ${sliderTokens.textFieldLetterSpacing}: var(--plasma-typo-body-m-letter-spacing); + ${sliderTokens.textFieldLineHeight}: var(--plasma-typo-body-m-line-height); + `, + s: css` + ${sliderTokens.height}: 1rem; + ${sliderTokens.doubleWrapperGap}: 0.375rem; + + ${sliderTokens.labelWrapperGap}: 0.25rem; + ${sliderTokens.labelWrapperMarginBottom}: 0.25rem; + ${sliderTokens.labelWrapperMarginRight}: 0.875rem; + + ${sliderTokens.labelFontFamily}: var(--plasma-typo-body-s-font-family); + ${sliderTokens.labelFontSize}: var(--plasma-typo-body-s-font-size); + ${sliderTokens.labelFontStyle}: var(--plasma-typo-body-s-font-style); + ${sliderTokens.labelFontWeight}: var(--plasma-typo-body-s-font-weight); + ${sliderTokens.labelLetterSpacing}: var(--plasma-typo-body-s-letter-spacing); + ${sliderTokens.labelLineHeight}: var(--plasma-typo-body-s-line-height); + + ${sliderTokens.rangeMinValueMargin}: 0.5rem; + ${sliderTokens.rangeMaxValueMargin}: 0.5rem; + ${sliderTokens.rangeValueBottomOffset}: -1.25rem; + + ${sliderTokens.rangeValueFontFamily}: var(--plasma-typo-body-xs-font-family); + ${sliderTokens.rangeValueFontSize}: var(--plasma-typo-body-xs-font-size); + ${sliderTokens.rangeValueFontStyle}: var(--plasma-typo-body-xs-font-style); + ${sliderTokens.rangeValueFontWeight}: var(--plasma-typo-body-xs-font-weight); + ${sliderTokens.rangeValueLetterSpacing}: var(--plasma-typo-body-xs-letter-spacing); + ${sliderTokens.rangeValueLineHeight}: var(--plasma-typo-body-xs-line-height); + + ${sliderTokens.thumbSize}: 1rem; + ${sliderTokens.thumbBorder}: 0.125rem solid; + + ${sliderTokens.currentValueTopOffset}: 1.25rem; + + ${sliderTokens.currentValueFontFamily}: var(--plasma-typo-text-xs-font-family); + ${sliderTokens.currentValueFontSize}: var(--plasma-typo-text-xs-font-size); + ${sliderTokens.currentValueFontStyle}: var(--plasma-typo-text-xs-font-style); + ${sliderTokens.currentValueFontWeight}: var(--plasma-typo-text-xs-font-weight); + ${sliderTokens.currentValueLetterSpacing}: var(--plasma-typo-text-xs-letter-spacing); + ${sliderTokens.currentValueLineHeight}: var(--plasma-typo-text-xs-line-height); + + ${sliderTokens.railHeight}: 0.25rem; + ${sliderTokens.railBorderRadius}: 0.125rem; + ${sliderTokens.railIndent}: 0.75rem; + + ${sliderTokens.textFieldWrapperGap}: 0.125rem; + + ${sliderTokens.textFieldHeight}: 2.5rem; + ${sliderTokens.textFieldPadding}: 0.5rem 1rem 0.5rem 1rem; + ${sliderTokens.textFieldBorderRadius}: 0.625rem; + ${sliderTokens.textFieldFontFamily}: var(--plasma-typo-body-s-font-family); + ${sliderTokens.textFieldFontSize}: var(--plasma-typo-body-s-font-size); + ${sliderTokens.textFieldFontStyle}: var(--plasma-typo-body-s-font-style); + ${sliderTokens.textFieldFontWeight}: var(--plasma-typo-body-s-font-weight); + ${sliderTokens.textFieldLetterSpacing}: var(--plasma-typo-body-s-letter-spacing); + ${sliderTokens.textFieldLineHeight}: var(--plasma-typo-body-s-line-height); + `, + }, + disabled: { + true: css` + ${sliderTokens.disabledOpacity}: 0.4; + `, + }, + }, +}; diff --git a/packages/plasma-b2c/src/components/Slider/Slider.stories.tsx b/packages/plasma-b2c/src/components/Slider/Slider.stories.tsx index 111a1f9824..276bdd7eea 100644 --- a/packages/plasma-b2c/src/components/Slider/Slider.stories.tsx +++ b/packages/plasma-b2c/src/components/Slider/Slider.stories.tsx @@ -1,22 +1,38 @@ import React, { useState } from 'react'; +import type { ComponentProps } from 'react'; import styled from 'styled-components'; import { InSpacingDecorator, disableProps } from '@salutejs/plasma-sb-utils'; -import type { Meta, StoryObj } from '@storybook/react'; +import type { StoryObj, Meta } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { Slider, SliderProps, SliderProps as DoubleSliderProps } from '.'; +import { Slider } from './Slider'; -const meta: Meta = { +const sizes = ['l', 'm', 's']; +const views = ['default', 'accent', 'gradient']; +const labelPlacements = ['outer', 'inner']; +const rangeValuesPlacement = ['outer', 'inner']; + +const meta: Meta = { title: 'Controls/Slider', component: Slider, decorators: [InSpacingDecorator], argTypes: { + view: { + options: views, + control: { + type: 'select', + }, + }, + size: { + options: sizes, + control: { + type: 'inline-radio', + }, + }, ...disableProps([ 'value', 'onChangeCommitted', - 'theme', - 'as', - 'forwardedAs', + 'ariaLabel', 'onChange', 'fontSizeMultiplier', 'gap', @@ -24,22 +40,21 @@ const meta: Meta = { 'hasHoverAnimation', ]), }, - args: { - min: 0, - max: 100, - disabled: false, - ariaLabel: ['Минимальная цена товара', 'Максимальная цена товара'], - multipleStepSize: 10, - }, }; export default meta; +type StoryProps = Omit, 'value' | 'onChangeCommitted'>; +type StorySingleProps = StoryProps; + +type StorySingle = StoryObj; +type StoryDouble = StoryObj; + const SliderWrapper = styled.div` width: 25rem; `; -const StoryDefault = (args: SliderProps) => { +const StoryDefault = (args: StorySingleProps) => { const [value, setValue] = useState(30); const onChangeCommittedHandle = (values) => { @@ -48,29 +63,86 @@ const StoryDefault = (args: SliderProps) => { return ( - + ); }; -export const Default: StoryObj = { +export const Default: StorySingle = { + argTypes: { + labelPlacement: { + options: labelPlacements, + control: { + type: 'inline-radio', + }, + }, + rangeValuesPlacement: { + options: rangeValuesPlacement, + control: { + type: 'inline-radio', + }, + }, + }, + args: { + view: 'default', + size: 'm', + min: 0, + max: 100, + disabled: false, + ariaLabel: 'Цена товара', + multipleStepSize: 10, + label: 'Цена товара', + labelPlacement: 'outer', + rangeValuesPlacement: 'outer', + showRangeValues: true, + showCurrentValue: false, + }, render: (args) => , }; -const StoryMultipleValues = (args: DoubleSliderProps) => { +const StoryMultipleValues = (args: StoryProps) => { const [value, setValue] = useState([10, 80]); + const sortValues = (values) => { + return values.sort((a, b) => a - b); + }; - const onChangeCommittedHandle = (values) => { - setValue(values); + const onChangeHandle = (values) => { + setValue(sortValues(values)); + }; + + const onBlurTextField = (values) => { + setValue(sortValues(values)); + }; + + const onKeyDownTextField = (values, event) => { + if (event.key === 'Enter') { + setValue(sortValues(values)); + } }; return ( - + ); }; -export const MultipleValues: StoryObj = { +export const MultipleValues: StoryDouble = { + args: { + view: 'default', + size: 'm', + min: 0, + max: 100, + disabled: false, + label: 'Цена товара', + ariaLabel: ['Минимальная цена товара', 'Максимальная цена товара'], + multipleStepSize: 10, + }, render: (args) => , }; diff --git a/packages/plasma-b2c/src/components/Slider/Slider.tsx b/packages/plasma-b2c/src/components/Slider/Slider.tsx index 1507788c15..9fbafc706c 100644 --- a/packages/plasma-b2c/src/components/Slider/Slider.tsx +++ b/packages/plasma-b2c/src/components/Slider/Slider.tsx @@ -1,22 +1,12 @@ -import React from 'react'; -import styled from 'styled-components'; -import { SliderCore, SliderProps, SliderSettings } from '@salutejs/plasma-core'; -import { SliderThumb } from '@salutejs/plasma-hope'; -import { accent, white, surfaceLiquid03, buttonAccent as fillColor } from '@salutejs/plasma-tokens-b2c'; +import { sliderConfig, component, mergeConfig } from '@salutejs/plasma-new-hope/styled-components'; -const sliderSettings: SliderSettings = { - backgroundColor: surfaceLiquid03, - fillColor, -}; +import { config } from './Slider.config'; -const StyledThumb = styled(SliderThumb)` - border-color: ${surfaceLiquid03}; +const mergedConfig = mergeConfig(sliderConfig, config); +const SliderComponent = component(mergedConfig); - background-color: ${white}; - - color: ${accent}; -`; - -export const Slider = (props: SliderProps) => { - return ; -}; +/** + * Слайдер позволяет определить числовое значение в пределах указанного промежутка. + * Можно указать два значения. + */ +export const Slider = SliderComponent; diff --git a/packages/plasma-new-hope/src/components/Slider/Slider.tokens.ts b/packages/plasma-new-hope/src/components/Slider/Slider.tokens.ts new file mode 100644 index 0000000000..53822c72e9 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/Slider.tokens.ts @@ -0,0 +1,88 @@ +export const classes = { + labelPlacementOuter: 'slider-label-placement-outer', + labelPlacementInner: 'slider-label-placement-inner', + rangeValuesPlacementOuter: 'slider-range-values-placement-outer', + rangeValuesPlacementInner: 'slider-range-values-placement-inner', + maxRangeValue: 'slider-max-range-value', + hideMinValue: 'slider-hide-min-value', + hideMaxValue: 'slider-hide-max-value', + textFieldActive: 'slider-text-field-active', + firstTextField: 'slider-first-text-field', + secondTextField: 'slider-second-text-field', + activeRangeValue: 'slider-active-range-value', +}; + +export const tokens = { + height: '--plasma-slider-height', + + labelWrapperGap: '--plasma-slider-label-wrapper-gap', + labelWrapperMarginBottom: '--plasma-slider-label-wrapper-margin-bottom', + labelWrapperMarginRight: '--plasma-slider-label-wrapper-margin-right', + + labelColor: '--plasma-slider-label-color', + labelFontFamily: '--plasma-slider-label-font-family', + labelFontSize: '--plasma-slider-label-font-size', + labelFontStyle: '--plasma-slider-label-font-style', + labelFontWeight: '--plasma-slider-label-font-weight', + labelLetterSpacing: '--plasma-slider-label-letter-spacing', + labelLineHeight: '--plasma-slider-label-line-height', + + rangeMinValueMargin: '--plasma-slider-range-min-value-margin', + rangeMaxValueMargin: '--plasma-slider-range-max-value-margin', + rangeValueBottomOffset: '--plasma-slider-range-value-bottom-offset', + + rangeValueColor: '--plasma-slider-range-value-color', + rangeValueFontFamily: '--plasma-slider-range-value-font-family', + rangeValueFontSize: '--plasma-slider-range-value-font-size', + rangeValueFontStyle: '--plasma-slider-range-value-font-style', + rangeValueFontWeight: '--plasma-slider-range-value-font-weight', + rangeValueLetterSpacing: '--plasma-slider-range-value-letter-spacing', + rangeValueLineHeight: '--plasma-slider-range-value-line-height', + + doubleWrapperGap: '--plasma-slider-double-wrapper-gap', + + thumbSize: '--plasma-slider-thumb-size', + thumbBorder: '--plasma-slider-thumb-border', + thumbBorderColor: '--plasma-slider-thumb-border-color', + thumbBackgroundColor: '--plasma-slider-thumb-background-color', + + thumbFocusBorderColor: '--plasma-slider-thumb-focus-border-color', + + currentValueTopOffset: '--plasma-slider-current-value-top-offset', + + currentValueFontFamily: '--plasma-slider-current-value-font-family', + currentValueFontSize: '--plasma-slider-current-value-font-size', + currentValueFontStyle: '--plasma-slider-current-value-font-style', + currentValueFontWeight: '--plasma-slider-current-value-font-weight', + currentValueLetterSpacing: '--plasma-slider-current-value-letter-spacing', + currentValueLineHeight: '--plasma-slider-current-value-line-height', + + railBackgroundColor: '--plasma-slider-rail-background-color', + railHeight: '--plasma-slider-rail-height', + railBorderRadius: '--plasma-slider-rail-border-radius', + railIndent: '--plasma-slider-rail-indent', + + fillColor: '--plasma-slider-fill-color', + + textFieldWrapperGap: '--plasma-slider-text-field-wrapper-gap', + + textFieldFontFamily: '--plasma-slider-text-field-font-family', + textFieldFontSize: '--plasma-slider-text-field-font-size', + textFieldFontStyle: '--plasma-slider-text-field-font-style', + textFieldFontWeight: '--plasma-slider-text-field-font-weight', + textFieldLetterSpacing: '--plasma-slider-text-field-letter-spacing', + textFieldLineHeight: '--plasma-slider-text-field-line-height', + + textFieldHeight: '--plasma-slider-text-field-height', + textFieldPadding: '--plasma-slider-text-field-padding', + textFieldBorderRadius: '--plasma-slider-text-field-border-radius', + + textFieldColor: '--plasma-slider-text-field-color', + textFieldActiveColor: '--plasma-slider-text-field-active-color', + textFieldBackgroundColor: '--plasma-slider-text-field-background-color', + textFieldCaretColor: '--plasma-slider-text-field-caret-color', + textFieldPlaceholderColor: '--plasma-slider-text-field-placeholder-color', + textFiledFocusColor: '--plasma-slider-text-field-focus-color', + + disabledOpacity: '--plasma-slider-disabled-opacity', +}; diff --git a/packages/plasma-new-hope/src/components/Slider/Slider.tsx b/packages/plasma-new-hope/src/components/Slider/Slider.tsx new file mode 100644 index 0000000000..2d71997db0 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/Slider.tsx @@ -0,0 +1,51 @@ +import React, { forwardRef } from 'react'; + +import type { RootPropsOmitOnChange } from '../../engines'; + +import { base as viewCSS } from './variations/_view/base'; +import { base as sizeCSS } from './variations/_size/base'; +import { base as disabledCSS } from './variations/_disabled/base'; +import { SingleSlider, DoubleSlider } from './components'; +import type { SingleSliderProps } from './components'; +import { SliderProps } from './Slider.types'; + +const isSingleValueProps = (props: SliderProps): props is SingleSliderProps => typeof props.value === 'number'; + +export const sliderRoot = (Root: RootPropsOmitOnChange) => + forwardRef((props, ref) => { + if (isSingleValueProps(props)) { + return ( + + + + ); + } + return ( + + + + ); + }); + +export const sliderConfig = { + name: 'Slider', + tag: 'div', + layout: sliderRoot, + base: '', + variations: { + view: { + css: viewCSS, + }, + size: { + css: sizeCSS, + }, + disabled: { + css: disabledCSS, + attrs: true, + }, + }, + defaults: { + view: 'default', + size: 'm', + }, +}; diff --git a/packages/plasma-new-hope/src/components/Slider/Slider.types.ts b/packages/plasma-new-hope/src/components/Slider/Slider.types.ts new file mode 100644 index 0000000000..46c8189664 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/Slider.types.ts @@ -0,0 +1,3 @@ +import type { DoubleSliderProps, SingleSliderProps } from './components'; + +export type SliderProps = SingleSliderProps | DoubleSliderProps; diff --git a/packages/plasma-new-hope/src/components/Slider/components/Double/Double.styles.ts b/packages/plasma-new-hope/src/components/Slider/components/Double/Double.styles.ts new file mode 100644 index 0000000000..55dfcc280c --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/components/Double/Double.styles.ts @@ -0,0 +1,75 @@ +import { styled } from '@linaria/react'; + +import { classes, tokens } from '../../Slider.tokens'; +import { component, mergeConfig } from '../../../../engines'; +import { textFieldConfig, textFieldTokens } from '../../../TextField'; + +const mergedConfig = mergeConfig(textFieldConfig); +const TextField = component(mergedConfig); + +export const LabelWrapper = styled.div` + color: var(${tokens.labelColor}); + + display: flex; + gap: var(${tokens.labelWrapperGap}); + margin-bottom: var(${tokens.labelWrapperMarginBottom}); +`; + +export const LabelContentLeft = styled.div``; + +export const Label = styled.label` + font-family: var(${tokens.labelFontFamily}); + font-size: var(${tokens.labelFontSize}); + font-style: var(${tokens.labelFontStyle}); + font-weight: var(${tokens.labelFontWeight}); + letter-spacing: var(${tokens.labelLetterSpacing}); + line-height: var(${tokens.labelLineHeight}); +`; + +export const InputsWrapper = styled.div` + display: flex; + gap: var(${tokens.textFieldWrapperGap}); + margin-top: var(${tokens.doubleWrapperGap}); +`; + +// NOTE: переопределение токенов TextField +export const StyledInput = styled(TextField)` + flex: 1; + + ${textFieldTokens.color}: var(${tokens.textFieldColor}); + ${textFieldTokens.backgroundColor}: var(${tokens.textFieldBackgroundColor}); + ${textFieldTokens.caretColor}: var(${tokens.textFieldCaretColor}); + ${textFieldTokens.placeholderColor}: var(${tokens.textFieldPlaceholderColor}); + ${textFieldTokens.disabledOpacity}: var(${tokens.disabledOpacity}); + + ${textFieldTokens.height}: var(${tokens.textFieldHeight}); + ${textFieldTokens.padding}: var(${tokens.textFieldPadding}); + ${textFieldTokens.borderRadius}: var(${tokens.textFieldBorderRadius}); + + ${textFieldTokens.fontFamily}: var(${tokens.textFieldFontFamily}); + ${textFieldTokens.fontSize}: var(${tokens.textFieldFontSize}); + ${textFieldTokens.fontStyle}: var(${tokens.textFieldFontStyle}); + ${textFieldTokens.fontWeight}: var(${tokens.textFieldFontWeight}); + ${textFieldTokens.letterSpacing}: var(${tokens.textFieldLetterSpacing}); + ${textFieldTokens.lineHeight}: var(${tokens.textFieldLineHeight}); + + &.${classes.firstTextField} > div { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &.${classes.secondTextField} > div { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + input:focus, &.${classes.textFieldActive} { + ${textFieldTokens.color}: var(${tokens.textFiledFocusColor}); + } +`; + +export const DoubleWrapper = styled.div` + opacity: var(${tokens.disabledOpacity}); +`; + +export const SliderWrapper = styled.div``; diff --git a/packages/plasma-new-hope/src/components/Slider/components/Double/Double.tsx b/packages/plasma-new-hope/src/components/Slider/components/Double/Double.tsx new file mode 100644 index 0000000000..84fe67bdb7 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/components/Double/Double.tsx @@ -0,0 +1,328 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import type { FC, ChangeEvent, KeyboardEvent, FocusEvent } from 'react'; + +import { SliderBase } from '../SliderBase/SliderBase'; +import { Handle } from '../../ui'; +import type { HandleProps } from '../../ui'; +import { sizeData } from '../../utils'; +import { cx, isNumber } from '../../../../utils'; +import { classes } from '../../Slider.tokens'; + +import { DoubleSliderProps } from './Double.types'; +import { + SliderWrapper, + InputsWrapper, + Label, + LabelContentLeft, + LabelWrapper, + StyledInput, + DoubleWrapper, +} from './Double.styles'; + +function getXCenterHandle(handle: HTMLDivElement) { + const containerX = handle.parentElement?.getBoundingClientRect()?.x || 0; + const handleRect = handle.getBoundingClientRect(); + const handlePosition = handleRect.x; + return handlePosition - containerX; +} + +export const DoubleSlider: FC = ({ + min, + max, + value, + disabled, + label, + labelContentLeft, + size = 'm', + onChangeCommitted, + onChangeTextField, + onBlurTextField, + onKeyDownTextField, + onChange, + ariaLabel, + multipleStepSize = 10, + ...rest +}) => { + const [state, setState] = useState({ + stepSize: 0, + railFillWidth: 0, + railFillXPosition: 0, + xFirstHandle: 0, + xSecondHandle: 0, + firstHandleZIndex: 100, + secondHandleZIndex: 101, + firstValue: value[0], + secondValue: value[1], + }); + const [firstInputActive, setFirstInputActive] = useState(false); + const [secondInputActive, setSecondInputActive] = useState(false); + + const firstHandleRef = useRef(null); + const secondHandleRef = useRef(null); + + const firstHandleValue = useRef(value[0]); + const secondHandleValue = useRef(value[1]); + + const [firstValue, setFirstValue] = useState(value[0]); + const [secondValue, setSecondValue] = useState(value[1]); + + const { stepSize } = state; + + const hasLabelContent = label || labelContentLeft; + const firstInputActiveClass = firstInputActive && !disabled ? classes.textFieldActive : undefined; + const secondInputActiveClass = secondInputActive && !disabled ? classes.textFieldActive : undefined; + + useEffect(() => { + const firstLocalValue = Math.min(Math.max(value[0], min), max) - min; + const secondLocalValue = Math.min(Math.max(value[1], min), max) - min; + + setFirstValue(value[0]); + setSecondValue(value[1]); + + setState((prevState) => ({ + ...prevState, + railFillXPosition: stepSize * firstLocalValue, + railFillWidth: stepSize * secondLocalValue - stepSize * firstLocalValue, + xFirstHandle: stepSize * firstLocalValue, + xSecondHandle: stepSize * secondLocalValue, + })); + }, [value, stepSize, min, max, setFirstValue, setSecondValue]); + + const setStepSize = useCallback( + (newStepSize: number) => { + setState((prevState) => ({ + ...prevState, + stepSize: newStepSize, + })); + }, + [setState], + ); + + const onFirstHandleChange = useCallback>( + (handleValue, data) => { + if (!secondHandleRef?.current) { + return; + } + const newHandleXPosition = data.x; + const secondHandleXPosition = getXCenterHandle(secondHandleRef.current); + const fillWidth = secondHandleXPosition - newHandleXPosition; + + firstHandleValue.current = handleValue; + + setFirstValue(handleValue); + + setState((prevState) => ({ + ...prevState, + firstHandleZIndex: 101, + secondHandleZIndex: 100, + railFillWidth: fillWidth < 0 ? 0 : fillWidth, + railFillXPosition: newHandleXPosition, + })); + if (onChange) { + onChange([handleValue, value[1]]); + } + }, + [onChange, value], + ); + + const onFirstHandleChangeCommitted = useCallback>( + (handleValue, data) => { + setFirstValue(handleValue); + onChangeCommitted && onChangeCommitted([handleValue, value[1]]); + + setState((prevState) => ({ + ...prevState, + firstValue: handleValue, + xFirstHandle: data.lastX, + })); + }, + [onChangeCommitted, value], + ); + + const onFirstTextfieldChange = useCallback( + (event: ChangeEvent) => { + if (!isNumber(event.target.value)) { + return; + } + + const handleValue = Number(event.target.value); + + setFirstValue(handleValue); + onChangeTextField && onChangeTextField([handleValue, secondValue], event); + }, + [isNumber, setFirstValue, secondValue], + ); + + const onFirstTextfieldBlur = useCallback( + (event: FocusEvent) => { + if (!isNumber(event.target.value)) { + return; + } + + const handleValue = Number(event.target.value); + + setFirstValue(handleValue); + onBlurTextField && onBlurTextField([handleValue, secondValue], event); + }, + [isNumber, setSecondValue, onBlurTextField, secondValue], + ); + + const onSecondHandleChange = useCallback>( + (handleValue, data) => { + if (!firstHandleRef?.current) { + return; + } + const firstXHandleXPosition = getXCenterHandle(firstHandleRef.current); + + const newHandleXPosition = data.x; + const fillWidth = newHandleXPosition - firstXHandleXPosition; + + secondHandleValue.current = handleValue; + + setSecondValue(handleValue); + setState((prevState) => ({ + ...prevState, + firstHandleZIndex: 100, + secondHandleZIndex: 101, + railFillWidth: fillWidth < 0 ? 0 : fillWidth, + railFillXPosition: firstXHandleXPosition, + })); + if (onChange) { + onChange([value[0], handleValue]); + } + }, + [onChange, value], + ); + + const onSecondHandleChangeCommitted = useCallback>( + (handleValue, data) => { + onChangeCommitted && onChangeCommitted([value[0], handleValue]); + setSecondValue(handleValue); + setState((prevState) => ({ + ...prevState, + secondValue: handleValue, + xSecondHandle: data.lastX, + })); + }, + [onChangeCommitted, value], + ); + + const onSecondTextfieldChange = useCallback( + (event: ChangeEvent) => { + if (!isNumber(event.target.value)) { + return; + } + const handleValue = Number(event.target.value); + + setSecondValue(handleValue); + onChangeTextField && onChangeTextField([firstValue, handleValue], event); + }, + [isNumber, setSecondValue, onChangeTextField, firstValue], + ); + + const onSecondTextfieldBlur = useCallback( + (event: FocusEvent) => { + if (!isNumber(event.target.value)) { + return; + } + + const handleValue = Number(event.target.value); + + setSecondValue(handleValue); + onBlurTextField && onBlurTextField([firstValue, handleValue], event); + }, + [isNumber, setSecondValue, onBlurTextField, firstValue], + ); + + const onTextfieldKeyDown = useCallback( + (event: ChangeEvent & KeyboardEvent) => { + onKeyDownTextField && onKeyDownTextField([firstValue, secondValue], event); + }, + [[isNumber, setSecondValue, onKeyDownTextField], firstValue, secondValue], + ); + + const [ariaLabelLeft, ariaLabelRight] = ariaLabel || []; + const currentFirstSliderValue = Math.max(state.firstValue, min); + + return ( + + {hasLabelContent && ( + + {labelContentLeft && {labelContentLeft}} + {label && } + + )} + + + setFirstInputActive(true)} + onMouseLeave={() => setFirstInputActive(false)} + /> + setSecondInputActive(true)} + onMouseLeave={() => setSecondInputActive(false)} + /> + + + + + + + + + ); +}; diff --git a/packages/plasma-new-hope/src/components/Slider/components/Double/Double.types.ts b/packages/plasma-new-hope/src/components/Slider/components/Double/Double.types.ts new file mode 100644 index 0000000000..61a9c11a62 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/components/Double/Double.types.ts @@ -0,0 +1,70 @@ +import type { HTMLAttributes, ChangeEvent, KeyboardEvent, FocusEvent } from 'react'; + +import type { SliderBaseProps, SliderInternalProps } from '../SliderBase/SliderBase.types'; + +export interface DoubleSliderProps + extends SliderBaseProps, + SliderInternalProps, + Omit, 'onChange'> { + /** + * Текущее значение + */ + value: number[]; + /** + * Вызывается при отпускании ползунка + */ + onChangeCommitted?: (value: number[]) => void; + /** + * Вызывается при перемещении ползунка + */ + onChange?: (value: number[]) => void; + /** + * Вызывается при изменении TextField + */ + onChangeTextField?: (value: number[], event: ChangeEvent) => void; + /** + * Вызывается при анфокусе TextField + */ + onBlurTextField?: (value: number[], event: FocusEvent) => void; + /** + * Вызывается при нажатии на клавишу в TextField + */ + onKeyDownTextField?: (value: number[], event: KeyboardEvent) => void; + /** + * Расположение значений минимума и максимума интервала. + */ + rangeValuesPlacement?: never; + /** + * Отображать ли текущее значение. + */ + showCurrentValue?: never; + /** + * Отображать ли значения минимума и максимума интервала. + */ + showRangeValues?: never; + /** + * Разница между текущим значением и минимальным, при котором минимальное будет скрыто. + */ + hideMinValueDiff?: never; + /** + * Разница между текущим значением и максимальным, при котором максимальное будет скрыто. + */ + hideMaxValueDiff?: never; + /** + * Расположение подписи. + */ + labelPlacement?: never; + /** + * Ярлык, определяющий назначение ползунка, например «Минимальная цена» [a11y]. + */ + ariaLabel?: string[]; + /** + * Размера увеличенного шага (для клавиш PageUp, PageDown). + * Указывает процентное отношение от максимально возможного значения. + * Указав значение 20 при максимуме в 100, получим 20%. + */ + multipleStepSize?: number; + + view?: string; + size?: 's' | 'm' | 'l'; +} diff --git a/packages/plasma-new-hope/src/components/Slider/components/Single/Single.styles.ts b/packages/plasma-new-hope/src/components/Slider/components/Single/Single.styles.ts new file mode 100644 index 0000000000..d82958cba2 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/components/Single/Single.styles.ts @@ -0,0 +1,92 @@ +import { styled } from '@linaria/react'; + +import { classes, tokens } from '../../Slider.tokens'; + +export const LabelWrapper = styled.div` + color: var(${tokens.labelColor}); + + display: flex; + gap: var(${tokens.labelWrapperGap}); +`; + +export const LabelContentLeft = styled.div``; + +export const Label = styled.label` + font-family: var(${tokens.labelFontFamily}); + font-size: var(${tokens.labelFontSize}); + font-style: var(${tokens.labelFontStyle}); + font-weight: var(${tokens.labelFontWeight}); + letter-spacing: var(${tokens.labelLetterSpacing}); + line-height: var(${tokens.labelLineHeight}); +`; + +export const StyledRangeValue = styled.span` + color: var(${tokens.rangeValueColor}); + font-family: var(${tokens.rangeValueFontFamily}); + font-size: var(${tokens.rangeValueFontSize}); + font-style: var(${tokens.rangeValueFontStyle}); + font-weight: var(${tokens.rangeValueFontWeight}); + letter-spacing: var(${tokens.rangeValueLetterSpacing}); + line-height: var(${tokens.rangeValueLineHeight}); + + transition: opacity 0.1s ease-in-out; + + &.${classes.hideMinValue}, &.${classes.hideMaxValue} { + opacity: 0; + } + + &.${classes.activeRangeValue} { + color: var(${tokens.labelColor}); + } +`; + +export const SliderBaseWrapper = styled.div` + position: relative; + display: flex; + flex: 1; + + &.${classes.rangeValuesPlacementOuter} { + ${StyledRangeValue} { + position: absolute; + bottom: var(${tokens.rangeValueBottomOffset}); + left: 0; + + &.${classes.maxRangeValue} { + left: unset; + right: 0; + } + } + } + + &.${classes.rangeValuesPlacementInner} { + align-items: center; + + ${StyledRangeValue} { + margin-right: var(${tokens.rangeMinValueMargin}); + + &.${classes.maxRangeValue} { + margin-right: 0; + margin-left: var(${tokens.rangeMaxValueMargin}); + } + } + } +`; + +export const SingleWrapper = styled.div` + display: flex; + opacity: var(${tokens.disabledOpacity}); + + &.${classes.labelPlacementOuter} { + flex-direction: column; + + ${LabelWrapper} { + margin-bottom: var(${tokens.labelWrapperMarginBottom}); + } + } + + &.${classes.labelPlacementInner} { + ${LabelWrapper} { + margin-right: var(${tokens.labelWrapperMarginRight}); + } + } +`; diff --git a/packages/plasma-new-hope/src/components/Slider/components/Single/Single.tsx b/packages/plasma-new-hope/src/components/Slider/components/Single/Single.tsx new file mode 100644 index 0000000000..3526b8f91e --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/components/Single/Single.tsx @@ -0,0 +1,183 @@ +import React, { useCallback, useEffect, useState, useRef } from 'react'; +import type { FC } from 'react'; + +import { SliderBase } from '../SliderBase/SliderBase'; +import { Handle } from '../../ui'; +import { sizeData } from '../../utils'; +import type { HandleProps } from '../../ui'; +import { cx, isNumber } from '../../../../utils'; +import { classes } from '../../Slider.tokens'; + +import type { SingleSliderProps } from './Single.types'; +import { + Label, + LabelContentLeft, + LabelWrapper, + SingleWrapper, + SliderBaseWrapper, + StyledRangeValue, +} from './Single.styles'; + +export const SingleSlider: FC = ({ + min, + max, + value, + disabled, + onChangeCommitted, + onChange, + ariaLabel, + label, + labelContentLeft, + showRangeValues, + showCurrentValue, + hideMinValueDiff, + hideMaxValueDiff, + labelPlacement = 'outer', + rangeValuesPlacement = 'outer', + multipleStepSize = 10, + size = 'm', + ...rest +}) => { + const [state, setState] = useState({ + xHandle: 0, + stepSize: 0, + railFillWidth: 0, + }); + + const { stepSize } = state; + + const hasLabelContent = label || labelContentLeft; + const labelPlacementClass = labelPlacement === 'outer' ? classes.labelPlacementOuter : classes.labelPlacementInner; + const rangeValuesPlacementClass = + rangeValuesPlacement === 'outer' ? classes.rangeValuesPlacementOuter : classes.rangeValuesPlacementInner; + const hideMinValueDiffClass = hideMinValueDiff && value - min <= hideMinValueDiff ? classes.hideMinValue : ''; + const hideMaxValueDiffClass = hideMaxValueDiff && max - value <= hideMaxValueDiff ? classes.hideMaxValue : ''; + + const startLabelRef = useRef(null); + const endLabelRef = useRef(null); + + const [startOffset, setStartOffset] = useState(0); + const [endOffset, setEndOffset] = useState(0); + + const [dragValue, setDragValue] = useState(value); + + const activeFirstValue = dragValue === min ? classes.activeRangeValue : undefined; + const activeSecondValue = dragValue === max ? classes.activeRangeValue : undefined; + + useEffect(() => { + const localValue = Math.min(Math.max(value, min), max) - min; + + if (rangeValuesPlacement === 'outer') { + const startWidth = startLabelRef.current?.offsetWidth; + if (isNumber(startWidth)) { + setStartOffset(Number(startWidth)); + } + + const endWidth = endLabelRef.current?.offsetWidth; + if (isNumber(endWidth)) { + setEndOffset(Number(endWidth)); + } + } else { + setStartOffset(1); + setEndOffset(1); + } + + setState((prevState) => ({ + ...prevState, + xHandle: stepSize * localValue, + railFillWidth: stepSize * localValue, + })); + }, [value, labelPlacement, stepSize, rangeValuesPlacement, min, max, setStartOffset, setEndOffset]); + + const setStepSize = useCallback((newStepSize: number) => { + setState((prevState) => ({ + ...prevState, + stepSize: newStepSize, + })); + }, []); + + const onHandleChange = useCallback>( + (handleValue, data) => { + const newHandleXPosition = data.x; + setState((prevState) => ({ + ...prevState, + railFillWidth: newHandleXPosition, + })); + + if (onChange) { + onChange(handleValue); + } + + setDragValue(handleValue); + }, + [onChange, setDragValue], + ); + + const onHandleChangeCommitted = useCallback>( + (handleValue, data) => { + onChangeCommitted && onChangeCommitted(handleValue); + setState((prevState) => ({ + ...prevState, + xHandle: data.lastX, + railFillWidth: data.lastX, + })); + + setDragValue(handleValue); + }, + [onChangeCommitted, setDragValue], + ); + + return ( + + {hasLabelContent && ( + + {labelContentLeft && {labelContentLeft}} + {label && } + + )} + + {showRangeValues && ( + + {min} + + )} + + + + {showRangeValues && ( + + {max} + + )} + + + ); +}; diff --git a/packages/plasma-new-hope/src/components/Slider/components/Single/Single.types.ts b/packages/plasma-new-hope/src/components/Slider/components/Single/Single.types.ts new file mode 100644 index 0000000000..43e4361e7d --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/components/Single/Single.types.ts @@ -0,0 +1,60 @@ +import type { HTMLAttributes } from 'react'; + +import type { SliderBaseProps, SliderInternalProps } from '../SliderBase/SliderBase.types'; + +export interface SingleSliderProps + extends SliderBaseProps, + SliderInternalProps, + Omit, 'onChange'> { + /** + * Текущее значение + */ + value: number; + /** + * Вызывается при отпускании ползунка + */ + onChangeCommitted?: (value: number) => void; + /** + * Вызывается при перемещении ползунка + */ + onChange?: (value: number) => void; + /** + * Ярлык, определяющий назначение ползунка, например «Минимальная цена» [a11y]. + */ + ariaLabel?: string; + /** + * Отображать ли текущее значение. + */ + showCurrentValue?: boolean; + /** + * Отображать ли значения минимума и максимума интервала. + */ + showRangeValues?: boolean; + /** + * Разница между текущим значением и минимальным, при котором минимальное будет скрыто. + */ + hideMinValueDiff?: number; + /** + * Разница между текущим значением и максимальным, при котором максимальное будет скрыто. + */ + hideMaxValueDiff?: number; + /** + * Расположение значений минимума и максимума интервала. + * @default `outer` + */ + rangeValuesPlacement?: 'inner' | 'outer'; + /** + * Расположение подписи. + * @default `outer` + */ + labelPlacement?: 'inner' | 'outer'; + /** + * Размера увеличенного шага (для клавиш PageUp, PageDown). + * Указывает процентное отношение от максимально возможного значения. + * Указав значение 20 при максимуме в 100, получим 20%. + */ + multipleStepSize?: number; + + view?: string; + size?: 's' | 'm' | 'l'; +} diff --git a/packages/plasma-new-hope/src/components/Slider/components/SliderBase/SliderBase.styles.ts b/packages/plasma-new-hope/src/components/Slider/components/SliderBase/SliderBase.styles.ts new file mode 100644 index 0000000000..4ec9361bc8 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/components/SliderBase/SliderBase.styles.ts @@ -0,0 +1,36 @@ +import { styled } from '@linaria/react'; + +import { tokens } from '../../Slider.tokens'; + +export const Slider = styled.div` + flex: 1; + position: relative; + user-select: none; + height: var(${tokens.height}); +`; + +export const RailWrap = styled.div` + height: 100%; +`; + +export const Rail = styled.div` + position: relative; + top: 50%; + + height: var(${tokens.railHeight}); + + border-radius: var(${tokens.railBorderRadius}); + background-color: var(${tokens.railBackgroundColor}); + + overflow: hidden; + transform: translateY(-50%); +`; + +export const Fill = styled.div` + position: absolute; + height: 100%; + top: 0; + left: 0; + background: var(${tokens.fillColor}); + width: 0; +`; diff --git a/packages/plasma-new-hope/src/components/Slider/components/SliderBase/SliderBase.tsx b/packages/plasma-new-hope/src/components/Slider/components/SliderBase/SliderBase.tsx new file mode 100644 index 0000000000..42b839c21b --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/components/SliderBase/SliderBase.tsx @@ -0,0 +1,87 @@ +import React, { PropsWithChildren, useCallback, useRef, MouseEventHandler, useEffect } from 'react'; +import { DraggableData } from 'react-draggable'; + +import { useIsomorphicLayoutEffect } from '../../../../hooks'; + +import type { SliderViewProps } from './SliderBase.types'; +import { Fill, Rail, RailWrap, Slider } from './SliderBase.styles'; + +export const SliderBase: React.FC> = ({ + max, + min, + setStepSize, + railFillWidth, + children, + railFillXPosition = 0, + disabled, + labelPlacement, + rangeValuesPlacement, + onChange, + settings = {}, +}) => { + const { indent = 0.75, fontSizeMultiplier = 16 } = settings; + + const ref = useRef(null); + const gap = indent * fontSizeMultiplier * 2; + + useEffect(() => { + const resizeHandler = () => { + if (ref.current) { + const railSize = ref.current.offsetWidth - gap; + const totalSteps = max - min; + + setStepSize(railSize / totalSteps); + } + }; + + resizeHandler(); + }, [labelPlacement, rangeValuesPlacement, ref.current]); + + const onHandleChange: MouseEventHandler = useCallback( + (e) => { + if (!onChange || disabled) { + return; + } + + const { x, width } = e.currentTarget.getBoundingClientRect(); + + const lastX = e.clientX - x; + + const position = min + (lastX / (width - gap)) * (max - min); + const result = Math.max(min, Math.min(max, position)); + + onChange(result, { lastX } as DraggableData); + }, + [onChange, disabled, min, gap, max, settings], + ); + + useIsomorphicLayoutEffect(() => { + const resizeHandler = () => { + if (ref.current) { + const railSize = ref.current.offsetWidth - gap; + const totalSteps = max - min; + + setStepSize(railSize / totalSteps); + } + }; + + resizeHandler(); + window.addEventListener('resize', resizeHandler); + + return () => window.removeEventListener('resize', resizeHandler); + }, [min, max, setStepSize, ref.current, gap, labelPlacement, rangeValuesPlacement]); + + const fillStyle = { left: `${railFillXPosition}px`, width: `${railFillWidth}px` }; + + return ( + + + + {children} + + ); +}; diff --git a/packages/plasma-new-hope/src/components/Slider/components/SliderBase/SliderBase.types.ts b/packages/plasma-new-hope/src/components/Slider/components/SliderBase/SliderBase.types.ts new file mode 100644 index 0000000000..fe4dd68e1c --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/components/SliderBase/SliderBase.types.ts @@ -0,0 +1,48 @@ +import type { ReactNode } from 'react'; +import type { DraggableData } from 'react-draggable'; + +export type SliderSettings = Partial<{ + indent?: number; + fontSizeMultiplier?: number; + backgroundColor?: string; + fillColor?: string; +}>; + +export interface SliderBaseProps { + /** + * Минимальное значение + */ + min: number; + /** + * Максимальное значение + */ + max: number; + /** + * подпись к слайдеру + */ + label?: string; + /** + * Слот под контент слева от подписи (например иконку) + */ + labelContentLeft?: ReactNode; + /** + * Компонент неактивен + */ + disabled?: boolean; + labelPlacement?: string; + rangeValuesPlacement?: string; +} + +export interface SliderInternalProps { + /** + * Настройки внешнего вида slider + */ + settings?: SliderSettings; +} + +export interface SliderViewProps extends SliderBaseProps, SliderInternalProps { + railFillWidth: number; + setStepSize(stepSize: number): void; + railFillXPosition?: number; + onChange?(value: number, data: DraggableData): void; +} diff --git a/packages/plasma-new-hope/src/components/Slider/components/index.ts b/packages/plasma-new-hope/src/components/Slider/components/index.ts new file mode 100644 index 0000000000..5901209024 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/components/index.ts @@ -0,0 +1,6 @@ +export * from './Single/Single'; +export * from './Double/Double'; + +export type { SingleSliderProps } from './Single/Single.types'; +export type { DoubleSliderProps } from './Double/Double.types'; +export type { SliderInternalProps, SliderSettings } from './SliderBase/SliderBase.types'; diff --git a/packages/plasma-new-hope/src/components/Slider/index.ts b/packages/plasma-new-hope/src/components/Slider/index.ts new file mode 100644 index 0000000000..c4422a3bf0 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/index.ts @@ -0,0 +1,7 @@ +export { sliderConfig, sliderRoot } from './Slider'; +export { tokens as sliderTokens } from './Slider.tokens'; + +export { ThumbBase } from './ui'; + +export type { SliderProps } from './Slider.types'; +export type { SliderSettings, SingleSliderProps, DoubleSliderProps } from './components'; diff --git a/packages/plasma-new-hope/src/components/Slider/ui/Handle/Handle.styles.ts b/packages/plasma-new-hope/src/components/Slider/ui/Handle/Handle.styles.ts new file mode 100644 index 0000000000..9e46a7b619 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/ui/Handle/Handle.styles.ts @@ -0,0 +1,28 @@ +import { styled } from '@linaria/react'; + +import { tokens } from '../../Slider.tokens'; + +export const HandleStyled = styled.div` + cursor: pointer; + position: absolute; + z-index: 1; + top: 0; + left: 0; +`; + +export const StyledValue = styled.span` + position: absolute; + z-index: 1; + top: var(${tokens.currentValueTopOffset}); + text-align: center; + width: 100%; + margin-left: -0.125rem; + display: flex; + justify-content: center; + font-family: var(${tokens.currentValueFontFamily}); + font-size: var(${tokens.currentValueFontSize}); + font-style: var(${tokens.currentValueFontStyle}); + font-weight: var(${tokens.currentValueFontWeight}); + letter-spacing: var(${tokens.currentValueLetterSpacing}); + line-height: var(${tokens.currentValueLineHeight}); +`; diff --git a/packages/plasma-new-hope/src/components/Slider/ui/Handle/Handle.tsx b/packages/plasma-new-hope/src/components/Slider/ui/Handle/Handle.tsx new file mode 100644 index 0000000000..69be8f3c56 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/ui/Handle/Handle.tsx @@ -0,0 +1,185 @@ +import React, { useState, useRef, useCallback, forwardRef, KeyboardEvent } from 'react'; +import Draggable, { DraggableEventHandler } from 'react-draggable'; +import type { DraggableData } from 'react-draggable'; + +import { getSliderThumbValue, getOffsets } from '../../utils'; +import { Thumb } from '../Thumb/Thumb'; + +import type { HandleProps } from './Handle.types'; +import { HandleStyled, StyledValue } from './Handle.styles'; + +// TODO: PLASMA-1707 +declare module 'react-draggable' { + export interface DraggableProps { + children: React.ReactNode; + } +} + +const KeyboardSupport = { + PageUp: 33, + PageDown: 34, + End: 35, + Home: 36, + ArrowLeft: 37, + ArrowUp: 38, + ArrowRight: 39, + ArrowDown: 40, +}; + +export const Handle = forwardRef( + ( + { + stepSize, + onChangeCommitted, + onChange, + xPosition = 0, + min, + max, + bounds = [], + zIndex, + disabled, + side, + showCurrentValue = false, + startOffset = 0, + endOffset = 0, + ...rest + }, + ref, + ) => { + const lastOnChangeValue = useRef(); + const currentSliderValue = lastOnChangeValue?.current ?? rest.value; + + const [offsetLeft, offsetRight] = getOffsets(ref, side); + + const [leftValueBound, rightValueBound] = bounds; + const leftPositionBound = leftValueBound ? (leftValueBound - min) * stepSize : null; + const rightPositionBound = rightValueBound ? (rightValueBound - min) * stepSize : null; + + const position = typeof xPosition === 'number' ? { x: xPosition, y: 0 } : undefined; + const tabIndex = disabled ? -1 : 0; + + const [positionX, setPositionX] = useState(position?.x ?? 0); + + const computedBounds = { + left: (leftPositionBound ?? 0) + offsetLeft, + right: (rightPositionBound ?? stepSize * (max - min)) - offsetRight, + }; + + const showCurrentValueCondition = + showCurrentValue && positionX >= startOffset && positionX <= max * stepSize - endOffset; + + const onDrag = useCallback( + (_, data) => { + const newValue = getSliderThumbValue(data.x, stepSize, min, max); + if (lastOnChangeValue.current !== newValue) { + onChange?.(newValue, data); + setPositionX(data.x); + lastOnChangeValue.current = newValue; + } + }, + [onChange, setPositionX, stepSize, min, max], + ); + + const onStop = useCallback( + (_, data) => { + const newValue = getSliderThumbValue(data.x, stepSize, min, max); + setPositionX(data.x); + onChangeCommitted && onChangeCommitted(newValue, data); + }, + [onChangeCommitted, setPositionX, stepSize, min, max], + ); + + const onKeyPress = useCallback( + (event: KeyboardEvent) => { + event.persist(); + + const { keyCode, target } = event; + + if (!Object.values(KeyboardSupport).includes(keyCode)) { + return; + } + + const { ArrowUp, ArrowRight, ArrowDown, ArrowLeft, Home, End, PageDown, PageUp } = KeyboardSupport; + + const computedMultipleSteps = stepSize * ((rest.multipleStepSize / 100) * max); + + const data: DraggableData = { + x: 0, + deltaX: stepSize, + lastX: xPosition, + y: 0, + deltaY: 0, + lastY: 0, + node: target as HTMLDivElement, + }; + + switch (keyCode) { + case ArrowUp: + case ArrowRight: + data.x = xPosition + stepSize; + break; + case ArrowDown: + case ArrowLeft: + data.x = xPosition - stepSize; + data.deltaX = -stepSize; + break; + case PageUp: + data.x = xPosition + computedMultipleSteps; + data.deltaX = computedMultipleSteps; + break; + case PageDown: + data.x = xPosition - computedMultipleSteps; + data.deltaX = -computedMultipleSteps; + break; + case End: + data.x = max * stepSize; + break; + case Home: + data.x = 0; + break; + default: + data.x = 0; + } + + const { left, right } = computedBounds; + + /* + * INFO: Находим значение в диапазоне между указанными левой и правой границами. + * Необходимо для правильного расчета положения SliderThumb. + * см. функция clamp + */ + const boundedValue = Math.max(Math.min(right, data.x), left); + + const computedValue = getSliderThumbValue(boundedValue, stepSize, min, max); + lastOnChangeValue.current = computedValue; + + onChangeCommitted && onChangeCommitted(computedValue, data); + }, + [onChangeCommitted, bounds, stepSize, rest.multipleStepSize, min, max, xPosition], + ); + + return ( + + + + {showCurrentValueCondition && {currentSliderValue}} + + + ); + }, +); diff --git a/packages/plasma-new-hope/src/components/Slider/ui/Handle/Handle.types.ts b/packages/plasma-new-hope/src/components/Slider/ui/Handle/Handle.types.ts new file mode 100644 index 0000000000..55e430ff2a --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/ui/Handle/Handle.types.ts @@ -0,0 +1,24 @@ +import type { DraggableData } from 'react-draggable'; + +export interface HandleProps { + stepSize: number; + min: number; + max: number; + multipleStepSize: number; + onChangeCommitted?(value: number, data: DraggableData): void; + side?: 'left' | 'right'; + bounds?: number[]; + xPosition?: number; + zIndex?: number; + disabled?: boolean; + value?: number; + ariaLabel?: string; + ariaValueMin?: number; + hasHoverAnimation?: boolean; + showCurrentValue?: boolean; + startOffset?: number; + endOffset?: number; + onChange?(value: number, data: DraggableData): void; + onMouseEnter?(): void; + onMouseLeave?(): void; +} diff --git a/packages/plasma-new-hope/src/components/Slider/ui/Thumb/Thumb.styles.ts b/packages/plasma-new-hope/src/components/Slider/ui/Thumb/Thumb.styles.ts new file mode 100644 index 0000000000..85be9a8e84 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/ui/Thumb/Thumb.styles.ts @@ -0,0 +1,47 @@ +import { styled } from '@linaria/react'; + +import { addFocus } from '../../../../mixins'; +import { tokens } from '../../Slider.tokens'; + +export const ThumbBase = styled.div<{ disabled?: boolean }>` + width: var(${tokens.thumbSize}); + height: var(${tokens.thumbSize}); + position: relative; + left: -0.125rem; + top: -0.125rem; + border-radius: 50%; + box-sizing: border-box; + background: var(${tokens.thumbBackgroundColor}); + margin: 0.125rem; + transition: border-color 0.1s ease-in-out; + + &:after { + background: var(${tokens.thumbBorderColor}); + margin: -0.125rem; + content: ''; + position: absolute; + inset: 0; + z-index: -1; + border-radius: inherit; + } + + &:not([disabled]):hover:after, + &:not([disabled]):active:after { + background: var(${tokens.thumbFocusBorderColor}); + } + + &[disabled] { + cursor: not-allowed; + } + + &:focus { + outline: none; + } + + ${addFocus({ + outlineOffset: '0.125rem', + outlineSize: '0.125rem', + outlineRadius: '50%', + outlineColor: `var(${tokens.thumbFocusBorderColor})`, + })} +`; diff --git a/packages/plasma-new-hope/src/components/Slider/ui/Thumb/Thumb.tsx b/packages/plasma-new-hope/src/components/Slider/ui/Thumb/Thumb.tsx new file mode 100644 index 0000000000..11bb6898db --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/ui/Thumb/Thumb.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import type { ThumbProps } from './Thumb.types'; +import { ThumbBase } from './Thumb.styles'; + +export const Thumb = ({ min, max, value, ariaValueMin = min, ariaLabel, disabled, ...rest }: ThumbProps) => { + return ( + + ); +}; diff --git a/packages/plasma-new-hope/src/components/Slider/ui/Thumb/Thumb.types.ts b/packages/plasma-new-hope/src/components/Slider/ui/Thumb/Thumb.types.ts new file mode 100644 index 0000000000..7afd741c4e --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/ui/Thumb/Thumb.types.ts @@ -0,0 +1,11 @@ +export interface ThumbProps { + min: number; + max: number; + multipleStepSize: number; + tabIndex: number; + value?: number; + ariaLabel?: string; + ariaValueMin?: number; + hasHoverAnimation?: boolean; + disabled?: boolean; +} diff --git a/packages/plasma-new-hope/src/components/Slider/ui/index.ts b/packages/plasma-new-hope/src/components/Slider/ui/index.ts new file mode 100644 index 0000000000..3b3067aab0 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/ui/index.ts @@ -0,0 +1,7 @@ +export * from './Handle/Handle'; +export * from './Thumb/Thumb'; + +export { ThumbBase } from './Thumb/Thumb.styles'; + +export * from './Handle/Handle.types'; +export * from './Thumb/Thumb.types'; diff --git a/packages/plasma-new-hope/src/components/Slider/utils/index.ts b/packages/plasma-new-hope/src/components/Slider/utils/index.ts new file mode 100644 index 0000000000..fd746e9dda --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/utils/index.ts @@ -0,0 +1,58 @@ +import { MutableRefObject } from 'react'; + +/** + * Расчитать значение слайдера с учетом его координат и шага изменений. + * @param {number} handleCenterXRelative + * @param {number} stepSize + * @param {number} min + * @param {number} max + * @return {number} + */ +export function getSliderThumbValue(handleCenterXRelative: number, stepSize: number, min: number, max: number) { + const newValue = Math.round(handleCenterXRelative / stepSize) + min; + + return Math.min(Math.max(newValue, min), max); +} + +/** + * Расчитывает значение отступа слайдера на основе его положения (справа, слева) на отрезке слайдера. + * Значение используется для правильного расчета ограничения движения слайдера. + * @param ref + * @param {'left' | 'right'} side + * @return Array + */ +export function getOffsets( + ref: ((instance: HTMLDivElement | null) => void) | MutableRefObject | null, + side?: 'left' | 'right', +): number[] { + if (!ref || !('current' in ref) || !ref.current || !side) { + return [0, 0]; + } + + const size = ref.current.clientWidth; + + if (side === 'left') { + return [0, size]; + } + + if (side === 'right') { + return [size, 0]; + } + + return [0, 0]; +} + +export const sizeData = { + s: { + indent: 0.5, + fontSizeMultiplier: 16, + }, + m: { + indent: 0.75, + fontSizeMultiplier: 16, + }, + l: { + indent: 0.75, + fontSizeMultiplier: 16, + }, +}; diff --git a/packages/plasma-new-hope/src/components/Slider/variations/_disabled/base.tsx b/packages/plasma-new-hope/src/components/Slider/variations/_disabled/base.tsx new file mode 100644 index 0000000000..cd585b76c4 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/variations/_disabled/base.tsx @@ -0,0 +1,3 @@ +import { css } from '@linaria/core'; + +export const base = css``; diff --git a/packages/plasma-new-hope/src/components/Slider/variations/_disabled/tokens.json b/packages/plasma-new-hope/src/components/Slider/variations/_disabled/tokens.json new file mode 100644 index 0000000000..66b76487b8 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/variations/_disabled/tokens.json @@ -0,0 +1,4 @@ +{ + "type": "boolean", + "tokens": ["--plasma-slider-disabled-opacity"] +} diff --git a/packages/plasma-new-hope/src/components/Slider/variations/_size/base.tsx b/packages/plasma-new-hope/src/components/Slider/variations/_size/base.tsx new file mode 100644 index 0000000000..cd585b76c4 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/variations/_size/base.tsx @@ -0,0 +1,3 @@ +import { css } from '@linaria/core'; + +export const base = css``; diff --git a/packages/plasma-new-hope/src/components/Slider/variations/_size/tokens.json b/packages/plasma-new-hope/src/components/Slider/variations/_size/tokens.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/plasma-new-hope/src/components/Slider/variations/_view/base.tsx b/packages/plasma-new-hope/src/components/Slider/variations/_view/base.tsx new file mode 100644 index 0000000000..cd585b76c4 --- /dev/null +++ b/packages/plasma-new-hope/src/components/Slider/variations/_view/base.tsx @@ -0,0 +1,3 @@ +import { css } from '@linaria/core'; + +export const base = css``; diff --git a/packages/plasma-new-hope/src/components/Slider/variations/_view/tokens.json b/packages/plasma-new-hope/src/components/Slider/variations/_view/tokens.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/plasma-new-hope/src/components/TextField/TextField.tsx b/packages/plasma-new-hope/src/components/TextField/TextField.tsx index b2f4e95f5f..fad5899873 100644 --- a/packages/plasma-new-hope/src/components/TextField/TextField.tsx +++ b/packages/plasma-new-hope/src/components/TextField/TextField.tsx @@ -1,5 +1,5 @@ import React, { forwardRef, useEffect, useRef, useState } from 'react'; -import type { ChangeEventHandler } from 'react'; +import type { ChangeEventHandler, KeyboardEvent, ChangeEvent } from 'react'; import { safeUseId, useForkRef } from '@salutejs/plasma-core'; import { css } from '@linaria/core'; @@ -37,6 +37,8 @@ export const textFieldRoot = (Root: RootProps) = ( { id, + className, + style, // layout contentLeft, @@ -60,6 +62,7 @@ export const textFieldRoot = (Root: RootProps) = onChange, onChangeChips, onSearch, + onKeyDown, ...rest }, @@ -144,6 +147,11 @@ export const textFieldRoot = (Root: RootProps) = } }; + const handleOnKeyDown = (event: ChangeEvent & KeyboardEvent) => { + handleInputKeydown(event); + onKeyDown && onKeyDown(event); + }; + useEffect(() => { if (!isChipEnumeration && !values?.length) { return; @@ -166,6 +174,8 @@ export const textFieldRoot = (Root: RootProps) = readOnly={!disabled && readOnly} labelPlacement={innerLabelPlacementValue} onClick={handleInputFocus} + className={className} + style={style} > {labelInside || (innerLabelValue && ( @@ -211,7 +221,7 @@ export const textFieldRoot = (Root: RootProps) = disabled={disabled} readOnly={!disabled && readOnly} onChange={handleChange} - onKeyDown={handleInputKeydown} + onKeyDown={handleOnKeyDown} /> {labelInside && (