diff --git a/src/components/Dialog/BaseDialog/BaseDialog.tsx b/src/components/Dialog/BaseDialog/BaseDialog.tsx index b9f325277..7c45bd2f0 100644 --- a/src/components/Dialog/BaseDialog/BaseDialog.tsx +++ b/src/components/Dialog/BaseDialog/BaseDialog.tsx @@ -18,6 +18,8 @@ export const BaseDialog: FC = React.forwardRef( actionButtonOneProps, actionButtonTwoProps, actionButtonThreeProps, + closeButtonProps, + closeIcon = IconName.mdiClose, parent = document.body, visible, onClose, @@ -106,8 +108,10 @@ export const BaseDialog: FC = React.forwardRef( )} diff --git a/src/components/Dialog/BaseDialog/BaseDialog.types.ts b/src/components/Dialog/BaseDialog/BaseDialog.types.ts index 131dc9e90..358e0f833 100644 --- a/src/components/Dialog/BaseDialog/BaseDialog.types.ts +++ b/src/components/Dialog/BaseDialog/BaseDialog.types.ts @@ -1,6 +1,9 @@ import React, { Ref } from 'react'; import { OcBaseProps } from '../../OcBase'; import { ButtonProps } from '../../Button'; +import { IconName } from '../../Icon'; + +export type CloseButtonProps = Omit; type EventType = | React.KeyboardEvent @@ -22,6 +25,14 @@ export interface BaseDialogProps * Props for the third header action button */ actionButtonThreeProps?: ButtonProps; + /** + * Close button extra props + */ + closeButtonProps?: CloseButtonProps; + /** + * Close icon name + */ + closeIcon?: IconName; /** * Dialog is visible or not */ diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx index af24a31a9..1f9fb4604 100644 --- a/src/components/Dialog/Dialog.tsx +++ b/src/components/Dialog/Dialog.tsx @@ -12,6 +12,8 @@ export const Dialog: FC = React.forwardRef( actionButtonOneProps, actionButtonTwoProps, actionButtonThreeProps, + closeButtonProps, + closeIcon, parent = document.body, size = DialogSize.medium, headerClassNames, @@ -52,6 +54,8 @@ export const Dialog: FC = React.forwardRef( actionButtonOneProps={actionButtonOneProps} actionButtonTwoProps={actionButtonTwoProps} actionButtonThreeProps={actionButtonThreeProps} + closeButtonProps={closeButtonProps} + closeIcon={closeIcon} dialogClassNames={dialogClasses} headerClassNames={headerClasses} bodyClassNames={bodyClasses} diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index f75a6e16b..f427131e5 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -11,6 +11,8 @@ export const Modal: FC = React.forwardRef( actionButtonOneProps, actionButtonTwoProps, actionButtonThreeProps, + closeButtonProps, + closeIcon, size = ModalSize.medium, headerClassNames, bodyClassNames, @@ -51,6 +53,8 @@ export const Modal: FC = React.forwardRef( actionButtonOneProps={actionButtonOneProps} actionButtonTwoProps={actionButtonTwoProps} actionButtonThreeProps={actionButtonThreeProps} + closeButtonProps={closeButtonProps} + closeIcon={closeIcon} dialogWrapperClassNames={modalWrapperClassNames} dialogClassNames={modalClasses} headerClassNames={headerClasses} diff --git a/src/components/Panel/Panel.stories.tsx b/src/components/Panel/Panel.stories.tsx index 6f54e2a4c..eaf946c39 100644 --- a/src/components/Panel/Panel.stories.tsx +++ b/src/components/Panel/Panel.stories.tsx @@ -346,6 +346,33 @@ const Top_Story: ComponentStory = (args) => { export const Top = Top_Story.bind({}); +const Header_Actions_Story: ComponentStory = (args) => { + const [visible, setVisible] = useState(false); + return ( + <> + setVisible(true)} + /> + + setVisible(false)} + /> + + } + visible={visible} + onClose={() => setVisible(false)} + /> + + ); +}; + +export const Header_Actions = Header_Actions_Story.bind({}); + const panelArgs: Object = { size: PanelSize.small, visible: false, @@ -467,3 +494,19 @@ Top.args = { size: PanelSize.small, placement: 'top', }; + +Header_Actions.args = { + ...panelArgs, + actionButtonOneProps: { + iconProps: { path: IconName.mdiCogOutline }, + }, + actionButtonTwoProps: { + iconProps: { + path: IconName.mdiHistory, + }, + }, + actionButtonThreeProps: { + iconProps: { path: IconName.mdiDatabaseArrowDownOutline }, + }, + size: PanelSize.medium, +}; diff --git a/src/components/Panel/Panel.test.tsx b/src/components/Panel/Panel.test.tsx index 9d08f12a5..86295750f 100644 --- a/src/components/Panel/Panel.test.tsx +++ b/src/components/Panel/Panel.test.tsx @@ -3,8 +3,7 @@ import Enzyme, { mount, ReactWrapper } from 'enzyme'; import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import MatchMediaMock from 'jest-matchmedia-mock'; import { Panel } from './'; -import { create } from 'react-test-renderer'; -import { Tab, Tabs } from '../Tabs'; +import { IconName } from '../Icon'; Enzyme.configure({ adapter: new Adapter() }); @@ -65,4 +64,25 @@ describe('Panel', () => { wrapper.find('.panel-backdrop').at(0).simulate('click'); expect(onClose).toHaveBeenCalledTimes(2); }); + + test('panel header actions exist', () => { + wrapper.setProps({ + visible: true, + actionButtonOneProps: { + classNames: 'header-action-button-1', + iconProps: { path: IconName.mdiCogOutline }, + }, + actionButtonTwoProps: { + classNames: 'header-action-button-2', + iconProps: { path: IconName.mdiHistory }, + }, + actionButtonThreeProps: { + classNames: 'header-action-button-3', + iconProps: { path: IconName.mdiDatabaseArrowDownOutline }, + }, + }); + expect(wrapper.find('.header-action-button-1').length).toBeTruthy(); + expect(wrapper.find('.header-action-button-2').length).toBeTruthy(); + expect(wrapper.find('.header-action-button-3').length).toBeTruthy(); + }); }); diff --git a/src/components/Panel/Panel.tsx b/src/components/Panel/Panel.tsx index f285831d6..42060dcee 100644 --- a/src/components/Panel/Panel.tsx +++ b/src/components/Panel/Panel.tsx @@ -25,6 +25,9 @@ const PANEL_WIDTHS: Record = Object.freeze({ export const Panel = React.forwardRef( ( { + actionButtonOneProps, + actionButtonTwoProps, + actionButtonThreeProps, size = PanelSize.medium, visible = false, closable = true, @@ -108,14 +111,25 @@ export const Panel = React.forwardRef( const getHeader = (): JSX.Element => (
{title}
- {closable && ( - - )} + + {actionButtonThreeProps && ( + + )} + {actionButtonTwoProps && ( + + )} + {actionButtonOneProps && ( + + )} + {closable && ( + + )} +
); diff --git a/src/components/Panel/Panel.types.ts b/src/components/Panel/Panel.types.ts index d9d8366c8..a81cf960e 100644 --- a/src/components/Panel/Panel.types.ts +++ b/src/components/Panel/Panel.types.ts @@ -23,6 +23,18 @@ type EventType = export type CloseButtonProps = Omit; export interface PanelProps extends Omit, 'title'> { + /** + * Props for the first header action button + */ + actionButtonOneProps?: ButtonProps; + /** + * Props for the second header action button + */ + actionButtonTwoProps?: ButtonProps; + /** + * Props for the third header action button + */ + actionButtonThreeProps?: ButtonProps; /** * Autofocus on the panel on visible * @default true diff --git a/src/components/Panel/panel.module.scss b/src/components/Panel/panel.module.scss index 0e58ea290..260ade4d7 100644 --- a/src/components/Panel/panel.module.scss +++ b/src/components/Panel/panel.module.scss @@ -68,6 +68,14 @@ top: 0; display: flex; justify-content: space-between; + + &-buttons { + align-items: flex-end; + align-self: start; + height: fit-content; + justify-content: right; + white-space: nowrap; + } } .body { diff --git a/src/components/RadioButton/RadioButton.stories.tsx b/src/components/RadioButton/RadioButton.stories.tsx index 6c88c9bfd..ad4d07925 100644 --- a/src/components/RadioButton/RadioButton.stories.tsx +++ b/src/components/RadioButton/RadioButton.stories.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { Stories } from '@storybook/addon-docs'; import { ComponentStory, ComponentMeta } from '@storybook/react'; import { RadioButton, RadioButtonValue, RadioGroup } from './'; +import { Stack } from '../Stack'; export default { title: 'Radio Button', @@ -106,9 +107,23 @@ export default { }, } as ComponentMeta; -const RadioButton_Story: ComponentStory = (args) => ( - -); +const RadioButton_Story: ComponentStory = (args) => { + const [selected, setSelected] = useState('label1'); + + const radioChangeHandler = ( + e?: React.ChangeEvent + ): void => { + setSelected(e.target.value); + }; + + return ( + + ); +}; export const Radio_Button = RadioButton_Story.bind({}); @@ -128,6 +143,50 @@ const RadioGroup_Story: ComponentStory = (args) => { export const Radio_Group = RadioGroup_Story.bind({}); +const Bespoke_RadioGroup_Story: ComponentStory = (args) => { + const [selected, setSelected] = useState('label1'); + + const radioChangeHandler = ( + e?: React.ChangeEvent + ): void => { + setSelected(e.target.value); + }; + + return ( + + + + + + ); +}; + +export const Bespoke_Radio_Group = Bespoke_RadioGroup_Story.bind({}); + const radioButtonArgs: Object = { allowDisabledFocus: false, ariaLabel: 'Label', @@ -136,7 +195,7 @@ const radioButtonArgs: Object = { classNames: 'my-radiobutton-class', disabled: false, name: 'myRadioButtonName', - value: 'Label', + value: 'Label1', id: 'myRadioButtonId', }; @@ -155,3 +214,8 @@ Radio_Group.args = { })), layout: 'vertical', }; + +Bespoke_Radio_Group.args = { + ...radioButtonArgs, + name: 'roleGroupName', +}; diff --git a/src/components/RadioButton/RadioButton.tsx b/src/components/RadioButton/RadioButton.tsx index 695140464..50035f4c8 100644 --- a/src/components/RadioButton/RadioButton.tsx +++ b/src/components/RadioButton/RadioButton.tsx @@ -1,5 +1,5 @@ import React, { FC, Ref, useEffect, useRef, useState } from 'react'; -import { RadioButtonProps } from './'; +import { RadioButtonProps, RadioButtonValue } from './'; import { mergeClasses, generateId } from '../../shared/utilities'; import { useRadioGroup } from './RadioGroup.context'; @@ -27,6 +27,8 @@ export const RadioButton: FC = React.forwardRef( const [isActive, setIsActive] = useState( radioGroupContext?.value === value || checked ); + const [selectedValue, setSelectedValue] = + useState(value); const radioButtonClassNames: string = mergeClasses([ styles.radioButton, @@ -41,6 +43,10 @@ export const RadioButton: FC = React.forwardRef( { [styles.labelNoValue]: value === '' }, ]); + // TODO: Follow-up on React issue 24439 https://github.com/facebook/react/issues/24439 + // Bug: Checked attribute does not update in the dom - Meta folks are planning on + // updating this in a future React version, this could be an A11y issue so need + // to find a anti-pattern workaround. They closed the bug, but it's definitely still repro. useEffect(() => { setIsActive(radioGroupContext?.value === value); }, [radioGroupContext?.value]); @@ -49,9 +55,11 @@ export const RadioButton: FC = React.forwardRef( e: React.ChangeEvent ): void => { if (!radioGroupContext) { - setIsActive(!isActive); + setSelectedValue(e.currentTarget.value as RadioButtonValue); + } else { + radioGroupContext?.onChange?.(e); } - radioGroupContext?.onChange?.(e); + onChange?.(e); }; @@ -64,7 +72,11 @@ export const RadioButton: FC = React.forwardRef( `; +exports[`Storyshots Panel Header Actions 1`] = ` + +`; + exports[`Storyshots Panel Large 1`] = `