Skip to content

Commit

Permalink
Feat - Native Select / Select - Supporting additional content (#1159)
Browse files Browse the repository at this point in the history
  • Loading branch information
chris-cedrone-cengage authored Nov 29, 2023
1 parent 39fc389 commit c47fc18
Show file tree
Hide file tree
Showing 13 changed files with 610 additions and 57 deletions.
5 changes: 5 additions & 0 deletions .changeset/feat-selectChildren.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-magma-dom': minor
---

feat(Select/NativeSelect): New `additionalContent` prop to provide the ability to add extra content inline with the label similar to the Input component.
3 changes: 1 addition & 2 deletions packages/react-magma-dom/src/components/InputBase/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ function getIconButtonTransform(props) {
return position;
}

const IconButtonContainer = styled.span<{
export const IconButtonContainer = styled.span<{
iconPosition?: InputIconPosition;
inputSize?: InputSize;
theme: ThemeInterface;
Expand Down Expand Up @@ -623,7 +623,6 @@ export const InputBase = React.forwardRef<HTMLInputElement, InputBaseProps>(
const passwordBtnWidth = () => {
const btnWidth =
children?.props?.children?.[0]?.ref?.current?.offsetWidth;

if (typeof btnWidth === 'number') {
return btnWidth;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { CardBody } from '../Card/CardBody';
import { NativeSelect, NativeSelectProps } from '.';
import { Story, Meta } from '@storybook/react/types-6-0';
import { LabelPosition } from '../Label';
import { Tooltip } from '../Tooltip';
import { IconButton } from '../IconButton';
import { HelpIcon } from 'react-magma-icons';
import { ButtonSize, ButtonType, ButtonVariant } from '../Button';

const Template: Story<NativeSelectProps> = args => (
<NativeSelect {...args}>
Expand Down Expand Up @@ -53,6 +57,39 @@ Disabled.args = {
disabled: true,
};

const WithContentTemplate: Story<NativeSelectProps> = args => {
const helpLinkLabel = 'Learn more';
const onHelpLinkClick = () => {
alert('Help link clicked!');
};
return (
<NativeSelect
{...args}
additionalContent={
<Tooltip content={helpLinkLabel}>
<IconButton
aria-label={helpLinkLabel}
icon={<HelpIcon />}
onClick={onHelpLinkClick}
type={ButtonType.button}
size={ButtonSize.small}
variant={ButtonVariant.link}
/>
</Tooltip>
}
>
<option>Red</option>
<option>Green</option>
<option>Blue</option>
</NativeSelect>
);
};

export const WithContent = WithContentTemplate.bind({});
WithContent.args = {
...Default.args,
};

export const HasError = Template.bind({});
HasError.args = {
...Default.args,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import { magma } from '../../theme/magma';
import { NativeSelect } from '.';
import { render } from '@testing-library/react';
import { transparentize } from 'polished';
import { Tooltip } from '../Tooltip';
import { IconButton } from '../IconButton';
import { HelpIcon } from 'react-magma-icons';
import { ButtonSize, ButtonType, ButtonVariant } from '../Button';

describe('NativeSelect', () => {
const testId = 'test-id';

it('should find element by testId', () => {
const testId = 'test-id';
const { getByTestId } = render(
<NativeSelect testId={testId}></NativeSelect>
);
Expand All @@ -28,7 +33,6 @@ describe('NativeSelect', () => {
});

it('should render a disabled select', () => {
const testId = 'test-id';
const { getByTestId } = render(
<NativeSelect disabled testId={testId}></NativeSelect>
);
Expand All @@ -40,7 +44,6 @@ describe('NativeSelect', () => {
});

it('should render a disabled inverse select', () => {
const testId = 'test-id';
const { getByTestId } = render(
<NativeSelect disabled isInverse testId={testId}></NativeSelect>
);
Expand All @@ -52,7 +55,6 @@ describe('NativeSelect', () => {
});

it('should render a default border', () => {
const testId = 'test-id';
const { getByTestId } = render(
<NativeSelect testId={testId}></NativeSelect>
);
Expand All @@ -64,7 +66,6 @@ describe('NativeSelect', () => {
});

it('should render a default inverse border', () => {
const testId = 'test-id';
const { getByTestId } = render(
<NativeSelect isInverse testId={testId}></NativeSelect>
);
Expand All @@ -76,7 +77,6 @@ describe('NativeSelect', () => {
});

it('should render an error state', () => {
const testId = 'test-id';
const errorMessage = 'This is an error';
const { getByTestId, getByText } = render(
<NativeSelect errorMessage={errorMessage} testId={testId}></NativeSelect>
Expand All @@ -91,14 +91,9 @@ describe('NativeSelect', () => {
});

it('should render an inverse error state', () => {
const testId = 'test-id';
const errorMessage = 'This is an error';
const { getByTestId, getByText } = render(
<NativeSelect
errorMessage={errorMessage}
isInverse
testId={testId}
></NativeSelect>
<NativeSelect errorMessage={errorMessage} isInverse testId={testId} />
);

expect(getByTestId(testId).parentElement).toHaveStyleRule(
Expand All @@ -108,4 +103,61 @@ describe('NativeSelect', () => {

expect(getByText(errorMessage)).toBeInTheDocument();
});

describe('additional content', () => {
const helpLinkLabel = 'Learn more';

const onHelpLinkClick = () => {
alert('Help link clicked!');
};

it('Should accept additional content to the right of the native select label', () => {
const { getByTestId } = render(
<NativeSelect
testId={testId}
additionalContent={
<Tooltip content={helpLinkLabel}>
<IconButton
aria-label={helpLinkLabel}
icon={<HelpIcon />}
onClick={onHelpLinkClick}
testId="Icon Button"
type={ButtonType.button}
size={ButtonSize.small}
variant={ButtonVariant.link}
/>
</Tooltip>
}
/>
);
expect(getByTestId(testId)).toBeInTheDocument();
expect(getByTestId('Icon Button')).toBeInTheDocument();
});

it(`Should display additional content inline with the native select label when labelPosition is set to 'left'`, () => {
const { getByTestId } = render(
<NativeSelect
labelPosition="left"
testId={testId}
additionalContent={
<Tooltip content={helpLinkLabel}>
<IconButton
aria-label={helpLinkLabel}
icon={<HelpIcon />}
onClick={onHelpLinkClick}
testId="Icon Button"
type={ButtonType.button}
size={ButtonSize.small}
variant={ButtonVariant.link}
/>
</Tooltip>
}
/>
);
expect(getByTestId(`${testId}-form-field-container`)).toHaveStyleRule(
'display',
'flex'
);
});
});
});
131 changes: 103 additions & 28 deletions packages/react-magma-dom/src/components/NativeSelect/NativeSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import styled from '../../theme/styled';

import { css } from '@emotion/core';
import { inputBaseStyles, inputWrapperStyles } from '../InputBase';
import {
FormFieldContainer,
Expand All @@ -13,13 +13,18 @@ import { useIsInverse } from '../../inverse';
import { useGenerateId } from '../../utils';
import { ThemeInterface } from '../../theme/magma';
import { transparentize } from 'polished';
import { LabelPosition } from '../Label';

/**
* @children required
*/
export interface NativeSelectProps
extends Omit<FormFieldContainerBaseProps, 'inputSize'>,
React.SelectHTMLAttributes<HTMLSelectElement> {
/**
* Content above the select. For use with Icon Buttons to relay information.
*/
additionalContent?: React.ReactNode;
/**
* @internal
*/
Expand Down Expand Up @@ -75,9 +80,41 @@ const StyledNativeSelect = styled.select<{
}
`;

const StyledFormFieldContainer = styled(FormFieldContainer)<{
additionalContent?: React.ReactNode;
hasLabel?: boolean;
labelPosition?: LabelPosition;
}>`
display: ${props =>
props.labelPosition === LabelPosition.left ? 'flex' : ''};
${props =>
props.additionalContent &&
css`
flex: 1;
label {
display: flex;
justify-content: space-between;
align-items: center;
}
`}
`;

const StyledAdditionalContentWrapper = styled.div`
align-items: center;
display: flex;
label {
margin: 0 ${props => props.theme.spaceScale.spacing03} 0 0;
}
button {
margin: 0 0 0 ${props => props.theme.spaceScale.spacing03};
}
`;

export const NativeSelect = React.forwardRef<HTMLDivElement, NativeSelectProps>(
(props, ref) => {
const {
additionalContent,
children,
containerStyle,
disabled,
Expand All @@ -100,40 +137,78 @@ export const NativeSelect = React.forwardRef<HTMLDivElement, NativeSelectProps>(

const id = useGenerateId(defaultId);

const hasLabel = !!labelText;

// If the labelPosition is set to 'left' then a <div> wraps the FormFieldContainer, NativeSelectWrapper, and NativeSelect for proper styling alignment.
function AdditionalContentWrapper(props) {
if (
labelPosition === LabelPosition.left ||
(labelPosition === LabelPosition.top && !hasLabel)
) {
return (
<StyledAdditionalContentWrapper theme={theme}>
{props.children}
</StyledAdditionalContentWrapper>
);
}
return props.children;
}

function inlineContent() {
if (!labelText || labelPosition !== LabelPosition.top) {
return additionalContent;
}
}

return (
<FormFieldContainer
containerStyle={containerStyle}
errorMessage={errorMessage}
fieldId={id}
labelPosition={labelPosition}
labelStyle={labelStyle}
labelText={labelText}
labelWidth={labelWidth}
isInverse={isInverse}
helperMessage={helperMessage}
messageStyle={messageStyle}
ref={ref}
>
<StyledNativeSelectWrapper
disabled={disabled}
hasError={!!errorMessage}
<AdditionalContentWrapper labelPosition={labelPosition}>
<StyledFormFieldContainer
additionalContent={additionalContent}
containerStyle={containerStyle}
data-testId={testId && `${testId}-form-field-container`}
errorMessage={errorMessage}
fieldId={id}
hasLabel={!!labelText}
labelPosition={labelPosition}
labelStyle={labelStyle}
labelText={
labelPosition !== LabelPosition.left && additionalContent ? (
<>
{labelText}
{labelText && additionalContent}
</>
) : (
labelText
)
}
labelWidth={labelWidth}
isInverse={isInverse}
theme={theme}
helperMessage={helperMessage}
messageStyle={messageStyle}
ref={ref}
>
<StyledNativeSelect
data-testid={testId}
hasError={!!errorMessage}
<StyledNativeSelectWrapper
disabled={disabled}
id={id}
hasError={!!errorMessage}
isInverse={isInverse}
theme={theme}
{...other}
>
{children}
</StyledNativeSelect>
<DefaultDropdownIndicator disabled={disabled} />
</StyledNativeSelectWrapper>
</FormFieldContainer>
<StyledNativeSelect
data-testid={testId}
hasError={!!errorMessage}
disabled={disabled}
id={id}
isInverse={isInverse}
theme={theme}
{...other}
>
{children}
</StyledNativeSelect>
<DefaultDropdownIndicator disabled={disabled} />
</StyledNativeSelectWrapper>
</StyledFormFieldContainer>
{inlineContent()}
</AdditionalContentWrapper>
);
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { I18nContext } from '../../i18n';

export function MultiSelect<T>(props: MultiSelectProps<T>) {
const {
additionalContent,
ariaDescribedBy,
components: customComponents,
errorMessage,
Expand Down Expand Up @@ -160,6 +161,7 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {

return (
<SelectContainer
additionalContent={additionalContent}
descriptionId={ariaDescribedBy}
errorMessage={errorMessage}
getLabelProps={getLabelProps}
Expand Down
Loading

2 comments on commit c47fc18

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.