diff --git a/packages/ui-editable/package.json b/packages/ui-editable/package.json index 2ad8b7132e..985792d6bd 100644 --- a/packages/ui-editable/package.json +++ b/packages/ui-editable/package.json @@ -73,18 +73,18 @@ "default": "./es/exports/a.js" }, "./v11_7": { - "src": "./src/exports/a.ts", - "types": "./types/exports/a.d.ts", - "import": "./es/exports/a.js", - "require": "./lib/exports/a.js", - "default": "./es/exports/a.js" + "src": "./src/exports/b.ts", + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" }, "./latest": { - "src": "./src/exports/a.ts", - "types": "./types/exports/a.d.ts", - "import": "./es/exports/a.js", - "require": "./lib/exports/a.js", - "default": "./es/exports/a.js" + "src": "./src/exports/b.ts", + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" } } } diff --git a/packages/ui-editable/src/Editable/v1/README.md b/packages/ui-editable/src/Editable/v1/README.md index e0d39c2e8e..0705bf91c0 100644 --- a/packages/ui-editable/src/Editable/v1/README.md +++ b/packages/ui-editable/src/Editable/v1/README.md @@ -56,7 +56,7 @@ const Example = (props) => { } const renderViewer = () => { - return {value} + return {value} } const handleValueChange = (event) => { @@ -69,8 +69,10 @@ const Example = (props) => { const renderEditor = ({ onBlur, editorRef }) => { return ( - { + const [mode, setMode] = useState(props.mode || 'view') + const [value, setValue] = useState('This is some text') + const [inline, setInline] = useState(true) + + // You must provide this to Editable to be + // notified of mode changes + const handleChangeMode = (mode) => { + setMode(mode) + } + + // You attach an event handler to your edit component + // to be notified of value changes from user interactions + const handleChange = (event) => { + setValue(event.target.value) + } + + // Renders the view component + // Be sure to give it the current value + const renderView = () => ( + + {value || 'Enter some text'} + + ) + + // Renders the edit component. + // You have to forward the props on, which + // includes an onBlur property to help manage + // the mode changes. + // Be sure to give it the current value + const renderEdit = ({ onBlur, editorRef }) => ( + + ) + + // Renders the edit button. + // Leverage the default implementation provided by InPlaceEdit + const renderEditButton = (props) => { + props.label = `Edit title "${value}"` + return InPlaceEdit.renderDefaultEditButton(props) + } + + const onChangeLayout = (event) => { + setInline(event.target.checked) + } + + return ( + + + + + + + ) + } + + render() +``` + +A readOnly `InPlaceEdit` + +```js +--- +type: example +--- + const Example = (props) => { + const [mode, setMode] = useState(props.mode || 'view') + const [value, setValue] = useState('This is some text') + + // You must provide this to Editable to be + // notified of mode changes + const handleChangeMode = (mode) => { + setMode(mode) + } + + // You attach an event handler to your edit component + // to be notified of value changes from user interactions + const handleChange = (event) => { + setValue(event.target.value) + } + + // Renders the view component + // Be sure to give it the current value + const renderView = () => {value} + + // Renders the edit component. + // You have to forward the props on, which + // includes an onBlur property to help manage + // the mode changes. + // Be sure to give it the current value + const renderEdit = ({ onBlur, editorRef }) => ( + + ) + + // Renders the edit button. + // Leverage the default implementation provided by InPlaceEdit + const renderEditButton = (props) => { + props.label = `Edit title "${value}"` + return InPlaceEdit.renderDefaultEditButton(props) + } + + return ( + + ) + } + + render() +``` + +To edit end-justified text, wrap `` in a +`` component, as follows: + +```js +--- +type: example +--- + const Example = (props) => { + const [mode, setMode] = useState(props.mode || 'view') + const [value, setValue] = useState('This is some text') + + // You must provide this to Editable to be + // notified of mode changes + const handleChangeMode = (mode) => { + setMode(mode) + } + + // You attach an event handler to your edit component + // to be notified of value changes from user interactions + const handleChange = (event) => { + setValue(event.target.value) + } + + // Renders the view component + // Be sure to give it the current value + const renderView = () => {value} + + // Renders the edit component. + // You have to forward the props on, which + // includes an onBlur property to help manage + // the mode changes. + // Be sure to give it the current value + const renderEdit = ({ onBlur, editorRef }) => ( + + ) + + // Renders the edit button. + // Leverage the default implementation provided by InPlaceEdit + const renderEditButton = (props) => { + props.label = `Edit title "${value}"` + return InPlaceEdit.renderDefaultEditButton(props) + } + + return ( + + + + ) + } + + render() +``` + +Same as the first example, but notifies `Editable`'s `onChange` +when the user has finished editing and the value has changed. + +```js +--- +type: example +--- + const Example = (props) => { + const [mode, setMode] = useState(props.mode || 'view') + const [value, setValue] = useState('Edit me') + const [onChangeValue, setOnChangeValue] = useState(undefined) + + // typically provided by the application so it can + // be notified of value changes when the user is + // finished editing + const onChange = (newValue) => { + setOnChangeValue(newValue) + } + + // You must provide this to Editable to be + // notified of mode changes + const handleChangeMode = (mode) => { + setMode(mode) + } + + // You attach an event handler to your edit component + // to be notified of value changes from user interactions + const handleChange = (event) => { + setValue(event.target.value) + } + + // Renders the view component + // Be sure to give it the current value + const renderView = () => {value} + + // Renders the edit component. + // You have to forward the props on, which + // includes an onBlur property to help manage + // the mode changes. + // Be sure to give it the current value + const renderEdit = ({ onBlur, editorRef }) => ( + + ) + + // Renders the edit button. + // Leverage the default implementation provided by InPlaceEdit + const renderEditButton = (props) => { + props.label = `Edit title "${value}"` + return InPlaceEdit.renderDefaultEditButton(props) + } + + return ( + + +
+ + {onChangeValue !== undefined + ? `onChange said: ${onChangeValue}` + : `You haven't edited me yet!`} + +
+
+ ) + } + + render() +``` diff --git a/packages/ui-editable/src/InPlaceEdit/v1/__tests__/InPlaceEdit.test.tsx b/packages/ui-editable/src/InPlaceEdit/v2/__tests__/InPlaceEdit.test.tsx similarity index 100% rename from packages/ui-editable/src/InPlaceEdit/v1/__tests__/InPlaceEdit.test.tsx rename to packages/ui-editable/src/InPlaceEdit/v2/__tests__/InPlaceEdit.test.tsx diff --git a/packages/ui-editable/src/InPlaceEdit/v2/index.tsx b/packages/ui-editable/src/InPlaceEdit/v2/index.tsx new file mode 100644 index 0000000000..e90374e7d2 --- /dev/null +++ b/packages/ui-editable/src/InPlaceEdit/v2/index.tsx @@ -0,0 +1,205 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2018 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { Component } from 'react' + +import { Flex } from '@instructure/ui-flex/latest' +import { IconButton } from '@instructure/ui-buttons/latest' +import type { IconButtonProps } from '@instructure/ui-buttons/latest' +import { PencilInstUIIcon } from '@instructure/ui-icons' +import { logWarn as warn } from '@instructure/console' +import { createChainedFunction } from '@instructure/ui-utils' +import { withStyle } from '@instructure/emotion' +import { View } from '@instructure/ui-view/latest' + +import { Editable } from '../../Editable/v1' +import generateStyle from './styles' + +import { allowedProps } from './props' +import type { InPlaceEditProps } from './props' +import type { EditableRenderProps } from '../../Editable/v1/props' + +/** +--- +category: components +--- +**/ +@withStyle(generateStyle) +class InPlaceEdit extends Component { + static readonly componentId = 'InPlaceEdit' + static allowedProps = allowedProps + static defaultProps = { + readOnly: false, + showFocusRing: true, + inline: true, + editButtonPlacement: 'end' + } + + ref: Element | null = null + _editButtonRef: HTMLButtonElement | null = null + + handleRef = (el: Element | null) => { + this.ref = el + } + + constructor(props: InPlaceEditProps) { + super(props) + + warn( + props.readOnly ? props.mode === 'view' : true, + '[InPlaceEdit] When readOnly is true, mode is forced to "view"' + ) + } + + componentDidMount() { + this.props.makeStyles?.() + } + + componentDidUpdate() { + this.props.makeStyles?.() + } + + handleEditButtonRef = (el: HTMLButtonElement) => { + this._editButtonRef = el + } + + renderEditor({ + mode, + onBlur, + editorRef, + readOnly + }: ReturnType) { + const { showFocusRing, renderEditor } = this.props + const isEditMode = !readOnly && mode === 'edit' + + return isEditMode ? ( + + {renderEditor({ onBlur, editorRef })} + + ) : null + } + + renderViewer({ + readOnly, + mode + }: ReturnType) { + return readOnly || mode === 'view' ? this.props.renderViewer() : null + } + + renderEditButton({ + buttonRef, + ...rest + }: ReturnType) { + return this.props.renderEditButton({ + elementRef: createChainedFunction(this.handleEditButtonRef, buttonRef), + ...rest + }) + } + + // Render a default edit button, an icon button with the edit icon + // the margin makes room for the focus ring + static renderDefaultEditButton = ({ + isVisible, + readOnly, + label, + ...buttonProps + }: { + isVisible: boolean + readOnly?: boolean + label: string + } & Partial) => { + if (readOnly) { + return null + } + return ( + + {isVisible ? PencilInstUIIcon : null} + + ) + } + + renderAll = ({ + getContainerProps, + getViewerProps, + getEditorProps, + getEditButtonProps + }: EditableRenderProps) => { + const flexDir = + this.props.editButtonPlacement === 'start' ? 'row-reverse' : 'row' + const justifyItems = flexDir === 'row-reverse' ? 'end' : 'start' + const buttonMargin = + this.props.editButtonPlacement === 'start' + ? '0 xx-small 0 0' + : '0 0 0 xx-small' + return ( + + + {this.renderEditor(getEditorProps())} + {this.renderViewer(getViewerProps())} + + + {this.renderEditButton(getEditButtonProps())} + + + ) + } + + render() { + const { mode, value, onChange, onChangeMode, readOnly } = this.props + + return ( + + ) + } +} + +export default InPlaceEdit +export { InPlaceEdit } diff --git a/packages/ui-editable/src/InPlaceEdit/v2/props.ts b/packages/ui-editable/src/InPlaceEdit/v2/props.ts new file mode 100644 index 0000000000..44c8ad8ee3 --- /dev/null +++ b/packages/ui-editable/src/InPlaceEdit/v2/props.ts @@ -0,0 +1,146 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import type { WithStyleProps, ComponentStyle } from '@instructure/emotion' +import type { EditableProps } from '../../Editable/v1/props' +import React from 'react' + +type ExtendedRenderEditButton = { + elementRef?: (el: HTMLButtonElement) => void + onClick: () => void + onFocus: () => void + onBlur: () => void + isVisible: boolean + readOnly?: boolean +} + +type InPlaceEditOwnProps = { + /** + * Function to render the view mode component. + * It is the consumer's responsibility to provide the + * current value or children. + * + * Return value: + * - element: the viewer DOM sub-tree. + */ + renderViewer: () => React.ReactNode + /** + * Function to render the edit mode component + * It is the consumer's responsibility to provide the + * current value, and to attach the appropriate onChange + * event handler needed to capture the updated value. This + * new value must then be forwarded to the view mode component. + * + * Return value: + * - element: the editor DOM sub-tree. + */ + renderEditor: (data: { + onBlur: () => void + editorRef: (el: HTMLElement | null) => void + }) => React.ReactNode + /** + * Function to render the edit button. + * + * Parameters: + * - object: { isVisible, onClick, onFocus, onBlur, buttonRef } + * + * Return value: + * + * - element: the edit button DOM sub-tree + * + * If you choose to use the default edit button, add `label` to the + * incoming `props` parameter and call `InPlaceEdit.renderDefaultEditButton(props)` + * + * If you choose to render a custom button, attach the on* event handlers + * and set `buttonRef` as a `ref` type property on the `button` element. + * + * `isVisible` is a hint as to whether the button is _typically_ shown, + * but you're free to ignore it for your use-case. + */ + renderEditButton: (props: ExtendedRenderEditButton) => React.ReactNode | null + /** + * If `'view'`: the view component is rendered, + * if `'edit'`: the edit component is rendered + */ + mode: 'view' | 'edit' + /** + * Called when the component's mode changes + * Parameter: + * - newMode: string + */ + onChangeMode: EditableProps['onChangeMode'] + /** + * The current value. + * The value is managed by the consuming app, but we need to tell InPlaceEdit + * it's changed or it won't re-render + */ + value?: any + /** + * Called when Editable switches from edit to view mode and the value has changed. + * Parameter: + * - value: any + */ + onChange?: EditableProps['onChange'] + /** + * The mode is fixed as 'view' + */ + readOnly?: boolean + /** + * Show a focus outline when the input is focused + */ + showFocusRing?: boolean + /** + * Put the edit button before or after the view + */ + editButtonPlacement?: 'start' | 'end' + /** + * Render outermost element inline v. block + */ + inline?: boolean +} + +type PropKeys = keyof InPlaceEditOwnProps + +type AllowedPropKeys = Readonly> + +type InPlaceEditProps = InPlaceEditOwnProps & + WithStyleProps + +type InPlaceEditStyle = ComponentStyle<'inPlaceEdit'> +const allowedProps: AllowedPropKeys = [ + 'renderViewer', + 'renderEditor', + 'renderEditButton', + 'mode', + 'onChangeMode', + 'value', + 'onChange', + 'readOnly', + 'showFocusRing', + 'editButtonPlacement', + 'inline' +] + +export type { InPlaceEditProps, InPlaceEditStyle } +export { allowedProps } diff --git a/packages/ui-editable/src/InPlaceEdit/v2/styles.ts b/packages/ui-editable/src/InPlaceEdit/v2/styles.ts new file mode 100644 index 0000000000..b0c4f3dcd5 --- /dev/null +++ b/packages/ui-editable/src/InPlaceEdit/v2/styles.ts @@ -0,0 +1,58 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import type { InPlaceEditStyle } from './props' + +/** + * --- + * private: true + * --- + * Generates the style object from the theme and provided additional information + * @param {Object} componentTheme The theme variable object. + * @param {Object} props the props of the component, the style is applied to + * @param {Object} state the state of the component, the style is applied to + * @return {Object} The final style object, which will be used in the component + */ +const generateStyle = (): InPlaceEditStyle => { + return { + inPlaceEdit: { + label: 'inPlaceEdit', + boxSizing: 'border-box', + maxWidth: '100%', + position: 'relative', + overflow: 'visible', + direction: 'inherit', + margin: 0, + textDecoration: 'none' /* for links styled as buttons */, + textAlign: 'inherit', + userSelect: 'none', + touchAction: 'manipulation', + background: 'transparent', + border: 'none', + outline: 'none' + } + } +} + +export default generateStyle diff --git a/packages/ui-editable/src/exports/b.ts b/packages/ui-editable/src/exports/b.ts new file mode 100644 index 0000000000..e259e17dd7 --- /dev/null +++ b/packages/ui-editable/src/exports/b.ts @@ -0,0 +1,29 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2018 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +export { Editable } from '../Editable/v1' +export { InPlaceEdit } from '../InPlaceEdit/v2' + +export type { EditableProps } from '../Editable/v1/props' +export type { InPlaceEditProps } from '../InPlaceEdit/v2/props' diff --git a/packages/ui-text/src/Text/v2/styles.ts b/packages/ui-text/src/Text/v2/styles.ts index 28dc430ec4..7df46355e2 100644 --- a/packages/ui-text/src/Text/v2/styles.ts +++ b/packages/ui-text/src/Text/v2/styles.ts @@ -206,7 +206,6 @@ const generateStyle = ( borderRadius: 0, padding: 0, margin: 0, - color: 'inherit', height: 'auto', width: '100%', lineHeight: 'inherit',