{}}
+ 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}
+
+```
+
+### `baseFontSize`
+Adds `baseFontSize` prop, which allows you to override the `LeafyGreenProvider` value.
+
+### 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.
+
+## 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/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..b55823c534 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' ));
-
-
-
-
-
-
- Copy Icon
-
-
-
-
-
-
-
-
-
-
+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,148 @@ 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, getExpandButtonUtils } = getTestUtils();
+ const { getInput, getOptions, getOptionByValue, getInputValue, isDisabled: isLanguageSwitcherDisabled } = getLanguageSwitcherUtils();
+ const { getButton, queryButton, findButton, isDisabled } = getCopyButtonUtils();
+ const { getButton, queryButton, findButton } = getExpandButtonUtils();
+
+ 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().getButton()).toBeInTheDocument();
+ expect(getCopyButtonUtils().findButton()).toBeInTheDocument();
+ expect(getCopyButtonUtils().queryButton()).toBeInTheDocument();
+ expect(getCopyButtonUtils().isDisabled()).toBe(false);
+ expect(getExpandButtonUtils().getButton()).toBeInTheDocument();
+ expect(getExpandButtonUtils().findButton()).toBeInTheDocument();
+ expect(getExpandButtonUtils().queryButton()).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,
+ getIsLoading,
+ getTitle,
+ queryPanel,
+ getLanguageSwitcherUtils: {
+ getInput,
+ getOptions,
+ getOptionByValue,
+ isDisabled,
+ },
+ getCopyButtonUtils: { getButton, queryButton, findButton, isDisabled },
+ getExpandButtonUtils: { getButton, queryButton, findButton },
+} = getTestUtils();
+```
+
+| Util | Description | Returns |
+| ---------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
+| `getLanguage()` | Returns the current language of the code block | `string` |
+| `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) |
+| `getExpandButtonUtils()` | Returns utils for interacting with the expand button | [Button test utils return type](https://github.com/mongodb/leafygreen-ui/blob/main/packages/button/README.md#test-utils) |
+| `getIsExpanded()` | Returns whether the code block is expanded | `boolean` |
+| `getTitle()` | Returns the title of the code block | `string` \| `null` |
+| `queryPanel()` | Returns the panel element | `HTMLElement` \| `null` |
+
+### 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 75e3ce1e5a..2f86a801d7 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..3887f9d9d3 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 { queryPanel } = renderCode();
+ expect(queryPanel()).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 when no props are passed', () => {
+ const { queryPanel } = renderCode({ panel: });
+ expect(queryPanel()).toBeDefined();
+ });
+
+ test('panel when onCopy is passed', () => {
+ const { queryPanel } = renderCode({
+ panel: {}} />,
+ });
+ expect(queryPanel()).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 { queryPanel } = renderCode({
+ panel: (
+
+ ),
+ });
+ expect(queryPanel()).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,14 @@ describe('packages/Code', () => {
(_, i) => `const greeting${i} = "Hello, world! ${i}";`,
).join('\n');
- test(`shows no expand button when <= ${numOfCollapsedLinesOfCode} lines of code`, () => {
+ test(`returns null and shows no expand button when <= ${numOfCollapsedLinesOfCode} lines of code`, () => {
render(
{getCodeSnippet(numOfCollapsedLinesOfCode - 1)}
,
);
-
- expect(screen.queryByTestId('lg-code-expand_button')).toBeNull();
+ const { getExpandButtonUtils } = getTestUtils();
+ expect(getExpandButtonUtils().queryButton()).toBeNull();
});
test(`shows expand button when > ${numOfCollapsedLinesOfCode} lines of code`, () => {
@@ -263,7 +573,9 @@ describe('packages/Code', () => {
,
);
- expect(screen.getByTestId('lg-code-expand_button')).toBeInTheDocument();
+ const { getExpandButtonUtils } = getTestUtils();
+
+ expect(getExpandButtonUtils().getButton()).toBeInTheDocument();
});
test('shows correct number of lines of code on expand button', () => {
@@ -275,7 +587,9 @@ describe('packages/Code', () => {
,
);
- const actionButton = screen.getByTestId('lg-code-expand_button');
+ const { getExpandButtonUtils } = getTestUtils();
+
+ const actionButton = getExpandButtonUtils().getButton();
expect(actionButton).toHaveTextContent(
`Click to expand (${lineCount} lines)`,
);
@@ -288,9 +602,12 @@ describe('packages/Code', () => {
,
);
- const actionButton = screen.getByTestId('lg-code-expand_button');
- fireEvent.click(actionButton);
+ const { getExpandButtonUtils, getIsExpanded } = getTestUtils();
+
+ const actionButton = getExpandButtonUtils().getButton();
+ 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 +619,257 @@ describe('packages/Code', () => {
,
);
- const actionButton = screen.getByTestId('lg-code-expand_button');
- fireEvent.click(actionButton); // Expand
- fireEvent.click(actionButton); // Collapse
+ const { getExpandButtonUtils } = getTestUtils();
+
+ const actionButton = getExpandButtonUtils().getButton();
+ 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 { queryPanel } = renderCode({
+ customActionButtons,
+ });
+ expect(queryPanel()).toBeNull();
+ });
+ test('does not renders a panel with custom action buttons when only showCustomActionButtons is true', () => {
+ const { queryPanel } = renderCode({
+ showCustomActionButtons: true,
+ });
+ expect(queryPanel()).toBeNull();
+ });
+ test('renders a panel with with custom action buttons when showCustomActionButtons is true and customActionButtons is passed', () => {
+ const { queryPanel } = renderCode({
+ showCustomActionButtons: true,
+ customActionButtons,
+ });
+ expect(queryPanel()).toBeDefined();
+ });
+ });
+
+ describe('language switcher', () => {
+ test('renders a panel when only language, onChange, and languageOptions are defined', () => {
+ const { queryPanel } = renderCode({
+ language: languageOptions[0].displayName,
+ languageOptions,
+ onChange: () => {},
+ });
+ expect(queryPanel()).toBeDefined();
+ });
+ test('does not render a panel when language and onChange are defined but languageOptions is not defined', () => {
+ const { queryPanel } = renderCode({
+ language: languageOptions[0].displayName,
+ onChange: () => {},
+ });
+ expect(queryPanel()).toBeNull();
+ });
+ test('does not render a panel when language and languageOptions are defined but onChange is not defined', () => {
+ const { queryPanel } = renderCode({
+ language: languageOptions[0].displayName,
+ languageOptions,
+ });
+ expect(queryPanel()).toBeNull();
+ });
+ test('does not render a panel when languageOptions is an empty array', () => {
+ const { queryPanel } = renderCode({
+ language: languageOptions[0].displayName,
+ languageOptions: [],
+ onChange: () => {},
+ });
+ expect(queryPanel()).toBeNull();
+ });
+ test('does not render a panel if language is a string', () => {
+ const { queryPanel } = renderCode({
+ language: 'javascript',
+ languageOptions: [],
+ onChange: () => {},
+ });
+ expect(queryPanel()).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 { queryPanel } = Context.within(
+ Jest.spyContext(ClipboardJS, 'isSupported'),
+ spy => {
+ spy.mockReturnValue(true);
+ return renderCode({
+ copyable: false,
+ });
+ },
+ );
+ expect(queryPanel()).toBeNull();
+ });
+ });
+
+ describe('panel slot', () => {
+ describe('copyable', () => {
+ test('is overridden by the panel prop', () => {
+ const { queryPanel } = renderCode({
+ copyable: false,
+ panel: ,
+ });
+ expect(queryPanel()).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 && (
{expanded ? : }
Click to{' '}
@@ -287,7 +291,7 @@ function Code({
)}
-
+
);
}
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..bbc78e114c 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,14 @@ import Tooltip, {
RenderMode,
} from '@leafygreen-ui/tooltip';
+import { useCodeContext } from '../CodeContext/CodeContext';
+import { getLgIds } from '../utils';
+
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 +37,9 @@ function CopyButton({ onCopy, contents }: CopyProps) {
const timeoutRef = useRef(null);
const { theme } = useDarkMode();
const { portalContainer } = usePopoverPortalContainer();
+ const { showPanel, isLoading } = useCodeContext();
+
+ const lgids = getLgIds();
/**
* toggles `tooltipOpen` state
@@ -123,33 +125,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) => (
-
- {children}
-
-));
-
-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 (
{languageOptions?.map(option => (
diff --git a/packages/code/src/LanguageSwitcher/LanguageSwitcherExample.tsx b/packages/code/src/LanguageSwitcher/LanguageSwitcherExample.tsx
index 967544714e..165cb7bdf1 100644
--- a/packages/code/src/LanguageSwitcher/LanguageSwitcherExample.tsx
+++ b/packages/code/src/LanguageSwitcher/LanguageSwitcherExample.tsx
@@ -1,103 +1,17 @@
import React, { useState } from 'react';
-import { Language, LanguageOption } from '../types';
-import Code from '..';
+import { LanguageOption } from '../Panel/Panel.types';
+import { Language } from '../types';
+import Code, { Panel } from '..';
-export function PythonLogo({ className }: { className?: string }) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-function JavaScriptLogo({ className }: { className?: string }) {
- return (
-
-
-
-
- );
-}
-
-const languageOptions = [
+export const languageOptions = [
{
displayName: 'JavaScript',
language: Language.JavaScript,
- image: ,
},
{
displayName: 'Python',
language: Language.Python,
- image: ,
},
];
@@ -125,13 +39,12 @@ const snippetMap = {
[Language.Python]: pythonSnippet,
};
-function LanguageSwitcher({
- darkMode,
+export function LanguageSwitcherWithPanelExample({
onChange,
customActionButtons = [],
- showCustomActionButtons = false,
+ showCustomActionButtons = true,
+ ...rest
}: {
- darkMode?: boolean;
onChange?: Function;
customActionButtons?: Array;
showCustomActionButtons?: boolean;
@@ -147,17 +60,50 @@ function LanguageSwitcher({
return (
+ }
+ >
+ {snippetMap[languageIndex as 'javascript' | 'python']}
+
+ );
+}
+
+export function LanguageSwitcherWithDeprecatedPropsExample({
+ onChange,
+ customActionButtons = [],
+ ...rest
+}: {
+ onChange?: Function;
+ customActionButtons?: Array;
+}) {
+ const [language, setLanguage] = useState(languageOptions[0]);
+
+ const handleChange = (languageObject: LanguageOption) => {
+ setLanguage(languageObject);
+ onChange?.(languageObject);
+ };
+
+ const languageIndex = language.language;
+
+ return (
+
{snippetMap[languageIndex as 'javascript' | 'python']}
);
}
-
-export default LanguageSwitcher;
diff --git a/packages/code/src/Panel/Panel.styles.ts b/packages/code/src/Panel/Panel.styles.ts
index 8cbb9fd056..c6fc852d41 100644
--- a/packages/code/src/Panel/Panel.styles.ts
+++ b/packages/code/src/Panel/Panel.styles.ts
@@ -1,70 +1,69 @@
import { css, cx } from '@leafygreen-ui/emotion';
import { Theme } from '@leafygreen-ui/lib';
import { palette } from '@leafygreen-ui/palette';
-import { spacing } from '@leafygreen-ui/tokens';
+import { color, spacing } from '@leafygreen-ui/tokens';
-export const basePanelStyle = css`
- display: flex;
- align-items: center;
- flex-shrink: 0;
- gap: ${spacing[1]}px;
+export const getBasePanelStyle = ({
+ hasTitle,
+ theme,
+ className,
+}: {
+ hasTitle: boolean;
+ theme: Theme;
+ className?: string;
+}) =>
+ cx(
+ css`
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ flex-direction: row;
+ justify-content: space-between;
+ gap: ${spacing[100]}px;
- svg {
- width: 16px;
- height: 16px;
- }
-`;
+ z-index: 2; // Above the shadows
+ grid-area: panel;
+
+ border-bottom: 1px solid;
+ padding-inline: ${spacing[400]}px ${spacing[200]}px;
+ height: 36px;
+
+ svg {
+ width: 16px;
+ height: 16px;
+ }
+ `,
+ {
+ [css`
+ justify-content: flex-end;
+ `]: !hasTitle,
+ },
+ basePanelThemeStyle[theme],
+ className,
+ );
export const basePanelThemeStyle: Record = {
[Theme.Light]: css`
background-color: ${palette.white};
+ border-color: ${palette.gray.light2};
`,
[Theme.Dark]: css`
background-color: ${palette.gray.dark2};
+ border-color: ${palette.gray.dark1};
`,
};
-export const sidePanelStyle = css`
- flex-direction: column;
- padding: 6px;
- border-left: solid 1px;
+export const panelLeftStyles = css`
+ display: flex;
+ align-items: center;
+ gap: ${spacing[200]}px;
`;
-export const sidePanelThemeStyles: Record = {
- [Theme.Light]: cx(
- sidePanelStyle,
- css`
- border-color: ${palette.gray.light2};
- `,
- ),
- [Theme.Dark]: cx(
- sidePanelStyle,
- css`
- border-color: ${palette.gray.dark2};
- `,
- ),
-};
-
-export const languageSwitcherPanelStyle = css`
- flex-direction: row;
- border-bottom: 1px solid;
- justify-content: space-between;
- padding: 0;
- padding-right: 8px;
- height: 40px; // 28px (icon) + 2 x 6px (focus shadow). Can't use padding b/c switcher
+export const panelIconsStyles = css`
+ display: flex;
+ gap: ${spacing[100]}px;
`;
-export const languageSwitcherPanelThemeStyles: Record = {
- [Theme.Light]: cx(
- languageSwitcherPanelStyle,
- css`
- border-color: ${palette.gray.light2};
- `,
- ),
- [Theme.Dark]: cx(
- languageSwitcherPanelStyle,
- css`
- border-color: ${palette.gray.dark1};
- `,
- ),
-};
+export const getPanelTitleStyles = (theme: Theme) => css`
+ color: ${color[theme].text.secondary.default};
+`;
diff --git a/packages/code/src/Panel/Panel.tsx b/packages/code/src/Panel/Panel.tsx
index dd15e86557..aca9368477 100644
--- a/packages/code/src/Panel/Panel.tsx
+++ b/packages/code/src/Panel/Panel.tsx
@@ -1,79 +1,98 @@
import React from 'react';
+import ClipboardJS from 'clipboard';
import { cx } from '@leafygreen-ui/emotion';
import { useDarkMode } from '@leafygreen-ui/leafygreen-provider';
+import { isComponentType } from '@leafygreen-ui/lib';
+import { Body } from '@leafygreen-ui/typography';
+import { useCodeContext } from '../CodeContext/CodeContext';
import CopyButton from '../CopyButton/CopyButton';
import LanguageSwitcher from '../LanguageSwitcher/LanguageSwitcher';
-import {
- LanguageOption,
- LanguageSwitcher as LanguageSwitcherProps,
-} from '../types';
+import { getLgIds } from '../utils';
import {
- basePanelStyle,
- basePanelThemeStyle,
- languageSwitcherPanelThemeStyles,
- sidePanelThemeStyles,
+ getBasePanelStyle,
+ getPanelTitleStyles,
+ panelIconsStyles,
+ panelLeftStyles,
} from './Panel.styles';
-
-type PanelProps = Partial> & {
- onCopy?: Function;
- contents: string;
- showCopyButton?: boolean;
- language?: LanguageOption;
- isMultiline?: boolean;
- customActionButtons?: Array;
- showCustomActionButtons?: boolean;
- className?: string;
-};
+import { PanelProps } from './Panel.types';
function Panel({
- language,
languageOptions,
- contents,
onChange,
onCopy,
- showCopyButton,
- customActionButtons,
+ customActionButtons = [],
showCustomActionButtons,
+ title,
className,
+ ...rest
}: PanelProps) {
const { theme } = useDarkMode();
+ const { contents, language } = useCodeContext();
+
+ const lgids = getLgIds();
+
+ const hasTitle = !!title;
+
+ const filteredCustomActionIconButtons = customActionButtons.filter(
+ (item: React.ReactElement) => isComponentType(item, 'IconButton'),
+ );
+
+ const showCustomActionsInPanel =
+ showCustomActionButtons && !!filteredCustomActionIconButtons.length;
+
+ const currentLanguage = languageOptions?.find(
+ option => option.displayName === language,
+ );
+
+ const shouldRenderLanguageSwitcher =
+ language !== undefined &&
+ languageOptions !== undefined &&
+ languageOptions.length !== 0 &&
+ onChange !== undefined &&
+ !!currentLanguage;
return (
- {language !== undefined &&
- languageOptions !== undefined &&
- onChange !== undefined && (
+ {hasTitle && (
+
+ {title}
+
+ )}
+
+
+ {shouldRenderLanguageSwitcher && (
)}
- {showCopyButton && (
-
- )}
- {showCustomActionButtons && (
- <>{customActionButtons?.map((action: React.ReactNode) => action)}>
- )}
+
+ {showCustomActionsInPanel && (
+ <>
+ {filteredCustomActionIconButtons?.map(
+ (action: React.ReactNode) => action,
+ )}
+ >
+ )}
+ {ClipboardJS.isSupported() && (
+
+ )}
+
+
);
}
diff --git a/packages/code/src/Panel/Panel.types.ts b/packages/code/src/Panel/Panel.types.ts
new file mode 100644
index 0000000000..c5f20199d1
--- /dev/null
+++ b/packages/code/src/Panel/Panel.types.ts
@@ -0,0 +1,60 @@
+import { ComponentPropsWithRef } from 'react';
+
+import { Language } from '../types';
+
+export interface LanguageOption {
+ /**
+ * This display name for the language option
+ */
+ displayName: string;
+
+ /**
+ * The language enum value
+ */
+ language: Language;
+}
+
+export type PanelProps = Omit<
+ ComponentPropsWithRef<'div'>,
+ 'onChange' | 'onCopy'
+> & {
+ /**
+ * Callback fired when Code is copied via the copy button.
+ *
+ */
+ onCopy?: Function;
+
+ /**
+ * Custom action buttons. Should be an array of `IconButton`.
+ *
+ * @type []
+ */
+ customActionButtons?: Array;
+
+ /**
+ * When true, custom action buttons will be shown.
+ *
+ */
+ showCustomActionButtons?: boolean;
+
+ /**
+ * Renders a file name or other descriptor for a block of code
+ */
+ title?: string;
+} & (
+ | {
+ languageOptions?: undefined;
+ onChange?: undefined;
+ }
+ | {
+ /**
+ * An array of `LanguageOptions` to select from. Enables the Language switcher.
+ */
+ languageOptions: Array;
+
+ /**
+ * Callback fired when the language option changes.
+ */
+ onChange: (arg0: LanguageOption) => void;
+ }
+ );
diff --git a/packages/code/src/Syntax/Syntax.tsx b/packages/code/src/Syntax/Syntax.tsx
index 95d3378207..024d6d8ff4 100644
--- a/packages/code/src/Syntax/Syntax.tsx
+++ b/packages/code/src/Syntax/Syntax.tsx
@@ -17,7 +17,8 @@ import renderingPlugin, {
TableContent,
} from '../renderingPlugin/renderingPlugin';
import { SyntaxContext } from '../Syntax/SyntaxContext';
-import { Language, SyntaxProps } from '../types';
+import { Language } from '../types';
+import { SyntaxProps } from '..';
type FilteredSupportedLanguagesEnum = Omit<
typeof SupportedLanguages,
diff --git a/packages/code/src/Syntax/Syntax.types.ts b/packages/code/src/Syntax/Syntax.types.ts
new file mode 100644
index 0000000000..0771f8aeeb
--- /dev/null
+++ b/packages/code/src/Syntax/Syntax.types.ts
@@ -0,0 +1,35 @@
+import { ComponentPropsWithoutRef } from 'react';
+
+import { Language, LineHighlightingDefinition } from '../types';
+
+export interface SyntaxProps extends ComponentPropsWithoutRef<'code'> {
+ /**
+ * The children to render inside Code. This is usually going to be a formatted code block or line.
+ * @required
+ */
+ children: string;
+
+ /**
+ * The language to highlight the syntax of.
+ */
+ language: Language;
+
+ /**
+ * Shows line numbers. This is specifically used for the Code component implementation.
+ *
+ * default: `false`
+ */
+ showLineNumbers?: boolean;
+
+ /**
+ * Specifies the number by which to start line numbering.
+ *
+ * default: `1`
+ */
+ lineNumberStart?: number;
+
+ /**
+ * An 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]`);
+ */
+ highlightLines?: LineHighlightingDefinition;
+}
diff --git a/packages/code/src/WindowChrome/WindowChrome.spec.tsx b/packages/code/src/WindowChrome/WindowChrome.spec.tsx
deleted file mode 100644
index d369f953ff..0000000000
--- a/packages/code/src/WindowChrome/WindowChrome.spec.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-import { cleanup, render } from '@testing-library/react';
-
-import { typeIs } from '@leafygreen-ui/lib';
-
-import WindowChrome from './WindowChrome';
-
-afterAll(cleanup);
-
-const title = 'chrome/title.js';
-
-describe('packages/Syntax', () => {
- const { container } = render( );
-
- const windowChromeContainer = container.firstChild as HTMLElement;
-
- if (!windowChromeContainer || !typeIs.element(windowChromeContainer)) {
- throw new Error('Code element not found');
- }
-
- test(`renders ${title} within the simulated window chrome`, () => {
- expect(windowChromeContainer.textContent).toBe(title);
- });
-});
diff --git a/packages/code/src/WindowChrome/WindowChrome.styles.ts b/packages/code/src/WindowChrome/WindowChrome.styles.ts
deleted file mode 100644
index 47a7925f98..0000000000
--- a/packages/code/src/WindowChrome/WindowChrome.styles.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { css } from '@leafygreen-ui/emotion';
-import { Theme } from '@leafygreen-ui/lib';
-import { palette } from '@leafygreen-ui/palette';
-import { fontFamilies, typeScales } from '@leafygreen-ui/tokens';
-
-import { variantColors } from '../globalStyles';
-
-export const windowChromeHeight = 28;
-const controlSize = 12;
-const controlSpacing = 8;
-const borderRadius = 4;
-
-export const windowChromeStyle = css`
- display: flex;
- align-items: center;
- justify-content: center;
- height: ${windowChromeHeight}px;
- padding-left: ${controlSize}px;
- padding-right: ${controlSize}px;
- border-radius: ${borderRadius}px ${borderRadius}px 0 0;
- font-family: ${fontFamilies.default};
-`;
-
-export const windowChromeThemeStyles: Record = {
- [Theme.Light]: css`
- color: ${palette.gray.dark2};
- background-color: ${variantColors.light[1]};
- `,
- [Theme.Dark]: css`
- color: ${palette.gray.light1};
- background-color: ${variantColors.dark[1]};
- `,
-};
-
-export const textStyle = css`
- padding-left: ${controlSpacing}px;
- padding-right: ${controlSpacing}px;
- font-size: ${typeScales.body1.fontSize}px;
-`;
diff --git a/packages/code/src/WindowChrome/WindowChrome.tsx b/packages/code/src/WindowChrome/WindowChrome.tsx
deleted file mode 100644
index 599f6c515d..0000000000
--- a/packages/code/src/WindowChrome/WindowChrome.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from 'react';
-
-import { cx } from '@leafygreen-ui/emotion';
-import { useDarkMode } from '@leafygreen-ui/leafygreen-provider';
-
-import {
- textStyle,
- windowChromeStyle,
- windowChromeThemeStyles,
-} from './WindowChrome.styles';
-
-interface WindowChromeProps {
- chromeTitle?: string;
-}
-
-function WindowChrome({ chromeTitle = '' }: WindowChromeProps) {
- const { theme } = useDarkMode();
-
- return (
-
- );
-}
-
-WindowChrome.displayName = 'WindowChrome';
-
-export default WindowChrome;
diff --git a/packages/code/src/WindowChrome/index.ts b/packages/code/src/WindowChrome/index.ts
deleted file mode 100644
index d181cbc3f9..0000000000
--- a/packages/code/src/WindowChrome/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as WindowChrome } from './WindowChrome';
diff --git a/packages/code/src/index.ts b/packages/code/src/index.ts
index 6df8169e98..8600078856 100644
--- a/packages/code/src/index.ts
+++ b/packages/code/src/index.ts
@@ -1,13 +1,17 @@
-import { Code } from './Code';
-export { Code };
+import { Code, CopyButtonAppearance } from './Code';
+export { Code, CopyButtonAppearance };
+export type { CodeProps } from './Code/Code.types';
export { variantColors } from './globalStyles';
export { Panel } from './Panel';
-export type {
- CodeProps,
- LineHighlightingDefinition,
- SyntaxProps,
-} from './types';
-export type { LanguageOption } from './types';
-export { Language, Mode } from './types';
+export type { LanguageOption } from './Panel/Panel.types';
+export type { SyntaxProps } from './Syntax/Syntax.types';
+export type { LineHighlightingDefinition } from './types';
+export { Language } from './types';
+export {
+ getLgIds,
+ type GetLgIdsReturnType,
+ getTestUtils,
+ type TestUtilsReturnType,
+} from './utils';
export default Code;
diff --git a/packages/code/src/renderingPlugin/renderingPlugin.tsx b/packages/code/src/renderingPlugin/renderingPlugin.tsx
index c60ea2b3eb..89d6cfc42d 100644
--- a/packages/code/src/renderingPlugin/renderingPlugin.tsx
+++ b/packages/code/src/renderingPlugin/renderingPlugin.tsx
@@ -105,7 +105,7 @@ export function processToken(token: TreeItem, key?: number): React.ReactNode {
const cellStyle = css`
border-spacing: 0;
vertical-align: top;
- padding: 0 ${spacing[3]}px;
+ padding: 0 ${spacing[300]}px;
`;
function getHighlightedRowStyle(darkMode: boolean) {
@@ -178,7 +178,7 @@ export function LineTableRow({
css`
user-select: none;
text-align: right;
- padding-left: ${spacing[3] - 1}px;
+ padding-left: ${spacing[400]}px;
padding-right: 0;
color: ${highlighted ? highlightedNumberColor : numberColor};
`,
diff --git a/packages/code/src/types.ts b/packages/code/src/types.ts
index a56164b2c9..b7c619cb3f 100644
--- a/packages/code/src/types.ts
+++ b/packages/code/src/types.ts
@@ -1,14 +1,5 @@
-import { HTMLElementProps } from '@leafygreen-ui/lib';
-
import { SupportedLanguages } from './languages';
-export const Mode = {
- Light: 'light',
- Dark: 'dark',
-} as const;
-
-export type Mode = (typeof Mode)[keyof typeof Mode];
-
export const Language = {
...SupportedLanguages,
None: 'none',
@@ -19,127 +10,3 @@ export type Language = (typeof Language)[keyof typeof Language];
export type LineHighlightingDefinition = ReadonlyArray<
number | readonly [number, number]
>;
-
-export interface SyntaxProps extends HTMLElementProps<'code'> {
- /**
- * The children to render inside Code. This is usually going to be a formatted code block or line.
- * @required
- */
- children: string;
-
- /**
- * The language to highlight the syntax of.
- */
- language: Language;
-
- /**
- * Shows line numbers. This is specifically used for the Code component implementation.
- *
- * default: `false`
- */
- showLineNumbers?: boolean;
-
- /**
- * Specifies the number by which to start line numbering.
- *
- * default: `1`
- */
- lineNumberStart?: number;
-
- /**
- * An 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]`);
- */
- highlightLines?: LineHighlightingDefinition;
-}
-
-export type CodeProps = Omit<
- SyntaxProps,
- 'onCopy' | 'language' | 'onChange'
-> & {
- /**
- * Shows window chrome for code block;
- *
- * @default: `false`
- */
- showWindowChrome?: boolean;
-
- /**
- * Renders a file name or other descriptor for a block of code
- */
- chromeTitle?: string;
-
- /**
- * When true, allows the code block to be copied to the user's clipboard by clicking the rendered copy button.
- *
- * @default: `true`
- */
- copyable?: boolean;
-
- /**
- * 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;
-
- /**
- * Custom action buttons. Should be an array of `IconButton`.
- *
- * @type []
- */
- customActionButtons?: Array;
-
- /**
- * When true, custom action buttons will be shown.
- *
- */
- showCustomActionButtons?: boolean;
-
- /**
- * Determines whether or not the syntax will be rendered in dark mode.
- *
- * @default `false`
- */
- darkMode?: boolean;
-} & (
- | {
- /**
- * The language to format the code. See {@link https://github.com/mongodb/leafygreen-ui/blob/main/packages/code/src/languages.ts | SupportedLanguages}.
- */
- language: Language;
- languageOptions?: undefined;
- onChange?: undefined;
- }
- | {
- /**
- * The `displayName` of the selected `languageOption`
- */
- language: LanguageOption['displayName'];
- /**
- * An array of `LanguageOptions` to select from. Enables the Language switcher.
- */
- languageOptions: Array;
- /**
- * Callback fired when the language option changes.
- */
- onChange: (arg0: LanguageOption) => void;
- }
- );
-
-export interface LanguageOption {
- displayName: string;
- language: Language;
- image?: React.ReactElement;
-}
-
-export interface LanguageSwitcher {
- onChange: (arg0: LanguageOption) => void;
- language: LanguageOption['displayName'];
- languageOptions: Array;
-}
diff --git a/packages/code/src/utils/getLgIds.ts b/packages/code/src/utils/getLgIds.ts
new file mode 100644
index 0000000000..71d97a8286
--- /dev/null
+++ b/packages/code/src/utils/getLgIds.ts
@@ -0,0 +1,18 @@
+export const DEFAULT_LGID_ROOT = 'lg-code';
+
+export const getLgIds = (root: `lg-${string}` = DEFAULT_LGID_ROOT) => {
+ const ids = {
+ root,
+ panel: `${DEFAULT_LGID_ROOT}-panel`,
+ select: `${root}-select`,
+ copyButton: `${DEFAULT_LGID_ROOT}-copy_button`,
+ copyTooltip: `${DEFAULT_LGID_ROOT}-copy_tooltip`,
+ expandButton: `${root}-expand_button`,
+ skeleton: `${root}-skeleton`,
+ pre: `${root}-pre`,
+ title: `${DEFAULT_LGID_ROOT}-title`,
+ } as const;
+ return ids;
+};
+
+export type GetLgIdsReturnType = ReturnType;
diff --git a/packages/code/src/utils/getTestUtils/getTestUtils.spec.tsx b/packages/code/src/utils/getTestUtils/getTestUtils.spec.tsx
new file mode 100644
index 0000000000..21fcd8d472
--- /dev/null
+++ b/packages/code/src/utils/getTestUtils/getTestUtils.spec.tsx
@@ -0,0 +1,448 @@
+import userEvent from '@testing-library/user-event';
+import ClipboardJS from 'clipboard';
+
+import { Context, jest as Jest } from '@leafygreen-ui/testing-lib';
+
+import {
+ renderCode,
+ renderCodeWithLanguageSwitcher,
+ renderMultipleCodes,
+} from '../../Code.testutils';
+
+import { getTestUtils } from './getTestUtils';
+
+describe('packages/tabs/getTestUtils', () => {
+ test('throws error if Code is not found', () => {
+ try {
+ renderCode({ 'data-lgid': 'lg-different-id' });
+
+ const _ = getTestUtils();
+ } catch (error) {
+ expect(error).toBeInstanceOf(Error);
+ expect(error).toHaveProperty(
+ 'message',
+ expect.stringMatching(
+ /Unable to find an element by: \[data-lgid="lg-code"\]/,
+ ),
+ );
+ }
+ });
+
+ describe('single Code', () => {
+ describe('getLanguage', () => {
+ test('returns the language', () => {
+ const { getLanguage } = renderCode();
+ expect(getLanguage()).toBe('javascript');
+ });
+ });
+
+ describe('getTitle', () => {
+ describe('Without the panel', () => {
+ test('returns null', () => {
+ const { getTitle } = renderCode();
+ expect(getTitle()).toBeNull();
+ });
+ });
+ describe('With panel', () => {
+ test('returns null if there is no title prop', () => {
+ const { getTitle } = renderCodeWithLanguageSwitcher({});
+ expect(getTitle()).toBeNull();
+ });
+
+ test('returns the title', () => {
+ const { getTitle } = renderCodeWithLanguageSwitcher({
+ props: { title: 'Leafygreen' },
+ });
+ expect(getTitle()).toBe('Leafygreen');
+ });
+ });
+ });
+
+ describe('getLanguageSwitcherUtils', () => {
+ describe('getInput', () => {
+ test('returns the language switcher', () => {
+ const { getLanguageSwitcherUtils } = renderCodeWithLanguageSwitcher(
+ {},
+ );
+
+ expect(getLanguageSwitcherUtils().getInput()).toBeInTheDocument();
+ });
+
+ test('throws error is the languageSwitcher cannot be found', () => {
+ try {
+ const { getLanguageSwitcherUtils } = renderCode();
+ 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"\]/,
+ ),
+ );
+ }
+ });
+ });
+
+ describe('isDisabled', () => {
+ test('returns false', () => {
+ const { getLanguageSwitcherUtils } = renderCodeWithLanguageSwitcher(
+ {},
+ );
+ expect(getLanguageSwitcherUtils().isDisabled()).toBe(false);
+ });
+
+ test('returns true', () => {
+ const { getLanguageSwitcherUtils } = renderCodeWithLanguageSwitcher({
+ isLoading: true,
+ });
+ expect(getLanguageSwitcherUtils().isDisabled()).toBe(true);
+ });
+ });
+
+ describe('getOptions', () => {
+ test('returns all options', () => {
+ const { getLanguageSwitcherUtils } = renderCodeWithLanguageSwitcher(
+ {},
+ );
+ userEvent.click(getLanguageSwitcherUtils().getInput()!);
+ expect(getLanguageSwitcherUtils().getOptions()).toHaveLength(2);
+ });
+ });
+
+ describe('getOptionByValue', () => {
+ test('returns the option', () => {
+ const { getLanguageSwitcherUtils } = renderCodeWithLanguageSwitcher(
+ {},
+ );
+ userEvent.click(getLanguageSwitcherUtils().getInput()!);
+ expect(
+ getLanguageSwitcherUtils().getOptionByValue('JavaScript'),
+ ).toBeInTheDocument();
+ });
+
+ test('returns null', () => {
+ const { getLanguageSwitcherUtils } = renderCodeWithLanguageSwitcher(
+ {},
+ );
+ userEvent.click(getLanguageSwitcherUtils().getInput()!);
+ expect(
+ getLanguageSwitcherUtils().getOptionByValue('wrong'),
+ ).toBeNull();
+ });
+ });
+
+ describe('getInputValue', () => {
+ test('returns the value', () => {
+ const { getLanguageSwitcherUtils } = renderCodeWithLanguageSwitcher(
+ {},
+ );
+ userEvent.click(getLanguageSwitcherUtils().getInput()!);
+ expect(getLanguageSwitcherUtils().getInputValue()).toBe('JavaScript');
+ });
+ });
+ });
+
+ describe('getIsLoading', () => {
+ test('returns false', () => {
+ const { getIsLoading } = renderCode();
+ expect(getIsLoading()).toBe(false);
+ });
+
+ test('returns true', () => {
+ const { getIsLoading } = renderCode({ isLoading: true });
+ expect(getIsLoading()).toBe(true);
+ });
+ });
+
+ describe('getCopyButtonUtils', () => {
+ describe('getButton', () => {
+ describe('Without a panel', () => {
+ test('returns the copy button', () => {
+ const { getCopyButtonUtils } = Context.within(
+ Jest.spyContext(ClipboardJS, 'isSupported'),
+ spy => {
+ spy.mockReturnValue(true);
+ return renderCode();
+ },
+ );
+ expect(getCopyButtonUtils().getButton()).toBeInTheDocument();
+ });
+
+ test('throws error is the copy button cannot be found', () => {
+ try {
+ const { getCopyButtonUtils } = Context.within(
+ Jest.spyContext(ClipboardJS, 'isSupported'),
+ spy => {
+ spy.mockReturnValue(true);
+ return 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 a panel', () => {
+ test('returns the copy button', () => {
+ const { getCopyButtonUtils } = Context.within(
+ Jest.spyContext(ClipboardJS, 'isSupported'),
+ spy => {
+ spy.mockReturnValue(true);
+ return renderCodeWithLanguageSwitcher({});
+ },
+ );
+ expect(getCopyButtonUtils().getButton()).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('queryButton', () => {
+ describe('Without a panel', () => {
+ test('returns the copy button', () => {
+ const { getCopyButtonUtils } = Context.within(
+ Jest.spyContext(ClipboardJS, 'isSupported'),
+ spy => {
+ spy.mockReturnValue(true);
+ return renderCode();
+ },
+ );
+ expect(getCopyButtonUtils().queryButton()).toBeInTheDocument();
+ });
+
+ test('returns null when copyButtonAppearance is none', () => {
+ const { getCopyButtonUtils } = renderCode({
+ copyButtonAppearance: 'none',
+ });
+ expect(getCopyButtonUtils().queryButton()).toBeNull();
+ });
+ });
+
+ describe('With a panel', () => {
+ test('returns the copy button', () => {
+ const { getCopyButtonUtils } = Context.within(
+ Jest.spyContext(ClipboardJS, 'isSupported'),
+ spy => {
+ spy.mockReturnValue(true);
+ return renderCodeWithLanguageSwitcher({});
+ },
+ );
+ expect(getCopyButtonUtils().queryButton()).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('findButton', () => {
+ describe('Without a panel', () => {
+ test('returns the copy button', async () => {
+ const { getCopyButtonUtils } = Context.within(
+ Jest.spyContext(ClipboardJS, 'isSupported'),
+ spy => {
+ spy.mockReturnValue(true);
+ return renderCode();
+ },
+ );
+ const button = await getCopyButtonUtils().findButton();
+ expect(button).toBeInTheDocument();
+ });
+
+ test('throws error is the copy button cannot be found', async () => {
+ try {
+ const { getCopyButtonUtils } = Context.within(
+ Jest.spyContext(ClipboardJS, 'isSupported'),
+ spy => {
+ spy.mockReturnValue(true);
+ return renderCode({ copyButtonAppearance: 'none' });
+ },
+ );
+ const _ = await getCopyButtonUtils().findButton();
+ } 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 a panel', () => {
+ test('returns the copy button', async () => {
+ const { getCopyButtonUtils } = Context.within(
+ Jest.spyContext(ClipboardJS, 'isSupported'),
+ spy => {
+ spy.mockReturnValue(true);
+ return renderCodeWithLanguageSwitcher({});
+ },
+ );
+ const button = await getCopyButtonUtils().findButton();
+ expect(button).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('isDisabled', () => {
+ test('returns true', () => {
+ const { getCopyButtonUtils } = Context.within(
+ Jest.spyContext(ClipboardJS, 'isSupported'),
+ spy => {
+ spy.mockReturnValue(true);
+ return renderCodeWithLanguageSwitcher({ isLoading: true });
+ },
+ );
+ expect(getCopyButtonUtils().isDisabled()).toBe(true);
+ });
+
+ test('returns false', () => {
+ const { getCopyButtonUtils } = Context.within(
+ Jest.spyContext(ClipboardJS, 'isSupported'),
+ spy => {
+ spy.mockReturnValue(true);
+ return renderCodeWithLanguageSwitcher({});
+ },
+ );
+ expect(getCopyButtonUtils().isDisabled()).toBe(false);
+ });
+ });
+ });
+
+ describe('getExpandButtonUtils', () => {
+ describe('queryButton', () => {
+ test('returns null', () => {
+ const { getExpandButtonUtils } = renderCode();
+ expect(getExpandButtonUtils().queryButton()).toBeNull();
+ });
+
+ test('returns the expand button', () => {
+ const { getExpandButtonUtils } = renderCode({ expandable: true });
+ expect(getExpandButtonUtils().queryButton()).toBeInTheDocument();
+ });
+ });
+
+ describe('findButton', () => {
+ test('throws error is the expand button cannot be found', async () => {
+ try {
+ const { getExpandButtonUtils } = Context.within(
+ Jest.spyContext(ClipboardJS, 'isSupported'),
+ spy => {
+ spy.mockReturnValue(true);
+ return renderCode();
+ },
+ );
+ const _ = await getExpandButtonUtils().findButton();
+ } 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('returns the expand button', async () => {
+ const { getExpandButtonUtils } = renderCode({ expandable: true });
+ const button = await getExpandButtonUtils().findButton();
+ expect(button).toBeInTheDocument();
+ });
+ });
+
+ describe('getButton', () => {
+ test('throws error is the expand button cannot be found', () => {
+ try {
+ const { getExpandButtonUtils } = Context.within(
+ Jest.spyContext(ClipboardJS, 'isSupported'),
+ spy => {
+ spy.mockReturnValue(true);
+ return renderCode();
+ },
+ );
+ const _ = getExpandButtonUtils().getButton();
+ } 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('returns the expand button', () => {
+ const { getExpandButtonUtils } = renderCode({ expandable: true });
+ expect(getExpandButtonUtils().getButton()).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('isExpanded', () => {
+ test('returns true', () => {
+ const { getIsExpanded } = renderCode({ expandable: true });
+ // Code snippet is collapsed by default
+ expect(getIsExpanded()).toBe(false);
+ });
+
+ test('returns false', () => {
+ const { getExpandButtonUtils, getIsExpanded } = renderCode({
+ expandable: true,
+ });
+ const expandButton = getExpandButtonUtils().getButton();
+ userEvent.click(expandButton!);
+ expect(getIsExpanded()).toBe(true);
+ });
+ });
+
+ describe('queryPanel', () => {
+ test('returns the panel', () => {
+ const { queryPanel } = renderCodeWithLanguageSwitcher({});
+ expect(queryPanel()).toBeInTheDocument();
+ });
+
+ test('returns null', () => {
+ const { queryPanel } = renderCode({});
+ expect(queryPanel()).toBeNull();
+ });
+ });
+ });
+
+ describe('multiple Codes', () => {
+ test('returns the correct language', () => {
+ renderMultipleCodes();
+
+ const testUtils1 = getTestUtils('lg-code-1');
+ const testUtils2 = getTestUtils('lg-code-2');
+
+ expect(testUtils1.getLanguage()).toBe('JavaScript');
+ expect(testUtils2.getLanguage()).toBe('Python');
+ });
+
+ test('returns the corresponding language switcher', () => {
+ renderMultipleCodes();
+
+ const testUtils1 = getTestUtils('lg-code-1');
+ const testUtils2 = getTestUtils('lg-code-2');
+
+ const firstLanguageSwitcher = testUtils1
+ .getLanguageSwitcherUtils()
+ .getInput();
+ const secondLanguageSwitcher = testUtils2
+ .getLanguageSwitcherUtils()
+ .getInput();
+
+ expect(firstLanguageSwitcher.textContent).toBe('JavaScript');
+ expect(secondLanguageSwitcher.textContent).toBe('Python');
+ });
+ });
+});
diff --git a/packages/code/src/utils/getTestUtils/getTestUtils.ts b/packages/code/src/utils/getTestUtils/getTestUtils.ts
new file mode 100644
index 0000000000..6c1ff1c257
--- /dev/null
+++ b/packages/code/src/utils/getTestUtils/getTestUtils.ts
@@ -0,0 +1,91 @@
+import { getByLgId, queryBySelector } from '@lg-tools/test-harnesses';
+
+import { getTestUtils as getButtonTestUtils } from '@leafygreen-ui/button';
+import { getTestUtils as getSelectTestUtils } from '@leafygreen-ui/select';
+
+import { DEFAULT_LGID_ROOT, getLgIds } from '../getLgIds';
+
+import { TestUtilsReturnType } from './getTestUtils.types';
+
+export const getTestUtils = (
+ lgId: `lg-${string}` = DEFAULT_LGID_ROOT,
+): TestUtilsReturnType => {
+ const lgIds = getLgIds(lgId);
+
+ const element = getByLgId!(lgIds.root);
+
+ const getLanguage = () => {
+ const language = element.getAttribute('data-language');
+
+ if (!language) {
+ throw new Error('Unable to find language');
+ }
+
+ return language;
+ };
+
+ const getTitle = () => {
+ const title = queryBySelector(
+ element,
+ `[data-lgid=${lgIds.title}]`,
+ );
+
+ return title?.textContent || null;
+ };
+
+ const getLanguageSwitcherUtils = () => {
+ const testUtils = getSelectTestUtils(lgIds.select);
+
+ return {
+ getInput: () => testUtils.getInput(),
+ isDisabled: () => testUtils.isDisabled(),
+ getOptions: () => testUtils.getOptions(),
+ getOptionByValue: (value: string) => testUtils.getOptionByValue(value),
+ getInputValue: () => testUtils.getInputValue(),
+ };
+ };
+
+ const getIsLoading = () => {
+ return !!queryBySelector(
+ element,
+ `[data-lgid=${lgIds.skeleton}]`,
+ );
+ };
+
+ const getCopyButtonUtils = () =>
+ getButtonTestUtils(lgIds.copyButton);
+
+ const getExpandButtonUtils = () => {
+ const { queryButton, getButton, findButton } =
+ getButtonTestUtils(lgIds.expandButton);
+ return {
+ getButton,
+ queryButton,
+ findButton,
+ };
+ };
+
+ const getIsExpanded = () => {
+ const button = queryBySelector(
+ element,
+ `[data-lgid=${lgIds.expandButton}]`,
+ );
+
+ return !!button?.textContent?.includes('Click to collapse');
+ };
+
+ const queryPanel = () => {
+ return queryBySelector(element, `[data-lgid=${lgIds.panel}]`);
+ };
+
+ return {
+ getLanguage,
+ getLanguageSwitcherUtils,
+ getIsLoading,
+ getCopyButtonUtils,
+ getExpandButtonUtils,
+ getIsExpanded,
+ getTitle,
+ queryPanel,
+ };
+};
diff --git a/packages/code/src/utils/getTestUtils/getTestUtils.types.ts b/packages/code/src/utils/getTestUtils/getTestUtils.types.ts
new file mode 100644
index 0000000000..a9ce6c6a85
--- /dev/null
+++ b/packages/code/src/utils/getTestUtils/getTestUtils.types.ts
@@ -0,0 +1,56 @@
+import { GetTestUtilsReturnType as GetButtonTestUtilsReturnType } from '@leafygreen-ui/button';
+import { GetTestUtilsReturnType as GetSelectTestUtilsReturnType } from '@leafygreen-ui/select';
+
+export interface TestUtilsReturnType {
+ /**
+ * Returns the language of the code snippet
+ */
+ getLanguage: () => string;
+
+ /**
+ * Returns the title of the code snippet
+ */
+ getTitle: () => string | null;
+
+ /**
+ * Returns the language switcher
+ */
+ getLanguageSwitcherUtils: () => LanguageSwitcherUtils;
+
+ /**
+ * Returns whether the code snippet is loading
+ */
+ getIsLoading: () => boolean;
+
+ /**
+ * Returns utils for interacting with the copy button
+ */
+ getCopyButtonUtils: () => GetButtonTestUtilsReturnType;
+
+ /**
+ * Returns utils for interacting with the expand button
+ */
+ getExpandButtonUtils: () => Omit<
+ GetButtonTestUtilsReturnType,
+ 'isDisabled'
+ >;
+
+ /**
+ * Returns whether the code snippet is expanded
+ */
+ getIsExpanded: () => boolean;
+
+ /**
+ * Returns the panel
+ */
+ queryPanel: () => HTMLElement | null;
+}
+
+export type LanguageSwitcherUtils = Pick<
+ GetSelectTestUtilsReturnType,
+ | 'getInput'
+ | 'isDisabled'
+ | 'getOptions'
+ | 'getOptionByValue'
+ | 'getInputValue'
+>;
diff --git a/packages/code/src/utils/index.ts b/packages/code/src/utils/index.ts
new file mode 100644
index 0000000000..4e9cc8bd76
--- /dev/null
+++ b/packages/code/src/utils/index.ts
@@ -0,0 +1,3 @@
+export { getLgIds, type GetLgIdsReturnType } from './getLgIds';
+export { getTestUtils } from './getTestUtils/getTestUtils';
+export type { TestUtilsReturnType } from './getTestUtils/getTestUtils.types';
diff --git a/packages/code/tsconfig.json b/packages/code/tsconfig.json
index 78dee50b58..b266709304 100644
--- a/packages/code/tsconfig.json
+++ b/packages/code/tsconfig.json
@@ -1,20 +1,27 @@
{
"extends": "@lg-tools/build/config/package.tsconfig.json",
- "compilerOptions": {
+ "compilerOptions": {
"declarationDir": "dist",
"outDir": "dist",
"rootDir": "src",
"baseUrl": ".",
"paths": {
- "@leafygreen-ui/icon/dist/*": ["../icon/src/generated/*"],
- "@leafygreen-ui/*": ["../*/src"]
+ "@leafygreen-ui/icon/dist/*": [
+ "../icon/src/generated/*"
+ ],
+ "@leafygreen-ui/*": [
+ "../*/src"
+ ]
}
},
"include": [
"src/**/*",
"../../typings"
],
- "exclude": ["**/*.spec.*", "**/*.stories.*"],
+ "exclude": [
+ "**/*.spec.*",
+ "**/*.stories.*"
+ ],
"references": [
{
"path": "../a11y"
@@ -43,14 +50,20 @@
{
"path": "../select"
},
+ {
+ "path": "../skeleton-loader"
+ },
{
"path": "../tokens"
},
{
"path": "../tooltip"
},
+ {
+ "path": "../typography"
+ },
{
"path": "../leafygreen-provider"
}
]
-}
+}
\ No newline at end of file
diff --git a/packages/lib/src/getMobileMediaQuery/getMobileMediaQuery.ts b/packages/lib/src/getMobileMediaQuery/getMobileMediaQuery.ts
new file mode 100644
index 0000000000..2316900fd0
--- /dev/null
+++ b/packages/lib/src/getMobileMediaQuery/getMobileMediaQuery.ts
@@ -0,0 +1,11 @@
+const _baseQuery = (size: number) =>
+ `@media only screen and (max-width: ${size}px) and (hover: none)`;
+
+/** Any screen with no pointer, or a coarse pointer and no hover capability (i.e. touch screen)
+ * For more details, see: https://css-tricks.com/touch-devices-not-judged-size/
+ * @param size - The maximum width of the screen
+ */
+export const getMobileMediaQuery = (size: number) =>
+ `${_baseQuery(size)} and (pointer: coarse), ${_baseQuery(
+ size,
+ )} and (pointer: none)`;
diff --git a/packages/lib/src/getMobileMediaQuery/index.ts b/packages/lib/src/getMobileMediaQuery/index.ts
new file mode 100644
index 0000000000..cb5ae4cf4b
--- /dev/null
+++ b/packages/lib/src/getMobileMediaQuery/index.ts
@@ -0,0 +1 @@
+export { getMobileMediaQuery } from './getMobileMediaQuery';
diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts
index c1a6d3a9e2..777e4a25d9 100644
--- a/packages/lib/src/index.ts
+++ b/packages/lib/src/index.ts
@@ -5,6 +5,7 @@ import getTheme from './getTheme';
import { type LgIdProps } from './LgIdProps';
import * as typeIs from './typeIs';
export { createSyntheticEvent } from './createSyntheticEvent';
+export { getMobileMediaQuery } from './getMobileMediaQuery';
export * from './helpers';
export type {
Concat,
diff --git a/packages/select/src/index.ts b/packages/select/src/index.ts
index 94f9f4fa60..a9720e6d63 100644
--- a/packages/select/src/index.ts
+++ b/packages/select/src/index.ts
@@ -12,4 +12,4 @@ export {
Size,
State,
} from './Select';
-export { getTestUtils } from './utils';
+export { getTestUtils, type GetTestUtilsReturnType } from './utils';
diff --git a/packages/select/src/utils/getTestUtils/getTestUtils.ts b/packages/select/src/utils/getTestUtils/getTestUtils.ts
index 93c05b6177..9d0112259b 100644
--- a/packages/select/src/utils/getTestUtils/getTestUtils.ts
+++ b/packages/select/src/utils/getTestUtils/getTestUtils.ts
@@ -6,7 +6,7 @@ import { LGIDS_TYPOGRAPHY } from '@leafygreen-ui/typography';
import { LGIDS_SELECT } from '../../constants';
-import { TestUtilsReturnType } from './getTestUtils.types';
+import { GetTestUtilsReturnType } from './getTestUtils.types';
export function waitForSelectTransitionDuration() {
return new Promise(res => setTimeout(res, transitionDuration.slower));
@@ -14,7 +14,7 @@ export function waitForSelectTransitionDuration() {
export const getTestUtils = (
lgId: string = LGIDS_SELECT.root,
-): TestUtilsReturnType => {
+): GetTestUtilsReturnType => {
/**
* Queries the DOM for the element using the `data-lgid` data attribute.
* Will throw if no element is found.
diff --git a/packages/select/src/utils/getTestUtils/getTestUtils.types.ts b/packages/select/src/utils/getTestUtils/getTestUtils.types.ts
index 065ba9bb60..2ed2d28b5a 100644
--- a/packages/select/src/utils/getTestUtils/getTestUtils.types.ts
+++ b/packages/select/src/utils/getTestUtils/getTestUtils.types.ts
@@ -7,4 +7,4 @@ import {
export type SelectElements = FormElements & DropdownElements;
export type SelectUtils = Omit;
-export type TestUtilsReturnType = SelectElements & SelectUtils;
+export type GetTestUtilsReturnType = SelectElements & SelectUtils;
diff --git a/packages/select/src/utils/index.ts b/packages/select/src/utils/index.ts
index baf91cfc9f..4c878f3437 100644
--- a/packages/select/src/utils/index.ts
+++ b/packages/select/src/utils/index.ts
@@ -1 +1,2 @@
export { getTestUtils } from './getTestUtils/getTestUtils';
+export { type GetTestUtilsReturnType } from './getTestUtils/getTestUtils.types';
diff --git a/packages/select/src/utils/utils.tsx b/packages/select/src/utils/utils.tsx
index 20824f01e5..b115652b54 100644
--- a/packages/select/src/utils/utils.tsx
+++ b/packages/select/src/utils/utils.tsx
@@ -1,7 +1,11 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { isFragment } from 'react-is';
-import { consoleOnce, isComponentType } from '@leafygreen-ui/lib';
+import {
+ consoleOnce,
+ getMobileMediaQuery,
+ isComponentType,
+} from '@leafygreen-ui/lib';
import { breakpoints } from '@leafygreen-ui/tokens';
import {
@@ -12,10 +16,7 @@ import {
} from '../Option';
import { InternalOptionGroup, OptionGroupElement } from '../OptionGroup';
-// Any screen smaller than a tablet with no pointer, or a coarse pointer and no hover capability (i.e. touch screen)
-// For more details, see: https://css-tricks.com/touch-devices-not-judged-size/
-const _baseQuery = `@media only screen and (max-width: ${breakpoints.Tablet}px) and (hover: none)`;
-export const MobileMediaQuery = `${_baseQuery} and (pointer: coarse), ${_baseQuery} and (pointer: none)`;
+export const MobileMediaQuery = getMobileMediaQuery(breakpoints.Tablet);
function isReactEmpty(value: React.ReactNode): value is ReactEmpty {
return (
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bb44816109..bcaa4d8874 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -907,12 +907,21 @@ importers:
'@leafygreen-ui/select':
specifier: workspace:^
version: link:../select
+ '@leafygreen-ui/skeleton-loader':
+ specifier: workspace:^
+ version: link:../skeleton-loader
'@leafygreen-ui/tokens':
specifier: workspace:^
version: link:../tokens
'@leafygreen-ui/tooltip':
specifier: workspace:^
version: link:../tooltip
+ '@leafygreen-ui/typography':
+ specifier: workspace:^
+ version: link:../typography
+ '@lg-tools/test-harnesses':
+ specifier: workspace:^
+ version: link:../../tools/test-harnesses
'@types/facepaint':
specifier: ^1.2.1
version: 1.2.2