From 40708c7c63cb295828d0d3edb54119c26e734f33 Mon Sep 17 00:00:00 2001 From: nikitaorliak-cengage Date: Mon, 21 Oct 2024 10:31:13 +0200 Subject: [PATCH] feat(Tabs, NavTabs): Add support for overwriting `TextTransform`. (#1494) Co-authored-by: Nikita Orliak --- ...avTab-support-overwriting-textTransform.md | 5 + .../src/components/Drawer/Drawer.stories.tsx | 10 +- .../src/components/NavTabs/NavTab.tsx | 18 ++- .../components/NavTabs/NavTabs.stories.tsx | 122 +++++++++++++----- .../src/components/NavTabs/NavTabs.test.js | 24 ++++ .../src/components/NavTabs/NavTabs.tsx | 6 +- .../src/components/Tabs/Tab.tsx | 22 +++- .../src/components/Tabs/Tabs.stories.tsx | 8 +- .../src/components/Tabs/Tabs.test.js | 24 ++++ .../src/components/Tabs/Tabs.tsx | 15 ++- .../Tabs/TabsScrollSpyContainer.tsx | 3 +- .../src/components/Tabs/shared.ts | 7 +- packages/react-magma-dom/src/index.ts | 2 +- .../src/pages/api/nav-tabs.mdx | 70 +++++++++- .../react-magma-docs/src/pages/api/tabs.mdx | 59 ++++++++- 15 files changed, 337 insertions(+), 58 deletions(-) create mode 100644 .changeset/feat-navTab-support-overwriting-textTransform.md diff --git a/.changeset/feat-navTab-support-overwriting-textTransform.md b/.changeset/feat-navTab-support-overwriting-textTransform.md new file mode 100644 index 0000000000..ae24115d80 --- /dev/null +++ b/.changeset/feat-navTab-support-overwriting-textTransform.md @@ -0,0 +1,5 @@ +--- +'react-magma-dom': minor +--- + +feat(Tabs, NavTabs): Add support for overwriting `TextTransform`: add `textTransform` prop. diff --git a/packages/react-magma-dom/src/components/Drawer/Drawer.stories.tsx b/packages/react-magma-dom/src/components/Drawer/Drawer.stories.tsx index 025fe92677..8cc638a488 100644 --- a/packages/react-magma-dom/src/components/Drawer/Drawer.stories.tsx +++ b/packages/react-magma-dom/src/components/Drawer/Drawer.stories.tsx @@ -4,7 +4,7 @@ import { Button } from '../Button'; import { VisuallyHidden } from '../VisuallyHidden'; import { DrawerPosition } from './Drawer'; import { NavTab, NavTabs } from '../NavTabs'; -import { TabsOrientation } from '../Tabs'; +import { TabsOrientation } from '../Tabs/shared'; const info = { component: Drawer, @@ -23,7 +23,7 @@ export default info; export const Default = args => { const [showDrawer, setShowDrawer] = React.useState(false); - const buttonRef = React.useRef(); + const buttonRef = React.useRef(null); return ( <> @@ -31,7 +31,7 @@ export const Default = args => { header="Drawer Title" onClose={() => { setShowDrawer(false); - buttonRef.current.focus(); + buttonRef.current?.focus(); }} isOpen={showDrawer} closeAriaLabel="Close drawer" @@ -52,14 +52,14 @@ export const Default = args => { export const SiteNavigation = args => { const [showDrawer, setShowDrawer] = React.useState(false); - const buttonRef = React.useRef(); + const buttonRef = React.useRef(null); return ( <> { setShowDrawer(false); - buttonRef.current.focus(); + buttonRef.current?.focus(); }} isOpen={showDrawer} position={DrawerPosition.right} diff --git a/packages/react-magma-dom/src/components/NavTabs/NavTab.tsx b/packages/react-magma-dom/src/components/NavTabs/NavTab.tsx index 170e4e879a..9949b11b69 100644 --- a/packages/react-magma-dom/src/components/NavTabs/NavTab.tsx +++ b/packages/react-magma-dom/src/components/NavTabs/NavTab.tsx @@ -9,9 +9,9 @@ import { TabStyles, TabsIconPosition, } from '../Tabs'; -import { TabsOrientation } from '../Tabs/shared'; +import { TabsOrientation, TabsTextTransform } from '../Tabs/shared'; import { ThemeContext } from '../../theme/ThemeContext'; -import { omit, useForkedRef, XOR } from '../../utils'; +import { omit, resolveProps, useForkedRef, XOR } from '../../utils'; export interface BaseNavTabProps extends React.HTMLAttributes { @@ -37,6 +37,11 @@ export interface BaseNavTabProps * @default TabsOrientation.horizontal */ orientation?: TabsOrientation; + /** + * Determines whether the tab appears in all-caps + * @default TabsTextTransform.uppercase + */ + textTransform?: TabsTextTransform; /** * @internal */ @@ -91,6 +96,7 @@ const StyledTab = styled.a<{ isFullWidth?: boolean; isInverse?: boolean; orientation: TabsOrientation; + textTransform?: TabsTextTransform; theme: any; }>` ${TabStyles} @@ -137,7 +143,9 @@ export const NavTab = React.forwardRef( (props, forwardRef) => { let children; let component; - const { isActive, icon, isFocused, testId, to, ...other } = props; + const contextProps = React.useContext(NavTabsContext); + const resolvedProps = resolveProps(contextProps, props); + const { isActive, icon, isFocused, testId, to, ...other } = resolvedProps; const theme = React.useContext(ThemeContext); if (instanceOfNavComponentTab(props)) { @@ -154,7 +162,8 @@ export const NavTab = React.forwardRef( iconPosition, isInverse, isFullWidth, - } = React.useContext(NavTabsContext); + textTransform, + } = resolvedProps; const tabIconPosition = iconPosition ? iconPosition @@ -216,6 +225,7 @@ export const NavTab = React.forwardRef( iconPosition={tabIconPosition} isInverse={isInverse} orientation={orientation} + textTransform={textTransform} theme={theme} > {icon && ( diff --git a/packages/react-magma-dom/src/components/NavTabs/NavTabs.stories.tsx b/packages/react-magma-dom/src/components/NavTabs/NavTabs.stories.tsx index 04d6bca0c6..186ae535fc 100644 --- a/packages/react-magma-dom/src/components/NavTabs/NavTabs.stories.tsx +++ b/packages/react-magma-dom/src/components/NavTabs/NavTabs.stories.tsx @@ -1,52 +1,108 @@ import React from 'react'; -import { NavTabs } from '.'; +import { NavTabs, NavTabsProps } from '.'; import { NavTab } from './NavTab'; import { Card } from '../Card'; import { magma } from '../../theme/magma'; -import { Meta } from '@storybook/react/types-6-0'; +import { Meta, Story } from '@storybook/react/types-6-0'; import { AndroidIcon, EmailIcon, NotificationsIcon } from 'react-magma-icons'; +import { TabsAlignment, TabsBorderPosition, TabsIconPosition } from '../Tabs'; +import { TabsOrientation, TabsTextTransform } from '../Tabs/shared'; export default { component: NavTabs, title: 'NavTabs', + argTypes: { + alignment: { + control: { + type: 'select', + options: TabsAlignment, + }, + }, + borderPosition: { + control: { + type: 'select', + options: TabsBorderPosition, + }, + }, + iconPosition: { + control: { + type: 'select', + options: TabsIconPosition, + }, + }, + orientation: { + control: { + type: 'select', + options: TabsOrientation, + }, + }, + textTransform: { + control: { + type: 'select', + options: TabsTextTransform, + }, + }, + isInverse: { + control: { + type: 'boolean', + }, + }, + }, } as Meta; -export const Default = () => { +const Template: Story = args => { return ( - - - Current Page - - Link to Google - + + + + Current Page + + Link to Google + + ); }; -export const IconOnly = () => { +export const Default = Template.bind({}); +Default.args = {}; + +export const IconOnly: Story = args => { return ( - - } to="#" isActive /> - } to="#" /> - } to="#" /> - + + + } to="#" isActive /> + } to="#" /> + } + to="#" + /> + + ); }; -export const BackgroundColor = () => { +export const BackgroundColor: Story = args => { return ( - - - Current Page - - Link to Yahoo - + + + + Current Page + + Link to Yahoo + + ); }; -export const Inverse = () => { +const InverseTemplate: Story = args => { return ( - - + + Current Page @@ -55,18 +111,22 @@ export const Inverse = () => { ); }; +export const Inverse = InverseTemplate.bind({}); +Inverse.args = { isInverse: true }; -export const CustomTab = () => { +export const CustomTab: Story = args => { const Link = ({ to, children, ...rest }) => ( {children} ); return ( - - Main page} /> - FAQ} /> - About us} /> - + + + Main page} /> + FAQ} /> + About us} /> + + ); }; diff --git a/packages/react-magma-dom/src/components/NavTabs/NavTabs.test.js b/packages/react-magma-dom/src/components/NavTabs/NavTabs.test.js index 6db45f706c..6acb5e80a6 100644 --- a/packages/react-magma-dom/src/components/NavTabs/NavTabs.test.js +++ b/packages/react-magma-dom/src/components/NavTabs/NavTabs.test.js @@ -71,6 +71,30 @@ describe('NavTabs', () => { ); }); + it('should render navtabs with textTransform prop', () => { + const { container, rerender } = render( + + Tab 1 + + ); + + expect(container.querySelector('a')).toHaveStyleRule( + 'text-transform', + 'uppercase' + ); + + rerender( + + Tab 1 + + ); + + expect(container.querySelector('a')).toHaveStyleRule( + 'text-transform', + 'none' + ); + }); + it('should show icon in top position', () => { const testId = 'test-id'; diff --git a/packages/react-magma-dom/src/components/NavTabs/NavTabs.tsx b/packages/react-magma-dom/src/components/NavTabs/NavTabs.tsx index 80912dc50d..e594a4531e 100644 --- a/packages/react-magma-dom/src/components/NavTabs/NavTabs.tsx +++ b/packages/react-magma-dom/src/components/NavTabs/NavTabs.tsx @@ -10,7 +10,7 @@ import { Orientation, } from '../Tabs'; import { NavTabProps, NavTab } from './NavTab'; -import { TabsOrientation } from '../Tabs/shared'; +import { TabsOrientation, TabsTextTransform } from '../Tabs/shared'; import { Omit } from '../../utils'; import { ThemeContext } from '../../theme/ThemeContext'; import { ButtonNext, ButtonPrev } from '../Tabs/TabsScrollButtons'; @@ -25,6 +25,7 @@ interface NavTabsContextInterface { isInverse?: boolean; isFullWidth?: boolean; orientation?: TabsOrientation; + textTransform?: TabsTextTransform; } export const NavTabsContext = React.createContext({ @@ -33,6 +34,7 @@ export const NavTabsContext = React.createContext({ isInverse: false, isFullWidth: false, orientation: TabsOrientation.horizontal, + textTransform: TabsTextTransform.uppercase, }); export const NavTabs = React.forwardRef< @@ -47,6 +49,7 @@ export const NavTabs = React.forwardRef< iconPosition, isFullWidth, orientation, + textTransform, testId, ...rest } = props; @@ -128,6 +131,7 @@ export const NavTabs = React.forwardRef< isInverse: isInverse, isFullWidth, orientation, + textTransform: textTransform || TabsTextTransform.uppercase, }} > {navTabsChildren} diff --git a/packages/react-magma-dom/src/components/Tabs/Tab.tsx b/packages/react-magma-dom/src/components/Tabs/Tab.tsx index 99c1525d35..52370b166b 100644 --- a/packages/react-magma-dom/src/components/Tabs/Tab.tsx +++ b/packages/react-magma-dom/src/components/Tabs/Tab.tsx @@ -4,8 +4,8 @@ import { ThemeContext } from '../../theme/ThemeContext'; import { css } from '@emotion/react'; import isPropValid from '@emotion/is-prop-valid'; import { TabsIconPosition, TabsBorderPosition, TabsContext } from './Tabs'; -import { TabsOrientation } from './shared'; -import { useForkedRef } from '../../utils'; +import { TabsOrientation, TabsTextTransform } from './shared'; +import { resolveProps, useForkedRef } from '../../utils'; import { useForceUpdate } from '../../hooks/useForceUpdate'; import { TabsContainerContext } from './TabsContainer'; import { ThemeInterface } from '../../theme/magma'; @@ -18,6 +18,11 @@ export interface TabProps */ icon?: React.ReactElement | React.ReactElement[]; isInverse?: boolean; + /** + * Determines whether the tab appears in all-caps + * @default TabsTextTransform.uppercase + */ + textTransform?: TabsTextTransform; /** * @internal */ @@ -39,7 +44,7 @@ export const StyledTabsChild = styled('li', { orientation: TabsOrientation; theme: ThemeInterface; }>` - cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'}; + cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')}; flex-grow: 0; flex-shrink: ${props => (props.isFullWidth ? '1' : '0')}; height: ${props => (props.orientation === 'vertical' ? 'auto' : '100%')}; @@ -144,7 +149,7 @@ export const TabStyles = props => css` pointer-events: ${props.disabled ? 'none' : ''}; text-align: center; text-decoration: none; - text-transform: uppercase; + text-transform: ${props.textTransform}; width: ${props.isFullWidth ? '100%' : 'auto'}; ${props.orientation === 'vertical' && @@ -191,6 +196,7 @@ const StyledTab = styled('button', { shouldForwardProp: isPropValid })<{ isFullWidth?: boolean; isInverse?: boolean; orientation: TabsOrientation; + textTransform: TabsTextTransform; theme: ThemeInterface; }>` ${TabStyles} @@ -231,7 +237,9 @@ export const StyledIcon = styled.span<{ export const Tab = React.forwardRef( (props, forwardedRef) => { - const { children, icon, disabled, testId, ...rest } = props; + const contextProps = React.useContext(TabsContext); + const resolvedProps = resolveProps(contextProps, props); + const { children, icon, disabled, testId, ...rest } = resolvedProps; const { activeTabIndex } = React.useContext(TabsContainerContext); const { buttonRefArray, registerTabButton } = React.useContext(TabsContext); const ownRef = React.useRef(); @@ -254,7 +262,8 @@ export const Tab = React.forwardRef( iconPosition, isInverse, isFullWidth, - } = React.useContext(TabsContext); + textTransform, + } = resolvedProps; const handleClick = (index, e) => { changeHandler(index, e); @@ -304,6 +313,7 @@ export const Tab = React.forwardRef( role="tab" tabIndex={isActive ? 0 : -1} theme={theme} + textTransform={textTransform} > {icon && ( { expect(getByTestId('buttonPrev')).toBeDefined(); }); + it('should render tabs with textTransform prop', () => { + const { getByText, rerender } = render( + + + Tab 1 + Tab 2 + + + ); + + expect(getByText('Tab 1')).toBeInTheDocument(); + expect(getByText('Tab 2')).toBeInTheDocument(); + expect(getByText('Tab 1')).toHaveStyleRule('text-transform', 'uppercase'); + + rerender( + + Tab 1 + Tab 2 + + ); + + expect(getByText('Tab 1')).toHaveStyleRule('text-transform', 'none'); + }); + it('should render centered tabs', () => { const { container } = render( diff --git a/packages/react-magma-dom/src/components/Tabs/Tabs.tsx b/packages/react-magma-dom/src/components/Tabs/Tabs.tsx index e18e34250f..8d72cd44f9 100644 --- a/packages/react-magma-dom/src/components/Tabs/Tabs.tsx +++ b/packages/react-magma-dom/src/components/Tabs/Tabs.tsx @@ -9,6 +9,7 @@ import { ThemeInterface } from '../../theme/magma'; import { ButtonNext, ButtonPrev } from './TabsScrollButtons'; import { useTabsMeta } from './utils'; import { I18nContext } from '../../i18n'; +import { TabsOrientation, TabsTextTransform } from './shared'; export enum TabsAlignment { center = 'center', @@ -30,11 +31,6 @@ export enum TabsIconPosition { top = 'top', } -export enum TabsOrientation { - horizontal = 'horizontal', - vertical = 'vertical', -} - export interface VerticalTabsProps { orientation?: TabsOrientation.vertical; borderPosition?: TabsBorderPosition.left | TabsBorderPosition.right; @@ -79,6 +75,11 @@ export interface TabsProps * @default TabsOrientation.horizontal */ orientation?: TabsOrientation; + /** + * Determines whether the tab appears in all-caps + * @default TabsTextTransform.uppercase + */ + textTransform?: TabsTextTransform; /** * @internal */ @@ -96,6 +97,7 @@ interface TabsContextInterface { isInverse?: boolean; isFullWidth?: boolean; orientation?: TabsOrientation; + textTransform?: TabsTextTransform; registerTabButton: ( itemRefArray: React.MutableRefObject[]>, itemRef: React.MutableRefObject @@ -109,6 +111,7 @@ export const TabsContext = React.createContext({ isInverse: false, isFullWidth: false, orientation: TabsOrientation.horizontal, + textTransform: TabsTextTransform.uppercase, registerTabButton: (elements, element) => {}, }); @@ -179,6 +182,7 @@ export const Tabs = React.forwardRef( onChange, iconPosition, testId, + textTransform, ...rest } = props; @@ -443,6 +447,7 @@ export const Tabs = React.forwardRef( isInverse, isFullWidth, orientation, + textTransform: textTransform || TabsTextTransform.uppercase, registerTabButton, }} > diff --git a/packages/react-magma-dom/src/components/Tabs/TabsScrollSpyContainer.tsx b/packages/react-magma-dom/src/components/Tabs/TabsScrollSpyContainer.tsx index 877cb81f90..a6fa3ca336 100644 --- a/packages/react-magma-dom/src/components/Tabs/TabsScrollSpyContainer.tsx +++ b/packages/react-magma-dom/src/components/Tabs/TabsScrollSpyContainer.tsx @@ -1,10 +1,11 @@ import React, { useCallback } from 'react'; import styled from '@emotion/styled'; import { ScrollSpy } from './utils'; -import { Tabs, TabsOrientation } from './Tabs'; +import { Tabs } from './Tabs'; import { Tab } from './Tab'; import { TabsContainer } from './TabsContainer'; import { toCamelCase } from '../../utils'; +import { TabsOrientation } from './shared'; export interface TabsScrollSpyContainerProps extends React.HTMLAttributes { diff --git a/packages/react-magma-dom/src/components/Tabs/shared.ts b/packages/react-magma-dom/src/components/Tabs/shared.ts index 68958eebd9..40bf6edbde 100644 --- a/packages/react-magma-dom/src/components/Tabs/shared.ts +++ b/packages/react-magma-dom/src/components/Tabs/shared.ts @@ -1,4 +1,9 @@ export enum TabsOrientation { - horizontal = 'horizontal', + horizontal = 'horizontal', // default vertical = 'vertical', } + +export enum TabsTextTransform { + uppercase = 'uppercase', // default + none = 'none', +} diff --git a/packages/react-magma-dom/src/index.ts b/packages/react-magma-dom/src/index.ts index 2c8d6e5ed3..e94d4e3a98 100644 --- a/packages/react-magma-dom/src/index.ts +++ b/packages/react-magma-dom/src/index.ts @@ -221,8 +221,8 @@ export { TabsAlignment, TabsBorderPosition, TabsIconPosition, - TabsOrientation, } from './components/Tabs'; +export { TabsOrientation, TabsTextTransform } from './components/Tabs/shared'; export { Tab } from './components/Tabs/Tab'; export { TabPanelsContainer } from './components/Tabs/TabPanelsContainer'; export { TabPanel } from './components/Tabs/TabPanel'; diff --git a/website/react-magma-docs/src/pages/api/nav-tabs.mdx b/website/react-magma-docs/src/pages/api/nav-tabs.mdx index 603b749bbd..0a6619d3a5 100644 --- a/website/react-magma-docs/src/pages/api/nav-tabs.mdx +++ b/website/react-magma-docs/src/pages/api/nav-tabs.mdx @@ -73,7 +73,7 @@ import React from 'react'; import { NavTab, NavTabs } from 'react-magma-dom'; export function Example() { // We support Link components from libraries such as react-router/reach-router/gatsby that use the `to` prop in place of `href` - const Link = ({ to, children, ...rest }) => ( + const Link = ({ to, children, ...rest }) => ( {children} @@ -187,6 +187,65 @@ export function Example() { } ``` +## Text Transform + +#### For all NavTabs + +Options for the `textTransform` prop for navtabs include `uppercase` and `none`, with `uppercase` (all caps) being the default value. +This sets the CSS `text-transform` property. + +```tsx +import React from 'react'; +import { NavTabs, NavTab, TabsTextTransform } from 'react-magma-dom'; + +export function Example() { + return ( + <> + + + First uppercase + + Second uppercase + +
+
+
+ + + First lowercase + + Second lowercase + + + ); +} +``` + +#### For a certain Tab + +`textTransform` prop can also be used for `NavTab` component. + +```tsx +import React from 'react'; +import { NavTabs, NavTab, TabsTextTransform } from 'react-magma-dom'; + +export function Example() { + return ( + + + All caps (default) + + + No Text Transform + + + Uppercase transform + + + ); +} +``` + ## Full-Width The `isFullWidth` prop is an optional boolean that will cause the tabs to take up the full width of their container. The `isFullWidth` prop is specified on the `NavTabs` component. @@ -496,6 +555,15 @@ All of the [global HTML attributes](https://developer.mozilla.org/en-US/docs/Web 'If true, the component will have inverse styling to better appear on a dark background', defaultValue: 'false', }, + textTransform: { + type: { + name: 'enum', + options: ['TabsTextTransform.uppercase', 'TabsTextTransform.none'], + }, + required: false, + description: 'Determines whether the tab appears in all-caps', + defaultValue: 'TabsTextTransform.uppercase', + }, }} /> diff --git a/website/react-magma-docs/src/pages/api/tabs.mdx b/website/react-magma-docs/src/pages/api/tabs.mdx index ce73bbf7d8..55fc4d9272 100644 --- a/website/react-magma-docs/src/pages/api/tabs.mdx +++ b/website/react-magma-docs/src/pages/api/tabs.mdx @@ -424,7 +424,64 @@ export function Example() { } ``` -## Full-Width +## Text Transform + +#### For all Tabs + +Options for the `textTransform` prop for tabs include `uppercase` and `none`, with `uppercase` (all caps) being the default value. +This sets the CSS `text-transform` property. + +```tsx +import React from 'react'; +import { Tabs, Tab, TabsContainer, TabsTextTransform } from 'react-magma-dom'; + +export function Example() { + return ( + <> + + + First uppercase + Second uppercase + + +
+
+
+ + + First lowercase + Second lowercase + + + + ); +} +``` + +#### For a certain Tab + +`textTransform` prop can also be used for `Tab` component. + +```tsx +import React from 'react'; +import { Tabs, Tab, TabsContainer, TabsTextTransform } from 'react-magma-dom'; + +export function Example() { + return ( + + + All caps (default) + No Text Transform + + Uppercase transform + + + + ); +} +``` + +## Full Width The `isFullWidth` prop is an optional boolean that will cause the tabs to take up the full width of their container. The `isFullWidth` prop is specified on the `Tabs` component.