From c42d500014b56b945392467094ca66f05a6b5365 Mon Sep 17 00:00:00 2001 From: Bartosz Adamczyk Date: Thu, 9 Feb 2023 17:17:15 +0100 Subject: [PATCH 01/33] form component with step highlight --- src/components/dialog/Dialog.spec.tsx | 182 +------------------------- src/components/form/Form.spec.tsx | 28 ++++ src/components/form/Form.stories.mdx | 94 +++++++++++++ src/components/form/Form.tsx | 86 ++++++++++++ src/components/form/form.scss | 33 +++++ src/sass/main.scss | 1 + test.js | 0 7 files changed, 244 insertions(+), 180 deletions(-) create mode 100644 src/components/form/Form.spec.tsx create mode 100644 src/components/form/Form.stories.mdx create mode 100644 src/components/form/Form.tsx create mode 100644 src/components/form/form.scss create mode 100644 test.js diff --git a/src/components/dialog/Dialog.spec.tsx b/src/components/dialog/Dialog.spec.tsx index 5cc2d69383..6603a5024e 100644 --- a/src/components/dialog/Dialog.spec.tsx +++ b/src/components/dialog/Dialog.spec.tsx @@ -1,181 +1,3 @@ -import * as React from 'react'; -import {mount} from 'enzyme'; -import Dialog from './Dialog'; -import DialogCloseButton from './DialogCloseButton'; +import {shallow} from 'enzyme'; -window.scrollTo = jest.fn(); -describe('', () => { - it('renders children', () => { - const wrapper = mount(content text); - - expect(wrapper.contains('content text')).toBe(true); - }); - it('renders proper size', () => { - const wrapper = mount( - - content text - - ); - - expect(wrapper.find('.sg-dialog__overlay--size-xl')).toHaveLength(1); - }); - it('renders outside scroll', () => { - const wrapper = mount( - - content text - - ); - - expect(wrapper.find('.sg-dialog__overlay--scroll')).toHaveLength(1); - }); - it('renders inside scroll', () => { - const wrapper = mount( - - content text - - ); - - expect(wrapper.find('.sg-dialog__container--scroll')).toHaveLength(1); - }); - it('fires onDismiss callback on Escape key', () => { - const onDismiss = jest.fn(); - const wrapper = mount( - - content text - - ); - - wrapper.simulate('keyUp', { - key: 'Escape', - }); - expect(onDismiss).toHaveBeenCalledTimes(1); - }); - it('fires onDismiss callback on Overlay click', () => { - const onDismiss = jest.fn(); - const wrapper = mount( - - content text - - ); - - wrapper.find('.sg-dialog__overlay').simulate('click'); - expect(onDismiss).toHaveBeenCalledTimes(1); - }); - it('fires onEntryTransitionEnd callback on entry', () => { - const onEntryTransitionEnd = jest.fn(); - - mount( - - content text - - ); - expect(onEntryTransitionEnd).toHaveBeenCalledTimes(1); - }); - it('fires onExitTransitionEnd callback on exit', () => { - const onExitTransitionEnd = jest.fn(); - const wrapper = mount( - - content text - - ); - - wrapper.setProps({ - open: false, - }); - expect(onExitTransitionEnd).toHaveBeenCalledTimes(1); - }); - it('does not fire onEntryTransitionEnd callback before open', () => { - const onEntryTransitionEnd = jest.fn(); - - mount( - - content text - - ); - expect(onEntryTransitionEnd).toHaveBeenCalledTimes(0); - }); - it('does not fire onExitTransitionEnd callback before open', () => { - const onExitTransitionEnd = jest.fn(); - - mount( - - content text - - ); - expect(onExitTransitionEnd).toHaveBeenCalledTimes(0); - }); - it('returns null after exit transition', () => { - const wrapper = mount(content text); - - expect(wrapper.isEmptyRender()).toBe(false); - wrapper.setProps({ - open: false, - }); - wrapper.update(); - expect(wrapper.isEmptyRender()).toBe(true); - }); - it('sets given zIndex', () => { - const wrapper = mount( - - content text - - ); - const dialogOverlay = wrapper.find('.sg-dialog__overlay'); - - expect(dialogOverlay.props().style?.zIndex).toEqual(10); - }); - it('sets given data-testid to dialog', () => { - const wrapper = mount( - - content text - - ); - const dialog = wrapper.find('.sg-dialog__container'); - - expect(dialog.props()['data-testid']).toEqual('test_id'); - }); - it('sets given data-testid to dialog close button', () => { - const wrapper = mount( - - - - ); - const closeBtn = wrapper.find('Button.sg-dialog__close-button'); - - expect(closeBtn.props()['data-testid']).toEqual('test_id'); - }); - it('forces no-scroll class removal before onExitTransitionEnd callback', () => { - const onExitTransitionEnd = () => { - expect( - document.body.classList.contains('sg-dialog-no-scroll') - ).toBeFalsy(); - }; - - const wrapper = mount( - - content text - - ); - - wrapper.setProps({ - open: false, - }); - }); - it('does not force no-scroll class removal before onEntryTransitionEnd callback', () => { - const onEntryTransitionEnd = () => { - expect( - document.body.classList.contains('sg-dialog-no-scroll') - ).toBeTruthy(); - }; - - const wrapper = mount( - - content text - - ); - - wrapper.setProps({ - open: false, - }); - }); -}); +const wrapper = shallow(); diff --git a/src/components/form/Form.spec.tsx b/src/components/form/Form.spec.tsx new file mode 100644 index 0000000000..52f46c48ff --- /dev/null +++ b/src/components/form/Form.spec.tsx @@ -0,0 +1,28 @@ +describe('Form', () => { + // current step + it('shows mid transparent overlay on the content, except current step'); + it( + 'WHEN current step content is smaller than screen height, THEN it centers current step' + ); + it( + 'WHEN entering on step, WHEN current step has been touched, THEN validation is fired' + ); + + // validation + it( + 'WHEN validation is fired AND WHEN validate props returns true, THEN validation pass' + ); + + // navigation + it('WHEN on step other than first, THEN shows previous step button'); + it( + 'WHEN on step other than last AND WHEN current step pass validation, THEN shows next step button' + ); + it( + 'WHEN next button is clicked AND WHEN validation is passed, THEN step is changed to next' + ); + it('shows press enter hint'); + + // notification + it('WHEN step has changed, THEN fires onStepChange'); +}); diff --git a/src/components/form/Form.stories.mdx b/src/components/form/Form.stories.mdx new file mode 100644 index 0000000000..32f98e8b5c --- /dev/null +++ b/src/components/form/Form.stories.mdx @@ -0,0 +1,94 @@ +import {Form, FormStep} from './Form'; +import {ArgsTable, Meta, Story, Canvas} from '@storybook/addon-docs'; +import PageHeader from 'blocks/PageHeader'; +import Flex from '../flex/Flex'; + + + +Form + +- [Stories](#stories) + +## Overview + + + + {args => ( +
+
+
+ )} +
+
+ + + +## Stories + +### Types + + + + {args => ( +
+ + + + Consequat id eiusmod enim labore aliquip reprehenderit. Elit + ullamco mollit veniam amet quis labore labore magna aute anim ad + do ex est. Excepteur reprehenderit ipsum in anim elit magna + voluptate Lorem eu anim excepteur proident dolore. Nostrud + excepteur et nostrud nisi amet voluptate dolor duis laborum + proident et. Veniam aliqua pariatur cupidatat officia deserunt + occaecat cillum ex proident cupidatat quis nostrud irure quis. + Occaecat voluptate cillum excepteur ex irure ut ea exercitation + consequat et commodo exercitation velit. Consectetur reprehenderit + aute dolor ullamco consectetur nostrud laboris dolore irure + pariatur pariatur incididunt mollit exercitation. In nulla ipsum + laboris culpa tempor eu ea est ad. Velit dolor labore voluptate + ullamco incididunt reprehenderit consequat quis id laborum aliqua. + + + + + Eu sunt ad sunt labore culpa minim officia pariatur elit + exercitation Lorem. Sunt commodo pariatur aliquip ad dolore culpa + nostrud veniam occaecat quis ad. Ea ea do dolor labore. Pariatur + eiusmod in do et. Commodo ex cillum dolor nulla aliquip enim qui + ea veniam exercitation aute non officia. Deserunt amet commodo + Lorem aliqua commodo esse dolore id enim amet dolor Lorem mollit + dolor. Id anim enim proident laborum et ipsum laboris veniam sint. + Ad aliquip laboris in consequat id. Excepteur adipisicing labore + anim eiusmod veniam reprehenderit aute. Cillum culpa quis sint et + minim esse sit quis esse proident proident. Ad Lorem officia amet + elit proident ad deserunt ullamco ullamco id aute. Lorem irure + fugiat sint deserunt est pariatur anim. Non mollit dolor enim + occaecat. Ullamco ullamco mollit irure esse commodo aliqua aliquip + dolor minim. Cillum ex ea nostrud nulla eu consectetur voluptate. + Ea laboris incididunt elit commodo minim sit qui aliquip id. + + + + + Id aute aute cupidatat laboris occaecat officia commodo veniam. + Lorem laborum excepteur aliquip nisi nulla enim amet ullamco. Eu + eiusmod Lorem eiusmod irure. Proident aliquip eu non magna. Eu qui + in cupidatat cillum anim commodo adipisicing mollit mollit cillum + quis ullamco cillum. Non enim tempor fugiat laboris qui mollit. + Amet consequat ea minim sint ut ea ut cupidatat eiusmod veniam. + Proident ut ipsum eiusmod deserunt ipsum Lorem dolore labore duis. + Duis commodo ullamco ut tempor magna culpa sunt culpa exercitation + consequat labore esse id ipsum. Culpa eiusmod Lorem non elit + mollit voluptate officia ad consequat culpa dolore aliqua mollit. + Ullamco laboris aute qui adipisicing occaecat tempor enim. + + + +
+ )} +
+
+ +## Accessibility + + diff --git a/src/components/form/Form.tsx b/src/components/form/Form.tsx new file mode 100644 index 0000000000..52dea81168 --- /dev/null +++ b/src/components/form/Form.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import Button from '../buttons/Button'; +import Flex from '../flex/Flex'; +import Icon, {TYPE} from '../icons/Icon'; + +type ContextType = { + currentStep: string; +}; + +export const FormContext = React.createContext({} as ContextType); + +export const Form: React.FunctionComponent = ({children}) => { + const [currentStepIndex, setCurrentStepIndex] = React.useState(0); + const contentRef = React.useRef(null); + const stepRefs = React.useRef([]); + const steps = React.Children.toArray(children); + const onStepChange = React.useCallback(index => { + const stepNode: HTMLElement = stepRefs.current[index]; + const contentScrollPosition = stepNode.offsetTop; + + setCurrentStepIndex(index); + + if (contentRef) { + contentRef.current.style.transform = `translateY(-${contentScrollPosition}px)`; + } + }, []); + const handleUp = React.useCallback(() => { + onStepChange(currentStepIndex - 1); + }, [currentStepIndex, onStepChange]); + const handleDown = React.useCallback(() => { + onStepChange(currentStepIndex + 1); + }, [currentStepIndex, onStepChange]); + + return ( +
+
+ {steps.map((child, index) => ( +
{ + stepRefs.current[index] = ref; + }} + > + {child} + {currentStepIndex !== index ? ( + + ) : null} +
+ ))} +
+ + {currentStepIndex > 0 ? ( +
+ ); +}; + +type FormStepPropsType = { + children?: React.ReactNode; +}; + +export const FormStep = React.forwardRef( + ({children}: FormStepPropsType, ref) => { + return ( +
+
{children}
+
+ ); + } +); diff --git a/src/components/form/form.scss b/src/components/form/form.scss new file mode 100644 index 0000000000..174039890d --- /dev/null +++ b/src/components/form/form.scss @@ -0,0 +1,33 @@ +.sg-form { + height: 100%; + position: relative; + overflow: hidden; + + &-content { + transition: transform 200ms linear; + } + + &-navigation { + position: absolute; + right: 24px; + bottom: 24px; + } + + &-step-area { + position: relative; + + &__overlay { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + background-color: $white; + opacity: 0.5; + } + } +} + +.sg-form-step { + position: relative; +} diff --git a/src/sass/main.scss b/src/sass/main.scss index 7456fdfc2d..e86951ac22 100644 --- a/src/sass/main.scss +++ b/src/sass/main.scss @@ -66,3 +66,4 @@ $sgFontsPath: 'fonts/' !default; @import '../components/dialog/dialog-no-scroll'; @import '../components/transition/transition'; @import '../components/skip-link/skip-link'; +@import '../components/form/form.scss'; diff --git a/test.js b/test.js new file mode 100644 index 0000000000..e69de29bb2 From fafaebfb0d544f64f2643913059fce5ef8579839 Mon Sep 17 00:00:00 2001 From: Bartosz Adamczyk Date: Mon, 13 Feb 2023 15:29:16 +0100 Subject: [PATCH 02/33] demo --- .../public/form_stories__avatar-mock.png | Bin 0 -> 19952 bytes .../public/form_stories__clickable-area.png | Bin 0 -> 54444 bytes package.json | 3 +- src/components/form/Form.stories.mdx | 357 +++++++++++++++--- src/components/form/Form.tsx | 246 +++++++++--- src/components/form/form.scss | 26 +- src/components/form/form.stories.scss | 46 +++ src/components/form/useFocusTrap.ts | 143 +++++++ src/components/form/useFollowFocus.ts | 36 ++ src/components/search/Search.spec.tsx | 2 +- src/components/utils/forms/src/index.ts | 3 + .../utils/forms/src/lib/forms.spec.tsx | 228 +++++++++++ src/components/utils/forms/src/lib/types.ts | 103 +++++ .../utils/forms/src/lib/useBrainlyForm.tsx | 143 +++++++ .../forms/src/lib/useBrainlyFormField.ts | 116 ++++++ yarn.lock | 5 + 16 files changed, 1340 insertions(+), 117 deletions(-) create mode 100644 .storybook/public/form_stories__avatar-mock.png create mode 100644 .storybook/public/form_stories__clickable-area.png create mode 100644 src/components/form/form.stories.scss create mode 100644 src/components/form/useFocusTrap.ts create mode 100644 src/components/form/useFollowFocus.ts create mode 100644 src/components/utils/forms/src/index.ts create mode 100644 src/components/utils/forms/src/lib/forms.spec.tsx create mode 100644 src/components/utils/forms/src/lib/types.ts create mode 100644 src/components/utils/forms/src/lib/useBrainlyForm.tsx create mode 100644 src/components/utils/forms/src/lib/useBrainlyFormField.ts diff --git a/.storybook/public/form_stories__avatar-mock.png b/.storybook/public/form_stories__avatar-mock.png new file mode 100644 index 0000000000000000000000000000000000000000..93f5f441ec0ce6c520df0922c47fef9fecfa094c GIT binary patch literal 19952 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 literal 0 HcmV?d00001 diff --git a/.storybook/public/form_stories__clickable-area.png b/.storybook/public/form_stories__clickable-area.png new file mode 100644 index 0000000000000000000000000000000000000000..165a382f7c4bff7d0f3e5625efc9b562806aec40 GIT binary patch literal 54444 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 F{V%algGc}X literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 9f10ef5d23..cf76ba644d 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "inquirer": "^8.2.0", "jscodeshift": "^0.13.1", "meow": "^5.0.0", - "path": "^0.12.7" + "path": "^0.12.7", + "react-hook-form": "^7.43.1" }, "devDependencies": { "@babel/cli": "^7.8.3", diff --git a/src/components/form/Form.stories.mdx b/src/components/form/Form.stories.mdx index 32f98e8b5c..5dacaa8035 100644 --- a/src/components/form/Form.stories.mdx +++ b/src/components/form/Form.stories.mdx @@ -1,7 +1,13 @@ -import {Form, FormStep} from './Form'; -import {ArgsTable, Meta, Story, Canvas} from '@storybook/addon-docs'; -import PageHeader from 'blocks/PageHeader'; +import {Form, FormStep, FormContext} from './Form'; import Flex from '../flex/Flex'; +import Button from '../buttons/Button'; +import Headline from '../text/Headline'; +import Text from '../text/Text'; +import Icon from '../icons/Icon'; +import Checkbox from '../form-elements/checkbox/Checkbox'; +import Input from '../form-elements/Input'; +import {ArgsTable, Meta, Story, Canvas} from '@storybook/addon-docs'; +import './form.stories.scss'; @@ -29,63 +35,294 @@ import Flex from '../flex/Flex'; - {args => ( -
-
- - - Consequat id eiusmod enim labore aliquip reprehenderit. Elit - ullamco mollit veniam amet quis labore labore magna aute anim ad - do ex est. Excepteur reprehenderit ipsum in anim elit magna - voluptate Lorem eu anim excepteur proident dolore. Nostrud - excepteur et nostrud nisi amet voluptate dolor duis laborum - proident et. Veniam aliqua pariatur cupidatat officia deserunt - occaecat cillum ex proident cupidatat quis nostrud irure quis. - Occaecat voluptate cillum excepteur ex irure ut ea exercitation - consequat et commodo exercitation velit. Consectetur reprehenderit - aute dolor ullamco consectetur nostrud laboris dolore irure - pariatur pariatur incididunt mollit exercitation. In nulla ipsum - laboris culpa tempor eu ea est ad. Velit dolor labore voluptate - ullamco incididunt reprehenderit consequat quis id laborum aliqua. - - - - - Eu sunt ad sunt labore culpa minim officia pariatur elit - exercitation Lorem. Sunt commodo pariatur aliquip ad dolore culpa - nostrud veniam occaecat quis ad. Ea ea do dolor labore. Pariatur - eiusmod in do et. Commodo ex cillum dolor nulla aliquip enim qui - ea veniam exercitation aute non officia. Deserunt amet commodo - Lorem aliqua commodo esse dolore id enim amet dolor Lorem mollit - dolor. Id anim enim proident laborum et ipsum laboris veniam sint. - Ad aliquip laboris in consequat id. Excepteur adipisicing labore - anim eiusmod veniam reprehenderit aute. Cillum culpa quis sint et - minim esse sit quis esse proident proident. Ad Lorem officia amet - elit proident ad deserunt ullamco ullamco id aute. Lorem irure - fugiat sint deserunt est pariatur anim. Non mollit dolor enim - occaecat. Ullamco ullamco mollit irure esse commodo aliqua aliquip - dolor minim. Cillum ex ea nostrud nulla eu consectetur voluptate. - Ea laboris incididunt elit commodo minim sit qui aliquip id. - - - - - Id aute aute cupidatat laboris occaecat officia commodo veniam. - Lorem laborum excepteur aliquip nisi nulla enim amet ullamco. Eu - eiusmod Lorem eiusmod irure. Proident aliquip eu non magna. Eu qui - in cupidatat cillum anim commodo adipisicing mollit mollit cillum - quis ullamco cillum. Non enim tempor fugiat laboris qui mollit. - Amet consequat ea minim sint ut ea ut cupidatat eiusmod veniam. - Proident ut ipsum eiusmod deserunt ipsum Lorem dolore labore duis. - Duis commodo ullamco ut tempor magna culpa sunt culpa exercitation - consequat labore esse id ipsum. Culpa eiusmod Lorem non elit - mollit voluptate officia ad consequat culpa dolore aliqua mollit. - Ullamco laboris aute qui adipisicing occaecat tempor enim. - - -
-
- )} + {args => { + const nextButtonRef = React.useRef(null); + return ( +
+
+ + {({changeStep, currentStepIndex}) => { + return ( + { + if (event.key === 'Enter') { + if (event.target !== nextButtonRef.current) { + changeStep(currentStepIndex + 1); + } + } + }} + > +
+ + Almost there. + + Let us know you better + + + +
+ + 1 + +
+ Who are you? +
+ + + + + + or press + + + + +
+
+
+
+ ); + }} +
+ + {({changeStep, currentStepIndex}) => ( + +
+ + +
+ + 2 + +
+ + What email adress can we reach you at? + +
+ + This is only to get in touch, not to send spam. We love + GDPR and GDPR loves us. + + + + + + + or press + + + + +
+
+
+ )} +
+ + {({changeStep, currentStepIndex}) => ( + +
+ + +
+ + 3 + +
+ Tell us a little bit more +
+ + We want to keep Brainly safe space for everybody + + + Username + + + + Password + + + + Your age + + + + Your country + + + + + + + or press + + + + +
+
+
+ )} +
+ + {({changeStep, currentStepIndex}) => ( + +
+ + +
+ + 4 + +
+ Tell us a little bit more +
+ + We want to keep Brainly safe space for everybody + + + + + + + or press + + + + +
+
+
+ )} +
+ + {({changeStep, currentStepIndex}) => ( + +
+ + + By creating an account, you accept the Brainly Terms of + Service & Privacy Policy + + + + + + or press + + + + + + + + + + We will post your question immediately after finishing + registration. + + + +
+
+ )} +
+
+
+ ); + }}
diff --git a/src/components/form/Form.tsx b/src/components/form/Form.tsx index 52dea81168..ec01e6adad 100644 --- a/src/components/form/Form.tsx +++ b/src/components/form/Form.tsx @@ -2,85 +2,225 @@ import * as React from 'react'; import Button from '../buttons/Button'; import Flex from '../flex/Flex'; import Icon, {TYPE} from '../icons/Icon'; +import cc from 'classnames'; +import {useFocusTrap} from './useFocusTrap'; +import {useFollowFocus} from './useFollowFocus'; -type ContextType = { - currentStep: string; +// https://w3c.github.io/aria-practices/examples/js/utils.js +function isFocusable(element: HTMLElement) { + if (element.tabIndex < -1 || element.getAttribute('disabled')) { + return false; + } + + switch (element.nodeName) { + case 'A': + return ( + !!element.getAttribute('href') && + element.getAttribute('rel') !== 'ignore' + ); + + case 'INPUT': + return element.getAttribute('type') !== 'hidden'; + + case 'BUTTON': + case 'SELECT': + case 'TEXTAREA': + return true; + + default: { + return false; + } + } +} + +function attemptFocus(element: HTMLElement) { + if (!isFocusable(element)) { + return false; + } + + try { + element.focus(); + } catch {} + + return document.activeElement === element; +} + +function focusFirstDescendant(element: HTMLElement) { + for (let i = 0; i < element.children.length; i++) { + const child = element.children[i] as HTMLElement; + + if (attemptFocus(child) || focusFirstDescendant(child)) { + return true; + } + } + + return false; +} + +type FormContextType = { + isLastStep: boolean; + currentStepIndex: number; + changeStep: (step: number) => void; }; -export const FormContext = React.createContext({} as ContextType); +export const FormContext = React.createContext( + {} as FormContextType +); export const Form: React.FunctionComponent = ({children}) => { + // state & refs const [currentStepIndex, setCurrentStepIndex] = React.useState(0); const contentRef = React.useRef(null); - const stepRefs = React.useRef([]); + const stepContainersRef = React.useRef>([]); + const stepContentElementsRef = React.useRef>([]); const steps = React.Children.toArray(children); - const onStepChange = React.useCallback(index => { - const stepNode: HTMLElement = stepRefs.current[index]; - const contentScrollPosition = stepNode.offsetTop; - setCurrentStepIndex(index); + // navigation + const changeStep = React.useCallback( + index => { + const stepNode: HTMLElement = stepContainersRef.current[index]; - if (contentRef) { - contentRef.current.style.transform = `translateY(-${contentScrollPosition}px)`; - } - }, []); + if (index < 0 || index > steps.length - 1) { + return; + } + + setCurrentStepIndex(index); + + if (contentRef) { + contentRef.current.style.transform = `translateY(-${ + index === 0 ? stepNode.offsetTop : stepNode.offsetTop - 100 + }px)`; + + focusFirstDescendant(stepNode); + } + }, + [steps] + ); const handleUp = React.useCallback(() => { - onStepChange(currentStepIndex - 1); - }, [currentStepIndex, onStepChange]); + changeStep(currentStepIndex - 1); + }, [currentStepIndex, changeStep]); const handleDown = React.useCallback(() => { - onStepChange(currentStepIndex + 1); - }, [currentStepIndex, onStepChange]); + changeStep(currentStepIndex + 1); + }, [currentStepIndex, changeStep]); + + const buttonPrevClassNames = cc('sg-form-navigation__button-prev', { + 'sg-form-navigation__button-prev--hidden': currentStepIndex === 0, + 'sg-form-navigation__button-prev--bottom': + currentStepIndex === 0 || currentStepIndex === steps.length - 1, + }); + + const buttonNextClassNames = cc('sg-form-navigation__button-next', { + 'sg-form-navigation__button-next--hidden': + currentStepIndex === steps.length - 1, + }); + + // step focus trap + // useFocusTrap({ + // stepContainersRef, + // currentStepIndex, + // stepContentElementsRef, + // }); + + // change step when focus is outside current step + useFollowFocus({ + changeStep, + currentStepIndex, + stepContainersRef, + }); + + const handleArrowNavigation = React.useCallback( + event => { + switch (event.key) { + case 'ArrowUp': { + changeStep(currentStepIndex - 1); + break; + } + case 'ArrowDown': { + // Down pressed + changeStep(currentStepIndex + 1); + break; + } + default: { + // + } + } + }, + [changeStep, currentStepIndex] + ); return ( -
-
- {steps.map((child, index) => ( -
{ - stepRefs.current[index] = ref; - }} - > - {child} - {currentStepIndex !== index ? ( - - ) : null} -
- ))} -
- - {currentStepIndex > 0 ? ( + +
+
+ {steps.map((child, index) => ( +
{ + stepContainersRef.current[index] = ref; + }} + > +
{ + stepContentElementsRef.current[index] = ref; + }} + > + {child} + {currentStepIndex !== index ? ( + + ) : null} +
+
+ ))} +
+
+
+
+ ); }; type FormStepPropsType = { - children?: React.ReactNode; + children: (FormContextType) => React.ReactNode; }; -export const FormStep = React.forwardRef( - ({children}: FormStepPropsType, ref) => { - return ( -
-
{children}
+export const FormStep: React.FunctionComponent = ({ + children, +}) => { + const {changeStep, currentStepIndex} = React.useContext(FormContext); + + return ( +
+
+ {children({changeStep, currentStepIndex})}
- ); - } -); +
+ ); +}; diff --git a/src/components/form/form.scss b/src/components/form/form.scss index 174039890d..4327ff398e 100644 --- a/src/components/form/form.scss +++ b/src/components/form/form.scss @@ -4,17 +4,39 @@ overflow: hidden; &-content { - transition: transform 200ms linear; + transition: transform $durationGentle2 $easingRegular; } &-navigation { position: absolute; right: 24px; bottom: 24px; + + &__button-prev { + z-index: 20; + transition: transform $durationModerate2 $easingRegular; + + &--bottom { + transform: translateY(55px); + } + + &--hidden { + z-index: 19; + } + } + + &__button-next { + z-index: 20; + + &--hidden { + z-index: 19; + } + } } &-step-area { position: relative; + padding: 50px 0; &__overlay { position: absolute; @@ -23,7 +45,7 @@ height: 100%; width: 100%; background-color: $white; - opacity: 0.5; + opacity: 0.8; } } } diff --git a/src/components/form/form.stories.scss b/src/components/form/form.stories.scss new file mode 100644 index 0000000000..04bcb99c31 --- /dev/null +++ b/src/components/form/form.stories.scss @@ -0,0 +1,46 @@ +@import '../../sass/colors'; + +.form-stories { + &__step-wide { + width: 550px; + } + + &-press-enter { + margin-left: 16px; + + &__icon { + margin-left: 8px; + } + } + + &-counter { + background: $black; + color: $white; + border-radius: 50%; + height: 24px; + width: 24px; + display: flex; + justify-content: center; + align-items: center; + margin-right: 16px; + + .sg-text { + margin-top: 2px; + } + } + + &__step-subheadline { + margin-top: 16px; + } + + &__exclamation-mark { + background: $gray-50; + width: 20px; + height: 20px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; + } +} diff --git a/src/components/form/useFocusTrap.ts b/src/components/form/useFocusTrap.ts new file mode 100644 index 0000000000..8dcddbf3a2 --- /dev/null +++ b/src/components/form/useFocusTrap.ts @@ -0,0 +1,143 @@ +import * as React from 'react'; + +export function useFocusTrap({ + stepContainersRef, + stepContentElementsRef, + currentStepIndex, +}: { + stepContainersRef: { + current: Array | null; + }; + stepContentElementsRef: { + current: Array | null; + }; + currentStepIndex: number; +}) { + React.useEffect(() => { + const originalActiveElement = document.activeElement as HTMLElement; + + if ( + !stepContentElementsRef.current[currentStepIndex] || + !stepContainersRef.current[currentStepIndex] + ) { + return; + } + + const currentStepContainerElement = + stepContainersRef.current[currentStepIndex]; + const currentStepContentElement = + stepContentElementsRef.current[currentStepIndex]; + + // Initial focus + focusDescendant(currentStepContentElement, true); + let isTabbingForward = true; + + function handleKeydown(event: KeyboardEvent) { + isTabbingForward = event.key === 'Tab' && !event.shiftKey; + } + + function handleKeyup() { + isTabbingForward = true; + } + + function handleFocusTrap(event: FocusEvent) { + if ( + event.target instanceof Node && + currentStepContentElement.contains(event.target) + ) { + console.log('is descendant'); + return; + } + + console.log('is not descendant'); + + focusDescendant(currentStepContentElement, isTabbingForward); + } + + currentStepContentElement.addEventListener('keydown', handleKeydown); + currentStepContentElement.addEventListener('keyup', handleKeyup); + currentStepContainerElement.addEventListener('focusin', handleFocusTrap); + return () => { + currentStepContentElement.removeEventListener('keydown', handleKeydown); + currentStepContentElement.removeEventListener('keyup', handleKeyup); + currentStepContainerElement.removeEventListener( + 'focusin', + handleFocusTrap + ); + // Should restore original focus on unmount. + originalActiveElement?.focus(); + }; + }, [currentStepIndex, stepContainersRef, stepContentElementsRef]); +} + +function focusDescendant(element: HTMLElement, isTabbingForward: boolean) { + const descendantFocused = isTabbingForward + ? focusFirstDescendant(element) + : focusLastDescendant(element); + + return descendantFocused || attemptFocus(element); +} + +// https://www.w3.org/TR/wai-aria-practices-1.1/examples/dialog-modal/js/dialog.js +function focusFirstDescendant(element: HTMLElement) { + for (let i = 0; i < element.children.length; i++) { + const child = element.children[i] as HTMLElement; + + if (attemptFocus(child) || focusFirstDescendant(child)) { + return true; + } + } + + return false; +} + +function focusLastDescendant(element: HTMLElement) { + for (let i = element.children.length - 1; i >= 0; i--) { + const child = element.children[i] as HTMLElement; + + if (attemptFocus(child) || focusLastDescendant(child)) { + return true; + } + } + + return false; +} + +function attemptFocus(element: HTMLElement) { + if (!isFocusable(element)) { + return false; + } + + try { + element.focus(); + } catch {} + + return document.activeElement === element; +} + +// https://w3c.github.io/aria-practices/examples/js/utils.js +function isFocusable(element: HTMLElement) { + if (element.tabIndex < -1 || element.getAttribute('disabled')) { + return false; + } + + switch (element.nodeName) { + case 'A': + return ( + !!element.getAttribute('href') && + element.getAttribute('rel') !== 'ignore' + ); + + case 'INPUT': + return element.getAttribute('type') !== 'hidden'; + + case 'BUTTON': + case 'SELECT': + case 'TEXTAREA': + return true; + + default: { + return element.tabIndex >= -1; + } + } +} diff --git a/src/components/form/useFollowFocus.ts b/src/components/form/useFollowFocus.ts new file mode 100644 index 0000000000..cc12d31ceb --- /dev/null +++ b/src/components/form/useFollowFocus.ts @@ -0,0 +1,36 @@ +import * as React from 'react'; + +export const useFollowFocus = ({ + stepContainersRef, + currentStepIndex, + changeStep, +}) => { + React.useEffect(() => { + const stepContainersElements = stepContainersRef.current; + + const handleFocus = event => { + stepContainersElements.forEach((stepContainerElement, index) => { + if ( + stepContainerElement.contains(event.target) && + index !== currentStepIndex + ) { + changeStep(index); + } + }); + }; + + if (stepContainersElements) { + stepContainersElements.forEach(element => { + element.addEventListener('focusin', handleFocus); + }); + } + + return () => { + if (stepContainersElements) { + stepContainersElements.forEach(element => { + element.removeEventListener('focusin', handleFocus); + }); + } + }; + }, [stepContainersRef, currentStepIndex, changeStep]); +}; diff --git a/src/components/search/Search.spec.tsx b/src/components/search/Search.spec.tsx index 0279ef59aa..12232d57cb 100644 --- a/src/components/search/Search.spec.tsx +++ b/src/components/search/Search.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import Search, {SIZE, COLOR} from './Search'; -import Input from 'form-elements/Input'; +import Input from 'src/components/form-elements/Input'; import Icon, {TYPE, ICON_COLOR} from 'icons/Icon'; import Button from 'buttons/Button'; import {shallow} from 'enzyme'; diff --git a/src/components/utils/forms/src/index.ts b/src/components/utils/forms/src/index.ts new file mode 100644 index 0000000000..20678474a5 --- /dev/null +++ b/src/components/utils/forms/src/index.ts @@ -0,0 +1,3 @@ +export * from './lib/types'; +export * from './lib/useBrainlyForm'; +export * from './lib/useBrainlyFormField'; diff --git a/src/components/utils/forms/src/lib/forms.spec.tsx b/src/components/utils/forms/src/lib/forms.spec.tsx new file mode 100644 index 0000000000..bb1e5744f0 --- /dev/null +++ b/src/components/utils/forms/src/lib/forms.spec.tsx @@ -0,0 +1,228 @@ +import * as React from 'react'; +import {useBrainlyFormField} from './useBrainlyFormField'; +import {useBrainlyForm} from './useBrainlyForm'; +import {ValidationRules} from './types'; +import {renderHook} from '@testing-library/react-hooks'; +import {fireEvent, render} from '@testing-library/react'; + +const mockSubmit = jest.fn(); + +const renderForm = ( + submit: any = mockSubmit, + hookProps: any = {defaultValues: undefined, validationStrategy: 'change'}, + validationRules?: ValidationRules +) => { + const formHookResult = renderHook(() => + useBrainlyForm<{input: string}>(hookProps) + ); + + const InputComponent = ({ + validationRules, + }: { + validationRules?: ValidationRules; + }) => { + const {ref, onChange, error} = useBrainlyFormField({ + name: 'input', + validationRules, + }); + + return ( + <> + +
{error}
+ + ); + }; + + const Component = () => ( + + + + + ); + + const renderedInput = render(); + + return {formHookResult, renderedInput}; +}; + +describe('Brainly Forms', () => { + it('renders form correctly and change values on input', () => { + const {renderedInput, formHookResult} = renderForm(); + + const input = renderedInput.getByTestId('input'); + + expect(formHookResult.result.current.getValues()).toEqual({ + input: undefined, + }); + + fireEvent.change(input, {target: {value: 'new value!'}}); + + expect(formHookResult.result.current.getValues()).toEqual({ + input: 'new value!', + }); + }); + + it('renders form correctly and submitting changes', async () => { + const {renderedInput, formHookResult} = renderForm(); + + const input = renderedInput.getByTestId('input'); + + fireEvent.change(input, {target: {value: 'new value!'}}); + + await formHookResult.waitForNextUpdate(); + + expect(formHookResult.result.current.formState).toEqual({ + isDirty: true, + isSubmitted: false, + isValid: true, + isSubmitting: false, + isSubmitSuccessful: false, + errors: {}, + }); + + const submitButton = renderedInput.getByTestId('submit'); + + fireEvent.submit(submitButton); + + await formHookResult.waitForNextUpdate(); + + expect(formHookResult.result.current.formState).toEqual({ + isDirty: true, + isSubmitted: true, + isValid: true, + isSubmitting: false, + isSubmitSuccessful: true, + errors: {}, + }); + }); + + it('renders form correctly and submitting changes with promise Submit', async () => { + const {renderedInput, formHookResult} = renderForm(async (values: any) => { + return Promise.resolve(mockSubmit(values)); + }); + + const input = renderedInput.getByTestId('input'); + + fireEvent.change(input, {target: {value: 'new value!'}}); + + await formHookResult.waitForNextUpdate(); + + const submitButton = renderedInput.getByTestId('submit'); + + fireEvent.submit(submitButton); + + expect(formHookResult.result.current.formState).toEqual({ + isDirty: true, + isSubmitted: false, + isValid: true, + isSubmitting: true, + isSubmitSuccessful: false, + errors: {}, + }); + + await formHookResult.waitForNextUpdate(); + + expect(formHookResult.result.current.formState).toEqual({ + isDirty: true, + isSubmitted: true, + isValid: true, + isSubmitting: false, + isSubmitSuccessful: true, + errors: {}, + }); + }); + + it('renders form correctly with default values', async () => { + const {formHookResult} = renderForm(mockSubmit, { + defaultValues: { + input: 'hello', + }, + }); + + expect(formHookResult.result.current.getValues()).toEqual({ + input: 'hello', + }); + }); + + it('validate required validator', async () => { + const {renderedInput, formHookResult} = renderForm( + mockSubmit, + {}, + { + required: { + message: 'Field is required!', + value: true, + }, + } + ); + + const input = renderedInput.getByTestId('input'); + + const submitButton = renderedInput.getByTestId('submit'); + + fireEvent.submit(submitButton); + + await formHookResult.waitForNextUpdate(); + + const error = renderedInput.getByTestId('error'); + + expect(error.textContent).toEqual('Field is required!'); + + fireEvent.change(input, {target: {value: 'new value!'}}); + + await formHookResult.waitForNextUpdate(); + + const errorAfterRender = renderedInput.getByTestId('error'); + + expect(errorAfterRender.textContent).toEqual(''); + }); + + it.each` + validator | error | valueToFill + ${{min: {message: 'Min value needs to be 10', value: 10}}} | ${'Min value needs to be 10'} | ${8} + ${{max: {message: 'Max value needs to be 10', value: 10}}} | ${'Max value needs to be 10'} | ${12} + ${{minLength: {message: 'MinLength needs to be 10 characters', value: 10}}} | ${'MinLength needs to be 10 characters'} | ${'hello'} + ${{maxLength: {message: 'MaxLength needs to be 10 characters', value: 10}}} | ${'MaxLength needs to be 10 characters'} | ${'some long text over 10char'} + ${{pattern: {message: 'Email is incorrect!', value: /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/}}} | ${'Email is incorrect!'} | ${'incorrectMail.com'} + ${{validate: (value = '') => (value === 'hello' ? 'valid' : 'value needs to contain hello')}} | ${'value needs to contain hello'} | ${'value'} + `('validate $validator', async ({validator, error, valueToFill}) => { + const {renderedInput, formHookResult} = renderForm( + mockSubmit, + {}, + validator + ); + + const input = renderedInput.getByTestId('input'); + + fireEvent.change(input, {target: {value: valueToFill}}); + + const submitButton = renderedInput.getByTestId('submit'); + + fireEvent.submit(submitButton); + + await formHookResult.waitForNextUpdate(); + + const err = renderedInput.getByTestId('error'); + + expect(err.textContent).toEqual(error); + }); + + it('changes form value using setValue', async () => { + const {formHookResult} = renderForm(mockSubmit, { + defaultValues: { + input: 'hello', + }, + }); + + expect(formHookResult.result.current.getValues()).toEqual({ + input: 'hello', + }); + + formHookResult.result.current.setValue('input', 'new value'); + + expect(formHookResult.result.current.getValues()).toEqual({ + input: 'new value', + }); + }); +}); diff --git a/src/components/utils/forms/src/lib/types.ts b/src/components/utils/forms/src/lib/types.ts new file mode 100644 index 0000000000..2cee2387e4 --- /dev/null +++ b/src/components/utils/forms/src/lib/types.ts @@ -0,0 +1,103 @@ +import * as React from 'react'; + +export type FormFields = Record; + +type FormSetValue = ( + name: keyof TFieldValues, + value: TFieldValues[keyof TFieldValues] +) => void; + +export type BrainlyForm = { + Form: (props: FormComponentPropsType) => JSX.Element; + clearErrors: (name?: keyof T | keyof T[]) => void; + reset: (values?: T, options?: Record) => void; + getValues: () => T; + setError: (field: keyof T, message: string) => void; + setValue: FormSetValue; + formState: { + isSubmitting: boolean; + isSubmitted: boolean; + isValid: boolean; + isDirty: boolean; + isSubmitSuccessful: boolean; + errors: Record; + }; + register: ( + name: keyof T, + validationRules?: ValidationRules + ) => { + onChange: (...event: any[]) => void; + onBlur: (...event: any[]) => void; + ref: React.RefCallback; + name: keyof T; + min?: string | number; + max?: string | number; + maxLength?: number; + minLength?: number; + pattern?: string; + required?: boolean; + disabled?: boolean; + }; +}; + +type SetValueOptionsType = { + shouldTouch: boolean; + shouldValidate: boolean; +}; + +export type BrainlyField = { + onChange: (...event: any[]) => void; + onBlur: (...event: any[]) => void; + isTouched: boolean; + isDirty: boolean; + error: string | undefined; + ref: React.RefCallback; + value: string | number | undefined; + setError: (error: string) => void; + setValue: ( + value: string | number | boolean | undefined, + options?: SetValueOptionsType + ) => void; + clearError: () => void; + resetField: () => void; + setFocus: () => void; + formState: { + isSubmitting: boolean; + isSubmitted: boolean; + isValid: boolean; + isDirty: boolean; + }; +}; + +export type FormComponentPropsType = { + onSubmit: (data: T, event?: React.BaseSyntheticEvent) => any | Promise; + children: React.ReactNode; +}; + +export type ValidationRules = { + min?: { + message: string; + value: number; + }; + max?: { + message: string; + value: number; + }; + minLength?: { + message: string; + value: number; + }; + maxLength?: { + message: string; + value: number; + }; + required?: { + message: string; + value: boolean; + }; + pattern?: { + message: string; + value: RegExp; + }; + validate?: (value: string | number) => boolean | string; +}; diff --git a/src/components/utils/forms/src/lib/useBrainlyForm.tsx b/src/components/utils/forms/src/lib/useBrainlyForm.tsx new file mode 100644 index 0000000000..8c7ee50d27 --- /dev/null +++ b/src/components/utils/forms/src/lib/useBrainlyForm.tsx @@ -0,0 +1,143 @@ +import * as React from 'react'; + +import {useForm, FormProvider, UseFormReturn} from 'react-hook-form'; +import { + BrainlyForm, + FormComponentPropsType, + FormFields, + ValidationRules, +} from './types'; + +function createFormComponent(form: UseFormReturn) { + return ({children, onSubmit}: FormComponentPropsType) => ( + +
{children}
+
+ ); +} + +export function useBrainlyForm( + { + defaultValues, + validationStrategy, + delayErrorMs, + }: { + defaultValues?: T; + validationStrategy?: 'onChange' | 'onSubmit' | 'onBlur'; + delayErrorMs?: number; + } = { + defaultValues: undefined, + validationStrategy: 'onChange', + delayErrorMs: 0, + } +): BrainlyForm { + const form = useForm({ + // ignoring typing to do not use types from React hook form directly + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + defaultValues: defaultValues || {}, + mode: validationStrategy || 'onChange', + reValidateMode: validationStrategy || 'onChange', + delayError: delayErrorMs, + }); + + const Form = React.useMemo(() => createFormComponent(form), [form]); + + const { + isDirty, + isSubmitted, + isValid, + isSubmitting, + isSubmitSuccessful, + errors: useFormErrors, + } = React.useMemo(() => form.formState, [form.formState]); + + const setError = React.useCallback( + (field: keyof FormFields, message: string) => { + // ignoring typing to do not use types from React hook form directly + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return form.setError(field, {type: 'custom', message}); + }, + [form.setError] + ); + + const errors = React.useMemo(() => { + if (useFormErrors) { + return Object.keys(useFormErrors).reduce((acc, error) => { + acc[error] = useFormErrors[error]?.message as string | undefined; + return acc; + }, {} as Record); + } + return {}; + }, [useFormErrors]); + + const register = React.useCallback( + (name: keyof T, validationRules?: ValidationRules) => { + const { + onBlur, + onChange, + ref, + name: returnName, + min, + max, + maxLength, + minLength, + pattern, + required, + disabled, + // ignoring typing to do not use types from React hook form directly + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + } = form.register(name, {...(validationRules ? validationRules : {})}); + + return { + onBlur, + onChange, + ref, + name: returnName, + min, + max, + maxLength, + minLength, + pattern, + required, + disabled, + }; + }, + [form.register] + ); + + return { + Form, + register, + formState: { + isDirty, + isSubmitted, + isValid, + isSubmitting, + isSubmitSuccessful, + // ignoring typing to do not use types from React hook form directly + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + errors, + }, + // ignoring typing to do not use types from React hook form directly + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + setError, + // ignoring typing to do not use types from React hook form directly + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + clearErrors: form.clearErrors, + // ignoring typing to do not use types from React hook form directly + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + reset: form.reset, + getValues: form.getValues, + // ignoring typing to do not use types from React hook form directly + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + setValue: form.setValue, + }; +} diff --git a/src/components/utils/forms/src/lib/useBrainlyFormField.ts b/src/components/utils/forms/src/lib/useBrainlyFormField.ts new file mode 100644 index 0000000000..b4f2bc6f44 --- /dev/null +++ b/src/components/utils/forms/src/lib/useBrainlyFormField.ts @@ -0,0 +1,116 @@ +import * as React from 'react'; +import {useController, useFormContext} from 'react-hook-form'; +import {BrainlyField, ValidationRules} from './types'; + +type FieldPropsType = { + name: string; + validationRules?: ValidationRules; +}; + +export function useBrainlyFormField({ + name, + validationRules, +}: FieldPropsType): BrainlyField { + const form = useFormContext(); + const { + field: {onChange, onBlur, ref, value}, + fieldState, + formState: {isSubmitting, isValid, isSubmitted, isDirty: isFormDirty}, + } = useController({ + name, + rules: validationRules, + }); + + const {error, isTouched, isDirty} = React.useMemo( + () => + fieldState || { + error: {message: undefined}, + isTouched: false, + isDirty: false, + }, + [fieldState] + ); + + const clearError = React.useCallback(() => { + if (form) { + form.clearErrors(name); + } else { + console.warn( + 'Form Context not exist, field is not able to handle form state' + ); + } + }, [form, name]); + + const setError = React.useCallback( + (message: string) => { + if (form) { + form.setError(name, {message}); + } else { + console.warn( + 'Form Context not exist, field is not able to handle form state' + ); + } + }, + [form, name] + ); + + const setValue = React.useCallback( + ( + value: string | number | boolean | undefined, + {shouldTouch, shouldValidate} = { + shouldValidate: false, + shouldTouch: false, + } + ) => { + if (form) { + form.setValue(name, value, {shouldTouch, shouldValidate}); + } else { + console.warn( + 'Form Context not exist, field is not able to handle form state' + ); + } + }, + [form, name] + ); + + const setFocus = React.useCallback(() => { + if (form) { + form.setFocus(name); + } else { + console.warn( + 'Form Context not exist, field is not able to handle form state' + ); + } + }, [form, name]); + + const resetField = React.useCallback(() => { + if (form) { + form.resetField(name); + } else { + console.warn( + 'Form Context not exist, field is not able to handle form state' + ); + } + }, [form, name]); + + return { + onChange, + onBlur, + clearError, + setError, + setValue, + setFocus, + error: error?.message, + isDirty, + isTouched, + ref, + value, + resetField, + formState: { + isSubmitting, + isValid, + isSubmitted, + isDirty: isFormDirty, + }, + }; +} diff --git a/yarn.lock b/yarn.lock index 16e6a943fd..c30af70cc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17072,6 +17072,11 @@ react-github-btn@^1.2.0: dependencies: github-buttons "^2.8.0" +react-hook-form@^7.43.1: + version "7.43.1" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.43.1.tgz#0d0d7822f3f7fc05ffc41d5f012b49b90fcfa0f0" + integrity sha512-+s3+s8LLytRMriwwuSqeLStVjRXFGxgjjx2jED7Z+wz1J/88vpxieRQGvJVvzrzVxshZ0BRuocFERb779m2kNg== + react-hot-loader@^4.12.8: version "4.12.21" resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.21.tgz#332e830801fb33024b5a147d6b13417f491eb975" From c70300935ee913cc0b5c8a12b2c9ede6e0be1afe Mon Sep 17 00:00:00 2001 From: Bartosz Adamczyk Date: Mon, 13 Feb 2023 15:48:28 +0100 Subject: [PATCH 03/33] demo prep --- src/components/form/Form.stories.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/form/Form.stories.mdx b/src/components/form/Form.stories.mdx index 5dacaa8035..2952b221d9 100644 --- a/src/components/form/Form.stories.mdx +++ b/src/components/form/Form.stories.mdx @@ -9,7 +9,7 @@ import Input from '../form-elements/Input'; import {ArgsTable, Meta, Story, Canvas} from '@storybook/addon-docs'; import './form.stories.scss'; - + Form @@ -81,7 +81,7 @@ import './form.stories.scss';
Who are you? - + } + > + + By creating an account, you accept the Brainly Terms of Service & + Privacy Policy + + + + + ); +}; diff --git a/src/components/wizard/Wizard.tsx b/src/components/wizard/Wizard.tsx new file mode 100644 index 0000000000..ac0bfbcaba --- /dev/null +++ b/src/components/wizard/Wizard.tsx @@ -0,0 +1,194 @@ +import React from 'react'; +import Button from '../buttons/Button'; +import Icon from '../icons/Icon'; +import Flex from '../flex/Flex'; +import ProgressBar from '../progress-bar/ProgressBar'; +import classNames from 'classnames'; +import Text from '../text/Text'; +import Box from '../box/Box'; +import Headline from '../text/Headline'; +import Counter from '../counters/Counter'; + +/** + * Wizard + * - renders current step + * - renders navigation + * - handle submit event + * - handle next button click event + * - renders next button + * - renders keyboard hint + */ + +type WizardProps = { + onSubmit?(event: SubmitEvent): void; + onNext?(step: number): boolean; + title?: string; +}; + +const WizardContext = React.createContext<{ + currentStep: number; + stepsLength: number; + onSubmit?: React.FormEventHandler; +}>({ + currentStep: 0, + stepsLength: 0, +}); + +const Wizard: React.FunctionComponent = ({ + children, + onSubmit, + onNext, + title, +}) => { + const stepsArray = React.Children.toArray(children).filter(reactNode => { + return ( + (React.isValidElement(reactNode) && reactNode.type === WizardStep) || + !React.isValidElement(reactNode) + ); + }); + + const [currentStep, setCurrentStep] = React.useState(0); + const handleSubmit = React.useCallback( + event => { + event.preventDefault(); + + if (currentStep < stepsArray.length - 1) { + const shouldChangeStep = onNext ? onNext(currentStep) : true; + + if (shouldChangeStep) { + setCurrentStep(currentStep + 1); + } + } else { + onSubmit(event); + } + }, + [onSubmit, currentStep, onNext, stepsArray] + ); + + return ( +
+ + +
+ + + {title} + + + {stepsArray.length > 0 ? ( + + ) : null} +
+
+ {stepsArray.map((step, index, array) => { + return ( + + + {index === 0 ? ( + <> + Almost there. + + Let us know you better + + + ) : null} + {step} + + + ); + })} +
+
+
+
+ ); +}; + +const WizardStep: React.FunctionComponent<{ + title?: string; + description?: string; + submit?: string | React.ReactNode; +}> = ({children, title, description, submit}) => { + const {currentStep, stepsLength, onSubmit} = React.useContext(WizardContext); + let submitElement: React.ReactNode; + + if (submit) { + if (typeof submit === 'string') { + submitElement = ; + } else { + submitElement = submit; + } + } else { + submitElement = ( + + ); + } + + return ( +
+ + {title ? ( + + + + {currentStep + 1} + + + {title} + + ) : null} + {description ? ( + + {description} + + ) : null} + + {children} + + {submitElement} + + + + press  + + Enter + + + + + + + + +
+ ); +}; + +const WizardStepSubmit: React.FunctionComponent<{ + onClick?: () => void; +}> = ({label, onClick}) => { + return ( + + ); +}; + +export {Wizard, WizardStep, WizardStepSubmit}; diff --git a/src/components/wizard/_wizard.scss b/src/components/wizard/_wizard.scss new file mode 100644 index 0000000000..7565084a5f --- /dev/null +++ b/src/components/wizard/_wizard.scss @@ -0,0 +1,40 @@ +.wizard { + height: 100%; + width: 100%; + + &-step { + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 100%; + transform: translateX(-5000px); + + &-content { + width: 600px; + } + + &-counter { + background-color: $black; + } + + &--current { + transform: translateX(0); + } + } + + &-progress-bar { + flex: none; + } + + &-form { + position: relative; + flex: 1; + } + + &-footer { + &__icon { + width: auto; + } + } +} diff --git a/src/sass/main.scss b/src/sass/main.scss index 96ea135a1d..3f310eb1f8 100644 --- a/src/sass/main.scss +++ b/src/sass/main.scss @@ -69,3 +69,4 @@ $sgFontsPath: 'fonts/' !default; @import '../components/form/form'; @import '../components/progress-bar/progress-bar'; @import '../components/sparks/sparks'; +@import '../components/wizard/wizard'; From a06c746d8e647a0fb5ea66fdd55fdb9bf425adfe Mon Sep 17 00:00:00 2001 From: Bartosz Adamczyk Date: Tue, 9 May 2023 13:58:05 +0200 Subject: [PATCH 11/33] implement 3 forms to test flow --- src/components/wizard/Wizard.stories.tsx | 148 ++++++++++++---- src/components/wizard/Wizard.tsx | 207 ++++++++++++----------- src/components/wizard/_wizard.scss | 4 + 3 files changed, 222 insertions(+), 137 deletions(-) diff --git a/src/components/wizard/Wizard.stories.tsx b/src/components/wizard/Wizard.stories.tsx index 7fe3090ee3..6139d89054 100644 --- a/src/components/wizard/Wizard.stories.tsx +++ b/src/components/wizard/Wizard.stories.tsx @@ -1,51 +1,129 @@ import React from 'react'; -import {Wizard, WizardStep} from './Wizard'; +import {Wizard, WizardStepHeader, WizardStepSubmit} from './Wizard'; import Input from '../form-elements/Input'; import Checkbox from '../form-elements/checkbox/Checkbox'; -import Button from '../buttons/Button'; +import Textarea from '../form-elements/Textarea'; +import RadioGroup from '../form-elements/radio/RadioGroup'; +import Radio from '../form-elements/radio/Radio'; +import Dialog from '../dialog/Dialog'; export default { title: 'Components/Wizard', + decorators: [ + Story => ( + + + + ), + ], }; export const Default = () => { - const handleSubmit = e => { - console.log(e); + const onChange = () => null; + const onSubmit = () => { alert('form submitted successfuly'); }; - const handleNext = () => { - return true; + + return ( + + + step 1 title + + next + + + + step 2 title + + + option 1 + option 2 + option 3 + + finish + + + ); +}; + +export const CreateAccount = () => { + const onChange = () => null; + const onSubmit = () => { + alert('form submitted successfuly'); + }; + + return ( + + + Who are you? + + Student + Parent + + next + + + + What email adress can we reach you at? + + + next + + + + By creating an account, you accept the Brainly Terms of Service & + Privacy Policy + + + create account + + + + ); +}; + +export const AskYourQuestion = () => { + const onChange = () => null; + const onSubmit = () => { + alert('form submitted successfuly'); }; return ( -
- - - - - - - - create account} - > - - By creating an account, you accept the Brainly Terms of Service & - Privacy Policy - - - -
+ + + + What can we help you with? + +