From b89518f9ef93923e562e6b8dd56d207901b9857a Mon Sep 17 00:00:00 2001 From: Roberto Alvarez Date: Fri, 16 Apr 2021 10:29:04 -0500 Subject: [PATCH] feat(component): add Pill Tabs component (#515) --- .../src/components/PillTabs/PillTabs.tsx | 142 + .../PillTabs/__snapshots__/spec.tsx.snap | 2379 +++++++++++++++++ .../src/components/PillTabs/index.ts | 1 + .../src/components/PillTabs/spec.tsx | 364 +++ .../src/components/PillTabs/styled.tsx | 34 + .../StatefulTable/StatefulTable.tsx | 33 +- .../src/components/StatefulTable/reducer.ts | 62 +- .../src/components/StatefulTable/spec.tsx | 124 +- .../big-design/src/components/Table/Table.tsx | 18 +- .../Table/__snapshots__/spec.tsx.snap | 34 +- .../big-design/src/components/Table/spec.tsx | 54 + .../big-design/src/components/Table/types.ts | 2 + packages/big-design/src/components/index.ts | 1 + packages/big-design/src/hooks/index.ts | 1 + .../src/hooks/useWindowResizeListener.ts | 11 + .../big-design/src/hooks/useWindowSize.ts | 26 +- .../docs/PropTables/PillTabsPropTable.tsx | 50 + .../PropTables/StatefulTablePropTable.tsx | 34 + packages/docs/PropTables/TablePropTable.tsx | 9 + packages/docs/PropTables/index.ts | 1 + packages/docs/components/SideNav/SideNav.tsx | 3 + packages/docs/next.config.js | 1 + packages/docs/pages/PillTabs/PillTabsPage.tsx | 91 + .../pages/StatefulTable/StatefulTablePage.tsx | 40 +- packages/docs/pages/Table/TablePage.tsx | 39 +- packages/docs/pages/_app.tsx | 2 +- 26 files changed, 3510 insertions(+), 46 deletions(-) create mode 100644 packages/big-design/src/components/PillTabs/PillTabs.tsx create mode 100644 packages/big-design/src/components/PillTabs/__snapshots__/spec.tsx.snap create mode 100644 packages/big-design/src/components/PillTabs/index.ts create mode 100644 packages/big-design/src/components/PillTabs/spec.tsx create mode 100644 packages/big-design/src/components/PillTabs/styled.tsx create mode 100644 packages/big-design/src/hooks/useWindowResizeListener.ts create mode 100644 packages/docs/PropTables/PillTabsPropTable.tsx create mode 100644 packages/docs/pages/PillTabs/PillTabsPage.tsx diff --git a/packages/big-design/src/components/PillTabs/PillTabs.tsx b/packages/big-design/src/components/PillTabs/PillTabs.tsx new file mode 100644 index 000000000..72f3d300f --- /dev/null +++ b/packages/big-design/src/components/PillTabs/PillTabs.tsx @@ -0,0 +1,142 @@ +import { CheckIcon, MoreHorizIcon } from '@bigcommerce/big-design-icons'; +import React, { createRef, useCallback, useEffect, useMemo, useState } from 'react'; + +import { useWindowResizeListener } from '../../hooks'; +import { Button } from '../Button'; +import { Dropdown } from '../Dropdown'; +import { Flex } from '../Flex'; + +import { StyledFlexItem, StyledPillTab } from './styled'; + +export interface PillTabItem { + id: string; + title: string; +} + +export interface PillTabsProps { + items: PillTabItem[]; + activePills: string[]; + onPillClick: (itemId: string) => void; +} + +export const PillTabs: React.FC = ({ activePills, items, onPillClick }) => { + const parentRef = createRef(); + const dropdownRef = createRef(); + const [isMenuVisible, setIsMenuVisible] = useState(false); + const [pillsState, setPillsState] = useState( + items.map((item) => ({ + isVisible: true, + item, + ref: createRef(), + })), + ); + + const hideOverflowedPills = useCallback(() => { + const parentWidth = parentRef.current?.offsetWidth; + const dropdownWidth = dropdownRef.current?.offsetWidth; + + if (!parentWidth || !dropdownWidth) { + return; + } + + let remainingWidth = parentWidth; + + const newState = pillsState.map((stateObj) => { + const pillWidth = stateObj.ref.current?.offsetWidth; + + if (!pillWidth) { + return stateObj; + } + + if (remainingWidth - pillWidth > dropdownWidth) { + remainingWidth = remainingWidth - pillWidth; + + return { + ...stateObj, + isVisible: true, + }; + } + + return { + ...stateObj, + isVisible: false, + }; + }); + + const visiblePills = pillsState.filter((stateObj) => stateObj.isVisible); + const newVisiblePills = newState.filter((stateObj) => stateObj.isVisible); + + if (visiblePills.length !== newVisiblePills.length) { + setIsMenuVisible(newVisiblePills.length !== items.length); + setPillsState(newState); + } + }, [items, parentRef, dropdownRef, pillsState]); + + const renderedDropdown = useMemo(() => { + const dropdownItems = pillsState + .filter((stateObj) => !stateObj.isVisible) + .map((stateObj) => { + const item = items.find(({ title }) => title === stateObj.item.title); + const isActive = item ? activePills.includes(item.id) : false; + + return { + content: stateObj.item.title, + onItemClick: () => onPillClick(stateObj.item.id), + hash: stateObj.item.title.toLowerCase(), + icon: isActive ? : undefined, + }; + }); + + return ( + + } variant="subtle" />} /> + + ); + }, [items, pillsState, isMenuVisible, dropdownRef, activePills, onPillClick]); + + const renderedPills = useMemo( + () => + items.map((item, index) => ( + + onPillClick(item.id)} + marginRight="xLarge" + > + {item.title} + + + )), + [items, pillsState, activePills, onPillClick], + ); + + useEffect(() => { + hideOverflowedPills(); + }, [items, parentRef, pillsState, hideOverflowedPills]); + + useWindowResizeListener(() => { + hideOverflowedPills(); + }); + + return items.length > 0 ? ( + + {renderedPills} + {renderedDropdown} + + ) : null; +}; + +PillTabs.displayName = 'Pill Tabs'; diff --git a/packages/big-design/src/components/PillTabs/__snapshots__/spec.tsx.snap b/packages/big-design/src/components/PillTabs/__snapshots__/spec.tsx.snap new file mode 100644 index 000000000..748fe6dc1 --- /dev/null +++ b/packages/big-design/src/components/PillTabs/__snapshots__/spec.tsx.snap @@ -0,0 +1,2379 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dropdown is not visible if items fit 1`] = ` +.c9 { + vertical-align: middle; + height: 1.5rem; + width: 1.5rem; +} + +.c1 { + box-sizing: border-box; +} + +.c10 { + box-sizing: border-box; + z-index: 1060; +} + +.c2 { + -webkit-align-content: stretch; + -ms-flex-line-pack: stretch; + align-content: stretch; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c3 { + -webkit-align-self: auto; + -ms-flex-item-align: auto; + align-self: auto; + -webkit-flex-basis: auto; + -ms-flex-preferred-size: auto; + flex-basis: auto; + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; + -webkit-order: 0; + -ms-flex-order: 0; + order: 0; + -webkit-flex-shrink: 1; + -ms-flex-negative: 1; + flex-shrink: 1; +} + +.c4 { + -webkit-transition: all 150ms ease-out; + transition: all 150ms ease-out; + -webkit-transition-property: background-color,border-color,box-shadow,color; + transition-property: background-color,border-color,box-shadow,color; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid #D9DCE9; + border-radius: 0.25rem; + color: #FFFFFF; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1rem; + font-weight: 400; + height: 2.25rem; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 2rem; + outline: none; + padding: 0 1rem; + pointer-events: auto; + position: relative; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + white-space: nowrap; + width: 100%; + background-color: transparent; + border-color: transparent; + color: #3C64F4; +} + +.c4.c4 { + margin-right: 1.5rem; +} + +.c4:focus { + outline: none; +} + +.c4[disabled] { + border-color: #D9DCE9; + pointer-events: none; +} + +.c4 + .bd-button { + margin-top: 0.5rem; +} + +.c4:active { + background-color: #DBE3FE; +} + +.c4:focus { + box-shadow: 0 0 0 0.25rem #DBE3FE; +} + +.c4:hover:not(:active) { + background-color: #F0F3FF; +} + +.c4[disabled] { + border-color: transparent; + color: #D9DCE9; +} + +.c8 { + -webkit-transition: all 150ms ease-out; + transition: all 150ms ease-out; + -webkit-transition-property: background-color,border-color,box-shadow,color; + transition-property: background-color,border-color,box-shadow,color; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid #D9DCE9; + border-radius: 0.25rem; + color: #FFFFFF; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1rem; + font-weight: 400; + height: 2.25rem; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 2rem; + outline: none; + padding: 0 1rem; + pointer-events: auto; + position: relative; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + white-space: nowrap; + width: 100%; + padding: 0 0.5rem; + background-color: transparent; + border-color: transparent; + color: #3C64F4; +} + +.c8:focus { + outline: none; +} + +.c8[disabled] { + border-color: #D9DCE9; + pointer-events: none; +} + +.c8 + .bd-button { + margin-top: 0.5rem; +} + +.c8:active { + background-color: #DBE3FE; +} + +.c8:focus { + box-shadow: 0 0 0 0.25rem #DBE3FE; +} + +.c8:hover:not(:active) { + background-color: #F0F3FF; +} + +.c8[disabled] { + border-color: transparent; + color: #D9DCE9; +} + +.c5 { + -webkit-align-content: center; + -ms-flex-line-pack: center; + align-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: inline-grid; + grid-auto-flow: column; + grid-gap: 0.5rem; + visibility: visible; +} + +.c11 { + border-radius: 0.25rem; + box-shadow: 0px 1px 6px rgba(49,52,64,0.2); + background-color: #FFFFFF; + color: #313440; + margin: 0; + max-height: 15.625rem; + outline: none; + overflow-y: auto; + padding: 0.5rem 0; + z-index: 1060; +} + +.c7 { + position: relative; +} + +.c6 { + position: absolute; + visibility: hidden; + z-index: -1070; +} + +.c0 { + width: 200px; +} + +@media (min-width:720px) { + .c4 + .bd-button { + margin-top: 0; + margin-left: 0.5rem; + } +} + +@media (min-width:720px) { + .c4 { + width: auto; + } +} + +@media (min-width:720px) { + .c8 + .bd-button { + margin-top: 0; + margin-left: 0.5rem; + } +} + +@media (min-width:720px) { + .c8 { + width: auto; + } +} + +
+
+
+
+ +
+
+
+ +
+
    +
+
+
+
+
+
+`; + +exports[`it renders the given tabs 1`] = ` +.c9 { + vertical-align: middle; + height: 1.5rem; + width: 1.5rem; +} + +.c1 { + box-sizing: border-box; +} + +.c10 { + box-sizing: border-box; + z-index: 1060; +} + +.c2 { + -webkit-align-content: stretch; + -ms-flex-line-pack: stretch; + align-content: stretch; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c3 { + -webkit-align-self: auto; + -ms-flex-item-align: auto; + align-self: auto; + -webkit-flex-basis: auto; + -ms-flex-preferred-size: auto; + flex-basis: auto; + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; + -webkit-order: 0; + -ms-flex-order: 0; + order: 0; + -webkit-flex-shrink: 1; + -ms-flex-negative: 1; + flex-shrink: 1; +} + +.c4 { + -webkit-transition: all 150ms ease-out; + transition: all 150ms ease-out; + -webkit-transition-property: background-color,border-color,box-shadow,color; + transition-property: background-color,border-color,box-shadow,color; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid #D9DCE9; + border-radius: 0.25rem; + color: #FFFFFF; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1rem; + font-weight: 400; + height: 2.25rem; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 2rem; + outline: none; + padding: 0 1rem; + pointer-events: auto; + position: relative; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + white-space: nowrap; + width: 100%; + background-color: transparent; + border-color: transparent; + color: #3C64F4; +} + +.c4.c4 { + margin-right: 1.5rem; +} + +.c4:focus { + outline: none; +} + +.c4[disabled] { + border-color: #D9DCE9; + pointer-events: none; +} + +.c4 + .bd-button { + margin-top: 0.5rem; +} + +.c4:active { + background-color: #DBE3FE; +} + +.c4:focus { + box-shadow: 0 0 0 0.25rem #DBE3FE; +} + +.c4:hover:not(:active) { + background-color: #F0F3FF; +} + +.c4[disabled] { + border-color: transparent; + color: #D9DCE9; +} + +.c8 { + -webkit-transition: all 150ms ease-out; + transition: all 150ms ease-out; + -webkit-transition-property: background-color,border-color,box-shadow,color; + transition-property: background-color,border-color,box-shadow,color; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid #D9DCE9; + border-radius: 0.25rem; + color: #FFFFFF; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1rem; + font-weight: 400; + height: 2.25rem; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 2rem; + outline: none; + padding: 0 1rem; + pointer-events: auto; + position: relative; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + white-space: nowrap; + width: 100%; + padding: 0 0.5rem; + background-color: transparent; + border-color: transparent; + color: #3C64F4; +} + +.c8:focus { + outline: none; +} + +.c8[disabled] { + border-color: #D9DCE9; + pointer-events: none; +} + +.c8 + .bd-button { + margin-top: 0.5rem; +} + +.c8:active { + background-color: #DBE3FE; +} + +.c8:focus { + box-shadow: 0 0 0 0.25rem #DBE3FE; +} + +.c8:hover:not(:active) { + background-color: #F0F3FF; +} + +.c8[disabled] { + border-color: transparent; + color: #D9DCE9; +} + +.c5 { + -webkit-align-content: center; + -ms-flex-line-pack: center; + align-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: inline-grid; + grid-auto-flow: column; + grid-gap: 0.5rem; + visibility: visible; +} + +.c11 { + border-radius: 0.25rem; + box-shadow: 0px 1px 6px rgba(49,52,64,0.2); + background-color: #FFFFFF; + color: #313440; + margin: 0; + max-height: 15.625rem; + outline: none; + overflow-y: auto; + padding: 0.5rem 0; + z-index: 1060; +} + +.c7 { + position: relative; +} + +.c6 { + position: absolute; + visibility: hidden; + z-index: -1070; +} + +.c0 { + width: 200px; +} + +@media (min-width:720px) { + .c4 + .bd-button { + margin-top: 0; + margin-left: 0.5rem; + } +} + +@media (min-width:720px) { + .c4 { + width: auto; + } +} + +@media (min-width:720px) { + .c8 + .bd-button { + margin-top: 0; + margin-left: 0.5rem; + } +} + +@media (min-width:720px) { + .c8 { + width: auto; + } +} + +
+
+
+
+ +
+
+
+ +
+
    +
+
+
+
+
+
+`; + +exports[`only the pills that fit are visible 1`] = ` +.c9 { + vertical-align: middle; + height: 1.5rem; + width: 1.5rem; +} + +.c1 { + box-sizing: border-box; +} + +.c10 { + box-sizing: border-box; + z-index: 1060; +} + +.c2 { + -webkit-align-content: stretch; + -ms-flex-line-pack: stretch; + align-content: stretch; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c3 { + -webkit-align-self: auto; + -ms-flex-item-align: auto; + align-self: auto; + -webkit-flex-basis: auto; + -ms-flex-preferred-size: auto; + flex-basis: auto; + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; + -webkit-order: 0; + -ms-flex-order: 0; + order: 0; + -webkit-flex-shrink: 1; + -ms-flex-negative: 1; + flex-shrink: 1; +} + +.c4 { + -webkit-transition: all 150ms ease-out; + transition: all 150ms ease-out; + -webkit-transition-property: background-color,border-color,box-shadow,color; + transition-property: background-color,border-color,box-shadow,color; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid #D9DCE9; + border-radius: 0.25rem; + color: #FFFFFF; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1rem; + font-weight: 400; + height: 2.25rem; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 2rem; + outline: none; + padding: 0 1rem; + pointer-events: auto; + position: relative; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + white-space: nowrap; + width: 100%; + background-color: transparent; + border-color: transparent; + color: #3C64F4; +} + +.c4.c4 { + margin-right: 1.5rem; +} + +.c4:focus { + outline: none; +} + +.c4[disabled] { + border-color: #D9DCE9; + pointer-events: none; +} + +.c4 + .bd-button { + margin-top: 0.5rem; +} + +.c4:active { + background-color: #DBE3FE; +} + +.c4:focus { + box-shadow: 0 0 0 0.25rem #DBE3FE; +} + +.c4:hover:not(:active) { + background-color: #F0F3FF; +} + +.c4[disabled] { + border-color: transparent; + color: #D9DCE9; +} + +.c8 { + -webkit-transition: all 150ms ease-out; + transition: all 150ms ease-out; + -webkit-transition-property: background-color,border-color,box-shadow,color; + transition-property: background-color,border-color,box-shadow,color; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid #D9DCE9; + border-radius: 0.25rem; + color: #FFFFFF; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1rem; + font-weight: 400; + height: 2.25rem; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 2rem; + outline: none; + padding: 0 1rem; + pointer-events: auto; + position: relative; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + white-space: nowrap; + width: 100%; + padding: 0 0.5rem; + background-color: transparent; + border-color: transparent; + color: #3C64F4; +} + +.c8:focus { + outline: none; +} + +.c8[disabled] { + border-color: #D9DCE9; + pointer-events: none; +} + +.c8 + .bd-button { + margin-top: 0.5rem; +} + +.c8:active { + background-color: #DBE3FE; +} + +.c8:focus { + box-shadow: 0 0 0 0.25rem #DBE3FE; +} + +.c8:hover:not(:active) { + background-color: #F0F3FF; +} + +.c8[disabled] { + border-color: transparent; + color: #D9DCE9; +} + +.c5 { + -webkit-align-content: center; + -ms-flex-line-pack: center; + align-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: inline-grid; + grid-auto-flow: column; + grid-gap: 0.5rem; + visibility: visible; +} + +.c11 { + border-radius: 0.25rem; + box-shadow: 0px 1px 6px rgba(49,52,64,0.2); + background-color: #FFFFFF; + color: #313440; + margin: 0; + max-height: 15.625rem; + outline: none; + overflow-y: auto; + padding: 0.5rem 0; + z-index: 1060; +} + +.c7 { + position: relative; +} + +.c6 { + position: absolute; + visibility: hidden; + z-index: -1070; +} + +.c0 { + width: 200px; +} + +@media (min-width:720px) { + .c4 + .bd-button { + margin-top: 0; + margin-left: 0.5rem; + } +} + +@media (min-width:720px) { + .c4 { + width: auto; + } +} + +@media (min-width:720px) { + .c8 + .bd-button { + margin-top: 0; + margin-left: 0.5rem; + } +} + +@media (min-width:720px) { + .c8 { + width: auto; + } +} + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
    +
+
+
+
+
+
+`; + +exports[`only the pills that fit are visible 2 1`] = ` +.c9 { + vertical-align: middle; + height: 1.5rem; + width: 1.5rem; +} + +.c1 { + box-sizing: border-box; +} + +.c10 { + box-sizing: border-box; + z-index: 1060; +} + +.c2 { + -webkit-align-content: stretch; + -ms-flex-line-pack: stretch; + align-content: stretch; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c3 { + -webkit-align-self: auto; + -ms-flex-item-align: auto; + align-self: auto; + -webkit-flex-basis: auto; + -ms-flex-preferred-size: auto; + flex-basis: auto; + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; + -webkit-order: 0; + -ms-flex-order: 0; + order: 0; + -webkit-flex-shrink: 1; + -ms-flex-negative: 1; + flex-shrink: 1; +} + +.c4 { + -webkit-transition: all 150ms ease-out; + transition: all 150ms ease-out; + -webkit-transition-property: background-color,border-color,box-shadow,color; + transition-property: background-color,border-color,box-shadow,color; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid #D9DCE9; + border-radius: 0.25rem; + color: #FFFFFF; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1rem; + font-weight: 400; + height: 2.25rem; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 2rem; + outline: none; + padding: 0 1rem; + pointer-events: auto; + position: relative; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + white-space: nowrap; + width: 100%; + background-color: transparent; + border-color: transparent; + color: #3C64F4; +} + +.c4.c4 { + margin-right: 1.5rem; +} + +.c4:focus { + outline: none; +} + +.c4[disabled] { + border-color: #D9DCE9; + pointer-events: none; +} + +.c4 + .bd-button { + margin-top: 0.5rem; +} + +.c4:active { + background-color: #DBE3FE; +} + +.c4:focus { + box-shadow: 0 0 0 0.25rem #DBE3FE; +} + +.c4:hover:not(:active) { + background-color: #F0F3FF; +} + +.c4[disabled] { + border-color: transparent; + color: #D9DCE9; +} + +.c8 { + -webkit-transition: all 150ms ease-out; + transition: all 150ms ease-out; + -webkit-transition-property: background-color,border-color,box-shadow,color; + transition-property: background-color,border-color,box-shadow,color; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid #D9DCE9; + border-radius: 0.25rem; + color: #FFFFFF; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1rem; + font-weight: 400; + height: 2.25rem; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 2rem; + outline: none; + padding: 0 1rem; + pointer-events: auto; + position: relative; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + white-space: nowrap; + width: 100%; + padding: 0 0.5rem; + background-color: transparent; + border-color: transparent; + color: #3C64F4; +} + +.c8:focus { + outline: none; +} + +.c8[disabled] { + border-color: #D9DCE9; + pointer-events: none; +} + +.c8 + .bd-button { + margin-top: 0.5rem; +} + +.c8:active { + background-color: #DBE3FE; +} + +.c8:focus { + box-shadow: 0 0 0 0.25rem #DBE3FE; +} + +.c8:hover:not(:active) { + background-color: #F0F3FF; +} + +.c8[disabled] { + border-color: transparent; + color: #D9DCE9; +} + +.c5 { + -webkit-align-content: center; + -ms-flex-line-pack: center; + align-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: inline-grid; + grid-auto-flow: column; + grid-gap: 0.5rem; + visibility: visible; +} + +.c11 { + border-radius: 0.25rem; + box-shadow: 0px 1px 6px rgba(49,52,64,0.2); + background-color: #FFFFFF; + color: #313440; + margin: 0; + max-height: 15.625rem; + outline: none; + overflow-y: auto; + padding: 0.5rem 0; + z-index: 1060; +} + +.c7 { + position: relative; +} + +.c6 { + position: absolute; + visibility: hidden; + z-index: -1070; +} + +.c0 { + width: 200px; +} + +@media (min-width:720px) { + .c4 + .bd-button { + margin-top: 0; + margin-left: 0.5rem; + } +} + +@media (min-width:720px) { + .c4 { + width: auto; + } +} + +@media (min-width:720px) { + .c8 + .bd-button { + margin-top: 0; + margin-left: 0.5rem; + } +} + +@media (min-width:720px) { + .c8 { + width: auto; + } +} + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
    +
+
+
+
+
+
+`; + +exports[`renders all the filters if they fit 1`] = ` +.c9 { + vertical-align: middle; + height: 1.5rem; + width: 1.5rem; +} + +.c1 { + box-sizing: border-box; +} + +.c10 { + box-sizing: border-box; + z-index: 1060; +} + +.c2 { + -webkit-align-content: stretch; + -ms-flex-line-pack: stretch; + align-content: stretch; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c3 { + -webkit-align-self: auto; + -ms-flex-item-align: auto; + align-self: auto; + -webkit-flex-basis: auto; + -ms-flex-preferred-size: auto; + flex-basis: auto; + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; + -webkit-order: 0; + -ms-flex-order: 0; + order: 0; + -webkit-flex-shrink: 1; + -ms-flex-negative: 1; + flex-shrink: 1; +} + +.c4 { + -webkit-transition: all 150ms ease-out; + transition: all 150ms ease-out; + -webkit-transition-property: background-color,border-color,box-shadow,color; + transition-property: background-color,border-color,box-shadow,color; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid #D9DCE9; + border-radius: 0.25rem; + color: #FFFFFF; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1rem; + font-weight: 400; + height: 2.25rem; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 2rem; + outline: none; + padding: 0 1rem; + pointer-events: auto; + position: relative; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + white-space: nowrap; + width: 100%; + background-color: transparent; + border-color: transparent; + color: #3C64F4; +} + +.c4.c4 { + margin-right: 1.5rem; +} + +.c4:focus { + outline: none; +} + +.c4[disabled] { + border-color: #D9DCE9; + pointer-events: none; +} + +.c4 + .bd-button { + margin-top: 0.5rem; +} + +.c4:active { + background-color: #DBE3FE; +} + +.c4:focus { + box-shadow: 0 0 0 0.25rem #DBE3FE; +} + +.c4:hover:not(:active) { + background-color: #F0F3FF; +} + +.c4[disabled] { + border-color: transparent; + color: #D9DCE9; +} + +.c8 { + -webkit-transition: all 150ms ease-out; + transition: all 150ms ease-out; + -webkit-transition-property: background-color,border-color,box-shadow,color; + transition-property: background-color,border-color,box-shadow,color; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid #D9DCE9; + border-radius: 0.25rem; + color: #FFFFFF; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1rem; + font-weight: 400; + height: 2.25rem; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 2rem; + outline: none; + padding: 0 1rem; + pointer-events: auto; + position: relative; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + white-space: nowrap; + width: 100%; + padding: 0 0.5rem; + background-color: transparent; + border-color: transparent; + color: #3C64F4; +} + +.c8:focus { + outline: none; +} + +.c8[disabled] { + border-color: #D9DCE9; + pointer-events: none; +} + +.c8 + .bd-button { + margin-top: 0.5rem; +} + +.c8:active { + background-color: #DBE3FE; +} + +.c8:focus { + box-shadow: 0 0 0 0.25rem #DBE3FE; +} + +.c8:hover:not(:active) { + background-color: #F0F3FF; +} + +.c8[disabled] { + border-color: transparent; + color: #D9DCE9; +} + +.c5 { + -webkit-align-content: center; + -ms-flex-line-pack: center; + align-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: inline-grid; + grid-auto-flow: column; + grid-gap: 0.5rem; + visibility: visible; +} + +.c11 { + border-radius: 0.25rem; + box-shadow: 0px 1px 6px rgba(49,52,64,0.2); + background-color: #FFFFFF; + color: #313440; + margin: 0; + max-height: 15.625rem; + outline: none; + overflow-y: auto; + padding: 0.5rem 0; + z-index: 1060; +} + +.c7 { + position: relative; +} + +.c6 { + position: absolute; + visibility: hidden; + z-index: -1070; +} + +.c0 { + width: 200px; +} + +@media (min-width:720px) { + .c4 + .bd-button { + margin-top: 0; + margin-left: 0.5rem; + } +} + +@media (min-width:720px) { + .c4 { + width: auto; + } +} + +@media (min-width:720px) { + .c8 + .bd-button { + margin-top: 0; + margin-left: 0.5rem; + } +} + +@media (min-width:720px) { + .c8 { + width: auto; + } +} + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
    +
+
+
+
+
+
+`; + +exports[`renders dropdown if items do not fit 1`] = ` +.c9 { + vertical-align: middle; + height: 1.5rem; + width: 1.5rem; +} + +.c1 { + box-sizing: border-box; +} + +.c10 { + box-sizing: border-box; + z-index: 1060; +} + +.c2 { + -webkit-align-content: stretch; + -ms-flex-line-pack: stretch; + align-content: stretch; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c3 { + -webkit-align-self: auto; + -ms-flex-item-align: auto; + align-self: auto; + -webkit-flex-basis: auto; + -ms-flex-preferred-size: auto; + flex-basis: auto; + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; + -webkit-order: 0; + -ms-flex-order: 0; + order: 0; + -webkit-flex-shrink: 1; + -ms-flex-negative: 1; + flex-shrink: 1; +} + +.c5 { + -webkit-transition: all 150ms ease-out; + transition: all 150ms ease-out; + -webkit-transition-property: background-color,border-color,box-shadow,color; + transition-property: background-color,border-color,box-shadow,color; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid #D9DCE9; + border-radius: 0.25rem; + color: #FFFFFF; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1rem; + font-weight: 400; + height: 2.25rem; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 2rem; + outline: none; + padding: 0 1rem; + pointer-events: auto; + position: relative; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + white-space: nowrap; + width: 100%; + background-color: transparent; + border-color: transparent; + color: #3C64F4; +} + +.c5.c5 { + margin-right: 1.5rem; +} + +.c5:focus { + outline: none; +} + +.c5[disabled] { + border-color: #D9DCE9; + pointer-events: none; +} + +.c5 + .bd-button { + margin-top: 0.5rem; +} + +.c5:active { + background-color: #DBE3FE; +} + +.c5:focus { + box-shadow: 0 0 0 0.25rem #DBE3FE; +} + +.c5:hover:not(:active) { + background-color: #F0F3FF; +} + +.c5[disabled] { + border-color: transparent; + color: #D9DCE9; +} + +.c8 { + -webkit-transition: all 150ms ease-out; + transition: all 150ms ease-out; + -webkit-transition-property: background-color,border-color,box-shadow,color; + transition-property: background-color,border-color,box-shadow,color; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid #D9DCE9; + border-radius: 0.25rem; + color: #FFFFFF; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1rem; + font-weight: 400; + height: 2.25rem; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 2rem; + outline: none; + padding: 0 1rem; + pointer-events: auto; + position: relative; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + white-space: nowrap; + width: 100%; + padding: 0 0.5rem; + background-color: transparent; + border-color: transparent; + color: #3C64F4; +} + +.c8:focus { + outline: none; +} + +.c8[disabled] { + border-color: #D9DCE9; + pointer-events: none; +} + +.c8 + .bd-button { + margin-top: 0.5rem; +} + +.c8:active { + background-color: #DBE3FE; +} + +.c8:focus { + box-shadow: 0 0 0 0.25rem #DBE3FE; +} + +.c8:hover:not(:active) { + background-color: #F0F3FF; +} + +.c8[disabled] { + border-color: transparent; + color: #D9DCE9; +} + +.c6 { + -webkit-align-content: center; + -ms-flex-line-pack: center; + align-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: inline-grid; + grid-auto-flow: column; + grid-gap: 0.5rem; + visibility: visible; +} + +.c11 { + border-radius: 0.25rem; + box-shadow: 0px 1px 6px rgba(49,52,64,0.2); + background-color: #FFFFFF; + color: #313440; + margin: 0; + max-height: 15.625rem; + outline: none; + overflow-y: auto; + padding: 0.5rem 0; + z-index: 1060; +} + +.c7 { + position: relative; +} + +.c4 { + position: absolute; + visibility: hidden; + z-index: -1070; +} + +.c0 { + width: 200px; +} + +@media (min-width:720px) { + .c5 + .bd-button { + margin-top: 0; + margin-left: 0.5rem; + } +} + +@media (min-width:720px) { + .c5 { + width: auto; + } +} + +@media (min-width:720px) { + .c8 + .bd-button { + margin-top: 0; + margin-left: 0.5rem; + } +} + +@media (min-width:720px) { + .c8 { + width: auto; + } +} + +
+
+
+
+ +
+
+ +
+
+
+ +
+
    +
+
+
+
+
+
+`; diff --git a/packages/big-design/src/components/PillTabs/index.ts b/packages/big-design/src/components/PillTabs/index.ts new file mode 100644 index 000000000..4a3b2661f --- /dev/null +++ b/packages/big-design/src/components/PillTabs/index.ts @@ -0,0 +1 @@ +export * from './PillTabs'; diff --git a/packages/big-design/src/components/PillTabs/spec.tsx b/packages/big-design/src/components/PillTabs/spec.tsx new file mode 100644 index 000000000..652721d68 --- /dev/null +++ b/packages/big-design/src/components/PillTabs/spec.tsx @@ -0,0 +1,364 @@ +import { theme as defaultTheme } from '@bigcommerce/big-design-theme'; +import { fireEvent, render, wait } from '@testing-library/react'; +import 'jest-styled-components'; +import React from 'react'; +import styled from 'styled-components'; + +import { PillTabs, PillTabsProps } from './PillTabs'; + +const Wrapper = styled.div` + width: 200px; +`; + +Wrapper.defaultProps = { theme: defaultTheme }; + +const TestComponent: React.FC = ({ activePills, items, onPillClick }) => { + return ( + + + + ); +}; + +const originalPrototype = Object.getOwnPropertyDescriptors(window.HTMLElement.prototype); + +afterAll(() => Object.defineProperties(window.HTMLElement.prototype, originalPrototype)); + +const HIDDEN_STYLES = { + 'z-index': -defaultTheme.zIndex.tooltip, + position: 'absolute', + visibility: 'hidden', +}; + +test('it renders the given tabs', () => { + const onClick = jest.fn(); + const items = [ + { + title: 'In stock', + id: 'filter1', + }, + ]; + + const { container, getByText } = render(); + const inStock = getByText('In stock'); + + expect(container).toMatchSnapshot(); + expect(inStock).not.toHaveStyle(HIDDEN_STYLES); +}); + +test('dropdown is not visible if items fit', () => { + Object.defineProperties(window.HTMLElement.prototype, { + offsetWidth: { + get() { + if (this.dataset.testid === 'pilltabs-wrapper') { + return 400; + } + + if (this.dataset.testid === 'pilltabs-pill-0') { + return 100; + } + + return parseFloat(this.style.width) || 0; + }, + }, + }); + const onClick = jest.fn(); + const items = [ + { + title: 'In stock', + id: 'filter1', + }, + ]; + + const { container, getByText, queryByTestId } = render( + , + ); + const inStock = getByText('In stock'); + const dropdownToggle = queryByTestId('pilltabs-dropdown-toggle'); + + expect(container).toMatchSnapshot(); + expect(inStock).not.toHaveStyle(HIDDEN_STYLES); + expect(dropdownToggle).toHaveStyle(HIDDEN_STYLES); +}); + +test('renders dropdown if items do not fit', async () => { + Object.defineProperties(window.HTMLElement.prototype, { + offsetWidth: { + get() { + if (this.dataset.testid === 'pilltabs-wrapper') { + return 400; + } + + return parseFloat(this.style.width) || 300; + }, + }, + }); + + const onClick = jest.fn(); + const items = [ + { + title: 'In stock', + + id: 'filter1', + }, + { + title: 'Long filter name', + + id: 'filter1', + }, + ]; + + const { container, getByText, queryByTestId } = render( + , + ); + await wait(); + + const inStock = getByText('Long filter name'); + const dropdownToggle = queryByTestId('pilltabs-dropdown-toggle'); + + expect(container).toMatchSnapshot(); + expect(inStock).not.toHaveStyle(HIDDEN_STYLES); + expect(dropdownToggle).not.toHaveStyle(HIDDEN_STYLES); +}); + +test('renders all the filters if they fit', async () => { + Object.defineProperties(window.HTMLElement.prototype, { + offsetWidth: { + get() { + if (this.dataset.testid === 'pilltabs-wrapper') { + return 400; + } + + return parseFloat(this.style.width) || 50; + }, + }, + }); + + const onClick = jest.fn(); + const items = [ + { + title: 'In stock', + + id: 'filter1', + }, + { + title: 'Filter 2', + + id: 'filter1', + }, + { + title: 'Filter 3', + + id: 'filter1', + }, + ]; + + const { container, getByText, queryByTestId } = render( + , + ); + await wait(); + + const inStock = getByText('In stock'); + const filter2 = getByText('Filter 2'); + const filter3 = getByText('Filter 3'); + const dropdownToggle = queryByTestId('pilltabs-dropdown-toggle'); + + expect(container).toMatchSnapshot(); + expect(inStock).not.toHaveStyle(HIDDEN_STYLES); + expect(filter2).not.toHaveStyle(HIDDEN_STYLES); + expect(filter3).not.toHaveStyle(HIDDEN_STYLES); + expect(dropdownToggle).toHaveStyle(HIDDEN_STYLES); +}); + +test('only the pills that fit are visible', async () => { + Object.defineProperties(window.HTMLElement.prototype, { + offsetWidth: { + get() { + if (this.dataset.testid === 'pilltabs-wrapper') { + return 400; + } + + if (this.dataset.testid === 'pilltabs-dropdown-toggle') { + return 50; + } + + if (this.dataset.testid === 'pilltabs-pill-0') { + return 300; + } + + if (this.dataset.testid === 'pilltabs-pill-1') { + return 300; + } + + if (this.dataset.testid === 'pilltabs-pill-2') { + return 300; + } + + return parseFloat(this.style.width) || 0; + }, + }, + }); + + const onClick = jest.fn(); + const items = [ + { + title: 'In stock', + + id: 'filter1', + }, + { + title: 'Filter 2', + + id: 'filter1', + }, + { + title: 'Filter 3', + + id: 'filter1', + }, + ]; + + const { container, queryByTestId, getByTestId } = render( + , + ); + await wait(); + + const inStock = getByTestId('pilltabs-pill-0'); + const filter2 = getByTestId('pilltabs-pill-1'); + const filter3 = getByTestId('pilltabs-pill-2'); + const dropdownToggle = queryByTestId('pilltabs-dropdown-toggle'); + + expect(container).toMatchSnapshot(); + expect(inStock).not.toHaveStyle(HIDDEN_STYLES); + expect(filter2).toHaveStyle(HIDDEN_STYLES); + expect(filter3).toHaveStyle(HIDDEN_STYLES); + expect(dropdownToggle).not.toHaveStyle(HIDDEN_STYLES); +}); + +test('only the pills that fit are visible 2', async () => { + Object.defineProperties(window.HTMLElement.prototype, { + offsetWidth: { + get() { + if (this.dataset.testid === 'pilltabs-wrapper') { + return 400; + } + + if (this.dataset.testid === 'pilltabs-pill-0') { + return 100; + } + + if (this.dataset.testid === 'pilltabs-pill-1') { + return 100; + } + + if (this.dataset.testid === 'pilltabs-pill-2') { + return 300; + } + + return parseFloat(this.style.width) || 50; + }, + }, + }); + + const onClick = jest.fn(); + const items = [ + { + title: 'In stock', + + id: 'filter1', + }, + { + title: 'Filter 2', + + id: 'filter1', + }, + { + title: 'Filter 3', + + id: 'filter1', + }, + ]; + + const { container, queryByTestId, getByTestId } = render( + , + ); + await wait(); + + const inStock = getByTestId('pilltabs-pill-0'); + const filter2 = getByTestId('pilltabs-pill-1'); + const filter3 = getByTestId('pilltabs-pill-2'); + const dropdownToggle = queryByTestId('pilltabs-dropdown-toggle'); + + expect(container).toMatchSnapshot(); + expect(inStock).not.toHaveStyle(HIDDEN_STYLES); + expect(filter2).not.toHaveStyle(HIDDEN_STYLES); + expect(filter3).toHaveStyle(HIDDEN_STYLES); + expect(dropdownToggle).not.toHaveStyle(HIDDEN_STYLES); +}); + +test('it executes the given callback on click', () => { + const onClick = jest.fn(); + const item1 = { + title: 'In stock', + id: 'filter1', + }; + const item2 = { + title: 'Not in stock', + id: 'filter2', + }; + const items = [item1, item2]; + + const { getByText } = render(); + const inStock = getByText('In stock'); + + fireEvent.click(inStock); + + expect(onClick).toHaveBeenCalledWith(item1.id); +}); + +test('cannot click on a hidden item', async () => { + Object.defineProperties(window.HTMLElement.prototype, { + offsetWidth: { + get() { + if (this.dataset.testid === 'pilltabs-wrapper') { + return 400; + } + + if (this.dataset.testid === 'pilltabs-dropdown-toggle') { + return 50; + } + + if (this.dataset.testid === 'pilltabs-pill-0') { + return 340; + } + + if (this.dataset.testid === 'pilltabs-pill-1') { + return 200; + } + + return parseFloat(this.style.width) || 50; + }, + }, + }); + const onClick = jest.fn(); + const item1 = { + title: 'In stock', + id: 'filter1', + }; + const item2 = { + title: 'Not in stock', + id: 'filter2', + }; + const items = [item1, item2]; + + const { getByText, getByTestId } = render(); + + await wait(); + const notInStock = getByText('Not in stock'); + const filter1 = getByTestId('pilltabs-pill-1'); + + fireEvent.click(notInStock); + + expect(filter1).toHaveStyle(HIDDEN_STYLES); + expect(onClick).not.toHaveBeenCalled(); +}); diff --git a/packages/big-design/src/components/PillTabs/styled.tsx b/packages/big-design/src/components/PillTabs/styled.tsx new file mode 100644 index 000000000..273c0d1bb --- /dev/null +++ b/packages/big-design/src/components/PillTabs/styled.tsx @@ -0,0 +1,34 @@ +import { theme as defaultTheme } from '@bigcommerce/big-design-theme'; +import styled, { css } from 'styled-components'; + +import { StyleableButton } from '../Button/private'; +import { FlexItem } from '../Flex'; + +interface StyledPillTabProps { + isActive: boolean; +} + +interface StyledFlexItemProps { + isVisible: boolean; +} + +export const StyledPillTab = styled(StyleableButton)` + ${(props) => + props.isActive && + css` + background-color: ${({ theme }) => theme.colors.primary20}; + `} +`; + +export const StyledFlexItem = styled(FlexItem)` + ${(props) => + !props.isVisible && + css` + position: absolute; + visibility: hidden; + z-index: ${({ theme }) => -theme.zIndex.tooltip}; + `} +`; + +StyledPillTab.defaultProps = { theme: defaultTheme }; +StyledFlexItem.defaultProps = { theme: defaultTheme }; diff --git a/packages/big-design/src/components/StatefulTable/StatefulTable.tsx b/packages/big-design/src/components/StatefulTable/StatefulTable.tsx index 682e0f5c7..0ac7fdd93 100644 --- a/packages/big-design/src/components/StatefulTable/StatefulTable.tsx +++ b/packages/big-design/src/components/StatefulTable/StatefulTable.tsx @@ -1,11 +1,17 @@ -import React, { useCallback, useMemo, useReducer } from 'react'; +import React, { useCallback, useEffect, useMemo, useReducer } from 'react'; import { useDidUpdate } from '../../hooks'; import { typedMemo } from '../../utils'; +import { PillTabItem, PillTabsProps } from '../PillTabs'; import { Table, TableColumn, TableItem, TableProps, TableSelectable, TableSortDirection } from '../Table'; import { createReducer, createReducerInit } from './reducer'; +export interface StatefulTablePillTabFilter { + pillTabs: PillTabItem[]; + filter(itemId: string, items: T[]): T[]; +} + export type StatefulTableSortFn = (a: T, b: T, direction: TableSortDirection) => number; export interface StatefulTableColumn extends Omit, 'isSortable'> { @@ -14,9 +20,10 @@ export interface StatefulTableColumn extends Omit, 'isSortable } export interface StatefulTableProps - extends Omit, 'columns' | 'pagination' | 'selectable' | 'sortable' | 'onRowDrop'> { + extends Omit, 'columns' | 'pagination' | 'filters' | 'selectable' | 'sortable' | 'onRowDrop'> { columns: Array>; pagination?: boolean; + filters?: StatefulTablePillTabFilter; selectable?: boolean; defaultSelected?: T[]; onRowDrop?(items: T[]): void; @@ -45,6 +52,7 @@ const InternalStatefulTable = ({ onSelectionChange, onRowDrop, pagination = false, + filters, selectable = false, stickyHeader = false, ...rest @@ -53,7 +61,7 @@ const InternalStatefulTable = ({ const reducerInit = useMemo(() => createReducerInit(), []); const sortable = useMemo(() => columns.some((column) => column.sortKey || column.sortFn), [columns]); - const [state, dispatch] = useReducer(reducer, { columns, defaultSelected, items, pagination }, reducerInit); + const [state, dispatch] = useReducer(reducer, { columns, defaultSelected, items, pagination, filters }, reducerInit); const columnsChangedCallback = useCallback(() => dispatch({ type: 'COLUMNS_CHANGED', columns }), [columns]); const itemsChangedCallback = useCallback( @@ -113,17 +121,34 @@ const InternalStatefulTable = ({ [state.currentItems, onRowDrop, pagination], ); + useEffect(() => { + if (!filters) { + return; + } + + const pillTabsProps: PillTabsProps = { + activePills: state.activePills, + onPillClick: (pillId) => { + dispatch({ type: 'TOGGLE_PILL', pillId, filter: filters.filter }); + }, + items: filters.pillTabs, + }; + + dispatch({ type: 'SET_PILL_TABS_PROPS', pillTabsProps }); + }, [filters, state.activePills]); + return ( ); diff --git a/packages/big-design/src/components/StatefulTable/reducer.ts b/packages/big-design/src/components/StatefulTable/reducer.ts index 75e100251..d6a774443 100644 --- a/packages/big-design/src/components/StatefulTable/reducer.ts +++ b/packages/big-design/src/components/StatefulTable/reducer.ts @@ -1,12 +1,15 @@ import { Reducer } from 'react'; +import { PillTabsProps } from '../PillTabs'; import { TableSortDirection } from '../Table'; -import { StatefulTableColumn, StatefulTableSortFn } from './StatefulTable'; +import { StatefulTableColumn, StatefulTablePillTabFilter, StatefulTableSortFn } from './StatefulTable'; interface State { + activePills: string[]; currentItems: T[]; columns: Array & { isSortable: boolean }>; + filteredItems: T[]; isPaginationEnabled: boolean; items: T[]; pagination: { @@ -15,6 +18,7 @@ interface State { itemsPerPageOptions: number[]; totalItems: number; }; + pillTabsProps?: PillTabsProps; selectedItems: T[]; sortable: { direction: TableSortDirection; @@ -35,8 +39,10 @@ export const createReducerInit = () => ({ columns, defaultSelected, items, pa const currentItems = getItems(items, pagination, { currentPage, itemsPerPage }); return { + activePills: [], currentItems, columns: augmentColumns(columns), + filteredItems: [], isPaginationEnabled: pagination, items, pagination: { @@ -58,7 +64,9 @@ export type Action = | { type: 'ITEMS_PER_PAGE_CHANGE'; itemsPerPage: number } | { type: 'PAGE_CHANGE'; page: number } | { type: 'SELECTED_ITEMS'; selectedItems: T[] } - | { type: 'SORT'; column: StatefulTableColumn; direction: TableSortDirection }; + | { type: 'SORT'; column: StatefulTableColumn; direction: TableSortDirection } + | { type: 'SET_PILL_TABS_PROPS'; pillTabsProps: PillTabsProps } + | { type: 'TOGGLE_PILL'; pillId: string; filter: StatefulTablePillTabFilter['filter'] }; export const createReducer = (): Reducer, Action> => (state, action) => { switch (action.type) { @@ -101,8 +109,10 @@ export const createReducer = (): Reducer, Action> => (state, acti } case 'PAGE_CHANGE': { + const filtersActive = state.activePills.length > 0; + const items = filtersActive ? state.filteredItems : state.items; const currentPage = action.page; - const currentItems = getItems(state.items, true, { + const currentItems = getItems(items, true, { currentPage, itemsPerPage: state.pagination.itemsPerPage, }); @@ -140,6 +150,10 @@ export const createReducer = (): Reducer, Action> => (state, acti return { ...state, selectedItems: action.selectedItems }; } + case 'SET_PILL_TABS_PROPS': { + return { ...state, pillTabsProps: action.pillTabsProps }; + } + case 'SORT': { const { hash, sortFn, sortKey } = action.column; const direction = action.direction; @@ -174,6 +188,48 @@ export const createReducer = (): Reducer, Action> => (state, acti }; } + case 'TOGGLE_PILL': { + if (!state.pillTabsProps) { + return state; + } + + const pillTabs = state.pillTabsProps.items; + const toggledPill = pillTabs.find((pill) => pill.id === action.pillId); + + if (!toggledPill) { + return state; + } + + const isToggledPillActive = !state.activePills.includes(action.pillId); + const activePills = isToggledPillActive + ? [...state.activePills, toggledPill.id] + : state.activePills.filter((activeFilter) => activeFilter !== toggledPill.id); + const currentItems = + activePills.length > 0 + ? activePills + .map((pillId) => { + return action.filter(pillId, state.items); + }) + .reduce((results, filteredItems) => { + const dedupedItems = filteredItems.filter((item) => !results.includes(item)); + + return [...results, ...dedupedItems]; + }, []) + : state.items; + + return { + ...state, + activePills, + pagination: { + ...state.pagination, + currentPage: 1, + totalItems: currentItems.length, + }, + currentItems, + filteredItems: currentItems, + }; + } + default: return state; } diff --git a/packages/big-design/src/components/StatefulTable/spec.tsx b/packages/big-design/src/components/StatefulTable/spec.tsx index 2b08e0948..54ae0aba1 100644 --- a/packages/big-design/src/components/StatefulTable/spec.tsx +++ b/packages/big-design/src/components/StatefulTable/spec.tsx @@ -3,7 +3,7 @@ import 'jest-styled-components'; import { fireEvent, render, waitForElement } from '@test/utils'; -import { StatefulTable, StatefulTableProps } from './StatefulTable'; +import { StatefulTable, StatefulTablePillTabFilter, StatefulTableProps } from './StatefulTable'; interface TestItem { name: string; @@ -324,3 +324,125 @@ test('renders headers by default and hides then via prop', () => { expect(container.querySelector('th')).toBeInTheDocument(); expect(container.querySelector('th')).not.toBeVisible(); }); + +test('renders pill tabs with the pillTabFilters prop', () => { + const filters: StatefulTablePillTabFilter = { + pillTabs: [{ title: 'In stock', id: 'in_stock' }], + filter: (_itemId, items) => items.filter((item) => item.stock > 0), + }; + const { getByText } = render(getSimpleTable({ filters })); + const customFilter = getByText('In stock'); + + expect(customFilter).toBeInTheDocument(); + expect(customFilter).toBeVisible(); +}); + +test('it executes the filter function on click', async () => { + const filters: StatefulTablePillTabFilter = { + pillTabs: [{ title: 'In stock', id: 'in_stock' }], + filter: (_itemId, items) => items.filter((item) => item.stock === 1), + }; + const { container, getByText, getByTestId } = render(getSimpleTable({ filters })); + + const customFilter = getByText('In stock'); + + fireEvent.click(customFilter); + await waitForElement(() => getByTestId('simple-table')); + + const rows = container.querySelectorAll('tbody > tr'); + + expect(rows.length).toBe(1); +}); + +test('can paginate on filtered rows', async () => { + const filters: StatefulTablePillTabFilter = { + pillTabs: [{ title: 'Low stock', id: 'in_stock' }], + filter: (_itemId, items) => items.filter((item) => item.stock < 40), + }; + const { getByText, getByTestId } = render(getSimpleTable({ filters, pagination: true })); + const customFilter = getByText('Low stock'); + + fireEvent.click(customFilter); + await waitForElement(() => getByTestId('simple-table')); + + const itemText = getByText('1 - 25 of 39'); + + expect(itemText).toBeInTheDocument(); +}); + +test('can undo filter actions', async () => { + const filters: StatefulTablePillTabFilter = { + pillTabs: [{ title: 'Stock 1', id: 'in_stock' }], + filter: (_itemId, items) => items.filter((item) => item.stock === 1), + }; + const { container, getByText, getByTestId } = render(getSimpleTable({ filters })); + const customFilter = getByText('Stock 1'); + + fireEvent.click(customFilter); + + let rows = container.querySelectorAll('tbody > tr'); + + expect(rows.length).toBe(1); + + fireEvent.click(customFilter); + await waitForElement(() => getByTestId('simple-table')); + + rows = container.querySelectorAll('tbody > tr'); + expect(rows.length).toBe(104); +}); + +test('can stack filter actions (OR logic)', async () => { + const filters: StatefulTablePillTabFilter = { + pillTabs: [ + { title: 'Stock 1', id: 'stock_1' }, + { title: 'Stock 2', id: 'stock_2' }, + ], + filter: (itemId, items) => + itemId === 'stock_1' ? items.filter((item) => item.stock === 1) : items.filter((item) => item.stock === 2), + }; + const { container, getByText, getByTestId } = render(getSimpleTable({ filters })); + const customFilter1 = getByText('Stock 1'); + const customFilter2 = getByText('Stock 2'); + + fireEvent.click(customFilter1); + await waitForElement(() => getByTestId('simple-table')); + fireEvent.click(customFilter2); + await waitForElement(() => getByTestId('simple-table')); + + const rows = container.querySelectorAll('tbody > tr'); + + expect(rows.length).toBe(2); +}); + +test('can undo stacked filter actions', async () => { + const filters: StatefulTablePillTabFilter = { + pillTabs: [ + { title: 'Stock 1', id: 'stock_1' }, + { title: 'Stock 2', id: 'stock_2' }, + ], + filter: (itemId, items) => + itemId === 'stock_1' ? items.filter((item) => item.stock === 1) : items.filter((item) => item.stock === 2), + }; + const { container, getByText, getByTestId } = render(getSimpleTable({ filters })); + const customFilter1 = getByText('Stock 1'); + const customFilter2 = getByText('Stock 2'); + + fireEvent.click(customFilter1); + fireEvent.click(customFilter2); + await waitForElement(() => getByTestId('simple-table')); + + let rows = container.querySelectorAll('tbody > tr'); + expect(rows.length).toBe(2); + + fireEvent.click(customFilter1); + await waitForElement(() => getByTestId('simple-table')); + + rows = container.querySelectorAll('tbody > tr'); + expect(rows.length).toBe(1); + + fireEvent.click(customFilter2); + await waitForElement(() => getByTestId('simple-table')); + + rows = container.querySelectorAll('tbody > tr'); + expect(rows.length).toBe(104); +}); diff --git a/packages/big-design/src/components/Table/Table.tsx b/packages/big-design/src/components/Table/Table.tsx index 028af5059..22fa6633f 100644 --- a/packages/big-design/src/components/Table/Table.tsx +++ b/packages/big-design/src/components/Table/Table.tsx @@ -4,6 +4,8 @@ import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautif import { useEventCallback, useUniqueId } from '../../hooks'; import { MarginProps } from '../../mixins'; import { typedMemo } from '../../utils'; +import { Box } from '../Box'; +import { PillTabs } from '../PillTabs'; import { Actions } from './Actions'; import { Body } from './Body'; @@ -16,10 +18,11 @@ import { TableColumn, TableItem, TableProps } from './types'; const InternalTable = (props: TableProps): React.ReactElement> => { const { + actions, className, columns, - actions, emptyComponent, + filters, headerless = false, id, itemName, @@ -206,8 +209,21 @@ const InternalTable = (props: TableProps): React.ReactEl return null; }; + const renderPillTabs = () => { + if (!filters) { + return; + } + + return ( + + + + ); + }; + return ( <> + {renderPillTabs()} {shouldRenderActions() && ( (
sku }, @@ -513,3 +518,52 @@ describe('draggable', () => { expect(onRowDrop).toHaveBeenCalledWith(0, 1); }); }); + +test('it renders pill tabs with the pillTabs prop', () => { + const items = [ + { + title: 'In Stock', + id: 'in_stock', + }, + { + title: 'Low Stock', + id: 'low_stock', + }, + ]; + const pillTabs: PillTabsProps = { + onPillClick: jest.fn(), + activePills: [], + items, + }; + const { getByText } = render(getSimpleTable({ pillTabs })); + const inStock = getByText('In Stock'); + const lowStock = getByText('Low Stock'); + + expect(inStock).toBeInTheDocument(); + expect(lowStock).toBeInTheDocument(); +}); + +test('it executes the given callback', () => { + const onPillClick = jest.fn(); + const items = [ + { + title: 'In Stock', + id: 'in_stock', + }, + { + title: 'Low Stock', + id: 'low_stock', + }, + ]; + const pillTabs: PillTabsProps = { + onPillClick, + activePills: [], + items, + }; + const { getByText } = render(getSimpleTable({ pillTabs })); + const inStockBtn = getByText('In Stock'); + + fireEvent.click(inStockBtn); + + expect(onPillClick).toHaveBeenCalledWith('in_stock'); +}); diff --git a/packages/big-design/src/components/Table/types.ts b/packages/big-design/src/components/Table/types.ts index dcbb8abb0..99724eef2 100644 --- a/packages/big-design/src/components/Table/types.ts +++ b/packages/big-design/src/components/Table/types.ts @@ -2,6 +2,7 @@ import { ReactNode } from 'react'; import { MarginProps } from '../../mixins'; import { PaginationProps } from '../Pagination'; +import { PillTabsProps } from '../PillTabs'; import { TableColumnDisplayProps } from './mixins'; @@ -45,6 +46,7 @@ export interface TableProps extends React.TableHTMLAttributes; diff --git a/packages/big-design/src/components/index.ts b/packages/big-design/src/components/index.ts index 3f8e9740e..2fd2dc5b7 100644 --- a/packages/big-design/src/components/index.ts +++ b/packages/big-design/src/components/index.ts @@ -21,6 +21,7 @@ export * from './Modal'; export * from './MultiSelect'; export * from './Pagination'; export * from './Panel'; +export * from './PillTabs'; export * from './Popover'; export * from './ProgressBar'; export * from './ProgressCircle'; diff --git a/packages/big-design/src/hooks/index.ts b/packages/big-design/src/hooks/index.ts index 471a22a55..5f3339f3b 100644 --- a/packages/big-design/src/hooks/index.ts +++ b/packages/big-design/src/hooks/index.ts @@ -4,4 +4,5 @@ export * from './useEventCallback'; export * from './useIsomorphicLayoutEffect'; export * from './useRafState'; export * from './useUniqueId'; +export * from './useWindowResizeListener'; export * from './useWindowSize'; diff --git a/packages/big-design/src/hooks/useWindowResizeListener.ts b/packages/big-design/src/hooks/useWindowResizeListener.ts new file mode 100644 index 000000000..92ea02e0f --- /dev/null +++ b/packages/big-design/src/hooks/useWindowResizeListener.ts @@ -0,0 +1,11 @@ +import { useEffect } from 'react'; + +export const useWindowResizeListener = (callback: () => void) => { + useEffect(() => { + window.addEventListener('resize', callback); + + return () => { + window.removeEventListener('resize', callback); + }; + }, [callback]); +}; diff --git a/packages/big-design/src/hooks/useWindowSize.ts b/packages/big-design/src/hooks/useWindowSize.ts index 307ba7cd6..d4ae82d51 100644 --- a/packages/big-design/src/hooks/useWindowSize.ts +++ b/packages/big-design/src/hooks/useWindowSize.ts @@ -1,8 +1,7 @@ -import { useEffect } from 'react'; - import { isClient } from '../utils'; import { useRafState } from './useRafState'; +import { useWindowResizeListener } from './useWindowResizeListener'; interface State { height: number; @@ -14,21 +13,14 @@ export const useWindowSize = () => { height: isClient ? window.innerHeight : -1, width: isClient ? window.innerWidth : -1, }); - - useEffect(() => { - const resizeHandler = () => { - setState({ - height: window.innerHeight, - width: window.innerWidth, - }); - }; - - window.addEventListener('resize', resizeHandler); - - return () => { - window.removeEventListener('resize', resizeHandler); - }; - }, [setState]); + const resizeHandler = () => { + setState({ + height: window.innerHeight, + width: window.innerWidth, + }); + }; + + useWindowResizeListener(resizeHandler); if (!isClient) { return state; diff --git a/packages/docs/PropTables/PillTabsPropTable.tsx b/packages/docs/PropTables/PillTabsPropTable.tsx new file mode 100644 index 000000000..f302ddb68 --- /dev/null +++ b/packages/docs/PropTables/PillTabsPropTable.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +import { NextLink, Prop, PropTable, PropTableWrapper } from '../components'; + +const pillTabsPropTable: Prop[] = [ + { + name: 'activePills', + types: 'string[]', + description: 'The currently active pill ids as an array of strings.', + }, + { + description: ( + <> + See below for usage. + + ), + name: 'items', + required: true, + types: PillTabItem[], + }, + { + name: 'onPillClick', + types: '(itemId: string) => void', + description: 'Function that will get called when a pill tab is clicked.', + required: true, + }, +]; + +export const PillTabsPropTable: React.FC = (props) => ( + +); + +const tabItemProps: Prop[] = [ + { + name: 'title', + types: 'string', + description: 'The text inside the Pill Tab Item.', + required: true, + }, + { + description: 'A unique identifier for the pill.', + name: 'id', + required: true, + types: 'string', + }, +]; + +export const PillTabItemPropTable: React.FC = (props) => ( + +); diff --git a/packages/docs/PropTables/StatefulTablePropTable.tsx b/packages/docs/PropTables/StatefulTablePropTable.tsx index f4df7c4fe..3c0f4ba4e 100644 --- a/packages/docs/PropTables/StatefulTablePropTable.tsx +++ b/packages/docs/PropTables/StatefulTablePropTable.tsx @@ -79,6 +79,15 @@ const statefulTableProps: Prop[] = [ types: '(items: any[]) => void', description: 'Callback called with updated items list once drag and drop action has been completed.', }, + { + name: 'filters', + types: Filters, + description: ( + <> + See below for usage. + + ), + }, ]; const tableColumnsProps: Prop[] = [ @@ -147,6 +156,27 @@ const tableColumnsProps: Prop[] = [ }, ]; +const filterProps: Prop[] = [ + { + description: ( + <> + An array of pill tab items to render in the table. See{' '} + + Pill Tabs + {' '} + for usage. + + ), + name: 'pillTabs', + types: 'PillTabItem[]', + }, + { + name: 'filter', + types: '(itemId: string, items: Item[]) => Item[]', + description: 'A function that takes the current items and filters them according to the desired functionality.', + }, +]; + export const StatefulTablePropTable: React.FC = (props) => ( ); @@ -154,3 +184,7 @@ export const StatefulTablePropTable: React.FC = (props) => ( export const StatefulTableColumnsPropTable: React.FC = (props) => ( ); + +export const StatefulTableFiltersPropTable: React.FC = (props) => ( + +); diff --git a/packages/docs/PropTables/TablePropTable.tsx b/packages/docs/PropTables/TablePropTable.tsx index bb5e9037f..5e10e7f4c 100644 --- a/packages/docs/PropTables/TablePropTable.tsx +++ b/packages/docs/PropTables/TablePropTable.tsx @@ -44,6 +44,15 @@ const tableProps: Prop[] = [ ), description: 'See pagination component for details.', }, + { + name: 'filters', + types: ( + + Pill Tabs + + ), + description: 'See Pill Tabs component for details.', + }, { name: 'selectable', types: Selectable, diff --git a/packages/docs/PropTables/index.ts b/packages/docs/PropTables/index.ts index 7e8f2bfde..e14016432 100644 --- a/packages/docs/PropTables/index.ts +++ b/packages/docs/PropTables/index.ts @@ -21,6 +21,7 @@ export * from './MultiSelectPropTable'; export * from './PaddingPropTable'; export * from './PaginationPropTable'; export * from './PanelPropTable'; +export * from './PillTabsPropTable'; export * from './PopoverPropTable'; export * from './ProgressBarPropTable'; export * from './ProgressCirclePropTable'; diff --git a/packages/docs/components/SideNav/SideNav.tsx b/packages/docs/components/SideNav/SideNav.tsx index d23215f7a..37567f3d6 100644 --- a/packages/docs/components/SideNav/SideNav.tsx +++ b/packages/docs/components/SideNav/SideNav.tsx @@ -101,6 +101,9 @@ export const SideNav: React.FC = () => { Link + + Pill Tabs + Radio diff --git a/packages/docs/next.config.js b/packages/docs/next.config.js index 1d4d15ec9..86463469e 100644 --- a/packages/docs/next.config.js +++ b/packages/docs/next.config.js @@ -49,6 +49,7 @@ module.exports = { '/padding': { page: '/Padding/PaddingPage' }, '/pagination': { page: '/Pagination/PaginationPage' }, '/panel': { page: '/Panel/PanelPage' }, + '/pill-tabs': { page: '/PillTabs/PillTabsPage' }, '/popover': { page: '/Popover/PopoverPage' }, '/progress-bar': { page: '/Progress/ProgressBarPage' }, '/progress-circle': { page: '/Progress/ProgressCirclePage' }, diff --git a/packages/docs/pages/PillTabs/PillTabsPage.tsx b/packages/docs/pages/PillTabs/PillTabsPage.tsx new file mode 100644 index 000000000..6b82aa72d --- /dev/null +++ b/packages/docs/pages/PillTabs/PillTabsPage.tsx @@ -0,0 +1,91 @@ +import { Flex, FlexItem, H0, H1, Link, Panel, PillTabs, Text } from '@bigcommerce/big-design'; +import React, { useState } from 'react'; + +import { Code, CodePreview } from '../../components'; +import { PillTabItemPropTable, PillTabsPropTable } from '../../PropTables'; + +const PillTabsPage = () => ( + <> + Pill Tabs + + + The Pill Tabs component is used to provide a set of filters that act on a collection of + items. For usage with tables, see their respective components. + + + + {/* jsx-to-string:start */} + {function Example() { + const [activePills, setActivePills] = useState([]); + const Card: React.FC<{ name: string; description: string }> = ({ name, description }) => ( + + + {name} + + + {description} + + + Install + + + ); + const items = [ + { title: 'Shipping', id: 'shipping' }, + { title: 'Orders', id: 'orders' }, + ]; + const onPillClick = (pillId: string) => { + const isPillActive = !activePills.includes(pillId); + const updatedPills = isPillActive + ? [...activePills, pillId] + : activePills.filter((activePillId) => activePillId !== pillId); + + setActivePills(updatedPills); + }; + const cards = [ + { + name: 'Shipping App Pro', + description: 'All your shipping needs in a one stop shop.', + type: 'shipping', + }, + { + name: 'Order Tracker Deluxe', + description: 'Track your orders across all your devices.', + type: 'orders', + }, + { + name: 'Expedited Shipper', + description: 'The best rush rates in the country.', + type: 'shipping', + }, + { + name: 'Inventory Wizard', + description: 'Inventory tracking app to cover all your needs.', + type: 'other', + }, + ]; + const isFiltered = Boolean(activePills.length); + const filteredCards = cards.filter((card) => activePills.includes(card.type)); + const appCards = isFiltered ? filteredCards : cards; + + return ( + + + + {appCards.map(({ name, description }) => ( + + ))} + + + ); + }} + {/* jsx-to-string:end */} + + +

API

+ + + +); + +export default PillTabsPage; diff --git a/packages/docs/pages/StatefulTable/StatefulTablePage.tsx b/packages/docs/pages/StatefulTable/StatefulTablePage.tsx index 6c55edee2..bae49167f 100644 --- a/packages/docs/pages/StatefulTable/StatefulTablePage.tsx +++ b/packages/docs/pages/StatefulTable/StatefulTablePage.tsx @@ -2,7 +2,7 @@ import { H0, H1, H2, StatefulTable, Text } from '@bigcommerce/big-design'; import React from 'react'; import { CodePreview, NextLink } from '../../components'; -import { StatefulTableColumnsPropTable, StatefulTablePropTable } from '../../PropTables'; +import { StatefulTableColumnsPropTable, StatefulTableFiltersPropTable, StatefulTablePropTable } from '../../PropTables'; const items = [ { sku: '3137737c', name: 'Rice - Wild', stock: 29 }, @@ -143,6 +143,7 @@ const StatefulTablePage = () => {

API

+

Examples

Usage with pagination, selection, and sorting.

@@ -185,6 +186,43 @@ const StatefulTablePage = () => { /> {/* jsx-to-string:end */} + +

Usage with filters

+ + + {/* jsx-to-string:start */} + sku }, + { header: 'Name', hash: 'name', render: ({ name }) => name }, + { header: 'Stock', hash: 'stock', render: ({ stock }) => stock }, + ]} + items={[ + { sku: 'SM13', name: '[Sample] Smith Journal 13', stock: 25 }, + { sku: 'DPB', name: '[Sample] Dustpan & Brush', stock: 34 }, + { sku: 'OFSUC', name: '[Sample] Utility Caddy', stock: 0 }, + { sku: 'CLC', name: '[Sample] Canvas Laundry Cart', stock: 2 }, + { sku: 'CGLD', name: '[Sample] Laundry Detergent', stock: 29 }, + ]} + filters={{ + filter: (pillId, items) => + pillId === 'low_stock' + ? items.filter((item) => item.stock !== 0 && item.stock < 10) + : items.filter((item) => item.stock === 0), + pillTabs: [ + { + id: 'low_stock', + title: 'Low Stock', + }, + { + id: 'out_of_stock', + title: 'Out of Stock', + }, + ], + }} + /> + {/* jsx-to-string:end */} + ); }; diff --git a/packages/docs/pages/Table/TablePage.tsx b/packages/docs/pages/Table/TablePage.tsx index e9c0d0634..fa30cd921 100644 --- a/packages/docs/pages/Table/TablePage.tsx +++ b/packages/docs/pages/Table/TablePage.tsx @@ -1,4 +1,4 @@ -import { H0, H1, Small, Table, TableFigure, TableItem, Text } from '@bigcommerce/big-design'; +import { H0, H1, PillTabsProps, Small, Table, TableFigure, TableItem, Text } from '@bigcommerce/big-design'; import React, { useEffect, useState } from 'react'; import { Code, CodePreview } from '../../components'; @@ -278,6 +278,43 @@ const TablePage = () => { }} {/* jsx-to-string:end */} + +

Usage with filters

+ + + {/* jsx-to-string:start */} + {function Example() { + const [items, setItems] = useState(data); + const [activePills, setActivePills] = useState([]); + const pillTabs: PillTabsProps = { + activePills, + onPillClick: (id) => { + const isFilterActive = !activePills.includes(id); + const newItems = isFilterActive ? items.filter((item) => item.stock < 10) : data; + const updatedPills = isFilterActive + ? [...activePills, id] + : activePills.filter((activePillId) => activePillId !== id); + + setItems(newItems); + setActivePills(updatedPills); + }, + items: [{ title: 'Low Stock', id: 'low_stock' }], + }; + + return ( +
sku, isSortable: true }, + { header: 'Name', hash: 'name', render: ({ name }) => name, isSortable: true }, + { header: 'Stock', hash: 'stock', render: ({ stock }) => stock, isSortable: true }, + ]} + items={items} + filters={pillTabs} + /> + ); + }} + {/* jsx-to-string:end */} + ); }; diff --git a/packages/docs/pages/_app.tsx b/packages/docs/pages/_app.tsx index 3acfd40b6..710a989b1 100644 --- a/packages/docs/pages/_app.tsx +++ b/packages/docs/pages/_app.tsx @@ -24,7 +24,7 @@ const gridTemplate = { `, tablet: ` ". nav main ." 1fr - / 1fr 210px minmax(min-content, 1050px) 1fr; + / 1fr 210px minmax(0, 1050px) 1fr; `, };