diff --git a/example/storybook/.ondevice/storybook.requires.js b/example/storybook/.ondevice/storybook.requires.js index ee82d1911a..1539780502 100644 --- a/example/storybook/.ondevice/storybook.requires.js +++ b/example/storybook/.ondevice/storybook.requires.js @@ -59,6 +59,8 @@ const getStories = () => { require('../src/styled/api/MultipleTheme/MultipleTheme.stories.tsx'), // require('../src/ui/components/DataDisplay/Badge/Badge.stories.tsx'), require('../src/ui/components/DataDisplay/Card/Card.stories.tsx'), + require('../src/ui/components/Forms/RangeSlider/RangeSlider.stories.tsx'), + // require('../src/ui/components/Forms/Button/ButtonGroup.stories.tsx'), // require('../src/ui/components/Forms/Checkbox/Checkbox.stories.tsx'), // require('../src/ui/components/DataDisplay/Divider/Divider.stories.tsx'), @@ -76,7 +78,7 @@ const getStories = () => { // require('../src/ui/components/Feedback/Progress/Progress.stories.tsx'), // require('../src/ui/components/Forms/Radio/Radio.stories.tsx'), // require('../src/ui/components/Forms/Select/Select.stories.tsx'), - // require('../src/ui/components/Forms/Slider/Slider.stories.tsx'), + require('../src/ui/components/Forms/Slider/Slider.stories.tsx'), // require('../src/ui/components/Feedback/Spinner/Spinner.stories.tsx'), // require('../src/ui/components/Forms/Switch/Switch.stories.tsx'), // require('../src/ui/components/Forms/Textarea/Textarea.stories.tsx'), diff --git a/example/storybook/.storybook/preview.js b/example/storybook/.storybook/preview.js index eba42f09af..15139f1bfb 100644 --- a/example/storybook/.storybook/preview.js +++ b/example/storybook/.storybook/preview.js @@ -73,6 +73,7 @@ export const parameters = { 'Radio', 'Select', 'Slider', + 'Range Slider', 'Switch', 'Tabs', 'Textarea', diff --git a/example/storybook/babel.config.js b/example/storybook/babel.config.js index 17bdf88d3d..2d546bcd5a 100644 --- a/example/storybook/babel.config.js +++ b/example/storybook/babel.config.js @@ -12,6 +12,10 @@ module.exports = function (api) { __dirname, '../../packages/themed/src' ), + '@gluestack-ui/range-slider': path.join( + __dirname, + '../../packages/unstyled/range-slider/src' + ), '@gluestack-ui/config': path.join( __dirname, '../../packages/config/src/gluestack-ui.config' diff --git a/example/storybook/metro.config.js b/example/storybook/metro.config.js index 75447906eb..e95320da78 100644 --- a/example/storybook/metro.config.js +++ b/example/storybook/metro.config.js @@ -32,4 +32,4 @@ config.transformer.getTransformOptions = async () => ({ config.watchFolders = [...config.watchFolders]; -module.exports = config; +module.exports = config; \ No newline at end of file diff --git a/example/storybook/package.json b/example/storybook/package.json index fc0d02721a..bd7a46eafa 100644 --- a/example/storybook/package.json +++ b/example/storybook/package.json @@ -56,7 +56,7 @@ "react": "^18.2.0", "react-aria": "^3.30.0", "react-dom": "^18.2.0", - "react-native": "0.72.4", + "react-native": "^0.73.2", "react-native-gesture-handler": "^2.12.1", "react-native-safe-area-context": "^4.4.1", "react-native-svg": "13.4.0", diff --git a/example/storybook/src/ui/components/Forms/RangeSlider/RangeSlider.png b/example/storybook/src/ui/components/Forms/RangeSlider/RangeSlider.png new file mode 100644 index 0000000000..cbf37081d7 Binary files /dev/null and b/example/storybook/src/ui/components/Forms/RangeSlider/RangeSlider.png differ diff --git a/example/storybook/src/ui/components/Forms/RangeSlider/RangeSlider.stories.tsx b/example/storybook/src/ui/components/Forms/RangeSlider/RangeSlider.stories.tsx new file mode 100644 index 0000000000..59ee74a69e --- /dev/null +++ b/example/storybook/src/ui/components/Forms/RangeSlider/RangeSlider.stories.tsx @@ -0,0 +1,11 @@ +import type { ComponentMeta } from '@storybook/react-native'; +import RangeSlide from './RangeSlider'; + +const SliderMeta: ComponentMeta = { + title: 'stories/FORMS/Range Slider', + component: RangeSlide, +}; + +export default SliderMeta; + +export { RangeSlide }; diff --git a/example/storybook/src/ui/components/Forms/RangeSlider/RangeSlider.tsx b/example/storybook/src/ui/components/Forms/RangeSlider/RangeSlider.tsx new file mode 100644 index 0000000000..a228810d7e --- /dev/null +++ b/example/storybook/src/ui/components/Forms/RangeSlider/RangeSlider.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { + Center, + RangeSlider, + RangeSliderTrack, + RangeSliderFilledTrack, + RangeSliderLeftThumb, + RangeSliderRightThumb, + Text, + VStack, + HStack, + Box, + Icon, + Heading, + Tooltip, + TooltipContent, + Button, +} from '@gluestack-ui/themed'; + +const RangeSliderBasic = ({ ...props }: any) => { + const [sliderValue, setSliderValue] = React.useState([20, 49]); + + return ( +
+ + + + + + + +
+ ); +}; + +RangeSliderBasic.description = + 'This is a basic RangeSlider component example. RangeSlider are used to select a value from a range of values.'; + +export default RangeSliderBasic; + +export { + Center, + RangeSlider, + RangeSliderTrack, + RangeSliderFilledTrack, + RangeSliderLeftThumb, + RangeSliderRightThumb, + Text, + VStack, + HStack, + Box, + Icon, + Heading, + Tooltip, + TooltipContent, + Button, +}; +export { Volume, Volume2Icon, LightbulbIcon } from 'lucide-react-native'; diff --git a/example/storybook/src/ui/components/Forms/RangeSlider/index.stories.mdx b/example/storybook/src/ui/components/Forms/RangeSlider/index.stories.mdx new file mode 100644 index 0000000000..6249a5c795 --- /dev/null +++ b/example/storybook/src/ui/components/Forms/RangeSlider/index.stories.mdx @@ -0,0 +1,861 @@ +--- +title: Range Slider | gluestack-ui | Installation, Usage, and API + +description: The Range Slider component enables an intuitive selection of values within a designated range. Users can easily adjust their selection by sliding a visual indicator along the track. + +pageTitle: Range Slider + +pageDescription: The Range Slider component enables an intuitive selection of values within a designated range. Users can easily adjust their selection by sliding a visual indicator along the track. + +showHeader: true + +tag: beta +--- + +import { Meta } from '@storybook/addon-docs'; +import { useRef, useEffect, useState } from 'react'; + + + +import { + RangeSlider, + RangeSliderTrack, + RangeSliderFilledTrack, + RangeSliderLeftThumb, + RangeSliderRightThumb, + VStack, + Volume, + Text, +} from './RangeSlider'; +import { + Slider, + SliderTrack, + SliderFilledTrack, + SliderThumb, +} from "@gluestack-ui/themed" +import { HStack, Volume2Icon, Box, Center } from './RangeSlider'; +import { + LightbulbIcon, + Icon, + Heading, + Tooltip, + TooltipContent, + Button, +} from './RangeSlider'; +import { transformedCode } from '../../../utils'; +import { + AppProvider, + CodePreview, + InlineCode, + Table, + TableContainer, +} from '@gluestack/design-system'; + +import Wrapper from '../../Wrapper'; + +This is an illustration of a **Themed Range Slider** component with default configuration. + + + + + + + + + + + + + `, + transformCode: (code) => { + return transformedCode(code); + }, + scope: { + Wrapper, + RangeSlider, + RangeSliderTrack, + RangeSliderFilledTrack, + RangeSliderLeftThumb, + RangeSliderRightThumb, + Center, + }, + argsType: { + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + default: 'md', + }, + orientation: { + control: 'select', + options: ['vertical', 'horizontal'], + default: 'horizontal', + }, + isDisabled: { + control: 'boolean', + }, + }, + }} + /> + + +
+ +## API Reference + +### Import + +To use this component in your project, include the following import statement in your file. + +```bash +import { RangeSlider } from '@gluestack-ui/themed'; +``` + +### Anatomy + +The structure provided below can help you identify and understand a Range Slider component's various parts. + +```jsx +export default () => ( + + + + + + + +); +``` + +### Component Props + +This section provides a comprehensive reference list for the component props, detailing descriptions, properties, types, and default behavior for easy project integration. + +#### Range Slider + +It inherits all the properties of React Native's [View](https://reactnative.dev/docs/view) component. + + + + + + + + Prop + + + Type + + + Default + + + Description + + + + + + + + onChange + + + + {'(value: number) => void'} + + + - + + + + Function called when the state of the Range Slider changes. + + + + + + + isDisabled + + + + bool + + + false + + + When true, this will disable Range Slider + + + + + + isReadOnly + + + + boolean + + + false + + + + To manually set read-only to the checkbox. + + + + + + + sliderTrackHeight + + + + number + + + 8 + + + To change the range slider track height . + + + + + + defaultValue + + + + number + + + - + + + To change the range slider value . + + + + + + minValue + + + + number + + + - + + + The range slider's minimum value + + + + + + maxValue + + + + number + + + - + + + The range slider's maximum value. + + + + + + value + + + + number + + + - + + + The range slider's current value. + + + + + + step + + + + number + + + - + + + The range slider's step value. + + + +
+
+
+ +**Descendants Styling Props** +Props to style child components. + + + + + + + + Sx Prop + + + Description + + + + + + + + _thumb + + + + {`Prop to style RangSliderThumb Component`} + + + + + + _track + + + + {`Prop to style RangSliderTrack Component`} + + + + + + _filledTrack + + + + {`Prop to style RangeSliderFilledTrack Component`} + + + +
+
+
+ +#### RangeSliderTrack + +It inherits all the properties of React Native's [Pressable](https://reactnative.dev/docs/Pressable) component. + +#### RangeSliderFilledTrack + +It inherits all the properties of React Native's [View](https://reactnative.dev/docs/view) component. + +#### RangeSliderLeftThumb + +It inherits all the properties of React Native's [View](https://reactnative.dev/docs/view) component. + +#### RangeSliderRightThumb + +It inherits all the properties of React Native's [View](https://reactnative.dev/docs/view) component. + +### Features + +- Keyboard support for actions. +- Support for hover, focus and active states. + +### Accessibility + +We have outlined the various features that ensure the Range Slider component is accessible to all users, including those with disabilities. These features help ensure that your application is inclusive and meets accessibility standards.Adheres to the [WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider/). + +#### Keyboard + +- `Tab`: Moves focus to the next focusable element. +- `Right Arrow`: Increase the value of the range slider by one step. +- `Up Arrow`: Increase the value of the range slider by one step. +- `Left Arrow`: Decrease the value of the range slider by one step. +- `Down Arrow`: Decrease the value of the range slider by one step. + +### Screen Reader + +- VoiceOver: When the range slider is focused, the screen reader will announce the range slider's value. + +## Themed + +The themed version of the component is a pre-styled version of the component, which allows you to quickly integrate the component into your project. The component's design and functionality are fully defined, allowing you to focus on the more important aspects of your project. To know more about Themed Library please visit this [link](https://gluestack.io/ui/docs/core-concepts/themed-library). + +### Props + +Range Slider component is created using View component from react-native. It extends all the props supported by [React Native View](https://reactnative.dev/docs/view#props), [utility props](/ui/docs/styling/utility-and-sx-props) and the props mentioned below. + +#### Range Slider + + + + + + + + Name + + + Value + + + Default + + + + + + + + orientation + + + + horizontal | vertical + + + horizontal + + + + + + size + + + + sm | md | lg + + + md + + + +
+
+
+ +> Note: These props are exclusively applicable when utilizing the default configuration of gluestack-ui/config. If you are using a custom theme, these props may not be available. + +### Examples + +The Examples section provides visual representations of the different variants of the component, allowing you to quickly and easily determine which one best fits your needs. Simply copy the code and integrate it into your project. + +#### Range Slider with Orientation + +An example of the Range Slider component being used with the Range Slider with Orientation feature to customize the orientation of the range slider, providing flexibility in the direction of sliding and input for numerical or adjustable values within a user interface. + + + { + setSliderValue(value); + }; + return ( +
+ + + + + + + +
+ ); +} +`, + transformCode: (code) => { + return transformedCode(code, 'function', 'App'); + }, + scope: { + Box, + RangeSlider, + RangeSliderTrack, + RangeSliderFilledTrack, + RangeSliderLeftThumb, + RangeSliderRightThumb, + Center, + Wrapper, + }, + argsType: {}, + }} + /> +
+ +#### Color scheme + +A Range Slider component with a color scheme adds visual styling and customization options, allowing the range slider track and handle to be displayed in different colors, enhancing the aesthetic appeal and visual coherence of the range slider within a user interface. + + + +
+ + + + + + + +
+
+ + + + + + + +
+
+ + + + + + + +
+
+ + + + + + + +
+ + ); +} +`, + transformCode: (code) => { + return transformedCode(code, 'function', 'App'); + }, + scope: { + Center, + RangeSlider, + RangeSliderTrack, + RangeSliderFilledTrack, + RangeSliderLeftThumb, + RangeSliderRightThumb, + VStack, + Wrapper, + }, + argsType: {}, + }} + /> +
+ +#### With tooltip + +A Range Slider component with a tooltip displays a visual indicator or text overlay that provides real-time feedback on the selected value as users interact with the range slider, improving usability and precision in inputting or adjusting numeric or continuous data within a user interface. + + + { + setSliderValue(value); + }; + return ( + + $0 + { + return ( +
+ + + + + + + +
+ ) + }} + > + + {"$" + sliderValue} + +
+ $60 +
+); +} +`, + transformCode: (code) => { + return transformedCode(code, 'function', 'App'); + }, + scope: { + Wrapper, + RangeSlider, + RangeSliderTrack, + RangeSliderFilledTrack, + RangeSliderLeftThumb, + RangeSliderRightThumb, + Center, + Volume, + VStack, + HStack, + Volume2Icon, + Text, + Box, + Tooltip, + TooltipContent, + Button, + }, + argsType: {}, + }} + /> +
+ +#### Form Controlled + +A Range Slider component with form-controlled behavior allows for seamless integration with a form's state management, enabling the range slider value to be controlled and updated through a parent component's form state, providing a consistent and synchronized user experience for capturing and manipulating numeric or continuous data within a form. + + + { + setSliderValue(value); + }; + return ( + + Select the quantity +
+ { + handleChange(value); + }}> + + + + + + +
+ Slide the knob to select the number of products +
+ ); +} +`, + transformCode: (code) => { + return transformedCode(code, 'function', 'App'); + }, + scope: { + Wrapper, + Center, + RangeSlider, + RangeSliderTrack, + RangeSliderFilledTrack, + RangeSliderLeftThumb, + RangeSliderRightThumb, + Center, + LightbulbIcon, + Icon, + Text, + VStack, + Heading, + }, + argsType: {}, + }} + /> +
+ +#### Custom + +A custom Range Slider component with an icon incorporates a personalized design by combining a graphical symbol or icon with the slider interface, adding a unique visual element and enhancing the user experience when interacting with numeric or continuous data input in a user interface. + + + { + setSliderValue(value); + }; + return ( + + Brightness +
+ { + handleChange(value); + }}> + + + + + + + + + + +
+
+ ); +} +`, + transformCode: (code) => { + return transformedCode(code, 'function', 'App'); + }, + scope: { + Wrapper, + Center, + RangeSlider, + RangeSliderTrack, + RangeSliderFilledTrack, + RangeSliderLeftThumb, + RangeSliderRightThumb, + LightbulbIcon, + Icon, + Text, + VStack, + }, + argsType: {}, + }} + /> +
+ +#### Volume + +A Range Slider component used as a volume control allows users to adjust the audio volume by sliding the handle along the track, providing an intuitive and interactive way to control the sound output within a user interface. + + + { + setSliderValue(value); + }; + return ( + + + current sliderValue - {sliderValue} + onChangeEndValue - {onChangeEndValue} + + + +
+ + + + + + + +
+ +
+
+ ); +} +`, + transformCode: (code) => { + return transformedCode(code, 'function', 'App'); + }, + scope: { + Wrapper, + Center, + RangeSlider, + RangeSliderTrack, + RangeSliderFilledTrack, + RangeSliderLeftThumb, + RangeSliderRightThumb, + Center, + Volume, + VStack, + HStack, + Volume2Icon, + Text, + Box, + }, + argsType: {}, + }} + /> +
+ +## Unstyled + +All the components in `gluestack-ui` are unstyled by default. To customize your UI using the extendedTheme, please refer to this [link](https://gluestack.io/ui/docs/theme-configuration/customizing-theme). The import names of components serve as keys to customize each component. \ No newline at end of file diff --git a/example/storybook/src/ui/components/Forms/Slider/index.stories.mdx b/example/storybook/src/ui/components/Forms/Slider/index.stories.mdx index c5d005beb6..9dd57610dd 100644 --- a/example/storybook/src/ui/components/Forms/Slider/index.stories.mdx +++ b/example/storybook/src/ui/components/Forms/Slider/index.stories.mdx @@ -889,7 +889,6 @@ export default () => ( ``` --> - ## Spec Doc Explore the comprehensive details of the Slider in this document, including its implementation details, checklist, and potential future additions. Dive into the thought process behind the component and gain insights into its development journey. diff --git a/example/storybook/tsconfig.json b/example/storybook/tsconfig.json index 50a1359be5..19ca5cc699 100644 --- a/example/storybook/tsconfig.json +++ b/example/storybook/tsconfig.json @@ -5,6 +5,9 @@ "paths": { "@gluestack-ui/themed": ["../../packages/themed/src"], "@gluestack-ui/config": ["../../packages/config/src/gluestack-ui.config"], + "@gluestack-ui/range-slider": [ + "../../packages/unstyled/range-slider/src" + ], "react-native": ["./node_modules/react-native-web"], "@gluestack-style/react": ["../../packages/styled/react/src"], "@gluestack-style/animation-resolver": [ diff --git a/packages/config/package.json b/packages/config/package.json index 1c9ab18822..dad44e57cc 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@gluestack-ui/config", - "version": "1.1.17", + "version": "1.1.18", "main": "build/gluestack-ui.config.js", "types": "build/gluestack-ui.config.d.ts", "module": "build/gluestack-ui.config", diff --git a/packages/config/src/theme/RangeSlider.ts b/packages/config/src/theme/RangeSlider.ts new file mode 100644 index 0000000000..2cb61730f5 --- /dev/null +++ b/packages/config/src/theme/RangeSlider.ts @@ -0,0 +1,192 @@ +import { createStyle } from '@gluestack-style/react'; + +export const RangeSlider = createStyle({ + justifyContent: 'center', + alignItems: 'center', + variants: { + orientation: { + horizontal: { + w: '$full', + _track: { + width: '$full', + }, + _filledTrack: { + height: '$full', + }, + }, + vertical: { + h: '$full', + _track: { + height: '$full', + }, + _filledTrack: { + width: '$full', + }, + }, + }, + isReversed: { + true: {}, + false: {}, + }, + size: { + sm: { + _thumb: { + h: '$4', + w: '$4', + }, + }, + md: { + _thumb: { + h: '$5', + w: '$5', + }, + }, + lg: { + _thumb: { + h: '$6', + w: '$6', + }, + }, + }, + }, + compoundVariants: [ + { + orientation: 'horizontal', + size: 'sm', + value: { + _track: { + height: '$1', + flexDirection: 'row', + }, + }, + }, + { + orientation: 'horizontal', + size: 'sm', + isReversed: true, + value: { + _track: { + height: '$1', + flexDirection: 'row-reverse', + }, + }, + }, + { + orientation: 'horizontal', + size: 'md', + value: { + _track: { + height: 5, + flexDirection: 'row', + }, + }, + }, + { + orientation: 'horizontal', + size: 'md', + isReversed: true, + value: { + _track: { + height: 5, + flexDirection: 'row-reverse', + }, + }, + }, + { + orientation: 'horizontal', + size: 'lg', + value: { + _track: { + height: '$1.5', + flexDirection: 'row', + }, + }, + }, + { + orientation: 'horizontal', + size: 'lg', + isReversed: true, + value: { + _track: { + height: '$1.5', + flexDirection: 'row-reverse', + }, + }, + }, + { + orientation: 'vertical', + size: 'sm', + value: { + _track: { + w: '$1', + flexDirection: 'column-reverse', + }, + }, + }, + { + orientation: 'vertical', + size: 'sm', + isReversed: true, + value: { + _track: { + width: '$1', + flexDirection: 'column', + }, + }, + }, + { + orientation: 'vertical', + size: 'md', + value: { + _track: { + width: 5, + flexDirection: 'column-reverse', + }, + }, + }, + { + orientation: 'vertical', + size: 'md', + isReversed: true, + value: { + _track: { + width: 5, + flexDirection: 'column', + }, + }, + }, + { + orientation: 'vertical', + size: 'lg', + value: { + _track: { + width: '$1.5', + flexDirection: 'column-reverse', + }, + }, + }, + { + orientation: 'vertical', + size: 'lg', + isReversed: true, + value: { + _track: { + width: '$1.5', + flexDirection: 'column', + }, + }, + }, + ], + _web: { + ':disabled': { + // @ts-ignore + pointerEvents: 'all !important', + cursor: 'not-allowed', + opacity: 0.4, + }, + }, + defaultProps: { + size: 'md', + orientation: 'horizontal', + }, +}); diff --git a/packages/config/src/theme/RangeSliderLeftThumb.ts b/packages/config/src/theme/RangeSliderLeftThumb.ts new file mode 100644 index 0000000000..06a0869e08 --- /dev/null +++ b/packages/config/src/theme/RangeSliderLeftThumb.ts @@ -0,0 +1,57 @@ +import { createStyle } from '@gluestack-style/react'; + +export const RangeSliderLeftThumb = createStyle({ + 'bg': '$primary500', + '_dark': { + bg: '$primary400', + }, + 'position': 'absolute', + 'borderRadius': '$full', + ':focus': { + bg: '$primary600', + _dark: { + bg: '$primary300', + }, + }, + ':active': { + bg: '$primary600', + _dark: { + bg: '$primary300', + }, + }, + ':hover': { + bg: '$primary600', + _dark: { + bg: '$primary300', + }, + }, + ':disabled': { + bg: '$primary500', + _dark: { + bg: '$primary500', + }, + }, + '_web': { + //@ts-ignore + 'cursor': 'pointer', + ':active': { + outlineWidth: 4, + outlineStyle: 'solid', + outlineColor: '$primary400', + _dark: { + outlineColor: '$primary500', + }, + }, + ':focus': { + outlineWidth: 4, + outlineStyle: 'solid', + outlineColor: '$primary400', + _dark: { + outlineColor: '$primary500', + }, + }, + }, + 'defaultProps': { + hardShadow: '1', + }, +}); diff --git a/packages/config/src/theme/RangeSliderRightThumb.ts b/packages/config/src/theme/RangeSliderRightThumb.ts new file mode 100644 index 0000000000..7112ff37d2 --- /dev/null +++ b/packages/config/src/theme/RangeSliderRightThumb.ts @@ -0,0 +1,57 @@ +import { createStyle } from '@gluestack-style/react'; + +export const RangeSliderRightThumb = createStyle({ + 'bg': '$primary500', + '_dark': { + bg: '$primary400', + }, + 'position': 'absolute', + 'borderRadius': '$full', + ':focus': { + bg: '$primary600', + _dark: { + bg: '$primary300', + }, + }, + ':active': { + bg: '$primary600', + _dark: { + bg: '$primary300', + }, + }, + ':hover': { + bg: '$primary600', + _dark: { + bg: '$primary300', + }, + }, + ':disabled': { + bg: '$primary500', + _dark: { + bg: '$primary500', + }, + }, + '_web': { + //@ts-ignore + 'cursor': 'pointer', + ':active': { + outlineWidth: 4, + outlineStyle: 'solid', + outlineColor: '$primary400', + _dark: { + outlineColor: '$primary500', + }, + }, + ':focus': { + outlineWidth: 4, + outlineStyle: 'solid', + outlineColor: '$primary400', + _dark: { + outlineColor: '$primary500', + }, + }, + }, + 'defaultProps': { + hardShadow: '1', + }, +}); diff --git a/packages/config/src/theme/index.ts b/packages/config/src/theme/index.ts index d1e344f9ba..3862da6034 100644 --- a/packages/config/src/theme/index.ts +++ b/packages/config/src/theme/index.ts @@ -148,6 +148,9 @@ export * from './TooltipContent'; export * from './TooltipText'; export * from './VStack'; export * from './View'; +export * from './RangeSlider'; +export * from './RangeSliderLeftThumb'; +export * from './RangeSliderRightThumb'; export * from './ImageBackground'; export * from './InputAccessoryView'; export * from './SafeAreaView'; diff --git a/packages/react-native-aria/slider/src/useSlider.web.ts b/packages/react-native-aria/slider/src/useSlider.web.ts index 742971b6f0..224f80ebcc 100644 --- a/packages/react-native-aria/slider/src/useSlider.web.ts +++ b/packages/react-native-aria/slider/src/useSlider.web.ts @@ -118,6 +118,13 @@ function useSliderWeb( // Find the closest thumb const trackPosition = trackLayout[isVertical ? 'top' : 'left']; const clickPosition = isVertical ? clientY : clientX; + console.log( + trackPosition, + trackLayout, + clickPosition, + 'trackPosition, clickPosition', + trackLayout + ); const offset = clickPosition - trackPosition; let percent = offset / size; if (reverseX) { diff --git a/packages/themed/babel.config.js b/packages/themed/babel.config.js new file mode 100644 index 0000000000..3924eab8cd --- /dev/null +++ b/packages/themed/babel.config.js @@ -0,0 +1,28 @@ +const path = require('path'); +module.exports = function (api) { + api.cache(true); + return { + presets: ['babel-preset-expo'], + plugins: [ + process.env.NODE_ENV !== 'production' + ? [ + 'module-resolver', + { + alias: { + '@gluestack-ui/range-slider': path.join( + __dirname, + '../unstyled/range-slider/src' + ), + }, + }, + ] + : [ + 'babel-plugin-react-docgen-typescript', + { + exclude: 'node_modules', + }, + ], + '@babel/plugin-transform-modules-commonjs', + ], + }; +}; diff --git a/packages/themed/package.json b/packages/themed/package.json index 3844920b5d..67b4d9efd3 100644 --- a/packages/themed/package.json +++ b/packages/themed/package.json @@ -1,6 +1,6 @@ { "name": "@gluestack-ui/themed", - "version": "1.1.11", + "version": "1.1.17", "main": "build/index.js", "types": "build/index.d.ts", "module": "build/index", @@ -59,6 +59,7 @@ "@gluestack-ui/progress": "0.1.13", "@gluestack-ui/provider": "0.1.10", "@gluestack-ui/radio": "0.1.25", + "@gluestack-ui/range-slider": "0.1.1", "@gluestack-ui/select": "0.1.24", "@gluestack-ui/slider": "0.1.21", "@gluestack-ui/spinner": "0.1.14", diff --git a/packages/themed/src/components/RangeSlider/config.json b/packages/themed/src/components/RangeSlider/config.json new file mode 100644 index 0000000000..7b0813defc --- /dev/null +++ b/packages/themed/src/components/RangeSlider/config.json @@ -0,0 +1,4 @@ +{ + "dependencies": { "@gluestack-ui/slider": "latest" }, + "keywords": ["components", "core"] +} diff --git a/packages/themed/src/components/RangeSlider/index.tsx b/packages/themed/src/components/RangeSlider/index.tsx new file mode 100644 index 0000000000..8f74f58653 --- /dev/null +++ b/packages/themed/src/components/RangeSlider/index.tsx @@ -0,0 +1,24 @@ +import { createRangeSlider } from '@gluestack-ui/range-slider'; + +import { + Root, + LeftThumb, + RightThumb, + Track, + FilledTrack, + ThumbInteraction, +} from './styled-components'; + +export const RangeSlider = createRangeSlider({ + Root, + ThumbInteraction, + LeftThumb, + RightThumb, + Track, + FilledTrack, +}); + +export const RangeSliderLeftThumb = RangeSlider.LeftThumb; +export const RangeSliderRightThumb = RangeSlider.RightThumb; +export const RangeSliderTrack = RangeSlider.Track; +export const RangeSliderFilledTrack = RangeSlider.FilledTrack; diff --git a/packages/themed/src/components/RangeSlider/styled-components/FilledTrack.tsx b/packages/themed/src/components/RangeSlider/styled-components/FilledTrack.tsx new file mode 100644 index 0000000000..0350da835d --- /dev/null +++ b/packages/themed/src/components/RangeSlider/styled-components/FilledTrack.tsx @@ -0,0 +1,7 @@ +import { styled } from '@gluestack-style/react'; +import { View } from 'react-native'; + +export default styled(View, {}, { + componentName: 'SliderFilledTrack', + ancestorStyle: ['_filledTrack'], +} as const); diff --git a/packages/themed/src/components/RangeSlider/styled-components/LeftThumb.tsx b/packages/themed/src/components/RangeSlider/styled-components/LeftThumb.tsx new file mode 100644 index 0000000000..c6b0d826cf --- /dev/null +++ b/packages/themed/src/components/RangeSlider/styled-components/LeftThumb.tsx @@ -0,0 +1,7 @@ +import { styled } from '@gluestack-style/react'; +import { View } from 'react-native'; + +export default styled(View, {}, { + componentName: 'RangeSliderLeftThumb', + ancestorStyle: ['_thumb'], +} as const); diff --git a/packages/themed/src/components/RangeSlider/styled-components/RightThumb.tsx b/packages/themed/src/components/RangeSlider/styled-components/RightThumb.tsx new file mode 100644 index 0000000000..2687060d34 --- /dev/null +++ b/packages/themed/src/components/RangeSlider/styled-components/RightThumb.tsx @@ -0,0 +1,7 @@ +import { styled } from '@gluestack-style/react'; +import { View } from 'react-native'; + +export default styled(View, {}, { + componentName: 'RangeSliderRightThumb', + ancestorStyle: ['_thumb'], +} as const); diff --git a/packages/themed/src/components/RangeSlider/styled-components/Root.tsx b/packages/themed/src/components/RangeSlider/styled-components/Root.tsx new file mode 100644 index 0000000000..288e85caaa --- /dev/null +++ b/packages/themed/src/components/RangeSlider/styled-components/Root.tsx @@ -0,0 +1,7 @@ +// @ts-nocheck +import { styled } from '@gluestack-style/react'; +import { View } from 'react-native'; +export default styled(View, {}, { + componentName: 'RangeSlider', + descendantStyle: ['_thumb', '_track', '_filledTrack'], +} as const); diff --git a/packages/themed/src/components/RangeSlider/styled-components/ThumbInteraction.tsx b/packages/themed/src/components/RangeSlider/styled-components/ThumbInteraction.tsx new file mode 100644 index 0000000000..2a90e25ed9 --- /dev/null +++ b/packages/themed/src/components/RangeSlider/styled-components/ThumbInteraction.tsx @@ -0,0 +1,6 @@ +import { styled } from '@gluestack-style/react'; +import { View } from 'react-native'; + +export default styled(View, {}, { + componentName: 'SliderThumbInteraction', +} as const); diff --git a/packages/themed/src/components/RangeSlider/styled-components/Track.tsx b/packages/themed/src/components/RangeSlider/styled-components/Track.tsx new file mode 100644 index 0000000000..57b1d79756 --- /dev/null +++ b/packages/themed/src/components/RangeSlider/styled-components/Track.tsx @@ -0,0 +1,7 @@ +import { styled } from '@gluestack-style/react'; +import { Pressable } from 'react-native'; + +export default styled(Pressable, {}, { + componentName: 'SliderTrack', + ancestorStyle: ['_track'], +} as const); diff --git a/packages/themed/src/components/RangeSlider/styled-components/index.tsx b/packages/themed/src/components/RangeSlider/styled-components/index.tsx new file mode 100644 index 0000000000..c33bf2bc25 --- /dev/null +++ b/packages/themed/src/components/RangeSlider/styled-components/index.tsx @@ -0,0 +1,6 @@ +export { default as Root } from './Root'; +export { default as LeftThumb } from './LeftThumb'; +export { default as RightThumb } from './RightThumb'; +export { default as Track } from './Track'; +export { default as FilledTrack } from './FilledTrack'; +export { default as ThumbInteraction } from './ThumbInteraction'; diff --git a/packages/themed/src/components/index.tsx b/packages/themed/src/components/index.tsx index d90271be98..6631a07be9 100644 --- a/packages/themed/src/components/index.tsx +++ b/packages/themed/src/components/index.tsx @@ -49,3 +49,4 @@ export * from './Icons/Icons'; export * from './VirtualizedList'; export * from './RefreshControl'; export * from './ImageBackground'; +export * from './RangeSlider'; diff --git a/packages/unstyled/range-slider/.npmignore b/packages/unstyled/range-slider/.npmignore new file mode 100644 index 0000000000..187790b632 --- /dev/null +++ b/packages/unstyled/range-slider/.npmignore @@ -0,0 +1,20 @@ +# Dotfiles +.babelrc +.eslintignore +.eslintrc.json +.gitattributes +_config.yml +.editorconfig + + +#Config files +babel.config.js + +# Documents +CONTRIBUTING.md +ISSUE_TEMPLATE.txt +img + +# Test cases +__tests__ +dist/__tests__ diff --git a/packages/unstyled/range-slider/CHANGELOG.md b/packages/unstyled/range-slider/CHANGELOG.md new file mode 100644 index 0000000000..cd903f472c --- /dev/null +++ b/packages/unstyled/range-slider/CHANGELOG.md @@ -0,0 +1,91 @@ +# @gluestack-ui/slider + +## 0.1.19 + +### Patch Changes + +- Updated dependencies + - @gluestack-ui/hooks@0.1.11 + +## 0.1.12 + +### Patch Changes + +- Reversed functionality fixed + +## 0.1.11 + +### Patch Changes + +- fix: figma refactor + +## 0.1.10 + +### Patch Changes + +- Changed component APIs from dot notation to normal +- Updated dependencies + - @gluestack-ui/form-control@0.1.10 + +## 0.1.9 + +### Patch Changes + +- Fixed slider with orientation and reversed and moved out style from styled files + +## 0.1.8 + +### Patch Changes + +- Added support for isReversed and orientation prop. + +## 0.1.7 + +### Patch Changes + +- component typings updated + +## 0.1.6 + +### Patch Changes + +- included path links in compilerOptions in tsconfig file +- Updated dependencies + - @gluestack-ui/form-control@0.1.8 + - @gluestack-ui/hooks@0.1.2 + - @gluestack-ui/utils@0.1.5 + +## 0.1.5 + +### Patch Changes + +- feat: migration of hook to react native aria +- Updated dependencies + - @gluestack-ui/form-control@0.1.7 + +## 0.1.4 + +### Patch Changes + +- package json and readme fixes +- Updated dependencies + - @gluestack-ui/hooks@0.1.1 + - @gluestack-ui/utils@0.1.4 + +## 0.1.3 + +### Patch Changes + +- fix forwardref warning issues + +## 0.1.2 + +### Patch Changes + +- added form control support + +## 0.1.1 + +### Patch Changes + +- added accessibility diff --git a/packages/unstyled/range-slider/README.md b/packages/unstyled/range-slider/README.md new file mode 100644 index 0000000000..d8bf6733e7 --- /dev/null +++ b/packages/unstyled/range-slider/README.md @@ -0,0 +1,76 @@ +# @gluestack-ui/slider + +## Installation + +To use `@gluestack-ui/slider`, all you need to do is install the +`@gluestack-ui/slider` package: + +```sh +$ yarn add @gluestack-ui/slider + +# or + +$ npm i @gluestack-ui/slider +``` + +## Usage + +The Slider component enables an intuitive selection of values within a designated range. Users can easily adjust their selection by sliding a visual indicator along the track. Here's an example how to use this package to create one: + +```jsx +import { + Root, + Thumb, + Track, + FilledTrack, + ThumbInteraction, +} from '../components/core/slider/styled-components'; +import { createSlider } from '@gluestack-ui/slider'; +const Slider = createSlider({ + Root, + Thumb, + Track, + FilledTrack, + ThumbInteraction, +}); +``` + +## Customizing the slider: + +Default styling of all these components can be found in the components/core/slider file. For reference, you can view the [source code](https://github.com/gluestack/gluestack-ui/blob/development/example/storybook/src/ui-components/Slider/index.tsx) of the styled `slider` components. + +```jsx +// import the styles +import { + Root, + Thumb, + Track, + FilledTrack, + ThumbInteraction, +} from '../components/core/slider/styled-components'; + +// import the createSlider function +import { createSlider } from '@gluestack-ui/slider'; + +// Understanding the API +const Slider = createSlider({ + Root, + Thumb, + Track, + FilledTrack, + ThumbInteraction, +}); + +// Using the Slider component +export default () => ( + + + + + + +); +``` + +More guides on how to get started are available +[here](https://ui.gluestack.io/docs/components/forms/slider). diff --git a/packages/unstyled/range-slider/babel.config.js b/packages/unstyled/range-slider/babel.config.js new file mode 100644 index 0000000000..c564c723f7 --- /dev/null +++ b/packages/unstyled/range-slider/babel.config.js @@ -0,0 +1,24 @@ +const path = require('path'); + +module.exports = function (api) { + api.cache(true); + return { + presets: ['babel-preset-expo'], + plugins: [ + process.env.NODE_ENV !== 'production' + ? [ + 'module-resolver', + { + alias: { + ['@react-native-aria/slider']: path.resolve( + __dirname, + '../../react-native-aria/slider/src' + ), + // For development, we want to alias the library to the source + }, + }, + ] + : ['babel-plugin-react-docgen-typescript', { exclude: 'node_modules' }], + ], + }; +}; diff --git a/packages/unstyled/range-slider/package.json b/packages/unstyled/range-slider/package.json new file mode 100644 index 0000000000..b961bc6beb --- /dev/null +++ b/packages/unstyled/range-slider/package.json @@ -0,0 +1,106 @@ +{ + "name": "@gluestack-ui/range-slider", + "description": "A universal headless range slider component for React Native, Next.js & React", + "version": "0.1.1", + "main": "lib/commonjs/index", + "module": "lib/module/index", + "types": "lib/typescript/index.d.ts", + "react-native": "src/index", + "source": "src/index", + "typings": "lib/typescript/index.d.ts", + "scripts": { + "prepare": "bob build", + "release": "release-it", + "build": "bob build", + "clean": "rm -rf lib", + "dev:web": "cd example/native && yarn web --clear", + "storybook": "cd example/native/storybook && yarn web" + }, + "devDependencies": { + "@types/react": "^18.0.22", + "@types/react-native": "^0.72.3", + "babel-plugin-transform-remove-console": "^6.9.4", + "react": "^18.1.0", + "react-dom": "^18.1.0", + "react-native": "^0.72.4", + "react-native-builder-bob": "^0.20.1", + "react-native-web": "^0.19.9", + "tsconfig": "7", + "typescript": "^4.9.4" + }, + "dependencies": { + "@gluestack-ui/form-control": "^0.1.14", + "@gluestack-ui/hooks": "0.1.11", + "@gluestack-ui/utils": "^0.1.12", + "@react-aria/visually-hidden": "^3.8.1", + "@react-native-aria/interactions": "^0.2.11", + "@react-native-aria/slider": "^0.2.10", + "@react-stately/slider": "^3.2.4" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + "commonjs", + [ + "module" + ], + "typescript" + ] + }, + "files": [ + "lib/", + "src/" + ], + "jest": { + "preset": "jest-expo", + "transform": { + "^.+\\.js$": "/node_modules/react-native/jest/preprocessor.js" + }, + "modulePathIgnorePatterns": [ + "/example/*", + "/lib/" + ], + "transformIgnorePatterns": [ + "node_modules/(?!(@react-native|react-native|expo-asset|expo-constants|@unimodules|react-native-unimodules|expo-font|react-native-svg|@expo/vector-icons|react-native-vector-icons|@react-native-aria/checkbox|@react-native-aria/interactions|@react-native-aria/button|@react-native-aria/switch|@react-native-aria/toggle|@react-native-aria/utils|@react-native-aria/*))" + ], + "setupFiles": [ + "/src/jest/mock.ts" + ] + }, + "release-it": { + "git": { + "commitMessage": "chore: release ${version}", + "tagName": "v${version}" + }, + "npm": { + "publish": true + }, + "github": { + "release": true + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": "angular" + } + } + }, + "keywords": [ + "react", + "native", + "react-native", + "slider", + "gluestack-ui", + "universal", + "headless", + "typescript", + "component", + "android", + "ios", + "nextjs" + ] +} diff --git a/packages/unstyled/range-slider/src/Context.ts b/packages/unstyled/range-slider/src/Context.ts new file mode 100644 index 0000000000..1e8acfcbe6 --- /dev/null +++ b/packages/unstyled/range-slider/src/Context.ts @@ -0,0 +1,3 @@ +import React from 'react'; + +export const RangeSliderContext = React.createContext({}); diff --git a/packages/unstyled/range-slider/src/RangeSlider.tsx b/packages/unstyled/range-slider/src/RangeSlider.tsx new file mode 100644 index 0000000000..ee458c11ff --- /dev/null +++ b/packages/unstyled/range-slider/src/RangeSlider.tsx @@ -0,0 +1,160 @@ +import React, { forwardRef } from 'react'; +import { useSliderState } from '@react-stately/slider'; +import { useLayout } from '@gluestack-ui/hooks'; +import type { IRangeSliderProps } from './types'; +import { RangeSliderContext } from './Context'; +import { useSlider } from '@react-native-aria/slider'; +import { useFormControlContext } from '@gluestack-ui/form-control'; + +function RangeSlider( + StyledRangeSlider: React.ComponentType +) { + return forwardRef( + ( + { + // isDisabled = false, + isReversed = false, + // 'isHovered', + // 'isDisabled', + // 'isFocused', + // 'isFocusVisible', + // 'isPressed', + // @ts-ignore + 'aria-label': ariaLabel = 'Slider', + children, + ...props + }: StyledSliderProps & IRangeSliderProps, + ref?: any + ) => { + const formControlContext = useFormControlContext(); + const { isDisabled, isReadOnly, ...newProps } = { + ...formControlContext, + ...props, + 'aria-label': ariaLabel, + } as any; + let trackRef = React.useRef(null); + // @ts-ignore + if (props.value && props?.value?.prop?.constructor === Array) { + //@ts-ignore - React Native Aria slider accepts array of values + newProps.value = props.value; + } + + if ( + props.defaultValue && + // @ts-ignore + props?.defaultValue?.prop?.constructor === Array + ) { + //@ts-ignore - React Native Aria slider accepts array of values + newProps.defaultValue = props.defaultValue; + } + props = newProps; + + const { onLayout, layout: trackLayout } = useLayout(); + + const updatedProps: IRangeSliderProps = Object.assign({}, props); + + if (isReadOnly || isDisabled) { + updatedProps.isDisabled = true; + } + + const state = useSliderState({ + ...updatedProps, + //@ts-ignore + numberFormatter: { format: (e) => e }, + minValue: props.minValue, + maxValue: props.maxValue, + orientation: props.orientation ?? 'horizontal', + + onChange: (val: any) => { + props.onChange && props.onChange(val); + }, + onChangeEnd: (val: any) => { + props.onChangeEnd && props.onChangeEnd(val); + }, + }); + + let { groupProps, trackProps, outputProps } = useSlider( + props as any, + state, + trackLayout + // trackRef + ); + + const [isFocused, setIsFocused] = React.useState(false); + const [isFocusVisible, setIsFocusVisible] = React.useState(false); + const [isHovered, setIsHovered] = React.useState(false); + // const [isPressed, setIsPressed] = React.useState(false); + const contextValue = React.useMemo(() => { + return { + trackLayout, + state, + orientation: props.orientation ? props.orientation : 'horizontal', + isDisabled: isDisabled, + isFocused: isFocused, + setIsFocused: setIsFocused, + isFocusVisible: isFocusVisible, + setIsFocusVisible: setIsFocusVisible, + outputProps, + // isPressed: isPressed, + // setIsPressed: setIsPressed, + isHovered: isHovered, + setIsHovered: setIsHovered, + isReversed: isReversed, + trackProps, + trackRef, + isReadOnly: isReadOnly, + onTrackLayout: onLayout, + // isHoveredProp, + // isDisabledProp, + // isFocusedProp, + // isFocusVisibleProp, + // isPressedProp, + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + trackProps, + trackRef, + trackLayout, + state, + // orientation, + isDisabled, + isReversed, + isReadOnly, + onLayout, + isFocused, + setIsFocused, + isFocusVisible, + setIsFocusVisible, + // isPressed, + // setIsPressed, + // isHoveredProp, + // isDisabledProp, + // isFocusedProp, + // isFocusVisibleProp, + // isPressedProp, + ]); + + return ( + + + {children} + + + ); + } + ); +} +export default RangeSlider; diff --git a/packages/unstyled/range-slider/src/RangeSliderFilledTrack.tsx b/packages/unstyled/range-slider/src/RangeSliderFilledTrack.tsx new file mode 100644 index 0000000000..43fd37aef5 --- /dev/null +++ b/packages/unstyled/range-slider/src/RangeSliderFilledTrack.tsx @@ -0,0 +1,101 @@ +import React, { forwardRef } from 'react'; +import { RangeSliderContext } from './Context'; +import { Platform } from 'react-native'; +import { mergeRefs } from '@gluestack-ui/utils'; + +function RangeSliderFilledTrack( + StyledSliderFilledTrack: React.ComponentType +) { + return forwardRef( + ( + { + _experimentalSliderFilledTrack = false, + _experimentalSliderFilledTrackValue = 0, + style, + ...props + }: any, + ref?: any + ) => { + const _ref = React.useRef(null); + + const { + state, + trackLayout, + orientation, + isDisabled, + isFocused, + isHovered, + isPressed, + isFocusVisible, + isHoveredProp, + isDisabledProp, + isFocusedProp, + isFocusVisibleProp, + isPressedProp, + } = React.useContext(RangeSliderContext); + + const getSliderTrackPosition = () => { + if (orientation === 'vertical') { + return ( + trackLayout.height - + (trackLayout.height * state.getThumbPercent(0) + + (trackLayout.height - + trackLayout.height * state.getThumbPercent(1))) + ); + } else { + return ( + trackLayout.width - + (trackLayout.width * state.getThumbPercent(0) + + (trackLayout.width - + trackLayout.width * state.getThumbPercent(1))) + ); + + // return trackLayout.width * state.getThumbPercent(0); + } + }; + + const sliderTrackPosition = getSliderTrackPosition(); + + let positionProps = + orientation === 'vertical' + ? { + height: Math.abs(sliderTrackPosition), + bottom: trackLayout?.height * state.getThumbPercent(0), + position: 'absolute', + } + : { + width: sliderTrackPosition, + left: trackLayout?.width * state.getThumbPercent(0), + }; + // if (_experimentalSliderFilledTrack) { + // // @ts-ignore + // positionProps = + // orientation === 'vertical' + // ? { height: _experimentalSliderFilledTrackValue } + // : { width: _experimentalSliderFilledTrackValue }; + // } + return ( + + ); + } + ); +} +export default RangeSliderFilledTrack; diff --git a/packages/unstyled/range-slider/src/RangeSliderLeftThumb.tsx b/packages/unstyled/range-slider/src/RangeSliderLeftThumb.tsx new file mode 100644 index 0000000000..afdc253b37 --- /dev/null +++ b/packages/unstyled/range-slider/src/RangeSliderLeftThumb.tsx @@ -0,0 +1,209 @@ +import React, { forwardRef, useEffect } from 'react'; +import { Platform } from 'react-native'; +import { useSliderThumb } from '@react-native-aria/slider'; +import { VisuallyHidden } from '@react-aria/visually-hidden'; +import { RangeSliderContext } from './Context'; +import { useHover } from '@react-native-aria/interactions'; +import { mergeRefs } from '@gluestack-ui/utils'; +import type { IRangeSliderThumbProps } from './types'; +import { useFocusRing, useFocus } from '@react-native-aria/focus'; +import { composeEventHandlers } from '@gluestack-ui/utils'; + +const positionMap = new Map([ + ['horizontal true', 'right'], + ['horizontal false', 'left'], + ['vertical true', 'top'], + ['vertical false', 'bottom'], +]); +const LeftThumbIndex = 0; +function RangeSliderThumb< + StyledRangeSliderThumb, + StyledRangeSliderThumbInteraction +>( + StyledRangeSliderThumb: React.ComponentType, + StyledRangeSliderThumbInteraction: React.ComponentType +) { + return forwardRef( + ( + { + children, + scaleOnPressed = 1, + style, + ...props + }: StyledRangeSliderThumbInteraction & + StyledRangeSliderThumbInteraction & + IRangeSliderThumbProps & { children?: any; style?: any }, + ref?: any + ) => { + // const [setThumbSize] = React.useState({ + // height: 0, + // width: 0, + // }); + + const _ref = React.useRef(null); + const { isHovered } = useHover({}, _ref); + // const [isFocused, setIsFocused] = React.useState(false) + const [isPressed, setIsPressed] = React.useState(false); + // const [isFocusVisible, setIsFocusVisible] = React.useState(false); + // const [isHovered, setIsHovered] = React.useState(false); + // const [isPressed, setIsPressed] = React.useState(false); + const { + state, + trackLayout, + orientation, + isDisabled, + isReversed, + // isPressed, + setIsHovered, + // setIsPressed, + setIsFocused, + setIsFocusVisible, + // isHoveredProp, + // isDisabledProp, + // isFocusedProp, + // isFocusVisibleProp, + // isPressedProp, + trackRef, + } = React.useContext(RangeSliderContext); + // const handleFocus = (focusState: boolean, callback: any) => { + // setIsFocused(focusState); + // callback(); + // }; + const inputRef = React.useRef(null); + // const { thumbProps, inputProps } = useSliderThumb( + // { + // index: LeftThumbIndex, + // trackLayout, + // inputRef, + // orientation: orientation, + // }, + // state, + // isReversed + // ); + + let { thumbProps, inputProps } = useSliderThumb( + { + index: LeftThumbIndex, + trackLayout, + orientation, + inputRef, + }, + state + ); + const { isFocusVisible, focusProps: focusRingProps }: any = + useFocusRing(); + const { isFocused, focusProps } = useFocus(); + + const thumbStyles: any = { + // transform: + // orientation === 'vertical' + // ? [ + // { + // translateY: isReversed + // ? -thumbSize?.height / 2 + // : thumbSize?.height / 2, + // }, + // ] + // : [ + // { + // translateX: isReversed + // ? thumbSize?.height / 2 + // : -thumbSize?.height / 2, + // }, + // ], + }; + + thumbStyles[`${positionMap.get(`${orientation} ${isReversed}`)}`] = `${ + state.getThumbPercent(LeftThumbIndex) * 100 + }%`; + thumbStyles?.transform?.push({ + scale: state.isThumbDragging(LeftThumbIndex) ? scaleOnPressed : 1, + }); + // thumbStyles?.transform?.push({ + // translateX: trackLayout.width ? -trackLayout.width / 2 : 0, + // }); + // transform: [{ translateX: layout.width ? -layout.width / 2 : 0 }], + // left: `${state.getThumbPercent(LeftThumbIndex) * 100}%`, + + useEffect(() => { + setIsPressed(state.isThumbDragging(LeftThumbIndex)); + }, [state, setIsPressed, isPressed]); + + React.useEffect(() => { + setIsPressed(state.isThumbDragging(LeftThumbIndex)); + }, [state, setIsPressed, isPressed]); + + useEffect(() => { + setIsFocused(isFocused); + }, [isFocused, setIsFocused]); + + useEffect(() => { + setIsFocusVisible(isFocusVisible); + }, [isFocusVisible, setIsFocusVisible]); + + useEffect(() => { + setIsHovered(isHovered); + }, [isHovered, setIsHovered]); + + return ( + { + // // @ts-ignore + // // setThumbSize({ + // // height: layout?.nativeEvent?.layout?.height, + // // width: layout?.nativeEvent?.layout?.width, + // // }); + // }} + states={{ + hover: isHovered, + disabled: isDisabled, + focus: isFocused, + focusVisible: isFocusVisible, + active: isPressed, + }} + disabled={isDisabled} + {...thumbProps} + style={{ + ...style, + ...thumbStyles, + }} + // style={{ + // // @ts-ignore + // }} + // @ts-ignore - web only + onFocus={composeEventHandlers( + composeEventHandlers(props?.onFocus, focusProps.onFocus), + focusRingProps.onFocus + )} + // @ts-ignore - web only + onBlur={composeEventHandlers( + composeEventHandlers(props?.onBlur, focusProps.onBlur), + focusRingProps.onBlur + )} + ref={mergeRefs([mergeRefs([_ref, ref]), trackRef])} + {...props} + > + {/* @ts-ignore */} + + {children} + {Platform.OS === 'web' && ( + + + + )} + + + ); + } + ); +} +export default RangeSliderThumb; diff --git a/packages/unstyled/range-slider/src/RangeSliderRightThumb.tsx b/packages/unstyled/range-slider/src/RangeSliderRightThumb.tsx new file mode 100644 index 0000000000..62373e8b66 --- /dev/null +++ b/packages/unstyled/range-slider/src/RangeSliderRightThumb.tsx @@ -0,0 +1,183 @@ +import React, { forwardRef, useEffect } from 'react'; +import { Platform } from 'react-native'; +import { useSliderThumb } from '@react-native-aria/slider'; +import { VisuallyHidden } from '@react-aria/visually-hidden'; +import { RangeSliderContext } from './Context'; +import { useHover } from '@react-native-aria/interactions'; +import { mergeRefs } from '@gluestack-ui/utils'; +import type { IRangeSliderThumbProps } from './types'; +import { useFocusRing, useFocus } from '@react-native-aria/focus'; +import { composeEventHandlers } from '@gluestack-ui/utils'; + +const positionMap = new Map([ + ['horizontal true', 'right'], + ['horizontal false', 'left'], + ['vertical true', 'top'], + ['vertical false', 'bottom'], +]); +const RightThumbIndex = 1; + +function RangeSliderThumb< + StyledRangeSliderThumb, + StyledRangeSliderThumbInteraction +>( + StyledRangeSliderThumb: React.ComponentType, + StyledRangeSliderThumbInteraction: React.ComponentType +) { + return forwardRef( + ( + { + children, + // FIX: Commenting to fix linting error + // scaleOnPressed = 1, + style, + ...props + }: StyledRangeSliderThumbInteraction & + StyledRangeSliderThumbInteraction & + IRangeSliderThumbProps & { children?: any; style?: any }, + ref?: any + ) => { + // const [setThumbSize] = React.useState({ + // height: 0, + // width: 0, + // }); + + const _ref = React.useRef(null); + const { isHovered } = useHover({}, _ref); + const [isPressed, setIsPressed] = React.useState(false); + + const { + state, + trackLayout, + orientation, + isDisabled, + isReversed, + // isPressed, + setIsHovered, + // setIsPressed, + setIsFocused, + setIsFocusVisible, + isHoveredProp, + isDisabledProp, + isFocusedProp, + isFocusVisibleProp, + isPressedProp, + } = React.useContext(RangeSliderContext); + + const inputRef = React.useRef(null); + const { thumbProps, inputProps } = useSliderThumb( + { + index: RightThumbIndex, + trackLayout, + inputRef, + orientation: orientation, + }, + state, + isReversed + ); + const { isFocusVisible, focusProps: focusRingProps }: any = + useFocusRing(); + const { isFocused, focusProps } = useFocus(); + + const thumbStyles: any = { + // transform: + // orientation === 'vertical' + // ? [ + // { + // translateY: isReversed + // ? -thumbSize?.height / 2 + // : thumbSize?.height / 2, + // }, + // ] + // : [ + // { + // translateX: isReversed + // ? thumbSize?.height / 2 + // : -thumbSize?.height / 2, + // }, + // ], + }; + thumbStyles[`${positionMap.get(`${orientation} ${isReversed}`)}`] = `${ + state.getThumbPercent(RightThumbIndex) * 100 + }%`; + thumbStyles?.transform?.push({ + transform: { + translateX: trackLayout.width ? -trackLayout.width / 2 : 0, + }, + // scale: state.isThumbDragging(0) ? scaleOnPressed : 1, + }); + + useEffect(() => { + setIsPressed(state.isThumbDragging(RightThumbIndex)); + }, [state, setIsPressed]); + + useEffect(() => { + setIsFocused(isFocused); + }, [isFocused, setIsFocused]); + + useEffect(() => { + setIsFocusVisible(isFocusVisible); + }, [isFocusVisible, setIsFocusVisible]); + + useEffect(() => { + setIsHovered(isHovered); + }, [isHovered, setIsHovered]); + + return ( + { + // // @ts-ignore + // setThumbSize({ + // height: layout?.nativeEvent?.layout?.height, + // width: layout?.nativeEvent?.layout?.width, + // }); + // }} + states={{ + hover: isHovered || isHoveredProp, + disabled: isDisabled || isDisabledProp, + focus: isFocused || isFocusedProp, + focusVisible: isFocusVisible || isFocusVisibleProp, + active: isPressed || isPressedProp, + }} + disabled={isDisabled} + {...thumbProps} + style={{ + ...style, + ...thumbStyles, + }} + // @ts-ignore - web only + onFocus={composeEventHandlers( + composeEventHandlers(props?.onFocus, focusProps.onFocus), + focusRingProps.onFocus + )} + // @ts-ignore - web only + onBlur={composeEventHandlers( + composeEventHandlers(props?.onBlur, focusProps.onBlur), + focusRingProps.onBlur + )} + ref={mergeRefs([_ref, ref])} + {...props} + > + {/* @ts-ignore */} + + {children} + {Platform.OS === 'web' && ( + + + + )} + + + ); + } + ); +} +export default RangeSliderThumb; diff --git a/packages/unstyled/range-slider/src/RangeSliderTrack.tsx b/packages/unstyled/range-slider/src/RangeSliderTrack.tsx new file mode 100644 index 0000000000..dba58f1139 --- /dev/null +++ b/packages/unstyled/range-slider/src/RangeSliderTrack.tsx @@ -0,0 +1,53 @@ +import React, { forwardRef } from 'react'; +import { RangeSliderContext } from './Context'; +import { mergeRefs } from '@gluestack-ui/utils'; +import { useHover } from '@react-native-aria/interactions'; + +function RangeSliderTrack( + StyledRangeSliderTrack: React.ComponentType +) { + return forwardRef(({ children, style, ...props }: any, ref?: any) => { + const _ref = React.useRef(null); + const { isHovered } = useHover({}, _ref); + const { + trackProps, + onTrackLayout, + isFocused, + isFocusVisible, + isDisabled, + isPressed, + isHoveredProp, + isDisabledProp, + isFocusedProp, + isFocusVisibleProp, + isPressedProp, + // state, + } = React.useContext(RangeSliderContext); + // const [isPressed, setIsPressed] = React.useState(false); + + // const { onPointerDown } = trackProps; + + return ( + + {children} + + ); + }); +} +export default RangeSliderTrack; diff --git a/packages/unstyled/range-slider/src/index.tsx b/packages/unstyled/range-slider/src/index.tsx new file mode 100644 index 0000000000..72c9ac529d --- /dev/null +++ b/packages/unstyled/range-slider/src/index.tsx @@ -0,0 +1,53 @@ +import RangeSliderMain from './RangeSlider'; +import RangeSliderLeftThumb from './RangeSliderLeftThumb'; +import RangeSliderRightThumb from './RangeSliderRightThumb'; +import RangeSliderTrack from './RangeSliderTrack'; +import RangeSliderFilledTrack from './RangeSliderFilledTrack'; +import type { IRangeSliderComponentType } from './types'; + +export { RangeSliderContext } from './Context'; + +export type { IRangeSliderProps } from './types'; + +export function createRangeSlider< + RangeSliderProps, + RangeSliderThumbInteractionProps, + RangeSliderLeftThumbProps, + RangeSliderRightThumbProps, + RangeSliderTrackProps, + RangeSliderFilledTrackProps +>({ + Root, + ThumbInteraction, + LeftThumb, + RightThumb, + Track, + FilledTrack, +}: { + Root: React.ComponentType; + LeftThumb: React.ComponentType; + RightThumb: React.ComponentType; + ThumbInteraction: React.ComponentType; + Track: React.ComponentType; + FilledTrack: React.ComponentType; +}) { + const RangeSlider: any = RangeSliderMain(Root); + RangeSlider.LeftThumb = RangeSliderLeftThumb(LeftThumb, ThumbInteraction); + RangeSlider.RightThumb = RangeSliderRightThumb(RightThumb, ThumbInteraction); + RangeSlider.Track = RangeSliderTrack(Track); + RangeSlider.FilledTrack = RangeSliderFilledTrack(FilledTrack); + + RangeSlider.displayName = 'RangeSlider'; + RangeSlider.LeftThumb.displayName = 'RangeSlider.LeftThumb'; + RangeSlider.RightThumb.displayName = 'RangeSlider.RightThumb'; + RangeSlider.Track.displayName = 'RangeSlider.Track'; + RangeSlider.FilledTrack.displayName = 'RangeSlider.FilledTrack'; + + return RangeSlider as IRangeSliderComponentType< + RangeSliderProps, + RangeSliderLeftThumbProps, + RangeSliderRightThumbProps, + RangeSliderTrackProps, + RangeSliderFilledTrackProps + >; +} diff --git a/packages/unstyled/range-slider/src/types.tsx b/packages/unstyled/range-slider/src/types.tsx new file mode 100644 index 0000000000..39980d9e9b --- /dev/null +++ b/packages/unstyled/range-slider/src/types.tsx @@ -0,0 +1,96 @@ +export interface InterfaceRangeSliderProps { + /** The current value of the Slider */ + value?: number; + /** The default value (uncontrolled). */ + defaultValue?: number; + /** Handler that is called when the value changes. */ + onChange?: (value: number) => void; + children?: any; + /** + * If true, the value will be incremented or decremented in reverse. + */ + isReversed?: boolean; + /** + * The orientation of the Slider. + * @default 'horizontal' + */ + orientation?: 'horizontal' | 'vertical'; + /** Whether the whole Slider is disabled. */ + isDisabled?: boolean; + /** Fired when the slider stops moving, due to being let go. */ + onChangeEnd?: (value: number) => void; + /** + * The slider's minimum value. + * @default 0 + */ + minValue?: number; + /** + * The slider's maximum value. + * @default 100 + */ + maxValue?: number; + /** + * The slider's step value. + * @default 1 + */ + step?: number; + /** Whether the whole Slider is readonly. */ + isReadOnly?: boolean; + /** Prop applied to change slider track height */ + sliderTrackHeight?: (string & {}) | number; + /**Prop applied to change size of slider thumb */ + thumbSize?: (string & {}) | number; + isHovered?: boolean; + isFocused?: boolean; + isFocusVisible?: boolean; + isPressed?: boolean; +} + +export interface IRangeSliderTrackProps { + /** Whether the whole Slider is readonly. */ + isReadOnly?: boolean; + children?: any; +} + +export interface IRangeSliderTrackFilledProps { + /** Whether the whole Slider is readonly. */ + isReadOnly?: boolean; +} + +export interface IRangeSliderThumbProps { + /** + * The orientation of the Slider. + * @default 'horizontal' + */ + orientation?: 'horizontal' | 'vertical'; + /** Whether the Thumb is disabled. */ + isDisabled?: boolean; + /** Whether the whole Slider is readonly. */ + isReadOnly?: boolean; + onFocus?: (e: any) => void; + onBlur?: (e: any) => void; + scaleOnPressed?: any; +} + +export type IRangeSliderComponentType< + StyledRangeSlider, + StyledRangeSliderLeftThumb, + StyledRangeSliderRightThumb, + StyledRangeSliderTrack, + StyledRangeSliderFilledTrack +> = React.ForwardRefExoticComponent & { + LeftThumb: React.ForwardRefExoticComponent< + StyledRangeSliderLeftThumb & IRangeSliderThumbProps + >; + RightThumb: React.ForwardRefExoticComponent< + StyledRangeSliderRightThumb & IRangeSliderThumbProps + >; + Track: React.ForwardRefExoticComponent< + StyledRangeSliderTrack & IRangeSliderTrackProps + >; + FilledTrack: React.ForwardRefExoticComponent< + StyledRangeSliderFilledTrack & IRangeSliderTrackFilledProps + >; +}; + +export type IRangeSliderProps = InterfaceRangeSliderProps; diff --git a/packages/unstyled/range-slider/tsconfig.json b/packages/unstyled/range-slider/tsconfig.json new file mode 100644 index 0000000000..3eed3361a6 --- /dev/null +++ b/packages/unstyled/range-slider/tsconfig.json @@ -0,0 +1,33 @@ +{ + "include": ["src"], + "exclude": ["node_modules", "example"], + "paths": { + "@gluestack-ui/utils": ["../utils/src"], + "@gluestack-ui/form-control": ["../form-control/src"], + "'@react-native-aria/slider'": ["../../react-native-aria/slider/src"] + }, + "compilerOptions": { + "emitDeclarationOnly": true, + "noEmit": false, + "baseUrl": "", + "declaration": true, + "allowUnreachableCode": false, + "allowUnusedLabels": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "lib": ["esnext", "dom"], + "module": "esnext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUnusedLocals": false, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "esnext" + } +} diff --git a/packages/unstyled/slider/src/SliderTrack.tsx b/packages/unstyled/slider/src/SliderTrack.tsx index e27a4cab06..e017feeb27 100644 --- a/packages/unstyled/slider/src/SliderTrack.tsx +++ b/packages/unstyled/slider/src/SliderTrack.tsx @@ -22,7 +22,6 @@ function SliderTrack( isFocusVisibleProp, isPressedProp, } = React.useContext(SliderContext); - return (