diff --git a/packages/api-explorer/src/components/CopyLinkButton/CopyLinkButton.spec.tsx b/packages/api-explorer/src/components/CopyLinkButton/CopyLinkButton.spec.tsx new file mode 100644 index 000000000..9aca8b006 --- /dev/null +++ b/packages/api-explorer/src/components/CopyLinkButton/CopyLinkButton.spec.tsx @@ -0,0 +1,54 @@ +/* + + MIT License + + Copyright (c) 2021 Looker Data Sciences, 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 { renderWithTheme } from '@looker/components-test-utils' +import { screen, waitFor } from '@testing-library/react' +import React from 'react' +import userEvent from '@testing-library/user-event' +import { CopyLinkButton } from '../CopyLinkButton' + +describe('CopyLinkButton', () => { + test('it renders', () => { + renderWithTheme() + expect(screen.getByText('Copy link to this page view')).toBeInTheDocument() + }) + const mockClipboardCopy = jest + .fn() + .mockImplementation(() => Promise.resolve()) + Object.assign(navigator, { + clipboard: { + writeText: mockClipboardCopy, + }, + }) + test('it copies to clipboard', async () => { + jest.spyOn(navigator.clipboard, 'writeText') + renderWithTheme() + await waitFor(() => { + userEvent.click(screen.getByRole('button')) + expect(mockClipboardCopy).toHaveBeenCalledWith(location.href) + }) + }) +}) diff --git a/packages/api-explorer/src/components/CopyLinkButton/CopyLinkButton.tsx b/packages/api-explorer/src/components/CopyLinkButton/CopyLinkButton.tsx new file mode 100644 index 000000000..29ed56c18 --- /dev/null +++ b/packages/api-explorer/src/components/CopyLinkButton/CopyLinkButton.tsx @@ -0,0 +1,68 @@ +/* + + MIT License + + Copyright (c) 2021 Looker Data Sciences, 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 React, { useState } from 'react' +import { IconButton } from '@looker/components' +import { Link } from '@styled-icons/material-outlined/Link' +import styled from 'styled-components' + +interface DocumentInterfaceProps { + top: string + right: string + visible: boolean +} +export const CopyLinkButton = ({ + top, + right, + visible, +}: DocumentInterfaceProps) => { + const [title, CopyLinkTooltip] = useState('Copy link to this page view') + return ( + + { + CopyLinkTooltip('Copied to clipboard') + await navigator.clipboard.writeText(location.href) + }} + onMouseEnter={() => CopyLinkTooltip('Copy link to this section')} + size="small" + icon={} + label={title} + tooltipPlacement="bottom" + /> + + ) +} + +const CopyLink = styled('span')<{ + top: string + right: string + visible: boolean +}>` + position: absolute; + top: ${({ top }) => top}; + right: ${({ right }) => right}; + display: ${({ visible }) => (visible ? 'block' : 'none')}; +` diff --git a/packages/api-explorer/src/components/CopyLinkButton/index.ts b/packages/api-explorer/src/components/CopyLinkButton/index.ts new file mode 100644 index 000000000..98548aea9 --- /dev/null +++ b/packages/api-explorer/src/components/CopyLinkButton/index.ts @@ -0,0 +1,26 @@ +/* + + MIT License + + Copyright (c) 2021 Looker Data Sciences, 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 { CopyLinkButton } from './CopyLinkButton' diff --git a/packages/api-explorer/src/components/CopyLinkWrapper/CopyLinkWrapper.tsx b/packages/api-explorer/src/components/CopyLinkWrapper/CopyLinkWrapper.tsx new file mode 100644 index 000000000..0311b6e3c --- /dev/null +++ b/packages/api-explorer/src/components/CopyLinkWrapper/CopyLinkWrapper.tsx @@ -0,0 +1,76 @@ +/* + + MIT License + + Copyright (c) 2021 Looker Data Sciences, 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 React, { ReactNode, ReactNodeArray, useState, useCallback } from 'react' +import { IconButton, Space } from '@looker/components' +import { Link } from '@styled-icons/material-outlined/Link' + +interface CopyLinkWrapperProps { + visible?: boolean + children: ReactNode | ReactNodeArray +} + +const COPY_TO_CLIPBOARD = 'Copied to clipboard' +const COPY_TO_SECTION = 'Copy link to this section' + +export const CopyLinkWrapper = ({ + visible = true, + children, +}: CopyLinkWrapperProps) => { + const [showCopyLinkButton, setShowCopyLinkButton] = useState(false) + const [tooltipContent, setTooltipContent] = useState(COPY_TO_SECTION) + + const onCopyLink = useCallback(async () => { + setTooltipContent(COPY_TO_CLIPBOARD) + // TODO - this wont work in the extension - there are other mechanisms + await navigator.clipboard.writeText(location.href) + }, [setTooltipContent]) + + const onCopyButtonMouseLeave = useCallback(async () => { + setTooltipContent(COPY_TO_SECTION) + }, [setTooltipContent]) + + return ( + <> + setShowCopyLinkButton(true)} + onMouseLeave={() => setShowCopyLinkButton(false)} + width="100%" + > + {children} + {visible && showCopyLinkButton && ( + } + label={tooltipContent} + tooltipPlacement="bottom" + /> + )} + + + ) +} diff --git a/packages/api-explorer/src/components/CopyLinkWrapper/index.ts b/packages/api-explorer/src/components/CopyLinkWrapper/index.ts new file mode 100644 index 000000000..eda788cf6 --- /dev/null +++ b/packages/api-explorer/src/components/CopyLinkWrapper/index.ts @@ -0,0 +1,26 @@ +/* + + MIT License + + Copyright (c) 2021 Looker Data Sciences, 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 { CopyLinkWrapper } from './CopyLinkWrapper' diff --git a/packages/api-explorer/src/components/SideNav/SideNav.spec.tsx b/packages/api-explorer/src/components/SideNav/SideNav.spec.tsx index 17300ab7e..006564f76 100644 --- a/packages/api-explorer/src/components/SideNav/SideNav.spec.tsx +++ b/packages/api-explorer/src/components/SideNav/SideNav.spec.tsx @@ -117,6 +117,18 @@ describe('Search', () => { }) }) + test('renders and removes copy link button based on mouse hover', async () => { + renderWithRouterAndReduxProvider(, ['/3.1/methods']) + expect(screen.getByText('Copy link to this page view')).not.toBeVisible() + const searchPattern = 'embedsso' + const input = screen.getByLabelText('Search') + await userEvent.paste(input, searchPattern) + userEvent.hover(input) + expect(screen.getByText('Copy link to this page view')).toBeVisible() + userEvent.unhover(input) + expect(screen.getByText('Copy link to this page view')).not.toBeVisible() + }) + test('sets search default value from store on load', async () => { const searchPattern = 'embedsso' const store = createTestStore({ diff --git a/packages/api-explorer/src/components/SideNav/SideNav.tsx b/packages/api-explorer/src/components/SideNav/SideNav.tsx index 0074e71bb..33a4cd97d 100644 --- a/packages/api-explorer/src/components/SideNav/SideNav.tsx +++ b/packages/api-explorer/src/components/SideNav/SideNav.tsx @@ -34,6 +34,7 @@ import { TabPanels, useTabs, InputSearch, + Box2, } from '@looker/components' import type { SpecItem, @@ -51,6 +52,7 @@ import { SideNavMethodTags } from './SideNavMethodTags' import { SideNavTypeTags } from './SideNavTypeTags' import { useDebounce, countMethods, countTypes } from './searchUtils' import { SearchMessage } from './SearchMessage' +import { CopyLinkWrapper } from '../CopyLinkWrapper' interface SideNavState { tags: TagList @@ -95,6 +97,7 @@ export const SideNav: FC = ({ headless = false, spec }) => { const searchCriteria = useSelector(selectSearchCriteria) const searchPattern = useSelector(selectSearchPattern) const [pattern, setSearchPattern] = useState(searchPattern) + const [showCopyLinkButton, setShowCopyLinkButton] = useState(false) const debouncedPattern = useDebounce(pattern, 250) const [sideNavState, setSideNavState] = useState(() => ({ tags: spec?.api?.tags || {}, @@ -107,6 +110,7 @@ export const SideNav: FC = ({ headless = false, spec }) => { const handleInputChange = (value: string) => { setSearchPattern(value) + setShowCopyLinkButton(!!value) } useEffect(() => { @@ -160,18 +164,25 @@ export const SideNav: FC = ({ headless = false, spec }) => { return (