diff --git a/.changeset/chatty-ears-exercise.md b/.changeset/chatty-ears-exercise.md new file mode 100644 index 0000000000..eba707373b --- /dev/null +++ b/.changeset/chatty-ears-exercise.md @@ -0,0 +1,152 @@ +--- +'@leafygreen-ui/code': major +--- + +## What's new? + +### `panel` + +Adds a new slot prop, `panel`, that accepts the new `` sub-component. This will render the top panel with a language switcher, custom action buttons, and copy button. If no props are passed to the panel sub-component, the panel will render with only the copy button. + +**_Note: `copyButtonAppearance` cannot be used with `panel`. Either use `copyButtonAppearance` or `panel`, not both._** + +e.g. + +```js +panel={ + {}} + languageOptions={[]} + showCustomActionButtons + customActionButtons={[]} + title="Title" + /> +} +``` +or +```js +panel={} +``` + +### `copyButtonAppearance` +Adds a new prop, `copyButtonAppearance`. This prop determines the appearance of the copy button if the `panel` prop is not defined. If `panel` is defined, this prop will be ignored. + +If `hover`, the copy button will only appear when the user hovers over the code block. On mobile devices, the copy button will always be visible. + +If `persist`, the copy button will always be visible. + +If `none`, the copy button will not be rendered. + +**_Note: 'panel' cannot be used with `copyButtonAppearance`. Either use `copyButtonAppearance` or `panel`, not both._** + +e.g. + +```js + + {snippet} + +``` + +### `isLoading` +Adds a new prop, `isLoading`. This prop determines whether or not the loading skeleton will be rendered in place of the code block. If `true`, the language switcher and copy button will be disabled in the top panel. + +e.g. + +```js + + {snippet} + +``` + + +### `chromeTitle` + +`` accepts the [deprecated `Code` props](https://github.com/mongodb/leafygreen-ui/tree/main/packages/code#deprecated) listed below, with one key difference: the `chromeTitle` prop has been replaced by `title`. Instead of rendering inside the window chrome bar, the `title` now appears within the top panel, as the window chrome bar has been removed. + +e.g. + +**Before**: +```js +{snippet} +``` + +**After**: +```js +} +> + {snippet} + +``` + +### Test Harnesses +Exports `getTestUtils`, a util to reliably interact with LG `Code` in a product test suite. For more details, check out the [README](https://github.com/mongodb/leafygreen-ui/tree/main/packages/code#test-harnesses) [LG-4799](https://jira.mongodb.org/browse/LG-4799) + +Exports `getLgIds`, a util to instantiate an object of `data-lgid` values for a given LG `Code` component instance. + +### `baseFontSize` +Adds `baseFontSize` prop, which allows you to override the `LeafyGreenProvider` value. + +## What's changed? + +The `className` prop is no longer applied to the `
` tag. Instead it is applied to the parent `
` wrapper. + + +## Deprecated + +The following props have been marked as `deprecated`: +- `customActionButtons` +- `showCustomActionButtons` +- `chromeTitle` +- `languageOptions` +- `onChange` +- `copyable` + +Moving forward these props should be passed to the new sub-component, ``. + +**Before**: +```js + {}} + darkMode={true} + onChange={() => {}} + languageOptions={[]} + showCustomActionButtons + customActionButtons={[]} + chromeTitle='Title' +> + {snippet} + +``` + +**After**: +```js + {}} + darkMode={true} + // NEW PANEL PROP + panel={ + {}} + languageOptions={[]} + showCustomActionButtons + customActionButtons={[]} + title="Title" + /> + } +> + {snippet} + +``` + +Check out the [README](https://github.com/mongodb/leafygreen-ui/tree/main/packages/code#panel) for information on the `` sub-component. \ No newline at end of file diff --git a/.changeset/cool-bottles-kick.md b/.changeset/cool-bottles-kick.md new file mode 100644 index 0000000000..45049080cb --- /dev/null +++ b/.changeset/cool-bottles-kick.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/lib': minor +--- + +Adds `getMobileMediaQuery` helper. This helper targets a specified screen size with no pointer, or a coarse pointer and no hover capability diff --git a/.changeset/soft-experts-dance.md b/.changeset/soft-experts-dance.md new file mode 100644 index 0000000000..87a4f72a00 --- /dev/null +++ b/.changeset/soft-experts-dance.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/select': minor +--- + +Exports `type GetTestUtilsReturnType`. \ No newline at end of file diff --git a/.changeset/tame-timers-exist.md b/.changeset/tame-timers-exist.md new file mode 100644 index 0000000000..2eed8c9fdd --- /dev/null +++ b/.changeset/tame-timers-exist.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/button': minor +--- + +Exports `type GetTestUtilsReturnType`. diff --git a/.changeset/wild-insects-remain.md b/.changeset/wild-insects-remain.md new file mode 100644 index 0000000000..284794e76b --- /dev/null +++ b/.changeset/wild-insects-remain.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/select': patch +--- + +Internally updates `MobileMediaQuery` to use `getMobileMediaQuery` from `@leafygreen-ui/li` diff --git a/packages/code/README.md b/packages/code/README.md index 5d4e4f7fff..cdc6fcfe8b 100644 --- a/packages/code/README.md +++ b/packages/code/README.md @@ -48,77 +48,74 @@ function greeting(entity) { console.log(greeting('World')); `; -const SomeComponent = () => {codeSnippet}; -``` +const SomeComponentWithPanel = () => ( + {}} + darkMode={true} + panel={ + {}} + languageOptions={[]} + showCustomActionButtons + customActionButtons={[]} + title="Title" + /> + } + > + {codeSnippet} + +); -**Output HTML** - -```html -
-
-
-			
-				
-					function
-					greeting
-					(
-					entity
-					)
-				
-				{
-				return
-				
-					`Hello, ${entity}!`
-				;
-				}
-				console
-				.log(greeting(
-				'World'));
-			
-		
-
- -
-
-
+const SomeComponentWithoutPanel = () => ( + {}} + darkMode={true} + copyButtonAppearance="persist" + > + {codeSnippet} + +); ``` -## Properties - -| Prop | Type | Description | Default | -| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| `children` (Required) | `string` | This is the code snippet that will be rendered in the code block. | `''` | -| `language` (Required) | `'javascript'`, `'typescript'`, `'cs'`, `'csharp'`, `'cpp'`, `'go'`, `'http'`,`'java'`, `'perl'`, `'php'`, `'python'`, `'ruby'`, `'scala'`, `'swift'`, `'kotlin'`,`'objectivec'`, `'dart'`, `'bash'`, `'shell'`, `'sql'`, `'yaml'`, `'json'`, `'diff'`, `'xml'`, `'none'` | The language to highlight the code block as. When set to `'none'`, no syntax highlighting will be applied. | | -| `darkMode` | `boolean` | Determines whether or not the component will appear in dark mode. | `false` | -| `className` | `string` | Applies a className to the root element's classList. | | -| `showLineNumbers` | `boolean` | Shows line numbers next to each line of code in the passed code snippet. **NOTE:** While you can set this to `true` regardless of the code component being multiline, the line numbers will not be displayed if the `multiline` prop is `true`. | `false` | -| `lineNumberStart` | `number` | Specifies the number by which to start line numbering. | `1` | -| `showWindowChrome` | `boolean` | Shows a stylized window chrome frame around the code snippet. This is purely stylistic. | `false` | -| `chromeTitle` | `string` | Shows a filename-like title in the window chrome frame.**NOTE:** While you can set this prop if `showWindowChrome` is `false`, it will not be displayed unless the `showWindowChrome` prop is `true`. | `''` | -| `showCustomActionButtons` | `boolean` | Shows custom action buttons in the panel if set to `true` and there is at least one item in `customActionButtons` | `false` | -| `customActionButtons` | `Array` | An array of custom action buttons using the `IconButton` component. For example: `[, ]` | `[]` | -| `onCopy` | `Function` | Callback fired when Code is copied | | -| `copyable` | `boolean` | When true, allows the code block to be copied to the user's clipboard | `true` | -| `expandable` | `boolean` | When true, allows the code block to be expanded and collapsed when there are more than 5 lines of code. | `false` | -| `highlightLines` | `Array` | An optional array of lines to highlight. The array can only contain numbers corresponding to the line numbers to highlight, and / or tuples representing a range (e.g. `[6, 10]`); | | -| `languageOptions` | `Array` (see below) | An array of language options. When provided, a LanguageSwitcher dropdown is rendered. | | -| `onChange` | `(language: LanguageOption) => void` | A change handler triggered when the language is changed. Invalid when no `languageOptions` are provided | | +# Props + +## Code + +| Prop | Type | Description | Default | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `children` (Required) | `string` | This is the code snippet that will be rendered in the code block. | `''` | +| `language` (Required) | `'javascript'`, `'typescript'`, `'cs'`, `'csharp'`, `'cpp'`, `'go'`, `'http'`,`'java'`, `'perl'`, `'php'`, `'python'`, `'ruby'`, `'scala'`, `'swift'`, `'kotlin'`,`'objectivec'`, `'dart'`, `'bash'`, `'shell'`, `'sql'`, `'yaml'`, `'json'`, `'diff'`, `'xml'`, `'none'` | The language to highlight the code block as. When set to `'none'`, no syntax highlighting will be applied. | | +| `darkMode` | `boolean` | Determines whether or not the component will appear in dark mode. | `false` | +| `className` | `string` | Applies a className to the root element's classList. | | +| `showLineNumbers` | `boolean` | Shows line numbers next to each line of code in the passed code snippet. **NOTE:** While you can set this to `true` regardless of the code component being multiline, the line numbers will not be displayed if the `multiline` prop is `true`. | `false` | +| `lineNumberStart` | `number` | Specifies the number by which to start line numbering. | `1` | +| `onCopy` | `Function` | Callback fired when Code is copied | | +| `expandable` | `boolean` | When true, allows the code block to be expanded and collapsed when there are more than 5 lines of code. | `false` | +| `highlightLines` | `Array` | An optional array of lines to highlight. The array can only contain numbers corresponding to the line numbers to highlight, and / or tuples representing a range (e.g. `[6, 10]`); | | +| `copyButtonAppearance` | `'none'`, `'hover'`, `'persist'` | Determines the appearance of the copy button without a panel. The copy button allows the code block to be copied to the user's clipboard by clicking the button. If `hover`, the copy button will only appear when the user hovers over the code block. On mobile devices, the copy button will always be visible. If `persist`, the copy button will always be visible. If `none`, the copy button will not be rendered. **_Note: `panel` cannot be used with `copyButtonAppearance`. Either use `copyButtonAppearance` or `panel`, not both_**. | `hover` | +| `panel` | `React.ReactNode` | Slot to pass the `` sub-component which will render the top panel with a language switcher, custom action buttons, and copy button. If no props are passed to the panel sub-component, the panel will render with only the copy button. **_Note: `copyButtonAppearance` cannot be used with `panel`. Either use `copyButtonAppearance` or `panel`, not both._** | `` | +| `isLoading` | `boolean` | Determines whether or not the loading skeleton will be rendered in place of the code block. If`true`, the language switcher and copy button will be disabled in the top panel. | `false` | +| `baseFontSize` | `'13'` \| `'16'` | Determines the base font-size of the component | `13` | +| `copyable` (`Deprecated`) | `boolean` | When true, allows the code block to be copied to the user's clipboard. **_Note:_** `@deprecated` - use `` or `copyButtonAppearance` | `false` | +| `chromeTitle`(`Deprecated`) | `string` | Shows a filename-like title in the window chrome frame.**NOTE:** While you can set this prop if `showWindowChrome` is `false`, it will not be displayed unless the `showWindowChrome` prop is `true`. **_Note:_** `@deprecated` - use `panel={}` | `''` | +| `showCustomActionButtons` (`Deprecated`) | `boolean` | Shows custom action buttons in the panel if set to `true` and there is at least one item in `customActionButtons`. **_Note:_** `@deprecated` - use `` | `false` | +| `customActionButtons`(`Deprecated`) | `Array` | An array of custom action buttons using the `IconButton` component. For example: `[, ]` **_Note:_** `@deprecated` - use `` | `[]` | +| `languageOptions` (`Deprecated`) | `Array` (see below) | An array of language options. When provided, a LanguageSwitcher dropdown is rendered. **_Note:_** `@deprecated` - use `` | | +| `onChange` (`Deprecated`) | `(language: LanguageOption) => void` | A change handler triggered when the language is changed. Invalid when no `languageOptions` are provided **_Note:_** `@deprecated` - use `` | + +## Panel + +| Prop | Type | Description | Default | +| ------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | +| `showCustomActionButtons` | `boolean` | Shows custom action buttons in the panel if set to `true` and there is at least one item in `customActionButtons`. | `false` | +| `customActionButtons` | `Array` | An array of custom action buttons using the `IconButton` component. For example: `[, ]` | `[]` | +| `title` | `string` | Shows a filename-like title in the window chrome frame. | `''` | +| `languageOptions` | `Array` | An array of language options. When provided, a LanguageSwitcher dropdown is rendered. | | +| `onChange` | `(language: LanguageOption) => void` | A change handler triggered when the language is changed. Invalid when no `languageOptions` are provided. | | ``` interface LanguageOption { @@ -127,3 +124,141 @@ interface LanguageOption { image?: React.ReactElement; } ``` + +# Test Harnesses + +## getTestUtils() + +`getTestUtils()` is a util that allows consumers to reliably interact with LG `Code` in a product test suite. If the `Code` component cannot be found, an error will be thrown. + +### Usage + +```tsx +import { getTestUtils } from '@leafygreen-ui/code'; + +const utils = getTestUtils(lgId?: `lg-${string}`); // lgId refers to the custom `data-lgid` attribute passed to `Code`. It defaults to 'lg-code' if left empty. +``` + +#### Single `Code` component + +```tsx +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import Code, { getTestUtils } from '@leafygreen-ui/code'; + +... + +test('code', () => { + render( + {}} + languageOptions={languageOptions} + title="Title" + /> + } + > + {codeSnippet} + + ); + + const { getLanguage, getLanguageSwitcherUtils, getIsLoading, getCopyButtonUtils, getExpandButton } = getTestUtils(); + const { getInput, getOptions, getOptionByValue, getInputValue, isDisabled: isLanguageSwitcherDisabled } = getLanguageSwitcherUtils(); + const { getButton: getCopyButtonUtils, isDisabled: isCopyButtonDisabled } = getCopyButtonUtils(); + + expect(getLanguage()).toBe('javascript'); + expect(getTitle()).toBe('Title'); + expect(getInput()).toBeInTheDocument(); + expect(getOptions()).toHaveLength(2); + expect(getOptionByValue('js')).toBeInTheDocument(); + expect(getInputValue()).toBe('javascript'); + expect(isLanguageSwitcherDisabled()).toBe(false); + expect(getIsLoading()).toBe(false); + expect(getCopyButtonUtils()).toBeInTheDocument(); + expect(isCopyButtonDisabled()).toBe(false); + expect(getExpandButton()).toBeInTheDocument(); + expect(isExpanded()).toBe(false); +}); +``` + +#### Multiple `Code` components + +When testing multiple `Code` components, it is recommended to add the custom `data-lgid` attribute to each `Code`. + +```tsx +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Code, getTestUtils } from '@leafygreen-ui/code'; + +... + +test('code', () => { + render( + <> + } + data-lgid="lg-code-1" + > + {codeSnippet} + + } + data-lgid="lg-code-2" + > + {codeSnippet} + + + ); + + const testUtils1 = getTestUtils('lg-code-1'); + const testUtils2 = getTestUtils('lg-code-2'); + + // First Code + expect(testUtils1.getLanguage()).toBe('javascript'); + + // Second Code + expect(testUtils2.getLanguage()).toBe('python'); +}); +``` + +### Test Utils + +```tsx +const { + getLanguage, + getTitle, + getLanguageSwitcherUtils: { + getInput, + getOptions, + getOptionByValue, + isDisabled, + }, + getIsLoading, + getCopyButtonUtils: { getButton, isDisabled }, + getExpandButton,, +} = getTestUtils(); +``` + +| Util | Description | Returns | +| ---------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `getLanguage()` | Returns the current language of the code block | `string` | +| `getTitle()` | Returns the title of the code block | `string` \| `null` | +| `getLanguageSwitcherUtils()` | Returns utils for interacting with the language switcher | `LanguageSwitcherUtils` | +| `getIsLoading()` | Returns whether the code block is in loading state | `boolean` | +| `getCopyButtonUtils()` | Returns utils for interacting with the copy button | [Button test utils return type](https://github.com/mongodb/leafygreen-ui/blob/main/packages/button/README.md#test-utils) | +| `getExpandButton()` | Returns the expand button | `HTMLButtonElement` | +| `getIsExpanded()` | Returns whether the code block is expanded | `boolean` | + +### LanguageSwitcherUtils + +| Util | Description | Returns | +| --------------------------------- | ------------------------------------------------- | ----------------------- | +| `getInput()` | Returns the language switcher trigger | `HTMLButtonElement` | +| `getInputValue()` | Returns the language switcher input value | `string` | +| `getOptions()` | Returns all options in the language switcher | `Array` | +| `getOptionByValue(value: string)` | Returns the option element by its value | `HTMLElement` \| `null` | +| `isDisabled()` | Returns whether the language switcher is disabled | `boolean` | diff --git a/packages/code/package.json b/packages/code/package.json index dd3c7d23a7..528ff46c32 100644 --- a/packages/code/package.json +++ b/packages/code/package.json @@ -32,8 +32,11 @@ "@leafygreen-ui/lib": "workspace:^", "@leafygreen-ui/palette": "workspace:^", "@leafygreen-ui/select": "workspace:^", + "@leafygreen-ui/skeleton-loader": "workspace:^", "@leafygreen-ui/tokens": "workspace:^", "@leafygreen-ui/tooltip": "workspace:^", + "@leafygreen-ui/typography": "workspace:^", + "@lg-tools/test-harnesses": "workspace:^", "@types/facepaint": "^1.2.1", "@types/highlight.js": "^10.1.0", "clipboard": "^2.0.6", diff --git a/packages/code/src/Code.stories.tsx b/packages/code/src/Code.stories.tsx index bdfb319e37..62236aa0dd 100644 --- a/packages/code/src/Code.stories.tsx +++ b/packages/code/src/Code.stories.tsx @@ -7,12 +7,31 @@ import { type StoryType, } from '@lg-tools/storybook-utils'; +import { css } from '@leafygreen-ui/emotion'; import Icon from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; -import LeafygreenProvider from '@leafygreen-ui/leafygreen-provider'; -import LanguageSwitcherExample from './LanguageSwitcher/LanguageSwitcherExample'; -import Code, { CodeProps, Language } from '.'; +import { + languageOptions, + LanguageSwitcherWithDeprecatedPropsExample, + LanguageSwitcherWithPanelExample, +} from './LanguageSwitcher/LanguageSwitcherExample'; +import Code, { CodeProps, CopyButtonAppearance, Language, Panel } from '.'; + +const customActionButtons = [ + {}} aria-label="label" key="1"> + + , + , + + + , +]; const jsSnippet = ` import datetime from './'; @@ -60,32 +79,27 @@ const meta: StoryMetaType = { 'showCustomButtons', 'customActionButtons', 'languageOptions', + 'children', ], }, generate: { - combineArgs: { - darkMode: [false, true], - copyable: [true, false], - expandable: [true, false], - showWindowChrome: [false, true], - showLineNumbers: [false, true], - }, + storyNames: ['WithPanel', 'WithoutPanel', 'Loading'], }, }, args: { language: 'js', baseFontSize: 14, children: shortJsSnippet, - chromeTitle: 'example.ts', + copyButtonAppearance: CopyButtonAppearance.Hover, + chromeTitle: '', }, argTypes: { + isLoading: { control: 'boolean' }, copyable: { control: 'boolean' }, expandable: { control: 'boolean' }, - showWindowChrome: { control: 'boolean' }, showLineNumbers: { control: 'boolean' }, highlightLines: { control: 'boolean' }, darkMode: storybookArgTypes.darkMode, - chromeTitle: { control: 'text' }, lineNumberStart: { control: 'number' }, baseFontSize: storybookArgTypes.baseFontSize, language: { @@ -94,6 +108,12 @@ const meta: StoryMetaType = { type: 'select', }, }, + copyButtonAppearance: { + options: Object.values(CopyButtonAppearance), + control: { + type: 'select', + }, + }, }, }; @@ -105,18 +125,18 @@ interface FontSizeProps { } export const LiveExample: StoryType = ({ - baseFontSize, highlightLines, ...args }: CodeProps & FontSizeProps) => ( - - - {jsSnippet} - - + + {jsSnippet} + ); LiveExample.parameters = { chromatic: { @@ -124,38 +144,208 @@ LiveExample.parameters = { }, }; -const customActionButtons = [ - {}} aria-label="label" key="1"> - - , - , - = ({ + highlightLines, + ...args +}: CodeProps & FontSizeProps) => ( + + } > - - , -]; - -export const WithCustomActions = LiveExample.bind({}); -WithCustomActions.args = { - showCustomActionButtons: true, - customActionButtons, -}; + {jsSnippet} + +); export const WithLanguageSwitcher: StoryType = ({ - baseFontSize, ...args }: CodeProps & FontSizeProps) => ( - - - + +); +WithLanguageSwitcher.parameters = { + controls: { + exclude: [ + 'highlightLines', + 'copyButtonAppearance', + 'copyable', + 'children', + 'expandable', + 'chromeTitle', + ], + }, +}; + +export const WithDeprecatedCustomActionProps: StoryType< + typeof Code, + FontSizeProps +> = ({ highlightLines, ...args }: CodeProps) => ( + + {jsSnippet} + +); +WithDeprecatedCustomActionProps.parameters = { + controls: { + exclude: [ + 'highlightLines', + 'copyButtonAppearance', + 'copyable', + 'children', + 'expandable', + 'language', + ], + }, +}; + +export const WithDeprecatedLanguageSwitcherProps: StoryType< + typeof Code, + FontSizeProps +> = ({ ...args }: CodeProps) => ( + ); +WithDeprecatedLanguageSwitcherProps.parameters = { + controls: { + exclude: [ + 'highlightLines', + 'copyButtonAppearance', + 'copyable', + 'children', + 'expandable', + 'chromeTitle', + 'language', + ], + }, +}; + +export const WithPanel = () => {}; +WithPanel.parameters = { + generate: { + combineArgs: { + darkMode: [false, true], + expandable: [true, false], + showLineNumbers: [false, true], + language: ['js', languageOptions[0].displayName], + panel: [ + , + , + {}} + key={3} + />, + {}} key={4} />, + , + , + {}} + key={7} + />, + ], + }, + excludeCombinations: [ + { + language: 'js', + panel: , + }, + ], + }, +}; + +export const WithoutPanel: StoryType = () => <>; +WithoutPanel.parameters = { + generate: { + args: { + language: languageOptions[0].displayName, + }, + combineArgs: { + // @ts-expect-error - data-hover is not a valid prop + 'data-hover': [false, true], + darkMode: [false, true], + expandable: [true, false], + copyButtonAppearance: [ + CopyButtonAppearance.Hover, + CopyButtonAppearance.Persist, + ], + showLineNumbers: [false, true], + }, + excludeCombinations: [ + { + // @ts-expect-error - data-hover is not a valid prop + ['data-hover']: true, + copyButtonAppearance: CopyButtonAppearance.Persist, + }, + ], + }, +}; -export const Generated = () => {}; +export const Loading = () => {}; +Loading.parameters = { + chromatic: { viewports: [1500] }, + controls: { + exclude: /.*/g, + }, + generate: { + combineArgs: { + darkMode: [false, true], + expandable: [true, false], + panel: [ + undefined, + {}} + key={7} + />, + ], + }, + args: { + language: languageOptions[0].displayName, + isLoading: true, + }, + decorator: Instance => { + return ( +
+ +
+ ); + }, + }, +}; diff --git a/packages/code/src/Code.testutils.tsx b/packages/code/src/Code.testutils.tsx new file mode 100644 index 0000000000..af08ab5cb0 --- /dev/null +++ b/packages/code/src/Code.testutils.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { render, RenderResult } from '@testing-library/react'; + +import Code from './Code/Code'; +import { CodeProps } from './Code/Code.types'; +import { PanelProps } from './Panel/Panel.types'; +import { Panel } from './Panel'; +import { Language } from './types'; +import { getTestUtils, TestUtilsReturnType } from './utils'; + +const codeSnippet = ` +import datetime from './'; + +const myVar = 42; + +var myObj = { + someProp: ['arr', 'ay'], + regex: /([A-Z])w+/ +} + +export default class myClass { + constructor(){ + // access properties + this.myProp = false + } +} + +function greeting(entity) { + return \`Hello, \${entity}! Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper.\`; +} + +console.log(greeting('World'));`; + +export const languageOptions = [ + { + displayName: 'JavaScript', + language: Language.JavaScript, + }, + { + displayName: 'Python', + language: Language.Python, + }, +]; + +export const renderCode = ( + props: Partial = {}, +): RenderResult & TestUtilsReturnType => { + const renderResults = render( + + {codeSnippet} + , + ); + + const testUtils = getTestUtils(); + + return { + ...renderResults, + ...testUtils, + }; +}; + +export const renderCodeWithLanguageSwitcher = ({ + props = {}, + isLoading = false, +}: { + props?: Partial; + isLoading?: boolean; +}): RenderResult & TestUtilsReturnType => { + const renderResults = render( + {}} + {...props} + languageOptions={languageOptions} + /> + } + > + {codeSnippet} + , + ); + + const testUtils = getTestUtils(); + + return { + ...renderResults, + ...testUtils, + }; +}; + +export const renderMultipleCodes = (): RenderResult => { + const renderResults = render( + <> + {}} languageOptions={languageOptions} />} + > + {codeSnippet} + + {}} languageOptions={languageOptions} />} + > + {codeSnippet} + + , + ); + + return { + ...renderResults, + }; +}; diff --git a/packages/code/src/Code/Code.spec.tsx b/packages/code/src/Code/Code.spec.tsx index 942a0c6c87..379df8a8e2 100644 --- a/packages/code/src/Code/Code.spec.tsx +++ b/packages/code/src/Code/Code.spec.tsx @@ -1,107 +1,96 @@ import React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import ClipboardJS from 'clipboard'; import { axe } from 'jest-axe'; import Icon from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; -import { typeIs } from '@leafygreen-ui/lib'; import { Context, jest as Jest } from '@leafygreen-ui/testing-lib'; +import { + languageOptions, + renderCode, + renderCodeWithLanguageSwitcher, +} from '../Code.testutils'; import { numOfCollapsedLinesOfCode } from '../constants'; -import LanguageSwitcherExample, { - PythonLogo, -} from '../LanguageSwitcher/LanguageSwitcherExample'; +import { Panel } from '../Panel'; +import { getTestUtils } from '../utils/getTestUtils/getTestUtils'; -import Code, { hasMultipleLines } from './Code'; +import Code from './Code'; +import { hasMultipleLines } from './utils'; -const codeSnippet = 'const greeting = "Hello, world!";'; -const className = 'test-class'; -const onCopy = jest.fn(); - -const actionData = [ +const customActionButtons = [ {}} aria-label="label" - darkMode={true} key="1" + data-testid="lg-code-icon_button" + > + + , + , + - + , ]; +jest.mock('clipboard', () => { + const ClipboardJSOriginal = jest.requireActual('clipboard'); + + // Return a mock that preserves the class and mocks `isSupported` + return class ClipboardJSMock extends ClipboardJSOriginal { + static isSupported = jest.fn(() => true); // Mock isSupported + }; +}); + describe('packages/Code', () => { - const { container } = Context.within( - Jest.spyContext(ClipboardJS, 'isSupported'), - spy => { - spy.mockReturnValue(true); - - return render( - - {codeSnippet} - , - ); + // https://stackoverflow.com/a/69574825/13156339 + Object.defineProperty(navigator, 'clipboard', { + value: { + // Provide mock implementation + writeText: jest.fn().mockReturnValueOnce(Promise.resolve()), }, - ); + }); describe('a11y', () => { test('does not have basic accessibility violations', async () => { + const { container } = renderCode(); const results = await axe(container); expect(results).toHaveNoViolations(); }); - test('announces copied to screenreaders when content is copied', () => { - Context.within(Jest.spyContext(ClipboardJS, 'isSupported'), spy => { - spy.mockReturnValue(true); - - render( - - {codeSnippet} - , - ); + describe('copy button', () => { + test('announces copied to screenreaders when content is copied without a panel', () => { + renderCode(); + const copyIcon = screen.getByRole('button'); + userEvent.click(copyIcon); + expect(screen.getByRole('alert')).toBeInTheDocument(); }); - const copyIcon = screen.getByRole('button'); - fireEvent.click(copyIcon); - expect(screen.getByRole('alert')).toBeInTheDocument(); + test('announces copied to screenreaders when content is copied in the panel', () => { + renderCode({ panel: }); + const copyIcon = screen.getByRole('button'); + userEvent.click(copyIcon); + expect(screen.getByRole('alert')).toBeInTheDocument(); + }); }); }); - const codeContainer = (container.firstChild as HTMLElement).lastChild; - const codeRoot = (codeContainer as HTMLElement).firstChild; - const copyButton = codeRoot?.nextSibling?.firstChild as HTMLElement; - - if (!codeRoot || !typeIs.element(codeRoot)) { - throw new Error('Code element not found'); - } - - if (!copyButton || !typeIs.element(copyButton)) { - throw new Error('Copy button not found'); - } - - test('root element renders as a
 tag', () => {
-    expect(codeRoot.tagName).toBe('PRE');
-  });
-
-  test(`renders "${className}" in the root element's classList`, () => {
-    expect(codeRoot.classList.contains(className)).toBe(true);
-  });
-
+  // TODO: remove this test when we remove the prop
   describe('when copyable is true', () => {
     test('onCopy callback is fired when code is copied', () => {
-      Context.within(Jest.spyContext(ClipboardJS, 'isSupported'), spy => {
-        spy.mockReturnValue(true);
-
-        render(
-          
-            {codeSnippet}
-          ,
-        );
-      });
+      const onCopy = jest.fn();
+      renderCode({ onCopy, copyable: true });
 
       const copyIcon = screen.getByRole('button');
-      fireEvent.click(copyIcon);
+      userEvent.click(copyIcon);
       expect(onCopy).toHaveBeenCalledTimes(1);
     });
   });
@@ -128,39 +117,349 @@ describe('packages/Code', () => {
     });
   });
 
-  describe('panel', () => {
-    test('is not rendered when language switcher is not present and when copyable is false and showCustomActionButtons is false', () => {
-      expect(container).not.toContain(
-        screen.queryByTestId('leafygreen-code-panel'),
-      );
+  describe('isLoading prop', () => {
+    describe('when isLoading is true', () => {
+      test('renders a skeleton', () => {
+        const { getByTestId } = renderCode({ isLoading: true });
+        expect(getByTestId('lg-code-skeleton')).toBeInTheDocument();
+      });
+
+      test('does not render a pre tag', () => {
+        const { queryByTestId } = renderCode({ isLoading: true });
+        expect(queryByTestId('lg-code-pre')).toBeNull();
+      });
+
+      describe('with panel slot', () => {
+        test('language switcher is disabled', () => {
+          const { getCopyButtonUtils } = renderCode({
+            isLoading: true,
+            language: languageOptions[0].displayName,
+            panel: (
+               {}} languageOptions={languageOptions} />
+            ),
+          });
+
+          expect(getCopyButtonUtils().getButton()).toBeInTheDocument();
+          expect(getCopyButtonUtils().isDisabled()).toBe(true);
+        });
+        test('copy button is disabled', () => {
+          const { getCopyButtonUtils } = renderCode({
+            isLoading: true,
+            language: languageOptions[0].displayName,
+            panel: ,
+          });
+
+          expect(getCopyButtonUtils().getButton()).toBeInTheDocument();
+          expect(getCopyButtonUtils().isDisabled()).toBe(true);
+        });
+      });
+
+      describe('without panel slot', () => {
+        test('throws error and copy button is not rendered', () => {
+          try {
+            const { getCopyButtonUtils } = Context.within(
+              Jest.spyContext(ClipboardJS, 'isSupported'),
+              spy => {
+                spy.mockReturnValue(true);
+                return renderCode({
+                  isLoading: true,
+                  copyable: false,
+                });
+              },
+            );
+            const _ = getCopyButtonUtils().getButton();
+          } catch (error) {
+            expect(error).toBeInstanceOf(Error);
+            expect(error).toHaveProperty(
+              'message',
+              expect.stringMatching(
+                /Unable to find an element by: \[data-lgid="lg-code-copy_button"\]/,
+              ),
+            );
+          }
+        });
+      });
     });
+    describe('when isLoading is false', () => {
+      test('does not render a skeleton', () => {
+        const { queryByTestId } = renderCode({ isLoading: false });
+        expect(queryByTestId('lg-code-skeleton')).toBeNull();
+      });
+      test('renders a pre tag', () => {
+        const { getByTestId } = renderCode({ isLoading: false });
+        expect(getByTestId('lg-code-pre')).toBeInTheDocument();
+      });
 
-    test('is rendered when language switcher is not present, when copyable is false, showCustomActionButtons is true, and actionsButtons has items', () => {
-      render(
-        
-          {codeSnippet}
-        ,
-      );
-      expect(screen.queryByTestId('leafygreen-code-panel')).toBeDefined();
+      describe('with panel slot', () => {
+        test('language switcher is enabled', () => {
+          const { getLanguageSwitcherUtils } = renderCode({
+            isLoading: false,
+            language: languageOptions[0].displayName,
+            panel: (
+               {}} languageOptions={languageOptions} />
+            ),
+          });
+
+          expect(getLanguageSwitcherUtils().getInput()).toBeInTheDocument();
+          expect(getLanguageSwitcherUtils().isDisabled()).toBe(false);
+        });
+        test('copy button is enabled', () => {
+          const { getCopyButtonUtils } = Context.within(
+            Jest.spyContext(ClipboardJS, 'isSupported'),
+            spy => {
+              spy.mockReturnValue(true);
+              return renderCode({
+                isLoading: false,
+                language: languageOptions[0].displayName,
+                panel: ,
+              });
+            },
+          );
+
+          expect(getCopyButtonUtils().getButton()).toBeInTheDocument();
+          expect(getCopyButtonUtils().isDisabled()).toBe(false);
+        });
+      });
+
+      describe('without panel slot', () => {
+        test('copy button is enabled', () => {
+          const { getCopyButtonUtils } = Context.within(
+            Jest.spyContext(ClipboardJS, 'isSupported'),
+            spy => {
+              spy.mockReturnValue(true);
+              return renderCode({
+                isLoading: false,
+              });
+            },
+          );
+
+          expect(getCopyButtonUtils().getButton()).toBeInTheDocument();
+          expect(getCopyButtonUtils().isDisabled()).toBe(false);
+        });
+      });
     });
+  });
 
-    test('is not rendered when language switcher is not present, when copyable is false, when showCustomActionButtons is true, and actionsButtons has no items', () => {
-      const { container } = render(
-        
-          {codeSnippet}
-        ,
-      );
-      expect(container).not.toContain(
-        screen.queryByTestId('leafygreen-code-panel'),
-      );
+  describe('Without panel slot', () => {
+    test('does not render a panel', () => {
+      const { queryByTestId } = renderCode();
+      expect(queryByTestId('lg-code-panel')).toBeNull();
+    });
+
+    describe('renders a copy button', () => {
+      test('with default value of hover', () => {
+        const { getCopyButtonUtils } = Context.within(
+          Jest.spyContext(ClipboardJS, 'isSupported'),
+          spy => {
+            spy.mockReturnValue(true);
+            return renderCode();
+          },
+        );
+
+        expect(getCopyButtonUtils().getButton()).not.toBeNull();
+      });
+      test('when copyButtonAppearance is persist', () => {
+        const { getCopyButtonUtils } = Context.within(
+          Jest.spyContext(ClipboardJS, 'isSupported'),
+          spy => {
+            spy.mockReturnValue(true);
+            return renderCode({ copyButtonAppearance: 'persist' });
+          },
+        );
+
+        expect(getCopyButtonUtils().getButton()).not.toBeNull();
+      });
+      test('when copyButtonAppearance is hover', () => {
+        const { getCopyButtonUtils } = Context.within(
+          Jest.spyContext(ClipboardJS, 'isSupported'),
+          spy => {
+            spy.mockReturnValue(true);
+            return renderCode({ copyButtonAppearance: 'hover' });
+          },
+        );
+
+        expect(getCopyButtonUtils().getButton()).not.toBeNull();
+      });
+    });
+
+    describe('throws error', () => {
+      test('when copyButtonAppearance is none', () => {
+        try {
+          const { getCopyButtonUtils } = renderCode({
+            copyButtonAppearance: 'none',
+          });
+
+          const _ = getCopyButtonUtils().getButton();
+        } catch (error) {
+          expect(error).toBeInstanceOf(Error);
+          expect(error).toHaveProperty(
+            'message',
+            expect.stringMatching(
+              /Unable to find an element by: \[data-lgid="lg-code-copy_button"\]/,
+            ),
+          );
+        }
+      });
+    });
+  });
+
+  describe('With panel slot', () => {
+    describe('renders', () => {
+      test('panel with only the copy button when no props are passed', () => {
+        const { queryByTestId } = renderCode({ panel:  });
+        expect(queryByTestId('lg-code-panel')).toBeDefined();
+      });
+
+      test('panel with only the copy button when onCopy is passed', () => {
+        const { queryByTestId } = renderCode({
+          panel:  {}} />,
+        });
+        expect(queryByTestId('lg-code-panel')).toBeDefined();
+      });
+    });
+
+    describe('language switcher', () => {
+      test('renders when languageOptions, language, and onChange are defined', () => {
+        const { getLanguageSwitcherUtils } = renderCode({
+          language: languageOptions[0].displayName,
+          panel: (
+             {}} languageOptions={languageOptions} />
+          ),
+        });
+        expect(getLanguageSwitcherUtils().getInput()).toBeDefined();
+      });
+
+      test('does not render and throws if the languageOptions is not defined', () => {
+        try {
+          const { getLanguageSwitcherUtils } = renderCode({
+            language: languageOptions[0].displayName,
+            // @ts-expect-error
+            panel:  {}} />,
+          });
+          const _ = getLanguageSwitcherUtils().getInput();
+        } catch (error) {
+          expect(error).toBeInstanceOf(Error);
+          expect(error).toHaveProperty(
+            'message',
+            expect.stringMatching(
+              /Unable to find an element by: \[data-lgid="lg-code-select"\]/,
+            ),
+          );
+        }
+      });
+
+      test('does not render and throws if onChange is not defined', () => {
+        try {
+          const { getLanguageSwitcherUtils } = renderCode({
+            language: languageOptions[0].displayName,
+            // @ts-expect-error - onChange is not defined
+            panel: ,
+          });
+          const _ = getLanguageSwitcherUtils().getInput();
+        } catch (error) {
+          expect(error).toBeInstanceOf(Error);
+          expect(error).toHaveProperty(
+            'message',
+            expect.stringMatching(
+              /Unable to find an element by: \[data-lgid="lg-code-select"\]/,
+            ),
+          );
+        }
+      });
+
+      test('does not render and throws if languageOptions is an empty array', () => {
+        try {
+          const { getLanguageSwitcherUtils } = renderCode({
+            language: languageOptions[0].displayName,
+            panel:  {}} languageOptions={[]} />,
+          });
+          const _ = getLanguageSwitcherUtils().getInput();
+        } catch (error) {
+          expect(error).toBeInstanceOf(Error);
+          expect(error).toHaveProperty(
+            'message',
+            expect.stringMatching(
+              /Unable to find an element by: \[data-lgid="lg-code-select"\]/,
+            ),
+          );
+        }
+      });
+
+      test('does not render and throws if langauage is a string', () => {
+        try {
+          const { getLanguageSwitcherUtils } = renderCode({
+            language: 'javascript',
+            panel:  {}} languageOptions={[]} />,
+          });
+          const _ = getLanguageSwitcherUtils().getInput();
+        } catch (error) {
+          expect(error).toBeInstanceOf(Error);
+          expect(error).toHaveProperty(
+            'message',
+            expect.stringMatching(
+              /Unable to find an element by: \[data-lgid="lg-code-select"\]/,
+            ),
+          );
+        }
+      });
+
+      test('throws an error if language is not in languageOptions', () => {
+        try {
+          renderCode({
+            language: 'Testing',
+            panel:  {}} languageOptions={[]} />,
+          });
+        } catch (error) {
+          expect(error).toBeInstanceOf(Error);
+          expect(error).toHaveProperty(
+            'message',
+            expect.stringMatching(/Unknown language: "Testing"/),
+          );
+        }
+      });
+    });
+
+    describe('custom action buttons', () => {
+      test('do not render if showCustomActionButtons is false', () => {
+        const { queryAllByTestId } = renderCode({
+          panel: (
+            
+          ),
+        });
+        expect(queryAllByTestId('lg-code-icon_button')).toHaveLength(0);
+      });
+      test('do not render if customActionButtons is an empty array', () => {
+        const { queryAllByTestId } = renderCode({
+          panel: ,
+        });
+        expect(queryAllByTestId('lg-code-icon_button')).toHaveLength(0);
+      });
+
+      test('renders when custom action buttons are present and showCustomActionButtons is true', () => {
+        const { queryByTestId } = renderCode({
+          panel: (
+            
+          ),
+        });
+        expect(queryByTestId('lg-code-panel')).toBeDefined();
+      });
+      test('only renders IconButton elements', () => {
+        const { queryAllByTestId } = renderCode({
+          panel: (
+            
+          ),
+        });
+        expect(queryAllByTestId('lg-code-icon_button')).toHaveLength(2);
+      });
     });
   });
 
@@ -188,52 +487,63 @@ describe('packages/Code', () => {
     });
 
     test('a collapsed select is rendered, with an active state based on the language prop', () => {
-      render();
-      expect(
-        screen.getByRole('button', { name: 'JavaScript' }),
-      ).toBeInTheDocument();
+      const { getLanguageSwitcherUtils } = renderCodeWithLanguageSwitcher({});
+      expect(getLanguageSwitcherUtils().getInput()).toBeInTheDocument();
+      expect(getLanguageSwitcherUtils().getInput()).toHaveTextContent(
+        'JavaScript',
+      );
     });
 
     test('clicking the collapsed select menu button opens a select', () => {
-      render();
-      const trigger = screen.getByRole('button', { name: 'JavaScript' });
-      fireEvent.click(trigger);
-      expect(screen.getByRole('listbox')).toBeInTheDocument();
+      const { getLanguageSwitcherUtils } = renderCodeWithLanguageSwitcher({});
+      const trigger = getLanguageSwitcherUtils().getInput();
+      userEvent.click(trigger!);
+      expect(getLanguageSwitcherUtils().getOptions()).toHaveLength(2);
     });
 
     test('options displayed in select are based on the languageOptions prop', () => {
-      render();
-      const trigger = screen.getByRole('button', { name: 'JavaScript' });
-      fireEvent.click(trigger);
+      const { getLanguageSwitcherUtils } = renderCodeWithLanguageSwitcher({});
+      const { getInput, getOptionByValue } = getLanguageSwitcherUtils();
+      const trigger = getInput();
+      userEvent.click(trigger!);
 
       ['JavaScript', 'Python'].forEach(lang => {
-        expect(screen.getByRole('option', { name: lang })).toBeInTheDocument();
+        expect(getOptionByValue(lang)).toBeInTheDocument();
       });
     });
 
     test('onChange prop gets called when new language is selected', () => {
       const onChange = jest.fn();
-      render();
+      const { getLanguageSwitcherUtils } = renderCodeWithLanguageSwitcher({
+        props: {
+          onChange,
+        },
+      });
+      const { getOptionByValue, getInput } = getLanguageSwitcherUtils();
 
-      const trigger = screen.getByRole('button', { name: 'JavaScript' });
-      fireEvent.click(trigger);
+      const trigger = getInput();
+      userEvent.click(trigger!);
 
-      fireEvent.click(screen.getByRole('option', { name: 'Python' }));
+      userEvent.click(getOptionByValue('Python')!);
       expect(onChange).toHaveBeenCalled();
     });
 
     test('onChange prop is called with an object that represents the newly selected language when called', () => {
       const onChange = jest.fn();
-      render();
+      const { getLanguageSwitcherUtils } = renderCodeWithLanguageSwitcher({
+        props: {
+          onChange,
+        },
+      });
+      const { getOptionByValue, getInput } = getLanguageSwitcherUtils();
 
-      const trigger = screen.getByRole('button', { name: 'JavaScript' });
-      fireEvent.click(trigger);
+      const trigger = getInput();
+      userEvent.click(trigger!);
 
-      fireEvent.click(screen.getByRole('option', { name: 'Python' }));
+      userEvent.click(getOptionByValue('Python')!);
 
       expect(onChange).toHaveBeenCalledWith({
         displayName: 'Python',
-        image: ,
         language: 'python',
       });
     });
@@ -246,14 +556,24 @@ describe('packages/Code', () => {
         (_, i) => `const greeting${i} = "Hello, world! ${i}";`,
       ).join('\n');
 
-    test(`shows no expand button when <= ${numOfCollapsedLinesOfCode} lines of code`, () => {
-      render(
-        
-          {getCodeSnippet(numOfCollapsedLinesOfCode - 1)}
-        ,
-      );
-
-      expect(screen.queryByTestId('lg-code-expand_button')).toBeNull();
+    test(`throws error and shows no expand button when <= ${numOfCollapsedLinesOfCode} lines of code`, () => {
+      try {
+        render(
+          
+            {getCodeSnippet(numOfCollapsedLinesOfCode - 1)}
+          ,
+        );
+        const { getExpandButton } = getTestUtils();
+        const _ = getExpandButton();
+      } catch (error) {
+        expect(error).toBeInstanceOf(Error);
+        expect(error).toHaveProperty(
+          'message',
+          expect.stringMatching(
+            /Unable to find an element by: \[data-lgid="lg-code-expand_button"\]/,
+          ),
+        );
+      }
     });
 
     test(`shows expand button when > ${numOfCollapsedLinesOfCode} lines of code`, () => {
@@ -263,7 +583,9 @@ describe('packages/Code', () => {
         ,
       );
 
-      expect(screen.getByTestId('lg-code-expand_button')).toBeInTheDocument();
+      const { getExpandButton } = getTestUtils();
+
+      expect(getExpandButton()).toBeInTheDocument();
     });
 
     test('shows correct number of lines of code on expand button', () => {
@@ -275,7 +597,9 @@ describe('packages/Code', () => {
         ,
       );
 
-      const actionButton = screen.getByTestId('lg-code-expand_button');
+      const { getExpandButton } = getTestUtils();
+
+      const actionButton = getExpandButton();
       expect(actionButton).toHaveTextContent(
         `Click to expand (${lineCount} lines)`,
       );
@@ -288,9 +612,12 @@ describe('packages/Code', () => {
         ,
       );
 
-      const actionButton = screen.getByTestId('lg-code-expand_button');
-      fireEvent.click(actionButton);
+      const { getExpandButton, getIsExpanded } = getTestUtils();
+
+      const actionButton = getExpandButton();
+      userEvent.click(actionButton!);
       expect(actionButton).toHaveTextContent('Click to collapse');
+      expect(getIsExpanded()).toBe(true);
     });
 
     test('shows expand button again when collapse button is clicked', () => {
@@ -302,13 +629,257 @@ describe('packages/Code', () => {
         ,
       );
 
-      const actionButton = screen.getByTestId('lg-code-expand_button');
-      fireEvent.click(actionButton); // Expand
-      fireEvent.click(actionButton); // Collapse
+      const { getExpandButton } = getTestUtils();
+
+      const actionButton = getExpandButton();
+      userEvent.click(actionButton!); // Expand
+      userEvent.click(actionButton!); // Collapse
 
       expect(actionButton).toHaveTextContent(
         `Click to expand (${lineCount} lines)`,
       );
     });
   });
+
+  describe('Deprecated props', () => {
+    describe('custom action buttons', () => {
+      test('does not renders a panel with custom action buttons when only customActionButtons is passed', () => {
+        const { queryByTestId } = renderCode({
+          customActionButtons,
+        });
+        expect(queryByTestId('lg-code-panel')).toBeNull();
+      });
+      test('does not renders a panel with custom action buttons when only showCustomActionButtons is true', () => {
+        const { queryByTestId } = renderCode({
+          showCustomActionButtons: true,
+        });
+        expect(queryByTestId('lg-code-panel')).toBeNull();
+      });
+      test('renders a panel with with custom action buttons when showCustomActionButtons is true and customActionButtons is passed', () => {
+        const { getByTestId } = renderCode({
+          showCustomActionButtons: true,
+          customActionButtons,
+        });
+        expect(getByTestId('lg-code-panel')).toBeDefined();
+      });
+    });
+
+    describe('language switcher', () => {
+      test('renders a panel when only language, onChange, and languageOptions are defined', () => {
+        const { getByTestId } = renderCode({
+          language: languageOptions[0].displayName,
+          languageOptions,
+          onChange: () => {},
+        });
+        expect(getByTestId('lg-code-panel')).toBeDefined();
+      });
+      test('does not render a panel when language and onChange are defined but languageOptions is not defined', () => {
+        const { queryByTestId } = renderCode({
+          language: languageOptions[0].displayName,
+          onChange: () => {},
+        });
+        expect(queryByTestId('lg-code-panel')).toBeNull();
+      });
+      test('does not render a panel when language and languageOptions are defined but onChange is not defined', () => {
+        const { queryByTestId } = renderCode({
+          language: languageOptions[0].displayName,
+          languageOptions,
+        });
+        expect(queryByTestId('lg-code-panel')).toBeNull();
+      });
+      test('does not render a panel when languageOptions is an empty array', () => {
+        const { queryByTestId } = renderCode({
+          language: languageOptions[0].displayName,
+          languageOptions: [],
+          onChange: () => {},
+        });
+        expect(queryByTestId('lg-code-panel')).toBeNull();
+      });
+      test('does not render a panel if language is a string', () => {
+        const { queryByTestId } = renderCode({
+          language: 'javascript',
+          languageOptions: [],
+          onChange: () => {},
+        });
+        expect(queryByTestId('lg-code-panel')).toBeNull();
+      });
+      test('throws an error if language is not in languageOptions', () => {
+        try {
+          renderCode({
+            language: 'Testing',
+            languageOptions,
+            onChange: () => {},
+          });
+        } catch (error) {
+          expect(error).toBeInstanceOf(Error);
+          expect(error).toHaveProperty(
+            'message',
+            expect.stringMatching(/Unknown language: "Testing"/),
+          );
+        }
+      });
+    });
+
+    describe('chromeTitle', () => {
+      test('renders a panel with a title when chromeTitle is defined', () => {
+        const { getByTestId } = renderCode({
+          chromeTitle: 'Title',
+        });
+        expect(getByTestId('lg-code-panel')).toBeDefined();
+        expect(getByTestId('lg-code-panel')).toHaveTextContent('Title');
+      });
+    });
+
+    describe('copyable', () => {
+      test('renders a panel with a copy button when only copyable is true', () => {
+        const { getByTestId } = Context.within(
+          Jest.spyContext(ClipboardJS, 'isSupported'),
+          spy => {
+            spy.mockReturnValue(true);
+            return renderCode({
+              copyable: true,
+            });
+          },
+        );
+        expect(getByTestId('lg-code-panel')).toBeDefined();
+        expect(getByTestId('lg-code-copy_button')).toBeDefined();
+      });
+      test('does not render a panel with a copy button when copyable is false', () => {
+        const { queryByTestId } = Context.within(
+          Jest.spyContext(ClipboardJS, 'isSupported'),
+          spy => {
+            spy.mockReturnValue(true);
+            return renderCode({
+              copyable: false,
+            });
+          },
+        );
+        expect(queryByTestId('lg-code-panel')).toBeNull();
+      });
+    });
+
+    describe('panel slot', () => {
+      describe('copyable', () => {
+        test('is overridden by the panel prop', () => {
+          const { getByTestId } = renderCode({
+            copyable: false,
+            panel: ,
+          });
+          expect(getByTestId('lg-code-panel')).toBeDefined();
+        });
+      });
+
+      describe('language switcher', () => {
+        test('is overridden by the panel prop', () => {
+          const { getByTestId } = renderCode({
+            language: languageOptions[1].displayName,
+            languageOptions,
+            onChange: () => {},
+            panel: (
+               {}}
+              />
+            ),
+          });
+          expect(getByTestId('lg-code_panel-override')).toBeDefined();
+          expect(
+            screen.getByRole('button', { name: 'Python' }),
+          ).toBeInTheDocument();
+        });
+      });
+    });
+  });
+
+  // eslint-disable-next-line jest/no-disabled-tests
+  test.skip('types behave as expected', () => {
+    <>
+       {}}
+        copyable={true}
+      >
+        snippet
+      
+      snippet
+      {/* @ts-expect-error - missing language prop */}
+      snippet
+      {/* @ts-expect-error - missing children */}
+      
+       {}}
+        darkMode={true}
+        panel={}
+      >
+        snippet
+      
+
+      {/* @ts-expect-error - cannot pass both panel and copyButtonAppearance */}
+       {}}
+        darkMode={true}
+        panel={}
+        copyButtonAppearance="hover"
+      >
+        snippet
+      
+
+       {}}
+        darkMode={true}
+        copyButtonAppearance="hover"
+      >
+        snippet
+      
+
+       {}}
+        darkMode={true}
+        // @ts-expect-error - onChange prop is missing on 
+        panel={}
+      >
+        snippet
+      
+       {}}
+        darkMode={true}
+        // @ts-expect-error - languageOptions prop is missing on 
+        panel={ {}} />}
+      >
+        snippet
+      
+       {}}
+        darkMode={true}
+        panel={
+           {}}
+            languageOptions={[]}
+            showCustomActionButtons
+            customActionButtons={[]}
+            title="Title"
+          />
+        }
+      >
+        snippet
+      
+    ;
+  });
 });
diff --git a/packages/code/src/Code/Code.styles.ts b/packages/code/src/Code/Code.styles.ts
index 46796bf6af..7a908cbe72 100644
--- a/packages/code/src/Code/Code.styles.ts
+++ b/packages/code/src/Code/Code.styles.ts
@@ -1,11 +1,16 @@
 import facepaint from 'facepaint';
 import { transparentize } from 'polished';
 
-import { css } from '@leafygreen-ui/emotion';
-import { Theme } from '@leafygreen-ui/lib';
+import { css, cx } from '@leafygreen-ui/emotion';
+import {
+  createUniqueClassName,
+  getMobileMediaQuery,
+  Theme,
+} from '@leafygreen-ui/lib';
 import { palette } from '@leafygreen-ui/palette';
 import {
   BaseFontSize,
+  breakpoints,
   color,
   fontFamilies,
   spacing,
@@ -14,7 +19,9 @@ import {
 
 import { variantColors } from '../globalStyles';
 
-import { ScrollState } from './Code.types';
+import { CopyButtonAppearance, ScrollState } from './Code.types';
+
+const copyButtonWithoutPanelClassName = createUniqueClassName('copy_button');
 
 // We use max-device-width to select specifically for iOS devices
 const mq = facepaint([
@@ -26,51 +33,152 @@ const singleLineComponentHeight = 36;
 const lineHeight = 24;
 const codeWrappingVerticalPadding = spacing[200];
 
-export const wrapperStyle: Record = {
-  [Theme.Light]: css`
-    border: 1px solid ${variantColors[Theme.Light][1]};
-    border-radius: 12px;
-    overflow: hidden;
-  `,
-  [Theme.Dark]: css`
-    border: 1px solid ${variantColors[Theme.Dark][1]};
-    border-radius: 12px;
-    overflow: hidden;
-  `,
-};
+export const getWrapperStyles = ({
+  theme,
+  className,
+}: {
+  theme: Theme;
+  className?: string;
+}) =>
+  cx(
+    css`
+      border: 1px solid ${variantColors[theme][1]};
+      border-radius: 12px;
+      overflow: hidden;
+      width: 100%;
+    `,
+    className,
+  );
+
+export const getCodeStyles = ({
+  scrollState,
+  theme,
+  showPanel,
+  showExpandButton,
+  isLoading,
+}: {
+  scrollState: ScrollState;
+  theme: Theme;
+  showPanel: boolean;
+  showExpandButton: boolean;
+  isLoading: boolean;
+}) =>
+  cx(contentWrapperStyles, baseScrollShadowStyles, {
+    [getScrollShadow(scrollState, theme)]: !isLoading,
+    [codeWithPanelStyles]: showPanel,
+    [codeWithoutPanelStyles]: !showPanel,
+    [expandableContentWithPanelStyles]: showExpandButton && showPanel,
+    [expandableContentWithoutPanelStyles]: showExpandButton && !showPanel,
+  });
+
+export const getCodeWrapperStyles = ({
+  theme,
+  showPanel,
+  expanded,
+  codeHeight,
+  collapsedCodeHeight,
+  isMultiline,
+  showExpandButton,
+  className,
+}: {
+  theme: Theme;
+  showPanel: boolean;
+  expanded: boolean;
+  codeHeight: number;
+  collapsedCodeHeight: number;
+  isMultiline: boolean;
+  showExpandButton: boolean;
+  className?: string;
+}) =>
+  cx(
+    codeWrapperStyle,
+    getCodeWrapperVariantStyle(theme),
+    codeWrapperHoverStyles,
+    {
+      [codeWrapperWithPanelStyles]: showPanel,
+      [codeWrapperWithoutPanelStyles]: !showPanel,
+      [codeWrapperSingleLineStyles]: !isMultiline,
+      [getExpandableCodeWrapperStyle(
+        expanded,
+        codeHeight,
+        collapsedCodeHeight,
+      )]: showExpandButton,
+    },
+    className,
+  );
+
+export const getExpandedButtonStyles = ({ theme }: { theme: Theme }) =>
+  cx(expandButtonStyle, getExpandButtonUtilsVariantStyle(theme));
+
+export const getCopyButtonWithoutPanelStyles = ({
+  copyButtonAppearance,
+}: {
+  copyButtonAppearance: CopyButtonAppearance;
+}) =>
+  cx(
+    copyButtonWithoutPanelClassName,
+    css`
+      position: absolute;
+      z-index: 1;
+      top: ${spacing[200]}px;
+      right: ${spacing[200]}px;
+      transition: opacity ${transitionDuration.default}ms ease-in-out;
+
+      // On hover or focus, the copy button should always be visible
+      &:hover,
+      &:focus-within {
+        opacity: 1;
+      }
+    `,
+    {
+      [css`
+        opacity: 0;
+
+        // On a mobile device, the copy button should always be visible
+        ${getMobileMediaQuery(breakpoints.Desktop)} {
+          opacity: 1;
+        }
+      `]: copyButtonAppearance === CopyButtonAppearance.Hover,
+    },
+  );
 
 export const contentWrapperStyles = css`
   position: relative;
   display: grid;
-  grid-template-areas: 'code panel';
-  grid-template-columns: auto 38px;
   border-radius: inherit;
   z-index: 0; // new stacking context
+  grid-template-areas:
+    'panel'
+    'code';
 `;
 
-export const contentWrapperStylesNoPanel = css`
+export const codeWithoutPanelStyles = css`
   // No panel, all code
   grid-template-areas: 'code code';
+
+  &:after {
+    grid-column: -1; // Placed on the right edge
+  }
 `;
 
-export const contentWrapperStyleWithPicker = css`
+export const codeWithPanelStyles = css`
   grid-template-areas:
     'panel'
     'code';
   grid-template-columns: unset;
-`;
 
-export const expandableContentWrapperStyle = css`
-  grid-template-areas: 'code panel' 'expandButton expandButton';
-  grid-template-rows: auto 28px;
+  &:before,
+  &:after {
+    grid-row: 2; // Placed on the top under the Picker Panel
+  }
 `;
 
-export const expandableContentWrapperStyleNoPanel = css`
+export const expandableContentWithoutPanelStyles = css`
   grid-template-areas: 'code code' 'expandButton expandButton';
   grid-template-rows: auto 28px;
 `;
 
-export const expandableContentWrapperStyleWithPicker = css`
+export const expandableContentWithPanelStyles = css`
   grid-template-areas:
     'panel'
     'code'
@@ -105,26 +213,36 @@ export const codeWrapperStyle = css`
   }
 `;
 
-export const codeWrapperStyleNoPanel = css`
+export const codeWrapperWithoutPanelStyles = css`
   border-left: 0;
   border-radius: inherit;
   border-top-right-radius: 0;
   border-top-left-radius: 0;
 `;
-export const codeWrapperStyleWithLanguagePicker = css`
+export const codeWrapperWithPanelStyles = css`
   border-left: 0;
   border-radius: inherit;
   border-top-right-radius: 0;
   border-top-left-radius: 0;
 `;
 
-export const singleLineCodeWrapperStyle = css`
+export const codeWrapperSingleLineStyles = css`
   display: flex;
   align-items: center;
   padding-top: ${(singleLineComponentHeight - lineHeight) / 2}px;
   padding-bottom: ${(singleLineComponentHeight - lineHeight) / 2}px;
 `;
 
+export const codeWrapperHoverStyles = css`
+  &:hover,
+  &[data-hover='true'] {
+    // On hover of the pre tag, the sibling copy button should be visible
+    & + .${copyButtonWithoutPanelClassName} {
+      opacity: 1;
+    }
+  }
+`;
+
 export function getExpandableCodeWrapperStyle(
   expanded: boolean,
   codeHeight: number,
@@ -137,11 +255,6 @@ export function getExpandableCodeWrapperStyle(
   `;
 }
 
-export const panelStyles = css`
-  z-index: 2; // Above the shadows
-  grid-area: panel;
-`;
-
 export function getCodeWrapperVariantStyle(theme: Theme): string {
   const colors = variantColors[theme];
 
@@ -175,7 +288,7 @@ export const expandButtonStyle = css`
   }
 `;
 
-export function getExpandButtonVariantStyle(theme: Theme): string {
+export function getExpandButtonUtilsVariantStyle(theme: Theme): string {
   const colors = variantColors[theme];
 
   return css`
@@ -216,19 +329,6 @@ export const baseScrollShadowStyles = css`
   }
 `;
 
-export const scrollShadowStylesNoPanel = css`
-  &:after {
-    grid-column: -1; // Placed on the right edge
-  }
-`;
-
-export const scrollShadowStylesWithPicker = css`
-  &:before,
-  &:after {
-    grid-row: 2; // Placed on the top under the Picker Panel
-  }
-`;
-
 export function getScrollShadow(
   scrollState: ScrollState,
   theme: Theme,
@@ -260,3 +360,11 @@ export function getScrollShadow(
     }
   `;
 }
+
+export const getLoadingStyles = (theme: Theme) =>
+  cx(css`
+    grid-area: code;
+    padding-block: ${spacing[400]}px 80px;
+    padding-inline: ${spacing[400]}px 28px;
+    background-color: ${variantColors[theme][0]};
+  `);
diff --git a/packages/code/src/Code/Code.tsx b/packages/code/src/Code/Code.tsx
index 85aee5ab58..5d4e605a6e 100644
--- a/packages/code/src/Code/Code.tsx
+++ b/packages/code/src/Code/Code.tsx
@@ -1,123 +1,74 @@
-import React, { useEffect, useMemo, useRef, useState } from 'react';
+import React, { useMemo, useRef, useState } from 'react';
 import ClipboardJS from 'clipboard';
 import debounce from 'lodash/debounce';
 
-import { cx } from '@leafygreen-ui/emotion';
 import { useIsomorphicLayoutEffect } from '@leafygreen-ui/hooks';
 import ChevronDown from '@leafygreen-ui/icon/dist/ChevronDown';
 import ChevronUp from '@leafygreen-ui/icon/dist/ChevronUp';
-import LeafyGreenProvider, {
-  useDarkMode,
-} from '@leafygreen-ui/leafygreen-provider';
-import { useBaseFontSize } from '@leafygreen-ui/leafygreen-provider';
-import { isComponentType } from '@leafygreen-ui/lib';
+import { useDarkMode } from '@leafygreen-ui/leafygreen-provider';
+import { CodeSkeleton } from '@leafygreen-ui/skeleton-loader';
+import { useUpdatedBaseFontSize } from '@leafygreen-ui/typography';
 
+import CodeContextProvider from '../CodeContext/CodeContext';
 import { numOfCollapsedLinesOfCode } from '../constants';
+import CopyButton from '../CopyButton/CopyButton';
 import { Panel } from '../Panel';
 import { Syntax } from '../Syntax';
-import { CodeProps, Language } from '../types';
-import { WindowChrome } from '../WindowChrome';
+import { Language } from '../types';
+import { DEFAULT_LGID_ROOT, getLgIds } from '../utils/getLgIds';
 
 import {
-  baseScrollShadowStyles,
-  codeWrapperStyle,
-  codeWrapperStyleNoPanel,
-  codeWrapperStyleWithLanguagePicker,
-  contentWrapperStyles,
-  contentWrapperStylesNoPanel,
-  contentWrapperStyleWithPicker,
-  expandableContentWrapperStyle,
-  expandableContentWrapperStyleNoPanel,
-  expandableContentWrapperStyleWithPicker,
-  expandButtonStyle,
-  getCodeWrapperVariantStyle,
-  getExpandableCodeWrapperStyle,
-  getExpandButtonVariantStyle,
-  getScrollShadow,
-  panelStyles,
-  scrollShadowStylesNoPanel,
-  scrollShadowStylesWithPicker,
-  singleLineCodeWrapperStyle,
-  wrapperStyle,
+  getCodeStyles,
+  getCodeWrapperStyles,
+  getCopyButtonWithoutPanelStyles,
+  getExpandedButtonStyles,
+  getLoadingStyles,
+  getWrapperStyles,
 } from './Code.styles';
-import { DetailedElementProps, ScrollState } from './Code.types';
-
-export function hasMultipleLines(string: string): boolean {
-  return string.trim().includes('\n');
-}
-
-function getHorizontalScrollbarHeight(element: HTMLElement): number {
-  return element.offsetHeight - element.clientHeight;
-}
+import {
+  CodeProps,
+  CopyButtonAppearance,
+  DetailedElementProps,
+  ScrollState,
+} from './Code.types';
+import { getHorizontalScrollbarHeight, hasMultipleLines } from './utils';
 
-/**
- *
- * React Component that outputs single-line and multi-line code blocks.
- *
- * @param props.children The string to be formatted.
- * @param props.className An additional CSS class added to the root element of Code.
- * @param props.language The language used for syntax highlighting.
- * @param props.darkMode Determines if the code block will be rendered in dark mode. Default: `false`
- * @param props.showLineNumbers When true, shows line numbers in preformatted code blocks. Default: `false`
- * @param props.lineNumberStart Specifies the numbering of the first line in the block. Default: 1
- * @param props.copyable When true, allows the code block to be copied to the user's clipboard. Default: `true`
- * @param props.onCopy Callback fired when Code is copied
- * @param props.expandable When true, allows the code block to be expanded and collapsed when there are more than 5 lined of code. Default: `false`
- */
 function Code({
-  children = '',
-  className,
   language: languageProp,
   darkMode: darkModeProp,
   showLineNumbers = false,
   lineNumberStart = 1,
-  showWindowChrome = false,
-  chromeTitle = '',
-  copyable = true,
   expandable = false,
-  onCopy,
+  isLoading = false,
   highlightLines = [],
-  languageOptions,
-  onChange,
-  customActionButtons = [],
+  copyButtonAppearance = CopyButtonAppearance.Hover,
+  children = '',
+  className,
+  onCopy,
+  panel,
+  'data-lgid': dataLgId = DEFAULT_LGID_ROOT,
+  baseFontSize: baseFontSizeProp,
+  // Deprecated props
+  copyable = false,
   showCustomActionButtons = false,
+  languageOptions = [],
+  customActionButtons,
+  chromeTitle,
+  onChange,
+  // rest
   ...rest
 }: CodeProps) {
   const scrollableElementRef = useRef(null);
   const [scrollState, setScrollState] = useState(ScrollState.None);
-  const [showCopyBar, setShowCopyBar] = useState(false);
   const [expanded, setExpanded] = useState(!expandable);
   const [numOfLinesOfCode, setNumOfLinesOfCode] = useState();
-  const [codeHeight, setCodeHeight] = useState();
-  const [collapsedCodeHeight, setCollapsedCodeHeight] = useState();
+  const [codeHeight, setCodeHeight] = useState(0);
+  const [collapsedCodeHeight, setCollapsedCodeHeight] = useState(0);
   const isMultiline = useMemo(() => hasMultipleLines(children), [children]);
   const { theme, darkMode } = useDarkMode(darkModeProp);
-  const baseFontSize = useBaseFontSize();
-
-  const filteredCustomActionIconButtons = customActionButtons.filter(
-    (item: React.ReactElement) => isComponentType(item, 'IconButton') === true,
-  );
-
-  const showCustomActionsInPanel =
-    showCustomActionButtons && !!filteredCustomActionIconButtons.length;
-
-  const currentLanguage = languageOptions?.find(
-    option => option.displayName === languageProp,
-  );
-
-  const showPanel =
-    !showWindowChrome &&
-    (copyable || !!currentLanguage || showCustomActionsInPanel);
-
-  const highlightLanguage = currentLanguage
-    ? currentLanguage.language
-    : languageProp;
-
-  const showLanguagePicker = !!currentLanguage;
+  const baseFontSize = useUpdatedBaseFontSize(baseFontSizeProp);
 
-  useEffect(() => {
-    setShowCopyBar(copyable && ClipboardJS.isSupported());
-  }, [copyable, showWindowChrome]);
+  const lgIds = getLgIds(dataLgId);
 
   useIsomorphicLayoutEffect(() => {
     const scrollableElement = scrollableElementRef.current;
@@ -159,6 +110,14 @@ function Code({
     baseFontSize, // will cause changes in code height
   ]);
 
+  const currentLanguage = languageOptions?.find(
+    option => option.displayName === languageProp,
+  );
+
+  const highlightLanguage = currentLanguage
+    ? currentLanguage.language
+    : languageProp;
+
   const renderedSyntaxComponent = (
      numOfCollapsedLinesOfCode
+    numOfLinesOfCode > numOfCollapsedLinesOfCode &&
+    !isLoading
   );
 
-  return (
-    
-      
- {showWindowChrome && } + // TODO: remove when deprecated props are removed https://jira.mongodb.org/browse/LG-4909 + const hasDeprecatedCustomActionButtons = + showCustomActionButtons && + !!customActionButtons && + customActionButtons.length > 0; + + // TODO: remove when deprecated props are removed https://jira.mongodb.org/browse/LG-4909 + const hasDeprecatedLanguageSwitcher = + !!languageOptions && + languageOptions.length > 0 && + !!currentLanguage && + !!onChange; + + // This will render a temp deprecated panel component if deprecated props are used + // TODO: remove when deprecated props are removed https://jira.mongodb.org/browse/LG-4909 + const shouldRenderDeprecatedPanel = + !panel && + (hasDeprecatedCustomActionButtons || + hasDeprecatedLanguageSwitcher || + !!chromeTitle || + copyable); + + // TODO: remove when deprecated props are removed. Should only check panel https://jira.mongodb.org/browse/LG-4909 + const showPanel = !!panel || shouldRenderDeprecatedPanel; + + const showCopyButtonWithoutPanel = + !showPanel && + copyButtonAppearance !== CopyButtonAppearance.None && + ClipboardJS.isSupported() && + !isLoading; + return ( + +
-
)}
-            className={cx(
-              codeWrapperStyle,
-              getCodeWrapperVariantStyle(theme),
-              {
-                [codeWrapperStyleWithLanguagePicker]: showLanguagePicker,
-                [codeWrapperStyleNoPanel]: !showPanel,
-                [singleLineCodeWrapperStyle]: !isMultiline,
-                [getExpandableCodeWrapperStyle(
-                  expanded,
-                  codeHeight as number,
-                  collapsedCodeHeight as number,
-                )]: showExpandButton,
-              },
-              className,
-            )}
-            onScroll={onScroll}
-            ref={scrollableElementRef}
-            // Adds to Tab order when content is scrollable, otherwise overflowing content is inaccessible via keyboard navigation
-            // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
-            tabIndex={scrollState !== ScrollState.None ? 0 : -1}
-          >
-            {renderedSyntaxComponent}
-          
- - {/* Can make this a more robust check in the future */} - {/* Right now the panel will only be rendered with copyable or a language switcher */} - {showPanel && ( - )} + className={getCodeWrapperStyles({ + theme, + showPanel, + expanded, + codeHeight, + collapsedCodeHeight, + isMultiline, + showExpandButton, + className, + })} + onScroll={onScroll} + ref={scrollableElementRef} + // Adds to Tab order when content is scrollable, otherwise overflowing content is inaccessible via keyboard navigation + // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex + tabIndex={scrollState !== ScrollState.None ? 0 : -1} + > + {renderedSyntaxComponent} +
+ )} + + {isLoading && ( + + )} + + {/* This div is below the pre tag so that we can target it using the css sibiling selector when the pre tag is hovered */} + {showCopyButtonWithoutPanel && ( + + )} + + {!!panel && panel} + + {/* if there are deprecated props then manually render the panel component */} + {/* TODO: remove when deprecated props are removed, https://jira.mongodb.org/browse/LG-4909 */} + {shouldRenderDeprecatedPanel && ( + {})} // No-op function as default onCopy={onCopy} - showCopyButton={showCopyBar} - isMultiline={isMultiline} - customActionButtons={filteredCustomActionIconButtons} - showCustomActionButtons={showCustomActionsInPanel} /> )} {showExpandButton && (
- + ); } diff --git a/packages/code/src/Code/Code.types.ts b/packages/code/src/Code/Code.types.ts index 85edc34d48..c10baf0c3e 100644 --- a/packages/code/src/Code/Code.types.ts +++ b/packages/code/src/Code/Code.types.ts @@ -1,3 +1,10 @@ +import { DarkModeProps, LgIdProps } from '@leafygreen-ui/lib'; +import { BaseFontSize } from '@leafygreen-ui/tokens'; + +import { LanguageOption } from '../Panel/Panel.types'; +import { SyntaxProps } from '../Syntax/Syntax.types'; +import { Language } from '../types'; + export const ScrollState = { None: 'none', Left: 'left', @@ -7,7 +14,148 @@ export const ScrollState = { export type ScrollState = (typeof ScrollState)[keyof typeof ScrollState]; +export const CopyButtonAppearance = { + Hover: 'hover', + Persist: 'persist', + None: 'none', +} as const; + +export type CopyButtonAppearance = + (typeof CopyButtonAppearance)[keyof typeof CopyButtonAppearance]; + export type DetailedElementProps = React.DetailedHTMLProps< React.HTMLAttributes, T >; + +export type CodeProps = Omit & + LgIdProps & + DarkModeProps & { + /** + * Makes code blocks longer than 5 lines long expandable + * + * @default `false` + */ + expandable?: boolean; + + /** + * Callback fired when Code is copied via the copy button. + * + */ + onCopy?: Function; + + /** + * The language to format the code. See {@link https://github.com/mongodb/leafygreen-ui/blob/main/packages/code/src/languages.ts | SupportedLanguages}. + */ + + language: Language | LanguageOption['displayName']; + + /** + * Determines whether or not the loading skeleton will be rendered in place of the code block. + * + * @default `false` + */ + isLoading?: boolean; + + /** + * Custom action buttons. Should be an array of `IconButton`. + * + * @type [] + * use `` instead + * + * @deprecated + */ + customActionButtons?: Array; + + /** + * When true, custom action buttons will be shown. + * + * Use `panel={}` instead + * + *@deprecated + */ + showCustomActionButtons?: boolean; + + /** + * Renders a file name or other descriptor for a block of code + * + * Use `panel={}` instead + * + * @deprecated + */ + chromeTitle?: string; + + /** + * use `panel={}` instead + * @deprecated + */ + languageOptions?: Array; + + /** + * use `panel={}` instead + * @deprecated + */ + onChange?: (arg0: LanguageOption) => void; + + /** + * When true, allows the code block to be copied to the user's clipboard by clicking the rendered copy button. + * + * Use `panel={}` or `copyButtonAppearance` instead + * + * @default `false` + * @deprecated + */ + copyable?: boolean; + + /** + * Determines the base font-size of the component + * + * @default 13 + */ + baseFontSize?: BaseFontSize; + } & ( + | { + /** + * Determines the appearance of the copy button without a panel. The copy button allows the code block to be copied to the user's clipboard by clicking the button. + * + * If `hover`, the copy button will only appear when the user hovers over the code block. On mobile devices, the copy button will always be visible. + * + * If `persist`, the copy button will always be visible. + * + * If `none`, the copy button will not be rendered. + * + * Note: 'panel' cannot be used with `copyButtonAppearance`. Either use `copyButtonAppearance` or `panel`, not both. + * + * @default `hover` + */ + copyButtonAppearance?: CopyButtonAppearance; + + /** + * Slot to pass the `` sub-component which will render the top panel with a language switcher, custom action buttons, and copy button. If no props are passed to the panel sub-component, the panel will render with only the copy button. Note: `copyButtonAppearance` cannot be used with `panel`. Either use `copyButtonAppearance` or `panel`, not both. + * + */ + panel?: never; + } + | { + /** + * Determines the appearance of the copy button without a panel. The copy button allows the code block to be copied to the user's clipboard by clicking the button. + * + * If `hover`, the copy button will only appear when the user hovers over the code block. On mobile devices, the copy button will always be visible. + * + * If `persist`, the copy button will always be visible. + * + * If `none`, the copy button will not be rendered. + * + * Note: 'panel' cannot be used with `copyButtonAppearance`. Either use `copyButtonAppearance` or `panel`, not both. + * + * @default `hover` + */ + copyButtonAppearance?: never; + + /** + * Slot to pass the `` sub-component which will render the top panel with a language switcher, custom action buttons, and copy button. If no props are passed to the panel sub-component, the panel will render with only the copy button. Note: `copyButtonAppearance` cannot be used with `panel`. Either use `copyButtonAppearance` or `panel`, not both. + * + */ + panel?: React.ReactNode; + } + ); diff --git a/packages/code/src/Code/index.ts b/packages/code/src/Code/index.ts index 659f6dc6f3..c521a9e3dc 100644 --- a/packages/code/src/Code/index.ts +++ b/packages/code/src/Code/index.ts @@ -1 +1,2 @@ export { default as Code } from './Code'; +export { CopyButtonAppearance } from './Code.types'; diff --git a/packages/code/src/Code/utils/getHorizontalScrollbarHeight.ts b/packages/code/src/Code/utils/getHorizontalScrollbarHeight.ts new file mode 100644 index 0000000000..b1049c4fba --- /dev/null +++ b/packages/code/src/Code/utils/getHorizontalScrollbarHeight.ts @@ -0,0 +1,8 @@ +/** + * Get the height of the horizontal scrollbar of an element. + * @param element The element to get the scrollbar height of. + * @returns The height of the horizontal scrollbar. + */ +export function getHorizontalScrollbarHeight(element: HTMLElement): number { + return element.offsetHeight - element.clientHeight; +} diff --git a/packages/code/src/Code/utils/hasMultipleLines.ts b/packages/code/src/Code/utils/hasMultipleLines.ts new file mode 100644 index 0000000000..ffe6007843 --- /dev/null +++ b/packages/code/src/Code/utils/hasMultipleLines.ts @@ -0,0 +1,8 @@ +/** + * Determines if a string has multiple lines. This is determined by the presence of a newline character + * @param string + * @returns boolean + */ +export function hasMultipleLines(string: string): boolean { + return string.trim().includes('\n'); +} diff --git a/packages/code/src/Code/utils/index.ts b/packages/code/src/Code/utils/index.ts new file mode 100644 index 0000000000..714bd013cd --- /dev/null +++ b/packages/code/src/Code/utils/index.ts @@ -0,0 +1,2 @@ +export { getHorizontalScrollbarHeight } from './getHorizontalScrollbarHeight'; +export { hasMultipleLines } from './hasMultipleLines'; diff --git a/packages/code/src/CodeContext/CodeContext.tsx b/packages/code/src/CodeContext/CodeContext.tsx new file mode 100644 index 0000000000..fad595815f --- /dev/null +++ b/packages/code/src/CodeContext/CodeContext.tsx @@ -0,0 +1,48 @@ +import React, { + createContext, + PropsWithChildren, + useContext, + useMemo, +} from 'react'; + +import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; + +import { type CodeProviderProps } from './CodeContext.types'; + +export const CodeContext = createContext>({}); + +export const useCodeContext = () => + useContext( + CodeContext as React.Context, + ); + +const CodeContextProvider = ({ + children, + contents, + darkMode, + language, + isLoading, + showPanel, + lgids, +}: PropsWithChildren) => { + const CodeProvider = (CodeContext as React.Context) + .Provider; + + const CodeProviderData = useMemo(() => { + return { + contents, + language, + showPanel, + isLoading, + lgids, + }; + }, [contents, language, showPanel, isLoading, lgids]); + + return ( + + {children} + + ); +}; + +export default CodeContextProvider; diff --git a/packages/code/src/CodeContext/CodeContext.types.ts b/packages/code/src/CodeContext/CodeContext.types.ts new file mode 100644 index 0000000000..5dc6fffcdc --- /dev/null +++ b/packages/code/src/CodeContext/CodeContext.types.ts @@ -0,0 +1,32 @@ +import { DarkModeProps } from '@leafygreen-ui/lib'; + +import { LanguageOption } from '../Panel/Panel.types'; +import { Language } from '../types'; +import { GetLgIdsReturnType } from '../utils'; + +export type CodeProviderProps = DarkModeProps & { + /** + * The contents of the code snippet. + */ + contents: string; + + /** + * The language of the code snippet. + */ + language: Language | LanguageOption['displayName']; + + /** + * Whether or not the code snippet has a panel. + */ + showPanel: boolean; + + /** + * Whether the loading skeleton should be shown. + */ + isLoading: boolean; + + /** + * LGIDs for the code snippet. + */ + lgids: GetLgIdsReturnType; +}; diff --git a/packages/code/src/CopyButton/CopyButton.spec.tsx b/packages/code/src/CopyButton/CopyButton.spec.tsx index a6013fdbf8..9a69e1f04f 100644 --- a/packages/code/src/CopyButton/CopyButton.spec.tsx +++ b/packages/code/src/CopyButton/CopyButton.spec.tsx @@ -3,9 +3,11 @@ import { fireEvent, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import ClipboardJS from 'clipboard'; -import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { keyMap } from '@leafygreen-ui/lib'; +import CodeContextProvider from '../CodeContext/CodeContext'; +import { getLgIds } from '../utils'; + import { COPIED_SUCCESS_DURATION, COPIED_TEXT, COPY_TEXT } from './constants'; import CopyButton from './CopyButton'; import { CopyProps } from './CopyButton.types'; @@ -18,16 +20,19 @@ jest.mock('clipboard', () => { describe('CopyButton', () => { const contents = 'Lorem ipsum'; - const testIds = { - copyButton: 'code_copy-button', - tooltip: 'code_copy-button_tooltip', - }; + const testIds = getLgIds(); const renderCopyButton = ({ onCopy }: Pick) => { return render( - + - , + , ); }; @@ -44,13 +49,13 @@ describe('CopyButton', () => { test(`tooltip displays "${COPY_TEXT}" text while trigger is hovered`, async () => { const { getByTestId, queryByTestId } = renderCopyButton({}); const copyButton = getByTestId(testIds.copyButton); - let tooltip = queryByTestId(testIds.tooltip); + let tooltip = queryByTestId(testIds.copyTooltip); expect(tooltip).toBeNull(); fireEvent.mouseEnter(copyButton); await waitFor(() => { - tooltip = queryByTestId(testIds.tooltip); + tooltip = queryByTestId(testIds.copyTooltip); expect(tooltip).toHaveTextContent(COPY_TEXT); }); }); @@ -61,7 +66,7 @@ describe('CopyButton', () => { userEvent.click(copyButton); - let tooltip = queryByTestId(testIds.tooltip); + let tooltip = queryByTestId(testIds.copyTooltip); await waitFor(() => { expect(tooltip).toHaveTextContent(COPIED_TEXT); }); @@ -69,7 +74,7 @@ describe('CopyButton', () => { jest.advanceTimersByTime(COPIED_SUCCESS_DURATION); await waitFor(() => { - tooltip = queryByTestId(testIds.tooltip); + tooltip = queryByTestId(testIds.copyTooltip); expect(tooltip).toHaveTextContent(COPY_TEXT); }); }); @@ -78,18 +83,18 @@ describe('CopyButton', () => { test('opens tooltip onMouseEnter and closes tooltip onMouseLeave', async () => { const { getByTestId, queryByTestId } = renderCopyButton({}); const copyButton = getByTestId(testIds.copyButton); - let tooltip = queryByTestId(testIds.tooltip); + let tooltip = queryByTestId(testIds.copyTooltip); expect(tooltip).toBeNull(); await waitFor(() => { fireEvent.mouseEnter(copyButton); - tooltip = queryByTestId(testIds.tooltip); + tooltip = queryByTestId(testIds.copyTooltip); expect(tooltip).toBeInTheDocument(); }); await waitFor(() => { fireEvent.mouseLeave(copyButton); - tooltip = queryByTestId(testIds.tooltip); + tooltip = queryByTestId(testIds.copyTooltip); expect(tooltip).toBeNull(); }); }); @@ -112,20 +117,20 @@ describe('CopyButton', () => { test('closes tooltip when clicking out of focused button', async () => { const { queryByTestId } = renderCopyButton({}); - let tooltip = queryByTestId(testIds.tooltip); + let tooltip = queryByTestId(testIds.copyTooltip); expect(tooltip).toBeNull(); userEvent.tab(); await waitFor(() => { - tooltip = queryByTestId(testIds.tooltip); + tooltip = queryByTestId(testIds.copyTooltip); expect(tooltip).toBeInTheDocument(); }); fireEvent.click(document); await waitFor(() => { - tooltip = queryByTestId(testIds.tooltip); + tooltip = queryByTestId(testIds.copyTooltip); expect(tooltip).toBeNull(); }); }); @@ -137,20 +142,20 @@ describe('CopyButton', () => { async key => { const { getByTestId, queryByTestId } = renderCopyButton({}); const copyButton = getByTestId(testIds.copyButton); - let tooltip = queryByTestId(testIds.tooltip); + let tooltip = queryByTestId(testIds.copyTooltip); expect(tooltip).toBeNull(); userEvent.tab(); await waitFor(() => { - tooltip = queryByTestId(testIds.tooltip); + tooltip = queryByTestId(testIds.copyTooltip); expect(tooltip).toBeInTheDocument(); }); fireEvent.keyDown(copyButton, { key }); await waitFor(() => { - tooltip = queryByTestId(testIds.tooltip); + tooltip = queryByTestId(testIds.copyTooltip); expect(tooltip).toBeNull(); }); }, @@ -162,20 +167,20 @@ describe('CopyButton', () => { const onCopy = jest.fn(); const { getByTestId, queryByTestId } = renderCopyButton({ onCopy }); const copyButton = getByTestId(testIds.copyButton); - let tooltip = queryByTestId(testIds.tooltip); + let tooltip = queryByTestId(testIds.copyTooltip); expect(tooltip).toBeNull(); userEvent.tab(); await waitFor(() => { - tooltip = queryByTestId(testIds.tooltip); + tooltip = queryByTestId(testIds.copyTooltip); expect(tooltip).toBeInTheDocument(); }); fireEvent.keyDown(copyButton, { key }); await waitFor(() => { - tooltip = queryByTestId(testIds.tooltip); + tooltip = queryByTestId(testIds.copyTooltip); expect(onCopy).toHaveBeenCalledTimes(1); expect(ClipboardJS).toHaveBeenCalledWith(copyButton, { text: expect.any(Function), @@ -186,4 +191,7 @@ describe('CopyButton', () => { }, ); }); + + // TODO: get this to work https://jira.mongodb.org/browse/LG-4760 + test.todo('copies the correct text when copy button is clicked'); }); diff --git a/packages/code/src/CopyButton/CopyButton.styles.ts b/packages/code/src/CopyButton/CopyButton.styles.ts index 67b845f29c..c619fbc7c9 100644 --- a/packages/code/src/CopyButton/CopyButton.styles.ts +++ b/packages/code/src/CopyButton/CopyButton.styles.ts @@ -1,35 +1,82 @@ -import { css } from '@leafygreen-ui/emotion'; +import { css, cx } from '@leafygreen-ui/emotion'; import { Theme } from '@leafygreen-ui/lib'; import { palette } from '@leafygreen-ui/palette'; +import { color, transitionDuration } from '@leafygreen-ui/tokens'; -export const tooltipStyles = css` - svg { - width: 26px; - height: 26px; - } -`; +export const getCopyButtonStyles = ({ + theme, + copied, + showPanel, + className, +}: { + theme: Theme; + copied: boolean; + showPanel: boolean; + className?: string; +}) => + cx( + css` + align-self: center; + + &[aria-disabled='false'] { + color: ${color[theme].icon.primary.default}; + } + + div[role='tooltip'] svg { + width: 26px; + height: 26px; + } + + &, + & > div > svg { + transition: all ${transitionDuration.default}ms ease-in-out; + } + `, + { + [copiedThemeStyle[theme]]: copied, + [minimalButtonThemeStyle[theme]]: !showPanel, + }, + className, + ); export const copiedThemeStyle: Record = { [Theme.Light]: css` - color: ${palette.white}; + &, + & > div > svg { + color: ${palette.white}; + + &:focus, + &:hover { + color: ${palette.white}; + } + } + background-color: ${palette.green.dark1}; &:focus, &:hover { - color: ${palette.white}; - + background-color: ${palette.green.dark1}; &:before { background-color: ${palette.green.dark1}; } } `, [Theme.Dark]: css` - color: ${palette.gray.dark3}; + &, + & > div > svg { + color: ${palette.gray.dark3}; + + &:focus, + &:hover { + color: ${palette.gray.dark3}; + } + } + background-color: ${palette.green.base}; &:focus, &:hover { - color: ${palette.gray.dark3}; + background-color: ${palette.green.base}; &:before { background-color: ${palette.green.base}; @@ -38,13 +85,19 @@ export const copiedThemeStyle: Record = { `, }; -export const copyButtonThemeStyles: Record = { +export const getMinimalButtonCopiedStyles = ({ + theme, +}: { + theme: Theme; +}) => css` + border-color: ${color[theme].icon.primary.default}; +`; + +export const minimalButtonThemeStyle: Record = { [Theme.Light]: css` - align-self: center; - color: ${palette.gray.base}; + border-color: ${palette.gray.base}; `, [Theme.Dark]: css` - align-self: center; - color: ${palette.gray.light1}; + border-color: ${palette.gray.light2}; `, }; diff --git a/packages/code/src/CopyButton/CopyButton.tsx b/packages/code/src/CopyButton/CopyButton.tsx index adeb86f111..0fa40c10cb 100644 --- a/packages/code/src/CopyButton/CopyButton.tsx +++ b/packages/code/src/CopyButton/CopyButton.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import ClipboardJS from 'clipboard'; import { VisuallyHidden } from '@leafygreen-ui/a11y'; -import { cx } from '@leafygreen-ui/emotion'; +import Button from '@leafygreen-ui/button'; import { useBackdropClick } from '@leafygreen-ui/hooks'; import CheckmarkIcon from '@leafygreen-ui/icon/dist/Checkmark'; import CopyIcon from '@leafygreen-ui/icon/dist/Copy'; @@ -19,15 +19,13 @@ import Tooltip, { RenderMode, } from '@leafygreen-ui/tooltip'; +import { useCodeContext } from '../CodeContext/CodeContext'; + import { COPIED_SUCCESS_DURATION, COPIED_TEXT, COPY_TEXT } from './constants'; -import { - copiedThemeStyle, - copyButtonThemeStyles, - tooltipStyles, -} from './CopyButton.styles'; +import { getCopyButtonStyles } from './CopyButton.styles'; import { CopyProps } from './CopyButton.types'; -function CopyButton({ onCopy, contents }: CopyProps) { +function CopyButton({ onCopy, contents, className, ...rest }: CopyProps) { const [copied, setCopied] = useState(false); /** * `CopyButton` controls `tooltipOpen` state because when `copied` state @@ -38,6 +36,7 @@ function CopyButton({ onCopy, contents }: CopyProps) { const timeoutRef = useRef(null); const { theme } = useDarkMode(); const { portalContainer } = usePopoverPortalContainer(); + const { showPanel, isLoading, lgids } = useCodeContext(); /** * toggles `tooltipOpen` state @@ -123,33 +122,52 @@ function CopyButton({ onCopy, contents }: CopyProps) { */ const shouldClose = () => !tooltipOpen; + const sharedButtonProps = { + 'aria-label': COPY_TEXT, + 'data-testid': lgids.copyButton, + 'data-lgid': lgids.copyButton, + className: getCopyButtonStyles({ + theme, + copied, + showPanel, + className, + }), + onClick: handleClick, + onKeyDown: handleKeyDown, + onMouseEnter: handleMouseEnter, + onMouseLeave: handleMouseLeave, + ref: buttonRef, + disabled: isLoading, + ...rest, + }; + return ( - {copied ? : } - {copied && ( - {COPIED_TEXT} - )} - + showPanel ? ( + + {copied ? : } + {copied && ( + {COPIED_TEXT} + )} + + ) : ( + + ) } shouldClose={shouldClose} > diff --git a/packages/code/src/CopyButton/CopyButton.types.ts b/packages/code/src/CopyButton/CopyButton.types.ts index fa9d554add..3c4f70f483 100644 --- a/packages/code/src/CopyButton/CopyButton.types.ts +++ b/packages/code/src/CopyButton/CopyButton.types.ts @@ -1,4 +1,7 @@ -export interface CopyProps { +import { ComponentPropsWithoutRef } from 'react'; + +export interface CopyProps + extends Omit, 'onCopy'> { onCopy?: Function; contents: string; withLanguageSwitcher?: boolean; diff --git a/packages/code/src/CustomSelectMenuButton/CustomSelectMenuButton.tsx b/packages/code/src/CustomSelectMenuButton/CustomSelectMenuButton.tsx deleted file mode 100644 index 8b84670c6c..0000000000 --- a/packages/code/src/CustomSelectMenuButton/CustomSelectMenuButton.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; - -import Button, { BaseButtonProps } from '@leafygreen-ui/button'; - -/** - * Custom language switcher button. - * - * Passing down just the function which will be instantiated inside `Select` - * @internal - */ -export const CustomSelectMenuButton = React.forwardRef< - HTMLButtonElement, - BaseButtonProps ->(({ children, ...props }, ref) => ( - -)); - -CustomSelectMenuButton.displayName = 'CustomSelectMenuButton'; diff --git a/packages/code/src/CustomSelectMenuButton/index.ts b/packages/code/src/CustomSelectMenuButton/index.ts deleted file mode 100644 index 863dfeabe5..0000000000 --- a/packages/code/src/CustomSelectMenuButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CustomSelectMenuButton } from './CustomSelectMenuButton'; diff --git a/packages/code/src/LanguageSwitcher/LanguageSwitcher.styles.ts b/packages/code/src/LanguageSwitcher/LanguageSwitcher.styles.ts index 7f81a42697..2a5385e71d 100644 --- a/packages/code/src/LanguageSwitcher/LanguageSwitcher.styles.ts +++ b/packages/code/src/LanguageSwitcher/LanguageSwitcher.styles.ts @@ -1,7 +1,4 @@ import { css } from '@leafygreen-ui/emotion'; -import { Theme } from '@leafygreen-ui/lib'; -import { palette } from '@leafygreen-ui/palette'; -import { spacing } from '@leafygreen-ui/tokens'; export const containerStyle = css` display: flex; @@ -10,80 +7,7 @@ export const containerStyle = css` height: 100%; `; -export const menuButtonStyle = css` - // Override default menuButton styles - margin-top: 0; - width: 100%; - height: 100%; - border-radius: 0px; - border: 0; - font-size: 12px; - - &:hover[aria-disabled='false'], - &:focus, - &:active { - box-shadow: 0 0 0 0; - border: 0; - } - - // Override button defaults - > *:last-child { - grid-template-columns: 16px 1fr 16px; - padding: 0 12px; - > svg { - width: 16px; - height: 16px; - } - } -`; - -export const buttonModeStyle: Record = { - [Theme.Light]: css` - background-color: ${palette.white}; - border-right: 1px solid ${palette.gray.light2}; - box-shadow: 0 0 0 0; - - &:hover[aria-disabled='false'], - &:active, - &:focus { - border-right: 1px solid ${palette.gray.light2}; - } - - &:hover[aria-disabled='false'] { - background-color: ${palette.gray.light2}; - } - - &:focus-visible { - background-color: ${palette.blue.light2}; - } - `, - [Theme.Dark]: css` - background-color: ${palette.gray.dark2}; - border-right: 1px solid ${palette.gray.dark1}; - color: ${palette.gray.light2}; - - &:hover[aria-disabled='false'], - &:focus, - &:active { - border-right: 1px solid ${palette.gray.dark1}; - } - - &:hover[aria-disabled='false'], - &:active { - background-color: ${palette.gray.dark1}; - } - - &:focus-visible { - background-color: ${palette.blue.light1}; - } - `, -}; - export const selectStyle = css` min-width: 144px; height: 100%; `; - -export const iconMargin = css` - margin-right: ${spacing[3]}px; -`; diff --git a/packages/code/src/LanguageSwitcher/LanguageSwitcher.tsx b/packages/code/src/LanguageSwitcher/LanguageSwitcher.tsx index efadb13450..6860f8cd47 100644 --- a/packages/code/src/LanguageSwitcher/LanguageSwitcher.tsx +++ b/packages/code/src/LanguageSwitcher/LanguageSwitcher.tsx @@ -1,32 +1,13 @@ import React from 'react'; -import { css, cx } from '@leafygreen-ui/emotion'; import { usePrevious } from '@leafygreen-ui/hooks'; -import { isComponentGlyph } from '@leafygreen-ui/icon'; -import FileIcon from '@leafygreen-ui/icon/dist/File'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; -import { isComponentType } from '@leafygreen-ui/lib'; -import { palette } from '@leafygreen-ui/palette'; -import { Option, RenderMode, Select } from '@leafygreen-ui/select'; +import { Option, RenderMode, Select, Size } from '@leafygreen-ui/select'; -import { CustomSelectMenuButton } from '../CustomSelectMenuButton'; -import { LanguageOption } from '../types'; +import { useCodeContext } from '../CodeContext/CodeContext'; +import { LanguageOption } from '../Panel/Panel.types'; -import { - buttonModeStyle, - containerStyle, - iconMargin, - menuButtonStyle, - selectStyle, -} from './LanguageSwitcher.styles'; - -function isLeafyGreenIcon(element: React.ReactNode) { - if (isComponentGlyph(element) || isComponentType(element, 'Icon')) { - return true; - } - - return false; -} +import { containerStyle, selectStyle } from './LanguageSwitcher.styles'; interface Props { language: LanguageOption; @@ -35,8 +16,9 @@ interface Props { } function LanguageSwitcher({ language, languageOptions, onChange }: Props) { - const { theme, darkMode } = useDarkMode(); + const { darkMode } = useDarkMode(); const previousLanguage = usePrevious(language); + const { isLoading, lgids } = useCodeContext(); const handleChange = (val: string) => { if (val === '' && previousLanguage !== undefined) { @@ -52,28 +34,6 @@ function LanguageSwitcher({ language, languageOptions, onChange }: Props) { } }; - const iconStyle = cx( - iconMargin, - css` - color: ${darkMode ? palette.gray.light1 : palette.gray.base}; - `, - ); - - // Placeholder for file icon - let renderedLogo = ; - - if (language.image != null) { - if (isLeafyGreenIcon(language.image)) { - renderedLogo = React.cloneElement(language.image, { - className: iconStyle, - }); - } else { - renderedLogo = React.cloneElement(language.image, { - className: iconMargin, - }); - } - } - return (