@financial-times/dotcom-ui-layout 7.3.1 → 7.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/dotcom-ui-layout",
3
- "version": "7.3.1",
3
+ "version": "7.3.2",
4
4
  "description": "",
5
5
  "main": "component.js",
6
6
  "browser": "browser.js",
@@ -37,7 +37,11 @@
37
37
  "npm": "7.x || 8.x"
38
38
  },
39
39
  "files": [
40
- "dist/"
40
+ "dist/",
41
+ "src/",
42
+ "browser.js",
43
+ "component.js",
44
+ "styles.scss"
41
45
  ],
42
46
  "repository": {
43
47
  "type": "git",
@@ -0,0 +1,38 @@
1
+ .demo {
2
+ text-align: center;
3
+ }
4
+
5
+ .demo__message {
6
+ display: inline-block;
7
+ margin: 50px 0;
8
+ padding: 1em;
9
+ border: 1px solid currentColor;
10
+ border-radius: 0.5em;
11
+
12
+ text-align: center;
13
+ font-size: 24px;
14
+
15
+ &:before,
16
+ &:after {
17
+ padding: 0 1em;
18
+ font-size: 75%;
19
+ }
20
+ }
21
+
22
+ .demo__message--scroll {
23
+ margin-bottom: 2000px;
24
+ border: none;
25
+
26
+ &:before,
27
+ &:after {
28
+ content: '▼';
29
+ }
30
+ }
31
+
32
+ .extra {
33
+ margin: 0;
34
+ padding: 0.5em;
35
+ text-align: center;
36
+ background: cornflowerblue;
37
+ color: white;
38
+ }
@@ -0,0 +1,110 @@
1
+ import React from 'react'
2
+ import { OnReady } from '../../../../.storybook/components/OnReady'
3
+
4
+ import './demos.scss'
5
+ import '../../styles.scss'
6
+
7
+ import * as layout from '../../browser'
8
+
9
+ import { navigationProps } from '../../../../__fixtures__/navigation'
10
+
11
+ import { Layout } from '..'
12
+
13
+ const Extra = ({ children }) => <p className="extra">{children}</p>
14
+
15
+ const initUiComponents = () => {
16
+ layout.init()
17
+ }
18
+
19
+ export default {
20
+ title: 'FT / Layout'
21
+ }
22
+
23
+ export const DefaultComponents = (args) => {
24
+ return (
25
+ <OnReady callback={initUiComponents}>
26
+ <Layout navigationData={navigationProps} {...args}>
27
+ <main className="demo">
28
+ <p className="demo__message">Defaults: only passing data</p>
29
+ </main>
30
+ </Layout>
31
+ </OnReady>
32
+ )
33
+ }
34
+
35
+ DefaultComponents.story = {
36
+ name: 'Default components'
37
+ }
38
+ DefaultComponents.args = {
39
+ headerVariant: 'simple',
40
+ footerVariant: 'simple'
41
+ }
42
+ DefaultComponents.argTypes = {
43
+ headerVariant: {
44
+ control: {
45
+ type: 'select',
46
+ options: {
47
+ Simple: 'simple',
48
+ 'Large Logo': 'large-logo',
49
+ 'Logo Only': 'logo-only',
50
+ None: ''
51
+ }
52
+ }
53
+ },
54
+ footerVariant: {
55
+ control: {
56
+ type: 'select',
57
+ options: {
58
+ Simple: 'simple',
59
+ Legal: 'legal',
60
+ None: ''
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ export const CustomSlots = () => {
67
+ return (
68
+ <OnReady callback={initUiComponents}>
69
+ <Layout
70
+ navigationData={navigationProps}
71
+ headerBefore={<Extra>Header before</Extra>}
72
+ headerAfter={<Extra>Header after</Extra>}
73
+ footerBefore={<Extra>Footer before</Extra>}
74
+ footerAfter={<Extra>Footer after</Extra>}>
75
+ <main className="demo">
76
+ <p className="demo__message">Custom content slots</p>
77
+ </main>
78
+ </Layout>
79
+ </OnReady>
80
+ )
81
+ }
82
+
83
+ CustomSlots.story = {
84
+ name: 'Custom slots'
85
+ }
86
+ CustomSlots.parameters = {
87
+ controls: { hideNoControlsWarning: true }
88
+ }
89
+
90
+ export const CustomComponents = () => {
91
+ return (
92
+ <OnReady callback={initUiComponents}>
93
+ <Layout
94
+ navigationData={navigationProps}
95
+ headerComponent={<Extra>Custom header</Extra>}
96
+ footerComponent={<Extra>Custom footer</Extra>}>
97
+ <main className="demo">
98
+ <p className="demo__message">Custom components</p>
99
+ </main>
100
+ </Layout>
101
+ </OnReady>
102
+ )
103
+ }
104
+
105
+ CustomComponents.story = {
106
+ name: 'Custom components'
107
+ }
108
+ CustomComponents.parameters = {
109
+ controls: { hideNoControlsWarning: true }
110
+ }
@@ -0,0 +1,125 @@
1
+ import React from 'react'
2
+ import {
3
+ Header as HeaderSimple,
4
+ Header as HeaderLarge,
5
+ LogoOnly,
6
+ NoOutboundLinksHeader,
7
+ Drawer,
8
+ THeaderOptions
9
+ } from '@financial-times/dotcom-ui-header/component'
10
+ import { TNavigationData } from '@financial-times/dotcom-types-navigation'
11
+ import { Footer, LegalFooter, TFooterOptions } from '@financial-times/dotcom-ui-footer/component'
12
+ import Template from './Template'
13
+
14
+ enum Headers {
15
+ simple = HeaderSimple,
16
+ 'large-logo' = HeaderLarge,
17
+ 'logo-only' = LogoOnly,
18
+ 'no-outbound-links' = NoOutboundLinksHeader
19
+ }
20
+
21
+ enum Footers {
22
+ simple = Footer,
23
+ legal = LegalFooter
24
+ }
25
+
26
+ export type TLayoutProps = {
27
+ navigationData?: TNavigationData
28
+ headerOptions?: THeaderOptions
29
+ headerBefore?: string | React.ReactNode
30
+ headerVariant?: Headers | false
31
+ headerComponent?: React.ReactNode
32
+ headerAfter?: string | React.ReactNode
33
+ footerOptions?: TFooterOptions
34
+ footerBefore?: string | React.ReactNode
35
+ footerVariant?: Footers | false
36
+ footerComponent?: React.ReactNode
37
+ footerAfter?: string | React.ReactNode
38
+ children?: React.ReactNode
39
+ contents?: string
40
+ }
41
+
42
+ export function Layout({
43
+ navigationData,
44
+ headerOptions,
45
+ headerBefore,
46
+ headerVariant,
47
+ headerComponent,
48
+ headerAfter,
49
+ footerOptions,
50
+ footerBefore,
51
+ footerVariant,
52
+ footerComponent,
53
+ footerAfter,
54
+ children,
55
+ contents
56
+ }: TLayoutProps) {
57
+ let header = null
58
+ let drawer = null
59
+
60
+ if (headerVariant && Headers[headerVariant] && !headerComponent) {
61
+ const Header = Headers[headerVariant]
62
+ header = <Header {...headerOptions} data={navigationData} variant={headerVariant} />
63
+
64
+ if (Header === HeaderSimple || Header === HeaderLarge) {
65
+ drawer = <Drawer {...headerOptions} data={navigationData} />
66
+ }
67
+ }
68
+
69
+ let footer = null
70
+
71
+ if (footerVariant && Footers[footerVariant] && !footerComponent) {
72
+ const Footer = Footers[footerVariant]
73
+ footer = <Footer {...footerOptions} data={navigationData} variant={footerVariant} />
74
+ }
75
+
76
+ return (
77
+ <div className="n-layout">
78
+ <a
79
+ data-trackable="a11y-skip-to-help"
80
+ className="n-layout__skip-link"
81
+ href="https://www.ft.com/accessibility">
82
+ Accessibility help
83
+ </a>
84
+
85
+ {drawer ? (
86
+ <a data-trackable="a11y-skip-to-navigation" className="n-layout__skip-link" href="#site-navigation">
87
+ Skip to navigation
88
+ </a>
89
+ ) : null}
90
+
91
+ <a data-trackable="a11y-skip-to-content" className="n-layout__skip-link" href="#site-content">
92
+ Skip to content
93
+ </a>
94
+
95
+ {footer ? (
96
+ <a data-trackable="a11y-skip-to-footer" className="n-layout__skip-link" href="#site-footer">
97
+ Skip to footer
98
+ </a>
99
+ ) : null}
100
+
101
+ <div className="n-layout__row n-layout__row--header">
102
+ <Template className="n-layout__header-before">{headerBefore}</Template>
103
+ {headerComponent || header || null}
104
+ <Template className="n-layout__header-after">{headerAfter}</Template>
105
+ </div>
106
+ <div className="n-layout__row n-layout__row--content">
107
+ <Template>{contents || children}</Template>
108
+ </div>
109
+ <div className="n-layout__row n-layout__row--footer">
110
+ <Template className="n-layout__footer-before">{footerBefore}</Template>
111
+ {footerComponent || footer || null}
112
+ <Template className="n-layout__footer-after">{footerAfter}</Template>
113
+ </div>
114
+
115
+ {drawer}
116
+ </div>
117
+ )
118
+ }
119
+
120
+ Layout.defaultProps = {
121
+ headerVariant: 'simple',
122
+ footerVariant: 'simple',
123
+ headerOptions: {},
124
+ footerOptions: {}
125
+ }
@@ -0,0 +1,18 @@
1
+ import React from 'react'
2
+
3
+ type TTemplateProps = {
4
+ children?: string | React.ReactNode
5
+ [rest: string]: any
6
+ }
7
+
8
+ export default function Template(props: TTemplateProps) {
9
+ const { children, ...rest } = props
10
+
11
+ if (!children) return null
12
+
13
+ if (typeof children === 'string') {
14
+ return <div {...rest} dangerouslySetInnerHTML={{ __html: children }} />
15
+ } else {
16
+ return <div {...rest}>{children}</div>
17
+ }
18
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import 'jest-enzyme'
5
+ import React from 'react'
6
+ import { shallow } from 'enzyme'
7
+
8
+ import { Layout as Subject } from '../Layout'
9
+ import { Header, Drawer, LogoOnly, NoOutboundLinksHeader } from '@financial-times/dotcom-ui-header'
10
+ import { Footer, LegalFooter } from '@financial-times/dotcom-ui-footer'
11
+
12
+ describe('dotcom-ui-layout/src/components/Layout', () => {
13
+ const result = shallow(<Subject />)
14
+
15
+ it('renders skip links to page landmarks', () => {
16
+ expect(result.find('a[href="#site-navigation"]')).toHaveText('Skip to navigation')
17
+ expect(result.find('a[href="#site-content"]')).toHaveText('Skip to content')
18
+ expect(result.find('a[href="#site-footer"]')).toHaveText('Skip to footer')
19
+ })
20
+
21
+ it('renders the header and drawer components', () => {
22
+ expect(result.find(Header)).toExist()
23
+ expect(result.find(Drawer)).toExist()
24
+ })
25
+
26
+ it('renders the footer component', () => {
27
+ expect(result.find(Footer)).toExist()
28
+ })
29
+
30
+ describe('header variations', () => {
31
+ describe('with the simple variant', () => {
32
+ let result
33
+
34
+ beforeAll(() => {
35
+ result = shallow(<Subject headerVariant={'simple' as any} />)
36
+ })
37
+
38
+ it('renders the header component', () => {
39
+ expect(result.find(Header)).toExist()
40
+ })
41
+
42
+ it('renders the drawer component', () => {
43
+ expect(result.find(Drawer)).toExist()
44
+ })
45
+
46
+ it('renders the navigation skip link', () => {
47
+ expect(result.find('a[href="#site-navigation"]')).toExist()
48
+ })
49
+ })
50
+
51
+ describe('with the large-logo variant', () => {
52
+ let result
53
+
54
+ beforeAll(() => {
55
+ result = shallow(<Subject headerVariant={'large-logo' as any} />)
56
+ })
57
+
58
+ it('renders the header component', () => {
59
+ expect(result.find(Header)).toExist()
60
+ })
61
+
62
+ it('renders the drawer component', () => {
63
+ expect(result.find(Drawer)).toExist()
64
+ })
65
+
66
+ it('renders the navigation skip link', () => {
67
+ expect(result.find('a[href="#site-navigation"]')).toExist()
68
+ })
69
+ })
70
+
71
+ describe('with the logo-only variant', () => {
72
+ let result
73
+
74
+ beforeAll(() => {
75
+ result = shallow(<Subject headerVariant={'logo-only' as any} />)
76
+ })
77
+
78
+ it('renders the logo only header component', () => {
79
+ expect(result.find(LogoOnly)).toExist()
80
+ })
81
+
82
+ it('does not render the drawer component', () => {
83
+ expect(result.find(Drawer)).not.toExist()
84
+ })
85
+
86
+ it('does not render the navigation skip link', () => {
87
+ expect(result.find('a[href="#site-navigation"]')).not.toExist()
88
+ })
89
+ })
90
+
91
+ describe('with the no-outbound-links variant', () => {
92
+ let result
93
+
94
+ beforeAll(() => {
95
+ result = shallow(<Subject headerVariant={'no-outbound-links' as any} />)
96
+ })
97
+
98
+ it('renders the no-outbound-links header component', () => {
99
+ expect(result.find(NoOutboundLinksHeader)).toExist()
100
+ })
101
+
102
+ it('does not render the drawer component', () => {
103
+ expect(result.find(Drawer)).not.toExist()
104
+ })
105
+
106
+ it('does not render the navigation skip link', () => {
107
+ expect(result.find('a[href="#site-navigation"]')).not.toExist()
108
+ })
109
+ })
110
+
111
+ describe('with the header disabled', () => {
112
+ let result
113
+
114
+ beforeAll(() => {
115
+ result = shallow(<Subject headerVariant={false} />)
116
+ })
117
+
118
+ it('does not render the header component', () => {
119
+ expect(result.find(Header)).not.toExist()
120
+ })
121
+
122
+ it('does not render the drawer component', () => {
123
+ expect(result.find(Drawer)).not.toExist()
124
+ })
125
+
126
+ it('does not render the navigation skip link', () => {
127
+ expect(result.find('a[href="#site-navigation"]')).not.toExist()
128
+ })
129
+ })
130
+
131
+ describe('with a custom header component', () => {
132
+ let result
133
+
134
+ beforeAll(() => {
135
+ const customHeader = <header>My custom header</header>
136
+ result = shallow(<Subject headerComponent={customHeader} />)
137
+ })
138
+
139
+ it('renders the given component', () => {
140
+ expect(result.find('header')).toHaveText('My custom header')
141
+ })
142
+
143
+ it('does not render the header component', () => {
144
+ expect(result.find(Header)).not.toExist()
145
+ })
146
+
147
+ it('does not render the drawer component', () => {
148
+ expect(result.find(Drawer)).not.toExist()
149
+ })
150
+
151
+ it('does not render the navigation skip link', () => {
152
+ expect(result.find('a[href="#site-navigation"]')).not.toExist()
153
+ })
154
+ })
155
+ })
156
+
157
+ describe('footer variations', () => {
158
+ describe('with the simple variant', () => {
159
+ let result
160
+
161
+ beforeAll(() => {
162
+ result = shallow(<Subject footerVariant={'simple' as any} />)
163
+ })
164
+
165
+ it('renders the footer component', () => {
166
+ expect(result.find(Footer)).toExist()
167
+ })
168
+
169
+ it('renders the footer skip link', () => {
170
+ expect(result.find('a[href="#site-footer"]')).toExist()
171
+ })
172
+ })
173
+
174
+ describe('with the legal variant', () => {
175
+ let result
176
+
177
+ beforeAll(() => {
178
+ result = shallow(<Subject footerVariant={'legal' as any} />)
179
+ })
180
+
181
+ it('renders the legal footer component', () => {
182
+ expect(result.find(LegalFooter)).toExist()
183
+ })
184
+
185
+ it('renders the footer skip link', () => {
186
+ expect(result.find('a[href="#site-footer"]')).toExist()
187
+ })
188
+ })
189
+
190
+ describe('with the footer disabled', () => {
191
+ let result
192
+
193
+ beforeAll(() => {
194
+ result = shallow(<Subject footerVariant={false} />)
195
+ })
196
+
197
+ it('does not render the footer component', () => {
198
+ expect(result.find(Footer)).not.toExist()
199
+ })
200
+
201
+ it('does not render the footer skip link', () => {
202
+ expect(result.find('a[href="#site-footer"]')).not.toExist()
203
+ })
204
+ })
205
+
206
+ describe('with a custom footer component', () => {
207
+ let result
208
+
209
+ beforeAll(() => {
210
+ const customFooter = <footer>My custom footer</footer>
211
+ result = shallow(<Subject footerComponent={customFooter} />)
212
+ })
213
+
214
+ it('renders the given component', () => {
215
+ expect(result.find('footer')).toHaveText('My custom footer')
216
+ })
217
+
218
+ it('does not render the footer component', () => {
219
+ expect(result.find(Footer)).not.toExist()
220
+ })
221
+
222
+ it('does not render the footer skip link', () => {
223
+ expect(result.find('a[href="#site-footer"]')).not.toExist()
224
+ })
225
+ })
226
+ })
227
+ })
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import 'jest-enzyme'
5
+ import React from 'react'
6
+ import { mount } from 'enzyme'
7
+
8
+ import Subject from '../Template'
9
+
10
+ describe('dotcom-ui-layout/src/components/Template', () => {
11
+ it('can render contents provided as a string', () => {
12
+ const result = mount(<Subject>{'<p>Hello World</p>'}</Subject>)
13
+ expect(result.html()).toBe('<div><p>Hello World</p></div>')
14
+ })
15
+
16
+ it('can render contents provided as children', () => {
17
+ const result = mount(
18
+ <Subject>
19
+ <p>Hello World</p>
20
+ </Subject>
21
+ )
22
+ expect(result.html()).toBe('<div><p>Hello World</p></div>')
23
+ })
24
+ })
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './components/Layout'
package/styles.scss ADDED
@@ -0,0 +1,50 @@
1
+ // This is needed by Origami components that speak to the image service. Setting
2
+ // it here means it'll be the same no matter which app generates it and it will
3
+ // be reused from the CDN
4
+ $system-code: 'page-kit-layout' !default;
5
+
6
+ @import '@financial-times/dotcom-ui-base-styles/styles';
7
+
8
+ .n-layout {
9
+ display: table;
10
+ width: 100%;
11
+ height: 100vh;
12
+ table-layout: fixed;
13
+ }
14
+
15
+ .n-layout__row {
16
+ display: table-row;
17
+ }
18
+
19
+ .n-layout__row--content {
20
+ height: 100%;
21
+ }
22
+
23
+ .n-layout__header-before {
24
+ background-color: oColorsByName('black-20')
25
+ }
26
+
27
+ .n-layout__skip-link {
28
+ @include nUiZIndexFor('notification');
29
+ position: absolute;
30
+ top: 1em;
31
+ left: -9999px;
32
+ padding: 0.5em 1em;
33
+ background: oColorsByName('velvet');
34
+ color: oColorsByName('white');
35
+
36
+ &:focus {
37
+ left: 0;
38
+ color: oColorsByName('white');
39
+ }
40
+ }
41
+
42
+ @media print {
43
+ .n-layout,
44
+ .n-layout__row {
45
+ display: block;
46
+ }
47
+ }
48
+
49
+ @import '@financial-times/dotcom-ui-header/styles';
50
+ @import '@financial-times/dotcom-ui-footer/styles';