From 96c0a191c2c965af31a215333956f68fe5eddde6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aappo=20=C3=85lander?= Date: Wed, 11 Nov 2020 12:30:08 +0200 Subject: [PATCH 01/16] Refactor Expander to remove components level implementation. --- src/components/Expander/Expander.tsx | 242 ------------------ src/components/Expander/ExpanderGroup.tsx | 207 --------------- src/components/index.ts | 1 - src/core/Expander/Expander.baseStyles.tsx | 34 +-- src/core/Expander/Expander.test.tsx | 44 ++-- src/core/Expander/Expander.tsx | 203 ++++++++++----- .../Expander/ExpanderGroup.baseStyles.tsx | 7 + src/core/Expander/ExpanderGroup.tsx | 216 +++++++++------- .../__snapshots__/Expander.test.tsx.snap | 137 ++++------ .../__snapshots__/ExpanderGroup.test.tsx.snap | 195 ++++++-------- .../theme/__snapshots__/tokens.test.tsx.snap | 199 ++++++-------- 11 files changed, 508 insertions(+), 977 deletions(-) delete mode 100644 src/components/Expander/Expander.tsx delete mode 100644 src/components/Expander/ExpanderGroup.tsx diff --git a/src/components/Expander/Expander.tsx b/src/components/Expander/Expander.tsx deleted file mode 100644 index 2d399cb71..000000000 --- a/src/components/Expander/Expander.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import React, { Component, ReactNode, Fragment } from 'react'; -import { default as styled } from 'styled-components'; -import { Button, ButtonProps } from '../Button/Button'; -import { allStates } from '../../utils/css'; -import { HtmlDiv, HtmlDivProps } from '../../reset'; -import classnames from 'classnames'; -import { ExpanderGroupConsumer, ExpanderProviderState } from './ExpanderGroup'; - -const baseClassName = 'fi-expander'; -const openClassName = `${baseClassName}--open`; -const titleClassName = `${baseClassName}_title`; -const titleOpenClassName = `${titleClassName}--open`; -const titleNoTagClassName = `${titleClassName}--no-tag`; -const titleTagClassName = `${baseClassName}_title-tag`; -const contentBaseClassName = `${baseClassName}_content`; -const contentOpenClassName = `${contentBaseClassName}--open`; - -export const StyledDiv = styled((props: HtmlDivProps) => ( - -))` - display: block; - width: 100%; - max-width: 100%; -`; - -interface SharedExpanderProps { - /** Custom classname to extend or customize */ - className?: string; - /** - * Expander element content - */ - children?: ReactNode; -} - -interface StyledExpanderContentProps extends SharedExpanderProps { - openState?: boolean; - hidden: boolean; -} - -export const StyledExpanderContent = styled( - ({ openState, className, ...passProps }: StyledExpanderContentProps) => ( - - ), -)` - display: ${({ openState }) => (!!openState ? 'block' : 'none')}; -`; - -const StyledExpanderTitle = styled( - ({ open, className, ...passProps }: ButtonProps & { open?: boolean }) => ( - + + + ); } } +const StyledExpander = styled( + ({ noPadding, tokens, ...passProps }: ExpanderProps & InternalTokensProp) => { + return ( + + ); + }, +)` + ${(props) => baseStyles(props)}; +`; + +interface ExpanderState { + openState: boolean; +} export class Expander extends Component { static group = (props: ExpanderGroupProps) => { return ; @@ -176,10 +260,15 @@ export class Expander extends Component { render() { return !!this.props.expanderGroup ? ( - {(consumer) => } + {(consumer) => ( + + )} ) : ( - + ); } } diff --git a/src/core/Expander/ExpanderGroup.baseStyles.tsx b/src/core/Expander/ExpanderGroup.baseStyles.tsx index a2e07983a..94313248a 100644 --- a/src/core/Expander/ExpanderGroup.baseStyles.tsx +++ b/src/core/Expander/ExpanderGroup.baseStyles.tsx @@ -8,6 +8,9 @@ export const baseStyles = withSuomifiTheme( ${element({ theme })} display: flex; flex-direction: column; + width: 100%; + max-width: 100%; + & > .fi-expander-group_expanders { flex: none; @@ -19,6 +22,10 @@ export const baseStyles = withSuomifiTheme( transition: margin ${`${theme.transitions.basicTime} ${theme.transitions.basicTimingFunction}`}; + & .fi-icon { + color: ${theme.colors.highlightBase}; + } + &:first-child { border-radius: ${theme.radius.basic} ${theme.radius.basic} 0 0; border-top: none; diff --git a/src/core/Expander/ExpanderGroup.tsx b/src/core/Expander/ExpanderGroup.tsx index b07fa9f82..16a2d84fc 100644 --- a/src/core/Expander/ExpanderGroup.tsx +++ b/src/core/Expander/ExpanderGroup.tsx @@ -1,51 +1,39 @@ -import React from 'react'; +import React, { Component } from 'react'; import { default as styled } from 'styled-components'; -import { Omit } from '../../utils/typescript'; +import classnames from 'classnames'; import { withSuomifiDefaultProps } from '../theme/utils'; import { TokensProp, InternalTokensProp } from '../theme'; -import { - ExpanderGroup as CompExpanderGroup, - ExpanderGroupProps as CompExpanderGroupProps, - ExpanderProviderState as CompExpanderProviderState, - ExpanderGroupState, - OpenExpanders, -} from '../../components/Expander/ExpanderGroup'; -import { ExpanderProps as CompExpanderProps } from '../../components/Expander/Expander'; +import { HtmlDiv } from '../../reset'; +import { ExpanderProps } from './Expander'; import { Button, ButtonProps } from '../Button/Button'; import { baseStyles } from './ExpanderGroup.baseStyles'; -const openAllButtonClassName = 'fi-expander-group_all-button'; +const baseClassName = 'fi-expander-group'; +const openClassName = `${baseClassName}--open`; +const expandersContainerClassName = `${baseClassName}_expanders`; +const openAllButtonClassName = `${baseClassName}_all-button`; -type ButtonOrText = React.ReactElement | string; +type ToggleAllExpanderState = { + toState: boolean; +}; -interface OpenCloseAll { - /** 'Open all'-component (Button) */ - OpenAll: ButtonOrText; - /** 'Close all'-component (Button) */ - CloseAll: ButtonOrText; +interface OpenExpanders { + [key: number]: boolean; } -export interface ExpanderGroupProps - extends Omit, - TokensProp, - OpenCloseAll {} -interface ExpanderOpenAllButtonProps extends ButtonProps, TokensProp { - children: ButtonOrText; +interface ExpanderGroupState { + /** Expanders that are open */ + openExpanders: OpenExpanders; + toggleAllExpanderState: ToggleAllExpanderState; } -const StyledExpanderGroup = styled( - ({ tokens, ...passProps }: CompExpanderGroupProps & InternalTokensProp) => ( - - ), -)` - ${(props) => baseStyles(props)}; -`; +interface OpenAllButtonProps extends ButtonProps { + onClick: (event: React.MouseEvent) => void; + children: React.ReactElement | string; +} -const OpenAllButton = ({ - children, - ...passProps -}: ExpanderOpenAllButtonProps) => { - if (typeof children === 'string' || children.type !== Button) { +const OpenAllButton = ({ children, ...passProps }: OpenAllButtonProps) => { + if (typeof children === 'string' || children?.type !== Button) { return ( {children} @@ -55,36 +43,39 @@ const OpenAllButton = ({ return children; }; -const ExpanderGroupItems = ( - children: Array>, -) => - React.Children.map( - children, - (child: React.ReactElement, index) => { - if (React.isValidElement(child)) { - const isChildOpen = child.props.open; - return React.cloneElement(child, { - index, - expanderGroup: true, - open: isChildOpen, - }); - } - return child; - }, - ); +interface InternalExpanderGroupProps { + /** 'Open all'-component (Button) */ + OpenAll: React.ReactElement | string; + /** 'Close all'-component (Button) */ + CloseAll: React.ReactElement | string; + /** Custom classname to extend or customize */ + className?: string; + /** + * Use Expander's here + */ + children: Array>; + /** Properties for OpenAllButton */ + openAllButtonProps?: ButtonProps; +} + +export interface ExpanderProviderState { + onExpanderOpenChange: (index: number, toState: boolean) => void; + toggleAllExpanderState: ToggleAllExpanderState; +} -const defaultProviderValue: CompExpanderProviderState = { +const defaultProviderValue: ExpanderProviderState = { onExpanderOpenChange: () => null, toggleAllExpanderState: { toState: false, }, }; + const { Provider, Consumer: ExpanderGroupConsumer } = React.createContext( defaultProviderValue, ); const InitialStateOfExpanders = ( - children: Array>, + children: Array>, ) => { const openExpanders: OpenExpanders = {}; @@ -101,11 +92,7 @@ const OpenExpandersCount = (expanders: OpenExpanders) => { return Object.values(expanders).filter((value) => value).length; }; -/** - * - * Used for grouping expanders - */ -export class ExpanderGroup extends React.Component { +class BaseExpanderGroup extends Component { state: ExpanderGroupState = { openExpanders: InitialStateOfExpanders(this.props.children), toggleAllExpanderState: { @@ -126,46 +113,93 @@ export class ExpanderGroup extends React.Component { }); }; + allExpandersOpen = () => { + return ( + this.props.children.length > OpenExpandersCount(this.state.openExpanders) + ); + }; + handleAllToggleClick = () => { - this.setState( - (prevState: ExpanderGroupState, props: ExpanderGroupProps) => { - return { - toState: - props.children.length > OpenExpandersCount(prevState.openExpanders), - }; + this.setState({ + toggleAllExpanderState: { + toState: this.allExpandersOpen(), }, - ); + }); }; render() { - const { toggleAllExpanderState } = this.state; - const { - OpenAll, - CloseAll, - children, - ...passProps - } = withSuomifiDefaultProps(this.props); + const { className, children, OpenAll, CloseAll, ...passProps } = this.props; + const { openExpanders, toggleAllExpanderState } = this.state; + const openExpandersCount = OpenExpandersCount(openExpanders); + const allOpen = openExpandersCount === React.Children.count(children); return ( - 0, + })} > - {OpenAll} - } - CloseAll={ - {CloseAll} - } - onClickAll={this.handleAllToggleClick} - > - {ExpanderGroupItems(children)} - - + + {allOpen ? CloseAll : OpenAll} + + + + {ExpanderGroupItems(children)} + + + + ); + } +} + +const StyledExpanderGroup = styled( + ({ tokens, ...passProps }: ExpanderGroupProps & InternalTokensProp) => ( + + ), +)` + ${(props) => baseStyles(props)}; +`; + +export interface ExpanderGroupProps + extends InternalExpanderGroupProps, + TokensProp {} + +const ExpanderGroupItems = ( + children: Array>, +) => + React.Children.map( + children, + (child: React.ReactElement, index) => { + if (React.isValidElement(child)) { + const isChildOpen = child.props.open; + return React.cloneElement(child, { + index, + expanderGroup: true, + open: isChildOpen, + }); + } + return child; + }, + ); + +/** + * + * Used for grouping expanders + */ +export class ExpanderGroup extends React.Component { + render() { + const { children, ...passProps } = withSuomifiDefaultProps(this.props); + + return ( + + {ExpanderGroupItems(children)} + ); } } diff --git a/src/core/Expander/__snapshots__/Expander.test.tsx.snap b/src/core/Expander/__snapshots__/Expander.test.tsx.snap index 137de9c8d..3d4376a46 100644 --- a/src/core/Expander/__snapshots__/Expander.test.tsx.snap +++ b/src/core/Expander/__snapshots__/Expander.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`calling render with the same component on the same container does not remount 1`] = ` -.c3 { +exports[`Basic expander shoud match snapshot 1`] = ` +.c2 { line-height: 1.15; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; @@ -27,25 +27,25 @@ exports[`calling render with the same component on the same container does not r cursor: pointer; } -.c3:-moz-focusring { +.c2:-moz-focusring { outline: 1px dotted ButtonText; } -.c3::-moz-focus-inner { +.c2::-moz-focus-inner { border-style: none; padding: 0; } -.c3::-webkit-file-upload-button { +.c2::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; } -.c3::-webkit-inner-spin-button { +.c2::-webkit-inner-spin-button { height: auto; } -.c3::-webkit-outer-spin-button { +.c2::-webkit-outer-spin-button { height: auto; } @@ -73,38 +73,20 @@ exports[`calling render with the same component on the same container does not r white-space: normal; } -.c4.fi-button--disabled { - cursor: not-allowed; +.c4 { + display: inline-block; + vertical-align: baseline; } -.c1 { - display: block; - width: 100%; - max-width: 100%; +.c3.fi-button--disabled { + cursor: not-allowed; } -.c7 { +.c5 { display: none; } -.c5, -.c5 * { - cursor: pointer; -} - -.c5:hover, -.c5:active, -.c5:focus, -.c5:focus-within { - cursor: pointer; -} - -.c6 { - display: inline-block; - vertical-align: baseline; -} - -.c2 { +.c1 { color: hsl(0,0%,16%); -webkit-letter-spacing: 0; -moz-letter-spacing: 0; @@ -125,9 +107,12 @@ exports[`calling render with the same component on the same container does not r padding: 0; border-radius: 2px; box-shadow: 0 1px 2px 0 rgba(41,41,41,0.14),0 1px 5px 0 rgba(41,41,41,0.12); + display: block; + width: 100%; + max-width: 100%; } -.c2:before { +.c1:before { content: ''; position: absolute; top: 0; @@ -138,12 +123,12 @@ exports[`calling render with the same component on the same container does not r height: 100%; } -.c2:before { +.c1:before { background-color: hsl(212,63%,98%); opacity: 0; } -.c2 .fi-expander_title { +.c1 .fi-expander_title { color: hsl(0,0%,16%); -webkit-letter-spacing: 0; -moz-letter-spacing: 0; @@ -170,11 +155,11 @@ exports[`calling render with the same component on the same container does not r min-height: 60px; } -.c2 .fi-expander_title:focus { +.c1 .fi-expander_title:focus { outline: 0; } -.c2 .fi-expander_title:focus:after { +.c1 .fi-expander_title:focus:after { content: ''; position: absolute; pointer-events: none; @@ -190,60 +175,24 @@ exports[`calling render with the same component on the same container does not r z-index: 9999; } -.c2 .fi-expander_title--no-tag { +.c1 .fi-expander_title--no-tag { padding: 17px 60px 16px 20px; color: hsl(212,63%,45%); } -.c2 .fi-expander_title-icon { - position: absolute; - height: 20px; - width: 20px; - top: 0; - right: 0; - margin: 20px; -} - -.c2 .fi-expander_title--open .fi-expander_title-icon, -.c2 .fi-expander_title-icon--open { - -webkit-transform: rotate(-180deg); - -ms-transform: rotate(-180deg); - transform: rotate(-180deg); -} - -.c2 .fi-expander_title { - color: hsl(0,0%,16%); - -webkit-letter-spacing: 0; - -moz-letter-spacing: 0; - -ms-letter-spacing: 0; - letter-spacing: 0; - -webkit-text-decoration: none; - text-decoration: none; - word-break: break-word; - overflow-wrap: break-word; - -webkit-font-smoothing: antialiased; - font-family: 'Source Sans Pro','Helvetica Neue','Arial',sans-serif; - font-size: 16px; - line-height: 1.5; - font-weight: 600; - font-size: 14px; - line-height: 20px; - position: relative; - display: block; - width: 100%; - font-size: font-family:'Source Sans Pro','Helvetica Neue','Arial',sans-serif; - font-size: 18px; - line-height: 1.5; - font-weight: 600; - min-height: 60px; +.c1 .fi-expander_title, +.c1 .fi-expander_title * { + cursor: pointer; } -.c2 .fi-expander_title--no-tag { - padding: 17px 60px 16px 20px; - color: hsl(212,63%,45%); +.c1 .fi-expander_title:hover, +.c1 .fi-expander_title:active, +.c1 .fi-expander_title:focus, +.c1 .fi-expander_title:focus-within { + cursor: pointer; } -.c2 .fi-expander_title-icon { +.c1 .fi-expander_title-icon { position: absolute; height: 20px; width: 20px; @@ -252,14 +201,14 @@ exports[`calling render with the same component on the same container does not r margin: 20px; } -.c2 .fi-expander_title--open .fi-expander_title-icon, -.c2 .fi-expander_title-icon--open { +.c1 .fi-expander_title--open .fi-expander_title-icon, +.c1 .fi-expander_title-icon--open { -webkit-transform: rotate(-180deg); -ms-transform: rotate(-180deg); transform: rotate(-180deg); } -.c2 > .fi-expander_content { +.c1 > .fi-expander_content { position: relative; display: block; height: 0; @@ -276,18 +225,18 @@ exports[`calling render with the same component on the same container does not r will-change: transition,height; } -.c2 > .fi-expander_content:not(.fi-expander_content--no-padding) { +.c1 > .fi-expander_content:not(.fi-expander_content--no-padding) { padding: 0 16px; } -.c2 > .fi-expander_content.fi-expander_content--open { +.c1 > .fi-expander_content.fi-expander_content--open { height: 10%; overflow: visible; -webkit-animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; } -.c2 > .fi-expander_content.fi-expander_content--open:not(.fi-expander_content--no-padding) { +.c1 > .fi-expander_content.fi-expander_content--open:not(.fi-expander_content--no-padding) { padding-top: 0; padding-right: 20px; padding-bottom: 20px; @@ -295,12 +244,12 @@ exports[`calling render with the same component on the same container does not r }
diff --git a/src/core/Expander/__snapshots__/ExpanderGroup.test.tsx.snap b/src/core/Expander/__snapshots__/ExpanderGroup.test.tsx.snap index d80dea7ea..b4b671927 100644 --- a/src/core/Expander/__snapshots__/ExpanderGroup.test.tsx.snap +++ b/src/core/Expander/__snapshots__/ExpanderGroup.test.tsx.snap @@ -327,6 +327,7 @@ exports[`Basic expander group should match snapshot 1`] = ` } .c3 > .fi-expander_content { + visibility: hidden; position: relative; display: block; height: 0; @@ -348,6 +349,7 @@ exports[`Basic expander group should match snapshot 1`] = ` } .c3 > .fi-expander_content.fi-expander_content--open { + visibility: visible; height: 10%; overflow: visible; -webkit-animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; @@ -377,10 +379,12 @@ exports[`Basic expander group should match snapshot 1`] = ` class="c0 c3 fi-expander" > @@ -411,10 +417,12 @@ exports[`Basic expander group should match snapshot 1`] = ` class="c0 c3 fi-expander" > diff --git a/src/core/theme/__snapshots__/tokens.test.tsx.snap b/src/core/theme/__snapshots__/tokens.test.tsx.snap index 4f501d0fb..dd3239f3c 100644 --- a/src/core/theme/__snapshots__/tokens.test.tsx.snap +++ b/src/core/theme/__snapshots__/tokens.test.tsx.snap @@ -327,6 +327,7 @@ exports[`snapshot testing 1`] = ` } .c3 > .fi-expander_content { + visibility: hidden; position: relative; display: block; height: 0; @@ -348,6 +349,7 @@ exports[`snapshot testing 1`] = ` } .c3 > .fi-expander_content.fi-expander_content--open { + visibility: visible; height: 10%; overflow: visible; -webkit-animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; @@ -377,9 +379,11 @@ exports[`snapshot testing 1`] = ` class="c0 c3 fi-expander" > @@ -410,9 +416,11 @@ exports[`snapshot testing 1`] = ` class="c0 c3 fi-expander" > @@ -443,9 +453,11 @@ exports[`snapshot testing 1`] = ` class="c0 c3 fi-expander" > diff --git a/src/core/theme/tokens.test.tsx b/src/core/theme/tokens.test.tsx index bc4ed5948..4c5f9cfda 100644 --- a/src/core/theme/tokens.test.tsx +++ b/src/core/theme/tokens.test.tsx @@ -13,13 +13,19 @@ const customColors = { const Test = ( - Test expander content 1 - Test expander content 2 - Test expander content 3 + + Test expander content 1 + + + Test expander content 2 + + + Test expander content 3 + ); From 43b5daf359d6011fe227b94ee7d0871b86bcce5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aappo=20=C3=85lander?= Date: Fri, 20 Nov 2020 17:17:04 +0200 Subject: [PATCH 04/16] Refactor Expander implmentation to use more composition like approach for title and content. --- .styleguidist/styleguidist.sections.js | 5 +- src/core/Expander/Expander.baseStyles.tsx | 108 ------ src/core/Expander/Expander.md | 54 --- src/core/Expander/Expander.tsx | 286 --------------- .../Expander/Expander/Expander.baseStyles.tsx | 28 ++ src/core/Expander/Expander/Expander.md | 80 +++++ .../Expander/{ => Expander}/Expander.test.tsx | 155 ++++----- src/core/Expander/Expander/Expander.tsx | 223 ++++++++++++ .../__snapshots__/Expander.test.tsx.snap | 248 +++++++------ .../ExpanderContent.baseStyles.tsx | 51 +++ .../ExpanderContent/ExpanderContent.tsx | 81 +++++ .../ExpanderGroup.baseStyles.tsx | 6 +- .../{ => ExpanderGroup}/ExpanderGroup.md | 0 .../ExpanderGroup.test.tsx | 153 ++++---- .../{ => ExpanderGroup}/ExpanderGroup.tsx | 15 +- .../__snapshots__/ExpanderGroup.test.tsx.snap | 287 ++++++++------- .../ExpanderTitle.baseStyles.tsx | 77 ++++ .../Expander/ExpanderTitle/ExpanderTitle.tsx | 87 +++++ .../ExpanderTitle/ExpanderTitleCheckbox.tsx | 0 src/core/Expander/index.ts | 13 + .../theme/__snapshots__/tokens.test.tsx.snap | 328 ++++++++++-------- src/core/theme/tokens.test.tsx | 25 +- src/core/theme/utils/mousefocus.ts | 18 +- src/index.tsx | 11 +- 24 files changed, 1322 insertions(+), 1017 deletions(-) delete mode 100644 src/core/Expander/Expander.baseStyles.tsx delete mode 100644 src/core/Expander/Expander.md delete mode 100644 src/core/Expander/Expander.tsx create mode 100644 src/core/Expander/Expander/Expander.baseStyles.tsx create mode 100644 src/core/Expander/Expander/Expander.md rename src/core/Expander/{ => Expander}/Expander.test.tsx (50%) create mode 100644 src/core/Expander/Expander/Expander.tsx rename src/core/Expander/{ => Expander}/__snapshots__/Expander.test.tsx.snap (63%) create mode 100644 src/core/Expander/ExpanderContent/ExpanderContent.baseStyles.tsx create mode 100644 src/core/Expander/ExpanderContent/ExpanderContent.tsx rename src/core/Expander/{ => ExpanderGroup}/ExpanderGroup.baseStyles.tsx (92%) rename src/core/Expander/{ => ExpanderGroup}/ExpanderGroup.md (100%) rename src/core/Expander/{ => ExpanderGroup}/ExpanderGroup.test.tsx (67%) rename src/core/Expander/{ => ExpanderGroup}/ExpanderGroup.tsx (92%) rename src/core/Expander/{ => ExpanderGroup}/__snapshots__/ExpanderGroup.test.tsx.snap (70%) create mode 100644 src/core/Expander/ExpanderTitle/ExpanderTitle.baseStyles.tsx create mode 100644 src/core/Expander/ExpanderTitle/ExpanderTitle.tsx create mode 100644 src/core/Expander/ExpanderTitle/ExpanderTitleCheckbox.tsx create mode 100644 src/core/Expander/index.ts diff --git a/.styleguidist/styleguidist.sections.js b/.styleguidist/styleguidist.sections.js index 135fbbdc5..afbb5581f 100644 --- a/.styleguidist/styleguidist.sections.js +++ b/.styleguidist/styleguidist.sections.js @@ -111,7 +111,10 @@ module.exports = { { name: 'Expander', components: getComponentWithVariants('Expander')([ - 'ExpanderGroup', + 'Expander/Expander', + 'ExpanderGroup/ExpanderGroup', + 'ExpanderTitle/ExpanderTitle', + 'ExpanderContent/ExpanderContent', ]), }, ], diff --git a/src/core/Expander/Expander.baseStyles.tsx b/src/core/Expander/Expander.baseStyles.tsx deleted file mode 100644 index 952ab2a13..000000000 --- a/src/core/Expander/Expander.baseStyles.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { css } from 'styled-components'; -import { withSuomifiTheme, TokensAndTheme } from '../theme'; -import { button, element, font } from '../theme/reset'; -import { absolute, allStates } from '../../utils/css'; -import { padding, absoluteFocus } from '../theme/utils'; - -import { ExpanderProps } from './Expander'; - -/* stylelint-disable no-descending-specificity, no-duplicate-selectors */ -export const baseStyles = withSuomifiTheme( - ({ theme }: TokensAndTheme & Partial) => { - return css` - ${element({ theme })} - ${font({ theme })('bodyText')} - background-color: ${theme.colors.whiteBase}; - ${absolute('before')} - position: relative; - padding: 0; - border-radius: ${theme.radius.basic}; - box-shadow: ${theme.shadows.panelShadow}; - display: block; - width: 100%; - max-width: 100%; - - &:before { - background-color: ${theme.colors.highlightLight4}; - opacity: 0; - } - - & .fi-expander_title { - ${button({ theme })} - position: relative; - display: block; - width: 100%; - font-size: ${theme.typography.bodySemiBold}; - min-height: 60px; - - &:focus { - outline: 0; - &:after { - ${absoluteFocus} - } - } - - &--no-tag { - padding: 17px ${theme.spacing.xxxl} 16px ${theme.spacing.m}; - color: ${theme.colors.highlightBase}; - } - - &, - & * { - cursor: pointer; - } - ${allStates('cursor: pointer;')} - } - - & .fi-expander_title-icon { - position: absolute; - height: 20px; - width: 20px; - top: 0; - right: 0; - margin: ${theme.spacing.m}; - } - & .fi-expander_title--open .fi-expander_title-icon, - & .fi-expander_title-icon--open { - transform: rotate(-180deg); - } - - & > .fi-expander_content { - visibility: hidden; - position: relative; - display: block; - height: 0; - overflow: hidden; - word-break: break-word; - transform: scaleY(0); - transform-origin: top; - transition: all ${`${theme.transitions.basicTime} - ${theme.transitions.basicTimingFunction}`}; - will-change: transition, height; - &:not(.fi-expander_content--no-padding) { - padding: 0 ${theme.spacing.insetXl}; - } - &.fi-expander_content--open { - visibility: visible; - height: 10%; - overflow: visible; - /* This is very robust - cannot animate dynamic height with height-definition */ - animation: fi-expander_content-anim ${theme.transitions.basicTime} - ${theme.transitions.basicTimingFunction} 1 forwards; - &:not(.fi-expander_content--no-padding) { - ${padding({ theme })('0', 'm', 'm', 'm')} - } - } - @keyframes fi-expander_content-anim { - 0% { - height: auto; - transform: scaleY(0); - } - 100% { - transform: scaleY(1); - } - } - } - `; - }, -); diff --git a/src/core/Expander/Expander.md b/src/core/Expander/Expander.md deleted file mode 100644 index bb368aa02..000000000 --- a/src/core/Expander/Expander.md +++ /dev/null @@ -1,54 +0,0 @@ -```jsx -import { Expander } from 'suomifi-ui-components'; - - - Test expander -; -``` - -```jsx -import { Expander } from 'suomifi-ui-components'; - - - Test expander content 1 - Test expander content 2 - Test expander content 3 -; -``` - -## Controlled - -- State for the individual Expanders are stored outside of the component and user has full control. -- Therefore when clicking the individual Expander they are not opened by default, user have to give the logic to change it. -- It's user's responsibility to keep the state stored outside to be updated as Open/Close All is used. -- `defaultOpen` prop will not work when Expander is in controlled state == `open` prop is given. - -```jsx -import { Expander } from 'suomifi-ui-components'; - -const [expanderThreeOpen, setExpanderThreeOpen] = React.useState( - false -); - -<> - - - Test expander content 1 - - - Test expander content 2 - - { - if (window.confirm('Toggle Expander 3')) { - setExpanderThreeOpen(!openState); - } - }} - > - Test expander content 3 - - -; -``` diff --git a/src/core/Expander/Expander.tsx b/src/core/Expander/Expander.tsx deleted file mode 100644 index 2c1d4cce0..000000000 --- a/src/core/Expander/Expander.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import React, { Component, ReactNode, Fragment } from 'react'; -import { default as styled } from 'styled-components'; -import classnames from 'classnames'; -import { withSuomifiDefaultProps } from '../theme/utils'; -import { idGenerator } from '../../utils/uuid'; -import { TokensProp, InternalTokensProp } from '../theme'; -import { HtmlDiv } from '../../reset'; -import { baseStyles } from './Expander.baseStyles'; -import { Icon } from '../Icon/Icon'; -import { Button, ButtonProps } from '../../components/Button/Button'; -import { - ExpanderGroupProps, - ExpanderGroup, - ExpanderGroupConsumer, - ExpanderProviderState, -} from './ExpanderGroup'; - -const baseClassName = 'fi-expander'; -const openClassName = `${baseClassName}--open`; -const titleClassName = `${baseClassName}_title`; -const titleOpenClassName = `${titleClassName}--open`; -const titleNoTagClassName = `${titleClassName}--no-tag`; -const titleTagClassName = `${titleClassName}-tag`; -const iconClassName = `${titleClassName}-icon`; -const iconOpenClassName = `${iconClassName}--open`; -const contentBaseClassName = `${baseClassName}_content`; -const contentOpenClassName = `${contentBaseClassName}--open`; -const noPaddingClassName = `${contentBaseClassName}--no-padding`; - -type ExpanderVariant = 'expander' | 'expanderGroup'; - -interface SharedExpanderProps { - /** Custom classname to extend or customize */ - className?: string; - /** - * Expander element content - */ - children?: ReactNode; - /** - * Id for expander button - */ - id?: string; -} - -interface StyledExpanderContentProps extends SharedExpanderProps { - openState?: boolean; -} - -const StyledExpanderContent = styled( - ({ openState, className, ...passProps }: StyledExpanderContentProps) => ( - - ), -)` - display: ${({ openState }) => (!!openState ? 'block' : 'none')}; -`; - -interface InternalExpanderProps extends SharedExpanderProps { - /** Title for Expander */ - title: ReactNode; - /** Title HTML-tag (h1, h2, h3 etc.) - * @default none - */ - titleTag?: string; - open?: boolean; - /** Properties for title-Button */ - titleProps?: ButtonProps & { open?: boolean }; - /** Properties for the content div */ - contentProps?: SharedExpanderProps; - /** Default status of expander open - * @default false - */ - defaultOpen?: boolean; - /** Event handler to execute when clicked */ - onClick?: ({ openState }: { openState: boolean }) => void; - index?: number; - expanderGroup?: boolean; - consumer?: ExpanderProviderState; -} - -export interface ExpanderProps extends InternalExpanderProps, TokensProp { - /** Remove padding from expandable content area (for background usage with padding in given container etc.) */ - noPadding?: boolean; - /** - * 'expander' | 'expanderGroup' - * @default expander - */ - variant?: ExpanderVariant; -} - -const IfTitleTag = ({ - titleTag, - children, -}: { - titleTag: string | undefined; - children: ReactNode; -}) => ( - - {!!titleTag - ? React.createElement(titleTag, { - children, - className: titleTagClassName, - }) - : children} - -); - -class BaseExpanderItem extends Component { - state: ExpanderState = { - openState: - this.props.defaultOpen !== undefined ? this.props.defaultOpen : false, - }; - - id = idGenerator(this.props.id); - - contentId = `${this.id}_content`; - - componentDidUpdate(prevProps: ExpanderProps, prevState: ExpanderState) { - if ( - !!this.props.consumer && - !!prevProps.consumer && - this.props.consumer.toggleAllExpanderState !== - prevProps.consumer.toggleAllExpanderState - ) { - const { openState } = this.state; - const { - expanderGroup, - index, - consumer: { toggleAllExpanderState }, - open, - } = this.props; - - if ( - !!expanderGroup && - index !== undefined && - ((open === undefined && - !!openState !== toggleAllExpanderState.toState) || - (open !== undefined && !!open !== toggleAllExpanderState.toState)) - ) { - this.handleClick(); - } - } - - const { open } = this.props; - const { openState } = this.state; - const controlled = open !== undefined; - if ( - (prevState.openState !== openState && !controlled) || - (prevProps.open !== open && controlled) - ) { - const { - expanderGroup, - index, - consumer: { onExpanderOpenChange } = { - onExpanderOpenChange: undefined, - }, - } = this.props; - if (!!expanderGroup && !!onExpanderOpenChange && index !== undefined) { - const currentState = controlled ? !!open : openState; - onExpanderOpenChange(index, currentState); - } - } - } - - handleClick = () => { - const { open, onClick } = this.props; - const { openState } = this.state; - const controlled = open !== undefined; - const newOpenState = controlled ? !!open : !openState; - - if (!controlled) { - this.setState({ openState: newOpenState }); - } - if (!!onClick) { - onClick({ openState: newOpenState }); - } - }; - - render() { - const { - id, - open, - defaultOpen, - onClick, - className, - children, - title, - titleTag, - titleProps, - index, - expanderGroup: dissmissExpanderGroup, - consumer, - contentProps: { className: contentClassName, ...contentPassProps } = { - className: undefined, - }, - ...passProps - } = this.props; - const openState = open !== undefined ? !!open : this.state.openState; - - return ( - - - - - - {children} - - - ); - } -} - -const StyledExpander = styled( - ({ noPadding, tokens, ...passProps }: ExpanderProps & InternalTokensProp) => { - return ( - - ); - }, -)` - ${(props) => baseStyles(props)}; -`; - -interface ExpanderState { - openState: boolean; -} -export class Expander extends Component { - static group = (props: ExpanderGroupProps) => { - return ; - }; - - render() { - return !!this.props.expanderGroup ? ( - - {(consumer) => ( - - )} - - ) : ( - - ); - } -} diff --git a/src/core/Expander/Expander/Expander.baseStyles.tsx b/src/core/Expander/Expander/Expander.baseStyles.tsx new file mode 100644 index 000000000..1cbaa0633 --- /dev/null +++ b/src/core/Expander/Expander/Expander.baseStyles.tsx @@ -0,0 +1,28 @@ +import { css } from 'styled-components'; +import { withSuomifiTheme, TokensAndTheme } from '../../theme'; +import { element, font } from '../../theme/reset'; + +import { ExpanderProps } from './Expander'; + +/* stylelint-disable no-descending-specificity, no-duplicate-selectors */ +export const baseStyles = withSuomifiTheme( + ({ theme }: TokensAndTheme & Partial) => { + return css` + ${element({ theme })} + ${font({ theme })('bodyText')} + background-color: ${theme.colors.whiteBase}; + position: relative; + padding: 0; + border-radius: ${theme.radius.basic}; + box-shadow: ${theme.shadows.panelShadow}; + display: block; + width: 100%; + max-width: 100%; + + &:before { + background-color: ${theme.colors.highlightLight4}; + opacity: 0; + } + `; + }, +); diff --git a/src/core/Expander/Expander/Expander.md b/src/core/Expander/Expander/Expander.md new file mode 100644 index 000000000..9f83674c3 --- /dev/null +++ b/src/core/Expander/Expander/Expander.md @@ -0,0 +1,80 @@ +```jsx +import { + Expander, + ExpanderTitle, + ExpanderContent +} from 'suomifi-ui-components'; + + + Test expander + Test expander +; +``` + +```jsx +import { + Expander, + ExpanderGroup, + ExpanderTitle, + ExpanderContent +} from 'suomifi-ui-components'; + + + + Test expander 1 + Test expander content 1 + + + Test expander 2 + Test expander content 2 + + + Test expander 3 + Test expander content 3 + +; +``` + +## Controlled + +- State for the individual Expanders are stored outside of the component and user has full control. +- Therefore when clicking the individual Expander they are not opened by default, user have to give the logic to change it. +- It's user's responsibility to keep the state stored outside to be updated as Open/Close All is used. +- `defaultOpen` prop will not work when Expander is in controlled state == `open` prop is given. + +```jsx +import { + Expander, + ExpanderGroup, + ExpanderTitle, + ExpanderContent +} from 'suomifi-ui-components'; + +const [expanderThreeOpen, setExpanderThreeOpen] = React.useState( + false +); + +<> + + + Test expander 1 + Test expander content 1 + + + Test expander 2 + Test expander content 2 + + { + if (window.confirm('Toggle Expander 3')) { + setExpanderThreeOpen(!openState); + } + }} + > + Test expander 3 + Test expander content 3 + + +; +``` diff --git a/src/core/Expander/Expander.test.tsx b/src/core/Expander/Expander/Expander.test.tsx similarity index 50% rename from src/core/Expander/Expander.test.tsx rename to src/core/Expander/Expander/Expander.test.tsx index 5d7aee4c9..4c2e983a0 100644 --- a/src/core/Expander/Expander.test.tsx +++ b/src/core/Expander/Expander/Expander.test.tsx @@ -1,28 +1,47 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; -import { axeTest } from '../../utils/test/axe'; - +import { axeTest } from '../../../utils/test/axe'; import { Expander, ExpanderProps } from './Expander'; -import { cssFromBaseStyles } from '../utils'; +import { + ExpanderContent, + ExpanderContentProps, + ExpanderTitle, + ExpanderTitleProps, +} from '../'; +import { cssFromBaseStyles } from '../../utils'; import { baseStyles } from './Expander.baseStyles'; -const TestExpanderWithProps = (props: ExpanderProps, content: string) => { - const { title, id = 'test-id', ...passProps } = props; +const TestExpanderWithProps = ( + props: Omit, + titleProps: ExpanderTitleProps, + contentProps: ExpanderContentProps, + testId?: string, +) => { + const { id = 'test-id', ...passProps } = props; return ( - - {content} + + + ); }; const TestExpander = TestExpanderWithProps( { - title: 'Test expander', - titleProps: { 'data-testid': 'expander-title' }, className: 'expander-test', }, - 'Test expander content', + { + children: 'Test expander', + }, + { + children: 'Test expander content', + }, + 'expander-title', ); + describe('Basic expander', () => { it('render with the same component on the same container does not remount', () => { const expanderRenderer = render(TestExpander); @@ -33,11 +52,15 @@ describe('Basic expander', () => { rerender( TestExpanderWithProps( { - title: 'Test expander two', - titleProps: { 'data-testid': 'expander-title-2' }, className: 'expander-test', }, - 'Test expander content', + { + children: 'Test expander two', + }, + { + children: 'Test expander content', + }, + 'expander-title-2', ), ); expect(getByTestId('expander-title-2').textContent).toBe( @@ -55,22 +78,18 @@ describe('Basic expander', () => { describe('defaultOpen', () => { it('gives the classname to expander title and icon', () => { const { getByTestId } = render( - TestExpanderWithProps( - { - title: 'Test expander open by default', - titleProps: { 'data-testid': 'expander-open-by-default-title' }, - className: 'expander-open-by-default-test', - defaultOpen: true, - }, - 'Test expander open by default content', - ), + + + Test expander open by default + + Test expander open by default content + , ); - const button = getByTestId('expander-open-by-default-title'); - expect(button.classList.contains('fi-expander_title--open')).toBe(true); - + const div = getByTestId('expander-open-by-default-title'); + expect(div.classList.contains('fi-expander_title--open')).toBe(true); expect( - button + div .querySelector('svg') ?.classList.contains('fi-expander_title-icon--open'), ).toBe(true); @@ -79,16 +98,12 @@ describe('defaultOpen', () => { it('classnames will be removed when clicked', () => { const mockClickHandler = jest.fn(); const { getByTestId } = render( - TestExpanderWithProps( - { - title: 'Test expander open by default', - titleProps: { 'data-testid': 'expander-open-by-default-title' }, - className: 'expander-open-by-default-test', - defaultOpen: true, - onClick: mockClickHandler, - }, - 'Test expander open by default content', - ), + + + Test expander open by default + + Test expander open by default content + , ); const buttonToClick = getByTestId('expander-open-by-default-title'); @@ -109,45 +124,35 @@ describe('defaultOpen', () => { describe('onClick', () => { it('is called', async () => { const mockClickHandler = jest.fn(); - const { getByTestId } = render( - TestExpanderWithProps( - { - title: 'Test expander onClick testing', - titleProps: { - 'data-testid': 'expander-onclick-testing-title', - }, - onClick: mockClickHandler, - className: 'expander-onclick-test', - }, - 'Test expander click testing content', - ), + const { getByRole } = render( + + Test expander open by default + Test expander open by default content + , ); - const button = getByTestId('expander-onclick-testing-title'); + const button = getByRole('button'); fireEvent.mouseDown(button); expect(mockClickHandler).toHaveBeenCalledTimes(1); }); }); describe('open', () => { + const ControlledExpander = (props?: Partial) => ( + + + Test expander onClick testing + + Test expander click testing content + + ); + it('open-classnames should be found ', async () => { - const { getByTestId } = render( - TestExpanderWithProps( - { - title: 'Test expander onClick testing', - titleProps: { - 'data-testid': 'expander-onclick-testing-title', - }, - className: 'expander-onclick-test', - open: true, - }, - 'Test expander click testing content', - ), - ); - const button = getByTestId('expander-onclick-testing-title'); - expect(button.classList.contains('fi-expander_title--open')).toBe(true); + const { getByTestId } = render(ControlledExpander()); + const div = getByTestId('expander-title-id'); + expect(div.classList.contains('fi-expander_title--open')).toBe(true); expect( - button + div .querySelector('svg') ?.classList.contains('fi-expander_title-icon--open'), ).toBe(true); @@ -155,24 +160,14 @@ describe('open', () => { it('is clicked. Should not change as it is controlled outside', async () => { const mockClickHandler = jest.fn(); - const { getByTestId } = render( - TestExpanderWithProps( - { - title: 'Test expander onClick testing', - titleProps: { - 'data-testid': 'expander-onclick-testing-title', - }, - onClick: mockClickHandler, - className: 'expander-onclick-test', - open: true, - }, - 'Test expander click testing content', - ), + const { getByRole, getByTestId } = render( + ControlledExpander({ onClick: mockClickHandler }), ); - const button = getByTestId('expander-onclick-testing-title'); + const button = getByRole('button'); fireEvent.mouseDown(button); expect(mockClickHandler).toHaveBeenCalledTimes(1); - expect(button.classList.contains('fi-expander_title--open')).toBe(true); + const div = getByTestId('expander-title-id'); + expect(div.classList.contains('fi-expander_title--open')).toBe(true); expect( button diff --git a/src/core/Expander/Expander/Expander.tsx b/src/core/Expander/Expander/Expander.tsx new file mode 100644 index 000000000..32e04870f --- /dev/null +++ b/src/core/Expander/Expander/Expander.tsx @@ -0,0 +1,223 @@ +import React, { Component, ReactNode } from 'react'; +import { default as styled } from 'styled-components'; +import classnames from 'classnames'; +import { withSuomifiDefaultProps } from '../../theme/utils'; +import { idGenerator } from '../../../utils/uuid'; +import { TokensProp, InternalTokensProp } from '../../theme'; +import { HtmlDiv } from '../../../reset'; +import { baseStyles } from './Expander.baseStyles'; +import { + ExpanderGroupConsumer, + ExpanderGroupProviderState, +} from '../ExpanderGroup/ExpanderGroup'; + +const baseClassName = 'fi-expander'; +const openClassName = `${baseClassName}--open`; + +export interface ExpanderProviderState { + onToggleExpander: () => void; + open: boolean; + /** + * Id for expander button + */ + titleId: string | undefined; + /** + * Id for expander content + */ + contentId: string | undefined; +} + +const defaultProviderValue: ExpanderProviderState = { + onToggleExpander: () => null, + open: false, + titleId: undefined, + contentId: undefined, +}; + +const { + Provider: ExpanderProvider, + Consumer: ExpanderConsumer, +} = React.createContext(defaultProviderValue); + +interface InternalExpanderProps { + /** + * Children, extend type ExpanderTitleBaseProps or ExpanderContentBaseProps + * ExpanderProviderState context is used to communicate between title, content and expander + */ + children: ReactNode; + /** Custom classname to extend or customize */ + className?: string; + /** + * Id for expander button + */ + id?: string; + /** Default status of expander open + * @default false + */ + defaultOpen?: boolean; + /* Controlled open property */ + open?: boolean; + /** Event handler to execute when clicked */ + onClick?: ({ openState }: { openState: boolean }) => void; + index?: number; + expanderGroup?: boolean; + consumer?: ExpanderGroupProviderState; +} + +export interface ExpanderProps extends InternalExpanderProps, TokensProp {} + +export interface ExpanderTitleBaseProps { + /** Custom classname to extend or customize */ + className?: string; + /** + * Expander consumer for open state and toggle open callback + */ + consumer: ExpanderProviderState; +} + +export interface ExpanderContentBaseProps { + /** Custom classname to extend or customize */ + className?: string; + /** + * Expander consumer for open state + */ + consumer: ExpanderProviderState; +} + +class BaseExpander extends Component { + state: ExpanderState = { + openState: + this.props.defaultOpen !== undefined ? this.props.defaultOpen : false, + }; + + id = idGenerator(this.props.id); + + contentId = `${this.id}_content`; + + componentDidUpdate(prevProps: ExpanderProps, prevState: ExpanderState) { + if ( + !!this.props.consumer && + !!prevProps.consumer && + this.props.consumer.toggleAllExpanderState !== + prevProps.consumer.toggleAllExpanderState + ) { + const { openState } = this.state; + const { + expanderGroup, + index, + consumer: { toggleAllExpanderState }, + open, + } = this.props; + + if ( + !!expanderGroup && + index !== undefined && + ((open === undefined && + !!openState !== toggleAllExpanderState.toState) || + (open !== undefined && !!open !== toggleAllExpanderState.toState)) + ) { + this.handleClick(); + } + } + + const { open } = this.props; + const { openState } = this.state; + const controlled = open !== undefined; + if ( + (prevState.openState !== openState && !controlled) || + (prevProps.open !== open && controlled) + ) { + const { + expanderGroup, + index, + consumer: { onExpanderOpenChange } = { + onExpanderOpenChange: undefined, + }, + } = this.props; + if (!!expanderGroup && !!onExpanderOpenChange && index !== undefined) { + const currentState = controlled ? !!open : openState; + onExpanderOpenChange(index, currentState); + } + } + } + + handleClick = () => { + const { open, onClick } = this.props; + const { openState } = this.state; + const controlled = open !== undefined; + const newOpenState = controlled ? !!open : !openState; + if (!controlled) { + this.setState({ openState: newOpenState }); + } + if (!!onClick) { + onClick({ openState: newOpenState }); + } + }; + + render() { + const { + id, + open, + defaultOpen, + onClick, + className, + children, + index, + expanderGroup: dissmissExpanderGroup, + consumer, + ...passProps + } = this.props; + const openState = open !== undefined ? !!open : this.state.openState; + + return ( + + + {children} + + + ); + } +} + +const StyledExpander = styled( + ({ tokens, ...passProps }: ExpanderProps & InternalTokensProp) => { + return ; + }, +)` + ${(props) => baseStyles(props)}; +`; + +interface ExpanderState { + openState: boolean; +} + +export class Expander extends Component { + render() { + return !!this.props.expanderGroup ? ( + + {(consumer) => ( + + )} + + ) : ( + + ); + } +} + +export { ExpanderConsumer }; diff --git a/src/core/Expander/__snapshots__/Expander.test.tsx.snap b/src/core/Expander/Expander/__snapshots__/Expander.test.tsx.snap similarity index 63% rename from src/core/Expander/__snapshots__/Expander.test.tsx.snap rename to src/core/Expander/Expander/__snapshots__/Expander.test.tsx.snap index 7c07707c5..682f5b244 100644 --- a/src/core/Expander/__snapshots__/Expander.test.tsx.snap +++ b/src/core/Expander/Expander/__snapshots__/Expander.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Basic expander shoud match snapshot 1`] = ` -.c2 { +.c3 { line-height: 1.15; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; @@ -27,25 +27,25 @@ exports[`Basic expander shoud match snapshot 1`] = ` cursor: pointer; } -.c2:-moz-focusring { +.c3:-moz-focusring { outline: 1px dotted ButtonText; } -.c2::-moz-focus-inner { +.c3::-moz-focus-inner { border-style: none; padding: 0; } -.c2::-webkit-file-upload-button { +.c3::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; } -.c2::-webkit-inner-spin-button { +.c3::-webkit-inner-spin-button { height: auto; } -.c2::-webkit-outer-spin-button { +.c3::-webkit-outer-spin-button { height: auto; } @@ -73,19 +73,6 @@ exports[`Basic expander shoud match snapshot 1`] = ` white-space: normal; } -.c4 { - display: inline-block; - vertical-align: baseline; -} - -.c3.fi-button--disabled { - cursor: not-allowed; -} - -.c5 { - display: none; -} - .c1 { color: hsl(0,0%,16%); -webkit-letter-spacing: 0; @@ -103,7 +90,6 @@ exports[`Basic expander shoud match snapshot 1`] = ` font-weight: 400; background-color: hsl(0,0%,100%); position: relative; - position: relative; padding: 0; border-radius: 2px; box-shadow: 0 1px 2px 0 rgba(41,41,41,0.14),0 1px 5px 0 rgba(41,41,41,0.12); @@ -112,23 +98,82 @@ exports[`Basic expander shoud match snapshot 1`] = ` max-width: 100%; } -.c1:before { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - width: 100%; - height: 100%; -} - .c1:before { background-color: hsl(212,63%,98%); opacity: 0; } -.c1 .fi-expander_title { +.c6 { + color: hsl(0,0%,16%); + -webkit-letter-spacing: 0; + -moz-letter-spacing: 0; + -ms-letter-spacing: 0; + letter-spacing: 0; + -webkit-text-decoration: none; + text-decoration: none; + word-break: break-word; + overflow-wrap: break-word; + -webkit-font-smoothing: antialiased; + font-family: 'Source Sans Pro','Helvetica Neue','Arial',sans-serif; + font-size: 18px; + line-height: 1.5; + font-weight: 400; + background-color: hsl(0,0%,100%); + position: relative; + visibility: hidden; + display: block; + height: 0; + overflow: hidden; + word-break: break-word; + -webkit-transform: scaleY(0); + -ms-transform: scaleY(0); + transform: scaleY(0); + -webkit-transform-origin: top; + -ms-transform-origin: top; + transform-origin: top; + -webkit-transition: all 50ms cubic-bezier(0.28,0.84,0.42,1); + transition: all 50ms cubic-bezier(0.28,0.84,0.42,1); + will-change: transition,height; +} + +.c6:not(.fi-expander_content--no-padding) { + padding: 0 16px; +} + +.c6.fi-expander_content--open { + visibility: visible; + height: 10%; + overflow: visible; + -webkit-animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; + animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; +} + +.c6.fi-expander_content--open:not(.fi-expander_content--no-padding) { + padding-top: 0; + padding-right: 20px; + padding-bottom: 20px; + padding-left: 20px; +} + +.c5 { + display: inline-block; + vertical-align: baseline; +} + +.c4.fi-button--disabled { + cursor: not-allowed; +} + +.c2 { + color: hsl(0,0%,16%); + position: relative; + display: block; + width: 100%; + max-width: 100%; + min-height: 60px; +} + +.c2 .fi-expander_title-button { color: hsl(0,0%,16%); -webkit-letter-spacing: 0; -moz-letter-spacing: 0; @@ -145,21 +190,41 @@ exports[`Basic expander shoud match snapshot 1`] = ` font-weight: 600; font-size: 14px; line-height: 20px; - position: relative; + -webkit-letter-spacing: 0; + -moz-letter-spacing: 0; + -ms-letter-spacing: 0; + letter-spacing: 0; + -webkit-text-decoration: none; + text-decoration: none; + word-break: break-word; + overflow-wrap: break-word; + -webkit-font-smoothing: antialiased; + font-family: 'Source Sans Pro','Helvetica Neue','Arial',sans-serif; + font-size: 18px; + line-height: 1.5; + font-weight: 400; + background-color: hsl(0,0%,100%); + color: hsl(212,63%,45%); display: block; - width: 100%; font-size: font-family:'Source Sans Pro','Helvetica Neue','Arial',sans-serif; font-size: 18px; line-height: 1.5; font-weight: 600; + width: 100%; + max-width: 100%; min-height: 60px; + padding: 17px 60px 16px 20px; } -.c1 .fi-expander_title:focus { +.c2 .fi-expander_title-button:focus { outline: 0; } -.c1 .fi-expander_title:focus:after { +.c2 .fi-expander_title-button:focus-within { + outline: 0; +} + +.c2 .fi-expander_title-button:focus-within:after { content: ''; position: absolute; pointer-events: none; @@ -175,24 +240,19 @@ exports[`Basic expander shoud match snapshot 1`] = ` z-index: 9999; } -.c1 .fi-expander_title--no-tag { - padding: 17px 60px 16px 20px; - color: hsl(212,63%,45%); -} - -.c1 .fi-expander_title, -.c1 .fi-expander_title * { +.c2 .fi-expander_title-button, +.c2 .fi-expander_title-button * { cursor: pointer; } -.c1 .fi-expander_title:hover, -.c1 .fi-expander_title:active, -.c1 .fi-expander_title:focus, -.c1 .fi-expander_title:focus-within { +.c2 .fi-expander_title-button:hover, +.c2 .fi-expander_title-button:active, +.c2 .fi-expander_title-button:focus, +.c2 .fi-expander_title-button:focus-within { cursor: pointer; } -.c1 .fi-expander_title-icon { +.c2 .fi-expander_title-icon { position: absolute; height: 20px; width: 20px; @@ -201,84 +261,50 @@ exports[`Basic expander shoud match snapshot 1`] = ` margin: 20px; } -.c1 .fi-expander_title--open .fi-expander_title-icon, -.c1 .fi-expander_title-icon--open { +.c2 .fi-expander_title--open .fi-expander_title-icon, +.c2 .fi-expander_title-icon--open { -webkit-transform: rotate(-180deg); -ms-transform: rotate(-180deg); transform: rotate(-180deg); } -.c1 > .fi-expander_content { - visibility: hidden; - position: relative; - display: block; - height: 0; - overflow: hidden; - word-break: break-word; - -webkit-transform: scaleY(0); - -ms-transform: scaleY(0); - transform: scaleY(0); - -webkit-transform-origin: top; - -ms-transform-origin: top; - transform-origin: top; - -webkit-transition: all 50ms cubic-bezier(0.28,0.84,0.42,1); - transition: all 50ms cubic-bezier(0.28,0.84,0.42,1); - will-change: transition,height; -} - -.c1 > .fi-expander_content:not(.fi-expander_content--no-padding) { - padding: 0 16px; -} - -.c1 > .fi-expander_content.fi-expander_content--open { - visibility: visible; - height: 10%; - overflow: visible; - -webkit-animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; - animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; -} - -.c1 > .fi-expander_content.fi-expander_content--open:not(.fi-expander_content--no-padding) { - padding-top: 0; - padding-right: 20px; - padding-bottom: 20px; - padding-left: 20px; -} -
- + Test expander + + +
diff --git a/src/core/Expander/ExpanderContent/ExpanderContent.baseStyles.tsx b/src/core/Expander/ExpanderContent/ExpanderContent.baseStyles.tsx new file mode 100644 index 000000000..89e2abcf9 --- /dev/null +++ b/src/core/Expander/ExpanderContent/ExpanderContent.baseStyles.tsx @@ -0,0 +1,51 @@ +import { css } from 'styled-components'; +import { withSuomifiTheme, TokensAndTheme } from '../../theme'; +import { element, font } from '../../theme/reset'; +import { padding } from '../../theme/utils'; + +import { ExpanderContentProps } from './ExpanderContent'; + +/* stylelint-disable no-descending-specificity, no-duplicate-selectors */ +export const baseStyles = withSuomifiTheme( + ({ theme }: TokensAndTheme & Partial) => { + return css` + ${element({ theme })} + ${font({ theme })('bodyText')} + background-color: ${theme.colors.whiteBase}; + position: relative; + visibility: hidden; + display: block; + height: 0; + overflow: hidden; + word-break: break-word; + transform: scaleY(0); + transform-origin: top; + transition: all ${`${theme.transitions.basicTime} + ${theme.transitions.basicTimingFunction}`}; + will-change: transition, height; + &:not(.fi-expander_content--no-padding) { + padding: 0 ${theme.spacing.insetXl}; + } + &.fi-expander_content--open { + visibility: visible; + height: 10%; + overflow: visible; + /* This is very robust - cannot animate dynamic height with height-definition */ + animation: fi-expander_content-anim ${theme.transitions.basicTime} + ${theme.transitions.basicTimingFunction} 1 forwards; + &:not(.fi-expander_content--no-padding) { + ${padding({ theme })('0', 'm', 'm', 'm')} + } + } + @keyframes fi-expander_content-anim { + 0% { + height: auto; + transform: scaleY(0); + } + 100% { + transform: scaleY(1); + } + } + `; + }, +); diff --git a/src/core/Expander/ExpanderContent/ExpanderContent.tsx b/src/core/Expander/ExpanderContent/ExpanderContent.tsx new file mode 100644 index 000000000..8ca75583a --- /dev/null +++ b/src/core/Expander/ExpanderContent/ExpanderContent.tsx @@ -0,0 +1,81 @@ +import React, { Component } from 'react'; +import { default as styled } from 'styled-components'; +import classnames from 'classnames'; +import { withSuomifiDefaultProps } from '../../theme/utils'; +import { TokensProp, InternalTokensProp } from '../../theme'; +import { HtmlDiv, HtmlDivProps } from '../../../reset'; +import { baseStyles } from './ExpanderContent.baseStyles'; +import { + ExpanderConsumer, + ExpanderContentBaseProps, +} from '../Expander/Expander'; + +const baseClassName = 'fi-expander'; +const contentBaseClassName = `${baseClassName}_content`; +const contentOpenClassName = `${contentBaseClassName}--open`; +const noPaddingClassName = `${contentBaseClassName}--no-padding`; + +export interface ExpanderContentProps extends Omit { + /** Remove padding from expandable content area (for background usage with padding in given container etc.) */ + noPadding?: boolean; +} + +interface InternalExpanderContentProps + extends ExpanderContentBaseProps, + ExpanderContentProps {} + +class BaseExpanderContent extends Component { + render() { + const { + children, + title, + className, + noPadding, + consumer, + ...passProps + } = this.props; + return ( + + {children} + + ); + } +} +// display: ${({ openState }) => (!!openState ? 'block' : 'none')}; +const StyledExpanderContent = styled( + ({ + tokens, + className, + ...passProps + }: ExpanderContentProps & InternalTokensProp) => ( + + {(consumer) => ( + + )} + + ), +)` + ${(props) => baseStyles(props)}; +`; + +export class ExpanderContent extends Component< + ExpanderContentProps & TokensProp +> { + render() { + return ; + } +} diff --git a/src/core/Expander/ExpanderGroup.baseStyles.tsx b/src/core/Expander/ExpanderGroup/ExpanderGroup.baseStyles.tsx similarity index 92% rename from src/core/Expander/ExpanderGroup.baseStyles.tsx rename to src/core/Expander/ExpanderGroup/ExpanderGroup.baseStyles.tsx index 79a513484..ce7d663f9 100644 --- a/src/core/Expander/ExpanderGroup.baseStyles.tsx +++ b/src/core/Expander/ExpanderGroup/ExpanderGroup.baseStyles.tsx @@ -1,7 +1,7 @@ import { css } from 'styled-components'; -import { withSuomifiTheme, TokensAndTheme } from '../theme'; -import { element, font } from '../theme/reset'; -import { absoluteFocus } from '../theme/utils'; +import { withSuomifiTheme, TokensAndTheme } from '../../theme'; +import { element, font } from '../../theme/reset'; +import { absoluteFocus } from '../../theme/utils'; export const baseStyles = withSuomifiTheme( ({ theme }: TokensAndTheme) => css` diff --git a/src/core/Expander/ExpanderGroup.md b/src/core/Expander/ExpanderGroup/ExpanderGroup.md similarity index 100% rename from src/core/Expander/ExpanderGroup.md rename to src/core/Expander/ExpanderGroup/ExpanderGroup.md diff --git a/src/core/Expander/ExpanderGroup.test.tsx b/src/core/Expander/ExpanderGroup/ExpanderGroup.test.tsx similarity index 67% rename from src/core/Expander/ExpanderGroup.test.tsx rename to src/core/Expander/ExpanderGroup/ExpanderGroup.test.tsx index 8129be395..2dc24e0f8 100644 --- a/src/core/Expander/ExpanderGroup.test.tsx +++ b/src/core/Expander/ExpanderGroup/ExpanderGroup.test.tsx @@ -1,46 +1,57 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; -import { axeTest } from '../../utils/test/axe'; -import { Expander, ExpanderProps } from './Expander'; +import { axeTest } from '../../../utils/test/axe'; +import { + ExpanderGroup, + Expander, + ExpanderProps, + ExpanderTitle, + ExpanderTitleProps, + ExpanderContent, +} from '../'; const TestExpanderWithProps = ( - props: ExpanderProps, + props: Omit, + titleProps: ExpanderTitleProps & { 'data-testid': string }, content: string, key: number, ) => { - const { title, ...passProps } = props; + const { children: title, ...titlePassProps } = titleProps; return ( - - {content} + + {title} + {content} ); }; const TestExpanderGroup = ( - expanderDatas: { expanderProps: ExpanderProps; content: string }[], + expanderData: { + expanderProps: Omit; + titleProps: ExpanderTitleProps & { 'data-testid': string }; + content: string; + }[], ) => ( - - {expanderDatas.map((d, index) => - TestExpanderWithProps(d.expanderProps, d.content, index), + + {expanderData.map((d, index) => + TestExpanderWithProps(d.expanderProps, d.titleProps, d.content, index), )} - +
); const basicExpanderProps = [ { expanderProps: { - title: 'First', id: 'id-first', - titleProps: { 'data-testid': 'expander-title-1' }, }, + titleProps: { 'data-testid': 'expander-title-1', children: 'First' }, content: 'First content', }, { expanderProps: { - title: 'Second', id: 'id-second', - titleProps: { 'data-testid': 'expander-title-2' }, }, + titleProps: { 'data-testid': 'expander-title-2', children: 'Second' }, content: 'Second content', }, ]; @@ -55,18 +66,19 @@ describe('Basic expander group', () => { TestExpanderGroup([ { expanderProps: { - title: 'First but not the best', id: 'id-first-2', - titleProps: { 'data-testid': 'expander-title-1-1' }, + }, + titleProps: { + 'data-testid': 'expander-title-1-1', + children: 'First but not the best', }, content: 'First but not the content', }, { expanderProps: { - title: 'Second', id: 'id-second-2', - titleProps: { 'data-testid': 'expander-title-2' }, }, + titleProps: { 'data-testid': 'expander-title-2', children: 'Second' }, content: 'Second content', }, ]), @@ -87,18 +99,16 @@ describe('default behaviour', () => { TestExpanderGroup([ { expanderProps: { - title: 'First', id: 'id-first', - titleProps: { 'data-testid': 'expander-title-1' }, }, + titleProps: { 'data-testid': 'expander-title-1', children: 'First' }, content: 'First content', }, { expanderProps: { - title: 'Second', id: 'id-second', - titleProps: { 'data-testid': 'expander-title-2' }, }, + titleProps: { 'data-testid': 'expander-title-2', children: 'Second' }, content: 'Second content', }, ]), @@ -135,19 +145,18 @@ describe('defaultOpen', () => { TestExpanderGroup([ { expanderProps: { - title: 'First', id: 'id-first', - titleProps: { 'data-testid': 'expander-title-1' }, }, + titleProps: { 'data-testid': 'expander-title-1', children: 'First' }, content: 'First content', }, { expanderProps: { - title: 'Second', id: 'id-second', - titleProps: { 'data-testid': 'expander-title-2' }, + defaultOpen: true, }, + titleProps: { 'data-testid': 'expander-title-2', children: 'Second' }, content: 'Second content', }, ]), @@ -168,33 +177,41 @@ describe('defaultOpen', () => { TestExpanderGroup([ { expanderProps: { - title: 'First', id: 'id-first', - titleProps: { 'data-testid': 'expander-title-1' }, + }, + titleProps: { + 'data-testid': 'expander-title-1', + children: 'First', }, content: 'First content', }, { expanderProps: { - title: 'Second', id: 'id-second', - titleProps: { 'data-testid': 'expander-title-2' }, defaultOpen: true, onClick: mockClickHandler, }, + titleProps: { + 'data-testid': 'expander-title-2', + children: 'Second', + toggleButtonProps: { + 'data-testid': 'toggle-button-2', + }, + }, content: 'Second content', }, ]), ); - const buttonToClick = getByTestId('expander-title-2'); - fireEvent.mouseDown(buttonToClick); + const button = getByTestId('toggle-button-2'); + const wrapperDiv = getByTestId('expander-title-2'); + fireEvent.mouseDown(button); - expect(buttonToClick.classList.contains('fi-expander_title--open')).toBe( + expect(wrapperDiv.classList.contains('fi-expander_title--open')).toBe( false, ); expect( - buttonToClick + button .querySelector('svg') ?.classList.contains('fi-expander_title-icon--open'), ).toBe(false); @@ -208,23 +225,27 @@ describe('onClick', () => { TestExpanderGroup([ { expanderProps: { - title: 'First', id: 'id-first', - titleProps: { 'data-testid': 'expander-title-1' }, }, + titleProps: { 'data-testid': 'expander-title-1', children: 'First' }, content: 'First content', }, { expanderProps: { - title: 'Second', - titleProps: { 'data-testid': 'expander-title-2' }, onClick: mockClickHandler, }, + titleProps: { + 'data-testid': 'expander-title-2', + children: 'Second', + toggleButtonProps: { + 'data-testid': 'toggle-button-2', + }, + }, content: 'Second content', }, ]), ); - const button = getByTestId('expander-title-2'); + const button = getByTestId('toggle-button-2'); fireEvent.mouseDown(button); expect(mockClickHandler).toHaveBeenCalledTimes(1); }); @@ -236,29 +257,30 @@ describe('open', () => { TestExpanderGroup([ { expanderProps: { - title: 'First', id: 'id-first', - titleProps: { 'data-testid': 'expander-title-1' }, open: true, }, + titleProps: { 'data-testid': 'expander-title-1', children: 'First' }, content: 'First content', }, { expanderProps: { - title: 'Second', id: 'id-second', - titleProps: { 'data-testid': 'expander-title-2' }, open: true, }, + titleProps: { + 'data-testid': 'expander-title-2', + children: 'Second', + }, content: 'Second content', }, ]), ); - const button = getByTestId('expander-title-2'); - expect(button.classList.contains('fi-expander_title--open')).toBe(true); + const wrapperDiv = getByTestId('expander-title-2'); + expect(wrapperDiv.classList.contains('fi-expander_title--open')).toBe(true); expect( - button + wrapperDiv .querySelector('svg') ?.classList.contains('fi-expander_title-icon--open'), ).toBe(true); @@ -270,31 +292,34 @@ describe('open', () => { TestExpanderGroup([ { expanderProps: { - title: 'First', id: 'id-first', - titleProps: { 'data-testid': 'expander-title-1' }, open: true, }, + titleProps: { 'data-testid': 'expander-title-1', children: 'First' }, content: 'First content', }, { expanderProps: { - title: 'Second', id: 'id-second', - titleProps: { - 'data-testid': 'expander-title-2', - }, onClick: mockClickHandler, open: true, }, + titleProps: { + 'data-testid': 'expander-title-2', + children: 'Second', + toggleButtonProps: { + 'data-testid': 'toggle-button-2', + }, + }, content: 'Second content', }, ]), ); - const button = getByTestId('expander-title-2'); - expect(button.classList.contains('fi-expander_title--open')).toBe(true); + const button = getByTestId('toggle-button-2'); + const wrapperDiv = getByTestId('expander-title-2'); + expect(wrapperDiv.classList.contains('fi-expander_title--open')).toBe(true); expect( - button + wrapperDiv .querySelector('svg') ?.classList.contains('fi-expander_title-icon--open'), ).toBe(true); @@ -302,10 +327,10 @@ describe('open', () => { fireEvent.mouseDown(button); expect(mockClickHandler).toHaveBeenCalledTimes(1); - expect(button.classList.contains('fi-expander_title--open')).toBe(true); + expect(wrapperDiv.classList.contains('fi-expander_title--open')).toBe(true); expect( - button + wrapperDiv .querySelector('svg') ?.classList.contains('fi-expander_title-icon--open'), ).toBe(true); @@ -316,20 +341,18 @@ describe('open', () => { TestExpanderGroup([ { expanderProps: { - title: 'First', id: 'id-first', - titleProps: { 'data-testid': 'expander-title-1' }, open: false, }, + titleProps: { 'data-testid': 'expander-title-1', children: 'First' }, content: 'First content', }, { expanderProps: { - title: 'Second', id: 'id-second', - titleProps: { 'data-testid': 'expander-title-2' }, open: false, }, + titleProps: { 'data-testid': 'expander-title-2', children: 'Second' }, content: 'Second content', }, ]), @@ -358,18 +381,16 @@ test( TestExpanderGroup([ { expanderProps: { - title: 'First', id: 'id-first', - titleProps: { 'data-testid': 'expander-title-1' }, }, + titleProps: { 'data-testid': 'expander-title-1', children: 'First' }, content: 'First content', }, { expanderProps: { - title: 'Second', id: 'id-second', - titleProps: { 'data-testid': 'expander-title-2' }, }, + titleProps: { 'data-testid': 'expander-title-2', children: 'Second' }, content: 'Second content', }, ]), diff --git a/src/core/Expander/ExpanderGroup.tsx b/src/core/Expander/ExpanderGroup/ExpanderGroup.tsx similarity index 92% rename from src/core/Expander/ExpanderGroup.tsx rename to src/core/Expander/ExpanderGroup/ExpanderGroup.tsx index a21d8b91f..ffcf7874d 100644 --- a/src/core/Expander/ExpanderGroup.tsx +++ b/src/core/Expander/ExpanderGroup/ExpanderGroup.tsx @@ -1,11 +1,11 @@ import React, { Component } from 'react'; import { default as styled } from 'styled-components'; import classnames from 'classnames'; -import { withSuomifiDefaultProps } from '../theme/utils'; -import { noMouseFocus } from '../theme/utils/mousefocus'; -import { TokensProp, InternalTokensProp } from '../theme'; -import { HtmlDiv, HtmlButton, HtmlButtonProps } from '../../reset'; -import { ExpanderProps } from './Expander'; +import { withSuomifiDefaultProps } from '../../theme/utils'; +import { noMouseFocus } from '../../theme/utils/mousefocus'; +import { TokensProp, InternalTokensProp } from '../../theme'; +import { HtmlDiv, HtmlButton, HtmlButtonProps } from '../../../reset'; +import { ExpanderProps } from '../Expander/Expander'; import { baseStyles } from './ExpanderGroup.baseStyles'; const baseClassName = 'fi-expander-group'; @@ -42,12 +42,12 @@ interface InternalExpanderGroupProps { openAllButtonProps?: Omit; } -export interface ExpanderProviderState { +export interface ExpanderGroupProviderState { onExpanderOpenChange: (index: number, toState: boolean) => void; toggleAllExpanderState: ToggleAllExpanderState; } -const defaultProviderValue: ExpanderProviderState = { +const defaultProviderValue: ExpanderGroupProviderState = { onExpanderOpenChange: () => null, toggleAllExpanderState: { toState: false, @@ -196,7 +196,6 @@ const ExpanderGroupItems = ( export class ExpanderGroup extends React.Component { render() { const { children, ...passProps } = withSuomifiDefaultProps(this.props); - return ( {ExpanderGroupItems(children)} diff --git a/src/core/Expander/__snapshots__/ExpanderGroup.test.tsx.snap b/src/core/Expander/ExpanderGroup/__snapshots__/ExpanderGroup.test.tsx.snap similarity index 70% rename from src/core/Expander/__snapshots__/ExpanderGroup.test.tsx.snap rename to src/core/Expander/ExpanderGroup/__snapshots__/ExpanderGroup.test.tsx.snap index b4b671927..feadbef41 100644 --- a/src/core/Expander/__snapshots__/ExpanderGroup.test.tsx.snap +++ b/src/core/Expander/ExpanderGroup/__snapshots__/ExpanderGroup.test.tsx.snap @@ -73,15 +73,6 @@ exports[`Basic expander group should match snapshot 1`] = ` white-space: normal; } -.c5 { - display: inline-block; - vertical-align: baseline; -} - -.c4.fi-button--disabled { - cursor: not-allowed; -} - .c1 { color: hsl(0,0%,16%); display: -webkit-box; @@ -200,10 +191,6 @@ exports[`Basic expander group should match snapshot 1`] = ` z-index: 9999; } -.c6 { - display: none; -} - .c3 { color: hsl(0,0%,16%); -webkit-letter-spacing: 0; @@ -221,7 +208,6 @@ exports[`Basic expander group should match snapshot 1`] = ` font-weight: 400; background-color: hsl(0,0%,100%); position: relative; - position: relative; padding: 0; border-radius: 2px; box-shadow: 0 1px 2px 0 rgba(41,41,41,0.14),0 1px 5px 0 rgba(41,41,41,0.12); @@ -230,23 +216,82 @@ exports[`Basic expander group should match snapshot 1`] = ` max-width: 100%; } -.c3:before { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - width: 100%; - height: 100%; -} - .c3:before { background-color: hsl(212,63%,98%); opacity: 0; } -.c3 .fi-expander_title { +.c7 { + color: hsl(0,0%,16%); + -webkit-letter-spacing: 0; + -moz-letter-spacing: 0; + -ms-letter-spacing: 0; + letter-spacing: 0; + -webkit-text-decoration: none; + text-decoration: none; + word-break: break-word; + overflow-wrap: break-word; + -webkit-font-smoothing: antialiased; + font-family: 'Source Sans Pro','Helvetica Neue','Arial',sans-serif; + font-size: 18px; + line-height: 1.5; + font-weight: 400; + background-color: hsl(0,0%,100%); + position: relative; + visibility: hidden; + display: block; + height: 0; + overflow: hidden; + word-break: break-word; + -webkit-transform: scaleY(0); + -ms-transform: scaleY(0); + transform: scaleY(0); + -webkit-transform-origin: top; + -ms-transform-origin: top; + transform-origin: top; + -webkit-transition: all 50ms cubic-bezier(0.28,0.84,0.42,1); + transition: all 50ms cubic-bezier(0.28,0.84,0.42,1); + will-change: transition,height; +} + +.c7:not(.fi-expander_content--no-padding) { + padding: 0 16px; +} + +.c7.fi-expander_content--open { + visibility: visible; + height: 10%; + overflow: visible; + -webkit-animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; + animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; +} + +.c7.fi-expander_content--open:not(.fi-expander_content--no-padding) { + padding-top: 0; + padding-right: 20px; + padding-bottom: 20px; + padding-left: 20px; +} + +.c6 { + display: inline-block; + vertical-align: baseline; +} + +.c5.fi-button--disabled { + cursor: not-allowed; +} + +.c4 { + color: hsl(0,0%,16%); + position: relative; + display: block; + width: 100%; + max-width: 100%; + min-height: 60px; +} + +.c4 .fi-expander_title-button { color: hsl(0,0%,16%); -webkit-letter-spacing: 0; -moz-letter-spacing: 0; @@ -263,21 +308,41 @@ exports[`Basic expander group should match snapshot 1`] = ` font-weight: 600; font-size: 14px; line-height: 20px; - position: relative; + -webkit-letter-spacing: 0; + -moz-letter-spacing: 0; + -ms-letter-spacing: 0; + letter-spacing: 0; + -webkit-text-decoration: none; + text-decoration: none; + word-break: break-word; + overflow-wrap: break-word; + -webkit-font-smoothing: antialiased; + font-family: 'Source Sans Pro','Helvetica Neue','Arial',sans-serif; + font-size: 18px; + line-height: 1.5; + font-weight: 400; + background-color: hsl(0,0%,100%); + color: hsl(212,63%,45%); display: block; - width: 100%; font-size: font-family:'Source Sans Pro','Helvetica Neue','Arial',sans-serif; font-size: 18px; line-height: 1.5; font-weight: 600; + width: 100%; + max-width: 100%; min-height: 60px; + padding: 17px 60px 16px 20px; } -.c3 .fi-expander_title:focus { +.c4 .fi-expander_title-button:focus { outline: 0; } -.c3 .fi-expander_title:focus:after { +.c4 .fi-expander_title-button:focus-within { + outline: 0; +} + +.c4 .fi-expander_title-button:focus-within:after { content: ''; position: absolute; pointer-events: none; @@ -293,24 +358,19 @@ exports[`Basic expander group should match snapshot 1`] = ` z-index: 9999; } -.c3 .fi-expander_title--no-tag { - padding: 17px 60px 16px 20px; - color: hsl(212,63%,45%); -} - -.c3 .fi-expander_title, -.c3 .fi-expander_title * { +.c4 .fi-expander_title-button, +.c4 .fi-expander_title-button * { cursor: pointer; } -.c3 .fi-expander_title:hover, -.c3 .fi-expander_title:active, -.c3 .fi-expander_title:focus, -.c3 .fi-expander_title:focus-within { +.c4 .fi-expander_title-button:hover, +.c4 .fi-expander_title-button:active, +.c4 .fi-expander_title-button:focus, +.c4 .fi-expander_title-button:focus-within { cursor: pointer; } -.c3 .fi-expander_title-icon { +.c4 .fi-expander_title-icon { position: absolute; height: 20px; width: 20px; @@ -319,50 +379,13 @@ exports[`Basic expander group should match snapshot 1`] = ` margin: 20px; } -.c3 .fi-expander_title--open .fi-expander_title-icon, -.c3 .fi-expander_title-icon--open { +.c4 .fi-expander_title--open .fi-expander_title-icon, +.c4 .fi-expander_title-icon--open { -webkit-transform: rotate(-180deg); -ms-transform: rotate(-180deg); transform: rotate(-180deg); } -.c3 > .fi-expander_content { - visibility: hidden; - position: relative; - display: block; - height: 0; - overflow: hidden; - word-break: break-word; - -webkit-transform: scaleY(0); - -ms-transform: scaleY(0); - transform: scaleY(0); - -webkit-transform-origin: top; - -ms-transform-origin: top; - transform-origin: top; - -webkit-transition: all 50ms cubic-bezier(0.28,0.84,0.42,1); - transition: all 50ms cubic-bezier(0.28,0.84,0.42,1); - will-change: transition,height; -} - -.c3 > .fi-expander_content:not(.fi-expander_content--no-padding) { - padding: 0 16px; -} - -.c3 > .fi-expander_content.fi-expander_content--open { - visibility: visible; - height: 10%; - overflow: visible; - -webkit-animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; - animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; -} - -.c3 > .fi-expander_content.fi-expander_content--open:not(.fi-expander_content--no-padding) { - padding-top: 0; - padding-right: 20px; - padding-bottom: 20px; - padding-left: 20px; -} -
@@ -378,37 +401,40 @@ exports[`Basic expander group should match snapshot 1`] = `
- + First + + +
@@ -416,37 +442,40 @@ exports[`Basic expander group should match snapshot 1`] = `
- + Second + + +
diff --git a/src/core/Expander/ExpanderTitle/ExpanderTitle.baseStyles.tsx b/src/core/Expander/ExpanderTitle/ExpanderTitle.baseStyles.tsx new file mode 100644 index 000000000..55757836d --- /dev/null +++ b/src/core/Expander/ExpanderTitle/ExpanderTitle.baseStyles.tsx @@ -0,0 +1,77 @@ +import { css } from 'styled-components'; +import { withSuomifiTheme, TokensAndTheme, SuomifiTheme } from '../../theme'; +import { element, button, font } from '../../theme/reset'; +import { allStates } from '../../../utils/css'; +import { absoluteFocus } from '../../theme/utils'; + +import { ExpanderTitleProps } from './ExpanderTitle'; + +/* stylelint-disable no-descending-specificity, no-duplicate-selectors */ +export const baseStyles = withSuomifiTheme( + ({ theme }: TokensAndTheme & Partial) => { + return css` + ${expanderTitleBaseStyle(theme)} + & .fi-expander_title-button { + ${expanderTitleButtonBaseStyle(theme)} + font-size: ${theme.typography.bodySemiBold}; + width: 100%; + max-width: 100%; + min-height: 60px; + padding: 17px ${theme.spacing.xxxl} 16px ${theme.spacing.m}; + } + ${expanderTitleIconBaseStyle(theme)} + `; + }, +); + +export const expanderTitleBaseStyle = (theme: SuomifiTheme) => { + return css` + ${element({ theme })} + position: relative; + display: block; + width: 100%; + max-width: 100%; + min-height: 60px; + `; +}; + +export const expanderTitleButtonBaseStyle = (theme: SuomifiTheme) => { + return css` + ${button({ theme })} + ${font({ theme })('bodyText')} + background-color: ${theme.colors.whiteBase}; + color: ${theme.colors.highlightBase}; + display: block; + &:focus { + outline: 0; + } + &:focus-within { + outline: 0; + &:after { + ${absoluteFocus} + } + } + &, + & * { + cursor: pointer; + } + ${allStates('cursor: pointer;')} + `; +}; + +export const expanderTitleIconBaseStyle = (theme: SuomifiTheme) => { + return css` + & .fi-expander_title-icon { + position: absolute; + height: 20px; + width: 20px; + top: 0; + right: 0; + margin: ${theme.spacing.m}; + } + & .fi-expander_title--open .fi-expander_title-icon, + & .fi-expander_title-icon--open { + transform: rotate(-180deg); + } + `; +}; diff --git a/src/core/Expander/ExpanderTitle/ExpanderTitle.tsx b/src/core/Expander/ExpanderTitle/ExpanderTitle.tsx new file mode 100644 index 000000000..bc8e0e694 --- /dev/null +++ b/src/core/Expander/ExpanderTitle/ExpanderTitle.tsx @@ -0,0 +1,87 @@ +import React, { Component, ReactNode } from 'react'; +import { default as styled } from 'styled-components'; +import classnames from 'classnames'; +import { withSuomifiDefaultProps } from '../../theme/utils'; +import { TokensProp, InternalTokensProp } from '../../theme'; +import { noMouseFocus } from '../../theme/utils/mousefocus'; +import { HtmlDiv } from '../../../reset'; +import { baseStyles } from './ExpanderTitle.baseStyles'; +import { Icon } from '../../Icon/Icon'; +import { Button, ButtonProps } from '../../../components/Button/Button'; +import { ExpanderConsumer, ExpanderTitleBaseProps } from '../Expander/Expander'; + +const baseClassName = 'fi-expander'; +const titleClassName = `${baseClassName}_title`; +const titleOpenClassName = `${titleClassName}--open`; +const titleButtonClassName = `${titleClassName}-button`; +const iconClassName = `${titleClassName}-icon`; +const iconOpenClassName = `${iconClassName}--open`; + +export interface ExpanderTitleProps { + /** Title for Expander */ + children?: ReactNode; + /** Properties for title open/close toggle button */ + toggleButtonProps?: Omit; +} + +export interface InternalExpanderTitleProps + extends ExpanderTitleProps, + ExpanderTitleBaseProps {} + +class BaseExpanderTitle extends Component { + render() { + const { + children, + className, + toggleButtonProps, + consumer, + ...passProps + } = this.props; + + return ( + + + + ); + } +} + +const StyledExpanderTitle = styled( + ({ tokens, ...passProps }: ExpanderTitleProps & InternalTokensProp) => { + return ( + + {(consumer) => } + + ); + }, +)` + ${(props) => baseStyles(props)}; +`; + +export class ExpanderTitle extends Component { + render() { + return ; + } +} diff --git a/src/core/Expander/ExpanderTitle/ExpanderTitleCheckbox.tsx b/src/core/Expander/ExpanderTitle/ExpanderTitleCheckbox.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/core/Expander/index.ts b/src/core/Expander/index.ts new file mode 100644 index 000000000..258f6413f --- /dev/null +++ b/src/core/Expander/index.ts @@ -0,0 +1,13 @@ +export { Expander, ExpanderProps } from './Expander/Expander'; +export { + ExpanderGroup, + ExpanderGroupProps, +} from './ExpanderGroup/ExpanderGroup'; +export { + ExpanderContent, + ExpanderContentProps, +} from './ExpanderContent/ExpanderContent'; +export { + ExpanderTitle, + ExpanderTitleProps, +} from './ExpanderTitle/ExpanderTitle'; diff --git a/src/core/theme/__snapshots__/tokens.test.tsx.snap b/src/core/theme/__snapshots__/tokens.test.tsx.snap index dd3239f3c..891127708 100644 --- a/src/core/theme/__snapshots__/tokens.test.tsx.snap +++ b/src/core/theme/__snapshots__/tokens.test.tsx.snap @@ -73,12 +73,12 @@ exports[`snapshot testing 1`] = ` white-space: normal; } -.c5 { +.c6 { display: inline-block; vertical-align: baseline; } -.c4.fi-button--disabled { +.c5.fi-button--disabled { cursor: not-allowed; } @@ -200,10 +200,6 @@ exports[`snapshot testing 1`] = ` z-index: 9999; } -.c6 { - display: none; -} - .c3 { color: hsl(0,0%,16%); -webkit-letter-spacing: 0; @@ -221,7 +217,6 @@ exports[`snapshot testing 1`] = ` font-weight: 400; background-color: hsl(0,0%,100%); position: relative; - position: relative; padding: 0; border-radius: 2px; box-shadow: 0 1px 2px 0 rgba(41,41,41,0.14),0 1px 5px 0 rgba(41,41,41,0.12); @@ -230,23 +225,73 @@ exports[`snapshot testing 1`] = ` max-width: 100%; } -.c3:before { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - width: 100%; - height: 100%; -} - .c3:before { background-color: hsl(212,63%,98%); opacity: 0; } -.c3 .fi-expander_title { +.c7 { + color: hsl(0,0%,16%); + -webkit-letter-spacing: 0; + -moz-letter-spacing: 0; + -ms-letter-spacing: 0; + letter-spacing: 0; + -webkit-text-decoration: none; + text-decoration: none; + word-break: break-word; + overflow-wrap: break-word; + -webkit-font-smoothing: antialiased; + font-family: 'Source Sans Pro','Helvetica Neue','Arial',sans-serif; + font-size: 18px; + line-height: 1.5; + font-weight: 400; + background-color: hsl(0,0%,100%); + position: relative; + visibility: hidden; + display: block; + height: 0; + overflow: hidden; + word-break: break-word; + -webkit-transform: scaleY(0); + -ms-transform: scaleY(0); + transform: scaleY(0); + -webkit-transform-origin: top; + -ms-transform-origin: top; + transform-origin: top; + -webkit-transition: all 50ms cubic-bezier(0.28,0.84,0.42,1); + transition: all 50ms cubic-bezier(0.28,0.84,0.42,1); + will-change: transition,height; +} + +.c7:not(.fi-expander_content--no-padding) { + padding: 0 16px; +} + +.c7.fi-expander_content--open { + visibility: visible; + height: 10%; + overflow: visible; + -webkit-animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; + animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; +} + +.c7.fi-expander_content--open:not(.fi-expander_content--no-padding) { + padding-top: 0; + padding-right: 20px; + padding-bottom: 20px; + padding-left: 20px; +} + +.c4 { + color: hsl(0,0%,16%); + position: relative; + display: block; + width: 100%; + max-width: 100%; + min-height: 60px; +} + +.c4 .fi-expander_title-button { color: hsl(0,0%,16%); -webkit-letter-spacing: 0; -moz-letter-spacing: 0; @@ -263,21 +308,41 @@ exports[`snapshot testing 1`] = ` font-weight: 600; font-size: 14px; line-height: 20px; - position: relative; + -webkit-letter-spacing: 0; + -moz-letter-spacing: 0; + -ms-letter-spacing: 0; + letter-spacing: 0; + -webkit-text-decoration: none; + text-decoration: none; + word-break: break-word; + overflow-wrap: break-word; + -webkit-font-smoothing: antialiased; + font-family: 'Source Sans Pro','Helvetica Neue','Arial',sans-serif; + font-size: 18px; + line-height: 1.5; + font-weight: 400; + background-color: hsl(0,0%,100%); + color: hsl(212,63%,45%); display: block; - width: 100%; font-size: font-family:'Source Sans Pro','Helvetica Neue','Arial',sans-serif; font-size: 18px; line-height: 1.5; font-weight: 600; + width: 100%; + max-width: 100%; min-height: 60px; + padding: 17px 60px 16px 20px; } -.c3 .fi-expander_title:focus { +.c4 .fi-expander_title-button:focus { outline: 0; } -.c3 .fi-expander_title:focus:after { +.c4 .fi-expander_title-button:focus-within { + outline: 0; +} + +.c4 .fi-expander_title-button:focus-within:after { content: ''; position: absolute; pointer-events: none; @@ -293,24 +358,19 @@ exports[`snapshot testing 1`] = ` z-index: 9999; } -.c3 .fi-expander_title--no-tag { - padding: 17px 60px 16px 20px; - color: hsl(212,63%,45%); -} - -.c3 .fi-expander_title, -.c3 .fi-expander_title * { +.c4 .fi-expander_title-button, +.c4 .fi-expander_title-button * { cursor: pointer; } -.c3 .fi-expander_title:hover, -.c3 .fi-expander_title:active, -.c3 .fi-expander_title:focus, -.c3 .fi-expander_title:focus-within { +.c4 .fi-expander_title-button:hover, +.c4 .fi-expander_title-button:active, +.c4 .fi-expander_title-button:focus, +.c4 .fi-expander_title-button:focus-within { cursor: pointer; } -.c3 .fi-expander_title-icon { +.c4 .fi-expander_title-icon { position: absolute; height: 20px; width: 20px; @@ -319,50 +379,13 @@ exports[`snapshot testing 1`] = ` margin: 20px; } -.c3 .fi-expander_title--open .fi-expander_title-icon, -.c3 .fi-expander_title-icon--open { +.c4 .fi-expander_title--open .fi-expander_title-icon, +.c4 .fi-expander_title-icon--open { -webkit-transform: rotate(-180deg); -ms-transform: rotate(-180deg); transform: rotate(-180deg); } -.c3 > .fi-expander_content { - visibility: hidden; - position: relative; - display: block; - height: 0; - overflow: hidden; - word-break: break-word; - -webkit-transform: scaleY(0); - -ms-transform: scaleY(0); - transform: scaleY(0); - -webkit-transform-origin: top; - -ms-transform-origin: top; - transform-origin: top; - -webkit-transition: all 50ms cubic-bezier(0.28,0.84,0.42,1); - transition: all 50ms cubic-bezier(0.28,0.84,0.42,1); - will-change: transition,height; -} - -.c3 > .fi-expander_content:not(.fi-expander_content--no-padding) { - padding: 0 16px; -} - -.c3 > .fi-expander_content.fi-expander_content--open { - visibility: visible; - height: 10%; - overflow: visible; - -webkit-animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; - animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; -} - -.c3 > .fi-expander_content.fi-expander_content--open:not(.fi-expander_content--no-padding) { - padding-top: 0; - padding-right: 20px; - padding-bottom: 20px; - padding-left: 20px; -} -
@@ -378,73 +401,71 @@ exports[`snapshot testing 1`] = `
-
- + Test expander 2 + + +
@@ -452,36 +473,39 @@ exports[`snapshot testing 1`] = `
- + Test expander 3 + + +
diff --git a/src/core/theme/tokens.test.tsx b/src/core/theme/tokens.test.tsx index 4c5f9cfda..46f6326f6 100644 --- a/src/core/theme/tokens.test.tsx +++ b/src/core/theme/tokens.test.tsx @@ -3,7 +3,12 @@ import { render } from '@testing-library/react'; import { axeTest } from '../../utils/test/axe'; import { defaultThemeTokens } from './'; -import { Expander } from '../../'; +import { + ExpanderGroup, + Expander, + ExpanderTitle, + ExpanderContent, +} from '../../'; const { colors } = defaultThemeTokens; const customColors = { @@ -12,21 +17,23 @@ const customColors = { }; const Test = ( - - - Test expander content 1 + + Test expander 1 - - Test expander content 2 + + Test expander 2 + Test expander content 2 - - Test expander content 3 + + Test expander 3 + Test expander content 3 - + ); test('snapshot testing', () => { diff --git a/src/core/theme/utils/mousefocus.ts b/src/core/theme/utils/mousefocus.ts index 97121452d..59e386de9 100644 --- a/src/core/theme/utils/mousefocus.ts +++ b/src/core/theme/utils/mousefocus.ts @@ -1,7 +1,7 @@ import { MouseEvent, KeyboardEvent } from 'react'; -export interface NoMouseFocusProps { - callback: (event: MouseEvent | KeyboardEvent) => void; +export interface NoMouseFocusProps { + callback: (event: MouseEvent | KeyboardEvent) => void; } /** @@ -9,24 +9,24 @@ export interface NoMouseFocusProps { * @property {function} onMouseDown handler for onMouseDown * @property {function} onKeyUp handler for onKeyUp */ -export interface NoMouseFocusReturnProps { - onMouseDown: (event: MouseEvent) => void; - onKeyUp: (event: KeyboardEvent) => void; +export interface NoMouseFocusReturnProps { + onMouseDown: (event: MouseEvent) => void; + onKeyUp: (event: KeyboardEvent) => void; } /** Prevent button :focus on mouse use and allow focus only with keyboard * @param {function} callback function to execute on click or on key press * @returns {clickAndKeyboardHandler} */ -export const noMouseFocus = ({ +export const noMouseFocus = ({ callback, -}: NoMouseFocusProps): NoMouseFocusReturnProps => { +}: NoMouseFocusProps): NoMouseFocusReturnProps => { return { - onMouseDown: (event) => { + onMouseDown: (event: MouseEvent) => { event.preventDefault(); callback(event); }, - onKeyUp: (event: any) => { + onKeyUp: (event: KeyboardEvent) => { if (event.key === 'Enter' || event.key === ' ') { callback(event); } diff --git a/src/index.tsx b/src/index.tsx index 3c89bbfd0..b97be3fc2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -51,7 +51,16 @@ export { LanguageMenuLinkLanguage, LanguageMenuLinkLanguageProps, } from './core/LanguageMenu/LanguageMenuItem'; -export { Expander, ExpanderProps } from './core/Expander/Expander'; +export { + Expander, + ExpanderProps, + ExpanderGroup, + ExpanderGroupProps, + ExpanderContent, + ExpanderContentProps, + ExpanderTitle, + ExpanderTitleProps, +} from './core/Expander/'; export { Paragraph, ParagraphProps } from './core/Paragraph/Paragraph'; export { Text, TextProps } from './core/Text/Text'; export { Textarea, TextareaProps } from './core/Form/Textarea/Textarea'; From 8de662e40bfa7f7489d5709287a069692afaa428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aappo=20=C3=85lander?= Date: Wed, 25 Nov 2020 12:42:25 +0200 Subject: [PATCH 05/16] Change ExpanderGroup state to child id based subscription model. --- src/core/Expander/Expander/Expander.md | 15 ++ src/core/Expander/Expander/Expander.tsx | 69 ++++----- .../Expander/ExpanderGroup/ExpanderGroup.tsx | 139 +++++++----------- 3 files changed, 101 insertions(+), 122 deletions(-) diff --git a/src/core/Expander/Expander/Expander.md b/src/core/Expander/Expander/Expander.md index 9f83674c3..1700a19fa 100644 --- a/src/core/Expander/Expander/Expander.md +++ b/src/core/Expander/Expander/Expander.md @@ -54,7 +54,16 @@ const [expanderThreeOpen, setExpanderThreeOpen] = React.useState( false ); +const [showExpander, setShowExpander] = React.useState(false); + <> + Test expander 1 @@ -64,6 +73,12 @@ const [expanderThreeOpen, setExpanderThreeOpen] = React.useState( Test expander 2 Test expander content 2 + {!!showExpander && ( + + Test expander X + Test expander content X + + )} { diff --git a/src/core/Expander/Expander/Expander.tsx b/src/core/Expander/Expander/Expander.tsx index 32e04870f..2543f645e 100644 --- a/src/core/Expander/Expander/Expander.tsx +++ b/src/core/Expander/Expander/Expander.tsx @@ -59,8 +59,6 @@ interface InternalExpanderProps { open?: boolean; /** Event handler to execute when clicked */ onClick?: ({ openState }: { openState: boolean }) => void; - index?: number; - expanderGroup?: boolean; consumer?: ExpanderGroupProviderState; } @@ -94,53 +92,48 @@ class BaseExpander extends Component { contentId = `${this.id}_content`; + constructor(props: InternalExpanderProps) { + super(props); + if (!!props.consumer) { + const defaultOpen = + props.open !== undefined ? props.open : props.defaultOpen || false; + props.consumer.onExpanderOpenChange(this.id, defaultOpen); + } + } + componentDidUpdate(prevProps: ExpanderProps, prevState: ExpanderState) { + const { consumer, open } = this.props; + const controlled = open !== undefined; if ( - !!this.props.consumer && - !!prevProps.consumer && - this.props.consumer.toggleAllExpanderState !== - prevProps.consumer.toggleAllExpanderState + !!consumer && + consumer.toggleAllExpanderState !== + prevProps.consumer?.toggleAllExpanderState ) { - const { openState } = this.state; - const { - expanderGroup, - index, - consumer: { toggleAllExpanderState }, - open, - } = this.props; - if ( - !!expanderGroup && - index !== undefined && - ((open === undefined && - !!openState !== toggleAllExpanderState.toState) || - (open !== undefined && !!open !== toggleAllExpanderState.toState)) + (!controlled && + !!this.state.openState !== consumer.toggleAllExpanderState.toState) || + (controlled && open !== consumer.toggleAllExpanderState.toState) ) { this.handleClick(); } } - - const { open } = this.props; - const { openState } = this.state; - const controlled = open !== undefined; if ( - (prevState.openState !== openState && !controlled) || - (prevProps.open !== open && controlled) + (!controlled && prevState.openState !== this.state.openState) || + (controlled && prevProps.open !== open) ) { - const { - expanderGroup, - index, - consumer: { onExpanderOpenChange } = { - onExpanderOpenChange: undefined, - }, - } = this.props; - if (!!expanderGroup && !!onExpanderOpenChange && index !== undefined) { - const currentState = controlled ? !!open : openState; - onExpanderOpenChange(index, currentState); + if (!!consumer && this.id !== undefined) { + const currentState = controlled ? !!open : this.state.openState; + consumer.onExpanderOpenChange(this.id, currentState); } } } + componentWillUnmount() { + if (!!this.props.consumer && !!this.props.consumer.onExpanderOpenChange) { + this.props.consumer.onExpanderOpenChange(this.id, undefined); + } + } + handleClick = () => { const { open, onClick } = this.props; const { openState } = this.state; @@ -162,8 +155,6 @@ class BaseExpander extends Component { onClick, className, children, - index, - expanderGroup: dissmissExpanderGroup, consumer, ...passProps } = this.props; @@ -205,7 +196,7 @@ interface ExpanderState { export class Expander extends Component { render() { - return !!this.props.expanderGroup ? ( + return ( {(consumer) => ( { /> )} - ) : ( - ); } } diff --git a/src/core/Expander/ExpanderGroup/ExpanderGroup.tsx b/src/core/Expander/ExpanderGroup/ExpanderGroup.tsx index ffcf7874d..2798a87e7 100644 --- a/src/core/Expander/ExpanderGroup/ExpanderGroup.tsx +++ b/src/core/Expander/ExpanderGroup/ExpanderGroup.tsx @@ -4,9 +4,10 @@ import classnames from 'classnames'; import { withSuomifiDefaultProps } from '../../theme/utils'; import { noMouseFocus } from '../../theme/utils/mousefocus'; import { TokensProp, InternalTokensProp } from '../../theme'; -import { HtmlDiv, HtmlButton, HtmlButtonProps } from '../../../reset'; +import { HtmlDiv, HtmlButton, HtmlButtonProps, HtmlSpan } from '../../../reset'; import { ExpanderProps } from '../Expander/Expander'; import { baseStyles } from './ExpanderGroup.baseStyles'; +import { VisuallyHidden } from '../../../components'; const baseClassName = 'fi-expander-group'; const openClassName = `${baseClassName}--open`; @@ -17,33 +18,37 @@ type ToggleAllExpanderState = { toState: boolean; }; -interface OpenExpanders { - [key: number]: boolean; +interface ExpanderOpenState { + [key: string]: boolean; } interface ExpanderGroupState { /** Expanders that are open */ - openExpanders: OpenExpanders; + expanders: ExpanderOpenState; toggleAllExpanderState: ToggleAllExpanderState; } interface InternalExpanderGroupProps { - /** 'Open all'-component (Button) */ + /** + * Use Expander's here + */ + children: Array>; + /** 'Open all' button text */ OpenAllText: string; /** 'Close all'-component (Button) */ CloseAllText: string; /** Custom classname to extend or customize */ className?: string; - /** - * Use Expander's here - */ - children: Array>; + /** 'Open all' button text for screen readers, hides OpenAllText for screen readers if provided */ + AriaOpenAllText?: string; + /** 'Close all' button text for screen readers, hides CloseAllText for screen readers if provided */ + AriaCloseAllText?: string; /** Properties for OpenAllButton, aria-hidden = true by default */ openAllButtonProps?: Omit; } export interface ExpanderGroupProviderState { - onExpanderOpenChange: (index: number, toState: boolean) => void; + onExpanderOpenChange: (id: string, toState: boolean | undefined) => void; toggleAllExpanderState: ToggleAllExpanderState; } @@ -58,55 +63,45 @@ const { Provider, Consumer: ExpanderGroupConsumer } = React.createContext( defaultProviderValue, ); -const InitialStateOfExpanders = ( - children: Array>, -) => { - const openExpanders: OpenExpanders = {}; - - React.Children.forEach(children, (child, index) => { - if (React.isValidElement(child)) { - openExpanders[index] = - child.props.defaultOpen || child.props.open || false; - } - }); - return openExpanders; -}; - -const OpenExpandersCount = (expanders: OpenExpanders) => { - return Object.values(expanders).filter((value) => value).length; -}; - class BaseExpanderGroup extends Component { state: ExpanderGroupState = { - openExpanders: InitialStateOfExpanders(this.props.children), + expanders: {}, toggleAllExpanderState: { - toState: - OpenExpandersCount(InitialStateOfExpanders(this.props.children)) === - this.props.children.length && this.props.children.length > 0, + toState: false, }, }; - handleExpanderOpenChange = (index: number, newState: boolean) => { + handleExpanderOpenChange = (id: string, newState: boolean | undefined) => { this.setState((prevState: ExpanderGroupState) => { - const { openExpanders: prevOpenExpanders } = prevState; - const openExpanders = Object.assign({}, prevOpenExpanders); - openExpanders[index] = newState; - return { - openExpanders, - }; + const { expanders: prevExpanders } = prevState; + const expanders = Object.assign({}, prevExpanders); + if (newState !== undefined) { + expanders[id] = newState; + return { + expanders, + }; + } + delete expanders[id]; + return { expanders }; }); }; - allExpandersOpen = () => { - return ( - this.props.children.length > OpenExpandersCount(this.state.openExpanders) - ); + expandersOpenState = () => { + const expanderCount = Object.keys(this.state.expanders).length; + const openExpanderCount = Object.values(this.state.expanders).filter( + (value) => value === true, + ).length; + return { + expanderCount, + openExpanderCount, + allOpen: expanderCount === openExpanderCount, + }; }; handleAllToggleClick = () => { this.setState({ toggleAllExpanderState: { - toState: this.allExpandersOpen(), + toState: !this.expandersOpenState().allOpen, }, }); }; @@ -116,33 +111,35 @@ class BaseExpanderGroup extends Component { className, children, OpenAllText, + AriaOpenAllText, CloseAllText, - openAllButtonProps: { - 'aria-hidden': openAllAriaHidden, - ...openAllButtonPassProps - } = { - 'aria-hidden': true, - }, + AriaCloseAllText, + openAllButtonProps, ...passProps } = this.props; - const { openExpanders, toggleAllExpanderState } = this.state; - const openExpandersCount = OpenExpandersCount(openExpanders); - const allOpen = openExpandersCount === React.Children.count(children); + const { toggleAllExpanderState } = this.state; + const { openExpanderCount, allOpen } = this.expandersOpenState(); return ( 0, + [openClassName]: openExpanderCount > 0, })} > - {allOpen ? CloseAllText : OpenAllText} + + {allOpen ? CloseAllText : OpenAllText} + + + {allOpen + ? AriaCloseAllText || CloseAllText + : AriaOpenAllText || OpenAllText} + { toggleAllExpanderState, }} > - {ExpanderGroupItems(children)} + {children} @@ -171,36 +168,14 @@ export interface ExpanderGroupProps extends InternalExpanderGroupProps, TokensProp {} -const ExpanderGroupItems = ( - children: Array>, -) => - React.Children.map( - children, - (child: React.ReactElement, index) => { - if (React.isValidElement(child)) { - const isChildOpen = child.props.open; - return React.cloneElement(child, { - index, - expanderGroup: true, - open: isChildOpen, - }); - } - return child; - }, - ); - /** * * Used for grouping expanders */ export class ExpanderGroup extends React.Component { render() { - const { children, ...passProps } = withSuomifiDefaultProps(this.props); - return ( - - {ExpanderGroupItems(children)} - - ); + const { ...passProps } = withSuomifiDefaultProps(this.props); + return ; } } From 139ebe1aa6792cd554af1e3b20e784fb68d27e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aappo=20=C3=85lander?= Date: Wed, 25 Nov 2020 17:24:06 +0200 Subject: [PATCH 06/16] Expander test updates. --- src/core/Expander/Expander/Expander.tsx | 3 +- .../__snapshots__/Expander.test.tsx.snap | 8 +- .../ExpanderGroup/ExpanderGroup.test.tsx | 250 ++++++++++-------- .../__snapshots__/ExpanderGroup.test.tsx.snap | 118 ++++++--- .../theme/__snapshots__/tokens.test.tsx.snap | 120 ++++++--- 5 files changed, 304 insertions(+), 195 deletions(-) diff --git a/src/core/Expander/Expander/Expander.tsx b/src/core/Expander/Expander/Expander.tsx index 2543f645e..174e5d54c 100644 --- a/src/core/Expander/Expander/Expander.tsx +++ b/src/core/Expander/Expander/Expander.tsx @@ -48,7 +48,8 @@ interface InternalExpanderProps { /** Custom classname to extend or customize */ className?: string; /** - * Id for expander button + * Id for expander, must be unique. Duplicate id's break ExpanderGroup functionality. + * Autogenerated if not provided */ id?: string; /** Default status of expander open diff --git a/src/core/Expander/Expander/__snapshots__/Expander.test.tsx.snap b/src/core/Expander/Expander/__snapshots__/Expander.test.tsx.snap index 682f5b244..2f2b91c8d 100644 --- a/src/core/Expander/Expander/__snapshots__/Expander.test.tsx.snap +++ b/src/core/Expander/Expander/__snapshots__/Expander.test.tsx.snap @@ -73,6 +73,10 @@ exports[`Basic expander shoud match snapshot 1`] = ` white-space: normal; } +.c4.fi-button--disabled { + cursor: not-allowed; +} + .c1 { color: hsl(0,0%,16%); -webkit-letter-spacing: 0; @@ -160,10 +164,6 @@ exports[`Basic expander shoud match snapshot 1`] = ` vertical-align: baseline; } -.c4.fi-button--disabled { - cursor: not-allowed; -} - .c2 { color: hsl(0,0%,16%); position: relative; diff --git a/src/core/Expander/ExpanderGroup/ExpanderGroup.test.tsx b/src/core/Expander/ExpanderGroup/ExpanderGroup.test.tsx index 2dc24e0f8..0a1c94360 100644 --- a/src/core/Expander/ExpanderGroup/ExpanderGroup.test.tsx +++ b/src/core/Expander/ExpanderGroup/ExpanderGroup.test.tsx @@ -3,6 +3,7 @@ import { render, fireEvent } from '@testing-library/react'; import { axeTest } from '../../../utils/test/axe'; import { ExpanderGroup, + ExpanderGroupProps, Expander, ExpanderProps, ExpanderTitle, @@ -31,8 +32,15 @@ const TestExpanderGroup = ( titleProps: ExpanderTitleProps & { 'data-testid': string }; content: string; }[], + expanderGroupProps?: Partial, ) => ( - + {expanderData.map((d, index) => TestExpanderWithProps(d.expanderProps, d.titleProps, d.content, index), )} @@ -94,48 +102,60 @@ describe('Basic expander group', () => { }); describe('default behaviour', () => { - it('open/close all should open/close the Expanders', () => { - const { getByTestId, getByText } = render( - TestExpanderGroup([ - { - expanderProps: { - id: 'id-first', - }, - titleProps: { 'data-testid': 'expander-title-1', children: 'First' }, - content: 'First content', + const DefaultGroup = TestExpanderGroup( + [ + { + expanderProps: { + id: 'id-first', }, - { - expanderProps: { - id: 'id-second', - }, - titleProps: { 'data-testid': 'expander-title-2', children: 'Second' }, - content: 'Second content', + titleProps: { 'data-testid': 'expander-title-1', children: 'First' }, + content: 'First content', + }, + { + expanderProps: { + id: 'id-second', }, - ]), + titleProps: { + 'data-testid': 'expander-title-2', + children: 'Second', + toggleButtonProps: { 'data-testid': 'expander-title-2-button' }, + }, + content: 'Second content', + }, + ], + { openAllButtonProps: { 'data-testid': 'open-all-button' } }, + ); + it('open/close all should open/close the Expanders', () => { + const { getByTestId } = render(DefaultGroup); + const titleDiv = getByTestId('expander-title-2'); + const button = getByTestId('expander-title-2-button'); + expect(titleDiv).not.toHaveClass('fi-expander_title--open'); + expect(button.querySelector('svg')).not.toHaveClass( + 'fi-expander_title-icon--open', + ); + fireEvent.mouseDown(button); + expect(titleDiv).toHaveClass('fi-expander_title--open'); + expect(button.querySelector('svg')).toHaveClass( + 'fi-expander_title-icon--open', + ); + fireEvent.mouseDown(button); + expect(titleDiv).not.toHaveClass('fi-expander_title--open'); + expect(button.querySelector('svg')).not.toHaveClass( + 'fi-expander_title-icon--open', ); - const button = getByTestId('expander-title-2'); - expect(button.classList.contains('fi-expander_title--open')).toBe(false); - expect( - button - .querySelector('svg') - ?.classList.contains('fi-expander_title-icon--open'), - ).toBe(false); - const allOpenButton = getByText('Open all'); - fireEvent.mouseDown(allOpenButton); - expect(button.classList.contains('fi-expander_title--open')).toBe(true); - expect( - button - .querySelector('svg') - ?.classList.contains('fi-expander_title-icon--open'), - ).toBe(true); - const allCloseButton = getByText('Close all'); - fireEvent.mouseDown(allCloseButton); - expect(button.classList.contains('fi-expander_title--open')).toBe(false); - expect( - button - .querySelector('svg') - ?.classList.contains('fi-expander_title-icon--open'), - ).toBe(false); + }); + it('open/close all should have providedTexts and screen reader texts', async () => { + const { getByTestId, getByText } = render(DefaultGroup); + const buttonVisibleText = getByText('Open all'); + expect(buttonVisibleText).toHaveAttribute('aria-hidden', 'true'); + const buttonAriaText = getByText('Open all expanders'); + expect(buttonAriaText).toHaveClass('fi-visually-hidden'); + const openAllbutton = getByTestId('open-all-button'); + fireEvent.mouseDown(openAllbutton); + const buttonVisibleCloseText = getByText('Close all'); + expect(buttonVisibleCloseText).toHaveAttribute('aria-hidden', 'true'); + const buttonAriaCloseText = getByText('Close all expanders'); + expect(buttonAriaCloseText).toHaveClass('fi-visually-hidden'); }); }); @@ -161,14 +181,11 @@ describe('defaultOpen', () => { }, ]), ); - const button = getByTestId('expander-title-2'); - expect(button.classList.contains('fi-expander_title--open')).toBe(true); - - expect( - button - .querySelector('svg') - ?.classList.contains('fi-expander_title-icon--open'), - ).toBe(true); + const titleDiv = getByTestId('expander-title-2'); + expect(titleDiv).toHaveClass('fi-expander_title--open'); + expect(titleDiv.querySelector('svg')).toHaveClass( + 'fi-expander_title-icon--open', + ); }); it('classnames will be removed when clicked', () => { @@ -195,26 +212,20 @@ describe('defaultOpen', () => { 'data-testid': 'expander-title-2', children: 'Second', toggleButtonProps: { - 'data-testid': 'toggle-button-2', + 'data-testid': 'expander-title-2-button', }, }, content: 'Second content', }, ]), ); - const button = getByTestId('toggle-button-2'); - const wrapperDiv = getByTestId('expander-title-2'); + const button = getByTestId('expander-title-2-button'); + const titleDiv = getByTestId('expander-title-2'); + expect(titleDiv).toHaveClass('fi-expander_title--open'); fireEvent.mouseDown(button); - - expect(wrapperDiv.classList.contains('fi-expander_title--open')).toBe( - false, + expect(button.querySelector('svg')).not.toHaveClass( + 'fi-expander_title-icon--open', ); - - expect( - button - .querySelector('svg') - ?.classList.contains('fi-expander_title-icon--open'), - ).toBe(false); }); }); @@ -238,14 +249,14 @@ describe('onClick', () => { 'data-testid': 'expander-title-2', children: 'Second', toggleButtonProps: { - 'data-testid': 'toggle-button-2', + 'data-testid': 'expander-title-2-button', }, }, content: 'Second content', }, ]), ); - const button = getByTestId('toggle-button-2'); + const button = getByTestId('expander-title-2-button'); fireEvent.mouseDown(button); expect(mockClickHandler).toHaveBeenCalledTimes(1); }); @@ -276,14 +287,12 @@ describe('open', () => { }, ]), ); - const wrapperDiv = getByTestId('expander-title-2'); - expect(wrapperDiv.classList.contains('fi-expander_title--open')).toBe(true); + const titleDiv = getByTestId('expander-title-2'); + expect(titleDiv).toHaveClass('fi-expander_title--open'); - expect( - wrapperDiv - .querySelector('svg') - ?.classList.contains('fi-expander_title-icon--open'), - ).toBe(true); + expect(titleDiv.querySelector('svg')).toHaveClass( + 'fi-expander_title-icon--open', + ); }); it('is clicked. Should not change as it is controlled outside', async () => { @@ -308,70 +317,77 @@ describe('open', () => { 'data-testid': 'expander-title-2', children: 'Second', toggleButtonProps: { - 'data-testid': 'toggle-button-2', + 'data-testid': 'expander-title-2-button', }, }, content: 'Second content', }, ]), ); - const button = getByTestId('toggle-button-2'); - const wrapperDiv = getByTestId('expander-title-2'); - expect(wrapperDiv.classList.contains('fi-expander_title--open')).toBe(true); - expect( - wrapperDiv - .querySelector('svg') - ?.classList.contains('fi-expander_title-icon--open'), - ).toBe(true); - + const button = getByTestId('expander-title-2-button'); + const titleDiv = getByTestId('expander-title-2'); + expect(titleDiv).toHaveClass('fi-expander_title--open'); + expect(titleDiv.querySelector('svg')).toHaveClass( + 'fi-expander_title-icon--open', + ); fireEvent.mouseDown(button); - expect(mockClickHandler).toHaveBeenCalledTimes(1); - expect(wrapperDiv.classList.contains('fi-expander_title--open')).toBe(true); - - expect( - wrapperDiv - .querySelector('svg') - ?.classList.contains('fi-expander_title-icon--open'), - ).toBe(true); + expect(titleDiv).toHaveClass('fi-expander_title--open'); + expect(titleDiv.querySelector('svg')).toHaveClass( + 'fi-expander_title-icon--open', + ); }); it('open/close all clicked should not force the state to change.', () => { - const { getByTestId, getByText } = render( - TestExpanderGroup([ - { - expanderProps: { - id: 'id-first', - open: false, + const { getByTestId } = render( + TestExpanderGroup( + [ + { + expanderProps: { + id: 'id-first', + open: false, + }, + titleProps: { + 'data-testid': 'expander-title-1', + children: 'First', + }, + content: 'First content', }, - titleProps: { 'data-testid': 'expander-title-1', children: 'First' }, - content: 'First content', - }, + { + expanderProps: { + id: 'id-second', + open: false, + }, + titleProps: { + 'data-testid': 'expander-title-2', + children: 'Second', + toggleButtonProps: { + 'data-testid': 'expander-title-2-button', + }, + }, + content: 'Second content', + }, + ], { - expanderProps: { - id: 'id-second', - open: false, + openAllButtonProps: { + 'data-testid': 'open-all-button', }, - titleProps: { 'data-testid': 'expander-title-2', children: 'Second' }, - content: 'Second content', }, - ]), + ), + ); + const titleDiv = getByTestId('expander-title-2'); + const button = getByTestId('expander-title-2-button'); + expect(titleDiv).not.toHaveClass('fi-expander_title--open'); + expect(button.querySelector('svg')).not.toHaveClass( + 'fi-expander_title-icon--open', + ); + const openAllButton = getByTestId('open-all-button'); + + fireEvent.click(openAllButton); + expect(titleDiv).not.toHaveClass('fi-expander_title--open'); + expect(button.querySelector('svg')).not.toHaveClass( + 'fi-expander_title-icon--open', ); - const button = getByTestId('expander-title-2'); - expect(button.classList.contains('fi-expander_title--open')).toBe(false); - expect( - button - .querySelector('svg') - ?.classList.contains('fi-expander_title-icon--open'), - ).toBe(false); - const allOpenButton = getByText('Open all'); - fireEvent.click(allOpenButton); - expect(button.classList.contains('fi-expander_title--open')).toBe(false); - expect( - button - .querySelector('svg') - ?.classList.contains('fi-expander_title-icon--open'), - ).toBe(false); }); }); diff --git a/src/core/Expander/ExpanderGroup/__snapshots__/ExpanderGroup.test.tsx.snap b/src/core/Expander/ExpanderGroup/__snapshots__/ExpanderGroup.test.tsx.snap index feadbef41..f68119d26 100644 --- a/src/core/Expander/ExpanderGroup/__snapshots__/ExpanderGroup.test.tsx.snap +++ b/src/core/Expander/ExpanderGroup/__snapshots__/ExpanderGroup.test.tsx.snap @@ -73,6 +73,46 @@ exports[`Basic expander group should match snapshot 1`] = ` white-space: normal; } +.c3 { + line-height: 1.15; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + margin: 0; + padding: 0; + border: 0; + box-sizing: border-box; + font: 100% inherit; + line-height: 1; + text-align: left; + -webkit-text-decoration: none; + text-decoration: none; + vertical-align: baseline; + color: inherit; + background: none; + cursor: inherit; + display: inline; + max-width: 100%; + word-wrap: normal; + word-break: normal; + white-space: normal; +} + +.c4 { + position: absolute; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + border: 0; + overflow: hidden; +} + +.c7.fi-button--disabled { + cursor: not-allowed; +} + .c1 { color: hsl(0,0%,16%); display: -webkit-box; @@ -191,7 +231,7 @@ exports[`Basic expander group should match snapshot 1`] = ` z-index: 9999; } -.c3 { +.c5 { color: hsl(0,0%,16%); -webkit-letter-spacing: 0; -moz-letter-spacing: 0; @@ -216,12 +256,12 @@ exports[`Basic expander group should match snapshot 1`] = ` max-width: 100%; } -.c3:before { +.c5:before { background-color: hsl(212,63%,98%); opacity: 0; } -.c7 { +.c9 { color: hsl(0,0%,16%); -webkit-letter-spacing: 0; -moz-letter-spacing: 0; @@ -254,11 +294,11 @@ exports[`Basic expander group should match snapshot 1`] = ` will-change: transition,height; } -.c7:not(.fi-expander_content--no-padding) { +.c9:not(.fi-expander_content--no-padding) { padding: 0 16px; } -.c7.fi-expander_content--open { +.c9.fi-expander_content--open { visibility: visible; height: 10%; overflow: visible; @@ -266,23 +306,19 @@ exports[`Basic expander group should match snapshot 1`] = ` animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; } -.c7.fi-expander_content--open:not(.fi-expander_content--no-padding) { +.c9.fi-expander_content--open:not(.fi-expander_content--no-padding) { padding-top: 0; padding-right: 20px; padding-bottom: 20px; padding-left: 20px; } -.c6 { +.c8 { display: inline-block; vertical-align: baseline; } -.c5.fi-button--disabled { - cursor: not-allowed; -} - -.c4 { +.c6 { color: hsl(0,0%,16%); position: relative; display: block; @@ -291,7 +327,7 @@ exports[`Basic expander group should match snapshot 1`] = ` min-height: 60px; } -.c4 .fi-expander_title-button { +.c6 .fi-expander_title-button { color: hsl(0,0%,16%); -webkit-letter-spacing: 0; -moz-letter-spacing: 0; @@ -334,15 +370,15 @@ exports[`Basic expander group should match snapshot 1`] = ` padding: 17px 60px 16px 20px; } -.c4 .fi-expander_title-button:focus { +.c6 .fi-expander_title-button:focus { outline: 0; } -.c4 .fi-expander_title-button:focus-within { +.c6 .fi-expander_title-button:focus-within { outline: 0; } -.c4 .fi-expander_title-button:focus-within:after { +.c6 .fi-expander_title-button:focus-within:after { content: ''; position: absolute; pointer-events: none; @@ -358,19 +394,19 @@ exports[`Basic expander group should match snapshot 1`] = ` z-index: 9999; } -.c4 .fi-expander_title-button, -.c4 .fi-expander_title-button * { +.c6 .fi-expander_title-button, +.c6 .fi-expander_title-button * { cursor: pointer; } -.c4 .fi-expander_title-button:hover, -.c4 .fi-expander_title-button:active, -.c4 .fi-expander_title-button:focus, -.c4 .fi-expander_title-button:focus-within { +.c6 .fi-expander_title-button:hover, +.c6 .fi-expander_title-button:active, +.c6 .fi-expander_title-button:focus, +.c6 .fi-expander_title-button:focus-within { cursor: pointer; } -.c4 .fi-expander_title-icon { +.c6 .fi-expander_title-icon { position: absolute; height: 20px; width: 20px; @@ -379,8 +415,8 @@ exports[`Basic expander group should match snapshot 1`] = ` margin: 20px; } -.c4 .fi-expander_title--open .fi-expander_title-icon, -.c4 .fi-expander_title-icon--open { +.c6 .fi-expander_title--open .fi-expander_title-icon, +.c6 .fi-expander_title-icon--open { -webkit-transform: rotate(-180deg); -ms-transform: rotate(-180deg); transform: rotate(-180deg); @@ -393,23 +429,33 @@ exports[`Basic expander group should match snapshot 1`] = ` class="c2 fi-expander-group_all-button" type="button" > - Open all + + + Open all expanders +
- + Test expander 1 Test expander content 1 @@ -81,9 +91,9 @@ const [showExpander, setShowExpander] = React.useState(false); )} { + onOpenChange={(open) => { if (window.confirm('Toggle Expander 3')) { - setExpanderThreeOpen(!openState); + setExpanderThreeOpen(!open); } }} > diff --git a/src/core/Expander/Expander/Expander.test.tsx b/src/core/Expander/Expander/Expander.test.tsx index 4c2e983a0..92fe8f6fe 100644 --- a/src/core/Expander/Expander/Expander.test.tsx +++ b/src/core/Expander/Expander/Expander.test.tsx @@ -98,7 +98,7 @@ describe('defaultOpen', () => { it('classnames will be removed when clicked', () => { const mockClickHandler = jest.fn(); const { getByTestId } = render( - + Test expander open by default @@ -121,11 +121,11 @@ describe('defaultOpen', () => { }); }); -describe('onClick', () => { +describe('onOpenChange', () => { it('is called', async () => { const mockClickHandler = jest.fn(); const { getByRole } = render( - + Test expander open by default Test expander open by default content , @@ -161,7 +161,7 @@ describe('open', () => { it('is clicked. Should not change as it is controlled outside', async () => { const mockClickHandler = jest.fn(); const { getByRole, getByTestId } = render( - ControlledExpander({ onClick: mockClickHandler }), + ControlledExpander({ onOpenChange: mockClickHandler }), ); const button = getByRole('button'); fireEvent.mouseDown(button); diff --git a/src/core/Expander/Expander/Expander.tsx b/src/core/Expander/Expander/Expander.tsx index 174e5d54c..388d53828 100644 --- a/src/core/Expander/Expander/Expander.tsx +++ b/src/core/Expander/Expander/Expander.tsx @@ -15,15 +15,13 @@ const baseClassName = 'fi-expander'; const openClassName = `${baseClassName}--open`; export interface ExpanderProviderState { + /** Callback for communicating ExpanderTitle button event to Expander */ onToggleExpander: () => void; + /** Open state for expander */ open: boolean; - /** - * Id for expander button - */ + /** Id for expander button */ titleId: string | undefined; - /** - * Id for expander content - */ + /** Id for expander content */ contentId: string | undefined; } @@ -56,10 +54,10 @@ interface InternalExpanderProps { * @default false */ defaultOpen?: boolean; - /* Controlled open property */ + /** Controlled open property */ open?: boolean; /** Event handler to execute when clicked */ - onClick?: ({ openState }: { openState: boolean }) => void; + onOpenChange?: (open: boolean) => void; consumer?: ExpanderGroupProviderState; } @@ -68,25 +66,20 @@ export interface ExpanderProps extends InternalExpanderProps, TokensProp {} export interface ExpanderTitleBaseProps { /** Custom classname to extend or customize */ className?: string; - /** - * Expander consumer for open state and toggle open callback - */ + /** Expander consumer for open state and toggle open callback */ consumer: ExpanderProviderState; } export interface ExpanderContentBaseProps { /** Custom classname to extend or customize */ className?: string; - /** - * Expander consumer for open state - */ + /** Expander consumer for open state */ consumer: ExpanderProviderState; } class BaseExpander extends Component { state: ExpanderState = { - openState: - this.props.defaultOpen !== undefined ? this.props.defaultOpen : false, + openState: this.props.defaultOpen || false, }; id = idGenerator(this.props.id); @@ -115,7 +108,7 @@ class BaseExpander extends Component { !!this.state.openState !== consumer.toggleAllExpanderState.toState) || (controlled && open !== consumer.toggleAllExpanderState.toState) ) { - this.handleClick(); + this.handleOpenChange(); } } if ( @@ -135,16 +128,16 @@ class BaseExpander extends Component { } } - handleClick = () => { - const { open, onClick } = this.props; + handleOpenChange = () => { + const { open, onOpenChange } = this.props; const { openState } = this.state; const controlled = open !== undefined; const newOpenState = controlled ? !!open : !openState; if (!controlled) { this.setState({ openState: newOpenState }); } - if (!!onClick) { - onClick({ openState: newOpenState }); + if (!!onOpenChange) { + onOpenChange(newOpenState); } }; @@ -153,7 +146,7 @@ class BaseExpander extends Component { id, open, defaultOpen, - onClick, + onOpenChange, className, children, consumer, @@ -173,7 +166,7 @@ class BaseExpander extends Component { open: openState, contentId: this.contentId, titleId: this.id, - onToggleExpander: this.handleClick, + onToggleExpander: this.handleOpenChange, }} > {children} @@ -195,6 +188,10 @@ interface ExpanderState { openState: boolean; } +/** + * + * Hide or show content with always visible title + */ export class Expander extends Component { render() { return ( diff --git a/src/core/Expander/ExpanderContent/ExpanderContent.md b/src/core/Expander/ExpanderContent/ExpanderContent.md new file mode 100644 index 000000000..e69de29bb diff --git a/src/core/Expander/ExpanderContent/ExpanderContent.tsx b/src/core/Expander/ExpanderContent/ExpanderContent.tsx index 8ca75583a..fb765fca1 100644 --- a/src/core/Expander/ExpanderContent/ExpanderContent.tsx +++ b/src/core/Expander/ExpanderContent/ExpanderContent.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { Component, ReactNode } from 'react'; import { default as styled } from 'styled-components'; import classnames from 'classnames'; import { withSuomifiDefaultProps } from '../../theme/utils'; @@ -16,6 +16,8 @@ const contentOpenClassName = `${contentBaseClassName}--open`; const noPaddingClassName = `${contentBaseClassName}--no-padding`; export interface ExpanderContentProps extends Omit { + /** Content for expander */ + children: ReactNode; /** Remove padding from expandable content area (for background usage with padding in given container etc.) */ noPadding?: boolean; } @@ -32,13 +34,14 @@ class BaseExpanderContent extends Component { className, noPadding, consumer, + 'aria-labelledby': ariaLabelledBy, ...passProps } = this.props; return ( { ); } } -// display: ${({ openState }) => (!!openState ? 'block' : 'none')}; + const StyledExpanderContent = styled( ({ tokens, @@ -72,6 +75,10 @@ const StyledExpanderContent = styled( ${(props) => baseStyles(props)}; `; +/** + * + * Expander content wrapper, controlled by expander + */ export class ExpanderContent extends Component< ExpanderContentProps & TokensProp > { diff --git a/src/core/Expander/ExpanderGroup/ExpanderGroup.test.tsx b/src/core/Expander/ExpanderGroup/ExpanderGroup.test.tsx index 0a1c94360..d92471bc3 100644 --- a/src/core/Expander/ExpanderGroup/ExpanderGroup.test.tsx +++ b/src/core/Expander/ExpanderGroup/ExpanderGroup.test.tsx @@ -206,7 +206,7 @@ describe('defaultOpen', () => { expanderProps: { id: 'id-second', defaultOpen: true, - onClick: mockClickHandler, + onOpenChange: mockClickHandler, }, titleProps: { 'data-testid': 'expander-title-2', @@ -229,7 +229,7 @@ describe('defaultOpen', () => { }); }); -describe('onClick', () => { +describe('onOpenChange', () => { it('is called', () => { const mockClickHandler = jest.fn(); const { getByTestId } = render( @@ -243,7 +243,7 @@ describe('onClick', () => { }, { expanderProps: { - onClick: mockClickHandler, + onOpenChange: mockClickHandler, }, titleProps: { 'data-testid': 'expander-title-2', @@ -310,7 +310,7 @@ describe('open', () => { { expanderProps: { id: 'id-second', - onClick: mockClickHandler, + onOpenChange: mockClickHandler, open: true, }, titleProps: { diff --git a/src/core/Expander/ExpanderGroup/ExpanderGroup.tsx b/src/core/Expander/ExpanderGroup/ExpanderGroup.tsx index 2798a87e7..26fae1ca2 100644 --- a/src/core/Expander/ExpanderGroup/ExpanderGroup.tsx +++ b/src/core/Expander/ExpanderGroup/ExpanderGroup.tsx @@ -1,11 +1,10 @@ -import React, { Component } from 'react'; +import React, { Component, ReactNode } from 'react'; import { default as styled } from 'styled-components'; import classnames from 'classnames'; import { withSuomifiDefaultProps } from '../../theme/utils'; import { noMouseFocus } from '../../theme/utils/mousefocus'; import { TokensProp, InternalTokensProp } from '../../theme'; import { HtmlDiv, HtmlButton, HtmlButtonProps, HtmlSpan } from '../../../reset'; -import { ExpanderProps } from '../Expander/Expander'; import { baseStyles } from './ExpanderGroup.baseStyles'; import { VisuallyHidden } from '../../../components'; @@ -25,14 +24,13 @@ interface ExpanderOpenState { interface ExpanderGroupState { /** Expanders that are open */ expanders: ExpanderOpenState; + /** State change transition request */ toggleAllExpanderState: ToggleAllExpanderState; } interface InternalExpanderGroupProps { - /** - * Use Expander's here - */ - children: Array>; + /** Expanders and option other components */ + children: ReactNode; /** 'Open all' button text */ OpenAllText: string; /** 'Close all'-component (Button) */ @@ -43,8 +41,10 @@ interface InternalExpanderGroupProps { AriaOpenAllText?: string; /** 'Close all' button text for screen readers, hides CloseAllText for screen readers if provided */ AriaCloseAllText?: string; - /** Properties for OpenAllButton, aria-hidden = true by default */ - openAllButtonProps?: Omit; + openAllButtonProps?: Omit< + HtmlButtonProps, + 'onClick' | 'onKeyDown' | 'onMouseUp' + >; } export interface ExpanderGroupProviderState { @@ -170,9 +170,9 @@ export interface ExpanderGroupProps /** * - * Used for grouping expanders + * Wrapper for multiple expanders with Open/Close All button */ -export class ExpanderGroup extends React.Component { +export class ExpanderGroup extends Component { render() { const { ...passProps } = withSuomifiDefaultProps(this.props); return ; diff --git a/src/core/Expander/ExpanderTitle/ExpanderTitle.md b/src/core/Expander/ExpanderTitle/ExpanderTitle.md new file mode 100644 index 000000000..e69de29bb diff --git a/src/core/Expander/ExpanderTitle/ExpanderTitle.tsx b/src/core/Expander/ExpanderTitle/ExpanderTitle.tsx index bc8e0e694..033104b18 100644 --- a/src/core/Expander/ExpanderTitle/ExpanderTitle.tsx +++ b/src/core/Expander/ExpanderTitle/ExpanderTitle.tsx @@ -4,10 +4,9 @@ import classnames from 'classnames'; import { withSuomifiDefaultProps } from '../../theme/utils'; import { TokensProp, InternalTokensProp } from '../../theme'; import { noMouseFocus } from '../../theme/utils/mousefocus'; -import { HtmlDiv } from '../../../reset'; +import { HtmlDiv, HtmlButton, HtmlButtonProps } from '../../../reset'; import { baseStyles } from './ExpanderTitle.baseStyles'; import { Icon } from '../../Icon/Icon'; -import { Button, ButtonProps } from '../../../components/Button/Button'; import { ExpanderConsumer, ExpanderTitleBaseProps } from '../Expander/Expander'; const baseClassName = 'fi-expander'; @@ -21,7 +20,10 @@ export interface ExpanderTitleProps { /** Title for Expander */ children?: ReactNode; /** Properties for title open/close toggle button */ - toggleButtonProps?: Omit; + toggleButtonProps?: Omit< + HtmlButtonProps, + 'onClick' | 'onKeyUp' | 'onMouseDown' + >; } export interface InternalExpanderTitleProps @@ -45,12 +47,10 @@ class BaseExpanderTitle extends Component { [titleOpenClassName]: !!consumer.open, })} > - + ); } @@ -80,6 +80,10 @@ const StyledExpanderTitle = styled( ${(props) => baseStyles(props)}; `; +/** + * + * Expander title for content and toggle for content visiblity + */ export class ExpanderTitle extends Component { render() { return ; From 0113cf55617589f3d23e2e28fbb2b616a8a9da9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aappo=20=C3=85lander?= Date: Thu, 26 Nov 2020 16:43:31 +0200 Subject: [PATCH 08/16] Expander and ExpanderGroup API fixes and snapshot updates. --- .../__snapshots__/Expander.test.tsx.snap | 22 +++++------- .../ExpanderContent/ExpanderContent.tsx | 6 +++- .../ExpanderGroup/ExpanderGroup.test.tsx | 4 +-- .../Expander/ExpanderGroup/ExpanderGroup.tsx | 17 ++++++--- .../__snapshots__/ExpanderGroup.test.tsx.snap | 30 ++++++---------- .../Expander/ExpanderTitle/ExpanderTitle.tsx | 7 +++- .../theme/__snapshots__/tokens.test.tsx.snap | 36 +++++++------------ 7 files changed, 58 insertions(+), 64 deletions(-) diff --git a/src/core/Expander/Expander/__snapshots__/Expander.test.tsx.snap b/src/core/Expander/Expander/__snapshots__/Expander.test.tsx.snap index 2f2b91c8d..60ef65e18 100644 --- a/src/core/Expander/Expander/__snapshots__/Expander.test.tsx.snap +++ b/src/core/Expander/Expander/__snapshots__/Expander.test.tsx.snap @@ -73,10 +73,6 @@ exports[`Basic expander shoud match snapshot 1`] = ` white-space: normal; } -.c4.fi-button--disabled { - cursor: not-allowed; -} - .c1 { color: hsl(0,0%,16%); -webkit-letter-spacing: 0; @@ -107,7 +103,7 @@ exports[`Basic expander shoud match snapshot 1`] = ` opacity: 0; } -.c6 { +.c5 { color: hsl(0,0%,16%); -webkit-letter-spacing: 0; -moz-letter-spacing: 0; @@ -140,11 +136,11 @@ exports[`Basic expander shoud match snapshot 1`] = ` will-change: transition,height; } -.c6:not(.fi-expander_content--no-padding) { +.c5:not(.fi-expander_content--no-padding) { padding: 0 16px; } -.c6.fi-expander_content--open { +.c5.fi-expander_content--open { visibility: visible; height: 10%; overflow: visible; @@ -152,14 +148,14 @@ exports[`Basic expander shoud match snapshot 1`] = ` animation: fi-expander_content-anim 50ms cubic-bezier(0.28,0.84,0.42,1) 1 forwards; } -.c6.fi-expander_content--open:not(.fi-expander_content--no-padding) { +.c5.fi-expander_content--open:not(.fi-expander_content--no-padding) { padding-top: 0; padding-right: 20px; padding-bottom: 20px; padding-left: 20px; } -.c5 { +.c4 { display: inline-block; vertical-align: baseline; } @@ -277,17 +273,15 @@ exports[`Basic expander shoud match snapshot 1`] = ` >
diff --git a/src/core/Expander/ExpanderGroup/__snapshots__/ExpanderGroup.test.tsx.snap b/src/core/Expander/ExpanderGroup/__snapshots__/ExpanderGroup.test.tsx.snap index d09e8f39c..5d7952ec9 100644 --- a/src/core/Expander/ExpanderGroup/__snapshots__/ExpanderGroup.test.tsx.snap +++ b/src/core/Expander/ExpanderGroup/__snapshots__/ExpanderGroup.test.tsx.snap @@ -442,6 +442,7 @@ exports[`Basic expander group should match snapshot 1`] = ` >