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
-
+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 (
+
- {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}
+ )}
+
+ ) : (
+ : }
+ size="xsmall"
+ {...sharedButtonProps}
+ >
+ {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 (